混合精度训练是什么?为什么大模型偏爱 BF16 而不是 FP16?
简化版
混合精度训练让矩阵乘等重计算使用 FP16/BF16,同时让归约、优化器状态或主参数等敏感环节保留更高精度,以换取吞吐和显存收益。FP16 尾数更多但指数范围窄,训练通常需要动态 loss scaling;BF16 与 FP32 指数位相同、动态范围相近而尾数更短,通常不需要 loss scaling,因此在原生支持它的硬件上常更易稳定训练。
详细版
混合精度怎么做
- 权重、激活、梯度用 FP16/BF16 计算(前向、反向的矩阵乘走低精度,快且省显存)。
- 优化器状态与参数更新通常使用 FP32,部分实现保留 FP32 主权重;具体是否存在独立 master copy 取决于框架与参数存储方案。 3.(FP16 时)用 loss scaling 放大损失,把偏小的梯度顶进 FP16 可表示范围,更新前再缩放回去。
FP16 vs BF16 位分配
| 格式 | 总位 | 指数位 | 尾数位 | 数值范围 | 精度 |
|---|---|---|---|---|---|
| FP32 | 32 | 8 | 23 | 很宽 | 很高 |
| FP16 | 16 | 5 | 10 | 窄(易上/下溢) | 较高 |
| BF16 | 16 | 8 | 7 | 宽(同 FP32) | 较低 |
关键:指数位决定范围,尾数位决定精度。
- FP16:5 位指数、范围窄 → 更易溢出/下溢,训练中通常配动态 loss scaling。
- BF16:8 位指数(和 FP32 一样)、范围宽 → 溢出风险显著降低,通常无需 loss scaling,但 7 位尾数带来更粗舍入。
为什么大模型选 BF16
- 训练中梯度动态范围大,FP16 的窄范围易导致下溢/溢出、训练发散,调 loss scaling 麻烦。
- BF16 范围宽、训练稳定、无需 loss scaling,精度虽低但对深度学习足够(模型对精度不敏感、对范围敏感)。
- 现代硬件(A100/H100/TPU)原生高速支持 BF16。
完整版教学
一、为什么要用低精度
默认 FP32(单精度,32 位)又准又稳,但对大模型有两个负担:占显存(每个数 4 字节)和算得慢(吞吐低)。把计算换成 16 位有立竿见影的好处:
- 显存减半:参数、激活、梯度都从 4 字节降到 2 字节,能放更大模型/更大 batch。
- 算得更快:GPU 的张量核心(Tensor Core)对 16 位矩阵乘有数倍于 FP32 的吞吐。
但纯用 16 位训练会出问题(精度/范围不足导致发散),所以采用「低精度算、FP32 兜底」的混合精度,既拿到速度和显存的好处,又不牺牲收敛。
二、混合精度的三个关键设计
① 低精度前向/反向:矩阵乘等重计算用 FP16/BF16 走 Tensor Core,快且省。
② 高精度更新状态:优化器每步更新可能很小,若直接在 16 位参数上累加,低位可能被舍入掉。常见实现让 Adam 的 m/v 和参数更新在 FP32 中完成,并可能保留 FP32 master weight,再把低精度视图用于计算;框架也可采用 BF16 参数配 FP32 状态等不同布局,所以要以实际内存模型为准。
③(FP16 专属)loss scaling:解决梯度下溢。反向传播中很多梯度值很小,落在 FP16 能表示的最小正数之下,就被舍入成 0,信息丢失。做法是训练时把 loss 乘一个大系数(如 1024),链式法则会让所有梯度同比放大,顶进 FP16 可表示区间;在优化器更新前再把梯度除回去。动态 loss scaling 还能自动调节这个系数(溢出就调小,长期不溢出就调大)。
三、FP16 和 BF16 的本质差别:范围 vs 精度
16 位就 16 个 bit,怎么分配指数位和尾数位,决定了这个格式的性格:
- 指数位(exponent)决定能表示多大/多小的数(动态范围)。
- 尾数位(mantissa)决定同一量级内的精细程度(精度)。
FP16:5 指数 + 10 尾数。尾数多,16 位格式内的相对精度高;最大有限值为 65504,最小正规格化正数约 6.1e-5,还存在更小的 subnormal。训练中仍更容易发生大值溢出与小梯度下溢,所以通常配动态 loss scaling。
BF16:8 指数 + 7 尾数,指数范围与 FP32 相近,因此比 FP16 更不易因动态范围不足而溢出;代价是同一数量级内的舍入更粗。许多神经网络在 FP32 累加/状态配合下能容忍这种精度,但这不是“永不溢出”或“任何模型都无损”,仍要监控 NaN、梯度范数与收敛质量。
记忆钩子:指数位主要决定动态范围,尾数位主要决定相对精度;FP16 常靠 loss scaling 保范围,BF16 以更粗精度换取接近 FP32 的指数范围。
四、为什么 BF16 成了大模型训练的默认
在硬件原生支持且任务验证通过时,BF16 常有以下工程优势:
- 训练更稳:宽范围降低 FP16 常见的溢出/下溢风险,通常不必维护动态 loss scale。
- 精度常可接受:7 位尾数配合高精度累加和优化器状态,在许多大模型训练中能达到目标质量,但仍需回归验证。
- 硬件支持好:A100、H100、TPU 都原生高速支持 BF16。
因此 BF16 是现代训练中的常见选择,FP16 仍会出现在硬件支持、已有训练栈或特定性能路径更合适的场景。FP8 可进一步提高吞吐与降低带宽,但需要分块缩放、格式选择和更严格的数值监控。
五、混合精度和其他省显存手段的配合
混合精度是显存优化的一环,通常和其他手段叠加:
- 混合精度:参数/激活/梯度降到 16 位(甚至 8 位)。
- 梯度检查点:不存全部激活、反向时重算,省激活显存。
- ZeRO/FSDP:分片参数/梯度/优化器状态,消除数据并行冗余。
- 张量/流水线并行:把模型拆到多卡。
这些技术可以组合使用。混合精度通常是收益显著的基础项,但若硬件不原生支持目标格式、某些算子数值敏感或收敛回归不通过,仍需保留更高精度路径。
六、比较数值范围而不是只数位宽
FP16 与 BF16 都占 16 bit,但 FP16 有 5 位指数、BF16 有 8 位指数。一个绝对值约 1e5 的梯度超过 FP16 最大有限值 65504,可能溢出为 Inf;BF16 的指数范围接近 FP32,可以表示它,但尾数更短、舍入更粗。
| 格式 | 指数位 | 尾数位 | 训练特征 |
|---|---|---|---|
| FP16 | 5 | 10 | 精度较细,范围窄,常需 loss scaling |
| BF16 | 8 | 7 | 范围大,通常更稳 |
| FP32 | 8 | 23 | 状态更新精确,存储成本高 |
FP16 max finite = 65,504
example gradient = 100,000 -> overflow in FP16
BF16 keeps FP32-like exponent range -> representable but coarser
混合精度通常让前向/反向使用低精度,同时保留 FP32 优化器状态或主权重。选择不能只看“是否支持”:还要监控 overflow、NaN、梯度范数、吞吐和目标硬件的 Tensor Core 路径。
七、常见误区与追问
- 误区:混合精度就是全用 FP16。 它是低精度计算与高精度累加/状态的组合,具体是否保留独立 FP32 主权重取决于实现。
- 误区:BF16 精度更高所以更好。 恰恰相反,BF16 精度更低,它的优势是范围宽、训练稳、免 loss scaling。
- 追问:Loss scaling 解决什么,为什么 BF16 通常不用? 它放大 FP16 梯度以减少下溢;BF16 指数范围更宽,所以通常无需该补偿。
- 追问:为什么更新常保留 FP32 精度? 小更新在 16 位参数上可能被舍入掉,FP32 状态/主参数可提高累加精度。
- 追问:指数位和尾数位分别管什么? 指数管数值范围,尾数管精度。
- 追问:神经网络为何常能容忍低精度? 训练具有一定数值冗余,并由高精度累加/优化器状态保护关键更新;可容忍程度仍随模型和算子变化。
- 追问:FP8 是什么? 更激进的 8 位训练(H100 支持),吞吐更高,但需更精细的分块缩放来控范围。
八、加强记忆
混合精度记“低精度算、高精度累加或更新”:FP16/BF16 负责矩阵吞吐,FP32 常保护归约、优化器状态或主参数。FP16 有 5 位指数、10 位尾数,范围窄而通常需要动态 loss scaling;BF16 有 8 位指数、7 位尾数,以更粗舍入换来接近 FP32 的动态范围,通常无需 scaling。选择还取决于硬件 Tensor Core、框架内存布局和收敛回归,不能把 BF16 写成永不溢出或对所有模型无损。