Agent 工程学习站 做棵大树 出品 主站 beatree.cn 免费 · 无需登录 · 进度存本机
I 版 · 大模型原理 ◆ 第 01 章 ◆ Attention Is All You Need
I · 大模型原理 01 / 22 入门

Transformer 与自注意力

Attention Is All You Need
预计阅读 35 分钟
难度 入门
关键词 Self-Attention · QKV
本机状态 未读

Agent 的一切都跑在注意力机制之上。这一章只做三件事:把「token 怎么变成一组数、这组数怎么流过注意力、n² 究竟出在哪一步」按顺序讲清楚——因为后面每一章的成本、延迟、上下文策略,都是这个 n² 的推论。

编辑部注 · 本版重排了讲解顺序:先用一整节把名词说清楚,再看形状与公式。凡是加了虚线底的名词都可以点开——弹出来的卡片里有含义、出现场景、例子与延伸阅读,不必离开本页。

如果要给「Harness 工程师为什么要懂模型原理」找一个最实际的理由,那就是:你所有的取舍最后都会被三个物理事实定死——注意力是平方复杂度的、KV Cache 是按前缀命中的、输出是采样出来的。这一章讲第一个,而且要从头讲起:先解释清楚 token、张量、形状、投影、打分这些名词,再去看它们怎么拼成一次前向。跳过名词直接看公式,是绝大多数人读不懂 Transformer 的真正原因。

一、为什么做工程的人要关心注意力

1.1 面试官其实在问什么

面试里被问到「Transformer 原理」,很多人以为考官在考古。其实不是。考官真正想问的是:

  • 你知不知道上下文变长,成本是怎么长的?
  • 你知不知道「把整个代码库塞进上下文」这条路会在哪一步崩掉?
  • 你知不知道长任务的瓶颈到底在算力、显存,还是在带宽?

这三个问题都从 自注意力(Self-Attention) 出发——它是 Transformer 里唯一一个「让任意两个位置互相看一眼」的机制。而「互相看」有个直接的后果:位置有 n 个,两两配对就是 n² 对,所以它的开销是 平方复杂度(Quadratic complexity)。

凡是能用 n² 解释清楚的现象,都不该用「模型能力不够」来解释。

1.2 这一章你只需要带走一件事

如果读完只记住一句话,请记住这句:

注意力里的平方项,只来自「任意两个位置都要互相比较」这件事;剩下的一切(嵌入、投影、前馈网络)都是逐位置做的,随长度线性增长。

记住它,你就能判断任何一项「长上下文优化技术」到底在优化什么:它要么减少需要比较的位置对数(滑窗、稀疏、检索),要么让比较这一步更省内存(FlashAttention),要么干脆不让模型自己回顾历史(状态外置、摘要)。三条路,没有第四条。

1.3 怎么读这一章

本章刻意分成两半:

  • 上半(第二、三节):只讲「是什么」。第二节把名词逐个说清,第三节跟着数据走一遍形状变化。这一半不需要任何数学。
  • 下半(第四、五节):只讲「代价」。把复杂度摊开,看 n² 出在哪,再翻译成工程推论。

凡是加了虚线底的名词——比如 自注意力——点一下就会弹出卡片,里面有它的含义、出现场景、相关学科、例子和延伸阅读。这些卡片的内容全站统一维护,所以你在任何一章点同一个词,看到的都是同一份解释。

二、先把名词说清楚:一段文字怎么变成一组数

很多人读不懂注意力,不是因为数学难,而是因为名词没对齐。所以这一节不出现任何公式,只把后面要用的七个名词逐个说清楚。已经熟的读者可以直接跳到第三节。

2.1 Token:模型眼里的最小单位

一段文字交给模型之前,先被切成一串 Token。token 不一定是「字」,也不一定是「词」,而是分词器按统计规律切出来的最小单位。同一个词在不同上下文里可能被切成不同的 token 组合。

一条够用的换算经验是:1 个中文字符约需 0.6 个 token,1 个英文字符约需 0.3 个 token,反过来粗略地说,1 个 token 约等于 0.67 个英文单词或 1.67 个汉字,因此 8000 token 大约对应 5000 多个英文单词或 1.3 万到 1.4 万个汉字,而代码行数因语言和写法差异很大,不能简单换算,实际 token 数请以模型返回的 usage 为准。

记住 token 的另一个理由更现实:它是计费单位,也是上下文窗口的计量单位。你说的「上下文 128k」指的是 128k 个 token,不是 128k 个字。

2.2 Embedding:把 token 编号变成一串数

token 本身只是个编号(比如第 12345 号)。编号是离散的,编号之间没有远近关系——12345 和 12346 不代表意思接近。

Embedding 解决的就是这件事:它是一张巨大的查找表,每个 token 编号对应一串数。这串数不是人写的,是训练出来的——训练的结果是语义相近的 token,对应的数串在空间里也彼此接近。这就是「Embedding 之后可以算相似度」的全部原理。

那串数本身叫 向量(Vector):一维排列的一组数。至于「一组数为什么能表示意思」——因为方向比大小更重要:两个向量的方向越接近,表示的意思越接近。

2.3 张量与形状:一组被排成网格的数

现在我们有 n 个 token,每个 token 用一串 d 个数字表示。把这些数摆在一起,就得到一个 张量(Tensor)。

「张量」这个词听起来吓人,其实只是「数 + 排列方式」。用一个叫阶数(rank)的东西描述它:

阶数名字例子要几个下标才能定位一个数
0 阶标量3.140 个
1 阶向量[0.2, 0.7, 0.1]1 个
2 阶矩阵一张数表2 个(第几行、第几列)
3 阶及以上更高维网格多份二维表叠起来3 个及以上

所以「张量」和「矩阵」「向量」不是并列关系,而是同一类东西的不同阶数——就像「立方体」和「正方形」都是方的一样。

工程上我们几乎只关心两件事:阶数(有几个下标)和每一维多大。这两者合起来就叫 形状(Shape),写作方括号里的列表。

于是前面那批数据可以一句话说清:n 个 token、每个 token 一个 d 维向量,整批数据的形状是 [n, d]。看到 [n, d] 你就该读出:「这里有 n 个位置,每个位置用 d 个数描述」。

形状不是学术概念,它是最省力的正确性检查:推导形状比推导公式容易得多,也更容易发现错误。本站讲注意力时故意「先不碰公式,只跟踪形状」,就是这个原因——形状对得上,说明数据流没走错;形状对不上,公式写得再漂亮也是错的。

2.4 隐藏维度 d:4096 这个数从哪来

上一步说的「每个 token 用一串数表示」,那串数到底多长?这个长度就叫 隐藏维度(d_model),记作 d,现代模型里常见的是 4096、8192 这类数字。

它有两个来源:一是模型出厂就固定了,你改不了;二是它决定了模型内部能装多少信息。d 太小,表示能力不够;d 太大,参数和显存都爆炸。

为什么 Harness 工程师要关心一个自己改不了的数?因为成本公式里有两个变量,性质完全不同:

  • n(序列长度)是你直接控制的——你决定往上下文里塞多少历史、多少工具返回、多少检索片段;
  • d 是出厂固定的——你只能通过选模型来间接影响它。

所以工程上的优化空间,几乎全部落在 n 上。凡是讨论「怎么把成本降下来」而不谈 n 的,基本可以跳过。

2.5 位置编码:注意力天生不认语序

最后一个名词,也是最容易被忽略的一个。

自注意力有一个反直觉的性质:它不认语序。如果你把输入序列打乱,注意力给出的结果只会跟着一起打乱,模型没有任何机制知道「谁在前谁在后」。对「猫追狗」和「狗追猫」这两句话,纯注意力的输出完全相同。

位置编码(Positional Encoding) 就是把顺序信息注入表示的手段:在 token 的向量上,叠加一个由位置决定的向量。这样「第 3 个位置」和「第 7 个位置」在数值上就区分开了。

现在主流方案是相对位置思路(RoPE 是代表),它编码的是「两者相距多远」而不是「各自排第几」。这带来一个附带好处:模型对超出训练长度的位置有一定外推能力。但请注意一个容易搞混的推论——「把窗口调大」不等于「模型真的能用好长窗口」。窗口是接口能力,用好是能力问题,两者必须分开评测。

三、一次前向到底做了什么

名词说清楚了,现在跟着数据走一遍。这一节依然尽量不碰公式,只跟踪形状——看懂形状变化,就看懂了注意力。

设一批输入是 n 个 token,每个 token 被映射成一个 d 维向量。下面这条链路从头到尾走一遍,就叫一次 前向(Forward pass)——注意它指的是方向(数据从输入推向输出,与反向传播相反),不是某一层;真正的那一层叫「前馈」。下面的图把整条链路画成一条流水线。

自注意力:只看形状 n = 序列长度(token 数) d = 隐藏维度(如 4096) h = 头数 输入 token 序列 形状 [n] Embedding + 位置编码 形状 [n, d] Q = X · W_q [n, d] K = X · W_k [n, d] V = X · W_v [n, d] A = softmax(QKᵀ / √d) 形状 [n, n] ← 平方复杂度的源头 注意力矩阵(每格是一次 token 间权重) n 列 × n 行 = n² 个数 输出 = A · V 形状 [n, d](回到原尺寸)
图 1 注意力的核心只有一步:把 [n, d] 的输入先压成 [n, n] 的权重矩阵,再用它加权聚合 V。所有关于「上下文很贵」的直觉,都来自中间那个 [n, n]:它随 n 平方增长,而输入本身只随 n 线性增长。

三步走,记住这三步就够用了:

  1. 投影:同一份输入 X,用三组不同的权重矩阵投影出 Q(我想找什么)、K(我能被什么找到)、V(我实际携带的内容)。
  2. 打分:用 Q · Kᵀ 算出任意两个位置之间的相关度,得到一个 [n, n] 的矩阵,除以 √d 做缩放,再 softmax 成权重。
  3. 聚合:用这组权重对 V 做加权求和,输出形状回到 [n, d]。

下面把这三步拆开讲,每一步都回答同一个问题:输入是什么、输出是什么、为什么非得这么做。

3.1 投影:同一份输入,三种问法

先说清楚「投影(Linear Projection)」这个词。它在数学上的意思是「换一组坐标去看同一个东西」——不改变「它是什么」,只改变「用什么语言描述它」。实现上就是乘一个矩阵。

关键在于:这个矩阵不是人写的,是训练出来的。训练时反向传播调整的就是这些矩阵里的数。所谓「模型学到了什么」,很大一部分就存在这些矩阵里。

现在看自注意力的入场动作:同一份输入 X(形状 [n, d]),乘三个不同的权重矩阵 W_q、W_k、W_v(形状都是 [d, d]),得到三个形状同为 [n, d] 的张量。

投影:同一份输入,三种问法 X 的同一行 → 乘三套不同的权重 → 三个用途不同的向量(小方格 = 一个数,颜色深浅 = 数值大小) X 的每一行 = 一个 token X [n, d] W_q [d, d] W_k [d, d] W_v [d, d] q = X·W_q k = X·W_k v = X·W_v q:我在这里「要找什么」 k:我「能被什么找到」 v:我实际「交出去的内容」 W 里的数是训练出来的, 不是人写进去的 形状不变:进去 [n, d],出来还是 [n, d] 整批一起做:X [n, d] × W [d, d] → Q / K / V,三者形状都是 [n, d] —— 只换了坐标,没换「有几个位置」
图 2 投影不是把数据「变成别的东西」,而是换一组坐标去看同一个东西。同一行输入乘三套管不同问题的权重矩阵,就得到 q(我要找什么)、k(我能被什么找到)、v(我交出去什么)。注意三者的形状都是 [n, d]——投影只改内容、不改革制。
  • 输入:X,形状 [n, d]——n 个位置,每个位置一个 d 维向量;
  • 输出:Q、K、V,形状都是 [n, d]——同样是 n 个位置、每个位置 d 维,但已经换了一套坐标;
  • 为什么要这么做:因为「找东西」和「被找到」和「实际内容」是三件不同的事,用同一套表示会互相干扰。用一个比喻:Q 是「我要找什么」,K 是「我能被什么找到」,V 是我实际要交出去的内容。

这三者的正式名字是 Q / K / V(Query / Key / Value)。它们来自同一份输入,所以叫「自」注意力——每个位置在向包括自己在内的所有位置提问。

顺带记住一个后面会反复用到的推论:K 和 V 只由输入决定。这正是下一章 KV Cache 成立的前提——只要前缀没变,K、V 就可以重用。

3.2 打分:让每个位置和所有位置互相看一眼

「打分」这个词指的是 打分(Scoring):给每一对位置算一个相关度分数。用什么算?用点积(点积(Dot product))——两个向量方向越接近,点积越大。

实现上,把 Q 和 K 的转置相乘:Q · Kᵀ。形状上发生了第一次也是唯一一次危险的变化:

  • 输入:Q 是 [n, d],Kᵀ 是 [d, n];
  • 输出:[n, n];
  • 为什么是 n²:结果矩阵的第 i 行第 j 列 = 「第 i 个位置该给第 j 个位置打多少分」。n 个位置两两配对,就是 n × n 个格子。
打分:每一格都是一次「谁该关注谁」的判断 Q 的第 i 行 · Kᵀ 的第 j 列 → 一个数 → 放进分数矩阵的第 i 行第 j 列 第 i 行 Q [n, d] · 第 j 列 Kᵀ [d, n] 一次点积 分数矩阵 [n, n] 每一格 = 一个位置给另一个位置的分 颜色越深 = 分数越大 两个维度都长成 n 输入只有一维是 n,这里是两维 n 个位置两两配对 → n × n 格。n² 不是「模型算得慢」,而是「两两比较」这件事本身的规模 这正是下面 KV Cache 与长上下文优化的全部前提:省不掉 n²,就只能少算、少存
图 3 一次点积就是一次「第 i 个位置问、第 j 个位置答」,所以分数的个数 = 位置对的数量 = n²。注意左边的 Q 和 Kᵀ 各自只有一维是 n,右边这张表却是两维都为 n——平方项就是在这一步被制造出来的。

这个 [n, n] 的矩阵叫 注意力矩阵(Attention matrix)。它是全章唯一一个同时被两个维度 n 撑大的东西——输入 [n, d] 只有一维是 n,而它是两维都是 n。

这就是 n² 的全部来源。请把这句话记牢:不是模型「算得慢」,是「两两比较」这件事本身就要 n² 次计算。

3.3 缩放:为什么必须除以 √d

Q · Kᵀ 出来的是原始的分数,直接送进 softmax 会有问题,所以要除以 √d。这一步叫 缩放(Scaling factor)。

为什么是 √d 而不是别的?直觉版的解释:点积是 d 个数相乘再相加。如果每个数都是均值为 0、方差为 1 的随机数,那么 d 项相加的结果方差会变成 d,标准差变成 √d。除以 √d 正好把它拉回单位量级。

  • 输入:[n, n] 的原始分数;
  • 输出:[n, n] 的缩放后分数,量级回到 1 附近;
  • 不这么做会怎样:分数太大时,softmax 会被最大的那个数「吃掉」,输出接近 one-hot(极端情况下梯度会消失)。模型会变得难以训练,注意力权重也会退化——它不再是在做「综合考虑几个位置」,而是硬挑一个位置。
缩放:差一个 √d,行为差一个物种 同一组「谁更相关」的判断,只因为分数被放大,softmax 的结果就从「综合看几处」变成「只挑一处」 不缩放 d = 64,分数被放大 ≈ 8 倍 原始分数:18 · 72 · 12 · 54(差距被拉得很开) softmax 后 ≈ 0.99 / 0 / 0 / 0 → 退化成 one-hot 不但不再「综合考虑」,梯度也趋近于 0 除以 √d 分数回到 1 附近 缩放后分数:2.2 · 3.4 · 1.8 · 2.8(量级可比) softmax 后 ≈ 0.17 / 0.38 / 0.14 / 0.31 → 真的在加权 每行权重之和仍然等于 1 —— 变的是「怎么分」
图 4 这两张柱子用的是同一组相关性判断,唯一的区别是分数有没有被放大。左图那个 0.99 不是「模型很确定」,而是 softmax 被大数吃掉了:一旦变成这样,注意力就不再是「把几处信息综合起来」,而退化成「硬挑一处」,同时梯度趋近 0。所以 √d 不是可选的调参,是让它别退化的必要条件。

3.4 Softmax 与加权求和:从分数回到向量

接下来两步,把 [n, n] 的分数变成 [n, d] 的输出。

第一步是 Softmax:按行归一化,让每一行的 n 个数都变成非负数且加起来等于 1。做完之后,每一行就是一组「分配比例」——第 i 行说的是「第 i 个位置把注意力按什么比例分给所有位置」。

  • 输入 [n, n] 原始分数 → 输出 [n, n] 权重,每行和为 1。

第二步是加权求和(加权求和(Weighted Sum)):用每一行的权重,把 V 里对应的向量按比例加起来,得到一个 d 维向量。n 行各做一次,就得到 n 个 d 维向量。

  • 输入 [n, n] 权重 × [n, d] 的 V → 输出 [n, d]。
从分数回到向量:按比例混合 V 的每一行 这一行说得是「第 i 个位置把注意力按什么比例分给各个位置」,比例就写在权重那一列里 权重(和 = 1) 0.04 0.72 0.01 0.23 V [n, d](每行一个位置的内容) 每行乘自己的权重 = 四行相加(权重大的行贡献大,权重≈0 的行几乎不参与) 输出的一行 输出的一行 = V 各行的「加权平均」 权重非负、和为 1 所以结果一定落在 V 各行的 「包围范围」之内,不会跑飞 n 行各做一次 → [n, d] 进去 [n, d](V 的形状),出来还是 [n, d] —— 注意力是一个「保持形状」的信息交换层 正因为形状不变,它才能一层一层堆上去;也正因为中间那张 [n, n] 表要留下来算,显存才吃紧
图 5 「加权求和」不是新机制,就是把 V 的几行按比例混成一行。softmax 保证权重非负且和为 1,所以输出永远落在这些行的「包围范围」里——这既让它稳定,也解释了一件事:如果权重全被一个位置吃掉(图 4 左),输出就等于直接抄那一行。

形状回到了和输入完全一样的 [n, d]。这不是巧合,而是刻意设计:注意力是一个「信息交换」层,它必须保持形状不变,这样才能一层层叠下去。你可以在图 1 里看到这个「进去 [n, d]、出来 [n, d]」的闭环。

3.5 多头:把一次注意力拆成 h 次并行

只做一套 Q/K/V 是不够的。现实中一句话里同时存在好几种关系——语法上的主谓、指代上的照应、语义上的远近。一套注意力只能表达一种「关注方式」。

多头注意力(Multi-Head Attention) 的做法是把 d 维切成 h 份,每份长度 d/h,各自独立跑一遍上面的流程,最后把 h 份结果拼回 [n, d]。

  • 形状变化:[n, d] → [h, n, d/h](多了「头」这一阶)→ 各头独立算注意力 → 拼回 [n, d];
  • 代价不变:总计算量和不分头基本一样,因为每个头处理的维度只有 1/h;
  • 换来了什么:h 套不同的 W_q/W_k/W_v,等于让模型同时用 h 种「提问方式」看同一段文本。

这就是为什么本站在讲形状时会强调阶数——多头只是多了一阶,机制一步都没变。

多头:把 d 切成 h 份,让 h 种「提问方式」同时上场 机制一步没变 —— 同一行输入按维度切片,每片独立跑一遍 3.1–3.4,跑完再拼回来 输入的一行 [1, d] d 维被切成 h = 4 份 头 1 Wq Wk Wv 学出「看前一个」 头 2 Wq Wk Wv 学出「看自己」 头 3 Wq Wk Wv 学出「盯开头」 头 4 Wq Wk Wv 学出「看全体」 沿维度拼接回 [n, d],再过一次输出投影 W_O —— 各头的信息到这里才真正混到一起 ① 算力几乎不变:每个头只管 d/h 维,h 个头加起来仍是 O(n²d) ② 形状只多一阶:[n, d] → [h, n, d/h] → [n, d](实现上常写成一次大矩阵乘再 reshape) ③ 代价:每个头只看得到 d/h 维,视野更窄 —— 所以最后要用 W_O 把各头重新融合
图 6 四张注意力表都画成同样大小的方格,但深色格子的位置完全不同——这就是「多头」真正花力气的地方。同一个词,头 1 在找出现在它前面的词,头 2 主要看它自己,头 3 一直盯着句首,头 4 则把注意力摊开给全体。一套注意力只能表达一种「关注方式」,h 个头就是 h 种。注意最上面和最下面那两行红蓝黄绿的格子仍然一模一样:输入没被复制,只是被切开了。

3.6 因果掩码:为什么 decoder 只能看左边

生成式模型有一个硬约束:预测第 i 个 token 时,不能看到第 i+1 个及以后的内容,否则等于抄答案。

因果掩码(Causal Mask) 就是实现这个约束的手段:在送进 softmax 之前,把注意力矩阵里「未来位置」那些格子填成负无穷。负无穷过 softmax 之后变成 0,权重就落不到未来位置上了。

  • 输入:[n, n] 缩放后分数;
  • 输出:[n, n],其中上三角(未来部分)全被压成 0。

这一步看着简单,但它是一个大前提:有了它,每个位置的计算只依赖它左边的历史。下一章要讲的 KV Cache,整个立足点就在这里——前缀算过一次就不用再算。

因果掩码:把「未来」的格子先填成 −∞,softmax 之后自然变成 0 第 i 个位置只能看 j ≤ i 的格子 —— 这一步是「增量推理 / KV Cache」能成立的前提 ① 送进 softmax 前的分数 [n, n] ② softmax 之后的权重 [n, n] 1.4 −∞ −∞ −∞ −∞ 0.9 1.1 −∞ −∞ −∞ 0.6 1.7 1.3 −∞ −∞ 1.1 0.4 0.8 1.5 −∞ 0.7 1.2 0.9 1.6 1.0 + 掩码 M 上三角全填 −∞ 1.00 0 0 0 0 0.45 0.55 0 0 0 0.20 0.55 0.25 0 0 0.30 0.10 0.20 0.40 0 0.15 0.25 0.20 0.30 0.10 每行只有左侧 i+1 格有值, 其余格是 0,权重分不过去。 → 第 i 个位置只看得到 1…i → 未来位置永远不会被看到 顺带的好处:训练时 n 个位置 可以并行算完,不用排队 金色格子的深浅只是「自己看自己」的标记,阴影格子就是「未来」:先填 −∞ 再做 softmax,指数化后直接变 0 —— 不用写 if 注意右边每行的和都恰好是 1:被掩掉的位置不是「跳过」,而是以 0 权重的身份参与,所以输出的形状一步都没变 ⚠ 顺序不能反:掩码必须加在 softmax 之前 —— 先掩码再归一化,被掩位置才恰好占 0 权重;若先 softmax 再抹零,  每行的和就不再是 1,等于把注意力凭空丢掉了。 ▸ 因果掩码是「只能看左边」的实现手段;而「左边算过就不用重算」,正是下一章 KV Cache 的立足点。
图 7 这张图值得记住的地方有两个。第一:掩码加在 softmax 之前,所以未来位置是「−∞ → 权重 0」,而不是「事后删掉」——右边矩阵每行的和照样是 1,形状和流程一个字都没改。第二:右边矩阵的上三角整块是 0,这个「下三角」形状就是因果性的形状;下一章的 KV Cache,全部的道理就是这张下三角矩阵里「同一列被反复用到」。

四、n² 究竟出在哪一步

4.1 把每一步的形状与复杂度摊开

把每一步的复杂度列出来,问题就一目了然了。这里用到大 O 记号(大 O 记号(Big-O notation))——它只关心「随 n 增长的量级」,不关心常数。

阶段做的事张量形状计算量级随 n 的增长
投影X·W_q/k/v[n, d] → [n, d]O(n · d²)线性
打分Q·Kᵀ[n, d] × [d, n] → [n, n]O(n² · d)平方
缩放除以 √d[n, n]O(n²)平方
softmax按行归一化[n, n]O(n²)平方
聚合A·V[n, n] × [n, d] → [n, d]O(n² · d)平方
前馈网络逐位置 MLP[n, d] → [n, d]O(n · d²)线性

结论就一句话:平方项只来自「任意两个位置都要互相看一眼」这件事,也就是打分、缩放、softmax、聚合这四步。剩下的一切都是逐位置做的:

  • 投影:每个位置各自乘自己的矩阵,位置之间互不影响,线性;
  • 前馈网络(前馈网络(Feed-Forward Network)):同样是逐位置的一小段 MLP,线性。

这也解释了为什么 FFN 明明占了模型大部分参数,却从来不是长上下文的主要瓶颈——它的开销随 n 线性增长,而注意力是平方。n 小的时候感觉不出来,n 一大就彻底颠倒。

4.2 一个必须记住的数量级直觉

一个必须记住的数量级直觉

设 d = 4096,n = 8,000(大约 3 万汉字或 6,000 行代码):

  • 线性项的量级约 n·d² ≈ 1.34×10¹¹;
  • 平方项的量级约 n²·d ≈ 2.6×10¹¹。

此时两者还在同一数量级——这就是「中等长度上下文时,注意力还不是绝对瓶颈」的原因。但只要 n 再翻一倍,平方项就翻四倍,线性项只翻两倍。拐点大约出现在 n 与 d 可比的时候;一旦 n 远超 d,平方项就彻底主导成本。

反过来读这条结论也成立:当 n 还小于 d 时,花大力气优化注意力收益有限,先把上下文塞满反而是划算的。判断「该不该优化」比「怎么优化」更重要。

五、这对做 Agent 意味着什么

把上面的结论翻译成工程语言,会得到三条非常硬的推论。

5.1 成本不是线性的,「多塞点上下文」是高息负债

往上下文里加一倍内容,注意力部分的代价接近四倍。这解释了为什么长上下文模型的 API 定价通常按阶梯上涨,而不是线性——定价模型跟着物理规律走。

反过来说,这也给了一个判断标准:如果你发现某次调用的成本远超预期,先去看输入 token 数(n),而不是去怀疑模型供应商乱收费。

5.2 裁剪与压缩不是优化,而是必备能力

如果你的 Harness 只会「一直追加消息」,那你实际上是在让成本按平方增长。上下文压缩、状态外置、历史摘要这三件事,是 Harness 的基本功而不是加分项。

它们各自对应一条减少 n 的路径:

  • 摘要:把旧的多轮内容压成一段,直接减少 n;
  • 状态外置:把不必要的内容移到外部存储,需要时再取,让 n 保持小而稳定;
  • 检索:只把相关内容塞进 上下文窗口(Context Window),而不是全塞。

注意这三条路线都在动 n——没有一条在动 d。这与第二节点出的结论一致:d 出厂固定,n 才是你的操作杆。

5.3 首 token 延迟与后续 token 延迟是两笔账

处理输入(Prefill(Prefill / Decode))要跑完整个 [n, n];生成每个 token(Decode)只走一步,但需要读缓存(KV Cache)。

这两个阶段的瓶颈不同:前者是算力受限,后者是显存带宽受限。所以「优化延迟」这句话是不完整的——你必须先问清是哪一段延迟。这直接决定了优化手段:加算力对 Prefill 有效,对 Decode 基本没用;而 Decode 的优化要靠减少每步要读的数据量。

这是下一章的主题。这里先记住结论的形式:当你听到一个优化方案时,先问它优化的是 Prefill 还是 Decode——分不清这两者的方案,通常两边都不讨好。

把 n² 记住,你就不需要背任何「长上下文为什么要小心」的结论了——那些结论全都是它的推论。本刊编辑部

六、动手:三十行手写一遍注意力

6.1 把图 1 写成代码

不写一遍就容易把形状记混。下面这段 NumPy 代码把图 1 完整实现了一遍——重点不是算法,而是每一行注释里标注的形状。请对照着代码读一遍,确认每一步的形状变化和第三节讲的一致。

scaled_dot_product_attention.pypython
import numpy as np
def softmax(x, axis=-1):
    m = x.max(axis=axis, keepdims=True)      # 数值稳定:先减去最大值
    e = np.exp(x - m)
    return e / e.sum(axis=axis, keepdims=True)
def attention(X, Wq, Wk, Wv):
    """X: [n, d]  三个权重: [d, d]"""
    Q = X @ Wq          # [n, d]  我想找什么
    K = X @ Wk          # [n, d]  我能被什么找到
    V = X @ Wv          # [n, d]  我携带什么
    d = Q.shape[-1]
    S = Q @ K.T         # [n, n]  ← 平方复杂度的来源
    S = S / np.sqrt(d)  # 缩放,防止 softmax 进入饱和区
    A = softmax(S)      # [n, n]  每行和为 1
    return A @ V        # [n, d]  回到原形状
n, d = 5, 8
rng = np.random.default_rng(0)
X  = rng.normal(size=(n, d))
Wq = rng.normal(size=(d, d)) / np.sqrt(d)
Wk = rng.normal(size=(d, d)) / np.sqrt(d)
Wv = rng.normal(size=(d, d)) / np.sqrt(d)
out = attention(X, Wq, Wk, Wv)
print(out.shape)          # (5, 8) —— 形状与输入一致,这是所有注意力变体的共同特征
# 顺手验证因果掩码(decoder 只能看左边)
mask = np.triu(np.ones((n, n)), k=1).astype(bool)
print(mask.astype(int))

6.2 实操任务(建议 20 分钟)

动手三件事
  • 把 n 从 5 改到 2000,测量 S = Q @ K.T 与 X @ Wq 的耗时比例,亲手看到平方项吃掉时间;
  • 把 mask 加进 S(被 mask 的位置填 -inf),再做 softmax,确认第一行只依赖第一个 token;
  • 回答自己一个问题:如果我想让「第 500 个 token 只关注最近 100 个 token」(滑窗注意力),代码要改哪一行?改完之后,中间那个矩阵还是 [n, n] 吗?

七、常见误区与追问

这一节收的是「看起来懂了、一被追问就露馅」的几个地方。它们的共同点不是难,而是中文技术叙述里长期流传的错误类比。

7.1 把「参数量」和「序列长度」当成同一条轴上的两件事

错在哪:说「这个模型很大所以很贵」,或者反过来,用参数量去解释「为什么上下文变长会变贵」。为什么自然:日常经验里「东西大就贵」几乎总成立,而大模型的「大」字被滥用了。正确做法:这是两条正交的轴。参数量 d 决定显存下限与单 token 的算力,出厂固定、你改不了;序列长度 n 决定算力与显存的增长速度,是你唯一能直接控制的变量。判据:一个 7B 模型喂 128k 上下文的注意力开销,可以远超一个 70B 模型喂 2k 的开销 —— 因为前者是 O(n²·d),n 在平方项里,后者只是使 d 变大。

7.2 以为注意力矩阵的显存和 KV Cache 是同一个量级

错在哪:把「长上下文吃显存」笼统归给注意力矩阵。为什么自然:都是「长上下文变贵」,很容易当成一件事。正确做法:分清两个不同的东西。注意力矩阵是 [n, n],是算力侧的中间结果,FlashAttention 的思路就是「算完即弃、不显式存下来」;KV Cache 是 [n, d] 每层两份,是推理必留的状态,随 n 线性增长,省不掉。判据:n=8,000 时注意力矩阵单精度约 256 MB(可优化掉),而 80 层的 KV Cache 是 GiB 量级(必须预算)。面试里把这两笔账分开算,是分水岭。

7.3 以为「投影会改变形状」

错在哪:把 Q/K/V 三步当成「把输入变换成三种不同形状」。为什么自然:符号上 X → Q 看着就像变了个东西。正确做法:投影只换坐标,不改形状。X 是 [n, d],W_q 是 [d, d],Q 还是 [n, d] —— 三个都是 [n, d],差别只在「用什么坐标系描述」。真正改变形状、并制造出 n² 的是下一步打分:Q·Kᵀ 把 [n, d] 变成了 [n, n]。判据:如果哪天你看到一个注意力变体把形状写错了,八成是把「投影」和「打分」混在一起了。

7.4 以为「头数越多越好」

错在哪:把多头理解成「多几个模型一起投票」。为什么自然:「多个视角」听起来总是更强。正确做法:头的本质是把 d 维切成 h 份并行,d = h × head_dim。好处是可以同时维持多种关注方式(位置邻近、指代、句法);代价是每个头的表达能力被稀释,而且注意力矩阵的数量随头数成倍增加。判据:现代模型头数多在 32–128 之间,是实验调出来的,不是越大越好;同时要记住所有头共享同一层的输入,它们不是独立模型。

7.5 把注意力权重视为「模型给出的解释」

错在哪:看到某次注意力把 0.7 分给了某个词,就说「模型这样判断是因为它注意到了这个词」。为什么自然:权重看起来就是一张「谁重要」的名单,很像解释。正确做法:注意力权重是线索,不是证明。它只说明这一层、这个头、这个样本上的分配;不同层不同头的权重往往互相矛盾,而后续的 FFN 完全可以在权重之外改变结论。把它当作排查工具(找注意力涣散、找异常聚焦)是合理的,当作因果解释就不成立。

八、自测

本章自测答错的题建议加入复习队列
Q1注意力里随序列长度平方增长的是哪一部分?
B。投影与 FFN 都是逐位置计算的,复杂度 O(n·d²),随 n 线性。只有当两个位置必须互相比较时(QKᵀ 产生 [n, n]),才出现平方项。记住这个区分,就能判断任何「优化长上下文」的技术到底在优化什么。
Q2如果把上下文长度从 8k 提到 32k,注意力部分的算力大约变成原来的几倍?
C。n 变成 4 倍,n² 变成 16 倍。这正是「长上下文很贵」的物理来源,也是阶梯定价的合理性所在。注意这是注意力部分的量级,端到端延迟还受 KV Cache 读写带宽影响,实际倍率不一定正好是 16 倍,但量级关系成立。
Q3位置编码如果不加,模型会出什么问题?
B。注意力本身对输入顺序不敏感——打乱输入,输出只会跟着打乱,模型没有任何机制知道谁在前谁在后。位置编码(现在主流是 RoPE 这类相对位置方案)把顺序信息注入表示。顺带的推论是:相对位置方案让模型对超出训练长度的位置有一定外推能力,但「把窗口调大」不等于「模型真的能用好长窗口」——这是长上下文必须单独评测的原因。

九、小结:把这一章压成四句话

  1. 数据是张量,形状是 [n, d]。 张量只是「数 + 排列方式」,形状是它最省力的正确性检查。n 是 token 数,d 是出厂固定的隐藏维度——你能控制的是 n。
  2. 注意力的三步是投影、打分、聚合。 投影把同一份输入翻译成 Q / K / V 三种问法;打分让每个位置和所有位置互相看一眼;加权求和把结果聚合回 [n, d]。
  3. 平方项只出在「两两比较」上。 中间那个 注意力矩阵是 [n, n],它是唯一一个两维都被 n 撑大的东西。前馈网络和投影都是逐位置的,线性。
  4. 两条工程推论:n 越大成本越贵,所以压缩、外置、检索是基本功;Prefill 和 Decode 瓶颈不同,所以「优化延迟」必须说清是哪一段——这是下一章 KV Cache 的起点。

下一章我们把这个 n² 换个角度再看一次:既然前缀会被反复重算,那能不能缓存?能,但缓存命中与否取决于一个你平时根本不会注意到的东西——前缀稳定性。

十、参考与延伸

本章的机制部分只讲到「够用」为止。想往下深挖,下面这几份材料按「先看图、再看代码、最后读论文」的顺序排好了。全站不做原文转载,这里只登记链接与「为什么值得读」。

先看图(建立直觉)

  • The Illustrated Transformer —— 用动画式的静态图把 Q/K/V、多头、位置编码全画了一遍。读本章第三节卡住时,来这儿看图解最快。它的图比任何公式都更容易建立「形状在流动」的感觉。
  • 3Blue1Brown · Attention in transformers, visually explained —— 把注意力讲成「向量之间互相传递信息」的几何过程。如果你对「点积为什么表示相关度」没有直觉,看这一集,它是目前最好的可视化解释。

再看代码(动手实现)

最后读论文(对齐一手定义)

  • Attention Is All You Need(arXiv:1706.03762) —— 一切的一手来源。建议只精读第 3.2 节(Scaled Dot-Product Attention)与 3.5 节(Positional Encoding),其余部分与本岗位关系不大。
  • Lilian Weng · The Transformer Family v2.0 —— 把注意力的各种变体(稀疏、线性、FlashAttention)按「改了什么」整理成了一张谱系图。想理解「长上下文优化都有哪几条路」,读这一篇的目录结构就够了。
  • RoFormer / RoPE(arXiv:2104.09864) —— 现在主流的位置编码方案。只需要看它怎么把「绝对位置」换成「相对旋转」那一段。理解这一点,才能听懂「为什么扩窗口需要额外训练」。

◇ 面试官会怎么问

共 3 条 · 其中 1 条高频 · 先自己答一遍,再展开对照

为什么注意力复杂度是 O(n²)?这对做 Agent 意味着什么? 高频
注意力要让每个位置对其他所有位置算一个权重,得到的是 seq × seq 的注意力矩阵,所以算力随序列长度呈平方增长。
对 Agent 的直接含义有三层:第一,上下文越长,成本不是线性上涨而是平方级上涨,这是"长上下文很贵"的物理原因;第二,往上下文里多塞一倍内容,代价远不止翻一倍;第三,裁剪、压缩、状态外置不是优化技巧,而是必须做的基本功。

追问链

  1. 那 KV Cache 的显存占用怎么估算?它和 n² 是同一个量级吗?
  2. 如果只关心首 token 延迟,哪个成本占主导?
加分点:能把「注意力矩阵的平方级算力」和「KV Cache 的线性级显存」分开讲,就说明不是背结论。
多头注意力比单头好在哪?
单头只有一组 Q/K/V 投影,只能学到一种关注方式。多头把表示空间切成若干子空间并行做注意力,不同的头可以各自关注不同类型的关联——位置邻近、指代关系、句式结构等,最后拼接再投影。
工程意义在于:模型能同时维持多种关系,这是长链路任务里"既记住目标又盯住细节"的基础。注意头的数量与 head_dim 的乘积大致等于隐藏维度,所以加头并不等比例增加算力,但头太多也会稀释每个头的表达空间。
加分点:主动补一句"不是越多越好",显示你有工程判断而不是背书。
位置编码解决什么问题?没有会怎样?
注意力本身是置换等变的——把输入顺序打乱,输出只会跟着打乱,模型无法区分"猫追狗"和"狗追猫"。位置编码把顺序信息注入表示。演化路径大致是可学习绝对位置嵌入 → 正弦编码 → 现在主流的 RoPE 这类相对位置方案。
对工程的启示:相对位置方案让模型对超出训练长度的位置有一定外推能力,但外推能力有限。"把窗口调大"不等于"模型真的能用好长窗口",这就是长上下文需要专门训练和评测的原因。
加分点:把位置编码与"有效上下文长度"联系起来,是一个很自然的加分项。
答这类题的通用结构

结论先行(一句话给出取舍)→ 机制(为什么是这样,涉及哪条链路)→ 代价或边界(这么做放弃了什么)→ 你的实践或数字(真实场景里怎么落地)。 只讲机制不讲代价,是背题;只讲取舍没有数字,是空谈。

本章收尾 · Wrap up
掌握度自评
点一下给自己打分;低于 3 分建议加入复习队列
个人笔记 · Notes
    下一章 KV Cache:多轮对话的隐性账单 Agent 的成本大头不在输入,而在每一轮都要重新送一遍的历史。KV Cache 把重复计算变成缓存命中,而命中率取决于一个你从没注意过的细节:前缀稳定性。 I · 大模型原理 · 第 02 章 · 进阶 · 约 26 分钟