Ring & Tree Algorithms
How collectives are actually implemented — ring AllReduce, tree reduction, and why the algorithm choice determines whether communication scales.
Knowing what AllReduce does is table stakes. Knowing how it’s implemented — ring vs tree, latency vs bandwidth — is what lets you predict whether adding more GPUs will actually help.
1. The Naive Approach: Why It Doesn’t Scale
Simplest AllReduce: every GPU sends its data to GPU 0, GPU 0 sums everything, GPU 0 broadcasts the result.
Step 1: All → GPU 0 (P-1 messages of N bytes = N(P-1) bytes into GPU 0)
Step 2: GPU 0 reduces (compute)
Step 3: GPU 0 → All (P-1 messages of N bytes)
Problem: GPU 0’s network link is the bottleneck — it must receive/send bytes. With 1000 GPUs and 1 GB gradients: GPU 0 must handle 999 GB of traffic. Meanwhile, all other GPUs sit idle during Steps 1 and 3.
Time: — gets slower with more GPUs. Completely useless.
2. Ring AllReduce: Bandwidth-Optimal
The Idea
Arrange GPUs in a ring. Each GPU only communicates with its two neighbors. The algorithm has two phases:
Phase 1: ReduceScatter (P-1 steps)
- Divide each GPU’s data into chunks
- Each step: every GPU sends one chunk to the right neighbor, receives one from the left, and reduces (sums) the received chunk with its own
- After steps: each GPU holds one fully-reduced chunk
Phase 2: AllGather (P-1 steps)
- Each step: every GPU sends its reduced chunk to the right, receives the next reduced chunk from the left
- After steps: every GPU has all reduced chunks = the complete AllReduce result
Communication Volume
Each GPU sends bytes per step, for steps total:
For large : — independent of GPU count.
This is bandwidth-optimal — you can’t do AllReduce with less total data movement (proven lower bound).
Why It Works
The key insight: every GPU is simultaneously sending and receiving every step. No GPU is ever idle. The ring’s aggregate bandwidth is (where is per-link bandwidth), and the algorithm uses all of it.
Limitation: Latency
Ring AllReduce takes steps. Each step has a startup latency (establishing communication). Total latency: .
For 1000 GPUs: 1998 steps × ~5μs/step = ~10ms of pure latency overhead — even before counting data transfer. For small messages (e.g., a single scalar), this latency dominates.
3. Tree Reduction: Latency-Optimal
The Idea
Organize GPUs in a binary tree. Reduce up the tree (log₂P steps), then broadcast down.
GPU0 (root, has final sum)
/ \
GPU0 GPU2
/ \ / \
GPU0 GPU1 GPU2 GPU3
Steps: (reduce up + broadcast down)
Latency
— for 1000 GPUs: . That’s 100× less than ring’s 10ms.
Bandwidth
Each step sends bytes through a single link. Total: bytes through the root link — not bandwidth-optimal for large messages.
4. When to Use Which
| Algorithm | Best for | Latency | Bandwidth |
|---|---|---|---|
| Ring | Large messages (gradients, parameters) | — high | Optimal: |
| Tree | Small messages (scalars, control) | — low | Suboptimal: |
| Recursive halving-doubling | Medium messages | Near-optimal |
NCCL’s Strategy
NCCL doesn’t use a single algorithm — it switches based on message size:
- Small messages (< ~256 KB): Tree algorithm (latency matters more)
- Large messages (> ~256 KB): Ring algorithm (bandwidth matters more)
- Very large messages: Chunked ring with pipelining
This adaptive strategy is one reason NCCL outperforms manual implementations.
5. The Communication Time Model
Total time for a collective:
Where:
- = startup latency per message (~5-10 μs)
- = message size in bytes
- = per-link bandwidth (e.g., 450 GB/s for one NVLink direction)
- = algorithm-dependent scaling factor
| Algorithm | term | Bandwidth term |
|---|---|---|
| Ring AllReduce | ||
| Tree AllReduce |
Crossover: Ring wins when . For typical values: ring wins for messages > ~100 KB.
Practical Example
AllReduce of 1 GB gradients across 8 H100s on NVLink (450 GB/s per direction):
Ring:
Tree:
Ring is 3.3× faster for this large message. Tree’s advantage only appears for tiny messages.
6. Overlapping Communication with Computation
In practice, AllReduce doesn’t have to wait until backward completes. Gradient bucketing starts AllReduce for early layers while later layers are still computing:
Backward: [Layer 32] [Layer 31] [Layer 30] ... [Layer 1]
AllReduce: [Bucket 8] [Bucket 7] ... [Bucket 1]
↑ starts as soon as bucket is ready
PyTorch DDP does this automatically — gradients are grouped into “buckets” (default 25 MB), and AllReduce starts for each bucket as soon as its gradients are computed.
Ideal: AllReduce is completely hidden behind backward computation. Reality: for the last few buckets, AllReduce must wait (no more computation to overlap with). The overhead is the tail — the AllReduce time for the last bucket.
Self-Check
- Explain why naive AllReduce is O(N×P) and doesn’t scale
- Draw the ring AllReduce algorithm (ReduceScatter + AllGather phases)
- Prove that ring AllReduce per-GPU volume is
- Explain why tree is better for small messages and ring for large
- Calculate AllReduce time for given N, P, B using the time model
- Explain gradient bucketing and how it overlaps AllReduce with backward