前言

在昇腾NPU的生态版图中,CANN扮演着连接硬件与上层框架的核心角色。CANN的算子体系并非铁板一块,而是按照能力层次划分为多个专项仓库,其中ops-math于2025年9月正式上线,专注于补全CANN基础数学算子的覆盖空白。这个时间节点——彼时ops-nn(高阶神经网络算子)和ops-transformer(大模型Transformer算子)已经相对成熟,基础层的散装数学运算却始终是短板。ops-math的出现正是为了解决这个问题。

当前版本支持Ascend 950PR、Ascend 950DT以及KirinX90三个芯片系列,覆盖了从服务器到移动端的不同场景需求。950PR面向推理场景做了功耗优化,950DT偏向训练类负载,KirinX90则是面向麒麟芯片的NPU加速路径。三者的硬件特性差异决定了ops-math内部对不同芯片的适配逻辑各有侧重。

本文面向初次接触ops-math仓库的开发者,通过梳理仓库结构、算子注册机制以及CANN Simulator调试链路,帮助读者建立从源码到运行的完整认知链条。文章不预设读者的CANN深度,但默认具备基础的深度学习算子知识。

算子库整体架构

ops-math的目录结构遵循CANN算子仓库的一贯设计语言,顶层分为stable和experimental两大区块。stable目录收录的是经过充分验证的成熟算子,每一个算子对应一个独立子目录,包含算子原型定义(.json格式的IR描述)、Kernel实现(通常是C++文件)以及单测用例。experimental则承载两类内容:其一是正在社区评审中的新算子提案,其二是芯片厂商或第三方贡献者贡献的算子实现——这类算子尚未合并入stable,但代码已经进入仓库可以被引用和试用。

从算子功能维度划分,ops-math中的算子可分为三大类别。第一类是基础运算类,典型的如add、mul、sub、div等逐元素数学运算,这类算子是所有神经网络计算图的最底层构件,也是调优空间最大的部分。以add为例,其Kernel实现需要处理广播语义(broadcast semantics),即两个输入张量形状不同时需要按NumPy风格的广播规则扩展后再相加,广播路径的选取直接影响指令级并行的效率。第二类是数据变换类,典型的如concat、lerp、select、gather等,这类算子负责张量形状重组和条件路由,它们的共同特征是不涉及数值计算本身,瓶颈通常在内存拷贝和索引查表上。第三类是随机算子类,以dropout为典型代表,dropout在推理阶段和训练阶段的行为完全不同——推理时是恒等映射,训练时需要按概率将部分激活值置零并做缩放,这个条件分支如果写得不干净,会导致推理路径和训练路径的行为不一致,在调试时容易踩坑。

算子与CANN层的对接关系值得单独展开。ops-math中的算子在IR层面遵循统一的算子原型描述规范,定义输入输出的张量形状、数据类型和属性值。定义完成后,算子需要向ACL(Ascend Computing Language)层注册,使其能够被GE(Graph Engine)图引擎在构图阶段识别和调度。这一步通常在算子目录下的注册文件中完成,注册文件以某种约定俗成的宏展开方式将算子信息注入到CANN的全局算子表中。框架侧(如MindSpore或PyTorch ACL接入层)在构图时会查询这张表,找到对应算子的实现路径并生成Task下单指令。

算子注册与调用流程

理解ops-math中一个算子从源码到可调用的全链路,是掌握这个仓库的关键。这里以一个假设的加法算子为例,拆解整个注册与调用流程。

第一步是算子原型定义。开发者需要在算子目录下创建原型JSON文件,声明算子的类型名称、输入输出数量和顺序、属性列表以及每个张量的形状约束。以下是一段典型的原型定义框架:

{
  "op": "add",
  "input_desc": [
    {"name": "x", "shape_type": "ND", "dtype": "float16,float32"},
    {"name": "y", "shape_type": "ND", "dtype": "float16,float32"}
  ],
  "output_desc": [
    {"name": "z", "shape_type": "ND", "dtype": "float16,float32"}
  ]
}

原型定义中的dtype约束不是可选项,而是硬件寄存器位宽的直接映射。float16和float32的差异决定了算子需要调度不同的Vector Core指令序列。如果不声明支持float16,硬件的半精度计算单元就不会被利用,推理延迟会凭空增加一倍以上。
第二步是编写Kernel实现。Kernel是真正跑在NPU矢量核上的代码,通常以C++模板的形式编写以覆盖多种数据类型和形状组合。Kernel代码放在算子目录的kernel目录下,文件名以kernel_开头,CANN的编译系统会扫描并编译这些文件。

template <typename T>
__aicore__ inline void AddKernel(const LocalTensor<T>& x,
                                  const LocalTensor<T>& y,
                                  LocalTensor<T>& z,
                                  const int32_t total_len) {
    const int32_t block_size = 256;
    const int32_t tint = 8;
    for (int32_t i = 0; i < total_len; i += block_size * tint) {
        auto x_b = x[i];
        auto y_b = y[i];
        z[i] = x_b + y_b;
    }
}

NPU的矢量核执行模型和CUDA不同,它采用块级并行而非线程级并行。block_size取256、tint取8是针对Ascend矢量核的流水线深度做的经验值,盲目增大block_size会导致L1缓存溢出,盲目减小则矢量核的指令发射效率下降。这个参数组合是硬件微架构决定的,不是拍脑袋的数字。
第三步是算子注册。注册动作把算子的原型和Kernel关联起来,告知图引擎"遇到add算子时应该调用哪个.so中的哪个Kernel函数"。注册通常通过一个静态注册宏完成:

REG_OP(add)
    .INPUT(x, TensorType({DT_FLOAT, DT_FLOAT16}))
    .INPUT(y, TensorType({DT_FLOAT, DT_FLOAT16}))
    .OUTPUT(z, TensorType({DT_FLOAT, DT_FLOAT16}))
    .OP_TYPE(OP_TYPE_PRIM)
    .DYNAMIC_OP_NUM(1)
    .REGISTER_OP_LIB(addKernel, AddKernel)
END_REG_OP(add);

REG_OP宏的INPUT/OUTPUT声明顺序必须和Kernel函数签名严格对应,顺序错位会导致参数绑定错误,而这类错误在编译期完全不会报错,只会在运行时产生数据越界或结果NaN。动态算子数量(DYNAMIC_OP_NUM)默认为1,指算子在图融合时可以作为融合组的第几个算子成员,这个参数影响CANN的算子融合决策器是否愿意把这个算子和上下游算子合并执行。
端到端调用链路在框架层的视角下是这样的:用户在MindSpore或PyTorch脚本中写c = add(a, b),框架的接入层将这个调用转换为CANN的ACL API调用,ACL根据算子名称在全局表中查找到add算子的实现路径,GE图引擎在图编译阶段将算子实例化为具体的Task,Task随后被调度到NPU矢量核上执行。整个链路的延迟来源主要有三处:框架接入层的转换开销、GE图编译的优化决策耗时、以及核上执行的实际计算耗时。ops-math本身只负责收尾阶段一段,但它的算子原型定义质量会间接影响图编译阶段的融合收益。

第三方贡献者如果希望向ops-math贡献新的算子,标准路径是将实现放入experimental目录下,之后向仓库维护者提交Pull Request。experimental目录中的算子虽然不在stable中,但已经可以通过CANN的开发模式(export ASCEND_OPP_PATH=/path/to/experimental)被图引擎识别,这种设计让社区可以在不破坏稳定分支的前提下试用新算子。

CANN Simulator仿真调试

CANN Simulator是昇腾官方提供的软件仿真环境,它在x86主机上模拟NPU的指令执行行为,使开发者能够在没有物理Ascend芯片的情况下完成算子的开发、调试和验证工作。这个工具链对于大多数在非服务器环境下工作的开发者来说是刚需——毕竟不是每个人手边都有插着Ascend 910或950的物理机器。

使用Simulator调试ops-math算子的基本流程从环境变量配置开始:

export ASCEND_SIMULATOR_MODE=1
export SOC_VERSION=Ascend910
export CANN_VERSION=8.0.RC1
export PATH=$ASCEND_HOME/compiler/bin:$PATH

SOC_VERSION必须和实际目标芯片匹配,Ascend910和Ascend910B的矢量核微指令集存在差异,Simulator的行为也会因此不同。如果目标部署环境是950PR而Simulator用Ascend910模拟,部分新型矢量指令会被Simulator降级为等效序列,调试结果可能与真机不符。这不是Simulator的bug,而是它尽力逼近物理硬件的必然限制。
配置完成后,可以通过ACL接口加载和执行算子,Simulator会拦截这些调用并在软件层面模拟执行。开发者可以使用GDB或IDE的调试器对Kernel代码设置断点,观察张量数据的实际值。需要注意的是,Simulator的执行速度比真机慢数十倍到数百倍不等,大shape的算子测试用例在Simulator中运行几分钟是正常的,不要误以为是死循环。

仿真环境与真机环境的差异集中在三个层面。第一是执行精度,Simulator使用IEEE 754标准浮点运算,而部分Ascend芯片的矢量核对特殊值(如非规格化数)的处理策略与x86不一致,导致某些corner case下Simulator结果和真机结果存在微小差异。第二是内存层级,Simulator的缓存模拟是简化的软件模型,实际硬件的L1/L2缓存行大小和预取策略在Simulator中无法完全还原,因此涉及缓存敏感型数据排布的算子在Simulator中测得的性能数据没有参考价值。第三是并行调度,Simulator是单线程模拟,图引擎的多Stream并行执行在Simulator中被串行化,这对于依赖多Stream同步的算子(例如包含异步Copy的算子)会导致调试阶段无法复现真机上可能出现的竞态条件。

性能profiling方面,Simulator提供基础的算子耗时输出,但数据精度有限。真正有意义的性能调优仍然需要在真机上通过HCCS(华为集合通信组件)提供的工具链完成。Simulator的profiling更适合用来做逻辑正确性验证,而不是性能对标。

效率对比

在算子开发与调试的实际工作中,引入ops-math的标准算子实现与从零自研相比,在多个维度上存在显著差异。以下从开发效率、硬件利用率、跨芯片兼容性和调试周期四个维度进行对比。

维度 使用ops-math前 使用ops-math后 差异来源
开发周期 从原型定义到可用的成熟算子通常需要数周,包括IR设计、Kernel实现正确性验证和性能调优 直接引用stable目录中的已有算子,或基于现有算子做少量属性扩展,开发周期压缩至数天 ops-math提供了经过验证的原型模板和Kernel框架,开发者只需填充核心计算逻辑,重复性的注册和适配工作被省略
硬件指令覆盖 自研算子可能只覆盖float32路径,或因对硬件指令集不熟悉而选择了次优的向量化策略 标准算子覆盖float16/float32/float64多种数据类型,且Vector Core指令排布经过针对性调优 CANN矢量核的半精度(float16)指令吞吐量是单精度的一倍,不利用float16的算子性能损失在50%以上,ops-math的Kernel实现默认启用最优数据类型路径
多芯片适配 为Ascend 950PR/950DT/KirinX90分别适配需要理解三者的微架构差异,工作量呈线性增长 算子原型定义一次,各芯片的差异在Kernel编译阶段通过条件编译处理,适配成本大幅降低 ops-math的stable算子在三个芯片上均有CI验证覆盖,各芯片的适配逻辑收敛在统一的编译配置体系中
调试周期 无Simulator支持时需要频繁在真机上调试,每次部署周期较长,且真机资源通常需要排队竞争 Simulator支持在x86环境下完成算子逻辑验证,真机只用于收尾阶段的性能验收,调试迭代效率提升 Simulator的逻辑正确性验证能力避免了真机调试中反复编译部署的开销,尤其在算子组合和边界条件测试中收益明显

上述对比存在一个值得指出的约束条件:多芯片适配这一维度在使用ops-math后并非完全消除了工作量。不同芯片的矢量核在指令集上有细微差异,ops-math通过条件编译覆盖了这些差异,但当新芯片发布或算子需要利用新芯片的特有指令时,开发者仍需要理解底层硬件约束并编写平台特定的代码路径。这意味着ops-math解决的是通用路径的复用问题,而非彻底的零适配承诺。

Simulator仿真调试进阶

在完成了基础的环境变量配置后,算子开发者还需要掌握Simulator的更多调试能力,才能应对复杂的Kernel调试场景。aclops-build是ops-math提供的算子专用编译工具,它将算子的原型定义、Kernel实现和注册信息打包编译为可被Simulator加载的算子库文件。

aclops-build的基本调用需要在命令行中指定算子名称、算子源码路径、目标芯片版本和输出目录四个必填参数。编译流程内部依次完成四件事项。其一是对算子原型JSON文件做语法和语义校验,确保输入输出的形状约束和数据类型声明无误。其二是将Kernel的C++源码针对目标SOC版本做条件编译,生成对应芯片的矢量核机器码。其三是将注册宏展开后的注册信息链接到CANN的全局算子表中,使Simulator在运行时能够查找到算子实现。其四是生成算子库文件(.so)和配套的元数据文件(.json),前者包含Kernel的二进制实现,后者描述算子的接口签名以供框架侧调用。

编译过程中最常见的两类错误是原型定义与Kernel签名不匹配、以及条件编译的芯片宏未正确定义。前者表现为Simulator加载算子时报告"op not found",后者表现为Kernel中的芯片特定内建函数(intrinsic)编译失败。调试这两类问题时,可以先检查aclops-build的输出日志,日志中会逐阶段报告编译状态,出错阶段会被明确标注。

Simulator运行算子后会在当前目录下生成仿真输出文件,默认的命名规则为sim_result_<op_name>_.log。输出文件包含三部分内容。第一部分是算子执行摘要,记录算子名称、输入张量描述、输出张量描述以及执行耗时。第二部分是张量数据转储,以十六进制格式输出输入和输出的原始内存数据,开发者可以将这些数据和参考实现(如NumPy的计算结果)做对比来验证正确性。第三部分是Simulator的内部状态信息,包括指令级执行轨迹和异常事件记录,当算子执行出现错误结果或异常退出时,这部分信息是定位问题的核心依据。

算子性能分析与测试框架

当算子的逻辑正确性在Simulator中得到验证后,性能分析和系统化测试是上线前的两个必经环节。CANN提供了多层次的性能分析工具,帮助开发者定位算子在各硬件单元上的资源利用情况。

算子的实际执行时间可以通过HCCS的aclprof工具获取。aclprof在算子执行期间对NPU硬件计数器做采样,生成包含算子级耗时的性能报告。使用方式是在算子调用代码前后插入aclprof的启动和停止API,执行完成后性能数据会被写入指定的输出目录。报告中的Task Duration字段即为算子在NPU上的实际执行时间,需要注意区分的是,这个值不包括数据在Host和Device之间搬运的通信时间,也不包括图编译阶段的开销。如果开发者观察到Task Duration远远小于端到端的总耗时,瓶颈通常不在算子计算本身,而在数据搬运或图调度上。

UB(Unified Buffer)是NPU矢量核的高速暂存存储器,其利用率直接决定了矢量计算的效率。UB缓冲区利用率的查看需要借助CANN的msprof工具,msprof在算子执行期间对UB的读写请求做采样统计。分析报告中的UB Read Throughput和UB Write Throughput两个指标反映了UB的实际带宽利用情况,与理论峰值(通常为GB/s级别)的比值即为利用率。利用率低于50%通常意味着Kernel代码中的矢量指令编排存在问题——可能是矢量指令之间的数据依赖导致流水线断流,也可能是数据搬运指令(如DataCopy)和矢量计算指令之间的流水并行度不够。提升UB利用率的核心思路是增大计算的块大小(block_size),使矢量核在等待数据搬运完成期间有足够多的计算任务可以覆盖等待时间。

DMA(Direct Memory Access)搬运次数的统计同样在msprof的报告中进行。DMA Transaction Count字段记录了算子执行期间NPU和全局内存之间的数据搬运次数。搬运次数过多是算子性能的常见杀手,尤其当输入张量的形状导致搬运的数据块尺寸小于DMA引擎的最优传输粒度时,有效带宽会急剧下降。优化方向通常包括两方面的具体措施。其一是通过算子融合减少中间结果的写出和读入,例如将逐元素运算和相邻的矩阵运算融合为单个算子,避免中间激活值落盘。其二是调整数据布局使搬运的数据块在内存中连续,减少非合并访问(non-coalesced access)带来的额外搬运开销。

结尾

ops-math仓库的出现标志着CANN算子生态补全计划进入了基础层的深水区。从架构上看,它与ops-nn、ops-transformer形成的功能分层是合理的——让擅长底层数学的人专注基础算子,让框架集成者专注于高阶语义,是符合软件工程分工原则的设计。算子注册机制的核心是原型定义加Kernel实现加静态注册的三件套,这套模式在CANN体系内高度统一,掌握了add算子的注册流程后,其他算子的注册逻辑可以触类旁通。


仓库地址:https://atomgit.com/cann/ops-math

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐