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_proj | Query 投影,捕获”查询什么”的能力 |
k_proj | Key 投影,捕获”关注什么”的能力 |
v_proj | Value 投影,捕获”记忆什么”的能力 |
o_proj | Output 投影,聚合注意力输出 |
部分研究也会在 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 需要区分三个概念:
- 量化(Quantization):将高精度权重(如 FP16/FP32)映射到低精度表示(如 INT4,16 个离散值)。
- 反量化(Dequantization):在使用时将低精度表示恢复为近似的 FP16/FP32 进行计算。注意:QLoRA 中基座模型 W₀ 以 4-bit 存储在显存中,前向传播前临时反量化为 BF16 进行计算。
- 计算精度: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 accelerate6.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.06236.4 超参选择建议
以下建议基于 LoRA 论文、QLoRA 论文及主流开源实践:
| 超参 | 建议范围 | 说明 |
|---|---|---|
| rank (r) | 4~64,常见 8/16/32 | 越大表达能力越强,但参数量也增加 |
| alpha (α) | 通常 2r 或 r | 调整等效学习率 |
| target_modules | QKV 最低配,QKV+MLP 全配 | 视任务复杂度定 |
| lora_dropout | 0.0~0.1,数据少时可用 0.1 | 防止过拟合 |
| learning_rate | 1e-4 ~ 3e-4(AdamW) | LoRA 对学习率较敏感,建议从 1e-4 开始 |
| batch_size | 1~8(per device) | 受限于显存 |
| gradient accumulation steps | 4~32 | 与 batch_size 配合达到目标 effective batch |
| warmup_steps | 总步数的 5%~10% | 预热学习率 |
| max_steps | 100~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 显存不够
- 减小 rank:rank=4 在极端情况下仍可工作
- 减小 batch_size,增大 gradient_accumulation_steps
- 开启
gradient_checkpointing=True:用计算换显存 - 使用
paged_adamw_8bit优化器:自动将 optimizer states paging 到 CPU - 切换到更激进的量化:将
bnb_4bit_quant_type改为fp4(4-bit float) - 降低 sequence_length:减少 KV cache 显存占用
7.2 训练不稳定
- 降低 learning_rate:从 5e-5 开始尝试
- 调整 alpha:如果 loss 爆炸,尝试将 alpha 从 2r 降到 r
- 启用 bf16:A100/A800 建议用 bf16,避免 fp16 溢出
- 检查数据质量:脏数据(重复、截断错误)会导致 loss 不收敛
- 增加 warmup:将 warmup_ratio 从 0.1 提升到 0.2
7.3 Loss 不下降
- 确认 target_modules 正确:错误的目标模块会导致训练无效
- 检查学习率:太低会几乎不更新,太高会跳过最优解
- 数据配比问题:如果用混合数据集,确认各类别样本量比例合理
- 验证数据预处理:tokenize 是否正确、padding/truncation 是否符合预期
- 尝试更大的 rank:低 rank 可能欠拟合
8. QLoRA 在不同硬件上的注意事项
| 显卡 | 建议量化精度 | 建议 batch_size | 注意事项 |
|---|---|---|---|
| A100 80G | FP16/BF16(可不量化) | 8~16 | 性能充裕,QLoRA 提升有限 |
| A800 80G | 同 A100 | 8~16 | NVLink 有利于多卡 |
| A10 / RTX 3090 | 4-bit NF4 | 1~4 | 注意散热,QLoRA 收益明显 |
| 消费卡(如 4090) | 4-bit NF4 | 1~2 | 显存限制严苛,建议 rank ≤ 8 |
| 多卡 A100 | BF16 + FSDP | 16~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 超参建议表
| 场景 | rank | alpha | target_modules | learning_rate | dropout |
|---|---|---|---|---|---|
| 快速实验 | 4~8 | 8~16 | q_proj, v_proj | 3e-4 | 0.0 |
| 标准微调 | 8~16 | 16~32 | q_proj, k_proj, v_proj, o_proj | 1e-4 | 0.05 |
| 高质量微调 | 32~64 | 32~64 | 上述 + MLP 全模块 | 5e-5 | 0.1 |
| 大模型(≥13B) | 8~32 | 16~32 | q_proj, v_proj(保守) | 1e-4 | 0.0~0.05 |
LoRA 训练常见错误排查表
| 症状 | 可能原因 | 解决方案 |
|---|---|---|
print_trainable_parameters() 显示 0 | target_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 |
| 多卡训练 OOM | FSDP 配置问题 | 检查 device_map 是否为 “auto”,或手动分片 |
| Gradient overflow | fp16 溢出 | 切换到 bf16,或降低学习率 |
参考
- Hu et al., “LoRA: Low-Rank Adaptation of Large Language Models”, ICLR 2022.
- Dettmers et al., “QLoRA: Efficient Finetuning of Quantized LLMs”, NeurIPS 2023.
peft库文档:https://huggingface.co/docs/peftbitsandbytes库文档:https://github.com/bitsandbytes-foundation/bitsandbytes