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 算子接口体系中,每个算子采用两段式接口调用模式(详见 两段式接口说明):
- 第一段
aclnnRandpermGetWorkspaceSize:完成入参校验,计算算子执行所需的 workspace 大小,并创建包含算子计算流程的执行器aclOpExecutor; - 第二段
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 | 输出 | 输出 tensor | INT64、INT32、INT16、UINT8、INT8、FLOAT、FLOAT16、DOUBLE、BFLOAT16 | ND | 为 [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_NULLPTR | 161001 | 传入的 out 是空指针 |
| ACLNN_ERR_PARAM_INVALID | 161002 | 传入的 n 小于 0 |
| ACLNN_ERR_PARAM_INVALID | 161002 | out 的 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设置算子执行超时时间。
- INT64、INT32、INT16、UINT8、INT8、FLOAT、FLOAT16、BFLOAT16 类型下,n 不得超过 int32 的最大值(即 n ≤ 2147483647),因为 kernel 内部使用 32 位索引对随机数缓冲区寻址(对应
源码级原理剖析
算子定义与 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():
- 随机数生成(Philox):
Philox内核函数用 Philox 4×32-10 算法为每个元素生成一个随机位串(randomBits位),写入randWorkSpace_缓冲区。Philox 算法的完整实现(ComputeSingleRound、RaiseKey、PhiloxRandom、SkipAhead、RandInit、Rand4等)位于 op_kernel/arch35/stateless_randperm_random.h,其中RandInit通过SkipAhead_Sequence(subsequence)和SkipAhead(offset)把 seed 子序列号与用户 offset 编码进 counter,实现"无状态 + 可复现"; - 排序(Sort):
Sort模板对随机位串排序,使得位串相同的元素聚集为连续的"岛屿(island)",同时维护元素原始索引indexWorkSpace_。由于相同位串被聚到一起,洗牌只需要在 islands 内部进行,大幅减少了跨 core 的同步开销; - Fisher-Yates 洗牌(FindAndFisherYares):对每个 island 内部的索引执行 Fisher-Yates 原地洗牌(从
islandSize - 1递减到 1,逐个与[0, j]内的随机位置交换),使用的随机数依旧来自 Philox 序列,确保随机性完全由 (seed, offset) 决定; - 拷贝输出(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 算子的固定写法:
- 资源初始化:
aclInit→aclrtSetDevice→aclrtCreateStream; - 构造输入输出:通过
aclrtMalloc申请 Device 内存、aclrtMemcpy拷贝 Host 数据,并用aclCreateTensor创建 ND 格式的aclTensor; - 两段式调用:先调用
aclnnRandpermGetWorkspaceSize获取 workspaceSize 与 executor,按需申请 workspace 内存后调用aclnnRandperm提交计算; - 同步等待:
aclrtSynchronizeStream确保任务在 Stream 上执行完毕; - 取回结果:用
aclrtMemcpy(ACL_MEMCPY_DEVICE_TO_HOST)将排列结果拷回 Host 侧并打印,期望看到的是 0~7 的一个随机排列; - 释放 tensor:
aclDestroyTensor(out); - 释放资源:依次
aclrtFree、aclrtDestroyStream、aclrtResetDevice、aclFinalize。
示例的编译与执行方法(依赖 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),仅供参考