一、混合精度训练:从原理到实战
混合精度训练,说白了就是让模型在训练时同时用 FP16 和 FP32 两种精度。我刚开始接触这个概念时也觉得奇怪——好好的 FP32 不用,为什么要折腾半精度?后来在训练一个 7B 参数的模型时,显存直接爆了,才意识到这玩意儿有多重要。
1.1 FP16 和 BF16 的原理差异
先说说 FP16。它用 1 位符号位、5 位指数位、10 位尾数位。表示范围大概是 ±65504,精度大约 3 位十进制有效数字。你想想看,这个精度对于梯度更新来说其实挺紧张的。
BF16 呢?Google 搞出来的东西。1 位符号位、8 位指数位、7 位尾数位。表示范围和 FP32 一样大(±3.4×10³⁸),但精度更差,只有 2 位有效数字。
| 特性 | FP16 | BF16 | FP32 |
|---|---|---|---|
| 指数位 | 5 位 | 8 位 | 8 位 |
| 尾数位 | 10 位 | 7 位 | 23 位 |
| 表示范围 | ±65504 | ±3.4×10³⁸ | ±3.4×10³⁸ |
| 精度 | ~3 位有效数字 | ~2 位有效数字 | ~7 位有效数字 |
我个人习惯用 BF16 做训练。为什么?因为它的指数范围和 FP32 一样,不容易出现梯度下溢。我在项目中遇到过用 FP16 训练 BERT-large,梯度直接变成 0,模型死活不收敛。换成 BF16 就好了。
1.2 AMP 的核心机制
AMP 不是简单地把所有东西都转成半精度。它有三个关键设计:
Loss Scaling(损失缩放)
FP16 的表示范围太小了。梯度值经常小于 2⁻²⁴,直接变成 0。解决办法?把 loss 放大 2⁸ 到 2¹⁶ 倍,梯度也跟着放大,就不会下溢了。反向传播完再把梯度缩回来。
关键点:Loss Scaling 的缩放因子不是固定的。我一般设初始值 2¹⁶,如果连续 2000 步没出现梯度溢出,就翻倍;如果溢出了,就减半。PyTorch 的 GradScaler 会自动做这个。
Master Weights(主权重)
权重更新时,如果用 FP16 累加,误差会累积。所以 AMP 的做法是:
- 前向传播用 FP16(省显存、加速)
- 反向传播用 FP16(梯度也是半精度)
- 权重更新用 FP32(保持精度)
说白了就是保留一份 FP32 的权重副本,每次更新完再转成 FP16 用于下一次前向。多占一点显存,但换来训练稳定性。
注意:Master Weights 会增加约 50% 的参数量显存。但相比整个模型显存,这点开销完全值得。我曾经试过不用 Master Weights,训练到一半 loss 直接飞了。
1.3 PyTorch AMP 实战
PyTorch 从 1.6 开始原生支持 AMP。用法很简单:
import torch
from torch.cuda.amp import autocast, GradScaler
model = MyModel().cuda()
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4)
scaler = GradScaler() # 自动管理 loss scaling
for data, target in dataloader:
optimizer.zero_grad()
# 前向传播用 autocast 包裹
with autocast():
output = model(data)
loss = criterion(output, target)
# 反向传播用 scaler 处理
scaler.scale(loss).backward()
scaler.step(optimizer)
scaler.update()
嗯,这里要注意几个坑:
- 不要手动调用 loss.backward(),要用 scaler.scale(loss).backward()
- 梯度裁剪要在 scaler.unscale_() 之后做,否则梯度还是缩放后的值
- 某些操作不支持 FP16,比如 LayerNorm、Softmax,autocast 会自动回退到 FP32
我的经验:如果你用分布式训练,每个 GPU 都要有自己的 scaler。我曾经踩过坑,多个 GPU 共用一个 scaler,结果 loss scaling 乱套了。
1.4 混合精度对显存的影响
直接说结论:AMP 能省大约 40%-50% 的显存。为什么不是 50%?因为 Master Weights 和某些必须用 FP32 的操作会抵消一部分收益。
我拿一个 1.5B 参数的模型做过测试:
| 配置 | 显存占用 | 训练速度 |
|---|---|---|
| 纯 FP32 | 24.3 GB | 1.0x |
| AMP (FP16) | 14.1 GB | 1.8x |
| AMP (BF16) | 13.8 GB | 1.9x |
你看,显存省了将近一半,速度还快了近一倍。这就是为什么现在大模型训练几乎必用 AMP。
但要注意,显存节省主要来自:
- 激活值:前向传播时保存的中间结果,从 FP32 变成 FP16,省一半
- 梯度:反向传播时也是半精度
- 优化器状态:Adam 的动量、方差等,如果用 FP16 存储也能省
不过,权重本身因为要保留 FP32 的 Master Weights,省得不多。我建议你重点关注激活值的优化,那才是显存大头。
避坑指南:我曾经在训练一个视觉 Transformer 时,发现 AMP 反而更慢了。排查了半天,发现是数据加载成了瓶颈——GPU 算得太快,CPU 喂数据跟不上。所以 AMP 加速的前提是 IO 不是瓶颈。
1.5 知识体系图
下面这张图总结了 AMP 的核心逻辑:
这张图把 AMP 的完整流程串起来了。你仔细看,从输入到前向、反向、更新,每一步的精度选择都是有讲究的。说白了,AMP 就是在精度和效率之间找平衡。
最后说一句:AMP 不是银弹。如果你的模型很小(比如小于 100M 参数),纯 FP32 可能更简单。但一旦模型超过 500M,AMP 就是必选项。我个人的经验是:能用 BF16 就别用 FP16,能开 AMP 就别裸跑。