Type at least 2 letters. Enter opens the first result, Esc closes.

    ML Interview Prep · Lesson 12 · Training and serving at scale

    Training at scale: memory, parallelism, rooflines and infrastructure design

    Advanced 105 min 5 interactive figures 3 videos Colab notebook

    Before this lesson: lesson 07 (the one-GPU training step, 16 bytes per parameter, gradient accumulation), lesson 08 (the transformer layer that tensor parallelism splits) and lesson 11 (C≈6NDC \approx 6ND). The full theory is in the ML Systems track: training memory, collectives, data parallelism and ZeRO, tensor and pipeline parallelism and the roofline model. Time: about 105 minutes, plus the notebook.


    Why this matters #

    "Will a 13B model train on 8 GPUs with 80 GB?" and "How would you train this model on 1,000 GPUs?" are standard questions in research engineer and ML engineer interviews at LLM labs, and in ML-for-science groups that train foundation models. Some rounds ask for code: a data-parallel step with all_reduce, a simulated ring all-reduce, a column-parallel linear layer, or a pipeline schedule. Design rounds ask for a full training plan: GPU-hours, a parallel layout, a check that the network and the data loader keep up, what happens when a node dies, and how you know the model is getting better. The answers are a few formulas and a few hardware numbers. This lesson gives you both, with proofs, so that you can derive them on a whiteboard in minutes.

    1. Predict first #

    2. The picture #

    One GPU first. A training step keeps four kinds of tensors in GPU memory:

    • parameters WW, used by the forward and the backward pass;
    • gradients ∇W\nabla W, made by the backward pass and used by the optimizer;
    • optimizer states: for AdamW the first and second moments mm and vv, and in mixed precision also an fp32 master copy of the weights;
    • activations: the values made by the forward pass and kept until the backward pass has used them.

    The first three are the model states. Their size depends only on the number of parameters Ψ\Psi. The activations depend on the number of tokens processed at once, on the width and on the depth.

    Interactive figure: loading…

    Make a 7B model fit on 80 GB GPUs. With DDP (stage 0) the model states alone take 112 GB: the bar crosses the 80 GB line. Switch to ZeRO stages 1, 2 and 3 and watch the optimizer states, then the gradients, then the parameters shrink by the number of GPUs. Then try "standard attention" at s=8192s = 8192: the activations explode, and ZeRO does not help them. Tensor parallelism (tt) and full checkpointing do.

    Parallelism is a choice of what to split.

    Method What is split What is sent How often
    data parallelism (DDP) the batch all-reduce of the gradients once per step, during the backward pass
    ZeRO-1, ZeRO-2 the batch + optimizer states (+ gradients) reduce-scatter of gradients, all-gather of parameters once per step
    ZeRO-3 / FSDP the batch + all model states all-gather of parameters (forward and backward), reduce-scatter of gradients layer by layer
    tensor parallelism (TP) every weight matrix all-reduce of activations 2 per layer forward, 2 backward
    pipeline parallelism (PP) the layers activations and their gradients, to the next stage per micro-batch, at each stage boundary

    Three collectives do almost all the communication. A collective is an operation in which all GPUs of a group take part. Take pp GPUs, each with a vector of nn bytes:

    • all-reduce: every GPU ends with the sum of all pp vectors;
    • reduce-scatter: GPU ii ends with chunk ii (of size n/pn/p) of the sum;
    • all-gather: every GPU starts with one chunk and ends with all pp chunks.

    An all-reduce is a reduce-scatter followed by an all-gather. The ring algorithm arranges the GPUs in a ring. In each step every GPU sends one chunk of n/pn/p bytes to its neighbour. Each phase takes p−1p - 1 steps, so an all-reduce sends

    2 p−1p n  ≈  2n bytes per GPU,2\,\frac{p-1}{p}\,n \;\approx\; 2n \text{ bytes per GPU},

    almost independent of the number of GPUs. This is why data parallelism scales: more GPUs do not mean more bytes per GPU.

    Interactive figure: loading…

    Step through a ring all-reduce on 4 GPUs. Press Next step. In the first 3 steps (reduce-scatter) each GPU sends one chunk (orange outline) to the next GPU, which adds it: the small k/pk/p counts the GPUs already in the partial sum. After step 3 each GPU holds one complete chunk. The next 3 steps (all-gather) copy the complete chunks around the ring. Watch the counter reach 2(p−1)/p=1.52(p-1)/p = 1.5 times the vector size.

    Quick check

    8 GPUs run a ring all-reduce on 2 GB of bf16 gradients each. About how many bytes does each GPU send?

    3. Definitions and main results #

    Notation (as in the Megatron and ZeRO papers). Ψ\Psi number of parameters; LL layers; hh hidden size; aa attention heads; ss sequence length; bb micro-batch size (sequences per GPU per forward pass); NdN_d data-parallel GPUs; tt tensor-parallel degree; pp pipeline stages; mm micro-batches per step; NN GPUs in total; DD training tokens; FF FLOP/s per GPU; BW\text{BW} bandwidth in bytes/s. Hardware (dense, bf16): A100-80GB SXM 312 TFLOP/s and 2.04 TB/s HBM; H100 SXM 989 TFLOP/s and 3.35 TB/s HBM, NVLink 450 GB/s per direction inside a node, about 50 GB/s per GPU between nodes (one 400 Gb/s network card per GPU). 1 GB =109= 10^9 bytes.

    3.1 The memory of one training step #

    Model states. Mixed-precision AdamW keeps, per parameter (ZeRO paper, §3.1):

    bf16 weight bf16 gradient fp32 master weight fp32 Adam mm fp32 Adam vv total
    2 2 4 4 4 16 bytes

    The last three are the optimizer states: K=12K = 12 bytes per parameter. PyTorch autocast keeps fp32 weights instead (4 for the weight, 4 for the gradient, 8 for Adam): also 16 bytes, split differently. A 7B model needs 16⋅7⋅109=11216 \cdot 7 \cdot 10^9 = 112 GB before any activation.

    Parameter count. A GPT-style layer (MLP width 4h4h) has 12h2+13h12h^2 + 13h parameters: attention 4h2+4h4h^2 + 4h, MLP 8h2+5h8h^2 + 5h, two LayerNorms 4h4h. So Ψ≈12Lh2\Psi \approx 12Lh^2, plus the embedding.

    Activations. For y=xWy = xW the weight gradient is ∇W=x⊤∇y\nabla W = x^\top \nabla y, so the backward pass needs the input xx. Every matmul input, every nonlinearity input and every dropout mask is stored. Korthikanti et al. (2022, §4.1) count, per layer, with 16-bit activations and 1-byte dropout masks:

    activation bytes per layer=sbh(34+5ash).\text{activation bytes per layer} = sbh\left(34 + \frac{5as}{h}\right).
    • 34 sbh34\,sbh covers the matmul inputs, the GeLU input, the LayerNorm inputs and the dropout masks.
    • 5as2b5as^2b is the attention core: the s×ss \times s softmax output, its dropout mask and the dropout output, for each of the aa heads. It grows with s2s^2.
    • FlashAttention never stores the s×ss \times s matrices: about 34 sbh34\,sbh per layer.
    • Full activation checkpointing stores only the input of each layer (2sbh2sbh bytes) and recomputes the rest in the backward pass. This costs one extra forward pass: about 8Ψ8\Psi instead of 6Ψ6\Psi FLOPs per token, 33% more compute.
    • Tensor parallelism with sequence parallelism of degree tt divides all of it by tt.

    Example: s=4096s = 4096, b=1b = 1, h=4096h = 4096, a=32a = 32 (a 7B model). sbh≈1.68⋅107sbh \approx 1.68 \cdot 10^7 and 5as/h=1605as/h = 160. Without FlashAttention: 194 sbh≈3.25194\,sbh \approx 3.25 GB per layer, 104 GB for 32 layers. With FlashAttention: 34 sbh≈0.5734\,sbh \approx 0.57 GB per layer, 18.3 GB in total. With full checkpointing: 2sbh⋅32≈1.072sbh \cdot 32 \approx 1.07 GB, plus one layer during recomputation.

    3.2 Data parallelism #

    Theorem (data parallelism = one large batch). Let the loss be the mean over a batch of BB samples, where each sample's loss depends only on that sample and WW (no BatchNorm statistics across the batch). Split the batch into NdN_d shards of equal size. Then the mean of the NdN_d shard gradients equals the full-batch gradient.

    Proof.

    1. L(W)=1B∑i=1Bℓi(W)\mathcal L(W) = \frac{1}{B}\sum_{i=1}^{B} \ell_i(W), where ℓi\ell_i is the loss of sample ii.
    2. Let SkS_k be shard kk and Lk=NdB∑i∈Skℓi\mathcal L_k = \frac{N_d}{B}\sum_{i \in S_k} \ell_i its mean loss. Each sample is in exactly one shard, so L=1Nd∑kLk\mathcal L = \frac{1}{N_d}\sum_{k} \mathcal L_k.
    3. The gradient is linear: ∇L=1Nd∑k∇Lk\nabla \mathcal L = \frac{1}{N_d}\sum_{k} \nabla \mathcal L_k.
    4. GPU kk computes ∇Lk\nabla \mathcal L_k. An all-reduce (sum), then a division by NdN_d, gives ∇L\nabla \mathcal L on every GPU.
    5. All GPUs start from the same WW and apply the same update to the same gradient, so the copies stay identical. ■\blacksquare

    With unequal shards (a ragged last batch, or a different number of tokens per GPU), weight each ∇Lk\nabla \mathcal L_k by its count; section 4(a) shows the error of the plain mean. Gradient accumulation is the same proof in time instead of space: run mm micro-batches one after another, add up their gradients, then take one optimizer step. The global batch is b⋅m⋅Ndb \cdot m \cdot N_d sequences.

    How DDP hides the all-reduce. PyTorch DDP groups the gradients into buckets (25 MiB by default). When a bucket is complete, its all-reduce starts while the backward pass continues with earlier layers. Inside no_sync() (the accumulation steps) DDP skips the all-reduce.

    When is it hidden? With TT tokens per GPU per step, the backward pass costs about 4ΨT4\Psi T FLOPs (4 of the 6Ψ6\Psi FLOPs per token), and the bf16 ring all-reduce sends about 2⋅2Ψ=4Ψ2 \cdot 2\Psi = 4\Psi bytes per GPU. Communication hides behind the backward pass when

    4ΨBW≤4ΨTF  ⟺  T≥FBW.\frac{4\Psi}{\text{BW}} \le \frac{4\Psi T}{F} \iff T \ge \frac{F}{\text{BW}} .

    The model size cancels. With F=400F = 400 TFLOP/s achieved and BW=50\text{BW} = 50 GB/s between nodes, each GPU needs at least 8,000 tokens per step. With gradient accumulation, only the last micro-batch's backward pass overlaps with the all-reduce, so count the tokens of that micro-batch.

    3.3 ZeRO and FSDP #

    ZeRO (Zero Redundancy Optimizer) is data parallelism without copies of the model states: each GPU keeps a 1/Nd1/N_d slice and collects the rest only when it needs it.

    Theorem (ZeRO memory and communication). With Ψ\Psi parameters, NdN_d data-parallel GPUs and K=12K = 12, the model states per GPU and the values each GPU sends per step are (times (Nd−1)/Nd(N_d-1)/N_d for ring collectives; 2 bytes per value in bf16):

    Stage What is sharded Model states per GPU (bytes) Sent per GPU per step
    DDP (stage 0) nothing (2+2+K)Ψ=16Ψ(2 + 2 + K)\Psi = 16\Psi 2Ψ2\Psi
    ZeRO-1 optimizer states 2Ψ+2Ψ+KΨ/Nd2\Psi + 2\Psi + K\Psi/N_d 2Ψ2\Psi
    ZeRO-2 + gradients 2Ψ+(2+K)Ψ/Nd2\Psi + (2 + K)\Psi/N_d 2Ψ2\Psi
    ZeRO-3 / FSDP + parameters (2+2+K)Ψ/Nd(2 + 2 + K)\Psi/N_d 3Ψ3\Psi

    Proof.

    1. Memory. Each stage keeps the unsharded parts in full and the sharded parts as a 1/Nd1/N_d slice. Add the bytes per parameter: 2 (weights), 2 (gradients), KK (optimizer).
    2. DDP. A ring all-reduce of Ψ\Psi gradient values is a reduce-scatter (Ψ\Psi) plus an all-gather (Ψ\Psi): 2Ψ2\Psi.
    3. ZeRO-1 and ZeRO-2. GPU kk updates only its 1/Nd1/N_d of the parameters, so it needs only its 1/Nd1/N_d of the summed gradient: a reduce-scatter (Ψ\Psi). After the update, every GPU needs all the new parameters: an all-gather (Ψ\Psi). This is the DDP all-reduce cut in two, with the optimizer step in the middle: 2Ψ2\Psi, no extra traffic.
    4. ZeRO-3. Parameters are not kept between uses. Before each layer's forward pass, all-gather its weights, then free them: Ψ\Psi over the whole model. The backward pass needs them again: a second all-gather (Ψ\Psi). Then reduce-scatter the gradients (Ψ\Psi). No all-gather after the update, because the next forward pass does it. Total 3Ψ3\Psi, which is 1.5×1.5\times DDP. ■\blacksquare

    FSDP is PyTorch's ZeRO-3. It wraps the model in units (usually one per transformer block), prefetches the all-gather of the next unit while the current unit computes, and frees the gathered weights after use. HSDP (hybrid sharding) shards inside a node and replicates across nodes, so that the all-gathers stay on NVLink.

    What ZeRO does not shard: activations. Each GPU still holds the activations of its own micro-batch. For those you need checkpointing, a smaller micro-batch, or TP with sequence parallelism.

    Quick check

    Which is the first ZeRO stage at which no GPU holds a full copy of the bf16 weights?

    3.4 Tensor parallelism (Megatron) #

    Simplest case: one matmul Y=XAY = XA on two GPUs.

    • Split AA by columns, A=[A1,A2]A = [A_1, A_2]. Then Y=[XA1,XA2]Y = [XA_1, XA_2]: each GPU computes its own columns. No communication, if XX is on both GPUs.
    • Split AA by rows, and split X=[X1,X2]X = [X_1, X_2] to match. Then Y=X1A1+X2A2Y = X_1A_1 + X_2A_2: each GPU has a partial sum, and an all-reduce adds them.

    The MLP block Z=GeLU⁡(XA) BZ = \operatorname{GeLU}(XA)\,B, with AA of size h×4hh \times 4h and BB of size 4h×h4h \times h (Shoeybi et al. 2019, §3):

    1. Split AA by columns. GPU ii computes Yi=GeLU⁡(XAi)Y_i = \operatorname{GeLU}(XA_i). GeLU acts on each element, so no communication is needed.
    2. Split BB by rows. GPU ii computes Zi=YiBiZ_i = Y_iB_i, a partial sum. One all-reduce gives Z=∑iZiZ = \sum_i Z_i.
    3. Why not split AA by rows? Then GPU ii would hold XiAiX_iA_i, and GeLU⁡(X1A1+X2A2)≠GeLU⁡(X1A1)+GeLU⁡(X2A2)\operatorname{GeLU}(X_1A_1 + X_2A_2) \ne \operatorname{GeLU}(X_1A_1) + \operatorname{GeLU}(X_2A_2): a second all-reduce before the GeLU.
    4. In the backward pass, every GPU used the same XX, so ∇X\nabla X is a sum over GPUs: one more all-reduce.

    Attention is split by heads: the Q, K, V projections are column-parallel (each GPU gets a/ta/t heads), attention runs locally, and the output projection is row-parallel. So each layer needs 2 all-reduces in the forward pass and 2 in the backward pass, each on b⋅s⋅hb \cdot s \cdot h values.

    Cost. TP divides weights, gradients, optimizer states and (with sequence parallelism) the activations by tt. But its communication sits on the critical path of every layer. In the forward pass a GPU computes 24bsh2/t24bsh^2/t FLOPs per layer and sends 2⋅2t−1t⋅2bsh2 \cdot 2\frac{t-1}{t} \cdot 2bsh bytes, so

    communication timecompute time=(t−1) F3h BW.\frac{\text{communication time}}{\text{compute time}} = \frac{(t-1)\,F}{3h\,\text{BW}} .

    For h=8192h = 8192, t=8t = 8 and F=989F = 989 TFLOP/s this is about 0.63 on NVLink (450 GB/s), but about 5.6 between nodes (50 GB/s). So TP stays inside a node.

    3.5 Pipeline parallelism #

    Split the LL layers into pp stages, one per GPU. Between stages only point-to-point messages travel: the activations of a micro-batch forward, their gradients backward (b⋅s⋅hb \cdot s \cdot h values each). This is little data, so PP works well across nodes. The naive pipeline sends the whole batch from stage to stage, so only one GPU works at a time. GPipe cuts the batch into mm micro-batches, so stage 2 works on micro-batch 1 while stage 1 works on micro-batch 2.

    Theorem (pipeline bubble). With pp stages, mm micro-batches, and times tft_f, tbt_b for the forward and backward pass of one micro-batch on one stage (equal on all stages, communication ignored), a GPipe step takes (m+p−1)(tf+tb)(m + p - 1)(t_f + t_b). The idle ("bubble") fraction of the step is

    p−1m+p−1.\frac{p-1}{m+p-1} .

    Proof.

    1. Micro-batch jj (j=0,…,m−1j = 0, \dots, m-1) can run on stage kk (k=0,…,p−1k = 0, \dots, p-1) only after it left stage k−1k-1, and after micro-batch j−1j-1 left stage kk. By induction on j+kj + k, its forward pass on stage kk ends at time (j+k+1) tf(j + k + 1)\,t_f.
    2. So the last forward pass (j=m−1j = m-1, k=p−1k = p-1) ends at (m+p−1) tf(m + p - 1)\,t_f.
    3. The backward phase is the mirror image and takes (m+p−1) tb(m + p - 1)\,t_b.
    4. Each stage does useful work for m(tf+tb)m(t_f + t_b), so it is idle for (p−1)(tf+tb)(p-1)(t_f + t_b): a fraction (p−1)/(m+p−1)(p-1)/(m+p-1) of the step. ■\blacksquare

    Relative to the ideal time m(tf+tb)m(t_f + t_b), the bubble is (p−1)/m(p-1)/m; the Ultra-Scale Playbook and Narayanan et al. use this form. Say which one you use.

    Interactive figure: loading…

    Shrink the bubble. Each row is a stage; blue boxes are forward passes, orange boxes backward passes (twice as long), numbered by micro-batch; the grey gaps are the bubble. With p=4p = 4, m=4m = 4 the bubble is 3/7≈43%3/7 \approx 43\%. Raise mm to 16: it falls to 3/193/19. Then switch to 1F1B: the step time stays the same, but stage 0 now holds the activations of at most pp micro-batches instead of mm.

    • More micro-batches shrink the bubble. With m≥4pm \ge 4p the bubble is below 20% of the step, because (p−1)/(5p−1)<1/5(p-1)/(5p-1) < 1/5.
    • But GPipe stores the activations of all mm micro-batches, because every forward pass runs before the first backward pass.
    • 1F1B (one forward, one backward) starts each backward pass as early as possible. Stage kk runs p−k−1p - k - 1 warm-up forward passes, then alternates. The bubble is the same, but stage kk holds at most p−kp - k micro-batches of activations (Narayanan et al. 2021, §2.2).
    • Interleaved 1F1B gives each GPU vv non-adjacent chunks of layers. The bubble drops to (p−1)/(vm+p−1)(p-1)/(vm + p - 1), at the cost of vv times more point-to-point messages.
    Quick check

    A pipeline has p=4p = 4 stages and m=4m = 4 micro-batches (GPipe, tb=2tft_b = 2t_f). What fraction of the step is the bubble?

    3.6 Putting them together #

    Large runs combine the methods: total GPUs == DP ×\times TP ×\times PP (and more, see below). The order of decisions (Ultra-Scale Playbook, "Finding the Best Training Configuration"):

    1. Fit one step in memory. Below about 10B parameters, ZeRO with FlashAttention (and, if needed, activation checkpointing) is usually enough. Larger models add TP inside a node (at most its GPU count, 8 on an H100 node) and PP or ZeRO-3 across nodes.
    2. Reach the target global batch with DP and gradient accumulation.
    3. Map to the hardware. Put the method with the most traffic (TP) on the fastest links (NVLink), and PP and DP across nodes.

    Context parallelism (CP) splits the ss tokens of one long sequence over GPUs. Ring attention passes key and value blocks around a ring of GPUs; DeepSpeed-Ulysses uses an all-to-all to switch from a split over tokens to a split over heads. CP divides the activation memory by its degree; use it when one sample's activations do not fit (ML Systems 10). Expert parallelism puts the experts of a mixture-of-experts layer on different GPUs, and tokens travel with an all-to-all (Deep Learning 15).

    Example: Llama 3 405B trained on 16,384 H100s with TP 8, PP 16 and FSDP data parallelism 128 (DP is outermost, because it tolerates the longest network latency); the long-context stage added CP 16 (Llama 3 paper, §3.3.2 and Table 4).

    3.7 Rooflines for compute, network and storage #

    Arithmetic intensity II = FLOPs / bytes moved. For any resource with rate RR (bytes/s), the roofline bound is

    t  ≥  max⁡ ⁣(FLOPsF, bytesR).t \;\ge\; \max\!\left(\frac{\text{FLOPs}}{F},\ \frac{\text{bytes}}{R}\right).

    The ridge point is I∗=F/BWHBMI^* = F/\text{BW}_{\text{HBM}}: an operation with I>I∗I > I^* is compute-bound, one with I<I∗I < I^* is memory-bound. On an H100, I∗=989/3.35≈295I^* = 989/3.35 \approx 295 FLOP/byte; on an A100, 312/2.04≈153312/2.04 \approx 153.

    Result 1 (when a matmul is compute-bound). A bf16 matmul (B×D)⋅(D×F′)(B \times D)\cdot(D \times F') that reads its inputs and writes its output once has I=BDF′BD+DF′+BF′I = \frac{BDF'}{BD + DF' + BF'}, which tends to BB when B≪D,F′B \ll D, F'.

    1. FLOPs: BF′BF' outputs, each a length-DD dot product: 2BDF′2BDF'.
    2. Bytes: read BDBD and DF′DF' values, write BF′BF' values, 2 bytes each: 2(BD+DF′+BF′)2(BD + DF' + BF').
    3. Divide. For B≪D,F′B \ll D, F' the weight term DF′DF' dominates the denominator, so I≈BI \approx B. ■\blacksquare
    Interactive figure: loading…

    Move a matmul along the roofline. The blue roof is the best possible FLOP/s at each intensity. The orange point is a (B×4096)⋅(4096×4096)(B \times 4096)\cdot(4096 \times 4096) matmul. With B=1B = 1 it sits at I≈1I \approx 1, far left: memory-bound, less than 1% of peak. Slide BB up: the point climbs the slanted part and reaches the flat roof between B=256B = 256 and B=512B = 512 on an H100 (the dashed ridge, I∗≈295I^* \approx 295). Compare A100 and H100: the H100 needs a larger BB.

    What this means for training. In training, BB is the number of tokens in a micro-batch, b⋅sb \cdot s, usually thousands. So every weight matmul is compute-bound. The memory-bound parts are elementwise operations (LayerNorm, GeLU, residual adds) and an unfused attention that writes the s×ss \times s scores to HBM. The fixes are kernel fusion (torch.compile) and FlashAttention. Small matmuls are a serving problem (lesson 13).

    Network and storage rooflines use the same max rule. Network: compare the bytes each GPU sends per step with its link bandwidth, and the result with the compute time (section 3.2 did this for DP). Storage: the data loader must deliver (bytes per sample) ×\times (samples per step) / (step time). Compare it with what the shared file system sustains for your job, not with its peak. If the work on different resources overlaps, the step takes about the maximum of the times; if nothing overlaps, the sum.

    3.8 Compute budget and MFU #

    Result 2 (training FLOPs per token). With Ψ≈12Lh2\Psi \approx 12Lh^2 weight parameters, training costs per token

    6Ψ+12Lsh(full attention),6Ψ+6Lsh(causal, with a kernel that skips masked blocks).6\Psi + 12Lsh \quad\text{(full attention)}, \qquad 6\Psi + 6Lsh \quad\text{(causal, with a kernel that skips masked blocks).}
    1. A weight matrix of shape m×nm \times n applied to one token costs 2mn2mn FLOPs. Over all weight matrices: 2Ψ2\Psi in the forward pass.
    2. The backward pass has two matmuls per forward matmul (gradient with respect to the input and to the weight), each of the same size: 4Ψ4\Psi. Total 6Ψ6\Psi.
    3. The attention scores have no weights. For one query token in one layer, QK⊤QK^\top costs 2sh2sh FLOPs (a dot product with ss keys over the total head width hh), and PVPV another 2sh2sh. Forward 4sh4sh, with backward 12sh12sh per layer: 12Lsh12Lsh.
    4. With a causal mask a token sees on average s/2s/2 keys: 6Lsh6Lsh. ■\blacksquare

    The ratio attention / weights is 12Lsh/(6⋅12Lh2)=s/(6h)12Lsh/(6 \cdot 12Lh^2) = s/(6h) for full attention. It is small for an LLM (s=4096s = 4096, h=4096h = 4096, causal: about 8%) but large for long sequences.

    MFU (model FLOPs utilization) == (model FLOPs per token ×\times tokens per second) / (peak FLOP/s of all GPUs). It counts only the FLOPs the model needs; recomputation does not count. Llama 3 405B reached 38–43% BF16 MFU on 8k–16k H100s (Table 4). Plan with 35–45% for a dense model, and say so. Then

    GPU-hours=D⋅(FLOPs per token)F⋅MFU⋅3600,N≥GPU-hoursdeadline in hours×goodput.\text{GPU-hours} = \frac{D \cdot (\text{FLOPs per token})}{F \cdot \text{MFU} \cdot 3600}, \qquad N \ge \frac{\text{GPU-hours}}{\text{deadline in hours} \times \text{goodput}} .

    Goodput is the fraction of the elapsed time spent on useful training. Llama 3 reports more than 90% on 16k GPUs. MFU measures how well one step uses the GPUs; goodput measures how much of the calendar is made of useful steps.

    3.9 Checkpointing and failures #

    Job failure rate. A synchronous job stops when any of its NN GPUs (or their hosts, links or software) fails. If each GPU fails independently at rate 1/Mgpu1/M_{\text{gpu}} (exponential lifetimes), the time to the first failure satisfies P(min⁡iTi>t)=∏ie−t/Mgpu=e−Nt/MgpuP(\min_i T_i > t) = \prod_i e^{-t/M_{\text{gpu}}} = e^{-Nt/M_{\text{gpu}}}. So the job's mean time between failures (MTBF) is

    M=MgpuN.M = \frac{M_{\text{gpu}}}{N} .

    Llama 3 405B saw 419 unexpected interruptions in 54 days on 16,384 GPUs, about 78% from hardware (§3.3.4, Table 5). That is M≈3.1M \approx 3.1 hours for the job, and Mgpu=54×16,384/419≈2,111M_{\text{gpu}} = 54 \times 16{,}384 / 419 \approx 2{,}111 days ≈5.8\approx 5.8 years per GPU.

    Result 3 (Young's checkpoint interval). Save a checkpoint after every τ\tau of useful work. Each checkpoint blocks training for δ\delta; failures come at random with mean time MM; a restart costs RR. If δ≪τ≪M\delta \ll \tau \ll M, the extra time per unit of useful time is

    overhead(τ)≈δτ+τ2M+RM,τ∗=2δM,overhead(τ∗)≈2δM+RM.\text{overhead}(\tau) \approx \frac{\delta}{\tau} + \frac{\tau}{2M} + \frac{R}{M}, \qquad \tau^* = \sqrt{2\delta M}, \qquad \text{overhead}(\tau^*) \approx \sqrt{\frac{2\delta}{M}} + \frac{R}{M}.

    Why this is true, in one sentence: frequent checkpoints waste time writing, rare checkpoints lose more work at each failure, and the best interval balances the two.

    1. Writing: each τ\tau of work pays δ\delta, so δ/τ\delta/\tau per unit of work.
    2. A failure hits a random point of a segment, so on average half a segment, τ/2\tau/2, of work is lost, plus the restart RR.
    3. Failures come at rate 1/M1/M, so this costs (τ/2+R)/M(\tau/2 + R)/M per unit of time.
    4. Add: overhead ≈δ/τ+τ/(2M)+R/M\approx \delta/\tau + \tau/(2M) + R/M (first order in δ/τ\delta/\tau, τ/M\tau/M, R/MR/M).
    5. Set the derivative to zero: −δ/τ2+1/(2M)=0-\delta/\tau^2 + 1/(2M) = 0, so τ∗=2δM\tau^* = \sqrt{2\delta M}.
    6. At τ∗\tau^* the first two terms are equal, δ/(2M)\sqrt{\delta/(2M)} each; their sum is 2δ/M\sqrt{2\delta/M}. ■\blacksquare
    Interactive figure: loading…

    Find the best checkpoint interval. The writing cost falls with τ\tau, the lost work grows with τ\tau; the total has its minimum where they cross, at the violet dot τ∗=2δM\tau^* = \sqrt{2\delta M}. Press Simulate failures: the magenta dots come from a Monte Carlo run with random failures and follow the formula. Now move from 1,024 to 16,384 GPUs (16×16\times): the part 2δ/M\sqrt{2\delta/M} of the overhead grows by 4×4\times, the square root of 16 (the restart part R/MR/M grows 16×16\times).

    The square root is the key point: 100 times more GPUs make MM 100 times shorter and the overhead 10 times larger. This is why large runs cut δ\delta with asynchronous checkpoints: copy the state to CPU memory in seconds, then write it to storage in the background. Young (1974) gave this first-order formula; Daly (2006) gave corrections for τ\tau close to MM.

    What a checkpoint must contain, so that a resumed run continues the same run:

    • the model weights (the fp32 master copy) and the optimizer states (AdamW mm, vv and its step counter): 12–16 bytes per parameter;
    • the learning-rate scheduler state, the global step and the tokens seen;
    • the random number generator (RNG) states of every rank (dropout and data sampling use them);
    • the data position. Simplest: make the data order a pure function of (seed, epoch, step), so the step counter is enough;
    • the gradient-scaler state (fp16 only), the config and the code version (git commit).

    How to write it. Sharded: every rank writes its own shard in parallel (PyTorch Distributed Checkpoint, DCP), which can also reload into a different number of GPUs. Atomic: write to a new directory and mark it complete only at the end, so that a crash during the write leaves the previous checkpoint valid. Retention: keep the last 2–3, plus a permanent one every few thousand steps for analysis and for rollback after a loss spike (PaLM restarted about 100 steps before each spike and skipped 200–500 batches).

    The restart path. Detect failures (collective timeouts, heartbeats, a jump in step time: one slow GPU slows every rank); restart without a human (for example Slurm --requeue); exclude bad nodes and keep spare ones; load the latest complete checkpoint. And test it: kill the job on purpose and check that the resumed loss curve matches the uninterrupted one.

    Quick check

    With Mgpu≈5.8M_{\text{gpu}} \approx 5.8 years per GPU, about how often is a 16,384-GPU synchronous job interrupted?

    3.10 The eval pipeline #

    Two loops.

    • Inside the training loop (cheap, every few hundred steps): training loss, loss on a small fixed validation set, gradient norm, learning rate, tokens/s and MFU, memory, and the time the loader makes the GPUs wait.
    • Outside the loop (asynchronous): an eval runner watches the checkpoint directory and starts eval jobs on other GPUs. Training never waits for them. Each result is stored with its key: run, step, eval-set version, eval-code version and settings (for example decoding parameters).

    Eval data. Freeze the eval sets and store their hashes. For time-dependent data (weather, finance, logs), split by time, never at random; choose checkpoints on the validation period and open the test period once, for the final report. For LLMs, check contamination (n-gram overlap between the training data and the eval sets).

    Eval cost. Count it like training: about 2Ψ2\Psi forward FLOPs per token (plus attention), times the tokens of the eval suite. Keep it to a few percent of the training compute, and check that one eval finishes before the next checkpoint arrives.

    Answers to section 1. (1) DDP: 16⋅7.5⋅109=12016 \cdot 7.5 \cdot 10^9 = 120 GB per GPU, more than one 80 GB GPU. ZeRO-3: 120/64=1.875120/64 = 1.875 GB (ZeRO paper, Figure 1; ZeRO-1 gives 31.4 GB and ZeRO-2 16.6 GB). (2) Only 1.5×1.5\times. Each all-gather moves every parameter once, like each half of the DDP all-reduce. Two all-gathers and one reduce-scatter make 3Ψ3\Psi, against 2Ψ2\Psi for DDP.

    Quick check

    A 30B model trains on 64 GPUs with 8 per node (NVLink inside, 50 GB/s per GPU between nodes). You add tensor parallelism t=8t = 8. Where should each TP group live?

    4. Worked example #

    (a) Data parallelism by hand #

    A model y^=wx\hat y = wx with one weight w=1w = 1, squared loss, mean over 4 samples: x=(1,2,3,4)x = (1, 2, 3, 4), y=(2,4,5,9)y = (2, 4, 5, 9). The gradient of L=1B∑i(wxi−yi)2\mathcal L = \frac{1}{B}\sum_i (wx_i - y_i)^2 is 2B∑i(wxi−yi) xi\frac{2}{B}\sum_i (wx_i - y_i)\,x_i.

    • Full batch. Residuals wxi−yi=(−1,−2,−2,−5)wx_i - y_i = (-1, -2, -2, -5); times xix_i: (−1,−4,−6,−20)(-1, -4, -6, -20). So ∇L=24(−31)=−15.5\nabla \mathcal L = \frac{2}{4}(-31) = -15.5.
    • Two GPUs. GPU 0 gets samples 1–2: 22(−1−4)=−5\frac{2}{2}(-1 - 4) = -5. GPU 1 gets samples 3–4: 22(−6−20)=−26\frac{2}{2}(-6 - 20) = -26.
    • All-reduce. The sum is −31-31; divided by Nd=2N_d = 2 it is −15.5-15.5 ✓. Without the division the gradient is twice too large (for SGD, a doubled learning rate).
    • Uneven shards. GPU 0 gets sample 1, GPU 1 gets samples 2–4: gradients −2-2 and −20-20. Their plain mean is −11-11: wrong. Weighted by the counts: (1⋅(−2)+3⋅(−20))/4=−15.5(1 \cdot (-2) + 3 \cdot (-20))/4 = -15.5 ✓.

    (b) The design round: pretrain a 7B model on 1T tokens #

    Prompt. "Pretrain a 7B decoder (L=32L = 32, h=4096h = 4096, a=32a = 32, s=4096s = 4096, causal) on 1 trillion tokens within two weeks. You have H100 nodes with 8 GPUs (80 GB, NVLink), 50 GB/s per GPU between nodes, and a shared file system. Global batch about 4M tokens."

    Step 1 — compute budget.

    • FLOPs per token: 6Ψ+6Lsh=4.2⋅1010+3.2⋅109≈4.52⋅10106\Psi + 6Lsh = 4.2 \cdot 10^{10} + 3.2 \cdot 10^{9} \approx 4.52 \cdot 10^{10} (attention about 7%).
    • Total: D×4.52⋅1010=4.52⋅1022D \times 4.52 \cdot 10^{10} = 4.52 \cdot 10^{22} FLOPs.
    • At 40% MFU, Feff=0.4×989≈396F_{\text{eff}} = 0.4 \times 989 \approx 396 TFLOP/s per GPU: 4.52⋅1022/3.96⋅1014≈1.14⋅1084.52 \cdot 10^{22} / 3.96 \cdot 10^{14} \approx 1.14 \cdot 10^{8} GPU-seconds ≈\approx 31,750 GPU-hours.
    • Two weeks with 90% goodput: N≥31,750/(336×0.9)≈105N \ge 31{,}750 / (336 \times 0.9) \approx 105. Take 128 GPUs (16 nodes): 10.3 days of compute, about 11.5 days of wall-clock time.

    Step 2 — memory per GPU.

    • Global batch: 4,194,3044{,}194{,}304 tokens =1,024= 1{,}024 sequences, so each of 128 GPUs processes 8 sequences per step: micro-batch b=1b = 1, m=8m = 8 accumulation steps.
    • Model states with DDP: 16Ψ=11216\Psi = 112 GB. Does not fit.
    • ZeRO-1 over Nd=128N_d = 128: 4Ψ+12Ψ/128=28+0.66≈28.74\Psi + 12\Psi/128 = 28 + 0.66 \approx 28.7 GB.
    • Activations (b=1b = 1, FlashAttention): 18.3 GB. Without FlashAttention: 104 GB, impossible.
    • Total about 47 GB of 80 GB, with room for the logits and the communication buffers.
    • Layout: ZeRO-1 data parallelism over 128 GPUs. No TP, no PP, no activation checkpointing. Why ZeRO-1 and not ZeRO-3? ZeRO-1 communicates once per optimizer step, even with accumulation. ZeRO-3 would all-gather the weights for each of the 8 micro-batches: about 8 times the traffic.

    Step 3 — roofline checks.

    • HBM: the MLP matmul (4096×4096)⋅(4096×16384)(4096 \times 4096)\cdot(4096 \times 16384) has I=4096⋅163844096+16384+16384≈1,820I = \frac{4096 \cdot 16384}{4096 + 16384 + 16384} \approx 1{,}820 FLOP/byte, far above 295. Compute-bound. Fuse the elementwise operations with torch.compile.
    • Network: one optimizer step is 8 micro-batches of 4.52⋅1010×4096/3.96⋅1014≈0.474.52 \cdot 10^{10} \times 4096 / 3.96 \cdot 10^{14} \approx 0.47 s, so 3.75 s. The gradient reduce-scatter + all-gather (bf16, 2Ψ=142\Psi = 14 GB) sends 2⋅127128⋅14≈27.82 \cdot \frac{127}{128} \cdot 14 \approx 27.8 GB per GPU. Worst case, every byte crosses the 50 GB/s link and nothing overlaps: 0.56 s, 15% of the step. A hierarchical all-reduce (reduce inside the node over NVLink, all-reduce of each GPU's 1/81/8 across nodes, gather inside the node) sends only 2⋅1516⋅148≈3.32 \cdot \frac{15}{16} \cdot \frac{14}{8} \approx 3.3 GB across nodes: 66 ms, under 2%. Note the overlap rule: the last micro-batch has 4,096 tokens, less than Feff/BW≈7,900F_{\text{eff}}/\text{BW} \approx 7{,}900, so a flat ring would not hide fully.
    • Storage: 4.2M tokens per step at 4 bytes per token ID is 17 MB per 3.75 s: about 5 MB/s. Not a risk.

    Step 4 — checkpointing and failures.

    • Content: fp32 weights + mm + vv =12Ψ=84= 12\Psi = 84 GB, plus RNG states, step, scheduler, data position and config. Sharded: 0.66 GB per GPU. Assume a blocking save of δ≈30\delta \approx 30 s.
    • M=2,111/128≈16.5M = 2{,}111/128 \approx 16.5 days ≈1.43⋅106\approx 1.43 \cdot 10^6 s. Young: τ∗=2⋅30⋅1.43⋅106≈9,250\tau^* = \sqrt{2 \cdot 30 \cdot 1.43 \cdot 10^6} \approx 9{,}250 s ≈2.6\approx 2.6 h; overhead 2δ/M≈0.65%\sqrt{2\delta/M} \approx 0.65\%. Expect less than one failure in the whole run.
    • Choose every 2,000 steps (2.1 h). Keep the last 3 and every 20,000th. Do a kill test first.
    • Contrast with 16,384 GPUs: M≈3.1M \approx 3.1 h, τ∗≈14\tau^* \approx 14 min and 7.3% overhead with δ=30\delta = 30 s; with asynchronous saves (δ=5\delta = 5 s), τ∗≈5.6\tau^* \approx 5.6 min and 3%.

    Step 5 — eval pipeline.

    • In-loop: validation loss every 500 steps on a fixed held-out set.
    • Asynchronous, at every checkpoint (about 120 over the run): a benchmark suite of about 50M tokens. Forward cost ≈(2Ψ+2Lsh)×5⋅107≈7.6⋅1017\approx (2\Psi + 2Lsh) \times 5 \cdot 10^7 \approx 7.6 \cdot 10^{17} FLOPs, 0.5 GPU-hours per checkpoint, about 63 GPU-hours in total: 0.2% of training.

    Step 6 — monitoring and trade-offs. Dashboards: loss, gradient norm, tokens/s, MFU per rank, loader wait, exposed communication time. Alerts: NaN or a loss spike, MFU 10% below normal (a slow node), checkpoint older than 4 hours. At 10 times the model (70B): model states 1.12 TB, so TP 8 inside the node plus PP or ZeRO-3 across nodes (check yourself, question 6). At 10 times the GPUs: failures every 1–2 days, so asynchronous checkpoints and automatic node exclusion.

    (c) A different case: a long-sequence ViT #

    A vision transformer for global weather fields: L=24L = 24, h=2048h = 2048, s=16,200s = 16{,}200 patch tokens per sample, full (non-causal) attention.

    • Attention share: s/(6h)=16,200/12,288≈1.32s/(6h) = 16{,}200/12{,}288 \approx 1.32, so attention is 1.32/2.32≈57%1.32/2.32 \approx 57\% of the FLOPs. The rule 6ΨD6\Psi D would underestimate the compute 2.3 times.
    • Activations with FlashAttention: 34 sbh≈1.1334\,sbh \approx 1.13 GB per layer, 27 GB per sample: the activations, not the model states (16Ψ≈1916\Psi \approx 19 GB), fill the GPU.
    • Storage: one sample (input and target state, 70 channels on a 721×1440721 \times 1440 grid, 16-bit) is about 290 MB. At 64 samples per 0.7-second step this is about 27 GB/s from the file system: here the data path, not the network, is the main risk. The same three rooflines, a different answer.

    5. In code #

    NumPy checks the numbers above: the data-parallel gradients of 4(a), the ZeRO table, the Megatron split, the bubble and Young's interval. The notebook has these cells and the exercises.

    import numpy as np
    
    # (a) data parallelism = one large batch: y_hat = w x, squared loss, mean over the batch
    x, y, w = np.array([1., 2., 3., 4.]), np.array([2., 4., 5., 9.]), 1.0
    grad = lambda xs, ys: 2 * np.mean((w * xs - ys) * xs)
    full = grad(x, y)
    g0, g1 = grad(x[:2], y[:2]), grad(x[2:], y[2:])            # two GPUs, two samples each
    print(full, g0, g1, (g0 + g1) / 2)                          # -15.5 -5.0 -26.0 -15.5
    u0, u1 = grad(x[:1], y[:1]), grad(x[1:], y[1:])            # uneven shards: 1 and 3 samples
    print((u0 + u1) / 2, (1 * u0 + 3 * u1) / 4)                 # -11.0 (wrong) -15.5 (count-weighted)
    
    # (b) model-state memory per GPU (GB) for mixed-precision AdamW, K = 12
    def zero_gb(psi, n):
        return [16 * psi, 4 * psi + 12 * psi / n, 2 * psi + 14 * psi / n, 16 * psi / n]
    print([round(v / 1e9, 2) for v in zero_gb(7.5e9, 64)])     # [120.0, 31.41, 16.64, 1.88]
    
    # (c) Megatron: split the first MLP weight by columns, the second by rows
    gelu = lambda z: 0.5 * z * (1 + np.tanh(np.sqrt(2 / np.pi) * (z + 0.044715 * z**3)))
    rng = np.random.default_rng(0)
    X, A, B = rng.normal(size=(4, 8)), rng.normal(size=(8, 32)), rng.normal(size=(32, 8))
    Z = gelu(X @ A) @ B                                          # the unsplit MLP
    Z1 = gelu(X @ A[:, :16]) @ B[:16]                            # GPU 0: its 16 columns of A, 16 rows of B
    Z2 = gelu(X @ A[:, 16:]) @ B[16:]                            # GPU 1
    print(np.allclose(Z, Z1 + Z2))                               # True: one all-reduce (a sum) at the end
    P1, P2 = X[:, :4] @ A[:4], X[:, 4:] @ A[4:]                  # row split of A instead
    print(np.allclose(gelu(X @ A), gelu(P1) + gelu(P2)))         # False: GeLU of a sum is not a sum of GeLUs
    
    # (d) pipeline bubble and (e) Young's interval
    p, m = 4, 8
    print(round((p - 1) / (m + p - 1), 4))                       # 0.2727
    M_gpu = 54 * 16384 / 419                                     # days per GPU between failures (Llama 3)
    for n in (128, 16384):
        M = M_gpu * 86400 / n                                    # job MTBF in seconds
        for delta in (30, 5):
            tau = np.sqrt(2 * delta * M)
            print(n, delta, round(M / 3600, 1), round(tau / 60, 1), round(100 * np.sqrt(2 * delta / M), 2))
    # 128 30 395.9 154.1 0.65      (GPUs, delta in s, M in h, tau* in min, overhead in %)
    # 128 5 395.9 62.9 0.26
    # 16384 30 3.1 13.6 7.34
    # 16384 5 3.1 5.6 3.0
    

    A Monte Carlo check of Young's formula, with random failures (time in hours):

    import numpy as np
    
    M, delta, R = 24.0, 0.05, 0.1         # hours: job MTBF, checkpoint cost, restart cost
    work = 2000.0                         # hours of useful training
    
    
    def simulate(tau, rng):
        """Elapsed hours to finish `work`, with a checkpoint after every tau hours of work."""
        t, done, next_fail = 0.0, 0.0, rng.exponential(M)
        while done < work:
            seg = min(tau, work - done) + delta            # work, then write a checkpoint
            if t + seg <= next_fail:                       # the segment finishes: progress is saved
                t, done = t + seg, done + seg - delta
            else:                                          # failure: lose the segment, pay the restart
                t = next_fail + R
                next_fail = t + rng.exponential(M)
        return t
    
    
    rng = np.random.default_rng(0)
    taus = np.array([0.25, 0.5, 1.0, 1.55, 2.5, 4.0, 8.0])
    sim = np.array([np.mean([simulate(tau, rng) / work - 1 for _ in range(40)]) for tau in taus])
    formula = delta / taus + taus / (2 * M) + R / M
    print(np.round(100 * sim, 1))       # [21.2 11.8  7.8  7.2  8.2 10.9 19.8]
    print(np.round(100 * formula, 1))   # [20.9 11.5  7.5  6.9  7.6 10.  17.7]
    print(round(np.sqrt(2 * delta * M), 2), taus[np.argmin(sim)])   # 1.55 1.55
    

    The minimum is at τ∗=1.55\tau^* = 1.55 h in both. The simulation is a little above the first-order formula, and more so for large τ\tau, where τ≪M\tau \ll M holds less well.

    6. Where it shows up #

    • LLM pretraining. Every large run is 3-D or 4-D parallel, with an MFU around 40% and failures every few hours at the largest scale (Llama 3, §3.3). The data-parallel size sets the global batch, and the global batch sets the learning rate.
    • Fine-tuning. FSDP with LoRA or QLoRA: the frozen base model is sharded, and only the small adapters have gradients and Adam states (lesson 11).
    • ML for science. Weather, climate and other scientific foundation models often have moderate parameter counts but very long token sequences and large samples. Attention FLOPs, activations and the input pipeline then matter more than ZeRO (section 4(c)). HPC clusters add node-hour allocations, job time limits and a shared parallel file system.
    • RL post-training. An inference engine runs inside the training loop to generate rollouts, so the design mixes the training numbers of this lesson with the serving numbers of lesson 13.

    7. Common mistakes #

    • "2 bytes per parameter" for training. That is bf16 inference. Training with AdamW needs 16 bytes per parameter, plus the activations.
    • "ZeRO-3 shards activations." ZeRO-1/2/3 shard model states only. Activations need checkpointing, a smaller micro-batch, TP with sequence parallelism, or context parallelism.
    • "ZeRO-3 costs NdN_d times the communication." It is 3Ψ3\Psi against 2Ψ2\Psi.
    • Gradient accumulation with ZeRO-2/3. ZeRO-2 reduce-scatters after every micro-batch, and ZeRO-3 also all-gathers the weights for every micro-batch: about mm times the traffic of ZeRO-1.
    • Sum instead of mean. Without the division by NdN_d the gradient is NdN_d times too large. With uneven shards, weight by the counts.
    • TP across nodes. Its per-layer all-reduces need NVLink-class bandwidth.
    • Too few micro-batches in a pipeline. With m<pm < p most stages wait; aim for m≥4pm \ge 4p and use 1F1B so that the activations do not grow with mm.
    • 6ΨD6\Psi D at long sequence lengths. It drops the attention term; for s≫6hs \gg 6h it can be off by a factor of 2 or more.
    • Planning at peak FLOP/s. Use 35–45% MFU for a dense model, and say so.
    • Weights-only checkpoints, written in place, never tested. Without optimizer state, RNG states and data position a "resume" is a different run; a crash during an in-place write destroys the only copy; an untested restart fails when you need it.

    8. Check yourself #

    1. A 70B model, mixed-precision AdamW, 64 GPUs with 80 GB each. Model-state memory per GPU with ZeRO-1, ZeRO-2 and ZeRO-3? Which ones fit?
      AnswerZeRO-1: 4Ψ+12Ψ/64=280+13.1≈2934\Psi + 12\Psi/64 = 280 + 13.1 \approx 293 GB. ZeRO-2: 2Ψ+14Ψ/64=140+15.3≈1552\Psi + 14\Psi/64 = 140 + 15.3 \approx 155 GB. ZeRO-3: 16Ψ/64=17.516\Psi/64 = 17.5 GB. Only ZeRO-3 fits (or add TP/PP); the activations come on top.
    2. Why does ZeRO-2 send no more data than DDP?
      AnswerA ring all-reduce already is a reduce-scatter plus an all-gather. ZeRO-2 keeps the two halves and puts the optimizer step on the shard between them: reduce-scatter the gradients (Ψ\Psi), update the own shard, all-gather the parameters (Ψ\Psi). Total 2Ψ2\Psi, the same as DDP.
    3. p=8p = 8 stages, m=32m = 32 micro-batches. What is the bubble with GPipe and with 1F1B, and with interleaving (v=2v = 2)? How many micro-batches of activations does stage 0 hold?
      AnswerGPipe and 1F1B: 7/39≈17.9%7/39 \approx 17.9\% of the step. Interleaved: 7/71≈9.9%7/71 \approx 9.9\%. Stage 0 holds 32 micro-batches with GPipe and at most 8 with 1F1B.
    4. DDP across nodes with F=400F = 400 TFLOP/s achieved per GPU and BW=50\text{BW} = 50 GB/s per GPU. How many tokens per GPU per step hide the all-reduce? Does the answer change for a model 10 times larger?
      AnswerT≥F/BW=8,000T \ge F/\text{BW} = 8{,}000 tokens. No: both the backward FLOPs (4ΨT4\Psi T) and the all-reduce bytes (4Ψ4\Psi) are proportional to Ψ\Psi, so Ψ\Psi cancels.
    5. A job on 1,024 GPUs (Mgpu≈2,111M_{\text{gpu}} \approx 2{,}111 days), blocking checkpoint δ=60\delta = 60 s, restart R=10R = 10 min. Give MM, τ∗\tau^* and the overhead.
      AnswerM=2,111⋅86,400/1,024≈1.78⋅105M = 2{,}111 \cdot 86{,}400/1{,}024 \approx 1.78 \cdot 10^5 s ≈49.5\approx 49.5 h. τ∗=2⋅60⋅1.78⋅105≈4,620\tau^* = \sqrt{2 \cdot 60 \cdot 1.78 \cdot 10^5} \approx 4{,}620 s ≈77\approx 77 min. Overhead 2δ/M≈2.6%\sqrt{2\delta/M} \approx 2.6\% plus R/M≈0.34%R/M \approx 0.34\%: about 2.9%.
    6. Pretrain a 70B decoder (L=80L = 80, h=8192h = 8192, s=8192s = 8192) on 1,024 H100s (128 nodes of 8). Why can it not use ZeRO-1 alone, and what layout would you propose?
      AnswerZeRO-1 keeps full bf16 weights and gradients: 4Ψ=2804\Psi = 280 GB per GPU. Propose TP 8 inside each node (states and activations divided by 8), PP 4 across nodes and DP 32 with ZeRO-1. Each GPU holds Ψ′=Ψ/(t p)≈2.2⋅109\Psi' = \Psi/(t\,p) \approx 2.2 \cdot 10^9 parameters, so its model states are 4Ψ′+12Ψ′/32≈8.75+0.82≈9.64\Psi' + 12\Psi'/32 \approx 8.75 + 0.82 \approx 9.6 GB. The TP communication/compute ratio is about 0.63 on NVLink; the pipeline bubble with m=32m = 32 is 3/35≈8.6%3/35 \approx 8.6\%.
    Recall cardsAnswer in your head first, then reveal. Add the cards to your review deck to see them again just before you would forget.
    1. How many bytes per parameter does mixed-precision AdamW keep, and how are they split?

    2. How many bytes does each GPU send in a ring all-reduce of nn bytes over pp GPUs?

    3. Why does data parallelism with equal shards give the full-batch gradient?

    4. When does the DDP all-reduce hide behind the backward pass?

    5. What are the model states per GPU with ZeRO-3 on NdN_d GPUs?

    6. Why does ZeRO-3 send 1.5×1.5\times the data of DDP?

    7. How many all-reduces does a Megatron TP layer (attention + MLP) need per training step?

    8. What is the pipeline bubble fraction with pp stages and mm micro-batches?

    9. What is the mean time between failures of a job on NN GPUs?

    10. Give Young's checkpoint interval and its overhead.

    Open the review →

    9. Interview angle #

    Recognize the task from the wording.

    The prompt says Start with
    "will it fit?", "out of memory" the memory table: 16Ψ16\Psi, activations 34 sbh34\,sbh per layer, then ZeRO stage, TP, checkpointing
    "how would you train this on N GPUs?" DP (with ZeRO) first; TP inside a node; PP across nodes only if needed
    "the step is slow", "low GPU utilization" rooflines per resource: HBM, network (exposed communication), data loader
    "communication is the bottleneck" T≥F/BWT \ge F/\text{BW}; hierarchical collectives; ZeRO-1 instead of ZeRO-3 with accumulation
    "how long will training take?", "how many GPUs?" FLOPs per token ×D\times D, MFU 35–45%, goodput 90%
    "nodes fail", "a three-month run" M=Mgpu/NM = M_{\text{gpu}}/N, Young's τ∗\tau^*, asynchronous sharded checkpoints, restart path
    "very long sequences", "images or fields as tokens" attention FLOPs 12Lsh12Lsh, FlashAttention, context parallelism, the data path

    The routine to say aloud (about 30 seconds): "Let me first get the numbers: parameters, layers, width, sequence length, tokens, GPUs per node, memory, link bandwidths and the deadline. Then I compute the FLOPs and GPU-hours, then the memory per GPU, then I choose the smallest layout that fits, and I check compute, network and storage with rooflines. Last, failures and evals."

    A plan for a 45-minute design round.

    Minutes Step What you write down
    0–5 Clarify model shape, data, token budget, deadline, hardware, what "done" means
    5–10 Compute budget FLOPs per token, total FLOPs, GPU-hours at an assumed MFU, number of GPUs
    10–15 Memory model states, activations, what must be sharded
    15–22 Layout DP / ZeRO / TP / PP / CP degrees and their mapping to nodes
    22–27 Roofline checks HBM, network (is communication hidden?), storage (is the loader fast enough?)
    27–33 Reliability checkpoint content and size, interval, restart path
    33–39 Eval pipeline in-loop signals, asynchronous checkpoint evals, eval sets, cost
    39–45 Monitoring, trade-offs dashboards and alerts; what changes at 10× the model, data or cluster

    Typical questions and short model answers.

    1. "Will a 13B model train on one node of 8 × 80 GB?" — "The model states are 16×13=20816 \times 13 = 208 GB, so DDP does not fit. ZeRO-3 over 8 GPUs gives 26 GB per GPU; ZeRO-2 gives 2⋅13+14⋅13/8≈48.82 \cdot 13 + 14 \cdot 13/8 \approx 48.8 GB. Activations with FlashAttention at s=4096s = 4096, h=5120h = 5120, 40 layers: 34 sbh≈0.7134\,sbh \approx 0.71 GB per layer, 28.5 GB per sequence. So ZeRO-3 with one sequence per GPU fits at about 55 GB; with ZeRO-2 I would add activation checkpointing. NVLink makes the ZeRO-3 all-gathers cheap inside the node."
    2. "Explain ZeRO stages 1, 2 and 3." — "All three are data parallel. Stage 1 shards the optimizer states, stage 2 also the gradients, stage 3 also the parameters. Stages 1 and 2 send the same 2Ψ2\Psi as DDP, because they split the all-reduce into a reduce-scatter before the update and an all-gather after it. Stage 3 gathers the weights twice per step: 3Ψ3\Psi, 1.5 times DDP, in exchange for 16Ψ/Nd16\Psi/N_d memory."
    3. "Why does tensor parallelism stay inside a node?" — "It needs 4 all-reduces of the activations per layer, on the critical path. The ratio of communication to compute is (t−1)F/(3h BW)(t-1)F/(3h\,\text{BW}): about 0.6 on NVLink for h=8192h = 8192, but about 5.6 at 50 GB/s between nodes."
    4. "What is the pipeline bubble and how do you reduce it?" — "With pp stages and mm micro-batches, the first stage waits p−1p-1 slots for the pipeline to fill and the last waits to drain: a fraction (p−1)/(m+p−1)(p-1)/(m+p-1). More micro-batches shrink it, m≥4pm \ge 4p gives under 20%. 1F1B keeps the activations at pp micro-batches instead of mm; interleaving divides the bubble by vv."
    5. "How often should you checkpoint a 16k-GPU job?" — "The job fails about every 3 hours. Young's interval is 2δM\sqrt{2\delta M}: with a 30-second blocking save, about 14 minutes and 7% overhead. That is too much, so I make the save asynchronous (copy to CPU, write in the background): with 5 seconds, about 6 minutes and 3%."
    6. "MFU dropped from 42% to 30% after a restart. What do you check?" — "A slow node first: compare per-rank step times, run GPU and network health checks, exclude the node. Then the data path after the restart (a cold cache), and exposed communication in a profile."

    Typical follow-ups. "How does FSDP overlap communication?" (it prefetches the next unit's all-gather and runs reduce-scatters while the backward pass continues). "What happens when you restart on a different number of GPUs?" (sharded checkpoints are resharded at load time; the global batch must stay the same, so change the accumulation steps). "The loss spikes at step 80k?" (roll back to a permanent checkpoint before the spike, skip the batches around it, then lower the learning rate or tighten clipping if it repeats).

    10. Practice #

    The notebook for this lesson runs in Colab, in NumPy. Each exercise has tests against a reference:

    1. ring_allreduce: simulate the ring on pp GPUs (a list of arrays); check the sum and that each GPU sends 2(p−1)/p⋅n2(p-1)/p \cdot n values.
    2. data_parallel_grad: the count-weighted mean of shard gradients equals the full-batch gradient of a linear model, also for uneven shards.
    3. zero_memory: the per-GPU model states of DDP and ZeRO-1/2/3, checked against the ZeRO paper's 7.5B example.
    4. pipeline_schedule: simulate GPipe and 1F1B and return the step time, the bubble and the peak in-flight micro-batches; compare with (p−1)/(m+p−1)(p-1)/(m+p-1).
    5. young_interval: find the best interval numerically and by Monte Carlo, and compare with 2δM\sqrt{2\delta M}.

    Timed practice (25 minutes each): Sasha Rush's LLM-Training-Puzzles, in order: "Standard Training", "Gradient Accumulation", "Distributed Data Parallel", "Weight-Sharded Data Parallel", "Fully-Sharded Data Parallel", "Pipeline Parallel", "GPipe Schedule", "Pipeline + FSDP". Predict the time and peak memory of each puzzle before you code it. Then two design drills of 45 minutes: the worked example of section 4(b) from memory, and question 6 of section 8 as a full design.

    Say it aloud drill. Without notes, explain in 2 minutes why ZeRO-1 and ZeRO-2 send as much as DDP and ZeRO-3 sends 1.5 times as much. Then derive Young's interval in 1 minute.

    11. Watch and read #

    Data parallelism and the communication primitives behind it, including the ring all-reduce; watch it next to sections 2 and 3.2.
    Open on YouTube · the player loads from youtube-nocookie.com when you press play
    How FSDP shards the model states, and when it gathers and frees the weights; watch it next to section 3.3.
    Open on YouTube · the player loads from youtube-nocookie.com when you press play
    The first author of the ZeRO paper explains ZeRO, 3D parallelism and the DeepSpeed tools built on them.
    Open on YouTube · the player loads from youtube-nocookie.com when you press play