跳到正文
原文
Hugging Face Blog·· 2022-08-17精选AI 评分71

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

AI 导读

Hugging Face 联合 BigScience 将 LLM.int8() 8 位矩阵乘法量化集成到 transformers 和 accelerate,声称在 BLOOM-176B、OPT-175B 等模型上推理性能无下降,显存占用减半,例如 BLOOM-176B 只需 4 张 80GB A100 而非 8 张。

推荐理由

Hugging Face 与 bitsandbytes 合作把 LLM.int8() 落入 transformers,读者可了解大模型量化省显存的条件与代价。

正文 · AI 翻译

thumbnail

引言

语言模型的规模一直在不断扩大。截至本文撰写之时,PaLM 拥有 5400 亿参数,OPT、GPT-3 和 BLOOM 则大约有 1760 亿参数,并且这一趋势正朝着更大模型的方向发展。下图展示了一些近期语言模型的规模。

LLM

因此,这些模型很难在易于获取的设备上运行。例如,仅对 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/)。

Summary

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 来实现相同的结果。

Model-storage

要计算以字节为单位的模型大小,需要将参数数量乘以所选精度的字节大小。例如,如果我们使用 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 的量化误差。这些看似微小的误差往往会累积,并随着它们在模型各层中传播而增大,最终导致性能下降。

quantization

(图片取自:这篇博文)

现在我们来看一下 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]。

out-quant.gif

要恢复原始值,只需将 int8 数除以量化因子,并以全精度进行运算,但由于上述结果是“舍入”的,会丢失一些精度。

quant-freeze

对于无符号 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() 试图通过三个步骤完成矩阵乘法计算:

  1. 从输入的隐藏状态中,按列提取离群值(即大于某个阈值的值)。
  2. 用 FP16 对离群值进行矩阵乘法,用 int8 对非离群值进行矩阵乘法。
  3. 将非离群值的结果反量化,并将离群值结果与非离群值结果相加,以 FP16 得到完整结果。

这些步骤可以总结为以下动画:

Mixed-int8.gif

离群特征的重要性

超出某些数字全局分布范围的值通常被称为离群值。离群值检测已被广泛使用并涵盖在当前文献中,而事先了解特征分布有助于完成离群值检测任务。更具体地说,我们观察到,经典量化在规模上对大于 6B 参数的基于 Transformer 的模型会失败。虽然在较小模型中也存在大型离群特征,但我们观察到,超过某个阈值后,这些离群值来自 Transformer 中高度系统化的模式,这些模式存在于 Transformer 的每一层中。有关这些现象的更多细节,请参见 LLM.int8() 论文和涌现特征博客文章。

如前所述,8 位精度受到极大限制,因此量化一个包含若干大值的向量可能会产生极其错误的结果。此外,由于基于 Transformer 的架构具有将所有元素连接在一起的内置特性,这些错误在传播到多个层时往往会叠加。因此,开发了混合精度分解,以便在这种极端离群值下实现高效量化。接下来将讨论这一点。

MatMul 内部

一旦计算出隐藏状态,我们就使用自定义阈值提取离群值,并按上述方式将矩阵分解为两部分。我们发现,以这种方式提取所有幅度为 6 或更大的离群值可以恢复完整的推理性能。离群值部分以 fp16 完成,因此是经典矩阵乘法,而 8 位矩阵乘法则是通过使用向量级量化将权重和隐藏状态量化为 8 位精度来完成的——也就是说,对隐藏状态进行按行量化,对权重矩阵进行按列量化。 此步骤之后,结果会被反量化并以半精度返回,以便将它们加到第一个矩阵乘法结果中。

Matmul.png

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。

  1. 首先我们需要下面的正确导入!
import torch
import torch.nn as nn

import bitsandbytes as bnb
from bnb.nn import Linear8bitLt
  1. 然后您可以定义自己的模型。请注意,您可以将任何精度(FP16、BF16 或 FP32)的检查点或模型转换为 8 位,但目前,模型的输入必须是 FP16,我们的 Int8 模块才能工作。因此我们在此将模型视为 fp16 模型。
fp16_model = nn.Sequential(
    nn.Linear(64, 64),
    nn.Linear(64, 64)
)
  1. 假设您已经在您最喜欢的数据集和任务上训练了模型!现在该保存模型了:
[... train the model ...]
torch.save(fp16_model.state_dict(), "model.pt")
  1. 现在您的 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。

  1. 现在该以 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 值了(上面打印了两次)!

  1. 现在你可以安全地使用你的模型进行推理了,只需确保你的输入位于正确的 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 成功修复了一切!

总结一下

因此,终极配方是:

  1. 在meta设备上使用正确的模块初始化模型
  2. 在正确的GPU设备上逐一设置参数,并确保不要重复执行此过程!
  3. 在所有地方正确放置新的关键字参数,并添加一些优秀的文档
  4. 添加非常全面的测试!详情请查看我们的测试 这里 这听起来可能很简单,但我们一起经历了许多艰难的调试过程,常常涉及到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,轻松适配:

Open In Colab: T5-11b demo

或者这个BLOOM-3B的演示:

Open In Colab: BLOOM-3b demo

改进范围

我们认为,这种方法极大地提高了对超大型模型的可访问性。在不降低性能的情况下,它使计算资源较少的用户能够访问以前无法访问的模型。 我们已经发现了一些可以未来改进的领域,以使这种方法对大型模型更加有效!

小型模型更快的推理速度

正如我们在基准测试部分所看到的,我们可以将小型模型(参数≤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