Skip to content

数值精度 ​

FP8 Formats for Deep Learning

Mixed Precision Training

A Study of BFLOAT16 for Deep Learning Training

NVIDIA Mixed Precision Training Guide

Intel BF16 Hardware Numerics Definition

NVIDIA Transformer Engine FP8 Primer

一个数,到底用多少 bit 表示;这些 bit 又要怎么在“范围”和“精度”之间分配。

很多初学者会把“位数更小”简单理解成“更差”,但真正的情况不是这样。

更小的精度通常意味着:

  • 显存占用更低;
  • 带宽压力更小;
  • 矩阵乘吞吐更高;
  • 训练和推理速度更快。

但代价也很明确:

  • 可表示的数值范围可能变小;
  • 小数精度会下降;
  • 更容易出现上溢、下溢和舍入误差;
  • 训练会更依赖混合精度和缩放技巧。

不同精度的区别,本质上就是在“动态范围”和“有效精度”之间做权衡。

浮点数到底在表示什么 ​

一个二进制浮点数通常由三部分组成:

  • sign:符号位;
  • exponent:指数位;
  • fraction / mantissa:尾数位。

对一个归一化的二进制浮点数,它的值通常可以写成:

(−1)s×2e−bias×(1.f)

这里:

  • s 决定正负;
  • e 决定数量级,也就是“范围”;
  • f 决定小数部分的精细程度,也就是“精度”。

这就解释了为什么指数位和尾数位都重要,但它们负责的不是同一件事。

指数位多意味着什么 ​

指数位更多,意味着能表示的数量级更广。

也就是说,更大的正数、更小的正数,都更不容易溢出或下溢。

这通常被叫做 动态范围。

尾数位多意味着什么 ​

尾数位更多,意味着相邻两个可表示数之间的间距更小。

也就是说,在同样数量级附近,你能表达得更细。

这通常被理解成 数值精度。

所以:

  • 想让数“不要太容易炸掉”,要看指数位;
  • 想让数“表达得更细”,要看尾数位。

为什么训练里会越来越常见低精度 ​

深度学习里,矩阵乘和张量读写非常多。位宽一旦减半,通常会同时带来两类收益:

第一类是 存储收益。例如同样一块显存,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;
  • 可训练的模型规模。

常见格式 ​

格式总位数符号位指数位尾数位(显式)典型特点
FP32321823范围和精度都比较均衡,是传统训练基线
FP16161510尾数比 BF16 多,但指数更少,范围更窄
BF1616187指数和 FP32 一样,范围大,但精度比 FP16 粗
FP8 E4M38143精度稍高于 E5M2,但范围更小
FP8 E5M28152范围更大,常更适合梯度等更容易爆的张量

其中:

一般来说:

  • 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,位分配是:

这个格式的特点是:

  • 和 FP32 比,位宽减半;
  • 小数精度并不算太差;
  • 但指数位只有 5 位,所以动态范围明显更小。

这会直接带来一个训练里的典型问题:

梯度很小的时候容易下溢成 0,激活或中间量很大时又更容易溢出。

Micikevicius 等人的 mixed precision training 论文把 FP16 训练常见的三个关键技巧总结得很清楚:

  1. 使用 FP32 master weights;
  2. 在矩阵乘等关键运算里做更高精度累积;
  3. 使用 loss scaling,避免小梯度在 FP16 下直接下溢。(Mixed Precision Training)

为什么 FP16 常需要 loss scaling ​

假设某个梯度本来就很小,写成 FP16 后可能直接变成 0。那一旦变成 0,后面的优化器就再也看不到这个梯度了。

所以常见做法是把 loss 先乘一个缩放因子 s:

L′=sL

这样反向传播得到的梯度也会整体放大:

∇θL′=s∇θL

等到梯度传回优化器前,再除回去:

∇θL=1s∇θL′

这样做的目的不是改变训练目标,而是让中间梯度在低精度里更不容易被抹成 0。NVIDIA 的 mixed precision 文档和论文都明确把 loss scaling 作为 FP16 训练的核心技巧之一。(Mixed Precision Training, NVIDIA Mixed Precision Training Guide)

FP16 的一句话总结 ​

FP16 的核心问题不是位数少,而是指数太少,所以范围偏小。

这就是为什么它虽然快,但训练时往往比 BF16 更需要小心处理数值稳定性。

BF16 ​

BF16 也是 16-bit,但它和 FP16 的设计取舍完全不同。

它通常是:

最关键的一点是:

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 浮点格式:

为什么要两个 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 就完了”。实际使用时往往更细:

所以 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)