CANN ATVOSS 算子开发指南:PlaceHolderTmpLike 临时对象占位符详解
【免费下载链接】atvossATVOSS(Ascend C Templates for Vector Operator Subroutines)是一套基于Ascend C开发的Vector算子库,致力于为昇腾硬件上的Vector类融合算子提供极简、高效、高性能、高拓展的编程方式。项目地址: https://gitcode.com/cann/atvoss
导读
PlaceHolderTmpLike是 CANN ATVOSS(Ascend C Templates for Vector Operator Subroutines)算子库中用于在 Compute 表达式中定义临时对象的模板函数。它允许开发者以"参照某个已定义参数的类型"的方式快速声明中间变量,配合PlaceHolder一起使用,就能把一条复杂的融合算子计算链拆解成"先算中间结果、再参与后续运算"的多步表达式,从而让算子逻辑更清晰、更贴近手写 Kernel 的书写习惯。读完本文,你将掌握PlaceHolderTmpLike的完整签名、三个模板参数(N/T/L)的语义与默认行为、返回的LocalVar表达式类型,以及如何在 ATVOSS 的Compute()中用它承接中间结果并交给后端做 Buffer 复用与表达式化简。
一、为什么需要 PlaceHolderTmpLike
在 ATVOSS 中,算子开发者通过Compute()内部的表达式来声明整个融合计算过程。表达式的基本要素是"对象",而对象分为两类:
- 参数对象(Param):由 PlaceHolder 创建,对应 Host 侧通过
ArgumentsBuilder::inputOutput()传入的实参(张量或标量),代表算子真正的输入、输出; - 临时对象(LocalVar):由
PlaceHolderTmpLike创建,对应计算过程中的中间结果,不直接对应任何 Host 实参。
如果一条计算链很长(例如 RMS Norm 这类"平方和 → 求均值 → 开方 → 归一化 → 加权"的多步流程),直接写成一个超长表达式虽然可行,但可读性差,且中间结果无法显式命名复用。PlaceHolderTmpLike正是为了把这种长表达式分段命名而设计的:
auto tmp = Atvoss::PlaceHolderTmpLike<1>(in1); // 声明一个临时对象 tmp return (tmp = in1 + in2, // 先算中间结果 out = tmp - in3); // 中间结果参与后续计算这里的tmp是一个LocalVar表达式对象,在同一个 Compute 表达式中可以像普通变量一样被多次赋值、被引用。其"临时"语义在后端有完整支撑:ATVOSS 会在表达式线性化阶段(见 include/graph/expr_linearizer.h)对LocalVar进行收集、去重、编号校验,并在 Block 调度阶段为它们分配独立的内部 Buffer(见 include/elewise/block/schedule.h 中基于LocalVarUseList的AllocInserter/FreeInserter处理),做到临时空间的自动复用。
二、函数原型与所属头文件
PlaceHolderTmpLike声明于 ATVOSS 表达式模板核心头文件 include/expression/expr_template.h(约 L593-L602),函数原型如下:
template <std::size_t N, typename T = void, typename L> __host_aicore__ constexpr auto PlaceHolderTmpLike(Expression<L> /*unused*/)关键信息解读:
- 它是
constexpr函数,标注__host_aicore__,可同时用于 Host 侧编译期推导与 AI Core 侧执行环境; - 入参是一个
Expression<L>类型的表达式对象(通常是某个PlaceHolder创建的参数表达式),形参名为unused,即运行时不产生任何实际数据读写,仅用于在编译期推导模板参数L; - 返回值为
Expression<LocalVar<N, T, L>>类型(当T缺省为void时,实际返回Expression<LocalVar<N, typename L::Type, L>>)。
源码级实现
template <std::size_t N, typename T = void, typename L> __host_aicore__ constexpr auto PlaceHolderTmpLike(Expression<L> /*unused*/) { static_assert(IsParam_v<L>, "[ERROR]: [Atvoss][Expression] A LocalVar can only be like a Param"); if constexpr (std::is_void_v<T>) { return Expression<LocalVar<N, typename L::Type, L>>{}; } else { return Expression<LocalVar<N, T, L>>{}; } }实现要点:
- 编译期约束:
static_assert(IsParam_v<L>, ...)强制要求参照对象L必须是Param类型——即PlaceHolderTmpLike只能"参照"由PlaceHolder声明的参数对象来创建临时对象,错误信息明确指出 "A LocalVar can only be like a Param"; - 类型推导二选一:当
T为默认的void时,临时对象类型取自L::Type(即被参照参数的原始类型);当显式给出T时,临时对象类型即为T; LocalVar携带Like信息:生成的LocalVar<N, T, L>中第三个模板参数L记录了"仿照对象",可供后端在图构建阶段追溯临时对象与哪个参数同构,从而正确推导其形状、内存布局与生命周期(参见 include/elewise/graph/bind.h 中针对LocalVar/Param的赋值绑定与释放分析逻辑)。
三、模板参数说明
| 参数名称 | 参数类型 | 输入/输出 | 数据类型 | 参数说明 | 默认值 |
|---|---|---|---|---|---|
N | 模板参数 | 输入 | NA | 临时对象位序,从 1 开始顺序编号 | NA |
T | 模板参数 | 输入 | NA | 临时对象类型,可以是基础类型和Tensor;如果不传,使用L模板参数的类型 | void |
L | 模板参数 | 输入 | NA | 临时对象按照L指定的对象来生成,L必须是struct Param类型 | NA |
参数语义详解
N—— 临时对象位序
与PlaceHolder中参数位序的语义类似,N用于在同一 Compute 表达式中唯一标识一个临时对象,从 1 开始顺序编号。在同一表达式中可声明多个临时对象(如PlaceHolderTmpLike<1>、PlaceHolderTmpLike<2>、PlaceHolderTmpLike<3>……),它们各自独立。源码侧LocalVar<N, T, L>通过static constexpr std::size_t number = N暴露编号,LocalVars<T>与Params<T>(见 include/expression/expr_template.h L301-L337)会分别收集表达式中的全部临时对象与参数,并静态断言"必须从 1 开始连续编号":
static_assert( Atvoss::Util::All_v<InRange, UnsortedType>, "[ERROR]: [Atvoss][Expression] LocalVars must be numbered sequentially from 1");T—— 临时对象类型
可显式指定临时对象的数据类型,支持基础类型(如float、int32_t)与Tensor类型。缺省(void)时自动取参照参数L的类型。一个典型场景见 examples/muls/muls.cpp 的MulsComputePromtIn:
auto in = Atvoss::PlaceHolder<1, Tensor<TensorDtype>, Atvoss::ParamUsage::IN>(); auto scalar = Atvoss::PlaceHolder<2, ScalarDtype, Atvoss::ParamUsage::IN>(); auto out = Atvoss::PlaceHolder<3, Tensor<ScalarDtype>, Atvoss::ParamUsage::OUT>(); auto inTmp = Atvoss::PlaceHolderTmpLike<1, Tensor<ScalarDtype>>(in); // 显式指定临时对象类型 return (inTmp = Atvoss::Cast<Atvoss::CastMode::CAST_NONE, ScalarDtype>(in), out = inTmp * scalar);该示例中输入张量类型为TensorDtype,而输出是ScalarDtype,因此通过显式指定T = Tensor<ScalarDtype>,让临时对象inTmp承接一次类型转换(Cast)的结果,再参与乘法运算,实现"输入类型 → 中间类型 → 输出类型"的渐进式计算。
L—— 参照对象
L必须是struct Param类型,即由PlaceHolder<N, T, U>返回的表达式所承载的Param类型。PlaceHolderTmpLike的入参正是Expression<L>,编译器通过模板实参推导自动确定L,开发者通常无需手写。约束由源码中的static_assert(IsParam_v<L>, ...)在编译期强制保证。
四、返回值说明
| 返回值数据类型 | 返回值说明 |
|---|---|
Expression<LocalVar<N, T, U>> | 返回一个LocalVar表达式对象 |
LocalVar是 ATVOSS 表达式系统中的"局部变量"载体,定义于 include/expression/expr_template.h L72-L87:
template <std::size_t N, typename T, typename L = void> struct LocalVar { static_assert(!std::is_reference_v<T>, "[ERROR]: [Atvoss][Expression] A LocalVar must not be a reference"); using Type = T; using RetType = std::decay_t<T>; using TensorType = RetType; using Like = L; static constexpr std::size_t number = N; ... };要点:
LocalVar与Param结构同源,都具备Type/RetType/TensorType/number元数据,并额外带有Like字段指向其仿照的参数;LocalVar不能是引用类型(编译期static_assert拦截);LocalVar本身不提供赋值运算,赋值只能发生在"表达式层",即Expression<LocalVar<...>>之间——这保证了所有对临时对象的读写都进入表达式系统、由后端统一线性化与调度,而不是在用户侧直接产生内存操作。
在表达式语法层面,LocalVar与Param一样可以作为赋值左侧(Expression::operator=允许IsParam_v<T> || IsLocalVar_v<T> || 左值引用),也可以作为算术运算的操作数,完全融入 ATVOSS 的表达式 DSL。
五、约束说明
原文档标注的约束为 NA(无额外约束),但从源码实现可确认以下编译期约束属于固有语义,使用时须注意:
L必须是 Param:PlaceHolderTmpLike的参照对象必须由PlaceHolder创建,不能以另一个LocalVar或裸类型作为参照(static_assert强制);N从 1 开始连续编号:一个 Compute 表达式内全部LocalVar的编号必须为1, 2, 3, ...连续递增,否则触发 "LocalVars must be numbered sequentially from 1" 静态断言;- 类型安全:
LocalVar不允许为引用类型,T传void时自动继承参照参数类型。
六、使用示例:AddSub 融合算子
原文档给出了一个完整的 AddSub 融合算子示例:输入in1、in2(Tensor)与标量in3,计算out = (in1 + in2) - in3。其中引入PlaceHolderTmpLike<1>(in1)把in1 + in2的中间结果暂存为tmp,再参与减法。完整代码如下:
template <typename InputDtype, typename OutputDtype> struct AddSubConfig { struct AddSubCompute { template <template <typename> class Tensor> __host_aicore__ constexpr auto Compute() const { auto in1 = Atvoss::PlaceHolder<1, Tensor<InputDtype>, Atvoss::ParamUsage::IN>(); auto in2 = Atvoss::PlaceHolder<2, Tensor<InputDtype>, Atvoss::ParamUsage::IN>(); auto in3 = Atvoss::PlaceHolder<3, InputDtype, Atvoss::ParamUsage::IN>(); auto out = Atvoss::PlaceHolder<4, Tensor<OutputDtype>, Atvoss::ParamUsage::OUT>(); // 使用示例:声明参照 in1 类型的临时对象 tmp auto tmp = Atvoss::PlaceHolderTmpLike<1>(in1); return (tmp = in1 + in2, out = tmp - in3); }; }; using ArchTag = Atvoss::Arch::DAV_3510; using BlockOp = Atvoss::Ele::BlockBuilder<AddSubCompute, ArchTag>; using KernelOp = Atvoss::Ele::KernelBuilder<BlockOp>; using DeviceOp = Atvoss::DeviceAdapter<KernelOp>; }; template <typename InputDtype, typename OutputDtype> static void Run() { /* ACL init and stream create */ ... Atvoss::Tensor<InputDtype> in1(deviceIn1, {{3, 4, 0, 0, 0, 0, 0, 0}}, 2); Atvoss::Tensor<InputDtype> in2(deviceIn2, {{3, 4, 0, 0, 0, 0, 0, 0}}, 2); InputDtype in3 = 5.0; Atvoss::Tensor<OutputDtype> out(deviceOut, {{3, 4, 0, 0, 0, 0, 0, 0}}, 2); auto arguments = Atvoss::ArgumentsBuilder{}.inputOutput(in1, in2, in3, out).attr("dim", 5).build(); using DeviceOp = typename AddSubConfig<InputDtype, OutputDtype>::DeviceOp; DeviceOp deviceOp; deviceOp.Run(arguments, stream); } int main(int argc, char const* argv[]) { Run<float, float>(); return 0; }示例要点说明:
- 声明方式:
PlaceHolderTmpLike<1>(in1)中in1是PlaceHolder<1, Tensor<InputDtype>, ParamUsage::IN>返回的表达式,T缺省为void,因此tmp自动获得Tensor<InputDtype>类型; - 使用方式:
tmp先作为赋值左值接收in1 + in2的结果,再作为操作数参与tmp - in3,通过逗号运算符(,)串接成一条完整的 Compute 返回表达式;ATVOSS 的重载operator,(见 include/expression/expr_template.h L533-L537)会把多语句打包成OpAndThen链; - 位序独立性:
tmp的编号1与四个PlaceHolder的位序1~4属于两套独立编号体系,互不冲突——Param编号对应 Host 实参顺序,LocalVar编号对应临时对象顺序; - Host 侧对应关系:
ArgumentsBuilder{}.inputOutput(in1, in2, in3, out)的实参顺序与PlaceHolder的位序一一对应,而tmp不占用任何实参位。
七、典型实战场景:多步中间结果与 Buffer 复用
场景一:连续多个临时对象承接多步计算
在 tests/ut/host/test_expr_linearizer.cpp 中,测试用例一次性声明了 9 个临时对象(temp~temp8),演示了典型的多步"归一化 + 残差"计算链:
auto temp = Atvoss::PlaceHolderTmpLike<1>(in1); auto temp1 = Atvoss::PlaceHolderTmpLike<2>(in1); auto temp2 = Atvoss::PlaceHolderTmpLike<3>(in1); // ... 依此类推直到 temp8 = PlaceHolderTmpLike<9>(in1) auto xx1 = (temp = in1 * in1, temp1 = Atvoss::ReduceSum<Atvoss::Pattern::AR>(temp), temp2 = Atvoss::Broadcast<Atvoss::Pattern::AB>(temp1), temp3 = Atvoss::Divs<WIDTH>(temp2), temp4 = temp3 * in3, temp5 = temp4 + temp, temp6 = Atvoss::Sqrt(temp5), temp7 = temp6 + temp, temp8 = in1 / temp7, out = in2 * temp8, out2 = in2 + temp8, out3 = in2 / temp5);该用例同时展示了 ATVOSS 的自动临时化能力:xx1(手写LocalVar版本)与Atvoss::ToLinearizerExpr(xx)(自动版本,其中xx为无显式临时对象的长表达式)在std::is_same_v类型对比下完全一致,说明用户显式使用PlaceHolderTmpLike与框架自动缓存中间结果为LocalVar是同一套底层机制——后者的实现可见于 include/graph/expr_linearizer.h 中的OptimizeWithLocalVars(将每个非 Param 子表达式提升为LocalVar并做后续替换)。
场景二:临时对象的重复赋值与 Buffer 复用
在 tests/st/test_compute_buffer_reuse.cpp 中,tmp1/tmp2被反复交叉赋值:
auto tmp1 = Atvoss::PlaceHolderTmpLike<1>(in1); auto tmp2 = Atvoss::PlaceHolderTmpLike<2>(in1); return (tmp1 = in1, tmp2 = tmp1 + in1, tmp1 = tmp2 + in2, tmp2 = tmp1, in1 = tmp1, in2 = tmp2, out = in1 + in1);该用例验证了 compute 表达式的 Buffer 复用能力:LocalVar的生命周期由编译器按"最后一次使用"分析(参见 include/elewise/graph/bind.h 中LastAssignRhs/IsAbleToFree等机制,以及 include/elewise/block/schedule.h 中基于LocalVarUseList的分配/释放插入),因此多个临时对象可以在硬件 Buffer 空间上复用同一块内存,降低片上存储占用。类似的 buffer 复用、冗余消除测试还见于test_compute_buffer_reuse.cpp系列的 test_compute_tmp_redundant_with_autopolicy.cpp、test_compute_tmp_redundant_with_manupolicy.cpp 与 test_compute_expression_redundant_with_autopolicy.cpp。
场景三:RMS Norm 风格的真实算子中间量
在 tests/ut/host/test_arguments.cpp 的 RMS Norm 配置中,temp = PlaceHolderTmpLike<1>(in1)被用作"平方和"中间结果的载体,后续ReduceSum/Broadcast/Divs/Sqrt等逐步作用其上,最终仅以out = _11一个赋值语句收尾——这正是融合算子把整条数学链写在 Compute 中的标准写法。类似的 RMS Norm 测试用例在 tests/st 下有test_tile_rms_norm_3.cpp至test_tile_rms_norm_17.cpp等多个文件,均以auto temp = Atvoss::PlaceHolderTmpLike<1>(in1);开头,可作为批量参考。
八、PlaceHolderTmpLike 与 PlaceHolder 的对比
| 维度 | PlaceHolder | PlaceHolderTmpLike |
|---|---|---|
| 功能 | 定义参数对象(算子输入/输出/标量) | 定义临时对象(中间结果) |
| 对应 Host 实参 | 是,位序与ArgumentsBuilder::inputOutput()实参一一对应 | 否,不占实参位序 |
| 模板参数 | N(位序)、T(类型)、U(数据流向 IN/OUT/IN_OUT) | N(位序)、T(类型,可缺省)、L(参照对象,必为 Param) |
| 返回值 | Expression<Param<N, T, U>> | Expression<LocalVar<N, T, L>> |
| 类型来源 | 必须显式指定 | 缺省时自动继承参照对象类型 |
| 典型用途 | 声明in1/in2/out等参数 | 声明tmp,承接in1 + in2等中间结果 |
两者搭配使用的通用范式:
// 1. 先声明参数 auto in = Atvoss::PlaceHolder<1, Tensor<float>, Atvoss::ParamUsage::IN>(); auto out = Atvoss::PlaceHolder<2, Tensor<float>, Atvoss::ParamUsage::OUT>(); // 2. 再声明临时对象(参照某个 Param) auto tmp = Atvoss::PlaceHolderTmpLike<1>(in); // 3. 多步计算 return (tmp = /* 中间计算 */, out = /* 使用 tmp 的最终计算 */);九、常见问题(FAQ)
Q1:PlaceHolderTmpLike可以参照另一个PlaceHolderTmpLike创建的临时对象吗?
不可以。源码static_assert(IsParam_v<L>, ...)明确要求L必须是Param类型,参照对象只能是PlaceHolder创建的参数对象。
Q2:临时对象的编号需要和 PlaceHolder 的编号区分开吗?
不需要刻意区分。两者是两套独立的编号体系:PlaceHolder<N>的N对应 Host 实参顺序,PlaceHolderTmpLike<N>的N只表示"第几个临时对象"。但每个体系内部都要求从 1 开始连续编号。
Q3:一个 Compute 表达式最多能声明多少个临时对象?
没有硬性数量上限(测试用例中曾一次声明 9 个),但所有临时对象编号必须连续。实际数量受片上 Buffer 容量与调度策略制约,ATVOSS 会通过 Buffer 复用尽量压缩占用。
Q4:不写PlaceHolderTmpLike,直接用长表达式可以吗?
可以。ATVOSS 的表达式线性化(ToLinearizerExpr,见 include/graph/expr_linearizer.h)会自动将长表达式中的中间结果提升为LocalVar。但显式书写PlaceHolderTmpLike能让计算步骤一目了然,也便于在Compute()内复用同一个中间结果多次。
Q5:临时对象能直接作为算子的输出(对应 Host 侧 out 实参)吗?
不能直接对应。算子输出必须由PlaceHolder以ParamUsage::OUT(或IN_OUT)声明,临时对象只承担表达式中途的"暂存"职责,最终结果必须通过赋值语句写入Param输出。
十、延伸阅读
- PlaceHolder:参数对象占位符,与本文的临时对象占位符配套使用;
- include/expression/expr_template.h:
PlaceHolderTmpLike、PlaceHolder、LocalVar、Param及全部表达式操作符的源码定义; - include/graph/expr_linearizer.h:表达式线性化与
LocalVar自动提升/化简的实现; - include/elewise/graph/bind.h 与 include/elewise/block/schedule.h:
LocalVar的赋值绑定、Buffer 分配与释放调度; - examples/muls/muls.cpp:显式指定临时对象类型承接 Cast 结果的真实算子示例;
- tests/ut/host/test_expr_linearizer.cpp 与 tests/st/test_compute_buffer_reuse.cpp:多临时对象与 Buffer 复用行为的测试验证。
【免费下载链接】atvossATVOSS(Ascend C Templates for Vector Operator Subroutines)是一套基于Ascend C开发的Vector算子库,致力于为昇腾硬件上的Vector类融合算子提供极简、高效、高性能、高拓展的编程方式。项目地址: https://gitcode.com/cann/atvoss
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考