Skip to content

Ascend C:算子优化方法论

这篇文档只记录可迁移的方法论。具体比赛算子的实现、shape 特化、tiling 参数、代码和可直接复现的优化 recipe 不放在公开文档中。

对 Ascend C 算子优化,可以先把问题从“写一个更快的 kernel”改写成:

这个算子的主要瓶颈到底在哪里,以及怎样用最小实验把它测出来?

很多性能问题不是靠更复杂的代码解决,而是先把算子归类,再选择正确的数据通路、tiling 和流水方式。

面对一个新算子,第一步不要马上写 kernel,而是把语义拆成底层 primitive,例如:

连续数据搬运
规则 strided 访问
Vector 逐元素计算
Reduction
Layout / transpose
Irregular gather / scatter
Atomic update
Cube / 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每秒能消费多少输入数据
Stridestride 增大后有效带宽如何下降
Transposelayout 重排的真实代价
Irregular Update随机访问 / 原子冲突的代价
Pipelinedouble buffer 能隐藏多少搬运时间
CubeL1/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 应该是一个实验变量,而不是凭经验固定。

对很多 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 峰值没有意义。

实际算子通常不需要一套 kernel 覆盖所有形状。

更合理的策略是先根据 shape 特征分类,例如:

小 / 中 / 大规模
连续 / 非连续
长 inner / 短 inner
高并行 / 低并行
规则 / irregular

然后为少量有明显差异的类别准备不同 dispatch。

但 shape specialization 的前提是:

每个分支都必须由实际 profiling 证明值得存在。

否则只会增加维护成本和 tail bug。

一个比较稳定的优化流程是:

1. 正确 baseline
2. 记录 kernel time
3. 算有效带宽 / 吞吐
4. 看 profiler 中各执行单元利用率
5. 看 stall / pipeline bubble
6. 提出一个可验证假设
7. 只修改一个主要变量
8. 再测

不要一口气同时改 tile、core 数、buffer depth、layout 和算法;那样即使变快了,也很难知道原因。

13. Autotuning 应该搜索“受约束的策略空间”

Section titled “13. Autotuning 应该搜索“受约束的策略空间””

自动调优适合搜索:

core 数
Tile 大小
Buffer depth
Vector repeat / layout
Tail 策略
Reduction 组织
Copy burst 几何
少量 dispatch variant

每个 candidate 至少记录:

correctness
kernel time
effective bandwidth / throughput
profiling evidence

自动调优的目标不是盲搜,而是让机器帮助验证已经建立好的硬件假设。

最后可以把整个流程压缩成:

算子语义
↓
拆成 primitive
↓
估算 Bytes / Ops
↓
判断主要瓶颈
↓
建立对应 Microbench
↓
选择 Tiling / Pipeline
↓
Profiler 验证
↓
Shape 分类与有限特化
↓
Autotuning
↓
回到 profiler

核心原则只有一句:

先测清硬件在做什么,再决定代码应该怎么写。