LoRA、QLoRA 与 P-Tuning
从低秩更新、量化内存和提示参数化出发,计算三类 PEFT 方法真正节省的参数与显存。
- 建议时长
- 9–12 小时
- 难度
- 微调核心
- 课程位置
- 6 / 12
学完这一课,你应该能:
- 能现场计算 LoRA 与全参训练的参数量差异
- 能解释 rank、alpha 与 target_modules 的作用
- 能拆分 QLoRA 的权重、计算和优化器精度
- 能按任务和部署约束选择 PEFT 方法
1. LoRA 的核心假设
全参微调把预训练权重 (W) 更新为 (W+\Delta W)。LoRA 假设任务更新可由低秩矩阵近似:
ΔW = (α/r) · B A
A: [r, d_in]
B: [d_out, r]
原矩阵参数量为 (d_{out}d_{in}),LoRA 可训练参数为:
r(d_in + d_out)
例:4096×4096 线性层约 1678 万参数。r=16 的 LoRA 只有 16×(4096+4096)=131072,约为原矩阵 0.78%。
冻结基座不仅少存梯度,还减少优化器状态。Adam 通常为每个可训练参数保存一阶、二阶状态,因此“只训练 1% 参数”的内存收益可能大于 1%。
2. Rank、alpha 与初始化
- rank r:低秩子空间容量。更大不保证更好,会增加训练和存储成本。
- alpha:通过 (\alpha/r) 缩放更新,影响有效步长。
- dropout:作用于 adapter 输入的正则化,不是必须固定为 0.05。
- 初始化:常把 B 初始化为 0,使开始时 (\Delta W=0),模型初始行为与基座一致。
调参应做单变量消融。例如固定 alpha/r 比例比较 r=8/16/32,或固定 r 扫描 alpha。不要同时改 rank、学习率、数据量后宣称 rank 带来提升。
3. Target modules 决定能力落在哪里
Transformer 中可注入:
- Attention 的 q/k/v/o projection
- FFN 的 up/down/gate projection
- 其他线性层
只改 q/v 成本低,但未必适合所有任务;覆盖所有线性层容量更强、显存也更高。先打印模型模块名:
for name, module in model.named_modules():
if isinstance(module, torch.nn.Linear):
print(name, tuple(module.weight.shape))
然后明确配置,而不是复制另一个模型的 target_modules。名字匹配错误可能导致零个模块被训练,或训练了意料外的 head。
4. 用 PEFT 配置 LoRA
from peft import LoraConfig, get_peft_model
config = LoraConfig(
task_type="CAUSAL_LM",
r=16,
lora_alpha=32,
lora_dropout=0.05,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
bias="none",
)
model = get_peft_model(model, config)
model.print_trainable_parameters()
trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)
total = sum(p.numel() for p in model.parameters())
print(trainable, total, trainable / total)
训练前断言可训练参数数量符合预期,并检查一轮反向后 adapter 的梯度非零。
5. QLoRA 到底量化了什么
QLoRA 通常把 冻结的基座权重 以 4-bit 形式存储,计算时反量化到 BF16/FP16;LoRA adapter 仍用较高精度训练。需要分开四类内存:
- 量化基座权重
- LoRA 可训练参数
- LoRA 的梯度与优化器状态
- 激活、临时 buffer 与 CUDA allocator 预留
因此“7B × 4 bit ≈ 3.5GB”不是完整显存预算。还要加量化元数据、激活、KV/attention 中间量和运行时碎片。
NF4 针对近似正态分布权重设计量化点;double quantization 进一步量化量化常数。Paged optimizer 主要用于缓解偶发显存峰值,不代表计算免费。
from transformers import BitsAndBytesConfig
quant = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_use_double_quant=True,
bnb_4bit_compute_dtype=torch.bfloat16,
)
硬件不支持 BF16 时需改 FP16,并验证数值稳定性。
6. P-Tuning 与 Prefix Tuning
Prompt tuning 学习一组连续向量作为“软提示”,不修改模型大多数权重。Prefix tuning 常向多层注意力注入可训练 prefix K/V;P-Tuning v2 把深层 prompt 参数化推广到更多任务和模型规模。
它们的特点:
- 参数极少,适合多任务快速切换。
- 容量和稳定性可能更依赖任务与模型规模。
- 推理时 prefix 会占上下文或缓存,并非完全没有延迟成本。
- 与 LoRA 的部署、合并和缓存复用方式不同。
选择时不仅比较评测分数,还要比较 adapter 切换、服务并发、冷启动与制品管理。
7. Merge 不是无条件的
标准 LoRA 可把 (\Delta W) 合并到 W,减少推理时额外矩阵操作:
merged_model = peft_model.merge_and_unload()
但合并后:
- 失去快速切换 adapter 的便利。
- 若基座是量化权重,合并精度与保存流程要验证。
- 多 adapter 组合顺序可能影响结果。
- 某些动态 adapter 方法本身不可简单合并。
上线前比较未合并与合并模型的 logits、任务指标和服务延迟。
8. 本课对照实验
同一模型、数据、seed、训练 token 预算下,完成:
| 方案 | 变量 |
|---|---|
| Frozen baseline | 只训练任务 head 或不训练 |
| LoRA r=8 | 固定 target modules |
| LoRA r=32 | 仅改变 rank |
| QLoRA r=16 | 与 LoRA 比较内存/吞吐 |
记录:
- trainable/total params
- peak allocated 与 peak reserved VRAM
- tokens/s、总训练时间
- 任务指标与三 seed 方差
- adapter 大小、加载时间、合并后延迟
报告的结论应是“在本任务和预算下哪个 Pareto 点更好”,不是笼统宣布某方法最佳。
9. 常见失败
target_modules没匹配,实际没有 adapter。- 学习率沿用全参微调,LoRA 更新过慢或过快。
- 量化配置省了权重,却被过长序列激活挤爆显存。
- 只比较最终分数,不对齐训练 token 与 wall time。
- 保存 adapter 却没记录基座 revision,之后无法加载同一模型。
10. 练习与答案提示
- 8192×4096 矩阵、r=8 的 LoRA 参数量?答:
8×(8192+4096)=98304。 - r 翻倍时参数量如何变?**答:**线性翻倍。
- QLoRA 是否把 adapter 也训练成 4-bit?**答:**通常不是,主要量化冻结基座。
- 为什么 adapter 必须记录基座 revision?**答:**它只表达相对该基座的增量。
- LoRA 合并后一定更快吗?**答:**通常减少额外算子,但需以真实服务 profile 验证。