CANN ops-math SignBitsUnpack 算子全解析:1 位 Adam 符号拆包的原理、实现与 aclnn 调用实战
2026/9/20 6:29:36 网站建设 项目流程
  • 算子库
  • 人工智能
  • CANN

【免费下载链接】ops-math

本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。

项目地址:https://gitcode.com/cann/ops-math
点击查看免费下载

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 计算的入参,公式中的 x1uint8ND
size参数reshape 时输出张量的第一个维度int641
dtype参数决定输出的数据类型int641
y输出待进行 SignBitsUnpack 计算的出参,公式中的输出float16, floatND

对应的算子定义注册在 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 的CheckNotNullCheckDtypeValidCheckFormatCheckShapeCheckValue等函数中。完整错误码列表如下(具体返回码含义可参考 aclnn 返回码):

返回值错误码描述
ACLNN_ERR_PARAM_NULLPTR161001传入的 self 或 out 是空指针
ACLNN_ERR_PARAM_INVALID161002self、out 的数据类型 / 数据格式不在支持的范围内
ACLNN_ERR_PARAM_INVALID161002size 小于等于 0,或者(self 的元素个数)× 8 % size != 0
ACLNN_ERR_PARAM_INVALID161002out 的数据类型与 dtype 不一致
ACLNN_ERR_PARAM_INVALID161002self 的维度不是 1 维
ACLNN_ERR_PARAM_INVALID161002out 的第一维度与 size 不一致

从源码看,PACK_SIZE被定义为 8(每个 uint8 展开 8 个元素),校验要点为:self必须是 1 维、out必须是 2 维、size > 0selfDim * 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调用内部会依次拼接三个算子:

  1. l0op::Contiguous(self):将输入self转换为连续的 tensor(对应 workspace_0);
  2. l0op::SignBitsUnpack(selfContiguous, size, dtype):执行真正的符号位拆包计算(对应 workspace_1);
  3. 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:核心计算由DuplicateSelect两个向量指令完成。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 = 2TQue<TPosition::VECIN/VECOUT, DOUBLE_BUFFER>),并通过 tiling 数据中的bufferOpen字段支持在数据量较小时退化为单缓冲以节省 UB 空间。float 路径额外申请了一块half类型的VECCALC临时缓冲tmpQueue0用于中间计算。

Tiling 策略:按 Core 均分与尾块处理

Tiling 逻辑在 op_host/sign_bits_unpack_tiling.cpp 中,流程分为四步:

  1. 获取平台信息GetPlatformInfo):通过PlatformAscendC获取 UB 大小(GetCoreMemSize(UB))与核数(GetCoreNum());
  2. 获取 shape 与属性GetShapeAttrsInfo):以BLOCK_SIZE = 64字节为对齐粒度,根据输入字节数、输出类型长度(float 为 4、half 为 2)估算单个 tile 可容纳的数据量tileDataNum,并决定是否开启双缓冲;
  3. 获取 workspaceGetWorkspaceSize):合并用户 workspace(本算子为 0)与框架系统 workspace;
  4. 计算各核负载CalculateCoreBlockNums):把输入按 64 字节块均分到各核,得到smallCoreDataNum/bigCoreDataNum(前tailBlockNum个核多分 1 块)、finalSmallTileNum/finalBigTileNumsmallTailDataNum/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 级单测,测试链路如下:

  1. 通过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;

  2. 测试用例手动构造SignBitsUnpackTilingDatasmallCoreDataNum = 128bigCoreDataNum = 160tileDataNum = 2048bufferOpen = 0等),通过ICPU_RUN_KF(func, blockDim, self, out, workspace, tilingData)在 CPU 仿真环境(tikicpulib)中运行 kernel;
  3. 输出写入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上加速计算。

项目地址:https://gitcode.com/cann/ops-math
点击查看免费下载

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

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

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

立即咨询