Skip to content

核心概念:Process Mesh 与 Actor Mesh ​

进入中级阶段,我们需要更系统地管理大规模资源。Monarch 使用 Mesh(网格) 将杂乱的分布式节点抽象为规整的、可命名的多维数组。

1. 两层抽象 ​

Monarch 的资源模型分两层:

  • ProcMesh(进程网格):物理资源的映射,代表集群中已分配的一组进程;
  • ActorMesh(Actor 网格):把 Actor 类"铺"到 ProcMesh 上得到的计算网格。
python
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——一个有序映射,描述每个维度的大小:

python
print(procs.extent)   # {"gpus": 8}
print(hosts.extent)   # {"hosts": 32, "gpus": 8}(多机场景)

维度名称通常是 hosts(索引集群中的主机)和 gpus(索引单机内的设备),但你可以用任何有意义的名字。

3. 多机场景:HostMesh ​

当规模超出单机,进程就不再是 this_host() 拉起,而是来自调度系统分配的一组主机。Monarch 用配置文件封装 Slurm、Kubernetes、SkyPilot 等调度器的差异:

python
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(),把网格布局打成表格,调试拓扑时非常有用:

python
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 15

6. 定制子进程的启动命令与环境 ​

spawn_procs 拉起的每个进程都有一个启动命令(BootstrapCommand):程序名、参数,加上一份环境变量。monarch.actor.default_bootstrap_cmd() 返回当前环境下的默认命令,用它的 with_env() 派生定制副本:

python
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 仍用原来的命令。

子进程的环境是怎么拼出来的 ​

环境在目标主机上、进程启动时按四步拼装:

  1. inherit_env 为真(默认)时,从该主机上 spawn 它的 host agent 进程的环境起步——也就是 run_worker_loop_forever 或你的作业启动器在那个主机上拉起的进程;为假时从空环境起步。
  2. 删掉 env 里值为 None 的变量。
  3. 设置 env 里其余的变量。
  4. 写入 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)叠在最上层:

python
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 = False

callable 是在控制器上执行的,所以想在目标主机上算值(用它自己的 PATH、source 它的配置文件)就得套一层 shell wrapper,末尾用 exec 让进程替换掉 shell:

python
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 的 bootstrap callable 是在解析器启动之后才运行的,因此启动期就被读取的变量(如 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 数组一样,对这个网格做切片与广播。

Monarch中文教程