---
tags: [CANN]
doc_type: knowledge-note
返回目录: "[[计算机系/昇腾与CANN/大模型与算子专题学习导航|大模型与算子专题学习导航]]"
上一篇: "[[计算机系/昇腾与CANN/Ascend C算子入图与图执行|Ascend C算子入图与图执行]]"
---

# Ascend C调试与性能调优

调试解决计算是否正确，调优解决正确实现为什么还慢。先建立可重复的正确性和性能基线，再按 **Profile → Analyze → Design → Validate** 循环，每次用数据验证改动效果。

算法与布局前置分别见 [Memory 矢量开发](Ascend%20C%20Memory矢量算子开发.md)、[矩阵算子](Ascend%20C矩阵算子与数据布局.md)、[融合协同](Ascend%20C融合算子与核间协同.md)。

## 1. 功能问题应该用什么工具

| 工具/方式 | 解决的问题 |
| --- | --- |
| printf | 核编号、形状、分支、偏移等是否符合预期 |
| assert | 尽早发现容量、尺寸等前提不成立 |
| Dump | 检查某个内存阶段的数据内容 |
| msSanitizer | 越界、竞争、未初始化读取和同步异常 |
| msDebug | 断点、单步、变量、寄存器、内存和栈定位 |
| msobjdump | 了解编译产物与 Kernel 二进制信息 |

```cpp
#include "utils/debug/asc_printf.h"
#include "utils/debug/asc_assert.h"

AscendC::printf("M=%d, N=%d\n", M, N);
ascendc_assert(baseM * baseK * sizeof(half) <= 64 * 1024,
               "L0A tile exceeds capacity\n");
```

这里的 64 KiB 只用于对应 L0A 容量条件。Dump 要先保证目标数据已经生产完成，再按正确类型解释地址；half 数据按 float 导出会得到无意义结果。

Host 侧要检查 ACL 返回值，并在出错时读取 `aclGetRecentErrMsg()`。可用 `ASCEND_SLOG_PRINT_TO_STDOUT=1` 辅助查看日志。NPU 异步错误可能在后续同步时才暴露，应结合此前提交的任务定位。

## 2. 用 Sanitizer 查内存与依赖

![内存、竞争、初始化与同步检测](.assets/AscendC进阶/tuning-9.png)

在支持相应工具的 Linux 开发环境中：

```bash
bisheng demo.asc --npu-arch=dav-2201 -o demo --sanitizer -g
mssanitizer --tool=memcheck ./demo
mssanitizer --tool=racecheck ./demo
msdebug ./demo
```

架构参数应与目标设备一致。`-g` 提供源代码定位信息，Sanitizer 插桩用于诊断，不应用其运行耗时代表正常发布版本性能。

典型故障及其含义：

- 越界写：逻辑尾块较短，但搬运仍按完整块写回。
- 未初始化读：漏掉搬入、padding 未置零，或只计算了部分输出。
- RAW/WAR/WAW 竞争：不同流水或不同核访问同一缓冲，缺少相应先后约束。
- Flag 不配对：分支和循环改变了通知/等待次数，可能造成死锁或污染后续同步。

调试中只打印一个指定核和少量元素，更容易定位问题，也能减少日志对时序的扰动。出现“加打印就正常”时优先检查竞争，不能把日志当作同步方法。

## 3. 数值校验：先定义什么叫正确

浮点加法通常不满足**结合律**：

$$
(a+b)+c\ne a+(b+c).
$$

并行归约改变求和顺序，融合可能改变中间舍入，低精度输入也可能采用高精度累加。因此，正确的实现不一定与另一条计算路径逐位一致。

纯数据搬运应尽量按位比较；计算算子采用高精度参考值，并使用合理误差容限。常见逐元素判据为：

$$
|y_i-\hat y_i|\le\mathrm{atol}+\mathrm{rtol}|\hat y_i|.
$$

还可以统计平均和最大相对误差：

$$
e_i=\frac{|y_i-\hat y_i|}{|\hat y_i|+\epsilon},\quad
\mathrm{MERE}=\operatorname{mean}(e_i),\quad
\mathrm{MARE}=\max(e_i).
$$

一组可作为起点的经验标准为 MERE 小于 Threshold，MARE 小于其 10 倍：

| 类型 | Threshold |
| --- | --- |
| FP16 | $2^{-10}\approx9.77\times10^{-4}$ |
| BF16 | $2^{-7}\approx7.81\times10^{-3}$ |
| FP32 | $2^{-13}\approx1.22\times10^{-4}$ |

这些值不是所有算子的通用验收标准。参考值接近零时相对误差会放大，需结合绝对误差；NaN/Inf、极值、长归约和非整除输入要单独检查。FP32 这项经验阈值也不等于其机器 epsilon。

## 4. 采集性能与阅读流水图

```bash
msprof op ./demo
msopprof simulator --soc-version=Ascend910B1 ./demo
```

上板采集反映实际硬件执行；仿真有助于分析指令先后、气泡和依赖。`Ascend910B1` 是具体示例目标，必须换成受支持且与任务一致的型号。另一条面向受支持 Ascend 950 环境的仿真路径是 `npusim record ./demo -s Ascend950 --gen-report`，不能拿它直接替代 A2/A3 配置。

| 产物 | 关注内容 |
| --- | --- |
| `OpBasicInfo.csv` | Kernel 名、block 数、频率和整体耗时 |
| `PipeUtilization.csv` | 计算与搬运流水占用 |
| `ArithmeticUtilization.csv` | Cube/Vector 运算相关指标 |
| `Memory.csv`、`MemoryL0.csv`、`MemoryUB.csv` | 各级访存和带宽 |
| `L2Cache.csv` | L2 命中与复用 |
| `ResourceConflictRatio.csv` | 资源冲突 |
| `trace.json`、`visualize_data.bin` | 时间线、源码关联与可视化 |

![性能工具中的指令时间线](.assets/AscendC进阶/tuning-16.png)

读图时先找最长关键路径，再看其等待什么。某条流水利用率低，可能是缺少输入、Scalar 发指令太慢、同步过粗，也可能只是该算子本来不需要它。不能为了提高每个百分比而优化无关单元。

稳定测量需要预热、固定形状和 dtype，区分编译/初始化与稳态执行，并正确同步异步任务。只测 Host 提交函数的耗时会低估实际计算时间。

## 5. 理论下界与 Roofline

$$
t_{\mathrm{compute}}=F/P,\qquad t_{\mathrm{memory}}=Q/B,
$$

$$
t_{\mathrm{ideal}}\gtrsim\max(t_{\mathrm{compute}},t_{\mathrm{memory}}).
$$

F 为操作量，P 为匹配数据类型与工作模式的峰值算力；Q 为相应层级实际流量，B 为该层级带宽。取最大值假定可以充分重叠；真实执行还包含依赖、调度、填充和收尾，通常高于此下界。

算术强度 $I=F/Q$，Roofline 上界为：

$$
P_{\mathrm{attainable}}\le\min(P_{\mathrm{peak}}, B I).
$$

对 M=N=K=8192 的矩阵乘，操作量约为 1.10 TFLOPs；两个 half 输入共 256 MiB，float 输出 256 MiB。但 512 MiB 只是理想的一次输入读取加一次输出写入，不能忽略分块引起的重复加载。HBM、L2、L1 的流量可能不同。

![矩阵计算和搬运时间估算](.assets/AscendC进阶/tuning-21.png)

图中耗时依赖频率、核心数、tiling 和缓存假设，不能作为固定结论。乘加计为 2 FLOPs 时，计算量和峰值口径应保持一致；MB 与 MiB 也要一致。

## 6. Tiling 与负载均衡

块太小会增加启动、循环和搬运开销；块太大可能挤占缓冲、阻碍双缓冲或降低并行度。选择 block 数要结合实际可用 AIC/AIV 数、计算规模与混合比例，不能固定认为所有 A2/A3 都有同一核数。

让所有核工作并不等于负载均衡。若最后一轮只有一个核处理长尾，其余核提前结束，整体时间仍由最慢者决定。调整每核任务数、主尾块分配和 tile 大小，目标是缩短最长路径，而不只是增加逻辑 block 数。

## 7. Double Buffer 与 UnitFlag

![单缓冲与双缓冲](.assets/AscendC进阶/tuning-24.png)

双缓冲用 Ping/Pong 两套空间，让搬入下一块与计算当前块重叠。`slot = tileIndex & 1` 只完成槽位选择，同步协议还必须保证：

1. 写输入前，上一轮 V 已读完该槽。
2. 启动计算前，本轮输入已搬入。
3. 写输出前，上一轮 MTE3 已读完该输出槽。
4. 搬出前，本轮 V 已写完输出。

首次迭代要正确初始化“可复用”状态，最后一轮要排空仍在运行的工作。直接在循环开头添加 WaitFlag，却没有初始通知，可能立即死锁。同步原理见 [流水线双向同步](流水线双向同步原理.md)。

![Mmad 与 Fixpipe 的 UnitFlag 协同](.assets/AscendC进阶/tuning-25.png)

UnitFlag 在支持的矩阵路径中用更细粒度的完成状态，让 Fixpipe 与 Mmad 的部分执行重叠。对应 A2/A3 优化方案使用 512B 粒度，常见配置是非最后 K 块 `unitFlag=2`、最后 K 块为 3，并配合 Fixpipe 的设置。

这些值属于 Mmad/Fixpipe 的协议配置，不能只给其中一个接口赋值就认为已完成优化；还要保证累加、输出和 L0C 复用的正确顺序。

## 8. 提高访存效率

### UB Bank 冲突

![UB Bank 与 Bank Group](.assets/AscendC进阶/tuning-26.png)

A2/A3 的这组 UB 结构为 192 KiB、48 个 Bank，分成 16 个 Bank Group，每组 3 个 Bank。每 Bank 为 128 行，每行 32B，即 4 KiB。

$$
\mathrm{bank\_num}=\mathrm{bank\_group\_id}+16\,\mathrm{bank\_id}.
$$

同一 Bank Group 的多个读或多个写，以及同一 Bank 的读写，可能争用相关资源。多个数组即使逻辑互不重叠，地址映射仍可能冲突。可调整起始偏移、padding、数据布局或操作顺序，并用冲突指标验证；仅满足 32B 对齐不保证没有冲突。

### 搬运粒度、对齐与缓存

![搬运粒度与有效带宽](.assets/AscendC进阶/tuning-27.png)

较大的连续搬运通常更容易摊薄固定开销，但仍受缓冲容量与流水策略限制。512B 对齐是相关搬运优化中的性能考虑，不能替代接口自身的最低合法对齐规则。

有复用的数据应争取利用 L2；一次性流式数据可以评估减少缓存污染。GM 是程序使用的全局地址空间，访问它可能命中 L2，不能把“访问 GM”一律解释为实际访问 HBM。

多核访问同一小范围地址时，尤其涉及写入、原子操作或一致性维护，可能出现串行化。可调整任务布局和访问顺序；不能泛化为任何共享只读数据都必然串行。

## 9. 减少 Scalar 开销

Scalar 负责地址计算、分支和指令发射。如果它供给指令不够快，Cube/Vector 即使还有算力也会出现气泡。

优化重点是帮助编译器完成常量传播与寄存器分配：

- 对固定规格使用 `constexpr` 和模板，把可确定的运算移到编译期。
- 将主块与尾块处理分开，减少热循环中的反复边界判断。
- 用局部只读值保存热点参数，缩短变量活跃区间。
- 避免不必要的多级指针和动态索引，注意别名可能阻碍优化。
- 按实际热路径组织结构体，不携带大量不使用的参数。

这些是优化方向，不是“成员变量每次必然访存”或“结构体超过 64B 一定慢”的语言规则。应检查生成代码和性能数据。

拆分首轮、主循环、末轮时，必须单独考虑循环次数为 0、1、2 的情况。否则可能重复执行最后一次、遗漏第一次加载，或因无符号数 `count-1` 下溢而越界。

## 10. 一轮优化怎样才算完成

保留同一输入集和基准版本，只改一个主要因素：例如 tile 大小、双缓冲或 Bank 布局。先通过数值和内存检查，再比较稳定耗时、带宽和流水重叠。平均更快但尾块失败，或只在一种形状更快而常用形状退化，都不能视作完成优化。

样例入口：[性能最佳实践](https://gitcode.com/cann/asc-devkit/tree/master/examples/01_simd_cpp_api/05_best_practices)、[调测工具](https://gitcode.com/cann/asc-devkit/tree/master/examples/01_simd_cpp_api/01_utilities)。
