v2 → v3:DDP 到 ZeRO / FSDP
v2 用 DDP 把训练铺到 32 张卡上,快了约 32 倍,但每张卡仍要放一整份 128 GB 的 model state。v3 发现这 32 份一模一样,于是用 ZeRO / FSDP 把它们切开,每卡降到 35 GB、19.5 GB,最少 4 GB。你在 TikTok 用的 DeepSpeed 做的就是这件事。
v2:DDP 的一个 step 做五件事
DDP(DistributedDataParallel)是最直接的多卡训练。每张卡做同样的事,只是吃不同的数据。
- 复制:32 张卡各放一份完整模型,开始时 rank0 把初始 weight broadcast 给所有卡
- 切 batch:一个 step 的 batch 切成 32 份,每张卡一份
- 各算各的:每张卡用自己那份 forward、backward,得到自己的 gradient
- all-reduce gradient:求平均,每张卡拿到同一个 gradient
- 各自更新:起点一样、gradient 一样、optimizer 一样,新 weight 必然一样,所以 weight 本身从来不用传
每个 step 只同步一次 16 GB 的 gradient,按第 3 课的 ring 公式每卡发 2 × 31/32 × 16 = 31 GB。
bucket:backward 还没算完就开始发
backward 从最后一层往前算,最后几层的 gradient 最先算好。DDP 把 gradient 按顺序装进 bucket(默认每个 25 MiB),装满一个就立刻异步 all-reduce,GPU 接着算前面层的 backward。好比做好一道菜就先端出去,不等一桌菜全做完。
- 8B 的 16 GB gradient 大约装成 600 个 bucket(16 GB ÷ 25 MiB ≈ 610)
- 只要通信跟得上 backward,只有最后一个 bucket 的通信露在外面(exposed)
gradient accumulation 和 global batch
一张卡一次只放得下 b 条样本(micro-batch),想要更大的 batch 就连跑 m 个 micro-batch。每次 backward 的 gradient 都加到 .grad 上(PyTorch 默认就是累加),攒满 m 次才更新一次,每个 micro-batch 的 loss 要先除以 m。
例(假设的数):b = 2、m = 4、32 张卡,一个 step 是 2 × 4 × 32 = 256 条样本。DDP 下前 m − 1 次放进 model.no_sync() 只在本卡攒,最后一次才 all-reduce。activation 只按 b 算,所以 accumulation 是拿时间换 memory。
DDP 的问题:32 份一模一样的 128 GB
DDP 快了,memory 却一点没省,8B 全参训练用纯 DDP 根本跑不起来。再看每张卡这 128 GB 装的是什么:
- optimizer state(fp32 master weight、m、v):8B × 12 = 96 GB。32 张卡上 32 份完全一样,合计 3 TB 存的是同一份东西
- gradient 在 all-reduce 之后也一样,parameter 更新后也一样
ZeRO(Zero Redundancy Optimizer)的想法是每张卡只保管、只更新 1/N,要用别人那份时再借。
v3:ZeRO 一级一级往下切
| 做法 | 切掉什么 | 每卡 model state | N = 32、Ψ = 8B | 每卡每步发送 |
|---|---|---|---|---|
| DDP | 不切 | \(16\Psi\) | 128 GB | all-reduce,31 GB |
| ZeRO-1 | optimizer state | \(4\Psi + 12\Psi/N\) | 32 + 3 = 35 GB | reduce-scatter + all-gather,31 GB |
| ZeRO-2 | 再切 gradient | \(2\Psi + 14\Psi/N\) | 16 + 3.5 = 19.5 GB | 同上,31 GB |
| ZeRO-3 / FSDP | 再切 parameter | \(16\Psi/N\) | 4 GB | 多一次 all-gather,46.5 GB(1.5×) |
公式里没切的部分每卡一整份,切了的部分除以 N。activation 不在表里,要另算。
ZeRO-1/2 为什么不多花通信
靠的就是第 3 课的等式:all-reduce = reduce-scatter + all-gather。ZeRO 把这两步拆开,在中间插入更新:
- reduce-scatter gradient:每张卡只拿到自己负责的那 1/N 的 gradient 总和
- 只更新自己那份:用自己保管的 1/N optimizer state,更新 1/N 的 parameter
- all-gather parameter:把各卡更新好的那份拼回完整的 bf16 parameter,下一步 forward 要用
ZeRO-2 再多做一点:backward 时每个 bucket 一做完 reduce-scatter,不归自己管的 gradient 就立刻释放,所以 gradient 也只剩 1/N。
ZeRO-3 / FSDP:每层用之前借,用完就还
ZeRO-3 连 parameter 也切了,每张卡平时只有每层 1/32 的 weight。PyTorch 里的同一种做法叫 FSDP(Fully Sharded Data Parallel)。模型分成若干 unit(一般一层一个)逐个处理:
- forward:all-gather 这一层的完整 parameter → 算 forward → 释放不归自己的部分
- backward:再 all-gather 一次 → 算 gradient → reduce-scatter,每卡只留自己那 1/N 的 gradient
- prefetch:算这一层的时候,下一层的 all-gather 已经发出去了,通信藏在计算后面
Qwen3-VL-8B 的一层约 0.19B parameter,bf16 约 0.39 GB。所以每卡 memory ≈ 切好的 4 GB + 正在用和 prefetch 的一两层完整 weight + activation。
FSDP2 和 FSDP1:FSDP1 把一组 parameter 摊平拼成一个大的 FlatParameter 再切。FSDP2 用 DTensor(PyTorch 的分布式 tensor)把每个 parameter 各自按第 0 维切,更容易和 TP 组合。
另外两个名字
- Megatron 的 distributed optimizer ≈ ZeRO-1,只切 optimizer state。Megatron 常用 bf16 param + fp32 gradient,所以每卡是 \(6\Psi + 12\Psi/N\),32 卡约 48 + 3 = 51 GB
- offload(ZeRO-Offload):把 optimizer state(甚至 parameter)放到 CPU memory,在 CPU 上做 optimizer step。代价是每步都要走 PCIe,CPU 也算得慢,step 明显变慢
去查一下面试一定会问你在 TikTok 的 setup。翻出当时的 DeepSpeed config(ds_config)和训练日志,记下:zero_optimization.stage 是哪一级,为什么选它;有没有 offload_optimizer / offload_param;train_micro_batch_size_per_gpu、gradient_accumulation_steps、train_batch_size 各是多少,验证 micro-batch × accumulation × 32 = global batch(写的是 "auto" 就去 HF TrainingArguments 里找);每卡 memory 峰值多少,和上表的 model state 对一对,差出来的大头就是 activation。另外,verl 的训练端可以选 FSDP 或 Megatron(配置里 actor 的 strategy),你在 OpenManus-RL 里用的是哪个?
v3 还剩什么问题
- activation 不切:ZeRO 只切 model state。一条 8K token 的样本开 FlashAttention 还有约 41 GB,每张卡都得自己扛,是 ZeRO-3 那 4 GB 的 10 倍
- DP 卡数 ≤ global batch 的样本数:每张卡至少分到 1 条样本,global batch 256 条就最多 256 张卡。再加卡只能加大 batch,batch 太大又伤收敛
- 70B 以上必须跨机切:70B × 16 = 1120 GB,一台机器 8 × 80 = 640 GB 装不下。于是每一层的两次 all-gather 和一次 reduce-scatter 都要走慢 9 倍的 IB
30 秒英文回答
In DDP every GPU holds a full replica, trains on its own data, and all-reduces gradients bucket by bucket during backward, so every GPU applies the same update. The problem is memory: mixed-precision Adam needs 16 bytes per parameter, 128 GB for an 8B model on every GPU, all identical. ZeRO shards them across data-parallel ranks: stage 1 shards optimizer states, stage 2 adds gradients, stage 3 adds parameters; on 32 GPUs that's 35, 19.5 and 4 GB. Stages 1 and 2 split the all-reduce into a reduce-scatter and an all-gather, so communication is unchanged. Stage 3, which is FSDP, re-gathers each layer's parameters in backward, about 1.5×.
自测
How does FSDP work?(NVIDIA Research Intern 原题)
每张卡只存每个 unit 1/N 的 parameter、gradient 和 optimizer state。forward 和 backward 前各 all-gather 一次完整 parameter,用完就释放。gradient 用 reduce-scatter 同步,各卡只更新自己那份。prefetch 让通信和计算 overlap。每卡 model state 约 \(16\Psi/N\),通信约是 DDP 的 1.5 倍。
32 卡训 8B,ZeRO-2 够吗?为什么不直接 ZeRO-3?
ZeRO-2 每卡 19.5 GB,加上约 41 GB 的 activation(一条 8K token 样本,开 FlashAttention)约 60 GB,放得进 80 GB,再开 recompute 只要约 22 GB。ZeRO-3 再省 15.5 GB,代价是每卡每步多发 15.5 GB,每层还要等 all-gather,放得下就不值。
8B 在 32 卡上为什么用不着 Megatron 的 TP/PP?
ZeRO 已经放得下,DP 每个 step 只同步一次 gradient,还能藏在 backward 后面。TP 每层要 4 次同步 all-reduce,PP 有 bubble(流水线上的空等),都是额外代价,要到上面三个问题出现时才值得。
ZeRO-1 和 ZeRO-2 的通信量有区别吗?(牛客原题)
按论文没有,都是 gradient reduce-scatter 加 parameter all-gather,每卡约 31 GB,和 DDP 的 all-reduce 一样。区别只在 memory,ZeRO-1 要留一整份 gradient。
micro-batch 4、accumulation 8、64 张卡,global batch 多大?DDP 每个 step all-reduce 几次?
4 × 8 × 64 = 2048 条样本。前 7 个 micro-batch 放进 no_sync(),每个 step 只在最后 all-reduce 1 次;不用 no_sync() 就是 8 次。
想看细一点 6.6 DP、ZeRO、FSDP 与 distributed optimizer(公式推导、hybrid sharding、手写 FSDP linear)· 6.20 gradient accumulation(loss ÷ m 的手算例子和代码)· 6.17 overlap(DDP bucket、FSDP prefetch 的细节)· 6.10 training memory