创建通信器
在创建通信器时,必须为通信器中的每个n个MUSA设备分配一个唯一的等级,等级范围从0到n-1。不支持将同一个MUSA设备多次用作同一个MCCL通信器的不同等级,这可能会导致挂起。
给定等级到MUSA设备的静态映射,mcclCommInitRank()、mcclCommInitRankConfig()和mcclCommInitAll()函数将创建通信器对象,每个通信器对象都与一个固定的等级和MUSA设备相关联。然后,这些对象将被用来启动通信操作。
在调用mcclCommInitRank()之前,您需要首先创建一个独特的对象,该对象将被所有进程和线程用来同步并理解它们是同一个通信器的一部分。这是通过调用mcclGetUniqueId()函数完成的。
mcclGetUniqueId()函数返回一个ID,该ID必须使用任何CPU通信系统广播到所有参与的线程和进程,例如,将ID指针传递给多个线程,或者使用MPI或其他并行环境(例如,套接字)广播到其他进程。
您还可以调 用mcclCommInitAll操作,在单个进程中一次性创建n个通信器对象。由于它仅限于单个进程,因此此函数不允许节点间通信。mcclCommInitAll等同于调用mcclGetUniqueId和mcclCommInitRank的组合。
以下示例代码是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);
使用多个mcclUniqueId创建通信器
mcclCommInitRankScalable()函数允许使用多个mcclUniqueId创建MCCL通信器。所有MCCL等级必须提供相同的mcclUniqueId数组(相同的mcclUniqueIds,并且顺序相同)。为了获得最佳性能,我们建议尽可能均匀地在MCCL等级之间分配mcclUniqueIds。
在内部,MCCL等级主要与单个mcclUniqueId通信。因此,为 了获得最佳结果,我们建议在等级之间均匀分配mcclUniqueIds。
以下函数可以用来决定MCCL等级是否应该创建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等级,那么第一个mcclUniqueId将与等级0-2关联,而其他等级将与等级3-4和5-6关联。因此,该函数将在等级0、3和5上返回true,否则返回false。
注意:只有第一个mcclUniqueId将用于创建通信器哈希ID,该ID用于在日志文件和重放工具中识别通信器。
创建更多通信器
mcclCommSplit函数可以用来基于现有通信器创建通信器。这允许将现有通信器分割成多个子分区,复制现有通信器,甚至创建一个等级更少的单个通信器。
mcclCommSplit函数需要由原始通信器中的所有等级调用。如果某些等级将不属于任何子组,它们仍然需要用颜色为MCCL_SPLIT_NOCOLOR的mcclCommSplit调用。
新创建的通信器将继承父通信器的配置(例如非阻塞)。如果父通信器以非阻塞模式运行,则可以通过在父通信器上调用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);
这创建了一个只有前两个等级的通信器:
int rank;
mcclCommUserRank(comm, &rank);
mcclCommSplit(comm, rank<2 ? 0 : MCCL_SPLIT_NOCOLOR, rank, &newcomm, NULL);
相关链接:
同时使用多个MCCL通信器
在MCCL 2.26之前,每个设备使用多个MCCL通信器需要通过MUSA流依赖性或同步将所有通信操作的顺序序列化到一致的全局顺序,否则可能会导致死锁。从2.26开始,MCCL引入了MCCL_LAUNCH_ORDER_IMPLICIT,当启用时,它通过遵循操作从主机发出的顺序隐式地动态创建这个顺序。因此,为了保持无死锁,用户必须确保所有设备主机端启动的顺序匹配。这最容易通过使用每个设备单个主机线程的确定性顺序来实现。例如:
mcclAllReduce(..., comm1, stream1); // 所有等级首先执行这个
mcclAllReduce(..., comm2, stream2); // 然后执行这个
当MCCL在MUSA图中被捕获时,相同的规则适用于捕获时间和启动时间。在捕获时间,这意味着同一图中的MCCL调用必须以相同的顺序被捕获:
// stream1和stream2都在同一图中捕获
mcclAllReduce(..., comm1, stream1); // 所有等级首先执行这个
mcclAllReduce(..., comm2, stream2); // 然后执行这个
在图启动时间,不同的图必须以全局一致的顺序启动:
musaGraphLaunch(graph1, stream1); // 所有等级首先执行这个
musaGraphLaunch(graph2, stream2); // 然后执行这个
当在MUSA 12.3或更高版本上运行时,操作的隐式排序是使用MUSA启动完成事件创建的,这允许两个通信器的内核并行执行。
最终化通信器
mcclCommFinalize将通信器从mcclSuccess状态转换为mcclInProgress状态,开始在后台完成所有操作,并与其他可能使用资源进行通信的等级同步。与通信器相关的所有未完成的操作和网络相关资源将被mcclCommFinalize刷新和释放。一旦所有MCCL操作完成,通信器将转换为mcclSuccess状态。用户可以使用mcclCommGetAsyncError查询该状态。如果通信器被标记为非阻塞,则此操作是非阻塞的;否则,它是阻塞的。
相关链接:mcclCommFinalize()
销毁通信器
一旦通信器被最终确定,下一步是释放所有资源,包括通信器本身。与通信器相关的本地资源可以使用mcclCommDestroy销毁。如果通信器的状态在调用mcclCommDestroy时为mcclSuccess,则调用保证是非阻塞的;否则mcclCommDestroy可能会阻塞。在所有情况下,mcclCommDestroy调用将释放通信器的资源并返回,通信器在mcclCommDestroy返回后不应再被访问。
相关链接:mcclCommDestroy()

