一、为什么我要在24G显存上微调7B模型
事情起因很简单:公司有个垂直领域的客服问答场景,用Qwen2.5-7B-Instruct的原始权重跑,回答总是"太通用"——问产品参数,它给你扯行业趋势;问故障处理,它给你背安全规范。Prompt engineering试了两周,效果不稳定,于是决定上微调。
但手头只有一张RTX 4090,24GB显存。全量微调7B模型,FP16下光权重就要14GB,加上优化器状态、梯度、激活值,轻轻松松突破60GB。所以路线很明确:QLoRA(4-bit量化 + LoRA适配器)。
最终目标:让模型学会用我们内部的问答风格回答,同时不丢失通用能力。
二、环境与版本:别小看版本兼容性
先列一下我实际跑通的环境,这块坑最多:
- GPU:RTX 4090 24GB
- CUDA:12.1
- Python:3.10.13
- PyTorch:2.3.1+cu121
- transformers:4.44.2
- peft:0.12.0
- bitsandbytes:0.43.3
- trl:0.9.6
- accelerate:0.33.0
- datasets:2.20.0
特别提醒:bitsandbytes 在Windows上原生支持很差,我最后是在WSL2里跑的。另外 peft 0.12.0 和 transformers 4.44.2 是验证过兼容的组合,别乱升。
三、方案设计:QLoRA + 自定义数据格式
整体思路:
- 数据准备:收集了3200条内部问答对,按8:1:1切分训练/验证/测试。格式统一成Alpaca风格,但只保留
instruction和output,因为我们不需要多轮。 - 量化:用bitsandbytes做NF4 4-bit量化,双量化开启,计算类型bfloat16。
- LoRA配置:只挂载在
q_proj, k_proj, v_proj, o_proj, gate_proj, up_proj, down_proj上,r=16,alpha=32,dropout=0.05。 - 训练:3个epoch,batch size=4,梯度累积=4,等效batch=16,学习率2e-4,cosine调度,warmup比例0.03。
- 推理对比:用同样的测试集,分别跑原始模型和微调后模型,人工+规则双重评估。
为什么r=16?试过r=8,欠拟合;r=32,显存涨到21GB且验证loss波动大。r=16是这张卡上的甜点。
四、核心实现:两个可直接跑的代码块
4.1 数据准备与格式化
import json
from datasets import Dataset
def load_and_format(file_path):
with open(file_path, 'r', encoding='utf-8') as f:
raw = json.load(f)
formatted = []
for item in raw:
# 统一成Qwen的chat template
text = f"user\n{item['instruction']}\nassistant\n{item['output']}"
formatted.append({"text": text})
return Dataset.from_list(formatted)
train_ds = load_and_format("data/train.json")
val_ds = load_and_format("data/val.json")
print(f"Train: {len(train_ds)}, Val: {len(val_ds)}")
# 输出:Train: 2560, Val: 320
4.2 QLoRA训练脚本
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_name = "Qwen/Qwen2.5-7B-Instruct"
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_use_double_quant=True,
bnb_4bit_compute_dtype=torch.bfloat16,
)
tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True)
tokenizer.pad_token = tokenizer.eos_token
model = AutoModelForCausalLM.from_pretrained(
model_name,
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,
target_modules=["q_proj","k_proj","v_proj","o_proj",
"gate_proj","up_proj","down_proj"],
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM",
)
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
# 输出:trainable params: 51,380,224 || all params: 7,667,111,936 || trainable%: 0.67
training_args = TrainingArguments(
output_dir="./qwen2.5-7b-lora",
per_device_train_batch_size=4,
gradient_accumulation_steps=4,
num_train_epochs=3,
learning_rate=2e-4,
lr_scheduler_type="cosine",
warmup_ratio=0.03,
logging_steps=10,
save_strategy="epoch",
evaluation_strategy="epoch",
bf16=True,
optim="paged_adamw_8bit",
report_to="none",
save_total_limit=2,
)
trainer = SFTTrainer(
model=model,
args=training_args,
train_dataset=train_ds,
eval_dataset=val_ds,
tokenizer=tokenizer,
dataset_text_field="text",
max_seq_length=1024,
packing=False,
)
trainer.train()
trainer.model.save_pretrained("./qwen2.5-7b-lora-final")
实测显存占用:训练峰值 18.7GB,推理时 13.2GB。训练总耗时 2小时37分钟。
五、踩坑与优化:三个真实问题
坑1:loss不降反升。 第一轮跑的时候学习率用了5e-4,结果第2个epoch开始loss从0.9飙到1.6。降到2e-4后正常。QLoRA对学习率比全量微调敏感,建议从1e-4到2e-4之间试。
坑2:验证loss比训练loss低。 一开始以为数据泄露,后来发现是dropout=0.05在训练时生效、验证时关闭导致的。正常现象,别慌。
坑3:推理时输出重复。 微调后模型偶尔会重复上一句。原因是训练数据里有几条output本身就有重复,清洗后解决。数据质量比数据数量重要得多。
优化点:开启 packing=True 能把训练速度提升约22%,但会让loss曲线变得不平滑,看个人取舍。我最后没开,因为要监控真实loss。
六、效果数据:loss曲线与推理对比
Loss曲线(3个epoch):
- Epoch 1:train loss 1.82 → 1.21,val loss 1.35
- Epoch 2:train loss 1.05 → 0.79,val loss 0.82
- Epoch 3:train loss 0.74 → 0.67,val loss 0.71
没有明显过拟合,val loss和train loss差距在0.1以内。
推理效果对比(自建测试集,200条):
| 指标 | 原始模型 | LoRA微调后 |
|---|---|---|
| 领域准确率 | 41% | 87% |
| 回答格式合规率 | 63% | 96% |
| 平均响应长度 | 142 token | 98 token |
| 单条推理耗时 | 1.21s | 1.31s |
准确率提升一倍多,响应长度反而缩短——因为模型学会了直接给答案,不再绕圈子。推理速度只降了8%,完全可接受。
一个具体case:
问:"X200设备报E05错误怎么处理?"
原始模型:"E05错误通常表示设备通信异常,建议您检查网络连接、重启设备,如果问题依旧请联系技术支持……"
微调后:"E05为X200的传感器校准失败。请先长按复位键5秒,若指示灯仍红,更换传感器模块(型号SN-205)。"
后者才是我们要的。
七、总结
QLoRA让单卡24GB微调7B模型变得完全可行。核心经验就三条:学习率别超过2e-4、数据质量决定上限、r=16在7B模型上是性价比最高的选择。整个流程从数据准备到推理验证,一天内可以跑完。如果你也在做垂直领域微调,建议先用500条数据跑一轮看loss趋势,再决定要不要扩数据。
代码已经全部贴出,环境版本也列清楚了。有问题的评论区聊。