LEARNING UNIT · 10
KV Cache 与 Prefill/Decode 推理路径
沿 prefill、decode、多轮对话、前缀复用和滑动窗口追踪 KV 的生成、堆积、复用与淘汰。
- 已整理章节
- 15 节
- 单元来源
- 14 条视频
- 总时长
- 42:00
- 状态
- 已发布
- 学习位置
- 10 / 20
主题讲解 · 03:38
提示词修改后,哪些 KV Cache 还能复用
学习目标
- 能指出提示词单点修改后的缓存失效边界。
- 能用逐行线性投影解释第一层为什么只有改动位置的 K、V 先变化。
- 能用因果注意力矩阵解释修改点之后的 hidden state 为什么都会变化。
- 能区分第一层与更深层的 KV Cache 失效范围。
- 能说明去掉因果掩码后,为什么前缀也不再稳定。
前置与衔接
本课假设你已经知道 self-attention 中 Q、K、V 的来源,并理解 causal mask 禁止当前位置读取未来 token。
它回答一个很实用的问题:对话生成到一半后,如果用户回头修改提示词,服务端需要把整段 KV Cache 全部丢掉吗?
答案不是简单的“全丢”或“全留”,而是沿修改位置形成一条精确边界。
本课先在数学上推导这条边界,后续课程再把它连接到公共前缀复用、Radix Tree 与缓存替换策略。
核心讲解
1. 修改点把序列分成前缀与后缀
设原提示词 token 序列为
用户只把第 个 token 改成 ,其余 token ID 不变。
提示词只改动一个 token 时,修改点把序列分成可复用的前缀和需要重新计算的后缀。
原视频 · 00:20 ↗直观结论是:
- 之前的公共前缀仍然可以复用;
- 从 开始的缓存必须重新计算;
- 这条结论依赖 decoder-only 模型的因果注意力。
“后面的 token 自身没有改”不等于“后面的表示不变”。
因为后方 token 会读取第 个位置,而那个位置的内容已经改变。
2. 第一层先利用逐行投影的独立性
一层中的 Q、K、V 投影可写成
矩阵乘法对 token 行是独立的:输出第 行只依赖输入第 行和共享权重。
逐 token 的线性投影互不串行干扰:未改位置的输入行保持不变,改动位置的 Q、K、V 才首先变化。
原视频 · 01:20 ↗因此在第一层输入处,若只有 改变,则
只有 会直接变化。
这里必须纠正字幕中的一个易混点:KV Cache 保存的是 K 与 V,不保存 Q。
所以第一层缓存的直接变化项是 ,不是所谓“Q3 的 KV Cache”。
3. 注意力把局部变化扩散成后缀变化
因果注意力分数为
而 的位置被 mask 成不可见。
被改位置的查询对应一整行,被改位置的键对应一整列;因果掩码决定真正受影响的是修改点及其后方区域。
原视频 · 02:00 ↗第 个 query 变化,会改变第 行可见区域。
第 个 key 变化,会改变所有允许读取 的查询行。
在 causal mask 下,只有 的查询能够读取第 个 key。
因此:
- 对 ,第 行看不到位置 ,整行输入也没变;
- 对 ,query、key、value 都可能改变;
- 对 ,query 在第一层仍不变,但它与 的分数会改变。
4. Softmax 会让受影响行整体重分配权重
只要一行中某个合法分数改变,按行 Softmax 的归一化分母就会改变。
于是该行所有注意力权重都可能重新分配,而不只是第 列的权重变化。
按行 Softmax 后,修改点之前的注意力行不变,修改点及其后的行会改变。
原视频 · 02:20 ↗注意力输出为
对 ,合法的 与 都没变,所以 不变。
对 ,权重或 至少有一项变化,所以输出通常会变化。
这正是“前缀稳定、后缀失效”的数学来源。
5. 第一层与后续层的失效范围不同
第一层输入 embedding 中只有位置 改变,所以第一层只有 需要直接更新。
但第一层输出从位置 开始已经整体改变。
这些输出经过输出投影、残差连接与 FFN 后,作为第二层输入。
第一层的局部变化经过注意力输出、输出投影与 FFN 后,成为下一层从修改点开始的整段变化。
原视频 · 03:00 ↗于是从第二层开始,所有 的输入行都可能不同。
相应地,更深层的
都要重新计算。
这给出工程上的失效策略:保留所有层在 之前的 KV,截断从 开始的后缀,再对新后缀做增量 prefill。
6. 为什么双向编码器没有这条前缀边界
若取消 causal mask,每个位置都能读取位置 。
那么 或 的变化会进入所有查询行,包括 之前的位置。
若去掉因果掩码,前方位置也能读取被改 token 的值,单点修改会向整段表示传播。
原视频 · 03:20 ↗第一层输出的所有位置都可能改变;进入第二层后,所有位置的 K、V 也都可能改变。
因此对标准 BERT 式全序列编码而言,单点编辑通常要求整段重算。
这不是因为矩阵乘法突然失去逐行独立性,而是因为双向注意力把所有位置连在了一起。
跟练与练习
原视频练习
编者练习
一个 decoder-only 模型的 10 token 提示词中,第 4 个 token 被修改。分别说明第一层和第二层应重算哪些位置的 KV Cache。
查看参考答案
第一层输入只有第 4 个 embedding 改变,因此直接生成的 K、V 只有第 4 个位置不同;但第一层注意力输出从第 4 个位置开始都会改变。第二层以这些输出为输入,因此第 4 到第 10 个位置的 K、V 都必须重算。工程上通常保留各层第 1 到第 3 个位置的公共前缀缓存,并重新执行后缀。
编者练习 2
为什么第 7 个位置的第一层 query 没变,它的注意力输出仍可能变化?
查看参考答案
第 7 个 query 仍会读取第 4 个 key 和 value。第 4 个 token 修改后,、 变化,导致第 7 行中与 的分数变化;Softmax 会重新归一化整行权重,最终对 V 的加权和也随之变化。
常见误区
- 误区:KV Cache 里也保存 Q。纠正:自回归复用的主体是历史 K、V;旧 Q 完成查询后通常无需保存。
- 误区:只改一个 token,所有层都只重算一个位置。纠正:第一层是单点直接变化,更深层是从修改点开始的后缀变化。
- 误区:后方 token ID 没改,所以它们的 KV 不会改。纠正:它们读取了被改位置,hidden state 已经改变。
- 误区:Softmax 只改变被改分数对应的那一个权重。纠正:归一化分母改变会让整行合法权重重新分配。
- 误区:任何 Transformer 都能保留修改点之前的缓存。纠正:该边界依赖因果掩码;双向注意力会把变化传播到前方。
- 误区:公共前缀只看文字相同即可。纠正:tokenization、位置编号、模型权重和相关推理配置也必须一致。
本课小结
- 单点编辑把 decoder-only 序列分成可复用前缀与需重算后缀。
- 第一层逐行投影只让改动位置的 K、V 直接变化。
- 因果注意力与按行 Softmax 会把变化传播到修改点及其后方。
- 从第二层开始,后缀所有位置的 K、V 都可能变化。
- 双向注意力没有同样的稳定前缀边界。
- 下一课将把这条边界提升为“KV Cache 何以存在”的一般判据。
主题讲解 · 02:43
KV Cache 能成立的真正条件
学习目标
- 能解释为什么“BERT 一次看完整句”只是表层答案。
- 能从因果掩码推导 GPT 的公共前缀为何稳定。
- 能从双向注意力推导 BERT 的单点修改为何导致整段失效。
- 能说出前缀 KV Cache 可复用的一般判据。
- 能区分模型内部的增量 KV Cache 与其他形式的静态缓存。
前置与衔接
上一课已经推导:decoder-only 模型修改第 个 token 后,第 个位置之前的缓存仍然稳定,而后缀需要重算。
本课把这个结论上升为一个更一般的问题:KV Cache 为什么能够存在?
常见回答是“GPT 自回归、BERT 不是自回归”。
这句话方向没错,但没有指出真正的数学条件。
决定性条件是:相同公共前缀在两次前向计算中,必须产生相同的逐层 K、V。
核心讲解
1. 表层差别:因果生成与双向编码
GPT 式 decoder-only 模型使用 causal mask,每个位置只能读取自己和过去。
BERT 式 encoder-only 模型使用双向 self-attention,每个位置都能读取整段输入。
GPT 的因果注意力只让修改点及其后方变化,而 BERT 的双向注意力会让整段表示相互依赖。
原视频 · 00:20 ↗因此生成新 token 时,GPT 既不需要也不允许让历史位置读取未来 token。
已经算好的历史 K、V 可以留在缓存中,新 token 只需追加自己的 K、V。
BERT 的典型任务则是对一段完整输入做一次编码,没有逐 token 生成循环。
但“任务形式不同”仍不是最深层原因;真正的区别要看输入变化是否会反向污染公共前缀。
2. 两种架构都从局部 Q、K、V 变化开始
假设第 个输入向量改变。
线性投影仍然逐行独立:
第一步只有 直接变化。
单个输入行变化先影响同位置的 Q、K、V,并在注意力分数中形成查询行与键列的影响范围。
原视频 · 01:00 ↗注意力分数矩阵中, 影响第 行, 影响第 列。
到这里,GPT 和 BERT 的局部代数完全相同。
分歧发生在 mask:哪些查询行实际包含第 列?
3. GPT:因果掩码保护公共前缀
对 causal attention,第 行只能访问 的 key。
如果 ,这一行根本看不到第 个 key。
因此公共前缀位置的注意力分数、Softmax 权重和加权结果都不变。
在因果掩码下,变化被限制在当前位置及其后方,前面的隐状态与 KV Cache 保持不变。
原视频 · 01:40 ↗可以用依赖关系表述为
而不是依赖完整序列 。
只要两个请求的前 个 token 及相关位置条件相同,就有
于是由这些 hidden state 投影得到的 也相同。
这就是前缀缓存可以跨请求复用的根基。
4. BERT:双向注意力让所有行依赖改动位置
BERT 没有 causal mask,第 个 key 对所有 query 行都可见。
当 改变时,每一行至少有一个分数可能改变。
Softmax 按行归一化,因此每一行的权重分布都可能变化。
BERT 没有因果掩码,被改键所在的列可进入所有查询行,使每一行 Softmax 和后续表示都可能变化。
原视频 · 02:00 ↗再考虑 也变化,所有位置的加权和都可能改变。
第一层输出整体变化后,第二层的 Q、K、V 也整体变化。
因此对标准 BERT 全序列编码,哪怕只修改中间一个 token 或在末尾追加一个 token,旧序列的上下文化表示也不能假定仍然有效。
这说明“BERT 没有 KV Cache”的准确语境是:它通常没有像自回归 decode 那样可逐步追加并长期复用的 self-attention KV Cache。
5. 一般判据:公共前缀必须产生相同 KV
课程最后把两条输入画在一起,例如
与
它们的公共前缀是 a a b。
缓存可复用的判据是:两次输入的公共前缀在模型中必须产生完全相同的逐层 KV。
原视频 · 02:20 ↗若模型满足因果依赖,则公共前缀在每层得到相同 K、V,后续分支可以从同一缓存节点继续。
因此更一般的判据是:
文字看起来相同还不够。
至少还要保证:
- tokenizer 与 token ID 序列相同;
- position ID 或位置编码条件相同;
- 模型权重、LoRA/adapter 与精度配置兼容;
- attention mask 和影响 hidden state 的请求级配置一致。
这些属于编者补充的工程边界,用来避免把“公共字符串”误当成“公共缓存”。
6. 不要把结论扩大成“BERT 绝对不能缓存”
课程讨论的是 self-attention 的增量 KV Cache。
在其他系统中仍可能缓存 tokenizer 结果、embedding、完整句向量、检索结果,或 encoder-decoder cross-attention 所需的 encoder K/V。
这些缓存的复用条件和生命周期与自回归前缀 KV Cache 不同。
所以应说“标准 BERT 编码过程缺少稳定的增量 self-attention 前缀缓存”,而不是“任何使用 BERT 的系统都不能缓存任何东西”。
跟练与练习
原视频练习
编者练习
两个请求的文本前缀看起来完全相同,但第二个请求从不同的 position ID 开始。能否直接复用第一份请求的 KV Cache?
查看参考答案
不能直接假定可以。位置编码会参与 Q、K 或 hidden state 的形成;position ID 不同可能使相同 token 产生不同的逐层 K、V。只有证明两次请求在准备复用的前缀上产生兼容且相同的缓存,才满足复用判据。
编者练习 2
为什么在双向注意力中,把一个 token 追加到序列末尾也可能让旧位置表示全部变化?
查看参考答案
旧位置可以读取新追加 token 的 key 与 value。每个旧查询行多了一个合法分数,Softmax 的归一化集合改变,旧位置的加权输出随之可能变化;进入下一层后,这种变化继续传播。因此不能像 causal decode 那样只追加末尾 KV。
常见误区
- 误区:BERT 没有 KV Cache 只是因为它不是生成模型。纠正:更深原因是双向注意力使公共前缀表示随后续输入变化。
- 误区:单点变化只会改变注意力矩阵中的一个格子。纠正:query 影响一行,key 影响一列,Softmax 又会重分配整行权重。
- 误区:公共前缀字符串相同就一定能复用。纠正:必须比较 token、位置、模型与推理配置。
- 误区:GPT 的所有旧缓存永远有效。纠正:前缀本身或相关配置变化时,失效边界之后仍需重算。
- 误区:BERT 系统不能做任何缓存。纠正:本课只讨论增量 self-attention KV Cache。
- 误区:缓存的是 hidden state 就和缓存 K、V 完全等价。纠正:具体复用对象和下游算子接口必须匹配。
本课小结
- 因果掩码让每个位置只依赖前缀,因此相同前缀能产生相同逐层 K、V。
- 双向注意力让所有位置互相依赖,单点修改或追加 token 都可能使整段失效。
- KV Cache 可复用的真正判据是公共前缀的缓存数值与布局兼容。
- token、位置、模型权重和 attention 配置都属于复用条件。
- 下一课将讨论:当可复用前缀越来越多、显存放不下时,系统如何选择保留和驱逐对象。
主题讲解 · 02:33
前缀 KV Cache 为什么也需要 LRU
学习目标
- 能用容量有限解释 KV Cache 为什么需要替换策略。
- 能把 CPU Cache 的 LRU 直觉迁移到前缀缓存。
- 能说明 KV Cache 的对象包含每个 token、每一层的 K 与 V。
- 能识别跨用户、跨任务和跨时间三种公共前缀复用。
- 能解释 Radix Tree 中“保留哪条前缀分支”为什么是价值判断。
前置与衔接
前两课已经得到两个结论:因果模型的公共前缀能够产生相同 KV;修改点之前的前缀可以保留,之后的缓存需要重算。
当服务端同时维护大量请求时,可复用前缀会越来越多。
但 GPU 显存是有限的,缓存不可能无限增长。
所以问题从“能不能复用”进一步变成“哪些值得继续留在显存里”。
课程以 SGLang 的前缀缓存场景为例,用 LRU、LFU 等策略说明这一选择。
具体实现和默认策略可能随版本变化,本课重点是替换问题的结构,而不是绑定某一版接口。
核心讲解
1. LRU 的基本直觉:长期不用的先离场
LRU 是 Least Recently Used,即最近最少使用。
可以为缓存块维护新旧度:访问一次就把它标记得更“新”,长期未访问的块会变“旧”。
LRU 用访问新旧度决定淘汰对象:被访问的块变新,长期未访问的块成为优先驱逐候选。
原视频 · 00:20 ↗当容量不足必须腾出空间时,LRU 优先驱逐最近最久没有被访问的对象。
它背后的经验假设是时间局部性:刚被访问的内容,短期内更可能再次被访问。
LFU 则关注访问频率,倾向保留长期高频对象。
两者回答的都是同一个资源分配问题,只是对“未来价值”的估计方式不同。
2. KV Cache 的缓存对象到底是什么
一句公共前缀不是只对应一个 K 和一个 V。
对每个 token、每个 Transformer 层、每个 KV head,都有相应的张量切片。
公共前缀中的每个 token 在每一层都有对应的 K 与 V,缓存对象不是一份文本字符串,而是一组逐层张量。
原视频 · 00:40 ↗可用一个简化形状表示每层缓存:
若共有 层,K 与 V 合计元素数近似为
因此一个被共享很多次的长前缀可能很有复用价值,同时也会占据可观显存。
替换策略需要在“节省未来计算”和“释放当前内存”之间做权衡。
3. 跨用户复用:引擎按 token 前缀而不是身份命中
假设两个用户的请求分别以“你好”开头,随后走向不同后缀。
只要前缀 token、位置和模型配置兼容,公共部分的逐层 KV 可以只生成一次。
不同用户请求只要具有相同 token 前缀,就可以命中同一份公共前缀 KV Cache。
原视频 · 01:00 ↗这里的共享不意味着用户的完整会话数据互相可见。
共享的是数值相同的前缀计算结果;权限隔离、请求归属与后续分支仍由服务系统管理。
工程上还必须遵守隐私与租户隔离策略,不能只凭数值可复用就跨越安全边界。
课程强调的是计算图层面的可复用性。
4. 跨任务复用:多个 Agent 继承共同背景
多个 SubAgent 可能都需要同一段系统提示、工具说明或背景知识。
如果每个 Agent 都从头 prefill 这段背景,会重复生成完全相同的前缀 KV。
多个子任务共享必要背景知识时,可复用背景前缀的 KV,再从分叉处各自继续计算。
原视频 · 01:20 ↗更合理的上下文布局是:
- 把必要且公共的背景放在前缀;
- 为这段前缀生成一份缓存;
- 在角色或任务描述处形成不同分支;
- 各 Agent 只为自己的后缀继续计算。
这种布局既减少重复 prefill,也让公共前缀成为高价值缓存对象。
5. 跨时间复用:修改提示词仍保留修改点之前的路径
同一用户先发送一句提示词,随后回头修改其中一个 token。
从推理引擎的 token 路径看,这与两个请求共享一段前缀再走向不同后缀没有本质差别。
同一用户修改提示词时,修改点之前的 token 前缀仍可复用,修改点及后方需要重新生成缓存。
原视频 · 01:40 ↗修改点之前的缓存保持有效。
修改点及其后方的缓存需要重新生成。
原 AI 字幕在这一段出现“后面的不需要重新生成”的否定错位;完整上下文和画面明确表达的是“后面的必须重新生成”。
这也是本课唯一需要显式说明的字幕纠错。
6. 为什么要按前缀分支选择驱逐对象
当显存装不下所有缓存时,系统面对的不是抽象的单个文本,而是带有共享关系的前缀节点和分支。
显存不足时,系统必须在不同前缀子树之间选择保留或驱逐对象,近期高频使用的公共背景更值得留下。
原视频 · 02:20 ↗若一段背景知识刚被多个请求频繁命中,把它驱逐会让后续请求重复付出很大的 prefill 成本。
若另一条长分支长期没有访问,保留它的机会成本可能更高。
因此可以用 LRU、LFU 或成本感知策略给候选对象排序。
在树结构中还要考虑父子关系:删除父前缀可能影响挂在其下的多个可复用分支。
具体驱逐粒度取决于实现,可能是 token 块、页、节点或一组关联块;课程板书表达的是通用决策逻辑。
7. LRU 不是“永远正确”的最优算法
LRU 只依据最近访问时间,无法直接感知:
- 前缀长度和重算成本;
- 后代分支数量;
- 未来请求的已知调度信息;
- 不同租户的优先级与隔离要求;
- 当前块被释放后能否形成足够连续的可用容量。
因此 LRU 是容易理解、维护成本低的基线,并不保证所有工作负载下最优。
LFU、成本加权、优先级或混合策略可以补充它,但也会增加元数据和调度复杂度。
跟练与练习
原视频练习
编者练习
缓存 A 是 100 token 的公共系统提示,刚被 20 个请求命中;缓存 B 是 500 token 的私有前缀,10 分钟未被访问。显存不足时,纯 LRU 会倾向驱逐谁?为什么这不一定等于全局最优?
查看参考答案
纯 LRU 会倾向驱逐更久未访问的 B。这个选择可能合理,但 LRU 没有直接比较 A、B 的长度、重算成本、未来请求概率或树上后代关系。若 B 很快会被高优先级任务再次使用,它仍可能是错误选择;因此实际系统可能加入频率、成本或优先级。
编者练习 2
为什么“两个用户共享前缀 KV”不等于“两个用户能看到彼此的提示词”?
查看参考答案
数值复用发生在相同 token 前缀的计算结果层面。请求身份、后缀内容、输出和访问权限仍由服务层隔离。实现还必须满足租户与隐私策略;缓存命中本身不应向请求暴露其他用户的数据或身份。
常见误区
- 误区:KV Cache 只有一份 K 和 V。纠正:它按层、token、KV head 等维度组织。
- 误区:不同用户绝对不能共享计算结果。纠正:数值可复用性取决于相同前缀,但仍必须服从安全策略。
- 误区:同一用户修改提示词后要全量重算。纠正:因果模型可保留修改点之前的公共前缀。
- 误区:LRU 会删除“内容最不重要”的缓存。纠正:它只近似未来价值,依据的是最近访问时间。
- 误区:高频前缀永远不能驱逐。纠正:容量、优先级、重算成本和调度约束仍可能要求释放它。
- 误区:课程提到 SGLang 就意味着所有版本的具体策略都相同。纠正:应以当前实现文档为准,本课只提炼通用机制。
本课小结
- KV Cache 与 CPU Cache 一样受容量约束,因此需要替换策略。
- 公共前缀缓存包含逐 token、逐层的 K 与 V,价值和成本都可能很高。
- 跨用户、跨任务、跨时间都能形成相同 token 前缀的复用机会。
- LRU 倾向淘汰最近最久未使用的对象,LFU 更关注访问频率。
- 前缀树上的共享关系让驱逐决策比删除孤立块更复杂。
- 下一课将专门展开多 SubAgent 共享前缀缓存的成立条件和系统视角。
主题讲解 · 01:51
多个 SubAgent 如何共享一份前缀缓存
学习目标
- 能区分 Agent 上下文的“干净”与“可复用”。
- 能解释推理服务为何主要按 token 路径识别公共前缀。
- 能说明不同用户与同一用户修改提示词在缓存树上的等价性。
- 能设计“公共背景在前、任务指令在后”的上下文布局。
- 能说清共享前缀 KV Cache 的收益、条件与安全边界。
前置与衔接
上一课已经看到,跨任务是公共前缀复用的典型来源。
本课把镜头收窄到多 Agent:父 Agent 给多个 SubAgent 相同的背景知识,再分配不同任务时,为什么不必把公共部分重复 prefill 多次?
讨论假设服务端具备跨请求前缀缓存能力,并能用类似 Radix Tree 的结构维护 token 路径。
若后端没有开启前缀缓存、请求配置不兼容,或安全策略禁止跨请求共享,则这里只能作为计算图上的可能性,不能自动变成实际命中。
核心讲解
1. “干净”与“可复用”并不矛盾
给每个 SubAgent 独立上下文,目的是让它专注自己的任务,避免其他分支的中间过程造成干扰。
但独立不意味着所有内容都必须重复。
子任务上下文应同时满足隔离干扰的“干净”和继承必要背景的“可复用”。
原视频 · 00:20 ↗可以把上下文拆成两部分:
SharedPrefix 包含所有子任务都必须知道的系统约束、工具说明或背景材料。
TaskSuffix_i 只包含第 个 SubAgent 的角色、目标和专属输入。
这样既保持分支干净,又为公共前缀制造复用机会。
2. 服务端需要维护跨请求的公共前缀
课程以 SGLang 的 Radix Tree 思路说明:多个请求的 token 序列可以共享树上的前缀路径。
推理服务维护跨请求的公共前缀结构,并主要按 token 序列而不是用户身份识别复用机会。
原视频 · 00:40 ↗若两个请求具有相同的前 个 token,它们可共同指向前缀节点,再从第 个 token 开始分叉。
每个共享节点关联相应的 KV Cache 块或映射。
课程把推理服务称为“无状态”,需要加上引号理解。
这里不是说服务端真的不维护任何状态;恰恰相反,前缀缓存、请求队列和块表都属于状态。
更准确的意思是:KV 数值主要由 token、位置与模型条件决定,而不是由“张三还是李四”这个身份标签决定。
3. 引擎“认 token,不认人”
设用户 A 发送:
用户 B 发送:
两条序列在某个 token 处分叉。
两个用户分别发送相近序列,与同一用户先后修改序列,在推理引擎看来都只是 token ID 路径的分叉。
原视频 · 01:00 ↗从缓存树视角看,这与用户 A 先发送第一句、随后回头修改成第二句具有相同结构:共同走过一段 token 路径,再进入不同后缀。
因此缓存命中不应仅按会话 ID 判断,而要检查可复用前缀本身。
当然,计算上可共享不等于策略上允许共享。
多租户系统仍需执行权限、隐私、加密域和侧信道防护等安全规则。
4. 把必要背景放在分叉之前
假设父 Agent 要启动两个 SubAgent:
- 一个负责检索证据;
- 一个负责撰写总结。
两者都需要同一份项目背景和输出规范。
多个子任务把必要且公共的背景知识放在前缀,再在角色或任务描述处形成不同分支。
原视频 · 01:20 ↗适合缓存的排列是:
- 公共系统约束;
- 公共背景知识;
- 公共工具或格式说明;
- 各自的角色与任务指令;
- 各自的动态输入。
如果把每个 SubAgent 的不同角色描述放在最前面,token 路径会过早分叉,后面即使出现相同背景也难以作为“前缀”直接命中。
所以前缀缓存优化不仅是推理引擎问题,也会反过来影响上下文编排顺序。
5. 为什么只需要一份公共 KV
对同一个模型和完全相同的前缀,第 层前缀 hidden state 相同,因此
公共背景只需生成一份逐层 KV Cache,后续不同子任务从同一前缀节点继续解码。
原视频 · 01:40 ↗若为两个 SubAgent 各存一份相同缓存,只会重复占用显存和 prefill 计算。
共享后,两条请求都从公共前缀末端继续,只为各自不同后缀计算新的 K、V。
收益可粗略理解为:公共前缀越长、复用次数越多,避免的重复 prefill 越多。
但实际收益还受缓存命中、调度、块粒度、数据搬运和并发批处理影响。
6. 共享成立需要同时满足的条件
至少需要检查:
- token ID 前缀逐项一致;
- position ID 与 attention mask 兼容;
- 模型权重、adapter、量化与精度配置兼容;
- 服务端能识别并保留这段前缀;
- 租户和安全策略允许该范围的复用;
- 缓存尚未因容量压力被驱逐。
“文本看起来一样”并不是充分条件,因为 tokenizer 或模板差异可能改变 token 序列。
“同一个父 Agent 创建”也不是必要条件,因为不同请求只要满足以上条件,同样可能命中相同前缀。
7. 上下文隔离仍然要保留
共享公共前缀不意味着多个 SubAgent 共享完整对话历史。
每个分支的任务后缀、工具结果和中间推理应保持独立,除非工作流明确需要同步。
因此比较稳妥的设计是“共享只读背景,隔离动态后缀”。
这既减少重复计算,也降低分支间信息污染。
跟练与练习
原视频练习
编者练习
两个 SubAgent 都要阅读 2,000 token 的项目规范,一个做代码审查,一个写测试。如何排列上下文才能最大化前缀缓存命中?
查看参考答案
把共同的系统约束和 2,000 token 项目规范放在最前面,随后再分别追加“代码审查”和“测试编写”的角色、目标与动态输入。这样两条 token 路径在规范末端之前完全一致,只生成一份公共前缀 KV;若把不同角色放在开头,路径会过早分叉。
编者练习 2
两个请求的公共文本相同,但一个启用了不同 LoRA adapter。为什么不能直接共享 KV Cache?
查看参考答案
adapter 会改变层内投影或 hidden state,因而同一 token 前缀可能产生不同的逐层 K、V。缓存复用要求数值和布局兼容,不能只比较文本。应把模型与 adapter 配置纳入缓存键或隔离缓存域。
常见误区
- 误区:每个 SubAgent 有独立上下文,所以任何 token 都不能共享。纠正:公共只读前缀可复用,动态后缀仍隔离。
- 误区:只有同一用户的请求才能命中同一缓存。纠正:计算上主要取决于 token 与模型条件,但策略还需满足安全边界。
- 误区:“无状态推理服务”不保存任何信息。纠正:这里强调不按用户身份决定数值;前缀树、块表和队列仍是服务状态。
- 误区:相同自然语言字符串一定得到相同 token。纠正:模板、空格、special token 和 tokenizer 都可能改变序列。
- 误区:公共背景放在上下文任何位置都一样。纠正:前缀缓存只能直接复用分叉之前的连续路径。
- 误区:共享前缀会自动让所有后端都更快。纠正:还依赖缓存实现、命中率、调度与内存压力。
本课小结
- 多 Agent 上下文可以同时做到公共背景复用和任务后缀隔离。
- 推理服务按兼容的 token 前缀识别计算复用机会,而非只看用户身份。
- 公共背景应放在各任务分叉之前,才能形成长而稳定的缓存前缀。
- 相同前缀只需保存一份逐层 K、V,各 SubAgent 从末端继续计算。
- 模型配置、安全边界与缓存生命周期都属于复用条件。
- 下一课将从缓存内容转向算子形态:为什么 prefill 主要表现为 GEMM,而单请求 decode 近似 GEMV。
主题讲解 · 03:38
prefill 为何是 GEMM,decode 为何近似 GEMV
学习目标
- 能从 token 维 shape 判断 prefill 与 decode 的线性层算子形态。
- 能追踪 prefill 中 QKV 投影、注意力和 FFN 的矩阵乘法。
- 能解释推理时为什么只消费最后位置的 logits。
- 能说明 decode 中哪些张量追加到 KV Cache,哪些无需保存。
- 能给出“prefill compute-bound、decode memory-bound”的适用边界。
前置与衔接
前面的课程已经说明 KV Cache 如何复用历史 K、V。
本课进一步看这种复用如何改变计算形态。
prefill 一次处理提示词中的多个 token;单请求 decode 每轮只新增一个 token。
同一个权重矩阵面对不同的 token 维长度,会从“大矩阵乘大矩阵”退化成“单行乘权重矩阵”。
这就是 GEMM 与 GEMV 差异的来源,而不是模型在 decode 时换了一套参数。
核心讲解
1. 一眼看懂两阶段的 shape
设隐藏维度为 ,提示词长度为 ,线性层权重为
prefill 输入为
所以计算
是矩阵乘矩阵,即 GEMM。
prefill 同时处理整段 token,核心线性层表现为矩阵乘矩阵;decode 每步只处理一个新 token,表现为向量乘矩阵。
原视频 · 00:20 ↗单请求 decode 每轮只有一个新 token:
于是
可视作向量乘矩阵,即 GEMV,或实现层面的 瘦 GEMM。
课程把两者简称为 GEMM 与 GEMV,是为了突出 token 维从 收缩为 1。
2. prefill 的 QKV 投影是 GEMM
整段提示词的 hidden states 已经同时给出,因此可一次计算
三者都保留 行,每行对应一个 token。
不同 token 的投影互相独立,GPU 可以用高并行度的矩阵核处理。
课程示例省略 batch、head 和多层维度,目的是让 token 维最醒目。
真实实现通常会把 batch、token、head 等维度重排或融合,但不会改变“同时处理多行”的本质。
3. prefill 的注意力核心仍是矩阵乘法
得到 Q、K 后,注意力分数为
prefill 中 Q、K、V 都带有 token 维,QKᵀ 与注意力权重乘 V 都是矩阵乘矩阵。
原视频 · 01:00 ↗加上缩放与 causal mask,再按行做 Softmax:
随后
也是矩阵乘矩阵。
FlashAttention 会分块并在线维护 Softmax 统计量,但最终数学结果仍对应这两次矩阵收缩。
4. prefill 一次完成全部提示词位置
注意力输出还会经过输出投影、残差与 FFN。
FFN 的上投影和下投影同样对 行一起执行。
整段 token 依次经过注意力、输出投影和 FFN,最后一层一次得到全部提示词位置的隐藏状态。
原视频 · 01:40 ↗一层完成后,整段输出再作为下一层的整段输入。
所以 prefill 的并行性是:同一层内的 token 维可并行,层与层仍有前后依赖。
当最后一层得到 个位置的 hidden state 时,prefill 完成,并为每一层留下提示词的 K、V。
5. 训练使用多个 logits,推理只取最后位置
teacher forcing 训练时,多个位置都可以投影到词表并计算下一 token 损失。
例如第 个位置预测第 个 token。
训练可并行使用多个位置的 logits 计算损失,而推理生成下一 token 时只消费最后一个位置的预测。
原视频 · 02:20 ↗推理 prefill 结束时,生成下一 token 只需要最后一个有效位置的 logits。
前面位置的 logits 即使能够算出,也不需要用于这一步采样。
选出新 token 后,把它的 token ID 做 embedding,得到下一轮 decode 的单行输入。
这一步把计算从 行切换为 1 行。
6. decode 只为新 token 计算 Q、K、V
新 token 的 hidden state 分别乘 ,得到
追加到当前层 KV Cache。
decode 只为新 token 计算 Q、K、V;新 K、V 追加进缓存,新 Q 查询所有历史 K 并对历史 V 加权。
原视频 · 03:00 ↗新 query 与历史 key 做
再用权重读取
这些都是一行参与的矩阵运算,单请求视角下具有 GEMV 或瘦 GEMM 特征。
新 hidden state 逐层推进,直到输出下一 token,然后循环。
7. 为什么不重新计算历史 Q
历史位置的 query 已经在它们生成时完成了查询。
未来 token 出现后,因果模型也不会回头更新历史位置的输出。
历史位置的 Q 已经完成各自那一步的查询,后续生成无需把旧 Q 再算一遍,只需保留历史 K、V。
原视频 · 03:20 ↗因此下一轮只需要新 去读取全部历史 K、V。
旧 Q 没有后续用途,通常不会进入 KV Cache。
这同时解释了两个常见问题:
- 为什么缓存叫 KV Cache,而不是 QKV Cache;
- 为什么 decode 不必把整段 token 再做一遍 QKV GEMM。
8. compute-bound 与 memory-bound 是常见倾向,不是绝对定律
prefill 的矩阵较大,权重加载后可参与大量乘加,算术强度通常较高,因此常表现为 compute-bound。
单请求 decode 每步只有一行输入,却仍要读取大量模型权重和越来越长的 KV Cache,数据搬运相对乘加更多,因此常表现为 memory-bound。
但这不是只看阶段名称就能下的绝对结论。
以下因素会改变算术强度:
- batch size 与 continuous batching;
- prompt 或上下文长度;
- GQA/MQA 的 KV head 数;
- 量化精度与 kernel 融合;
- speculative decoding 或一次验证多个 token;
- 硬件带宽、峰值算力和调度效率。
当多个请求被批成多行,decode 的线性层也可再次使用 GEMM。
因此更准确的表述是:单序列、单 token decode 的 很小,常接近 GEMV,并更容易受内存带宽限制。
跟练与练习
原视频练习
编者练习
设 hidden size 为 4096。prefill 长度为 512,decode 每轮新增 1 token。忽略 batch 和 head,写出同一线性层在两阶段的输入 shape,并判断更接近 GEMM 还是 GEMV。
查看参考答案
prefill 输入是 ,乘 权重得到 ,是多行矩阵乘矩阵,属于 GEMM。单请求 decode 输入是 ,乘同一权重得到 ,可视为向量乘矩阵 GEMV,或 的瘦 GEMM。
编者练习 2
为什么把 64 个并发 decode 请求连续批处理后,不能再简单说“decode 一定是 GEMV”?
查看参考答案
把 64 个请求当前 token 的 hidden state 堆成 后,线性层输入重新具有多行,kernel 可以执行 GEMM。每个请求在序列维仍只新增一个 token,但批维提高了矩阵的 ,算术强度和瓶颈也可能变化。
常见误区
- 误区:decode 使用了不同的模型权重。纠正:权重相同,变化的是 token/batch 维 shape 与缓存状态。
- 误区:prefill 的所有 Transformer 层可以并行。纠正:同层 token 可并行,层间仍顺序依赖。
- 误区:KV Cache 让 decode 完全不做注意力计算。纠正:新 query 仍要读取全部历史 K、V。
- 误区:历史 Q 也必须缓存。纠正:旧 query 的查询已经结束,因果生成不会回头更新旧位置。
- 误区:decode 在任何场景都只能调用 GEMV。纠正:批处理或多 token 验证可形成 GEMM。
- 误区:prefill 永远 compute-bound,decode 永远 memory-bound。纠正:这是常见倾向,实际取决于 shape、精度、kernel 与硬件。
本课小结
- prefill 同时处理 个 token,线性层、注意力和 FFN 具有 GEMM 形态。
- 单请求 decode 每轮只新增一个 token,线性层退化为 GEMV 或 瘦 GEMM。
- 推理只消费最后位置 logits,新 token 再进入下一轮。
- decode 追加新 K、V,用新 Q 查询历史缓存,不重算旧 Q。
- compute-bound 与 memory-bound 是受 shape 和硬件影响的性能倾向。
- 下一步可结合 KV Cache 的增长、分页与批处理,分析服务端吞吐和延迟权衡。
主题讲解 · 02:58
看懂 KV Cache 如何在 prefill 与 decode 中增长
学习目标
- 能区分 prefill 与 decode 在 token 维上的并行方式。
- 能解释为什么每一层都要保存自己的 K/V,数值不能跨层复用。
- 能用“面”和“条”理解逻辑增长,同时不把它误解为物理连续内存。
- 能说明训练与 prefill 的相似处,以及“训练≈prefill”的适用边界。
- 能把新增 K/V 与自回归的下一 token 预测连成完整循环。
前置与衔接
建议先理解 causal self-attention 中 query、key、value 的来源,并知道 Transformer block 必须逐层执行。
本课位于“KV Cache 的生命周期与内存管理”单元。它回答“缓存怎么长”,后续还要回答“缓存占多少显存”“如何分页”“何时复用或淘汰”。
核心讲解
1. 先声明:手绘堆叠是逻辑视图
课程用一个三维堆叠图表示 layer、token 与 K/V。
KV Cache 的“面”和“条”是逻辑视图;PagedAttention 或 RadixAttention 下物理显存未必连续。
原视频 · 00:20 ↗图中的“堆一面”“加一条”是为了说明计算依赖,并不要求显存中的地址真的连续。
PagedAttention 会把逻辑连续的 token 映射到若干物理块;RadixAttention 还会围绕前缀复用组织缓存。
所以读图时应分开两件事:
- 逻辑上,第几个 token、哪一层、哪个 KV head 对应哪组 K/V;
- 物理上,这些张量页实际落在显存的哪些块。
只要映射表正确,物理不连续不影响注意力访问。
2. prefill:同一层并行处理整段 prompt
假设 prompt 有 个 token。
在第一层,所有位置的输入 embedding 已知,因此 Q/K/V 投影可以对 个位置并行计算。
虽然 causal attention 会屏蔽未来位置,但这个 mask 不妨碍矩阵形式的一次并行计算。
得到第一层全部 token 的 K/V 后,才能生成第一层输出并送入第二层。
prefill 在同一层并行计算整段 token 的 K/V,再沿网络层逐层形成新的“面”。
原视频 · 01:00 ↗因此课程把每层整段 token 的 K/V 画成一个“面”:
- 一个面内部的多个 token 可以并行算;
- 网络层之间仍有前后依赖,只能从浅层推进到深层;
- 每推进一层,就得到该层自己的整段 K/V。
这里容易忽略一个细节:同一个 token 在不同层虽然都可标成 ,数值并不相同。
原因是每层输入 hidden state 和投影矩阵都不同:
所以缓存必须按层保存,不能把第一层的 K/V 直接给第二层使用。
3. 训练与 prefill 相似,但不是同一件事
课程用“训练约等于 prefill”帮助理解并行结构。
训练阶段的 teacher forcing 会在各位置计算下一词概率和损失,但不把预测词回填为后续输入。
原视频 · 01:40 ↗相似之处是:teacher forcing 已经给出完整目标序列,因此训练时可以在 causal mask 下同时计算多个位置的预测。
训练不需要先采样第一个预测 token,再把它喂回去才能算第二个位置。
但这个类比有三个边界:
- 训练要保留激活并计算梯度,prefill 是纯前向推理。
- 训练需要计算各位置损失,prefill 通常只为后续生成准备状态。
- 常规训练不会把 KV Cache 跨样本、跨优化步骤长期保留。
所以更准确的说法是:训练与 prefill 都能沿 token 维并行;不能据此认为二者的内存生命周期完全相同。
4. decode:每轮新增一个 token 的 K/V
prefill 完成后,模型已有 prompt 的缓存。
第一个 decode 轮次只拿最新 token 的 hidden state 进入第一层,计算该 token 在第一层的 。
decode 每轮只为新 token 计算一组 K/V,并必须先穿过当前层再进入下一层。
原视频 · 02:00 ↗它的 query 会读取该层已有的所有 key/value:
第一层算完后,新的 hidden state 才能进入第二层;因此同一个新 token 仍要逐层向前传播。
当它穿过全部 层后,每一层都新增了一个时间位置的 K/V。
课程把这个过程画成逐层增加的“条”。从整个模型看,一轮 decode 最终会在所有层各追加一个 token 位置;从执行时序看,这些追加随新 token 逐层发生。
5. 自回归闭环:缓存增长是生成结果的副作用
最后一层得到新位置的 hidden state 后,模型把它投影到词表:
经过 softmax 与采样/选取,模型得到真实的下一个 token。
最后一层输出投影到词表得到下一个 token,真实生成后再进入下一轮 decode。
原视频 · 02:40 ↗这个 token 再作为下一轮输入,于是形成循环:
- 读取上轮生成 token;
- 逐层计算它的 Q/K/V;
- 把新的 K/V 追加到各层缓存;
- 用最后一层输出预测下一个 token;
- 重复,直到停止条件触发。
decode 不能像 prefill 那样一次并行算出未来多个 token,根本原因不是 K/V 的存储格式,而是未来 token 尚未生成。
6. 编者补充:用 shape 和显存公式检查直觉
若每层缓存形状近似为
那么 K 与 V 合计的元素数约为
若每个元素占 字节,显存量约为
prefill 会把 从 0 一次推进到 prompt 长度;之后每轮 decode 让 增加 1。
这说明“面”和“条”只是视觉比喻,真正决定显存斜率的是层数、KV head 数、head 维度、批量和当前上下文长度。
跟练与练习
原视频练习
编者练习
一个 32 层模型完成 prompt prefill 后开始 decode。生成一个新 token 时,是“只新增一组 K/V”,还是“每层各新增一组 K/V”?说明执行顺序。
查看参考答案
每层各新增一组 K/V,共 32 层的缓存都会增长一个 token 位置。执行时,新 token 先在第一层生成该层 K/V 和输出 hidden state,再进入第二层,逐层推进到第 32 层;不是 32 层同时无依赖地追加。
编者练习 2
为什么“训练可以并行预测所有位置”不等于“推理可以一次并行生成所有未来 token”?
查看参考答案
训练使用 teacher forcing,目标序列中的前缀 token 已知,所以每个位置的条件输入可以一次构造。推理时未来 token 未知,第 个 token 的输入依赖第 个 token 的真实生成结果,因此存在自回归数据依赖。
常见误区
- 误区:手绘的 KV 立方体就是显存连续布局。纠正:它是逻辑索引,分页实现可以物理离散。
- 误区:同一个 token 在所有层的 K/V 相同。纠正:每层 hidden state 和投影矩阵都不同。
- 误区:prefill 所有层可以同时完成。纠正:token 维可并行,层与层仍需顺序推进。
- 误区:训练会像服务端推理一样长期维护 KV Cache。纠正:相似的是并行计算结构,不是缓存生命周期。
- 误区:decode 每轮只让某一层缓存增长。纠正:一个 token 完成前向后,所有层都增加该位置的 K/V。
本课小结
- prefill 在同一层并行处理整段 prompt,并逐层生成各自的 K/V。
- decode 受自回归依赖约束,每轮只新增一个 token 位置。
- “面”和“条”描述逻辑增长,不等同于物理连续内存。
- 训练与 prefill 都能沿 token 维并行,但训练还要反向传播且通常不跨步保留缓存。
- 下一步可继续学习 PagedAttention、前缀复用和 KV Cache 显存公式。
主题讲解 · 03:30
GQA 共享的是 KV Head,不是 Token
学习目标
- 能准确回答 GQA 中“共享”的对象。
- 能区分 token 轴、query head 轴与 KV head 轴。
- 能写出 query head 到 KV head 的分组映射。
- 能说明 GQA 为什么减少 KV Cache。
- 能比较 MHA、GQA 与 MQA 的存储差异。
前置与衔接
上一课已经从计算形态看过 prefill 与 decode。
本课把注意力内部的 head 维单独展开。
最容易混淆的一句话是“多个 query 共享 K、V”。
这里的“多个”指多个 query head,不是多个 token。
每个 token 仍有自己的 K、V;只是同一 token 在 K/V 侧使用的 head 数少于 Q 侧。
核心讲解
1. 先固定三个轴
设序列长度为 ,query head 数为 ,KV head 数为 ,每头维度为 。
投影和分头后,可以把张量写成
token 轴长度仍是 。
GQA 缩小的是 ,不是 。
GQA 的共享发生在 head 维度:多个 query head 复用较少的 KV head,而不是让不同 token 共用同一份 KV。
原视频 · 00:20 ↗因此“共享 token”这个说法会把两个完全不同的轴混在一起。
2. 投影不会合并不同 token
输入矩阵中的每一行对应一个 token。
它们分别经过 、、:
同一批 token 分别经过 Q、K、V 投影;GQA 改变的是投影后的 head 数量,不会抹去 token 轴。
原视频 · 00:40 ↗同一 token 会得到自己的 Q、K、V 表示。
不同 token 不会因为使用 GQA 就共用同一条 K 或 V。
变化只发生在投影输出的宽度与分头方式上。
3. 四个 Q head 可以只配两个 KV head
看一个最小例子:
分组比例为
也就是每两个 query head 共用一个 KV head。
示例中四个 query head 只对应两个真实的 K/V head,KV Cache 因而按更小的 KV head 数存储。
原视频 · 02:00 ↗一种常见映射是
这里“共用”只表示选择同一个 KV head。
每个 query head 的 query 向量仍不同。
4. 广播是逻辑配对,不一定是物理复制
为了让计算图的 head 数对齐,可以把两个 KV head 想象成逻辑扩展到四份。
计算时可把两个 KV head 逻辑广播到四个 query head;这是一种索引或广播关系,不要求复制四份缓存。
原视频 · 02:20 ↗但高效实现不会为了对齐就把 KV Cache 真复制一遍。
kernel 可以通过索引计算 KV head:
也可以用 view、expand 或专用 grouped-query attention kernel 表达同一关系。
所以“图上画出四份”不等于“显存里存了四份”。
5. 共享 KV 不等于共享注意力权重
对第 个 query head,仍然独立计算
然后按行 Softmax:
再读取对应的 V:
完成 head 配对后,每个 query head 仍独立计算注意力权重,并与其对应的 V head 得到本头输出。
原视频 · 02:40 ↗即使两个 query head 读取同一 K/V,它们的 Q 不同,得到的分数和权重通常也不同。
6. 输出 head 数仍跟随 Q 侧
示例中四个 query head 会产生四个输出 head。
这些输出最后沿 head 维拼接,再经过输出投影。
各 head 的输出最终拼接;MHA、GQA、MQA 的主要差别是实际存储的 KV head 数,而非输出 head 数。
原视频 · 03:00 ↗因此 GQA 压缩的是 K/V 侧,并没有把最终输出 head 数也压成两个。
7. 为什么能省 KV Cache
忽略 batch、层和字节数时,KV Cache 元素量与下式成正比:
其中 2 代表 K 和 V 两份。
若从 MHA 的 改为 GQA 的 ,KV Cache 理论元素量降为原来的四分之一。
token 数 没有减少。
省下的是每个 token、每一层需要保存的 KV head 数。
8. MHA、GQA、MQA 是同一条谱系
| 变体 | 与 的关系 | KV 共享范围 |
|---|---|---|
| MHA | 每个 Q head 有独立 KV head | |
| GQA | 一组 Q head 共用一个 KV head | |
| MQA | 全部 Q head 共用唯一 KV head |
GQA 在注意力表达能力、KV Cache 容量和读取带宽之间取折中。
跟练与练习
原视频练习
编者练习
设 、。每个 KV head 被多少个 query head 复用?query head 19 对应哪个 KV head?head 编号从 0 开始。
查看参考答案
分组大小 。每四个 query head 共用一个 KV head。,所以 query head 19 对应 KV head 4。
编者练习 2
为什么 GQA 的 KV Cache 变小,但不能说注意力输出只有 个 head?
查看参考答案
每个 query head 都独立产生一个输出,只是若干 query head 读取同一个 K/V head。输出 head 数仍是 ,最后按 个 head 拼接;缩小的是缓存中的 K/V head 数。
常见误区
- 误区:GQA 让多个 token 共用一份 KV。纠正:每个 token 仍有自己的 KV,复用发生在 head 维。
- 误区:逻辑广播会把 KV Cache 物理复制到 份。纠正:可用索引或 kernel 映射完成配对。
- 误区:共享 K/V 的 query head 会得到相同注意力权重。纠正:它们的 Q 不同,分数通常不同。
- 误区:GQA 输出 head 数等于 。纠正:输出跟随 query head 数 。
- 误区:任何 、 都能均匀分组。纠正:常规实现通常要求 可被 整除。
- 误区:GQA 减少了上下文 token 数。纠正:它减少的是每个 token 的 KV head 数。
本课小结
- GQA 共享的是 KV head,不是 token。
- Q 保留 个 head,K/V 只保留 个 head。
- 多个 query head 通过分组映射读取同一个 KV head,但各自独立计算注意力。
- 逻辑广播不要求物理复制缓存。
- KV Cache 容量与 成正比。
- MHA、GQA、MQA 可按 KV head 数理解为同一谱系。
主题讲解 · 03:45
因果注意力如何真正跳过上三角计算
学习目标
- 能区分逻辑注意力矩阵与实际物理存储。
- 能解释 prefill 如何在 tile 级跳过未来区域。
- 能说明对角 tile 为什么仍需细粒度 causal mask。
- 能解释 decode 为什么没有未来 K 可计算。
- 能避免把“掩码为零”误说成“所有上三角元素都先算后丢弃”。
前置与衔接
因果注意力要求位置 只能读取位置 。
教科书常画一个完整的 分数矩阵,再把上三角设为负无穷。
这张图用于表达数学约束,却不代表高效 kernel 真会把整个矩阵算出来并存到显存。
本课分别从 prefill 和 decode 看“跳过”在物理执行层意味着什么。
核心讲解
1. 数学上的因果矩阵
注意力 logits 为
其中
Softmax 后,未来位置的权重为 0。
prefill 的因果注意力是下三角有效区,decode 则只新增最底部一行;两者都无需计算未来 token 对应的上三角。
原视频 · 00:20 ↗关键问题不是“最终权重是否为 0”,而是“能否提前知道整片区域无效,从而连乘法和数据搬运都不做”。
2. prefill 的有效区是下三角
prefill 同时有 行 Q 与 行 K。
第 行输出为
输出只由因果掩码内的有效注意力权重与对应 V 相乘得到,纯未来区域不会贡献结果。
原视频 · 01:00 ↗的 V 不应贡献给 。
若先算完整 再掩码,数学正确,但浪费了上三角乘加与中间存储。
3. FlashAttention 按 tile 搬运与计算
FlashAttention 不把完整分数矩阵写回 HBM。
它把 Q、K、V 划分为较小的 tile,装入 GPU 片上存储,再在线维护 Softmax 所需统计量。
FlashAttention 把 Q、K、V 分块送入片上存储;完全位于未来区域的 K/V tile 可不加载、不计算。
原视频 · 01:40 ↗对于一个 Q tile,kernel 能根据 tile 的行列范围判断某个 K/V tile 是否完全位于未来区域。
若整个 tile 都满足 ,就可以:
- 不加载对应 K/V tile;
- 不计算该块 ;
- 不更新在线 Softmax;
- 不计算该块权重乘 V。
因此“跳过”首先是一种块级调度与数据搬运优化。
4. 完全上三角 tile 与对角 tile 不同
设 Q tile 覆盖行 ,K tile 覆盖列 。
若
整个 K tile 都在这些 query 的未来,可以整块跳过。
但穿过主对角线的 tile 同时包含合法元素和未来元素。
完全落在上三角的 tile 可以整块跳过,但穿过对角线的 tile 仍需在块内执行逐元素因果掩码。
原视频 · 02:20 ↗这类 tile 必须先计算合法部分,并在块内对未来元素施加 causal mask。
所以正确表述是:
- 完全被掩码的 tile 可粗粒度跳过;
- 对角 tile 仍有细粒度掩码成本;
- 已经完全落在下三角的 tile 无需因果判断。
5. 在线 Softmax 不需要完整分数矩阵
对每个 query 行,Softmax 可以分块更新最大值与归一化和。
若当前块 logits 为 ,维护运行最大值 与指数和 。
新块到来时先更新
再缩放旧统计量并加入新块贡献。
这使得 kernel 可以边读 tile、边更新输出,不需要保存完整 的 A。
因果 tile 剪枝与在线 Softmax 共同减少计算和显存流量。
6. decode 的“跳过”更直接
在第 步 decode,当前只有一个新 query:
KV Cache 中只有已经生成的
decode 时只有当前 query 与已经存在的 KV Cache;未来 K 尚未生成,因此未来列在物理上根本不存在。
原视频 · 02:40 ↗于是分数只有一行:
未来的 尚未生成,也没有存入缓存。
因此 decode 不是“先构造一行含未来列的数据再掩码”,而是根本没有这些列。
7. decode 仍有别的边界条件
单 token decode 对所有已存在 K 都是因果合法的。
但若一次验证多个候选 token,或采用 speculative decoding 的多 token chunk,chunk 内部仍会重新出现因果边界。
这时执行形态更像一小块增量 prefill,而不是纯单行 decode。
所以“decode 无需 causal mask”应限定在每步单 token、未来 K 不存在的常规情形。
8. 整体三角矩阵只是逻辑拼图
把所有步骤画在一起,可以得到一个下三角矩阵。
三角注意力矩阵是逻辑视图:prefill 分 tile 计算,decode 每步只算一行,并不会把完整矩阵写入显存。
原视频 · 03:20 ↗但真实执行是:
- prefill 以 tile 处理合法区域;
- 对角 tile 做块内掩码;
- 后续 decode 每步增加一条横向分数行;
- 完整矩阵通常不会作为持久张量写入 HBM。
这张逻辑图用于推理依赖关系,不应被误当作实际内存布局。
跟练与练习
原视频练习
编者练习
一个 Q tile 覆盖 query 行 64–95,一个 K tile 覆盖 key 列 96–127。对 causal attention 而言,这个 tile 能否整块跳过?
查看参考答案
可以。最大 query 位置是 95,而最小 key 位置是 96,整个 K tile 都位于这些 query 的未来。该块无合法元素,可以不加载 K/V,也不计算分数。
编者练习 2
若 Q tile 与 K tile 都覆盖位置 64–95,为什么不能整块跳过或整块保留?
查看参考答案
该 tile 穿过主对角线。位置 80 可以读取 key 64–80,但不能读取 81–95。块内同时有合法下三角和非法上三角,必须执行细粒度 causal mask。
常见误区
- 误区:上三角权重最终为 0,所以一定先算过。纠正:完全掩码的 tile 可在乘法前跳过。
- 误区:prefill 的所有上三角都能按大块跳过。纠正:对角 tile 仍需块内掩码。
- 误区:FlashAttention 会把完整 A 写入显存。纠正:它分块并在线维护 Softmax 统计量。
- 误区:decode 也有一个显式 矩阵。纠正:单步只产生一行分数。
- 误区:decode 永远不需要因果掩码。纠正:多 token chunk 内仍有未来位置。
- 误区:跳过只节省算术。纠正:不加载无效 K/V tile 也节省片外到片上的带宽。
本课小结
- 因果注意力的上三角是逻辑无效区。
- prefill 可以整块跳过完全位于未来区域的 tile。
- 穿过对角线的 tile 仍需细粒度 causal mask。
- FlashAttention 不需要物化完整注意力矩阵。
- 单 token decode 中未来 K 尚不存在,所以物理上没有未来列。
- “跳过上三角”要分别从数学掩码、tile 调度与存储布局理解。
主题讲解 · 03:23
PagedAttention 如何把连续逻辑映射到离散显存块
学习目标
- 能区分预留浪费、内部碎片与外部碎片。
- 能解释逻辑 KV 块与物理显存块的关系。
- 能读懂 block table 的地址映射。
- 能说明分页为什么提高显存利用率。
- 能指出 PagedAttention 没有消除的尾块浪费。
前置与衔接
KV Cache 会随请求逐 token 增长。
若每条请求都要求一整段连续显存,并提前为最大长度预留,服务端很快会遇到碎片与浪费。
PagedAttention 借用了虚拟内存分页的核心思想:
逻辑序列保持连续,物理显存允许离散,再用映射表把两者连接起来。
核心讲解
1. 连续分配为什么会预留过多
服务端通常不知道一条请求最后会生成多少 token。
若按最大长度一次性申请连续 KV 空间,请求提前遇到 EOS 时,后半段预留永远不会使用。
传统连续 KV Cache 常按最大生成长度预留空间;请求提前结束时,尚未使用的尾部预留就成为浪费。
原视频 · 00:20 ↗设最大生成长度为 ,实际生成长度为 ,每 token 的逐层 KV 大小为 。
单请求预留浪费可粗略写成
并发请求越多,这种“为了可能的未来而占住显存”的成本越大。
2. 内部碎片与外部碎片不是一回事
内部碎片指已经分配给某个请求的连续区域内部,有一部分没有被实际 token 使用。
外部碎片指多个已分配区域之间出现空洞;空闲总量可能足够,却找不到一段足够长的连续区间。
连续分配既可能在请求内部留下未使用尾部,也可能让请求之间的空洞无法容纳下一段连续 KV。
原视频 · 01:00 ↗例如三个空洞分别有 2、3、4 个单位,总空闲是 9。
若新请求必须连续申请 6 个单位,仍会失败。
问题不在总量不足,而在连续性约束。
3. 先把逻辑 KV Cache 切成固定块
PagedAttention 将一条请求的 token 序列按固定 token 数分块。
若 block size 为 ,逻辑 token 所在逻辑块为
块内偏移为
PagedAttention 先把每条序列的 KV Cache 划分为固定大小的逻辑块,逻辑 token 顺序仍保持连续。
原视频 · 01:40 ↗逻辑块 0、1、2 仍按 token 顺序排列。
这层连续性属于请求视角,不要求物理地址也连续。
4. 物理块可以离散
显存池被划分成同样大小的物理块。
逻辑块 0 可以落在物理块 3,逻辑块 1 可以落在物理块 11。
逻辑上相邻的 KV 块可以落到不相邻的物理显存块,连续性由映射表维护,而不再要求物理连续。
原视频 · 02:20 ↗只要运行时知道映射关系,注意力读取时仍能按照逻辑 token 顺序访问 K、V。
这与“把分散数据重新复制成连续大数组”不同。
分页的价值恰恰是避免这种大规模整理与搬迁。
5. block table 完成地址翻译
每个请求维护自己的 block table。
它记录
块表把逻辑块号翻译为物理块号,使注意力内核能够按序读到分散存放的 KV。
原视频 · 02:40 ↗访问 token 时:
- 计算逻辑块号 ;
- 查表得到物理块号 ;
- 保留块内偏移 ;
- 在对应物理块内读取该 token 的逐层 K/V。
因此 block table 是顺序语义与离散显存之间的桥梁。
6. 新 token 按需获得新块
当前物理块未写满时,新 token 继续写入该块。
写满后,分配器从全局空闲池取任意一个物理块,并把新映射追加到 block table。
不需要提前申请完整最大长度,也不需要新块紧挨着旧块。
这把分配粒度从“整条可能很长的序列”缩小为“一个固定块”。
7. 为什么显存利用率更高
新请求可以占用任意空闲物理块,显著减少预留浪费与外部碎片;最后一个块仍可能有至多一个块的尾部空闲。
原视频 · 03:00 ↗分页带来三项主要收益:
- 几乎消除按最大长度预留整段空间的浪费;
- 允许新请求复用任意空闲物理块,显著缓解外部碎片;
- 请求增长时按需追加块,减少搬迁和重新分配。
它没有减少模型数学上需要保存的有效 KV 元素。
它减少的是分配策略造成的额外浪费。
8. 尾块内部碎片仍然存在
若 block size 为 ,请求长度不是 的整数倍,最后一个块会有空槽。
对单条活跃序列,尾部最多浪费 个 token 槽位。
因此 block size 有权衡:
- 块大:block table 更短,地址管理更简单,但尾块浪费可能更大;
- 块小:内部碎片更小,但映射表、调度和 kernel 管理成本更高。
PagedAttention 是把不可控的大段浪费约束成有限的块尾浪费,不是让碎片严格归零。
9. PagedAttention 与注意力数学分开理解
注意力公式仍是
分页改变 K、V 的物理寻址方式,不改变逻辑 token 顺序或注意力结果。
高效 kernel 会按 block table 收集需要的物理块并完成计算。
所以 PagedAttention 更接近“KV Cache 内存管理与配套 kernel”,而不是一种新的注意力概率定义。
跟练与练习
原视频练习
编者练习
block size 为 16 token,一条请求当前有 35 个 token。它占用多少个物理块?最后一块有多少空槽?
查看参考答案
需要 个物理块。前两块存 32 个 token,最后一块存 3 个 token,因此尾块还有 个空槽。
编者练习 2
请求的逻辑块 0、1、2 分别映射到物理块 7、2、11。读取逻辑 token 34,block size 为 16,应访问哪个物理块和块内偏移?
查看参考答案
,所以逻辑块号为 2;,块内偏移为 2。查 block table 得逻辑块 2 对应物理块 11,因此访问物理块 11 的偏移 2。
常见误区
- 误区:PagedAttention 减少了有效 KV 的数学数量。纠正:它主要减少分配与碎片浪费。
- 误区:逻辑块必须映射到连续物理块。纠正:物理块可以任意离散。
- 误区:block table 会把数据重新复制成连续数组。纠正:它做地址翻译,kernel 按映射读取。
- 误区:分页消除了所有内部碎片。纠正:最后一个物理块仍可能未填满。
- 误区:block 越小一定越好。纠正:小块会增加映射、调度和 kernel 管理开销。
- 误区:PagedAttention 改变了注意力公式。纠正:它改变的是 KV 的存储与访问方式。
本课小结
- 连续 KV Cache 会产生预留浪费、内部碎片和外部碎片。
- PagedAttention 把逻辑序列切成固定大小的块。
- 逻辑块连续,物理块可以离散。
- block table 负责逻辑块号到物理块号的翻译。
- 按需、按块分配显著提高显存利用率。
- 最后一个块仍可能有有限的尾部内部碎片。
主题讲解 · 02:38
一条公式算清 KV Cache 显存
学习目标
- 能从 KV Cache 的几何形状推导显存公式。
- 能说明公式中每个乘数的物理含义。
- 能正确使用 KV head 数而不是盲用 Q head 数。
- 能复算 Llama2-7B 的 128 GiB 示例。
- 能处理不同请求长度与不同数据类型的变体。
前置与衔接
前面的课程已经说明 decode 为什么缓存 K、V,以及 GQA 如何减少 KV head 数。
现在把这些 shape 乘起来,就能估算服务端最重要的动态显存之一。
先记结论:
真正重要的不是背公式,而是能把每一项在张量中找到。
核心讲解
1. 把单请求、单层缓存看成立方体
单层中,每个 token 都有 K 和 V。
先只看 K,它的典型形状可以写成
长度是 token 数 ,另外两个方向组成每个 token 的 KV 宽度。
把单个请求的 KV Cache 看成按 token、层和 head 展开的立方体,再把 batch 中多个请求并排堆叠。
原视频 · 00:20 ↗再复制一份给 V,于是出现最前面的系数 2。
2. 加上层数与 batch
每一层都有自己的 K、V,不能跨层混用。
所以乘 Transformer 层数 。
并发 batch 中每条请求也有独立缓存,所以再乘请求数 。
立方体的长度来自序列 token 数,高度来自层数,宽度来自 KV head 数与每个 head 的维度。
原视频 · 00:40 ↗若所有请求都补齐到同一长度 ,元素总数为
3. 元素数还不是字节数
张量元素必须乘每个元素占用的字节 :
KV Cache 显存由 K/V 两份、batch、序列长度、层数、KV head 数、head 维度和元素字节数共同决定。
原视频 · 01:00 ↗常见取值包括:
| 数据类型 | 每元素字节数 |
|---|---|
| FP32 | 4 |
| FP16 / BF16 | 2 |
| FP8 / INT8 KV | 1 |
实际系统还可能有 scale、元数据、对齐和分配器开销;基础公式只计算 KV 主体。
4. 为什么一定要用
MHA 中 ,所以有些旧公式直接写 attention head 数。
但 GQA 与 MQA 中 K/V head 更少。
正确公式应使用模型配置里的 num_key_value_heads。
若误用 num_attention_heads,会把 GQA/MQA 的缓存显著高估。
Llama2-7B 使用 MHA,,所以示例中写 32 没有冲突。
5. BF16 为什么再乘 2
BF16 每个元素占 16 bit,也就是 2 byte。
BF16 每个元素占 2 字节;模型结构体积乘以元素字节数后才得到实际显存占用。
原视频 · 01:40 ↗注意公式开头和结尾的两个“2”来源不同:
- 开头的 2:K 与 V 两份张量;
- 结尾的 2:BF16 每个元素 2 byte。
漏掉任何一个,结果都会差一倍。
6. 代入 Llama2-7B 示例
视频示例取:
Llama2-7B 示例代入 32 层、32 个 KV head、head_dim 128,并与 batch 256、序列 1024 一起计算。
原视频 · 02:00 ↗代入得到
把每项写成 2 的幂:
7. 128 GB 还是 128 GiB
示例总量为 字节,即 字节,也就是 128 GiB。
原视频 · 02:20 ↗视频板书写 128 GB,是工程口语中的常见写法。
严格区分单位时:
- 1 GiB = byte;
- 1 GB = byte。
所以该二进制计算结果应称 128 GiB,约等于 137.44 GB。
8. 请求长度不齐时不要机械用
连续 batching 中,各请求长度可能分别为 。
有效 KV 主体更准确地写成
若系统把请求 padding 到统一 ,才直接使用 。
PagedAttention 会按块分配,实际占用还需把每条长度向上取整到 block size,并加入 block table 等元数据。
9. 每新增一个 token 增加多少显存
单条请求每新增一个 token,会在每层新增一份 K 和一份 V:
这条增量公式适合估算 decode 中缓存增长速度。
若 batch 中同时有 条请求各生成一个 token,则乘 。
它也解释了为什么长上下文、深层数和大 KV head 数会迅速推高显存。
跟练与练习
原视频练习
编者练习
某模型有 32 层、8 个 KV head、head_dim 128,KV 使用 BF16。单请求每新增一个 token,KV Cache 增加多少字节?
查看参考答案
byte,即 128 KiB。两个 2 分别来自 K/V 两份与 BF16 的 2 byte。
编者练习 2
上题模型有三条请求,当前长度分别为 100、200、300。忽略分页对齐与元数据,缓存主体是多少 MiB?
查看参考答案
总 token 数为 600。每 token 是 128 KiB,所以总量 KiB = 76800 KiB = 75 MiB。这里应使用长度和,而不是用最大长度 300 乘 batch 3。
常见误区
- 误区:公式中的第一个 2 是 BF16。纠正:第一个 2 表示 K 和 V;字节数是最后的 。
- 误区:所有模型都用 Q head 数计算缓存。纠正:GQA/MQA 必须用 。
- 误区:参数量越大,KV Cache 一定同比例变大。纠正:缓存主要由层数、KV heads、head_dim 与上下文决定。
- 误区:128 GB 与 128 GiB 完全相同。纠正:视频结果严格说是 128 GiB。
- 误区:并发请求长度不同时仍直接用 表示有效缓存。纠正:有效主体应按 计算。
- 误区:基础公式就是运行时总显存。纠正:还可能有分页对齐、量化 scale、元数据和 workspace。
本课小结
- KV Cache 主体公式是 。
- 每一项都能对应到 K/V、请求、token、层、KV head、head 维和元素字节。
- GQA/MQA 要使用 KV head 数。
- Llama2-7B 示例结果为 byte,即 128 GiB。
- 长度不齐时可用 替代 。
- 单请求每新增一个 token,缓存增加 字节。
主题讲解 · 03:05
多轮对话如何把增量 Prefill 并入 KV Cache
学习目标
- 能画出首轮 prefill、decode 与下一轮增量 prefill 的顺序。
- 能说明三阶段分别追加多少行 K/V。
- 能解释 Q 为什么不进入 KV Cache。
- 能写出增量 prefill 的可见 K 范围。
- 能说明新 prompt 块内部为什么仍需因果掩码。
前置与衔接
单轮生成的流程很熟悉:提示词先做 prefill,随后逐 token decode。
多轮对话多了一步:模型回复结束后,用户又提交一段包含多个 token 的新 prompt。
这段新 prompt 不应让旧上下文从头重算。
系统会保留旧 KV Cache,只对新增 prompt 做 incremental prefill,再继续 decode。
核心讲解
1. 多轮流程不是 prefill 后永远 decode
设首轮用户 prompt 有 个 token,模型回复生成 个 token,下一轮用户 prompt 有 个 token。
时间顺序是:
多轮对话在首轮 prefill 和若干次 decode 之后,会遇到新的用户 prompt:这段新 token 的 KV 应怎样并入旧缓存?
原视频 · 00:20 ↗“增量”表示旧前缀不重算,只处理新来的 个 token。
2. prefill 与 decode 的追加粒度
首轮 prefill 会为每层一次产生 行 K 和 V。
常规单 token decode 每步只产生 1 行新 K 和 V。
下一轮 incremental prefill 则一次产生 行 K 和 V。
prefill 一次产生多行 Q/K/V,decode 每步只产生一个 token 的 K/V;两者的区别主要在增量粒度。
原视频 · 00:40 ↗所以三阶段使用同一组 Transformer 权重,主要差别是新 token 块的行数。
3. 首轮 prefill 建立缓存前缀
对首轮输入 ,每层计算
注意力使用 causal mask,完成所有提示词位置的隐藏状态。
该层结束时,把 保存到缓存。
不保存,因为这些 query 已经完成了自己的读取任务。
4. decode 每步追加一行
第一个回复 token 到来时,当前层只计算
新 query 读取旧缓存和当前新 key:
decode 的当前 query 查询旧缓存和新 K,得到一行注意力输出;新 K/V 随后追加到缓存尾部,Q 不进入缓存。
原视频 · 01:40 ↗随后把 追加到缓存尾部。
下一步 decode 会看到更长一行的 K/V。
这种追加在每一层分别发生。
5. 下一轮新 prompt 一次追加多行
设模型已完成回复,此时旧缓存长度为
新用户 prompt 有 行 hidden states。
增量 prefill 为这些新行计算
下一轮用户 prompt 包含多个 token 时,增量 prefill 会一次追加多行 K/V,并让多行 Q 查询旧前缀与新块中的可见位置。
原视频 · 02:00 ↗逻辑缓存直接扩展为
这里的分号表示沿 token 轴拼接。
若使用 PagedAttention,物理块不必连续;“拼接”仍是逻辑顺序。
6. 新 Q 可以看见哪些 K
新 prompt 中第 个 token 的 query 可以读取:
- 全部 个旧 K;
- 新 prompt 中从第 1 个到第 个 K;
- 不能读取新 prompt 中第 个及更后的 K。
若新块长度为 ,其注意力分数形状为
左侧旧前缀矩形区域全部可见,右侧新块区域只有下三角可见。
7. incremental prefill 仍需要 causal mask
同一批新 prompt token 是并行投影出来的。
若不在新块内部加 causal mask,较早 token 会读取较晚 token,造成未来信息泄漏。
增量 prefill 的新块内部仍需因果掩码:较早的新 token 不能读取同一块中更晚 token 的 K/V。
原视频 · 02:20 ↗掩码可以写成
具体索引是否从 0 开始会改变边界写法,但语义不变:新 token 只能看旧前缀和新块中不晚于自己的位置。
8. 为什么单 token decode 看起来不需要显式 mask
decode 时 。
缓存中只有过去 K 和当前 K,未来 K 尚不存在。
因此唯一一行 query 的所有现存列都合法,因果边界由“只存已生成 token”隐式保证。
当一次处理多个 token 时,未来列重新出现在同一计算块里,所以要显式掩码。
9. 阶梯图如何阅读
把多轮过程的所有 query 行按时间堆起来,会看到阶梯形有效区域。
多轮过程在逻辑上形成阶梯:首轮 prefill 是三角块,decode 是单行,下一轮增量 prefill 再接一个带因果边界的矩形块。
原视频 · 02:40 ↗- 首轮 prefill:一个下三角块;
- 每次 decode:新增一行,读取全部已有 K;
- 下一轮 incremental prefill:新增一个矩形块,其中旧前缀区全可见、新块区下三角可见。
这张阶梯图表达逻辑依赖,不代表完整注意力矩阵会被长期存入显存。
10. 缓存追加还要满足模板一致性
真实聊天系统会把 system、user、assistant 分隔符都编码成 token。
若下一轮重新序列化后的旧前缀与缓存创建时不完全相同,不能盲目追加。
可靠实现必须确认 token 级前缀匹配,再从匹配边界开始 incremental prefill。
这也是 prefix cache、chat template 与 KV Cache 正确性之间的连接点。
跟练与练习
原视频练习
编者练习
旧缓存长度为 10,新用户 prompt 有 3 个 token。增量 prefill 的注意力分数矩阵是什么 shape?第三个新 token 可以读取多少个 K?
查看参考答案
分数矩阵 shape 为 。第三个新 token 能读取 10 个旧 K 和新块中前 3 个 K,共 13 个。第一个和第二个新 token 分别只能读取 11、12 个。
编者练习 2
为什么新 prompt 的三行 Q 不加入缓存,而三行 K/V 要加入?
查看参考答案
这三行 Q 在本次增量 prefill 中已经完成查询,后续 token 不会使用历史 Q。未来 token 需要用自己的新 Q 去读取所有历史 K/V,因此 K/V 必须保留并沿 token 轴追加。
常见误区
- 误区:多轮对话只在第一次做 prefill。纠正:每段新用户 prompt 都会触发增量 prefill。
- 误区:增量 prefill 要重算整个旧对话。纠正:token 前缀一致时只计算新增部分。
- 误区:incremental prefill 只追加一个 K/V。纠正:它一次追加新 prompt 的多行 K/V。
- 误区:新块能双向读取,因为它们一起输入。纠正:自回归模型的新块内部仍需 causal mask。
- 误区:Q、K、V 都要缓存。纠正:后续只需要历史 K/V,旧 Q 无后续用途。
- 误区:逻辑拼接要求物理显存连续。纠正:分页缓存可通过块表保持逻辑顺序。
本课小结
- 多轮对话交替经历 prefill、decode 与 incremental prefill。
- decode 每步追加一行 K/V,新 prompt 增量 prefill 一次追加多行。
- Q 只用于当前层查询,不进入 KV Cache。
- 新 prompt 的每行 Q 能读取全部旧 K 和新块中不晚于自己的 K。
- 新块内部仍需 causal mask。
- 多轮逻辑注意力可画成阶梯,但完整矩阵通常不会物化。
主题讲解 · 01:10
从前缀树到分页块:读懂 SGLang 的 KV Cache
学习目标
- 能用前缀树解释多请求 KV Cache 复用。
- 能指出共享从哪个 token 开始失效。
- 能说明一个 token 节点包含逐层 K/V。
- 能区分逻辑前缀树与物理分页映射。
- 能解释固定 block size 带来的尾块对齐空闲。
前置与衔接
前面的课程已经说明:只有完全相同的 token 前缀才能安全复用逐层 KV。
PagedAttention 又说明:逻辑连续的缓存不必放在连续物理显存中。
SGLang 的可视化把这两个视角叠到一起:
- 上层用前缀树表达哪些请求共享 token 前缀;
- 下层用分页块表达这些 KV 实际怎样落入显存池。
核心讲解
1. 把请求画成前缀树
假设两条请求都以同一个系统提示词开始:
系统提示词 token 完全相同,因此两条请求先沿同一条主干前进。
SGLang 可把 token 序列画成带 KV 的前缀树:系统提示词形成公共主干,不同用户内容从分叉点继续。
原视频 · 00:00 ↗遇到不同用户输入后,token 序列分叉。
树上的公共路径就是可复用前缀,分支是各请求独有的后缀。
2. 为什么公共前缀能共享 KV
因果 Transformer 中,位置 的逐层表示只依赖 token 0 到 。
若两条请求的前 个 token 完全相同,并且模型、位置编码与推理配置一致,那么公共前缀上对应的逐层 K/V 相同。
两条请求拥有完全相同的系统提示词时,公共前缀 token 对应的逐层 KV Cache 可以直接复用。
原视频 · 00:15 ↗第二条请求无需为这段系统提示词重新 prefill。
它可以引用已经存在的 KV Cache,再从自己的第一个新 token 开始增量计算。
这同时减少重复计算与重复缓存。
3. 共享的不是“相似语义”
前缀复用依据 token ID 序列的一致性,不是句子意思相近。
下面这些变化都可能让 token 序列不同:
- 多一个空格或换行;
- chat template 不同;
- system/user/assistant 分隔符不同;
- tokenizer 或模型版本不同;
- 位置起点或 RoPE 配置不同。
所以缓存命中必须做精确前缀匹配,不能用语义相似度替代。
4. 一个 token 节点挂着所有层的 KV
板书把 token 下方画成一串小 KV 块。
树上的一个 token 节点不是一对孤立向量,而是包含模型每一层对应的 K/V 条目。
原视频 · 00:30 ↗这不是说一个 token 只有一对 K/V 向量。
对包含 层的模型,每个 token 在每一层都有 K 和 V:
若使用 GQA,每层的 K/V 还包含 个 head。
因此树上的“一个 token 节点”代表一组逐层缓存记录。
5. 第一个不同 token 就是分叉边界
设两条序列分别为
和
且 。
从第一个不同 token 开始,两条请求的隐藏状态与后续逐层 KV 不再相同,缓存树也在此处分叉。
原视频 · 00:45 ↗位置 到 可以共享。
从位置 开始,两条请求的 hidden state 不同,后续逐层 K/V 也通常不同。
因此共享边界是最长公共 token 前缀的末端。
6. 树结构与引用计数
多个活跃请求可以同时引用同一公共前缀。
系统需要记录哪些缓存块仍被请求引用。
只有当引用该前缀的请求都结束或缓存策略决定淘汰时,公共块才可回收到空闲池。
这解释了为什么前缀缓存不只是“查到就用”,还涉及生命周期、引用和淘汰策略。
7. 前缀树是逻辑层,分页块是物理层
前缀树回答:
分页映射回答:
逻辑前缀还需映射到固定大小的物理 KV 块;以 16 token 为例,长度不是块大小整数倍时会留下尾块对齐空闲。
原视频 · 01:00 ↗同一条逻辑路径上的相邻 token 不必位于相邻显存地址。
运行时通过块表或内存池元数据找到对应物理位置。
不要把树边误解成物理指针必须连续。
8. block size 为 16 只是示例
视频用 16 token 一块来说明分页。
若 block size 为 ,长度 18 的前缀需要
个物理块。
第二块只用 2 个 token 槽位,剩余 14 个槽位暂时空闲。
具体系统和配置的 block/page size 可能不同,16 不是 SGLang 在所有场景中的固定常数。
9. 对齐空闲与共享收益要同时看
固定块让物理分配和复用更容易,但最后一个块可能未填满。
若前缀在块中间分叉,两个分支是否能共享部分块,取决于实现的页粒度、缓存索引和 copy-on-write 策略。
因此有两个不同粒度:
- token 前缀匹配粒度决定逻辑上可复用到哪里;
- page/block 粒度决定物理上怎样引用、扩展或复制。
不能只看树形图就假设显存会按 token 无限细粒度共享。
10. 命中前缀后的执行过程
新请求到来时,可以按以下顺序理解:
- tokenizer 与 chat template 生成精确 token 序列;
- 在缓存索引中查找最长公共前缀;
- 引用命中的逐层 KV 块;
- 对未命中的后缀执行 incremental prefill;
- 把新 KV 写入已有尾块或新分配的物理块;
- decode 时继续沿分支扩展。
这一流程把“前缀复用”和“分页增长”连接成完整系统视角。
跟练与练习
原视频练习
编者练习
请求 A 的 token 为 [10, 20, 30, 40],请求 B 为 [10, 20, 31, 40]。两条请求能共享多少个 token 的 KV?
查看参考答案
只能共享前两个 token [10,20]。第三个 token 已经不同,位置 2 的逐层状态与后续 KV 都从这里分叉。即使第四个 token ID 又同为 40,它前面的上下文不同,也不能重新合并为同一缓存节点。
编者练习 2
block size 为 16,一个公共前缀长度为 35 token。至少需要多少个块,最后一块有多少空槽?
查看参考答案
需要 个块。前两块容纳 32 个 token,最后一块用 3 个槽位,还剩 13 个空槽。这个尾部空闲是分页对齐成本,不否定前缀共享收益。
常见误区
- 误区:意思相近的 system prompt 可以共享 KV。纠正:必须精确匹配 token 前缀及推理配置。
- 误区:一个 token 只对应一对 K/V。纠正:每个 Transformer 层都有该 token 的 K/V。
- 误区:分叉后遇到相同 token 就能重新合并。纠正:其历史上下文不同,逐层状态通常不同。
- 误区:前缀树节点就是一块连续物理显存。纠正:树是逻辑索引,物理由分页映射管理。
- 误区:16 token 是任何 SGLang 配置的固定 page size。纠正:视频只是用 16 举例。
- 误区:分页后完全没有浪费。纠正:尾块仍可能存在对齐空闲与元数据开销。
本课小结
- SGLang 的 KV Cache 可用前缀树理解共享关系。
- 相同 token 主干复用逐层 KV,第一个不同 token 处开始分叉。
- 每个 token 节点代表所有层的 K/V,而非单一向量。
- 前缀树负责逻辑复用,分页块负责物理存储。
- block size 会带来有限尾块对齐空闲。
- 新请求命中最长公共前缀后,只需对未命中后缀做增量 prefill。
主题讲解 · 03:43
为什么公共前缀能够复用相同的 KV Cache
学习目标
- 能用逐层归纳证明 decoder-only Transformer 的相同公共前缀产生相同 K/V。
- 能解释位置映射、逐 token 投影与因果掩码分别承担什么作用。
- 能说明差异为什么只能沿因果方向向后传播,不能反向污染前缀。
- 能判断 RoPE、多头注意力和不同总序列长度是否破坏结论。
- 能列出前缀 KV 复用所需的模型状态、位置编号、掩码与数值实现前提。
前置与衔接
考虑同一个 decoder-only Transformer 接收两条序列:
前 个 token ID 完全相同,之后可以不同,甚至两条序列总长度也可以不同。
我们要证明:在满足相同模型状态、相同位置编号与相同因果掩码等条件时,每一层公共前缀位置的 K/V 相同,因此可以复用同一份 KV Cache。
证明不是从“字符串看起来相同”直接跳到“缓存相同”,而是逐层追踪隐藏状态如何计算。
核心讲解
1. 逐 token 子层不会混合序列位置
对某层输入矩阵
线性投影对每一行应用相同权重:
第 行只由 决定:
线性投影、逐元素激活和 FFN 对序列的每一行应用同一映射,本身不会让不同 token 位置互相混合。
原视频 · 00:20 ↗逐元素激活、RMSNorm/LayerNorm(沿单个 token 的特征维归一化)和标准 FFN 同样不跨 token 行混合。
真正把不同序列位置汇聚到一起的是 self-attention。
2. 找到首个不同位置
假设首个不同位置是 。
在嵌入层,公共前缀使用相同 token ID 和相同 position ID,因此
首个差异及之后的位置可能不同。
两条序列在首个差异位置之前的嵌入行相同;首层 Q、K、V 的对应前缀行也保持相同。
原视频 · 01:00 ↗视频例中前两个 token 是公共前缀,第三个 token 首次不同。
即使第四个 token 的 token ID 恰好再次相同,它的隐藏状态也未必相同,因为它已经读取了不同的第三个 token。
所以“公共前缀”必须是从序列起点连续相同的一段,不能把差异之后偶然相同的 token 也算进去。
3. 首层公共前缀的 Q、K、V 相同
由逐位置线性投影可知,对 :
后续不同位置的 Q/K/V 可以不同,但它们是否会影响公共前缀,还要看 causal mask。
4. 因果掩码阻止未来差异回流
位置 的注意力分数是
标准因果掩码满足
因此公共前缀中的位置 只能读取 的 K/V。
这些历史位置也全部处在公共前缀内,两个序列的 Q、K、V 对应行相同。
因果掩码遮住未来列,使公共前缀位置的注意力行只能读取相同的历史,后续差异无法反向污染前缀。
原视频 · 01:40 ↗故公共前缀位置的合法分数、softmax 权重和注意力输出逐项相同。
如果去掉 causal mask,前缀位置可能看到后面的差异,结论一般不再成立。
5. 差异可以向后传播,但不能向前传播
首个不同 token 能改变自己及后续 token 的注意力分数与 value 加权和。
所以从首个差异位置开始,隐藏状态通常都会分叉。
首个差异位置之后的注意力输出可以被不同历史影响,而公共前缀行仍相同;FFN 不会再次跨位置传播。
原视频 · 02:20 ↗而公共前缀行看不到未来差异,保持相同。
后接的输出投影、残差连接、归一化与 FFN 都是逐位置操作,因此不会把后缀差异重新混回前缀行。
6. 用层归纳完成证明
归纳假设:第 层输入的公共前缀隐藏状态相同:
则有:
- 逐位置投影得到相同的公共前缀 Q/K/V;
- 因果掩码让位置 只读取相同的 ;
- 注意力输出在公共前缀相同;
- 残差、归一化和 FFN 保持公共前缀相同。
所以
把上一层公共前缀表示相同作为归纳假设,下一层的 Q、K、V、注意力和 FFN 仍保持这些前缀行相同。
原视频 · 02:40 ↗由嵌入层的基础情形出发,结论对所有 Transformer 层成立。
每一层缓存的公共前缀 K/V 因而相同。
7. RoPE 为什么不破坏结论
RoPE 对位置 的 Q/K 应用由 position ID 决定的旋转:
若两条序列的公共前缀使用相同 position ID,则相同 Q/K 乘上同一个旋转矩阵,结果仍相同。
注意力内积还可写为
其中相对旋转只依赖相同的位置差 。
公共前缀使用相同位置编号时,RoPE 对相同 Q、K 应用相同旋转,注意力分数与缓存相等性不被破坏。
原视频 · 03:20 ↗因此 RoPE 本身不会破坏公共前缀缓存相等性。
但如果同一 token 前缀被放到不同绝对 position ID,旋转后的 K 通常不同,不能直接按字节复用。
8. 多头与总长度不同为什么仍成立
多头注意力只是对各头分别应用相同论证。
每个头的公共前缀 Q/K/V 和注意力输出相同,拼接后再经 的结果也相同。
两条序列的总长度不同不影响已经计算的公共前缀,因为因果位置看不到未来追加的 token。
这正是 prefix caching 能让多个请求共享相同提示词前缀缓存的数学基础。
9. 工程上“相同”需要哪些前提
语义上的相同 K/V 至少需要:
- 同一模型权重、adapter、量化配置和推理模式;
- 相同 tokenizer 输出的 token ID,而不只是肉眼相同文本;
- 相同 position ID、RoPE scaling 和上下文位置约定;
- 相同 attention mask、prefix mask 或 packed-sequence 边界;
- 相同会影响隐藏状态的外部条件,例如 cross-attention memory;
- 关闭 dropout 等随机训练行为。
浮点 kernel、设备和归约顺序不同还可能造成末位数值差异。
所以工程缓存通常以“请求条件完全匹配并复用已算出的同一缓存块”为准,而不是分别重算后要求文件哈希绝对相同。
10. 结论不适用的结构
以下情况不能直接套用:
- BERT 式双向 self-attention,前缀会读取后续 token;
- 沿序列维做全局归一化或其他跨 token 运算;
- 相同文本经过不同 tokenization;
- prefix-LM 使用了不同的可见性掩码;
- 同一前缀被重新映射到不同位置编号;
- 模型状态或外部 memory 不同。
判断能否复用缓存,必须检查真实计算图,而不是只比较字符串前缀。
跟练与练习
原视频定位
编者练习
两条序列的前 100 个 token ID 相同,但一条从 position ID 0 开始,另一条从 position ID 50 开始。使用标准 RoPE 时,能否直接共享这 100 个位置的旋转后 K Cache?
查看参考答案
一般不能。虽然 token ID 相同,但对应位置使用的旋转矩阵 不同,旋转后的 K 会变化。只有框架采用可证明等价的重定位机制并做了相应变换时,才能另行讨论复用;普通 prefix cache 要求 position ID 约定一致。
编者练习 2
为什么两个序列在第三个 token 不同、第四个 token ID 再次相同时,第四个位置的 KV 仍通常不同?
查看参考答案
第四个位置的 query 会通过因果注意力读取前三个位置。第三个 token 已经不同,因此第四个位置的注意力输入上下文不同,隐藏状态与后续投影出的 K/V 通常也不同。公共前缀只能延伸到首次差异之前。
常见误区
- 误区:相同字符串必然产生相同缓存。纠正:还要相同 token ID、位置编号、掩码、模型状态和外部条件。
- 误区:线性层会把序列各行互相混合。纠正:标准投影和 FFN 逐位置计算;跨位置混合来自 attention。
- 误区:后缀不同会改变前缀 attention。纠正:标准 causal mask 阻止前缀读取未来后缀。
- 误区:差异之后相同的 token 也可复用。纠正:它已经处在不同历史上下文中。
- 误区:RoPE 一定破坏公共前缀复用。纠正:相同 position ID 下应用相同旋转;位置编号不同才构成问题。
- 误区:数学等价保证不同设备重算后逐比特相同。纠正:浮点归约和 kernel 可能带来末位差异,工程复用依赖共享已计算块。
本课小结
- 相同公共前缀从嵌入层开始具有相同隐藏状态。
- 逐位置投影产生相同 Q/K/V,因果掩码保证前缀只读取相同历史。
- 残差、归一化和 FFN 不会把后缀差异反向混入前缀。
- 逐层归纳可证明所有层的公共前缀 K/V 相同。
- 相同位置编号下,RoPE 和多头注意力都不破坏结论。
- 真正的缓存复用还要求 tokenizer、模型状态、位置、mask 和外部条件完全匹配。
主题讲解 · 03:25
滑动窗口 Decode 为什么适合环形 KV Cache
学习目标
- 能区分滑动窗口注意力的逻辑可见范围与 KV Cache 的物理存储顺序。
- 能写出环形写指针,并解释窗口满后如何覆盖最老槽位。
- 能说明为什么不必每步把全部 K/V 左移一格。
- 能证明 K、V 使用相同置换时,物理环形顺序不改变加权和。
- 能识别窗口预热、位置编码、相对偏置和不同推理框架实现的边界。
前置与衔接
完整因果注意力在生成位置 可以读取从 1 到 的所有历史 K/V。
KV Cache 因而随序列长度线性增长。
滑动窗口注意力把可见范围限制在最近 个位置。
按本课约定,当前位置也计入窗口:位置 可读取
若模型永远不会再次读取窗口外的 K/V,就没有必要继续保存它们。
环形缓存用固定 个物理槽位反复承载最近窗口,从而把单层单头的时间轴容量固定在 。
核心讲解
1. 窗口未填满时顺序写入
假设窗口大小 。
生成前三个 token 后,缓存里依次保存
窗口大小为 4 时,缓存未填满前按生成顺序写入 K/V;容量只需覆盖最近的窗口。
原视频 · 00:20 ↗这时容量还没用满,物理顺序与逻辑时间顺序一致。
第 4 个 token 到来时,把 写入最后一个空槽,缓存第一次填满。
2. T4 Decode 怎样使用窗口
第 4 个 token 的隐藏状态投影出 。
与窗口内 K 做点积:
经过 softmax 得到长度为 4 的权重向量 ,再计算
生成第 4 个 token 时,单个 query 与窗口中的四个 key 计算分数,再以同一槽位顺序加权对应 value。
原视频 · 00:40 ↗随后输出投影、FFN 和词表投影产生下一 token。
3. 窗口满后覆盖最老槽位
到第 5 个 token 时,逻辑窗口应从
滑到
位置 1 已经永远在窗口外,可以淘汰。
环形缓存不移动位置 2、3、4,而是直接把新 写到原来位置 1 的物理槽。
窗口已满后,新 K5/V5 直接覆盖最老 K1/V1 的物理槽位,避免把其余三组 K/V 整体左移。
原视频 · 01:20 ↗此时物理数组看起来是
而逻辑时间顺序仍是
4. 环形写指针
若从 0 开始编号物理槽,token 位置 的简单写槽可记为
在 时:
- 写槽 ;
- 回到槽 0;
- 写槽 1。
这就是用定长数组实现的循环队列。
实际内核还会保存绝对位置、有效长度或页表信息,以便位置编码、mask 和调度正确工作。
5. 为什么不把 K/V 每步左移
一种直观实现是:窗口满后先把槽 1 到 的数据整体复制到槽 0 到 ,再把新 K/V 放到最后。
但每个生成步都会搬移近乎整个窗口:
个缓存元素,其中 是层数, 是 KV head 数。
环形方案每步只写入当前 token 的新 K/V,并更新少量索引元数据。
这里的“零搬运”是避免为了维持时间顺序而整体移动已有缓存;新 K/V 的计算与写入仍然存在,读取时也可能发生索引、gather 或分页映射。
6. 物理乱序为什么不改变注意力结果
对第 5 个 token,物理 K 顺序可能是
于是分数按同样物理顺序排列:
只要 V 使用完全相同的槽位顺序
每个分数仍配对正确的 value。
即使物理槽位顺序变成 5、2、3、4,只要 K 与 V 使用同一索引映射,分数 A 的每一项仍配对正确的 V 行。
原视频 · 02:00 ↗更形式化地,设 是同一个置换矩阵,物理数组为
则
softmax 只把分量按同一置换重排,随后乘 又把分数与 value 正确配对,结果不变。
7. 逻辑时间顺序与物理槽位是两层概念
逻辑注意力仍按 token 位置和滑动窗口定义;内核可通过环形索引读取物理槽位,无需先把缓存重排成时间顺序。
原视频 · 02:20 ↗模型语义由逻辑 token 位置决定:
- 哪些位置在窗口内;
- causal mask 允许读取谁;
- RoPE 或位置偏置使用什么位置;
- 哪个位置是最老、应被淘汰。
物理环形数组只回答:对应 K/V 字节存在哪个槽。
内核可以按物理顺序直接算,只要同时携带正确逻辑位置信息;也可以把环形区间拆成尾段与头段两段读取。
不需要先把所有 K/V 搬回时间顺序。
8. 位置编码为什么仍要用逻辑位置
RoPE 通常在写入 K Cache 前就按 token 的 position ID 旋转 K。
覆盖物理槽并不意味着新 token 继承旧槽的 position ID。
新 必须使用位置 5 的旋转,而不是位置 1 的旋转。
若注意力还有 ALiBi 或其他相对位置偏置,计算偏置时也要使用逻辑 query/key 位置,不能把物理槽号误当成 token 位置。
所以“顺序任意”只指 K/V 对的物理排列可共同置换,不代表位置语义可以丢失。
9. 窗口开始阶段是梯形预热
序列刚开始时还没有 个历史位置。
第 1、2、3 个位置分别只能看到 1、2、3 个合法 token。
填满窗口后,每个新 query 才稳定读取 个位置。
序列开头历史不足时窗口从 1、2、3 个位置逐步展开,填满后逻辑注意力稳定为固定宽度的因果带。
原视频 · 03:00 ↗若把所有 query 的逻辑注意力画成矩阵,左上角先呈三角/梯形展开,之后形成宽度固定的因果带。
这个完整矩阵只是理解工具;Decode 内核通常不会在显存里物化整个 注意力矩阵。
10. 显存复杂度
不使用窗口时,单请求 KV Cache 随生成长度 增长,数量级为
固定窗口后,时间轴最多保留 个位置:
环形缓存解决的是固定窗口的物理复用,不会减少每个保留 token 自身的 K/V 维度。
11. 实现版本边界
视频用连续定长数组解释 circular/rolling buffer,直觉清晰。
真实推理框架也可能采用:
- 分页 KV Cache 与逻辑块表;
- 两段式环形读取;
- kernel 内的 modulo 索引;
- 为不同层配置不同窗口;
- 部分层全局注意力、部分层滑动窗口。
只要实现维持相同的逻辑窗口和 K/V 配对,物理数据结构不必长得完全像一段简单数组。
窗口宽度的定义也可能区分“包含当前 token”还是“左侧历史 token 数”,阅读配置时要核对具体约定。
跟练与练习
原视频定位
编者练习
窗口大小 ,依次写入 token 1 到 10,槽号从 0 开始。token 10 写入哪个槽?写完后的四个物理槽分别保存哪些 token?逻辑时间顺序是什么?
查看参考答案
。写完后槽 0、1、2、3 分别保存 token 9、10、7、8,即物理顺序 ;最近窗口的逻辑时间顺序是 。
编者练习 2
如果 K 按物理顺序 读取,但 V 却按逻辑顺序 读取,会发生什么?
查看参考答案
分数与 value 错位:例如第一项 的权重会错误乘到 。共同置换不改变结果的前提是 K、V 使用完全相同的槽位映射;只重排其中一方会破坏注意力语义。
常见误区
- 误区:滑动窗口必须每步把缓存整体左移。纠正:环形写指针可直接覆盖最老槽位。
- 误区:环形缓存完全没有任何内存操作。纠正:仍需写新 K/V 和读取窗口;省掉的是已有缓存的整体搬移。
- 误区:物理数组必须按时间排序。纠正:只要 K/V 同序配对并使用正确逻辑位置,共同置换不改变加权和。
- 误区:物理槽号就是 position ID。纠正:位置编码和偏置必须使用逻辑 token 位置。
- 误区:序列一开始就有完整宽度 。纠正:历史未填满时窗口先逐步预热。
- 误区:所有框架都使用同一种连续环形数组。纠正:分页缓存、块表和 kernel 索引也可实现同一逻辑语义。
本课小结
- 滑动窗口只保留最近 个位置,使 KV Cache 的时间轴容量固定。
- 环形缓存用 循环覆盖最老槽位。
- 相比每步整体左移,它只需写入新 K/V 并更新索引。
- K、V 使用同一物理置换时,softmax 分数仍与正确 value 配对,输出不变。
- 逻辑位置、mask 和位置编码不能由物理槽号替代。
- 实际框架可用连续环、分页或块表实现,需按其窗口宽度和位置约定解释。
单元综合
从因果前缀到分页与环形缓存:KV Cache 的完整生命周期
单元能力目标
完成本单元后,应能从数学依赖、算子 shape、显存布局和服务调度四层解释 KV Cache。
具体需要做到:
- 证明 decoder-only 公共前缀的逐层 K/V 相同;
- 判断提示词修改后可复用与需重算的边界;
- 区分 prefill、decode 与 incremental prefill;
- 说明 prefill 为何是 GEMM、单请求 decode 为何近似 GEMV;
- 计算 MHA/GQA/MQA 的 KV Cache 显存;
- 区分逻辑前缀树与物理分页块;
- 解释 PagedAttention 的 block table 与碎片来源;
- 说明多轮对话怎样追加新 K/V;
- 设计前缀缓存替换与安全边界;
- 解释滑动窗口为何适合环形缓存。
概念连接
1. KV Cache 存的是什么
在第 层、位置 ,隐藏状态投影为
自回归生成后续 token 时,需要新的 query 与全部可见历史 K/V 计算注意力。
旧位置的 K/V 已经确定且会被反复读取,所以缓存:
避免每轮重算历史前缀。
Q 只服务当前查询,不需要跨轮保存为 KV Cache。
2. 因果性是稳定前缀的数学基础
decoder-only causal attention 满足:位置 只读取
若两个请求具有完全相同的 token 前缀、位置、mask 与模型状态,则第一层对应位置输入相同,投影得到相同 Q/K/V。
attention、残差、归一化与 FFN 都不会读取未来后缀,因此前缀隐藏状态在下一层仍相同。
逐层归纳可得所有层的公共前缀 K/V 相同。
3. 公共前缀复用还有工程条件
数学 token 相同还不够,至少需要兼容:
- tokenizer 与 token IDs;
- position IDs 与 RoPE 位置;
- causal/attention mask;
- 模型权重与 adapter;
- dtype、量化和缓存布局;
- 确定性执行状态;
- 外部条件或多模态输入;
- 安全隔离与生命周期策略。
复用判据应落到缓存数值与布局兼容,而不是只看文本字符串或用户身份。
4. 提示词单点修改后的边界
若位置 的 token 被修改:
- 的前缀没有读取修改点,逐层 K/V 可保持不变;
- 的嵌入与第一层 K/V 直接变化;
- 的因果 attention 可读取修改位置,隐藏状态随之变化;
- 从后续层开始,修改点及其后方 K/V 都可能变化。
因此只能复用最长不变公共前缀,修改点开始的后缀需重算。
后缀中偶然再次出现相同 token,也不能按 token 字面值复用,因为它的历史上下文不同。
5. 双向 attention 没有同样的追加稳定性
双向模型中,早期位置可以读取后续 token。
追加或修改后缀可能改变整个序列的隐藏状态与 K/V。
所以 BERT 式双向编码通常不能像 causal decoder 那样在追加生成中保留稳定前缀缓存。
真正判据是依赖图,而不是模型名称。
6. Prefill 一次处理整段 prompt
给定 prompt 长度 ,线性层输入通常为
投影、FFN 和 attention 可以沿 token 维批量计算,核心形态是 GEMM。
每一层同时生成 个位置的 K/V,并把它们写入该层缓存。
因果 mask 限制信息方向,但不阻止已知 prompt 位置在同一次前向中并行组织线性代数。
7. Decode 每轮只有一个新 token
单请求 decode 在一轮中输入
线性层表现为 GEMV 或 的瘦 GEMM。
新位置计算 Q/K/V:
- 新 K/V 追加到缓存;
- 新 Q 查询全部历史 K/V;
- 旧 Q 不重算;
- 只消费最后位置 logits 采样下一 token。
batching 多请求或多 token 验证时,decode 也可能重新形成更宽 GEMM,所以“GEMV”是单请求单 token 的 shape 倾向。
8. Prefill 与 Decode 的性能倾向不同
prefill 有较大 token 维,权重复用更充分,常更 compute-bound。
decode 每步读取大量权重与 KV,却只产生一个新位置,常更 memory-bandwidth-bound。
这不是无条件定理;具体取决于:
- batch;
- hidden size;
- context length;
- quantization;
- kernel;
- GPU 架构。
9. KV Cache 的逻辑增长
prefill 对每层一次增加 个位置。
之后每次 decode 增加一个位置。
多轮对话还会交替出现:
- 旧对话已有缓存;
- 用户新输入形成 incremental prefill,一次增加多行;
- assistant decode 每轮再增加一行。
“面”和“条”的图示描述逻辑时间轴,不要求底层物理内存连续。
10. 增量 Prefill 仍有块内因果 mask
新用户 prompt 含多个 token。
其第 个新位置的 query 可读取:
- 全部旧缓存;
- 新块中不晚于自己的 K;
- 不能读取新块未来位置。
所以新块内部仍需 causal mask。
新块所有位置的 K/V 最终按顺序并入缓存,Q 不保存。
11. GQA 共享的是 KV head
设 query head 数为 ,KV head 数为 。
GQA 让多个 query head 映射到同一个 KV head。
共享发生在 head 维,不是 token 维;每个历史 token 仍有自己的 K/V。
逻辑上可以把 KV head 广播给多个 query head,但缓存中不需要物理复制。
MHA、GQA、MQA 可按
理解为同一谱系。
12. KV Cache 显存公式
设:
- batch/request 数 ;
- 每请求缓存 token 数 ;
- 层数 ;
- KV head 数 ;
- head 维 ;
- 每元素字节数 。
K 与 V 两份缓存总字节为
长度不齐时,用
替代 更准确。
单请求每新增一个 token 增量为
13. GQA/MQA 的容量收益
在其他量相同下:
将 从 降低到更小值,会按比例减少 KV Cache 主体容量和 decode 中的 KV 读取。
不能误用 query head 数计算 GQA/MQA 缓存。
元数据、分页表、对齐与碎片还会带来额外开销。
14. 因果上三角怎样真正被跳过
prefill attention 的逻辑分数矩阵含未来上三角无效区域。
tiled kernel 可以:
- 完全跳过位于未来区域的 tile;
- 对穿过主对角线的 tile 在块内逐元素 mask;
- 不物化完整 分数矩阵。
decode 时历史 K 只包含已存在位置,物理上没有未来列。
所以“跳上三角”要分别描述数学 mask、tile 调度和存储。
15. 连续缓存的三类浪费
若为每个请求预留连续最大长度空间,会出现:
- 预留浪费:尚未生成的 future token 占位;
- 内部碎片:已分配区域末端未用;
- 外部碎片:释放后留下难以利用的孔洞。
请求长度与生命周期动态变化,使大连续块管理困难。
16. PagedAttention 分离逻辑块与物理块
把逻辑 token 序列切成固定大小 block。
逻辑 block 编号连续,但物理 block 可以分散在显存。
block table 完成映射:
系统按需分配 block,不必为未来长度预留整段连续空间。
最后一个 block 仍可能有有限尾部内部碎片。
PagedAttention 不减少有效 K/V 数据本身,而是减少分配与碎片浪费。
17. 前缀树负责逻辑复用
多个请求的 token 序列可形成前缀树或 radix tree。
相同 token 主干共享逐层 K/V,第一个不同 token 处开始分叉。
新请求查找最长公共前缀:
- 命中部分直接引用现有缓存;
- 未命中后缀执行增量 prefill;
- 新分支添加到逻辑树。
每个 token 节点代表所有层相关 K/V,而不是单一向量。
18. 前缀树与分页块解决不同问题
- 前缀树:哪些请求共享哪段逻辑 token 历史?
- 分页块:这些缓存数据放在哪些物理显存块?
逻辑节点可引用分页块,分页块可被多个逻辑分支共享或引用计数管理。
把两者混成一层,会难以解释复用、copy-on-write 与驱逐。
19. Prefix Cache 需要替换策略
容量有限时,缓存必须驱逐。
LRU 优先淘汰最近最久未使用对象,适合利用时间局部性。
LFU 更关注访问频率,可能保留长期热点。
但前缀缓存还需考虑:
- block 大小与重算成本;
- 树节点共享引用;
- 父节点被多个后缀依赖;
- 用户与租户隔离;
- adapter/model 版本;
- TTL 与隐私策略。
20. 多 SubAgent 共享公共背景
多个 SubAgent 若使用完全相同的系统提示、工具定义与公共背景,可共享这段公共前缀缓存。
各任务特有内容应放在分叉之后:
系统按 token 前缀和模型状态识别复用机会,不应只按“同一用户”或“同一 Agent 名称”判断。
共享计算不应突破权限与数据隔离边界。
21. 滑动窗口把时间容量固定为 W
滑动窗口 attention 只保留最近 个 token 的 K/V。
新 token 到来时,最老位置离开窗口。
若每步整体左移连续数组,会产生 数据搬运。
环形缓存用
覆盖最老槽位,每步只写新 K/V 并更新逻辑索引。
22. 物理环形顺序不改变注意力语义
若对 K 与 V 使用同一物理置换 :
则分数顺序同步置换,softmax 权重也按同一方式置换,最终加权和保持不变。
但逻辑位置、causal/window mask 与 RoPE 位置必须使用真实时间位置,不能拿物理槽号替代。
“zero-copy”只表示不整体搬移缓存;索引、块表与元数据仍有成本。
对比与决策
1. 复用前检查四类兼容性
- token 与位置;
- 模型、adapter 与数值配置;
- mask、窗口与多模态条件;
- 权限、租户与生命周期。
2. 存储方案的选择
- 单请求固定短序列:简单连续缓存足够。
- 多请求变长服务:分页块减少预留与外部碎片。
- 大量公共 prompt:前缀树/radix cache 提供逻辑复用。
- 固定滑动窗口:环形或分页窗口避免整体搬移。
这些方案可以组合。
3. 性能分析至少分 Prefill 与 Decode
- Prefill:关注 prompt token、GEMM、attention tile 与 TTFT。
- Decode:关注权重/KV 带宽、batch、每 token 延迟与缓存增长。
- 多轮:额外关注 incremental prefill 与前缀命中率。
综合训练
编者练习
一个 32 层模型使用 GQA,、、FP16。单请求每新增一个 token,KV Cache 增加多少字节?缓存 8192 token 约多少 MiB?
查看参考答案
每 token 为 bytes,即 128 KiB。8192 token 为 bytes,即 1024 MiB,也就是 1 GiB。未计 block table、对齐和碎片。
编者练习 2
prompt 在第 100 个 token 被修改,总长 500。decoder-only 多层模型中,哪些位置可以复用,为什么后面再次出现相同 token 也不能直接复用?
查看参考答案
位置 1–99 的公共前缀在 token、位置、mask 与模型状态相同的前提下可复用。位置 100 起需重算,因为修改会通过 causal attention 影响自身及后缀隐藏状态,并在后续层传播。后缀里相同 token 的 K/V 还依赖不同历史上下文和位置,字面 token 相同不足以保证缓存数值相同。
编者练习 3
窗口宽度 ,逻辑时间已有 5、6、7、8,物理槽依次为 0、1、2、3。时间 9 到来时写哪个槽?读取 attention 时还需保存什么?
查看参考答案
,覆盖时间 5 的槽。窗口逻辑顺序为 6、7、8、9,物理槽为 1、2、3、0。系统还需保存逻辑时间到槽位映射,并用真实 position IDs/RoPE、window mask 与一致的 K/V 排列;不能把槽 0 当作逻辑位置 0。
进入下一单元前
- 已能证明 causal 公共前缀的逐层 K/V 可复用。
- 已能区分 prefill、decode 与 incremental prefill 的 shape 和缓存增长。
- 已能用 计算 GQA/MQA 缓存。
- 已能区分前缀树的逻辑共享与分页块的物理分配。
- 已能解释 LRU/LFU 驱逐还需考虑共享、安全和重算成本。
- 已能说明滑动窗口环形缓存的置换正确性与位置边界。
- 若仍把相同 token 当作相同 KV,回看 P14、P133。
- 若仍用 query head 数估算 GQA 缓存,回看 P20、P23。
- 若仍把 PagedAttention 当作压缩有效 KV,回看 P22、P25。