土法炼钢兴趣小组的算法知识备份

【Transformer 与注意力机制】31|微调演进:从全参数到 LoRA

文章导航

分类入口
transformer
标签入口
#transformer#fine-tuning#lora#peft#qlora

目录

如果你只听说过 LoRA 这个词,却从来没有亲手算过它省下了多少参数、为什么能省、为什么省了之后效果还能保持,那么这一篇就是为你准备的。

预训练的范式给了我们一个让人着迷又让人头疼的东西:一个几十亿到几千亿参数的「通用大脑」。它确实通用,但每一个具体任务——做客服、做代码补全、做医疗问答、做情感分类——都需要让这个大脑「再学一点点东西」。最直接的办法就是把所有参数都更新一遍,这个过程叫做全参数微调(full fine-tuning)。听起来很自然,做起来痛不欲生:每一个下游任务你都得保存一份和原模型一样大的副本,训练时显存爆炸,部署时存储爆炸,多任务切换时带宽爆炸。

人们花了几年时间寻找替代方案,从 Adapter 到 Prefix Tuning,从 Prompt Tuning 到最后定型的 LoRA(Low-Rank Adaptation),再到 QLoRA、DoRA。这是一段非常有趣的演化史,每一步都对应着一个具体的痛点被一个具体的洞察破解。这一篇把这条线串起来。读完之后你应该能做到:

本篇假设你已经读过 27|训练流程30|预训练,知道一个标准 Transformer 的反向传播大致长什么样。


一、从「再训一遍」开始的故事

1.1 BERT 时代的微调心智模型

回到 2018 年。BERT 刚刚发布,它给所有 NLP 工程师写下一个新的 mental model:

预训练一次,处处微调。

那时候的标准操作流程,简单得几乎像一句口号:拿到 BERT-base 的权重(约 110M 参数);在它后面接一个特定任务的小头(分类头、span 头、token 标签头);把整个模型——所有 110M 参数加上小头——一起 fine-tune 几个 epoch。

110M 在那时候是个相当大的数字,但还在「单卡能跑」的范围里。一个工程师在自己的工位上有一张 V100 32G,下载 BERT-base,五分钟跑起来,半小时迭代一次,这是非常舒服的体验。

那时候没人觉得「全参数微调」有什么问题。它是默认选项,是唯一选项,也是最自然的选项。预训练学到的是什么?是一个「初始化」。微调做的是什么?是「从这个初始化出发,继续做梯度下降」。

唯一的小麻烦是:每个任务你得存一份完整的 110M 模型。十个任务存十份,1.1 GB。还能忍。

1.2 GPT-3 时代的破坏

然后是 2020 年的 GPT-3。175B 参数。

把这个数字放进上面的心智模型,整个故事就垮了:

如果一个公司有 100 个内部任务,意味着 35 TB 的存储仅仅用来放权重——还不算优化器、checkpoint。这远远超出了一个内部基础设施团队愿意承担的量级。

那一段时间业界讨论的不是「怎么微调 GPT-3」,而是「微调 GPT-3 是否还是个好主意」。OpenAI 自己给的答案是:把微调当成 API,不要让用户碰底层模型。但这个答案没有解决一个更基本的问题:每一个用户的微调,在 OpenAI 后台都是一份独立的 175B 副本吗?显然不可能。后端工程一定做了某种参数高效(parameter-efficient)的方案。

业界从这里开始系统性寻找替代方案,得到了一个集合的名字:参数高效微调(Parameter-Efficient Fine-Tuning, PEFT)。

full-vs-lora

1.3 PEFT 想解决的具体问题

把痛点列清楚,目标就清楚了。PEFT 想同时满足下面四件事:

第一,显存友好:单卡能训。理想情况下不需要分布式训练,甚至能在消费级显卡上跑。

第二,存储友好:每个新任务只新增「很小一份」参数,base 模型可以共享。

第三,部署友好:多任务可以热切换。最好能做到一个 base 模型 + N 份小适配器,运行时按请求选择。

第四,效果不能差:在大多数下游任务上,要逼近全参数微调的效果——否则一切的省都没有意义。

注意这四个目标里没有「训练快」。PEFT 的训练时间通常不会比全参数微调短多少,因为前向反向都得过 base 模型;省的是显存、存储、和你切换任务时的成本。这是个常被新手误解的点:人们看到「只训练 0.1% 参数」就以为训练速度也快 1000 倍。其实差别可能只有 1.2 倍——优化器更新这一步省了,梯度算一半省了,但前向反向占的时间没省。

下面我们按时间顺序看这条演化线。


二、Adapter:在每层中间塞一个小模块

2.1 Houlsby 2019 的提案

最早把「不动 base,只加新模块」这条路认真走通的,是 Houlsby 等人 2019 年的论文 Parameter-Efficient Transfer Learning for NLP,提出了 Adapter 这个名字。

他们的做法非常直接:在每个 Transformer 层的两个子层(self-attention 和 FFN)之后,分别插入一个小的瓶颈结构(bottleneck),然后冻结所有原始参数,只训练这些新加进去的模块。

一个 Adapter 模块的内部结构非常简单:

y = x + W_up · ReLU(W_down · x)

它先把 x(维度 d,比如 768)通过 W_down 压到一个低维 m(比如 64),过一个非线性,再通过 W_up 升回 d,最后加一个残差连接回 x。一对 Adapter 的参数量是 2 · d · m,相比一层 Transformer 的 12d² 量级,是 m / (6d) 的比例——当 m=64、d=768 时,约占一层参数的 1.4%。

每层两个 Adapter,整个模型加起来的新增参数大约是原模型的 3% 左右。把这个数字记住,待会儿和 LoRA 比。

2.2 Adapter 的两个工程缺陷

Adapter 看起来漂亮,论文效果也不错,BERT 上多个 GLUE 任务接近全参数微调。但工程上它有两个不容易绕开的问题。

第一个问题是推理延迟。Adapter 模块是串行插入到主路径上的——你不能跳过它,因为前向必须穿过。即使它的参数量只占 1.4%,它带来的算子调用、显存读写、kernel launch 开销在小 batch 推理时是看得见的。在生产环境给一个延迟敏感的请求服务,多 5% 到 10% 的尾延迟就足以让 SRE 把这个方案否掉。这个缺陷在训练时不显著,因为训练以吞吐为主;但部署到在线服务上,问题就尖锐了。

第二个问题是结构侵入性。Adapter 改变了 Transformer 的前向图。这意味着所有依赖原始权重做推理的工具——量化、编译、特殊 kernel——都得重新适配。tensor parallel 的切分方式得重新设计;FlashAttention 的应用范围因为多了非 attention 的小算子而被拆碎;llama.cpp 这种 C++ 推理引擎要专门加 Adapter 支持。这是个长尾的痛点:它不会在论文里出现,但每个把模型推到生产的人都会遇到。

后来 Adapter 衍生出一系列改进版本,例如 Pfeiffer 2020 提出的 AdapterFusion(多个 Adapter 组合)、Compacter(用低秩张量代替 W_down/W_up)、Parallel Adapter(把 Adapter 与主路径并联而不是串联)。这些方案都有各自的论文和适用场景,但都没能成为业界默认。原因之一就是上面的两个工程痛点没有彻底解决。

2.3 一个值得记住的细节:冻结的不只是权重

Adapter 论文里有一个被很多人忽略的细节:除了冻结原始权重,Houlsby 还冻结了 LayerNorm 的参数。这看起来是个小细节,但实际效果很不一样。LayerNorm 的 γ 和 β 是分布敏感的——它们决定了每一层激活的「形状」。一旦你解冻它们,模型就有了一条非常便宜的「走捷径」通道:通过调整 γ 和 β,模型可以在不动权重的情况下,整体放大或抑制某些维度的激活。

后续工作 BitFit(Zaken et al. 2021)干脆走到极端:只训练所有 bias 参数,冻结其他一切。在 BERT 上居然也能在很多任务上接近全参数微调。这个发现暗示了一件后来 LoRA 论文重申的事:预训练之后的微调所引入的「变化」,可能远比我们想象的要小。这个直觉是 LoRA 的种子。


三、Prefix Tuning:从权重转向 KV

3.1 Li & Liang 2021 的另一条路

Adapter 选择「往主路径里塞模块」,Prefix Tuning(Li & Liang 2021)选择了完全不同的一条路:不动权重,往输入侧塞东西

他们的观察是这样的:在 Transformer 里,影响一层输出的不只是权重 W_q、W_k、W_v,还有「上下文」——也就是这一层每个 token 看到的 K 和 V 集合。如果我能在每一层的 K 和 V 前面,拼接上一组「可学习的 prefix」,那就相当于给整个模型注入了一个「永久的、可训练的上下文」,而原始权重一点都不用动。

具体做法是:对每一层,定义两个可学习矩阵 P_k 和 P_v,形状都是 (l, d),其中 l 是 prefix 长度(比如 20),d 是 hidden size。在做 attention 时,把这两个 prefix 拼到原本的 K、V 前面:

K' = concat([P_k, K_orig], axis=0)   # 形状 (l + n, d)
V' = concat([P_v, V_orig], axis=0)   # 形状 (l + n, d)

Q 不动。Attention 的计算方式不变,只是现在每个 token 在「看回去」的时候,会先看到这 l 个虚拟的 prefix token,再看到真实的历史。

参数量呢?每层 2·l·d,整个模型 2·L·l·d。当 L=24、l=20、d=1024 时,大约 1M 参数——比 Adapter 还小一个数量级。

3.2 它的优势与代价

Prefix Tuning 有一些非常诱人的性质:

不动主路径。原始权重一字不改,所以推理引擎、kernel、量化都不需要适配。Adapter 的两个工程痛点中的第二个被绕开了。

作用在 KV 上,是 attention 的「上下文」。从语义上很好理解:你给每一层提供了一个「任务相关的 prompt」,而且这个 prompt 是连续的、可微的,不像离散的人工 prompt 那样要靠运气找。

它的任务切换成本极低:只要换一组 P_k 和 P_v 就行,base 模型完全共享。

但它也有自己的痛点:

第一,长度受限。l 不能太大,否则推理时序列长度膨胀,FlashAttention 的好处会被吃掉一部分;KV cache 也会变大。

第二,优化困难。原论文报告训练 prefix 比训练 Adapter 更敏感,需要重参数化(reparameterization)——也就是先用一个 MLP 把 prefix 从一个低维向量映射出来,训练完之后丢掉 MLP,只保留 prefix。这个 trick 增加了实现复杂度,也增加了出错的机会。

第三,只在 KV 上加 prefix 是个有点武断的设计选择。后来的 P-Tuning v2(Liu et al. 2022)做了同样的事但在更多层、更多位置上加 prefix,效果有提升;UniPELT 把多种方法组合起来。Prefix Tuning 自己作为一个独立方案,最终没有像 LoRA 那样成为业界默认。

3.3 Prompt Tuning:把 prefix 退化到只在输入层

如果 prefix 是「每一层」的可学 token,那能不能只在「输入层」加?这就是 Prompt Tuning(Lester et al. 2021)。

Prompt Tuning 的逻辑非常简洁:在 input embedding 前面拼接一组可学习的 soft prompt(也是 (l, d) 的矩阵),然后用 base 模型正常前向,loss 只反传到这个 prompt 上。所有 Transformer 层的权重都不动。

参数量更小了:2·l·d 在每层都加,变成只在输入层加,相当于削掉了 L 倍的参数,单 prompt 通常只有几万到十几万参数。

但 Prompt Tuning 的代价也变得更大:模型容量变小,对 base 模型规模高度依赖。Lester 的论文里有一张关键的图:在 BERT 这种百兆参数的模型上,Prompt Tuning 显著弱于全参数微调;但当模型规模到 10B 以上时,差距迅速收敛——原因是大模型的「内部已经学到」的知识足够多,一个小小的 prompt 就足以唤起合适的行为。

这个观察后来在 GPT-3 的 in-context learning 研究里被反复重述:模型够大,小信号够用。

peft-comparison

到这里为止,PEFT 这条线已经走了三步:Adapter(在主路径插模块)、Prefix Tuning(在每层 KV 插 prefix)、Prompt Tuning(在输入层插 prompt)。每一步都在「更省」的方向上推进,但都没有解决「主路径推理延迟」与「效果稳定性」这两件事。LoRA 的关键就在于它不在主路径上插任何东西,又能在效果上稳定追平全参数。


四、LoRA:低秩才是答案

4.1 Hu et al. 2021 的核心想法

LoRA 论文(Hu et al., LoRA: Low-Rank Adaptation of Large Language Models, ICLR 2022)的标题就把答案写在了脸上:低秩分解。

他们的观察可以浓缩成一句话:

在微调过程中,权重 W 的「变化」 ΔW 是低秩的——它的有效秩远小于 W 自己的秩。

这句话听起来像直觉断言,但它有实证支持:作者跑了一系列实验,把全参数微调得到的 ΔW 拿出来做 SVD,发现它的奇异值衰减极快,前若干个奇异值占了绝大部分能量。换句话说,ΔW 几乎可以被一个低秩矩阵良好近似。

如果这个观察成立,那么我们可以预先就把 ΔW 限制成低秩的,让训练直接在低秩空间里进行:

W' = W + ΔW = W + B · A

其中 B ∈ ℝ^{d×r},A ∈ ℝ^{r×d},r ≪ d。原始 W 完全冻结,训练只更新 B 和 A。

参数量:

当 d=4096,r=8 时,全参数是 1677 万,LoRA 是 6.5 万——压缩比 256:1。

这就是为什么大家说 LoRA「只训练 0.1% 参数」。这个 0.1% 不是吹的,是一个具体的算式。

lora-decomposition

4.2 公式背后的几何意义

把 W + B·A 这个分解放到几何里看,会更直观。

你可以把 W 想象成一个「映射」,从输入空间 ℝ^d 到输出空间 ℝ^d。预训练把这个映射学得足够好:它已经能干各种各样的事。

微调要做什么?要在这个映射上叠加一个微小的修正,让它在某个具体任务上表现得更好。这个修正就是 ΔW。

LoRA 假设:这个修正不需要在所有方向上动作,只需要在「少数几个方向」上动作。具体来说,A 把输入向量投影到一个 r 维子空间(这是「在哪些方向上感兴趣」),B 把这个 r 维向量再投回 d 维空间(这是「在哪些方向上做修正」)。整个 ΔW = B·A 是一个秩最多为 r 的矩阵——它的列空间是 B 撑起来的 r 维子空间,行空间是 A 的转置撑起来的 r 维子空间。

为什么微调的 ΔW 真的低秩?没有严格证明,但有一个直观的解释:预训练已经覆盖了几乎所有「通用语义方向」,微调只是在其中挑出和当前任务相关的那少数几个方向加强或抑制。所以微调引入的变化天然就是「稀疏的、定向的」,对应到矩阵世界就是「低秩的」。

这个直觉和我们前面讲到的 BitFit 是一脉相承的:微调引入的变化远比模型本身要简单。

4.3 初始化的小心机

LoRA 的初始化方式是个细节,但值得专门讲一下。

A 用 Kaiming 初始化(高斯分布,方差按维度归一化),B 用零初始化。

为什么 B 是零?因为这样训练开始的瞬间,B·A = 0,整个 LoRA 旁路对前向输出没有任何影响——和「base 模型直接前向」完全等价。

这是非常关键的稳定性保证。如果 B·A 在 step 0 就有非零贡献,就会扰动一个已经训得很好的 base 模型的输出,给训练引入一个不必要的「需要先恢复回去」的过程。零初始化 B 让训练从一个干净的起点出发,每一步梯度都只在「需要的方向上」走。

代码层面这通常长这样:

self.A = nn.Parameter(torch.randn(r, d) * (1 / math.sqrt(r)))
self.B = nn.Parameter(torch.zeros(d, r))

注意 A 的方差也不是任意取的——它和 LoRA 的 alpha(下面讲)一起决定了 LoRA 旁路的「学习率有效尺度」。这是一个常见的踩坑点。

4.4 alpha 与 r:经常被搞错的关系

LoRA 的实现里有一个看起来很神秘的超参数 alpha(α)。前向计算实际上是这样:

W' = W + (α / r) · B · A

注意那个 α / r 的缩放因子。它的作用是:让 LoRA 旁路的「有效贡献」与 r 解耦。

为什么需要这个?想象你在 r=8 上调参调到 alpha=16,得到一个不错的结果。后来你想试试 r=64 看会不会更好。如果没有 α/r 这个缩放,r 变成 64 之后,B·A 的「数值幅度」会大致同步变大,相当于你不仅改了秩,还顺手把学习率放大了。结果就是训练动力学完全不同,很难做横向比较。

加上 α/r 之后,调参的逻辑变得更清楚:

社区里流传一个经验法则:把 α 设成 r 的两倍。也就是 r=8 → α=16,r=16 → α=32。这个法则不是出自论文,而是从大量实践中归纳的,背景是:「要让 LoRA 旁路的初始尺度大致和原始权重的更新步长可比」。它是个起点,不是终点;具体任务上 α=r、α=2r、α=4r 都有人在用。

但下面这个错误是真的常见,要单独说一下:

「我把 r 调成 64,但 alpha 没改还是 16。结果效果反而比 r=8、alpha=16 差。」

原因正是 α/r 这个缩放:r 翻 8 倍但 alpha 没动,相当于学习率变成了原来的 1/8,模型还没学到东西就停了。如果要扩大 r,至少应该按比例同步扩大 α。

4.5 为什么只在 W_q 和 W_v 上加 LoRA

读 LoRA 原论文你会发现一个有意思的实验:作者尝试了把 LoRA 加在不同位置(W_q、W_k、W_v、W_o、FFN 的两个矩阵),最后给出的推荐是:只在 W_q 和 W_v 上加。

为什么?两个原因。

第一,预算分配的考虑。给定一个固定的可训练参数预算,作者发现把它平铺到「更多的矩阵但每个 r 更小」往往不如「集中在少数几个矩阵但每个 r 更大」。在这个权衡下,W_q 和 W_v 是最划算的两个。

第二,经验上的差异。W_q 决定模型「怎么看输入」,W_v 决定「输出什么内容」,这两个对应的语义最贴近「微调要调整的事情」;W_k 主要参与「相似度计算」,似乎对它的调整收益较小;W_o 是 attention 输出的线性投影,作用更像 feature mixing。

不过这个推荐不是绝对的。LLaMA 时代之后很多 LoRA 实现默认在所有线性层(W_q、W_k、W_v、W_o,以及 FFN 的 W_gate、W_up、W_down)都加 LoRA,r 设小一些(比如 8 或 16)。社区的 LoRA fine-tuning 工具(PEFT、unsloth 等)通常给一个 target_modules 参数让用户自己决定。一个稳妥的起点是:聊天助手、指令跟随这种「行为类」任务,全部线性层都加;分类、抽取这种「判别类」任务,只在 q、v 加就够了。

4.6 推理时的合并

LoRA 还有一个 Adapter 永远做不到的工程优势:推理时可以把 ΔW 合并回 W

推理路径:

W_merged = W + (α / r) · B · A

这一步是个 d×r 矩阵乘 r×d 矩阵再加到 d×d 矩阵上的操作,在 GPU 上几毫秒就做完。合并完之后,整个模型回到「一个标准的 Transformer」的形状,所有原本针对 base 模型的优化(量化、编译、tensor parallel)都可以原封不动地用。

这是 LoRA 比 Adapter 更受工业界欢迎的最直接原因:

但合并也有代价:合并后这个 LoRA 就和这个 base 绑定了,没法再热切换。如果你的部署场景需要「一个 base 服务多个任务」,那么不要合并;如果是「这个版本只服务一个任务」,那么合并掉是最简单的部署。


五、QLoRA:让单卡能微调 65B

5.1 显存账:4-bit 量化的诱惑

LoRA 解决了「训练参数量」的问题,但没有解决「base 模型本身要放在显存里」的问题

举一个具体的账:你想用 LoRA 微调 LLaMA 65B。LoRA 适配器的参数量也许只有 100M,可以忽略。但 LLaMA 65B 的 fp16 权重就是 130 GB——这一份得整个加载到显存里、参与前向反向。如果是 fp32,260 GB。然后你还得加上激活、KV cache、梯度(虽然只对 LoRA 算,但中间激活仍要保存)、优化器状态。即使做了所有能做的优化,65B 的 LoRA 也至少要 4 张 A100 80G。

QLoRA(Dettmers et al. 2023, QLoRA: Efficient Finetuning of Quantized LLMs)问的是:能不能把 base 量化成 4-bit 放着,只在反向时即时反量化?

如果可以,65B 的 base 就从 130 GB 缩到 33 GB 左右——单张 48 GB 显卡就能装下。论文报告他们成功在单张 48 GB GPU 上微调了 65B 的 Guanaco 模型,并接近 ChatGPT 的对话质量(ChatGPT 在那个时间点的水平)。

qlora-flow

5.2 NF4:为什么不是普通的 INT4

QLoRA 的第一个技术贡献是 NF4(NormalFloat 4-bit)量化。

为什么不能直接用 INT4?因为神经网络的权重分布不是均匀的——它们更接近一个零均值高斯分布。如果你把 [-1, 1] 区间均匀切成 16 段,那么靠近零的权重(占绝大多数)会被几个粗糙的桶覆盖,丢失大量精度;而远离零的权重(很少)会占用大量桶,浪费分辨率。

NF4 的做法是:预先假设权重是标准正态分布,然后选择 16 个量化值,让它们对应于这个正态分布的等密度分位点。这样落在概率密度高的区域分辨率就高,落在尾部的区域分辨率就低。和权重的真实分布对齐之后,4-bit 的精度损失被压到最小。

工程上 NF4 的查表是固定的(一个 16 元素的常数表),实现起来非常简单:每 4 个比特对应一个查表索引。每 64 个权重共享一个 fp32 的尺度因子(block-wise quantization),把一个 block 里的最大绝对值归一化到 1,再做 NF4。整个 7B 模型量化下来约 4 GB。

5.3 Double Quantization 与 Paged Optimizer

QLoRA 还做了两件锦上添花的事:

Double Quantization(双重量化):注意上面提到的尺度因子——每 64 个权重一个 fp32 标量,65B 模型下来这部分本身就有几百 MB。Dettmers 把这些尺度因子量化成 8-bit,再为这些 8-bit 量化提供一个二级尺度因子(每 256 个标量一个 fp32)。这一步又把整体显存占用降低了约 0.5 GB,对训 65B 来说是看得见的差别。

Paged Optimizer:训练时 Adam 的状态(m、v)是显存大头,而且不是每一步都需要全部访问。QLoRA 用 NVIDIA 的统一内存(unified memory)机制,把优化器状态分页到 CPU 主存,需要时按页调进显存——类似操作系统的虚拟内存。这避免了 OOM 在小显存机器上的频繁出现,代价是每个 batch 慢一点点。

这三个技术加起来,让原本需要 8 张 A100 80G 的 65B 微调,缩成了一张 48 GB 卡的工作。这是 QLoRA 论文最实质性的工程贡献。

5.4 反向传播怎么走

一个常见的疑惑:base 是 4-bit 量化的、冻结的,反向传播怎么穿过它?

答案是:反向传播穿过去但不更新它

具体过程:

  1. 前向时,把 4-bit 的 W_q 反量化到 fp16(在 CUDA kernel 里即时做),得到 W;
  2. 计算 W·x,把这个中间结果用于后续的 BA·x(LoRA 旁路);
  3. 反向时,按链式法则,梯度会一路反传回 W 的层。但因为 W 是冻结的,这个梯度只用来计算「上一层激活的梯度」,不用来更新 W;
  4. LoRA 旁路 BA 的梯度被计算出来,这个梯度只对 B、A 有,更新它们。

W 在每一步都重新「即时反量化」,永远不会以 fp16 形式持久保存。这就是为什么显存占用主要由量化的 4-bit 权重决定。

5.5 QLoRA 与 LoRA 的效果差距

一个直接的问题:4-bit 量化会不会让效果变差?

QLoRA 论文做了相当系统的对比,结论简而言之是:在 LLaMA-65B 这一档上,QLoRA 与 fp16 LoRA 的效果几乎不可分辨。在小模型(7B、13B)上,差距也很小。

但这件事有一个隐含前提:只在前向时即时反量化,权重的训练目标空间仍是 fp16。如果你试图直接在 4-bit 空间里做训练(更新 4-bit 权重),那是另一回事,几乎肯定会显著变差。QLoRA 的精髓是「冻结的部分量化,可训练的部分保留精度」——这个分工才是它的关键。


六、DoRA:方向与幅值的分离

6.1 LoRA 的一个隐藏假设

到 2024 年,LoRA 已经是事实上的微调标准了。但人们继续在挖它的局限。

一个观察是这样的:LoRA 把 ΔW 整体拟合成一个低秩矩阵。但「修改一个权重矩阵」其实有两种动作——改方向(让某些列指向不同的位置)和改幅值(让某些列变长或变短)。LoRA 把这两件事混在一起拟合。

如果让模型分开拟合这两件事,会不会更好?

DoRA(Weight-Decomposed Low-Rank Adaptation, Liu et al. 2024)回答了这个问题。它的做法是:

W = m · (V / ||V||)

把每个权重列分解成「幅值 m(标量)」和「方向 V/||V||(单位向量)」两部分。然后微调时:

结果是:在相同 r 下,DoRA 在多个基准上比 LoRA 略好,尤其是在「需要较大改动的任务」上差距更明显。

6.2 DoRA 是 LoRA 的一个升级,但不是替代

DoRA 不是另一种 PEFT 方法,而是 LoRA 的一个改进。它的实现兼容 LoRA 生态的大部分工具(PEFT 库已经支持)。代价是:

实践上,DoRA 在「LoRA 已经够用」的场景里收益不大,但在「LoRA 调到极限了还想再榨出一点」的场景里值得试。它和 LoRA 的关系,类似于 RMSNorm 和 LayerNorm——一个微妙但有效的精炼。


七、PEFT 与全参数微调的效果差距

7.1 一个反复出现的问题

每次有人在群里问「LoRA 能不能完全替代全参数微调」,下面总会出现两派:

A 派:能,效果几乎没差距,全参数微调是历史。

B 派:不能,特定任务上差距明显,关键场合还是要全参数。

两派都有道理,因为他们说的是不同的场景。

7.2 LoRA 接近全参数的场景

下面这些场景里,LoRA 通常能逼近全参数微调:

指令微调(SFT)。这是 LoRA 最擅长的领域。原因是 SFT 学的是「输出风格、格式、跟随指令的习惯」,这些是相对浅层的能力,主要靠分布的小调整就能学到,正好符合「ΔW 低秩」的假设。LLaMA-2、Qwen、Mistral 等等开源对话模型有大量在 LoRA 上做 SFT 的实践,效果几乎和全参数一致。

风格迁移。让模型说话像鲁迅、像周星驰、像小红书写手——这些都是高层风格的注入,本质是浅层的、定向的修改,LoRA 表现极好。

领域适配(domain adaptation)的轻量版。让模型对某个领域的术语、表达方式更熟悉,但没有引入大量新知识——LoRA 够用。

7.3 LoRA 显著弱于全参数的场景

下面这些场景里,LoRA 通常会输给全参数:

注入大量新知识。比如让 base 模型学会一个全新的语言(base 几乎没见过的语言),或者学一大批最新的事实。这些场景需要「在权重的很多方向上都做修改」,低秩假设不成立,LoRA 容量不够。这个时候要么继续做预训练(continued pretraining)做全参数,要么把 r 拉得很大(r=128 或 256)来近似全参数。

复杂的能力学习。比如教模型一种它原本完全不会的新技能(不是风格的调整,而是新算法)。这种学习往往对所有层都有需求,LoRA 的参数预算分配会不够用。

评估极度敏感的场景。在某些 benchmark 上(特别是数学、代码这种「正确率有阶梯」的任务),LoRA 与全参数的几个百分点差距是真实存在的,会被排行榜放大成「明显差距」。

7.4 实际策略

业界主流的做法已经很清晰:


八、灾难性遗忘与多任务部署

8.1 灾难性遗忘是怎么回事

任何形式的微调——全参数也好、LoRA 也好——都面临一个反复出现的问题:灾难性遗忘(catastrophic forgetting)。

简而言之,模型在新任务上训得越好,在原本擅长的任务上往往就越差。这件事在小模型上几乎是绝对的,在大模型上稍微缓和但仍然存在。

为什么?因为微调的目标函数只看新任务的 loss。模型为了把新任务做对,会调整一切能调整的权重,哪怕这些权重原本承载着旧任务相关的知识。一旦旧任务的训练信号不再出现,那些「为了旧任务而存在」的权重模式就会被新数据冲掉。

LoRA 在这件事上有个有趣的天然优势:因为 ΔW 是低秩的、可控的,对原模型的扰动相对受限。实证上 LoRA 微调后的模型,在「与微调任务无关」的基准上通常掉得比全参数少。这并不意味着 LoRA 完全免疫遗忘——只是它的「破坏力」天然更小。

8.2 多 Adapter 热切换

LoRA 真正的「杀手级特性」之一,是它的 多 Adapter 部署能力。

设想一个场景:你有一个 base 模型,要服务多个客户,每个客户都有自己的专属 LoRA。如果是全参数微调,你要为每个客户复制一份完整模型,存储和显存爆炸。如果是 LoRA,你只需要:

进一步还可以做批内多 LoRA——同一个 batch 里不同的请求挂不同的 LoRA。开源项目 S-LoRA、LoRAX 等就在这个方向上做工程。这种部署方式让一台机器同时服务几十个甚至上百个微调任务成为可能,这是 PEFT 经济模型最有杀伤力的一点。

8.3 LoRA 的「负副作用」

但 LoRA 不是完全没有代价的。社区里被反复提及的两个负面观察:

LoRA 容易过拟合。因为可训练参数少、容量有限,对小数据集来说,LoRA 反而更容易「死记硬背」。这在 r 设得过大时尤其明显——容量上来了,但数据量没跟上。

LoRA 在 long tail 知识上不可靠。如果你的微调数据里某个类别只出现了几次,LoRA 学到的可能只是「这几个具体样本」,而不是「这个类别的一般规律」。全参数微调在这件事上稍好,因为它有更大的容量和更分散的更新机会。

这两点提醒我们:选择 LoRA 不是免费午餐,而是一个权衡。


九、与 RLHF 的关系:为下一篇做铺垫

下一篇(33|RLHF)会讲对齐训练。这里先把它和微调的关系讲清楚,避免概念混淆。

现代大模型的标准训练流水线是:

Pretrain → SFT → RLHF/DPO

其中:

Pretrain(预训练):在海量无标注语料上做下一个 token 预测。这是模型「能力」的来源。

SFT(Supervised Fine-Tuning,监督微调):在高质量的「指令-回答」对上做监督学习。这是模型「会跟从指令」的来源。下一篇 32 会专门讲 SFT。

RLHF(Reinforcement Learning from Human Feedback)或 DPO(Direct Preference Optimization):在人类偏好数据上做对齐训练。这是模型「回答符合人类偏好」的来源。33 篇专门讲。

LoRA 在这条流水线里可以用在 SFT 阶段(通常用),也可以用在 RLHF 阶段(PPO 时 policy 用 LoRA、reward model 用全参数是常见配置)。LoRA 的价值在每个阶段都在,不只是 SFT。

但要注意一点:Pretrain 阶段几乎没有人用 LoRA。预训练学的是「通用能力」,需要在权重的所有方向上做大幅修改,低秩假设根本不成立。预训练只能是全参数。


十、动手验证一个最小例子

下面这段代码用 PyTorch 手写一个 LoRA 层,跑一个最小的玩具实验,让你眼见为实。

import torch
import torch.nn as nn
import math

class LoRALinear(nn.Module):
    def __init__(self, in_features, out_features, r=8, alpha=16):
        super().__init__()
        self.in_features = in_features
        self.out_features = out_features
        self.r = r
        self.alpha = alpha
        self.scaling = alpha / r

        # 冻结的 base 权重
        self.weight = nn.Parameter(
            torch.randn(out_features, in_features) * (1 / math.sqrt(in_features)),
            requires_grad=False,
        )
        # 可训练的 LoRA 旁路
        self.lora_A = nn.Parameter(torch.randn(r, in_features) * (1 / math.sqrt(r)))
        self.lora_B = nn.Parameter(torch.zeros(out_features, r))

    def forward(self, x):
        base = x @ self.weight.T
        lora = (x @ self.lora_A.T) @ self.lora_B.T
        return base + self.scaling * lora


# 玩具任务:拟合 y = W_target · x
torch.manual_seed(42)
d = 64
W_target = torch.randn(d, d)

layer = LoRALinear(d, d, r=4, alpha=8)

# 把 base 设成 W_target 的「噪声扰动」,模拟「预训练已经做了 90% 的事」
with torch.no_grad():
    layer.weight.copy_(W_target + 0.1 * torch.randn(d, d))

opt = torch.optim.Adam([layer.lora_A, layer.lora_B], lr=1e-3)

for step in range(2000):
    x = torch.randn(32, d)
    y = x @ W_target.T
    pred = layer(x)
    loss = ((pred - y) ** 2).mean()
    opt.zero_grad()
    loss.backward()
    opt.step()
    if step % 200 == 0:
        print(f"step {step}: loss = {loss.item():.6f}")

# 检查:合并后的权重 vs 目标
W_merged = layer.weight + layer.scaling * layer.lora_B @ layer.lora_A
print("误差:", (W_merged - W_target).norm().item())

跑这段代码你会看到几件事:

第一,loss 会从一个不大不小的值降到很低。LoRA 的 r=4,但 ΔW = W_target − W_base 是一个秩接近 d 的「噪声扰动」——理论上 r=4 的 LoRA 拟合不了。但因为扰动幅度小,前 4 个奇异值占了绝大部分能量,r=4 的近似已经够好。

第二,把 r 调成 1,loss 收敛会变慢、最终值会高一些;调成 8 或 16,几乎没有进一步提升——这就是「ΔW 的有效秩」直观上的样子。

第三,把 lora_B 的初始化从 zeros 改成 randn,前几步的 loss 会先飙升再回落——这就是「破坏 W 干净起点」的代价。

这是一个非常小的实验,但它把这一篇里几乎所有抽象概念落到具体数字上,建议自己跑一次。


十一、一些工程细节里的坑

把 LoRA 推到生产时会遇到一些没出现在论文里的小坑,这里集中列一下,免得读者重复踩。

Tokenizer 不一致。一个微调好的 LoRA 假设 base 模型的 tokenizer 不变。如果你换了 tokenizer 或者扩了词表(比如给中文加 token),LoRA 立刻失效——因为 embedding 层不一样了。这种情况要么连 embedding 一起做 LoRA(target_modules 包括 embed_tokens),要么就得重训。

精度溢出。在 fp16 训练 LoRA 时偶尔会遇到 NaN,原因是 LoRA 旁路的中间激活在某些极端 batch 下会过大。简单的修复是把 LoRA 计算放到 fp32:先把输入 cast 成 fp32 算 BA,再 cast 回 fp16 加到 base 输出上。这样多花一点点显存换稳定。

合并后的精度损失。LoRA 训练时是 fp16/bf16,合并到 base 可能有数值漂移。如果 base 是量化的(比如 INT8),合并 LoRA 的过程不能在量化空间做,要先反量化、合并、再量化——这一步会引入额外误差。生产部署上常见的做法是:训练时用 QLoRA、推理时不合并(直接走 LoRA 旁路)。

多 LoRA 累加。理论上你可以同时挂多个 LoRA:W’ = W + ΔW_1 + ΔW_2。但多个 LoRA 是独立训练的,它们的方向可能冲突,加在一起的效果可能比任何单个都差。MoE-LoRA 这一类工作就是为了解决这个问题——给每个 LoRA 加一个 router,让它们按需激活而不是简单叠加。

保存格式。HuggingFace 的 PEFT 库默认只保存 LoRA 权重(不含 base),文件很小(几十 MB)。但有人不小心把整个模型 model.save_pretrained(...) 保存了,结果出来 14 GB——这种问题在 CI/CD 里特别尴尬。要确认你保存的是什么。

学习率。LoRA 的学习率通常比全参数微调大一个数量级。原因是 LoRA 的可训练参数很少,每个参数承担的责任更大;而且 base 是冻结的,不会被「带跑」。常见值:全参数 SFT 用 1e-5 到 5e-5,LoRA SFT 用 1e-4 到 5e-4。如果直接套用全参数的学习率,LoRA 训不动;如果套用 LoRA 的学习率做全参数,模型会爆炸。


十二、关键概念回顾

回到这一篇的开头,我们想讲清楚的是「为什么不再有人对 7B 模型做全参数 SFT」。现在你应该有了一个完整的故事。

预训练给了我们一个庞大的通用模型,但每个具体任务都需要让它再学一点。直接把所有参数都更新一遍——也就是全参数微调——这条路在 BERT 时代很自然,到 GPT-3 时代变得难以承受。每个任务一份 350 GB 的副本、训练时一台机器装不下、部署时多任务切换困难,这些痛点逼着业界寻找更省的微调方式。

PEFT 这条路上有几个早期尝试:Adapter 在每层中间塞小模块,效果好但推理时会引入额外延迟;Prefix Tuning 在每层 KV 前面拼可学 prefix,回避了主路径修改但训练敏感、长度受限;Prompt Tuning 把 prefix 退化到只在输入层加,参数最少但对模型规模高度依赖。这三条路都有效,但都没成为业界默认。

LoRA 给出了真正稳定的答案。它的核心洞察很短:微调引入的 ΔW 是低秩的。把 ΔW 限制成 B·A 这样一个低秩分解,参数量从 d² 降到 2rd,几百倍的压缩。初始化把 B 设成零,让训练从 base 模型的干净起点出发;缩放因子 α/r 让秩和有效学习率解耦;推理时可以把 ΔW 合并回 W,零额外延迟。这几个工程细节加在一起,让 LoRA 兼具「训练时省」与「推理时无感」两个优点,这是 Adapter 永远做不到的。

QLoRA 在 LoRA 的基础上多走了一步:把冻结的 base 模型量化到 4-bit 放着,只在前向时即时反量化。NF4 量化让权重的不均匀分布得到更好的精度分配,double quantization 进一步压缩尺度因子,paged optimizer 把 Adam 状态分页到 CPU。三件套加起来,让单张 48 GB GPU 微调 65B 模型成为可能。这是 LoRA 流派最具生产力的演进。

DoRA 把权重分解成幅值和方向,分别拟合,是 LoRA 的精炼。在「LoRA 调到极限」的场景里值得用,在「LoRA 已经够用」的场景里收益有限。

LoRA 不是免费午餐:在需要注入大量新知识、复杂能力学习、long tail 数据的场景里,它会显著弱于全参数微调。但在指令微调、风格迁移、轻量领域适配这些主流场景里,它已经是事实标准。配合多 LoRA 热切换、批内多 LoRA 这些部署方式,它让「一台机器服务多个微调任务」从想象变成现实。

最后一句:LoRA 是「微调」这件事的当前最优解,但不是终点。下一段历史很可能是「微调」与「prompt」的边界进一步模糊——更小的适配器、更动态的注入、更智能的多任务路由。这条线还在演化中。


十三、常见误解

13.1 LoRA 训练比全参数微调快得多

不准确。

LoRA 省的是显存和存储,不是训练时钟时间。前向反向都得过 base 模型,这部分开销和全参数几乎一样;省下的只有「优化器更新」那一小步。实际训练时间通常是全参数的 70%–90%,不是 1%。

如果你看到「LoRA 快 100 倍」的说法,那个数字说的是参数量,不是时间。

13.2 LoRA 的 r 越大效果越好

不一定。

r 越大,LoRA 的容量越大,但也越容易过拟合。在小数据集上,r=4 或 r=8 经常比 r=64 表现更好。论文推荐的起点是 r=8,社区 SFT 常用 r=16 或 32,超过 64 的场景不多。

而且 r 调大时如果没有同步调大 alpha,有效学习率会变小,反而训不动。

13.3 LoRA 必须只加在 W_q 和 W_v 上

不一定。

原论文的推荐是基于 GPT-2/3 时代的实验。LLaMA 时代之后,主流做法是在所有线性层(包括 FFN)都加 LoRA,r 适当调小。具体选什么 target_modules,看任务和预算。

13.4 QLoRA 是 LoRA 的精度阉割版

不是。

QLoRA 把 base 量化成 4-bit,但可训练的 LoRA 部分仍是 fp16/bf16。base 量化引入的精度损失在大模型上几乎不可见,QLoRA 与 fp16 LoRA 的最终效果在 65B 上几乎一致。

QLoRA 的价值是「让单卡能微调大模型」,不是「精度低一点的 LoRA」。

13.5 LoRA 可以代替预训练

完全不能。

预训练学的是「通用能力」,需要在权重的所有方向上做大幅更新,低秩假设根本不成立。LoRA 是为「微调」设计的——前提是已经有了一个训得很好的 base。

如果你试图在一个未训练的模型上跑 LoRA,效果会非常差——因为 base 还没学到任何东西,旁路的 BA 也无所适从。

13.6 LoRA 训练时不会遗忘原模型的能力

不准确。

LoRA 比全参数遗忘少,但不是不遗忘。如果你的 SFT 数据全是「正面情感分类」,模型很可能会变成「啥都说成正面」——原本的客观能力被冲掉一部分。这是微调本身的问题,与是不是 LoRA 无关。

防止遗忘的常见做法是混入一定比例的通用语料(pretrain mix),或者在多个任务上联合训练。

13.7 alpha 设成 r 的两倍是黄金法则

是个好起点,不是黄金法则

α=2r 是社区经验值,背景是「让 LoRA 旁路的初始尺度大致和原始权重的有效更新可比」。但具体任务、具体模型、具体学习率下,α=r、α=4r、α=8r 都有人在用,效果各有差异。要把它当成一个起点,调参时和学习率、r 一起 sweep。


十四、下一步

下一篇 32|指令微调 会专门讲 SFT 数据从哪里来、怎么构造、规模和多样性如何权衡,以及为什么「指令微调」让模型从「补全机器」变成「听话助手」。

那一篇你会看到 FLAN、T0、InstructGPT 是怎么把「自然语言指令」这件事一步步做对的。在那之后,第 33 篇讲 RLHF 与 DPO——为什么 SFT 不够,还需要再走一步对齐;第 34 篇讲 Scaling Laws 与 Chinchilla 法则,为什么大模型训练的「最优配方」在 2022 年被推翻又重写了一次;第 35 篇讲数据工程,预训练这盘大棋里最不被欣赏却最关键的那一手。

如果你想动手验证今天的内容,最快的路径是:

把这一套跑下来,你对 PEFT 这条线的全部要点就建立起来了。


十五、参考文献

下面按相关度排序,列出本篇直接引用与延伸阅读,每条附一句话提示其在本篇中的角色。

  1. Hu, E. J. et al. “LoRA: Low-Rank Adaptation of Large Language Models.” ICLR 2022 (arXiv:2106.09685, 2021). 本篇核心方法的原始论文。
  2. Dettmers, T. et al. “QLoRA: Efficient Finetuning of Quantized LLMs.” NeurIPS 2023 (arXiv:2305.14314). 4-bit 量化 + LoRA,让单卡能微调 65B 的工程突破。
  3. Houlsby, N. et al. “Parameter-Efficient Transfer Learning for NLP.” ICML 2019. Adapter 的原始论文。
  4. Li, X. L., Liang, P. “Prefix-Tuning: Optimizing Continuous Prompts for Generation.” ACL 2021. Prefix Tuning 的原始论文。
  5. Lester, B., Al-Rfou, R., Constant, N. “The Power of Scale for Parameter-Efficient Prompt Tuning.” EMNLP 2021. Prompt Tuning 的原始论文。
  6. Liu, S.-Y. et al. “DoRA: Weight-Decomposed Low-Rank Adaptation.” ICML 2024 (arXiv:2402.09353). 方向与幅值分离的 LoRA 升级。
  7. Zaken, E. B., Goldberg, Y., Ravfogel, S. “BitFit: Simple Parameter-efficient Fine-tuning for Transformer-based Masked Language-models.” ACL 2022 (arXiv:2106.10199, 2021). 只训 bias 的极端 PEFT,验证了「微调变化很小」的直觉。
  8. Pfeiffer, J. et al. “AdapterFusion: Non-Destructive Task Composition for Transfer Learning.” EACL 2021. Adapter 的多任务组合扩展。
  9. Liu, X. et al. “P-Tuning v2: Prompt Tuning Can Be Comparable to Fine-tuning Universally Across Scales and Tasks.” ACL 2022 (arXiv:2110.07602). Prefix Tuning 的多层强化版。
  10. Mangrulkar, S. et al. “PEFT: State-of-the-art Parameter-Efficient Fine-Tuning methods.” HuggingFace, 2022. PEFT 库的官方文档与实现。
  11. Sheng, Y. et al. “S-LoRA: Serving Thousands of Concurrent LoRA Adapters.” MLSys 2024 (arXiv:2311.03285). 多 LoRA 推理服务的代表工作。
  12. Frankle, J., Carbin, M. “The Lottery Ticket Hypothesis.” ICLR 2019. 与「ΔW 低秩」直觉相关的更早工作(稀疏 vs 低秩)。
  13. Aghajanyan, A. et al. “Intrinsic Dimensionality Explains the Effectiveness of Language Model Fine-Tuning.” ACL 2021. 微调可以在低维子空间中完成的实证证据,为 LoRA 提供理论铺垫。
  14. Dettmers, T. et al. “8-bit Optimizers via Block-wise Quantization.” ICLR 2022 (arXiv:2110.02861). 后来成为 QLoRA 一部分的基础工作。
  15. Touvron, H. et al. “LLaMA: Open and Efficient Foundation Language Models.” arXiv:2302.13971, 2023. 开源大模型,本篇大部分实践讨论的 base。
  16. Touvron, H. et al. “Llama 2: Open Foundation and Fine-Tuned Chat Models.” arXiv:2307.09288, 2023. LLaMA-2 的全参数 vs LoRA 微调对比可参考。
  17. Hadi, M. U. et al. “A Survey on Large Language Models: Applications, Challenges, Limitations, and Practical Usage.” Authorea preprint, 2023. PEFT 方法在大模型工程中的概览。
  18. Liu, H. et al. “Few-Shot Parameter-Efficient Fine-Tuning is Better and Cheaper than In-Context Learning.” NeurIPS 2022. PEFT 与 in-context learning 的系统对比。
  19. Chen, T. et al. “LongLoRA: Efficient Fine-tuning of Long-Context Large Language Models.” ICLR 2024 (arXiv:2309.12307). LoRA 在长上下文场景的扩展。
  20. Wang, Y. et al. “Multi-Task Learning with Adapters.” NeurIPS 2022. 多任务 PEFT 的代表性工作。

← 上一篇:30|预训练:自监督学习的胜利 | 下一篇:32|指令微调:把「补全」变成「听话」

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。


By .