CCU算子
本节以CCU的集合通信算子为例,介绍使用HCCL通信编程接口开发通信算子的整体流程,帮助用户快速了解通信算子的开发步骤。
集合通信算子
本节以AllGather集合通信算子为例进行介绍。AllGather操作是将通信域内所有节点的输入按照rank id重新排序(rank id按照从小到大的顺序排序),然后拼接起来,再将结果发送到所有节点的输出buffer。

样例介绍
开发者可以点击CCU样例获取完整样例代码,该样例使用HCCL通信算子开发接口实现了基于CCU通信引擎的AllGather算子,主要实现流程如下所示:
- 查询通信域的拓扑信息:调用拓扑信息查询接口HcclGetRankId()和HcclGetRankSize()获取当前线程操作的rank_id和通信域的rank数量。
- 创建Thread资源:调用资源管理接口HcclThreadAcquire()分配通信线程资源。
- 建立通信通道:调用HcclChannelAcquire()接口创建rank间的channel链路。
- 注册CCU Kernel:调用HcommCcuKernelRegister()接口创建CCU Kernel,生成CCU指令,用于在CCU上执行。
- 获取本端通信内存:调用HcclGetHcclBuffer()接口获取本端的通信内存信息。
- 准备输入数据:调用HcommLocalCopyOnThread()接口将输入数据拷贝到本端通信内存。
- 生成Token秘钥:调用HcommCcuGetMemToken()接口生成秘钥,唯一标识一块地址信息,用于远端交互。
- 下发CCU Kernel:调用HcommCcuKernelLaunch()接口下发CCU Kernel。
- 前同步:调用ccu::WriteVariableWithNotify()接口告知远端数据已经准备完成;
- 写入数据:调用ccu::Write()接口将本端数据写入到远端通信内存。
- 后同步:调用ccu::NotifyRecord()、ccu::NotifyWait()接口告知远端已经写入完成。
同时,该样例中还包含了测试程序,其中创建1个通信域,每个线程操作1个设备,共同完成AllGather操作。包含以下功能点:
- 设备检测,通过aclrtGetDeviceCount()接口查询可用设备数量。
- 将rank0作为root节点,通过HcclGetRootInfo()接口生成root节点的rootinfo标识信息。
- 在每个线程中,基于rootinfo标识信息通过HcclCommInitRootInfo()接口初始化通信域。
- 调用算子接口HcclAllGatherCustom(),并打印接收结果。
编译安装
在CANN/hccl代码仓根目录下执行如下命令,编译并安装自定义算子包:
# 设置CANN软件包环境变量,此处以root用户默认安装路径为例
source /usr/local/Ascend/cann/set_env.sh
# 执行build.sh脚本进行编译,通过custom_ops_path指定自定义算子工程路径
bash build.sh --vendor=cust --ops=allgather --custom_ops_path=./examples/05_custom_ops_allgather/ccu
# 自定义算子安装包在代码仓的build_out目录下
./build_out/cann-hccl_custom_allgather_linux-<arch>.run --install
自定义算子包安装信息如下:
-
头文件:${ASCEND_HOME_PATH}/opp/vendors/cust/include/hccl_custom_allgather.h
-
动态库:${ASCEND_HOME_PATH}/opp/vendors/cust/lib64/libhccl_custom_allgather.so
-
安装脚本:${ASCEND_HOME_PATH}/opp/vendors/cust/scripts/install.sh
执行样例
编译并执行测试样例。
# 进入样例代码目录
cd examples/05_custom_ops_allgather/ccu/testcase
# 编译
make
# 执行测试用例
make test
结果解析
所有节点的输入数据初始化为该节点的DeviceId。运行成功后,终端将输出类似以下的日志信息(以2卡运行为例):
Found 2 NPU device(s) available
rankId: 0, output: [ 0 1 ]
rankId: 1, output: [ 0 1 ]
关键代码解析
下面以自定义AllGather算子为例,讲解其实现细节。
-
解析通信域的拓扑信息。
uint32_t rank, rankSize; CHK_RET(HcclGetRankId(comm, &rank)); CHK_RET(HcclGetRankSize(comm, &rankSize)); -
创建资源。
// 申请thread资源。host模式下,将主流封装为thread,并创建主流上的notify ThreadHandle thread; CHK_RET(HcclThreadAcquireWithStream(comm, COMM_ENGINE_CCU, stream, 0, &thread)); -
建立通信链路。
// 申请channel资源 uint32_t netLayer = 0, listSize = 0; CommLink *linkList = nullptr; CHK_RET(HcclRankGraphGetLinks(comm, netLayer, param.myRank, remoteRank, &linkList, &listSize)); // 获取srcRank和dstRank间link信息 HcclChannelDesc desc; CHK_RET(HcclChannelDescInit(&desc, 1)); CommProtocol protocol = CommProtocol::COMM_PROTOCOL_UBC_CTP; bool protocolExists = false; for (uint32_t idx = 0; idx < listSize; idx++) { CommLink link = linkList[idx]; if (link.linkAttr.linkProtocol == protocol) { desc.remoteRank = remoteRank; desc.notifyNum = CHANNEL_NOTIFY_NUM; desc.channelProtocol = link.linkAttr.linkProtocol; desc.localEndpoint.protocol = link.srcEndpointDesc.protocol; desc.localEndpoint.commAddr = link.srcEndpointDesc.commAddr; desc.localEndpoint.loc = link.srcEndpointDesc.loc; desc.remoteEndpoint.protocol = link.dstEndpointDesc.protocol; desc.remoteEndpoint.commAddr = link.dstEndpointDesc.commAddr; desc.remoteEndpoint.loc = link.dstEndpointDesc.loc; protocolExists = true; break; } } if (!protocolExists) { HCCL_ERROR("[GetChannelForCcu] Protocol %d not found between rank %u and rank %u", protocol, param.myRank, remoteRank); return HCCL_E_NOT_FOUND; } CHK_RET(HcclChannelAcquire(comm, param.engine, &desc, 1, &kernelChannels[channelIndex])); // 获取channelhandle -
注册CCU Kernel。
CcuResult regStartRet = HcommCcuKernelRegisterStart(insHandle); // 注册kernel CcuKernelHandle kernelHandle; constexpr uint32_t dieId = 0; constexpr uint32_t kernelArgNum = 1; const void *kernelArgsArr[] = { kernelInfo.kernelArg }; // 按HcommCcuKernelRegister签名构造入参指针数组 CcuResult regRet = HcommCcuKernelRegister(insHandle, dieId, kernelInfo.kernelFuncName, reinterpret_cast<void*>(kernelInfo.kernelFunc), kernelArgsArr, kernelArgNum, &kernelHandle); resCtxHost.ccuKernels[0] = kernelHandle; CcuResult regEndRet = HcommCcuKernelRegisterEnd(insHandle); -
获取远端的通信内存地址。
CHK_RET(HcclGetHcclBuffer(comm, &cclBufferAddr, &cclBufferSize)); // 从通信域获取CCL buffer -
准备输入数据。
HcommLocalCopyOnThread(resCtx.threads[0], param.outputPtr, param.inputPtr, dataSize); -
生成Token秘钥,唯一标识一块地址信息,用于远端交互。
uint64_t token = 0; uint64_t baseInputAddr = reinterpret_cast<uint64_t>(param.inputPtr); uint64_t baseOutputAddr = reinterpret_cast<uint64_t>(param.outputPtr); if (param.inputPtr != nullptr) { HcommCcuGetMemToken(baseInputAddr, static_cast<uint64_t>(dataSize), &token); } else if (param.outputPtr != nullptr) { HcommCcuGetMemToken(baseOutputAddr, static_cast<uint64_t>(dataSize), &token); } -
下发CCU Kernel。
std::vector<uint64_t> taskArgs = { inputAddr, outputAddr, token, currentRankSliceInputOffset, currentRankSliceOutputOffset, sliceSize, goSize[0], goSize[1], goSize[2], goSize[3], }; CcuResult launchRet = HcommCcuKernelLaunch(resCtx.threads[0], resCtx.ccuKernels[0], taskArgs.data(), taskArgs.size()); -
前同步,告知远端数据已经准备完成。
for (uint32_t i = 0; i < ctx.arg->channelCount; i++) { CCU_CHK_RET(ccu::WriteVariableWithNotify(ctx.arg->channels[i], ctx.output[ctx.arg->rankId], OUTPUT_XN_ID, CKE_IDX_0, 1 << OUTPUT_XN_ID)); CCU_CHK_RET(ccu::WriteVariableWithNotify(ctx.arg->channels[i], ctx.token[ctx.arg->rankId], TOKEN_XN_ID, CKE_IDX_0, 1 << TOKEN_XN_ID)); } -
将本端数据写入到远端通信内存。
CCU_IF(ctx.sliceSize != 0) { uint32_t channelId = 0; for (uint64_t rankIdx = 0; rankIdx < ctx.arg->rankSize; rankIdx++) { const uint16_t mask = 1 << rankIdx; if (rankIdx != ctx.arg->rankId) { CCU_CHK_RET(ccu::Write(ctx.arg->channels[channelId], dst[rankIdx], src, ctx.sliceSize, ctx.event, mask)); // 本卡数据写入远端地址 channelId++; } } } -
后同步,告知远端已经写入完成。
for (uint32_t i = 0; i < ctx.arg->channelCount; i++) { CCU_CHK_RET(ccu::NotifyRecord(ctx.arg->channels[i], CKE_IDX_0, 1 << POST_SYNC_ID)); } for (uint32_t i = 0; i < ctx.arg->channelCount; i++) { CCU_CHK_RET(ccu::NotifyWait(ctx.arg->channels[i], CKE_IDX_0, 1 << POST_SYNC_ID)); // 等待远端卡数据搬运完成 }