第9讲 · Stanford CS224N:高效训练(Efficient Training)
| 讲师 | Shikhar Murty |
| 课程 | Stanford CS224N / Ling284: Natural Language Processing with Deep Learning(2024 春季) |
| 视频 | https://www.youtube.com/watch?v=UVX7SYGCKkA | 时长:约 62 分钟 |
| 课程主页 | https://web.stanford.edu/class/cs224n/ |
对应讲次:第9讲(Stanford CS224N 自然语言处理)
制作模型:deepseek-flash(vb compose,据 run.log)
版权说明:课程名称、讲义与幻灯片版权归 Stanford CS224N 课程团队所有。随堂提供的官方幻灯片文件为
cs224n-2026-lecture09-peft.pdf,其署名作者为 Diyi Yang。本书为课堂口语记录的中文书面重构,仅供学习参考。
导览
这一讲和前面所有课都不一样:它几乎不涉及自然语言本身,讲的是一台机器到底怎么把一个大模型训出来。用讲师开场的原话说,今天要谈的是"如何在 GPU 上训练大模型,以及这些机器学习系统运作的一些基础"。(参考时间 00:00:37)
① 老师真正想回答的核心问题
老师不是按教科书目录讲的,他抛出的其实是这样一串很具体的问题:
- 训练时数字在计算机里是怎么表示的?为什么这会影响我能不能训得动模型?
- 我拿到一块(或几块)GPU,跑起来就
OutOfMemory,有哪些招可以救? - 多张 GPU 一起训练时,参数、梯度、优化器状态到底各自放在哪?它们怎么同步?
- 如果连"全量微调"都放不下,还能怎么办?
他把前三个问题收进了一条决策流程图,把第四个问题引向了参数高效微调(PEFT)与 LoRA。
② 难点在哪
最容易卡住的地方,是"内存账本"。多数人第一次意识到"优化器也要吃显存"时会很意外(讲师说自己当年也一样惊讶,参考时间 00:14:50)。要把 ZeRO 的 stage 1/2/3 听懂,必须先算清楚:一个参数在混合精度下究竟占多少字节、这些字节分布在几块 GPU 上。
第二个坎是通信与计算的重叠。FSDP 那几张图乍看很乱,但它的本质只是"用通信换显存"——难点在于看清哪一步在通信、哪一步在算、哪一步在释放内存。
③ 读者可能带着的疑问
A. 老师当场提出的问题(他自己的顺序): - fp32 换 fp16 能省一半显存,为什么不能直接把一切都转成 fp16? - 混合精度里既然已经维护了 fp32 的主权重,为什么还会出问题? - 为什么 bf16 就不需要梯度缩放(grad scaler)了? - ZeRO stage 1 到底"免费"在哪? - 什么时候必须上 FSDP?
B. 编者预判的疑问(标注:编者预判):
- 这些系统知识,对写 NLP 作业/做项目到底有什么用?
- LoRA 的 rank、alpha 这两个超参数我该设多少?
- 课上没有展开的 DPO、prompting、适配器这些幻灯片,和这讲是什么关系?
④ 本讲路线
老师的路径是自底向上:先讲最底层的浮点数表示(fp32 / fp16 / bf16)→ 由此引出混合精度训练 → 再从单卡扩到多卡(DDP → ZeRO 1/2/3 → FSDP)→ 补上"模型激活"这块被忽略的显存开支 → 最后用一张决策流程图收口,并顺势进入参数高效微调与 LoRA。
一、从浮点数讲起:为什么它和深度学习有关
(参考时间: 00:02:00)
老师开场先把话题岔到"数字在计算机里怎么存"上,并承诺"很快你就会看到它和深度学习的关系"。
1.1 fp32 的构成
一个 fp32 占 32 位 = 4 字节。对神经网络而言,这意味着每个模型参数要占 4 字节显存。
📷 fp32 的位构成示意图(符号位、指数位、尾数位)
回到原视频 (00:02:50)
它的位被切成三段:
- 符号位:1 位,决定正负;
- 指数部分(老师用绿色标注):决定动态范围——能表示多大的数、多小的数;
- 尾数部分(老师用蓝色标注):决定精度——一个数能被表示得多细。
编者说明:用 LaTeX 写出来,一个浮点数表示为
$$(-1)^{s}\times 2^{e-\text{bias}}\times(1.m)$$
其中 $s$ 为符号位,$e$ 为指数位,$m$ 为尾数位。fp32 的分配是 1 / 8 / 23。
fp32 的范围大、精度也高,代价是每个数吃掉 4 字节。
1.2 fp16:省一半显存,代价是什么?
fp16 只占 16 位 = 2 字节,显存直接砍半。它是怎么做到的?把绿色(范围)和蓝色(精度)都削掉一部分:指数位变少 → 动态范围变小;尾数位变少 → 精度变低。
于是有两个典型后果:
- 范围问题:非常小的数会被直接截断成 0,非常大的数会变成
inf; - 精度问题:出现舍入误差。例如
1.1在 fp16 里会被舍入成1。
老师给了一个"测试各种数据类型属性"的小截图:
📷 用代码查看数据类型的 epsilon、smallest normal 等属性
回到原视频 (00:05:00)
其中两个关键量:
- epsilon(机器精度):最小的数,满足"把它加到 1 上仍不丢精度"。比它还小的数加到 1 上会被直接舍回到 1。
- smallest normal(最小规格化数):该类型能表示的最小正数,比它小的直接变 0。
编者拓展|fp16 与 bf16 的具体数值 对 fp16(1/5/10 分配):机器精度 $\varepsilon = 2^{-10}\approx 0.000977$,最小规格化数 $=2^{-14}\approx 6.1\times10^{-5}$。 对 bf16(1/8/7 分配):机器精度 $\varepsilon = 2^{-7}\approx 0.0078$,最小规格化数与 fp32 相同,约 $1.2\times10^{-38}$。 这正好解释了老师后面那句"小于 6e-5 在 fp16 里会变成 0"。
1.3 为什么 fp16 对训练是灾难
📷 NVIDIA 博客中的梯度分布:超过一半的梯度在 fp16 下会被置零
回到原视频 (00:05:50)
老师引用了一张 NVIDIA 博客里的图,展示训练过程中梯度的分布:半数以上的梯度值小到会超出 fp16 的动态范围,被直接置为 0。这对训练是致命的——大量参数根本拿不到有效更新。
编者说明:这正是"混合精度训练"存在的理由——它不是为了省显存而省,而是为了解决 fp16 表示不了小数值这一物理限制。
二、混合精度训练(Mixed Precision Training)
(参考时间: 00:06:15)
2.1 基本思路:主权重
光用 fp16 不行,光用 fp32 太贵,那就两者都用:在 fp32 里维护一份模型的"主副本"(master weights),每次计算时临时转成 fp16。
流程如下:
graph TD
A["fp32 主权重"] -->|"下行转换 cast"| B["fp16 模型"]
B -->|"前向传播"| C["fp16 激活 / 损失"]
C -->|"反向传播"| D["fp16 梯度"]
D -->|"上行转换 upcast"| E["fp32 梯度"]
E -->|"更新"| A
A -->|"复制回 fp16"| B
具体步骤(老师逐步讲的):
- 在 fp32 中维护一份主权重;
- 取一批数据,把主权重转成 fp16 跑前向传播;
- 在 fp16 里跑反向传播,得到 fp16 梯度;
- 把梯度上转(upcast)到 fp32,用它更新主权重;
- 再把更新后的主权重拷回 fp16 版本。
2.2 这个方案还漏了什么?
老师当场提问:"为什么这个看似合理的方案仍然有问题?"
学生:会不会很慢,因为要在 GPU 和别的地方之间来回拷 32 位的版本?
老师:这是个好点。实践中 IO 通常可以和前向/反向重叠(overlap),所以大多数情况不是问题——除非你的网络非常非常小。
真正的答案是第二个学生的回答:"梯度通常很小,当你把 fp16 算出来的梯度拷回 fp32 时,可能已经把网络带到了你不想去的地方。"
因为回到那张梯度分布图——反向传播的梯度本身就是在 fp16 里算的,其中一多半会先被截断成 0,再拷进 fp32。精度损失已经发生了。
2.3 梯度缩放(Gradient Scaling)
解法是把损失放大再算梯度:
graph TD
A["取一批数据"] --> B["fp16 前向传播"]
B --> C["得到损失"]
C -->|"乘以放大因子(如 1000)"| D["放大后的损失"]
D -->|"fp16 反向传播"| E["放大后的 fp16 梯度"]
E -->|"上转 fp32"| F["fp32 梯度"]
F -->|"除以放大因子"| G["真实 fp32 梯度"]
G -->|"更新"| H["fp32 主权重"]
把损失乘一个大数,梯度分布整体向大值方向平移,落在 fp16 死区里的梯度就大大减少了;算完之后再除回来。
在 PyTorch 里实现只需要:
- 实例化一个
GradScaler对象; - 在
autocast上下文里跑前向和反向; - 把梯度缩放回去,再更新参数。
2.4 缩放因子的麻烦
老师指出,梯度缩放本身带来新的负担:如果放大因子设成 10000 导致出现 NaN,你就得回退到 1000,下一轮还要根据网络动态调整。所以他问:能不能干脆不做梯度缩放?
答案藏在"绿色位"里——缩放之所以必要,是因为 fp16 的动态范围比 fp32 小得多,表示不了很小的数。那位学生的回答一语中的:牺牲精度,换更多的范围。
2.5 bf16(Brain Float 16)
这就是 bf16 的思想:给它和 fp32 完全相同的指数位数(8 位),于是动态范围和 fp32 一样,但精度低得多(尾数只剩 7 位)。重要的是——对神经网络训练来说,这个取舍可以接受。
用 bf16 之后:
- 不再需要 GradScaler;
- 实现只是把前向/反向包进正确的上下文里。
唯一的限制:bf16 不是所有 GPU 都支持,需要较新的 NVIDIA Ampere 架构,如 H100、A100、A6000。老卡就用不了。
编者说明:
torch.cuda.is_bf16_supported()可以检查当前 GPU 是否支持 bf16。
2.6 效果与原因
老师给出一组在单张 A100 上、用 DistilBERT 做情感分类的结果,分几档精度比较:
📷 不同数值精度的训练时间、准确率与显存对比
回到原视频 (00:12:40)
- 最上面是 fp64:表示最"富余",约 25 分钟,准确率不错,但吃显存最多;
- 一路下来到 bf16 混合精度:训练时间大约缩短三分之一,准确率几乎相同(甚至略好一点,老师解释这是半精度带来的某种正则化效果),显存占用则少得多。
为什么能提速? 因为矩阵乘法在半精度下更快——这是硬件层面的加速,不只是省显存。
三、多 GPU 训练:先把内存账本算清楚
(参考时间: 00:14:00)
场景换成多张 GPU,目标是让整个网络在它们上面协同训练。
3.1 单卡上到底存了什么
📷 单 GPU 训练时的模型与优化器内存示意
回到原视频 (00:14:30)
先看单卡(假设做混合精度训练):
- 模型参数:fp16,2 字节/参数;
- 优化器状态:这里要特别强调——优化器也要吃显存。以 Adam 为例,需要存动量(momentum)和方差(variance),每个梯度到来时都要更新它们;而且因为做的是混合精度,这些状态必须用 fp32 存储。
于是"额外 12 字节/参数"的来历是:
| 项 | 精度 | 字节/参数 |
|---|---|---|
| 模型参数 | fp16 | 2 |
| 梯度 | fp16 | 2 |
| 主权重 | fp32 | 4 |
| Adam 动量 | fp32 | 4 |
| Adam 方差 | fp32 | 4 |
| 合计 | 16 |
3.2 DDP(Distributed Data Parallel)
最朴素的多卡方案:切分数据、复制模型。若有 4 张 GPU,就把数据集分成 4 份,每个 worker 保持一份同步的模型副本,各吃一份数据。
graph TD
subgraph GPU0
M0["模型副本"] --> D0["数据分片 0"]
end
subgraph GPU1
M1["模型副本"] --> D1["数据分片 1"]
end
subgraph GPU2
M2["模型副本"] --> D2["数据分片 2"]
end
D0 --> G0["梯度 0"]
D1 --> G1["梯度 1"]
D2 --> G2["梯度 2"]
G0 --> AR["all-reduce:累加并广播梯度"]
G1 --> AR
G2 --> AR
AR --> U["每个 GPU 用相同梯度更新,保持同步"]
流程是:初始时所有模型同步 → 各跑前向(激活不同)→ 各跑反向(梯度不同)→ 同步步骤:在 worker 之间通信梯度。
老师引入本讲的第一个 MPI 原语——all-reduce:把多块 GPU 上的信息合并,再分发回所有 GPU。
- 通信开销:每参数 2 字节(因为梯度是 fp16)。
- 同步之后,每个优化器都拿到完整梯度,更新后模型继续保持同步。
问题:这种方案显存扩展性很差——上面那 16 字节/参数的账,是每一块 GPU 都要完整存一份的。
四、ZeRO:消除冗余
(参考时间: 00:18:20)
改进方向是把优化器状态、梯度、参数分片(shard)到不同 GPU 上。这套技术叫 ZeRO(Zero Redundancy Optimizer),由微软作为 DeepSpeed 项目的一部分发布。
4.1 ZeRO Stage 1:只分片优化器状态
只分片"绿色的那部分"(优化器状态),做法是:
- 每块 GPU 仍持有完整的 fp16 参数和它自己数据的梯度;
- 但只持有一份分片的优化器状态;
- 每块 GPU 只负责更新它那份分片对应的参数。
graph TD
D["各 GPU 各自的梯度"] --> RS["reduce-scatter:把梯度分片发给对应负责的 GPU"]
RS --> U["各 GPU 用自己那份优化器状态更新参数分片"]
U --> AG["all-gather:把更新后的参数广播回所有 GPU"]
AG --> SYNC["恢复同步"]
老师引入了第二个 MPI 原语——reduce-scatter:每块 GPU 手上都有自己数据的完整梯度,于是把属于别人的那部分块发给别人。
例如 8 个参数的模型分给 4 张卡:以前每块卡都要维护全部 8 个参数的优化器状态,现在每块卡只需维护 2 个参数的优化器状态。reduce-scatter 后,每块卡只拿到自己负责的那 2 个参数的完整梯度,就用这部分优化器状态更新这 2 个参数;最后用 all-gather 同步。
为什么这是"免费的"?
关键等式:all-reduce = reduce-scatter + all-gather。
而 DDP 本来就要做一次 all-reduce。所以把 DDP 换成"reduce-scatter + all-gather",通信量完全相同,却省下了显存。
老师的原话大意:这没什么好犹豫的,直接用就对了——省显存,又不增加通信开销。
4.2 ZeRO Stage 2:连梯度也分片
除了优化器状态,再把梯度也分片。稍微复杂一点,因为每个 worker 仍然需要自己数据切片对应的梯度,而单卡的内存现在只够放一小部分参数的梯度。
做法是:从不实例化完整梯度向量。在反向传播过程中,每当某块 GPU 收到某个参数的梯度,就临时创建一个向量、算完梯度后立刻发走、立刻销毁。
graph TD
A["各 worker 逐层反向传播"] --> B["算出第 J 层参数的梯度"]
B --> C["立刻发给负责第 J 层的那个 worker"]
C --> D["释放刚创建的临时内存"]
D --> E["负责该层的 worker 用收到的完整梯度 + 自己的优化器状态更新参数分片"]
E --> F["最后 all-gather 同步"]
这是个 reduce 操作,本质和 reduce-scatter 差别不大。由于它是 reduce + all-gather,相对 DDP 仍然几乎没有额外通信开销。
4.3 ZeRO Stage 3 / FSDP:连参数也分片
如果连优化器状态都解决不了——模型太大了,连一份参数都放不进单张卡——那就只能把模型参数本身也切开(蓝色部分)。这就是 ZeRO Stage 3,也叫 FSDP(Fully Sharded Data Parallel)。
代价是:这次不再是免费的,会有额外的通信开销。
五、深入 FSDP:一次前向 + 一次反向是怎么跑的
(参考时间: 00:29:00)
FSDP 反而比 stage 1/2 容易理解,因为每一步都必须通信,没有藏着掖着。
5.1 准备阶段:把模型切成"FSDP 单元"
📷 FSDP 将模型划分为多个 FSDP 单元并打平成 flat parameter 分片
回到原视频 (00:29:30)
- 把整个模型转成若干个 FSDP 单元(图中是三个单元);
- 每个单元再转成一种叫 flat parameter 的数据结构;
- 把这个扁平化后的参数平均分给每块 GPU(例如 16 块卡、14 个参数,会补一些 padding 让它们能整除)。
一句话:创造数据结构、把参数分下去,每块 GPU 只拿到一部分参数。
5.2 前向传播
任何一块 GPU 都不拥有完整的某一层参数。以第 4 层为例:
graph TD
A["要算第 4 层"] -->|"all-gather"| B["集齐第 4 层所有参数分片"]
B --> C["前向传播"]
C --> D["第 4 层不再需要 → 丢弃参数分片,释放显存"]
D --> E["继续下一层(重复 all-gather)"]
先 all-gather 把第 4 层的所有分片凑齐,跑完前向,然后因为这一层的参数已经用不上了,就丢弃它们——这是 FSDP 省显存的核心。
5.3 反向传播
反向时的逻辑对称,但多一步:
graph TD
A["回到第 4 层,拿到上游梯度"] -->|"all-gather"| B["再次凑齐第 4 层参数"]
B --> C["反向传播,算出各 GPU 自己数据的梯度"]
C -->|"reduce-scatter"| D["把完整梯度发给负责第 4 层对应分片的 GPU"]
D --> E["各 GPU 用完整梯度更新自己的参数分片"]
E --> F["同步"]
因为每块 GPU 的数据点不同,梯度也不同,所以要用 reduce-scatter 把完整梯度送往"负责这块参数的那张卡"。
总开销:两次 all-gather + 一次 reduce-scatter。这比 stage 1/2 重得多,但当你的显存连模型都装不下时,这就是必须付的代价。
5.4 四种方案对比
| 方案 | 分片内容 | 通信开销 |
|---|---|---|
| DDP | 只切数据 | 一次 all-reduce(2 字节/参数) |
| ZeRO Stage 1 | 优化器状态 | ≈ all-reduce,免费 |
| ZeRO Stage 2 | 优化器状态 + 梯度 | ≈ reduce + all-gather,基本免费 |
| ZeRO Stage 3 / FSDP | 参数 + 梯度 + 优化器状态 | 2×all-gather + reduce-scatter,开销明显增加 |
六、别忘了"模型激活"这块显存
(参考时间: 00:35:00)
老师纠正了前面为了简化而撒的一个"谎"。之前只算了模型参数、梯度和优化器状态,但还有一项被漏掉了:模型激活值(activations)。
- 反向传播时需要用到激活值,所以前向时必须存下来;
- 激活值占用随 batch size 线性增长——这就是为什么你把 batch size 往上调,GPU 迟早会说"装不下了";
- 如果做混合精度,激活值是 fp16 或 bf16 的,但依然随 batch size 增长。
关键警告:前面讲的所有分片技术(ZeRO、FSDP)都不能分担模型激活。
七、一张图收口:到底该怎么做
(参考时间: 00:36:00)
老师把所有内容压缩成一张决策流程图,并半开玩笑地说:"如果你整节课都在打瞌睡,现在是醒过来看一眼这张图的时候了。"(参考时间 00:53:05)
graph TD
A["开始微调"] --> B["永远先用混合精度训练"]
B --> C{"用的是 Ampere 之后的新卡吗?"}
C -->|"是(H100/A100/A6000)"| D["用 bf16"]
C -->|"否"| E["用 fp16 + 梯度缩放"]
D --> F{"batch size = 1 能放进单张 GPU 吗?"}
E --> F
F -->|"能"| G["尝试更大的 batch size<br/>并用 ZeRO Stage 2(免费)"]
F -->|"不能"| H["尝试 ZeRO Stage 3<br/>(分片模型参数)"]
H --> I{"还是 OOM?"}
I -->|"是"| J["尝试梯度检查点 / 激活检查点"]
J --> K{"还是不行?"}
K -->|"是"| L["放弃全量微调<br/>改用参数高效微调(PEFT)"]
要点重申:
- 永远用混合精度训练,几乎看不到性能(泛化/准确率)损失;
- 有新架构的卡就用 bf16,它就是更好,可以用
torch命令检查是否支持; - batch size = 1 装得下:试着加大 batch size,并且总是用 ZeRO Stage 2(免费);
- batch size = 1 装不下(且有多卡):试 ZeRO Stage 3,必要时加梯度检查点;
- 还不行,才轮到 PEFT。
学生提问:这张流程是不是假设有多张 GPU?
老师:对,以上全部只适用于多卡。单卡的话,你只能重度量化模型,而且即便那样,我也认为你很难微调真正大的模型。
八、为什么我们需要"高效适配"
(参考时间: 00:38:30)
🖼️ 第 1 页幻灯片 · Natural Language Processing
到这里,话题从"系统技巧"转向"参数高效微调"。老师先讲了动机,理由分三层。
📷 当前 AI 研究更重准确率而非效率:顶会论文中追求 accuracy 的比例
回到原视频 (00:39:10)
8.1 效率被系统性忽视
机器学习——尤其是 NLP——有一个惯性:重准确率、轻效率。老师给出一张统计图:从顶级 AI 会议的 60 篇论文样本看,绝大多数论文的首要贡献是"更准的模型",只有很少一部分是"同等准确率但更高效"。(这张幻灯片由 Benji Xie 和 Regina Wang 提供。)
这种趋势可能正在导致某种"单一文化(monoculture)",也许正因如此,我们才更应该关注效率。
8.2 隐藏的环境成本
老师提到读到的一份报告:训练 GPT-3 的碳排放当量约 110 万吨,据其估算相当于一座燃煤电厂连续运转 10 小时。
编者说明:老师原话是"cost of training GPT-3 was equivalent to 1.1 million tons of carbon emission"。常见的公开估计(如 Patterson et al., 2021)要小得多(数百吨 CO₂ 量级),此处数字以讲师课堂表述为准,见文末"已知疑点"。
更贴近生活的一个例子来自斯坦福的强化学习课(CS234):
📷 斯坦福 CS234 作业中不同算法的功耗差异,节约 880 千瓦时
回到原视频 (00:41:40)
"在斯坦福,某门强化学习课上 200 多名学生被要求实现一些常见算法。虽然两个算法的表现一样好,其中一个耗电高得多。研究者估算,如果所有学生都用了更高效的那个算法,集体功耗可减少 880 千瓦时——大约是一个普通美国家庭一个月的用电量。"
8.3 算力集中与价值偏置
老师还提出两点更深层的担忧:
- 如果训练成本持续攀升,AI 开发会集中在几家资金雄厚的机构(尤其是工业界)手里,"作为学生,我们就做不起了";
- 如果只有少数玩家在训练和微调模型,他们可能按自己的价值体系去偏置模型,而不是反映更广泛的公众价值。
九、从全量微调到参数高效微调
(参考时间: 00:43:20)
9.1 先回顾全量微调
📷 全量微调:为每个下游任务更新并存储一整套参数
回到原视频 (00:43:40)
假设有一个大型自回归语言模型 $P_\phi(y\mid x)$(例如基于 Transformer 的 GPT)。你想把它用到下游任务:
- 摘要(summarization);
- 语义解析(semantic parsing,例如自然语言转 SQL);
- 段落问答(QA)。
你会收集一批 $(x, y)$ 数据对,然后做全量微调——按损失函数的梯度更新所有模型参数:
$$\max_{\phi}\ \sum_{(x,y)}\ \sum_{t=1}^{|y|} \log P_\phi\big(y_t \mid x, y_{ 问题在于: 📷 全量微调 vs 参数高效微调 不要更新全部参数,只更新很小一部分。这样做的收益是: 🖼️ 第 38 页幻灯片 · 2. Different perspectives to think about PEFT (参考时间: 00:44:40) 📷 LoRA 的两条路径:冻结预训练权重,只训练低秩增量矩阵 LoRA(Low-Rank Adaptation)来自一个观察:对大语言模型做微调时,梯度在几何结构上往往具有很低的内在秩。 老师这里问大家是否记得"秩"和"奇异值分解"——这是线性代数的基础。 既然更新是低秩的,那就不去微调完整的参数矩阵,而是为模型中每一个满秩矩阵,只微调一个低秩矩阵。 📷 低秩参数化的更新矩阵 设有一个预训练权重矩阵 $W_0 \in \mathbb{R}^{d\times k}$,LoRA 约束它的更新具有如下低秩分解形式: $$W = W_0 + \Delta W = W_0 + \alpha BA$$ 其中 $B \in \mathbb{R}^{d\times r}$,$A \in \mathbb{R}^{r\times k}$,且 $r \ll \min(d,k)$。 编者说明:矩阵乘积 $BA$ 的秩至多为 $r$,所以 $\Delta W$ 被强制活得"很低维"。这正是把"学一个 $d\times k$ 的更新"换成"学两个小矩阵"的代价与回报所在。 (1)它是一个可调的旋钮。 当你增大可训练参数数量(即增大 $r$),LoRA 的训练就收敛到训练原始模型。 (2)没有额外的推理延迟。 切换任务时,把 $BA$ 减掉就恢复了 $W_0$,再加上另一个 $B'A'$ 即可。存储这些低秩矩阵的成本远低于存一整套 $\Delta$。 用在哪里? 老师建议应用到自注意力模块中的权重矩阵上。更具体的经验法则是:对把隐藏状态转成 query 的矩阵、以及转成 value 的矩阵应用 LoRA,这样总体上能得到最好的效果。 📷 LoRA 的实现示例:在前向结果上加一个可训练的低秩偏移项 老师说,落实到代码其实很简单: 学生提问:代码倒数第二行里的 $\alpha$ 在哪里定义?还是随便放? 老师:你自己定义它。设为 1 意味着"预训练知识和新任务知识各占一半",这是最常用的默认值;觉得任务很新,就设大于 1;不想改动太多模型,就设小于 1。 📷 LoRA 在 GPT-3 175B 上的实践结果:匹配或超过全量微调基线 在 GPT-3 级别的大模型上: 关于最优 rank:即使是一个很小的 rank,也能得到相当高的性能。而这个小 rank 值远小于当今大多数模型的隐藏状态维度。 编者拓展|LoRA 的后续演进(幻灯片提及但课堂未展开)
官方幻灯片里还给出了两个后续方向:QLoRA 把 Transformer 量化到 4-bit 精度并使用分页优化器(paged optimizer),引入对正态分布权重信息论最优的 4-bit NormalFloat(NF4)数据类型;以及最新的研究发现,即使 rank 低到 1,LoRA 也能匹配全量微调(在 Qwen3-8b-base 的 DeepMath 上),且表现出更宽的有效学习率区间。这些内容本讲并未口头讲解,特此注明供延伸阅读。 (参考时间: 00:52:40) 老师的总结顺序是:浮点数 → 混合精度训练 → 多 GPU 训练(DDP、FSDP)→ LoRA,然后回到那张决策流程图。 对 LoRA,他给出的默认超参数起点是: "照着这个来,你的微调结果应该就还过得去。" (参考时间: 00:54:45) 一位学生请老师回到幻灯片 48(FSDP 通信图)再走一遍。这段补充很重要,展开如下。 这张图想说明的是:只要模型足够大,一次前向传播所需的时间里,就足以预取(prefetch)下一层的全部参数,于是通信开销其实"没那么糟"。 这是一个通用原则——尽量把 GPU 喂满,并把通信与额外计算重叠。顺带一提,PyTorch 默认就会做这件事。 图中 0、1、2 是三个不同的 FSDP 单元: 🖼️ 第 47 页幻灯片 · LoRA: low rank adaptation (Hu et al., 2021) 老师反复强调这就是"把 all-gather 操作与前向叠加"。 反向稍难,因为要先 all-gather 参数,再 reduce-scatter 梯度: 以 10 层网络为例:要算第 10 层的完整梯度,先 all-gather 拿到第 10 层所有参数,然后各 GPU 反向得到不同梯度,再用 reduce-scatter 把梯度合并、切分给正确的 GPU。reduce-scatter 同样可以和反向过程重叠。 此外,还要在固定间隔释放显存:比如第 1 层前向跑完、进入第 2 层后,第 1 层的东西就不再需要,直接释放。 这也解释了为什么 PyTorch 的 FSDP 封装需要你提供一个分片策略,而它非常依赖具体架构。因为现在大家都用 Transformer,社区已经为 Transformer 手工调好了分片策略;但如果你在课程项目里搞了个全新架构(比如次二次注意力),它就未必高效了——因为你没有配套的分片策略。 学生追问:你提到前向之后可以丢弃权重,那反向时要一层层重新流式加载回来吗?还是有缓存? 老师:系统内部可能有些缓存,但对用户而言,从显存占用上看就是"全扔了",然后每层再重新流式加载。所以合理分片非常重要:如果每个相邻层都被切到多块 GPU 上,你就会一直在通信;反之,如果你一次 all-gather 把后面几层都装进来,就能减少通信。 说明:本讲(第十二讲)的课堂内容为"高效训练"(浮点数、混合精度、DDP/ZeRO/FSDP、LoRA 等)。随堂提供的官方幻灯片文件 【第 1 页】 封面:Natural Language Processing with Deep Learning,CS224N/Ling284,Diyi Yang,"Lecture 9: Efficient Adaptation"。 【第 2 页】 目录:DPO(15 分钟)、人类偏好数据(5 分钟)、Prompting、PEFT 导论、剪枝/子网络、LoRA、Prompt-tuning、Adapters、其他适配方法。 【第 3 页】 RL(PPO)相当复杂:计算昂贵且棘手、要拟合价值函数、在线采样慢、对超参数敏感。 【第 4 页】 去掉 RLHF 中的"RL"——DPO:RLHF 目标函数及其闭式解,通过对数变换导出奖励模型。 【第 5 页】 DPO 汇总:导出的奖励模型 + 基于 Bradley-Terry 偏好模型的最终损失 $J_{DPO}$;并说明 $\log Z$ 项因损失只衡量奖励之差而抵消。 【第 6 页】 DPO 优于以往方法:复杂的 RL 部分可替换为简单的加权 MLE 目标;出现 KTO、IPO 等变体;TL;DR 摘要相对人工摘要的胜率(由 GPT-4 充当评委)。 【第 7 页】 开源 RLHF 如今大多数已不是 RL:开源大模型几乎都用 DPO,且效果不错。 【第 8 页】 改进 RLHF 中的"RL"——GRPO(Shao et al., DeepSeekMath, 2024)。 【第 9 页】 RLHF 的数据从哪来:标注常由海外低薪工人获得。 【第 10 页】 标注来自哪里:需警惕标注者偏见渗入语言模型(Santurkar et al. 2023)。 【第 11 页】 偏好微调可能产生非预期影响:Starling 7B 奖励模型在"你来自哪里"问题上的表现(Ryan et al., 2024)。 【第 12 页】 RLHF 的下一步:仍是探索不足、快速发展的领域;比指令微调走得更远但数据昂贵;RL from AI feedback、在自己的输出上微调等方向;以及规模与幻觉等未必能靠 RLHF 解决的局限。 【第 13 页】 大模型涌现能力之 GPT(2018,117M):12 层 Transformer 解码器;在 BooksCorpus(7000 多本书,4.6GB 文本)上训练;证明大规模语言建模是有效的预训练技术。 【第 14 页】 GPT-2(2019,1.5B):架构相同只是更大;数据从 4GB 增至 40GB(WebText),通过 Reddit 上至少 3 个赞的链接抓取。 【第 15 页】 GPT-2 的关键涌现能力是零样本学习:通过指定正确的序列预测问题、比较序列概率(Winograd Schema Challenge)来完成多任务。 【第 16 页】 零样本学习的创造力:在 CNN/DailyMail 上做摘要,用 "TL;DR:" 提示,零样本即可击败当时的 SOTA。 【第 17 页】 GPT-3(2020,175B):规模从 1.5B 增到 175B,数据从 40GB 增到 600GB 以上。 【第 18 页】 涌现的少样本学习:在示例前拼接任务样例进行"上下文学习"(in-context learning),不做梯度更新。 【第 19 页】 少样本学习示意:Zero-shot。 【第 20 页】 少样本学习示意:One-shot。 【第 21 页】 少样本学习示意:Few-shot。 【第 22 页】 少样本学习是模型规模的涌现属性:合成的"单词还原"任务(循环字母、随机插入、反写),100-shot。 【第 23 页】 第 1 部分:Prompting——传统微调 vs 零/少样本提示的对比。 【第 24 页】 提示的局限:需要多步推理的较难任务即使对大模型也太难(人类在这些任务上也吃力);解法是改变提示。 【第 25 页】 思维链(Chain-of-Thought)提示(Wei et al., 2022;另见 Nye et al., 2021)。 【第 26 页】 思维链提示是模型规模的涌现属性:中学数学应用题。 【第 27 页】 思维链提示:是否一定要给推理示例?能否直接让模型自己推理? 【第 28 页】 零样本思维链提示:"Let's think step by step"(Kojima et al., 2022)。 【第 29 页】 零样本思维链提示:手写 CoT 仍然更好,但零样本 CoT 已大幅超过普通零样本。 【第 30 页】 零样本思维链提示:由 LM 自动设计的提示(Zhou et al., 2022)。 【第 31 页】 提示的敏感性与不一致性:分类与选择任务中随机示例的影响(Min et al., 2022);输出不一致(Moore et al., 2024)。 【第 32 页】 "提示工程"这门新的黑魔法:用 Google 代码头生成更"专业"的代码;让模型推理反而可能引入偏见与毒性(Shaikh et al., 2023);越狱(jailbreaking)示例。 【第 33 页】 "提示工程"黑魔法(续)。 【第 34 页】 基于提示的学习的缺点:① 效率低(每次预测都要处理提示);② 性能差于微调;③ 对措辞、示例顺序敏感;④ 模型究竟从提示里学到了什么并不清楚(随机标签也有效)。 【第 35 页】(正文已用)从微调到参数高效微调:全量微调更新所有参数,PEFT 只更新一小部分参数。 【第 36 页】(正文已用)为什么需要高效适配:重准确率轻效率的范式、隐藏的环境成本、算力集中趋势。 【第 37 页】(正文已用)连一门课都有影响:CS234 的例子,节约 880 千瓦时。 【第 38 页】 思考 PEFT 的三种视角:参数(Parameter)、输入(Input)、函数(Function)。部分幻灯片与示例改编自 Ruder、Pfeiffer、Vulić 在 EMNLP 2022 的教程 "Modular and Parameter-Efficient Fine-Tuning for NLP Models"(https://www.modulardeeplearning.com/)。 【第 39 页】 从参数视角看适配:稀疏子网络、低秩组合。 【第 40 页】 稀疏子网络(Han et al., 2017):常见归纳偏置是稀疏性;最常用的稀疏化方法是剪枝;剪枝可看作应用一个二元掩码 $b\in\{0,1\}^\theta$ 得到子网络;最常见的剪枝准则是权重大小。 【第 41 页】 剪枝:去掉一部分最小幅值的权重,再对未剪枝的权重重新训练;多次迭代剪枝更常见(Frankle & Carbin, 2019)。一次性剪枝 vs 迭代剪枝。 【第 42 页】 剪枝与二元掩码:可把剪枝视为给参数加一个任务特定向量 $\phi$,$f'_\theta = f_\theta + \phi$;若要最终模型稀疏,可与掩码做逐元素乘(Hadamard 积)$f'_\theta = f_\theta \circ b + \phi$;Diff pruning 仅依据 $\phi$ 的幅值剪枝(Guo et al., 2021)。 【第 43 页】 彩票假说(Lottery Ticket Hypothesis):稠密随机初始化网络中存在子网络("中奖彩票"),单独训练可达到与原始网络相当的精度;在 RL、NLP、CV 中均得到验证;在 BERT 等预训练模型中也找到了中奖彩票;稀疏率从 40%(SQuAD)到 90%(QQP、WNLI);在掩码语言建模这类通用任务上训练的子网络迁移最好。 【第 44 页】 从参数视角看适配(续):低秩组合(对应第 45 页起的 LoRA)。 【第 45 页】(正文已用)回顾全量微调。 【第 46 页】(正文已用)LoRA:低秩适配(Hu et al., 2021)。 【第 47 页】(正文已用)LoRA:核心思想是用更小的参数集 $\Theta$ 编码任务特定增量。 【第 48 页】(正文已用)低秩参数化的更新矩阵。 【第 49 页】(正文已用)低秩参数化的更新矩阵(续):增大可训练参数会收敛到原始模型;无额外推理延迟;常用于自注意力权重矩阵。 【第 50 页】(正文已用)LoRA 的实现示例(来源:lightning.ai 教程)。 【第 51 页】(正文已用)LoRA 实践:扩展到 GPT-3 175B。 【第 52 页】 理解低秩适配(Understanding low-rank adaptation)。 【第 53 页】 从 LoRA 到 QLoRA:把 Transformer 量化到 4-bit 精度并使用分页优化器;引入信息论最优的 4-bit NormalFloat(NF4)数据类型(Dettmers et al., 2023)。 【第 54 页】 LoRA 在 RL 上也能匹配全量微调,即使 rank 低到 1:Qwen3-8b-base 上的 DeepMath 曲线,LoRA 展现出更宽的有效学习率范围并达到与全量微调相同的峰值。 【第 55 页】 从输入视角看适配:可学习的前缀参数(Li & Liang, 2021;Lester et al., 2021)。 【第 56 页】 Prefix-Tuning(Li & Liang, 2021):加入一段前缀参数并冻结所有预训练参数;前缀是一段连续的、任务特定的向量,被模型像真实单词一样处理,即"虚拟 token";优点是推理时一个 batch 中每个元素可以跑不同的微调模型。 【第 57 页】 Prompt-Tuning(Lester et al., 2021):学习"软提示"来条件化冻结的语言模型;在输入前拼接虚拟 token,只学习这些特殊 token 的嵌入。 【第 58 页】 Prompt tuning 只有在大规模下才有效:标准模型微调性能强但每个任务都要单独存一份模型;随着规模增大,prompt tuning 的质量可追平模型微调(Lester et al., 2021)。 【第 59 页】 从函数视角看适配:函数组合,用新的任务特定函数增强模型函数,最常见于多任务学习中不同任务的模块组合。 【第 60 页】 Adapter(Houlsby et al., 2019):在预训练模型的层间插入新函数 $f_\phi$;一个 Transformer 层中的 adapter 由前馈下投影 $W_D\in\mathbb{R}^{k\times d}$、前馈上投影 $W_U\in\mathbb{R}^{d\times k}$ 组成,$f_\phi(x)=W_U(\sigma(W_D x))$。 【第 61 页】 Adapter(续):通常放在多头注意力之后和/或前馈层之后;多数方法采用这种带线性层的瓶颈设计。 【第 62 页】 准确率与"任务特定可训练参数数量"的权衡:曲线给出 GLUE 上九项任务的第 20、50、80 百分位;基于 adapter 的微调用少两个数量级的可训练参数达到接近全量微调的性能。 【第 63 页】 语言 adapter?任务知识 ≈ 语言知识:adapter 学习使底层模型更适合某任务或某语言的变换;用掩码语言建模(MLM)可以为英语、克丘亚语等学习语言特定的变换。 【第 64 页】 用 adapter 做英语方言适配:把在标准美式英语上训练的大模型适配到不同英语方言(Held et al., 2023;Liu et al., 2023);分为任务 adapter 与方言 adapter。 【第 65 页】 统一视角(He et al., 2022):LoRA、prefix tuning、adapters 可以用相近的函数形式表达——都是在修改模型的隐藏表示 $h$;稀疏性、结构、低秩近似、重缩放等性质也可在多种设定下应用与组合。 【第 66 页】 性能对比:prompt tuning 因容量有限而逊于其他方法;adapter 性能更好但引入更多参数。 【第 67 页】 其他(高效)适配变体:用知识蒸馏获得更小的模型;另见 Gist tokens(Wu et al., 2024)、ReFT(Wu et al., 2024)等;图为知识蒸馏的通用教师—学生框架。 以下是编者在本讲整理过程中存疑、或无法在课堂上当场判定的地方,逐条列出并注明依据或时间点。 课程资料与视频内容不匹配(重要):随堂提供的官方幻灯片 GPT-3 碳排放数字存疑:讲师称"训练 GPT-3 的碳排放当量约 110 万吨,相当于燃煤电厂连续运转 10 小时"(参考时间 00:41:40)。章节号常见公开估计(如 Patterson et al., 2021)在数百吨 CO₂ 量级,与 110 万吨差别很大。此处按讲师课堂表述记录,未能当场核实,读者请以权威文献为准。 口误与自我更正:讲师在讲 fp32 时先说"32 bytes",随即更正为"32 bits(=4 bytes)"(参考时间 00:02:30)。本书采用更正后的说法。 ASR(语音识别)可能的同音错词:字幕中 "Laura" 应指 LoRA;"zero" 应指 ZeRO;"fsdp" 应指 FSDP;"B float 16" 应指 bfloat16;"DDP" 指 Distributed Data Parallel;"reduced scatter" 应指 reduce-scatter;"shot/shotting" 应指 shard/sharding(分片)。这些均在正文中按标准术语改写。 具体数值:讲师提到 fp16 中"小于 6e-5 会被舍入为 0"(参考时间 00:11:15)。这与 fp16 最小规格化数 $2^{-14}\approx6.1\times10^{-5}$ 相符,编者判定无误。 一名学生的追问未被完全记录:在 00:12:31 附近,有学生问"为什么精度更低但位数相同"之类的问题,讲师回应 "it's B 6 and oh never mind sorry",随后转回结果表。该处字幕不完整,对话的确切问题与回答内容无法确定。 幻灯片 48 的时间锚点:这段内容出现在正文的 LoRA 小节(本讲幻灯片第 48 页对应的是"低秩参数化更新矩阵"),但答疑时讲师也用它讲解 FSDP 通信重叠图。编者在两处分别引用同一页码,属正常复用——正文中该页只插入一次,答疑段落引用的是同一页。 "优化器也要吃显存"的惊讶表述:讲师说这是他几年前第一次看到时很意外的事(参考时间 00:14:50)。这属于讲师的个人经历叙述,编者按原意保留,未加改动。 🖼️ 第 15 页幻灯片 · One key emergent ability in GPT-2 Radford et al., 2019 is zero-shot learning: the ability to do 下面这些页来自官方幻灯片,但课堂录制里没有对应的展示画面。由 🖼️ 第 2 页幻灯片 · Overview 🖼️ 第 3 页幻灯片 · RL (PPO) can be quite complex!!! 🖼️ 第 4 页幻灯片 · Removing the ‘RL’ from RLHF --- DPO 🖼️ 第 5 页幻灯片 · Putting it together for DPO 🖼️ 第 6 页幻灯片 · DPO outperforms prior methods 🖼️ 第 7 页幻灯片 · Open source RLHF is now mostly (not RL) 🖼️ 第 8 页幻灯片 · Improving the “RL” from RLHF --- GRPO 🖼️ 第 9 页幻灯片 · Where does the RLHF data come from? 🖼️ 第 10 页幻灯片 · Where does the label come from? 🖼️ 第 11 页幻灯片 · 12 🖼️ 第 12 页幻灯片 · What’s next for RLHF? 🖼️ 第 13 页幻灯片 · Let’s revisit the Generative Pretrained Transformer (GPT) 🖼️ 第 14 页幻灯片 · Emergent abilities of large language models: GPT-2 (2019) 🖼️ 第 16 页幻灯片 · Emergent zero-shot learning 🖼️ 第 17 页幻灯片 · Emergent abilities of large language models: GPT-3 (2020) 🖼️ 第 18 页幻灯片 · Emergent few-shot learning 🖼️ 第 19 页幻灯片 · Emergent few-shot learning 🖼️ 第 20 页幻灯片 · Emergent few-shot learning 🖼️ 第 21 页幻灯片 · Emergent few-shot learning 🖼️ 第 22 页幻灯片 · Few-shot learning is an emergent property of model scale 🖼️ 第 23 页幻灯片 · 1. Prompting 🖼️ 第 24 页幻灯片 · Limits of prompting for harder tasks? 🖼️ 第 25 页幻灯片 · Chain-of-thought prompting 🖼️ 第 26 页幻灯片 · Chain-of-thought prompting is an emergent property of model scale 🖼️ 第 27 页幻灯片 · Chain-of-thought prompting 🖼️ 第 28 页幻灯片 · There are 16 🖼️ 第 29 页幻灯片 · Zero-shot chain-of-thought prompting 🖼️ 第 30 页幻灯片 · Zero-shot chain-of-thought prompting 🖼️ 第 31 页幻灯片 · Sensitivity and inconsistency in prompting 🖼️ 第 32 页幻灯片 · The new dark art of “prompt engineering”? 🖼️ 第 33 页幻灯片 · The new dark art of “prompt engineering”? 🖼️ 第 34 页幻灯片 · Downside of prompt–based learning 🖼️ 第 39 页幻灯片 · A Parameter Perspective of Adaptation 🖼️ 第 40 页幻灯片 · 3. Sparse subnetworks 🖼️ 第 41 页幻灯片 · Pruning 🖼️ 第 42 页幻灯片 · Pruning and Binary Mask 🖼️ 第 43 页幻灯片 · The Lottery Ticket Hypothesis 🖼️ 第 49 页幻灯片 · Low-rank-parameterized update matrices 🖼️ 第 52 页幻灯片 · Understanding low-rank adaptation 🖼️ 第 53 页幻灯片 · From LoRA to QLoRA 🖼️ 第 54 页幻灯片 · LoRA matches full finetuning for RL, even with rank as low as 1 🖼️ 第 55 页幻灯片 · 5. An input perspective of adaptation 🖼️ 第 56 页幻灯片 · Prefix-Tuning (Li and Liang, 2021) 🖼️ 第 57 页幻灯片 · Prompt-Tuning (Lester et al., 2021) 🖼️ 第 58 页幻灯片 · Prompt tuning only works well at scale 🖼️ 第 59 页幻灯片 · 6. A functional perspective of adaptation 🖼️ 第 60 页幻灯片 · Adapter (Houlsby et al. 2019) 🖼️ 第 61 页幻灯片 · 62 🖼️ 第 62 页幻灯片 · Trade-off btw accuracy and # of trained task specific parameters 🖼️ 第 63 页幻灯片 · Language adapters? Task knowledge ~= language knowledge 🖼️ 第 64 页幻灯片 · Using adapters for English dialect adaptation 🖼️ 第 65 页幻灯片 · Unifying View 🖼️ 第 66 页幻灯片 · Performance comparison 🖼️ 第 67 页幻灯片 · 7. Other variants of (efficient) adaptation
9.2 PEFT 的核心思想
回到原视频 (00:38:20)
十、LoRA:低秩适配
回到原视频 (00:45:00)10.1 出发点:梯度的"内在秩"很低
10.2 数学形式
回到原视频 (00:46:20)
10.3 两个关键性质
10.4 代码长什么样
回到原视频 (00:48:40)# 冻结原模型参数,只训练 A、B
for p in model.parameters():
p.requires_grad = False
<div class="video-card screenshot-card">
<img src="images/slide_044.png" alt="第 44 页幻灯片 · A Parameter Perspective of Adaptation" loading="lazy">
<p class="video-caption">🖼️ 第 44 页幻灯片 · A Parameter Perspective of Adaptation</p>
</div>
def forward(x, W0, A, B, alpha):
h = W0 @ x # 原始前向,W0 冻结
h = h + alpha * (B @ (A @ x)) # 加上可训练的低秩偏移
return h
10.5 实践效果
回到原视频 (00:50:30)
十一、把整节课串起来
十二、答疑补充:把 FSDP 那张图讲透
12.1 核心思路:通信与计算重叠
12.2 前向传播逐步走
graph TD
A["要跑第 0 层,但手上没有它的全部参数"] -->|"AG0: all-gather"| B["集齐第 0 层参数"]
B -->|"F0: 前向"| C["计算第 0 层"]
C -->|"同时"| D["AG1: 预取第 1 层参数"]
D -->|"F1: 前向"| E["计算第 1 层"]
E -->|"同时"| F["AG2: 预取第 2 层参数"]
F --> G["...继续"]
C -.->|"不再需要第 0 层"| H["释放第 0 层显存"]
12.3 反向传播逐步走
graph TD
A["在第 10 层,需要它的完整参数"] -->|"all-gather"| B["集齐第 10 层参数"]
B --> C["反向传播,各 GPU 得到自己的梯度"]
C -->|"reduce-scatter"| D["合并梯度,再切分给负责对应分片的 GPU"]
D -.->|"与前向/反向重叠"| E["通信与计算重叠进行"]
12.4 两个实现细节
附录:课堂未展示的幻灯片
cs224n-2026-lecture09-peft.pdf 对应的是课程的另一讲"Efficient Adaptation"。为满足"幻灯片全覆盖"要求,此处集中列出课堂未展示的页面;课堂展示过的页面(35、36、37、45、46、47、48、49、50、51)已就近插入正文。
附:参考与延伸阅读
编者补充
已知疑点
cs224n-2026-lecture09-peft.pdf 的标题为 "Lecture 9: Efficient Adaptation",内容涵盖 DPO、RLHF、prompting、Prompt-tuning、Adapters 等;而本视频(Lecture 12)实际讲的是"高效训练"(浮点数、混合精度、DDP、ZeRO、FSDP、LoRA)。两者只有 LoRA 与 PEFT 动机部分重叠。(依据:幻灯片第 1–2 页文本 vs. 字幕全文。)本书正文以视频字幕为准,幻灯片按"每页恰好出现一次"的规则就近插入或收入附录。
附录:课堂未展示的幻灯片
附录:官方幻灯片里课堂未展示的页
vb slides 自动补入,供对照查阅。