按问题定位源码#
这一节从症状出发,告诉你如果问题发生在请求主线、batch 调度、cache 或输出侧,第一步该打开哪个文件、grep 什么符号。
为什么"按问题定位"比"按目录阅读"更值钱#
知道仓库结构,和真正会用仓库排障,是两件不同的事。很多人在知道"目录有哪些"之后,面对真实问题时还是不知道第一步该打开哪个文件。
这一节不再讲"目录长什么样",而是讲"遇到具体症状,先从哪里下手"。
症状 1:请求进来了,但没有响应#
首先确认请求有没有到达 TokenizerManager:
# 在 server 日志里找 request id
grep "rid=" server.log | tail -20
# 如果看不到 rid=xxx,说明请求没有通过 HTTP 层进入主链如果没有 rid,问题在 entrypoints/:
# 找 /v1/chat/completions 路由注册
grep -n "chat/completions\|completions" python/sglang/srt/entrypoints/openai/serving_chat.py | head -10
# 找 /generate 路由
grep -n "@router\|add_route\|route" python/sglang/srt/entrypoints/http_server.py | head -20如果有 rid 但没有后续日志,问题在 TokenizerManager 和 Scheduler 之间的 IPC:
# 找 TokenizerManager 把请求发给 Scheduler 的位置
grep -n "send_to_scheduler\|send.*generate" python/sglang/srt/managers/tokenizer_manager.py | head -20
# 找 Scheduler 接收请求的位置
grep -n "recv_from_tokenizer\|handle_generate" python/sglang/srt/managers/scheduler.py | head -20症状 2:请求在 waiting queue 里停太久#
首先通过 metrics 确认是哪种等待:
curl -s http://localhost:30000/metrics | grep "num_running\|num_waiting\|token_usage"
# num_running: 正在 decode 的请求数
# num_waiting: 在 waiting queue 里的请求数
# token_usage: 当前 KV slots 占用率如果 token_usage 接近 1.0,是资源等待,不是调度问题——请求在等 KV 空间释放。
如果 token_usage 不高但 num_waiting 很大,是调度决策问题,看 scheduler.py:
# 找 admission 判断逻辑
grep -n "_get_next_batch_to_run\|can_run\|admit" python/sglang/srt/managers/scheduler.py | head -20
# 找 chunked prefill 相关配置
grep -n "chunked_prefill\|max_prefill_tokens" python/sglang/srt/managers/scheduler.py | head -10症状 3:prefix reuse 没有生效(cache hit rate 很低)#
首先确认 cache hit rate 当前值:
curl -s http://localhost:30000/metrics | grep "cache_hit"
# sglang:cache_hit_ratio 应该在高重复前缀场景下 > 0.5如果 cache hit rate 确实低,有两类原因:
原因 A:前缀不够长或不够对齐
# 找 page_size 配置(前缀需要是 page_size 整数倍才能被复用)
grep -n "page_size\|PAGE_SIZE" python/sglang/srt/mem_cache/radix_cache.py | head -10如果 page_size=16,而你的前缀是 20 个 token,实际只复用了 16 个 token(截断到 page_size 的整数倍)。
原因 B:RadixCache 的 match_prefix 逻辑
# 找 match_prefix 函数实现
grep -n "def match_prefix" python/sglang/srt/mem_cache/radix_cache.py
# 找 insert 函数(确认新请求完成后有没有正确插入)
grep -n "def insert" python/sglang/srt/mem_cache/radix_cache.py症状 4:finish_reason 不符合预期#
常见情况:应该 stop 的请求出现了 length,或反过来。
# 找 finish_reason 的生成位置
grep -rn "finish_reason\|FinishReason" python/sglang/srt/managers/scheduler.py | head -20
grep -rn "finish_reason\|FinishReason" python/sglang/srt/model_executor/ | head -20finish_reason=length 意味着请求达到了 max_new_tokens 上限。检查两处:
- 请求里传入的
max_new_tokens是否被正确传递到SamplingParams; SamplingParams里是否有全局max_new_tokens覆盖了请求级别的值。
grep -n "max_new_tokens\|max_tokens" python/sglang/srt/sampling_params.py | head -20症状 5:structured output 没有约束住输出#
即设置了 response_format={"type": "json_schema", ...},但输出仍然不是合法 JSON。
第一步:确认 response_format 有没有被翻译成 json_schema 参数:
grep -n "response_format\|json_schema" python/sglang/srt/entrypoints/openai/protocol.py | head -20
# 找 to_sampling_params 里的映射逻辑
grep -n "to_sampling_params\|json_schema" python/sglang/srt/entrypoints/openai/protocol.py | head -20第二步:确认 SamplingParams 里的结构化约束字段传到了 grammar 引擎:
grep -n "json_schema\|xgrammar\|grammar" python/sglang/srt/managers/scheduler.py | head -20第三步:如果参数传递没问题,看 grammar constraint 是不是在某个 batch 里被跳过了(grammar 计算有时会因为 batch 过大而降级):
grep -n "grammar_queue\|grammar_backend\|apply_mask" python/sglang/srt/managers/scheduler.py | head -20症状 6:OOM / CUDA out of memory#
OOM 通常有两类来源:
类型 A:KV cache 耗尽(token_usage → 1.0)
这不是真正的 CUDA OOM,而是 SGLang 内部的 KV 资源耗尽。表现是请求被拒绝而不是进程崩溃。
curl -s http://localhost:30000/metrics | grep "token_usage\|num_running"
# 如果 token_usage ≈ 1.0,减小 max_running_requests 或减小 max_new_tokens类型 B:真正的 CUDA OOM
这通常发生在 activation memory(不是 KV cache)超出显存。看 crash 时的 stack trace,关键词是 torch.cuda.OutOfMemoryError。
# 找 GPU 显存分配逻辑
grep -n "cuda_memory\|mem_fraction\|mem_pool" python/sglang/srt/model_executor/model_runner.py | head -20启动参数里的 --mem-fraction-static 控制为 KV cache 预留的显存比例,降低它可以给 activation 留更多空间,但也会减少 KV cache 容量。
一张快速定位表#
| 症状 | 第一步 grep / 文件 |
|---|---|
| 无响应 | server.log grep rid= |
| waiting queue 积压 | /metrics 看 token_usage + scheduler.py _get_next_batch_to_run |
| cache hit rate 低 | /metrics + radix_cache.py match_prefix + 检查 page_size |
| finish_reason 异常 | scheduler.py + sampling_params.py max_new_tokens |
| structured output 失效 | protocol.py to_sampling_params → scheduler.py grammar |
| OOM | /metrics token_usage 先判断是 KV 耗尽还是 CUDA OOM |
| 输出乱码 | radix_cache.py evict + 检查引用计数逻辑 |
| TP 下输出不一致 | NCCL AllReduce 时机 + model_runner.py tp_rank==0 判断 |
小结#
按问题定位源码的起手式只有两步:
- 先用 metrics 和日志缩小问题所在的层次(入口?调度?缓存?执行?)
- 再 grep 对应层次的核心符号,找到第一个相关函数后开始 step-through 阅读
不要在还没缩小范围之前就打开 scheduler.py 的全文——它有上千行,在没有方向的情况下阅读代价非常高。
叶王 © 2013-2026 版权所有。如果本文档对你有所帮助,可以请作者喝饮料。