加速器系统课程 / 主线 / 专题三 / 注意力演进 📝 讲义(授课稿)

注意力演进

Three Knobs, Not Thirty Names
2020 年的 GPT-3 只能记住 2048 个 token;今天的模型记 100 万
—— 这六年的注意力演进,讲的就是这 512 倍 是怎么换来的。

MLA、GQA、SWA、DSA、NSA、CSA、DeltaNet、GDN、KDA…… 名词多到像各搞各的,但只有三个旋钮可以拧。 这一讲不按名字讲,按它们各自是怎么被逼出来的讲 ——  每一个都回到一手论文与 config 核过。

前置 专题一 · 专题二 主线 三个旋钮 + 一条硬件线 我们自己的 v7 实测 70′ / 113.5′ / 136.5′ 三档

课程作者 Chris Yang·Google Cloud AI Infra 架构师

这个专题在讲一件什么事

2020 年的 GPT-3,上下文是 2048 个 token。那时候的模型是个很聪明的补全器 —— 你给它一段话,它接得很好。

今天你让它做的事完全不一样了:读完整个代码库再改一处 bug、 连着跑几十轮工具调用、记住整场对话里你反复改过的主意

⭐⭐ 上下文长度就是 agent 的工作记忆。 记不住,就什么都干不成 —— 2K 的上下文,连一个文件都读不完。
⛔ 而每加长一分上下文,KV cache(模型每吐一个字都要回看前面所有字,于是把每个字算出来的 K、V 存着不重算 —— 存下来的这堆就叫它)就线性涨一分
⭐ 所以这六年注意力的全部演化,是为了让「记得住」这件事付得起

⭐⭐ 先把这一讲的框给你 —— 前半程只有一个账本:KV cache。 后面那三个旋钮,是同一个账本的三个面。
(三个面分别是什么,看完下面这张骨架图再说。)
⚠️ 说「前半程」是认真的:到旋钮② 这个账本就不够用了 —— DSA 省的是「读多少」,KV 一个字节都没少。 从那里起账本分成三样(显存 / 算力 / 访存规整度), §十 收在那三样上。

⛔ 课前勿点 —— 开讲前的两道热身题现场会一起做;提前看答案=自己剧透,这两道题就废了

第一题 · 一个用户的 KV cache,到底有多大

DeepSeek V3(671B,61 层,128 个注意力头,head_dim 128), 128K 上下文、单个用户、bf16 存
同样这个形状,换四种注意力,KV cache 各是多大? 四行分开选,各选各的。

(a) 最朴素的 MHA(128 个 KV 头)
(b) GQA-8(KV 头砍到 8)
(c) MQA(KV 头砍到 1)
(d) MLA(V3 真实用的方案)

488 / 30.5 / 3.8 / 8.6 GiB。
算法就一条:每 token 每层要留下几个数, 乘 61 层、乘 2 字节、乘 131,072 个 token。
MHA = 2×128×128 = 32,768 → 488 GiB; GQA-8 = 2×8×128 = 2,048 → 30.5(16×); MQA = 2×1×128 = 256 → 3.8(128×); MLA = 压缩维 512 + RoPE 64 = 576 → 8.6(56.9×)。
⭐⭐ 488 GiB 到底是多大?换算成机器就有感觉了:按专题二推的 可分配口径(94.74 GiB/device)算,488 ÷ 94.74 = 5.2 个 device,也就是约 2.6 块 v7 芯片
一个用户、一段输入,就要把两块半芯片的 HBM 整个拿来放 KV —— 而模型权重还一个字节都没放进去。
📌 RoPE(旋转位置编码)后面会反复出现,这里先把它的职责摆正: 「谁在前谁在后」并不归它管。那件事是因果掩码白送的 —— 每个位置只看得见自己左边,多堆几层就能数出自己前面有几个人 (NoPE,arXiv 2305.19466,NeurIPS 2023 ——  直接证明了 decoder-only 不加任何显式位置编码也学得会顺序)。
⭐ RoPE 真正加进来的是另一样东西:「我跟它差几格」。 给每个位置的 Q 和 K 按它的位置转一个角度,点积时两个绝对角度相减, 相对距离就直接出现在打分里—— 零参数,而且每一层都能用, 不必靠堆层去数。 ⭐ 它只改 Q/K,不改 V。后面 §五会讲它给 MLA 惹的麻烦——  上面那 64 维的成本,买的就是这一件事。

⭐⭐ 这道题真正的题眼在 (c) 和 (d) 的大小关系MQA 只要 3.8 GiB,比 MLA 的 8.6 还小 2.25 倍。 MLA 并不是最省的那个。
⭐ 所以这一支的目标从来不是「谁存得最少」——  MQA 早在 2019 年就把它压到头了,代价是质量掉得厉害。 真正要比的是「同样一份字节,换回多少能力」。 这正是第五节要讲的那条线。

⚠️ 还有一个口径要说清: 488 GiB 是「假如 V3 用 MHA」的反事实数字, 不是 V3 的实测值 —— V3 从第一天就是 MLA。 而且这里沿用了 V3 论文比较表的口径(K、V 都按 head_dim=128 算); V3 真实的 K 每头是 128+64=192 维,严格算这个基线还会更大一点。

第二题 · 那条「算得过来还是搬得过来」的线

上一讲那把尺子:算力 ÷ 带宽 ——  每从内存搬一个字节,这台机器配套能算多少次。
TPU v7 上,两层各是多少?两行分开选。

(a) 对 HBM(片外)
(b) 对 VMEM(片上)

(a) 约 313。(b) 比 HBM 低一个量级,落在几十这一档。
(a) = 2,307 TFLOP/s ÷ 7.37 TB/s。 这个数不是本讲新造的,它就是专题二整整一节在立的那条屋脊线。

⭐ (b) 最容易选反,而选反的人通常是把「快」和「门槛高」搞混了: 片上更快,所以分母变大 ——  同一个分子除以更大的分母,商只会更小越靠近计算,这条线越低。 门槛低意味着:同一个算子挪到片上以后,更容易变成算力受限。

⛔ 而 (b) 为什么只给量级、不给数 —— 这才是这道题最想教的一件事VMEM 的带宽官方没有公开。 而屋脊点乘以算力就等于带宽 —— 给出一个精确的屋脊点, 等于把那个没公开的数反推出来。所以我们到「几十这一档」为止,不往下猜
⭐ 这正是专题二第 6 节那四句问法里的一句: 先问这个数的出处和口径,再用它。 而那四句不是拿来审别人材料的 —— 是先拿来审自己的。

这是一个什么故事 —— 六年时间,把「记得住」变成一件付得起的事 上下文长度就是 agent 的工作记忆。2K 的时候模型只能当个聪明的补全器;要读整个代码库、跑几十轮工具调用、记住整场对话,先得记得住。而每加长一分上下文,KV cache 就 线性涨一分 还没有 KV cache 这回事 让每一份更小 每步只读一部分 换回一个固定大小的状态 1990–2017 RNN 1 2 3 4 5 带一个固定大小的状态 序列建模第一次可行 串行:算不快、记不住 2017 MHA 谁都能看见谁 把循环整个拿掉 训练能并行,规模才起得来 状态没了 → KV cache 出生 2019–2024 旋钮① 格子没少,每格里的东西变小 让每一份更小 2K → 128K 成常态 砍太狠会掉质量 2023–2026 旋钮② 格子照样在,这一步只读两个 每步只读一部分 1M 进入可用区间 省读不省存 ↳ 接着上一行 2020–2026 旋钮③ 一块板子 换回一块固定大小的板子 那些层的 KV 归零 串行跟着回来了 2024–今天 混合 普通 普通 普通 资深 三个便宜的配一个贵的 两头都要 3:1 ~ 7:1 成了共识 便宜的管长度,贵的管质量 能跑多长 ↗ 每个点是一个模型声明的上下文上限 2K 32K 128K 1M 10M 2021 2022 2023 2024 2025 2026 GPT-3 Llama 4 Scout Kimi K3 绿线=当时的纪录:2K → 10M(Llama 4 Scout 声明值,之后没人再刷) 同一长度的代价 ↘ 每个点是那个模型在 128K 时的 KV cache(BF16、batch 1) 576 GiB 64 GiB 8 GiB 697 MiB 2021 2022 2023 2024 2025 2026 GPT-3 PaLM DeepSeek-V4-Flash 混元 Hy3 绿线=当时的最省记录。576 GiB → 697 MiB,846 倍 不是所有人都在走 —— 右图 2026 年还有模型停在 40 GiB(混元 Hy3,纯 GQA)。动的是「最好成绩」那条线,不是每一家。 纪录也不等于常态:左图那条线的终点是 10M(一家的声明值);本讲说的 512 倍 是 2K → 1M 这个常态 —— 模型表里 12 家做到。 两条曲线反着走 —— 这才是这六年真正发生的事 这两条是两把尺子,不能相乘:左边量「能跑多长」,右边量「同一长度下省了多少」。把 512 倍 × 846 倍说成四十万倍,是把两把尺子当成一把。 但它们同时发生,才有今天的 agent—— 上下文能装下整个代码库,而且装得起。 所以这一讲的前半程只有一个账本:KV cache( 到旋钮② 它就不够用了) 在张量形状里找 S(KV 长度)—— 全图只有 K 和 V 两处带它,那就是唯一要跨 token 留下来的东西 三个旋钮是它的三个面:① 每份多大 ② 每步读多少 ③ 干脆别让它变长
整个专题的骨架。六段,每段只问三件事:图啥、带来了什么、欠下了什么。底下那两条数是这一讲真正的落点 —— 能跑的长度涨了,同一长度下要付的钱降了,两件事同时发生,才有今天的 agent。
出处与口径

2K = GPT-3 的 max_position_embeddings;1M 与 846 倍见本课模型表(128K、BF16、batch 1,两端都取 ≥100B 的模型)

⚠️ 512 倍(2K→1M)与 846 倍(KV 降幅)是两个口径,本图刻意分两行给出 —— 把它们相乘是把两把尺子当成一把

两张趋势图的点全部由 topic03_models.ROWS 现算,没有手写常数;绿线是「跑到当月为止的最好成绩」,不是拟合曲线

右图只收 ≥100B 的模型 —— 跟「846 倍」同一个口径。不过滤的话包络线会被 Mistral 7B 的 512 MiB 拽到底,而那只是因为它本来就小

RWKV-7 的 KV = 0,对数轴上画不出来,没有进右图

⭐ 这一讲的前半程只有一个账本:KV cache。 在张量形状里找 S(KV 长度)—— 全图只有 K 和 V 两处「留得下来」的带它(注意力矩阵那一处也带 S,但它算完就扔,不进 KV cache), 那就是唯一需要跨 token 留下来的东西。
三个旋钮是同一个账本的三个面:① 每份多大 ② 每步读多少 ③ 干脆别让它变长。 看到任何一个新名词,先问它在拧哪一面。

骨架之后,先看一眼全景与全部证据:这些名词是什么时候、按什么顺序冒出来的, 以及今天各家实际上是怎么配的每一家的 KV cache 到底多大

Attention 编年史 —— 从 RNN 的一个补丁,到今天各家的混合配比 ⭐ 从 2014 年到今天,这一路在干的其实是一件事 —— 现在不用懂,最后一章会回来收这句 Transformer 当初做的交易是「用平方的计算量,买来完全的并行度」—— 而这段历史,是在把那笔交易往回赎,但不能把并行度还回去。 (信息截至 2026-09-07,全部现搜) 一、编年史 —— 四条支线,各修各的毛病 (横轴按每年的内容疏密压缩过:没有事件的年份收窄、事件多的年份放宽 —— 间距不代表时间长短 '14 '15 '16 '17 '18 '19 '20 '21 '22 '23 '24 '25 '26 前史 · 注意力是 RNN 的补丁 Bahdanau 注意力 ⭐ Transformer / MHA ① 每个 token 存多少(KV 怎么小) MQA GQA MLA CLA 跨层共享 MFA 低秩分解 Gated MLA · K=V 共享 ② 每个 query 看多少(稀疏 · 压缩) SWA · sink NSA · DSA CSA+HCA · MSA IndexShare · IndexCache ③ 换一套数学(线性注意力 · SSM 同源) 线性 Transformer DeltaNet Mamba Mamba-2(SSD)· 可并行 DeltaNet · GDN KDA · Lightning Mamba-3 · Gated DeltaNet-2 ⊘ 不在旋钮上:实现层(不改数学,只改怎么算) ⭐ FlashAttention ⭐ 全图落点:三家公司,各自把旋钮拧了一遍 —— 而且拧的过程全写在公开的 config 里 MiniMax 线性(01)→ 退回全注意力(M2)→ 稀疏(M3)—— 三次转向,每次都公开写了为什么 腾讯混元 Hy3 80 层纯 GQA-8(连线性都没上)→ Hy4 78 层全 Gated DSA—— 跳过线性那一支,直接进稀疏 智谱 GLM 5 上 DSA → 5.2 加 IndexShare → 5.3-Flash 第一次线性+稀疏同锅—— 半年走完三步 ⭐⭐ 再看一眼第二列:这次补完混元和 GLM,多出来一条原先看不见的线 混元 Hy4 的 IndexCache 和 GLM-5.2 的 IndexShare 是同一个想法:两家的 indexer_types 都是 full, shared, shared, shared 稳态四层一循环(前几层各家不同)—— 每 4 层只有 1 层自己算索引。 ⭐ 所以稀疏的第二阶段优化,已经不是「让每个 query 少看几块」,而是「别每层都重新算一遍该看谁」—— 索引本身变成了新的开销。这是 2026 年才冒出来的一层。

📌 这张表的取数规则(⭐ 2026-09-13 补 —— 由表长出来的结论全靠这条规则撑着)

  • 层配比 / 层数 / 头数 / 维度:一律读公开 config(HF 上那份 config.json)。
  • 上下文:取 config 的 max_position_embeddings。⚠️ 它是容量不是能力 —— 声明 10M 不等于 10M 上都好用。
  • KV cache:一律按 @128K · bf16 · batch 1 由形状当场算出,不取厂商自报值。⚠️ 滑窗模型给的是窗口封顶后的量。
  • 三处例外,各自标了:混元 Hy3 那一行读自我们自己的仓库;Ling 一族读自模型卡;两处声明与实测口径不一致的(MiniMax-01 的 4M / config 10M)在备注里写了。

⚠️ 凡是上下文 < 128K 的行,那一格的 KV 是反事实值(把它的形状放到 128K 上算)—— 包括「846 倍」的分子 GPT-3,它的实际上下文只有 2K。这个倍数是用来看量级的,不是用来引用的。

点表头可以排序—— 时间 / 厂商 / 便宜层占比 / 上下文 / KV 大小,再点一次反向。默认按时间。两种模式下排序都作用在全部 44 行上。

时间模型 按厂商一个循环 一格=一层上下文 声明值KV cache@128K备注
2020-05GPT-3 175B 稠密 · 96 层MHA —— 每一层都是这个2K576 GiB(10 条才画得下)基线:KV 按头数线性长,没有任何省法起点。576 GiB 这把尺,后面所有省下来的都拿它比
2022-04PaLM 540B 稠密 · 118 层MQA —— 每一层都是这个2K15 GiB48 头共用 1 组 KV —— 第一次大规模砍 KVMQA —— 第一次把 KV 砍到只剩 1 组
2023-02Llama 1 65B 稠密 · 80 层MHA —— 每一层都是这个2K320 GiB(6 条才画得下)一代还是纯 MHA,下一代才上 GQA
2023-07Llama 2 70B 稠密 · 80 层GQA —— 每一层都是这个4K40 GiBMQA 砍太狠掉质量,GQA 是折中(arXiv 2305.13245GQA —— 砍到 1 组太狠,折中版成了此后十年的默认
2023-09Mistral 7B 7B 稠密 · 32 层SWA —— 每一层都是这个32K512 MiBSWA 进主流的第一枪,窗口 4096SWA —— 「不看全部」这个想法的起点,窗口 4096
2024-03Jamba 52B/12B · 32 层MambaMambaMambaMambaMambaMambaMambaGQA256K2.0 GiB⭐⭐ 层间混合的开源起点,比 MiniMax-01 早十个月层间混合的开源起点,7:1 —— 比 MiniMax-01 早十个月
2024-05DeepSeek-V2 236B/21B · 60 层MLA —— 每一层都是这个128K8.4 GiB低秩压缩。KV 降 93.3%=只剩 2.25 组 GQAMLA 首发。换了个思路:不砍头数,改存压缩过的隐向量
2024-06Gemma 2 27B 27B 稠密 · 46 层SWAFULL8K24 GiB⭐ 谷歌开始交替:1:1,窗口 4096
2024-07Llama 3.1 405B 稠密 · 126 层GQA —— 每一层都是这个128K63 GiB(2 条才画得下)不上花招硬推 128K 的代价:比 Llama 2 还多
2024-11混元 Hunyuan-Large 389B/52B · 64 层GQA —— 每一层都是这个128K10 GiBCLA:每 2 层共享一份 KV —— 旋钮①的第三招
2024-12DeepSeek-V3 671B/37B · 61 层MLA —— 每一层都是这个160K8.6 GiB专题一的锚点。跟 V2 只差 1 层,KV 几乎相同跟 V2 同机制,但参数大 2.8 倍而 KV 只差 1.7% —— KV 脱钩的证据
2025-01MiniMax-01 456B/45.9B · 80 层LTNLTNLTNLTNLTNLTNLTNGQA4M5.0 GiB⭐ 线性首次上旗舰。训练 1M、外推 4M(config 的 10M 是容量)线性第一次上到几百 B 的规模,同样是 7:1
2025-03Gemma 3 27B 27B 稠密 · 62 层SWASWASWASWASWAFULL128K10 GiB⭐⭐ 5:1、窗口 1024 —— 小米那个 5:1 不是首创滑窗混合定型:5 层窗口配 1 层全局,窗口反而收到 1024
2025-03RWKV-7 Goose 0.19B–2.9B · 纯 RNNRWKV —— 每一层都是这个无限(理论)0(无 KV)全表唯一 KV 为零:常数内存、常数单 token 时间另一头的极端:纯 RNN,KV cache literally 是 0
2025-04Llama 4 Scout 109B/17B · 48 层SWASWASWAFULL10M7.1 GiB块状局部 8192 + NoPE 全局。⚠️ 声称 10M,又一个报容量的
2025-04Qwen3-235B-A22B 235B/22B · 94 层GQA —— 每一层都是这个40K24 GiB千问转线性之前的那一代:纯 GQA-4
2025-07Kimi K2 1T/32B · 61 层MLA —— 每一层都是这个128K8.6 GiBKimi 上 KDA 之前:纯 MLA,架构名就是 DeepseekV3
2025-07GLM-4.5 355B/32B · 92 层GQA —— 每一层都是这个128K46 GiB⭐⭐ 智谱上 DSA 之前:46 GiB → GLM-5 的 11,降 4 倍
2025-08gpt-oss-120b 117B/5.1B · 36 层SWAFULL128K4.5 GiBOpenAI 首个开放权重:1:1 交替、窗口 128 + sink
2025-09DeepSeek-V3.2-Exp 671B/37B · 61 层DSA —— 每一层都是这个160K8.6 GiB稀疏的起点:V3 + Lightning Indexer。⛔ KV 跟 V3 一样DSA —— 层内稀疏起点:不是少几层,是每层只挑一部分 token 看
2025-09Qwen3-Next 80B/3B · 48 层GDNGDNGDNgAT256K3.0 GiB36 线性 + 12 全注意力(GQA-2,头维 256)GDN —— Mamba 那一支的直系后代第一次进主流大模型
2025-10Ling-1T(Ling 2.0) 1T/50B · 80 层GQA —— 每一层都是这个32K40 GiB⭐⭐ Ling 2.6 就是从它改造的:40 GiB → 1.4 GiB
2025-10MiniMax M2 230B/10B · 62 层GQA —— 每一层都是这个192K31 GiB「退回全注意力」≠ 什么都没做:GQA-8 + partial RoPE反例:退回全注意力。证明「全注意力」说的是旋钮③,不是①
2025-10Kimi Linear 48B/3B · 27 层KDAKDAKDAMLA1M1008 MiB20 KDA + 7 MLA(末层强制 full)。已用 NoPEKDA —— 线性的新一代,配 NoPE 的 MLA
2025-12DeepSeek-V3.2 671B/37B · 61 层DSA —— 每一层都是这个160K8.6 GiBExp 转正。index_topk 512 → 2048,KV 与 V3 一样Exp 转正:稀疏从实验走进生产,top-k 512 → 2048
2025-12Mistral Large 3 675B/41B · 61 层MLA —— 每一层都是这个288K8.6 GiBMLA 超参跟 V3 逐字段一样;参数读自 params.jsonMLA 扩散到了西方:欧洲最大开源旗舰逐字段照抄 V3 的 MLA 超参
2026-01小米 MiMo-V2-Flash 309B/15B · 48 层SWASWASWASWASWAFULL256K5.0 GiB窗口 128。卡上自称 KV 省近 6×,48÷8 正好对上
2026-02GLM-5 744B/40B · 78 层DSA —— 每一层都是这个198K11 GiBMLA + DSA。GLM-5.1 同架构,只有后训练不同
2026-02MiniMax M2.5 230B/10B · 62 层GQA —— 每一层都是这个192K31 GiB⚠️ 架构与 M2 逐字段相同,稀疏要等 M3
2026-03Qwen3.5 397B/17B · 60 层GDNGDNGDNgAT256K3.8 GiB45 线性 + 15 全(config: full_attention_interval 4)千问把混合注意力从旁支 Qwen3-Next 收进了主线
2026-04小米 MiMo-V2.5-Pro 1.02T/42B · 70 层SWASWASWASWASWASWAFULL1M6.3 GiB60 SWA + 10 全,窗口 128 —— 1M 那档最省的
2026-04DeepSeek-V4-Pro 1.6T/49B · 61 层HCAHCACSAHCACSA1M999 MiB⛔ 跟 Flash 不同:前两层是 HCA。1.6T 而 KV 不到 1 GiB
2026-04Ling 2.6-1T 1T/63B · 80 层LTNLTNLTNLTNLTNLTNLTNMLA256K1.4 GiB不是 KDA;思考版 Ring-2.6-1T 架构逐字段相同
2026-04Gemma 4 31B 31B 稠密 · 60 层SWASWASWASWASWAFULL256K11 GiB⭐ 全局层 K 维加倍 + K=V 共享,窗口 1024旋钮①又出新招:全局层 K 维加倍再让 K=V 共享一份
2026-05DeepSeek-V4-Flash 284B/13B · 43 层SWASWACSAHCACSAHCA1M697 MiB⭐ 2 层 SWA 引导,CSA/HCA 交替。MLA 换成 shared-KV MQA终点。697 MiB,比 GPT-3 小 846 倍,而且 MLA 被整个换掉了
2026-06GLM-5.2 744B/40B · 78 层DSA —— 每一层都是这个1M11 GiB⭐ +IndexShare:四层共用一个 indexer。198K → 1M 靠这步IndexShare —— 稀疏的第二阶段:索引本身变成了新的开销
2026-06MiniMax M3 428B/23B · 60 层MSA —— 每一层都是这个1M15 GiB⭐⭐ GQA-4 + 稀疏。KV 比走 MLA 的 GLM-5.2 还大
2026-07Kimi K3 2.8T/104B · 93 层KDAKDAKDAgMLA1M3.4 GiB69 KDA + 24 Gated MLA(末层 92、93 连着两层 full
2026-07混元 Hy3 295B/21B · 80 层GQA —— 每一层都是这个256K40 GiB80 层全 GQA-8 —— 线性一层都没上
2026-07Ling-3.0-flash 124B/5.1B · 42 层KDAKDAKDAKDAKDAgMLA256K1008 MiB跟 2.6 换了一支。同代 tiny 用 3:1,它用 5:1;3.0 无 1T
2026-08GLM-5.3 744B/40B · 78 层DSA —— 每一层都是这个1M11 GiB⚠️ 跟 5.2 同一个 base,纯后训练,架构没动
2026-08混元 Hy4-preview 770B/49B · 78 层gDSA —— 每一层都是这个1M11 GiB78 层全稀疏 + IndexCache(每 4 层 1 层算索引)跳过线性那一支,从纯 GQA 直接跳进全层稀疏
2026-08⭐ GLM-5.3-Flash 320B/18B · 45 层KDAKDAKDADSA1M1.5 GiB34 KDA + 11 稀疏 MLA —— GLM 首次线性+稀疏同锅唯一一个把②和③同锅:34 层 KDA + 11 层稀疏 MLA
2026-08Qwen3.8-Flash-Next 125B/6B · 48 层GDNGDNGDNgAT256K3.0 GiBQwen4 架构预览;新东西在 51B 的 n-gram 嵌入表

⭐ 这张表的一句话落点:同一个 128K 长度, 从 GPT-3 的 576 GiB 到 DeepSeek-V4-Flash 的 697 MiB,六年 846 倍 —— 而这不是一个旋钮拧出来的,三个旋钮各贡献了一段

⭐ 这张表还能一眼看出八件事 (含上面那条的完整推导,以及一条表自己长出来的结论)

⭐ 这张表一眼能看出八件事 (以下统计恒按全部 44 行算,切到 Highlight 也不变 —— 不然「有几家怎么样」这种话会跟着显示模式变,那就不是结论了)

  1. 表里 44 家:17 家是「便宜的层 + 一层贵的」,25 家每层同构, 2 家是别的混法。那 17 家混合的,配比落在 1:1 ~ 7:1 —— 而拆成两族看更有意思:线性混合 3:1、5:1、7:1、滑窗混合 1:1、3:1、5:1、6:1两族重叠在 3:1 ~ 6:1。⭐ 两类看起来毫不相干的混合落进同一段区间, 这件事本身就是一条证据。
    ⚠️ 但别说成「无一例外」Mistral 7B、RWKV-7 Goose 就是纯便宜层、一层贵的都没有的反例。 ⭐ 而这个反例恰恰值得讲 —— 纯滑窗真的上过生产、用了两年, 它正好说明那条 L2M 条件渐近命题, 管不了 32K 这个尺度。
  2. 前几行是基线,也是一条完整的小史:MHA → MQA(砍到 1 组)→ GQA(折中)→ MLA(改压缩) → CLA(跨层共享)——全都只在动「每个 token 存多少」这一个旋钮
  3. ⭐ 扫一眼颜色搭配:混合的那 17 家里 16 家是「冷色 + 黄橙」(便宜的层配一层全注意力)。 只有 GLM-5.3-Flash 是「蓝 + 红」—— 它配的那层「贵的」,本身已经是稀疏的了。
  4. ⭐⭐ 把上下文那一列排一下:做到 1M 以上的 12 家,无一例外都动了旋钮②或③; 纯全注意力那一档最高只到 288K(Mistral Large 3)。最硬的对照来自 MiniMax 自己: 01 用 7:1 线性外推到 4M,M2 退回纯全注意力只剩 192K——同一家、同一批人,差二十倍。
  5. ⭐⭐ 把 KV cache 排一下:576 GiB(GPT-3)到 697 MiB(DeepSeek-V4-Flash),整整 846 倍 —— 两端都取 100B 以上的,不然「最小」会落到 Mistral 7B 头上, 那比的是模型大小不是机制。 而这不是一个旋钮拧出来的——MHA→GQA 砍头数、MLA 改压缩、CLA 跨层共享是旋钮①; 线性把大部分层的 KV 直接删成零是旋钮③;CSA/HCA 存压缩池是旋钮②。 三个旋钮各贡献了一段。⭐ 而 RWKV 那一行干脆是 0——纯 RNN 没有 KV cache 这个东西。
  6. ⛔⛔ 「上下文」这一列报的是声明,不是能用 数来自各家 config 的 max_position_embeddings,而这个字段各家含义并不一样—— MiniMax-01 那格 config 写着 10,240,000,官方只声称训练 1M、外推 4M; Llama 4 Scout 声称 10M 也是同一回事。 声明和能用之间还隔着一整个 benchmark 的落差:小米自己的模型卡就写着 MiMo-V2.5-Pro「到 1M 时塌到 0.00」。⭐ 看到「支持 N 万上下文」,先问是谁、在什么任务上、测出多少分
  7. ⭐⭐ 最反直觉的一条:MLA 之后,KV cache 跟「模型多大」脱钩了。 DeepSeek-V2 是 236B、V3 是 671B,参数差 2.8 倍,KV 却只差 1.7%(8.4 GiB vs 8.6 GiB)。 因为 MLA 的 KV 只跟「层数 × (kv_lora_rank + rope 维)」走—— 跟专家多少、hidden 多宽、总参多大一点关系都没有。 MHA 时代 KV 是跟着模型一起长的,这条链在 MLA 这里被剪断了
  8. ⭐⭐ 最后一条是这张表自己长出来的,不是我们想说的。 把 2025-11 ~ 2026-03 这五个月单独看:6 个模型发布,用到 7 种层类型 (DSA、FULL、GDN、GQA、MLA、SWA、gAT)—— 而其中在这个窗口里才首次出现的,只有 0 种
    换句话说,那五个月发布很密,密的是模型,不是机制:全是既有招式的重新组合与规模化。 ⭐ 这是一段「消化期」—— 新机制在 2025 年秋天集中冒出来(DSA、GDN、KDA), 然后各家花了一个冬天把它们搬进自己的旗舰。 下一波新东西要等到 2026 春天(Gemma 4 的 K=V 共享、V4 的 CSA/HCA)。
    ⛔ 而这条结论是被 Highlight 视图逼出来的:全量 44 行里那段看着只是「少几行」, 一筛成机制主线,空白立刻刺眼。筛选不只是省地方,它还是一种体检。

这一整块最该带走的一句MiniMax 一家、三代模型, 把旋钮③和②各试了一遍,中间还退回过基线(线性 → 退回全注意力 → 稀疏),而且每次转向都公开写了理由 —— 「三个旋钮」这个框架不是我们归纳出来的,是有人真的一个一个试过去了。


第 零 节

起点:RNN —— 被注意力补的那个东西

先给一个反直觉的事实:你现在用的每一个大模型,在往外吐每一个字的时候, 都退回成了 1990 年那条链的形状。
Transformer 赢在训练能并行。可生成的时候,它一个 token 一个 token 地走, 每走一步都要把全部权重从显存里搬一遍 —— 这跟 RNN 一模一样
它没有治好 RNN 的病,它只是把病从训练挪到了推理;而且挪过去之后更重。

⭐ 这一节跟封面那 512 倍是什么关系?—— 封面问的是「能记多长」, 这一节问的是「它记东西的形状是什么」。 两者是同一件事的两面:正因为那个形状会一路变长,「记得更长」才会贵得离谱。 要涨 512 倍,就得先看清要涨的是什么。

📌 把名字拆开:RNN = Recurrent Neural Network = 循环神经网络。

R 指的是状态回到自己输入端的那条边(recurrent connection)。 词源是拉丁 recurrere = re(回)+ currere(跑)。

这个词是从解剖学借的,不是数学造的 ——  解剖学里它一直用来描述环状结构:Cajal 1901 在小脑皮层观察到 "recurrent semicircles",Lorente de Nó 1933 用高尔基染色发现 "recurrent, reciprocal connections"。在那之前神经系统被当成纯前馈的, 1940 年代才有人正经提出脑内存在反馈。 §七 末尾 Albert Gu 那句「像一个大脑」,正好把这个词还回去了。

中文误译提醒「递归神经网络」指的是 Recursive Neural Network(沿语法树递归),是另一类模型。 读中文文献时这两个词经常被混用,看到「递归」先确认原文是哪一个。

所以这一节不是背景介绍,是本专题的舞台说明四个问题,四张图。

0.1 先把它是什么说清楚:一个固定大小的状态

序列有先后,所以得有个东西把历史带下去。RNN 的答案是带一个固定大小的状态向量 h —— 全部设计就这一句

RNN —— 把「之前发生了什么」塞进一个固定大小的盒子,一步一步往下传 全部设计就这一句。后面所有的好处、所有的痛点,都是这一个决定的后果。 输入 状态 h —— 整个模型的记忆 输出 原样抄写,不训练 ① 折叠着看 Elman 1990 原始画法 Cognitive Science 14 输出 y y = W_y · h 隐藏 h [d] —— 固定大小 输入 x 这一步的词 context 层 上一步的 h 原样复制一份 固定权重 1.0,不训练 那条虚线不是学出来的—— 原文写死「one-for-one basis, with fixed weight of 1.0」。整个循环就靠这一条硬接线。 ② 展开着看 它其实就是一条链,每一格都得等前一格算完 t = 1 x1 h1 y1 t = 2 x2 h2 y2 t = 3 x3 h3 y3 t = 4 x4 h4 y4 t = n xn hn yn 上一步的 h 不是总有 —— 见 ③ 红底那一行就是全部问题的根源:h3 要用 h2,h2 要用 h1 —— 一百万步就得排一百万轮。 竖着的绿、蓝箭头彼此不相干—— 能并行的方向一直都在,被卡住的只有横着这一个。 ③ 那 y 到底要不要接回去当 x —— 看这条 RNN 站在哪个位置上 所以「自回归」不是 RNN 的性质,是用法的性质 语言模型(只有解码器) y → 下一个 x:有 吐一个字,接回去当下一个输入。 但只在生成时有 —— 训练时喂的是 真实的下一个词(teacher forcing)。 翻译 · 编码器那一侧 y → 下一个 x:没有 它只管把源句读成一个状态。 逐步的 y 根本不用 —— 要的只是读完之后那个 h。 翻译 · 解码器那一侧 y → 下一个 x:有,而且还多一条 ① 自己吐的接回去当下一个输入; 还要去看编码器 —— 早期是一个 context 向量,后来就是交叉注意力。 所以这一张图画的是「一条 RNN」,不是「一个模型」 同一个 RNN 单元,摆在编码器就不自回归,摆在解码器就自回归。上面 ② 那条链是它共有的部分;y 接不接回去,是外面怎么用它决定的。 而这也说明 ② 那个「排一百万轮」的痛是跑不掉的它来自红色那条横线(状态),不是来自 y 那条虚线 —— 哪怕你根本不自回归(编码器那一侧),这条链照样得一格一格排 拆开一格看:里面就是两个矩阵乘、一个加法、一个非线性 ht = f( Wh · ht−1 + Wx · xt + b ) yt = Wy · ht 形状:h 是 [d],Wh 是 [d × d] d 就是它能记住的全部容量—— 喂 10 个词还是 10 万个词,盒子一样大。
同一个东西的两种画法。看完只要带走一件事:竖着的箭头一直可以并行,被卡住的只有横着那一根 —— 后面所有「让它变快」的努力,动的都只是那一根。
出处与口径

Elman 1990《Finding Structure in Time》—— context units「copied … on a one-for-one basis, with fixed weight of 1.0」;LSTM: Hochreiter & Schmidhuber 1997;GRU: Cho et al. 2014

0.2 关键的是哪一列:为什么它在加速器上快不起来

⛔ 先别去比总计算量。O(n·d²)O(n²·d) 谁大,取决于 nd 谁大; Vaswani 原文说的是 n < d 时自注意力更快,而那正是当年的常态。 固定不变的是另一列 —— 串行步数。

层的类型每层计算量串行步数两个位置之间的最长路径
自注意力O(n² · d)O(1)O(1)
循环(RNN)O(n · d²)O(n)O(n)
卷积O(k · n · d²)O(1)O(log_k n)

这张表是 Transformer 作者自己算的(arXiv 1706.03762 表 1)。 中间那一列就是全部答案。

为什么 RNN 在加速器上就是跑不快 —— 同一块权重被搬了 n 次 「串行」在硬件上的具体形态就是这个:权重每一步重搬一遍,而每次只配一条 batch 那么窄的向量。 带网格的块 = 从 HBM 搬进来的权重 配给它的激活 串行:一步一格 并行:整段一次 Ⓐ RNN 每一个时间步,都要把同一块权重 W 从 HBM 重新搬进片上一次 训练和推理都一样 W [k·d × d] · [d×B] 第 1 步 W [k·d × d] · [d×B] 第 2 步 W [k·d × d] · [d×B] 第 3 步 W [k·d × d] · [d×B] 第 4 步 W [k·d × d] · [d×B] 第 5 步 W [k·d × d] · [d×B] 第 n 步 NVIDIA 官方文档的说法是「a GEMM with one dimension of one」—— 名义上矩阵乘,实际是矩阵乘向量。 Ⓑ Transformer 训练 同一块权重只搬一次,n 个位置一起喂进去 W [k·d × d] · [ d × (n · B) ] —— n 个位置全在这一块里 一次搬运换来 n 倍的活干。这就是「用平方的计算量买完全的并行度」那笔交易的硬件形态。 算术强度 —— 说人话就是「跑一趟,能干多少活」 第一次读可以先跳过这一格 它决定你是在等算力,还是在等内存 🚚 RNN 每一步 搬的是同一块权重 W (车一样大,跑一趟的成本一样) 算术强度 B 车照样跑一趟,车上只有一个 batch 的活 🚚 Transformer 训练 搬的是同一块权重 W (车一样大,跑一趟的成本一样) 算术强度 n · B 同样跑一趟,装了 n 倍的活 短序列:显存还让你开这么大 长序列:只能开到这里 S 越长,显存允许的 batch 越往左 可行区为空:能给的上限,已经跑到要求的下限左边 TPU v7 屋脊线 313 FLOP/byte = 2307 TFLOP/s ÷ 7.37 TB/s(BF16) batch 1 batch 32 batch 128 这一整段 = 在等内存 这一段 = MXU 吃饱了 硬件要你站进来,显存不让你进来 Transformer 训练 = n·B → 冲出图外 那个 k(LSTM 是 4、朴素 RNN 是 1)上下一约就没了 —— 强度跟门数、跟隐藏维都无关。(激活的搬运比权重小两个数量级,略去。) 口径:两个 ▼ 的位置是示意—— 具体能开多大取决于模型和显存;这张图只说方向:S 一长它必然往左走,早晚越过 313 横轴是线性的,不是双对数:换成 log,1 和 313 会挤在一起,「差两个数量级」当场就看不见了。
这张图要带走的是那个「跟 d 无关」:不管隐藏维是 512 还是 8192,RNN 每一步的算术强度就等于 batch size它意味着「把模型做小」根本救不了 RNN。⭐⭐ 然后看最下面那根横轴 —— 那是「两头堵死」的图形版:最粗的那根竖线是地板(313,站右边才算喂饱),上面两个蓝三角是显存的天花板(序列一长它往左走),而它们之间那块画满斜线的地方就是「硬件要你站进来,显存不让你进来」。⚠️ 三角的位置是示意、图上没标数字:这张图只承诺方向,不承诺数值。⛔ 横轴是线性的不是对数 —— 换成 log,batch 1 和 313 会挤在一起,整张图唯一的论点当场就看不见了
出处与口径

NVIDIA《Recurrent Layers User's Guide》—— 「a GEMM with one dimension of one」、「can combine these GEMMs over the minibatch size, but not over different sequence steps」;Vaswani et al. 2017 (arXiv 1706.03762) 引言

⛔ 两头堵死:batch 是它唯一的算术强度来源, 而 Vaswani 引言那句原话说的正是另一头 —— 「memory constraints limit batching across examples」序列一长,显存就不让你把 batch 开大。

0.3 本节高潮:解码时,Transformer 又变回了这个形状

📌 两个词先说清,后面一直要用:把整段输入一次算完prefill(预填充);之后一个一个往外吐decode(解码)。 ⭐ 这一讲后面有三处结论在这两个阶段是相反的 ——  看到一个「省了多少」,先问它说的是哪个阶段。

先把那三处列出来,读到时你会认出它们 —— (这一栏是路标,现在不用懂,读到那儿回头看一眼就行)

旋钮prefill 这边decode 那边在哪一节
① MLA压缩不生效:训练与 prefill 的前向要把 KV 解压出来算 省得最狠:只读那 576 维§五
② 稀疏可能一点不省:要先算出注意力图才知道挑谁 —— NSA 论文 §2 的第一个坑说的就是这个 每步只读 k 条§6.3b
③ 线性要靠分块才榨得出并行度(块内并行、块间串行) 就是一条纯递推,每步只碰那块固定大小的板子 §7.4

所以「省了 N 倍」这句话,不带阶段就是半句话。 §九那张代价表专门有一列「⭐ 省在哪个阶段」,就是为了逼出这一问。

而生成文字的时候,Transformer 又变回了 1990 年那条链的形状 它没有治好 RNN 的病,它只是把病从「训练」挪到了「推理」—— 而且挪过去之后更重。 串行 并行 串行 + 一路变长的 KV Ⓐ RNN 训练和推理都一样 串行 每步搬:W + 一个固定大小的 h 算术强度 = B t1 W + h t2 W + h t3 W + h t4 W + h t5 W + h tn W + h 一步一格,权重每步重搬 Ⓑ Transformer 训练 它当年赢下来的地方 并行 整段只搬一次:W 算术强度 = n · B t1 … tn 全部一起算 搬 W 一次 一次搬运换 n 倍的活干 Ⓒ Transformer 解码 你现在用的每个大模型 病在这里 串行 每步搬:W + 越来越长的 KV 权重那半 = B|KV 那半 = G(跟 B 无关) t1 W + KV t2 W + KV t3 W + KV t4 W + KV t5 W + KV tn W + KV 加 batch 只救得了权重那半,救不了 KV 那半 Ⓒ 那一行的算术强度,必须拆成两半看 权重那一半:全 batch 共享一份,所以攒 batch 有用 —— 强度 = B。这是所有「加大 batch 提吞吐」的依据。 KV 那一半:每个请求各存各的。batch 一大,读的字节和算的 FLOPs 同比例涨 —— 强度恒等于 GQA 的组大小,跟 B 无关。 把 Ⓐ 和 Ⓒ 摆在一起看 —— 这就是整个专题三的舞台 两行用的是上面同一组 t1…tn 的位置 两边一模一样的部分 每步把整份 W 重搬一遍 W W W W W W 这一排两边共用,区分不了任何事 不一样的只有一样:每步还得额外搬什么 Ⓐ 状态 h 平的 —— 跟上下文多长无关 Ⓒ KV cache 一路长 —— 128K 时比权重还大 h 这条画出来只为当对照 —— 它比权重小两个数量级,账上一直略去(见上一格)。这里比的是会不会随 token 数变长,不是比谁占得多。 整门课要拧的,就是上面那排紫条 后面三个旋钮拧的全是同一件事:让 Ⓒ 那一排每步额外搬的东西变小—— 存少点(①)、看少点(②),或者干脆换回上面那排红的(③,线性注意力)。
⭐⭐ 本节的落点,看最后那一格就够。上面那排 W 两边共用,所以它区分不了任何事;真正分开两条路的只有下面那两排条的宽度 ——红的从头到尾一样宽(状态 h 跟上下文多长无关),紫的每一步都比上一步长,到 tn 直接冲出格子(KV cache)。整个专题三都发生在紫色这一排上。
出处与口径

⛔ ⛔ Ⓒ 那一行的算术强度,必须拆成两半看(接上图)

⭐ 拿 GQA-8 算就是 8 FLOP/byte,对着上一格那条 313 的屋脊线差约 39 倍 —— 而且攒 batch 一点都救不了。这正是后面三个旋钮要动它的原因。

⛔ 口径(这条必须钉死,否则跟上面那条 313 不是一套尺子):读的字节 = K 和 V 各 S·d 个元素 × 2 B = 4·S·d;算的 FLOPs = G 个 query 头 × (QKᵀ + AV) 各 S·d 次乘加 × 2 = 4·G·S·d。两者一除,强度就是 G。⭐ 拿 MHA(G=1)自检:1 FLOP/byte —— 正好对上业界那句「decode 的注意力强度约等于 1」。

Ⓐ 和 Ⓒ 都是「一步一个,每步搬一遍权重」。 唯一的区别是每步还得额外搬什么:
RNN 搬的是一个固定大小的状态;Transformer 搬的是一路线性变长的 KV cache —— 128K 时它能比权重本身还大(下一节算给你看)。
⭐ 后面三个旋钮拧的全是同一件事:让这一行每步要搬的东西变小。

0.4 三个痛点,各自通向哪

RNN 的三个痛点 后面三十年的路线图,就是这三条各自的解药 三个痛点,三个画面 一个一个看 ① 算不快 一排人传话,后一个必须等前一个开口 1 2 3 4 5 6 7 序列多长,就排多少轮 而且每一轮都要把全部权重重搬一遍 ② 记不住 话传到后面,越来越淡 反传要连乘 n 次 小于 1 就指数衰减 —— 学不到远处 ③ 装不下 一整段话,塞进一个固定大小的小盒子 h 不管多长,都压进这一个向量 长句子必然丢东西 解药:Transformer 把循环整个拿掉 欠下:注意力矩阵 O(N²) 解药:门控 → 注意力 LSTM 1997 / GRU 2014 任意两个位置只隔一步 解药:Bahdanau 2014 别只看最后那个向量 注意力的出生证明 而第 ① 条后来被反着又走了一遍 —— 这就是专题三真正的主脊 Transformer 用「放弃状态」换来了并行度。线性注意力和 Mamba 这一支,是想把状态请回来—— 因为有状态,每步要搬的东西才不再变长。 但状态一回来,串行也跟着回来。于是又得把并行度找回来:chunk 化、parallel scan
这一页是整个专题的路标:后面每一个变体都能追回到这三条里的某一条。最底下那一格是主脊 —— 第①条后来被反着又走了一遍,而「必须线性到能被 scan」就是线性注意力那些公式的由来。
出处与口径

Bengio, Simard, Frasconi 1994(梯度消失);Hochreiter & Schmidhuber 1997(LSTM);Bahdanau et al. 2014 (arXiv 1409.0473)「a fixed-length vector is a bottleneck」

Vaswani et al. 2017 (arXiv 1706.03762) 表 1;Martin & Cundy 2018 (arXiv 1709.04057):非线性依赖挡住并行,只有线性依赖能用 parallel scan 扫

⚠️ 一个常见的张冠李戴:「固定长度向量是瓶颈」不是 Sutskever 说的。 他那篇只是描述做法(映射到「a vector of a fixed dimensionality」); 「这是个瓶颈」是 Bahdanau 那篇的原话(arXiv 1409.0473「we conjecture that the use of a fixed-length vector is a bottleneck」)。 别把后人的批评安到原作者头上。

0.5 本节落点

这一节要留下的只有一句解码那一行,从 1990 年到今天,形状没变过。

变的只是每一步额外要搬的那个东西:RNN 搬一个固定大小的状态, Transformer 搬一路变长的 KV cache。 后面三个旋钮拧的全是它 —— 所以这一节不是背景介绍, 是本专题的舞台说明

📌 这一节的出处清单(全部一手核过)
  • Elman 1990《Finding Structure in Time》—— context units「copied … on a one-for-one basis, with fixed weight of 1.0」。
  • Bengio, Simard, Frasconi 1994—— 梯度消失;Hochreiter & Schmidhuber 1997—— LSTM。
  • Bahdanau et al. 2014(arXiv 1409.0473)—— 「fixed-length vector is a bottleneck」。
  • Vaswani et al. 2017(arXiv 1706.03762)—— 引言「This inherently sequential nature precludes parallelization within training examples…」+ 表 1 三列。
  • NVIDIA《Recurrent Layers User's Guide》—— 「a GEMM with one dimension of one」; 「can combine these GEMMs over the minibatch size, but not over different sequence steps」。
  • Martin & Cundy 2018(arXiv 1709.04057)—— 非线性依赖挡住并行, 只有线性依赖能用 parallel scan 扫,实测最高 9× 加速。
  • 拐点 313 FLOP/byte = v7 官方每芯片 FP8 4614 TFLOP/s(BF16 取一半 2307)÷ 官方 HBM 7.37 TB/s。
    这个 313 不是本讲新造的数 —— 它就是专题二 整整一节在立的那条屋脊线每从显存搬一个字节,这台机器配套能算 313 次。 那边花了十几分钟把它立起来,这里一句话就能用。
    (2026-09-12 补:原来这里只有算式、没点名出处, 于是这个数在两讲之间是断的 —— 台下不会自己把它接上。)

第 一 节

MHA —— 把循环拿掉,代价是什么

RNN 疼在三处:算不快、记不住、装不下。注意力最早只解决了第三条,而且是作为 RNN 的一个附件出现的。于是 2017 年有人问:既然这个附件这么好使,能不能把 RNN 整个扔掉,只留附件?

这一节要回答的就一件事:KV cache 是从哪儿来的 —— §零那张「解码时又变回 RNN」图上 Ⓒ 那一行的病根,在这一节。

1.0 它是怎么被发明出来的

注意力是怎么被发明出来的 三步 —— 每一步都在修上一步的一个具体毛病 上一步哪儿不对 这一步怎么修 被硬件推着走的那一刀 ① 2014 一个向量装不下 Bahdanau / Cho / Bengio 今天 开心 一个 向量 整段译文都从这儿出 句子越长,挤得越狠 原文:we conjecture that … a fixed- length vector is a bottleneck(作者自己的推测) 修法的关键是那个「软」字 硬挑一个词不可导;加权平均可导 ② 2015 第一刀 打分太慢 加性 → 点积 加性(旧) 每一对,过一个小网络 net net net net net net net net net n² 次 小网络 点积(今天) 整张表,一次矩阵乘 Q · Kᵀ 一次 ③ 2017 第二刀 那还要 RNN 干什么 两点之间要走几步 循环:一步一步传 1 2 3 4 5 ①→⑤ 要走 4 步 自注意力:直接连 1 2 3 4 5 ①→⑤ 一步 同一句话里,论文认了代价 加权平均降低了有效分辨率 多头就是拿来补这个的 第二格那一刀,是这门课的主线第一次出现 论文自己写的理由是:两者理论复杂度相仿,但点积快得多、省内存,因为它能用高度优化的矩阵乘实现 换句话说 —— 选点积不是因为它更准,是因为它能变成矩阵乘。这一讲后面每一个变体,几乎都能追到同一句话上。
⭐⭐ 三步,每一步都在修上一步的一个具体毛病:一个向量装不下 → 软对齐;小网络打分太慢 → 点积;既然能直连 → 扔掉循环,而扔掉之后分辨率变糙 → 多头补回来。中间那格是本课主线第一次露面 —— 点积胜出不是因为它更准,是因为它能写成矩阵乘,这是论文原话。
出处与口径

① Bahdanau 等 arXiv 1409.0473

② 点积打分出自 Luong 等 arXiv 1508.04025(2015),该文同时给了 dot / general / concat 三种打分

⛔ Vaswani 等 arXiv 1706.03762 §3.2.1 加的是 1/√d_k 那个缩放,不是点积本身 —— 原话是「identical to our algorithm, except for the scaling factor」;③ 同文 §3.2.2(多头补偿分辨率)

⚠️ 画面里的句子、方框数量都是示意,不对应任何一次真实实验

读论文的姿势看一个机制,先找它在修上一版的哪一个具体毛病。 「有人灵光一闪设计了注意力」什么也教不了你; 「一个固定长度向量装不下长句子,于是让解码器自己去源句里软性地找」 —— 这个你下次能照着用。

1.1 先看它换掉了什么

2017 年那一刀 —— 把那根横箭头拿掉,换成「每个位置直接看所有位置」 开篇讲 RNN 那三个痛点里的第 ① 条,就是在这里被解掉的。而解法本身,就是这一讲后面要还的那笔账。 串行:只能一步一步走 要算的格子 被因果遮罩挡住的未来 Ⓐ RNN:一条链 信息只能沿着链爬 讲 RNN 时说过 t1 t2 t3 t4 t5 t6 t1 想影响 t6,得经过 5 跳—— 每一跳都是一次矩阵乘,而且必须排队。 串行步数 O(n) · 最长路径 O(n) · 连线数 n − 1 信息走得越远越容易被冲淡 —— 这同时也是痛点 ② 梯度消失的几何解释:梯度也得沿着同一条链爬回去。 而且这条链一次只能动一格,加速器上再多的并行单元也用不上 Ⓑ 自注意力:一张 n × n 的表 每一格 = 一个 query 对一个 key 的打分 q1 q7 qn k1 kn ← q7 这一行:它看得到 k1…k7 灰格 = 被因果遮罩挡住的未来 (softmax 之前置成 −∞) 串行步数 O(1) · 最长路径 O(1) · 格子数 n² —— 这才是这一刀的账单 任意两个位置之间只隔一格,而且整张表可以一次算完—— 左边那条链两样都做不到。 这一刀换来了什么,又欠下了什么 换来的:串行步数从 O(n) 掉到 O(1)—— 整段序列一次算完,加速器终于喂得饱了(这正是「RNN 在硬件上为什么慢」那张图那笔账的反面)。 欠下的:连线数从 n 变成 n²。 而且注意 —— 状态没了:RNN 那个固定大小的 h 被换成了「把所有历史原封不动留着」。
左边是 §零 那条链,右边是它的替代品。看完带走一件事:要算的格子从 n 变成了 n²,而那张表就是后面反复出现的「注意力矩阵」本人
出处与口径

Vaswani et al. 2017 (arXiv 1706.03762) 表 1:自注意力 串行步数 O(1)/最长路径 O(1);循环层两项都是 O(n)。因果遮罩见同一篇论文 §3.2.3:把非法连接「setting to −∞」

⛔ 被换掉的不只是那根箭头 —— 状态也没了。 RNN 那个固定大小的 h,被换成了「把所有历史原封不动留着」。 这一句后面每一节都要用到。

1.1b 那它到底怎么把信息传过去的

📌 先认一个后面要用两次的单位:困惑度(perplexity)。 它衡量「模型对下一个词有多没把握」—— 越低越好。 粗略地讲,困惑度 30 就是「大约在 30 个词之间犹豫」。

⚠️ 它只在同一个基准、同一份数据上可比 ——  所以后面 §五那张表的 30 上下,和 §六那张表的 5 上下, 不能互相比,只能各自看组内差多少。

一次注意力,干的就是一件事 —— 每个位置都去全场取一次货 跟着最后那个 token 走一遍:它怎么提问、别人怎么报价、货怎么汇到它身上 query 我想找什么 key 我这儿有什么 value 被选中我就交这个 ① 每个位置都长出三样东西 同一个向量,乘三个不同的矩阵 t1 q k v t2 q k v t3 q k v t4 q k v t5 q k v t6 q k v ② 最后那个 token 拿它的 q 去对每块牌子 打分 → softmax → 一组加起来 = 1 的权重 q t6 的问题 k k k k k k softmax 之后 5% 30% 8% 45% 7% 5% 加起来 = 1 ③ 按权重把所有人的 value 加起来 权重越大,交上来的那份占比越大 v v v v v v t6 的新向量 一个向量里,现在装着 全场按需加权的内容。 为什么标题敢叫「Attention Is All You Need」 RNN 要让 t1 影响 t6,得一跳一跳传五次;上面这一步 —— 一跳。而且六个位置是同时做的,不是排队。 整个过程只有矩阵乘和一次 softmax,没有任何循环 —— 所以它能一次性并行算完整个序列。 两个最常被跳过的「为什么」 为什么要投三次,不能只用一个向量?—— 去图书馆找书:你脑子里那个需求(q)跟书脊上印的标题(k)不是同一个东西。 那 v 为什么还要跟 k 再分一次?k 是书脊,v 是书里的内容—— 「凭什么被选中」和「被选中之后交出什么」,本来就是两回事。
跟着最后那个 token 走一遍:它提问(q)、每个位置报价(k)、按权重把所有人的货(v)加起来。一步之内够到全场,而且六个位置同时在做 —— 这就是那句标题的意思:混合信息这件事,只要注意力就够了。
出处与口径

「query / key / value」与「输出是 value 的加权和」是 Vaswani 2017 论文 §3.2 的原话,不是本课编的比喻;图中那组权重(5/30/8/45/7/5 %)是示意值,不是实测

⚠️ 本图只画注意力 —— 一层 Transformer 里还有 FFN、残差、归一化,它们不在本专题这条轴上(本专题的账本只有 KV cache)

1.2 拆开看:一层里到底在算什么

一层里到底在算什么 —— 论文自己的说法就是「检索」,不是我们编的比喻 原文:mapping a query and a set of key-value pairs to an output … the output is a weighted sum of the values Q 要找什么 K 挂出来的牌子 V 牌子后面的货 打分 + 归一 ① 三件套都是从同一个 x 投影出来的 同一个 token,用三套不同的权重看它三次 x 这个 token Q = x·W_Q 我要找什么 K = x·W_K 我是什么,挂个牌 V = x·W_V 我肚子里有什么货 为什么要投三次而不是直接拿 x 比 因为「我想找什么」和「我能提供什么」本来就是两回事 同一个词当 query 时该问的问题,跟它当 key 时该挂的牌子,不该是同一个向量。 而 V 又跟 K 分开,是因为「凭什么被选中」和「被选中之后交出什么」也是两回事。 三个投影 = 三个可训练矩阵,这也是 KV cache 里存的那两样东西的出处 ② 一次注意力 —— 不是「挑一家」,是「按权重把每一家的货混成一碗」 Attention(Q,K,V) = softmax(QKᵀ / √d_k) · V Q 「放在哪儿了?」 拿着这个去挨家问 小明 V 打分 14 ÷√d_k → 1.24 V 打分 -7 ÷√d_k → -0.62 钥匙 V 打分 22 ÷√d_k → 1.94 放进 V 打分 31 ÷√d_k → 2.74 抽屉 V 打分 38 ÷√d_k → 3.36 上排 = K(挂出来的牌子) 下排 = V(牌子后面的货) 不除 √d_k 抽屉 99.9% 这一整条就是输出的那一碗 —— 长度恒等于 100%,softmax 不许有人弃权 整条被一家吃掉:模型只看得见「抽屉」 另外四家加起来 0.09%—— 在这根条上不到一个像素。 除以 √d_k 钥匙 13% 放进 28% 抽屉 52% 这一整条就是输出的那一碗 —— 长度恒等于 100%,softmax 不许有人弃权 混出来的是「放进 + 抽屉」,这才答得上问题 前两家占 80%,剩下三家还留着 20% 的余地。 打分那一步(Q·Kᵀ)是 n × n 个数—— 但它不落地,只在片上过。贵在要算的次数,不在显存。 真正要留下来的是牌子和货(K 和 V)—— 下一个 token 还要挨家问一遍。这两份就是 KV cache,也是本讲唯一的账本。 那个 √d_k 不是玄学,推导链在论文脚注里,两行就能讲完 假设 q、k 各维独立、均值 0、方差 1,那么 q·k = Σ qiki均值是 0,方差是 d_k—— 标准差就是 √d_k。 所以除以 √d_k 只做一件事:把打分的方差拉回 1。维度越高分数摊得越开,不拉回来 softmax 会被推到「几乎全是 0 和 1」的角落,梯度就没了
⭐⭐ 看第二格那两根横条就够了 —— 它们破的是全课最常见的那个误解:注意力不是「找出最像的那一个」,而是把五家的货按百分比混成一碗;而且这一整条的长度永远是 100% —— softmax 不许有人弃权(§六 讲 attention sink 时会回到这一句)。左右两根条唯一的差别就是除没除 √d_k:不除,整条被一家吃掉、模型只看得见「抽屉」;除了,才混得出「放进 + 抽屉」。⭐ 所以 √d_k 决定的不只是梯度,是这一层到底在「挑一家」还是在「混一碗」而牌子和货(K 和 V)要留着给下一个 token 用,留下来的那两份就叫 KV cache。
出处与口径

Vaswani et al. 2017 论文 §3.2:「a weighted sum of the values … compatibility function of the query with the corresponding key」

同一篇论文 §3.2.1 与脚注 4:「the dot products grow large in magnitude, pushing the softmax function into regions where it has extremely small gradients」

⚠️ 「query / key / value」不是我们编的比喻,是论文自己的措辞: 「mapping a query and a set of key-value pairs to an output … the output is a weighted sum of the values」
那个 √d_k推导链在图里的蓝框(论文脚注 4,两行)。
说白了:它就是一个标准差。 除掉它只做一件事 —— 把打分的方差拉回 1。

两个「为什么不那样做」 把同一组分数走三条路,柱子长什么样,一眼就知道 能用 不能用 检索是非对称的 ① 注意力最后一步是「加权平均」—— 所以权重只能长成一种样子 同一组分数,三条路 原始分数 昨天 +2 苹果 -1 天气 +3 很好 +1 ← 有一个是负的 直接当权重 负着看一眼? 0 +2.0 昨天 -1.0 苹果 +3.0 天气 +1.0 很好 除以总和 和为 1 了,负柱还在 0 0.40 昨天 -0.20 苹果 0.60 天气 0.20 很好 softmax 全部朝上 0 0.24 昨天 0.01 苹果 0.66 天气 0.09 很好 ② 刚才那三根柱子,其实只是这张网格的一行 点越大 = 这两个词越对得上 · 所有词是同时互看的 softmax 之前 有大有小,还有负的(空心) softmax 逐行做 softmax 之后 全部朝上,而且每一行加起来正好是 1 + 因果掩码 只许看自己和前面的 挡住的那些格子,第一反应是填 0 —— 但填 0 不行 填 0 之后那一行加起来就不等于 1 了。所以要在 softmax 之前−∞ —— e 的 −∞ 次方是 0,归一化时它根本不参与分母。 ② K 和 V 为什么要分家 —— 因为检索天生是「按 A 找,取回 B」 一张图书馆卡片 K ≠ V(今天的做法) 书脊(K):天气 书里写的(V): 今天 25 度,多云 按书脊找,取回内容 问:外面怎么样? 跟「天气」像 取回:25 度,多云 拿到的正是我缺的那块 如果 K = V 卡片上就是全文 书脊 = 书里: 今天 25 度,多云 找它只能靠「像不像它」 问:外面怎么样? 跟这句话不像 找不到 只找得到像我的 ③ 同一个图书馆,人类和注意力的用法完全不一样 这一步不说清,softmax 那里一定会误解 ① 人类找书 一条线,指向一本 抱着它走了 —— 其余五本一页没动 如果注意力是这样,就不需要 softmax 了 ② 注意力找书 十几条粗细不同的线,同时指向一整排 5% 30% 8% 45% 7% 5% 每本撕下相应比例的一页,混成一本新书带走 所以它其实不在「注意」,它在加权平均 —— 每本都被撕了一页,只是有的撕得多 带走一条判据:先问这个分数拿去干什么 拿去加权平均 —— 那就必须非负、和为 1、可导,softmax 一次全给了 只拿去排序 —— 那「和为 1」根本不必要。DSA 的索引器走的正是这一条:它换成了 ReLU,论文给的理由就两个字 —— 吞吐(讲 DSA 时会再碰到)。
⭐⭐ softmax 的三条要求(非负、和为 1、可导)全是「加权平均」逼出来的。所以 DSA 的索引器敢换成 ReLU —— 它只拿分数去排序,不做加权平均。右边:k 是书脊、v 是内容,检索天生非对称 —— 按 A 去找,取回 B。
出处与口径

三条性质由 softmax 的定义直接给出;柱子的数由本脚本当场算并断言

「索引器用 ReLU,for throughput consideration」出自 DeepSeek-V3.2-Exp 技术报告 §2.1

⚠️ 图书馆卡片是类比,不是论文原话 —— 论文只给了 query / key / value 三个名字

1.2b 那为什么非得是 softmax,k 和 v 又为什么不能是同一个

⭐ 这条判据的通用形式: 看到一个设计的副作用,先回头看它的优点是靠哪条性质换来的 —— 十有八九是同一条。 softmax 那条「一行加起来等于 1」,在这里让加权平均的尺度稳定(优点), 到 §六 就是 attention sink 的成因(副作用)。 而 softmax-off-by-one 这个补丁,本质就是把这一条放松掉。

1.3 多头在多什么

多头在多什么 —— 不是堆算力,是不让「平均」把不同的关注糊成一团 原文:「With a single attention head, averaging inhibits this.」—— 单头不是不够用,是会把该分开的东西平均掉。 Ⓐ 单头 一个 query,三个想去的地方 一个 query 上一个词 这句话的主语 三段前那个人名 平均之后 落在这儿 只有一组权重 —— 那根粗线又短又谁都没指到。 Ⓑ 多头(h = 8) 切成 8 份,每份 64 维,各看各的,最后拼回来 head 1 d = 64 head 2 d = 64 head 3 d = 64 head 4 d = 64 head 5 d = 64 head 6 d = 64 head 7 d = 64 head 8 d = 64 Concat → 再过一个 W_O,拼回 512 那个 W_O 到底是什么? 订在一起 = W_O 它不是新东西 —— 八个头各自往同一条残差流上加了一份,加法而已。 8 × 64 = 512—— 总维度没变、参数量没变。多头是「切开」,不是「加倍」。 这只是原始 Transformer 的取法,不是通例 本课的主角 DeepSeek-V3 是 128 头 × 128 维 = 16,384,比它的 d_model 7,168 大 2.29 倍 多头的代价,正好是本专题的题眼 好处:一个头要同时盯语法、盯指代、盯主题,只能给一个折中的答案;切成 8 个,各盯各的,互不干扰 换个说法:多头买到的不是算力,是分辨率。 而这个形状的代价,2019 年就被点名了 Shazeer 2019(MQA 那篇)摘要原话:训练很快,因为序列方向可以并行;但 —— 「incremental inference … is often slow, due to the memory-bandwidth cost of repeatedly loading the large "keys" and "values" tensors
8 × 64 = 512:多头是「切开」不是「加倍」,总维度和计算量都没变。唯一被乘上去的是 KV —— 下一节起要砍的就是它。
出处与口径

Vaswani et al. 2017 论文 §3.2.2:「jointly attend to information from different representation subspaces」、「With a single attention head, averaging inhibits this」;h = 8,d_k = d_v = d_model/h = 64

Shazeer 2019 (arXiv 1911.02150)《Fast Transformer Decoding: One Write-Head is All You Need》摘要 —— 本课模型表第二行 PaLM 用的就是它

📌 先回答一个很自然的问题:存不下,为什么不能每步重算?

因为重算的代价不是「再算一遍」,是每一步都把整段历史重算一遍。 生成第 n 个 token 时,如果不存 K/V,就得拿前面 n−1 个 token 重新过一遍投影 —— 而下一步又要重来。 整段生成的总开销从正比于 N² 变成正比于 N³

⭐ 所以这不是「省一点」的优化,是能不能用的分界。 KV cache 是拿显存换时间,而这一讲全部的账, 都是在算这笔交换到底有多贵。

1.3b 顺便回答一个立刻会想到的问题:128 个数,凭什么记得住上万个 token

上一段说多头买到的是分辨率:一个头只管 128 维。 那问题就很自然了 —— 128 个数,怎么可能记得住一整篇文章里上万个词的区别?

为什么会记混 —— 一个 128 维的脑袋,装得下多少件「互不相干」的事 每篇文章都写「每个头 128 维」,然后就过去了。可 128 个数凭什么记得住上万个 token 的区别?这张图把它算出来:严格不撞只能装 128 个,放宽成「差不多不撞」就能装上万个 —— 而「记混」正是这笔放宽的价钱。 完全不撞(两两垂直) 差不多不撞 串扰=记混 ① 先问上限:要求「谁跟谁都不沾边」,一间屋子里能立几根杆? 秩论证 · 当场验 一张桌面(2 维) 横一根、竖一根,就满了 2 根 房间的墙角(3 维) 再加一根「往里」,三根,也满了 3 根 一个注意力头(128 维) 画不出来 —— 但这个上限不用画也知道 最多 128 根。 第 129 根一定能被前面那些拼出来 —— 它不是新东西。 (脚本当场验:128 维里取 129 个向量,秩只有 128) 所以「互不相干的方向」这种奢侈品,一个头只买得起 128 件。如果模型真按这个标准办事,它一辈子也只认得 128 个概念。 ② 那就别那么讲究:只要「差不多不撞」就行 —— 容量立刻爆炸 本课当场算 · numpy 把标准从「正好 90°」放宽到「大概 90° 就行」,能塞进去多少根?下面三个表盘是真的随机丢进 128 维再量出来的 —— 量的是最挤的那一对 90°(完全不撞) 70.0° 丢 128 根进去 最挤的一对仍差 70° 才重合 90°(完全不撞) 65.5° 丢 1,000 根进去 最挤的一对仍差 66° 才重合 90°(完全不撞) 60.8° 丢 10,000 根进去 最挤的一对仍差 61° 才重合 读这三个表盘只读一件事:根数涨了 78 倍(128 → 1 万),最挤的那一对才挤了 9°。 容量几乎是白捡的。 反过来,维度一小就立刻崩:同样丢 1000 根,8 维里最挤的一对只剩 12° —— 那两根基本就是同一根,模型分不出它们。这就是「为什么不用 8 维」。 ③ 便宜没白占:「差不多」就是「有点像」,而有点像会漏票 —— 这就是记混 cos=0.2 · logit=√d·cos 先看一个干扰项有多小 正主 一个干扰项 细到画不出来 = 正主的 0.0117% 单看它,完全可以忽略。 但「可以忽略」不能乘以一万。 再看数量一上来会怎么样 100 个干扰 正主 99% 正主拿到的 1,000 个干扰 正主 90% 正主拿到的 10,000 个干扰 正主 46% 正主拿到的 漏给干扰的 生活里的同一件事:一个人在台下小声嘀咕,你听不见;一万个人同时小声嘀咕,台上的人就喊不过了。 而长上下文是两头夹击:嘀咕的人变多(N 涨),而且最吵的那个还离得更近了(②里的夹角在变小)。 这张图一次回答了本讲三个「为什么」 为什么每个头是 128 维,不是 8 维?—— 因为容量不是线性的。8 维里丢 1000 根,最挤的一对只剩 12°(几乎是同一根);128 维里丢 1 万根还有 61°。 为什么 MLA 压到 512 维还能用?—— 因为它要的从来不是「完全正交」。这正是「白送的那段 vs 赌的那段」里赌的那段赌的东西:赌压完之后大家还够不像 顺手接住后面讲稀疏那一节:稀疏注意力到底在省什么 如果记混是「干扰项太多」造成的,那少看几个反而可能更准 —— 这不是省钱的副作用,是它自己的好处。 DSA 在 128K 里只挑 2048 个(1.5625%),NSA 分三路挑。本讲前面把它们讲成「为了省」,③ 这三根条说明它们同时也在降噪
⭐⭐ 这张图的重点在②和③的对照:② 说容量几乎是白捡的(根数涨 78 倍,最挤的一对才挤 9°),③ 说这笔便宜的价钱是串扰—— 而价钱是按数量收的⚠️ ③ 的 logit = √d·cos 是推导来的不是测来的,读趋势别读绝对值。
出处与口径

⭐ 图里所有数字都是本课当场算的(numpy;随机单位向量,d=128;夹角量的是最挤的那一对),脚本里带断言 —— 不是引来的

⚠️ 但想法不是本课的:「差不多正交能装指数多个」是 Johnson–Lindenstrauss 引理的推论;「叠加(superposition)」这套解释出自 Anthropic Elhage 等《Toy Models of Superposition》(arXiv 2209.10652

⚠️ 串扰那格的 logit = √d·cos 是推导来的不是测来的:q、k 各分量 O(1) 时内积 ≈ d·cos,注意力再除以 √d,剩下 √d·cos。真实模型里 q、k 的模长会被训练调整,所以这一格读趋势,别读绝对值

答案分两层,而第二层才是有用的那层

第一层是硬上限:要求「谁跟谁都不沾边」(两两垂直), 128 维里最多只能立 128 根 杆子 —— 第 129 根一定能被前面那些拼出来。 如果模型真按这个标准办事,一个头一辈子只认得 128 个概念。

第二层是模型实际在做的:把标准放宽成「差不多不沾边」。 图②是当场算的 —— 128 维里随便丢 一万根,最挤的那一对仍然差着 61° 才会重合。根数涨了 78 倍,最挤的那一对才挤了 9°。

⭐ 所以容量不是 128,是几万。这笔便宜不是白占的 ——  「差不多」的意思就是「有点像」,而有点像会漏票。

漏票有多少?图③把它算成了两个数,这两个数放在一起才是重点:

  1. 单个干扰项小到画不出来 —— 只有正主的 0.0117%。 完全可以忽略。
  2. 但「可以忽略」不能乘以一万。一万个加起来是正主的 1.17 倍, 正主只剩 46%

⚠️ 这就是「记混」,它不是模型偷懒,是 softmax 的直接后果

生活里的同一件事:一个人在台下小声嘀咕,你听不见; 一万个人同时小声嘀咕,台上的人就喊不过了。

⛔ 而长上下文是两头夹击:嘀咕的人变多(图③那根轴), 而且最吵的那一个还离得更近了(图②那根轴,夹角在变小)。 两根轴同时往坏的方向走 —— 这就是为什么上下文一长,模型就开始「串台」。

📌 这一小节后面要用三次,先在这里打个招呼

为什么每个头是 128 维,不是 8 维 —— 容量不是线性的。 8 维里丢 1000 根,最挤的一对只剩 12°,那两根基本就是同一根。

为什么 MLA 敢压到 512 维(§五)—— 因为它要的从来不是「完全正交」。 「赌的那段」赌的就是这件事:压完之后大家还够不像

为什么稀疏注意力不只是省钱(§六)—— 如果记混是干扰项太多造成的, 那少看几个反而可能更准降噪是它的正面效果,不是省钱的副作用。

1.4 本节压轴:把形状标出来(这张图后面一直在用)

前面讲的是想法。标上形状之后,这笔账可以用眼睛读 —— 在形状里找那个会越变越长的维度就行。

主线图 · 一层 Transformer,每一步的张量形状 ⭐ 这张图会在整个专题里反复出现。每讲一个方案,就把它重画一遍,只把被改动的那一处点亮 —— 你永远知道自己在图上的哪儿。 读这张图先认字母: B 批量 · T query 长度 · S KV 长度 · D d_model · F MLP 隐层 · H 头维 · N query 头数 · K KV 头数 · G = N∕K X BTD —— 一层的输入 BTD W_Q · DNH BTNH Q BSD W_K · DKH BSKH K BSD W_V · DKH BSKH V reshape BTNH → BTKGH BTKGH BSKH BTSKG Q·Kᵀ + masks 谁能看谁 softmax BTSKG(注意力矩阵,记作 A) 这一步不落地,贵在要算的次数 BTSKG BSKH BTKGH A·V reshape BTKGH → BTNH BTNH W_O · NHD BTD 输出投影 + 残差 BTD norm BTD MLP(本专题一个字都不改它) BTD ·DF→ BTF ─gelu⊛─ BTF ·FD→ BTD + 残差 → norm BTD → 下一层 这一层算完,桌上的东西分三堆 常驻的设备 权重 W 所有人共用,不随对话变 草稿纸 中间那些量 算完就扔,不留到下一步 锁进柜子的 K 和 V 每来一个 token 就多一格 ★ 先只看一件事:图上什么东西需要留到下一个 token 这笔账在后面四张图里一直挂着 —— 每张会说清它把这笔账动到了哪一格。 📌 这一格的完整讲解在图下面的正文里 ⬆ 红底那两处就是它 —— 全图只有这两个输出要跨 token 留下来 📌 形状记号沿用 How to Scale Your Model(jax-ml.github.io/scaling-book),本图为重画。
本专题的主线图:一层 Transformer,每一步的张量形状都标出来。先只看一件事 —— 在形状里找 S(KV 长度):只有两处「留得下来」的带它,K 和 V 的输出。那就是唯一需要跨 token 留下来的东西 —— KV cache。三个旋钮各是一种跟它较劲的方式。图式借自 How to Scale Your Model,本图为重画。

先只看一件事:图上什么东西需要留到下一个 token

在形状里找 S(KV 长度)—— 只有两处「留得下来」的带它: K 的输出 BSKHV 的输出 BSKH。 S 是唯一会随对话越变越长的那一维。

⚠️ 图上带 S 的形状不止两处(K/V 的进出、两个 matmul 的操作数都带)——  「带 S」和「要跨 token 留下来」是两件事: 中间那些带 S 的算完就扔(FlashAttention 连物化都不物化)。 → 唯一要跨 token 留下来的,是 K 和 V 的输出。

⭐⭐ 给它起个名字吧 —— 这一讲的主角就是这个 S

后面每一节,你其实只要盯着它一件事 —— 这一招, 是让 S 前面的系数变小(旋钮①)、让每步读到的 S 变少(旋钮②), 还是干脆让 S 从形状里消失(旋钮③)?

这三问就是全课的骨架。 名词有几十个,但它们全都只在回答这三问中的一个。

⭐⭐ 整个专题三,就是在跟这一份 KV cache 较劲: 让每一份更小(改产生 K/V 的那两条支路)· KV 照存但每步只读一部分(改 mask 那一格)· 换成一个固定大小的状态(换一套数学,S 直接消失)。

📐 把字母换成数字 —— 那份要留下来的到底有多大 (这笔账后面四张图一直挂着;想自己核的人点开)

取序列 128K、BF16、batch 1,把带 S 的那两处换成实际占多少:

算到哪一步多大说明
每层 K + V8 GiB2 × S × K头 × H × 2 B(MHA,128 头)
× 61 层488 GiB一个用户、一段输入
对照:整个模型的权重625 GiB671B,原生 FP8,1 B/参数
⭐ 于是78%单用户就占 78%;两个并发,KV 就超过权重本身

⚠️ 这一栏在训练里是空的。 K/V 是算完就扔的激活,不跨 step 留 ——  全讲所有倍数都只对推理成立。

⭐⭐ 权重是所有人共享的一份,KV cache 是每人一份 ——  所以它决定的不是「装不装得下」,是能同时服务多少人

1.5 本节落点

这一节要留下的只有一句KV cache 就是在这里出生的。

把循环换成一张 n×n 的表,换来了训练能并行; 代价是解码时每一步都要把之前所有 token 的 K 和 V 重新读一遍 ——  而这份东西会随对话一直长下去

下一节把这笔账算成具体的字节数,再看它为什么必须省。


第 二 节

为什么是现在 —— 长上下文的两条独立动机

2.0 先回答一个更前面的问题:为什么是现在

Transformer 是 2017 年的东西。KV cache 会涨这件事, 2019 年 MQA 那篇论文的摘要里就写着那为什么全行业真正动手改注意力,是 2024 年以后?

为什么是现在 —— 同一个模型,两个场景,主角换人了 这个形状 2017 年就造出来了,2019 年就有人指着它说是问题,可全行业真正动手改是 2024 年以后。中间那几年,技术一个字没变。—— 这张图画的是那个变了的东西。 模型权重(两边完全一样) KV cache 要几张卡 ① 同一个模型(DeepSeek-V3)。权重那一段一个字节没动。 61 层 · bf16 · MLA 权重 625 GiB KV cache 0.27 GiB(细到画不出来) 一个人,短对话 上下文 4K 要 7 张卡 权重 625 GiB KV cache 549 GiB 64 个人,长文档 上下文 128K 要 13 张卡 把这两根柱子读成三个数 权重 625 GiB → 625 GiB (一个字节没动) KV cache 0.27 GiB → 549 GiB (涨了 2048 倍) KV 占总量 0.04% → 46.8% 要几张卡 7 → 13 (翻了将近一倍) 什么都没改。工作负载变了,不是技术变了。 ② 别忘了:上面那根红柱子已经是 MLA 压过之后的 —— 如果它用 MHA 呢 同口径的反事实 把四个场景摆到同一根尺子上 —— 横轴就是「要几张卡」(权重 625 GiB + KV,按 TPU v7 每 device 94.74 GiB 向上取整)。 0 4 8 12 16 20 张卡 MLA · 一个人 7 张卡 MHA · 一个人 12 张卡 MLA · 64 个人 13 张卡 MHA · 64 个人 337 张卡 —— 按这把尺子还要再往右画 11.8 个图宽 一张卡,人数差 64 倍 把第 2、3 行叠在一起读:MHA 伺候一个人要 12 张卡,MLA 伺候 64 个人要 13 张卡。同样一台机器 —— 一个换六十四个。 所以 MLA 那个 56.9×(= 488 ÷ 8.58)不是一次「优化」—— 它是把这件事从「做不了」变成「做得了」。 这张图解释了一条时间线上的怪事 2017 年这个形状就造出来了;2019 年 MQA 那篇论文的摘要里就写着「incremental inference is often slow, due to the memory-bandwidth cost of repeatedly load ing the large keys and values tensors」—— 问题早就被指出来了 可全行业真正动手改,是 2024 年以后。中间那几年,技术一个字没变 顺手给一条提问顺序,本讲后面一直在用 问「省了多少」之前,先问「省的是哪一样」。权重是所有人共享一份,KV cache 是每人一份 —— 它们根本不是同一类开销。 所以 KV cache 不是「显存里的一项」,它直接决定你能同时服务多少人这一条到专题六会变成 batch size 的硬上限。
⭐⭐ 只盯灰色那一段:两根柱子里它完全一样。涨的全在红色那一段。⚠️ 「要几张卡」只算了装得下装不下,没算带宽 —— 带宽那一笔在 §9.1 还;换别的硬件只是刻度变,结论不变。
出处与口径

装置偷自知乎 姜富春《deepseek 技术解读(1)-彻底理解 MLA》(zhuanlan.zhihu.com/p/16730036197)—— 他用 Qwen-72B 做的这个对照,本图换成本讲一直在用的 V3 口径重算

⚠️ 数全部来自本讲前面已核过的三个:权重 625 GiB(671B 原生 FP8)·MLA 的 KV 8.58 GiB(61 层 · 128K · bf16 · 一个用户)·反事实 MHA 488 GiB(同口径)。488 ÷ 8.58 = 56.9,正好对上 MLA 那个 4.571 × 12.44 —— 两条路算出同一个数,互为交叉验证

📌 「要几张卡」按 TPU v7 每 device 94.74 GiB 算(这个数本课的 AOT 工具链里核过:编译器自己报的 95.38G − 94.74G = 656.93M,只有按 1024 才成立)。⚠️ 换别的硬件只是这两根柱子的刻度变,结论不变;而且这笔账只算了装得下装不下,没算带宽

因为变的不是技术,是工作负载

同一个模型(V3),权重那一段一个字节没动,都是 625 GiB。 只把两个东西拧了一下 —— 上下文从 4K 到 128K,同时服务的人从 1 个到 64 个:

KV cache 从 0.27 GiB 涨到 549 GiB, 占总量从 0.04% 变成 46.8%主角换人了。

⭐⭐ 而这两个数是相乘的,而且乘的是同一项 —— 这就是为什么它不是慢慢变严重,而是突然变成了首要问题。

2.1 + 2.2 + 2.3 两条线,一个交汇处

这两条必须分开讲。它们指向同一批技术,但出发点完全不同 —— 混在一起讲,就成了名词罗列。

为什么是现在 —— 两条完全独立的线,在同一个地方交汇 这两条必须分开讲:它们指向同一批技术,但出发点完全不同 —— 混在一起讲就成了名词罗列 线索 A · 硬件账算不过来 结论:必须省。不解决它,长上下文根本上不了线 拿 DeepSeek V3 砸体感(61 层 · 128 头 · 每头 128 维 · 128K · bf16) 一个 token 要存 3.81 MiB × 131,072 个 token 488 GiB 一个用户 (中间那几步在「出处与口径」里) 换个单位就看得见了 —— 一格 = 一块 v7 device 的 HBM(94.74 GiB 可分配) 这一个用户的 KV cache 488 GiB 一个人就要 5.2 块 V3 全部权重(671B,原生 FP8) 625 GiB 所有人共享这一份 换成 GQA-8 30.50 GiB 省 16 倍 —— 还是装不进一块 换成 MLA(V3 真实方案) 8.58 GiB 省 56.9 倍 —— 这才塞得下 权重是所有用户共享一份,KV cache 是每人一份—— 所以它直接决定你能同时服务多少人。 488 是「假如 V3 用 MHA」的反事实值,不是实测。 线索 B · 信息本身不需要那么多 结论:可以省,而且不太亏 128K 的序列,真需要 128K 份独立的 KV 吗?三个观察 —— 实测极其稀疏 绝大部分权重集中在很少的位置, 其余近乎为零。 那把近零的那些算出来,算的是什么? ↑ 这一列 总是亮的 Attention sink 注意力被大量「停放」在开头几个 token 上, 跟内容无关 有一部分权重根本不是在做检索。 远 ← → 近 远近有别 邻近几十个 token 密集细粒度; 几万之外稀疏粗粒度。 凭什么用同一套精度处理这两种? 于是这三样都变得合理:压缩(远处多个合并成一个) · 稀疏(只挑相关的看) · 分层(近精细远粗糙) 两条缺一不可 —— 所有变体都活在它们的交汇处 只有 A(必须省):你得到的是一堆有损压缩的权宜之计,效果掉了只能认 只有 B(可以省):你没有动力去付 kernel 那么难写的代价。
左边是硬件账(必须省),右边是信息账(可以省而不太亏)。两条完全独立,却在同一个地方交汇 —— 所有变体都活在那儿。判一个变体好不好,就看它在「省了多少」和「亏了多少」之间落在哪。
出处与口径

📐 488 GiB 那条乘法链(原来画在图上,2026-09-15 折到这里):每 token 每层 2 × 128 × 128 = 32,768 个数 → × 61 层 = 1,998,848 个数 → × 2 字节 = 3.81 MiB/token → × 131,072 token = 488 GiB

四个对照值均按公式当场算出(脚本里带断言);口径沿用 V3 论文比较表(K、V 都按 d_h=128)—— V3 真实的 K 每头是 128+64=192 维,严格算这个基线还会更大

⚠️ 右栏三个观察是现象,不是本课实测;它们各自对应后面的一支方案(稀疏 → §六、分层 → §六、压缩 → §五)

「远处可以少看」这句直觉,严格版本长什么样 一本越读越厚的书讲 —— 以及一个固定大小的笔记本 两句话之间 两半之间 笔记本得多大 三个旋钮站在哪 ① 隔得越远的两句话,关系越弱 —— 但弱得有多快 这是 log-log 坐标,换成普通坐标就看不出区别了 隔得越远 → 真实的语言 隔一百句还剩一点 只记上一句 隔几十句就归零 这就是语言不能用「只记上一句」近似的原因 任何有限状态的记忆方式, 衰减都是指数的 —— 说没就没; 而真实语言是幂律的 —— 一直有一点。 「有限状态」这个限定不能省 注意这是 log-log 坐标 —— 换成普通坐标,两条线都长成「往下掉的一条线」,这张图要说的区别就看不见了。 ② 可是把书从中间劈开,两半之间的关联反而在变大 不矛盾 —— 单对越来越弱,成对的数量越来越多 薄书 3 × 3 = 9 对 厚一点 6 × 6 = 36 对 很厚 12 × 12 = 144 对 一个在减,一个在增 单对的关联越来越弱, 可对数增长得更快。 于是 L2M 那条定理,说人话就是这一句: 你记笔记的那个本子,得跟着书一起变厚 —— 本子大小固定,总有一本书是它兜不住的。 ③ 那三个旋钮,各自站在哪一边 这一格是本课按定义做的推导 旋钮① 每份更小 本子还是跟着书一起变厚 只是每页写得更省 安全 旋钮② 挑着看 本子照样跟着变厚 只是不是每页都翻 安全 旋钮③ 固定状态 本子大小写死了 书再厚,本子不变 总有一本兜不住 纯滑窗跟纯线性是同一类 —— 它们动的都是,不是常数。 这一张真正的用处:它把一句直觉变成了一条能证伪的规律 「远处可以少看」不等于「远处不重要」—— 两半之间的关联是随长度增长的,只是增长得慢。 于是整件事很清楚:Transformer 的 KV 线性增长是「供给过量」,而这条规律是「实际需求」。前两个旋钮就是在不掉到需求线以下的前提下,把那个过量的常数压小
⭐⭐ 两点互信息随距离幂律衰减(所以语言不能用马尔可夫近似),而两半之间的双部互信息随长度幂律增长。L2M 条件:模型历史状态的维度必须至少以同样的幂律增长 —— 这就是为什么固定大小状态那一支必须混着用。
出处与口径

① 出自 Lin & Tegmark《Criticality in Formal Languages and Statistical Physics》(Entropy 2017, arXiv 1606.06737

⚠️ arXiv v1 的旧题名是《Critical Behavior in Physics and Probabilistic Formal Languages》,两者是同一篇 —— 标了正式出处就用正式题名

原文口径:互信息在任何概率正则文法下指数衰减,而上下文无关文法下可以是幂律

② 出自 L2M(arXiv 2503.04725, ICML 2025):双部互信息幂律 scaling、「状态维度必须至少同阶增长」的定理、以及 Transformer 自动满足的那段分析

⚠️ 「书 / 笔记本」是本课的比喻;③ 为本课推导,非论文结论

2.3b 严格版本:两半之间的互信息

⭐ 还要补一句改口: 「远处可以少看」不等于「远处不重要」。 两半之间的互信息是随长度增长的 —— 只是增长得慢。 所以正确的说法是:Transformer 那条线性增长的 KV 是「供给过量」, 而这条幂律是「实际需求」; 前两个旋钮做的事,是在不掉到需求线以下的前提下把过量的常数压小

图上那 488 GiB 是「假如 V3 用 MHA」的反事实值, 不是 V3 的实测 —— V3 从第一天就是 MLA这门课自己的规矩:推出来的数必须带推导链和口径,图里两样都写了。

⭐ 还有一句图上画不出来、后面却要反复用: KV cache 不是显存里的一项开销,它直接决定你能同时服务多少人 —— 权重所有人共享一份,KV cache 每人一份。这一条到专题六 会变成 batch size 的硬上限。

第 三 节

FlashAttention —— 「怎么算」这条路已经走到头了

3.1 先认几个硬件词

📌 这一节会用到的几个硬件词MXU=矩阵乘单元(TPU v7 上是 256×256)· VPU=向量单元(做加减、指数这类)· SRAM / VMEM=片上暂存,快但极小 · SM=GPU 上的一个计算单元 · warp=GPU 上 32 条线程一组 · gather=按下标从内存里东一个西一个地捞。

⭐ 它跟这一讲要讨论的东西不是一类。 三个旋钮改的是算什么(模型变了,通常要重训); FlashAttention 一个字都不改数学,它改的是怎么算 —— 跟谁都能叠,而且必须叠
📌 正因为它跟谁都能叠、叠上去效果都一样,它在这一讲里没有区分度: 它既不区分 MLA 和 GQA,也不区分稀疏和线性。所以本节只留这一句。

⭐ 它做的事一句话:让 softmax 前后那一步不落到显存, 只在片上一块一块地过。这也是为什么本讲从头到尾不讨论「那个中间矩阵有多大」 —— 它根本不占显存。这一讲的账本只有一个:KV cache

🔬 课外:FlashAttention 深潜 —— 它怎么做到不落地、块该开多大、以及 为什么融合完还是只跑到 35%(硬件细节,写 kernel 的人才需要)

3.2 它删掉的是哪一项

朴素写法是三步:① 算 A = QKᵀ ② 对 A 做 softmax 得 P ③ 算 O = PV

⚠️ 记号提醒:FlashAttention 原论文这一步写作 S = QKᵀ。 本课改用 A —— 因为这门课的形状记号里 S 已经被 KV 长度占住了(BSKH 里那个 S),而「在形状里找 S」是全讲重复最多的一条指令。 一个符号只能有一个意思。读论文时看到 S = QKᵀ,那是同一个东西。

三步之间,那个 L×L 的 S 每次都要落一趟 HBM: 写 S、读 S、写 P、读 P —— 三个步骤,四趟。

而这个 S 有多大:128K 上下文、单个头就是 131072² × 2 B = 32 GiB,一层 128 个头。

FlashAttention 把三步融成一个 kernel:S 分块在片上算完, softmax 用在线归约边走边更新最大值和求和项,S 整项消失

⭐ 所以三笔账的形状是所有算子融合共有的: FLOPs 一分不省,省的全在「中间产物不落地」这一行。

3.2b ⭐ 那张经典图:内循环、外循环 —— 以及 FA2 为什么把它掉了个个儿

FlashAttention 论文那张图,画的是两层循环: HBM 里躺着 Q、K、V、O,SRAM 是旁边一个小方块; 外层循环搬一块进 SRAM,内层循环扫过另一边的所有块。 (原文 Figure 1 的 caption 把颜色也说死了:红箭头是外层、走 K 和 V蓝箭头是内层、走 Q。)

⚠️ 但那张图画的是第一版的顺序,而第二版把它换了过来 ——  这个「换」本身,比图更值得讲。

第一版:外层是 K/V,内层是 Q。

for j in K/V 的每一块:              # 外层
    把 K_j, V_j 搬进 SRAM
    for i in Q 的每一块:            # 内层
        把 Q_i、O_i、m_i、ℓ_i 从 HBM 读进来
        算这一格,更新 O_i、m_i、ℓ_i
        再把 O_i、m_i、ℓ_i 写回 HBM

⛔ 看内层那三行:O_i 和那两个统计量每一轮外循环都要读进来、写回去一次。 为什么躲不掉?—— 因为下一块 K/V 还会碰到同一个 Q 块, 它的输出没算完,只能先存回去。 ⭐ 这句话下面那张图的①画出来了:一趟外层循环碰到的是一列, 而一个输出要等齐的是一行 —— 两者只交于一格,所以这一趟凑不齐任何一个成品。

第二版:把两层对调。

for i in Q 的每一块:                # 外层 ← 换成了 Q
    把 Q_i 搬进 SRAM,O_i、m_i、ℓ_i 就地清零
    for j in K/V 的每一块:          # 内层
        搬 K_j, V_j 进来,算,就地累加进 O_i
    整个内层跑完,才把 O_i 写出去一次

两个后果,都很硬:

  1. O_i 和那两个统计量在整个内层循环里一直待在片上,一次都不落 HBM。
  2. 不同的 Q 块之间彻底独立 ——  可以直接铺到几百个执行单元上,互相不用通信

⭐ 一句话记住它: 外循环放谁,谁的中间状态就不用来回搬。 而 attention 里「需要被累加到最后」的是 O,所以外循环必须放 Q

⭐ 那到底省了多少?—— 这个问题值得认真数一遍, 因为只盯着 O 看,会得出一个错的量级

内外循环对调:省下的到底是什么 满网的图画的都是遍历顺序;这张画的是后果 —— 把四个张量分开称一次重 载一次 K/V 搬一次 O 这一趟碰到的 输出要等齐的 ① 同一张块网格,两种走法 行 = Q 的块,列 = K/V 的块;浅灰 = causal 整块跳过;8×8 是示意,真实 64×64(L = 8192、块 128) FA1:外层 K/V,内层 Q 一列一列地走 K / V 的块 → Q 的块 O_5 要等齐 这一趟 FA2:外层 Q,内层 K/V 一行一行地走 K / V 的块 → Q 的块 O_5 要等齐 这一趟 vs K/V 每列只载一次(64 次) O 每一格都要读回来再写回去 这一趟 = 一列要等齐的 = 一行 —— 只交于 1 格,这一趟只给 O_5 凑齐 1/6 K/V 每一格都要重新载一次 O 每行只写一次(64 次) 这一趟 = 一行要等齐的 = 同一行 —— 完全重合,走完这一趟 O_5 就定稿 判据就长在这两个集合上:一趟循环碰到的格子,跟一个输出需要的格子,是不是同一批。不是 → 这一趟凑不齐任何一个成品, 中间结果只能先落回 HBM(FA1 那 4160 次 O 读写就是这么来的);是 → 算完即定稿。 这跟面板 ③ 的 warp 判据是同一句话 —— 求和号跑在 K/V 这一维上。 ② 那到底省了多少 —— 四个张量分开称一次 本课自己按两份 Algorithm 1 数的,论文没给过这个数 Q FA1 2080 FA2 64 K、V FA1 128 FA2 4160 O(读+写) FA1 4160 FA2 64 合计 FA1 6368 FA2 4288 → 只差 1.49 倍 只看 O 那一行,会得出一个错的量级:4160 对 64,是 65 倍。「一次都不落 HBM」这句话本身没错,但拿它当总账就错了 因为 FA2 把省下的 O 流量,又用 K/V 的重读还了回去:K/V 在 FA1 里待在外圈、只读一趟;到 FA2 跑进内圈,每个 Q 块都要把全部 K/V 重新走一遍。 换成非 causal 口径这个比值是 1.492,结论不挑口径;而且这是按 HBM 读写次数数的理想账,真机上 L2 会吃掉一部分 K/V 重读 —— 1.49 倍是偏乐观那一侧 ③ 同一条判据,在 warp 这一层又用了一遍 一个 thread block 内部,4 个 warp 怎么分活 FA1:切 K/V(论文原词 “split-K”) 输出块 O_i 4 份部分和 谁都不完整 FA2:切 Q 输出块 O_i warp 0 的那一片(定稿) warp 1 的那一片(定稿) warp 2 的那一片(定稿) warp 3 的那一片(定稿) 4 个 warp 各持 K/V 的一片,Q 大家共用 每个 warp 只算得出一份部分和 —— 必须写进共享内存、同步、再加起来。 论文原话:这些读写拖慢了前向 4 个 warp 各持 Q 的一片,K/V 大家共用 每个 warp 直接算出自己那一片完整输出 —— warp 之间完全不需要通信。 跟上面 Q 块之间互不通信,是同一件事。 一条判据,三个尺度通用 切「要被累加的那一维」就得合;切「各自独立出结果的那一维」就不用合。attention 里前者是 K/V(它们在求和号里面),后者是 Q(每行输出各管各的)。 所以面板 ① 那句「这一趟碰到的,是不是这个输出要的」,跟这里的 warp 分工,是同一条判据在三个尺度上:循环、thread block、warp —— 动作都是把外圈让给 Q。这条判 据的用处远不止 attention:任何融合 kernel 分工前,先问一句「我切的这一维在不在求和号里」。 三处别讲过头 论文从没说过「对调是为了让 O 不落 HBM」。它给的理由是 ① 减少 non-matmul FLOP ② occupancy —— 本图那个 1.49 倍是本课自己数的,FA2 连 IO-complexity 定理都 没有。 只对调了 forward。FA1 的 backward 和 FA2 的 backward 都还是 K/V 在外层
⭐⭐ 满网的 FA 图画的都是遍历顺序;这张画的是后果 —— 换一个循环顺序,四个张量各自要被搬多少趟。只看 O 是 65 倍,四个一起数只有 1.49 倍:FA2 把省下的 O 流量,又用 K/V 的重读还了回去。所以对调换来的不主要是带宽,是并行度 —— 这跟论文自己给的两条理由(non-matmul FLOP、occupancy)对得上。
出处与口径

📌 FA1 arXiv 2205.14135 §3.1 Algorithm 1(外层 K/V、内层 Q;O_i、ℓ_i、m_i 的写回在内层循环里,line 12–13);Figure 1 caption 逐字「In the outer loop (red arrows) ... loops through blocks of the K and V matrices」。

📌 FA2 arXiv 2307.08691 §3.1.1 Algorithm 1(外层 Q、内层 K/V,O_i 与 L_i 在内层结束后各写一次);宣布对调那句在论文 §3.2 “Parallelism”,并注明归功于 Phil Tillet 的 Triton 实现。warp 层全部出自 §3.3 与 Figure 3 —— ⚠️ FA1 论文里 “warp” 出现 0 次,那段是 FA2 的回溯。

⛔ 原词是 “split-K”,不是坊间常见的 “sliced-K”(两篇全文零次)。⚠️ 本图不引用任何加速比 —— FA2 论文里那几个百分比分属不同口径(attention 单算子 vs 端到端训练、不同硬件配置),混用会得出错的结论,本讲专门讲过这条纪律。

⚠️ 「O 不落 HBM」是真的,但它不是这次对调的主要收益。

按块级 HBM 读写数一遍(L = 8192、块 128,按 causal): 只看 O 是 4160 : 64,六十五倍四个张量一起数是 6368 : 4288,只差 1.49 倍差额去哪了 —— KV 从外圈挪进了内圈, 每个 Q 块都要把它们重新走一遍,省下的又还回去了。

⭐⭐ 所以这次对调换来的不主要是带宽,是并行度: FA1 只能按 batch × head 铺,FA2 多出「Q 块」一整维。 batch 小、头数少、上下文长的时候,前两维本来就铺不满几百个执行单元 ——  那正是 FA2 最吃香的场合。

这个 1.49 是本课自己按两份 Algorithm 1 数出来的,不是论文报的数 ——  FA2 全文没有 IO-complexity 定理,整个 IO 分析 defer 给了 FA1。 论文自己给的理由是两条:减少 non-matmul FLOP(§3.1)、 提高 occupancy 且 thread block 之间无需通信(§3.2)。

📌 三处容易记错,一并说清

  • 这个顺序不是 FA2 首创。论文自己写的是 “first suggested and implemented by Phil Tillet in the Triton implementation”。
  • 只对调了 forward。FA1 的 backward 和 FA2 的 backward 都还是 K/V 在外层
  • 宣布对调那句话不在 §3.1,在 §3.2 “Parallelism”§3.1 只是把新顺序直接写进 Algorithm 1,没有一句话说「我们换了」。

3.2c 同一个原则,在 warp 这一层又用了一遍

这件事在块内部还发生了第二次 —— 而且这一次是官方原话 (上面那张图的 ③ 已经把它画出来了):

⚠️ 出处要说对FA1 论文里「warp」出现 0 次。 下面这两段对 FA1 warp 切分的描述,全部出自 FA2 §3.3 的回溯,不是 FA1 自己写的。

  • 第一版:把 K 和 V 切给 4 个 warp,Q 大家共用 (论文原词是 split-K —— 坊间常见的「sliced-K」两篇论文里一次都没出现过)。 ⛔ 于是每个 warp 都得把中间结果写进共享内存、同步、再加起来 ——  这些读写拖慢了前向。
  • 第二版:反过来,把 Q 切给 4 个 warp,K 和 V 大家共用。 ⭐ 每个 warp 算出自己那一片 QKᵀ,直接乘共享的那片 V 就得到自己那片输出 —— warp 之间完全不需要通信。

⭐⭐ 两个尺度,同一条判据: 切「要被累加的那一维」就得合;切「各自独立出结果的那一维」就不用合。 attention 里前者是 K/V(它们在求和号里面),后者是 Q(每行输出各管各的)。

⚠️ 这条判据的用处远不止 attention ——  任何融合 kernel 在分工时,先问一句「我切的这一维在不在求和号里」。

3.3 ⚠️ 在线 softmax 不是免费的 —— 它是后面 35% 那个数的根

softmax 要减最大值才数值稳定,而最大值要看完整行才知道。 —— 那 FlashAttention 一次只看一块,凭什么算得对?

在线 softmax —— 改到一半发现有人考得更高,不用把前面的卷子重翻一遍 softmax 要先知道整行的最大值才能算,而 FlashAttention 一次只看一小块。它凭什么算得对?这张图用五个真数字把两条路各走一遍 —— 最后一个数你可以自己对。 传统:整行摊开 在线:一块一块来 重标定的那个系数 ① 传统算法:得先把整行摊开 —— 因为要减最大值 片上要放得下 S 个数 一整行的打分(这里只画 5 个,真实是 S 个) 2 内容 1 3 内容 5 1 内容 2 5 内容 8 4 内容 3 先找最大值 这里是 5 这一步必须看完全部才能动笔 —— 所以整行都得同时摆在片上暂存里。 128K 上下文,这一行就是 13 万个数。 💬 「我得先把整行看完,才知道该减掉多少 —— 所以这一行你别想让我分块,它必须整个摊在我面前。」 注意省的是什么:减最大值是为了数值稳定(不减,exp 会溢出)。所以「必须先看完整行」不是实现懒,是这一步的定义就这样。 而这正是那张 S×S 的大表非建不可的原因 —— FlashAttention 要拆掉的就是它。 ② 在线算法:一摞一摞地改,手里只攥三个数 片上只放一块 第一摞(前 3 个) 2 内容 1 3 内容 5 1 内容 2 手里的三个数 目前最高分 m = 3 目前总和 s = 1.5032 加权和(未除 s)o = 5.6386 就这三个标量。 前三张卷子可以扔了 💬 「我先假装 3 就是全场最高分,照这个口径把总和和加权和都算出来。反正真出现更高的,我到时候再改。」 第二摞(后 2 个)—— 出事了 5 内容 8 4 内容 3 这里有个 5 比手里的 3 还高。 之前算的全都得改口径。 重标定 —— 只要乘一个数 系数 = exp(旧最高 − 新最高) = exp(3 − 5) = 0.1353 把 s 和 o 各乘一次,完事。 更新后的三个数 m = 5 s = 1.5713 o = 9.8667(除以 s 才是输出) 💬 「果然来了个 5。我不回头翻卷子 —— 只把刚才『假装 3 最高』在账上留下的痕迹擦掉: 把 s 和 o 各乘一次 exp(3−5),口径就换成『5 最高』了。」 💬 「擦完了。我手里这三个数,跟一开始就知道 5 最高、一次性算出来的完全一样 —— 下一格你可以自己核。」 🏠 这就是那句生活里的话 改到一半发现有人考得更高,不用把前面的卷子重翻一遍 —— 把手里已经算出来的总分乘一个系数就行。 ③ 两条路,同一个数 —— 这一格请自己核 精确相等,不是近似 传统:摊开整行 减最大值 5 之后: 0.0498 0.1353 0.0183 1.0000 0.3679 总和 s = 1.5713 加权和 o = 9.8667 输出 = o ÷ s = 6.2793 在线:两摞 + 一次重标定 第一摞:s = 1.5032 o = 5.6386 乘系数 0.1353,再加第二摞: 总和 s = 1.5713 加权和 o = 9.8667 输出 = o ÷ s = 6.2793 差是 0,不是「误差很小」—— 在线 softmax 是恒等变换,不是近似算法。它不掉点。 但它不是免费的 —— 这就是 3.3 那句话的实体 每来一块,就多一次求 max、一次 exp、一次按系数重标定。 这三样都不是矩阵乘 更要命的是它们卡在归约链上:下一块要用上一块更新出来的 m 和 s,排不进矩阵乘的流水里一起跑 它省掉的到底是什么(这一条最容易说错) 不是省算力:该做的乘加一次不少,重标定还额外多了一些。 省的是片上暂存:传统要同时摆下整行 S 个数,在线一次只摆一块。于是那张 S×S 的大表根本不用建。
⭐⭐ 盯住②里那个橙色的系数:发现更高分之后,不用回头重算 —— 把手里的 s 和 o 各乘一次它就行⚠️ 而这个「各乘一次」不是矩阵乘,还卡在归约链上 —— 3.6 那个「只跑到 35%」有一部分就在这儿。
出处与口径

在线归约的做法出自 Milakov & Gimelshein《Online normalizer calculation for softmax》(arXiv 1805.02867),FlashAttention(arXiv 2205.14135)把它用进了注意力

⭐ 图里五个打分和五个「内容」是本课随手编的示例数,但两条路的计算是脚本当场跑的,并且断言精确相等(差恒为 0)

⚠️ 「内容」这里简化成了一个数;真实的 V 是一个 d_h 维向量,重标定对整个向量同时做 —— 道理一样,画成一个数只是为了能核

办法是:手里只攥三个数,来了更高分就把它们缩一下

每来一块,就更新一次 running maxrunning sum, 并把已经累好的那份加权和按比例重标定一次

🏠 生活版改到一半发现有人考得更高, 不用把前面的卷子重翻一遍 —— 把手里已经算出来的总分乘一个系数就行。

它不是近似:图③里两条路算出来的差是 0,不是「误差很小」。 在线 softmax 是恒等变换,不掉点

📌 顺手把「省的是什么」说清楚,这一条最容易说错

不是省算力 —— 该做的乘加一次不少,重标定还额外多了一些。
⭐ 省的是片上暂存:传统要同时摆下整行 S 个数, 在线一次只摆一块。于是那张 S×S 的大表根本不用建。

⚠️ 这跟 §五 的账是两回事:那边省的是要 跨 token 留下来的 KV cache,这里省的是算一步时中途摊开的中间结果。 两个都叫「省显存」,省的却不是同一样东西。

⛔ 这里有个几乎人人都会踩的误会:压力不在那两个 running 值上

「在线 softmax 只要存一个最大值和一个求和项」—— 这句话是对的, 而且它们确实很小:每行各一个标量。一个 bq = 512 的 Q 块, 两个加起来也就 4 KB 量级。

真正占地方的是 S = Q@Kᵀ 那一整块。 它的形状是 [bq, bkv] —— 512 × 512 的 fp32 就是 1 MB, 2048 × 2048 是 16 MB。比那两个 running 值大两三个数量级。

而且它不是存一次就完了:每来一个 KV 块就重新生成一整块。

所以卡住的是 S 的生命周期,不是那两个数。

一个 KV 块内,S 要连着走完四步才能扔: ① MXU 产出 → ② 沿着行求最大值 → ③ 减掉它再取指数 → ④ 喂给第二个矩阵乘。

这四步里它一直是活的。 ②③ 是向量单元的活,而 ① ④ 是矩阵单元的活 ——  矩阵单元想开始下一块,可它的输出还被 ②③ 占着。

⭐ 再加上流水线:要让下一块的矩阵乘和这一块的向量运算重叠, 就得同时留住不止一块 S 于是「一块」变成「好几块」。

📌 ② 那一步在 TPU 上还额外贵一点:行方向的最大值是一次跨 lane 归约, 而 lane 正是硬件那 128 的方向。归约越慢,S 活得越久。

这条链在 3.6 会变成一个具体的数字。

3.4 块开多大 —— GPU 那边是一堵墙

块大小不是调着玩的旋钮,它由片上暂存的容量直接顶死。

GPU 侧:一个线程块最多拿 227 KiB 共享内存。 装三块 128×128 的 bf16 tile(Q/K/V)就是 3 × 128 × 128 × 2 B = 96 KiB, 再留双缓冲,基本到顶

而块开不大,代价是 K/V 被重复读 —— 重读次数 ≈ 序列长度 ÷ Q 块大小。 所以那 227 KiB 不只是「装不下」,它通过块大小间接决定了 HBM 流量。

⚠️ 顺带拆掉一个常见误解:标准 FlashAttention 前向不需要跨块归并 每个执行单元拿走一个 Q 块,自己走完整条 KV 循环,在线 softmax 在块内就闭合了。 需要合的只有两种:KV 也被切开时(长上下文解码那类做法, 要跨切片重新对齐最大值和求和项),以及反向(对 Q 的梯度要跨块累加)。 所以 GPU 的代价在「分」,不在「合」。

3.5 ⭐ TPU 那边是三堵墙 —— 而且「块越大越好」是错的

TPU 侧对应的是 Splash Attention。它的灶台看着大得多 (Ironwood 每个 TensorCore 64 MB VMEM,一颗 chip 两个核 = 128 MB), 但块反而不能随便开大。

JAX 里 Splash Attention 的默认块是 128 × 128, 而且源码里挂着一句 TODO:「以后按启发式选更好的参数」。

我们自己在 Hunyuan3-295B 上扫过这个参数seq = 4096,v7 16 芯片 / 20 层 / pdbs 8, 完整数据见 tpu/Hunyuan3-295B-Pretraining/TUNING-v7.md):

run块布局KV 方向切几块TFLOP/s/devicevs 基线
B1全 20482 块223.6基线
S2官方非均匀布局(含 sa_block_kv_compute=5128 块221.3−1.0%
S1全 2048 + use_max_logit_estimate=302 块228.4+2.1%

再往上(块 4096,KV 方向只剩 1 块)撞 VMEM 墙。 源文件只记了「往上撞 VMEM 墙」这一句,没有留下 OOM 现场,所以这一档这里不给数。

⛔ 这张表最容易读错的地方:228.4 不是块大小的功劳

228.4 那一行同时开了 use_max_logit_estimate=30 —— 那是另一个开关(用一个可证明的 logit 上界替掉 online softmax 的 running max)。 纯粹属于「块大小 = 2048」的成绩是 223.6,差的那 2.1% 得记在 max_logit 头上。

⭐ 一份消融表里同时变了两个旋钮时,「最好的那一行」和「你正在讲的那个旋钮」往往不是一回事。 引数之前先看清那一行还开了什么。

三堵墙,方向各不相同 —— 而且甜点是个绝对值 块大小往上撞两堵、往下撞一堵;剩下的缝正对 2048,而这个 2048 换到 seq 16384 上没有跟着动 实测 没测过 被证伪 ① 三堵墙,方向各不相同 seq = 4096 块大小(以及 KV 方向因此切出几块) 512 切 8 块 1024 切 4 块 2048 切 2 块 4096 切 1 块 往下:碎块开销 1024 没测过,但同向 往上:容量墙 往上:并行度 三面都够不着的,只剩这一列 往下 · 碎块开销 每块的固定开销(mask 检查、running max/sum 更新、pipeline stage 切换)摊不动。512 实测 −1.0%。 往上 · 容量墙 S 块是 b×b,块开到 4096 时它一个人就占满 64 MiB VMEM —— 见面板 ③。 往上 · 并行度 KV 方向只切得出一块,那一维的流水直接塌掉。 这一堵是从 kernel 结构推的,源文件未实测。 ② 判决:最优块跟着 seq 走吗 两条轴按同一个绝对像素尺画 seq = 4096 生产形状 512 1024 2048 4096 最优 = seq/2 seq = 16384 同一个 kernel,只把 seq 拉长 4 倍 512 1024 2048 4096 8192 16384 最优 = seq/8 比例规则预测最优块在这里(8192)—— 实测没有 同一个绝对位置 seq 拉长 4 倍,最优块纹丝不动 —— 它是 seq/2 还是 seq/8 纯属巧合,共同点是那个 2048 本身。 课件原文写的是「看 block/seq 的比例,不是绝对值」,并注明「尚未验证」—— 方向反了,而且那次验证早就做完了。 ③ 为什么是绝对值:片上那份工作集里,一个 seq 都没有 面积按真实字节数等比 块 = 2048 VMEM 64 MiB / TensorCore 16 MiB 块 = 4096 VMEM 64 MiB / TensorCore 64 MiB S 块 16 MiB,还剩得下 Q/K/V(合 1.5 MiB) S 块正好 64 MiB —— 一个人占满,Q/K/V 无处可放 片上工作集 Q[b×d] + K[b×d] + V[b×d] + S[b×b] 前三项线性,第四项平方 —— 所以块一翻倍,S 翻四倍。 这四项里没有 seq。 seq 只决定你要绕几趟:seq ÷ b。 甜点由 VMEM 顶死,而 VMEM 不知道你的 seq 是多少 —— 这就是它为什么是个绝对值。 而 b = seq 时「切 1 块」= 压根没分块: S 就是整张注意力矩阵,你回到了原点。那堵容量墙就是 FlashAttention 本来要解决的那一堵。 带得走的那一条 块大小的最优值是硬件定的绝对值,不是 `block/seq` 的比例。换序列长度时不要按比例缩放块大小 —— 换硬件(VMEM 容量变了)才需要重扫。 照抄别人的配置之所以翻车,不是因为比例变了,而是因为他们那个绝对值是给他们的硬件调的。 这一节是审出来的 —— 原表四行错了三行 228.4 是 `全 2048 + use_max_logit_estimate=30`(run S1);纯块大小 2048 的基线是 223.6(run B1)。差的那 2.1% 是另一个开关的功劳。 「4096 → VMEM 爆在反向」与「compute 压回 2048 → −11.5%」在源文件里都不存在:前者是 MoE ragged-dot `tile_k` 的 OOM(另一个 kernel),后者是「把 forward 优 化到无限快」的收益天花板 `23% × 50%`。
⭐⭐ 三堵墙方向各不相同:往下是碎块开销,往上是容量墙和并行度 —— 剩下的那道缝正对 2048。而第 ② 格是这一节真正的判决:把 seq 拉长 4 倍,最优块纹丝不动,所以它是个绝对值,不是 block/seq 的比例。第 ③ 格给出为什么必然如此 —— 片上工作集里一个 seq 都没有。
出处与口径

📌 实测数据出自 `tpu/Hunyuan3-295B-Pretraining/TUNING-v7.md` 「消融实测」表(16 chip / 20 层 / pdbs 8 / seq 4096)与「四条可复用结论」①。

⛔ 该文件前后自相矛盾:第 634 行的「方法论教训 ①」说看比例,两千行后的「可复用结论 ①」用 seq 16384 的直接实验说与 seq 无关。后者晚一轮、有实验,以它为准。

⚠️ 面板① 的「并行度墙」是从 kernel 结构推的,源文件只实测了容量墙那一侧;1024 那一档没有可追溯的 splash 实测,故画成虚线。

三堵墙,方向各不相同:

  1. 往上是容量墙 —— 再开大一档就装不下。 ⭐ 而它的成因图上能一眼看到:片上要放的 S 块是 b×b块翻倍它翻四倍;块开到 4096 时,光这一块就正好占满 64 MiB VMEM
  2. 往上还有第二堵:并行度 —— 块一大,KV 方向只切得出一块,那一维的流水直接塌掉。 ⭐ 这一堵最反直觉:装得下,却更慢。(⚠️ 这一堵是从 kernel 结构推的,我们没有单独实测它)
  3. 往下是碎块开销 —— 每块的固定开销(mask 检查、running max/sum 更新、 pipeline stage 切换)摊不动
⭐ 一条可迁移的教训:最优块是个绝对值,不随序列长度缩放

那个 512 不是我们瞎试的,是官方 tpu7x benchmark 里的值 —— 那份配置是给 max_target_length = 131072 调的。照抄它会反向优化,这一点没错; 但原因不是「比例变了」,而是 512 本身就不是这块硬件的甜点

后来我们在同一个 kernel 上把 seq 从 4096 拉到 16384(长了 4 倍)—— 最优块还是 2048。它在前者身上是 seq/2,在后者身上是 seq/8两次的共同点是那个绝对值,不是比例。

⭐ 为什么必然如此,看上面那张图的第 ③ 格:片上工作集是 Q[b×d] + K[b×d] + V[b×d] + S[b×b]这四项里一个 seq 都没有seq 只决定你要绕几趟(seq ÷ b)。甜点由 VMEM 顶死,而 VMEM 不知道你的 seq 是多少

所以:换序列长度不用重扫块大小,换硬件才要。

⚠️ 这一节是审出来的 —— 顺带一条关于「怎么引别人的结论」

这一节此前的版本写的是「要看 block/seq 的比例」,并注明「尚未验证」。 方向反了,而且那次验证早就做完了。

出错的机制很具体:源文件在前面有一段「方法论教训」,是从一个数据点(照抄 512,−1.0%)反推出来的; 两千行之后的「可复用结论」里,后一轮用 seq 16384 的直接实验把它推翻了。 我们读到前一半就停了。

⭐ 判据:同一份长文档里,晚出现的结论可能推翻早出现的。 引一句「教训」之前,先搜一遍全文还有没有同主题的第二段 —— 尤其当那句话你打算加粗的时候。

⭐ 所以两边的墙不是「硬件 vs 调参」,是一面 vs 三面: GPU 那边容量一堵墙顶死,方向反倒清楚 —— 能开多大就开多大; TPU 那边最优往往不在最大处,得在三面之间找那个点。

3.6 ⭐ 融合之后,它还是只跑到 35% —— 三层原因,都不是配置问题

📌 先把「效率」这个词定义一次(⭐ 2026-09-13 补 ——  原先这一段有四个百分比,一个定义都没有,而它结尾正好在教「引用百分比要写清出自哪个工具页」):

下文的效率XLA 记的该算子 FLOP ÷(该算子耗时 × per-device BF16 峰值), 峰值取 1153.5 TFLOP/s(v7 每芯片 2307 ÷ 2),数出自 XProf 的 op stats 页。 测量条件:Hunyuan3-295B · seq 4096 · v7 64 芯片 · 训练(含反向)

⚠️ 它不是 MFU(MFU 的分子是模型级有效 FLOP,分母是整步耗时); 也不是 MXU 利用率(那是硬件计数器读出来的)。这三个数在同一段里都出现过,别混。

这是这一节最该带走的一段:FlashAttention 不是终点。 我们在 v7 上量到 splash attention 占 23% 的时间、效率只有 35.5%,全场最低

① 记账口径:报出来的那个百分比,分子是虚高的。 XLA 给 splash 记的 FLOP 是不折 causal 的全量 4·b·s²·h·d。 所以 32.8–39.0% 这个区间是拿虚高的分子算出来的,真实执行效率比它更低

⚠️ causal 跳过上三角是节省,不是又一道要乘上去的折扣 —— 它只造成记账错位。这两件事最容易混。

② 形状锁死 50%:MXU 是 256×256,而 head_dim = 128 只吃得下一半。

matmul形状浪费在哪
QKᵀ[q_len, 128] @ [128, kv_len]收缩维只有 128 → 废一半
PV[q_len, kv_len] @ [kv_len, 128]输出维只有 128 → 废一半

两个矩阵乘各撞一次,所以整个算子的 MXU 利用率封顶 50%。 (⭐ 为什么「输出维只有 128」也会浪费一半, 以及为什么这种浪费能救而收缩维那种救不了 ——  机制在 专题二 3.4。) Google 侧的结论是明确的:head_dim = 128 时 MXU 利用率无法超过 50%, 没有办法绕过

③ 为什么连 50% 都到不了 —— 回到 3.3 那条链。 持有 Q@K 输出(也就是整块 S,不是那两个 running 值)的寄存器, 在最大值和减法完成前不能释放,于是不断堆积 → 寄存器压力 → spill 到 VMEM → MXU 停在等数据载回。 一句话:VPU 跟不上 MXU。

⭐ 这也解释了 3.5 那三堵墙里最反直觉的一堵为什么存在: 块开大 → S 那一块跟着变大 → 生命周期更长、更容易 spill。 「装得下」和「跑得快」在这里是两回事。

三层叠起来的结论很硬: 形状锁死一半、寄存器压到 35%、记账口径还让它看着比实际好看。 这三层没有一层是配置能救的 —— 要么改 head_dim,要么改 kernel 的数据流。 (我们试过的那条出路是把矩阵乘全转置,见 TUNING-v7 的附录。)

3.6b ⚠️ 还有一条方法论:同一份 profile,不同工具页的百分比不可混用

「HBM 受限占多少」—— 一个工具页说 35.6%,另一个说 19.5%。 破案的钥匙是两者的 self-time 合计正好差 2.00 倍, 而 v7 恰好是 2 device/chip。分母不是同一个东西,分子上的百分比自然对不上。 ⇒ 判瓶颈用 roofline 那一页,归因到算子用 op stats 那一页; 引用任何百分比都要写清出自哪个工具页。

FlashAttention —— 图上一处都没改,改的是「落不落地」 ⭐ 点亮的三格跟底图一模一样:同样的算子、同样的形状、同样的 FLOPs。它不是第四个旋钮。 X BTD —— 一层的输入 BTD W_Q · DNH BTNH Q BSD W_K · DKH BSKH K BSD W_V · DKH BSKH V reshape BTNH → BTKGH BTKGH BSKH BTSKG Q·Kᵀ + masks 谁能看谁 softmax BTSKG(注意力矩阵,记作 A) 这一步不落地,贵在要算的次数 BTSKG BSKH BTKGH A·V reshape BTKGH → BTNH BTNH W_O · NHD BTD 输出投影 + 残差 BTD norm BTD MLP(本专题一个字都不改它) BTD ·DF→ BTF ─gelu⊛─ BTF ·FD→ BTD + 残差 → norm BTD → 下一层 这一层算完,桌上的东西分三堆 常驻的设备 权重 W 所有人共用,不随对话变 草稿纸 中间那些量 算完就扔,不留到下一步 锁进柜子的 K 和 V 每来一个 token 就多一格 ⊘ 不在旋钮上:它删掉的不是计算,是搬运 ➜ 这一格 这笔账一个字节都不变。FlashAttention 改的是「怎么跑」, 它省的是那个从来没落地过的中间矩阵 —— 跟 488 无关。 📌 这一格的完整讲解在图下面的正文里 ⭐ 一句话记住它:三个旋钮改的是这张图,它改的是这张图怎么跑。所以它跟谁都不冲突 —— 这也是它能成为默认实现的原因。
同一张主线图,而点亮的三格跟底图一模一样:同样的算子、同样的形状、同样的 FLOPs。FlashAttention 不是第四个旋钮 —— 三个旋钮改的是这张图,它改的是这张图怎么跑

不在旋钮上:它删掉的不是计算,是搬运

朴素写法会把中间那一步写进 HBM 再读回来:写 S → 读 S → 写 P → 读 P,四趟。 ⭐ 而这一步本来就不该落地 —— 融合之后它只在片上过。

⭐⭐ FlashAttention 把这四趟全删了:分块算, 中间结果只在片上暂存里走一遭,从不落 HBM。

📌 所以它跟三个旋钮不是一类东西,可以同时用。 旋钮改的是「算什么」,它改的是「算出来的东西放哪」——  今天所有方案的实现里都有它,它是地板不是选项

⚠️ 它也不是免费的:在线 softmax 要多做一遍重标定, 融合之后实测也只跑到约 35%,三层原因就在上面 3.5/3.6 两小节。

3.7 这一节留下的那句话

「怎么算」这条路,到这里基本走到头了。

FlashAttention 这一套已经是标配,而且已经调到头了 ——  剩下的空间不在「怎么算」里,只能去改「算什么」。 (⭐ 「已经调到头」的三条证据 —— 中间产物不落地、块大小贴着三堵墙、 算子仍只跑到三成多 —— 都在上面那个折叠里。 不展开也不影响这句结论:它要的只是「这条路是熟路,不是新路」。)

下一节的三个旋钮,就是「改算什么」的全部可能位置。


第 四 节

骨架:三个旋钮,是同一个账本的三个面

4.1 三个旋钮

这是这个专题的骨架。把所有名词收进一张表:

三个旋钮 —— 这块面板上就这么三个 不是「有三类方法」,是一个 query 只做三件事,所以只有三个地方能拧 存什么 读哪些 用什么数学 不在面板上 ① 先看一个 query 从头到尾做了什么 只有三步 —— 这就是「只有三个旋钮」的全部理由 第一步 从每个位置各取一份 K 和 V 第二步 跟其中哪些位置算 然后加权求和 第三步 用哪一套数学 算这个加权求和 数一数 —— 就这三步,没有第四步。所以下面那块面板上,也只可能有三个旋钮。 ② 于是面板上就这三个旋钮 每个旋钮下面,画的是「拧过去之后那一格变成什么样」 旋钮 ① 每个 token 存多少 拧过去之后:每份变小 格子还是那么多,每格里的东西变小 旋钮 ② 每个 query 读多少 拧过去之后:读的变少 每格还是那么大,这一步只读其中几个 旋钮 ③ 换一套数学 拧过去之后:整块换掉 一长排 一块板子 整排换成一块固定大小的板子 ③ 那 FlashAttention 呢 —— 它不在这块面板上 它改的不是算什么,是怎么算 它是机器侧面的一个开关 打开:同样的结果,少搬很多次;关上:一样算得出来 它一个字节的 KV 都不省 按 FLOPs 切 平方项 12.3% 注意力投影 27.4% 其余 60.3% 按时间切(splash attn) 23.0% 其余 77.0% ≈ 两倍 同一件事,两种量法,形状对不上 —— 差的那部分全在路上 这一对数不是从论文抄的,是我们自己在 v7 上量的(同一个 seq=4096) 判据 一个在所有分支上 取值都一样的变量, 对这一讲没有解释力 这张图要留下的是「封闭感」,不是「有三类方法」 一个 query 只做三件事 → 只有三个地方能拧。拿到任何一个新名字,先把它放进某一格 —— 放得进去的,它的优点和代价你已经知道了放不进去的,才值得你花时间。 一定会被问的那一条:KV 量化算不算第四个旋钮?—— 不算。三个旋钮管的是存几个数、读几个数,量化管的是每个数用几个 bit。两者正交,可以任意组合(V4 就是稀疏 + KV 混合精度一起上)。精度整个归专题八。
⭐⭐ 三个旋钮不是凑出来的:一个 query 只做三步,一步一个位置可动。这张图要留下的是「没有第四个位置」这个封闭感 —— 而 FlashAttention 不改算什么、只改怎么算,所以它不在这三条里。
出处与口径

三步的拆法与三个旋钮的对应关系是本课的骨架,不是某一篇论文的分类;每个旋钮下面的代表方法见 §五 / §六 / §七 各自的出处

⚠️ 「控制面板 / 旋钮」是本课的比喻 —— 它承担的是「封闭性」这个论证,不只是一个好记的名字

事后压,还是从头按压缩训 就像老房改造图纸阶段就按这个户型画 改造(事后) 新建(native) 同一个机制的两种用法 ① 两种做法 —— 各有各的好,不是谁淘汰谁 装修过的人都懂 改造 —— 房子已经盖好了 快、便宜,不用搬家(不用重训) 想撤随时撤(很多是推理期开关) 承重墙动不了 —— 有些地方就是别扭 新建 —— 图纸阶段就这么画 要重盖(重训),贵、慢 盖完了就改不回去 哪儿都合适 —— 省得更狠,掉点更小 ② 这条对立,在这一讲里出现四次 四次都在不同的分支上 —— 所以它不是巧合 第 1 次 旋钮① 低秩分解 改造 承重墙 动不了 Eigen / Palu / LoRC 省 40% / 50% 新建 还在 图纸上 MLA 省 56.9× 第 2 次 旋钮① 砍头 改造 承重墙 动不了 GQA uptraining 接近 MHA 新建 还在 图纸上 GQA from scratch 不用两段 第 3 次 旋钮② 挑着看 改造 承重墙 动不了 H2O 随时可开关 新建 还在 图纸上 DSA 掉点小得多 第 4 次 旋钮② 挑着看 改造 承重墙 动不了 ClusterKV / Quest 三家三种毛病 新建 还在 图纸上 NSA 持平或超过 旋钮③ 换数学 这一栏空着 本讲没有核过的例子 也空着 为什么留空见正文 四次全落在旋钮①② 上 —— 旋钮③ 那一栏是空的。空着比硬凑一栏好:空栏可以被后来的人填上,凑出来的会被当成事实背下去。 一条不讲会教出错误心智模型的口径 「改造 / 新建」说的是这个方法这一次被怎么用,不是方法本身的属性。 最好的例子就是 GQA:它最初是 uptraining 出来的(改造),可今天 Llama 那一系从第一天就是 GQA(新建)。 同一个机制,两栏都待过。 带走的用法:拿到一个新名字,先把它放进某一格 放得进去的,它的优点和代价你已经知道了 —— 改造类的就去问「承重墙在哪」(哪里改不动);新建类的就去问「重训要多少钱」。 放不进去的,才值得你花时间 —— 那才是真正的新东西。
⭐⭐ 三个旋钮说的是「改什么」,这一张说的是「什么时候改」—— 两个轴正交。事后那一列便宜、可开关,但天花板就是「接近原来那个」;native 那一列要重训,却能持平甚至超过。⚠️ 注意:这说的是方法「这次被怎么用」,不是它本身的属性。
出处与口径

各家的出处见 §五 / §六 对应小节;GQA 的 mean pooling + α=5% 续训出自 arXiv 2305.13245 §2.2

⚠️ 「老房改造 / 图纸阶段」是本课的比喻;⚠️ 「四次」只数到旋钮①②,旋钮③ 那一格本讲没有核过数,故留空

⚠️ 这里一定会被问:「那 KV 量化算不算第四个旋钮?」 —— 不算 —— 它跟这三步正交。 三个旋钮管的是存几个数、读几个数;量化管的是每个数用几个 bit。 两者可以任意组合(DeepSeek-V4 就是稀疏 + KV 混合精度一起上)。 精度那一维整个归专题八

📌 所以「没有第四个位置」这句话的准确版本是: 在「要不要留、留多少、读多少」这件事上没有第四个位置; 「每个数多大」是另一根轴。

旋钮在改什么代表
① 每个 token 存多少减少 KV 的份数维度MQA → GQA → MLA → Gated MLA
② 每个 query 看多少限制范围动态挑选SWA · NSA · DSA · CSA / HCA(⚠️ CSA 同时也在拧 ①,见 §6.4c
③ 换一套数学固定大小的状态代替不断变长的 KV线性注意力:DeltaNet → GDNKDA
①+②+③ 混着来不同层用不同方案Hybrid:V4 的 CSA+HCA、K3 的 KDA+Gated MLA

⛔⛔ 账本在这里交棒。

开场那句「只有一个账本:KV cache」到这里为止还成立, 但拧下去就不成立了

  • 旋钮① 确实在改这个账本 —— 每份更小,488 直接变小。
  • 旋钮② 一个字节都不省。DSA 的 KV 全都存着,只是不读 (§九 那张表里这一格写的就是「—」不是「↓」)。 它改的是算力和访存
  • 旋钮③ 把这个账本整个作废 —— S 从张量形状里消失了。

所以「一个账本」是一个极好的开场钩子,但它不是全讲那句话。 它的职责是 §零–§五 的记账装置:让你在最初那段有个具体的东西可以盯。 从这一节起,账分成三样 —— 显存、算力、访存规整度§十 收在那三样上,那才是全讲唯一的落点。

⚠️ 这一句不能跳过:带着「省 KV」这一个念头读完 §六 和 §七, 到 §十 会发现对不上 —— 而那时候很容易以为是自己没看懂。

4.2b 一条反复出现的判据:事后压,还是从头按压缩训

⭐ 这张地图的用法:拿到一个新名字,先把它放进某一格。 放得进去的,它的优点和代价你已经知道了,不用细看; 放不进去的,才值得你花时间 —— 那才是真正的新东西。

⚠️ 这一条曾经被本讲写成「第二个轴」,现在降级了 ——  因为它撑不起「轴」这个词。 上面那张图四行全落在旋钮①②上;旋钮③ 一行都没有

📌 旋钮③ 那一格为什么空着,值得如实说:本讲那张 44 行表里, 所有线性/混合模型都是 native 的(从第一天就按这个结构训)。 「把一个训好的 Transformer 事后线性化」是有人在做的一支, 但本讲没有核过它的数,所以这一格留空 ——  ⭐ 留空比硬凑一行好:空格是可以被后来的人填上的,凑出来的行只会被当成事实背下去。

⭐ 所以它的准确身份是:一条在旋钮①②上反复出现四次的判据, 而不是跟三个旋钮正交的第二根轴。作为判据它很好用,作为轴是虚的。

4.3 一张名词收纳表

这张表可以边读边填 —— 每读完一个支线回来补一格:

名词出处旋钮一句话
MQAShazeer, arXiv 1911.02150所有头共用一份 K/V
GQAAinslie 等, arXiv 2305.13245分组共用,MQA 与 MHA 之间的连续旋钮
MLADeepSeek-V2 / V3, arXiv 2412.19437KV 压成 512 维隐向量,用时再升回 128 头
Gated MLAKimi K3, arXiv 2607.24653MLA 输出端加一个全秩门控
SWAMistral 7B, arXiv 2310.06825只看前面固定窗口(Mistral 是 4096)
Attention sinkStreamingLLM, arXiv 2309.17453—(现象)开头几个 token 被当作"停车位",扔了就崩
NSAarXiv 2502.11089压缩 / 选择 / 滑窗三条支路,门控融合,训练时就用
DSADeepSeek-V3.2, arXiv 2512.02556Lightning Indexer 给每个 query 挑 top-k
CSA / HCADeepSeek-V4, arXiv 2606.19348①(token 维)+②先把 KV 按块压缩(=沿 token 维压,见 §6.4c),再稀疏挑选;两档压缩率混排
DeltaNet起源 Schlag 等 arXiv 2102.11174;可并行化 arXiv 2406.06484状态更新用 delta rule:擦掉旧的再写新的
GDN(Gated DeltaNet)arXiv 2412.06464在 delta rule 上加遗忘门
KDAKimi Linear, arXiv 2510.26692遗忘门从标量升级成 per-channel 向量
FlashAttentionarXiv 2205.14135不是旋钮数学一个字不改,只改访存顺序
RoPERoFormer, arXiv 2104.09864不是旋钮把位置变成转角,让打分直接带上「差几格」。它管距离,不管谁在前;给 MLA 惹出 decoupled 那一路(§5.3
NoPEarXiv 2305.19466(NeurIPS 2023)不是旋钮decoder-only 靠因果掩码就能学会顺序 —— 显式位置编码不是必需品(§8.3 混合架构因此能整层不放 RoPE)

第 五 节

旋钮①:让每一份更小

旋钮① 每个 token 存多少 —— 动的是这两条支路 点亮的两格就是这个旋钮的全部作用域 —— 正是带 S 的那两处。⭐ 它省的是每一份 KV 有多大,不是算力。 X BTD —— 一层的输入 BTD W_Q · DNH BTNH Q BSD W_K · DKH BSKH K BSD W_V · DKH BSKH V reshape BTNH → BTKGH BTKGH BSKH BTSKG Q·Kᵀ + masks 谁能看谁 softmax BTSKG(注意力矩阵,记作 A) 这一步不落地,贵在要算的次数 BTSKG BSKH BTKGH A·V reshape BTKGH → BTNH BTNH W_O · NHD BTD 输出投影 + 残差 BTD norm BTD MLP(本专题一个字都不改它) BTD ·DF→ BTF ─gelu⊛─ BTF ·FD→ BTD + 残差 → norm BTD → 下一层 这一层算完,桌上的东西分三堆 常驻的设备 权重 W 所有人共用,不随对话变 草稿纸 中间那些量 算完就扔,不留到下一步 锁进柜子的 K 和 V 每来一个 token 就多一格 ① 三种改法,都只改这两格 ➜ 这一格 把 488 这个数直接改小 —— 它动的就是这笔账的分子。 📌 这一格的完整讲解在图下面的正文里 ⭐ 一句话记住这个旋钮:它改的是「存什么」,不是「算什么」。所以它治的是显存墙,治不了 O(N²) 的计算量。
同一张主线图,只点亮旋钮① 动到的地方 —— 产生 K 和 V 的那两条支路。注意那个平方大的矩阵一点没动:这个旋钮治的是显存墙,治不了 O(N²) 的计算量

三种改法,都只改这两格

改法动了什么
MQAK 从多个头砍到 1 个(K=1)
GQA砍到几个,多个 query 头共用一个 KV 头(G=N∕K)
MLA不砍头,改成先压到一个低秩的 c,用时再上投影

⭐ 看形状就知道省在哪:KV cache 存的是 BSKH, 里面那个 K 变小,缓存就等比变小 —— 就这么直接。

⚠️ 它不省核心那两个 matmul 的 FLOPs。 Q·Kᵀ 出来的还是 BTSKG,该算的乘加一次不少 ——  GQA 靠的是把 KV 头广播开再算。

⭐ 但训练侧它省:W_K / W_V 跟着缩 ——  参数、优化器状态、投影的 FLOPs 三样都省。

📌 MLA 那条另有一处麻烦:RoPE 必须单独走一路, 因为上投影吸收不了带位置旋转的那几维(见本节前面那段)。

RoPE —— 把位置写成一排里程表转盘 这张图回答三件事:它到底负责什么为什么点积自动带上了相对距离、以及凭什么能外推到 1M 掩码管顺序 位置 m 位置 n 夹角=m−n 三种改法 ① 先摆正职责 —— 「谁在前谁在后」并不归 RoPE 管 这一格几乎所有讲法都拧了:那个经典例子证明的是「没有掩码时分不出」,不是「所以必须有位置编码」 没有掩码时(双向 encoder) 同一个 o 两句的输出 一模一样 因为 o = 对所有位置求和,而求和不认顺序 —— 把输入重排,输出只是跟着重排。这才是那个例子真正说明的事。 有因果掩码时(今天的 decoder-only LLM) 第 2 格看得见 = {你, 打} 第 2 格看得见 = {我, 打} 已经不一样了 每个位置只对自己的前缀求和,两句的前缀不同 → 中间那一格就已经分开; 再上一层,顺序就传到了句尾。NoPE(arXiv 2305.19466)证明的就是这件事。 ② 一个位置 = 一排转盘的读数 最右边转得飞快,往左逐级变慢 —— 跟里程表一模一样 第 1 对 转速 1/β^0 第 10 对 转速 1/β^9 第 19 对 转速 1/β^18 第 28 对 转速 1/β^27 第 37 对 转速 1/β^36 第 46 对 转速 1/β^45 第 55 对 转速 1/β^54 ← 快(每个位置都转一大格) 慢(几万个位置才转一圈)→ 这排转盘就是 位置 n 的 β 进制写法(β = 10000^(2/d) ≈ 1.155) d=128 就是 64 位数;第 m 位的除数 β^(m-1),正是第 m 对维度的转速 顺带拆一个流传最广的说法:「RoPE 有用是因为越远打分越低」—— 这不是它起作用的原因(arXiv 2410.06205 专门论证了这点:RoPE 并不单调衰减)。 那篇论文扒开训练好的 Gemma 7B 看到的是分工最快的那几个盘被拿去搭稳定的位置型注意力(「盯住我前面第几个」), 最慢的那几个盘用得最多 —— 它们几乎不随位置变,论文推测是被模型腾出来携带语义。 快盘当尺子,慢盘当载货位。 ③ 为什么点积自动带上了相对距离 —— 看夹角就行 两排转盘叠起来,每个盘上的夹角都是 (m−n)×转速 夹角 = (m−n)×转速 夹角 = (m−n)×转速 夹角 = (m−n)×转速 夹角 = (m−n)×转速 夹角 = (m−n)×转速 蓝=位置 m 转过的角 · 橙=位置 n 转过的角 每个盘上你只看得出两根针差多少 —— 看不出各自转到了哪儿。 而点积 a·b = |a||b|cos θ 只吃夹角和长度:同转一个角,两样都没变。 所以绝对位置被转掉了,留下来的只有 m−n ④ 于是长文本那三种做法,一句话各自归位 同一排转盘,三种改法 直接外推 转盘一格不改 硬往超出刻度的地方读 最慢那个盘从没转到过那儿,模型没见过 位置内插(PI) 每格改成走半格 整排一起放慢 最快那个盘现在分不清相邻两个位置了 NTK-aware 换一个进制 β 变大:快盘几乎不动,慢盘明显变慢 一句话:高频外推、低频内插 这张图跟「寄快递」不冲突,是同一件事的两个切面 寄快递回答的是「为什么 MLA 吸收不了它」—— 那个旋转矩阵夹在上投影和隐向量中间,拆不开。 里程表回答的是「为什么点积自动带相对距离」和「凭什么能外推」。
⭐⭐ 一个位置 = 一排里程表转盘的读数:最右边转得飞快,往左逐级变慢。两个位置各自转完之后,每个盘上你只看得出两根针差多少,看不出各自转到哪儿 —— 这就是「点积自动带相对距离」的全部内容。⭐ 顺带把长文本那三种做法一句话各自归位:直接外推=硬往超刻度处读;内插=每格走半格;NTK-aware=换一个进制。
出处与口径

①「顺序归因果掩码,不归位置编码」:NoPE,arXiv 2305.19466(NeurIPS 2023)—— 证明 decoder-only 不加任何显式位置编码也能学会顺序。⛔ 常见讲法拿「你打我/我打你」论证「所以必须有位置编码」,是把前提(没有掩码的双向模型)漏掉了。

②「RoPE 不是靠越远越衰减起作用」+「快盘做位置、慢盘携带语义」:Barbero et al.,arXiv 2410.06205 —— 对训练好的 Gemma 7B 做的内部分析;⚠️ 其中「携带语义」原文是 we suspect,这里照样只当推测

「RoPE = 位置的 β 进制写法」以及外推/内插/NTK 的统一解释,出自苏剑林 kexue.fm/archives/9675(⚠️ 原文是推导,没有图)

「二进制计数器 → 正弦」的动画装置出自 Fleetwood huggingface.co/blog/designing-positional-encoding;「点积只吃夹角和长度」出自 EleutherAI 的 RoPE 博客

📐 θ_m = 10000^(−2(m−1)/d) = 1/β^(m−1) 这条等式由本脚本当场验证并断言,不是转述;RoPE 原始出处 RoFormer arXiv 2104.09864

四种存法 —— 先只看接线,一个数都不看 这张图只回答一件事:省,省在哪儿。省多少、值不值,留给下一张。 看最下面那排行李箱就够了 —— 四家的算力几乎一样,差别全在「这段对话得一直留着多少东西」 Q 头 实线盒子=要进箱子 虚线盒子=用完就扔 MLA 的压缩件 同一个模型、同样多的头,只换「K/V 怎么存」这一件事 每一格从上往下读:一排头 → 它们各自去问谁要 K/V → 最后哪几个盒子进箱子 MHA 每个头各问各的 Q Q Q Q K V K V K V K V 跨 token 要留下来的 谁也不共用 —— 这就是基准线,没省。 GQA 几个头合用一份 Q Q Q Q K V K V 跨 token 要留下来的 头还是那么多, 箱子里的盒子变少了。 MQA 所有头合用同一份 Q Q Q Q K V 跨 token 要留下来的 共用到头了, 也最容易掉点。 MLA 谁也不直接存 K/V Q Q Q Q 压缩件 K V K V K V K V 用的时候现场展开,用完就扔 跨 token 要留下来的 箱子里那个不是 K/V 这一排箱子,就是后面所有账的那个「一份」 四家该做的乘加几乎一样多 —— 省下来的不是算力,是「这段对话从头到尾得一直背着的东西」 MLA 那一格请多看两眼:箱子里只有一个,而且它不是 K/V。每个头真正要用的 K/V 是从它现场展开出来的 —— 那几个虚线盒子用完就扔,不占箱子。 这张图故意不回答的两件事 ① 省多少。下一张散点图 —— 那里横轴是「一份占多少地方」,每个数本课都当场算过。 那张图有个反直觉的题眼:MQA 比 MLA 存得还少 ② 凭什么能这么省、以及谁掉点。「共用」那一步到底是什么,看 那张把复制矩阵一格一格画出来的图;MLA 展开那一步为什么不白费力气,看 「吸收」那张图
⭐⭐ 只看最下面那排行李箱:四家该做的乘加几乎一样多,差别全在「这段对话得一直背着多少」。⭐ MLA 那格多看两眼 —— 箱子里那个不是 K/V,虚线的那几个是用的时候现场展开、用完就扔的。
出处与口径

⭐ 这张图不含任何数,所以也没有需要核的口径 —— 它画的是四种做法的接线关系,四家各自的出处(MQA arXiv 1911.02150、GQA arXiv 2305.13245、MLA DeepSeek-V2 arXiv 2405.04434)记在后面那几张有数的图上

⚠️ 头的个数画成 4 只是为了一眼能数完,跟任何真实模型的头数都无关;GQA 画成两组同理 —— 真实分组数看讲四种存法的那张散点图

⛔ 图里不区分 K 和 V 各自的份数(它们在这四家里都是同进同出),一个盒子代表「一个头位置上的 K 和 V 合起来的那一份」

旋钮 ① 让每一份更小 —— 但比的从来不是「谁存得最少」 同一个形状(V3:61 层 · 128 头 · 每头 128 维 · 128K · bf16),换四种存法 四种存法 —— 把它们摆到一张图上 横轴:一份占多少地方(对数) · 纵轴:换回多少能力( 定性) 跟 MHA 基本打平 明显更差 10 100 GiB · 对数轴,每往右一格 ×10 MHA 488 GiB 每个头各存一份 GQA-8 30.50 GiB 8 组,组内共用 MLA 8.58 GiB 压成 512 + 64 MQA 3.81 GiB 所有头共用一份 2.25× 题眼:MQA 比 MLA 还小 2.25 倍,却更差 所以这一支比的 不是「谁存得最少」 MQA 早在 2019 年就把 体积压到头了。 要比的是:同样一份 字节,换回多少能力。 (这正是第五节的线) 纵轴是定性的 —— 四家没有同一份可比的实测。唯一同基准的一对是 MQA 论文表 3:MHA 29.9 / MQA 30.2 / 真单头 31.2(困惑度,越低越好)。 GQA 与 MLA 的论文只声称「接近/不弱于 MHA」,不是同一张表 —— 所以这里只画两档,不画连续刻度。
⭐⭐ 左边四种存法摆在同一形状下:MQA 反而比 MLA 还小 2.25 倍。所以这一支比的不是「谁存得最少」,是「同样一份字节换回多少能力」。右边那三行代数,就是 MLA 为什么非要把 RoPE 拆出去单走一路。
出处与口径

📐 横轴那几个数:61 层 · 128K · bf16 · batch 1,一个用户一份 —— MHA 488 GiB / GQA-8 30.50 / MLA 8.58 / MQA 3.81

四个数由公式当场算出并断言(脚本内);MLA 超参出自 V3 论文 §4.2:n_h=128, d_h=128, d_c=512, d_h^R=64, 61 层

MQA:Shazeer arXiv 1911.02150 GQA:Ainslie 等 arXiv 2305.13245 MLA:DeepSeek-V2/V3 arXiv 2412.19437

⭐ 「MLA 那 576 里为什么有个 64」在 §5.4 的 fig3-two-lanes 里 —— 本图只到「四种存法各占多少地方」为止

MLA 凭什么敢这么压 把它想成一张原稿,复印了 128 份 复印件 原稿 缩印 = 赌 白送的那段 ① 第一步是白送的 —— 扔掉复印件,只留原稿 复印 128 份,信息还是那一张 今天的存法:128 份复印件 K / V 第 128 份 一共 32,768 个数 每 token、每一层 其实都是 从它算出来的 原稿 h 7,168 个数 这一层的输入向量 复印件再多,也不会比原稿多出内容 所以「存 128 份」本来就是白占地方 —— 改成「只存那一张原稿」,一个字的信息都没丢。 32,768 → 7,168 白送 4.57 倍 这一段不用做实验 算一下就是这样 ② 第二步才是赌 —— 把原稿再缩印一次 缩印是真会糊的 原稿 7,168 缩印 576 个数 = 512 + 64 后面那 64 是 RoPE,另走一路 这一步跟上一步,性质完全不同 上一步是「扔掉复印件」—— 不丢信息,算出来的。 这一步是「把原稿缩小 12 倍」—— 一定会糊, 赌的是糊了也不影响用 赌 12.4 倍 两段相乘 = 56.9 倍 ③ 凭什么敢赌 —— 因为有人先拿训好的模型试过了 事后缩印 vs 生来就这么窄 —— 终点一样宽,区别在什么时候变窄 事后缩印(MLA 之前就有人做) 训好的模型 每一笔都写开了 缩印 (事后低秩) 挤成一团 同样 9 笔硬塞进这么窄 —— 挨上的那些就是被丢掉的方向 写的时候,没人知道以后要缩。 MLA:生来就这么窄 根根分明 没有「原稿」这一步。 训练时格子就这么大, 模型自己决定 这三笔写什么 两张终点稿纸一样宽 —— 区别不在压得多狠, 什么时候变窄的 事后缩印这条路 MLA 之前早有人走,这也是本课说「赌得有依据」的依据: Eigen Attention 省 40% · Palu 分组头低秩 + 自动分配秩,省 50% LoRC 逐层分配不同的秩(未报统一比例 —— 它按每层的敏感度分配,没有一个全局数字)。 带走一条判据:看任何一个压缩方案,先把它拆成两段 哪一段是「扔复印件」 —— 信息本来就重复,算一下就知道,不用做实验。这一段是白送的。 哪一段是「缩印」 —— 赌它糊了也不影响用。这一段必须看掉点。 一个方案只报总倍数、不拆这两段,多半是把赌的那部分算成了白送的。 两条口径,别讲过头 「白送 4.57 倍」不是普适的:它等于 2·n_kv·d_h ÷ d_model,分子是真正被缓存的头数。V3 是 MHA 所以 4.57;Llama-3-70B 是 GQA-8,算出来 0.25 —— 那里根本没 有「白送」这一段。 被缩印的是 K 和 V 合起来那一份(两者共用同一张原稿),比「只压 K」狠得多;带 RoPE 的那 64 维另走一路,不在缩印范围里
⭐⭐ 32,768 个数是从一个 7,168 维的 h 算出来的 —— 一次确定性映射不造信息,所以 4.57 倍是白送的。真正的赌注是后面那步:7,168 压到 576,等于强制 K 投影的秩 ≤ 512。而敢赌是因为「K/V 本来就低秩」在 MLA 之前已经被量过了。
出处与口径

维度出自 DeepSeek-V2 论文 §2.1(arXiv 2405.04434)与 V3 config:d=7168, n_h=128, d_h=128, d_c=512, d_h^R=64;三个倍数由脚本当场算并断言

事后低秩:Eigen Attention(EMNLP Findings 2024)、Palu、LoRC 「MLA 优于 MHA」的口径见 DeepSeek-V2 仓库 issue #26

⚠️ 「复印 / 缩印」是本课的比喻 —— 论文那一侧的说法是「低秩联合压缩」

砍头这一支 —— 贵的是通讯录,不是查的人 一个 query 头 = 一个要查东西的人;一份 K/V = 一本占抽屉的通讯录 各带一本 共用一本 只派一个人 ① 三种办法 —— 后两种占的抽屉一样多 同一篇论文的消融表 ① 八个人,各带一本 通讯录: 抽屉塞满 困惑度 29.9 MHA ② 八个人,共用一本 通讯录: 抽屉空了 困惑度 30.2 MQA ③ 只派一个人去查 通讯录: 抽屉一样空 困惑度 31.2 真单头 h=1 后两种占的抽屉一模一样(都只有一本),差别只在「还有几个人在查」—— 就这一点,困惑度差了整整 1.0 ② GQA —— 分组,每组一本 它的两个发明点,都不是「折中一下」 第 1 组 共用一本 第 2 组 共用一本 第 3 组 共用一本 第 4 组 共用一本 发明点一:不用从头重训 原来八本 平均 一本 续训 只要 5% 算力 → 直接能用 发明点二: 真正的动机是「模型越大,头越多」 全组共用一本,在小模型上还行;模型一大,削减力度就失控了 —— 分组让「几个人共用一本」这个比例,跟着模型规模走。 带走一句:贵的是通讯录,不是查的人 砍通讯录(K/V)省的是真金白银砍查的人(query 头)省不了多少,却很伤。同样一本通讯录,八个人查和一个人查,困惑度差 1.0。 这也正好解释了后面那件反直觉的事:MQA 存得比 MLA 还少,效果却更差 —— 它砍到的不只是通讯录,还砍掉了「从几个角度去查」这件事。 暗线第二次出现:事后压 vs 从头按压缩训 GQA 是事后的典范 —— 把已有的那几本通讯录平均成一本,再用 5% 的原始预训练算力续一下就能用,不用从头重训 MLA 是从头的典范 —— 要重训,但换来的是 56.9× 而不是 16×。 这两条路一直并存到今天,讲稀疏时还会再遇见两次。
⭐⭐ 最下面那一格只有两根轴,先看它们是垂直的:横轴上那两个点是 2019 年 MQA 论文列给别人的选项(后来叫滑窗、叫稀疏 / 压缩,也就是今天的旋钮②),纵轴上那条是它自己走的(旋钮①)。原点那个小方角是全图的论点 —— 而「正交」是论文原话,不是我们事后安的词。左边那个灰虚线框(限制序列长度)故意画在平面外:它也在那张单子上,但它不是旋钮,硬塞到某根轴上就是撒谎。中间那三行是「自由度不是宽度」的论文自带证据:同样只缓存一份 K/V,保留 8 个 query 头能把困惑度拉回整整 1.0。
出处与口径

①② 出自 MQA 原论文 Shazeer arXiv 1911.02150(§2.4 与表 3 —— ⚠️ 这篇一共只有 3 张表,Billion-Word LM 基准的 dev 困惑度);「正交」是原文用词

③ 出自 GQA 原论文 Ainslie 等 arXiv 2305.13245 §2.1–2.2:mean pooling、α=5% 续训、GQA-1=MQA / GQA-H=MHA

⚠️ 「通讯录 / 查的人」是本课的比喻

5.0b 那为什么更窄的 MQA 反而输了

⭐ 图里那三行困惑度值得单独记一下:MQA 和「真的只剩一个头」缓存的 K/V 一样多, 差别只在 query 侧还留不留 8 个不同的问法 —— 就这一点差了整整 1.0。 所以后面看任何一个「压缩」方案,都要分清它压掉的是体积还是自由度压体积通常还好,压自由度很贵。

MLA 省的那 56.9 倍,一半是白送的,一半是在赌 两栏都是真数字,你可以自己核 —— 不用相信我们 白送:秩够,恒等 赌:秩不够,有损 ① 白送的那一段 一摞复印件 → 只留一张原稿 原来的矩阵 2 5 4 1 1 2 1 0 3 7 5 1 1 4 5 2 拆成两块 (秩 2) × 再乘回来 2 5 4 1 1 2 1 0 3 7 5 1 1 4 5 2 每一个数都一模一样 秩够的时候,低秩分解是恒等变换 —— 零损失,不用做实验。 ② 赌的那一段 把原稿缩印 —— 秩不够了 原来的矩阵 3 7 2 3 5 3 4 3 4 4 1 3 3 4 5 2 拆成两块 (秩 1) × 再乘回来 4.2 5.1 3.4 3.1 3.8 4.7 3.1 2.8 3.2 3.9 2.6 2.4 3.6 4.4 2.9 2.7 红格子就是差出来的 这已经是最优的 rank-1 逼近 (SVD 的理论下界)—— 不是没调好,是压不下去。 所以看任何一个压缩方案,都先把它拆成这两段 哪一段是「表示冗余」白送的?—— 那一段不用看掉点,它是恒等变换。MLA 把 K、V 合进一个共享隐向量,这一步就属于白送(本课算出来是 4.571×)。 哪一段是在赌「它低秩/稀疏/可近似」?—— 那一段必须看掉点。MLA 把它压到 512 维,这一步是赌(12.44×)。两段相乘才是 56.9×。
⭐⭐ 两栏都是真数字,你可以自己核 —— 秩够的时候拆开再复原,每一个数一模一样(白送);秩不够硬压,红格子就是差出来的(赌)。⚠️ 右栏那个误差是 SVD 给的理论下界,不是「没调好」。
出处与口径

「拆开再复原、数字一模一样」这个装置偷自 planetbanatt.net/articles/mla.html 的 Manim 动画;⭐ 但他只画了左栏,右栏是本课补的

两栏的矩阵都由脚本当场构造并断言:左栏真秩 2、右栏真秩 3;低秩逼近用 numpy SVD(Eckart–Young 最优解)

⚠️ 4×4 是示意尺寸;MLA 真实是 32,768 → 576

5.0c 为什么它能压 —— 一笔信息账

这条推理链本身是可迁移的看任何一个压缩方案 —— KV、权重、激活、梯度 —— 都先把它拆成两段问: 哪一段是「表示冗余」白送的,哪一段是「赌它低秩/稀疏/可近似」赌来的? 白送的那段不用做实验,赌的那段必须看掉点。

5.1 + 5.2 + 5.3 + 5.4 从砍头到压缩

⭐ 这里真正值得单独说一句的,是「GQA 是一个连续旋钮」这件事本身 —— 它不是一个新机制,是把 MHA 和 MQA 之间的空白填上, 让你可以按需要选一个点。 这门课后面会反复见到这个套路:把一个二选一变成一个可调的连续量。

⭐⭐ 而这个旋钮之所以是连续的,理由比「取个中间值」深一层 ——  MHA、MQA、GQA 之间差的,只是同一个位置上同一块矩阵里写了什么。 把这块矩阵一格一格画出来,MLA 也会自己落到同一条轴上。

「分割和复制,都是简单的线性变换」—— 那就把它画出来 同一条流水线、同一个位置的同一块矩阵 · 例子取 4 个头、每头 2 维,所以每层的 K 一共 8 维 写死的 1 恒为 0 学出来的实数 ① 四个成员走的是同一条流水线 左右两头完全一样,区别全部集中在中间那一块 输入 x 这一层的隐向量 压一次 乘 W_c,得到要缓存的 c 这一块 从 c 还原出 4 个头的 K 4 个头的 K 一共 8 维,四个成员完全一样 四个成员的区别,全在这一块矩阵里 缓存的是这里的 c,不是右边的 K ② 那一块矩阵长什么样 行数 = 要缓存的 c 有多宽 · 列数都是 8(= 还原出来的 K)· 蓝格写着 1,灰格恒为 0 MHA c 宽 8 · 8×8 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 MQA c 宽 2 · 2×8 1 0 1 0 1 0 1 0 0 1 0 1 0 1 0 1 GQA-2 c 宽 4 · 4×8 1 0 1 0 0 0 0 0 0 1 0 1 0 0 0 0 0 0 0 0 1 0 1 0 0 0 0 0 0 1 0 1 左边两块是两个极端:MHA 退化成单位阵 —— 一个数都没复制,也一点都没压,c 就是 K 本身;MQA 是同一份被抄了 4 遍 —— 只存 2 维,代价是四个头拿到的 K 一模一样。 右边两块形状完全相同,都是 4×8、cache 都是 4 维:GQA-2 的 32 格里只有 8 个 1,其余 24 格恒为 0、而且不可训练 MLA 的同样 32 格全部可训练。 这就是全部的区别 —— 不在「压不压」,在这块矩阵是写死的还是学出来的 MLA c 宽 4 · 4×8 同样的 4×8,同样的 cache 宽度 只是这 32 格全部可训练 (不填数字:编出来的小数会被当成真权重) 把 0 和 1 换成学出来的实数 ③ 所以分界线在哪 不是「有没有低秩」,是「那个矩阵是写死的还是学出来的」 低秩区分不了这两个 把 GQA 所有的 K、V 叠在一起,GQA 本身就是一次低秩投影 —— 这一步 MHA 也好 MLA 也好,大家都在做。所以「低秩」不是分界线。 真正不同的是低秩之后那一步 GQA 用分割 + 复制把 c 凑成 4 个头的 K, 而分割和复制本身就是线性变换 —— MLA 只是把这个写死的变换,换成一个一般的、可学的。 但这么一换,KV cache 会涨回去 矩阵一放开,四个头的 K 又各不相同了 —— 要是照常缓存 K,cache 就退回 MHA 那么大, 省的初衷当场作废 MLA 能成立,靠的是下一小节那个恒等变换: 只缓存 c,把上投影矩阵挪到 q 那一侧去 这张图是「吸收」那一步的前提,不是它的替代。 一句话记住 GQA 的上投影是一个写死的 0/1 复制矩阵;MLA 把同一个位置、同一个形状的矩阵松开让它学 两头也在这条轴上:MHA = 单位阵(不复制也不压)、MQA = 一份抄满
⭐⭐ 「MLA 就是给 KV 做低秩分解」这句话没错,但它区分不了 GQA 和 MLA —— 因为 GQA 也是低秩投影。分界线在低秩之后那一步:GQA 用「分割 + 复制」把 c 凑成各头的 K,而分割和复制本身就是线性变换 —— 它对应的就是图里那块只有 8 个 1、其余全为 0、而且不可训练的矩阵。MLA 只是把同一个位置、同一个形状的矩阵松开让它学。⭐ 四个成员在图里列数完全相同(都是 8),只有行数(= cache 宽度)和格子内容不同:MHA = 单位阵、MQA = 一份抄满、GQA-2 与 MLA 同为 4×8。⛔ 但矩阵一松开,各头的 K 又各不相同,cache 会涨回 MHA 那么大 —— 所以必须接 §5.4b 的「吸收」。这张图是它的前提,不是替代。
出处与口径

📌 苏剑林《缓存与效果的极限拉扯:从MHA、MQA、GQA到MLA》kexue.fm/archives/10091 —— 「低秩投影这个角度并不贴近本质」「MLA的本质改进不是低秩投影,而是低秩投影之后的工作」「我们知道分割、复制都是简单的线性变换」三句均为原文逐字。

📌 图上那几个矩阵是本课按 h=4 / d_k=2 自己构造的,脚本用 numpy 断言过「c 乘这个矩阵」与「把 c 按组复制」逐元素相等 —— 这不是类比,是恒等。

📌 MLA 那一格刻意不填数字:本课没有 V3 的真权重,编几个小数放上去会被当成真的。

这张图要破的是一个很顺口、但区分不了事情的说法: 「MLA 就是给 KV 做低秩分解」。

低秩这个描述没错,但它区分不了 GQA 和 MLA ——  把 GQA 所有的 K、V 叠在一起,GQA 本身就是一次低秩投影。 苏剑林的原话是:「笔者认为低秩投影这个角度并不贴近本质…… MLA的本质改进不是低秩投影,而是低秩投影之后的工作。

⭐⭐ 低秩之后那一步,才是分界线。 GQA 用「分割 + 复制」把 c 凑成各头要的 K、V; 而分割和复制本身就是线性变换 ——  它们对应的就是图里那块只有 8 个 1、其余全是 0、而且不可训练的矩阵。 MLA 做的事,就是把这块写死的矩阵松开,让它学。

⚠️ 引这一段时,两处归属要说准。

① 苏剑林写的是「MLA 被视为 GQA 的一般化」, 不是「MHA / MQA / GQA 都是 MLA 的特例」 ——  后一句是网上转述时放大出来的,别挂在他名下。

② 他也没有否认 MLA 是低秩分解(他在另一篇里还写过 「从 MHA 的角度看,MLA 是给 K、V 加了 rank=512 的 LoRA」)。 准确的说法是上面那句:低秩这个描述没错,只是它区分不了这两个

③ 他给的条件 d_c = g(d_k+d_v) < dMHA 那一端会失效 ——  g 取到 h 时 d_c 就等于 K、V 本身的总宽,根本没压, 图里那块矩阵也退化成单位阵。所以「同一条轴」说的是那块矩阵的位置与形状, 不是说四个成员互为特例。

MLA 的超参出自 V3 论文 §4.2n_h=128, d_h=128, d_c=512, d_h^R=64, 61 层 —— 图上那 576 就是 512 + 64。

⛔⛔ MLA 最大的推理部署坑:它在张量并行下会退化(⭐ 2026-09-13 补)

⚠️ 先把作用域说死:下面讲的只是推理训练侧没有这个问题 —— 训练没有 cache 要复制,WUK/WUV 都是按头切的,TP 照切;被复制的只有那个 576 维的隐向量激活,跟 7168 维的 hidden 比可以忽略。

GQA / MHA 的 KV 是按头切的,TP=8 就每张卡各存八分之一,天然可分。 而 MLA 的 KV 是一份 576 维的隐向量,根本不按头分 —— 于是只剩三条路:

  • 每张卡各存一份(复制)—— TP=8 就等于把省下来的 57 倍当场还回去 8 倍
  • attention 那段改成数据并行(DP attention,SGLang 给 DeepSeek 就是这么做的) —— 代价是 attention 和 MoE 两段的并行策略不一致,中间得插 all-gather。
  • 只切 query 头,KV 仍然复制 —— 省了计算,没省显存。

⭐ 这条值得单独记,因为它是本讲主线的又一个例子: 一个在单卡上很漂亮的数学结构(把 K、V 合进一个共享隐向量), 到了多卡上恰恰因为「不按头分」而失去了最自然的切法。 ⛔ 所以引用「MLA 省 57 倍」的时候,要带一句「在什么并行配置下」。

📌 口径:三条做法是公开实现里能看到的;各自的具体开销本课没有实测。⚠️ 再强调一次:这三条全是推理侧的。

5.4b 「吸收」到底是什么 —— 这是 MLA 能省下来的前提

前面一直在说 MLA 把 K、V 压成一个 576 维的隐向量存起来。 那生成下一个词的时候,不是还得把它们拆回来才能比对吗?

「吸收」到底是什么 —— 把括号挪一下,仓库里的压缩包一个都不用拆 MLA 把 K 和 V 压成一个 512 维的压缩包存起来。那生成下一个词的时候,不是得把它们全拆开才能比对吗?—— 不用。这张图画的就是那个「不用」。 天真做法:全拆开 吸收:一个都不拆 同一块矩阵 W_UK(认这个紫色) ① 天真做法:每生成一个词,把仓库里所有压缩包全部拆开 解压 S 次 仓库(KV cache) 512 512 512 512 512 共 S 个 (128K 上下文就是 13 万个) W_UK 上投影 每个 K 都是 128 维 拆出来的 K,也是 S 个 而且每生成一个词就得全部重拆一遍 注意「重拆」两个字:拆完不能留 —— 留下来就等于又存了一份没压缩的 K,那正是 MLA 想省掉的东西。所以每生成一个词,这 S 次解压全都要重做一遍。 论文原话(DeepSeek-V2):这样就「must recompute the keys for all the prefix tokens during inference」 🏠 换成生活里的说法:书架上有一万本外文书,你每查一个词,就把一万本全翻译成中文再找。查完扔掉,下次再翻译一万本。 ② 吸收:把那块紫色矩阵搬到 q 那一侧 —— 压缩包一个都不用拆 解压 0 次 查询 q(一个,就一个) q(128) W_UK 转置着用 q′(512) 只做这一次 (不管仓库里有多少个包) 仓库(原封不动) 一个都没拆 直接拿 q′ 去跟压缩包点积 q ( W_UK c )( W_UK q ) c 同一个乘法。只是括号挪了个位置。 左边括号在 c 那边 —— 有几个 c 就算几次。 右边括号在 q 那边 —— 只有一个 q,所以只算一次。 🏠 生活版就是一句话:与其把一万本外文书全翻译过来,不如把你的搜索词翻译过去。 V 那一侧同理:W_UV 可以吸进输出投影 W_O —— 所以 K 和 V 两边都不用拆 ③ 省多少?—— 算出来的答案跟直觉不一样 d_c=512 · d_h=128 · 本课自算 直觉会说「解压 S 次变成 1 次,所以省 S 倍」。 不对 —— 吸收之后每个 token 的点积从 128 维变成了 512 维,这一头贵了 4 倍,在把省下的吃回去。 S = 128 (128 个 token 在仓库里) 省 64× S = 1,024 (1,024 个 token 在仓库里) 省 114× S = 4,096 (4,096 个 token 在仓库里) 省 124× S = 128K (131,072 个 token 在仓库里) 省 128× 上限 128× 这个上限正好是每个头的维度 d_h = 128,S 再长也过不去 —— 因为解压一个 token 要 d_h×d_c 次乘加,而点积只要 d_c 次,两者的比就是 d_h。 两条定律,一条允许、一条禁止 —— MLA 最难的两件事都在这里 结合律允许你挪括号:a(bc) = (ab)c。②那一步靠的就是它,论文原话是 「due to the associative law of matrix multiplication, we can absorb W^UK into W^UQ, a nd W^UV into W^O」。 但交换律不成立:ab ≠ ba。RoPE 会往 q 和 W_UK 中间塞进一个跟位置有关的旋转 R,而夹在中间的东西挪不出去 —— 原话「a RoPE matrix … will lie between W^Q and W^UK and matrix multiplication does not obey a commutative law」。 同一个把戏,本讲一共只挪两次括号 —— 这是第一次 这一次 —— MLA 的吸收(这张图):把括号从 qᵀ(W_UK c) 挪成 (W_UKᵀ q)ᵀ c —— 压缩包就不用拆了。 还有一次,在线性注意力那一章:把括号从 (QKᵀ)V 挪成 Q(KᵀV) —— 那个句长×句长的大方块就不用建了。那一次更赚。
⭐⭐ 盯住那块紫色的 WUK:①里它站在缓存那一侧,②里它搬到了 q 那一侧 —— 整张图讲的就是这一次搬家。⚠️ ③ 那个「上限 128×」是本课自己算的,只数乘加没算访存。
出处与口径

「吸收」与「RoPE 挡住它」两处原文均出自 DeepSeek-V2,arXiv 2405.04434§2.1.2、§2.1.3);超参 d_c=512 / d_h=128 / n_h=128 是 DeepSeek-V3 的口径

⭐ ③ 那笔账是本课自己算的,脚本里带断言:天真 = S×(d_h·d_c + d_h),吸收 = d_h·d_c + S·d_c;上限 (d_h·d_c+d_h)/d_c ≈ d_h。⚠️ 只数乘加,没算访存 —— 真机上访存往往才是瓶颈,所以这是个下界不是实测

⚠️ 吸收只在 decode 用得上:prefill 时一批里有很多个 q,「只变换一次」这个便宜就没了(这也是 §五 表里「压缩不生效」那一行的意思)

不用拆。把括号挪一下就行

qᵀ (WUK c) = (WUKᵀ q)ᵀ c —— 同一个乘法,只是括号换了个位置。

左边括号在 c 那侧:缓存里有几个 c 就得算几次。 右边括号在 q 那侧:一步只有一个 q,所以只算一次, 而缓存里的压缩包一个都不用拆

🏠 生活版就一句话与其把一万本外文书全翻译过来, 不如把你的搜索词翻译过去。

⭐ V 那一侧同理 —— WUV 可以吸进输出投影 WO。所以 K 和 V 两边都不用拆。

⚠️ 省多少?算出来的答案跟直觉不一样

直觉会说「解压从 S 次变成 1 次,所以省 S 倍」。不对 —— 吸收之后每个 token 的点积从 128 维变成了 512 维, 这一头贵了 4 倍,在把省下的吃回去。

两笔加起来算(图③),省的倍数有个上限,而这个上限正好是每头维度 d_h = 128:解压一个 token 要 d_h×d_c 次乘加, 而点积只要 d_c 次,两者的比就是 d_hS 再长也过不去这个数。

📌 口径:这笔账是本课自己算的,只数乘加,没算访存 —— 真机上访存往往才是瓶颈,所以它是个下界不是实测。

两条定律,一条允许、一条禁止 —— MLA 最难的两件事都在这儿

结合律允许你挪括号。上面那一步靠的就是它。 论文原话:due to the associative law of matrix multiplication, we can absorb WUK into WUQ, and WUV into WO

但交换律不成立。RoPE 会往 qWUK 中间塞进一个跟位置有关的旋转矩阵,而夹在中间的东西挪不出去原话:a RoPE matrix … will lie between WQ and WUK and matrix multiplication does not obey a commutative law

⭐⭐ 再往下追一层:卡住的不是「中间有东西」,是那东西带下标 假如塞在中间的是一块固定的矩阵 M,那 WUQᵀ M WUK 照样能预乘成一个矩阵, 吸收完全成立 —— 这一步只用到结合律,压根没要交换律。

可 RoPE 是相对的:RtᵀRj = Rj−t —— 每一对 (query, key) 对应一个不同的矩阵。 要预乘就得预乘出一整套,有多少种相对距离就有多少个。 这才是真做不到的那一步。

⭐ 而 5.3 那张图画的就是解法:把带位置的那一小块单独拎出来走 64 维一路 —— 宽的那条轨上没有闸,紫块照旧搬得走。

📌 同一个把戏,本讲这是第三次出场

§七 线性注意力:把括号从 (QKᵀ)V 挪成 Q(KᵀV) —— 那个句长×句长的大方块就不用建了。
§五 MLA 吸收(这一小节):把括号从 qᵀ(WUKc) 挪成 (WUKᵀq)ᵀc —— 压缩包就不用拆了。

⭐ 两次是同一个数学恒等式,而且都被同一类东西挡过: §七被因果 mask 挡住,这里被 RoPE 挡住。 挡住结合律的,永远是「中间被塞了个东西」。

⚠️ 作用域:吸收只在 decode 用得上 —— prefill 时一批里有很多个 q,「只变换一次」这个便宜就没了 (这正是本节开头那张表里「压缩不生效」那一行的意思)。

⭐ 于是同一个数学式子有了两种算法实现,选哪种取决于是 prefill 还是 decode —— 怎么在一个引擎里同时装下两套,留到专题六

5.4c 那条「无奈」的窄轨,可能才是 MLA 好的原因

§五 到这儿把「存多少」算干净了。但有个问题一直悬着 —— 压这么狠,模型会不会变笨? 前面每一笔账数的都是字节,质量那一维一次实测都没出现过

那条「无奈」的窄轨,可能才是 MLA 好的原因 同一组受控实验,每一步只改一件事 —— 而 KV Cache 全程钉死不动 ① 把 KV Cache 钉死在 512,只改注意力的形状 —— 两级台阶 纵轴是训练 loss,越低越好。三档的 KV Cache 一模一样,所以台阶量的是「同样的代价换回多少」 ~900M dense · 16B tokens · seq 4096 2.700 2.710 2.720 2.730 2.740 2.750 训练 loss 越低越好 MLA 2.721 cache 576 GQA2-128 2 组 × 128 维 2.750 −0.030 head_dims 128 → 256 (cache、层数、训练 tokens 全没动) GQA1-256 1 组 × 256 维 2.720 −0.009 只给那 64 维加 RoPE GQA1-256-PR 256 拆成 192 + 64 2.711 低于 MLA 0.010 KV Cache 512 三档一模一样 两级台阶合计 −0.039,而 MLA 比这一列的起点只好 −0.029 也就是说:把 head_dims 放宽、再拆出一小段给 RoPE,在普通 GQA 上就已经走过了 MLA —— 而这两样,一样都不是低秩。 这个减法是本课做的,不是原文的话 —— 原文的结论是「增大 head_dims 收益最大,Partial RoPE 也有一定帮助」。 ② 反过来看另一头:把 cache 放大 7 倍,一分没赚 同一张表里的 MHA —— 两根条按真实维度数等比画 MLA 576 loss 2.721 MHA 4096 loss 2.721 两个 loss 一模一样(都是 2.721),而上面那根的 KV Cache 是下面的 7.1 倍 —— 多存的那 3520 个维度,一分没换回来。 这跟 DeepSeek-V2 论文里「MLA 甚至优于 MHA」是同一件事的两种说法。原文对这个现象的猜测是:被比的那个 MHA,head_dims 只有 128。 ③ 先堵一个洞:宽的那档参数量本来就多,会不会赢在参数上 原作者补了三种对齐方式,这里画其中最干净的一种:把窄的那档 num_heads 翻倍,两边都是 943 M GQA2-128 32 条窄的 loss 2.723 GQA1-256 16 条宽的 loss 2.720 同样 943 M 参数、同样 512 的 cache,只差「切成几条」 —— 还是宽的那边赢 0.003。所以刚才那两级台阶不是参数量堆出来的。 另外两种对齐方式(缩 MLP、给 Q/O 上 LoRA)结论同向,原文给的幅度是「heads 翻倍相比 head_dims 翻倍,loss 稳定差 0.003 左右」。 带得走的那一条 压得少和学得好,是两件事。讲「白送 vs 赌」那笔 56.9× 的账管的是「存多少」,一个字都不用改;这一格管的是「同样存这么多,学得多好」。 所以那条被 RoPE 逼出来的 64 维窄轨,大概率不是妥协,是这个设计顺手做对的一件事 —— 原文的说法是「看似无奈的设计,极有可能是它效果优异的关键原因」。
⭐⭐ 盯住那条紫色虚线(MLA 的水位)和三级台阶:KV Cache 全程钉死 512,只把 head_dims 放宽、再拆一小段给 RoPE,第二级就已经踩到线下去了⚠️ ~900M dense、16B tokens 的小规模消融 —— 方向可信,幅度别往 671B 搬。
出处与口径

📌 全部数字一手核自 苏剑林《Transformer升级之路:20、MLA好在哪里?(上)》kexue.fm/archives/10907 —— Part I / II / VI 三张表。

📌 公共设置:类 LLAMA3 Dense,hidden 2048 / 12 层 / 16 头,优化器 Muon,训练长度 4096,总 16B tokens / 16k 步;除面板③ 外参数量不严格对齐(原文说明)。

📌 「两级台阶 0.039 vs MLA 0.029」这个减法是本课做的,不是原文结论;原文结论为「增大 head_dims 收益最大,Partial RoPE 也有一定帮助」。

这组数反转了本讲自己刚说过的一句话

§5.3 讲到 RoPE 那条 64 维窄轨时,本课用的词是「被赶到一条窄轨上」 —— 把它当成被代数逼出来的妥协。 可这组消融指的是反方向:同样 512 的 KV Cache,光把 256 拆成 192+64、 只给那 64 维加 RoPE,loss 就从 2.720 掉到 2.711。

那可能不是妥协的代价,而是这个设计顺手做对的一件事。 原作者的说法是「看似无奈的设计,极有可能是它效果优异的关键原因」。

📌 三条猜测,图里只画了两条

原文一共提了三个可能的功臣:head_dimsPartial RoPEKV-Shared(K 和 V 共享大部分维度)。前两条图里都有受控对照, 第三条本课只提,不画、不给数 —— 它要跟 RoPE 兼容得额外引入 一套新的位置编码,实验设计绕得多,而原文自己对它的措辞也最保守: 「应该也有一定作用」

顺手想一步:如果主因真是 head_dims, 那想让普通 GQA 追平 MLA,该从哪个数字改起、改到多少

原文给的答复很具体:「head_dims 应该要 192 起步了,并辅以 Partial RoPE」 —— 注意这是一条能直接写进配置文件的结论, 而它是从上面那两级台阶读出来的,不是从哪篇论文的摘要抄的。


第 六 节

旋钮②:KV 照存,但每步只读一部分

旋钮② 每个 query 看多少 —— 动的是 mask 那一格 点亮的是 Q·Kᵀ 和它后面那个 mask。⭐ 矩阵的形状一点没变,变的是里面有多少格子真的要算。 X BTD —— 一层的输入 BTD W_Q · DNH BTNH Q BSD W_K · DKH BSKH K BSD W_V · DKH BSKH V reshape BTNH → BTKGH BTKGH BSKH BTSKG Q·Kᵀ + masks 谁能看谁 softmax BTSKG(注意力矩阵,记作 A) 这一步不落地,贵在要算的次数 BTSKG BSKH BTKGH A·V reshape BTKGH → BTNH BTNH W_O · NHD BTD 输出投影 + 残差 BTD norm BTD MLP(本专题一个字都不改它) BTD ·DF→ BTF ─gelu⊛─ BTF ·FD→ BTD + 残差 → norm BTD → 下一层 这一层算完,桌上的东西分三堆 常驻的设备 权重 W 所有人共用,不随对话变 草稿纸 中间那些量 算完就扔,不留到下一步 锁进柜子的 K 和 V 每来一个 token 就多一格 ② 稀疏,本质上就是换一张 mask ➜ 这一格 这笔账一个字节都不变。它省的是「每步读多少」,不是「存多少」。 ⛔ 所以看着 488 别以为它在往下走 —— 它原地不动 📌 这一格的完整讲解在图下面的正文里 ⭐ 一句话记住这个旋钮:它改的是「看哪些」,形状不变、只是很多格子不算。所以省的是 FLOPs,而 KV 该存多少还得存多少。
同一张主线图,只点亮旋钮② 动到的地方 —— Q·Kᵀ 和它后面那张 mask。矩阵的形状一点没变,变的是里面有多少格子真的要算。⛔ 所以它跟旋钮① 正交,两个可以同时上。

稀疏,本质上就是换一张 mask

标准因果注意力的 mask 是一个下三角 —— 看全部历史。 ⭐ 所谓稀疏,就是把这张 mask 换成别的形状:

方案换成什么形状
SWA只留主对角线附近一条带 —— 只看最近 W 个
NSA三条路并存:压缩看全局 + top-k 挑重点 + 滑窗看近处
DSA拿一个轻量索引器先打分,只留 top-k 那几块
CSA先把每 4 个 token 压成 1 个 entry,在压缩后的格上挑

它跟旋钮① 是正交的:一个改 KV 存多少,一个改这张 mask ——  所以两个可以同时上(GLM-5 就是)。

⚠️ 纸面省下的 FLOPs,要 kernel 跟上了才算数。 不规则的 mask 对硬件不友好 —— 这是这一支真正的门槛, §六 那几家推理期选择器栽的就是这一跤。

旋钮 ② 每步只读一部分 —— 五种读法,画成五张 mask 就看明白了 这一支的历史本身就说明该画图:attention sink 那个 bug 从公式上看不出来,是把矩阵画出来才发现的 先说怎么读:格子是示意(16×16 画不出 1.56% 那种量级),每张下面那条细带才是真比例 —— 读格子看形状,读细带看狠不狠。 全注意力 基线:下三角全算 O(L²),KV 随长度线性涨 真实比例:100%(基线) SWA 滑动窗口 只看前面固定窗口(Mistral 4096) 跨 128K 要堆 32 层才摸得到 真实比例:3.1%(窗口 4096 / 128K) + Attention sink 滑窗 + 留住最开头 4 个 只留 4 个就够,扔了立刻崩 真实比例:3.1% + 4 个 sink DSA 学着挑 Indexer 给每个 query 挑 top-k 128K → 2K,64 倍;k=2048 真实比例:1.56%(k=2048 / 128K) CSA 先压再挑 每 4 个 token 压成 1 个,再在压缩后挑 V4:1M 下 KV 降到约 2% 口径不同,不能并排比(见下) 五张 mask 摆在一起,这一支的共同结构就出来了 同一个骨架,三条路粗看(压缩/全局,保证不漏)+细看(挑出来的 top-k,保证准)+近处(滑动窗口,保证局部连贯)。 NSA 三条都有(显式三支路 + 门控融合);DSA 主要是细看配一点局部;CSA/HCA —— HCA 粗看、CSA 细看,另挂一条滑窗;SWA 单用只有第三条 —— 所以它单用不行 Attention sink:一个「只有画出来才看得见」的 bug 朴素滑窗把开头几个 token 一起滑掉,模型立刻崩。而解法简单到荒谬 —— 留住最开头 4 个就够(原话 “with just 4 initial tokens sufficing”)。 为什么?softmax 强制所有权重加起来等于 1 —— 模型有时什么都不想看,却没有「弃权」这个选项,于是学会把多余的注意力倾倒在开头几个位置那几个 token 不是在传信息 ,是停车位。 这三年走了多远 —— 把 2% 那个数算给学生看 同样形状的 GQA-8 在 1M 上下文下是 244 GiB;V4 报的 约 2% 就是不到 5 GiB —— 一百万 token 的上下文,KV 装得进一块卡的零头。 对照本讲开头那个 MHA 的 488 GiB —— 而那还只是 128K这就是三年的进展。
⭐⭐ 五张 mask 并排,这一支的共同结构就出来了:先用一个便宜得多的办法决定「看哪些」,再只对那些做主注意力。区别只在那个「便宜的办法」是写死的规则,还是学出来的。
出处与口径

mask 图案为示意,用来表达各方案的读取形状,不是实测注意力分布;GQA-8 在 1M 下的 244 GiB 由公式当场算出(脚本带断言)

SWA:Mistral 7B arXiv 2310.06825 sink:StreamingLLM arXiv 2309.17453 NSA:arXiv 2502.11089 DSA:DeepSeek-V3.2 arXiv 2512.02556 CSA/HCA:DeepSeek-V4 arXiv 2606.19348

6.1 ~ 6.6 从「砍成一条带」到「先压再挑」

滑窗:凭什么敢砍,砍了为什么会崩 一个按时间顺序讲的侦探故事 敢砍的理由 崩了 真正的原因 ① 凭什么敢砍 —— 每层只看身边几个,但话能往外传 能传多远,比「层数 × 窗口」小得多 话确实能一层层往外传 第 1 层 1 2 3 4 5 6 7 第 2 层 1 2 3 4 5 6 7 第 3 层 1 2 3 4 5 6 7 但每一层往回跳多远,是随机的 L 层 = L 个随机数相加 —— 堆成一个钟形 「层数 × 窗口」 在这儿 —— 钟形的尾巴 早就没了 而且真实模型里,九成五的信息根本没走注意力这条路 残差 —— 直接从底下窜到顶上(约 95%) 注意力 —— 真正往回看的那一小股(约 5%) 每往回跳一个窗口就再乘一次这个小数 —— 于是有效射程跟层数无关,大约就是一到两个窗口宽 Mistral 7B:4,096 × 32 层 = 131,072 那是理论上限,不是能用的长度 所以工业界的答案不是「堆更多层」,是隔一层插一层真正的全注意力 gpt-oss-20b:24 层,滑窗 / 全注意力 1:1 交替 而且它的窗口只有 128 —— 窗口小到这个地步,靠堆层数是绝无可能够到 128K 的。 ② 砍了为什么会崩 —— 扔掉最前面四个,就崩了 Llama-2-13B,PG19 先说怎么读:这三根柱子按对数画 —— 线性画的话后两根根本看不见。 5158 和 5.40 差的是三个数量级,不是三倍。 5158.07 只留窗口 0 + 1024 崩了 5.40 留最前面 4 个 4 + 1020 好了 5.60 那 4 个换成换行符 4 + 1020 几乎一样 困惑度(越低越好) 判决性的是第三根 把那四个 token 换成 毫无意义的换行符 结果几乎一样 → 起作用的是位置,不是内容
⭐⭐ 一个侦探故事:先有办法(层数是免费的射程,32 层 × 4096 = 131,072),再出事故(困惑度 5.40 → 5158),最后才找到原因(softmax 要求一行加起来等于 1)。判决性实验在中间那格:把最前面四个 token 换成换行符,照样管用 —— 起作用的是位置,不是语义。
出处与口径

① 有效射程那一格出自 guangxuanx.com/blog/stacking-swa.html(作者是 StreamingLLM 一作,⚠️ 个人博客非同行评议):纯 SWA ≈ 0.58·W·√L;有残差时跟层数无关。⚠️ 其中 α≈0.95 是作者断言不是实测,所以本图只说「一到两个窗口宽」不写死倍数

gpt-oss-20b 的 1:1 交替与 sliding_window=128 是本课直接读 huggingface.co/openai/gpt-oss-20b 的 config.json 得到的

Mistral 7B arXiv 2310.06825 §2(k×W 射程、W=4096 / 32 层、rolling buffer cache);131,072 由脚本当场乘出来并断言

② 出自 StreamingLLM(Xiao 等 arXiv 2309.17453, ICLR 2024)论文表 1 / 表 2 与 §3.1 / §3.3:5158.07 → 5.40、换行符 5.60、留 1/2/4/8 个的对照

⚠️ 表 1(PG19 第一本书,65K)与表 2(拼接后 400K)不是同一个评测集;⚠️ 「传话」是本课的比喻

⭐ 「为什么会有这么个废票桶」在下一张 fig3-sink 里 —— 本图只到「它崩了」为止

6.1b 滑窗:凭什么敢砍,砍了为什么会崩

⭐ 一条课程内的接线: 图上那句「量化里那批 outlier 跟这是同一件事」不是顺口一提 ——  那批 outlier 在 专题八 有专门一节到那儿你会再遇到同一个形状:一个「毫无道理却极其稳定」的现象, 背后是一条你没注意到的守恒约束。同一个形状分在两讲里各讲一遍, 比在一讲里说两遍有用。

⭐ 图上五张 mask 从左到右,就是这一支的演进: 砍成一条带(SWA)→ 补回停车位(sink)→ 学着挑(NSA / DSA)→ 先压再挑(CSA/HCA)

6.1c 那张弃权票有什么用 —— 它是刹车片

上面说了 sink 是怎么来的。但还有半句没说: 它有什么用?—— 如果它纯粹是个副作用, StreamingLLM 特意把它留着就只是在打补丁。而事实不是这样。

attention sink 有什么用 —— 一滴红墨水,和一个替它踩刹车的人 前面只讲了 sink 是怎么来的(softmax 不许弃权,模型就造一个弃权用的候选人)。那它有什么用?—— 这张图说:那张弃权票不是浪费掉的,它是刹车片 被扰动的那个词,以及它染红的部分 sink(第 0 个 token) 没被影响到的词 同一句话、同一个扰动、同样四层 —— 右边只多了一个 sink 本课的线性简化模拟,不是实测 ① 没有 sink 红色像墨水一样铺开 ⟨起始⟩ 今天 股价 涨了 很多 输入 100% 第 1 层 100% 20% 17% 14% 第 2 层 100% 41% 39% 36% 第 3 层 100% 64% 67% 67% 第 4 层 100% 88% 102% 110% ② 有 sink 红色基本困在原地 ⟨起始⟩ 今天 股价 涨了 很多 输入 sink 100% 第 1 层 吸 80% 100% 6% 5% 4% 第 2 层 吸 80% 100% 13% 10% 8% 第 3 层 吸 80% 100% 19% 16% 14% 第 4 层 吸 80% 100% 26% 22% 20% 只读一件事:最后一行有多红。左边四层之后,旁观的三个词被推动了约 100% —— 跟当事人一样多,整句话都被污染了;右边只有约 23%,小 4.4 倍 ③ 它凭什么能刹住?—— 机制是两半,少一半就不成立 第二半最常被漏掉 1 吸得多 (attention 权重) 它把绝大部分注意力吸到自己身上。Llama 405B 里将近 80% 的注意力落在 第一个 token 上。—— 别人分到的就少了。 2 吐得少 (‖v‖ ≈ 0) 而它的 value 几乎是零。所以吸走的那一大块不带任何内容回来 —— 等于 凭空把混合强度按下去了。 只讲第一半是讲不通的:如果它吸走的注意力照样带内容回来,那信息一样会混 —— 只是换了条路。 所以准确的说法是:它是一个「几乎什么都不做」的去处 —— 论文管这个叫 approximate no-op。 于是前面讲 sink 那一格要改一句口径 前面说「softmax 不许弃权,模型就自己造了一个弃权用的候选人」—— 那一句只说到成因 这张图补上的是:那张弃权票不是浪费掉的,它是刹车片。没有它,扰动一个词,四层之后整句话都跟着动。 两个 80%,长得一样,意思完全不同 「Llama 405B 里将近 80% 的注意力落在第一个 token 上」—— 这是权重占比 「LLaMa 3.1 405B 里有 80% 的注意力头形成了强 sink」—— 这是头的比例(判据是阈值 ε=0.8)。
⭐⭐ 只读一件事:最后一行有多红。左边四层之后旁观的词被推动得跟当事人一样多,右边只有约四分之一。⚠️ 两张网格是本课的线性简化模拟,不是实测 —— Barbero 的 Figure 1 本身也是示意图。
出处与口径

装置与两句原话出自 Barbero 等《Why do LLMs attend to the first token?》(arXiv 2504.02732)Figure 1 caption:「The presence of attention sinks slows down the mixing of information between tokens and hence makes Transformers more robust to perturbations of prompts」·「The presence of a sink draws attention away from the rest of the tokens, limiting the spread of perturbed information」

⚠️ ①② 那两张网格是本课的线性简化模拟(均匀注意力 + 残差流 h ← h + Attn(h),sink 吸 80% 且 value 记 0),脚本当场跑并带断言 —— 不是实测。Barbero 的 Figure 1 本身也是示意图,他们的实测在 Figure 2

⛔ 三条别引错:「pump the brakes」不是 Barbero 的原话(全文零命中);「泄压阀」是 MIT HAN Lab 博客的转述不是论文的词;「一滴墨水」这个画面是本课原创,论文里没有任何生活比喻

机制是两半,第二半最常被漏掉

吸得多 —— 它把绝大部分注意力吸到自己身上 (Llama 405B 里将近 80% 的注意力落在第一个 token 上),别人分到的就少了。
吐得少 —— ⭐ 而它的 value 几乎是零。 所以吸走的那一大块不带任何内容回来

只讲第一半是讲不通的:如果它吸走的注意力照样带内容回来, 那信息一样会混,只是换了条路。论文管这个叫 approximate no-op —— 一个「几乎什么都不做」的去处。

📌 于是前面那句口径要补一半

「softmax 不许弃权,模型就自己造了一个弃权用的候选人」—— 那只说到成因。 补上的这一半是:那张弃权票不是浪费掉的,它是刹车片。

⭐ 所以 StreamingLLM 为什么砍掉开头几个 token 模型就崩 —— 不是丢了信息,是刹车没了。 (那几个 token 本来就没什么内容,这正是它们能当 sink 的原因。)

两个 80%,长得一样,意思完全不同

「Llama 405B 里将近 80% 的注意力落在第一个 token 上」—— 这是权重占比
「LLaMa 3.1 405B 里有 80% 的注意力头形成了强 sink」—— 这是头的比例 (判据是阈值 ε=0.8)。

⭐ 两句都出自同一篇论文,数字一样、含义毫不相干。 引的时候说串了,懂行的人一听就知道。

凭什么只看 2048 个就够 把它想成查资料:书墙很长,可这一次真正用得上的只有几本 只用一小格 鸡生蛋 让老师教徒弟 徒弟凭什么快 ① 先说一个经验事实 —— 书墙很长,用得上的很少 H2O 2023 量出来的 这一步能看到的历史:131,072 个 token 真正用得上的 就这么一小格 2,048 个 = 1.56% 剩下那 98.4% 几乎分不到权重 ② 难的不是「少看」,是「怎么知道该看谁」 一个鸡生蛋,和 DSA 的解法 要知道哪本书有用 —— 得先把它读了 可「每本都读一遍」正是我想省掉的事 又转回来了 DSA 的解法:让老师带一个便宜的徒弟 老师 = 真注意力 它当然知道哪本有用 —— 但它贵 照着学 徒弟 = 索引器 不需要懂内容 只要把书排对顺序 关键是「怎么算徒弟学得像不像」—— 不是让它猜,是对着老师的答案打分 热身阶段全模型冻住、注意力照常密集地算,只训徒弟去拟合老师那张分布(损失就是 KL)。 热身 2.1B token → 稀疏期再训 943.7B token ③ 徒弟凭什么便宜 —— 三样减法 它只排序,不做加权平均 64 头 主注意力是 128 头 人手减一半 ReLU 打分 不用 softmax 只排序,就不必「和为 1」 FP8 精度更糙 排序不需要那么准 还有一条硬的 它必须搭 MQA 模式 否则 kernel 上不划算 —— 旋钮①② 在这儿被硬件绑住了 带走一条套路:要省掉一个贵的东西,先让它自己说出答案 再训一个便宜的去复制那个答案。DSA 的索引器不是「一个猜谁重要的启发式」—— 它是主注意力分布的一个廉价复制品 这个套路到处都能用:蒸馏、投机解码、各种预测器 —— 都是「贵的当老师,便宜的当徒弟,拿老师的输出当标准答案」。 那 2048 这个数怎么来的 —— 老实说:论文没给消融 能说的只有三件事:① 128K 下它占 1.56%;② 两个公开口径(H2O「前 5%→95%」、Chen「前 20%→70%」)在 1.56% 处分别给 93% 和 40% —— 差一倍多 历史不足 2048 时,top-2048 就是全选 —— 这是定义直接推出来的,所以短上下文下 DSA 就是普通 MLA,稀疏只在长上下文才启动 暗线第三次出现:事后才压 vs 训练时就知道会被压 H2O 是事后才压的 —— 一个 KV 驱逐策略,不改训练,随时可开关。 DSA 是训练时就知道的 —— 热身 2.1B + 稀疏期 943.7B token 的继续训练,模型是在「我会被稀疏」这个前提下学出来的。
⭐⭐ 「能不能少看」是经验事实(H2O 量到注意力矩阵 95% 以上稀疏);「怎么知道该看谁」才是难点 —— 要判断谁重要得先算注意力分数,而那正是你想省掉的东西。DSA 的解法不是猜,是让真注意力当老师:冻住模型、保持密集,训一个便宜的索引器去 KL 拟合主注意力的分布。
出处与口径

① 出自 H2O:Zhang 等 arXiv 2306.14048(「over 95% sparse」与累计注意力分数的幂律分布,均为原文表述)

②③ 出自 DeepSeek-V3.2-Exp 技术报告 §1–§2.1:KL 对齐、冻结主模型热身、跨头求和后 L1 归一、梯度断开、ReLU「for throughput consideration」、index_topk=2048、索引器 64 头且跑在 FP8 上

⚠️ 「书墙 / 师徒」是本课的比喻;⛔ k 的消融论文没有给 —— 2048 这个数只能说它占多少、以及短序列下等于全选

先说清这个死结:要挑出重要的,得先算注意力;可算完了再挑,就没意义了 本课原来只讲了一种破法。其实有三条,而且是真正不同的三条 死结 师徒 一份算两用 降维打击 ① 死结长什么样 先把它画出来 想只算重要的那几块 → 得先知道哪几块重要 想知道哪几块重要 → 得先把注意力算一遍 转回来了 所以三条破法都在回答同一句话: 「怎么在不算全的前提下,知道该看谁」 ② 三条破法 —— 它们是真正不同的三条 不是同一招的三种说法 ① 师徒 DSA 老师 学生 只有名次表 让真注意力当老师 训一个便宜的学生去学它的排序 代价:训徒弟只要 2.1B token, 可之后师傅得陪着再训 943.7B ② 一份算两用 NSA 压缩分支 本来就要算 ① 当输出用 ② 当路由信号用 不训第二个打分器 压缩分支的分数本来就要算,直接拿它当路由 白捡:top-k 在前向图上是个 no-op, 只决定从显存搬哪些块 ③ 降维打击 CSA·V4 / IndexPool·GLM 12 格要挑 只剩 3 格 先把序列压短 4 倍 鸡生蛋没破 —— 但那只鸡小了 4 倍 两家独立想到同一招 —— 一家压 token, 一家压 indexer 的 key,都是 4 合 1 顺着这条死结往下走一步,「为什么必须整块取」就不用单独讲了 把链子接起来:相似度一旦算出来,事后再稀疏就没好处 —— 所以必须有一个便宜的近似打分器;而便宜的近似打分只能按块做(按 token 打分就等于把全表算了)。 于是「整块取」不是对硬件的妥协,是这个死结的直接推论。本课原来把它们讲成两段互不相干的话 —— 接上之后,两个难点变成一个。 最后那条路,两家独立走到了一起 —— 但他们压的不是同一个东西 DeepSeek 的 CSA 压的是序列本身:4 个 token 合成 1 个 entry,后面主注意力看到的就是压过的 智谱的 IndexPool 压的只是索引器手里那份 key:4 个池化成 1 个、挑出 Top-512 个 pool 之后再展开回 2048 个原 token —— 主注意力吃到的一个没少
⭐⭐ 三条破法是真正不同的三条:师徒(训一个便宜的学生学老师的排序)·一份算两用(压缩分支的分数本来就要算,直接当路由)·降维打击(先把序列压短 4 倍,鸡生蛋没破但那只鸡小了 4 倍)。⭐ 顺着这个死结往下一步,「为什么必须整块取」就不用单独讲了 ——便宜的近似打分只能按块做,整块取是这个死结的直接推论。
出处与口径

① DSA 的师徒(两阶段训练、冻主模型 + KL warmup)出自 DeepSeek-V3.2-Exp 技术报告 sec. 2;② NSA 的三支路出自 arXiv 2502.11089 sec. 3

③ CSA 每 4 个 token 压成 1 个 entry、在压缩后的格上挑,出自 DeepSeek-V4 相关公开材料(见 CSA 那张图的出处);IndexPool「把 4 个 indexer key 向量加权池化成 1 个」出自智谱 GLM-5.3-Flash 官方博客(z.ai/blog/glm-5.3-flash,2026-08,模型 MIT 许可开源)

⚠️「相似度算出来再稀疏就没好处 → 只能按块打分」这条链子的表述出自 zhouyifan.net 的 Log-linear Sparse Attention 一文;「师徒 / 一份算两用 / 降维打击」是本课的命名

6.2b 「怎么知道该看谁」是个鸡生蛋 —— DSA 让真注意力当老师

⭐ 这条套路值得起个名字:要省掉一个贵的东西,先让它自己说出答案, 再训一个便宜的去复制那个答案。 DSA 的索引器就是这么来的 —— 它不是一个猜谁重要的启发式, 它是主注意力分布的一个廉价复制品(跨头求和、L1 归一、KL 对齐)。 这个套路在推理优化里到处都是,值得单独记住。

NSA 的三条路 —— 其实就是你读一本厚书的三种办法 三条同时用,用一个学出来的门决定各占多少 翻目录 挑章精读 手边这几页 前人踩的坑 ① 一本很厚的书摆在你面前 —— 你会怎么读 这就是那三条分支 翻目录 每章压成一行 全书都扫到了,但粗 = 压缩分支 挑几章精读 整章整章地挑 挑中的看得很细 = 选择分支 手边这几页 刚翻过的上下文 最近的一定看 = 滑窗分支 三条同时用 一个学出来的门 决定这一步 各占多少 不是三选一 ② 全图最该记住的一格:为什么非得「整章整章」地挑 东一句西一句,书要来回翻 东一句西一句地抽 要翻 6 次书 —— 每次只拿一句 落到硬件上就是:访存不连续,FlashAttention 用不上 整章整章地拿 只翻 2 次 —— 每次整块搬走 同样多的内容,搬运次数差好几倍 而在 GQA 上,这件事更狠 一组里每个头各挑各的,可它们共用同一份 KV —— 真正要搬的是所有头挑中的并集 → 算是省了,搬没省。这正是 NSA 点名 Quest 的那一条。 ③ 这些讲究是从哪来的 —— 前人踩过的四个坑 NSA 论文 §2 逐条点了名 坑 1 省了算,没省时间 解码时稀疏,可 prefill 还得先把注意力图算出来 例:H2O 这一类 坑 2 挑的动作不可导 k-means、SimHash 这种挑法 学不到「该怎么挑」 例:ClusterKV / MagicPIG 坑 3 按 token 挑 → 来回翻书 散落各处的 token, FlashAttention 用不上 例:HashAttention 坑 4 GQA 上要搬并集 每个头各挑各的, 真正搬的是它们的并集 例:Quest 这一张真正的教益:稀疏不是「少算」,是「少搬」 四个坑里有三个都跟「搬」有关 —— 省了计算没省时间、访存不连续、搬的是并集。只有「挑的动作不可导」那条是算法问题。 所以看一篇讲稀疏的文章,先问一句:它省的是 FLOPs,还是字节?省 FLOPs 谁都会 —— 在纸上少算 90% 的格子而已;可只要那些格子散落在显存各处,要搬的字节一点没少 两条口径 「四个坑」是论文 §2 自己列的(逐条点了名,连例子都是原文的); 但「三条分支一条对一个坑」是本课的读法 —— 论文没有做这个一一映射。 稀疏到底能稀疏到什么程度,公开口径本身就打架:H2O 说「95% 稀疏、5% 够用」,NSA 引的 Chen 等 2024 说「前 20% 只覆盖 70% 的注意力分数」。 方向一致、程度差 一倍多 —— 别把任何一个当普适常数。 暗线第四次出现:事后压 vs 从头按压缩训 前面三次是 Eigen Attention 对 MLA、GQA 对 MLA、H2O 对 DSA。这是第四次,而且它就写在名字里 —— NSA 的 N 就是 Natively trainable 事后稀疏是「拿一个按『每个都看』训出来的模型,临时叫它少看」—— 它偏离了自己的预训练轨迹;native 是从第一天就按「我会少看」训四个分支,同一条暗线。
⭐⭐ NSA 的三条分支不是设计出来的,是被四个坑逼出来的 —— 而那四个坑是论文 §2 自己列的。最值钱的一条:计算稀疏 ≠ 访存稀疏。按块选、组内共享,都是为了访存,不是为了精度。
出处与口径

四个坑与三条分支出自 NSA(Yuan 等 arXiv 2502.11089)§2 与 §3;27B backbone / 260B token 亦出自该文

⚠️ 「读厚书 / 翻目录 / 整章拿」是本课的比喻;论文那侧的说法是 compression / selection / sliding window 三分支加一个 learned gate

6.3b NSA 的三条路,是被四个坑逼出来的

⭐ 这条判据的用法看任何一篇讲稀疏的文章,先问「它省的是 FLOPs 还是字节」。 省 FLOPs 谁都会 —— 在纸上少算 90% 的格子而已; 但只要那些格子散落在显存各处、或者同组的头各挑各的,要搬的字节一点没少。 NSA 的两个看起来很朴素的决定(按块选、组内共享),都是为访存,不是为精度

6.3c 唯一必须单独拎出来的一张表:「训练期稀疏」和「推理期稀疏」不是一回事

NSA 的「native」指的就是训练时就这么做,不是训练完再加的推理优化。 这个区别值得单独说三十秒 —— 它解释了后面一个反复出现的现象。

推理期稀疏训练期稀疏(native)
模型知不知道自己会被稀疏不知道知道,权重是在稀疏条件下学出来的
掉点有,且难预测小得多,甚至能反超
能不能省训练成本不能(NSA 报的反向 6.0×)
代价无,随时可开关要重训,没法给已有模型打补丁

⭐ 这张表解释了为什么这些新注意力方案总是跟新模型一起发布, 而不是作为一个推理框架的开关。

CSA 压的是 token,不是头 把 KV 想成一摞会议便签 —— 压它有两个完全不同的方向 每张写更短 四张订一张 只翻几页 两个版本交错 ① 一摞便签,两种压法 —— 它们是两件事 这一格弄混了,后面全乱 原来:8 张便签,每张写得满 8 条 × 每条 5 行 方向 A:每张写得更短 还是 8 条,但每条只剩 2 行 → 这是旋钮①(MQA / GQA / MLA) 方向 B:四张订成一张 第 1 摞 第 2 摞 条数 8 → 2 → 这才是 CSA(每 4 条压 1 条) 两个方向互不相干 「每条更短」和「条数更少」可以同时做 —— DeepSeek-V4 就是两个一起,再加上「只翻几页」 ② CSA 和 HCA —— 同一场会的两个版本 一个细但会漏,一个粗但不漏 CSA = 详细版 + 只翻其中几页 翻到的那几页,内容很全 没翻到的,等于完全不知道 HCA = 极简版 + 整本都看 一页都不会漏 但每页只剩一行,很粗 两种漏法正好相反 —— 交错摆也许正是为了让它们互相兜底 但论文只说了采用交错配置,没给这个理由 —— 这是一个讲得通的解释,不是它的设计意图。 ③ 成绩:一百万上下文成为常规配置 1M 下,V4-Pro 对 V3.2 27% 单 token 推理 FLOPs 「只翻几页」省的是算 10% KV cache 「四条并一条」省的是存 这两个数不一样,是有话说的 横着压主要省存储, 稀疏主要省计算 带走一条:拿到一个新方案,先问它动了哪几个方向 每条更短(一个 token 存多少)· 条数更少(几个 token 合一条)· 只翻几条(这一步读哪些)—— 三个方向互不相干。 前两个都在回答「那份要留下来的有多大」,所以都算旋钮①(只是下刀的维度不同);第三个是旋钮②V4 三个一起拧。 口径:这个 10% 跟别处那个 2% 不是一个基线 这里的 27% / 10% 比的是 V3.2;那张五格 mask 图上写的「约 2%」比的是同形状的 GQA-8,而且那 2% 还叠了一层跟注意力机制无关的 KV 混合精度。 两个数都对 —— 对的是各自的基线,别并排比。
⭐⭐ 压缩有两个互不相干的方向:竖着压(一个 token 存多少个数)是旋钮①,横着压(几个 token 合成一条)才是 CSA/HCA。CSA 压得轻+挑,看得细但会漏;HCA 压得狠+全看,不漏但看得粗 —— 两种坏法正好相反。⚠️ 「所以交错摆着互相兜底」是一个讲得通的解释,论文只说了采用交错配置、没给理由 —— 讲的时候别说成设计意图。
出处与口径

CSA / HCA 的机制出自 DeepSeek-V4 技术报告 arXiv 2606.19348 论文 §2.3–2.3.1(每 m 个压一条 → DSA top-k → 并上滑窗;HCA 压 m′≫m 但保持密集,m=4 / m′=128)

27% FLOPs / 10% KV cache 出自同一篇摘要与 §2.3.4(1M 上下文、对比 V3.2)

⚠️ 「会议便签 / 两个版本」是本课的比喻;⚠️ 「两种漏法互补所以交错」是从定义推出的解释,论文未给这个理由,也未在此给出层间配比

6.4c CSA 压的是 token 不是头 —— 压缩的两个方向

⭐ 那这三个轴跟三个旋钮怎么对上?——  而这一处全讲原先有三种说法,必须在这儿定死一个:

⭐⭐ 「横着压」不是第四个旋钮,它是旋钮① 的第二个方向旋钮① 问的是「每个 token 留多少字节」,而这件事有两个压法: 沿特征维压(MQA/GQA/MLA —— §五讲的全是这个方向)和 沿 token 维压(CSA:几个 token 合成一条,于是平均每 token 也变小了)。 两个方向都在同一个旋钮上,因为它们改的是同一件事:那份要留下来的有多大。

所以三处口径统一成这一条:§四 的名词收纳表写「旋钮①(token 维)+ 旋钮②」, 编年史那条泳道同理。⚠️ 而「一个 query 只做三步」这个封闭性论证不受影响 ——  横着压改的仍然是第一步(要留什么),只是换了个维度下刀。

三个轴互不相干,所以可以同时拧 ——  DeepSeek-V4 就是三个一起拧:MLA + CSA/HCA + DSA。

6.5b ⭐ 第二阶段:索引本身成了开销 —— IndexShare 与 IndexCache

前面四个方案(NSA / DSA / CSA+HCA)都有一个共同的零件:一个决定"该看哪几块"的索引器。 §6.2b 讲 DSA 的时候它叫 Lightning Indexer。到这一步为止,所有心思都花在让每个 query 少看几块上。

但索引器自己也要算。 它要为每一层、每一个 token,跟全部历史块打一次分。稀疏注意力把主体那部分省下来之后, 这笔原本不起眼的账就浮上来了 —— 尤其在 1M 上下文下,历史块本身就有几万个。

2026 年年中,两家公司几乎同时给出了同一个答案:别每层都重新算一遍"该看谁"。

模型叫法做法官方给的收益
GLM-5.2(智谱,2026-06-16,744B)IndexShare每四个稀疏注意力层共用同一个索引器1M 上下文下每 token FLOPs 降 2.9×
混元 Hy4-preview(腾讯,2026-08-28,770B/49B)IndexCache同上:跨层复用稀疏索引未单独给数

⭐ 这两家撞了同一个想法,而且证据不用查博客 —— 打开两份 config 就看得见。 它们的 indexer_types 字段都是同一个循环:

GLM-5.2  : full, full, full, shared, shared, shared, full, shared, shared, shared, ...
混元 Hy4 : full, full,       shared, shared, shared, full, shared, shared, shared, ...
                              └──────── 每 4 层里,只有 1 层自己算索引 ────────┘

GLM-5.2 的 config 里还有一个 index_topk_freq: 4 直接把这个 4 写了出来。

为什么这一小节值得单独留一块地方:它是一个优化制造出新的被优化对象的干净例子。 稀疏注意力是为了省 attention 的账而来的;省成了,于是索引 —— 它原本只是这个方案的附属零件 —— 变成了新的大头,再被优化一轮。这一节讲的三个旋钮都会经历这一步,不只是稀疏这一支。

⚠️ 这不是白拿的,代价要说出来。 官方博客只报了省下来的 FLOPs。 但共享索引器意味着这四层被迫看同一批块 —— 它们不能各自挑各自的。 这是表达力上的一次让步:原本每一层可以按自己那一层的语义去决定关注哪里,现在四层绑在一起。

📌 口径,分清能推到哪一步:「四层被迫看同一批块」是能推的 —— 索引器共享,选出的 top-k 集合当然就一样,这一步不需要实验。 但「这会不会掉点、掉多少」推不出来,而且两家都没公开。 ⚠️ 所以这里说的是「这是一次表达力上的让步」,不是「它让效果变差了」 —— 两句话差得很远,而且它给出 2.9× 的同时并没有报质量回退, 也可能这一让步在实测上几乎无损。

可带走的那一句只到这里为止:看到「降 2.9× FLOPs」这种数字, 先去找它在结构上放弃了什么自由度 ——  找到了不等于代价大,但没找到就说明你还没看懂它省在哪。

📌 DSA 的打分式子值得念一遍: I(t,s) = Σ_j w_j · ReLU(q_j · k_s) ——  ⭐ 用 ReLU 不用 softmax,纯粹是为了吞吐,论文自己这么说的。 索引器 64 头(主注意力 128 头),而且跑在 FP8 上;k = 2048

⭐ 段尾:两个旋钮拧完了,回头看一眼它们的共同点 ——  旋钮①② 都还在跟那张表打交道:一个让每格更小,一个让读的格子更少, 但那张会一路变长的表,始终都在。

⭐⭐ 下一节是唯一一个不跟它讨价还价的 —— 它直接把表拿走。 而拿走之后换来的,不是一个更快的注意力,是另一个模型。


第 七 节

旋钮③:换回一个固定大小的状态 —— 线性注意力

旋钮③ 换一套数学 —— 把那个平方大的矩阵整个删掉 ⛔ 这一张跟前两张不一样:它不是高亮,是替换。原来那四格(Q·Kᵀ → mask → softmax → S·V)没了,换成点亮的这两格。 X BTD —— 一层的输入 BTD W_Q · DNH BTNH Q BSD W_K · DKH BSKH K BSD W_V · DKH BSKH V reshape BTNH → BTKGH BSKH(K) BSKH(V) BKHH 先算 KᵀV = 状态 BTKGH(Q) BKHH(状态) BTKGH 再拿 Q 去读状态 reshape BTKGH → BTNH BTNH W_O · NHD BTD 输出投影 + 残差 BTD norm BTD MLP(本专题一个字都不改它) BTD ·DF→ BTF ─gelu⊛─ BTF ·FD→ BTD + 残差 → norm BTD → 下一层 这一层算完,桌上的东西分三堆 常驻的设备 权重 W 所有人共用,不随对话变 草稿纸 中间那些量 算完就扔,不留到下一步 锁进柜子的 K 和 V 每来一个 token 就多一格 ③ 不用听解释 —— 读输出形状就够了 ➜ 这一格 这笔账整个作废。S 从形状里消失,状态跟长度无关 ——  上面这张按 S=128K 算出来的表,在旋钮③ 下根本不成立 📌 这一格的完整讲解在图下面的正文里 ⭐ 这是三个旋钮里唯一改变了模型能表达什么的一个。另外两个改的是存法和看法,这一个改的是数学本身
这一张不是高亮,是替换:原来那四格(Q·Kᵀ → mask → softmax → S·V)整个没了,换成点亮的两格。⭐⭐ 不用听解释,读输出形状就够 —— 点亮那格输出 BKHH,S 不见了。状态大小只跟头维有关,跟序列多长无关。

不用听解释 —— 读输出形状就够了

softmax 的分母要对所有位置求和,所以它锁死了乘法顺序: 必须先 Q·Kᵀ(于是必须造出那个平方大的矩阵),再乘 V。 ⭐ 把 softmax 拿掉,乘法就可以重新结合:先 KᵀV,再乘 Q。

⭐⭐ 看点亮那格的输出形状:BKHH —— S 不见了。 状态大小只跟头维 H 有关,跟序列多长无关。 这就是 O(N²) → O(N) 的全部内容,写在形状里,不用相信谁。

但代价也写在同一格里。 因果版不能真的这么一乘 —— 状态要按 t 一步步累加,于是串行回来了; 分块并行(chunkwise)就是为了把并行度再找回来(见 §7.4)。

⚠️ 它不是「更快的 attention」,是另一个模型。 固定大小的状态 → 信息必然有损,长程精确检索会力不从心。

旋钮③ 的立身之本 —— 同一个乘法,只是把括号挪了个位置 矩阵按真实比例画。那个大方块有没有被造出来,是看出来的,不是读出来的。 必须造出来的大方块 跟句子长短无关的小方块 ① softmax 在的时候 —— 必须先把那个大方块造出来 分母要对所有位置求和,所以乘法顺序被锁死 Q 句长 × 头维 × Kᵀ 头维 × 句长 ① 先算这一对 Q·Kᵀ 句长 × 句长 句子翻倍,它翻四倍 × V 句长 × 头维 这张表到底多大(S = 128K) 171.8 亿 个数 = S² = 17,179,869,184(每层、每个头) 它不是「要占这么多显存」—— FlashAttention 之后分块算、算完就扔。 但每一个格子还是都要算一遍。 省掉的是显存,不是算力。 句子翻倍 → 这个数翻四倍 softmax 的分母要对所有位置求和 —— 所以这张表躲不开,必须先整个算出来。 ② 把 softmax 拿掉 —— 括号一挪,大方块根本没被造出来 同一个乘法,同一个结果 Q 句长 × 头维 × Kᵀ 头维 × 句长 × V 句长 × 头维 ① 改成先算这一对 KᵀV 头维 × 头维 句子再长,它还是这么大 这块到底多大(D = 128) 16,384 个数 = D² 跟 S 没有任何关系 句长从 2K 涨到 128K, 这个数一个都没变 —— 它压根不认识 S。 跟上面那张表差 1,048,576 倍 (2²⁰,正好一百万出头)。 这就是旋钮③ 的全部本钱 消失的那个 S,就是这个没被造出来的大方块。 这也正是为什么它的状态形状是 BKHH —— 里面根本没有句长这一维。
⭐⭐ 同一个乘法,只是把括号挪了个位置 —— 左边被迫造出一个句长×句长的大方块,右边中间那块只有头维×头维,跟句子多长完全无关⛔ 但因果 mask 会把这个重排挡住 —— 逐元素乘那张下三角,恰恰就是挡住你用结合律的东西。所以真实实现是分块:块内按左边算,块间才用右边(见 §7.4)。
出处与口径

装置偷自 Google Research《Rethinking Attention with Performers》(2020-10):括号画成彩色虚线框、矩阵按真实比例画

「mask 才是挡住结合律的那个东西」出自 Hailey Schoelkopf 《Linear Attention Fundamentals》;「结合律是张量收缩顺序的特例」出自 Mamba-2 (SSD) 博客 Part II

⚠️ 图里 8×3 的格数是示意;真实量级是句长 128K、头维 128

线性注意力 = 一块固定大小的记事板 板子就那么大 —— 三代的差别,全在「写之前擦不擦、擦多少」 不擦 先擦再写 选择性地擦 ① 同一块板子,三种写法 现场那个比喻,画出来 ① 疯狂往里写 后写的盖住先写的 糊成一团 S ← S + v kᵀ ② 先叉掉,再写 写之前,把这一栏擦干净 干净 S ← S(I − β k kᵀ) + β v kᵀ ③ 选择性地叉 擦多少、擦哪几栏,学出来 GDN / KDA 这一支 再加一个学出来的门 α ② 「叉掉」这个动作,在式子里就是这一块 把比喻钉到代数上 S ← S ( I − β k kᵀ ) + β v kᵀ 这一块就是「叉掉」 在 k 这个方向上,按比例把旧的擦掉 它还有另一个读法 「板子上现在能取出什么」减「本来该取出什么」, 按这个去改板子 —— 这就是一步梯度下降 于是状态不再是一块缓存, 而是一个边跑边被训练的小模型 前提只有两条:学习率取 β、损失是瞬时的 ②b 「叉掉」叉的是一个方向,不是一整块 沿 k 方向压扁;所有垂直于 k 的方向,一动不动 灰平面 = 所有跟 k 无关的方向 k S β = 0 什么都没擦 k S β = 0.5 这个方向压掉一半 k S β = 1 这个方向擦干净了 k S β > 1 压过头,翻到平面另一侧 一张图三件事 ① 叉的是一个方向,不是一整块 · ② β 是「叉多狠」的连续旋钮,不是开关 ③ 也顺便解释了 k 为什么一定要归一化 —— 不归一化,这个「压扁」就不受控。 ③ 板子为什么会「装不下」—— 它不是条数超了 装的不是 token,是「键到值」的对应关系 板子上有 d 个「方向」,你要往里记 L 条对应关系 d 个方向 L 条要记的对应关系 L 超过 d 之后, 总有两条指到同一个方向上 —— 它们就开始互相盖。 所以别把板子想成一个盒子 它装的不是 token 是「按这个 key,该取出那个 value」 这样的对应关系 「装不下」= 方向不够用了,不是条数超了 这个比喻好在哪:它把三代的差别缩到了一个动作上 三代读的方式几乎没变,全部差别都在「写之前擦不擦、擦多少」:不擦 → 按固定方向擦 → 学着擦 所以看到这一支的任何一个新名字,只要问一句:它的「擦」是怎么决定的?—— 剩下的部分,三代之间几乎没变。
⭐⭐ 三种写法就是这一支的三代:疯狂往里写(纯加,L > d 必撞车)→ 先把这个方向上的旧内容叉掉再写(delta rule)→ 选择性地叉(门控)。那个 (I − β k kᵀ) 就是「叉掉」。再换个角度看,它等价于对 ½‖Sk−v‖² 做一步 SGD —— 状态不再是缓存,是一个边跑边被训练的小模型。
出处与口径

递推式、key collision(L > d)、delta rule = Widrow-Hoff、以及「等价于对 ½‖Sk−v‖² 做一步 SGD」,均出自 DeltaNet 论文 Yang 等 arXiv 2406.06484 §2.1–2.2

⚠️ 该文 §6 那句「表达力与并行度之间存在根本权衡」说的是 Recurrent DeltaNet / mesa-layer 那一批比 delta 更强的模型,不是 delta 对纯加法,而且原文是带引用的 suggests

⚠️ 「记事板 / 擦」是现场给的比喻,不是论文措辞

7.0 一块固定大小的记事板

⭐ 它换的东西比前两个旋钮都大: 旋钮①②改的是那张 n×n 的表怎么存、怎么读,表本身一直都在; 旋钮③ 是第一次把那张表整个拿掉,换成一块固定大小的板子。 —— 所以它是唯一一个让 S 从张量形状里消失的旋钮, 也是唯一一个换了数学的。

⭐⭐ §1.4 认的那个主角,在这一节退场了。 前两个旋钮一直在跟 S 讨价还价 —— 少存一点、少读一点; 这一个直接把它请出了张量形状。代价你马上会看到: 它换来的是另一个模型,不是一个更快的注意力。

7.0b 把那个「擦」的动作拆开看 —— 查 · 擦 · 写

上面那块记事板有三种写法,区别都在擦不擦、怎么擦这一小节就把那个动作拆成三步来看 ——  苏剑林给了它最好的中文名字:除旧迎新

查 · 擦 · 写 —— 三个动作,三代模型各缺一个 线性注意力把历史压进一块固定大小的板子。板子会写满,这不奇怪 —— 奇怪的是:为什么改进了这么多代,改的一直是「怎么擦」?这张图把那个擦的动作拆开来看。 查:按地址找 擦:只掏空这一格 写:放新的进去 ① 把 delta rule 拆成三个动作 —— 除旧迎新 S ← (I − βkkᵀ)·S + βkvᵀ 1 拿地址 k₃ 去找那一格,读出里面现在装的是什么 地址 k₁ 地址 k₂ 地址 k₃ 地址 k₄ 地址 k₅ 读出 v_old = S·k₃ 2 只把这一格掏空。 旁边四格纹丝不动 地址 k₁ 地址 k₂ 地址 k₃ 地址 k₄ 地址 k₅ (I − βk₃k₃ᵀ) 3 把新的内容放进同一格 地址 k₁ 地址 k₂ 地址 k₃ 地址 k₄ 地址 k₅ + βk₃vᵀ(新内容) ② 三代模型的区别,就是这三个动作会几个 改的一直是「怎么擦」 纯线性注意力 只会「写」 只加不减。新的直接摞在旧的上面 —— 板子越写越花 遗忘门(标量 α) 会「整体变淡」,不会定点擦 每来一步,所有格子一起按同一个比例变淡。腾得出地方,但腾的是全部人的地方。 delta rule 会「定点擦」,不会整体淡 只擦你指名的那一格,别的一点不碰。改得准,但没人主动腾地方。 门控 delta(GDN) 两个都会 先整体淡一点,再定点擦一格。这就是 Gated DeltaNet。 KDA(逐通道门) 整体淡,但每格淡得不一样快 调光器从一个总开关变成每格一个 这一支的改进史,一句话就能收住:decay 会忘但不会改,delta rule 会改但不会忘 —— 所以自然的下一步就是两个拼起来 ③ 「逐通道」到底是什么意思 —— 整屋一个调光开关,还是每个灯泡一个 KDA 相对 GDN 只改了这一处 一个总开关 标量 α —— Gated DeltaNet 百分比 = 一步之后还剩多少 55% 55% 55% 55% 55% 五格淡得一模一样 每个灯泡一个旋钮 向量 α —— KDA 百分比 = 一步之后还剩多少 85% 38% 12% 70% 28% 五格各淡各的 —— 有的留得久,有的一步就没 这十个百分比是示意值,不是实测 —— 逐通道的衰减率没有公开数据。这一格要画的只是「五格一个样」和「五格各不一样」这个结构差别 🏠 为什么要分开调?「你现在在写哪门编程语言」这条该留很久;「刚离开的那个函数里的变量名」可以马上忘掉。—— 一个总开关做不到这件事。 为什么固定大小的板子一定会坏 —— 一软一硬两句 硬的那句(可以验算):d 维空间里最多只能有 d 个互相正交的方向,板子一满,新记录就只能挤在别人旁边这一条上一张图已经画出来了 —— 记事板那张的第 ③ 格:d 根 方向、L 条要记的对应关系,L 一超过 d,就总有两条指到同一个方向上。(把它放宽成「差不多不撞」之后还能装多少,在「主线 L300 · 完整版」。) 软的那句(会被记住)「记忆的敌人不是时间,是别的记忆。」—— 你忘掉一个电话号码,不是因为时间久,是因为你又记了新的。
⭐⭐ ②那五行只看颜色就够:格子全满=只会写,五格一起淡=只会忘,单格掏空=只会改,又淡又掏=两个都会五格淡得不一样快=逐通道⚠️ 抽屉是离散的,真实地址是连续方向 —— 所以「定点擦」其实是个近似,落点带里说破了。
出处与口径

delta rule 的 (I − βkkᵀ) 与「β∈(0,1] 是部分擦除、不是完整反射」出自 DeltaNet(arXiv 2406.06484);逐通道遗忘门出自 Kimi Linear 的 KDA(arXiv 2510.26692

⚠️ 图里的比喻都不是本课原创,逐条记明:「除旧迎新」—— 苏剑林 kexue.fm/archives/11033;「手术刀橡皮 + 高压水枪」—— Towards AI《Gated DeltaNet: The Surgical Eraser》;「整屋一个调光开关 vs 每个灯泡一个调光器」—— Amit Kapoor《Inside Kimi K3》;「记忆的敌人不是时间,是别的记忆」—— Eagleman《Livewired》,经 Songlin Yang 的 DeltaNet 博客引用

⭐ 抽屉里的颜色深浅是示意,不对应任何模型的实测门控值

一句话收住这一支的全部改进史

decay 会忘但不会改,delta rule 会改但不会忘。 —— 所以自然的下一步就是两个拼起来,那就是 Gated DeltaNet; 而 KDA 只在它之上改了一处:调光器从一个总开关变成每格一个

🏠 为什么要分开调?「你现在在写哪门编程语言」这条该留很久; 「刚离开的那个函数里的变量名」可以马上忘掉一个总开关做不到这件事。

⚠️ 抽屉那个画面有一处不诚实,这里说破

真实的「地址」不是一格一格的抽屉,是连续的方向;擦也是按比例擦 (β 决定擦多干净),而且会顺带擦到相近的地址

⭐⭐ 这正好回指 §1.3b:那里算过,128 维里塞一万个方向, 最挤的一对还差 60 度 —— 「差不多不像」的代价,在这里就变成「擦串了」所以「定点擦」是个近似,不是真的只动一格。板子越满,擦得越串。

📌 为什么固定大小的板子一定会坏 —— 一软一硬两句

硬的那句(可以验算)d 维空间里最多只能有 d 个互相正交的方向。板子一满,新记录就只能挤在别人旁边。
软的那句(会被记住)「记忆的敌人不是时间,是别的记忆。」 —— 你忘掉一个电话号码,不是因为时间久,是因为你又记了新的。

⭐ 一软一硬配在一起,比任何一句单独说都有用: 诗给画面,数给它一个可以验算的身体。 (后一句出自 Eagleman《Livewired》,经 Songlin Yang 的 DeltaNet 博客引用。)

7.1 基本换法(上面那句「读形状就够了」的推导版)

📌 这一节的记号约定(⭐ 2026-09-13 补 ——  原先三处朝向不一致,数学背景的读者第一眼就卡住): 状态 S ∈ ℝ^(d_v × d_k);转移矩阵 A_t 一律右乘S_t = S_{t-1} · A_t + v_t k_tᵀ);读出写作 S_t · q_tA_t 是 d_k×d_k —— 只有右乘,维度才对得上。

把 softmax 去掉(换成某个可分解的核函数),求和就可以重排:

softmax 版: out_t = Σ_{s≤t} softmax(q_t·k_s) v_s      ← 必须留下所有 (k_s, v_s)
线性版:     S_t   = S_{t-1} + v_t k_tᵀ                ← 一个固定大小的状态
            out_t = S_t · q_t

于是:

代价说死:状态大小固定 → 信息必然有损。 序列越长,往同一个矩阵里塞的东西越多,长程精确检索("第 30 万字提到的那个电话号码") 会力不从心。这不是实现问题,是这个换法的性质。

7.2 + 7.2b A_t 的形状决定了一切

⭐ 一条读法: 看到一个新的线性注意力,先把它的 A 写出来,再问「这个形状还能不能分块并行」。 两个问题的答案一配对,你就知道它会不会活下来 ——  表达力和可算性是一起设计的,不是先设计再优化。

⚠️ 两处容易记错的出处,讲的时候要说对:

  • delta rule 不是 2024 年的东西,是 Schlag 等 2021 年那篇 Linear Transformers Are Secretly Fast Weight Programmers (可上溯到 1990 年代的 fast weight programmer)。 2024 年那篇(arXiv 2406.06484)做的是把它并行化 ——  这是另一件事,而且是很关键的一件事。
  • KDA 是 2025 年 10 月的 Kimi Linear,不是 2026 年。

⭐ 图上最后那两格为什么值得单独说一句: KDA 把逐通道的门做成了一个特殊的 DPLR(对角 + 低秩)形式 ——  正是因为这个特殊形式,才配得出一个比通用 DPLR 便宜得多的分块并行算法。 它不是「先设计一个强的,再去优化」,是一边看着能不能算得动,一边设计

7.2c 对偶:同一个东西的两种读法

上面那张图的落点里藏了一个没解释过的词: 「正因为退了,才证得出跟线性注意力的对偶」。 对偶是什么?这一格补上 ——  它不是个术语,是个读论文的技巧。

对偶:同一个东西的两种读法 本讲说过一次「Mamba-2 退回去才证得出对偶」—— 对偶到底是什么,这一格补上 递推读法 矩阵读法 换 L 就换架构 ① 同一个计算,两种写法 不是近似,是逐元素相等 —— 这个等号由脚本当场验过 读法甲 · 一步一个状态(递推那条) S1 S2 S3 S4 S5 S6 S_t = S_{t-1} · A_t + v_t k_tᵀ,读出 out_t = S_t · q_t 一次只动一个状态,必须按顺序走 代价 O(L · d²)(本例 54 次乘加)—— 跟句长成正比。 不是近似 读法乙 · 整条序列一次算完 M = (Q Kᵀ) ∘ L Y = M V 整张摆出来,一次矩阵乘 代价 O(L² · d)(本例 108 次) Mamba-2 作者的原话(逐字):把那个矩阵摆出来、做一次二次型乘法,「is exactly the same as quadratic masked kernel attention」。 不是像,是同一个。两边的代价却差得很远:递推 O(L·d²) 随句长线性涨,矩阵 O(L²·d) 随句长平方涨 —— 同一个结果,两种算法 ② 那张 L 换一张,就换一个架构 M =(Q Kᵀ)∘ L —— 变的只有 L 全 1 下三角 L ≡ 1,一点不衰减 线性注意力 标量指数衰减 L = γ^(t−s),全局一个 γ RetNet 每步一个 a_t 连乘 1-semiseparable Mamba-2 / SSD 每个通道一把门 不是一张 L,是每维一张 GLA / GDN / KDA 本课按同一框架的推广 前三张是 Mamba-2 那张 SMA 图明确列出的对应(causal mask → 线性注意力、decay mask → RetNet、1-semiseparable → SSD); 第四张是本课按同一框架的推广,原图里没有这一格 —— 它的 L 不再是一个标量表,而是每个通道各有一张 ③ 这个对偶换来的不是美感,是 Tensor Core 顺便先记一个数,讲分块那张图要用到它 为什么 Mamba-2 要往回退 A_t 越花哨,表达力越强,但那张 L 就越难被高效地乘 Mamba-2 主动退回最简的 a·I(一步一个标量)—— 退了之后 L 变成 1-semiseparable,两条路都能走: 块内走矩阵读法吃 Tensor Core,块间走递推读法省内存 这就是「表达力和可算性是一起设计的」那句话的 具体样子 —— 不是先设计一个强的,再去优化。 先记一个数:这张 L 能被压多扁 半可分矩阵的定义是 —— 对角线及以下的任意子矩阵,秩不超过 N 这个 N 就是状态维度 换句话说:L 里任何一块「不跨对角线」的子矩阵,都能被 N 维压住 讲分块那张图会把「块间那一整块」画成一根竖条—— 那根竖条的 高度就是这里的 N。「块间能被压掉」和「状态有多大」是同一件事 一句话记住 递推读法和矩阵读法是同一个计算,差别只在先算哪一步 —— Mamba-2 管这个叫 “a different contraction ordering”,也就是本讲反复出现的「换括号」 M =(Q Kᵀ)∘ L 这个骨架下,你换的从来只有 L:全 1 是线性注意力、γ^(t−s) 是 RetNet、每步一个 a_t 是 SSD、每通道一把门是 GLA 那一支。 两处别讲过头 ① 这个等号只对「A_t 是标量乘单位阵」这一支严格成立(本图验的就是这一支)。A_t 一旦是一般矩阵,L 就不再是一张标量表,对偶还在,但那张 L 要按通道展开 ② 「换 L 就换架构」是一个整理框架,不是说这些模型都是从这个框架推出来的 —— RetNet、GLA 都比 Mamba-2 的这套说法更早,是框架回头把它们收进来的
⭐⭐ 递推读法和矩阵读法是同一个计算,差别只在先算哪一步。Mamba-2 管这个叫 “a different contraction ordering” —— 也就是本讲反复出现的「换括号」。M =(Q Kᵀ)∘ L 这个骨架下,你换的从来只有 L图里那个等号是脚本用 numpy 当场验的 —— 两条路算出来逐元素相等,不是打比方。
出处与口径

📌 三句逐字引文均核自 arXiv 2405.21060 的 e-print 源码:半可分矩阵的定义(「every submatrix contained in the lower triangular portion ... has rank at most N」)、Figure 2 caption 里的「equal to the SSM's state dimension」、以及对偶那一句「is exactly the same as quadratic masked kernel attention」。

📌 SMA 骨架 M = QK^T ∘ L 与 “a different contraction ordering” 出自同文 Figure 3 caption。⚠️ 前三张 L 的对应关系照它画;第四张(逐通道门 → GLA / GDN / KDA)是本课的推广,原图没有。

⛔ 图里那个「=」不是修辞:本脚本用 numpy 把递推读法和矩阵读法各算一遍并断言逐元素相等,还额外验了两个退化情形(a ≡ 1 → 全 1 下三角;a ≡ γ → γ^(t−s))。

⭐ 这一格真正有用的地方,是让你会读论文了: 一篇自称 SSM 的和一篇自称 linear attention 的, 很可能在讲同一件事 —— 只是一个从递推那头写,一个从矩阵那头写。 拿到新论文先问一句:它的那张 L 长什么样? 这个问题能穿过命名,直接问到结构。

⚠️ 别把这条对偶讲成「所以它们都一样」。 框架相同不代表模型相同 —— L 换一张,表达力和可算性都跟着变, 这恰恰是 7.2 那条演化线在折腾的全部内容。 对偶说的是「能不能换个顺序算」,不是「算出来的东西一样」。

7.3 ⚠️ 它不是「更快的 attention」,是另一个模型

三个旋钮里,只有旋钮 ③ 改变了模型能表达什么

7.4 分块:凭什么可以,以及硬件上怎么排

先补上前面欠下的那一步。我们说过换括号能把平方变成线性, 也说过因果 mask 会把这个换括号挡住 ——  那为什么切成块之后又能换回来

⭐ 答案不在「块小所以算得动」,而在一件把矩阵画出来就看得见的事上。

同一张矩阵里,两种括号 为什么非得分块 —— 被 mask 挡住的其实只有对角块 左括号 · 二次型 右括号 · 状态 被 mask ① 不分块的时候,括号是焊死的 同一个乘法的两种算法,右边那种这里用不了 A = (Q Kᵀ) ⊙ M 12 × 12 行 = 第几个 token 在问 · 列 = 它读到谁 左括号:先造 A,再乘 V (Q Kᵀ) ⊙ M 这一步必须把整张 12×12 摆出来 —— 句子长一倍,这张表就大四倍。这就是二次 右括号:先把 Kᵀ V 攒成一个状态 Q (Kᵀ V) 中间那块只有 d × d, 跟句子多长完全无关 —— 这就是线性。 但这里插不进去 ⊙ M 夹在 Q Kᵀ 和 V 中间 ——  括号一挪,逐元素乘就无处安放。 被挡住的到底是什么 Mamba-2 作者自己的说法(逐字): 「once the L mask is incorporated ..., we can no longer directly apply matrix associativity!」 但同一段紧接着还有一句,必须一起讲 被挡住的是「结合律」这个特例 不是「重排」本身 —— 换成更一般的 张量缩并顺序,mask 是能被吸收进去的。 这正是分块能成立的理由,下一格就是。 ② 切成块之后,同一张矩阵里出现两种括号 注意「有没有画出格子」本身就是编码 同一张 12 × 12,只是切成 3 × 3 个块(C = 4) 画成一格一格 = 真的摆出来了 · 画成一整块 = 压成状态 S1 S2 前面攒下的状态 只有 2 个 对角块 → 左括号 (Q_r K_rᵀ ⊙ M) V_r —— 块内老实算 块里的 token 前缀长度不同,mask 必须留着 代价只有 C × C,跟整句多长无关。 块间 → 右括号 Q_r S_r —— 一次乘法顶掉一整块 这些块里根本没有 mask:chunk r 的 每个 token 都能读前面 chunk 的每个 token。 没有 mask,结合律在这儿从来没被挡过 所以那根竖条是什么意思 整块 16 个格的内容,被压进一个 d × d 的 S —— 它从来没被摆出来过 ③ 把句子拉长一倍,涨的是哪一部分 每个数都是这张图自己数出来的 句长 12 (C 仍然是 4,共 3 块) 因果矩阵里有效的格子 78 格 其中落在对角块里、要老实算的 30 格 其余被状态顶掉48 格 占比:老实算的只有 38% —— 剩下 62% 全部收进 2 个 S 里 句长 24 (C 仍然是 4,共 6 块) 因果矩阵里有效的格子 300 格 其中落在对角块里、要老实算的 60 格 其余被状态顶掉240 格 占比:老实算的只有 20% —— 剩下 80% 全部收进 5 个 S 里 这张图的落点:被 mask 挡住的只有对角块 句长翻倍,整张表的有效格子涨了 3.85 倍(78 → 300)—— 这就是二次;而真正要一格一格算的只涨了 2 倍(30 → 60),正好线性 原因不是「块小所以算得动」,是块间那些块里根本没有 mask —— 结合律在那儿从来没被挡住过,所以它们能被压成一个与句长无关的状态。 三处别讲过头 ① 被 mask 挡住的是「结合律」这个特例,不是「重排」本身 —— Mamba-2 作者明确写了:换成更一般的张量缩并顺序,mask 是能被吸收进去的。图里画的是通常实现走的那条路 ,不是数学上的唯一解。 ② 对角块那 38% 不会随句长消失,它只是从平方变成线性。想再压,只能调小 C —— 而 C 被片上内存顶死,那是下一张图的事。
⭐⭐ 同一张矩阵画了两遍:不分块时右括号插不进去,切成块之后两种括号同框。关键不是「块小所以算得动」—— 是块间那些块里根本没有 mask,结合律在那儿从来没被挡住过。⚠️ 被挡住的是「结合律」这个特例,不是「重排」本身:换成更一般的张量缩并顺序,mask 是能被吸收进去的。分块是通常实现走的路,不是数学上的唯一解。
出处与口径

📌 「结合律被挡住/但张量缩并仍可吸收 mask」出自 Mamba-2 作者博客 goombalab.github.io/blog/2024/mamba2-part2-theory,逐字核对。

📌 「块间不需要 mask,因为 chunk r 的每个 token 都能读前面 chunk 的每个 token」出自 snowchord.com/blog/linear-attention-visualized,逐字核对。

📌 「块内用左乘法、块间用右乘法」出自 Lightning Attention-2(arXiv 2401.04658)原文 intra-blocks employing the left product and inter-blocks utilizing the right product。

📌 画法上借了四处:块色=主角张量色、mask 用纹理不用颜色(snowchord);块间一根竖条表示低秩(rudrite research);右侧状态串珠(Songlin Yang 讲座 slides);两级分块与 tensor core 口径(GLA, arXiv 2312.06635 Figure 3)。⛔ 图里的格子数全部是本脚本当场数的,不是引来的。

⭐⭐ 这张图真正的落点,是一句反直觉的话: 被 mask 挡住的,从来只有对角块。

块间那些块里根本没有 mask ——  第 r 块里的每个 token,都能读前面每一块里的每个 token,没有谁被挡。 既然那里从来没有逐元素乘挡路,结合律在那儿也就从来没被挡住过, 自然可以把整块压成一个与句长无关的状态。

⭐ 这也解释了图上那笔账:句长翻倍,整张表的有效格子涨 3.85 倍, 而真正要一格一格算的只涨 2 倍 —— 正好线性。 对角块那一部分不会随句长消失,它只是从平方变成了线性。

⚠️ 一处别讲过头。Mamba-2 的作者在同一段里紧接着写了: 被挡住的是「结合律」这个特例,不是「重排」本身 ——  结合律只是张量缩并顺序的一个特例,换成更一般的缩并顺序, mask 是能被吸收进去的。

所以准确的说法是:分块是通常实现走的那条路,不是数学上的唯一解。 —— ⭐ 这跟本讲反复出现的那条纪律是同一件事: 「做不到」和「这条路上做不到」是两句话。

知道了凭什么能分,剩下的就是硬件上怎么排

串行的状态,怎么榨出并行度 把它想成排队办事:一个窗口一个一个来,还是分批办 一个一个来 分批办 一批放多少人 ① 逐 token 跑 —— 一个窗口,一个一个来 这就是递推本身 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 每一步都要等上一步的结果 一百万个 token = 一百万步,一步都不能并 加速器最怕这个 —— 几千个算力单元,一次只喂得上一个 ② chunkwise —— 分批办:一批同时办,只把交接单传下去 块内并行,块间串行 第 1 批 1 2 3 4 5 6 这一批同时办 交接单 第 2 批 7 8 9 10 11 12 这一批同时办 第 3 批 13 14 15 16 17 18 这一批同时办 第 4 批 19 20 21 22 23 24 这一批同时办 交接单上只有一样东西:那块板子现在的样子(状态 S) 所以并行度从 1 变成 6,串行步数从 24 变成 4 —— 数学一个字没改,改的是算的顺序。 而且每一张交接单都一样大 —— 跟这一批有几个人、整句话有多长,都没关系。这才是它敢这么切的全部理由。 S d_k × d_v ← 这个尺寸 从头到尾不变 ②b 块内到底怎么算 —— 同一个和式,按「块外 / 块内」劈成两截 这不是近似 —— 两截加起来,跟一个一个跑出来的结果一模一样 第一截 块 前面所有块,早压成了一张交接单 这一块里每个 query,拿它去问那张单子就行 —— 前面有多少个 token,跟这一步的代价无关。 Q(C×d) × S(d×d) → 一次矩阵乘 第二截 块 只剩「本块里、我前面的那几个」 这一截躲不掉,但它只有 C 个人 —— 于是它退化成一个 C×C 的小全注意力。 Tril(Q Kᵀ)(C×C) × V(C×d) → 又一次矩阵乘 + 然后在块末更新一次交接单:S ← S + Kᵀ V —— 还是一次矩阵乘 三件事全是矩阵乘 —— 这正是 Tensor Core / MXU 要的形状 为什么这一改就快了 —— 答案在第一章那句话里 第一章说过:RNN 在硬件上的病不是算不动,是每一步都只是「一个维度等于 1 的矩阵乘」 —— 几千个算力单元,一次只喂得上一个。 chunkwise 做的事,一句话:把那个 1 变成了 C。串行步数从 L 步降到 L÷C 步,而每一步都变成了一把胖矩阵乘。 论文原话是「块间递归、块内并行,以最大化矩阵乘吞吐 ,从而吃满 Tensor Core」。 ②c 可 GDN / KDA 还多一步 —— 因为块内那些人互相有依赖 这一步就是那几家「自研 kernel」真正在解的东西 朴素线性注意力:块内大家各写各的 每个 token 只是往板子上一张卡片, 谁也不看别人写了什么。 所以块内直接能并行 就是上面那个 C×C 的小注意力。 delta rule:后面的人要看前面的人擦了什么 第 r 个 token 擦掉的东西,会改变第 r+1 个读到的 —— 块内不能直接并行 解法:把块内这一串依赖折成一个 C×C 的下三角矩阵 求一次它的逆 —— 整块就能一次算完。 论文说这个逆用高斯消元的逐行前代算,很便宜 所以那句「自研 kernel」,具体是在自研什么 Kimi 那篇说它做了一个 DPLR 的「特化版本」 —— 特化的地方就在这儿:通用写法要解的那个三角系统更贵,而 delta rule 这个特例可以写得更省 所以「线性注意力更快」这句话是有前提的:它快在有人替它把块内那一串依赖解开了没有这个 kernel,它在硬件上还是那条又细又长的链。 ③ 那 C 到底是什么 —— 它是一条轴,而两头都是你已经认识的东西 批越宽,交接单越少 —— 两端各少掉一样 C=1 24 批 · 23 张交接单 ← 就是①那个一个窗口 C=3 8 批 · 7 张交接单 C=6 4 批 · 3 张交接单 ← 就是②画的那一行 C=24 1 批 · 0 张交接单 ← 一张交接单都不用 C 越大 所以 chunkwise 不是第三种算法,是连接那两端的一个旋钮 —— 往上拧回 ①(一步一个,喂不饱算力),往下拧到底就是一整张矩阵算完。 右端是线性注意力的并行形式(L×L 下三角一次算完),不是 softmax 注意力 —— 整条轴从头到尾没有 softmax,「chunk 开满就变回 Transformer」是错的。 ④ 一批该放多少人 —— 这是个纯硬件问题 两头都被夹住 太少 柜台空着 一批 2 个人,几千个算力单元只用上几个 刚好 柜台坐满,大厅站得下 这就是要找的那个 C 太多 大厅站不下 块内的中间结果塞不进片上内存,被迫往外倒 这一张真正的落点:它不是「更快的注意力」,是一次改算法顺序 chunkwise 数学上跟逐 token 递推等价( 数值上不完全等价 —— 求和顺序变了,舍入就变了)。它改的只有一件事:什么时候算什么 而这恰好是这门课的主线又一次出现:一个数学上无所谓的选择,在硬件上决定生死 —— 不分块,线性注意力根本喂不饱加速器,再省显存也没用。 块大小这件事,专题一已经证过一次 C 被片上内存顶死,跟专题一 splash attention 的块大小是同一类问题 —— 而那一讲已经证过:块大小看的是比例,不是绝对值,换一代硬件就得重调。 所以别去记「C 取多少」这个数 —— 记「它被什么夹住」:下面是算力吃不满,上面是片上内存放不下。
⭐⭐ 第三个旋钮本来是为了省 —— 把平方降成线性;结果先丢掉的是并行度。解法是块内并行、块间串行:并行度从 1 变成 C,串行步数从 L 变成 L/C,而且数学一点没改。块长两头被夹住 —— 小了算力吃不满,大了放不进片上内存。
出处与口径

chunkwise 的形式与并行度 / 串行步数的改变出自 DeltaNet 并行化那篇 Yang 等 arXiv 2406.06484(WY 表示 + 分块)

⚠️ 图里的 24 个 token / 每块 6 个是示意,真实实现的块长在几十到几百之间,随硬件变

⚠️ 「排队办事 / 交接单」是本课的比喻

⭐ 这一条可以迁移出去: 块大小被片上内存顶死,这跟本讲 §3.5 里 splash attention 的块大小是同一类问题 ——  而那一节已经证过一次:最优块是个绝对值,不随序列长度缩放 (seq 从 4096 拉到 16384,最优块都是 2048)。换 seq 不用重扫,换硬件才要。


第 八 节

元旋钮 ⊕:混合

混合 —— 几个普通员工,配一个资深 便宜的层 = 只看手边记事板的普通员工;贵的层 = 能翻全部档案的资深 普通员工 资深 两头都不好 ② 格:线性混合(旋钮③那一支) ② 格:滑窗 / 局部混合(旋钮②那一支) ① 为什么不能只用一种人 两头都试过,两头都不好 全是资深 什么都查得到 贵得离谱 —— 消融里它还不是最好的 全是普通员工 又快又省 一长就兜不住:板子大小是固定的 混着用 大部分人快,少数几个能查全部 今天所有人的选择 左边那个结果最反直觉:Kimi 的消融里 0:1(全是资深)反而表现不好 ——  原文只有这一句定性描述,没公开数值 如果混合只是「拿便宜的换点钱」,那全用贵的应该最好才对。它不是 —— 说明加便宜层不只是省钱 ② 那到底几个配一个 —— 各家都落在哪 横轴:便宜层 : 贵层(对数刻度)· 一个点 = 一个模型 消融建议区间 3:1 ~ 6:1 8 : 1 1 : 1 2 个 3 : 1 5 个 5 : 1 3 个 6 : 1 1 个 7 : 1 3 个 线性混合 滑窗 / 局部混合 两族实际重叠的,正好也是这一段 —— 而它不是从建议区间推出来的,是各家自己选出来的 线性混合 Kimi Linear · Kimi K3 · Qwen3.5 · GLM-5.3F · Ling-3.0-flash · Ling 2.6 · MiniMax-01 · Jamba 滑窗 / 局部混合 Gemma 2 · gpt-oss-120b · Llama 4 Scout · MiMo-V2-Flash · Gemma 3/4 · MiMo-V2.5-Pro ③ 为什么资深不用配很多 他查到的东西会往下传 普通 第 1 层 普通 第 2 层 资深 第 3 层 普通 第 4 层 普通 第 5 层 普通 第 6 层 普通 第 7 层 普通 第 8 层 他查到的结果,顺着残差流传给后面每一层 只要有几层能「查全部档案」 后面的人直接用他的结论就行 —— 不用每层自己查一遍 意外红利:全注意力层可以不加位置编码 因为夹在中间的便宜层本身带时序 这条轴上最值得说的,是「大家都在哪」 最高的那一摞在 3:1,这十四家里占五个;往右没有一家超过 7:1( 指这一批)。这就是「大家都在哪」—— 不用数,堆在那儿。 再看最左边那一摞:1:1 上那两个清一色是橙的(滑窗 / 局部)—— 线性混合最省也是从 3:1 起步,没有一家敢一比一。 一种读法是:滑窗层毕竟还是注意力,只是不看远处 ;线性换掉的东西更多,得多配几层来摊。这只是读法 —— 没有哪篇论文这么归因过。 配比是超参,别背成常识 这条轴上的「便宜层」不全是线性 —— 小米那两家是滑窗3:1 是消融出来的,不是推出来的;同一家不同规模就换配比(Ling 的 tiny 是 3:1、flash 是 5:1)。 「纯线性一定兜不住」要说准:那条 L2M 条件要求的是状态必须随长度变大 —— 而「变大」的办法不止混合一种(论文自己给的另一条是按长度整个放大模型)。 混合是工程 上选的那条,不是定理逼出来的那条。
⭐⭐ 配比是一条轴,而两头都不好。中间那格一个点就是一个模型 —— 14 个点堆起来,最高的一摞在 3:1(十四家占五家)、往右没有一家超过 7:1,都不用数;点的颜色只编码一件事:便宜的那层是线性,还是滑窗 / 局部(按 §8.4 逐家核过的旋钮分的)。最左边 1:1 那两个清一色是滑窗族 —— 做线性混合的没有一家敢一比一。纯线性那一头不是「实测不行」—— §二 那条 L2M 条件说它的状态必须随长度变大。⚠️ 但「变大」的办法不止混合一种:论文自己给的另一条是按长度整个放大模型。混合是工程上选的那条,不是定理逼出来的那条。而左端那个反直觉结果更值得讲:Kimi 的消融里 0:1(纯全注意力)反而表现不好 —— 加线性层不只是省钱。那张消融表长什么样、它又问不出什么,见 §8.2
出处与口径

配比与型号见本课那两张配比表(每一行都标了出处,多数可在公开 config 里核);系统性消融的建议区间出自 arXiv 2507.06457

「0:1 反而表现不好」出自 Kimi Linear arXiv 2510.26692 —— ⚠️ 原文只有一句定性描述,没有公开数值

NoPE 见 Kimi Linear 同文(对所有全注意力层用 NoPE)—— ⚠️ 是它先做的,K3 是沿用

⚠️ 「普通员工 / 资深」是本课的比喻

8.1 为什么混合几乎是唯一的答案

单用任何一个旋钮都有一个致命短板:

单用短板
SWA跨不了长距离
纯线性精确检索塌
纯全注意力KV 和 FLOPs 都爆

混合的逻辑很朴素:全局层负责精确长程检索,线性/窗口层负责局部与效率,各司其职。 关键在于全局层不需要很多 —— 只要有几层能做无损检索, 信息就能沿着残差流传给其余层用。

📌 「残差流」是什么:主线图上每一层都有两处「+ 残差」——  每一层不是把上一层的结果换掉,是在它上面「加一笔」。 于是从第一层到最后一层,有一条一路贯通、只被不断加料的通道, 这条通道就叫残差流。

这正是「资深不用配很多」成立的原因: 某一层全注意力查到的东西,被加进残差流之后, 后面每一层都读得到 —— 不需要每层都自己再查一遍。

8.2 配比:3:1 是怎么来的,以及它不是定律

模型配比出处
Kimi Linear27 层 = 每 4 层一个全注意力 + 末层再补一个,实际层数 KDA : MLA = 20 : 7⚠️ 循环配比是 3:1,但一除是 2.857 —— 跟下面 K3 是同一回事,别写成 3:1arXiv 2510.26692 + HF config
full_attn_layers
Kimi K393 层 = 23 × (3 KDA + 1 Gated MLA) + 1 MLA —— 循环配比 3 : 1,实际层数 69 : 24。⚠️ 别写成「69 : 24 = 3 : 1」,一除就是 2.875 —— 末层补一个不是 K3 的花样,上面 Kimi Linear 也是这么排的arXiv 2607.24653 表 1 + sec. 2.1
Ling-3.0-tinyKDA : MLA = 3 : 1模型卡
Ling-3.0-flashKDA : MLA = 5 : 1模型卡
一篇系统性消融建议区间 3:1 ~ 6:1arXiv 2507.06457

三件事要讲清楚:

  1. 3:1 是消融出来的,不是推出来的。 Kimi Linear 的消融里, 0:1(纯全注意力)反而不是最好的 —— 这个结果比"3:1 最好"更有意思: 加线性层不只是省钱,它可能还带来了别的东西
  2. 同一家不同规模就换了配比(Ling 的 tiny 3:1 / flash 5:1)—— 配比是超参,跟规模和数据有关,不要背下来当常识
  3. 区间比点值可信。 记 "3:1 到 6:1 这个量级" 就够了
「3:1 最好」这句话,底下是一张五行的表 数值全都公开了 · arXiv 2510.26692v2 Table 1 · 消融模型 16 头 16 层,同等 FLOPs 预算 论文标出的最优 全注意力层 摆不匀,进不了表 ① 表画出来长什么样 左右两张图的纵轴跨度完全相同(都是 0.30),所以斜率可以直接比 训练 PPL 9.50 9.20 9.45 0:1 9.29 1:1 9.23 3:1 9.23 7:1 9.34 15:1 3:1 和 7:1 在训练集上一模一样,都是 9.23 验证 PPL(论文据以定稿的那一条) 5.90 5.60 5.77 0:1 5.66 1:1 5.65 3:1 5.70 7:1 5.82 15:1 同样这两个配置,验证集上差 0.05 —— 训练集看不出来 排第 4 最差 纯全注意力那一头(0:1)不是最差的 —— 它排第 4,最差的是另一头的 15:1 ② 同样这五个配置,摆成 16 层看 每一列都是那个 16 层模型 · 蓝格 = 全注意力层,灰格 = 线性层 0:1 16 层全注意力 验证 PPL 5.77 1:1 8 层全注意力 验证 PPL 5.66 3:1 4 层全注意力 验证 PPL 5.65 7:1 2 层全注意力 验证 PPL 5.70 15:1 1 层全注意力 验证 PPL 5.82 4:1 5 层一组 16 摆不匀 这五个配比不是挑出来的, 是 16 除出来的。 16 层要摆得匀,每组 (r+1) 层 就必须整除 16。而 16 的约数 只有 1、2、4、8、16 —— 于是 r 只能取 0、1、3、7、15 正好就是表里那五行。 所以 4:1 从来没被试过 —— 它要 5 层一组, 摆到第 15 层就多出一层没地方放(右边那一列)。 2:1(3 层一组)同样摆不匀,所以 1:1 和 3:1 中间那一段,这张表也问不出来。 「(r+1) 必须整除 16」是对那张表做的算术观察 不是作者给的理由 —— 论文并没有解释为什么选这五个。 但结论不变:「为什么是 3 不是 4」,这张表回答不了。 ③ 那最低点到底是「3:1」,还是「4 层」 16 层里两者是同一件事 · 93 层里差 6 倍 · 公开文献没答案 同一个最低点,两种读法 读成比例:3 个线性配 1 个全注意力。→ 93 层的 K3 要配 24 层全注意力。 读成个数:一共有 4 层全注意力就够。→ 93 层的 K3 也只要 4 层 在 16 层里,这两句话完全等价 —— 16 ÷ 4 = 4,怎么读都是 4 层。 到了 93 层,两种读法差 6 倍 K3 实际保的是比例(93 层里 24 层全注意力,本课那张配比表已核)。 该保哪个,我没有找到任何公开实验回答过 —— 所有配比消融都在小模型上做的。 还有一个口径问题:这整张表是 PPL 全场五个配比,验证 PPL 从 5.65 到 5.82 —— 一共差 3% 另一篇 340M / 1.3B 的系统性消融(arXiv 2507.06457 把两个口径分开画,结论是一条平的、一条涨的 · 语言建模分 —— 各架构都挤在 0.55~0.57,几乎不受影响; · 召回(RULER)—— 从纯线性的 0.1~0.35 一路涨到全 注意力基线约 0.42,多数架构在 3:1 追平或超过 它给这个结论起的标题是: 「决定配比的是召回,不是困惑度」 这张表能回答什么,不能回答什么 :3:1 在这五个候选里最好,而且两头都比它差 —— 包括纯全注意力那一头。 不能:为什么是 3 不是 4。4:1 在 16 层里摆不匀,从来没进过候选。
⭐⭐ 「3:1 最好」底下是一张五行的表,数值全都公开了 —— 而把它画出来之后,多出来三件原文那句定性描述看不到的事。一:纯全注意力那一头(0:1)排第四,不是最差;最差的是另一头的 15:1。二:3:1 和 7:1 的训练 PPL 完全相同(都是 9.23),要到验证集上才分得开。⭐⭐ 三:这五个配比不是挑出来的,是 16 除出来的 —— 消融模型只有 16 层,每组 (r+1) 层必须整除 16,所以 r 只能取 0 / 1 / 3 / 7 / 15。「为什么是 3 不是 4」这个问题,那张表问不出来⚠️ 「(r+1) 必须整除 16」是本课对该表做的算术观察,不是论文给的理由 —— 论文没有解释为什么选这五个。
出处与口径

📌 Kimi Linear,arXiv 2510.26692v2 §5.2 Table 1 与同段正文(「16 heads, 16 layers」「same FLOPs budget」)—— 五个配比的训练 / 验证 PPL 均逐字抄自该表。

📌 系统性消融,arXiv 2507.06457 §4.2 正文与 Figure 3 描述 —— 语言建模「大体持平、各架构都在 0.55-0.57」、召回「从 0.1-0.35 涨向全注意力基线约 0.42」两句均为原文转述。⛔ 该文摘要里的「召回近乎翻倍」本图不引:只核到正文这段描述,没有逐格核过它的 Table 6 / 7。

📌 Kimi K3 的 93 层 / 24 层全注意力见本课那张配比表(arXiv 2607.24653 表 1 + §2.1)。「(r+1) 必须整除 16」是本课对该表做的算术观察,不是论文的说法。

⭐⭐ 顺着这张图,把上面第 1 条再往前推一格。 那五个配比看起来像是精心挑的,其实是被 16 除出来的 ——  消融模型只有 16 层,配比 r:1 要摆得匀,每组 (r+1) 层就必须整除 16; 16 的约数只有 1、2、4、8、16,于是 r 只能取 0、1、3、7、15正好就是表里那五行

⛔ 所以 4:1 从来没被试过(它要 5 层一组,16 层摆到第 15 层多出一层), 2:1 也没有。「为什么是 3 不是 4」这个问题,那张表问不出来

⚠️ 这是我们对那张表做的算术观察,不是论文给的理由 ——  原文没有解释为什么选这五个配比。

⚠️ 还有一个口径问题,跟 §9.1 那条判据是同一件事。 这张消融表量的是 PPL,而全场五个配比的验证 PPL 只从 5.65 到 5.82, 一共差 3%

另一篇 340M / 1.3B 的系统性消融(arXiv 2507.06457)把两个口径分开画, 结论是一条平的、一条涨的:语言建模分各架构都挤在 0.55~0.57、几乎不受配比影响; 而召回(RULER)从纯线性的 0.1~0.35 一路涨到全注意力基线约 0.42, 多数架构在 3:1 追平或超过。它给这个结论起的标题是 「决定配比的是召回,不是困惑度」。

⭐ 换句话说:一个「最优配比」也得说清它是按哪个指标最优的 ——  跟 §9.1 说「一个倍数得说清它是哪一样的倍数」是同一条纪律。

8.2b 守恒的是比值,还是全注意力的层数

上面那张消融表只在一个深度上扫配比,所以它答不了一个更基本的问题: 配比 3:1 是超参,还是「几层全注意力」才是超参?

⛔ 这个问题有实际后果。§8.1 写过「全局层不需要很多 ——  只要有几层能做无损检索,信息就能沿残差流传给其余层用」。 这句话按字面读,预测的是绝对条数与深度无关:模型越深,比值就该越大。 它是可以被证伪的 —— 去数配置文件就行。

守恒的是比值,还是全注意力的层数 去数配置文件,不猜 —— 十四个模型的 layer_types 逐个拉下来( 跟配比点阵那张不是同一批) Qwen 系 Kimi 系 其他各家 ① 先把两个假说写清楚,它们的预测不一样 能被数据分开的问题才值得问 假说 A ·「比值守恒」 配比 3:1 是超参,模型越深,全注意力层数跟着线性涨 预测:把「层数」对「深度」画出来,是一条斜着上去的直线 假说 B ·「几层就够」 本课自己写过:「全局层不需要很多 —— 只要有几层能做 无损检索,信息就能沿残差流传给其余层。」按字面读,它预测的是 一条平的线:全注意力层数与深度无关。 ② 十四个模型的配置文件,逐个数出来(跟配比点阵那张不是同一批) 横轴 = 总层数 · 纵轴 = 全注意力层的绝对条数 0 20 40 60 80 100 0 5 10 15 20 25 总层数 全注意力层数 假说 A 预测的样子(3 : 1) 假说 B 预测的样子(平的) Kimi Linear 27 层 / 7 条 Kimi K3 93 层 / 24 条 Qwen3.5-27B 64 / 16 Qwen3.5-0.8B 24 / 6 MiniMax-Text-01 80 / 10 Granite 4.0 40 / 4 点落在哪条线上 Qwen3.5 七个尺寸(图上合成 5 个点): 深度 24 → 64,`layer_types` 逐层写死, 恰好 3:1,一次不差 Kimi 自己就是最干净的反证 27 层 7 条 → 93 层 24 条,绝对数涨 3.4 倍 比值纹丝不动。 全体 14 个点,深度与条数的相关系数 r = 0.85;只看 Kimi + Qwen 两族, r = 0.999 假说 B 被否掉了 —— 而它就是本课 本课那句话的字面读法。 ③ 但也别一刀切 —— 真实规则是两条叠加 一条随深度涨,一条是常数 两条规则叠在一起长什么样 —— Kimi Linear 的 27 层,一层不落 主体:蓝的那六个 —— 每 4 层一个。层数一多,它跟着涨 外挂:橙的那一个 —— 末层必为全局。不管多深,永远就这一个 于是 20 : 7,不是 3 : 1 —— 多出来的就是最右边那一格 (主体那条各家不同:3:1 · 7:1 · 每 10 层一个) 两条叠加,正好解释一个小数点 末层那一个额外的全局层,会把实际比值从 3.0 压下来一点 Kimi Linear 20 : 7 = 2.857 (27 层,那一个摊不开) Kimi K3 69 : 24 = 2.875 (93 层,同一个 +1 被摊得更薄) 本课那张配比表只给 K3 加了「别写成 3:1」的警告 —— 同一句对 Kimi Linear 一样成立 而且「末层补一个」不是 K3 的花样 是这一家的通用排法。 所以「全局层不需要很多」该怎么说才准 准确的意思是占比低(各家落在 1/4 到 1/10 之间),不是绝对条数少。全体 14 个点里,绝对条数从 3 到 24 差 8 倍,而比值只差 3.4 倍 —— 散得开的那个不是超参 反过来说,模型越深,你要付的全注意力层就越多 —— 混合省下的是一个固定比例,不是「越深越划算」。
⭐⭐ 把「几层就够」当成一个可证伪的预测,然后去数 14 个模型的 config.json。点全落在 3:1 那条斜线上,不落在那条平线上 —— 守恒的是比值,绝对条数随深度线性涨。⭐ 更准的说法是两条叠加:主体按比例铺,外加几个按「位置」钉死的全局层(Kimi 的「末层必为全局」)—— 后者正好解释了 2.857 和 2.875 这两个别扭的小数。
出处与口径

📌 全部数据来自各模型 HuggingFace 仓库的 config.json 逐个字段:Kimi 两款读 linear_attn_config.full_attn_layers(27 层 7 个 / 93 层 24 个);Qwen3.5 七档读 layer_types 数组;MiniMax-Text-01 读 attn_type_list 求和;Granite / Bamba 读 layer_types 与 attn_layer_indices。

📌 Qwen3-Next(interval=4)、Jamba v0.1(period=8, offset=4)、Ring 两款(layer_group_size)是按字段推算的,推导链已写在图注与脚本头里。

⛔ 两个相关系数是本脚本当场算的;「⌊每 4 层一个⌋ + 末层补一个」这条排法由 assert 对着两份 config 的实际数组核过。⚠️ 只有两个点,把它当「观察到的排法」,不要当 Kimi 公布的规则。

数完之后:那个字面读法是错的。

Qwen3.5 七个尺寸(深度 24 → 64)的 layer_types 逐层写死, 恰好 3:1 一次不差;Kimi 自己更是最干净的反证 ——  Kimi Linear 27 层 / 7 条,K3 93 层 / 24 条, 绝对数涨了 3.4 倍,比值纹丝不动

所以「不需要很多」的准确意思是占比低(各家落在 1/4 到 1/10 之间), 不是绝对条数少。反过来说:模型越深,你要付的全注意力层就越多 ——  混合省下的是一个固定比例,不是「越深越划算」。

但真实规则是两条叠加,不是单一的「按比例」:

主体按固定比例铺(3:1、7:1、每 10 层一个)—— 这部分随深度线性涨; ② 外加几个按「位置」钉死的全局层 —— 这部分是常数。

Kimi 两个模型都是「每 4 层一个 + 末层必为全局」。 正是末层那一个额外的,把实际比值从 3.0 压下来一点点: 27 层时是 2.857,93 层时被摊薄到 2.875 ——  表里那两个别扭的小数,是这么来的。

⚠️ 假说 B 也不是全无依据,但依据不在「数量」上。 Hymba 的全局层只有首 / 中 / 末三层,理论上多深都是 3 ——  但它是按位置定的,不是按数量定的,这是两回事。

⛔ 还有一个反面参照:MiniMax-M2 干脆退回了全注意力 (62 层全是 full attention)。混合不是一条只进不退的路。

8.3 ⭐ 从 Kimi Linear 到 K3 的 NoPE —— 混合带来的一个意外红利

Kimi Linear 就已经这么做了,K3 只是照搬:全注意力(Gated MLA)层完全不加位置编码(NoPE)—— 没有 RoPE,没有 YaRN,什么都没有。

NoPE —— 不是绕过挡路的那个东西,是让它根本不用存在 讲 MLA 时留了个疙瘩:它本来可以把上投影「吸收」掉,可 RoPE 往中间塞了个跟位置有关的旋转 R,把这条路挡死了,只好拆出 64 维一路专门扛它。—— 混合架构之后,那个 R 干 脆没了。 挡路的 R 线性层:天生带顺序 全注意力层:只管检索 ① 今天大多数模型:每一层都得自己知道先后,所以每一层的 K 上都得带个 R MLA 留下的那个疙瘩 q R 按位置转 W_UK c 想把它挪到 q 那边 —— 被这个 R 挡住 夹在中间的东西挪不出去 于是 MLA 只好拆成两路: 512 维 · 可以吸收 64 维 · 扛 R = 576 那 64 维不是为了存信息,是为了给 R 找个不挡路的地方待着 🏠 生活版:办公室里每个人都自己戴表对时 —— 人人都要,人人都得带着。 ② 混合架构之后:「谁负责位置」这件事换人干了 Kimi Linear 原话:delegates the entire responsibility 线性层 天生带顺序 线性层 天生带顺序 线性层 天生带顺序 全注意力 只管检索 一个循环单元(3 : 1)—— Kimi Linear 27 层,就是它重复 6 次再加 3 层 …… 这样的单元再重复 5 次 线性层本来就是一步一步往下递推的 它的衰减和门控,本身就在编码「谁先谁后」 所以位置这件事,已经有人干了 → 全注意力层就不用再编一遍 那个 R 没有理由存在了。 论文原话:「delegates the entire responsibility for encoding positional information and recency bias … to the KDA layers」 🏠 生活版:流水线本身就是按顺序走的 —— 你在第几站是自明的,表可以不戴了 ③ 那三样麻烦本来就长在 R 身上 —— 所以它一走,三个位置同时空了 三条都是原文说的 R 还在 —— q 吸不进去 q R 按位置转 W_UK c 1 吸收被挡住 2 多出 64 维 512 64 = 576 3 外推要重标定 R 没了 —— q 直接吸进去 q R W_UK c 1 2 512 = 512 3 右边那三个空槽,分别变成了: 1 吸收完全生效 没有 R 夹在中间,上投影可以整个吸进 q 那一侧。推理时 MLA 直接退化成纯 MQA。 2 那 64 维没了 576 = 512 + 64 里的 64 整个消失 —— 在这一步之上再省 11.1% 3 不用再调外推 没有位置编码,就没有外推要重标定。长上下文扩展里最烦人的一块调参,直接不存在了。 注意这不是「找到了绕过 R 的技巧」 —— 是让别人替它把活干了,于是 R 根本不用存在被绕过不存在,是两件事。 这张图真正想教的不是 NoPE,是约束之间有连接 一个看起来纯粹是效率考虑的选择(层怎么配比),解开了一个看起来完全无关的约束(位置编码挡住吸收)。 本讲这样的连接已经出现过好几次:MLA 那次是「为了保住一个代数变换,把功能拆成两路」;这里是它的反面 —— 为了不再需要那个变换,干脆换个人来提供它的前提
⭐⭐ ③ 那三条本来看着互不相干,却是同一个动作带来的。⛔ 注意措辞:这不是「绕过了 R」,是让 R 根本不用存在 —— 被绕过和不存在,是两件事。
出处与口径

四句原话均出自 Kimi Linear(arXiv 2510.26692):「we apply NoPE to all full attention (MLA) layers」·「delegates the entire responsibility for encoding positional information and recency bias … to the KDA layers」·「KDA is thus established as the primary position-aware operator」·「NoPE enables their conversion to the highly-efficient pure Multi-Query Attention (MQA) during inference」

⚠️ 图里 576 = 512 + 64 用的是 DeepSeek-V3 的形状(当尺子用,机制一样);Kimi Linear 自己那套 MLA 超参本课没有核过,别把这三个数安到它头上

⭐ K3 是照搬 Kimi Linear 这套做法,不是它先做的 —— K3 自己写的是「follows the hybrid design of Kimi Linear」

为什么敢这么做? 因为它们中间夹着的 KDA 层, 本身就是靠递归的衰减和门控在编码顺序 —— 一个天然带时序的算子。 位置信息由线性层提供,全注意力层只管检索。

两个后果,一个比一个实在(⚠️ 原先这里写「三个」—— 第三条已经在下面那个框里撤回了,数字忘了跟着改):

  1. 不用调 RoPE 外推。 模型直接外推到 1M,不需要任何位置编码的重标定 —— 长上下文扩展里最烦人的一块调参,直接消失了
  2. MLA 层在推理时可以退化成纯 MQA。 位置编码没了, §5.3 里那条"不可吸收的 64 维"也就不存在了 —— 上投影可以完全吸收

这里原先还列了第三条「KV cache 最多降 75%」—— 那一条不是 NoPE 的功劳。 75% 来自 3:1 的配比(四层里只有一层是全注意力),跟加不加位置编码无关⭐ 这个错误值得留在页面上:本节那张配比图的 ⚠️ 注早就写对了, 而正文没跟着改 —— 图改对了不等于文改对了,同一个事实有两个落点就会有两个版本。 1M 下 TPOT 从 11.48 ms 降到 1.84 ms(6.3×)这个数仍然成立,它记在配比头上。

这才是"混合"真正的意思:不是"两个方案各跑一半凑合用", 而是让每一层只做自己擅长的事,然后把别人不用做的事一并省掉。 一个架构选择(混合)解开了另一个看起来完全无关的约束(位置编码)。 这门课想教的就是这种"看见约束之间的连接"的能力。

8.4 ⭐ 各家速查:你日常在用的那些模型,注意力到底是什么

三个旋钮到这里就拆完了。这一小节反过来 —— 按公司排一遍,看每一家实际拧的是哪个旋钮。 都是能在公开 config 或官方博客里查到的,信息截至 2026-09-07

代表型号拧的是哪个旋钮配比 / 形态
阿里 千问Qwen3-Next(80B/3B)③ 线性(Gated DeltaNet)3 : 1
Qwen3.5(0.8B–397B)③ 线性3 : 1,全家族统一
月之暗面 KimiKimi Linear(48B/3B)③ 线性(KDA)3 : 1
Kimi K3(2.8T)③ 线性 + NoPE(沿用 Kimi Linear)93 层 = 23 × (3 KDA + 1 MLA) + 1 MLA
蚂蚁 百灵 LingLing 2.6③ 线性(Lightning)7 : 1
Ling-3.0-flash(124B/5.1B)③ 线性(KDA)5 : 1 = 35 KDA + 7 MLA
小米 MiMoMiMo-V2-Flash② 稀疏(SWA,窗口 128)5 : 1
MiMo-V2.5-Pro② 稀疏(SWA,窗口 128)6 : 1
DeepSeekV3.2② 稀疏(DSA)层内稀疏
V4② 稀疏(CSA + HCA)层内稀疏,按距离分层压缩
MiniMax01 → M2 → M3③ → 退回基线 → ②7 : 1 → 纯全 → 层内稀疏

这张速查表里有两个型号值得单独点名,各代表一种「不走大路」的走法

  • 腾讯 Hy3 —— 混合不是唯一解。 80 层里没有一层线性、没有一层稀疏,纯 GQA-8 做到 256K。 ⭐ 这一列不是查来的,是读我们自己仓库里那份 config 数出来的 (tpu/Hunyuan3-295B-Pretraining/)—— 全课唯一的一手反例。 然后 Hy4 一步跨到全层稀疏,整个跳过了线性这一支。
  • 智谱 GLM-5.3-Flash —— 三个旋钮可以叠着拧。 它是整张表里唯一一个「线性配稀疏」的:便宜的那层是 KDA, 而它配的那层「贵的」本身已经是稀疏的(NoPE 稀疏 MLA)。 ⭐ 而配比还是落回 3 : 1 —— 换了公司、换了搭档层的类型,区间不变。
📊 这两家的完整轨迹表 (层数、config 字段、逐版本对照 —— 想自己核的人点开;讲课时不展开)

① 腾讯混元 —— 跳过线性那一支,直接进稀疏

看哪一项Hy3(295B/21B)Hy4-preview(770B/49B)
发布preview 2026-04-23,正式版 2026-07-062026-08-28
层数8078
注意力纯 GQA-8(64 头 / 8 KV 头,head dim 128)全部 78 层都是 Gated DSAlayer_types 全为 deepseek_sparse_attention
混合没有 —— 不掺线性,不掺稀疏不是层间混合,是层内稀疏;索引器 32 头 × 128 维,top-k 2048
上下文256K1M
另外192 专家 + 1 共享,top-8256 + 1 共享 top-8;iHC(4 条残差流);gated_mla;IndexCache

② 智谱 GLM —— 半年之内走完三步,而且步步可查

版本时间注意力这一步新增了什么
GLM-5(355B–744B)2026-02-12MLA + DSA智谱第一次上稀疏
GLM-5.2(744B)2026-06-16MLA + DSA + IndexShare每四个稀疏层共用一个索引器(见 §6.5b),1M 下省 2.9× FLOPs
GLM-5.3-Flash(320B/18B)2026-08-26KDA 线性 + NoPE 稀疏 MLA⭐ GLM 家族第一次把线性和稀疏放进同一个模型;原生多模态

GLM-5.3-Flash 的 layer_types 是一个干净的四层循环:

linear, linear, linear, deepseek_sparse_attention,   ← 重复 11 次
linear                                               ← 第 45 层多出来的一层

45 层 = 34 层 KDA + 11 层稀疏 MLA        循环配比 3 : 1

8.5 ⭐ 六十秒课后题:这一讲唯一一次让你自己动手

下面三个机制,本讲都提到过但没有明确归位。 每一个只回答三问,各一个词

  1. 它在拧哪个旋钮?(①每份多大 / ②每步读多少 / ③换数学 / 都不是)
  2. 事后,还是 native?
  3. 它省的是字节,还是 FLOPs?
机制本讲在哪儿提过
ACLA —— 每 2 层共享同一份 KV44 行表里有一行
BKV 量化 —— 每个数从 16 bit 降到 8 bit§四 那个 ⚠️ 框
CIndexShare —— 每四个稀疏层共用一个索引器§6.5b
对答案 (⛔ 先自己写下九个词再点开 —— 看着答案想「我本来也这么想」是没有用的)
哪个旋钮事后 / native省字节还是 FLOPs⭐ 这题在考什么
A · CLA(少存几份)native字节 旋钮① 的第三招:MQA 砍头数、MLA 压维度、CLA 减层数 ——  三招都在回答「那份要留下来的有多大」
B · KV 量化都不是两种都有字节 这是个陷阱题,而且是§四主动说掉的那个: 三个旋钮管「存几个数、读几个数」,量化管「每个数几个 bit」—— 它正交
C · IndexShare都不是nativeFLOPs ⭐ 最难的一道:它不改「读哪些」,它省的是旋钮② 自己的开销 (索引器)—— 所以它是旋钮上的优化,不是旋钮。 ⚠️ KV 一个字节都没少

⭐⭐ 三道里有两道答案是「都不是」—— 这是故意的。 一张分类表真正的用处不是「什么都装得下」,是让装不进去的东西显形。 §4.2b 那句话反过来说一遍就是: 放不进去的,才值得你花时间。


第 九 节

代价:没有免费的午餐

一张表把所有方案摆在一起:

方案KV 显存计算量⭐ 省在哪个阶段长程质量kernel 复杂度能否给已有模型打补丁
MHA基准基准基准简单
GQA↓↓decode(省带宽)简单需微调
MLA↓↓↓↑(训练时)decode(训练前向反而更贵)不能
SWA↓↓↓↓↓两边都省↓↓↓简单勉强(要留 sink)
DSA(KV 全存,只是不读)↓↓↓prefill 为主(decode 省的是读)需专门训练阶段
CSA/HCA↓↓↓↓↓↓两边都省很高不能
线性(KDA 等)无 KV,但有固定状态↓↓↓decode(prefill 要 chunk 化才不亏)↓↓很高不能,必须从头训

9.1 把 ↓↓↓ 换成一笔能自己核的账

上面这张表每一格都是箭头。箭头能排序,但它回答不了「省的是哪一样」 —— 而那正是本节唯一想说死的那条。所以把同一批方案,换成一个能拿计算器核的问题: 吐一个字,要从 HBM 上搬多少字节?

代价 —— 把 ↓↓↓ 换成一步 decode 要搬多少字节 讲代价那一节的表里每一格都是箭头。可这门课自己的规矩是「问『省了多少』之前,先问『省的是哪一样』」 —— 箭头恰恰回答不了这个。那就把同一批方案,一路换算到毫秒 权重(所有人共享,每步一样多) 这一个人的 KV 换算成时间 ① 吐一个字,HBM 上要走的是这两样 DeepSeek-V3 · 128K · 一个用户 · batch 1 权重 34.46 GiB 每 token 激活 37B,原生 FP8 所有方案完全一样,而且所有人共享 这个人的 KV 随方案变 下面那五根柱子的差别全在这儿 每人一份,人越多、话越长,它越大 ② 一步要搬多少 —— 灰色那一段五根完全一样,差别全在红色那一段 柱高 ∝ 每步读的字节数 488.0 MHA 每步读 522.46 GiB KV 占 93.4% 要 12 张 device 30.5 GQA-8 每步读 64.96 GiB KV 占 47.0% 要 7 张 device 3.81 MQA 每步读 38.27 GiB KV 占 10.0% 要 7 张 device 8.58 MLA 每步读 43.04 GiB KV 占 19.9% 要 7 张 device 1.09 DSA 每步读 35.55 GiB KV 占 3.1% 要 7 张 device 权重 34.46 GiB 五根都顶到这条线 一个字节不差 后三根的红段在这个刻度下画不出来,只能标数字 —— 而「画不出来」本身就是 488 有多离谱的证据。 第一条读法:MHA 那一步里,93.4% 的字节是一个人的 KV —— 权重只占 6.6%。「KV cache 是瓶颈」这句话,到这儿才算画出来了。 顺手一条:MQA 比 MLA 还省(3.81 vs 8.58)—— 它从来不是慢,它是质量塌。省字节和能不能用是两回事。 ③ 除以带宽 —— 字节变毫秒,这里才出现两条反直觉的 v7 每 device 3.685 TB/s 一步要多久(下界) MHA ÷ 12 张 12.69 ms 79 tok/s GQA-8 ÷ 7 张 2.70 ms 370 tok/s MQA ÷ 7 张 1.59 ms 628 tok/s MLA ÷ 7 张 1.79 ms 558 tok/s DSA ÷ 7 张 1.48 ms 676 tok/s 0 4 8 12 ms MHA 那一根,比其余四根加起来还长(12.69 vs 7.57 ms)。 只算 HBM 读 —— 真机只会更慢,但各方案之间的比例站得住 反直觉一:显存省了 56.9 倍,只快了 7.08 倍 整条 = 显存的 56.9×;三段按对数切,所以正好铺满 —— 没有第三个原因。 7.08× ÷4.69 ÷1.71 7.08× 真正到手 —— 而右边两段合起来是 8.03×,吃掉的比到手的还多 ÷4.69 被共享的权重吃掉 —— 每步还得把 34.46 GiB 权重读一遍,这段谁都省不掉 ÷1.71 被多出来的卡吃掉 —— MHA 装不下,被迫用 12 张而不是 7 张(12÷7) 所以 MLA 真正省下的不是时间,是那 5 张卡 —— 它们可以拿去服务别人。 反直觉二:MLA 之后再上稀疏,一个人用的时候只再快 1.21 倍 因为瓶颈已经搬到权重那一段:读 34.46,KV 只剩 1.09。这不是稀疏没 用 —— 这正是上面表里「DSA:prefill 为主」的数值版 但这条只对一个人成立。KV 是每人一份的,人一多它就重新长回来:同时伺 候 32 个人,这个数是 1.55 倍,64 个人 1.98 倍 这里有个连专家都会踩的坑,值得单说:「权重是全员分摊的」是稠密模型 的直觉,V3 是 MoE,不成立。256 个专家里每个字挑 8 个 —— 人一多, 中的专家凑在一起就逼近全部:64 个人时期望激活 222 个,权重那一段自己 从 34 涨到 545 GiB。 所以稀疏确实随人数变值钱,但没那么夸张 「先问省的是哪一样」,现金价值就在这三行里 省显存 ≠ 省时间。MLA 对 MHA:显存 56.9×,单用户 decode 只有 7.08× —— 省下来的那 5 张卡才是真正的收益。 省读 ≠ 省存。DSA 那根柱子的灰段红段一个字节没少存,它只是每步不读 —— 所以它在这张图上只影响高度,不影响「要几张卡」。 这张图只成立在 batch = 1 上,这一条必须说 权重那一段是所有人分摊的,KV 那一段不摊。所以人一多,灰段被摊薄、红段成倍长 —— 画面会翻回 KV 主导 这正是那张「长度一变,主角就换人」图的另一面:那里是把人加上去让 KV 变成主角,这里是只留一个人让权重变成主角。同一个模型,问法不同,答案就不同 —— 这本身就是本 节的主题。
⭐⭐ 先只看灰段:五根柱子里它一模一样(每步都要读那 34.46 GiB 激活权重)。差别全在红段 —— 而 MHA 那根红段是灰段的 14 倍⚠️ 这是 batch = 1 的账,也是只算 HBM 读的下界;人一多,灰段被摊薄、红段不摊,画面会翻回去。
出处与口径

装置偷自 Epoch AI 那条「字节 → 毫秒 → 钱」的换算链 —— 抽象的「省了多少」一路换算到有直觉的量,每一步都能自己验算。本图走到毫秒为止(再往下换成钱要报价,那个本课核不了)

⚠️ 数全部来自本讲前面已核过的:KV 488 / 30.50 / 3.81 / 8.58 GiB(61 层 · 128K · bf16 · 一个用户)· 权重 625 GiB · 每 token 激活 37B(671B/37B,官方模型卡,见开篇那张年表)· v7 每 chip 7.37 TB/s、每 device 94.74 GiB

📌 这张图不引入任何新数,全部是前面核过的数做除法 —— 所以读者可以拿计算器逐格核,这也是它敢把那张「每一格都是箭头」的代价表替换掉的底气

三行收口,都能自己验算:

⚠️ 这笔账只成立在 batch = 1 上。 权重那一段是所有人分摊的,KV 那一段不摊 ——  人一多,前者被摊薄、后者成倍长,画面会翻回 KV 主导

⭐ 这正好是 §二那张图的另一面:那里是把人加上去, 让 KV 变成主角;这里是只留一个人,让权重变成主角。 同一个模型,问法不同,答案就不同 —— 这本身就是本节的主题。

⭐ 这张表要配着 §十那张落点图 的第二格看 —— 那里把四个最容易被低估的取舍画在了一起: 省显存 ≠ 省计算 · 训练时省 ≠ 推理时省 · 不规则访存的代价常被低估 · 收益有天花板。

⭐ 这一节唯一要在这儿说死的是提问顺序问「省了多少」之前,先问「省的是哪一样」。 这张表的每一列,就是一样不同的资源。


第 十 节

落到硬件(本专题的落点)

10.1 三种资源之间的搬家史

回到全课那条主线:每一个变体都是被硬件逼出来的,也都对硬件提出了新要求。

落点 —— 注意力的变体史,是一部在三种资源之间反复搬家的历史 搬的顺序是有道理的:先搬能算的,最后才搬算不出来的 显存 算力 访存规整度 防骗判据 ① 三个房间,一条搬家路线 顺序不是随机的 —— 先搬能拿尺子量的,最后才搬量不出来的 早期 —— 搬「显存」这个房间 东西还是那些, 每一份变小了 MQA → GQA → MLA 换来:每份更小 中期 —— 搬「算力」这个房间 格子一个没少, 这一趟只搬两个 SWA → DSA → CSA 换来:格子更少 现在 —— 搬「访存规整度」这个房间 卡住 门就那么宽 —— 只能一行行写 chunk 化的线性 换来:读得更顺 为什么偏偏是这个顺序 前两样能拿尺子量 —— 多少字节、多少 FLOPs,坐下来就能算。 而访存规整度量不出来,它只出现在 kernel 里 —— 所以被留到了最后。 ② 四个取舍,一个都别漏 每一条都是一次翻车预防 1. 省显存 ≠ 省计算 MLA 省显存却加了计算 DSA 省计算但 KV 还在那儿 2. 训练时省 ≠ 推理时省 MLA 的压缩在训练前向里不生效 NSA 的 native 意味着训练也省。 3. 不规则访存的代价常被低估 纸面 64 倍,落到 gather 和不连续访问上远拿不到 4. 收益有天花板 注意力只是账单的一部分; MoE、MLP、通信一分没省 问「省了多少」之前,先问省的是哪一样 ③ 一条防骗判据 这一讲最该带走的一句 一次前向的算力都花在哪( 算力,不是时间) 4K 平方项 投影 其余 平方项 12.3% · 注意力投影 27.4% · 其余 60.3% 128K 平方项 其余 平方项 81.8% · 注意力投影 5.7% · 其余 12.5% 1M 平方项 平方项 97.3% · 注意力投影 0.8% · 其余 1.9% 同一个机制,占比差 八倍 4K 下平方项 12%,1M 下 97%。 「省了 N 倍」在这两端根本不是同一件事 而且短上下文那 12% 之外,注意力自己的投影还占 27% 所以任何一个倍数, 必须带上「在多长的上下文下」 不带这句,那些倍数全是耍流氓。 而且要问清楚:省的是 FLOPs,还是墙钟时间? 一句话收全课:访存规整度是最难搬的那一样 显存能算、算力能算 —— 所以这两样先被搬完了;访存规整度算不出来,它只出现在 kernel 里、出现在 SM 空转的那几个微秒里。 这也是为什么今天这一支的前沿工作看起来越来越像「写 kernel」而不是「改模型」 —— FlashKDA、TileLang、专用的上下文并行,全是这一类。
⭐⭐ 注意力的变体史,是一部在显存 / 算力 / 访存规整度之间反复搬家的历史 —— 而顺序不是随机的:先搬能算的,最后才搬算不出来的。右边那格是全课最该带走的一句:注意力只是账单的一部分,任何一个倍数都必须带上「在多长的上下文下」。
出处与口径

四个取舍与硬件假设表见 §九 / §十 正文(每条都可追到前面对应小节)

③ 的三根条由本脚本当场算并断言,公式与常数同专题一那条曲线(V3:61 层 / 128 头 / MoE top-8+1 共享,因果掩码按半算)

⚠️ 它是 FLOPs 口径,不是时间 —— 同样 seq=4096,本讲实测(Hunyuan3-295B · v7 64 芯片 · XProf op stats 页)splash attention 占 23% 的时间、效率只有 35.5%;效率最低的算子,时间占比一定高于算力占比⚠️ 这是量级示意,具体占比随模型结构、批大小、序列长度变

§9.1 刚给这条判据补上了另一半: 一个倍数除了要带「在多长的上下文下」,还得说清它是哪一样的倍数

同一个 MLA:显存 56.9×单用户 decode 的时间只有 7.08×。 两个数都对,而它们差了整整八倍 ——  报哪一个,取决于你想让听的人以为你省了多少。

变体它假设了什么硬件条件条件不成立会怎样
MLA算力相对充裕、显存相对紧张算力紧张的机器上,用计算换显存这笔交易不划算
稀疏(DSA/NSA/CSA)gather 不太贵对规整访存友好的加速器反而吃亏 —— 纸面 64 倍拿不到
线性(KDA)片上内存够放下 chunk 的中间量chunk 被迫调小 → 并行度掉 → 优势被吃掉
长上下文 + MoE 同时上HBM 带宽够两边分all-to-all 与 KV cache 抢同一份带宽

10.2 ⭐⭐ 上面那张表的第二行,落到我们自己的机器上是什么样

那张表里「稀疏(DSA/NSA/CSA)假设 gather 不太贵」这一行, 在 TPU 上就是一整个工程战场。而这恰好是这门课唯一有资格讲、别人讲不了的部分 —— 所以它值得单独占两张图。

落到 TPU 上 —— 先看清楚是哪两件事对不上 中央厨房 碰上 全是临时改单的客人 中央厨房 = TPU 临时改单 = 现代注意力 最疼的一处 已经有的解法 ① 两种厨房 —— 这不是谁好谁坏 中央厨房快,正是因为它不接临时改单 TPU = 中央厨房 今日菜单 钉在墙上,改不了 一排蒸屉,整批上 1 2 3 4 5 连号货架,一趟拉走 出餐极快 —— 代价是临时改单很贵 GPU = 点单现做 临时改单 3 号桌 少辣、加一份 来什么做什么 一个灶台 一次一份 一份也做 满仓库跑腿,散落取货也认了 灵活 —— 代价是每道菜都要现开火 ② 而现代注意力,偏偏全是临时改单 三样,全是最近五年长出来的 每桌人数都不一样 一个 batch 里各请求长度不同 来源:vLLM 那套调度带来的 食材散在仓库各处 一条序列的 KV 散在不连续的页上 来源:PagedAttention 带来的 今天做哪几道菜,开工了才知道 这一步到底读哪 2048 条 来源:DSA / NSA 这一支带来的 这三样全是在 GPU 上先长出来的 —— 它们默认了一台「随手跑腿不太贵」的机器。 ③ 对上之后,最疼的是哪一处 —— 仓库取货 RPA 论文 §2 逐条点了名 连号货架:一趟推车拉走 一次搬运,地址连着 散落各处:跑很多趟 六次搬运,地址还是跑起来才算出来的 论文原话:这让DMA 调度变得困难 —— DMA 就是那台推车:它最擅长「一趟拉一整排」,最怕「这一趟拉哪几个,得先算一下」。 这句话值得原样读一遍 —— 它出自 2026 年 4 月,不是五年前 「现有 LLM 推理 kernel 和服务系统基本都是 GPU 中心的,而且还没有一套成熟的办法把 LLM 负载高效地映射到 TPU 架构上。」 —— Google 自己那篇 Ragged Paged Attention 论文的摘要(arXiv 2604.15464)。 所以这一节讲的不是「怎么调参」,是一个还在打开的工程战场 一条能带走的判据:看一个机制默认了什么样的机器 临时改单、散落取货、开工才知道做什么 —— 这三样都默认「随手跑腿不太贵」。在一家为「按批预制」优化的厨房里,这个假设不成立 所以移植的活儿不是「翻译代码」,是把那个隐含的硬件假设找出来,再换一个等价但规整的做法。下一张讲的就是这个「换法」。
⭐⭐ 先看清楚是哪两件事对不上:TPU 的三条硬约束(静态形状、tiled 粗粒度布局、偏好规整访存)对上现代注意力的三个动态性来源(ragged、分页 KV、运行时 top-k)。不摆清楚这个错配,后面那些 kernel 技巧看起来就只是一堆技巧。
出处与口径

三处疼与那句摘要出自 Ragged Paged Attention(Jiang 等,arXiv 2604.15464,2026-04)§1 与 §2.4;TPU 的三条约束亦见该文 §1

⚠️ 「中央厨房 / 点单现做 / 仓库取货」是本课的比喻 —— 论文那侧的说法是 static-first 编译、tiled 粗粒度布局、以及「从动态算出来的不连续地址 gather」

⚠️ 先把「谁是给谁设计的」说清楚,免得听成 TPU 的黑历史。 这一讲从 §四 到 §八 讲的每一个机制, 它们的第一版 kernel 全部是在 GPU 上写出来的 ——  FlashAttention、PagedAttention、NSA 的三支路、DSA 的 indexer,无一例外。 所以「搬到 TPU 上有难度」不是 TPU 的缺陷,是这批机制自带的一条硬件假设: 随手 gather 不太贵⭐ 而这条假设,正是 TPU 为了换取规整访存下的高效率而主动放弃的。

10.3 ⭐⭐ 那怎么克服 —— 三招,以及两个一定会被问到的问题

那怎么克服 —— 三招,和两个最容易被问倒的地方 共同形状:把「一个临时改单」换成「一批预制套餐」 三招 谁来算 拣货小队 还没被验证的 ① 三招 —— 都是「不现开火,改成挑一个预制的」 RPA 论文的三个做法 保温台:开工前就把几套做好摆上 全长的 全短的 混着的 来单了,挑这套 留一格 共同形状(下面三招都是它) ? 运行时才知道的一个 → 开工前就备好的几个,来了挑一个 下面三招,形状跟右上角那个一模一样 —— 不一样的只有右边那行字。 ? 1. 把盘子切小一点 强制用最小的那种餐盒 —— 长短不一的那一维,别放在切盘子的方向上 ? 2. 上菜和收盘并成一趟 decode 时那一下零碎的写,融进主菜一起做,用做菜的时间盖住它 ? 3. 按客流预制几套套餐 最像中央厨房:不做万能菜谱,做几套再挑 —— 就是上面这张图 成绩:Llama 3 8B 在 TPU7x 上 —— decode MBU 86% · prefill MFU 73% ② 那个「今天做哪几道菜」的决定,谁来算 —— 答案分两层 别答成一个字 前台 host CPU 今天 12 桌 每桌几个人 一顿饭只报一次 每道菜都跑去问? 一来一回是微秒级, 而这一步只有几十微秒 后厨(就在卡上) 1 号 2 号 3 号 4 号 5 号 这道菜的料从哪个货架 —— 自己算 每道菜都要算一次 这笔账是拿闲人付的 颠勺的 算账的 正没事干 矩阵乘忙得冒烟的时候, 标量单元正闲着 —— 地址计算是他的活 所以这一问的答案是两层:「今天有几桌」前台报,一顿饭一次;「这道菜从哪个货架拿」后厨自己算,每道菜一次。 混成一句就必错。 ③ 那 SparseCore 能不能干这个 一支专门跑腿拣货的小队 推车 最多 2048 件 规矩:这一趟最多拿几件,必须开工前就报 超了就分批,或者直接丢掉一部分 对 DSA 反而天然满足 —— k 就是 2048,定死的 架构上非常对口 · 天生干散落取货(不规则、稀疏访存) · 能按条件决定去哪儿拿 · 跨通道排序、过滤、前缀和 —— 正是 top-k 要的 但要诚实:公开的那套 TPU 生产注意力 kernel 走的是主厨这条线(TensorCore + Pallas/Mosaic), 不是拣货小队。没有公开材料说有人用它跑注意力的 top-k —— 这一条到今天仍然只是「看起来对」。 跨层共享那一支,在这台机器上比在 GPU 上更值钱 GPU 上省的是:索引器那部分算力(GLM-5.2 报 1M 下每 token 降 2.9×)。 TPU 上还额外省三样:① 「这一趟拿哪几件」只算一次,后面几层直接复用;② 几层的取货路线完全一样,推车的单子可以重用;③ 临时改单的次数本身降了四倍
⭐⭐ 三招的共同形状:把「一个动态」换成「一批静态」。而运行时那个 top-k 决定不用出卡 —— 它用的是 FlashAttention 阶段本来就闲着的标量单元。SparseCore 架构上正对口(它天生支持数据相关的控制流与访存),但公开的那套 TPU 生产注意力 kernel 用的还是 TensorCore —— 这条留在「看起来对但未被公开验证」。
出处与口径

三招、「SREG 在计算密集阶段欠用」、以及 MBU 86% / MFU 73%,均出自 Ragged Paged Attention(Jiang 等 arXiv 2604.15464)§3–§5

SparseCore 的定位与「必须声明静态上界、超了就 mini-batch 或丢 ID」出自 openxla.org 的 SparseCore 公开文档

⚠️ 「中央厨房 / 前台后厨 / 拣货小队」是本课的比喻;⚠️ 跨层共享在 TPU 上更值钱那一条是本课的推导,无公开对照实测

⭐⭐ 「能不能在卡里边完全算完、要不要发回 CPU」这一问,答案是分两层的 ——  别答成一个字。

哪一层的决定谁来算频率为什么放在这一层
批次级:这一步有哪些请求、各自多长、页表长什么样host CPU(服务框架)每步一次它本来就是调度器的产物,而且一步只算一次,摊到几千个 token 上可以忽略
token 级:这个 query 要读哪 2048 条、对应哪些 HBM 地址卡上的标量单元每 token 每层发回 host 是不可能的 —— 一次 PCIe 往返以微秒计,而这一步的预算是几十微秒

⭐ 所以准确的说法是:top-k 那个「决定」不出卡;出卡的只有本来就在 host 上的批次级元信息。 ⛔ 不要说成「全在卡上算」—— 页表是 host 给的;也不要说成「要发回 CPU」——  逐 token 的地址计算发回去一次就废了。

⭐⭐ 跨层共享那一支(§6.5b 的 IndexShare / IndexCache), 在 TPU 上比在 GPU 上更值钱 —— 这是一条本课的推导,写清楚它多省的是什么:

  • GPU 上省的是:indexer 那部分 FLOPs(GLM-5.2 报 1M 下每 token 降 2.9×,见 §6.5b
  • TPU 上还额外省三样:① 动态元信息的标量计算只做一次,后面几层直接复用; ② 几层的 gather 模式完全相同,DMA 描述符可以重用,不必每层重编一遍; ③ 动态决定的「次数」本身降了四倍

⭐ 最后那条是这一节真正想留下的判据: 在一台 static-first 的机器上,动态性的次数本身就是成本 ——  不只是每次动态有多贵。 ⚠️ 「TPU 上额外更值钱」是本课从 RPA 那篇描述的机制推出来的, 没有公开的对照实测;GLM-5.2 的 2.9× 是 FLOPs 口径、且不是在 TPU 上测的。

段尾:这一节把全部机制放回了它们出生的那台机器上 ——  你会发现同一个聪明办法,换台机器就要重新算一遍值不值

⭐⭐ 下一节只剩最后一件事:回到开场那句话, 把我们许下的那笔账算完。


第 十一 节

收尾:把谱系放回时间线

前面那些节讲的是谱系(可迁移的判断框架),这一节是时间线(记忆的挂钩)。 顺序不能反 —— 先给框架,时间线才有意义;先给时间线,框架就变成了流水账。

🖥 回到开篇那张编年史图(三条泳道那张)——  这一节不需要新画面,需要的是一个新问法

试着自己先从那条线上读出三件事,再往下看

  1. 前半段是单点突破,后半段全是组合。 2025 年之后没有哪个模型只用一招
  2. "推理期的补丁"逐年变成"训练期的架构" —— NSA 的 native、DSA 的训练阶段、 K3 的从头混合训练,是同一个趋势的三次出现
  3. 每一步都是在修上一步暴露出来的具体毛病,不是凭空发明。 所以下一步大概率也是在修今天这批方案暴露的毛病 —— 那么今天这批的毛病是什么?(留给学生,也留给下一版课件)

11.1 最后一件事:回到封面那 512 倍

开场我们说:2020 年的 GPT-3 记 2048 个 token,今天的模型记 100 万 —— 这一讲讲的就是这 512 倍 是怎么换来的。 现在把这笔账算完。

收尾 —— 封面那 512 倍,到底是怎么换来的 开场我们说:2020 年的 GPT-3 记 2048 个 token,今天的模型记 100 万,这一讲讲的就是这 512 倍是怎么换来的。现在把这笔账算完。—— 而算它的办法,正是那条提问顺序: 「省了多少」之前,先问「省的是哪一样」。 要还的:上下文涨了 512 倍 显存账 元旋钮:混合 带宽账 ① 先把要还的那笔摆出来 封面立的那个承诺 2020 2,048 GPT-3 的上下文 今天 1,048,576 前沿模型的上下文 × 512 整整 512 倍,除得尽 什么都不改的话, KV cache 也要涨 512 倍。 这就是要还的那笔。 ② 它不是一笔账,是两笔 —— 同一把尺子量,切法完全不同 这正是讲代价时那条提问顺序 两根条一样长,长度都是那 512 倍。 对数刻度 —— 段长读的是「乘几倍」,不是「占几成」;相邻两段是相乘的关系。 显存账 一个人要占多少 单位:GiB/每 token 56.9× 旋钮① MLA 把每一份压小 元旋钮 混合 3:1 2.25× 剩下的这一截(见下) 带宽账 每走一步要读多少 单位:字节/每一步 512× 旋钮② 稀疏注意力:每步固定只读 2,048 个,一整条全被它抵掉 要还的 512× 末尾露出来的那一小截红,就是六年里真正多买的硬件 —— 只有 2.25 倍。前面 227.5 倍是「把每个 token 变便宜」换来的。 这一根一点都没露 —— 上下文涨了 512 倍,每步要读的一个字节都没涨。 而它不省显存:KV 全存着,所以它不进上面那根条。 这两根条不是一本账,别加起来 —— 上面量的是「一个 token 占多少」,下面量的是「一步要读多少」。分母不一样,所以那 512 倍里不含带宽 56.9× = 488 GiB → 8.58 GiB(V3 口径,前面核过) · 4× = 四层里只有一层是全注意力 · 512× = 1,048,576 ÷ 2,048 ③ 于是「三个旋钮」这条主线,在这里收成一句话 每个旋钮还的不是同一笔 旋钮① 让每一份更小 显存账 —— 把每个 token 的开销压下去 旋钮② 每步只读一部分 带宽账 ——  它不还显存账 旋钮③ 换成固定大小的状态 两笔一起还 —— 因为它让那个 S 整个消失了 所以「先问省的是哪一样」不是一句方法论口号 —— 不分开问,这两笔账根本对不上。 那 512 倍,是这么换来的 不是内存变大了。显存这一笔,是把每个 token 的开销压了 227.5 倍 —— 整条里最长的那两段,都是「把 token 变便宜」 而末尾那 2.25 倍,要老实说清楚它是什么:它是前两段除完之后剩下的那一截,不是查来的数。本课把它叫「硬件」,是个偷懒的叫法 —— 它里面至少还混着两样本讲没拆开的 东西:KV 用更低的精度存(今天各家默认就这么干,光这一项近乎再减半),以及前缀复用
⭐⭐ 两个 512 为什么会相等:GPT-3 的上下文长度和 DSA 的 k 恰好都是 2048 —— 两条不相干的设定撞成一个数。⚠️ 这是巧合不是规律,但它让这笔账好记得多。
出处与口径

488 GiB / 8.58 GiB(61 层 · 128K · bf16 · 一个用户)· 3:1 配比 · k=2048 这四个数,本讲前面都已逐一核过并标了口径,这一张只做乘除

⭐ 两个 512 为什么会相等,说破了很简单:GPT-3 的上下文长度(2048)和 DSA 的 k(2048)恰好是同一个数 —— 两条完全不相干的设定撞成了一个数,所以「今天 ÷ 当年」和「今天 ÷ k」才算出同样的 512。⚠️ 这是巧合,不是什么规律,但它让这笔账好记得多。脚本里两个等式都断言了

那 512 倍,是这么换来的

不是内存变大了。显存这一笔,是把每个 token 的开销压了 227.5 倍 (旋钮① 的 56.9× × 混合 3:1 的 4×)——  要还 512 倍,还上了 227.5 倍,剩下那 2.25 倍,才是真正多买的硬件

也不是带宽变快了。带宽这一笔,是干脆不读了 —— 稀疏注意力每步固定只看 2048 个, 上下文涨了 512 倍,它一个都没多读。

⭐⭐ 一句话收:这六年真正变的不是机器, 是「一个 token 到底该花多少钱」这件事被重新定价了。

还有一条线,到这儿也该合上了

§1.4 我们认了一个主角 —— 那个 S, 张量形状里唯一会越变越长的一维。

之后每一节做的都是同一件事:旋钮① 让 S 前面的系数变小, 旋钮② 让每步读到的 S 变少,旋钮③ 干脆让 S 从形状里消失混合是把三个答案摆在不同的层上。

⭐⭐ 所以下次再看到一个没听过的注意力名字,你只需要问它一句: 你是在跟这个 S 讨价还价,还是打算把它请出去? —— 答得上来,它就已经在你这张地图上了。

📌 顺手看一眼这笔账是怎么算出来的 —— 它就是这门课的方法

它不是一笔账,是两笔:显存账问「一个人要占多少」,带宽账问「每走一步要读多少」。 而三个旋钮各还各的那一笔 ——  旋钮① 只还显存,旋钮② 只还带宽(它一个字节都不省显存), 旋钮③ 两笔一起还。

⭐ 所以 §九 那句「问『省了多少』之前,先问『省的是哪一样』」 不是一句方法论口号 —— 不分开问,这两笔账根本对不上。


📎 附:这个专题明确不讲什么(以及为什么) (想知道边界在哪、或者在找某个主题去了哪一讲,点开)
  • MLA 逐步的矩阵推导 → 在专题一第 2 步。 ⭐ 分工是这样定的:专题一讲"V3 这一个模型里它怎么算", 这里讲"为什么会有它、它在谱系里站哪、它换走了什么"。 这里只用一张图复述结论(576 = 512 + 64),不重讲推导 —— 重讲会占掉 5 分钟,而这 5 分钟买不到任何新东西
  • 注意力的 kernel 怎么写 → 实现细节在专题七
  • 序列并行 / Context Parallelism 怎么切专题五。 这里只说"线性注意力的 CP 跟标准 CP 不一样",不展开
  • prefill / decode 的形状差异专题六
  • 各家模型的完整参数表专题九。 这里给的每个数字都只为说明一个机制,不做横向评测
  • Mamba / SSM 那一支 —— 跟线性注意力是近亲,但它自成体系。 这门课的听众用不上,明确不讲(问到就说一句"同一个思路的另一个分支")
📚 这一讲的出处清单 (每个机制配它的一手论文;想复核任何一个数字都从这儿进)

整页所有 arXiv 编号都是可点的 —— 正文里的、表里的、连图上那些小字出处,点一下直接开论文。 (图里的链接带下划线 —— SVG 的链接不像网页那样自动变蓝, 所以特意标出来。)

⚠️ 几个不是 arXiv 的,在表里单独挂了链接: DeepSeek-V3.2-Exp 技术报告(GitHub)、NVIDIA 的 RNN 性能指南、 张量形状记号沿用的 How to Scale Your Model各家模型的 config 没挂链接 —— 它们在各自的 Hugging Face 仓库里, 版本会动,写死一个链接迟早指到改过的那一版

要什么在哪
MQA / GQAarXiv 1911.02150 / 2305.13245
MLADeepSeek-V3, arXiv 2412.19437 sec. 2.1 + 4.2(超参那段给了 n_h/d_h/d_c/d_h^R 的准确值)
Gated MLA / K3 全貌Kimi K3, arXiv 2607.24653 sec. 2.1.2、表 1(93 层 / 69 KDA + 24 MLA / 2.78T-104.2B)
SWAMistral 7B, arXiv 2310.06825(窗口 4096)
Attention sinkStreamingLLM, arXiv 2309.17453(4 个 token / 400 万 / 22.2×)
NSAarXiv 2502.11089(三支路 + 门控;64k 下 11.6× / 9.0× / 6.0×)
DSA + Lightning IndexerDeepSeek-V3.2-Exp 技术报告 sec. 1–2.1(ReLU 打分 / FP8 / k=2048 / 稠密预热阶段 / KL 对齐)—— ⚠️ 不是后来那篇 arXiv 2512.02556(《DeepSeek-V3.2》),两者节号对不上;本讲图里核的数全部来自 Exp 那份。我们有一手实测
CSA / HCADeepSeek-V4, arXiv 2606.19348 sec. 2.3 + 2.3.4(m=4 / m′=128 / top-k / 27%·10% / 2%)
线性注意力谱系2006.16236(线性)→ 2102.11174(delta rule, 2021)→ 2406.06484(可并行化)→ 2412.06464(GDN)→ 2510.26692(KDA)
混合配比2510.26692(3:1 + 消融)、Ling-3.0 模型卡(3:1 / 5:1)、2507.06457(建议 3:1~6:1)
FlashAttentionarXiv 2205.14135 + TPU 侧 Splash Attention 实测(tpu/ 下多处)
MHA 本体(§一)Vaswani et al. 2017, arXiv 1706.03762 —— sec. 3.2 / 3.2.1 / 3.2.2 / 3.2.3表 1;四条原话见下方折叠
KV cache 被点名成瓶颈Shazeer 2019, arXiv 1911.02150(MQA 那篇)—— 「memory-bandwidth cost of repeatedly loading the large keys and values tensors」
RNN 一支(§零)Elman 1990《Finding Structure in Time》;Bengio, Simard, Frasconi 1994;Hochreiter & Schmidhuber 1997;Cho et al. 2014;Bahdanau et al. 2014, arXiv 1409.0473
只有线性依赖才扫得动Martin & Cundy 2018, arXiv 1709.04057(ICLR'18)—— 实测最高 9× 加速
RNN 在硬件上为什么慢NVIDIA《Recurrent Layers User's Guide》—— 「a GEMM with one dimension of one」「can combine these GEMMs over the minibatch size, but not over different sequence steps」
我们自己的 kernel 实战Tokamax KDA kernel、tpu/ 下 DSA 相关
§2.1 那张 KV cache 对照表自己按公式推的2·n_h·d_h·L(d_c+d_h^R)·L,输入全部来自 V3 论文 sec. 4.2口径(K/V 都按 d_h=128)要在讲的时候声明
🔍 这一讲最吃劲的四条,把原话摆出来 (省得读者去翻论文对措辞)
这一讲怎么讲的论文原话
「query 问、key 挂牌、value 是货」不是我们编的比喻 「mapping a query and a set of key-value pairs to an output … the output is computed as a weighted sum of the values, where the weight assigned to each value is computed by a compatibility function of the query with the corresponding key.」(sec. 3.2)
√d_k 别只说「防止 softmax 饱和」,那是结论不是理由 「for large values of d_k, the dot products grow large in magnitude, pushing the softmax function into regions where it has extremely small gradients.」 (sec. 3.2.1)
⭐ 理由在脚注 4:q、k 各维独立、均值 0、方差 1 时, q·k 均值 0、方差 d_k —— 标准差就是 √d_k。
多头不是不够用,是会把该分开的关注平均掉 「jointly attend to information from different representation subspaces at different positions. With a single attention head, averaging inhibits this. (sec. 3.2.2;h = 8,d_k = d_v = d_model/h = 64)
§零那张「解码时又变回 RNN」图 Ⓒ 那一行不是我们的推论,是原文 「training these layers is generally fast and simple, due to parallelizability across the length of the sequence, incremental inference (where such parallelization is impossible) is often slow, due to the memory-bandwidth cost of repeatedly loading the large "keys" and "values" tensors.」(Shazeer 2019 摘要)
⭐ 2017 年造出这个形状,2019 年就有人把它命名成问题了;那篇给的解法 MQA 正是本讲模型表的第二行。

📌 记号约定§X.Y本课的小节号, sec. X.Y被引论文自己的小节号。 ⛔ 两者曾经用同一个记号,结果整体重编号时把论文的节号也改了 ——  而且改完恰好落在真实存在的本课小节上,体检全绿。


← 回 课程总纲 · 硬件背景在 专题二 · TPU 与 GPU · 量化那一支在 专题八 · 精度与量化 · 📝 讲义

本页由 Courses/tools/topic03-build-L300.py 生成 ——  正文就写在那个脚本的 BODY 里(md 只是大纲)。本目录采用 CC BY-NC-SA 4.0。