BotOf TechAI / IoT / Full-Stack / 植物养护知识分享
W0602 / 聚焦微调实战演练

LoRA、QLoRA 与 P-Tuning

从低秩更新、量化内存和提示参数化出发,计算三类 PEFT 方法真正节省的参数与显存。

建议时长
9–12 小时
难度
微调核心
课程位置
6 / 12
LEARNING OBJECTIVES

学完这一课,你应该能:

  • 能现场计算 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 仍用较高精度训练。需要分开四类内存:

  1. 量化基座权重
  2. LoRA 可训练参数
  3. LoRA 的梯度与优化器状态
  4. 激活、临时 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. 常见失败

  1. target_modules 没匹配,实际没有 adapter。
  2. 学习率沿用全参微调,LoRA 更新过慢或过快。
  3. 量化配置省了权重,却被过长序列激活挤爆显存。
  4. 只比较最终分数,不对齐训练 token 与 wall time。
  5. 保存 adapter 却没记录基座 revision,之后无法加载同一模型。

10. 练习与答案提示

  1. 8192×4096 矩阵、r=8 的 LoRA 参数量?答:8×(8192+4096)=98304
  2. r 翻倍时参数量如何变?**答:**线性翻倍。
  3. QLoRA 是否把 adapter 也训练成 4-bit?**答:**通常不是,主要量化冻结基座。
  4. 为什么 adapter 必须记录基座 revision?**答:**它只表达相对该基座的增量。
  5. LoRA 合并后一定更快吗?**答:**通常减少额外算子,但需以真实服务 profile 验证。

11. 延伸阅读