精度对齐基础概念:浮点数表示与误差分析

做算子精度对齐,说白了就是跟浮点数打交道。你想想看,我们在芯片上跑一个模型,算出来的结果跟CPU上的参考结果差了几个比特,这到底能不能接受?要回答这个问题,得先搞明白浮点数是怎么存、怎么算的。

我个人习惯把浮点数理解成「用有限比特去近似无限实数」的一种妥协方案。既然是近似,那误差就不可避免。我们的目标不是消除误差——那不可能——而是把误差控制在可接受的范围内。

FP32 / FP16 / BF16:三种主流格式

先看一张图,这是我做精度分析时脑子里常浮现的结构:

三种浮点数格式对比 FP32 S E (8 bit) M (23 bit) 总32位 FP16 S E (5 bit) M (10 bit) 总16位 BF16 S E (8 bit) M (7 bit) 总16位 S=符号位 E=指数位 M=尾数位

这张图我每次培训都会放。你看,FP32有8位指数、23位尾数,动态范围大,精度也高。FP16只有5位指数、10位尾数,动态范围小很多。BF16呢?它保留了8位指数,但尾数砍到7位。

为什么会有BF16这种看起来「头重脚轻」的设计?

我在项目中遇到过这样的情况:用FP16训练一个大模型,梯度直接下溢到零——指数范围不够。换成BF16,指数跟FP32一样宽,动态范围保住了,虽然尾数精度差一点,但训练反而能收敛。这就是BF16存在的意义。

格式总位数指数位尾数位最大正数最小正数
FP3232823~3.4e38~1.4e-45
FP161651065504~5.96e-8
BF161687~3.4e38~1.2e-38
关键结论: BF16和FP32的动态范围相同,但精度只有约7-8位有效数字。FP16动态范围窄,但精度约10-11位有效数字。选哪个,看你的场景更缺「范围」还是更缺「精度」。

舍入模式:不是简单的四舍五入

浮点数运算后,结果往往不能精确表示,这时候就需要舍入。很多人以为就是四舍五入,其实IEEE 754标准定义了多种舍入模式。

我最早做算子开发时,就踩过舍入模式的坑。当时一个卷积算子在芯片上跑的结果跟CPU对不上,查了三天,最后发现是芯片默认用了「向零舍入」,而CPU用的是「最近舍入」。差之毫厘,谬以千里。

常见的舍入模式有四种:

  • 最近舍入(Round to Nearest, ties to even):默认模式。舍入到最近的可表示值,恰好在中间时舍入到偶数。比如1.5舍入到2.0,2.5也舍入到2.0。
  • 向零舍入(Round toward Zero):直接截断多余位。正数向下,负数向上。
  • 向上舍入(Round toward +∞):结果往正无穷方向取。
  • 向下舍入(Round toward -∞):结果往负无穷方向取。
避坑指南: 我曾经在写量化工具时,默认用了向零舍入,结果精度评估一直不过。后来发现,大多数深度学习框架的参考实现用的是最近舍入。所以做精度对齐时,第一件事就是确认双方的舍入模式是否一致。

数值稳定性:为什么同样的公式结果不同?

数值稳定性是个容易被忽视的问题。同样的数学公式,不同的计算顺序,结果可能差很多。

举个例子,计算方差:E[X²] - (E[X])² 和 Σ(xᵢ - μ)² / n,数学上等价,但数值上完全不同。

我在项目中遇到过:用第一种公式算方差,当数据量很大且数值接近时,两个大数相减,有效数字全丢了。换成第二种公式,先减均值再平方,稳定性好得多。

我的建议: 做算子精度对齐时,如果发现误差异常大,先别急着怀疑硬件。检查一下你的参考实现是不是用了数值不稳定的算法。有时候换个计算顺序,误差就降下来了。

ULP与相对误差:两个核心度量

ULP(Unit in the Last Place)和相对误差,是精度对齐中最常用的两个指标。

ULP是什么? 说白了,就是两个相邻浮点数之间的距离。比如FP32中,1.0的ULP大约是1.19e-7。如果两个结果差1个ULP,说明只在最后一位上不同。

相对误差呢? 就是绝对误差除以真实值。比如真实值是100,你算出来是100.01,相对误差就是0.01%。

我个人习惯这样用:

  • 数值较大时(比如>1.0),看相对误差更直观
  • 数值接近零时,看ULP更合理——因为相对误差会爆炸
  • 做精度对齐时,我一般要求ULP误差不超过2,相对误差不超过1e-5
经验之谈: 我曾经调试过一个LayerNorm算子,相对误差一直在1e-3量级,怎么都降不下去。后来用ULP一看,发现大部分误差只有1-2个ULP,只是数值本身很小导致相对误差看起来大。最终结论:精度合格,虚惊一场。

精度对齐的实用流程

说了这么多理论,来点实际的。我做精度对齐时,一般按这个流程走:

  1. 确认数据类型:双方用的是FP32、FP16还是BF16?舍入模式是什么?
  2. 逐层比对:从输入开始,每算一层就比对一次,定位误差来源
  3. 分析误差模式:是随机误差还是系统性偏差?随机误差通常可接受,系统性偏差要追查
  4. 计算ULP和相对误差:两个指标一起看,别只看一个
  5. 判断是否可接受:根据业务场景定阈值,没有一刀切的标准
一个小技巧: 我习惯在算子中加一个调试模式,把中间结果全部dump出来。这样一旦精度对不上,可以直接定位到是哪一步出了问题,不用从头猜。

嗯,浮点数表示和精度分析,说白了就是这些内容。理解了这个基础,后面讲具体的算子映射和调试方法时,你就能明白为什么有些误差是「正常」的,有些则是「有问题」的。

一句话总结: 浮点数精度对齐,不是追求「一模一样」,而是追求「误差可控」。搞清楚你的数据格式、舍入模式、误差度量,你就掌握了精度对齐的主动权。

想要更多资料,联系可可