TT Lab
博客

한국어 English 日本語 中文

开始
博客

博客

将相同的请求发送给 Dynamo,实测轮询和感知 KV 路由

状态:草稿(2026-11-14),实测。 本文中的数据由作者于 2026-11-13 亲自实测得出。环境与请求同 第 1 篇相同。Kubernetes v1.34、三台 GPU 服务器(一张 24GB 级 GPU,两张 8GB 级笔记本 GPU)、节点间 1GbE、Dynamo v1.6.2 的 vllm-runtime:1.6.2 镜像(vLLM 0.31.0)、模型为 Qwen3-0.6B。路由器参数全部为默认值。 编写规则:以实测数据为基础,未能测出的标为「未测」,未能确认的标为「未确认」。

轮询与感知 KV 路由

目录

1. 本篇要回答的问题

第 1 篇的结论如下:如果在 Service 背后部署多台 vLLM,前缀缓存的命中率由「单个 Worker 容纳的前缀数 ÷ 前缀种类数」决定,增加 Worker 也无法提高命中率。本篇将相同的请求发送到 Dynamo,以确认以下三点。

  1. 仅仅通过 Dynamo 是否会有所不同(轮询)。
  2. 开启感知 KV 路由后,命中率和首个 Token 时间会有多大变化。
  3. 如果去掉文档中提醒注意的两点(即哈希种子和 KV 事件),结果会怎样。

2. Manifest(清单)中的变更

基准线使用的是 Deployment 和 Service。而在 Dynamo 中,这两者合并成了 DynamoGraphDeployment(DGD)这一个资源。Operator 会读取该资源并创建 Pod 和 Service。引擎参数、镜像、模型和节点分配保持不变,仅修改了以下四项。

变更项 基准线 Dynamo
资源 Deployment + Service 单个 DGD(组成部分:1 个前端,多个 Worker)
Worker 的启动命令 python3 -m vllm.entrypoints.openai.api_server python3 -m dynamo.vllm
开放 OpenAI API 的位置 每个 Worker 均为 8000 仅前端为 8000,Worker 不开放 HTTP
路由 Service 按连接级别随机分发 前端的 DYN_ROUTER_MODE

启用感知 KV 路由需要两项设置。

# 프런트엔드
env:
  - {name: DYN_ROUTER_MODE, value: kv}

# 워커 인자
- --kv-events-config
- '{"publisher":"zmq","topic":"kv-events","endpoint":"tcp://*:20080","enable_kv_cache_events":true}'

每当创建或删除 KV 块时,Worker 都会向该地址发送事件,前端内部的路由器接收这些事件并构建「哪个 Worker 拥有哪个块」的映射地图。当请求到来时,路由器会选择与请求前半部分重叠度最高的 Worker,同时也会考虑 Worker 的负载情况。

在此记录一下迁移过程中确认的事实。

3. 五种变体

名称 配置 观测目的
基准线 Service 背后的 vLLM(第 1 篇) 对比值
轮询 DGD, DYN_ROUTER_MODE=round-robin 仅经过 Dynamo 的效果
KV 路由 DGD, DYN_ROUTER_MODE=kv, KV 事件, PYTHONHASHSEED=0 核心功能
无种子 KV 路由中去掉 PYTHONHASHSEED 选项 是否确实需要哈希种子
近似模式 DYN_ROUTER_USE_KV_EVENTS=false, 同时关闭 Worker 事件 在没有事件、仅靠猜测的情况下效果如何

请求与第 1 篇相同。共享前缀约为 2,000 个 Token,每个请求附加不同的尾部约 300 个 Token,单个 Worker 的 KV 容量限制为 1GiB(相当于约 3.95 个请求)。前缀种类为 2、7、12、24,并发请求为 4、8、16,每个数据点运行 3 次并取中位数。由于种子相同,所有变体接收到的请求和顺序完全一致。五种变体在所有数据点上的错误数均为 0。

轮询变体中的某个数据点(2 种前缀·并发 4)排除了第一次迭代。因为紧邻其后以相同种子发送了测试请求,导致前缀已经存在于缓存中。该数据点取剩余 2 次运行的中位数。

4. 结果:轮询与基准线相同,KV 路由则不同

같은 요청, 세 가지 길 워커 6대 · 동시 8 · 3회 중앙값 · 요청 단위 적중 비율 0 25% 50% 75% 100% 2 7 12 24 서로 다른 접두의 수 KV 인지 라우팅 Service(동그라미), 라운드로빈(네모)
图表:三种路径的请求级别命中率

请求中命中缓存的比例(6 个 Worker,并发 8)。

前缀种类 Service 背后的 vLLM 轮询 KV 路由
2 95.0% 95.0% 97.5%
7 55.5% 53.5% 83.0%
12 32.1% 29.2% 67.9%
24 17.5% 17.5% 43.3%

从输入 Token 中来自缓存的比例来看结果也是一样的。在此请求中,2,371 个 Token 中有 2,032 个是前缀,因此 0.857 是能够达到的最大值。

前缀种类 并发数 Service 背后的 vLLM 轮询 KV 路由
2 4 0.814 0.814 0.857
7 4 0.431 0.385 0.788
12 4 0.241 0.262 0.664
24 4 0.135 0.126 0.429
2 16 0.814 0.814 0.835
7 16 0.425 0.442 0.779
12 16 0.268 0.237 0.695
24 16 0.141 0.148 0.431

轮询调度与基准线没有区别。 仅仅在 Dynamo 前端前面套一层并不能让缓存复活。从前缀缓存的视角来看,把请求均匀分发给工作节点和随机分发是一回事。

在单台容量无法容纳前缀的区间内,KV 路由将命中率提升了两到三倍。 在 2 种前缀的情况下,差异较小。正如第 1 篇中所写,只要前缀能完全装进单台机器,用路由几乎得不到什么好处。

路由器实际上是否汇聚了请求,是通过“一个前缀发往了多少个工作节点”来确认的。在请求的 nvext.extra_fields 中加入 worker_id 后,响应中就会带有处理该请求的工作节点的标识符。我们就是通过它来计数的。

前缀类型 轮询调度 KV 路由
2 6.00台 1.00台
7 6.00台 2.86台
12 6.00台 4.00台
24 5.92台 5.17台

(6个工作节点,并发 4,3 次中位数。)轮询调度将所有前缀发送到了所有工作节点。KV 路由在 2 种前缀时每个前缀只发送到 1 台机器,而随着前缀增加,分布范围也随之扩大。

5. 首Token时间和吞吐量

첫 토큰까지의 시간, 꼬리(p99) 워커 6대 · 동시 16 · 3회 중앙값 0 0.4초 0.8초 1.2초 1.6초 2 7 12 24 서로 다른 접두의 수 Service 뒤 vLLM Dynamo 라운드로빈 Dynamo KV 인지 라우팅
图:首Token时间 p99

首Token时间 (TTFT)。每格中为 p50 / p99(6 个工作节点)。

前缀类型 并发 Service 后的 vLLM 轮询调度 KV 路由
2 4 37 / 122ms 41 / 125ms 34 / 73ms
7 4 63 / 184ms 89 / 142ms 40 / 129ms
12 4 94 / 193ms 106 / 140ms 43 / 138ms
24 4 101 / 254ms 113 / 143ms 64 / 149ms
2 16 64 / 325ms 74 / 332ms 67 / 206ms
7 16 142 / 1,353ms 111 / 576ms 72 / 240ms
12 16 193 / 1,441ms 190 / 646ms 77 / 193ms
24 16 205 / 1,508ms 199 / 774ms 109 / 242ms

这里有三点值得一读。

吞吐量(每秒请求数,6 个工作节点·并发 16)也随之上升。

前缀类型 Service 后的 vLLM 轮询调度 KV 路由
2 18.9 19.1 21.1
7 14.1 16.0 20.1
12 13.9 14.7 19.2
24 13.3 14.3 17.6

GPU 保持不变,但在 24 种前缀下多处理了 32% 的请求。这相当于减少了用于重新计算前缀的时间。

6. 在 3 个工作节点下是否同样如此

这是一个每张 GPU 放置一个工作节点的部署配置(并发 8)。

前缀类型 缓存 token 比例:基准线 / 轮询调度 / KV TTFT p99:基准线 / 轮询调度 / KV 每秒请求数:基准线 / 轮询调度 / KV
2 0.840 / 0.840 / 0.848 165 / 171 / 83ms 17.0 / 17.3 / 20.6
7 0.487 / 0.475 / 0.690 853 / 413 / 145ms 12.4 / 13.6 / 18.5
12 0.283 / 0.291 / 0.474 1,125 / 679 / 152ms 11.2 / 12.5 / 17.2
24 0.143 / 0.141 / 0.252 1,216 / 830 / 202ms 10.7 / 11.8 / 16.6

趋势是相同的。在 24 种前缀下,吞吐量增加了 55%。不过,因为只有 3 个工作节点,能够容纳的前缀总量减半,其命中率也低于 6 节点的部署配置。

还有一个负载超出容量的点。在 3 个工作节点·并发 16·24 种前缀的条件下,KV 路由的 TTFT p50 为 279ms,p99 为 962ms。虽然比基准线(563ms、3,060ms)要好,但并没有像其他情况那样降到 0.2 秒以内。路由只能减少浪费,而无法创造出短缺的计算资源。

7. 距离上限还有多接近

第 1 篇中计算的理论上限是“工作节点数 × 单台机器容纳的前缀数 ÷ 前缀种类数”。这是一个假设前缀互不重叠地分配给工作节点的值。

部署配置 前缀种类 上限 实际请求单位命中(并发 4 / 8 / 16)
6个工作节点 7 100% 93 / 83 / 91%
6个工作节点 12 100% 80 / 68 / 82%
6个工作节点 24 99% 52 / 43 / 52%
3个工作节点 7 100% 77 / 82 / 90%
3个工作节点 12 99% 53 / 58 / 64%
3个工作节点 24 49% 29 / 30 / 28%

默认配置下的 KV 路由在缓存紧张的区间内达到了 上限的一半到三分之二之间的效果。虽然比随机分散好两到三倍,但仍未达到毫无保留榨干容量的部署配置。

原因在第 4 节的表格中有所体现。在 24 种前缀中,单个前缀平均发往了 5 个工作节点。路由器不仅考虑重叠度,还会同时考虑工作节点的负载,因此如果重叠的工作节点很忙,它就会发往其他工作节点。该工作节点会重新计算并缓存前缀,从而挤掉其他前缀。缓存越紧张,这种挤占就会引发连锁反应。

Dynamo 文档中有一个用于调整重叠度与负载权重的路由器参数。本文仅按默认值进行了测量,更改该参数是否能更接近上限尚未进行实际测量。

8. 哈希种子与近似模式

文档和示例中要求为 worker 设置 PYTHONHASHSEED=0 。理由是 vLLM 的块哈希会经过 Python 的哈希,因此如果每个进程的 seed 不同,相同的字首在各个 worker 中就会产生不同的哈希。我们去掉了 seed 进行了重新测试(6 个 worker,并发 8,缓存 token 比例)。

字首种类 轮询 KV 路由 无 seed 近似模式
2 0.814 0.835 0.848 0.848
7 0.439 0.702 0.729 0.638
12 0.247 0.573 0.547 0.480
24 0.149 0.361 0.343 0.245

即使去掉 seed,结果也完全相同。 这四点的差距在 0.03 以内,方向也互有交错。其幅度无法与重复测试之间的抖动区分开来。至少在此次测试与配置下,没有 seed 并没有导致路由失效。原因尚未确认。我们未能从代码中确认运行时是自行确定 seed,还是路由器存在不依赖 worker 哈希的途径。既然这是文档要求的配置且没有额外成本,我们并不建议仅仅因为结果相同就将其去掉。

近似模式处于中间水平。 在该模式下,worker 不发送事件,路由器通过自己发送的请求记录来推测“那个 worker 上应该还留有这个字首”。在 7 种字首时,它与使用 KV 事件时没有太大差异(0.638 对 0.702),但在 24 种字首时降至 0.245,与轮询(0.149)的差距缩小了一半。这可能是因为缓存紧张导致块频繁被逐出时,路由器的推测与实际情况不符。可以将其理解为在难以开放事件端口的环境中可以使用的一种次优选择。

9. 总结

本次测量的局限性与第 1 篇相同。字首是均匀抽取的,请求长度全部相同,且模型较小。对于大模型来说,一个未命中的请求所需的计算时间要长得多,同样的命中率差距会体现为更大的时间差距,但这并非本文所测。当部署多个前端时,路由器之间是否能看到相同的映射表也未经实测。

参考资料

登录后即可点赞

评论

还没有评论。

登录后即可发表评论