Process、rank、port 与 IPC 拓扑#

这一节负责把进程、rank、port 和 IPC 拓扑稳定下来。很多行为差异根本不是业务逻辑分支,而是拓扑位置变化。

这一节解决什么问题#

前两节已经把入口分层和 manager 边界讲清楚了,但还缺最后一层:这些 manager 到底怎样跨进程互相说话?同样是 TokenizerManager -> Scheduler -> DetokenizerManager 这条链,单进程心智和多进程拓扑心智完全不是一回事。

这一节要解决的是三类问题:

  1. PortArgs 到底在命名什么;
  2. get_zmq_socket(...) 怎样把这些名字变成真正的 IPC 端点;
  3. rank、worker 数和 enable_dp_attention 这些条件怎样改写拓扑。

一张图先看默认拓扑#

先看最普通的单 tokenizer、非 DP attention 拓扑:

flowchart LR
    A["TokenizerManager<br/>scheduler_input_ipc_name"] --> B["Scheduler"]
    B --> C["DetokenizerManager<br/>detokenizer_ipc_name"]
    C --> D["TokenizerManager<br/>tokenizer_ipc_name"]

这张图虽然简单,但已经足够说明一件事:请求主链的跨进程通信不是抽象概念,而是三个被明确命名的端点。

PortArgs 是拓扑字典#

PortArgs 的字段本身就很像一张拓扑表:

  • tokenizer_ipc_name
  • scheduler_input_ipc_name
  • detokenizer_ipc_name
  • rpc_ipc_name
  • metrics_ipc_name
  • tokenizer_worker_ipc_name

这些名字不是普通配置项,而是在给整张进程通信图命名。对读者来说,理解 PortArgs 最稳的方式不是记每个字段,而是先记住:后面每个 manager 读到的不是随意字符串,而是当前拓扑里的通信端点表。

PortArgs.init_new(...) 还进一步说明,拓扑不是固定不变的:

  • 非 DP attention 时,默认用本地 ipc://...
  • 开了 DP attention 以后,会切到 TCP + 明确端口

所以这里的“port”并不只是网络端口,而是整套通信方式选择的一部分。

get_zmq_socket(...) 负责把名字变成真 socket#

get_zmq_socket 的代码不复杂,但位置非常关键。它做的是:

  • 创建 ZMQ socket;
  • 根据 bind 决定 bind(endpoint) 还是 connect(endpoint)
  • 在 endpoint 为空时自动绑定随机 TCP 端口。

也就是说,PortArgs 是“这张拓扑图里有哪些名字”,get_zmq_socket(...) 则是“把名字变成真 socket”。

三个 manager 各自绑定了什么#

从初始化代码看:

这意味着:

  • TokenizerManager 同时站在请求链起点和返回链终点;
  • Scheduler 站在中间,并持有最多的通信方向;
  • DetokenizerManager 是返回链上的单一变换节点。

rank 为什么会改写拓扑#

如果只在单机、单 worker、非 DP attention 模式下读这套代码,很容易误以为拓扑是固定的。但 Scheduler.init_ipc_channels(...) 里有一个重要判断:

if self.pp_rank == 0 and self.attn_tp_rank == 0 and self.attn_cp_rank == 0:
    self.recv_from_tokenizer = ...
else:
    self.recv_from_tokenizer = None

这说明并不是所有 rank 都直接承担同样的收发角色。只有特定 rank 会真正承担 request ingress / egress 的通信职责。

所以“scheduler” 不是一个单点对象,而是一组带 rank 角色差异的进程。很多行为差异根本不是业务逻辑,而是当前 rank 位置不同。

多 tokenizer worker 怎样继续改写拓扑#

一旦 tokenizer_worker_num > 1,拓扑还会继续变化:

  • TokenizerManager 不再直接向 scheduler_input_ipc_name 发请求;
  • 而是先经过 tokenizer_worker_ipc_name
  • 同时请求里还要额外携带 worker 相关 routing 信息。

所以多 worker 不是“单机模式多开几个分词线程”,而是直接改写了 request / response 的通信图。

为什么这一层很容易被误读#

最常见的误解有两种:

  1. 把这些字段都看成“实现细节里的 socket 名称”
    这样读代码时只能看到 bind / connect,看不到真正的拓扑含义。

  2. 把 rank 行为差异都看成业务逻辑
    这样排障时会反复去追 scheduler 分支,却忽略了问题其实只是当前 rank 根本不承担这条通信边。

对这一层,最稳的阅读方法永远是:先画图,再看 socket 初始化,而不是反过来。

调试 IPC 问题时先看哪里#

如果怀疑问题出在 IPC 或拓扑层,更稳的顺序通常是:

  1. 先确认当前模式:单 tokenizer 还是多 tokenizer,是否开启 DP attention;
  2. 再确认 PortArgs 生成出来的端点是不是符合当前模式;
  3. 然后看三个 manager 各自 bind / connect 了什么;
  4. 最后再看具体 rank 是否承担了当前这条通信边。

这样做的好处是,你会先确认“这条边是否存在”,再去追“这条边上的消息有没有送到”。

小结#

这一节要稳定下来的,不是几个 socket 名字,而是一张 runtime 通信图:

  • PortArgs 命名端点;
  • get_zmq_socket(...) 创建真实通信边;
  • 三个 manager 在这些边上各自承担不同角色;
  • rank、worker 数和 DP 模式会继续改写这张拓扑图。

只要这张图先稳住,后面看多进程行为、排查通信异常或理解 rank 差异时,就不会再把它们误读成“普通函数分支”。