一、为什么我要在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 + 自定义数据格式

整体思路:

  1. 数据准备:收集了3200条内部问答对,按8:1:1切分训练/验证/测试。格式统一成Alpaca风格,但只保留 instructionoutput,因为我们不需要多轮。
  2. 量化:用bitsandbytes做NF4 4-bit量化,双量化开启,计算类型bfloat16。
  3. LoRA配置:只挂载在 q_proj, k_proj, v_proj, o_proj, gate_proj, up_proj, down_proj 上,r=16,alpha=32,dropout=0.05。
  4. 训练:3个epoch,batch size=4,梯度累积=4,等效batch=16,学习率2e-4,cosine调度,warmup比例0.03。
  5. 推理对比:用同样的测试集,分别跑原始模型和微调后模型,人工+规则双重评估。

为什么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趋势,再决定要不要扩数据。

代码已经全部贴出,环境版本也列清楚了。有问题的评论区聊。