核心概念:Process Mesh 与 Actor Mesh
进入中级阶段,我们需要更系统地管理大规模资源。Monarch 使用 Mesh(网格) 将杂乱的分布式节点抽象为规整的、可命名的多维数组。
1. 两层抽象
Monarch 的资源模型分两层:
- ProcMesh(进程网格):物理资源的映射,代表集群中已分配的一组进程;
- ActorMesh(Actor 网格):把 Actor 类"铺"到 ProcMesh 上得到的计算网格。
from monarch.actor import Actor, endpoint, ProcMesh, this_host
# 第一层:进程网格 —— 在当前主机启动 8 个进程,维度名 "gpus"
procs: ProcMesh = this_host().spawn_procs(per_host={"gpus": 8})
# 第二层:Actor 网格 —— 每个进程上跑一个 Counter
counters = procs.spawn("counters", Counter, 0)2. Extent:网格的逻辑形状
每个 Mesh 都有一个 extent——一个有序映射,描述每个维度的大小:
print(procs.extent) # {"gpus": 8}
print(hosts.extent) # {"hosts": 32, "gpus": 8}(多机场景)维度名称通常是 hosts(索引集群中的主机)和 gpus(索引单机内的设备),但你可以用任何有意义的名字。
3. 多机场景:HostMesh
当规模超出单机,进程就不再是 this_host() 拉起,而是来自调度系统分配的一组主机。Monarch 用配置文件封装 Slurm、Kubernetes、SkyPilot 等调度器的差异:
from monarch.actor import HostMesh
from monarch.job import hosts_from_config
# 从环境变量 MONARCH_HOSTS 指向的配置加载主机网格
hosts: HostMesh = hosts_from_config("MONARCH_HOSTS")
print(hosts.extent) # 例如 {"hosts": 32}
# 在每台主机上启动 4 个 GPU 进程
trainer_procs: ProcMesh = hosts.spawn_procs(per_host={"gpus": 4})
print(trainer_procs.extent) # {"hosts": 32, "gpus": 4}4. 维度命名的意义
为什么要有名字(hosts、gpus)?因为编写感知拓扑的分布式策略时,这是关键信息:
- 沿着
hosts维度操作:跨节点通信(InfiniBand/RoCE,较慢); - 沿着
gpus维度操作:节点内通信(NVLink,极快)。
后续你会看到:分布式张量的 reduce("gpu", "sum") 与 reduce("host", "sum") 直接沿维度名做归约;rename / split 也靠维度名把物理拓扑重整成并行策略(dp/tp/pp)。
5. 可视化与调试
ProcMesh 提供 to_table(),把网格布局打成表格,调试拓扑时非常有用:
mesh = this_host().spawn_procs({"host": 4, "gpu": 4})
print(mesh.to_table())输出形如:
host\gpu 0 1 2 3
0 0 1 2 3
1 4 5 6 7
2 8 9 10 11
3 12 13 14 156. 定制子进程的启动命令与环境
spawn_procs 拉起的每个进程都有一个启动命令(BootstrapCommand):程序名、参数,加上一份环境变量。monarch.actor.default_bootstrap_cmd() 返回当前环境下的默认命令,用它的 with_env() 派生定制副本:
from monarch.actor import default_bootstrap_cmd, this_host
host = this_host()
base = default_bootstrap_cmd()
# 按坐标给每个进程设定环境变量
def make_bootstrap(point):
return base.with_env({
"CUDA_VISIBLE_DEVICES": str(point["gpus"]),
"RANK": str(point["hosts"] * 4 + point["gpus"]),
})
procs = host.spawn_procs(
per_host={"gpus": 4},
bootstrap_command=make_bootstrap,
)bootstrap_command 只影响这一次 spawn_procs,不会改 HostMesh 本身,后续 spawn 仍用原来的命令。
子进程的环境是怎么拼出来的
环境在目标主机上、进程启动时按四步拼装:
inherit_env为真(默认)时,从该主机上 spawn 它的 host agent 进程的环境起步——也就是run_worker_loop_forever或你的作业启动器在那个主机上拉起的进程;为假时从空环境起步。- 删掉
env里值为None的变量。 - 设置
env里其余的变量。 - 写入 Monarch 自己的启动变量:
HYPERACTOR_MESH_BOOTSTRAP_MODE、HYPERACTOR_PROCESS_NAME,配置了日志转发时还有BOOTSTRAP_LOG_CHANNEL。这三个在inherit_env=False时依然存在,也删不掉。
两点要注意:控制器的环境永远不会被隐式发送;值是字面字符串,"${PATH}:/opt/bin" 不会被展开。inherit_env=False 时 Monarch 只加 env 和自己的启动变量,但进程启动器还可能加料——systemd launcher 把每个进程当 systemd service 跑,会注入 INVOCATION_ID、SYSTEMD_EXEC_PID 这类元数据。
配方表
| 目的 | 写法 |
|---|---|
| 保留主机变量,覆盖/新增几个 | cmd.with_env({"X": "value"}) |
| 删掉一个变量 | cmd.with_env({"X": None}) |
| 设成空字符串 | cmd.with_env({"X": ""})——与 None 不同,变量仍然存在 |
| 挑选几个控制器变量带过去 | cmd.with_env({k: os.environ[k] for k in ("HF_HOME", "WANDB_PROJECT") if k in os.environ}) |
| 最小/白名单环境 | cmd = cmd.with_env({"PATH": "/usr/bin:/bin"}); cmd.inherit_env = False |
| 每个主机或进程不同取值 | 给 bootstrap_command 传 callable(见上) |
扩展主机自己的 PATH、source 脚本 | 用主机侧 wrapper(见下) |
要用控制器的完整环境替换主机环境,记得把默认命令自带的变量(例如 PAR/XAR 构建里选择 bootstrap 入口的 PAR_MAIN_OVERRIDE)叠在最上层:
import os
from monarch.actor import default_bootstrap_cmd
base = default_bootstrap_cmd()
cmd = base.with_env(dict(os.environ)).with_env(base.env)
cmd.inherit_env = Falsecallable 是在控制器上执行的,所以想在目标主机上算值(用它自己的 PATH、source 它的配置文件)就得套一层 shell wrapper,末尾用 exec 让进程替换掉 shell:
from monarch.actor import default_bootstrap_cmd
cmd = default_bootstrap_cmd()
cmd.args = [
"-c",
'export PATH="/opt/tools/bin:$PATH"; . /etc/profile.d/cuda.sh; exec "$0" "$@"',
cmd.program,
*cmd.args,
]
cmd.program = "/bin/sh"几个容易踩的点:
cmd.env返回的是副本,cmd.env["X"] = "1"不生效。要合并用cmd = cmd.with_env({"X": "1"}),要整体替换用cmd.env = {...}。with_env接受任意 mapping(包括os.environ),后调用的覆盖先调用的,所以cmd.with_env({"X": None}).with_env({"X": "1"})最终会设置X。- 传给
spawn_procs的bootstrapcallable 是在解析器启动之后才运行的,因此启动期就被读取的变量(如LD_PRELOAD、PYTHONPATH,或 CUDA 初始化后的CUDA_VISIBLE_DEVICES)必须走env或 wrapper。 default_bootstrap_cmd()连控制器的程序和参数一起发给所有主机;目前没法"只改环境、保留各主机自己的默认程序",真有这种需求就得显式构造命令。default_bootstrap_cmd()返回的是本地(控制器)环境的默认值,不保证和远程主机自己用的默认命令一致(程序路径、参数都可能不同)。用它派生出的命令传给bootstrap_command,等于把客户端的程序和参数发给了主机并顶替掉它们自己的默认值——大多数环境两者等价,所以通常没问题;主机默认值不同时请显式构造命令。另外注意:环境不会被这样带过去,每个进程继承的是自己主机的环境加env。
关键点
- ProcMesh 管物理,ActorMesh 管计算,
spawn()是两者之间的桥; - extent 是逻辑形状:有序映射,
{"hosts": 32, "gpus": 8}描述一个 256 进程的集群; - 多机靠配置:
hosts_from_config()屏蔽 Slurm/K8s/SkyPilot 差异; - 维度名承载拓扑语义,是后续切片、归约、重命名的基础;
- 子进程的命令与环境可定制:
with_env()合并变量、None删除、inherit_env=False从空环境起步。
下一章,我们将像操作 NumPy 数组一样,对这个网格做切片与广播。
