按问题定位源码#

这一节从症状出发,告诉你如果问题发生在请求主线、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 -20

finish_reason=length 意味着请求达到了 max_new_tokens 上限。检查两处:

  1. 请求里传入的 max_new_tokens 是否被正确传递到 SamplingParams
  2. 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 积压/metricstoken_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_paramsscheduler.py grammar
OOM/metrics token_usage 先判断是 KV 耗尽还是 CUDA OOM
输出乱码radix_cache.py evict + 检查引用计数逻辑
TP 下输出不一致NCCL AllReduce 时机 + model_runner.py tp_rank==0 判断

小结#

按问题定位源码的起手式只有两步:

  1. 先用 metrics 和日志缩小问题所在的层次(入口?调度?缓存?执行?)
  2. 再 grep 对应层次的核心符号,找到第一个相关函数后开始 step-through 阅读

不要在还没缩小范围之前就打开 scheduler.py 的全文——它有上千行,在没有方向的情况下阅读代价非常高。