第 9 章 · GEMM 深入

⏱️ 90 分钟🎯 触摸 Tensor Core📂 code/ch09_gemm/🔥 关键瓶颈章

学习目标

前置知识

已完成 Ch06 与 Ch08,能解释 tiled GEMM、arithmetic intensity、warmup 和可重复计时。

核心概念

9.1 GEMM 在 LLM 里的地位

在 Transformer 中,GEMM 通常占据大部分 FLOPs。Transformer 每层主要算子:

所以 prefill、大 batch decode 和 verifier 阶段通常优先优化 GEMM;但 batch=1 decode 还会被权重读取、KV cache 和 launch gap 限制,必须分阶段 profile。

关键代码

9.2 寄存器 Tile:从 Ch6 再进一步

Ch6 里每 thread 算 1 个 cell。瓶颈是每读 As/Bs 一次只用 2 个 FLOP。 如果每 thread 算 8×8 = 64 个 cell:每读 8+8 个 shared 值就做 64 个 FLOP,shared 访问减少 8×。

graph TD
    A["block 处理 C 的 128×128 tile
thread block = 16×16 = 256 threads"] A --> B["每 thread 持有 8×8 reg accumulator"] B --> C["K 维分 BK=16 切片
每片协作加载 128×16 + 16×128 到 shared"] C --> D["内层 K 循环 (BK=16):
读 8 个 As 行 + 8 个 Bs 列 → 64 个 fmadd"] D --> E["写回 C"] style B fill:#f3f1e8,stroke:#2f5d3a style D fill:#f3f1e8,stroke:#a86420
constexpr int BM = 128, BN = 128, BK = 16;
constexpr int TM = 8,   TN = 8;

__global__ void gemm_reg_tile(const float* A, const float* B, float* C, int M, int N, int K) {
    __shared__ float As[BM][BK], Bs[BK][BN];
    int ty = threadIdx.y, tx = threadIdx.x;
    int row0 = blockIdx.y * BM + ty * TM;
    int col0 = blockIdx.x * BN + tx * TN;
    float acc[TM][TN] = {0};

    for (int kt = 0; kt < K; kt += BK) {
        /* 协作 load A 的 128x16 + B 的 16x128 (每 thread load 8+8 个数) */
        __syncthreads();

        #pragma unroll
        for (int k = 0; k < BK; ++k) {
            float a_reg[TM], b_reg[TN];
            #pragma unroll for (int i=0;i<TM;++i) a_reg[i] = As[ty*TM+i][k];
            #pragma unroll for (int j=0;j<TN;++j) b_reg[j] = Bs[k][tx*TN+j];
            #pragma unroll for (int i=0;i<TM;++i)
            #pragma unroll for (int j=0;j<TN;++j) acc[i][j] += a_reg[i] * b_reg[j];
        }
        __syncthreads();
    }
    /* write back acc → C */
}

关键洞察:内层 8×8 = 64 个 fmadd 全部在寄存器之间发生。BK=16 步内只触发 16×(8+8) = 256 次 shared load,每 thread 做 16×64 = 1024 个 FLOP。算术强度 4 FLOP/shared-byte。

9.3 Tensor Core — WMMA API

fp32 CUDA core 路线很难接近低精度 Tensor Core 的吞吐。要让 GEMM 跑到现代 GPU 的高吞吐区间,必须使用 Tensor Core,并确认 Nsight Compute 中 tensor pipe 真正活跃。

WMMA fragment 三件套

#include <mma.h>
using namespace nvcuda;

wmma::fragment<wmma::matrix_a, 16, 16, 16, __half, wmma::row_major> a_frag;
wmma::fragment<wmma::matrix_b, 16, 16, 16, __half, wmma::row_major> b_frag;
wmma::fragment<wmma::accumulator, 16, 16, 16, float>                c_frag;

wmma::fill_fragment(c_frag, 0.f);
for (int kt = 0; kt < K; kt += 16) {
    wmma::load_matrix_sync(a_frag, A + row*K + kt, K);
    wmma::load_matrix_sync(b_frag, B + kt*N + col, N);
    wmma::mma_sync(c_frag, a_frag, b_frag, c_frag);   // <-- TC magic
}
wmma::store_matrix_sync(C + row*N + col, c_frag, N, wmma::mem_row_major);

每个 warp 共享一个 fragment,硬件保证整个 warp 32 个 lane 协作完成 16×16×16 = 4096 FMA,一次 mma_sync 指令吞 8 个时钟。

限制: 仅 sm_70+;M/N/K 通常要 16 倍数;fp16 输入精度有限(fp32 累加缓解,但仍不如纯 fp32)。 实际应用通常配 per-channel scaling + loss scaling 防止下溢。

运行结果

9.4 cuBLAS baseline

调 cuBLAS 是判断"自己写得好不好"的标尺:

#include <cublas_v2.h>
cublasHandle_t h; cublasCreate(&h);
float alpha = 1.f, beta = 0.f;

// 注意:cuBLAS 是 column-major!
// row-major C(M,N) = A(M,K) @ B(K,N)
//   ≡  C^T(N,M) = B^T(N,K) @ A^T(K,M)
//   ≡  cublasSgemm(N, N, N, M, K, B, N, A, K, C, N)
cublasSgemm(h, CUBLAS_OP_N, CUBLAS_OP_N, N, M, K,
            &alpha, dB, N, dA, K, &beta, dC, N);

性能数据

9.5 性能采集表(1024³ MatMul)

下面这张表留给目标 GPU 实测。记录 GPU、driver、CUDA、ARCH、矩阵维度、dtype、是否启用 Tensor Core,并用 Nsight Compute 对照 Tensor pipe 与 DRAM pipe。

实现精度GFLOPS% of peak
Ch6 tiled (32×32)fp32TODO(on GPU)TODO(on GPU)
Ch9 register tile (128×128, 8×8)fp32TODO(on GPU)TODO(on GPU)
cuBLAS sgemmfp32TODO(on GPU)TODO(on GPU)
本章 WMMA fp16fp16TODO(on GPU)TODO(on GPU)
cuBLAS gemmEx fp16 + TCfp16TODO(on GPU)TODO(on GPU)
CUTLASS fp16 + TCfp16TODO(on GPU)TODO(on GPU)

本章手写 WMMA 还有大量优化空间(async copy、swizzle、warpgroup mma)。它的目标是理解 Tensor Core 调用路径,不是替代 cuBLAS/CUTLASS。

9.6 下一步技术(不在本章实现,但要知道存在)

自检清单

Q1: register tile 越大越好吗?

不是。每 thread 寄存器有 255 上限,TM=TN=16 就用了 256 个累加寄存器,spill 到 local memory 反而慢。

Q2: 为什么 WMMA 用 fp32 accumulator?

fp16 累加范围太窄(mantissa 仅 10 bit),长 K 的求和会下溢/上溢。fp32 累加保持精度,最后才转 fp16 输出。

Q3: batch=1 decode 为什么通常落入 GEMV 路径?

M=1 时运算退化为 GEMV,算术强度较低,常由权重读取带宽主导;是否使用 Tensor Core 及其收益取决于实现和 shape。

Q4: CUTLASS 是什么?

NVIDIA 开源的 C++ GEMM 模板库,把 tile size / warp 切分 / async copy 都抽象成 type traits。生产 GEMM 几乎都用 CUTLASS 或它的 DSL (CuTe)。第 12 章 FlashAttention 也是基于 CUTLASS 写的。

Q5: Triton 是什么?

OpenAI 的 Python-like GPU DSL,编译到 PTX。比 CUDA 易写、性能接近 CUTLASS,vLLM 大量算子用 Triton 写。

练习题

  1. gemm_reg_tile 加 double buffer(开两块 shared,K-loop 中 prefetch 下一片)。
  2. __pipeline_memcpy_async(sm_80+)替换 shared 加载,看 GFLOPS 提升。
  3. 把 WMMA 改成支持 bf16(把 __half 换成 __nv_bfloat16)。
  4. 用 ncu 看 gemm_wmma 的 Tensor Active %,如果明显偏低,检查 tile、occupancy、shared bank conflict 和 global load 是否喂饱 Tensor Core。

9.9 工业实战:CUTLASS、低精度、autotune、生产 GEMM 决策树

9.9.1 CUTLASS — 不是库,是 GEMM 工厂

cuBLAS 是闭源黑盒,你不能改 kernel 内部行为;CUTLASS 是 NVIDIA 开源的 C++ 模板库,把 GEMM 拆成可拼装的组件:

// CUTLASS CUDA C++ template API(不是 Python CuTe DSL)
#include <cutlass/gemm/device/gemm_universal.h>

using Gemm = cutlass::gemm::device::GemmUniversal<
    cutlass::half_t, cutlass::layout::RowMajor,            // A: fp16 row-major
    cutlass::half_t, cutlass::layout::ColumnMajor,         // B: fp16 col-major
    cutlass::half_t, cutlass::layout::RowMajor,            // C: fp16 row-major
    float,                                                  // accumulator: fp32
    cutlass::arch::OpClassTensorOp,                        // Tensor Core
    cutlass::arch::Sm80,                                   // A100
    cutlass::gemm::GemmShape<128, 256, 32>,                // ThreadBlock tile
    cutlass::gemm::GemmShape<64, 64, 32>,                  // Warp tile
    cutlass::gemm::GemmShape<16, 8, 16>,                   // Instruction shape (mma.sync)
    cutlass::epilogue::thread::LinearCombinationRelu<...>, // epilogue: out = relu(alpha*acc + beta*C)
    cutlass::gemm::threadblock::GemmIdentityThreadblockSwizzle<>,
    3                                                       // pipeline stages (软流水深度)
>;

Gemm gemm;
gemm({M, N, K}, alpha, A, lda, B, ldb, beta, C, ldc, ...);

看似冗长,但 CUTLASS 模板能把 tile、pipeline、swizzle 和 epilogue 交给成熟库生成。大公司生产代码大量基于 CUTLASS(TensorRT-LLM、FlashAttention、xformers),最终性能仍要用 profiler 选择具体配置。

CUTLASS profiler — 自动 autotune

不知道你的 size 该选哪个 tile?跑 profiler:

cd cutlass/build
./tools/profiler/cutlass_profiler \
    --kernels=cutlass_tensorop_h16816gemm \
    --m=4096 --n=4096 --k=4096 \
    --A=f16:row --B=f16:col --C=f16 \
    --accumulator-type=f32 \
    --providers=cutlass,cublas

# 输出: top 5 best-performing configurations

9.9.2 mma.sync vs WMMA:CUTLASS 用前者

本章用了 WMMA API(nvcuda::wmma),它是 fragment 级抽象。CUTLASS 用更底层的 mma.sync PTX 指令:

WMMAmma.sync (PTX)
抽象层fragment + load/mma/store32 lane 的 inline ASM
shape 限制固定 16×16×16, 16×8×8 等少数几种所有硬件支持的 shape
fragment layoutopaque (不知道哪 lane 持有哪个值)明确 (能跟 epilogue 配合)
主要用途教学和快速原型生产 kernel 的精细调度
易写容易需要细心
// mma.sync 长这样 (Ampere fp16):
asm volatile(
    "mma.sync.aligned.m16n8k16.row.col.f32.f16.f16.f32 "
    "{%0,%1,%2,%3}, "                  // D (acc, 4 fp32)
    "{%4,%5,%6,%7}, "                  // A (4 fp16x2)
    "{%8,%9}, "                        // B (2 fp16x2)
    "{%0,%1,%2,%3};\n"                 // C (= D in/out)
    : "+f"(d0), "+f"(d1), "+f"(d2), "+f"(d3)
    : "r"(a0), "r"(a1), "r"(a2), "r"(a3),
      "r"(b0), "r"(b1));

普通工程师不需要直接写 mma.sync——CUTLASS 模板已封装。但读 FlashAttention v2 源码必看懂。

9.9.3 低精度类型一览:fp16 / bf16 / tf32 / fp8 / int8 / int4

类型位数动态范围典型用途支持架构
fp321+8+23±10⁻³⁸ 到 ±10³⁸baseline所有
tf321+8+10同 fp32 但精度低训练默认sm_80+
fp161+5+10±10⁻⁵ 到 ±10⁴ ⚠️ 易溢出推理首选sm_70+
bf161+8+7同 fp32 但精度低训练首选sm_80+ (A100, H100)
fp8 E4M31+4+3±0.0049 到 ±448推理 (前向)sm_89+ (Ada, H100)
fp8 E5M21+5+2±10⁻⁵ 到 ±57344训练 (反向)sm_89+
int8±127推理量化sm_75+
int4±7weight-only 量化sm_80+
fp4 (B200/GB200)格式相关Blackwell 推理sm_100;NVFP4/MXFP4 分开说明

fp16 vs bf16:选哪个?

fp8 — H100 的 LLM 杀手锏

Llama 70B 这类模型使用 fp8 推理(权重或 KV cache 采用 fp8)时,相比 fp16 的主要变化是:

调用:

cublasGemmEx(h, CUBLAS_OP_N, CUBLAS_OP_N, N, M, K,
             &alpha,
             dB_e4m3, CUDA_R_8F_E4M3, N,
             dA_e4m3, CUDA_R_8F_E4M3, K,
             &beta,
             dC_half, CUDA_R_16F, N,
             CUBLAS_COMPUTE_32F,
             CUBLAS_GEMM_DEFAULT);

9.9.4 production GEMM 决策树

graph TD
    Start["GEMM with M, N, K, dtype"] --> M1{"M 等于 1 ?"}
    M1 -->|"是"| GEMV["走 GEMV kernel
weight-only int4 量化"] M1 -->|"否"| Size{"min of M N 小于 64 ?"} Size -->|"是"| Thin["thin GEMM
每 SM 算一行或一列"] Size -->|"否"| K1{"K 大于 4 倍 min M N ?"} K1 -->|"是"| StreamK["CUTLASS Stream-K"] K1 -->|"否"| Dtype{"dtype 是 fp16 bf16 fp8 ?"} Dtype -->|"是"| TC["cublasGemmEx + TC
或 CUTLASS"] Dtype -->|"否 fp32"| Sgemm["cublasSgemm"] style GEMV fill:#f3f1e8,stroke:#a86420 style TC fill:#f3f1e8,stroke:#2f5d3a

9.9.5 LLM 各层 GEMM 的实际 shape

Llama 7B (D=4096, n_head=32, d_head=128, d_ff=11008),prefill 阶段 (T=2048, batch=1)

GEMMM × N × KFLOPs类型
QKV projection2048 × 12288 × 40960.2 TF普通
Output projection2048 × 4096 × 40960.07 TF普通
FFN gate2048 × 11008 × 40960.18 TF普通
FFN up同上同上普通
FFN down2048 × 4096 × 110080.18 TF普通
logits2048 × 32000 × 40960.5 TF普通但 N=32K 大

同样模型 decode 阶段 (M=1):所有 GEMM 都变 GEMV,FLOPs 减为 1/2048 但 HBM 流量不变 → 完全 memory-bound。

这就是 LLM 推理的"两个世界":prefill 在 compute-bound,可以用大 batch + TC 充分利用算力;decode 在 memory-bound,只能靠量化 + KV cache 优化 + speculative decoding 救。

9.10 研究前沿(2025-2026):FP4 GEMM、DeepSeek 原生 FP8、CUTLASS 4.6.1 / CuTe DSL(2026-07 快照)

9.10.1 FP4 GEMM — Blackwell 的杀招

2025 NVIDIA Blackwell 把"用 4-bit 浮点训练 / 推理"从研究变成产品。两种 fp4 微缩放格式:

格式编码block scale来源用途
NVFP4 (E2M1)1+2+1fp8 (E4M3), block=16NVIDIA 私有推理首选,精度高
MXFP4 (E2M1)1+2+1E8M0, block=32OCP 开放标准跨厂商兼容
MXFP6 (E3M2/E2M3)1+3+2 或 1+2+3E8M0, block=32OCP激活用,比 fp4 稳

FP4 GEMM 调用(CUTLASS 4.6.1 CUDA C++ path)

using Gemm = cutlass::gemm::collective::CollectiveBuilder<
    cutlass::arch::Sm100, cutlass::arch::OpClassBlockScaledTensorOp,
    cutlass::nv_float4_t, cutlass::layout::RowMajor, 16,        // A: NVFP4, block 16
    cutlass::nv_float4_t, cutlass::layout::ColumnMajor, 16,     // B: NVFP4
    float, /*accumulator*/                                       // accumulator fp32
    cutlass::gemm::collective::StageCount<4>,                    // 4-stage pipeline
    ...
>::CollectiveOp;

性能判断方式:先按官方 dense/sparse 峰值算 roofline,再用 CUTLASS profiler 或引擎内 benchmark 采集实际 TFLOPS。 FP4 的核心价值是同时降低权重/激活字节数并使用 Blackwell Tensor Core;真实吞吐还取决于 scale 读取、block size、epilogue 融合和 batch shape。

9.10.2 DeepSeek-V3 原生 FP8 训练 — 验证可行性

2024.12 DeepSeek-V3 发布,用 fp8 训练 671B MoE 模型成功,是产业第一个公开复现的案例。技术要点:

开源 fp8 训练框架推荐:

9.10.3 CUTLASS 4.6.1 与 CuTe DSL(2026-07 快照)

按 2026-07 的官方 overview 与 changelog,必须把两条入口分开理解:

# CUTLASS Python 调用 (2025+)
import cutlass
plan = cutlass.op.Gemm(element_a=cutlass.DataType.bf16,
                       element_b=cutlass.DataType.bf16,
                       element_c=cutlass.DataType.bf16,
                       element_accumulator=cutlass.DataType.f32,
                       layout=cutlass.LayoutType.RowMajor)
plan.run(A, B, C, alpha=1.0, beta=0.0)        # 自动选最优 tile

9.10.4 W4A4 / W4A8KV4 — 极致量化推理(2024-2025)

方案权重激活KV cache主要收益来源
fp16 baselinefp16fp16fp16精度稳、实现成熟
W8A8 (SmoothQuant)int8int8fp16权重/激活字节下降,硬件支持广
W4A16 (GPTQ/AWQ)int4fp16fp16decode GEMV 权重读取下降
W4A8 (QServe MIT, 2024)int4int8int4权重、激活和 KV 同时降字节
W4A4 (Atom, QuaRot 2024)int4int4int4需要旋转/scale 设计和专门 kernel
NVFP4 (Blackwell)fp4fp4fp8/fp4匹配 Blackwell Tensor Core 与 block-scale 数据格式

关键论文:

9.10.5 训练 GEMM 的新范式:Microscaling + Transformer Engine 2

Hopper 上的 fp8 训练已经稳定(NVIDIA TE 1)。Blackwell 把它升级到动态精度训练

9.10.6 工业 GEMM 库格局(2026)

定位覆盖
cuBLAS / cuBLASLtNVIDIA 标配, 闭源黑盒全精度, 全架构
CUTLASS 4.6.1 C++ / Python CuTe DSL并存的 template 与 DSL paths按架构、dtype 与当前 changelog 核验
TensorRT-LLM kernels推理 fused (qkv+norm+attention+output)Hopper+Blackwell
ThunderKittens极简研究 DSLHopper+ (Blackwell 适配中)
FlashInfer专注 attention 各变体多平台
TritonPython kernel, vLLM/SGLang 用Ampere-Blackwell
Marlin / MacheteW4A16 极致 kernelAmpere+
FBGEMM_GPU (Meta)fp8 + 量化推理Hopper+

常见坑

9.12 CUDA 官方手册精讲(CUDA Programming Guide 13.2(核验:2026-07-20))

WMMA Fragment、MMA PTX、wgmma、Warp Specialization、TMA + Swizzle

本节定位:把 NVIDIA 官方 CUDA Programming Guide 13.2(核验:2026-07-20) 当前版中和本章直接相关的硬核细节抽出来——概念、API、踩坑点、版本兼容性—— 让你不必通读官方手册也能掌握本章主题的"标准答案"。引用按命名章节回查,避免把旧版编号当作稳定接口。

H100 TMA:硬件张量加载器与张量映射 (CUtensorMap)

9.6 节末尾提到 sm_90+ 的 TMA (Tensor Memory Accelerator):一条指令把 global 张量 tile 异步搬到 shared memory,省掉手写 cp.async 循环。本节给出从主机端到 device 端的最小可运行骨架(基于 CUDA Programming Guide 13.2(核验:2026-07-20) §4.11.2.2)。

第一步:主机端 driver API 构造 CUtensorMap

TMA 不直接读全局指针,而是读一个 128 字节的 张量描述子。它由 driver API cuTensorMapEncodeTiled 在 host 上一次性构造,然后通过 __grid_constant__ 参数传入 kernel:

// host code — D=128, T_tile=64
    constexpr uint32_t rank = 2;
    uint64_t global_dim[rank]   = { (uint64_t)D, (uint64_t)T };        // (cols, rows)
    uint64_t global_stride[rank-1] = { D * sizeof(__half) };           // 单位: 字节
    uint32_t box_dim[rank]      = { 64, 64 };                           // shared 端 tile 形状
    uint32_t elem_stride[rank]  = { 1, 1 };

    CUtensorMap tmap{};
    CUresult res = cuTensorMapEncodeTiled(
        &tmap,
        CUtensorMapDataType::CU_TENSOR_MAP_DATA_TYPE_FLOAT16,
        rank,
        (void*)dQ,            // 全局起始地址
        global_dim,
        global_stride,
        box_dim,
        elem_stride,
        CUtensorMapInterleave::CU_TENSOR_MAP_INTERLEAVE_NONE,
        CUtensorMapSwizzle::CU_TENSOR_MAP_SWIZZLE_128B,                 // ← 关键, 见下
        CUtensorMapL2promotion::CU_TENSOR_MAP_L2_PROMOTION_NONE,
        CUtensorMapFloatOOBfill::CU_TENSOR_MAP_FLOAT_OOB_FILL_NONE);
    

第二步:device 端用 cp_async_bulk_tensor 触发 TMA

#include <cuda/barrier>
    #include <cuda/ptx>
    namespace ptx = cuda::ptx;

    __global__ void gemm_tma(const __grid_constant__ CUtensorMap tmap_A,
                             const __grid_constant__ CUtensorMap tmap_B,
                             __half* C, int M, int N, int K)
    {
        // 1024 字节对齐 — swizzle 128B 模式要求
        __shared__ alignas(1024) __half As[64][64];
        __shared__ alignas(1024) __half Bs[64][64];

        #pragma nv_diag_suppress static_var_with_dynamic_init
        __shared__ cuda::barrier<cuda::thread_scope_block> bar;
        if (threadIdx.x == 0) init(&bar, blockDim.x);
        __syncthreads();

        cuda::barrier::arrival_token tok;
        if (threadIdx.x == 0) {
            int crd[2] = { blockIdx.x * 64, blockIdx.y * 64 };
            // 一条指令:把 64x64 fp16 tile 异步搬到 shared
            ptx::cp_async_bulk_tensor(
                ptx::space_shared, ptx::space_global,
                &As, &tmap_A, crd,
                cuda::device::barrier_native_handle(bar));
            tok = cuda::device::barrier_arrive_tx(bar, 1, sizeof(As));
        } else {
            tok = bar.arrive();
        }
        bar.wait(std::move(tok));     // TMA 完成后继续

        /* ... 在 As/Bs 上跑 wgmma / mma.sync ... */
    }
    
TMA 三件套要点:
  • 共享内存目标必须 1024 字节对齐(128B swizzle 模式);32B/64B 模式按表 25 选择对齐。
  • 触发指令是 uniform:用 threadIdx.x == 0cuda::ptx::is_elected() 让一个线程发起即可,硬件自动调度 DMA。
  • 完成同步用 cuda::barrier + barrier_arrive_tx(事务计数 = tile 字节数),而不是 __syncthreads()

第三步:选 swizzle 模式避免 bank conflict

TMA 可让 shared 端按 swizzle 模式重新排列数据,使后续 wgmma/mma.sync 读取无 bank conflict。v13.1 PG 表 25 总结:

Swizzlespan 宽度shared 内 inner dim 限制用途
NONE≤128 B 对齐即可线性 layout,写回结果用
32B32 字节≤32 B窄 tile,例如 D=16 fp16
64B64 字节≤64 BD=32 fp16 或 D=16 fp32
128B128 字节≤128 BD≥64 fp16 / wgmma 默认

访问换算公式(128B 模式,PDF §4.11.2.2.5 表 24):smem[y][x]smem[y][((y + offset) % 8) ^ x],其中 offset = ((uintptr_t)smem_ptr / 128) % 8。这就是 CUTLASS / FA v3 内层访问看似 XOR 一通的来源——不是杂技,是按 swizzle 表展开。

限制: TMA 的 device 端 modify (tensormap_replace_*) 仅 sm_90a;纯调用 cp.async.bulk.tensor 需要 nvcc -arch sm_90a。本机 (macOS arm64) 无法编译,请在 H100/H200 Linux 节点上跑;B200/GB200 需使用其对应架构目标重新编译。

Hopper Cluster + STAS:跨 thread block 的生产者-消费者流水

Hopper (sm_90) 引入了介于 grid 和 block 之间的 thread block cluster(最多 16 个 block,典型 2-8)。同一 cluster 内的 block 可:

这让 CUTLASS 4.6.1 / CuTe DSL(2026-07 快照) 的 collective builder 把 GEMM 的 "load K/V" 和 "compute" 分到不同 block,靠 cluster 内 DSMEM 接力,实现真正的多级流水。下面是 PG §4.11.3 中的环形生产者-消费者骨架(精简):

#include <cooperative_groups.h>
    #include <cuda/barrier>
    #include <cuda/ptx>

    __global__ __cluster_dims__(8, 1, 1)
    void producer_consumer_kernel()
    {
        namespace cg  = cooperative_groups;
        namespace ptx = cuda::ptx;
        using barrier_t = cuda::barrier<cuda::thread_scope_block>;

        auto cluster = cg::this_cluster();
        __shared__ int       buffer[BLOCK_SIZE];
        __shared__ barrier_t filled, ready;

        if (threadIdx.x == 0) {
            init(&filled, 1);
            init(&ready,  BLOCK_SIZE);
        }
        cluster.sync();   // ← 保证所有 block 的远端 barrier 都已初始化

        int rk      = cluster.block_rank();
        int rk_next = (rk + 1) % 8;
        int rk_prev = (rk + 7) % 8;

        auto buffer_next = cluster.map_shared_rank(buffer, rk_next);
        auto bar_next    = cluster.map_shared_rank(
                              cuda::device::barrier_native_handle(filled), rk_next);

        for (int it = 0; it < 1000; ++it) {
            // 1) 生产者: 把寄存器值 STAS 到下一 block 的 shared buffer
            ptx::st_async(&buffer_next[threadIdx.x], rk, bar_next);

            if (threadIdx.x == 0) {
                ptx::mbarrier_arrive_expect_tx(
                    ptx::sem_release, ptx::scope_cluster, ptx::space_shared,
                    cuda::device::barrier_native_handle(filled), sizeof(buffer));
            }
            // 2) 消费者: 等本地 filled barrier (左邻居写来)
            while (!ptx::mbarrier_try_wait_parity(
                      cuda::device::barrier_native_handle(filled), it & 1)) { }

            /* ... 在 buffer 上做计算 ... */
        }
    }
    

在 GEMM 上的映射:

cluster 内角色典型 warp 数职责
Producer block(s)1-2 warp发起 TMA load,把 A/B tile 推送到 consumer block 的 DSMEM
Consumer block(s)4-8 warp跑 wgmma 累加,写回 epilogue
STAS 限制:
  • 仅支持 4 / 8 / 16 字节单次拷贝(PDF §4.11.3)。
  • 方向只能是 register → distributed shared,不能反向。
  • 必须用 shared memory barrier(不是 __syncthreads)来同步——本质是 async proxy 操作。
  • 跨 block barrier 必须用 ptx::scope_cluster,本地 barrier 用 ptx::space_shared,混用直接死锁。

wgmma vs mma.sync:Hopper warpgroup MMA 的 PTX 真身

Ampere 的 mma.sync一个 warp (32 lane) 协作完成 16×8×16 fp16;Hopper 的 wgmma.mma_async一个 warpgroup (4 warps = 128 lane) 协作完成更大的 m×n×k 操作,且是 异步(提交后立即返回,靠 fence/commit_group 等待)。

mma.sync (Ampere fp16)wgmma (Hopper fp16)
参与线程1 warp (32)1 warpgroup (4 warp = 128)
形状示例m16 n8 k16m64 n{8..256} k16
A 来源寄存器寄存器 shared memory + descriptor
B 来源寄存器shared memory + descriptor(必须 TMA 加载并 swizzle)
执行同步指令异步 issue + wgmma.commit_group + wgmma.wait_group
累加器每 lane 拥 4 fp32warpgroup 拥 m×n / 4 fp32,跨 warp 分布

典型 wgmma 触发序列(Hopper, fp16 输入 fp32 累加):

// 1) TMA load A, B 到 shared (上节代码), 用 128B swizzle
    // 2) 构造 wgmma "descriptor" — 编码 shared 地址 + swizzle 模式
    //    libcu++ / CUTLASS 把这步封装在 SM90_GMMA::Desc

    // 3) issue 一组 wgmma
    asm volatile (
        "wgmma.fence.sync.aligned;\n"
        "wgmma.mma_async.sync.aligned.m64n128k16.f32.f16.f16 "
        "{%0,%1,...,%63},"        // 64 个 fp32 累加器(warpgroup 持有)
        " %64, %65, "             // A, B 的 shared descriptor (uint64)
        " 1, 1, 1, 0, 0;\n"       // scale_d, scale_ab, im_layout 等
        "wgmma.commit_group.sync.aligned;\n"
        : /* outputs ... */
        : "l"(desc_A), "l"(desc_B));

    // 4) 等待这一组完成
    asm volatile ("wgmma.wait_group.sync.aligned 0;\n");
    
为什么 wgmma 必须配 TMA + cluster:
  1. wgmma 一次消化 m64 nN k16 数据量,没有 TMA 喂不饱。
  2. B descriptor 默认走 128B swizzle,要 TMA 在加载时同时完成 swizzle,软件再 rearrange 来不及。
  3. warpgroup = 128 lane 同时活跃,单个 block (256 thread = 2 warpgroup) 已紧;要做多级流水必须 cluster 内多 block 协作。
这就是 CUTLASS 4.6.1 / CuTe DSL(2026-07 快照) / FA v3 都把 TMA + cluster + wgmma 三件套捆绑使用的原因。
    graph LR
        H["Host: cuTensorMapEncodeTiled
构造 A/B 的 CUtensorMap"] H --> K["Kernel launch
__cluster_dims__(2,1,1)"] K --> P["Producer block:
cp.async.bulk.tensor (TMA)
→ DSMEM"] P --> C["Consumer block warpgroup:
wgmma.mma_async × N
fp32 累加在 regs"] C --> EP["Epilogue:
cp.async.bulk.tensor (TMA store)
→ HBM"] style P fill:#f3f1e8,stroke:#a86420 style C fill:#f3f1e8,stroke:#2f5d3a

普通工程师写 GEMM 不需要手写这段 wgmma 内联汇编——用 CUTLASS 4.6.1 / CuTe DSL(2026-07 快照) 的 CollectiveBuilder<arch::Sm90, ...> 自动生成。但 9.10.2 提到 DeepSeek-V3 自己手写了 fp8 wgmma kernel(绕开 CUTLASS 模板膨胀编译时间),这段背景是必备。

下一章导览

第 10 章把 reduction 与数值稳定性组合成 Softmax、LayerNorm 和 RMSNorm。