Collective communication and the alpha-beta cost model
Before this lesson: lesson 1 (what a GPU is and what HBM is); the block view of a matrix (linear algebra 1), because an all-to-all is a block transpose; the logarithm . Time: about 65 minutes, plus the notebook.
Why this matters #
Every distributed training step ends in collectives. Data parallelism (DDP) all-reduces the gradients after the backward pass. FSDP and ZeRO all-gather the weights and reduce-scatter the gradients (lesson 8). Tensor parallelism all-reduces activations in every layer (lesson 9). Mixture-of-experts layers and DeepSpeed-Ulysses use all-to-all (lesson 10).
The α-β model in this lesson answers three practical questions with a few lines of arithmetic: how long will this collective take, which algorithm should a library pick for this message size, and is my measured all-reduce slow because of the network or because of a configuration problem? Every later lesson on parallelism uses its results.
1. Predict first #
2. The picture #
Words first. The GPUs (or processes) that take part in a collective are called ranks, numbered . A message is one transfer of data from one rank to another. A collective is a communication operation that all ranks call together. The vector each rank holds has bytes.
The six standard collectives, on 4 GPUs. GPU 0 holds data , GPU 1 holds , GPU 2 holds and GPU 3 holds ; each is cut into 4 chunks (the subscript).
Pick a collective and press Next step (or Play). Each column is a GPU and each row a chunk. Grey cells are a GPU's own data. Aqua cells arrived over a link or are sums: "ABD₂" means , and "Σ₂" is the full sum of chunk 2. Compare the start and end states with the table in section 3. Notice that all-to-all ends as a transpose: GPU holds chunk of every GPU.
The simplest case, . Two GPUs hold vectors and of bytes. They want on both.
- One step: each GPU sends its whole vector to the other and adds. Each GPU sends bytes.
- Two steps: GPU 0 sends the second half of , and GPU 1 sends the first half of . Each adds what it receives, so now GPU 0 owns the first half of the sum and GPU 1 the second half. This is a reduce-scatter. Then each GPU sends its finished half to the other. This is an all-gather. Each GPU again sends bytes, but in two messages.
For two GPUs the one-step method is better (one message instead of two). For many GPUs the two-phase idea wins, because it keeps every link busy with small pieces.
The ring, ranks. Put the ranks on a ring: rank sends only to rank (mod ) and receives only from rank . Cut each vector into chunks of bytes. In every step, every rank sends one chunk to its right neighbour, so all links work at the same time.
- Reduce-scatter phase, steps : rank sends its partial sum of chunk ; the receiver adds it to its own copy. Each chunk travels around the ring and picks up one more contribution at each hop. After hops it contains all inputs.
- All-gather phase, steps : each rank forwards the finished chunk it received last. Nothing is added; the chunks are only copied.
Here is the reduce-scatter schedule for . The entry is the chunk that the rank sends to its right neighbour in that step:
| GPU 0 → 1 | GPU 1 → 2 | GPU 2 → 3 | GPU 3 → 0 | |
|---|---|---|---|---|
| step 1 | chunk 3 | chunk 0 | chunk 1 | chunk 2 |
| step 2 | chunk 2 | chunk 3 | chunk 0 | chunk 1 |
| step 3 | chunk 1 | chunk 2 | chunk 3 | chunk 0 |
Follow chunk 0: GPU 1 sends it (step 1, 2 inputs: arrive at GPU 2), GPU 2 forwards it (step 2, 3 inputs), GPU 3 forwards it (step 3, all 4 inputs arrive at GPU 0). So after the reduce-scatter, GPU owns the full sum of chunk .
Step through the ring. Each row is a GPU and each column a chunk; a cell shows how many of the inputs it already contains. In each step the aqua arrows carry the chunk written on them, the dashed cells are the chunks being sent, and the magenta frames mark the chunks that just arrived. Watch the full chunks (4/4) appear on the diagonal after step 3. Then change : the number of steps grows as , but the bytes each GPU sends stay close to . With the cells show the numbers of the worked example in section 4.
Why the bytes do not grow with . Each GPU sends chunks of bytes: in total . For this is , for it is , and for it is . More GPUs give more steps, but smaller chunks.
A ring all-reduce runs on 4 GPUs with the schedule above (in reduce-scatter step , GPU sends chunk ). Which chunk does GPU 1 send in step 2, and how many inputs does it contain when it arrives?
. GPU 1 received chunk 3 in step 1 from GPU 0 (which sent its own ) and added , so it sends . GPU 2 adds : 3 inputs. "Chunk 1" is the chunk GPU 1 owns at the end, not the one it sends now. A chunk reaches 4 inputs only after the third hop.
3. Definitions and main results #
The α-β model #
Definition (α-β model; Hockney's model, used by Thakur, Rabenseifner and Gropp 2005). Sending a message of bytes from one rank to another takes
is the latency per message, in seconds: the start-up cost of a transfer, paid even for one byte. is the time per byte, in seconds per byte: one over the link bandwidth. For a link of 50 GB/s, s/byte.
Assumptions. Keep them in mind; section 3 ends with what happens when they fail.
- Single port, full duplex: a rank sends at most one message and receives at most one message at the same time. Sending and receiving can overlap.
- Flat network: every pair of ranks has the same and , and different pairs do not slow each other down.
- No compute cost: the local additions of a reduction are free.
Notation: ranks, bytes per rank, . The cost of an algorithm is the time until the last rank is done. When all ranks send in parallel in a step, the step costs one message time.
The six collectives #
| Collective | Before (each rank) | After |
|---|---|---|
| broadcast | the root has | every rank has |
| reduce | rank has | the root has |
| all-reduce | rank has | every rank has |
| reduce-scatter | rank has | rank has chunk of ( bytes) |
| all-gather | rank has a shard ( bytes) | every rank has |
| all-to-all | rank has blocks | rank has |
The sum is elementwise. Libraries also offer max, min and product, with the same costs.
All-to-all is a transpose. Put the blocks in a block matrix: row is what rank holds, so block is . After the all-to-all, rank holds column . So the all-to-all replaces the block matrix by its block transpose (linear algebra 1). Unlike a reduction, every block is different and goes to one place, so nothing can be added up on the way.
All-reduce = reduce-scatter + all-gather. After a reduce-scatter, rank holds chunk of the sum. An all-gather of these chunks gives every rank every chunk of the sum, which is the all-reduce result. This identity is how the ring works, and it is how ZeRO and FSDP place the optimizer step between the two halves (lesson 8).
Ring all-reduce #
Theorem (ring all-reduce cost). On a ring of ranks, all-reduce of bytes takes
and each rank sends bytes.
Proof.
- Split each vector into chunks of bytes.
- Reduce-scatter. In step , rank sends its partial sum of chunk to rank , which adds it. Each rank sends one message and receives one, so the step costs .
- Follow chunk . In step 1 it leaves rank (because ) with that rank's input. In step it leaves rank , which received it in step and added its own input. So after step the chunk contains inputs. After step it arrives at rank with all inputs. So rank owns the full sum of chunk .
- All-gather. In step , rank sends chunk : in step 1 its own finished chunk, later the chunk it received in the step before. After steps every finished chunk has visited every rank. Each step again costs .
- Total: steps of , which is the formula. Each rank sends chunks of bytes.
The two phases alone: a ring reduce-scatter or a ring all-gather costs , and each rank sends bytes.
The cost has two parts. The latency term grows linearly with . The bandwidth term is almost for any .
A large ring all-reduce (the bandwidth term dominates) runs on 8 GPUs. You move it to 16 GPUs with the same links. What happens to its time?
Each GPU sends bytes: for and for , only 7% more. The chunks are half as large but there are twice as many steps, so the bytes stay nearly the same. Only the latency term grows, from to , which is small for a large message.
Recursive doubling #
Algorithm (recursive doubling, a power of two). In step , rank exchanges its whole current vector with rank ( = bitwise XOR, which flips bit of ), and both add.
Claim. After step , rank holds the sum over the ranks that agree with in all bits above bit . Proof by induction. Before step 0, rank holds its own input. In step , and its partner agree in all bits above bit and differ only in bit . Each holds the sum over its own group of ranks (bits above bit equal), and these two groups are disjoint. Their union is the group of ranks of the claim. After steps the group is all ranks.
Each step sends the whole vector, so
Few steps (a small latency term), but every byte is sent times (a large bandwidth term). Rabenseifner's algorithm combines both strengths: a reduce-scatter by recursive halving, then an all-gather by recursive doubling, with cost (Thakur et al. 2005, §4.5). MPI libraries use it for long messages when is a power of two.
The crossover message size #
Set and solve for :
Below recursive doubling is faster: the latency regime. Above the ring is faster: the bandwidth regime. For : . For both bandwidth terms are , so recursive doubling (1 step) always wins.
Move the message size (the magenta line) across the dashed grey crossover and watch the winner change. The solid aqua line is the ring, the dashed aqua line is recursive doubling and the dark grey line is Rabenseifner. Then raise the latency α: moves right. Raise : the ring's flat left part (its latency term ) rises. The α values of the presets are assumptions for illustration; measure your own cluster.
ranks, µs, bandwidth 50 GB/s ( s/byte). A 4 KB all-reduce (for example a global norm or a loss value): ring or recursive doubling?
Ring: µs. Recursive doubling: µs. The crossover is MB. "Bandwidth-optimal" says nothing about the latency term, which dominates small messages.
Why the ring's bandwidth term is optimal #
Theorem (lower bound; Patarasuk and Yuan 2009). Assume that messages carry values or partial sums of elements, and that nothing is compressed. Then in any all-reduce algorithm the busiest rank sends at least bytes. With one port per rank this takes at least .
Proof (sketch, one element at a time).
- Fix one element. Order all messages that carry it in time (break ties in any order).
- Let be the first rank that holds the full sum of this element, right after message .
- Every other rank must have sent at least one message with this element at or before : its input must reach , and only can send it first. That is messages.
- Right after , no rank other than holds the full sum ( has one receiver). So each of the other ranks must receive at least one message after : more.
- Every element needs at least messages. Summed over all elements, the ranks send at least bytes together, so the busiest rank sends at least .
The ring reaches this bound. For all-gather the bound is simpler: each rank must receive the bytes it does not have, and with one port this takes .
All-to-all #
Proposition (all-to-all cost). In an all-to-all, rank holds blocks of bytes ( bytes in total) and must send of them, bytes, to the other ranks. The pairwise exchange algorithm uses steps; in step rank sends block to rank (for a power of two; other orders work for any ). Its cost is
and the bandwidth term is optimal, because every rank must send bytes of its own data. Bruck's algorithm needs only steps but sends about bytes in each: for a power of two (Thakur et al. 2005, §4.3). It is used for small blocks.
All-to-all is harder to scale than all-reduce. In a ring, each rank talks only to its neighbours. In an all-to-all, every pair exchanges different data, so on a real network the traffic must cross the middle of the network: its bisection bandwidth (the bandwidth across a cut that splits the ranks into two halves) sets the speed. And when the blocks have unequal sizes (uneven expert loads in mixture-of-experts), the largest one sets the time.
In a mixture-of-experts layer, each GPU holds some tokens and some experts. Every token must go to the GPU that holds its expert, and the results must come back. Which collective moves the tokens?
GPU has a block of tokens for every GPU (the tokens routed to the experts on ), and GPU must collect block from every GPU: that is the definition of all-to-all (a transpose). A second all-to-all brings the results back. All-gather would send every token to every GPU, which is times more data than needed.
Algorithm bandwidth and bus bandwidth #
nccl-tests (NVIDIA's benchmark programs such as all_reduce_perf) reports the time and
two bandwidths:
- Algorithm bandwidth , where is the buffer size in bytes. For all-gather and reduce-scatter, is the full (gathered) size.
- Bus bandwidth , with the factor
| collective | |
|---|---|
| all-reduce | |
| reduce-scatter, all-gather, all-to-all | |
| broadcast, reduce |
Why this factor. For an algorithm that reaches the lower bound, , so : the speed of the slowest link, for any . So busbw can be compared with the link's hardware bandwidth and across different numbers of GPUs. algbw cannot: for all-reduce it is about half the link speed. The factor assumes a flat network; on several nodes, busbw can be higher than one network card's bandwidth (worked example (d)).
Reading one line. An all_reduce_perf line (made-up numbers, in the tool's format) on 8 GPUs:
# size count type redop time algbw busbw
1073741824 268435456 float sum 4500 238.61 417.57
The size is bytes (1 GiB, 268,435,456 floats of 4 bytes), the time is in microseconds, and the bandwidths are in GB/s ( bytes/s). Check: GB/s, and GB/s. Compared with the 450 GB/s NVLink of an H100 node, the all-reduce reaches 93% of the link speed.
nccl-tests runs an all-reduce on 8 GPUs and reports an algorithm bandwidth of 100 GB/s. What is the bus bandwidth, the number to compare with the link speed?
For all-reduce, GB/s. The factor is above 1 because each GPU sends about bytes for an -byte all-reduce. 87.5 GB/s uses the all-gather factor ; 800 GB/s multiplies by .
Topology: fast links inside a node, slower links between nodes #
Real clusters are not flat. Two numbers for an 8-GPU H100 SXM server (2022):
- Inside a node, GPUs talk over NVLink through NVSwitch: 450 GB/s per direction per GPU.
- Between nodes, each GPU usually has its own network card: InfiniBand NDR, 400 Gb/s GB/s per direction. The network is 9× slower than NVLink and has a higher latency.
(An A100 server: 300 GB/s NVLink and 25 GB/s per GPU on 200 Gb/s InfiniBand.)
-
Flat ring. All ranks move in lockstep, so each step waits for the slowest link. A single ring that visits all GPUs of all nodes crosses the network at each node boundary and runs at network speed for every step.
-
Hierarchical all-reduce, GPUs per node and nodes:
- reduce-scatter inside each node over NVLink: each GPU now holds of its node's sum;
- all-reduce across nodes: GPU of every node joins a ring with GPU of the other nodes, on its bytes. The rings run in parallel, each on its own network card;
- all-gather inside each node over NVLink.
Each network card carries bytes, and only steps pay the network latency.
-
Lower bound across nodes. Treat each node as one super-rank (messages inside a node are free). The argument above shows that some node must send at least bytes over its network cards. The hierarchical scheme reaches this.
Switch between flat ring and hierarchical, then change the number of nodes. The flat ring's magenta hops cross the network, and every step waits for them. In the hierarchical scheme the light aqua parts use NVLink and the solid aqua part is the network; the dashed grey line is the network lower bound. Notice the bus bandwidth: it is far above the 50 GB/s of one network card. Latencies (3 µs NVLink, 10 µs network) are assumed.
NCCL (the NVIDIA Collective Communications Library) detects the topology, builds rings and
trees over NVLink and the network, uses several rings in parallel (one per network card), and picks
the algorithm and protocol for each call from its own cost model of size, rank count and topology.
The environment variable NCCL_DEBUG=INFO logs what it built. Before a long run on a new cluster,
measure it with nccl-tests and compare busbw with the link speeds.
Trees in NCCL
For very many ranks, the ring's latency term becomes large even for medium messages. NCCL 2.4 added double binary trees (NVIDIA developer blog, 2019). In a binary tree, at most half of the ranks are inner nodes; the others are leaves. So a second tree can be built in which the leaves of the first tree are the inner nodes: no rank is an inner node in both trees. Each tree reduces and broadcasts half of the data. Then each rank receives and sends at most half of the data twice, which is as good as the ring in bytes, while the depth of a tree grows only like . NVIDIA reported up to 180× lower latency than rings at 24,576 GPUs. NCCL chooses between rings and trees per call.
When the assumptions fail. A real network is not flat (topology, above). Links can be shared (two flows through one switch port each get half). The reduction is not free on the GPU, which uses some SMs for the copy and add kernels. And is not a single number: it depends on the protocol the library chooses. The model is still the right first estimate: if a measured time is far above it, look for a configuration problem before blaming the hardware.
Answers to section 1. (1) 1.75 GB. The busiest GPU must send at least GB, and the ring sends exactly this. 0.125 GB is the size of one chunk and 1 GB is one copy of the vector; neither is enough, because every element needs messages. (2) About the same. The bandwidth term grows from to (+7%); only the latency term grows, from to .
4. Worked example #
(a) Ring all-reduce by hand, . , , ; one element per chunk; the sum is . Rule: in reduce-scatter step , GPU sends chunk ; in all-gather step , chunk .
| step | GPU 0 → 1 | GPU 1 → 2 | GPU 2 → 0 | state after the step (GPU 0; GPU 1; GPU 2) |
|---|---|---|---|---|
| start | ; ; | |||
| RS 1 | chunk 2: 3 | chunk 0: 10 | chunk 1: 200 | ; ; |
| RS 2 | chunk 1: 202 | chunk 2: 33 | chunk 0: 110 | ; ; |
| AG 1 | chunk 0: 111 | chunk 1: 222 | chunk 2: 333 | ; ; |
| AG 2 | chunk 2: 333 | chunk 0: 111 | chunk 1: 222 | on every GPU |
Check RS 1 on GPU 1: it receives chunk 2 from GPU 0 (value 3) and adds its own 30: 33. Check RS 2 on GPU 0: it receives chunk 0 from GPU 2 (value 110, which is ) and adds its own 1: 111. After RS 2, GPU owns the finished chunk (bold). There were steps, and each GPU sent 4 elements . ✓
(b) Ring or recursive doubling? , µs, 50 GB/s ( s/byte).
- MB: ring µs. Recursive doubling µs. Recursive doubling is 2.8× faster.
- GB: ring ms ms ms. Recursive doubling ms ms. The ring is 2.1× faster.
- Crossover: MB. PyTorch DDP's default gradient bucket of 25 MB is above it: gradients are all-reduced in the bandwidth regime.
(c) Bus bandwidth of (b). For the 1 GB ring all-reduce: GB/s, and GB/s, close to the 50 GB/s link. The 0.8% gap is the latency term.
(d) Hierarchical all-reduce on 4 nodes. nodes of H100s, GB. NVLink: 450 GB/s, µs (assumed). Network: 50 GB/s per GPU, µs (assumed).
- Flat single ring over GPUs. Every step crosses a network link somewhere, so each step takes µs. There are steps: ms.
- Hierarchical. Reduce-scatter in the node: µs ms. All-reduce across the 4 nodes on 125 MB per GPU: µs ms. All-gather in the node: 1.97 ms. Total: ms, 5.1× faster than the flat ring.
- Lower bound for the network part: ms. The hierarchical scheme is within 2% of it (the gap is the latency).
- What nccl-tests would print: GB/s, and GB/s. This is 5× the bandwidth of one network card. Nothing is wrong: the busbw factor assumes a flat network, and here most bytes travel over NVLink.
5. In code #
A simulated ring: NumPy vectors, messages collected first and delivered together (one step), and a counter of the elements each rank sends. It checks the worked example and the byte count.
import numpy as np
def ring_allreduce(xs):
"""Ring all-reduce of p vectors; returns results and elements sent."""
p = len(xs)
chunks = [np.array_split(np.array(x, dtype=float), p) for x in xs] # copies
sent = [0] * p
for k in range(1, p): # reduce-scatter
msgs = [(r, (r - k) % p) for r in range(p)]
data = [chunks[r][c].copy() for r, c in msgs]
for (r, c), d in zip(msgs, data):
chunks[(r + 1) % p][c] += d
sent[r] += d.size
for k in range(1, p): # all-gather
msgs = [(r, (r - k + 1) % p) for r in range(p)]
data = [chunks[r][c].copy() for r, c in msgs]
for (r, c), d in zip(msgs, data):
chunks[(r + 1) % p][c] = d
sent[r] += d.size
return [np.concatenate(ch) for ch in chunks], sent
out, sent = ring_allreduce([[1, 2, 3], [10, 20, 30], [100, 200, 300]])
print(out[0], out[2], sent) # [111. 222. 333.] [111. 222. 333.] [4, 4, 4]
rng = np.random.default_rng(0)
xs = [rng.standard_normal(8000) for _ in range(8)]
out, sent = ring_allreduce(xs)
print(all(np.allclose(o, sum(xs)) for o in out)) # True
print(sent[0], 2 * 7 / 8 * 8000) # 14000 14000.0
The cost formulas, the crossover and the busbw factor of the worked example:
from math import log2
def t_ring(p, a, b, n):
return 2 * (p - 1) * a + 2 * (p - 1) / p * n * b
def t_rd(p, a, b, n):
return log2(p) * (a + n * b)
def n_star(p, a, b):
return (2 * (p - 1) - log2(p)) * a / ((log2(p) - 2 * (p - 1) / p) * b)
a, b = 10e-6, 1 / 50e9 # 10 µs, 50 GB/s
for n in (1e6, 1e9):
print(f"{n:.0e} B: ring {t_ring(16, a, b, n) * 1e3:.4f} ms,"
f" rd {t_rd(16, a, b, n) * 1e3:.4f} ms")
# 1e+06 B: ring 0.3375 ms, rd 0.1200 ms
# 1e+09 B: ring 37.8000 ms, rd 80.0400 ms
print(f"n* = {n_star(16, a, b) / 1e6:.2f} MB") # n* = 6.12 MB
print(f"p = 4: {n_star(4, a, b) / (a / b):.1f} a/b") # p = 4: 8.0 a/b
algbw = 1e9 / t_ring(16, a, b, 1e9)
print(f"algbw {algbw / 1e9:.1f} GB/s, busbw {algbw * 30 / 16 / 1e9:.1f} GB/s")
# algbw 26.5 GB/s, busbw 49.6 GB/s
On a machine with PyTorch, torch.distributed gives the real collectives:
dist.all_reduce(x), dist.reduce_scatter(out, chunks), dist.all_gather(outs, x) and
dist.all_to_all(outs, ins). With the CPU backend gloo they run on a laptop; on GPUs the backend
is NCCL. The notebook builds them from point-to-point messages instead.
6. Where it shows up #
- Data parallelism (lesson 8). DDP all-reduces gradient buckets while the backward pass runs. ZeRO-3 and FSDP all-gather the weights before the forward and before the backward pass and reduce-scatter the gradients: 3 collectives of times the weight bytes, against 2 for DDP's all-reduce, so 1.5× the traffic.
- Tensor and sequence parallelism (lesson 9, lesson 10). Activation all-reduces in every layer, or a reduce-scatter plus an all-gather with sequence parallelism. They sit on the critical path of every layer, so they run inside a node over NVLink.
- All-to-all. Expert parallelism in mixture-of-experts layers (dispatch tokens to experts and combine the results) and DeepSpeed-Ulysses (switch from sequence sharding to head sharding).
- Measuring a cluster. nccl-tests on 2, 4, 8, 16, … GPUs, with busbw compared against NVLink and network speeds, finds broken links, wrong network plugins (a fallback to TCP sockets gives a busbw many times lower) and bad topology settings before they waste a long training run.
- Scientific computing. Conjugate gradients and other Krylov solvers need global dot products in every iteration: all-reduces of a few bytes, purely in the latency regime. Large weather and climate models, which split a global grid over many GPUs, exchange halo regions with neighbours and reduce global quantities (energy, mass) the same way.
7. Common mistakes #
- Comparing algbw with the link speed. For all-reduce, algbw is lower than the link speed by the factor . Compare busbw.
- Comparing busbw of a multi-node run with one network card and concluding that something is impossible or broken. The busbw factor assumes a flat network; a hierarchical algorithm moves most bytes over NVLink.
- Counting an all-reduce as bytes per GPU. It is .
- Using the ring for tiny messages. Its latency term dominates. Very small DDP buckets pay it many times.
- Expecting a large all-reduce to slow down in proportion to . The bandwidth term is almost constant in ; only the latency term grows.
- Applying recursive doubling to any . It needs to be a power of two, or extra steps.
8. Check yourself #
- ranks, 400 MB per rank. How many bytes does each rank send in a ring all-reduce, a ring
reduce-scatter and a ring all-gather (400 MB result)?
Answer
All-reduce: MB. Reduce-scatter: MB. All-gather: 300 MB. - nccl-tests on 8 GPUs: an all-reduce of 2 GB takes 20 ms, and an all-gather of 2 GB (full size)
takes 10 ms. Give algbw and busbw for each.
Answer
All-reduce: algbw 100 GB/s, busbw GB/s. All-gather: algbw 200 GB/s, busbw GB/s. The same busbw: the same links, and both algorithms are bandwidth-optimal. - , µs, 100 GB/s. At which message size do the ring and recursive doubling
take the same time?
Answer
Ring: . Recursive doubling: . Equal when , so MB. - Why do all ranks hold bitwise identical results after a ring all-reduce, and why does data
parallelism need this?
Answer
Each chunk's sum is formed once, on one rank, in one fixed order, and is then only copied. (In recursive doubling, partners compute and , which IEEE addition makes equal.) If replicas received slightly different gradients, their weights would drift apart, step by step. - A flat ring grows from 64 to 128 GPUs with the same links. What happens to the time of a 1 GB
all-reduce?
Answer
The bandwidth term goes from to (+0.8%). The latency term goes from to . For 1 GB the time stays almost the same. - All-gather needs only about bytes per rank. Why does all-reduce need about ?
Answer
For each element, messages are needed before the first rank can hold the full sum (every other input must leave its rank), and more after it (every other rank must still receive the sum). messages per element give bytes for the busiest rank. In an all-gather nothing must be combined first, so only the second half is needed.
What does the α-β model say about one message of bytes?
It takes : a latency per message plus seconds per byte ( = 1 / bandwidth).
Cost of a ring all-reduce of bytes on ranks?
: steps, each sending one chunk of bytes.
How is all-reduce built from two other collectives?
Reduce-scatter (rank gets chunk of the sum), then all-gather of those chunks.
Cost of recursive doubling all-reduce?
: steps, each exchanging the whole vector with partner .
How many bytes does each rank send in a ring reduce-scatter (or ring all-gather) with -byte full buffers?
bytes, in steps of .
Compute the ring/recursive-doubling crossover for .
Ring = recursive doubling gives .
Why does a large ring all-reduce not get slower with more GPUs?
Each rank sends bytes for any : more steps, but smaller chunks. Only the latency term grows.
How do you get bus bandwidth from algorithm bandwidth for an all-reduce?
busbw = algbw × . For a bandwidth-optimal algorithm on a flat network, busbw ≈ the link bandwidth.
What does an all-to-all do?
Rank holds blocks ; afterwards rank holds : a block transpose.
What is a hierarchical all-reduce?
Reduce-scatter inside each node (NVLink), all-reduce of the shard across nodes (one ring per network card), all-gather inside each node.
9. Interview angle #
- "Explain ring all-reduce and its cost." — Reduce-scatter then all-gather, steps of bytes: . Bandwidth-optimal; the latency is linear in .
- "When is a tree or recursive doubling better than a ring?" — Small messages or very many ranks: latency instead of . Give the crossover .
- "DDP scales well on one node and badly across 8 nodes. What do you check?" — busbw with
nccl-tests against the network card speed; the network plugin and transport (a TCP fallback);
overlap of the all-reduce with the backward pass; bucket size; which algorithm NCCL chose
(
NCCL_DEBUG=INFO). - "Why does FSDP send 1.5× the bytes of DDP?" — Two all-gathers (forward and backward) and one reduce-scatter, against one reduce-scatter and one all-gather inside DDP's all-reduce.
- "Why is all-to-all hard to scale?" — Different data for every pair, so it needs bisection bandwidth, and uneven block sizes (expert load) make the largest block set the time.
10. Practice #
The notebook for this lesson runs in Colab, with NumPy only. A small SimRing class
counts the steps and the bytes each simulated GPU sends. Five exercises, each with a check cell and a
folded solution:
- Ring reduce-scatter and ring all-gather on
SimRing, checked againstsumand against the expected step and byte counts. - Ring all-reduce built from exercise 1, for and vector lengths that does not divide.
- Recursive doubling for a power of two, with the same checks.
- The α-β cost functions and the crossover size , checked against the simulator's step and byte counts.
- Convert a table of nccl-tests results (algbw) to busbw and find the run with the slowest link.
11. Watch and read #
- Thakur, Rabenseifner and Gropp (2005), Optimization of Collective Communication Operations in MPICH: §3 (the cost model), §4.1 (all-gather: ring, recursive doubling, Bruck), §4.3 (all-to-all), §4.5 (reduce and all-reduce, Rabenseifner's algorithm).
- Patarasuk and Yuan (2009), Bandwidth optimal all-reduce algorithms for clusters of workstations: §3 (the lower bound), §4 (the ring algorithm).
- NVIDIA nccl-tests, Performance reported by NCCL tests: time, algbw, busbw and the factor of each collective.
- NVIDIA NCCL documentation, Collective operations, with pictures of each collective; and the blog post Massively Scale Your Deep Learning Training with NCCL 2.4 on double binary trees.
- Andrew Gibiansky (2017), Bringing HPC Techniques to Deep Learning: the ring all-reduce, step by step, with pictures.
- Hugging Face, The Ultra-Scale Playbook: the appendix on collective operations and the data-parallelism chapter.
- Stanford CS336 (2025), lecture 7 Parallelism 1 (on YouTube): collectives and their costs in the context of LLM training.
Runs in your browser with nothing to install. The notebook repeats the key demos and has exercises with checks; solutions are inside, folded.
No Google account needed: Python runs inside your browser. The first start downloads about 40 MB; your changes are saved in this browser only.