前言

在人工智能算力需求高速增长的背景下,昇腾AI处理器作为华为面向推理和训练场景推出的高性能AI芯片,已经在国内外众多数据中心和边缘场景中得到了广泛部署。支撑昇腾硬件上层生态的核心软件栈是CANN(Compute Architecture for Neural Networks),它提供了从算子开发、图编译到运行时调度的完整底层能力。长期以来,基于CANN进行昇腾NPU上的Vector类融合算子开发,要求开发者深入掌握Ascend C编程模型、复杂的Tiling切分策略以及多核并行调度的细节,这带来了相当高的技术门槛和学习成本。ATVOSS(Ascend C Templates for Vector Operator Subroutines)正是为解决这一痛点而生的开源项目,它构建于Ascend C之上,以表达式模板技术为核心,提供了一套声明式的Vector算子编程框架,使开发者能够用极少的代码量描述复杂的融合算子逻辑,同时保持接近手工优化的运行性能。

ATVOSS项目概述

ATVOSS是华为CANN团队在AtomGit开源平台上发布的一套基于Ascend C的Vector算子模板库,全称为Ascend C Templates for Vector Operator Subroutines。该项目的核心目标是为昇腾硬件上的Vector类融合算子提供极简、高效、高性能、高拓展的编程方式。从项目定位来看,ATVOSS并非要取代Ascend C,而是对Ascend C的一次上层封装与抽象升华,它将开发者从繁重的底层硬件细节中解放出来,让他们能够将精力集中在计算逻辑本身。

该项目于2025年11月首次上线,当前版本为2.0,主要支持Ascend 950PR和Ascend 950DT两款昇腾硬件产品。CANN版本要求为8.5.0及以上,编译器需要GCC 7.3.0或更高版本,构建系统则依赖CMake 3.16.0。ATVOSS采用分层架构设计,从上到下依次为Device层、Kernel层、Block层、Tile层和Basic层,每一层各司其职,抽象程度逐层递减。这种设计思路借鉴了经典软件工程中的分层模型,使得框架既具备良好的可维护性,又为性能调优保留了充足的底层操作空间。

昇腾Vector算子开发的历史背景与挑战

传统Ascend C开发模式

在ATVOSS出现之前,基于昇腾NPU开发Vector算子的标准方式是直接使用Ascend C API。Ascend C是CANN提供的异构编程模型,它将AI Core的并行计算能力抽象为一组C++接口,开发者通过编写Kernel函数来描述在NPU上执行的具体计算逻辑。这种方式的优势在于灵活性极高,开发者可以精确控制每一次数据搬运和每一条计算指令的调度;劣势同样明显——过度的灵活性反而成了一种负担。Vector类算子通常具有高度规则的计算模式,例如逐元素运算(Elementwise)、归约运算(Reduce)和广播运算(Broadcast),这些模式的核心逻辑其实高度相似,但用Ascend C直接实现时,开发者仍然需要手写完整的数据分块、Tiling策略制定、双缓冲流水线编排等样板代码。

以一个简单的逐元素乘法为例,传统Ascend C开发流程需要依次完成:定义输入输出Tensor的Global Memory布局、计算符合AI Core片上存储限制的Tile形状、编写数据从Global Memory到Local Memory的搬运代码、编排计算与搬运的双缓冲流水线、编写计算Kernel本身、处理多核间任务分配、编写Host侧调用代码完成ACL初始化和数据搬运。一个原本数学表达式仅需"out = in1 * in2"的算子,最终的Ascend C实现可能包含数百行代码,其中大部分是工程模板而非真正的业务逻辑。

Tiling切分的复杂性

Tiling策略是Vector算子开发中最具技术挑战性的环节之一。由于AI Core的片上存储(Local Memory)容量有限,而输入数据通常远大于片上可用空间,因此需要将数据切分为多个Tile块,分批加载到片上执行计算。Tile形状的选择直接影响数据复用率和计算效率:过大的Tile会导致片上存储溢出,过小的Tile则会增加数据搬运的次数和开销。在实际开发中,开发者需要综合考虑数据类型大小、Tensor维度、硬件缓存层级、广播维度对齐等多个因素,反复试验才能找到一个接近最优的Tiling参数。这个过程既耗时又需要丰富的硬件知识,对于刚接触昇腾平台的开发者而言是巨大的障碍。

多核并行调度的门槛

昇腾AI处理器采用多核架构,计算任务需要在多个AI Core之间合理分配。传统的Ascend C编程要求开发者手动完成多核间的任务分解:分析计算任务的并行度、将总数据量均匀或非均匀地分配到各个核心、协调核心间的数据依赖关系、处理负载不均衡情况下的动态调整策略。这些工作不仅需要开发者对硬件架构有深入了解,还需要相当多的调试经验才能确保多核间的正确协同和高效利用。

ATVOSS的分层架构设计

Device层:Host侧调用总入口

Device层是ATVOSS框架的最高层,它承担着与Host侧(CPU端)交互的全部职责。在这一层中,核心工作是参数校验、ACL资源管理、Host与Device之间的数据管理、计算任务的切分与调度,以及最终的Kernel调用和同步。Device层的设计理念是让这一系列复杂的准备工作对开发者完全透明。开发者通过DeviceAdapter这一统一入口使用Device层功能,无需关心ACL初始化的具体流程、无需手动管理内存分配释放的时序、也无需理解Workspace的具体工作机制。

DeviceAdapter是Device层的核心抽象,它封装了aclInit、aclrtSetDevice、aclrtMalloc等底层API的调用序列,同时提供了一套统一的算子执行接口。当开发者构造好算子的输入输出Tensor后,只需调用DeviceOp::Run方法,Device层会自动完成从参数校验到Kernel调用的完整链路。这种设计使得开发者从琐碎的底层基础设施工作中抽身出来,专注于算子逻辑本身。

Kernel层:多核并行任务分解

Kernel层负责在多个AI Core之间进行任务分解和控制Block调度。这一层需要分析计算任务的总并行度,确定可用的AI Core数量,此后将近计算任务合理分配到每个核心。ATVOSS在Kernel层提供了KernelBuilder模板类,通过KernelPolicy配置核数和分段策略。开发者可以选择默认的UniformSegment(均匀分段)策略,由框架自动将数据划分为等大小的块分配到各个核心;也可以根据算子的具体特性选择动态负载均衡策略,确保计算资源的高效利用。

Kernel层的另一个重要职责是协调多核间的数据依赖关系。在某些需要跨核通信的场景中,Kernel调度器会处理数据依赖图,生成正确的执行顺序。对于大多数规则的Vector运算而言,各核心的计算完全独立,不需要任何跨核同步,这正是Vector算子能够获得高并行效率的根本原因。ATVOSS充分利用了这一特性,在Kernel层实现了高效的无冲突任务分配机制。

Block层:单核内部Tile块编排

Block层处理单个AI Core内部的Tile块计算。每个AI Core获得属于自己的数据块后,Block层负责将其进一步划分为适合片上存储的Tile单元,此后再编排数据搬运和计算的流水线执行。BlockBuilder模板类接收Compute结构体(定义计算逻辑)、ArchTag(目标架构标签)、blockPolicy(分块策略)等参数,生成可在单核内执行的BlockOp。

Block层引入了TPipe(传输管道)的概念来实现计算与搬运的并行化。通过双缓冲技术(Double Buffer),Block调度器可以让下一次Tile的数据搬运与当前Tile的计算同时进行,从而充分隐藏数据搬运的延迟。开发者无需手工编写复杂的双缓冲调度代码,只需通过BlockPolicy指定分块形状,框架即可自动生成高效的单核执行流水线。这种自动化流水线编排的能力,是ATVOSS在保持易用性的同时能够提供高性能的关键所在。

Tile层:Ascend C API的声明式封装

Tile层是ATVOSS与Ascend C底层API的直接接口层,也是开发者实际编写算子逻辑的层次。Tile层封装了VecIn、VecOut等数据搬运接口,以及Add、Mul、Sqrt、ReduceSum、Broadcast等计算操作。开发者通过表达式模板技术,以声明式的方式描述计算逻辑:定义输入输出占位符,编写计算表达式,交由框架在编译期生成完整的类型化抽象语法树(AST)。

Tile层的设计哲学是让常见操作开箱即用,同时保留足够的灵活性以支持定制化需求。ATVOSS提供了一系列Assign函数(AddAssign、SqrtAssign、DivsAssign等),开发者可以直接调用这些函数组合出复杂的算子逻辑。对于更特殊的需求,Tile层也支持与Basic层的底层API混合使用,在不破坏框架整体结构的前提下满足个性化的高性能实现需求。

Basic层:Ascend C底层能力支撑

Basic层直接使用Ascend C的基础API,是整个架构的底层支撑。虽然在日常开发中开发者几乎不需要直接接触这一层,但Basic层的存在保证了ATVOSS框架的性能上限——当Tile层提供的标准组件无法满足特定算子的极致性能需求时,开发者可以绕过Tile层,直接在Basic层编写高度定制化的计算逻辑。这种渐进式的开放策略,使得ATVOSS既能服务广大普通开发者,又能满足高级用户在极致性能场景下的定制需求。

极简编程:表达式模板技术的应用

表达式模板的核心原理

ATVOSS采用表达式模板(Expression Template)技术实现声明式算子描述。这一技术的本质是在编译期构建类型化的计算图。传统的C++表达式求值是在运行时进行的,而表达式模板通过模板元编程将表达式的结构信息编码到类型系统中,使得编译器能够在编译期就了解到整个计算的数据依赖关系和操作类型。在实际编译过程中,编译器根据模板参数推导出完整的数据流图,生成高度优化的机器码,由于计算图的结构在编译期已经完全确定,运行时几乎不存在额外的调度开销。

以RMSNorm算子为例,传统实现可能需要数十行代码描述归约、求和、开方、除法等操作及其之间的数据依赖关系;而在ATVOSS中,开发者只需定义三个PlaceHolder占位符(分别对应输入、权重和输出),此后用一条返回语句描述计算表达式即可。编译器自动分析表达式中的ReduceSum和Broadcast操作,推导出正确的数据搬运路径;自动识别除法操作的维度广播需求,生成对应的广播策略;自动计算中间结果的存储位置,避免不必要的全局内存访问。

从需求到代码的最小化路径

使用ATVOSS开发一个Vector算子的典型流程包含以下步骤:导入头文件、定义TileShape、编写Compute结构体、配置BlockPolicy和KernelPolicy、实例化DeviceOp、在主函数中准备输入数据并调用DeviceOp::Run。核心的开发工作量集中在Compute结构体的编写上,而这部分代码通常不超过二十行。相比直接使用Ascend C的开发方式,ATVOSS将代码量缩减了数倍甚至一个数量级,同时将出错概率最高的Tiling计算和多核调度逻辑交由框架自动处理。

ATVOSS还通过一套统一的主函数模板(example_common.h)进一步简化了开发流程。这套模板封装了ACL初始化、内存分配释放、数据拷贝等通用操作,开发者无需重复编写这些样板代码,可以直接使用模板中定义好的辅助函数完成算子调用。这种"约定优于配置"的设计理念贯穿ATVOSS的整个架构,使得开发者能够以最小的心智负担完成高质量的算子开发。

核心API体系与使用方式

参数占位符与类型系统

ATvoss的参数系统围绕PlaceHolder展开。PlaceHolder是一个模板化的占位符类型,它接受三个模板参数:参数索引(决定输入输出Tensor在参数列表中的位置)、Tensor数据类型、以及参数用途(IN、OUT或INOUT)。通过类型系统而非运行时参数传递来确定参数角色,ATVOSS将参数信息在编译期就固定下来,为后续的优化提供了充分的信息基础。

PlaceHolder的另一个重要特性是它支持泛型Tensor类型。开发者无需为每种数据类型(float、half等)分别实现算子,ATvoss的模板机制会自动为每种数据类型生成对应的实例化代码。这种基于模板的多态性是ATVOSS实现类型无关编程的核心机制。

运算API概览

ATVOSS提供了丰富的运算API,覆盖了Vector算子开发中的常见场景。数学运算包括Add、Mul、Sqrt、Div、Exp、Log等基本函数;归约运算包括ReduceSum、ReduceMax、ReduceMin等;广播运算包括Broadcast(支持不同维度间的自动对齐);比较运算包括Equal、Greater、Less等。此外,框架还提供了类型转换API,可以在一个计算表达式中完成跨数据类型的运算需求。

这些API的设计遵循了与NumPy和Eigen等主流数值计算库相似的接口语义,降低了熟悉数值计算的开发者学习和迁移的成本。例如,ReduceSum操作用来对Tensor的某个或某几个维度进行求和归约;Broadcast操作用于处理维度不匹配时的自动扩展场景。在表达式模板的框架下,多个运算可以链式组合,编译器会自动合并相邻的兼容运算(如连续的两个Mul操作),生成融合后的单一计算Kernel。

策略配置接口

ATVOSS的策略配置通过BlockPolicy和KernelPolicy两个核心结构体完成。BlockPolicy控制单核内部的分块策略,通过TileShape指定每个Tile块的大小形状。KernelPolicy控制多核间的任务分配策略,支持UniformSegment(均匀分段)和动态负载均衡等多种模式。

// 配置块级策略:指定分块形状
static constexpr Atvoss::Ele::DefaultBlockPolicy<TileShape> blockPolicy{TileShape{}};
// 配置核级策略:使用均匀分段
static constexpr Atvoss::Ele::DefaultKernelPolicy kernelPolicy{
    Atvoss::Ele::DefaultSegmentPolicy::UniformSegment
};

BlockPolicy和KernelPolicy采用编译期配置而非运行时参数的设计,是因为策略参数直接影响计算图的结构和代码生成策略。将策略选择在编译期固定,可以让编译器进行更激进的优化,同时避免运行时分支判断带来的性能开销。对于不同的硬件架构和算子特性,开发者只需修改模板参数即可切换策略,无需改动计算逻辑代码。

// 定义BlockOp
using BlockOp = Atvoss::Ele::BlockBuilder<
    MulsCompute,
    ArchTag,
    blockPolicy,
    Atvoss::Ele::DefaultBlockConfig,
    Atvoss::Ele::DefaultBlockSchedule>;
// 定义KernelOp
using KernelOp = Atvoss::Ele::KernelBuilder<
    BlockOp,
    kernelPolicy,
    Atvoss::Ele::DefaultKernelConfig,
    Atvoss::Ele::DefaultKernelSchedule>;
// 定义DeviceOp
using DeviceOp = Atvoss::DeviceAdapter<KernelOp>;

采用类型别名链式构造(BlockOp → KernelOp → DeviceOp)而非单一大模板的设计,好处在于每一层的构造都是独立且可复用的。BlockOp可以与不同的KernelPolicy组合以适配不同并行度需求,KernelOp也可以替换BlockOp的实现而无需修改上层调用代码。这种可组合性使得ATVOSS的扩展成本极低——添加一种新的调度策略或计算模式,只需实现对应的Builder模板即可接入现有框架。

auto arguments = Atvoss::ArgumentsBuilder{}
    .inputOutput(in, scalar, out)
    .build();

using DeviceOp = typename MulsConfig<TensorDtype, ScalarDtype>::DeviceOp;
DeviceOp deviceOp;
deviceOp.Run(arguments, stream);

ArgumentsBuilder采用流式接口(Fluent API)模式组织输入输出参数,好处在于调用代码的可读性极高——参数的数量和顺序一目了然,且编译期会进行参数类型的严格检查。与直接传递结构体指针相比,流式接口在保持类型安全的同时提供了更友好的使用体验。此外,将参数构建与算子执行分离为两个独立的步骤,允许开发者在调用前对参数进行额外的验证或预处理。

编译执行与验证体系

编译架构与工具链

ATVOSS项目的编译系统基于CMake构建,默认使用CANN提供的bisheng编译器(基于LLVM的昇腾专用编译器)。编译时需要指定目标NPU架构(如dav-3510对应Ascend 950系列),编译选项中开启-xasc参数以启用昇腾特定优化。在CMake配置中,开发者需要正确指定ATVOSS头文件路径、CANN安装路径以及所需的链接库(ascendcl、platform、register、tiling_api、runtime等)。

项目根目录下的scripts/build.sh脚本提供了便捷的一键编译能力,支持通过命令行参数指定目标硬件平台和编译目标类型(example、UT、ST等)。这种脚本化的构建入口降低了开发者的操作复杂度,特别是在需要频繁修改代码并重新编译调试的场景中,一行命令即可完成从源码到可执行文件的完整流程。

仿真执行与精度校验

对于没有物理昇腾硬件的环境,ATVOSS支持通过CANN Simulator(cannsim)进行仿真执行。仿真器可以精确模拟AI Core的指令执行过程和数据访问行为,开发者无需真实硬件即可验证算子的功能正确性。执行仿真时,将正常运行脚本写入一个Shell文件,在该脚本中配置参数后使用cannsim record命令启动仿真并生成执行报告。如果算子运行成功且精度校验通过,输出将显示"Accuracy verification passed."的信息。

仿真执行的另一个重要价值在于调试能力。开发者可以在仿真环境下逐步追踪计算过程,检查中间结果的正确性,定位数据布局或Tiling策略中存在的问题。这种"离线调试"的能力对于在开发初期快速迭代算子实现具有不可替代的作用。

UT与ST测试体系

ATVOSS在tests目录下提供了完整的UT(单元测试)和ST(系统测试)用例集。UT测试主要验证算子的单核逻辑正确性,覆盖各种输入形状和数据类型的组合。ST测试则覆盖完整的端到端流程,包括多核并行执行和大规模数据场景。通过build.sh脚本可以一键编译并运行这些测试用例,确保每次代码修改后算子仍然保持正确的行为。

使用前vs使用后:效率与体验全面对比

以下表格从多个维度对比了直接使用Ascend C原生API开发Vector算子与使用ATVOSS模板库进行开发的核心差异,涵盖开发效率、代码复杂度、性能表现和维护成本等关键指标。

对比维度 直接使用Ascend C原生API 使用ATVOSS模板库 改善幅度
实现一个简单逐元素算子所需代码行数 200至400行 15至30行 减少约90%代码量
开发者需要掌握的硬件知识深度 需深入理解Tiling、Local Memory、多核调度 专注于计算逻辑表达 门槛大幅降低
手动编写Tiling策略 必须,复杂且易出错 框架自动处理 无需手动介入
多核并行任务分配 需手动实现核间任务分解 框架策略自动分配 框架自动完成
双缓冲流水线编排 需手写完整调度逻辑 BlockBuilder自动生成 无需手工编写
代码可复用性 低,不同算子间大量重复代码 高,计算逻辑与框架分离 模块化复用
编译后端优化空间 依赖开发者手动优化 表达式模板编译期优化 优化由编译器完成
学习曲线 陡峭,需数月才能熟练 平缓,数天可上手 学习周期缩短80%以上
运行时调试复杂度 高,需追踪多核协同和内存访问 低,算子行为可预测 调试难度显著降低
新增算子的开发周期 数天至数周 数小时至一天 开发效率提升5至10倍
框架升级对算子代码的影响 需同步更新大量底层代码 核心逻辑与框架解耦 维护成本降低
精度问题排查难度 较高,涉及多环节协同验证 框架屏蔽底层细节,定位更集中 问题定位更高效

从上述对比可以看出,ATVOSS在开发体验层面带来了质的飞跃。代码量的减少不是简单的语法简化,而是通过抽象层次的提升实现的真正工程简化——开发者不再需要关心那些与计算本质无关的工程细节。从性能角度看,ATVOSS通过表达式模板技术实现的编译期优化,能够生成与手工优化代码相近的高效指令序列,运行时开销几乎为零。这使得ATVOSS在大幅降低开发门槛的同时,并没有以牺牲性能为代价。

工程实践:典型算子开发示例

矩阵标量乘法算子muls的开发全过程

以矩阵标量乘法算子muls的开发为例,展示ATVOSS的完整使用流程。muls算子的数学表达极为简单:Y = X × scalar,即对输入矩阵的每个元素乘以一个标量值。从功能角度看,这是一个典型的一元输入加一个标量输入、输出与输入形状相同的Vector类逐元素运算;从工程角度看,它涉及多输入参数的处理、类型一致性检查以及结果的精度验证。

开发的第一步是导入必要的头文件。kernel_operator.h是Ascend C的底层API定义头文件,atvoss.h是ATVOSS框架的总头文件,example_common.h提供了ACL初始化和结果验证的通用辅助函数。第二步是定义TileShape,在ATVOSS中TileShape通过Shape模板类指定,Shape<32>表示每个Tile块处理32个数据元素——这个数值是根据昇腾AI Core的片上存储容量和性能特性精心选择的一个经验值,兼顾了数据复用率和片上存储占用。

接下来编写Compute结构体,这是整个算子开发的核心。Compute结构体中定义了PlaceHolder占位符,分别标记两个输入参数(矩阵X和标量scalar)和一个输出参数(结果矩阵Y)。在Compute()方法的返回语句中,开发者只需写出"return (out = in * scalar)"这行表达式,ATVOSS的表达式模板引擎就会自动将其展开为完整的计算图,生成相应的数据搬运、逐元素乘法和结果写回逻辑。整个Compute结构体的代码不超过十行,却完整定义了算子的全部计算语义。

策略配置部分指定了BlockPolicy和KernelPolicy,分别控制单核分块策略和多核任务分配策略。由于muls算子的各元素计算完全独立且规则,框架默认的均匀分段策略即可获得良好的多核并行效率,无需开发者进行额外的调优。此后再通过BlockBuilder、KernelBuilder和DeviceAdapter的三层链式构造,生成可在昇腾硬件上直接运行的DeviceOp。整个从需求到可执行代码的流程,在熟练使用ATVOSS的情况下,可以在数小时内完成。

RMSNorm融合算子的高效实现

RMSNorm(Root Mean Square Normalization)是Transformer架构中广泛使用的一种归一化操作,相比LayerNorm具有更低的计算复杂度。在ATVOSS中实现RMSNorm的优势尤为明显:传统的RMSNorm需要分别实现ReduceSum(计算平方和的归约)、Broadcast(将归约结果广播到所有元素)、Sqrt和Div等操作,并通过正确的数据流编排连接这些操作——这在Ascend C中需要精心设计的数据流图代码。使用ATVOSS时,开发者只需用ReduceSumPattern::AR声明一个归约操作,用BroadcastPattern::AB声明一个广播操作,用Divs声明一个标量除法操作,此后再用除法操作组合出归一化结果,整个计算逻辑在表达式层面一目了然。

ATVOSS在编译期自动识别ReduceSum和Broadcast之间的数据依赖关系,在运行时自动安排归约结果先写入临时存储再广播到计算单元的时序。这种自动化的数据流编排能力,使得复杂的融合算子开发变得异常简洁。


总结

ATVOSS代表了昇腾NPU算子开发从手工作坊式向工程化、模板化方向演进的趋势。它通过分层抽象、表达式模板和自动化策略配置三大核心技术,在开发效率和运行性能之间取得了出色的平衡。对于需要在昇腾硬件上开发Vector类融合算子的团队和个人开发者而言,ATVOSS是当前最具实用价值的选择之一——它既能大幅缩短开发周期、降低技术门槛,又能保证最终产出的算子在昇腾NPU上达到接近手工优化的性能水平。


仓库地址:https://atomgit.com/cann/atvoss

Logo

鲲鹏昇腾开发者社区是面向全社会开放的“联接全球计算开发者,聚合华为+生态”的社区,内容涵盖鲲鹏、昇腾资源,帮助开发者快速获取所需的知识、经验、软件、工具、算力,支撑开发者易学、好用、成功,成为核心开发者。

更多推荐