PyTorch in the wild

PyTorch ships one small set of primitives, modules, an autograd tape, optimizers, a data loader, and every serious project bends them into a different shape. nanoGPT keeps an entire GPT training run in one readable file. torchtitan composes parallelism out of sharded tensors. Unsloth rewrites backward passes by hand. vLLM keeps the model and throws away the loop. diffusers splits everything into swappable parts. This page reads those codebases side by side, as a field guide to PyTorch idioms in production, what each one buys, and what each one costs.

The idioms everyone shares

Before the styles diverge, every PyTorch codebase stands on the same five or six load-bearing conventions, and it pays to name them precisely, because the interesting projects are the ones that deliberately break one of them.

Module composition. nn.Module is a tree, not a graph. Assigning a submodule or an nn.Parameter to an attribute registers it, and that registration is what makes .parameters(), .state_dict(), .to(device), and every recursive traversal work. The forward pass is ordinary Python, recorded as it runs, so control flow is free and the model you debug is the model you wrote. Calling a module goes through __call__ rather than forward directly, and that indirection is where hooks attach, which matters later. The one rule people trip on is that only registered things move and save. A raw tensor stashed on self is invisible to state_dict and stays on the wrong device, and the fix is register_buffer, which is why masks and running statistics live in buffers across every codebase in this survey.

The training-loop contract. The canonical loop is four calls in a fixed order, zero the gradients, run forward to a scalar loss, call loss.backward(), call optimizer.step(). Everything else in the ecosystem is built against that contract. Gradient accumulation works because backward adds into .grad rather than overwriting it, which is also the reason zero_grad must exist at all. Gradient clipping slots between backward and step because that is the only moment when the full gradient exists and has not been consumed. Mixed precision wraps the same four calls with a scaler. Schedulers step alongside the optimizer, and the scheduler adjusts the learning rate stored inside the optimizer's param groups rather than owning anything itself. The optimizer, in turn, holds live references to the parameters, not copies, which is why you must construct it after moving the model to its device if any transformation replaces parameter tensors.

Autograd's tape. Every operation on a tensor that requires grad appends a node to a dynamically built graph, and backward walks that graph in reverse, freeing it as it goes by default. The tape is per-iteration and rebuilt from scratch each forward, which is what makes PyTorch feel like Python rather than a compiler, and it is also the thing every performance-oriented project in this survey works around. The memory cost of training is dominated not by parameters but by the activations the tape saves for the backward pass, and that single fact explains activation checkpointing, FlashAttention-style recomputation, and most of what Unsloth does below.

Device movement. module.to(device) moves in place and returns the same module, tensor.to(device) returns a new tensor, and mixing up the two is a rite of passage. Data transfer overlaps with compute only when the host memory is pinned and the copy passes non_blocking=True, a pairing you will find in the input pipeline of essentially every training repo that cares about throughput.

The escape hatches. When the standard ops are not enough, PyTorch offers three well-marked exits. torch.autograd.Function lets you define forward and backward by hand, with ctx.save_for_backward controlling exactly what the tape keeps, and it is the seam through which every hand-written kernel enters training. Custom operators, registered through torch.library or bound from C++ and CUDA through the cpp_extension machinery, make foreign kernels look like native ops so the dispatcher and the compiler can see them. And hooks, forward hooks, backward hooks, and per-tensor gradient hooks, let you observe or edit values without touching the model's code, which is how feature extraction, activation statistics, and most interpretability tooling attach to models they do not own. Each project below is, in a real sense, a choice of which escape hatch to live in. My walkthrough of the PyTorch codebase itself, dispatcher and autograd internals included, is at /oss/pytorch.

The clean reference loop, nanoGPT

karpathy/nanoGPT is the null hypothesis of this survey, the project that uses PyTorch exactly as the tutorials describe and proves how far that gets you. The entire repository that matters is two files. model.py defines GPT-2 in about three hundred lines of plain nn.Module composition, and train.py is the whole training run, data loading, mixed precision, distributed data parallel, gradient accumulation, evaluation, checkpointing, and logging, in about three hundred more. There is no trainer class, no callback system, no configuration framework. Config is a file of module-level variables that a small helper overrides by executing an optional config file and command-line assignments over the globals, which sounds lawless and reads perfectly, because every knob is a named variable you can search for.

The idioms inside are worth cataloguing because they are the standard ones executed cleanly. Batches come from a numpy memmap of pre-tokenized data, sliced at random offsets, pinned, and copied to the GPU with non_blocking=True, which means the data pipeline is about ten lines and never becomes the bottleneck at this scale. Evaluation is a function under torch.no_grad that flips the model to eval and back. Mixed precision is one autocast context plus a GradScaler that is a no-op in bfloat16. DDP is a thin wrapper applied only when the script detects a distributed launch, and gradient accumulation cooperates with it by syncing gradients only on the final micro-step. Weight decay is applied to matrices but not to biases and norms, done once at optimizer construction by partitioning parameters into two groups, an idiom that reappears in nearly every LLM codebase.

Then there is the line that dates the repo to the torch.compile era, model = torch.compile(model), guarded by a single flag. On an A100 it is worth tens of percent of throughput and it costs one line and some startup latency, no rewrite, no annotation. The reason it works so well here is the same reason the whole file works, the model is static, the shapes are fixed, the loop is regular, so the compiler sees one graph and optimizes it once. The general lesson is that torch.compile pays best exactly where code is boring.

What nanoGPT teaches about abstraction is mostly negative space. Nothing in the file is reusable, and that is the point. Because the loop is flat, every intervention a researcher actually wants, a new logging line, a different schedule, a surgery on the model between steps, is a direct edit at the obvious place, with no framework to negotiate with. The file fits in your head, so the cost of change stays proportional to the size of the change. The repo's limits are equally instructive, one node, one model family, no elastic restarts, no tensor parallelism, and the moment those requirements appear, this style stops scaling and the next section's style begins. My annotated walkthrough of the codebase is at /oss/nanogpt.

Distributed as a first-class design, torchtitan

pytorch/torchtitan is the official demonstration of how PyTorch wants large-scale training written now, and its organizing idea is that parallelism should be composition, not architecture. The model definition is a plain single-device nn.Module, deliberately free of any distributed code. Parallelism is then applied to it from the outside, as a sequence of transformations over a DeviceMesh, an n-dimensional array of ranks with named axes for data, tensor, and pipeline parallelism. Tensor parallelism rewrites attention and MLP projections into column- and row-sharded pairs through a parallelization plan. Pipeline parallelism splits the stack of blocks into stages. Data parallelism arrives last as FSDP2's fully_shard, applied per transformer block and then once over the root.

The substrate that makes the composition legal is DTensor. A DTensor is a tensor plus a mesh plus a placement per mesh axis, sharded along some dimension or replicated, and the arithmetic of placements is what lets a parameter be simultaneously sharded by FSDP along data-parallel ranks and by tensor parallelism along model ranks without either wrapper knowing about the other. FSDP2 in particular is a rewrite of the original flat-parameter FSDP into this idiom, each parameter individually becomes a DTensor sharded on dim zero, gathered just in time for compute and freed after, which removes the old design's awkward flattening and makes per-parameter dtypes, frozen subsets, and partial resharding straightforward.

Two supporting idioms complete the design. The first is meta device initialization. A 405B-parameter model cannot be materialized on one host, so torchtitan constructs it under torch.device("meta"), where tensors have shapes and dtypes but no storage, applies all the sharding decisions to the skeleton, and only then allocates real memory for each rank's shard and runs the model's own weight init on the materialized pieces. Construction cost stops depending on model size entirely. The second is distributed checkpointing through torch.distributed.checkpoint, where every rank writes its own shards and, crucially, a checkpoint saved on one world size and parallelism layout can be loaded into another, because DTensor metadata records what each shard is rather than which rank wrote it. Saving can proceed asynchronously while training continues, which at cluster scale turns checkpointing from a stop-the-world event into a background task.

A minimal sketch of the FSDP2 idiom, meta init included, looks like this. In current releases fully_shard lives in torch.distributed.fsdp, and in slightly older ones it lived in a private composable namespace, so check your version's import path.

import torch
from torch.distributed.device_mesh import init_device_mesh
from torch.distributed.fsdp import fully_shard, MixedPrecisionPolicy

mesh = init_device_mesh("cuda", (world_size,))

with torch.device("meta"):
    model = Transformer(cfg)          # shapes only, no storage yet

mp = MixedPrecisionPolicy(param_dtype=torch.bfloat16,
                          reduce_dtype=torch.float32)
for block in model.layers:
    fully_shard(block, mesh=mesh, mp_policy=mp)   # one group per block
fully_shard(model, mesh=mesh, mp_policy=mp)       # root handles the rest

model.to_empty(device="cuda")         # allocate only this rank's shards
model.init_weights()                  # your own init, now on real memory

The per-block fully_shard calls are not boilerplate, they define the communication schedule, each block's parameters are gathered as a unit just before that block runs, so compute for one block overlaps with the gather for the next. The style's cost is conceptual, you now debug placements and mesh axes instead of tensors, and a shape error can be a sharding error wearing a disguise. I work through the full torchtitan training step, collectives and all, at /oss/torchtitan, and the underlying parallelism theory, including why tensor parallel wants the fast intra-node links, in my parallel computing notes.

Rewriting the hot path, Unsloth

unslothai/unsloth occupies the opposite corner from torchtitan. It targets one situation, fine-tuning open-weight LLMs on one or few GPUs where memory is the binding constraint, and its method is to take models loaded through Hugging Face transformers and monkey-patch their hot paths, swapping stock module forwards for hand-written Triton kernels with hand-written backward passes. RMSNorm, RoPE, the SwiGLU MLP, the cross-entropy over a vocabulary of over a hundred thousand entries, and the LoRA matmuls all get this treatment. The user-visible API is deliberately boring, load the model through Unsloth's wrapper and train with any standard trainer, everything interesting happens in what got replaced underneath.

The enabling idiom is the custom autograd.Function wrapped around a fused kernel. Autograd's default is to save whatever intermediates the chain rule of the recorded ops needs, and a chain of small ops saves a lot. A fused kernel replaces the chain with one op, and the hand-written backward gets to choose the minimal set of residuals, often recomputing cheap values instead of storing them, and to exploit mathematical structure the op-by-op chain rule cannot see. The cross-entropy kernel is the clean example, the naive path materializes a full logits-sized softmax for the backward, while the fused path needs only the logits it already has plus one scalar per row, which at LLM vocabulary sizes is gigabytes of difference per batch. The pattern in miniature, with the eager fallback every such wrapper should keep, looks like this. The two fused_* calls stand in for your own Triton launches.

import torch

def rms_norm_ref(x, weight, eps=1e-6):
    # Eager reference. Autograd derives the backward on its own,
    # saving intermediates op by op.
    rstd = torch.rsqrt(x.float().pow(2).mean(-1, keepdim=True) + eps)
    return (x.float() * rstd).to(x.dtype) * weight

class FusedRMSNorm(torch.autograd.Function):
    @staticmethod
    def forward(ctx, x, weight, eps):
        y, rstd = fused_rmsnorm_fwd(x, weight, eps)  # one kernel launch
        ctx.save_for_backward(x, weight, rstd)       # residuals we chose
        ctx.eps = eps
        return y

    @staticmethod
    def backward(ctx, grad_out):
        x, weight, rstd = ctx.saved_tensors
        dx, dw = fused_rmsnorm_bwd(grad_out, x, weight, rstd)
        return dx, dw, None                          # one grad per input

def rms_norm(x, weight, eps=1e-6):
    if x.is_cuda and fused_kernels_available():
        return FusedRMSNorm.apply(x, weight, eps)
    return rms_norm_ref(x, weight, eps)              # correctness anchor

The reference implementation is not decoration. It is the oracle the kernel is tested against with torch.autograd.gradcheck in float64, and the fallback that keeps CPU tests and unsupported devices working. Every serious kernel-writing project keeps this pair.

The costs of the patching style deserve equal airtime. Unsloth couples itself to the private internals of transformers, so upstream refactors can break it in ways its own test suite only catches after the fact, and supporting each new model family means writing and validating new patches rather than inheriting support for free. Patched code is also invisible at the call site, the model prints like a stock Llama while executing something else, which is excellent for adoption and hostile to debugging. And a hand-written backward is a standing correctness liability, silently wrong gradients train plausibly and fail subtly. The trade is real, though, the memory and speed wins are large and measured, and for the single-GPU fine-tuning population that is the difference between a run fitting or not. My notes on the codebase and its kernel inventory are at /oss/unsloth.

Inference as systems engineering, vLLM

vllm-project/vllm demonstrates a third relationship with PyTorch, keep the tensor library, discard the execution model. Training frameworks iterate over a dataset. A serving engine iterates over a changing population of requests, each at a different position in its own sequence, arriving and finishing continuously, and none of PyTorch's loop machinery addresses that. So vLLM's core is a scheduler, written in plain Python, that at every step decides which requests run, admits new ones mid-flight, and preempts old ones under memory pressure, an idiom called continuous batching. PyTorch appears one level down, the models are ordinary nn.Module implementations, maintained in-repo per architecture, loading the same weights as their transformers counterparts but rewritten for inference, fused projections, no dropout, tensor-parallel sharding built in.

The famous contribution is memory management. A KV cache allocated contiguously per request must reserve worst-case length, and fragmentation between requests wastes most of the GPU. PagedAttention treats the cache like virtual memory, fixed-size blocks of a few tokens each, a block table per request mapping logical to physical blocks, and an attention kernel that follows the indirection. Waste drops to under a block per sequence, and identical prefixes, the shared system prompt case, can point at the same physical blocks with copy-on-write. None of this is expressible in stock PyTorch tensors, which is exactly why the kernel is custom.

The second systems move attacks launch overhead. A decode step does a small amount of math per request, and a model that is a few hundred ops of a few microseconds each spends comparable time in CUDA launch overhead as in compute. vLLM captures the decode pass into CUDA graphs at a set of padded batch sizes, so a whole step replays as one graph launch with new inputs written into fixed buffers. The constraint CUDA graphs impose, fixed shapes and fixed control flow, is the reason the capture happens at bucketed sizes and the reason the model code must stay free of data-dependent branching, a discipline you can see enforced throughout the model implementations. The newer engine leans on torch.compile for the same class of wins with less hand management, compiling the model into fused pieces while custom ops stay opaque to the compiler.

The custom kernels themselves, paged attention, fused activation and norm kernels, quantization paths, are C++ and CUDA compiled as an extension and registered as proper torch ops rather than called as raw Python bindings. Registration is what keeps them composable, the dispatcher can route to them, profilers see them by name, and torch.compile can treat them as scheduling units instead of graph breaks. The overall lesson generalizes beyond serving, PyTorch does not mind being demoted to a kernel library under someone else's runtime, and it is good at it. My walkthrough of the engine, scheduler to kernels, is at /oss/vllm.

The library of parts, diffusers

huggingface/diffusers is the clearest study in API design of the group, because diffusion systems have a natural seam that the library commits to completely. A diffusion system is three different kinds of thing. Models, the UNets, DiTs, and VAEs, are learned nn.Modules with weights. Schedulers, the DDPM, DDIM, Euler, and DPM-Solver samplers, are small numerical steppers with no parameters at all, pure functions of noise levels and predictions plus a little bookkeeping, and the library pointedly does not make them modules. Pipelines are thin orchestration, tokenize the prompt, encode it, loop the scheduler over the model, decode the latents, and hold no logic worth stealing. The separation is what lets research recombine parts, a new sampler is a new scheduler class that drops into every existing pipeline unchanged, and a new model reuses two dozen existing samplers on day one.

The mechanism holding it together is config-driven instantiation. Every model and scheduler records its constructor arguments through a registration decorator, and serializes them to a JSON config next to the weights. A saved pipeline is then a directory of named components, each a config plus optional weights, and an index file naming which class implements each slot. from_pretrained rebuilds the whole object graph from that description, and because the description names classes rather than baking in behavior, swapping a component is one line, construct a different scheduler from the old scheduler's config and assign it. Sampler upgrades that halve inference steps ship as pure config swaps, no retraining, no pipeline edits.

The style has a shadow side that the maintainers accept openly. Pipelines are copy-pasted per model family rather than abstracted, because the abstraction that would unify them would obscure the per-family differences that researchers come to read, the repo calls this single-file policy out as a design principle inherited from transformers. The cost is enormous duplication and the occasional divergence bug, the benefit is that any one pipeline reads top to bottom without indirection, which for a library whose users fork pipelines daily is the right trade. Component seams, config instantiation, readable orchestration, duplicated leaves, that cluster of choices is the library-of-parts idiom, and it transfers well beyond diffusion. My notes on the codebase are at /oss/diffusers.

Structured loops, Lightning and torchtune

A different lineage decided the raw loop was the problem. PyTorch Lightning asks you to restate your model as a LightningModule, the forward logic in training_step, optimizer construction in configure_optimizers, validation in its own method, and hands the loop itself to a Trainer that owns iteration, device placement, precision, distributed strategy, checkpointing, and logging, with a callback system for everything cross-cutting. This is inversion of control, the framework calls you. When your problem fits the template, supervised training with standard cadence, the deal is excellent, multi-GPU and mixed precision become constructor arguments, and a team's runs all share one operational shape, same checkpoints, same logs, same resume semantics.

The deal sours at the edges, and it is worth being specific about which edges. Loops with unusual structure, interleaved optimizers with data-dependent scheduling, RL-style environment interaction, anything where the step boundary is not one batch, force you into manual-optimization mode and a growing pile of overridden hooks, at which point you are writing the raw loop again, but distributed across callback methods and harder to read than the flat version. Stack traces pass through framework frames, and behavior comes from configuration as much as from code, so debugging requires knowing the framework, not just your model. None of this is a flaw exactly, it is the standing price of inversion of control, and the honest question is only whether your loop is standard enough to pay it.

pytorch/torchtune is the interesting middle position, a post-Lightning answer to the same need. It offers recipes, each a complete, flat, readable training script for one job, LoRA fine-tuning, full fine-tuning, DPO, QAT, driven by a YAML config whose entries name classes and arguments to instantiate, models, tokenizers, datasets, optimizers. The crucial design decision is that recipes are meant to be copied, the CLI has a command that copies a recipe and its config into your project for editing, framework as starting point rather than dependency. You get the operational consistency that makes frameworks attractive, while the loop stays a file you own, nanoGPT's readability with torchtitan-grade distributed pieces underneath, since recipes use FSDP2 and activation checkpointing through plain PyTorch APIs. The cost is duplication across recipes, accepted deliberately, the same trade diffusers makes with pipelines, and it is striking that two ecosystems arrived at it independently. My walkthrough is at /oss/torchtune.

Data loading in practice

Every project above feeds on the same two abstractions, and the choice between them is really a choice about whether your dataset has an index. A map-style Dataset exposes __getitem__ and __len__, and the DataLoader's sampler owns ordering, which makes global shuffling, exact epochs, and resumable iteration trivial, so it is the right default whenever random access is affordable, which in practice means files on local disk or anything memory-mapped. An IterableDataset is just a stream, the right shape for corpora that do not fit, that live behind object storage, or that are generated on the fly, and it moves all responsibility for ordering, sharding, and epoch boundaries onto you.

The DataLoader's workers are processes, not threads, each running a copy of the dataset object, with prefetch_factor batches queued per worker, pin_memory staging output for fast async transfer, and persistent_workers keeping them alive across epochs. For map-style datasets the loader splits work by handing different indices to different workers, and correctness is automatic. For iterable datasets it hands each worker the whole stream, and the classic bug follows, four workers each yielding the full dataset means every sample arrives four times per epoch, and the model trains on duplicated data with no error raised. The fix is explicit sharding inside __iter__, and at scale the webdataset-style layout makes that natural, the corpus lives as a few thousand shard files of a few thousand samples each, written sequentially, so streaming reads are fast on any storage, shuffling happens at two levels, shard order globally and a small in-memory buffer locally, and sharding across ranks and workers is just slicing the shard list.

import glob
import torch
from torch.utils.data import IterableDataset, get_worker_info

class ShardStream(IterableDataset):
    """Each (rank, worker) pair reads a disjoint slice of the shards."""

    def __init__(self, pattern, rank=0, world_size=1, seed=0):
        self.shards = sorted(glob.glob(pattern))
        self.rank, self.world_size, self.seed = rank, world_size, seed

    def __iter__(self):
        info = get_worker_info()             # None in the main process
        wid = info.id if info else 0
        nw = info.num_workers if info else 1
        g = torch.Generator().manual_seed(self.seed)
        order = torch.randperm(len(self.shards), generator=g).tolist()
        # Global worker index strides through the shuffled shard list,
        # so no shard is read twice and no worker duplicates another.
        mine = order[self.rank * nw + wid :: self.world_size * nw]
        for idx in mine:
            yield from read_shard(self.shards[idx])  # your tar reader

The last piece is the one most codebases treat as an afterthought and the good ones treat as the real API. The collate_fn is the boundary where per-sample structure becomes batched tensors, and it is where padding policy, attention masks, sequence packing, and any nested batch structure actually live. The default collate stacks same-shaped tensors and recurses into dicts and tuples, which is fine until sequences have different lengths, and from then on the collate function is where your batch format is defined, which makes it the thing to read first in an unfamiliar repo.

def collate_padded(batch, pad_id=0):
    seqs = [torch.as_tensor(s, dtype=torch.long) for s in batch]
    width = max(s.numel() for s in seqs)
    tokens = torch.full((len(seqs), width), pad_id, dtype=torch.long)
    mask = torch.zeros(len(seqs), width, dtype=torch.bool)
    for i, s in enumerate(seqs):
        tokens[i, : s.numel()] = s
        mask[i, : s.numel()] = True
    return tokens, mask                      # the batch format, defined here

loader = torch.utils.data.DataLoader(
    ShardStream("data/shard-*.tar", rank=rank, world_size=world_size),
    batch_size=16, num_workers=4, prefetch_factor=4,
    pin_memory=True, persistent_workers=True,
    collate_fn=collate_padded)

Two habits from the wild worth copying. Tokenize and shard offline, as nanoGPT and every pretraining stack do, so the loader's job at train time is I/O and collation rather than compute. And when a run mysteriously starves the GPU, look at worker count and collate cost before touching the model, because a loader that cannot fill prefetch_factor queues shows up as idle GPU with no error anywhere.

Interop and the deployment edges

The story of getting a PyTorch model out of Python has been rewritten twice, and reading old codebases requires knowing which era they belong to. TorchScript was the first answer, torch.jit.trace and torch.jit.script compiled models into a serialized graph runnable from C++, and for years it was the deployment path, which is why @torch.jit.script annotations and .pt archives still litter production repos. It is now in maintenance mode, kept working but not developed, because scripting a meaningful subset of Python proved to be a permanent tax on both the language and the library. Treat TorchScript in a new dependency as legacy surface.

The present splits the problem in two. For making Python execution fast in place, torch.compile traces your actual running code into graphs, falls back to eager at graph breaks, and hands the graphs to the Inductor backend, which generates Triton kernels for GPU and vectorized C++ for CPU. It is the piece nanoGPT turns on with one line and the piece vLLM builds its newer engine around, and its characteristic failure mode is not wrong answers but recompilation and graph breaks, which you diagnose with the logging tools the compiler stack ships. For leaving Python entirely, torch.export captures a whole model ahead of time into a single graph with explicit dynamic-shape annotations, and that exported program is the shared foundation the other edges consume. The modern ONNX exporter converts through it into the cross-runtime format TensorRT and ONNX Runtime consume. AOTInductor compiles it into a shared library callable without a Python interpreter. And ExecuTorch lowers it for phones and microcontrollers, producing a compact program file whose subgraphs are delegated to hardware backends like XNNPACK or Core ML, which is how on-device PyTorch models actually ship today.

The remaining edge runs the other direction, foreign code entering PyTorch. The torch.utils.cpp_extension machinery compiles C++ and CUDA sources against libtorch, either ahead of time in a package build or JIT at import, and is how vLLM, flash-attention, and every kernel-carrying project binds its kernels. The part that has changed recently is registration, the modern habit is to declare bound kernels as custom ops through torch.library, with a fake implementation that gives the compiler shape semantics, so that a custom kernel no longer forces a graph break, which is precisely the detail that lets hand-written kernels and torch.compile coexist in one codebase instead of competing.

Which idiom to reach for

The projects in this survey are not stages of enlightenment, they are fitted solutions to different constraints, and the practical skill is matching your constraint to the idiom without romance. The table is the honest summary.

Situation Idiom Reference example What it costs
Research or learning, one node Flat train.py, plain modules, torch.compile flag nanoGPT Nothing reusable, rewrite to scale
Pretraining across many nodes DTensor and FSDP2 composition, meta init, DCP torchtitan Distributed concepts enter every debug session
Fine-tuning against a memory wall Fused kernels behind custom autograd.Function Unsloth Maintenance coupling, gradient correctness risk
Serving at high throughput Own runtime, paged KV, CUDA graphs, custom ops vLLM A second codebase per model architecture
Building a library others recombine Parts with config instantiation, duplicated leaves diffusers Duplication, divergence between copies
Team-standard training jobs Recipes or trainer frameworks torchtune, Lightning Inversion of control when the loop is unusual
Shipping off the Python server torch.export, then ONNX, AOTInductor, or ExecuTorch ExecuTorch examples Export-compatible model discipline up front

A few closing rules of thumb that the survey supports. Start flat, and add structure only when a named constraint forces it, because every style here that abandoned the plain loop did so for a reason it can state in one sentence. Prefer the official composition seams, DTensor, fully_shard, custom op registration, over private-API patching whenever both can work, since the seams are the parts with a compatibility promise. Keep an eager reference implementation next to every hand-written kernel, forever. And when reading an unfamiliar PyTorch codebase, find the three places where it deviates from the vanilla loop, because that is where its actual ideas live, everything else is the shared dialect this page started with.

Key takeaway. PyTorch in production is one shared dialect plus a deliberate deviation. nanoGPT deviates nowhere and wins readability, torchtitan deviates at the tensor layer to compose parallelism, Unsloth deviates inside autograd to buy memory, vLLM deviates at the execution loop to buy throughput, diffusers and torchtune deviate at the packaging layer to buy recombination. Learn the shared idioms once, then read each codebase by asking which contract it broke and what it bought with the pieces.