Lazy loaded imageAscend C算子开发(高级)笔记
2026-8-20
| 2026-8-20
字数 8688阅读时长 22 分钟
@ZZHow(ZZHow1024)
参考课程:
【Ascend C算子开发(高级)】
【Ascend C系列教程(高级)】

1-算子调试

1-1 算子调试概述

为什么需要 CPU/NPU 孪生调试

  • 传统算子开发如果只依赖 NPU 实机调试,通常会遇到两类问题:
    • 调试时间长:NPU 环境调试成本较高,且编译器编译过程会隐藏部分并行实现细节。
    • 问题定位难:并行执行时序、同步死锁、地址越界、数值精度以及数据溢出等问题,在 NPU 上不容易快速定位。
  • Ascend C 提供 CPU/NPU 孪生调试能力。同一份 Ascend C 算子源码可以分别用于 CPU 域和 NPU 域调测:
    • CPU 域更适合做功能和精度调试,可以使用 GDB、printf、断言等常规调试手段,主要定位逻辑错误、数值错误和内存问题。
    • NPU 域更适合做真实硬件上的功能和性能调试,可以使用数据打印、仿真、Profiling 等手段,主要定位性能问题和算子同步问题。
CPU/NPU 孪生调试概览
CPU/NPU 孪生调试概览
调试域
主要调试手段
更适合定位的问题
CPU 域
GDB、printf/cout、ASSERT
逻辑错误、数据计算错误、内存问题
NPU 域
Ascend C 打印、DumpTensor、仿真调试、Profiling、上板调试
性能问题、同步问题、真实硬件行为
  • 推荐的调试思路是:先在 CPU 域把功能和精度问题调通,再到 NPU 域验证真实运行行为和性能

1-2 CPU域调试

printf 与 LocalTensor 打印

  • CPU 域最直接的调试方式是打印中间变量。
  • 对于标量,可以直接使用 printf
    • 对于 LocalTensor,可以使用其 Print() 接口输出数据。该接口按照 datablcok 打印数据,每个 datablock 为 32 Bytes,例如:
      • 这种方式适合快速确认:
        • Tiling 参数是否正确。
        • Tensor 长度、偏移是否正确。
        • CopyIn 后的数据是否符合预期。
        • Compute 前后的中间结果是否正确。

      GDB 单步调试

      • CPU 调测可以借助 GDB 做断点和单步调试。CPU 调测模型可能会为不同 AI Core/计算单元拉起子进程,因此调试时需要切换为跟随子进程的方式。
      • 典型调试流程如下:
        • 常用命令含义:
          • 命令
            作用
            set follow-fork-mode child
            跟随并调试新创建的子进程
            break file:line
            在指定源文件行号处设置断点
            run
            启动程序并运行到断点
            list
            查看断点附近源码
            backtrace
            查看当前调用栈
            print var
            打印变量值
            continue
            继续执行到下一个断点
            display var
            在程序暂停时持续显示变量
            quit
            退出 GDB
        • CPU 域调试的重点不是模拟 NPU 的真实性能,而是充分利用成熟的软件调试能力,把逻辑、边界、数据和精度问题尽量提前解决。

        1-3 NPU域调试

        NPU 域打印

        • 在 NPU 上也可以打印标量信息:
          • 对于 LocalTensorGlobalTensor,可以使用 DumpTensor 输出指定 Tensor 的数据,同时还可以附带一个 uint32_t 类型的自定义描述信息:
            • Dump 输出中会包含额外的头信息:
              • 每个 block 的 dump 信息前会增加 DumpHead,记录核号、资源使用等信息。
              • 每次输出 Tensor 数据前会增加 DumpTensorHead,记录 Tensor 的地址、数据类型、存储位置等信息。
            • 因此,NPU 数据打印不仅可以查看数值,还可以辅助确认当前核、Tensor 存储位置和实际数据布局

            msSanitizer 内存与竞争检测

            • msSanitizer 是面向昇腾 AI 处理器的异常检测工具,主要包含:
              • 内存检测。
              • 竞争检测。
              • CANN 软件栈内存检测。
            • 常见使用方式:
              • 编译和链接时需要开启相应的 sanitizer 选项:
                • 内存检测可以定位的典型问题包括:
                  • 异常
                    含义
                    主要位置
                    支持地址空间
                    非法读写
                    访问未分配或无权限的内存
                    kernel、host
                    GM
                    多核踩踏
                    多个 AI Core 对重叠内存进行错误访问
                    kernel
                    GM
                    非对齐访问
                    DMA 地址或长度没有满足最小访问粒度要求
                    kernel
                    GM、UB、L0A/L0B/L0C、L1
                    分配内存未使用
                    申请后未真正使用的内存
                    kernel、host
                    GM
                    内存泄漏
                    申请内存后未释放
                    host
                    GM
                    非法释放
                    释放未分配或已经释放的地址
                    host
                    GM
                • 竞争检测重点关注多个内存事件之间的访问顺序:
                  • 类型
                    含义
                    WAW(Write-After-Write)
                    两次写同一块内存,最终结果依赖实际写入顺序
                    WAR(Write-After-Read)
                    写操作过早执行,破坏尚未完成的读取
                    RAW(Read-After-Write)
                    读操作过早执行,读到写入完成之前的旧值

                msProf 性能分析

                • msProf 用于采集和分析运行在昇腾 AI 处理器上的算子性能数据,可以辅助定位软件和硬件性能瓶颈。
                • 常见能力包括:
                  • Block 级数据采集。
                  • 多类 AIC Metrics 性能指标。
                  • 指令流水图。
                  • 代码行和指令耗时。
                  • 代码热点图。
                  • 指令与代码行映射。
                • 典型命令:
                  • 使用 msProf 时重点观察:
                    • 计算单元是否存在长时间空闲。
                    • 搬运和计算是否形成有效流水。
                    • Scalar、Vector、Cube、DMA 之间是否存在明显等待。
                    • 某条指令、某一代码区间是否成为热点。

                  2-矩阵编成(高级API)

                  2-1 矩阵乘基础知识

                  MatMul 的数学定义

                  • 矩阵乘计算可表示为:
                    • 其中:
                      • A 为左矩阵,形状为 [M, K]
                      • B 为右矩阵,形状为 [K, N]
                      • C 为输出矩阵,形状为 [M, N]
                      • Bias 为偏置,形状为 [N],在结果矩阵的 M 方向进行广播。
                    • 矩阵乘的核心是沿 K 维做乘加归约,因此实现时通常会围绕 M、N、K 三个维度分别考虑多核切分、单核切分以及片上存储空间约束

                    ND 与 NZ 分形格式

                    • 矩阵数据常见两类格式:
                      • ND:普通 N 维 Tensor 的连续存储格式。
                      • NZ:为了适配 Cube 高性能矩阵计算而使用的特殊分形格式。
                    • NZ 会对 Tensor 最低两维进行 Pad、拆分和转置。其思路可以表示为:
                      • 也就是说,矩阵不再简单按照原始二维顺序连续排布,而是先切分为适合 Cube 计算的小分形,再按照硬件访问更友好的方式重排。

                      矩阵计算中的逻辑位置

                      • 矩阵乘的输入、切分、中间结果分别存放在不同逻辑位置:
                        • 逻辑位置
                          主要用途
                          可类比理解
                          A1
                          存放整块 A 矩阵
                          二级缓存
                          B1
                          存放整块 B 矩阵
                          二级缓存
                          C1
                          存放 Bias
                          二级缓存
                          A2
                          存放切分后的 A 小块
                          一级缓存
                          B2
                          存放切分后的 B 小块
                          一级缓存
                          C2
                          存放切分后的 Bias 小块
                          一级缓存
                          CO1
                          存放矩阵乘局部结果
                          Cube Out
                          CO2
                          存放聚合后的矩阵结果
                          Cube Out
                          VECCALC
                          Vector 计算中的临时数据
                          Vector 临时计算区
                      • 矩阵乘的典型数据流可以概括为:
                        矩阵乘数据流
                        矩阵乘数据流
                        • 典型路径包括:
                          • A:GM -> A1 -> A2,也可以直接 GM -> A2
                          • B:GM -> B1 -> B2,也可以直接 GM -> B2
                          • 计算:A2 × B2 -> CO1
                          • 聚合:CO1 -> CO2
                          • 输出:CO2 -> GMCO2 -> VECIN
                        • 这种逻辑位置设计的目的,是让矩阵数据尽可能按照硬件流水逐级进入计算单元,而不是频繁返回 GM。

                        2-2 矩阵乘的核函数

                        Matmul 高阶 API 的整体流程

                        • Ascend C 提供 Matmul 高阶 API,把常见的数据切分、搬运和矩阵计算逻辑封装起来。Kernel 侧使用 Matmul 高阶 API 的基本流程为:
                            1. 创建 Matmul 对象。
                            1. 初始化 Matmul 对象。
                            1. 设置左矩阵 A、右矩阵 B 和 Bias。
                            1. 执行矩阵乘。
                            1. 结束 Matmul 计算。
                        • 一个典型结构如下:
                          • 也可以使用 IterateAll 一次完成当前单核范围内的完整矩阵计算。

                          Matmul 常用 API

                          API
                          功能
                          Init
                          初始化 Matmul 对象,传入 Tiling 参数和 TPipe 对象
                          SetTensorA
                          设置左矩阵 A
                          SetTensorB
                          设置右矩阵 B
                          SetBias
                          设置 Bias
                          Iterate
                          每次计算一个 baseM × baseN 的 C 矩阵分片
                          IterateAll
                          计算一个 singleCoreM × singleCoreN 范围的 C 矩阵
                          GetTensorC
                          配合 Iterate 获取一次迭代产生的 C 分片
                          SetTail
                          尾块场景下重设本次计算的 M/N/K 大小
                          End
                          结束一次 Matmul 计算并完成必要收尾

                          MatmulType

                          • 创建 Matmul 对象前,需要通过 MatmulType 描述 A、B、C、Bias 的属性,主要包括:
                            • 内存逻辑位置,例如 GMVECCALCTSCM
                            • 数据格式,例如 NDNZ
                            • 数据类型,例如 halffloat
                            • 是否转置。
                          • 其抽象形式类似:
                            • Matmul 高阶 API 因此不仅知道数据类型,还知道数据存储在哪里、采用什么布局、是否需要转置。

                            设置 A、B 和 Bias

                            • A、B 可以来自 GlobalTensor,也可以来自 LocalTensor
                              • 接口还支持是否转置的控制。对于融合算子,这一点很重要,因为前一个计算阶段产生的局部结果可以直接作为 Matmul 的输入,而不一定要先回写 GM。

                              Iterate、GetTensorC 与 IterateAll

                              Iterate
                              • Iterate() 每次完成一个 baseM × baseN 的 C 分片。开发者可以通过循环自行决定执行多少次:
                                • 这种模式控制更灵活,适合:
                                  • 需要逐分片处理 C 结果。
                                  • 需要把 C 分片直接送给后续 Vector 计算。
                                  • 需要自定义尾块或融合流水。
                                • GetTensorC 可以将结果写入 GM,也可以输出到局部 Tensor,供后续计算继续使用。
                                IterateAll
                                • IterateAll 一次计算当前单核分配到的 singleCoreM × singleCoreN 范围,适合不需要自行控制每个分片的场景。
                                • Tiling 参数中的 iterateOrder 可以控制 M/N 方向的遍历顺序。

                                SetTail

                                • 多核切分后,最后一个核可能得到不足一个完整 singleCoreMsingleCoreNsingleCoreK 的尾块,此时可以使用:
                                  • 特点:
                                    • 不需要重新生成一套 Tiling。
                                    • 未重新设置的维度继续使用原 Tiling 值。
                                    • 尾块值不能大于原始 singleCoreM/N/K

                                  End

                                  • 一次 Matmul 计算结束后必须调用:
                                    • 特别是在融合算子中,如果多个 Matmul 对象或多个 Matmul 阶段交替使用而没有正确调用 End(),可能造成不可预期的执行错误。

                                    2-3 矩阵乘的Tiling

                                    Matmul Tiling 的作用

                                    • Local Memory 通常无法一次容纳完整输入输出,因此矩阵需要分块搬运和分块计算。对于 Matmul,Tiling 需要同时解决两层问题:
                                      • 多核切分:M/N 等维度如何分给多个 AI Core。
                                      • 核内切分:单个 AI Core 内部如何继续切成 baseM/baseN/baseK 小块,使其满足 L1、L0A、L0B、L0C、UB 等空间约束。
                                    • 静态 Shape 场景下很多参数可以在编译期确定;动态 Shape 场景中输入 Shape 运行时才确定,因此需要由 Host 侧 Tiling 函数在运行时计算并下发参数。

                                    Matmul Tiling 核心参数

                                    参数
                                    作用
                                    usedCoreNum
                                    参与计算的 AI Core 数量
                                    M, N, K
                                    原始矩阵乘问题规模
                                    singleCoreM/N/K
                                    单核负责的矩阵范围
                                    baseM/N/K
                                    单次矩阵乘指令处理的分块大小
                                    depthA1/depthB1
                                    A/B 在 A1/B1 中能够缓存的分块份数
                                    stepM/stepN
                                    A1/B1 缓冲区沿 M/N 方向一次缓存多少个 base 块
                                    iterateOrder
                                    C 分片的遍历顺序
                                    • baseM/baseN/baseK 需要满足片上空间约束,例如:
                                      • 同时还需要满足矩阵分形对齐要求。

                                      多核与核内切分关系

                                      Matmul API 切分示意
                                      Matmul API 切分示意
                                      • 可以把整个 Matmul Tiling 理解成两级切分:
                                        • 多核切分决定每个 AI Core 的工作量,核内切分决定一次 Cube 计算的数据块大小。

                                        iterateOrder

                                        • 一次 Iterate 会输出一个 baseM × baseN 的 C 分片。多次迭代时,Matmul 会自动移动到下一个输出位置。
                                        • iterateOrder 用来描述自动偏移顺序:
                                          • 0:先沿 M 轴方向移动,再沿 N 轴移动。
                                          • 1:先沿 N 轴方向移动,再沿 M 轴移动。
                                        • 合理选择遍历顺序有利于复用 A1/B1 中已经缓存的数据。

                                        Matmul Tiling API

                                        • Ascend C 提供 Matmul Tiling API 来生成 Kernel 所需参数,常用接口包括:
                                          • API
                                            作用
                                            SetAType/SetBType
                                            设置 A/B 的逻辑位置、格式、数据类型以及是否转置
                                            SetCType
                                            设置 C 的逻辑位置、格式和数据类型
                                            SetBiasType
                                            设置 Bias 的位置、格式和数据类型
                                            SetShape
                                            设置本次 Matmul 计算的 singleM/singleN/singleK
                                            SetOrgShape
                                            设置原始矩阵 M/N/K
                                            SetBias
                                            设置是否参与 Bias 计算
                                            SetFixSplit
                                            手工固定 baseM/baseN/baseK
                                            SetBufferSpace
                                            设置 Matmul 可使用的 L1/L0C/UB 空间
                                            SetTraverse
                                            设置 M 或 N 优先的遍历方式
                                            GetBaseM/N/K
                                            读取计算出的 baseM/baseN/baseK
                                            GetTiling
                                            生成并获取 Tiling 参数
                                            SetDim
                                            设置多核 Matmul 可参与计算的核数
                                            SetSingleShape
                                            设置多核场景下单核范围 singleCoreM/N/K
                                            GetSingleShape
                                            获取计算得到的单核 Shape
                                            GetCoreNum
                                            获取多核切分后实际使用的 BlockDim
                                        • 典型获取过程:
                                            1. 创建单核或多核 Tiling 对象。
                                            1. 设置 A/B/C/Bias 的类型信息。
                                            1. 设置 M/N/K Shape 和可用片上空间。
                                            1. 调用 GetTiling,生成 TCubeTiling
                                            1. 将生成的 Tiling 参数下发给 Kernel。

                                        单核 Tiling

                                        • 单核场景一般直接围绕 SetShapeSetOrgShape 配置:
                                          • 在单核场景中,singleM/N/K 会进一步生成 Matmul 内部所需的 singleCoreM/N/KbaseM/N/K
                                          • 对于融合算子,如果 L1、L0C、UB 的一部分需要留给其他计算阶段,可以通过 SetBufferSpace 显式限制 Matmul 可使用的片上空间。

                                          多核 Tiling

                                          • 多核场景需要先指定可用核数,再对每个核的工作范围进行约束:
                                            • 多核 Tiling 会先把整体问题切成多份 singleCoreM/N/K,再把每一份继续切成 baseM/N/K
                                            • SetSingleShape 可用于限制部分单核维度;如果某个维度不设置或设置为 -1,则由 Tiling 策略自行选择。

                                            2-4 运行验证

                                            • 运行验证时重点检查:
                                              • A、B、Bias 的数据类型和格式是否与 MatmulType 一致。
                                              • Host 侧生成的 TCubeTiling 是否成功。
                                              • blockDim 是否与多核 Tiling 结果一致。
                                              • SetTensorA/B/Bias 是否指向正确的数据区域。
                                              • Iterate/GetTensorCIterateAll 的输出是否完整覆盖 C 矩阵。
                                              • 尾块场景是否正确使用 SetTail
                                              • 每次 Matmul 计算结束后是否调用 End()

                                            3-融合算子

                                            3-1 再谈架构

                                            AI Core 基本组成

                                            • AI Core 负责标量、向量和矩阵等计算密集型任务,主要包括:
                                              • Cube 矩阵计算单元。
                                              • Vector 向量计算单元。
                                              • Scalar 标量计算单元。
                                              • 片上存储单元。
                                              • 数据搬运单元。
                                              • 控制单元。
                                            • 根据 Cube 和 Vector 是否部署在同一个核内,把硬件形态划分为耦合架构分离架构

                                            耦合架构

                                            耦合架构
                                            耦合架构
                                            • 耦合架构中,Cube 和 Vector 位于同一个 AI Core:
                                              • Cube 与 Vector 可以围绕共享的片上存储形成流水。
                                              • Cube 结果可以通过 CO2 等路径直接进入 Vector。
                                              • Vector 结果也可以进一步作为矩阵计算输入。
                                              • MTE1/MTE2/MTE3 等搬运单元负责不同存储层级之间的数据移动。
                                            • 耦合架构产品包括 Atlas 推理系列(Ascend 310P)、Atlas 训练系列以及 Atlas 200/500 A2 推理产品。

                                            分离架构

                                            分离架构
                                            分离架构
                                            • 分离架构把矩阵计算核 AIC 和向量计算核 AIV 分开:
                                              • AIC 负责 Cube 计算,并拥有自身 Scalar 和矩阵相关存储。
                                              • AIV 负责 Vector 计算,并拥有自身 Scalar 与 UB。
                                              • AIC 与 AIV 之间需要通过 Global Memory 完成跨核数据交换。
                                              • AIC 额外包含 BT Buffer 和 Fixpipe Buffer,用于 Bias、量化参数等特殊数据。
                                            • Atlas A2 训练系列/Atlas 800I A2 推理产品采用分离架构。
                                            • 在分离架构下,典型路径为:
                                              • 理解这些路径是后续设计融合算子的基础,因为融合的本质之一就是尽可能减少不必要的跨层级、跨核和回 GM 的数据搬运

                                              3-2 融合算子基础知识

                                              什么是融合算子

                                              • 融合算子是把多个原本独立的小算子合并成一个大的 Kernel。融合后的算子在功能上与原算子序列等价,但可以通过统一的数据流和流水调度获得更高性能。
                                              • 例如 Flash Attention 可以把多个 MatMul、Scale、Mask、Softmax 等阶段组合为一个整体实现,而不是让每个算子都独立完成一次完整的 GM 输入输出。

                                              融合带来的收益

                                              • 与多个独立算子串行执行相比,融合算子主要有四类收益:
                                                • 减少计算和调度开销:多个小算子合并为一个 Kernel,减少多次算子调度。
                                                • 减少内存占用:中间结果尽量保留在片上存储,而不是为每个中间结果单独分配 GM 空间。
                                                • 优化数据流:减少 GM 与 Local Memory 之间的往返搬运。
                                                • 简化代码组织:把紧密关联的数据流放到同一个实现中,便于统一控制流水与资源。
                                              • 对于 Cube + Vector 融合,如果仍然按照独立算子方式执行,通常是:
                                                • 而合理融合后,可以对数据进行分片和流水调度,使 Cube 与 Vector 在不同数据分片上重叠执行,并尽量减少中间结果回 GM 的次数。

                                                融合算子的编程范式

                                                • 融合算子仍然围绕数据在逻辑位置之间的流动来设计:
                                                  • Cube 输出可以作为 Vector 输入:CO2 -> VECIN
                                                  • Vector 输出可以作为 Cube 输入:VECOUT -> A1 -> A2VECOUT -> B1 -> B2
                                                • 一个基础的 MatMul + Vector 数据流如下:
                                                MatMul + Vector 数据流
                                                MatMul + Vector 数据流
                                                • 基本步骤:
                                                    1. 将输入从 Global Memory 搬入 Cube 侧。
                                                    1. 执行 MatMul。
                                                    1. 将 MatMul 结果送入 Vector 侧。
                                                    1. 执行 Vector 计算。
                                                    1. 最终结果写回 Global Memory。
                                                • 设计融合算子时,重点不是简单地把两个函数写在一个 Kernel 里,而是让切分策略、片上内存和不同执行单元之间真正形成流水

                                                3-3 以Matmul+LeakyRelu为例

                                                算子定义

                                                • 本例融合 MatMul 与 LeakyReLU:
                                                  • LeakyReLU 的定义为:
                                                    • 设计流程分为三部分:
                                                        1. 算子分析。
                                                        1. 数据流分析。
                                                        1. Tiling 策略设计。

                                                    输入输出规格

                                                    类型
                                                    名称
                                                    Shape
                                                    数据类型
                                                    Format
                                                    输入
                                                    a
                                                    [M, K]
                                                    half
                                                    ND
                                                    输入
                                                    b
                                                    [K, N]
                                                    half
                                                    ND
                                                    输入
                                                    bias
                                                    [1, N]
                                                    float32
                                                    -
                                                    属性/输入
                                                    alpha
                                                    标量
                                                    float32
                                                    -
                                                    输出
                                                    c
                                                    [M, N]
                                                    float32
                                                    ND
                                                    • Kernel 名称为:
                                                      • Kernel 参数需要能够访问 abbiasalpha 与输出 c 对应的 Global Memory 地址。

                                                      数据流分析

                                                      • 未经高阶 API 简化时,可以把整个过程理解为五个阶段:
                                                          1. 将 A、B、Bias 从 Global Memory 搬入 Cube 相关存储。
                                                          1. 执行 MatMul。
                                                          1. 把 MatMul 结果送到 Vector 侧。
                                                          1. 执行 LeakyReLU。
                                                          1. 将最终结果写回 Global Memory。
                                                      • 由于 Matmul 高阶 API 已经封装了前面矩阵搬运、切分和计算细节,因此实际 Kernel 可以进一步简化为三个 Stage:
                                                        • 其中:
                                                          • Stage1 使用 Matmul 高阶 API 获取矩阵乘结果。
                                                          • Stage2 在 Vector 侧对 Matmul 输出做 LeakyReLU,并通过 Queue 管理局部 Tensor。
                                                          • Stage3 使用 DataCopy 将最终结果写回 GM。
                                                        • 涉及的主要接口包括:
                                                          • Matmul 高阶 API。
                                                          • LeakyReLU Vector 计算接口。
                                                          • DataCopy
                                                          • EnQue / DeQue

                                                        Tiling 策略

                                                        • Tiling 同时考虑多核和核内切分。
                                                        多核切分
                                                        • 根据可用核数和输入 M/K/N,得到每个核负责的:
                                                          核内切分
                                                          • 再根据 Local Memory 空间,把单核范围切分为:
                                                            • 如果 GetTensorC 的结果直接保存在 Local Memory/UB 中供 LeakyReLU 使用,那么 baseM × baseN 对应结果必须满足 UB 空间约束。
                                                            • 因此融合算子中的 Matmul Tiling 不能只追求 Matmul 本身的最大分块,还必须给 LeakyReLU、Queue 和其他临时数据预留空间。

                                                            3-4 代码精讲&运行验证

                                                            Host 侧与 Kernel 侧配合

                                                            • 融合算子的整体实现可以分成两部分:
                                                              • Host 侧
                                                                  1. 创建 Matmul Tiling 对象。
                                                                  1. 设置 A/B/C/Bias 的数据类型和格式。
                                                                  1. 设置矩阵 Shape。
                                                                  1. 设置 Matmul 可使用的片上空间。
                                                                  1. 生成 Matmul 所需 Tiling 参数。
                                                                  1. 结合 LeakyReLU 的需求生成融合算子的其他 Tiling 参数。
                                                                  1. 将 Tiling 下发给 Kernel。
                                                              • Kernel 侧
                                                              • 融合实现的关键是让 Matmul 高阶 API 的输出能够直接进入后续 Vector 处理,而不是退化为“Matmul 写 GM、LeakyReLU 再从 GM 读取”的两个独立算子。
                                                              • 运行验证时应重点确认:
                                                                • Matmul 输出的数据类型与 LeakyReLU 输入是否匹配。
                                                                • Matmul Tiling 给后续 Vector 阶段预留的局部内存是否足够。
                                                                • Queue 的 EnQue/DeQue 是否一一对应。
                                                                • 跨 Cube/Vector 数据流是否符合目标芯片的架构特点。
                                                                • 多核尾块的 M/N 范围是否正确。
                                                                • 最终输出是否与 LeakyRelu(A × B + Bias, alpha) 的参考结果一致。

                                                              4-性能优化(理论)

                                                              4-1 搬运优化

                                                              理论性能评估:先判断 Compute Bound 还是 Memory Bound

                                                              • 做算子性能优化前,应先估计理论上限,而不是直接从局部代码开始修改。
                                                              • 需要的输入信息主要包括:
                                                                • 芯片通路带宽。
                                                                • Buffer 大小。
                                                                • 各类计算指令的 cycle 数据。
                                                                • 算法 FLOPs。
                                                                • 总数据搬运量。
                                                              • 可以分别估算:
                                                                • 计算所需时间 tc
                                                                • 数据搬运所需时间 tb
                                                              • 判断原则:
                                                                • tc > tb:更偏 Compute Bound,理论基准主要由计算时间决定。
                                                                • tb > tc:更偏 Memory Bound,理论基准主要由搬运时间决定。
                                                              • 经验目标:实际性能能够达到理论基准的大约 80% 时,通常已经比较接近该实现的合理上限。
                                                              • 需要注意:达到某个执行单元的 bound 并不等于整个算子已经最优。还要检查算法是否存在重复计算、是否已经接近最小数据搬运量,以及是否存在设计本身造成的额外 bound。

                                                              尽量一次搬运更大的数据块

                                                              • 搬运带宽对单次搬运长度非常敏感。小块搬运难以充分利用通路带宽;实测经验是,单次搬运达到较大的连续数据块后,带宽利用率才趋于稳定。
                                                              • 优化原则:
                                                                • 尽量把多个小 DataCopy 合并为一次较大的搬运。
                                                                • 避免在 Kernel 中使用大量循环逐小块搬运。
                                                                • 让一次搬运覆盖尽可能多的有效数据。

                                                              GM 地址尽量 512B 对齐

                                                              • 在针对的 Atlas A2 训练系列/Atlas 800I A2 推理产品中,从 GM 向 Local Memory 搬运数据时,如果 GM 地址满足 512 Bytes 对齐,可以更高效地利用带宽。
                                                              • 测试表明,在部分数据长度下,32 Bytes 对齐场景的有效带宽可能只有 512 Bytes 对齐场景的大约 70%。
                                                              • 因此在可控的数据布局下,优先考虑:
                                                                • 起始 GM 地址对齐。
                                                                • Tile 起点对齐。
                                                                • 批量数据结构按较大对齐粒度规划。

                                                              高效使用 DataCopy 参数

                                                              • 如果数据在 GM 中呈规则间隔分布,应优先利用 DataCopy 自带的参数完成二维/跨步搬运,而不是手写 for 循环。
                                                              • 常见参数包括:
                                                                • blockCount:需要搬运多少个 block。
                                                                • blockLen:每个 block 的连续搬运长度。
                                                                • srcStride:源数据相邻 block 之间的间隔。
                                                                • dstStride:目的数据相邻 block 之间的间隔。
                                                              • 例如,每行 16 KB,但只需要每行前 2 KB,共 16 行。如果用循环,需要发起 16 次 2 KB 搬运;利用 stride 参数后,可以通过一次 DataCopy 描述整个二维搬运任务。
                                                              • 优化原则是:让搬运 API 描述数据布局,而不是让 Scalar 使用循环一块块发起搬运

                                                              4-2 内存优化

                                                              UB Buffer 融合

                                                              • 如果一个 Vector 计算的输出就是下一次 Vector 计算的输入,则不应把中间结果写回 GM 后再重新搬入 UB。
                                                              • 反例:
                                                                • 优化后:
                                                                  UB Buffer 融合
                                                                  UB Buffer 融合
                                                                  • 这种方式:
                                                                    • 减少 CopyOut/CopyIn 次数。
                                                                    • 降低 GM 带宽压力。
                                                                    • 提高 UB 使用效率。
                                                                    • 是算子融合最基础的数据流优化之一。

                                                                  使用 BT Buffer 做 Bias 融合

                                                                  • 带 Bias 的矩阵乘可以直接把 Bias 搬到 C2/BT(Bias Table Buffer),在 Mmad 计算阶段完成 Bias 融合。
                                                                  • 不推荐的流程:
                                                                    • 推荐流程:
                                                                      • 这样可以避免矩阵结果为了加 Bias 额外经历一次 GM 往返和 Vector Add。

                                                                      Fixpipe Buffer 与随路量化

                                                                      • 如果需要对矩阵乘结果做量化,可以把量化参数放入 Fixpipe Buffer,并在 Fixpipe 阶段完成量化。
                                                                      • 相比先把矩阵乘结果写回 GM、再读到 UB 做 Vector 量化,随路量化可以减少:
                                                                        • CO1 -> workspace 搬运。
                                                                        • workspace -> UB 搬运。
                                                                        • 额外的 Vector 量化计算流程。
                                                                      • 这一优化手段主要面向 Atlas A2 训练系列/Atlas 800I A2 推理产品。

                                                                      LOC 中保留中间矩阵乘结果

                                                                      • 如果需要累加多次矩阵乘结果,例如:
                                                                        • 不应每次都把局部矩阵乘结果写回 GM 再做累加。更合理的方式是:
                                                                          • 将前一次结果保留在 CO1/LOC。
                                                                          • 下一次调用 Mmad 时继续在 LOC 上累加。
                                                                          • 通过类似 cmatrixInitVal 的参数控制是否从 0 初始化。
                                                                          • 所有局部累加完成后再一次性输出。
                                                                        • 这可以显著减少多次 CO1 -> GM -> UB 的往返搬运和额外 Add。

                                                                        4-3 API使用优化

                                                                        Scalar 优化

                                                                        • Scalar 主要负责:
                                                                          • 地址计算。
                                                                          • 循环控制。
                                                                          • 条件控制。
                                                                          • 指令发射。
                                                                        • 如果 Scalar 工作量过大,会使 Vector、Cube 或 DMA 单元因为等待指令而空闲,导致整个异步流水被 Scalar 阻塞。
                                                                        • 常见优化手段:
                                                                          • 循环变量不要频繁外提,消除循环内重复计算。
                                                                          • 将循环中的乘法、复杂偏移计算尽量转化为递增加法。
                                                                          • 能在 Host/Tiling 阶段提前算出的值,不要放到 Kernel Scalar 中重复计算。
                                                                          • 适当增加单次 Tile 工作量,减少循环次数,使 Scalar 发射开销被更多计算覆盖。
                                                                          • 小函数在合适时使用 inline,但大函数被多处调用时不要盲目 inline,否则会增大指令代码尺寸。
                                                                        • 发现 Scalar Bound 的主要手段:
                                                                          • 上板 PMU 统计。
                                                                          • 仿真流水图。
                                                                          • 指令/代码日志分析。

                                                                        避免 Scalar 直接参与数据计算

                                                                        • 如果一个数值本身来自 UB/Vector 计算结果,再由 Scalar 读取并参与后续运算,会引入 Scalar 与 Vector 之间的同步依赖。
                                                                        • 例如:
                                                                          • 这种写法可能产生 Vector 等 Scalar、Scalar 再等 Vector 的同步链。
                                                                          • 更好的方式是让该数值继续留在 Vector 数据路径中,即使只计算一个元素,也使用 Vector 指令完成:
                                                                            • 核心思想:Scalar 尽量只负责“控制”,不要进入大数据计算依赖链

                                                                            Ascend C API 分级

                                                                            • 计算 API 的核心数据结构是 LocalTensor。把 Vector/Cube 计算接口按抽象程度分为多个层级:
                                                                              • API 层级
                                                                                特点
                                                                                示例
                                                                                3 级
                                                                                运算符重载,表达最简单
                                                                                dst = src1 + src2
                                                                                2 级
                                                                                一维连续计算,可以指定连续元素数量
                                                                                Add(dst, src1, src2, count)
                                                                                1 级
                                                                                多维 Slice 计算
                                                                                开发中
                                                                                0 级
                                                                                功能最丰富,最贴近硬件,可控性最高
                                                                                Add(..., repeatTimes, repeatParams)
                                                                            • 抽象层级越高,代码越简单;越接近 0 级,开发者越能直接控制:
                                                                              • repeatTimes
                                                                              • repeatStride
                                                                              • blockStride
                                                                              • mask
                                                                            • 因此追求极致性能时,可以在明确理解硬件数据布局后使用更低层级 API。

                                                                            0 级 API 中的 block、repeat 和 mask

                                                                            • 以 Vector abs 类接口为例:
                                                                              • 其基本概念:
                                                                                • 内部 SIMD 以固定 32B block 为基础。
                                                                                • 一个 repeat 通常由 8 个 block 组成,即 256B 宽度。
                                                                                • blockStride 描述同一 repeat 内相邻 block 的地址步长。
                                                                                • repeatStride 描述相邻 repeat 的起始地址步长。
                                                                                • repeatNum 控制 repeat 次数。
                                                                                • mask 控制一个 repeat 中哪些元素真正参与计算。
                                                                              • 对于 FP16,一次 256B repeat 对应 128 个元素,因此可以使用 128-bit Mask 精细控制每个元素是否参与计算。
                                                                              • 0 级 API 的优势在于,它能避免额外的数据重排和多次 API 调用,但也要求开发者自己保证地址、步长和 Mask 的正确性。

                                                                              iCache 优化

                                                                              • 如果 Kernel 代码体积过大、分支很多、循环和长跳转复杂,可能导致 iCache miss。
                                                                              • iCache miss 时,AIC 需要重新从更远的存储层级取指令,造成计算中断。
                                                                              • 常见原因:
                                                                                • Kernel 代码尺寸超过 iCache 容量。
                                                                                • 大量复杂条件分支。
                                                                                • 循环和长距离跳转。
                                                                                • 过度 inline 导致代码膨胀。
                                                                              • 优化思路:
                                                                                • 减小 Kernel 代码体积。
                                                                                • 必要时把大型互斥分支拆成不同 Kernel。
                                                                                • 把执行频率更高的分支放在更有利的位置。
                                                                                • 避免无意义的函数展开。
                                                                              • 是否存在 iCache miss,可以通过上板 PMU 的 ICACHE miss 指标辅助判断。

                                                                              用 Cube 实现大型 ReduceSum

                                                                              • 当 ReduceSum 的归约规模超出 Vector 单元适合的并行范围时,可以把归约转换为矩阵乘问题。
                                                                              • 例如对 [16, 32] Tensor 的最后一维做 ReduceSum,可以构造一个 [32, 1] 的全 1 矩阵:
                                                                                • 这样就可以把最后一维求和转化为 Matmul,由 Cube 单元完成。
                                                                                • 给出的思路是:
                                                                                  • Vector FP16 一次并行处理能力有限。
                                                                                  • 通过构造辅助全 1 矩阵,把 ReduceSum 改写为矩阵乘。
                                                                                  • 只要矩阵 M/K 维度足够大,就可以利用 Cube 更高的并行能力。
                                                                                • 这类优化说明:性能优化不仅是“同一个 API 调参数”,还可以通过计算单元转换和算法等价变换,把任务改写成更适合硬件的形式。
                                                                              • 文字
                                                                              • 推荐
                                                                              • 学技术
                                                                              • Ascend
                                                                              • 机器学习
                                                                              • Ascend C算子开发(进阶)笔记期末复习合集
                                                                                Loading...
                                                                                目录
                                                                                0%