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 | 模板参数 | 输入 | NA | Exp 操作数数据类型 | NA |
| lhs | 函数形参 | 输入 | NA | Exp 左操作数,当类型是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)该特化展示了完整的调用链:
GetShape<Operation::Unary>从运行时参数中取出本次参与运算的数据长度;- 左操作数
op.GetLhs()(即outPlaceHolder)通过Evaluator<T>解析出目标 UB Tensor; - 右操作数
op.GetRhs().GetData()(即Exp(in)中的in)解析出源 UB Tensor; - 最后交给
ExpAssign执行AscendC::Exp。
同理,OpExp节点本身继承自UnaryOp<T>,其DataType、TensorType、RetType等类型元信息由 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>()的十个步骤):
aclInit初始化 ACL 运行时,注册aclFinalize释放守卫;aclrtSetDevice(0)绑定设备;- 创建 Context 与 Stream;
- 通过
aclrtMalloc为输入输出分配设备内存(各 8 个元素); aclrtMemcpy将 Host 数据拷入设备;- 构造
Atvoss::Tensor<T>与ArgumentsBuilder{}.inputOutput(t1, t2).build(); - 实例化
DeviceOp并调用deviceOp.Run(arguments, stream)执行; - 同步 Stream 后将结果拷回 Host;
- 与 golden 数据比对(输入为 0,期望
exp(0) = 1),打印验证结果。
该测试验证了 Exp 的数值正确性:当输入张量元素全为 0 时,输出应为全 1,即VerifyResults(golden, hostOutput)判定通过。同时它也证明了 Exp 表达式经 Block/Kernel/Device 三层构建后可以直接在昇腾设备上运行。
约束与注意事项
原文档中 Exp 的"约束说明"为 NA,但结合仓库源码,实际使用时仍应注意以下几点:
- 类型一致性:
ExpAssign底层调用AscendC::Exp(dst, src, count),要求源与目标 LocalTensor 的数据类型一致(如均为 float),跨类型使用应先在表达式中插入Cast(参考 docs/api/Cast.md); - 禁止存储右值引用:
Expression<T>与UnaryOp<T>内部均有static_assert(!std::is_rvalue_reference_v<T>, ...)编译期检查,禁止将右值引用存入表达式对象(见 include/expression/expr_template.h 与 include/expression/expr_template.h); - 标量/张量混用:Exp 是单操作数运算,输入只能有一个张量或标量,不存在二元运算中的"双标量"限制(对比 Add/Mul 的
OpAdd's inputs not accepts all scalar types断言); - 宏展开命名:
DeclareUnaryOp(Exp)会同时生成OpExp结构体与Exp工厂函数,命名空间为Atvoss,使用时应保持Atvoss::Exp前缀或using namespace Atvoss。
总结
本文围绕 docs/api/Exp.md 完整梳理了 ATVOSS Exp 指数运算符:
- 接口层面:
Exp提供张量、标量两种重载,统一返回Expression<OpExp<T>>,与PlaceHolder、ParamUsage配合即可在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),仅供参考