预计阅读 8 分钟

源码精读: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)

这层有四个职责:

  1. 把 sglang serve MODEL_PATH 的位置参数正规化成 --model-path。
  2. 加载插件,并通过 registry 自动选择 LLM、diffusion 或外部 backend;也可用 --model-type 显式指定。
  3. LLM 路径在 _run_llm 中调用 prepare_server_args,再进入 launch_server.run_server。
  4. 命令退出时清理整个子进程树。

旧的 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+)

源码:http_server.py: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, ...
    )

两步:

  1. _launch_subprocesses:起 Scheduler 子进程 + Detokenizer 子进程,建 TokenizerManager 在主进程。
  2. _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

源码:http_server.py:2518。

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 时:

  1. Uvicorn(默认)处理 SIGINT/SIGTERM;启用 HTTP/2 时,嵌入式 Granian 也显式注册 server.stop。
  2. CLI 的 finally 和 Engine.shutdown 停止 subprocess watchdog,关闭 RPC socket,并清理 scheduler/detokenizer 等子进程。
  3. 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. 自检

  1. 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 做切片计算。
  1. 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)。
  1. 启动日志卡在 "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。
  1. 我想拦截每个新请求加自定义鉴权,应该在哪里挂?
答案 两个推荐位置: (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)也可,规模上更建议走这条路。
  1. 当前什么时候使用 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. 下一步

上游源码起点:sglang/python/sglang/cli/main.py。