对应原课:CMU 11-667 Fall 2025 · Lecture 13 · Tue 7-Oct-2025 · Scaling Up LLM Pretraining: Parallel Training(Jaromir Savelka、Yubin Kim) 原课 slides:Scaling Up LLM Pretraining: Parallel Training(课表上标为 scaling) 阅读材料:Kingma & Ba (2015), Adam: A Method for Stochastic Optimization;Huang et al. (2019), GPipe: Efficient Training of Giant Neural Networks using Pipeline Parallelism;Rajbhandari et al. (2020), ZeRO: Memory Optimizations Toward Training Trillion Parameter Models 配套代码:code/lecture13/(2 个模块 + 5 个演示脚本,全部约 1.5 分钟;所有”多卡”都在一个进程里模拟) 本讲的写法:骨架来自原课 slides(优化基础 → 数值格式 → 数据并行 → 流水线并行 → 张量并行 → 组合 → ZeRO → OPT 的训练日志)。每个机制都在 CPU 上真的实现一遍,并和单卡结果逐位比较。所有数字都来自配套代码的实际输出。本仓库的 CMU11-868(LLM 系统)对集合通信、分布式训练讲得更深,这一讲站在”训练 LLM 的人”的角度讲清楚每种方法解决什么问题、付出什么代价。
目录
- 0. 本讲导读
- 1. 优化:从 SGD 到 Adam
- 2. 数值格式与混合精度
- 3. 数据并行
- 4. 模型并行:张量并行与流水线并行
- 5. ZeRO:去掉数据并行里的冗余
- 6. 大规模训练的现实:OPT-175B 的日志
- 7. 小结、常见误区、练习
0. 本讲导读
0.1 这一讲要回答的问题
第 12 讲告诉我们”该训练多大的模型、用多少数据”。这一讲回答”怎样把它训出来”:
- 优化器为什么几乎总是 Adam(W)?它比 SGD 多做了什么、多存了什么?
- FP32、FP16、BF16 有什么区别?为什么要”混合精度”?
- 一块 GPU 算不完时,怎样把训练分到很多块 GPU 上?一块 GPU 放不下时呢?
- 数据并行、张量并行、流水线并行、ZeRO 各自切的是什么、要通信什么、代价是什么?
- 真正在一千张 GPU 上训练时会遇到什么?
0.2 学完这一讲你应该能
- 写出 SGD、动量、AdaGrad、Adam、AdamW 的更新公式,说明偏差修正和解耦权重衰减的作用;
- 比较 FP32 / FP16 / BF16 的范围和精度,解释损失放大和 FP32 主权重;
- 算出混合精度 + Adam 每个参数要多少字节,以及 ZeRO 各阶段每张卡的显存;
- 解释数据并行的 all-reduce、ring 算法的通信量;张量并行的切法;流水线并行的气泡 ;
- 说出大规模训练里的硬件故障、训练不稳定和检查点策略。
0.3 本讲的实验平台
- 模型:第 2 讲的因果 Transformer,d = 32、2 层,28,672 个参数;语料是第 1 讲的花园世界(每批 32 篇文档)。
- “多卡”:Python 列表里的 K 个模型副本。每张卡上的计算是真实的 PyTorch 计算;卡之间的通信是显式的函数(
all_reduce、reduce_scatter、all_gather),并统计每张卡发送的字节数(parallel.py)。 - 低精度:CPU 上的 PyTorch 不能直接用半精度训练这个模型,所以在 FP32 训练里,把真实系统会以低精度存储的量(层间梯度、参数梯度、权重)用 PyTorch 真实的 float16 / bfloat16 类型转换舍入一遍——下溢、上溢、舍入都是真的。
1. 优化:从 SGD 到 Adam
1.1 SGD 和它的两个问题
深度学习里,小批量训练是常态,随机梯度下降(SGD)是一切优化器的基础:
和经典的凸优化相比:每一步只用一小部分数据(随机采样),而且损失是非凸的,有很多效果不同的局部最优。slides 指出 SGD 的两个问题:
- **步长怎么选?**不同的参数有不同的范数、敏感度、对优化过程的影响,偏好不同的步长;没法为数十亿个参数逐个手调。
- 只用局部的信息:只看当前小批量,容易在局部震荡、被困住。
1.2 自适应步长:AdaGrad
AdaGrad(Duchi et al. 2011)用每个参数累积的历史梯度大小给它的步长重新加权:
- 被频繁、大幅更新的参数,步长越来越小;
- 稀疏的特征(很少有梯度),步长保持较大——对语言模型里罕见词的嵌入特别重要。
问题: 只增不减,步长最终衰减到 0。RMSProp 把累积换成指数滑动平均 。
1.3 动量
动量(momentum)用梯度的滑动平均代替当前梯度:
在一致的方向上越走越快,在来回震荡的方向上正负抵消。
1.4 Adam:两者合体
Adam(Kingma & Ba 2015,Adaptive Moment Estimation):
可以调的超参数:、(常取 0.9)、(0.999;LLM 常用 0.95)、。Adam 是今天深度学习的默认选择:它把两个有效的想法合在一起,在很宽的网络结构和任务上都好用,几乎所有常见的 LLM 都用 Adam 或它的变体预训练。
slides 列出的扩展:减少动量状态的显存(AdaFactor、8-bit Adam);更好的预热(RAdam);在动态学习率里加入更多信息(AdamSAGE 用敏感度、Sophia 近似二阶信息)。还有一个最重要的:AdamW(Loshchilov & Hutter 2019),见 §1.7。
1.5 实验:二维的碗和鞍点
代码 01 §1:(y 方向比 x 陡 50 倍)和鞍点 (从 出发)。每个优化器在学习率网格里取最好的:
| 优化器 | 碗: 的步数(学习率) | 鞍点: 的步数(学习率) |
|---|---|---|
| SGD | 186(0.03) | 22(0.3) |
| SGD + 动量 0.9 | 84(0.03) | 45(0.1) |
| AdaGrad | 33(1.0) | 5(1.0) |
| RMSProp | 7(0.3) | 5(0.1) |
| Adam | 61(0.3) | 17(1.0) |
- 碗:SGD 的学习率被陡的方向卡住(超过 2/50 就在 y 方向来回发散),平的方向只能慢慢挪;动量让平的方向越走越快;自适应方法按每个坐标自己的梯度大小缩放步长,陡的和平的方向走得一样快。
- 鞍点:y 方向的梯度极小。SGD 只能靠每步把 y 放大一个倍数、指数式地”滑下去”;AdaGrad、RMSProp 把极小的梯度除以它自己的大小,第一步就迈出正常大小的一步。
1.6 实验:训练花园语言模型
代码 01 §2 先验证:从零写的 5 种优化器和 torch.optim 在 16 步之后逐位相同(最大差异 0)。
第一版的 Adam 用 更新一阶矩,PyTorch 用
lerp,两者只差最后一位的舍入,16 步后参数却差到 6.6e-5——Adam 把梯度除以它自己的大小,对梯度几乎为 0 的参数会把舍入差放大,而训练是混沌的。(代码 README 第 1 条)
代码 01 §3:128 个批次 × 4 遍,验证集上真实分布的交叉熵是 1.296(任何模型都不可能更低)。
| 优化器 | 学习率: 验证损失 | |||
|---|---|---|---|---|
| SGD | 0.1: 1.874 | 0.3: 1.737 | 1: 1.581 | 3: 发散 |
| SGD + 动量 0.9 | 0.03: 1.631 | 0.1: 1.467 | 0.3: 1.522 | 1: 发散 |
| AdaGrad | 0.01: 1.531 | 0.03: 1.400 | 0.1: 1.566 | 0.3: 1.779 |
| RMSProp | 0.001: 1.451 | 0.003: 1.407 | 0.01: 1.576 | 0.03: 1.911 |
| Adam | 0.001: 1.509 | 0.003: 1.383 | 0.01: 1.417 | 0.03: 1.443 |
Adam 最好,而且在 30 倍的学习率范围内都不差;RMSProp、AdaGrad 调好时接近,但对学习率更敏感;SGD 要把学习率调得刚刚好(再大一档就发散),同样的步数下明显落后。
1.7 实验:Adam 的三个细节
偏差修正(代码 01 §4a)。、 从 0 开始,前几步的滑动平均被拉向 0。不修正时,实际步长相当于学习率乘上 :第 1 步 3.16 倍、第 10 步 6.53 倍、第 100 步 3.24 倍、第 1000 步 1.26 倍。训练一遍(128 步)的损失:
| 学习率 | 第 10 步:修正 / 不修正 | 第 128 步:修正 / 不修正 |
|---|---|---|
| 0.003 | 3.892 / 3.474 | 1.779 / 1.591 |
| 0.01 | 3.413 / 3.218 | 1.709 / 1.965 |
| 0.03 | 3.096 / 3.896 | 1.983 / 2.988 |
学习率小时,不修正等于白白把学习率放大几倍,前期反而降得快;学习率正常或偏大时,放大几倍的步长让训练不稳,最后明显更差。偏差修正让”设定的学习率就是实际的步长”,学习率才好调。
罕见词(代码 01 §4b):训练一遍后,嵌入向量离初始值的平均距离——
| 最常见的 10 个词(2,509~13,538 次) | 最罕见的 10 个词(85~127 次) | |
|---|---|---|
| SGD + 动量(lr 0.1) | 1.571 | 0.475 |
| Adam(lr 3e-3) | 0.572 | 0.599 |
SGD 的更新量和梯度成正比,罕见词很少有梯度,移动得少得多;Adam 除以 ,罕见词一旦有梯度就迈出正常大小的一步——AdaGrad 的那个好处。
AdamW vs Adam + L2(代码 01 §5):
| 验证损失 | 参数的范数 | |
|---|---|---|
| Adam(不衰减) | 1.383 | 28.79 |
| Adam + L2(λ = 0.1) | 3.734 | 3.36 |
| AdamW(λ = 0.1) | 1.409 | 26.46 |
L2 正则把 加进梯度,于是它也被 除了:梯度大的参数几乎不被衰减,梯度小的参数被衰减得很猛。AdamW 把衰减和梯度分开——每步先 ,再做 Adam 的更新——所有参数按同样的比例衰减。AdamW 是今天 LLM 预训练的标准选择(常见设置 、、)。
还要记住代价:Adam 为每个参数多存 、 两份状态——这是 §5 显存账的大头。
2. 数值格式与混合精度
2.1 三种格式
代码 02 §1:
| 格式 | 指数位 | 尾数位 | 最大值 | 最小正规数 | 最小次正规数 | 相对精度 ε |
|---|---|---|---|---|---|---|
| FP32 | 8 | 23 | 3.4e38 | 1.18e-38 | 1.4e-45 | 1.19e-7 |
| FP16 | 5 | 10 | 65504 | 6.1e-5 | 5.96e-8 | 9.77e-4 |
| BF16 | 8 | 7 | 3.39e38 | 1.18e-38 | 9.18e-41 | 7.81e-3 |
| 数 | FP32 | FP16 | BF16 |
|---|---|---|---|
| 1 + 0.001 | 1.001 | 1.00098 | 1 |
| 70000 | 70000 | inf | 70144 |
| 1e-9 | 1e-9 | 0 | 9.97e-10 |
FP16 精度高、范围小;BF16 把 FP32 的 8 位指数原样保留(范围一样大),尾数只剩 7 位。slides 的历史:TPU 很早(2019 年或更早)就支持 BF16;A100 之前 GPU 只有 FP32 和 FP16,BF16 大约 2021 年才在 GPU 上得到硬件支持。
神经网络更需要大的范围,而不是高的精度(Narang et al. 2018 的梯度直方图:很多计算需要比 FP16 更大的范围)。
2.2 混合精度训练
slides 的做法(Micikevicius / Narang et al. 2018):
- 参数、激活值、梯度用 FP16 存和算;
- 优化器状态需要 FP32,保留一份 FP32 的主权重用于更新;
- 动态放大损失,让梯度落进 FP16 的范围。
代码 02 §2:训练 100 步后取一个批次(1141 个 token),统计层与层之间传递的梯度。损失是整批 token 的平均,每个 token 分到的梯度 ∝ 1/批里的 token 数——真实 LLM 一批有几百万个 token,是这里的几千倍:
| 中位数 | < 6e-8(FP16 下溢为 0) | < 6.1e-5(FP16 失去精度) | |
|---|---|---|---|
| 本脚本的批次 | 2.46e-5 | 1.1% | 83.2% |
| 批次大 4096 倍 | 6.01e-9 | 100.0% | 100.0% |
把损失乘一个大数 再反向传播,所有梯度都放大 倍、挪进 FP16 的范围;更新前再除以 。 时,大批次的梯度只有 12.4% 落在正规数以下。 太大会上溢,所以实践中用动态损失放大:出现 inf 就把 减半、跳过这一步,长时间没出现就加倍。
训练 400 步(模拟批次大 4096 倍时的梯度量级):
| 设置 | 验证损失 | 参数梯度为 0 的比例 |
|---|---|---|
| FP32 | 1.405 | 0.0% |
| FP16 梯度,不放大 | 1.988 | 55.4% |
| FP16 梯度,损失 × 4096 | 1.405 | 0.2% |
| BF16 梯度,不放大 | 1.405 | 0.0% |
在脚本自己的小批次下,FP16 不放大也完全不影响训练——每个 token 分到的梯度还没小到下溢。梯度会不会下溢,取决于批次大小、网络深度、损失的归一化方式,这也是要动态调 的原因。(代码 README 第 4 条)
BF16 的范围和 FP32 一样,不放大也没有下溢——slides:“BF16 是 A100、H100 上的首选:消除了混合精度训练的那些麻烦,又稳定得多;精度粗,大多数地方够用,只有少数地方需要更细的精度。”
2.3 为什么还要 FP32 的主权重
代码 02 §3:权重直接存成 BF16、在 BF16 上更新:
| 设置 | 验证损失 | 被舍入吃掉的更新 |
|---|---|---|
| FP32 权重(lr 3e-3 / 3e-4) | 1.405 / 2.366 | — |
| BF16 权重,直接更新(lr 3e-3) | 1.443 | 33.1% |
| BF16 权重,直接更新(lr 3e-4) | 3.077 | 69.2% |
| BF16 权重 + FP32 主权重(lr 3e-3 / 3e-4) | 1.404 / 2.366 | — |
一次更新只有权重的千分之一左右时,BF16(相对精度约 0.4%)表示不出 和 的区别,这一步就白做了;学习率越小(例如余弦调度的末尾),被吃掉的越多。FP32 主权重让小的更新慢慢累积。
于是每个参数的”模型状态”:2(BF16 权重)+ 2(BF16 梯度)+ 4(FP32 主权重)+ 4 + 4(FP32 的 、)= 16 字节。
3. 数据并行
3.1 为什么要并行
规模变大后,一块 GPU 不够用了。单卡上有很多提效的办法——把部分数据挪到 CPU 内存(offload)、重算激活值、把训练的不同部分量化——但最终需要更多的 FLOPs,也就是更多的卡。
3.2 做法
slides:把训练批次切到不同的 GPU 上——
- 每张卡维护自己的一份模型和优化器;
- 每张卡拿到不同的局部批次,算出局部梯度;
- 把局部梯度汇总到每张卡(all-reduce),做全局更新。
代码 03 §1:一个批次 32 篇文档、275 个 token。
| 卡数 K | 每张卡的文档数 | 和单卡梯度的最大差异 | 每张卡发送的字节 |
|---|---|---|---|
| 1 | 32 | 1.49e-7 | 0 |
| 2 | 16 | 1.19e-7 | 114,688 |
| 4 | 8 | 1.79e-7 | 172,032 |
| 8 | 4 | 1.79e-7 | 200,704 |
差异只在浮点舍入的量级。一个细节:按 token 数加权——每张卡先算”自己那份的 NLL 之和”的梯度,汇总后除以整批的 token 数;如果每张卡先求平均、再平均 K 个平均数,卡之间 token 数不同时就不对了。
代码 03 §2:4 张卡各自用汇总后的梯度更新,训练 60 步。4 张卡之间的参数差异始终是 0——它们收到同一个梯度、用同一个公式更新,永远不会走散;损失曲线和单卡训练完全一样。这就是 PyTorch DDP 的做法。
3.3 all-reduce 与通信
ring all-reduce(代码 03 §3 逐步模拟):K 张卡连成一个环,向量切成 K 段。前 K−1 步(reduce-scatter)每张卡把一段发给右边的邻居、对方加到自己那段上;结束时每张卡各拥有一段的完整和。后 K−1 步(all-gather)把完整的段沿环传一圈。每张卡共发送 × 向量大小——几乎和 K 无关,所有链路同时在用。
slides:“每个批次,每对 GPU 之间都要交换完整的梯度张量。同一台机器里的 GPU、或者机器之间有 InfiniBand 时不成问题;没有快速的跨卡连接时需要变通。“代码 03 §4:70 亿参数、BF16 梯度 14 GB,每步每张卡的通信时间:
| 连接 | K = 8 | K = 64 | K = 512 |
|---|---|---|---|
| NVLink(约 300 GB/s) | 0.08 s | 0.09 s | 0.09 s |
| InfiniBand(约 50 GB/s) | 0.49 s | 0.55 s | 0.56 s |
| 以太网 10 Gb/s(约 1.25 GB/s) | 19.60 s | 22.05 s | 22.36 s |
前两种可以和反向传播重叠(边算边传);只有普通以太网时,每一步要等二十秒——变通办法有梯度累积(攒几个批次再同步)、梯度压缩,或者换成通信少的并行方式。
数据并行的根本限制:每张卡都存了一整份模型、梯度和优化器状态。模型大到一张卡放不下时,它就无能为力了。
4. 模型并行:张量并行与流水线并行
4.1 一张卡放不下
slides 的例子:100 亿参数,全部用 BF16(理想情况):参数 2Ψ、梯度 2Ψ、Adam 的一阶矩 2Ψ、二阶矩 2Ψ,合计 8Ψ = 80 GB——一张 A100 / H100 的全部显存,还没算激活值。实际的混合精度(§2.3)是 16Ψ = 160 GB。
解决办法:把网络参数(连同它们的梯度和优化器状态)切到不同的 GPU 上。两种切法:按层切(流水线并行),切开张量(张量并行)。
4.2 张量并行
把一层里的参数矩阵切到不同的卡上,并行地做矩阵运算(Megatron-LM,Shoeybi et al. 2019)。MLP 的切法:——
- 按输出维度(列)切:每张卡算出隐藏层的一部分,GeLU 逐元素作用,各卡互不依赖;
- 按输入维度(行)切:每张卡得到输出的一个部分和;
- 最后 all-reduce 一次把部分和加起来。
代码 04 §2:MLP 64 → 256 → 64 切到 k = 2、4、8 张卡,和单卡输出的最大差异约 1e-6(浮点舍入),每张卡只存 1/k 的参数,每个 MLP 前向只要一次 all-reduce。反过来先按行切行吗?那样 GeLU 之前就要先把部分和加起来——GeLU(a + b) ≠ GeLU(a) + GeLU(b)(实测差 0.23),必须多通信一次。
注意力按头切(代码 04 §3):每个头本来就独立,、、 按输出维度切、 按输入维度切,最后一次 all-reduce,和单卡的差异约 8e-7。
slides 总结:优点是没有气泡;缺点是不同的块要用不同的切法,需要大量定制,而且每层都要 all-gather / all-reduce 激活值和梯度——通信非常频繁,一般只在同一台机器的 8 张卡之内用。
4.3 流水线并行
按层切:各张卡按层的顺序排成一条流水线,数据依次流过(GPipe,Huang et al. 2019)。为了让各张卡同时有活干,把批次切成微批次(micro-batch)。通信只在相邻的卡之间:前向传激活值,反向传部分梯度。
代码 04 §4:4 层的模型分成 2 或 4 段、批次切成 1~8 个微批次,梯度和整批一次算完的最大差异约 1e-7。
气泡:GPipe 的时间表(代码 04 §5,4 个阶段、4 个微批次,前向 1 个单位、反向 2 个):
卡 0 |F0F1F2F3 B0B0B1B1B2B2B3B3|
卡 1 | F0F1F2F3 B0B0B1B1B2B2B3B3 |
卡 2 | F0F1F2F3 B0B0B1B1B2B2B3B3 |
卡 3 | F0F1F2F3B0B0B1B1B2B2B3B3 |
空白就是气泡:前向刚开始时后面的卡在等数据,反向刚开始时前面的卡在等梯度。空闲比例 = :
| 阶段数 p | m = 1 | m = 4 | m = 16 | m = 64 |
|---|---|---|---|---|
| 2 | 50.0% | 20.0% | 5.9% | 1.5% |
| 4 | 75.0% | 42.9% | 15.8% | 4.5% |
| 8 | 87.5% | 63.6% | 30.4% | 9.9% |
slides 总结:优点是概念简单、和网络结构无关(所有网络都有很多层);缺点是气泡浪费算力,卡越多气泡越大。微批次越多气泡越小,但批次固定时切得太碎,每个微批次太小、算得没效率。另外 GPipe 要存下所有微批次的激活值直到反向;1F1B 调度(PipeDream)让每张卡做完一个前向就尽快做对应的反向,最多只存 p 份。
4.4 组合起来:3D 并行
代码 04 §6:以 32 层、隐藏维度 4096、每步 8 × 2048 个 token(BF16)为例——张量并行每层前向、反向各 2 次 all-reduce,每次 134 MB,每步约 17.2 GB,而且每层都要等它完成;流水线并行只在阶段边界每步传 268 MB;数据并行每步 all-reduce 一次全部梯度。
所以 slides 说”数据并行和模型并行通常一起用——没有理由不用数据并行”;流水线并行和张量并行也可以一起用(Narayanan et al. 2021):张量并行放在一台机器之内(NVLink 快),流水线并行跨机器,外面再套一层数据并行。
5. ZeRO:去掉数据并行里的冗余
5.1 冗余在哪里
GPU 显存的大头在优化器那一侧:梯度和动量。数据并行里每张卡都存一整份——代码 05 §1,ZeRO 论文的例子(Ψ = 7.5B,K = 64,每个参数 16 字节):
| 参数 | 梯度 | 优化器状态 | 每张卡合计 | 每步通信 | |
|---|---|---|---|---|---|
| 普通数据并行 | 15.0 G | 15.0 G | 90.0 G | 120.0 G | 30 G |
| ZeRO-1(切优化器状态) | 15.0 G | 15.0 G | 1.4 G | 31.4 G | 30 G |
| ZeRO-2(再切梯度) | 15.0 G | 0.2 G | 1.4 G | 16.6 G | 30 G |
| ZeRO-3(再切参数) | 0.2 G | 0.2 G | 1.4 G | 1.9 G | 45 G |
5.2 为什么阶段 1、2 是”搭便车”
数据并行本来就要 all-reduce 梯度,而 all-reduce = reduce-scatter + all-gather。ZeRO-1/2 把它拆开用:reduce-scatter 之后每张卡只拿到自己那一段的梯度和,只为这一段保存 Adam 状态、只更新这一段参数;再 all-gather 参数。通信量不变。
代码 05 §2:4 张卡、40 步,真的跑一遍 ZeRO-1。和普通数据并行的参数最大差异 0,各卡之间也是 0;每张卡的 Adam 状态从 229,376 字节降到 57,344 字节(1/4);每张卡发送的字节都是 6,881,280。
5.3 阶段 3
再把参数也切开:每张卡只常驻 1/K 的参数,用到某一层时临时 all-gather,用完就扔。前向、反向各要收集一次,通信量多 50%。slides 特别提醒:阶段 3 和张量并行不同——它在需要时传参数,但每张卡仍然完整地计算整个层,本质上是”带显存切分的数据并行”(PyTorch FSDP 就是这个思路)。优点:阶段 1、2 免费省下大量显存;缺点(2025 年 slides 的看法):开源支持不如数据并行成熟。
5.4 激活值与重算
模型状态之外,反向传播还要用前向存下的激活值。按 Korthikanti et al. (2022),每层约 字节(BF16,不用 FlashAttention)。代码 05 §3:32 层、h = 4096、32 个头、8 × 2048 个 token,每层 7.7 GB、共 245 GB。重算(activation checkpointing)每层只存输入(0.13 GB),反向时重新算一遍这一层的前向:显存降到约 12 GB,代价是多做一次前向(总计算量约增加 1/3)。前置讲 P3 有完整的显存计算器。
6. 大规模训练的现实:OPT-175B 的日志
Meta 公开了训练 OPT-175B 的日志(metaseq 的 chronicles),slides 讲了三个挑战。背景:2021 年 11 月初接到任务,要在年底前用 1024 张 80 GB 的 A100 训完一个 1750 亿参数的模型(至少要 33 天),团队之前没做过这么大的预训练。
- 很多研究改进不能扩展:开始时希望把小规模上的改进都用上;现实是时间紧、钱多,最后”什么花哨的都不敢用”。
- 硬件故障:GPU 机器并不可靠,1024 张卡上坏节点是必然的。解决办法?“希望将来有更好的工具,但现在——组织一个值班小组盯着训练。”
- 优化不稳定:损失爆炸、梯度上溢 / 下溢、训练停滞……反复回滚到检查点、降低学习率。
代码 05 §4 把第二条量化:假设每张卡平均 5 万小时坏一次——8 张卡的集群平均 6250 小时坏一张,1024 张卡 48.8 小时,16384 张卡 3.1 小时。一张卡坏了,整个训练从上一个检查点重启。存一次检查点 10 分钟时,按 Young 的公式,最优存档间隔 ≈ 分钟:
| 存档间隔 | 存档占用的时间 | 故障后平均重做 | 合计浪费 |
|---|---|---|---|
| 30 分钟 | 33.3% | 0.5% | 33.8% |
| 242 分钟 | 4.1% | 4.1% | 8.3% |
| 600 分钟 | 1.7% | 10.2% | 11.9% |
slides 的结论:规模定律的重要性——在大规模上做什么,必须由小规模上的观察来决定(第 12 讲)。slides 最后列出的其他相关文献:层归一化的位置(pre-LN / post-LN,Xiong et al. 2020;RMSNorm)、更长上下文的位置编码(RoPE)、自适应初始化带来的稳定性(Liu et al. 2020)——第 4 讲都讲过。
7. 小结、常见误区、练习
7.1 小结
- 优化:动量解决震荡、自适应步长解决”不同参数需要不同步长”,Adam 两者兼有、对学习率最不敏感;偏差修正让设定的学习率就是实际步长;AdamW 把权重衰减和梯度分开。从零实现和
torch.optim逐位相同。 - 数值格式:FP16 精度高、范围小,梯度会下溢(大批次时尤其严重),要损失放大;BF16 范围和 FP32 一样;低精度权重吃不下小更新,要 FP32 主权重。混合精度 + Adam = 每参数 16 字节。
- 数据并行:每卡一份模型,all-reduce 梯度,结果和单卡一样;ring 算法的通信量约为 2 倍梯度、几乎不随卡数增长;瓶颈是互联带宽和”每张卡一整份模型状态”。
- 模型并行:张量并行切矩阵(MLP 先列后行、注意力按头),没有气泡但每层通信;流水线并行按层切,气泡 ;实践中组合成 3D 并行。
- ZeRO:切优化器状态(阶段 1)、梯度(阶段 2)免费省显存;切参数(阶段 3)多 50% 通信。重算用计算换激活值的显存。
- 现实:硬件故障和训练不稳定是常态,检查点间隔有最优值;大规模上的决定依赖小规模上的规模定律。
7.2 常见误区
- “自己写的优化器和库函数差一点点没关系”:最后一位的舍入差会被训练的混沌放大,复现性要求逐位一致(§1.6)。
- “偏差修正只是个小细节”:不修正时前几百步的实际步长是设定值的 3~6.5 倍,学习率稍大就训坏(§1.7)。
- “L2 正则和权重衰减是一回事”:对 SGD 是,对 Adam 不是(§1.7)。
- “FP16 和 BF16 都是 16 位,差不多”:一个范围小精度高,一个范围大精度低;FP16 需要损失放大,BF16 通常不需要(§2)。
- “用 BF16 训练就是所有东西都存成 BF16”:权重的更新要在 FP32 主权重上累积(§2.3)。
- “数据并行能训练任意大的模型”:每张卡都要放下整份模型状态(§3.3)。
- “张量并行和 ZeRO-3 都是切参数,所以一样”:张量并行把计算也切开;ZeRO-3 只切存储,每张卡仍然算完整的层(§5.3)。
- “微批次越多越好”:气泡变小,但每个微批次太小会降低计算效率(§4.3)。
7.3 练习
练习 1(β2 = 0.95 时的偏差修正):LLM 常用 、。算出不做偏差修正时,第 1、2、5、10、100 步的实际步长是设定值的多少倍,和 比较。
参考答案
倍数 = :第 1 步 0.447、第 2 步 0.608、第 5 步 0.861、第 10 步 1.028、第 100 步 1.003。 时是 3.16、4.25、5.80、6.53、3.24。 小时二阶矩的偏差几步就消失了,偏差主要来自一阶矩,不修正的版本前几步反而更小——相当于自带一小段预热; 接近 1 时二阶矩的偏差持续几千步,不修正的步长被放大好几倍。无论哪种,修正后步长都等于设定值,行为可预期。
练习 2(70B 模型的显存):一个 700 亿参数的模型,混合精度 + Adam。(a) 模型状态一共多少?(b) 用 ZeRO-3 切到 64 张卡,每张卡多少?(c) 只考虑模型状态,ZeRO-3 最少要几张 80 GB 的卡?(d) 为什么实际需要的卡远多于 (c)?
参考答案(用 memory_per_device 验证)
(a) 16 × 70e9 = 1120 GB。(b) 1120 / 64 = 17.5 GB。(c) 1120 / 80 = 14 张(放满不留余地,实际要更多)。(d) 还要放激活值(§5.4,几十到几百 GB,取决于批次和序列长度,重算能大幅减少)、ZeRO-3 临时收集的整层参数、通信缓冲区、内存碎片;而且算力也要够——按第 12 讲 Chinchilla 的配比,70B 要训练约 1.4 万亿 token(6ND ≈ 5.9e23 FLOPs),14 张卡要算好几年,所以实际用几千张卡,瓶颈是算力而不是显存。
练习 3(气泡):流水线有 8 个阶段,要让空闲比例不超过 10%,至少要几个微批次?4 个阶段呢?
参考答案(用 gpipe_schedule 验证)
:p = 8 时 m ≥ 63,p = 4 时 m ≥ 27(模拟的时间表给出完全相同的结果)。如果全局批次是 512 条序列,p = 8 时每个微批次只有约 8 条——再多就太小、算得没效率了。这就是为什么流水线的阶段数一般不会太多,或者要用 1F1B、交错调度(interleaved)等方法减少气泡。
练习 4(ring vs 参数服务器):8 张卡、10 亿参数、BF16 梯度。(a) ring all-reduce 每张卡发送多少?(b) 如果改成”所有卡把梯度发给 0 号卡,0 号卡加起来再发回去”(参数服务器),0 号卡要收、发多少?(c) 带宽 50 GB/s 时两者各要多久?
参考答案
梯度 2 GB。(a) GB,每张卡同时收发,约 0.07 s。(b) 0 号卡收 7 × 2 = 14 GB、发 14 GB,共 28 GB——全部挤在一张卡的链路上,约 0.56 s,而且随卡数线性增长。ring 把通信平摊到所有链路上,每张卡的量几乎不随 K 增长(代码 03 §1 的最后一列)。
练习 5(BF16 能精确表示的整数):BF16 有 7 位尾数。(a) 从 0 开始,它能精确表示的连续整数到多少为止?FP16 呢?(b) 在 BF16 里 1 + 2^-8 等于几?1 + 2^-7 呢?(c) 这对”用 BF16 累加一个很长的求和”意味着什么?
参考答案(用 torch 验证)
(a) 有效位数 = 尾数位 + 1:BF16 是 8 位,能精确表示到 ,257 会被舍入成 256(258 可以表示);FP16 是 11 位,到 ,2049 变成 2048。(b) 1 + 2^-8 恰好在 1 和下一个可表示的数 1 + 2^-7 = 1.0078125 中间,按”舍入到偶数”变成 1;1 + 2^-7 能精确表示。(c) 累加到 256 之后,再加 1 就加不上了——长求和(例如一个大矩阵乘法的内积、一个批次的损失之和)必须用 FP32 累加器。这就是为什么张量核心”BF16 输入、FP32 累加”,以及为什么优化器状态和主权重用 FP32。
练习 6(张量并行的通信次数):一个 Transformer 层(注意力 + MLP)用 Megatron 的方式做张量并行,k = 8。前向、反向各要几次 all-reduce?每次的数据量和什么成正比?为什么张量并行很少跨机器?
参考答案
前向:注意力输出一次、MLP 输出一次,共 2 次;反向:对应地在两个块的输入处各一次,共 2 次。每次 all-reduce 的是激活值(或它的梯度),大小 = 批次 × 序列长度 × 隐藏维度,和参数量无关、和批次成正比。每层都要通信,而且下一步的计算必须等它完成,很难和计算重叠;跨机器的带宽(InfiniBand ~50 GB/s)比机器内的 NVLink(~300 GB/s 以上)小得多,所以张量并行一般限制在一台机器的 8 张卡之内(代码 04 §6 算过:这个例子里每步 17 GB)。
练习 7(梯度累积):只有 1 张卡,但想用”32 张卡 × 每卡 8 条”那么大的批次。怎样用梯度累积做到?要注意什么?它和数据并行有什么相同、有什么不同?
参考答案
每次只前向 + 反向 8 条,梯度累加起来(不清零、不更新),做满 32 次后才 optimizer.step()。注意:(1) 损失的归一化要用整个大批次的 token 数(和 pipeline_grad 一样),不能每个小批次各自平均再简单相加;(2) BatchNorm 这类依赖批次统计量的层会不一样(Transformer 用 LayerNorm,不受影响);(3) 学习率调度按”大批次的步数”计。相同:数学上等价于一次算完整个大批次,也等价于 32 卡的数据并行。不同:时间是 32 倍(串行),显存只要一张卡放下 8 条;数据并行是用 32 张卡同时算。实践中两者常一起用:数据并行 × 梯度累积 = 全局批次。
练习 8(思考:ZeRO-3 和张量并行):slides 说”ZeRO-3 和张量并行不同”。从”每张卡算什么""通信什么""通信和计算的关系”三方面比较,并说出各自适合的场景。
参考答案
算什么:张量并行里,每张卡只算每层的 1/k(一部分头、一部分隐藏单元);ZeRO-3 里,每张卡算完整的层,只是对不同的数据(它本质是数据并行)。通信什么:张量并行传激活值(和批次大小成正比);ZeRO-3 传参数(和模型大小成正比,前向、反向各 all-gather 一次)和梯度(reduce-scatter)。和计算的关系:张量并行的通信在每层的关键路径上,必须等;ZeRO-3 可以预取下一层的参数,和当前层的计算重叠。场景:张量并行适合单层太大、或者需要降低每张卡的计算量和激活值的情况,限制在高速互联之内;ZeRO-3 / FSDP 适合”模型放不下、但每张卡算一整层没问题”的情况,可以跨机器扩展,实现上也更通用(不需要为每种层定制切法)。
练习 9(思考:检查点的间隔):推导 Young 的近似:存一次检查点要 δ,平均每 M 时间坏一次,每隔 τ 存一次。浪费的时间比例约为 δ/τ + τ/(2M),求最优 τ。如果把存档改成异步(训练不停,后台写盘,δ 降到原来的 1/10),最优间隔和浪费各变成多少?
参考答案
对 τ 求导:,代回去浪费 。代码 05 的例子 δ = 10 分钟、M = 2930 分钟:τ* ≈ 242 分钟,浪费约 8.3%。δ 降到 1 分钟:τ* = 分钟,浪费 ——检查点越便宜,就能存得越勤、浪费越少,所以大规模训练都在优化存档速度(异步写、分片写、先写到其他节点的内存)。卡越多,M 越小,这件事越重要:16384 张卡时 M ≈ 3 小时,δ = 10 分钟的浪费约 33%。
7.4 延伸阅读
- 原课阅读:Kingma & Ba (2015), Adam;Huang et al. (2019), GPipe;Rajbhandari et al. (2020), ZeRO
- 优化器:Duchi et al. (2011), AdaGrad;Ruder (2017), An overview of gradient descent optimization algorithms(slides 的图来源);Loshchilov & Hutter (2019), Decoupled Weight Decay Regularization(AdamW);Shazeer & Stern (2018), Adafactor;Dettmers et al. (2022), 8-bit Optimizers;Liu et al. (2020), RAdam;Liu et al. (2023), Sophia
- 数值格式:Micikevicius et al. (2018), Mixed Precision Training;Kalamkar et al. (2019), A Study of BFLOAT16 for Deep Learning Training
- 并行:Shoeybi et al. (2019), Megatron-LM;Narayanan et al. (2021), Efficient Large-Scale Language Model Training on GPU Clusters Using Megatron-LM;Narayanan et al. (2019), PipeDream;Zhao et al. (2023), PyTorch FSDP;Korthikanti et al. (2022), Reducing Activation Recomputation in Large Transformer Models
- 实践:Zhang et al. (2022), OPT 与 metaseq 的 chronicles 日志;Young (1974), A first order approximation to the optimum checkpoint interval
- 本仓库的相关内容:前置讲 P3(显存与 FLOPs 计算器);第 4 讲(归一化、RoPE);第 12 讲(规模定律);CMU11-868 第 14、15 讲(集合通信、数据并行的系统实现)
7.5 下一讲预告
前两讲把 LLM 当作一个要”做大、训出来”的黑箱。第 14 讲打开这个黑箱:可解释性。模型的注意力头在看什么?它的表示里存了哪些语言学知识,在哪一层?为什么预训练过的模型在微调时泛化得更好——损失地形的”平坦”能解释吗?上下文学习是不是一种隐式的梯度下降?我们会在花园世界的模型上做注意力分析、逐层探针、损失地形可视化,并亲手构造一个”用梯度下降做上下文学习”的线性注意力层。