别再 Round Robin 了:Prefix Cache-Aware Routing 如何降低 LLM 推理延迟?

1 阅读6分钟

别再 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 RateKV 块淘汰频率

还要区分“命中次数”和“命中价值”。复用 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 开始理解请求内容和实例状态。

要真正获得收益,需要同时做好三件事:

  1. 让 Prompt 拥有稳定、可复用的前缀;
  2. 让路由器知道缓存位于哪个实例;
  3. 在缓存收益和实时负载之间动态权衡。

模型越来越强之后,推理系统的竞争会逐渐落到这些看似底层、却直接决定延迟和成本的工程细节上。