跳转到主要内容
博客

使用 PyTorch 和 Hugging Face 生态系统工具在您的消费级硬件上微调 LLM

我们演示了如何使用 LoRA 和 PyTorch 以及 Hugging Face 生态系统中的工具,在典型的消费级 GPU(NVIDIA T4 16GB)上微调 7B 参数模型,并提供完整的可重现 Google Colab 笔记本。

引言

大型语言模型(LLM)在工业应用中展现出令人印象深刻的能力。通常,开发人员会寻求针对特定用例和应用程序定制这些 LLM,以对其进行微调以获得更好的性能。然而,LLM 的设计通常很大,需要大量的 GPU 才能进行微调。

我们以一个具体示例为例,尝试在免费的 Google Colab 实例(1x NVIDIA T4 16GB)上微调 Llama 模型。Llama-2 7B 拥有 70 亿个参数,如果模型以全精度加载,总大小为 28GB。考虑到我们 GPU 内存的限制(16GB),模型甚至无法加载,更不用说在我们的 GPU 上进行训练了。这种内存需求可以通过减半而性能下降微乎其微。您可以此处了解更多关于半精度和混合精度模型训练的信息。

什么让我们的 Llama 微调如此昂贵?

在全微调与 Adam 优化器结合半精度模型和混合精度模式的情况下,每个参数需要分配:

  • 权重 2 字节
  • 梯度 2 字节
  • Adam 优化器状态 4 + 8 字节

→ 每个可训练参数总计 16 字节,总计 112GB(不包括中间隐藏状态)。鉴于目前最大的 GPU VRAM 可达 80GB,这使得微调变得具有挑战性,并且对所有人来说都不易获得。为了弥合这一差距,参数高效微调(PEFT)方法在今天被社区广泛采用。

参数高效微调(PEFT)方法

PEFT 方法旨在大幅减少模型的可训练参数数量,同时保持与全微调相同的性能。

它们可以通过其概念框架进行区分:该方法是微调现有参数的子集,引入新参数,引入可训练提示等等?我们建议读者查阅下面分享的论文,该论文广泛比较了现有的 PEFT 方法。

Venn diagram

图片摘自论文:缩小规模以扩大:参数高效微调指南

对于这篇博客文章,我们将重点介绍大型语言模型的低秩适应(LoRA),因为它是社区中最常用的 PEFT 方法之一。

使用 🤗 PEFT 进行大型语言模型的低秩适应(LoRA)

LoRA 方法由微软团队的 Hu 等人于 2021 年提出,其工作原理是在模型(我们称之为基础模型)中附加额外的可训练参数。

为了提高微调效率,LoRA 将一个大的权重矩阵分解为两个较小的低秩矩阵(称为更新矩阵)。这些新矩阵可以进行训练以适应新数据,同时保持总体变化量较低。原始权重矩阵保持冻结状态,不再进行任何调整。为了产生最终结果,将原始权重和适应后的权重结合起来。

这种方法有几个优点:

  • LoRA 通过大幅减少可训练参数的数量,使微调更加高效。
  • 原始预训练权重保持冻结,这意味着您可以拥有多个轻量级且便携的 LoRA 模型,用于在其之上构建各种下游任务。
  • LoRA 与许多其他参数高效方法正交,并且可以与其中许多方法结合使用。
  • 使用 LoRA 微调的模型性能与完全微调模型的性能相当。
  • 当适配器权重与基础模型合并时,LoRA 不会增加任何推理延迟。

原则上,LoRA 可以应用于神经网络中任何权重矩阵的子集,以减少可训练参数的数量。然而,为了简化和进一步提高参数效率,在 Transformer 模型中,LoRA 通常仅应用于注意力块。LoRA 模型中可训练参数的最终数量取决于低秩更新矩阵的大小,这主要由秩 r 和原始权重矩阵的形状决定。

Animated diagram that show how LoRA works in practice

展示 LoRA 实际工作原理的动画图——原始内容改编自 LoRA 原始论文图 1

以下是使用 Hugging Face PEFT 库训练 LoRA 模型的代码片段:

code snippet showing how to train LoRA model using  Hugging Face PEFT library

基础模型可以是任意 `dtype`:利用 SOTA LLM 量化并将基础模型加载为 4 位精度

根据 LoRA 的公式,基础模型可以压缩为任何数据类型('dtype'),只要基础模型中的隐藏状态与 LoRA 矩阵输出的隐藏状态在同一数据类型中即可。

压缩和量化大型语言模型最近成为一个热门话题,因为 SOTA 模型变得越来越大,难以服务和供最终用户使用。社区中的许多人提出了各种方法来有效压缩 LLM,同时将性能下降降至最低。

这就是 bitsandbytes 库的用武之地。它的目的是让量化和深度学习硬件加速器使用领域的领先学术专家 Tim Dettmers 的尖端研究成果能够被大众所接触。

QLoRA:`bitsandbytes` 对人工智能民主化的核心贡献之一

LLM 的量化主要集中在推理量化,但 QLoRA(量化模型权重 + 低秩适配器)论文展示了在大模型规模下,通过冻结、量化权重进行反向传播的突破性实用性。

通过 QLoRA,我们在所有规模和模型上都能达到 16 位微调的性能,同时将微调内存占用减少 90% 以上——从而允许在消费级硬件上微调 SOTA 模型。

在这种方法中,LoRA 在微调和校正微小残余量化误差方面都至关重要。由于量化模型尺寸显著减小,因此可以在每个网络层慷慨地放置低秩适配器,这些适配器合计仍仅占原始模型权重内存占用的 0.2%。通过这种 LoRA 的使用,我们实现了与 16 位全模型微调相当的性能。

System diagram

除了大量使用 LoRA,为了实现 4 位模型的高保真微调,QLoRA 还使用了 3 种额外的算法技巧:

  1. 4 位 NormalFloat (NF4) 量化,一种自定义数据类型,利用模型权重正态分布的特性,并将相同数量的权重(每个块)分配到每个量化桶中——从而提高信息密度。
  2. 双重量化,即对量化常数进行量化(进一步节省)。
  3. 分页优化器,防止梯度检查点期间的内存峰值导致内存不足错误。

一个有趣的方面是 GPU 缓存中 4 位权重的去量化,矩阵乘法作为 16 位浮点运算执行。换句话说,我们使用一个低精度存储数据类型(在本例中为 4 位,但原则上可互换)和一个正常精度计算数据类型。这很重要,因为后者出于硬件兼容性和数值稳定性原因默认为 32 位,但对于支持它的新硬件,应将其设置为最佳 BFloat16 以实现最佳性能。

总而言之,通过结合这些量化过程的改进和大量使用 LoRA,我们将模型压缩了 90% 以上,并保持了完整的模型性能,没有通常的量化退化,同时还在每个层保留了 16 位 LoRA 适配器的完整微调能力。

QLoRA 在实践中的应用

这些 SOTA 量化方法打包在 `bitsandbytes` 库中,并方便地与 HuggingFace 🤗 Transformers 集成。例如,要分别使用 LLM.int8 和 QLoRA 算法,只需将 `load_in_8bit` 和 `load_in_4bit` 传递给 `from_pretrained` 方法即可。

import torch
from transformers import AutoModelForCausalLM, AutoTokenizer

model_id = "facebook/opt-125m"
# For LLM.int8()
# model = AutoModelForCausalLM.from_pretrained(model_id, load_in_8bit=True)

# For QLoRA
model = AutoModelForCausalLM.from_pretrained(model_id, load_in_4bit=True)

您可以在文档的这个特定部分阅读更多关于量化功能的信息:https://hugging-face.cn/docs/transformers/main_classes/quantization

当使用 QLoRA 和 Adam 优化器,并结合 4 位基础模型和混合精度模式时,我们需要为每个参数分配:

  • 权重约 0.5 字节
  • 梯度 2 字节
  • Adam 优化器状态 4 + 8 字节

由于 QLoRA 最终只产生 0.29% 的可训练参数,因此每个可训练参数总计 14 字节乘以 0.0029,使得 QLoRA 训练设置成本约为 4.5GB。但在实践中,为了包含始终以半精度存在的中间隐藏状态,它需要约 7-10GB(序列长度为 512 时为 7GB,序列长度为 1024 时为 10GB),具体请参阅下一节中共享的 Google Colab 演示。

下面是使用 Hugging Face PEFT 训练 QLoRA 模型的代码片段:

code snippet showing how to train QLoRA model using Hugging Face PEFT

使用 TRL 进行 LLM 训练

ChatGPT、GPT-4 和 Claude 等模型是强大的语言模型,它们通过一种名为“人类反馈强化学习 (RLHF)”的方法进行了微调,以便更好地与我们期望的行为和使用方式保持一致。微调过程分 3 个步骤:

  • 监督式微调(SFT)
  • 奖励/偏好建模(RM)
  • 人类反馈强化学习(RLHF)
Process diagram

摘自 InstructGPT 论文:Ouyang, Long, et al. “训练语言模型以遵循人类反馈指令。” arXiv 预印本 arXiv:2203.02155 (2022)。

在这里,我们只关注监督微调步骤。我们按照类似于预训练的过程在新数据集上训练模型。目标是预测下一个 token(因果语言建模)。可以应用多种技术来提高训练效率:

  • 打包:与其在批次中每个样本只包含一个文本,然后填充到最长的文本或模型的最大上下文,不如将大量文本与句子结束 (EOS) 标记连接起来,并截取上下文大小的块来填充批次而无需任何填充。这种方法显著提高了训练效率,因为模型处理的每个 token 都对训练有贡献。
Sample diagram
  • 仅对完成部分进行训练:我们希望模型能够理解提示并生成答案。与其训练模型处理整个输入(提示 + 答案),如果只训练模型处理完成部分,训练将更高效。

您可以使用 SFTTrainer 通过这些技术进行监督微调。

from trl import SFTTrainer

trainer = SFTTrainer(
    model=model,
    args=training_arguments,
    train_dataset=train_dataset,
    dataset_text_field="text",
    max_seq_length=1024,
    packing=True,
)

由于 SFTTrainer 后端由 🤗accelerate 提供支持,因此您只需一行代码即可轻松调整训练以适应您的硬件设置!

例如,如果您有 2 个 GPU,可以使用以下命令执行分布式数据并行训练:

accelerate launch --num_processes=2 training_llama_script.py

整合所有部分

我们制作了一个完整的可重现 Google Colab 笔记本,您可以通过 此链接查看。我们使用了上面各节中共享的所有组件,并使用 QLoRA 在 UltraChat 数据集上微调了 llama-7b 模型。如下图所示,当使用 1024 的序列长度和 4 的批次大小时,内存使用量保持非常低(约 10GB)。

Memory usage diagram