分布式训练与显存优化¶
本章导读
- 大模型训练的第一约束通常不是算力,而是显存:参数、梯度、优化器状态、激活和临时缓冲区共同决定单卡能放多大的模型。
- 数据并行(Data Parallel,DP)、张量并行(Tensor Parallel,TP)、流水线并行(Pipeline Parallel,PP)、序列并行(Sequence Parallel,SP)、专家并行(Expert Parallel,EP)解决的是不同维度的切分问题,通信量和适用拓扑完全不同。
- 零冗余优化器(Zero Redundancy Optimizer,ZeRO)与完全分片数据并行(Fully Sharded Data Parallel,FSDP)把“每张卡都存全量状态”改成“按需 all-gather”,用通信换显存;理解分片对象和通信次数,才能判断某个配置是否可行。
- 训练吞吐用模型浮点利用率(Model FLOPs Utilization,MFU)衡量;估算训练时间只需 \(6ND\) 与峰值算力,但真实瓶颈常在通信、重计算与故障恢复。
核心要点
- 混合精度 + AdamW 全参训练约需 16 字节/参数:bf16 权重 2、bf16 梯度 2、fp32 master 4、Adam 一阶矩 4、二阶矩 4。
- 7B 模型全参状态约 112GB,70B 约 1.12TB;单卡放不下时必须组合 TP/PP/ZeRO-3。
- TP 每层有多次 all-reduce,通信密集,通常限制在单机 8 卡内;PP 只在 stage 边界传激活,适合跨机;DP 通信量随参数量线性增长。
- ZeRO-1 分片优化器状态,ZeRO-2 再分片梯度,ZeRO-3 再分片参数;FSDP 是 ZeRO-3 的工程实现,可按层 all-gather。
- 激活重计算用约 33% 额外前向计算换显存,是长序列训练能跑起来的常用手段。
- 训练 FLOPs 约 \(6ND\)(前向 \(2ND\)、反向 \(4ND\));MFU 把实测吞吐折算成峰值算力的比例,预训练常见 35%–50%。
- 通信瓶颈由“通信量/计算量”与拓扑共同决定:NVLink 域内做 TP,InfiniBand 域间做 DP/PP。
- 容错的核心是可恢复的分布式检查点(checkpoint):保存分片参数、优化器、RNG、数据加载器状态,并定期校验。
1. 显存构成¶
1.1 五类显存¶
设模型参数量为 \(P\),参数精度字节数为 \(b_{\text{param}}\),则:
其中 \(M_{\text{param}}\) 是权重显存,\(M_{\text{grad}}\) 是梯度显存。bf16 下两者各为 \(2P\) 字节。
Adam/AdamW 为每个参数维护一阶矩 \(m\) 与二阶矩 \(v\),通常用 fp32 存储:
混合精度训练还需要一份 fp32 的 master weights(主权重),用于吸收小梯度更新,避免 bf16/fp16 直接更新时被舍入:
因此“bf16 权重 + bf16 梯度 + fp32 master + Adam m/v”的总状态为:
若用纯 fp32 训练,则是 \(4P\) 参数 + \(4P\) 梯度 + \(8P\) 优化器 = \(16P\),总字节数相同,但纯 fp32 没有 master 副本,前向也更慢。混合精度的收益主要在计算速度与激活显存,而不是优化器状态。
| 模型 | 参数量 | bf16 权重 | bf16 梯度 | fp32 master | Adam m/v | 合计(估算) |
|---|---|---|---|---|---|---|
| 7B | 7×10⁹ | 14 GB | 14 GB | 28 GB | 56 GB | 112 GB |
| 13B | 1.3×10¹⁰ | 26 GB | 26 GB | 52 GB | 104 GB | 208 GB |
| 70B | 7×10¹⁰ | 140 GB | 140 GB | 280 GB | 560 GB | 1120 GB |
| 175B | 1.75×10¹¹ | 350 GB | 350 GB | 700 GB | 1400 GB | 2800 GB |
表中按十进制 GB 估算,未计激活、临时缓冲与显存碎片;实际至少再留 20%–30% 余量。可以看到 7B 全参训练在单张 80GB 卡上放不下,必须分片或卸载。
1.2 激活显存¶
激活(activation)是前向过程中保存、反向传播要用的中间张量。以 Transformer 每层每样本的粗略公式(Megatron-LM 常用,单位字节,fp16):
其中 \(s\) 是序列长度,\(b\) 是 micro-batch,\(h\) 是隐藏维度,\(a\) 是注意力头数。括号中的 \(34\) 来自 QKV、MLP 中间层、LayerNorm 等;\(5as/h\) 来自注意力分数矩阵(若 FlashAttention 不物化分数,这一项会大幅下降)。
以 7B(\(h=4096\)、\(a=32\)、32 层)、\(s=4096\)、\(b=1\) 估算:每层每样本约 \(4096\times4096\times(34+160)\approx3.26\) GB,32 层约 104GB。这是“不用 FlashAttention、不做重计算”的最坏情况;实际使用 FlashAttention 后分数不落盘,激活可降到 20–40GB;再做全层激活重计算,只保留每层输入,约 \(2sbh\) 字节/层,总计 1–2GB,另加反向重算时的工作区(量级估算,随实现变化)。
激活显存与 batch、序列长度近似线性(注意力分数项对 \(s\) 是平方),因此长上下文训练首先撞上的是激活墙,而不是参数墙。
1.3 临时缓冲区与碎片¶
- 通信缓冲区:NCCL 的 all-reduce/all-gather 需要连续显存;bucket 越大越省启动开销,但峰值越高。
- cuBLAS/cuDNN workspace:GEMM、FlashAttention 反向会申请临时空间,长序列时可达数 GB。
- 显存碎片:反复分配不同大小张量会造成碎片,
PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True可缓解。 - CUDA context 与 kernel:每张卡约 0.5–1GB 固定开销(估算)。
- offload 的 pinned memory:CPU 端固定页内存不计入 GPU 显存,但会占用主机内存与 PCIe 带宽。
2. 并行策略¶
2.1 数据并行(DP)¶
每张卡保存完整模型,把全局 batch 切成 \(N\) 份;各卡独立前向/反向,最后对梯度做 all-reduce 求平均。设参数量 \(P\)、精度 2 字节,则每步梯度通信量约 \(2P\) 字节(ring all-reduce 的流量为 \(2P(N-1)/N\),接近 \(2P\))。
优点:实现简单、计算通信比高、适合单机多卡与多机扩展。缺点:每卡都要存全量参数/梯度/优化器状态,7B 以上很快放不下;global batch 随卡数线性增大,需要调整学习率与 warmup;梯度 all-reduce 在跨机时受 IB 带宽限制。
梯度累积(gradient accumulation)用小 micro-batch 模拟大 batch,不增加显存中的激活峰值,但会增加单步时间;它与 DP 正交,常一起使用。
2.2 张量并行(TP)¶
TP 把单层矩阵乘按列/行切分到多卡。以 MLP 的 \(Y=XW\)、\(W\in\mathbb{R}^{h\times4h}\) 为例:
- 列并行:\(W=[W_1,W_2]\),各卡算 \(XW_i\),输出按列拼接;无需通信。
- 行并行:\(W=[W_1;W_2]\),各卡算 \(X_iW_i\),输出需要 all-reduce 求和。
Transformer 一层通常包含 attention 的 QKV 列并行、输出投影行并行、MLP 上投影列并行、下投影行并行,因此前向有 2 次 all-reduce,反向有 2 次,共 4 次/层;每次通信量约 \(b\cdot s\cdot h\) 个元素,合计约 \(8bsh\) 字节/层(估算,随切分方式变化)。TP 度越大,单卡计算越少、通信占比越高,通常不超过单机 8 卡,且必须走 NVLink。
TP 的收益是单层就能切,适合“层数不多但单层很宽”的模型;缺点是通信频繁、对拓扑敏感,跨机 TP 往往得不偿失。
2.3 流水线并行(PP)¶
PP 按层把模型切成 \(p\) 个 stage,每个 stage 放在不同设备上;micro-batch 依次流过 stage。经典 GPipe 先做完全部前向再反向,气泡(bubble)比例为:
其中 \(m\) 是 micro-batch 数;\(m\) 远大于 \(p\) 时近似为 \((p-1)/m\)。1F1B(one forward one backward)在稳态下交错执行前向与反向,气泡约为 \((p-1)/m\),并减少激活占用;交错式 1F1B 把每个 stage 再切成 \(v\) 个虚拟 stage,气泡进一步降到约:
PP 只在 stage 边界传激活,每个 micro-batch 前向传 \(bsh\) 个元素、反向传 \(bsh\) 个梯度元素(bf16 下合计约 \(4bsh\) 字节),适合跨机;难点是 stage 负载均衡(按 FLOPs 而非层数切)、micro-batch 数要足够多才能掩盖气泡、以及反向依赖导致的调度复杂。
2.4 序列并行(SP)¶
SP 把序列维度切到多卡,主要针对 LayerNorm、Dropout、残差等“按 token 独立”的操作,避免 TP 后每张卡重复存完整序列的激活。典型做法(Megatron 的 sequence parallel):
- 进入 attention/MLP 前对序列做 all-gather,得到完整 QKV 或输入;
- 计算完 attention/MLP 后对输出做 reduce-scatter,回到序列分片状态。
通信模式与 TP 类似,但作用在序列维,能显著降低长序列激活;对超长上下文(32k–1M),还会用 Ring Attention、Ulysses 等等价切分注意力的方法,把 \(s^2\) 的注意力计算分散到多卡。
2.5 专家并行(EP)¶
MoE(Mixture of Experts)把 \(E\) 个专家分布到不同卡,每个 token 经路由选 top-\(k\) 个专家。EP 的核心通信是 all-to-all:dispatch 把 token 发到对应专家所在卡,combine 把结果收回,每层两次,通信量约 \(O(bsh\cdot k)\)。
难点:负载不均导致“热点专家”等待;需要 capacity factor(每专家最多接收 token 数)与负载均衡辅助损失;all-to-all 对网络带宽和延迟敏感;EP 通常与 DP/TP 组合,专家参数量大但每 token 只激活一部分,适合“总参数量大、单 token 计算量可控”的稀疏模型。
2.6 并行策略对比¶
| 策略 | 切分维度 | 每步通信 | 通信频率 | 适用范围 | 主要瓶颈 |
|---|---|---|---|---|---|
| DP | batch | 梯度 all-reduce,约 \(2P\) 字节 | 每步一次 | 全局,跨机 | 参数量、跨机带宽 |
| TP | 隐藏维/头 | 每层 4 次 all-reduce,约 \(8bsh\) 字节 | 每层 | 单机 8 卡内 | NVLink 带宽、延迟 |
| PP | 层 | stage 边界激活,前向+反向约 \(4bsh\) 字节/micro-batch | 每 micro-batch | 跨机 | 气泡、负载均衡 |
| SP | 序列 | all-gather + reduce-scatter | 每层 | 长序列、与 TP 配合 | 通信量、实现复杂度 |
| EP | 专家 | 每层 2 次 all-to-all | 每层 | MoE、多机 | 负载不均、all-to-all |
3. ZeRO 与 FSDP¶
3.1 三个 stage 分片什么¶
ZeRO(Zero Redundancy Optimizer)把 DP 中每卡重复存储的状态切开,需要时再收集:
| 阶段 | 分片对象 | 每卡显存(混合精度,估算) | 额外通信 |
|---|---|---|---|
| 基线 DP | 无 | \(16P\) | 梯度 all-reduce,\(2P\) |
| ZeRO-1 | 优化器状态(master + m + v) | \(4P+12P/N\) | reduce-scatter 梯度 + all-gather 参数,共约 \(2P\) |
| ZeRO-2 | 优化器状态 + 梯度 | \(2P+14P/N\) | reduce-scatter 梯度 + all-gather 参数,共约 \(2P\) |
| ZeRO-3 | 优化器状态 + 梯度 + 参数 | \(16P/N\) | 2 次参数 all-gather + 梯度 reduce-scatter,约 \(3P\) |
其中 \(N\) 是 DP 组大小。以 7B、8 卡为例:基线 112GB,ZeRO-1 约 38.5GB,ZeRO-2 约 26.3GB,ZeRO-3 约 14GB(均不含激活)。
70B、64 卡:ZeRO-1 约 293GB,ZeRO-2 约 155GB,二者都超过 80GB;ZeRO-3 约 17.5GB 才能放下。这说明“加卡”不能解决 ZeRO-2 的参数复制问题——参数和梯度仍然每卡一份,必须靠 TP/PP 或 ZeRO-3 真正切开。
3.2 通信开销¶
ZeRO-1/2 的通信量与普通 DP 同阶(reduce-scatter 代替 all-reduce),但可以把梯度分片通信与反向计算重叠;ZeRO-3 每层前向要 all-gather 参数、反向要再次 all-gather 并 reduce-scatter 梯度,通信量约为 DP 的 1.5 倍(\(3P\) vs \(2P\),估算),对跨机带宽更敏感。
ZeRO-3 的显存收益来自“参数只在用时存在”:一层算完就释放,峰值约为单层参数量加分片状态;代价是频繁的小 all-gather,需要把 bucket 调大以摊薄延迟。ZeRO-Offload 进一步把优化器状态放到 CPU,ZeRO-Infinity 放到 NVMe,用 PCIe/NVMe 带宽换显存。
3.3 FSDP 与 ZeRO-3 的关系¶
完全分片数据并行(Fully Sharded Data Parallel,FSDP)是 PyTorch 原生实现,思想与 ZeRO-3 一致:参数按 flat parameter 分片,前向按层 all-gather,计算后释放,反向再次 all-gather 并 reduce-scatter 梯度。区别主要在工程细节:
- FSDP 以
FlatParameter为单位分片,支持FULL_SHARD、SHARD_GRAD_OP(类似 ZeRO-2)、HYBRID_SHARD(节点内全分片、节点间复制,即 HSDP)。 - FSDP 可与 TP/PP 组合;
device_mesh让多维并行配置更清晰。 - FSDP 的 checkpoint 需要
FullStateDictConfig或分片保存,不能直接torch.save(model.state_dict())后按单卡加载。 - ZeRO 是 DeepSpeed 的实现,生态里还有 ZeRO-Infinity、MoE 支持、通信压缩等。
3.4 3D 并行组合¶
典型配置:70B 模型、512 张 80GB H100,TP=8、PP=4、DP=16。每 stage 约 17.5B 参数,TP=8 后每卡约 2.19B 参数,按 16 字节/参数估算约 35GB;再加激活与通信缓冲约 45–50GB,可落在 80GB 内。global batch = DP 16 × micro-batch 1 × gradient accumulation 16 = 256 条序列(示例)。若再叠加 ZeRO-1,优化器状态还能按 DP 继续分片,显存更宽裕。
组合原则:
- 先让单层放得下:TP 度覆盖单层参数与激活;
- 再让层数分得均:PP 按 FLOPs 均衡,stage 数不超过 micro-batch 数;
- 最后用 DP 扩吞吐:DP 度决定 global batch 与梯度 all-reduce 规模;
- 长序列额外开 SP/CP(context parallel),MoE 额外开 EP。
4. 混合精度¶
4.1 FP16、BF16、FP8¶
| 格式 | 位宽 | 指数位 | 尾数位 | 动态范围 | 是否需要 loss scaling | 典型用途 |
|---|---|---|---|---|---|---|
| FP32 | 32 | 8 | 23 | 约 \(10^{\pm38}\) | 否 | master weights、优化器状态 |
| TF32 | 19 | 8 | 10 | 同 FP32 | 否 | A100 上替代 FP32 做 GEMM |
| FP16 | 16 | 5 | 10 | 约 \(6\times10^4\) 上限 | 是 | 推理、部分训练 |
| BF16 | 16 | 8 | 7 | 约 \(10^{\pm38}\) | 否 | 训练默认 |
| FP8 E4M3 | 8 | 4 | 3 | 约 \(\pm448\) | 是(缩放) | 前向权重/激活 |
| FP8 E5M2 | 8 | 5 | 2 | 约 \(\pm57344\) | 是(缩放) | 反向梯度 |
FP16 尾数多、精度高,但最大可表示数只有 65504,小梯度容易下溢,需要损失缩放(loss scaling):前向算出的 loss 乘一个缩放因子 \(S\),反向得到放大 \(S\) 倍的梯度,更新前再除回去。动态损失缩放会根据是否出现 inf/NaN 调整 \(S\):连续若干步成功则增大,溢出则减半并跳过该步。
# PyTorch 原生 AMP:自动维护 loss scale(示意)
amp_dtype = torch.bfloat16 # fp16 时改为 torch.float16
scaler = torch.cuda.amp.GradScaler(init_scale=2**16, growth_interval=2000,
enabled=(amp_dtype == torch.float16))
for x, y in loader:
opt.zero_grad(set_to_none=True)
with torch.autocast(device_type="cuda", dtype=amp_dtype):
loss = model(x, labels=y).loss
scaler.scale(loss).backward() # 仅 fp16 需要;bf16 时 scaler 已禁用
scaler.unscale_(opt) # 裁剪梯度前必须先 unscale
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
scaler.step(opt)
scaler.update()
BF16 指数位与 FP32 相同,动态范围足够,通常不需要 loss scaling;代价是尾数只有 7 位,舍入误差比 FP16 大,但在深度学习中被证明足够。BF16 已成为预训练默认,FP16 更多用于推理或旧硬件。
4.2 FP8 训练¶
FP8 训练的核心是缩放(scaling):把张量按 per-tensor 或 per-block 的 amax 映射到 FP8 的可表示范围。前向的权重/激活用 E4M3(精度高、范围小),反向梯度用 E5M2(范围大、精度低),并维护 amax 历史做延迟缩放(delayed scaling)。H100 的 Transformer Engine 把缩放、cast、GEMM 融合,FP8 理论吞吐约为 BF16 的 2 倍。
风险:离群值(outlier)会拉大 amax,导致其余值量化过粗;需要 per-channel/per-block 缩放、残差与 LayerNorm 保持高精度、定期用 BF16 校准。FP8 不是“免费加速”,在中小模型或短序列上,缩放开销与精度回退可能抵消收益。
4.3 master weights 为什么需要¶
假设权重是 bf16,学习率 \(10^{-5}\),参数值约 \(0.1\),更新量约 \(10^{-6}\)。bf16 在 \(0.1\) 附近的间隔约 \(10^{-3}\),更新会被直接舍入掉。保留一份 fp32 master,更新在 fp32 上累加,再把结果 cast 回 bf16 用于前向,可以避免“权重被冻住”。这也是为什么混合精度训练的状态字节数仍是 16P。
5. 激活重计算与 offload¶
5.1 梯度检查点¶
激活重计算(gradient checkpointing / activation recomputation)在前向时只保存每个 checkpoint 段的输入,不保存段内中间张量;反向时重新前向该段,重建中间量再算梯度。全层检查点把激活从 \(O(L)\) 降到 \(O(1)\) 量级(只存层输入),代价是每个 step 多一次前向:
因为前向是 \(2ND\),反向是 \(4ND\),重算一次前向增加 \(2ND\),总计算增加约 33%。若用“每 \(\sqrt{L}\) 层设一个 checkpoint”的 sqrt 策略,显存为 \(O(\sqrt{L})\),重算量更小。
from torch.utils.checkpoint import checkpoint
class TransformerBlock(nn.Module):
def forward(self, x):
# 训练时只存输入,反向重算 attention/MLP 内部激活
if self.training and self.grad_checkpointing:
return checkpoint(self._forward, x, use_reentrant=False)
return self._forward(x)
def _forward(self, x):
x = x + self.attn(self.norm1(x))
x = x + self.mlp(self.norm2(x))
return x
选择策略:全层检查点最省显存但最慢;选择性检查点只重算注意力(或只重算 MLP),通常能省 50%–70% 激活而只增加 10%–15% 计算(估算)。use_reentrant=False 对新版 PyTorch 的自动混合精度、非张量输入更友好。
5.2 Offload¶
- 优化器 offload:把 master weights、m、v 放 CPU,GPU 只保留 bf16 参数与梯度;更新时把梯度搬到 CPU 算 Adam,再把参数拷回。DeepSpeed ZeRO-Offload 是典型实现。PCIe Gen4 x16 双向约 64GB/s,远低于 HBM 的 3TB/s,频繁搬运会让 GPU 空等。
- 参数/激活 offload:ZeRO-Infinity 用 NVMe 存优化器状态,容量大但带宽只有数 GB/s,适合“显存极度紧张、吞吐不敏感”的场景。
- 计算与搬运重叠:用独立 CUDA stream 预取下一层参数,能把部分带宽隐藏;但 offload 的训练吞吐通常只有全 GPU 的 30%–60%(估算)。
offload 的取舍是“用时间换显存”:当模型只差一点放不下时很有效;当差距是数倍时,吞吐会不可接受,应优先考虑 TP/PP/ZeRO-3。
6. 计算量估算与 MFU¶
6.1 6ND 从哪来¶
对参数量 \(P\)、训练 token 数 \(D\) 的稠密 Transformer,忽略 attention 的 \(s^2\) 项,前向每个 token 约 \(2P\) FLOPs(每个参数一次乘加),反向约为前向的 2 倍(计算对权重和对输入的梯度),因此:
其中 \(N\) 是参数量(习惯用 \(N\) 而非 \(P\)),\(D\) 是训练 token 数。长序列时注意力项不可忽略:
其中 \(n_{\text{layer}}\) 是层数,\(s\) 是序列长度,\(d\) 是隐藏维度;系数 12 来自前向两次 \(s^2d\) 矩阵乘、反向约两倍,causal 注意力可近似减半。\(s=4096\)、\(d=4096\)、32 层时,注意力项约占 6ND 的 15%(估算),序列到 128k 时会成为主导。
6.2 MFU¶
其中 \(T\) 是训练时间,\(P_{\text{peak}}\) 是硬件峰值算力(bf16 dense)。MFU 把“实际吞吐”折算成理论峰值的比例,便于跨硬件比较。
| 硬件 | BF16 峰值(dense,估算) | 显存/带宽 | 典型 MFU |
|---|---|---|---|
| A100 SXM 80GB | 312 TFLOPS | 80GB HBM2e,2.0 TB/s | 35%–45%(预训练) |
| H100 SXM 80GB | 989 TFLOPS | 80GB HBM3,3.35 TB/s | 40%–50%(预训练) |
| H800 SXM | 989 TFLOPS | 80GB,NVLink 400GB/s | 35%–45%(受互联限制) |
| 消费级 RTX 4090 | 165 TFLOPS(bf16,估算) | 24GB,1.0 TB/s | 10%–25%(单卡/小集群) |
以 7B、1T token、1024 张 H100、MFU 40% 估算:
70B、15T token 在同样集群上约 \(180\) 天(估算),所以实际需要数千到上万张卡,或更长时间。数量级比精确值更重要:它决定了“这个训练计划是否现实”。
6.3 通信与 roofline¶
一个算子是否受通信限制,看它的算术强度(FLOPs/byte)与机器的“算力/带宽”比值。H100 的 bf16 峰值/显存带宽约 \(989\text{ TFLOPS}/3.35\text{ TB/s}\approx295\) FLOPs/byte;NVLink 900GB/s 对应约 265 TFLOPS 的有效算力,IB 400Gb/s(50GB/s)只有约 15 TFLOPS。这解释了:
- TP 的 all-reduce 走 NVLink,仍可能达到 50% 以上峰值;跨 IB 做 TP 会被带宽拖死。
- DP 的梯度 all-reduce 量随 \(P\) 增长,70B 模型每步 140GB 量级,跨机必须用分层 all-reduce、reduce-scatter、通信压缩或 ZeRO。
- PP 的边界激活只有约 \(4bsh\) 字节/micro-batch,跨机成本低,但气泡会浪费算力。
7. 拓扑、通信与容错¶
7.1 常见拓扑¶
- NVLink/NVSwitch:单机 8 卡全互联。H100 每卡 NVLink 4 约 900GB/s 双向,NVSwitch 让 8 卡任意两两通信不经过 CPU;A100 为 600GB/s。TP/SP 应限制在这个域内。
- InfiniBand(IB):跨机主力,NDR 400Gb/s ≈ 50GB/s/端口,HDR 200Gb/s ≈ 25GB/s。大规模集群用 rail-optimized 胖树(fat-tree),保证任意两卡间有对等带宽;也有 DragonFly 等低直径拓扑。
- 以太网 RoCEv2:成本低,但需要 PFC/ECN 调优,尾延迟与拥塞控制不如 IB。
- PCIe:GPU-CPU/GPU-GPU(无 NVLink 时)通道,Gen4 x16 约 64GB/s,Gen5 约 128GB/s;offload 与跨卡通信都受它限制。
NCCL 会根据拓扑选择 ring 或 tree;大规模 all-reduce 常用分层算法(节点内 reduce-scatter + 节点间 all-reduce + 节点内 all-gather)。通信与计算重叠(overlap)是提升 MFU 的关键:把 all-gather 放在上一层的反向计算中预取,把梯度 reduce-scatter 与下一层反向重叠。
7.2 容错与断点续训¶
万卡训练中,硬件故障是常态:GPU 掉卡、ECC 错误、NVLink 降速、网卡抖动、静默数据损坏(silent data corruption,SDC)。工程要点:
- 分片 checkpoint:每个 rank 只写自己的分片,避免全量 gather 到 rank0 造成内存峰值;保存 optimizer、scheduler、随机数生成器(RNG)、data loader 的 epoch/step。
- 异步 checkpoint:先把分片拷到 CPU/远端存储,再继续训练;checkpoint 期间用双缓冲避免阻塞。
- 原子写与版本号:先写临时目录,完成后 rename;保留最近 \(k\) 个 checkpoint,防止写坏。
- 故障检测:NCCL watchdog、心跳、loss/梯度范数异常检测;发现 NaN 或 loss 突增时回滚到最近 checkpoint,并隔离可疑节点。
- 弹性训练:torchelastic/JobSet 支持 worker 重启后重新 rendezvous,改变 world size 后要重新切分数据与并行组。
- checkpoint 间隔:按平均无故障时间(Mean Time Between Failures,MTBF)与 checkpoint 成本权衡,大集群常 30–60 分钟一次(估算);间隔太长,故障后重算损失大;太短,I/O 拖慢训练。
8. 代码示例¶
8.1 DDP 最小示例¶
import os, torch, torch.distributed as dist
from torch.nn.parallel import DistributedDataParallel as DDP
from torch.utils.data import DataLoader, DistributedSampler
# 启动:torchrun --nproc_per_node=8 ddp.py
dist.init_process_group("nccl")
rank = int(os.environ["LOCAL_RANK"]); torch.cuda.set_device(rank)
model = DDP(MyModel().to(rank), device_ids=[rank])
ds = MyDataset(); sampler = DistributedSampler(ds, shuffle=True)
loader = DataLoader(ds, batch_size=8, sampler=sampler)
opt = torch.optim.AdamW(model.parameters(), lr=3e-4)
for epoch in range(10):
sampler.set_epoch(epoch) # 每个 epoch 的切分不同
for x, y in loader:
x, y = x.to(rank), y.to(rank)
opt.zero_grad(set_to_none=True)
loss = model(x, labels=y).loss
loss.backward(); opt.step() # DDP 反向中自动 all-reduce 梯度
dist.destroy_process_group()
8.2 激活检查点¶
from torch.utils.checkpoint import checkpoint
class CheckpointedBlock(torch.nn.Module):
def __init__(self, attn, mlp, norm1, norm2):
super().__init__()
self.attn, self.mlp, self.norm1, self.norm2 = attn, mlp, norm1, norm2
self.grad_checkpointing = True
def _forward(self, x):
x = x + self.attn(self.norm1(x))
x = x + self.mlp(self.norm2(x))
return x
def forward(self, x):
if self.training and self.grad_checkpointing:
return checkpoint(self._forward, x, use_reentrant=False)
return self._forward(x)
9. 面试高频问题¶
1. 7B 模型全参训练需要多少显存?如何拆解?¶
- 混合精度 + AdamW 约 16 字节/参数:bf16 权重 2、梯度 2、fp32 master 4、m/v 各 4。
- 7B 约 112GB,70B 约 1.12TB;还要加激活、通信缓冲和碎片,实际预留 20%–30%。
- 单卡 80GB 放不下 7B,需要 ZeRO-3/FSDP、TP 或 offload。
- 纯 fp32 也是 16 字节/参数,混合精度省的是计算和激活,不是优化器状态。
2. DP、TP、PP 的通信量和适用场景有什么区别?¶
- DP 每步 all-reduce 约 \(2P\) 字节,计算通信比高,适合跨机扩吞吐,但每卡存全量状态。
- TP 每层 4 次 all-reduce、约 \(8bsh\) 字节,通信密集,适合单机 NVLink 内 8 卡。
- PP 只在 stage 边界传激活,约 \(4bsh\) 字节/micro-batch,适合跨机,但有气泡和负载均衡问题。
- 实际用 3D 并行:TP 放单机、PP 跨机切层、DP 扩全局 batch。
3. ZeRO-1/2/3 分别分片什么?通信开销如何变化?¶
- Stage 1 分片优化器状态(master + m + v),每卡 \(4P+12P/N\);Stage 2 再分片梯度,\(2P+14P/N\);Stage 3 再分片参数,\(16P/N\)。
- Stage 1/2 通信与 DP 同阶(reduce-scatter),Stage 3 每层 all-gather 参数,通信约为 DP 的 1.5 倍。
- Stage 3 显存最省,但小 all-gather 频繁,需要 bucket 与重叠;跨机时可能被带宽拖慢。
- 70B 在 64 张 80GB 上,ZeRO-2 仍需每卡 140GB 参数+梯度,根本放不下,必须 ZeRO-3 或 TP/PP。
4. FSDP 和 ZeRO-3 是什么关系?¶
- 思想相同:参数分片、前向按需 all-gather、反向 reduce-scatter 梯度。
- FSDP 是 PyTorch 原生实现,支持 FULL_SHARD、SHARD_GRAD_OP、HYBRID_SHARD;ZeRO 是 DeepSpeed 实现。
- FSDP 以 FlatParameter 为单位,按层 wrap 很重要;wrap 太粗会增大峰值,太细会增加通信。
- checkpoint 要保存分片状态,或用
FullStateDictConfig聚合,不能直接单卡加载。
5. 混合精度为什么需要 master weights?BF16 还需要 loss scaling 吗?¶
- bf16 在参数附近的量化间隔大于小学习率更新量,直接更新会被舍入掉。
- fp32 master 累积更新,再把结果 cast 回 bf16 前向,保证更新不丢失。
- BF16 指数位与 FP32 相同,动态范围足够,通常不需要 loss scaling。
- FP16 需要动态 loss scale;FP8 需要 per-tensor/per-block scaling 与 amax 历史。
6. 激活重计算的代价是什么?什么时候不该用?¶
- 前向只存段输入,反向重算中间激活;计算从 \(6ND\) 增到约 \(8ND\),多 33%。
- 显存从 \(O(L)\) 降到 \(O(1)\) 或 \(O(\sqrt{L})\),是长序列训练的关键手段。
- 计算已经受限于算力、显存充足时,重计算会白白降低吞吐。
- 选择性重算(只重算 attention)通常性价比最高;优先用 FlashAttention 降低激活,再考虑重算。
7. 训练 FLOPs 和 MFU 怎么算?常见值是多少?¶
- 稠密模型约 \(6ND\):前向 \(2ND\)、反向 \(4ND\);长序列加 \(12n s^2 d\) 的注意力项。
- MFU = 实测 FLOPs /(峰值算力 × 时间),用 bf16 dense 峰值作分母。
- 预训练常见 35%–50%,微调 20%–35%,小集群/消费卡更低;FP8 峰值不同,不能直接横比。
- 估算训练时间时先算 \(6ND\),再除以“卡数 × 峰值 × MFU”。
8. 为什么 TP 通常不超过单机 8 卡?¶
- TP 每层多次 all-reduce,通信量与激活大小成正比,计算量却随 TP 度下降。
- 跨机 TP 走 IB,带宽比 NVLink 低一个数量级,通信时间会超过计算时间。
- TP 度超过单机 GPU 数后,层内通信必须跨机,延迟和拥塞都会放大。
- 需要更大模型时,优先 PP/DP 跨机,TP 保持在 NVLink 域内。
9. 大规模训练如何做容错和断点续训?¶
- 分片保存参数、优化器、scheduler、RNG、数据加载器状态;异步写、原子 rename、保留多版本。
- 心跳与 NCCL watchdog 检测掉卡/挂起;loss/梯度异常触发回滚与节点隔离。
- checkpoint 间隔按 MTBF 与写入成本权衡,大集群常 30–60 分钟。
- 弹性训练要处理 world size 变化后的并行组重建与数据重切分;SDC 需要定期校验。
10. 长上下文训练为什么需要序列并行?它和 TP 的区别是什么?¶
- 长序列的激活与注意力计算随 \(s\) 增长,TP 只切隐藏维,序列维仍然每卡一份。
- SP 切序列维,LayerNorm/Dropout 等按 token 操作可并行,attention 前 all-gather、后 reduce-scatter。
- 更长的上下文还需要 Ring Attention/Ulysses 把 \(s^2\) 的注意力分散到多卡,通信模式不同。
- SP 降低激活而不减参数;TP 降低参数与计算;两者常组合使用。
本文档为面试复习资料,公式与数字建议结合原始论文复核。