结果:已合并(2026-10-17 03:08, llm-d/llm-d#2835). 事实上,这些数据取自 GitHub 的 PR、评审与检查记录,llm-d 源码(PR 基于的提交
696b5e81)、vLLM 源码(发布标签)、llm-d 自动扩缩器源码(标签 v0.9.0),以及我们留下的实测原始数据。时间均为韩国时间。截至 2026-10-17,尚无包含此变更的 llm-d 版本发布(最新版本为 v0.10.0,发布于 2026-09-29)。
目录
- 一句话总结
- 1. 问题所在:我们观察到的现象
- 2. 原因:源码中的哪一行出了问题
- 3. 我们改了什么:diff 要点
- 4. 反应:没有机器人,没有行内评论,耗时 2 小时 9 分钟
- 5. 收获:开源贡献技巧
- 链接
一句话总结
| PR | llm-d/llm-d#2835 docs: replace vLLM metric names that current vLLM no longer exports |
| 修改内容 | 仅修改了两个文档和一个仪表盘 JSON 中的指标名称。增加 6 行,删除 6 行 |
| 反应 | 无评审机器人·无行内评论。两个批准(一个无言批准, /lgtm 一个批准),一个站点预览机器人评论 |
| 结果 | PR 公开 10-17 00:59 → 合并 03:08,约 2 小时 9 分钟。通过 squash 方式合并单个提交(我们的账号为作者) |
1. 问题所在:我们观察到的现象
首先坦白说明,TT Lab 集群中并没有 llm-d。这次的问题并非运营故障,而是在学习过程中遇到的。在阅读围绕 vLLM 引擎的各种项目(production-stack、LMCache、llm-d)的代码时,我们追踪了“引擎定义的指标名称是如何扩散到其他仓库的查询中的”,相关内容已整理在 《阅读开源架构》第 4 篇中。
为了避免仅通过文档字面量比对指标名称,我们在提交 PR 前夕,将 vLLM v0.31.0(vllm/vllm-openai 镜像,当时的最新版本)作为一次性 Pod 运行在小型集群的单个 GPU 节点上(8GB 级别 GPU),并拉取了 /metrics 。为了避免下载任何额外内容,我们仅使用创建了配置文件的随机权重模型(Qwen2 架构,2 层,hidden 256, --load-format dummy),并发送了 5 个前 160 个 token 相同的请求(每个请求生成 16 个 token)。在其输出中,并没有出现 llm-d 文档所引用的名称。
| llm-d 文档引用的名称 | 出处 | v0.31.0 /metrics |
|---|---|---|
vllm:gpu_prefix_cache_hit_rate |
turn-priority-fairness 指南的观测指标表 | 无 |
vllm:time_per_output_token_seconds |
自动扩缩器 (WVA) 设计文档,指标列表一行与查询表一行 | 无 |
vllm:time_per_output_token_seconds_bucket |
DigitalOcean 仪表盘 “Time Per Output Token Latency” 面板的 3 个查询 | 无 |
在原始数据中,以这两个旧名称开头的行为 0 行,取而代之的是以下数值:
vllm:prefix_cache_queries_total{engine="0",model_name="tiny"} 805.0
vllm:prefix_cache_hits_total{engine="0",model_name="tiny"} 640.0
vllm:inter_token_latency_seconds_count{engine="0",model_name="tiny"} 75.0
这些数字与请求的形态完全吻合。5 个请求 × 161 个 token = 805 次查找,其中第一个请求全部未命中,其余四个请求各命中 160 个 token,共计 640 次命中(块大小 16,相同输出的 cache_config_info)。5 个分别生成 16 个 token 的请求,其 token 间的间隔为 15 × 5 = 75。命中率为 640 / 805 ≈ 0.795。
使用不存在的名称编写的 PromQL 不会报错,而是返回空结果(这也是 Prometheus 的常见行为)。按照指南输入查询的读者只会看到“什么都没有输出”,原因只能自己去寻找。仪表盘面板预计也会显示为空,不过我们没有将其上传到 Grafana 进行实际测试(未实测)。
以相同方式对 llm-d v0.10.0 标签的整个文档和指南进行扫描的结果也记录在第 4 篇中。 vllm: 在 71 个名称中,有 40 个存在于输出中,27 个是需要开启离线卸载(offload)或 NIXL 连接器才会出现的条件性指标,还有 4 个在 v0.31.0 的源码中没有定义。其中 4 个中的 3 个属于
time_per_output_token_seconds 系列。由于 PR 是基于 main 分支的,因此 v0.10.0 中尚不存在 fairness 指南。
2. 原因:源码中的哪一行出了问题
llm-d 的行号基于 PR 的基础提交 696b5e81 ,而 vLLM 基于发布标签。
引擎端:名称正如预告的那样消失了
vLLM 在删除指标时遵循一套明确的规则。根据 docs/usage/metrics.md 第 78–82 行
所述,它会在 X.Y 版本预告废弃,在 X.Y+1 版本中将其隐藏(--show-hidden-metrics-for-version=X.Y 可以使其恢复),并在 X.Y+2 版本中彻底删除。
vllm:gpu_prefix_cache_hit_rate是 V0 引擎的 Gauge。在 v0.9.0 的vllm/engine/metrics.py第 119–128 行 中,它带着“自 0.8 版本废弃,V1 中将由查询与命中计数器替代”、“自 0.9 版本隐藏,计划于 0.10 版本删除”的注释处于隐藏状态,而在 v0.10.0 的同名文件中已无此定义。V1 引擎不采用命中率,而是输出两个计数器vllm:prefix_cache_queries·vllm:prefix_cache_hits。设计文档(docs/design/metrics.md第 437–447 行)对此解释道:“不要用 Gauge 固定比例,而应让用户像rate(…[5m])那样自行计算所需区间的比例。”值得注意的是,fairness 指南头部指定的基准环境是 vLLM 0.26 。也就是说,即使在验证过该指南的版本中,这个 Gauge 也已经不存在了。vllm:time_per_output_token_seconds是 V1 引擎的 Histogram。v0.11.0 的vllm/v1/metrics/loggers.py第 387–388 行 的注释写着“自 0.11 版本废弃,已更名为vllm:inter_token_latency_seconds”,而在 v0.12.0(第 689–692 行)中它被改为隐藏状态,并附加了“将在 0.13.0 版本删除”的待办事项。实际上,它一直以隐藏状态保留到了 v0.14.0(第 718–721 行),而在 v0.15.0(2026-01-29 发布)的同名文件中已无此定义。当前的名称是vllm:inter_token_latency_seconds。
引擎严格遵循了规则,有时甚至比规则更慷慨地保留了这些名称。问题在于, 读取这些名称的消费端位于其他仓库中。此外,源码中的计数器名称中虽然没有 _total ,但由于 prometheus_client 在导出时会自动添加前缀,因此在查询时必须写成 vllm:prefix_cache_hits_total
的形式。在使用 grep 检索源码并迁移名称时,这里往往是最容易混淆的地方。
消费端:代码跟上了,但文档留在了原地
- 自动扩缩器设计文档
docs/architecture/autoscaling/wva.md的第 144 行指标列表以及 第 210 行 的avg_itl查询中依然写着旧名称。然而,该文档所描述的收集器代码(仓库名为 llm-d-autoscaling)v0.9.0 版本的internal/collector/registration/queueing_model.go第 70 行 已经开始查询vllm:inter_token_latency_seconds_sum·_count了。 文档与代码脱节了。 - DigitalOcean 仪表盘 JSON 中的 “Time Per Output Token Latency” 面板的 p95、p90、p50 查询(第 1544 行,
第 1559 行,
第 1575 行)全部将
vllm:time_per_output_token_seconds_bucket传入了histogram_quantile中。 - fairness 指南的观测表 第 663 行
则要求查看作为“策略优化机制”的
vllm:gpu_prefix_cache_hit_rate。
3. 我们改了什么:diff 要点
我们仅修改了名称。文案、面板标题以及查询的其余部分均保持原样。
# docs/architecture/autoscaling/wva.md
-- `vllm:time_per_output_token_seconds` -- ITL histogram (sum/count for average)
+- `vllm:inter_token_latency_seconds` -- ITL histogram (sum/count for average)
-| `avg_itl` | `rate(vllm:time_per_output_token_seconds_sum[1m]) / rate(..._count[1m])` | ...
+| `avg_itl` | `rate(vllm:inter_token_latency_seconds_sum[1m]) / rate(..._count[1m])` | ...
# docs/infrastructure/providers/digitalocean/monitoring/inference-gateway-dashboard.json (3곳)
-"expr": "histogram_quantile(0.95, sum by(le) (rate(vllm:time_per_output_token_seconds_bucket[$__rate_interval])))",
+"expr": "histogram_quantile(0.95, sum by(le) (rate(vllm:inter_token_latency_seconds_bucket[$__rate_interval])))",
# guides/turn-priority-fairness/README.md
-| `vllm:gpu_prefix_cache_hit_rate` | The mechanism the strategy is optimizing. |
+| `rate(vllm:prefix_cache_hits_total[5m]) / rate(vllm:prefix_cache_queries_total[5m])` | Prefix-cache hit ratio: the mechanism the strategy is optimizing. |
在命中率的位置,我们没有填入单个 Gauge 名称,而是填入了由两个计数器计算比例的表达式。这是因为没有同名的 Gauge 可供替换,这也是 vLLM 设计文档所推荐的写法。区间的 [5m] 则保持与设计文档中的示例一致。
PR 正文在一个表格中列出了文件、旧名称、新名称和原因,并直接附上了第 1 节中的 grep 输出的三行内容以及“旧名称为 0 行”的结论。同时注明了:“仪表盘 JSON 仍可正常解析,未在 Grafana 中实际上传测试,变动的仅是 histogram_quantile 三个表达式内部的名称,面板标题保持不变”。llm-d 的 CONTRIBUTING 规定,接受由 AI 协助的贡献时应遵循与其他贡献相同的标准,但提交者必须作为记录上的作者能够亲自解释相关内容。因此,我们在正文末尾添加了一句话,说明该内容是在 AI 的协助下编写并经过了亲自审查。我们提交了一个单独的 commit,并附上了仓库要求的 DCO 签名(Signed-off-by) 与 SSH 签名。检查项中也包含了对是否为签名提交的验证。
4. 反应:没有机器人,没有行内评论,耗时 2 小时 9 分钟
| 时间(10-17) | 事件 |
|---|---|
| 00:45 | Commit(DCO 签名·SSH 签名) |
| 00:59 | 公开 PR |
| 00:59 ~ | 21 项检查:通过 12,跳过 9,失败 0 |
| 01:53 | 根目录 OWNERS 的一位批准者无言批准 |
| 01:56 | 站点预览机器人评论:“已使用此 commit 构建站点,请下载打开查看” |
| 03:08 | 另一位维护者随 /lgtm 一同批准,11 秒后通过 squash 合并 |
通过的检查包括 DCO、签名提交验证、pre-commit、指南检查(CI Guides Check)、指南 dry-run 以及站点预览构建。跳过的 9 个属于镜像构建等操作,推测是因为属于仅修改文档的 PR 而被跳过。
与第 1 篇中的 SeaweedFS 不同,这里没有评审机器人介入;与第 2 篇中的 Grafana 图表不同,这里也没有一句 “Thanks!”。没有行内评论,没有提问,仅凭两个批准就顺利结束了。我们无需采取任何后续应对措施。合并提交 2b0b8de7
的作者是我们的账号,消息则是我们 commit 消息标题末尾附上了 PR 编号的内容。
还有一个结构相同的 PR。production-stack 的默认仪表盘也在调用已消失的 vllm:gpu_prefix_cache_* 名称,为此我们提交了
vllm-project/production-stack#1147 ,截至 2026-10-17 仍在进行中。
5. 收获:开源贡献技巧
- 同一个名称会扩散到多个仓库中。 引擎虽然按规则进行了废弃预告、隐藏和删除,但这些规则仅在引擎仓库内部遵守。指南、设计文档、仪表盘 JSON 分布在其他仓库中,即便名称错误也不会报错,只会返回空结果。只要进行一次引擎的
/metrics以及消费端的vllm:名称列表检查,就能找出这些隐患。 - 不要猜测替代名称,应当从代码中寻找。 WVA 文档中的替代名称其实已经被该文档所描述的收集器代码所使用了。只要用代码行数证明“文档与代码不符”,维护者几乎无需额外核实。
- 警惕相似的名称。 在 v0.31.0 中有
vllm:request_time_per_output_token_seconds有另一个名为 的直方图。从相同的原始数据来看,time_per_output_token_seconds如果不固定行首用 grep 搜索会出 27 行,而用^vllm:time_per_output_token_seconds固定则为 0 行。 况且源码中的注释说明一个叫“token 间延迟”,另一个叫“单个请求每个输出 token 的时间”,含义也完全不同。“名字差不多, 就把这个换成那个吧”往往会变成一个错误的 PR。 - 测过的数据和没测过的数据,权重不能混为一谈。 没上传到 Grafana 查看这句话并没有削弱这个 PR 的力度。维护者会根据 验证了什么、未验证什么来做出判断。
- 文档 PR 也有其验证的路径。 llm-d 即使只改动文档也会构建网站并提供预览,还会运行指南检查和 dry-run。 越是小的 PR,如果这些检查全部变绿,审阅者需要看的就只有几行 diff。
Titi 的总结。 朋友改了名字,但通讯录里还留着旧名字。叫他却没有回应,大家都以为是“联系不上了” 呢。亲自呼喊了一声(0 行!),确认了新名字(75 行!),修改了通讯录里的三个地方后,仅仅两小时就盖上了“已采纳”的印章。 大家决定以后一定要小心,避免打错给名字相似的其他朋友。
链接
所有资料均于 2026-10-17 查阅。
- PR: llm-d/llm-d#2835 (合并提交
2b0b8de7) - llm-d 源码(基础提交):
docs/architecture/autoscaling/wva.md,inference-gateway-dashboard.json,guides/turn-priority-fairness/README.md,CONTRIBUTING.md - 自动扩缩容收集器:
queueing_model.go(v0.9.0) - vLLM 指标废弃规则:
docs/usage/metrics.md(v0.31.0), 选择该计数器的原因:docs/design/metrics.md(v0.31.0) - vLLM 指标定义:
vllm/v1/metrics/loggers.py(v0.31.0), 旧的 V0 Gaugevllm/engine/metrics.py(v0.9.0) - 类似形态的 PR(进行中): vllm-project/production-stack#1147