从症状到根因的调试路径#

这一节会给出一条更像运行手册的调试路线:先看什么、再排什么、哪些证据能真正缩小问题边界。

为什么这一节必须放在最后#

前面的调试章节已经把 request logger、trace、metrics、profiling、测试和回归路径分别讲出来了,但如果没有一节把它们重新压成一条工作顺序,读者在现场仍然很容易知道"有哪些工具",却不知道"第一步到底该做什么"。

这一节的职责,就是把前面几节重新收束成三条从症状走到根因的具体路径。

场景一:请求高延迟或 P99 劣化#

症状ttft_secondse2e_req_latency_seconds 的 P99 升高。

第一步:拉 metrics 确认资源状态:

curl http://localhost:30000/metrics | grep -E 'num_running|num_waiting|token_usage|cache_hit'

根据数字判断属于哪类问题:

现象指向下一步
num_waiting 远大于 num_running调度瓶颈token_usage:若 > 0.9,是 KV 压力导致 batch 受限
token_usage > 0.9,cache_hit_ratio 下降KV 资源紧张看 waiting queue 里请求的平均长度;考虑调整 --mem-fraction-static
num_running 饱和,inter_token_latency 正常正常高负载关注 gen_throughput 是否持续下降,考虑扩容
两者都低但 P99 高单请求问题进入第二步看 per-request trace

第二步:找到高延迟的单个请求,通过 rid 拿到它的 ReqTimeStats

TTFT = 2.8s 但 prefill_start_time - tokenize_end_time = 2.3s

这说明高延迟的 2.3 秒发生在 waiting queue 里,不是 prefill 本身慢。对应的修方向是调度侧,不是 GPU 侧。

第三步:如果 prefill latency 本身高(first_token_time - prefill_start_time > 预期),才进入 torch profiler 看具体内核分布。

场景二:OOM 或显存报错#

症状:服务进程 OOM 崩溃,或 token_usage 长期接近 1.0。

第一步:看 token_usage 的历史趋势。如果是持续爬升到 1.0,说明 KV cache 没有足够驱逐空间:

curl http://localhost:30000/metrics | grep token_usage
sglang:token_usage 0.97

第二步:查 num_running_reqs 和当前 batch 的平均序列长度。如果单个请求 output_len 远超预期,可能是请求没有设置 max_new_tokens,导致 decode 一直跑不停,占满 KV。

第三步:看 request logger 里的 finish_reason

rid=abc123 | finish_reason=length | input=512 toks | output=4096 toks

如果大量请求以 length 结束而不是 stop,说明 max_new_tokens 没有设置上限。这不是 GPU 内存泄漏,而是请求本身的配置问题。

第四步:如果请求配置没问题,但 token_usage 仍然长期饱和,看 cache_hit_ratio。如果命中率很高(> 0.8),说明 RadixCache 保留了大量前缀,显存在"有意识地保留"。这时应该考虑调整 eviction_policymem-fraction-static 参数,而不是当作 bug 处理。

场景三:prefix cache 命中率突然下降#

症状cache_hit_ratio 从 0.7 以上下降到 0.3 以下,但请求内容没有改变。

第一步:确认请求的 input_ids 是否真的一致。有几类常见的"看起来一样但其实不同":

  • system prompt 里有时间戳或随机 seed,导致每次 tokenize 出来的 input_ids 不同;
  • chat template 渲染后有空格或换行差异;
  • 不同 LoRA 路由到了同一个 rank,但 lora_id 不同,cache 无法共享。

第二步:如果 input_ids 确实一致,看 token_usage:如果 token_usage 接近 1.0,可能是 RadixCache 在频繁驱逐之前 committed 的前缀节点——命中率下降是驱逐压力的结果,不是 cache 逻辑的 bug。

第三步:如果 token_usage 正常但命中率仍低,检查 page_size 配置。RadixCache 的匹配只在 page_size 对齐的边界上发生(默认 page_size=16)。如果两个请求的公共前缀长度不是 16 的倍数,最后一个未满页不会被复用。例如公共前缀 20 tokens,只有前 16 个 tokens 能被复用。

一个最小的值班工作流#

把三个场景的共同结构压成一个最短流程:

症状(高延迟 / OOM / 命中率低)
  → 先看 metrics 确认全局状态(num_running, token_usage, cache_hit_ratio)
    → 找 request logger 锁定单个有问题的请求(rid + finish_reason)
      → 用 ReqTimeStats / trace 切开时间线(queue wait? prefill? decode?)
        → 只有在确认了"慢在执行侧"之后,才打开 profiling
          → 修复
            → 对比修复前后的 metrics histogram(P99 是否恢复)

这条工作流的核心价值,是它强迫你先把"问题属于哪一层"判断清楚,再选对应工具,而不是一上来就打开最重的工具。

小结#

这一节真正想补齐的,不是更多工具,而是一种顺序感:

  • 先用 metrics 确认全局状态和问题层级;
  • 再用 request logger + trace 缩小到单请求和单阶段;
  • 最后才用 profiling 回到代码路径。

只要这条顺序稳住,第四部分就更像维护者真正能用的后半程,而不是工具列表。