在本教程中介绍如何使用的 peft 库和 bitsandbytes 来以 8-bits 加载大语言模型,并对其进行高效微调。微调方法使用"低秩适配器"(LoRA)的方法
一、加载模型
Facebook opt-1.3b 模型,模型权重大约需要1.53GB显存。
python
import os
import torch
import torch.nn as nn
import bitsandbytes as bnb
from transformers import GPT2Tokenizer, AutoConfig, OPTForCausalLM
model_id = "facebook/opt-6.7b"
model = OPTForCausalLM.from_pretrained(model_id, load_in_8bit=True)
tokenizer = GPT2Tokenizer.from_pretrained(model_id)
二、模型处理
在使用 peft 训练 int8 模型之前,需要进行一些预处理:
- 将所有非
int8模块转换为全精度(fp32)以保证稳定性 - 为输入嵌入层添加一个
forward_hook,以启用输入隐藏状态的梯度计算 - 启用梯度检查点以实现更高效的内存训练
使用 peft 库预定义的工具函数 prepare_model_for_int8_training,便可自动完成以上模型处理工作。
python
from peft import prepare_model_for_int8_training
model = prepare_model_for_int8_training(model)
获取当前模型占用的 GPU显存大小:
python
memory_footprint_bytes = model.get_memory_footprint()
memory_footprint_mib = memory_footprint_bytes / (1024 ** 3) # 转换为 GB
print(f"{memory_footprint_mib:.2f}GB")
输出:
1.53GB
查看model参数:
python
print(model)
输出:
python
OPTForCausalLM(
(model): OPTModel(
(decoder): OPTDecoder(
(embed_tokens): Embedding(50272, 2048, padding_idx=1)
(embed_positions): OPTLearnedPositionalEmbedding(2050, 2048)
(final_layer_norm): LayerNorm((2048,), eps=1e-05, elementwise_affine=True)
(layers): ModuleList(
(0-23): 24 x OPTDecoderLayer(
(self_attn): OPTAttention(
(k_proj): Linear8bitLt(in_features=2048, out_features=2048, bias=True)
(v_proj): Linear8bitLt(in_features=2048, out_features=2048, bias=True)
(q_proj): Linear8bitLt(in_features=2048, out_features=2048, bias=True)
(out_proj): Linear8bitLt(in_features=2048, out_features=2048, bias=True)
)
(activation_fn): ReLU()
(self_attn_layer_norm): LayerNorm((2048,), eps=1e-05, elementwise_affine=True)
(fc1): Linear8bitLt(in_features=2048, out_features=8192, bias=True)
(fc2): Linear8bitLt(in_features=8192, out_features=2048, bias=True)
(final_layer_norm): LayerNorm((2048,), eps=1e-05, elementwise_affine=True)
)
)
)
)
(lm_head): Linear(in_features=2048, out_features=50272, bias=False)
)
三、LoRA Adapter 配置
在 peft 中使用LoRA非常简捷,借助 PeftModel抽象,可以快速使用低秩适配器(LoRA)到任意模型。
通过使用 peft 中的 get_peft_model 工具函数来实现。
-
关于 LoRA 超参数的说明:
MatMul(B,A) * Scaling
Scaling = LoRA_Alpha / Rank
python
# 从peft库导入LoraConfig和get_peft_model函数
from peft import LoraConfig, get_peft_model
# 创建一个LoraConfig对象,用于设置LoRA(Low-Rank Adaptation)的配置参数
config = LoraConfig(
r=8, # LoRA的秩,影响LoRA矩阵的大小
lora_alpha=32, # LoRA适应的比例因子
# 指定将LoRA应用到的模型模块,通常是attention和全连接层的投影
target_modules = ["q_proj", "k_proj", "v_proj", "out_proj", "fc_in", "fc_out"],
lora_dropout=0.05, # 在LoRA模块中使用的dropout率
bias="none", # 设置bias的使用方式,这里没有使用bias
task_type="CAUSAL_LM" # 任务类型,这里设置为因果(自回归)语言模型
)
# 使用get_peft_model函数和给定的配置来获取一个PEFT模型
model = get_peft_model(model, config)
# 打印出模型中可训练的参数
model.print_trainable_parameters()
输出:
trainable params: 8,388,608 || all params: 6,666,862,592 || trainable%: 0.12582542214183376
参考:打印待训练模型参数的实现逻辑
python
def print_trainable_parameters(self,):
"""
Prints the number of trainable parameters in the model.
"""
trainable_params = 0
all_param = 0
for _, param in model.named_parameters():
all_param += param.numel()
if param.requires_grad:
trainable_params += param.numel()
print(
f"trainable params: {trainable_params} || all params: {all_param} || trainable%: {100 * trainable_params / all_param}"
)
四、加载数据
加载数据集
python
from datasets import load_dataset
dataset = load_dataset("Abirate/english_quotes")
print(dataset["train"])
输出:
python
Dataset({
features: ['quote', 'author', 'tags'],
num_rows: 2508
})
查看数据集:
python
from datasets import ClassLabel, Sequence
import random
import pandas as pd
from IPython.display import display, HTML
def show_random_elements(dataset, num_examples=10):
assert num_examples <= len(dataset), "Can't pick more elements than there are in the dataset."
picks = []
for _ in range(num_examples):
pick = random.randint(0, len(dataset)-1)
while pick in picks:
pick = random.randint(0, len(dataset)-1)
picks.append(pick)
df = pd.DataFrame(dataset[picks])
for column, typ in dataset.features.items():
if isinstance(typ, ClassLabel):
df[column] = df[column].transform(lambda i: typ.names[i])
elif isinstance(typ, Sequence) and isinstance(typ.feature, ClassLabel):
df[column] = df[column].transform(lambda x: [typ.feature.names[i] for i in x])
display(HTML(df.to_html()))
show_random_elements(dataset["train"], num_examples=1)
输出:
| | quote | author | tags |
| 0 | "As usual, there is a great woman behind every idiot." | John Lennon | [beatles, men, women] |
|---|
数据收集器,用于处理语言模型的数据,这里设置为不使用掩码语言模型(MLM)
python
from transformers import DataCollatorForLanguageModeling
tokenized_dataset = dataset.map(lambda samples: tokenizer(samples["quote"]), batched=True)
data_collator = DataCollatorForLanguageModeling(tokenizer, mlm=False)
输出:
python
Generating train split: 2508 examples [00:00, 112455.52 examples/s]
Map: 100%|██████████| 2508/2508 [00:00<00:00, 4468.83 examples/s]
五、微调模型
设置模型训练参数:
python
from transformers import TrainingArguments, Trainer
model_dir = "models"
training_args = TrainingArguments(
output_dir=f"{model_dir}/{model_id}-lora", # 指定模型输出和保存的目录
per_device_train_batch_size=4, # 每个设备上的训练批量大小
learning_rate=2e-4, # 学习率
fp16=True, # 启用混合精度训练,可以提高训练速度,同时减少内存使用
logging_steps=20, # 指定日志记录的步长,用于跟踪训练进度
# max_steps=100, # 最大训练步长
num_train_epochs=1 # 训练的总轮数
)
模型开始训练:
python
trainer = Trainer(
model=model, # 指定训练时使用的模型
train_dataset=tokenized_dataset["train"], # 指定训练数据集
args=training_args,
data_collator=data_collator,
)
model.use_cache = False
trainer.train()
输出:
{'train_runtime': 565.5103, 'train_samples_per_second': 4.435, 'train_steps_per_second': 1.109, 'train_loss': 2.33140508600019, 'epoch': 1.0}
- 保存 LoRA 模型
python
model_path = f"{model_dir}/{model_id}-lora-int8"
#trainer.save_model(model_path)
model.save_pretrained(model_path)
六、文本预测
通过在 english_quotes 数据集上的少量微调,LoRA 适配器恢复了阿尔伯特·爱因斯坦的名言警句。
加载模型:
python
lora_model = trainer.model
进行文本生成测试:
python
text = "Two things are infinite: "
inputs = tokenizer(text, return_tensors="pt").to(0)
out = lora_model.generate(**inputs, max_new_tokens=48)
print(tokenizer.decode(out[0], skip_special_tokens=True))
输出:
Two things are infinite: The universe and human stupidity; and I'm not sure about the universe.
I think the universe is infinite, but I'm not sure about the stupidity.
I think the universe is infinite, but I'm not sure about the stupidity
附件(完整代码)
python
import torch
import torch.nn as nn
import bitsandbytes as bnb
from datasets import load_dataset
from peft import LoraConfig, get_peft_model
from peft import prepare_model_for_kbit_training
from transformers import TrainingArguments, Trainer
from transformers import DataCollatorForLanguageModeling
from transformers import GPT2Tokenizer, AutoConfig, OPTForCausalLM
def print_model_memory(model):
"""
打印模型占用的显存(以 GB 为单位)、模型结构以及可训练参数数量。
"""
# 获取模型在 GPU 上占用的内存(字节)
memory_footprint_bytes = model.get_memory_footprint()
# 转换为 GB(注意:1024^3 = 1 GiB,但此处注释写为 GB,实际是 GiB)
memory_footprint_mib = memory_footprint_bytes / (1024 ** 3)
print(f"模型显存占用: {memory_footprint_mib:.2f} GB")
# 打印模型结构
print(model)
# 打印可训练参数的数量和占比(PEFT 特有方法)
model.print_trainable_parameters()
# === 1. 加载预训练模型(使用 8-bit 量化以节省显存)===
model_id = "facebook/opt-1.3b"
# 从预训练模型加载 OPT-1.3B,并启用 8-bit 量化(大幅降低显存需求)
model = OPTForCausalLM.from_pretrained(model_id, load_in_8bit=True)
# 对量化后的模型进行适配,使其支持后续的梯度训练(如 LoRA)
model = prepare_model_for_kbit_training(model)
# 加载与模型配套的 tokenizer(OPT 使用 GPT-2 的 tokenizer)
tokenizer = GPT2Tokenizer.from_pretrained(model_id)
# === 2. 配置并应用 LoRA(低秩自适应)===
# 创建 LoRA 配置对象,用于指定哪些层、如何插入低秩矩阵
config = LoraConfig(
r=8, # LoRA 矩阵的秩(rank),控制可训练参数量;值越小,参数越少
lora_alpha=32, # 缩放因子,控制 LoRA 更新的幅度(通常 alpha/r 决定实际学习率缩放)
# 指定在哪些模块上插入 LoRA。OPT 模型中常见的注意力和 FFN 投影层:
target_modules=["q_proj", "k_proj", "v_proj", "out_proj", "fc_in", "fc_out"],
lora_dropout=0.05, # LoRA 层中的 dropout 概率,用于防止过拟合
bias="none", # 不对 bias 参数进行 LoRA 微调(也可选 'all' 或 'lora_only')
task_type="CAUSAL_LM" # 任务类型为因果语言模型(即自回归生成)
)
# 将 LoRA 适配器注入到原始模型中,返回一个可训练的 PEFT 模型
model = get_peft_model(model, config)
# 打印模型显存占用、结构及可训练参数信息(用于调试和资源评估)
print_model_memory(model)
# === 3. 加载并预处理数据集 ===
# 从 Hugging Face Datasets 加载英文名言数据集
dataset = load_dataset("Abirate/english_quotes")
# 对数据集中的 "quote" 字段进行分词(批量处理以提高效率)
tokenized_dataset = dataset.map(
lambda samples: tokenizer(samples["quote"]), # 对每条 quote 进行 tokenize
batched=True # 启用批处理加速
)
# 创建用于语言建模的数据整理器(collator)
# mlm=False 表示不使用掩码语言建模(MLM),而是用于因果语言建模(CLM)
data_collator = DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm=False)
# === 4. 配置训练参数并初始化 Trainer ===
model_dir = "train-model" # 本地模型保存根目录
# 定义训练超参数
training_args = TrainingArguments(
output_dir=f"{model_dir}/{model_id}-lora", # 模型和日志保存路径
per_device_train_batch_size=4, # 每个 GPU 的 batch size(根据显存调整)
learning_rate=2e-4, # 学习率(LoRA 常用范围:1e-4 ~ 3e-4)
fp16=True, # 启用混合精度训练(加速 + 节省内存)
logging_steps=50, # 每 50 步记录一次训练日志
num_train_epochs=1 # 只训练 1 个 epoch(可根据需要调整)
# max_steps=100, # 可选:限制最大训练步数(与 epochs 二选一)
)
# 初始化 Hugging Face Trainer
trainer = Trainer(
model=model, # 使用已注入 LoRA 的模型
train_dataset=tokenized_dataset["train"], # 使用训练集
args=training_args, # 训练配置
data_collator=data_collator, # 数据整理器
)
# 关闭模型的缓存机制(在训练时通常设为 False,避免梯度计算错误)
model.use_cache = False
# 训练,取消下面两行注释
trainer.train()
# 保存微调后的 LoRA 权重(仅保存适配器,非全模型)
# model_path = f"{model_dir}/{model_id}-lora-int8"
model.save_pretrained(model_path)
# === 5. 使用微调后的模型进行文本生成(推理)===
# 获取训练器中的模型(即当前的 LoRA 模型)
lora_model = trainer.model
# 输入提示文本
text = "Two things are infinite: "
# 对输入文本进行分词,并将张量移动到 GPU(设备 0)
inputs = tokenizer(text, return_tensors="pt").to(0)
# 使用模型生成新文本(最多生成 48 个新 token)
out = lora_model.generate(**inputs, max_new_tokens=48)
# 解码生成的 token,跳过特殊符号(如 <s>, </s> 等)
print(tokenizer.decode(out[0], skip_special_tokens=True))