近日,Facebook 发布文章,介绍了一项新研究,该研究提出了一种使人工智能模型高效运行的方法,从根本上优化了浮点运算。
近年来,计算密集型的人工智能任务推动了各种用于高效运行这些强大的新型系统的定制化硬件的出现。我们采用浮点运算来训练深度学习模型,如 ResNet-50 卷积神经网络。但是,由于浮点数十分消耗资源,真正部署的人工智能系统通常依赖于使用 int8/32 数学运算的少数几个流行的整型量化技术。
Facebook 开发了一种使人工智能模型高效运行的方法。基于 70 多年前计算机科学发展初期的思想,该方法从根本上优化了浮点运算。
Facebook 对浮点数进行了彻底的改造,使它比 int8/32 数学运算的效率高出 16%。该方法用于卷积神经网络时,网络仍能达到很高的准确率,此外它还具备以下优势:
该技术可以提高人工智能研发的速度。当该方法被应用于人工智能模型训练中使用的更高精度的浮点数时,模型训练的效率能够提高 69%。
如今,模型通常使用浮点数进行训练,但是之后它们必须转换成更高效的、可以部署到生产环境中的量化格式。如果使用该方法,在部署模型时就不需要重新训练或重新学习。这样一来,人工智能开发者就可以更容易地部署高效的新模型。
现在的整型量化方法正变得越来越复杂。在某些情况下,这些方法可能对某些特定任务「过拟合」(因此不能维持通用性)。高效的通用浮点运算可以在保持准确率的情况下避免过拟合问题。
论文「Rethinking float point for deep learning」详细介绍了 Facebook 的这项技术。开发使用这些新技术执行浮点运算的新芯片需要耗费一定的时间。但这样做可能得到的好处包括使数据中心的人工智能计算速度更快,在移动设备上获得更好的人工智能运算性能的低功耗设计,以及在进行较少的软件更改的情况下以更简单、迅速的方法达到性能目标。摩尔定律正在逐渐减速,而「暗硅」(dark silicon)时代即将到来。持续的性能提升需要重新考虑几十年前的底层硬件设计决策(比如 IEEE 754 浮点标准),以及在合适的情况下使用数学近似(mathematical approximation)。尤其是神经网络的出现成为对它们进行重新评估的绝佳契机,因为神经网络对数值运算的变化和实验具有相当大的容忍度。
Facebook 为 ASIC/FPGA 做出的硬件设计和为评估编写的 C++/PyTorch 代码已经向人工智能社区开放。地址:http://github.com/facebookresearch/deepfloat
传统的浮点运算
众所周知,浮点数可以在合理的计算机存储空间中表示大小实数,其使用的系统与科学计数法大体相似。这种格式可用于以固定宽度的编码和基数(通常是二进制)表示 1,000,000 和 0.0625 这样的值。需要注意的是,浮点数只能精确地表示有限的实数,因为我们拥有的比特位数是有限的。所有其他值都可以用一种四舍五入的形式表示为最接近的可以表示出来的浮点值。
传统的二进制浮点格式包含符号(sign)、尾数(significand)和指数(exponent)。符号位表示数字是正的还是负的。尾数(其小数部分通常称为尾数)是形如 0.bbb... 或 1.bbb... 的二进制定点数,其中小数部分 bbb… 由若干二进制位在基数点之后表示的。(在十进制运算中,基数点也称为小数点,将整数与小数部分分开。)指数是一个有符号整数,它表示尾数需要乘以 2 的多少次幂。尾数有前导二进制 1(1.bbb…)的情况是规格化的,而尾数有前导二进制 0(0.bbb…)的情况是非规格化的(二者可以相互转化)。现代计算机中常用的 IEEE 754 浮点标准有规格化和非规格化两种尾数表示方法。尾数的前导位不需要被显式存储;在 IEEE 754 中,指数域决定前导位是 1 还是 0。
下图显示了以 16 位 IEEE 754 的 binary16 半精度浮点数对 -1.625 进行的编码,它具有固定大小的 5 位指数和 10 位的尾数部分。IEEE 指数的偏置值为 -15,因此下面编码的指数 15 实际上表示(15-15)即 0。
人工智能运算的现状和未来
为许多人工智能系统赋能的神经网络通常使用 32 位 IEEE 754 binary32 单精度浮点数进行训练。将位数降低到 16 位(半精度浮点数或类似 bfloat16 的格式)会获得一定的性能提升,但与同样位宽整数运算的效率相比,它仍然相形见绌。这些浮点变量可以很容易地使用原来的 32 位浮点神经网络数据,但要将整数量化到 8 位(或更少)常常需要学习量化参数并对模型进行再训练。许多 int8/32 量化方案可以像原始的浮点模型达到同样的准确率,但是它们也可能在处理任务时发生过拟合现象,从而无法在 ImageNet 验证集以外的任务上保证其准确率。
但是对于今天的计算机运算来说,除了整数、定点或浮点运算外,还有许多其他的选择。其中一些方法可以追溯到 20 世纪 50 年代:
非线性尾数映射
二进制随机数
熵编码
Facebook 使用这一思路创造出了一种浮点运算,其性能超过 int8/32。该方法的实现与目前硬件中的浮点运算及其变体(如非规格化的清零行为或字大小/字段位宽度变化,如 bfloat16 或 minifloat)有很大的区别。与 int8/32 量化不同的是,该实现仍然是一种通用的浮点运算,可以直接对其运算结果进行解释。
实现更高效浮点运算的关键
为了开发一种高效浮点运算的新方法,Facebook 研究人员认真分析了硬件浮点数低效的各种原因:
字(word)大小过大:有很大一部分算力被用于移动数据:从外部的 DRAM 到内部的 SRAM,从 SRAM 到寄存器,或从寄存器到寄存器(触发器)。浮点的字大小越大,消耗的算力就越多。
通用的定点数机制:尾数是定点数,定点数的加法器、乘法器和除法器是算术运算所必需的。浮点类型的精度(尾数长度)越高,这些组件就越大。硬件乘法器和除法器通常比硬件加法器更消耗资源(芯片面积、功耗和延迟)。
通用的浮点数机制:该机制处理基数点的「浮点」,因此是浮点表示法的一部分。例如,用于重新规格化的前导零(LZ)计数器、用于尾数对齐的移位器和舍入逻辑。浮点精度也对该机制所使用的硬件资源有很大的影响。
IEEE 754 的专有机制:这为 IEEE 754 标准中实现的逐渐下溢提供了非规格化的支持,并提供了额外的移位器、LZ 计数器和尾数重新规格化所需要的其他修改。非规格化的处理增加了大多数浮点操作的复杂性和计算开销。
减小字大小
缩小字(word)的大小可以显著地提升算力的利用率。我们可以尝试将 32 位数据压缩为 8 位或 16 位。典型的浮点数固定大小字段编码迫使我们做出困难的选择,是减少动态范围(指数)还是减小精度(尾数),而我们需要的是二者的折中。
我们可以用不同的方式来处理这种权衡。浮点数本身就是(无限精度)实数的量化形式。适用于见过的数据分布的量化器在数据复制的过程中误差会较小。对于在通用计算机上遇到的数据分布,我们通常没有太多的先验知识。然而,神经网络的分布在实际中是接近高斯分布的,有时还会受到批归一化等过程的控制。标准浮点数尽可能保证尾数在 10^-5 的精度与在 10^5 的精度相当,但大多数神经网络在一个相对较小的范围内(如 -10.0 到 10.00)执行计算。这个范围内的小数字(例如 0.0001)会被频繁使用,而大数字的使用频率较低。理想情况下,我们可以改变量化器,在需要的地方提供更高的精度,并保持一定的小数字动态范围。
缩减(tapered)浮点数可以帮助实现这些目标并减小字的大小。Gustafson 提出的 Posit 方法是一种效果很好的缩减形式。Posit 使用无前缀编码将指数编码为可变位数,其余部分为尾数。它在+/-1.0 左右将精度最大化,而在 0 或+/-∞ 处的精度则较低。它的压缩和扩展都是有损的,以损失某些地方的精度为代价来保证在其他地方的动态范围。因此,与 IEEE 风格的浮点数相比,它可以提供更高的精度(在某些地方)和更大的动态范围。当我们不知道待处理数据的数据分布时,posit 可以扩展到其他无前缀编码中(比如霍夫曼编码)。
定点数机制
我们可以设法避免在尾数上进行的乘法和除法运算。尾数可以被看作是小数部分的映射 f(x),它将取值范围在 [0, 1) 间的定点数 x 映射到 [1, 2) 中。(这种方法在 Lindstrom 等人 2018 年的论文《Universal Coding of the Reals: Alternatives to IEEE Floating Point》中有详细的介绍。)在典型的规格化浮点运算中,f(x) 是仿射函数 1+x(我们称之为线性域数)。
当 f(x) = 2^x 时,我们可以使用对数数字系统(LNS)将乘法和除法变成加法和减法。不过,添加 LNS 需要大量的硬件查找表来计算两个对数域数的和或差。这是采用 LNS 的一个主要问题,因为这些表可能比硬件乘法器更难以处理。注意,典型的浮点数已经是对数(指数)和线性(尾数)表示的组合,但是 LNS 表示完全是基于对数的。
浮点数机制
在计算机线性代数中,有效的操作是「乘加」:计算值「c」和 a 与 b 乘积「a x b」的和,得到「c + a x b」。通常情况下,对于 ResNet-50 等模型,单个累加器中可能会对数千个这样的乘积求和;在部署过程中运行一个模型时涉及数百万独立累加操作,而训练模型时则需要进行数以亿计的累加操作。
浮点融合乘加(FMA)是一种常见的乘加方法,它能够减小误差,但它比标准的浮点加法器或乘法器复杂得多。一种称为 Kulisch 累加的技术可以避免 FMA 的复杂操作。类似的操作在第一台可编程数字计算机 Konrad Zuse Z3 上被采用过。Gustafson 也在他最近的浮点研究中提出了 Kulisch 累加的标准用法(https://www.crcpress.com/The-End-of-Error-Unum-Computing/Gustafson/p/book/9781482239867)。其思想不是在浮点运算中进行累加,而是在定点运算中维护一个运行时的和(running sum),这个和需要足够大以避免下溢或溢出。与浮点加法不同的是,Kulisch 累加可以精确地表示任意数量的浮点值的和。不论顺序如何,求和方法都是满足结合率并可复现的。在完成所有求和工作之后,我们通过尾数对齐和舍入将其转换回浮点数。
下图显示了一个累加步骤的示例。Kulisch 累加器当前包含值 35.5,我们将其与 0.84375 相加,表示为线性域浮点值。这个被求和的浮点值可能以前来自于标量值的乘积,或者只是我们希望累加的单个值。在浮点指数的基础上,通过对齐尾数的基数点,将浮点数转换为定点数。该转换使用了一个调整因子,它是累加器最多尾数位(在下面的例子中是 6)的有效指数。然后将对齐后的尾数和累加器与进位相加。(为简单起见,此处省略了 Kulisch 累加器可能支持下溢和溢出所需的额外精度位)。在高于 32 位的浮点数中,Kulisch 累加的代价很高,因为累加器、移位器和加法器的大小可能大于 500 位,但是对于较小的浮点类型来说,它是非常实用的。
Kulisch 累加不能直接用于对数域求和。但是,正如 Kulisch 累加以不同于参数(浮点数)的形式(定点数)执行求和运算一样,这里也可以采用类似的方法,因此我们不需要一个巨大的 LNS 和/差查找表。我们可以在线性域中对 log 值取近似,在线性域中做 Kulisch 累加,然后在所有求和工作都完成时将其转换回 log 域。这种方法对于通用线性代数非常有效,因为向量内积需要在累加器中多次重复求和。
IEEE 754 的专有机制
有助于减小字大小的 posit 编码也避免了这个问题,因为 posit 的尾数总是规格化的。缓慢的下溢立刻防止了精度下降,而不是逐渐缓解精度下降,这在 IEEE 754 非规格化表示中是通过尾数部分中前导一的位置解决的。Posit 逐渐减小到更小的数字上,导致在指数上使用尾数部分的位,从而扩展了动态范围并降低了精度。Posit 的缩减(taper)技术在功能上类似于非规格化的逐渐下溢,但是没有重新规格化尾数的开销。Posit 缓慢溢出也以类似的方式支持缩减。
融合这些方法
为了获得性能的提升,研究人员考虑将这四种技术结合起来。对数域表示避免了硬件乘法器,我们将 posit 用于对数。为了与 int8/32 抗衡,研究人员考虑使用一种称为 (8, 1, alpha, beta, gamma) 对数的 8-bit 格式,其中 (8,1) 是 posit 参数。这种编码在最大和最小正值之间提供了超过 1600 万:1 的比率,同时在 1.0 左右保留了 4 位(对数域)精度,所有这些都是用 8 位表示的(只有 256 个可能的值)。alpha、beta 和 gamma 值可以控制对数到线性以及线性到对数的转换准确率。
如上所述,我们在线性域中执行对数域的求和。这个结果是非常近似的,但与 FMA 不同的是,对于序列和,这里没有 Kulisch 累加的线性域误差。我们称这种技术为 ELMA,或精确的对数线性乘加。对数域乘法是精确的,所有的线性域求和过程也都是精确的,但是对数到线性的转换是近似的,返回的线性到对数的转换也是近似的。这种折中在实践中是可以接受的。
硬件查找表用于转换,但它们要比 LNS 加法所需表小得多。使用更大的 alpha、beta 和 gamma 参数会得到更精确的结果,但也会消耗更多的芯片面积和功耗。与浮点型 FMA 相比,ELMA 乘加电路及其核心更加简单。它使用三个加法器、一个查找表和一个移位器就完成了大部分工作:
直接替代方法
与 int8/32 不同,Facebook 研究人员针对神经网络的 8 位对数格式不需要学习量化参数、激活采样或对原始网络进行再训练。只需要获取网络(如 ResNet-50)的 32 位浮点参数,然后使用「取与该参数最近的偶数」的规则对其进行转换。使用 posit 编码可以在如此小的类型中保证所需的动态范围和精度。
使用 ELMA 的 (8, 1, 5, 5, 7) 对数与原始的 ResNet-50 的运算方法相同,在 ImageNet 验证集上获得了 75.23% 的 top-1 准确率和 92.66% 的 top-5 准确率,分别比原始网络的准确率降低了 0.9% 和 0.2%。这些结果与许多现有的 int8/32 量化方法类似。在 int8/32 量化中使用的调优训练和模型调整可以进一步提高该方法的性能,但是这里基线的结果是在对软件进行最少修改的情况下实现的。所有的数学仍然在通用浮点运算中执行,使用压缩编码作为量化器。ELMA 设计也可以被用于非线性代数任务(如多项式计算)。
硬件效率
研究人员使用一种商用 28 纳米 ASIC 工艺技术将 (8, 1, 5, 5, 7) 对数的 ELMA 以 int8/32 乘加 0.96 倍的功耗作用于独立的处理单元(PE)。在一个完整的 32×32 矩阵乘法的脉动阵列中,使用对数 ELMA 处理单元方案的功耗是使用 int8/32 处理单元版本的 0.865 倍。该方案之所以能够省电主要是因为取消了硬件乘法器。
扩展到 16 位后,即使没有非规格化的支持(这在很大程度上降低了 IEEE 754 的效率),这种方法使用的功耗是 IEEE 754 半精度 FMA 的 0.59 倍,而使用的芯片面积是后者的 0.68 倍,同时还降低了延迟。得益于这些 16 位浮点数上的性能提升,我们可以在相同的时间内支持更复杂的人工智能模型的训练。然而,与 32 位的 IEEE 754 单精度 FMA 相比,ELMA 并没有更加有效,这是由于 Kulisch 累加器过于庞大(增加了加法器/移位器尺寸和触发器功率),而且禁止使用从对数到线性的查找表。
未来的工作
实现人工智能技术的极大发展需要计算效率的显著提高,而这只有通过新方法才能实现,而不仅仅是建立在旧方法的基础上。例如,软件仿真工作常常很慢,无法有效地在前沿的人工智能模型上测试新的运算方法。不幸的是,在 FPGA/ASIC 硬件上进行实验比在软件上还要困难得多,这使得这些潜在的提升空间在很大程度上没有得到充分的开发。然而,如果开发出利用这些技术的新硬件,它将有利于广泛的人工智能研究和应用。
Facebook 计划研究硬件上的 16 位 ELMA 的设计,并将其在人工智能模型训练和其他任务上的性能表现与 IEEE 754 半精度浮点数和 bfloat16 进行对比。这些新想法和数值近似方法并不总是适用的,但是人工智能提供了一个独特的机会来探索它们的边界,并帮助推翻人们关于硬件可能完成的任务的旧有看法。
原文链接:https://code.fb.com/ai-research/floating-point-math/