KV Cache 能不能淘汰?淘汰策略怎么设计?
简化版
KV Cache 可以淘汰,但要先区分两类对象:已结束或已取消请求的缓存可以直接释放;仍在生成的请求若丢掉历史 KV,会改变后续注意力结果,不能像普通 Web Cache 一样随意做 LRU。
显存紧张时通常按“先准入控制,再回收无效块,再处理低优先请求”设计。活跃请求可选择暂停并把 KV 交换到 CPU、整条抢占后重算,或只在模型支持滑动窗口/特定压缩算法时淘汰旧 Token。策略必须同时衡量释放显存、恢复成本、质量损失和租户公平性。
详细版
安全回收包括请求完成、取消、超时和前缀缓存条目过期;这些缓存不再参与正确生成。困难的是过载下如何处理仍活跃的序列。
| 手段 | 是否保持精确语义 | 主要代价 |
|---|---|---|
| CPU Swap | 是 | PCIe/NVLink 传输延迟 |
| 丢弃后重算 | 是 | 恢复时重复 Prefill |
| 整条请求终止 | 否 | 用户失败或重试 |
| 滑动窗口 | 取决于模型设计 | 看不到窗口外信息 |
| Token 压缩/选择 | 通常近似 | 可能损失长程依赖 |
生产策略应结合优先级、已等待时间、已生成长度、KV 占用、恢复成本和截止时间。高优先级也不能无限抢占;系统需记录抢占率、重算 Token、Swap 带宽、恢复延迟、OOM 和质量回归,并在达到高水位之前停止接收超预算请求。
完整版教学
1. KV Cache 的生命周期
请求通过 Prefill 建立初始 KV,Decode 每产生一个 Token 就追加新的 K/V。请求完成、取消或失败后,对应物理块应及时归还内存池。
admit -> allocate -> prefill -> append during decode -> finish/cancel -> free
所谓“淘汰”既可能指释放已无用途的块,也可能指在压力下移走仍有用途的块。两者的正确性风险完全不同。
2. 为什么不能直接套用普通 LRU
普通缓存未命中时可从后端重新读取对象;活跃 KV 是当前生成状态的一部分。直接删除最久未使用的若干 Token 后继续生成,会让注意力输入发生变化。
而且 Decode 中每轮都会访问历史 KV,“最近使用”对同一活跃序列几乎没有区分度。LRU 更适合管理可重新构建的前缀缓存条目,不适合直接挑选活跃序列内部的历史 Token。
3. 哪些缓存可以无损释放
请求结束、客户端明确取消、服务端超时终止、模型版本下线时,其 KV 都可以释放。共享前缀块只有在引用计数归零后才能回收,否则会破坏其他请求。
def release(sequence):
for block in sequence.blocks:
block.ref_count -= 1
if block.ref_count == 0:
free_pool.put(block)
真实实现还要处理异步 Kernel 已提交但尚未完成的情况,不能在 GPU 仍引用块时过早复用。
4. 显存压力应先做准入控制
如果系统已知道新请求的 Prompt 长度与最大输出预算,就应在进入执行队列前估算 KV 需求。无法满足安全水位时,选择排队、降级或拒绝,通常比执行到一半再抢占更便宜。
准入条件不能只按请求数,应按 Token 预算、当前 KV 块、优先级和预计释放时间判断。预留少量应急空间可以吸收输出长度预测误差。
5. 整序列抢占如何选择
过载时可以暂停一条完整序列,把资源让给更紧急请求。候选评分可组合:
score = a × priority
+ b × waiting_time
- c × kv_blocks
- d × recompute_cost
释放大序列能快速回收显存,但它可能已经投入大量计算;总是抢占最大请求会伤害长任务。评分还需包含老化机制,确保被暂停请求最终能够恢复。
6. Swap 与重算怎么取舍
Swap 将 KV 搬到 CPU 或较慢设备,恢复时再传回;重算则丢弃 KV,保留原 Token,恢复时重新做 Prefill。选择取决于传输时间与重算时间:
swap_cost ≈ KV_bytes / effective_link_bandwidth
recompute_cost ≈ prefill_time(sequence_length)
长上下文、链路快时 Swap 可能合算;短上下文或 CPU 内存压力大时重算更简单。两者都要计入端到端 SLO,而不是只看释放了多少显存。
| 压力处理方式 | 语义是否保持 | 恢复成本 | 适用条件 |
|---|---|---|---|
| CPU Swap | 是 | KV 往返传输 | 链路快、重算昂贵 |
| 丢弃后重算 | 是 | 重做 Prefill | 上下文较短、计算充足 |
| 终止请求 | 否 | 客户端重试或失败 | 已超截止时间、低价值任务 |
| Token 近似淘汰 | 不一定 | 算法相关 | 允许质量取舍且经过专项评测 |
选择策略时应以恢复后的端到端完成成本比较;只统计当下释放的 GiB,会偏向牺牲已经完成大量计算的长请求。
7. Token 级淘汰为什么困难
标准全注意力模型理论上允许每个新 Token 访问全部历史位置。删除某些 KV 后,输出分布可能改变,尤其当关键信息位于早期 Prompt。
滑动窗口模型只设计为关注最近 W 个位置,窗口外 KV 可按模型规则丢弃。对普通模型使用 H2O、StreamingLLM 等近似保留策略,需要明确这是算法性近似,并单独验证任务质量。
8. 前缀缓存如何淘汰
可复用前缀是派生缓存,未命中时可重新 Prefill,因此适合按 LRU、LFU、TTL 或收益密度管理。单看访问次数仍不够,因为条目大小与节省计算不同。
可用近似价值:
value_density = expected_reuse × saved_prefill_ms / occupied_bytes
优先淘汰价值密度低、版本过期或接近 TTL 的条目。包含敏感数据的租户私有前缀还必须在租户退出或权限变化时强制失效。
9. 分页与引用计数的作用
Paged KV 以固定块分配,便于按块回收并减少外部碎片。共享前缀时多个逻辑序列可引用同一物理块,引用计数保证只有最后一个使用者离开后才归还。
块越大,元数据和地址查找更少,但尾块浪费更高;块越小,管理开销增加。淘汰策略应知道实际可释放的完整块数,而不是按逻辑 Token 数高估收益。
10. 多租户与优先级公平
没有租户配额时,一个租户的超长请求可能占满 KV,并使其他租户反复被抢占。应设置每租户最大并发、Token 预算和优先级上限。
业务优先级可以决定抢占顺序,但要防止低优先级永久饥饿。等待时间老化、最低服务份额和最大抢占次数是常见保护机制。
11. 故障和取消如何避免泄漏
客户端断开、网络重试或 Worker 异常都可能让逻辑请求消失而物理块仍被占用。资源管理应使用请求状态机和最终清理路径,并对孤儿块做周期核对。
不能只依赖正常回调。进程崩溃后的设备内存会随进程释放,但跨节点前缀缓存、CPU Swap 区和调度器元数据仍需一致性恢复。
12. 监控哪些指标
至少记录 KV 使用率、可用块、分配失败、前缀缓存命中/淘汰、活跃请求抢占率、Swap 字节、重算 Token、恢复延迟和因压力拒绝的请求。
平均显存水位不足以定位问题,要关联输入长度、输出长度、租户和优先级。若抢占率很低但 OOM 增多,可能是临时工作区峰值或内存估算错误,而不是淘汰触发太晚。
13. 如何验证策略
构造短对话、超长 Prompt、长生成、取消风暴和高低优先级混合流量,比较无策略基线与候选策略。性能上看 P95/P99、有效吞吐和重算成本;正确性上检查未被近似淘汰的请求输出应与基线一致。
若启用 Token 压缩或窗口淘汰,要在长程检索、代码依赖和多轮对话评测集上单独验收。灰度时设置 OOM、抢占和质量回滚阈值。
淘汰策略的目标不是把显存永远塞满,而是在资源压力下做可解释、可恢复且公平的降级。
14. 常见误区与追问
- 误区:KV Cache 就是普通缓存,可以直接 LRU。 活跃 KV 属于生成状态,删除会影响正确性。
- 误区:PagedAttention 自动决定淘汰策略。 分页是内存组织方式,策略仍由调度器决定。
- 误区:丢弃最老 Token 总是安全。 早期系统指令或证据可能仍然关键。
- 误区:抢占最大请求一定最优。 它释放多,但可能浪费大量已投入计算并伤害公平性。
- 误区:Swap 保持语义就没有代价。 传输会占带宽并拉高恢复延迟。
- 追问:何时选择重算而非 Swap? 当 Prefill 重算比来回传输更快,或 CPU 缓存资源不足时。
- 追问:怎样避免请求饿死? 使用等待时间老化、最低份额和最大连续抢占次数。
15. 加强记忆
- 先分对象:无效 KV 可释放,活跃 KV 不能随意删。
- 再做预防:Token 级准入优于中途抢占。
- 再选恢复:Swap 保状态,重算换带宽。
- 再看模型:滑动窗口可淘汰,普通全注意力需谨慎。
- 再管共享:前缀缓存可按价值淘汰,引用计数防误删。
- 再守公平:租户配额、老化和最低服务份额。
- 最后验收:同时看 OOM、尾延迟、重算成本和任务质量。