news 2026/9/21 21:48:56

CANN ops-math 算子库 aclnnRandperm 接口实战指南:从随机排列原理到两段式 API 调用

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
CANN ops-math 算子库 aclnnRandperm 接口实战指南:从随机排列原理到两段式 API 调用

CANN ops-math 算子库 aclnnRandperm 接口实战指南:从随机排列原理到两段式 API 调用

【免费下载链接】ops-math本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-math

aclnnRandperm 是 CANN ops-math 数学算子库中 StatelessRandperm 算子对外暴露的 aclnn 接口,用于在 NPU 上生成从 0 到 n-1 的整数随机排列(无状态随机排列)。本文以 aclnnRandperm.md 为骨架,结合仓库中该算子的 op_api、op_host、op_kernel 与示例代码,系统讲解函数原型、两段式调用流程、参数约束、底层实现原理及可编译运行的完整样例,帮助读者在 Atlas 训练/推理产品上快速落地该算子。

功能概述与产品支持情况

StatelessRandperm 算子的功能是:返回从 0 到 n-1 的整数随机排列。所谓"无状态(stateless)",指的是随机序列完全由用户传入的 seed 与 offset 决定,不依赖运行时的全局随机状态,因此同一组 (seed, offset) 输入总能得到相同的排列结果,便于复现实验与单元测试。

根据 aclnnRandperm.md 与算子目录下 README.md 的说明,各产品系列的支持情况如下:

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

注:算子目录内 README.md 的产品表格与接口文档存在差异(README 中标注 Atlas 200I/500 A2、Atlas 推理系列为支持),二者以接口文档 aclnnRandperm.md 为准。实际运行时请以所安装 CANN 版本的配套说明为准。

两段式接口与函数原型

在 CANN 的 aclnn 算子接口体系中,每个算子采用两段式接口调用模式(详见 两段式接口说明):

  1. 第一段aclnnRandpermGetWorkspaceSize:完成入参校验,计算算子执行所需的 workspace 大小,并创建包含算子计算流程的执行器aclOpExecutor
  2. 第二段aclnnRandperm:传入 Device 侧申请的 workspace 内存与执行器,在指定 Stream 上真正执行计算。

两个接口的函数原型如下(来自 aclnnRandperm.md):

aclnnStatus aclnnRandpermGetWorkspaceSize( int64_t n, int64_t seed, int64_t offset, aclTensor* out, uint64_t* workspaceSize, aclOpExecutor** executor)
aclnnStatus aclnnRandperm( void* workspace, uint64_t workspaceSize, aclOpExecutor* executor, aclrtStream stream)

在仓库源码 op_api/aclnn_randperm.cpp 中可以看到两段接口的实现骨架:第一段接口先通过OP_CHECK_COMM_INPUT校验输出参数指针,随后调用CheckParams完成参数合法性检查,再创建OpExecutor并利用l0op::StatelessRandperm构建计算图(含ViewCopy节点用于处理输出可能为非连续 tensor 的情况),最后通过uniqueExecutor->GetWorkspaceSize()返回 workspace 大小;第二段接口则直接调用框架能力CommonOpExecutorRun完成计算。

aclnnRandpermGetWorkspaceSize 参数详解

第一段接口的完整参数说明如下(摘自 aclnnRandperm.md):

参数名输入/输出描述数据类型数据格式维度(shape)非连续tensor
n输入取随机数的上界INT64---
seed输入随机数生成器的种子,影响生成的随机数序列INT64---
offset输入随机数生成器的偏移量,影响生成的随机数序列的位置。设置偏移量后,生成的随机数序列会从指定位置开始INT64---
out输出输出 tensorINT64、INT32、INT16、UINT8、INT8、FLOAT、FLOAT16、DOUBLE、BFLOAT16ND为 [n]
workspaceSize输出返回需要在 Device 侧申请的 workspace 大小----
executor输出返回 op 执行器,包含了算子计算流程----

参数要点说明:

  • n:生成排列的长度(排列元素取自 0 到 n-1),为标量 INT64 值。当 n 等于 0 时,第一段接口直接返回空 tensor(见 op_api/aclnn_randperm.cpp 中if (n == 0)分支),此时 workspaceSize 为 0。
  • seed / offset:均为 INT64 标量,共同决定随机数序列。在 kernel 层,二者被用于初始化 Philox 伪随机数生成器的 key 与 counter(见下文"底层实现原理"),其中 offset 用于跳过随机数序列中的前若干位。
  • out:输出 tensor,shape 必须为[n],支持多种数据类型(详见"数据类型约束"),且支持非连续 tensor——这是通过第一段接口中附加的l0op::ViewCopy节点实现的,计算中间结果先写入连续内存,再按 out 的实际 strides 拷贝到目标 tensor。

第一段接口的返回值与错误码

aclnnStatus返回状态码的完整定义参见 aclnn返回码。第一段接口完成入参校验,出现如下场景时报错(摘自原文档):

返回码错误码描述
ACLNN_ERR_PARAM_NULLPTR161001传入的 out 是空指针
ACLNN_ERR_PARAM_INVALID161002传入的 n 小于 0
ACLNN_ERR_PARAM_INVALID161002out 的 shape 不为 n

上述校验逻辑在源码 op_api/aclnn_randperm.cpp 的CheckParams中逐一落实:CheckNotNull检查 out 空指针;CheckShapeValid将 out 的 shape 与[n]比对;n >= 0校验 n 非负;CheckDtypeValid则根据当前 NPU 架构校验 out 的数据类型。

数据类型约束

  • 通用支持类型:INT64、INT32、INT16、UINT8、INT8、FLOAT、FLOAT16、DOUBLE;
  • Atlas 训练系列产品(Ascend 910)不支持 BFLOAT16
  • BFLOAT16 仅在支持它的架构(如 Atlas A2/A3 训练与推理系列、Ascend 950 系列)上可用。

该约束与源码中的两张支持列表一致:DTYPE_SUPPORT_LIST_DEFAULT不含 BF16,而DTYPE_SUPPORT_LIST_2201(对应 DAV_2201/DAV_3510 架构)在末尾追加了DT_BF16,见 op_api/aclnn_randperm.cpp。

aclnnRandperm 参数详解

第二段接口完成真正的计算任务,参数说明如下(摘自原文档):

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

使用要点:

  • workspaceSize为 0 时无需申请 workspace,可传入空指针;大于 0 时需通过aclrtMalloc在 Device 侧分配对应大小的内存(建议使用ACL_MEM_MALLOC_HUGE_FIRST标志),并在计算结束后释放;
  • executor必须来自第一段接口的输出,二者严格配对使用;
  • 第二段接口的返回值同样是aclnnStatus,具体参见 aclnn返回码。

约束说明

  • 确定性计算:aclnnRandperm 默认采用确定性实现,即相同 (seed, offset) 输入在多核/多次运行下产生一致的排列结果。这一点也与算子名中的"stateless"相呼应,相关背景可参考 确定性计算。
  • Ascend 950PR/Ascend 950DT 专属约束
    • INT64、INT32、INT16、UINT8、INT8、FLOAT、FLOAT16、BFLOAT16 类型下,n 不得超过 int32 的最大值(即 n ≤ 2147483647),因为 kernel 内部使用 32 位索引对随机数缓冲区寻址(对应canUse32bitIndexing的判断逻辑,见 op_kernel/arch35/stateless_randperm.h);
    • DOUBLE 类型下,当 n 大于 268000000 时有运行超时风险,可通过aclrtSetOpExecuteTimeOut设置算子执行超时时间。

源码级原理剖析

算子定义与 Shape 推导

在算子宿主侧,op_host/stateless_randperm_def.cpp 通过OpDef注册了 StatelessRandperm 算子:三个必选输入 n、seed、offset(均为 INT64、ND 格式、ValueDepend(OPTIONAL)表示其值参与 shape 推导),一个输出 y,以及两个可选属性layout(默认 0)和dtype(默认ge::DT_INT64,用于指定输出数据类型)。

op_host/stateless_randperm_infershape.cpp 实现了 shape 推导:由于 n 是标量输入而非 shape 描述,推导逻辑通过InputsDataDependency({0})声明对输入 0(即 n)的数据依赖,读取 n 的值后将输出 shape 的第 0 维设为 n,从而保证输出 shape 恒为[n]

计算内核:Philox 随机数 + 排序 + Fisher-Yates 洗牌

在 Ascend 950 系列(arch35)上,算子内核实现了"伪随机数生成 → 排序 → Fisher-Yates 洗牌"三阶段算法,入口为 op_kernel/stateless_randperm_apt.cpp,核心流程见 op_kernel/arch35/stateless_randperm.h 的Process()

  1. 随机数生成(Philox)Philox内核函数用 Philox 4×32-10 算法为每个元素生成一个随机位串(randomBits位),写入randWorkSpace_缓冲区。Philox 算法的完整实现(ComputeSingleRoundRaiseKeyPhiloxRandomSkipAheadRandInitRand4等)位于 op_kernel/arch35/stateless_randperm_random.h,其中RandInit通过SkipAhead_Sequence(subsequence)SkipAhead(offset)把 seed 子序列号与用户 offset 编码进 counter,实现"无状态 + 可复现";
  2. 排序(Sort)Sort模板对随机位串排序,使得位串相同的元素聚集为连续的"岛屿(island)",同时维护元素原始索引indexWorkSpace_。由于相同位串被聚到一起,洗牌只需要在 islands 内部进行,大幅减少了跨 core 的同步开销;
  3. Fisher-Yates 洗牌(FindAndFisherYares):对每个 island 内部的索引执行 Fisher-Yates 原地洗牌(从islandSize - 1递减到 1,逐个与[0, j]内的随机位置交换),使用的随机数依旧来自 Philox 序列,确保随机性完全由 (seed, offset) 决定;
  4. 拷贝输出(CopyData):最后按洗牌后的索引顺序把 0..n-1 写入输出outGm_,并完成类型转换(由模板参数Ty指定,对应 out 的数据类型)。

AICORE / AICPU 双路径分派

在 op_api/stateless_randperm.cpp 中,l0op::StatelessRandperm根据当前 NPU 架构与输出数据类型自动选择执行路径:当架构为DAV_3510(Ascend 950)且输出 dtype 在 AICORE 支持列表(FLOAT、FLOAT16、INT32、BF16、INT64、INT8、UINT8、INT16)内时,走StatelessRandpermAiCore的 AICORE kernel;否则回退到StatelessRandpermAiCpu的 AICPU kernel。这种双路径设计既保证了 950 系列上的高性能,也保证了其他平台的可用性。

完整调用示例

原文档给出了可直接编译运行的 C++ 示例(仓库中 examples/test_aclnn_randperm.cpp 与之一致)。该示例以 n=8、seed=1234、offset=0、输出类型 FLOAT 为例,完整展示了 aclnnRandperm 的标准调用流程:

#include <iostream> #include <vector> #include "acl/acl.h" #include "aclnnop/aclnn_randperm.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> outShape = {8}; void* outDeviceAddr = nullptr; aclTensor* out = nullptr; std::vector<float> outHostData = {0, 0, 0, 0, 0, 0, 0, 0}; int64_t n = 8; int64_t seed = 1234; int64_t offset = 0; // 创建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; // 调用aclnnRandperm第一段接口 ret = aclnnRandpermGetWorkspaceSize(n, seed, offset, out, &workspaceSize, &executor); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnRandpermGetWorkspaceSize 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); } // 调用aclnnRandperm第二段接口 ret = aclnnRandperm(workspaceAddr, workspaceSize, executor, stream); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnRandperm 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> 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: %f\n", i, resultData[i]); } // 6. 释放aclTensor和aclScalar,需要根据具体API的接口定义修改 aclDestroyTensor(out); // 7. 释放device资源 aclrtFree(outDeviceAddr); if (workspaceSize > 0) { aclrtFree(workspaceAddr); } aclrtDestroyStream(stream); aclrtResetDevice(deviceId); aclFinalize(); return 0; }

代码整体分为七个阶段,其中第 1、4、6、7 步为所有 aclnn 算子的固定写法:

  1. 资源初始化aclInitaclrtSetDeviceaclrtCreateStream
  2. 构造输入输出:通过aclrtMalloc申请 Device 内存、aclrtMemcpy拷贝 Host 数据,并用aclCreateTensor创建 ND 格式的aclTensor
  3. 两段式调用:先调用aclnnRandpermGetWorkspaceSize获取 workspaceSize 与 executor,按需申请 workspace 内存后调用aclnnRandperm提交计算;
  4. 同步等待aclrtSynchronizeStream确保任务在 Stream 上执行完毕;
  5. 取回结果:用aclrtMemcpyACL_MEMCPY_DEVICE_TO_HOST)将排列结果拷回 Host 侧并打印,期望看到的是 0~7 的一个随机排列;
  6. 释放 tensoraclDestroyTensor(out)
  7. 释放资源:依次aclrtFreeaclrtDestroyStreamaclrtResetDeviceaclFinalize

示例的编译与执行方法(依赖 CANN 工具包中的头文件与链接库,需按安装环境配置编译选项与 LD 路径)可参考 编译与运行样例。

测试与验证

仓库为该算子配备了完整的单测与端到端测试,可作为功能验证与二次开发的参照:

  • Host 侧单测:tests/ut/op_host/test_stateless_randperm_infershape.cpp 验证 shape 推导逻辑,确认输出 shape 恒为[n]
  • Kernel 侧单测:tests/ut/op_kernel/test_stateless_randperm.cpp 直接对 kernel 进行数值验证;
  • Tiling 单测:tests/ut/op_host/arch35/test_stateless_randperm_tiling.cpp 校验 950 系列的 tiling 参数计算;
  • 端到端测试:tests/st/aclnnRandperm/executor_aclnnRandperm.py 配合atk_aclnnRandperm.json通过算子测试工具(ATK)在真实设备上执行,其参考结果由 tests/assets/golden.py 生成,可作为预期输出的权威参照——由于算子为确定性实现,golden 结果可精确比对。

总结

aclnnRandperm 是 CANN ops-math 中实现无状态整数随机排列的 aclnn 接口:通过"GetWorkspaceSize + 执行"两段式调用即可在 NPU 上生成 0 到 n-1 的随机排列,输出数据类型覆盖整型与浮点型的九种类型并支持非连续 tensor。从源码看,其确定性由"Philox 伪随机 + 排序 + Fisher-Yates 洗牌"的内核算法与 (seed, offset) 的显式编码共同保证,并依据架构在 AICORE 与 AICPU 路径间自动分派。结合仓库中的示例与测试,开发者可以在 Atlas 训练/推理系列及 Ascend 950 系列产品上快速完成该算子的集成与验证。

【免费下载链接】ops-math本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-math

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/21 21:45:32

高效计算中位数的算法与实践

1. 项目背景与核心问题"NP0014&#xff1a;中间的数"这个看似简单的标题背后&#xff0c;隐藏着一个经典的算法问题——如何高效地找到一组数据的中间值。在实际开发中&#xff0c;这个问题远比表面看起来复杂&#xff0c;特别是在处理海量数据流、实时统计系统或金融…

作者头像 李华
网站建设 2026/9/21 21:43:57

Microchip Studio 7 烧录全流程:从工具配置到熔丝位设置与故障排查

1. 为什么还要聊 Microchip Studio 7 的烧录如果你手头有一块 ATmega328P、ATtiny85 或者 SAM D21 的板子&#xff0c;大概率绕不开 Microchip Studio 7 这个 IDE。它前身是 Atmel Studio&#xff0c;Microchip 收购 Atmel 之后换了名字&#xff0c;但骨子里还是那套 AVR/ARM 的…

作者头像 李华
网站建设 2026/9/21 21:43:55

Cadence Virtuoso原理图设计与仿真实战指南

1. 这不是软件安装说明书&#xff0c;而是一份“能画出第一张可仿真的原理图”的实战手记我带过十几届微电子和集成电路方向的本科生做课程设计&#xff0c;也帮过不少转行做模拟IC设计的工程师补基础。每次看到新人打开Cadence Virtuoso 6.1.7&#xff0c;鼠标悬停在Schematic…

作者头像 李华
网站建设 2026/9/21 21:13:19

跨地域GPU算力调度:从原理到落地的完整拆解

说起来有点惭愧&#xff0c;我第一次接到“跨地域GPU算力调度”这个需求时&#xff0c;第一反应是“这不就是给K8s加几个节点吗”。但真正动手以后才发现&#xff0c;把一批GPU集群从“单地域调度”升级成“跨地域调度”&#xff0c;复杂度完全是另一个量级&#xff1a;你面对的…

作者头像 李华
网站建设 2026/9/21 21:12:33

千笔工具:提升继续教育论文写作效率的智能解决方案

1. 工具定位与核心价值解析作为一名在学术领域摸爬滚打多年的研究者&#xff0c;我深刻理解继续教育群体在论文写作中的痛点。千笔这款工具精准切中了三大核心需求&#xff1a;时间碎片化、学术规范性强、写作效率要求高。它不像通用型写作软件那样大而全&#xff0c;而是专门针…

作者头像 李华
网站建设 2026/9/21 20:58:54

第三方ROM刷不进去?多半是底包版本没配对

刷过安卓机的人大概率都经历过这样一种场面&#xff1a;TWRP里滑动确认刷入&#xff0c;进度条刚走两秒&#xff0c;红色报错直接拍在脸上——“Cant install this package on top of incompatible data”又或者是“Error 7”&#xff0c;再看下面小字&#xff0c;写着“This p…

作者头像 李华