解释 KV 缓存为什么存在、它如何随上下文增长,以及 Qwen3.5 的混合注意力在内存上换来了什么。
如果不缓存 K 和 V,每解码一个 token 都要从头重做全部注意力计算——使生成的代价随上下文长度呈平方增长。
术语表 · 7 个术语
- KV cache
- 每一层为每个提示词 token 和已生成 token 存储的 K、V 张量,在各个 decode 步骤间复用,使注意力计算保持大致线性。
- prefill
- 推理的第一阶段:整个提示词在一趟以矩阵乘法为主的传播中处理完毕,并为每个提示词 token 写入 K 和 V。
- decode
- 逐 token 生成阶段:只计算新 token 的 Q/K/V,Q 在缓存的 K/V 历史上做注意力。
- GatedDeltaNet
- Qwen3.5 采用的线性注意力变体。每层存储一个固定大小的循环状态,而不是逐 token 的 KV 缓存。
- hybrid attention
- 在同一个层堆叠里混用全量 softmax 注意力层和线性注意力层。Qwen3.5 用 6 个全量层 + 18 个线性层。
- linear state
- 每层一个形状为 [16, 128, 128] 的张量(线性 value 头数 × value 维度 × key 维度),把全部历史压缩在内;其大小与序列长度无关。
- recurrent state (RNN-style)
- 一块固定大小的记忆,模型在每一步覆盖并向前滚动它——就像累计总和或滑动平均——而不是保留每一条历史记录。线性注意力层用它来保证内存占用不随上下文变长而增长。
KV 缓存与混合注意力
到目前为止,每一章描述的都是一次前向传播内部发生的事。但生成是一连串前向传播——每输出一个 token 跑一次——朴素的实现里,每一次传播都要为历史中的每个 token 重做全部注意力计算。这会让 decode(解码)的代价随上下文长度呈平方增长,一旦提示词里有一份 32k token 的文档,显然就毫无希望了。解决办法是一项工程手段,而不是新架构:把步骤之间不变的部分缓存起来。这块缓存正是推理内存的大头,也左右着你在现代 LLM 里看到的大多数架构决策。
Prefill 与 decode
推理分两个阶段。Prefill(预填充)一次性接收整个提示词,在一趟以矩阵乘法为主的大计算中算出每个 token 的 K、V 和隐藏状态。Decode(解码)则逐 token 生成:把上一个输出送回模型,只为这一个新 token 计算 K/V/Q,所有更早 token 的 K/V 直接从缓存里读出,而不是重新计算。没有缓存,每一步 decode 都会和 prefill 一样昂贵——有了它,decode 的代价大致与缓存大小成线性关系。
为什么缓存 K 和 V 行得通
在 decode 第 t 步,每一层都需要新 token 的注意力输出: softmax(q_t · K^T / √d) · V——和第 4 章一模一样的注意力公式。Q 是全新的(它来自刚算出的隐藏状态)。新 token 的 K 和 V 也是新的。但 token 0..t-1 的 K 和 V 在更早的步骤里就已经算过了—— 而且它们不依赖于 token t。所以我们只存一次,之后一直读回来用。
从形状上看,缓存为每一层存两个形状为 [num_kv_heads, seq_len, head_dim] 的张量。随着 seq_len 增长,缓存线性增长。问题就出在这里:缓存内存与上下文长度成正比,上下文一长,它就会远超权重。
Prefill 把全部 5 个提示词 token 一起处理,并把它们的 K/V 写入缓存。
KV 缓存是解码依然负担得起的原因。开启它(绿色),每一步只为这一个新 token 计算 Q/K/V,而不是重新投影整个前缀——所以它(重新)处理的位置数恒为 1,跨 n 个 token 的总量是线性而非平方。新 token 仍要对每一个缓存的 key 做注意力,所以那次注意力读取确实随上下文增长——缓存去掉的是前缀的重新投影,而不是不断增长的注意力。关闭它(红色),每一步都从头重跑整个前缀,处理的位置数步步攀升,总工作量随序列长度呈平方级。上面的累计总数把差距摆得很具体:这里三步解码之后,缓存开共处理了 5 + 1 + 1 + 1 = 8 个位置,缓存关则是 5 + 6 + 7 + 8 = 26——而且每多一个 token,差距就再拉大一截。
脚本化演示,只为展示循环的形态与“缓存 vs 重新计算”的代价——并非模型的实时输出。
用 Qwen3.5-0.8B 算一笔账
每个全量注意力层、每个 token 的缓存开销是 2 · num_kv_heads · head_dim · 2 bytes = 2 · 2 · 256 · 2 = 2048 bytes。在 32k token 的上下文下,一个全量注意力层占 64.0 MB。如果全部 24 层都是全量注意力,整个模型的缓存将达到 1.50 GB——而这还只是最小的 Qwen3.5 型号。更大的 Qwen3.5 型号在此基础上继续放大。
这就是为什么现代推理服务的头条数字是 KV 缓存内存,而不是权重。也正因如此,一整代架构决策——多查询注意力、分组查询注意力、滑动窗口,以及各类线性注意力变体——全都围绕着“在不损失太多质量的前提下削减缓存”展开。
Qwen3.5 的答案:混合注意力
Qwen3.5 交错使用两种不同的注意力层。24 层中的六层(每四层一个,位于索引 3, 7, 11, 15, 19, 23)是常规的全量注意力层——就是第 4 章那种,其 KV 缓存随seq_len 线性增长。其余 18 层使用一种叫 GatedDeltaNet 的线性注意力变体——之所以叫“线性”,是因为它的开销与序列长度成正比,而不是像全量 softmax 注意力那样与其平方成正比。GatedDeltaNet 层不逐 token 缓存 K 和 V,而是把全部历史压缩进一个形状为 [16, 128, 128] 的固定大小循环状态(线性 value 头数 × value 维度 × key 维度——与全量注意力那 8 个 head-dim 为 256 的头是两套不同的维度)。这个状态在每一步向前滚动,就像早期的循环神经网络(RNN)维护一份“迄今所见一切”的滚动摘要——与流过多少 token 无关。
在 16 个线性头、head_dim = 128 的配置下,每个线性层的状态恒为 512.0 KB,与上下文无关。所以 Qwen3.5-0.8B 的总缓存是 6 × full + 18 × constant——右侧的图表把它和一个每层都用全量注意力的假想 Qwen 画在了一起。
这也正是模型那个完整的 262,144 token 窗口之所以负担得起的原因。设想把一部 200 页的小说——就算它 262,144 个 token——整段粘进提示词。如果全部 24 层都像那 6 个注意力层一样逐 token 保留完整的 KV 缓存,缓存就会贴着图中那条琥珀色的"GQA 全层"曲线——在这个长度下大约是混合方案那几 GB 占用的 4 倍(无论哪种方式,缓存都随上下文线性增长,混合架构只是把斜率压了下来)。可实际上,那 18 个线性层无论上下文多长都只占恒定的 512.0 KB,于是窗口能在原来 32,768 这道线之上再放大约 8 倍,而缓存不会爆。只有那 6 个全量注意力层仍要按 token 付出代价。
下面把这种对比落到 token 级别:把同一条 token 流分别喂给两种记忆,看一边每来一个 token 就追加一个格子,另一边只在原地更新同一个槽位。
已喂入 token:0 / 7。全量注意力内存:已存 0 条记录(每个 token 增加一条)。线性运行状态:1 个固定槽位(常数),当前值 S = 0.00。
那个单一的运行槽位就是循环神经网络的隐藏状态:一块固定大小的记忆,每一步向前滚动(S ← decay·S + u),就像累计总和或滑动平均。全量注意力则保留每一个过去的 token,所以它的内存无限增长。这就是记忆 vs 回忆的取舍——更高的 decay 记得更久,但仍会把过去糊在一起,永远无法像完整缓存那样精确取出某一个旧 token。
示意图——一个标量卡通。GatedDeltaNet 的真实状态是每个头一个矩阵——0.8B 模型的形状为 [16, 128, 128](value 头数 × value 维度 × key 维度)——它的真实更新是带门控的delta 规则:S ← g·S + β·(v − (g·S)·k)·kᵀ:先用 g 衰减旧状态,再向新值写入一个按 β 加权的修正量,而不是简单的外积。但“固定大小 vs 持续增长”的对比是完全正确的。
取舍
线性注意力是近似的。它无法像全量 softmax 注意力那样,精确回忆历史深处某个任意的单个 token——它的压缩状态把所有东西混在了一起。它擅长的是语言处理的主体工作:模式延续、句法走向、局部风格。与六个全量注意力层交错,就把精确回忆的能力补回到需要它的地方。具体来说:线性层负责长上下文的语言流转,全量层负责"回看那个特定 token"的任务。
这件事的“人类一侧”你大概体会过:在一段很长的对话里,模型把线索跟丢了——你早早提过一个名字,四十轮之后它就溜走了。造成这种情况的原因有两种。第一,你可能已经滑过了 262,144 token 这道上限,那么那一早期的轮次已经整段掉出窗口,干脆就没了。第二——也是更有意思的一种——那一轮其实还在窗口内,只是那个细节自始至终只活在那18 个线性层模糊的循环状态里,正是下面面板让你看到的那种被抹开的分布,而那 6 个全量注意力层只是偶尔出手、还能把它钉住的安全网。两种失败都不是绝对的:一个名字重复得足够多就能熬过这种模糊,而即便滑过了上限,模型先前写下的一段摘要也能把这个事实一路带下去。
Softmax 注意力直接拿“针”与每一个缓存的 key 打分,所以它能把几乎全部概率质量放在被要求回忆的那一个确切 token 上。
固定大小的循环状态把全部历史压缩进一个常数大小的张量;这种混合让“针”只比形近的干扰项略高一点,而不是被牢牢锁定。
全量注意力层保留对每一个缓存 key 的逐 token 直接访问,所以很擅长按需取出某一个确切的 token。固定状态的循环层(GatedDeltaNet)把全部历史压缩进一个常数大小的张量,在回忆压力下锁定任意一个 token 就不那么可靠——正如这个示意场景:那根“针”只略微领先于它的形近干扰项。两者都不是绝对的(注意力也可能看错地方,循环状态也并非总是模糊),但正是这种可靠性差距,让 Qwen3.5 保留了 6 个全量注意力层来处理“找到那个特定 token”的任务——它那 18 层的线性主体做这类事不够精确。
示意性的分布——为展示“精确 vs 模糊”的形态而手工挑选,并非模型的实时输出。
为什么这是未来的方向
当上下文长度突破 1M token,平方级内存的注意力将难以为继——缓存和计算都是如此。混合注意力是业界正在探索的技术之一。像 Mamba 这样的纯状态空间模型——用上面 GDN 那样的滚动状态彻底取代注意力——走得更远。趋势很清楚:缓存就是瓶颈,架构会继续围绕它被重塑。
人们有时把它想象成一个从 token 文本或位置映射到 (k, v) 元组的哈希表。不是的。缓存是一个预先分配好的张量,形状为 [layers, 2, batch, kv_heads, max_seq, head_dim] (其中 2 是 key/value 这一对——一块给 K,一块给 V),每个注意力层向它写入、从中读取,并由一个位置计数器记录张量里有多少是有效数据。所谓的“键”是位置索引,不是哈希。它是一块缓冲区,不是哈希表。
Prefill 与 decode:时间维度
内存是一个维度,墙钟时间是另一个。下图对比了 prefill(对整个提示词做一次并行矩阵乘法)和 decode(许多次小而串行的矩阵乘法,每生成一个 token 一次)。
为什么每 token 的差距如此巨大——大约 0.7 ms 对 200 ms,约 293×?一步 decode 必须把模型的每个权重都从内存里读出来,而这趟搬运只换来一个 token:它是内存受限的,没有任何可并行的对象。Prefill 同样的权重只读一次,却同时作用在全部 1,024 个提示词 token 上,搬运成本被摊成 1,024 份。
混合布局在这里还多出一道褶皱。线性层的循环状态必须严格地一次更新一个 token、按顺序来——没有第 t-1 步的状态就算不出第 t 步——而全量注意力层没有这条链,本可以一次性给所有位置打分。所以这份恒定内存,部分是用让出 attention 在 decode 时的一些并行性换来的。(这只是 decode 阶段的问题:训练时线性注意力有 parallel-scan 形式,能把丢掉的并行性找回来。)
Prefill 是一次并行矩阵乘法;解码是 N 次小而串行的矩阵乘法。这就是为什么第一个 token 来得快、其余的以流式吐出。按每 token 计:prefill 约 0.7 ms,解码约 200 ms——相差约 293×。
缓存如何随上下文增长
这里单独画出了每种布局的"缓存 vs 上下文"曲线。公式在下方逐项拆开:2 · L · kv_heads · d · seq · bf16 里的每一个因子,眼下都有人在想办法削减。
bytes_per_full_layer = 2 · kv_heads · d · seq · bf16
= 2 · 2 · 256 · seq · 2
bytes_per_linear_layer = constant (512.0 KB)
total = 6 · bytes_per_full_layer + 18 · bytes_per_linear_layerhead_dim。右侧的小组件画出了 MHA、纯 GQA 和 Qwen3.5 混合布局的缓存曲线;拖动上下文滑块即可读出任意长度下的内存占用。它下方的层条展示精确的交错方式——点击任意一层,可查看该层在当前上下文长度下的缓存形态。
- KV 缓存的内存随上下文线性增长,长上下文时它会远超模型权重本身。
- Qwen3.5 的 6 全量 + 18 线性布局压平了缓存曲线,因为线性层只贡献一个常数,而不是随 token 数增长的块。
- 全量注意力层负责"精确回忆某个特定 token"的任务;线性层在很紧的内存预算下承担语言流转的主体工作。
把上下文长度滑块从 1k 拖到 131k,同时观察三条 KV 缓存曲线。在 131k token 处,混合布局的“实际”曲线大约比 MHA 假想曲线小多少倍?在短上下文时,哪条曲线与混合曲线重合?