正在加载,请稍候…

LLM 微调实战指南:LoRA、QLoRA 与 PEFT 在真实项目中的应用

大型语言模型微调实用指南:何时微调优于提示工程,使用 Hugging Face PEFT 实现 LoRA 和 QLoRA,数据集准备,以及使用 vLLM 进行生

LLM 微调实战指南:LoRA、QLoRA 与 PEFT 在真实项目中的应用

LLM 微调:LoRA、QLoRA 与 PEFT 实践

微调常被当作一个寻找问题的解决方案。在投入数周的工程工作之前,请了解微调何时真正优于提示工程。

何时微调 vs. 提示工程

优先使用提示工程(几乎总是正确的):

  • 任务可在少于 10 个上下文示例中演示
  • 需要频繁切换行为
  • 数据量少于 1000 个示例

微调胜出时

  • 大规模一致输出格式(缩短提示词可节省成本)
  • 领域特定知识不在预训练中
  • 任务需要超过 10 个少样本示例
  • 延迟至关重要(较小的微调模型优于较大的基础模型)
  • 数据隐私阻止发送到 API

LLM 微调实战指南:LoRA、QLoRA 与 PEFT 在真实项目中的应用 插图

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!

LLM 微调实战指南:LoRA、QLoRA 与 PEFT 在真实项目中的应用 插图

数据集准备

数据质量远比数量重要:

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()

LLM 微调实战指南:LoRA、QLoRA 与 PEFT 在真实项目中的应用 插图

合并与保存

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 在小型模型上开始,通过评估进行验证,然后扩展规模。