llm-inference-explained
← /learn · 09

Parallelism

Tensor, pipeline and expert parallelism and what they send.

Concept

Llama-3-70B's weights alone are 141.10 GB in BF16: they don't fit on one 80 GB GPU, before any KV cache. Even when a model fits, spreading it over several GPUs adds their memory bandwidth together, and decode is bandwidth-bound. There are three main ways to split a model, and each one adds a different kind of message.

Tensor parallelism (TP) splits every weight matrix across the nn devices. Megatron-LM's arrangement splits the attention heads and the MLP's hidden dimension so that each block needs just two all-reduces in the forward pass: one after attention, one after the MLP. Every device holds 1/n1/n of every layer and does 1/n1/n of every multiply. The messages are small (one activation vector per token) but there are many of them, twice per layer, on the critical path of every step. TP wants the fastest link you have, which is why it normally stays inside one NVLink-connected server.

Pipeline parallelism (PP) gives each device a contiguous slice of the layers. A token's activations pass from stage to stage: one message per boundary per step. Cheap on the link, so PP can span slower links between servers. The catch is the bubble: stage 2 can't start until stage 1 finishes, so with a single batch only one stage is busy at a time. Keeping several micro-batches in flight fills the pipeline; GPipe's bubble fraction is (n−1)/(m+n−1)(n-1)/(m+n-1) for nn stages and mm micro-batches.

Expert parallelism (EP) applies to mixture-of-experts models, whose MLP is many "expert" MLPs and a router that sends each token to its top-kk. Different devices hold different experts, so every MoE layer needs an all-to-all: send each token to the devices holding its experts (dispatch), and bring the results back (combine).

Real deployments mix them, for example TP inside a server, PP or EP across servers, and data parallelism (independent replicas) on top.

Interactive

What splitting a model across devices sends

Llama-3-70B decode, context 2,048 per sequence, BF16 activations. Link bandwidths and latencies from the simulator's hardware.py. First-order estimates: no overlap with compute; real collective libraries use faster algorithms for small messages.

Strategy
Link
Sent per device / step
62.91 MB
Messages / step
960
Comm time / step
4.94 ms
NVLink 4 (one direction)
Compute step
14.47 ms
4× H100, memory-bound

Communication is 25% of a decode step if nothing overlaps. Two all-reduces per layer (after attention and after the MLP), 160 per step for 80 layers.

Concept

With the simulator's link figures and this page's first-order model, a Llama-3-70B decode step at batch 16 on 4× H100 takes 14.47 ms of compute, and TP's 160 all-reduces add 4.94 ms over NVLink but 24.23 ms over 100 GbE. Notice that on NVLink almost all of it (4.80 ms) is the per-message latency term, not bandwidth: decode messages are tiny. That is a pessimistic estimate (collective libraries have low-latency algorithms for small messages, and engines overlap communication with compute), but the direction is right: decode-time TP is latency-sensitive, and slow links make it impractical. A pipeline boundary over InfiniBand, by contrast, moves 256 KiB per step in about 46 µs.

Maths

Ring all-reduce of SS bytes over nn devices sends 2(n−1)nS\frac{2(n-1)}{n}S bytes per device in 2(n−1)2(n-1) messages, so on a link with bandwidth βℓ\beta_\ell and per-message latency λ\lambda

tAR(S)=2(n−1)nSβℓ+2(n−1)λ.t_{\text{AR}}(S) = \frac{2(n-1)}{n}\frac{S}{\beta_\ell} + 2(n-1)\lambda .

TP per decode step: 2L2L all-reduces of S=b d aS = b\,d\,a (bb tokens, width dd, aa bytes per activation). PP: (n−1)(n-1) boundaries of b d ab\,d\,a bytes each. EP with top-kk routing over LmoeL_{\text{moe}} layers: about 2 b k d a n−1n2\,b\,k\,d\,a\,\frac{n-1}{n} bytes per layer leave each device. Pipeline bubble: n−1m+n−1\frac{n-1}{m+n-1}; with 4 stages, 4 micro-batches idle 43% of the time and 16 micro-batches 16%.

Code

// src/lib/inference/parallel.ts (excerpt)
export function ringAllReduce(size: number, n: number, link: Link): Comm {
  if (n <= 1) return ZERO;
  const bytes = (2 * (n - 1) * size) / n;
  const messages = 2 * (n - 1);
  return {
    bytes,
    time: bytes / link.bandwidth + messages * link.latency,
    messages,
  };
}

The links (NVLink 4, PCIe Gen5, InfiniBand NDR, 100 GbE) are copied from the simulator's hardware.py, and a unit test checks the copy against the Python fixture.