Tile Programming 是 CUDA 13.3 新增的 CUDA Tile 编程模型 ,不是传统 SIMT 代码里用 shared memory 手工做 tiling,也不是
cooperative_groups::tiled_partition。
在传统 SIMT 中,你写的是"每个线程做什么 ";在 CUDA Tile 中,你写的是"整个 thread block 对一块多维数据 tile 做什么 ",由编译器把 tile 操作映射到具体线程。CUDA Tile C++ 使用 cuda_tile.h、cuda::tiles 和 __tile_global__;host 端的 cudaMalloc、cudaMemcpy 等代码不变。(NVIDIA Docs)
一、原始 SIMT vectorAdd 在做什么
NVIDIA 原版 kernel 是:
cpp
__global__ void vectorAdd(
const float *A,
const float *B,
float *C,
int numElements)
{
int i = blockDim.x * blockIdx.x + threadIdx.x;
if (i < numElements) {
C[i] = A[i] + B[i] + 0.0f;
}
}
启动方式:
cpp
int threadsPerBlock = 256;
int blocksPerGrid =
(numElements + threadsPerBlock - 1) / threadsPerBlock;
vectorAdd<<<blocksPerGrid, threadsPerBlock>>>(
d_A, d_B, d_C, numElements);
它的视角是:
text
一个线程
↓
计算一个全局下标 i
↓
读取 A[i]、B[i]
↓
计算 C[i]
也就是:
text
thread 0 → C[0]
thread 1 → C[1]
thread 2 → C[2]
...
这是典型的 per-thread programming 。(GitHub)
二、改成 CUDA Tile 后的核心代码
下面用一个 tile 处理连续的 128 个 float。
cpp
#include "cuda_tile.h"
__tile_global__ void vectorAddTile(
const float *__restrict__ A,
const float *__restrict__ B,
float *__restrict__ C,
int numElements)
{
namespace ct = ::cuda::tiles;
using namespace ct::literals;
// 给原始一维指针附加数组形状:
// A、B、C 都是长度为 numElements 的一维数组。
auto aSpan = ct::tensor_span{
A, ct::extents{numElements}
};
auto bSpan = ct::tensor_span{
B, ct::extents{numElements}
};
auto cSpan = ct::tensor_span{
C, ct::extents{numElements}
};
// 将数组逻辑上切分成一个个 128 元素的 tile。
//
// tile 0: [0, 127]
// tile 1: [128, 255]
// tile 2: [256, 383]
// ...
auto aView = ct::partition_view{
aSpan, ct::shape{128_ic}
};
auto bView = ct::partition_view{
bSpan, ct::shape{128_ic}
};
auto cView = ct::partition_view{
cSpan, ct::shape{128_ic}
};
// 当前 block 在 grid 中的编号。
// 这里它同时也是要处理的 tile 编号。
int tileId = ct::bid().x;
// 整个 block 协作加载一个 128 元素的 tile。
// 最后一个 tile 如果不足 128 个元素,越界位置自动填 0。
auto aTile = aView.load_masked(tileId);
auto bTile = bView.load_masked(tileId);
// 这是整个 tile 的逐元素加法:
//
// cTile[j] = aTile[j] + bTile[j]
//
// 你不再写 threadIdx,也不再自己决定每个线程算哪个 j。
// 编译器负责把 128 个加法分配给 block 内的硬件线程。
auto cTile = aTile + bTile;
// 写回当前 tile。
// 最后一个 tile 中越界的写入会被自动丢弃。
cView.store_masked(cTile, tileId);
}
这段代码的核心语义是:
text
一个 tile block
↓
加载 A 的 128 个元素
加载 B 的 128 个元素
↓
对两个 tile 做逐元素加法
↓
写回 C 的 128 个元素
这里的 tile 是由编译器管理的数据值,不保证一定存放在 shared memory;编译器可以根据目标架构选择寄存器、shared memory 或其他 SM 资源。Tile 的每个维度必须在编译期已知并且为 2 的幂。(NVIDIA Docs)
三、启动代码怎么改
原来的 SIMT 启动代码:
cpp
int threadsPerBlock = 256;
int blocksPerGrid =
(numElements + threadsPerBlock - 1) / threadsPerBlock;
vectorAdd<<<blocksPerGrid, threadsPerBlock>>>(
d_A, d_B, d_C, numElements);
改成:
cpp
constexpr int tileElements = 128;
int tileBlocks =
(numElements + tileElements - 1) / tileElements;
// 注意第二个参数必须写 1。
//
// 它不是说真正只有一个硬件线程。
// CUDA Tile 中,实际每个 block 使用多少线程由编译器决定。
vectorAddTile<<<tileBlocks, 1>>>(
d_A, d_B, d_C, numElements);
CUDA Tile 仍然使用三尖括号启动,但第二个 launch 参数必须是 1。这个 1 只是 Tile 编程模型的启动形式,并不代表 GPU 真正只使用一个线程执行 128 次加法;实际的 block 线程数量及线程分工由 Tile 编译器生成。(NVIDIA Docs)
四、完整可编译版本
保存为:
text
vectorAddTile.cu
代码如下:
cpp
#include <cuda_runtime.h>
#include "cuda_tile.h"
#include <cmath>
#include <cstdio>
#include <cstdlib>
#define CUDA_CHECK(call) \
do { \
cudaError_t error__ = (call); \
if (error__ != cudaSuccess) { \
std::fprintf( \
stderr, \
"CUDA error at %s:%d: %s\n", \
__FILE__, \
__LINE__, \
cudaGetErrorString(error__)); \
std::exit(EXIT_FAILURE); \
} \
} while (0)
constexpr int kNumElements = 50000;
// 这是一个 tile 中的数据元素数量,
// 不是每个 block 的线程数量。
constexpr int kTileElements = 128;
/*
* CUDA Tile kernel
*
* 每个 tile block 负责 kTileElements 个连续元素:
*
* block 0 -> [0, 127]
* block 1 -> [128, 255]
* block 2 -> [256, 383]
* ...
*/
__tile_global__ void vectorAddTile(
const float *__restrict__ A,
const float *__restrict__ B,
float *__restrict__ C,
int numElements)
{
namespace ct = ::cuda::tiles;
using namespace ct::literals;
/*
* tensor_span 给原始指针附加数组的逻辑形状。
*
* numElements 是运行时值,因此:
*
* ct::extents{numElements}
*
* 表示一个运行时长度的一维数组。
*/
auto aSpan = ct::tensor_span{
A,
ct::extents{numElements}
};
auto bSpan = ct::tensor_span{
B,
ct::extents{numElements}
};
auto cSpan = ct::tensor_span{
C,
ct::extents{numElements}
};
/*
* partition_view 把数组逻辑划分成固定大小、互不重叠的 tile。
*
* 128_ic 表示编译期常数 128。
* Tile 的形状必须在编译期确定。
*/
auto aView = ct::partition_view{
aSpan,
ct::shape{128_ic}
};
auto bView = ct::partition_view{
bSpan,
ct::shape{128_ic}
};
auto cView = ct::partition_view{
cSpan,
ct::shape{128_ic}
};
/*
* 当前 tile block 的 x 方向编号。
*
* SIMT 中通常写:
*
* blockIdx.x
*
* Tile 编程中使用:
*
* ct::bid().x
*/
int tileId = ct::bid().x;
/*
* load_masked:
*
* 加载当前 tile。
*
* 如果最后一个 tile 只有一部分有效元素,
* 越界元素自动使用 0 填充。
*/
auto aTile = aView.load_masked(tileId);
auto bTile = bView.load_masked(tileId);
/*
* 对整个 tile 进行逐元素运算。
*
* 这是一个 tile-level expression,不是单个线程的标量表达式。
* 编译器负责把这些加法映射到 block 内的线程。
*/
auto cTile = aTile + bTile;
/*
* store_masked:
*
* 写回有效元素。
* 最后一个 tile 中越界部分的写入会被丢弃。
*/
cView.store_masked(cTile, tileId);
}
int main()
{
const int numElements = kNumElements;
const std::size_t size =
static_cast<std::size_t>(numElements) * sizeof(float);
std::printf(
"[CUDA Tile vector addition of %d elements]\n",
numElements);
/*
* 分配 host memory。
*/
float *h_A = static_cast<float *>(std::malloc(size));
float *h_B = static_cast<float *>(std::malloc(size));
float *h_C = static_cast<float *>(std::malloc(size));
if (h_A == nullptr || h_B == nullptr || h_C == nullptr) {
std::fprintf(stderr, "Failed to allocate host memory\n");
std::free(h_A);
std::free(h_B);
std::free(h_C);
return EXIT_FAILURE;
}
/*
* 初始化输入。
*/
for (int i = 0; i < numElements; ++i) {
h_A[i] = static_cast<float>(i) * 0.001f;
h_B[i] = 1.0f + static_cast<float>(i) * 0.0001f;
}
/*
* 分配 device memory。
*/
float *d_A = nullptr;
float *d_B = nullptr;
float *d_C = nullptr;
CUDA_CHECK(cudaMalloc(
reinterpret_cast<void **>(&d_A), size));
CUDA_CHECK(cudaMalloc(
reinterpret_cast<void **>(&d_B), size));
CUDA_CHECK(cudaMalloc(
reinterpret_cast<void **>(&d_C), size));
/*
* Host -> Device。
*/
CUDA_CHECK(cudaMemcpy(
d_A,
h_A,
size,
cudaMemcpyHostToDevice));
CUDA_CHECK(cudaMemcpy(
d_B,
h_B,
size,
cudaMemcpyHostToDevice));
/*
* 每个 tile block 处理 128 个数据元素。
*/
const int tileBlocks =
(numElements + kTileElements - 1) /
kTileElements;
std::printf(
"Launch %d tile blocks, "
"%d data elements per tile\n",
tileBlocks,
kTileElements);
/*
* Tile kernel 启动时:
*
* 第一个参数:grid 中有多少个 tile block
* 第二个参数:必须是 1
*
* 真实 block 内线程数量由 Tile 编译器决定。
*/
vectorAddTile<<<tileBlocks, 1>>>(
d_A,
d_B,
d_C,
numElements);
CUDA_CHECK(cudaGetLastError());
CUDA_CHECK(cudaDeviceSynchronize());
/*
* Device -> Host。
*/
CUDA_CHECK(cudaMemcpy(
h_C,
d_C,
size,
cudaMemcpyDeviceToHost));
/*
* 验证结果。
*/
for (int i = 0; i < numElements; ++i) {
const float expected = h_A[i] + h_B[i];
const float error = std::fabs(h_C[i] - expected);
if (error > 1.0e-5f) {
std::fprintf(
stderr,
"Verification failed at index %d: "
"A=%f B=%f C=%f expected=%f\n",
i,
h_A[i],
h_B[i],
h_C[i],
expected);
CUDA_CHECK(cudaFree(d_A));
CUDA_CHECK(cudaFree(d_B));
CUDA_CHECK(cudaFree(d_C));
std::free(h_A);
std::free(h_B);
std::free(h_C);
return EXIT_FAILURE;
}
}
std::printf("Test PASSED\n");
CUDA_CHECK(cudaFree(d_A));
CUDA_CHECK(cudaFree(d_B));
CUDA_CHECK(cudaFree(d_C));
std::free(h_A);
std::free(h_B);
std::free(h_C);
return EXIT_SUCCESS;
}
这基本保留了原始 vectorAdd 的 host 端流程,只把 SIMT kernel 和 launch 配置换成了 Tile 编程形式。官方文档也说明,内存分配、host/device 数据传输和 kernel 调度等外围代码与 SIMT 相同,主要变化发生在 kernel 内部。(NVIDIA Docs)
五、编译命令
CUDA Tile C++ 要求:
- CUDA Toolkit 13.3 或更新版本;
- 包含
cuda_tile.h; - 使用
--enable-tile; cuda_tile.hAPI 需要 C++20;- 目标 GPU 架构至少为
sm_80。
编译示例:
bash
nvcc \
--enable-tile \
-std=c++20 \
-arch=sm_80 \
vectorAddTile.cu \
-o vectorAddTile
运行:
bash
./vectorAddTile
应输出类似:
text
[CUDA Tile vector addition of 50000 elements]
Launch 391 tile blocks, 128 data elements per tile
Test PASSED
请把 sm_80 替换成你实际需要生成代码的架构,但不能低于 sm_80。如果不指定架构,NVCC 默认目标可能不会产生可运行的 Tile code,因此显式写 -arch=sm_XX 很重要。(NVIDIA Docs)
六、50000 个元素实际是怎么切的
设:
text
N = 50000
TILE = 128
tile block 数量:
text
ceil(50000 / 128)
= 391
映射关系:
text
tile block 0
A[0 ... 127]
B[0 ... 127]
C[0 ... 127]
tile block 1
A[128 ... 255]
B[128 ... 255]
C[128 ... 255]
tile block 2
A[256 ... 383]
B[256 ... 383]
C[256 ... 383]
...
tile block 390
理论范围:[49920 ... 50047]
有效范围:[49920 ... 49999]
最后一个 tile:
text
128 个 tile 位置
├── 80 个有效元素
└── 48 个越界位置
因此:
cpp
aView.load_masked(tileId);
把越界的 48 个位置填成零,而:
cpp
cView.store_masked(cTile, tileId);
只写回 80 个有效结果,另外 48 个越界写入被丢弃。这就取代了 SIMT 代码中的:
cpp
if (i < numElements)
官方 Tile C++ API 对 load_masked 和 store_masked 的边界行为就是这样定义的。(NVIDIA Docs)
七、SIMT 与 Tile 代码一一对应
SIMT vectorAdd |
CUDA Tile vectorAddTile |
|---|---|
__global__ |
__tile_global__ |
| 一个 kernel 实例对应一个线程 | 一个 kernel 实例对应一个逻辑 tile block |
threadIdx.x |
不使用 |
blockDim.x |
不使用 |
blockIdx.x |
ct::bid().x |
| 每个线程处理一个元素 | 一个 block 操作整个 tile |
C[i] = A[i] + B[i] |
cTile = aTile + bTile |
if (i < N) |
load_masked/store_masked |
<<<grid, 256>>> |
<<<grid, 1>>> |
| 程序员决定 block 线程数 | 编译器决定 block 线程数 |
| 程序员映射元素到线程 | 编译器映射 tile 运算到线程 |
最重要的认知变化是:
cpp
// SIMT:程序员写一个线程的行为
C[i] = A[i] + B[i];
变为:
cpp
// Tile:程序员写整个数据块的行为
cTile = aTile + bTile;
八、这里的 tile 与传统"手工 tiling"不是同一个层次
传统 CUDA GEMM 里经常写:
cpp
__shared__ float As[16][16];
__shared__ float Bs[16][16];
As[threadIdx.y][threadIdx.x] = ...;
Bs[threadIdx.y][threadIdx.x] = ...;
__syncthreads();
这仍然属于 SIMT programming:
- 你自己指定
threadIdx; - 自己决定每个线程搬哪个数据;
- 自己分配 shared memory;
- 自己同步;
- 自己做线程到 tile 元素的 mapping。
而 CUDA Tile 是更高一级的表达:
cpp
auto aTile = aView.load_masked(tileId);
auto bTile = bView.load_masked(tileId);
auto cTile = aTile + bTile;
cView.store_masked(cTile, tileId);
程序员只描述 tile 的数据运算,编译器决定:
text
需要多少线程
如何分工
如何搬数据
tile 放寄存器还是 shared memory
是否使用特定硬件单元
因此它更接近你学习 AI 芯片 mapper 时的思路:
text
算法描述:
整块 tile 做什么
编译器:
把 tile 映射到线程、warp、寄存器、shared memory 和硬件单元
九、不要期待这个 vector add 因 Tile 自动快很多
vectorAdd 只有:
text
读取 A
读取 B
做一次加法
写 C
它几乎没有数据复用,通常主要受 global-memory bandwidth 限制。把它改成 Tile 的主要价值是帮助理解编程模型,而不一定获得明显性能提升。
CUDA Tile 更有价值的场景通常是:
- GEMM;
- Attention;
- reduction;
- transpose;
- 多维 tile 搬运;
- 使用 Tensor Core 的矩阵乘;
- 能利用 Tensor Memory Accelerator 的规则数据搬运。
官方文档也把 CUDA Tile 的主要价值描述为:让程序员用更高层的 tile 操作表达并行性,并由编译器更方便地映射到 TMA、Tensor Core 等新硬件能力;它并不替代所有需要精细线程控制的 SIMT kernel。(NVIDIA Docs)