一、问题背景:为什么我要微调一个7B模型
先说场景。我这边有一个垂直领域的问答需求,用户提问大多是行业术语+口语化表达混在一起,比如“这个料号交期能不能压到两周内”。直接拿Qwen2.5-7B-Instruct去跑,回答格式不稳定,有时候给一堆通用建议,有时候干脆答非所问。
试过几种方案:
- Prompt工程:写了接近600字的system prompt,效果提升有限,而且token成本上去了。
- RAG:检索能补知识,但改不了“说话方式”和输出结构。
- 全量微调:7B模型全参训练,光优化器状态就爆显存,单卡A100 40G根本放不下。
所以最终选了LoRA。目标很明确:让模型学会这个领域的回答风格和固定输出结构,而不是灌大量新知识。数据量不大,1.2万条,LoRA正好合适。
二、环境与版本
环境这块我踩过版本坑,先把确定能跑通的组合列出来:
Python 3.10.13
torch 2.4.0+cu121
transformers 4.46.2
peft 0.13.2
bitsandbytes 0.44.1
trl 0.12.1
datasets 3.0.1
accelerate 1.0.1
GPU A100 40GB * 1
重点提醒:peft 0.13.x 和 transformers 4.46 搭配是稳的,我之前用peft 0.11 + transformers 4.44,加载Qwen2.5时报target_modules找不到,折腾了半天。另外bitsandbytes在Windows上基本别想,直接Linux。
三、方案设计:QLoRA + LoRA,rank怎么选
我最终用的是QLoRA,也就是4-bit量化基座 + LoRA适配器。原因很简单:40G显存虽然能跑fp16的LoRA,但batch size只能开到1,训练太慢。4-bit量化后显存直接砍到15G左右,batch size能上到8,速度反而更快。
关键参数:
| 参数 | 取值 | 说明 |
|---|---|---|
| lora_rank | 16 | 垂域风格适配够用 |
| lora_alpha | 32 | 一般取2*r |
| lora_dropout | 0.05 | 防过拟合 |
| target_modules | q/k/v/o/gate/up/down | 全注意力+MLP |
| learning_rate | 2e-4 | LoRA常用区间 |
| batch_size | 8 | 单卡 |
| grad_accum | 4 | 等效batch 32 |
| epochs | 2 | 多了会过拟合 |
| max_length | 1024 | 覆盖99%样本 |
rank我试过8、16、32。8的时候loss降不下去,32和16差距很小但显存多占1.8G,所以16是性价比最高的点。
四、核心实现
4.1 数据准备
数据格式我统一成Alpaca风格,然后用手写的chat template拼。这里要注意Qwen2.5有自己的特殊token,别自己乱拼。
import json
from datasets import Dataset
def build_sample(item):
system = "你是一个行业助手,回答要简洁、分点、给出可执行建议。"
user = item["question"]
assistant = item["answer"]
text = (
f"system\n{system}\n"
f"user\n{user}\n"
f"assistant\n{assistant}"
)
return {"text": text}
with open("data/train.json", "r", encoding="utf-8") as f:
raw = json.load(f)
ds = Dataset.from_list([build_sample(x) for x in raw])
ds = ds.train_test_split(test_size=0.02, seed=42)
print(ds)
# DatasetDict({train: 11760, test: 240})
数据清洗我做了三件事:去掉长度超过1024的样本(约120条)、去掉答案里带“作为AI”这类模板话术的、把重复问题去重。最后剩1.2万条。
4.2 训练脚本
import torch
from transformers import (
AutoModelForCausalLM, AutoTokenizer,
BitsAndBytesConfig, TrainingArguments
)
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
from trl import SFTTrainer
model_path = "Qwen/Qwen2.5-7B-Instruct"
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16,
bnb_4bit_use_double_quant=True,
)
tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
tokenizer.pad_token = tokenizer.eos_token
model = AutoModelForCausalLM.from_pretrained(
model_path,
quantization_config=bnb_config,
device_map="auto",
trust_remote_code=True,
)
model = prepare_model_for_kbit_training(model)
lora_config = LoraConfig(
r=16,
lora_alpha=32,
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM",
target_modules=["q_proj","k_proj","v_proj","o_proj",
"gate_proj","up_proj","down_proj"],
)
args = TrainingArguments(
output_dir="./output_qwen_lora",
per_device_train_batch_size=8,
gradient_accumulation_steps=4,
num_train_epochs=2,
learning_rate=2e-4,
lr_scheduler_type="cosine",
warmup_ratio=0.03,
logging_steps=10,
save_strategy="epoch",
eval_strategy="epoch",
bf16=True,
optim="paged_adamw_8bit",
report_to="none",
gradient_checkpointing=True,
)
trainer = SFTTrainer(
model=model,
args=args,
train_dataset=ds["train"],
eval_dataset=ds["test"],
peft_config=lora_config,
tokenizer=tokenizer,
dataset_text_field="text",
max_seq_length=1024,
packing=False,
)
trainer.train()
trainer.save_model("./output_qwen_lora/final")
跑起来显存峰值14.6G,单步约1.9秒,2个epoch总共用时约3小时10分钟。
五、踩坑与优化
坑1:packing开了loss反而抖。 我一开始开了packing=True,想提升吞吐,结果loss曲线锯齿特别大。原因是把多条短样本拼进同一条序列后,attention mask处理不干净,跨样本注意力泄漏。垂域数据长度分布不均,直接关掉。
坑2:eval_loss比train_loss低。 第一次跑完发现eval_loss 0.79 数据数量。1.2万条精标数据,比5万条脏数据效果好得多。
2. rank 16 + alpha 32 是垂域风格适配的甜点区,别一上来就上64。
3. QLoRA不是精度妥协,在风格和格式任务上,和fp16 LoRA差距在1%以内,但显存省一半。
4. loss曲线要看趋势不看绝对值**,平滑下降、eval不反弹,基本就稳了。
下一步我打算试试把LoRA权重合并回基座,再量化成GPTQ部署,看看推理延迟能压到多少。有进展再写一篇。