上一篇从高斯消元推到了低秩分解,最后用 LoRA 举了个例子——\(\Delta W = BA\)。当时想清楚的是数学层面为什么两个小矩阵可以代替一个大矩阵,但 LoRA 本身没有展开。
顺着这条线,我还想搞清楚几件事:微调一个 7B 模型,真的需要更新全部参数吗?低秩约束到底省了什么、没省什么?推理的时候又该怎么处理这些额外的小矩阵?
全量微调的代价#
Full Fine-Tuning 的做法很直接:在预训练权重 \(W_0\) 的基础上,用下游任务数据继续训练,每个权重矩阵都可以自由更新:
$$ W' = W_0 + \Delta W $$\(\Delta W\) 和 \(W_0\) 一样大。一个 4096×4096 的权重矩阵,\(\Delta W\) 就有约 1678 万个自由参数。
更要命的是训练时的显存开销。之前算过,BF16 mixed precision + AdamW 配置下,每个可训练参数对应 16 bytes——参数本身、梯度、FP32 master weights 和 Adam 的两个矩估计。7B 模型的 Model States 加起来 112 GB,一张 80 GB 的 A100 连参数都放不下。
但预训练模型已经在海量数据上学到了通用的语言能力和知识表示。下游任务的适配——比如让模型学会某种回答格式,或者适应一个垂直领域的术语——真的需要在所有方向上都做调整吗?
用低秩矩阵参数化权重更新#
LoRA 不动 \(W_0\),把更新量约束为低秩形式:
$$ \Delta W = BA $$其中 \(B \in \mathbb{R}^{d \times r}\),\(A \in \mathbb{R}^{r \times d}\),\(r \ll d\)。
以 \(d = 4096\)、\(r = 16\) 为例:
- 完整 \(\Delta W\):\(4096 \times 4096 = 16{,}777{,}216\) 个参数
- LoRA 的 \(B + A\):\(4096 \times 16 + 16 \times 4096 = 131{,}072\) 个参数
可训练参数降低了 128 倍。
我在看这个公式的时候,有一个地方需要想清楚:LoRA 不是在压缩 \(W_0\),也不是把大模型变小。它限制的是权重更新 \(\Delta W\) 的自由度,不是基础模型的表达能力。
从子空间的角度看,\(A\) 把输入从 \(d\) 维投影到 \(r\) 维,\(B\) 再从 \(r\) 维映射回 \(d\) 维。这样构成的 \(\Delta W = BA\) 秩不超过 \(r\)——更新只能沿着 \(r\) 个方向调整权重,而不是在整个 \(d \times d\) 空间里自由移动。
但这不意味着整个 Transformer 的表达能力被限制在 \(r\) 维。每一层的每个目标矩阵(Q、K、V、O、FFN 等)都可以挂自己的 LoRA,各自选择不同的 \(r\) 维子空间。模型整体的更新是所有这些低秩增量的组合。
LoRA 能工作的前提假设是:预训练模型已经具备足够好的通用表示,下游任务的适配只需要在相对低维的子空间里做调整。这个假设在很多任务上经验性地成立——但它是一个经验观察,不是一个适用于所有场景的数学定理。
省掉的是什么,没省掉的又是什么#
LoRA 冻结 \(W_0\),只训练 \(A\) 和 \(B\)。直觉上"参数少了 128 倍,训练就快 128 倍"——实际没有这回事。
| 显存组件 | Full Fine-Tuning | LoRA |
|---|---|---|
| 基础权重 \(W_0\) | 存储,可训练 | 存储,冻结 |
| \(W_0\) 的梯度 | 需要 | 不需要 |
| \(W_0\) 的 Optimizer States | 需要 | 不需要 |
| A、B 的权重 + 梯度 + States | — | 需要(很小) |
| Activations | 需要 | 基本不变 |
| Forward 计算量 | 全模型 | 全模型 + 低秩分支 |
LoRA 真正省掉的是 \(W_0\) 的梯度和 Optimizer States。对于 7B 模型,这部分是 \(2 + 12 = 14\) bytes/param,合计 98 GB。A 和 B 参数量极小,它们的训练状态可以忽略。
但有几件事没有因此省掉:
Forward 仍然要跑完整的基础模型——输入要经过所有冻结层的矩阵乘法,这些 FLOPs 一个不少。Backward 通常也需要经过冻结层计算输入梯度——虽然 \(W_0\) 不更新,但梯度要传到前面的 LoRA 模块去。Activation Memory 也不会自动消失——反向传播需要的中间激活值跟哪些参数可训练没什么关系。
LoRA 主要降低的是可训练参数及其状态的显存开销,不是把整个训练的计算量按同比例缩减。
顺带一提,LoRA 和 ZeRO 优化的维度不同:ZeRO 通过分片分摊每张 GPU 的 Model States 负担,LoRA 通过减少可训练参数来降低训练状态总量。两者可以组合使用。
训练完把 Adapter 合并回去#
LoRA 有一个很漂亮的设计:推理时可以把低秩更新合并进基础权重。
训练时的前向计算是:
$$ y = W_0 x + \frac{\alpha}{r} BA x $$其中 \(\alpha\) 是一个缩放超参数,控制低秩更新的强度。因为矩阵乘法对加法满足分配律:
$$ y = \left(W_0 + \frac{\alpha}{r} BA\right) x $$训练完成后,预先计算 \(W' = W_0 + \frac{\alpha}{r} BA\),把合并后的权重存下来,推理时就是一个普通的 Linear Layer——不需要保留 A 和 B,也没有额外的矩阵乘法。
训练时用低秩分支降低可训练参数的显存成本;推理时通过权重合并,恢复成普通的矩阵乘法。 不需要在推理路径上长期保留额外的 Kernel Launch 和计算开销。
实际部署有两种常见场景:
- 单任务:直接 merge 权重,拿到一个标准模型,执行路径和普通模型完全一样。
- 多任务:多个任务共享 Base Model,各自保留独立的 LoRA Adapter。切换任务只需要换一组 A、B,不用重新加载整个基础模型。代价是推理时要保留分支结构,或者做动态 merge/unmerge。
低秩更新不是万能的#
几个需要清醒认识的边界。
Rank 的选择没有通用公式。 \(r\) 越大,允许的更新秩上限越高,但效果不保证单调提升。过大的 \(r\) 丢掉参数效率优势,过小的 \(r\) 可能限制有效适配。实践中需要在目标任务上调。
显存省不等于速度省。 LoRA 的显存节省很显著(Model States 的大头被砍掉了),但训练速度不会按同比例提升——Forward 和大部分 Backward 的计算量没有变化。
不是所有任务都适合低秩约束。 如果下游任务需要大幅改变模型的知识结构或表示方式,低秩更新可能不够用,全量微调仍然是更好的选择。不要把 LoRA 当成"几乎不损失精度的廉价替代"——不同任务的结果差异很大。
最后厘清一组概念:SFT(Supervised Fine-Tuning)是训练方式,LoRA 是参数高效微调技术,两者正交。LoRA SFT 就是用 LoRA 的方式做监督微调。如果想进一步压缩冻结的 Base Model 显存,QLoRA 通过量化 \(W_0\) 来实现,但那是另一个话题了。
写在最后#
三个我觉得值得记住的点:
LoRA 限制的是权重更新空间,不是基础模型的表达空间。 原始模型的能力都在冻结的 \(W_0\) 里,低秩约束只作用在增量 \(\Delta W\) 上。
LoRA 减少的是训练状态的显存,不是模型的计算量。 省掉的是 \(W_0\) 的梯度和 Optimizer States;Forward/Backward 的 FLOPs 基本不变。
低秩线性结构让 Adapter 可以合并回基础权重。 训练时享受参数效率,推理时恢复成普通 Linear Layer——参数效率和执行效率兼得。
参考资料#
- 从高斯消元到矩阵的 Rank:重新理解主元、自由变量与低秩 — 本文的数学前置
- 7B 模型只有 14 GB 权重,一张 80 GB GPU 为什么仍可能 OOM? — 16 bytes/param 的显存账本
- 从 DDP 到 ZeRO — 分布式训练的 Model States 分片
- Hu et al., LoRA: Low-Rank Adaptation of Large Language Models, 2021