07 — NCCL & The Network Fabric¶
Why this file: Every parallelism strategy compiles down to a small vocabulary of collective operations. Their cost is not “network bandwidth” — it’s a specific function of algorithm × topology × message size. If you can’t derive the ring all-reduce cost formula, you can’t reason about TP scaling. If you don’t know when NCCL picks tree vs ring, you can’t debug the “why is my 8-GPU run slower than 4?” mystery.
Reading:
Sylvain Jeaugey (NVIDIA), Massively Scale Your Deep Learning Training with NCCL 2.4 (blog, 2019) — origin of the 2(N-1)/N formula popularization.
NCCL docs: https://docs.nvidia.com/deeplearning/nccl/user-guide/docs/
Patarasuk & Yuan, Bandwidth Optimal All-reduce Algorithms for Clusters of Workstations (2009) — where the ring algorithm proof lives.
Baidu’s Bringing HPC Techniques to Deep Learning (2017 blog) — the paper that put ring all-reduce on the ML map.
The Collectives You Must Know Cold¶
Collective |
Pattern |
Uses |
|---|---|---|
AllReduce |
Sum/max across N ranks → identical result at each |
DP grad sync, TP forward/backward |
ReduceScatter |
AllReduce then shard: rank i gets 1/N of the reduced result |
ZeRO/FSDP grad reduction, Megatron-SP |
AllGather |
Each rank contributes 1/N, all ranks get the concatenated whole |
FSDP param unshard, Megatron-SP |
Broadcast |
Rank 0 → everyone |
Weight init, checkpoint scatter |
Reduce |
Sum to rank 0 only |
Rare in training |
AllToAll |
Rank i’s j-th chunk → rank j’s i-th slot |
MoE dispatch, sequence-parallel Ulysses |
Send/Recv (point-to-point) |
Directed message |
PP microbatch handoff, Ring-Attention K/V pass |
Key identity: AllReduce ≡ ReduceScatter + AllGather. This factorization is not academic — it’s how NCCL implements ring all-reduce, and it’s why FSDP (which uses the two halves separately) has the same total bytes moved as DDP AllReduce.
The Ring All-Reduce Cost Formula (Derive This From Memory)¶
Setup: N ranks, each holding a tensor of S bytes (say 1 GB of gradients each).
Ring algorithm:
Logically split each rank’s tensor into N chunks of size
S/N.Reduce-scatter phase: N-1 steps. In step k, rank i sends chunk (i-k mod N) to rank i+1 and receives chunk (i-k-1 mod N) from rank i-1, accumulating into its local copy. After N-1 steps, rank i owns the fully-reduced chunk (i+1 mod N).
All-gather phase: N-1 more steps rotating the reduced chunks around the ring.
Per-link bytes: each step moves S/N bytes. Total steps 2(N-1).
$$ \boxed{\text{bytes per rank on the wire} = 2(N-1) \cdot \frac{S}{N} = 2S \cdot \frac{N-1}{N}} $$
As N → ∞, this approaches 2S per rank. That is the constant that dominates all your napkin math. Bandwidth utilization is bandwidth optimal — you cannot do better on a ring topology.
Time: t_allreduce ≈ 2S(N-1)/(N·B) + 2(N-1)·α where B is the slowest link’s unidirectional bandwidth and α is per-message latency.
The first term dominates for large S (bandwidth-bound).
The second term dominates for small S (latency-bound) — this is why NCCL fuses small collectives, and why you’ll see tree/double-tree at small message sizes.
Consequence for TP scaling¶
A TP=8 transformer block does 2 all-reduces of size B·L·H bytes per forward pass. With H100 NVLink4 ≈ 450 GB/s per GPU (bidirectional aggregate ~900 GB/s), an 8B hidden × batch × seq of ~256 MB per all-reduce costs ~0.5 ms — fine when GEMMs take 3-5 ms, catastrophic if you tried to run TP across PCIe (~64 GB/s ≈ 15x slower → all-reduce longer than compute).
This is why TP stays within an NVLink domain. Full stop.
Ring vs Tree vs Double-Tree¶
NCCL picks the algorithm based on message size and topology.
Algorithm |
Bandwidth cost |
Latency cost |
Best for |
|---|---|---|---|
Ring |
Optimal |
|
Large messages, in-node NVLink |
Tree |
|
|
Small messages, latency-critical |
Double-tree (NCCL default multi-node) |
Near-ring bandwidth |
Near-tree latency |
Large-scale multi-node all-reduce |
CollNet / SHARP |
In-network reduction (IB switch does the sum) |
O(1) hop |
Frontier-scale, needs Mellanox HDR/NDR + SHARP |
Rule of thumb: within a single NVLink domain, ring dominates. Across nodes with IB, double-tree or CollNet-SHARP wins.
The Fabric Reality (Order-of-Magnitude Table)¶
Fabric |
Per-GPU bidi bandwidth |
Typical latency |
Where you meet it |
|---|---|---|---|
NVLink 4 (H100) |
~450 GB/s (900 GB/s NVSwitch aggregate) |
sub-μs |
Within 8-GPU node — TP, EP-dispatch |
NVLink 5 (B200) |
~900 GB/s per GPU (1.8 TB/s aggregate) |
sub-μs |
Blackwell nodes |
PCIe Gen5 x16 |
~64 GB/s |
~1-2 μs |
Consumer boards, some cost-optimized H100 SXM variants without NVLink Bridge |
InfiniBand NDR (400 Gb/s) |
~50 GB/s per NIC |
~1-2 μs |
Cross-node in H100/B200 clusters (rail-optimized: 8 NICs per node, one per GPU) |
RoCE v2 (Ethernet) |
~25-50 GB/s per NIC (varies) |
2-5 μs |
Meta, MSFT clusters; hyperscaler alternative to IB |
CX-7 400G RDMA over IB |
50 GB/s |
1 μs |
Modern rail-optimized topology |
The 10x cliff: intra-node NVLink is ~10x faster than inter-node IB. Every parallelism decision reduces to “which comm patterns can I keep on the fast side of the cliff?”
TP → NVLink only. Two all-reduces per block per direction × dozens of blocks per forward → totally unaffordable on IB.
PP → IB is fine. One send/recv of activations per stage boundary per microbatch. Tiny relative to compute.
DP/FSDP → IB is fine at scale. Grad sync happens once per step, can overlap with backward.
EP → NVLink preferred, but wide-EP crosses nodes. DeepSeek-R1 style wide-EP uses IB with heavy overlap (see DeepEP kernels).
Rail-Optimized Topology (What NVIDIA Reference Clusters Look Like)¶
A standard 8xH100 node:
GPU0 - GPU1 - GPU2 - GPU3
\ | / \ | /
NVSwitch (450 GB/s per GPU, fully connected within node)
/ | \ / | \
GPU4 - GPU5 - GPU6 - GPU7
| | | |
NIC0 NIC1 NIC2 NIC3 ← rail 0..3 (one InfiniBand NIC per GPU)
| | | |
├── Rail 0 IB switch ──┐
└── Rail 1 IB switch ──┤ Each rail is its own leaf-spine network.
Rail 2 IB switch ──┤ Cross-rail traffic must traverse a spine hop.
Rail 3 IB switch ──┘
Key implication: NCCL is topology-aware. It uses NVLink for the intra-node portion of any collective, then IB per-rail for inter-node. Setting NCCL_TOPO_FILE or letting NCCL auto-detect via NCCL_TOPO_DUMP_FILE matters for large runs. Rails mean GPU_i on node A talks to GPU_i on node B on the same rail — never GPU_i→GPU_j across nodes without going up a spine.
The NCCL_DEBUG=INFO Ritual¶
When a distributed run hangs, silently corrupts, or scales like a brick, the first thing you do:
NCCL_DEBUG=INFO NCCL_DEBUG_SUBSYS=INIT,COLL,ENV python train.py 2>&1 | tee nccl.log
What to look for in nccl.log:
NCCL INFO Bootstrap : Using <ifname>:<ip>— is it using IB or falling back to TCP over Ethernet?NCCL INFO NET/IB : ... GID <n>— IB path is active. If you seeNCCL INFO NET/Socket, you’re on TCP → 100x slower → fix it.NCCL INFO Channel <k>— number of channels (parallel rings). Small message sizes use 2, large sizes 8-32.NCCL INFO Ring <n>vsTree <n>— which algorithm was selected.NCCL INFO comm 0x... rank <r> nranks <N> ... commHash— verify all ranks joined the same communicator.
Common failure signatures:
Slower than expected +
NET/Socketin log → NIC not detected, IB config wrong, fallback to TCP.Hang at init with mismatched ranks → some processes died silently (check
dmesgfor OOM kills,torchrunlogs).Corruption on multi-node but not single → likely a PCIe→NIC affinity mismatch; force
NCCL_IB_HCA=mlx5_0etc.Slow tail latency on IB → GID mismatch or wrong SL (service level); set
NCCL_IB_GID_INDEX,NCCL_IB_SL.
Critical environment variables you WILL need:
NCCL_DEBUG=INFO|WARN— verbosityNCCL_IB_DISABLE=1— force TCP (debugging only)NCCL_P2P_DISABLE=1— force host-staged copies (rare, debugging)NCCL_SOCKET_IFNAME=eth0— pin the bootstrap interfaceNCCL_IB_HCA=^mlx5_1— exclude an HCA (rail isolation)NCCL_ALGO=Ring|Tree— force algorithm (benchmarking)NCCL_NVLS_ENABLE=1— NVLink SHARP (Hopper+)NCCL_ASYNC_ERROR_HANDLING=1— make hangs fail fast rather than deadlock forever
Overlap: The Whole Game¶
At scale, compute overlaps communication or you lose. The specific overlaps:
DDP: backward computes grads layer-by-layer; each layer’s grad kicks off an all-reduce while the next layer’s backward runs. PyTorch DDP does this automatically via bucket-fused all-reduce.
FSDP: prefetch the next parameter all-gather during current forward compute; reduce-scatter current grad while next backward computes. Watch
torch.profilertrace: you want NCCL streams solid, compute streams solid, minimal white space.TP+SP (Megatron-3): the reduce-scatter after MLP is followed by GEMM that can hide part of it. Recent Megatron does explicit
torch.cuda.streamoverlap.PP: the bubbles ARE the failure to overlap — 1F1B and Zero-Bubble are the schedule fixes.
EP: all-to-all overlaps with expert compute in DeepEP.
Diagnostic: In Nsight Systems, colored NCCL rows should sit alongside compute rows, not sequentially. Gaps = perf left on the floor.
Napkin Math: How Much All-Reduce Time Are You Buying?¶
Say you’re training a 70B model with FSDP on 64 H100s across 8 nodes, IB NDR (50 GB/s per NIC).
Total params: 70B → 140 GB in bf16.
Grad reduce-scatter each step:
2·(N-1)/N · 140 GB ≈ 275 GBmoved per rank aggregate across the phases (but split across N ranks, so per-rank on the wire is~2·140/64 · 63 ≈ 4.3 GB).At 50 GB/s per NIC (best case, hitting one rail):
~86 msfor the pure IB all-reduce phase.FSDP splits it into per-layer overlapped chunks → target: hide most of it under backward compute.
If your measured step time is 500 ms and comm is 86 ms un-overlapped → 17% comm tax. Get it overlapped → <3% effective. This is why comm/compute overlap is where the money is.
AMD / ROCm Note (Awareness)¶
ROCm has RCCL — ABI-compatible with NCCL, same collective API. MI300X uses Infinity Fabric intra-node (~896 GB/s per GPU) and Broadcom/Marvell 400G NICs for inter-node. The math and mental model transfer 1:1; the constants change. If a customer says “we’re standardizing on MI300X for on-prem,” you’ll want ROCm literacy — the algorithms don’t change, only the fabric constants.
Exercises¶
Derive the ring formula from scratch. No peeking. Write it on paper, plug in N=8, S=1GB, B=450 GB/s → predicted time. Then run
nccl-tests/all_reduce_perfon a real 8xH100 node and see how close you got (usually within 30%).Force ring vs tree. With
NCCL_ALGO=RingthenNCCL_ALGO=Tree, sweep message sizes from 1KB to 1GB. Plot bandwidth vs message size. Confirm the crossover point.Diagnose a TCP fallback. On any 2-node run, deliberately
unset NCCL_SOCKET_IFNAMEand let NCCL guess. Read the log. Find why it might pick a slow interface. Fix it.Kill a scaling curve. Train the same tiny GPT with DDP on 2, 4, 8 GPUs. Measure step time. Compute scaling efficiency. Explain the gap.
Links¶
NCCL user guide: https://docs.nvidia.com/deeplearning/nccl/user-guide/docs/
nccl-testsbenchmark suite: https://github.com/NVIDIA/nccl-testsSylvain Jeaugey’s NCCL 2.4 blog (double-tree): https://developer.nvidia.com/blog/massively-scale-deep-learning-training-nccl-2-4/
Baidu Ring AllReduce: https://andrew.gibiansky.com/blog/machine-learning/baidu-allreduce/
RCCL (AMD): https://github.com/ROCm/rccl
Next: file 08 applies this to inference — how TP/PP scaling actually plays out in vLLM and SGLang at small batch.