深入篇 21 / Transformer 逐步拆解 · 2026-11-08
KV cache:用等价实验理解“只算新 token”
解释为什么存 K/V 不存历史 Q,并验证完整前向与增量前向在误差容限内一致。
解释为什么存 K/V 不存历史 Q,并验证完整前向与增量前向在误差容限内一致。
省掉的是哪些重复计算
没有缓存时,为生成下一个 token,会对整个前缀重新执行各层投影、FFN 和 attention。因果结构下,旧位置的本层输入不因未来 token 追加而变化,其 K/V 可以复用。下一轮需要的是新 query 对历史 key 的匹配,再用历史 value 汇总;历史 query 对未来输出无直接用途,所以通常不持久缓存 Q。
复杂度要说清“每一步”还是“全过程”
固定层数和维度,前缀长度为 t 时,不缓存的 dense attention 完整重算约 O(t²) 个 query-key 对;缓存后单步 decode 只有一个新 query,需要 O(t) 个。若从空序列生成 N 个 token,累计 attention 对数从 Σt²=O(N³) 降至 Σt=O(N²)。这不是“整段生成变成 O(N)”;投影、FFN、prefill、batch 等成本还要另外算。这个结论是对标准全注意力的运算量计数,不能直接套到 sliding window 或稀疏 attention。
一条容易错一位的不变量
num_tokens = num_cached_tokens + 1
本轮输入 = last_token;position = num_tokens − 1
forward 后 cached += 1;采样后 num_tokens += 1
prefill 把 prompt 的 KV 全算完,采样出的第一个输出 token 此时没有 KV。下一次 decode 输入它,才创建它的 KV。nano-vllm 的 postprocess 先 hash_blocks,再更新 cached,最后 append_token;chunked prefill 尚未完成时会跳过 append。这些顺序正是在维护这个边界。
显存容量与生命周期
标准 K/V 缓存字节数是2LBTHkv d bytes。它包含每层、每请求、每 token 的两份张量;分页改变分配方式,不会让有效 token 的 K/V 字节神奇消失。权重、工作区和页尾浪费不在此式内。实验中允许小浮点差异;并行形状和 kernel 路径可能改变舍入,不能一律要求逐位完全相等。
把维度变化拆到每一步
- 追加的是cache的token轴;query只包含本轮新token。
- 分页写入依靠slot_mapping,逻辑追加不等于反复cat整段缓存。
改变变量,验证直觉
启用 JavaScript 后可交互;上面的图和下面的手算练习始终可读。
数值练习:先算,再核对
运行 python labs/transformer_lab.py cache。实验用固定随机种子、两层 pre-norm toy decoder、RoPE、GQA 和 SwiGLU,比较完整 causal 前向、逐 token 前向与两段 chunk。故意错误的 chunk mask 必须产生明显偏差,否则实验没检验到关键问题。
闭卷检索
不要回看正文。答错后先解释原因,再重新打开这一课。
把结论落到源码
- 按 scheduler.postprocess 的实际顺序跟踪 cached、scheduled 与 num_tokens。
- 用 prepare_decode 的 context_lens 和 positions 解释为什么两者相差1。
以下行号由本地 vendor 文件生成;上游链接固定完整 commit,离线摘录可直接打开。
- nano-scheduler:nanovllm/engine/scheduler.py L25–92 · 上游证据
- nano-decode-inputs:nanovllm/engine/model_runner.py L172–188 · 上游证据
- nano-cache-allocation:nanovllm/engine/model_runner.py L103–122 · 上游证据
优先阅读:本地 scheduler.postprocess 和 PagedAttention 论文 §2–3。
速查:形状与公式 · 三框架映射 · 术语表 · 复习与实战路线 · 4070 Ti本地实验
只在当前浏览器保存自评;不会自动写成已掌握的 learning record。