别再 Round Robin 了:Prefix Cache-Aware Routing 如何降低 LLM 推理延迟?
传统 Web 服务做负载均衡时,Round Robin 是非常合理的默认选择:请求依次分发到不同实例,流量大致均匀。
但 LLM 推理实例并不是完全无状态的。
同一个系统 Prompt、长文档或者多轮会话已经在某个实例计算过时,该实例的 KV Cache 可能保留了这段前缀的中间状态。后续请求如果仍然落到这个实例,就可以跳过大量重复计算。
如果负载均衡器把请求随机分配到另一台机器,缓存优势就消失了。
这也是 Prefix Cache-Aware Routing 开始成为推理基础设施热点的原因。Google 公布的 GKE Inference Gateway 实践表明,上下文感知路由可以显著提高 Prefix Cache 命中率;其生产案例中,Vertex AI 的缓存命中率从 35% 提升到 70%。Google Cloud:GKE Inference Gateway
一、Prefix Cache 缓存了什么?
模型生成每个 Token 时,会基于此前 Token 计算 Attention。为了避免每一步都重新计算全部历史,推理引擎会保存 Key/Value 状态,也就是 KV Cache。
假设请求结构是:
[固定系统提示词 4000 tokens]
[固定产品文档 12000 tokens]
[用户问题 50 tokens]
不同用户的问题会变化,但前面的 16000 个 Token 可能完全相同。
Prefix Cache 将这段公共前缀对应的 KV 状态保存下来。下一次请求命中缓存时,只需要处理新的用户问题。
适合复用的前缀包括:
- 系统 Prompt;
- 代码仓库基础上下文;
- 企业知识文档;
- Few-shot 示例;
- 多轮会话历史;
- Agent 固定工具描述。
二、为什么普通负载均衡会破坏缓存?
假设有三个推理实例:
Pod A:缓存了文档 X
Pod B:缓存了文档 Y
Pod C:没有相关缓存
一个携带文档 X 前缀的新请求到来:
- Round Robin 可能分配给 Pod C;
- Least Connections 可能分配给 Pod B;
- Cache-Aware Router 会优先选择 Pod A。
因此路由目标不再只是“找到最空闲实例”,而是综合考虑:
缓存匹配度
+ 当前队列长度
+ KV Cache 剩余容量
+ GPU 负载
+ 租户优先级
= 路由分数
如果 Pod A 已严重拥塞,等待缓存命中可能比去 Pod C 重新计算更慢,所以缓存亲和性不能成为唯一指标。
三、一个简化的 Go 路由策略
首先为请求计算稳定的前缀指纹:
func PrefixHash(tokens []int, prefixLength int) string {
if prefixLength > len(tokens) {
prefixLength = len(tokens)
}
h := sha256.New()
for _, token := range tokens[:prefixLength] {
_ = binary.Write(h, binary.LittleEndian, int32(token))
}
return hex.EncodeToString(h.Sum(nil))
}
不能直接对原始字符串取 Hash,因为空格、模板渲染和 Tokenizer 版本变化都可能导致实际 Token 序列不同。
然后综合缓存和负载评分:
type ModelPod struct {
ID string
QueueDepth int
KVUtilization float64
CachedPrefixes map[string]bool
}
func scorePod(pod ModelPod, prefix string) float64 {
score := 100.0
if pod.CachedPrefixes[prefix] {
score += 80
}
score -= float64(pod.QueueDepth) * 12
score -= pod.KVUtilization * 40
return score
}
func SelectPod(pods []ModelPod, prefix string) (ModelPod, error) {
if len(pods) == 0 {
return ModelPod{}, ErrNoHealthyPod
}
best := pods[0]
for _, pod := range pods[1:] {
if scorePod(pod, prefix) > scorePod(best, prefix) {
best = pod
}
}
return best, nil
}
生产环境还会考虑模型版本、并行配置、缓存块位置和预估输入长度。示例重点是说明:缓存命中和实时负载必须同时进入路由决策。
四、提高命中率要从 Prompt 结构开始
即使有智能路由,如果每次请求的前缀都不同,也无法命中缓存。
把稳定内容放前面
稳定系统指令
-> 稳定工具描述
-> 稳定参考资料
-> 动态用户信息
-> 当前问题
不要在开头加入随机值
下面这些字段会让所有请求前缀不同:
request_id
当前精确时间
随机追踪信息
动态排序的 JSON
它们应放到后面,或者通过独立元数据传递。
保证序列化稳定
工具列表和 JSON Map 的顺序如果每次变化,即使语义相同,Token 前缀也可能不同。
对公共上下文进行版本化
prefix_key = tenant + document_set + document_version + model + tokenizer
文档更新后使用新版本,避免错误复用旧缓存。
五、哪些指标值得监控?
只看平均响应时间无法判断 Prefix Cache 是否生效。
建议监控:
| 指标 | 作用 |
|---|---|
| Prefix Cache Hit Rate | 缓存命中比例 |
| Cached Tokens | 每次请求复用的 Token 数量 |
| Time to First Token | 用户感知的首字延迟 |
| Prefill Latency | 输入前缀处理耗时 |
| Queue Time | 路由到实例后的等待时间 |
| KV Cache Utilization | 缓存空间压力 |
| Eviction Rate | KV 块淘汰频率 |
还要区分“命中次数”和“命中价值”。复用 100 个 Token 与复用 2 万个 Token 的收益完全不同,因此可以统计:
saved_prefill_tokens
saved_gpu_seconds
cost_saved_per_request
Google 的另一份推理优化总结也指出,仅通过 Prefix-Aware Routing,在不更换硬件和模型的情况下,就能降低首 Token 延迟并提高缓存效率。Google Cloud:Efficient frontier of LLM inference
六、Prefix Cache 不是免费的
它也会带来新的权衡:
- KV Cache 占用大量显存;
- 热点前缀可能让流量集中到少数实例;
- 缓存目录本身需要同步;
- 多租户环境要防止侧信道和数据泄漏;
- 模型或 Tokenizer 更新会让旧缓存失效;
- 为命中缓存而等待过久可能适得其反。
一个实用策略是设置最大亲和等待时间:如果命中实例的预估等待超过阈值,就选择空闲实例重新 Prefill。
cache_saved_time > extra_queue_time
-> 选择缓存实例
否则
-> 选择空闲实例
路由目标应该是最低总完成时间,而不是最高命中率。
结语
LLM 推理不是普通无状态 HTTP 服务。请求携带的上下文越长,前缀计算成本就越高,实例上的 KV Cache 也越有价值。
Round Robin 只看请求数量,Prefix Cache-Aware Routing 开始理解请求内容和实例状态。
要真正获得收益,需要同时做好三件事:
- 让 Prompt 拥有稳定、可复用的前缀;
- 让路由器知道缓存位于哪个实例;
- 在缓存收益和实时负载之间动态权衡。
模型越来越强之后,推理系统的竞争会逐渐落到这些看似底层、却直接决定延迟和成本的工程细节上。