第 1 周 · Day 6:高效注意力、FlashAttention 与 BERT 家族 + Week 1 整合

Transformers and Large Language Models · Week 1 · Day 6。个人学习讲义与配套实验,按主题整理并持续修订。

课程源码 ZIP ↓

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

本篇目录

时长 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 的嵌入分解、蒸馏的 T2T^2、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 的后半段讲两件事,它们之间没有依赖:

  1. 注意力的 O(T2)O(T^2) 怎么办? Day 1 §5 算过:长序列上注意力的计算和显存都按 T2T^2 增长,显存先撞墙。
  2. Encoder-only 这一支怎么发展? Day 2 §4.3 把三种架构并列介绍过。decoder-only 是本课主线,但 BERT 家族在理解任务和检索上仍然是主力,第 4 周的 RAG 会直接用到。

0.2 让注意力变快的三条路

          标准注意力:算全部 T² 个打分,存全部 T² 个概率
                │
   ┌────────────┼────────────────────┐
   ▼            ▼                    ▼
 少算一些      换一种数学              数学不变,换一种算法调度
 稀疏注意力    线性 / 核注意力          FlashAttention
 (§1)        (§2)                  (§3)
 近似          近似                    精确

前两条改变了模型本身(算出来的东西和标准注意力不同),第三条只改变计算的顺序(算出来的东西完全一样)。这个区分是今天最重要的一句话,也是学习计划里那道自测题的答案:“FlashAttention 加速的是计算还是内存访问?它改变了数学结果吗?”

0.3 今天要用到的旧结论

旧结论出处
注意力的 FLOPs、显存;短序列上瓶颈不在 T2T^2Day 1 §5.1–5.2
注意力反向的 Di=yˉi⊤yiD_i=\bar y_i^\top y_iDay 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 看什么代表
滑动窗口最近 ww 个 tokenLongformer 的局部部分、Mistral
跨步(strided)局部 T\sqrt T 个 + 每隔 T\sqrt T 取一个Sparse Transformer(Child et al. 2019),O(TT)O(T\sqrt T)
全局 token少数特殊位置(如 [CLS])看全部、被全部看Longformer、BigBird
随机随机选几个 keyBigBird

w1day6_checks.py 第 1 组(T=8192T=8192,因果全注意力 3355 万个打分):窗口 256 只算 6.2%,跨步模式 4.4%,窗口 256 加 16 个全局 token 6.6%。

1.2 代价

  • 信息要靠多层接力:窗口 ww 的模型,第 LL 层才能间接看到 L⋅wL\cdot w 远(第 1 组:w=4096w=4096、32 层可以到 131k)。但接力传递就是 RNN 的老问题(Week 0 Day 4 §2),远距离信息会衰减。
  • 模式是人定的:模型不能自己决定”这一次我需要看 5000 个 token 之前的那个名字”。
  • 硬件不友好:不规则的稀疏模式很难写出高效的 GPU 内核,理论上省的 FLOPs 不一定变成实际加速。滑动窗口是例外,它规则、好实现,所以被 Mistral 这类生产模型采用。

§2 线性注意力:换一种数学

2.1 动机

T2T^2 从哪来?来自 QK⊤QK^\top 这个 T×TT\times T 矩阵。能不能不算它?

2.2 推导:去掉 softmax,结合律就能用了

把 softmax 注意力写成一般形式:

yi=∑jsim(qi,kj) vj∑jsim(qi,kj),sim(q,k)=exp⁡(q⊤k/d)y_i=\frac{\sum_j\mathrm{sim}(q_i,k_j)\,v_j}{\sum_j\mathrm{sim}(q_i,k_j)},\qquad\mathrm{sim}(q,k)=\exp(q^\top k/\sqrt d)

如果相似度可以写成特征映射的内积 sim(q,k)=ϕ(q)⊤ϕ(k)\mathrm{sim}(q,k)=\phi(q)^\top\phi(k),那么

yi=∑jϕ(qi)⊤ϕ(kj) vj⊤∑jϕ(qi)⊤ϕ(kj)=ϕ(qi)⊤(∑jϕ(kj)vj⊤)⏞S∈Rd×dϕ(qi)⊤(∑jϕ(kj))⏟z∈Rdy_i=\frac{\sum_j\phi(q_i)^\top\phi(k_j)\,v_j^\top}{\sum_j\phi(q_i)^\top\phi(k_j)}=\frac{\phi(q_i)^\top\overbrace{\Big(\sum_j\phi(k_j)v_j^\top\Big)}^{S\in\mathbb R^{d\times d}}}{\phi(q_i)^\top\underbrace{\Big(\sum_j\phi(k_j)\Big)}_{z\in\mathbb R^{d}}}

关键一步是交换求和与内积的顺序,也就是矩阵乘法的结合律:(ϕ(Q)ϕ(K)⊤)V=ϕ(Q)(ϕ(K)⊤V)(\phi(Q)\phi(K)^\top)V=\phi(Q)(\phi(K)^\top V)。左边要先算 T×TT\times T 的矩阵,O(T2d)O(T^2d);右边先算 d×dd\times d 的 SS,O(Td2)O(Td^2)。T2T^2 消失了。

Katharopoulos et al.(2020)取 ϕ(x)=elu(x)+1\phi(x)=\mathrm{elu}(x)+1(保证非负)。第 2 组:两种算法最大差 1.7×10−161.7\times10^{-16},精确相等。

2.3 因果版本就是一个 RNN

加上因果掩码,第 tt 个输出只用前 tt 个 key:

St=St−1+ϕ(kt)vt⊤,zt=zt−1+ϕ(kt),yt=ϕ(qt)⊤Stϕ(qt)⊤ztS_t=S_{t-1}+\phi(k_t)v_t^\top,\qquad z_t=z_{t-1}+\phi(k_t),\qquad y_t=\frac{\phi(q_t)^\top S_t}{\phi(q_t)^\top z_t}

这就是一个 RNN,状态是 d×dd\times d 的矩阵 StS_t。 第 2 组:带掩码的并行计算与逐步更新状态的循环计算,最大差 4.4×10−164.4\times10^{-16}。论文标题就叫 Transformers are RNNs。

意义:训练时可以并行(像 Transformer),推理时每步 O(d2)O(d^2)、不需要 KV Cache(像 RNN)。Day 4 的所有推理烦恼都没有了。我们从 Week 0 Day 4 的 RNN 出发,经过注意力,又回到了循环结构。Mamba、RWKV、各种门控线性注意力这些”后 Transformer”架构,都在沿着这条线寻找更好的状态更新规则。

2.4 能不能精确地近似 softmax?Performer

softmax 的核 exp⁡(q⊤k)\exp(q^\top k) 能不能写成特征内积?可以,只是要无穷维。Performer(Choromanski et al.)用正随机特征近似:

exp⁡(q⊤k)=Ew∼N(0,I)[ew⊤q−∥q∥2/2 ew⊤k−∥k∥2/2]\exp(q^\top k)=\mathbb E_{w\sim\mathcal N(0,I)}\Big[e^{w^\top q-\|q\|^2/2}\,e^{w^\top k-\|k\|^2/2}\Big]

取 mm 个随机 ww 做蒙特卡洛估计。第 3 组:1000 次估计的均值与真值一致(无偏),相对标准差从 m=16m=16 的 0.446 降到 m=4096m=4096 的 0.033,“相对标准差 × m\sqrt m“稳定在 2 附近,即误差按 1/m1/\sqrt m 下降。但 ∥q∥,∥k∥\|q\|,\|k\| 一大方差就急剧增长,要在整个矩阵上都近似得好,mm 得相当大。

2.5 代价:看不准

第 4 组把 q,kq,k 逐步放大,看注意力能多”尖锐”:

放大倍数softmax 最大权重softmax 秩线性注意力最大权重线性注意力秩
10.0621280.01116
40.4801280.01616
160.8311280.02216

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 都是逐元素运算,每读一个数只做几次运算,算术强度极低。SS 和 PP 这两个 T×TT\times T 的矩阵在显存和计算单元之间来回搬运,时间花在搬数据上,不是算上。

GPU 有两级存储:HBM(显存,几十 GB,带宽约 2 TB/s)和 SRAM(片上,每个 SM 一两百 KB,全卡约 20 MB,但带宽高一个数量级)。FlashAttention 论文的核心思想一句话:让 T×TT\times T 的中间矩阵永远不离开 SRAM。

3.2 数学基础:online softmax

障碍:softmax 要先知道整行的最大值和分母,才能算出任何一个概率。难道必须先把整行打分算完、存下来?

不必。 一块一块地看 key,只维护三个量:当前见过的最大值 mm、当前的分母 ℓ\ell、当前未归一化的输出 oo。来了新的一块打分 s(j)s^{(j)}:

mnew=max⁡(m,max⁡s(j))m^{\text{new}}=\max\big(m,\max s^{(j)}\big)

ℓnew=ℓ⋅em−mnew+∑es(j)−mnew\ell^{\text{new}}=\ell\cdot e^{m-m^{\text{new}}}+\sum e^{s^{(j)}-m^{\text{new}}}

onew=o⋅em−mnew+∑kesk(j)−mnew vko^{\text{new}}=o\cdot e^{m-m^{\text{new}}}+\sum_k e^{s^{(j)}_k-m^{\text{new}}}\,v_k

最后 y=o/ℓy=o/\ell。核心是那个修正因子 em−mnewe^{m-m^{\text{new}}}:之前的累积量是按旧最大值算的,最大值变了,就整体乘一个因子”重新缩放”。这就是 Week 0 Day 1 §10.1 的”减最大值”技巧的流式版本。

推导为什么对。 归纳假设:处理完前 j−1j-1 块后,ℓ=∑k∈已见esk−m\ell=\sum_{k\in\text{已见}}e^{s_k-m},o=∑k∈已见esk−mvko=\sum_{k\in\text{已见}}e^{s_k-m}v_k。乘上 em−mnewe^{m-m^{\text{new}}} 后变成 ∑esk−mnew\sum e^{s_k-m^{\text{new}}} 和 ∑esk−mnewvk\sum e^{s_k-m^{\text{new}}}v_k,再加上新块的贡献,假设对 jj 成立。最后 o/ℓ=∑kesk−mvk/∑kesk−mo/\ell=\sum_ke^{s_k-m}v_k/\sum_ke^{s_k-m},恰好是 softmax 加权和,mm 在分子分母中约掉。■\blacksquare

w1day6_checks.py 第 5 组:分 8 块,与一次性 softmax 最大差 8.3×10−178.3\times10^{-17};logits 放大 50 倍后 fp32 的朴素 exp 会溢出,online 版本始终减去当前最大值,不会溢出。顺带得到每行的 logsumexp =m+log⁡ℓ=m+\log\ell,反向要用。

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

SijS_{ij} 和 PijP_{ij} 只在片上存在,算完就丢。 HBM 里只写 OO(T×dT\times d)和每行一个 logsumexp(TT 个数)。

FlashAttention 论文证明,片上存储为 MM 时,HBM 访问量是 Θ(T2d2/M)\Theta(T^2d^2/M),而标准实现是 Θ(Td+T2)\Theta(Td+T^2)。第 7 组按这个量级粗估(每头、fp16、d=64d=64、每个 SM 约 100 KB):

TT标准实现分块实现比值
10248.5 MiB0.7 MiB12.9×
4096130.0 MiB4.6 MiB28.5×
163842056.0 MiB49.0 MiB42.0×

(忽略常数,只看量级。)

因果掩码的额外红利:未来的整块直接跳过,大约省掉一半计算。标准实现是先算完整的 T×TT\times T 再盖掩码,省不了。

3.4 反向:重算,而不是存

标准反向需要 PP(Day 1 §2.3:Vˉ=P⊤Yˉ\bar V=P^\top\bar Y,Sˉ=P⊙(Pˉ−D)\bar S=P\odot(\bar P-D))。FlashAttention 不存 PP,反向时分块重算:

  1. 前向只存了 OO 和每行的 logsumexp LiL_i。
  2. 反向时,对每一块重算 SijS_{ij},然后 Pij=exp⁡(Sij−Li)P_{ij}=\exp(S_{ij}-L_i),一步得到正确归一化的概率,不需要再做一遍 online softmax。
  3. Di=∑jPˉijPijD_i=\sum_j\bar P_{ij}P_{ij} 需要整行,但 Day 1 §2.3 证明过 Di=yˉi⊤yiD_i=\bar y_i^\top y_i,只用 Oˉ\bar O 和 OO 的第 ii 行就能算。

Day 1 那个”看起来平淡的恒等式”,在这里是反向能分块进行的关键。

第 6 组用 numpy 完整实现了这个算法(外层 query 块、内层 key 块、因果跳块、只存 logsumexp、反向分块重算):前向与标准实现最大差 4.4×10−164.4\times10^{-16},Qˉ,Kˉ,Vˉ\bar Q,\bar K,\bar V 最大差 3.3×10−163.3\times10^{-16}、1.1×10−151.1\times10^{-15}、1.3×10−151.3\times10^{-15}。

重算不是更慢吗? FLOPs 确实多了,但注意力是访存密集的:多算一点、少搬很多,净效果是更快。这是”用计算换访存”,和 Week 0 Day 1 §9.4 的梯度检查点(用计算换显存)是同一种思想。

3.5 回答那道自测题

FlashAttention 加速的是计算还是内存访问?它改变了数学结果吗?

  • 加速的是内存访问:FLOPs 甚至略有增加,但 HBM 读写大幅减少。显存从 O(T2)O(T^2) 降到 O(T)O(T)。
  • 不改变数学:它是标准注意力的精确算法,结果只在浮点求和顺序上有差别(第 6 组:10−1610^{-16} 量级)。它不是稀疏注意力,也不是线性注意力。

论文报告 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:为什么不能直接做双向语言模型,以及怎么绕过

障碍:如果每个位置都能看两边,“预测第 tt 个词”就成了抄答案:第 tt 个位置自己就在输入里(和 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,学习率 2×10−52\times10^{-5} 到 5×10−55\times10^{-5}。这个范式在 2018–2020 年统治了 NLP 的几乎所有基准,直到被 GPT-3 式的提示学习(Lecture 3)挑战。

4.5 BERT 的配置

BERT-baseBERT-large
层 / 宽 / 头12 / 768 / 1224 / 1024 / 16
参数110M340M
预训练数据BooksCorpus(8 亿词)+ 英文维基(25 亿词)同左

预训练 100 万步、batch 256 条序列,其中 90% 的步用长度 128、最后 10% 用 512(注意力的 T2T^2 在 512 上太贵,Day 1 §5.2)。

4.6 变体:每一个都在修一个具体的问题

模型修的问题做法
RoBERTa(Liu et al. 2019)BERT 训练不充分更多数据(160GB)、更大 batch、更久;动态掩码(每个 epoch 重新随机掩);去掉 NSP;字节级 BPE
ALBERT(Lan et al. 2019)参数太多嵌入分解 V×H→V×E+E×HV\times H\to V\times E+E\times H;所有层共享参数;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 组,V=30000V=30000,E=128E=128):H=768H=768 时嵌入参数从 23.0M 降到 3.9M,H=4096H=4096 时从 122.9M 降到 4.4M。直觉:词嵌入只需要表达”词是什么”,不需要和装上下文的隐藏层一样宽。加上跨层共享,ALBERT-base 只有约 12M 参数。但推理计算量没有减少:层数和宽度都没变,只是参数被重复使用。“参数少”不等于”快”。

蒸馏的温度(第 10 组):学生去拟合教师在温度 TT 下的软化分布。软目标的梯度大约按 1/T21/T^2 缩小(T=16T=16 时梯度范数 0.0035,乘 T2T^2 后 0.89,与 T=1T=1 时的 0.90 同量级),所以 Hinton et al.(2015)建议软目标损失乘 T2T^2。高温让教师”错误类别之间的相对大小”(所谓暗知识)显露出来。

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:LL 次”通信 + 计算”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:缓存只存 nkvn_{kv} 个头Day 4 §2.3
sdpa(q, k, v, is_causal=True)因果掩码;底层可能是 FlashAttentionDay 1 §3.1、今天 §3
/ sqrt(d_k)(SDPA 内部)让打分方差与 dkd_k 无关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 的 1/dk1/\sqrt{d_k}(打分方差)→ Day 1 §2.1 初始注意力近乎均匀 → Day 2 §2.3 的 1/2L1/\sqrt{2L}(残差流)→ Day 3 §3.2 的 logits 标准差 0.02d0.02\sqrt d → Day 5 §3.4 YaRN 的温度 0.1ln⁡s+10.1\ln s+1。每一个”魔法常数”都在把一个会随规模变化的量拉回 O(1)O(1)。

计算 vs 访存。 Day 1 §5.1(短序列上瓶颈不在 T2T^2)→ 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 分钟,纸笔)

  1. 自注意力同时解决了 Week 0 留下的哪两个问题?证明它是置换等变的。
  2. 为什么需要三个独立的投影?给出至少三个理由。
  3. 闭卷推出注意力的完整反向,包括 Di=yˉi⊤yiD_i=\bar y_i^\top y_i,并说明它在 FlashAttention 里的作用。
  4. 为什么要多头?用”一个 softmax 一个分布”论证,并说明 dkd_k 不能太小的原因。
  5. 写出一层 MHA 的 FLOPs,说明短序列上瓶颈在哪里、长序列上瓶颈在哪里、显存为什么先撞墙。
  6. 推出正弦位置编码的 pt+k=Rkptp_{t+k}=R_kp_t,说明它为什么经过投影后失去相对性。
  7. 精确算出 GPT-2 small 的参数量;推出 C≈6NDC\approx6ND。
  8. 证明 KV Cache 成立(过去的 K/V 不变),写出显存公式,算出 70B、GQA-8、batch 32、序列 8192 的结果。
  9. 用 roofline 解释解码为什么慢;GQA 省了什么、没省什么。
  10. 从”内积只依赖相对位置”推出 RoPE;说明 RoPE 外推失败的具体原因,以及 PI、NTK-aware、YaRN 各改了什么。
  11. 从 online softmax 推出 FlashAttention;回答”加速的是计算还是访存、改没改数学”。
  12. 说出 BERT 的两个预训练目标和 80/10/10 的理由;列出四个变体各修了什么问题。

实战(60 分钟)

  1. 不看任何资料,写出带因果掩码和 padding 掩码的多头注意力,与 F.scaled_dot_product_attention 对拍通过。
  2. 给一个 mini-GPT 加上 KV Cache,证明(用测试)它与全量前向逐位一致。
  3. 实现 RoPE,验证相距相同的两对位置打分相同。
  4. 写出 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:融合内核根本不产生完整的 PP 矩阵。

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 没有段嵌入,不需要它。
  • 学习率 2×10−52\times10^{-5} 到 5×10−55\times10^{-5}、带 warmup、2–4 个 epoch 是 BERT 论文推荐的范围。用预训练时的 10−410^{-4} 量级去微调,很容易把预训练学到的东西冲掉。
  • 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 实操

  1. 跑通 w1day6_checks.py,11 组结论都看懂。第 5、6 组是今天的核心:先读懂 numpy 版的分块前向和重算反向,再看参考代码。
  2. 推导(1h,纸笔):(a) 线性注意力的结合律与 RNN 形式;(b) online softmax 的更新公式及其归纳证明;(c) 为什么反向可以用 Pij=exp⁡(Sij−Li)P_{ij}=\exp(S_{ij}-L_i) 一步重算。
  3. 实现分块注意力(2h):在 PyTorch 里写一个 autograd.Function,前向分块 + online softmax、只存 logsumexp,反向分块重算。与 SDPA 对拍前向和梯度;用 torch.cuda.max_memory_allocated 比较它与”存下 PP“的手写版的显存(Python 循环会很慢,目标是理解,不是速度)。
  4. 线性注意力(0.5h):实现并行版和 RNN 版,验证一致。
  5. BERT 微调(1.5h,学习计划的动手任务):在 SST-2 上微调 bert-base-uncased(或更快的 distilbert-base-uncased),记录验证准确率;再用 DataCollatorForLanguageModeling 看一批 MLM 样本,亲手数一下 80/10/10。
  6. 句向量检索(0.5h):用 Sentence-BERT 对几十个句子建索引,做一次语义检索;再用交叉编码器给前 5 个重排,对比结果。这是第 4 周 RAG 的预演。
  7. Week 1 出关测试(§5.3)。

参考实现:code/w1d6_flash_linear.py(第 3、4 题)、code/w1d6_bert_finetune.py(第 5、6 题)。


出关标准

  • 能把注意力加速方法分成”少算 / 换数学 / 换调度”三类,说出每类的代价
  • 能推出线性注意力的结合律和 RNN 形式,说出它”看不准”的原因
  • 能推出 online softmax 并证明其正确性
  • 能说清 FlashAttention 的分块、只存 logsumexp、反向重算,以及 Di=yˉi⊤yiD_i=\bar y_i^\top y_i 的作用
  • 能准确回答”FlashAttention 加速的是什么、改没改数学”
  • 能讲清 MLM 的动机、80/10/10 的理由、NSP 的命运
  • 能说出 RoBERTa / ALBERT / DistilBERT / ELECTRA / Sentence-BERT 各修了什么问题
  • 能解释双塔 vs 交叉编码器,以及它们在 RAG 里的分工
  • 完成 BERT 微调
  • 通过 §5.3 的 Week 1 出关测试

疑问清单

← 返回课程目录