CS336 · 第 8 课

ZeRO 优化器与 FSDP

上一讲说明了如何把计算分到多张 GPU;这一讲处理更现实的容量约束:即使计算可以并行,每个 data-parallel rank 仍可能保存一整份参数、梯度和优化器状态。ZeRO / FSDP 的目标是消除这些冗余,但代价是按需通信。

授课:Tatsu Hashimoto 预计用时:80 分钟 难度:困难

Spring 2026 官方来源:Lecture 8 PDF · 课程日程

本页是中文学习导读;讲次主题、教师与材料版本以 Stanford 官方页面和讲义为准。


这一讲到底解决什么问题?

DDP 通过复制模型来扩展吞吐,因此同步后每个 rank 拥有相同参数、梯度与 optimizer state。对大模型而言,问题不是集群总显存不够,而是这些相同状态被重复存了很多份。

30 秒知识地图

DDP 状态冗余 → ZeRO 分阶段分片 optimizer / gradient / parameter → FSDP 按 unit All-Gather 参数、Reduce-Scatter 梯度 → mixed precision 与 checkpointing 继续压缩峰值 → 与 TP / PP 组合成多维并行。

本课问题与 prerequisite refresher

上一课知道 DDP 每卡有完整副本。本课逐项问:parameter、gradient、Adam \(m/v\) 在 forward、backward、update 的哪个时刻必须完整?若某对象只在局部时刻需要,就可能其余时间只保存 shard。

ZeRO stages 改的是不同对象

                 parameter   gradient   optimizer m/v
DDP                 copy        copy          copy
ZeRO-1              copy        copy          shard
ZeRO-2              copy        shard         shard
ZeRO-3 / FSDP FULL_SHARD
                    shard       shard         shard

steady-state shard ≠ peak memory during all-gather
state sharding ≠ activation sharding

4 ranks、12 个参数元素

ZeRO-3 steady state 可让每 rank 常驻约 3 个 parameter elements。但某 FSDP unit forward 前,要 All-Gather 该 unit 的完整参数进行 matmul,计算后 reshard;backward 又可能 gather,随后 Reduce-Scatter gradients。由此可见“除以 4”描述常驻份额,不自动描述峰值。

为什么不是 stage 越高永远越好

Stage 3 容量最好,却增加 parameter materialization、通信次数与管理开销;小模型或慢网络上 DDP 可能更快。Activation checkpointing 也不是 sharding:它丢弃部分 forward activations,backward 时重算,用额外 FLOPs 换显存。

DDP 的冗余

在标准 Data Parallelism 中,每个 GPU 都存了完整副本:

每个 GPU 存储(70B 模型, fp32 + AdamW):
参数:     4 × 70B = 280 GB
梯度:     4 × 70B = 280 GB
优化器 m: 4 × 70B = 280 GB
优化器 v: 4 × 70B = 280 GB
─────────────────────────────
总计:     1120 GB  ← 远超单卡 80 GB!

# 问题: 8 个 GPU 各存一份 → 8 × 1120 GB = 8960 GB
# 其中 7/8 是冗余的!

核心洞察:在 DDP 中,每个 GPU 最终得到的是相同的梯度(All-Reduce 后),然后用相同的梯度更新参数。这意味着每个 GPU 的优化器状态在每一步都是相同的——完全冗余!


ZeRO:消除冗余

ZeRO 的三阶段

ZeRO(Zero Redundancy Optimizer, Rajbhandari et al. 2020)通过分片消除冗余:

Stage分片什么每 rank 持久模型状态主要通信
Stage 1优化器状态参数/梯度复制,optimizer states 为 1/NReduce-Scatter 梯度 + All-Gather 更新后参数;带宽量可与 DDP 同阶
Stage 2+ 梯度参数复制,梯度与 optimizer states 为 1/N分层 Reduce-Scatter + 参数 All-Gather
Stage 3+ 参数参数、梯度、optimizer states 近似 1/N按需参数 All-Gather + 梯度 Reduce-Scatter

分片越深入,持久显存越低,但计算前越需要重新取得当前参数。Stage 3 的关键代价是:每个 FSDP unit 通常在前向、反向分别 All-Gather 参数,并在反向 Reduce-Scatter 梯度;能否隐藏这些通信取决于 unit 粒度、计算量、网络和 prefetch。

Deep Dive · Stage 1 / 2 / 3 的逐项显存核算

ZeRO Stage 1:分片优化器状态

标准 DDP:
# 每个 GPU 存完整的 m 和 v(各 4P bytes)

# ZeRO Stage 1:
# 把 m 和 v 均分到 N 个 GPU
# GPU i 只存 m[i*N/N : (i+1)*N/N] 和 v[...]

# 每个 GPU 内存:
# 参数: 4P(完整)
# 梯度: 4P(完整)
# 优化器: 8P/N(分片)
# 总计: 8P + 8P/N bytes

# 8 GPU: 8P + P = 9P(vs 标准 16P)→ 节省 44%

ZeRO Stage 2:+ 分片梯度

ZeRO Stage 2:
# 梯度也分片——每个 GPU 只存自己负责的那部分参数的梯度

# 关键: All-Reduce → Reduce-Scatter
# Reduce-Scatter 让每个 GPU 只得到自己那部分的聚合梯度

# 每个 GPU 内存:
# 参数: 4P(完整)
# 梯度: 4P/N(分片)
# 优化器: 8P/N(分片)
# 总计: 4P + 12P/N bytes

# 8 GPU: 4P + 1.5P = 5.5P → 节省 66%

ZeRO Stage 3:+ 分片参数

ZeRO Stage 3:
# 参数也分片——每个 GPU 只存 1/N 的参数

# 前向/反向计算时:
# 1. All-Gather 获取完整参数
# 2. 计算
# 3. 丢弃非本地的参数分片

# 每个 GPU 内存:
# 参数: 4P/N(分片)
# 梯度: 4P/N(分片)
# 优化器: 8P/N(分片)
# 总计: 16P/N bytes

# 8 GPU: 2P → 节省 87.5%!

ZeRO 内存总结

70B 模型, 8 GPU, fp32 + AdamW

标准 DDP:     16P = 1120 GB/GPU  ← 放不下
ZeRO Stage 1: 9P  = 630 GB/GPU   ← 还是放不下
ZeRO Stage 2: 5.5P = 385 GB/GPU  ← 还是放不下
ZeRO Stage 3: 2P  = 140 GB/GPU   ← 还是放不下(但接近了!)

# 加上 bf16 混合精度:
# 参数 bf16: 2P/N
# 主参数 fp32: 4P/N
# 梯度 bf16: 2P/N
# 优化器 fp32: 8P/N
# 总计: 16P/N bytes

# 8 GPU: 2P = 140 GB → 还是超
# 16 GPU: 持久模型状态约 70 GB/GPU
# 这仍未计入完整参数 All-Gather buffer、activation、allocator 碎片和通信 workspace,
# 因此不能仅凭 70 < 80 GB 断言一定放得下。

FSDP:PyTorch 原生的 fully sharded data parallelism

什么是 FSDP?

FSDP (Fully Sharded Data Parallel) 是 PyTorch 原生的 fully sharded data parallelism。其 FULL_SHARD 会分片 parameters、gradients 与 optimizer states,因此状态分片行为与 ZeRO Stage 3 的核心思想高度类似;但两者不是同一个框架实现,也不应把所有 FSDP 配置都等同于 ZeRO-3。

Implementation · PyTorch FSDP 包装示例
from torch.distributed.fsdp import FullyShardedDataParallel as FSDP

# 包装模型
model = FSDP(
    model,
    sharding_strategy=ShardingStrategy.FULL_SHARD,  # ZeRO Stage 3
    cpu_offload=CPUOffload(offload_params=False),
    auto_wrap_policy=transformer_auto_wrap_policy,
)

FSDP 的工作流程

  1. All-Gather 参数:计算当前 FSDP unit 前,各 rank 临时恢复该 unit 的完整参数。
  2. 本地计算:每个 rank 仍处理不同的数据片,完成 forward 或 backward。
  3. 释放非本地参数:计算结束后重新只保留本 rank 的 shard,以降低持久显存。
  4. Reduce-Scatter 梯度:反向结束后聚合梯度,并让每个 rank 只保留自己负责的部分。

这解释了 FSDP 的核心权衡:它用短暂的完整参数 buffer 和更多 collective,换取大部分时间只保存 \(1/N\) 的模型状态。

Deep Dive · FSDP 通信量与 sharding strategy

FSDP 的通信量

令某个 FSDP unit 的参数/梯度 payload 为 S bytes、world_size=N:
# 前向 All-Gather:              (N-1)/N × S per rank
# 反向参数 All-Gather:          (N-1)/N × S per rank
# 反向梯度 Reduce-Scatter:      (N-1)/N × S per rank
# 总计:                    3(N-1)/N × S,N 大时约 3S

# 同一 payload 的 ring DDP All-Reduce 约为 2(N-1)/N × S

# FSDP 通信更多,但内存更少
# 适合: 模型太大,DDP 放不下时

FSDP 策略对比

策略描述内存通信
FULL_SHARD参数、梯度、optimizer state 全分片;行为与 ZeRO-3 高度类似持久模型状态最低参数 gather 最频繁
SHARD_GRAD_OP反向后分片梯度与 optimizer states;前向参数保持未分片介于 DDP 与 FULL_SHARD依 PyTorch 版本和 reshard 行为而定
NO_SHARD不分片;新代码通常直接使用 DDP模型状态复制梯度 All-Reduce
HYBRID_SHARD组内 FULL_SHARD,组间复制按 shard group 缩小组内 gather/scatter + 组间同步

HYBRID_SHARD(拓扑相关选项):

  • 节点内(8 GPU):FULL_SHARD,利用 NVLink 高带宽
  • 节点间:DDP 复制,减少跨节点通信
  • 可在内存节省与跨节点通信之间折中,但不是所有集群的默认最优

混合精度 + 并行

混合精度下的内存

混合精度训练 (bf16 前向 + fp32 主参数):

参数:
bf16 参数: 2P(用于前向/反向计算)
fp32 主参数: 4P(用于优化器更新)
梯度:
fp32 梯度: 4P(用于优化器)
优化器:
fp32 m: 4P
fp32 v: 4P

总计: 18P bytes(vs fp32 的 16P,稍多但激活值省一半)

混合精度 + FSDP

FSDP + 混合精度:
参数、梯度和 optimizer states 的持久 shard 可近似为上述预算的 1/N

8 GPU, 70B 模型:
每 GPU: 18P / 8 = 2.25P = ~157 GB → 还是超
16 GPU: 18P / 16 = ~79 GB 的持久状态下界

实际峰值还包括 All-Gather 后的完整 FSDP unit、activations、临时 buffer 与碎片;
79 GB 持久状态不可能安全装入 80 GB GPU,需要更细 sharding、offload、
更低状态精度或更多设备,并常配合 activation checkpointing。

Gradient Checkpointing

用计算换内存——不保存中间激活,反向时重算:

from torch.utils.checkpoint import checkpoint

# 不 checkpoint: 保存较多前向激活,通常随层数 L 线性增长
# checkpoint: 只保存选定边界,反向时重算区间内部
# 内存/计算权衡取决于分段策略,不能统一写成必然 O(√L) 或 O(1)

# PyTorch 中 activation checkpointing 与 FSDP wrapping 是两个步骤;
# 可使用 torch.utils.checkpoint.checkpoint,或 distributed checkpoint_wrapper
# 按 Transformer block 应用,而不是传入不存在的 FSDP 构造参数。

Deep Dive · Ring、NCCL、带宽测量与多维并行

通信原语实现

Ring All-Reduce

Ring All-Reduce 的两个阶段:

Phase 1: Reduce-Scatter
每个 GPU 把数据分成 N 份
在 ring 中传递并累加
经过 N-1 步,每个 GPU 有 1/N 的完整结果

Phase 2: All-Gather
每个 GPU 把自己的 1/N 结果在 ring 中传递
经过 N-1 步,每个 GPU 有完整结果

总通信量: 2 × (N-1)/N × data_size ≈ 2 × data_size
固定 payload 时,每-rank 字节量是 O(1) 并趋近 2 × data_size;
但需要 2(N-1) 个 ring steps,因此 latency 项仍随 N 增长。

NCCL:NVIDIA 的通信库

# NCCL 会按 collective、消息大小、拓扑和协议选择/组合算法:
# - Tree 往往有更少的通信 steps,适合 latency 敏感的小消息
# - Ring 往往更容易逼近链路带宽,适合 bandwidth 敏感的大消息
# - 节点内可利用 NVLink/NVSwitch,跨节点再结合网络与分层算法
# - 节点间: 利用 InfiniBand/RoCE

# PyTorch 接口:
import torch.distributed as dist

# All-Reduce
dist.all_reduce(tensor, op=dist.ReduceOp.SUM)

# Reduce-Scatter
dist.reduce_scatter_tensor(output, input, op=dist.ReduceOp.SUM)

# All-Gather
dist.all_gather_into_tensor(output_tensor, input_tensor)

带宽测量

# 简化的 NCCL benchmark 示意;正式测量优先使用 nccl-tests 并多次迭代
tensor = torch.randn(100 * 1024**2, device='cuda')  # 400 MiB (float32)

for _ in range(5):  # warmup
    dist.all_reduce(tensor, op=dist.ReduceOp.SUM)
torch.cuda.synchronize()

start = time.perf_counter()
for _ in range(20):
    dist.all_reduce(tensor, op=dist.ReduceOp.SUM)
torch.cuda.synchronize()
duration = (time.perf_counter() - start) / 20

size_bytes = tensor.element_size() * tensor.numel()
algorithm_bandwidth = (
    2 * (world_size - 1) / world_size * size_bytes / duration
)

# 这是按 ring 字节模型换算的 algorithm bandwidth,不等于单条链路规格。
# 结果必须连同 GPU/NIC、拓扑、world_size、dtype 和 NCCL 版本一起报告。

从单轴到 2D / 3D / 多维并行

Spring 2026 官方 Lecture 8 不把 ZeRO/FSDP 当作终点,而是把它与模型不同轴的切分组合起来:

轴切分对象主要通信常见放置
FSDP / ZeRO参数、梯度、optimizer stateall-gather + reduce-scatter可跨节点扩展
Tensor Parallel单层矩阵的宽度维每层 activation all-reduce / reduce-scatter / all-gather优先节点内高速互联
Sequence ParallelLayerNorm/Dropout 等非 TP activation 的序列维与 TP 边界配合的 reduce-scatter / all-gather通常与 TP 同组
Pipeline Parallel网络深度/层stage 间 P2P activation 与 gradient可跨相对较慢网络
Expert ParallelMoE experts 与 tokentoken dispatch / combine 的 all-to-all优先低延迟、高带宽域

2D parallelism 常指 FSDP × TP 网格;更完整的 3D/4D 方案再加入 PP、EP/SP。总设备数可写成各独立并行轴 degree 的乘积,但前提是分组定义、batch/sequence 可整除,并且每一轴的通信资源没有互相争用。

通信重叠:字节量不等于墙钟代价

# 若两条通信轴使用独立资源并能与计算重叠:
step_time ≈ max(compute, fsdp_collective, tp_collective)

# 若共享同一 NIC / NVLink channel 且不能重叠:
step_time ≈ compute + fsdp_collective + tp_collective

实际框架会用 bucket、prefetch、异步 collective、microbatch 与 schedule 尝试隐藏通信;因此并行方案必须用 profiler 观察时间线,而不能只比较理论 communication volume。


Reference · 70B 配置与 scaling-efficiency 核算

实际训练配置

70B 模型训练配置

# 硬件: 16 节点 × 8 H100 = 128 GPU

# 并行策略:
TP = 8 (节点内)
PP = 4 (4 个 pipeline stages)
DP = 128 / (8 × 4) = 4

# 内存(每 GPU):
参数: 70B / (8 × 4) = 2.19B 参数
  bf16: 4.4 GB
  fp32 主参数: 8.8 GB
梯度: 8.8 GB
优化器: 17.6 GB
# activation、通信 buffer 和峰值参数 gather:
# 必须用实际 shape、FSDP unit 边界与 profiler 测量

# 吞吐量:
MFU ≈ 0.5
每 GPU: 989 × 0.5 = 495 TFLOP/s
128 GPU: 63.4 PFLOP/s
训练 15T tokens: 6 × 70B × 15T / 63.4P
              ≈ 9.94e7 秒 ≈ 1150 天

# 这个结果说明 128 张 H100 不足以在数天内完成如此大的 token 预算;
# 原先“99 小时”低估了约 280 倍。真实规划还要加入非训练 FLOPs 与停机。

Scaling Efficiency

理想: N 个 GPU → N× 加速
实际: 通信开销导致亚线性加速

弱 scaling 固定每 GPU 工作量,强 scaling 固定总工作量。
效率没有跨模型/集群通用的 80-95% 或 60-80% 区间,必须报告
GPU 数、网络、global batch、并行分解、序列长度和测量口径。

影响 scaling 的因素:
1. 模型大小(越大,计算占比越高,scaling 越好)
2. Batch size(越大,通信占比越低)
3. 网络带宽(NVLink vs InfiniBand)
4. 并行策略选择


知识检查

问题 1:理想均匀分片下,ZeRO Stage 3 的持久模型状态约为标准复制的多少?

问题 2:FSDP 前向传播需要什么通信?

问题 3:FSDP 的 HYBRID_SHARD 策略是指?

问题 4:固定 payload、忽略 latency 时,Ring All-Reduce 每 rank 的带宽通信量随 N 如何增长?

问题 5:Gradient checkpointing 的核心思想是什么?


Concept Check · stage 与 peak

ZeRO-3 在 4 ranks 上把参数常驻份额约降到 1/4,某 FSDP unit 计算时该 rank 是否永远只持有 1/4 unit 参数?

答案:否;计算前通常 All-Gather 完整 unit,因此峰值包含 materialized unit。

若答“永远 1/4”,你把 steady-state shard 账本当成了时间轴上的 peak 账本。

你现在应该能够解释

通往第 9 课

系统已经给出可承受的模型、token 与计算预算;下一课反过来问:固定 compute 时,参数量 \(N\) 与数据量 \(D\) 应如何分配才可能获得更低 loss。

如果只能记住 5 件事

  1. DDP 的容量问题来自模型状态复制;集群总显存很多,不代表单个 rank 能容纳完整副本。
  2. ZeRO Stage 1 / 2 / 3 依次分片 optimizer state、gradient 和 parameter,显存越低,按需通信越多。
  3. FSDP 在计算前 All-Gather 当前 unit 参数,反向后 Reduce-Scatter 梯度,再恢复只保存本地 shard。
  4. FSDP unit 粒度同时影响峰值 buffer、collective 次数和可重叠计算量,因此不是越细或越粗越好。
  5. Mixed precision、activation checkpointing、FSDP、TP 与 PP 解决不同资源约束,真实配置需要联合核算。

延伸阅读

Reference · ZeRO、FSDP 与 NCCL 一手资料