浮点数基础:IEEE 754标准、单精度与双精度、浮点误差来源
做算子精度对齐,说白了就是在跟浮点数打交道。你想想看,我们在芯片上算的每一个数,最终都要落到二进制里。但现实世界里的实数,比如 0.1、π、√2,在二进制里根本没法精确表示。这就引出了我们今天要聊的核心——IEEE 754 标准。
我记得刚入行那会儿,有一次写一个卷积算子,结果跟 CPU 结果对不上。排查了三天,最后发现是浮点舍入方式不一样。嗯,从那以后,我再也不敢小看浮点数标准了。
IEEE 754 标准是什么?
IEEE 754 是浮点数在计算机里存储和计算的国际标准。它规定了三样东西:符号位、指数位、尾数位。说白了,就是把一个数拆成三部分来存。
公式长这样:
(-1)^符号位 × 1.尾数 × 2^(指数 - 偏移量)
这个「1.尾数」是隐含的,标准里叫「隐藏位」。为什么能隐藏?因为二进制规格化数的小数点前一定是 1,所以干脆不存了,省一位精度。这个小技巧,我在做低比特量化时经常用到。
单精度与双精度
我们平时说的 float32 和 float64,就是单精度和双精度。它们的区别,我直接给你列个表:
| 类型 | 总位数 | 符号位 | 指数位 | 尾数位 | 指数偏移量 | 大约精度 |
|---|---|---|---|---|---|---|
| 单精度 (float32) | 32 | 1 | 8 | 23 | 127 | 7位十进制 |
| 双精度 (float64) | 64 | 1 | 11 | 52 | 1023 | 16位十进制 |
单精度指数范围是 -126 到 127,双精度是 -1022 到 1023。超出这个范围,就会溢出成 Inf 或 0。我在做算子对齐时,经常遇到的一个坑就是:中间结果溢出。比如两个大数相乘,结果超出了 float32 的范围,直接变成 Inf,后面再怎么算都是 Inf。
核心要点:算子精度对齐时,尽量用双精度做参考。我个人的习惯是:先用 float64 跑一遍 golden 数据,再用 float32 跑算子结果,两者对比。如果误差在 1e-5 以内,基本可以接受。
浮点误差的来源
浮点误差不是 bug,它是浮点数天生的特性。我总结了三个主要来源:
- 舍入误差:比如 0.1 在二进制里是无限循环小数,存的时候只能截断。你想想看,每次运算都截断一点,累积起来误差就大了。
- 对消误差:两个非常接近的数相减,有效位数会大量丢失。比如 1.0000001 - 1.0 = 0.0000001,原本 7 位有效数字,现在只剩 1 位了。
- 溢出与下溢:数太大变成 Inf,数太小变成 0。这在做 softmax 或 sigmoid 时特别常见。
我曾经在做一个 LayerNorm 算子时,发现输出结果跟 PyTorch 对不上。排查到最后,发现是求和时累加顺序不同导致的。PyTorch 用的是树状求和,我用的顺序求和,误差就差了 1e-6。嗯,这就是典型的舍入误差累积。
避坑指南:做精度对齐时,不要只看最终结果。我建议你把中间结果也 dump 出来,逐层对比。我曾经因为没检查中间结果,浪费了两天时间。
特殊值:NaN 和 Inf
IEEE 754 还定义了一些特殊值:
- NaN:0/0、∞-∞、sqrt(-1) 的结果。任何运算遇到 NaN,结果都是 NaN。
- +Inf / -Inf:1/0 的结果,或者溢出时出现。
- ±0:有正零和负零,但比较时相等。
我在算子开发中,最怕的就是 NaN 传播。一个地方出了 NaN,后面全白算。所以我的习惯是:在关键节点检查数值是否合法,比如 softmax 之前检查输入是否有 NaN。
警告:不同硬件对 NaN 的处理可能不同。有些硬件会静默处理 NaN,有些会触发异常。做跨平台对齐时,一定要确认 NaN 行为是否一致。
知识体系结构图
下面这张图,帮你理清浮点数基础的核心逻辑:
这张图把浮点数基础串起来了。从上到下,从标准到实践,每一步都跟算子精度对齐直接相关。我个人建议你把它存下来,做对齐时对照着看,思路会清晰很多。
小技巧:写算子时,如果发现精度对不上,先检查是不是浮点误差。用 double 跑一遍,如果结果一致,那就是浮点问题。如果不一致,那就是逻辑 bug。
好了,浮点数基础就聊到这儿。记住一句话:浮点误差不是 bug,但忽略它一定会出 bug。做算子对齐时,心里时刻装着 IEEE 754,能少踩很多坑。