【AI】CUDA的新编程模型:tile编程模型

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.hcuda::tiles__tile_global__;host 端的 cudaMalloccudaMemcpy 等代码不变。(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.h API 需要 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_maskedstore_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)

相关推荐
Qyr995 小时前
吞咽困难介护食:老龄化社会中的营养安全守护者
人工智能
火山引擎开发者社区5 小时前
文件上传即可检索|实时多模态向量链路落地实践分享
人工智能
YYJ-F5 小时前
Anthropic——AI安全人工智能研究公司,核心产品Claude Code辅助编程
人工智能·安全
具身智能进化论5 小时前
协作机器人产业进入规模化部署期,未来几年的增长从何而来
大数据·运维·人工智能·机器人·自动化·工厂方法模式
hzcj8885 小时前
汇正财经:储能装机回暖,估值有待提升
大数据·人工智能
妄想出头的工业炼药师5 小时前
GLAM-SLAM
人工智能
一直都在5725 小时前
LangChain4j精讲
开发语言·人工智能
八号当铺5 小时前
使用 Figma Agent Kit:插件 + MCP + 还原 Skill,打通本地设计协作
前端·人工智能·ai编程
今天AI了吗5 小时前
时序大模型 TimechoAI 实战:从数据接入到智能时序分析全链路指南
人工智能
AI服务老曹5 小时前
多路摄像头AI分析完整流程:硬件选型与GPU/NPU算力估算指南
人工智能