cs336 / lessons/09 · model parallelismlesson 10 / 20

Part II — Systems

Model parallelism — tensor, pipeline, the 3D mesh

Lesson 08 sharded the storage: ZeRO-3 / FSDP cut the 16-bytes/param training state by the data-parallel degree, so a 7B that overflowed one 80 GB GPU now fits across eight. But every FSDP rank still executes the whole model on its own slice of the batch — it gathers each layer's params just-in-time, runs the full forward, then frees them. That breaks the moment a single layer is too big to compute on one device, the activations for one layer overflow, or the model is simply larger than any GPU even after gathering. Data parallelism can shrink where weights live; it can never split where the math runs. This lesson splits the model itself.

The previous step left this broken
FSDP's trick is "shard the parameters, but gather each layer back to full size right before you compute it." That gather is the catch: at the instant of the matmul, one rank holds the entire layer and runs the entire forward for its batch shard. So the peak compute footprint of a single layer — its full weight matrix plus its activations for B·T tokens — must still fit on one device. A 70B's SwiGLU MLP block (3 matrices, intermediate ≈3.5d → ≈705M params, ~1.4 GB in bf16) is fine; but push d, the batch, or the context far enough and one layer's transient footprint alone overflows the 80 GB HBM, and FSDP has no answer because it never splits the computation, only the resident bytes between computations. You need to cut the matmul across devices, and cut the layer stack across devices.
Linear position
Forced by: the per-layer compute and activations exceed one device, and the model can be larger than any single GPU — DP/FSDP shard storage but never split the computation itself.
New idea: split the model along two axes — tensor parallelism (cut each matmul across GPUs; one all-reduce per attention/MLP block per forward and per backward; so bandwidth-hungry it must stay inside a node on NVLink) and pipeline parallelism (cut the layer stack into stages across nodes; feed micro-batches with a 1F1B schedule to shrink the idle bubble), plus sequence/context parallelism (cut along T with ring attention for long context) — composed with DP into a 3D mesh DP × TP × PP.
Forces next: this lets you train an arbitrarily large dense model, but capability tracks total parameters while you pay 6ND FLOPs through every one of them — can you add parameters without paying the FLOPs? (MoE, lesson 10.)
The plan
Five moves. (1) Tensor parallelism the Megatron way — column-parallel then row-parallel so one all-reduce covers a whole MLP/attention block; split heads; price the comm and prove it must stay intra-node. (2) Pipeline parallelism — stages, micro-batches, the bubble fraction (p−1)/(m+p−1), the 1F1B schedule, and why PP rides cheap point-to-point links across nodes. (3) Sequence/context parallelism and ring attention for long T. (4) Assemble the 3D mesh with a worked 64-GPU layout (DP 4 × TP 8 × PP 2) and the rules of thumb. (5) Drive the mesh planner and watch TP spanning a node-boundary turn the plan red.

1 · Tensor parallelism — split the matmul itself

The unit of work inside a Transformer is a matmul: Y = X·A with A a weight matrix. Tensor parallelism (TP), introduced by Megatron-LM, splits A across t GPUs so each holds a slice, computes a partial result, and the partials are combined by a collective. The art is arranging the cuts so that two matmuls in a row — which every Transformer block has — cost exactly one communication instead of two.

Column-parallel then row-parallel. Take the MLP, two matmuls back to back: Z = GELU(X·A)·B. Split the first matrix A by its columns into [A₁ A₂ … At]; GPU i computes GELU(X·Ai) — a full slice of the hidden activation, no communication needed because GELU is elementwise. Now split the second matrix B by its rows into [B₁; B₂; …; Bt] so that GPU i already holds the matching activation slice and computes the partial product GELU(X·Ai)·Bi. The full output is the sum of those partials — one all-reduce across the t GPUs, and you're done. Two matmuls, one collective.

Z  =  Σi=1..t GELU(X·Ai)·Bi    ← one all-reduce sums the partials

Attention takes the identical pattern. The QKV projection is column-parallel: assign whole attention heads to GPUs, so GPU i owns a subset of heads and computes their Q, K, V and the full per-head attention locally — heads are independent, so no cross-GPU comm inside the softmax. The output projection Wo is then row-parallel, and a single all-reduce sums the per-head contributions back into the residual stream. So the canonical Megatron block costs two all-reduces in the forward (one for attention, one for the MLP) and, by symmetry, two in the backward — four collectives per layer, each over the t TP ranks.

  MLP block, t=2 tensor-parallel GPUs
                 X  (replicated on both)
        ┌──────────────┴──────────────┐
   GPU0: X·A₁ (cols 0..h/2)       GPU1: X·A₂ (cols h/2..h)
        GELU (elementwise, local)      GELU (local)
   GPU0: (·)·B₁ (rows 0..h/2)     GPU1: (·)·B₂ (rows h/2..h)
        └──────────────┬──────────────┘
              ALL-REDUCE (sum partials)  ← the one collective
                        Z  (replicated)

Why TP must stay inside a node. That all-reduce is not cheap and it sits on the critical path — the matmul cannot finish until the bytes arrive. Each forward all-reduce of the residual stream moves on the order of B·T·d elements; with four collectives per layer over dozens of layers, TP communicates an enormous amount per step, and it is synchronous (no useful overlap — the next op depends on the result). The only way this is affordable is on an intra-node fabric: NVLink/NVSwitch gives ~900 GB/s per GPU between the 8 GPUs in a node, versus ~25–50 GB/s per GPU over InfiniBand between nodes — roughly a 20–40× gap. Span TP across a node boundary and every block stalls on the slow link; MFU collapses. Hence the iron rule: TP degree ≤ GPUs per node (typically ≤ 8). TP is your "shrink one layer below one GPU" tool, and it is a within-node tool only.

column-parallel
split weight by output columns; each GPU makes a slice of the activation. No comm after (GELU/head-attn are local).
row-parallel
split next weight by input rows to match the slice; output is the sum of partials → one all-reduce.
per-layer cost
2 all-reduces forward + 2 backward, each ≈ B·T·d elements over t ranks — synchronous, on the critical path.
the rule
keep TP ≤ GPUs/node so the collectives ride NVLink (~900 GB/s), not InfiniBand (~25–50 GB/s).

2 · Pipeline parallelism — split the layer stack

TP shrinks a layer; it does nothing about a model with more layers than fit. Pipeline parallelism (PP) cuts the L-layer stack into p contiguous stages, one group of layers per device (or per node). A batch flows stage 0 → stage 1 → … → stage p−1 for the forward, then back through for the backward. The only communication is the activation tensor handed from one stage to the next — a single point-to-point send of shape (B, T, d), no collective. That makes PP comm tiny and, crucially, tolerant of slow links: it is the axis you run across nodes.

The problem PP introduces is idleness. If you push one batch through naively, stage 1 sits idle until stage 0 finishes, stage 2 waits on stage 1, and so on — at any instant only one of p stages is busy. The fix is to chop the batch into m micro-batches and stream them, so stages fill up like an assembly line. But the line still has a startup and drain: the first micro-batch must traverse all p stages before the pipeline is full, and the last must drain out. That dead time is the bubble:

bubble fraction  ≈  (p − 1) / (m + p − 1)

The intuition: there are p−1 stage-times of fill and p−1 of drain spread over a schedule of m + p − 1 stage-times of length — so the wasted fraction shrinks as you raise m. With p = 4 stages and m = 4 micro-batches the bubble is 3/7 ≈ 43% — brutal. Raise to m = 32 and it falls to 3/35 ≈ 8.6%; at m = 64, 3/67 ≈ 4.5%. The rule of thumb is m ≳ 4p to keep the bubble in the ~15–20% range; for under ~10% you need m ≳ 8–10p. The cost of more micro-batches is more in-flight activations to store, so the bubble trades against activation memory — another instance of the course's spend-the-cheap-resource move.

naive 1-batchone stage busy at a time → (p−1)/p of the machine idle. For p=4 that's 75% waste.
m micro-batches (GPipe)stream them so stages overlap; bubble drops to (p−1)/(m+p−1). All m forwards, then all m backwards.
1F1B (PipeDream)interleave: once warmed up, each stage does one forward then one backward, freeing each micro-batch's activations as early as possible → far lower peak activation memory, same bubble.

1F1B (one-forward-one-backward) is the schedule everyone ships. GPipe runs all m forwards and only then all m backwards, so every micro-batch's activations are alive at the peak — memory scales with m. 1F1B instead, after a short warmup, alternates a forward and a backward on each stage, so a micro-batch's activations are released as soon as its backward runs. Same bubble fraction, dramatically lower peak activation memory — which is what lets you crank m high enough to make the bubble negligible. Interleaved 1F1B goes further, giving each device several non-contiguous stage chunks to shave the bubble further at the cost of more point-to-point sends.

3 · Sequence and context parallelism — split along T

TP splits the hidden dimension d and the heads; PP splits the layer index. Neither helps when the sequence T is the thing that overflows — a 128k-token context produces activations ∝ B·T·d·L and an attention term ∝ B·h·T² that no amount of head-splitting or stage-splitting shrinks, because each token's activation still lives somewhere.

Sequence parallelism (SP) is a cheap companion to TP: the parts of a block that TP leaves replicated and elementwise — the LayerNorms, dropout, the residual adds — are split along T across the same TP ranks, turning TP's all-reduces into all-gather + reduce-scatter pairs of the same total volume while cutting the activation memory of those regions by t. It is nearly free and ships with Megatron TP by default.

Context parallelism with ring attention tackles the attention itself. Shard the sequence into c chunks, one per GPU; each GPU holds its chunk's Q, K, V. To compute attention, the K,V chunks are passed around a ring: GPU i attends its local Q to the K,V it currently holds, computes a partial using the same online-softmax running (max, sum) recurrence from FlashAttention (lesson 7), then forwards that K,V block to its neighbor and receives the next — after c steps every Q has seen every K,V, and the T×T score matrix was never materialized anywhere. Communication (one K,V block per step, point-to-point) overlaps with the local attention compute, so for long T the ring is nearly free. This is how frontier models train at 128k–1M token contexts without any single device holding the whole sequence.

4 · The 3D mesh — composing DP × TP × PP

These axes are orthogonal, so you multiply them. Arrange all your GPUs into a logical 3D mesh: every GPU has a coordinate (dp, tp, pp), and the total must factor exactly: G = DP · TP · PP. Each axis uses the collective it can afford on the link it's placed on. The placement rules fall straight out of the comm costs derived above:

TP — innermost
heaviest, synchronous comm → place inside a node on NVLink. TP ≤ GPUs/node.
PP — across nodes
tiny point-to-point activation sends, latency-tolerant → put the slow inter-node hops here.
DP — outermost
one all-reduce/reduce-scatter of gradients per step, fully overlappable with backward → the cheapest axis, spread it widest.
SP/EP — riders
SP shares the TP group (split T on the same ranks); expert parallelism (lesson 10) adds an all-to-all axis for MoE.

A worked 64-GPU layout. Take 8 nodes of 8 H100s each, 64 GPUs total, training a model too big for any one node. A standard factorization is DP 4 × TP 8 × PP 2 = 64:

axisdegreeplacementcollective / steplink it rides
TP8the 8 GPUs within a node2 all-reduce fwd + 2 bwd (≈B·T·d each)NVLink ~900 GB/s
PP22 nodes form one pipeline (stage 0 / stage 1)point-to-point activation hand-offInfiniBand ~25–50 GB/s
DP44 such (TP×PP) groups, each a full model replica1 grad all-reduce (overlapped)InfiniBand, overlapped

Read it as a hierarchy: the 8 GPUs in a node are one TP group (one layer's matmuls split 8 ways on NVLink); two nodes chain into a 2-stage pipeline (16 GPUs holding the full depth, activations handed over IB); and four of those 16-GPU model-groups form a 4-way data-parallel cluster, each chewing a quarter of the global batch and all-reducing gradients at the step boundary. Every GPU holds 1/(TP·PP) of the parameters and 1/DP of the batch. The rules of thumb: fill TP first up to GPUs/node, then PP across nodes to fit depth, then put all remaining GPUs into DP for throughput — and prefer more DP over more PP when the model already fits, because DP comm overlaps and PP adds a bubble.

Interaction with FSDP. The DP axis does not have to be plain replication — it is exactly where ZeRO/FSDP from lesson 8 plugs in. Run ZeRO-1/2/3 along the DP dimension: the optimizer state, gradients, and (for ZeRO-3) parameters are sharded across the DP ranks of the mesh, gathered just-in-time, while TP and PP split each replica's compute. This composition — FSDP as the outer dimension, TP×PP as the inner model split — is the standard frontier recipe: TP shrinks the layer below one GPU, PP fits the depth across nodes, and FSDP-DP both scales throughput and shards the leftover 16-bytes/param state across the widest, cheapest axis. (The same cluster topology shows up again in post-training; see reinforcement_learning · the RL/post-training topology lessons for how rollout, reward, and trainer workers are laid out across a similar mesh.) The fused-optimizer step that runs on each rank is detailed in gpu_kernels · 30 (fused optimizers & the training step).

5 · Drive the mesh planner

Set total GPUs, GPUs per node, model size N, and layer count L, then pick a DP × TP × PP factorization. The planner checks it multiplies to the total, computes the per-GPU memory (state sharded by TP·PP·DP under FSDP), the PP bubble at a sensible micro-batch count, and flags the comm-cost regime. The configuration that breaks: push TP past GPUs/node and the plan turns red — your tensor-parallel all-reduces would cross the node boundary onto InfiniBand and the run would crawl. Prove three things: (a) TP=16 with 8 GPUs/node is invalid; (b) at a fixed GPU count the 16N state is sharded across all three axes, so per-GPU memory depends only on the product DP·TP·PP — raising PP (and dropping DP to compensate) inflates the bubble while memory holds flat; (c) the corollary — shifting that split back from PP into DP lowers the bubble at no memory cost, which is why you prefer DP over PP once the model fits.

3D-mesh planner — factor your cluster into DP × TP × PP
The product DP·TP·PP must equal total GPUs or the plan is invalid. TP must stay ≤ GPUs/node (NVLink) — exceed it and the tensor all-reduces spill onto the slow inter-node fabric and the plan goes red. Per-GPU memory is the 16-bytes/param state sharded across all three axes (FSDP on the DP axis); the bubble uses m = 4·PP micro-batches. Watch TP=GPUs/node·2 break it.
DP·TP·PP = 64
Plan valid?
Per-GPU state (GB)
PP bubble (m=4·PP)
Comm regime
layout OK

The planner makes the whole hierarchy legible at a glance: the only configurations that stay green are the ones where TP fits inside a node, PP carries the cross-node depth, and DP soaks up the rest — exactly the placement the comm costs of §1–§4 forced.

Failure modes & checklist

Failure modes

  • TP spanning a node boundary. Setting TP=16 on 8-GPU nodes so the all-reduce crosses InfiniBand. Signal: MFU craters to single digits and per-step time is dominated by the attention/MLP collectives, not the matmuls.
  • Too few micro-batches. Running PP with m≈p. Signal: ~40%+ of pipeline time is bubble; GPUs show long synchronized idle gaps at the start and end of each step.
  • Over-using PP when DP would do. Adding pipeline stages on a model that already fits. Signal: a persistent bubble tax and rising activation memory, where plain (FSDP-)DP would have overlapped its comm and had no bubble.
  • Forgetting the factorization must be exact. DP·TP·PP ≠ total GPUs. Signal: launch fails to assign ranks, or some GPUs sit unused / are over-subscribed.
  • Long-context OOM with no context parallelism. Stretching T without SP/ring attention. Signal: the O(B·h·T²) attention activations overflow even though weights are well-sharded.

Checklist

  • Cap TP at GPUs/node (≤8) so every TP all-reduce rides NVLink, never the inter-node fabric.
  • Run PP across nodes — its only comm is a point-to-point activation hand-off, which tolerates the slow link.
  • Keep m ≳ 4·PP micro-batches and use 1F1B to hold the bubble in the ~15–20% range without blowing up activation memory; push to m ≳ 8–10·PP for under ~10%.
  • Make DP the outermost, widest axis and fold FSDP/ZeRO into it to shard the 16-bytes/param state.
  • Add SP for free with TP, and ring attention when T is what overflows, not d or L.

Checkpoint

Try it
Open the planner at 64 GPUs, 8/node, 70B, and the default DP 4 × TP 8 × PP 2 — confirm it's valid and green, and read the per-GPU state. Now (a) bump TP to 16 (leave DP·TP·PP product matching by dropping DP to 2): the product still equals 64, but the plan turns red — say in one sentence why TP=16 is illegal on 8-GPU nodes. (b) Reset, then shift the split from DP into PP (DP 1 × TP 8 × PP 8): per-GPU state stays flat (the 16N state is sharded across all three axes, so only the product 64 sets it) while the bubble KPI climbs from ~11% to ~18% — read both KPIs and explain why trading DP for PP buys you a bubble for no memory gain when the model already fits. (c) By hand: with p=8 stages, how many micro-batches m do you need to get the bubble under 10%? Finally: a 175B model on 128 GPUs (8/node) — give a valid DP × TP × PP that keeps TP intra-node and explain your PP choice.

Where this points next

You can now train a model of any size: TP shrinks a layer below one GPU, PP fits the depth across nodes, context parallelism handles arbitrary T, and FSDP-DP shards the leftover state across the widest axis. There is no longer a dense model too big to train. But notice what you are paying for that reach — the budget identity C ≈ 6ND from lessons 0 and 05 says every token's forward and backward run through every parameter. Capability scales with total parameters; cost scales with the same total parameters, because each one costs FLOPs on every token. The two are welded together, and the budget caps both at once. The obvious question — the one that breaks this lesson's victory — is whether you can decouple them: grow the parameter count without growing the FLOPs each token pays. You can, by making each token use only a sparse slice of the parameters. That is 10 · Mixture of Experts — parameters without the FLOPs.

Takeaway
Data parallelism (lesson 8) shards storage but every rank still executes the whole model; when a layer's compute or activations exceed one device, or the model exceeds any GPU, you must split the model itself. Tensor parallelism cuts each matmul across GPUs — column-parallel then row-parallel so a whole attention or MLP block costs exactly one all-reduce per forward and one per backward, with attention heads divided across ranks; that comm is synchronous and on the critical path, so TP must stay inside a node on NVLink (TP ≤ GPUs/node). Pipeline parallelism cuts the layer stack into p stages across nodes, communicating only a point-to-point activation hand-off; its idle bubble ≈ (p−1)/(m+p−1) shrinks as you add micro-batches, and the 1F1B schedule keeps peak activation memory low so you can. Sequence/context parallelism with ring attention splits along T for long contexts, reusing FlashAttention's online softmax so the T×T scores never materialize. Compose all three into a 3D mesh DP × TP × PP that must factor the GPU count exactly — for 64 GPUs, DP 4 × TP 8 × PP 2 — placing the heaviest comm (TP) innermost on NVLink, the lightest (PP) across nodes, and DP outermost with FSDP folded in to shard the 16-bytes/param state. The reward: any dense size is now trainable. The catch that forces lesson 10: you still pay 6ND FLOPs through every parameter, so capability and cost rise together — unless you make each token touch only a sparse slice of the weights.

Interview prompts