← All Posts

Communication: AllReduce, AllGather, Ring

Collective Operations

Distributed training relies on a small set of collective communication primitives. Understanding each operation's data movement pattern is essential for analyzing distributed training performance.

Broadcast One → All A A A Reduce All → One A B C Σ AllReduce All → Reduce → All A B C Σ Σ Σ AllGather Each shard → All full a₀ a₁ a₂ a₀a₁a₂ a₀a₁a₂ ReduceScatter Reduce + distribute shards A₀A₁A₂ B₀B₁B₂ Σ₀ Σ₁ AllToAll Personalized exchange a₀b₀c₀ a₁b₁c₁ a₀a₁ b₀b₁ Where Each Op Appears in Training Operation Used In Data Volume per GPU AllReduce DDP gradient sync 2(p-1)/p × N ≈ 2N AllGather FSDP forward (gather params) (p-1)/p × N ≈ N ReduceScatter FSDP backward (shard grads) (p-1)/p × N ≈ N AllToAll Expert parallelism (MoE routing) Depends on routing Broadcast Parameter initialization N (one-time) Reduce Loss aggregation, metrics N (to root only) p = number of GPUs, N = total data size in bytes
Critical insight for interviews: AllReduce = ReduceScatter + AllGather. Understanding this decomposition is key to understanding both DDP and FSDP. DDP uses AllReduce (every GPU has all gradients). FSDP uses ReduceScatter (each GPU only gets its shard of reduced gradients), which is more memory-efficient.

AllReduce Algorithms

There are several algorithms to implement AllReduce, each with different trade-offs in latency, bandwidth utilization, and scalability.

Naive (Parameter Server / Star)

  • All workers send to one node, it reduces, broadcasts back
  • Latency: 2α (two rounds)
  • Bandwidth: 2(p−1)N at the root → bottleneck
  • Root bandwidth doesn't scale with p

Ring AllReduce

  • Workers arranged in a logical ring
  • Latency: 2(p−1)α (more rounds)
  • Bandwidth: 2(p−1)/p × N per worker (optimal!)
  • Every link equally utilized — bandwidth optimal

Tree AllReduce

  • Binary tree structure for reduce + broadcast
  • Latency: 2 log₂(p) × α (logarithmic!)
  • Bandwidth: 2N per worker (not bandwidth-optimal)
  • Best for small messages (latency-bound)

Recursive Halving-Doubling

  • Divide-and-conquer: halve communicating groups
  • Latency: log₂(p) × α
  • Bandwidth: 2(p−1)/p × N (optimal)
  • Best for power-of-2 process counts

Ring AllReduce Deep Dive

Ring AllReduce is the most widely used algorithm in deep learning because it achieves optimal bandwidth utilization regardless of the number of workers. It proceeds in two phases.

Phase 1: Scatter-Reduce (p−1 steps) Each GPU sends 1/p of its data to the next GPU in the ring GPU 0 GPU 1 GPU 2 GPU 3 After p−1 steps: each GPU holds the fully reduced 1/p chunk Phase 2: AllGather (p−1 steps) Each GPU sends its reduced chunk around the ring GPU 0 GPU 1 GPU 2 GPU 3 After p−1 steps: every GPU has the complete reduced result Total: 2(p−1) steps, each transferring N/p data → 2(p−1)/p × N total per GPU

Step-by-step for 4 GPUs: Each GPU divides its data into 4 chunks. In the scatter-reduce phase (3 steps), each GPU sends one chunk to its successor and receives one from its predecessor, accumulating (summing) received chunks with its local data. After 3 steps, each GPU holds the fully reduced version of one unique chunk. The allgather phase (3 more steps) circulates these reduced chunks until every GPU has all 4 reduced chunks.

Bandwidth optimality proof: Each GPU must send at least (p−1)/p × N data (the data it uniquely contributes to the reduction) and receive the same. Ring AllReduce achieves exactly this lower bound: 2(p−1)/p × N total bytes per GPU. No algorithm can do better in terms of bandwidth.

Bandwidth Analysis

Communication performance is modeled using the alpha-beta model: the time for a message of size m bytes is T = α + m/β, where α is latency (seconds per message) and β is bandwidth (bytes/second).

Ring AllReduce time:

Tring = 2(p−1) × α + 2(p−1)/p × N/β

For large messages (N ≫ α × β), the bandwidth term dominates: T ≈ 2N/β. This is independent of p — ring AllReduce scales perfectly with the number of GPUs when bandwidth-bound.

For small messages (many small tensors), the latency term 2(p−1) × α dominates. This is why PyTorch DDP uses gradient bucketing: small gradient tensors are concatenated into large buckets (default 25 MB) before AllReduce, amortizing the per-message latency cost.

# Bandwidth utilization calculation
def allreduce_time(num_gpus, data_bytes, latency_s, bandwidth_bps):
    # Alpha-beta model for ring AllReduce
    alpha_term = 2 * (num_gpus - 1) * latency_s
    beta_term = 2 * (num_gpus - 1) / num_gpus * data_bytes / bandwidth_bps
    return alpha_term + beta_term

# Example: 8 GPUs, 1GB gradients, NVLink
# NVLink: ~600 GB/s bidirectional, ~1μs latency
t_nvlink = allreduce_time(8, 1e9, 1e-6, 600e9)  # ≈ 0.0047s

# Same setup over InfiniBand 200 Gb/s (~25 GB/s)
t_ib = allreduce_time(8, 1e9, 2e-6, 25e9)      # ≈ 0.14s

# 30× slower over IB vs NVLink — this is why
# intra-node topology matters enormously

Network Topology

Modern GPU clusters have a hierarchical network topology with vastly different bandwidths at each level. Understanding this hierarchy is critical for placement decisions and parallelism strategy.

Node 1 (DGX A100) GPU 0 GPU 1 GPU 2 GPU 3 NVSwitch: 600 GB/s all-to-all Node 2 (DGX A100) GPU 4 GPU 5 GPU 6 GPU 7 NVSwitch: 600 GB/s all-to-all InfiniBand HDR 200 Gb/s = 25 GB/s Bandwidth Hierarchy: NVLink (600 GB/s) > PCIe 4.0 (64 GB/s) > IB HDR (25 GB/s) > Ethernet (12.5 GB/s)
Design implication: Keep communication-heavy parallelism (tensor parallel) within a node on NVLink. Use communication-lighter parallelism (data parallel, pipeline parallel) across nodes over InfiniBand. This hierarchical parallelism is why 3D parallelism (TP × PP × DP) maps TP to intra-node and DP/PP to inter-node.

Overlap Strategies

The key to high GPU utilization in distributed training is overlapping communication with computation. If we can transfer gradients while computing the next layer's gradients, communication cost can be hidden.

Gradient bucketing (DDP): PyTorch DDP fires AllReduce for a bucket as soon as all gradients in that bucket are ready. Since backward computes gradients in reverse layer order, the first bucket (last layers' gradients) starts AllReduce while earlier layers are still computing. With proper bucket sizing, up to 60–80% of communication can be overlapped.

# DDP with tuned bucket size for overlap
model = torch.nn.parallel.DistributedDataParallel(
    model,
    device_ids=[local_rank],
    bucket_cap_mb=25,             # Default 25MB — tune for your model
    gradient_as_bucket_view=True, # Memory optimization
    find_unused_parameters=False, # Faster if all params used every step
)

# Smaller buckets → more overlap but more latency overhead
# Larger buckets → less overlap but better bandwidth utilization
# Rule of thumb: start with 25MB, increase if network is slow

Prefetch in FSDP: FSDP performs AllGather for the next layer's parameters while the current layer is computing. This prefetching strategy hides AllGather latency when the compute time exceeds the communication time for one layer's worth of parameters.

When overlap fails: If your per-layer compute time is shorter than the AllReduce time for that layer's gradients, overlap cannot fully hide communication. This happens with (1) very small models on many GPUs, (2) very fast GPUs with slow interconnects, or (3) models with many small layers. In these cases, reduce the number of GPUs or upgrade the interconnect.

Debugging Communication

Distributed communication bugs are among the hardest to debug in ML infrastructure. Here are the essential tools and techniques.

# Enable NCCL debug logging
# Set before launching training
import os
os.environ["NCCL_DEBUG"] = "INFO"        # Shows topology, algorithm selection
os.environ["NCCL_DEBUG_SUBSYS"] = "ALL"  # All subsystems

# Use monitored_barrier for hang detection
import torch.distributed as dist

# This will timeout and report WHICH rank is stuck
dist.monitored_barrier(timeout=timedelta(minutes=5))

# Verify all ranks can communicate
tensor = torch.ones(1).cuda()
dist.all_reduce(tensor)
assert tensor.item() == dist.get_world_size(), "AllReduce failed!"
Common failure modes: (1) Hangs: Usually caused by one rank crashing silently or a rank mismatch in collective calls. Use monitored_barrier after suspicious operations. (2) Slow AllReduce: Check NCCL_DEBUG=INFO for algorithm/topology — NCCL may fall back to slower paths if NVLink isn't detected. (3) Incorrect results: Non-deterministic reductions due to floating-point non-associativity — set NCCL_ALGO=Ring for deterministic (but potentially slower) results.

Debugging Checklist

  • ✓ NCCL_DEBUG=INFO — verify topology detection
  • ✓ nvidia-smi topo -m — check NVLink/PCIe layout
  • ✓ nccl-tests — benchmark raw AllReduce bandwidth
  • ✓ torch.distributed.monitored_barrier — find stuck ranks
  • ✓ Check NCCL version matches across all nodes

Performance Monitoring

  • Log per-step time breakdown: compute vs. comm
  • Monitor GPU utilization (should be >80%)
  • Track AllReduce time vs. backward time (overlap ratio)
  • Use torch.profiler with NCCL trace for comm timeline
  • Compare achieved vs. theoretical bus bandwidth