浮点数值类型

date
Apr 6, 2025
slug
float_point_in_aiinfra
tags
AI Infra
Basic
summary
数值计算的主要挑战在于如何管理误差的传播 — James H. Wilkinson 数值分析之父
type
Post
标签
混合精度
精度
数值类型
状态
完成
描述
数值计算的主要挑战在于如何管理误差的传播
重要性
🌟🌟
关键字
status
Published

🥰总结

notion image
精度
符号位(sign)
指数位(exponent)
小数位(fraction)
总位数
FP64
1
11
52
64
FP32
1
8
23
32
TF32
1
8
10
19
BF16
1
8
7
16
FP16
1
5
10
16
FP8-E4M3
1
4
3
8
FP8-E5M2
1
5
2
8
FP4
1
2
1
4
 

⁉️问题

如果你能快速回答出如下问题, 说明你已经掌握相关内容, 就不需要阅读本文章啦
浮点数的表示方法?
 
为什么有这么多的浮点数类型
浮点数有哪些精度问题?
如何解决精度问题?
 

🧐内容

数值计算的主要挑战在于如何管理误差的传播 — James H. Wilkinson 数值分析之父

定点数

概念

是一种带小数点的数, 但小数点的位置是固定的

如何存储?

假如用4个字节, 存储182.3758624这个数, 一种最简单的办法: 把这4个字节进行划分,来存储不同部分:
notion image
那么换一个数:5963776.375, 还能存储吗?
不能, 因为这个这个存储方法存不了太大的数. 想要存储就要给整数多分一点位数:
notion image
这样就可以存储比较大的数了, 但是小数点后的精度却下降了
而且如果这两个数要做加法运算也比较麻烦, 因为整数部分和小数部分占得位数不一样

更统一的方式

如果我们使用科学计数法, 那么这两个数就统一了:
这样数字就可以分为符号位(+1), 尾数(1.823758624), 基数(10), 指数(2)四部分了.
这就是浮点数

浮点数

什么是浮点数?

是与定点数相对的概念, 用于表示非整数数据

浮点数的存储

C/C++及AI计算采用了国际标准IEEE 754, 任意一个二进制浮点数V可以表示为:
规定:
  • 表示符号位
  • M表示有效数字, , 由于1总是不变, 存储时省略, 读取时再加上
  • E表示指数, 是一个无符号整数. 这意味着,如果 E 为 8 位,它的取值范围为 0~255。然而科学计数法中的 E 是可 以出现负数的,所以 E 的真实值必须再减去一个中间数,对于 8 位的 E,这个中间数是 127; 比如,2^2 的E是2,所以保存成 float 32 位浮点数时,必须保存成 2+127=129,即 10000001。
  • E 不全为 0 或不全为 1。这时,浮点数就采用上面的规则表示,即指数 E 的计算值减去中间值,得到真实值,再将有效数字 M 前加上第一位的 1,最后计算出真实值
  • E 全为 0。这时,浮点数的指数: ,有效数字 M 不再加上 第一位的 1,而是还原为 0.xxxxxx 的小数。这样做是为了表示±0,以及接近于 0 的很小的数字。
  • E 全为 1。这时,如果有效数字 M 全为 0,表示±无穷大(正负取决于符号位 s);如果有效数字 M 不全为 0,表示这个数不是一个数(NaN)。

什么是精度?

浮点数据类型按理说是无穷多的, 计算机无法表示这么多的连续数据, 因此都是通过离散方式表示及存储的. 这样的话就有一些数无法表示, 只能用近似值表示.

为什么有这么多精度?

因为成本和准确度。
都知道精度高肯定更准确,但是也会带来更高的计算和存储成本。较低的精度会降低计算精度,但可以提高计算效率和性能。所以多种不同精度,可以让你在不同情况下选择最适合的一种。
双精度比单精度表达的更精确,但是存储占用多一倍,计算耗时也更高,如果单精度足够,就没必要双精度。

浮点精度类型

FP精度

Floating Point, 是IEEE定义的标准浮点数类型。由符号位(sign)、指数位(exponent)和小数位(fraction)三部分组成。符号位都是1位,指数位影响浮点数范围,小数位影响精度。
其中:
  • FP64, 也称之为double, 双精度浮点数
  • FP64, FP32, FP16是IEEE定义的标准格式的FP类型, 但是FP8和FP4不是

TF32(Tensor Float 32)

英伟达针对机器学习设计的一种特殊的数值类型,用于替代FP32。首次在A100 GPU中支持
由1个符号位,8位指数位(对齐FP32)和10位小数位(对齐FP16)组成,实际只有19位。在性能、范围和精度上实现了平衡。而且TF32向FP16转换也比较容易
notion image

BF16(Brain Float 16)

由Google Brain提出,也是为了机器学习而设计。由1个符号位,8位指数位(和FP32一致)和7位小数位(低于FP16)组成。所以精度低于FP16,但是表示范围和FP32一致,和FP32之间很容易转换。
在 NVIDIA GPU 上,只有 Ampere 架构以及之后的GPU 才支持。当前
混合精度训练
主要使用的格式

精度问题

上溢出(Overflow)

很大的数据被round成了, 比如在IEEE754标准里面double的最大数值在1e308这个量级(可以用
 
校验),如果你在python里用 1e308 + 1e308,得到的结果就是inf了

下溢出(Underflow)

接近0的结果被round成了0,比如double的最小的正数是, 这个-1074来源于-1023(11 bit exp) - 51(52 bit mantissa),大约为,所以如果在python里使用,得到的结果就是0了,这里就是发生了underflow

精度损失(Loss of Precision)

因为浮点数的表示方法在数轴上是不均匀的(或者说是分段均匀的),所以天然存在一个问题:只能近似表示某些数据,这就涉及到近似表示带来的精度损失。比如在python里面,计算0.1 + 0.2,得到的结果会是0.30000000000000004,0.1 + 0.2 == 0.3返回的结果也会是False,也就是发生了精度损失。如果你进一步用 struct 查看hex数值,0.1 + 0.2的结果是0x3FD3333333333334,而0.3则是0x3FD3333333333333(这个数值在数轴上离0.3更近)

数值稳定的解法

主要分为两个方向:
  • 使得数值计算的方法给出的结果更接近真实结果
  • 在计算结果不能更优的基础上,防止误差进一步扩散
可以归纳为以下四种方法:

重写数学公式

很多时候,数学公式在理论上是等价的,但在数值计算中可能存在极大的稳定性差异。

示例

对于除法运算 x / y,自动求导的结果是  ,这也导致了早期的实现都是 - x / (y * y) 的形式,但是我们考虑接近0的数值y(比如1e-8),的结果往往很有可能出现underflow的问题;而对于比较大的数值y,很有可能overflow。
而如果先计算 x / y,则这个中间结果通常处在一个更良好的范围里,所以对于除法的反传函数, - x / y / y 通常是一个更加数值稳定的写法。这也是pytorch官方的实现方式
如果想要这对两个写法有更直观的理解,可以试着跑一下下面的code

使用其他算法

有些算法虽然数学上是等价的,但在实际计算中表现差异很大。

示例

例如在求方差的时候,既可以按照方差的定义先求期望,再求方差:
也可以利用  求方差:
在求方差的时候,如果采用标准的方差定义,需要先确定数据的均值,这样的话,需要遍历数据两次(two-pass):第一次计算出均值,第二次计算出方差,这样对IO不是很友好。如果采用方法2计算方差,只需要遍历一次数据且能够在线更新(online update),但是这样会有非常明显的数值稳定问题,一个典型的case就是在小方差大均值的数据。

提高精度或改变数值类型

默认情况下,模型使用的是 float32 / float16 来进行训练,但在关键节点上,特别是梯度累加、参数更新等环节,如果使用低精度可能会导致误差误差甚至训练不收敛。所以有时候为了达到更好的稳定性,经常会在某些模块中采用更高的精度,或者临时将变量转换成高精度计算再转换回来。比如在
混合精度训练
中,往往会做loss scaling,或者在某些操作上autocast到 float32 以保证训练稳定。

限制输入范围

这个方法应该是短期work around的时候最常用的方法,比如定位到出现不稳定的具体算子,然后对输入或者输出做一下clip,或者加一个epsilon,类似clip(x, min=1e-5) 或者 x = x + 1e-5这种写法。通常对于一些除以极小值或者log一个极小值的case能起到效果。
 

参考链接

  1. 那些年,我们没想过的数值稳定算法 https://zhuanlan.zhihu.com/p/1892992256388084891
 

© 木白 2024 - 2025