CANN ops-math 算子 aclnnExpand 使用指南:广播扩展接口的原理解析与编程实践
2026/9/20 6:51:41 网站建设 项目流程
  • 算子库
  • 人工智能
  • CANN

【免费下载链接】ops-math

本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。

项目地址:https://gitcode.com/cann/ops-math
点击查看免费下载

本指南以 CANN ops-math 数学算子库中的 aclnnExpand 算子文档 为核心,系统讲解 Expand(广播扩展)算子的功能语义、两段式 aclnn 接口原型、参数约束、返回码以及完整调用示例,并深入其所在算子目录 math/expand 的源码实现,帮助你理解该接口从入参校验、算子下发到 NPU/AICPU 内核执行的完整链路,能够独立编写并运行调用 aclnnExpand 的应用代码。

算子功能与数学定义

Expand 算子将输入张量self广播(broadcast)成指定 shape 的张量。其语义与 PyTorch 的Tensor.expand一致:当输入张量的某一维度为 1 时,可以在对应维度上重复该维数据以扩展至目标 shape;维度不足时则在头部补 1 再参与广播。

扩展后张量 Y 与原始张量 X 的元素映射关系为:

$$Y_{i_1,\dots,i_k,\dots,i_n} = X_{i_1,\dots,\lfloor i_k / m \rfloor,\dots,i_n}$$

其中 Y 为 expand 后的张量,m 为第 k 维的扩展倍数。从算子仓库的 README 可得到直观示例:输入 tensor 的 shape 为(1, 4),指定 size 为(2, 4),则输出是 shape 为(2, 4)的 tensor——第 0 维由 1 扩展为 2。

产品支持情况

根据 aclnnExpand.md 与 README,当前支持的产品如下:

产品是否支持
Ascend 950PR / Ascend 950DT支持
Atlas A3 训练系列产品 / Atlas A3 推理系列产品支持
Atlas A2 训练系列产品 / Atlas A2 推理系列产品支持
Atlas 200I/500 A2 推理产品不支持
Atlas 推理系列产品支持
Atlas 训练系列产品支持

两段式接口与函数原型

aclnnExpand 采用 CANN aclnn 算子库通用的两段式接口设计:必须先调用aclnnExpandGetWorkspaceSize获取计算所需 workspace 大小以及包含了算子计算流程的执行器,再调用aclnnExpand接口执行计算。

aclnnStatus aclnnExpandGetWorkspaceSize( const aclTensor* self, const aclIntArray* size, aclTensor* out, uint64_t* workspaceSize, aclOpExecutor** executor)
aclnnStatus aclnnExpand( void* workspace, uint64_t workspaceSize, aclOpExecutor* executor, aclrtStream stream)

从源码 op_api/aclnn_expand.cpp 可以看到两段接口的实现分工:

  • 第一段aclnnExpandGetWorkspaceSize依次完成创建 OpExecutor、参数校验(CheckParams)、空 tensor 处理,然后通过l0op::Contiguous将输入self转成连续 tensor,调用l0op::Expand构图计算,再通过l0op::ViewCopy将结果拷贝到输出outout可以是非连续 tensor),最后通过uniqueExecutor->GetWorkspaceSize()汇总计算所需的 workspace 大小;
  • 第二段aclnnExpand直接调用CommonOpExecutorRun(workspace, workspaceSize, executor, stream)完成实际计算,这也是所有 aclnn 接口统一的执行入口。

这里还包含两个值得注意的特殊处理:

  1. 空 tensor 短路:当selfout为空 tensor 时,workspaceSize直接置 0 并返回成功,无需真正下发计算;
  2. 0 维标量特例:当selfDimNum == 0 && size->Size() == 0(即 0 维标量)时,直接使用l0op::ViewCopy完成拷贝,而非常规的 Contiguous + Expand 流程。

aclnnExpandGetWorkspaceSize 参数说明

参数名输入/输出描述使用说明数据类型数据格式维度(shape)非连续张量 Tensor
self输入表示待广播的目标张量,公式中的 selfshape 与 size 满足 broadcast 关系FLOAT16、FLOAT、UINT8、INT8、INT32、INT64、BOOL、BF16ND0-8
size输入广播时指定的 size-INT---
out输出广播后的张量shape 需要满足 self 的 shape 根据 size 的推导结果与 self 一致ND-
workspaceSize输出返回需要在 Device 侧申请的 workspace 大小-----
executor输出返回 op 执行器,包含了算子计算流程-----

BF16 平台差异说明Atlas 训练系列产品(Ascend 910)与Atlas 推理系列产品(Ascend 310P)不支持 BFLOAT16 数据类型。这一限制在源码中得到印证——aclnn_expand.cpp 中定义了按 NPU 架构区分的支持列表:默认的ASCEND910_DTYPE_DTYPE_SUPPORT_LIST仅包含 FLOAT16、FLOAT、UINT8、INT8、INT32、INT64、BOOL 七种类型,而DAV_2201(910B)与DAV_3510(350/950 系列)架构额外支持DT_BF16

此外,CheckFormat会对self的存储格式进行检查:若为 NZ(FORMAT_FRACTAL_NZ)格式会打印警告日志,提示该格式可能导致精度问题(见 aclnn_expand.cpp)。

返回值与错误码

aclnnStatus 为函数返回状态码,具体含义参见 aclnn 返回码说明。第一段接口完成入参校验,以下场景会报错:

返回值错误码描述
ACLNN_ERR_PARAM_NULLPTR161001传入的 self、size、out 是空指针
ACLNN_ERR_PARAM_INVALID161002self、out 的数据类型或数据格式不在支持的范围之内
ACLNN_ERR_PARAM_INVALID161002self 与 out 的数据类型不一致
ACLNN_ERR_PARAM_INVALID161002self 或 out 的 shape 和 size 不匹配(size 个数小于 self 的 dimNum;self 任意维度不等于 size 偏移后对应维度且不等于 1(偏移量为 size 与 self 的 dimNum 差值);output 的 shape 不等于预期 shape(size))
ACLNN_ERR_PARAM_INVALID161002self 最大维度超过 8

这些校验在 aclnn_expand.cpp 的CheckParams中依次执行:CheckNotNull(空指针检查)、CheckDtypeValid(dtype 一致性 + 支持列表检查)、CheckShape(广播关系检查)、CheckMaxDimension(最大 8 维限制,对应源码中MAX_SUPPORT_DIM = 8)。其中CheckShape还支持 size 中维度值为-1的语义:-1表示该维继承 self 对应维的大小(见 aclnn_expand.cpp)。

aclnnExpand 参数说明

参数名输入/输出描述
workspace输入在 Device 侧申请的 workspace 内存地址
workspaceSize输入在 Device 侧申请的 workspace 大小,由第一段接口 aclnnExpandGetWorkspaceSize 获取
executor输入op 执行器,包含了算子计算流程
stream输入指定执行任务的 Stream

第二段接口同样返回 aclnnStatus 状态码,参见 aclnn 返回码说明。

约束说明

  • 确定性计算:aclnnExpand 默认确定性实现,同一输入多次执行结果可复现。
  • 除上述参数校验约束外,无其他额外约束。

调用示例

以下示例代码摘自 aclnnExpand.md,仓库中还提供了可直接运行的完整样例 test_aclnn_expand.cpp(使用 RAII 智能指针管理资源)以及图模式调用样例 test_geir_expand.cpp。具体编译和执行过程请参考编译与运行样例。

#include <iostream> #include <vector> #include "acl/acl.h" #include "aclnnop/aclnn_expand.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> selfShape = {4, 2}; std::vector<int64_t> outShape = {4, 2}; void* selfDeviceAddr = nullptr; void* outDeviceAddr = nullptr; aclTensor* self = nullptr; aclIntArray* size = nullptr; aclTensor* out = nullptr; std::vector<float> selfHostData = {0, 1, 2, 3, 4, 5, 6, 7}; std::vector<float> outHostData = {0, 0, 0, 0, 0, 0, 0, 0}; int64_t sizeValue[2] = {4, 2}; size = aclCreateIntArray(&(sizeValue[0]), 2); // 创建self aclTensor ret = CreateAclTensor(selfHostData, selfShape, &selfDeviceAddr, aclDataType::ACL_FLOAT, &self); 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); // 3. 调用CANN算子库API,需要修改为具体的API名称 uint64_t workspaceSize = 0; aclOpExecutor* executor; // 调用aclnnExpand第一段接口 ret = aclnnExpandGetWorkspaceSize(self, size, out, &workspaceSize, &executor); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnExpandGetWorkspaceSize 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); } // 调用aclnnExpand第二段接口 ret = aclnnExpand(workspaceAddr, workspaceSize, executor, stream); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnExpand 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 resultSize = GetShapeSize(outShape); std::vector<float> resultData(resultSize, 0); ret = aclrtMemcpy(resultData.data(), resultData.size() * sizeof(resultData[0]), outDeviceAddr, resultSize * sizeof(resultData[0]), ACL_MEMCPY_DEVICE_TO_HOST); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("copy resultData from device to host failed. ERROR: %d\n", ret); return ret); for (int64_t i = 0; i < resultSize; i++) { LOG_PRINT("resultData[%ld] is: %f\n", i, resultData[i]); } // 6. 释放aclTensor和aclScalar,需要根据具体API的接口定义修改 aclDestroyTensor(self); aclDestroyTensor(out); aclDestroyIntArray(size); // 7. 释放device资源,需要根据具体API的接口定义修改 aclrtFree(selfDeviceAddr); aclrtFree(outDeviceAddr); if (workspaceSize > 0) { aclrtFree(workspaceAddr); } aclrtDestroyStream(stream); aclrtResetDevice(deviceId); aclFinalize(); return 0; }

示例程序遵循 aclnn 调用的标准七步流程:① device/stream 初始化 → ② 构造输入输出 aclTensor(通过aclCreateIntArray创建 size、aclCreateTensor创建 self/out)→ ③ 两段式接口调用(先aclnnExpandGetWorkspaceSize拿 workspace 大小并申请内存,再aclnnExpand执行)→ ④aclrtSynchronizeStream同步 → ⑤ 将结果从 device 侧拷回 host 侧并打印 → ⑥ 销毁 tensor/intArray → ⑦ 释放 device 资源并aclFinalize

源码级实现纵深:从算子定义到内核执行

算子定义与 Infershape

算子注册在 op_host/expand_def.cpp 中:输入x与输出y支持 FLOAT、FLOAT16、INT32、UINT8、INT8、BOOL、BF16、INT64 共 8 种数据类型,格式均为 ND;输入shape为 INT32/INT64 的常量张量,且声明了ValueDepend(OPTIONAL)(该输入参与 shape 推导,属于值依赖输入)。AICore 配置声明了动态 shape、动态 rank 支持,并指定内核文件expand_apt

形状推导逻辑在 op_host/expand_infershape.cpp 的InferShape4Expand中实现,其广播规则与 PyTorch 语义对齐:

  • shape维数不能小于x的维数(否则报错),多出的头部维度视为对x补 1;
  • 目标维度为-1时,输出该维取x对应维的大小;
  • 目标维度为 1 而x对应维不为 1 时,输出该维取x的维度(不做双向广播);
  • 广播合法性检查:x维度必须为 1 或与目标维度相等,否则报"cannot be broadcast"错误。

同时该文件通过InputsDataDependency({1})声明 shape 输入为值依赖,框架会先取到 shape 的常量值再执行推导。

Tiling 与 Kernel

Ascend 350/950 架构(arch35)的 tiling 实现在 op_host/arch35/expand_tiling_arch35.cpp 中,从源码结构看它复用了广播类算子的 tiling 基类brcto::BroadcastToTilingAscendC(来自 conversion/broadcast_to 模块):先通过AdjustShapesToSameDimNum将输入输出对齐到相同维数,再依次执行广播规则校验、DeleteOneSizeAxis(删除维度为 1 的轴)与MergeAxis(轴合并)等优化,最后调用DoTiling生成计算分片信息。

内核实现 op_kernel/expand_apt.cpp 更为直接——expand内核函数直接调用broadcast_to_impl(x, shape, y, workspace, tiling),即 Expand 在 NPU 侧完全复用 broadcast_to 的向量内核实现。

AICPU 兜底实现

op_kernel_aicpu/expand_aicpu.cpp 提供了 AICPU 侧的兜底实现ExpandCpuKernel,其计算流程为:

  1. 空 tensor 处理HandleEmptyTensor在 shape 元素数为 0(标量场景)时直接拷贝输入到输出;
  2. shape 归一化NormalizeExpandShape对 target_shape 做合法性与广播规则校验(-1继承输入维度、维度 1 保持、输入非 1 且不等于目标则报错);
  3. 逐层扩展ExpandByLayer从最低维开始,找到第一个输入与目标不同的维度作为 break_axis,计算出copy_size(需要整块复制的元素数)与expand_factor(扩展倍数),通过CalculateOutIndex按块复制生成中间结果,迭代直到所有维度对齐。

内核注册通过OPS_MATH_REGISTER_CPU_KERNELV2(kExpand, ExpandCpuKernel)完成,并在 expand_aicpu_def.cpp 中补充对应定义。

测试与验证

仓库为 Expand 提供了多层次验证:

  • API 层单测:tests/ut/op_api/test_aclnn_expand.cpp;
  • Infershape 单测:tests/ut/op_host/test_expand_infershape.cpp,覆盖广播合法性、-1维度继承等规则;
  • Tiling 单测:tests/ut/op_host/arch35/test_expand_tiling.cpp;
  • AICPU 单测:tests/ut/op_kernel_aicpu/test_expand.cpp;
  • ST 用例:tests/st/aclnnExpand/atk_aclnnExpand.json 与配套的 executor_aclnnExpand.py。

常见问题与建议

  • shape 与 size 不匹配self任意维度必须等于 size 偏移后对应维度或为 1,且out的 shape 必须等于 size 推导结果;size的维数不能小于self的维数。
  • 数据类型不一致selfout的数据类型必须完全一致,且需落在当前产品架构支持的 dtype 列表中(注意 Ascend 910 / 310P 不支持 BF16)。
  • 维度上限self最大支持 8 维,超出会返回ACLNN_ERR_PARAM_INVALID
  • 空指针与内存selfsizeout传空指针会返回ACLNN_ERR_PARAM_NULLPTR;workspace 只有在workspaceSize > 0时才需要申请,申请与释放均应使用aclrtMalloc/aclrtFree成对完成。
  • 非连续张量selfout均支持非连续 tensor,接口内部通过ContiguousViewCopy自动完成连续性处理,调用方无需手动转换。
  • 算子库
  • 人工智能
  • CANN

【免费下载链接】ops-math

本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。

项目地址:https://gitcode.com/cann/ops-math
点击查看免费下载
上一篇:Pokémon卡片CSS全息特效部署指南:从开发到生产的完整流程
下一篇:res-downloader:3步捕获视频号、抖音无水印资源

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询