CANN ops-math 极坐标算子 Polar 深度解析:aclnnPolar 接口、Ascend C 融合 Kernel 设计与广播实现
2026/9/20 13:50:54 网站建设 项目流程
  • 算子库
  • 人工智能
  • CANN

【免费下载链接】ops-math

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

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

本文围绕 CANN 数学算子库 ops-math 中experimental/math/polar目录的 Polar 算子展开,系统讲解其从数学定义、接口语义、参数约束到 Ascend C 单 Kernel 融合实现与性能验证的完整链路。读完本文,你将掌握如何通过两段式 aclnnPolar 接口构造极坐标复数张量、理解该算子将 6 个 l0 算子融合为单 Kernel 的设计思路,并能在 Atlas A2/A3 系列产品上正确调用与验证该算子。

产品支持情况

Polar 算子当前在以下产品上获得支持(数据来源:experimental/math/polar/README.md):

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

从算子原型定义(polar_def.cpp)可以看到,对应的底层芯片配置为ascend910b(Atlas A2 系列)与ascend910_93(Atlas A3 系列)两个 AICore 配置:

this->AICore().AddConfig("ascend910b"); // Atlas A2 训练/推理系列产品 this->AICore().AddConfig("ascend910_93"); // Atlas A3 训练/推理系列产品

功能说明

Polar 算子的功能是由模长(abs)与幅角(angle)构造极坐标复数张量,对应 PyTorch 接口torch.polar(abs, angle)

计算公式如下:

$$ out_i = input_i \times (\cos(angle_i) + i \cdot \sin(angle_i)) $$

等价地拆开实部与虚部:

  • out.real = input · cos(angle)
  • out.imag = input · sin(angle)

这是一个典型的逐元素(elementwise)计算:每个输出元素只依赖对应位置的输入元素,天然具备多核并行与确定性计算的特性(无 Reduce/Scatter 等跨元素聚合)。

参数说明

Polar 算子的核心参数如下(数据来源:experimental/math/polar/README.md):

参数名输入/输出/属性描述数据类型数据格式
input输入极坐标模长分量,公式中的 inputFLOATND
angle输入极坐标幅角(弧度),公式中的 angleFLOATND
out输出计算结果,公式中的 out;shape 为 input 与 angle 广播后的 shapeCOMPLEX64ND

约束说明

  • input 与 angle 的数据类型必须一致(均为 FLOAT)。
  • input 与 angle 的 shape 满足 NumPy 广播关系,out 的 shape 为两者广播后的 shape。
  • input、angle 的维度数不超过 8 维,数据格式仅支持 ND。
  • out 的数据类型恒为 COMPLEX64(与 input 的数据类型无关)。
  • 不支持的 dtype:fp16 / bf16 / fp64 / complex128(任务范围外,l0 参考实现亦不支持)。
  • 确定性计算:aclnnPolar 默认确定性实现。

关于广播关系,官方接口文档给出了更细的说明(见 aclnnPolar.md):input 与 angle 的 shape 需满足 broadcast 关系,即右对齐后逐维相等或其一为 1,out 的 shape 为两者各维取 max 的结果。例如[3,1,4][2,4]广播得到[3,2,4]

接口定义:两段式 aclnnPolar

Polar 算子采用 CANN 标准的两段式接口设计。每个算子必须先调用aclnnPolarGetWorkspaceSize接口获取入参并根据计算流程计算所需 workspace 大小,再调用aclnnPolar接口执行计算。

函数原型

aclnnStatus aclnnPolarGetWorkspaceSize( const aclTensor* input, const aclTensor* angle, aclTensor* out, uint64_t* workspaceSize, aclOpExecutor** executor)
aclnnStatus aclnnPolar( void* workspace, uint64_t workspaceSize, aclOpExecutor* executor, const aclrtStream stream)

aclnnPolarGetWorkspaceSize 参数说明

参数名输入/输出描述使用说明数据类型数据格式维度(shape)非连续Tensor
input输入公式中的 input(极坐标模长分量)数据类型需与 angle 一致;input 与 angle 的 shape 满足 broadcast 关系FLOATND0-8
angle输入公式中的 angle(极坐标幅角,弧度)数据类型需与 input 一致;input 与 angle 的 shape 满足 broadcast 关系FLOATND0-8
out输出公式中的 outout 的 shape 与 input、angle 做 broadcast 后的 shape 一致;数据类型恒为 COMPLEX64COMPLEX64ND0-8
workspaceSize输出返回需要在 Device 侧申请的 workspace 大小-----
executor输出返回 op 执行器,包含了算子计算流程-----

返回值aclnnStatus,返回状态码,具体参见 aclnn 返回码。

第一段接口完成入参校验,出现如下场景时报错:

返回值错误码描述
ACLNN_ERR_PARAM_NULLPTR161001传入的 input、angle、out 是空指针
ACLNN_ERR_PARAM_INVALID161002input、angle 的数据类型或数据格式不在支持的范围之内
ACLNN_ERR_PARAM_INVALID161002input 与 angle 的数据类型不一致
ACLNN_ERR_PARAM_INVALID161002input 或 angle 的 shape 超过 8 维
ACLNN_ERR_PARAM_INVALID161002input 与 angle 的 shape 不满足 broadcast 推导关系
ACLNN_ERR_PARAM_INVALID161002out 的 shape 与 input、angle 做 broadcast 后的 shape 不一致,或 out 的数据类型不是 COMPLEX64

aclnnPolar 参数说明

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

调用示例

下面给出完整的可编译调用示例(参考 aclnnPolar.md,编译与运行全过程请参考编译与运行样例):

#include <iostream> #include <vector> #include "acl/acl.h" #include "aclnnop/aclnn_polar.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手册 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> inputShape = {4, 2}; std::vector<int64_t> angleShape = {4, 2}; std::vector<int64_t> outShape = {4, 2}; void* inputDeviceAddr = nullptr; void* angleDeviceAddr = nullptr; void* outDeviceAddr = nullptr; aclTensor* input = nullptr; aclTensor* angle = nullptr; aclTensor* out = nullptr; std::vector<float> inputHostData = {1, 1, 1, 1, 2, 2, 2, 2}; std::vector<float> angleHostData = {0, 1.5707964f, 3.1415927f, -1.5707964f, 0, 1.5707964f, 3.1415927f, -1.5707964f}; // 创建input/angle aclTensor(FLOAT) ret = CreateAclTensor(inputHostData, inputShape, &inputDeviceAddr, aclDataType::ACL_FLOAT, &input); CHECK_RET(ret == ACL_SUCCESS, return ret); ret = CreateAclTensor(angleHostData, angleShape, &angleDeviceAddr, aclDataType::ACL_FLOAT, &angle); CHECK_RET(ret == ACL_SUCCESS, return ret); // 创建out aclTensor(COMPLEX64:每元素 = real/imag 两个 float) int64_t outNum = GetShapeSize(outShape); std::vector<float> outHostData(outNum * 2, 0); size_t outByte = static_cast<size_t>(outNum) * 2 * sizeof(float); ret = aclrtMalloc(&outDeviceAddr, outByte, ACL_MEM_MALLOC_HUGE_FIRST); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtMalloc failed. ERROR: %d\n", ret); return ret); ret = aclrtMemcpy(outDeviceAddr, outByte, outHostData.data(), outByte, ACL_MEMCPY_HOST_TO_DEVICE); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtMemcpy failed. ERROR: %d\n", ret); return ret); std::vector<int64_t> outStrides(outShape.size(), 1); for (int64_t i = outShape.size() - 2; i >= 0; i--) { outStrides[i] = outShape[i + 1] * outStrides[i + 1]; } out = aclCreateTensor(outShape.data(), outShape.size(), aclDataType::ACL_COMPLEX64, outStrides.data(), 0, aclFormat::ACL_FORMAT_ND, outShape.data(), outShape.size(), outDeviceAddr); // 3. 调用CANN算子库API uint64_t workspaceSize = 0; aclOpExecutor* executor; // 调用aclnnPolar第一段接口 ret = aclnnPolarGetWorkspaceSize(input, angle, out, &workspaceSize, &executor); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnPolarGetWorkspaceSize 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); } // 调用aclnnPolar第二段接口 ret = aclnnPolar(workspaceAddr, workspaceSize, executor, stream); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnPolar 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. 获取输出的值(complex64:每元素 real/imag 两个 float) std::vector<float> resultData(outNum * 2, 0); ret = aclrtMemcpy(resultData.data(), resultData.size() * sizeof(resultData[0]), outDeviceAddr, outByte, 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 < outNum; i++) { LOG_PRINT("out[%ld] = %f + %fi\n", i, resultData[2 * i], resultData[2 * i + 1]); } // 6. 释放aclTensor,需要根据具体API的接口定义修改 aclDestroyTensor(input); aclDestroyTensor(angle); aclDestroyTensor(out); // 7. 释放device资源 aclrtFree(inputDeviceAddr); aclrtFree(angleDeviceAddr); aclrtFree(outDeviceAddr); if (workspaceSize > 0) { aclrtFree(workspaceAddr); } aclrtDestroyStream(stream); aclrtResetDevice(deviceId); aclFinalize(); return 0; }

示例中的输入angle0π/2π-π/2等特殊角,方便验证输出:当input=1时,输出应为1+0i0+1i-1+0i0-1i等标准复数点;当input=2时模长相应放大。

底层实现原理:从 l0 拼接优化到单 Kernel 融合

Polar 算子在本仓库中并没有传统的 TBE 历史实现,而是以开源仓 l0 拼接参考实现为对齐基准与性能基线(详见 design.md)。参考实现的思路是串接约 6 个独立设备算子:Contiguous → Sin → Cos → Mul → Mul → Complex → ViewCopy,每个算子都有独立的 kernel 启动(dispatch)开销,中间结果必须经过 GM↔UB 搬运(≥10 次),在小 shape 场景下启动延迟远超计算时间。

本实现的优化目标是将整条计算链路融合为单个 Ascend C Kernel:一次启动、两次 GM↔UB 搬运,广播在单 Kernel 的 CopyIn 阶段内完成。整个算子分为 Host 侧(参数校验、广播 shape/stride 计算、Tiling 下发)与 Kernel 侧(Init + Process 三段式 CopyIn→Compute→CopyOut)。

Host 侧设计

InferShape / InferDataType:InferShape 按 NumPy 广播规则推导 out 的 shape(右对齐各轴取 max,对齐参考实现的OP_CHECK_BROADCAST_AND_INFER_SHAPE);InferDataType 将 out 恒置DT_COMPLEX64。对应实现见 polar_infershape.cpp。

分核策略:优先使用满核(平台 AIV 核数);小 case 收敛到少核(每核至少MIN_PER_CORE = 2048个元素,降低 launch/同步开销);强制偶数核(vector core 两两绑定,核数 > 2 时下取偶)。能均分则无大小核之分;不能均分时余块分给前几核(前rem个核各per+1个元素,其余per个)。对应实现见 polar_tiling.cpp 中的SplitCoresElementwiseClampWantEven

TilingData 字段(host 填写 / kernel 读取共用同一结构体,见 polar_tiling_data.h):

字段含义
totalLen输出元素总数
tileLen每 tile 元素数(2048)
bigCoreNum / bigCoreLen / smallCoreLen大核数量与大小核分块长度
coreNum实际启用核数(偶数)
tmpBufferSizeSin/Cos 显式 sharedTmpBuffer 字节数(由GetSinMaxMinTmpSize/GetCosMaxMinTmpSize计算)
inN / anNinput.numel / angle.numel
bcastMode0: same-shape;1: angle inner-broadcast

关于 Sin/Cos 临时 buffer 大小的计算,源码注释中有一个值得注意的细节:必须取GetCosMaxMinTmpSize返回的maxValue 而非 minValue。minValue 只是"勉强够用",Sin/Cos 会按小 buffer 分块多趟计算,导致大 same-shape case 性能暴跌(实测 16M same-shape 场景 cosMin 约 5800μs → cosMax 约 650μs,约 9 倍差距),而 UB 余量充足(约 24KB << 192KB),用 maxValue 可让 Sin/Cos 一次算完整 tile。

Kernel 侧设计

单 Kernel,Init + Process,全程 fp32 计算。核心流程:

  • Init:按blockIdx计算本核区间;BuildOffsetTable()用纯 int32 矢量指令构造交织静态偏移表off[j] = 4·(j>>1) + (j&1)·4T(一次性构造、全 tile 复用、无逐元素标量操作);广播操作数经LoadFull整块载入 UB。
  • CopyIn(FetchOperand):全形操作数走连续DataCopyPad快路径;广播操作数走矢量化 unravel——对 tile 输出线性 idx,逐输出轴用Muls(1/D)+Floor求商,再通过两次CompareScalar+Select精确 ±1 修正算坐标,累加偏移后Cast为 int32,最后Gather从 UB 整块操作数取数,全矢量无逐元素标量。
  • ComputeCos(angle)写入 packed 实部区、Sin(angle)写入 packed 虚部区,随后原地Mul × abs得到 real/imag,最后用Gather+ 静态偏移表交织为(r,i,r,i,…)
  • CopyOutDataCopyPad将 complex64 GM 写出(视作 2N 个 fp32 大块对齐)。

资源布局(BUFFER_NUM=2 double buffer,来自 polar.h 头部注释):

  • qIn(VECIN):2 buffer × (2×T fp32),合并存放[abs(0:n) | ang(0:n)]
  • qOut(VECOUT):2 buffer × (2×T fp32),complex64 交织结果
  • bufPacked(VECCALC):1 buffer × (2×T fp32),[cos | sin]中间结果
  • bufOff(VECCALC):1 buffer × (2×T u32),Gather 静态 offset 表
  • bufSinCosTmp(VECCALC):tilingData.tmpBufferSize,Sin/Cos 显式 sharedTmpBuffer

UB 总用量约 120KB / 192KB(910B),TPipe Tensor 数 7,恰好不超过单 kernel InitBuffer 张量上限 8(源码注释提醒:超限会直接 run failed 且无报错)。

复数交织方案选型

910B(dav_c220)的 vector 单元不支持 complex64,输出需将分离的 real/imag 交织为[r0,i0,r1,i1,…](complex64 视作 2N 个交织 fp32)。design.md 记录了候选方案评估:

方案结果原因
Copy<float,true>maskmask 同时作用于 src/dst,无法错位
TransposeNCHW2NHWC✅ 但慢受 16×16 分形约束,占绝大部分耗时
vintlv/Interleavedav_c310+ 独有,c220 不支持
stridedDataCopyPadVECOUT 32B 粒度,逐 4B 写出是 MTE 灾难
Gather + 静态偏移表✅ 选用纯向量指令,偏移表 Init 一次构造全 tile 复用

inner-broadcast 快速路径

针对angle在 trailing 维周期性重复(满足inN == totalLen && anN < inN && inN % anN == 0 && anN <= tileLen && anN % 8 == 0,即 32B 对齐)的场景,实现了bcastMode=1的 inner-broadcast 快速路径:

  • Host 侧按 K-block 分核(每核负载是 K 的整数倍,保证 tile 起点 period 对齐);
  • Kernel 侧在 Init 阶段对angle[0:K]预计算Cos/Sin,并一次性广播填满常驻cosTile[0:T] / sinTile[0:T]
  • 主循环只读 input,做Mul × 2后直接 Gather 交织写出,省去每 tile 的 angle 搬运与 Sin/Cos 重算

该资格判定在 aclnn_polar.cpp 的 L2 层与 polar_tiling.cpp 中严格保持一致:满足资格时 angle 保留原 shape[K]直通 kernel(省一次 BroadcastTo 的 HBM 写),否则由 L2 层BroadcastTo兜底。

L2 层调用链

从 aclnn_polar.cpp 可以看到完整的调用链:CheckParams(空指针/dtype/广播 shape 校验)→Contiguous × 2(解决 view/transpose 等非连续输入)→ 需要时BroadcastTol0op::Polar(调用本目录的 Ascend C kernel)→ViewCopy(把 kernel 输出拷贝到调用者提供的 out)。整个 L2 层通过 l0 拼接完成非连续与广播的前处理,而 kernel 侧假设 input/angle/out 同 shape 逐元素计算。

与参考实现的差异点

#参考 l0本 Ascend C原因
1约 6 个独立设备算子串接,各 HBM 往返 + dispatch单 kernel 融合省 ≥5 次中间 HBM 与多次 dispatch(核心场景快 1.78×)
2l0op::Complex构造复数Gather + 静态偏移表交织910B vector 不支持 complex64;规避 strided/Transpose 约束
3广播由 l0 Mul 内部处理host 算 stride + kernel 矢量化 unravel原生融合需自实现广播;全矢量避免标量
4Contiguous + ViewCopy 处理非连续框架传连续 GM 无需aclnn/测试输入连续,省拷贝

精度与性能验证

根据 design.md 与 自测报告 中的记录:

精度标准:满足 AscendOpTest 默认阈值——fp32→complex64 实/虚部各max_abs = 1e-4rate = 1e-4。由于 complex64 在 AscendOpTestaccuracy_config中没有内置默认,测试用例 JSON 中显式配置err_threshold=[1e-4,1e-4]compare_complex为实/虚部各自纯绝对误差判定。全程 fp32 计算不引入 Cast 中间精度损失,complex64 经 ReinterpretCast 视作交织 fp32 分量,精度等价 fp32。官方 AscendOpTest 实跑 6 个用例(同 shape 小 shape/16M + 广播低→高/标量/双向 + 高维非对齐)全部 PASS,含 16M[4096,4096]大 shape。

性能实测(vs 系统 l0 参考基线,官方 AscendOpTest msprof / 每调用设备时):

场景l0 基线本算子结论
小 [2,6,10]14.25 µs12.52 µs更快 ✓
16M [4096,4096](所有核)1665 µs937.98 µs快 1.78×,核心验收达标✓✓
广播 [4,1,8]×[4,5,8]15.17 µs14.92 µs更快 ✓

全场景优于 l0 参考,远超任务书"所有核参与场景 ≥ 参考 l0 的 95%"的验收要求。性能提升主要来自:kernel 启动从约 6 次降到 1 次、GM↔UB 搬运从 ≥10 次降到 2 次、中间结果留在片上、angle 一次读入后 cos+sin 在同一个 tile 内计算(参考实现无法复用)。

测试与验证路径

本目录提供了多层次验证手段,便于开发者复现与继续深入:

  • 独立 aclnn 调用测试:test_aclnn_polar.cpp 与参考实现 test_aclnn_polar_ref.cpp,覆盖精度与自带计时;性能采集可配合msprof --application="./test_aclnn_polar"
  • 测试步骤:测试步骤指导.md 与 自测报告.md。
  • AOT 用例与 golden 数据:polar_cases.json 与 polar_golden.py。
  • pybind 验证:tests/pybind 目录通过 pybind 方式将自定义算子接入 PyTorch 侧进行训练/推理验证。
  • Host 侧单测:test_polar_infershape.cpp。

贡献说明

贡献者贡献方贡献算子贡献时间贡献内容
开源社区贡献者开源社区Polar2026/06/30Polar 算子适配开源仓

该算子源自昇腾 CANN 训练营第二季社区任务(序号 04-5 Polar),基于 Ascend C 实现aclnnPolar,对齐开源仓 l0 拼接参考实现并新增广播支持,验收通过后贡献至cann/ops-math。克隆仓库后可参考 QUICKSTART 与 CONTRIBUTING 了解算子构建与合入流程。

  • 算子库
  • 人工智能
  • CANN

【免费下载链接】ops-math

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

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

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

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

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

立即咨询