预计阅读 7 分钟

源码精读:服务入口 launch_server → HTTP → 子进程拉起

谁该读这一篇? 想搞清"python -m sglang.launch_server 这条命令背后到底拉了多少进程、绑了多少端口"的工程师。 前置阅读: 01-overview/02-architecture.md(三进程拓扑)。 耗时: 30 分钟 学完能: 1. 从 launch_server.pyif __name__ == "__main__" 一路点进去,知道每个文件做了什么; 2. 列出 SGLang 启动会拉的进程和它们之间的 ZMQ 端口; 3. 知道哪里读 server_args、哪里建 model config、哪里启 HTTP; 4. 修改启动逻辑(比如加新的 startup hook)时知道在哪里挂; 5. 看到启动日志能对照源码定位每一行。


1. 主入口:launch_server.py

源码:launch_server.py(不到 100 行,简短)。

if __name__ == "__main__":
    warnings.warn("'python -m sglang.launch_server' ... use 'sglang serve' instead", ...)

    from sglang.srt.plugins import load_plugins
    load_plugins()                                         # ① 加载插件

    server_args = prepare_server_args(sys.argv[1:])        # ② 解析命令行

    try:
        run_server(server_args)                            # ③ 启动
    finally:
        kill_process_tree(os.getpid(), include_parent=False)  # ④ 退出时清子进程

四步:

  1. 加载插件srt/plugins/)—— 第三方钩子能注入。
  2. 解析命令行ServerArgs dataclass(server_args.py)。
  3. 运行 server → 根据 args 分派不同 server 实现。
  4. 清理:父进程退出时强制杀掉子进程树,避免残留。

第 4 步常被忽视:如果你 SIGKILL 父进程,子进程会变孤儿,端口被占、显存不释放。始终用 SIGTERM 或 Ctrl-C


2. run_server:路由到具体 server 实现

def run_server(server_args):
    if server_args.encoder_only:
        # encoder-only 模式(disagg encoder)
        from sglang.srt.disaggregation.encode_grpc_server import serve_grpc_encoder
        asyncio.run(serve_grpc_encoder(server_args))
    elif server_args.grpc_mode:
        # gRPC 模式
        from sglang.srt.entrypoints.grpc_server import serve_grpc
        asyncio.run(serve_grpc(server_args))
    elif server_args.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)

四条分支,默认 HTTP。下面我们只跟默认路径。


3. http_server.launch_server(2335+)

源码:http_server.py:2335

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

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, ...)

注意:

  • SGLang 默认 HTTP server 是 Granian(Rust 写的 ASGI server,比 Uvicorn 快);可以切回 Uvicorn。
  • 所有 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: python -m sglang.launch_server ...
    Main->>Main: load_plugins() + prepare_server_args()
    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)。

7. 优雅关闭

源码:_close_main_process_socketshttp_server.py:2086)+ kill_process_treeutils.py)。

收到 SIGTERM / SIGINT 时:

  1. HTTP server 停止接新连接。
  2. 等所有 in-flight 请求完成(有超时)。
  3. 给子进程发 SIGTERM。
  4. 等子进程退出。
  5. 关 ZMQ socket。
  6. 强制 kill 残留子进程树。

K8s preStop hook 经常配 sleep 30 && curl -X POST /shutdown 来触发优雅退出。


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. 关键源码索引

内容 文件:行
主入口 launch_server.py(全文)
默认 HTTP launch_server http_server.py:2335
Engine._launch_subprocesses engine.py
_setup_and_run_http_server http_server.py:2133
路由注册 entrypoints/openai/
ServerArgs server_args.py
PortArgs io_struct.py
Watchdog http_server.py:_close_main_process_sockets 附近
Scheduler 进程入口 scheduler.py:run_scheduler_process(3837)
Detokenizer 进程入口 detokenizer_manager.py:run_detokenizer_process(426)

10. 小结

  • 入口极简(launch_server.py < 100 行);真正复杂在 http_server.launch_server 里。
  • _launch_subprocesses 拉起 Scheduler(每 TP rank 一个)+ Detokenizer 子进程。
  • HTTP server 默认 Granian / Uvicorn,注册 OpenAI 兼容路由。
  • 启动绝大多数时间在 model load + CUDA Graph 捕获。
  • 可通过传 *_func 回调自定义启动逻辑而不必 fork 仓库。

11. 自检

  1. python -m sglang.launch_server --tp 4 会起几个进程?
答案 **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 可被捕获,`launch_server.py` 的 `finally: kill_process_tree(...)` 会优雅传播给子进程,等当前请求处理完、关 ZMQ、释放 GPU。 K8s 默认发 SIGTERM 然后等 `terminationGracePeriodSeconds`,再发 SIGKILL;所以 grace period 一定要给够(70B 模型至少 60-120s)。
  1. 启动日志卡在 "Capture cuda graph" 60s+,可能原因?
答案 (a) **graph batch size 列表太长**:默认 9 个 bs(1,2,4,8,16,32,64,128,256),70B 模型每个 bs capture 要 5-10s,累计 1 分钟+。减 `--cuda-graph-bs "1,32,128"` 即可。 (b) **同时开了 `--enable-torch-compile`**:compile + graph 双重开销,每 bs 多花 5-20s。 (c) **模型超大** 或 **TP 通信慢**:每个 graph 录制时也要走 AllReduce,NCCL 慢则整体慢。 (d) **首次运行**:torch.compile 缓存空、Inductor 编译。debug 期间用 `--disable-cuda-graph` 跳过。
  1. 我想拦截每个新请求加自定义鉴权,应该在哪里挂?
答案 两个推荐位置: (a) **FastAPI 中间件**:在 `_setup_and_run_http_server`([`http_server.py:2133`](../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 的差异在哪里?为什么 SGLang 默认 Granian?
答案 Granian 是 Rust 写的 ASGI server(基于 hyper),相比 Uvicorn(纯 Python,基于 uvloop+httptools)有: (a) **更低的 HTTP 解析开销**:Rust 解析每请求节省 0.05-0.1 ms。 (b) **更好的并发模型**:tokio 多线程 vs Python asyncio + GIL。 (c) **更稳的高 QPS 表现**:10k+ QPS 时 Uvicorn 会因 GIL 出现尾延迟。 SGLang 默认 Granian 是为了把 HTTP 层从 TM 的瓶颈中剥离。可以 `--http-server uvicorn` 切回(兼容性 fallback)。 差距在低 QPS 时基本看不出(< 5% 时延差异)。

12. 下一步

上游源码起点:sglang/python/sglang/launch_server.py