
LLM 微调:LoRA、QLoRA 与 PEFT 实践
微调常被当作一个寻找问题的解决方案。在投入数周的工程工作之前,请了解微调何时真正优于提示工程。
何时微调 vs. 提示工程
优先使用提示工程(几乎总是正确的):
- 任务可在少于 10 个上下文示例中演示
- 需要频繁切换行为
- 数据量少于 1000 个示例
微调胜出时:
- 大规模一致输出格式(缩短提示词可节省成本)
- 领域特定知识不在预训练中
- 任务需要超过 10 个少样本示例
- 延迟至关重要(较小的微调模型优于较大的基础模型)
- 数据隐私阻止发送到 API

LoRA:核心概念
全参数微调会更新数十亿参数。LoRA 在冻结基础模型的同时添加小型可训练矩阵:
原始 W (4096 x 4096) = 1600 万参数
LoRA: W + BA 其中 B (4096 x 16) 和 A (16 x 4096) = 13.1 万参数
可训练参数:原始参数的 0.8%!
from peft import get_peft_model, LoraConfig, TaskType
from transformers import AutoModelForCausalLM, AutoTokenizer
import torch
model_name = 'meta-llama/Llama-3.1-8B-Instruct'
model = AutoModelForCausalLM.from_pretrained(
model_name, torch_dtype=torch.bfloat16, device_map='auto'
)
lora_config = LoraConfig(
task_type=TaskType.CAUSAL_LM,
r=16, # 秩:8-64
lora_alpha=32, # 缩放(通常为 2 倍秩)
lora_dropout=0.1,
target_modules=['q_proj', 'k_proj', 'v_proj', 'o_proj',
'gate_proj', 'up_proj', 'down_proj'],
bias='none',
)
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
# trainable params: 83,886,080 || all params: 8,114,114,560 || trainable%: 1.03
QLoRA:在消费级 GPU 上微调
QLoRA 将基础模型量化为 4 位,使得在 48GB GPU 上微调 70B 模型成为可能:
from transformers import BitsAndBytesConfig
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type='nf4', # NormalFloat4(最佳质量)
bnb_4bit_compute_dtype=torch.bfloat16,
bnb_4bit_use_double_quant=True, # 量化量化常数
)
model = AutoModelForCausalLM.from_pretrained(
model_name,
quantization_config=bnb_config,
device_map='auto'
)
# 为 QLoRA 训练准备模型
from peft import prepare_model_for_kbit_training
model = prepare_model_for_kbit_training(model)
model = get_peft_model(model, lora_config)
# 70B 模型现在可装入 48GB VRAM!

数据集准备
数据质量远比数量重要:
from datasets import Dataset
from transformers import AutoTokenizer
def format_instruction(example: dict) -> str:
return f'''<|begin_of_text|><|start_header_id|>system<|end_header_id|>
You are a helpful assistant.<|eot_id|>
<|start_header_id|>user<|end_header_id|>
{example['instruction']}<|eot_id|>
<|start_header_id|>assistant<|end_header_id|>
{example['output']}<|eot_id|>'''
def tokenize_dataset(examples, tokenizer, max_length=2048):
formatted = [format_instruction(ex) for ex in examples]
tokenized = tokenizer(
formatted,
truncation=True,
max_length=max_length,
padding=False,
return_tensors=None,
)
# 在标签中屏蔽填充 token
tokenized['labels'] = [
[-100 if t == tokenizer.pad_token_id else t for t in ids]
for ids in tokenized['input_ids']
]
return tokenized
# 数据质量检查清单:
# - 至少 500 个示例(最好 1000+)
# - 格式一致
# - 无个人身份信息
# - 任务类型分布均衡
# - 人工审核 10% 的样本
训练配置
from transformers import TrainingArguments
from trl import SFTTrainer
training_args = TrainingArguments(
output_dir='./checkpoints',
num_train_epochs=3,
per_device_train_batch_size=4,
gradient_accumulation_steps=4, # 有效批量大小:16
learning_rate=2e-4,
lr_scheduler_type='cosine',
warmup_ratio=0.03,
fp16=False,
bf16=True,
logging_steps=10,
save_strategy='epoch',
evaluation_strategy='epoch',
load_best_model_at_end=True,
gradient_checkpointing=True, # 以 20% 速度为代价节省内存
optim='paged_adamw_32bit', # QLoRA 优化器
dataloader_num_workers=4,
)
trainer = SFTTrainer(
model=model,
train_dataset=train_dataset,
eval_dataset=eval_dataset,
tokenizer=tokenizer,
args=training_args,
max_seq_length=2048,
dataset_text_field='text',
packing=True, # 将多个短示例打包到一个序列中
)
trainer.train()

合并与保存
from peft import PeftModel
# 将 LoRA 权重合并到基础模型
base_model = AutoModelForCausalLM.from_pretrained(
model_name, torch_dtype=torch.bfloat16
)
model = PeftModel.from_pretrained(base_model, './checkpoints/best')
merged_model = model.merge_and_unload() # 将 LoRA 融合到基础权重中
merged_model.save_pretrained('./fine-tuned-model')
tokenizer.save_pretrained('./fine-tuned-model')
使用 vLLM 进行生产服务
pip install vllm
# 使用 OpenAI 兼容 API 提供服务
python -m vllm.entrypoints.openai.api_server \
--model ./fine-tuned-model \
--served-model-name my-fine-tuned-llama \
--tensor-parallel-size 2 \
--max-model-len 4096 \
--gpu-memory-utilization 0.95 \
--quantization awq
# 相同的 OpenAI 客户端可与 vLLM 一起使用
from openai import OpenAI
client = OpenAI(base_url='http://localhost:8000/v1', api_key='token')
response = client.chat.completions.create(
model='my-fine-tuned-llama',
messages=[{'role': 'user', 'content': 'Your prompt here'}],
max_tokens=512
)
评估框架
from evaluate import load
rouge = load('rouge')
bertscore = load('bertscore')
def evaluate_model(model, tokenizer, test_cases):
predictions = []
references = []
for case in test_cases:
inputs = tokenizer(format_instruction(case), return_tensors='pt')
output = model.generate(**inputs, max_new_tokens=256)
pred = tokenizer.decode(output[0][inputs.input_ids.shape[1]:], skip_special_tokens=True)
predictions.append(pred)
references.append(case['expected_output'])
rouge_scores = rouge.compute(predictions=predictions, references=references)
print(f'ROUGE-L: {rouge_scores["rougeL"]:.3f}')
return rouge_scores
当您拥有高质量的训练数据、特定的持续任务以及超越提示工程的理由时,微调能带来真正的价值。从使用 QLoRA 在小型模型上开始,通过评估进行验证,然后扩展规模。