你好!

在上一篇文章中,我们详细介绍了int8量化的实现方法。
今天,我们将继续深入,使用GPTQ技术进行4位量化。后续还会陆续讲解GGML、EXL2等量化大模型的主流技术。
得益于量化技术的持续进步,大模型的部署成本正在显著降低,让更多开发者能够负担得起。
这里整理了几款热门量化工具的开源代码,所有资源完全免费,并在Colab/Aliyun上全部验证通过。内容安排如下:
day3:手把手带你量化大模型!有哪些方法?什么是量化?【点击学习】
day4:手把手GPTQ量化大模型!有何区别?优势在哪?【本篇】
day5:手把手GGML量化大模型!与llama.cpp有何异同?
day6:手把手使用ExLlamaV2量化!精简版GPTQ?
如果你之前抢到了阿里云的免费算力,还可以使用医疗组伙伴共享的方法,将源改为国内镜像,方便下载并量化模型,效果非常棒。
import os
# 设置环境变量HF_ENDPOINT
os.environ['HF_ENDPOINT'] = 'https://hf-mirror.com'
人的专注力通常只有10分钟,那么我们就直接进入正题。
第一部分:量化技术的背景与挑战
谈到量化,就不得不提长期以来被广泛使用的“剪枝”技术。稍后可以自行查阅相关文献:
https://arxiv.org/abs/2208.11580
最佳脑压缩:一种用于精确训练后量化与剪枝的框架
伊莱亚斯·弗兰塔、西达克·帕尔·辛格、丹·阿里斯塔
本文探讨了深度神经网络(DNN)在具有挑战性的单次/训练后环境中的模型压缩问题——即给定一个精确训练好的模型,仅基于少量校准数据对其进行压缩,无需重新训练。由于新兴的软件和硬件支持通过剪枝和/或加速量化来执行模型压缩,这一问题已变得非常流行。此前,针对这两种压缩方法,已经分别提出了性能良好的解决方案。本文提出了一种全新的压缩框架,在统一设置下同时涵盖权重剪枝与量化,既节省时间又节省空间,并显著提升了现有训练后方法的实际性能。在技术层面,我们的方法基于经典最佳脑外科医生(OBS)框架 [LeCun, Denker, and Solla, 1990] 的精确高效实现,并扩展至涵盖现代DNN规模的权重量化。从实践角度看,实验结果表明,该方法能显著改善现有训练后方法的压缩精度权衡,并能在训练后环境中实现剪枝与量化的精准复合应用。该方法灵感来源于剪枝技术——从训练好的密集神经网络(最佳脑外科医生)中移除权重。
利用近似技术,可以给出明确的公式,用于确定要删除的最佳单个权重w_q以及相应的最佳更新策略,从而调整剩余的非量化权重F集,以弥补删除带来的损失:
首先定义一个与该优化过程相关的变量:
使用OBQ方法,我们可以先简单地对权重进行量化,尽管会产生一定损失。
然后,调整所有剩余的非量化权重,以补偿精度损失。
接着,选择下一个权重进行量化,依次类推。
你是否发现?这种方法存在一个潜在问题:当存在异常值权重时,会产生超高量化误差。
通常,这些异常值会被放在最后量化,此时剩余的非量化权重已经很少,无法有效补偿巨大的误差。
此外,当某些权重被中间更新推离网格范围时,影响可能会进一步恶化。为此,我们应用了一个简单的启发式方法:一旦出现异常值,就立即对其进行量化。
但该过程的计算量可能非常庞大。
为了解决这个问题,OBQ方法使用了一个技巧,避免每次简化权重时都重新执行整个计算。量化某个权重后,通过删除与该权重关联的行和列(使用高斯消元法),来调整计算中使用的矩阵(Hessian矩阵):
该方法采用矢量化技术,一次处理权重矩阵的多行,效率较高。但随着权重矩阵规模的增大,OBQ的计算时间会急剧上升。
因此,OBQ并不适合当前千亿参数级别的超大型模型。
第二部分:什么是GPTQ?底层算法解析
GPTQ算法由Frantar等人(2023)提出,灵感来源于OBQ方法,经过重写后扩展至超大规模参数的语言模型。论文地址如下,后续可自行查阅:
https://arxiv.org/abs/2210.17323
其核心工作流程如下:
第一步:任意顺序的洞察
OBQ方法会按照特定顺序选择权重(模型中的参数)进行量化,顺序取决于哪些权重会增加最少的额外误差。
然而,GPTQ发现,对于大型模型,以任何固定顺序量化权重都能取得同样出色的效果。
原因在于:尽管某些单独权重可能引入更多误差,但在量化过程后期,当剩余可调整的权重已经不多时,这些权重才会被量化。因此,量化的顺序重要性并不像我们最初想象的那样大。
基于这一洞察,GPTQ对矩阵的所有行使用相同的顺序量化所有权重。这样做可以显著加快处理速度,因为某些计算只需为每一列执行一次,而无需为每个权重单独计算。
第二步:惰性批量更新
但该方案仍不够快,因为它要求用极少的计算量更新一个巨大矩阵中的每个条目。这类操作难以充分利用GPU的全部计算能力,并会受到内存限制(内存带宽瓶颈)的影响而减速。
为解决这一问题,GPTQ引入了“惰性批量”更新的概念。给定列的最终舍入决策仅受该列上执行的更新影响,而不受后续列的影响。
因此,GPTQ可以一次对一批列(例如128列)应用算法,仅更新这些列以及矩阵中的对应块。处理完一个块后,再对整个矩阵执行全局更新。
这种方法提高了效率,减少了内存访问频率,通过批量处理列来更有效地利用GPU的并行计算能力,并减少全局更新次数以缓解内存带宽瓶颈。
第三步:Cholesky 重构
当算法扩展到超大规模模型时,数值不精确性成为新的挑战。具体来说,重复应用某个特定操作可能会累积数值误差。
为解决这个问题,GPTQ使用Cholesky分解——一种数值稳定的方法,用于解决某些数学问题。通过Cholesky方法预先计算矩阵中所需的部分信息,并结合对矩阵对角元素进行轻微的“阻尼”(添加一个小常数),可以有效避免数值问题。
完整的算法流程如下:
- Cholesky分解启动:GPTQ算法首先对Hessian逆矩阵(一个帮助决定如何调整权重的矩阵)进行Cholesky分解。
- 循环处理:随后以循环方式运行,每次处理一批列。
- 按批次处理列:对于批次中的每一列,量化权重、计算误差,并相应地更新块中的权重。
- 基于块误差更新:处理完批次后,根据该块的误差更新所有剩余的权重。
GPTQ算法测试:GPTQ算法在多种语言生成任务上进行了测试,并与将所有权重四舍五入到最近量化值(RTN)等其他量化方法进行了比较。GPTQ与BLOOM(1760亿参数)和OPT(1750亿参数)模型系列配合使用,且模型量化过程仅需单个NVIDIA A100 GPU。实验表明,GPTQ在保持模型性能的同时,显著降低了内存开销。
第三部分:动手实践!边运行边聊细节!
打开上方代码,无论是在本地、阿里云还是Colab上均可运行。
推荐使用Colab,因为它免费且无需配置环境。GPTQ技术非常热门,你可以在Hugging Face上找到大量示例。如果你使用纯CPU,GGML是最佳选择,下一篇会详细介绍。
上传代码后,开始运行。
定义模型
只要资源足够,你可以替换任意模型,只需将下方model_id后的模型名称改为Hugging Face上的模型路径即可。
import random
from auto_gptq import AutoGPTQForCausalLM, BaseQuantizeConfig
from datasets import load_dataset
import torch
from transformers import AutoTokenizer
model_id = "gpt2"
out_dir = model_id + "-GPTQ"
将上述路径替换为目标模型。例如,要量化llama3:
model_id = "meta-llama/Meta-Llama-3-8B"
系统会自动从Hugging Face下载模型。
加载模型和分词器
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)
分词器通过库中的经典类加载。而加载模型时,需要传递一个特定的配置。
此配置中,我们指定了量化位数(bits)和组大小(惰性批处理的大小)。注意,组大小是可选的:你也可以对整个权重矩阵使用一组参数。实践中,这些组通常能以极低的成本显著提升量化质量。该值有助于Cholesky重构,请不要随意更改。
加载数据
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("nn".join(data['text']), return_tensors='pt')
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})
这段代码执行以下任务:
- 数据加载:首先设置
n_samples = 1024,指定要处理的数据样本数量。然后使用load_dataset函数从AllenAI的C4数据集中加载数据。具体加载的是英文数据部分("en/c4-train.00001-of-01024.json.gz"),并限制加载范围为前n_samples * 5个样本作为训练数据。 - 文本预处理:加载的数据通过
tokenizer进行预处理,将文本转换为模型可理解的token形式。这里,所有文本样本通过nn连接成一个长字符串,传递给tokenizer,并要求返回PyTorch张量('pt'格式)。 - 生成示例:随后进入循环,从tokenized数据中创建模型输入示例。对于每个样本(共
n_samples个):- 随机选择一个起始点
i,确保选取的序列长度(tokenizer.model_max_length)不会超出tokenized数据的最大边界。 - 计算结束点
j,等于i加上模型允许的最大长度。 - 从
tokenized_data.input_ids中提取从i到j的序列,作为模型的input_ids。 - 创建一个与
input_ids形状相同的全1张量attention_mask,用于指示模型哪些token应被关注。 - 将
input_ids和attention_mask作为字典添加到examples_ids列表中,形成最终训练示例。
- 随机选择一个起始点
开始量化
%%time
model.quantize(
examples_ids,
batch_size=1,
use_triton=True,
)
model.sa ve_quantized(out_dir, use_safetensors=True)
tokenizer.sa ve_pretrained(out_dir)
量化完成后,即可将模型下载到本地。
本文全面介绍了GPTQ算法——一种先进的量化技术,用于在消费级硬件上运行大语言模型(LLM)。
我们展示了它如何解决逐层压缩问题,该技术基于改进的OBS方法,融合了任意顺序洞察、延迟批量更新和Cholesky重构。这种新颖方法大幅降低了内存和计算需求,使LLM的部署更加普及。
此外,我们在免费的T4 GPU上成功量化了LLM模型,并让其生成文本。你还可以将自己的GPTQ 4位量化模型版本推送至Hugging Face Hub。正如开头所提,GPTQ并非唯一的4位量化算法:GGML和NF4也都是不错的选择。
下一篇我们将详细探讨这些方法。
