ATVOSS DeviceAdapter 详解:host/device 桥接层的设计原理与 Run 运行流程
【免费下载链接】atvossATVOSS(Ascend C Templates for Vector Operator Subroutines)是一套基于Ascend C开发的Vector算子库,致力于为昇腾硬件上的Vector类融合算子提供极简、高效、高性能、高拓展的编程方式。项目地址: https://gitcode.com/cann/atvoss
DeviceAdapter 是 ATVOSS 中位于 host 侧的 device 适配层类,它把用户以表达式方式书写的算子配置(KernelOp)封装为可直接在昇腾硬件上运行的实体,内部统一完成 ACL 资源管理、参数解析、Tiling 计算与 kernel 启动。本文以 DeviceAdapter.md 与 DeviceAdapter_Run.md 为骨架,结合 device_adapter.h、tiling.h、arguments.h 等源码,深入讲解其类设计、Run接口参数语义、底层五步执行流水线,并给出可直接落地的完整代码示例,帮助读者掌握 ATVOSS 算子从“表达式配置”到“device 上执行”的完整通路。
一、DeviceAdapter 的功能定位
ATVOSS(Ascend C Templates for Vector Operator Subroutines)将 Vector 融合算子的开发抽象为三个层次:
- Block 层:由
BlockBuilder承担,把单个核的任务切分为多个 tile,完成每次数据搬运与计算; - Kernel 层:由
KernelBuilder承担,负责计算 Tiling 信息、依据 block ID 确定当前核需要处理的 GM 数据,并下发给 Block; - Device 层:由
DeviceAdapter承担,构造 device 适配层对象,桥接 host 与 device。
按照 device_adapter.h 中的类注释,DeviceAdapter 是一个通用适配器(generic adapter),为不同的算子调用提供统一的 host 侧接口,内部封装了 Acl 相关的资源管理并自动处理 kernel 调用。也就是说,用户只需要面向表达式描述“算什么、怎么算”,DeviceAdapter 负责把计算描述落到真实的昇腾执行环境中。
其头文件为 device_adapter.h,文档中给出的类原型如下:
template <typename KernelOp> class DeviceAdapter{ DeviceAdapter() {}; }类模板的唯一模板参数KernelOp是用户算子配置中kernel层的静态配置与调度策略,即由Atvoss::Ele::KernelBuilder<BlockOp>实例化得到的类型。
二、模板参数 KernelOp 说明
| 参数名称 | 参数类型 | 输入/输出 | 数据类型 | 参数说明 | 默认值 |
|---|---|---|---|---|---|
| KernelOp | 模板参数 | 输入 | NA | kernel 层的用户静态配置和调度策略 | NA |
从 device_adapter.h 可以看到,KernelOp上还派生出一组关键类型别名,它们决定了 DeviceAdapter 内部如何使用该配置:
using ExprMaker = typename KernelOp::ScheduleClz::ExprMaker; using BlockOp = typename KernelOp::ScheduleClz::BlockTemplate; using OpParam = typename KernelOp::ScheduleCfgClz; template <typename T> using Tensor = DeviceTensor<T>;ExprMaker:表达式构造器,用于在 host 侧重新实例化用户的计算表达式(Compute());BlockOp:kernel 层携带的 Block 模板类型,供 Tiling 计算时逐级调用;OpParam:kernel/block 两级调度配置(Tiling 信息)的组合类型;Tensor:device 侧的张量视图,即 DeviceTensor,它持有T* ptr_指针并提供GetPtr()、operator[]等访问方式,IsTensor标记用于模板特化识别。
在 examples/abs/abs.cpp 中可以看到真实的组装顺序:先由BlockBuilder生成BlockOp,再由KernelBuilder包装成KernelOp,最终交给DeviceAdapter:
using ArchTag = Atvoss::Arch::DAV_3510; using BlockOp = Atvoss::Ele::BlockBuilder<AbsCompute, ArchTag, blockPolicy, Atvoss::Ele::DefaultBlockConfig>; using KernelOp = Atvoss::Ele::KernelBuilder<BlockOp, kernelPolicy>; using DeviceOp = Atvoss::DeviceAdapter<KernelOp>;DeviceAdapter的无参构造函数(DeviceAdapter() {})意味着该对象是轻量句柄,通常作为局部对象实例化后直接调用Run。
三、DeviceAdapter::Run 主运行接口
3.1 函数原型
template <typename Args> int64_t Run(const Args& arguments, aclrtStream stream = nullptr)Run是 device 适配层的主运行接口,负责完成host 侧的参数解析与device 侧入参数据结构对象的准备,随后触发 kernel 启动。
3.2 参数说明
| 参数名称 | 参数类型 | 输入/输出 | 数据类型 | 参数说明 | 默认值 |
|---|---|---|---|---|---|
| Args | 模板参数 | 输入 | NA | 用户的输入参数列表,类型根据用户传入的参数实例化 | NA |
| arguments | 函数形参 | 输入 | Args | 用户传入的参数列表 | NA |
| stream | 函数形参 | 输入 | aclrtStream | 用户创建的 stream 流对象 | nullptr |
其中arguments通常由Atvoss::ArgumentsBuilder构建(见 arguments.h),它实际是一个二元组:(inputOutput 元组, attrs 元组)。Run内部通过std::get<0>(arguments)取出输入输出参数元组继续处理。
3.3 返回值说明
| 返回值数据类型 | 返回值说明 |
|---|---|
| int64_t | device 层执行的结果,0:成功,-1:失败 |
当 Tiling 计算失败时,Run会打印[ERROR]: [Atvoss][Device] CalcParam failed!并返回 -1(见 device_adapter.h)。因此调用方应检查返回值以确认算子是否成功下发。
四、Run 的源码级五步执行流水线
Run的实现(device_adapter.h)可以分解为五个阶段,下面结合源码逐步展开。
4.1 表达式线性化:还原用户计算语义
auto expr = ToLinearizerExpr(ExprMaker{}.template Compute<Tensor>()); using Expr = typename decltype(expr)::Type; using Params = Atvoss::Params_t<Expr>;这里用KernelOp携带的ExprMaker在 host 侧重新执行用户定义的Compute(),得到计算表达式树后线性化(linearize),再通过Params_t<Expr>提取出全部PlaceHolder参数的静态描述(参数序号、数据类型、ParamUsage流向等)。这一步是后续参数准备与转换的“类型蓝图”。
4.2 参数准备:PrepareParams
auto argTuple = std::get<0>(arguments); auto params = PrepareParams<Params>(argTuple);PrepareParams逐个把用户传入的实参按照PlaceHolder的序号构造为DeviceTensor或标量包装对象(ConstructParam,见 device_adapter.h):
- 当参数声明为
Tensor<InputDtype>而实参是Atvoss::Tensor时,构造DeviceTensor<T>,其内部直接引用用户传入的 device 指针(ptr_ = src.data(),见 device_tensor.h); - 当参数是标量(如示例中的
in3)时,直接构造标量类型。
静态断言保证了Params的参数数量与用户实参元组大小严格一致,不一致会在编译期报错(见 device_adapter.h)。
4.3 动态参数计算:Tiling
OpParam opParam; if (!CalculateTiling<KernelOp>(arguments, opParam)) { ... return -1; }CalculateTiling定义在 tiling.h,它分两级调用调度策略:
- 先调用
KernelOp::ScheduleClz::MakeScheduleConfig(arguments, cfg.kernelParam)计算 kernel 层配置(如核数blockNum、每核处理单元数等,参见 DefaultKernelConfig); - 再调用
BlockOp::ScheduleClz::MakeScheduleConfig(arguments, cfg.kernelParam, cfg.blockParam)计算 block 层配置(如整 tile 数、尾 tile 元素数等,参见 DefaultBlockConfig)。
arguments中的 attr(如示例里的attr("dim", 5))在这里作为调度依据被消费。
4.4 参数转换:ConvertArgs 与 TransformArgs
auto convertArgs = ConvertArgs<Params>(params, argTuple);ConvertArgs依据Params中每个参数的位置与 usage,把params与原始argTuple重新对齐成最终传给 kernel 的元组。随后LaunchKernelWithDataTuple会通过TransformArgs(见 device_adapter.h)完成最终形态转换:
- 标量类型:原样转发(
std::forward<T>(value)); - Tensor 类型:取其 device 指针(
value.GetPtr())。
TransformArgs有编译期static_assert,只接受标量或IsTensor特化类型,从类型系统上杜绝了非法入参。
4.5 Kernel 启动:LaunchKernelWithDataTuple
LaunchKernelWithDataTuple<KernelOp>(opParam.kernelParam.blockNum, stream, opParam, convertArgs);启动入口是KernelCustom(device_adapter.h),它是一个__global__ __aicore__核函数,声明了KERNEL_TASK_TYPE_DEFAULT(KERNEL_TYPE_AIV_ONLY)(即 AIV 核执行),通过AscendC::Std::make_index_sequence展开参数元组并调用KernelWrapper,最终执行KernelOp op; op.Run(cfg, args...)进入 kernel 层调度(kernel/builder.h)。
此外,源码中提供ATVOSS_DEBUG_MODE == 2的性能剖析分支:此时 kernel 会连续启动 200 次(代码注释标注为 profiling run times),便于观测算子耗时;默认分支只启动一次。
五、完整使用示例
5.1 算子配置中的 DeviceAdapter
以下示例完整来自 DeviceAdapter.md 与 DeviceAdapter_Run.md,展示了一个 AddSub(out = in1 + in2 - in3)算子从 Compute 到 DeviceOp 的完整组装:
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>(); return (out = in1 + in2 - 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>; };要点说明:
PlaceHolder<N, T, ParamUsage>中的序号N决定参数在 kernel 实参列表中的位置,ParamUsage::IN/OUT/IN_OUT声明数据流向(参见 ParamUsage.md);ArchTag = Atvoss::Arch::DAV_3510指定目标昇腾芯片架构。
5.2 host 侧调用 DeviceAdapter::Run
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; }几点实战说明:
Atvoss::Tensor的构造函数为Tensor(T* dataPtr, uint64_t* inputShape, size_t dims),shape 数组固定按 8 维描述,未用到的维度补 0(见 tensor.h);示例中{{3, 4, 0, 0, 0, 0, 0, 0}}表示一个 3×4 的二维张量;ArgumentsBuilder的inputOutput只接受Atvoss::Tensor与标量类型,attr添加的键值对(如"dim")会被 Tiling 计算消费;不允许传入裸指针(有static_assert保护,见 arguments.h);stream可省略(默认nullptr),但实际使用时建议传入通过aclrtCreateStream创建的 stream 流对象,以便与其他 ACL 任务同步调度。
5.3 与 ACL 初始化流程的完整衔接
以 examples/abs/abs.cpp 为参照,一次完整的DeviceAdapter::Run调用需要包裹在标准 ACL 生命周期中:
aclInit(nullptr)初始化 ACL,结束时aclFinalize();aclrtSetDevice(deviceId)设置 device;aclrtCreateContext(&context, deviceId)创建 Context;aclrtCreateStream(&stream)创建 Stream;aclrtMalloc为输入/输出分配 device 内存(ACL_MEM_MALLOC_HUGE_FIRST);aclrtMemcpy完成 Host→Device 数据拷贝;- 用
Atvoss::Tensor包装 device 指针并构建arguments; - 实例化
DeviceOp deviceOp;并调用deviceOp.Run(arguments, stream);; aclrtSynchronizeStream(stream)同步,再aclrtMemcpy拷回结果验证。
该样例同时展示了返回值检查的实践:Run返回 0 表示 kernel 成功下发,结合 stream 同步即可安全读取输出。
六、约束说明与易错点
- 约束说明:文档标注
NA,即无额外硬件/软件约束;但使用时需保证:Atvoss::Tensor包装的是已经通过aclrtMalloc分配的 device 内存,而非 host 内存;ArgumentsBuilder::inputOutput的实参类型必须为Atvoss::Tensor或标量(禁止裸指针);- 实参数量与
Compute()中PlaceHolder的数量保持一致,否则编译期static_assert会失败; - shape 维度数在 1~8 之间(
MAX_DIMS = 8,见 tensor.h)。
- 返回值检查:务必检查
Run的int64_t返回值,0 成功、-1 失败(Tiling 计算失败时返回 -1)。 - 性能剖析:编译期定义
ATVOSS_DEBUG_MODE == 2会令Run连续启动 200 次 kernel 用于性能采样,正式发布请勿开启。
七、从文档到源码的进阶阅读路径
- 类定义与 Run 实现:device_adapter.h
- Tiling 计算:tiling.h
- Device 侧张量视图:device_tensor.h
- host 侧参数构建:arguments.h
- Kernel 层构建器:include/elewise/kernel/builder.h
- Block 层构建器:include/elewise/block/builder.h
- 端到端样例:examples/abs/abs.cpp、examples/muls/muls.cpp、examples/rms_norm/rms_norm.cpp
- 相关 API 文档:DeviceAdapter_Run.md、ArgumentsBuilder_inputOutput.md、ParamUsage.md
通过本文可以完整理解:用户在Compute()中书写表达式 →BlockBuilder/KernelBuilder逐级封装 →DeviceAdapter::Run完成表达式还原、参数准备、Tiling 计算、参数转换与 kernel 启动。掌握这条通路后,即可基于 ATVOSS 快速开发并调试自己的 Vector 融合算子。
【免费下载链接】atvossATVOSS(Ascend C Templates for Vector Operator Subroutines)是一套基于Ascend C开发的Vector算子库,致力于为昇腾硬件上的Vector类融合算子提供极简、高效、高性能、高拓展的编程方式。项目地址: https://gitcode.com/cann/atvoss
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考