土法炼钢兴趣小组的算法知识备份

【GPU 算子工程】Tensor Core 与 MMA:wmma、mma.sync 与数据布局

文章导航

分类入口
gpuarchitecture
标签入口
#cuda#tensor-core#wmma#mma#mma-sync#fp16#bf16#tf32#hmma

目录

Tensor Core 与 MMA:wmma、mma.sync 与数据布局

GEMM 篇 把手写 FP32 GEMM 优化到了 6.4 TFLOP/s,约 FP32 峰值的 39%。但深度学习训练和推理的算力其实大部分不跑在 FP32 CUDA core 上,而跑在 Tensor Core 这种专用矩阵单元上。这一篇讲 Tensor Core 是什么、怎么用、有什么约束,以及为什么用了它常常还是快不起来。

一、Tensor Core:把矩阵乘做进硬件

普通 CUDA core 一次做一个标量 FMA。Tensor Core 不一样——它一条指令直接算一个小矩阵的乘加:

\[ D = A \times B + C \]

其中 \(A\)\(B\)\(C\)\(D\) 是小矩阵(如 \(16\times16\))。这条指令叫 MMA(Matrix Multiply-Accumulate),由一个 warp 的 32 个线程协作执行,硬件在内部并行完成所有乘加。把 \(O(n^3)\) 的乘加打包进专用电路,单位时间的吞吐远高于标量 FMA。

Tensor Core 从 Volta(第一代)引入,本系列测试卡 RTX 3060 Ti 是 Ampere 消费级(GA10x),第三代 Tensor Core,每个 SM 4 个,共 152 个。它支持 FP16、BF16、TF32 输入,FP32 累加;不支持 FP8(FP8 Tensor Core 从 Hopper sm_90 才有,见 第 21 篇)。

二、实测:Tensor Core 比 FP32 快多少

用一个微基准量化:每个 warp 反复执行 wmma 的 \(16\times16\times16\) MMA(FP16 输入、FP32 累加),用多个独立累加器填满流水线,统计达到的浮点吞吐。RTX 3060 Ti 实测:

计算方式 吞吐 相对 FP32 峰值
FP32 CUDA core(理论峰值) 16.2 TFLOP/s 1.0×
手写 FP32 GEMM(第 10 篇 6.4 TFLOP/s 0.4×
FP16 Tensor Core(实测吞吐) 72.8 TFLOP/s 4.5×

Tensor Core 的 FP16 吞吐达 72.8 TFLOP/s,是 FP32 峰值的约 4.5 倍、是手写 FP32 GEMM 的约 11 倍。这就是为什么混合精度训练和低精度推理是标配——不是为了省显存那么简单,而是 Tensor Core 的算力本来就远超 FP32 通路。

需要说明:这是消费级 Ampere(GA10x)的数字。数据中心卡(A100/H100)的 Tensor Core 吞吐更高;消费级 GA10x 的 FP16 输入 / FP32 累加这条通路相比数据中心 GA100 被削减(FP32 累加速率受限),但这只是「跨卡对比」的相对结论——本卡实测的 72.8 TFLOP/s 正是在这条受限通路上跑出来的真实值,二者并不矛盾,一个是本卡实测、一个是与 GA100 的横向对比。不同代际、不同精度(FP16/BF16/TF32)的峰值差异很大,做 Roofline 分析时要用对应精度的峰值。

三、wmma API:fragment 与三步操作

CUDA 提供 nvcuda::wmma(warp matrix multiply-accumulate)这套 C++ API 来用 Tensor Core。它的核心抽象是 fragment——一个 warp 协作持有的小矩阵分片,数据分布在 32 个线程的寄存器里,布局对程序员不透明(由硬件决定)。

用法三步:load → mma → store。

#include <mma.h>
using namespace nvcuda;

wmma::fragment<wmma::matrix_a, 16,16,16, half, wmma::row_major> a;
wmma::fragment<wmma::matrix_b, 16,16,16, half, wmma::col_major> b;
wmma::fragment<wmma::accumulator, 16,16,16, float> c;

wmma::fill_fragment(c, 0.0f);
wmma::load_matrix_sync(a, ptrA, lda);   // 从 global/shared 加载分片
wmma::load_matrix_sync(b, ptrB, ldb);
wmma::mma_sync(c, a, b, c);             // c += a * b,整个 warp 协作
wmma::store_matrix_sync(ptrC, c, ldc, wmma::mem_row_major);

几个要点:

四、wmma 之下:mma.sync PTX

wmma 是较高层的封装,简单但不够灵活——fragment 布局不透明,限制了和 shared memory 的精细配合。高性能库(CUTLASS)直接用更底层的 mma.sync PTX 指令,它暴露明确的线程-数据映射(哪个 lane 持有矩阵的哪些元素),让库能精确控制数据如何从 shared 进入 fragment,配合 swizzle 布局消除 bank conflict。

mma.sync.aligned.m16n8k16.row.col.f32.f16.f16.f32  {d0,d1,d2,d3}, {a0,a1,a2,a3}, {b0,b1}, {c0,c1,c2,c3};

手写 mma.sync 要管理寄存器到矩阵元素的映射,非常繁琐,所以一般不直接手写——要么用 wmma,要么用 CUTLASS 的 CuTe 抽象(第 12 篇)来管理这些布局。

五、真正的瓶颈:喂数据

一个反复出现的现象:换上 Tensor Core,性能却没涨多少。原因几乎总是数据喂不上。Tensor Core 算得太快,如果数据从 global/shared 进入 fragment 的速度跟不上,它就大量空转。

回到 Roofline:Tensor Core 把算力屋顶大幅抬高(本卡 FP16 约 73 TFLOP/s vs FP32 16 TFLOP/s),脊点的算术强度要求随之提高——原本 compute-bound 的 GEMM 在 Tensor Core 下可能重新变成 memory-bound。所以用 Tensor Core 的 GEMM 必须把访存做到极致:

这些正是 CUTLASS 在做的事——把 Tensor Core 喂饱,让它逼近峰值。手写一个能跑满 Tensor Core 的 GEMM 工作量极大,所以下一篇转向 CUTLASS。

六、什么时候直接用、什么时候交给库

七、小结与下一步

把 Tensor Core 喂到接近峰值是一项系统工程。下一篇看工业界的答案——CUTLASS 与 CuTe:模板化 GEMM 与布局代数

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-06-28 · gpu / architecture

【GPU 算子工程】CUTLASS 与 CuTe:模板化 GEMM 与布局代数

CUTLASS 用分层模板把 GEMM 拆成 device/kernel/threadblock/warp/instruction 五层,CuTe 用统一的 Layout 代数描述张量在各级存储的布局。讲清这套抽象如何在不手写 PTX 的前提下把 Tensor Core 喂到接近峰值。

2026-06-26 · gpu / architecture

GPU 高性能算子工程

从 GPU 执行模型与内存层次出发,系统讲解如何写出并调优高性能 CUDA 算子:访存合并、occupancy、Roofline、Nsight 调优,reduction/GEMM/Tensor Core/FlashAttention 核心算子实现,以及 Triton、CUTLASS、kernel fusion 与算子库工程。


By .