训练大模型的时候,很多人遇到过这样的情况:前几个step的loss还正常下降,突然某一步直接变成了NaN,训练彻底崩掉。排查半天发现学习率没问题、数据也没问题,最后定位到是浮点数溢出或者精度丢失造成的。单纯用FP32训练虽然稳定,但显存占用和计算开销都不小,尤其是在大模型场景下几乎撑不住。于是BF16与FP32混合精度成了目前工业界的主流做法,这篇文章就把这件事讲透。

一、浮点数到底是怎么存储的,误差从哪来
要理解精度问题,得先看浮点数在计算机里的表示方式。按照IEEE 754标准,一个浮点数由三部分组成:符号位(Sign)、指数位(Exponent)和尾数位(Mantissa)。以FP32为例,它总共32位,其中1位符号、8位指数、23位尾数。指数位决定了数值的动态范围,也就是能表示多大的数和多小的数;尾数位决定了有效数字的精度,也就是一个小数能被表示得多么精确。
误差主要来自两个地方。第一是舍入误差:十进制的0.1转换成二进制是无限循环小数,尾数位只有23位,只能截断,于是0.1 + 0.2在浮点数运算里不严格等于0.3,这类误差在亿万次累加后会不断放大。第二是溢出和下溢:当数值超出指数位能表示的范围,就会变成正无穷或者负无穷;当数值太小低于最小规格化数时,会被 flush 成零。深度学习里梯度经常小到1e-8以下,如果用精度范围不足的格式存储,这些梯度直接归零,模型就学不到东西了。
二、BF16和FP32、FP16的对比
BF16的全称是Brain Floating Point,最初由Google Brain团队提出,在TPU上大规模使用。它的结构很特别:1位符号、8位指数、7位尾数。注意它的指数位和FP32完全一样是8位,这意味着两者的动态范围相同,BF16能表示的数值上限和下限跟FP32基本一致。代价是尾数从23位砍到7位,精度只剩下大约3位十进制有效数字。
对比之下,FP16是1位符号、5位指数、10位尾数。它的尾数比BF16多3位,精度稍高,但指数位少了3位,最大只能表示65504,最小规格化数约为6e-8。在深度学习场景里,这个范围明显不够用:激活值稍大就上溢,梯度稍小就下溢,所以FP16必须配合损失放大技术才能勉强用。而BF16因为动态范围大,训练中基本不会溢出,也不太需要损失放大,这正是它在Transformer训练中被广泛采用的核心原因。
| 格式 | 符号位 | 指数位 | 尾数位 | 动态范围特点 |
|---|---|---|---|---|
| FP32 | 1 | 8 | 23 | 基准 |
| BF16 | 1 | 8 | 7 | 与FP32同范围,精度低 |
| FP16 | 1 | 5 | 10 | 范围窄,易溢出 |
BF16转FP32非常简单,只要把BF16的位模式左移16位补零即可,反过来截断就行,硬件实现代价极低。这也是为什么NVIDIA从Ampere架构开始,在GPU上原生支持BF16的原因。
三、混合精度的核心思想与实现
混合精度的思路不是全程用低精度,而是分工:前向传播和反向传播中的矩阵乘法用BF16计算,速度快、显存省;而权重主副本、优化器状态、损失计算和累加操作保留在FP32中,保证数值稳定性。因为权重更新量往往非常小,比如学习率乘以梯度后可能是1e-7量级,如果权重本身是BF16(精度只有7位尾数),这个更新量加上去会被直接舍入掉,模型就停止学习了。所以权重必须以FP32保存,每一步更新后再截断成BF16参与下一轮计算。
在PyTorch中,可以用autocast上下文管理器自动完成类型转换:
import torch
model = MyModel().cuda()
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4)
for data, target in dataloader:
optimizer.zero_grad()
with torch.autocast(device_type="cuda", dtype=torch.bfloat16):
output = model(data)
loss = loss_fn(output, target)
loss.backward()
optimizer.step()
autocast会自动判断哪些算子适合用低精度运行(比如matmul、conv),哪些必须保持高精度(比如softmax、layer norm、loss计算),不需要手动逐层转换。如果用的是FP16而不是BF16,还需要额外引入GradScaler做损失放大,先把loss乘一个较大的系数,让反向传播中的梯度整体放大到FP16能表示的范围,更新权重前再缩回去。而BF16因为动态范围够大,通常可以省掉这一步,代码更简洁,也少了一个可能出错的环节。
四、实践中的注意事项
第一,混合精度要配合权重备份。确保模型的master权重和优化器状态始终是FP32,常见做法是模型参数用BF16,但优化器内部维护FP32副本。像DeepSpeed、FSDP这类框架已经内置了这个机制,直接开bf16配置即可。第二,注意某些算子对精度敏感,比如求和、归一化、指数运算,如果发现在低精度下出现异常,可以用autocast的disabled上下文局部强制FP32执行。
第三,硬件方面,BF16加速需要Ampere架构以上的NVIDIA GPU或者较新的CPU支持,老卡上跑BF16可能反而更慢,先确认硬件支持再切换。第四,推理阶段同样可以用BF16,模型体积减半,吞吐提升明显,而且BF16的溢出风险比FP16低,部署稳定性更好。总结一下:BF16负责快,FP32负责准,两者各司其职,既解决了纯低精度训练的数值问题,又拿到了接近一半的显存和速度收益,这就是混合精度成为大模型训练标配的原因。