QLoRA:在你的GPU上微调大型语言模型

1️⃣ Standard(标准微调)

  • 模型参数:16-bit(通常是 FP16)

  • Optimizer State:32-bit

  • 直接更新整个模型,内存占用大

2️⃣ LoRA(Low-Rank Adaptation)

  • 模型仍为 16-bit,只冻结预训练模型权重

  • 添加多个小的可训练 Adapter(16-bit)

  • 只训练 Adapter + 优化器状态(32-bit)

  • 大幅减少可训练参数数量和内存使用

3️⃣ QLoRA(Quantized LoRA)

  • 模型参数被量化为 4-bit(极大节省内存)

  • 模型权重不可训练

  • 只训练 16-bit 的 Adapter 层(与 LoRA 类似)

  • 优化器状态仍为 32-bit

  • 支持 CPU Paging:当显存不足时,将优化器状态分页到 CPU 内存中,进一步节省显卡资源

QLoRA = 4-bit 模型权重 + LoRA Adapter + 高效优化状态管理 + CPU 分页技术

绝大多数大语言模型(LLM)参数量过于庞大,无法在消费级硬件上进行微调。例如,要微调一个 650 亿参数的模型,需要超过 780GB 的 GPU 显存,这相当于十块 A100 80GB 显卡的显存总量。换句话说,您必须依赖云计算资源才能完成模型微调。

而现在通过 QLoRa 技术(Dettmers 等人,2023 年),仅需单块 A100 显卡即可实现。

本文我将介绍 QLoRA 技术,阐述其工作原理,并展示如何利用它在您的 GPU 上微调一个拥有 200 亿参数的 GPT 模型。

QLoRA:量化低秩适配器 LLMs

2021 年 6 月,Hu 等人(2021)提出了用于 LLMs 的低秩适配器(LoRA)。LoRA 为 LLM 的每一层添加了少量可训练参数(即适配器),同时冻结所有原始参数。在微调过程中,我们只需更新适配器权重,这显著降低了内存消耗。QLoRA 在此基础上更进一步,引入了 4 位量化、双重量化技术,并利用 NVIDIA 统一内存实现分页管理。

简而言之,这些改进步骤的工作原理如下:

4 位 NormalFloat 量化:这是一种改进分位数量化的方法。它能确保每个量化区间包含等量的数值,从而避免异常值带来的计算问题和误差。

双重量化:QLoRA 的作者将其定义为:"对量化常数进行二次量化以进一步节省内存的过程"。

统一内存分页:该技术基于 NVIDIA 统一内存特性,能自动处理 CPU 与 GPU 之间的页面传输。它确保 GPU 处理过程零错误,尤其在 GPU 可能耗尽内存的情况下仍能可靠运行。

这些步骤在保持与标准微调几乎同等性能的同时,大幅降低了微调过程中的内存消耗。

使用 QLoRA 微调 GPT 模型

QLoRA 的硬件需求:

GPU:它可以在配备 12GB 显存的 GPU 上运行,适用于参数少于 200 亿的模型,例如 GPT-J。举例来说,我就在我的 RTX 3060 12GB 显卡上运行过。如果你拥有 24GB 显存的高端显卡,则可以运行 200 亿参数的模型,例如 GPT-NeoX-20b。

内存:建议至少 6GB。大多数现代电脑都具备足够的内存容量。

硬盘空间:GPT-J 和 GPT-NeoX-20b 都是超大型模型。建议至少保留 80GB 可用空间。

QLoRa 的软件要求:

我们需要 CUDA。请确保您的机器已安装。

我们还需要安装所有依赖项:

bitsandbytes:一个包含量化 LLM 所需全部功能的库。

Hugging Face Transformers 与 Accelerate:这些标准库用于高效训练 Hugging Face Hub 中的模型。

PEFT:该库提供了多种方法的实现,仅需微调少量(额外)模型参数。我们需要它来实现 LoRa。

Datasets:这部分并非必需。我们将仅用它来获取用于微调的数据集。当然,您也可以提供自己的数据集。

我们可以通过 PIP 获取所有这些内容:

pip install -q -U bitsandbytes
pip install -q -U transformers
pip install -q -U peft
pip install -q -U accelerate
pip install -q -U datasets

GPT 模型的加载与量化

我们需要以下导入项来加载并量化一个 LLM。

import torch
from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig

 我们将对由 EleutherAI 预训练的 GPT NeoX 模型进行微调。这是一个拥有 200 亿参数的模型。注意:GPT NeoX 采用宽松许可协议(Apache 2.0),允许商业用途。

我们可以从 Hugging Face Hub 获取该模型及对应的分词器:

model_name = "EleutherAI/gpt-neox-20b"

#Tokenizer
tokenizer = AutoTokenizer.from_pretrained(model_name)

为了获得更快且更稳定的训练效果,我建议使用 FlashAttention 技术,并采用 bfloat16 数据类型而非 float16 来存储模型参数。您可以通过以下方式测试 GPU 是否兼容 bfloat16 和 FlashAttention:

major_version, minor_version = torch.cuda.get_device_capability()
if major_version >= 8:
  !pip install flash-attn
  torch_dtype = torch.bfloat16
  attn_implementation='flash_attention_2'
  print("Your GPU is compatible with FlashAttention and bfloat16.")
else:
  torch_dtype = torch.float16
  attn_implementation='eager'
  print("Your GPU is not compatible with FlashAttention and bfloat16.")

接下来我们需要详细说明量化器的配置,具体如下:

quant_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_use_double_quant=True,
    bnb_4bit_quant_type="nf4",
    bnb_4bit_compute_dtype=torch_dtype
)

 load_in_4bit:模型将以 4 比特精度加载到内存中。

bnb_4bit_use_double_quant:我们将执行 QLoRa 提出的双重量化方案。

bnb_4bit_quant_type:量化类型。"nf4"代表 4 位 NormalFloat。

bnb_4bit_compute_dtype:虽然我们以 4 位格式加载和存储模型,但在需要时会部分反量化,并以 16 位精度(根据 GPU 类型使用 bfloat16 或 float16)进行计算。

现在,我们可以以 4 位格式加载模型:

model = AutoModelForCausalLM.from_pretrained(model_name, quantization_config=quant_config, device_map={"":0}, attn_implementation=attn_implementation)

 接着,启用梯度检查点并为训练准备模型:

model = prepare_model_for_kbit_training(model)

为 GPT 模型进行 LoRA 预处理

这里我们使用 PEFT 工具。我们为模型配置 LoRA,为每个层级添加可训练的适配器。

from peft import prepare_model_for_kbit_training, LoraConfig, get_peft_model

config = LoraConfig(
    r=8, 
    lora_alpha=32, 
    target_modules=["query_key_value"], 
    lora_dropout=0.05, 
    bias="none", 
    task_type="CAUSAL_LM"
)

model = get_peft_model(model, config)

 在 LoraConfig 中,您可以调整 r、alpha 和 dropout 参数以获得更好的任务效果。更多选项和细节可查阅 PEFT 代码库。要查看可针对的模块,可以打印模型结构:

print(model)

  打印结果显示:

GPTNeoXForCausalLM(
  (gpt_neox): GPTNeoXModel(
    (embed_in): Embedding(50432, 6144)
    (emb_dropout): Dropout(p=0.0, inplace=False)
    (layers): ModuleList(
      (0-43): 44 x GPTNeoXLayer(
        (input_layernorm): LayerNorm((6144,), eps=1e-05, elementwise_affine=True)
        (post_attention_layernorm): LayerNorm((6144,), eps=1e-05, elementwise_affine=True)
        (post_attention_dropout): Dropout(p=0.0, inplace=False)
        (post_mlp_dropout): Dropout(p=0.0, inplace=False)
        (attention): GPTNeoXFlashAttention2(
          (rotary_emb): GPTNeoXRotaryEmbedding()
          (query_key_value): Linear4bit(in_features=6144, out_features=18432, bias=True)
          (dense): Linear4bit(in_features=6144, out_features=6144, bias=True)
          (attention_dropout): Dropout(p=0.0, inplace=False)
        )
        (mlp): GPTNeoXMLP(
          (dense_h_to_4h): Linear4bit(in_features=6144, out_features=24576, bias=True)
          (dense_4h_to_h): Linear4bit(in_features=24576, out_features=6144, bias=True)
          (act): FastGELUActivation()
        )
      )
    )
    (final_layer_norm): LayerNorm((6144,), eps=1e-05, elementwise_affine=True)
  )
  (embed_out): Linear(in_features=6144, out_features=50432, bias=False)
)

 在 LoRA 配置中,我只针对"query_key_value"模块,但所有"dense*"模块同样适合作为微调更优模型的目标对象。

仅针对单一模块进行优化,我们新增了 800 万个参数。这些参数将成为唯一被训练的部分,其余参数均保持冻结状态。这种微调方式将大幅提升训练速度。

准备好数据集

我使用"english_quotes"数据集进行微调。这是一个由 CC BY 4.0 许可下分发的名人名言组成的数据集。

from datasets import load_dataset
data = load_dataset("Abirate/english_quotes")
data = data.map(lambda samples: tokenizer(samples["quote"]), batched=True)

 使用 QLoRa 微调 GPT-NeoX-20B

最后,使用 Hugging Face Transformers 进行微调的过程非常标准。

import transformers

tokenizer.pad_token = tokenizer.eos_token
model.config.use_cache = False
trainer = transformers.Trainer(
    model=model,
    train_dataset=data["train"],
    args=transformers.TrainingArguments(
        per_device_train_batch_size=1,
        gradient_accumulation_steps=8,
        warmup_ratio=0.1,
        num_train_epochs=1,
        learning_rate=2e-4,
        fp16 = torch_dtype == torch.float16,
        bf16 = torch_dtype == torch.bfloat16,
        logging_steps=100,
        save_strategy="epoch",
        output_dir="trained_adapter/",
        optim="paged_adamw_8bit"
    ),
    data_collator=transformers.DataCollatorForLanguageModeling(tokenizer, mlm=False),
)
trainer.train()

别忘了设置 optim="paged_adamw_8bit"。这会启用分页功能以实现更好的内存管理。

在A100 显卡上运行这次微调仅需 3 小时。若使用 NVIDIA RTX 显卡,预计耗时相近。

就这样,我们完成了 GPT-NeoX 的微调,使其能够引用名人名言。

效果如何?让我们试试推理效果。

我们微调过的 QLoRA 模型可以直接与标准 Hugging Face Transformers 推理流程配合使用,具体如下:

text = "Ask not what your country"
device = "cuda"
inputs = tokenizer(text, return_tensors="pt").to(device)
outputs = model.generate(**inputs, pad_token_id=tokenizer.eos_token_id, max_new_tokens=20)
print(tokenizer.decode(outputs[0], skip_special_tokens=True))

 您应该会得到如下输出结果:

Ask not what your country can do for you, ask what you can do for your country.” -John F. Kennedy

我们得到了预期的回答。 

结论

大型语言模型正变得越来越大,但与此同时,我们终于获得了在消费级硬件上进行微调和推理的工具。得益于 LoRA,以及现在的 QLoRA,根据 QLoRA 论文所述,我们能够在不依赖云计算的情况下对具有数十亿参数的模型进行微调,且性能不会显著下降。

!pip install -q -U bitsandbytes
!pip install -q -U transformers
!pip install -q -U peft
!pip install -q -U accelerate
!pip install -q -U datasets
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig

model_name = "EleutherAI/gpt-neox-20b"

#Tokenizer
tokenizer = AutoTokenizer.from_pretrained(model_name)
major_version, minor_version = torch.cuda.get_device_capability()
if major_version >= 8:
  !pip install flash-attn
  torch_dtype = torch.bfloat16
  attn_implementation='flash_attention_2'
  print("Your GPU is compatible with FlashAttention and bfloat16.")
else:
  torch_dtype = torch.float16
  attn_implementation='eager'
  print("Your GPU is not compatible with FlashAttention and bfloat16.")

quant_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_use_double_quant=True,
    bnb_4bit_quant_type="nf4",
    bnb_4bit_compute_dtype=torch_dtype
)
model = AutoModelForCausalLM.from_pretrained(model_name, quantization_config=quant_config, device_map={"":0}, attn_implementation=attn_implementation)
print(model)
from peft import prepare_model_for_kbit_training, LoraConfig, get_peft_model

model = prepare_model_for_kbit_training(model)

config = LoraConfig(
    r=8,
    lora_alpha=32,
    target_modules=["query_key_value"],
    lora_dropout=0.05,
    bias="none",
    task_type="CAUSAL_LM"
)

model = get_peft_model(model, config)
from datasets import load_dataset
data = load_dataset("Abirate/english_quotes")
data = data.map(lambda samples: tokenizer(samples["quote"]), batched=True)
print(data)
import transformers

tokenizer.pad_token = tokenizer.eos_token
model.config.use_cache = False
trainer = transformers.Trainer(
    model=model,
    train_dataset=data["train"],
    args=transformers.TrainingArguments(
        per_device_train_batch_size=1,
        gradient_accumulation_steps=8,
        warmup_ratio=0.1,
        num_train_epochs=1,
        learning_rate=2e-4,
        fp16 = torch_dtype == torch.float16,
        bf16 = torch_dtype == torch.bfloat16,
        logging_steps=100,
        save_strategy="epoch",
        output_dir="trained_adapter/",
        optim="paged_adamw_8bit"
    ),
    data_collator=transformers.DataCollatorForLanguageModeling(tokenizer, mlm=False),
)
trainer.train()
text = "Ask not what your country"
device = "cuda"
inputs = tokenizer(text, return_tensors="pt").to(device)
outputs = model.generate(**inputs, pad_token_id=tokenizer.eos_token_id, max_new_tokens=20)
print(tokenizer.decode(outputs[0], skip_special_tokens=True))

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

runner000001

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值