CANN ops-math 算子 aclnnChunkCat 接口详解:分块级联(Chunk + Cat)的两段式调用实战
【免费下载链接】ops-math本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-math
本文围绕 CANN 开源数学算子库 ops-math 中 ChunkCat 算子的官方接口文档 aclnnChunkCat.md 展开,完整介绍 aclnnChunkCat 的产品支持情况、计算语义、两段式 API 的函数原型与全部入参约束、错误码场景,并结合仓库内 op_host / op_api / op_kernel 的源码实现说明其 Shape 推导、Tiling 切分与 Kernel 计算原理,最后给出可直接编译运行的完整调用示例。读者读完可掌握在 Ascend NPU 上通过 aclnn 接口完成“先分块、再级联、再转类型”的张量变换。
一、功能说明:ChunkCat 到底做了什么
ChunkCat 算子的功能是将输入的 tensor 列表tensors中所有 tensor:
- 先按照维度
dim切分为numChunks块; - 再按照
dim后一维进行级联(concatenate); - 最后转换为
out的数据类型输出。
即out = Cast(Concat(Chunk(tensors, dim, numChunks), dim+1), out_dtype)。该算子位于仓库的 conversion 目录(conversion/chunk_cat),属于数据转换类基础算子,适用于需要对张量做“分块后横向拼接”的场景。
从算子定义 chunk_cat_def.cpp 可以看到它的完整 IO 与属性声明:
- 输入
x:动态个数(ParamType(DYNAMIC))的 tensor 列表,数据类型支持 FLOAT16、BF16、FLOAT; - 输出
y:必选输出,数据类型支持 FLOAT16、BF16、FLOAT(可以与输入不同); - 属性
dim:必选整型属性,切分维度; - 属性
num_chunks:必选整型属性,切分块数; - 支持的硬件配置为
ascend910b、ascend910_93、ascend950、ascend350四类 AICore 配置。
输出 Shape 的推导规则
从 chunk_cat_infershape.cpp 的实现可以看出输出 shape 的计算方式:
- 输出维度数为
dim + 2(dim维之后的所有维度被合维); - 输出的第
dim维大小为numChunks; - 对每个输入 tensor:
chunkCol = (chunkDimSize + numChunks - 1) / numChunks(向上取整,即每个分块的列数); - 第
dim维之前各维度保持输入形状(所有输入需一致); dim + 1维的大小为所有输入 tensor 的chunkCol × (dim 之后各维的乘积)之和。
以文档调用示例为例:两个输入 shape 分别为{2, 3}与{1, 3},dim = 0、numChunks = 2,则输出 shape 为{2, 6}——第 0 维为 2(numChunks),第 1 维为(3/2 向上取整)=2与(3/2 向上取整)=2之和再乘以后续维(无)的乘积,即 2+2 再按规则累加得到 6。
二、产品支持情况
ChunkCat 算子通过 aclnnChunkCat 接口调用时,各产品的支持情况如下:
| 产品 | 是否支持 |
|---|---|
| Ascend 950PR/Ascend 950DT | 支持 |
| Atlas A3 训练系列产品 / Atlas A3 推理系列产品 | 支持 |
| Atlas A2 训练系列产品 / Atlas A2 推理系列产品 | 支持 |
| Atlas 200I/500 A2 推理产品 | 不支持 |
| Atlas 推理系列产品 | 不支持 |
| Atlas 训练系列产品 | 不支持 |
该支持矩阵与算子定义中AICore().AddConfig(...)声明的ascend910b、ascend910_93、ascend950、ascend350四类配置一一对应(chunk_cat_def.cpp),同时在 chunk_cat/README.md 中有同样的产品支持表。
三、两段式接口与函数原型
每个 aclnn 算子采用两段式接口(转换为仓库根路径:两段式接口说明)设计,必须先调用第一段接口aclnnChunkCatGetWorkspaceSize获取计算所需 workspace 大小以及包含了算子计算流程的执行器,再调用第二段接口aclnnChunkCat执行计算。
第一段接口:aclnnChunkCatGetWorkspaceSize
aclnnStatus aclnnChunkCatGetWorkspaceSize( const aclTensorList *tensors, int64_t dim, int64_t numChunks, aclTensor *out, uint64_t *workspaceSize, aclOpExecutor **executor)第二段接口:aclnnChunkCat
aclnnStatus aclnnChunkCat( void *workspace, uint64_t workspaceSize, aclOpExecutor *executor, aclrtStream stream)从源码 aclnn_chunk_cat.cpp 可以看到两段接口的真实分工:
- 第一段接口
aclnnChunkCatGetWorkspaceSize内先校验dim != 0、numChunks <= 0、tensors空指针等前置条件,然后通过CheckParams完成数据类型/格式/维度的完整入参校验,再调用SplitToChunkCat构建算子执行图(对输入做 Contiguous、MergeLastDims、按每 512 个输入为一组分批 ChunkCat),最后把uniqueExecutor->GetWorkspaceSize()写入workspaceSize并将执行器通过ReleaseTo释放给调用方; - 第二段接口
aclnnChunkCat直接调用CommonOpExecutorRun(workspace, workspaceSize, executor, stream)在指定 Stream 上异步执行。
四、aclnnChunkCatGetWorkspaceSize 参数说明
| 参数名 | 输入/输出 | 描述 | 使用说明 | 数据类型 | 格式类型 | 维度(shape) | 非连续Tensor |
|---|---|---|---|---|---|---|---|
| tensors(aclTensorList*) | 输入 | 输入的 tensor 列表 | 列表内数据类型需一致 | FLOAT、FLOAT16、BFLOAT16 | ND | 1-8 | √ |
| dim(int64_t) | 输入 | 需要切分块的维度 | int 类型的值,当前仅支持为 0 | INT64 | - | - | - |
| numChunks(int64_t) | 输入 | 需要切分块的数量 | 必须大于 0 | INT64 | - | - | - |
| out(aclTensor*) | 输出 | 输出 tensor | 数据类型所占字节数需要是输入数据类型所占字节数的非 0 整数倍 | FLOAT、FLOAT16、BFLOAT16 | ND | - | - |
| workspaceSize(uint64_t*) | 输出 | 返回需要在 Device 侧申请的 workspace 大小 | - | - | - | - | - |
| executor(aclOpExecutor**) | 输出 | 返回 op 执行器,包含了算子计算流程 | - | - | - | - | - |
对应源码中的入参校验逻辑(aclnn_chunk_cat.cpp)可以进一步印证上述约束:
CheckNotNull:tensors及其内部每个 tensor、out均不能为空指针;CheckDtypeValid:输入列表内所有 tensor 数据类型必须一致,且必须落在{DT_FLOAT, DT_FLOAT16, DT_BF16}支持列表内;当输入为 FLOAT 时,输出也必须是 FLOAT(output dtype must be float when input dtype is float);输出数据类型同样必须在支持列表内;CheckFormat:输入输出不允许私有 format(仅支持 ND、NCHW、NHWC、HWCN、NDHWC、NCDHW 等公共格式,文档中要求为 ND);CheckShape:每个输入 tensor 的维数不能为 0,且不能超过MAX_SUPPORT_DIMS_NUMS(对应文档 1~8 维)。
另外,numChunks与输入个数的关系在接口层有特殊处理:当tensors->Size() == 0时,第一段接口直接返回成功;当输入数量超过CAT_INPUT_NUM = 512时,SplitToChunkCat会按每 512 个一组分批执行 ChunkCat,再对中间结果继续分批,直到收敛为单个 tensor(见 aclnn_chunk_cat.cpp)。
返回值
aclnnStatus:返回状态码,具体参见 aclnn返回码。
第一段接口完成入参校验,出现以下场景时报错:
| 返回值 | 错误码 | 描述 |
|---|---|---|
| ACLNN_ERR_PARAM_NULLPTR | 161001 | 传入的 tensors 或 out 是空指针。 |
| ACLNN_ERR_PARAM_INVALID | 161002 | tensors 列表中 tensor 的数据类型和数据格式不在支持的范围之内。 |
| ACLNN_ERR_PARAM_INVALID | 161002 | tensors 列表中 tensor 数据类型不一致。 |
| ACLNN_ERR_PARAM_INVALID | 161002 | tensors 列表中 tensor 的 shape 不在 1~8 维。 |
| ACLNN_ERR_PARAM_INVALID | 161002 | dim 不为 0。 |
| ACLNN_ERR_PARAM_INVALID | 161002 | numChunks 小于等于 0。 |
五、aclnnChunkCat 参数说明
| 参数名 | 输入/输出 | 描述 |
|---|---|---|
| workspace | 输入 | 在 Device 侧申请的 workspace 内存地址。 |
| workspaceSize | 输入 | 在 Device 侧申请的 workspace 大小,由第一段接口 aclnnChunkCatGetWorkspaceSize 获取。 |
| executor | 输入 | op 执行器,包含了算子计算流程。 |
| stream | 输入 | 指定执行任务的 Stream。 |
返回值
aclnnStatus:返回状态码,具体参见 aclnn返回码。
六、约束说明
- 确定性计算:aclnnChunkCat 默认确定性实现。
- 输入 tensor 列表内数据类型需一致,且仅支持 FLOAT、FLOAT16、BFLOAT16;
- 输入输出数据格式为 ND,shape 维度为 1~8 维;
dim当前仅支持为 0;numChunks必须大于 0;- 输出数据类型所占字节数需为输入数据类型所占字节数的非 0 整数倍(这一点与算子内核对“对齐/转置”路径的选择密切相关,见下文)。
七、源码级实现原理
7.1 图融合/编译配置
算子为每个目标平台提供了 binary 配置。以 ascend910b/chunk_cat_binary.json 为例,它声明了 7 组输入/输出数据类型组合对应的 kernel 二进制:
ChunkCat_fp16_fp16、ChunkCat_fp16_bf16、ChunkCat_fp16_fp32ChunkCat_bf16_fp16、ChunkCat_bf16_bf16、ChunkCat_bf16_fp32ChunkCat_fp32_fp32
其中输入x为paramType: "dynamic"的动态个数输入,输出y为paramType: "required",属性dim与num_chunks为运行时传入(value: null),shape 均以-2表示动态 shape。同理的配置还存在于 ascend350、ascend910_93、ascend950 目录下。
7.2 L0 层算子入口
chunk_cat.cpp 中的l0op::ChunkCat是底层入口:它通过executor->AllocTensor(outDtype, FORMAT_ND, FORMAT_ND)分配输出 tensor,随后调用INFER_SHAPE完成 shape 推导,再通过ADD_TO_LAUNCHER_LIST_AICORE把算子加入 AICore 启动列表。该入口被上层 aclnn_chunk_cat.cpp 的SplitToChunkCat调用,并配合l0op::Contiguous(转连续)、l0op::ViewCopy(视图拷贝)完成整条计算链。
7.3 Tiling 策略
chunk_cat_tiling.cpp 展示了该算子的 tiling 设计要点:
- 通过
PlatformAscendC获取 AIV 核数coreNum_与 UB 大小ubSize_,并申请系统 workspace; - 根据输入输出数据类型是否相同决定 UB 切分比例:类型不同时
inUbSize_ = (ubSize_ + reserveUb) / 3(UB 三切分),类型相同时inUbSize_ = (ubSize_ + reserveUb) / 2(UB 对半切分),其余留给输出; - 计算每个输入 tensor 的
chunkCol = (chunkDimSize + numChunks - 1) / numChunks,累加得到outputCol_,并判断整列是否按 32B/16B 对齐(isAllAlign_、isHalfAlign_); DoUbSplit依据isRegBase、isAllAlign_、isOneConcat_等标志选择不同的行/列切分路径(列切优先,必要时行切),DoBlockSplit按核数做二维 block 切分,最终SetBlockDim(usedCoreNum_)设置启动核数,并把全部 tiling 参数写入ChunkCatTilingData。
7.4 Kernel 计算路径
op_kernel/chunk_cat.cpp 为 kernel 入口,根据__CCE_AICORE__宏选择通用实现(chunk_cat.h)或 Arch35 实现(chunk_cat_arch35.h),并通过ORIG_DTYPE_X == ORIG_DTYPE_Y模板分支决定是否启用NEED_CAST类型转换。
通用实现 chunk_cat.h 的主流程Process对每个 UB 子块执行“清零 UB → CopyIn 搬入 → Compute 计算 → CopyOut 搬出”的流水:
Compute依据场景分派四条路径:isOneConcat_(单行且不对齐)走ComputeOneConcat直接搬移;全对齐走ComputeAllAlign(UB 重排后 concat);单输入且列对齐走简单拷贝;其余情况走ComputeNotAlign——通过UBRearrange4Trans(UB 重排)、Trans1(跨 block 对齐转置,调用TransDataTo5HD)、UBRearrange4TransConcat、Trans2完成分块数据的拼接与对齐;- 当输入输出类型不同(
NEED_CAST)时,DoCast根据字节数是否相同选择“经 FP32 中转的CAST_RINT两次 Cast”或“直接CAST_NONECast”; ComputeOver32会在单次 UB 累积超过 32 个输入 tensor 时提前做部分 concat,避免输入个数过多导致的 UB 溢出。
八、调用示例
示例代码如下,仅供参考,具体编译和执行过程请参考 编译与运行样例。该示例与仓库中的真实样例 examples/test_aclnn_chunk_cat.cpp 一致,两个输入 shape 分别为{2, 3}与{1, 3},dim = 0、numChunks = 2,输出 shape 为{2, 6}:
#include <iostream> #include <vector> #include "acl/acl.h" #include "aclnnop/aclnn_chunk_cat.h" #define CHECK_RET(cond, return_expr) \ do { \ if (!(cond)) { \ return_expr; \ } \ } while (0) #define LOG_PRINT(message, ...) \ do { \ printf(message, ##__VA_ARGS__); \ } while (0) int64_t GetShapeSize(const std::vector<int64_t>& shape) { int64_t shapeSize = 1; for (auto i : shape) { shapeSize *= i; } return shapeSize; } int Init(int32_t deviceId, aclrtStream* stream) { // 固定写法,资源初始化 auto ret = aclInit(nullptr); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclInit failed. ERROR: %d\n", ret); return ret); ret = aclrtSetDevice(deviceId); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtSetDevice failed. ERROR: %d\n", ret); return ret); ret = aclrtCreateStream(stream); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtCreateStream failed. ERROR: %d\n", ret); return ret); return 0; } template <typename T> int CreateAclTensor(const std::vector<T>& hostData, const std::vector<int64_t>& shape, void** deviceAddr, aclDataType dataType, aclTensor** tensor) { auto size = GetShapeSize(shape) * sizeof(T); // 调用aclrtMalloc申请device侧内存 auto ret = aclrtMalloc(deviceAddr, size, ACL_MEM_MALLOC_HUGE_FIRST); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtMalloc failed. ERROR: %d\n", ret); return ret); // 调用aclrtMemcpy将host侧数据拷贝到device侧内存上 ret = aclrtMemcpy(*deviceAddr, size, hostData.data(), size, ACL_MEMCPY_HOST_TO_DEVICE); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtMemcpy failed. ERROR: %d\n", ret); return ret); // 计算连续tensor的strides std::vector<int64_t> strides(shape.size(), 1); for (int64_t i = shape.size() - 2; i >= 0; i--) { strides[i] = shape[i + 1] * strides[i + 1]; } // 调用aclCreateTensor接口创建aclTensor *tensor = aclCreateTensor(shape.data(), shape.size(), dataType, strides.data(), 0, aclFormat::ACL_FORMAT_ND, shape.data(), shape.size(), *deviceAddr); return 0; } int main() { // 1.(固定写法)device/stream初始化,参考acl API手册 // 根据自己的实际device填写deviceId int32_t deviceId = 0; aclrtStream stream; auto ret = Init(deviceId, &stream); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("Init acl failed. ERROR: %d\n", ret); return ret); // 2.构造输入与输出,需要根据API的接口自定义构造 std::vector<int64_t> selfShape1 = {2, 3}; std::vector<int64_t> selfShape2 = {1, 3}; std::vector<int64_t> outShape = {2, 6}; void* input1DeviceAddr = nullptr; void* input2DeviceAddr = nullptr; void* outDeviceAddr = nullptr; aclTensor* input1 = nullptr; aclTensor* input2 = nullptr; aclTensor* out = nullptr; std::vector<float> input1HostData = {1, 2, 3, 4, 5, 6}; std::vector<float> input2HostData = {7, 8, 9}; std::vector<float> outHostData(12, 0); // 创建input1 aclTensor ret = CreateAclTensor(input1HostData, selfShape1, &input1DeviceAddr, aclDataType::ACL_FLOAT, &input1); CHECK_RET(ret == ACL_SUCCESS, return ret); // 创建input2 aclTensor ret = CreateAclTensor(input2HostData, selfShape2, &input2DeviceAddr, aclDataType::ACL_FLOAT, &input2); CHECK_RET(ret == ACL_SUCCESS, return ret); // 创建out aclTensor ret = CreateAclTensor(outHostData, outShape, &outDeviceAddr, aclDataType::ACL_FLOAT, &out); CHECK_RET(ret == ACL_SUCCESS, return ret); std::vector<aclTensor*> tmp{input1, input2}; aclTensorList* tensorList = aclCreateTensorList(tmp.data(), tmp.size()); // 3.调用CANN算子库API,需要修改为具体的API名称 int64_t dim = 0; int64_t numChunks = 2; uint64_t workspaceSize = 0; aclOpExecutor* executor; // 调用aclnnChunkCat第一段接口 ret = aclnnChunkCatGetWorkspaceSize(tensorList, dim, numChunks, out, &workspaceSize, &executor); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnChunkCatGetWorkspaceSize failed. ERROR: %d\n", ret); return ret); // 根据第一段接口计算出的workspaceSize申请device内存 void* workspaceAddr = nullptr; if (workspaceSize > 0) { ret = aclrtMalloc(&workspaceAddr, workspaceSize, ACL_MEM_MALLOC_HUGE_FIRST); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("allocate workspace failed. ERROR: %d\n", ret); return ret); } // 调用aclnnChunkCat第二段接口 ret = aclnnChunkCat(workspaceAddr, workspaceSize, executor, stream); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnChunkCat failed. ERROR: %d\n", ret); return ret); // 4.(固定写法)同步等待任务执行结束 ret = aclrtSynchronizeStream(stream); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtSynchronizeStream failed. ERROR: %d\n", ret); return ret); // 5.获取输出的值,将device侧内存上的结果拷贝至host侧,需要根据具体API的接口定义修改 auto size = GetShapeSize(outShape); std::vector<float> outData(size, 0); ret = aclrtMemcpy(outData.data(), outData.size() * sizeof(outData[0]), outDeviceAddr, size * sizeof(outData[0]), ACL_MEMCPY_DEVICE_TO_HOST); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("copy result from device to host failed. ERROR: %d\n", ret); return ret); for (int64_t i = 0; i < size; i++) { LOG_PRINT("result[%ld] is: %f\n", i, outData[i]); } // 6.释放aclTensor和aclScalar,需要根据具体API的接口定义修改 aclDestroyTensorList(tensorList); aclDestroyTensor(out); // 7.释放Device资源,需要根据具体API的接口定义修改 aclrtFree(input1DeviceAddr); aclrtFree(input2DeviceAddr); aclrtFree(outDeviceAddr); if (workspaceSize > 0) { aclrtFree(workspaceAddr); } aclrtDestroyStream(stream); aclrtResetDevice(deviceId); aclFinalize(); return 0; }示例要点拆解
- Init 初始化:
aclInit→aclrtSetDevice→aclrtCreateStream,固定写法; - 构造 aclTensor:
CreateAclTensor模板函数通过aclrtMalloc申请 Device 内存、aclrtMemcpy拷入 Host 数据、按行主序计算连续 strides,最后aclCreateTensor创建 ND 格式 tensor;多个输入通过aclCreateTensorList打包为aclTensorList; - 两段式调用:先
aclnnChunkCatGetWorkspaceSize拿到workspaceSize与executor,再按需aclrtMalloc申请 workspace,最后aclnnChunkCat(workspaceAddr, workspaceSize, executor, stream)提交执行; - 同步与取数:
aclrtSynchronizeStream等待执行结束,aclrtMemcpy(DEVICE_TO_HOST)取回结果并打印; - 资源释放:依次释放 tensor list、out tensor、各 Device 内存、workspace、Stream,最后
aclrtResetDevice与aclFinalize。
九、进一步阅读
- 算子总览与调用方式对照表:conversion/chunk_cat/README.md
- 两段式接口设计说明:docs/zh/context/two_phase_api.md
- aclnn 返回码说明:docs/zh/context/aclnn_return_code.md
- 编译与运行样例:docs/zh/context/compile_and_run_sample.md
- 算子注册与 Shape 推导:op_host/chunk_cat_def.cpp、op_host/chunk_cat_infershape.cpp
- Tiling 与 Kernel 实现:op_host/chunk_cat_tiling.cpp、op_kernel/chunk_cat.h
- 平台 binary 配置:op_host/config/ascend910b/chunk_cat_binary.json(ascend350 / ascend910_93 / ascend950 目录下配置结构相同)
【免费下载链接】ops-math本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-math
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考