Wink Pings

同样的系统提示要重算 170 倍,问题出在路由层

生产环境里,每次请求的 prompt 开头几乎一模一样,只有末尾是新的。但一个不看缓存的路由器,会把下一轮请求扔给另一张 GPU,让它从头重算整个前缀。8 个 vLLM pod、16 张 H100 的测试里,p90 首 token 时间从 92.5 秒降到 0.54 秒,差距就来自这里。

生产环境的 LLM 请求,大部分 token 都是重复的。

同一个 Agent 跑起来,每轮请求开头都是同样的 system prompt、同样的工具定义、同样的历史对话,只有最后一段是新的。上一轮服务你的那张 GPU,KV cache 里还留着这些前缀的中间状态。

问题是,如果路由器不看缓存,下一轮请求可能被扔到另一张卡上。那张卡没有这段前缀,必须从头 prefill 一遍,才能吐出第一个 token。

Avi Chawla 把这个过程拆得很清楚:前缀感知路由(prefix-aware routing)解决的就是这件事。

它的工作方式分几步:

请求带着前缀块 A 到 D 和一个新的后缀 E 进来,路由器按块对 prompt 做哈希,每个哈希覆盖它之前的所有内容。

这些哈希会去查一张 block-to-worker 索引。这张索引只存位置,不存 KV 张量本身。

假设 Worker A 命中 2 块,Worker C 命中 3 块,Worker B 命中全部 4 块。请求发给 B。

B 复用 A 到 D 的缓存 KV,只计算 E 的 KV,然后开始解码。E 被缓存后,一个 cache event 会更新路由器的索引,下一轮请求继续落在 B 上。

如果没有任何 worker 命中,请求就在路由器选中的 worker 上做一次完整的 prefill。

在 8 个 vLLM pod、16 张 H100 的基准测试里,前缀感知路由把 p90 首 token 时间压到 0.54 秒。同样的 pod,随机路由是 92.5 秒。约 170 倍。

真实的路由器不会只看缓存命中。Avi 在回复里提到,Dynamo 的 KV router 会同时给两件事打分:扣掉缓存抵扣后仍需 prefill 的成本,以及当前 worker 的解码负载。所以一个前缀全命中但排队很长的 GPU,可能输给一个空闲但需要重算部分前缀的 GPU。

有网友把生产环境的权衡写得更直白:纯缓存亲和性会制造热点 pod,一个 worker 被压垮,其他 worker 闲着。实际做法是在网关层算一个加权成本,(cache_hit_blocks * prefill_cost_saved) - (queue_depth * queue_latency_penalty)。

这套机制最容易翻车的地方,是前缀本身失效。系统提示开头放一个当前时间戳,或者一个随机请求 ID,后面所有块的哈希全部对不上,缓存整段作废。多位网友在回复里都点了这个坑:把动态内容推到 prompt 末尾,让前面的部分保持字节级一致。

还有一个区分值得记住。Artem Tarasov 在讨论里指出,KV 缓存、前缀缓存、prompt 缓存都是透明的:同样的输出,更少的计算。语义缓存不一样,它可能返回一个你原本不会得到的答案。把这几样东西混在一起讲,上线时容易跳过 eval。

前缀缓存本身也有边界。GPU 缓存可能被驱逐,或者随重启丢失。有网友提到可以把 KV 存到磁盘,vLLM 和 SGLang 里能按字节重新加载。

说到底,模型没变,算力没变,首 token 时间差了两个数量级。路由和缓存这一层,比很多人以为的更值钱。

发布时间: 2026-10-30 01:34