多 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_argswarmup_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() 顺序很清楚:

  1. 从共享内存读取 port_argsserver_argsscheduler_info
  2. 为当前 worker 生成新的 tokenizer_ipc_name
  3. 创建 TokenizerWorker(server_args, port_args)
  4. 初始化 TemplateManager
  5. max_req_input_len 从 scheduler info 回填给 tokenizer 侧。
  6. 把这些对象放进进程内 _GlobalState

这条路径说明,多 worker 模式下每个 HTTP worker 并不是“拿主进程里已经存在的 tokenizer manager 来用”,而是自行构建一个 worker-local TokenizerWorker。它和主书前面讲的 TokenizerManager 仍然是同一家族,但通信终点和返回路径已经变化了。

_attach_multi_http_worker_info(...) 为什么关键#

TokenizerWorker._attach_multi_http_worker_info(...) 会把 http_worker_ipchttp_worker_ipcs 附到请求对象上。这个字段看似不起眼,实际上是后面回包能否送回正确 HTTP worker 的关键。

这也是多 worker 模式下一个很典型的工程判断:请求对象不再只携带“模型执行需要什么”,还要携带“结果应当回到哪一个入口进程”。也就是说,请求在入口侧多了一个 routing identity。

MultiTokenizerRouter 承担了什么职责#

python/sglang/srt/managers/multi_tokenizer_mixin.py 里的 MultiTokenizerRouter 明确做了两件事:

  1. router_worker_obj():从 worker 收请求,再统一发给 scheduler。
  2. handle_loop():从 detokenizer 收结果,再按 http_worker_ipc 分发回正确 worker。

它既不是 scheduler,也不是 detokenizer 的替代者,而是多入口模式下新增的交通枢纽。书里把它单独写出来,是为了避免读者把“HTTP worker 多进程”误读成“后端也复制了很多份”。

_distribute_result_to_workers(...) 为什么决定了 streaming 能否正常工作#

router 在回包时会区分:

  • BaseReq:只有一个 http_worker_ipc
  • BaseBatchReq:有一组 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() 很像,也会从共享内存读取参数、构建 TokenizerManagerTemplateManager。区别在于它服务的是 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 路径出问题,先怎么查#

建议按这条顺序:

  1. tokenizer_worker_num 是否真的进入了多 worker 分支。
  2. 看共享内存 multi_tokenizer_args_* 是否写入成功。
  3. 看每个 worker 是否生成了自己的 tokenizer_ipc_name
  4. MultiTokenizerRouter.router_worker_obj() 是否能把请求送到 scheduler。
  5. handle_loop()_distribute_result_to_workers(...) 是否把结果送回正确 worker。

小结#

这一章真正想让你建立的,是 request lifecycle 在入口放大后的版本:

  • 主链路没有变,变的是入口如何汇入它。
  • 多 worker 模式新增了 TokenizerWorker -> MultiTokenizerRouter -> shared scheduler 这层桥。
  • 回包路径也必须按 worker identity 重新拆分。

到这里,请求生命周期就不只覆盖单入口理想路径,也开始覆盖“真实服务为了扩入口吞吐而引入的额外路由层”。