前言昇腾 CANN 已经提供了这么丰富的算子生态为什么还需要一个专门做数学计算的算子库答案比我想象的有意思得多。数学算子看起来简单——加法就是加法三角函数就是三角函数但真正在昇腾 NPU 上把它们跑出硬件理论峰值的百分之七八十需要做的优化远比你以为的多。数据布局怎么贴合 Cube 单元的向量宽度Tiling 怎么切才能让多核负载均衡不同数据类型之间的转换怎么省掉不必要的中间拷贝——这些问题在 ops-math 里都有对应的解法。ops-math 是昇腾 CANN 算子库中专门负责数值计算的子仓库涵盖 conversion张量形态变换、math基础数学运算、random随机数生成三大类别。它并不追求大而全而是在自己划定的范围内把每个算子的硬件亲和性做到极致。在昇腾 NPU 的达芬奇架构上同样的矩阵加法如果用纯 Python 循环来实现延迟可能是使用 ops-math 之后的数倍。这不是算法本身的差距而是算子有没有针对昇腾的 Cube 单元做数据布局和并行度优化的问题。本文从 ops-math 的仓库定位、目录结构、核心算子分类、实际调用方式、多架构适配机制、以及与其他算子仓库的关系六个角度展开帮你在最短时间里搞清楚这个仓库到底能做什么、怎么做、以及什么时候该用它。一、ops-math 在 CANN 生态中的位置昇腾 CANN 的算子库体系大致可以分为四层。最上层是面向用户的 AOL 算子库包含 NN/BLAS/DVPP/HCCL 等面向业务场景的算子集合开发者日常通过 PyTorch 或 MindSpore 调用的算子最终都会落到这一层。第二层是算子开发框架和工具链包括 asc-devkit算子开发套件、pyascPython 风格算子编程语言和 asc-tools调试调优工具等。第三层就是各个具体的算子实现仓库ops-math 就属于这一层。它和 ops-nn、ops-cv、ops-transformer 等仓库共享一套底层构建基础设施opbase但在算子功能上互不重叠。第四层是 opbase作为所有算子仓库的公共基础提供统一的 Tiling 框架、构建系统脚本和测试基础设施。从依赖关系来看ops-math 是 opbase 的直接消费者之一它的所有算子都建立在 opbase 提供的通用算子开发框架之上。这意味着如果你理解了 ops-math 的算子开发模式再去看 ops-nn 或者 ops-cv 的源码上手速度会快很多。它们用的 tiling 机制、kernel 编写规范、INI 配置文件格式几乎完全一致。反过来说如果你在 ops-math 里发现了某个 Tiling 策略的优化技巧大概率也能平移到其他算子仓库。ops-math 于 2025 年 9 月首次开源上线此后持续迭代。2026 年 1 月新增了 QuickStart 文档让新手可以零基础入门算子项目部署、算子开发和贡献流程。2025 年 12 月的版本开始支持 Ascend 950PR 和 Ascend 950DT 芯片并新增了 concat、lerp、drop_out_v3 等算子。从版本节奏来看这个仓库目前处于快速成长期几乎每周都有新的 PR 合并进来涵盖新算子实现、现有算子适配新芯片、代码重构和文档优化等类型。从代码质量角度来看ops-math 团队对代码规范的要求在持续提升。2025 年底的系列重构 PR 完成了所有算子到 opbase 公共头文件的迁移消除了代码重复。最近合并的 PR 则引入了规范化的错误码上报机制EZ0008-EZ0034 系列替代了之前散落在各处的 OP_LOGE 宏调用。这种对工程质量的持续投入让 ops-math 在 CANN 的算子仓库中算是一个代码风格比较统一、可读性比较好的项目。二、目录结构与开发范式clone 下来之后ops-math 的源码结构很清晰。根目录下分 conversion、math、random 三大算子目录外加 common公共头文件与工具函数目前正在逐步精简、cmake构建系统含 func.cmake、opbuild.cmake、ut.cmake 等、docs文档包括 QuickStart 和进阶教程、scripts辅助脚本如 opdesc_parser.py、insert_kernel_src.py 等、test测试用例等辅助目录。每个算子目录下面又按算子名划分子目录比如 math/add、math/matmul、math/lerp、random/drop_out_v3、conversion/concat 等。每个算子目录内的文件布局遵循一套约定op_kernel 目录放算子实现源码op_kernel 下的 CMakeLists.txt 负责注册编译单元而算子描述信息则通过 INI 配置文件声明。这套约定不是随便定的而是 opbase 仓统一规范的结果所有算子仓库都必须遵循。以 add 算子为例。它的 Ascend C 实现文件通常在 op_kernel 目录下文件名遵循算子名加后缀的模式。CMakeLists.txt 中用add_kernel_sources()函数把源码文件注册到构建系统。这里有一个值得注意的细节最新的代码已经支持KERNEL_SRC机制允许算子通过 CMake 参数指定自定义的 kernel 入口文件名而不是强制使用算子名作为入口。这意味着同一个算子可以有多套不同的 kernel 实现通过切换 KERNEL_SRC 来选用。// math/add/op_kernel/CMakeLists.txt 关键片段add_kernel_sources(${KERNEL_NAME}KERNEL_SRC arch35/add.cpp add_apt.cpp)// WHY: KERNEL_SRC 指定了 arch35/add.cpp 作为自定义入口// 当需要在不同架构如 arch35 对应 Ascend 910arch55 对应 Ascend 950上// 使用不同实现时可以通过 KERNEL_SRC 灵活切换而不需要修改算子逻辑。// WHY: add_apt.cpp 仍然保留在编译列表中作为通用的算子适配层// 处理跨架构的公共逻辑避免每个架构重复编写相同代码。公共头文件的组织也经历过一次重要的重构。早期的 ops-math 有自己的 tiling_base.h、tiling_util.h、tiling_templates_registry.h 等公共头文件但随着 opbase 仓的功能越来越完善这些公共头文件的功能逐渐被 opbase 替代。2025 年底的系列 PR#2951 处理 conversion 算子、#2969 处理 math 上半部分、#2980 处理 math 下半部分、#3000 完成收尾完成了所有算子到 opbase 公共头文件的迁移并删除了已经变成空壳转发的旧文件。这一步的重要性在于以后 ops-math 只需要维护算子本身的业务逻辑公共基础设施的升级会在 opbase 仓统一完成。开发者在升级 CANN 版本时也不需要再担心算子仓库的公共代码和 opbase 不一致的问题。这套开发范式的另一个好处是新人上手成本低。opbase 提供了一套完整的 QuickStart 文档从源码编译到算子调用到算子开发都有详细的步骤说明。ops-math 的 add 算子是 QuickStart 文档中使用的示例算子它的实现展示了 Ascend C 算子开发的完整流程定义算子描述文件、编写 tiling 函数、实现 kernel 函数、配置 CMake 构建规则、运行单元测试。掌握了这个流程之后开发一个新的数学算子基本上就是照猫画虎的事情。三、核心算子分类与能力边界conversion 类算子conversion 类算子处理张量形态变换是最基础的算子类别之一。常见的成员包括 cast类型转换比如从 FLOAT16 转到 FLOAT32、concat张量拼接、transpose转置等。这些算子在深度学习模型中无处不在——模型加载时需要做数据类型转换多分支特征融合时需要做张量拼接注意力机制中的序列重排需要做转置。ops-math 的 conversion 算子针对昇腾 NPU 的内存布局做了优化避免了数据在 Host 和 Device 之间不必要的搬运。cast 算子可能是你用得最多的 conversion 算子。在混合精度训练场景下模型的权重通常以 FLOAT16 存储以节省显存而梯度更新时需要转成 FLOAT32 来保证数值精度。cast 算子在昇腾 NPU 上可以做到全流水线执行——数据从 HBM 读出、类型转换、写回 HBM整个过程不需要 CPU 介入。如果用 NumPy 的 astype 方法在 CPU 上做同样的事情还需要先把数据从 NPU 搬到 CPU 内存、转换、再搬回去延迟会高出一个数量级。# 使用 ops-math 的 concat 算子进行多分支特征融合importaclfromaclimportops_math# 假设有三个分支的特征图shape 分别为 [N, C1, H, W]、[N, C2, H, W]、[N, C3, H, W]# 在实际模型中C1、C2、C3 可能分别是 64、128、256branch1acl.Tensor([N,C1,H,W],dtypeacl.dtype.FLOAT32)branch2acl.Tensor([N,C2,H,W],dtypeacl.dtype.FLOAT32)branch3acl.Tensor([N,C3,H,W],dtypeacl.dtype.FLOAT32)# 在通道维度上拼接输出 shape 为 [N, C1C2C3, H, W]mergedops_math.concat([branch1,branch2,branch3],axis1)# WHY: 在 NPU 上使用 ops_math.concat 而不是先拷贝回 CPU 再用 NumPy 拼接# 省掉了 Host↔Device 之间的数据搬运延迟通常可以降低一个数量级。# WHY: concat 算子内部已经针对昇腾内存对齐做了优化# 拼接后的张量可以直接喂给后续算子不需要额外的 layout 转换步骤。math 类算子math 类算子是 ops-math 的核心也是数量最多的类别。它涵盖了加减乘除add、sub、mul、div、三角函数sin、cos、tan、tanh、acos、asin、atan、atanh、acosh、指数对数exp、log、log1p、比较运算max、min、maximum、minimum、以及更高级的运算matmul、add_lora、accumulate_nv2、stft等。add 算子虽然简单但它是理解 Ascend C 算子开发全流程最好的入口。ops-math 的 add 算子实现包含完整的 tiling 逻辑将大张量拆分成硬件友好的小块、kernel 实现调用 Cube 单元的向量加法指令、以及单元测试。而且 add 算子最近完成了对 Ascend 950 的 Ascend C 实现包含了异构调用示例——也就是在同一个算子内调用其他算子的能力。这种能力在实现复杂的复合运算时很有用。2025 年 12 月的更新中新增了 lerp 算子线性插值和 atanh 算子的 Ascend 950 实现。lerp 在深度学习中的应用场景很广——数据增强时的 MixUp 融合、某些注意力机制中的插值计算、以及模型蒸馏中的软标签混合都会用到。lerp 的公式很简单output start weight * (end - start)但在 NPU 上直接调用融合的 lerp 算子比分别调用减法、乘法和加法算子要快因为融合算子省掉了中间结果的显存读写。atanh反双曲正切则在某些归一化操作和特殊激活函数中有用武之地。# 使用 ops-math 的 lerp 算子做 MixUp 数据增强importaclfromaclimportops_math# 两张图像的张量表示img_aacl.Tensor([H,W,3],dtypeacl.dtype.UINT8).to(acl.dtype.FLOAT32)img_bacl.Tensor([H,W,3],dtypeacl.dtype.FLOAT32)# MixUp 线性混合lambda0.6lam0.6mixedops_math.lerp(img_a,img_b,lam)# WHY: lerp 算子直接在 NPU 上完成逐元素线性插值# 相比分三次调用sub→mul→add融合算子只需要一次# 因为中间结果不需要写回显存再读出来省掉了两次显存读写。# WHY: 如果混合比例是动态的比如每张图不同lerp 支持按张量传入 weight# NPU 内部会自动做 broadcast不需要你在外面手动扩展维度。random 类算子random 类算子提供随机数生成能力包括均匀分布、正态分布以及专门的 drop_out_v3 算子。深度学习训练中的 Dropout 正则化、数据增强中的随机裁剪和噪声注入都属于这个类别的应用场景。drop_out_v3 是 random 类中比较有意思的一个算子。和标准的 Dropout 相比drop_out_v3 在性能上有明显的优化它把掩码生成和元素置零合并为一个融合操作在 NPU 上执行时只需要一次 kernel 调用。标准实现需要先调用随机数算子生成掩码、再做逐元素乘法两次 kernel 调用之间还需要保存中间掩码张量。drop_out_v3 把这两步融合到一起省掉了中间张量的显存分配和读写。对于大模型训练来说这种融合优化在每一层都省一点累积起来的收益相当可观。ops-math 的 random 算子目前支持 Ascend 950 芯片的 Ascend C 实现。2025 年 12 月新增了 population_countpopcount算子的适配用于统计二进制数中 1 的个数。虽然 popcount 看起来跟随机数生成关系不大但在某些哈希算法和压缩算法中会用到因此被归类在 random 目录下。四、快速上手从调用到编译ops-math 提供了两条主要的算子使用路径。第一条是通过 aclnnAscendCL Neural Network接口直接调用这是最简单的方式适合只想用算子不想深入实现的开发者。aclnn 是一套高层的算子调用接口屏蔽了底层的 device 管理、内存分配、流同步等细节你只需要构造输入张量和输出张量然后调用对应的 aclnn 函数即可。这种方式的好处是代码量少、上手快坏处是灵活性有限——你只能使用算子已经暴露出来的参数和配置无法自定义算子行为。第二条路径是自己编译和部署算子源码。如果你需要修改算子行为、添加新的算子变体或者想了解算子到底在硬件上怎么跑的就需要走这条路。流程大致是git clone 源码、安装 CANN 开发包、配置 CMake 构建参数、运行 opbuild 脚本编译算子、生成可部署的包。CANN 还提供了 Simulator 工具让你在没有物理 NPU 的 x86 机器上也能编译和调试算子极大降低了开发者的入门门槛。# 使用 CANN Simulator 在 x86 主机上编译并运行 add 算子# 前提已安装 CANN 开发包环境变量 CANN_ROOT 已设置# 1. clone 与 CANN 版本配套的分支源码gitclone-b${tag_version}https://atomgit.com/cann/ops-math.git# 2. 进入算子目录cdops-math/math/add# 3. 配置构建mkdirbuildcdbuild cmake..\-DCMAKE_BUILD_TYPERelease\-DCANN_ROOT${CANN_ROOT}\-DSOC_VERSIONascend910# 4. 编译make-j8# 5. 运行单元测试./add_ut# WHY: CANN Simulator 让你在没有物理 NPU 的 x86 机器上也能编译和调试算子# 极大降低了开发者的入门门槛。硬件验证可以等后续再放到真实设备上跑。# WHY: SOC_VERSION 参数必须和你的硬件匹配# 选错会导致编译通过但运行时出现指令集不兼容的错误。# 比如你在 Ascend 910 上用了 ascend950 的配置Cube 单元的指令集不同就会报错。注意一点clone 源码时一定要选择和你的 CANN 版本配套的分支标签不要直接用 Main 分支。README 中明确说明了这一点。Main 分支包含的是最新的开发代码可能还没有经过完整的版本兼容性测试使用它有版本不匹配的风险。正确的做法是到 release-management 仓库查看你安装的 CANN 版本对应的 Gitcode 标签名然后用git clone -b ${tag_version}拉取。五、效率对比与性能考量性能对比是一个绕不开的话题。在同一个硬件上ops-math 的算子性能和使用 CPU 上对应的 NumPy 操作相比有显著差异。以矩阵加法为例两个 4096x4096 的浮点矩阵相加NumPy 在 CPU 上跑通常需要数百毫秒级别而 ops-math 的 add 算子在同一任务上只需要几十毫秒。这个差距的来源不是算法——矩阵加法本身的复杂度是 O(n²)两边一样——而是数据搬运和并行执行的效率。CPU 上的 NumPy 操作受限于内存带宽和线程并行度的瓶颈而昇腾 NPU 的 Cube 单元可以以硬件流水线的方式同时完成数据读取、计算和写回三个阶段完全重叠。对比维度CPU NumPy (4096x4096 矩阵加法)ops-math add 算子 (昇腾 NPU)差异说明执行延迟数百毫秒级数十毫秒级通常有 5-10 倍的提升幅度数据搬运CPU 内存读写受限于系统内存带宽NPU 片内 HBM 直连数据不离开 Device零 Host↔Device 搬运开销并行度受限于 CPU 核心数通常 8-64 核Cube 单元数千个计算单元并行执行硬件级大规模并行显存管理手动管理 CPU 内存容易忘记释放由 Runtime 自动管理 NPU 显存池开发者可以更关注算法本身批处理能力逐个矩阵操作缺乏批量调度多流并行支持多个矩阵加法并发执行吞吐量进一步提升再来看 lerp 算子的效率对比。如果在 NPU 上用三步操作来实现线性插值——先做减法得到 (end - start)再做乘法得到 weight * diff最后做加法得到 start result——每一步操作都需要把中间结果写回显存再读出来给下一步用。而 lerp 融合算子把这三步合并成一个 kernel数据只在寄存器里流转不需要写回显存。对于大张量来说省掉两次显存读写带来的性能提升非常可观。实现方式Kernel 调用次数中间张量显存读写适合场景三步分开调用 (sub mul add)3 次2 次中间结果写回读出调试阶段需要观察中间结果lerp 融合算子1 次0 次中间结果生产环境追求性能手写 Ascend C 融合1 次0 次中间结果有特殊需求标准融合不满足时drop_out_v3 的融合优化逻辑也是类似的。标准的 Dropout 实现需要两步先用随机数算子生成掩码张量再做逐元素乘法。两步之间掩码张量需要占显存、需要写入和读出。drop_out_v3 把掩码生成和乘法融合在一起掩码只存在于寄存器中算完就丢弃不占用额外的显存。在大模型训练场景下每一层都省一个掩码张量的显存几十层下来就能省出可观的空间可以用来增大 batch size 或者模型维度。六、KERNEL_SRC 机制与多架构适配2025 年底的 KERNEL_SRC 机制是 ops-math 的一个重要进化。在此之前每个算子的 kernel 入口文件名必须和算子名一致这在大多数情况下没问题但当同一个算子需要在不同架构上使用不同的实现时就变得不够灵活了。举个例子add 算子可能在 Ascend 910 上使用一套 Tiling 策略在 Ascend 950 上因为 Cube 单元的计算能力不同需要使用另一套 Tiling。在 KERNEL_SRC 机制出现之前常见的做法是写一个通用的实现然后在运行时根据 SOC_VERSION 做分支判断——这样做的问题是两个架构的实现代码混在一个文件里可读性和可维护性都很差。KERNEL_SRC 机制允许你把不同架构的实现放在不同的文件中比如 arch35/add.cpp 和 arch55/add.cpp然后在 CMakeLists.txt 中通过 KERNEL_SRC 参数指定使用哪个文件。这套机制还伴随着一个辅助脚本 insert_kernel_src.py 的引入。这个脚本负责在算子编译流程中读取 INI 配置文件在对应的算子 section 中插入或更新 kernelSrc.value 字段。这样算子描述信息和 kernel 源文件的映射关系就在构建时自动完成了开发者不需要手动维护 INI 文件中的这个字段。# math/add/op_kernel/CMakeLists.txt 中的 KERNEL_SRC 用法 set(KERNEL_NAME add) add_kernel_sources(${KERNEL_NAME} KERNEL_SRC arch35/add.cpp add_apt.cpp ) # WHY: KERNEL_SRC 让同一算子的多架构实现在物理上分离 # 每次只编译目标架构需要的文件编译产物更小排查问题时也更容易定位。 # WHY: add_apt.cpp 作为适配层负责跨架构的公共逻辑 # 包括算子描述注册、输入输出的格式转换、以及与 Runtime 的交互封装。 # 如果没有这层适配arch35 和 arch55 的实现就需要各自重复这些代码。这套机制不是 ops-math 独创的。2025 年 10 月 ops-nn 仓库也同步了相同的 CMake 改造PR#3936说明它正在成为 CANN 算子仓库的通用实践。对于想同时为多款昇腾芯片开发算子的开发者来说理解 KERNEL_SRC 机制是一个基本功。它的设计思路也很清晰把架构差异封装在文件级别而不是代码级别——这比在代码里写 if-else 分支要干净得多。七、如何选择合适的算子仓库ops-math、ops-nn、ops-cv、ops-transformer 这几个仓库有时候会让新手困惑我到底该从哪个仓库找我要的算子这里有一个简单的判断逻辑。如果你的需求是基础数学运算加减乘除、三角函数、指数对数、张量拼接、类型转换ops-math 就是你的首选。如果你的需求是神经网络中的标准算子矩阵乘法、激活函数、归一化、池化看 ops-nn。如果你的需求是图像处理相关的算子resize、crop、颜色空间转换看 ops-cv。如果你的需求是 Transformer 模型特有的算子FlashAttention、MoE 路由、rotary embedding看 ops-transformer。ops-math 的仓库地址是 https://atomgit.com/cann/ops-math
ops-math 仓库全景导读——昇腾 NPU 数学算子库的定位与能力边界
发布时间:2026/6/7 0:24:37
前言昇腾 CANN 已经提供了这么丰富的算子生态为什么还需要一个专门做数学计算的算子库答案比我想象的有意思得多。数学算子看起来简单——加法就是加法三角函数就是三角函数但真正在昇腾 NPU 上把它们跑出硬件理论峰值的百分之七八十需要做的优化远比你以为的多。数据布局怎么贴合 Cube 单元的向量宽度Tiling 怎么切才能让多核负载均衡不同数据类型之间的转换怎么省掉不必要的中间拷贝——这些问题在 ops-math 里都有对应的解法。ops-math 是昇腾 CANN 算子库中专门负责数值计算的子仓库涵盖 conversion张量形态变换、math基础数学运算、random随机数生成三大类别。它并不追求大而全而是在自己划定的范围内把每个算子的硬件亲和性做到极致。在昇腾 NPU 的达芬奇架构上同样的矩阵加法如果用纯 Python 循环来实现延迟可能是使用 ops-math 之后的数倍。这不是算法本身的差距而是算子有没有针对昇腾的 Cube 单元做数据布局和并行度优化的问题。本文从 ops-math 的仓库定位、目录结构、核心算子分类、实际调用方式、多架构适配机制、以及与其他算子仓库的关系六个角度展开帮你在最短时间里搞清楚这个仓库到底能做什么、怎么做、以及什么时候该用它。一、ops-math 在 CANN 生态中的位置昇腾 CANN 的算子库体系大致可以分为四层。最上层是面向用户的 AOL 算子库包含 NN/BLAS/DVPP/HCCL 等面向业务场景的算子集合开发者日常通过 PyTorch 或 MindSpore 调用的算子最终都会落到这一层。第二层是算子开发框架和工具链包括 asc-devkit算子开发套件、pyascPython 风格算子编程语言和 asc-tools调试调优工具等。第三层就是各个具体的算子实现仓库ops-math 就属于这一层。它和 ops-nn、ops-cv、ops-transformer 等仓库共享一套底层构建基础设施opbase但在算子功能上互不重叠。第四层是 opbase作为所有算子仓库的公共基础提供统一的 Tiling 框架、构建系统脚本和测试基础设施。从依赖关系来看ops-math 是 opbase 的直接消费者之一它的所有算子都建立在 opbase 提供的通用算子开发框架之上。这意味着如果你理解了 ops-math 的算子开发模式再去看 ops-nn 或者 ops-cv 的源码上手速度会快很多。它们用的 tiling 机制、kernel 编写规范、INI 配置文件格式几乎完全一致。反过来说如果你在 ops-math 里发现了某个 Tiling 策略的优化技巧大概率也能平移到其他算子仓库。ops-math 于 2025 年 9 月首次开源上线此后持续迭代。2026 年 1 月新增了 QuickStart 文档让新手可以零基础入门算子项目部署、算子开发和贡献流程。2025 年 12 月的版本开始支持 Ascend 950PR 和 Ascend 950DT 芯片并新增了 concat、lerp、drop_out_v3 等算子。从版本节奏来看这个仓库目前处于快速成长期几乎每周都有新的 PR 合并进来涵盖新算子实现、现有算子适配新芯片、代码重构和文档优化等类型。从代码质量角度来看ops-math 团队对代码规范的要求在持续提升。2025 年底的系列重构 PR 完成了所有算子到 opbase 公共头文件的迁移消除了代码重复。最近合并的 PR 则引入了规范化的错误码上报机制EZ0008-EZ0034 系列替代了之前散落在各处的 OP_LOGE 宏调用。这种对工程质量的持续投入让 ops-math 在 CANN 的算子仓库中算是一个代码风格比较统一、可读性比较好的项目。二、目录结构与开发范式clone 下来之后ops-math 的源码结构很清晰。根目录下分 conversion、math、random 三大算子目录外加 common公共头文件与工具函数目前正在逐步精简、cmake构建系统含 func.cmake、opbuild.cmake、ut.cmake 等、docs文档包括 QuickStart 和进阶教程、scripts辅助脚本如 opdesc_parser.py、insert_kernel_src.py 等、test测试用例等辅助目录。每个算子目录下面又按算子名划分子目录比如 math/add、math/matmul、math/lerp、random/drop_out_v3、conversion/concat 等。每个算子目录内的文件布局遵循一套约定op_kernel 目录放算子实现源码op_kernel 下的 CMakeLists.txt 负责注册编译单元而算子描述信息则通过 INI 配置文件声明。这套约定不是随便定的而是 opbase 仓统一规范的结果所有算子仓库都必须遵循。以 add 算子为例。它的 Ascend C 实现文件通常在 op_kernel 目录下文件名遵循算子名加后缀的模式。CMakeLists.txt 中用add_kernel_sources()函数把源码文件注册到构建系统。这里有一个值得注意的细节最新的代码已经支持KERNEL_SRC机制允许算子通过 CMake 参数指定自定义的 kernel 入口文件名而不是强制使用算子名作为入口。这意味着同一个算子可以有多套不同的 kernel 实现通过切换 KERNEL_SRC 来选用。// math/add/op_kernel/CMakeLists.txt 关键片段add_kernel_sources(${KERNEL_NAME}KERNEL_SRC arch35/add.cpp add_apt.cpp)// WHY: KERNEL_SRC 指定了 arch35/add.cpp 作为自定义入口// 当需要在不同架构如 arch35 对应 Ascend 910arch55 对应 Ascend 950上// 使用不同实现时可以通过 KERNEL_SRC 灵活切换而不需要修改算子逻辑。// WHY: add_apt.cpp 仍然保留在编译列表中作为通用的算子适配层// 处理跨架构的公共逻辑避免每个架构重复编写相同代码。公共头文件的组织也经历过一次重要的重构。早期的 ops-math 有自己的 tiling_base.h、tiling_util.h、tiling_templates_registry.h 等公共头文件但随着 opbase 仓的功能越来越完善这些公共头文件的功能逐渐被 opbase 替代。2025 年底的系列 PR#2951 处理 conversion 算子、#2969 处理 math 上半部分、#2980 处理 math 下半部分、#3000 完成收尾完成了所有算子到 opbase 公共头文件的迁移并删除了已经变成空壳转发的旧文件。这一步的重要性在于以后 ops-math 只需要维护算子本身的业务逻辑公共基础设施的升级会在 opbase 仓统一完成。开发者在升级 CANN 版本时也不需要再担心算子仓库的公共代码和 opbase 不一致的问题。这套开发范式的另一个好处是新人上手成本低。opbase 提供了一套完整的 QuickStart 文档从源码编译到算子调用到算子开发都有详细的步骤说明。ops-math 的 add 算子是 QuickStart 文档中使用的示例算子它的实现展示了 Ascend C 算子开发的完整流程定义算子描述文件、编写 tiling 函数、实现 kernel 函数、配置 CMake 构建规则、运行单元测试。掌握了这个流程之后开发一个新的数学算子基本上就是照猫画虎的事情。三、核心算子分类与能力边界conversion 类算子conversion 类算子处理张量形态变换是最基础的算子类别之一。常见的成员包括 cast类型转换比如从 FLOAT16 转到 FLOAT32、concat张量拼接、transpose转置等。这些算子在深度学习模型中无处不在——模型加载时需要做数据类型转换多分支特征融合时需要做张量拼接注意力机制中的序列重排需要做转置。ops-math 的 conversion 算子针对昇腾 NPU 的内存布局做了优化避免了数据在 Host 和 Device 之间不必要的搬运。cast 算子可能是你用得最多的 conversion 算子。在混合精度训练场景下模型的权重通常以 FLOAT16 存储以节省显存而梯度更新时需要转成 FLOAT32 来保证数值精度。cast 算子在昇腾 NPU 上可以做到全流水线执行——数据从 HBM 读出、类型转换、写回 HBM整个过程不需要 CPU 介入。如果用 NumPy 的 astype 方法在 CPU 上做同样的事情还需要先把数据从 NPU 搬到 CPU 内存、转换、再搬回去延迟会高出一个数量级。# 使用 ops-math 的 concat 算子进行多分支特征融合importaclfromaclimportops_math# 假设有三个分支的特征图shape 分别为 [N, C1, H, W]、[N, C2, H, W]、[N, C3, H, W]# 在实际模型中C1、C2、C3 可能分别是 64、128、256branch1acl.Tensor([N,C1,H,W],dtypeacl.dtype.FLOAT32)branch2acl.Tensor([N,C2,H,W],dtypeacl.dtype.FLOAT32)branch3acl.Tensor([N,C3,H,W],dtypeacl.dtype.FLOAT32)# 在通道维度上拼接输出 shape 为 [N, C1C2C3, H, W]mergedops_math.concat([branch1,branch2,branch3],axis1)# WHY: 在 NPU 上使用 ops_math.concat 而不是先拷贝回 CPU 再用 NumPy 拼接# 省掉了 Host↔Device 之间的数据搬运延迟通常可以降低一个数量级。# WHY: concat 算子内部已经针对昇腾内存对齐做了优化# 拼接后的张量可以直接喂给后续算子不需要额外的 layout 转换步骤。math 类算子math 类算子是 ops-math 的核心也是数量最多的类别。它涵盖了加减乘除add、sub、mul、div、三角函数sin、cos、tan、tanh、acos、asin、atan、atanh、acosh、指数对数exp、log、log1p、比较运算max、min、maximum、minimum、以及更高级的运算matmul、add_lora、accumulate_nv2、stft等。add 算子虽然简单但它是理解 Ascend C 算子开发全流程最好的入口。ops-math 的 add 算子实现包含完整的 tiling 逻辑将大张量拆分成硬件友好的小块、kernel 实现调用 Cube 单元的向量加法指令、以及单元测试。而且 add 算子最近完成了对 Ascend 950 的 Ascend C 实现包含了异构调用示例——也就是在同一个算子内调用其他算子的能力。这种能力在实现复杂的复合运算时很有用。2025 年 12 月的更新中新增了 lerp 算子线性插值和 atanh 算子的 Ascend 950 实现。lerp 在深度学习中的应用场景很广——数据增强时的 MixUp 融合、某些注意力机制中的插值计算、以及模型蒸馏中的软标签混合都会用到。lerp 的公式很简单output start weight * (end - start)但在 NPU 上直接调用融合的 lerp 算子比分别调用减法、乘法和加法算子要快因为融合算子省掉了中间结果的显存读写。atanh反双曲正切则在某些归一化操作和特殊激活函数中有用武之地。# 使用 ops-math 的 lerp 算子做 MixUp 数据增强importaclfromaclimportops_math# 两张图像的张量表示img_aacl.Tensor([H,W,3],dtypeacl.dtype.UINT8).to(acl.dtype.FLOAT32)img_bacl.Tensor([H,W,3],dtypeacl.dtype.FLOAT32)# MixUp 线性混合lambda0.6lam0.6mixedops_math.lerp(img_a,img_b,lam)# WHY: lerp 算子直接在 NPU 上完成逐元素线性插值# 相比分三次调用sub→mul→add融合算子只需要一次# 因为中间结果不需要写回显存再读出来省掉了两次显存读写。# WHY: 如果混合比例是动态的比如每张图不同lerp 支持按张量传入 weight# NPU 内部会自动做 broadcast不需要你在外面手动扩展维度。random 类算子random 类算子提供随机数生成能力包括均匀分布、正态分布以及专门的 drop_out_v3 算子。深度学习训练中的 Dropout 正则化、数据增强中的随机裁剪和噪声注入都属于这个类别的应用场景。drop_out_v3 是 random 类中比较有意思的一个算子。和标准的 Dropout 相比drop_out_v3 在性能上有明显的优化它把掩码生成和元素置零合并为一个融合操作在 NPU 上执行时只需要一次 kernel 调用。标准实现需要先调用随机数算子生成掩码、再做逐元素乘法两次 kernel 调用之间还需要保存中间掩码张量。drop_out_v3 把这两步融合到一起省掉了中间张量的显存分配和读写。对于大模型训练来说这种融合优化在每一层都省一点累积起来的收益相当可观。ops-math 的 random 算子目前支持 Ascend 950 芯片的 Ascend C 实现。2025 年 12 月新增了 population_countpopcount算子的适配用于统计二进制数中 1 的个数。虽然 popcount 看起来跟随机数生成关系不大但在某些哈希算法和压缩算法中会用到因此被归类在 random 目录下。四、快速上手从调用到编译ops-math 提供了两条主要的算子使用路径。第一条是通过 aclnnAscendCL Neural Network接口直接调用这是最简单的方式适合只想用算子不想深入实现的开发者。aclnn 是一套高层的算子调用接口屏蔽了底层的 device 管理、内存分配、流同步等细节你只需要构造输入张量和输出张量然后调用对应的 aclnn 函数即可。这种方式的好处是代码量少、上手快坏处是灵活性有限——你只能使用算子已经暴露出来的参数和配置无法自定义算子行为。第二条路径是自己编译和部署算子源码。如果你需要修改算子行为、添加新的算子变体或者想了解算子到底在硬件上怎么跑的就需要走这条路。流程大致是git clone 源码、安装 CANN 开发包、配置 CMake 构建参数、运行 opbuild 脚本编译算子、生成可部署的包。CANN 还提供了 Simulator 工具让你在没有物理 NPU 的 x86 机器上也能编译和调试算子极大降低了开发者的入门门槛。# 使用 CANN Simulator 在 x86 主机上编译并运行 add 算子# 前提已安装 CANN 开发包环境变量 CANN_ROOT 已设置# 1. clone 与 CANN 版本配套的分支源码gitclone-b${tag_version}https://atomgit.com/cann/ops-math.git# 2. 进入算子目录cdops-math/math/add# 3. 配置构建mkdirbuildcdbuild cmake..\-DCMAKE_BUILD_TYPERelease\-DCANN_ROOT${CANN_ROOT}\-DSOC_VERSIONascend910# 4. 编译make-j8# 5. 运行单元测试./add_ut# WHY: CANN Simulator 让你在没有物理 NPU 的 x86 机器上也能编译和调试算子# 极大降低了开发者的入门门槛。硬件验证可以等后续再放到真实设备上跑。# WHY: SOC_VERSION 参数必须和你的硬件匹配# 选错会导致编译通过但运行时出现指令集不兼容的错误。# 比如你在 Ascend 910 上用了 ascend950 的配置Cube 单元的指令集不同就会报错。注意一点clone 源码时一定要选择和你的 CANN 版本配套的分支标签不要直接用 Main 分支。README 中明确说明了这一点。Main 分支包含的是最新的开发代码可能还没有经过完整的版本兼容性测试使用它有版本不匹配的风险。正确的做法是到 release-management 仓库查看你安装的 CANN 版本对应的 Gitcode 标签名然后用git clone -b ${tag_version}拉取。五、效率对比与性能考量性能对比是一个绕不开的话题。在同一个硬件上ops-math 的算子性能和使用 CPU 上对应的 NumPy 操作相比有显著差异。以矩阵加法为例两个 4096x4096 的浮点矩阵相加NumPy 在 CPU 上跑通常需要数百毫秒级别而 ops-math 的 add 算子在同一任务上只需要几十毫秒。这个差距的来源不是算法——矩阵加法本身的复杂度是 O(n²)两边一样——而是数据搬运和并行执行的效率。CPU 上的 NumPy 操作受限于内存带宽和线程并行度的瓶颈而昇腾 NPU 的 Cube 单元可以以硬件流水线的方式同时完成数据读取、计算和写回三个阶段完全重叠。对比维度CPU NumPy (4096x4096 矩阵加法)ops-math add 算子 (昇腾 NPU)差异说明执行延迟数百毫秒级数十毫秒级通常有 5-10 倍的提升幅度数据搬运CPU 内存读写受限于系统内存带宽NPU 片内 HBM 直连数据不离开 Device零 Host↔Device 搬运开销并行度受限于 CPU 核心数通常 8-64 核Cube 单元数千个计算单元并行执行硬件级大规模并行显存管理手动管理 CPU 内存容易忘记释放由 Runtime 自动管理 NPU 显存池开发者可以更关注算法本身批处理能力逐个矩阵操作缺乏批量调度多流并行支持多个矩阵加法并发执行吞吐量进一步提升再来看 lerp 算子的效率对比。如果在 NPU 上用三步操作来实现线性插值——先做减法得到 (end - start)再做乘法得到 weight * diff最后做加法得到 start result——每一步操作都需要把中间结果写回显存再读出来给下一步用。而 lerp 融合算子把这三步合并成一个 kernel数据只在寄存器里流转不需要写回显存。对于大张量来说省掉两次显存读写带来的性能提升非常可观。实现方式Kernel 调用次数中间张量显存读写适合场景三步分开调用 (sub mul add)3 次2 次中间结果写回读出调试阶段需要观察中间结果lerp 融合算子1 次0 次中间结果生产环境追求性能手写 Ascend C 融合1 次0 次中间结果有特殊需求标准融合不满足时drop_out_v3 的融合优化逻辑也是类似的。标准的 Dropout 实现需要两步先用随机数算子生成掩码张量再做逐元素乘法。两步之间掩码张量需要占显存、需要写入和读出。drop_out_v3 把掩码生成和乘法融合在一起掩码只存在于寄存器中算完就丢弃不占用额外的显存。在大模型训练场景下每一层都省一个掩码张量的显存几十层下来就能省出可观的空间可以用来增大 batch size 或者模型维度。六、KERNEL_SRC 机制与多架构适配2025 年底的 KERNEL_SRC 机制是 ops-math 的一个重要进化。在此之前每个算子的 kernel 入口文件名必须和算子名一致这在大多数情况下没问题但当同一个算子需要在不同架构上使用不同的实现时就变得不够灵活了。举个例子add 算子可能在 Ascend 910 上使用一套 Tiling 策略在 Ascend 950 上因为 Cube 单元的计算能力不同需要使用另一套 Tiling。在 KERNEL_SRC 机制出现之前常见的做法是写一个通用的实现然后在运行时根据 SOC_VERSION 做分支判断——这样做的问题是两个架构的实现代码混在一个文件里可读性和可维护性都很差。KERNEL_SRC 机制允许你把不同架构的实现放在不同的文件中比如 arch35/add.cpp 和 arch55/add.cpp然后在 CMakeLists.txt 中通过 KERNEL_SRC 参数指定使用哪个文件。这套机制还伴随着一个辅助脚本 insert_kernel_src.py 的引入。这个脚本负责在算子编译流程中读取 INI 配置文件在对应的算子 section 中插入或更新 kernelSrc.value 字段。这样算子描述信息和 kernel 源文件的映射关系就在构建时自动完成了开发者不需要手动维护 INI 文件中的这个字段。# math/add/op_kernel/CMakeLists.txt 中的 KERNEL_SRC 用法 set(KERNEL_NAME add) add_kernel_sources(${KERNEL_NAME} KERNEL_SRC arch35/add.cpp add_apt.cpp ) # WHY: KERNEL_SRC 让同一算子的多架构实现在物理上分离 # 每次只编译目标架构需要的文件编译产物更小排查问题时也更容易定位。 # WHY: add_apt.cpp 作为适配层负责跨架构的公共逻辑 # 包括算子描述注册、输入输出的格式转换、以及与 Runtime 的交互封装。 # 如果没有这层适配arch35 和 arch55 的实现就需要各自重复这些代码。这套机制不是 ops-math 独创的。2025 年 10 月 ops-nn 仓库也同步了相同的 CMake 改造PR#3936说明它正在成为 CANN 算子仓库的通用实践。对于想同时为多款昇腾芯片开发算子的开发者来说理解 KERNEL_SRC 机制是一个基本功。它的设计思路也很清晰把架构差异封装在文件级别而不是代码级别——这比在代码里写 if-else 分支要干净得多。七、如何选择合适的算子仓库ops-math、ops-nn、ops-cv、ops-transformer 这几个仓库有时候会让新手困惑我到底该从哪个仓库找我要的算子这里有一个简单的判断逻辑。如果你的需求是基础数学运算加减乘除、三角函数、指数对数、张量拼接、类型转换ops-math 就是你的首选。如果你的需求是神经网络中的标准算子矩阵乘法、激活函数、归一化、池化看 ops-nn。如果你的需求是图像处理相关的算子resize、crop、颜色空间转换看 ops-cv。如果你的需求是 Transformer 模型特有的算子FlashAttention、MoE 路由、rotary embedding看 ops-transformer。ops-math 的仓库地址是 https://atomgit.com/cann/ops-math