Type at least 2 letters. Enter opens the first result, Esc closes.

    ML Systems · Lesson 7 · Training on many GPUs

    Collective communication and the alpha-beta cost model

    Intermediate 65 min 4 interactive figures 3 videos Colab notebook

    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 lg⁡p=log⁡2p\lg p = \log_2 p. 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 0,…,p−10, \dots, p - 1. A message is one transfer of data from one rank to another. A collective is a communication operation that all pp ranks call together. The vector each rank holds has nn bytes.

    The six standard collectives, on 4 GPUs. GPU 0 holds data AA, GPU 1 holds BB, GPU 2 holds CC and GPU 3 holds DD; each is cut into 4 chunks (the subscript).

    Interactive figure: loading…

    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 A2+B2+D2A_2 + B_2 + D_2, 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 jj holds chunk jj of every GPU.

    The simplest case, p=2p = 2. Two GPUs hold vectors x0x_0 and x1x_1 of nn bytes. They want x0+x1x_0 + x_1 on both.

    • One step: each GPU sends its whole vector to the other and adds. Each GPU sends nn bytes.
    • Two steps: GPU 0 sends the second half of x0x_0, and GPU 1 sends the first half of x1x_1. 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 n/2+n/2=nn/2 + n/2 = n 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, pp ranks. Put the ranks on a ring: rank rr sends only to rank r+1r + 1 (mod pp) and receives only from rank r−1r - 1. Cut each vector into pp chunks of n/pn/p bytes. In every step, every rank sends one chunk to its right neighbour, so all pp links work at the same time.

    • Reduce-scatter phase, steps k=1,…,p−1k = 1, \dots, p - 1: rank rr sends its partial sum of chunk (r−k) mod p(r - k) \bmod p; the receiver adds it to its own copy. Each chunk travels around the ring and picks up one more contribution at each hop. After p−1p - 1 hops it contains all pp inputs.
    • All-gather phase, steps k=1,…,p−1k = 1, \dots, p - 1: each rank forwards the finished chunk it received last. Nothing is added; the chunks are only copied.

    Here is the reduce-scatter schedule for p=4p = 4. 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: B0+C0B_0 + C_0 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 rr owns the full sum of chunk rr.

    Interactive figure: loading…

    Step through the ring. Each row is a GPU and each column a chunk; a cell shows how many of the pp 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 pp: the number of steps grows as 2(p−1)2(p-1), but the bytes each GPU sends stay close to 2n2n. With p=3p = 3 the cells show the numbers of the worked example in section 4.

    Why the bytes do not grow with pp. Each GPU sends 2(p−1)2(p-1) chunks of n/pn/p bytes: in total 2(p−1)pn\frac{2(p-1)}{p}n. For p=2p = 2 this is nn, for p=8p = 8 it is 1.75n1.75n, and for p=1000p = 1000 it is 1.998n1.998n. More GPUs give more steps, but smaller chunks.

    Quick check

    A ring all-reduce runs on 4 GPUs with the schedule above (in reduce-scatter step kk, GPU rr sends chunk (r−k) mod 4(r - k) \bmod 4). Which chunk does GPU 1 send in step 2, and how many inputs does it contain when it arrives?

    3. Definitions and main results #

    The α-β model #

    Definition (α-β model; Hockney's model, used by Thakur, Rabenseifner and Gropp 2005). Sending a message of mm bytes from one rank to another takes

    T=α+mβ.T = \alpha + m\beta .

    α\alpha is the latency per message, in seconds: the start-up cost of a transfer, paid even for one byte. β\beta is the time per byte, in seconds per byte: one over the link bandwidth. For a link of 50 GB/s, β=1/(50×109)=2×10−11\beta = 1/(50 \times 10^9) = 2 \times 10^{-11} s/byte.

    Assumptions. Keep them in mind; section 3 ends with what happens when they fail.

    1. 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.
    2. Flat network: every pair of ranks has the same α\alpha and β\beta, and different pairs do not slow each other down.
    3. No compute cost: the local additions of a reduction are free.

    Notation: pp ranks, nn bytes per rank, lg⁡p=log⁡2p\lg p = \log_2 p. 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 xx every rank has xx
    reduce rank rr has xrx_r the root has ∑rxr\sum_r x_r
    all-reduce rank rr has xrx_r every rank has ∑rxr\sum_r x_r
    reduce-scatter rank rr has xrx_r rank rr has chunk rr of ∑rxr\sum_r x_r (n/pn/p bytes)
    all-gather rank rr has a shard srs_r (n/pn/p bytes) every rank has (s0,…,sp−1)(s_0, \dots, s_{p-1})
    all-to-all rank rr has blocks br,0,…,br,p−1b_{r,0}, \dots, b_{r,p-1} rank jj has b0,j,…,bp−1,jb_{0,j}, \dots, b_{p-1,j}

    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 p×pp \times p block matrix: row rr is what rank rr holds, so block (r,j)(r, j) is br,jb_{r,j}. After the all-to-all, rank jj holds column jj. 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 rr holds chunk rr 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 pp ranks, all-reduce of nn bytes takes

    Tring=2(p−1) α+2(p−1)p nβ,T_{\text{ring}} = 2(p-1)\,\alpha + \frac{2(p-1)}{p}\, n\beta ,

    and each rank sends 2(p−1)pn\frac{2(p-1)}{p}n bytes.

    Proof.

    1. Split each vector into pp chunks of n/pn/p bytes.
    2. Reduce-scatter. In step k=1,…,p−1k = 1, \dots, p-1, rank rr sends its partial sum of chunk (r−k) mod p(r - k) \bmod p to rank r+1r + 1, which adds it. Each rank sends one message and receives one, so the step costs α+(n/p)β\alpha + (n/p)\beta.
    3. Follow chunk cc. In step 1 it leaves rank c+1c + 1 (because (c+1)−1=c(c + 1) - 1 = c) with that rank's input. In step kk it leaves rank c+kc + k, which received it in step k−1k - 1 and added its own input. So after step kk the chunk contains k+1k + 1 inputs. After step p−1p - 1 it arrives at rank c+p≡cc + p \equiv c with all pp inputs. So rank rr owns the full sum of chunk rr.
    4. All-gather. In step k=1,…,p−1k = 1, \dots, p - 1, rank rr sends chunk (r−k+1) mod p(r - k + 1) \bmod p: in step 1 its own finished chunk, later the chunk it received in the step before. After p−1p - 1 steps every finished chunk has visited every rank. Each step again costs α+(n/p)β\alpha + (n/p)\beta.
    5. Total: 2(p−1)2(p-1) steps of α+(n/p)β\alpha + (n/p)\beta, which is the formula. Each rank sends 2(p−1)2(p-1) chunks of n/pn/p bytes. ■\blacksquare

    The two phases alone: a ring reduce-scatter or a ring all-gather costs (p−1)α+p−1pnβ(p-1)\alpha + \frac{p-1}{p}n\beta, and each rank sends p−1pn\frac{p-1}{p}n bytes.

    The cost has two parts. The latency term 2(p−1)α2(p-1)\alpha grows linearly with pp. The bandwidth term 2(p−1)pnβ\frac{2(p-1)}{p}n\beta is almost 2nβ2n\beta for any pp.

    Quick check

    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?

    Recursive doubling #

    Algorithm (recursive doubling, pp a power of two). In step k=0,1,…,lg⁡p−1k = 0, 1, \dots, \lg p - 1, rank rr exchanges its whole current vector with rank r⊕2kr \oplus 2^k (⊕\oplus = bitwise XOR, which flips bit kk of rr), and both add.

    Claim. After step kk, rank rr holds the sum over the 2k+12^{k+1} ranks that agree with rr in all bits above bit kk. Proof by induction. Before step 0, rank rr holds its own input. In step kk, rr and its partner r⊕2kr \oplus 2^k agree in all bits above bit kk and differ only in bit kk. Each holds the sum over its own group of 2k2^k ranks (bits above bit k−1k - 1 equal), and these two groups are disjoint. Their union is the group of 2k+12^{k+1} ranks of the claim. After lg⁡p\lg p steps the group is all pp ranks. ■\blacksquare

    Each step sends the whole vector, so

    Trd=lg⁡p (α+nβ).T_{\text{rd}} = \lg p\,(\alpha + n\beta).

    Few steps (a small latency term), but every byte is sent lg⁡p\lg p 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 2lg⁡p α+2(p−1)pnβ2\lg p\,\alpha + \frac{2(p-1)}{p}n\beta (Thakur et al. 2005, §4.5). MPI libraries use it for long messages when pp is a power of two.

    The crossover message size #

    Set Tring=TrdT_{\text{ring}} = T_{\text{rd}} and solve for nn:

    n∗=(2(p−1)−lg⁡p) α(lg⁡p−2(p−1)p) β.n^* = \frac{\big(2(p-1) - \lg p\big)\,\alpha}{\big(\lg p - \frac{2(p-1)}{p}\big)\,\beta}.

    Below n∗n^* recursive doubling is faster: the latency regime. Above n∗n^* the ring is faster: the bandwidth regime. For p=4p = 4: n∗=(6−2)α/((2−1.5)β)=8α/βn^* = (6 - 2)\alpha / ((2 - 1.5)\beta) = 8\alpha/\beta. For p=2p = 2 both bandwidth terms are nβn\beta, so recursive doubling (1 step) always wins.

    Interactive figure: loading…

    Move the message size nn (the magenta line) across the dashed grey crossover n∗n^* 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 α: n∗n^* moves right. Raise pp: the ring's flat left part (its latency term 2(p−1)α2(p-1)\alpha) rises. The α values of the presets are assumptions for illustration; measure your own cluster.

    Quick check

    p=16p = 16 ranks, α=10\alpha = 10 µs, bandwidth 50 GB/s (β=2×10−11\beta = 2 \times 10^{-11} s/byte). A 4 KB all-reduce (for example a global norm or a loss value): ring or recursive doubling?

    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 2(p−1)pn\frac{2(p-1)}{p}n bytes. With one port per rank this takes at least 2(p−1)pnβ\frac{2(p-1)}{p}n\beta.

    Proof (sketch, one element at a time).

    1. Fix one element. Order all messages that carry it in time (break ties in any order).
    2. Let XX be the first rank that holds the full sum of this element, right after message m∗m^*.
    3. Every other rank jj must have sent at least one message with this element at or before m∗m^*: its input must reach XX, and only jj can send it first. That is p−1p - 1 messages.
    4. Right after m∗m^*, no rank other than XX holds the full sum (m∗m^* has one receiver). So each of the other p−1p - 1 ranks must receive at least one message after m∗m^*: p−1p - 1 more.
    5. Every element needs at least 2(p−1)2(p-1) messages. Summed over all elements, the ranks send at least 2(p−1)n2(p-1)n bytes together, so the busiest rank sends at least 2(p−1)pn\frac{2(p-1)}{p}n. ■\blacksquare

    The ring reaches this bound. For all-gather the bound is simpler: each rank must receive the p−1pn\frac{p-1}{p}n bytes it does not have, and with one port this takes p−1pnβ\frac{p-1}{p}n\beta.

    All-to-all #

    Proposition (all-to-all cost). In an all-to-all, rank rr holds pp blocks of n/pn/p bytes (nn bytes in total) and must send p−1p - 1 of them, p−1pn\frac{p-1}{p}n bytes, to the other ranks. The pairwise exchange algorithm uses p−1p - 1 steps; in step kk rank rr sends block br, r⊕kb_{r,\,r \oplus k} to rank r⊕kr \oplus k (for pp a power of two; other orders work for any pp). Its cost is

    Ta2a=(p−1) α+p−1p nβ,T_{\text{a2a}} = (p-1)\,\alpha + \frac{p-1}{p}\,n\beta ,

    and the bandwidth term is optimal, because every rank must send p−1pn\frac{p-1}{p}n bytes of its own data. Bruck's algorithm needs only lg⁡p\lg p steps but sends about n/2n/2 bytes in each: lg⁡p (α+n2β)\lg p\,(\alpha + \tfrac{n}{2}\beta) for pp 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.

    Quick check

    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?

    Algorithm bandwidth and bus bandwidth #

    nccl-tests (NVIDIA's benchmark programs such as all_reduce_perf) reports the time tt and two bandwidths:

    • Algorithm bandwidth algbw=S/t\text{algbw} = S/t, where SS is the buffer size in bytes. For all-gather and reduce-scatter, SS is the full (gathered) size.
    • Bus bandwidth busbw=algbw×f\text{busbw} = \text{algbw} \times f, with the factor
    collective ff
    all-reduce 2(p−1)/p2(p-1)/p
    reduce-scatter, all-gather, all-to-all (p−1)/p(p-1)/p
    broadcast, reduce 11

    Why this factor. For an algorithm that reaches the lower bound, t≈f⋅Sβt \approx f \cdot S\beta, so busbw=fS/t≈1/β\text{busbw} = f S/t \approx 1/\beta: the speed of the slowest link, for any pp. 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 S=230S = 2^{30} bytes (1 GiB, 268,435,456 floats of 4 bytes), the time is in microseconds, and the bandwidths are in GB/s (10910^9 bytes/s). Check: S/t=1,073,741,824/0.0045 s=238.6S/t = 1{,}073{,}741{,}824 / 0.0045\text{ s} = 238.6 GB/s, and 238.6×2⋅7/8=417.6238.6 \times 2 \cdot 7/8 = 417.6 GB/s. Compared with the 450 GB/s NVLink of an H100 node, the all-reduce reaches 93% of the link speed.

    Quick check

    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?

    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 =50= 50 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, gg GPUs per node and NN nodes:

      1. reduce-scatter inside each node over NVLink: each GPU now holds 1/g1/g of its node's sum;
      2. all-reduce across nodes: GPU kk of every node joins a ring with GPU kk of the other nodes, on its n/gn/g bytes. The gg rings run in parallel, each on its own network card;
      3. all-gather inside each node over NVLink.

      Each network card carries 2(N−1)N⋅ng\frac{2(N-1)}{N}\cdot\frac{n}{g} bytes, and only 2(N−1)2(N-1) 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 2(N−1)Nn\frac{2(N-1)}{N}n bytes over its gg network cards. The hierarchical scheme reaches this.

    Interactive figure: loading…

    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 2(p−1)α2(p-1)\alpha 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 lg⁡p\lg p. 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 α\alpha 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 2(p−1)pn=2⋅78⋅1 GB=1.75\frac{2(p-1)}{p}n = 2 \cdot \frac{7}{8} \cdot 1\text{ GB} = 1.75 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 2(p−1)2(p-1) messages. (2) About the same. The bandwidth term grows from 1.75 nβ1.75\,n\beta to 1.875 nβ1.875\,n\beta (+7%); only the latency term grows, from 14α14\alpha to 30α30\alpha.

    4. Worked example #

    (a) Ring all-reduce by hand, p=3p = 3. x0=(1,2,3)x_0 = (1, 2, 3), x1=(10,20,30)x_1 = (10, 20, 30), x2=(100,200,300)x_2 = (100, 200, 300); one element per chunk; the sum is (111,222,333)(111, 222, 333). Rule: in reduce-scatter step kk, GPU rr sends chunk (r−k) mod 3(r - k) \bmod 3; in all-gather step kk, chunk (r−k+1) mod 3(r - k + 1) \bmod 3.

    step GPU 0 → 1 GPU 1 → 2 GPU 2 → 0 state after the step (GPU 0; GPU 1; GPU 2)
    start (1,2,3)(1, 2, 3); (10,20,30)(10, 20, 30); (100,200,300)(100, 200, 300)
    RS 1 chunk 2: 3 chunk 0: 10 chunk 1: 200 (1,202,3)(1, 202, 3); (10,20,33)(10, 20, 33); (110,200,300)(110, 200, 300)
    RS 2 chunk 1: 202 chunk 2: 33 chunk 0: 110 (111,202,3)(\mathbf{111}, 202, 3); (10,222,33)(10, \mathbf{222}, 33); (110,200,333)(110, 200, \mathbf{333})
    AG 1 chunk 0: 111 chunk 1: 222 chunk 2: 333 (111,202,333)(111, 202, \mathbf{333}); (111,222,33)(\mathbf{111}, 222, 33); (110,222,333)(110, \mathbf{222}, 333)
    AG 2 chunk 2: 333 chunk 0: 111 chunk 1: 222 (111,222,333)(111, 222, 333) 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 10+10010 + 100) and adds its own 1: 111. After RS 2, GPU rr owns the finished chunk rr (bold). There were 2(p−1)=42(p-1) = 4 steps, and each GPU sent 4 elements =2(p−1)p⋅3= \frac{2(p-1)}{p} \cdot 3. ✓

    (b) Ring or recursive doubling? p=16p = 16, α=10\alpha = 10 µs, 50 GB/s (β=2×10−11\beta = 2 \times 10^{-11} s/byte).

    • n=1n = 1 MB: ring =2⋅15 α+3016nβ=300+1.875⋅20=300+37.5=337.5= 2 \cdot 15\,\alpha + \frac{30}{16} n\beta = 300 + 1.875 \cdot 20 = 300 + 37.5 = 337.5 µs. Recursive doubling =lg⁡16 (α+nβ)=4 (10+20)=120= \lg 16\,(\alpha + n\beta) = 4\,(10 + 20) = 120 µs. Recursive doubling is 2.8× faster.
    • n=1n = 1 GB: ring =0.3= 0.3 ms +1.875⋅20+ 1.875 \cdot 20 ms =37.8= 37.8 ms. Recursive doubling =4 (0.01+20)= 4\,(0.01 + 20) ms =80.04= 80.04 ms. The ring is 2.1× faster.
    • Crossover: n∗=(30−4) α(4−1.875) β=262.125⋅αβ=12.24⋅5×105 bytes≈6.1n^* = \dfrac{(30 - 4)\,\alpha}{(4 - 1.875)\,\beta} = \dfrac{26}{2.125}\cdot\dfrac{\alpha}{\beta} = 12.24 \cdot 5 \times 10^5 \text{ bytes} \approx 6.1 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: algbw=1 GB/37.8 ms=26.5\text{algbw} = 1\text{ GB}/37.8\text{ ms} = 26.5 GB/s, and busbw=26.5×1.875=49.6\text{busbw} = 26.5 \times 1.875 = 49.6 GB/s, close to the 50 GB/s link. The 0.8% gap is the latency term.

    (d) Hierarchical all-reduce on 4 nodes. N=4N = 4 nodes of g=8g = 8 H100s, n=1n = 1 GB. NVLink: 450 GB/s, α=3\alpha = 3 µs (assumed). Network: 50 GB/s per GPU, α=10\alpha = 10 µs (assumed).

    1. Flat single ring over P=32P = 32 GPUs. Every step crosses a network link somewhere, so each step takes 10 µs+1 GB32/50 GB/s=10+625=63510\text{ µs} + \frac{1\text{ GB}}{32} / 50\text{ GB/s} = 10 + 625 = 635 µs. There are 2⋅31=622 \cdot 31 = 62 steps: T=39.4T = 39.4 ms.
    2. Hierarchical. Reduce-scatter in the node: 7⋅(3 µs+125 MB/450 GB/s)=7⋅280.87 \cdot (3\text{ µs} + 125\text{ MB}/450\text{ GB/s}) = 7 \cdot 280.8 µs =1.97= 1.97 ms. All-reduce across the 4 nodes on 125 MB per GPU: 6⋅(10 µs+31.25 MB/50 GB/s)=6⋅6356 \cdot (10\text{ µs} + 31.25\text{ MB}/50\text{ GB/s}) = 6 \cdot 635 µs =3.81= 3.81 ms. All-gather in the node: 1.97 ms. Total: T=7.74T = 7.74 ms, 5.1× faster than the flat ring.
    3. Lower bound for the network part: 2⋅34⋅1 GB8⋅50 GB/s=3.75\frac{2 \cdot 3}{4} \cdot \frac{1\text{ GB}}{8 \cdot 50\text{ GB/s}} = 3.75 ms. The hierarchical scheme is within 2% of it (the gap is the latency).
    4. What nccl-tests would print: algbw=1 GB/7.74 ms=129\text{algbw} = 1\text{ GB}/7.74\text{ ms} = 129 GB/s, and busbw=129×2⋅31/32=250\text{busbw} = 129 \times 2 \cdot 31/32 = 250 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: pp 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 p−1p\frac{p-1}{p} 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 2(p−1)p\frac{2(p-1)}{p}. 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 nn bytes per GPU. It is 2(p−1)pn≈2n\frac{2(p-1)}{p}n \approx 2n.
    • Using the ring for tiny messages. Its latency term 2(p−1)α2(p-1)\alpha dominates. Very small DDP buckets pay it many times.
    • Expecting a large all-reduce to slow down in proportion to pp. The bandwidth term is almost constant in pp; only the latency term grows.
    • Applying recursive doubling to any pp. It needs pp to be a power of two, or extra steps.

    8. Check yourself #

    1. p=4p = 4 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)?
      AnswerAll-reduce: 2⋅34⋅400=6002 \cdot \tfrac34 \cdot 400 = 600 MB. Reduce-scatter: 34⋅400=300\tfrac34 \cdot 400 = 300 MB. All-gather: 300 MB.
    2. 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.
      AnswerAll-reduce: algbw 100 GB/s, busbw 100⋅1.75=175100 \cdot 1.75 = 175 GB/s. All-gather: algbw 200 GB/s, busbw 200⋅0.875=175200 \cdot 0.875 = 175 GB/s. The same busbw: the same links, and both algorithms are bandwidth-optimal.
    3. p=4p = 4, α=5\alpha = 5 µs, 100 GB/s. At which message size do the ring and recursive doubling take the same time?
      AnswerRing: 6α+1.5nβ6\alpha + 1.5n\beta. Recursive doubling: 2α+2nβ2\alpha + 2n\beta. Equal when 4α=0.5nβ4\alpha = 0.5n\beta, so n∗=8α/β=8⋅5×10−6⋅1011=4n^* = 8\alpha/\beta = 8 \cdot 5 \times 10^{-6} \cdot 10^{11} = 4 MB.
    4. Why do all ranks hold bitwise identical results after a ring all-reduce, and why does data parallelism need this?
      AnswerEach chunk's sum is formed once, on one rank, in one fixed order, and is then only copied. (In recursive doubling, partners compute a+ba + b and b+ab + a, which IEEE addition makes equal.) If replicas received slightly different gradients, their weights would drift apart, step by step.
    5. A flat ring grows from 64 to 128 GPUs with the same links. What happens to the time of a 1 GB all-reduce?
      AnswerThe bandwidth term goes from 1.969 nβ1.969\,n\beta to 1.984 nβ1.984\,n\beta (+0.8%). The latency term goes from 126α126\alpha to 254α254\alpha. For 1 GB the time stays almost the same.
    6. All-gather needs only about nn bytes per rank. Why does all-reduce need about 2n2n?
      AnswerFor each element, p−1p - 1 messages are needed before the first rank can hold the full sum (every other input must leave its rank), and p−1p - 1 more after it (every other rank must still receive the sum). 2(p−1)2(p-1) messages per element give 2(p−1)pn\frac{2(p-1)}{p}n bytes for the busiest rank. In an all-gather nothing must be combined first, so only the second half is needed.
    Recall cardsAnswer in your head first, then reveal. Add the cards to your review deck to see them again just before you would forget.
    1. What does the α-β model say about one message of mm bytes?

    2. Cost of a ring all-reduce of nn bytes on pp ranks?

    3. How is all-reduce built from two other collectives?

    4. Cost of recursive doubling all-reduce?

    5. How many bytes does each rank send in a ring reduce-scatter (or ring all-gather) with nn-byte full buffers?

    6. Compute the ring/recursive-doubling crossover for p=4p = 4.

    7. Why does a large ring all-reduce not get slower with more GPUs?

    8. How do you get bus bandwidth from algorithm bandwidth for an all-reduce?

    9. What does an all-to-all do?

    10. What is a hierarchical all-reduce?

    Open the review →

    9. Interview angle #

    • "Explain ring all-reduce and its cost." — Reduce-scatter then all-gather, 2(p−1)2(p-1) steps of n/pn/p bytes: 2(p−1)α+2(p−1)pnβ2(p-1)\alpha + \frac{2(p-1)}{p}n\beta. Bandwidth-optimal; the latency is linear in pp.
    • "When is a tree or recursive doubling better than a ring?" — Small messages or very many ranks: lg⁡p\lg p latency instead of 2(p−1)2(p-1). Give the crossover n∗n^*.
    • "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:

    1. Ring reduce-scatter and ring all-gather on SimRing, checked against sum and against the expected step and byte counts.
    2. Ring all-reduce built from exercise 1, for p=2,…,9p = 2, \dots, 9 and vector lengths that pp does not divide.
    3. Recursive doubling for pp a power of two, with the same checks.
    4. The α-β cost functions and the crossover size n∗n^*, checked against the simulator's step and byte counts.
    5. Convert a table of nccl-tests results (algbw) to busbw and find the run with the slowest link.

    11. Watch and read #

    Data parallelism and the communication primitives (all-reduce, ring, tree), then ZeRO, pipeline and tensor parallelism.
    Open on YouTube · the player loads from youtube-nocookie.com when you press play
    How NCCL implements collectives on real GPUs and how to call them.
    Open on YouTube · the player loads from youtube-nocookie.com when you press play
    Multi-GPU communication and NCCL's algorithms, explained by people who build it.
    Open on YouTube · the player loads from youtube-nocookie.com when you press play