LLM Inference | For You
Lecture 6 of 9

Parallelism Strategies: DP, TP, PP, EP, CP and Large-Scale MoE Serving

The main question of this lecture: when one GPU is not enough, in how many different directions can we cut the model? And what does each cut cost us per token?

Your 70B model arrives at the datacenter as 140 GB of BF16 weights. Your GPU holds 80 GB. Before a single token of KV cache, before a single request, the model simply does not fit. And even when a smaller model does fit, Lecture 1 handed you a second disappointment: one H100's \(\beta \approx 3.35\) TB/s of memory bandwidth caps a 70B decode at \(2P/\beta \approx 42\) ms per token. That is about 24 tok/s: drip, drip, drip. Two different walls, one direction out: more GPUs. This lecture is the map of how to cut, and the price tag of every cut.

Main idea: every parallelism strategy is a choice of which tensor lives where plus which communication primitive pays for it. And whether the choice is feasible is decided by one hard hardware boundary: the gap between intra-node NVLink and inter-node RDMA bandwidth. Keep that gap in your pocket for the whole lecture, right next to Lecture 1's intensity formula.

Why More Than One GPU?

There are exactly two reasons, and you should be able to name both in your sleep:

  1. Capacity. Weights plus KV cache do not fit in one GPU's HBM. A 70B model in BF16 is \(2P = 140\) GB, twice an H100's 80 GB, before a single token of KV. DeepSeek-V3's 671B parameters in FP8 (≈ 671 GB) overflow even a full 8-GPU node: \(671 > 640\) GB just to be resident.
  2. Performance. Decode is bandwidth-bound (Lecture 1): every step streams \(2P + B \cdot kv\) bytes through HBM. Split those bytes across \(T\) GPUs and bandwidth aggregates to \(T\beta\): ideally TPOT divides by \(T\), and the \(2P/\beta\) latency floor from Lecture 1 becomes \(2P/(T\beta)\).

Inference borrows its whole parallel toolbox from training (tensor, pipeline, data, expert, and context parallelism all originated there), but it plays the game under different constraints:

The 18× Wall: Where Collectives May Live

Each strategy binds to exactly one communication primitive, and the primitive's cost is decided by the machine's topology. Meet the cast first:

PrimitiveUsed byFrequency / shapeSensitivity
All-reduceTPmultiple times per layer, small messages at decodelatency-sensitive
P2P send/recvPPone-way between adjacent stages, small volumehideable behind compute
All-to-allEPpool-wide exchange, twice per MoE layerhighest topology demand
All-gather / reduce-scatterTP variants, CPring or tree, moderate volumein between

Now the hardware those primitives run on. Inside one node, 8 GPUs form a fully connected NVLink clique at ≈ 900 GB/s (bidirectional). Between nodes, IB/RoCE RDMA delivers ≈ 400 Gbps, about 50 GB/s per GPU. Look at the ratio: \[ \frac{900}{50} = 18. \] The 18× gap is the single most important number in distributed serving. GB200 NVL72 stretches the NVLink domain from 8 to 72 GPUs. But notice: the hierarchy does not disappear, only the location of the boundary moves. Everything in this lecture phrased as "inside vs. outside the NVLink domain" applies verbatim with 72 substituted for 8.

host CPU PCIe ≈ 64 GB/s node: 8× GPU, NVLink clique GPUGPU GPUGPU GPUGPU GPUGPU intra-node NVLink ≈ 900 GB/s bidirectional (GB200 NVL72: domain extended 8 → 72 GPUs) node GPUGPU GPUGPU GPUGPU GPU··· IB/RoCE RDMA ≈400 Gbps ≈ 50 GB/s / GPU ≈ 18× gap
How to: two nodes, one boundary. Every collective you design either stays inside a green/blue box (fast) or crosses the red link (18× slower). The whole lecture is a study of which primitives are allowed to cross.
Engineering Takeaway: classify every collective in your serving stack by whether it must cross the NVLink boundary. High-frequency, latency-sensitive communication (TP all-reduce at decode) must stay inside the NVLink domain; small or pipeline-hideable communication (PP P2P) may cross nodes; all-to-all sits in between and is feasible cross-node only with a dedicated implementation (DeepEP, Lecture 7). When a design review proposes cross-node TP, the review is over.

Two Phases, Two Cost Structures

Same scheme, same primitive, and yet prefill and decode feel completely different pains, because message sizes differ by orders of magnitude. Take the TP all-reduce message:

Formally, a collective is latency-dominated when \(m/\beta_{net} \ll t_{lat}\) and bandwidth-dominated when \(m/\beta_{net} \gg t_{lat}\): decode lives in the first regime, prefill in the second. File this away: it is why the two pools of a PD-disaggregated deployment (Lecture 5) may legitimately choose different parallelism degrees. The prefill pool optimizes a bandwidth problem; the decode pool, a latency problem.

Tensor Parallelism (TP): Buy TPOT with All-Reduces

The easiest thing you can do with several GPUs: cut every weight matrix into \(T\) pieces and give each GPU one piece. Megatron-style TP does it so that communication stays minimal: the MLP's up-projection \(W_1 \in \mathbb{R}^{d \times d_{ff}}\) is split column-wise (\(W_1 = [W_1^{(1)}\,|\,\cdots\,|\,W_1^{(T)}]\)), so each GPU computes a \(1/T\) slice of the intermediate activations with no traffic; the down-projection \(W_2\) is split row-wise conformally, so each GPU's partial output is already a partial sum of the full product; one all-reduce completes the layer. Attention is split by head: with \(n_h\) query heads and \(T\) GPUs, each GPU owns \(n_h/T\) heads (queries, keys, values, its own slice of the KV cache) and a column-slice of \(W_O\); one all-reduce after \(W_O\) merges the partial sums.

How: TP completes every layer with just two all-reduces (after the attention output projection and after the second MLP matrix), each of size \[ m_{AR} = B \cdot d_{model} \cdot b_{dtype} \qquad (3.6.1) \] tokens-in-batch × hidden size × bytes/element. At \(B = 32\), \(d_{model} = 8192\), BF16: \(m_{AR} = 32 \cdot 8192 \cdot 2 = 512\) KB.
W: split column-wise each GPU gets a 1/T slice GPU 1: partial sum GPU 2: partial sum GPU 3: partial sum GPU 4: partial sum ALL-REDUCE 512 KB at B=32, decode full y ×2 per layer × 80 layers × ~10 µs = 1.6 ms baked into every token the next GEMM needs the reduced activation, so the all-reduce is on the critical path
How to: follow one column-slice from W to its GPU (no traffic yet; this is why the column/row pairing exists), watch the four partial sums meet at the all-reduce, then count: this gathering happens twice per layer, and at decode each instance pays latency, not bandwidth.

What does TP buy? Weights and KV divide evenly by \(T\): per GPU, the bytes read per decode step become \[ \frac{2P}{T} + \frac{B \cdot kv}{T} \qquad (3.6.2) \] Aggregate HBM bandwidth scales to \(T\beta\), memory capacity aggregates to \(T \times 80\) GB, and TPOT ideally decreases linearly in \(T\). TP is the most direct lever on single-request latency.

The TPOT model under TP

Assumptions, honestly stated: (i) decode is memory-bound, so step time is bytes-read over bandwidth; (ii) sharding is perfectly even; (iii) each all-reduce costs a fixed \(t_{lat} \approx\) 10 µs, because decode messages are small (see the previous section); (iv) two all-reduces per layer, not overlapped with compute: they sit on the critical path, since the next GEMM needs the reduced activation. Then the memory term is (3.6.2) divided by \(\beta\), plus the communication term: \[ \mathrm{TPOT}(T) \approx \underbrace{\frac{2P/T + B \cdot kv / T}{\beta}}_{\color{#5d8120}\text{HBM streaming}} + \underbrace{2\, n_{layers} \cdot t_{lat}(T)}_{\color{#4a6fa5}\text{all-reduce latency}} \qquad (3.6.3) \] Sanity checks before trusting it: as \(T \to \infty\) the first term vanishes but the second does not, so the curve is linear-then-flat, floored at \(2 n_{layers} t_{lat}\). And at \(T = 1\) there are no peers, so the true comm term is zero: (3.6.3) overstates the \(T=1\) row by \(2 n_{layers} t_{lat}\). Keep that in mind when reading the table below.

Worked example (do it with me): Llama-3.3-70B (FP16/BF16) on H100s. Constants: \(2P = 140\) GB, \(n_{layers} = 80\), GQA with \(n_{kv} = 8\), \(d_h = 128\), so \(kv = 2 \cdot 80 \cdot 8 \cdot 128 \cdot 2 = 320\) KB/token; \(B = 32\); \(\beta = 3.35\) TB/s per GPU; \(t_{lat} = 10\) µs. The communication term (any \(T \ge 2\)): \(2 \times 80 \times 10\,\mu s = 1.6\) ms, a batch-independent fixed overhead baked into every token.
\(T\)\((2P + B{\cdot}kv)/T\)memory termcomm termTPOTtok/s per req
1140 + 0.0105 GB140.01/3.35 = 41.8 ms0 (no peers)41.8 ms24
270.005 GB20.9 ms1.6 ms22.5 ms44
435.003 GB10.4 ms1.6 ms12.0 ms83
817.501 GB5.2 ms1.6 ms6.8 ms147
(\(B \cdot kv = 32 \times 320\,\text{KB} = 10.5\) MB. Notice how small the KV term is at moderate context; weights dominate until contexts grow long.) The curve halves at each doubling, nearly ideal. But the fixed 1.6 ms comm term is already 23% of TPOT at \(T=8\) and sets the floor: even \(T \to \infty\) cannot beat ∼ 1.6 ms/token, and real \(t_{lat}(T)\) grows with \(T\) for ring algorithms (hop count \(\propto T-1\)), bending the measured curve upward past the optimum. The practical optimum for this model on H100 nodes is exactly \(T = 8\): the NVLink-domain size. Not a coincidence.

The three limits of TP

  1. Latency overhead grows with degree, and cannot cross nodes. Two calls per layer × 60–80 layers × ∼ 10 µs is the fixed overhead above. Cross-node, the same 512 KB message pays \(512\,\text{KB}/50\,\text{GB/s} \approx 10\) µs of transmission alone, plus inter-node RTT and NCCL ring hops. Verdict: cross-node TP all-reduce latency is unusable for interactive decode. Hence the rule: TP degree ≤ NVLink domain size (8 on H100 HGX, 72 on GB200 NVL72).
  2. KV-head count bounds KV sharding under plain TP. Attention is sharded by head, so the number of KV heads caps KV sharding. Llama-3.3-70B has only \(n_{kv} = 8\): at \(T = 8\) each GPU holds exactly one KV head. At \(T = 16\), KV heads must be replicated (two GPUs per head): the per-GPU KV read stops decreasing: each GPU still reads one eighth of the logical cache, rather than one sixteenth. Weight reads keep shrinking. DCP below lets those two GPUs store different token positions for their shared KV head.
  3. MLA leaves its latent cache replicated under plain TP. An MLA latent cache (Lecture 2) is a single shared latent vector per token (576 elements per layer for DeepSeek-V3), not per-head tensors: there is nothing to shard by head. TP-sharding attention just replicates the full latent cache on every GPU: aggregate cache storage ×\(T\), with no reduction in per-GPU KV bytes. Attention DP and TP combined with DCP offer two ways to address this, with different request placement and communication costs.
Engineering Takeaway: inside the NVLink domain, TP is your primary TPOT lever. But engineer the collective, not just the sharding: use the engine's custom small-message all-reduce (one-shot/two-shot algorithms beat the NCCL ring for ≤ MB-scale messages), fuse it where possible, and overlap residual communication with compute. Check the KV-head limit when increasing \(T\): once cache replication appears, compare TP + DCP with attention DP for your workload.

Pipeline Parallelism (PP): Capacity That Crosses Nodes

You probably can guess why TP alone cannot save a 405B model: even at \(T = 8\), you need \(\ge 810/8 \approx 101\) GB of weights per GPU, more than any H100 has. The next cut is coarser: instead of slicing every matrix, give each GPU group whole layers. PP splits the model into \(s\) stages; each stage holds only its own layers' weights and those layers' KV cache. Between stages flows only the hidden state \(B \times d_{model} \times b_{dtype}\) (the same 512 KB at \(B = 32\)) as one-way P2P traffic. And here is the key difference from TP: this traffic is hideable. There is no partial sum on a critical path; a 512 KB hop costs ≈ 10 µs of transmission plus latency over RDMA, and it can be overlapped with the next microbatch's compute. PP is the only cross-node-friendly model sharding.

But you probably can guess PP's flaw too: a pipeline only earns its keep when several microbatches are in flight. With \(s\) stages and \(m\) microbatches per flush, the fill/drain phases idle \(s - 1\) stage-times against \(m + s - 1\) total: \[ \text{bubble fraction} \approx \frac{s - 1}{m + s - 1} \qquad (3.6.4) \] Check the limits with me: \(m \to \infty\) gives 0 (the throughput regime); \(m = 1\) gives \((s-1)/s\) (a single request fills only one stage at a time: utilization \(1/s\)). Numbers: \(s{=}4, m{=}8 \Rightarrow\) 27%; \(s{=}8, m{=}32 \Rightarrow\) 18%; \(s{=}8, m{=}4 \Rightarrow\) 64%. And here is the killer for interactive serving: \(m\) is not yours to choose. It fluctuates with the arrival process. At low concurrency few requests are available to fill the pipe, stages idle, and the bubble becomes the dominant cost exactly when latency SLOs matter most.

Two more fixed taxes. Every token sequentially traverses all \(s\) stages, so TTFT and TPOT each absorb \(s - 1\) inter-stage hops plus per-stage scheduling overhead: \(7 \times (\sim 20\,\mu s) \approx 0.14\) ms/token at \(s = 8\), before any bubbles. And, crucially: each stage still streams its own layers at full size every step, so PP does not reduce total weight bytes read per token. No TPOT bandwidth benefit, only capacity.

Engineering Takeaway: treat PP as cross-node capacity extension, not a performance strategy. The canonical oversized-dense-model configuration is TP (intra-node, ≤ 8) × PP (cross-node, as shallow as capacity allows). Throughput-oriented offline loads tolerate deep pipelines; interactive loads should avoid them whenever any alternative exists (bigger NVLink domain, quantization, MLA-class KV compression).

Expert Parallelism (EP): Turning Sparsity into Bandwidth

Now switch models. DeepSeek-V3, the reference MoE of this course, has 256 routed experts + 1 shared expert per layer, top-8 activated per token, 61 layers (58 MoE; the first 3 are dense), \(d_{model} = 7168\). Each V3 routed expert is \(3 \times 7168 \times 2048 \approx 44.0\)M params ≈ 44 MB in FP8.

How: EP distributes whole experts across \(E\) GPUs (GPU \(g\) holds experts \(\{g, g + E, g + 2E, \dots\}\) of each layer) and tokens travel to their experts. After the router picks top-8 expert IDs per token, an all-to-all dispatch sends each token's activation (FP8) to the GPUs holding its experts; expert GEMMs run locally; an all-to-all combine sends outputs back, weighted by the gating scores (BF16). Two all-to-alls per MoE layer, total volume \[ V_{a2a} = \underbrace{B_{tot} \cdot k \cdot d_{model} \cdot b_{disp}}_{\text{dispatch}} + \underbrace{B_{tot} \cdot k \cdot d_{model} \cdot b_{comb}}_{\text{combine}} \qquad (3.6.5) \] Per token: \(8 \times 7168 \times (1 + 2) = 172{,}032\) B ≈ 168 KiB. At a pool-wide decode step of \(B_{tot} = 1024\) tokens: \(V_{a2a} \approx 176\) MB per layer, ≈ 10.2 GB across the 58 MoE layers; spread over \(E = 144\) GPUs that's ≈ 1.2 MB per GPU per layer, i.e. ≈ 24 µs of transmission at 50 GB/s RDMA. That is small enough to engineer away, but only with a dedicated all-to-all (DeepEP, Lecture 7); naive NCCL all-to-all across 144 RDMA endpoints would not hit those times.
batch of tokens t₁ → E3,E31, t₂ → E3,E70, t₃ → E17,E44, t₄ → E3,E9, ALL-TO-ALL dispatch (FP8) 168 KiB / token GPU 1 holds E0, E3, E6, ... GPU 2 holds E1, E4, E7, ... GPU 3 holds E2, E5, E8, ... ALL-TO-ALL combine (BF16) weighted by gate scores outputs return to their tokens' homes one hot expert = one straggler GPU; everyone waits every step (EPLB fixes this) ... at EP144 each GPU holds only ⌈256/144⌉ ≈ 2 routed experts per layer
How to: start at the token batch on the left; the router has already written each token's top-8 expert IDs. Dispatch arrows carry tokens to the GPUs that hold those experts (FP8); expert GEMMs run locally; dashed combine arrows carry weighted outputs back (BF16). Two pool-wide exchanges per MoE layer, lockstep for everyone.

EP vs. TP for MoE: who converts sparsity into bandwidth?

Compare the two ways to shard the same MoE layer across 8 GPUs:

Main idea (EP edition): EP is the only sharding that converts MoE sparsity into decode bandwidth savings, because weight reads become proportional to resident-and-hit experts rather than to all experts.

Why wide EP: the EP144 logic

DeepSeek's published V3/R1 deployment: prefill unit EP32 (4 nodes, 32 GPUs), decode unit EP144 (18 nodes, 144 GPUs). Why so wide on the decode side? Three chained reasons:

  1. Minimize resident weights per GPU. Under EP144 each GPU holds \(\lceil 256/144 \rceil \approx 2\) routed experts per layer (≈ 88 MB FP8). Per-GPU weight bytes read per decode step (the denominator term of Lecture 1's intensity formula) is minimized, and the freed HBM all goes to KV cache, enlarging the batch ceiling.
  2. Expert-granularity batch aggregation. An expert's GEMM processes only the tokens routed to it, so its arithmetic intensity is \[ I_{expert} \approx \frac{2 \cdot P_{expert} \cdot t_e}{P_{expert} \cdot b_{fp8}} \approx 2\, t_e \ \text{FLOP/byte} \qquad (3.6.6) \] where \(t_e\) = tokens hitting that expert per step. Against the H100 ridge \(\pi/\beta \approx 295\), an expert GEMM becomes compute-bound only at \(t_e \gtrsim 148\). A small single-GPU pool with \(B_{tot} = 8\) gets \(t_e = 8 \times 8/256 = 0.25\): hopeless GEMM shapes, pure weight streaming. A 144-GPU pool gathers all concurrent tokens and regroups by expert: at \(B_{tot} = 1024\), \(t_e = 32\) (\(I \approx 64\)); at a large decode batch \(B_{tot} = 18{,}432\) (128 per GPU), \(t_e = 576\), \(I \approx 1152\), firmly compute-bound. This is Lecture 1/4's batching logic replayed along the expert dimension, and it requires a wide pool: the aggregation ceiling is the pool's total concurrent tokens.
  3. Prefill needs no extreme EP. Prefill is compute-bound (its tokens already saturate the GEMMs), so EP32 suffices; sizing the two pools independently is exactly the freedom PD disaggregation (Lecture 5) provides.
Worked example (EP144 vs. EP8): weight bytes read per GPU per layer, decode step with \(B_{tot} = 1024\) tokens, uniform routing. Each token hits \(k/256 = 1/32\) of all experts; a resident expert is hit by the local batch with probability \(p_{hit} = 1 - (1 - 1/32)^{B_{local}}\).
resident expertslocal batch\(p_{hit}\)expected hitbytes read/layer
EP8\(256/8 = 32\)\(1024/8 = 128\)\(1-(31/32)^{128} \approx 0.982\)≈ 31.4\(31.4 \times 44\) MB ≈ 1.39 GB
EP144≈ 1.78≈ 7.1\(1-(31/32)^{7.1} \approx 0.20\)≈ 0.36\(0.36 \times 44\) MB ≈ 16 MB
Per-GPU expert weight traffic per layer drops ≈ 88×; counting all 61 layers, EP144 reads ≈ 0.96 GB of expert weights per decode step vs. ≈ 85 GB at EP8 (FP8). Meanwhile TP-8-MoE reads 1.41 GB/layer, identical to EP8, confirming that TP cannot exploit sparsity. And notice: the 88× did not come from a faster kernel. It came from putting the tensors in the right place. (Caveat, honestly: at large per-GPU batches \(p_{hit}\) saturates toward 1 and the gap narrows toward the resident-count ratio \(32/1.78 \approx 18\times\); the win is largest exactly in the latency-oriented small-batch regime.)

Load balancing: EPLB. Routing is data-dependent: some experts are hot, and the GPUs holding them become the bottleneck of both the all-to-all (everyone waits for the slowest destination) and the compute (straggler GEMM). The expert-parallelism load balancer periodically rearranges placement from routing statistics, with two tools. Redundant experts: a hot expert is replicated across GPUs and the router balances among replicas (at \(B_{tot} = 18{,}432\), an expert running 4× hot serves \(4 \times 576 = 2304\) tokens, \(I \approx 4600\), a straggler GEMM 4× the average cost; splitting it into 4 replicas restores \(t_e = 576\)). Topology-aware placement: experts frequently co-activated by the same token sit on the same node where possible, shrinking cross-node all-to-all traffic. EPLB is a control-loop cost (statistics + occasional weight movement), amortized against a permanent straggler tax on every decode step.

LatentMoE: Kimi K3's 2× all-to-all reduction

Kimi K3 (Moonshot AI, weights 2026-07-27, the canonical 2026 MoE of this course) scales the DeepSeek-V3 recipe to 896 routed + 2 shared experts, top-16, at the same \(d_{model} = 7168\). Substitute into (3.6.5) and the news looks bad: top-16 would double the per-token all-to-all volume to \(16 \times 7168 \times 3\,\text{B} = 344\) KB. LatentMoE cancels exactly that factor of two by changing what crosses the network. Define the latent dispatch width \(d_{lat} = 3584\): the routed path down-projects each token \(7168 \to d_{lat}\) before dispatch (fused with the router GEMM, so no extra kernel is launched), and up-projects \(d_{lat} \to 7168\) after combine. Equation (3.6.5) then holds with \(d_{lat}\) in place of \(d_{model}\) on the routed path, and the per-(token, expert) payload is exactly halved: \(3584 \times 3 = 10{,}752\) B vs. \(7168 \times 3 = 21{,}504\) B. The 2 shared experts run full-width locally and never enter the all-to-all: zero network bytes.

Worked example (K3 vs. V3, per-token all-to-all volume): FP8 dispatch (\(b_{disp} = 1\) B), BF16 combine (\(b_{comb} = 2\) B): The latent projection buys the extra routing width for free in network terms: K3 gets top-16 quality at top-8 all-to-all cost. All the EP144 sizing above (per-GPU volume ≈ \(V_{a2a}/E\), transmission time at 50 GB/s RDMA) transfers verbatim with 172 KB/token.

TA says: what does this buy you in residency? Each K3 routed expert is 33.03M params ≈ 17.5 MB in MXFP4. At EP64 a GPU holds \(896/64 = 14\) experts per layer ⇒ \(14 \times 17.5 = 245\) MB/layer, ≈ 22 GiB across the 92 MoE layers. That is comfortable on 80 GB-class HBM with room for KV/KDA state, and the aggregation logic keeps improving past EP64. (Halving to EP32 doubles residency to 490 MB/layer and halves expert-batch aggregation.) This arithmetic is exactly Moonshot's recommendation of 64+ accelerator supernodes for K3 serving: the widest EP that still fits inside a rack-scale NVLink domain. On the wire, K3's all-to-all comes from MoonEP, Moonshot's training-first EP library: an online GPU planner assigns dynamic redundant experts from current router outputs so every rank receives exactly \(S \times K\) tokens regardless of routing skew; inference runs with only 3–4 weight-prefetch slots per rank (overflow experts are read remotely through symmetric memory, slower but correct); and zero-copy fused permute/unpermute with static shapes eliminates per-layer MoE host synchronization, which is precisely the CUDA-graph capturability condition of Lecture 7. Against DeepEP v2 (H20, EP8), MoonEP's communication time stays flat as routing imbalance grows, while DeepEP's latency, set by the hottest rank, degrades steadily. In vLLM's K3 stack the all-to-all backends are flashinfer_nvlink_one_sided (NVLink) and deepep_v2 (RDMA).

Engineering Takeaway: when a 2026-class MoE presents a wider top-k, check whether dispatch runs in a reduced latent width before sizing the network: LatentMoE halves all-to-all bytes per (token, expert), so K3's top-16 costs exactly DeepSeek-V3's top-8 on the wire. Size EP from resident MB per GPU per layer (245 MB at EP64 for MXFP4 experts) and from expert-batch aggregation, and prefer an all-to-all with static shapes and no host sync (MoonEP-style) so the MoE path stays inside CUDA graphs.

The Standard Form: Attention DP + MoE EP

If experts live pool-wide under EP, what happens to attention in these deployments? Surprisingly, the simplest thing of all: data parallelism. Each GPU maintains an independent request batch (local batch, local KV, local attention) and pool-wide communication happens only at the MoE-layer boundary (the two all-to-alls). Why this beats large TP for attention:

  1. No 2× all-reduce per layer. The 1.6 ms fixed overhead of (3.6.3) disappears entirely from the attention blocks.
  2. MLA's shared latent cache is not replicated. TP-Limit-3 dissolves: with DP, each GPU caches only its own requests' latents, 34 KB/token for V3, sharded by request rather than replicated by degree.

The cost is lockstep: all DP ranks' batches must advance together, because the whole pool enters each all-to-all every step. A rank with fewer tokens this step must pad to the maximum batch or wait. For example, eight ranks at local batches \([120, 118, 126, 90, 122, 124, 119, 121]\) padded to 126 waste ≈ 7% of expert-GEMM capacity. The remedy is scheduler-side cross-rank request balancing, an extra scheduler duty (Lecture 4) that only exists in multi-GPU settings. The standard form of large-scale MoE serving is, then: \[ \text{attention DP}\ (+ \text{small TP})\ \times\ \text{MoE EP}, \qquad \text{two pool-wide all-to-alls per layer} \qquad (3.6.7) \] and whether the whole construction works is decided by the quality of the all-to-all implementation. DeepEP and Mega-MoE kernels are Lecture 7's subject.

Context Parallelism (CP): Sharding a Single Sequence

Four strategies so far shard the model (weights, experts, layers) or the request set (DP). But look at what's left untouched: one very long sequence can itself be too big. At 128K tokens, a GQA-70B request carries \(320\,\text{KB} \times 131{,}072 \approx 43\) GB of KV. That cache competes with weights for HBM and takes time to read at every step. CP cuts along \(L\). Lecture 3 developed the algorithm (ring attention); here we analyze it as a parallel strategy.

Prefill CP: O(L) communication hidden by O(L²) compute

With \(N\) GPUs each holding an \(L/N\) query block, ring attention passes KV blocks around the ring; per GPU per layer the received volume is \[ V_{CP} \approx \frac{N-1}{N} \cdot L \cdot kv_{layer}, \qquad kv_{layer} = 2\, n_{kv}\, d_h\, b_{dtype} \qquad (3.6.8) \] which is linear in \(L\) (4 KB/token/layer for Llama-3.3-70B). Attention compute per GPU per layer is Lecture 1's spine formula divided by \(N\): \[ F_{CP} \approx \frac{4 L^2 d_{model}}{N} \qquad (3.6.9) \] which is quadratic in \(L\). Communication hides inside compute when \(V_{CP}/\beta_{net} \le F_{CP}/\pi\), i.e. \[ L \ge L^*_{CP} = \frac{(N-1)\, kv_{layer}\, \pi}{4\, d_{model}\, \beta_{net}} \qquad (3.6.10) \] The common \(1/N\) factors cancel, but \(N-1\) remains: the threshold depends on group size, model shape, and the network. Substituting Llama-3.3-70B constants (\(kv_{layer} = 4096\) B, \(d_{model} = 8192\), \(\pi = 990\) TFLOPS) with \(N = 8\): cross-node RDMA (50 GB/s) gives \(L^*_{CP} \approx\) 17.3K tokens; intra-node NVLink (450 GB/s/dir) gives ≈ 1.9K. Direct check at \(L = 128\)K, \(N = 8\): per GPU per layer, comm \(= (7/8) \times 131072 \times 4096\,\text{B} \approx 470\) MB (9.4 ms at 50 GB/s) vs. compute \(= 4 \times 131072^2 \times 8192 / 8 \approx 70\) TFLOP (71 ms at 990 TFLOPS). Compute exceeds communication by 7.6×, and the ratio grows linearly in \(L\). CP is one of the few high-communication parallelisms suited to cross-node: for long sequences even an RDMA P2P ring hides inside the quadratic compute.

prefill: ring attention GPU 1 q-block L/4 GPU 2 q-block L/4 GPU 3 q-block L/4 GPU 4 q-block L/4 KV blocks walk the ring comm O(L), compute O(L²) → hidden above L*₊ decode: DCP for one long request q share queries GPU 1: KV shardoutput o₁ + LSE₁ GPU 2: KV shardoutput o₂ + LSE₂ GPU 3: KV shardoutput o₃ + LSE₃ GPU 4: KV shardoutput o₄ + LSE₄ online-softmax merge weight outputs by LSE small payloads; collective latency still matters
How to: left panel, queries stay home while KV blocks circulate around the ring until every query has seen every KV; right panel, one token's query flies to all KV shards, each GPU runs partial attention and returns an output plus its log-sum-exp (LSE). These summaries let you merge the outputs with the correct softmax weights. Prefill can hide KV traffic in compute; decode keeps the long KV history local and communicates queries and attention summaries.

Decode Context Parallelism: Share the History, Keep the GPUs

You have already chosen eight GPUs to serve a model, but its attention has only four KV heads. Plain TP gives two GPUs a copy of each head's entire history. Doubling the GPUs from four to eight shrank the weight shards; the KV cache per GPU stayed the same. What could you put in those duplicate copies instead? Different token positions from the same history. This is the motivation for vLLM's decode context parallelism (DCP). The deployment guide uses Qwen3-235B-A22B, with four KV heads, as this example.

Main idea: split each shared KV history across GPUs, then combine their partial attention results. You reclaim cache capacity and reduce the KV bytes each GPU reads at a decode step.

For the examples below, keep prefill context parallelism at its default, PCP=1. In this configuration, vLLM forms DCP groups inside the existing TP group: TP=8, DCP=2 still uses eight GPUs. Linear layers retain their TP layout. DCP changes how attention uses those GPUs. The current CLI reference also describes layouts that combine PCP and DCP; their group arrangement differs.

Where does each token's cache go?

Zoom in on one of the four KV heads. Call its two GPUs A and B. With DCP=1, both store tokens 0, 1, 2, 3, … . With DCP=2, you can give A tokens 0, 2, 4, … and B tokens 1, 3, 5, … . The next token goes to its owner, so the cache stays balanced as generation continues. This is a token-level illustration of interleaved storage; the engine can interleave larger groups of tokens. The same growth problem and a round-robin solution appear in Helix Parallelism, §2.3.

One KV head, the same two GPUs
LayoutGPU A storesGPU B stores
TP only0, 1, 2, 3, 4, 5, …0, 1, 2, 3, 4, 5, …
TP + DCP=20, 2, 4, …1, 3, 5, …

Let \(M_{KV}\) be one request's logical cache size across all layers, before replication; \(H\) its KV-head count; \(T\) the TP degree; and \(D\) the DCP degree. For equal-sized heads, even head partitioning, and a supported DCP layout, your cache accounting is \[ M_{KV,\mathrm{GPU}} \approx \frac{M_{KV}}{\min(T,H)D}. \] This ignores block rounding and metadata. For a hypothetical 16 GiB cache with \(H=4\), TP=8 alone stores \(16/4=4\) GiB per GPU: 32 GiB across the group. Add DCP=2 and each GPU holds \(16/(4\cdot2)=2\) GiB: 16 GiB across the same eight GPUs. You removed duplication; the request still has all its tokens.

MLA makes the opportunity especially clear: its shared latent cache acts like \(H=1\) for this accounting. Plain TP=8 stores eight copies; DCP=8 can distribute one copy across those eight GPUs. Conversely, GQA with \(H=8\), TP=8 already has no duplicate KV heads to reclaim. For the TP-only layouts in vLLM's DCP walkthrough, when \(T\ge H\), choose \(D\) as a divisor of \(T/H\). Thus \(H=4,T=8\) allows DCP=1 or 2; MLA at TP=8 allows 1, 2, 4, or 8. With \(T<H\), this layout uses DCP=1. DCP is not an extra factor to multiply blindly into an already sharded cache.

One decode step: move the query, then merge the answers

The diagram above gives you the route. Follow one query head:

  1. Share the query. Each TP rank initially owns a subset of query heads. An all-gather within its DCP group makes the group's query heads available on every KV shard.
  2. Read local KV. Every rank computes attention for those queries against its own token positions. It produces a locally normalized output \(o_i\) and a log-sum-exp \(\ell_i\) for each query head. The long cached history stays on its owning GPUs.
  3. Restore the global softmax. In the all-gather/reduce-scatter implementation, ranks all-gather the LSE values, reweight their local outputs, then reduce-scatter the weighted outputs back to the TP head layout. See vLLM's merge implementation.
?GPU A returns a scalar output of 2; GPU B returns 10. Can you average them and return 6? Think about what each GPU's softmax normalized over.

You also need each shard's softmax mass

Suppose A's exponentiated attention scores sum to 3 and B's sum to 1. Their weights in the full attention are 3/4 and 1/4, giving \((3\cdot2+1\cdot10)/4=4\). A plain average gives 6 because it incorrectly grants both shards equal weight. Even equal-sized shards can have very different attention scores.

Formally, for one query \(q\), let \(S_i\) be the token positions on rank \(i\), and let \(s_j=q^\top k_j/\sqrt{d_h}\) be a token's scaled attention score. Store \(\ell_i=\log\sum_{j\in S_i}\exp(s_j)\). Then the correct merge is \[ a=\max_i\ell_i,\qquad w_i=\frac{\exp(\ell_i-a)}{\sum_r\exp(\ell_r-a)},\qquad o=\sum_i w_i o_i. \] In your example, \(\ell_A=\log3\), \(\ell_B=0\), so subtracting \(a=\log3\) gives unnormalized weights 1 and 1/3, or 3/4 and 1/4 after normalization. Subtracting the maximum keeps the exponentials numerically stable. You recover full attention mathematically, with ordinary floating-point differences from changing reduction order. This is the same split-and-merge principle used by Flash-Decoding within one GPU; DCP distributes the cache and merge across GPUs.

Note: the collective sequence depends on the implementation. vLLM also exposes --dcp-comm-backend a2a to exchange partial outputs and LSE together, and supported MLA paths can replicate the query projection to skip query all-gather. Check the CLI reference for your version. These choices change the communication cost, while preserving the same attention merge.

Does half the cache mean half the token latency?

You can predict the memory saving before running the server. Latency needs more care. At fixed TP degree, batch, and context length, an illustrative model for a bandwidth-bound decode step is \[ t_{\mathrm{step}}(D) \approx t_{\mathrm{other}}(T) + \frac{t_{KV}(T,1)}{D} + t_{\mathrm{DCP}}(T,D). \qquad (3.6.11) \] Here \(t_{KV}(T,1)\) is the baseline KV-read time after TP sharding; \(t_{\mathrm{other}}\) includes the remaining work and existing TP communication; and \(t_{\mathrm{DCP}}\) is the added exposed communication and merge cost, zero at DCP=1. The \(1/D\) term assumes balanced shards and comparable achieved HBM bandwidth.

Try made-up timings: 6 ms of other work plus 8 ms of KV reads gives 14 ms. With DCP=4 and 0.6 ms of added overhead, you predict \(6+8/4+0.6=8.6\) ms, about 1.63× faster overall. If KV reads took only 0.4 ms, the same arithmetic gives 6.7 ms instead of 6.4 ms: a slowdown. The useful condition is \(t_{KV}(T,1)(1-1/D)>t_{\mathrm{DCP}}(T,D)\). This is a reasoning model, not a benchmark: kernels, query-head replication, and compute limits can change the attention timing.

Important: DCP can buy more concurrent requests even when single-request latency barely improves. The saved HBM holds additional KV caches, and larger batches amortize weight reads. Small query and output messages still pay collective latency at every attention layer. Prefill's quadratic-compute argument in (3.6.10) does not establish that decode communication will be hidden.

There is evidence for both sides of this tradeoff. The August 2026 vLLM report compares TP and DCP on eight B200s with Kimi K2.6 in NVFP4 and an agentic trace. Its largest gains come from supporting concurrency beyond the replicated-cache baseline's capacity. In contrast, the million-token context parallelism paper, §4.3 shows cases where smaller local attention kernels lose their savings to query and output communication. Those are different implementations and workloads; together they explain why you should measure capacity, throughput, and token latency separately.

Try it in vLLM

Here is a two-GPU MLA example from the vLLM walkthrough. Restart the server with DCP=1 for your baseline, then DCP=2 for the comparison:

vllm serve deepseek-ai/DeepSeek-V2-Lite \
  --tensor-parallel-size 2 \
  --decode-context-parallel-size 2

For the four-KV-head GQA example, an eight-GPU configuration is:

vllm serve Qwen/Qwen3-235B-A22B \
  --tensor-parallel-size 8 \
  --decode-context-parallel-size 2

-tp and -dcp are the short forms. In the Python API, the corresponding arguments are tensor_parallel_size and decode_context_parallel_size. Choose hardware and dtypes that fit the model plus working memory. Verify DCP support for your model, GPU, KV dtype, and attention backend in the attention backend support matrix. Speculative decoding, sparse attention, hybrid layers, and P/D transfer connectors need compatible implementations too; support for DCP alone does not establish that every combination works.

  1. Hold the comparison steady. Fix the GPUs, TP, model and KV dtypes, request lengths, prefix-cache policy, and scheduler limits. Compare DCP=1 against supported degrees at the same concurrency first.
  2. Then test the capacity benefit. Sweep concurrency and, where needed, raise the scheduler's sequence limit to use the freed cache. Report KV usage, preemptions, output tokens/s/GPU, and p50/p95/p99 inter-token latency. Include TTFT when prefill shares the server.
  3. Keep your latency target visible. More throughput is useful only while requests meet your SLO. Test both short and long contexts, and record whether each DCP group stays inside NVLink or crosses the network.
?Research thinking: an MLA model already uses TP=16 across two eight-GPU nodes. DCP=8 leaves two copies of each history; DCP=16 leaves one. Which would you try first, and what measurement could change your mind? Think before opening.

Capacity and communication pull in different directions

Start with DCP=8 groups placed within each node, then test DCP=16 if cache capacity limits useful concurrency. The latter saves more memory but adds cross-node DCP traffic; existing cross-node TP traffic remains in both cases. Compare goodput at the same latency target. This is the Kimi-K2 tradeoff described in the vLLM deployment guide. For a research direction beyond choosing group size, Helix HOP-B overlaps communication for one part of a batch with attention computation for another. You are practicing the design question; you do not need to reinvent its kernels.

Engineering Takeaway: choose TP for the model and latency target, inspect the resulting KV replication, then sweep supported DCP degrees. Count the reclaimed cache bytes, and measure the communication they cost. Sparse attention can reduce the history read further, but its combination with DCP needs backend support and measurement; speedups need not multiply.

Choosing: What Each Cut Buys and Pays

Let us remember our main idea: every parallelism strategy is a choice of which tensor lives where plus which communication primitive pays for it, and feasibility is decided by the NVLink/RDMA boundary. Every strategy we have studied edits specific terms of Lecture 1's decode intensity \(I \approx 2PB/(2P + B \cdot kv)\) and latency model. The whole lecture fits in one table, and every cell is a claim we derived above:

StrategyWeight read / GPUKV read / GPUCommunicationCross-node?
TP\(2P \div T\) (even shard)\(\div T\), bounded by \(n_{kv}\)2× all-reduce/layer, latency-sensitiveNo: \(t_{lat}\) unusable across RDMA
PPunchanged (capacity by stage)sharded by stage (its own layers)P2P, one-way, hideableYes: the only cross-node-friendly one
EPonly hit resident experts (≈ \(256/E\))n/a (MoE layer; attention handled separately)2× all-to-all/layer, needs dedicated impl (DeepEP)Yes, with a dedicated all-to-all
DPunchanged (full replica)fully local, sharded by requestnone within attention; lockstep at EP boundarytrivially
Prefill CPset by model parallelism\(\div N\) for the sharded sequencering P2P in the example aboveYes for long sequences (\(L > L^*_{CP}\))
TP + DCPTP weight layout retained\(\div D\) relative to the TP-only cache layoutquery exchange + softmax-aware output mergeMeasure decode overhead; the prefill threshold does not apply

Read each row as: which bytes shrink, which collective pays, and may it leave the NVLink domain?

Decision procedure. Fix three inputs, and usually only one or two combinations remain feasible:

  1. Model structure. Dense or MoE? KV-head count \(n_{kv}\)? KV bytes per token (GQA vs. MLA latent)?
  2. Load type & SLO (Lecture 1's metrics): interactive (TPOT/TTFT-driven) vs. throughput (goodput-driven); context length distribution.
  3. Interconnect topology: NVLink domain size (8 on H100 HGX, 72 on GB200 NVL72), RDMA bandwidth.

The standing guidelines:

? A 405B-parameter dense model in BF16 must be served interactively on a cluster of 8×H100 SXM nodes (80 GB each, NVLink intra-node, 400 Gbps IB inter-node). Choose a parallel configuration. First do the capacity arithmetic, then worry about latency. Think for a minute before opening.

Possible answer (the textbook's design)

Capacity first: weights are \(2P = 810\) GB; one node holds \(8 \times 80 = 640\) GB < 810 GB. The model does not fit in any single node, so TP=8 alone is impossible. Try TP8 × PP2: \(810/16 \approx 50.6\) GB of weights per GPU, leaving ≈ 29 GB for KV and activations. Feasible. Per-token KV reads are sharded by stage (PP splits layers) and by head (TP): effective KV per GPU \(= B \cdot kv/(8 \times 2)\). Latency check: TP all-reduces stay on NVLink (good); PP adds one cross-node hop of \(B \cdot d_{model} \cdot 2\) bytes, hideable behind compute when \(m \ge 2\)–4 microbatches. The configuration fell out of capacity; the latency budget merely had to bless it.

Common Pitfalls

Pitfalls (the five classics):
  1. Cross-node TP. "We have 400 Gb/s IB, that's fast." At decode the 512 KB all-reduce is latency-dominated; RDMA transmission alone matches the entire intra-node latency budget, and ring hops make it worse. TP beyond the NVLink domain is effectively never right for interactive decode.
  2. TP past the KV-head count. TP=16 on a GQA-8 model replicates every KV head twice: the KV-read term stops shrinking at \(T = 8\) while communication overhead keeps growing. Check the replication factor, then consider DCP; for MLA, compare TP + DCP with attention DP.
  3. Deep PP for interactive loads. The bubble \((s-1)/(m+s-1)\) explodes exactly when concurrency drops, which is when latency SLOs matter most; and \(s-1\) hops land directly in TTFT and TPOT. PP is capacity plumbing, not a latency strategy.
  4. Ignoring EPLB hotspots. Under data-dependent routing, one hot expert makes its GPU the straggler for both all-to-all and compute, and every rank waits every step. Without periodic rebalancing, wide EP underperforms narrow EP.
  5. DP lockstep imbalance. Attention DP + MoE EP forces all ranks into the same all-to-all every step. Ranks with small local batches pad or stall; a scheduler that ignores cross-rank balancing silently donates several percent of pool throughput to padding.

Research Thinking

How to: read the starting point; read the question and think, for a minute, a day, a week... and only then open the answers. You are not supposed to redesign EP144 from the starting point; each deployment took its authors months of work. It's the habit of thinking that counts: you have ideas, you try; if they don't work, you think again.

? Card 1, "invent" expert parallelism. Starting point: TP-8 on the V3 MoE layer makes every GPU stream \(256/8 = 32\) experts' worth ≈ 1.41 GB per decode step no matter that each token touches only 8 experts; sparsity never reaches the byte count. Question: can you re-place which tensor lives where so that per-GPU weight reads become proportional to activated experts? What new primitive does your scheme need, and what new bottleneck does data-dependent routing create?

Some existing attempts

Place whole experts, not matrix slices: GPU \(g\) holds experts \(\{g, g+E, g+2E, \dots\}\), and tokens travel to their experts via a pool-wide all-to-all (dispatch in FP8, combine in BF16; equation (3.6.5)). Now per-GPU reads scale with resident-and-hit experts (\(\approx 256/E\)), which keeps falling as \(E\) grows: EP144 reads ≈ 0.96 GB of experts per step vs. ≈ 85 GB at EP8. The new bottleneck you should have predicted: routing is data-dependent, so hot experts create stragglers that every rank waits on every step. This is answered by EPLB (redundant experts, topology-aware placement) and, in 2026, MoonEP's online planner that hands every rank exactly \(S \times K\) tokens regardless of skew. Note how the all-to-all's quality gates everything: naive NCCL won't do; DeepEP (Lecture 7) will.

? Card 2, when does one more GPU stop paying? Starting point: the comm term of (3.6.3), \(2 n_{layers} t_{lat}\), is independent of \(B\) and \(T\): 2 × 60 layers × 10 µs = 1.2 ms even for a 13B model, where the TP=4 memory term is only \(26\,\text{GB}/(4 \times 3.35\,\text{TB/s}) = 1.94\) ms, a 38% communication share. Question: for a given model, derive the \(T\) beyond which another GPU improves TPOT by (say) less than 10%, and explain in one sentence why small models use small \(T\). Also: which row of your TPOT table does (3.6.3) systematically mis-state, and why?

Possible derivation

TPOT\((2T)\)/TPOT\((T) = \big(\frac{2P+B\cdot kv}{2T\beta} + c\big)\big/\big(\frac{2P+B\cdot kv}{T\beta} + c\big)\) with \(c = 2 n_{layers} t_{lat}\): halving the memory term helps less and less as \(c\) dominates. For 70B at \(B = 32\): going \(8 \to 16\) takes TPOT from 6.8 ms to ≈ \(2.6 + 1.6 = 4.2\) ms (−38%), still paying! Except \(T = 16\) both exceeds \(n_{kv} = 8\) (KV stops sharding) and leaves the NVLink domain (\(t_{lat}\) explodes). Both walls land at the same place, which is exactly why "TP optimum = NVLink-domain size" keeps reappearing. For the 13B exercise model, \(c = 1.2\) ms is already 38% of TPOT at \(T = 4\): small models use small \(T\) because the fixed overhead is a first-order term, not noise. And the mis-stated row: \(T = 1\), which has no peers and pays no \(c\) at all. The model overstates it by the entire comm term (remember this when fitting Lab 6!).

? Card 3, one lonely, very long request. Starting point: batching amortizes weights, but a single 128K-token request on GQA-70B reads 43 GB of private KV per step (12.8 ms of traffic on one GPU), and it's the only request in the system. Question: which axis of the lecture's toolbox can speed up this one request, which axes visibly cannot, and what group-size dependence remains in the prefill-side threshold? Bonus: evaluate the threshold for DeepSeek-V3's MLA cache (\(kv_{layer} = 576\) B FP8, \(d_{model} = 7168\), 8-GPU RDMA ring).

Possible answer

DP cannot split this one request across replicas. TP can distribute its eight KV heads: TP=8 already reduces the ideal KV-read term from 12.8 ms to 1.6 ms. Plain TP beyond eight stops reducing that term. DCP can then shard token positions among ranks that would otherwise duplicate a head; at TP=16, DCP=2 halves the per-GPU cache relative to TP=16 alone. You must still pay the added communication in (3.6.11). PP distributes layers, whose execution remains sequential for one request. On the prefill side, hiding requires \(\frac{(N-1)L\,kv_{layer}}{N\beta_{net}} \le \frac{4L^2d_{model}}{N\pi}\), and \(N\) sits in both denominators, so those factors cancel. But \(L^*_{CP} = (N-1)kv_{layer}\pi/(4 d_{model} \beta_{net})\) still depends on \(N-1\), model shape, and network (sanity: bigger \(N\) or \(\pi\), or smaller \(\beta_{net}\), raises the threshold, the correct direction). V3: \(L^*_{CP} = \frac{7 \times 576 \times 990 \times 10^{12}}{4 \times 7168 \times 50 \times 10^9} \approx 2.8\)K tokens. MLA's tiny latent KV makes CP cross-node-friendly at much shorter context than GQA-8's 17.3K in this model. DCP and sparse attention can address different parts of the cost, but combining them requires compatible kernels and communication. See Helix Parallelism for a concrete design that separates KV partitioning from FFN partitioning.

Have Fun! The TPOT Playground

You have the model; now feel it. Pick a model, a TP degree, and a batch, and watch the two terms of (3.6.3) fight. Missions: (i) find the \(T\) where the blue comm term first eats more than 40% of TPOT for the lab's 8B model, then check yourself against Lab 6's table below; (ii) notice what the 70B row does when you push \(T\) past \(n_{kv} = 8\); (iii) try to make TPOT worse by adding GPUs. Can you?


   
Playground physics: (3.6.3) with β = 3.35 TB/s, t_lat = 10 µs, KV sharding capped at n_kv heads. The T=1 row is computed peer-free, unlike the raw formula, which overstates it.

Seminar & Homework

Take Lab 6: Parallelism, the TP Sweep from the hands-on pack. Sweep --tensor-parallel-size over \(T \in \{1, 2, 4, 8\}\) on one node with vllm serve, under a deliberately latency-clean workload: fixed \(L = 1024\) prompts, 256 outputs, --max-concurrency 32 to hold \(B \approx 32\) steady, arrival rate low enough to avoid queueing. For each \(T\): warm up, run 400 requests, record TPOT P50/P99 and per-GPU resident memory from nvidia-smi.

Then do the fun part: regress measured TPOT against \(1/T\). The slope should match \((2P + B L\, kv)/\beta\); the intercept estimates \(2 n_{layers} t_{lat}\). Compare your fitted \(t_{lat}\) with the lecture's ≈ 10 µs (and ask yourself why ring hops growing as \(T-1\) might push it up). For reference, the pack's predictions at \(t_{lat} = 10\) µs (comm term \(2 \times 32 \times 10\,\mu s = 0.64\) ms):

\(T\)memory termcomm termTPOTweights / GPU
16.08 ms06.08 ms15.0 GiB
23.04 ms0.64 ms3.68 ms7.5 GiB
41.52 ms0.64 ms2.16 ms3.7 GiB
80.76 ms0.64 ms1.40 ms1.9 GiB

The curve should halve, then bend: by \(T = 8\) the fixed comm term is ~46% of TPOT. Watch the GQA limit: \(n_{kv} = 8\) means TP=8 leaves exactly one KV head per GPU.

Optional, multi-node: repeat TP=8 across two 4-GPU nodes over IB and compare the intercept. Feel the 18× wall personally. Debrief questions the pack asks you to answer: why is the 256 KB decode payload latency- rather than bandwidth-dominated? At what \(T\) does adding GPUs stop paying for single-request latency, given your intercept? And why can't TP=8 serve more aggregate throughput than TP=1 even though TPOT falls? Pitfalls to dodge: reading TPOT under queueing (TP sweeps must stay latency-clean); forgetting KV duplication at small \(T\); comparing across different --max-num-seqs; and remembering that (3.6.3) overstates the \(T = 1\) row; note it in your report.

Summary

← Lecture 5: PD Disaggregation Lecture 7: The Kernel Level →