为什么 LLM 推理会反复计算相同前缀
在真实 LLM 服务中,很多请求并非完全独立。它们往往共享一大段开头内容:system prompt、安全规则、工具 schema、RAG 模板、长文档开头、Few-shot examples、Agent 固定任务说明,甚至同一个企业客户的固定业务上下文。
如果每次请求都从头 prefill,系统就会重复计算同一批 token 的 key/value 张量。单机推理框架已经可以通过 prefix caching 复用这些 KV cache blocks;但进入多 worker、多节点、多租户部署后,新的问题出现了:请求如果被普通负载均衡随机发到不同 worker,缓存就会被打散。明明前缀一样,却因为路由没感知缓存位置,还是要重新 prefill。
所以,生产级优化的重点不只是”打开 prefix cache”,而是让 router、scheduler、KV cache manager、cache namespace、metrics 一起工作。
一句话理解 Prefix-Aware KV Cache Routing
Prefix-aware KV cache routing 的核心很简单:请求进来后,先判断它的 token prefix 和哪些 worker 上已有缓存最接近,再把请求发给最可能复用 KV cache 的 worker。
User Request
-> Tokenize and hash prefix blocks
-> Query worker cache index
-> Route to best cache-match worker
-> Reuse cached KV blocks
-> Prefill only missing suffix
-> Decode normally
这套机制要解决的不是单个请求能不能快,而是集群级别的重复计算浪费。
核心知识结构
可以把这个主题拆成 6 个模块。
1. Prefix Cache
Prefix cache 复用已经计算过的 KV blocks。它适合大量请求共享相同系统提示词、工具说明、模板或长文档前缀的场景。
2. Block Hash
推理框架通常不会只用整段 prompt 做缓存,而是按 token block 管理。每个 block 可以由父 block hash、当前 block tokens,以及 LoRA ID、多模态输入 hash、cache salt 等额外信息共同确定。
3. Cache-Aware Router
Router 不再只看 worker 当前负载,还要看请求 prefix 和 worker 本地缓存的匹配程度。它要在”缓存命中”和”负载均衡”之间做取舍。
4. Worker Cache Index
每个 worker 需要暴露轻量级缓存索引,告诉 router 自己拥有哪些 prefix blocks。这个索引可以是精确的,也可以是近似的。
5. Tenant Isolation
多租户场景不能让不同客户共享同一缓存命名空间。即使模型输出不变,攻击者也可能通过延迟差异推测某些前缀是否被缓存。
6. SLO Metrics
Prefix-aware routing 不能只看 cache hit rate。还要同时看 TTFT、TPOT、worker hotspot、queue time、cache transfer cost 和 SLO goodput。
为什么普通负载均衡会浪费缓存
普通 L7 负载均衡通常关心这些指标:worker 是否健康、连接数是否过高、请求数是否均衡、延迟是否异常。
这对传统 Web 服务足够,但对 LLM 推理不够。因为 LLM worker 的”状态”不只是负载,还有它已经计算过的 KV cache。
假设 3 个请求共享同一段 8K token 的工具说明:
Request A -> Worker 1,完成 prefill,缓存 prefix
Request B -> Worker 2,重新 prefill
Request C -> Worker 3,重新 prefill
从负载均衡角度看,这很公平;从 LLM 推理角度看,这是浪费。
更合理的做法是让后续请求尽量进入已有相同 prefix cache 的 worker。但这里也不能走极端——如果所有相同前缀请求都被打到同一个 worker,它又会变成热点。所以 prefix-aware routing 的难点是:既要复用缓存,又不能把流量压垮到少数 worker 上。
典型执行流程
一个生产级 prefix-aware routing 流程可以这样设计:
1. 请求进入网关
2. Tokenizer 生成 prefix token blocks
3. Router 计算或查询 prefix block hash
4. Cache index 返回各 worker 的最长命中前缀
5. Router 结合 cache match、queue length、GPU memory、SLO 做决策
6. 请求被发送到目标 worker
7. Worker 复用已有 KV blocks,只 prefill 未命中的 suffix
8. Decode 阶段正常生成
9. Metrics 系统记录 cache hit tokens、TTFT、queue time、fallback 和热点情况
10. 如果缓存索引过期,最多造成 cache miss,不应影响输出正确性
这里有一个很重要的设计原则:缓存元数据可以弱一致,但模型输出必须保持正确。也就是说,router 以为某个 worker 有缓存,结果去了之后发现没有,最多是重新 prefill;不能因为错误复用 KV cache 导致输出错乱。
路由策略:不要只追求最长前缀命中
最直觉的策略是:把请求发到拥有最长 prefix match 的 worker。但生产里还要加入更多约束。
1. Cache Match Score
衡量目标 worker 能复用多少 token。可以按命中 block 数、命中 token 数或预计节省 prefill 时间来计算。
2. Worker Load
如果一个 worker 虽然缓存命中高,但 queue 已经很长,继续发过去可能让 TTFT 变差。
3. GPU Memory Pressure
Worker 如果 KV cache 已经接近满载,新请求可能触发 eviction,反而降低后续命中率。
4. Tenant Boundary
不同租户、不同安全域、不同 cache salt 的请求不能只因为 prefix 一样就共享缓存。
5. Transfer Cost
如果支持跨节点 KV cache 传输,要评估传输成本是否低于重新 prefill 成本。短前缀不一定值得传输。
score = (
cache_hit_tokens * prefill_cost_per_token
- queue_wait_penalty
- memory_pressure_penalty
- transfer_cost
- hotspot_penalty
)
这个分数不是为了精确建模,而是提醒工程团队:最长缓存命中不一定是最佳路由。
集群缓存有三种架构选择
| 架构 | 优点 | 缺点 |
|---|---|---|
| Sticky Routing | 实现简单,不需要复杂 KV transfer | 容易热点,worker 重启后缓存收益消失 |
| Shared KV Cache Layer | 缓存生命周期独立于 worker,便于跨实例复用 | 引入序列化、传输和一致性管理成本 |
| Cache-Aware Routing + On-Demand Transfer | 更灵活,按需拉取 KV blocks | 系统复杂度最高,需处理 transfer latency 与失败恢复 |
Sticky Routing
同一类前缀请求尽量打到固定 worker。优点是实现简单,不需要复杂 KV transfer;缺点是容易热点,worker 重启后缓存收益消失。
Shared KV Cache Layer
通过独立缓存层让多个推理实例共享 KV cache,例如 CPU DRAM、NVMe、远程存储或专门的 KV cache service。优点是缓存生命周期独立于 worker,便于跨实例复用;缺点是会引入序列化、传输和一致性管理成本。
Cache-Aware Routing + On-Demand Transfer
Router 先把请求发到最合适的 worker;如果本地没有完整缓存,再按需从其他位置拉取 KV blocks。优点是更灵活;缺点是系统复杂度最高,需要处理 transfer latency、失败恢复和资源隔离。
安全问题:缓存命中也可能泄露信息
很多人会忽略一个细节:即使 KV cache 不直接暴露给用户,缓存命中造成的延迟差异也可能形成侧信道。
例如攻击者可以构造某个敏感前缀,然后观察 TTFT 是否明显下降,推测该前缀是否已经被其他用户请求过。
生产环境至少要做 4 件事:
1. 按租户、项目或信任组隔离 cache namespace
2. 在 hash 中加入 cache salt
3. 避免低安全 hash 算法导致碰撞风险
4. 对跨租户 cache hit 和异常低 TTFT 做监控
如果业务是企业知识库、代码助手、法律文档、医疗问答或内部 Agent,这个问题不能当成理论风险。
指标体系:不要只看 Cache Hit Rate
Prefix-aware routing 至少要看以下 8 类指标:
| 指标 | 含义 |
|---|---|
| Prefix Hit Tokens | 实际命中复用的 token 数,而非命中率 |
| TTFT Delta | cache hit 与 cache miss 的首 token 延迟差异 |
| Router Decision Latency | 路由决策本身的耗时 |
| Hotspot Rate | 是否存在少数 worker 承载绝大部分流量 |
| Eviction Churn | 缓存驱逐频率是否过高 |
| Transfer Cost | 跨节点 KV block 传输耗时与带宽开销 |
| SLO Goodput | 满足 SLO 的请求吞吐量 |
| Isolation Violation | 是否存在跨租户缓存命中的异常事件 |
metrics:
prefix_hit_tokens_p50: 4096
prefix_hit_tokens_p95: 16384
ttft_ms_cache_hit_p95: 420
ttft_ms_cache_miss_p95: 1380
router_decision_ms_p95: 12
hotspot_worker_ratio: 0.08
kv_transfer_ms_p95: 35
cache_eviction_churn_per_min: 240
slo_goodput_rps: 61
常见失败模式
Router 只看缓存,不看负载
这样会把热门 prefix 全打到少数 worker,最后 TTFT 反而变差。
Cache index 更新太频繁
索引过细、更新过频会增加控制面压力。很多场景下,弱一致近似索引就够了。
忽略 tokenizer 和模板版本
同样的文本,如果 tokenizer、chat template、tool schema 顺序不同,token prefix 可能并不一致。
把动态内容放在前面
时间戳、request id、用户私有字段放在 prompt 开头,会破坏前缀复用。
没有租户隔离
多租户共享缓存必须设计 namespace 或 salt,否则可能带来隐私和侧信道风险。
生产落地 Checklist
上线前建议逐项检查:
- 是否固定 system prompt、工具 schema 和模板顺序
- 是否把动态变量放在 prompt 后部,避免破坏 prefix
- 是否按 token block 而不是原始字符串计算缓存匹配
- Router 是否同时考虑 cache match、queue length、GPU memory 和 SLO
- 是否有 worker cache index,并允许弱一致导致 cache miss
- 是否记录 prefix hit tokens,而不只是 hit rate
- 是否监控 cache hit / miss 的 TTFT 差异
- 是否有热点检测和流量打散策略
- 是否评估跨节点 KV transfer 是否值得
- 是否使用 cache namespace 或 cache salt 做多租户隔离
- 是否为 tokenizer、chat template、tool schema 变更跑回归测试
- 是否保留 fallback:缓存不可用时仍能正确重新 prefill
结论
Prefix-aware KV cache routing 的本质,是把 KV cache 从单个 worker 的局部优化,升级为整个推理集群的调度资源。
Prefix cache -> 负责减少重复 prefill
Router -> 负责把请求发到最可能复用缓存的位置
Scheduler -> 负责避免热点和队列放大
Cache namespace -> 负责隔离租户和安全域
Metrics -> 负责证明 TTFT 与 goodput 是否真的变好
对工程团队来说,最实用的原则是:不要只问”我的 serving engine 是否支持 prefix caching”,而要问”我的请求是否真的被路由到能复用缓存的 worker”。
当 prompt 结构、block hash、worker 路由、缓存隔离和 SLO 监控形成闭环,prefix cache 才能从一个局部优化,变成稳定降低 LLM 推理成本和延迟的生产能力。