第 13 章:参数高效微调:LoRA / QLoRA / PEFT¶
1. 本章要解决的问题¶
第 12 章里,我们已经把 SFT 的基本逻辑讲清楚了:
- 预训练模型已经具备语言能力和知识能力
- SFT 用高质量示范把这些能力塑造成“会按指令完成任务”的行为
- 从训练目标上看,SFT 仍然是 next-token prediction,只是训练分布换成了任务示范
但一旦你真的准备动手做微调,就会立刻撞上一个非常现实的问题:
全量微调太贵了。
如果模型只有几亿参数,也许问题还不明显;但当底座模型来到 7B、13B,甚至更大时,事情马上会变得不一样:
- 模型参数本身占显存
- 反向传播还要保存梯度
- 优化器状态往往还要额外占一大块内存
- 每做一个任务都保存一整份新模型,存储成本也很高
这时我们会发现,很多团队并不是“不想做 SFT”,而是:
想做,但做不起;或者做得起一次,做不起很多次。
这就是 LoRA、QLoRA、PEFT 出现的背景。
从全书结构上看,这一章有三个作用:
- 它承接第 12 章,把“可以做 SFT”推进到“怎样更省资源地做 SFT”
- 它把“训练目标”进一步落到“参数该怎么更新、显存怎么省、权重怎么保存”这些工程问题上
- 它也为第 14 章的偏好对齐铺路,因为很多对齐训练在工程上同样依赖 PEFT,而不是每次都全量改整个底座模型
如果第 12 章回答的是:
当我们有了指令数据,怎样教会模型按示范回答。
那么这一章要回答的就是:
当底座模型已经很大、算力预算又有限时,怎样以更低成本完成这类微调。
2. 你学完后应该会什么¶
- 能解释为什么全量微调在大模型时代往往不划算
- 能理解 PEFT 这个总思路到底在省什么
- 能说清 LoRA 的核心直觉:冻结大权重,只训练一个低秩增量
- 能理解 rank、alpha、dropout、target modules 分别在控制什么
- 能说清 QLoRA 和 LoRA 的区别,以及 4-bit quantization 在这里扮演什么角色
- 能搭建一个最小可用的 LoRA / QLoRA 微调流程
- 能理解 adapter 的保存、加载、merge 各是什么意思
- 能知道什么时候该用 PEFT,什么时候不该盲目用
3. 为什么全量微调越来越不现实¶
先把最重要的判断讲清楚:
LoRA 不是因为“更高级”才流行,而是因为它更符合大模型时代的资源现实。
3.1 全量微调到底贵在哪里¶
很多初学者会把“7B 模型”理解成“只要能放进显存里就能训”,但训练和推理不是一回事。
推理时你主要需要:
- 模型权重
- KV cache
- 少量运行时缓存
训练时除此之外还要额外承担:
- 梯度
- 优化器状态
- 激活保存
尤其如果你用的是 AdamW 这类优化器,参数之外还会维护一阶矩、二阶矩等状态。于是实际训练显存远远大于“模型权重本体”。
所以真正的问题不是:
这个模型能不能加载进来。
而是:
这个模型能不能在反向传播和优化时活下来。
3.2 存储成本也会很快爆炸¶
即使你显存够,全量微调还有另一个容易被忽视的成本:
每个任务都要保存一整份新权重。
想象你有一个 7B 底座模型,之后要做:
- 中文客服版
- 医疗问答版
- 代码解释版
- 数据分析助手版
如果每个任务都产出一整份完整 checkpoint,那么模型管理会非常笨重。你会很快遇到:
- 磁盘占用高
- 分发成本高
- 版本管理复杂
- 回滚和比较麻烦
3.3 很多任务其实不需要“重写整个模型”¶
更关键的是,很多下游任务并不要求你把底座模型的全部知识重新学一遍。
很多时候你只是想让它:
- 学会某种回答风格
- 掌握某类格式约束
- 对某个领域任务更稳定
- 对某些特定模式更敏感
这类变化未必需要改动全部参数。
于是一个很自然的问题就出现了:
有没有办法保留大模型原有能力,只用很少一部分新增参数来表达“这次任务需要的偏移量”?
LoRA 的答案就是:有。
4. PEFT:参数高效微调到底在省什么¶
PEFT 是 Parameter-Efficient Fine-Tuning 的缩写。
它不是单一算法,而是一大类思路的总称。它们的共同目标是:
尽量少更新参数、尽量少增加显存开销,同时保留接近全量微调的任务适配能力。
4.1 它省的不是一步,而是整条链路¶
当我们说 PEFT 更省资源时,通常是在同时节省几样东西:
- 可训练参数数量
- 梯度和优化器状态
- 每个任务需要额外保存的权重体积
- 多任务切换时的部署成本
注意这里有一个常见误区:
PEFT 不一定让前向计算本身便宜很多,但它往往能显著降低训练和存储成本。
4.2 常见 PEFT 方法里,LoRA 为什么最流行¶
PEFT 不是只有 LoRA。
历史上还出现过各种方法,例如:
- 只训练 bias
- prefix tuning
- prompt tuning
- adapter tuning
但 LoRA 之所以广泛流行,是因为它在几个维度上取得了很好的平衡:
- 原理直观
- 改动小
- 与 Transformer 结构天然兼容
- 参数量低
- 实践中效果稳定
- 工具链成熟
所以今天很多人提到“做 PEFT”,实际默认说的就是“做 LoRA 或 QLoRA”。
5. LoRA 的核心直觉:不要重训整个矩阵,只学一个低秩增量¶
这一节是整章最关键的原理部分。
先从线性层开始。
假设模型里有一个权重矩阵:
全量微调的做法是直接更新整个 W。也就是训练后得到:
这里的 ΔW 和 W 一样大,所以你本质上还是在训练一整个大矩阵。
LoRA 的想法是:
也许这个任务需要的改动,本质上并不需要一个满秩的大矩阵来表达。
于是它把增量写成两个小矩阵的乘积:
其中:
A ∈ R^(r × d_in)B ∈ R^(d_out × r)r远小于d_in和d_out
于是新的权重变成:
而训练时:
- 原始权重
W冻结 - 只训练
A和B
这就是 LoRA 最核心的结构。
5.1 为什么叫 low-rank¶
因为 BA 的秩最多不会超过 r。
当 r 很小时,这个增量矩阵就只能表达一个相对低维的变化子空间。直觉上就是:
我们不允许模型朝任意方向大改,只允许它在一个低维增量空间里做任务适配。
这听起来像是在“限制模型”,但实际经验发现:
很多下游适配任务,本来就不需要改那么多自由度。
5.2 一个非常重要的直觉¶
LoRA 并不是说原始权重不重要,恰恰相反:
原始预训练权重非常重要,所以我们尽量不碰它,只在旁边加一个小修正。
可以把它理解成:
- 预训练底座负责提供通用能力
- LoRA 增量负责把能力轻推到某个任务方向
这也是为什么 LoRA 往往特别适合:
- 指令跟随增强
- 领域化适配
- 风格迁移
- 小规模数据上的快速定制
6. LoRA 到底插在哪些地方¶
LoRA 理论上可以加在很多线性层上,但在 Transformer 里,最常见的是加在 attention 和 MLP 的投影层上。
6.1 最常见的 target modules¶
不同模型命名会不一样,但经常能看到这些模块:
q_projk_projv_projo_projup_projdown_projgate_proj
其中最常见的起点通常是:
- 先打在 attention 的
q_proj、v_proj - 或者同时覆盖大部分主要线性层
6.2 为什么不是所有层都必须加¶
因为 LoRA 追求的是“足够表达任务偏移”,不是“尽可能多改”。
如果 target modules 选得太保守,可能:
- 参数太少
- 模型适配能力不够
但如果选得太激进,也可能:
- 可训练参数显著上升
- 训练更慢
- 更容易过拟合小数据
所以 target modules 本质上是在回答一个问题:
你希望任务信号主要通过哪些计算路径进入模型。
7. rank、alpha、dropout 分别在控制什么¶
LoRA 训练里最常见的几个超参数是:
rlora_alphalora_dropout
理解它们,比死记推荐默认值重要得多。
7.1 r:低秩空间的容量¶
r 就是 rank,也就是低秩分解里的中间维度。
可以粗略理解成:
LoRA 适配器到底有多大的表达容量。
一般来说:
r小:参数更少,更省,但表达能力更弱r大:表达能力更强,但训练成本更高,也更容易过拟合
它不是越大越好,而是要和任务复杂度、数据量一起看。
7.2 lora_alpha:对增量更新的缩放¶
LoRA 实际使用时,常会对 BA 再乘一个缩放系数,典型写法类似:
它的作用可以理解为:
控制 LoRA 增量注入主干权重时的强度。
如果 alpha 太小,适配器学到的变化即使存在,注入后影响也弱;如果太大,则可能让训练更不稳定。
7.3 lora_dropout:不是给底座加 dropout,而是给适配路径加 dropout¶
这点很多人第一次会混淆。
lora_dropout 一般是作用在 LoRA 支路上,而不是把整个底座模型都重新改成更强 dropout。
它主要是为了:
- 减少小数据过拟合
- 让 LoRA 增量不要过度依赖少量路径
如果你的数据量非常小,适当的 dropout 往往比一味增大 rank 更稳。
8. 从训练视角看,LoRA 到底省了什么¶
这里值得把账再算一遍。
8.1 可训练参数少了¶
原来一个大矩阵 W 全量训练要更新 d_out × d_in 个参数。
LoRA 只训练:
如果 r 很小,这个数量会比原矩阵小很多。
8.2 梯度和优化器状态也跟着少了¶
因为冻结的参数不需要维护梯度和优化器状态,所以真正省掉的不只是“参数文件大小”,而是训练期间最贵的一块内存负担。
这也是 LoRA 实际上非常有价值的地方:
它让“大模型适配”从只有高预算团队能做,变成个人或小团队也有机会做。
8.3 每个任务只需保存 adapter¶
训练结束后,你不一定要保存一整份大模型,而是只保存 LoRA adapter 权重。
这样做的好处非常直接:
- 文件小很多
- 多任务切换方便
- 底座模型可复用
- 分发和部署更轻
所以很多团队的做法是:
- 维护一个稳定的 base model
- 针对不同业务场景训练多个 adapter
- 需要哪个任务,就加载哪个 adapter
9. QLoRA:为什么还要再加量化¶
LoRA 已经省了很多训练参数,但还有一个没解决的问题:
底座模型本身还是很大。
即使你冻结它,单是把它加载进显存,也可能已经很吃紧。
于是 QLoRA 更进一步:
既然底座模型冻结,那能不能把冻结权重用更低精度存起来,从而进一步节省显存?
答案就是 QLoRA。
9.1 QLoRA 的核心思路¶
可以把 QLoRA 概括成一句话:
把冻结的底座模型量化到 4-bit 附近,再在其上训练 LoRA adapter。
也就是说:
- 底座模型:量化存储,减少显存
- 可训练部分:仍然是 LoRA adapter
- 反向传播:主要穿过 adapter 路径
因此,QLoRA 不是“量化后全量训练”,而是:
量化底座 + LoRA 微调。
9.2 为什么这件事成立¶
因为 LoRA 本来就不更新底座大权重。
既然底座只是前向时提供能力底盘,那么我们可以接受:
- 用更紧凑的格式存它
- 在计算时做必要的反量化或混合精度处理
这样就能在不显著牺牲效果的前提下,把训练资源门槛再往下压一大截。
9.3 常见关键词:4-bit、NF4、double quantization¶
你在 QLoRA 资料里经常会看到这些词:
- 4-bit quantization
- NF4
- double quantization
作为本书这一阶段,你先抓住三点就够了:
4-bit的目标是压缩冻结权重占用NF4是一种更适合正态分布权重的 4-bit 表示方式double quantization是进一步减少量化常数本身的存储开销
这里你不需要先钻进所有量化数学细节,先理解工程目标更重要:
QLoRA 的第一价值,是让本来放不下、训不起的模型,变成“可以在更有限资源下做微调”。
10. LoRA 和 QLoRA 到底有什么区别¶
很多人会把这两个词混用,但最好明确区分。
10.1 LoRA 关注的是“怎么少训练”¶
LoRA 的重点是:
- 冻结大权重
- 只训练低秩增量
它解决的是:
训练参数太多、优化器状态太大、每任务存储太重。
10.2 QLoRA 关注的是“怎么让底座更省显存地被加载”¶
QLoRA 在 LoRA 基础上再加一层:
- 把底座量化
它进一步解决的是:
即使不训练底座,它本身加载起来也太占显存。
所以可以把关系记成:
- LoRA:参数高效微调
- QLoRA:量化底座上的 LoRA 微调
换句话说:
QLoRA 不是 LoRA 的竞争者,而是 LoRA 在显存更紧场景下的一种更激进实现。
11. PEFT 工程实践里最常见的工作流¶
到这里,我们把概念转成操作流程。
一个最常见的 LoRA / QLoRA 微调链路通常长这样:
- 选择一个 instruction-tuned 或 base 模型作为底座
- 准备与第 12 章一致的 SFT 数据
- 加载 tokenizer 和 base model
- 如果做 QLoRA,则用 4-bit 方式加载冻结底座
- 配置 LoRA adapter,并指定 target modules
- 只训练 LoRA 参数
- 保存 adapter
- 推理时把 adapter 挂回底座,或在需要时 merge
11.1 最关键的心智模型¶
这条流程里,数据侧其实没有变。
第 12 章里你学会的:
- 指令数据组织
- chat template
- labels 和 loss mask
- 训练评估逻辑
在这一章仍然都成立。
真正变化的是模型侧:
不是改“训练目标”,而是改“哪些参数能动、底座以什么精度存在”。
12. 一个最小 LoRA 代码例子¶
下面给一个偏教学化的最小示意,帮助你看清楚 LoRA 是怎样接到 Hugging Face / PEFT 工作流里的。
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import LoraConfig, get_peft_model
model_name = "Qwen/Qwen2.5-0.5B-Instruct"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForCausalLM.from_pretrained(model_name)
lora_config = LoraConfig(
r=8,
lora_alpha=16,
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM",
target_modules=["q_proj", "v_proj"],
)
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
这段代码做了几件事:
- 加载原始 causal LM
- 定义 LoRA 配置
- 指定要插入 LoRA 的模块
- 把普通模型包装成 PEFT 模型
这时如果你去看参数状态,会发现:
- 底座绝大部分参数被冻结
- 只有 LoRA 相关参数是可训练的
然后你的训练代码,原则上就可以继续沿用第 12 章那套 SFT 流程。
13. 一个最小 QLoRA 代码例子¶
如果切到 QLoRA,常见区别主要出现在模型加载阶段。
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
from peft import LoraConfig, get_peft_model
import torch
model_name = "Qwen/Qwen2.5-0.5B-Instruct"
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_use_double_quant=True,
bnb_4bit_compute_dtype=torch.bfloat16,
)
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForCausalLM.from_pretrained(
model_name,
quantization_config=bnb_config,
device_map="auto",
)
lora_config = LoraConfig(
r=16,
lora_alpha=32,
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM",
target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
)
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
要注意的是,这段代码只是最小示意,不代表所有模型都该照抄同一组 target modules 或超参数。
但它足够说明一个本质:
QLoRA 和普通 SFT 最大的工程差异,不在数据,而在“量化底座 + LoRA 包装”这一步。
14. 保存、加载、merge:adapter 生命周期怎么理解¶
这一节很实用,因为很多人第一次做完训练后,并不知道该怎么理解产物。
14.1 保存 adapter,不是保存整模型¶
在 LoRA / QLoRA 训练里,最常见的保存方式是:
只保存适配器权重。
因为真正变化的是 LoRA 参数,而不是整个 base model。
这意味着你的 checkpoint 通常会比全量微调小很多。
14.2 推理时需要“底座 + adapter”¶
如果你只保存了 adapter,那么推理时并不是单独加载它就够了,而是要:
- 先加载与训练时一致的底座模型
- 再把 adapter 挂上去
这背后的逻辑非常自然:
adapter 表达的是“相对底座的增量”,不是一个能独立存在的完整模型。
14.3 merge 的含义¶
有些时候你会看到 merge_and_unload() 这类操作。
它的意思通常是:
把 LoRA 增量真正并回主权重,得到一份不再依赖独立 adapter 的模型。
这样做的优点可能是:
- 部署链路更简单
- 某些推理框架兼容性更好
但代价是:
- 失去“一个底座切多个 adapter”的轻量切换优势
- 产物体积可能重新变大
所以是否 merge,不是固定答案,而是部署选择。
15. 一个可展示的微调项目应该怎么设计¶
如果你想把这一章真正落成作品,最好的方式不是只跑通训练脚本,而是做一个能讲清楚实验逻辑的小项目。
15.1 一个适合展示的项目结构¶
你可以选择一个足够具体、但又不至于太重的任务,例如:
- 把通用模型微调成“机器学习概念讲解助手”
- 把通用模型微调成“中文论文摘要助手”
- 把通用模型微调成“SQL 问题解释助手”
项目里最好明确交代四件事:
- 任务是什么
- 数据怎么来、怎么清洗
- 为什么选 LoRA 或 QLoRA
- 微调前后有什么实际变化
15.2 最值得展示的不是 loss 曲线,而是行为变化¶
很多项目容易犯的错是:
- 放一堆训练日志
- 放一堆超参数表
- 但看不出模型到底变好了什么
更好的展示方式是直接对比:
- 微调前回答
- 微调后回答
- 哪些行为变稳了
- 哪些问题仍然没解决
例如可以重点展示:
- 是否更遵守输出格式
- 是否更少答非所问
- 是否更符合目标领域表达
- 是否对关键术语解释更稳定
15.3 最小实验矩阵就够了¶
你不需要一上来做十几组实验。
一个很实用的最小矩阵是:
- baseline:不微调
- LoRA:较小 rank
- LoRA:较大 rank
- QLoRA:同任务、相近配置
你真正想回答的是:
在你的任务上,参数效率方案到底换来了什么,牺牲了什么。
16. LoRA / QLoRA 常见坑¶
这一节非常重要,因为很多人“跑通了”,但结果不稳定,往往是踩了工程坑。
16.1 target modules 选错或没覆盖¶
如果模型结构和你预想的不一样,而你照着别人的名字填 q_proj、v_proj,可能会出现:
- 根本没插进去
- 只插进了很少一部分层
- 可训练参数数量异常
所以第一件事通常是检查模型模块名,而不是盲抄配置。
16.2 chat template 不一致¶
这一坑并不是 LoRA 独有,但在微调项目里非常常见。
如果训练时和推理时用的对话模板不一致,就会出现:
- 格式变怪
- 角色混乱
- 输出风格偏移
这时很多人会误以为“LoRA 效果不好”,其实问题出在第 12 章讲过的数据拼接协议。
16.3 只看训练 loss,不看真实样本¶
LoRA 参数少,并不意味着它不会过拟合。
尤其当你的数据:
- 很少
- 很重复
- 风格单一
模型可能很快把训练集模式学得很紧,但泛化并不好。
所以最值得坚持的做法仍然是:
- 留出验证样本
- 固定一组人工检查 prompt
- 比较微调前后真实输出
16.4 量化配置和硬件精度不匹配¶
做 QLoRA 时,如果量化配置、计算 dtype、硬件支持没有协调好,可能会碰到:
- 训练不稳定
- 显存节省不如预期
- 某些层报错
这类问题往往不是“理论错了”,而是工程栈兼容性问题。
16.5 以为 PEFT 可以无脑代替全量微调¶
这是另一个常见误区。
PEFT 很强,但不是对所有任务都一定最优。
如果你的任务需要:
- 对模型内部能力做非常深层重塑
- 大规模跨分布迁移
- 极强的任务专门化
那么 LoRA 不一定总能替代全量微调。
正确理解应该是:
PEFT 是非常强的默认起点,但不是永远唯一答案。
17. LoRA / QLoRA 和第 14 章偏好对齐的关系¶
这一节是为了把全书主线接起来。
第 12 章我们讲 SFT,本质是在学:
给定指令,什么叫“像样的回答”。
第 13 章我们讲 LoRA / QLoRA,本质是在学:
怎样更省资源地完成这种微调。
而第 14 章要继续往前走,讨论的是:
如果“像样的回答”不只取决于标准答案,而更多取决于偏好比较,我们该怎么训练?
所以顺序非常自然:
- 先有预训练底座
- 再用 SFT 学会基本指令跟随
- 再用 PEFT 把这件事做得更现实、更省钱
- 最后进入偏好对齐,学习更细粒度的“哪种回答更好”
更重要的是,在实际工程里:
很多 DPO、RLHF、RLAIF 实验,底层也会继续使用 LoRA 或 QLoRA。
因为偏好对齐本身通常也不便宜,而 PEFT 能显著降低试验门槛。
18. 常见误区¶
18.1 “LoRA 就是蒸馏”¶
不是。
蒸馏强调的是:
- 用一个教师模型指导学生模型
LoRA 强调的是:
- 在同一个底座模型上,用低秩适配器完成参数高效微调
两者解决的问题不同。
18.2 “做了 LoRA,就等于不需要高质量数据”¶
也不是。
LoRA 只是降低参数更新成本,并不会自动提高数据质量。
如果训练数据本身:
- 风格混乱
- 标注不一致
- 任务边界不清
那 LoRA 一样会学歪。
18.3 “rank 越大效果一定越好”¶
不成立。
更大的 rank 确实增加表达能力,但也带来:
- 更多参数
- 更多显存
- 更大过拟合风险
它是容量旋钮,不是无脑拉满的指标。
18.4 “QLoRA 一定和 LoRA 一样稳”¶
也不能这么说。
QLoRA 的资源效率更高,但因为引入了量化,工程复杂度通常也更高。你需要在:
- 显存预算
- 训练稳定性
- 工具链兼容性
之间做平衡。
19. 面试问题¶
19.1 为什么 LoRA 能省显存¶
因为它冻结大部分底座参数,只训练少量低秩适配器参数,因此可训练参数、梯度和优化器状态都显著减少。
19.2 LoRA 和全量微调的核心区别是什么¶
全量微调直接更新原模型全部或大部分参数;LoRA 保留原始权重不动,只学习一个低秩增量并叠加到指定线性层上。
19.3 QLoRA 比 LoRA 多做了什么¶
QLoRA 在 LoRA 基础上把冻结底座模型量化到更低比特数,从而进一步降低底座加载时的显存占用。
19.4 LoRA 的 rank 是什么¶
rank 是低秩分解的中间维度,决定适配器的表达容量。rank 越大,参数更多、容量更强,但成本和过拟合风险也会上升。
19.5 adapter 为什么可以单独保存¶
因为 LoRA 学到的是相对底座权重的增量,而底座本身没有被改动,所以每个任务只需要单独保存这组增量参数。
19.6 为什么做 LoRA 仍然需要关心第 12 章的数据模板¶
因为 LoRA 只是改变“怎么训练参数”,并没有改变“模型到底在学什么输入输出分布”。如果 chat template、labels 或 loss mask 出错,LoRA 一样会学坏。
20. 小结¶
这一章最重要的结论可以压缩成四句话:
- 第 12 章解决的是“怎样做 SFT”,这一章解决的是“怎样更省资源地做 SFT”
- LoRA 的核心是冻结底座,只训练低秩增量
- QLoRA 的核心是在 LoRA 基础上,把冻结底座进一步量化来省显存
- PEFT 改变的主要是参数更新方式,不改变第 12 章那套数据、模板和训练目标的基本逻辑
如果说第 12 章让我们第一次具备了“把模型训练成助手”的能力,那么这一章真正带来的,是把这件事从“理论上能做”推进到“预算有限时也能做”。
下一章我们继续往前走。
当模型已经会按指令回答,而且我们也知道怎样低成本地训练它后,新的问题就会变成:
两条回答都看起来合理时,模型到底该更偏向哪一种?
这就是偏好对齐要处理的问题。