返回 AICAP-180
B2 · Day 11attention/decoder/KV + judge 校准

Scaled dot-product + causal mask

B1(Day 1-10)把「评测方法论 + tokenization + 成本对齐」打底完毕——我们已经能数 token(1760 token 实测)、能把 cost 和 token×单价对齐。但那是把模型当黑箱看「外部行为」。从今天起 B2 进入模型「内部机理」:Day 11 拆开 decoder 的最小原子——单头 scaled dot-product attention + causal m

阶段: B2 · attention/decoder/KV + judge 校准(Day 11-20) 标签: #attention #causal-mask #softmax #decoder-only

今日导引(由浅入深)

B1(Day 1-10)把「评测方法论 + tokenization + 成本对齐」打底完毕——我们已经能数 token(1760 token 实测)、能把 cost 和 token×单价对齐。但那是把模型当黑箱看「外部行为」。从今天起 B2 进入模型「内部机理」:Day 11 拆开 decoder 的最小原子——单头 scaled dot-product attention + causal mask。这是整条 B1→B18 能力曲线第一次「不止做笔记、而是把机制写成可测试代码」的拐点:后续 Day 12(multi-head/decoder block)、Day 14(KV cache)、乃至 B12 的 MLA/FlashAttention,全部建立在今天这块代数上。今天的「最小可判定产出」是一句话:第 i 个 token 的输出对未来 token 的扰动必须不变(causal 性质可被单测证伪)

1. 机理精读

Attention 的定义极简:给定 query 矩阵 Q、key 矩阵 K、value 矩阵 V(都是 [seq, d]),先算 scores = QKᵀ,把每个 query 与所有 key 做点积得到 [seq, seq] 的相关性矩阵;除以 √d 缩放;再对每一行做 softmax 得到归一化权重;最后 weights · V 对 value 加权求和。一行的语义是:「当前位置该从其他位置各借多少信息」。

更细地拆三个角色:

  • query(问):当前 token「想找什么信息」的方向向量。
  • key(答的索引):每个位置「我能提供什么」的标签向量。
  • value(答的内容):每个位置实际被读出的内容向量。

scoreᵢⱼ = qᵢ·kⱼ 衡量「位置 i 的问」与「位置 j 的标签」有多匹配;softmax 把一行的匹配度归一化成「分配给各位置的注意力预算」(和为 1);weightsᵢ · V 是按预算把各位置的内容加权混合,得到位置 i 的输出。Q、K、V 本身是输入经三组可学习投影 Wq/Wk/Wv 得到的——投影矩阵才是「学到的知识」,attention 算子本身无参数。

为什么要除以 √d:Q、K 的分量若近似独立同分布、均值 0 方差 1,则点积 q·k = Σ qᵢkᵢ 是 d 个独立项之和,方差随 d 线性增长(≈ d),标准差 ≈ √d。d 大时(如 d=128),未缩放的 logits 会很大,softmax 进入饱和区——梯度趋近 0、权重退化成近似 one-hot,模型只盯一个位置。除以 √d 把 logits 方差拉回 ≈1,softmax 落在敏感区,梯度健康。这是 Vaswani 2017《Attention Is All You Need》里 §3.2.1 给出的原始动机。

为什么是 √d 而不是 d:我们要归一化的是点积的标准差(≈√d),不是方差(≈d)。除以 d 会过度压扁 logits,让分布过平、几乎均匀注意;除以 √d 恰好把标准差拉回 1 的量级,是「方差稳定化」的标准做法。

causal mask 为什么是上三角置 -∞:decoder-only 模型做的是「自回归语言建模」——预测第 i+1 个 token 时只能看 1..i,绝不能偷看未来,否则训练时泄露答案、推理时分布不一致。实现上,把 scores 矩阵中 j > i(列 j 在行 i 之后即未来)的元素置 -∞;softmax 时 exp(-∞)=0,未来列权重归零。注意是置 -∞ 而非置 0:若直接把权重置 0,softmax 的分母仍含未来项、归一化被污染;置 -∞ 在 softmax 之前做才正确。

causal 的工程价值:因为有 mask,训练时一条长度 n 的序列可以一次前向同时得到 n 个位置的「下一 token 预测」——位置 i 的损失只用到 1..i 的信息,互不串扰。没有 mask 就只能逐位置喂、训练慢 n 倍。这也是 decoder-only 训练高效的根因。

关键权衡:朴素 attention 的时间/空间都是 O(seq²·d)——scores 矩阵本身就是 seq×seq。短序列教学无碍,长上下文(128K)下这是显存与算力的主战场,催生了 FlashAttention(不显式落 seq×seq 矩阵、分块在 SRAM 里算)、MLA(压缩 KV)等。今天我们刻意用朴素版,因为手算可验证 + 单测可证伪比性能更重要。

与相邻概念的边界:今天是「单头」——一个 Q/K/V 投影、一组权重。Day 12 的 multi-head 是把 d 切成 h 份各自做一遍 attention 再拼回;Day 14 的 KV cache 是利用「历史 token 的 K/V 不随新 token 改变」省掉重算。三者共用今天这块 attention() 核。参考 3Blue1Brown《Attention in transformers, visually explained》(2024-04) 的几何直觉:query/key 是「问与答」的方向向量,点积大 = 语义匹配。

还需划清的边界:本日是 self-attention(Q/K/V 同源于一条序列)+ causal(只看过去)。它不同于 encoder 的双向 self-attention(无 mask,看全句),也不同于 encoder-decoder 的 cross-attention(Q 来自 decoder、K/V 来自 encoder)。现代生成式 LLM 用的就是「causal self-attention」这一种。三类对照:

类型Q/K/V 来源mask用在哪
causal self-attn同序列上三角 -∞decoder-only LLM(GPT/Llama)
bidirectional self-attn同序列encoder(BERT 类)
cross-attnQ=decoder, K/V=encoder无(或 padding mask)encoder-decoder(翻译、T5)

本日只实现第一行——它是今天到 B12 全部生成式机理的根。

2. 推导 / 手算 / 代码走读

本仓已把单头 attention 实现并测试,落在 src/agent/transformer/transformer.ts(seed 里规划的 attnSingleHead.ts 文件名最终被合并进这一份统一实现,机制本身一字不差地存在;独立文件未单建)。真实函数 attention(Q, K, V, causal)(第 64-70 行)走读:

  1. const dk = Q[0]!.length; const scale = 1 / Math.sqrt(dk)——取 key 维度 dk 算缩放因子 1/√d,与机理一致。
  2. let scores = matmul(Q, transpose(K)).map(row => row.map(v => v * scale))——QKᵀ 后逐元素乘 scale,得 [seq][seq]transposematmul 是同文件内自写的纯数值算子(无张量库)。
  3. if (causal) scores = scores.map((row, i) => row.map((v, j) => (j > i ? -Infinity : v)))——这一行就是 causal mask:列 j > i 即未来位置置 -Infinity。逻辑与「上三角置 -∞」完全对应。
  4. return matmul(softmaxRows(scores), V)——softmaxRows(第 36-43 行)是逐行数值稳定 softmax(先减 row max 再 exp,避免溢出),再 · V 得加权输出。

手算一个最小 causal 例子(d 任意,只看 mask 行为,2 token,scores 已算好):

原始 scores(缩放后)        causal mask 后            softmax 后(逐行)
[[2.0, 1.0],          →     [[2.0, -∞],        →    [[1.000, 0.000],
 [0.5, 3.0]]                 [0.5, 3.0]]              [0.076, 0.924]]
  • 行 0(第一个 token)只能看自己:未来列被 -∞,softmax 后 = [1, 0],行和 1.000。
  • 行 1(第二个 token)能看 0 和 1:softmax([0.5, 3.0]) ≈ [0.076, 0.924],行和 1.000。
  • 上三角恒为 0 ✓,每行和恒为 1 ✓——这正是 seed 要求截图的「下三角非零、上三角为 0、行和=1.000」。

把第 1 行的 softmax 完整算一遍(验证数值稳定写法):

  1. 取 row max = 3.0,逐项减去:[0.5-3.0, 3.0-3.0] = [-2.5, 0]
  2. exp:[exp(-2.5), exp(0)] = [0.0821, 1.0]
  3. 归一化:和 = 1.0821[0.0821/1.0821, 1.0/1.0821] = [0.0759, 0.9241] ≈ [0.076, 0.924]

「先减 max 再 exp」给出的结果与「直接 exp 再归一化」数学等价(分子分母同乘 exp(-max) 约掉),但避免了 exp(大数) 溢出——这正是本仓 softmaxRows 的写法。最终输出 out = weights · V:行 0 的输出 = 1.0·V[0] + 0·V[1] = V[0](只读到自己),行 1 = 0.076·V[0] + 0.924·V[1](主要读后者)。

verify the causal property by perturbation:把 V[1](未来位置的 value)改成任意值,行 0 的输出仍是 V[0],纹丝不动——因为行 0 给未来的权重恒为 0。这正是本仓单测 describe('causal attention') 的断言逻辑(扰动最后一行 V,前几行输出不变),也是今天「最小可判定产出」的可执行化身。

支撑算子也值得点名(同文件,被 attention 复用):

  • matmul(a, b)(第 8-25 行):朴素三重循环矩阵乘,含 if (b.length !== k) throw 的内维校验,并对 aip === 0 做跳过小优化。
  • transpose(a)(第 27-33 行):K → Kᵀ,供 QKᵀ 用。
  • softmaxRows(a)(第 36-43 行):逐行 max → exp(x-max) → /sum,数值稳定。

这些都是纯函数、无副作用、无随机,因此 attention 的输出对固定输入完全确定——这是单测能用 toBeCloseTo(..., 10)(10 位精度)做断言的前提。

3. 今日实战

seed 原计划新建 src/agent/transformer/attnSingleHead.ts,对 4-token、d=4 的定值输入逐步 console.log(scores, masked scores, weights)。实际落地时机制被合并进 src/agent/transformer/transformer.tsattention(),并配了真实单测 src/agent/__tests__/transformer.test.ts。可复现实战步骤:

  1. transformer.test.tsdescribe('causal attention') 用例里,用 mulberry32(11) 这个确定性 RNG 生成 4×4 的 Q/K/V(randMatrix(4, 4, rng))。
  2. attention(Q, K, V, true)out1;把 V 的最后一行整体 +5 得 V2,再跑 out2
  3. 断言:out1out2 的前 3 行(行 0..2)逐元素相等(toBeCloseTo(..., 10))——证明扰动「未来 token」不影响「过去 token」的输出,即 causal 性质成立。
  4. pnpm vitest src/agent/__tests__/transformer.test.ts,观察 causal 用例绿。

补充可做的手算实验(复现 seed 要的截图产出):

  1. 取定值 Q=K=V4×4 单位相关矩阵,手工调 attention(Q,K,V,true),打印中间 scores、masked scoresweights
  2. 检查 weights 是严格下三角非零、上三角全 0、每行和 = 1.000——即 seed 描述的「下三角非零、上三角为 0、行和=1.000」。
  3. causal=false 再跑一次对照:此时 weights 满阵、每行仍和为 1,但行 0 也会读到未来——直观看出 mask 的作用。
  4. 这一步纯离线、无 key、无网络,是今天「最小可判定产出」的可视化版本。

4. 今日实测 / 产出

  • 状态(按 seed):seed 标注「待建/待跑」。规划产出为 attnSingleHead.ts + 手算 4×4 weights 截图(下三角非零、上三角为 0、行和=1.000)。
  • 实际进度(诚实标注):机制已落在统一文件 src/agent/transformer/transformer.tsattention(),且 transformer.test.ts 的 causal 单测已绿(用 mulberry32(11) 固定种子,扰动未来 token 不改过去输出)。seed 计划的独立文件 attnSingleHead.ts 未单独创建——这是诚实差异,不能宣称已建独立文件。手算 4×4 weights 截图仍属待补产出。
  • 该实现纯确定性、无 API key 依赖,可直接配 vitest 单测——这一条与 seed 一致。

5. 常见误区 / 陷阱

  1. 把 mask 在 softmax 之后做:若先 softmax 再把未来列置 0,分母已被未来项污染、行和不再为 1。必须在 softmax 之前把未来 logits 置 -∞。
  2. softmax 不减 max 直接 expexp(大 logit) 溢出成 Inf。本仓 softmaxRows 先减 row max,是数值稳定的标准写法。
  3. 缩放因子用错维度:除的是 key/query 的特征维 d_k,不是 seq 长度。多头时是每个头的 d/h,不是全维 d——Day 12 会再踩到。
  4. 把朴素 attention 当生产方案:O(seq²) 显存在长上下文下爆炸,生产侧早已是 FlashAttention/MLA。教学手算用朴素版无妨,但别迁移结论到性能。
  5. 把 mask 用 -1e9 而非 -Infinity 还能接受,但用 0 不行-1e9 经 softmax 后 exp 出几乎 0,可接受;置 0exp(0)=1 会给未来位置巨大权重,彻底破坏 causal。本仓直接用 -Infinity,最干净。
  6. 混淆 QKᵀ 行列语义scores[i][j] 是「query i 对 key j」。mask 的是「j > i」(query 看不到它之后的 key),写成「i > j」就把过去 mask 掉了、方向反了。

6. 学习资源(每条带 YYYY-MM)

  • Vaswani et al.《Attention Is All You Need》(2017-06, arXiv:1706.03762)——scaled dot-product attention 与 √d 缩放的原始出处。
  • 3Blue1Brown《Attention in transformers, visually explained》(2024-04)——seed 指定复看,几何直觉最清晰。
  • Karpathy《Let's build GPT: from scratch》/ nanoGPT (2023-01 起持续更新)——单头/多头 attention 的从零实现范式。
  • FlashAttention-3 (2024-07, arXiv:2407.08608)——长序列下 attention 的当前推理 SOTA,作为「朴素版之上是什么」的参照。
  • Jay Alammar《The Illustrated Transformer》(2018-06, 持续维护)——Q/K/V 与 multi-head 的图解经典,补几何直觉。
  • 本仓 src/agent/transformer/transformer.ts + src/agent/__tests__/transformer.test.ts(AICAP-180, 2026-06)——本日 attention() 的真实实现与 causal 单测。

SOTA检查 (2026-06 更新)

  • 当前主流:scaled dot-product attention(Vaswani 2017)仍是所有 decoder-only 模型的基座,机理不过时;causal mask 是自回归 LM 的硬约束。3Blue1Brown 2024-04 讲解仍准确。
  • 是否仍 SOTA:作为「数学定义」是 SOTA;作为「推理实现」不是——生产侧已是 FlashAttention-3(2024-07)/ MLA(DeepSeek-V3,2024-12)等,目标是省掉 seq×seq 显存与带宽。教学手算仍用朴素版。
  • 过时黑名单:不要把 O(seq²) 朴素 attention 宣称为长上下文生产方案;不要用未缩放点积(softmax 饱和);不要在 softmax 后做 mask。
  • 下次复查点:B2 末(Day 20)回看;B12 attention/KV 深水区时用真实 FlashAttention-3 / MLA 论文替换教学版基线,重验当周 SOTA。

衔接

  • 昨天:Day 10 — 双数对齐收口(eval cost 与 token 报告对齐,B1 黑箱外部行为收尾)。
  • 今天:把 decoder 最小原子——单头 scaled dot-product attention + causal mask——写成可被单测证伪的代码,确立「未来不影响过去」。
  • 明天:Day 12 — Multi-head + decoder block 结构(把单头扩成多头,叠残差/LayerNorm/FFN 成完整 block)。