第 23 讲 · 大模型的高效微调:CIAT、LoRA 与 QLoRA · Parameter-Efficient Fine-Tuning

Large Language Model Systems · Lecture 23。个人学习讲义与配套实验,按主题整理并持续修订。

课程源码 ZIP ↓

个人整理的学习笔记,非官方讲义;课程来源与整理说明见课程介绍。配套示例未逐一运行验证。

本篇目录

对应 Maps to:CMU 11-868 / 11-968 Large Language Model Systems(Spring 2026,Lei Li),Lecture 23 · Wed 4/8 · Efficient fine-tuning for Large Models(slides:llmsys-23-peft.pdf,55 页)。 阅读材料:Zhu et al. (2021) Counter-Interference Adapter for Multilingual Machine Translation(CIAT,EMNLP Findings);Hu et al. (2022) LoRA: Low-Rank Adaptation of Large Language Models(ICLR);Dettmers et al. (2023) QLoRA: Efficient Finetuning of Quantized LLMs(NeurIPS);Lialin et al. (2024) Scaling Down to Scale Up: A Guide to Parameter-Efficient Fine-Tuning。 依据说明:slides 覆盖:上一讲的回顾;全量微调的显存账、LoRA 与 QLoRA 的显存账;PEFT 的三类(选择性、重参数化、加性);加性微调与低秩更新;LoRA 最早出现在 CIAT、后来在 LoRA 论文里被重新提出;LoRA / CIAT 加在哪些层;推理时的合并与按任务切换;反向只对 A、B 求梯度;训练时要存的东西与显存;LoRA 的好处;LoRA 论文的实验(NLU、NLG、六种基线、秩的选择、GPT-3 175B、GSM8K);PTQ 与 QAT;QLoRA 的三个创新(NF4、双重量化、分页优化器)与算法步骤;NF4 的构造、例子与反量化;双重量化的账;分页优化器;QLoRA 的实验;LoRA 与 QLoRA 的代码走读。CIAT 的嵌入 adapter、并行 adapter 的具体形式取自 CIAT 论文;NF4 的分位数构造细节取自 QLoRA 论文与 bitsandbytes 源码。本讲义的”新方言”微调任务、所有在城市语言模型上的实验(ΔW 的奇异值、秩扫描、方法比较、遗忘的测量、热切换的误差、多适配器的批处理、NF4 / FP4 / INT4 的比较、QLoRA)都是本讲义自己设计的,不代表课堂上讲了什么。 配套代码 Code:code/lecture23/(4 个脚本 + peft.py;复用第 19 讲的模型;1–100 秒) 预计阅读时间:3 小时(含运行代码)


0. 本讲导航 · How to read this lecture

0.1 学习目标 Learning goals

学完这一讲,你应该能够:

  1. 算出全量微调、LoRA、QLoRA 各自要多少显存,说清楚 PEFT 省下的是哪几项。
  2. 说出 PEFT 的三大类(选择性、重参数化、加性)并各举一例。
  3. 解释 LoRA:为什么”低秩”站得住、B 为什么初始化为 0、梯度怎样流、加在哪些层、秩取多少、怎样合并与切换、怎样同时服务很多适配器。
  4. 说出 CIAT 的两种 adapter(嵌入 adapter、并行的层 adapter)和它与 LoRA 的关系。
  5. 构造 NF4 查找表,算出双重量化省下的位数,说明 QLoRA 为什么几乎不损失效果。
  6. 用实验数据讨论 PEFT 的局限:它不自动保证”不遗忘”,提示调优在小模型上很弱。

0.2 先修与本讲范围 Prerequisites & scope

默认你已经会 Assumed本讲只给直觉,后面再展开 Previewed only
训练的显存账:权重、梯度、Adam 状态、激活(第 18 讲)在 LoRA 上做强化学习微调(第 25 讲)
分块量化、absmax、INT4 / FP4(第 19 讲、前置讲 P2)多 LoRA 服务的专门内核(S-LoRA、Punica)
矩阵的秩、奇异值分解;HW5 里的 LoRA 适配器(第 18 讲 §6)DoRA、LoRA+ 等后续改进

0.3 本讲地图 Map

§1  微调为什么贵:全量 / LoRA / QLoRA 的显存账                                     ← 代码 01
§2  PEFT 的三大类                                                                 ← 代码 01
§3  LoRA:低秩更新、初始化与梯度、加在哪里、秩、合并与切换、多适配器服务              ← 代码 02
§4  CIAT:嵌入 adapter 与并行 adapter                                              ← 代码 03
§5  方法比较:谁学得会、谁忘得多                                                    ← 代码 03
§6  QLoRA:NF4、双重量化、分页优化器                                               ← 代码 04
§7  代码走读   §8 小结   §9 常见误区   §10 练习   §11 延伸阅读   §12 下一讲预告

0.4 本讲的实验设置

为了能精确地回答”微调效果差多少”,peft.py 造了一个新的微调领域:城市语言的”新方言”——句型和词表不变,但每个城市”以什么著称”换成了往后数 3 个城市的那一套、“天气如何”换成了往后数 5 个城市的那一套(知识被改写了),而且新方言里没有”<城市> has very nice …”(T2)这种句型。新方言的真实分布是已知的,所以:

  • 学会了多少:新方言测试集上的交叉熵超出理论下界多少(毫 nat / token,越小越好)。预训练模型是 577.4。
  • 忘了多少:原语言的 T2 句子里,给定前缀”<城市> has very nice”,预测”好东西”那一个位置的超出(毫 nat)。T2 的知识在新旧语言里相同,但微调数据里从来不出现。预训练模型是 12.1。

1. 微调为什么贵 · Why fine-tuning is expensive

slides 第 4 页:全量微调要更新所有参数。按半精度 + Adam 算,每个参数要存权重(2 字节)、梯度(2 字节)、两份优化器状态(4 字节),再加上大约是参数 1–2 倍的激活——LLaMA-8B 约要 80 GB 的工作显存。01 号脚本第 1 部分把账算细(LLaMA-3-8B,8.03B 参数,不含激活):

做法权重梯度优化器状态合计
全量(slides 的口径:2 + 2 + 4 字节)16.1 GB16.1 GB32.1 GB64.2 GB
全量(混合精度的常见做法:再加 FP32 主权重、FP32 的 Adam;第 18 讲的 16 字节)16.116.196.4128.5
LoRA r = 8、所有线性层(21.0M 个可训练参数)16.100.0420.16816.31
QLoRA(NF4 + 双重量化的基座 + 同样的适配器)4.180.0420.1684.39

LoRA 冻结基座:基座只占权重本身,没有梯度、没有优化器状态;适配器的梯度和 Adam 状态加起来不到 0.25 GB。剩下的大头是激活——slides 估计 LoRA 总共约 33 GB(一块 A100 放得下),QLoRA 约 9.2 GB(消费级显卡也行)。注意:LoRA 省不了激活,反向传播仍然要用激活来算 A、B 的梯度。

QLoRA 论文的 65B 例子(第 3 部分):按每个参数 12 字节(2 + 2 + 8)正好 780 GB;QLoRA 的基座只要 33.5 GB,加上适配器、激活(配合梯度检查点)和分页优化器,论文在一块 48 GB 的 GPU 上完成了微调。


2. PEFT 的三大类 · Three families of PEFT

slides 第 7 页(Lialin et al. 的分类):

  • 选择性方法:从已有的参数里挑一小部分来训练。例:BitFit 只训练偏置;只训练 LayerNorm 的参数;只训练最后几层。
  • 重参数化方法:把权重的更新写成低秩矩阵的乘积。例:LoRA。
  • 加性方法:新增可训练的层或参数。例:adapter(在层之间插小网络)、软提示(prompt tuning、prefix tuning:在输入前面拼可训练的向量)。

在城市 LLaMA(797,824 个参数)上,它们各训练多少(01 号脚本第 4 部分):

类别方法可训练参数占比
选择性只训练 RMSNorm 的 γ(这个模型没有偏置,代替 BitFit)1,1520.14%
重参数化LoRA r = 8:Wq、Wv / 注意力四个矩阵 / 所有线性层14,336 / 28,672 / 77,8241.8% / 3.6% / 9.8%
加性串行 / 并行 adapter(瓶颈 8)17,4722.2%
加性提示调优(8 个虚拟 token)1,0240.13%

这个模型只有 d = 128,所以 LoRA 的占比看起来不小;在 d = 4096 的 LLaMA-8B 上,同样 r = 8 只占 0.04%(Wq、Wv)到 0.26%(所有线性层)。


3. LoRA:低秩更新 · Low-rank adaptation

3.1 想法

slides 第 12–13 页:冻结预训练的权重 W₀,只训练一个低秩的更新:

W′=W0+ΔW=W0+BA,B∈Rd×r, A∈Rr×k, r≪min⁡(d,k)W' = W_0 + \Delta W = W_0 + B A,\qquad B \in \mathbb{R}^{d\times r},\ A \in \mathbb{R}^{r\times k},\ r \ll \min(d, k)

一个 d × k 的矩阵,全量微调要训练 d·k 个参数;LoRA 只要 r·(d + k) 个。slides 特别指出:这个想法最早出现在 CIAT(Zhu et al., EMNLP 2021,本课老师 Lei Li 组的工作),后来在 LoRA 论文(Hu et al., ICLR 2022)里被重新提出。LoRA 论文报告:在 GPT-3 175B 上,可训练参数减少到约 1/10000,显存需求减少到约 1/3,效果与全量微调相当。实际实现里更新还要乘一个缩放 α/r,让换一个 r 时不必大调学习率。

3.2 为什么低秩站得住

直觉是:预训练模型已经学会了绝大部分东西,适应一个新任务只需要”沿着少数几个方向”调整(Aghajanyan et al. 的”内在维度”、LoRA 论文第 7 节的分析)。02 号脚本第 2 部分直接检验:先做一次全量微调(新方言上 8.1),看每个线性层的更新 ΔW = W_微调 − W₀ 的奇异值:

矩阵满秩前 1 个奇异值的能量前 8 个前 32 个对照:随机矩阵前 8 个
attn.q12834.0%86.3%98.6%6.2%
attn.v6436.1%91.3%99.4%12.5%
ffn.w212818.0%75.2%96.0%6.2%

全量微调的更新本身就是近似低秩的。把每个 ΔW 只保留前 r 个奇异方向:秩 1 时 232.4、秩 4 时 38.7、秩 8 时 9.6、秩 32 时 7.8(不截断 8.1)。秩 8 就保住了几乎全部效果。

3.3 初始化与梯度

LoRA 的标准初始化:A 随机(kaiming 均匀或高斯)、B = 0。所以刚装上时 ΔW = BA = 0,模型与预训练模型逐位相同(02 号脚本第 1 部分:最大差 0.0)——微调从预训练模型”原样”出发。

反向传播(slides 第 16 页):W₀ 冻结,只对 A、B 求梯度。设 h = W₀x + (α/r)·B·A·x,

∂L∂B=αr ∂L∂h (Ax)⊤,∂L∂A=αr B⊤ ∂L∂h x⊤\frac{\partial L}{\partial B} = \frac{\alpha}{r}\,\frac{\partial L}{\partial h}\,(Ax)^\top,\qquad \frac{\partial L}{\partial A} = \frac{\alpha}{r}\,B^\top\,\frac{\partial L}{\partial h}\,x^\top

B = 0 时 A 的梯度为 0:第一步只有 B 在动(脚本里 A 的梯度范数之和 0.000,B 的 1.236),之后 A 才开始更新。冻结的参数没有梯度,也就没有优化器状态(slides 第 17 页:只需存原始参数、适配器权重 2·d·r、它们的梯度 2·d·r、一阶和二阶矩)。

3.4 加在哪些层、秩取多少

slides 第 14 页:LoRA 论文把它加在注意力的 Wq、Wk、Wv、Wo 上(实验里主要是 Wq、Wv),而不加在 FFN 上——但 FFN 的权重”存储知识”;CIAT 把它也加在嵌入和 FFN 上,效果更好。秩通常取 8 或 16。

01 号脚本第 2 部分:LLaMA-3-8B 上 r = 8、Wq Wv 只有 3.4M 个参数(slides 第 18 页说”约 4M、8 MB”),加到所有线性层、r = 64 也不到基座的 3%。

秩的选择(02 号脚本第 3 部分,注意力四个矩阵,α = 2r):

r12481632
新方言:超出65.631.514.99.57.88.5

r 从 1 加到 8 效果明显变好,再往上几乎不变——slides 第 23 页的结论:“增大秩并不会覆盖更多有意义的子空间”,r = 8 左右就够了。每一步的时间几乎不随 r 变化:两个小矩阵乘相对冻结的大矩阵乘可以忽略。

3.5 推理:合并与切换

slides 第 15 页:推理时直接用 W’ = W₀ + BA,与普通模型一模一样,没有额外的推理开销;同时把 A、B 另存一份(r = 8、16 时很小)。要换一个任务 / 领域时:

W′′=W′−BA+B′′A′′W'' = W' - BA + B''A''

02 号脚本第 4 部分:合并前后 logits 只差 1.1e-5(float32 舍入)。但”减去旧的、加上新的”这种原地切换每次都会引入舍入误差,并且累积:FP32 存的权重切换 100 次后与”直接合并”差 1.5e-6;BF16 存的权重切换 1 次就差 2e-3,100 次差 4.9e-2。实践中保留一份原始的 W₀(或者干脆不合并),切换时从 W₀ 重新合并。

3.6 一个服务器,很多适配器

如果不合并,每个请求可以用自己的 A、B:基座的大矩阵乘对整批请求只算一次,两个小矩阵乘按请求取出各自的 A、B(gather + 批量矩阵乘)。02 号脚本第 5 部分把两个适配器的 8 个请求混在一个批里,与”每个请求用自己合并好的模型”差 0;显存上,100 个适配器只需要一份基座 + 100 份几十 KB 的 A、B,而 100 个合并好的模型要 21.8 倍的空间。这就是 S-LoRA、Punica 的思路(vLLM、SGLang 都支持多 LoRA 服务),代价是需要专门的内核(例如 Punica 的 BGMV)让不合并的计算也高效。


4. CIAT:抗干扰 adapter · Counter-Interference Adapter

CIAT 解决的是多语言机器翻译里的一个现象:一个多语言模型在资源丰富的语言对上,往往不如单独训练的双语模型。论文把原因归结为两种干扰:

  • 多语言嵌入的缺陷:共享词表里,拼写相同的词在不同语言里意思不同(论文的例子:英语的 bride 是新娘,法语的 bride 是马笼头)。
  • 层之间的干扰:一个容量有限的模型同时学很多语言,提取的特征互相干扰——可以把多语言模型某个子层的输出看成”双语模型的输出 + 一个由干扰造成的偏差 δ”。

CIAT 的做法:先在全部语料上训练一个多语言 Transformer 并冻结,再为每个语言方向插入小 adapter、只在这个方向的双语语料上训练:

  • 嵌入 adapter:Ẽ[w] = E[w] + G(LayerNorm(E[w]))——用一个瓶颈前馈网络(d → m → d,中间 ReLU、输出无激活)近似”为每个语言对微调一份嵌入矩阵”,参数少得多。
  • 并行的层 adapter:x_{i+1} = F(x_i) + G(x_i)。G 和子层 F 接收同一个输入,去拟合 −δ,“在偏差进入子层之前就去噪”;它可以和子层共用归一化。对照的串行 adapter(Bapna & Firat 2019)是 G(F(x_i)),只能看到已经被干扰过的输出。论文指出,并行 adapter 可以看作子层的低秩”补丁”——把 F 换成线性层、去掉 ReLU,G(x) = B·A·x,这正是 LoRA 的形式。

论文在 IWSLT、OPUS-100、WMT 上报告:66 个翻译方向里 64 个超过强的多语言基线,其中 42 个提升超过 0.5 BLEU。


5. 方法比较:谁学得会、谁忘得多 · Comparing methods

slides 第 20–22 页:LoRA 论文在 NLU(RoBERTa、DeBERTa 上的 GLUE 子任务)和 NLG(GPT-2、GPT-3 上的 E2E 等)上与六种基线(全量微调、BitFit、前缀嵌入调优、前缀层调优、adapter、LoRA)比较,LoRA 用更少的参数达到持平或更好的效果;GPT-3 175B 的压力测试显示,并不是所有方法都随可训练参数增加而单调变好(第 24–27 页)。

03 号脚本在新方言上做了同样的比较(每种方法用附近试过的 2–3 个学习率里最好的那个,300 步):

方法可训练参数新方言:超出旧知识:超出
全量微调797,8248.1236.0
只训练 RMSNorm 的 γ1,15247.61027.7
LoRA r = 8:Wq、Wv14,33612.5678.3
LoRA r = 8:注意力四个矩阵28,6728.3425.0
LoRA r = 8:FFN 三个矩阵49,1527.3470.9
LoRA r = 8:所有线性层77,8247.3654.4
串行 adapter(瓶颈 8)17,4728.4873.7
并行 adapter(CIAT)17,47212.21363.4
并行 + 嵌入 adapter(CIAT)19,91211.8609.8
提示调优(8 个虚拟 token)1,024486.757.4

(预训练模型:新方言 577.4,旧知识 12.1。)几点观察:

  • LoRA ≈ 全量微调:注意力四个矩阵、3.6% 的参数,8.3 vs 8.1。
  • 加在哪里很重要:这个任务要改写”城市 → 特色 / 天气”的知识,只动 Wq、Wv 最差(12.5),加到 FFN 或所有线性层最好(7.3)——与 slides 说 CIAT 加到 FFN 更好的方向一致。
  • adapter 也能学会:串行 8.4、CIAT 的并行 12.2(加嵌入 adapter 11.8)。这个设置里并行并没有比串行好;CIAT 的结论是在多语言翻译上得到的。
  • 选择性方法和提示调优力不从心:只调 γ 只学会一部分(47.6);提示调优几乎学不会(486.7)——在输入前面加几个向量改变不了模型”记住”的事实,而且 Lester et al. 发现提示调优要到几十亿参数的模型上才追得上全量微调。

学会与忘记。提示调优几乎不忘(57.4),因为它根本没动模型;所有学会了新方言的方法都明显损失了旧知识,而且全量微调反而最少(236.0)。03 号脚本第 2 部分换不同的学习率:全量微调 1e-4 / 3e-4 / 1e-3 → 新方言 89.7 / 23.1 / 8.1、旧知识 179.9 / 216.7 / 236.0;LoRA 1e-3 / 3e-3 / 1e-2 → 70.6 / 9.5 / 8.3、325.2 / 531.9 / 425.0。大体上学得越多、忘得越多;学到同样程度时,LoRA 忘得更多。这与文献里常说的”LoRA 学得少、也忘得少”(Biderman et al. 2024,在几十亿参数的模型上做继续预训练和指令微调)不同——这里模型很小、任务是改写互相冲突的知识。PEFT 省的是显存和存储,不自动保证不遗忘;需要的话混入旧数据,或者干脆按任务切换适配器(§3.5–3.6)。


6. QLoRA:在 4 位基座上做 LoRA · QLoRA

6.1 总览

slides 第 31–33 页:GPTQ 是训练后量化(PTQ);QLoRA 把量化和训练结合起来:一种存储类型(通常是 4 位 NormalFloat)+ 一种计算类型(BF16)。

  1. 把基座权重量化成 NF4(双重量化)存着;
  2. 用到时反量化成 BF16;
  3. 前向、反向都在 BF16 里算;
  4. 只对 LoRA 的参数求权重梯度。

三个创新:NF4、双重量化、分页优化器。效果:65B 模型微调的平均显存需求从 780 GB 降到一块 48 GB 的 GPU,同时保持 16 位微调的效果。QLoRA 论文训练的 Guanaco 在 Vicuna 基准上达到 ChatGPT 99.3% 的水平,只在一块 GPU 上微调了 24 小时;slides 的总结说它是第一个能在单块消费级 GPU 上微调 33B 模型的方法。

6.2 NF4:按分位数放格子

slides 第 34 页:4 位只有 16 个值。absmax 量化把范围等分,对非均匀分布的数据误差大——大量取值相近的小数挤在同一个格子里。预训练的权重近似正态分布,所以 NF4 让每个格子分到相同的概率质量:取标准正态的分位数。04 号脚本第 1 部分按 bitsandbytes 的 create_normal_map 构造:正半边取 N(0, 1) 在 [0.5, 0.9677] 之间等分的 8 个概率点上的分位数,负半边 7 个,再加精确的 0,归一化到 [−1, 1]:

[-1, -0.6962, -0.5251, -0.3949, -0.2844, -0.1848, -0.0911, 0,
 0.0796, 0.1609, 0.2461, 0.3379, 0.4407, 0.5626, 0.7230, 1]

与 slides 第 35 页的表在四位小数上一致,与 bitsandbytes 源码的常数差 1.1e-7。两个细节:正负不对称(8 + 7 + 0)是为了能精确表示 0;0.9677 是 1 − 1/(2·15) 与 1 − 1/(2·16) 的平均,避开分位数在 ±∞ 处的发散。

量化的步骤(slides 第 35 页):每 64 个数一块 → 求 max|x| 作为 scale → 归一化 → 在表里找最近的值 → 存它的下标(4 位)。slides 第 36、38 页的例子(第 2 部分):0.0045,块的 scale 0.0071,归一化 0.634,最近的表值 0.5626(下标 13;slides 按 −7…8 编号写成 6),反量化 0.00399,误差 0.00051(约 11%)。单个误差并不小,要靠”误差在大量权重上平均掉”和 LoRA 在量化后的基座上继续训练来弥补。

为什么用分位数:标准正态的数按 64 个一块归一化后,NF4 的 16 个码被使用得更均匀(最常用的码 9.25%,INT4 是 14.78%),码的熵 3.884 位(INT4 3.500)——4 位的信息量用得更满。同样 4 位、每 64 个数一个 scale 的相对误差(第 3 部分):

网格高斯数据拉普拉斯数据城市 LLaMA 的线性层
INT4(absmax)0.01160.01990.0121
FP4 E2M10.01120.01260.0114
NF40.00850.01080.0087

NF4 在三种数据上都最好;slides 第 45 页:NF4 比 FP4 效果更好。

6.3 双重量化

slides 第 39–40 页:块越小量化越准,但每块一个 FP32 的 scale 摊到每个参数上就是 32/64 = 0.5 位——4 位的八分之一。双重量化把这些 scale 再量化一次:减去均值后每 256 个 scale 一块,用 FP8 存,每块一个 FP32 的二级 scale。账是

4+864+3264⋅256=4.127 位≈0.516 字节 / 参数4 + \frac{8}{64} + \frac{32}{64\cdot 256} = 4.127\ \text{位} \approx 0.516\ \text{字节 / 参数}

04 号脚本第 4 部分:每个参数 4.500 → 4.127 位,省 0.373 位(65B 模型约 3 GB);scale 本身的相对误差 9e-5,权重的总误差只从 0.00872 变成 0.00881。

6.4 分页优化器

slides 第 42–44 页:训练时偶尔会因为一个特别长的序列、激活突然变大而 OOM。分页优化器用 NVIDIA 的统一内存为优化器状态分配”可分页”的显存:显存不够时自动把它们换出到 CPU 内存,优化器更新需要时再换回来——与操作系统在内存和磁盘之间换页是同一个思路(前置讲 P4)。本讲义没有模拟它。

6.5 QLoRA 的效果

04 号脚本第 5 部分(所有线性层的基座量化成 4 位,LoRA r = 8 加在所有线性层):

做法新方言:超出原语言:超出
全精度基座,不微调577.43.0
NF4 + 双重量化的基座,不微调576.03.3
LoRA(全精度基座)7.3523.1
QLoRA(NF4 + 双重量化)6.0531.4
QLoRA(NF4,不做双重量化)6.9513.6
LoRA(INT4 absmax 的基座)7.8518.1

只量化不微调,原语言上几乎无损(3.0 → 3.3);QLoRA 与全精度基座上的 LoRA 效果相当(slides 第 46 页:4 位 QLoRA 与 16 位全量微调、16 位 LoRA 持平),基座的显存却只有 BF16 的约 26%。(原语言上的超出在微调后都很大,是新方言改写了知识,与量化无关。)还有一点值得注意:LoRA 是在量化后的权重上训练的,它学到的 ΔW 也在补偿量化误差。slides 第 45 页还提醒:LoRA 的默认超参数(例如只加在注意力上)不足以追平 16 位微调,QLoRA 论文把 LoRA 加在所有线性层上。


7. 代码走读 · Code walkthrough

slides 第 49–51 页的 LoRA 层就是 peft.LoRALinear:

class LoRALinear(nn.Module):
    def __init__(self, base, r=8, alpha=16):
        self.base = base                                   # 冻结预训练的线性层
        for p in self.base.parameters(): p.requires_grad_(False)
        self.scale = alpha / r
        self.A = nn.Parameter(torch.empty(r, base.in_features))
        self.B = nn.Parameter(torch.zeros(base.out_features, r))   # B = 0:开始时 ΔW = 0
        nn.init.kaiming_uniform_(self.A, a=math.sqrt(5))

    def forward(self, x):                                  # 两条支路相加
        if self.merged: return self.base(x)
        return self.base(x) + self.scale * (x @ self.A.t()) @ self.B.t()

合并(merge_lora)把 scale·B·A 加进 base.weight,之后前向只走一条支路。slides 第 52 页的 QLoRA 用法:用 bitsandbytes 的 BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_quant_type="nf4", bnb_4bit_use_double_quant=True, bnb_4bit_compute_dtype=torch.bfloat16) 加载模型,再套上 LoRA。第 18 讲的 HW5 Problem 3 也走过一遍 LoRA 与 ZeRO 的组合。


8. 小结 · Summary

  1. 全量微调贵在梯度和优化器状态(每个参数 8–16 字节);PEFT 冻结基座,只为很少的参数付这笔钱;激活的显存省不掉。
  2. PEFT 有三类:选择性(BitFit、只调归一化)、重参数化(LoRA)、加性(adapter、软提示)。
  3. LoRA:ΔW = BA,r ≪ d;全量微调的更新本身近似低秩;B = 0 初始化让微调从原模型出发;r = 8 左右就够;推理时合并、没有额外开销;不合并时可以在一个批里服务很多适配器。
  4. CIAT 最早提出这种低秩的”补丁”:嵌入 adapter + 与子层并行的层 adapter,并且加在 FFN 和嵌入上。
  5. QLoRA = NF4(按正态分位数放格子)+ 双重量化(scale 再量化,4.5 → 4.127 位)+ 分页优化器 + BF16 计算,效果与 16 位 LoRA 持平。
  6. 局限:PEFT 不自动减少遗忘;提示调优在小模型上很弱;LoRA 加在哪里、学习率多少都要调。

9. 常见误区 · Common pitfalls

  1. “LoRA 能把训练显存降低几十倍。” 它省的是梯度和优化器状态;权重本身和激活还在(QLoRA 再压权重,梯度检查点再压激活)。
  2. “B 和 A 都初始化成随机数更好。” 那样一开始 ΔW ≠ 0,微调从一个被扰动过的模型出发;B = 0 保证从原模型出发。
  3. “秩越大越好。” r = 8 左右之后几乎不再改善(02 号脚本第 3 部分);但秩太小(1、2)确实不够。
  4. “LoRA 只能加在注意力上。” 那是 LoRA 论文的默认;改写知识的任务上加在 FFN / 所有线性层更好,CIAT 和 QLoRA 都这样做。
  5. “原地切换适配器(W − BA + B’A’)没有代价。” 舍入误差会累积,BF16 存的权重尤其明显;保留原始 W 重新合并,或者不合并。
  6. “PEFT 不会遗忘。” 在我们的实验里所有学会了新任务的 PEFT 方法都比全量微调忘得多(03 号脚本)。
  7. “NF4 就是 FP4。” NF4 的格子是按正态分位数放的,不是浮点数的指数间隔;同样 4 位,误差小约 25%。
  8. “双重量化会明显增加误差。” scale 本身的相对误差只有 1e-4 量级。

10. 练习 · Exercises

  1. LLaMA-3-8B(d = 4096、32 层、KV 维 1024、FFN 14336)上,r = 16 的 LoRA 只加在 Wq、Wk、Wv、Wo 上,有多少个参数?存成 BF16 多大?
  2. 用 Adam 微调一个 70B 模型,全量(每个参数 16 字节)要多少显存?QLoRA 的基座要多少?
  3. 推导 ∂L/∂A 和 ∂L/∂B。为什么 B = 0 时第一步 A 不动?如果反过来 A = 0、B 随机,会怎样?
  4. 一个 4096 × 4096 的矩阵,全量微调的 ΔW 前 8 个奇异值占 90% 的能量。用 r = 8 的 LoRA 至少能达到”ΔW 的 90%“吗?为什么不一定?
  5. 两个适配器的 r 都是 8、都加在同一组层上,能不能把它们”相加”成一个适配器?得到的秩是多少?
  6. 构造一个 2 位(4 个值)的 NormalFloat 表:按本讲的规则,正半边几个值、负半边几个、要不要 0?
  7. 双重量化里,如果第二级的块从 256 改成 64,每个参数多少位?
  8. 为什么 QLoRA 要”存 4 位、算 BF16”,而不是直接用 4 位做矩阵乘法?
  9. 为什么说提示调优几乎不会遗忘?它适合什么样的任务?
  10. 一个服务器要同时服务 1000 个 LoRA 适配器(r = 16、所有线性层、LLaMA-3-8B),适配器一共占多少显存?和 1000 个合并好的模型比呢?

参考答案

  1. Wq、Wo:16 × (4096 + 4096) = 131,072;Wk、Wv:16 × (4096 + 1024) = 81,920。每层 2 × 131,072 + 2 × 81,920 = 425,984,32 层 13.6M 个参数(01 号脚本的表),BF16 约 27 MB。
  2. 全量:70e9 × 16 = 1120 GB(还不算激活)。QLoRA 基座:70e9 × 4.127 / 8 ≈ 36 GB。
  3. h = W₀x + s·BAx(s = α/r)。∂L/∂B = s·g·(Ax)ᵀ,∂L/∂A = s·Bᵀ·g·xᵀ(g = ∂L/∂h)。B = 0 时 ∂L/∂A = 0。反过来 A = 0、B 随机也能让 ΔW = 0,但第一步只有 A 动——两种都可以,LoRA 选择 B = 0;不能两个都是 0(那样两者的梯度都是 0,永远不动)。
  4. 不一定。截断 SVD 给出的是”对全量微调的 ΔW 最好的秩 8 近似”;LoRA 是从头用梯度下降训练 A、B,走的路径、学到的子空间都可能不同(它不知道全量微调的 ΔW 是什么)。我们的实验里两者效果相近(9.6 vs 9.5),但这不是定理。
  5. 可以:B₁A₁ + B₂A₂ = [B₁ B₂]·[A₁; A₂],是一个 r = 16 的 LoRA(秩最多 16)。所以”合并两个适配器”不会让秩保持在 8。
  6. 2^(b−1) = 2 个正值、2^(b−1) − 1 = 1 个负值、再加一个 0:{−1, 0, a, 1},a 是 N(0, 1) 在对应概率点上的分位数归一化后的值。
  7. 4 + 8/64 + 32/(64·64) = 4.1328 位,比 256 时多 0.0059 位。
  8. 现有的张量核心不支持”4 位 NF4 × BF16”的矩阵乘(NF4 是查表格式,不是硬件数值类型);反量化成 BF16 再乘,数值稳定、实现简单,代价是一次反量化(可以融合进内核)。训练的瓶颈主要在显存,不在这一点计算。
  9. 它不改变模型的任何参数,旧任务只要不加软提示就和原来一模一样。适合”只需要改变模型行为方式、不需要新知识”的任务(格式、风格、任务指令),在大模型上效果更好。
  10. 每个适配器:r = 16、所有线性层 41.9M 个参数(01 号脚本),BF16 约 84 MB;1000 个约 84 GB——比一块 80 GB 的卡还大,所以真实系统把不常用的适配器放在 CPU 内存、按需换入(S-LoRA 的做法)。1000 个合并好的模型要 16 TB。

11. 延伸阅读 · Further reading

  • CIAT:Zhu et al. (2021);代码 github.com/Yaoming95/CIAT。串行 adapter:Houlsby et al. (2019)、Bapna & Firat (2019)。
  • LoRA:Hu et al. (2022);内在维度:Aghajanyan et al. (2021)。
  • QLoRA:Dettmers et al. (2023);bitsandbytes 源码里的 create_normal_map 与 4 位内核。
  • 提示调优 / 前缀调优:Lester et al. (2021);Li & Liang (2021)。BitFit:Ben Zaken et al. (2022)。
  • 遗忘:Biderman et al. (2024) LoRA Learns Less and Forgets Less。
  • 多 LoRA 服务:Sheng et al. (2024) S-LoRA;Chen et al. (2024) Punica。
  • 综述:Lialin et al. (2024) Scaling Down to Scale Up。

12. 下一讲预告 · Next lecture

第 22 讲的服务器把 KV 缓存当作一段段连续的张量来管理,第 24 讲是 vLLM 作者 Woosuk Kwon 的嘉宾讲座:PagedAttention。为什么过去的推理系统只用上了 20%–40% 的 KV 缓存显存?操作系统的分页思想怎样把它提到接近 100%,又怎样让并行采样、束搜索共享 KV?以及一个真实的开源推理引擎还要解决哪些问题(CPU 开销、CUDA 图、模型并行、混合模型的显存分配)。

← 返回课程目录