LoRA 与 QLoRA:把大模型微调拆成可负担的工程

从全量微调的成本出发,解释 LoRA 的低秩更新、QLoRA 的 4-bit 训练、NF4、双重量化、分页优化器,以及如何选择 rank、target modules 和数据。

目录

大模型微调最容易让人产生两个错觉。

第一个错觉是:只要把模型训练一遍,它就会“学会”新的能力。第二个错觉是:LoRA 只是把训练参数换成两个小矩阵,所以任何场景都可以直接用默认配置。

实际情况更有意思。LoRA 解决的是“如何用很少的可训练参数改变一个冻结模型的行为”,QLoRA 则进一步解决“如何在有限显存里训练一个量化后的大模型”。它们都不是数据质量、任务定义和评估方法的替代品,但确实把很多原本需要多张 GPU 的实验,变成了个人开发者可以尝试的工程。

本文从全量微调开始,一步步解释 LoRA 和 QLoRA 的数学直觉、内存账本、训练配置和常见陷阱。

1. 全量微调为什么贵

设预训练模型的参数为 W。全量微调会直接更新每一个参数:

W' = W + ΔW

问题在于,训练时显存不只存模型权重。以 Adam 为例,通常还需要保存:

  • 参数本身;
  • 梯度;
  • 一阶动量;
  • 二阶动量;
  • 前向传播需要的激活值;
  • 临时张量和通信缓冲区。

如果一个参数用 16-bit 权重存储,单看权重是 2 bytes;但优化器状态可能用 32-bit 保存,并且还要加上梯度与激活。模型越大,训练成本增长得越快。

更实际的问题是:假设你为三个不同客户、三种风格或三个领域训练了三个完整模型,就需要保存三份完整权重。部署和版本管理也会变得笨重。

2. LoRA 的核心:只学习一个低秩更新

LoRA 不直接训练整个 ΔW,而是把它近似成两个低秩矩阵的乘积:

ΔW = B A

如果原始权重 W 的形状是 d_out × d_in,全量更新需要 d_out × d_in 个参数;LoRA 只需要:

A: r × d_in
B: d_out × r

这里的 r 是 rank,通常远小于 d_in 和 d_out。前向计算可以写成:

h = W x + (α / r) B A x

其中 W 保持冻结,只有 A 和 B 参与训练。α 是缩放系数,用来控制 LoRA 更新相对原始层的影响。

这背后的直觉不是“模型只需要少数几个参数”,而是:特定任务需要的权重变化可能落在一个低维子空间里。LoRA 假设我们不必学习完整的高维更新,只需要学习其中最重要的方向。

原始 LoRA 论文在 GPT-3 175B 的实验中报告了大幅减少可训练参数和显存需求的结果,同时在多种模型上取得与全量微调接近或更好的效果。具体数字属于论文实验设置,不应被理解成所有模型、数据集和硬件都能复制的保证。

3. 为什么初始化不会立刻破坏原模型

LoRA 常见的初始化方式是让一个矩阵随机初始化,另一个矩阵初始化为零。这样一开始:

B A ≈ 0

适配器刚挂上去时,模型的行为接近原始模型;训练过程中,A 和 B 再逐步学会任务相关的变化。

这有两个工程好处:

  1. 新任务的训练从一个稳定的基线开始。
  2. 可以把多个 adapter 挂在同一个 base model 上,需要时切换或组合。

推理时,LoRA 可以保持独立加载,也可以将更新合并到基座权重:

W_merged = W + (α / r) B A

合并后不再需要单独计算 adapter 分支,适合不需要动态切换 adapter 的部署场景。独立加载则更适合一个基座模型服务多个任务或用户。

4. rank、alpha 和 dropout 到底怎么选

4.1 rank r

rank 越大,可表达的更新空间越大,参数量和训练成本也越高。rank 太小可能学不够,太大则可能:

  • 增加显存和训练时间;
  • 更容易过拟合小数据集;
  • 让 adapter 变得不再轻量。

不要把 rank 当成越大越好。一个实用流程是先用 r=8 或 r=16 建立基线,再根据验证集和任务难度尝试 r=32、r=64。如果 rank 加倍几乎没有收益,瓶颈可能在数据或目标模块,而不是容量。

4.2 lora_alpha

LoRA 分支通常乘以 alpha / r。不同项目对 alpha 的习惯不同,但核心是控制更新幅度。修改 rank 时,要同时记录 alpha,否则仅比较 rank 会混入缩放变化。

4.3 dropout

LoRA dropout 可以对适配器输入做随机丢弃,降低过拟合风险。数据量很大、任务接近继续预训练时,可以较低;数据量小、输出风格明显或标签噪声较多时,可以尝试 0.05 到 0.1,但不要机械套用。

5. target modules:挂在哪里比 rank 更重要

Transformer 中常见的注意力投影包括:

q_proj   查询投影
k_proj   键投影
v_proj   值投影
o_proj   输出投影

还有 MLP 或 FFN 中的投影层,例如:

gate_proj
up_proj
down_proj

早期 LoRA 实践常从 q_proj 和 v_proj 开始。QLoRA 论文及现在的 PEFT 实践通常会把 LoRA 应用到 Transformer 的所有线性层,以获得更充分的适配能力。不同模型的模块命名并不完全相同,不能盲目复制一份配置。

选择目标模块时可以这样考虑:

目标起点
快速验证、显存紧张q_proj、v_proj
指令微调、希望容量更足注意力层加 MLP 投影
按 QLoRA 风格训练所有线性层,或框架提供的 all-linear
分类头、回归头需要改变额外保存对应 head,不能只训练 LoRA

如果模型有 tied weights、MoE 专家层或自定义模块,还要检查适配器是否真的注入到了预期位置。最可靠的做法是打印可训练参数名称和比例,而不是只看配置文件。

6. QLoRA:冻结 4-bit 基座,训练 LoRA

LoRA 减少了可训练参数,但基座模型本身仍然可能占用大量显存。QLoRA 的关键思路是:

将冻结的预训练模型以 4-bit 量化保存
通过量化权重执行前向计算
梯度穿过量化基座,流向可训练的 LoRA adapter
只更新 LoRA 参数

它并不是“把 LoRA 参数也用 4-bit 训练”。通常情况下,adapter 会使用更高精度计算和存储,量化的是冻结的 base model。

QLoRA 论文提出了三个重要技术点。

6.1 NF4:适合正态分布权重的 4-bit 类型

普通 4-bit 整数并不是对预训练权重最合适的表示。NF4(NormalFloat 4-bit)根据权重近似正态分布的特点设计量化区间,让有限的离散值更有效地覆盖常见权重范围。

直觉上,它不是简单地把数轴等距离切成 16 格,而是让量化级别更贴合权重实际出现的概率。

6.2 双重量化

量化权重时需要保存缩放常数。双重量化进一步对这些缩放常数做量化,减少量化元数据的平均开销。在大模型上,许多层的缩放常数累积起来也会占据可观显存。

6.3 分页优化器

训练过程中会出现显存峰值,例如优化器状态或长序列激活突然变大。分页优化器借助统一内存管理,在 GPU 显存紧张时把部分状态转移出去,降低因为瞬时峰值而 OOM 的概率。

它不会让显存不足的机器凭空变快。数据搬运会带来性能代价,分页优化器更像是给峰值留出缓冲,而不是免费扩容。

7. 一笔更诚实的显存账

在讨论“某个模型需要多少显存”时,至少要区分:

基座权重
+ LoRA adapter 权重
+ 梯度
+ 优化器状态
+ 激活值
+ 临时张量
+ CUDA / 框架开销

4-bit 量化大幅降低了基座权重占用,但长上下文会显著增加激活和 KV cache;训练 batch、梯度累积、序列长度、gradient checkpointing、优化器类型也会影响峰值。

因此不要只用“参数量 × 0.5 bytes”估算 QLoRA 是否能跑。更稳妥的办法是:

  1. 先用很短的序列做一次 forward 和 backward。
  2. 记录 max_memory_allocated 和 max_memory_reserved。
  3. 再逐步增加序列长度和 batch。
  4. 为评估、保存和验证阶段单独留出余量。

8. 一个典型的 PEFT 配置

下面是一个概念性的配置示例。真实项目需要根据模型的模块命名、Transformers 版本、bitsandbytes 版本和显卡能力调整。

from peft import LoraConfig

lora_config = LoraConfig(
    r=16,
    lora_alpha=32,
    lora_dropout=0.05,
    bias="none",
    task_type="CAUSAL_LM",
    target_modules="all-linear",
)

量化配置常见的意图是:

from transformers import BitsAndBytesConfig
import torch

quant_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_quant_type="nf4",
    bnb_4bit_use_double_quant=True,
    bnb_4bit_compute_dtype=torch.bfloat16,
)

这里的 bfloat16 需要硬件支持。如果设备对 BF16 不友好,可以评估 FP16,但要注意数值稳定性和溢出问题。量化、计算 dtype、梯度 dtype 不是同一个概念,不能看到“4-bit”就以为整个训练过程都在 4-bit 中进行。

9. 数据比配置更容易决定成败

LoRA 的参数很少,并不意味着它对数据不敏感。相反,数据中的模式会更直接地写入 adapter。

9.1 指令数据要表达任务边界

一条好的指令样本至少应让模型知道:

  • 任务是什么;
  • 输入是什么;
  • 输出应该包含什么;
  • 不应该输出什么;
  • 遇到信息不足时怎么处理。

例如做文档问答,不要只给最终答案;最好让样本体现“依据资料回答、资料不足时拒答、引用来源”的行为。

9.2 少而干净通常胜过多而混乱

重复样本、互相矛盾的答案、格式不一致的对话,可能让 adapter 学到不稳定的行为。数据清洗至少应检查:

  • 同一问题是否有互相冲突的答案;
  • 训练集与验证集是否泄漏;
  • assistant 输出是否包含不应学习的系统提示;
  • 长度分布是否极端;
  • 特殊 token 和模板是否正确。

9.3 训练模板要和推理模板一致

如果训练时使用一种聊天模板,推理时换成另一种模板,模型可能表现明显下降。尤其要确认 system、user、assistant 的角色标记,以及 loss 是否只计算 assistant 部分。

10. 训练参数的取舍

常见的稳定化手段包括:

  • 使用较小学习率,先从 1e-4 到 2e-4 量级做实验,再根据模型和数据调整;
  • 用 gradient accumulation 模拟更大的有效 batch;
  • 用 gradient checkpointing 交换计算时间来降低激活显存;
  • 对长序列设置明确的截断策略;
  • 记录训练 loss 与验证集指标,不只看最后一个 checkpoint;
  • 使用 warmup 和合适的学习率调度;
  • 保存 adapter 配置、基座模型版本、数据版本和 tokenizer 版本。

这些数值不是通用定律。不同模型大小、任务类型和数据规模会改变最佳范围。特别是小数据集,训练得更久不等于效果更好。

11. 如何判断是欠拟合还是过拟合

可以先看训练集和验证集的差异:

现象可能原因可以尝试
训练和验证都差数据、模板或容量不足检查数据、增大 rank、扩大 target modules
训练好、验证差过拟合或泄漏减少 epoch、加 dropout、清洗重复数据
格式对了、事实错了数据事实质量或任务定义问题改数据,必要时配合 RAG
通用能力明显下降适配过强或数据分布窄降低学习率、混入通用数据、减小 adapter 强度
训练不稳定、loss 飙升dtype、学习率或量化配置问题检查 BF16/FP16、梯度裁剪和硬件支持

评估不能只用 loss。对聊天模型,还应准备真实用户问题,观察任务成功率、拒答质量、格式稳定性和原模型能力是否退化。

12. LoRA 和 RAG 不是竞争关系

一个很实用的组合是:

RAG 提供会变化的事实
LoRA 学习稳定的行为、格式和领域表达

例如做企业客服:

  • 产品价格、库存和政策用 RAG,因为它们会更新;
  • 公司术语、回复风格、工单分类格式可以用 LoRA;
  • 最终的权限与退款规则仍然需要业务系统校验。

如果把所有知识都训练进 LoRA,会遇到更新慢、难以追溯和容易遗忘的问题;如果把所有行为都交给 RAG,则可能让上下文变得复杂,输出格式仍不稳定。

13. 常见误区

误区一:4-bit 等于质量一定差

QLoRA 的目标是冻结量化基座,同时训练高质量 adapter。最终效果取决于量化方式、计算 dtype、任务、数据和评估,不应只凭 bit 数判断。

误区二:rank 越大越接近全量微调

rank 变大只是增加了适配器容量,不保证学到更好的方向,也可能更容易过拟合。

误区三:只看可训练参数比例

可训练参数很少是好事,但真正重要的是任务指标、泛化能力、稳定性和部署成本。

误区四:默认只挂 q_proj 和 v_proj

这是一个常见起点,不是所有模型和任务的最佳答案。QLoRA 风格的 all-linear 目标可能更强,也更耗资源。

误区五:训练完只保存 adapter 文件

adapter 很小,但它离不开对应的 base model、tokenizer、配置、数据模板和训练参数。发布时要把这些元数据一起保存。

14. 一套可复用的实验矩阵

不要一次改变十个变量。可以固定数据和训练预算,只比较:

实验 A:r=8,q/v
实验 B:r=16,q/v
实验 C:r=16,all-linear
实验 D:r=32,all-linear

每次记录:

  • 可训练参数数量;
  • 峰值显存;
  • 每 step 时间;
  • 训练和验证指标;
  • 典型样例输出;
  • 原模型能力回归结果。

如果显存允许,先把变量控制住;如果显存非常紧张,先以能稳定训练为目标,再谈容量优化。好的实验记录比一次“看起来很聪明”的配置更有价值。

15. 最后的判断

LoRA 的重要性不只是省显存。它改变了微调模型的组织方式:一个大的基座模型可以配很多轻量 adapter,任务之间互不覆盖,版本也更容易管理。

QLoRA 则把这件事进一步推向个人开发者:通过量化冻结基座、低秩适配器和更节省显存的训练策略,许多模型终于可以在有限硬件上进行领域实验。

但它们都依赖几个朴素的前提:任务边界清楚,数据干净,模板一致,评估真实,版本可追踪。真正值得保留的不是某一组神奇超参数,而是一套能回答下面这些问题的实验记录:

我训练的到底是什么行为?
哪些参数发生了变化?
它在哪些问题上变好,哪些问题上变坏?
如果换一批数据,它还成立吗?
部署时能否解释、回滚和复现?

参考资料


Ambient 默认关闭