浮点数基础: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,它是浮点数天生的特性。我总结了三个主要来源:

  1. 舍入误差:比如 0.1 在二进制里是无限循环小数,存的时候只能截断。你想想看,每次运算都截断一点,累积起来误差就大了。
  2. 对消误差:两个非常接近的数相减,有效位数会大量丢失。比如 1.0000001 - 1.0 = 0.0000001,原本 7 位有效数字,现在只剩 1 位了。
  3. 溢出与下溢:数太大变成 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 行为是否一致。

知识体系结构图

下面这张图,帮你理清浮点数基础的核心逻辑:

浮点数基础核心知识体系 IEEE 754 标准 符号位 + 指数 + 尾数 单精度 vs 双精度 特殊值:NaN / Inf 舍入误差 对消误差 溢出与下溢 精度对齐实践:逐层对比 + 中间结果 dump 建议:用 float64 做 golden,float32 做对比

这张图把浮点数基础串起来了。从上到下,从标准到实践,每一步都跟算子精度对齐直接相关。我个人建议你把它存下来,做对齐时对照着看,思路会清晰很多。

小技巧:写算子时,如果发现精度对不上,先检查是不是浮点误差。用 double 跑一遍,如果结果一致,那就是浮点问题。如果不一致,那就是逻辑 bug。

好了,浮点数基础就聊到这儿。记住一句话:浮点误差不是 bug,但忽略它一定会出 bug。做算子对齐时,心里时刻装着 IEEE 754,能少踩很多坑。