Cheat sheets

DDP, FSDP, ZeRO and Pipeline Parallel

Pick a parallelism, size the memory, wire the collectives and fix the hangs. PyTorch gloo on CPU.

  • Concepts and terms
  • Tasks and commands
  • Annotated code
  • Diagrams
  • Comparisons
  • Defaults and numbers
  • Troubleshooting
  • Questions and tips

Source

Four strategies side by side#

FactorDDPFSDP or ZeRO-3PipelineTensor parallel
Splitsthe batchparams, grads, optimizerthe model by layerseach weight matrix
Per-GPU memoryfull copy, no drop1/N of each stateone group of layersa slice of each layer
Trafficall-reduce of gradientsall-gather per layer, reduce-scatteractivations at layer boundariesall-reduce after each matmul
Linkoverlaps with computemore than plain DPlow bandwidth, across nodesNVLink inside a node
Main costmemorygather every forward passpipeline bubbleslatency after every layer

ZeRO stages#

FactorDDPZeRO-1ZeRO-2ZeRO-3
Shardednothingoptimiser stateoptim + gradsoptim + grads + params
Memory per rankparams + grads + optimparams + grads + optim/Nparams + grads/N + optim/Nparams/N + grads/N + optim/N
Comm per step1x allreduce1x reduce_scatter + 1x allgather1x reduce_scatter + 1x allgatherallgather and reduce_scatter per layer
Savingsnone~4x reduction~8x reduction~Nx reduction
PyTorch FSDP namenot listednot listedShardingStrategy.SHARD_GRAD_OPFULL_SHARD

Collectives#

FactorRing allreduceTree allreduceAllgatherReduce_scatter
Per-rank bytes2T(N-1)/NT log2(N)T(N-1)/NT(N-1)/N
Steps2(N-1)2 log2(N)N-1N-1
Use whenlarge Tsmall T, high latencyZeRO unshardZeRO gradient sharding
Message sizeabove ~1 MBbelow ~1 MBanyany

Ring allreduce on a queue mesh#

queue 0 to 1queue 1 to 2queue 2 to 3queue 3 to 0processrank 0owns chunk 0processrank 1owns chunk 1processrank 2owns chunk 2processrank 3owns chunk 3queue 0 to 1queue 1 to 2queue 2 to 3queue 3 to 0processrank 0owns chunk 0processrank 1owns chunk 1processrank 2owns chunk 2processrank 3owns chunk 3

DDP worker loop#

dist.init_process_group(
backend="gloo",
init_method=f"file://{init_file}",file rendezvous
rank=rank,
world_size=world_size,
)
torch.manual_seed(SEED)same init on every rank
ddp = DistributedDataParallel(model, world_size=world_size)broadcasts params
for step in range(steps):
offset = step * world_size * batch + rank * batchown slice
optim.zero_grad(set_to_none=True)
loss.backward()
ddp.sync_grads()all_reduce SUM, then div_ by world_size
optim.step()

Troubleshooting#

SymptomCauseFix
Ranks diverge from step oneEach rank uses its own initdist.broadcast(tensor, src=0) at construction
All ranks see the same batchesOne shared seed for shufflingtorch.manual_seed(seed + rank) for shuffling
Allreduce deadlocksSkipped params have no gradientfind_unused_parameters=True
Accumulation is slowAllreduce on every microbatchWrap microbatches in no_sync()
LR breaks when N changesSUM without the divisionDivide by world_size
Pipeline hangs on the wireEvery stage sends before receiveEven ranks send first, odd ranks receive first
One stage gates every cycleStages split by parameter countEqualise FLOPs per stage
70B FSDP over 80GB on 8 GPUs105 GB per GPU16 GPUs or activation checkpointing

Use each one when#

DDP
the model fits on one device. The dataset does not.
FSDP
the model does not fit on one device either.
ZeRO-1
the default. Same bandwidth as DDP, memory drop linear in N.
ZeRO-2 or ZeRO-3
the parameter shard memory is also a problem.
Tensor parallel
inside a node. It needs fast inter-GPU links.
Pipeline
across nodes. Bandwidth needs are low.
DualPipe
EP-heavy MoE at multi-thousand GPU scale.
DualPipeV
dense or EP-light models. One parameter copy.

Memory numbers#

Adam states
m and v in FP32. 70B x 4 bytes x 2 = 560GB.
Vanilla, mixed precision
16P bytes per rank.
ZeRO-1
4P + 12P/N bytes. At N=8 that is 5.5P, a 65% drop.
FSDP, 70B on 8 GPUs
105 GB per GPU. Still over 80GB.
Mixed precision, 7B
112 GB to 84 GB. Optimizer stays FP32.
Effective batch size
per_gpu_batch_size x N.

Pipeline schedules#

Factor1F1BZero BubbleDualPipeDualPipeV
Param copies1121
Bubble vs micro-batchesgrowsgrowsconstantsmall growth
Overlapminimalpartialfullpartial
Simulatorbubble_1f1bbubble_zero_bubblebubble_dualpipebubble_dualpipev

Collective calls#

TaskCode
Set the rendezvous portos.environ["MASTER_PORT"] = str(port)
Start a CPU groupdist.init_process_group(backend="gloo", rank=rank, world_size=world_size)
Same params on all ranksdist.broadcast(p.data, src=0)
Sum gradientsdist.all_reduce(p.grad.data, op=dist.ReduceOp.SUM)
Mean gradientsp.grad.data.div_(world_size)
ZeRO-1 gradient sharddist.reduce_scatter(local_grad, chunks, op=dist.ReduceOp.SUM)
ZeRO-1 params backdist.all_gather(gathered, self.master_shard)
GPipe bubblebubble_fraction(num_stages, num_microbatches)

Run the lessons#

CommandWhat it does
python3 code/main.pyLesson 48: two gloo ranks, writes outputs/ddp-demo.json
--world-size 4Lesson 48: four ranks. Param spread stays under 1e-3
--no-writeLesson 48: skip the JSON file
python3 code/main.pyLesson 76: queue mesh against gloo, byte counter
python3 code/main.pyLesson 77: single process against DDP on 4 ranks
python3 code/main.pyLesson 78: loss and the ZeRO-1 memory table
python3 code/main.pyLesson 79: Gantt chart and bubble percentage

Defaults in the code#

SettingDefaultNote
--world-size2Lesson 48 ranks
--backendglooUse nccl on GPUs
--num-steps6Lesson 48 training steps
--batch-size8Per rank
RECV_TIMEOUT_S30.0Queue mesh receive deadline
DDP bucket size25 MBOne allreduce per bucket
NUM_STAGES4Lesson 79 pipeline
NUM_MICROBATCHES8Lesson 79 pipeline

Q&A#

Why mean and not sum?
An LR tuned at one rank then works at four ranks.
Does mixed precision halve memory?
No. It saves about 25%.
GPipe bubble at M=8, N=4?
27%. At M=64, N=4 it is 4.5%.
GPipe or 1F1B?
Same bubble. 1F1B bounds activation memory.
Why reduce_scatter in ZeRO?
Each rank gets only its own shard sum.
Is ZeRO-3 the same as FSDP?
Yes. The mechanism is the same.
DualPipe under 1k GPUs?
Overkill. Bubbles are a small share of cost.