news 2026/9/20 16:56:13

CANN ops-math 算子 aclnnHistc 接口详解:张量直方图统计的完整实践指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
CANN ops-math 算子 aclnnHistc 接口详解:张量直方图统计的完整实践指南

CANN ops-math 算子 aclnnHistc 接口详解:张量直方图统计的完整实践指南

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

导读

aclnnHistc是 CANN ops-math 算子库中HistogramV2算子的上层调用接口,用于计算张量元素在等宽区间内的频数分布(即直方图),是实现数值分布统计、梯度裁剪分析、量化校准等场景的基础能力。本文以 math/histogram_v2/docs/aclnnHistc.md 为骨架,结合 aclnn_histc.cpp、histogram.cpp、histogram_v2.cpp 等源码,完整讲解接口原型、参数约束、错误码语义、确定性计算行为与端到端调用示例,帮助读者在 Ascend 系列硬件上快速、正确地完成直方图统计计算。

功能说明:直方图统计的数学语义

aclnnHistc完成的核心计算是张量直方图(Histogram)统计

  • minmax作为统计的上下限,在二者之间划出等宽的、数量为bins的区间;
  • 统计输入张量self中所有元素落入各个区间的数量,写入输出张量out
  • 如果minmax相等,则使用张量中所有元素的最小值和最大值作为统计上下限(该逻辑在 aclnn_histc.cpp 的NeedComputeMinMax中体现:在 Atlas A2 训练/推理系列与 Ascend 950 上,当min == max(浮点场景差值绝对值小于1e-6,或min == max == +inf / -inf)时,会通过AllMinMax调用l0op::ReduceMin/l0op::ReduceMax重新求取张量内的真实极值);
  • 小于min或大于max的元素不会被统计,直接丢弃。

以文档调用示例的数据为例:self = {1,2,3,4,5,6,7,8,9}bins = 3min = 1max = 9,区间被等分为[1, 3.667)[3.667, 6.333)[6.333, 9],各区间恰好落入 3 个元素,因此输出为{3, 3, 3}。该行为与 PyTorchtorch.histc对齐,仓库中的 golden 测试 aclnnHistc.py 正是以torch.histc(input0_t, bins=bins, min=minVal, max=maxVal)作为参照生成期望结果。

产品支持情况

aclnnHistc的硬件支持情况如下(以 aclnnHistc.md 为准):

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

从算子定义看,histogram_v2_def.cpp 为HistogramV2注册了ascend910bascend910_93(对应 A3)、ascend310pascend950四套 AICore 配置,与上述支持矩阵一致;其中 Ascend 950 配置额外指定了opFile.value = "histogram_v2_apt",对应 histogram_v2_apt.cpp 专属实现。

两段式接口:先规划、后执行

aclnnHistc遵循 CANN 算子的两段式接口设计,调用流程分为两步:

  1. 先调用aclnnHistcGetWorkspaceSize:完成入参校验、构建算子计算图(executor),并返回执行所需的 workspace 大小;
  2. 再调用aclnnHistc:在指定的 Stream 上真正执行计算。

在 aclnn_histc.cpp 的第一段接口实现中可以看到,接口内部依次完成参数校验、Contiguous连续性转换、min/max标量转张量、必要时重算极值、l0op::Histogram计算图构建、Cast类型转换与ViewCopy结果回写,最后通过GetWorkspaceSize()汇总出整个算子链的 workspace 需求。

函数原型

aclnnStatus aclnnHistcGetWorkspaceSize( const aclTensor* self, int64_t bins, const aclScalar* min, const aclScalar* max, aclTensor* out, uint64_t* workspaceSize, aclOpExecutor** executor)
aclnnStatus aclnnHistc( void* workspace, uint64_t workspaceSize, aclOpExecutor* executor, aclrtStream stream)

aclnnHistcGetWorkspaceSize 参数说明

第一段接口共 7 个参数,其含义、类型与使用约束如下表:

参数名输入/输出描述使用说明数据类型数据格式维度(shape)非连续Tensor
self(aclTensor*)输入待被统计元素在各个 bins 的数量的张量-FLOAT16、FLOAT32、INT32、INT64、INT16、INT8、UINT8ND0-8
bins(int64_t)输入直方图 bins 的数量取值范围需大于 0----
min(aclScalar*)输入直方图的统计下限(包括)数据类型需要是可转换成 FLOAT 的类型,取值范围不能大于 max 的值----
max(aclScalar*)输入直方图的统计上限(包括)数据类型需要是可转换成 FLOAT 的类型,取值范围不能小于 min 的值----
out(aclTensor*)输出直方图统计结果out 数据类型需要可转换为 self 的数据类型(参考互转换关系);元素个数等于 binsFLOAT16、FLOAT32、INT32、INT64、INT16、INT8、UINT8ND1
workspaceSize(uint64_t*)输出返回需要在 Device 侧申请的 workspace 大小-----
executor(aclOpExecutor**)输出返回 op 执行器,包含了算子计算流程-----

补充说明:

  • 数据类型限制与实现一致:aclnn_histc.cpp 中的DTYPE_SUPPORT_LIST_910B定义了与文档完全一致的支持列表(FLOAT、FLOAT16、INT64、INT32、INT16、INT8、UINT8)。
  • 维度上限self支持 0-8 维,对应源码中的MAX_DIM = 8(aclnn_histc.cpp),out必须是 1 维且元素个数等于bins
  • 非连续 Tensorselfout均支持非连续存储,第一段接口内部会先通过l0op::Contiguous完成连续性转换后再送入内核,因此调用方无需手动预做contiguous
  • 标量转换min/maxaclScalar,内部通过ConvertToTensorself的数据类型转换为单元素张量参与构图(aclnn_histc.cpp)。
  • 空张量场景:当self->IsEmpty()时,接口会直接走EmptyTensor分支,通过ZerosLike+ViewCopyout清零后返回,不进入真正的直方图计算(aclnn_histc.cpp)。

返回值:状态码与错误码语义

aclnnHistcGetWorkspaceSizeaclnnHistc均返回aclnnStatus状态码,通用返回码定义见 aclnn 返回码。

第一段接口aclnnHistcGetWorkspaceSize会完成全部入参校验,以下场景将返回错误:

返回码错误码描述
ACLNN_ERR_PARAM_NULLPTR161001传入的 self、out、min、max 是空指针
ACLNN_ERR_PARAM_INVALID161002self 和 out 的数据类型和数据格式不在支持的范围之内
ACLNN_ERR_PARAM_INVALID161002self 与 out 的数据类型不满足互转换关系
ACLNN_ERR_PARAM_INVALID161002传入的 bins 小于等于 0
ACLNN_ERR_PARAM_INVALID161002传入的 min 大于 max
ACLNN_ERR_PARAM_INVALID161002out 的 shape 维度不为 1
ACLNN_ERR_PARAM_INVALID161002self 的 shape 维度大于 8
ACLNN_ERR_PARAM_INVALID161002out 的 size 不等于 bins

这些校验逻辑在源码中有严格对应:

  • 空指针检查CheckNotNull对应错误码 161001(aclnn_histc.cpp);
  • 数据类型合法性CheckDtypeValid、类型互转CheckPromoteType、取值范围CheckValueRange(含bins <= 0min > max)、shape 约束CheckShape(含维度上限与 size 校验)均返回 161002,最终由CheckHistcParams按顺序串起完整的校验链(aclnn_histc.cpp);
  • 额外的合法性保护:CheckMinMaxIsInfNan会拒绝非法inf/nan作为min/max(仅允许min == max == +infmin == max == -inf的退化场景,此时交由自动极值重算处理,aclnn_histc.cpp)。

aclnnHistc 参数说明

第二段接口负责在 Device 上真正执行计算,共 4 个参数:

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

从实现上看,aclnn_histc.cpp 的第二段接口会记录 DFX 日志后直接调用CommonOpExecutorRun(workspace, workspaceSize, executor, stream)完成执行,调用方需保证 workspace 已按第一段接口返回的workspaceSize通过aclrtMalloc在 Device 侧申请。

约束说明:确定性计算

aclnnHistc的确定性行为与硬件平台相关,直方图统计本质上是多核并行的原子累加操作,不同平台默认策略不同:

  • Atlas A2 训练系列产品 / Atlas A2 推理系列产品、Atlas A3 训练系列产品 / Atlas A3 推理系列产品、Atlas 推理系列产品、Atlas 训练系列产品:默认确定性实现,相同输入必然得到逐位一致的输出;
  • Ascend 950PR / Ascend 950DT:默认非确定性实现,支持通过aclrtCtxSetSysParamOpt开启确定性计算。

该差异在 tiling 数据结构中也能观察到:histogram_v2_tiling.h 针对确定性 / 非确定性、UB 是否整块加载、输出为 fp32 等组合注册了大量专用 tiling key(如HistogramV2_101~_107为默认 int32 输出确定性版本,HistogramV2_1111/1117等为 fp32 输出的确定性版本,HistogramV2_2111/2117为 SIMD 确定性 fp32 输出版本),kernel 入口 histogram_v2.cpp 则按TILING_KEY将不同的数据类型组合分派到对应的HistogramV2Scalar模板实例。若业务对多次运行结果的一致性有强要求,请在 Ascend 950 上显式开启确定性开关。

端到端调用示例

下面代码来自 examples/test_aclnn_histc.cpp(与文档示例一致),演示了从环境初始化、构造 tensor/scalar、两段式调用到结果回拷与资源释放的完整流程。具体编译与执行方式可参考编译与运行样例。

#include <iostream> #include <vector> #include "acl/acl.h" #include "aclnnop/aclnn_histc.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 = {3, 3}; std::vector<int64_t> outShape = {3}; void* selfDeviceAddr = nullptr; void* outDeviceAddr = nullptr; aclTensor* self = nullptr; aclScalar* min = nullptr; aclScalar* max = nullptr; aclTensor* out = nullptr; std::vector<float> selfHostData = {1, 2, 3, 4, 5, 6, 7, 8, 9}; std::vector<float> outHostData = {0, 0, 0}; int64_t bins = 3; float minValue = 1.0f; float maxValue = 9.0f; // 创建self aclTensor ret = CreateAclTensor(selfHostData, selfShape, &selfDeviceAddr, aclDataType::ACL_FLOAT, &self); CHECK_RET(ret == ACL_SUCCESS, return ret); // 创建min aclScalar min = aclCreateScalar(&minValue, aclDataType::ACL_FLOAT); CHECK_RET(min != nullptr, return ret); // 创建max aclScalar max = aclCreateScalar(&maxValue, aclDataType::ACL_FLOAT); CHECK_RET(max != nullptr, 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; // 调用aclnnHistc第一段接口 ret = aclnnHistcGetWorkspaceSize(self, bins, min, max, out, &workspaceSize, &executor); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnHistcGetWorkspaceSize 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); } // 调用aclnnHistc第二段接口 ret = aclnnHistc(workspaceAddr, workspaceSize, executor, stream); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnHistc 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(self); aclDestroyScalar(min); aclDestroyScalar(max); aclDestroyTensor(out); // 7. 释放Device资源,需要根据具体API的接口定义修改 aclrtFree(selfDeviceAddr); aclrtFree(outDeviceAddr); if (workspaceSize > 0) { aclrtFree(workspaceAddr); } aclrtDestroyStream(stream); aclrtResetDevice(deviceId); aclFinalize(); return 0; }

示例中几个关键点需要留意:

  • workspace 申请以第一段接口返回值为准:即使workspaceSize为 0,也应保留条件分支判断,避免对 0 大小内存执行aclrtMalloc/aclrtFree
  • 输出结果需显式回拷aclnnHistc是异步任务,必须先用aclrtSynchronizeStream同步,再通过aclrtMemcpy将 Device 侧结果拷回 Host;
  • 示例运行结果self为 3x3 的{1,...,9}bins=3min=1max=9时,三个等宽区间各落入 3 个元素,打印结果为result[0] is: 3.000000result[1] is: 3.000000result[2] is: 3.000000

底层实现路径:从 aclnn 到 Kernel

从源码结构看,aclnnHistc的完整执行链路可分为四层,理解这条链路有助于排查问题与评估性能:

  1. 接口校验与构图层(op_api/aclnn_histc.cpp):完成全部入参校验,将self转连续张量、min/max标量转张量,并把Histogram算子注册进 executor 的启动列表;
  2. L0 算子分发层(op_api/histogram.cpp):l0op::Histogram根据当前 NPU 架构与数据类型做双路分发:
    • AiCore 路径IsAiCoreSupport返回 true,覆盖 Atlas A2、Ascend 950、Atlas 310P 且类型在支持列表内):走ADD_TO_LAUNCHER_LIST_AICORE启动 Vector/SIMT 内核。值得注意的是,输出中间类型为INT32(950 上可支持 FLOAT 输出),最终再统一Castout声明的类型(histogram.cpp);
    • AiCPU 路径(兜底):通过ADD_TO_LAUNCHER_LIST_AICPU启动 AiCPU 实现,FLOAT16 输入会先提升为 FLOAT 计算(histogram.cpp);
  3. Tiling 计算层(op_host/histogram_v2_tiling.cpp 及 histogram_v2_tiling.h):在 Host 侧读取平台 AIV 核数、UB 内存大小、libapi workspace 大小等编译期信息,依据bins、输入数据长度与平台约束计算出 former/tail 分块、UB 循环次数、参与计算的核数等切分参数;
  4. Kernel 执行层(op_kernel/histogram_v2.cpp 与 histogram_v2_scalar.h):按TILING_KEY将 7 种输入数据类型(FLOAT、INT32、INT8、UINT8、INT16、INT64、FLOAT16)实例化为对应的标量累加内核;arch35 目录下的 SIMD/SIMT 模板(如histogram_v2_simd_full_load_det_fp32out.hhistogram_v2_simt_full_load.h)则承载 950 的确定性/非确定性、fp32 输出等变体。

对于需要更高层集成的场景,仓库还提供了两条补充路径:

  • 图模式(GEIR)调用:通过 test_geir_histogram_v2.cpp 与 histogram_v2_proto.h 中的算子 IR 构图方式调用,图模式下的算子定义(输入x/min/max、输出y、可选属性binsy_dtype)见 histogram_v2_def.cpp;
  • 算子属性默认值:图模式下bins为可选属性,默认值为 100y_dtype默认 INT32,输出y初始化为 0(histogram_v2_def.cpp)。而 aclnn 接口中bins为必传参数,调用时需显式指定。

测试与验证参考

仓库为aclnnHistc提供了完整的测试资产,可作为自测与二次开发的参考:

  • OP API 单元测试:tests/ut/op_api/test_aclnn_histc.cpp 与 Python golden 脚本 aclnnHistc.py,后者以torch.histc为参照生成期望值;
  • OP Host 单元测试:tests/ut/op_host/test_histogram_v2_infershape.cpp 验证 shape 推导,arch22/arch35 下各有 test_histogram_v2_tiling.cpp 验证 tiling 计算;
  • OP Kernel 单元测试:tests/ut/op_kernel/test_histogram_v2.cpp 配合 gen_data.py 构造多类型、多 shape 的输入数据;
  • ST 测试:tests/st/aclnnHistc/atk_aclnnHistc.json 与 executor_aclnnHistc.py 覆盖端到端场景,arch35 下还有 ttk_e2e_histogram_v2_145.csv 等批量用例矩阵。

小结

aclnnHistc以简洁的两段式接口封装了直方图统计的完整实现:第一段接口负责参数校验与 workspace 规划,第二段接口完成流式执行;内部通过 AiCore/AiCPU 双路径分发适配不同硬件,并针对min == max、空张量、非连续存储、inf/nan 边界等场景做了精细处理。使用时可重点把握三类约束——bins > 0min <= maxout一维且元素个数等于bins,同时结合目标平台的确定性策略与输出类型支持(950 支持 FLOAT 输出,其余平台为 INT32)进行合理设计。算子完整定义、示例与测试代码均位于 math/histogram_v2 目录下,可供进一步查阅。

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

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

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

直流电源精度真相:分辨率不等于精度

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/20 16:55:16

GD32H759工控平台存储与交互子系统实战:SDRAM、SDIO与触摸屏调试

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/20 16:48:48

Zbrush高效雕刻必备核心快捷键整理与练习指南

简介&#xff1a;对于ZBrush用户而言&#xff0c;快捷键熟练度直接与建模效率挂钩。这份PDF系统整理了ZBrush常用快捷键&#xff0c;覆盖视图操控、笔刷切换、模型编辑、工具面板调用等高频操作&#xff0c;并附有使用要点。内容按基本操作、编辑、模型处理、其他功能划分&…

作者头像 李华
网站建设 2026/9/20 16:47:51

600美元以内DIY开源四足机器人:树莓派+舵机方案全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华