← 返回笔记列表
📝 学习笔记 · LLM 训练框架 · 微软 Research

DeepSpeed 完全图解:ZeRO / 3D 并行 / Offload 从零讲清楚

微软开源的大模型训练 + 推理加速库。本文从「训练显存到底花在哪」开始,把 ZeRO-1/2/3、ZeRO-Offload、ZeRO-Infinity、3D Parallelism、混合精度、Gradient Checkpointing 一次讲透,每个概念都有 SVG 示意图 + 具体数字。

出品
Microsoft Research
核心创新
ZeRO(Zero Redundancy Optimizer)
代表论文
ZeRO (SC'20) · ZeRO-Offload · ZeRO-Infinity (SC'21)
与谁集成
HuggingFace Trainer · LLaMA-Factory · Megatron-DeepSpeed
🤔
§0 DeepSpeed 是什么
一句话定位

DeepSpeed 是微软在 PyTorch 之上的训练加速插件,不替换 PyTorch,只是在它之上叠一层「显存极限切割 + 通信调度」。最核心的能力是 ZeRO——让 N 张 GPU 合力存一个大模型,而不是每张卡傻傻地各存一份完整拷贝。

你的代码 框架层 硬件层 你的训练脚本 (train.py / HuggingFace Trainer / LLaMA-Factory) DeepSpeed(ZeRO / Offload / 通信调度) PyTorch(自动微分 / 算子 / CUDA) GPU × N (通过 NCCL / NVLink / InfiniBand 互联)
图 0-1:DeepSpeed 在训练栈中的位置——它在 PyTorch 和你代码之间,负责显存切割与通信

💾 显存省到夸张

80GB 卡本来只能装 7B 模型,开 ZeRO-3 + Offload 可训 70B。原理:把优化器状态/梯度/参数都切碎分到多卡,用完即丢。

⚡ 训练速度更快

通信调度与计算重叠(overlap comm)、Fused Optimizer、混合精度已内置,无需手写 CUDA。

🚀 推理也支持

DeepSpeed-Inference 提供 Tensor Parallelism、INT8 量化、KV Cache 优化、融合 Kernel。

vs PyTorch 自带 DDP / FSDP:
  • DDP:每卡完整复制一份模型,只切 batch,模型大就直接 OOM
  • FSDP:PyTorch 官方 ZeRO-3 复刻版,2022 年出现,生态更原生但 Offload 到 CPU/NVMe 能力弱
  • DeepSpeed:ZeRO 发明者,三档灵活切换,Offload 到 CPU + NVMe 是独有优势
💾
§1 训练显存到底花在哪儿

要理解 ZeRO,先得把「训练一个模型,显存被谁吃了」算清楚。以 Adam 优化器 + 混合精度(FP16 前向 + FP32 优化器)训练为例:

1.1 16 字节/参数法则

每个参数的显存占用(Adam + FP16 混合精度) 总计 16 字节/参数 = 1B 参数 × 16 = 16 GB 参数 P FP16 · 2B 梯度 G FP16 · 2B FP32 参数副本 FP32 · 4B Adam 一阶动量 m FP32 · 4B Adam 二阶动量 v FP32 · 4B 优化器状态 O 12 字节 2 字节 2 字节 12 字节(FP32 副本+m+v) 总计:2 + 2 + 12 = 16 字节 / 参数 实例换算: 1B 参数 → 16 GB 7B 参数 → 112 GB 70B 参数 → 1120 GB(!)
图 1-1:Adam + FP16 混合精度训练的显存分解。优化器状态占大头(75%),是 ZeRO 重点切割对象。
类别内容精度每参数字节1B 模型7B 模型
参数 P模型权重FP1622 GB14 GB
梯度 G反向传播梯度FP1622 GB14 GB
优化器 OFP32 副本 + Adam m/vFP324+4+4=1212 GB84 GB
小计 P+G+O1616 GB112 GB
激活 A前向中间张量FP16视 batch/seq 而定~6 GB~30 GB

1.1.1 用一个极简神经网络理解 P / G / O / 激活

假设有一个只有 2 个参数的极简全连接网络(输入 x,中间一个权重 w₁,再一个权重 w₂,输出 ŷ),用 MSE loss 训练。这个例子虽然简单,但能把所有概念对应清楚。

x ×w₁ h x·w₁ ×w₂ ŷ h·w₂ Loss (ŷ-y)² 输入 激活值 h 激活值 ŷ 参数 w₁ 参数 w₂ 这个网络的所有参数: P = { w₁, w₂ } ← 就 2 个数 前向输出的中间值: 激活 = { h, ŷ } ← 中间结果 (每次前向都要临时存下来)
图 1-2:极简 2 参数网络。w₁ 和 w₂ 是参数(P),h 和 ŷ 是前向传播中的激活值(A)。

现在我们来逐一看,训练这个网络时 GPU 里存了什么:

P
参数(Parameters)= 模型的权重本身
就是 w₁w₂。网络有多少连接就有多少参数。
训练时用 FP16 存(2字节/个),推理时也需要,load 模型就是 load 这些数。
→ 真实的 LLaMA-7B 有 70 亿个这样的数,FP16 存需要 14 GB
A
激活值(Activations)= 前向传播时每一层的输出,临时存在显存里
前向计算时,先算 h = x × w₁,再算 ŷ = h × w₂
hŷ 这两个中间结果不能丢掉,因为反向传播要用它们来算梯度:
∂Loss/∂w₂ 需要 h;∂Loss/∂w₁ 需要 ŷ 和 w₂

→ 激活大小跟 batch size × seq len × 网络宽度 成正比,跟参数量无关。batch 越大、序列越长,激活越多。
G
梯度(Gradients)= 反向传播算出的"每个参数应该往哪个方向调"
链式法则从 Loss 反推:
∂Loss/∂w₂ = 2(ŷ−y) × h  ∂Loss/∂w₁ = 2(ŷ−y) × w₂ × x

每个参数对应一个梯度,所以梯度张量和参数张量形状完全相同,大小也一样(FP16,2字节/个)。
→ 梯度是训练独有的,推理完全不需要。LLaMA-7B 的梯度也是 14 GB
O
优化器状态(Optimizer States)= Adam 额外维护的三份"历史记录"
光有梯度还不够,Adam 不是直接用梯度更新权重,它还要额外存:
  • FP32 主权重副本:因为 FP16 精度不够做精确更新(见§6),Adam 存了一份 FP32 版本的 w₁、w₂ 用于计算。更新完再 cast 回 FP16。
  • 一阶动量 m:每个参数历史梯度的指数移动平均,m = β₁·m + (1−β₁)·g。记录"这个参数最近一直在往哪个方向走",让更新有惯性。
  • 二阶动量 v:每个参数历史梯度平方的移动平均,v = β₂·v + (1−β₂)·g²。用来自适应调整每个参数的学习率——历史梯度大的参数,步子迈小点。
这三份都是 FP32(4字节/个),每个参数对应三个数 → 12 字节/参数
→ LLaMA-7B:7B × 12 = 84 GB,是最大的一块,占总训练显存的 75%。
总结对应关系:
名称是什么何时存在大小由什么决定
P(参数)模型权重 w₁, w₂ …推理 + 训练都有模型层数 × 宽度
A(激活)每层前向的中间输出 h, ŷ …前向时产生,反向后释放batch × seq_len × 宽度
G(梯度)∂Loss/∂w,与 P 同形状仅训练,反向传播时= 参数量(同 P)
O(优化器状态)FP32主权重 + Adam m + Adam v仅训练,全程驻留= 3 × 参数量(FP32)

训练总显存 = P(2) + G(2) + O(12) + A(动态) = 16字节/参数 + 激活。推理只需 P,所以训练比推理重 8 倍。

1.2 激活(Activation)占用

激活是前向传播时产生的中间张量(每层的 attention 矩阵、FFN 输出 …),反向时要用来算梯度,所以默认会全量保留在显存。它的大小跟 batch size × seq len × 模型宽度成正比,跟参数量无关:

💡 举例:LLaMA-7B 激活估算
  • 32 层 Transformer,每层 hidden=4096,seq=2048,batch=4,FP16
  • 每层激活约:2048 × 4096 × 4 × 2字节 ≈ 67 MB/层
  • 32 层合计:~2 GB(Attention 中间矩阵还需额外 ~4 GB)
  • seq=8192 时线性扩大到 ~24 GB,单卡直接不够用 → 必须 Gradient Checkpointing
总结:P+G+O 一共 16 字节/参数,是固定开销,跟 batch / seq 无关。激活是动态开销,随 batch × seq 增大而增大。ZeRO 主要解决前者,Gradient Checkpointing 主要解决后者。这两个工具完全正交,可以同时开
§2 ZeRO:核心思想
ZeRO = Zero Redundancy Optimizer

「N 张 GPU 跑数据并行时,DDP 让每张卡都完整复制 P/G/O。ZeRO 把它切成 N 份分开存——需要哪段就临时 all-gather 凑回来,用完立刻扔掉。」

❌ DDP(传统数据并行):每张卡完整复制一份模型 4 卡 × 16 GB = 64 GB 总占用,冗余 3 份 GPU 参数 P(FP16, 2 GB) 梯度 G(FP16, 2 GB) 优化器状态 O(FP32, 12 GB) 合计 GPU 0 P(完整)2 GB G(完整)2 GB O(FP32 副本 + m + v)12 GB 16 GB GPU 1 P(完整)2 GB G(完整)2 GB O 12 GB 16 GB GPU 2 / 3 …… (同样各占 16 GB,完整复制,纯冗余) 16 GB 总显存占用:4 × 16 GB = 64 GB(所有冗余都是重复存储,毫无必要) ZeRO-3:把 P/G/O 分别切成 N=4 份,每卡只存自己那段 ✅ ZeRO-3(分片存储):每张卡只存 1/N,N=4 每卡理论占用 16/4 = 4 GB,总共 16 GB(无冗余) GPU 参数 P 分片 梯度 G 分片 优化器状态 O 分片 合计 GPU 0 P[0/4] 0.5 GB G[0/4] 0.5 GB O[0/4] 3 GB 4 GB GPU 1 P[1/4] 0.5 GB G[1/4] 0.5 GB O[1/4] 3 GB 4 GB GPU 2 / 3 P[2/4] / G[2/4] / O[2/4] 同理,各 4 GB/卡 4 GB 总显存占用:4 × 4 GB = 16 GB(= 1 份模型,节省 75%)
图 2-1:DDP vs ZeRO-3 存储对比(4 卡,1B 模型示例)。ZeRO-3 把 P/G/O 全部切成 N 份,每卡只持有 1/N,从 64 GB 降到 16 GB。

2.1 ZeRO-1:只切优化器状态 O

最保守的一档。只切「Adam 的 FP32 副本 + m + v」(共 12 字节/参数)。参数 P 和梯度 G 每张卡仍然完整持有。

ZeRO-1 训练一步的流程(4 卡) ① 前向传播 全量 P → loss ② 反向传播 全量 G 计算 ③ reduce-scatter G 每卡保留自己的 G 片段 ④ Adam step 只更新自己分片的 O ⑤ all-gather P 更新后 P 聚回每张卡 通信量对比(相对于 DDP): DDP:all-reduce G ≈ 2Φ × 2 字节 ZeRO-1:reduce-scatter G(Φ×2)+ all-gather P(Φ×2)= 完全相同 ✓
图 2-2:ZeRO-1 训练流程。通信量与 DDP 相同,但显存省 ~56%(优化器状态减为 1/N)。
💡 举例:1B 模型,4 卡 ZeRO-1 vs DDP
  • DDP 每卡:P(2) + G(2) + O(12) = 16 GB
  • ZeRO-1 每卡:P(2) + G(2) + O(12)/4 = 2+2+3 = 7 GB(省 56%)
  • 通信量:完全相同——reduce-scatter(G) + all-gather(P) 字节数 = DDP all-reduce(G)

2.2 ZeRO-2:再把梯度 G 也切了

在 ZeRO-1 基础上,梯度也分片。每张卡反向后立刻 reduce-scatter,只保留自己负责的那段梯度,不再持有全量 G。

💡 举例:1B 模型,4 卡 ZeRO-2
  • 每卡:P(2) + G(2)/4 + O(12)/4 = 2 + 0.5 + 3 = 5.5 GB
  • 相比 DDP(16 GB)节省 66%
  • 通信量:仍与 DDP 相同

2.3 ZeRO-3:连参数 P 都切(最猛)

P/G/O 全部分片,每张卡只持有 1/N 的模型权重。需要某层时 all-gather 凑回来,算完立刻释放。

ZeRO-3:前向时每层动态 all-gather 参数 Layer 1 P[0] on GPU0 all-gather 4 卡凑出 完整 L1 Layer 1(完整) 临时存在,算完即丢 前向 释放 Layer 1 恢复为分片状态 重复 Layer 2 再 all-gather … ··· 反向传播时: 逆序再 all-gather 一遍各层参数,反向算完对应层立刻释放。总通信量 ≈ DDP 的 1.5× 核心权衡: ✅ 显存降到 16Φ/N(最省) ❌ 通信量 ≈ 1.5× DDP,NVLink/IB 带宽是瓶颈
图 2-3:ZeRO-3 每层前向/反向时动态 all-gather 参数,用完立刻释放,显存最省但通信最多。
💡 举例:13B 模型在 8 卡 A100-40G 上能否跑起来
  • DDP:13B × 16 = 208 GB → 单卡装不下
  • ZeRO-2:13B × (2 + 2/8 + 12/8) ≈ 15.6 GB/卡 → 装得下!
  • ZeRO-3:13B × 16 / 8 = 26 GB/卡 → 装得下(注:ZeRO-2 理论上比 ZeRO-3 更适合这个场景,ZeRO-3 额外通信开销更大)
  • 70B 模型 + 8 卡 ZeRO-3:70B×16/8 = 140 GB/卡 → 仍然装不下 → 需要 Offload 或更多卡

2.4 通信量分析:ZeRO 为什么没变慢多少

通信字节数对比(每训练步,Φ = 参数量) DDP all-reduce(G) = 2Φ × 2B ZeRO-1 / ZeRO-2 reduce-scatter(G) + all-gather(P) = 2Φ × 2B (相同) ZeRO-3 all-gather(P)×2 + reduce-scatter(G) = 3Φ × 2B(是 DDP 的 1.5 倍) 结论:ZeRO-1/2 通信量与 DDP 完全相同,只有 ZeRO-3 有额外 50% 通信开销。
图 2-4:各 ZeRO 档位通信量对比。ZeRO-1/2 免费降显存,ZeRO-3 以 1.5× 通信换最大显存节省。

2.5 三档对比总表

档位切 O切 G切 P每卡显存(N 卡)额外通信适用场景
DDP16Φ 字节— baseline小模型 (<3B)
ZeRO-1(2+2)Φ + 12Φ/N= DDP(无额外)1~3B 模型
ZeRO-22Φ + 14Φ/N= DDP(无额外)3~13B 微调
ZeRO-316Φ/N×1.5 DDP13B+ 大模型
实操建议:从 ZeRO-2 开始尝试,OOM 才升 ZeRO-3。能不 Offload 就不 Offload(见 §3)。
📦
§3 ZeRO-Offload:把状态甩到 CPU 内存

ZeRO-3 还不够?还是 OOM?那就接着甩——把优化器状态、梯度,甚至参数本身,从 GPU HBM 搬到 CPU RAM。这就是 ZeRO-Offload(2021)。

GPU HBM 40 ~ 80 GB / 卡 带宽 ~2 TB/s P(前向时)+ 激活 all-gather Offload PCIe ~32GB/s CPU RAM 256 GB ~ 1 TB 带宽 ~100 GB/s O(FP32 副本+m+v)+ G prefetch Offload DRAM ~50GB/s NVMe SSD 10+ TB 带宽 ~7 GB/s ZeRO-Infinity:O + G + P ✓ Adam step 也在 CPU 上跑 CPU 多核执行 FP32 更新 瓶颈:PCIe 带宽只有 32 GB/s vs HBM 2 TB/s,每次 Offload 都会让训练吞吐量下降 2~5×
图 3-1:ZeRO-Offload 存储层次。GPU HBM 只放前向参数和激活;O/G 住 CPU RAM;Adam step 也在 CPU 执行。ZeRO-Infinity 再进一步把状态甩到 NVMe SSD。

✅ 能 Offload 什么

  • offload_optimizer:FP32 副本 + Adam m/v → CPU RAM(最常用)
  • offload_param:模型参数 P → CPU RAM(配合 ZeRO-3)
  • ZeRO-Infinity:P/G/O 全甩 NVMe

❌ 代价

  • PCIe 带宽只有 ~32 GB/s(GPU HBM 是 ~2 TB/s,差 60×)
  • 训练速度通常降 2~5×(取决于模型层大小和通信调度)
  • offload_param 时每层前向都要 CPU→GPU 搬参数,速度最慢
💡 举例:单张 RTX 4090 (24 GB) 训 LLaMA-7B
  • 不开 Offload:7B × 16 = 112 GB → 完全 OOM,想都别想
  • ZeRO-3 + offload_optimizer(不 offload 参数):P(14GB) + 激活(~4GB) ≈ 18 GB → 单卡能跑
  • ZeRO-3 + offload_optimizer + offload_param:进一步压到 ~8 GB → 可以跑更长 seq
  • 代价:吞吐量约为 8×A100 方案的 1/10,但能跑通 🎉
// ds_config.json 关键 Offload 字段
"zero_optimization": {
  "stage": 3,
  "offload_optimizer": {
    "device": "cpu",
    "pin_memory": true    // pin_memory=true 让 CPU 到 GPU 传输更快
  },
  "offload_param": {
    "device": "cpu",      // 只在 OOM 时才加这个!
    "pin_memory": true
  }
}
♾️
§4 ZeRO-Infinity:把状态再甩到 NVMe 硬盘

ZeRO-Infinity(SC'21)把同样的思路推到极致:P/G/O 全部可以 offload 到本地 NVMe SSD,同时设计了 bandwidth-centric partitioning(以带宽为中心的分片)来最大化 NVMe 读写吞吐。

理论极限:一台 8×A100 服务器(640 GB GPU HBM + 2 TB CPU RAM + 10 TB NVMe SSD),理论上可以训练 1 万亿参数(1T)级别的模型。
实战中:NVMe 带宽只有 ~7 GB/s(PCIe 4.0 × 4),每 step 需要大量 IO,训练速度极慢(分钟级 / step)。主要用在「我只需要跑通一次验证概念」或「超大参数量推理」的场景,生产环境大规模预训练还是首选多机 + ZeRO-3 不 Offload。
🧊
§5 3D 并行:DP × TP × PP
为什么 DeepSpeed 文章要讲 3D 并行?
ZeRO 本质上是对数据并行(DP)维度的优化——它解决的是"每张卡都完整复制一份模型状态"的冗余问题。但当模型规模继续增大,单层本身就装不下一张卡时,光靠 ZeRO 就不够了,必须引入 TP(张量并行)把矩阵切开、PP(流水线并行)把层分到不同机器上。DeepSpeed 通过与 Megatron-LM 联合,把这三个维度全部集成在一起——ZeRO 负责 DP 维度的显存,TP/PP 负责"单层/单机装不下"的问题,三者互补而非替代。

ZeRO 是「数据并行里的优化版」——多张卡处理不同 batch,但模型的每一层都在这 N 张卡里某张完整地算完。当模型大到单层都装不下时,光靠 ZeRO 还不够,需要三种并行同时上

5.1 数据并行(DP)— ZeRO 所在的维度

数据并行 DP:每卡处理不同 batch,定期同步梯度 GPU 0 完整模型 Batch [0,1,2,3] GPU 1 完整模型 Batch [4,5,6,7] GPU 2 完整模型 Batch [8,9,10,11] GPU 3 完整模型 Batch [12,13,14,15] all-reduce 梯度 G
图 5-1:数据并行 DP。每卡复制完整模型,处理不同 mini-batch,反向后 all-reduce 梯度同步。

5.2 张量并行(TP / Megatron Style)

同一层的权重矩阵切到多卡上分别计算,再通信合并结果。由 Megatron-LM 提出,节点内(NVLink)效率最高。

张量并行 TP:把一个 Linear(4096 → 4096) 层按列切到 4 张卡 输入 X Batch × 4096 广播到每张卡 (完全相同) 广播 GPU 0 权重 W[:, 0 : 1024] → 输出 Y[:, 0 : 1024] GPU 1 权重 W[:, 1024 : 2048] → Y[:, 1024 : 2048] GPU 2 权重 W[:, 2048 : 3072] → Y[:, 2048 : 3072] GPU 3 权重 W[:, 3072 : 4096] → Y[:, 3072 : 4096] all-reduce (拼接 4 段输出) 输出 Y Batch × 4096 完整合并结果 TP 特点 每卡存 1/4 参数 每卡算 1/4 算量 ✓ NVLink 高速 ✓ 节点内最优 ✗ 跨节点慢 (勿用) 适合同机 8 卡 (30B~100B) 注意:每张卡上的权重 W 宽度不同(1/4 列),但输入 X 完整广播到每卡;最终 all-reduce 把 4 段输出拼合成完整 Y。
图 5-2:张量并行 TP(4 卡)。Linear 层权重按列分到 4 卡,每卡各自计算 1/4 输出,最后 all-reduce 合并为完整输出 Y。

5.3 流水线并行(PP)

不同 Transformer 层放到不同 GPU 上,前向像工厂流水线一样推进。

流水线并行 PP:32 层模型 → 4 卡各放 8 层 GPU 0 Layer 1~8 Stage 0 激活 GPU 1 Layer 9~16 Stage 1 GPU 2 Layer 17~24 Stage 2 GPU 3 Layer 25~32 Stage 3 关键概念:Micro-batch(1F1B 调度) PP 最大问题是「流水线 Bubble」:当 Stage 0 算 batch N+1 时,Stage 3 还在等。 解决方案:把大 batch 拆成多个 micro-batch,1F1B 调度(先前向再反向,交替推进), 把 bubble 比例从 (N-1)/N 压低到 (pp_stages-1)/(pp_stages × micro_batches) ≈ 5~10%。
图 5-3:流水线并行 PP。32 层模型按层分到 4 卡;相邻 Stage 之间通过激活张量传递。

5.4 三维组合:DP × TP × PP

3D 并行示意(TP=2, PP=2, DP=2,总 8 卡) ◀── DP(数据并行)──▶:不同 batch,梯度 all-reduce ◀── TP(张量并行)──▶:同层矩阵切列,节点内 NVLink ◀── PP(流水线并行)──▶:不同层放不同卡 GPU 0 DP0,TP0,PP0 L1~8 / 左半W GPU 1 DP0,TP1,PP0 L1~8 / 右半W GPU 2 DP0,TP0,PP1 L9~16 / 左半W GPU 3 DP0,TP1,PP1 L9~16 / 右半W GPU 4 DP1,TP0,PP0 L1~8 / 左半W GPU 5 DP1,TP1,PP0 L1~8 / 右半W GPU 6 DP1,TP0,PP1 L9~16 / 左半W GPU 7 DP1,TP1,PP1 L9~16 / 右半W PP Stage 边界 DP 组边界 BLOOM-176B 实例:384 卡 A100,TP=4 + PP=12 + DP=8(4×12×8=384) TP 在节点内(NVLink 高速),PP 跨节点(IB),DP 最外层做 ZeRO-1
图 5-4:3D 并行三维分解(TP=2, PP=2, DP=2, 共 8 卡示例)。TP 在节点内靠 NVLink;PP 跨节点靠 InfiniBand;DP 最外层叠 ZeRO 降显存。
选型直觉:
  • 模型 <10B:纯 DP + ZeRO-2 就够
  • 10B~30B:DP + ZeRO-3(+可选 TP=2 节点内)
  • 30B~100B:TP=8(节点内)+ DP(节点间)
  • 100B+:TP + PP + DP 三者同时,DeepSpeed + Megatron 联合方案
💡 举例:BLOOM-176B 训练配置
  • 总 GPU:384 张 A100-80G(48 节点 × 8 卡)
  • TP=4:节点内 8 卡中 4 卡用于矩阵切分(NVLink 高速通信)
  • PP=12:48 节点 / 4 = 12 流水线 stage,每个 stage 处理 ~7 层
  • DP=8:剩余的维度做数据并行 + ZeRO-1
  • 4 × 12 × 8 = 384 ✓,总 batch = 2048 × micro-batch
🎯
§6 混合精度训练(FP16 / BF16)
核心思想:用半精度算,用全精度存

训练时前向/反向传播用 16 位浮点(速度快、显存省),但优化器更新参数时用 32 位浮点(精度高、不丢信息)。两种精度各取所长,合称「混合精度」。

6.1 浮点数格式基础:指数 vs 尾数

要理解 FP16 和 BF16 的区别,必须先搞清楚浮点数的两个核心字段:

📐 指数(Exponent)—— 决定「范围」

控制数字能表示多大或多小。指数位越多,能覆盖的数值范围越宽。
类比:科学计数法里的 10N,指数位就是 N 能取多大的范围。

范围不足 → 溢出(Overflow / Underflow),数字变成 ±inf 或 0,训练直接崩。

🔬 尾数(Mantissa)—— 决定「精度」

控制数字能精确到多少位小数。尾数位越多,能区分的相邻数值越细。
类比:科学计数法里 1.xxxxx 中小数点后的位数。

精度不足 → 舍入误差,计算结果有偏差,但只要不是系统性的就通常可以接受。

6.2 三种格式的位布局对比

FP32 / FP16 / BF16 位布局对比(总位数不同,但 BF16 和 FP16 均为 16 bit) 符号 S (1位) 指数 E 尾数 M FP32 (32 位) S 指数 8 位 → 范围 ~±3.4×10³⁸ 尾数 23 位 → 十进制约 7 位有效数字 FP16 (16 位) S 指数 5 位 范围 ≈ ±65504 ⚠️ 尾数 10 位 十进制约 3 位有效数字 ⚠️ 指数范围比 FP32 小 10^30 倍! 梯度稍大就溢出,需 Loss Scaling BF16 (16 位) S 指数 8 位(= FP32!) 范围 ~±3.4×10³⁸ ✅ 尾数 7 位 精度略低于FP16 ✅ 范围与 FP32 相同,不溢出 无需 Loss Scaling,训练更稳定 BF16 = 把 FP32 的前 16 位截断(保留指数 8 位,尾数只剩 7 位) FP16 = 重新设计,指数缩到 5 位换来更多尾数位(10 位)
图 6-1:FP32 / FP16 / BF16 位布局对比。BF16 与 FP32 指数位数相同,只是尾数更短;FP16 则指数很小但尾数更多。

6.3 FP16 vs BF16:核心差距在哪里?

一句话核心:FP16 的指数只有 5 位,能表示的最大数约是 65504,而神经网络训练时梯度或激活很容易超过这个范围,导致溢出(NaN/Inf)。BF16 保留了和 FP32 相同的 8 位指数,范围完全一致,不会溢出。

具体问题一:FP16 的溢出 / 下溢

💡 为什么 FP16 容易溢出?
  • FP16 能表示的最大正数 ≈ 65504,最小正数 ≈ 6×10⁻⁸
  • FP32 能表示的最大正数 ≈ 3.4×10³⁸,小好几十个数量级
  • 训练大模型时,某些层的激活值可能 >65504(Overflow),变成 +inf
  • 梯度经过多层反向后可能非常小(<6×10⁻⁸),变成 0(Underflow),权重停止更新
  • 这两种情况都会让训练直接崩掉,表现为 loss 变成 NaN

具体问题二:BF16 的精度代价

💡 BF16 精度低会有影响吗?
  • BF16 尾数只有 7 位,十进制约 2~3 位有效数字;FP16 尾数 10 位,约 3~4 位
  • FP32 尾数 23 位,约 7 位有效数字
  • 在大模型训练中,单步计算的舍入误差影响很小,经过大量 step 后统计上会被平均掉
  • 实践证明:用 BF16 训练的收敛曲线与 FP32 几乎一致(GPT-3、LLaMA 均如此)
  • 而 FP16 溢出导致的 NaN 则是灾难性的,一旦出现训练就废了
格式总位数指数位尾数位数值范围十进制精度主要问题适用 GPU
FP3232823~±3.4×10³⁸~7 位—(基准)所有 GPU
FP1616510~±65504~3-4 位⚠️ 容易溢出,需 Loss ScalingV100、RTX 3090/4090
BF161687~±3.4×10³⁸~2-3 位精度略低(可接受)A100、H100、TPU

6.4 为什么需要「混合」精度?直接全用 FP16 不行吗?

不行,原因是 Adam 优化器的更新步长极小

典型 LR = 1e-4,权重更新量 = LR × 梯度 ≈ 1e-5 ~ 1e-7
FP16 在数值 ~1.0 附近,相邻两个可表示的数之间间隔约 1e-3(精度为 10⁻³ 量级)。
1e-7 的更新量对 FP16 来说完全无法区分,直接被舍入为 0,权重永远不更新,等于训练失效。

所以必须在优化器里保留一份 FP32 主权重来做精确更新,这就是「混合」的含义:

混合精度训练一步的流程 FP32 主权重 存在优化器里 精确更新用 cast↓ FP16/BF16 半精度权重 FP16 或 BF16 前向/反向用 前向传播 半精度算 Tensor Core ⚡ 反向传播 半精度梯度 FP16需×scale cast↑ FP32 FP32 Adam step 精确更新主权重 m/v 也是 FP32 更新后 FP32 主权重 → 下一步再 cast 为半精度 Loss Scaling(仅 FP16 需要,BF16 不需要): 把 loss × 大倍数(如 65536)再反向,避免梯度下溢为 0;Adam step 前再除回来。
图 6-2:混合精度训练流程。半精度负责速度,FP32 主权重负责精确更新,两者各司其职。

6.5 Loss Scaling:专门解决 FP16 梯度下溢的工程手段

问题:FP16 能表示的最小正数约 6×10⁻⁸,而反向传播中深层梯度经常 <10⁻⁸,直接变成 0(下溢),这层的权重就永远不更新了。

解法:在计算 loss 后、反向传播前,把 loss 乘以一个大倍数 S(如 65536),这样梯度也等比放大 S 倍,不会下溢。Adam step 之前再除以 S 还原回正确值。

动态 Loss Scaling:DeepSpeed 会自动调整 S。如果本 step 梯度出现 inf/NaN,S 减半并跳过本 step;连续若干 step 没问题,S 再翻倍。完全自动,不用手动设置。
💡 BF16 为什么不需要 Loss Scaling?

BF16 指数位与 FP32 相同,能覆盖同样的数值范围(~±3.4×10³⁸)。无论梯度多小,都不会下溢到 0。BF16 的问题是精度低(尾数只有 7 位),但这种精度损失是均匀的、统计中性的,不会导致系统性的梯度消失,大模型实践中完全可以接受。

6.6 实际选型建议

场景推荐精度原因
A100 / H100 训练BF16硬件原生支持,范围=FP32,无需 Loss Scaling,稳定
V100 / RTX 3090 / 4090FP16这些 GPU 不支持 BF16,只能用 FP16 + 动态 Loss Scaling
推理(Inference)FP16 或 INT8/INT4推理不用更新权重,不怕精度损失,越省越快
调试 / 精度敏感任务FP32确保数值正确性,速度慢但精度有保证
// ds_config.json 精度配置(A100/H100 用 BF16)
"bf16": { "enabled": true  },
"fp16": { "enabled": false }

// 旧 GPU(V100/3090)用 FP16 + 动态 Loss Scaling
"fp16": {
  "enabled": true,
  "loss_scale": 0,              // 0 = 动态自适应
  "initial_scale_power": 16,   // 初始 S = 2^16 = 65536
  "loss_scale_window": 1000    // 连续 1000 step 无 overflow 则 S 翻倍
}
总结一句话:FP16 和 BF16 都是 16 位,但设计取舍相反。FP16 更精确但容易溢出(需要 Loss Scaling 兜底);BF16 范围更宽几乎不溢出但精度略低(实践中可忽略)。有 A100/H100 就用 BF16,没有就用 FP16,大模型训练标准已逐渐迁移到 BF16。
🔁
§7 Gradient Checkpointing:用算力换显存

ZeRO 切的是 P/G/O,但激活(activations)没碰。激活是前向中所有中间张量,反向时要用,默认全量保留。Gradient Checkpointing 的思路很暴力:

有 vs 无 Gradient Checkpointing(6 层 Transformer,ckpt 每 2 层) ❌ 不开 GC(标准训练):所有激活保留到反向 L1 激活 ✓ L2 激活 ✓ L3 激活 ✓ L4 激活 ✓ L5 激活 ✓ L6 激活 ✓ 显存:6 份激活 全部同时占用 ✅ 开 GC(每 2 层设一个 checkpoint):只存边界,反向时重算 L1 ckpt ✓ 保留 L2 丢掉 反向重算 L3 ckpt ✓ 保留 L4 丢掉 反向重算 L5 ckpt ✓ 保留 L6 丢掉 反向重算 显存:3 份激活 省 50%,多算 33% 权衡:激活显存减少 √(层数) 倍,代价是反向需要重算一遍前向(约 +33% 计算量) 长序列(>4K)必开;短序列可不开;与 ZeRO 完全正交可同时使用
图 7-1:Gradient Checkpointing 原理对比。只在每 N 层边界保存激活(checkpoint),丢掉中间层;反向时从 checkpoint 重新前向计算。
# HuggingFace Trainer / LLaMA-Factory 一行开启
model.gradient_checkpointing_enable()
# 或在 TrainingArguments 里
gradient_checkpointing=True
💡 举例:LLaMA-7B 不同 seq_len 下的激活显存对比
seq_lenbatch=4不开 GC开 GC速度代价
20484~8 GB~2 GB-25% throughput
40964~24 GB~6 GB-30% throughput
81924~80 GB(OOM)~18 GB-33% throughput
📋
§8 实战:ZeRO-3 + Offload 配置文件详解

下面是一份通用 ZeRO-3 + offload optimizer 配置,适用于 LLaMA-7B/13B 等大模型,配合 HuggingFace Trainer / LLaMA-Factory 直接使用。

{
  // ── Batch 大小(写 "auto" 让 Trainer 自动填入)──
  "train_batch_size": "auto",
  "gradient_accumulation_steps": "auto",
  "gradient_clipping": 1.0,         // 梯度裁剪,防止 NaN

  // ── 精度(A100/H100 用 BF16,V100 用 FP16)──
  "bf16": { "enabled": true  },
  "fp16": { "enabled": false },

  // ── ZeRO 核心配置 ──
  "zero_optimization": {
    "stage": 3,                     // ZeRO 档位:1/2/3
    "overlap_comm": true,           // 通信与计算重叠,提速 10~20%
    "contiguous_gradients": true,   // 梯度内存连续,减碎片
    "reduce_bucket_size": 5e8,      // 每桶梯度 ~500MB,太大通信延迟增大

    // ZeRO-3 参数分片细节
    "stage3_prefetch_bucket_size": 5e8,      // 提前 gather 下一层参数(流水线)
    "stage3_param_persistence_threshold": 1e6, // 小于 1M 参数的层不分片(节省通信)
    "stage3_max_live_parameters": 1e9,
    "stage3_max_reuse_distance": 1e9,
    "stage3_gather_16bit_weights_on_model_save": true, // ⚠️ 必须开!否则 ckpt 只存 1/N

    // ── CPU Offload ──
    "offload_optimizer": {
      "device": "cpu",          // 优化器状态(FP32副本+m+v)移到 CPU
      "pin_memory": true        // pin_memory 提升 PCIe 传输速度
    }
    // "offload_param": { "device": "cpu", "pin_memory": true }
    // ↑ 仅在极端 OOM 时才加,会大幅降低训练速度
  },

  // ── 优化器("auto" = 让 Trainer 接管)──
  "optimizer": {
    "type": "AdamW",
    "params": { "lr": "auto", "betas": "auto", "eps": "auto", "weight_decay": "auto" }
  },

  // ── 学习率调度 ──
  "scheduler": {
    "type": "WarmupDecayLR",
    "params": {
      "total_num_steps": "auto", "warmup_num_steps": "auto",
      "warmup_min_lr": "auto",   "warmup_max_lr": "auto"
    }
  }
}

启动命令

# 通过 HuggingFace Trainer
deepspeed train.py \
  --deepspeed ds_config.json \
  --model_name_or_path meta-llama/Llama-3-8B \
  --per_device_train_batch_size 2 \
  --learning_rate 2e-5 ...

# 通过 LLaMA-Factory(直接用预设模板)
llamafactory-cli train \
  examples/train_full/llama3_full_sft_ds3.yaml

# torchrun 多节点
torchrun --nproc_per_node=8 --nnodes=4 \
  --master_addr=... --master_port=29500 \
  train.py --deepspeed ds_config.json ...
§9 选型速查 & 常见坑

场景 × 配置速查表

DeepSpeed 配置决策树 开始训练 Q1: 模型参数量是多少? <3B ZeRO-2 BF16,不 Offload 3B~30B Q2: 卡数 × 卡内存够用? 够用 ZeRO-3 BF16,不 Offload OOM ZeRO-3 + offload_optimizer CPU Offload,速度下降 2~3× >30B 3D 并行 TP + PP + ZeRO,Megatron-DS 还 OOM? + offload_param 或 ZeRO-Infinity (NVMe)
图 9-1:DeepSpeed 配置选型决策树。从模型大小开始,逐层往下找最合适的配置。
场景模型显卡推荐配置备注
消费级单卡 LoRA 微调7BRTX 4090 (24G)ZeRO-2,BF16,不 OffloadLoRA 减少可训参数,显存友好
消费级单卡全参微调7BRTX 4090 (24G)ZeRO-3 + offload_optimizer速度慢 3×,但能跑通
单机 8 卡微调13B/30B8×A100-80GZeRO-3,BF16,不 Offload8 卡 ZeRO-3:13B 只占 26GB/卡
单机 8 卡极限70B Full SFT8×A100-80GZeRO-3 + offload_optimizer + GC70B×16/8=140GB,必须 Offload
多机预训练100B+32+ 卡TP=8 + PP=N + DP×ZeRO-1Megatron-DeepSpeed 联合方案
RLHF / GRPO7B Actor+Ref+Reward8×A100ZeRO-3 + Hybrid EngineDS-Chat 专门优化 RLHF 场景

7 个常见坑

  • ⚠️ 坑 1:Save 只存了 1/N 参数
    ZeRO-3 下必须设 "stage3_gather_16bit_weights_on_model_save": true,否则 checkpoint 残缺不全,load 会 shape mismatch。
  • ⚠️ 坑 2:offload_param 让训练慢 10×
    CPU RAM offload 参数会让每层前向都要 PCIe 搬运,极慢。先只开 offload_optimizer,再视情况开 offload_param。
  • ⚠️ 坑 3:BF16 出 NaN
    检查 gradient_clipping(建议 1.0),以及 LR 是否太大(比如 lr=1e-3 对 7B 全参太高)。
  • ⚠️ 坑 4:多机速度惨不忍睹
    检查 NCCL 是否走 InfiniBand(export NCCL_IB_DISABLE=0),走 TCP Ethernet 会让 ZeRO-3 通信成为极限瓶颈。
  • ⚠️ 坑 5:Resume 换 GPU 数失败
    DS checkpoint 按 GPU 切片存储,恢复时 world_size 必须与保存时一致。换卡数须先用 zero_to_fp32.py 合并权重。
  • ⚠️ 坑 6:"auto" 字段没生效
    "auto" 只有配合 HuggingFace Trainer 才会自动注入。裸用 DeepSpeed Engine (deepspeed.initialize) 时要写实际数值。
  • ⚠️ 坑 7:ZeRO-3 + TP 同时用
    ZeRO-3 和 Tensor Parallel 不能直接叠加(ZeRO-3 切 world,TP 也切 world,会冲突)。正确做法是用 Megatron-DeepSpeed 的 ZeRO-1 + TP 组合。

一句话总结

DeepSpeed 的本质是「把训练显存切到极致」。从 ZeRO-1/2/3 三档逐级激进,再到 Offload 把状态甩到 CPU/NVMe,最后到 3D 并行把整个集群的显存打通——这一整条路径,都是同一个动机:让你能在「同样的硬件预算」下训出更大的模型。理解了「训练显存被谁吃了」(16 字节/参数法则),再回头看这些配置,就只是「在哪一刀切下去」的工程选择。

💬
§10 Q&A

Q1:ZeRO 跟模型并行有啥区别?也是一个模型拆成几份放不同机器吗?

虽然都是「拆开放不同机器」,但拆的东西不一样,更关键的是计算方式不一样

模型并行(Model Parallelism)

Layer 1,2,3 → GPU 0
Layer 4,5,6 → GPU 1
Layer 7,8,9 → GPU 2
  • 拆的是模型结构本身(层)
  • 每个 GPU 只负责前向/反向传播的一部分
  • 数据需要在 GPU 之间串行流动(GPU0 算完传给 GPU1)
  • 典型问题:流水线气泡,某些 GPU 在等待时是空闲的

ZeRO(Zero Redundancy Optimizer)

全部 GPU 都跑完整的前向+反向
但 optimizer states / gradients / params
分片存储
  • 拆的是训练状态(参数、梯度、优化器状态),不是层
  • 每个 GPU 仍然参与完整的计算,只是存的东西更少
  • 本质是数据并行的升级版,通过通信换内存
维度模型并行ZeRO
拆什么模型层 / 权重训练状态(参数/梯度/优化器)
计算方式各 GPU 算不同层各 GPU 算完整前向反向
通信模式层间激活值传递AllGather / ReduceScatter
适用场景单层太大放不下整体状态太大放不下
一句话区分:ZeRO 拆的是「存储」,所有 GPU 仍参与完整计算;模型并行拆的是「计算图」,每个 GPU 只算一部分层。前者是数据并行的优化,后者是模型结构上的切割。

Q2:为什么 ZeRO 要分 Stage 1/2/3,而不是直接全部拆了(Stage 3)更好呢?

先看三个 Stage 各自切了什么:

Stage切了什么额外通信显存节省(相对 DDP)
Stage 1Optimizer States(最大头,占 ~12 bytes/param)= DDP(无额外)~56%
Stage 2Optimizer States + Gradients= DDP(无额外)~66%
Stage 3Optimizer States + Gradients + Parameters(权重本身)×1.5 DDP75%(最大)

Stage 3 的代价不可忽视:

  • ① 通信量大幅增加
    每次前向传播,某个 GPU 需要某个参数时,必须先从其他 GPU AllGather 过来,用完再丢掉。反向传播也一样。Stage 1/2 只在 optimizer step 时通信;Stage 3 是每一层的前向和反向都要通信,通信次数成倍增加(约 1.5× DDP)。
  • ② 实现更复杂,调试困难
    参数被分散在不同 GPU,框架需要在运行时动态拼装,对自定义层、非标准模型不友好。
  • ③ 小模型反而更慢
    如果模型能放得下,Stage 3 的通信开销会让速度比 Stage 0(普通数据并行)还慢。
为什么分阶段——性价比递进

三个 Stage 的内存节省是递进的,但通信开销也是递进的:

  • 内存压力小 → Stage 1(只解决优化器状态冗余,通信与 DDP 相同,速度无损失)
  • 内存压力中 → Stage 2(再加上梯度,适中的权衡,通信仍与 DDP 相同)
  • 内存实在不够 → Stage 3(拼了,用 1.5× 通信换最大显存节省)

实际用法:能用 Stage 1/2 跑的就别用 Stage 3,因为 Stage 3 的吞吐量可能比 Stage 1/2 低 20~40%。只有在模型实在放不下(比如几百 B 参数的大模型)时,才迫不得已用 Stage 3。

Q3:梯度 G 为什么要全部存下来,不能算完一层立刻覆盖掉吗?

表面上看"算完一层梯度就用掉然后覆盖"是合理的,但这里有一个关键时序问题。

多卡训练时,梯度在被用之前必须先做 AllReduce 求平均。
各卡处理不同 batch,各自算出的梯度是不一样的。必须先把所有卡的梯度汇总求平均,才能得到"全局梯度"用于更新参数。

你说的"边算边发、覆盖内存"是 DDP 的 bucket AllReduce 优化,确实是可行的——反向传播期间,每算完一层就立刻把这层梯度发出去做 AllReduce,同时继续算前面层的梯度。

但 AllReduce 的结果(平均后的梯度)必须写回显存,一直驻留到 Optimizer Step 用完为止:

1
反向传播阶段:算出各层梯度,立刻 AllReduce → 得到全局平均梯度,写回梯度 buffer
2
Optimizer Step 阶段:遍历所有参数,逐个读取梯度 buffer 里的值,执行 Adam 更新:m = β₁·m + (1−β₁)·gw = w - lr·m/√v
3
Optimizer Step 完成后:这一层的梯度才真正没用了,可以释放

关键在于 Step 1 和 Step 2 是串行的两个阶段,Step 1 全部完成后 Step 2 才开始。所以在 Step 2 进行期间,所有层的"平均后梯度"必须同时在显存里,等 optimizer 按参数顺序逐一消费完才能释放。

一句话总结:可以边反向边通信,但 AllReduce 返回的结果必须存着,等 Optimizer Step 全部用完才能清空。这段时间里所有层的梯度都在显存里,这就是 G 的显存占用。ZeRO-2 就是通过把这些梯度分片到各卡来节省这部分开销。

Q4:混合精度里 FP32 副本是干什么的?既然 FP16 精度不够,为什么不直接全用 FP32,还要多存一份 FP16 不是更浪费?

先澄清一个常见误解:FP32 副本不是用来做前向/反向计算的,它只在 Optimizer Step 的最后一步用。

FP32 主权重 只做 Adam 更新 cast↓FP16 FP16 权重 前向 + 反向用 前向传播 FP16 矩阵乘 反向传播 FP16 矩阵乘 cast↑FP32 Adam Step FP32 更新主权重
FP16 负责前向+反向(速度),FP32 主权重只在最后 Adam 更新时用(精度)。

FP32 副本解决的问题:更新步长太小被 FP16 舍入成 0。

💡 为什么更新量会被舍入成 0?

Adam 更新权重时,更新量 = lr × m/√v,典型值约 1e-5 ~ 1e-7

FP16 在数值 ~1.0 附近,相邻两个可表示的数之间间隔约 1e-3(精度粒度)。

w_new = w_old + 1e-6 —— 这个 1e-6 比 FP16 的精度粒度 1e-3 小 1000 倍,直接被舍入成 0,权重永远不变,训练失效。

那为什么不全用 FP32,反而还要多存一份 FP16?

核心原因:Tensor Core 只能加速 FP16/BF16 矩阵乘法。

A100 GPU 上:
  • FP16 矩阵乘吞吐:312 TFLOPS
  • FP32 矩阵乘吞吐:19.5 TFLOPS(慢 16×)
训练 95% 的时间都在前向+反向的矩阵乘法上。全用 FP32 就完全用不上 Tensor Core,相当于把 GPU 的主要算力浪费掉。

所以那份额外的 FP16 参数副本(14 GB)是「购买 Tensor Core 加速」的票价——换来的是前向+反向 16× 的速度提升。拿 14 GB 换这个,非常划算。
方案参数存储前向+反向速度更新精度实际可行性
全 FP3228 GB(7B 模型)慢 16×,用不上 Tensor Core✅ 足够显存翻倍、速度极慢
混合精度(FP16 + FP32 副本)14 GB FP16 + 28 GB FP32 = 42 GB✅ 快 16×✅ 足够(FP32 更新)业界标准做法
全 FP1614 GB✅ 快 16×❌ 更新量被舍入 0训练会失效