一、为什么要在7B模型上折腾LoRA
先说结论:如果你手上有一批领域数据,想让通用大模型"说人话、答对题",LoRA是目前性价比最高的方案,没有之一。
我这次的需求来自一个做B端SaaS的朋友:他们的客服系统接的是Qwen2.5-7B-Instruct,通用问题答得不错,但一碰到自家产品的功能细节就开始胡编——比如把"审批流支持三级会签"说成"支持无限级",把"导出Excel上限5万行"说成"无限制"。这种幻觉在To B场景是致命的。
全量微调7B?单卡4090想都别想,光是优化器状态就要吃掉60GB+。继续用RAG?他们试过,文档切片后检索召回率不稳定,而且模型经常"看到了但不用"。所以最后选了LoRA,并用QLoRA把显存压到单卡可跑。
这篇文章不讲LoRA的数学原理(低秩分解那套网上烂大街了),只讲我实际跑通的流程、踩的坑和最终数据。
二、环境与版本
环境这东西必须先列清楚,不然复现时版本对不上能折腾一整天:
GPU: NVIDIA RTX 4090 24GB (单卡)
CUDA: 12.1
PyTorch: 2.3.1
transformers: 4.44.2
peft: 0.12.0
bitsandbytes: 0.43.3
trl: 0.9.6
accelerate: 0.33.0
datasets: 2.20.0
Python: 3.10.14
几个版本上的硬约束提醒一下:
peft 0.12.0对transformers 4.44的兼容性没问题,但如果你用更老的peft 0.8,target_modules的自动推断会漏掉 Qwen2 的q_proj/v_proj,得手动指定。bitsandbytes在 Windows 上装 0.43.x 需要额外编译,建议直接 Linux 或者 WSL2。我一开始在 Windows 上折腾了两小时,最后老老实实切到 Ubuntu。trl的SFTTrainer在 0.9.x 里参数名从dataset_text_field改过一轮,抄老教程容易报unexpected keyword argument。
三、方案设计:LoRA 还是 QLoRA
先给结论:7B模型 + 24GB显存,QLoRA是唯一稳妥选择;如果是A100 40G以上,纯LoRA(fp16)更省心。
我实测过两套配置:
| 配置 | 显存峰值 | batch=1时能否跑 | 备注 |
|---|---|---|---|
| 纯LoRA (fp16 base) | OOM (>24GB) | 否 | 优化器+激活就爆了 |
| QLoRA (4-bit NF4) | 18.6GB | 是 | 可开gradient checkpointing后到15GB左右 |
关键参数:
- 量化:
load_in_4bit=True,bnb_4bit_quant_type="nf4",bnb_4bit_compute_dtype=torch.bfloat16,bnb_4bit_use_double_quant=True - LoRA:
r=16,lora_alpha=32,lora_dropout=0.05,target_modules=["q_proj","k_proj","v_proj","o_proj","gate_proj","up_proj","down_proj"] - 训练:
per_device_train_batch_size=2,gradient_accumulation_steps=8(等效batch=16),learning_rate=2e-4,lr_scheduler_type="cosine",warmup_ratio=0.03,num_train_epochs=3,max_seq_length=2048
为什么 rank 选 16 而不是 8 或 64?我做了个小消融:r=8 时验证loss停在1.05左右下不去,r=64 时loss能到0.91但显存涨了约2GB且过拟合迹象明显(训练loss 0.6,验证loss回升)。r=16 是这条曲线上的甜点,最终验证loss 0.94。
target_modules 全挂上(包括 MLP 的三个投影)比只挂 attention 的 q/v 效果好不少,验证loss差约0.08,代价是trainable params从0.3%涨到1.2%——对7B来说,1.2%也就8400万参数,完全可接受。
四、核心实现
4.1 数据准备
数据是朋友那边导出的客服工单+人工改写,原始格式很脏。我统一成了 ShareGPT 风格的多轮对话,然后转成模型需要的 prompt 格式。核心是一定要加 system prompt 并在训练时也保留,否则推理时行为对不上。
```python
import json
from datasets import Dataset
SYSTEM = "你是XX公司的智能客服助手,回答需基于产品事实,不确定时明确说明,不要编造功能细节。"
def build_sample(q, a):
# Qwen2.5 的 chat template 由 tokenizer 处理,这里只组织 messages
return {
"messages": [
{"role": "system", "content": SYSTEM},
{"role": "user", "content": q},
{"role": "assistant", "content": a},
]
}
raw = []
with open("raw_qa.jsonl", encoding="utf-8") as f:
for line in f:
item = json.loads(line)
q, a = item["question"].strip(), item["answer"].strip()
# 过滤:太短、纯链接、答案含"TODO"的脏数据
if len(q) 问:审批流最多支持几级会签?
基座:我们的审批流支持无限级会签,您可以根据需要自由配置……(错误)
微调后:当前版本审批流最多支持三级会签,超过三级需要联系管理员开通企业版。如需调整请联系客户成功经理。(正确,且给出了升级路径)
通用能力只掉了1个点,说明 LoRA 的灾难性遗忘控制得不错——前提是数据里混了约5%的通用问答做正则(我从开源的 alpaca 中文子集里抽了600条混进去),这个技巧强烈建议加上。
七、总结
整套流程跑下来,单卡4090、4.5小时、约12k条数据,把7B模型的领域准确率从61%拉到87%,我认为这个投入产出比是值得的。几个可以直接抄走的经验:
- QLoRA + r=16 + 全投影 target_modules 是7B单卡微调的稳妥起点,别一上来就调r=64。
- 验证集按问题ID分组切分,否则loss虚低,你会误以为模型学得很好。
- 混5%通用数据防遗忘,成本极低。
- 推理时动态加载adapter而非合并权重,4-bit场景下更稳。
- loss曲线看趋势不看绝对值,验证loss在epoch后期轻微回升是正常的,别慌着early stop。
下一步我打算试试 DoRA(peft 0.12 已支持)和更长上下文的样本,看看能不能把长文档问答的场景也吃下来。如果这篇对你有用,欢迎评论区交流你踩过的坑。