Skip to content

14. FP32、FP16、BF16 与 FP8 ​

第 3 篇估算显存时,用过“每个参数占 2 字节”。为什么是 2 字节,也能用 4 字节或 1 字节吗?这篇就从保存一个小数开始,认识 FP32、FP16 和 BF16,再看位数继续减少时会发生什么。

先想象要把温度 23.678 记进一张表格。如果只允许保留一位小数,就会写成 23.7,少保存了一些细节。模型里的数也有类似的取舍,只是计算机用二进制编码,要在有限位数中同时安排数值范围和细节。

32 位和 16 位,差在哪里 ​

计算机的一位,也叫 bit,只能取 0 或 1;8 位组成 1 字节。FP32 用 32 位保存一个浮点数,所以每个数占 4 字节;FP16 和 BF16 都用 16 位,所以各占 2 字节。FP 是 Floating Point 的缩写,意思是浮点。

如果有 70 亿个参数,采用 FP32 时原始参数约占 28 GB,采用 FP16 或 BF16 时约占 14 GB。只改变保存方式,就能让这部分空间减半。这也是人们关心低精度的一个直接原因。

代价在哪里?位数减少以后,可表示的数字组合也减少了,一些原来可以分开的值会落到同一个刻度上。比如 1.0351 在 FP16 中会舍入到 1.03515625,在 BF16 中则舍入到 1.03125。两者都占 16 位,细节却有差别,原因要从这 16 位怎样分工说起。

浮点数由三段组成 ​

先看熟悉的科学计数法:1234.5 = +1.2345 × 10^3。正负号决定方向,指数 3 决定数量级,1.2345 决定这个数量级内的细节。二进制浮点使用相似思路,把一组位分给符号、指数和尾数。

符号位记录正负,指数位主要决定能覆盖多大的数量级,尾数位主要决定同一数量级内能分多细。总位数有限,多分给其中一部分,就要从另一部分省出来。FP16 用 5 位指数、10 位尾数;BF16 用 8 位指数、7 位尾数,两者还各有 1 位符号。

因此,FP16 在 1 附近的刻度更细;BF16 给指数留的空间更大,能表示的数值范围更宽。它们分别把有限预算花在了不同地方。

各格式的位数与范围速查

表中的“有效位”包含规格化浮点数隐含的最高位;特殊值、次正规数和具体硬件实现会让边界略有差异。

格式位段(符号/指数/尾数)有效位最大有限值(约)每个数主要取舍
FP321 / 8 / 23243.4 × 10^384 字节范围和细节都较充足,成本高
FP161 / 5 / 10116.55 × 10^42 字节细节多于 BF16,范围较窄
BF161 / 8 / 783.4 × 10^382 字节范围接近 FP32,尾数更短
FP8 E4M31 / 4 / 34依实现约 4481 字节细节较少,通常需要 scale
FP8 E5M21 / 5 / 23依实现约 5.7 × 10^41 字节范围更大,细节更少
FP4 E2M11 / 2 / 12依实现和 scale0.5 字节只能依靠分组或块 scale 扩展范围

FP16 和 BF16 都占 16 位,但分配方式不同:FP16 把更多位给了尾数,BF16 保留了 FP32 的指数宽度。训练中出现大范围激活或梯度时,BF16 往往比 FP16 更不容易溢出;需要细粒度小数时,FP16 又可能更有优势。没有脱离模型和硬件的“绝对更高精度”。

同一个小数,会落在哪个刻度 ​

试着表示 1.0351。在 [1, 2) 这个区间里,FP16 相邻数相差 2^-10,BF16 相差 2^-7,FP8 E4M3 则相差 2^-3。每种格式都要把输入舍入到最近的可表示值,恰好居中时选择末位为偶数的一侧。

BF16:1.0351 → 1.03125;绝对误差 0.00385;[1, 2) 内间距 0.0078125

拖动输入值,可以看到同一格式的结果如何停在某个刻度上,再跳向下一个。FP32 的刻度在这个尺度下十分密集,画面抽样显示刻线;结果与误差仍按实际间距计算。这里观察的是 1 附近的细节,能表示多大的数则由指数位决定:FP16 尾数更多,却会在大约 65504 处到达有限值上限,BF16 的范围接近 FP32。

不同浮点格式的符号位、指数位和尾数位结构,以及它们在范围和细节上的取舍

进阶:FP32 输入与 TF32 计算模式

NVIDIA Ampere 及更新架构可以在矩阵乘法或卷积的特定路径中使用 TensorFloat-32。它读取 FP32 输入,但 Tensor Core 只使用较短的尾数参与计算,结果通常仍写回 FP32。它更像一种FP32 输入下的计算模式,而不是把模型权重改成一个叫 torch.tf32 的张量类型。

这解释了两个常见现象:

  • 内存统计仍显示 FP32,但矩阵乘法速度和数值结果与严格 IEEE FP32 不完全相同;
  • 单位矩阵、很小的差值或长归约可能暴露出误差,不能用“输入是 FP32”推断所有中间步骤都严格 FP32。

需要严格 FP32 时,应按当前 PyTorch 和 CUDA 文档设置对应的 fp32_precision,并把设置记录在基线中。不要把旧版 allow_tf32 开关和新接口混写成一套永久配置。

为什么 BF16 经常用于训练和推理 ​

模型计算中的数有时很大、有时很小,BF16 较宽的范围让它更容易容纳这些变化。如果中间结果超过格式能保存的范围,就会发生溢出,后续计算可能出现无穷大或无效结果。

另一种误差来自计算本身:许多乘积要不断相加,细小舍入可能随之积累。因此常见做法是让乘法使用较低精度,累加时保留更高精度。好比明细可以简记,总账要算细些。下一篇会用这件事解释混合精度。

排查数值异常时,先看哪些位置?

BF16 的大范围有助于稳定主干计算,归一化、softmax、概率、距离、阈值比较、分类头和后处理仍可能对误差敏感。出现 NaN、Inf、分数跨阈值翻转或语音质量异常时,先定位具体算子,再把敏感局部提升到 FP32。

FP8、FP4 还需要 scale ​

继续缩到 8 位或 4 位,每个数占的空间更小,可用刻度也更少。为了适应模型里大小不同的数,常见方法是把一组数共同缩放,并另外保存缩放因子(scale)。

例如,原始值 120 可以用低位数 1.2 配合缩放因子 100 来近似表示,读取时两者相乘恢复大小。这个十进制例子说明缩放关系;实际编码还会把 1.2 舍入到该格式允许的刻度,因此缩放以后仍有误差。

常见 FP8 有 E4M3、E5M2 两种记法,E 后的数字是指数位数,M 后的是尾数位数。E4M3 更侧重细节,E5M2 更侧重范围。FP4 的空间预算更紧,分组与缩放设计也更关键。

实际部署还要给格式配上适合硬件的计算程序。NVFP4、MXFP4 等方案的分组、缩放与计算路径见低位量化实践。

认识这些格式,先抓住两个问题:每个数占多少空间,以及省下空间时损失了哪些范围或细节。FP16 与 BF16 同样占 2 字节,前者在 1 附近刻度更细,后者范围更宽;继续降到 FP8、FP4,还需要配套缩放与硬件支持。

下一篇混合精度把这些格式放进训练和推理流程,解释为什么同一个模型会同时用到几种精度,以及 autocast 与 GradScaler 如何参与其中。

别急,先让缓存热一下。