对应 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
学完这一讲,你应该能够:
- 算出全量微调、LoRA、QLoRA 各自要多少显存,说清楚 PEFT 省下的是哪几项。
- 说出 PEFT 的三大类(选择性、重参数化、加性)并各举一例。
- 解释 LoRA:为什么”低秩”站得住、B 为什么初始化为 0、梯度怎样流、加在哪些层、秩取多少、怎样合并与切换、怎样同时服务很多适配器。
- 说出 CIAT 的两种 adapter(嵌入 adapter、并行的层 adapter)和它与 LoRA 的关系。
- 构造 NF4 查找表,算出双重量化省下的位数,说明 QLoRA 为什么几乎不损失效果。
- 用实验数据讨论 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 GB | 16.1 GB | 32.1 GB | 64.2 GB |
| 全量(混合精度的常见做法:再加 FP32 主权重、FP32 的 Adam;第 18 讲的 16 字节) | 16.1 | 16.1 | 96.4 | 128.5 |
| LoRA r = 8、所有线性层(21.0M 个可训练参数) | 16.10 | 0.042 | 0.168 | 16.31 |
| QLoRA(NF4 + 双重量化的基座 + 同样的适配器) | 4.18 | 0.042 | 0.168 | 4.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,152 | 0.14% |
| 重参数化 | LoRA r = 8:Wq、Wv / 注意力四个矩阵 / 所有线性层 | 14,336 / 28,672 / 77,824 | 1.8% / 3.6% / 9.8% |
| 加性 | 串行 / 并行 adapter(瓶颈 8) | 17,472 | 2.2% |
| 加性 | 提示调优(8 个虚拟 token) | 1,024 | 0.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₀,只训练一个低秩的更新:
一个 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.q | 128 | 34.0% | 86.3% | 98.6% | 6.2% |
| attn.v | 64 | 36.1% | 91.3% | 99.4% | 12.5% |
| ffn.w2 | 128 | 18.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,
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):
| r | 1 | 2 | 4 | 8 | 16 | 32 |
|---|---|---|---|---|---|---|
| 新方言:超出 | 65.6 | 31.5 | 14.9 | 9.5 | 7.8 | 8.5 |
r 从 1 加到 8 效果明显变好,再往上几乎不变——slides 第 23 页的结论:“增大秩并不会覆盖更多有意义的子空间”,r = 8 左右就够了。每一步的时间几乎不随 r 变化:两个小矩阵乘相对冻结的大矩阵乘可以忽略。
3.5 推理:合并与切换
slides 第 15 页:推理时直接用 W’ = W₀ + BA,与普通模型一模一样,没有额外的推理开销;同时把 A、B 另存一份(r = 8、16 时很小)。要换一个任务 / 领域时:
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,824 | 8.1 | 236.0 |
| 只训练 RMSNorm 的 γ | 1,152 | 47.6 | 1027.7 |
| LoRA r = 8:Wq、Wv | 14,336 | 12.5 | 678.3 |
| LoRA r = 8:注意力四个矩阵 | 28,672 | 8.3 | 425.0 |
| LoRA r = 8:FFN 三个矩阵 | 49,152 | 7.3 | 470.9 |
| LoRA r = 8:所有线性层 | 77,824 | 7.3 | 654.4 |
| 串行 adapter(瓶颈 8) | 17,472 | 8.4 | 873.7 |
| 并行 adapter(CIAT) | 17,472 | 12.2 | 1363.4 |
| 并行 + 嵌入 adapter(CIAT) | 19,912 | 11.8 | 609.8 |
| 提示调优(8 个虚拟 token) | 1,024 | 486.7 | 57.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)。
- 把基座权重量化成 NF4(双重量化)存着;
- 用到时反量化成 BF16;
- 前向、反向都在 BF16 里算;
- 只对 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.0116 | 0.0199 | 0.0121 |
| FP4 E2M1 | 0.0112 | 0.0126 | 0.0114 |
| NF4 | 0.0085 | 0.0108 | 0.0087 |
NF4 在三种数据上都最好;slides 第 45 页:NF4 比 FP4 效果更好。
6.3 双重量化
slides 第 39–40 页:块越小量化越准,但每块一个 FP32 的 scale 摊到每个参数上就是 32/64 = 0.5 位——4 位的八分之一。双重量化把这些 scale 再量化一次:减去均值后每 256 个 scale 一块,用 FP8 存,每块一个 FP32 的二级 scale。账是
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.4 | 3.0 |
| NF4 + 双重量化的基座,不微调 | 576.0 | 3.3 |
| LoRA(全精度基座) | 7.3 | 523.1 |
| QLoRA(NF4 + 双重量化) | 6.0 | 531.4 |
| QLoRA(NF4,不做双重量化) | 6.9 | 513.6 |
| LoRA(INT4 absmax 的基座) | 7.8 | 518.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
- 全量微调贵在梯度和优化器状态(每个参数 8–16 字节);PEFT 冻结基座,只为很少的参数付这笔钱;激活的显存省不掉。
- PEFT 有三类:选择性(BitFit、只调归一化)、重参数化(LoRA)、加性(adapter、软提示)。
- LoRA:ΔW = BA,r ≪ d;全量微调的更新本身近似低秩;B = 0 初始化让微调从原模型出发;r = 8 左右就够;推理时合并、没有额外开销;不合并时可以在一个批里服务很多适配器。
- CIAT 最早提出这种低秩的”补丁”:嵌入 adapter + 与子层并行的层 adapter,并且加在 FFN 和嵌入上。
- QLoRA = NF4(按正态分位数放格子)+ 双重量化(scale 再量化,4.5 → 4.127 位)+ 分页优化器 + BF16 计算,效果与 16 位 LoRA 持平。
- 局限:PEFT 不自动减少遗忘;提示调优在小模型上很弱;LoRA 加在哪里、学习率多少都要调。
9. 常见误区 · Common pitfalls
- “LoRA 能把训练显存降低几十倍。” 它省的是梯度和优化器状态;权重本身和激活还在(QLoRA 再压权重,梯度检查点再压激活)。
- “B 和 A 都初始化成随机数更好。” 那样一开始 ΔW ≠ 0,微调从一个被扰动过的模型出发;B = 0 保证从原模型出发。
- “秩越大越好。” r = 8 左右之后几乎不再改善(02 号脚本第 3 部分);但秩太小(1、2)确实不够。
- “LoRA 只能加在注意力上。” 那是 LoRA 论文的默认;改写知识的任务上加在 FFN / 所有线性层更好,CIAT 和 QLoRA 都这样做。
- “原地切换适配器(W − BA + B’A’)没有代价。” 舍入误差会累积,BF16 存的权重尤其明显;保留原始 W 重新合并,或者不合并。
- “PEFT 不会遗忘。” 在我们的实验里所有学会了新任务的 PEFT 方法都比全量微调忘得多(03 号脚本)。
- “NF4 就是 FP4。” NF4 的格子是按正态分位数放的,不是浮点数的指数间隔;同样 4 位,误差小约 25%。
- “双重量化会明显增加误差。” scale 本身的相对误差只有 1e-4 量级。
10. 练习 · Exercises
- LLaMA-3-8B(d = 4096、32 层、KV 维 1024、FFN 14336)上,r = 16 的 LoRA 只加在 Wq、Wk、Wv、Wo 上,有多少个参数?存成 BF16 多大?
- 用 Adam 微调一个 70B 模型,全量(每个参数 16 字节)要多少显存?QLoRA 的基座要多少?
- 推导 ∂L/∂A 和 ∂L/∂B。为什么 B = 0 时第一步 A 不动?如果反过来 A = 0、B 随机,会怎样?
- 一个 4096 × 4096 的矩阵,全量微调的 ΔW 前 8 个奇异值占 90% 的能量。用 r = 8 的 LoRA 至少能达到”ΔW 的 90%“吗?为什么不一定?
- 两个适配器的 r 都是 8、都加在同一组层上,能不能把它们”相加”成一个适配器?得到的秩是多少?
- 构造一个 2 位(4 个值)的 NormalFloat 表:按本讲的规则,正半边几个值、负半边几个、要不要 0?
- 双重量化里,如果第二级的块从 256 改成 64,每个参数多少位?
- 为什么 QLoRA 要”存 4 位、算 BF16”,而不是直接用 4 位做矩阵乘法?
- 为什么说提示调优几乎不会遗忘?它适合什么样的任务?
- 一个服务器要同时服务 1000 个 LoRA 适配器(r = 16、所有线性层、LLaMA-3-8B),适配器一共占多少显存?和 1000 个合并好的模型比呢?
参考答案
- 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。
- 全量:70e9 × 16 = 1120 GB(还不算激活)。QLoRA 基座:70e9 × 4.127 / 8 ≈ 36 GB。
- 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,永远不动)。
- 不一定。截断 SVD 给出的是”对全量微调的 ΔW 最好的秩 8 近似”;LoRA 是从头用梯度下降训练 A、B,走的路径、学到的子空间都可能不同(它不知道全量微调的 ΔW 是什么)。我们的实验里两者效果相近(9.6 vs 9.5),但这不是定理。
- 可以:B₁A₁ + B₂A₂ = [B₁ B₂]·[A₁; A₂],是一个 r = 16 的 LoRA(秩最多 16)。所以”合并两个适配器”不会让秩保持在 8。
- 2^(b−1) = 2 个正值、2^(b−1) − 1 = 1 个负值、再加一个 0:{−1, 0, a, 1},a 是 N(0, 1) 在对应概率点上的分位数归一化后的值。
- 4 + 8/64 + 32/(64·64) = 4.1328 位,比 256 时多 0.0059 位。
- 现有的张量核心不支持”4 位 NF4 × BF16”的矩阵乘(NF4 是查表格式,不是硬件数值类型);反量化成 BF16 再乘,数值稳定、实现简单,代价是一次反量化(可以融合进内核)。训练的瓶颈主要在显存,不在这一点计算。
- 它不改变模型的任何参数,旧任务只要不加软提示就和原来一模一样。适合”只需要改变模型行为方式、不需要新知识”的任务(格式、风格、任务指令),在大模型上效果更好。
- 每个适配器: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 图、模型并行、混合模型的显存分配)。