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 做的就是这件事。

面试会这样问How does FSDP work? · What do ZeRO stages 1, 2 and 3 shard, and what does each cost in communication? · How does DDP overlap gradient communication with backward?

考过NVIDIA Research Intern("How does torch FSDP work?",拿到 offer,Glassdoor RVW102358730,年份未核实)· NVIDIA 框架岗 2023 onsite 第 3 轮(实现 backprop 后追问实现 grad-accumulation,知乎)· NVIDIA DL 全职 VO 2022(讨论"为什么by default grad 要累计")· 文心("deepspeed zero1-3")· 字节("FSDP2和FSDP1的区别")· 牛客("DeepSpeed ZeRO stage1和stage 2的通信量区别,论文和代码实现有没有gap")· 快手("DP、TP-SP的计算通信重叠原理")· xAI ML/Infra 第一轮补全 DP 和 FSDP 的 distributed matmul(1p3a 1171574)

v2:DDP 的一个 step 做五件事

DDP(DistributedDataParallel)是最直接的多卡训练。每张卡做同样的事,只是吃不同的数据。

  1. 复制:32 张卡各放一份完整模型,开始时 rank0 把初始 weight broadcast 给所有卡
  2. 切 batch:一个 step 的 batch 切成 32 份,每张卡一份
  3. 各算各的:每张卡用自己那份 forward、backward,得到自己的 gradient
  4. all-reduce gradient:求平均,每张卡拿到同一个 gradient
  5. 各自更新:起点一样、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。好比做好一道菜就先端出去,不等一桌菜全做完。

gradient accumulation 和 global batch

一张卡一次只放得下 b 条样本(micro-batch),想要更大的 batch 就连跑 m 个 micro-batch。每次 backward 的 gradient 都加到 .grad 上(PyTorch 默认就是累加),攒满 m 次才更新一次,每个 micro-batch 的 loss 要先除以 m。

\[ \text{global batch} = \text{micro-batch} \times \text{accumulation steps} \times \text{DP size} \]

例(假设的数):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 装的是什么:

ZeRO(Zero Redundancy Optimizer)的想法是每张卡只保管、只更新 1/N,要用别人那份时再借。

v3:ZeRO 一级一级往下切

做法切掉什么每卡 model stateN = 32、Ψ = 8B每卡每步发送
DDP不切\(16\Psi\)128 GBall-reduce,31 GB
ZeRO-1optimizer state\(4\Psi + 12\Psi/N\)32 + 3 = 35 GBreduce-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 把这两步拆开,在中间插入更新:

  1. reduce-scatter gradient:每张卡只拿到自己负责的那 1/N 的 gradient 总和
  2. 只更新自己那份:用自己保管的 1/N optimizer state,更新 1/N 的 parameter
  3. 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(一般一层一个)逐个处理:

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 组合。

另外两个名字

去查一下面试一定会问你在 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 还剩什么问题

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 次。