1. 引言:一个被低估的名字
如果你写过 CUDA 高性能算子,大概率听过 CUTLASS,也大概率在某个深夜被 cute::Layout、cute::Tensor、Swizzle 这些词劝退过。而 CuTe,正是 CUTLASS 3.x 之后被推到台前的核心组件------它不是一个"库",更像是一套用 C++ 模板表达张量布局与线程映射的代数系统。
本文想聊清楚几件事:CuTe 是什么、它为什么会被设计出来、它的核心思想是什么(配可运行代码)、它这些年坚持了什么又放弃了什么、当前的热点方向,以及它和 Triton、TileLang 这类 GPU DSL 的对比------尤其是"它是 C++ 模板库而非 Python DSL、不基于 MLIR/TVM、编译过程不是黑盒"这一点带来的差异。
说明:CuTe 的 API 在 CUTLASS 3.x 各版本间有演进,本文示例基于 CUTLASS 3.x 的 CuTe 接口,实际使用时请以你本地版本的
cute/tensor.hpp为准。
2. 前世:CuTe 诞生之前的世界
2.1 手写 CUDA 的困境
在 CuTe 之前,写一个高性能 GEMM 大致是这样的流程:
- 手工推导 shared memory 的分块与 bank conflict 规避方案;
- 手工计算每个线程负责的
(m, n, k)坐标; - 手工写
ldmatrix/mma.sync的内联 PTX; - 换一个 tile size,上面所有推导重来一遍。
问题在于:布局(layout)和线程映射(thread mapping)是耦合在代码里的,一旦硬件换代(比如从 Ampere 到 Hopper 的 TMA、WGMMA),大量代码需要重写。
2.2 CUTLASS 2.x 的模板化尝试
CUTLASS 2.x 用 C++ 模板把 GEMM 拆成了 ThreadblockShape、WarpShape、InstructionShape 等策略类,通过模板参数组合出不同的 kernel。这解决了"复用"问题,但没解决"表达力"问题:
- 布局仍然是通过一堆嵌套的
cutlass::layout::RowMajor、PitchLinear组合出来的,可读性差; - 想做非标准的 swizzle、非标准的线程重排,需要写大量特化;
- 编译错误信息极其冗长,调试成本高。
2.3 行业环境与研究背景
CuTe 的设计动机,和 2020---2022 年前后的几个行业趋势直接相关:
- Tensor Core 指令越来越复杂 :从
mma.sync到 Hopper 的wgmma,再到 Blackwell 的tcgen05,指令对操作数布局的要求越来越苛刻(比如 WGMMA 要求 A 在 shared memory 中满足特定的 swizzle 模式)。 - 异步与流水线成为标配 :
cp.async、TMA、多级 pipeline,让"数据搬运"和"计算"的调度成为性能关键,而调度本质上是对布局的变换。 - 算子种类爆炸:Attention、MoE、卷积、归一化......每个算子都需要自定义的 tile 与线程映射,靠"特化"不可持续。
- MLIR/TVM 路线的启发与反思 :TVM 用调度原语(schedule primitives)表达变换,MLIR 用 dialect 表达多层抽象。CUTLASS 团队选择了另一条路------不引入编译器基础设施,而是用 C++ 模板在编译期完成布局代数运算。
于是 CuTe(CUDA Tensor)诞生了:把"张量布局"抽象成一个可以做代数运算的数学对象。
3. 核心设计思想:Layout 即函数
3.1 一句话概括
CuTe 的核心思想可以概括为:Layout 是一个从"逻辑坐标"到"物理偏移"的映射函数,而 Tensor 是 Layout 加上一块数据指针。
在 CuTe 中:
Layout由Shape和Stride组成,写作(Shape) : (Stride);- 一个
Layout可以理解为f(coord) = sum(coord[i] * stride[i]); Tensor=Layout+Engine(通常是指针)。
这个抽象的美妙之处在于:布局可以像数字一样做运算 。composition、product、logical_divide、swizzle 这些操作,本质上都是对映射函数的组合与变换。
3.2 一个可运行的代码示例
下面这段代码演示了 CuTe 最核心的三件事:构造 Layout、做布局变换、用 Tensor 访问数据。
cpp
// 编译:nvcc -std=c++17 -I<cutlass>/include example.cu -o example
#include <cute/tensor.hpp>
#include <cstdio>
using namespace cute;
int main() {
// 1) 构造一个 4x8 的行主序 Layout
// Shape = (4, 8), Stride = (8, 1)
// 即 f(i, j) = i * 8 + j
auto layout_row_major = make_layout(make_shape(Int<4>{}, Int<8>{}),
make_stride(Int<8>{}, Int<1>{}));
// 2) 构造一个列主序 Layout
// Shape = (4, 8), Stride = (1, 4)
// 即 f(i, j) = i * 1 + j * 4
auto layout_col_major = make_layout(make_shape(Int<4>{}, Int<8>{}),
make_stride(Int<1>{}, Int<4>{}));
print("row-major: "); print(layout_row_major); print("\n");
print("col-major: "); print(layout_col_major); print("\n");
// 3) 布局代数:把 (4, 8) 拆成 ((2, 2), (4, 2))
// logical_divide 是 CuTe 里最常用的变换之一
auto tiled = logical_divide(layout_row_major,
make_shape(Int<2>{}, Int<4>{}));
print("logical_divide: "); print(tiled); print("\n");
// 4) 用 Layout 构造 Tensor,并访问元素
float data[32];
for (int i = 0; i < 32; ++i) data[i] = static_cast<float>(i);
auto tensor = make_tensor(make_gmem_ptr(data), layout_row_major);
// tensor(2, 3) 等价于 data[2 * 8 + 3] = data[19]
printf("tensor(2, 3) = %.1f\n", tensor(2, 3));
// 5) 切片:取出第 2 行,得到一个长度为 8 的一维 Tensor
auto row2 = tensor(Int<2>{}, _);
print("row2 layout: "); print(row2.layout()); print("\n");
printf("row2(3) = %.1f\n", row2(3));
return 0;
}
这段代码里最值得体会的是第 3 步:logical_divide 把一个 (4,8):(8,1) 的布局拆成了"外层 tile 坐标 + 内层 tile 内坐标"的两级结构。这正是所有 GPU 分块算法的数学本质------把大矩阵切成 tile,再在 tile 内做局部计算。CuTe 把这个过程变成了一个可以组合的函数调用,而不是一堆手写的索引运算。
3.3 用 Layout 写一个算子:分块 GEMM 的骨架
上一节的示例只演示了"布局是什么",但 CuTe 真正的价值在于用布局直接驱动一个算子 。下面这段代码用 CuTe 实现一个最简化的分块 GEMM 骨架:把 C = A × B 拆成 threadblock 级 tile,用 local_tile 切块、用 TiledCopy 搬运、用 TiledMMA 计算。它不追求性能极致,但完整展示了"布局 → 分块 → 拷贝 → 计算"的算子写法。
cpp
// 编译:nvcc -std=c++17 -arch=sm_80 -I<cutlass>/include gemm_tile.cu -o gemm_tile
#include <cute/tensor.hpp>
#include <cstdio>
using namespace cute;
// 一个极简的分块 GEMM:C[M,N] = A[M,K] * B[K,N]
// 这里只演示 CuTe 的布局驱动写法,省略了 pipeline / swizzle 等优化
template <class ProblemShape, class TiledMMA, class TiledCopyA, class TiledCopyB>
__global__ void tile_gemm_kernel(ProblemShape problem,
TiledMMA tiled_mma,
TiledCopyA copy_a,
TiledCopyB copy_b,
float const* A, float const* B, float* C) {
// 1) 用 problem shape 构造全局张量:布局由 stride 决定
auto M = get<0>(problem);
auto N = get<1>(problem);
auto K = get<2>(problem);
auto gA = make_tensor(make_gmem_ptr(A), make_layout(make_shape(M, K), GenRowMajor{}));
auto gB = make_tensor(make_gmem_ptr(B), make_layout(make_shape(K, N), GenRowMajor{}));
auto gC = make_tensor(make_gmem_ptr(C), make_layout(make_shape(M, N), GenRowMajor{}));
// 2) 取出当前 threadblock 负责的 tile
// local_tile 用布局代数把全局张量切成 (BLK_M, BLK_K) 的块
auto blk_m = Int<128>{};
auto blk_n = Int<128>{};
auto blk_k = Int<8>{};
auto blk_coord = make_coord(blockIdx.x, blockIdx.y, _);
auto gA_blk = local_tile(gA, make_shape(blk_m, blk_k), make_coord(_, _, blockIdx.z));
auto gB_blk = local_tile(gB, make_shape(blk_k, blk_n), make_coord(_, _, blockIdx.z));
auto gC_blk = local_tile(gC, make_shape(blk_m, blk_n), blk_coord);
// 3) 为 shared memory 构造布局,并用 TiledCopy 建立拷贝关系
// smem layout 由 TiledCopy 的 dst 布局推导,保证与 MMA 的 fragment 对齐
__shared__ float smem_a[cosize_v<decltype(tiled_mma.get_slice(0).partition_A(
make_layout(make_shape(blk_m, blk_k))))>];
__shared__ float smem_b[cosize_v<decltype(tiled_mma.get_slice(0).partition_B(
make_layout(make_shape(blk_k, blk_n))))>];
auto sA = make_tensor(make_smem_ptr(smem_a), tiled_mma.get_slice(0).partition_A(
make_layout(make_shape(blk_m, blk_k))));
auto sB = make_tensor(make_smem_ptr(smem_b), tiled_mma.get_slice(0).partition_B(
make_layout(make_shape(blk_k, blk_n))));
// 4) 建立累加器 fragment,并清零
auto acc = partition_fragment_C(tiled_mma, make_shape(blk_m, blk_n));
clear(acc);
// 5) 主循环:拷贝 tile 到 smem,再交给 MMA 计算
for (int k = 0; k < K / blk_k; ++k) {
// 用 TiledCopy 把全局 tile 搬到 shared memory
copy(copy_a, gA_blk(_, _, k), sA);
copy(copy_b, gB_blk(_, _, k), sB);
// 用 TiledMMA 做分块矩阵乘,累加到 acc
gemm(tiled_mma, sA, sB, acc);
}
// 6) 把累加器写回全局内存
auto gC_tile = local_tile(gC, make_shape(blk_m, blk_n), blk_coord);
copy(acc, gC_tile);
}
int main() {
// 问题规模与 tile 配置
auto problem = make_shape(Int<1024>{}, Int<1024>{}, Int<1024>{});
auto tiled_mma = make_tiled_mma(SM80_16x8x8_F32F16F16F32_TN{});
auto copy_a = make_tiled_copy_A(SM75_16x8x8_F32F16F16F32_TN{}, tiled_mma);
auto copy_b = make_tiled_copy_B(SM75_16x8x8_F32F16F16F32_TN{}, tiled_mma);
// 这里只打印布局,实际运行需要分配 device 内存并 launch kernel
print("tiled_mma: "); print(tiled_mma); print("\n");
print("copy_a: "); print(copy_a); print("\n");
return 0;
}
这段代码里,每一处"分块"和"搬运"都是布局运算的结果,而不是手写的索引:
local_tile用make_shape(blk_m, blk_k)把全局张量切成 tile,返回的仍是一个 Tensor,布局自动变成"tile 坐标 + tile 内坐标"的两级结构;tiled_mma.get_slice(0).partition_A(...)根据 MMA 指令对操作数的布局要求,自动推导出 shared memory 中 A 的 fragment 布局------你不需要手算 swizzle,CuTe 会保证它满足指令约束;copy(copy_a, gA_blk, sA)中的copy_a是一个TiledCopy,它把"哪个线程搬哪个元素"编码成布局,copy只是按布局执行;gemm(tiled_mma, sA, sB, acc)同理,tiled_mma把"哪个线程算哪个输出"编码成布局,gemm按布局展开成具体的mma.sync指令。
换句话说,写算子的过程就是不断构造和变换布局的过程:全局布局 → tile 布局 → fragment 布局 → 指令布局。这也是为什么 CuTe 的代码看起来"全是类型推导"------因为布局本身就是类型的一部分,编译器在编译期就把所有索引算完了。
提示:上面这段代码为了突出布局驱动的主线,省略了
cp.async流水线、Swizzle防 bank conflict、边界处理等工程细节。真实的高性能 kernel 会在这些地方继续用布局代数做优化,但骨架是一致的。
3.4 为什么这个抽象重要
因为一旦布局是"值"而不是"代码结构",你就可以:
- 在编译期对布局做静态检查(
static_assert布局是否满足 WGMMA 要求); - 把布局作为模板参数传递,让编译器为每种布局生成最优代码;
- 用同一套代码适配不同的 tile size、不同的 swizzle 模式、不同的硬件指令。
这就是 CuTe 相比 CUTLASS 2.x 的本质跃迁:从"策略类组合"到"布局代数"。
4. 今生:CuTe 的演进
4.1 坚持了什么
- 零运行时开销的编译期抽象:所有布局运算都在编译期完成,生成的代码和手写 CUDA 等价。
- C++ 模板作为唯一载体 :不引入新的语言、不依赖外部编译器,
#include即可用。 - 布局与算法解耦 :
copy、gemm、mma这些算法对布局是泛型的,布局由调用方指定。 - 与 CUTLASS 生态深度绑定:CuTe 是 CUTLASS 3.x 的底座,所有 CUTLASS 3.x kernel 都建立在 CuTe 之上。
4.2 增加了什么
- 更完整的原子操作集 :
copy/copy_if/gemm/mma/axpby等通用算法,配合TiledCopy、TiledMMA表达"分块后的拷贝与矩阵乘"。 - 对 Hopper / Blackwell 新特性的支持 :TMA(
SM90_TMA_LOAD)、WGMMA、tcgen05等指令的 CuTe 封装。 - Swizzle 的一等公民化 :
Swizzle<B, M, S>成为布局变换的标准工具,用于规避 shared memory bank conflict。 - CuTe DSL(Python):这是近年最重要的新增方向,见 4.4。
4.3 放弃了什么
- 放弃了"一套模板打天下"的幻想:不同架构(Ampere / Hopper / Blackwell)的指令差异太大,CuTe 选择了"按架构提供不同原子"而非强行统一。
- 放弃了 friendly error message 的追求:模板元编程的报错依然冗长,这是 C++ 模板路线的固有代价。
- 部分旧接口的弃用:CUTLASS 3.x 逐步弃用了 2.x 的一些 layout 类型,迁移成本由用户承担。
4.4 CuTe DSL:从 C++ 模板到 Python
CuTe 的 C++ 模板虽然强大,但门槛极高:编译慢、报错难读、调试困难。于是出现了 CuTe DSL------用 Python 提供 CuTe 的布局代数能力,底层仍然生成高效的 GPU 代码。
它的意义在于:
- 降低入门门槛:Python 的交互式体验让布局调试变得可行;
- 保留 CuTe 的语义 :
Layout、Tensor、TiledCopy这些概念在 DSL 中一一对应; - 编译期与运行期分离:Python 侧负责描述与特化,编译产物仍然是原生的 CUDA kernel。
这实际上是对"CuTe 是 C++ 模板库"这一身份的一次重要补充:模板库负责极致性能与底层控制,DSL 负责易用性与快速迭代。
5. 当前热点与未来方向
5.1 当前热点
- Blackwell 架构适配 :
tcgen05指令族、新的 Tensor Memory 层次,CuTe 正在跟进封装。 - CuTe DSL 的成熟:如何让 Python DSL 生成的代码性能逼近手写 C++,是当前的核心工程问题。
- 与 PyTorch 生态的衔接:自定义算子、编译后端集成。
- Attention / MoE 等新算子的 CuTe 化:FlashAttention 系列的很多实现已经深度使用 CuTe 的布局抽象。
5.2 想增加的方向
- 更友好的编译期诊断(至少在 DSL 层面);
- 更通用的自动布局推导(给定指令约束,自动求解满足条件的 swizzle);
- 跨架构的可移植抽象,减少"换卡重写"的成本。
5.3 未来方向
CuTe 的长期定位大概率是:成为 NVIDIA GPU 上"布局与指令映射"的事实标准中间层。上层是各种 DSL(CuTe DSL、Triton 等),下层是具体的 PTX 指令,CuTe 负责把"逻辑上的张量运算"翻译成"物理上的指令序列"。
6. 横向对比:CuTe vs Triton vs TileLang
6.1 共同点
- 目标一致:都是为了让开发者用更高层的抽象写出接近硬件极限的 GPU 算子。
- 核心抽象相似:都围绕"tile / block"、"layout"、"thread mapping"这几个概念展开。
- 都强调编译期特化:通过模板或 JIT,把 tile size、布局等参数在编译期确定,避免运行时开销。
6.2 关键区别:CuTe 是 C++ 模板库
这是最本质的差异,值得展开:
| 维度 | CuTe | Triton | TileLang |
|---|---|---|---|
| 载体 | C++ 模板库 | Python DSL | Python DSL |
| 编译基础设施 | 无(纯 C++ 模板 + nvcc) | 自研编译器(基于 MLIR) | 基于 TVM/MLIR |
| 编译过程 | 白盒,可读生成的 PTX/SASS | 相对黑盒 | 相对黑盒 |
| 抽象层级 | 低(贴近指令) | 中(贴近算法) | 中(贴近算法) |
| 上手难度 | 高 | 低 | 中 |
| 性能上限 | 极高(可精确控制每条指令) | 高 | 高 |
| 调试体验 | 差(模板报错) | 好 | 较好 |
具体来说:
- 不基于 MLIR/TVM,编译不是黑盒 :CuTe 没有自己的编译器,它就是 C++ 模板 + nvcc。你写的每一行 CuTe 代码,最终展开成什么,是可以通过
-E、--ptx直接看到的。这意味着当性能不达预期时,你可以精确地定位到是哪条指令、哪个布局出了问题,而不是去猜编译器做了什么优化。 - 更底层,因此编译时更灵活:CuTe 允许你在编译期做任意复杂的布局运算,甚至可以根据硬件特性动态选择指令。这种灵活性是高层 DSL 难以提供的------DSL 的抽象一旦固定,就只能在框架内做优化。
- 上限更高:因为贴近指令,CuTe 可以榨干硬件的每一分性能,包括手工调度 pipeline、精确控制寄存器分配。
6.3 CuTe 的不足
- 学习曲线陡峭:模板元编程 + GPU 体系结构,双重门槛。
- 编译时间长:大量模板实例化,大型 kernel 编译动辄数分钟。
- 报错信息不友好:一个布局不匹配,可能产生几百行模板错误。
- 可移植性差:深度绑定 NVIDIA 硬件,换 AMD/其他加速器基本要重写。
- 开发效率低:相比 Triton 几十行写一个 kernel,CuTe 往往需要几百行。
6.4 如何选择
- 追求极致性能 、需要精确控制指令与布局、团队有 CUDA 专家 → CuTe;
- 追求开发效率、算子相对标准、希望快速迭代 → Triton;
- 介于两者之间、需要较好的性能与可接受的开发成本 → TileLang;
- 想要 CuTe 的能力但不想写 C++ 模板 → CuTe DSL。
7. 总结
CuTe 的价值不在于它"好用",而在于它把 GPU 编程中最难表达的部分------布局与线程映射------变成了可以做代数运算的数学对象。它坚持 C++ 模板路线,拒绝引入编译器黑盒,换来了极致的可控性与性能上限,代价是陡峭的学习曲线与糟糕的报错体验。
CuTe DSL 的出现,说明这个团队也意识到了"曲高和寡"的问题,正在尝试用 Python 的易用性包裹 C++ 模板的强大。未来 CuTe 大概率会继续扮演"底层布局中间层"的角色,而 DSL 层则负责把这份能力交付给更多开发者。
对于学习者,我的建议是:先用 Triton 建立对 GPU 算子优化的直觉,再回头读 CuTe 的 Layout 代数 ------当你真正理解 logical_divide 在做什么的时候,很多之前看不懂的 kernel 代码会突然变得清晰。