一、为什么要在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%,我认为这个投入产出比是值得的。几个可以直接抄走的经验:

  1. QLoRA + r=16 + 全投影 target_modules 是7B单卡微调的稳妥起点,别一上来就调r=64。
  2. 验证集按问题ID分组切分,否则loss虚低,你会误以为模型学得很好。
  3. 混5%通用数据防遗忘,成本极低。
  4. 推理时动态加载adapter而非合并权重,4-bit场景下更稳。
  5. loss曲线看趋势不看绝对值,验证loss在epoch后期轻微回升是正常的,别慌着early stop。

下一步我打算试试 DoRA(peft 0.12 已支持)和更长上下文的样本,看看能不能把长文档问答的场景也吃下来。如果这篇对你有用,欢迎评论区交流你踩过的坑。