已经有 Embedding 了,为什么还要变成 Q、K、V 三份?
本课一问:已经有 Embedding 了,为什么还要变成 Q、K、V 三份?
这一课先不碰:Q 和 K 为什么用点积比(第 2 课)、分数怎么变成权重(第 3 课)、谁能看谁(第 4 课)、为什么要好几套 Q/K/V(第 7 课)。
从已有出发
到上一章为止,「小明把苹果给了小王」已经变成 5 个 token:小明 / 把 / 苹果 / 给了 / 小王。每个 token 查表得到一个 5120 维向量(锚点 Qwen3.8-27B 的 hidden_size),位置信息由 RoPE 在注意力内部补上(C2)。
问题是:这 5 个向量彼此不知道对方的存在。「给了」的向量里没有「谁给的、给了什么、给了谁」。再看两句话:
「猫追老鼠,因为它饿了。」——「它」指猫
「猫追老鼠,因为它跑得快。」——「它」指老鼠
同一个「它」查出来的是同一个向量,可意思要看上下文。所以核心问题是:一个 token 的表示,怎么把它该关注的那些 token 的信息融进来? Transformer 里唯一做这件事的部件就是注意力,其余部件(FFN、归一化、残差)都是每个 token 各算各的——这也是为什么注意力是 Transformer 里最该搞懂的一块。
先自己设计:直接比相似度
最容易想到的做法:两个 token 的向量越像,就越该互相关注。Embedding 训练出来就是「用法相近的 token 向量相近」,拿来直接比,听起来很合理。
下面用 5 个 4 维教学向量试一下(手工构造的,不是模型学的)。先点「给了」当提问者,看它最该关注谁。
- ① 朴素方案
- ② 它在哪失败
- ③ 改进方案
朴素方案败在三处,前两处和向量怎么构造无关:
- 自己永远第一。余弦的对角线恒为 1;换成点积也一样,x·x = |x|²,只要长度差不多,别的 token 就比不过自己(柯西–施瓦茨不等式:x·y ≤ |x||y|) 证据等级 · 数学可证明 在写明的前提下可以严格推导出来,不依赖实验。只要前提成立,换数据、换规模仍然成立。 本条余弦对角线恒等于 1;点积 x·y ≤ |x||y|,长度相近时对角线最大 出处待补 。一个只会关注自己的 token 什么也取不回来。
- 关系没有方向。相似度是对称的:「给了」对「小王」多相关,「小王」对「给了」就一样相关 证据等级 · 数学可证明 在写明的前提下可以严格推导出来,不依赖实验。只要前提成立,换数据、换规模仍然成立。 本条cos(x,y) = cos(y,x),x·y = y·x 出处待补 。可「给了」需要找受事者,「小王」却未必需要找动词——关系是有方向的。
- 相似 ≠ 关系。「给了」和「把」都是动词性的词,向量最像;「给了」和「小王」一点也不像,关系却最重要。Embedding 只回答「这两个 token 像不像」,回答不了「此刻我需要谁」。
改进:把一个向量投影成三份
打分和取回,其实是三件不同的事:
| 角色 | 问的问题 | 怎么算 |
|---|---|---|
| Q(query) | 我现在要找什么? | qi = xi · WQ |
| K(key) | 我能被怎样找到? | kj = xj · WK |
| V(value) | 找到我之后,我交出去什么? | vj = xj · WV |
打分用 qi·kj:提问者用自己的 Q,被查者用自己的 K。因为 WQ ≠ WK,上面三个毛病一起消失——在「改进方案」那一侧看:「给了」自己排到最后,前三名正好是苹果、小王、小明;矩阵不再对称。V 在第 5 课才用上。
这三个矩阵从哪来?训练出来的。没有人写「遇到『给了』就去找人名」这种规则。WQ、WK、WV 一开始是随机数,模型在海量文本上做「预测下一个 token」,loss 通过反向传播一点点改这三个矩阵;某种投影让该互相关注的 token 打分更高、预测更准,它就被保留下来 证据等级 · 有理论动机 有数学或机制上的理由说明它可能有效,但没有证明它一定更好。最后是否采用,仍然要看实验。 本条把打分和取值拆成 Q、K、V 三个可学习投影,是 Transformer 的设计;它为什么更好,原文给的是设计动机与整体实验结果,没有单独证明 出处Vaswani et al. 2017 §3.2 ↗ 。下面组件里的 W 是手工构造的,只为让你看清机制;真模型学到的样子在实验课看。
用一个类比入门,然后立刻拆掉它。常见的说法是「搜索系统」:你输入的搜索词像 Q,每件商品的标签像 K,点进去看到的商品详情像 V。精确的对应是:
| 搜索系统 | 注意力里的对应物 |
|---|---|
| 搜索词 | 提问者 token 的 qi = xi·WQ |
| 商品标签 | 每个 token 的 kj = xj·WK |
| 匹配度 | qi·kj(第 2 课) |
| 商品详情 | vj = xj·WV |
它在四个地方不成立:① 搜索返回一个排序列表让你挑;注意力不「挑」,它把所有 V 按权重加权平均成一个向量(第 3、5 课);② 商品标签是人写的、有意义的词;K 是学出来的向量,每一维没有名字;③ 每个 token 同时是搜索者和商品,它自己也在结果里;④ 搜索词是用户给的,Q 是 token 自己算出来的——到了第二层,每个 token 的 x 已经混进了上下文,同一个词的 Q 在不同句子里就不一样。
极小真数字算一次
先用对话样本里的 2 维例子,手算一个 token 怎么变成三份:
再看锚点模型 Qwen3.8-27B 里真实的形状。T 取贯穿例句的 5 个 token;点「小规模」可以把维度缩小到能数清格子。
| 维度 | 含义 | 真实规模 | 小规模 |
|---|---|---|---|
| T | 序列长度:贯穿例句的 token 数 | 5 | 5 |
| dmodel | 模型宽度 hidden_size | 5120 | 4 |
| Hq·dh | Q 头数 × 每头维数 = 24 × 256 | 6144 | 4 |
| Hkv·dh | KV 头数 × 每头维数 = 4 × 256 | 1024 | 2 |
试三件事:① 在上面的比较器里把提问者换成「小王」,看余弦相似度下它最像谁、Q/K 打分下它最关注谁;② 在「改进方案」一侧找一对 i、j,核对「i 看 j」和「j 看 i」的分数不相等;③ 在张量图里点 W_K,看它为什么比 W_Q 窄那么多——答案先记着,第 9 课揭晓。
⚠ 常见误解
「Q、K、V 是三个新 token。」不是。它们是同一个 token 的三种投影,位置不变、个数不变:5 个 token 进来,出来的还是 5 行 Q、5 行 K、5 行 V。
「教学向量的第 1 维表示『是不是人名』。」这里的每一维都没有名字,我们只是按一个意图凑出了几个数;真实 embedding 的语义分散在很多维的组合里,单独一维读不出「人名程度」。
注意力的演进,你在这里
① 训练时:一个 token 怎么从别的 token 那里取信息
- 解决了能读任意长的序列,参数与长度无关;手里只带一个固定大小的状态 h。卡在ht 要等 ht−1 算完:T = 4,096 时每层 4,096 步不能并行,第 1 个 token 的信息要经过 4,095 次更新才到最后;翻译时整句还得挤进一个定长向量——Bahdanau 2014 说的瓶颈。展开岔路 B1
- 解决了解码器每一步回头给源句的每个位置打分、加权取回,不再只靠那一个定长向量:score = vT·tanh(W1q + W2k)。卡在每一对都要单独过 tanh,交不给矩阵乘单元;朴素实现要先落一个 T×T×da 的中间张量:T = 4,096、da = 256 时单头单层 8.0 GiB(BF16),点积只留 T×T 个分数,32 MiB。它还长在 RNN 翻译模型里,串行没解决。展开第 2 课
- 解决了直接比 embedding 时「给了」最像的是它自己(余弦 1.000),小明、苹果、小王排在最后;投影成 Q、K 再点积,它自己排到最后、前三名是苹果、小王、小明,全部配对一次矩阵乘 QKT;取回用单独的 V——两个人名的 K 余弦 0.995,取回 K 分不清是谁。卡在不缩放:锚点每头 256 维,点积标准差 √256 = 16,同一行分差十几二十是常态(e20 ≈ 4.9 亿),softmax 几乎独占,梯度传不回 WQ、WK。
- 解决了softmax 把分数变成正的、和为 1 的权重,只看分差(按占比分,对话样本的 10、3、−4 会给「汽车」−44.4%);除以 √dk = 16,点积方差回到 1。卡在训练时整句一次喂进去:不加掩码,「给了」一行有 32.5% 的注意力直接落在它要预测的答案「小王」上,loss 低得虚假。展开第 3 课
- 解决了旋转后 q·k 只依赖距离 j − i;V 不参与打分,不转(转了会把绝对位置混进取回的内容)。锚点每头 256 维里只转 64 维。卡在旋转矩阵随位置变,夹在展开矩阵和缓存之间就没法事先并进 Q:MLA 因此把 RoPE 单独拆出 64 维另存(DeepSeek-V2 config,第 10 课)。
- 解决了标准 softmax 逼每行分完 100%:「苹果」一行 3 个分数只差零点几(0.04、0.36、0.00),照样分成 30% / 41% / 29%;分母加可学习的 sink logit,或在输出上乘门,这个头就能「这次不取」——锚点开着输出门(attn_output_gate = true)。卡在出口的效果只有实验证据:Gated Attention 原文说门控「缓解」sink,不是消除;DeepSeek-V4 没给 sink logit 的单独消融。展开岔路 B4
② 生成时:K、V 怎么越存越少
- 解决了缓存直接除以头数:锚点形状下每 token 16 KiB,262,144 上下文 4 GiB。卡在质量会掉、训练可能不稳:GQA 论文表 1(T5-XXL,七个任务平均)MHA 47.2 → MQA 46.6。展开第 9 课
- 解决了24 个 Q 头分 4 组、每组 6 个共用 1 个 KV 头(组数 = KV 头数):每 token 64 KiB、262,144 上下文 16 GiB;T5-XXL 上 GQA-8 平均 47.1、推理 0.28 s,质量接近 MHA(47.2、1.51 s),速度接近 MQA(0.24 s)。卡在再减 KV 头就回到 MQA:DeepSeek LLM 67B 已是 GQA-8,每 token 仍要 389,120 B(每头一份的 MHA 是 3,112,960 B),1M token 单条请求 380 GiB。展开第 9 课
- 解决了不存 K、V,每层每 token 只存 512 维潜向量 + 64 维 RoPE = 576 个数,用时再展开:DeepSeek-V2(60 层)每 token 69,120 B、V3(61 层)70,272 B;相对 67B 按 BF16 降 82.2%,官方「−93.3%」还含约 6 bit 的 KV 量化。卡在仍随长度线性增长,打分仍是 T²:GLM-5.3 同是 576 个数、78 层,每 token 89,856 B,原生 1,048,576 上下文时单条请求只算潜向量就要 87.75 GiB。展开第 10 课
- 解决了换掉「按长度存」本身:锚点 64 层里 48 层不存 KV,只存固定大小的状态(FP32 共 144 MiB),与上下文长度无关(这些层是什么、怎么更新,C11 第 4 课揭晓);DeepSeek-V4.1 叠加压缩、稀疏与低精度,官方报告全局 KV 890 B/token。卡在固定状态装不下任意多对键值,锚点仍留 16 层全注意力;稀疏注意力每步只读挑出来的一部分,挑本身也有代价——各自省了什么、没省什么,C11 讲。
证据三问
- 它解决什么问题?让打分有方向、不偏向自己,并且把「凭什么被找到」和「被找到后交出什么」分开。
- 数学上为什么可能有效?q·k = xi·WQWKT·xjT:两个矩阵合起来是一个不对称的双线性形式,能表达「i 找 j」而「j 不找 i」;原始相似度做不到 证据等级 · 数学可证明 在写明的前提下可以严格推导出来,不依赖实验。只要前提成立,换数据、换规模仍然成立。 本条W_Q W_Kᵀ 一般不对称,所以 s_ij ≠ s_ji;而 x_i·x_j 恒对称 出处待补 。但「这样就能学到语言关系」是理论动机,不是证明 证据等级 · 有理论动机 有数学或机制上的理由说明它可能有效,但没有证明它一定更好。最后是否采用,仍然要看实验。 本条设计动机,无单独证明 出处Vaswani et al. 2017 §3.2 ↗ 。
- 实验上为什么最后选了它?2017 年的 Transformer 用它在翻译任务上超过了当时的 RNN 模型,之后所有主流大模型沿用 证据等级 · 实验验证 由对比实验得出的经验结论,只在出处写明的模型、数据和规模下成立。换了条件可能不再成立,引用时要带上实验设置。 本条整体架构的实验结果,不是只拆 Q/K/V 这一项的消融 出处Vaswani et al. 2017 §6 ↗ 。
一句话
Q 决定「我要找什么」,K 决定「我能被怎样找到」,V 决定「找到我之后给你什么」——同一个 token 的三种投影。
下一个缺口
打分用的是 q·k。可为什么是点积?欧氏距离、余弦、再过一个小网络,不都能算「匹不匹配」吗?→ 第 2 课:Q 和 K 怎么算匹不匹配?为什么偏偏用点积?
你可能会问
这一课还没有收到真实提问;你的提问会记到 05-问答记录/C3-L1.md。下面是学这一段时常见的两个问题。
🔍 为什么要 W_Q、W_K 两个矩阵,不直接学一个 M 算 x_i·M·x_j?
q·k = xi·(WQWKT)·xjT,确实等于一个矩阵 M = WQWKT。拆成两个窄矩阵有两个好处:① 参数少——锚点模型一个头的 WQ、WK 各是 5120 × 256,合计 2,621,440 个数;直接学 M 要 5120 × 5120 = 26,214,400 个,是 10 倍;M 的秩也被限制在 256 以内。② 每个 token 的 k 只算一次就能被所有提问者复用——生成时把 K 存起来反复用,正是第 8 课 KV cache 的前提。
🔍 同一个词在不同句子里,Q 会不一样吗?
第一层里,x 就是 embedding(加上位置),所以同一个 token 的 q 在不同句子里一样。从第二层开始,x 是上一层注意力和 FFN 的输出,已经混进了上下文,同一个「它」在「饿了」和「跑得快」两句里的 x 不同,q 也就不同——「它」在不同句子里找的对象可以不同,就是这么来的。