分布式训练扩展 本节摘要:你的 124M 模型在单卡上训完了。现在试 70 亿参数——模型放不进内存,数据在单机上要跑几周。大规模训练不是可选项,是唯一出路。本节带你吃透让大规模训练成为可能的四大策略:数据并行(每卡一份完整模型,梯度取平均)、张量并行(把单层切到多卡)、流水线并行(按层分阶段,微批次填气泡)、完全分片数据并行(FSDP/ZeRO,每卡只存 1/N 的参数/梯度/优化器状态)。你会算清「70B 模型到底要几张 A100」,理解为什么 Adam 优化器状态是显存杀手,以及为什么 Llama 3 405B 要用 16,384 张 H100。 学习目标 阅读完本节,你应当能够: 解释三种并行(数据、张量、流水线)各自何时必需,依据模型与集群规模判断。
本节摘要:你的 124M 模型在单卡上训完了。现在试 70 亿参数——模型放不进内存,数据在单机上要跑几周。大规模训练不是可选项,是唯一出路。本节带你吃透让大规模训练成为可能的四大策略:数据并行(每卡一份完整模型,梯度取平均)、张量并行(把单层切到多卡)、流水线并行(按层分阶段,微批次填气泡)、完全分片数据并行(FSDP/ZeRO,每卡只存 1/N 的参数/梯度/优化器状态)。你会算清「70B 模型到底要几张 A100」,理解为什么 Adam 优化器状态是显存杀手,以及为什么 Llama 3 405B 要用 16,384 张 H100。
阅读完本节,你应当能够:
一个 7B 参数模型,FP16 下光是权重就 14GB。Adam 优化器为每个参数存两份额外拷贝(一阶矩、二阶矩),又是 28GB。反向传播的梯度再加 14GB。一个激活还没存,你已经 56GB 了。
NVIDIA A100 显存 80GB。56/80 已用,剩 24GB 给激活——前向计算、反向要保留的中间值。2048-token 序列、4096 维模型,单层激活约 64MB;32 层,每样本 2GB;批 8 需 16GB。你只有 24GB,批 12 就爆。
现在试 70B。光权重 FP16 就 140GB,单卡放不下。至少要 2 张 A100(160GB)装权重,加上优化器状态和梯度远不止——至少 3 张,现实是 8~16 张视分片策略而定。Llama 3 405B 在 16,384 张 H100 上训练,估算算力成本约 1 亿美元。DeepSeek V3 训了个相当模型只花约 560 万美元——靠架构巧妙(MoE 每 token 只激活部分参数)和训练效率。
真实模型的显存账,每个数字都是算出来的,不是估的。
| 模型 | 参数 | 权重(FP16) | Adam 状态 | 梯度(FP16) | 合计(不含激活) |
|---|---|---|---|---|---|
| GPT-2 Small | 124M | 248 MB | 992 MB | 248 MB | 1.5 GB |
| Llama 3 8B | 8B | 16 GB | 64 GB | 16 GB | 96 GB |
| Llama 3 70B | 70B | 140 GB | 560 GB | 140 GB | 840 GB |
| Llama 3 405B | 405B | 810 GB | 3,240 GB | 810 GB | 4,860 GB |
「Adam 状态」那列是杀手。Adam 为每个参数存一份运行均值(m)和方差(v),都是 FP32。70B 模型即 70B×4 字节×2 = 560GB——光优化器就要 7 张 A100。单张 H100 80GB,Llama 3 405B 光装权重+优化器+梯度至少要 61 张 H100,加激活更多。Meta 用 16,384 张 GPU 不是想用,是不得不用。
最简单的分布式策略。把整个模型复制到 N 卡,每批切成 N 等份,每卡在自己数据片上前向+反向,反向后跨卡平均梯度,每卡用同样的平均梯度更新自己的权重副本,保持同步。好处:吞吐线性扩展,N 卡每步处理 N 倍数据,通信仅限梯度平均(可与计算重叠)。坏处:每卡都装一份完整模型+优化器+梯度,70B 模型每卡要 840GB——数据并行毫不降低单卡显存,只降训练时间。有效批大小 = 每卡批 × N。
把单层切到多卡。一次矩阵乘法分给多卡,各算部分结果。8192×8192 的权重矩阵,4 路张量并行每卡持 8192×2048 的片,各乘输入得部分结果,再 all-reduce 或 all-gather 合成完整输出。好处:降低单卡权重显存,70B 分到 8 卡每卡约 8.75B 参数。坏处:每层后都要快速卡间通信(NVLink 节点内 900 GB/s 可行,跨节点 InfiniBand 约 50 GB/s 不行),几乎总限于单节点内(8 卡)。Megatron-LM 开创此法,Llama 3 405B 节点内用 8 路张量并行。
按层切。GPU 1 跑 18 层,GPU 2 跑 916 层,依此类推。数据流过流水线:GPU 1 算完它的层把激活发给 GPU 2。好处:卡间通信极少——只有层边界的激活(比梯度或权重小得多),适合跨节点。坏处:流水线气泡——GPU 4 算微批 1 的前向时,GPU 1、2、3 在闲(已算完自己的部分)。朴素流水线 GPU 利用率只有 1/N。GPipe 和 PipeDream 把批切成微批解决:GPU 1 一算完微批 1 就开始微批 2,跨阶段重叠计算。M 个微批、N 阶段,气泡占比降到 (N-1)/M——16 微批 4 阶段,气泡 3/16 = 18.75%。
FSDP 结合数据并行的可扩展性与分片的省显存。每卡不持完整模型,而只持 1/N 的参数、梯度、优化器状态。某层前向前,FSDP 跑 all-gather 收集全卡完整参数到每卡内存,前向后丢弃非本地参数;反向时再 all-gather 重建参数算梯度;反向后 reduce-scatter 把梯度片分给各卡,每卡只存 1/N 梯度。
70B 模型在 8 卡上的账:
| 组件 | 无 FSDP | 有 FSDP |
|---|---|---|
| 权重(FP16) | 140 GB/卡 | 17.5 GB/卡 |
| Adam 状态(FP32) | 560 GB/卡 | 70 GB/卡 |
| 梯度(FP16) | 140 GB/卡 | 17.5 GB/卡 |
| 合计 | 840 GB/卡 | 105 GB/卡 |
无 FSDP 单张 80GB 卡装不下 70B;有 FSDP 8 卡每卡 105GB 仍超——至少要 16 卡才降到 80GB 以下,或配合激活检查点(反向时重算而非存储)。
DeepSpeed 的 ZeRO 在概念上与 FSDP 相同(Microsoft 独立开发),定义三阶段:ZeRO-1(只分片优化器状态,约 4 倍省显存)、ZeRO-2(+梯度,约 8 倍)、ZeRO-3(+参数,约 N 倍)。ZeRO-3 等价于 FSDP。DeepSpeed 还有 ZeRO-Offload(把优化器状态卸到更便宜更大的 CPU 内存)和 ZeRO-Infinity(卸到 NVMe SSD),用速度换容量。
现代训练同时用多种浮点格式:前向用 FP16/BF16(16 位,内存减半,tensor core 上矩阵乘快 2 倍);主权重用 FP32(优化器为数值精度维护);损失缩放(反向前把损失乘大常数防 FP16 梯度下溢,优化器步前再除回)。BF16 与 FP32 同指数范围(8 位指数)但精度低(7 位尾数 vs FP32 的 23),几乎不需损失缩放;FP16 指数 5 位尾数 10 位,能表示精细值但极端幅度会溢出/下溢。业界基本转向 BF16。
真实大规模训练三者并用:跨节点组数据并行(扩批)、节点内张量并行(8 卡切单层)、跨节点流水线并行(切层组)。Llama 3 405B 在 16,384 张 H100 上:节点内 8 路张量并行、跨节点 16 路流水线并行、剩余维度 128 路数据并行(16384/8/16=128)。这种 3D 分解(8×16×128=16384)就是扩展到数千卡的方法。DeepSeek V3 走另一条路:MoE 每 token 只激活 37B(总 671B),2048 张 H800——不到 Meta 的 1/8——花了 560 万美元 vs Meta 估的 1 亿。
import numpy as np def simulate_data_parallelism(data, num_gpus, model_fn): shard = len(data) // num_gpus gpu_losses, gpu_grads = [], [] for i in range(num_gpus): s = data[i*shard:(i+1)*shard] loss, grad = model_fn(s) gpu_losses.append(loss); gpu_grads.append(grad) return np.mean(gpu_losses), np.mean(gpu_grads, axis=0) # all-reduce 平均梯度
all-reduce(平均梯度)是数据并行唯一的通信,实践用 NVIDIA 的 NCCL 库做环形 all-reduce。
def simulate_tensor_parallelism(x, W, num_gpus): d_in, d_out = W.shape shard = d_out // num_gpus partials = [x @ W[:, i*shard:(i+1)*shard] for i in range(num_gpus)] full = np.concatenate(partials, axis=-1) # 列并行:拼接 return full, np.abs(full - x@W).max() # 误差应为 0
张量并行数学上精确——产出与单卡算完整矩阵乘相同。列并行(切输出维)用拼接,行并行(切输入维)用求和。Transformer FFN 第一线性(扩张)用列并行,第二(收缩)用行并行,避免两层间 all-reduce。
模拟层跨虚拟 GPU,展示气泡问题:4 阶段 1 微批,气泡 75%(4 卡中 3 卡闲);16 微批降到约 19%。消除气泡的代价是内存——要同时存所有在途微批的激活。
精确计算任意模型大小的显存需求——回答每个 ML 工程师都问的问题:「我要几张卡?」
def memory_calculator(params_b, precision_bytes=2, optimizer="adam", num_gpus=1, sharding="none"): p = params_b * 1e9 weights = p * precision_bytes optim = p * 4 * 2 if optimizer == "adam" else 0 # Adam 的 m 与 v,FP32 grads = p * precision_bytes if sharding in ("fsdp","zero3"): weights /= num_gpus; optim /= num_gpus; grads /= num_gpus elif sharding == "zero2": optim /= num_gpus; grads /= num_gpus elif sharding == "zero1": optim /= num_gpus per_gpu = weights + optim + grads return {"per_gpu_gb": per_gpu/1e9, "fits_80gb": per_gpu/1e9 <= 80}
喂入模型大小,调分片策略直到单卡总量降到 80GB 以下。
最大意外:混合精度不减半显存。7B 模型 FP32 训练用 112GB,混合精度(BF16+FP32 主权重)用 84GB——降 25% 而非 50%,因为优化器状态(Adam 的 m 和 v)无论精度都留 FP32,而优化器占大头。
PyTorch FSDP 是原生实现,DeepSpeed 是 Microsoft 的等价框架。配置上一行 --fsdp_strategy full_shard 即可。本节模拟器用纯 Python 让你理解机制——生产中你写的是配置文件,不是分布式原语。
本节产出 outputs/prompt-distributed-training-planner.md——一个提示,接收模型大小与可用硬件,产出完整分布式训练计划:并行策略、显存预算、通信开销、预期吞吐。
(Easy) 给显存计算器加激活检查点:每 K 层只存一次激活(K=1 即全重算),展示省多少显存、慢多少(约 33% 额外计算)。
(Medium) 扩展流水线模拟,实现 PipeDream 的 1F1B 调度,4 阶段 8 微批对比朴素调度的气泡占比——1F1B 峰值内存更小因为更早开始反向。
(Medium) 实现梯度累积模拟器:不在每个微批后 all-reduce,而是本地累积 K 步再 all-reduce,展示通信减 K 倍但最终梯度相同。
(Hard) 构建成本估算器:给定模型大小、目标 token 数、GPU 型号(A100 $2/时、H100 $3.5/时)、并行策略,估美元总成本,对照已知成本(Llama 3 405B 约 1 亿、DeepSeek V3 约 560 万)验证。
(Hard) 给显存计算器加 ZeRO-Offload:设 CPU 内存每节点 512GB、NVMe 2TB,展示把优化器状态卸到 CPU 让 70B 在 4 卡而非 16 卡上训练,代价是优化器步慢 30~50%。
下一节,我们把这个基座模型变成有用的助手——指令微调(SFT)是「token 预测器」与「能回答问题的助手」之间的桥梁。