CANN ops-math 中 Cross(linalg.cross)向量叉积算子的原理与 aclnn 调用实战
【免费下载链接】ops-math本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-math
导读
本文围绕 CANN ops-math 仓库中experimental/math/cross目录下的 Cross 算子展开,该算子实现数学上的向量叉积(linear_cross)运算,支持与 NumPy/PyTorchlinalg.cross对齐的语义:沿指定维度对两个张量逐元素求叉积,并支持广播。读完本文,你将掌握 Cross 算子的功能定义、参数与约束、基于两段式 aclnn 接口(aclnnLinalgCross)的完整调用方法,以及从算子定义、shape 推导、tiling 到 AscendC kernel 的源码级实现原理。
功能说明:线性叉积(linear_cross)算子
Cross 算子的功能是:对输入 Tensorself与other沿指定维度执行向量叉积(linear_cross)运算。向量叉积也称向量积、外积,其结果是一个与原向量所在平面垂直的新向量,是三维几何与线性代数中的基础运算。
其计算公式采用三阶行列式展开(单位向量 i、j、k 对应叉积结果的三轴分量):
$$ out = self\times other = \begin{vmatrix}i&j&k\x_1&y_1&z_1\x_2&y_2&z_2\end{vmatrix} = (y_1z_2-y_2z_1)i-(x_1z_2-x_2z_1)j+(x_1y_2-x_2y_1)k $$
其中self = (x1, y1, z1)、other = (x2, y2, z2)分别为进行叉积运算的两个三维向量,输出out的三个分量分别对应 i、j、k 的系数。
产品支持情况
Cross 算子目前支持的硬件平台如下:
| 产品 | 是否支持 |
|---|---|
| Atlas A2 训练系列产品 / Atlas A2 推理系列产品 | √ |
支持的数据类型
在 Atlas A2 训练/推理系列产品上,Cross 算子支持的数据类型包括:INT8、INT16、INT32、INT64、UINT8、FLOAT16、BFLOAT16、FLOAT、FLOAT64、COMPLEX64、COMPLEX128。
参数说明
Cross 算子的核心参数如下表所示:
| 参数名 | 输入/输出/属性 | 描述 | 数据类型 | 数据格式 |
|---|---|---|---|---|
| self | 输入 | 计算公式中的输入 self。 | FLOAT16、FLOAT、INT32、INT8、UINT8、INT16、DOUBLE、INT64、UINT16、UINT32、UINT64、COMPLEX64、COMPLEX128 | ND |
| other | 输入 | 计算公式中的输入 other。 | FLOAT16、FLOAT、INT32、INT8、UINT8、INT16、DOUBLE、INT64、UINT16、UINT32、UINT64、COMPLEX64、COMPLEX128 | ND |
| out | 输出 | 计算公式中的输出 out。 | FLOAT16、FLOAT、INT32、INT8、UINT8、INT16、DOUBLE、INT64、UINT16、UINT32、UINT64、COMPLEX64、COMPLEX128 | ND |
说明:上表中的数据类型为算子整体支持范围;针对 Atlas A2 训练/推理系列产品,实际支持范围以 aclnnLinalgCross 接口文档 中列出的
INT8、INT16、INT32、INT64、UINT8、FLOAT16、BFLOAT16、FLOAT、FLOAT64、COMPLEX64、COMPLEX128为准。此外,不同调用路径(AI Core 内核路径与 AICPU 路径)的支持范围存在差异,详见后文源码剖析。
约束说明
使用 Cross 算子时需满足以下约束:
- 叉积维度大小必须为 3:指定进行 cross 运算的维度(
dim参数指定的轴)的 size 必须为 3。这是因为叉积只定义在三维向量上。 - 输入需满足 broadcast 关系:两个输入 tensor 的 shape 需要兼容,能够进行广播运算,具体广播规则参见 broadcast_relationship.md。
- 数据类型需保持一致:
self、other、out三个 tensor 的数据类型必须完全一致。
aclnn 接口调用说明:两段式接口
Cross 算子对外提供 aclnn 层接口aclnnLinalgCross,采用 CANN 算子标准的两段式接口调用模式(参见 two_phase_api.md):
- 先调用
aclnnLinalgCrossGetWorkspaceSize接口,获取计算所需 workspace 大小以及包含了算子计算流程的执行器; - 再调用
aclnnLinalgCross接口执行实际计算。
调用示例位于 examples/test_aclnn_linalg_cross.cpp,接口详细说明见 aclnnLinalgCross.md。
函数原型
aclnnStatus aclnnLinalgCrossGetWorkspaceSize( const aclTensor* self, const aclTensor* other, int64_t dim, aclTensor* out, uint64_t* workspaceSize, aclOpExecutor** executor)aclnnStatus aclnnLinalgCross( void* workspace, uint64_t workspaceSize, aclOpExecutor* executor, aclrtStream stream)aclnnLinalgCrossGetWorkspaceSize 参数说明
| 参数名 | 输入/输出 | 描述 | 使用说明 | 数据类型 | 数据格式 | 维度 | 非连续 Tensor |
|---|---|---|---|---|---|---|---|
| self(aclTensor*) | 输入 | 公式中的 self。 | 数据类型与 other 和 out 一致;需与 other 满足 broadcast 关系,且 shape 在 dim 指定的轴广播后的值为 3。 | INT8、INT16、INT32、INT64、UINT8、FLOAT16、BFLOAT16、FLOAT、FLOAT64、COMPLEX64、COMPLEX128 | ND | 0-8 | √ |
| other(aclTensor*) | 输入 | 公式中的 other。 | 数据类型与 self 和 out 一致;需与 self 满足 broadcast 关系。 | INT8、INT16、INT32、INT64、UINT8、FLOAT16、BFLOAT16、FLOAT、FLOAT64、COMPLEX64、COMPLEX128 | ND | 0-8 | √ |
| dim(int64_t) | 输入 | 指定 self 进行 linear_cross 的轴。 | 若不指定则默认为 -1,取值范围为[-self维度数量, self维度数量-1]。 | - | - | - | - |
| out(aclTensor*) | 输出 | 公式中的 out。 | 数据类型与 self 和 other 一致;shape 需要与 self 和 other broadcast 后的 shape 一致。 | INT8、INT16、INT32、INT64、UINT8、FLOAT16、BFLOAT16、FLOAT、FLOAT64、COMPLEX64、COMPLEX128 | ND | - | √ |
| workspaceSize(uint64_t*) | 输出 | 返回需要在 Device 侧申请的 workspace 大小。 | - | - | - | - | - |
| executor(aclOpExecutor**) | 输出 | 返回 op 执行器,包含了算子计算流程。 | - | - | - | - | - |
返回值:aclnnStatus状态码,具体参见 aclnn 返回码。
第一段接口完成入参校验,出现如下场景时报错:
| 返回值 | 错误码 | 描述 |
|---|---|---|
| ACLNN_ERR_PARAM_NULLPTR | 161001 | 传入的 self 或 out 是空指针。 |
| ACLNN_ERR_PARAM_INVALID | 161002 | self、other、out 的数据类型不一致或数据格式不在支持的范围之内。 |
| ACLNN_ERR_PARAM_INVALID | 161002 | self 和 other 的维度大于 8。 |
| ACLNN_ERR_PARAM_INVALID | 161002 | self 和 other 不符合 broadcast 关系。 |
| ACLNN_ERR_PARAM_INVALID | 161002 | self 和 other broadcast 后的 shape 与 out 不一致。 |
| ACLNN_ERR_PARAM_INVALID | 161002 | self 在对应 dim 维度上的 shape 不为 3。 |
| ACLNN_ERR_PARAM_INVALID | 161002 | dim 的值不在[-self的维度数量, self的维度数量-1]范围内。 |
aclnnLinalgCross 参数说明
| 参数名 | 输入/输出 | 描述 |
|---|---|---|
| workspace | 输入 | 在 Device 侧申请的 workspace 内存地址。 |
| workspaceSize | 输入 | 在 Device 侧申请的 workspace 大小,由第一段接口aclnnLinalgCrossGetWorkspaceSize获取。 |
| executor | 输入 | op 执行器,包含了算子计算流程。 |
| stream | 输入 | 指定执行任务的 Stream。 |
返回值:aclnnStatus状态码,具体参见 aclnn 返回码。
确定性说明
aclnnLinalgCross默认为确定性实现,同一输入在多次运行中计算结果可复现。
完整调用示例
下面给出一个可编译运行的完整示例(基于 examples/test_aclnn_linalg_cross.cpp),以FLOAT类型、shape 为{3, 3}的输入,沿dim = 1计算叉积。具体编译与运行流程请参考 编译与运行样例。
#include <iostream> #include <vector> #include "acl/acl.h" #include "aclnnop/aclnn_linalg_cross.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; } aclError InitAcl(int32_t deviceId, aclrtStream* stream) { auto ret = Init(deviceId, stream); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("Init acl failed. ERROR: %d\n", ret); return ret); return ACL_SUCCESS; } aclError CreateInputs( std::vector<int64_t>& selfShape, std::vector<int64_t>& otherShape, std::vector<int64_t>& outShape, void** selfDeviceAddr, void** otherDeviceAddr, void** outDeviceAddr, aclTensor** self, aclTensor** other, aclTensor** out) { std::vector<float> selfHostData = {0, 1, 2, 3, 4, 5, 6, 7, 8}; std::vector<float> otherHostData = {1, 1, 1, 2, 2, 2, 3, 3, 3}; std::vector<float> outHostData = {0, 0, 0, 0, 0, 0, 0, 0, 0}; // 创建 self aclTensor auto ret = CreateAclTensor(selfHostData, selfShape, selfDeviceAddr, aclDataType::ACL_FLOAT, self); CHECK_RET(ret == ACL_SUCCESS, return ret); // 创建 other aclTensor ret = CreateAclTensor(otherHostData, otherShape, otherDeviceAddr, aclDataType::ACL_FLOAT, other); 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); return ACL_SUCCESS; } aclError ExecOpApi( aclTensor* self, aclTensor* other, aclTensor* out, int64_t dim, void** workspaceAddrOut, uint64_t& workspaceSize, void* outDeviceAddr, std::vector<int64_t>& outShape, aclrtStream stream) { aclOpExecutor* executor; // 调用 aclnnLinalgCross 第一段接口 auto ret = aclnnLinalgCrossGetWorkspaceSize(self, other, dim, out, &workspaceSize, &executor); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnLinalgCrossGetWorkspaceSize 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); } *workspaceAddrOut = workspaceAddr; // 调用 aclnnLinalgCross 第二段接口 ret = aclnnLinalgCross(workspaceAddr, workspaceSize, executor, stream); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnLinalgCross failed. ERROR: %d\n", ret); return ret); // 同步 ret = aclrtSynchronizeStream(stream); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtSynchronizeStream failed. ERROR: %d\n", ret); return ret); // 从 device 拷贝结果到 host auto size = GetShapeSize(outShape); std::vector<float> resultData(size, 0); ret = aclrtMemcpy( resultData.data(), resultData.size() * sizeof(resultData[0]), outDeviceAddr, size * sizeof(resultData[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: %lf\n", i, resultData[i]); } return ACL_SUCCESS; } int main() { // 1. device/stream 初始化 int32_t deviceId = 0; aclrtStream stream; CHECK_RET(InitAcl(deviceId, &stream) == ACL_SUCCESS, return -1); // 2. 构造输入与输出 std::vector<int64_t> selfShape = {3, 3}; std::vector<int64_t> otherShape = {3, 3}; std::vector<int64_t> outShape = {3, 3}; void* selfDeviceAddr = nullptr; void* otherDeviceAddr = nullptr; void* outDeviceAddr = nullptr; aclTensor* self = nullptr; aclTensor* other = nullptr; aclTensor* out = nullptr; aclError ret = CreateInputs( selfShape, otherShape, outShape, &selfDeviceAddr, &otherDeviceAddr, &outDeviceAddr, &self, &other, &out); CHECK_RET(ret == ACL_SUCCESS, return ret); // 3. 调用 CANN 算子 API int64_t dim = 1; uint64_t workspaceSize = 0; void* workspaceAddr = nullptr; ret = ExecOpApi(self, other, out, dim, &workspaceAddr, workspaceSize, outDeviceAddr, outShape, stream); CHECK_RET(ret == ACL_SUCCESS, return ret); // 6. 释放 aclTensor aclDestroyTensor(self); aclDestroyTensor(other); aclDestroyTensor(out); // 7. 释放 device 资源 aclrtFree(selfDeviceAddr); aclrtFree(otherDeviceAddr); aclrtFree(outDeviceAddr); if (workspaceSize > 0) { aclrtFree(workspaceAddr); } aclrtDestroyStream(stream); aclrtResetDevice(deviceId); aclFinalize(); return 0; }示例程序的执行流程可以概括为以下步骤:
- 资源初始化:
aclInit→aclrtSetDevice→aclrtCreateStream,建立与设备的会话和任务流; - 构造 aclTensor:通过
aclrtMalloc申请 Device 侧内存、aclrtMemcpy将 Host 数据拷贝到 Device,再由aclCreateTensor携带 shape/strides/dtype/format 创建三个张量描述符(self、other、out); - 两段式调用:先调用
aclnnLinalgCrossGetWorkspaceSize获取 workspace 大小并申请内存,再调用aclnnLinalgCross提交计算任务; - 结果回拷与校验:
aclrtSynchronizeStream等待任务完成,通过ACL_MEMCPY_DEVICE_TO_HOST将结果拷贝回 Host 并打印; - 资源释放:依次释放 aclTensor、Device 内存、workspace、Stream,最后
aclrtResetDevice与aclFinalize。
以示例输入为例,self = [0 1 2; 3 4 5; 6 7 8],other = [1 1 1; 2 2 2; 3 3 3],沿dim = 1对每一行向量求叉积,可验证第一行(0,1,2) × (1,1,1) = (1*1-2*1, 2*1-0*1, 0*1-1*1) = (-1, 2, -1),与公式一一对应。
源码实现剖析
Cross 算子完整的实现链路覆盖了 L0 API 层、Host 侧(算子定义 / InferShape / Tiling)与 AI Core Kernel 三层,下面逐一展开。
L0 API 层的多路径调度
op_api/cross.cpp 实现了 L0 层Cross接口,其核心逻辑是根据芯片型号与输入数据类型动态选择执行路径:
- AI Core 路径(Cross):当数据类型属于
AICORE_DTYPE_SUPPORT_LIST(DT_FLOAT、DT_FLOAT16、DT_INT8、DT_INT16、DT_INT32、DT_UINT8)时,通过ADD_TO_LAUNCHER_LIST_AICORE宏将算子加入 AI Core 任务队列,属性dim随任务下发; - AI Core 路径(CrossV2):在 Atlas A2 系列(
NpuArch::DAV_2201、DAV_3510)上,支持DT_FLOAT、DT_FLOAT16、DT_INT16、DT_INT32、DT_BF16类型走 CrossV2 内核; - AICPU 路径:其余数据类型(如 DOUBLE、INT64、COMPLEX 等)回退到 AICPU 内核执行,保证接口全类型可用。
同时该文件通过OP_TYPE_REGISTER(Cross)与OP_TYPE_REGISTER(CrossV2)完成算子类型注册,输出张量由executor->AllocTensor依据 self 的 shape 与 dtype 自动分配。
Host 侧:算子定义、shape 推导与 tiling
算子定义位于 op_host/cross_def.cpp,声明了两个输入x1、x2与一个输出y,数据类型支持DT_FLOAT、DT_INT32、DT_INT8、DT_FLOAT16、DT_UINT8、DT_INT16,格式均为FORMAT_ND,并注册ascend910b的 AI Core 配置。
shape 推导位于 op_host/cross_infershape.cpp,实现非常简洁:由于叉积不改变张量形状(仅逐元素重算),输出 shape 直接继承输入x1的 shape(*yShape = *xShape)。值得注意的是,这里的 InferShape 以x1为准,而 API 层的校验已保证 self 与 other 广播后的 shape 与 out 一致。
tiling 计算位于 op_host/cross_tiling.cpp,负责把任务切分到多个 AI Core 并确定每次搬运的数据量:
- 平台信息:通过
PlatformAscendC获取 UB 大小与可用 AI Core 数量; - 关键切分参数:
totalIdx(输入总元素数)、intervalNum(叉积维之外的前缀切片数,即totalIdx / (dim 及之前的维度乘积))、loopTimes(沿叉积维切出的三元组组数,即totalIdx / intervalNum / 3); - 分组模式(
IsGroupMode,即intervalNum == 1):当数据全部落在叉积维时,把多个三元组整体作为一个 tile 搬运,并针对 FLOAT16 增加了按核数限制的细粒度切分; - 非分组模式:按
tileDataNum切分 interval,并依据不同 dtype(FLOAT16/FLOAT/INT32/INT8 等)选择不同的 UB buffer 数量(UB_DATA_NUM_FP16=30、UB_DATA_NUM_FP32=19、UB_DATA_NUM_INT8=18)以适配数据宽度; - 每个 dtype 对应不同的
TilingKey(ELEMENTWISE_TPL_SCH_MODE_0到MODE_5),kernel 据此选择实例化的模板分支; - workspace 固定申请
16MB(WS_SYS_SIZE),块对齐粒度32B(BLOCK_SIZE); - 空输入(
totalIdx <= 0)时直接输出空 tiling 数据并设置blockDim = 1,避免无效计算。
AI Core Kernel:AscendC 实现与多核流水
Kernel 实现位于 op_kernel/cross.h,模板类NsCross::Cross<T>按T实例化,不同 dtype 走不同计算分支:
- 常规分支(float/int32_t/int16_t):利用向量指令
AscendC::Mul与AscendC::Sub将叉积展开为三组"乘-减"流水,每组之间通过PipeBarrier<PIPE_V>保证向量计算单元的依赖顺序,计算结果依次写入 z0/z1/z2 三个输出队列; - half(FLOAT16)分支:由于低精度乘法精度有限,先将数据
Cast到 FP32 进行计算,再以CAST_ROUND舍入回 FLOAT16 输出,中间结果使用 8 个独立的 FP32 临时 buffer(tmpBuf0~7)承载; - int8_t/uint8_t 分支:先将元素提升到
int32_t做乘减,再截断回原类型写回,避免中间溢出; - 分组模式(GroupMode):按三元组逐个取数、计算、写回,逻辑与上述标量计算一致。
数据流采用标准的CopyIn → Compute → CopyOut三段流水,队列深度QUEUE_DEPTH = 1、BUFFER_NUM = 2实现双缓冲;Process()中通过GetBlockNum()/GetBlockIdx()将总 tile 数均匀(含余数分配)切分到多个 AI Core 并行执行。搬运时以 32 字节对齐块为主路径DataCopy,尾部不足 32 字节的片段使用DataCopyPad补齐,确保内存访问对齐。
测试验证
算子配套的 Kernel 级单测位于 tests/ut/op_kernel/test_cross.cpp,测试框架基于gtest与tikicpulib(TIKI CPU 模拟器)。测试用例通过gen_data.py生成输入数据(如 shape(2, 3)的 FLOAT16 用例),读取二进制输入后分配 GM 内存、装载CrossTilingData,以单核(blockDim = 1)启动 kernel 并与期望输出对比;workspace 大小与 tiling 中的16MB保持一致。数据生成脚本位于 tests/ut/op_kernel/cross_data/gen_data.py,对比脚本见 compare_data.py。此外,tests/ut/op_host/test_cross_tiling.cpp 覆盖了 tiling 逻辑的单元测试。
使用建议与注意事项
- dim 的选择:dim 默认 -1(最后一维),指定其他轴时需保证该轴 size 为 3,且取值在
[-rank(self), rank(self)-1]之间,否则第一段接口会返回ACLNN_ERR_PARAM_INVALID(161002); - 广播场景:当 self 与 other shape 不同但满足广播关系时,结果 shape 为广播后的 shape,out 需按该 shape 预先分配;
- 数据类型一致性:三个张量 dtype 必须完全一致,建议统一使用 FLOAT/FLOAT16 等 AI Core 路径支持的类型以获得最优性能;DOUBLE、INT64、COMPLEX 等类型会走 AICPU 回退路径;
- 确定性:
aclnnLinalgCross为确定性实现,无需额外配置即可获得可复现结果。
贡献说明
Cross 算子在 2026 年 4 月由个人开发者 hth810 贡献并适配进开源仓库,贡献内容包括算子完整实现与测试用例。当前算子已纳入experimental/math/cross目录,读者可结合上述源码路径深入研读与二次开发。
【免费下载链接】ops-math本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-math
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考