目录
第五课里我们用“每个参数占多少 Bytes”估算显存,第六课又用多卡分摊训练状态。这里一直出现 FP32、FP16 和 BF16,它们到底有什么区别?
直观上,它们都是保存小数的方法。位数越少,显存和带宽压力越小,硬件通常也能算得更快;但能表达的范围和精细程度会发生变化。
低精度训练不是把所有数字粗暴地变小,而是让不同计算使用适合自己的精度。
1、浮点数在保存什么
浮点数通常由三部分组成:符号位、指数位和尾数位。
符号:正数还是负数
指数:数量级有多大或多小
尾数:这个数能表示得多精细
指数位决定动态范围,尾数位决定精度。范围和精度不是同一回事:能表示很大的数字,不代表能精细地区分两个很接近的数字。
2、FP32、FP16、BF16 的区别
| 格式 | 总位数 | 指数位 | 尾数位 | 每个数大小 |
|---|---|---|---|---|
| FP32 | 32 | 8 | 23 | 4 Bytes |
| FP16 | 16 | 5 | 10 | 2 Bytes |
| BF16 | 16 | 8 | 7 | 2 Bytes |
FP16 把指数和尾数都缩短,因此更精细,但范围明显小于 FP32。BF16 保留了与 FP32 相同的 8 位指数,所以范围接近 FP32;代价是尾数只有 7 位,数字更粗糙。
3、为什么低精度更快
从 FP32 换成 16 位格式,通常会带来三类收益:
- 同样数量的数据只占一半显存。
- GPU 与显存之间需要搬运的数据更少。
- Tensor Core 等硬件能以更高吞吐执行低精度矩阵乘法。
但“理论减半”不等于总显存一定减半。优化器状态、临时 Buffer、对齐和某些仍保持 FP32 的数据都会占空间。
4、FP16 为什么容易出问题
训练中的梯度可能非常小,也可能突然变大。FP16 动态范围较窄,容易出现:
| 问题 | 含义 | 后果 |
|---|---|---|
| Underflow | 小数太小,被舍入为 0 | 梯度消失 |
| Overflow | 数值超出上限,变成 Inf | Loss 变成 NaN |
| Rounding | 相近数字无法区分 | 更新不够精细 |
FP16 训练常使用 Loss Scaling:先把 Loss 乘以较大的缩放因子,让微小梯度进入 FP16 可表示范围;反向传播后再把梯度缩回去。
\[L_{scaled}=S\times L\]如果发现梯度溢出,动态 Loss Scaling 会跳过这次更新并降低 $S$。
5、混合精度怎样分工
混合精度不是“全程 FP16”,而是按照计算特点分工:
矩阵乘法、参数副本、激活:FP16 或 BF16
累加、归一化、部分归约:必要时 FP32
优化器状态、主权重:常保留 FP32
低精度负责大规模计算和存储,FP32 负责对数值稳定性敏感的部分。框架中的自动混合精度通常会根据算子选择合适类型。
6、BF16 为什么更适合大模型
BF16 的指数位与 FP32 一样,动态范围大,因此不容易因为数值过大或过小而溢出。它通常不需要像 FP16 那样依赖 Loss Scaling。
它的尾数更短,精度低于 FP16,但神经网络训练往往更怕范围不够,而不是每个数少几位小数。配合 FP32 累加和优化器状态,BF16 成为现代大模型训练的常见选择。
前提是硬件原生支持 BF16;否则速度收益可能有限。
三种格式对比
| 维度 | FP32 | FP16 | BF16 |
|---|---|---|---|
| 显存 | 高 | 低 | 低 |
| 动态范围 | 大 | 小 | 大 |
| 尾数精度 | 高 | 中 | 较低 |
| Loss Scaling | 不需要 | 常需要 | 通常不需要 |
| 大模型训练 | 稳定但慢 | 快,需谨慎 | 常用折中方案 |
容易混淆的三件事
- BF16 并不比 FP16 更“精确”。 它范围更大,但尾数更短。
- 混合精度不等于模型所有数据都是 16 位。 敏感计算和训练状态仍可能使用 FP32。
- 精度格式与 INT8/INT4 量化不是同一件事。 前者仍是浮点格式,后者通常属于整数或低比特表示。
记住这 5 件事
- 指数决定范围,尾数决定精细程度。
- FP16 范围较小,可能发生 Underflow 和 Overflow。
- BF16 保留 FP32 的指数范围,但尾数更短。
- 混合精度让低精度负责速度,FP32 负责稳定性。
- 最终选择取决于硬件支持、模型和训练配置。
第七课复习总图
下一课,我们可以把前七课串起来:从 Dataset、Forward、Loss、Backward 到 Optimizer Step,亲手实现一次完整训练循环。