数值精度
一个数,到底用多少 bit 表示;这些 bit 又要怎么在“范围”和“精度”之间分配。
很多初学者会把“位数更小”简单理解成“更差”,但真正的情况不是这样。
更小的精度通常意味着:
- 显存占用更低;
- 带宽压力更小;
- 矩阵乘吞吐更高;
- 训练和推理速度更快。
但代价也很明确:
- 可表示的数值范围可能变小;
- 小数精度会下降;
- 更容易出现上溢、下溢和舍入误差;
- 训练会更依赖混合精度和缩放技巧。
不同精度的区别,本质上就是在“动态范围”和“有效精度”之间做权衡。
浮点数到底在表示什么
一个二进制浮点数通常由三部分组成:
- sign:符号位;
- exponent:指数位;
- fraction / mantissa:尾数位。
对一个归一化的二进制浮点数,它的值通常可以写成:
这里:
决定正负; 决定数量级,也就是“范围”; 决定小数部分的精细程度,也就是“精度”。
这就解释了为什么指数位和尾数位都重要,但它们负责的不是同一件事。
指数位多意味着什么
指数位更多,意味着能表示的数量级更广。
也就是说,更大的正数、更小的正数,都更不容易溢出或下溢。
这通常被叫做 动态范围。
尾数位多意味着什么
尾数位更多,意味着相邻两个可表示数之间的间距更小。
也就是说,在同样数量级附近,你能表达得更细。
这通常被理解成 数值精度。
所以:
- 想让数“不要太容易炸掉”,要看指数位;
- 想让数“表达得更细”,要看尾数位。
为什么训练里会越来越常见低精度
深度学习里,矩阵乘和张量读写非常多。位宽一旦减半,通常会同时带来两类收益:
第一类是 存储收益。例如同样一块显存,16-bit 能装下大约两倍于 32-bit 的数;8-bit 又能装下大约两倍于 16-bit 的数。
第二类是 算力和带宽收益。现代 AI 硬件通常会对低精度矩阵运算提供更高吞吐,所以同样的模型,在低精度下往往能训得更快、推得更快。NVIDIA 的 mixed-precision 文档和 FP8 文档都把这一点作为核心动机之一。(Mixed Precision Training, FP8 Formats for Deep Learning)
所以从工程角度看,低精度并不是“为了省一点 bit 而省 bit”,而是因为它同时影响:
- 显存容量;
- 带宽开销;
- Tensor Core 吞吐;
- 可训练的 batch size;
- 可训练的模型规模。
常见格式
| 格式 | 总位数 | 符号位 | 指数位 | 尾数位(显式) | 典型特点 |
|---|---|---|---|---|---|
| FP32 | 32 | 1 | 8 | 23 | 范围和精度都比较均衡,是传统训练基线 |
| FP16 | 16 | 1 | 5 | 10 | 尾数比 BF16 多,但指数更少,范围更窄 |
| BF16 | 16 | 1 | 8 | 7 | 指数和 FP32 一样,范围大,但精度比 FP16 粗 |
| FP8 E4M3 | 8 | 1 | 4 | 3 | 精度稍高于 E5M2,但范围更小 |
| FP8 E5M2 | 8 | 1 | 5 | 2 | 范围更大,常更适合梯度等更容易爆的张量 |
其中:
- FP16 的 1/5/10 位分配是 IEEE half precision 的标准写法。(NVIDIA Mixed Precision Training Guide, CUDA Programming Guide)
- BF16 的核心特征是保留了和 FP32 一样的 8 位指数,因此范围接近 FP32,但尾数只剩 7 位。(A Study of BFLOAT16 for Deep Learning Training, Intel BF16 Hardware Numerics Definition)
- FP8 常见的两种格式是 E4M3 和 E5M2,这是 NVIDIA、Arm、Intel 联合提出的 8-bit 浮点格式。(FP8 Formats for Deep Learning, NVIDIA FP8 Standardization Blog)
一般来说:
FP16:精度相对好一点,但范围更小;BF16:范围相对大很多,但精度更粗;FP8:更省、更快,但训练更依赖缩放和更高精度累积。
FP32
FP32 就是常见的单精度浮点。它通常用 1 个符号位、8 个指数位和 23 个显式尾数位表示,因此既有足够的范围,也有不错的精度。CUDA 文档把 32-bit float 作为标准单精度格式来描述。(CUDA Programming Guide)
在深度学习里,FP32 长期是默认基线,原因很简单:
- 不太容易溢出或下溢;
- 数值稳定性好;
- 大多数优化器和算子都天然支持;
- 不需要额外担心 loss scaling 之类的技巧。
但它的问题也很明显:
- 显存占用大;
- 带宽压力高;
- 在现代 AI 加速器上吞吐通常不如更低精度。
所以今天很多训练虽然不再“全程 FP32”,但仍然会在关键位置保留 FP32,例如:
- master weights;
- optimizer states;
- 一些 reduction 和累加;
- layer norm、softmax 或统计量相关运算的内部累积。
FP16
FP16 是标准的 IEEE half precision,位分配是:
- 1 位 sign;
- 5 位 exponent;
- 10 位 fraction。(NVIDIA Mixed Precision Training Guide)
这个格式的特点是:
- 和 FP32 比,位宽减半;
- 小数精度并不算太差;
- 但指数位只有 5 位,所以动态范围明显更小。
这会直接带来一个训练里的典型问题:
梯度很小的时候容易下溢成 0,激活或中间量很大时又更容易溢出。
Micikevicius 等人的 mixed precision training 论文把 FP16 训练常见的三个关键技巧总结得很清楚:
- 使用 FP32 master weights;
- 在矩阵乘等关键运算里做更高精度累积;
- 使用 loss scaling,避免小梯度在 FP16 下直接下溢。(Mixed Precision Training)
为什么 FP16 常需要 loss scaling
假设某个梯度本来就很小,写成 FP16 后可能直接变成 0。那一旦变成 0,后面的优化器就再也看不到这个梯度了。
所以常见做法是把 loss 先乘一个缩放因子
这样反向传播得到的梯度也会整体放大:
等到梯度传回优化器前,再除回去:
这样做的目的不是改变训练目标,而是让中间梯度在低精度里更不容易被抹成 0。NVIDIA 的 mixed precision 文档和论文都明确把 loss scaling 作为 FP16 训练的核心技巧之一。(Mixed Precision Training, NVIDIA Mixed Precision Training Guide)
FP16 的一句话总结
FP16 的核心问题不是位数少,而是指数太少,所以范围偏小。
这就是为什么它虽然快,但训练时往往比 BF16 更需要小心处理数值稳定性。
BF16
BF16 也是 16-bit,但它和 FP16 的设计取舍完全不同。
它通常是:
- 1 位 sign;
- 8 位 exponent;
- 7 位 fraction。(A Study of BFLOAT16 for Deep Learning Training, Intel BF16 Hardware Numerics Definition)
最关键的一点是:
BF16 的指数位和 FP32 一样,都是 8 位。
这意味着它的动态范围和 FP32 非常接近。BF16 论文和 Intel 文档都把“与 FP32 相同的 range”作为 BF16 的核心优势。(A Study of BFLOAT16 for Deep Learning Training, Intel BF16 Hardware Numerics Definition)
BF16 和 FP16 的真正区别
很多初学者会误以为:
FP16 和 BF16 都是 16-bit,所以只是“名字不同”。
其实不是。
它们最本质的区别是:
- FP16 把更多 bit 给了尾数,所以局部精度更细;
- BF16 把更多 bit 给了指数,所以范围更大、更不容易溢出或下溢。
所以在训练里,它们的典型感觉会是:
- FP16:更“精细”,但更“脆”;
- BF16:更“粗”,但更“稳”。
为什么很多训练更喜欢 BF16
因为深度学习训练里,一个很常见的问题不是“尾数精度差一点”,而是“数值范围不够”。
如果范围不够,梯度和激活直接溢出 / 下溢,训练就会炸。
而 BF16 因为保留了 FP32 的指数宽度,通常不那么依赖 loss scaling。BF16 训练论文明确报告了:在他们的实验中,BF16 能在不改超参的情况下复现 FP32 结果;他们把这很大程度归因于 BF16 与 FP32 相同的数值范围。(A Study of BFLOAT16 for Deep Learning Training)
BF16 的一句话总结
BF16 牺牲的是小数精度,换来的是接近 FP32 的动态范围。
这也是为什么今天很多大模型训练比起 FP16,更偏爱 BF16。
FP8
FP8 的基本动机很直接:
如果 16-bit 已经能显著降低显存和提升吞吐,那 8-bit 会更进一步。
Micikevicius 等人的 FP8 论文提出了两种 8-bit 浮点格式:
E4M3:4 位指数、3 位尾数;E5M2:5 位指数、2 位尾数。(FP8 Formats for Deep Learning)
为什么要两个 FP8 格式
因为 8-bit 太紧张了,不可能同时兼顾很好的范围和很好的精度。
所以 FP8 不再追求“一种格式打天下”,而是拆成两个版本:
E4M3:多给 1 位尾数,精度更好一点,但范围更小;E5M2:多给 1 位指数,范围更大,但精度更粗。
FP8 论文明确指出,E5M2 更适合一些对动态范围要求更高的张量,例如梯度;而 NVIDIA Transformer Engine 文档也明确提到,常规 FP8 里一些梯度张量会使用 E5M2。(FP8 Formats for Deep Learning, NVIDIA Transformer Engine FP8 Primer)
FP8 为什么不能裸用
因为 8-bit 的可表示能力太有限,如果直接把整块张量生硬丢进 FP8,量化误差通常会非常明显。
所以 FP8 几乎总和 scaling 一起用。
最常见的思路是:
- 先为一个 tensor 选一个缩放因子;
- 把 tensor 缩放到更适合 FP8 动态范围的位置;
- 再存成 FP8;
- 真正计算时在更高精度上做累积。
NVIDIA Transformer Engine 文档明确写到,常规 FP8 通常为每个 tensor 配一个 FP32 scaling factor;而更细粒度的 MXFP8 则会做更细的 block-level scaling。(NVIDIA Transformer Engine FP8 Primer)
FP8 真正能工作,不是因为 8-bit 本身 magically 足够,而是因为它通常配合缩放、格式区分和更高精度累积一起使用。
动态范围和精度,到底谁更重要
这是最容易被问到的问题。
答案不是“永远范围更重要”或者“永远精度更重要”,而是要看你在训练中更容易被什么问题卡住。
当范围不够时会发生什么
如果指数位太少,你会更容易遇到:
- 很大的值变成
inf; - 很小的值直接下溢成 0;
- 梯度更新完全消失;
- softmax、norm、optimizer state 更容易不稳定。
这类问题通常是“训练直接出事”。
当精度不够时会发生什么
如果尾数位太少,你会更容易遇到:
- 舍入误差更大;
- 很接近的两个数分不开;
- 累加误差变大;
- 训练能跑,但最终效果掉点。
这类问题更像“训练能跑,但质量变差”。
所以从工程上看,训练通常更怕“范围不够导致直接爆炸或消失”。这也是 BF16 相比 FP16 常更省心、FP8 必须高度依赖 scaling 的原因。(A Study of BFLOAT16 for Deep Learning Training, FP8 Formats for Deep Learning)
训练里为什么经常不是“全程同一种精度”
真正的深度学习系统,通常不会简单粗暴地“全模型都用一种精度到底”。
更常见的是 mixed precision。
也就是说,不同对象用不同精度:
- 张量存储用低精度;
- 累积用更高精度;
- 参数主副本和优化器状态常留在 FP32;
- 某些敏感算子内部自动提升精度。
Micikevicius 2017 的 mixed precision 训练论文把一套经典做法讲得非常清楚:
- 权重、激活、梯度可以存成 FP16;
- 但更新时保留一份 FP32 master weights;
- 矩阵乘的累积常在 FP32 完成;
- 梯度过小时用 loss scaling。(Mixed Precision Training)
所以“混合精度”不是一项小技巧,而是低精度训练真正能稳定工作的核心原因。
为什么 optimizer state 往往还留在 FP32
以 Adam 为例,优化器通常不只保存参数本身,还要保存一阶、二阶动量等状态。
这些状态有两个特点:
- 会被长期累积;
- 对舍入误差比较敏感。
如果把它们也一股脑压得太低,数值误差可能会慢慢积起来,最后影响收敛。
所以在很多训练实现里,哪怕前向和反向大量使用 BF16 / FP16 / FP8,优化器状态仍常保留在 FP32。这和 mixed precision 训练中保留 FP32 master copy 的思路是一致的。(Mixed Precision Training)
什么时候更适合 FP16,什么时候更适合 BF16
更偏向 FP16 的情况
如果硬件对 FP16 支持成熟,而你的训练流程已经配好了:
- loss scaling;
- FP32 master weights;
- 合适的 autocast / AMP;
那么 FP16 仍然是很常见的高效选择。NVIDIA 的 mixed precision 训练路线最初就是围绕 FP16 展开的。(Mixed Precision Training, NVIDIA Mixed Precision Training Guide)
更偏向 BF16 的情况
如果你更在意训练稳定性,尤其是大模型、长序列、梯度范围跨度大时,BF16 往往更省心。
因为它的指数范围和 FP32 接近,通常比 FP16 更不容易被上溢 / 下溢折腾,也通常更少依赖精细的 loss scaling。(A Study of BFLOAT16 for Deep Learning Training)
FP16 更像“需要调教的快车”,BF16 更像“稳一点的默认选项”。
当然,真正选择还取决于硬件和框架支持。
FP8 在训练和推理里通常怎么用
FP8 并不是说“所有东西都直接改成 8-bit 就完了”。实际使用时往往更细:
- 某些前向激活、权重更适合用 E4M3;
- 某些梯度、更容易爆的张量更适合用 E5M2;
- 累积和关键归约通常仍保留更高精度;
- 每个 tensor 或每个 block 还需要单独 scaling。(FP8 Formats for Deep Learning, NVIDIA Transformer Engine FP8 Primer)
所以 FP8 不是“单一格式替代一切”,而更像是一整套数值系统设计:
- 低位宽表示;
- 动态缩放;
- 更高精度累积;
- 针对不同张量选择不同编码。
这也是为什么 FP8 虽然很省,但实现复杂度也明显高于 BF16 / FP16。
几个最常见的坑
1. 上溢和下溢
这是低精度训练里最常见、也最直接的问题。
- 太大了会变成
inf; - 太小了会变成 0。
FP16 更容易遇到这个问题;BF16 较少;FP8 则高度依赖 scaling。(Mixed Precision Training, A Study of BFLOAT16 for Deep Learning Training, FP8 Formats for Deep Learning)
2. reduction 误差
像求和、均值、方差、softmax 分母这类操作,如果直接在很低精度上做,误差可能会明显累积。
所以实际实现里,这类操作往往会在更高精度上做累加。
3. optimizer 不稳定
如果参数更新本身就在低精度上完成,尤其是学习率很小、更新幅度也很小时,更新可能直接被舍入掉。
这也是为什么 FP32 master weights 经常很重要。(Mixed Precision Training)
4. “能跑”和“训得好”不是一回事
有时模型在低精度下前几个 step 看起来没问题,但训练到后面质量会慢慢掉,原因可能是:
- 尾数精度不够;
- scaling 不合适;
- 某些张量用错了格式;
- 某些敏感算子没有提升精度。
所以低精度训练要看的不只是 loss 有没有 NaN,还要看最终质量是否稳定。
总结
FP16、BF16、FP8 的区别,本质上是用不同的 bit 分配,在“范围、精度、速度、显存”之间做工程权衡。
在深度学习里,FP32、FP16、BF16、FP8 不是简单的“位数越来越小”。它们背后真正的差别,是指数位和尾数位怎么分,从而决定动态范围和数值精度怎么取舍。
FP16 用更少的指数位换来了相对更细的尾数,因此更容易受范围限制;BF16 则把指数位做得和 FP32 一样大,所以更不容易出现数值爆炸或消失;FP8 更进一步压缩位宽,但必须配合 scaling、格式分工和更高精度累积才能真正稳定工作。
对于训练系统来说,选择哪种精度,本质上是在平衡速度、显存、带宽和数值稳定性,而这也是现代 mixed precision 训练的核心。(Mixed Precision Training, FP8 Formats for Deep Learning, A Study of BFLOAT16 for Deep Learning Training)