Skip to main content

创建通信器

在创建通信器时,必须为参与该通信器的 n 个 MUSA 设备都分配一个 0 到 n-1 之间的唯一 rank。不支持将同一个 MUSA 设备在同一个 MCCL 通信器中重复用作不同的 rank,这可能会导致挂起。

给定 rank 到 MUSA 设备的静态映射,mcclCommInitRank()mcclCommInitRankConfig()mcclCommInitAll() 函数将创建通信器对象,每个通信器对象都与一个固定的 rank 和 MUSA 设备相关联。然后,这些对象将被用来启动通信操作。

在调用 mcclCommInitRank() 之前,您需要首先创建一个独特的对象,该对象将被所有进程和线程用来同步并理解它们是同一个通信器的一部分。这是通过调用 mcclGetUniqueId() 函数完成的。

mcclGetUniqueId()函数返回一个 ID,该 ID 必须使用任何 CPU 通信系统广播到所有参与的线程和进程,例如,将 ID 指针传递给多个线程,或者使用 MPI 或其他并行环境(例如,套接字)广播到其他进程。

您还可以调用 mcclCommInitAll 操作,在单个进程中一次性创建 n 个通信器对象。由于它仅限于单个进程,因此此函数不允许节点间通信。mcclCommInitAll 等同于调用 mcclGetUniqueIdmcclCommInitRank 的组合。

以下示例代码是 mcclCommInitAll 的简化实现。

mcclResult_t mcclCommInitAll(mcclComm_t* comm, int ndev, const int* devlist) {
mcclUniqueId Id;
mcclGetUniqueId(&Id);
mcclGroupStart();
for (int i=0; i<ndev; i++) {
musaSetDevice(devlist[i]);
mcclCommInitRank(comm+i, ndev, Id, i);
}
mcclGroupEnd();
}

相关链接:

使用选项创建通信器

mcclCommInitRankConfig()函数允许使用特定选项创建 MCCL 通信器。

MCCL 支持的配置参数列在这里 mcclConfig_t

例如,可以将 "blocking" 设置为 0,要求 MCCL 在任何 MCCL 调用中都不阻塞,同时还可以设置其他配置参数以更精确地定义通信器行为。下面是一个简单的示例代码:

mcclConfig_t config = MCCL_CONFIG_INITIALIZER;
config.blocking = 0;
config.minCTAs = 4;
config.maxCTAs = 16;
config.cgaClusterSize = 2;
config.netName = "Socket";
CHECK(mcclCommInitRankConfig(&comm, nranks, id, rank, &config));
do {
CHECK(mcclCommGetAsyncError(comm, &state));
// 处理外部事件,超时,进度...
} while(state == mcclInProgress);

相关链接:mcclCommGetAsyncError()

使用多个 mcclUniqueId 创建通信器

mcclCommInitRankScalable()函数允许使用多个 mcclUniqueId 创建 MCCL 通信器。所有 MCCL rank 必须提供相同的 mcclUniqueId 数组(相同的 mcclUniqueIds,并且顺序相同)。为了获得最佳性能,我们建议尽可能均匀地在 MCCL rank 之间分配 mcclUniqueIds

在内部,MCCL rank 主要与单个 mcclUniqueId 通信。因此,为了获得最佳结果,我们建议在 rank 之间均匀分配 mcclUniqueIds

以下函数可以用来决定 MCCL rank 是否应该创建 mcclUniqueIds

bool rankHasRoot(const int rank, const int nRanks, const int nIds) {
const int rmr = nRanks % nIds;
const int rpr = nRanks / nIds;
const int rlim = rmr * (rpr+1);
if (rank < rlim) {
return !(rank % (rpr + 1));
} else {
return !((rank - rlim) % rpr);
}
}

例如,如果 3 个 mcclUniqueId 要分配给 7 个 MCCL rank,那么第一个 mcclUniqueId 将与 rank 0-2 关联,而其他 rank 将与 rank 3-4 和 5-6 关联。因此,该函数将在 rank 0、3 和 5 上返回 true,否则返回 false。

注意:只有第一个 mcclUniqueId 将用于创建通信器哈希 ID,该 ID 用于在日志文件和重放工具中识别通信器。

创建更多通信器

mcclCommSplit 函数可以用来基于现有通信器创建通信器。这允许将现有通信器分割成多个子分区,复制现有通信器,甚至创建一个 rank 更少的单个通信器。

mcclCommSplit 函数需要由原始通信器中的所有 rank 调用。如果某些 rank 将不属于任何子组,它们仍然需要用颜色为 MCCL_SPLIT_NOCOLORmcclCommSplit 调用。

新创建的通信器将继承父通信器的配置(例如非阻塞)。如果父通信器以非阻塞模式运行,则可以通过在父通信器上调用 mcclCommAbort,然后在任何新返回的通信器上调用 mcclCommAbort 来停止 mcclCommSplit 操作。这是因为在两个通信器的任何操作期间都可能发生挂起。

以下代码复制了现有的通信器:

int rank;
mcclCommUserRank(comm, &rank);
mcclCommSplit(comm, 0, rank, &newcomm, NULL);

这将通信器分成两半:

int rank, nranks;
mcclCommUserRank(comm, &rank);
mcclCommCount(comm, &nranks);
mcclCommSplit(comm, rank/(nranks/2), rank%(nranks/2), &newcomm, NULL);

这创建了一个只有前两个 rank 的通信器:

int rank;
mcclCommUserRank(comm, &rank);
mcclCommSplit(comm, rank<2 ? 0 : MCCL_SPLIT_NOCOLOR, rank, &newcomm, NULL);

相关链接:

mcclCommSplit()

查询通信器能力

mcclCommQueryProperties 用于在主机侧查询通信器的关键能力。该接口适合在应用启动阶段探测平台能力,再选择后续通信路径。

推荐初始化方式:

mcclCommProperties props = MCCL_COMM_PROPERTIES_INITIALIZER;
CHECK(mcclCommQueryProperties(comm, &props));

常用字段包括:

  • ranknRanks
  • musaDev
  • deviceApiSupport
  • multimemSupport
  • ginType
  • nLsaTeams
  • hostRmaSupport
  • railedGinType

应用侧常见用法:

  • deviceApiSupport == 0 时跳过 device API 或 LSA 路径
  • hostRmaSupport == 0 时回退到传统 send/recv 或 collectives
  • 根据 ginTyperailedGinType 判断 GIN 后端类别

参考示例:examples/01_communicators/04_comm_query_properties

动态扩容通信器

mcclCommGetUniqueIdmcclCommGrow 用于在现有通信器上扩展 rank。

典型模型如下:

  1. 先创建一个已有 rank 参与的父通信器
  2. 由父通信器中的 rank 调用 mcclCommGetUniqueId
  3. 已有 rank 与新 rank 一起调用 mcclCommGrow
  4. 获得扩容后的新通信器

需要注意:

  • 一个 grow unique ID 在消费前不能重复生成新的 grow unique ID
  • 每个 grow unique ID 只能使用一次
  • 上一次 grow 完成之前,不应再次发起 grow

最简示意:

mcclUniqueId growId;
CHECK(mcclCommGetUniqueId(oldComm, &growId));

// 旧 rank(负责提供 growId 的 rank):comm != NULL, rank = -1, uniqueId = &growId
CHECK(mcclCommGrow(oldComm, newNranks, &growId, -1, &newComm, &config));

// 其他旧 rank:comm != NULL, rank = -1, uniqueId = NULL
CHECK(mcclCommGrow(oldComm, newNranks, nullptr, -1, &newComm, &config));

// 新 rank:comm == NULL,传入 growId 和新 rank 编号
CHECK(mcclCommGrow(NULL, newNranks, &growId, newRank, &newComm, &config));

参考示例:examples/01_communicators/05_comm_grow

通信器内存管理

MCCL 提供以下通信器内存管理接口:

这些接口适用于:

  • 观测通信器托管内存规模
  • 在运维或资源敏感场景中暂时挂起通信器管理的可释放内存
  • 验证窗口注册、内存托管和挂起状态

基本流程如下:

uint64_t total = 0;
CHECK(mcclCommMemStats(comm, mcclStatGpuMemTotal, &total));
CHECK(mcclCommSuspend(comm, MCCL_SUSPEND_MEM));
CHECK(mcclCommResume(comm));

使用限制:

  • 这是通信器级别的集体调用
  • 同一通信器上的相关 host API 需要外部串行化
  • 不支持 split/share 通信器
  • 如果 suspend/resume 失败,该通信器的内存管理状态应视为仅允许 destroy/cleanup

mcclCommMemStats 返回的是逻辑统计值,包括:

  • mcclStatGpuMemSuspend
  • mcclStatGpuMemSuspended
  • mcclStatGpuMemPersist
  • mcclStatGpuMemTotal

补充说明:

  • size 类统计值以字节为单位
  • 仅统计已成功纳入通信器内存管理器跟踪的内存
  • 当内存管理被禁用,或管理器处于瞬态/错误状态时,接口会返回 mcclInvalidUsage

参考示例:examples/01_communicators/07_comm_manage_memory

同时使用多个 MCCL 通信器

每个设备使用多个 MCCL 通信器需要通过 MUSA stream 依赖性或同步将所有通信操作的顺序序列化到一致的全局顺序,否则可能会导致死锁。MCCL 引入了 MCCL_LAUNCH_ORDER_IMPLICIT,当启用时,它通过遵循操作从主机发出的顺序隐式地动态创建这个顺序。因此,为了保持无死锁,用户必须确保所有设备主机端启动的顺序匹配。这最容易通过使用每个设备单个主机线程的确定性顺序来实现。例如:

mcclAllReduce(..., comm1, stream1); // 所有 rank 首先执行这个
mcclAllReduce(..., comm2, stream2); // 然后执行这个

当 MCCL 在 MUSA Graph 中被捕获时,相同的规则适用于捕获时间和启动时间。在捕获时间,这意味着同一 MUSA Graph 中的 MCCL 调用必须以相同的顺序被捕获:

// stream1 和 stream2 都在同一图中捕获
mcclAllReduce(..., comm1, stream1); // 所有 rank 首先执行这个
mcclAllReduce(..., comm2, stream2); // 然后执行这个

在图启动时间,不同的图必须以全局一致的顺序启动:

musaGraphLaunch(graph1, stream1); // 所有 rank 首先执行这个
musaGraphLaunch(graph2, stream2); // 然后执行这个

当 MUSA 运行时支持时,操作的隐式排序是使用 MUSA 启动完成事件创建的,这允许两个通信器的内核并行执行。

最终化通信器

mcclCommFinalize 将通信器从 mcclSuccess 状态转换为 mcclInProgress 状态,开始在后台完成所有操作,并与其他可能使用资源进行通信的 rank 同步。与通信器相关的所有未完成的操作和网络相关资源将被 mcclCommFinalize 刷新和释放。一旦所有 MCCL 操作完成,通信器将转换为 mcclSuccess 状态。用户可以使用 mcclCommGetAsyncError 查询该状态。如果通信器被标记为非阻塞,则此操作是非阻塞的;否则,它是阻塞的。

相关链接:mcclCommFinalize()

销毁通信器

一旦通信器被最终确定,下一步是释放所有资源,包括通信器本身。与通信器相关的本地资源可以使用 mcclCommDestroy 销毁。如果通信器的状态在调用 mcclCommDestroy 时为 mcclSuccess,则调用保证是非阻塞的;否则 mcclCommDestroy 可能会阻塞。在所有情况下,mcclCommDestroy 调用将释放通信器的资源并返回,通信器在 mcclCommDestroy 返回后不应再被访问。

相关链接:mcclCommDestroy()