Skip to content

网格操作:切片、广播与消息路由 ​

拥有了 Mesh 之后,我们可以像操作 NumPy 数组一样,对分布式集群进行精细控制。

1. 广播调用 (Broadcast) ​

当你在一个 ActorMesh 对象上调用方法时,默认行为是广播:

python
# 通知 (2, 4) 网格上的全部 8 个 Actor 初始化权重
actor_mesh.init_weights.broadcast(seed=42)

# 需要收集响应时用 call
results = actor_mesh.get_status.call().get()

回顾初级教程:broadcast() 不等响应、call() 广播并收集、call_one() 点对点——这些"副词"在 Mesh 上同样适用。

2. 网格切片 (Slicing) ​

使用 slice() 方法按维度名选中 Mesh 的一部分,支持整数索引和 Python 切片语法:

python
# 选中名为 gpus 的维度上前 4 个(Group A)
group_a = mesh.slice(gpus=slice(0, 4))

# 后 4 个(Group B)
group_b = mesh.slice(gpus=slice(4, 8))

# 分别发送不同的指令
group_a.set_role.broadcast("Encoder")
group_b.set_role.broadcast("Decoder")

多维切片 ​

对 (hosts, gpus) 形状的网格,可以同时切多个维度:

python
# 每台主机的第 0 号 GPU(通常作为该机器的通讯协调者)
leaders = mesh.slice(gpus=0)
# leaders 的逻辑形状是 {"hosts": N},只有 N 个进程会收到消息
leaders.sync_logs.call()

# 精确到单点:1 号主机的 3 号 GPU
actor = mesh.slice(hosts=1, gpus=3)
actor.method.call_one()

一个更健壮的写法:flatten ​

官方 Cookbook 推荐的 rank-0 选取方式——先 flatten 把所有维度折叠成一个命名维度,再切片:

python
# 无论网格原始形状如何,都能拿到 rank-0
rank0 = generator.flatten().slice(rank=0)

这比 slice(hosts=0, gpus=0) 更健壮——后者在维度改名后会直接报错。

3. 场景实战:混合并行分组 ​

假设 8 个 GPU 要分成两组做不同的工作:

python
procs = this_host().spawn_procs(per_host={"gpus": 8})
workers = procs.spawn("workers", Worker)

# 前 4 卡做数据并行,后 4 卡做流水线
dp_group = workers.slice(gpus=slice(0, 4))
pp_group = workers.slice(gpus=slice(4, 8))

dp_group.train_step.call(step=0)
pp_group.push_batch.broadcast(batch_idx=0)

4. 传递 Actor 引用:Mesh 即资源句柄 ​

Actor、进程、主机都可以作为消息参数发给其他 Actor。这让程序能组装出复杂的消息拓扑——例如先把 DataLoader 网格启动好,再把它的引用传给 Trainer:

python
from monarch.actor import context

class DataLoader(Actor):
    def __init__(self):
        self.i = 0

    @endpoint
    def example(self):
        self.i += 1
        return f"<fake data {self.i}>"

class Trainer(Actor):
    def __init__(self, dataloader: DataLoader):
        # 每个 Trainer 用自己的 rank 坐标,
        # 从 DataLoader 网格中选中与自己对应的那一个
        my_rank = context().actor_instance.rank
        self.dataloader = dataloader.slice(**my_rank)

    @endpoint
    def step(self, input=None):
        data = self.dataloader.example.call_one().get()
        return f"Step with data {data}"

dataloaders = trainer_procs.spawn("dataloader", DataLoader)
dtrainers = trainer_procs.spawn("dtrainers", Trainer, dataloaders)
print(dtrainers.step.call().get())

这个模式的好处:DataLoader 可以搬迁到别的主机,而 Trainer 代码一行都不用改。

5. Actor 内部如何找到自己 ​

context() API 回答"我是谁、我在哪":在 endpoint 内部,它是当前正在执行的 worker Actor;在控制器的普通代码里,则是控制器进程的根客户端(root client)Actor:

python
from monarch.actor import context

class ContextAwareActor(Actor):
    @endpoint
    def get_context_info(self):
        actor_instance = context().actor_instance
        # message_rank 是"这条消息"送达的坐标,
        # 与 actor 自身 rank 可能不同(消息可能只发给切片)
        message_rank = context().message_rank
        return f"Actor rank: {actor_instance.rank}, message rank: {message_rank}"

Rank 永远是多维的,以 Point 对象(类字典映射)表示维度名到索引的对应关系。

与 context() 相对,还有一个容易混淆的函数:shutdown_context()。它停的不是某个 Actor,而是整个控制器进程的 Monarch 客户端——控制器用完 Monarch 后可以显式调用它干净收尾。正常退出解释器时 Monarch 会自动注册执行;客户端一旦关停,同一进程内无法重启:

python
from monarch.actor import shutdown_context

# 控制器完成全部 Monarch 工作之后:
shutdown = shutdown_context()
shutdown.get(timeout=30)   # 异步控制器也可以直接 await 这个 Future

两个要点:只想停单个网格时,用网格自己的 stop/shutdown 方法,客户端还能继续用;后续再调 shutdown_context() 拿到的是已完成的 Future,不会等待第一次关停结束——所以要保留第一次调用返回的 Future 来等待。

关键点 ​

  • slice(维度名=索引或切片) 是唯一的切片入口,支持多维组合;
  • flatten().slice(rank=0) 是选取 rank-0 的健壮写法;
  • Actor 引用可传递,slice(**my_rank) 实现按坐标配对;
  • context() 在 Actor 内部回答"我是谁、这条消息发给谁"(控制器代码里则返回根客户端上下文);
  • shutdown_context() 关停整个进程的客户端,与停单个网格的方法不同。

这种能力使得实现复杂的调度逻辑(如手动调度 Ring-AllReduce、复杂的流水线并行分配)变得异常直观和简洁。

Monarch中文教程