LoRA 与 QLoRA

1. LoRA 的基本思想

LoRA(Low-Rank Adaptation)源于一个核心洞察:神经网络的权重更新 ΔW 往往具有低 intrinsic rank。也就是说,全量微调时学到的权重变化并非在全部维度上均匀分布,而是集中在少数低秩方向上。基于这一假设,LoRA 不直接更新原始权重矩阵 W,而是通过低秩分解来参数化 ΔW,从而大幅减少可训练参数量。

形式上,给定一个预训练权重矩阵 W₀ ∈ ℝ^{d×k},LoRA 的前向传播定义为:

其中 B ∈ ℝ^{d×r},A ∈ ℝ^{r×k},r ≪ min(d, k) 为低秩维度。训练时 W₀ 冻结(不计算梯度),仅优化 A 和 B 两个小矩阵。


2. 数学推导:从低秩近似到参数化形式

2.1 为什么 ΔW 可以用低秩矩阵近似

考虑全量微调后的权重 W* = W₀ + ΔW。文献 [Hu et al., 2021] 的假设是:针对特定任务微调产生的 ΔW 位于一个低维子空间中,即 rank(ΔW) = r* ≪ d, k。这意味着 ΔW 可以写成 r 个秩一外积之和:

其中 σ_i 是奇异值,u_i、v_i 是对应的奇异向量。实际应用中 r* 往往很小(如 4~16),因此用 r 个正交基向量即可高保真地重建 ΔW 的主要方向。

2.2 缩放与 dropout

为了增强表达能力,LoRA 引入缩放因子:

  • α(alpha):缩放系数。梯度通过这个系数生效,通常 α 设置为 r 的倍数(常见 α = 2r 或 α = r)。当 α 增大时,等效学习率提升,收敛可能加快,但过大的 α 会导致训练不稳定。
  • dropout:部分实现(如 peft 库中的 LoraConfig)在训练时对 A 或 B 的输出施加 dropout。LoRA 论文原文其实不包含 dropout 层,但实际微调中 dropout 可以缓解过拟合,尤其在数据集较小时有用。如果使用,通常设置 lora_dropout = 0.0~0.1

2.3 前向传播的计算图

输入 x
  ↓
线性层: x → W₀ + (α/r) BA
  ↓
输出

梯度只流经 A 和 B,W₀ 保持冻结。在推理时,可以将 ΔW 直接合并进 W₀,推理代价与原模型完全相同。


3. LoRA 插在哪些层

3.1 常见的 target modules

最常见的做法是将 LoRA 应用在 Attention 模块中:

模块说明
q_projQuery 投影,捕获”查询什么”的能力
k_projKey 投影,捕获”关注什么”的能力
v_projValue 投影,捕获”记忆什么”的能力
o_projOutput 投影,聚合注意力输出

部分研究也会在 MLP 层gate_proj, up_proj, down_proj)添加 LoRA,尤其是在Mistral等模型上,MLP LoRA 对某些任务有帮助。

3.2 为什么不同 target module 会影响效果

不同的 target module 控制模型的不同方面:

  • Q/K/V:负责注意力机制的信息提取和匹配。修改它们直接影响模型”看什么”。
  • O:负责注意力结果的变换。修改它影响模型”怎么整合”。
  • MLP:负责非线性变换,与知识存储和推理强相关。

经验上,在大多数 NLP 任务中,q_proj + v_proj 是最低有效配置;全量 QKV + O 可以取得更好的任务适配效果,但参数量也相应增加。


4. LoRA 与全参微调的对比

维度LoRA全参微调
可训练参数量~0.1%~5% 的原模型100%
显存占用显著更低(详见 QLoRA 节)高,需全量梯度
推理延迟可合并为等价权重,无额外开销
任务适应性适用于单一任务/领域更强,可处理复杂分布偏移
训练稳定性较稳定,冻结 W₀ 提供稳定优化曲面可能不稳定,需要更大的学习率调优
灾难性遗忘保留 W₀,不易遗忘预训练知识风险较高

5. QLoRA:低显存条件下的 LoRA

5.1 核心思想:4-bit 量化

QLoRA(Quantized LoRA)的目标是让 7B~70B 级别的大模型在单张 A100(80G)或更小显存(如 24G)上完成 LoRA 微调。核心技术是 4-bit NormalFloat(NF4)量化,配合 双重量化(double quantization)分页优化器

5.1.1 量化、反量化与计算精度

理解 QLoRA 需要区分三个概念:

  1. 量化(Quantization):将高精度权重(如 FP16/FP32)映射到低精度表示(如 INT4,16 个离散值)。
  2. 反量化(Dequantization):在使用时将低精度表示恢复为近似的 FP16/FP32 进行计算。注意:QLoRA 中基座模型 W₀ 以 4-bit 存储在显存中,前向传播前临时反量化为 BF16 进行计算。
  3. 计算精度:LoRA 的 A、B 矩阵以 BF16 精度存储和计算,不受 W₀ 量化的影响。

关键点:LoRA 的梯度更新发生在 A、B 上,而 A、B 本身就是小矩阵,不需要量化。因此即使 W₀ 是 4-bit,LoRA 部分依然保持高精度训练。

5.2 QLoRA 的工程架构

4-bit 量化基座模型(存储在显存中)
  ↓
 前向/反向传播时反量化到 BF16
  ↓
 LoRA A/B(BF16,梯度更新)
  ↓
 梯度反传时,不更新 W₀(保持量化状态)

5.3 双重量化

Double Quantization 的思想是:对量化常数(quantization constants)本身再做量化。NormalFloat 量化需要为每个 tensor 存储一个 scale/shift,double quantization 将这些常量进一步压缩到 INT8。


6. 工程流程

6.1 环境准备

pip install peft bitsandbytes transformers accelerate

6.2 加载量化基座模型

from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
 
bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_quant_type="nf4",         # NormalFloat 4-bit
    bnb_4bit_compute_dtype=torch.bfloat16,
    bnb_4bit_use_double_quant=True,     # 双重量化
)
 
model = AutoModelForCausalLM.from_pretrained(
    "meta-llama/Llama-2-7b-hf",
    quantization_config=bnb_config,
    device_map="auto",
)
model = prepare_model_for_kbit_training(model)

6.3 配置 LoRA

lora_config = LoraConfig(
    r=16,                          # rank,低秩维度
    lora_alpha=32,                  # 缩放系数,通常设为 2*rank
    target_modules=["q_proj", "v_proj"],  # 可改为 ["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: 4,194,304 || all params: 6,738,415,616 || trainable%: 0.0623

6.4 超参选择建议

以下建议基于 LoRA 论文、QLoRA 论文及主流开源实践:

超参建议范围说明
rank (r)4~64,常见 8/16/32越大表达能力越强,但参数量也增加
alpha (α)通常 2r 或 r调整等效学习率
target_modulesQKV 最低配,QKV+MLP 全配视任务复杂度定
lora_dropout0.0~0.1,数据少时可用 0.1防止过拟合
learning_rate1e-4 ~ 3e-4(AdamW)LoRA 对学习率较敏感,建议从 1e-4 开始
batch_size1~8(per device)受限于显存
gradient accumulation steps4~32与 batch_size 配合达到目标 effective batch
warmup_steps总步数的 5%~10%预热学习率
max_steps100~5000视数据量而定,短数据集可更少步数

6.5 训练参数示例

training_arguments = TrainingArguments(
    output_dir="./lora_output",
    num_train_epochs=3,
    per_device_train_batch_size=4,
    gradient_accumulation_steps=4,
    learning_rate=1e-4,
    warmup_ratio=0.1,
    max_steps=500,
    logging_steps=10,
    save_steps=100,
    optim="paged_adamw_8bit",        # 节省显存的 AdamW 变体
    fp16=False,
    bf16=True,                       # A100/A800 支持 BF16
    gradient_checkpointing=True,     # 用时间换显存
)

6.6 保存 LoRA Adapter

# 仅保存 LoRA weights(不含基座模型)
model.save_pretrained("./lora_adapter")
 
# 合并权重到基座模型后保存(可用于推理)
from peft import PeftModel
merged_model = model.merge_and_unload()
merged_model.save_pretrained("./merged_model")

6.7 推理部署

方式一:加载 adapter(推荐,生产环境常用)

from transformers import AutoModelForCausalLM
from peft import PeftModel
 
base_model = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-2-7b-hf")
model = PeftModel.from_pretrained(base_model, "./lora_adapter")
# 注意:QLoRA 场景下,base_model 需要以相同量化配置加载

方式二:合并后推理(延迟最优)

# 合并后的模型可直接当原始模型使用,无额外延迟
merged_pipeline = pipeline("text-generation", model=merged_model, tokenizer=tokenizer)

7. 常见问题与处理

7.1 显存不够

  1. 减小 rank:rank=4 在极端情况下仍可工作
  2. 减小 batch_size,增大 gradient_accumulation_steps
  3. 开启 gradient_checkpointing=True:用计算换显存
  4. 使用 paged_adamw_8bit 优化器:自动将 optimizer states paging 到 CPU
  5. 切换到更激进的量化:将 bnb_4bit_quant_type 改为 fp4(4-bit float)
  6. 降低 sequence_length:减少 KV cache 显存占用

7.2 训练不稳定

  1. 降低 learning_rate:从 5e-5 开始尝试
  2. 调整 alpha:如果 loss 爆炸,尝试将 alpha 从 2r 降到 r
  3. 启用 bf16:A100/A800 建议用 bf16,避免 fp16 溢出
  4. 检查数据质量:脏数据(重复、截断错误)会导致 loss 不收敛
  5. 增加 warmup:将 warmup_ratio 从 0.1 提升到 0.2

7.3 Loss 不下降

  1. 确认 target_modules 正确:错误的目标模块会导致训练无效
  2. 检查学习率:太低会几乎不更新,太高会跳过最优解
  3. 数据配比问题:如果用混合数据集,确认各类别样本量比例合理
  4. 验证数据预处理:tokenize 是否正确、padding/truncation 是否符合预期
  5. 尝试更大的 rank:低 rank 可能欠拟合

8. QLoRA 在不同硬件上的注意事项

显卡建议量化精度建议 batch_size注意事项
A100 80GFP16/BF16(可不量化)8~16性能充裕,QLoRA 提升有限
A800 80G同 A1008~16NVLink 有利于多卡
A10 / RTX 30904-bit NF41~4注意散热,QLoRA 收益明显
消费卡(如 4090)4-bit NF41~2显存限制严苛,建议 rank ≤ 8
多卡 A100BF16 + FSDP16~32注意分片策略

9. 量化误差与数值稳定性

  • NF4 vs FP4:NF4 对神经网络权重分布做了优化(基于 0-1 均匀分布的假设),通常比直接 FP4 量化精度更高。
  • 数值溢出:4-bit 量化后,反量化到 BF16 时误差约为 0.01~0.05 量级。对绝大多数任务,这个误差可忽略;但在极其精确的数值计算任务(如金融)中需要额外验证。
  • LoRA 梯度误差隔离:由于 LoRA A/B 保持高精度,只有 W₀ 量化引入误差,梯度更新方向相对准确,整体数值稳定性尚可接受。
  • 吞吐 vs 显存权衡:量化节省了约 4x 显存,但部分 kernels 的计算效率可能下降 20%~30%(反量化开销)。实际使用中,显存节省通常带来的 batch_size 提升远超计算效率损失。

10. 附录:超参建议速查表

LoRA 超参建议表

场景rankalphatarget_moduleslearning_ratedropout
快速实验4~88~16q_proj, v_proj3e-40.0
标准微调8~1616~32q_proj, k_proj, v_proj, o_proj1e-40.05
高质量微调32~6432~64上述 + MLP 全模块5e-50.1
大模型(≥13B)8~3216~32q_proj, v_proj(保守)1e-40.0~0.05

LoRA 训练常见错误排查表

症状可能原因解决方案
print_trainable_parameters() 显示 0target_modules 没有匹配到任何层检查模型结构,用 model.named_modules() 确认层名
Loss 爆炸/NaN学习率过高 / alpha 过大降低 lr 到 5e-5,alpha 设为 r
Loss 完全不下降数据预处理问题 / target_module 错误验证数据 tokenize 流程;确认层名匹配
训练极慢batch_size 过大 / 显存不足减小 batch_size,开启 gradient_checkpointing
保存后合并报错量化版本不一致确保推理时量化配置与训练一致
推理结果差adapter 未正确加载确认 PeftModel.from_pretrained 使用了正确的 adapter path
多卡训练 OOMFSDP 配置问题检查 device_map 是否为 “auto”,或手动分片
Gradient overflowfp16 溢出切换到 bf16,或降低学习率

参考

  1. Hu et al., “LoRA: Low-Rank Adaptation of Large Language Models”, ICLR 2022.
  2. Dettmers et al., “QLoRA: Efficient Finetuning of Quantized LLMs”, NeurIPS 2023.
  3. peft 库文档:https://huggingface.co/docs/peft
  4. bitsandbytes 库文档:https://github.com/bitsandbytes-foundation/bitsandbytes