Skip to content

PyTorch Distributed Training Engine Dissected: Design Evolution from DDP to FSDP ​

Experiment Goal ​

Getting DDP/FSDP running with torchrun is easy, but "why does DDP double the VRAM", "why does FSDP have so much inter-card communication", "when are gradients actually synced" — these questions decide whether you can tune the engine optimally at thousand-GPU scale. This article dissects PyTorch distributed training from the framework engine mechanism view:

  • How does DDP sync gradients exactly? How do communication and compute overlap?
  • What does FSDP change relative to DDP? How are parameter sharding and communication layers designed?
  • Which scenarios pick DDP, which pick FSDP?

This is a mechanism-analysis + config-practice route; commands and configs are generic practice examples without fabricated performance benchmarks; concrete implementation follows the installed PyTorch version.

Section 1: DDP — Engineering Design of Gradient Synchronization ​

DDP (DistributedDataParallel) logic is simple: each card holds a full model copy + its own data; after backward, gradients are AllReduced and each card updates with the averaged gradient. The engineering difficulty lies in sync timing and overhead.

1. Gradient Buckets: Compress the Number of AllReduces ​

A naive approach AllReduces each parameter's gradient separately — a trillion-parameter model has tens of thousands of gradient tensors and communications, unacceptable. DDP uses gradient buckets:

python
# Conceptual snippet: DDP packs consecutive parameter gradients into buckets, AllReducing per bucket
# bucket_cap_mb: default 25MB; bucket size affects communication granularity and overlap
ddp_model = torch.nn.parallel.DistributedDataParallel(
    model,
    bucket_cap_mb=25,        # bucket capacity; small models can lower (e.g. 5MB)
    gradient_as_bucket_view=True,   # reuse bucket memory, fewer copies
)
  • Bucket = communication granularity: larger buckets → fewer communications but longer per-bucket waits; smaller buckets → finer overlap but more communications. 25MB is the default balance; small models can lower it;
  • gradient_as_bucket_view=True: gradients live as views into bucket memory, avoiding extra copies (VRAM and time win).

2. Communication-Compute Overlap: "Communicate While Computing" in Backward ​

DDP's core optimization is communicate-as-soon-as-ready (gradient ready → bucket ready → async AllReduce), overlapping with subsequent layers' backward compute:

backward pass
├─ layer N computes gradient → bucket ready → AllReduce starts (async, non-blocking)
├─ layer N-1 computes gradient (overlapped with N's communication) → ...
└─ all done → last AllReduce completes → update
  • Overlap effectiveness depends on bucket split and layer order: buckets split by parameter registration order; if a bucket spans many layers, its readiness is dragged by the longest layer — why model registration order affects DDP performance;
  • Verify overlap: use PyTorch Profiler to check for obvious communication gaps (AllReduce waiting for gradients) in the backward phase.

3. DDP's VRAM Accounting ​

  • Each card holds the full model → VRAM ≈ single-card model + activations + gradients + optimizer states;
  • Gradient AllReduce traffic ∝ model size (every card sends its own gradients); at thousand-card scale, communication is the bottleneck, not VRAM;
  • Engineering observation: DDP fits "model fits one card, but data/compute needs many cards" (fine-tuning, small-to-medium models); when the model no longer fits one card, move to FSDP.

Section 2: FSDP — From "Full Model Per Card" to "Shard Per Card" ​

FSDP (Fully Sharded Data Parallel)'s core idea: parameters, gradients, and optimizer states are all sharded along the data-parallel dimension, All-Gathering the needed parameters at compute time.

1. Three-Level Sharding: Parameters / Gradients / Optimizer States ​

DimensionDDPFSDP
Parametersfull copy per card1/DP shard per card, All-Gather on use
Gradientsfull per cardshard per card (Scatter-Reduce keeps only local shard)
Optimizer statesfull per cardshard per card (the big VRAM win)
CommunicationAllReduce gradients per stepAll-Gather params + Scatter-Reduce grads (~2-3× DDP traffic)
python
# FSDP basic usage
from torch.distributed.fsdp import FullyShardedDataParallel as FSDP

fsdp_model = FSDP(
    model,
    sharding_strategy=torch.distributed.fsdp.ShardingStrategy.FULL_SHARD,
    mixed_precision=torch.distributed.fsdp.MixedPrecision(
        param_dtype=torch.bfloat16,
        reduce_dtype=torch.bfloat16,
        buffer_dtype=torch.bfloat16,
    ),
)

2. Communication Layers: Flatten & All-Gather/Scatter-Reduce ​

  • Parameter flatten: FSDP flattens (managed) parameters into contiguous storage and All-Gathers whole blocks at once instead of per-parameter communication — fewer communications;
  • Forward: All-Gather a block's parameters before its compute, release after compute (VRAM fetched on demand);
  • Backward: each card computes gradients for its own shard → Scatter-Reduce → keeps only its shard gradient → updates its shard's optimizer state;
  • Traffic accounting: All-Gather (params) + Scatter-Reduce (grads) ≈ 2× parameter size per step — more than DDP's 1×. FSDP trades more communication for VRAM; with a weak network it may not be faster.

3. FSDP Tuning Knobs ​

KnobEffectEngineering advice
sharding_strategyFULL_SHARD / SHARD_GRAD_OP (gradients only, less comm) / NO_SHARDif VRAM allows, SHARD_GRAD_OP cuts communication
Sharding granularity (auto_wrap_policy)per-layer/block vs whole-modelper-layer → finer All-Gather, better overlap
cpu_offloadoffload params/grads to CPUextreme VRAM cases; watch CPU↔GPU transfer cost
forward_prefetch / backward_prefetchprefetch next block earlyoverlaps communication with compute; defaults usually better

Section 3: Selection Matrix — Which Scenario Uses What ​

ScenarioRecommendationWhy
Small/medium model (fits one card) + multi-card data scalingDDPleast communication, simple, VRAM not the constraint
Large model (doesn't fit one card) trainingFSDP (+ CPU offload if needed)VRAM sharding is the hard constraint; trade comm for VRAM
Large-model fine-tuning (LoRA etc.)FSDPsharding frees VRAM for activations/trainable params
Very large + extreme VRAMFSDP + tensor parallelismFSDP alone can't split single oversized layers (huge Embedding/FFN); stack TP

Engineering observation: selection isn't "newer is better" — DDP is the most communication-efficient option for small/medium models; FSDP's value is "VRAM sharding unlocks bigger models". Mixed scenarios (EP×TP×DP×FSDP, Chapter 10 MoE) are another layer of complexity; understand the DDP/FSDP communication-VRAM trade-off first, then stack parallelism dimensions.

Distributed Engine Checklist ​

  • [ ] DDP: bucket size matches model scale (lower bucket_cap_mb for small models); Profiler shows no communication gaps in backward
  • [ ] DDP: gradient_as_bucket_view=True enabled
  • [ ] FSDP: sharding granularity matches model structure (per-layer auto_wrap); mixed precision in the right place
  • [ ] FSDP: network bandwidth assessed — traffic ≈ 2× params; confirm Chapter 03 network supports it
  • [ ] Large-model scenarios: FSDP + TP stacking decided (who shards, who slices layers)
  • [ ] Communication time share included in training logs/Profiler observability

Engineering Takeaways ​

  • Engine mechanisms decide tuning direction: DDP's buckets, FSDP's sharding granularity are all "communication count × wait time" trade-offs — understand the mechanism to know which knob to turn.
  • VRAM and communication are two sides of one coin: FSDP trades 2× communication for VRAM; on a weak network it can be slower — do the math before choosing.
  • Profiler is the engine's stethoscope: communication gaps and All-Gather waits show up in flame graphs — more effective than guessing.

Next article preview: with the training engine ready, the inference side also needs "measurement first" — the next article, LLM Inference Benchmarking: Throughput, TTFT & Cost in Practice, breaks down a unified measurement method for inference services.

AI-HPC Expert · Contact: openaihpc@gmail.com