KV Cache 优化(三):局部注意力、StreamingLLM 与 KV Pruning

当上下文太长,哪些历史 token 值得继续保留?

Posted by Liu Mengxuan on September 7, 2026

前两篇分别减少了 KV head 的数量,或把每个 token 的 K/V 压缩成 latent。它们解决的是“每个 token 存得太宽”。这一篇换一个问题:

如果上下文已经长到几十万 token,是否真的需要保留每一个历史 token?

Sliding Window Attention、StreamingLLM 和 Pruning KV Cache 都在减少历史状态,但方式不同:

1
2
3
Sliding Window:从 Attention 规则上限制可见范围
StreamingLLM:保留开头的 attention sink,再保留最近窗口
KV Pruning:运行时根据重要性选择要留下的 token

完整注意力、滑动窗口、StreamingLLM 与 KV Pruning 的历史 token 保留方式

1. 先看完整 Attention 的代价

在普通因果 Attention 中,位置 t 的 Query 会访问全部历史 K/V:

$o_t=\operatorname{softmax}\left(\dfrac{q_tK_{1:t}^T}{\sqrt{D_h}}\right)V_{1:t}$

如果上下文从 4K 增长到 128K,单个 Decode Query 需要读取的历史位置也增长了 32 倍。即使使用 KV Cache 避免了历史 token 的重复投影,当前 Query 仍然要扫描这些历史 K/V。

这就产生两类成本:

  • 缓存容量:历史 token 越多,KV Cache 占用越大;
  • 单步读取:每生成一个 token,都要访问越来越长的历史缓存。

2. Sliding Window Attention:只看最近窗口

设窗口大小为 w。位置 t 不再访问 K_{1:t},而只访问最近的窗口:

$o_t=\operatorname{softmax}\left(\dfrac{q_tK_{\max(1,t-w+1):t}^T}{\sqrt{D_h}}\right)V_{\max(1,t-w+1):t}$

例如 w=4 时:

1
2
3
位置 8 的 Query:只能看 5、6、7、8
位置 9 的 Query:只能看 6、7、8、9
位置 10 的 Query:只能看 7、8、9、10

因此,在所有层都采用固定局部窗口、且不存在其他全局 Attention 通路时,Decode 只需保留最近 w 个 token 的 K/V,缓存上限不再随生成长度无限增长:

1
2
3
旧缓存:[4, 5, 6, 7]
处理 8:丢弃 4,保留 [5, 6, 7, 8]
处理 9:丢弃 5,保留 [6, 7, 8, 9]

如果窗口大小为 w,KV Cache 的序列维最多约为 w,而不是完整上下文长度 T

2.1 为什么网络变深后,远处信息仍可能传过来?

假设每层只能看自己和前面两个位置。第一层中,位置 8 只能直接读取位置 6、7、8:

1
第 1 层:8 ← 6、7、8

但位置 6 的隐藏状态可能已经包含位置 4 的信息:

1
2
第 1 层:6 ← 4、5、6
第 2 层:8 ← 6、7、8

于是存在一条跨层信息路径:

$4\rightarrow6\rightarrow8$

再堆一层,位置 8 还可以通过位置 6 的上一层表示间接获得更远的信息。若每层窗口向左扩展 r 个位置,堆叠 L 层后,理论有效感受野大致可以扩展到 Lr 个位置。

这解释了“网络足够深时局部窗口的影响可能减弱”:远处信息可以经过中间 token 接力传递。但它并不等价于 Full Attention,因为 Full Attention 一层就能直接建立远距离连接,而局部 Attention 需要多层中转,中转过程也会压缩和混合信息。

滑动窗口注意力通过多层隐藏状态扩大有效感受野

这里跨层传递的不是原始 token,也不是上一层的 K/V,而是上一层完整 Transformer Block 的输出隐藏状态。以位置 8 为例:

1
2
3
4
5
第一层:Attention 汇总位置 6、7、8
       → 残差、Norm、FFN
       → 得到 h₈⁽¹⁾

第二层:用 h₈⁽¹⁾ 重新生成 q₈⁽²⁾、k₈⁽²⁾、v₈⁽²⁾

FFN 不直接混合不同 token,但会独立加工每个位置已经收集到的上下文信息。真正跨位置搬运信息的是 Attention;FFN 和残差连接则把这些信息加工、保留在传给下一层的隐藏状态中。

2.2 它改变了什么?

Sliding Window Attention 不是单纯的缓存管理技巧,它改变了 Attention 的可见性规则。原生局部模型会在训练时使用对应的 Mask;对全局模型进行推理时截断可以不更新权重,但不能保证质量不变:

1
2
训练时使用全局 Attention,推理时突然截断窗口
可能导致训练和推理分布不一致

有些模型从训练阶段就采用局部 Attention,有些模型则混合全局层、局部层或特殊全局 token。

3. StreamingLLM:为什么要保留开头 token?

直觉上,滑动窗口应该只保留最近 token。但实践中发现,序列开头的一些 token 即使语义上不重要,也可能收到很高的 Attention 权重。这类 token 被称为 attention sink,可以理解为注意力分布的“落脚点”。

如果简单删除这些开头 token,Softmax 的注意力分布可能变得不稳定,生成质量明显下降。

StreamingLLM 的基本策略是同时保留两部分:

1
[开头的 sink tokens] + [最近的滑动窗口]

例如总缓存预算为 8,保留 4 个 sink 和最近 4 个 token:

1
2
完整历史: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
Streaming:[1, 2, 3, 4,             9, 10, 11, 12]

当新 token 到来时,只淘汰中间那段旧窗口,不淘汰开头 sink:

$\text{Cache}_t=\text{Sink}_{1:s}\;\Vert\;\text{Recent}_{t-w+1:t}$

这里 s 是 sink token 数,w 是最近窗口大小。

3.1 Sink token 携带的是什么?

它们不一定是“最有语义价值的词”。更准确地说,模型可能把它们当作稳定注意力分布的锚点。它们承担的是一种计算角色,而不是简单的语义摘要角色。

可以类比为:

1
2
3
Attention 像一群人分配注意力
Sink token 像固定的公告栏
即使公告内容不重要,大家仍需要一个稳定的落点

StreamingLLM 因此不是“智能挑出最重要的语义 token”,而是保留固定前缀锚点和最新上下文。

原始 StreamingLLM 方法可应用于已有模型而不微调,但需要配套的缓存和位置编码管理。尤其使用 RoPE 时,不能只把若干已经旋转的 K 随意拼在一起,再忽略它们对应的位置;应遵循实现中的缓存位置重映射规则。

这里的 Streaming Attention 指这种面向持续输入流的有限缓存 Attention,不是 FlashAttention 内部逐块计算 Softmax 的“流式计算”。StreamingLLM 可以持续处理很长的输入流,却没有获得无限记忆:如果很早之前的订单号所在位置已被淘汰,模型以后不一定能准确找回它。保留 sink 也不等于自动保存所有历史事实。

4. Pruning KV Cache:按重要性淘汰 token

KV pruning 更主动:它给历史 token 打分,只保留预算内更重要的 token。

一个简化的评分方式是统计历史 Key 被当前 Query 关注的程度。例如在若干最近步骤中,位置 j 的累计注意力权重为:

$s_j=\sum_{t\in\mathcal{W}}\alpha_{t,j}$

其中 α_{t,j} 是第 t 步 Query 对历史位置 j 的 Softmax 权重,$\mathcal{W}$ 是观察窗口。这只是说明打分思想的简化公式,并非所有方法都采用同一分数。

例如 H2O 保留累计注意力较高的 heavy hitters 和近期 token;SnapKV 则利用 Prompt 末尾的观察窗口,为各个 head 选择重要的 Prompt KV,并考虑相邻位置的聚集。前者强调生成过程中的淘汰,后者强调生成前对 Prompt 缓存的筛选。

保留分数最高的 token:

1
2
3
历史 token:  1   2   3   4   5   6   7   8
重要性分数: .1  .8  .2  .1  .7  .1  .6  .2
预算=4:      保留 2、5、7,以及最近位置 8

实际方法会考虑更多因素,例如最近 token、局部连续片段、不同层和不同 head 的重要性,以及是否保留特殊 token。

4.1 为什么不能只保留“注意力最高”的一个 token?

因为一次注意力分数只是当前 Query 的观点:

  • 当前 Query 关注的位置,不代表下一个 Query 仍然关注;
  • 某个 token 当前分数低,未来可能变得重要;
  • 不同 head 关注的 token 可能完全不同;
  • 删除后,后续 Query 的 Softmax 分布会发生变化。

因此 KV pruning 是有损策略,需要在显存节省和模型质量之间取舍。它还需要额外的打分、索引和缓存管理开销。

4.2 删除 K/V 后会发生什么?

被保留的缓存可以写成一个子集:

$K'=[k_{i_1},k_{i_2},\ldots,k_{i_m}],\qquad V'=[v_{i_1},v_{i_2},\ldots,v_{i_m}]$

当前 Query 改为:

$o_t'=\operatorname{softmax}\left(\dfrac{q_t(K')^T}{\sqrt{D_h}}\right)V'$

注意力不再看到被删除的位置。这里不是把它们的 Value 置零这么简单,因为 Softmax 的分母也会变化,剩余 token 的权重会重新归一化。

5. 三种方法放在一起比较

方法 保留策略 是否改变 Attention 规则 是否通常需要训练适配 主要优点
Sliding Window 最近窗口 原生模型训练适配;后加截断可不训练但须测质量 简单、缓存有上限
StreamingLLM 开头 sink + 最近窗口 原始方法无需微调 适合持续流式生成
KV Pruning 按重要性选 token 是运行时稀疏化 H2O、SnapKV 等可不训练,须测质量 更灵活地保留信息

它们和 GQA/MLA 的区别在于优化维度不同:

1
2
3
GQA:减少每个 token 的 KV head 数
MLA:减少每个 token 的 KV 表示宽度
局部/淘汰方法:减少保留的 token 数

6. 一个共同的缓存预算例子

假设原始上下文有 4096 个 token,模型每层每 token 的 KV 状态需要 4 KiB:

1
2
3
完整 Cache:4096 × 4 KiB = 16 MiB/layer
窗口大小 512:512 × 4 KiB = 2 MiB/layer
压缩比例:1/8

如果采用 StreamingLLM,保留 32 个 sink 和 480 个最近 token,总预算仍为 512:

1
32 个固定前缀 token + 480 个最近 token = 512 个 token

如果采用 pruning,则可以动态选择 512 个 token,但需要付出重要性评估和索引管理成本。

7. 这些方法和 KV Cache 的关系

它们都没有改变 KV Cache 的基本对象:缓存的仍然是各层历史 token 的 K/V,而不是文本、Attention 权重或最终输出。

改变的是缓存集合:

$\{1,2,\ldots,T\}\rightarrow\mathcal{S},\qquad |\mathcal{S}|\ll T$

未来 Query 只访问集合 S 中的 K/V。于是显存和单步读取量都下降,但被删除位置的信息不再能直接参与后续 Attention。

8. 总结

1
2
3
4
完整 Attention:保留并读取所有历史 token
Sliding Window:只保留最近 token
StreamingLLM:保留开头 sink + 最近 token
KV Pruning:按运行时重要性选择 token

最重要的区别是:

Sliding Window 是固定规则,StreamingLLM 是带注意力锚点的固定规则,KV Pruning 是动态选择规则。它们都通过减少 token 数量节省 KV Cache,但都会牺牲一部分全局信息通路。

系列导航

参考资料