news 2026/9/18 6:56:09

CANN ATVOSS 算子开发指南:PlaceHolderTmpLike 临时对象占位符详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
CANN ATVOSS 算子开发指南:PlaceHolderTmpLike 临时对象占位符详解

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 中基于LocalVarUseListAllocInserter/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>>{}; } }

实现要点:

  1. 编译期约束static_assert(IsParam_v<L>, ...)强制要求参照对象L必须是Param类型——即PlaceHolderTmpLike只能"参照"由PlaceHolder声明的参数对象来创建临时对象,错误信息明确指出 "A LocalVar can only be like a Param";
  2. 类型推导二选一:当T为默认的void时,临时对象类型取自L::Type(即被参照参数的原始类型);当显式给出T时,临时对象类型即为T
  3. 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—— 临时对象类型

可显式指定临时对象的数据类型,支持基础类型(如floatint32_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; ... };

要点:

  • LocalVarParam结构同源,都具备Type/RetType/TensorType/number元数据,并额外带有Like字段指向其仿照的参数;
  • LocalVar不能是引用类型(编译期static_assert拦截);
  • LocalVar本身不提供赋值运算,赋值只能发生在"表达式层",即Expression<LocalVar<...>>之间——这保证了所有对临时对象的读写都进入表达式系统、由后端统一线性化与调度,而不是在用户侧直接产生内存操作。

在表达式语法层面,LocalVarParam一样可以作为赋值左侧(Expression::operator=允许IsParam_v<T> || IsLocalVar_v<T> || 左值引用),也可以作为算术运算的操作数,完全融入 ATVOSS 的表达式 DSL。


五、约束说明

原文档标注的约束为 NA(无额外约束),但从源码实现可确认以下编译期约束属于固有语义,使用时须注意:

  1. L必须是 ParamPlaceHolderTmpLike的参照对象必须由PlaceHolder创建,不能以另一个LocalVar或裸类型作为参照(static_assert强制);
  2. N从 1 开始连续编号:一个 Compute 表达式内全部LocalVar的编号必须为1, 2, 3, ...连续递增,否则触发 "LocalVars must be numbered sequentially from 1" 静态断言;
  3. 类型安全LocalVar不允许为引用类型,Tvoid时自动继承参照参数类型。

六、使用示例:AddSub 融合算子

原文档给出了一个完整的 AddSub 融合算子示例:输入in1in2(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)in1PlaceHolder<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.cpptest_tile_rms_norm_17.cpp等多个文件,均以auto temp = Atvoss::PlaceHolderTmpLike<1>(in1);开头,可作为批量参考。


八、PlaceHolderTmpLike 与 PlaceHolder 的对比

维度PlaceHolderPlaceHolderTmpLike
功能定义参数对象(算子输入/输出/标量)定义临时对象(中间结果)
对应 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 实参)吗?

不能直接对应。算子输出必须由PlaceHolderParamUsage::OUT(或IN_OUT)声明,临时对象只承担表达式中途的"暂存"职责,最终结果必须通过赋值语句写入Param输出。


十、延伸阅读

  • PlaceHolder:参数对象占位符,与本文的临时对象占位符配套使用;
  • include/expression/expr_template.h:PlaceHolderTmpLikePlaceHolderLocalVarParam及全部表达式操作符的源码定义;
  • 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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/18 6:55:02

医学图像配准形变场可视化:从位移场到雅可比折叠检测

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/18 6:54:17

辞职不是解药:算清情绪、成本与时机再决定

不是劝你别辞职&#xff0c;而是劝你先想清楚再辞。我做职场内容这几年&#xff0c;见过太多把辞职挂在嘴边的人&#xff0c;也见过真的辞职之后过得更好的人。区别从来不在“辞”这个动作本身&#xff0c;而在辞之前有没有把账算明白、把牌看清楚。太多人把辞职当成了解决所有…

作者头像 李华
网站建设 2026/9/18 6:52:44

IM系统选型避坑指南:功能、性能与隐藏成本全解析

1. IM选型背后的"装修陷阱"现象去年接手公司IM系统重构项目时&#xff0c;我本以为选个SDK是件简单事——就像装修房子选建材&#xff0c;看参数对比下价格就能定。结果在实际选型过程中踩的坑&#xff0c;比我家装修时遇到的还多。这才发现IM SDK选型就像装修中的&q…

作者头像 李华
网站建设 2026/9/18 6:51:20

Linux core dump从入门到实战:配置、调试与生产环境最佳实践

1. core dump到底是什么&#xff0c;为什么关键时刻它能救命先聊点实在的。做Linux下C/C开发或者运维的朋友&#xff0c;大概率都见过类似这样的输出&#xff1a;Segmentation fault (core dumped)或者是Java服务崩溃时日志里的那句&#xff1a;Failed to write core dump. Cor…

作者头像 李华