Ascend C:算子优化方法论
这篇文档只记录可迁移的方法论。具体比赛算子的实现、shape 特化、tiling 参数、代码和可直接复现的优化 recipe 不放在公开文档中。
对 Ascend C 算子优化,可以先把问题从“写一个更快的 kernel”改写成:
这个算子的主要瓶颈到底在哪里,以及怎样用最小实验把它测出来?
很多性能问题不是靠更复杂的代码解决,而是先把算子归类,再选择正确的数据通路、tiling 和流水方式。
1. 先从语义拆成硬件 primitive
Section titled “1. 先从语义拆成硬件 primitive”面对一个新算子,第一步不要马上写 kernel,而是把语义拆成底层 primitive,例如:
连续数据搬运规则 strided 访问Vector 逐元素计算ReductionLayout / transposeIrregular gather / scatterAtomic updateCube / GEMM真正要优化的是这些 primitive 的组合方式,而不是算子名字本身。
例如一个算子如果大部分时间都在 GM 和 UB 之间搬数据,那么继续增加 Vector 计算复杂度通常不会改善性能;反过来,如果数据搬运已经被隐藏,才有必要继续研究 Vector / Cube 的执行效率。
2. 先判断 Memory Bound 还是 Compute Bound
Section titled “2. 先判断 Memory Bound 还是 Compute Bound”最常用的方法是先估算数据流量和计算量。
设:
Bytes = 必须从 GM 读取和写回的数据量Ops = 实际执行的计算量AI = Ops / Bytes如果算术强度很低,优化重点通常是:
- 减少 GM 往返;
- 合并连续搬运;
- 提高 burst 长度;
- 避免中间结果落回 GM;
- 用流水隐藏 CopyIn / CopyOut。
如果算术强度较高,再进一步分析:
- Vector / Cube 是否吃满;
- tile 是否足够大;
- 是否存在 pipeline bubble;
- L1 / UB / L0 容量是否限制了并行度。
3. 建立自己的 Microbench,而不是只看厂商峰值
Section titled “3. 建立自己的 Microbench,而不是只看厂商峰值”优化一个真实算子前,最好先有一组最小 benchmark 来回答硬件问题。
典型 Microbench 可以覆盖:
| 类别 | 主要回答的问题 |
|---|---|
| GM ↔ UB Copy | 实际可达到多少有效带宽 |
| Vector | 逐元素操作在不同 tile / core 配置下的吞吐 |
| Reduction | 每秒能消费多少输入数据 |
| Stride | stride 增大后有效带宽如何下降 |
| Transpose | layout 重排的真实代价 |
| Irregular Update | 随机访问 / 原子冲突的代价 |
| Pipeline | double buffer 能隐藏多少搬运时间 |
| Cube | L1/L0 数据准备与 Mmad 是否匹配 |
Microbench 的意义不是得到一个漂亮数字,而是给后续算子优化提供可复用的“能力表”。
4. Tiling 的目标不是切得越小越好
Section titled “4. Tiling 的目标不是切得越小越好”一个好的 tile 需要同时满足:
能放进本地存储搬运尽量连续Vector / Cube 一次处理足够多数据core 之间负载均衡尾块处理成本可控tile 太小,通常会出现:
- DataCopy burst 太碎;
- pipeline 填充/排空开销占比过高;
- 每个 core 工作太少;
- 同步和队列开销放大。
而 tile 太大又可能导致:
- UB / L1 / L0 容量不足;
- buffer depth 降低;
- 并行度下降;
- tail 处理复杂。
所以 tile size 应该是一个实验变量,而不是凭经验固定。
5. 优先让数据“连续地流动”
Section titled “5. 优先让数据“连续地流动””对很多 Vector 类 kernel,最理想的数据通路是:
GM ↓UB tile ↓Vector / Reduce ↓UB tile ↓GM优化时优先考虑:
- 能否把多个小搬运合并成大搬运;
- 能否把中间结果留在 UB;
- 能否避免为了“变成连续”而额外做一次全 tensor materialization;
- 能否把高维问题 collapse 成更简单的 outer / inner 形式;
- 能否让每个 core 负责较长的连续区间。
这里最重要的一条经验是:
为了省一次复杂寻址而增加一次完整 GM 往返,通常得不偿失。
6. Pipeline:把 Copy 和 Compute 重叠起来
Section titled “6. Pipeline:把 Copy 和 Compute 重叠起来”顺序执行时,一个 tile 的时间近似为:
T_tile = T_load + T_compute + T_store如果使用双缓冲或更深的流水,稳态可以逼近:
T_tile ≈ max(T_load, T_compute, T_store)因此 TPipe / TQue 的价值不只是 API 组织,而是在表达 producer-consumer、buffer 复用和阶段重叠。
真正需要验证的是:
- CopyIn 是否和 Compute 重叠;
- Compute 是否和 CopyOut 重叠;
- 是否存在某一级长期空转;
- 增加 buffer depth 是否真的改善吞吐。
7. Reduction 要看“输入消费带宽”
Section titled “7. Reduction 要看“输入消费带宽””Reduction 的输出通常很小,因此只看总读写字节容易误导。
更有意义的指标是:
BW_reduce = input_bytes / time也就是 kernel 每秒真正能消费多少输入。
Reduction 优化时重点观察:
- 每个 core 处理多少数据;
- partial reduction 如何组织;
- tile 内 reduction 是否足够长;
- 跨 tile / 跨 core 的合并成本;
- 精度要求是否需要更高精度累加。
8. Transpose / Permute 本质上是 Layout 问题
Section titled “8. Transpose / Permute 本质上是 Layout 问题”Layout 重排通常不是计算瓶颈,而是:
连续地址如何被打碎小块 transpose 如何组织读和写哪个方向更连续UB tile 如何映射到输出一个常见方法是先分类 permutation:
- 是否保留连续 inner suffix;
- 是否可以 collapse 相邻维度;
- 是否退化成简单 copy;
- 是否必须做真正的小块 transpose。
能做大块连续 copy 时,不要强行做逐元素重排。
9. Irregular Access 要把不同损失拆开
Section titled “9. Irregular Access 要把不同损失拆开”Gather / Scatter / Atomic 类 kernel 的性能问题往往混在一起。
可以用不同输入分布分别测:
连续访问无重复随机访问一般随机访问热点冲突这样就能区分:
地址重排代价cache / locality 损失随机 GM 访问代价atomic contention如果不拆开测试,很容易把某一种极端输入上的结论误认为普遍规律。
10. Cube 优化先看“喂数据”,再看 Mmad
Section titled “10. Cube 优化先看“喂数据”,再看 Mmad”对 Cube / GEMM 类 kernel,真正的链路是:
GM → L1 → L0A/L0B → Mmad → L0C → 后续阶段 / GM因此 Cube 优化应分别测:
- GM → L1;
- L1 → L0A / L0B;
- Mmad 本身;
- K-loop accumulation;
- L0C 写回;
- MTE 与 Cube 的 overlap。
如果操作数准备速度跟不上,单独追求 Mmad 峰值没有意义。
11. Shape Specialization 的原则
Section titled “11. Shape Specialization 的原则”实际算子通常不需要一套 kernel 覆盖所有形状。
更合理的策略是先根据 shape 特征分类,例如:
小 / 中 / 大规模连续 / 非连续长 inner / 短 inner高并行 / 低并行规则 / irregular然后为少量有明显差异的类别准备不同 dispatch。
但 shape specialization 的前提是:
每个分支都必须由实际 profiling 证明值得存在。
否则只会增加维护成本和 tail bug。
12. Profiling 的正确顺序
Section titled “12. Profiling 的正确顺序”一个比较稳定的优化流程是:
1. 正确 baseline2. 记录 kernel time3. 算有效带宽 / 吞吐4. 看 profiler 中各执行单元利用率5. 看 stall / pipeline bubble6. 提出一个可验证假设7. 只修改一个主要变量8. 再测不要一口气同时改 tile、core 数、buffer depth、layout 和算法;那样即使变快了,也很难知道原因。
13. Autotuning 应该搜索“受约束的策略空间”
Section titled “13. Autotuning 应该搜索“受约束的策略空间””自动调优适合搜索:
core 数Tile 大小Buffer depthVector repeat / layoutTail 策略Reduction 组织Copy burst 几何少量 dispatch variant每个 candidate 至少记录:
correctnesskernel timeeffective bandwidth / throughputprofiling evidence自动调优的目标不是盲搜,而是让机器帮助验证已经建立好的硬件假设。
14. 一个可复用的优化闭环
Section titled “14. 一个可复用的优化闭环”最后可以把整个流程压缩成:
算子语义 ↓拆成 primitive ↓估算 Bytes / Ops ↓判断主要瓶颈 ↓建立对应 Microbench ↓选择 Tiling / Pipeline ↓Profiler 验证 ↓Shape 分类与有限特化 ↓Autotuning ↓回到 profiler核心原则只有一句:
先测清硬件在做什么,再决定代码应该怎么写。