一、为什么要在消费级显卡上微调7B模型
先说结论:LoRA + 4-bit量化,让单卡24GB微调7B模型从"想想而已"变成"今晚就能跑"。
我最近接手一个垂直领域问答项目,基座模型是Qwen2.5-7B-Instruct。直接用它回答领域问题时,术语经常张冠李戴,格式也不稳定。全参微调7B至少需要80GB以上显存(AdamW + fp16梯度),我手上只有一张RTX 4090(24GB),所以LoRA成了唯一现实的选择。
LoRA的核心思想是冻结原模型权重,只在注意力层的部分矩阵旁注入低秩分解矩阵 $BA$,训练时只更新这两个小矩阵。参数量能降到原来的1%以下,显存占用和训练时间都大幅下降。QLoRA更进一步,把基座模型用4-bit NF4量化加载,进一步压缩显存。
这次实践的目标很明确:
- 单卡24GB能训得起来
- 微调后目标领域准确率显著提升
- 通用能力不能崩
- 推理延迟不能明显恶化
二、环境与版本
环境这块我踩过版本坑,先把确定能跑通的组合列出来:
# 关键版本
torch==2.4.0+cu121
transformers==4.46.3
peft==0.13.2
bitsandbytes==0.44.1
trl==0.12.1
datasets==3.1.0
accelerate==1.1.1
flash-attn==2.6.3
硬件:RTX 4090 24GB,系统Ubuntu 22.04,CUDA 12.1。
bitsandbytes 和 torch 的版本匹配很重要,0.44.x 对 torch 2.4 支持比较稳。flash-attn 建议从源码装,wheel经常对不上CUDA版本。
三、方案设计
整体流程分四步:
- 数据准备:把业务数据整理成Alpaca格式(instruction/input/output),清洗掉超长和空样本,最终得到12,000条。
- 模型加载:4-bit量化加载基座,配置LoRA适配器。
- 训练:用
trl的SFTTrainer,3 epoch,cosine调度。 - 推理与评估:合并适配器,对比微调前后的输出。
关键参数选择理由:
- r=16, lora_alpha=32:r太小欠拟合,太大容易过拟合且显存上升,16是7B模型的经验甜点。
- target_modules 选 q_proj, k_proj, v_proj, o_proj 四个注意力投影层,不动MLP。
- lora_dropout=0.05:小数据集上防过拟合。
- 学习率 2e-4,比全参微调高一个数量级,因为LoRA参数少。
四、核心实现
4.1 数据准备
```python
from datasets import load_dataset
def format_example(example):
if example.get("input"):
text = f"""### 指令:
{example['instruction']}
输入:
{example['input']}
回答:
{example['output']}"""
else:
text = f"""### 指令:
{example['instruction']}
回答:
{example['output']}"""
return {"text": text}
dataset = load_dataset("json", data_files="data/train.jsonl", split="train")
dataset = dataset.map(format_example)
过滤超长样本,max_length按tokenizer算,这里粗略按字符数
dataset = dataset.filter(lambda x: 50 数据数量。我一开始用了3万条含噪声的数据,效果反而不如清洗后的1.2万条。
4. 评估要分维度**。只看loss会骗人,必须同时看领域能力和通用能力,否则容易过拟合到目标分布上。
后续我打算试试 rsLoRA 和 DoRA,听说在相同r下效果更好。另外推理侧的适配器合并和量化部署也值得单独写一篇。有在折腾LoRA的朋友欢迎评论区交流,尤其是显存优化这块,坑是真的多。