源码精读:服务入口 launch_server → HTTP → 子进程拉起
谁该读这一篇? 想搞清"
python -m sglang.launch_server这条命令背后到底拉了多少进程、绑了多少端口"的工程师。 前置阅读:01-overview/02-architecture.md(三进程拓扑)。 耗时: 30 分钟 学完能: 1. 从launch_server.py的if __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) # ④ 退出时清子进程
四步:
- 加载插件(
srt/plugins/)—— 第三方钩子能注入。 - 解析命令行 →
ServerArgsdataclass(server_args.py)。 - 运行 server → 根据 args 分派不同 server 实现。
- 清理:父进程退出时强制杀掉子进程树,避免残留。
第 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+)
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, ...)
注意:
- 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_sockets(http_server.py:2086)+ kill_process_tree(utils.py)。
收到 SIGTERM / SIGINT 时:
- HTTP server 停止接新连接。
- 等所有 in-flight 请求完成(有超时)。
- 给子进程发 SIGTERM。
- 等子进程退出。
- 关 ZMQ socket。
- 强制 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. 自检
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 做切片计算。- 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)。- 启动日志卡在 "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` 跳过。- 我想拦截每个新请求加自定义鉴权,应该在哪里挂?
答案
两个推荐位置: (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)也可,规模上更建议走这条路。- 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. 下一步
02-tokenizer-manager.md— 进入 TM 内部。03-scheduler.md— 进入 Scheduler 内部。07-detokenizer.md— 进入 Detokenizer 内部。- 源码:
launch_server.py、entrypoints/http_server.py。