HCCL Scatter 集合通信算子实战:HcclScatter 接口详解与单机多卡样例解析
【免费下载链接】hccl集合通信库(Huawei Collective Communication Library,简称HCCL)是基于昇腾AI处理器的高性能集合通信库,为计算集群提供高性能、高可靠的通信方案项目地址: https://gitcode.com/cann/hccl
本篇技术文章围绕 HCCL(Huawei Collective Communication Library,昇腾集合通信库)的 Scatter 集合通信算子展开,基于仓库中的官方样例 examples/02_collectives/09_scatter/README.md 及其配套源码 main.cc,完整讲解如何调用HcclScatter()接口把 root 节点的数据均分并散布至通信域内其他 rank。读完本文,你将掌握 Scatter 算子的接口语义、参数约束、单机多卡组网下的完整开发流程(设备检测、rootinfo 生成、通信域初始化、算子下发、结果校验),以及编译执行样例所需的环境与命令细节。
1. Scatter 算子功能概述
Scatter 是集合通信六大基础算子之一:通信域内的 root 节点持有一份完整数据,调用 Scatter 后,这份数据被均分为与通信域大小相等的若干份,并散布到每个 rank 的接收缓冲区中——每个 rank 只拿到属于自己序号的那一份。它常用于数据分发场景,例如把某张卡上准备好的初始数据、词表或梯度切片分发到集群各 rank。
Scatter 操作与 AllGather、ReduceScatter 的关系可以简单理解为:AllGather 是"各发各的一份,收全量",Scatter 是其逆操作,"root 发全量,各收一份"。
本样例支持的功能点(与 README 一致):
- 设备检测:通过
aclrtGetDeviceCount()接口查询可用设备数量,单机 N 卡(N >= 2)组网; - root 节点信息生成:将 rank0 作为 root 节点,通过
HcclGetRootInfo()接口生成 root 节点的 rootinfo 标识信息。rootinfo 主要包含 Device IP、Device ID 等信息,需广播至集群内所有 rank 用来初始化通信域; - 通信域初始化:每个线程中基于同一份 rootinfo,通过
HcclCommInitRootInfo()接口初始化通信域; - 执行 Scatter:调用
HcclScatter()接口,将 root 节点的数据均分并散布至其他 rank,并打印结果。
2.HcclScatter接口定义
接口声明位于仓库头文件 include/hccl.h:
extern HcclResult HcclScatter( void* sendBuf, void* recvBuf, uint64_t recvCount, HcclDataType dataType, uint32_t root, HcclComm comm, aclrtStream stream);接口文档详见 docs/zh/api_ref/comm_op_interface/HcclScatter.md。
2.1 参数说明
| 参数名 | 输入/输出 | 描述 |
|---|---|---|
| sendBuf | 输入 | 源数据 buffer 地址,仅 root 节点必须有效,非 root 节点可为空 |
| recvBuf | 输出 | 目的数据 buffer 地址,集合通信结果输出至此 buffer 中 |
| recvCount | 输入 | 参与 scatter 操作的 recvBuf 的数据个数,比如只有一个 int32 数据参与,则 recvCount = 1 |
| dataType | 输入 | Scatter 操作的数据类型,HcclDataType类型,不同产品型号支持的数据类型不同 |
| root | 输入 | 作为 scatter root 的 rank id |
| comm | 输入 | 集合通信操作所在的通信域 |
| stream | 输入 | 本 rank 所使用的任务流(stream) |
2.2 各产品型号支持的数据类型
| 产品 | 支持的数据类型 |
|---|---|
| Ascend 950PR / Ascend 950DT | int8、uint8、int16、uint16、int32、uint32、int64、uint64、float8-e5m2、float8-e4m3、float8-e8m0、hifloat8、float16、float32、float64、bfp16 |
| Atlas A3 训练系列 / Atlas A3 推理系列 | int8、uint8、int16、uint16、int32、uint32、int64、uint64、float16、float32、float64、bfp16 |
| Atlas A2 训练系列 / Atlas A2 推理系列 | int8、uint8、int16、uint16、int32、uint32、int64、uint64、float16、float32、float64、bfp16 |
| Atlas 训练系列 | int8、uint8、int16、uint16、int32、uint32、int64、uint64、float16、float32、float64 |
2.3 返回值
| 返回值 | 说明 |
|---|---|
| HCCL_SUCCESS | 接口调用成功 |
| HCCL_E_PTR | 传入的指针参数为空,如 comm、recvBuf 等为 nullptr(root 节点的 sendBuf 也不能为 nullptr) |
| HCCL_E_PARA | 传入的参数无效,如 count 超过上限、root 越界等 |
| HCCL_E_NOT_SUPPORT | 操作不被支持,如 dataType 非法或当前型号不支持、混合组网不支持 Scatter 等 |
| HCCL_E_INTERNAL | 内部错误 |
2.4 约束说明(重点)
- 所有 rank 的
recvCount、dataType、root均应相同; - 全局只能有 1 个 root 节点;
- 非 root 节点的 sendBuf 可以为空,root 节点的 sendBuf 不能为空——这也是样例中 sendBuf 只在 root 分支申请的原因;
- 多个通信域下的所有通信算子在每个 Device 上需要保证串行下发,不允许乱序、多线程并发下发,也不支持线程重入;
- 在同一 Device 上,同一通信域内的所有通信算子的下发线程需要使用相同的 Context。
关于"多线程并发下发"的约束要注意:样例中虽然每张卡各起一个线程,但每个线程操作的是不同的 Device,且对各自 Device 的算子下发是串行的,因此不违反"单 Device 串行下发"的约束。
3. 样例目录结构
├── main.cc # 样例源文件 ├── Makefile # 编译/构建配置文件 └── scatter # 编译生成的可执行文件对应仓库路径分别为 examples/02_collectives/09_scatter/main.cc 与 examples/02_collectives/09_scatter/Makefile。
4. 环境准备
4.1 环境要求
本样例支持以下产品,组网为单机 N 卡(N >= 2):
- Ascend 950PR / Ascend 950DT
- Atlas A3 训练系列产品 / Atlas A3 推理系列产品
- Atlas A2 训练系列产品
- Atlas 训练系列产品
4.2 配置环境变量
设置 CANN 环境变量(以 root 用户默认安装路径为例):
source /usr/local/Ascend/cann/set_env.shMakefile 中通过ASCEND_HOME_PATH定位头文件与库路径,若未设置该变量会直接报错提示先 source 环境:
ifndef ASCEND_HOME_PATH $(error "ASCEND_HOME_PATH is not set, please ensure CANN is properly installed and \ source environment variables by running `source /path/to/Ascend/cann/set_env.sh`") endif ASCEND_INC_DIR = ${ASCEND_HOME_PATH}/include ASCEND_LIB_DIR = ${ASCEND_HOME_PATH}/lib64 LIBS = -L$(ASCEND_LIB_DIR) -lhccl -lascendcl即编译链接libhccl与libascendcl两个库,C++ 标准为 C++17,并开启了-Werror、-fstack-protector-strong及一组加固链接选项(relro、now、noexecstack等)。
5. 编译与执行样例
在本样例代码目录下执行:
make # 编译,生成可执行文件 scatter make test # 运行 ./scatter注意:可通过设置
HCCL_OP_EXPANSION_MODE环境变量配置通信算子的展开模式,不同产品型号支持的范围可参考仓库中的环境变量说明文档 HCCL_OP_EXPANSION_MODE.md。例如:# 设置通信算子的展开模式为 AI CPU 通信引擎 export HCCL_OP_EXPANSION_MODE=AI_CPU
6. 源码逐段解析
下面结合 main.cc 完整走读样例实现。
6.1 错误检查宏
样例定义了两个宏,把 ACL 与 HCCL 接口的返回值检查统一化,出错时打印文件、行号与错误码后立即返回:
#define ACLCHECK(ret) \ do { \ if (ret != ACL_SUCCESS) { \ printf("acl interface return err %s:%d, retcode: %d \n", __FILE__, __LINE__, ret); \ return ret; \ } \ } while (0) #define HCCLCHECK(ret) \ do { \ if (ret != HCCL_SUCCESS) { \ printf("hccl interface return err %s:%d, retcode: %d \n", __FILE__, __LINE__, ret); \ return ret; \ } \ } while (0)每个线程上下文结构体保存该 rank 所需的设备与 root 信息:
struct ThreadContext { HcclRootInfo* rootInfo; // 全体 rank 共享的 root 节点标识信息 uint32_t rootRank; // root rank(本例为 0) uint32_t device; // 本线程操作的 NPU 设备号 uint32_t devCount; // 通信域大小(设备总数) };6.2 主流程:设备检测与 rootinfo 生成
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)); // 生成 Root 节点信息,各线程使用同一份 RootInfo void* rootInfoBuf = nullptr; ACLCHECK(aclrtMallocHost(&rootInfoBuf, sizeof(HcclRootInfo))); HcclRootInfo* rootInfo = (HcclRootInfo*)rootInfoBuf; HCCLCHECK(HcclGetRootInfo(rootInfo));关键点:
aclInit(NULL)完成 ACL 运行时初始化,aclrtGetDeviceCount()获取本机 NPU 数量 devCount(对应输出 "Found 8 NPU device(s) available");- 先
aclrtSetDevice(rootRank)切到 rank0 设备,再调用HcclGetRootInfo(rootInfo)生成 rootinfo——其中封装了 Device IP、Device ID 等信息,是各 rank 初始化通信域的依据; - rootinfo 只需生成一次,之后所有线程共享同一份
HcclRootInfo,这正是"rootinfo 需广播至集群内所有 rank"在单机多线程模型下的落地方式(多进程场景下则需要通过进程间方式传递)。
随后按设备数启动等量线程:
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].rootRank = static_cast<uint32_t>(rootRank); 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(); }每个线程模拟一个 rank(rank id 即设备号),主线程join()全部线程后释放 rootinfo 并aclFinalize()收尾。
6.3 每个 rank 的执行逻辑(Sample 函数)
Sample()是单个 rank 的完整生命周期,数据量为sendCount = devCount个 float、recvCount = 1个 float,即 root 持有 N 个 float,Scatter 后每 rank 收到 1 个:
uint64_t sendCount = ctx->devCount; uint64_t recvCount = 1U; size_t sendSize = sendCount * sizeof(float); size_t recvSize = recvCount * sizeof(float); // 设置当前线程操作的设备 ACLCHECK(aclrtSetDevice(static_cast<int32_t>(device))); // 申请 Device 内存用于接收 Scatter 结果 ACLCHECK(aclrtMalloc(&recvBuf, recvSize, ACL_MEM_MALLOC_HUGE_ONLY)); // 在 Root 节点,申请 Device 内存用于存放发送数据,初始化为 0,1,2,… 递增序列 if (device == rootRank) { ACLCHECK(aclrtMalloc(&sendBuf, sendSize, ACL_MEM_MALLOC_HUGE_ONLY)); void* hostBuf = nullptr; ACLCHECK(aclrtMallocHost(&hostBuf, sendSize)); float* tmpHostBuf = static_cast<float*>(hostBuf); for (uint32_t i = 0; i < sendCount; ++i) { tmpHostBuf[i] = static_cast<float>(i); } // 将 Host 侧输入数据拷贝到 Device 侧 ACLCHECK(aclrtMemcpy(sendBuf, sendSize, hostBuf, sendSize, ACL_MEMCPY_HOST_TO_DEVICE)); ACLCHECK(aclrtFreeHost(hostBuf)); }从源码结构可以看出两个与接口约束严格对应的细节:
sendBuf只在device == rootRank分支内申请,非 root rank 的 sendBuf 保持nullptr,正对应"HcclScatter 约束"中"非 root 节点的 sendBuf 可以为空,root 节点的 sendBuf 不能为空";- 数据先写入 Host 内存再
ACL_MEMCPY_HOST_TO_DEVICE拷贝到 Device 侧,保证 Scatter 输入位于 Device 内存。
接着是通信域初始化、任务流创建与算子下发:
// 初始化集合通信域 HcclComm hcclComm; HCCLCHECK(HcclCommInitRootInfo(ctx->devCount, ctx->rootInfo, device, &hcclComm)); // 创建任务流 aclrtStream stream; ACLCHECK(aclrtCreateStream(&stream)); // 执行 Scatter,将通信域内 Root 节点的数据均分并散布至其他 Rank HCCLCHECK(HcclScatter(sendBuf, recvBuf, recvCount, HCCL_DATA_TYPE_FP32, rootRank, hcclComm, stream)); // 阻塞等待任务流中的集合通信任务执行完成 ACLCHECK(aclrtSynchronizeStream(stream));调用链上值得注意的三件事:
HcclCommInitRootInfo(devCount, rootInfo, device, &hcclComm)传入通信域大小、共享的 rootinfo、本设备号,返回本 rank 的通信域句柄;所有 rank 用相同参数初始化,从而构成同一个通信域;HcclScatter下发到指定 stream 后是异步的,必须aclrtSynchronizeStream(stream)阻塞等待,集合通信任务执行完成后 recvBuf 中结果才有效;- 同一 Device 上的算子必须串行下发,样例中每个线程只对一个设备下发一次 Scatter,天然满足该约束。
结果回传与打印:
// 将 Device 侧集合通信任务结果拷贝到 Host,并打印结果 std::this_thread::sleep_for(std::chrono::seconds(device)); void* resultHostBuf; ACLCHECK(aclrtMallocHost(&resultHostBuf, recvSize)); ACLCHECK(aclrtMemcpy(resultHostBuf, recvSize, recvBuf, recvSize, ACL_MEMCPY_DEVICE_TO_HOST)); float* tmpResultBuf = static_cast<float*>(resultHostBuf); std::cout << "rankId: " << device << ", output: ["; for (uint32_t i = 0; i < recvCount; ++i) { std::cout << " " << tmpResultBuf[i]; } std::cout << " ]" << std::endl; ACLCHECK(aclrtFreeHost(resultHostBuf));其中std::this_thread::sleep_for(std::chrono::seconds(device))让 rank0、rank1、rank2……依次延后 device 秒打印,使多卡结果按 rank 顺序稳定输出、避免日志交叉(注意这会把样例总耗时拉长,实际业务中不应保留该等待)。
最后是完整的资源释放序列,顺序与申请顺序严格对应:
HCCLCHECK(HcclCommDestroy(hcclComm)); // 销毁通信域 if (sendBuf != nullptr) { ACLCHECK(aclrtFree(sendBuf)); // 释放 Device 侧内存(仅 root 有效) } ACLCHECK(aclrtFree(recvBuf)); // 释放 Device 侧内存 ACLCHECK(aclrtDestroyStream(stream)); // 销毁任务流 ACLCHECK(aclrtResetDevice(device)); // 重置设备7. 结果示例
root 节点的内容初始化为 0~7,经过 Scatter 操作后,通信域内 root 节点的数据被均分并散布至其他 rank。8 卡场景下输出为:
Found 8 NPU device(s) available rankId: 0, output: [ 0 ] rankId: 1, output: [ 1 ] rankId: 2, output: [ 2 ] rankId: 3, output: [ 3 ] rankId: 4, output: [ 4 ] rankId: 5, output: [ 5 ] rankId: 6, output: [ 6 ] rankId: 7, output: [ 7 ]即 root(rank0)上的序列[0,1,2,3,4,5,6,7]按 rank 序号均分,rank i 恰好收到第 i 个元素,验证了 Scatter "均分并散布" 的语义。
8. 开发要点小结与延伸阅读
- 接口层面:Scatter 只有
recvCount而没有sendCount,root 侧发送数据量隐式等于recvCount × 通信域大小,写代码时务必保证 root 的 sendBuf 按此规格分配,否则会触发HCCL_E_PARA或读到非法内存; - 参数一致性:所有 rank 必须传入相同的
recvCount、dataType、root,全局仅一个 root; - 内存与同步:输入/输出 buffer 需位于 Device 内存(
aclrtMalloc),算子下发后需同步 stream 再读结果; - 资源生命周期:
HcclCommDestroy→ 释放 Device 内存 → 销毁 stream →aclrtResetDevice,多 rank 场景建议每线程独立走一遍该序列。
延伸阅读(均在当前仓库内):
- 接口参考文档:HcclScatter、集合通信算子总览
- 头文件定义:include/hccl.h
- 同类算子样例:AllGather、ReduceScatter
- 环境变量说明:HCCL_OP_EXPANSION_MODE、HCCL 环境变量总览
【免费下载链接】hccl集合通信库(Huawei Collective Communication Library,简称HCCL)是基于昇腾AI处理器的高性能集合通信库,为计算集群提供高性能、高可靠的通信方案项目地址: https://gitcode.com/cann/hccl
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考