Skip to content

PyTorch 分布式训练引擎拆解:DDP 到 FSDP 的设计演进 ​

实验目标 ​

用 torchrun 跑起 DDP/FSDP 很容易,但"为什么 DDP 显存翻倍""为什么 FSDP 卡间通信这么多""梯度什么时候同步的"这类问题,决定了你能否在千卡规模把引擎调到最优。本篇从框架引擎机制视角拆解 PyTorch 分布式训练:

  • DDP 的梯度同步到底怎么做的?通信与计算如何重叠?
  • FSDP 相对 DDP 改了什么?参数分片与通信层级如何设计?
  • 什么场景选 DDP、什么场景选 FSDP?

本篇为机制解析 + 配置实践路线,命令与配置为通用实践示例,不编造性能基准;具体实现以所用 PyTorch 版本为准。

第一段:DDP——梯度同步的工程化设计 ​

DDP(DistributedDataParallel)的逻辑很简单:每卡一份完整模型 + 各自数据,反向传播后 AllReduce 梯度,各卡用平均后的梯度更新。工程难点在同步的时机与开销。

1. 梯度桶(Gradient Bucket):把 AllReduce 的次数压下来 ​

朴素做法是每个参数的梯度各 AllReduce 一次——千亿参数就有上万个梯度张量、上万次通信,开销不可接受。DDP 的做法是梯度桶:

python
# 概念示意:DDP 内部把连续参数的梯度打包成桶,按桶做 AllReduce
# bucket_cap_mb:默认 25MB,桶大小影响通信粒度与重叠效果
ddp_model = torch.nn.parallel.DistributedDataParallel(
    model,
    bucket_cap_mb=25,        # 桶容量,小模型可调小(如 5MB)
    gradient_as_bucket_view=True,   # 复用桶内存,减少拷贝
)
  • 桶 = 通信粒度:桶越大通信次数越少但单个桶等待时间越长;桶越小重叠更细但通信次数多。25MB 是默认平衡点,小模型可调小;
  • gradient_as_bucket_view=True:让参数梯度直接以桶内存视图存在,避免额外拷贝(显存与耗时双收益)。

2. 通信与计算重叠:反向传播的"边算边通" ​

DDP 的核心优化是梯度就绪即通信(gradient ready → bucket ready → AllReduce 异步启动),与后续层的反向计算重叠:

backward 反向传播
├─ 层 N 计算梯度 → 桶就绪 → AllReduce 启动(异步,不阻塞)
├─ 层 N-1 计算梯度(与 N 的通信重叠)→ ...
└─ 全部完成 → 最后一次 AllReduce 完成 → 更新
  • 重叠是否有效取决于桶切分与层顺序:桶按参数注册顺序切分,若某桶跨越多个层,就绪时间被最长层拖累——这就是为什么模型注册顺序影响 DDP 性能;
  • 检查重叠效果:用 PyTorch Profiler 看 backward 阶段是否存在明显的通信空洞(AllReduce 在等待梯度)。

3. DDP 的显存账 ​

  • 每卡持有完整模型 → 显存 ≈ 单卡模型 + 激活 + 梯度 + 优化器状态;
  • 梯度 AllReduce 的通信量与模型大小成正比(每卡都要发自己的梯度),千卡时通信是瓶颈而非显存;
  • 工程观察:DDP 适合"模型能塞进单卡、但数据/算力需要多卡"的场景(微调、中小模型);模型大到单卡放不下时,进入 FSDP。

第二段:FSDP——把"每卡一份模型"改成"每卡一份分片" ​

FSDP(Fully Sharded Data Parallel)的核心思想:参数、梯度、优化器状态都按数据并行维度分片,计算时再 All-Gather 出需要的参数。

1. 三级分片:参数/梯度/优化器状态 ​

维度DDPFSDP
参数每卡完整副本每卡 1/DP 分片,用时 All-Gather
梯度每卡完整每卡分片(Scatter-Reduce 后只保留本卡分片)
优化器状态每卡完整每卡分片(显存大头省在这里)
通信每步 AllReduce 梯度All-Gather 参数 + Scatter-Reduce 梯度(通信量约 2-3 倍于 DDP)
python
# FSDP 基础用法
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. 通信层级:Flatten 与 All-Gather/Scatter-Reduce ​

  • 参数 flatten:FSDP 把(受管)参数展平成连续存储,一次 All-Gather 取回一整块,而不是逐个参数通信——降低通信次数;
  • 前向:每层/每块计算前 All-Gather 该块参数,计算后释放(显存按需取用);
  • 反向:各卡计算本卡分片的梯度 → Scatter-Reduce 聚合 → 只保留本卡分片梯度 → 更新本卡分片优化器状态;
  • 通信量账:每步 All-Gather(参数)+ Scatter-Reduce(梯度)≈ 2 倍参数量的通信,比 DDP 的 1 倍多——FSDP 用更多通信换显存,网络是瓶颈时未必更快。

3. FSDP 的调优旋钮 ​

旋钮作用工程建议
sharding_strategyFULL_SHARD / SHARD_GRAD_OP(只分片梯度,省通信)/ NO_SHARD显存够则 SHARD_GRAD_OP 减少通信
分片粒度(auto_wrap_policy)按层/块分片 vs 整模型分片按层分片 → 更细粒度 All-Gather,重叠更好
cpu_offload参数/梯度卸载到 CPU显存极限场景,注意 CPU↔GPU 传输开销
forward_prefetch / backward_prefetch提前预取下一块参数与计算重叠通信,默认开启更优

第三段:选择矩阵——什么场景用什么 ​

场景推荐理由
中小模型(单卡可放)+ 多卡扩展数据DDP通信最少、实现简单、显存不是瓶颈
大模型(单卡放不下)训练FSDP(+ 必要时 CPU offload)显存分片是硬约束,通信换显存
大模型微调(LoRA 等)FSDP分片后显存给激活/可训练参数留空间
超大规模 + 极端显存FSDP + 张量并行组合单靠 FSDP 无法拆分单层超大权重(如超大 Embedding/FFN),需 TP 叠加

工程观察:选择不是"越先进越好"——DDP 在中小模型上是通信最省的方案;FSDP 的价值在"显存分片解锁更大模型"。混合场景(EP×TP×DP×FSDP,10 章 MoE 篇)是另一层复杂度,先理解 DDP/FSDP 的通信-显存权衡,再叠加并行维度。

分布式引擎检查清单 ​

  • [ ] DDP:桶大小与模型规模匹配(小模型调小 bucket_cap_mb);Profiler 确认反向通信无空洞
  • [ ] DDP:gradient_as_bucket_view=True 已开启
  • [ ] FSDP:分片粒度与模型结构匹配(按层 auto_wrap),混合精度位置正确
  • [ ] FSDP:网络带宽评估——通信量约 2 倍参数,确认 03 章网络能支撑
  • [ ] 大模型场景:FSDP 与 TP 叠加方案明确(谁分片、谁切层)
  • [ ] 训练日志/Profiler 纳入通信耗时占比观测

工程启示 ​

  • 引擎机制决定调优方向:DDP 的桶、FSDP 的分片粒度,都是"通信次数 × 等待时间"的权衡——理解机制,才知道旋钮往哪拧。
  • 显存与通信是同一枚硬币:FSDP 用 2 倍通信换显存,网络弱时反而更慢;选型先算账。
  • Profiler 是引擎的听诊器:通信空洞、All-Gather 等待都体现在火焰图里,比猜有效。

下一篇预告:训练引擎就绪,推理侧同样需要"测量先行"——下一篇《LLM 推理性能基准方法:吞吐、TTFT 与成本实测》拆解推理服务的统一测法。

AI-HPC 智算超算专家 · Contact: openaihpc@gmail.com