HCCL AlltoAllV 集合通信实战:基于 HcclAlltoAllV 接口的单机多卡数据全交换样例解析
【免费下载链接】hccl集合通信库(Huawei Collective Communication Library,简称HCCL)是基于昇腾AI处理器的高性能集合通信库,为计算集群提供高性能、高可靠的通信方案项目地址: https://gitcode.com/cann/hccl
AlltoAllV(全交换)是集合通信中数据交换形态最灵活的算子:每个 rank 可以向通信域内所有 rank 发送长度各不相同的数据,同时从所有 rank 接收长度各异的数据。本文以 CANN/HCCL 开源仓库中的 AlltoAllV 官方样例 为骨架,逐行解析HcclAlltoAllV()的调用方式、通信域初始化流程与编译运行方法,并结合 HcclAlltoAllV 接口文档 与 算子源码 讲清接口参数语义、底层执行链路与工程约束,让读者能够独立在昇腾环境上运行该样例并迁移到自己的业务中。
AlltoAllV 是什么:从语义到接口原型
算子语义
AlltoAllV 是 MPI Alltoallv 在 HCCL 中的对应实现,其核心语义为:
- 发送侧:每个 rank 将本地
sendBuf中的数据切分成rankSize块,第i块发送给 ranki; - 接收侧:每个 rank 从其他所有 rank 接收数据块,按序拼接写入
recvBuf; - 关键区别:与数据量必须相等的 AlltoAll 不同,AlltoAllV 允许每个 rank 发给不同对端的数据量各不相同(通过
sendCounts/recvCounts两个数组精确控制),因此它是实现稀疏通信、变长数据交换、AlltoAllVC 等复杂调度的基础原语。
从 src/ops/all_to_all_v/all_to_all_v.cc 的源码可以看到,HcclAlltoAllV作为对外统一入口,会根据设备能力判断是否走"OutPlace 新流程"(IsOutPlaceDevice),否则回退到内部兼容流程HcclAlltoAllVInner。
函数原型与参数说明
接口声明位于 include/hccl.h:
extern HcclResult HcclAlltoAllV( const void* sendBuf, const void* sendCounts, const void* sdispls, HcclDataType sendType, const void* recvBuf, const void* recvCounts, const void* rdispls, HcclDataType recvType, HcclComm comm, aclrtStream stream);各参数语义(依据 HcclAlltoAllV 接口文档):
| 参数 | 输入/输出 | 含义 |
|---|---|---|
| sendBuf | 输入 | 源数据 buffer 地址 |
| sendCounts | 输入 | uint64 数组,sendCounts[i] = n表示本 rank 发给 rank i 的数据量为 n 个元素 |
| sdispls | 输入 | uint64 数组,sdispls[i] = n表示发给 rank i 的数据在 sendBuf 中的起始偏移,以sendType元素为单位 |
| sendType | 输入 | 发送数据类型(HcclDataType) |
| recvBuf | 输出 | 结果 buffer 地址;必须与 sendBuf 不同且内存范围不能重叠(AlltoAllV 不支持原地操作) |
| recvCounts | 输入 | uint64 数组,recvCounts[i] = n表示本 rank 从 rank i 接收的数据量为 n 个元素 |
| rdispls | 输入 | uint64 数组,rdispls[i] = n表示从 rank i 收到的数据在 recvBuf 中的起始偏移,以recvType元素为单位 |
| recvType | 输入 | 接收数据类型 |
| comm | 输入 | 通信域句柄 |
| stream | 输入 | 本 rank 使用的任务流 |
语义对照:若收发数据类型均为 FP32,则
sendCounts[i] = n表示向 rank i 发送 n 个 float 数据;偏移量sdispls[i] = n表示该块数据起始位置相对 sendBuf 的偏移为 n 个元素。
样例总览:功能与目录结构
本样例(位于 examples/02_collectives/07_alltoallv)在**单机多卡(单进程多线程)**场景下演示完整的 AlltoAllV 流程:
- 通过
aclrtGetDeviceCount()查询可用设备数量; - 以 rank0 为 root,通过
HcclGetRootInfo()生成 rootinfo 标识信息(包含 Device IP、Device ID 等信息),广播给集群内所有 rank 用于初始化通信域; - 在每个线程中基于同一份 rootinfo,通过
HcclCommInitRootInfo()初始化通信域; - 调用
HcclAlltoAllV()完成全交换并打印结果。
目录结构如下:
├── main.cc # 样例源文件 ├── Makefile # 编译/构建配置文件 └── alltoallv # 编译生成的可执行文件环境准备
支持的产品与组网
本样例支持**单机 N 卡(N >= 2)**组网,覆盖以下产品(与接口文档的产品支持情况一致):
- Ascend 950PR / Ascend 950DT
- Atlas A3 训练系列产品 / Atlas A3 推理系列产品
- Atlas A2 训练系列产品
- Atlas 训练系列产品
- Atlas 推理系列产品
配置 CANN 环境变量
编译前需保证 CANN 已正确安装并加载环境变量(root 用户默认安装路径为例):
source /usr/local/Ascend/cann/set_env.shset_env.sh会导出编译与链接必需的ASCEND_HOME_PATH等变量。Makefile 中对此有强校验:若未设置ASCEND_HOME_PATH,make会直接报错提示先执行source .../set_env.sh,见 Makefile。
源码逐段解析:从设备检测到结果打印
完整源码见 main.cc。代码先定义了两个错误检查宏ACLCHECK与HCCLCHECK,分别对 ACL(AscendCL)接口和 HCCL 接口的返回值做断言,失败即打印出错文件与行号并返回,这是昇腾编程的标准防御式写法。
第一步:初始化与设备检测(main 函数)
ACLCHECK(aclInit(NULL)); // 设备资源初始化 uint32_t devCount; ACLCHECK(aclrtGetDeviceCount(&devCount)); // 查询可用设备数量 std::cout << "Found " << devCount << " NPU device(s) available" << std::endl; int32_t rootRank = 0; ACLCHECK(aclrtSetDevice(rootRank)); // 将 rank0 设为 root 设备 void* rootInfoBuf = nullptr; ACLCHECK(aclrtMallocHost(&rootInfoBuf, sizeof(HcclRootInfo))); HcclRootInfo* rootInfo = (HcclRootInfo*)rootInfoBuf; HCCLCHECK(HcclGetRootInfo(rootInfo)); // 生成 rootinfo 标识HcclGetRootInfo()生成的是通信域初始化的"种子"信息,主要包含 Device IP、Device ID 等。它只需在 root rank 上生成一次,随后主线程将同一份rootInfo指针共享给所有工作线程,等价于完成了"广播给集群内所有 rank"的动作——在单机单进程多线程场景下,所有线程直接复用同一份 rootinfo 即可。
第二步:启动多线程,每个线程绑定一个设备
std::vector<std::thread> threads(devCount); std::vector<ThreadContext> args(devCount); for (uint32_t i = 0; i < devCount; i++) { args[i].rootInfo = rootInfo; args[i].device = i; args[i].devCount = devCount; threads[i] = std::thread(Sample, (void*)&args[i]); } for (uint32_t i = 0; i < devCount; i++) { threads[i].join(); }ThreadContext结构体封装了每个线程所需的上下文:共享的rootInfo、当前线程操作的device(即 rank id)以及通信域规模devCount(即 rankSize)。每个线程对应一张卡,模拟了多 rank 并行执行集合通信的语义。
第三步:设置设备、申请内存并准备输入数据(Sample 函数)
ACLCHECK(aclrtSetDevice(static_cast<int32_t>(device))); // 设置当前线程操作的设备 size_t mallocSize = count * sizeof(float); ACLCHECK(aclrtMalloc(&sendBuf, mallocSize, ACL_MEM_MALLOC_HUGE_ONLY)); ACLCHECK(aclrtMalloc(&recvBuf, mallocSize, ACL_MEM_MALLOC_HUGE_ONLY));其中count = devCount,即每个 rank 的收发缓冲区均能容纳rankSize个 float。接着在 Host 侧构造输入数据——每个 rank 的全部元素初始化为自己的 rank_id:
void* hostBuf = nullptr; ACLCHECK(aclrtMallocHost(&hostBuf, mallocSize)); float* tmpHostBuff = static_cast<float*>(hostBuf); for (uint64_t i = 0; i < count; ++i) { tmpHostBuff[i] = static_cast<float>(device); } ACLCHECK(aclrtMemcpy(sendBuf, mallocSize, hostBuf, mallocSize, ACL_MEMCPY_HOST_TO_DEVICE)); ACLCHECK(aclrtFreeHost(hostBuf));即 rank 0 的 sendBuf 内容为[0 0 0 ...]、rank 1 为[1 1 1 ...]、rank i 为[i i i ...],这样便于从输出结果直观验证 AlltoAllV 的数据交换是否正确。
第四步:初始化通信域
HcclComm hcclComm; HCCLCHECK(HcclCommInitRootInfo(rankSize, ctx->rootInfo, device, &hcclComm));HcclCommInitRootInfo()基于 rootinfo 为当前设备创建通信域:第一个参数rankSize为通信域包含的 rank 总数,第二个参数为共享的 rootinfo,第三个参数为当前设备号,第四个参数输出通信域句柄。所有线程使用同一份 rootInfo,从而保证整个通信域视图一致。
第五步:构造 AlltoAllV 的四个数组并执行算子
// 创建任务流 aclrtStream stream; ACLCHECK(aclrtCreateStream(&stream)); // 设置收发数据量,收发数据量相同 std::vector<uint64_t> sendCounts(rankSize, 1); std::vector<uint64_t> recvCounts(rankSize, 1); std::vector<uint64_t> sdispls(rankSize); std::vector<uint64_t> rdispls(rankSize); for (size_t i = 0; i < rankSize; ++i) { sdispls[i] = i; rdispls[i] = i; } HCCLCHECK(HcclAlltoAllV( sendBuf, sendCounts.data(), sdispls.data(), HCCL_DATA_TYPE_FP32, recvBuf, recvCounts.data(), rdispls.data(), HCCL_DATA_TYPE_FP32, hcclComm, stream)); // 阻塞等待任务流中的集合通信任务执行完成 ACLCHECK(aclrtSynchronizeStream(stream));这是整个样例的核心。以 8 卡为例,各数组取值为:
sendCounts[i] = 1:本 rank 发给 rank i 恰好 1 个 float;recvCounts[i] = 1:本 rank 从 rank i 恰好接收 1 个 float;sdispls[i] = i:发给 rank i 的数据在 sendBuf 中的偏移为 i 个元素;rdispls[i] = i:从 rank i 接收的数据写入 recvBuf 的偏移为 i 个元素。
因此,rank i 的 sendBuf 布局为[i, i, ..., i](共 rankSize 个 i),切分后发给 rank 0..7 各 1 个元素;每个 rank 从 rank 0..7 各收 1 个元素,按 rdispls 拼接到 recvBuf 中恰好得到[0, 1, 2, 3, 4, 5, 6, 7]。该样例同时是"各 rank 间数据量相等"的特例(每个元素数量均为 1),但sendCounts/recvCounts数组本身完全支持每对 rank 数据量可定制的不等量交换。
说明:
HcclAlltoAllV是异步接口,算子下发后需通过aclrtSynchronizeStream(stream)阻塞等待执行完成,才能保证后续 Host 侧读取的结果有效。
第六步:回读结果并打印
std::this_thread::sleep_for(std::chrono::seconds(device)); // 错峰打印,避免输出交错 void* resultBuff; ACLCHECK(aclrtMallocHost(&resultBuff, mallocSize)); ACLCHECK(aclrtMemcpy(resultBuff, mallocSize, recvBuf, mallocSize, ACL_MEMCPY_DEVICE_TO_HOST)); float* tmpResBuff = static_cast<float*>(resultBuff); std::cout << "rankId: " << device << ", output: ["; for (uint64_t i = 0; i < count; ++i) { std::cout << " " << tmpResBuff[i]; } std::cout << " ]" << std::endl; ACLCHECK(aclrtFreeHost(resultBuff));将 Device 侧结果回拷到 Host 后打印。sleep_for(device)秒是让各线程按 rank 顺序错峰输出,避免多线程 stdout 竞争导致打印交错。
第七步:资源释放
HCCLCHECK(HcclCommDestroy(hcclComm)); // 销毁通信域 ACLCHECK(aclrtFree(sendBuf)); // 释放 Device 侧内存 ACLCHECK(aclrtFree(recvBuf)); // 释放 Device 侧内存 ACLCHECK(aclrtDestroyStream(stream)); // 销毁任务流主线程在所有工作线程 join 后释放 rootinfo 的 Host 内存并调用aclFinalize()完成设备去初始化。资源释放顺序遵循"先销毁算子相关资源、再销毁通信域、最后设备去初始化"的规范。
编译与运行
在样例代码目录(examples/02_collectives/07_alltoallv)下执行:
make make testmake调用 Makefile 完成编译,关键编译选项包括:
-std=c++17:C++17 标准;- 链接
-lhccl -lascendcl:链接 HCCL 与 AscendCL 动态库(ASCEND_LIB_DIR = ${ASCEND_HOME_PATH}/lib64); - 头文件路径
-I$(ASCEND_HOME_PATH)/include; - 安全加固选项:
-fstack-protector-strong、-fPIE -pie、-Wl,-z,relro、-Wl,-z,now、-Wl,-z,noexecstack等。
make test则直接运行生成的可执行文件./alltoallv。另有make clean(清理构建产物)与make help(查看帮助)目标。
算子展开模式(可选配置)
可通过环境变量HCCL_OP_EXPANSION_MODE配置通信算子的展开模式,不同产品型号支持的范围不同,完整用法参见仓库内环境变量说明 HCCL_OP_EXPANSION_MODE.md。例如将通信算子展开模式设置为 AI CPU 通信引擎:
export HCCL_OP_EXPANSION_MODE=AI_CPU在 all_to_all_v.cc 中,HcclGetOpExpansionMode(comm, param)会读取该环境变量并把展开模式写入OpParam.engine,随后AlltoAllVExecDispatch依据引擎类型(AICPU_TS / AIV / CCU 等)分流到不同的执行器。
运行结果解读
在 8 卡环境上运行,输出如下:
Found 8 NPU device(s) available rankId: 0, output: [ 0 1 2 3 4 5 6 7 ] rankId: 1, output: [ 0 1 2 3 4 5 6 7 ] rankId: 2, output: [ 0 1 2 3 4 5 6 7 ] rankId: 3, output: [ 0 1 2 3 4 5 6 7 ] rankId: 4, output: [ 0 1 2 3 4 5 6 7 ] rankId: 5, output: [ 0 1 2 3 4 5 6 7 ] rankId: 6, output: [ 0 1 2 3 4 5 6 7 ] rankId: 7, output: [ 0 1 2 3 4 5 6 7 ]每个 rank 的输入数据初始化为对应的 rank_id,经过 AlltoAllV 后,各 rank 的输出均为所有节点输入数据的拼接[0 1 2 3 4 5 6 7],与上文数组推导完全吻合,可用于自检算子行为是否正确。
源码级原理:HcclAlltoAllV 底层执行链路
入口与参数校验
all_to_all_v.cc 中的HcclAlltoAllV在确认设备支持 OutPlace 新流程后,依次执行:
InitEnvConfig():初始化环境配置;CheckAlltoAllVInputPara():逐项校验 comm、sendCounts、sdispls、recvCounts、rdispls、stream 非空,并明确拒绝 sendBuf 与 recvBuf 相同(AlltoAllV does not support in-place operation,返回HCCL_E_PARA),见 源码第 542-546 行;- 查询 rankSize、userRank、commName,生成操作标签
tag = "ALLTOALLV_" + commName并做 tag 合法性检查; - 遍历 sendCounts/recvCounts 求出
maxSendRecvCount并做CheckCount与CheckDataType校验; - 通过
AlltoAllVEntryLog记录接口交互信息日志(sendCounts/recvCounts/sdispls/rdispls 四个数组均会打印); - 调用
AlltoAllVOutPlace进入真正的执行流程。
参数封装与算法分发
AlltoAllVOutPlaceCommon(源码第 792-829 行)完成 OpParam 构造:AlltoAllVConstructOpParam将四个数组打包进连续内存(varData),按 SEND_COUNT、RECV_COUNT、SEND_DISPL、RECV_DISPL 四个区段排布,并依据 sdispls+sendCounts 与 rdispls+recvCounts 计算输入输出总尺寸(CalcInputOutputSize),用于图模式下的内存安全校验。
随后AlltoAllVExecDispatch(源码第 730-790 行)负责算法级分发:
- rankSize == 1 时走
SingleRankProc单 rank 处理; - 否则调用
Selector依据拓扑信息选择算法,AlltoAllV 的自动选择器实现位于 alltoallv_auto_selector.cc; - 从源码目录结构可以推断,AlltoAllV 在不同引擎下实现了多套执行器与算法模板:AICPU 侧有
ins_temp_all_to_all_v_mesh_1D等模板,AIV 侧有aiv_temp_all_to_all_v_mesh_1D与 superkernel 实现,CCU 侧则有 mesh1d、mesh2die、multi_jetty 等多种拓扑变体(见 template 目录 与 executor 目录)。
与 AlltoAll / AlltoAllVC 的关系
从 all_to_all_v.cc 可以看到,HcclAlltoAll(等量全交换)在 OutPlace 新流程中会构造四个等长数组后复用 AlltoAllV 的执行路径;HcclAlltoAllVC(矩阵式变长全交换)同样通过ConvertAlltoAllVCParam将收发矩阵展开为 sendCounts/recvCounts/sdispls/rdispls 后走 AlltoAllV 逻辑。这说明 AlltoAllV 是 HCCL 全交换家族的公共底座,理解本样例即可顺带掌握 AlltoAll 与 AlltoAllVC 的执行框架。
工程约束与调优建议
依据 HcclAlltoAllV 接口文档 的约束说明,实际业务落地时需注意:
- 不支持原地操作:sendBuf 与 recvBuf 必须为不同地址且内存范围不能重叠,否则接口返回
HCCL_E_PARA; - 性能与缓存区相关:AlltoAllV 的性能与 NPU 之间共享数据的缓存区大小有关,当通信数据量超过缓存区大小时性能会明显下降。若业务通信数据量较大,建议通过环境变量 HCCL_BUFFSIZE 适当增大缓存区以提升性能;
- 串行下发约束:多个通信域下的所有通信算子在每个 Device 上需要保证串行下发,不允许乱序、多线程并发下发,也不支持线程重入;同一 Device 上、同一通信域内的所有通信算子下发线程需使用相同的 Context;
- 产品差异:针对 Atlas 训练系列产品,AlltoAllV 的通信域需满足 cluster 维度约束(单 Server 1p/2p 通信域需在同一 cluster 内,4p/8p 与多 Server 场景以 cluster 为基本单位且 Server 间 cluster 选取需一致),单 Server 场景下要求网卡状态为 up;Atlas 300I Duo 推理卡仅支持单 Server 场景(最多 2 张卡共 4 个 NPU);
- 数据类型:不同型号支持的 HcclDataType 集合不同,Ascend 950 系列额外支持 float8-e5m2、float8-e4m3、float8-e8m0、hifloat8 等低比特类型,A2/A3 系列支持 bfp16,Atlas 训练系列与 300I Duo 则支持 int8/uint8/int16/uint16/int32/uint32/int64/uint64/float16/float32/float64。
小结
本文围绕 AlltoAllV 官方样例 完整走通了"HCCL 环境准备 → 通信域初始化(rootinfo + HcclCommInitRootInfo)→ 四个数组构造 → HcclAlltoAllV 下发 → 结果回读 → 资源释放"的端到端链路,并结合 HcclAlltoAllV 接口文档 与 all_to_all_v.cc 源码剖析了参数校验、OpParam 构造与算法分发机制。读者可将本样例作为模板,把sendCounts/recvCounts/sdispls/rdispls四个数组改为不等长配置,即可实现任意变长数据量的全交换通信;如需在 PyTorch / TensorFlow 框架中使用该能力,可参考仓库内 03_ai_framework 下的框架集成样例。
【免费下载链接】hccl集合通信库(Huawei Collective Communication Library,简称HCCL)是基于昇腾AI处理器的高性能集合通信库,为计算集群提供高性能、高可靠的通信方案项目地址: https://gitcode.com/cann/hccl
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考