一、为什么必须 Tiling

写算子最容易翻车的地方不是核函数,是 Tiling。Tiling 没设计好,代码能跑但性能惨不忍睹;设计错了,直接精度错误或越界崩溃。但教材往往一句话带过——“把数据分块”。这篇把它拆到能上手。

两个硬约束:

  1. UB 太小。AI Core 片上 UB 只有几百 KB,而输入动辄几十 MB。数据没法一次全搬进来,必须切块轮流搬。
  2. 核要并行。几十个 AI Core 要同时干活,就得先把总任务切成互不重叠的份。

所以 Tiling 本质是两层切分:

总数据 totalLength
   │
   ├─ 第一层:按 AI Core 切 → 每个核分到 blockLength
   │
   └─ 第二层:核内按 UB 切 → 每次搬 tileLength,循环 tileNum 次

第一层决定并行度,第二层决定搬运效率。两个都算对,算子才既对又快。

二、第一层:按核算

uint32_t coreNum     = GetBlockNum();
uint32_t blockLength = totalLength / coreNum;

// 关键:除不尽时,最后一个核补上余数
if (GetBlockIdx() == coreNum - 1) {
    blockLength += totalLength % coreNum;
}

uint32_t offset = GetBlockIdx() * (totalLength / coreNum);

这两行是新手第一大坑。totalLength 一般不是核数的整数倍,直接整除会丢掉尾巴上的数据。判断条件写 blockLength(当前核处理的量),不要写 totalLength(永远是全量)。

三、第二层:核内切块

uint32_t tileNum  = blockLength / tileLength;   // 整块次数
uint32_t tileTail = blockLength % tileLength;   // 尾块长度

循环就是"整块 + 尾块":

for (uint32_t i = 0; i < tileNum; i++) {
    CopyIn(i, tileLength);
    Compute(tileLength);
    CopyOut(i, tileLength);
}

if (tileTail > 0) {          // 尾块单独处理,别省
    CopyIn(tileNum, tileTail);
    Compute(tileTail);
    CopyOut(tileNum, tileTail);
}

tileLength 从哪来?两种做法:

  • kernel 内直接算blockLength / BUFFER_NUM)——样例常用,简单够用
  • Host 侧算好,通过 TilingData 传进来——工程规范做法,能按芯片型号动态调优

真实项目里几乎都是第二种。你会在核函数签名里看到 GM_ADDR tiling,那就是 Tiling 参数的入口。

四、tileLength 取多大?

这是 Tiling 最核心的取舍:

取值后果
太小循环次数暴涨,搬运启动开销压过计算
太大UB 装不下,乒乓双缓冲失效,性能反降
合适2 × tileLength × sizeof(T) ≤ UB 容量

注意那个 2 ——它是 BUFFER_NUM=2 的双缓冲。想开双缓冲(强烈建议开),UB 至少要能放下两份 tile。

所以调优顺序通常是:

  1. 先按 UB 容量倒推出 tileLength 上限
  2. 再往下取整到 32 字节的整数倍
  3. 实测不同取值,看 profiler 里的搬运/计算时间占比

五、对齐:绕不过去的坎

DataCopy 要求数据量 32 字节对齐。float16 占 2 字节 → 一次至少搬 16 个元素。所以 tileLength 必须是 16 的整数倍(float32 则是 8 的倍数)。

尾块尤其要注意:tileTail 常常不是 16 的倍数,直接把非对齐长度丢给 DataCopy 会报错。两种解法:

  • 向上对齐 + mask:多搬一点,计算时用 mask 屏蔽多余元素
  • 尾块走 CPU 或标量处理:量大时不合算,量小时最省事

六、五个常见错误

  1. 最后一个核忘了补余数 → 尾部数据丢失,精度错误
  2. tileLength 不是 32B 倍数 → DataCopy 直接报错
  3. 为了整除硬凑 tileLength → 丢掉无法覆盖的数据
  4. 尾块省掉不写 → 数据对不上,测试必挂
  5. 判断条件写成 totalLength → 越界崩溃

七、总结

Tiling 就三件事:

按核算  →  blockLength + 余数补齐
按 UB 切 →  tileNum + tileTail
按对齐取 →  32B 倍数,且留够双缓冲空间

设计对了,DMA 搬运能和计算重叠起来,性能差距可以是数倍。

还没写过算子的,建议先看:《昇腾 Ascend C 入门:手撕 Add 算子,从核函数到 NPU 跑通》

想先搞懂底层架构,看:《昇腾的 “Hello World” 为什么不打印字符串?》

Logo

鲲鹏昇腾开发者社区是面向全社会开放的“联接全球计算开发者,聚合华为+生态”的社区,内容涵盖鲲鹏、昇腾资源,帮助开发者快速获取所需的知识、经验、软件、工具、算力,支撑开发者易学、好用、成功,成为核心开发者。

更多推荐