时长 8 小时:§1–§2 稀疏与线性注意力(1.5h)· §3 FlashAttention(2h)· §4 BERT 家族(2h)· §5 Week 1 整合与出关测试(2h)· §A–§B 穿插
对应正课 Lecture 2 的”注意力近似”和”BERT 及其衍生”两部分,以及 Lecture 2 结尾对 Transformer 系架构的总结。
配套代码
w1day6_checks.py(纯 numpy):稀疏模式的计算量、线性注意力的结合律与 RNN 形式、Performer 的随机特征、线性注意力为什么看不准、online softmax、分块前向 + 重算反向的 FlashAttention 与标准实现逐位一致、IO 账、BERT 的 80/10/10、ALBERT 的嵌入分解、蒸馏的 、Sentence-BERT 的双塔§B 参考实现(
code/目录):w1d6_flash_linear.py(FlashAttention 式分块注意力的前向与反向、线性注意力)、w1d6_bert_finetune.py(BERT 微调、MLM 掩码、句向量检索)。今天的目标:能把”让注意力变快”的方法分成三类并说清各自的代价;能从 online softmax 推出 FlashAttention,回答”它加速的是计算还是访存、改没改数学”;能讲清 BERT 的训练目标和每个变体改了什么;通过 Week 1 出关测试。
§0 今天的主线
0.1 两个独立的话题
Lecture 2 的后半段讲两件事,它们之间没有依赖:
- 注意力的 怎么办? Day 1 §5 算过:长序列上注意力的计算和显存都按 增长,显存先撞墙。
- Encoder-only 这一支怎么发展? Day 2 §4.3 把三种架构并列介绍过。decoder-only 是本课主线,但 BERT 家族在理解任务和检索上仍然是主力,第 4 周的 RAG 会直接用到。
0.2 让注意力变快的三条路
标准注意力:算全部 T² 个打分,存全部 T² 个概率
│
┌────────────┼────────────────────┐
▼ ▼ ▼
少算一些 换一种数学 数学不变,换一种算法调度
稀疏注意力 线性 / 核注意力 FlashAttention
(§1) (§2) (§3)
近似 近似 精确
前两条改变了模型本身(算出来的东西和标准注意力不同),第三条只改变计算的顺序(算出来的东西完全一样)。这个区分是今天最重要的一句话,也是学习计划里那道自测题的答案:“FlashAttention 加速的是计算还是内存访问?它改变了数学结果吗?”
0.3 今天要用到的旧结论
| 旧结论 | 出处 |
|---|---|
| 注意力的 FLOPs、显存;短序列上瓶颈不在 | Day 1 §5.1–5.2 |
| 注意力反向的 | Day 1 §2.3 |
| 算术强度与 roofline;访存密集 vs 计算密集 | Day 4 §1.5 |
| log-sum-exp 要减最大值 | Week 0 Day 1 §10.1 |
| RNN 的状态机构思 | Week 0 Day 4 §1.3 |
| 静态词向量的天花板、上下文相关表示 | Week 0 Day 3 §6 |
| 三种架构,BERT 为什么不能生成 | Day 2 §4.3 |
§1 稀疏注意力:少算一些
1.1 做法
每个 query 只看一部分 key,其余位置视为被掩掉:
| 模式 | 每个 query 看什么 | 代表 |
|---|---|---|
| 滑动窗口 | 最近 个 token | Longformer 的局部部分、Mistral |
| 跨步(strided) | 局部 个 + 每隔 取一个 | Sparse Transformer(Child et al. 2019), |
| 全局 token | 少数特殊位置(如 [CLS])看全部、被全部看 | Longformer、BigBird |
| 随机 | 随机选几个 key | BigBird |
w1day6_checks.py 第 1 组(,因果全注意力 3355 万个打分):窗口 256 只算 6.2%,跨步模式 4.4%,窗口 256 加 16 个全局 token 6.6%。
1.2 代价
- 信息要靠多层接力:窗口 的模型,第 层才能间接看到 远(第 1 组:、32 层可以到 131k)。但接力传递就是 RNN 的老问题(Week 0 Day 4 §2),远距离信息会衰减。
- 模式是人定的:模型不能自己决定”这一次我需要看 5000 个 token 之前的那个名字”。
- 硬件不友好:不规则的稀疏模式很难写出高效的 GPU 内核,理论上省的 FLOPs 不一定变成实际加速。滑动窗口是例外,它规则、好实现,所以被 Mistral 这类生产模型采用。
§2 线性注意力:换一种数学
2.1 动机
从哪来?来自 这个 矩阵。能不能不算它?
2.2 推导:去掉 softmax,结合律就能用了
把 softmax 注意力写成一般形式:
如果相似度可以写成特征映射的内积 ,那么
关键一步是交换求和与内积的顺序,也就是矩阵乘法的结合律:。左边要先算 的矩阵,;右边先算 的 ,。 消失了。
Katharopoulos et al.(2020)取 (保证非负)。第 2 组:两种算法最大差 ,精确相等。
2.3 因果版本就是一个 RNN
加上因果掩码,第 个输出只用前 个 key:
这就是一个 RNN,状态是 的矩阵 。 第 2 组:带掩码的并行计算与逐步更新状态的循环计算,最大差 。论文标题就叫 Transformers are RNNs。
意义:训练时可以并行(像 Transformer),推理时每步 、不需要 KV Cache(像 RNN)。Day 4 的所有推理烦恼都没有了。我们从 Week 0 Day 4 的 RNN 出发,经过注意力,又回到了循环结构。Mamba、RWKV、各种门控线性注意力这些”后 Transformer”架构,都在沿着这条线寻找更好的状态更新规则。
2.4 能不能精确地近似 softmax?Performer
softmax 的核 能不能写成特征内积?可以,只是要无穷维。Performer(Choromanski et al.)用正随机特征近似:
取 个随机 做蒙特卡洛估计。第 3 组:1000 次估计的均值与真值一致(无偏),相对标准差从 的 0.446 降到 的 0.033,“相对标准差 × “稳定在 2 附近,即误差按 下降。但 一大方差就急剧增长,要在整个矩阵上都近似得好, 得相当大。
2.5 代价:看不准
第 4 组把 逐步放大,看注意力能多”尖锐”:
| 放大倍数 | softmax 最大权重 | softmax 秩 | 线性注意力最大权重 | 线性注意力秩 |
|---|---|---|---|---|
| 1 | 0.062 | 128 | 0.011 | 16 |
| 4 | 0.480 | 128 | 0.016 | 16 |
| 16 | 0.831 | 128 | 0.022 | 16 |
softmax 可以随打分变大逼近硬查表(Week 0 Day 4 §5.3),线性注意力的矩阵秩被特征维封顶,权重几乎总是摊开的。精确检索、复制、“找到那个名字”这类任务恰好需要尖锐的注意力,这是线性注意力在语言建模上一直有质量差距的主要原因。
§3 FlashAttention:数学不变,换一种调度
3.1 动机:注意力慢,不是因为算得多
Day 1 §5.1 说过:在常见长度上,注意力的 FLOPs 并不是大头。但实测中注意力层往往很慢。原因在 Day 4 §1.5 的 roofline 里:
标准实现的数据流:
从 HBM 读 Q、K → 算 S = QKᵀ → 把 S (T×T) 写回 HBM
从 HBM 读 S → 算 P = softmax(S)(加掩码、dropout)→ 把 P (T×T) 写回 HBM
从 HBM 读 P、V → 算 O = PV → 把 O 写回 HBM
softmax、掩码、dropout 都是逐元素运算,每读一个数只做几次运算,算术强度极低。 和 这两个 的矩阵在显存和计算单元之间来回搬运,时间花在搬数据上,不是算上。
GPU 有两级存储:HBM(显存,几十 GB,带宽约 2 TB/s)和 SRAM(片上,每个 SM 一两百 KB,全卡约 20 MB,但带宽高一个数量级)。FlashAttention 论文的核心思想一句话:让 的中间矩阵永远不离开 SRAM。
3.2 数学基础:online softmax
障碍:softmax 要先知道整行的最大值和分母,才能算出任何一个概率。难道必须先把整行打分算完、存下来?
不必。 一块一块地看 key,只维护三个量:当前见过的最大值 、当前的分母 、当前未归一化的输出 。来了新的一块打分 :
最后 。核心是那个修正因子 :之前的累积量是按旧最大值算的,最大值变了,就整体乘一个因子”重新缩放”。这就是 Week 0 Day 1 §10.1 的”减最大值”技巧的流式版本。
推导为什么对。 归纳假设:处理完前 块后,,。乘上 后变成 和 ,再加上新块的贡献,假设对 成立。最后 ,恰好是 softmax 加权和, 在分子分母中约掉。
w1day6_checks.py 第 5 组:分 8 块,与一次性 softmax 最大差 ;logits 放大 50 倍后 fp32 的朴素 exp 会溢出,online 版本始终减去当前最大值,不会溢出。顺带得到每行的 logsumexp ,反向要用。
3.3 分块(tiling)
有了 online softmax,就可以分块:
对每个 query 块 Q_i(放进 SRAM):
m, ℓ, o ← −∞, 0, 0
对每个 key/value 块 K_j, V_j(放进 SRAM):
S_ij = Q_i K_jᵀ / √d ← 只在 SRAM 里存在
(因果:整块都在未来就跳过;对角块内部加掩码)
按 §3.2 更新 m, ℓ, o
O_i = o / ℓ,写回 HBM;logsumexp_i = m + log ℓ,写回 HBM
和 只在片上存在,算完就丢。 HBM 里只写 ()和每行一个 logsumexp( 个数)。
FlashAttention 论文证明,片上存储为 时,HBM 访问量是 ,而标准实现是 。第 7 组按这个量级粗估(每头、fp16、、每个 SM 约 100 KB):
| 标准实现 | 分块实现 | 比值 | |
|---|---|---|---|
| 1024 | 8.5 MiB | 0.7 MiB | 12.9× |
| 4096 | 130.0 MiB | 4.6 MiB | 28.5× |
| 16384 | 2056.0 MiB | 49.0 MiB | 42.0× |
(忽略常数,只看量级。)
因果掩码的额外红利:未来的整块直接跳过,大约省掉一半计算。标准实现是先算完整的 再盖掩码,省不了。
3.4 反向:重算,而不是存
标准反向需要 (Day 1 §2.3:,)。FlashAttention 不存 ,反向时分块重算:
- 前向只存了 和每行的 logsumexp 。
- 反向时,对每一块重算 ,然后 ,一步得到正确归一化的概率,不需要再做一遍 online softmax。
- 需要整行,但 Day 1 §2.3 证明过 ,只用 和 的第 行就能算。
Day 1 那个”看起来平淡的恒等式”,在这里是反向能分块进行的关键。
第 6 组用 numpy 完整实现了这个算法(外层 query 块、内层 key 块、因果跳块、只存 logsumexp、反向分块重算):前向与标准实现最大差 , 最大差 、、。
重算不是更慢吗? FLOPs 确实多了,但注意力是访存密集的:多算一点、少搬很多,净效果是更快。这是”用计算换访存”,和 Week 0 Day 1 §9.4 的梯度检查点(用计算换显存)是同一种思想。
3.5 回答那道自测题
FlashAttention 加速的是计算还是内存访问?它改变了数学结果吗?
- 加速的是内存访问:FLOPs 甚至略有增加,但 HBM 读写大幅减少。显存从 降到 。
- 不改变数学:它是标准注意力的精确算法,结果只在浮点求和顺序上有差别(第 6 组: 量级)。它不是稀疏注意力,也不是线性注意力。
论文报告 GPT-2(序列 1K)端到端训练加速约 3 倍。之后的 FlashAttention-2(2023)调整了并行划分和循环顺序(外层改成 query 块,也就是第 6 组的写法),又快了约 2 倍;FlashAttention-3(2024)针对 H100 的异步和 FP8 特性优化。PyTorch 的 F.scaled_dot_product_attention 在满足条件时会自动调用这类内核(Day 1 §A.1)。
§4 BERT 家族
4.1 动机:理解任务需要双向上下文
2018 年前后,NLP 的范式正在变成预训练 + 微调:先在大量无标注文本上学通用表示,再在少量标注数据上适配具体任务。问题是:用什么目标预训练?
- 从左到右的语言模型(GPT-1):每个位置只看左边。但理解”bank”是什么意思,右边的”of the river”同样重要。
- 两个单向模型拼起来(ELMo):左到右、右到左各训一个,最后拼接。每个方向内部仍然看不到另一边,两边的信息只在最后浅层地合并。
能不能让每一层的每个位置都同时看两边? 这就是 Week 0 Day 3 §6 想要的”上下文相关表示”的完整版本。
4.2 MLM:为什么不能直接做双向语言模型,以及怎么绕过
障碍:如果每个位置都能看两边,“预测第 个词”就成了抄答案:第 个位置自己就在输入里(和 Day 1 §3.1 不加因果掩码的问题一样)。
BERT 的办法:完形填空(Masked Language Model)。随机选 15% 的位置,把它们藏起来,只在这些位置上预测原词。模型看得到两边,但看不到答案。
80/10/10 规则:被选中的 15% 里,80% 换成 [MASK],10% 换成随机词,10% 保持原样。为什么不全部换成 [MASK]?
[MASK]只在预训练时出现,微调和推理时从不出现。如果模型只学会”在[MASK]的位置上做预测”,它对正常 token 的表示就没有被训练去包含”我应该是什么词”的信息。- 10% 随机替换:模型不知道哪些词被换过,只能对每个位置都保持警惕,为每个 token 都建立好的上下文表示。
- 10% 不变:让表示偏向真实的词,而不是总在怀疑。
w1day6_checks.py 第 8 组(100 万个 token 模拟):被选中带损失的占全部 token 的 14.96%;其中换成 [MASK] 的占全部 token 的 11.98%、换成随机词 1.49%、保持原词 1.49%。
代价:只有 15% 的位置提供监督信号,GPT 是 100%。同样的数据,BERT 学到的东西更少。ELECTRA(§4.6)就是针对这一点的改进。
4.3 NSP 与输入格式
BERT 的第二个目标是下一句预测(NSP):输入两个句子 A、B,50% 的情况 B 真的是 A 的下一句,50% 是随机句子,用 [CLS] 位置的输出做二分类。动机是让模型学会句子间关系(问答、推理任务需要)。
输入格式由此而来:
[CLS] 我 喜欢 猫 [SEP] 它们 很 安静 [SEP]
段 A A A A A B B B B ← 段嵌入(segment embedding)
每个 token 的输入 = 词嵌入 + 位置嵌入(可学习,512 个)+ 段嵌入。Day 2 §5.1 精确数过 BERT-base 的参数量:109,482,240,其中就包括这 2 行段嵌入和一个 pooler。
后来 RoBERTa 证明 NSP 没什么用,去掉之后效果不降反升(§4.6)。
4.4 微调范式
| 任务 | 用哪个输出 | 加什么头 |
|---|---|---|
| 句子分类(情感) | [CLS] | 一个线性层 |
| 句对分类(推理) | [CLS],输入 A [SEP] B | 一个线性层 |
| 序列标注(NER) | 每个 token | 每个位置一个线性层 |
| 抽取式问答 | 每个 token | 两个向量,分别打”起点”和”终点”分 |
整个模型一起微调,通常 2–4 个 epoch,学习率 到 。这个范式在 2018–2020 年统治了 NLP 的几乎所有基准,直到被 GPT-3 式的提示学习(Lecture 3)挑战。
4.5 BERT 的配置
| BERT-base | BERT-large | |
|---|---|---|
| 层 / 宽 / 头 | 12 / 768 / 12 | 24 / 1024 / 16 |
| 参数 | 110M | 340M |
| 预训练数据 | BooksCorpus(8 亿词)+ 英文维基(25 亿词) | 同左 |
预训练 100 万步、batch 256 条序列,其中 90% 的步用长度 128、最后 10% 用 512(注意力的 在 512 上太贵,Day 1 §5.2)。
4.6 变体:每一个都在修一个具体的问题
| 模型 | 修的问题 | 做法 |
|---|---|---|
| RoBERTa(Liu et al. 2019) | BERT 训练不充分 | 更多数据(160GB)、更大 batch、更久;动态掩码(每个 epoch 重新随机掩);去掉 NSP;字节级 BPE |
| ALBERT(Lan et al. 2019) | 参数太多 | 嵌入分解 ;所有层共享参数;NSP 换成句序预测(SOP:判断两句是否被颠倒) |
| DistilBERT(Sanh et al. 2019) | 推理太慢 | 6 层学生模型蒸馏 BERT-base:小 40%、快 60%,保留约 97% 的理解能力 |
| ELECTRA(Clark et al. 2020) | MLM 只有 15% 的位置有信号 | 替换检测:一个小生成器做 MLM 填词,判别器对每个位置判断”是不是被替换过”,监督信号回到 100% |
| Sentence-BERT(Reimers & Gurevych 2019) | BERT 不直接产出好的句向量 | 孪生网络在句对数据上训练,输出平均池化成句向量,用于相似度和检索 |
几个值得算一算的点:
ALBERT 的嵌入分解(第 9 组,,): 时嵌入参数从 23.0M 降到 3.9M, 时从 122.9M 降到 4.4M。直觉:词嵌入只需要表达”词是什么”,不需要和装上下文的隐藏层一样宽。加上跨层共享,ALBERT-base 只有约 12M 参数。但推理计算量没有减少:层数和宽度都没变,只是参数被重复使用。“参数少”不等于”快”。
蒸馏的温度(第 10 组):学生去拟合教师在温度 下的软化分布。软目标的梯度大约按 缩小( 时梯度范数 0.0035,乘 后 0.89,与 时的 0.90 同量级),所以 Hinton et al.(2015)建议软目标损失乘 。高温让教师”错误类别之间的相对大小”(所谓暗知识)显露出来。
Sentence-BERT 的双塔(第 11 组):在 1 万个句子里找最相似的一对,交叉编码器(把两句拼起来送进 BERT)要跑近 5000 万次前向,论文估算约 65 小时;双塔(每句单独编码成向量)只要 1 万次前向加点积,约 5 秒。交叉编码器更准(两句的 token 能互相注意),但只能用来给少量候选重排。第 4 周的 RAG 就是这个两阶段结构:双塔检索召回,交叉编码器重排(Lecture 7)。
4.7 BERT 为什么不能直接生成
Day 2 §4.3 给过一句话的答案,现在可以说完整:(1) 训练中每个位置都看得到两边,从没学过”只根据左边预测下一个”;(2) MLM 给出的是给定其余所有词时某个位置的条件分布,不是语言模型的自回归分解(Week 0 Day 5 §1.1),无法直接用来逐词采样;(3) 没有 KV Cache 可用(Day 4 §1.2)。可以用”反复遮住、反复填”的方式勉强生成,但质量和效率都远不如 decoder。
§5 Week 1 整合
5.1 全景图:现代版 mini-GPT 的每一行来自哪里
| 代码里的这一行 | 为什么这样写 | 出处 |
|---|---|---|
x = wte(idx)(没有位置嵌入) | 位置信息由每层注意力里的 RoPE 注入 | Day 5 §2.6 |
for block in blocks: | 次”通信 + 计算” | Day 2 §1.2 |
x = x + attn(rmsnorm(x)) | Pre-Norm:残差直通路径;RMSNorm 只管缩放 | Day 2 §2.2、§2.5 |
q, k = rope(q), rope(k) | 投影之后旋转:打分只依赖相对位置 | Day 5 §2.3–2.4 |
kv = Linear(d, 2·n_kv·d_k) | GQA:缓存只存 个头 | Day 4 §2.3 |
sdpa(q, k, v, is_causal=True) | 因果掩码;底层可能是 FlashAttention | Day 1 §3.1、今天 §3 |
/ sqrt(d_k)(SDPA 内部) | 让打分方差与 无关 | Week 0 Day 4 §5.5、Day 1 §2.1 |
x = x + mlp(rmsnorm(x)),SwiGLU | 逐位置的非线性加工;键值记忆 | Day 2 §1.3–1.4 |
std = 0.02 / sqrt(2L)(输出投影) | 残差流不随深度增长 | Day 2 §2.3 |
lm_head.weight = wte.weight | 权重绑定 | Week 0 Day 5 §3.3 |
caches[i].update(k, v) | 过去的 K/V 不变;位置可以一直往上数 | Day 4 §1.2、Day 5 §2.6 |
每一行你都能说出它为什么在那里、不这样写会怎样。 这就是本周要达到的状态。
5.2 贯穿一周的四条主线
尺度。 Week 0 Day 4 §5.5 的 (打分方差)→ Day 1 §2.1 初始注意力近乎均匀 → Day 2 §2.3 的 (残差流)→ Day 3 §3.2 的 logits 标准差 → Day 5 §3.4 YaRN 的温度 。每一个”魔法常数”都在把一个会随规模变化的量拉回 。
计算 vs 访存。 Day 1 §5.1(短序列上瓶颈不在 )→ Day 1 §5.2(显存先撞墙)→ Day 4 §1.5(解码是访存密集的,roofline)→ Day 4 §2.5(GQA 省访存不省计算)→ 今天 §3(FlashAttention 用计算换访存)。现代 GPU 上,“少搬数据”往往比”少做运算”更重要。
绝对 vs 相对。 Day 1 §1.5(置换等变,必须注入位置)→ Day 2 §3.4(正弦编码的旋转性质)→ Day 2 §3.6(投影后相对性丢失)→ Day 4 §1.6(绝对位置让缓存失效)→ Day 5 §2(RoPE 把相对性变成保证)→ Day 5 §3(外推失败在低频维度)。
精确 vs 近似。 KV Cache(Day 4)和 FlashAttention(今天 §3)是精确的优化,只改计算顺序;GQA(Day 4 §2)、稀疏注意力(§1)、线性注意力(§2)、PI/NTK/YaRN(Day 5 §3)是近似,改了模型本身,需要训练来弥补。面试时被问到任何一种加速方法,第一个要回答的就是它属于哪一类。
5.3 Week 1 出关测试(2.5h,闭卷)
理论(90 分钟,纸笔)
- 自注意力同时解决了 Week 0 留下的哪两个问题?证明它是置换等变的。
- 为什么需要三个独立的投影?给出至少三个理由。
- 闭卷推出注意力的完整反向,包括 ,并说明它在 FlashAttention 里的作用。
- 为什么要多头?用”一个 softmax 一个分布”论证,并说明 不能太小的原因。
- 写出一层 MHA 的 FLOPs,说明短序列上瓶颈在哪里、长序列上瓶颈在哪里、显存为什么先撞墙。
- 推出正弦位置编码的 ,说明它为什么经过投影后失去相对性。
- 精确算出 GPT-2 small 的参数量;推出 。
- 证明 KV Cache 成立(过去的 K/V 不变),写出显存公式,算出 70B、GQA-8、batch 32、序列 8192 的结果。
- 用 roofline 解释解码为什么慢;GQA 省了什么、没省什么。
- 从”内积只依赖相对位置”推出 RoPE;说明 RoPE 外推失败的具体原因,以及 PI、NTK-aware、YaRN 各改了什么。
- 从 online softmax 推出 FlashAttention;回答”加速的是计算还是访存、改没改数学”。
- 说出 BERT 的两个预训练目标和 80/10/10 的理由;列出四个变体各修了什么问题。
实战(60 分钟)
- 不看任何资料,写出带因果掩码和 padding 掩码的多头注意力,与
F.scaled_dot_product_attention对拍通过。 - 给一个 mini-GPT 加上 KV Cache,证明(用测试)它与全量前向逐位一致。
- 实现 RoPE,验证相距相同的两对位置打分相同。
- 写出 online softmax 的分块版本,与一次性 softmax 对拍。
通过线:理论 12 题至少 10 题讲清推导过程和动机(不是背结论),实战 4 项全部通过。达不到的项回到对应的 Day 重读。
5.4 进入第 2 周之前
- 保留现代版 mini-GPT 和它的训练日志。 第 2 周讲 LLM 的规模化时,你会用它来对照 Chinchilla 的计算最优配比(Lecture 4),并在它上面试验解码策略(Lecture 3)。
- 把 Day 3 的对照实验和 Day 5 的外推实验结果整理成一页笔记:表格 + 三句结论。这是你第一份”有数字支撑的实验报告”,也是 Capstone 方向 A(“从零实现现代 Transformer”)的起点。
- 第 2 周的 Lecture 3 从”为什么现代 LLM 全是 decoder-only”讲起,今天 §4.7 和 Day 2 §4.3 就是它的铺垫。
§A 高效注意力与 BERT:API 用法与边界
A.1 检查 SDPA 走了哪个后端
from torch.nn.attention import sdpa_kernel, SDPBackend
with sdpa_kernel(SDPBackend.FLASH_ATTENTION): # 强制只用 FlashAttention 后端
y = F.scaled_dot_product_attention(q, k, v, is_causal=True)
不满足条件时会直接报错,而不是悄悄回退,所以这是检查”我的调用能不能走快路径”的好办法。FlashAttention 后端的常见要求:CUDA、fp16/bf16、head_dim 不超过某个上限(常见实现是 256)、不支持任意的浮点 attn_mask(ALiBi 这类偏置会让它退出,Day 5 §A.5)。
A.2 HuggingFace 的注意力实现
model = AutoModelForCausalLM.from_pretrained(name, attn_implementation="sdpa") # 或 "flash_attention_2"、"eager"
"eager"是手写的标准实现,能返回注意力权重(output_attentions=True),但最慢。"sdpa"走 PyTorch 的 SDPA,通常是默认值。"flash_attention_2"需要单独安装flash-attn包,只支持 CUDA 和 fp16/bf16。- 要看注意力权重就只能用 eager:融合内核根本不产生完整的 矩阵。
A.3 BERT 微调
tok = AutoTokenizer.from_pretrained("bert-base-uncased")
model = AutoModelForSequenceClassification.from_pretrained("bert-base-uncased", num_labels=2)
enc = tok("sentence A", "sentence B", truncation=True, max_length=128) # 句对:自动加 [CLS] … [SEP] … [SEP]
- 加载时会提示”部分权重是新初始化的”(
classifier.weight等),这是正常的:分类头本来就要从头训练。 - 句对输入时,
token_type_ids标出段 A(0)和段 B(1),对应段嵌入。RoBERTa 没有段嵌入,不需要它。 - 学习率 到 、带 warmup、2–4 个 epoch 是 BERT 论文推荐的范围。用预训练时的 量级去微调,很容易把预训练学到的东西冲掉。
uncased模型会把输入转成小写,cased不会。换模型时别忘了换对应的分词器。
A.4 MLM 的数据整理器
from transformers import DataCollatorForLanguageModeling
collator = DataCollatorForLanguageModeling(tok, mlm=True, mlm_probability=0.15) # 内部实现了 80/10/10
- 不被预测的位置,标签设为 -100(
F.cross_entropy的默认ignore_index,Week 0 Day 5 §A.1)。 mlm=False时它变成因果语言模型的整理器(标签就是输入,模型内部错位)。
A.5 句向量
from sentence_transformers import SentenceTransformer
m = SentenceTransformer("all-MiniLM-L6-v2")
emb = m.encode(["句子一", "句子二"], normalize_embeddings=True) # 归一化后,点积就是余弦相似度(Week 0 Day 3 §5.3)
- 直接拿原始 BERT 的
[CLS]或平均池化当句向量,效果通常很差,这正是 Sentence-BERT 要解决的问题。 - 检索时文档和查询要用同一个模型编码;有些模型要求给查询和文档加不同的前缀(如
"query: "、"passage: "),看模型说明。
§B 实操
- 跑通
w1day6_checks.py,11 组结论都看懂。第 5、6 组是今天的核心:先读懂 numpy 版的分块前向和重算反向,再看参考代码。 - 推导(1h,纸笔):(a) 线性注意力的结合律与 RNN 形式;(b) online softmax 的更新公式及其归纳证明;(c) 为什么反向可以用 一步重算。
- 实现分块注意力(2h):在 PyTorch 里写一个
autograd.Function,前向分块 + online softmax、只存 logsumexp,反向分块重算。与 SDPA 对拍前向和梯度;用torch.cuda.max_memory_allocated比较它与”存下 “的手写版的显存(Python 循环会很慢,目标是理解,不是速度)。 - 线性注意力(0.5h):实现并行版和 RNN 版,验证一致。
- BERT 微调(1.5h,学习计划的动手任务):在 SST-2 上微调
bert-base-uncased(或更快的distilbert-base-uncased),记录验证准确率;再用DataCollatorForLanguageModeling看一批 MLM 样本,亲手数一下 80/10/10。 - 句向量检索(0.5h):用 Sentence-BERT 对几十个句子建索引,做一次语义检索;再用交叉编码器给前 5 个重排,对比结果。这是第 4 周 RAG 的预演。
- Week 1 出关测试(§5.3)。
参考实现:code/w1d6_flash_linear.py(第 3、4 题)、code/w1d6_bert_finetune.py(第 5、6 题)。
出关标准
- 能把注意力加速方法分成”少算 / 换数学 / 换调度”三类,说出每类的代价
- 能推出线性注意力的结合律和 RNN 形式,说出它”看不准”的原因
- 能推出 online softmax 并证明其正确性
- 能说清 FlashAttention 的分块、只存 logsumexp、反向重算,以及 的作用
- 能准确回答”FlashAttention 加速的是什么、改没改数学”
- 能讲清 MLM 的动机、80/10/10 的理由、NSP 的命运
- 能说出 RoBERTa / ALBERT / DistilBERT / ELECTRA / Sentence-BERT 各修了什么问题
- 能解释双塔 vs 交叉编码器,以及它们在 RAG 里的分工
- 完成 BERT 微调
- 通过 §5.3 的 Week 1 出关测试