Process、rank、port 与 IPC 拓扑#
这一节负责把进程、rank、port 和 IPC 拓扑稳定下来。很多行为差异根本不是业务逻辑分支,而是拓扑位置变化。
这一节解决什么问题#
前两节已经把入口分层和 manager 边界讲清楚了,但还缺最后一层:这些 manager 到底怎样跨进程互相说话?同样是 TokenizerManager -> Scheduler -> DetokenizerManager 这条链,单进程心智和多进程拓扑心智完全不是一回事。
这一节要解决的是三类问题:
PortArgs到底在命名什么;get_zmq_socket(...)怎样把这些名字变成真正的 IPC 端点;- 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_namescheduler_input_ipc_namedetokenizer_ipc_namerpc_ipc_namemetrics_ipc_nametokenizer_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.init_ipc_channels- 从
tokenizer_ipc_name收 detokenizer 回来的结果; - 向
scheduler_input_ipc_name或tokenizer_worker_ipc_name发请求。
- 从
Scheduler.init_ipc_channels- 从
scheduler_input_ipc_name收请求; - 向
tokenizer_ipc_name或detokenizer_ipc_name回送结果; - 还会额外建
rpc_ipc_name。
- 从
DetokenizerManager.init_ipc_channels- 从
detokenizer_ipc_name收 scheduler 的 token 结果; - 向
tokenizer_ipc_name回发字符串结果。
- 从
这意味着:
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 的通信图。
为什么这一层很容易被误读#
最常见的误解有两种:
把这些字段都看成“实现细节里的 socket 名称”
这样读代码时只能看到 bind / connect,看不到真正的拓扑含义。把 rank 行为差异都看成业务逻辑
这样排障时会反复去追 scheduler 分支,却忽略了问题其实只是当前 rank 根本不承担这条通信边。
对这一层,最稳的阅读方法永远是:先画图,再看 socket 初始化,而不是反过来。
调试 IPC 问题时先看哪里#
如果怀疑问题出在 IPC 或拓扑层,更稳的顺序通常是:
- 先确认当前模式:单 tokenizer 还是多 tokenizer,是否开启 DP attention;
- 再确认
PortArgs生成出来的端点是不是符合当前模式; - 然后看三个 manager 各自 bind / connect 了什么;
- 最后再看具体 rank 是否承担了当前这条通信边。
这样做的好处是,你会先确认“这条边是否存在”,再去追“这条边上的消息有没有送到”。
小结#
这一节要稳定下来的,不是几个 socket 名字,而是一张 runtime 通信图:
PortArgs命名端点;get_zmq_socket(...)创建真实通信边;- 三个 manager 在这些边上各自承担不同角色;
- rank、worker 数和 DP 模式会继续改写这张拓扑图。
只要这张图先稳住,后面看多进程行为、排查通信异常或理解 rank 差异时,就不会再把它们误读成“普通函数分支”。
叶王 © 2013-2026 版权所有。如果本文档对你有所帮助,可以请作者喝饮料。