Distributed Training - Systems Performance Skill
SkillAI & modelsGuides multi-GPU pre-training: DDP, FSDP2, ZeRO, tensor/pipeline/expert parallelism, fp8/Muon. Use when scaling a run, training MoE, or reproducing GPT-2 on rented GPUs.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Distributed Training - Systems Performance Skill skill
What this skill tells your AI
The instructions your AI receives, as published by vasilyu1983/ai-agents-public in frameworks/shared-skills/skills/ai-distributed-training/SKILL.md and read by ahel’s review.
Operational focus: picking and implementing the right parallelism strategy, not the theory. Covers data parallelism through FSDP/ZeRO/tensor+pipeline parallelism, memory-efficient attention, mixed precision at scale, activation checkpointing, rented-GPU cost discipline, and reproducing GPT-2 124M as the canonical sanity check.
Profile before you scale. Debug on the smallest GPU that fits. Stop the instance when done.
ASCII Flow
single GPU (debug/prototype)
└─ DDP: replicate model, all-reduce gradients — scales until the gradient all-reduce stops hiding behind compute
└─ FSDP2 / ZeRO: shard optimizer state, gradients, params across GPUs
└─ tensor parallelism: split weight matrices across GPUs (intra-node)
└─ pipeline parallelism: split layers across nodes (inter-node)
└─ context parallelism: shard the sequence dim (long context)
└─ expert parallelism: route MoE experts across GPUs (all-to-all)
└─ N-D parallelism: DP + TP + PP + CP + EP (frontier MoE)
profile-before-scale
└─ nsys / torch.profiler → find bottleneck (compute? memory? dataloader?)
└─ fix bottleneck at small scale, then scale
When to Use This Skill
Activate when the user asks about:
- Choosing between DDP, FSDP2, DeepSpeed ZeRO stages 1/2/3, or Megatron-LM
- Training Mixture-of-Experts (MoE) models: expert parallelism, all-to-all, load balancing
- OOM errors on multi-GPU training runs
- Memory-efficient attention (FlashAttention-2/3, xformers)
- Mixed precision (bf16, fp8, nvfp4) trade-offs at pre-training scale
- Optimizer choice at scale (AdamW vs Muon/MuonClip)
- Targeting current-gen hardware (H100, Blackwell B200/GB200 NVL72, early Rubin NVL72 access)
- Gradient checkpointing vs activation checkpointing cost
- Pre-training frameworks: litgpt, torchtitan, nanotron, levanter
- Reproducing GPT-2 (modded-nanoGPT or nanochat as the active reference; llm.c as the educational one)
- Rented GPU cost management (RunPod, Lambda, Vast.ai, Modal)
- Spot / interruptible instance checkpoint strategies
- Profiling a training run before deciding to scale
Scope Boundaries (Use These Skills for Depth)
- Data mix, filtering, dedup, decontamination -> ai-data-curation-pretraining. Before spending on N GPUs, the data mix matters more than the parallelism — a better corpus beats a better topology at the same budget, and it is far cheaper to change.
- Single-GPU pre-training build, data pipelines, tokenization -> ai-pretraining
- Checkpoint evals, benchmark harnesses, regression gates -> ai-evals
- Token budget, compute-optimal scaling, Chinchilla -> ai-scaling-laws
- Serving optimization, batching, quantization, inference -> ai-llm-inference
- General cloud/infra cost optimization -> ops-cost-optimization
- Production MLOps, model registry, monitoring, deployment -> ai-mlops
Default Workflow
- Confirm scale and budget: how many GPUs, which provider, on-demand or spot, target hours.
- Profile at small scale: run
nsysortorch.profileron 1-2 GPUs before adding more. - Pick parallelism strategy: data parallel (DDP) -> FSDP/ZeRO -> tensor+pipeline only as needed.
- Enable memory optimizations: FlashAttention, gradient checkpointing, bf16, activation offload.
- Wire checkpointing: use Distributed Checkpoint (DCP) for sharded state, save async to object storage every N steps; test restore before long runs.
- Scale and re-profile: verify near-linear throughput scaling; fix communication bottlenecks.
- Evaluate at checkpoints, not just at the end: run a small fixed eval suite on every saved checkpoint alongside the loss curve. Loss falling while a downstream benchmark flatlines is the signal that catches a bad data mix or a broken tokenizer while the GPUs are still running, and gating only on systems metrics (MFU, throughput) will not surface it. See ai-evals for harness and gate design.
- Stop instance: confirm instance termination; verify storage persistence; check billing.
Quick Reference
| Decision | Default Move | Promote When | Avoid |
|---|---|---|---|
| Parallelism for ≤8 GPUs | DDP or FSDP2 (ZeRO-2 equiv) | Model does not fit in one GPU | Jumping to tensor parallel before model is too large |
| Parallelism for >8 GPUs | FSDP2 (ZeRO-3 equiv) or DeepSpeed ZeRO-3 | Multiple nodes needed | Mixing FSDP + DeepSpeed naively |
| FSDP version | FSDP2 (fully_shard, DTensor) | All new PyTorch projects | FSDP1 (FullyShardedDataParallel) — deprecated since PyTorch 2.11 |
| MoE routing at scale | Expert parallelism + all-to-all | Sparse MoE, experts exceed one GPU | TP on experts before EP (all-to-all is cheaper on NVLink) |
| Attention kernel | FlashAttention-2/3 | A100+ / H100 (FA3 = Hopper) | xformers as default (verify support for your GPU) |
| Mixed precision | bf16 | A100 / H100 (native bf16) | fp16 on A100+ (bf16 is safer; less loss spike risk) |
| Low-precision training | fp8 (H100 TransformerEngine/torchao) | Proven recipe + per-tile scaling | nvfp4/fp8 without loss-vs-bf16 validation |
| Optimizer | AdamW | Default, well-understood | — |
| Optimizer (frontier) | Muon / MuonClip | Matmul params, want ~1.3–1.5× token efficiency | Muon on embeddings/scalars (keep those on AdamW) |
| Gradient checkpointing | Selective activation recomputation (SAC) | Any model >1B params; full per-block recompute only when SAC still does not fit | Wrapping a whole block that contains FlashAttention (double-recompute); quoting sqrt(n) savings for per-block policy |
| Optimizer state sharding | ZeRO-1 | Memory pressure from optimizer | ZeRO-3 when params fit on one GPU |
| Compile | torch.compile on the model | Want MFU; using torchtitan/FSDP2 | Leaving eager mode on long production runs |
| Framework for ≤7B pre-training | litgpt or torchtitan | Need Megatron-grade scale | Rolling your own training loop before reading existing frameworks |
| Dev / debug GPU | Smallest A10G or L4 that fits | Need bf16 native | H100/B200/Rubin for debugging (cost bloat) |
| Production training GPU | H100; B200/GB200 NVL72 for frontier; Rubin NVL72 where available | Need fp8/nvfp4 + NVLink-domain scale | Renting Blackwell/Rubin to debug a 124M model |
| Checkpoint storage | S3-compatible object store + DCP async | Spot instances (checkpoint every N steps) | Local disk only (lost on preemption) |
Parallelism Deep Dive
Data Parallelism (DDP)
Each worker holds a full model replica. Forward + backward runs independently per GPU. AllReduce synchronizes gradients. What sets the ceiling is gradient size ÷ interconnect bandwidth, not a GPU count — the all-reduce must finish inside the backward pass it overlaps. In practice that is order-64 GPUs on a well-connected cluster and far less on a small model over Ethernet; measure exposed all-reduce time in the profiler rather than trusting any number, including this one. Memory cost: full model + optimizer state on every GPU.
# PyTorch DDP minimal setup
model = DistributedDataParallel(model, device_ids=[local_rank])
FSDP2 (Fully Sharded Data Parallel)
PyTorch-native. Shards parameters, gradients, and optimizer state across all workers. Use FSDP2 (fully_shard) for all new work — the original FullyShardedDataParallel (FSDP1, FlatParameter-based) is deprecated as of PyTorch 2.11. FSDP2 shards each parameter individually as a DTensor (Shard(dim=0)), giving simpler/inspectable sharded state dicts, cleaner composition with TP/PP/CP via DeviceMesh, and tight torch.compile integration.
ZeRO-stage equivalents map onto reshard_after_forward:
reshard_after_forward=False→ keep params gathered after forward (ZeRO-2-like: shard grads + optimizer state, trade memory for fewer all-gathers)reshard_after_forward=True(default) → re-shard params after forward (ZeRO-3-like: shard params + grads + optimizer state)
# FSDP2 (PyTorch >=2.11). Shard each transformer block, then the root.
from torch.distributed.fsdp import fully_shard, MixedPrecisionPolicy
mp = MixedPrecisionPolicy(param_dtype=torch.bfloat16, reduce_dtype=torch.float32)
for block in model.layers:
fully_shard(block, mp_policy=mp)
fully_shard(model, mp_policy=mp)
FSDP1 (FullyShardedDataParallel + ShardingStrategy.FULL_SHARD/SHARD_GRAD_OP/NO_SHARD) still appears in older tutorials; migrate to FSDP2. Checkpoints are compatible across the two, but the construction API is not.
DeepSpeed ZeRO Stages
The reduction is a function of N (the shard count), not a constant — so state the baseline before quoting a multiplier. Per-parameter baseline for the mixed-precision AdamW recipe this skill recommends:
bf16 params 2 B
bf16 gradients 2 B
fp32 master copy 4 B
fp32 Adam m 4 B
fp32 Adam v 4 B
----
16 B/param (the 4+12 split the ZeRO paper uses:
4 B "compute" state, 12 B optimizer state)
| Stage | What is Sharded | Memory per param | Reduction vs 16 B | Overhead |
|---|---|---|---|---|
| ZeRO-1 | Optimizer state | 4 + 12/N | 16/(4+12/N) → 4× as N→∞ | Low |
| ZeRO-2 | Optimizer state + gradients | 2 + 14/N | 16/(2+14/N) → 8× as N→∞ | Low |
| ZeRO-3 | Optimizer state + gradients + params | 16/N | N — linear, no ceiling | Communication cost |
So 4× and 8× are the N→∞ asymptotes, not values you get on a small cluster, while ZeRO-3's reduction is N. At N=8 the three stages give 2.9× / 4.3× / 8×; at N=64, 3.8× / 7.2× / 64×. Quoting "ZeRO-3 gives 64×" without saying N=64 is how a reader on 8 GPUs ends up 8× short.
Worked example: a 7B model at 16 B/param is ~112 GB of model+optimizer state before a single activation — it does not fit on one 80 GB H100. At N=8 with ZeRO-3 that is 112/8 = 14 GB per GPU, leaving ~65 GB for activations.
ZeRO-Infinity extends stage 3 to NVMe offload. Use only when GPU memory is genuinely exhausted — disk bandwidth becomes the bottleneck.
Tensor Parallelism (Megatron-LM style)
Splits weight matrices across GPUs within a node (column/row parallel linear). Requires high-bandwidth NVLink. Megatron-LM implements Transformer-specific tensor parallel (TP) with sequence parallel (SP) for activation memory reduction. Best for models that cannot fit even with full sharding, or where communication budget allows.
Pipeline Parallelism
Splits model layers across nodes (or GPU groups). Interleaved schedules (1F1B) reduce pipeline bubble overhead. Adds complexity: microbatch sizing, bubble fraction tuning. Typically combined with TP and DP in 3-D parallelism (Megatron-LM, nanotron).
torch.distributed.pipelining is the PyTorch-native PP API (ScheduleGPipe, Schedule1F1B, ScheduleInterleaved1F1B); it composes with FSDP2 and TP through DeviceMesh, so it is the PP layer that fits the rest of the stack this skill recommends without adopting Megatron or nanotron wholesale.
DualPipe (DeepSeek-V3, 2024) is a bidirectional pipeline schedule that fully overlaps forward/backward compute with communication, driving the bubble toward zero — the reference design for large MoE training where cross-node all-to-all would otherwise dominate.
Expert Parallelism (MoE)
Mixture-of-Experts models activate only a few experts per token, so total params (e.g. 1T) vastly exceed activated params (e.g. 32B). Expert parallelism (EP) places different experts on different GPUs; the router dispatches each token to its experts via all-to-all communication (dispatch), then a second all-to-all gathers results (combine). EP composes with DP/TP/PP/CP as an extra mesh dimension.
Key concerns specific to MoE training:
- Load balancing: an auxiliary load-balancing loss (or DeepSeek-V3's auxiliary-loss-free bias-update scheme) keeps tokens spread across experts; without it, a few experts saturate and the rest idle.
- All-to-all is the bottleneck, not all-reduce. It scales with cross-node bandwidth — keep EP inside the NVLink domain where possible, and overlap it with compute (DualPipe). DeepSeek-V3 trained a 671B MoE with no tensor parallelism, relying on EP + DualPipe + fp8 instead.
- Dropless routing is the modern default; capacity factors are the legacy alternative. A capacity factor caps tokens per expert and drops or reroutes the overflow — simple, but it discards tokens to keep the GEMM shapes static. Since MegaBlocks, dropless MoE expresses the expert FFN as a block-sparse / grouped GEMM over variable-size expert batches, so no token is dropped and no capacity factor is tuned; Megatron-Core and torchtitan ship it. Reach for a capacity factor only when you deliberately want a throughput cap or a fixed memory envelope.
- Frameworks: Megatron-Core, DeepSpeed-MoE, and nanotron implement EP;
torch.distributedprovides the all-to-all primitives.
Overlapping Communication with Compute
"MFU below 30% means communication bound" tells you how to detect the problem. This is how to fix it: exposed collectives are the target, and the lever is overlap, not less communication.
- FSDP2 prefetch. The parameter all-gather for layer n+1 should be in flight while layer n computes, and the gradient reduce-scatter for layer n should overlap layer n-1's backward. Tune backward prefetch depth rather than accepting the default on an unusual model shape.
- Async collectives.
async_op=Truereturns a handle you wait on later; the work between issue and wait is your overlap window. Gradient bucketing (DDP) is the same idea — group small gradients so a collective is worth launching. - Async tensor parallelism fuses TP collectives into the matmul epilogue so the communication for one tile happens while the next tile computes. This is how torchtitan hides TP collectives; on recent PyTorch it is built on symmetric-memory primitives. Naming and API surface here are moving fast — check torchtitan's current config rather than quoting a flag from here.
- DualPipe (above, under Pipeline Parallelism) is the MoE-scale version of the same principle: schedule so the all-to-all is never exposed.
Measure overlap directly. A torch.profiler trace shows whether the NCCL stream sits idle during compute (good) or the compute stream sits idle during a collective (exposed communication). A ratio derived from step time cannot distinguish the two.
Will It Fit? A Memory Sanity Check
Before choosing a parallelism strategy, check whether activations alone rule out the naive configuration. The Megatron activation formula for a transformer layer stack is s·b·h·L·(10 + 24/t + 5·a·s/(h·t)) bytes (s = sequence length, b = micro-batch, h = hidden, L = layers, a = heads, t = TP degree).
For s=8192, b=1, h=4096, L=32, a=32, t=1 this comes to roughly 380 GB — a single 8k-context sample does not fit on an 80GB H100 without recomputation or tensor parallelism. That is the arithmetic that decides between "add gradient checkpointing" and "add TP", and it is worth running before renting anything. Note the 5·a·s/(h·t) term is quadratic in sequence length: at long context, attention activations, not weights, are what breaks you.
Memory-Efficient Attention
FlashAttention (Dao et al., 2022/2024): reorders attention computation to avoid materializing the full N×N attention matrix. Result: O(N) memory vs O(N²), significant speedup on A100/H100.
# PyTorch ≥2.3: select the Flash backend via the current API
from torch.nn.attention import sdpa_kernel, SDPBackend
with sdpa_kernel(SDPBackend.FLASH_ATTENTION):
out = F.scaled_dot_product_attention(q, k, v)
# (torch.backends.cuda.sdp_kernel(...) is the deprecated pre-2.3 form)
FlashAttention-3 (2024) targets H100 with further hardware-specific optimizations. xformers provides memory_efficient_attention as an alternative with broader GPU support.
Mixed Precision at Scale
bf16 (bfloat16) is the safe default for A100+ and H100. Same exponent range as fp32 (avoids the overflow spikes common in fp16), 16-bit mantissa precision. torch.amp.autocast('cuda', dtype=torch.bfloat16) or pass torch_dtype=torch.bfloat16. Gradient scaler (torch.amp.GradScaler('cuda')) is needed for fp16 but not for bf16. (The torch.cuda.amp.autocast / torch.cuda.amp.GradScaler spellings are deprecated — use the torch.amp forms.)
fp8 is now production-proven on Hopper (H100), not just emerging. DeepSeek-V3 trained at fp8 with fine-grained scaling — per-token 1×128 / per-block 128×128 tiles plus high-precision CUDA-core accumulation — keeping the loss within ~0.25% of bf16. Use TransformerEngine or torchao float8 for the linear layers; keep a bf16/fp32 master copy of weights and the optimizer state. Validate loss-vs-bf16 on your workload before committing a long run.
What "use fp8" actually means to set — an endorsement is not a recipe, and these are the parts people get wrong:
- Which tensors stay higher precision: embeddings, the LM head, all norms, and (in MoE) the router. fp8 applies to the FLOP-dense linear layers, not the whole model.
- Scaling granularity: per-tensor is the simplest and the most fragile; per-tile (DeepSeek-V3's 1×128 activations / 128×128 weights) is what made a long fp8 run hold. Delayed scaling reuses a scale from prior steps (cheaper, needs an amax history); current scaling computes it in-step (safer, costlier).
- Accumulation stays high-precision — fp8 inputs, fp32 accumulate. This is not optional.
- First and last layers are commonly excluded from fp8 even when everything else converts.
- Read the recipe off your framework's own docs (TransformerEngine
fp8_autocastrecipes, torchaofloat8configs) rather than a blog post; the defaults differ between them and both move.
nvfp4 / fp4 arrives with Blackwell. The B200/GB200 add hardware FP4 (including NVIDIA's NVFP4, 16-element micro-scaled blocks with e4m3 scales, vs MXFP4's 32-element UE8M0 blocks). It has moved past pure research: NVIDIA pre-trained a 12B model on 10T tokens with NVFP4 matching the fp8 baseline (arXiv 2509.25149), and MXFP4 needed ~36% more tokens to reach the same loss — so NVFP4 is the stronger FP4 format. Still validate against bf16/fp8 on your own workload before a long run; the recipe (which tensors stay higher-precision, scaling, outlier handling) is less battle-tested than fp8.
Hardware Tiers (mid-2026)
- A10G / L4 — cheap debug and architecture validation. Native bf16 on L4.
- A100 80GB — bf16 workhorse; still common and cost-effective on spot.
- H100 — bf16 + fp8 (TransformerEngine), FlashAttention-3, NVLink/NVSwitch domains. Now the mainstream production tier, not the frontier.
- Blackwell B200 / GB200 NVL72 — mainstream frontier: fp4/nvfp4 hardware, ~2–3× faster training than H100, and a 72-GPU NVLink domain (NVL72) that lets EP/TP span a whole rack at NVLink bandwidth. Widely available on major clouds by mid-2026.
- Rubin / Vera Rubin NVL72 — newest generation: entered production ~June 2026, with cloud/neocloud availability (AWS, GCP, Azure, CoreWeave, Lambda, Nebius) rolling out through H2 2026. Treat as capacity-constrained and premium-priced for now; verify current availability and quoted pricing before planning around it. Reserve any Blackwell/Rubin tier for frontier-scale runs, not 124M debugging.
torch.compile
torch.compile(model) (TorchInductor) fuses kernels and is essential for competitive MFU on modern hardware. It composes with FSDP2 and is on by default in torchtitan. Compile once outside the training loop; expect a warm-up cost on the first steps. Pair with bf16/fp8 — most of the published MFU numbers assume compile is on.
Where compile actually costs you MFU. Compile is a common source of "the run got slower and nobody knows why":
- Silent recompilation on dynamic shapes. Variable sequence length, a ragged last batch, or a changing micro-batch triggers a fresh compile per shape and can dominate step time. Pad to fixed shapes, or accept
dynamic=Truedeliberately. Diagnose withTORCH_LOGS=recompiles— if you have never run this on a compiled training loop, do it once before trusting the MFU number. - Compile the block, not the world. torchtitan compiles per transformer block rather than the whole model; regional compilation keeps compile times sane and lets the same compiled artifact be reused across identical layers. Whole-model compile on a deep model can cost minutes of warm-up per launch.
fullgraph=Truesurfaces graph breaks as errors instead of letting them silently fragment the graph. Use it while tuning, then decide which breaks you are willing to live with.mode="max-autotune"buys extra kernel search time up front for better steady-state kernels — worth it on a long run, wasteful on a debug loop.
Activation / Gradient Checkpointing
torch.utils.checkpoint.checkpoint(function, *args) recomputes activations during the backward pass instead of storing them. Two different policies get conflated — name the one you are using:
- Per-transformer-block checkpointing (what "wrap every block" means). You store one boundary activation per block instead of every intermediate tensor inside it. The saving is roughly the number of intra-block tensors you stop storing — on the order of 10–20×, and it is a constant factor independent of depth, not a function of L. Cost: one extra forward per block. Since backward ≈ 2× forward, one extra forward over a 1F+2B budget is 1/3, so ~33% is the theoretical ceiling for full recomputation; ~30–40% is the practical band.
- sqrt(n)-segment checkpointing (Chen et al. 2016, Training Deep Nets with Sublinear Memory Cost). Checkpoint
sqrt(n)segments out ofnlayers — the memory-optimal segmentation, and the origin of theO(sqrt(n))result. This is a different policy from per-block wrapping; do not quote its scaling for per-block checkpointing.
Prefer selective activation recomputation (the 2026 default). Instead of recomputing whole blocks, recompute only the tensors that are cheap to recompute and expensive to store — the attention softmax/dropout path — and keep the FLOP-dense matmuls stored. Megatron's Reducing Activation Recomputation in Large Transformer Models (arXiv 2205.05198) reports that sequence parallelism plus selective activation recomputation "reduces activation memory by 5x, while reducing execution time overhead from activation recomputation by over 90%", and that training a 530B GPT-3-style model on 2240 A100s reaches "a Model Flops Utilization of 54.2%, which is 29% faster than the 42.1% we achieve using recomputation" (quoted from the abstract, verified 2026-08-31).
Express it with torch.utils.checkpoint.create_selective_checkpoint_contexts (SAC) or torchtitan's SAC config; checkpoint_wrapper is the older FSDP-side entry point and only expresses whole-module recompute.
FlashAttention already recomputes — do not checkpoint over it
Shortened here. Read the whole file on GitHub.
Signals
- GitHub stars
- 87
- Forks
- 19
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
ai-distributed-training- Source
- github.com/vasilyu1983/ai-agents-public