从症状到根因的调试路径#
这一节会给出一条更像运行手册的调试路线:先看什么、再排什么、哪些证据能真正缩小问题边界。
为什么这一节必须放在最后#
前面的调试章节已经把 request logger、trace、metrics、profiling、测试和回归路径分别讲出来了,但如果没有一节把它们重新压成一条工作顺序,读者在现场仍然很容易知道"有哪些工具",却不知道"第一步到底该做什么"。
这一节的职责,就是把前面几节重新收束成三条从症状走到根因的具体路径。
场景一:请求高延迟或 P99 劣化#
症状:ttft_seconds 或 e2e_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_policy 或 mem-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 回到代码路径。
只要这条顺序稳住,第四部分就更像维护者真正能用的后半程,而不是工具列表。
叶王 © 2013-2026 版权所有。如果本文档对你有所帮助,可以请作者喝饮料。