混合精度训练是什么?为什么大模型偏爱 BF16 而不是 FP16?
简化版
混合精度训练是用低精度(FP16 或 BF16,16 位)做大部分计算、同时保留 FP32(32 位)关键副本的训练方式,目的是省显存、提速(低精度算得快、占得少),又靠 FP32 主副本保住数值精度。FP16 和 BF16 都是 16 位,区别在位分配:FP16 精度高但数值范围窄,训练时梯度容易下溢(变成 0),需要 loss scaling 补救;BF16 范围和 FP32 一样宽、但精度低,几乎不会上下溢,无需 loss scaling、训练更稳,所以在 A100/H100 上训大模型普遍用 BF16。
详细版
混合精度怎么做
- 权重、激活、梯度用 FP16/BF16 计算(前向、反向的矩阵乘走低精度,快且省显存)。
- 保留一份 FP32 主权重(master weights),优化器在 FP32 上累加更新(避免小更新被低精度吃掉)。 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,快且省。
② FP32 主权重(master copy):这是稳定收敛的关键。优化器每步的更新往往很小,如果直接加到 16 位权重上,会因为精度不足被”吃掉”(小数加大数,低位丢失,等于没更新)。所以保留一份 FP32 的权重副本,更新累加在 FP32 上,再转成 16 位用于计算。
③(FP16 专属)loss scaling:解决梯度下溢。反向传播中很多梯度值很小,落在 FP16 能表示的最小正数之下,就被舍入成 0,信息丢失。做法是训练时把 loss 乘一个大系数(如 1024),链式法则会让所有梯度同比放大,顶进 FP16 可表示区间;在优化器更新前再把梯度除回去。动态 loss scaling 还能自动调节这个系数(溢出就调小,长期不溢出就调大)。
三、FP16 和 BF16 的本质差别:范围 vs 精度
16 位就 16 个 bit,怎么分配指数位和尾数位,决定了这个格式的性格:
- 指数位(exponent)决定能表示多大/多小的数(动态范围)。
- 尾数位(mantissa)决定同一量级内的精细程度(精度)。
FP16:5 指数 + 10 尾数。尾数多 → 精度较高,但指数只有 5 位 → 范围很窄(约 6e-5 到 65504)。深度学习训练里,梯度的动态范围很大,FP16 的窄范围让大激活溢出、小梯度下溢成为常态,必须靠 loss scaling 小心伺候。
BF16(Brain Float 16):8 指数 + 7 尾数。指数位和 FP32 完全一样 → 范围一样宽,几乎不会上溢下溢;代价是尾数只有 7 位 → 精度较低。但实践证明,深度学习对”范围”远比对”精度”敏感——模型能容忍数值不那么精确,却受不了梯度变成 0 或爆炸。所以 BF16 用「牺牲精度换范围」的取舍,反而更适合训练。
记忆一句:指数管范围、尾数管精度;FP16 精度高但范围窄(要 loss scaling),BF16 范围宽但精度低(免 loss scaling)。深度学习吃”范围”不吃”精度”,所以大模型爱 BF16。
四、为什么 BF16 成了大模型训练的默认
综合来看,BF16 的优势压倒性:
- 训练更稳:宽范围避免了 FP16 常见的溢出/下溢导致的 NaN 和发散,不用调 loss scaling,工程上省心。
- 精度够用:7 位尾数配合 FP32 主权重,对大模型训练完全足够,最终质量与 FP16 相当甚至更好。
- 硬件支持好:A100、H100、TPU 都原生高速支持 BF16。
所以从 GPT、Llama 到几乎所有现代大模型,预训练标配 BF16 混合精度。FP16 更多见于早期或某些推理场景。近年还有 FP8 训练(H100 支持)进一步压精度提吞吐,但需要更精细的缩放策略。
五、混合精度和其他省显存手段的配合
混合精度是显存优化的一环,通常和其他手段叠加:
- 混合精度:参数/激活/梯度降到 16 位(甚至 8 位)。
- 梯度检查点:不存全部激活、反向时重算,省激活显存。
- ZeRO/FSDP:分片参数/梯度/优化器状态,消除数据并行冗余。
- 张量/流水线并行:把模型拆到多卡。
这些正交叠加,才让有限的卡训得动大模型。混合精度是其中「性价比最高、几乎必开」的基础项。
六、面试拆解算例
预训练题最好落到预算账和稳定性账。假设训练一个 7B 模型,目标 token 数是 1T,按常见粗估 6 × 参数量 × token 数,训练计算量约为 6 × 7e9 × 1e12 = 4.2e22 FLOPs。如果有效集群算力是 1e18 FLOPs/s,理想情况下也要约 42,000 秒;现实还要扣通信、数据加载、checkpoint 和故障恢复的损耗。
training_flops ≈ 6 * N * D
N = 7e9 parameters
D = 1e12 tokens
training_flops ≈ 4.2e22
| 账本 | 关键变量 | 常见瓶颈 | 排查信号 |
|---|---|---|---|
| 数据账 | token 数、重复率、质量分 | 脏数据和污染 | eval 异常偏高 |
| 算力账 | GPU 数、利用率、通信 | MFU 低 | step time 抖动 |
| 显存账 | batch、序列长、优化器状态 | OOM | 激活占用过高 |
| 稳定性账 | 学习率、精度、梯度 | loss spike | overflow/NaN |
语料 -> 清洗去重 -> tokenization -> 分布式训练 -> checkpoint -> 评测
| | | | |
质量 覆盖率 吞吐 可恢复 能力验证
所以回答「混合精度训练是什么?为什么大模型偏爱 BF16 而不是 FP16?」时,不能只说某个技巧“省显存”或“加速”。要说明它省的是哪一笔账、牺牲了什么、线上训练日志里应该观察哪个信号。
七、常见误区与追问
- 误区:混合精度就是全用 FP16。 是「低精度计算 + FP32 主权重(+FP16 时 loss scaling)」的混合,不是纯 16 位。
- 误区:BF16 精度更高所以更好。 恰恰相反,BF16 精度更低,它的优势是范围宽、训练稳、免 loss scaling。
- 追问:loss scaling 解决什么,为什么 BF16 不用? 解决 FP16 梯度下溢;BF16 范围和 FP32 一样宽,基本不下溢,故不需要。
- 追问:为什么要 FP32 主权重? 小的参数更新在 16 位会被精度不足”吃掉”,FP32 上累加才不丢失。
- 追问:指数位和尾数位分别管什么? 指数管数值范围,尾数管精度。
- 追问:深度学习为什么能容忍低精度? 模型对数值精度不敏感、对动态范围敏感,且有 FP32 主权重兜底关键更新。
- 追问:FP8 是什么? 更激进的 8 位训练(H100 支持),吞吐更高,但需更精细的分块缩放来控范围。
八、加强记忆
混合精度记「低精度算、FP32 兜底」:前向反向用 16 位(省显存、走 Tensor Core 提速),保留 FP32 主权重累加更新,FP16 时还要 loss scaling 防梯度下溢。 FP16 vs BF16 简要说钉死:指数管范围、尾数管精度——FP16(5 指数 10 尾数)精度高但范围窄,必须 loss scaling;BF16(8 指数 7 尾数)范围同 FP32、精度低,免 loss scaling、训练稳。 因为深度学习吃”范围”不吃”精度”,所以大模型预训练默认 BF16。记住这条取舍逻辑,就能解释一切相关追问。