CUDA Tile 不是把"整个并行结构"都交给编译器,而是把 一个 thread block 内部的线程级并行映射交给编译器。
程序员仍然负责更高层的算法分块与数据映射。
可以把它理解为:
text
SIMT:
程序员描述"每个线程做什么"
Tile:
程序员描述"整个 block 要对一块数据做什么"
编译器再决定 block 内的线程怎么合作完成
NVIDIA 官方的表述也是:Tile 程序在整个 thread block 的粒度描述对多维 tile 的操作,编译器将这些操作映射到 block 内部的具体硬件线程。程序员指定 grid,而每个 block 实际使用多少线程由编译器决定。(NVIDIA Docs)
1. SIMT 中,程序员管得非常细
传统 CUDA SIMT 的 vector add:
cpp
__global__ void vectorAdd(
const float* A,
const float* B,
float* C,
int N)
{
int i = blockIdx.x * blockDim.x + threadIdx.x;
if (i < N) {
C[i] = A[i] + B[i];
}
}
程序员必须决定:
text
一个 block 用多少线程?
threadIdx.x 对应哪个元素?
如何计算全局下标?
最后一个 block 越界怎么办?
访存能不能合并?
需不需要 shared memory?
哪个线程搬哪个数据?
什么时候 __syncthreads()?
每个 warp 会不会发生 divergence?
例如你写:
cpp
vectorAdd<<<ceil(N / 256), 256>>>(...);
其中:
text
256 threads/block
是你手工决定的。
再复杂一点,比如 GEMM,你还需要亲自写:
cpp
__shared__ float As[16][16];
__shared__ float Bs[16][16];
As[threadIdx.y][threadIdx.x] = ...;
Bs[threadIdx.y][threadIdx.x] = ...;
__syncthreads();
for (int k = 0; k < 16; ++k) {
acc += As[threadIdx.y][k]
* Bs[k][threadIdx.x];
}
__syncthreads();
这里你不只是在描述矩阵乘法,还在手工描述:
- 线程到数据的绑定;
- global memory 到 shared memory 的搬运;
- shared memory 布局;
- block 内同步;
- 每个线程的累加任务;
- 每个 warp 的执行行为。
这就是传统 CUDA 性能高,但学习和优化难度也很高的原因。
2. Tile 模型把关注点提升一层
Tile 编程中,你写的是:
cpp
auto aTile = load(...);
auto bTile = load(...);
auto cTile = aTile + bTile;
store(..., cTile);
或者 GEMM:
cpp
auto aTile = load(A, ...);
auto bTile = load(B, ...);
acc = mma(aTile, bTile, acc);
store(C, acc, ...);
你的表达变成:
text
加载一块 A
加载一块 B
对两块数据做矩阵乘加
写回一块 C
而不是:
text
thread 0 搬 A[0]
thread 1 搬 A[1]
warp 0 做第一部分
warp 1 做第二部分
线程之间在这里同步
Tile API 提供 elementwise、矩阵乘、归约、reshape、transpose、类型转换等整块操作;编译器负责将这些 tile 操作映射到实际硬件线程,并在适用时映射到 Tensor Core 等硬件。(NVIDIA Docs)
3. 真正发生了什么职责转移
最准确的划分是下面这样。
| 决策 | SIMT | Tile |
|---|---|---|
| 总数据如何切成多个 block | 程序员 | 程序员 |
| grid 有多少个 block | 程序员 | 程序员 |
| 每个 block 处理哪块数据 | 程序员 | 程序员 |
| tile 的形状 | 程序员 | 程序员 |
| 算法的循环和数据流 | 程序员 | 程序员 |
| block 内多少线程 | 程序员 | 编译器 |
| 每个线程处理哪些元素 | 程序员 | 编译器 |
| tile 操作如何分摊到 warp/thread | 程序员 | 编译器 |
| tile 放寄存器还是 shared memory | 大量人工控制 | 主要由编译器决定 |
| 如何使用 Tensor Core/TMA | 通常显式、复杂 | 可由 tile primitive 降低到硬件 |
| 是否需要精细线程控制 | 支持 | 通常被抽象掉 |
因此更精确的一句话是:
Tile 编程把 thread block 内部的 execution mapping 从程序员手里移交给编译器,但 workload 到 tile/block 的 mapping 仍然由程序员负责。
4. 它最重要的好处不只是"少写代码"
4.1 降低并行编程难度
传统 SIMT 程序中,很多代码其实不是算法本身,而是在描述硬件执行细节:
text
threadIdx
warp lane
shared memory
barrier
vectorized load
bank conflict
coalescing
Tile 编程把它们隐藏在:
cpp
load tile
tile operation
store tile
后面。
于是程序员可以更多关注:
text
数据是什么形状?
应该切成多大的块?
这块数据要做什么运算?
多个 K tile 如何累加?
这恰好就是你学 AI accelerator mapping 时使用的抽象层次。
4.2 编译器更容易识别你的真实意图
SIMT 代码中,编译器看到的是大量标量线程操作:
cpp
x = A[i];
y = B[i];
z = x + y;
它需要从很多线程的标量行为中推断:
text
这些线程共同在执行一个向量加法
Tile 代码直接告诉编译器:
cpp
cTile = aTile + bTile;
或者:
cpp
acc = mma(aTile, bTile, acc);
也就是说,程序直接保留了更高层的运算语义。
从编译器角度看:
text
SIMT IR:
很多线程级 load、multiply、add、store
Tile IR:
load tile → matrix multiply-accumulate → store tile
后者更容易针对具体 GPU 架构选择:
- Tensor Core 指令;
- Tensor Memory Accelerator;
- 合适的线程数量;
- 寄存器或 shared-memory 存放;
- load、compute、store 的调度方式。
官方文档明确提到,Tile 的一个目标就是更简单地使用 TMA 和 Tensor Core 等较新的硬件性能能力;规则 tile-space load 在支持的硬件上可以被编译器降低为 TMA 操作。(NVIDIA Docs)
4.3 提高跨代 GPU 的可移植性
传统高性能 SIMT kernel 往往会绑定某一代硬件的细节:
text
某代 GPU 的 warp 数量
某种 Tensor Core 指令形状
某种 shared-memory pipeline
某种寄存器组织
某种 TMA 指令
GPU 架构升级后,旧 kernel 虽然可能还能运行,但不一定仍是最佳映射。
Tile 编程的目标是让相同的 tile 级源码在不同 GPU 架构上,由编译器重新选择 block 内部的线程和硬件映射。NVIDIA 文档明确说,因为线程级决策留给编译器,相同 tile kernel 可以在不同 GPU 架构上运行,而不必修改源码。(NVIDIA Docs)
理想状态下是:
text
相同源码:
A_tile @ B_tile
在较早 GPU 上
↓
编译成适合该架构的 Tensor Core / thread mapping
在较新 GPU 上
↓
编译成新的 Tensor Core + TMA mapping
这个思路类似于:
text
C/C++ 程序员不再手工给每条指令分配 CPU pipeline
而是描述运算,让编译器生成机器指令
只不过 Tile 把这种抽象进一步推进到了 GPU block 内部的并行执行层。
4.4 简化同步和控制流推理
在传统 SIMT 中,一个 block 内有很多逻辑线程:
text
thread 0
thread 1
...
thread 255
你必须考虑:
- 哪个线程先执行;
- 数据是否已经写入 shared memory;
- 是否需要
__syncthreads(); - 分支是否导致 warp divergence。
Tile 模型从程序员视角把每个 tile block 表示为一条逻辑控制流。整个 block 对 tile 操作,编译器将操作并行分配给实际线程。因此在 Tile 语言层面没有传统的 warp divergence 概念;标量操作由单个线程执行,tile 操作由 block 中的线程集体并行执行。(NVIDIA Docs)
例如:
cpp
if (condition) {
cTile = aTile + bTile;
}
你思考的是:
text
整个 block 是否执行这个 tile 加法
而不是:
text
warp 中哪几个 lane 进入 if
哪几个 lane 被 mask
这能显著减少并发正确性问题。
5. 但它绝不是"写一句矩阵乘,编译器什么都帮你做好"
这是最需要避免的误解。
程序员仍然需要决定:
5.1 Tile 大小
例如 GEMM:
text
C tile = 128 × 128
A tile = 128 × 32
B tile = 32 × 128
还是:
text
C tile = 64 × 128
A tile = 64 × 64
B tile = 64 × 128
不同 tile 大小仍然会影响:
- 数据复用;
- 寄存器压力;
- shared-memory 消耗;
- occupancy;
- 边界浪费;
- Tensor Core 利用率。
而且当前 Tile 模型要求 tile 的各个维度在编译期已知,并且是 2 的幂。(NVIDIA Docs)
5.2 Grid 如何覆盖数据
例如:
cpp
int blocks_x = ceil_div(N, TILE_N);
int blocks_y = ceil_div(M, TILE_M);
kernel<<<dim3(blocks_x, blocks_y), 1>>>(...);
仍然是程序员决定:
text
一个 block 对应 C 中哪个 tile
grid.x 映射 N
grid.y 映射 M
Tile 编译器不会自动把整个任意程序切成最优 grid。
5.3 算法和 dataflow
对于 GEMM:
cpp
for (int kTile = 0; kTile < numKTiles; ++kTile) {
auto a = load(A, ...);
auto b = load(B, ...);
acc = mma(a, b, acc);
}
这里依然是你决定:
text
沿 K 维循环
每次加载多大的 A/B tile
partial sum 在 block 内累加
什么时候写回 C
也就是说:
编译器负责 tile 内的空间展开 ,程序员仍负责 tile 之间的时间与空间组织。
这与你学 DNN accelerator mapper 非常相似:
text
程序员/mapper:
选择 loop order、tile size、空间绑定
底层编译器:
把 tile primitive 映射到硬件执行单元
5.4 仍然需要性能提示
Tile 编程也不是完全不需要硬件知识。
官方文档仍建议在 C++ Tile kernel 中使用:
cpp
__restrict__
以及:
cpp
ct::assume_aligned(ptr, 16_ic)
帮助编译器确认数组不重叠、指针对齐,从而生成更好的内存操作。如果编译器无法证明指针不重叠,就必须采取更保守的 load/store 调度。(NVIDIA Docs)
这说明 Tile 的模式不是:
text
程序员完全不需要管性能
而是:
text
程序员提供高层结构、形状、布局和约束
编译器负责较低层的线程与硬件映射
6. 为什么 vectorAdd 体现不出 Tile 的主要价值
vector add:
text
读 A
读 B
加一次
写 C
几乎没有:
- 数据复用;
- 矩阵乘;
- 多级 reduction;
- shared-memory blocking;
- Tensor Core;
- 复杂流水。
它通常主要受内存带宽限制。
所以把:
cpp
C[i] = A[i] + B[i];
改成:
cpp
cTile = aTile + bTile;
主要收益是代码表达更高层,不一定获得明显性能提升。
Tile 真正有吸引力的场景是:
text
GEMM
Attention
卷积
Reduction
Transpose
Softmax
Norm
多维数据搬运
Tensor Core 计算
TMA 搬运
因为这些场景过去需要程序员亲自处理大量:
text
warp/thread mapping
shared-memory tile
异步搬运
barrier
Tensor Core fragment
pipeline stage
Tile 模型试图把其中相当一部分交给编译器和内置 primitive。
7. 它和 Triton 的思想很接近
从编程思想上看,可以把它理解为:
text
传统 CUDA SIMT:
线程中心编程
CUDA Tile / Triton 类模型:
数据块中心编程
传统 CUDA 问:
当前线程的
threadIdx.x是多少?这个线程负责哪个元素?
Tile 模型问:
当前 block 对应哪个数据 tile?对整个 tile 执行什么操作?
例如:
text
SIMT 心智模型:
thread 37
读取 A[37]
读取 B[37]
计算 C[37]
Tile 心智模型:
block 3
读取 A[384:512]
读取 B[384:512]
整块相加
写回 C[384:512]
这是一种从:
text
execution-centric
以执行线程为中心
转向:
text
data-centric
以数据块和算子为中心
的变化。
8. 为什么 NVIDIA 现在才推这种模式
现代 GPU 的高性能路径越来越复杂:
text
Tensor Core
TMA
异步 copy
多级 pipeline
warp specialization
cluster
distributed shared memory
如果每出现一种硬件能力,都要求普通 CUDA 程序员手写:
text
新 intrinsic
新 barrier
新 shared-memory layout
新 warp 分工
那么:
- 编程门槛会越来越高;
- 代码会绑定某一代 GPU;
- 软件跟不上硬件演进;
- 同一个算子需要维护大量架构特化版本。
Tile 模型试图在程序员和 GPU 微架构之间增加一层:
text
算法 / tile 表达
↓
CUDA Tile IR
↓
架构相关的编译优化
↓
threads / warps / Tensor Core / TMA
CUDA Tile C++ 和 cuTile Python 共用 CUDA Tile IR 后端,这也说明 NVIDIA 是在建立一个比传统 thread-level CUDA 更高层的编译表示。(NVIDIA Docs)
9. 它不会取代 SIMT
NVIDIA 明确说明 Tile 和 SIMT 会共存,选择是 per-kernel 的。Tile 不会替代 SIMT;当算法需要细粒度线程控制、特殊 warp 协作或某些高度定制优化时,SIMT 仍然必要。(NVIDIA Docs)
可以预期两种模式分别适合:
text
Tile 更适合:
规则张量计算
矩阵运算
规则 reduction
结构化数据搬运
算子开发
快速适配新硬件
SIMT 更适合:
不规则图算法
复杂 per-thread 分支
精细 warp specialization
特殊同步协议
手工追求极限性能
不规则 scatter/gather
因此它不是:
text
旧 CUDA 被新 CUDA 淘汰
而是:
text
CUDA 增加了一个更高层的 kernel 编程入口
10. 一句话总结
你的理解基本正确,但可以再精确一点:
CUDA Tile 的主要价值,是让程序员从手工指定 block 内每个 thread/warp 如何搬数据、计算和同步中释放出来,改为描述 tile 级的数据与运算;编译器负责把 tile primitive 映射到具体线程、寄存器、shared memory、Tensor Core 和 TMA。
但程序员仍必须负责:
text
workload 如何切 tile
tile 多大
grid 如何组织
数据布局是什么
tile 之间如何循环与复用
选择什么算法和 dataflow
所以不是"编译器替你做并行算法设计",而是:
text
程序员设计并行算法与 tile mapping
+
编译器完成 tile 内部的线程级实现
从 AI 芯片架构的角度,它相当于把编程接口从:
text
直接编写 PE/线程的每拍行为
提升为:
text
描述一个 compute tile 的输入、输出和算子
让 mapper/compiler 绑定到底层执行资源