大模型微调最容易让人产生两个错觉。
第一个错觉是:只要把模型训练一遍,它就会“学会”新的能力。第二个错觉是: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 再逐步学会任务相关的变化。
这有两个工程好处:
- 新任务的训练从一个稳定的基线开始。
- 可以把多个 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 是否能跑。更稳妥的办法是:
- 先用很短的序列做一次 forward 和 backward。
- 记录
max_memory_allocated和max_memory_reserved。 - 再逐步增加序列长度和 batch。
- 为评估、保存和验证阶段单独留出余量。
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 则把这件事进一步推向个人开发者:通过量化冻结基座、低秩适配器和更节省显存的训练策略,许多模型终于可以在有限硬件上进行领域实验。
但它们都依赖几个朴素的前提:任务边界清楚,数据干净,模板一致,评估真实,版本可追踪。真正值得保留的不是某一组神奇超参数,而是一套能回答下面这些问题的实验记录:
我训练的到底是什么行为?
哪些参数发生了变化?
它在哪些问题上变好,哪些问题上变坏?
如果换一批数据,它还成立吗?
部署时能否解释、回滚和复现?