- 人工智能
- 算子库
- 深度学习
- CANN
- Ascend
【免费下载链接】ops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
HardShrink(硬收缩)是一种逐元素稀疏化激活函数,将输入张量中绝对值小于等于阈值 λ 的元素置零、其余元素原样保留,常用于特征稀疏化与去噪场景。本文以 CANN 开源算子库 ops-nn 中 experimental/activation/hard_shrink 模块为准,完整讲解aclnnHardShrink两段式接口的参数语义、错误码、约束与调用示例,并结合算子定义、形状推导、Tiling 与 Kernel 源码剖析其 NPU 侧的实现原理,使读者能够直接照抄示例运行算子,并理解其底层工作机制。
产品支持情况
HardShrink 算子当前的适配范围如下(支持的产品在表中以 √ 标识):
| 产品 | 是否支持 |
|---|---|
| Ascend 950PR/Ascend 950DT | √ |
| Atlas A3 训练系列产品/Atlas A3 推理系列产品 | x |
| Atlas A2 训练系列产品/Atlas A2 推理系列产品 | x |
| Atlas 200I/500 A2 推理产品 | x |
| Atlas 推理系列产品 | x |
| Atlas 训练系列产品 | x |
从仓库源码看,算子定义 hard_shrink_def.cpp 中仅注册了ascend950的 AICore 配置(this->AICore().AddConfig("ascend950", aicoreConfig)),与上述产品支持矩阵一致;对应的调用示例也全部位于arch35(Ascend 950 架构)目录下。因此,若要在其他芯片型号上运行,需先确认对应版本的算子包是否提供实现。
功能说明与计算公式
- 接口功能:完成 HardShrink 激活函数计算,将输入张量中绝对值小于等于阈值 lambd 的元素置零,大于阈值的元素保持不变。该算子在功能上对标 PyTorch 的
torch.nn.functional.hardshrink。 - 计算公式:
$$ \text{HardShrink}(x) = \begin{cases} x, & \text{if } x > \lambda \ x, & \text{if } x < -\lambda \ 0, & \text{otherwise} \end{cases} $$
其中,x 为输入张量 self 中的元素,λ 为阈值参数 lambd,默认值为 0.5。
从公式可以看出,HardShrink 的关键语义是:只有当元素严格大于 λ 或严格小于 -λ(即 |x| > λ)时才保留原值;恰等于 ±λ 的元素(例如 x = 0.5 且 lambd = 0.5)同样被置零,这体现了“硬”收缩(hard thresholding)的截断特性。该行为在 op_kernel/hard_shrink.h 的两步比较实现中得到了精确对应:第一步用Compare(x > lambd)选出大于 λ 的元素,第二步用Compare(x < -lambd)选出小于 -λ 的元素,两者均不命中(含等于 ±λ)的元素在 Select 中被置 0。
函数原型与两段式调用流程
与 CANN 其他单算子 API 一致,aclnnHardShrink采用两段式接口,必须先调用第一段接口获取 workspace 大小和包含算子计算流程的执行器,再调用第二段接口真正执行计算。两段式接口的通用约定可参见仓库文档 两段式接口说明。
aclnnStatus aclnnHardShrinkGetWorkspaceSize( const aclTensor *self, const aclScalar *lambd, aclTensor *out, uint64_t *workspaceSize, aclOpExecutor **executor)aclnnStatus aclnnHardShrink( void *workspace, uint64_t workspaceSize, aclOpExecutor *executor, aclrtStream stream)说明:
- workspace 指除输入/输出外,算子在 NPU 上完成计算所需的临时内存,workspaceSize 表示其大小,由第一段接口计算得出。
- 第二段接口
aclnnHardShrink(...)不能重复调用,同一 executor 只支持执行一次,重复调用会异常。
aclnnHardShrinkGetWorkspaceSize 参数说明
第一段接口完成入参校验并构建执行器,各参数语义如下:
| 参数名 | 输入/输出 | 描述 | 使用说明 | 数据类型 | 数据格式 | 维度(shape) | 非连续Tensor |
|---|---|---|---|---|---|---|---|
| self(aclTensor*) | 输入 | 输入张量,对应公式中的 x。 | 支持空 Tensor。 | FLOAT、FLOAT16、BFLOAT16 | ND | 0-8 | √ |
| lambd(aclScalar*) | 输入 | 阈值参数,对应公式中的 λ,默认值为 0.5。 | 不支持空指针。 | FLOAT | - | - | - |
| out(aclTensor*) | 输出 | 输出张量,与 self 同 shape 同 dtype。 | 不支持空 Tensor;数据类型需与 self 一致;shape 需与 self 一致。 | FLOAT、FLOAT16、BFLOAT16 | ND | 0-8 | √ |
| workspaceSize(uint64_t*) | 输出 | 返回需要在 Device 侧申请的 workspace 大小。 | - | - | - | - | - |
| executor(aclOpExecutor**) | 输出 | 返回 op 执行器,包含了算子计算流程。 | - | - | - | - | - |
几点实战提示:
- 空 Tensor 支持:当 self 为空 Tensor(0 元素)时,第一段接口仍会正常返回,此时
workspaceSize为 0,out同样为空 Tensor,不执行计算。此逻辑在 hard_shrink_tiling.cpp 的HandleEmptyTensor中实现(将 blockDim 置为 1 并清空 TilingData)。 - lambd 取值:lambd 为 float 类型标量,理论上取值无限制。但 Tiling 侧 GetLambdAttr 会对 NaN/Inf 打出告警日志(此时输出可能全零);同时参考主仓 op_api 实现 aclnn_hardshrink.cpp,当 lambd 为负数时会被截断为 0.0f 再传入 L0 算子。实际使用时建议传入常规正数阈值。
- 非连续 Tensor:self 与 out 均支持非连续 Tensor(表中 √)。在 op_api 实现中,输入先经
Contiguous转为连续,计算完成后通过ViewCopy将结果写回可能非连续的 out,保证语义正确。
aclnnHardShrink 参数说明
第二段接口在申请好 workspace 后执行实际计算:
| 参数名 | 输入/输出 | 描述 |
|---|---|---|
| workspace | 输入 | 在 Device 侧申请的 workspace 内存地址。 |
| workspaceSize | 输入 | 在 Device 侧申请的 workspace 大小,由第一段接口 aclnnHardShrinkGetWorkspaceSize 获取。 |
| executor | 输入 | op 执行器,包含了算子计算流程。 |
| stream | 输入 | 指定执行任务的 Stream。 |
返回值与错误码
两段接口均返回aclnnStatus状态码,完整的状态码定义可参考仓库文档 aclnn 返回码。其中与 aclnnHardShrink 直接相关的参数校验错误如下:
| 返回值 | 错误码 | 描述 |
|---|---|---|
| ACLNN_ERR_PARAM_NULLPTR | 161001 | self、lambd、out 存在空指针。 |
| ACLNN_ERR_PARAM_INVALID | 161002 | self 的数据类型不在支持的范围之内。 |
| ACLNN_ERR_PARAM_INVALID | 161002 | out 的数据类型与 self 不一致。 |
| ACLNN_ERR_PARAM_INVALID | 161002 | out 的 shape 与 self 不一致。 |
从 aclnn_hardshrink.cpp 的CheckParams校验链可以看到第一段接口的完整校验流程:先做空指针检查(CheckNotNull),再做数据类型范围检查(CheckDtypeValid),再核对 shape 一致性(CheckShape),最后检查私有格式(CheckFormat)。错误码 161001/161002 分别对应 NULLPTR 与 INVALID 两类参数问题,其余内部异常(如算子二进制包未安装导致的 561xxx 系列)可结合aclGetRecentErrMsg接口获取详细报错信息排查。
约束说明
- aclnnHardShrink 为默认确定性实现,即相同输入在多次运行中结果确定。
- self 与 out 的数据类型必须一致,支持 FLOAT、FLOAT16、BFLOAT16。
- self 与 out 的 shape 必须一致,不涉及广播。
- self 支持 0-8 维,支持空 Tensor(0 元素),此时 out 也为空 Tensor,不执行计算。
上述约束在源码中有三重印证:算子定义 hard_shrink_def.cpp 中self/out均声明为REQUIRED且数据类型限定为DT_FLOAT16/DT_FLOAT/DT_BF16;形状推导 hard_shrink_infershape.cpp 中输出 shape 直接等于输入 shape(*outputShape = *inputShape);Tiling 侧 GetInputInfo 同样对数据类型做了白名单校验。
调用示例
下面给出完整的可运行示例(源自接口文档,与仓库 examples/arch35/test_aclnn_hard_shrink.cpp 示例保持一致)。示例输入特意构造了正值、负值、0、恰好等于 ±lambd 的值以及紧邻阈值的 0.49/0.51 等边界数据,便于验证 HardShrink 的截断语义。具体编译与执行过程请参考仓库文档 编译与运行样例。
#include <iostream> #include <vector> #include <cstring> #include "acl/acl.h" #include "aclnn_hard_shrink.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; } 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); 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); 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); 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]; } *tensor = aclCreateTensor( shape.data(), shape.size(), dataType, strides.data(), 0, aclFormat::ACL_FORMAT_ND, shape.data(), shape.size(), *deviceAddr); return 0; } 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; } int main() { // 1. ACL 初始化 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. 构造输入和输出 // 输入 self: shape=[4, 4], dtype=FLOAT, 包含正值、负值和接近阈值的值 std::vector<int64_t> selfShape = {4, 4}; std::vector<float> selfHostData = { 1.0f, -1.0f, 0.3f, -0.3f, 0.5f, -0.5f, 0.0f, 2.0f, -2.0f, 0.1f, -0.1f, 10.0f, 0.49f, -0.49f, 0.51f, -0.51f }; aclTensor* self = nullptr; void* selfDeviceAddr = nullptr; ret = CreateAclTensor(selfHostData, selfShape, &selfDeviceAddr, aclDataType::ACL_FLOAT, &self); CHECK_RET(ret == ACL_SUCCESS, return ret); // 输出 out: 与 self 同 shape 同 dtype aclTensor* out = nullptr; void* outDeviceAddr = nullptr; std::vector<float> outHostData(16, 0.0f); ret = CreateAclTensor(outHostData, selfShape, &outDeviceAddr, aclDataType::ACL_FLOAT, &out); CHECK_RET(ret == ACL_SUCCESS, return ret); // lambd 标量参数(double 类型) double lambd = 0.5; // 3. 调用 aclnnHardShrink 第一段接口 uint64_t workspaceSize = 0; aclOpExecutor* executor = nullptr; ret = aclnnHardShrinkGetWorkspaceSize(self, lambd, out, &workspaceSize, &executor); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnHardShrinkGetWorkspaceSize failed. ERROR: %d\n", ret); return ret); // 4. 申请 workspace void* workspaceAddr = nullptr; if (workspaceSize > static_cast<uint64_t>(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); } // 5. 调用 aclnnHardShrink 第二段接口 ret = aclnnHardShrink(workspaceAddr, workspaceSize, executor, stream); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnHardShrink failed. ERROR: %d\n", ret); return ret); // 6. 同步等待计算完成 ret = aclrtSynchronizeStream(stream); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtSynchronizeStream failed. ERROR: %d\n", ret); return ret); // 7. 获取输出的值,将device侧内存上的结果拷贝至host侧 auto size = GetShapeSize(selfShape); 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]); } // 8. 释放资源 aclDestroyTensor(self); aclDestroyTensor(out); aclrtFree(selfDeviceAddr); aclrtFree(outDeviceAddr); if (workspaceSize > static_cast<uint64_t>(0)) { aclrtFree(workspaceAddr); } aclrtDestroyStream(stream); aclrtResetDevice(deviceId); aclFinalize(); return 0; }以 lambd = 0.5 运行上述示例,可预期的输出为:1.0、-1.0、2.0、-2.0、10.0、0.51、-0.51等满足 |x| > 0.5 的元素原样保留;0.3、-0.3、0.5、-0.5、0.0、0.1、-0.1、0.49、-0.49等元素被置为 0。其中 0.5/-0.5(恰等于阈值)与 0.49/-0.49(略小于阈值)被置零,直观验证了“绝对值小于等于阈值置零”的边界语义。
从源码看 NPU 侧实现原理
Host 侧:算子定义与形状推导
- 算子定义hard_shrink_def.cpp:声明输入
self(REQUIRED,支持 FP16/FP32/BF16,ND 格式,AutoContiguous)、输出out(约束同 self),并将 lambd 声明为Attr("lambd").AttrType(OPTIONAL).Float(0.5f)——即 lambd 以 Attr 形式经 TilingData 传递到 Kernel,而非 Kernel 的运行时参数。同时开启动态编译、动态 rank、动态 shape 支持,注册到ascend950架构。 - 形状推导hard_shrink_infershape.cpp:HardShrink 是逐元素算子,输出 shape 与 dtype 直接继承输入(
*outputShape = *inputShape),无需广播。
Host 侧:Tiling 切分策略
hard_shrink_tiling.cpp 实现了运行时切分决策,核心策略可归纳为:
- 多核切分:
blockFactor = CeilDiv(totalNum, coreNum),按 AIV 核数(GetCoreNumAiv)把总元素均分到各核,实际使用核数由CeilDiv(totalNum, blockFactor)得出。 - UB 切分:根据 UB 内存大小(
GetCoreMemSize(UB))与缓冲模式计算ubFactor(每轮循环处理的元素数),并做 256B 对齐(alignElems = 256 / computeTypeSize)。 - 双缓冲阈值:当
totalNum > 1024时BUFFER_MODE=1(双缓冲,BUFFER_NUM=2),否则单缓冲,用于隐藏搬数与计算延迟。 - bf16 特殊处理:bf16 输入统一以 float 计算,TilingKey 中
IS_BF16=1,Buffer 预算额外包含两个 float 临时缓冲区(lambdBuf/negLambdBuf/tmpBuf/tmp2Buf)。 - 空 Tensor 处理:
HandleEmptyTensor将 blockDim 置 1、TilingData 清零、workspace 置 0,并仍通过ASCENDC_TPL_SEL_PARAM完成模板参数选择,保证编译期路径完备。 - 最终 Tiling 结果(
totalNum/blockFactor/ubFactor/lambd)写入 hard_shrink_tiling_data.h 定义的HardShrinkTilingData结构体,随 Tiling 一并下发给 Kernel。
Kernel 侧:两次 Compare + Select 的向量化实现
Kernel 实现位于 op_kernel/hard_shrink.h,入口 hard_shrink.cpp 通过REGISTER_TILING_DEFAULT读取 TilingData 后实例化模板类NsHardShrink::HardShrink<D_T, BUFFER_MODE, IS_BF16>并执行。
值得关注的是,实现刻意采用“两次 Select”而非一次 Or 组合,以避免 Or API 的兼容性风险,计算流水为:
- 预填常量:
Duplicate将lambd与-lambd填充进两个 UB 常驻缓冲区(lambdBuf/negLambdBuf)。 Compare(x > lambd)生成 mask1,Select(mask1 ? x : 0)得到中间结果 tmp(保留大于 λ 的元素)。Compare(x < -lambd)生成 mask2,Select(mask2 ? x : tmp)得到最终输出(保留小于 -λ 的元素,其余沿用 tmp 中的 0 或已保留值)。
对于 BF16 路径,数据需先Cast bf16 → float参与比较与选择,计算完成后再Cast float → bf16输出(舍入模式CAST_RINT);FP16/FP32 路径则直接以原生类型计算。Compare/Select 对 count 有 256B 对齐要求,因此 Kernel 内按alignElems = 256 / sizeof(COMPUTE_T)将currentNum向上对齐后调用向量指令。模板参数组合(FP16/FP32/BF16 × 单/双缓冲)由 hard_shrink_tiling_key.h 中的ASCENDC_TPL_ARGS_DECL/ASCENDC_TPL_SEL静态展开,共 6 个编译实例。
示例与测试配套
仓库为该算子提供了多种验证入口,可用于快速回归与精度确认:
- test_aclnn_hard_shrink.cpp:aaclnn 两段式调用示例,采用
std::unique_ptr+ 自定义 deleter(RAII)管理 ACL 资源,任何路径 return 都能保证 tensor、device 内存、stream、device、acl 正确释放,是学习资源管理的良好范本。 - test_aclnn_hard_shrink_fp16.cpp / test_aclnn_hard_shrink_bf16.cpp:分别验证 FP16、BF16 数据类型。
- test_aclnn_hard_shrink_large.cpp:大 Tensor 场景,覆盖多核切分与 UB 分块路径。
总结
aclnnHardShrink是 CANN ops-nn 中实现 HardShrink 硬阈值激活的标准入口,遵循“GetWorkspaceSize + 执行”的两段式调用范式,支持 FLOAT/FLOAT16/BFLOAT16 与 0-8 维 ND 张量(含空 Tensor 与非连续 Tensor),默认确定性实现。从源码链路看,其 NPU 实现依次经历算子定义(Attr 传递 lambd)、形状推导(输出继承输入)、Tiling(多核 + UB 双缓冲切分)与 Kernel(两次 Compare+Select 的向量化逐元素运算)四层,BF16 路径额外引入 float 中间计算以保证精度。开发者可直接复用本文示例完成单算子调用,并结合 接口文档 与 模块 README 深入定制。
- 人工智能
- 算子库
- 深度学习
- CANN
- Ascend
【免费下载链接】ops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
相关推荐
CANN ops-nn 算子实战:aclnnHardshrink 两段式接口的完整开发与调用指南
CANN ops nn 算子实战:aclnnHardshrink 两段式接口的完整开发与调用指南 导读 本文围绕 CANN 神经网络算子库 ops nn 中的
人工智能算子库深度学习CANNAscendCANN ops-nn 算子开发指南:aclnnSwiGlu 两段式接口详解与 SwiGlu 激活算子 NPU 实现剖析
CANN ops nn 算子开发指南:aclnnSwiGlu 两段式接口详解与 SwiGlu 激活算子 NPU 实现剖析 SwiGlu(Swish Gated
人工智能算子库深度学习CANNAscendCANN ops-nn 算子开发指南:aclnnCelu 与 aclnnInplaceCelu 两段式接口详解与 NPU 实战调用
CANN ops nn 算子开发指南:aclnnCelu 与 aclnnInplaceCelu 两段式接口详解与 NPU 实战调用 本篇技术指南以 activa
人工智能算子库深度学习CANNAscend
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考