目录
- 1. 小数的二进制转换
- 2. 浮点数的二进制转换
- 3. 浮点数的存储
- 3.1 以fp32为例
- 3.2 规约形式与非规约形式
- 4. 各种类型的浮点数
- 5. BF16和FP16的区别
- Ref
1. 小数的二进制转换
十进制小数转换成二进制小数采用「乘2取整,顺序排列」法。具体做法是:用 2 2 2 乘十进制小数,可以得到积,将积的整数部分取出,再用 2 2 2 乘余下的小数部分,又得到一个积,再将积的整数部分取出,如此进行,直到积中的小数部分为零,或者达到所要求的精度为止。
然后把取出的整数部分按顺序排列起来,先取的整数作为二进制小数的高位有效位,后取的整数作为低位有效位。
二进制小数转十进制小数则十分简单。我们知道二进制表示整数时,最低位代表 2 2 2 的 0 0 0 次方,往高位依次是 2 2 2 的 1 1 1 次方, 2 2 2 次方, 3 3 3 次方。那么对应的,二进制数小数点后面,最高位则是 2 2 2 的 − 1 -1 −1 次方,如下图所示:
因此: ( 0.1101 ) 2 = 1 ⋅ 2 − 1 + 1 ⋅ 2 − 2 + 0 ⋅ 2 − 3 + 1 ⋅ 2 − 4 = ( 0.8125 ) 10 (0.1101)_2=1\cdot2^{-1}+1\cdot2^{-2}+0\cdot2^{-3}+1\cdot2^{-4}=(0.8125)_{10} (0.1101)2=1⋅2−1+1⋅2−2+0⋅2−3+1⋅2−4=(0.8125)10。
整数可以使用 2 2 2 的非负幂次的有限和来表示,但小数就不一定能够用 2 2 2 的负幂次的有限和来表示了,部分情况下可能是无限和。
例如对于小数 0.6 0.6 0.6 而言,它的二进制形式为 0. 1001 ‾ 0.\overline{1001} 0.1001,是一个无限循环小数,循环节是 1001 1001 1001。
计算机只能识别 0 0 0 和 1 1 1,这就是为什么我们需要讨论十进制小数到二进制小数的转换。注意到在上述的例子中, 0.6 0.6 0.6 转换成二进制后是一个无限循环小数,由于计算机内存有限,无法存储无限长的数字序列,因此在实际存储这类数字时,只能存储小数点后的有限位数。这就意味着,无论如何,存储时都必须进行截断或四舍五入处理,而这种操作会引入所谓的「浮点数精度误差」。
很显然,对于这类小数,能够存储的位数越多,相应的精度就越高。这就是为什么 double
类型相比于 float
类型能提供更高精度的原因。
⚠️ 浮点数存储遵循IEEE 754规范。
为加深理解,我们使用Python来实现一遍:
def decimal_to_binary(decimal, max_step=30):binary = '0.'cnt = 0while decimal != 0 and cnt < max_step:decimal *= 2bit = int(decimal)binary += str(bit)decimal -= bitcnt += 1return binaryprint(decimal_to_binary(0.8125))
print(decimal_to_binary(0.6))
2. 浮点数的二进制转换
粗略地讲,一个浮点数由其整数部分和小数部分组成,在知道小数的进制转换后,我们可以将一个十进制浮点数转换成二进制形式。
以 10.625 10.625 10.625 为例。只需注意到
10.625 = 10 + 0.625 = 8 + 2 + 0.5 + 0.125 = 2 3 + 2 1 + 2 − 1 + 2 − 3 \begin{aligned} 10.625&=10+0.625=8+2+0.5+0.125 \\ &=2^3+2^1+2^{-1}+2^{-3} \end{aligned} 10.625=10+0.625=8+2+0.5+0.125=23+21+2−1+2−3
于是 ( 10.625 ) 10 = ( 1010.101 ) 2 (10.625)_{10}=(1010.101)_2 (10.625)10=(1010.101)2。我们还可以进一步将其表示成以 2 2 2 为底的科学记数法: 1010.101 = 1.010101 × 2 3 1010.101=1.010101\times 2^3 1010.101=1.010101×23。
一些其他的例子:
3.5 → 1.11 × 2 1 0.8125 → 1.101 × 2 − 1 − 0.6 → − 1. 0011 ‾ × 2 − 1 \begin{aligned} 3.5 &\to 1.11\times 2^1 \\ 0.8125 &\to 1.101\times 2^{-1} \\ -0.6&\to -1.\overline{0011}\times 2^{-1} \\ \end{aligned} 3.50.8125−0.6→1.11×21→1.101×2−1→−1.0011×2−1
由此可见,任何(正)十进制浮点数表示成二进制科学记数法以后,一定是 1 1 1 点几(尾数)乘以 2 2 2 的多少次方(指数)。对于负数来说,就是负 1 1 1 点几(尾数)乘以 2 2 2 的多少次方(指数)。因此,任意一个二进制浮点数都可以表示成下面的形式:
V = ( − 1 ) s × M × 2 E (1) V=(-1)^s\times M\times 2^E\tag{1} V=(−1)s×M×2E(1)
其中 s s s 代表符号位(sign), s = 1 s=1 s=1 说明是负数, s = 0 s=0 s=0 说明是正数。 M M M 是尾数(mantissa), E E E 是指数(exponent)。因此存储一个浮点数只需要存储 s , M , E s,M,E s,M,E 这三部分。
一些发现:
- 尾数 M M M 的最高位总是 1 1 1,因此实际存储时,只需要存储尾数的小数部分(fraction),即小数点后面的数字。
- M ∈ [ 1 , 2 ) M\in[1, 2) M∈[1,2)。这是因为, M M M 最高位是 1 1 1,所以至少是 1.000... 1.000... 1.000...,极端情况下是 1.111... 1.111... 1.111...,即 1 + 1 2 + 1 4 + . . . 1+\frac12+\frac14+... 1+21+41+...,即使尾数达到了 2 2 2,即 10.000... 10.000... 10.000...,也会被重新规范化到 1.000... 1.000... 1.000...,即 1 1 1。
- E ∈ Z E\in\mathbb{Z} E∈Z,可正可负可为 0 0 0。
- 注意到 M × 2 − 1 ∈ [ 0.5 , 1 ) , M × 2 1 ∈ [ 2 , 4 ) M\times 2^{-1}\in[0.5,1),\;M\times 2^1\in[2,4) M×2−1∈[0.5,1),M×21∈[2,4)。因此,在十进制下,任何小于 1 1 1 的数转化成二进制后,其指数一定小于 0 0 0;任何大于等于 2 2 2 的数转化成二进制后,其指数一定大于 0 0 0。进一步可知,对于任意非零十进制浮点数 x x x,将其转化成二进制后,其指数为 ⌊ log 2 ∣ x ∣ ⌋ \lfloor \log_2|x|\rfloor ⌊log2∣x∣⌋。
- 只考虑正浮点数(负浮点数亦然)。由上一条知,所有指数将所有的正浮点数 ( 0 , + ∞ ) (0,+\infty) (0,+∞) 划分成若干个互不相交的区间: ( 0 , + ∞ ) = ⋃ k = − ∞ + ∞ [ 2 k , 2 k + 1 ) (0,+\infty)=\bigcup_{k=-\infty}^{+\infty}[2^k,2^{k+1}) (0,+∞)=⋃k=−∞+∞[2k,2k+1)。设有两个符号相同的十进制浮点数 x 1 , x 2 x_1,x_2 x1,x2,它们在二进制下的指数分别为 k 1 , k 2 k_1,k_2 k1,k2,且 k 1 > k 2 k_1>k_2 k1>k2。若 x 1 , x 2 x_1,x_2 x1,x2 均为正,则 x 1 > x 2 x_1>x_2 x1>x2;若 x 1 , x 2 x_1,x_2 x1,x2 均为负,则 x 1 < x 2 x_1<x_2 x1<x2。这说明我们可以仅靠指数来比较两个浮点数的大小(前提是指数不同)。
- M M M 的位数通常决定了浮点数能表示的精度。例如对于十进制小数 0.6 0.6 0.6 而言,将其转化成二进制后,能存储的尾数位越多,表达自然越精确。 E E E 的位数则决定了浮点数能够表示的数值范围。
3. 浮点数的存储
3.1 以fp32为例
IEEE 754规定的浮点数存储格式如下:
最高位是符号位, 1 1 1 代表负数, 0 0 0 代表正数。接下来的若干位是指数位,指数位之后才是尾数位(不存储最高位 1 1 1)。
以fp32(32-bit floating point,32位浮点数)为例,它拥有 1 1 1 位符号位、 8 8 8 位指数位, 23 23 23 位尾数位。
8 8 8 位有符号整数的取值范围是 [ − 128 , 127 ] [-128,127] [−128,127],可以表示 256 256 256 个指数。但实际上,我们前面说过,指数不等时可以仅通过指数来比较两个浮点数的大小。为了简化这个比较手续,考虑采用 8 8 8 位无符号整数进行存储,这样就能仅通过字典序来比较两个指数的大小了。在该设定下,指数的取值范围变成了 [ 0 , 255 ] [0,255] [0,255]。
浮点数中还有三个特殊值: 0 , ∞ , NaN 0,\infty,\text{NaN} 0,∞,NaN。IEEE 754规定,需要留一些位置用来表示这些特殊值: 0 0 0 用来留给 0 0 0, 255 255 255 用来留给 ∞ , NaN \infty,\text{NaN} ∞,NaN。挖掉两个位置后,指数的取值范围变成了 [ 1 , 254 ] [1,254] [1,254]。注意到此时还没有负指数,为了平衡正负指数的个数,这里引入指数偏移量 c = 127 c=127 c=127,于是有
存储指数 − 127 = 实际指数 存储指数-127=实际指数 存储指数−127=实际指数
故知实际指数的范围为 [ − 126 , 127 ] [-126,127] [−126,127]。
🧑💻 一般地,设 e e e 为指数位的个数,则 c ≜ 2 e − 1 − 1 c\triangleq 2^{e-1}-1 c≜2e−1−1。fp32的指数偏移量为 2 8 − 1 − 1 = 127 2^{8-1}-1=127 28−1−1=127。
例如, 10.625 10.625 10.625 转化成二进制后指数为 3 3 3,那么用来存储的指数值为 127 + 3 = 130 127+3=130 127+3=130,即 10000010 10000010 10000010。尾数部分只需要存储 010101 010101 010101,因此 10.625 10.625 10.625 用fp32表示即为
0 ⏟ 1 个符号位 10000010 ⏟ 8 个指数位 01010100000000000000000 ⏟ 23 个尾数位 \underbrace{0}_{1个符号位}\;\underbrace{10000010}_{8个指数位}\;\underbrace{01010100000000000000000}_{23个尾数位} 1个符号位 08个指数位 1000001023个尾数位 01010100000000000000000
那反过来该如何操作呢?例如,给定一个fp32格式的浮点数 11000001010011000000000000000000
,首先分离出它的符号位、指数位和尾数位:
- 符号位: 1 1 1
- 指数位: 10000010 10000010 10000010
- 尾数位: 10011000000000000000000 10011000000000000000000 10011000000000000000000
由符号位可知这是一个负数,将指数位转换成十进制得到 130 130 130,因此实际指数为 130 − 127 = 3 130-127=3 130−127=3。由此可知该浮点数的二进制科学记数法表示为 − 1.10011 × 2 3 - 1.10011\times2^3 −1.10011×23,从而
− 1.10011 × 2 3 = − 1100.11 = − ( 2 3 + 2 2 + 2 − 1 + 2 − 2 ) = − 12.75 -1.10011\times2^3=-1100.11=-(2^3+2^2+2^{-1}+2^{-2})=-12.75 −1.10011×23=−1100.11=−(23+22+2−1+2−2)=−12.75
我们还可以用Python来完成这个互相转化的操作:
import structdef binary_to_float(binary_str):assert len(binary_str) == 32, "only support fp32"# 4 bytes, 32 bits.decimal_float = int(binary_str, 2).to_bytes(4, 'big')return struct.unpack('>f', decimal_float)[0]def float_to_binary(value):packed_float = struct.pack('>f', value)binary_str = ''.join(f'{byte:08b}' for byte in packed_float)return binary_strprint(binary_to_float('11000001010011000000000000000000'))
print(float_to_binary(-12.75))
3.2 规约形式与非规约形式
回到 ( 1 ) (1) (1) 式,已知 M ∈ [ 1 , 2 ) M\in[1,2) M∈[1,2),设有 e e e 个指数位,那么存储指数 E ∈ [ 0 , 2 e − 1 ] E\in[0,2^e-1] E∈[0,2e−1]。当 E ∈ [ 1 , 2 e − 2 ] E\in[1, 2^e-2] E∈[1,2e−2] 时,此时 ( 1 ) (1) (1) 式又称规约形式。
还是以fp32为例,只考虑正数,在规约形式下
- 最小值: M = 1 , E = 1 − 127 = − 126 M=1,\,E=1-127=-126 M=1,E=1−127=−126,故 V = 2 − 126 ≈ 1.18 × 1 0 − 38 V=2^{-126}\approx1.18\times10^{-38} V=2−126≈1.18×10−38。
- 最大值: M = 1 + 2 − 1 + ⋯ + 2 − 23 = 2 − 2 − 23 , E = 254 − 127 = 127 M=1+2^{-1}+\cdots+2^{-23}=2-2^{-23},\,E=254-127=127 M=1+2−1+⋯+2−23=2−2−23,E=254−127=127,故 V = ( 2 − 2 − 23 ) ⋅ 2 127 ≈ 3.40 × 1 0 38 V=(2-2^{-23})\cdot2^{127}\approx3.40\times 10^{38} V=(2−2−23)⋅2127≈3.40×1038
很显然,fp32无法表示低于最小值的数,低于最小值的数会被视为 0 0 0(下溢)。同样地,高于最大值的数会被视为 ∞ \infty ∞(上溢)。
更具体地:
- 用 M = 0 , E = 0 M=0,E=0 M=0,E=0 来表示 0 0 0。
- 用 M = 0 , E = 2 e − 1 M=0,E=2^e-1 M=0,E=2e−1 来表示 ∞ \infty ∞。
- 用 M ≠ 0 , E = 2 e − 1 M\neq 0,E=2^e-1 M=0,E=2e−1 来表示 NaN \text{NaN} NaN。
为了让fp32能表示低于最小值的数(即更接近0的数),并且让从最小规约数向 0 0 0 的过渡更加平滑,非规约数便由此引入。这种设计使得浮点数系统能够更细致地处理接近零的小数值。
我们先思考一下,如何让fp32来表示小于 2 − 126 2^{-126} 2−126 的数呢?显然存储指数 E E E 已经达到了最小值 1 1 1,而 M ∈ [ 1 , 2 ) M\in[1,2) M∈[1,2),将 M M M 的隐含最高位由 1 1 1 改为 0 0 0,从而 M ∈ [ 0 , 1 ) M\in[0,1) M∈[0,1),我们便可以表示比最小规约数还要小的数。
IEEE 754规定,非规约数的实际存储指数为 0 0 0,而计算公式则为 1 − c 1-c 1−c,其中 c c c 为指数偏移量。并且 M ∈ ( 0 , 1 ) M\in(0,1) M∈(0,1)。当 E = 0 , M ≠ 0 E=0,M\neq0 E=0,M=0 时,则按照非规约数来解析fp32。
我们来计算一下非规约形式下的最小值和最大值:
- 最小值: M = 2 − 23 , E = 0 M=2^{-23},E=0 M=2−23,E=0, V = 2 − 23 ⋅ 2 − 126 = 2 − 149 ≈ 1.4 × 1 0 − 45 V=2^{-23}\cdot 2^{-126}=2^{-149}\approx 1.4\times 10^{-45} V=2−23⋅2−126=2−149≈1.4×10−45。
- 最大值: M = 2 − 1 + 2 − 2 + ⋯ + 2 − 23 = 1 − 2 − 23 , E = 0 M=2^{-1}+2^{-2}+\cdots+2^{-23}=1-2^{-23},E=0 M=2−1+2−2+⋯+2−23=1−2−23,E=0, V = ( 1 − 2 − 23 ) ⋅ 2 − 126 ≈ 2 − 126 V=(1-2^{-23})\cdot 2^{-126}\approx2^{-126} V=(1−2−23)⋅2−126≈2−126。
我们还可以进一步计算得到一般形式下的一些结论。设一个浮点数有 e e e 个指数位和 f f f 个尾数位,令 c ≜ 2 e − 1 − 1 c\triangleq 2^{e-1}-1 c≜2e−1−1,则:
- 规约形式下的最小值: 2 1 − c 2^{1-c} 21−c。
- 规约形式下的最大值: ( 2 − 2 − f ) ⋅ 2 c (2-2^{-f})\cdot 2^{c} (2−2−f)⋅2c。
- 非规约形式下的最小值: 2 1 − c − f 2^{1-c-f} 21−c−f。
- 非规约形式下的最大值: ( 1 − 2 − f ) ⋅ 2 1 − c (1-2^{-f})\cdot 2^{1-c} (1−2−f)⋅21−c。
可以看出有以下等式成立:
非规约形式下的最小值 + 非规约形式下的最大值 = 规约形式下的最小值 非规约形式下的最小值+非规约形式下的最大值=规约形式下的最小值 非规约形式下的最小值+非规约形式下的最大值=规约形式下的最小值
关于特殊值的一些结论:
E E E | M M M | 形式 |
---|---|---|
[ 1 , 2 e − 2 ] [1,2^e-2] [1,2e−2] | [ 1 , 2 ) [1,2) [1,2) | 规约形式 |
0 0 0 | ( 0 , 1 ) (0,1) (0,1) | 非规约形式 |
0 0 0 | 0 0 0 | 零 |
2 e − 1 2^e-1 2e−1 | 0 0 0 | 无穷 |
2 e − 1 2^e-1 2e−1 | ≠ 0 \neq0 =0 | NaN |
4. 各种类型的浮点数
名称 | 指数位 | 尾数位 |
---|---|---|
FP16(半精度浮点数) | 5 5 5 | 10 10 10 |
BF16(Brain Floating) | 8 8 8 | 7 7 7 |
FP32(单精度浮点数) | 8 8 8 | 23 23 23 |
FP64(双精度浮点数) | 11 11 11 | 52 52 52 |
FP128(四精度浮点数) | 15 15 15 | 112 112 112 |
5. BF16和FP16的区别
先使用之前得到的公式计算一下规约形式下的最小值和最大值:
类型 | Min | Max |
---|---|---|
FP16 | 2 − 14 ≈ 6.10 × 1 0 − 5 2^{-14}\approx 6.10\times10^{-5} 2−14≈6.10×10−5 | ( 2 − 2 − 10 ) ⋅ 2 15 = 65504 (2-2^{-10})\cdot 2^{15}=65504 (2−2−10)⋅215=65504 |
BF16 | 2 − 126 ≈ 1.18 × 1 0 − 38 2^{-126}\approx1.18\times10^{-38} 2−126≈1.18×10−38 | ( 2 − 2 − 7 ) ⋅ 2 127 ≈ 3.39 × 1 0 38 (2-2^{-7})\cdot2^{127}\approx3.39\times10^{38} (2−2−7)⋅2127≈3.39×1038 |
- BF16和FP32的指数位相同,像是从FP32中截取了前16位一样。也正是因为指数位相同,使得BF16的数值范围与FP32相似。
- BF16相比FP16的数值范围更大,但是精度会更低(深度学习中,数值范围的重要性是远大于数值精度的)。
- FP16的数值范围过小,容易造成溢出。
- FP16更多用于需要较高精度但数值范围要求不是特别严格的场合,如某些图形处理和较轻量级的神经网络模型推理。
Ref
[1] https://blog.csdn.net/u013250861/article/details/131152163
[2] https://www.paddlepaddle.org.cn/documentation/docs/zh/dev_guides/amp_precision/amp_op_dev_guide_cn.html
[3] https://cloud.tencent.com/developer/article/1473541
[4] https://www.runoob.com/w3cnote/decimal-decimals-are-converted-to-binary-fractions.html
[5] https://leokongwq.github.io/2017/08/19/computer-how-float-stored.html