CANN ops-nn Heaviside 阶跃激活算子:原理、接口与 NPU 实现解析
CANN ops-nn Heaviside 阶跃激活算子原理、接口与 NPU 实现解析【免费下载链接】ops-nn本项目是CANN提供的神经网络类计算算子库实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-nnHeaviside赫维赛德阶跃函数是神经网络中一类特殊的二值激活函数负输入映射为 0正输入映射为 1输入恰好为 0 时输出由用户指定的 values 决定。本文以 CANN 开源算子库 ops-nn 中的 activation/heaviside 模块为主线完整讲解该算子的数学定义、产品支持范围、参数与约束、aclnn 两段式调用方式与图模式构图方式并结合op_host、op_kernel、op_api源码剖析其在 NPU 上的 tiling 策略与向量指令实现帮助开发者在 Ascend 平台上正确、高效地使用或移植该算子。一、功能说明与数学定义Heaviside 算子的功能是对输入张量 input 中的每个元素逐元素element-wise计算 Heaviside 阶跃函数并将结果作为激活函数的输出。它接收两个输入张量 input 与 values输出 out其计算公式为$$ \text{Heaviside}(\text{input}, \text{values}) \begin{cases} 0, \text{如果 input} 0 \ \text{values}, \text{如果 input} 0 \ 1, \text{如果 input} 0 \end{cases} $$从公式可以看出Heaviside 与常见的 ReLUmax(0, x)不同ReLU 在x 0时统一输出 0而 Heaviside 在x 0时输出的是用户传入的 values这就允许模型在零点处显式指定一个非零激活值是一种更灵活的二值门控。该算子在 op_api 头文件 与 算子 IR 定义 中的功能描述完全一致。二、产品支持情况根据 README 与 aclnnHeaviside 接口文档 的说明Heaviside 算子在以下产品上支持情况如下产品是否支持Ascend 950PR / Ascend 950DT√Atlas A3 训练系列产品 / Atlas A3 推理系列产品√Atlas A2 训练系列产品 / Atlas A2 推理系列产品√Atlas 200I/500 A2 推理产品×Atlas 推理系列产品×Atlas 训练系列产品×Atlas Kirin X90 处理器系列产品√Atlas Kirin 9030 处理器系列产品√注意两点差异在 Atlas Kirin X90、Atlas Kirin 9030 上不支持 BFLOAT16数据类型详见下文参数说明与 op_host/heaviside_def.cpp 中 Kirin 专属配置仅声明FLOAT16、FLOAT的佐证从源码角度看算子注册的 AICore 配置AddConfig(ascend910b)、AddConfig(ascend910_93)、AddConfig(ascend950)、AddConfig(kirinx90)、AddConfig(kirin9030)与上述支持矩阵一一对应配置文件位于 op_host/config 目录下。三、参数说明Heaviside 算子共有 3 个张量参数参数详情如下表参数名输入/输出/属性描述数据类型数据格式input输入公式中的输入 inputBFLOAT16、FLOAT16、FLOATNDvalues输入公式中的输入 valuesinput 为 0 时采用的输出值BFLOAT16、FLOAT16、FLOATNDout输出公式中的 Heaviside 计算结果BFLOAT16、FLOAT16、FLOATND其中Atlas Kirin X90 处理器系列产品、Atlas Kirin 9030 处理器系列产品不支持 BFLOAT16仅支持 FLOAT16 与 FLOAT。这一限制同样体现在算子定义文件 heaviside_def.cpp 中Kirin 平台的OpAICoreConfig只注册了ge::DT_FLOAT16与ge::DT_FLOAT两种数据类型而通用配置则注册了ge::DT_BF16 / ge::DT_FLOAT16 / ge::DT_FLOAT三种。在 aclnn 接口层面aclnnHeaviside.md还额外规定了input 支持 0~8 维、支持空 Tensor、支持非连续 Tensor数据格式为 NDvalues 支持 0~8 维、支持空 Tensor、支持非连续 Tensor数据格式为 NDout 的数据类型与输入保持一致其 shape 应为 input 与 values 经 broadcast 后的 shapeinput 与 values 之间需要满足 broadcast 关系。四、约束说明使用 Heaviside 算子时需满足以下约束来自 README数据类型一致input、values、out 的数据类型必须一致维度范围input 支持 1~8 维aclnn 接口层面支持 0~8 维values 形态限制values 必须为标量单元素或与 input 的 shape 相同。kernel 内部不进行广播而是按相同的线性偏移读取 values——也就是说当 values 与 input 同 shape 时逐位置一一对应读取当 values 为单元素时所有位置都使用该单一值。这一约束在 tiling 阶段被显式校验从 heaviside_tiling.cpp 的GetValuesType()可以看到仅当valuesShapeSize 1标量或valuesShape inputShape同 shape时才返回 true否则 tiling 直接返回失败。tiling 数据中的valuesType字段定义于 heaviside_tiling.h用于标记 values 是否为标量kernel 侧据此选择不同的取值路径。五、调用方式总览Heaviside 算子支持两种调用方式见 README 的调用说明调用方式调用样例说明aclnn 调用test_aclnn_heaviside.cpp通过 aclnnHeaviside 接口方式调用 Heaviside 算子图模式调用-通过 算子 IR 构图方式调用 Heaviside 算子5.1 图模式调用算子 IR图模式调用依赖 heaviside_proto.h 中通过REG_OP注册的 Heaviside 算子 IRREG_OP(Heaviside) .INPUT(input, TensorType({DT_BF16, DT_FLOAT16, DT_FLOAT})) .INPUT(values, TensorType({DT_BF16, DT_FLOAT16, DT_FLOAT})) .OUTPUT(output, TensorType({DT_BF16, DT_FLOAT16, DT_FLOAT})) .OP_END_FACTORY_REG(Heaviside)该 IR 声明了算子的两个输入 input、values 与一个输出 output数据类型均为 BF16/FP16/FP32格式为 NDshape 支持 0D~8D。开发者可基于该 IR 在 GEGraph Engine图中插入 Heaviside 节点完成构图调用。5.2 aclnn 调用两段式接口aclnn 接口采用 CANN 标准的两段式two-phase调用模式完整机制可参考 two_phase_api.md先调用第一段接口aclnnHeavisideGetWorkspaceSize完成入参校验并获取 workspace 大小与执行器再调用第二段接口aclnnHeaviside真正下发计算。// 第一段获取 workspaceSize 与执行器 aclnnStatus aclnnHeavisideGetWorkspaceSize( const aclTensor *input, const aclTensor *values, aclTensor *out, uint64_t *workspaceSize, aclOpExecutor **executor) // 第二段执行计算 aclnnStatus aclnnHeaviside( void *workspace, uint64_t workspaceSize, aclOpExecutor *executor, aclrtStream stream)第一段接口 aclnnHeavisideGetWorkspaceSize 参数参数名输入/输出描述使用说明数据类型数据格式维度(shape)非连续 TensorinputaclTensor*输入公式中的 input支持空 Tensor需与 values 满足 broadcast 关系BFLOAT16、FLOAT16、FLOATND0-8√valuesaclTensor*输入input 为零时使用的值支持空 Tensor需与 input 满足 broadcast 关系BFLOAT16、FLOAT16、FLOATND0-8√outaclTensor*输出输出的张量数据类型与输入一致shape 与 broadcast 后结果一致BFLOAT16、FLOAT16、FLOATND0-8√workspaceSizeuint64_t*输出返回需要在 Device 侧申请的 workspace 大小-----executoraclOpExecutor**输出返回 op 执行器包含算子计算流程-----第一段接口的返回码入参校验第一段接口会完成入参校验出现以下场景时报错返回码错误码描述ACLNN_ERR_PARAM_NULLPTR161001传入的输入参数input、values或输出参数 out 是空指针ACLNN_ERR_PARAM_INVALID161002输入/输出参数的数据类型不在支持范围之内ACLNN_ERR_PARAM_INVALID161002输入参数 input 和 values 的 shape 无法做 broadcastACLNN_ERR_PARAM_INVALID161002输出参数 out 的 shape 与输入参数经过 broadcast 后的 shape 不一致关于返回码的完整说明可参见 aclnn_return_code.md。第二段接口 aclnnHeaviside 参数参数名输入/输出描述workspace输入在 Device 侧申请的 workspace 内存地址workspaceSize输入在 Device 侧申请的 workspace 大小由第一段接口获取executor输入op 执行器包含了算子计算流程stream输入指定执行任务的 Stream确定性aclnnHeaviside默认采用确定性实现即相同输入在多次运行中产生完全一致的结果不会引入随机或不确定的中间计算路径。六、完整调用示例仓库提供了可直接参考的完整样例 examples/test_aclnn_heaviside.cpp样例编译与执行过程请参考 compile_and_run_sample.md。下面给出核心流程拆解。6.1 完整示例代码#include iostream #include vector #include acl/acl.h #include aclnnop/aclnn_heaviside.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::vectorint64_t shape) { int64_t shapeSize 1; for (auto i : shape) { shapeSize * i; } return shapeSize; } int Init(int32_t deviceId, aclrtStream* stream) { // 固定写法acl初始化 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::vectorT hostData, const std::vectorint64_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::vectorint64_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对外接口列表 // 根据自己的实际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::vectorint64_t selfShape {2, 4}; std::vectorint64_t valuesShape {2, 4}; std::vectorint64_t outShape {2, 4}; void* selfDeviceAddr nullptr; void* valuesDeviceAddr nullptr; void* outDeviceAddr nullptr; aclTensor* self nullptr; aclTensor* values nullptr; aclTensor* out nullptr; std::vectorfloat selfHostData {-1.0, -1.0, 0, 0, 0, 0, 5, 7}; std::vectorfloat valuesHostData {-1.0, -1.0, 0, 1, 2, 3, 4, 5}; std::vectorfloat outHostData {0, 0, 0, 0, 0, 0, 0, 0}; // 创建self aclTensor ret CreateAclTensor(selfHostData, selfShape, selfDeviceAddr, aclDataType::ACL_FLOAT, self); CHECK_RET(ret ACL_SUCCESS, return ret); // 创建values aclTensor ret CreateAclTensor(valuesHostData, valuesShape, valuesDeviceAddr, aclDataType::ACL_FLOAT, values); 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 uint64_t workspaceSize 0; aclOpExecutor* executor; // 调用aclnnHeaviside第一段接口 ret aclnnHeavisideGetWorkspaceSize(self, values, out, workspaceSize, executor); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(aclnnHeavisideGetWorkspaceSize 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;); } // 调用aclnnHeaviside第二段接口 ret aclnnHeaviside(workspaceAddr, workspaceSize, executor, stream); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(aclnnHeaviside 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侧 auto size GetShapeSize(outShape); std::vectorfloat resultData(size, 0); ret aclrtMemcpy(resultData.data(), resultData.size() * sizeof(resultData[0]), outDeviceAddr, size * sizeof(float), 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 aclDestroyTensor(self); aclDestroyTensor(values); aclDestroyTensor(out); // 7.释放device资源 aclrtFree(selfDeviceAddr); aclrtFree(valuesDeviceAddr); aclrtFree(outDeviceAddr); if (workspaceSize 0) { aclrtFree(workspaceAddr); } aclrtDestroyStream(stream); aclrtResetDevice(deviceId); aclFinalize(); return 0; }6.2 示例运行结果推导示例中输入self [-1, -1, 0, 0, 0, 0, 5, 7]values [-1, -1, 0, 1, 2, 3, 4, 5]按公式计算前两个元素 -1 0输出 0、0中间四个元素为 0输出对应位置的 values0、1、2、3最后两个元素 5、7 0输出 1、1。最终期望输出为[0, 0, 0, 1, 2, 3, 1, 1]。七、源码级实现原理7.1 Host 侧算子定义与 shape 推导op_host/heaviside_def.cpp 通过OP_ADD(Heaviside)注册算子定义input、values、output 三个张量均声明为必选参数通用平台支持DT_BF16 / DT_FLOAT16 / DT_FLOAT三种数据类型、ND 格式且启用AutoContiguous()Kirin 平台单独配置FLOAT16 / FLOAT。op_host/heaviside_infershape.cpp 实现 shape 与 dtype 推导InferShape4Heaviside输出 shape 直接取输入 input 的 shape*yShape *xShape即输出与 input 同 shapeInferDataType4Heaviside输出数据类型直接继承输入 input 的数据类型。这与values 需与 input 同 shape 或为标量、kernel 不广播的语义一致——输出 shape 由 input 决定不随 values 变化。7.2 Host 侧tiling 策略op_host/heaviside_tiling.cpp 是算子的 tiling切分逻辑核心要点如下数据量上限单核处理元素数上限MAX_ELEMENT_NUM_EACH_CORE 8 * 1024核数计算GetNeedCoreNum按CeilA2B(inputShapeSize, 8*1024)向上取整计算所需核数并钳制在平台 AIV 核数platformInfo.GetCoreNumAiv()之内最后通过SetBlockDim(needCoreNum)下发values 形态判定GetValuesType()判定 values 是标量valuesShapeSize 1valuesType true还是与 input 同 shapevaluesType false并将结果写入 tiling 数据供 kernel 选择不同的读取路径workspace固定申请16 * 1024 * 102416MB大小的 workspace写入tilingContext-GetWorkspaceSizes(1)tiling 数据HeavisideTilingData定义于 heaviside_tiling.h包含elementNum总元素数、valuesTypevalues 是否标量、needCoreNum所需核数三个字段通过SaveToBuffer序列化到 tiling buffer 中。7.3 Kernel 侧向量指令实现Kernel 入口定义于 op_kernel/heaviside.cpp实际计算在 op_kernel/heaviside.h 的HeavisideNDT模板类中完成核心计算逻辑Compute使用 AscendC 向量指令实现分段函数NAN 处理利用Compare(x, x, EQ)中 NAN ! NAN 的特性将 NAN 标记出来随后用Select将 NAN 替换为 -1避免 NAN 干扰后续比较生成两个选择掩码selMaskOneCompareScalar(x, 0, NE)标记非零元素位置即 0或 0的位置selMaskTwoCompareScalar(x, 0, LE)标记非正元素位置即 0的位置分段赋值Maxs(x, 0)将负元素钳到 0Select(selMaskTwo, x, 1)将 0的元素替换为 0合并了 0输出 0 与 0的占位最后Select(selMaskOne, x, values)将非零元素保留即负元素为 0、正元素为 1而零元素替换为对应 values——其中 values 为标量时走VSEL_TENSOR_SCALAR_MODEvalues 与 input 同 shape 时走VSEL_TENSOR_TENSOR_MODEBF16 路径由于向量计算统一提升到 float 精度执行BF16 输入先Cast到 float 参与运算输出前再Cast回 BF16RoundMode::CAST_RINT其他类型则直接参与运算流水并行通过CopyInAndCastMTE2 搬运→Compute向量计算→CastAndCopyOutMTE3 写出三级流水并使用SetFlag/WaitFlagEVENT_ID0/EVENT_ID1实现乒乓缓冲与跨流水硬件同步数据切分Process按PP_ELEMENT_NUM非 Kirin 平台 8K 元素、Kirin 平台 6K 元素分块将总块数均分到各核块号不足的部分由末核处理余数。从该实现可以推断Heaviside 是一个按元素纯并行的访存密集型算子其性能瓶颈主要在 GM 与 UB 之间的搬运带宽tiling 与流水设计均围绕最大化带宽利用率展开。7.4 二进制配置op_host/config 目录按产品存放二进制算子配置heaviside_binary.json以 ascend910b/heaviside_binary.json 为例声明了 float32、bfloat16、float16 三种 dtype 的编译产物Heaviside_xxx二进制文件输入输出均为shape: [-2]动态 shape的 ND 格式张量format_match_mode: FormatAgnostic与算子定义保持一致。7.5 测试覆盖仓库为 Heaviside 提供了较为完整的测试Host 侧单测tests/ut/op_host 下包含test_heaviside_infershape.cppshape/dtype 推导、test_heaviside_tiling.cpptiling 结果以及 op_api 层的test_aclnn_heaviside.cppKernel 侧单测tests/ut/op_kernel 下的test_heaviside.cpp配合 heaviside_tiling_def.h 在__CCE_UT_TEST__模式下运行数据由 heaviside_data 中的gen_data.py生成、compare_data.py对比ST 测试tests/st/aclnnHeaviside/atk_aclnnHeaviside.json 用于端到端功能验证。八、小结本文围绕 CANN ops-nn 的 Heaviside 算子从数学定义、产品支持、参数与约束、两种调用方式aclnn 两段式接口与图模式 IR、完整可运行示例到 Host/Kernel 源码实现做了系统梳理。核心要点可归纳为Heaviside 是三值分段激活函数input 0输出 0input 0输出 1input 0输出用户指定的 valuesvalues 必须是标量或与 input 同 shapekernel 不做广播按相同线性偏移读取支持 BFLOAT16 / FLOAT16 / FLOAT 三种类型与 ND 格式Kirin 系列不支持 BFLOAT16aclnn 接口采用aclnnHeavisideGetWorkspaceSizeaclnnHeaviside两段式调用第一段完成校验并返回 workspace 大小实现上采用按核均分的 tiling、UB 乒乓流水与 Compare/Select/Maxs 向量指令组合并对 NAN 做了显式处理输出具备确定性。开发者可直接复用 examples/test_aclnn_heaviside.cpp 作为接入模板并结合 README 与 aclnnHeaviside.md 核对具体产品与版本支持情况。【免费下载链接】ops-nn本项目是CANN提供的神经网络类计算算子库实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-nn创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考