多 tokenizer worker、Router 与 HTTP 入口放大#
这章解决什么问题#
前面几章把单条请求如何进入 TokenizerManager、穿过 scheduler、再由 detokenizer 回来的主路径讲清楚了,但还有一个很现实的入口问题没有补:当 tokenizer_worker_num > 1,或者服务跑在 Granian / 多进程 HTTP 模式下,请求入口会发生什么变化?如果不把这层讲出来,读者会默认整本书都在描述“单 tokenizer 进程”的理想路径,而忽略了 HTTP 入口本身也会被放大成多 worker 拓扑。
这一章的目标,就是把单入口主线扩成“多入口共享同一 runtime 后端”的版本,并解释为什么这不会推翻前面的主路径,只是给入口侧增加了路由与回包分发层。
为什么多 worker 不只是部署细节#
python/sglang/srt/entrypoints/http_server.py 明确区分了单 tokenizer 模式与 multi-tokenizer 模式。单 worker 时,app 可以直接挂 server_args、warmup_thread_kwargs 并在主进程里构造 TokenizerManager;多 worker 时,系统会把参数写入共享内存,worker 进程自行读取,再创建 TokenizerWorker。这说明多 worker 不是“同一逻辑开多个副本”这么简单,而是入口装配与 IPC 路径都被改写了。
这件事值得成章,是因为它会直接改变排障入口:
- 单 worker 问题通常先看
TokenizerManager。 - 多 worker 问题往往先看共享内存初始化、worker-local IPC 名称、
MultiTokenizerRouter分发和回包映射。
一张图:多 tokenizer worker 模式下,请求怎样汇入同一 scheduler#
这张图解决的理解障碍是:很多读者知道“会有多个 HTTP worker”,但不清楚它们怎样共享后端,而不是各自偷偷维护一套 scheduler。
flowchart LR
C["Clients"] --> W1["HTTP worker 1 / TokenizerWorker"]
C --> W2["HTTP worker 2 / TokenizerWorker"]
C --> WN["HTTP worker N / TokenizerWorker"]
W1 --> R["MultiTokenizerRouter"]
W2 --> R
WN --> R
R --> S["shared Scheduler"]
S --> D["DetokenizerManager"]
D --> R
R --> W1
R --> W2
R --> WN这张图比纯文字多解释了一点:多 worker 模式不是多个独立 runtime,而是多个入口进程共享一个中心路由器和同一套后端 manager。
init_multi_tokenizer() 到 TokenizerWorker:每个 HTTP worker 自己拉什么#
http_server.py 里的 init_multi_tokenizer() 顺序很清楚:
- 从共享内存读取
port_args、server_args、scheduler_info。 - 为当前 worker 生成新的
tokenizer_ipc_name。 - 创建
TokenizerWorker(server_args, port_args)。 - 初始化
TemplateManager。 - 把
max_req_input_len从 scheduler info 回填给 tokenizer 侧。 - 把这些对象放进进程内
_GlobalState。
这条路径说明,多 worker 模式下每个 HTTP worker 并不是“拿主进程里已经存在的 tokenizer manager 来用”,而是自行构建一个 worker-local TokenizerWorker。它和主书前面讲的 TokenizerManager 仍然是同一家族,但通信终点和返回路径已经变化了。
_attach_multi_http_worker_info(...) 为什么关键#
TokenizerWorker._attach_multi_http_worker_info(...) 会把 http_worker_ipc 或 http_worker_ipcs 附到请求对象上。这个字段看似不起眼,实际上是后面回包能否送回正确 HTTP worker 的关键。
这也是多 worker 模式下一个很典型的工程判断:请求对象不再只携带“模型执行需要什么”,还要携带“结果应当回到哪一个入口进程”。也就是说,请求在入口侧多了一个 routing identity。
MultiTokenizerRouter 承担了什么职责#
python/sglang/srt/managers/multi_tokenizer_mixin.py 里的 MultiTokenizerRouter 明确做了两件事:
router_worker_obj():从 worker 收请求,再统一发给 scheduler。handle_loop():从 detokenizer 收结果,再按http_worker_ipc分发回正确 worker。
它既不是 scheduler,也不是 detokenizer 的替代者,而是多入口模式下新增的交通枢纽。书里把它单独写出来,是为了避免读者把“HTTP worker 多进程”误读成“后端也复制了很多份”。
_distribute_result_to_workers(...) 为什么决定了 streaming 能否正常工作#
router 在回包时会区分:
BaseReq:只有一个http_worker_ipcBaseBatchReq:有一组http_worker_ipcs
然后对每个目标 worker 用 _handle_output_by_index(...) 裁出对应片段,再通过 socket_mapping.send_output(...) 发回。这个设计的含义很直接:多 worker 模式不仅要能接收请求,还必须把 batch 结果按入口 worker 重新拆开,否则 streaming 和 batch response 都会在入口层错位。
Granian worker 路径又有什么不同#
_init_granian_worker() 与普通 init_multi_tokenizer() 很像,也会从共享内存读取参数、构建 TokenizerManager 和 TemplateManager。区别在于它服务的是 Granian worker 进程,而不是 Uvicorn 多 tokenizer worker 模式。对读者来说,重要的不是细枝末节,而是更高层的判断:
- 无论是 Granian 还是多 tokenizer worker,入口放大都会把“每个 HTTP worker 如何拿到 runtime 连接”变成显式初始化过程。
- 这类问题一般发生在 HTTP entrance layer,不应误判成 scheduler 或 model runner 问题。
单 worker 与多 worker 的真实差异#
单 worker 模式#
- app 直接挂
server_args - 直接构造
TokenizerManager - API key middleware 可以直接挂在单个 app 上
- Uvicorn 直接以当前 app 进程运行
多 worker 模式#
- 参数通过共享内存传递
- 每个 worker 自己构造
TokenizerWorker - API key 逻辑不再按单 worker 路径处理
- 结果要通过
MultiTokenizerRouter再分发
这不是性能细节,而是入口控制面的拓扑差异。
这种设计的收益与代价#
收益:
- HTTP/tokenization 入口可以横向放大,而不用复制整套 scheduler/runtime。
- 请求和结果都能通过 router 统一汇总和分发,后端主链路不必大改。
- 对调用方来说,多 worker 仍然暴露同样的 HTTP surface。
代价:
- 请求对象多了一层 worker identity。
- 共享内存初始化和 worker-local IPC 名称让启动复杂度上升。
- 多 worker 问题的症状会散落在入口 worker、router 和 detokenizer 回包之间。
如果多 worker 路径出问题,先怎么查#
建议按这条顺序:
- 看
tokenizer_worker_num是否真的进入了多 worker 分支。 - 看共享内存
multi_tokenizer_args_*是否写入成功。 - 看每个 worker 是否生成了自己的
tokenizer_ipc_name。 - 看
MultiTokenizerRouter.router_worker_obj()是否能把请求送到 scheduler。 - 看
handle_loop()和_distribute_result_to_workers(...)是否把结果送回正确 worker。
小结#
这一章真正想让你建立的,是 request lifecycle 在入口放大后的版本:
- 主链路没有变,变的是入口如何汇入它。
- 多 worker 模式新增了
TokenizerWorker -> MultiTokenizerRouter -> shared scheduler这层桥。 - 回包路径也必须按 worker identity 重新拆分。
到这里,请求生命周期就不只覆盖单入口理想路径,也开始覆盖“真实服务为了扩入口吞吐而引入的额外路由层”。
叶王 © 2013-2026 版权所有。如果本文档对你有所帮助,可以请作者喝饮料。