AI 推理 KV Cache 淘汰:别让长会话吃掉所有显存
AI 推理 KV Cache 淘汰:别让长会话吃掉所有显存
一、KV Cache 是吞吐的朋友,也是显存的敌人
自回归模型推理里,KV Cache 可以避免重复计算历史 token,是流式输出性能的基础。但 KV Cache 会随着上下文长度和并发数增长,显存压力非常明显。长会话如果没有淘汰策略,短请求可能连进入队列的机会都没有。
KV Cache 管理不是内存小优化,而是推理服务能否稳定承载多用户的关键。
二、先看 KV Cache 生命周期
flowchart TD
A[Prefill 写入 KV] --> B[Decode 持续追加]
B --> C[会话保持]
C --> D{是否继续使用}
D -->|是| B
D -->|否| E[释放或淘汰]
Cache 的价值来自复用,但复用也意味着资源被占住。调度器要知道哪些会话活跃,哪些会话只是暂时挂着,哪些已经没有继续保留的必要。
kv_cache_policy:
max_session_idle_seconds: 120
max_tokens_per_session: 8192
evict_low_priority_first: true
空闲时间和最大 token 都要限制,否则长尾会话会慢慢把显存吃完。
三、淘汰不能只看时间
struct CacheEntry {
session_id: u64,
tokens: usize,
last_access_ms: u64,
priority: u8,
}
LRU 简单,但不一定适合推理服务。一个低优先级长会话和一个高价值在线会话,占用资源和业务价值不同。淘汰策略应综合 tokens、空闲时间、租户优先级和重建成本。
还要区分可重建和不可重建。某些会话可以从历史 prompt 重新 prefill,成本只是延迟;某些会话状态来自工具调用或临时上下文,丢失后可能影响连续性。策略不能只看内存。
淘汰策略在工程实现上可以借鉴操作系统的页面置换思想,但需要针对推理场景定制。multi-queue 是一种实用方案:维护两个 FIFO 队列和一个 LRU 队列,新请求先进 FIFO 1(快速淘汰区),短时间内有第二次访问则晋升到 LRU 队列,长会话中超过 token 阈值但仍活跃的进入 FIFO 2(长会话保护区)。三个队列按不同速率淘汰:FIFO 1 最快、LRU 按传统逻辑、FIFO 2 最慢。在 Rust 中可以用 VecDeque 和定时器驱动的 eviction worker 协作,每次 tick 按 water level 比例从各队列取目标页数,避免集中淘汰造成延迟抖动。还有一个关键细节:淘汰过程中对被淘汰会话的后续请求应返回特殊错误码告知"上下文已过期",而不是假装正常继续生成,否则用户会看到不连贯的输出。
四、内存压力要提前暴露
等显存分配失败才淘汰,通常已经太晚。推理服务应该持续监控 KV Cache 占用,接近阈值时主动压缩、淘汰或拒绝低优先级新请求。
kv_cache_guard:
warn_at_percent: 75
evict_at_percent: 85
reject_at_percent: 95
三段式阈值比一个硬失败点更稳定。警告阶段可以减少最大输出长度,淘汰阶段清理低优先级缓存,拒绝阶段保护服务不崩。
如果支持 paged KV cache,还要关注碎片。显存总量看起来够,但页碎片太多,仍然可能影响分配效率。监控指标里应包含页使用率、碎片率和搬迁次数。
最后,用户体验也要设计。缓存被淘汰后,如果需要重新 prefill,应让上层知道延迟可能变长。不要把所有额外等待都伪装成模型慢,否则定位问题会很困难。
服务内部最好维护 KV Cache 账本。每个会话占用多少页、属于哪个租户、最后访问时间、是否可重建,都要能查询。账本不只是调试工具,也是限流和成本归因的输入。
kv_cache_accounting:
track_tenant: true
track_page_count: true
track_rebuild_cost: true
expose_admin_query: true
如果某个租户长期占用大量 cache,却请求频率很低,平台就应该降级保留策略。缓存资源和计算资源一样,都需要配额意识。
淘汰策略上线前还要做回放测试。用真实请求日志模拟不同策略,比较命中率、淘汰次数、首 token 延迟和显存峰值。没有回放,策略参数很容易只适合某一天的流量。
五、总结
AI 推理 KV Cache 淘汰要结合空闲时间、token 占用、优先级、重建成本和显存压力阈值。
长会话值得优化,但不能让它吃掉所有显存。Cache 管理越清楚,多租户推理服务越稳定。