大模型量化技术实践指南:GPTQ、AWQ、BitsandBytes 和 Unsloth

在大模型(LLM)的时代,我们需要了解量化技术,以便在本地电脑上运行这些模型,因为它们的规模非常庞大。然而,实现量化的方法有很多,这让像我这样的初学者很容易感到困惑。本文介绍了我们必须掌握的主要量化技术,并附带了如何在 Google Colab 环境中实现它们的源码。

量化概述  

量化是一种将浮点数映射为低位整数的技术。它在减少 LLM 的模型大小和推理成本方面非常有效。例如,当我们将一个大约 4 × 7B = 28GB(float32)的 7B 模型量化为 float16 时,大小可以减少到 2 × 7B = 14GB。如果进一步将 float32 量化为 int8,模型大小甚至可以降至 1 × 7B = 7GB

下图展示了 LLM 中常见的数据类型数据类型的位表示。

图片

那么,我们究竟如何减少位宽呢?答案是通过 “舍入”(rounding)将数值从一种数据类型转换为另一种。为了更直观地理解量化的概念,我们将学习两种基本的舍入方法:零点量化(Zero-point quantization)绝对最大值量化(Absolute maximum quantization)

零点量化(Zero-point quantization)

零点量化通过线性映射,将原始数据的最小值和最大值对应到目标数据类型的最小值和最大值。例如,当我们将 FP16 数值转换为 Int8 时,就会用到这种量化方法。下图展示了这一过程。

图片

我们希望将数据缩放到 Int8 范围(在本例中为 0 ~ 255)。因此,我们需要计算数据在原始尺度中的相对值,并通过乘以 Int8 量化范围(255)来进行重新缩放。其计算公式如下所示:

图片

其中,n 是用于量化的位数(在本例中为 8)。需要注意的是,如果我们希望将数值缩放到 -127 ~ 127,则需要从上述结果中减去 127,然后对结果进行舍入。

这种方法比其他量化方法能够更精确地表示原始数据。但另一方面,它需要更复杂的计算,因此在实际应用中,我们需要在精度计算复杂度之间进行权衡。

绝对最大值量化(Absolute Maximum Quantization)

绝对最大值量化(Absolute Maximum Quantization)将原始数据中的最大绝对值映射到目标数据类型的有符号范围。仍然以 FP16 到 Int8 的转换为例。为了简化问题,我们使用受限的量化范围,即 -127 ~ 127(供参考,完整的量化范围是 -128 ~ 127)。下图展示了这种量化方法。

图片

我们首先计算数据中的最大绝对值,然后利用该值对原始数据进行重新缩放。其计算公式如下所示:

图片

这种方法在计算上比零点量化更简单。然而,它容易受到数据中离群值(outliers)的影响,因此在使用时需要仔细检查数据是否适合这种方法。

这些方法都属于 “舍入至最近值”(Round-to-Nearest, RTN) 量化技术。

然而,朴素量化(naive quantization) 存在精度下降的问题,因为减少位数的同时,也会丢失部分信息。因此,现代量化技术的目标是在减少位宽的同时尽可能降低精度损失。目前,主要有两种主流的现代量化技术:

1. 量化感知训练(Quantization Aware Training, QAT)

QAT 允许在量化模型的同时进行微调(fine-tuning),以恢复因量化导致的精度下降。相比于后训练量化(PTQ,见下文),QAT 可以更好地保持模型的准确性,但它通常需要额外的训练,并且计算成本较高,例如需要 A100 或 H100 等高性能计算资源。

2. 后训练量化(Post-Training Quantization, PTQ)

PTQ 允许对预训练模型进行量化,而无需额外训练。因此,它可以减少量化过程中对 GPU 显存(VRAM)的需求,是更具实用性的解决方案。近年来,研究人员在这一方向上投入了大量研究,开发了许多高效的 PTQ 方法。

在本博客中,我们将学习以下流行的量化技术:

  • GPTQ(Group-wise Precision Tuning Quantization)

  • AWQ(Activation-aware Weight Quantization)

  • Bitsandbytes(QLoRA)

其中,GPTQ 和 AWQ 属于 PTQ 方法,而 QLoRA 属于 QAT 方法。这些算法已经集成到 transformers 库中,因此用户可以高效地使用它们。在接下来的部分,我们将简要介绍这些算法的理论基础。

 GPTQ  

GPTQ(Group-wise Precision Tuning Quantization,分组精确调整量化)2022 年提出的早期成功的量化算法之一。当时,该技术首次成功地将 BLOOM 或 OPT-175B 等 175B 规模的大模型量化为 4-bit

GPTQ 采用逐行量化(per-row quantization),即独立地量化权重矩阵的每一行,以找到最优的量化权重,从而最小化量化误差

为了实现高精度量化,GPTQ 利用 Hessian 矩阵计算剩余权重的最优更新,从而减少量化带来的信息损失。需要注意的是,GPTQ 保留嵌入层(embedding layer)和输出层(output layer)为 FP16 以维持模型精度

下面的伪代码描述了 GPTQ 算法的工作原理:

图片

根据论文,使用单个 A100 GPU(80GB VRAM)大约需要四个小时就可以量化 175B 参数模型。OPT-175B 的量化结果如下表所示。

图片

困惑度(perplexity)衡量模型预测句子中下一个单词的能力。因此,困惑度值降低,这意味着模型对其预测更有信心。在上表中,与 16 位精度相比,通过 GPTQ 量化的模型将困惑度值的增加保持在尽可能小的水平。

AWQ  

AWQ(Activation-aware Weight Quantization,激活感知权重量化)最近是最流行的量化算法之一。该算法侧重于对 LLM 性能非常重要的显著权重。作者的研究表明,LLM 的权重比例很小(0.1~1%),对量化误差有显著影响。他们利用激活幅度来找到这一小部分权重,并应用每通道缩放来减轻显著权重的量化误差。此过程的说明如下图 (c) 所示。

图片

下表展示了 Llama-2 和 LLaMA 模型的 AWQ 结果。从中可以看出,AWQ 可以获得比舍入到最近 (RTN) 量化和 GPTQ 更好的困惑度。

图片

请注意,根据论文,对于某些模型(例如 Mistral 模型和指令调整模型),AWQ 有时不如 GPTQ。当我们量化 LLM 时,我们应该比较这些算法的结果并选择最佳算法。

BitsandBytes  

BitsandBytes 是一个将模型量化为 8 位或 4 位的量化库。由于其方便性和有效性,它是最常见的量化库。

BitsandBytes 的 8 位量化基于 LLM.int8() 论文。为了减轻量化误差,它将模型权重的敏感异常值保留在 FP16 中,将其他异常值保留在 INT8 中,然后分别进行矩阵乘法。最后,将它们相加以返回 FP16 格式。下图显示了此过程。

图片

该技术可应用于高达 170B 规模的模型,但运行 170B 模型仍需要 8 x 24GB VRAM,运行 66B 模型则需要 4 x 24GB VRAM。在消费级 GPU 上运行大型模型仍然不够。

因此,开发了 4 位量化来解决这个问题。Bitsandbytes 的 4 位量化通常与 QLoRA 一起使用,以微调量化后的 LLM。直观地说,QLoRA 以 4 位量化目标模型,使其冻结,然后使用 LoRA 对冻结的 4 位模型进行微调。以下是完全微调、LoRA 和 QLoRA 之间的比较。

图片

从技术上讲,QLoRA 主要利用了以下特性。

  • 4 位 NormalFloat 量化

    该技术利用了预训练的神经网络权重具有正态分布的事实,并引入了称为 4 位 NormalFloat 类型的新数据类型,这确保了每个量化箱中的预期值数量相等。

  • 分页优化器

    QLoRA 应用分页优化器来避免 GPU 内存突然激增。分页优化器使用 NVIDIA 统一内存功能自动在 CPU 和 GPU 之间切换页面到页面的传输(在这种情况下,你可以想象 GPU 页面类似于内存中的 CPU 页面)。

  • 双重量化

    当你将量化应用于模型权重时,你需要与模型权重具有相同数据类型的量化常数。由于 LLM 中有许多参数,量化常数也会给内存空间带来负担。为了最大限度地减少每个参数的内存占用,作者对量化常数进行了量化。

下表显示了参数大小与 GPU VRAM 消耗的关系。

图片

请注意,内存列显示模型的内存需求,因此它不包含微调部分。虽然这取决于数据集或超参数设置,但我们可以使用消费级 24GB VRAM 机器微调多达 13B 参数模型。

当零样本 LLM 精度不足以完成我们的目标任务时,我们应该使用 QLoRA。对于现实世界的 AI 项目,我们经常使用这种技术,因为基础模型通常不适用于特定的现场数据。

Unsloth  

虽然 Unsloth 不是量化方法,但我想介绍这个用于 LLM 的开源超高效微调库。Unsloth 是一个完全集成参数高效微调方法(如 LoRA 和 QLoRA)的库。它针对开源著名 LLM 优化了每个内核,并减少了高达约 80% 的内存使用率!例如,它支持DeepSeek-R1、Llama、Mistral、Phi、Qwen 和 Gemma 这些开源 LLM。

图片

Unsloth 与 HuggingFace 和 vllm 兼容,我们可以在它们之间互换格式。当模型受支持时,我们应该使用 Unsloth 来节省内存并提高性能。

就在最近(2024 年 12 月 4 日),Unsloth 发布了一种新的量化方法,称为 Unsloth — 动态 4 位量化(Dynamic 4-bit quantization)。该技术建立在 BitsandBytes 4 位量化之上,并确定是否动态量化某些参数。

图片

到目前为止,我们已经看到了流行的量化技术和库。总之,我们可以在以下情况下使用这些量化技术。

  • 想在零样本设置中使用 LLM → GPTQ 或 AWQ

  • 想使用带有自己数据的微调 LLM → QLoRA(BitsandBytes 或 Unsloth)

量化实战 

https://gist.github.com/tanukon/41fd470ae66697e744c01e23934fb270

https://medium.com/generative-ai/practical-guide-of-llm-quantization-gptq-awq-bitsandbytes-and-unsloth-bdeaa2c0bbf6

### 微调 UnSloth 大规模模型的方法 对于大规模预训练模型UnSloth而言,微调是指基于特定任务的数据集进一步优化参数的过程,使得该模型能够更好地适应具体应用场景的需求[^1]。 #### 准备工作 为了准备用于微调的任务数据集,需确保其格式符合框架的要求。通常情况下,这涉及到将原始文本转换成适合输入的形式,并准备好对应的标签信息。此外,还需要安装必要的库文件以及配置好运行环境。 #### 加载预训练模型 通过官方API可以方便地加载已经预先训练好的UnSloth模型实例: ```python from unsloth import AutoModelForSequenceClassification, AutoTokenizer model_name_or_path = "unsloth/your_model_variant" tokenizer = AutoTokenizer.from_pretrained(model_name_or_path) model = AutoModelForSequenceClassification.from_pretrained(model_name_or_path) ``` #### 数据处理与迭代器构建 定义Dataset类来封装自定义的数据读取逻辑,并利用DataLoader创建批次化的样本流供后续训练过程使用: ```python from torch.utils.data import Dataset, DataLoader class CustomTextDataset(Dataset): def __init__(self, texts, labels, tokenizer, max_len=512): self.texts = texts self.labels = labels self.tokenizer = tokenizer self.max_len = max_len def __len__(self): return len(self.texts) def __getitem__(self, idx): text = str(self.texts[idx]) label = int(self.labels[idx]) encoding = self.tokenizer.encode_plus( text, add_special_tokens=True, max_length=self.max_len, padding='max_length', truncation=True, return_attention_mask=True, return_tensors='pt' ) item = { 'input_ids': encoding['input_ids'].flatten(), 'attention_mask': encoding['attention_mask'].flatten(), 'labels': torch.tensor(label, dtype=torch.long) } return item train_dataset = CustomTextDataset(train_texts, train_labels, tokenizer) valid_dataset = CustomTextDataset(valid_texts, valid_labels, tokenizer) train_loader = DataLoader(train_dataset, batch_size=8, shuffle=True) valid_loader = DataLoader(valid_dataset, batch_size=8, shuffle=False) ``` #### 设置超参数并启动训练循环 指定学习率、epoch数量以及其他可能影响收敛性的因素;接着编写简单的for-loop结构实现前向传播、反向传播及权重更新操作: ```python import torch.optim as optim from transformers import get_linear_schedule_with_warmup device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model.to(device) optimizer = optim.AdamW(model.parameters(), lr=2e-5) total_steps = len(train_loader) * num_epochs scheduler = get_linear_schedule_with_warmup(optimizer, num_warmup_steps=0, num_training_steps=total_steps) num_epochs = 3 for epoch in range(num_epochs): model.train() total_loss = 0 for step, batch in enumerate(train_loader): input_ids = batch["input_ids"].to(device) attention_masks = batch["attention_mask"].to(device) labels = batch["labels"].to(device) outputs = model(input_ids=input_ids, attention_mask=attention_masks, labels=labels) loss = outputs.loss optimizer.zero_grad() loss.backward() optimizer.step() scheduler.step() total_loss += loss.item() avg_train_loss = total_loss / len(train_loader) print(f"Epoch {epoch + 1}, Average training loss: {avg_train_loss:.4f}") ``` 完成上述步骤之后,即实现了针对特定NLP任务场景下的UnSloth大模型微调流程。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值