Hugging Face 将 LLM.int8() 8-bit 量化集成进 transformers 与 accelerate
A Gentle Introduction to 8-bit Matrix Multiplication for transformers at scale using transformers, accelerate and bitsandbytes
Hugging Face 联合 BigScience 将 LLM.int8() 8 位矩阵乘法量化集成到 transformers 和 accelerate,声称在 BLOOM-176B、OPT-175B 等模型上推理性能无下降,显存占用减半,例如 BLOOM-176B 只需 4 张 80GB A100 而非 8 张。
Hugging Face 与 bitsandbytes 合作把 LLM.int8() 落入 transformers,读者可了解大模型量化省显存的条件与代价。
引言
语言模型的规模一直在不断扩大。截至本文撰写之时,PaLM 拥有 5400 亿参数,OPT、GPT-3 和 BLOOM 则大约有 1760 亿参数,并且这一趋势正朝着更大模型的方向发展。下图展示了一些近期语言模型的规模。
因此,这些模型很难在易于获取的设备上运行。例如,仅对 BLOOM-176B 进行推理,就需要配备8块80GB的A100 GPU(每块约1.5万美元)。若要对 BLOOM-176B 进行微调,则需要72块这样的GPU!而像 PaLM 这样更庞大的模型,所需资源更是成倍增加。
由于这些巨型模型运行所需GPU数量庞大,我们必须寻找方法来减少这些需求,同时保持模型性能。为此,业界开发了多种技术来尝试缩小模型尺寸,你可能已经听说过量化和蒸馏,还有许多其他方法。
在完成 BLOOM-176B 的训练后,我们 HuggingFace 和 BigScience 团队一直在寻找让这个大型模型能在更少GPU上更容易运行的方法。通过我们的BigScience社区,我们了解到一项关于Int8推理的研究,它不会降低大型模型的预测性能,并能将大型模型的内存占用减少2倍。很快,我们开始在这项研究上合作,最终将其完全整合到HF Mirrortransformers中。通过这篇博客文章,我们为所有HF Mirror模型提供了LLM.int8()集成,下面我们将更详细地解释。如果你想了解更多关于我们研究的内容,可以阅读我们的论文LLM.int8(): 大规模Transformer的8位矩阵乘法。
本文旨在提供这项量化技术的高级概述,概述将其纳入transformers库中的难点,并规划此合作的长期目标。
在这里,你将了解到究竟是什么让一个大型模型占用如此多的内存?是什么让BLOOM占用350GB?让我们从逐步回顾几个基本前提开始。
机器学习中常用的数据类型
我们首先了解不同浮点数据类型的基础知识,这些在机器学习背景下也被称为“精度”。
模型的大小取决于其参数数量及其精度,通常为float32、float16或bfloat16之一(下图来源:https://blogs.nvidia.com/blog/2020/05/14/tensorfloat-32-precision-format/)。
Float32(FP32)代表标准的IEEE 32位浮点表示。使用这种数据类型,可以表示广泛的浮点数。在FP32中,8位保留给“指数”,23位保留给“尾数”,1位用于数字的符号。此外,大多数硬件支持FP32运算和指令。
在float16(FP16)数据类型中,5位保留给指数,10位保留给尾数。这使得FP16数字的可表示范围远低于FP32。这让FP16数字面临溢出(试图表示非常大的数字)和下溢(表示非常小的数字)的风险。
例如,如果你执行 10k * 10k,你会得到 100M,这在 FP16 中无法表示,因为最大可能的数是 64k。因此,你会得到 NaN(非数字)结果,并且如果你有像神经网络中的顺序计算,所有先前的工作都会被破坏。
通常,使用损失缩放来克服这个问题,但它并不总是有效。
一种新的格式 bfloat16(BF16)被创建出来以避免这些限制。在 BF16 中,8 位保留给指数(与 FP32 相同),7 位保留给小数。
这意味着在 BF16 中我们可以保持与 FP32 相同的动态范围。但相对于 FP16,我们失去了 3 位精度。现在对于巨大的数字绝对没有问题,但这里的精度比 FP16 差。
在 Ampere 架构中,NVIDIA 还引入了 TensorFloat-32(TF32)精度格式,结合了 BF16 的动态范围和 FP16 的精度,仅使用 19 位。它目前仅在特定操作内部使用。
在机器学习术语中,FP32 被称为全精度(4 字节),而 BF16 和 FP16 被称为半精度(2 字节)。 此外,int8(INT8)数据类型由 8 位表示组成,可以存储 2^8 个不同的值(对于有符号整数,范围在 [0, 255] 或 [-128, 127] 之间)。
理想情况下,训练和推理应在 FP32 中进行,但它比 FP16/BF16 慢两倍,因此采用混合精度方法,其中权重以 FP32 保存作为精确的“主权重”参考,而前向和后向传递中的计算则以 FP16/BF16 进行以提高训练速度。然后使用 FP16/BF16 梯度来更新 FP32 主权重。
在训练期间,主权重始终以 FP32 存储,但在实践中,半精度权重在推理期间通常提供与 FP32 对应物相似的质量——只有当模型接收到多个梯度更新时,才需要精确的参考。这意味着我们可以使用半精度权重,并使用一半的 GPU 来实现相同的结果。
要计算以字节为单位的模型大小,需要将参数数量乘以所选精度的字节大小。例如,如果我们使用 BLOOM-176B 模型的 bfloat16 版本,我们有 176*10**9 x 2 bytes = 352GB!如前所述,将其放入几个 GPU 中是一个相当大的挑战。
但是,如果我们可以使用不同的数据类型以更少的内存存储这些权重呢?一种称为量化的方法已在深度学习中广泛使用。
模型量化简介
通过实验,我们发现,不使用 4 字节的 FP32 精度,我们可以使用 2 字节的 BF16/FP16 半精度获得几乎相同的推理结果,这将模型大小减半。进一步减少会很棒,但在更低的精度下,推理质量结果开始急剧下降。
为了解决这个问题,我们引入了 8 位量化。这种方法使用四分之一精度,因此只需要模型大小的 1/4!但这并不是通过简单地再减少一半的位数来实现的。
量化的本质是从一种数据类型“舍入”到另一种数据类型。例如,如果一种数据类型的范围是 0..9,另一种是 0..4,那么第一种数据类型中的值“4”会被舍入为第二种数据类型中的“2”。然而,如果第一种数据类型中的值是“3”,它位于第二种数据类型的 1 和 2 之间,那么我们通常会舍入到“2”。这表明第一种数据类型中的值“4”和“3”在第二种数据类型中具有相同的值“2”。这突出说明量化是一个有噪声的过程,可能导致信息丢失,是一种有损压缩。
两种最常见的 8 位量化技术是零点量化和绝对最大值(absmax)量化。零点量化和 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 数之间的映射,你必须先除以张量的绝对最大值,然后乘以数据类型的总范围。
例如,假设你想对一个包含 [1.2, -0.5, -4.3, 1.2, -3.1, 0.8, 2.4, 5.4] 的向量应用 absmax 量化。你提取它的绝对最大值,在这个例子中是 5.4。Int8 的范围是 [-127, 127],所以我们将 127 除以 5.4,得到缩放因子 23.5。因此,将原始向量乘以它,就得到量化后的向量 [28, -12, -101, 28, -73, 19, 56, 127]。
要恢复原始值,只需将 int8 数除以量化因子,并以全精度进行运算,但由于上述结果是“舍入”的,会丢失一些精度。
对于无符号 int8,我们会减去最小值,并除以绝对最大值进行缩放。这接近零点量化的做法。它类似于 min-max 缩放,但后者维持值的缩放方式,使得值“0”总是由一个整数表示,且没有任何量化误差。
这些技巧可以通过多种方式组合,例如,在矩阵乘法中,为了获得更准确的结果,可以使用逐行或逐向量量化。考虑矩阵乘法 A*B=C,与按每个张量的绝对最大值进行归一化的常规量化不同,逐向量量化会找到 A 的每一行和 B 的每一列的绝对最大值。然后我们通过除以这些向量来归一化 A 和 B。接着我们相乘 A*B 得到 C。最后,为了恢复 FP16 值,我们通过计算 A 和 B 的绝对最大值向量的外积来进行反归一化。有关此技术的更多细节,可以在 LLM.int8() 论文 或 Tim 博客上关于 量化和涌现特征的博文 中找到。
虽然这些基本技术使我们能够量化深度学习模型,但它们通常会导致较大模型的准确率下降。我们集成到 HF Mirror Transformers 和 Accelerate 库中的 LLM.int8() 实现是第一种即使对于具有 176B 参数的大型模型(例如 BLOOM)也不会降低性能的技术。
LLM.int8() 的简要总结:大型语言模型的零退化矩阵乘法
在 LLM.int8() 中,我们证明了理解 Transformer 中尺度依赖的涌现特性至关重要,这样才能理解为什么传统量化在大型模型上会失败。我们证明了性能下降是由离群特征引起的,我们将在下一节中解释这一点。LLM.int8() 算法本身可以解释如下。
本质上,LLM.int8() 试图通过三个步骤完成矩阵乘法计算:
- 从输入的隐藏状态中,按列提取离群值(即大于某个阈值的值)。
- 用 FP16 对离群值进行矩阵乘法,用 int8 对非离群值进行矩阵乘法。
- 将非离群值的结果反量化,并将离群值结果与非离群值结果相加,以 FP16 得到完整结果。
这些步骤可以总结为以下动画:
离群特征的重要性
超出某些数字全局分布范围的值通常被称为离群值。离群值检测已被广泛使用并涵盖在当前文献中,而事先了解特征分布有助于完成离群值检测任务。更具体地说,我们观察到,经典量化在规模上对大于 6B 参数的基于 Transformer 的模型会失败。虽然在较小模型中也存在大型离群特征,但我们观察到,超过某个阈值后,这些离群值来自 Transformer 中高度系统化的模式,这些模式存在于 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 | 值 - bf16 | 标准误差 - bf16 | - |
| 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 |
我们确实观察到这些模型没有性能退化,因为指标的绝对差异都低于标准误差(除了 BLOOM-int8 在 lambada 上略优于原生模型)。如需针对最新方法的更详细性能评估,请查看论文!
它比原生模型更快吗?
LLM.int8() 方法的主要目的是让大型模型在不损失性能的情况下更易于使用。但如果速度非常慢,这种方法就没那么有用了。因此我们基准测试了多个模型的生成速度。 我们发现,使用 LLM.int8() 的 BLOOM-176B 比 fp16 版本慢约 15% 到 23%——这仍然相当可以接受。我们发现较小模型(如 T5-3B 和 T5-11B)的减速更大。我们努力加速这些小型模型。在一天之内,我们将 T5-3B 的每 token 推理时间从 312 毫秒提高到 173 毫秒,将 T5-11B 的从 45 毫秒提高到 25 毫秒。此外,问题已经被识别,LLM.int8() 在即将发布的版本中对于小型模型可能会更快。目前,当前数字如下表所示。
| 精度 | 参数量 | 硬件 | 批大小 1 时每 token 时间(毫秒) | 批大小 8 时每 token 时间(毫秒) | 批大小 32 时每 token 时间(毫秒) |
|---|---|---|---|---|---|
| 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。
HF Mirror transformers 集成细节
接下来让我们讨论 HF Mirror transformers 集成的具体细节。让我们看看使用方法以及在尝试设置时可能遇到的常见问题。
用法
负责本博客文章中描述的全部魔法的模块称为 Linear8bitLt,您可以轻松地从 bitsandbytes 库中导入它。它派生自经典的 torch.nn Module,可以通过下面描述的代码轻松地在您的架构中使用和部署。
以下是以下用例的分步示例:假设您想使用 bitsandbytes 将一个小模型转换为 int8。
- 首先我们需要下面的正确导入!
import torch
import torch.nn as nn
import bitsandbytes as bnb
from bnb.nn import Linear8bitLt
- 然后您可以定义自己的模型。请注意,您可以将任何精度(FP16、BF16 或 FP32)的检查点或模型转换为 8 位,但目前,模型的输入必须是 FP16,我们的 Int8 模块才能工作。因此我们在此将模型视为 fp16 模型。
fp16_model = nn.Sequential(
nn.Linear(64, 64),
nn.Linear(64, 64)
)
- 假设您已经在您最喜欢的数据集和任务上训练了模型!现在该保存模型了:
[... train the model ...]
torch.save(fp16_model.state_dict(), "model.pt")
- 现在您的
state_dict已保存,让我们定义一个 int8 模型:
int8_model = nn.Sequential(
Linear8bitLt(64, 64, has_fp16_weights=False),
Linear8bitLt(64, 64, has_fp16_weights=False)
)
这里非常重要的一点是添加标志 has_fp16_weights。默认情况下,它设置为 True,用于以混合 Int8/FP16 精度进行训练。然而,我们感兴趣的是内存高效的推理,为此我们需要使用 has_fp16_weights=False。
- 现在该以 8 位加载您的模型了!
int8_model.load_state_dict(torch.load("model.pt"))
int8_model = int8_model.to(0) # Quantization happens here
请注意,量化步骤是在第二行中模型被设置到 GPU 上之后完成的。如果你在调用 .to 函数之前打印 int8_model[0].weight,你会得到:
int8_model[0].weight
Parameter containing:
tensor([[ 0.0031, -0.0438, 0.0494, ..., -0.0046, -0.0410, 0.0436],
[-0.1013, 0.0394, 0.0787, ..., 0.0986, 0.0595, 0.0162],
[-0.0859, -0.1227, -0.1209, ..., 0.1158, 0.0186, -0.0530],
...,
[ 0.0804, 0.0725, 0.0638, ..., -0.0487, -0.0524, -0.1076],
[-0.0200, -0.0406, 0.0663, ..., 0.0123, 0.0551, -0.0121],
[-0.0041, 0.0865, -0.0013, ..., -0.0427, -0.0764, 0.1189]],
dtype=torch.float16)
而如果你在第二行调用之后打印它,你会得到:
int8_model[0].weight
Parameter containing:
tensor([[ 3, -47, 54, ..., -5, -44, 47],
[-104, 40, 81, ..., 101, 61, 17],
[ -89, -127, -125, ..., 120, 19, -55],
...,
[ 82, 74, 65, ..., -49, -53, -109],
[ -21, -42, 68, ..., 13, 57, -12],
[ -4, 88, -1, ..., -43, -78, 121]],
device='cuda:0', dtype=torch.int8, requires_grad=True)
正如我们在前几节解释量化时所看到的,权重值被“截断”了。此外,这些值似乎分布在 [-127, 127] 之间。 你可能还想知道如何检索 FP16 权重,以便在 fp16 中执行离群值 MatMul?你可以简单地这样做:
(int8_model[0].weight.CB * int8_model[0].weight.SCB) / 127
然后你会得到:
tensor([[ 0.0028, -0.0459, 0.0522, ..., -0.0049, -0.0428, 0.0462],
[-0.0960, 0.0391, 0.0782, ..., 0.0994, 0.0593, 0.0167],
[-0.0822, -0.1240, -0.1207, ..., 0.1181, 0.0185, -0.0541],
...,
[ 0.0757, 0.0723, 0.0628, ..., -0.0482, -0.0516, -0.1072],
[-0.0194, -0.0410, 0.0657, ..., 0.0128, 0.0554, -0.0118],
[-0.0037, 0.0859, -0.0010, ..., -0.0423, -0.0759, 0.1190]],
device='cuda:0')
这已经足够接近原始的 FP16 值了(上面打印了两次)!
- 现在你可以安全地使用你的模型进行推理了,只需确保你的输入位于正确的 GPU 上并且是 FP16 格式:
input_ = torch.randn((1, 64), dtype=torch.float16)
hidden_states = int8_model(input_.to(torch.device('cuda', 0)))
查看 示例脚本 以获取完整的最小代码!
另外,你应该注意到这些模块与 nn.Linear 模块略有不同,因为它们的参数来自 bnb.nn.Int8Params 类而不是 nn.Parameter 类。稍后你会看到,这在我们的旅程中带来了额外的障碍!
现在是时候理解如何将其集成到 transformers 库中了!
accelerate 就是你需要的一切
在处理巨型模型时,accelerate 库包含了许多有用的工具。init_empty_weights 方法尤其有用,因为任何模型,无论大小,都可以使用此方法作为上下文管理器进行初始化,而无需为模型权重分配任何内存。
import torch.nn as nn
from accelerate import init_empty_weights
with init_empty_weights():
model = nn.Sequential([nn.Linear(100000, 100000) for _ in range(1000)]) # This will take ~0 RAM!
初始化后的模型将被放置在 PyTorch 的 meta 设备上,这是一种底层机制,用于表示形状和数据类型,而不分配存储内存。这有多酷?
最初,此函数在 .from_pretrained 函数内部被调用,并将所有参数覆盖为 torch.nn.Parameter。这不符合我们的要求,因为如上述,我们希望针对 Linear8bitLt 模块保留 Int8Params 类。我们在 以下 PR 中成功修复了这个问题,该 PR 修改了:
module._parameters[name] = nn.Parameter(module._parameters[name].to(torch.device("meta")))
为
param_cls = type(module._parameters[name])
kwargs = module._parameters[name].__dict__
module._parameters[name] = param_cls(module._parameters[name].to(torch.device("meta")), **kwargs)
既然这个问题已经修复,我们可以轻松利用这个上下文管理器,并使用一个自定义函数,在不消耗内存的情况下将所有 nn.Linear 模块替换为 bnb.nn.Linear8bitLt!
def replace_8bit_linear(model, threshold=6.0, module_to_not_convert="lm_head"):
for name, module in model.named_children():
if len(list(module.children())) > 0:
replace_8bit_linear(module, threshold, module_to_not_convert)
if isinstance(module, nn.Linear) and name != module_to_not_convert:
with init_empty_weights():
model._modules[name] = bnb.nn.Linear8bitLt(
module.in_features,
module.out_features,
module.bias is not None,
has_fp16_weights=False,
threshold=threshold,
)
return model
此函数递归地替换给定模型中所有在 meta 设备上初始化的 nn.Linear 层,并将它们替换为 Linear8bitLt 模块。必须将属性 has_fp16_weights 设置为 False,以便直接加载 int8 中的权重以及量化统计信息。
我们还放弃了对某些模块(此处为 lm_head)的替换,因为我们希望保留最新模块的原生精度,以获得更精确和稳定的结果。
但这还没有结束!上述函数是在 init_empty_weights 上下文管理器下执行的,这意味着新模型仍将位于 meta 设备上。
对于在此上下文管理器下初始化的模型,accelerate 将手动加载每个模块的参数并将它们移动到正确的设备上。
在 bitsandbytes 中,设置 Linear8bitLt 模块的设备是一个关键步骤(如果你好奇,可以查看 此处 的代码片段),正如我们在玩具脚本中所见。
在这里,量化步骤在第二次调用时失败了。我们不得不实现 accelerate 的 set_module_tensor_to_device 函数(称为 set_module_8bit_tensor_to_device)以确保不会调用两次。让我们在下面的章节中详细讨论这一点!
使用 accelerate 设置设备时要非常小心
在这里,我们与 accelerate 库进行了一场非常微妙的平衡!
一旦你加载了模型并将其置于正确的设备上,有时你仍需要调用 set_module_tensor_to_device 来在所有设备上通过钩子分派模型。这是在 accelerate 的 dispatch_model 函数内部完成的,这可能涉及多次调用 .to,是我们希望避免的情况。
为了实现我们的目标,需要2个拉取请求!最初提出的PR 在这里 导致了一些测试失败,但 这个PR 成功修复了一切!
总结一下
因此,终极配方是:
- 在
meta设备上使用正确的模块初始化模型 - 在正确的GPU设备上逐一设置参数,并确保不要重复执行此过程!
- 在所有地方正确放置新的关键字参数,并添加一些优秀的文档
- 添加非常全面的测试!详情请查看我们的测试 这里 这听起来可能很简单,但我们一起经历了许多艰难的调试过程,常常涉及到CUDA内核!
总而言之,这次集成冒险非常有趣;从深入探索和对不同库进行一些“手术”,到调整一切并使其正常工作!
现在来看看如何从这次集成中获益,以及如何在transformers中成功使用它!
如何在transformers中使用
硬件要求
CPU不支持8位张量核心。bitsandbytes可以在支持8位张量核心的硬件上运行,即Turing和Ampere GPU(RTX 20系列、RTX 30系列、A40-A100、T4及以上)。例如,Google Colab的GPU通常是NVIDIA T4 GPU,它们的最新一代GPU确实支持8位张量核心。我们的演示基于Google Colab,所以请查看下面的内容!
安装
只需使用以下命令安装最新版本的库(确保使用python>=3.8),然后运行以下命令进行尝试
pip install accelerate
pip install bitsandbytes
pip install git+https://github.com/huggingface/transformers.git
示例演示 - 在Google Colab上运行T5 11b
查看在BLOOM-3B模型上运行8位模型的Google Colab演示!
以下是运行T5-11B的演示。T5-11B模型检查点使用的是FP32,占用42GB内存,无法在Google Colab上运行。使用我们的8位模块,它仅使用11GB,轻松适配:
或者这个BLOOM-3B的演示:
改进范围
我们认为,这种方法极大地提高了对超大型模型的可访问性。在不降低性能的情况下,它使计算资源较少的用户能够访问以前无法访问的模型。 我们已经发现了一些可以未来改进的领域,以使这种方法对大型模型更加有效!
小型模型更快的推理速度
正如我们在基准测试部分所看到的,我们可以将小型模型(参数≤6B)的运行速度提高近2倍。然而,尽管对于像BLOOM-176B这样的大型模型,推理速度已经相当稳定,但对于小型模型仍有改进空间。我们已经确定了问题所在,很可能恢复到与fp16相同的性能,或获得小幅加速。这些变化将在接下来几周内整合。
对Kepler GPU(如GTX 1080等)的支持
虽然我们支持过去四年中的所有 GPU,但像 GTX 1080 这样的老 GPU 仍被大量使用。尽管这些 GPU 没有 Int8 张量核心,但它们确实拥有 Int8 向量单元(一种“弱”张量核心)。因此,这些 GPU 也能体验到 Int8 加速。然而,实现快速推理需要一套完全不同的软件栈。虽然我们确实计划集成对 Kepler GPU 的支持,以使 LLM.int8() 功能更广泛地可用,但由于其复杂性,实现这一目标尚需时日。
在 Hub 上保存 8 位状态字典
目前,8 位状态字典在推送到 Hub 后无法直接加载到 8 位模型中。这是因为模型计算的统计数据(记得 weight.CB 和 weight.SCB)目前并未存储在状态字典中,也未在状态字典中予以考虑,而且 Linear8bitLt 模块尚不支持此功能。
我们认为,能够保存这些数据并将其推送到 Hub 可能有助于提高可访问性。
CPU 支持
正如本博客文章开头所述,CPU 设备不支持 8 位核心。然而,我们能克服这一点吗?在 CPU 上运行此模块也将显著提升可用性和可访问性。
扩展到其他模态
目前,语言模型在超大型模型中占据主导地位。随着未来几年这些模型变得更加易于获取,将这种方法应用于超大型视觉、音频和多模态模型,可能会成为提高可访问性的一项有趣尝试。
致谢
衷心感谢以下人士为提高文章可读性以及在 transformers 集成过程中做出的贡献(按字母顺序排列):
JustHeuristic (Yozh),
Michael Benayoun,
Stas Bekman,
Steven Liu,
Sylvain Gugger,
Tim Dettmers
来源:Hugging Face Blog · hf-mirror.com







