源码精读:sglang serve → run_server → HTTP → 子进程拉起
谁该读这一篇? 想搞清"
sglang serve这条命令背后如何选择 backend、拉多少进程、绑多少端口"的工程师。 前置阅读:01-overview/02-architecture.md(三进程拓扑)。 耗时: 30 分钟 学完能: 1. 从cli/main.py、cli/serve.py一路跟到launch_server.run_server,知道每个文件做了什么; 2. 列出 SGLang 启动会拉的进程和它们之间的 ZMQ 端口; 3. 知道哪里读 server_args、哪里建 model config、哪里启 HTTP; 4. 修改启动逻辑(比如加新的 startup hook)时知道在哪里挂; 5. 看到启动日志能对照源码定位每一行。
1. 当前入口:sglang serve
pyproject.toml 把 sglang console script 指向
cli/main.py。main() 解析 serve 子命令后进入
cli/serve.py:166:
def serve(args, extra_argv):
backend_name, argv = _extract_model_type_override(extra_argv)
argv, positional = _normalize_positional_model_path(argv)
request = ServeRequest(argv=tuple(argv), ...)
registry = _create_backend_registry()
load_plugins()
try:
registered = registry.auto_detect(request) # LLM / diffusion / 插件
registered.backend.run(request)
finally:
kill_process_tree(os.getpid(), include_parent=False)
这层有四个职责:
- 把
sglang serve MODEL_PATH的位置参数正规化成--model-path。 - 加载插件,并通过 registry 自动选择 LLM、diffusion 或外部 backend;也可用
--model-type显式指定。 - LLM 路径在
_run_llm中调用prepare_server_args,再进入launch_server.run_server。 - 命令退出时清理整个子进程树。
旧的 python -m sglang.launch_server --model-path MODEL_PATH 仍兼容,但
launch_server.py 会发出迁移警告;新部署应使用
sglang serve MODEL_PATH。如果用 SIGKILL 绕过清理,子进程可能继续占用端口和显存,
因此正常运维应发送 SIGTERM 或使用 Ctrl-C。
2. run_server:路由到具体 server 实现
def run_server(server_args):
server_args.resolve_once()
cfg = resolving_view(server_args)
if cfg.encoder_only:
if cfg.smg_grpc_mode or cfg.grpc_mode:
from sglang.srt.disaggregation.encoder.grpc_server import serve_grpc_encoder
asyncio.run(serve_grpc_encoder(server_args))
else:
from sglang.srt.disaggregation.encoder.http_server import launch_server
launch_server(server_args)
elif cfg.smg_grpc_mode:
# 旧 SMG gRPC-only 路径
from sglang.srt.entrypoints.grpc_server import serve_grpc
asyncio.run(serve_grpc(server_args))
elif cfg.use_ray:
# Ray 模式
from sglang.srt.ray.http_server import launch_server
launch_server(server_args)
else:
# 默认 HTTP 模式(90% 用户走这条路)
from sglang.srt.entrypoints.http_server import launch_server
launch_server(server_args)
encoder-only 可走 HTTP 或 gRPC;smg_grpc_mode 是旧的 gRPC-only 路径。原生 Rust gRPC
由 --grpc-port 在默认 HTTP server 旁边启动,不经过这个 elif。下面只跟默认 HTTP 路径。
3. http_server.launch_server(2776+)
def launch_server(
server_args: ServerArgs,
init_tokenizer_manager_func: Callable = init_tokenizer_manager,
run_scheduler_process_func: Callable = run_scheduler_process,
run_detokenizer_process_func: Callable = run_detokenizer_process,
execute_warmup_func: Callable = _execute_server_warmup,
launch_callback: Optional[Callable[[], None]] = None,
):
"""Launch SRT Server.
...
The engine consists of three components:
1. TokenizerManager: Tokenizes requests, sends to scheduler.
2. Scheduler (subprocess): Receives, schedules, forwards.
3. DetokenizerManager (subprocess): Detokenizes outputs.
"""
# Launch subprocesses
(tokenizer_manager, template_manager, port_args,
scheduler_init_result, subprocess_watchdog) = Engine._launch_subprocesses(...)
_setup_and_run_http_server(
server_args, tokenizer_manager, template_manager, port_args,
scheduler_init_result.scheduler_infos, subprocess_watchdog, ...
)
两步:
_launch_subprocesses:起 Scheduler 子进程 + Detokenizer 子进程,建 TokenizerManager 在主进程。_setup_and_run_http_server:起 FastAPI 应用,绑定路由,监听端口。
3.1 Engine._launch_subprocesses
源码在 entrypoints/engine.py。
做的事(简化):
def _launch_subprocesses(server_args, ...):
port_args = PortArgs.init_new(server_args) # 算所有端口
tokenizer_manager = init_tokenizer_manager_func(server_args, port_args)
# 起 Scheduler 子进程(每个 TP rank 一个)
scheduler_procs = []
for tp_rank in range(server_args.tp):
proc = multiprocessing.Process(
target=run_scheduler_process_func,
args=(server_args, port_args, tp_rank, ...)
)
proc.start()
scheduler_procs.append(proc)
# 起 Detokenizer 子进程
detokenizer_proc = multiprocessing.Process(
target=run_detokenizer_process_func,
args=(server_args, port_args)
)
detokenizer_proc.start()
# 等子进程 ready
scheduler_init_result = wait_for_scheduler_init(...)
# Watchdog 监控子进程存活
watchdog = SubprocessWatchdog(scheduler_procs + [detokenizer_proc])
return tokenizer_manager, template_manager, port_args, scheduler_init_result, watchdog
要点:
port_args是一个 dataclass,包含所有 IPC socket 的名字(PortArgs找它)。- 子进程用
multiprocessing.Process(target=...)拉起,target 是run_scheduler_process这种顶层函数。 - 子进程启动后会通过一个"ready signal"告知主进程它准备好了(避免 race)。
- Watchdog 定期检查子进程存活,crash 就让父进程也退出。
3.2 _setup_and_run_http_server
def _setup_and_run_http_server(server_args, tokenizer_manager, ...):
# 1. 建 FastAPI app
app = build_fastapi_app(server_args, tokenizer_manager, ...)
# 2. 注册路由:/v1/chat/completions, /v1/completions, /v1/embeddings, ...
register_openai_routes(app, tokenizer_manager, ...)
register_admin_routes(app, ...)
register_metrics_routes(app, ...)
# 3. 启动 (Granian / Uvicorn)
if server_args.http_server == "granian":
_run_granian_server(server_args)
else:
uvicorn.run(app, host=server_args.host, port=server_args.port, ...)
注意:
- 默认路径仍是 Python HTTP server(FastAPI + Uvicorn/uvloop);启用
SGLANG_RUST_SERVER时,Rust server 负责 HTTP 数据面,gRPC 可通过--grpc-port并行暴露。 - 所有 OpenAI 兼容 API 都在
entrypoints/openai/下。 /metrics是 Prometheus;/health、/health_generate是健康检查。
4. 端口对照表
启动后会监听若干 IPC + 网络端口(粗略):
| 端口 | 由谁监听 | 用途 |
|---|---|---|
--port(默认 30000) |
HTTP server(主进程) | 客户端 API |
tokenizer_ipc_name |
TokenizerManager(主进程) | 接收 Detokenizer 输出 |
scheduler_input_ipc_name |
Scheduler 子进程 | 接收 TM 发来的请求 |
detokenizer_ipc_name |
Detokenizer 子进程 | 接收 Scheduler 发来的 token 流 |
| NCCL/NIXL ports | Scheduler 进程之间 | TP 集合通信 |
ZMQ socket 用的是 IPC(Unix domain socket)而非 TCP,文件路径在 /tmp 下。 多副本部署时要确保 IPC 文件名不冲突(带 pid 或 uuid)。
5. Startup 时序图
sequenceDiagram
participant CLI as CLI
participant Main as Main Process
participant Sched as Scheduler subproc
participant Detok as Detokenizer subproc
participant HTTP as HTTP server
CLI->>Main: sglang serve MODEL_PATH ...
Main->>Main: registry backend detection + load_plugins()
Main->>Main: prepare_server_args() + run_server()
Main->>Main: Engine._launch_subprocesses
Main->>Sched: spawn (multiprocessing)
Main->>Detok: spawn
Sched->>Sched: build ModelWorker (load weights, build attention backend)
Sched->>Sched: capture CUDA Graphs
Sched-->>Main: ready signal
Detok-->>Main: ready signal
Main->>HTTP: build FastAPI + register routes
Main->>HTTP: uvicorn/granian.run()
HTTP-->>CLI: log "Application startup complete"
HTTP-->>CLI: log "The server is fired up and ready to roll!"
可见绝大多数启动时间花在 Scheduler 子进程的 build ModelWorker + capture CUDA Graphs 两步(可能 30s 到数分钟)。
6. 启动日志逐行解读
[INFO] server_args=ServerArgs(...) ← server_args.py 解析后的 dataclass repr
[INFO] Init torch distributed begin. ← TP 集合通信初始化
[INFO] Init torch distributed ends. mem usage=...
[INFO] Load weight begin. ... ← model_loader 加载权重
[INFO] Load weight end. Type: float16, ...
[INFO] Memory pool end. avail mem=XX.X GB ← KV pool 预分配后剩余
[INFO] max_total_num_tokens=... ← KV pool 总 slot 数
[INFO] Use cuda graph for decoding mode.
[INFO] Capture cuda graph begin. ... ← 在录 graph,可能花 10-60s
[INFO] Capture cuda graph end. Time elapsed: X.X s.
[INFO] max_running_requests=..., max_total_num_tokens=...
[INFO] Application startup complete. ← FastAPI ready
[INFO] The server is fired up and ready to roll! ← 可以接请求了
排查启动慢:
- 卡在 "Init torch distributed" → NCCL 网络问题。
- 卡在 "Load weight" → 模型下载或加载 IO 瓶颈。
- "Capture cuda graph" 超 60s → cuda graph 数量/编译较多(看
--cuda-graph-bs-decode与 prefill backend)。
7. 优雅关闭
源码:cli/serve.py:166、
engine.py:1236 和
http_server.py:2440。
收到 SIGTERM / SIGINT 时:
- Uvicorn(默认)处理 SIGINT/SIGTERM;启用 HTTP/2 时,嵌入式 Granian 也显式注册
server.stop。 - CLI 的
finally和Engine.shutdown停止 subprocess watchdog,关闭 RPC socket,并清理 scheduler/detokenizer 等子进程。 kill_process_tree(..., wait_timeout=60)给退出留出上限,随后处理残留进程。
当前源码没有通用的 POST /shutdown 运维合同。Kubernetes 应让 Pod 收到 SIGTERM,配置足够的
terminationGracePeriodSeconds,并先从 Service/Gateway 摘除流量;不要依赖未注册的端点。
8. 自定义启动逻辑
launch_server 接受 5 个可选回调(init_tokenizer_manager_func / run_scheduler_process_func / ...)。
你可以完全替换某个进程的实现:
from sglang.srt.entrypoints.http_server import launch_server
from sglang.srt.managers.scheduler import run_scheduler_process
def my_scheduler(server_args, port_args, tp_rank, ...):
# 自定义调度逻辑,最后还是要 run_scheduler_process
print("Custom scheduler wrapper")
return run_scheduler_process(server_args, port_args, tp_rank, ...)
launch_server(server_args, run_scheduler_process_func=my_scheduler)
这是 SGLang 给"想魔改但又不想 fork 整个仓库"的开发者留的口子。
9. 关键源码索引
| 内容 | 文件:行 |
|---|---|
| Console script | cli/main.py |
serve backend 分派 |
cli/serve.py:166 |
| LLM server 分派 / 兼容入口 | launch_server.py |
| 默认 HTTP launch_server | http_server.py:2776 |
Engine._launch_subprocesses |
engine.py |
_setup_and_run_http_server |
http_server.py:2518 |
| 路由注册 | entrypoints/openai/ |
| ServerArgs | server_args.py |
| PortArgs | io_struct.py |
| 子进程 watchdog | utils/watchdog.py |
| Scheduler 进程入口 | scheduler.py:run_scheduler_process(5254) |
| Detokenizer 进程入口 | detokenizer_manager.py:run_detokenizer_process(537) |
10. 小结
sglang serve先做 backend 检测;LLM 路径再进入launch_server.run_server,真正复杂在http_server.launch_server。_launch_subprocesses拉起 Scheduler(每 TP rank 一个)+ Detokenizer 子进程。- HTTP server 默认使用 Uvicorn;HTTP/2 路径使用 Granian,并注册 OpenAI 兼容路由。
- 启动绝大多数时间在 model load + CUDA Graph 捕获。
- 可通过传
*_func回调自定义启动逻辑而不必 fork 仓库。
11. 自检
sglang serve MODEL_PATH --tp 4的标准单-tokenizer路径会起几个主要进程?
答案
**6 个**:1 个主进程(HTTP server + TokenizerManager)+ 4 个 Scheduler 子进程(每 TP rank 一个,各持一张 GPU)+ 1 个 DetokenizerManager 子进程。 主进程是 asyncio event loop;子进程是 `multiprocessing.Process` 拉起的、内部跑同步 while True 循环。 只有 TP rank 0 的 Scheduler 与外界通 ZMQ;rank 1-3 通过 NCCL 接收 hidden state 做切片计算。- SIGKILL 父进程后会发生什么?为什么应该用 SIGTERM?
答案
SIGKILL 不可被捕获,父进程立刻死,**子进程变孤儿**:Scheduler 子进程还持有 GPU context 和 ZMQ socket,端口和显存都不释放。下次启动会报端口被占 / CUDA OOM。 SIGTERM 可被 HTTP server 捕获,CLI 的 `finally: kill_process_tree(...)` 和 engine shutdown 负责停止 watchdog、关闭相关 socket 并回收子进程/GPU context。 K8s 默认发 SIGTERM 然后等 `terminationGracePeriodSeconds`,再发 SIGKILL;所以 grace period 一定要给够(70B 模型至少 60-120s)。- 启动日志卡在 "Capture cuda graph" 60s+,可能原因?
答案
(a) **graph batch size 列表太长**:capture 档位越多,启动越慢;用 `--cuda-graph-bs-decode 1 32 128` 做对照。 (b) **同时开了 `--enable-torch-compile`**:compile + graph 双重开销,每 bs 多花 5-20s。 (c) **模型超大** 或 **TP 通信慢**:每个 graph 录制时也要走 AllReduce,NCCL 慢则整体慢。 (d) **首次运行**:torch.compile 缓存空、Inductor 编译。debug 期间可用 `--cuda-graph-backend-decode disabled --cuda-graph-backend-prefill disabled` 跳过 graph capture。- 我想拦截每个新请求加自定义鉴权,应该在哪里挂?
答案
两个推荐位置: (a) **FastAPI 中间件**:在 `_setup_and_run_http_server`([`http_server.py:2518`](../sglang/python/sglang/srt/entrypoints/http_server.py))注册 FastAPI 路由前,加 `app.middleware("http")` 鉴权层。最干净,鉴权失败直接 401,请求不进 TM。 (b) **`launch_server` 的 callback** :`launch_server(server_args, launch_callback=my_setup)` 里挂自己的中间件注册逻辑。 不推荐改 `TokenizerManager.generate_request` —— 入侵性太强,不便于版本升级。 外层方案(nginx / envoy / Istio)也可,规模上更建议走这条路。- 当前什么时候使用 Granian,什么时候使用 Uvicorn?
答案
默认单-tokenizer Python HTTP 路径使用 Uvicorn + uvloop。启用 `--enable-http2` 时, `_run_granian_server` 使用 Granian;多 tokenizer 时它可启动多个 Granian worker。 此外 `SGLANG_RUST_SERVER` 是另一条 Rust 数据面路径,不能和 Granian ASGI server 混为一谈。 当前没有 `--http-server` 选择 flag;是否启用 HTTP/2 应按客户端协议、稳定性和目标 QPS 压测决定。12. 下一步
02-tokenizer-manager.md— 进入 TM 内部。03-scheduler.md— 进入 Scheduler 内部。07-detokenizer.md— 进入 Detokenizer 内部。- 源码:
cli/serve.py、launch_server.py、entrypoints/http_server.py。
上游源码起点:
sglang/python/sglang/cli/main.py。