精度对齐基础概念:浮点数表示与误差分析
做算子精度对齐,说白了就是跟浮点数打交道。你想想看,我们在芯片上跑一个模型,算出来的结果跟CPU上的参考结果差了几个比特,这到底能不能接受?要回答这个问题,得先搞明白浮点数是怎么存、怎么算的。
我个人习惯把浮点数理解成「用有限比特去近似无限实数」的一种妥协方案。既然是近似,那误差就不可避免。我们的目标不是消除误差——那不可能——而是把误差控制在可接受的范围内。
FP32 / FP16 / BF16:三种主流格式
先看一张图,这是我做精度分析时脑子里常浮现的结构:
这张图我每次培训都会放。你看,FP32有8位指数、23位尾数,动态范围大,精度也高。FP16只有5位指数、10位尾数,动态范围小很多。BF16呢?它保留了8位指数,但尾数砍到7位。
为什么会有BF16这种看起来「头重脚轻」的设计?
我在项目中遇到过这样的情况:用FP16训练一个大模型,梯度直接下溢到零——指数范围不够。换成BF16,指数跟FP32一样宽,动态范围保住了,虽然尾数精度差一点,但训练反而能收敛。这就是BF16存在的意义。
| 格式 | 总位数 | 指数位 | 尾数位 | 最大正数 | 最小正数 |
|---|---|---|---|---|---|
| FP32 | 32 | 8 | 23 | ~3.4e38 | ~1.4e-45 |
| FP16 | 16 | 5 | 10 | 65504 | ~5.96e-8 |
| BF16 | 16 | 8 | 7 | ~3.4e38 | ~1.2e-38 |
舍入模式:不是简单的四舍五入
浮点数运算后,结果往往不能精确表示,这时候就需要舍入。很多人以为就是四舍五入,其实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
精度对齐的实用流程
说了这么多理论,来点实际的。我做精度对齐时,一般按这个流程走:
- 确认数据类型:双方用的是FP32、FP16还是BF16?舍入模式是什么?
- 逐层比对:从输入开始,每算一层就比对一次,定位误差来源
- 分析误差模式:是随机误差还是系统性偏差?随机误差通常可接受,系统性偏差要追查
- 计算ULP和相对误差:两个指标一起看,别只看一个
- 判断是否可接受:根据业务场景定阈值,没有一刀切的标准
嗯,浮点数表示和精度分析,说白了就是这些内容。理解了这个基础,后面讲具体的算子映射和调试方法时,你就能明白为什么有些误差是「正常」的,有些则是「有问题」的。
想要更多资料,联系可可