- 算子库
- 人工智能
- CANN
【免费下载链接】ops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
SignBitsUnpack 是 CANN ops-math 开源算子库中面向 1 位 Adam(1-bit Adam)优化算法场景的数学类算子,负责把按位压缩的 uint8 类型符号位数据拆包(unpack)为 float32 或 float16 的浮点张量。本文以仓库中的 算子 README 与 aclnnSignBitsUnpack 接口文档 为主体,结合 op_api、op_host、op_kernel 与单测源码,完整讲解该算子的功能语义、参数约束、内部计算流程、两段式 aclnn 调用方式以及端到端验证方法。读完本文,你将能够独立完成 SignBitsUnpack 算子的环境适配、参数配置、代码编写与结果校验。
功能说明:什么是 1 位符号拆包
在 1 位 Adam 这类分布式训练压缩方案中,优化器状态通常被量化压缩为单个符号位(sign bit)存储:每个 float 只保留其符号,压缩进 uint8 字节的各个 bit 位中,从而大幅降低通信与存储开销。SignBitsUnpack算子正是这一压缩流程的解码端:它把 uint8 类型的输入逐位展开,恢复为浮点张量。
README 中对算子功能的描述为:
- 算子功能:对输入进行 unpack。
- 当位置为 1 时取 1.0,位置为 0 时取 0.0。
需要说明的是,从仓库中实际的 kernel 实现(op_kernel/sign_bits_unpack.h)与单测 golden(tests/ut/op_kernel/sign_bits_unpack_data/gen_data.py)来看,实现细节为:bit 位为 1 时输出 1.0,bit 位为 0 时输出 -1.0(即输出值为 ±1.0 的符号表示),这与 1 位 Adam 的 +1/-1 符号语义一致,也对应接口文档中“将 uint8 类型 1 位 Adam 拆包为 float32 或者 float16”的功能描述。
拆包规则与 NumPy 的np.unpackbits(..., bitorder='little')一致:按小端位序,即每个 uint8 元素展开为 8 个输出元素,第 0 位(最低位)对应输出中的第一个位置。仓库中的配套算子 SignBitsPack 完成反向的打包(pack)过程,二者构成“符号位打包/拆包”的完整闭环。
产品支持情况
根据 算子 README 中的产品支持表,SignBitsUnpack 的支持范围如下:
| 产品 | 是否支持 |
|---|---|
| Atlas A2 训练系列产品 / Atlas 800I A2 推理产品 / A200I A2 Box 异构组件 | √ |
更细粒度的支持情况见 aclnnSignBitsUnpack 接口文档:
| 产品 | 是否支持 |
|---|---|
| Ascend 950PR / Ascend 950DT | × |
| Atlas A3 训练系列产品 / Atlas A3 推理系列产品 | × |
| Atlas A2 训练系列产品 / Atlas A2 推理系列产品 | √ |
| Atlas 200I/500 A2 推理产品 | × |
| Atlas 推理系列产品 | × |
| Atlas 训练系列产品 | × |
该支持范围同样可以从源码中得到印证:
- 在 op_host/sign_bits_unpack_def.cpp 中,算子通过
this->AICore().AddConfig("ascend910b")注册 AICore 配置,ascend910b 即 Atlas A2 训练系列对应的昇腾 SoC 版本; - 在 op_api/aclnn_sign_bits_unpack.cpp 的
CheckDtypeValid中,接口层会通过GetCurrentPlatformInfo().GetCurNpuArch() != NpuArch::DAV_2201对运行设备做芯片架构检查,不满足时直接报ACLNN_ERR_PARAM_INVALID,日志提示"SignBitsUnpack is not supported on this device."。
参数说明
算子(IR 层)参数
README 给出的算子参数定义如下:
| 参数名 | 输入/输出/属性 | 描述 | 数据类型 | 数据格式 |
|---|---|---|---|---|
| self | 输入 | 待进行 SignBitsUnpack 计算的入参,公式中的 x1 | uint8 | ND |
| size | 参数 | reshape 时输出张量的第一个维度 | int64 | 1 |
| dtype | 参数 | 决定输出的数据类型 | int64 | 1 |
| y | 输出 | 待进行 SignBitsUnpack 计算的出参,公式中的输出 | float16, float | ND |
对应的算子定义注册在 op_host/sign_bits_unpack_def.cpp 中,输入self支持DT_UINT8、格式FORMAT_ND,输出y支持DT_FLOAT16 / DT_FLOAT、格式FORMAT_ND,并显式声明了 UnknownShape 场景下的格式,方便动态 shape 图编译。
aclnn 接口层参数
在 aclnnSignBitsUnpack 接口文档 与 aclnn_sign_bits_unpack.h 中,接口参数语义如下:
| 参数 | 方向 | 说明 |
|---|---|---|
| self | 计算输入 | 1D 的 Device 侧 aclTensor,数据类型 UINT8,格式 ND。支持空 tensor、支持非连续的 Tensor |
| size | 入参 | Host 侧 int64 整型,reshape 时输出张量的第一个维度 |
| dtype | 入参 | 输出 Tensor 的数据类型,支持 ACL_FLOAT16、ACL_FLOAT |
| out | 计算输出 | Device 侧 aclTensor,数据类型 FLOAT16/FLOAT(由 dtype 决定),格式 ND,支持非连续的 Tensor |
| workspaceSize | 出参 | 需要在 Device 侧申请的 workspace 大小 |
| executor | 出参 | 算子执行器,包含算子计算流程 |
参数校验规则与错误码
第一段接口aclnnSignBitsUnpackGetWorkspaceSize会完成入参校验,校验逻辑实现在 op_api/aclnn_sign_bits_unpack.cpp 的CheckNotNull、CheckDtypeValid、CheckFormat、CheckShape、CheckValue等函数中。完整错误码列表如下(具体返回码含义可参考 aclnn 返回码):
| 返回值 | 错误码 | 描述 |
|---|---|---|
| ACLNN_ERR_PARAM_NULLPTR | 161001 | 传入的 self 或 out 是空指针 |
| ACLNN_ERR_PARAM_INVALID | 161002 | self、out 的数据类型 / 数据格式不在支持的范围内 |
| ACLNN_ERR_PARAM_INVALID | 161002 | size 小于等于 0,或者(self 的元素个数)× 8 % size != 0 |
| ACLNN_ERR_PARAM_INVALID | 161002 | out 的数据类型与 dtype 不一致 |
| ACLNN_ERR_PARAM_INVALID | 161002 | self 的维度不是 1 维 |
| ACLNN_ERR_PARAM_INVALID | 161002 | out 的第一维度与 size 不一致 |
从源码看,PACK_SIZE被定义为 8(每个 uint8 展开 8 个元素),校验要点为:self必须是 1 维、out必须是 2 维、size > 0、selfDim * 8可被size整除、out第一维必须等于size,并且out的数据类型必须与dtype参数完全一致(通过OP_CHECK_DTYPE_NOT_MATCH校验)。
源码级实现原理
aclnn 接口层的完整计算流程
在 op_api/aclnn_sign_bits_unpack.cpp 中,以注释形式给出了算子的完整计算图:
self dtype size \ / / Contiguous(workspace_0) / / \ / / SignBitsUnpack(workspace_1) | ViewCopy | result即一次aclnnSignBitsUnpackGetWorkspaceSize调用内部会依次拼接三个算子:
l0op::Contiguous(self):将输入self转换为连续的 tensor(对应 workspace_0);l0op::SignBitsUnpack(selfContiguous, size, dtype):执行真正的符号位拆包计算(对应 workspace_1);l0op::ViewCopy(castOut, out):将计算结果写回用户提供的out,从而天然支持输出为非连续 tensor 的场景。
最终通过*workspaceSize = uniqueExecutor->GetWorkspaceSize()汇总计算所需的临时内存大小。接口还支持空 tensor 场景:当self->IsEmpty() || out->IsEmpty()时,直接置workspaceSize = 0并返回成功,不执行任何计算。整个流程由第二段接口aclnnSignBitsUnpack通过CommonOpExecutorRun(workspace, workspaceSize, executor, stream)提交到指定的 stream 上执行。
Kernel 计算:Duplicate + Select 实现符号展开
真正的拆包逻辑位于 op_kernel/sign_bits_unpack.cpp 与 op_kernel/sign_bits_unpack.h 中。kernel 入口根据 tiling key(schMode)实例化不同的输出类型模板:
TILING_KEY_IS(1):KernelSignBitsUnpack<half>,即 float16 输出路径;- 否则:
KernelSignBitsUnpack<float>,即 float32 输出路径。
KernelSignBitsUnpack采用经典的 CopyIn / Compute / CopyOut 三段式流水(AscendC 编程范式):
- CopyIn:通过
DataCopy将 Global Memory 中当前 tile 的 uint8 数据搬入VECIN队列的 LocalTensor; - Compute:核心计算由
Duplicate与Select两个向量指令完成。float 路径为:AscendC::Duplicate(outLocal, static_cast<float>(1.0), this->processDataNumOut); AscendC::Select(outLocal, selfLocal, outLocal, static_cast<float>(-1.0), AscendC::SELMODE::VSEL_TENSOR_SCALAR_MODE, this->processDataNumOut);即先把输出全部初始化为 1.0,再以 uint8 的每一位作为选择条件、以 -1.0 作为标量参与
VSEL_TENSOR_SCALAR_MODE选择,得到 bit=1 → 1.0、bit=0 → -1.0 的结果。half 路径则分成两次 Select(代码注释为“数据对齐”),最终语义一致。 - CopyOut:将
VECOUT队列中的结果写回 Global Memory,其中输出元素个数是输入元素个数的 8 倍(outCoreNum = coreDataNum * 8)。
流水线采用双缓冲(DOUBLE_BUFFER = 2,TQue<TPosition::VECIN/VECOUT, DOUBLE_BUFFER>),并通过 tiling 数据中的bufferOpen字段支持在数据量较小时退化为单缓冲以节省 UB 空间。float 路径额外申请了一块half类型的VECCALC临时缓冲tmpQueue0用于中间计算。
Tiling 策略:按 Core 均分与尾块处理
Tiling 逻辑在 op_host/sign_bits_unpack_tiling.cpp 中,流程分为四步:
- 获取平台信息(
GetPlatformInfo):通过PlatformAscendC获取 UB 大小(GetCoreMemSize(UB))与核数(GetCoreNum()); - 获取 shape 与属性(
GetShapeAttrsInfo):以BLOCK_SIZE = 64字节为对齐粒度,根据输入字节数、输出类型长度(float 为 4、half 为 2)估算单个 tile 可容纳的数据量tileDataNum,并决定是否开启双缓冲; - 获取 workspace(
GetWorkspaceSize):合并用户 workspace(本算子为 0)与框架系统 workspace; - 计算各核负载(
CalculateCoreBlockNums):把输入按 64 字节块均分到各核,得到smallCoreDataNum/bigCoreDataNum(前tailBlockNum个核多分 1 块)、finalSmallTileNum/finalBigTileNum、smallTailDataNum/bigTailDataNum等参数。
最终写入 sign_bits_unpack_tiling_data.h 定义的SignBitsUnpackTilingData结构体,并调用context->SetBlockDim(coreNum)设置并行核数。当单个 tile 即可容纳全部输入时,coreNum收敛为 1,退化为单核处理。tiling key 的取值定义在 sign_bits_unpack_tiling_key.h 中:ELEMENTWISE_TPL_SCH_MODE_0(float 输出)与ELEMENTWISE_TPL_SCH_MODE_1(half 输出)。
算子工程接入方式见 CMakeLists.txt:通过add_all_modules_sources(OPTYPE sign_bits_unpack ACLNNTYPE aclnn_exclude)自动收集各目录源文件并纳入构建。
两段式接口调用说明
与 CANN 其它单算子 API 一致,SignBitsUnpack 采用两段式接口(Two-Phase API)模式:必须先调用第一段接口获取 workspace 大小与执行器,再调用第二段接口执行计算。两段接口的函数原型如下:
aclnnStatus aclnnSignBitsUnpackGetWorkspaceSize( const aclTensor* self, int64_t size, aclDataType dtype, aclTensor* out, uint64_t* workspaceSize, aclOpExecutor** executor); aclnnStatus aclnnSignBitsUnpack( void* workspace, uint64_t workspaceSize, aclOpExecutor* executor, aclrtStream stream);第二段接口的参数语义:
| 参数名 | 输入/输出 | 描述 |
|---|---|---|
| workspace | 输入 | 在 Device 侧申请的 workspace 内存地址 |
| workspaceSize | 输入 | 在 Device 侧申请的 workspace 大小,由第一段接口获取 |
| executor | 输入 | op 执行器,包含了算子计算流程 |
| stream | 输入 | 指定执行任务的 Stream |
注意事项:第二段接口不可重复调用,即“GetWorkspaceSize → 执行”必须成对出现;第二段接口本身不校验入参,所有入参合法性都在第一段接口完成。
调用示例:完整可运行的 aclnn 样例
仓库在 examples/test_aclnn_sign_bits_unpack.cpp 提供了完整的端到端调用样例,编译与运行流程可参考 编译与运行样例。以下为完整示例代码:
#include <memory> #include <iostream> #include <vector> #include "acl/acl.h" #include "aclnnop/aclnn_sign_bits_unpack.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> selfShape = {2}; std::vector<int64_t> outShape = {2, 8}; int64_t outsize = 2; aclDataType dataType = ACL_FLOAT; void* selfDeviceAddr = nullptr; void* outDeviceAddr = nullptr; aclTensor* self = nullptr; aclTensor* out = nullptr; std::vector<uint8_t> selfHostData = {128, 128}; std::vector<float> outHostData = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; // 创建self aclTensor ret = CreateAclTensor(selfHostData, selfShape, &selfDeviceAddr, aclDataType::ACL_UINT8, &self); 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); // 3. 调用CANN算子库API,需要修改为具体的Api名称 uint64_t workspaceSize = 0; aclOpExecutor* executor; // 调用aclnnSignBitsUnpack第一段接口 ret = aclnnSignBitsUnpackGetWorkspaceSize(self, outsize, dataType, out, &workspaceSize, &executor); CHECK_RET( ret == ACL_SUCCESS, LOG_PRINT("aclnnSignBitsUnpackGetWorkspaceSize 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); } // 调用aclnnSignBitsUnpack第二段接口 ret = aclnnSignBitsUnpack(workspaceAddr, workspaceSize, executor, stream); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnSignBitsUnpack 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,需要根据具体API的接口定义修改 aclDestroyTensor(self); aclDestroyTensor(out); // 7. 释放device 资源 aclrtFree(selfDeviceAddr); aclrtFree(outDeviceAddr); if (workspaceSize > 0) { aclrtFree(workspaceAddr); } aclrtDestroyStream(stream); aclrtResetDevice(deviceId); aclFinalize(); return 0; }该样例的关键点解读:
- shape 关系:输入
selfShape = {2}(2 个 uint8),输出outShape = {2, 8}(2 行 × 8 列)。size = 2对应输出第一维,满足“(self 元素个数)× 8 % size == 0”的校验条件; - 数据验证:输入
{128, 128},其中 128 = 0b10000000,按小端位序展开为[0,0,0,0,0,0,0,1],对应输出应为[-1, -1, -1, -1, -1, -1, -1, 1](bit=1 → 1.0,bit=0 → -1.0),每个 uint8 元素扩展出 8 个 float; - 内存生命周期:self/out 的 Device 内存、workspace 内存均通过
aclrtMalloc申请,用完后依次aclDestroyTensor释放 tensor、aclrtFree释放内存、aclrtDestroyStream销毁 stream、aclrtResetDevice复位设备、aclFinalize完成收尾。
单测与数据校验:如何验证拆包正确性
仓库在 tests/ut/op_kernel/test_sign_bits_unpack.cpp 提供了基于 gtest 的 kernel 级单测,测试链路如下:
- 通过
python3 gen_data.py '(128)' 'uint8'生成随机输入数据与 golden 基准,生成逻辑见 sign_bits_unpack_data/gen_data.py:input_self = np.random.randint(0, 10, shape).astype(np_type) golden = np.unpackbits(input_self, bitorder='little').astype(np.float32) golden[golden == 0] = -1即先按小端位序做逐位拆包,再把 0 位替换为 -1,得到 ±1.0 的 float32 golden;
- 测试用例手动构造
SignBitsUnpackTilingData(smallCoreDataNum = 128、bigCoreDataNum = 160、tileDataNum = 2048、bufferOpen = 0等),通过ICPU_RUN_KF(func, blockDim, self, out, workspace, tilingData)在 CPU 仿真环境(tikicpulib)中运行 kernel; - 输出写入
float_output_t_sign_bits_unpack.bin,再调用compare_data.py与 golden 比对,验证拆包结果逐元素一致。
此外还有 Host 侧 tiling 单测(tests/ut/op_host/test_sign_bits_unpack_tiling.cpp)用于校验 tiling 参数计算逻辑。这套“脚本生成 golden + gtest 驱动 kernel + 数据比对”的组合,是复现算子正确性验证的直接入口。
约束说明
- 确定性计算:
aclnnSignBitsUnpack默认采用确定性实现,多次运行同一输入会得到逐位一致的结果,不会引入随机性,相关背景可参考确定性计算。README 中“约束说明:无”是指算子对输入数据本身无额外的 shape/取值范围限制(合法输入约束统一由第一段接口的入参校验保证)。 - 数据类型与格式约束汇总:输入仅支持 uint8、ND 格式;输出仅支持 float16/float(由 dtype 决定)、ND 格式;输入必须为 1 维,输出必须为 2 维且第一维等于 size。
- 运行环境约束:仅支持 Atlas A2 训练系列 / Atlas A2 推理系列(ascend910b / DAV_2201 架构)等产品,其它昇腾产品不支持(见上文产品支持表)。
参考文档导航
- SignBitsUnpack 算子 README:算子功能、参数、产品支持总览
- aclnnSignBitsUnpack 接口文档:接口原型、错误码、调用示例
- 两段式接口说明:单算子 API 的通用调用范式
- 编译与运行样例:样例工程的编译运行方法
- aclnn 返回码:接口状态码含义
- 非连续 Tensor 支持与数据格式:接口对 tensor 形态与格式的支持说明
- 配套算子:SignBitsPack:符号位打包(与本文拆包互为逆过程)
- 算子库
- 人工智能
- CANN
【免费下载链接】ops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
相关推荐
CANN ops-math 算子解析:aclnnSignBitsPack 符号位打包接口原理与两段式调用实战
CANN ops math 算子解析:aclnnSignBitsPack 符号位打包接口原理与两段式调用实战 导读 aclnnSignBitsPack 是 CA
算子库人工智能CANNCANN ops-math 算子详解:KLDivV2 的接口、实现原理与 aclnn 调用实战
CANN ops math 算子详解:KLDivV2 的接口、实现原理与 aclnn 调用实战 KLDivV2 是 CANN ops math 算子库中用于计算
算子库人工智能CANNCANN ops-math IsClose 算子深度解析:原理、aclnn API 调用与 AscendC 实现
CANN ops math IsClose 算子深度解析:原理、aclnn API 调用与 AscendC 实现 IsClose 是 CANN ops math
算子库人工智能CANN
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考