权重量化方面的最新进展使我们能够在消费级硬件上运行大量大型语言模型,例如 RTX 3090 GPU 上的 LLaMA-30B 模型。这要归功于性能下降最小的新型 4 位量化技术,例如GPTQ、GGML和NF4。
在本文中,我们将探索流行的 GPTQ 算法,以了解其工作原理并使用AutoGPTQ库实现它。
您可以在Google Colab和GitHub上找到代码。
🧠 最佳大脑量化
首先介绍一下我们要解决的问题。对于每一层ℓ在网络中,我们想要找到一个量化版本原始权重西
。这称为分层压缩问题。更具体地说,为了最大限度地降低性能下降,我们希望输出
。这些新权重尽可能接近原始权重
。换句话说,我们想要找到:
已经提出了不同的方法来解决此问题,但这里我们感兴趣的是最佳脑量化器(OBQ) 框架。
该方法的灵感来自一种修剪技术,可以从经过充分训练的密集神经网络(最佳脑外科医生)中小心地移除权重。它使用近似技术,并为最佳单个权重提供明确的公式瓦问_瓦__问_删除并进行最佳更新δF_δ__F_调整剩余的非量化权重集F_F_弥补被删除的部分:
公式中,quant(w) 是量化给出的权重舍入,并且是海塞。
使用 OBQ,我们可以先量化最简单的权重,然后调整所有剩余的未量化权重来补偿这个精度损失。然后我们选择下一个要量化的权重,依此类推。
这种方法的一个潜在问题是,当存在异常权重时,这会导致较高的量化误差。通常,这些异常值最后被量化,此时剩下的非量化权重很少,可以调整以补偿较大的误差。当一些权重通过中间更新被进一步推到网格之外时,这种影响可能会恶化。应用了一种简单的启发式方法来防止这种情况:异常值一出现就被量化。
这个过程的计算量可能很大,尤其是对于 LLM 来说。为了解决这个问题,OBQ 方法使用了一种技巧,可以避免每次简化权重时都重新进行整个计算。在量化权重后,它会通过删除与该权重相关的行和列(使用高斯消元法)来调整计算中使用的矩阵(Hessian):
该方法还采用矢量化来一次性处理权重矩阵的多行。尽管 OBQ 效率很高,但随着权重矩阵的大小增加,其计算时间会显著增加。这种立方增长使得 OBQ 很难用于具有数十亿个参数的超大型模型。
🧮 GPTQ 算法
[GPTQ 算法](https://arxiv.org/abs/2210.17323)由 Frantar 等人 (2023) 提出,它从 OBQ 方法中汲取灵感,但进行了重大改进,可以将其扩展到(非常)大型的语言模型。步骤 1:任意顺序洞察
OBQ 方法选择权重(模型中的参数)按特定顺序进行量化,该顺序由**增加的额外误差最小**决定。然而,GPTQ 观察到,对于大型模型,以任何固定顺序量化权重都可以获得同样好的效果。这是因为即使某些权重可能单独引入更多误差,但它们会在流程的后期量化,此时剩下的其他权重很少,这可能会增加误差。所以顺序并不像我们想象的那么重要。基于这一见解,GPTQ 旨在以相同的顺序量化矩阵所有行的所有权重。这使得该过程更快,因为某些计算只需对每列进行一次,而不是对每个权重进行一次。
步骤 2:延迟批量更新
这种方案速度不快,因为它需要更新一个**巨大的矩阵**,而每个条目的计算量却很少。这种类型的操作无法充分利用 GPU 的计算能力,并且会因内存限制(内存吞吐量瓶颈)而变慢。为了解决这个问题,GPTQ 引入了“惰性批量”更新。事实证明,给定列的最终舍入决策仅受对该列执行的更新的影响,而不会受后续列的影响。因此,GPTQ 可以一次将算法应用于一批列(例如 128 列),仅更新这些列和矩阵的相应块。在完全处理完一个块后,该算法会对整个矩阵执行全局更新。
步骤 3:Cholesky 重构
然而,还有一个问题需要解决。当算法扩展到非常大的模型时,数值不准确可能会成为一个问题。具体来说,重复应用某一操作可能会**累积数值误差**。为了解决这个问题,GPTQ 使用了Cholesky 分解,这是一种解决某些数学问题的数值稳定方法。它涉及使用 Cholesky 方法从矩阵中预先计算一些所需信息。这种方法与轻微的“阻尼”(在矩阵的对角元素中添加一个小常数)相结合,有助于算法避免数值问题。
完整的算法可以概括为几个步骤:
- GPTQ 算法首先对 Hessian 逆进行 Cholesky 分解(该矩阵有助于决定如何调整权重)
- 然后它循环运行,一次处理一批列。
- 对于批次中的每一列,它量化权重,计算误差,并相应地更新块中的权重。
- 处理批次后,它会根据块的错误更新所有剩余的权重。
GPTQ 算法在各种语言生成任务上进行了测试。它与其他量化方法进行了比较,例如将所有权重四舍五入为最接近的量化值 (RTN)。GPTQ 与 BLOOM(176B 参数)和 OPT(175B 参数)模型系列一起使用,并使用单个NVIDIA A100 GPU对模型进行量化。
💻 使用 AutoGPTQ 量化 LLM
GPTQ 非常流行,用于创建可在 GPU 上高效运行的 4 位精度模型。您可以在 Hugging Face Hub 上找到许多示例,尤其是来自[TheBloke](https://huggingface.co/TheBloke)的示例。如果您正在寻找一种对 CPU 更友好的方法,[GGML](https://github.com/ggerganov/ggml)目前是您的最佳选择。最后, transformers带有的库 bitsandbytes允许您在使用参数加载模型时量化模型 load_in_4bit=true,这需要下载完整模型并将其存储在 RAM 中。让我们使用 AutoGPTQ 库实现 GPTQ 算法并量化 GPT-2 模型。这需要 GPU,但 Google Colab 上的免费 T4 就可以了。我们首先加载库并定义我们要量化的模型(在本例中为 GPT-2)。
!BUILD_CUDA_EXT=0 pip install -q auto-gptq transformers
import random
from auto_gptq import AutoGPTQForCausalLM, BaseQuantizeConfig
from datasets import load_dataset
import torch
from transformers import AutoTokenizer
# Define base model and output directory
model_id = "gpt2"
out_dir = model_id + "-GPTQ"
现在我们要加载模型和标记器。标记器使用库AutoTokenizer中的经典类加载transformers。另一方面,我们需要传递特定配置(BaseQuantizeConfig)来加载模型。
在此配置中,我们可以指定要量化的位数(此处为bits=4)和组大小(惰性批处理的大小)。请注意,此组大小是可选的:我们也可以对整个权重矩阵使用一组参数。实际上,这些组通常以非常低的成本提高量化的质量(尤其是使用group_size=1024)。damp_percent此处的值是为了帮助 Cholesky 重新表述,不应更改。
最后,desc_act(也称为行为顺序)是一个棘手的参数。它允许您根据递减的激活来处理行,这意味着最重要或最有影响力的行(由采样的输入和输出确定)首先被处理。此方法旨在将大部分量化误差(不可避免地在量化过程中引入)放在不太重要的权重上。通过确保以更高的精度处理最重要的权重,此方法提高了量化过程的整体准确性。但是,当与组大小一起使用时,desc_act由于需要频繁重新加载量化参数,可能会导致性能下降。因此,我们不会在这里使用它(但它将来可能会得到修复)。
# Load quantize config, model and tokenizer
quantize_config = BaseQuantizeConfig(
bits=4,
group_size=128,
damp_percent=0.01,
desc_act=False,
)
model = AutoGPTQForCausalLM.from_pretrained(model_id, quantize_config)
tokenizer = AutoTokenizer.from_pretrained(model_id)
量化过程在很大程度上依赖于样本来评估和提高量化质量。它们提供了一种比较原始模型和新量化模型产生的输出的方法。提供的样本数量越多,进行更准确、更有效的比较的可能性就越大,从而提高量化质量。
在本文中,我们利用C4(Colossal Clean Crawled Corpus)数据集来生成样本。C4 数据集是从 Common Crawl 项目收集的大规模、多语言网络文本集合。这个庞大的数据集已经过清理,并专门为训练大规模语言模型做准备,使其成为此类任务的绝佳资源。WikiText 数据集是另一个受欢迎的选择。
在下面的代码块中,我们从 C4 数据集中加载 1024 个样本,对其进行标记并格式化。
# Load data and tokenize examples
n_samples = 1024
data = load_dataset("allenai/c4", data_files="en/c4-train.00001-of-01024.json.gz", split=f"train[:{n_samples*5}]")
tokenized_data = tokenizer("\n\n".join(data['text']), return_tensors='pt')
# Format tokenized examples
examples_ids = []
for _ in range(n_samples):
i = random.randint(0, tokenized_data.input_ids.shape[1] - tokenizer.model_max_length - 1)
j = i + tokenizer.model_max_length
input_ids = tokenized_data.input_ids[:, i:j]
attention_mask = torch.ones_like(input_ids)
examples_ids.append({'input_ids': input_ids, 'attention_mask': attention_mask})
WARNING:datasets.builder:Found cached dataset json (/root/.cache/huggingface/datasets/allenai___json/allenai--c4-6e494e9c0ee1404e/0.0.0/8bb11242116d547c741b2e8a1f18598ffdd40a1d4f2a2872c7a28b697434bc96)
Token indices sequence length is longer than the specified maximum sequence length for this model (2441065 > 1024). Running this sequence through the model will result in indexing errors
现在数据集已准备就绪,我们可以开始使用批处理大小为 1 的量化过程。我们还可以选择使用CUDA 替代方案OpenAI Triton与 GPU 进行通信。完成后,我们将标记器和模型保存为 safetensors 格式。
%%time
# Quantize with GPTQ
model.quantize(
examples_ids,
batch_size=1,
use_triton=True,
)
# Save model and tokenizer
model.save_quantized(out_dir, use_safetensors=True)
tokenizer.save_pretrained(out_dir)
CPU times: user 4min 35s, sys: 3.49 s, total: 4min 39s
Wall time: 5min 8s
('gpt2-GPTQ/tokenizer_config.json',
'gpt2-GPTQ/special_tokens_map.json',
'gpt2-GPTQ/vocab.json',
'gpt2-GPTQ/merges.txt',
'gpt2-GPTQ/added_tokens.json',
'gpt2-GPTQ/tokenizer.json')
AutoGPTQForCausalLM与往常一样,可以使用和类从输出目录加载模型和标记器AutoTokenizer。
device = "cuda:0" if torch.cuda.is_available() else "cpu"
# Reload model and tokenizer
model = AutoGPTQForCausalLM.from_quantized(
out_dir,
device=device,
use_triton=True,
use_safetensors=True,
)
tokenizer = AutoTokenizer.from_pretrained(out_dir)
WARNING:accelerate.utils.modeling:The safetensors archive passed at gpt2-GPTQ/gptq_model-4bit-128g.safetensors does not contain metadata. Make sure to save your model with the `save_pretrained` method. Defaulting to 'pt' metadata.
WARNING:auto_gptq.modeling._base:GPT2GPTQForCausalLM hasn't fused attention module yet, will skip inject fused attention.
WARNING:auto_gptq.modeling._base:GPT2GPTQForCausalLM hasn't fused mlp module yet, will skip inject fused mlp.
让我们检查一下模型是否正常工作。AutoGPTQ 模型(大部分)可作为普通transformers模型运行,这使其与推理管道兼容,如下例所示:
from transformers import pipeline
generator = pipeline('text-generation', model=model, tokenizer=tokenizer)
result = generator("I have a dream", do_sample=True, max_length=50)[0]['generated_text']
print(result)
The model 'GPT2GPTQForCausalLM' is not supported for text-generation. Supported models are ['BartForCausalLM', 'BertLMHeadModel', 'BertGenerationDecoder', 'BigBirdForCausalLM', 'BigBirdPegasusForCausalLM', 'BioGptForCausalLM', 'BlenderbotForCausalLM', 'BlenderbotSmallForCausalLM', 'BloomForCausalLM', 'CamembertForCausalLM', 'CodeGenForCausalLM', 'CpmAntForCausalLM', 'CTRLLMHeadModel', 'Data2VecTextForCausalLM', 'ElectraForCausalLM', 'ErnieForCausalLM', 'FalconForCausalLM', 'GitForCausalLM', 'GPT2LMHeadModel', 'GPT2LMHeadModel', 'GPTBigCodeForCausalLM', 'GPTNeoForCausalLM', 'GPTNeoXForCausalLM', 'GPTNeoXJapaneseForCausalLM', 'GPTJForCausalLM', 'LlamaForCausalLM', 'MarianForCausalLM', 'MBartForCausalLM', 'MegaForCausalLM', 'MegatronBertForCausalLM', 'MusicgenForCausalLM', 'MvpForCausalLM', 'OpenLlamaForCausalLM', 'OpenAIGPTLMHeadModel', 'OPTForCausalLM', 'PegasusForCausalLM', 'PLBartForCausalLM', 'ProphetNetForCausalLM', 'QDQBertLMHeadModel', 'ReformerModelWithLMHead', 'RemBertForCausalLM', 'RobertaForCausalLM', 'RobertaPreLayerNormForCausalLM', 'RoCBertForCausalLM', 'RoFormerForCausalLM', 'RwkvForCausalLM', 'Speech2Text2ForCausalLM', 'TransfoXLLMHeadModel', 'TrOCRForCausalLM', 'XGLMForCausalLM', 'XLMWithLMHeadModel', 'XLMProphetNetForCausalLM', 'XLMRobertaForCausalLM', 'XLMRobertaXLForCausalLM', 'XLNetLMHeadModel', 'XmodForCausalLM'].
Setting `pad_token_id` to `eos_token_id`:50256 for open-end generation.
I have a dream," she told CNN last week. "I have this dream of helping my mother find her own. But, to tell that for the first time, now that I'm seeing my mother now, just knowing how wonderful it is that
我们成功地从量化的 GPT-2 模型中获得了令人信服的完成度。更深入的评估需要测量量化模型与原始模型的困惑度。不过,我们不会将其纳入本文的讨论范围。