DDP 与 FSDP 本节摘要:多 rank 训练是两个集合通信加一条规则。启动时广播(broadcast)参数,反向后平均(all-reduce)梯度,各 rank 永不对当前在步几上产生分歧。数据并行(DDP):每 rank 在批的不同切片上跑同一模型,优化器步前平均梯度。FSDP:模型装不下单设备,每 rank 持每参数的一份,前向时逐层 all-gather 重组全张量、用后丢弃。痛在簿记:参数跨 rank 漂移则运行静默损坏;平均梯度不平均损失则仪表盘撒谎;集合后端对拓扑达不成一致则运行永远挂。修复是手写一遍集合通信,再不信你复现不出的包装器。本节跑在 CPU( 后端),CUDA 不假设;同代码切 即上多 GPU。
本节摘要:多 rank 训练是两个集合通信加一条规则。启动时广播(broadcast)参数,反向后平均(all-reduce)梯度,各 rank 永不对当前在步几上产生分歧。数据并行(DDP):每 rank 在批的不同切片上跑同一模型,优化器步前平均梯度。FSDP:模型装不下单设备,每 rank 持每参数的一份,前向时逐层 all-gather 重组全张量、用后丢弃。痛在簿记:参数跨 rank 漂移则运行静默损坏;平均梯度不平均损失则仪表盘撒谎;集合后端对拓扑达不成一致则运行永远挂。修复是手写一遍集合通信,再不信你复现不出的包装器。本节跑在 CPU(
gloo后端),CUDA 不假设;同代码切nccl即上多 GPU。
对应原课程:Phase 19 · Lesson 48 ·
distributed-fsdp-ddp(原英文phases/19-capstone-projects/48-distributed-fsdp-ddp/docs/en.md)。本节属「预训练/分布式」赛道第七节。
阅读完本节,你应当能够:
gloo 后端跨 N rank 起进程组,无需特殊硬件。模型装得下单设备,数据集装不下。优化预算要每墙秒看 N 倍样本。第一杠杆是数据并行:每 rank 在批不同切片上跑同模型,优化器步前平均梯度。第二杠杆是 FSDP:模型也装不下,每 rank 持每参数的一份,前向时逐层重组全张量。
| 集合 | 做什么 | 何时 |
|---|---|---|
broadcast |
把张量从一个 rank 拷给所有其他 | 参数初始化、调度器状态、任何一对多同步 |
all_reduce |
跨所有 rank 对张量求和(或均、或大),每 rank 得结果 | 反向后梯度平均 |
all_gather |
每 rank 贡献一张量,每 rank 得拼接 | logits 收集、FSDP 参数 unshard |
DDP 契约是构造时 broadcast、反向后 all_reduce。FSDP 草图在每层前向前加 all_gather。
在 N rank 上跨批 B 例训的模型,必产出与单进程在 N*B 批上训相同的梯度。诀窍是每 rank 梯度求和再除 N 给平均损失梯度,正是交叉熵(mean 归约)在全批上会产出的。本节代码用 max-abs-diff < 1e-3 断言手写 all-reduce 梯度与参考单进程梯度一致。
内存赢是精确的:每 rank 参数内存降到 1/N。成本是 gather,每次前向付。生产 FSDP 把 gather 与上一层计算重叠,墙时间成本远小于朴素会计。本节对每参数做往返,断言重组与原张量位等。
CUDA 是生产目标,但同代码路径在 CPU 上存在。gloo 是 CPU 集合后端,比 GPU 上 nccl 慢几个数量级,但 API 面相同。本节进程组用 backend="gloo",rank 用 torch.multiprocessing spawn(而非 torchrun);两者最终到同一 torch.distributed 调用。多 GPU 节点上,改动只是 backend="nccl"、设备张量、torchrun 启动。
code/main.py 五步。
起进程组:
os.environ["MASTER_ADDR"] = "127.0.0.1" os.environ["MASTER_PORT"] = str(port) # bind-and-close 取空闲端口 dist.init_process_group(backend="gloo", rank=rank, world_size=world_size)
MASTER_ADDR/MASTER_PORT 是 rendezvous:每 rank 拨同主机同端口。
构造时广播:MinimalDDP.__init__ 遍历每参数与缓冲,调 dist.broadcast(tensor, src=0)。rank 0 的值成规范初始化——无此,每 rank 用自己种子初始化,rank 从步一就分歧。
反向后 all-reduce 梯度:
def all_reduce_grads_(module, world_size): for p in module.parameters(): if p.grad is None: p.grad = torch.zeros_like(p.data) dist.all_reduce(p.grad.data, op=dist.ReduceOp.SUM) p.grad.data.div_(world_size) # 求和后除 N -> 均梯度
每 rank 得相同均梯度,优化器步是同输入的函数,故参数跨运行保持同步。
证等价:manual_all_reduce_matches_single_process 在 rank 0 建同模型,比 all-reduce 后梯度与单进程在拼接输入上会算的梯度,max-abs-diff 约 1e-8。
FSDP 往返:fsdp_round_trip_sketch 展平每参数、填到 world_size 倍数、切片、all-gather、去填,每 rank 重组等于原张量。
设计要点:broadcast 在构造时是防 rank 分歧的根——无它各 rank 从步一就不同。all-reduce 后除
world_size给均梯度,匹配交叉熵 mean 归约。FSDP 的 gather 一次前向付一次,生产上靠与上层计算重叠隐藏成本。本节每步都断言位等,因分布式静默损坏最难诊断。
PyTorch 的 DistributedDataParallel 加:反向后梯度钩子使 all-reduce 与反向重叠、分桶 all-reduce 把几个小梯度合并成一个集合、第 44 节用的 no_sync 上下文。PyTorch FSDP 加:每层扁平参数视图使每 rank 持一块连续缓冲、下层 unshard 与本层计算重叠、可选 CPU offload 分片。形状不变:启动 broadcast、反向后 reduce、装不下时分片。本节手写让你看清两个集合通信的裸调用、为什么 broadcast 防 rank 分歧、为什么除 N 给均梯度。生产栈(DeepSpeed ZeRO、Megatron-LM)在 FSDP 基础上还分片优化器状态与梯度(ZeRO-2/3),但根契约仍是 broadcast + all-reduce。
code/main.py + outputs/skill-distributed-fsdp-ddp.md(新训练脚本食谱:CPU 用 gloo、GPU 用 nccl 起进程组,模型包 DDP 壳(broadcast 构造、reduce 反向后),可选 all_gather 模式分片参数)。demo 默认 world_size=2,两 CPU 进程 spawn、经 gloo 互谈、退零。outputs/ddp-demo.json 捕获每 rank 参数和、all-reduce 后梯度范数、FSDP 往返结果、手写 vs 参考梯度 diff。
--world-size 4 跑,确认参数跨运行散度 < 1e-3。dist.all_reduce(op=dist.ReduceOp.AVG),计时差异。max-abs-diff < 1e-8。下一节,我们做「LM 评估框架」——把任务定义、指标、运行器、排行榜包进一个短、可换的形状。