在计算机体系架构中,实数的表示方式可以分为定点数和浮点数(整形可以归纳为定点数的一种)。定点数的所有比特都用来表示数值,其精度和范围由程序员定义。相同的二进制编码可能表示不同精度的数值,比如8比特定点数0x55(参见下图),即可能表示85(小数点在最低位右侧),可能表示3.4375(小数点在比特4右侧)。在使用时,程序员需要非常关注数据的表示范围,正确地进行数位的扩展和截断,才能保证得到预期的结果。
图1 相同编码的定点数表示不同的数值
浮点数使用科学计数的方式来表示数值,其二进制编码中使用一部分比特用来表示浮点数的位置,其他比特表示有效数字。在IEEE-754标准中,浮点数的定义如下:
$$(-1)^s\times b^e\times m$$
其中,
s
表示符号位(0或1)。b
表示基数。对于二进制系统,b=2
。e
表示指数,指数可正可负。m
表示有效数字,需要按照指定的基数表示。对于二进制来说,有效数字是处于[1,2)
范围的一个小数。
由于浮点数中直接编码了指数域,将小数点的位置直接记录在数值中,所以不需要软件工程师管理数据精度和范围,从而简化了编程。同时,指数域是二进制编码的,使得浮点数能够表达的数值范围远远大于相同比特数的定点数。获得这些优势的代价当然是更加复杂的硬件逻辑和处理规则。
对于计算机行业的工程师,浮点数是基础常识,尤其是浮点数的格式和浮点数的计算规则。但是,并不是所有的工程师都深入研究过浮点数的规范定义,以及引入这些定义的原因。从架构角度,除了格式和计算规则,浮点计算单元还需要关注的内容如下:
- 浮点舍入(rounding)
- 特殊值的计算规则(零、非规范数、无穷大和NaN)
- 浮点异常
这些内容仅仅是架构角度需要关注的内容。从微架构角度,还有更多地细节需要挖掘,那部分内容就不在本文的范畴内了。
本系列文章的目的就是通过梳理IEEE-754标准和Armv9体系的定义,展示计算机体系结构中浮点数规则的一些细节,供读者参考。本系列文章分为五期。本文作为第一期将主要介绍浮点数格式。
本系列文章依据IEEE 754-2008版本和Arm-ARM L.a版本。
浮点数格式
提到浮点数的定义,首先想到的就是IEEE-754标准。这是浮点数领域的基础标准,各种处理器和编程语言都遵从IEEE-754标准来定义其浮点数。IEEE-754中定义了两种浮点数格式:
- 尾数完全用二进制表达的二进制浮点数格式;
- 尾数用十进制数字的二进制编码(4比特二进制数表示1个十进制数)表达的十进制浮点数格式。
在现在的计算机系统中,还没有看到十进制浮点数格式。
IEEE-754标准定义的二进制浮点数格式如下:
图2 IEEE-754标准二进制浮点格式
其中包括符号位(1比特)、偏执指数和有效数字(尾数)三部分。
- 符号位(
S
):只有1比特,取值0或1。0表示正数,1表示负数。 - 偏执指数域(
E
,简称指数域):用无符号整数表示指数域,其表示的真实指数为E-bias
。指数域的宽度决定了浮点数表示的范围。 - 尾数域(
T
):只提供有效数字的小数部分,有效数字为
。因为有效数字是处于[1,2)
范围的一个小数,所以其整数部分一定是1,可以忽略。尾数域的宽度决定了浮点数表示的精度。
浮点数编码表示的数值是:
$$(-1)^s\times 2^{E-bias}\times 1.T$$
IEEE-754标准中提供了指数域宽度和尾数域宽度应该满足的关系,并且预定义了四种二进制浮点格式,分别是16比特浮点数(FP16)、32比特浮点数(FP32)、64比特浮点数(FP64)和128比特浮点数(FP128)。在处理器硬件和编程语言中,通常将FP64称为双精度(Double-precision)浮点数(简写DP),FP32称为单精度(Single-precision)浮点数(简写SP)。有时也将FP16称为半精度(Half-precision)浮点数(简写HP)。除了HP/SP/DP,还有一些非常少见的浮点数格式,比如Intel的X87指令集支持80比特浮点数。
随着机器学习的快速发展,AI模型对于内存带宽的高需求促进了蒸馏和压缩技术的发展,从而对于更小范围和更小精度的浮点格式提出了需求。Nvidia首先提出了BFloat16格式(BF16),并且称为LLM常用的数据格式。BF16提供与FP32相同的指数范围,但是更短的有效数字。随着LLM的进一步发展,Nvidia、Arm和Intel共同提出并定义了8比特浮点数(FP8)、6比特浮点数(FP6)和4比特浮点数(FP4)。BF16、FP8、FP6和FP4都不是IEEE标准数据格式,而是定义的OCP标准中。
各种浮点数据格式的编码如下图所示。
图3 浮点数格式
浮点数的特殊值编码
根据IEEE-754规范,并不是每一个二进制编码都对应了一个有效合规的数值。浮点数的编码空间根据指数域和尾数的不同取值划分为:零、非规范数(denormal、subnormal或denormalize)、规范数(normal或者normalize),无穷大(inifity)和NaN(Not-a-Number)。
如果指数E不是0也不是全1,浮点数编码表示一个规范数(normal)。规范数的解析符合上面介绍的浮点数编码。绝对值最小的规范数:E是1且T是0;绝对值最大的规范数:E是2^w-2且T是全1。
如果指数E是0且尾数也全是0,那么这个编码显然表示零。浮点数有+0和-0之分。计算规则需要定义结果的符号位。
如果指数E是0但是尾数不是0,浮点数编码表示的是大于0且小于最小规范数的数值,这部分称为非规范数(denormal)。在计算非规范数编码对应的数值时,尾数部分没有固定的整数部分:
$$(-1)^S\times 2^{(E-bias)} \times 0.T$$
为什么定义非规范数?
如果将规范数编码对应的数值标记在数轴上,会发现这些数值并不是均匀分布的。实际上,指数越小,数值越密集;指数越大,数值越稀疏。如下图中红色线表示的部分。
如果指数为零的部分也定义为规范数,那么最小的编码表示的就不是0,而是上图中0.25的位置。为了能够将指数为零的编码均匀布置在0到最小规范数之间,那么需要定义非规范数,按照有效数字的整数为0的方式进行解析。如上图中蓝色线所示,每个数值都是最小分辨率的整数倍。
另一个原因是,保证浮点数加减法的结果不会超出浮点数的表达能力,而不会引起下溢出。比如,最小的两个规范数减法,得到的结果是最小的非规范数。
如果指数E是全1且尾数全是0,这个编码表示无穷大。如果想要表达的数字超过了浮点数的表达范围,就应该用无穷大表示,并且触发上溢出。无穷大同样有正负两种编码,需要根据计算规则确定符号位。
如果指数E是全1且尾数不全是0,这个编码表示NaN。NaN不表示任何数字,而是一种错误。NaN进一步分为quiet NaN(qNaN)和signal NaN(sNaN)两种。如果尾数的最高位是1,编码表示qNaN;如果尾数的最高位是0,编码表示sNaN。NaN不会解析为某个数值参与计算,而是根据特定的规则产生结果。
我们将在后续文章中详细介绍非规范数、无穷大和NaN参与计算的规则。
并不是所有的浮点格式都完全按照上面的方法划分。如下图中所示:
- Arm替代FP16格式没有定义NaN数,而是将这部分同样视为规范数,按照规范数解析。
- FP8的E4M3格式没有定义无穷大和QNaN,只是将指数域和尾数域全1的情况定义为sNaN。
- FP6和FP4则完全没有定义无穷大和NaN,而是将其都视为规范数,按照规范数解析。毕竟FP6和FP4的可用编码已经很少了。
对于每种浮点格式,不同类型编码的范围如下图所示。
图4 各种浮点数编码的特殊值
注意:对于FP8 E4M3格式,sNaN只包含E是全1,T是全1这一种编码。
默认NaN
NaN包含了指数域为全1而且尾数不为0的所有编码,而非单独一种编码。当运算产生NaN时,需要根据计算规则选择返回的编码。Arm架构中选择了编码最小的qNaN作为默认NaN,符号位取决于FPCR.AH。特别地,FP8的E4M3格式只定义了一个sNaN编码,没有qNaN,所以选择这个唯一的NaN编码作为默认NaN。
HP | SP | DP | BF16 | FP8 E4M3 | FP8 E5M2 | |
---|---|---|---|---|---|---|
符号位 | FPCR.AH | FPCR.AH | FPCR.AH | FPCR.AH | FPCR.AH | |
指数 | 0x1F | 0xFF | 0x7FF | 0xFF | 0xF | 0x1F |
尾数 | bit[9]=1 bit[8:0]=0 | bit[22]=1 bit[21:0]=0 | bit[51]=1 bit[50:0]=0 | bit[6]=1 bit[5:0]=0 | bit[2:0]=111 | bit[1]=1 bit[0]=0 |
Armv8架构中支持的浮点格式
在Arm架构中能够直接使用的浮点数据格式,包括以下五种:
- 双精度浮点格式(DP/FP64)。
- 单精度浮点格式(SP/FP32)。
- 半精度浮点格式(HP/FP16)。
进一步分为IEEE标准格式和Arm替代格式,两者的格式是相同的,区别只在于解析指数全1而尾数不为0的编码。Arm替代FP16格式没有定义NaN数,而是将这部分同样视为规范数,按照规范数解析。当进行从FP16到其他浮点格式转换时,可以提供更大的数值范围,减少运算精度损失。 - BFloat16浮点格式(BF16)。
- FP8浮点格式(8比特),分为E5M2和E4M3两种格式。
对于这五种浮点格式,Arm架构处理器的计算单元提供对应的硬件运算单元,Arm架构提供了调用这些运算单元的指令。比如浮点加法操作,FADD提供了DP、SP和HP格式的浮点加法,BFADD提供了BF16格式的浮点加法操作。
直接支持一种浮点格式的好处显而易见:节省指令数,软件处理更加简单;提高计算能力,理论上FP8的浮点加法算力是FP16浮点加法算力的2倍,FP8的矩阵乘法算力是FP16的矩阵乘法算力的4倍。但是,这需要增加大量的实现开销和研发成本,包括实现对应的指令译码、数据通路(更小的数据单元会导致更加复杂的数据通路来传递操作数和结果)和执行单元;同时也增加了验证的工作量。
除了上面五种直接支持的数据格式,Arm还通过查表指令间接支持FP6和FP4浮点格式。通过查表LUTI指令可以将FP6和FP4浮点格式转换为FP8/FP16或BF16格式,然后以FP8/FP16/BF16格式进行所需要的计算。能够这样实现的原因在于:
- FP4和FP6都是由于机器学习引入的浮点格式,实际应用中需要的是FP4*FP4得到FP16或FP32的矩阵乘法操作,而不存在FP4与FP4运算后仍然得到FP4的操作。
- 因为FP4/FP6的数据范围和精度都远小于其他格式,在进行矩阵乘法时,可以将FP4/FP6先精确地转换为目标格式再进行运算。综上所述,直接支持FP4/FP6格式的动机不强。
显然,间接支持可以避免计算单元和数据通路的开销,同时可以适用于不同的浮点格式,而不需要反复增加数据格式。间接支持的缺陷在于,虽然能够实现FP4/FP6所需要的功能,但是并没有提高对应的算力。如果LUTI的输出格式是FP8,那么硬件提供的浮点算力仍是FP8的算力。
浮点数格式的控制
IEEE标准FP16格式而是和Arm替代FP16格式
FPCR.AHP
控制AArch64状态下使用的HP格式;FPSCR.AHP
控制AArch32状态下使用的HP格式。两个比特是一致的。
所有的半精度数据处理指令忽略FPCR.AHP
控制,总是使用IEEE标准FP16格式。只有半精度与其他浮点格式的转换指令才会考虑FPCR.AHP
的影响。
FPCR.AHP 或FPSCR.AHP | 含义 |
---|---|
0b0 | 使用IEEE标准HP格式。 |
0b1 | 使用Arm替代HP格式。 |
FP8 E5M2格式和FP8 E4M3格式
FPMR.F8D
, FPMR.F8S2
, FPMR.F8S1
控制FP8指令使用的数据格式。FPMR.F8S1
控制第一个源操作数的格式;FPMR.F8S2
控制第二个源操作数的格式;FPMR.F8D
控制目标操作数格式。
FPMR.F8D /FPMR.F8S2 /FPMR.F8S1 | 含义 |
---|---|
0b000 | OFP8 E5M2格式 |
0b001 | OFP8 E4M3格式 |