本文基于 Hugging Face Transformers、Accelerate 以及 bitsandbytes库。
- Transformers:Hugging Face 提供的一个开源库,包含了多种预训练的 Transformer 模子,方便用户举行各种 NLP 任务。
- Accelerate:Hugging Face 开辟的一个库,用于加快深度学习模子的训练和推理,支持多种硬件加快,如 GPU 和 TPU,简化了多设备训练的实现。
- bitsandbytes:一个用于深度学习的优化库,提供了低精度计算(如 8-bit 算术运算)和其他高效的计算技术,旨在加快模子训练并淘汰资源消耗。
弁言
语言模子不绝在变大。截至 2022年8月17日,PaLM 有 5400 亿参数,OPT、GPT-3 和 BLOOM 有约莫 1760 亿参数,而且我们仍在继续朝着更大的模子发展。下图总结了一些语言模子的尺寸。
由于这些模子很大,因此它们很难在一样平常的设备上运行。举个例子,仅推理 BLOOM-176B 模子,你就需要 8 个 80GB A100 GPU (每个约 15,000 美元)。而如果要微调 BLOOM-176B 的话,你需要 72 个这样的 GPU!更大的模子,如 PaLM,还需要更多资源。
由于这些庞大的模子需要大量 GPU 才能运行,因此我们需要找到低落资源需求而同时保持模子性能的方法。目前已有一些试图缩小模子尺寸的技术,好比你可能听说过的量化和蒸馏等技术。
完成 BLOOM-176B 的训练后,Hugging Face 和 BigScience 不绝在寻找能让这个大模子更容易在更少的 GPU 上运行的方法。通过 BigScience 社区,我们了解到一些有关 Int8 推理的研究,它不会低落大模子的预测性能,而且可以将大模子的内存占用量淘汰 2 倍。很快我们就开始合作举行这项研究,最终将其完全整合到 Hugging Face transformers 中。本文我们将详述我们集成在 Hugging Face 中的 LLM.int8() 方案,它适用于全部 Hugging Face 模子。如果你想了解更多研究细节,可以阅读论文 LLM.int8(): 8-bit Matrix Multiplication for Transformers at Scale。
本文将重要先容 LLM.int8() 量化技术,讨论将其纳入 transformers 库的过程中履历的困难。
通过本文,你将了解到究竟是什么让一个大模子占用这么多内存?是什么让 BLOOM 占用了 350GB 内存?我们先从一些底子知识开始,逐步展开。
机器学习中常用的数据类型
我们从明确差别浮点数据类型开始,这些数据类型在机器学习中也被称为“精度”。
模子的巨细由其参数量及其精度决定,精度通常为 float32、float16 或 bfloat16 之一。
Float32 (FP32) 是标准的 IEEE 32 位浮点表现。利用该数据类型,可以表现大范围的浮点数。在 FP32 中,为“指数”保留了 8 位,“尾数”保留了 23 位,符号保留了 1 位。因为是标准数据类型,所以大部分硬件都支持 FP32 运算指令。
而在 Float16 (FP16) 数据类型中,指数保留 5 位,尾数保留 10 位。这使得 FP16 数字的数值范围远低于 FP32。因此 FP16 存在上溢 (当用于表现非常大的数时) 和下溢 (当用于表现非常小的数时) 的风险。
例如,当你执行 10k * 10k 时,最终结果应为 100M,FP16 无法表现该数,因为 FP16 能表现的最大数是 64k。因此你最终会得到 NaN (Not a Number,不是数字),在神经网络的计算中,因为计算是按层和 batch 顺序举行的,因此一旦出现 NaN,之前的全部计算就全毁了。一样平常情况下,我们可以通过缩放损失 (loss scaling) 来缓解这个题目,但该方法并非总能奏效。
于是我们发明了一种新格式 Bfloat16 (BF16) 来规避这些限制。BF16 为指数保留了 8 位 (与 FP32 雷同),为小数保留了 7 位。这意味着利用 BF16 我们可以保留与 FP32 雷同的动态范围。但是相对于 FP16,我们损失了 3 位精度。因此,在利用 BF16 精度时,大数值绝对没有题目,但是精度会比 FP16 差。
在 Ampere 架构中,NVIDIA 还引入了 TensorFloat-32(TF32) 精度格式,它利用 19 位表现,联合了 BF16 的范围和 FP16 的精度。目前,它仅在某些操纵的内部利用 [即 TF32 是一个计算数据类型而不是存储数据类型]。
在机器学习术语中,FP32 称为全精度 (4 字节),而 BF16 和 FP16 称为半精度 (2 字节)。除此以外,另有 Int8 (INT8) 数据类型,它是一个 8 位的整型数据表现,可以存储 $2^8$ 个差别的值 (对于有符号整数,区间为 [-128, 127],而对于无符号整数,区间为 [0, 255])。
固然理想情况下训练和推理都应该在 FP32 中完成,但 FP32 比 FP16/BF16 慢两倍,因此实践中常常利用混淆精度方法,此中,利用 FP32 权重作为精确的 “主权重 (master weight)”,而利用 FP16/BF16 权重举行前向和后向流传计算以提高训练速度,最后在梯度更新阶段再利用 FP16/BF16 梯度更新 FP32 主权重。
在训练期间,主权重始终为 FP32。而在实践中,在推理时,半精度权重通常能提供与 FP32 相似的精度 —— 因为只有在模子梯度更新时才需要精确的 FP32 权重。这意味着在推理时我们可以利用半精度权重,这样我们仅需一半 GPU 显存就能得到雷同的结果。
以字节为单位计算模子巨细时,需要将参数量乘以所选精度的巨细 (以字节为单位)。例如,如果我们利用 BLOOM-176B 模子的 Bfloat16 版本,其巨细就应为 $176 \times 10^{9} \times 2 字节 = 352GB$!如前所述,这个巨细需要多个 GPU 才能装得下,这是一个相当大的挑战。
但是,如果我们可以利用另外的数据类型来用更少的内存存储这些权重呢?深度学习社区已广泛利用的方法是量化。
模子量化简介
通过实行,我们发现不利用 4 字节 FP32 精度转而利用 2 字节 BF16/FP16 半精度可以得到险些雷同的推理结果,同时模子巨细会减半。这促使我们想进一步削减内存,但随着我们利用更低的精度,推理结果的质量也开始急剧降落。
为了解决这个题目,我们引入了 8 位量化。仅用四分之一精度,因此模子巨细也仅需 1/4!但这次,我们不能简单地丢弃另一半位宽了。
根本上讲,量化过程是从一种数据类型“舍入”到另一种数据类型。举个例子,如果一种数据类型的范围为 0-9,而另一种数据类型的范围为 0-4,则第一种数据类型中的值 4 将舍入为第二种数据类型中的 2 。但是,如果在第一种数据类型中有值 3,它介于第二种数据类型的 1 和 2 之间,那么我们通常会四舍五入为 2。也就是说,第一种数据类型的值 4 和 3 在第二种数据类型中具有雷同的值 2。这充分表明量化是一个有噪过程,会导致信息丢失,是一种有损压缩。
两种最常见的 8 位量化技术是零点量化 (zero-point quantization) 和最大绝对值量化 (absolute maximum quantization,absmax) 。它们都将浮点值映射为更紧凑的 Int8 (1 字节) 值。这些方法的第一步都是用量化常数对输入举行归一化缩放。
在零点量化中,如果我的数值范围是 -1.0-1.0,我想量化到 -127…127,我需要先缩放 127倍,然后四舍五入到 8 位精度。要恢复原始值,我需要将 Int8 值除以雷同的量化因子 127。在这个例子中,值 0.3 将缩放为 0.3*127 = 38.1。四舍五入后得到值 38。恢复时,我们会得到 38/127=0.2992 —— 因此最终会有 0.008 的量化毛病。这些看似微小的毛病在沿着模子各层流传时往往会累积和增长,从而导致最终的精度降落。
译者注: 这个例子举得不好,因为浮点范围和整型范围都是对称的,所以不存在零点调解了,而零点调解是零点量化中最能体现其定名原因的部分。简而言之,零点量化分为两步,第一步值域映射,即通过缩放将原始的数值范围映射为量化后的数值范围; 第二步零点调解,即通过平移将映射后的数据的最小值对齐为目标值域的最小值
现在我们再看下 absmax 量化的细节。要计算 absmax 量化中 fp16 数与其对应的 int8 数之间的映射,你必须先除以张量的最大绝对值,然后再乘以数据类型的最大可表现值。
例如,假设你要用 absmax 对向量 [1.2, -0.5, -4.3, 1.2, -3.1, 0.8, 2.4, 5.4] 举行量化。首先需要计算该向量元素的最大绝对值,在本例中为 5.4。 Int8 的范围为 [-127, 127],因此我们将 127 除以 5.4,得到缩放因子 23.5。最后,将原始向量乘以缩放因子得到最终的量化向量 [28, -12, -101, 28, -73, 19, 56, 127]。
要恢复原向量,可以将 int8 量化值除以缩放因子,但由于上面的过程是“四舍五入”的,我们将丢失一些精度。
对于无符号 Int8,我们可以先减去最小值然后再用最大绝对值来缩放,这与零点量化的做法相似。其做法也与最小 - 最大缩放 (min-max scaling) 雷同,但后者在缩放时会额外保证输入中的 0 始终映射到一个整数,从而保证 0 的量化是无毛病的。
当举行矩阵乘法时,我们可以通过组合各种技巧,例如逐行或逐向量量化,来获取更精确的结果。举个例子,对矩阵乘法 $A \times B=C$,我们不会直接利用常规量化方式,即用整个张量的最大绝对值对张量举行归一化,而会转而利用向量量化方法,找到 A 的每一行和 B 的每一列的最大绝对值,然后逐行或逐列归一化 A 和 B 。最后将 A 与 B 相乘得到 C。最后,我们再计算与 A 和 B 的最大绝对值向量的外积,并将此与 C 求哈达玛积来反量化回 FP16。有关此技术的更多详细信息可以参考 LLM.int8() 论文 或 Tim 的博客上的 关于量化和涌现特性的博文。
固然这些根本技术能够帮助我们量化深度学习模子,但它们通常会导致大模子正确性的降落。集成到 Hugging Face Transformers 和 Accelerate 库中的 LLM.int8() 是第一个适用于大模子 (如 BLOOM-176B) 且不会低落正确性的量化技术。
扼要总结 LLM.int8(): 大语言模子的零退化矩阵乘法
在 LLM.int8() 中,我们已经证明明确 transformer 模子表现出的与模子规模相干的涌现特性对于明确为什么传统量化对大模子失效至关重要。我们证明性能降落是由离群特性 (outlier feature) 引起的,下一节我们会详细解释。LLM.int8() 算法本身如下。
本质上,LLM.int8() 通过三个步骤完成矩阵乘法计算:
- 从输入的隐含状态中,按列提取异常值 (即大于某个阈值的值)。
- 对 FP16 离群值矩阵和 Int8 非离群值矩阵分别作矩阵乘法。
- 反量化非离群值的矩阵乘结果并其与离群值矩阵乘结果相加,得到最终的 FP16 结果。
该过程可以总结为如下动画:
离群特性的重要性
超出某个分布范围的值通常称为离群值。离群值检测已得到广泛应用,在很多文献中也有涉及,且获取特性的先验分布对离群值检测任务很有助益。更具体地说,我们观察到对于参数量大于 6B 的 transformer 模子,经典的量化方法会失效。固然离群值特性也存在于较小的模子中,但在大于 6B 的 transformer 模子中,我们观察到险些每层都会出现超出特定阈值的离群点,而且这些离群点呈现出肯定的体系性模式。有关该征象的更多详细信息,请参阅 LLM.int8() 论文 和 涌现特性的博文。
如前所述,8 位精度的动态范围极其有限,因此量化具有多个大值的向量会产生严峻毛病。此外,由于 transformer 架构的固有特性,它会将全部元素互相干联起来,这样的话,这些毛病在流传几层后往往会混杂在一起。混淆精度分解的方法可以对此类极端离群值举行有效量化。接下来我们对此方法举行讨论。
MatMul 内部
计算隐含状态后,我们利用自定义阈值提取离群值,并将矩阵分解为两部分。我们发现,以这种方式提取全部幅度大于即是 6 的离群值可以完全恢复推理精度。离群值部分利用 FP16 表现,因此它是一个经典的矩阵乘法,而 8 位矩阵乘法是通过利用向量量化将权重和隐含状态分别量化为 8 位精度 - 即按行量化权重矩阵,并按列量化隐含状态,然后再举行相应向量乘加操纵。最后,将结果反量化至半精度,以便与第一个矩阵乘法的结果相加。
0 退化是什么意思?
我们如何正确评估该方法是否会对性能造成降落?利用 8 位模子时,我们的生成质量损失了多少?
我们利用 lm-eval-harness 在 8 位和原始模子上运行了几个常见的基准测试,结果如下。
对 OPT-175B 模子:
测试基准
| -
| -
| -
| -
| 差值
| 测试基准名
| 指标
| 指标值 - int8
| 指标值 - fp16
| 标准差 - fp16
| -
| hellaswag
| acc_norm
| 0.7849
| 0.7849
| 0.0041
| 0
| hellaswag
| acc
| 0.5921
| 0.5931
| 0.0049
| 0.001
| piqa
| acc
| 0.7965
| 0.7959
| 0.0094
| 0.0006
| piqa
| acc_norm
| 0.8101
| 0.8107
| 0.0091
| 0.0006
| lambada
| ppl
| 3.0142
| 3.0152
| 0.0552
| 0.001
| lambada
| acc
| 0.7464
| 0.7466
| 0.0061
| 0.0002
| winogrande
| acc
| 0.7174
| 0.7245
| 0.0125
| 0.0071
| 对 BLOOM-176 模子:
测试基准
| -
| -
| -
| -
| 差值
| 测试基准名
| 指标
| 指标值 - int8
| 指标值 - fp16
| 标准差 - fp16
| -
| hellaswag
| acc_norm
| 0.7274
| 0.7303
| 0.0044
| 0.0029
| hellaswag
| acc
| 0.5563
| 0.5584
| 0.005
| 0.0021
| piqa
| acc
| 0.7835
| 0.7884
| 0.0095
| 0.0049
| piqa
| acc_norm
| 0.7922
| 0.7911
| 0.0095
| 0.0011
| lambada
| ppl
| 3.9191
| 3.931
| 0.0846
| 0.0119
| lambada
| acc
| 0.6808
| 0.6718
| 0.0065
| 0.009
| winogrande
| acc
| 0.7048
| 0.7048
| 0.0128
| 0
| 我们可以看到上述这些模子的性能降落为 0,因为指标的绝对差异均低于原始模子的标准毛病 (BLOOM-int8 除外,它在 lambada 上略好于原始模子)。
比原始模子更快吗?
LLM.int8() 方法的重要目标是在不低落性能的情况下低落大模子的应用门槛。但如果速度非常慢,该方法用处也不会很大。所以我们对多个模子的生成速度举行了基准测试。
我们发现利用了 LLM.int8() 的 BLOOM-176B 比 FP16 版本慢了约莫 15% 到 23% —— 这应该是完全可以担当的。我们发现较小模子 (如 T5-3B 和 T5-11B) 的降速幅度更大。我们还在积极优化这些小模子的推理速度。在一天之内,我们可以将 T5-3B 的每词元推理延迟从 312 毫秒低落到 173 毫秒,将 T5-11B 从 45 毫秒低落到 25 毫秒。此外,我们 已经找到原因,在即将发布的版本中,LLM.int8() 在小模子上的推理速度可能会更快。下表列出了当前版本的一些性能数据。
精度
| 参数量
| 硬件
| 每词元延迟 (单位: 毫秒,batch size: 1)
| 每词元延迟 (单位: 毫秒,batch size: 8)
| 每词元延迟 (单位: 毫秒,batch size: 32)
| bf16
| 176B
| 8xA100 80GB
| 239
| 32
| 9.9
| int8
| 176B
| 4xA100 80GB
| 282
| 37.5
| 10.2
| bf16
| 176B
| 14xA100 40GB
| 285
| 36.5
| 10.4
| int8
| 176B
| 5xA100 40GB
| 367
| 46.4
| oom
| fp16
| 11B
| 2xT4 15GB
| 11.7
| 1.7
| 0.5
| int8
| 11B
| 1xT4 15GB
| 43.5
| 5.3
| 1.3
| fp32
| 3B
| 2xT4 15GB
| 45
| 7.2
| 3.1
| int8
| 3B
| 1xT4 15GB
| 312
| 39.1
| 10.2
| 上表中的 3 个模子分别为 BLOOM-176B、T5-11B 和 T5-3B。
如何在 transformers 中利用它
硬件要求
CPU 不支持 8 位张量焦点 [Intel 最新的 Sapphire Rapids CPU 已支持 8 位张量指令集: AMX]。 bitsandbytes 可以在支持 8 位张量焦点的硬件上运行,这些硬件有 Turing 和 Ampere GPU (RTX 20s、RTX 30s、A40-A100、T4+)。例如,Google Colab GPU 通常是 NVIDIA T4 GPU,而最新的 T4 是支持 8 位张量焦点的。我们反面的演示将会基于 Google Colab!
安装
利用以下命令安装最新版本的库 (确保你的 python>=3.8)。
- pip install accelerate
- pip install bitsandbytes
- pip install git+https://github.com/huggingface/transformers.git
复制代码 CPU 的支持
正如本文开头所述,CPU 设备不支持 8 位张量核。然而,在 CPU 上运行此模块可以明显提高可用性和可访问性。[最新的 Intel CPU 已支持 8 位张量核]
扩展至其他模态
目前,大模子以语言模子为主。在超大视觉、音频和多模态模子上应用这种方法可能会很有意思,因为随着这些模子在未来几年变得越来越多,它们的易用性也会越来越重要。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |