【infra之路】AI 编译器学习笔记(十四):为什么要 tiling,以及矩阵计算单元的“固定“

准备面试时我被问住过一个问题:如果片上存储放得下所有数据,还需要 tiling 吗?想清楚之后才发现,"装得下"只是 tiling 的理由之一。这一篇整理 tiling 的六个理由、寄存器 tiling 的复用原理,以及矩阵计算单元对输入的要求和它与"加法"的关系。


第一部分:为什么要 tiling

一、六个理由

1. 容量:装得下。 最直观的理由。片上存储放得下整个问题时,这一条就不成立了。

2. 带宽:每一级存储的带宽都有限,要靠复用来弥补。 最核心的理由,见第二节。

3. 延迟:需要足够多的独立计算。 只有一个累加器时,每条 FMA 都要等上一条的结果;多个独立累加器才能让 FMA 流水起来。

4. 并行:把工作分给多个计算单元。 即使数据都放得下,GPU 有多个 SM、CPU 有多个核、NPU 可能有多个矩阵单元,总要把问题切开分下去。

5. 重叠:让搬运和计算同时进行。 整个矩阵一次搬完再算,搬运期间计算单元全在等。切成块后,第一块搬完就能开始算,其余边算边搬。

6. 硬件形状要求。 矩阵计算单元每条指令处理的块尺寸固定,必须切成它要求的形状(见第三部分)。

一个踩过的坑:理由要和场景对上

我在模拟面试里解释 CPU 上 matmul 的 tiling 收益时,说了"tiling 让开始计算的时间提前了"。这其实是第 5 条,在有 DMA 搬运的场景下是对的;但我的场景是 CPU 上 512³ 的标量版本,数据早就在缓存里,没有搬运需要重叠,真正的瓶颈是第 3 条------依赖链。

同一个理由放在不同场景下,对错不同。先判断瓶颈在哪,再选解释。

二、为什么片上存储装得下,还要寄存器 tiling

计算单元不能直接从片上存储(或 CPU 的 L1 缓存)高速取数。关键是:每从存储读一个数,能做多少次计算。

以 AVX2 上的 matmul 外积微内核为例,每走一步 k,读入 A 的几个数(广播)和 B 的一段(向量读取),然后做一批 FMA:

寄存器块 每步 k 的读取 每步 k 的 FMA 每次读取换来的 FMA
1×8(一行、一个向量) 1 次广播 + 1 次向量读 = 2 1 条 0.5
4×16 4 次广播 + 2 次向量读 = 6 4 行 × 2 个向量 = 8 条 约 1.3

规律:寄存器块为 m×n 时,每步读 m + n 份数据,做 m × n 份计算。块越大,每份数据的复用次数越多。

1×8 的块每读一次只能做半条 FMA,读取先成为瓶颈,FMA 单元在等数据;4×16 的块每次读取能喂好几条 FMA,瓶颈才转到计算上。

所以寄存器 tiling 同时解决两件事:提高复用,让读取跟得上;提供独立的累加器,让 FMA 不互相等待。

上限是寄存器数量。AVX2 有 16 个 ymm:4×16 的累加器占 8 个,B 的一段占 2 个,A 的广播值占 1 到 4 个,刚好放得下。再大就会 spill(把值临时存到栈上),前面的收益全被抵消。

GPU 上是同一回事

即使数据都在 shared memory 里,Tensor Core 消耗数据的速度也远快于 shared memory 的供给速度。所以高性能 HGEMM 里每个 warp 都要用 ldmatrix 把数据读进寄存器,组成 fragment,反复喂给 mma.sync------这就是 GPU 上的寄存器 tiling。

shared memory 的块也不能随便放大:每个线程块占用越多,一个 SM 上能同时驻留的线程块就越少,隐藏延迟的能力就越差。

三、分层的视角

复制代码
DRAM / DDR    ── 块大小由"装得下片上存储"和"搬运与计算重叠"决定
   ↓
片上存储 / L1  ── 块大小由"复用以弥补带宽"和"并行分配"决定
   ↓
寄存器        ── 块大小由"复用以喂饱计算单元"和"独立累加器隐藏延迟"决定,上限是寄存器数量
   ↓
计算单元

每一级存储一个块,每一级的块解决的都是"上一级供数据太慢"的问题。 片上存储装得下,只是让第一级可以不切,后面几级照样要切。


第二部分:矩阵计算单元

四、输入是固定的吗

基本是的。每条矩阵指令处理的块大小固定,只是不同芯片的灵活程度不同。

  • GPU Tensor Core :提供一份可选形状的"菜单",形状与数据类型绑定。例如 FP16 的 mma.sync 常用 m16n8k16;换成 FP8、INT8,k 方向会变长。Hopper 的 wgmma 中 N 可以按 8 的倍数在一定范围内选择,但仍是有限几种。
  • CPU SIMD:向量宽度固定,AVX2 为 8 个 f32,AVX-512 为 16 个。Intel AMX 可以配置块的行数和每行字节数,但有上限。
  • NPU:通常是固定尺寸的脉动阵列(如 16×16、32×32),许多还要求数据按特定的分块格式存放。

三种"固定"

要求 含义 例子
形状 每条指令处理的块大小 m16n8k16,16×16 阵列
数据类型 支持哪些精度,形状常随之变化 FP16、BF16、FP8、INT8
布局 数据在寄存器或片上存储中的排法 mma.sync 规定每个线程持有哪些元素;NPU 的分块格式

布局最容易被忽视。mma.sync 规定了 warp 中每个线程必须在寄存器里拿着哪几个元素,ldmatrix 的存在意义就是一条指令把数据按这个排法分好。

编译器怎么应对

  1. 切成指令形状的整数倍 :一个 warp 负责的 64×64 块,是用许多条 16×8 的 mma 拼出来的。
  2. 边界补零:不是整数倍时补零凑满,代价是浪费部分算力。
  3. 布局转换 :权重离线转好(如 Marlin 的权重重排);激活在搬运时顺带转换,或用 ldmatrix 这类指令完成。
  4. 形状太小时利用率很低 :decode 时 M=1,而 mma.sync 的 M 方向是 16,每条指令有 15/16 的行在算零。常见做法:
    • 不用 Tensor Core,改用 CUDA core 做矩阵向量乘;
    • 对调操作数,计算 Cᵀ = Bᵀ·Aᵀ,让很大的 N 维落到 16 那一边。

利用率估算:有效计算量 ÷ 补齐到指令形状后的计算量。M=1 用 m16 指令,最多 1/16。

五、矩阵单元能做加法吗

要区分两种加法。

累加:矩阵单元自己做

矩阵单元做的是乘加:

复制代码
D = A × B + C

mma.sync 就是这个形式,C 是累加器。分块矩阵乘沿 K 方向切块后,每块结果都加到同一个 C 块上,这个累加由指令本身完成。

累加器精度通常高于输入:FP16 输入用 FP32 累加,INT8 输入用 INT32 累加,因为 K 很长时要加成百上千次,低精度累加误差会积累。

逐元素加法:交给别的部件

加偏置、残差连接、激活函数、缩放、归一化等,交给 NPU 的向量单元、GPU 的 CUDA core 或 CPU 的 SIMD 指令。

两者的配合:收尾阶段融合

矩阵单元在 K 方向累加完后,结果还在寄存器或片上存储里,这时就地加偏置、做激活,再写回内存,称为收尾阶段融合(epilogue fusion)。有些 NPU 甚至在矩阵单元的输出通路上直接带有简单的后处理能力。

一个小技巧 :把累加器 C 的初值设成偏置而不是清零,D = A×B + C 算完就自带了偏置,矩阵单元"免费"完成。ReLU 这类非线性运算则不行。


面试一句话

  • 为什么要 tiling:不只是装得下。每一级存储带宽有限,块让数据在这一级被复用足够多次;同时提供并行度、让搬运与计算重叠、提供独立累加器隐藏延迟。寄存器块为 m×n 时每读 m+n 份数据做 m×n 份计算,上限是寄存器数量。
  • 矩阵单元的输入:每条指令处理固定形状的块,形状与数据类型绑定,对布局有要求;编译器负责切块、补零、布局转换。形状远小于指令尺寸时利用率很低。
  • 矩阵单元与加法:K 方向的累加是乘加指令的一部分,累加器精度高于输入;逐元素运算交给向量单元,最好在收尾阶段融合。
相关推荐
七夜zippoe1 小时前
Spring AI 是什么?Java 工程师的 AI 开发第一站
java·人工智能·spring·ai·开发
szxinmai主板定制专家1 小时前
储能 EMS、微网控制器选型:RK3588、RK3576、RK3568+FPGA 怎么选?多网、多 CAN、EMC 设计权衡
人工智能·深度学习·机器学习·fpga开发·工业网关·rk3576·电力网关
2501_926978331 小时前
人机通信协议--人和机器之间的交互想象
人工智能·经验分享·算法·ai写作
海带紫菜菠萝汤1 小时前
PostgreSQL 19 延期一个月:AI 把找 bug 变便宜了,修 bug 还是人工价
人工智能·ai·程序员·架构·大模型
燕落南枝1 小时前
HTML 学习:辅助标签 label 与字符实体
前端·学习·html
不简说1 小时前
AI 帮你画模板、点一下就云打印:sv-print 这三个月都干了啥
前端·javascript·人工智能
hackchen1 小时前
LangChain 学习第 3 课 - 输出解析器(Output Parser)
数据库·学习·langchain
Dawson Zhu1 小时前
【AI Infra小白入门⑦】Transformer架构工程拆解:从Attention到MLA的优化密码
人工智能·语言模型·架构·aigc·agi