混合精度训练:FP16/BF16原理、AMP自动混合精度、损失缩放策略
好,咱们来聊聊混合精度训练。说实话,这玩意儿现在几乎是训练大模型的标配了。我刚开始接触的时候,也觉得不就是把精度砍一半嘛,能有多大区别?后来真上手了才发现,这里面的门道还挺深。
FP16 和 BF16:两种低精度格式
先说说 FP16 和 BF16 到底是个啥。你想想看,我们平时用的 FP32,也就是单精度浮点数,占 4 个字节,32 个比特位。这 32 位里,1 位是符号,8 位是指数,23 位是尾数。
FP16 呢,就是把它砍成 16 位。1 位符号,5 位指数,10 位尾数。好处很明显——显存占用直接减半,计算速度也能翻倍。但问题也来了:指数位从 8 位砍到 5 位,能表示的数值范围就小了很多。
BF16 是 Google 搞出来的。它也是 16 位,但分配方式不一样:1 位符号,8 位指数,7 位尾数。你看,指数位跟 FP32 一样多,所以数值范围没缩水。代价是尾数精度降低了。
核心区别一句话:
- FP16:范围小,精度还行。容易溢出或下溢。
- BF16:范围大,精度差点。但训练起来更稳。
我在项目中遇到过用 FP16 训练一个 NLP 模型,梯度直接下溢到零,模型死活不收敛。换成 BF16 之后,问题就解决了。嗯,这就是数值范围不够的典型场景。
AMP:自动混合精度
手动去管哪些操作用 FP16、哪些用 FP32,太累了。NVIDIA 搞了个 AMP(Automatic Mixed Precision),说白了就是帮你自动做这个决策。
AMP 的核心逻辑其实不复杂:
- 计算密集的操作(比如卷积、矩阵乘法)用 FP16/BF16
- 对精度敏感的操作(比如 Softmax、LayerNorm)用 FP32
- 主权重始终维护一份 FP32 的副本
我习惯用 PyTorch 的 torch.cuda.amp,用起来特别简单:
from torch.cuda.amp import autocast, GradScaler
scaler = GradScaler()
for data, label in dataloader:
optimizer.zero_grad()
with autocast():
output = model(data)
loss = loss_fn(output, label)
scaler.scale(loss).backward()
scaler.step(optimizer)
scaler.update()
你看,就多了几行代码。但这里有个坑——autocast 只负责前向和 loss 计算,反向传播它不管。反向传播的精度控制,靠的是 GradScaler。
我的小技巧:
如果你用 BF16,可以试试把 autocast 的 dtype 参数设成 torch.bfloat16。BF16 不需要损失缩放,因为它的数值范围够大。
损失缩放策略
为什么要做损失缩放?说白了就是 FP16 的数值范围太小,梯度容易下溢成零。你想想看,梯度值可能小到 1e-6,但 FP16 能表示的最小正数是 6e-8 左右。再小就变零了。
损失缩放的做法很简单:在反向传播之前,先把 loss 乘上一个缩放因子。这样梯度也跟着被放大了,就不会下溢了。反向传播完之后,再把梯度除以这个因子,恢复原来的大小。
缩放因子的选择是个技术活。设太大,梯度可能溢出;设太小,又解决不了下溢问题。
我见过有人手动设成 1024,然后训练到一半 loss 突然炸了。嗯,这就是缩放因子太大,梯度溢出了。
GradScaler 默认用的是动态缩放策略:
- 初始缩放因子一般是 2^16 = 65536
- 如果连续 N 步没有梯度溢出,就把缩放因子翻倍
- 如果某一步梯度溢出了,就跳过这一步,把缩放因子减半
这个策略其实挺聪明的。它自动适应你的模型和数据的特性,不需要你手动调参。
注意:
损失缩放只对 FP16 有效。BF16 不需要这个,因为它的指数位跟 FP32 一样多,数值范围足够大。
知识体系结构图
下面这张图是我自己整理的混合精度训练的核心逻辑,你看一眼就能明白整体脉络:
实际使用中的注意事项
我总结几个实际踩过的坑,你注意一下:
- Batch Size 别太大。混合精度训练下,显存省下来了,很多人就拼命加大 batch size。但 batch size 太大,梯度更新方向会变钝,模型反而不好收敛。
- 监控梯度统计。我习惯在训练脚本里加几行代码,打印梯度的均值和方差。如果发现梯度突然变成零或者突然爆炸,赶紧停。
- 验证集精度要盯紧。混合精度训练偶尔会导致精度轻微下降。如果验证集精度掉了超过 0.5%,建议检查一下是不是某些层对精度太敏感。
避坑指南:
我曾经在训练一个 7B 参数的模型时,用了 FP16 混合精度,结果训练到一半 loss 突然变成 NaN。排查了半天,发现是某个自定义的激活函数在 FP16 下数值不稳定。换成 BF16 之后,问题就消失了。所以,如果你遇到 NaN,先试试 BF16。
好了,混合精度训练的核心内容就这些。说白了就是三件事:选对格式、用对工具、调好缩放。你动手试一次,就能体会到显存省下来、速度提上去的快感了。
想要更多资料,联系可可