ATVOSS Exp 指数运算符:从表达式模板到昇腾硬件的一元运算实践指南
2026/9/18 16:28:43 网站建设 项目流程

ATVOSS Exp 指数运算符:从表达式模板到昇腾硬件的一元运算实践指南

【免费下载链接】atvossATVOSS(Ascend C Templates for Vector Operator Subroutines)是一套基于Ascend C开发的Vector算子库,致力于为昇腾硬件上的Vector类融合算子提供极简、高效、高性能、高拓展的编程方式。项目地址: https://gitcode.com/cann/atvoss

Exp 是 ATVOSS(Ascend C Templates for Vector Operator Subroutines)面向 Vector 算子编程提供的一元运算符接口,用于在 Compute 表达式中直接表达"以自然常数 e 为底的指数运算"。本文以 docs/api/Exp.md 为骨架,结合仓库源码与测试用例,完整讲解 Exp 的函数原型、参数语义、张量/标量两种调用方式,并深入其表达式模板机制与 AscendC 底层映射原理,帮助读者在昇腾硬件上写出正确、可运行的指数运算算子。

功能说明

Exp 运算符完成数学上的指数运算:

out[i] = e ^ in[i]

即以自然常数 e(约 2.71828)为底,对输入张量的每个元素(或标量)求指数。在算子开发中,Exp 是 softmax、sigmoid、指数加权平均等经典融合算子的基础计算单元,也是 ATVOSS 官方接口列表(见 docs/api/README.md 表 2 Operator 接口列表)中明确列出的内置运算符之一。

所属头文件与声明机制

Exp 的声明位于 include/operators/math_expression.h,该文件集中定义了 ATVOSS 的算术与超越函数运算符(Add、Sub、Mul、Div、Power、Divs、Sqrt、Exp、Abs、Cast、Max 等)。

与 Power、Divs 等"带标量模板参数"的运算符采用手写定义不同,Exp、Sqrt、Abs 这类纯一元运算统一通过DeclareUnaryOp宏声明:

DeclareUnaryOp(Sqrt); DeclareUnaryOp(Exp); DeclareUnaryOp(Abs);

宏的完整定义位于 include/expression/expr_template.h,展开后等价于生成以下三部分:

// 1) 运算符结构体:继承一元运算符基类 template <typename T> struct OpExp : UnaryOp<T> { OpExp() = default; constexpr OpExp(T t) : UnaryOp<T>(t) {} }; // 2) 张量重载:输入是 Expression<T> 表达式对象 template <typename T> __host_aicore__ constexpr auto Exp(Expression<T> lhs) { return Expression<OpExp<T>>{{lhs.data}}; } // 3) 标量/通用重载:输入是普通值 template <typename T> __host_aicore__ constexpr auto Exp(T&& lhs) { return Expression<OpExp<T>>{{std::forward<T>(lhs)}}; }

从源码结构可以推断,__host_aicore__constexpr的标注使得该函数既可在 Host 侧编译期求值,也可在 AI Core 侧运行,这是 ATVOSS 表达式系统能够在编译期完成类型推导与参数收集的基础。

函数原型

Exp 对外暴露三类接口:

template<typename T> struct OpExp : UnaryOp<T> template<typename T> __host_aicore__ constexpr auto Exp(Expression<T> lhs) template<typename T> __host_aicore__ constexpr auto Exp(T &&lhs)

其中OpExp是表达式树中的运算节点类型,用户一般不直接构造,而是通过调用Exp(...)工厂函数自动生成。

参数说明

参数名称参数类型输入/输出数据类型参数说明默认值
T模板参数输入NAExp 操作数数据类型NA
lhs函数形参输入NAExp 左操作数,当类型是Expression<T>时是张量,当类型是T时是标量NA

参数语义与仓库实现一一对应:

  • T(模板参数):决定操作数的数据类型。结合 tests/st/test_op_exp.cpp 可知,实际测试中 T 为float;由于底层映射到AscendC::Exp,输入与输出类型应保持一致(测试中以Run<float, float>()调用,即输入、输出均为 float)。
  • lhs(函数形参):接受张量表达式或标量。当传入Atvoss::PlaceHolder生成的Expression<Param<...>>时走张量路径;当传入普通数值(如5.0f)时走标量路径。该二元分发正是通过上述两个同名重载函数实现。

返回值说明

返回值数据类型返回值说明
Expression<OpExp<T>>返回一个 OpExp 的表达式对象

返回的Expression<OpExp<T>>并不会立即执行计算,而是作为表达式树的一个节点,等待被赋值运算符接入计算图。关于Expression模板的完整语义(不可赋值给同类型对象、RetType/TensorType推导等),可参考 docs/api/UnaryOp.md 与 include/expression/expr_template.h。

底层工作原理:从表达式到 AscendC 内核指令

Exp 的表达式节点最终需要被翻译成昇腾硬件指令,这一过程由求值器(Evaluator)完成。关键实现位于 include/operators/math_evaluator.h:

1. Tile 层的 ExpAssign:直接映射 AscendC::Exp

/*! * \brief dst[i] = exp(src[i]) * \param[in] src, Input LocalTensor * \param[out] dst, Output LocalTensor */ template <typename OperationShape, typename T> __aicore__ inline void ExpAssign( AscendC::LocalTensor<T>& dst, const AscendC::LocalTensor<T>& src, OperationShape& operationShape) { AscendC::Exp(dst, src, operationShape.axis0); }

(见 include/operators/math_evaluator.h)可以看到,ATVOSS 将AscendC::Exp向量指令封装为ExpAssign,一次调用即对整段连续数据(长度为operationShape.axis0)执行指数运算。

2. Evaluator 特化:OpAssign 与 OpExp 的组合求值

template <typename T, typename U> struct Evaluator<OpAssign<T, OpExp<U>>> { using Type = void; template <typename Context> __aicore__ inline auto operator()(const OpAssign<T, OpExp<U>>& op, Context& context) const { using Dtype = Dtype_t<T>; OperationShape operationShape = GetShape<Operation::Unary>(context.argsTensors); return Atvoss::Tile::ExpAssign<OperationShape, Dtype>( Evaluator<T>{}(op.GetLhs(), context).GetUbTensor(), Evaluator<U>{}(op.GetRhs().GetData(), context).GetUbTensor(), operationShape); } };

(见 include/operators/math_evaluator.h)该特化展示了完整的调用链:

  1. GetShape<Operation::Unary>从运行时参数中取出本次参与运算的数据长度;
  2. 左操作数op.GetLhs()(即outPlaceHolder)通过Evaluator<T>解析出目标 UB Tensor;
  3. 右操作数op.GetRhs().GetData()(即Exp(in)中的in)解析出源 UB Tensor;
  4. 最后交给ExpAssign执行AscendC::Exp

同理,OpExp节点本身继承自UnaryOp<T>,其DataTypeTensorTypeRetType等类型元信息由 include/expression/expr_template.h 中UnaryOp基类提供,供表达式编译期分析与参数收集使用。

使用示例

原文档给出了张量与标量两种等价写法,二者仅在PlaceHolder声明的参数类型上不同,计算表达式均为一行(out = Exp(...))

示例一:张量输入

template <typename InputDtype, typename OutputDtype> struct Config { struct Compute { template <template <typename> class Tensor> __host_aicore__ constexpr auto Compute() const { auto in = Atvoss::PlaceHolder<1, Tensor<InputDtype>, Atvoss::ParamUsage::IN>(); auto out = Atvoss::PlaceHolder<2, Tensor<OutputDtype>, Atvoss::ParamUsage::OUT>(); // 🔥🔥🔥 使用示例 🔥🔥🔥 return (out = Exp(in)); // 🔥🔥🔥 使用示例 🔥🔥🔥 }; }; };

示例二:标量输入

template <typename InputDtype, typename OutputDtype> struct Config { struct Compute { template <template <typename> class Tensor> __host_aicore__ constexpr auto Compute() const { auto scalar = Atvoss::PlaceHolder<1, InputDtype, Atvoss::ParamUsage::IN>(); auto out = Atvoss::PlaceHolder<2, Tensor<OutputDtype>, Atvoss::ParamUsage::OUT>(); // 🔥🔥🔥 使用示例 🔥🔥🔥 return (out = Exp(scalar)); // 🔥🔥🔥 使用示例 🔥🔥🔥 }; }; };

两处代码的使用要点:

  • PlaceHolder 位序PlaceHolder<N, T, U>中的N必须与运行时ArgumentsBuilder::inputOutput()传入实参的顺序一一对应、从 1 开始编号(详见 docs/api/PlaceHolder.md);
  • 标量声明方式:标量 PlaceHolder 的第二模板参数直接传数据类型(如float)而非Tensor<Dtype>,见示例二中的Atvoss::PlaceHolder<1, InputDtype, Atvoss::ParamUsage::IN>()
  • 返回表达式Compute()返回out = Exp(in)这一赋值表达式对象,ATVOSS 后续会据此自动推导输入输出参数集合与求值顺序。

端到端可运行验证:基于测试用例

仓库提供了 Exp 的完整可运行测试 tests/st/test_op_exp.cpp,展示了从 ACL 初始化到结果校验的完整流程。其核心结构如下:

template <typename T1, typename T2> struct ExpConfig { struct ExpCompute { template <template <typename> class Tensor> __host_aicore__ constexpr auto Compute() const { auto in = Atvoss::PlaceHolder<1, Tensor<T1>, Atvoss::ParamUsage::IN>(); auto out = Atvoss::PlaceHolder<2, Tensor<T2>, Atvoss::ParamUsage::OUT>(); return (out = Exp(in)); }; }; using ArchTag = Atvoss::Arch::DAV_3510; using BlockOp = Atvoss::Ele::BlockBuilder<ExpCompute, ArchTag>; using KernelOp = Atvoss::Ele::KernelBuilder<BlockOp>; using DeviceOp = Atvoss::DeviceAdapter<KernelOp>; };

ExpConfig展示了 ATVOSS 的分层组装方式:Compute表达计算逻辑 →BlockBuilder生成 block 层 →KernelBuilder生成 kernel 层 →DeviceAdapter生成 device 层可运行对象。

运行时流程(对应测试中Run<float, float>()的十个步骤):

  1. aclInit初始化 ACL 运行时,注册aclFinalize释放守卫;
  2. aclrtSetDevice(0)绑定设备;
  3. 创建 Context 与 Stream;
  4. 通过aclrtMalloc为输入输出分配设备内存(各 8 个元素);
  5. aclrtMemcpy将 Host 数据拷入设备;
  6. 构造Atvoss::Tensor<T>ArgumentsBuilder{}.inputOutput(t1, t2).build()
  7. 实例化DeviceOp并调用deviceOp.Run(arguments, stream)执行;
  8. 同步 Stream 后将结果拷回 Host;
  9. 与 golden 数据比对(输入为 0,期望exp(0) = 1),打印验证结果。

该测试验证了 Exp 的数值正确性:当输入张量元素全为 0 时,输出应为全 1,即VerifyResults(golden, hostOutput)判定通过。同时它也证明了 Exp 表达式经 Block/Kernel/Device 三层构建后可以直接在昇腾设备上运行。

约束与注意事项

原文档中 Exp 的"约束说明"为 NA,但结合仓库源码,实际使用时仍应注意以下几点:

  1. 类型一致性ExpAssign底层调用AscendC::Exp(dst, src, count),要求源与目标 LocalTensor 的数据类型一致(如均为 float),跨类型使用应先在表达式中插入Cast(参考 docs/api/Cast.md);
  2. 禁止存储右值引用Expression<T>UnaryOp<T>内部均有static_assert(!std::is_rvalue_reference_v<T>, ...)编译期检查,禁止将右值引用存入表达式对象(见 include/expression/expr_template.h 与 include/expression/expr_template.h);
  3. 标量/张量混用:Exp 是单操作数运算,输入只能有一个张量或标量,不存在二元运算中的"双标量"限制(对比 Add/Mul 的OpAdd's inputs not accepts all scalar types断言);
  4. 宏展开命名DeclareUnaryOp(Exp)会同时生成OpExp结构体与Exp工厂函数,命名空间为Atvoss,使用时应保持Atvoss::Exp前缀或using namespace Atvoss

总结

本文围绕 docs/api/Exp.md 完整梳理了 ATVOSS Exp 指数运算符:

  • 接口层面Exp提供张量、标量两种重载,统一返回Expression<OpExp<T>>,与PlaceHolderParamUsage配合即可在Compute()中一行表达指数运算;
  • 实现层面DeclareUnaryOp宏生成表达式节点,Evaluator<OpAssign<T, OpExp<U>>>特化将表达式树翻译为AscendC::Exp向量指令,完成从模板元编程到昇腾硬件的落盘;
  • 验证层面:tests/st/test_op_exp.cpp 提供了从 ACL 初始化、参数构建、算子执行到精度校验的完整参考实现。

对于需要在昇腾硬件上实现 softmax、sigmoid 等含指数运算的融合算子开发者,Atvoss::Exp是开箱即用的基础运算单元,可在此基础上继续叠加 Add、Mul、Div 等运算符组合出任意复杂度的计算表达式。更多接口可查阅 docs/api/README.md 中的完整接口列表。

【免费下载链接】atvossATVOSS(Ascend C Templates for Vector Operator Subroutines)是一套基于Ascend C开发的Vector算子库,致力于为昇腾硬件上的Vector类融合算子提供极简、高效、高性能、高拓展的编程方式。项目地址: https://gitcode.com/cann/atvoss

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

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

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

立即咨询