TTFT 多了 15 毫秒之后:一套实证过的 KV Cache 性能分析方法论
最近为推理引擎接入一个新的 KV Connector,把 KV Cache 的存取指向一个远端存储层。上线前压测与本地基线对照(我们知道带宽和延迟),两份配置只差一个 KV 来源,TTFT 稳定多出 15 毫秒(除去传输)。拷贝带宽跑满、GPU 大部分时间空闲、日志里没有任何报错。这 15 毫秒去了哪里,一时难以定位。
排查从九条假设开始,逐条检验,每条配一个判别实验。怀疑批数:隔离微基准中 N 批与 1 批速度相同,排除。怀疑 pageable 主存拷贝拖慢装载:更换执行器后耗时无变化,排除。怀疑拷贝与计算相互干扰:2×2 对照矩阵两侧结果一致,排除。其余假设以同样的方式逐一否决。
最终由一条五段分解的时间链定位:五段求和与客户端测得的 TTFT 误差 0.000 毫秒,链条闭合,差额锁定在提交段。该段的仪器读数一直是「稳态 1.5 毫秒」,真实成本为 22.5 毫秒:仪器量了任务分配,未量真正向显存填充数据的环节。向量化改写后,这笔开销降到 0.94 毫秒(见 3.2)。修复完成后,接入远端层的配置端到端反超本地基线 5.8%(过程见 2.5)。
博主把这次排查与同类实践中反复奏效的方法整理成本文,供大家参考。同类场景还有几种形态:PD 分离要跨实例传输 KV,KV 卸载要把历史块搬入第二级存储,分层缓存要在多级介质之间搬运,排查面对的是同一条链。本文组织成六件工具、六类问题与一份七步操作清单。数字口径说明如下:全部数字出自生产级推理引擎(vLLM、SGLang 系)的真机轮次与隔离微基准;介质绝对带宽、端到端时延包络与模型指纹按发布口径脱敏,只保留比值与结论,其余工程数字原样保留(写于 2026-11)。
阅读前提:熟悉 vLLM/SGLang 的调度模型与 KV 传输接口。本站PD 分离架构下的 KV Cache 传输与KV Offloading 架构对比讲这些机制的「是什么」,本文回答「慢了怎么查」。
一、性能问题分布在一条很长的链上
KV Cache 的性能问题分布在这条链上:介质 → 拷贝几何 → 宿主侧开销 → 同步与放行契约 → 步级调度 → 观测口径本身。这条链很长,症状(TTFT 高)与根因隔着好几层,一个症状还常常是多类叠加,仅凭直觉难以定位。开场那次排查里,拷贝几何、批数、拷贝-计算干扰几条线索同时在场,最后靠排除链切开。
二、六件分析工具
2.1 时钟链分解 + 闭合校验
把目标指标拆成首尾相接的段,每段配独立仪器,然后做闭合校验:Σ 各段 = 客户端测到的总量,误差 ±1ms 内才算闭合。
实证:那次压测的 +15ms 就是这么定位的。链拆成五段(入站 / 装载+放行 / 排批 / 前向→首 token / 出队→客户端),五段求和与客户端 TTFT 的误差为 0.000ms,差额一次锁定在提交段。仪器边界必须由外部闭合定义:那台一直报「稳态 1.5ms」的旧仪器量错了边界,量了任务分配,未量引擎填充,与真值差 15 倍。
2.2 排除链 + 判别实验
对每个候选假设:先写下判别实验和两种结果的读法,再跑。结论只有三种——证实、证伪、仪器不足以判。判别测试要用变异自证钉住:把假设的机制摘掉,测试必须变红,否则测试本身没有判别力。
那次压测共列出九条候选假设,逐一证伪,每条都有一条一锤定音的判据:
-
• 装载粒度(N 层 / N 个张量,计数核对) -
• 提交开销 -
• store 积压(对照配置) -
• pageable 主存拷贝(换执行器无变化) -
• 批数(隔离微基准,N 批与 1 批同速) -
• 拷贝几何(隔离环境全形态实测) -
• 层等待暴露(实测合计 0.15ms) -
• 拷贝-计算干扰(2×2 矩阵两侧都不掉) -
• 整段重算(store 计数逐字相同)
配套纪律:夹具会掩盖真问题。mock 引擎的同步拷贝把「放行与完成回执不同批」的窗口偶然闭合,单测全绿之下藏着真机崩溃窗;CPU mock 测不出带宽与拷贝几何。单测保 roundtrip,性能语义必须真机或隔离微基准证明。跑 CPU mock 还有一个环境问题:容器里必须把 CUDA_VISIBLE_DEVICES 置空,否则 torch 在 GPU 上分配张量,而 CPU mock 对设备指针做主机 memcpy,进程直接 SIGSEGV,连断言都不会触发。
2.3 同轮对照 + 锚点归一
比较只在同轮、同镜像、同协议内有效。跨轮数字不可直接比:实测的最大漂移源是观测开关本身,同一配置开关 trace 后,单段耗时漂移可达 4ms 级;同一观测状态的多轮之间则收敛到 1.6ms 以内。需要跨阶段比较时,用跨阶段都在场的锚点配置归一,同时报裸差与归一差。
复跑还要看漂移方向。两组配置的复跑漂移方向相反时,单轮「对齐」的结论可能落在噪声带内。落到流程上:判据锚(指标阈值)收锚之前不动其他改动,一次一变量,每轮读数独立入账。
2.4 观测口径纪律:仪器自身是嫌疑人
四条实证过的口径陷阱,外加一招定界法:
-
• poll 压缩。异步框架里 per-job 的 transfer_time 实际是「提交到下次轮询」的上界,所有 job 几乎同值,不能反推 GPU 时长或带宽。我们曾据此把主机拷贝带宽低估一半以上,那组「7.4/15.3 GB/s」的对比就是这类伪值;如今能引用的只有「两种几何不等价」这个方向。带宽唯一可信源是隔离微基准。 -
• 仪器扰动。DEBUG 日志里展开一次列表,TTFT 抬高 16ms;每步几十个计时 event 也有成本;trace 开关的实测扰动在 1.5–4.0ms,不同配置敏感度不同。性能结论只用无观测的轮次;要时间链就另跑 traced 轮,两组数字永不混比。 -
• 配对与聚合口径。请求级时间线必须按 req_id 配对,全局时间戳配对产出过 900ms 级伪值。取数聚合同理:同一请求匹配到两条日志、末条的命中标记为 0——聚合应取首条或最大值,而非窗口末条。 -
• 指标语义。指标名字与语义可能错位:SGLang 的 full_token_usage(srt/managers/scheduler_components/pool_stats_observer.py)名字里写着 usage,量的却是被运行请求锁住的活跃 KV,不是容量水位。我们误读了两次才确立判读标准。指标语义与实现对照过一次之后才可信。 -
• 金丝雀定界法。仪器零输出时,一个顶格金丝雀可以将问题三分支一次定位:金丝雀未触发 = 补丁不在实际执行的路径上;金丝雀触发而钩子未触发 = 路径被图捕获或编译分支绕过;两者都触发而数据未产生 = 排空卡死,最可能的原因是静默 except。
仪器内的异常必须记录一行日志后再吞掉。宽 except 是静默出口:首次异常之后所有仪器失效,而表面上一切正常。
2.5 参照系优先:native 对齐清单
与 native 路径(本地基线、上游实现)逐项对齐盘点:框架契约层对齐之后,数据面、执行器、时窗的每一处偏离都是嫌疑人,直到解释清楚。native 用更简单的形态达到了目的,优先复刻而不是发明新机制。
实证:自研拷贝路径相对 native 有三处未对账偏离(专用流+跨流屏障、自研执行器、自有布局),其中「每提交一次跨流屏障 × N 层」实测每装载 3.9ms,修掉后语义不变。对齐同样有反向收益:过程中发现 native 自身的装载是串行暴露的,而重叠执行可以把它藏进管线,最终端到端反超本地基线 5.8%。
2.6 微基准先行,单变量推进
机制问题先在隔离微基准里定死(受控、便宜、变量干净),再上引擎改动,最后才起真机验证。「微基准搞清楚 → 修引擎 → 轮次验证」一轮定案,避免在线上以十几分钟一次的节奏试错。
三、六类问题与解法
六类的分布与每类的第一反应如下;排查沿链走,不按猜测走。

