Kernel Dev 训练营/ 提示与讲解
提示与讲解

Tensor Core 与新指令集:从 WMMA 到 wgmma、TMA、tcgen05

面试题和附录里提到的“用 WMMA 改写 C4”“读 CuTe 的 MMA atom / TiledMMA”“读 FlashAttention-3/4 和 DeepGEMM 的主循环”,背后是同一条主线:每一代 GPU 都把更多的活从线程手里拿走,交给专用硬件。这一页按 Volta → Ampere → Hopper → Blackwell 的顺序讲清楚每一代新增了什么指令、解决了什么问题、代码里怎么用,并把每一步都链接到 cuda-samples、CUTLASS/CuTe、FA3、FA4 和 DeepGEMM 里对应的源码行。

怎么读这一页

前置:C4 讲解(三级复用、外积)、T4 讲解(tl.dot、num_stages)、CuTe 专题(Layout、TV layout)。本仓库实测用的 RTX 5090 是 sm_120(消费级 Blackwell),它有 Tensor Core 和 warp 级的 mma 指令,但没有 Hopper 的 wgmma,也没有数据中心 Blackwell(sm_100)的 tcgen05 和 Tensor Memory。所以第 ④⑤ 节的代码要在 H100 / B200 上才能运行,这也是面试题里写“有 H100 / B200 时”的原因。

① 先看全貌:每一代都在把活从线程手里拿走

为什么要专门的矩阵指令:C4 的 v2 里,一个线程每条 FMA 指令做 1 次乘加。Ampere 的 mma.sync.m16n8k16 是一个 warp 一起发的一条指令,一次做 16 × 8 × 16 = 2048 次乘加。指令数少了两个数量级,取指、调度、寄存器读写的开销都随之摊薄,这就是 Tensor Core 快的根本原因。本仓库的实测:RTX 5090 上 FP32 CUDA core 的 cuBLAS 是 65 TFLOP/s(C4),FP16 Tensor Core 的 Triton GEMM 是 218 TFLOP/s(T4)。

但 Tensor Core 算得越快,喂数据就越成问题。每一代新增的指令,几乎都是为了让数据跟上:

四代 GPU,每一代把哪件活交给了硬件悬停节点查看连接
读 FA3、FA4、DeepGEMM 之前最需要建立的直觉:越新的架构,线程自己干的活越少,越多的是“发起一个异步操作,然后等一个信号”。kernel 的写法也就从“每个线程一步步算”变成了“几组线程各司其职、用 barrier 互相通知”(warp specialization)。
谁发起矩阵乘 操作数从哪来 累加器放哪 谁搬数据 同步方式
Volta WMMA 一个 warp 寄存器(fragment) 寄存器 线程 ld / st __syncthreads
Ampere mma.sync 一个 warp 寄存器(ldmatrix 从 smem 装进来) 寄存器 cp.async(异步) cp.async.wait_group
Hopper wgmma 一个 warpgroup(4 个 warp) shared memory(描述符),A 也可以在寄存器 寄存器 TMA(一个线程发起整块) mbarrier(按字节数完成)
Blackwell tcgen05 一个线程 shared memory 或 Tensor Memory Tensor Memory TMA(可跨两个 CTA) mbarrier
类比 · 盖楼的工具在升级

最早是每个工人一块块搬砖、一块块砌(CUDA core 的 FMA)。Volta 给每个小组发了一台小型砌砖机(WMMA),砌得快多了,但砖还得人搬。Ampere 加了传送带(cp.async),砖可以自己从仓库送到工地旁边,工人不用来回跑。Hopper 换成了塔吊(TMA):一个人按一下按钮,一整托盘砖就吊到指定位置;砌砖机也变大了,四个小组合用一台(wgmma)。Blackwell 的砌砖机自带记录板(Tensor Memory),工人连“砌到哪了”都不用记在自己脑子里(寄存器),只要一个人按启动键就行(单线程发 MMA)。工地上的人越来越少地动手,越来越多地在“调度”。

② 第一步:WMMA,用 Tensor Core 改写 C4

nvcuda::wmma(#include <mma.h>)是 CUDA 9 起的 C++ 接口,最容易上手。它的核心是 fragment:一个 warp 共同持有的一小块矩阵,具体每个线程拿哪几个元素是不公开的(opaque)。

一个 warp 用 WMMA 算一个 16×16 的 C tile悬停节点查看连接
和 C4 v1 对照:C4 里一个线程算 C 的 1 个元素(v1)或 4×4 个(v2),这里一个 warp 算 C 的 16×16 = 256 个,乘加全部交给 Tensor Core。

cuda-samples 里最简单的版本 cudasamplessimple_wmma_gemm,核心就是这几行:

wmma::fragment<wmma::matrix_a, 16, 16, 16, half, wmma::row_major> a_frag;
wmma::fragment<wmma::matrix_b, 16, 16, 16, half, wmma::col_major> b_frag;
wmma::fragment<wmma::accumulator, 16, 16, 16, float> acc_frag;
wmma::fill_fragment(acc_frag, 0.0f);
for (int k = 0; k < K; k += 16) {
  wmma::load_matrix_sync(a_frag, a + k + aRow * lda, lda);
  wmma::load_matrix_sync(b_frag, b + k + bCol * ldb, ldb);
  wmma::mma_sync(acc_frag, a_frag, b_frag, acc_frag);
}
wmma::store_matrix_sync(c + cCol + cRow * ldc, acc_frag, ldc, wmma::mem_row_major);

改写 C4 的路线(面试题里的第一步):

  1. 先照抄上面这个版本:直接从全局内存 load fragment,没有 shared memory。它能工作,但每个 fragment 都从显存读,复用很差。
  2. 把 C4 v1 的 shared memory tiling 加回来:一个 block 把 A、B 的 tile 读进 shared memory,多个 warp 各自从 shared memory load fragment。cuda-samples 的完整版 cudasamplescompute_gemm 就是这样,cudasamples每个 warp 持有多个 a、b fragment,相当于 C4 v2 的“寄存器 tile”。
  3. 和 cuBLAS 的 FP16 GEMM 对比。参考上限:本仓库 T4 在 5090 上实测 cuBLAS FP16 约 218 TFLOP/s。

WMMA 的局限:fragment 的寄存器布局不公开,所以你没法自己决定每个线程持有哪些元素,也就没法和 softmax、量化之类的计算精细融合;shared memory 到 fragment 的加载方式也是固定的。这正是下一代写法(直接用 mma.sync PTX,或者用 CuTe)要解决的问题。文档:Programming Guide · WMMA。

③ Ampere:mma.sync、ldmatrix、cp.async,以及 CuTe 的 MMA Atom

三条新指令

指令 做什么 CuTe 里的封装
mma.sync.aligned.m16n8k16 一个 warp 做 16×8×16 的矩阵乘,每个线程持有的元素有公开的布局 cutlassSM80_16x8x16_F32F16F16F32_TN
ldmatrix(sm_75 起) 一个 warp 从 shared memory 读 8×8 的小块,直接放成 mma 要的寄存器布局 cutlassSM75_U32x4_LDSM_N
cp.async(sm_80 起) 全局内存 → shared memory 的异步拷贝,不经过寄存器 cutlassSM80_CP_ASYNC_CACHEALWAYS,配合 cutlasscp_async_fence / cp_async_wait
Ampere 上一个 K 块的数据流悬停节点查看连接
cp.async 让“搬下一块”和“算这一块”可以同时进行,这就是 T4 里 num_stages 做的事(软件流水线)。ldmatrix 省掉了线程自己从 shared memory 一个个读、再拼成 mma 布局的指令。

MMA Atom:把一条指令的布局写成 layout

mma.sync 的难点在于:32 个线程各自持有 A、B、C 的哪几个元素,是硬件规定好的。CuTe 用TV layout((线程号, 值号) → (行, 列))把这个规定写下来:cutlassMMA_Traits SM80_16x8x16 里的 ALayout、BLayout、CLayout,例如 C 的布局是 cutlassSM80_16x8_Row。这就是 CuTe 专题 第 ⑦ 节说的“硬件也用 Layout 说话”,也是 G1 里你写的“lane 持有哪几个元素”的计算。

TiledMMA:用小印章拼大印章

一条 mma 只算 16×8 的 C。make_tiled_mma 把这个 Atom 在多个 warp 上、在 M / N 方向上重复,拼成一个大的 tile:cutlassmake_tiled_mma(SM80_16x8x16_F16F16F16F16_TN, …)。之后的代码只调用 gemm(mma, A, B, C)(cutlass298 行),每个线程该算哪些、该持有哪些,全部由 layout 推出来。同一份 kernel 换成 cutlassUniversalFMA(普通 FMA)也能跑,只是换了一个 Atom。

类比 · 印章和拼版

一条 mma 指令像一枚 16×8 的小印章,盖一下出一小块图案,而且印章上每个格子对应哪个工人是出厂就定好的(TV layout)。TiledMMA 是把很多枚小印章按规则拼成一块大印版,一次盖出一整个 tile。你只需要说“用这种印章、横着拼几个、竖着拼几个”,每个工人负责哪个格子由 CuTe 自动推出来。换一代硬件,就是换一种印章,拼版的代码不用改。

多级流水线

cutlassK_PIPE_MAX 个 stage:先发出前几个 stage 的 cp.async,主循环里 cutlass等最老的一个 stage 到齐(cp_async_wait<K_PIPE_MAX−2>),算它,同时发出下一个 stage 的拷贝。

没有流水线:搬一块、算一块(C4 的写法)
同一组线程
搬 0
算 0
搬 1
算 1
搬 2
算 2
搬 3
算 3
cp.async 多级流水线(3 个 stage)
拷贝(异步)
搬 0
搬 1
搬 2
搬 3
Tensor Core
算 0
算 1
算 2
算 3

示意图,不是实测时间。有了异步拷贝,第 k 块的计算和第 k+1、k+2 块的搬运同时进行,总时间从“搬 + 算”的和变成接近两者中较大的那个。

④ Hopper(sm_90a):TMA、wgmma、mbarrier 与 warp specialization

TMA:一个线程搬一整个 tile

Ampere 的 cp.async 仍然是每个线程算自己那几个元素的地址、各发各的拷贝。Hopper 的 TMA(Tensor Memory Accelerator)是一个专用的拷贝引擎:

类比 · 快递柜和取件码

cp.async 像每个人自己去仓库取几件货。TMA 像下一张整车订单:一个人填好单子(tensor map + 坐标),仓库自动把一整托盘货送到你家门口的快递柜(shared memory)。mbarrier 是快递柜上的计数器:订单上写了“共 16 KB”,计数器数到 16 KB 才亮灯(phase 翻转),等着用货的人(消费者)看灯就知道能不能开柜。multicast 是一张订单同时送到楼里好几户的柜子。

wgmma:四个 warp 合用一台更大的机器,操作数直接从 shared memory 读

warp specialization:分工,而不是每个人什么都干

有了异步的 TMA 和异步的 wgmma,最自然的写法是分工:

warp specialization:生产者、消费者和两组 barrier悬停节点查看连接
每个 stage 有两个 mbarrier:full(数据到齐,消费者可以用)和 empty(消费者用完,生产者可以覆盖)。shared memory 是一个环形缓冲区,生产者往前填,消费者在后面追。CuTe 教程里这两组 barrier 是 producer_mbar 和 consumer_mbar(cutlass152–161 行)。
类比 · 后厨分工

以前后厨每个厨师都要自己去冷库拿菜、自己切、自己炒(每个线程都搬数据、都算)。现在分成采购员和厨师:采购员(生产者 warpgroup)只负责把菜从冷库送到备菜台(TMA → shared memory),自己两手空空,不需要灶台(寄存器很少);厨师(消费者)守着灶台只炒菜,灶台越大越好(寄存器很多)。备菜台上每个格子有两盏灯:“菜到了”(full)和“盘子空了”(empty),采购员和厨师看灯干活,不用互相喊话。setmaxnreg 就是把采购员用不上的灶台挪给厨师。

读 FlashAttention-3:生产者 + 两个消费者的乒乓

FA3 的 forward kernel 把上面这些全部用上:

FA3 的乒乓:两个消费者 warpgroup 轮流占用 Tensor Core(示意)
消费者 WG 1
GEMM
softmax
GEMM
softmax
GEMM
softmax
消费者 WG 2
softmax
GEMM
softmax
GEMM
softmax
GEMM
生产者 WG 0
TMA:K、V 的下几块
TMA
TMA

Tensor Core(绿色)几乎一直有活干:总有一个 warpgroup 在做 GEMM。softmax(紫色)的指数运算吞吐比 GEMM 低得多,如果不和 GEMM 重叠,它会成为瓶颈。

读 DeepGEMM(SM90):FP8 GEMM 的精度补偿

DeepGEMM 的 SM90 FP8 GEMM 结构完全一样:deepgemmkNumTMAThreads = 128(一个 warpgroup)+ 若干 math warpgroup,deepgemmTMA 侧 reg_dealloc、deepgemmmath 侧 reg_alloc,deepgemmfull / empty barrier。

它多做了一件事,和 M3 直接相关:每个 K 块做完 deepgemmFP8 的 wgmma 之后,把结果乘上这一块的 scale,deepgemm加到 FP32 的 final_accum 里(在 CUDA core 上做)。这就是 M3 里 acc += tl.dot(a, b) * (sa * sb) 的硬件级版本,同时也补偿了 Hopper FP8 Tensor Core 内部累加精度有限的问题。

⑤ Blackwell(sm_100a):tcgen05、Tensor Memory 与 2-CTA MMA

tcgen05.mma:一个线程发起,累加器放在 Tensor Memory

Hopper wgmma Blackwell tcgen05.mma
谁发起 一个 warpgroup(128 线程)一起发 一个线程发
累加器在哪 128 个线程的寄存器里 Tensor Memory(TMEM):每个 SM 一块专用的 256 KB(128 行 × 512 列 × 32 位)
读结果 直接在寄存器里 用 tcgen05.ld 从 TMEM 读到寄存器
跨 CTA 不支持 cta_group::2:两个 CTA(一对 SM)合作做一条更大的 MMA
低精度 FP8 FP8 / FP6 / FP4,硬件 block scaling(MXFP8、MXFP4、NVFP4)

源码:cutlassSM100_MMA_F16BF16_SS → tcgen05.mma.cta_group::1.kind::f16;TMEM 的分配 cutlasstcgen05.alloc;2-CTA 版本 cutlassSM100_MMA_F16BF16_2x1SM_SS → cta_group::2;block-scaled 版本 cutlassSM100_MMA_MXF8F6F4_SS → kind::mxf8f6f4.block_scale。

Blackwell 上一个 CTA 的分工(对照 DeepGEMM SM100 / FA4)悬停节点查看连接
和 Hopper 相比,累加器从“占满 128 个线程的寄存器”变成放在 TMEM 里,于是发 MMA 的只需要一个线程,大量寄存器空出来给 softmax、反量化、epilogue 这些“非矩阵”工作。

CuTe 的 Blackwell 教程按难度排好了:cutlass01:tcgen05.mma + tcgen05.ld(cutlass分配 TMEM、cutlassgemm 之后 umma_arrive 通知、cutlassmake_tmem_copy 把结果读回寄存器)→ 02:加 TMA → 03:TMA multicast → cutlass04:2-CTA 的 MMA(cutlassSM100_TMA_2SM_LOAD_MULTICAST)→ 05:TMA epilogue。

类比 · 公共账本

Hopper 上,每次大合唱(wgmma)的结果要分到 128 个歌手各自的小本子上记着(寄存器),歌手们因此没空干别的。Blackwell 在舞台边上放了一块公共大黑板(Tensor Memory),合唱结果直接写在黑板上,指挥(一个线程)喊一声开始就行。歌手们的小本子空出来了,可以去做和声以外的事(softmax、反量化)。2-CTA MMA 则是两个合唱团并排站,合起来唱一首更大的曲子,共用一块更大的黑板。

读 FlashAttention-4:用 CuTe DSL 写成,角色分得更细

FA4(faflash_fwd_sm100.py,用 Python 的 CuTe DSL 写)的角色分配一目了然:fasoftmax0、softmax1 各 4 个 warp,correction 4 个 warp,MMA 1 个 warp,epilogue 1 个 warp,load 1 个 warp。

读 DeepGEMM(SM100):一个 warp 一个角色

deepgemmwarp 0:TMA load,deepgemmwarp 1:MMA issue(只在 leader CTA 上),deepgemm其余 warp group:epilogue。TMEM 分配按是否 2-CTA 选择 deepgemmAllocator1Sm 或 Allocator2Sm,MMA 的形状见 deepgemmUMMA_M / UMMA_N / UMMA_K。和 SM90 版本相比,FP8 的 scale 由 Tensor Core 的 block scaling 在硬件里乘,不再需要 CUDA core 上的 promotion。

⑥ 哪块 GPU 能跑哪些

GPU 架构 能用的矩阵指令 拷贝 本页哪几节能动手
A100 sm_80 WMMA、mma.sync cp.async ②③
RTX 4090 / L40S sm_89 同上 + FP8 mma cp.async ②③
H100 / H200 sm_90a + wgmma + TMA、cluster ②③④(FA3、DeepGEMM SM90)
B200 / GB200 sm_100a + tcgen05、TMEM、2-CTA TMA(含 2-SM) ②③⑤(FA4、DeepGEMM SM100)
RTX 5090(本仓库) sm_120 WMMA、mma.sync(含 FP8 / FP4) cp.async 等 ②③;没有 wgmma 和 tcgen05

sm_90a、sm_100a 里的 a 表示“架构专用特性”:编译出来的代码只能在这一代上运行,不向后兼容。这也是 FlashAttention、DeepGEMM 为每一代 GPU 单独写一套主循环的原因。

⑦ 推荐的阅读和动手路线

从 C4 到 FA4悬停节点查看连接
  1. 在 5090 上就能做:用 WMMA 改写 C4(第 ②节),然后读 cutlasssgemm_sm80 和 cutlass0t_mma_atom.md,把 MMA Atom 的 TV layout 和 G1 / G2 对照起来。
  2. 有 H100 时:跑 cutlasswgmma_tma_sm90.cu,再读 FA3 的 fa生产者 / 消费者分支 和 DeepGEMM SM90 的主循环。
  3. 有 B200 时:按顺序跑 cutlassblackwell/01–05,再读 FA4 和 DeepGEMM SM100。
自测 1:一条 mma.sync m16n8k16 和一条 FFMA 指令,各做多少次乘加?

mma.sync:一个 warp 一条指令,16 × 8 × 16 = 2048 次乘加。FFMA:每个线程 1 次,一个 warp 一条指令 32 次。前者是后者的 64 倍。

自测 2:为什么 Hopper 的 kernel 要把一个 warpgroup 专门用来发 TMA,而不是让所有线程都一起搬一点、算一点?

TMA 只需要一个线程发起,搬运本身由硬件完成,所以“搬数据”几乎不消耗线程的计算和寄存器。让一组线程专门发 TMA、并把寄存器让给计算的 warpgroup(setmaxnreg),计算的 warpgroup 就能用更多寄存器存更大的累加器 tile;两边通过 full / empty barrier 异步配合,搬运和计算完全重叠。所有线程都既搬又算,就要在两件事之间来回切换,寄存器也要按两者中较大的需求分配。

自测 3:Blackwell 把累加器放进 Tensor Memory,最大的好处是什么?

累加器不再占用寄存器,发 MMA 也只需要一个线程,大量寄存器和线程可以去做 softmax、反量化、epilogue 等非矩阵工作。FA4 的 correction warp、exp2 软件模拟,都依赖这些空出来的资源。

源码与资料索引

本仓库:C4 讲解 · T4 讲解 · T5 讲解 · M3 讲解 · CuTe 专题 · G1 讲解

WMMA:cudasamplessimple_wmma_gemm · cudasamplescompute_gemm · Programming Guide · WMMA

CuTe(Ampere):cutlassSM80_16x8x16 · cutlassMMA_Traits · cutlassldmatrix · cutlasscp.async · cutlasssgemm_sm80 · cutlass0t_mma_atom.md

Hopper:cutlassTMA · cutlasswgmma · cutlasswgmma_tma_sm90.cu · faFA3 kernel · faFA3 主循环 · deepgemmDeepGEMM SM90 · flashinferFlashInfer 的 Hopper prefill(同样的生产者 / 消费者)

Blackwell:cutlasstcgen05.mma · cutlassTMEM 分配 · cutlassblackwell/01–05 · faFA4 · deepgemmDeepGEMM SM100

PTX 文档:mma(m16n8k16 的寄存器布局) · ldmatrix · cp.async · cp.async.bulk.tensor(TMA) · mbarrier · wgmma · setmaxnreg · tcgen05.mma · Tensor Memory 分配 · CTA pair · block scaling

CUDA Programming Guide:TMA · Thread Block Clusters

论文:FlashAttention-3 · DeepSeek-V3(FP8 训练与 DeepGEMM 的背景)