@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 域 | 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 上也可以打印标量信息:
- 对于
LocalTensor或GlobalTensor,可以使用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 -> GM或CO2 -> VECIN。
- 这种逻辑位置设计的目的,是让矩阵数据尽可能按照硬件流水逐级进入计算单元,而不是频繁返回 GM。
2-2 矩阵乘的核函数
Matmul 高阶 API 的整体流程
- Ascend C 提供 Matmul 高阶 API,把常见的数据切分、搬运和矩阵计算逻辑封装起来。Kernel 侧使用 Matmul 高阶 API 的基本流程为:
- 创建 Matmul 对象。
- 初始化 Matmul 对象。
- 设置左矩阵 A、右矩阵 B 和 Bias。
- 执行矩阵乘。
- 结束 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 的属性,主要包括: - 内存逻辑位置,例如
GM、VECCALC、TSCM。 - 数据格式,例如
ND、NZ。 - 数据类型,例如
half、float。 - 是否转置。
- 其抽象形式类似:
- 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
- 多核切分后,最后一个核可能得到不足一个完整
singleCoreM、singleCoreN或singleCoreK的尾块,此时可以使用:
- 特点:
- 不需要重新生成一套 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 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 |
- 典型获取过程:
- 创建单核或多核 Tiling 对象。
- 设置 A/B/C/Bias 的类型信息。
- 设置 M/N/K Shape 和可用片上空间。
- 调用
GetTiling,生成TCubeTiling。 - 将生成的 Tiling 参数下发给 Kernel。
单核 Tiling
- 单核场景一般直接围绕
SetShape和SetOrgShape配置:
- 在单核场景中,
singleM/N/K会进一步生成 Matmul 内部所需的singleCoreM/N/K和baseM/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/GetTensorC或IterateAll的输出是否完整覆盖 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 -> A2或VECOUT -> B1 -> B2。
- 一个基础的 MatMul + Vector 数据流如下:

- 基本步骤:
- 将输入从 Global Memory 搬入 Cube 侧。
- 执行 MatMul。
- 将 MatMul 结果送入 Vector 侧。
- 执行 Vector 计算。
- 最终结果写回 Global Memory。
- 设计融合算子时,重点不是简单地把两个函数写在一个 Kernel 里,而是让切分策略、片上内存和不同执行单元之间真正形成流水。
3-3 以Matmul+LeakyRelu为例
算子定义
- 本例融合 MatMul 与 LeakyReLU:
- LeakyReLU 的定义为:
- 设计流程分为三部分:
- 算子分析。
- 数据流分析。
- 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 参数需要能够访问
a、b、bias、alpha与输出c对应的 Global Memory 地址。
数据流分析
- 未经高阶 API 简化时,可以把整个过程理解为五个阶段:
- 将 A、B、Bias 从 Global Memory 搬入 Cube 相关存储。
- 执行 MatMul。
- 把 MatMul 结果送到 Vector 侧。
- 执行 LeakyReLU。
- 将最终结果写回 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 侧
- 创建 Matmul Tiling 对象。
- 设置 A/B/C/Bias 的数据类型和格式。
- 设置矩阵 Shape。
- 设置 Matmul 可使用的片上空间。
- 生成 Matmul 所需 Tiling 参数。
- 结合 LeakyReLU 的需求生成融合算子的其他 Tiling 参数。
- 将 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。
- 反例:
- 优化后:

- 这种方式:
- 减少 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 调参数”,还可以通过计算单元转换和算法等价变换,把任务改写成更适合硬件的形式。