一、问题背景:为什么不用全参微调?

上周接了个法律文书摘要项目,需要让模型理解《民法典》条文。试了全参微调Llama-2-7B,A100上跑了2小时显存就爆了——单条序列长度1024,batch_size=4时优化器状态就吃掉28GB。更离谱的是,微调完模型对训练集过拟合到loss=0.03,但验证集ROUGE-L只有0.19。

后来换成LoRA(Low-Rank Adaptation)才想明白:全参微调本质是在1024维空间里找最优解,但7B模型的参数矩阵本身就是低秩的(实测有效秩约37%)。与其更新全部70亿参数,不如冻结原权重,只训练两个低秩矩阵A和B(秩r=16),参数量瞬间降到420万,仅为原来的0.06%。

二、环境与版本:Python 3.10 + CUDA 11.8

建议直接用我这套组合,踩过坑的版本组合:

torch==2.1.2+cu118
transformers==4.38.2
peft==0.10.0
bitsandbytes==0.43.1
datasets==2.18.0
accelerate==0.27.2

特别提醒:bitsandbytes必须用0.43.1以上,否则4-bit量化会报CUDA error: no kernel image available。另外,transformerspeft版本必须配套,我一开始用transformer 4.36+peft 0.9,加载QLoRA模型时直接报KeyError: 'lora_A'

三、方案设计:QLoRA的量化与适配器布局

QLoRA相比原始LoRA多了两步:
1. 4-bit NF4量化:将原始FP16权重量化到4-bit,显存直接砍4倍。NF4(NormalFloat4)比INT4好在它按正态分布分段量化,对激活值分布更友好。
2. 双适配器设计:在q_projv_proj上挂LoRA适配器,而不是全部线性层。实测只挂q/v能保留70%效果,但显存少花2.1GB。

我的配置:

from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
from transformers import BitsAndBytesConfig

# 4-bit量化配置
bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_quant_type="nf4",
    bnb_4bit_use_double_quant=True,  # 二次量化,再省0.4GB
    bnb_4bit_compute_dtype=torch.bfloat16
)

# LoRA配置
lora_config = LoraConfig(
    r=16,               # 低秩矩阵维度
    lora_alpha=32,      # 缩放系数,alpha/r=2
    target_modules=["q_proj", "v_proj"],
    lora_dropout=0.1,
    bias="none",
    task_type="CAUSAL_LM"
)

model = AutoModelForCausalLM.from_pretrained(
    "meta-llama/Llama-2-7b-hf",
    quantization_config=bnb_config,
    device_map="auto",
    torch_dtype=torch.bfloat16
)
model = prepare_model_for_kbit_training(model)  # 冻结原权重,开启梯度检查点
model = get_peft_model(model, lora_config)

显存实测:4-bit加载后模型占5.8GB,加上梯度检查点(gradient_checkpointing=True)和适配器参数,峰值显存14.2GB。3090的24GB完全能跑,甚至还能开batch_size=8

四、核心实现:数据准备与训练循环

数据格式用的是Alpaca模板:

{
  "instruction": "根据以下法律条文,回答用户问题",
  "input": "《民法典》第1043条规定:家庭应当树立优良家风...",
  "output": "该规定确立了夫妻忠实义务,具体包括..."
}

清洗时发现三个坑:一是原始数据有HTML标签(`),要正则扒掉;二是长文本超出512 token直接截断,导致回答不完整;三是重复样本太多,用datasetsshuffle前先做了deduplicate`。

训练配置:

from transformers import TrainingArguments

training_args = TrainingArguments(
    output_dir="./legal-lora",
    per_device_train_batch_size=4,
    gradient_accumulation_steps=4,  # 等效batch_size=16
    num_train_epochs=3,
    learning_rate=2e-4,
    lr_scheduler_type="cosine",
    warmup_ratio=0.03,
    logging_steps=50,
    save_steps=500,
    fp16=False,
    bf16=True,  # 关键:3090支持BF16,比FP16更稳
    optim="paged_adamw_8bit",  # 8-bit优化器,再省2GB
)

训练时loss曲线(用tensorboard记录):

Step 0:   loss=1.82  # 初始
Step 500: loss=1.21  # 下降快
Step 1000: loss=0.83 # 变缓
Step 1500: loss=0.61 # 开始震荡
Step 2000: loss=0.47 # 收敛

重点:loss在1200步后出现抖动,怀疑是学习率过大。改成cosine衰减后,step1500的loss从0.72降到0.58。另外,前500步loss下降斜率很大,但生成质量没跟上——说明模型在学格式,还没学语义。建议前500步只记录loss,别急着评估。

五、踩坑与优化:三个致命错误

坑1:BF16精度导致loss=NaN
第一次训练用fp16=True,跑了200步loss直接变NaN。查了显卡温度(78°C正常),最后发现是FP16的精度溢出——Llama的激活值范围太大,FP16的指数位不够。换成bf16=True后问题消失,loss稳定下降。

坑2:序列长度512 vs 1024
用512训练完,推理时输入超过512,模型直接乱写。原因是位置编码没训练到长序列。把max_seq_length=1024后,显存多了3GB,但loss下降更均匀。建议直接按推理场景的最大长度设定。

坑3:LoRA适配器没合并就推理
model.generate()测试时,如果不先merge_and_unload(),推理速度只有2 token/s。合并后提升到18 token/s。代码:

from peft import PeftModel

model = PeftModel.from_pretrained(base_model, "./legal-lora/checkpoint-2000")
merged_model = model.merge_and_unload()  # 关键步骤
merged_model.save_pretrained("./legal-lora-merged")

六、效果数据:对比微调前后

用200条法律问答测试(非训练集),对比三个版本:

指标 原版Llama-2-7B LoRA微调(r=16) 全参微调(参考)
PPL (困惑度) 28.4 17.9 15.2
ROUGE-L 0.21 0.58 0.63
BLEU-4 0.09 0.34 0.41
单条生成时间 3.2s 1.8s 2.1s

定性对比

问:“夫妻分居满两年,是否自动离婚?”

原版回答:“根据法律规定,分居满两年可以自动离婚。”(错误,混淆了分居和离婚条件)

微调后回答:“根据《民法典》第1079条,因感情不和分居满二年,调解无效的,应当准予离婚。但需注意:分居需因感情不和,且需向人民法院提起诉讼,不存在自动离婚。”(正确,且引用法条)

七、总结与建议

  1. 什么时候用LoRA:领域数据量<10万条,或者只有单卡。如果数据量百万级,直接全参微调更好。
  2. 秩r怎么选:r=16是性价比甜点。我试过r=8(loss=0.61)和r=32(loss=0.42),但r=32显存多占4.1GB,推理速度慢20%。
  3. 量化精度:4-bit够用,3-bit(GPTQ)在7B模型上会掉2-3%准确率,不推荐。
  4. 训练策略:建议先用0.5个epoch试跑,看loss是否降到1.0以下,再决定是否全量训练。省电省时间。

最后说句实在的:LoRA不是银弹,它适合“快速适配”场景。如果你要模型学全新知识(比如新语言),还是得全参微调或者继续预训练。但就法律文本摘要这个任务,LoRA用1/10的显存达到了全参92%的效果,值了。

代码托管在github.com/yourname/legal-lora,有问题评论区聊。