NCCL collectives, ring all-reduce & launching TP/PP¶
Baseline: vLLM 0.26.0 · APIs verified via Context7 (ADR-0004)
Module: Part 7 · Multi-GPU & Distributed · Tests the lesson: NCCL Collective Communication & Launching TP/PP in vLLM
Q: What do all-reduce / all-gather / reduce-scatter each do, why is ring all-reduce ~2× the message independent of N, which collective does TP use and how often, and how does vLLM launch TP/PP single- vs multi-node (including debugging a hang)?¶
Direct answer¶
The collectives (a collective = one op every rank runs together, not a loop of sends):
- all-reduce — combine (sum) a tensor every GPU holds; all GPUs end with the result. TP's workhorse — one after attention, one after the FFN = 2 per layer, every token.
- all-gather — each GPU holds a shard → every GPU holds the full concatenation.
- reduce-scatter — dual of all-gather: sum across GPUs, each keeps only its slice of the sum.
- point-to-point send/recv — PP's stage-to-stage activation handoff.
Ring all-reduce = reduce-scatter + all-gather, each phase moving \(\frac{N-1}{N}\) of the message per GPU → total \(\approx 2\cdot\frac{N-1}{N}\cdot M \to 2M\), independent of N (bandwidth-optimal). So the cost driver isn't the GPU count — it's that TP's all-reduce fires every layer, every token, which is why it must ride NVLink and TP stays within a node.
NCCL runs these on NVIDIA GPUs (vLLM via PyNcclCommunicator); GLOO does CPU-side coordination.
Launching in vLLM:
- Single node → backend mp: vllm serve M --tensor-parallel-size 4.
- Multi-node → ray (default) or mp: the mp path launches on every node with --nnodes / --node-rank / --master-addr (+ --headless on workers); the ray path forms the cluster (ray start) then launches once on the head with --distributed-executor-backend ray. Rule TP = GPUs/node, PP = nodes.
Debugging a hang (multi-GPU fails by hanging at init, not crashing): run the torch.distributed NCCL all_reduce sanity test under torchrun; NCCL_DEBUG=TRACE; pin the interface with NCCL_SOCKET_IFNAME / GLOO_SOCKET_IFNAME; VLLM_HOST_IP for multi-homed nets; NCCL_P2P_DISABLE=1 only as a temporary workaround.
Deep dive¶
- Why all-reduce for TP. Megatron's column-then-row split leaves each GPU with a partial sum of the layer output; all-reduce turns partials into the full activation everyone carries forward. Two per transformer block.
- Bandwidth-optimal. Ring pipelines the data in \(N\) chunks so every link is busy; per-GPU traffic → \(2M\) regardless of \(N\). Naive "gather to rank 0 and broadcast" would be \(O(N)\) and bottleneck one link.
- mp vs ray.
mpspawns one process per local GPU (single node).raycoordinates processes across nodes. Choose via--distributed-executor-backend. - TP degree. Power of 2 dividing the (KV-)head count; ≤ GPUs/node (NVLink); smallest that fits/hits latency — more TP = more per-token all-reduce, sublinear return.
- The hang.
init_process_groupblocks until all ranks rendezvous; a wrong NIC (common on InfiniBand) stalls it silently — hence the interface env vars.
Code¶
The minimal NCCL sanity test — one all-reduce of ones must sum to world_size:
import torch, torch.distributed as dist
dist.init_process_group(backend="nccl") # hangs here if the NIC is wrong
torch.cuda.set_device(dist.get_rank() % torch.cuda.device_count())
data = torch.ones(128, device="cuda")
dist.all_reduce(data, op=dist.ReduceOp.SUM) # sum across ranks; result on every GPU
assert data.mean().item() == dist.get_world_size() # N ranks × 1.0 → N; else HW/driver/net fault
# launch: torchrun --nproc-per-node=2 nccl_sanity.py (bare python → world_size=1 → tests nothing)
Interviewer follow-ups¶
- "Does all-reduce get N× slower with more GPUs?" → No — ring all-reduce is ~\(2M\) per GPU, independent of N. The cost is per-token/per-layer frequency × link bandwidth.
- "Which collective does TP use, how often?" → all-reduce, twice per layer (after attention, after FFN), on every token including decode.
- "How is all-reduce built from simpler ops?" → reduce-scatter then all-gather.
- "Single-node vs multi-node backend?" →
mp(single) vsray(multi);--distributed-executor-backend. - "Multi-node serve hangs at startup — first move?" → Run the sanity test under
torchrun;NCCL_DEBUG=TRACE; setNCCL_SOCKET_IFNAME/GLOO_SOCKET_IFNAME; check every node shares--master-addrand workers are--headless. - "Why keep TP within a node?" → Its per-token all-reduce needs NVLink bandwidth; across nodes it crawls — use PP.
- "Bare
pythonon the sanity test passes — proof of health?" → No;world_size==1makes all-reduce a no-op. Must usetorchrun.
Linked concepts¶
- Lesson: NCCL Collective Communication & Launching TP/PP
- Related: Parallelism: TP/PP/DP/EP & when to use each (the strategies these collectives implement), CUDA graphs & kernel fusion (vLLM's NCCL + CUDA-graph path), VRAM budget & max concurrency (TP frees VRAM for KV)
- Glossary: Collective communication, TP degree