cuTile Python 把 “写内核” 这件事压缩成了三步:加载 tile、算 tile、写回 tile。它把线程块、共享内存排布、Tensor Core 调度全部藏进编译器,只留下一套 Python 级 DSL。本文用 16×16 矩阵乘做主线,把 “自动共享内存布局” 与 “可扩展参数” 这两个最容易踩坑的点拆给你看。
从 SIMT 到 tile-based:cuTile 的设计动机
传统 CUDA C 的 SIMT 模型要求开发者手动划分线程索引、计算偏移、申请 __shared__ 并处理 bank conflict。cuTile 把 “线程” 这个概念弱化,改用 “tile”—— 一个形状在编译期就确定的子矩阵 —— 作为最小并行单元。运行时,你只需要告诉 cuTile:
- 全局数组多大
- 每个 tile 多大
- 要做什么运算
编译器会自动把 tile 映射到线程块、把共享内存排成 swizzle 模式、把矩阵乘拆到 Tensor Core。宿主代码通过 ct.launch() 把 kernel 扔进 CUDA 流即可。
tile 与 array 的两级内存抽象
cuTile 的数据层只有两级:
- array:住在 global memory,可变、有 stride,宿主端可以是 CuPy、PyTorch 甚至 NumPy(通过 DLPack)。
- tile:住在寄存器或共享内存,不可变值对象,维度必须是 2 的幂且 ≤512。
关键函数只有四个:
a_tile = ct.load(array, index=(block_id,), shape=(TILE_SIZE,))
... # 运算
ct.store(array, index=(block_id,), tile=a_tile)
共享内存排布完全由编译器决定。以 16×16 的 float16 tile 为例,后端会选用 32-byte swizzle,保证相邻线程访问同一行时落在不同 bank,天然免疫冲突。如果你想手动 double-buffer,只需再申请一个同名 tile,编译器会自动做阶段划分与预取。
实战:16×16 矩阵乘 kernel 逐行拆解
下面给出 “C = A × B” 最精简写法,tile 大小 16×16,grid 按输出矩阵的 tile 数计算:
import cuda.tile as ct
import cupy as cp
@ct.kernel
def gemm_16x16(A, B, C, M, N, K):
# 当前 tile 在全局输出矩阵中的坐标
tile_x = ct.bid(0) # 列方向
tile_y = ct.bid(1) # 行方向
# 申请三个 tile
a_frag = ct.tile((16, 16), dtype=ct.float16)
b_frag = ct.tile((16, 16), dtype=ct.float16)
c_frag = ct.tile((16, 16), dtype=ct.float32)
# K 维按 16 步长累加
for k_tile in range(0, K, 16):
ct.load(a_frag, A, index=(tile_y, k_tile // 16))
ct.load(b_frag, B, index=(k_tile // 16, tile_x))
c_frag += ct.matmul(a_frag, b_frag) # 自动启用 Tensor Core
ct.store(C, index=(tile_y, tile_x), tile=c_frag)
# 宿主端调用
M, N, K = 4096, 4096, 4096
grid = (M // 16, N // 16, 1)
ct.launch(cp.cuda.get_current_stream(), grid, gemm_16x16,
(A, B, C, M, N, K))
注意三点:
ct.matmul会在编译期识别到float16×float16→float32的 Tensor Core 路径,无需wmma或mma指令。- 共享内存大小由编译器根据 tile 形状与双缓冲策略自动计算,默认每线程块 48 KiB 以内都能放下。
- 如果 M/N/K 不是 16 的整数倍,只需在 kernel 里加边界判断,或在外层用
ct.cdiv向上取整。
可扩展参数清单
| 参数 | 推荐值 | 上限 | 备注 |
|---|---|---|---|
| tile 行 / 列 | 16, 32, 64, 128 | 512 | 必须是 2 的幂;越大共享内存压力越高 |
| 每 SM 同时驻留块 | — | 32 | 由寄存器 + 共享内存同时决定,Nsight Compute 看 Achieved Active Warps Per SM |
| grid 维度 | 任意 | 2^31-1 | 按输出 tile 数计算即可,cuTile 自动映射到 hardware blockIdx |
| 占用率经验公式 | min(1, 32 / (reg_tiles + 2)) |
— | reg_tiles 为 kernel 里同时 live 的 tile 数,>0.5 即算良好 |
Nsight Compute 必看指标:
sm__sass_average_data_ports_busy:接近 100% 说明 Tensor Core 吃满。shared_load_conflict:cuTile 编译后应为 0,若不为 0 说明 tile 形状或 dtype 触发 fallback。launch__occupancy:与上述经验公式互验,低于 0.35 考虑减 tile 大小或减少 live tile。
局限与回退策略
- 动态形状:tile 维度必须编译期已知。若你的业务在运行时才知道矩阵宽高,需要在外层做 padding 到 2 的幂,或退回普通 CUDA kernel。
- 非 2 的幂维度:cuTile 会直接报错。可用
ct.cdiv向上对齐,再在 kernel 里用掩码忽略多余元素。 - 平台支持:目前仅 Linux x86_64/aarch64 与 Windows x86_64,macOS 无计划。CI 环境可用官方 Docker:
nvidia/cuda:13.1-devel-ubuntu22.04。
小结
cuTile Python 把 “共享内存排布” 与 “Tensor Core 调度” 这两件最费手的活收进编译器,留给开发者的只剩三件事:选 tile 大小、写运算、调 grid。只要记住 “tile 必须是 2 的幂” 与 “grid 按输出 tile 数算” 这两条铁律,你就能在 30 行代码内拿到一块可扩展、可剖面、跨 GPU 架构的 GEMM 内核。
资料来源
[1] NVIDIA/cutile-python GitHub 仓库
[2] cuTile Python 官方文档