读 multi_tokenizer_mixin.py:多 worker 入口与回包拆分#

这章解决什么问题#

前面的 request lifecycle 已经在 2.6 多 tokenizer worker、Router 与 HTTP 入口放大 里解释了多 worker 模式的主路径,但如果你真正打开源码,会发现这条支线最容易读散的文件其实是:

  • python/sglang/srt/managers/multi_tokenizer_mixin.py

这棵树同时承载了三件事:

  • TokenizerWorker 怎样给请求附着 worker 身份
  • MultiTokenizerRouter 怎样把多入口请求汇总到 shared scheduler
  • detokenizer 回包怎样再按 worker 拆回去

如果没有一章专门讲“这棵树该怎么读”,多 worker 支线在源码层仍然会显得像几段互不相关的胶水代码。

这章的目标,就是把多 worker 入口和回包拆分的源码骨架正式讲清楚。

为什么这棵树值得单独成章#

这是一个典型的“看起来像实现细节,实际上决定系统拓扑边界”的文件。

如果你只看 http_server.py,你能知道多 worker 模式存在;但真正决定:

  • 请求如何标记来源 worker
  • 输出如何按来源 worker 再拆开
  • socket 怎样懒注册和复用

的核心逻辑,在 multi_tokenizer_mixin.py 里。

从技术书角度看,这种文件特别值得做成导读章,因为它解释的不是单个算法,而是系统拓扑在源码层怎样落地。

一张图:多 worker 源码树真正要解决什么#

这张图解决的理解障碍是:多 worker 模式看起来像“多个 tokenizer worker + 一个 router”,但源码里的关键难点其实是如何把输出准确送回原始 worker。

flowchart LR
    HW1["HTTP worker / TokenizerWorker 1"] --> Tag["attach http_worker_ipc"]
    HW2["HTTP worker / TokenizerWorker 2"] --> Tag
    Tag --> Router["MultiTokenizerRouter"]
    Router --> Sch["shared Scheduler"]
    Sch --> Det["DetokenizerManager"]
    Det --> Split["_handle_output_by_index(...)"]
    Split --> Sock["SocketMapping.send_output(...)"]
    Sock --> HW1
    Sock --> HW2

图比纯文字多解释的一点是:多 worker 的真正复杂度不在“多开几个入口”,而在“同一批输出怎样按 worker 精确扇回去”。

第一层:SocketMapping 是这棵树的最小基础设施#

更稳的阅读顺序,应该先从 SocketMapping 开始。它做的事情很朴素:

  • 维护 ipc_name -> zmq socket 的映射
  • 需要时惰性注册 socket
  • send_output(ipc_name, output) 把对象送给目标 worker

它看起来只是基础设施,但它解释了一个很关键的工程选择:系统没有为每个 worker 固定预先建好所有 socket,而是按需要懒注册输出通道。

对多 worker 模式来说,这是一种很合理的折中:

  • 收益:不用在初始化时穷举所有输出通道
  • 代价:第一次命中某个 worker 回包时要做一次注册

_handle_output_by_index(...) 为什么是这棵树的核心#

这是整棵文件最值得先抓住的函数。因为它解决的是多 worker 路径里最关键的问题:

一批输出对象,怎样被拆成“只属于某个 worker 的那一份”

它会根据 output 类型分别处理:

  • BatchTokenIDOutput
  • BatchEmbeddingOutput
  • BatchStrOutput

并把:

  • rids
  • logprobs
  • token counts
  • cached token 细节
  • dp_ranks
  • time_stats

这些字段都裁成“单 worker 可消费的一份”。

这说明多 worker 回包并不是简单的 list indexing,而是一种正式的对象切片操作。

为什么 _extract_field_by_index(...) 很值得读#

这个小工具函数看似只是 helper,但它透露出一个很重要的现实:

  • 多 worker 输出拆分不是所有字段都完全同构
  • 有的字段是 list
  • 有的字段是 dict of lists
  • 有的字段可能为空

也就是说,multi-worker fan-out 不是只针对 rids 做一刀切,而是要在对象层面保持“结构仍然合法”。

这也是为什么这棵树不能被简单理解成 IPC 胶水。

MultiHttpWorkerDetokenizerMixin 为什么重要#

这部分很值得和 7.23 读 detokenizer_manager.py 配对起来看。它说明:

  • detokenizer 本体不只负责 decode 和文本收口
  • 在多 worker 模式下,它还要在尾部负责按 http_worker_ipcs 把结果拆回不同入口

multi_http_worker_event_loop() 的核心流程是:

  1. 从 scheduler 收到 batch output
  2. dispatch 成 output
  3. 遍历 recv_obj.http_worker_ipcs
  4. 对每个 worker 用 _handle_output_by_index(...) 切片
  5. 再通过 socket_mapping.send_output(...) 回给正确 worker

这说明 detokenizer 在多 worker 模式下,不只是文本尾部收口层,也成了“多 worker 回包分流器”。

MultiTokenizerRouter 应该怎么读#

更稳的顺序是只先抓两个 loop:

  • router_worker_obj()
  • handle_loop()

前者负责:

  • 从 worker 收对象
  • 再统一送到 scheduler

后者负责:

  • 从 detokenizer 收对象
  • 再分发回正确 worker

这意味着 MultiTokenizerRouter 本质上是一个双向 router:

  • 正向汇流
  • 反向分流

如果只把它看成“把请求路由到 scheduler”,你会漏掉它在回包方向上同样重要的一半职责。

TokenizerWorker 为什么不是“少一点功能的 TokenizerManager”#

TokenizerWorker 继承自 TokenizerManager,但它的关键差异在:

  • worker_id = os.getpid()
  • tokenizer_ipc_name 变成 worker-local identity
  • _attach_multi_http_worker_info(...) 会把 http_worker_ipchttp_worker_ipcs 附到请求对象上

这说明 TokenizerWorker 的真正职责不是减少功能,而是把“我是哪个入口 worker”这件事写进请求对象。

换句话说,多 worker 模式不是给普通 TokenizerManager 套壳,而是给请求对象增加了一个显式回包路由身份。

shared memory 写参为什么也要纳入这棵树的阅读范围#

write_data_for_multi_tokenizer(...) 负责把:

  • port_args
  • server_args
  • scheduler_info

写进共享内存,让 worker 进程能在启动时自行恢复运行上下文。

这说明多 worker 模式并不是“主进程把对象直接传给子进程”,而是借共享内存做一次轻量 bootstrap。它进一步表明:

  • 多 worker 的关键复杂度在入口装配与回包路由
  • 而不是在 scheduler / model runner 核心逻辑

这棵树对排障有什么直接价值#

多 worker 问题最容易被误判成 scheduler 或 detokenizer 故障,但很多真实问题其实发生在这里:

  • 某个 worker 没正确附着 http_worker_ipc
  • _handle_output_by_index(...) 切片错位
  • SocketMapping 没给某个目标 worker 建好 socket
  • Router 正向汇流没问题,但反向分流丢了某个 batch 项

只要把这棵树读稳,排障时就能先判断:

  • 是 shared runtime 没工作
  • 还是入口 worker 到 shared runtime 之间的 fan-in / fan-out 断了

如果你要顺着源码读这条支线,推荐顺序是什么#

建议按下面顺序:

  1. 先看 SocketMapping
  2. 再看 _handle_output_by_index(...)
  3. 再看 MultiHttpWorkerDetokenizerMixin.multi_http_worker_event_loop()
  4. 再看 MultiTokenizerRouter.router_worker_obj() / handle_loop()
  5. 最后再看 TokenizerWorker._attach_multi_http_worker_info(...)

这样读,你得到的是一张拓扑图,而不是几段零散 helper。

这一层最容易出现的误判#

1. 以为多 worker 只是多开几个 TokenizerManager#

实际上新增的是一整条 worker identity 与回包拆分路径。

2. 以为 router 只负责正向请求汇流#

它同样负责反向结果分流。

3. 以为 detokenizer 在多 worker 模式下职责不变#

它还要承担按 worker fan-out 的尾部拆分职责。

小结#

这一章真正要补齐的,是代码导读里多 worker 支线的源码入口:

  • TokenizerWorker 负责给请求打上入口身份
  • MultiTokenizerRouter 负责双向汇流与分流
  • SocketMapping_handle_output_by_index(...) 负责把 batch 结果精准拆回原始 worker

到这里,request lifecycle 里讲的多 worker 主线,就不再只是拓扑图,而有了一棵可以真正顺着去读源码的正式树。