BLOG

Record, summarize, and improve.

浮点数

浮点数标准

直到1985年,IEEE 组织推出了浮点数标准,就是我们经常听到的 IEEE754 浮点数标准,这个标准统一了浮点数的表示形式,并提供了 2 种浮点格式:

  • 单精度浮点数 float:32 位,符号位 S 占 1 bit,指数 E 占 8 bit,尾数 M 占 23 bit
  • 双精度浮点数 float:64 位,符号位 S 占 1 bit,指数 E 占 11 bit,尾数 M 占 52 bit

为了使其表示的数字范围、精度最大化,浮点数标准还对指数和尾数进行了规定:

  1. 尾数 M 的第一位总是 1(因为 1 <= M < 2),因此这个 1 可以省略不写,它是个隐藏位,这样单精度 23 位尾数可以表示了 24 位有效数字,双精度 52 位尾数可以表示 53 位有效数字
  2. 指数 E 是个无符号整数,表示 float 时,一共占 8 bit,所以它的取值范围为 0 ~ 255。但因为指数可以是负的,所以规定在存入 E 时在它原本的值加上一个中间数 127,这样 E 的取值范围为 -127 ~ 128。表示 double 时,一共占 11 bit,存入 E 时加上中间数 1023,这样取值范围为 -1023 ~ 1024。

除了规定尾数和指数位,还做了以下规定:

  • 指数 E 非全 0 且非全 1:规格化数字,按上面的规则正常计算
  • 指数 E 全 0,尾数非 0:非规格化数,尾数隐藏位不再是 1,而是 0(M = 0.xxxxx),这样可以表示 0 和很小的数
  • 指数 E 全 1,尾数全 0:正无穷大/负无穷大(正负取决于 S 符号位)
  • 指数 E 全 1,尾数非 0:NaN(Not a Number)
Image in a image block

在深度学习的背景下的浮点格式

FP80

x86 实现通常已知的80 位IEEE 754 扩展精度二进制浮点格式始于 Intel 8087 数学协处理器(过去的美好时光,CPU 不支持浮点计算,FPU 是一个单独的协处理器)。在此实现中,它包含:

  • 1位符号
  • 15 位指数
  • 64 位小数
Image in a image block

图片来自维基百科

范围: ~3.65e−4951 至~1.18e4932,精度约为 18 位有效数字。

用法:

  • 该格式用于对精度要求较高的科学计算。
  • 不用于 DL 计算。

软件支持:

  • 许多(但不是全部)C/C++ 编译器long double使用这种 80 位(10 字节)格式实现。
  • 通常在 DL 框架中不受支持。

硬件支持:

  • x86 CPU(其 x87 指令子集)原生支持。默认情况下,x87 处理器在内部都使用 80 位双扩展精度。
  • NVIDIA GPU 不支持。
FP64

64 位浮点,通常是IEEE 754 双精度二进制浮点格式,具有:

  • 1位符号
  • 11 位指数
  • 52 位小数
Image in a image block

图片来自维基百科

范围: ~2.23e-308 … ~1.80e308,具有完整的 15-17 位十进制数字精度。

用法:

  • 该格式用于对精度要求较高的科学计算。
  • 通常不用于 DL 计算。

软件支持:

  • 在大多数 C/C++ 系统上表示double类型。
  • TensorFlow(如tf.float64)/ PyTorch(如torch.float64torch.double)中受支持。

硬件支持:

  • 通常在 x86 CPU 中受支持。
  • 大多数 GPU,尤其是包括 RTX 系列在内的游戏 GPU,FP64 性能受到严重限制(通常是 FP32 性能的 1/32,而不是 1/2,有关更多详细信息,请参阅有关 GPU 的帖子)。
  • 最近不受限制的 FP64 支持的 GPU 包括Tesla P100/P40/P4 和 Quadro GP100 中的GP100/102/104GV100和GA100, Tesla V100/Quadro GV100/Titan V 中的最近发布的 A100中的(有趣的是,新的 Ampere 架构具有第 3 代支持 FP64 的张量核心,A100 张量核心现在包括新的符合 IEEE 标准的 FP64 处理,提供 2.5 倍于 V100 的 FP64 性能)。
FP32

长期以来,这种格式一直是深度学习的主力。另一种 IEEE 754 格式,单精度浮点数:

  • 1位符号
  • 8 位指数
  • 23 位小数
Image in a image block

图片来自维基百科

范围: ~1.18e-38 … ~3.40e38,精度为 6-9 位有效小数位。

用法:

  • 长期以来神经网络计算的标准类型。神经网络中的权重、激活和其他值长期以来一直默认在 FP32 中表示。
  • 对于许多科学计算(尤其是迭代计算),精度不够,导致错误累积。

软件支持:

  • 在大多数 C/C++ 系统上表示float类型。
  • TensorFlow(如tf.float32)/ PyTorch(如torch.float32torch.float)中受支持。

硬件支持:

  • 通常在 x86 CPU 中受支持。
  • 通常在 NVIDIA/AMD GPU 中受支持。

FP16

同样,IEEE 754 标准格式,半精度浮点格式,具有:

  • 1位符号
  • 5 位指数
  • 10 位小数
Image in a image block

图片来自维基百科

范围: ~5.96e−8 (6.10e−5) … 65504,精度为 4 位有效小数位。

用法:

  • 深度学习有使用 FP16而不是 FP32 的趋势,因为较低精度的计算似乎对神经网络来说并不重要。额外的精度没有任何意义,虽然速度较慢,但会占用更多内存并降低通信速度。
  • 可用于训练,通常使用混合精度训练(TensorFlow / PyTorch)。
  • 可用于训练后量化以加快推理速度 ( TensorFlow Lite )。用于训练后量化的其他格式是整数INT8(8 位整数)、INT4(4 位)甚至INT1(二进制值)。

软件支持:

硬件支持:

有用的链接:

BFLOAT16

另一种最初由 Google 开发的 16 位格式称为“ Brain Floating Point Format ”,简称“bfloat16”。这个名字来源于“谷歌大脑”,这是谷歌的一个人工智能研究小组,构思了这种格式的想法。

最初的 IEEE FP16 在设计时并未考虑深度学习应用,其动态范围太窄。BFLOAT16 解决了这个问题,提供与 FP32 相同的动态范围。

所以,BFLOAT16 有:

  • 1位符号
  • 8 位指数
  • 7 位小数
Image in a image block

图片来自维基百科

bfloat16 格式是截断的 IEEE 754 FP32,允许与 IEEE 754 FP32 快速转换。在转换为 bfloat16 格式时,指数位被保留,而有效数字字段可以通过截断来减少。

Image in a image block

来源

范围: ~1.18e-38 … ~3.40e38,带 3 个有效小数位。

用法:

  • 似乎现在正在取代 FP16。与通常需要通过损失缩放等技术进行特殊处理的 FP16 不同,BF16 在训练和运行深度神经网络时几乎可以直接替代 FP32。

软件支持:

  • 不在 C/C++ 标准中。可与特殊库一起使用。
  • TensorFlow(如tf.bfloat16)/ PyTorch(如torch.bfloat16)中受支持。

硬件支持:

TF32

TensorFloat-32或 TF32 是NVIDIA A100 GPU中的新数学模式。

TF32 使用与半精度 (FP16) 数学相同的 10 位尾数,显示出对 AI 工作负载的精度要求有足够的余量。并且TF32采用与FP32相同的8位指数,因此可以支持相同的数值范围。

它在技术上是一种 19 位格式。您可以将其视为扩展精度 BFLOAT16,比如“BFLOAT19” ☺ 或类似于降低精度的 FP32。

所以,TF32 有:

  • 1位符号
  • 8 位指数
  • 10 位小数
Image in a image block

图片来自 NVIDIA 博文

TF32的优点是格式和FP32一样。使用 TF32 计算内积时,输入操作数的尾数从 23 位四舍五入为 10 位。舍入操作数精确相乘,并在普通 FP32 中累加。

TF32 Tensor Cores 在 FP32 输入上运行并在 FP32 中产生结果,无需更改代码。非矩阵运算继续使用 FP32。这提供了一种在 DL 框架和 HPC 中加速 FP32 输入/输出数据的简单途径。

范围: ~1.18e-38 … ~3.40e38,精度为 4 位有效小数位。

用法:

  • TF32 的一大优势是编译器支持仅在最深层次上需要,即在 CUDA 编译器内部。其余代码只看到精度较低但动态范围相同的 FP32。利用 TF32 主要是调整库的调用者以指示 TF32 是否正常。TF32 作为一种可以快速插入以利用 Tensor Core 速度而无需太多工作的东西而存在。
  • FP16 和 BFLOAT16 等格式需要更多工作,因为它们涉及不同的位布局。值得努力使用这些格式,因为它们减少了内存带宽并因此允许更快的执行。(来源

作为比较,A100 的峰值性能是:

  • 没有张量核心的 FP32:19.5 TFLOPS
  • TF32 张量核心:156 TFLOPS(因此,使用 TF32 代替 FP32 可以轻松提高速度)。
  • FP16/BF16 张量核心:312 TFLOPS(因此,精心设计的 FP16/BF16 切换可以为您提供更多速度提升,但成本更高)。

软件支持:

  • 不在 C/C++ 标准中。
  • 在CUDA 11中受支持。

硬件支持:

有用的链接:

标准浮点数的表示

有了这个统一的浮点数标准,我们再把 25.125 转换为标准的 float 浮点数:

  1. 整数部分:25(D) = 11001(B)
  2. 小数部分:0.125(D) = 0.001(B)
  3. 用二进制科学计数法表示:25.125(D) = 11001.001(B) = 1.1001001 * 2^4(B)

所以 S = 0,尾数 M = 1.001001 = 001001(去掉1,隐藏位),指数 E = 4 + 127(中间数) = 135(D) = 10000111(B)。填充到 32 bit 中,如下:

Image in a image block

这就是标准 32 位浮点数的结果。

如果用 double 表示,和这个规则类似,指数位 E 用 11 bit 填充,尾数位 M 用 52 bit 填充即可。

编码技巧

NaN-boxing

NaN-boxing 是一种用于优化指针表示的技术,常用于 RISC-V 架构中。在 RISC-V 中,指针需要使用 64 位来表示,但是很多指针的位数实际上并不需要使用全部的 64 位。NaN-boxing 利用 NaN(非数)的特性来节省空间,将指针值存储在 NaN 的位表示中。

具体而言,NaN-boxing 将指针的有效位数存储在 NaN 的指数位中,而尾数位则用于标识该值是一个 NaN。由于 NaN 的指数位总是全部置为 1,因此可以利用未使用的 NaN 指数位存储指针的有效位数。这样就可以将指针值编码在 NaN 中,同时通过解码操作来还原指针。

NaN-boxing 技术的优势在于可以节省内存空间,因为指针的有效位数不需要占满整个 64 位。此外,由于 NaN 是浮点数中的特殊值,所以对于指针的解码操作可以直接利用浮点数的操作指令,而无需额外的指令。

NaN-boxing 在 RISC-V 中的具体实现可以参考相关文档和源代码。它是一种优化技术,可以提高指针的存储效率和访问速度,但需要在编译器和运行时系统中进行相应的支持和处理。

zfinx

zfinx(Zero, Flag, Infinity, NaN, and Exception)是指在浮点数运算中,关于零、标志位、无穷大、非数和异常等方面的处理。这个术语通常在处理浮点数时涉及到对这些特殊情况的检测和处理。

具体来说,zfinx 表示了以下几个方面:

  • Zero(零):判断浮点数是否为零,包括正零和负零。在浮点数运算中,需要注意对零的特殊处理,例如除法中的除以零异常。
  • Flag(标志位):检查浮点数运算过程中的标志位,包括溢出、下溢、无穷大、非数等情况。这些标志位可以用来判断运算结果的特殊状态,并进行相应的处理。
  • Infinity(无穷大):判断浮点数是否为无穷大,包括正无穷大和负无穷大。在浮点数运算中,当某个操作结果超出了浮点数表示范围时,就会得到无穷大。
  • NaN(非数):判断浮点数是否为非数(NaN),即不是一个有效的数值。NaN 在浮点数运算中通常表示无效的操作或不可确定的结果。
  • Exception(异常):处理浮点数运算中可能发生的异常情况,例如除以零、溢出等。对于这些异常情况,需要进行适当的处理或抛出异常。

zfinx 的概念在浮点数运算中非常重要,它涉及到对特殊情况的判断和处理,以确保浮点数运算的正确性和可靠性。在编程中,需要根据具体的编程语言和平台提供的相关函数和指令来进行 zfinx 相关操作。

FP32补充

1. fp32 的基本格式

单精度浮点(fp32)一共 32 bit

  • 1 bit:符号位 S
  • 8 bit:指数 E
  • 23 bit:尾数(小数部分)F

整体存储形式:

S | EEEEEEEE | FFFFFFFFFFFFFFFFFFFFFFF

实际数值(非特殊情况)是:

(1)S×2E127×(1.F)(−1)S×2^{E-127}×(1.F)

其中 1.F 表示前面有个隐含的 1,再加上 23 位小数。


2. 几类“特殊类型数据”

2.1 ±0(正零 / 负零)
  • 条件:E = 0F = 0
  • 数值:+0 或 -0
  • 区分正/负:看符号位 S
    • S=0 → +0
    • S=1 → -0

虽然数值看起来一样,但在某些运算里(比如 1/+0 和 1/-0)会有区别。


2.2 非规约数 / 次正规数(Subnormal / Denormal)
  • 条件:E = 0F ≠ 0
  • 数值:

(−1)S×21−127×(0.F)(-1)^S \times 2^{1-127} \times (0.F)

(−1)S×21−127×(0.F)

注意这里前面是 0.F没有隐含的 1

它们用来表示非常接近 0 的小数,使得精度在接近 0 时“不断层”。

调试时你看到 exp=0frac!=0 就是 subnormal。


2.3 ±∞(正无穷 / 负无穷)
  • 条件:E = 255 (0xFF)F = 0
  • 数值:+∞ 或 -∞

如:

  • 0x7F800000 → +inf
  • 0xFF800000 → -inf

常见产生场景:除零、溢出等。


2.4 NaN(Not a Number)
  • 条件:E = 255F ≠ 0
  • 表示“不是一个有效数”,比如 0/0inf - inf 等。

NaN 又分两类(IEEE 标准里的概念):

  1. Quiet NaN(qNaN,静默 NaN)
    • 一般最高的 frac bit(即 F 的最高 bit)= 1
    • 参与运算后会“静默传播”,不会触发异常,结果往往仍然是 NaN。
  2. Signaling NaN(sNaN,信号 NaN)
    • 通常最高的 frac bit = 0,且 frac 其他 bit ≠ 0
    • 用来表示“无效数据”,一旦参与运算就应该触发异常(在支持的实现里)。

在很多实际实现(包括不少 CPU / 模拟器)里,基本只用 qNaN,sNaN 处理比较简化,所以调试时看到 NaN,大部分其实是 qNaN。


3. 普通规范化数(Normalized)

除了上面几类,剩下就是“普通浮点数”:

  • 条件:1 ≤ E ≤ 254
  • 数值:

(−1)S×2E−127×(1.F)(-1)^S \times 2^{E - 127} \times (1.F)

(−1)S×2E−127×(1.F)

这就是我们日常理解的 “正常浮点数”。


4. 调试时如何快速判断

假设你拿到的是一个 uint32_t bits

uint32_t bits = ...;
uint32_t s = bits >> 31;
uint32_t e = (bits >> 23) & 0xFF;
uint32_t f = bits & 0x7FFFFF;

然后:

  • e == 0 && f == 0 → ±0
  • e == 0 && f != 0 → subnormal
  • e == 0xFF && f == 0 → ±inf
  • e == 0xFF && f != 0 → NaN