状态:草稿(2026-11-14),实测。 本文中的数据由作者于 2026-11-13 亲自实测得出。测试环境为 Kubernetes v1.34,配备三台 GPU 服务器(一张 24GB 级 GPU,两张 8GB 级笔记本 GPU),节点间网络为 1GbE。推理引擎为搭载在 Dynamo 运行时镜像中的
vllm-runtime:1.6.2vLLM 0.31.0,模型为 Qwen3-0.6B。由于这是使用小模型和小缓存测得的数据,绝对时间无法直接套用到大模型上,但可以借鉴的是命中率的确定方式。 编写规则:以实测数据为基础,未能测出的标记为“未实测”,无法确认的标记为“未确认”。
Service 后方 vLLM 的前缀缓存
目录
1. 为什么要从测定基准线开始
本系列文章通过实际测量,记录了在一个原本只使用 vLLM 的 Kubernetes 集群中引入 NVIDIA Dynamo 的过程。Dynamo 宣传的主打功能之一便是 KV 感知路由(KV-aware routing)。该功能能够将请求发送至已经持有与请求前缀相同 KV 缓存的 Worker。
要想评估其价值,首先必须弄清楚“现有方案究竟浪费了多少缓存”。所谓现有方案,即在多个 vLLM Pod 前方放置一个 Service 的经典架构。本文仅针对该架构进行测试。Dynamo 将从下一篇开始登场。
在此先明确一个背景知识。vLLM 的前缀缓存(prefix cache)机制是指,如果当前请求的前缀与之前的请求相同,系统便会直接复用该部分的 KV 缓存,而无需重新计算。对于在请求前附加系统提示词(system prompt)或长文档的服务来说,这可以大幅缩短首字延迟(TTFT)。然而,这种缓存是 每个进程各自独立 的。Pod A 计算并保存的 KV 缓存,Pod B 并无所知。
2. 测试对象与测试方法
| 项目 | 数值 |
|---|---|
| 模型 | Qwen3-0.6B,单 Token 的 KV 大小为 114,688 字节 |
| 引擎参数 | --max-model-len 4096 --kv-cache-memory-bytes 1073741824 --gpu-memory-utilization 0.01 |
| 单台 Worker 的 KV 容量 | 1GiB ÷ 114,688 字节 = 约 9,362 个 Token |
| 请求 | 共享前缀约 2,000 个 Token + 每个请求各不相同的尾部约 300 个 Token,输出 64 个 Token |
| 实际输入长度 | 平均 2,371 个 Token,若命中缓存,其中 2,032 个 Token 将会被复用 |
| 前缀种类数 | 2, 7, 12, 24 |
| 并发请求数 | 4, 8, 16 |
| 重复次数 | 每个数据点测试 3 次,取中位数 |
| 请求总数 | 每个数据点的前缀数 × 20(最少 200 个) |
我们刻意将 KV 容量限制在 1GiB。由于单个请求长度为 2,371 个 Token,因此单台 Worker 仅能容纳约 3.95 个请求体量的 KV 缓存。这样做的目的是为了模拟当种类超过 4 种时,单台设备无法完全容纳所有前缀的场景。这是一种在小型设备上模拟实际生产服务中使用大模型和长上下文时所面临的缓存压力的测试方法。
测试分为两种部署配置:
- 6 台 Worker:将一张 24GB 级 GPU 通过时分复用拆分为 4 台 Worker,另外两张 8GB 级笔记本 GPU 各跑 1 台。
- 3 台 Worker:每张 GPU 运行 1 台 Worker。
负载生成器运行在另一台没有 GPU 的服务器上,并通过 Service 的 ClusterIP 发送请求。为了确保测试公平,我们遵守了以下三点原则:
- 每个请求建立新连接。 Service 基于连接维度来选择 Pod。如果客户端复用连接,就会一直绑定到同一个 Pod 上,从而让基准线看起来比实际情况更好。
- 使用随机种子(Seed)固定请求顺序。 在后续针对 Dynamo 的测试变体中,也会发送相同顺序的完全相同的请求。
- 在统计中剔除预热(Warmup)阶段。 在正式测量前,先为每种前缀发送一次请求,且这些预热请求不计入统计。
命中情况通过两种途径进行确认。一是响应中的 usage.prompt_tokens_details.cached_tokens ,二是抓取各个 Worker 的 vllm:prefix_cache_hits_total ÷ vllm:prefix_cache_queries_total 在测量时间段内的差值。在 24 个数据点中,这两个数值有 23 个精确到了小数点后三位,剩下的一个也仅相差 0.001。例如,在前缀 2 种、并发 4 的情况下,两者的测量值均为 0.814。
这里有一个陷阱。如果单独运行的 vLLM 服务器没有开启 --enable-prompt-tokens-details ,就不会在响应中返回 cached_tokens 。如果在没有这个参数的情况下进行测试,所有请求看起来都会像是未命中。
3. 结果:前缀增多会导致命中率崩溃
此图表示请求中命中缓存的比例(并发 8,取 3 次测试的中位数)。
| 前缀种类数 | 6 台 Worker | 3 台 Worker | 3.95 ÷ 前缀数 |
|---|---|---|---|
| 2 | 95.0% | 98.0% | 100%(上限) |
| 7 | 55.5% | 59.5% | 56.4% |
| 12 | 32.1% | 33.8% | 32.9% |
| 24 | 17.5% | 16.9% | 16.5% |
在并发 4 和并发 16 的情况下也呈现出相同的趋势。以 6 台 Worker 为例,7 种前缀时的命中率为 52.5% 和 52.0%,12 种时为 28.7% 和 32.1%,24 种时为 16.0% 和 16.7%。
有两点值得我们注意:
首先,命中率与一个简单的公式相吻合。 当 Service 将请求均匀打散时,每个 Worker 都会接收到混杂了各种前缀的请求。由于 Worker 会优先清理最久未使用的块(LRU),因此单台设备的缓存中会保留“最近看过的约 3.95 种前缀”。下一个请求的前缀刚好落在其中的概率就是 3.95 ÷ 前缀种类数。表格最后一列即为该理论值,它与实测结果的误差在 1%~3.9%(应为 1~3%p)以内。
其次,增加 Worker 数量并不能提高命中率。 6 台和 3 台的表现几乎完全一致。这是因为在随机分发模式下,每个 Worker 上都在各自独立地重复着相同的事情。尽管总的 KV 容量翻倍了,但命中率却毫无变化。如果将 6 台 Worker 的容量加起来,本可以容纳大约 23 种前缀;因此在 24 种前缀的负载下,原本能够挽救大部分缓存的总容量,最终却只有 16% 发挥了作用。
前缀为 2 种时未能达到 100% 的原因也可以通过计算得出。如果使用 6 台 Worker 和 2 种前缀,则(Worker, 前缀)的组合共有 12 种。2 次预热填满了其中的 2 种,其余 10 种组合在首次遇到时会各自未命中一次。实测结果显示,在三次测试中,200 个请求里均有 190 个命中。3 台 Worker 的组合数为 6,减去 2 种预热后有 4 次未命中,从而实现了 196 次命中,这一结果在三次测试中也完全相同。
4. 命中与未命中在首字延迟(TTFT)上有何差异
我们在并发 4 的情况下,将请求按命中与未命中进行拆分,并观察了 TTFT(6 台 Worker,三次测试的合并数据)。
| 前缀种类 | 命中请求数 | 命中 p50 / p90 | 未命中请求数 | 未命中 p50 / p90 |
|---|---|---|---|---|
| 2 | 570 | 37 / 49ms | 30 | 113 / 164ms |
| 7 | 311 | 41 / 63ms | 289 | 107 / 140ms |
| 12 | 200 | 42 / 61ms | 520 | 105 / 141ms |
| 24 | 218 | 36 / 55ms | 1,222 | 104 / 139ms |
命中时约为 40ms,未命中时约为 105~113ms。单个请求的耗时不会随前缀种类的增加而改变, 改变的只有两类请求的占比。 因此,随着前缀增多,整体 TTFT 的中位数会向未命中的方向偏移。
| 前缀种类 | 并发 4 p50 / p99 | 并发 8 p50 / p99 | 并发 16 p50 / p99 |
|---|---|---|---|
| 2 | 37 / 122ms | 47 / 190ms | 64 / 325ms |
| 7 | 63 / 184ms | 93 / 526ms | 142 / 1,353ms |
| 12 | 94 / 193ms | 126 / 493ms | 194 / 1,441ms |
| 24 | 101 / 254ms | 136 / 635ms | 205 / 1,508ms |
(6 台 Worker,三次测试的中位数。)在并发 16 时,长尾延迟显著变长。当有 2 种前缀时,p99 为 325ms,而从 7 种前缀开始则恶化到了 1.3~1.5 秒。这是因为未命中的请求必须完整计算全部 2,371 个 Token,而这种计算会拖慢在同一 GPU 上运行的其他请求的进度。
吞吐量也随之下降。在 6 台 Worker、并发 16 的条件下,2 种前缀时的吞吐量为每秒 18.9 个请求,而 24 种前缀时降至 13.3 个。在 3 台 Worker 的配置下,则从 25.1 个降至 11.7 个。
尽管 3 台 Worker 的部署并没有对 GPU 进行时分复用,但在并发 16 时其长尾延迟反而更长(24 种前缀下的 p99 高达 3,060ms)。这是因为同样的负载压在了更少数量的 Worker 上。相反,在 2 种前缀的情况下,3 台 Worker 方案的吞吐量更高,因为当一张 24GB 级 GPU 被拆分给 4 个 Worker 时,计算资源并不会增加。我们比较这两种配置的绝对时间并非本文的目的,我们只需得出这样一个结论: 命中率遵循与部署配置无关的同一公式。
5. 这一基准线意味着什么
综上所述,在 Service 后方使用 vLLM 时,前缀缓存的命中率由以下公式决定:
적중률 ≈ min(1, 워커 한 대가 담는 접두 수 ÷ 자주 쓰이는 접두의 종류 수)
该公式中并未包含 Worker 数量。因此,我们可以得出以下结论:
- 如果前缀种类能够完全放入单台 Worker 中,那么即使改变路由策略也几乎捞不到什么好处。在前缀为 2 种的情况下,损失的仅仅是每种组合在首次遇到时的那一次未命中。
- 一旦前缀总量开始超出单台设备的容量,随机分发就会导致容量被 Worker 数量成倍重复占用。不同的 Worker 会各自重复计算并存储相同的前缀,而这又会反过来挤掉彼此不同的前缀。
- 如果按前缀将请求分发给对应的 Worker,每个 Worker 需要处理的前缀种类就会减少。理论上的上限会提升为“Worker 数量 × 单台可容纳的前缀数 ÷ 前缀种类总数”。以此计算,如果是 6 台 Worker,即使面对 24 种前缀,也能实现大部分命中。
最后一项正是 KV 感知路由所承诺的特性。我们通过以相同顺序再次发送相同的请求,来实际验证它究竟能多大程度上逼近这一理论上限,以及多经过一层前端所带来的开销究竟是多少。
在此也一并列出本次测量的局限性:
- 测试中均匀抽取了前缀。在实际生产服务中,某些前缀的使用频率会显著更高,因此随机分发的实际命中率可能会比本测试更高。未对热门分布进行实测。
- 请求长度完全相同。如果长度参差不齐,“单台可容纳的前缀数”便不再是一个固定值。
- 人为将 KV 容量固定在了 1GiB。如果给予充足的容量,即使在相同的前缀数下也不会产生压力。我们真正应当关注的不是前缀数量,而是 前缀总量与单机容量的比例。
- 混用了不同型号的 GPU。不同 Worker 之间的运行速度差异可能会导致 TTFT 的分布范围变宽。
参考
- vLLM 官方文档:Automatic Prefix Caching。
- 测量工具与原始数据摘要位于仓库的
docs/experiments/2026-11-13-dynamo-adoption/中。 - 关于 KV 缓存的原理以及单 Token 字节数的计算,请参阅 LLM KV 缓存解剖(第一篇);关于 Dynamo 的架构与以往实测,请参阅 第四篇。