CANN ops-math 中 Cross(linalg.cross)向量叉积算子的原理与 aclnn 调用实战
2026/9/19 12:50:38 网站建设 项目流程

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 算子的功能是:对输入 Tensorselfother沿指定维度执行向量叉积(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 算子支持的数据类型包括:INT8INT16INT32INT64UINT8FLOAT16BFLOAT16FLOATFLOAT64COMPLEX64COMPLEX128

参数说明

Cross 算子的核心参数如下表所示:

参数名输入/输出/属性描述数据类型数据格式
self输入计算公式中的输入 self。FLOAT16、FLOAT、INT32、INT8、UINT8、INT16、DOUBLE、INT64、UINT16、UINT32、UINT64、COMPLEX64、COMPLEX128ND
other输入计算公式中的输入 other。FLOAT16、FLOAT、INT32、INT8、UINT8、INT16、DOUBLE、INT64、UINT16、UINT32、UINT64、COMPLEX64、COMPLEX128ND
out输出计算公式中的输出 out。FLOAT16、FLOAT、INT32、INT8、UINT8、INT16、DOUBLE、INT64、UINT16、UINT32、UINT64、COMPLEX64、COMPLEX128ND

说明:上表中的数据类型为算子整体支持范围;针对 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。
  • 数据类型需保持一致selfotherout三个 tensor 的数据类型必须完全一致。

aclnn 接口调用说明:两段式接口

Cross 算子对外提供 aclnn 层接口aclnnLinalgCross,采用 CANN 算子标准的两段式接口调用模式(参见 two_phase_api.md):

  1. 先调用aclnnLinalgCrossGetWorkspaceSize接口,获取计算所需 workspace 大小以及包含了算子计算流程的执行器;
  2. 再调用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、COMPLEX128ND0-8
other(aclTensor*)输入公式中的 other。数据类型与 self 和 out 一致;需与 self 满足 broadcast 关系。INT8、INT16、INT32、INT64、UINT8、FLOAT16、BFLOAT16、FLOAT、FLOAT64、COMPLEX64、COMPLEX128ND0-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、COMPLEX128ND-
workspaceSize(uint64_t*)输出返回需要在 Device 侧申请的 workspace 大小。-----
executor(aclOpExecutor**)输出返回 op 执行器,包含了算子计算流程。-----

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

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

返回值错误码描述
ACLNN_ERR_PARAM_NULLPTR161001传入的 self 或 out 是空指针。
ACLNN_ERR_PARAM_INVALID161002self、other、out 的数据类型不一致或数据格式不在支持的范围之内。
ACLNN_ERR_PARAM_INVALID161002self 和 other 的维度大于 8。
ACLNN_ERR_PARAM_INVALID161002self 和 other 不符合 broadcast 关系。
ACLNN_ERR_PARAM_INVALID161002self 和 other broadcast 后的 shape 与 out 不一致。
ACLNN_ERR_PARAM_INVALID161002self 在对应 dim 维度上的 shape 不为 3。
ACLNN_ERR_PARAM_INVALID161002dim 的值不在[-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; }

示例程序的执行流程可以概括为以下步骤:

  1. 资源初始化aclInitaclrtSetDeviceaclrtCreateStream,建立与设备的会话和任务流;
  2. 构造 aclTensor:通过aclrtMalloc申请 Device 侧内存、aclrtMemcpy将 Host 数据拷贝到 Device,再由aclCreateTensor携带 shape/strides/dtype/format 创建三个张量描述符(self、other、out);
  3. 两段式调用:先调用aclnnLinalgCrossGetWorkspaceSize获取 workspace 大小并申请内存,再调用aclnnLinalgCross提交计算任务;
  4. 结果回拷与校验aclrtSynchronizeStream等待任务完成,通过ACL_MEMCPY_DEVICE_TO_HOST将结果拷贝回 Host 并打印;
  5. 资源释放:依次释放 aclTensor、Device 内存、workspace、Stream,最后aclrtResetDeviceaclFinalize

以示例输入为例,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_LISTDT_FLOATDT_FLOAT16DT_INT8DT_INT16DT_INT32DT_UINT8)时,通过ADD_TO_LAUNCHER_LIST_AICORE宏将算子加入 AI Core 任务队列,属性dim随任务下发;
  • AI Core 路径(CrossV2):在 Atlas A2 系列(NpuArch::DAV_2201DAV_3510)上,支持DT_FLOATDT_FLOAT16DT_INT16DT_INT32DT_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,声明了两个输入x1x2与一个输出y,数据类型支持DT_FLOATDT_INT32DT_INT8DT_FLOAT16DT_UINT8DT_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=30UB_DATA_NUM_FP32=19UB_DATA_NUM_INT8=18)以适配数据宽度;
  • 每个 dtype 对应不同的TilingKeyELEMENTWISE_TPL_SCH_MODE_0MODE_5),kernel 据此选择实例化的模板分支;
  • workspace 固定申请16MBWS_SYS_SIZE),块对齐粒度32BBLOCK_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::MulAscendC::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 = 1BUFFER_NUM = 2实现双缓冲;Process()中通过GetBlockNum()/GetBlockIdx()将总 tile 数均匀(含余数分配)切分到多个 AI Core 并行执行。搬运时以 32 字节对齐块为主路径DataCopy,尾部不足 32 字节的片段使用DataCopyPad补齐,确保内存访问对齐。

测试验证

算子配套的 Kernel 级单测位于 tests/ut/op_kernel/test_cross.cpp,测试框架基于gtesttikicpulib(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),仅供参考

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

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

立即咨询