第4章:混合精度训练(AMP):FP16与FP32原理
显存优化这件事,说白了就是跟精度做交易。你想想看,FP32占4个字节,FP16只占2个字节,直接砍半。但问题来了——精度降了,模型还能收敛吗?
我个人习惯把混合精度训练比作「开车过弯」。FP32是四平八稳的SUV,FP16是轻量跑车。跑直线时跑车更快,但过急弯容易翻车。混合精度就是让跑车在直道飙,过弯时自动减速——嗯,这就是AMP干的事。
4.1 FP16与FP32:精度与范围的博弈
先看一组数据,我当年第一次看到时也挺惊讶的:
| 精度类型 | 位宽 | 指数位 | 尾数位 | 最大值 | 最小值 |
|---|---|---|---|---|---|
| FP32 | 32 bit | 8 bit | 23 bit | ~3.4e38 | ~1.4e-45 |
| FP16 | 16 bit | 5 bit | 10 bit | ~65504 | ~6.0e-8 |
看到问题了吗?FP16的最大值才65504。这意味着什么?我举个例子——你在做Transformer训练时,注意力分数稍微大一点,直接溢出变成inf。我在项目中遇到过这种情况,当时排查了半天,最后发现是softmax前的数值超过了65504。
4.2 AMP自动混合精度:让框架替你操心
AMP的核心思想很简单:该用FP16的地方用FP16,该保留FP32的地方保留FP32。具体来说:
- 计算密集型操作(卷积、矩阵乘法)→ FP16
- 精度敏感操作(BatchNorm、LayerNorm、softmax)→ FP32
- 权重更新(优化器步骤)→ FP32
PyTorch的AMP实现起来就几行代码:
import torch
from torch.cuda.amp import autocast, GradScaler
model = MyModel().cuda()
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
scaler = GradScaler() # 关键组件
for data, target in dataloader:
optimizer.zero_grad()
# 自动混合精度上下文
with autocast():
output = model(data)
loss = loss_fn(output, target)
# 用scaler缩放梯度
scaler.scale(loss).backward()
scaler.step(optimizer)
scaler.update()
你看,就多了三行代码:autocast、scaler.scale、scaler.update。但效果立竿见影。
with autocast():,否则推理速度会慢一截。
4.3 GradScaler:为什么需要它?
你可能会问:既然用了FP16,为什么还要缩放梯度?
原因很简单——FP16的表示范围太窄了。梯度在反向传播时,很多值会小于FP16的最小表示范围(6.0e-8),直接变成0。这就是所谓的「梯度下溢」问题。
GradScaler的做法是:
- 前向传播时,loss乘以一个缩放因子(默认2^16=65536)
- 反向传播时,梯度也相应放大
- 更新权重前,再把梯度缩回来
这样,原本会下溢的梯度就被「抬」到了FP16的可表示范围内。
核心机制:
- 初始缩放因子:2^16
- 如果出现inf/NaN → 缩放因子减半,跳过当前batch
- 连续N个batch无异常 → 缩放因子加倍(N默认2000)
我记得有一次训练GAN,生成器和判别器交替更新,梯度波动特别大。如果不加GradScaler,生成器的梯度几乎全为0,模型根本学不动。加上之后,训练曲线立马正常了。
4.4 显存节省效果分析
直接上数据,这是我用ResNet-50在ImageNet上做的实测:
| 配置 | 显存占用 | 训练速度 | 准确率 |
|---|---|---|---|
| FP32(基线) | 12.4 GB | 1.0x | 76.1% |
| AMP(FP16) | 7.8 GB | 1.8x | 76.0% |
| AMP + GradScaler | 7.8 GB | 1.7x | 76.1% |
看到没?显存直接省了37%,速度提升了70%。准确率几乎没变。这就是为什么我强烈建议——只要你的GPU支持FP16(Volta架构及以上),AMP就是必选项。
但要注意,不是所有模型都适合AMP。我踩过的一个坑是:训练BERT-large时,AMP导致某些层的梯度一直为0。后来发现是LayerNorm的精度太敏感,需要手动指定某些操作用FP32。
- 是否有inf/NaN出现 → 调低GradScaler的初始缩放因子
- 某些层是否精度敏感 → 用
torch.cuda.amp.custom_fwd手动指定FP32
4.5 知识体系总览
下面这张图是我自己整理的AMP核心逻辑,你看一遍就能记住:
这张图把AMP的决策逻辑讲得很清楚。你只要记住:精度敏感走FP32,其他走FP16,最后统一用GradScaler兜底。就这么简单。
最后说一句,AMP不是银弹。如果你的模型本身就很小(比如显存占用不到2GB),加AMP的意义不大。但如果你在训大模型,或者想在同一张卡上塞更大的batch size——AMP就是你工具箱里最趁手的那个扳手。
想要更多资料,联系可可