DeepSpeed 是微软在 PyTorch 之上的训练加速插件,不替换 PyTorch,只是在它之上叠一层「显存极限切割 + 通信调度」。最核心的能力是 ZeRO——让 N 张 GPU 合力存一个大模型,而不是每张卡傻傻地各存一份完整拷贝。
💾 显存省到夸张
80GB 卡本来只能装 7B 模型,开 ZeRO-3 + Offload 可训 70B。原理:把优化器状态/梯度/参数都切碎分到多卡,用完即丢。
⚡ 训练速度更快
通信调度与计算重叠(overlap comm)、Fused Optimizer、混合精度已内置,无需手写 CUDA。
🚀 推理也支持
DeepSpeed-Inference 提供 Tensor Parallelism、INT8 量化、KV Cache 优化、融合 Kernel。
- DDP:每卡完整复制一份模型,只切 batch,模型大就直接 OOM
- FSDP:PyTorch 官方 ZeRO-3 复刻版,2022 年出现,生态更原生但 Offload 到 CPU/NVMe 能力弱
- DeepSpeed:ZeRO 发明者,三档灵活切换,Offload 到 CPU + NVMe 是独有优势
要理解 ZeRO,先得把「训练一个模型,显存被谁吃了」算清楚。以 Adam 优化器 + 混合精度(FP16 前向 + FP32 优化器)训练为例:
1.1 16 字节/参数法则
| 类别 | 内容 | 精度 | 每参数字节 | 1B 模型 | 7B 模型 |
|---|---|---|---|---|---|
| 参数 P | 模型权重 | FP16 | 2 | 2 GB | 14 GB |
| 梯度 G | 反向传播梯度 | FP16 | 2 | 2 GB | 14 GB |
| 优化器 O | FP32 副本 + Adam m/v | FP32 | 4+4+4=12 | 12 GB | 84 GB |
| 小计 P+G+O | 16 | 16 GB | 112 GB | ||
| 激活 A | 前向中间张量 | FP16 | 视 batch/seq 而定 | ~6 GB | ~30 GB |
1.1.1 用一个极简神经网络理解 P / G / O / 激活
假设有一个只有 2 个参数的极简全连接网络(输入 x,中间一个权重 w₁,再一个权重 w₂,输出 ŷ),用 MSE loss 训练。这个例子虽然简单,但能把所有概念对应清楚。
现在我们来逐一看,训练这个网络时 GPU 里存了什么:
就是
w₁ 和 w₂。网络有多少连接就有多少参数。训练时用 FP16 存(2字节/个),推理时也需要,load 模型就是 load 这些数。
→ 真实的 LLaMA-7B 有 70 亿个这样的数,FP16 存需要 14 GB。
前向计算时,先算
h = x × w₁,再算 ŷ = h × w₂。h 和 ŷ 这两个中间结果不能丢掉,因为反向传播要用它们来算梯度:∂Loss/∂w₂ 需要 h;∂Loss/∂w₁ 需要 ŷ 和 w₂
→ 激活大小跟 batch size × seq len × 网络宽度 成正比,跟参数量无关。batch 越大、序列越长,激活越多。
链式法则从 Loss 反推:
∂Loss/∂w₂ = 2(ŷ−y) × h ∂Loss/∂w₁ = 2(ŷ−y) × w₂ × x
每个参数对应一个梯度,所以梯度张量和参数张量形状完全相同,大小也一样(FP16,2字节/个)。
→ 梯度是训练独有的,推理完全不需要。LLaMA-7B 的梯度也是 14 GB。
光有梯度还不够,Adam 不是直接用梯度更新权重,它还要额外存:
- FP32 主权重副本:因为 FP16 精度不够做精确更新(见§6),Adam 存了一份 FP32 版本的 w₁、w₂ 用于计算。更新完再 cast 回 FP16。
- 一阶动量 m:每个参数历史梯度的指数移动平均,
m = β₁·m + (1−β₁)·g。记录"这个参数最近一直在往哪个方向走",让更新有惯性。 - 二阶动量 v:每个参数历史梯度平方的移动平均,
v = β₂·v + (1−β₂)·g²。用来自适应调整每个参数的学习率——历史梯度大的参数,步子迈小点。
→ 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 × 模型宽度成正比,跟参数量无关:
- 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
「N 张 GPU 跑数据并行时,DDP 让每张卡都完整复制 P/G/O。ZeRO 把它切成 N 份分开存——需要哪段就临时 all-gather 凑回来,用完立刻扔掉。」
2.1 ZeRO-1:只切优化器状态 O
最保守的一档。只切「Adam 的 FP32 副本 + m + v」(共 12 字节/参数)。参数 P 和梯度 G 每张卡仍然完整持有。
- 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。
- 每卡: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 凑回来,算完立刻释放。
- 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 为什么没变慢多少
2.5 三档对比总表
| 档位 | 切 O | 切 G | 切 P | 每卡显存(N 卡) | 额外通信 | 适用场景 |
|---|---|---|---|---|---|---|
| DDP | ✗ | ✗ | ✗ | 16Φ 字节 | — baseline | 小模型 (<3B) |
| ZeRO-1 | ✅ | ✗ | ✗ | (2+2)Φ + 12Φ/N | = DDP(无额外) | 1~3B 模型 |
| ZeRO-2 | ✅ | ✅ | ✗ | 2Φ + 14Φ/N | = DDP(无额外) | 3~13B 微调 |
| ZeRO-3 | ✅ | ✅ | ✅ | 16Φ/N | ×1.5 DDP | 13B+ 大模型 |
ZeRO-3 还不够?还是 OOM?那就接着甩——把优化器状态、梯度,甚至参数本身,从 GPU HBM 搬到 CPU RAM。这就是 ZeRO-Offload(2021)。
✅ 能 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 搬参数,速度最慢
- 不开 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
}
}
ZeRO-Infinity(SC'21)把同样的思路推到极致:P/G/O 全部可以 offload 到本地 NVMe SSD,同时设计了 bandwidth-centric partitioning(以带宽为中心的分片)来最大化 NVMe 读写吞吐。
ZeRO 本质上是对数据并行(DP)维度的优化——它解决的是"每张卡都完整复制一份模型状态"的冗余问题。但当模型规模继续增大,单层本身就装不下一张卡时,光靠 ZeRO 就不够了,必须引入 TP(张量并行)把矩阵切开、PP(流水线并行)把层分到不同机器上。DeepSpeed 通过与 Megatron-LM 联合,把这三个维度全部集成在一起——ZeRO 负责 DP 维度的显存,TP/PP 负责"单层/单机装不下"的问题,三者互补而非替代。
ZeRO 是「数据并行里的优化版」——多张卡处理不同 batch,但模型的每一层都在这 N 张卡里某张完整地算完。当模型大到单层都装不下时,光靠 ZeRO 还不够,需要三种并行同时上。
5.1 数据并行(DP)— ZeRO 所在的维度
5.2 张量并行(TP / Megatron Style)
把同一层的权重矩阵切到多卡上分别计算,再通信合并结果。由 Megatron-LM 提出,节点内(NVLink)效率最高。
5.3 流水线并行(PP)
把不同 Transformer 层放到不同 GPU 上,前向像工厂流水线一样推进。
5.4 三维组合:DP × TP × PP
- 模型 <10B:纯 DP + ZeRO-2 就够
- 10B~30B:DP + ZeRO-3(+可选 TP=2 节点内)
- 30B~100B:TP=8(节点内)+ DP(节点间)
- 100B+:TP + PP + DP 三者同时,DeepSpeed + Megatron 联合方案
- 总 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
训练时前向/反向传播用 16 位浮点(速度快、显存省),但优化器更新参数时用 32 位浮点(精度高、不丢信息)。两种精度各取所长,合称「混合精度」。
6.1 浮点数格式基础:指数 vs 尾数
要理解 FP16 和 BF16 的区别,必须先搞清楚浮点数的两个核心字段:
📐 指数(Exponent)—— 决定「范围」
控制数字能表示多大或多小。指数位越多,能覆盖的数值范围越宽。
类比:科学计数法里的 10N,指数位就是 N 能取多大的范围。
范围不足 → 溢出(Overflow / Underflow),数字变成 ±inf 或 0,训练直接崩。
🔬 尾数(Mantissa)—— 决定「精度」
控制数字能精确到多少位小数。尾数位越多,能区分的相邻数值越细。
类比:科学计数法里 1.xxxxx 中小数点后的位数。
精度不足 → 舍入误差,计算结果有偏差,但只要不是系统性的就通常可以接受。
6.2 三种格式的位布局对比
6.3 FP16 vs BF16:核心差距在哪里?
具体问题一:FP16 的溢出 / 下溢
- FP16 能表示的最大正数 ≈ 65504,最小正数 ≈ 6×10⁻⁸
- FP32 能表示的最大正数 ≈ 3.4×10³⁸,小好几十个数量级
- 训练大模型时,某些层的激活值可能 >65504(Overflow),变成 +inf
- 梯度经过多层反向后可能非常小(<6×10⁻⁸),变成 0(Underflow),权重停止更新
- 这两种情况都会让训练直接崩掉,表现为 loss 变成 NaN
具体问题二:BF16 的精度代价
- BF16 尾数只有 7 位,十进制约 2~3 位有效数字;FP16 尾数 10 位,约 3~4 位
- FP32 尾数 23 位,约 7 位有效数字
- 在大模型训练中,单步计算的舍入误差影响很小,经过大量 step 后统计上会被平均掉
- 实践证明:用 BF16 训练的收敛曲线与 FP32 几乎一致(GPT-3、LLaMA 均如此)
- 而 FP16 溢出导致的 NaN 则是灾难性的,一旦出现训练就废了
| 格式 | 总位数 | 指数位 | 尾数位 | 数值范围 | 十进制精度 | 主要问题 | 适用 GPU |
|---|---|---|---|---|---|---|---|
| FP32 | 32 | 8 | 23 | ~±3.4×10³⁸ | ~7 位 | —(基准) | 所有 GPU |
| FP16 | 16 | 5 | 10 | ~±65504 | ~3-4 位 | ⚠️ 容易溢出,需 Loss Scaling | V100、RTX 3090/4090 |
| BF16 | 16 | 8 | 7 | ~±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 主权重来做精确更新,这就是「混合」的含义:
6.5 Loss Scaling:专门解决 FP16 梯度下溢的工程手段
解法:在计算 loss 后、反向传播前,把 loss 乘以一个大倍数 S(如 65536),这样梯度也等比放大 S 倍,不会下溢。Adam step 之前再除以 S 还原回正确值。
动态 Loss Scaling:DeepSpeed 会自动调整 S。如果本 step 梯度出现 inf/NaN,S 减半并跳过本 step;连续若干 step 没问题,S 再翻倍。完全自动,不用手动设置。
BF16 指数位与 FP32 相同,能覆盖同样的数值范围(~±3.4×10³⁸)。无论梯度多小,都不会下溢到 0。BF16 的问题是精度低(尾数只有 7 位),但这种精度损失是均匀的、统计中性的,不会导致系统性的梯度消失,大模型实践中完全可以接受。
6.6 实际选型建议
| 场景 | 推荐精度 | 原因 |
|---|---|---|
| A100 / H100 训练 | BF16 | 硬件原生支持,范围=FP32,无需 Loss Scaling,稳定 |
| V100 / RTX 3090 / 4090 | FP16 | 这些 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 翻倍
}
ZeRO 切的是 P/G/O,但激活(activations)没碰。激活是前向中所有中间张量,反向时要用,默认全量保留。Gradient Checkpointing 的思路很暴力:
# HuggingFace Trainer / LLaMA-Factory 一行开启 model.gradient_checkpointing_enable() # 或在 TrainingArguments 里 gradient_checkpointing=True
| seq_len | batch=4 | 不开 GC | 开 GC | 速度代价 |
|---|---|---|---|---|
| 2048 | 4 | ~8 GB | ~2 GB | -25% throughput |
| 4096 | 4 | ~24 GB | ~6 GB | -30% throughput |
| 8192 | 4 | ~80 GB(OOM) | ~18 GB | -33% throughput |
下面是一份通用 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 ...
场景 × 配置速查表
| 场景 | 模型 | 显卡 | 推荐配置 | 备注 |
|---|---|---|---|---|
| 消费级单卡 LoRA 微调 | 7B | RTX 4090 (24G) | ZeRO-2,BF16,不 Offload | LoRA 减少可训参数,显存友好 |
| 消费级单卡全参微调 | 7B | RTX 4090 (24G) | ZeRO-3 + offload_optimizer | 速度慢 3×,但能跑通 |
| 单机 8 卡微调 | 13B/30B | 8×A100-80G | ZeRO-3,BF16,不 Offload | 8 卡 ZeRO-3:13B 只占 26GB/卡 |
| 单机 8 卡极限 | 70B Full SFT | 8×A100-80G | ZeRO-3 + offload_optimizer + GC | 70B×16/8=140GB,必须 Offload |
| 多机预训练 | 100B+ | 32+ 卡 | TP=8 + PP=N + DP×ZeRO-1 | Megatron-DeepSpeed 联合方案 |
| RLHF / GRPO | 7B Actor+Ref+Reward | 8×A100 | ZeRO-3 + Hybrid Engine | DS-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 字节/参数法则),再回头看这些配置,就只是「在哪一刀切下去」的工程选择。
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 |
| 适用场景 | 单层太大放不下 | 整体状态太大放不下 |
Q2:为什么 ZeRO 要分 Stage 1/2/3,而不是直接全部拆了(Stage 3)更好呢?
先看三个 Stage 各自切了什么:
| Stage | 切了什么 | 额外通信 | 显存节省(相对 DDP) |
|---|---|---|---|
| Stage 1 | Optimizer States(最大头,占 ~12 bytes/param) | = DDP(无额外) | ~56% |
| Stage 2 | Optimizer States + Gradients | = DDP(无额外) | ~66% |
| Stage 3 | Optimizer States + Gradients + Parameters(权重本身) | ×1.5 DDP | 75%(最大) |
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 为什么要全部存下来,不能算完一层立刻覆盖掉吗?
表面上看"算完一层梯度就用掉然后覆盖"是合理的,但这里有一个关键时序问题。
各卡处理不同 batch,各自算出的梯度是不一样的。必须先把所有卡的梯度汇总求平均,才能得到"全局梯度"用于更新参数。
你说的"边算边发、覆盖内存"是 DDP 的 bucket AllReduce 优化,确实是可行的——反向传播期间,每算完一层就立刻把这层梯度发出去做 AllReduce,同时继续算前面层的梯度。
但 AllReduce 的结果(平均后的梯度)必须写回显存,一直驻留到 Optimizer Step 用完为止:
m = β₁·m + (1−β₁)·g,w = w - lr·m/√v
关键在于 Step 1 和 Step 2 是串行的两个阶段,Step 1 全部完成后 Step 2 才开始。所以在 Step 2 进行期间,所有层的"平均后梯度"必须同时在显存里,等 optimizer 按参数顺序逐一消费完才能释放。
Q4:混合精度里 FP32 副本是干什么的?既然 FP16 精度不够,为什么不直接全用 FP32,还要多存一份 FP16 不是更浪费?
先澄清一个常见误解:FP32 副本不是用来做前向/反向计算的,它只在 Optimizer Step 的最后一步用。
FP32 副本解决的问题:更新步长太小被 FP16 舍入成 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?
A100 GPU 上:
- FP16 矩阵乘吞吐:312 TFLOPS
- FP32 矩阵乘吞吐:19.5 TFLOPS(慢 16×)
所以那份额外的 FP16 参数副本(14 GB)是「购买 Tensor Core 加速」的票价——换来的是前向+反向 16× 的速度提升。拿 14 GB 换这个,非常划算。
| 方案 | 参数存储 | 前向+反向速度 | 更新精度 | 实际可行性 |
|---|---|---|---|---|
| 全 FP32 | 28 GB(7B 模型) | 慢 16×,用不上 Tensor Core | ✅ 足够 | 显存翻倍、速度极慢 |
| 混合精度(FP16 + FP32 副本) | 14 GB FP16 + 28 GB FP32 = 42 GB | ✅ 快 16× | ✅ 足够(FP32 更新) | 业界标准做法 |
| 全 FP16 | 14 GB | ✅ 快 16× | ❌ 更新量被舍入 0 | 训练会失效 |