78 万次查询,零命中:我给 vLLM 提了个 bug
先看一个荒诞的数字:788,000 比 0。
这是我推理服务上 prefix caching 的查询计数器。78 万多次查询,命中零次。
这个功能是开着的。配置里写着开。
背景:prefix cache 为什么重要
跑大模型对话服务,多轮对话的性能全指望它。用户第二轮提问时,前面几轮的上下文不用重新计算,从缓存里增量续算就行。
没有它,每一轮都是冷启动:首字延迟和上下文长度成正比,对话越长,每一轮等得越久。
我的场景恰好是多轮长上下文,所以当时部署完就把它打开了,还专门确认过启动参数里没有那个禁用开关。
然后我发现多轮对话的首字延迟完全不符合预期。做实验:同一段 13K token 的 prompt,原样发三次。
三次首字延迟:15.9 秒、15.9 秒、15.9 秒。
如果缓存存在,第二三次应该明显更快。数字纹丝不动,说明每次都在冷启动。
先怀疑自己,再怀疑世界
按排查纪律,我先花时间排除了自己的问题:开关确认开启、版本核对、发请求的方式核对。都对。
然后去 GitHub 找同类。还真有:一个 H200 环境下的已知报告,症状几乎一样,DeepSeek-V4-Flash 的混合 KV 分组里,前缀块会被重分配销毁,导致命中长度塌缩成零。
但对照做完,发现我们的情况更糟。那个 issue 的环境里,紧挨着重发同一个请求是能命中的;而我们连紧邻重发都不命中。
这说明我们碰到的是更深一层的东西。
翻源码:这条路径上没有缓存的代码
于是去翻源码。翻完之后,事情变得很清楚,也很无奈。
我们这套 Thor 设备上的推理路径,是部署方自己维护的私有分支:上游 vLLM 的主仓库里根本没有对应的实现文件,硬件支持列表里也没有这块芯片的身影。
而分支里的实现,把所有后端变体都路由到了同一个注意力类上,这个类走的是 Triton 的退化方案,里面压根没有 prefix 查询的逻辑。
也就是说:开关开了,请求也来问了,但这条路径上从来没有实现过查询这件事。78 万次查询,问的是一个不存在的东西。
配置开着,不代表功能存在。开关和实现,是两回事。
报一个合格的 bug
定位清楚了,下一步是提交给谁。上游主仓库不包含这条路径,但问题真实存在,所有用这套部署的用户都会踩。我把三样东西整理成了一份报告:
复现数据:78 万次查询零命中的计数器、同段 prompt 三次 15.9 秒的延迟记录。
定位分析:源码层面说明这条 Thor 路径的注意力实现里没有 prefix 查询逻辑。
对照材料:关联上游已知的 H200 变体 issue 和 roadmap 讨论,说明不是重复报告,是覆盖缺失。
8 月 31 日晚上 8 点 40 提交。就在当天下午整机复位风波之后的几个小时,那一天的服务器很热闹。
issue 目前还挂着。短期方案我也认了:接受冷启动,把多轮对话的上下文控制在能忍的范围里。
三件事带走
第一,遇到"配置开了不生效",先别急着改配置。配置只是申请,功能存在才发放。去确认实现里到底有没有这段代码。
第二,用 fork 的时候心里要有数:上游的修复到不了你,你的 bug 也别指望别人顺手修。私有路径的每一个坑,都是你自己的。
第三,报 bug 的规格是三件套:能复现的数据、定位到的分析、和已知问题的对照。少了任何一样,维护者都得多花一小时,或者干脆不花。
那个计数器后来一直在涨,还是零命中。看着它,就像看着一部永远占线的电话。
你给开源项目报过最有意思的 bug 是什么?来聊聊。