第 6 章 · Shared Memory & Tile

⏱️ 75 分钟🎯 用复用提高 achieved GFLOPS📂 code/ch06_tile/🔥 关键瓶颈章

学习目标

前置知识

已完成 Ch05,能分析合并访问、shared-memory bank 和 global-memory 流量。

核心概念

6.1 Naive Matmul 为什么慢?

源码:matmul_naive.cu。每个 output cell 由一个 thread 跑一个 K 长度的内积:

__global__ void matmul_naive(const float* A, const float* B, float* C,
                             int M, int N, int K) {
    int row = blockIdx.y * blockDim.y + threadIdx.y;
    int col = blockIdx.x * blockDim.x + threadIdx.x;
    float acc = 0.f;
    for (int k = 0; k < K; ++k)
        acc += A[row * K + k] * B[k * N + col];
    C[row * N + col] = acc;
}

算术强度分析:每个 cell 做 2K FLOPs(K 次乘 + K 次加),读 2K floats = 8K bytes。 比率 0.25 FLOP/Byte。A100 的 roofline:

所以 naive kernel 往往先撞到内存 roofline,而不是峰值算力。优化的核心目标是:把数据读进 shared memory 一次、多次复用,再在目标 GPU 上测量 achieved throughput。

关键代码

6.2 Tiled MatMul — shared memory 复用

把 C 切成 32×32 的 tile,每个 block 算一个 tile。K 维度也切:

graph LR
    subgraph "K-loop: K/BK 个 tile"
      direction TB
      L1["拉 A 的 32×32 slice → As"]
      L2["拉 B 的 32×32 slice → Bs"]
      L3["__syncthreads()"]
      L4["32 次 fmadd 到 reg acc"]
      L5["__syncthreads()"]
      L1 --> L2 --> L3 --> L4 --> L5
    end
    style L4 fill:#f3f1e8,stroke:#2f5d3a

源码:matmul_tiled.cu。核心:

constexpr int BM = 32, BN = 32, BK = 32;

__global__ void matmul_tiled(const float* A, const float* B, float* C,
                             int M, int N, int K) {
    __shared__ float As[BM][BK];
    __shared__ float Bs[BK][BN];

    int row = blockIdx.y * BM + threadIdx.y;
    int col = blockIdx.x * BN + threadIdx.x;
    float acc = 0.f;

    for (int kt = 0; kt < K; kt += BK) {
        As[threadIdx.y][threadIdx.x] = A[row * K + kt + threadIdx.x];
        Bs[threadIdx.y][threadIdx.x] = B[(kt + threadIdx.y) * N + col];
        __syncthreads();
        #pragma unroll
        for (int k = 0; k < BK; ++k)
            acc += As[threadIdx.y][k] * Bs[k][threadIdx.x];
        __syncthreads();
    }
    C[row * N + col] = acc;
}

关键收益:每个 A、B 元素被 32 个线程读,但只从 global memory 读 1 次。算术强度从 0.25 提升到 8(提高 32×),roofline 上限拉到 12 TFLOPS。

运行结果

先对所有矩阵形状执行 CPU/GPU 对拍,再把计时结果写入同一硬件、同一精度的实验记录。

性能数据

⚠️ 需 GPU 验证:统一性能表暂记为 TODO(on GPU),不得以跨设备数字替代实测。

6.3 继续往上爬:四个进阶方案

6.2 的 32×32 tile 通过 shared-memory 复用提高算术强度,但距离具体 GPU 上的最佳实现还有多级优化空间。 继续优化有四条路,本节做概念预览,第 9 章会给完整代码与 Nsight 分析。 理解这四条路的思路,6.3 末尾的结果对照表才看得懂。

6.3.1 寄存器 tile:每 thread 算 4×4 或 8×8 个 cell

当前 tiled 版本:每 thread 在内层循环里读 2 个 shared 值(一个 As 行、一个 Bs 列),做 1 个 fmadd。 shared 读取与算力的比例 = 1 FLOP / 2 shared-load = 0.5,shared mem 仍是瓶颈。

如果每 thread 算 4×4 = 16 个 cell:

constexpr int BM = 64, BN = 64, BK = 8;
constexpr int TM = 4,  TN = 4;       // 每 thread 算 4×4
float acc[TM][TN] = {0};             // 16 个 reg 累加器

for (int kt = 0; kt < K; kt += BK) {
    /* 协作把 A 的 64×8 + B 的 8×64 加载到 shared */
    __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();
}

这就是本章练习题 #1starter)要写的版本。 BM=BN=64, BK=8 时 block 大小 16×16=256 thread,每 thread 持有 16 个 reg; 扩到 BM=BN=128, TM=TN=8 还能再快一倍——代价是寄存器用得更狠(接近 256/thread 上限)。 第 9 章会推到这个版本。

6.3.2 直接调 cuBLAS(baseline)

cuBLAS 是 NVIDIA 闭源的 BLAS 实现,跟着 CUDA Toolkit 一起装。 它包含 cublasSgemm(fp32 矩阵乘)、cublasGemmEx(任意精度)等。 是衡量"我自己写得怎么样"的标尺——能跑到 cuBLAS 的 80%+ 就算优秀。

cuBLAS 之所以快,是因为底层用了几种自己手写很难做到的工程:

#include <cublas_v2.h>
cublasHandle_t h; cublasCreate(&h);
float alpha = 1, beta = 0;
// 注意 cuBLAS 是 column-major (沿用 Fortran/LAPACK)
// row-major C(M,N) = A(M,K) @ B(K,N) 等价于 C^T = B^T @ A^T,
// 所以传参顺序"看似乱反": (N, M, K, B, A, C).
cublasSgemm(h, CUBLAS_OP_N, CUBLAS_OP_N, N, M, K,
            &alpha, dB, N, dA, K, &beta, dC, N);

本仓库 code/ch09_gemm/gemm_cublas.cu 就是这个 baseline。

6.3.3 启用 Tensor Core(fp16 / bf16 / tf32)

到目前为止我们都在用 CUDA core——一周期吐一个 fp32 FMA。 Volta(2017)开始 NVIDIA 在 SM 里塞了Tensor Core——专门做 16×16×16 矩阵乘加的硬件, 一条指令完成 4096 个 FMA。算力跳跃式提升:

GPUfp32 (CUDA core)fp16 (Tensor Core)倍数
T48.1 TFLOPS65 TFLOPS
A10019.5 TFLOPS312 TFLOPS16×
H10067 TFLOPS989 TFLOPS15×
RTX 409083 TFLOPS330 TFLOPS

触发 Tensor Core 需要满足:

  1. 数据类型为 fp16 / bf16 / tf32 / int8 / fp8(不能是纯 fp32)
  2. M / N / K 对齐到 8 或 16 的倍数
  3. cublasGemmEx 并把 compute type 设为 Tensor Core 类型
cublasGemmEx(h, CUBLAS_OP_N, CUBLAS_OP_N, N, M, K,
             &alpha,
             dB_half, CUDA_R_16F, N,
             dA_half, CUDA_R_16F, K,
             &beta,
             dC_half, CUDA_R_16F, N,
             CUBLAS_COMPUTE_16F,           // <-- 关键: 让 cuBLAS 走 TC 路径
             CUBLAS_GEMM_DEFAULT_TENSOR_OP);

第 9 章会手写 WMMA API 让你看到 Tensor Core 的 "魔法" 是怎么发生的——其实就是 16×16 的 fragment + mma.sync 指令。 后面 LLM 推理章节(11/12/14)所有 GEMM 都默认走 TC 路径。

6.3.4 终极:CUTLASS / 手写 WMMA(建立可测 baseline)

cuBLAS 是闭源黑盒。CUTLASS(NVIDIA 开源的 C++ 模板库)把上面这些技巧都暴露成 type traits,让你能:

CUTLASS 是许多 NVIDIA GPU kernels 的重要构件。模板代码量、可维护性与性能取决于具体集成;用 CUTLASS profiler 和同 shape 的 cuBLAS baseline 比较。

6.3.5 把上面五条路放一起看(1024³ MatMul · T4)

实现精度时间GFLOPSvs naive对应章节
naive (Ch6.1)fp32TODO(on GPU)TODO(on GPU)baseline本章
tiled 32×32 (Ch6.2)fp32TODO(on GPU)TODO(on GPU)TODO(on GPU)本章
2D register tile (6.3.1)fp32TODO(on GPU)TODO(on GPU)TODO(on GPU)练习 / Ch9
cuBLAS Sgemm (6.3.2)fp32TODO(on GPU)TODO(on GPU)TODO(on GPU)Ch9 对比
cuBLAS GemmEx + TC (6.3.3)fp16TODO(on GPU)TODO(on GPU)TODO(on GPU)Ch9 详讲
CUTLASS 手调 + TC (6.3.4)fp16TODO(on GPU)TODO(on GPU)TODO(on GPU)Ch9 剧透
读懂这张表的关键:naive → tiled 主要观察shared memory 复用; tiled → register tile 主要观察寄存器复用;register tile → cuBLAS 还包含软流水 + autotune; cuBLAS sgemm → fp16+TC 则切换到 Tensor Core。 每一步都对应一种独立的优化思路,可以拆开学也可以叠加用。

6.4 Bank Conflict — Shared Memory 的隐藏陷阱

shared memory 物理上分 32 个 bank(32-bit-word 交错)。一个 warp 同时访问 shared 时:

经典案例:__shared__ float tile[32][32],访问 tile[k][threadIdx.x] 时一切正常;但访问 tile[threadIdx.x][k] 时(按列),32 个 lane 落到同一 bank → 32-way conflict。

对照实验:transpose.cu

kernelshared 布局带宽 (T4)
naive (写 strided)无 sharedTODO(on GPU)
shared, [32][32]32-way bank conflict on readTODO(on GPU)
shared, [32][33]+1 padding 消除冲突TODO(on GPU)

消除方法 1:+1 padding(教学版,够用但浪费 ~3% shared)

// 没 padding: tile[ty][k] 和 tile[ty+1][k] 落到同一 bank
__shared__ float tile[32][32];

// 有 padding: 列方向 + 1 → 每行长度变 33, 错开 bank
__shared__ float tile[32][33];

消除方法 2:XOR swizzle(工业版,CUTLASS 在用)

+1 padding 的问题:① 浪费 1/32 的 shared 容量;② 跟 Tensor Core 要求的 fragment 对齐冲突(fragment 行必须是 16 / 32 字节倍数);③ 一旦 BK 不是固定 32,padding 量要重新算。生产代码用XOR swizzle

// 写入时把 col 与 row 高位 XOR 一下, 让相邻 row 的同 col 落到不同 bank
// (CUTLASS 的 cutlass::layout::TensorOpMultiplicand 自动选择)
auto swizzle_idx = [](int row, int col) {
    return row * 32 + (col ^ ((row >> 0) & 7));
    //                       ^^^^^^^^^^^^^^^^
    //                       每 8 行循环错位一次
};
tile[swizzle_idx(ty, tx)] = global_A[...];

swizzle 可避免 padding 空间,同时保留 Tensor Core 对齐,并为行列访问设计无冲突映射。代价是布局更复杂,所以通常用 CUTLASS 模板生成并通过 bank-conflict metric 验证。

读写两侧都要看

Bank conflict 不只在 load 时发生,store(写 shared)也会。常见坑:转置类 kernel 写入是 coalesced,读出按列就 32-way conflict——这是 6.4 的 transpose 例子。写一次读一次的对称性是 shared 布局设计的灵魂。

6.5 工业实战:教程到生产之间的 8 个套路

到这里你已完成从 naive 到 tiled matmul 的结构性优化。 与 cuBLAS 的差距由 shape、dtype、tile 和 GPU 决定。本节列出常用优化套路,并要求逐项测量而不是预设排序。

6.5.1 向量化加载(float4 / half2)— 最低投入回报最高

标量 As[i][j] = A[...] 与 vectorized load 会产生不同的 load 指令和 transaction 组合。 对齐、数据类型、lane 地址与 cache line 决定实际合并结果;用 SASS 和 requested/actual bytes 验证。

// ❌ 慢:每 thread 一个 float
for (int i = tid; i < BM*BK; i += blockDim.x*blockDim.y)
    As[i / BK][i % BK] = A_global[...];

// ✅ 快:每 thread 一个 float4,指令数 1/4,HBM 事务合并
const float4* A4 = reinterpret_cast<const float4*>(A_global);
float4*       S4 = reinterpret_cast<float4*>(&As[0][0]);
for (int i = tid; i < (BM*BK)/4; i += blockDim.x*blockDim.y)
    S4[i] = A4[base + i];

要求:地址必须自然对齐 16 字节;tile 尺寸的最低维(通常是 BK)是 4 的倍数。fp16 时用 __half2uint4(一次搬 8 个 fp16)。

练习:固定 GPU、CUDA、shape、dtype 与 warmup,比较标量和 vectorized load 的 指令数、global-load efficiency 与 kernel 时间;先 allclose,再判断差异是否超过计时噪声。

6.5.2 软流水 + cp.async(sm_80+)— 把 HBM 延迟藏起来

教程 K-loop 是串行的:load → sync → compute → sync → 下一轮。GPU 在 load HBM 那 ~200 个周期里计算单元闲着。Ampere 引入 cp.async PTX 指令让 global → shared 拷贝异步触发:

// 同步版 (教程):
As[i][j] = A[...];                          // LDG → STS, 阻塞
__syncthreads();
compute(As);                                 // 等 load 完才能开始

// 异步版 (生产):
__pipeline_memcpy_async(&As[i][j], &A[...], sizeof(float4));
__pipeline_commit();                         // 提交但不等
compute(As_prev);                            // 用上一轮已到位的 As, 计算与 load 并行
__pipeline_wait_prior(0);                    // 真正要用新 As 时才 wait
__syncthreads();

配合 double buffer(开两块 shared 奇偶轮交替)实现完整软流水:

串行 (教程)               时间轴 →
  load_tile_0      ━━━━
  compute_tile_0       ━━━━
  load_tile_1              ━━━━
  compute_tile_1                ━━━━
  load_tile_2                       ━━━━
  compute_tile_2                          ━━━━

软流水 (生产, 重叠)       时间轴 →
  load_tile_0      ━━━━
  load_tile_1          ━━━━                ← prefetch
  compute_tile_0       ━━━━                ← 同时 compute
  load_tile_2              ━━━━
  compute_tile_1           ━━━━
  compute_tile_2               ━━━━

总时间从 12 缩到 8 单位 (节省 33%) — 长流水越长收益越大

CUTLASS 的多阶段流水会预取多个 tile,以重叠内存访问与 MMA;stage count 受 shared memory、 register、tile shape 和 occupancy 限制。Ch9 用 profiler 比较不同配置。

6.5.3 解锁 48 KB+ shared memory

CUDA 默认每 block 最多 48 KB shared,但 sm_80+ SM 物理上有 164–228 KB。要解锁更多必须显式申请

size_t shm_bytes = 100 * 1024;
cudaFuncSetAttribute(my_kernel,
                     cudaFuncAttributeMaxDynamicSharedMemorySize,
                     shm_bytes);
my_kernel<<<grid, block, shm_bytes, stream>>>(...);

// kernel 内用 extern shared,运行时定大小:
extern __shared__ float dyn_shm[];
float* As = dyn_shm;
float* Bs = dyn_shm + BM * BK;

典型生产配置:

典型坑:忘了 cudaFuncSetAttribute 直接申请 100 KB → too many resources requested for launch。Nsight 也不会告诉你具体原因,只能凭经验。

6.5.4 Epilogue 融合 — LLM 性能的真正胜负手

LLM 推理里几乎每个 GEMM 都接着做点别的:

Y = activation(GEMM(X, W) + bias)        // 标准 FFN 第 1 层
Y = RMSNorm(GEMM(X, W) + residual)        // attention output
Y = silu(GEMM(X, W_gate)) ⊙ GEMM(X, W_up) // SwiGLU
Y = dequant(int8_GEMM(X_int8, W_int8) * scale)  // 量化推理

教程做法:3-5 个独立 kernel,中间结果每次进出 HBM。memory-bound 算子被 HBM 带宽锁死。生产做法:把 bias / activation / norm / quantize 都融合到 GEMM 的写回阶段(epilogue):

// GEMM mainloop 结束后,acc[TM][TN] 还在寄存器里
// 直接在这里做 bias + activation 再写 HBM, 省掉一次 N×D 的 HBM 来回
#pragma unroll
for (int i = 0; i < TM; ++i)
#pragma unroll
for (int j = 0; j < TN; ++j) {
    float v = acc[i][j];
    v += bias[col0 + j];                    // bias broadcast
    v = v * (1.f / (1.f + __expf(-v)));     // SiLU
    C[(row0 + i) * N + col0 + j] = v;       // 一次性写出
}

对 memory-bound 的小算子,融合 epilogue 可能减少中间张量读写;收益取决于 shape、dtype 与后续操作。CUTLASS 把 epilogue 设计成可拼接的模板(cutlass::epilogue::collective),应与未融合路径做正确性和 profile 对照。

6.5.5 GEMV (M=1):LLM decode 完全不能用 tile

到目前为止讨论假设 M、N、K 都比较大。但 LLM 推理 decode 阶段,每步只生成 1 个 token

hidden state shape  : (1, D)         ← M=1 !
weight              : (D, 4D)
output              : (1, 4D)

这是 GEMV 不是 GEMM。tile 策略完全不适用:

工业方案是专用 GEMV kernel

vLLM、TensorRT-LLM、llama.cpp 都有专门的 GEMV 实现路径。本仓库 Ch13 会让你自己写一个 fp16 GEMV,Ch14 capstone 默认走这条路。

6.5.6 Split-K / Stream-K — 处理"细长"形状

常规 tile 策略假设 M、N、K 大致相当。但下面这种 GEMM 让 SM 大半闲着:

M = 128, N = 128, K = 16384
→ tile 128×128 → 只 1 个 output tile → 1 个 block → A100 上 108 个 SM 用 1 个,其他 107 个浪费

Split-K:把 K 维拆 8 份,每份独立算 partial sum 写不同 buffer,最后再 reduce。block 数 ×8,SM 跑满。 Stream-K(NVIDIA 2023):进一步把 K 切到比 block 还细的单位,按 SM 调度均衡负载——是当前 CUTLASS 默认策略,比 Split-K 更适应不规则 size。

什么时候用:min(M, N) < 256K > 2048 时强烈推荐。生产代码会有形状感知逻辑:

if (M * N < 16 * 16 * num_sms && K > 2048) {
    launch_stream_k_kernel(...);   // 长 K
} else if (M == 1) {
    launch_gemv_kernel(...);       // decode
} else {
    launch_gemm_kernel(...);       // 普通
}

6.5.7 持久化 kernel — 杀掉 launch overhead

kernel 启动包含 CPU→GPU 命令传递与 scheduler 分发;当单步 decode 触发许多短 kernel 时,launch gap 可能成为端到端瓶颈,应由 Nsight Systems 实测。

持久化 kernel:让 block 长期驻留 SM,从 host 准备的 work queue 拉任务循环处理,整个 decoding loop 只发一次 launch:

__global__ void persistent_engine(WorkItem* queue, int* head, int n_items) {
    while (true) {
        int my_id = atomicAdd(head, 1);
        if (my_id >= n_items) break;
        process(queue[my_id]);
    }
}

推理引擎会组合 persistent-style kernels 与 CUDA Graph(见 Ch8)来减少 host launch gaps。端到端收益取决于 batch、graph bucket、CPU scheduler 和 kernel 时长,应从 timeline 测量。

6.5.8 Hopper TMA — 硬件级 tile 加载

H100 引入 TMA (Tensor Memory Accelerator):一条指令告诉硬件 "把 global memory 的 64×64 tile 拷到 shared mem 的某处",硬件自动处理跨页、跨 bank 的 swizzle、边界,CPU 完全不写 load 循环。

等价代码(PTX 简化):

// pre-Hopper: 几十行手写 load 循环
for (int i = tid; i < BM*BK/4; i += 256)
    cp_async(&As[...], &A[...]);

// Hopper TMA: 1 条指令
cuda::ptx::cp_async_bulk_tensor_2d(&As[0][0], tma_descriptor, {row0, col0});

FlashAttention v3 几乎完全用 TMA 替代手写 load。这是 Hopper 上 fp16 GEMM 能跑到 700+ TFLOPS(接近 989 TF peak)的核心机制。

6.6 工业 tile size 选择速查表

记忆比推导实用。下面是 production GEMM kernel 在不同场景下的常用 tile 配置(基于 CUTLASS profiler + 真实部署):

场景shape 范围推荐 (BM × BN × BK)per-thread (TM × TN)备注
fp32 通用 GEMMM,N,K ≥ 256128 × 128 × 88 × 8A100 甜点,本章练习题方向
fp16 + TC 训练大 batchM,N,K ≥ 512128 × 256 × 32WMMA 16×8×16CUTLASS 默认
fp16 + TC 推理 prefillM ≥ 25664 × 256 × 32WMMA倾向更瘦的 BM
LLM decode (M=1)M = 1不用 tile切到 GEMV kernel
长 KK > 4096, M·N 小同上 + Stream-K见 6.5.6
小 batch CNNK 小 (<64)32 × 32 × 164 × 4K 小时 TC 收益小
fp8 GEMM (Hopper)M,N,K ≥ 1024128 × 256 × 64wgmma 64×N×16用 TMA

tile 选择的 4 条经验法则

  1. BM × BN ≤ 16K(fp32)或 ≤ 32K(fp16):保证两块 shared + accumulator 装得下 SM 的 100-200 KB
  2. BK ≥ 8:BK 太小 → 软流水间隙小,HBM 延迟藏不住;BK 太大 → shared 占用多挤压 occupancy
  3. grid 数 ≥ SM 数 × 2:让 GPU 有多波 (wave) work 可调度,避免尾部 SM 闲置(H100 有 132 SM → 至少 264 个 block)
  4. per-thread tile TM × TN ≤ 64:超过会触发寄存器 spill 到 local memory(实际是 global 私有分区,奇慢)。Nsight 的 "Register Spills" 行非零就是这个

6.7 实战案例:一次 fused QKV GEMM 的调优历程

下面是待采集的 GEMM 优化记录模板,目标 workload 为 fused QKV projection: M=2048、N=2304、K=768。仓库没有对应 GPU artifact,因此不填入数字;在目标 GPU 上用相同输入和 correctness gate 逐步采集到 benchmarks/reports/ch06/<gpu>-<date>.md

#改动时间TFLOPSpeak 口径本教程对应
0baseline tileTODO(on GPU)TODO(on GPU)注明 dtype 与 dense/sparseCh6.2
register tileTODO(on GPU)TODO(on GPU)同上Ch6.3.1 / Ch9
vectorized loadTODO(on GPU)TODO(on GPU)同上Ch6.5.1
WMMA Tensor CoreTODO(on GPU)TODO(on GPU)同上Ch9
async copy + double bufferTODO(on GPU)TODO(on GPU)同上Ch6.5.2
cuBLAS baselineTODO(on GPU)TODO(on GPU)同上Ch9

分析:

给读者的现实期望:本教程的目标是完成 register tile、vectorized load、Tensor Core 与 async-copy 的正确实现,并学会用 cuBLAS/CUTLASS 建立对照;不承诺固定峰值比例。

自检清单

Q1: 为什么 tile 大小不直接开到 128×128?

shared memory 受限。32×32 fp32 = 4 KB/tile, 两块 8 KB;128×128 fp32 = 64 KB/tile,两块 128 KB——超过 SM 上限 (A100 192 KB) 后 occupancy 急降。fp16 + Tensor Core 时数据占一半,可以放更大 tile。也别忘了 cudaFuncSetAttribute

Q2: #pragma unroll 不写会怎样?

编译器自己决定。BK=32 这种小循环通常会自动展开,但对 BK=数百 的循环可能保守。展开能让寄存器内积累更紧凑,避免依赖链。

Q3: 为什么需要两次 __syncthreads()

第一次:保证 As/Bs 都装满后才开始算(生产者→消费者)。第二次:保证全 block 都算完后才覆盖 As/Bs(消费者→生产者)。少任一个都会 race,且常常表现为"大部分输出对,个别 cell 错",调试地狱。

Q4: float4 vec load 有什么前提?

① 起始地址 16 字节对齐(malloc 出来的指针自然满足,但 +offset 后必须 offset 也是 4 的倍数);② 拷贝长度是 4 的倍数;③ tile 的最低维(BK 或 BN)是 4 的倍数。不满足任一个,nvcc 会回退到逐 float load,悄悄丢性能。

Q5: 我看到 Nsight 报 "Stack Frame Spill" 是什么?

寄存器溢出。编译器为某 kernel 分配的寄存器超过硬件上限(A100 thread 最多 255 reg),多余的本地变量被放到 local memory(其实是 global memory 的私有分区,奇慢)。常见于 TM×TN 过大或循环展开过头。解法:减小 per-thread tile、加 -maxrregcount=N、或用 __launch_bounds__

Q6: 我的 kernel 在 size=1024 时跑得很好,size=1023 就崩,为什么?

tile 边界没处理好。1024 = 32×32 整除,1023 不整除——最后一个 tile 越界。修复:load 时加 if (gr<M && gc<K) 写 0,write 时加 if (row<M && col<N) 跳过。生产代码用 CUTLASS 的 "predicated tile iterator" 自动处理。

Q7: 我把 BM 从 32 改成 64,性能反而降了,为什么?

三种可能:① shared 占用增加,SM 上驻留 block 数减少;② 寄存器用量增加触发 spill;③ block 内 thread 不够多导致 warp 不满。Nsight 的 "Theoretical Occupancy" vs "Achieved Occupancy" 能帮你定位。

练习题

  1. 01_2d_block_tile_starter.cu:实现"每 thread 算 4×4 个 cell"的版本,目标 GFLOPS 提升到 tiled 版的 2×。
  2. matmul_tiled__shared__ float As[BM][BK+1] 看 bank conflict 减少情况(用 Nsight Compute 看 l1tex__data_bank_conflicts_pipe_lsu_mem_shared)。
  3. 调 BK 从 8 到 64,记录 GFLOPS 曲线——为啥 BK=32 是甜点?(提示:BK 过小流水间隙小,BK 过大 shared 挤压 occupancy)。
  4. 把 transpose 的 TILE 从 32 改 16/64,看带宽变化。
  5. 工业级:把练习 #1 的 kernel 改成 vec-load 版(float4)。预期再 +30%。
  6. 工业级:把 epilogue 融合到练习 #1 中——在写回前加一个 ReLU 或 +bias。对比 "GEMM+独立 bias_add kernel" 的总耗时。
  7. 挑战:让你的 kernel 支持 M、N、K 任意值(不是 32 的倍数)。这是 CUTLASS 几千行模板代码要解决的核心问题。

6.10 研究前沿(2025-2026):Warp Specialization、WGMMA、TMEM GEMM

6.10.1 Warp Specialization — FA v3 + DeepSeek FlashMLA 的核心模式

传统 GEMM/Attention kernel:block 内所有 warp 做同样的事(load → compute → load → compute),通过 __syncthreads 协调。瓶颈:load 时 compute 单元闲、compute 时 load 单元闲。

warp specialization 把 block 内 warp 分两组:

__global__ void warp_spec_gemm(...) {
    extern __shared__ __align__(128) uint8_t smem[];
    __shared__ uint64_t mbar_load[N_STAGES], mbar_compute[N_STAGES];
    if (threadIdx.x == 0)
        for (int i = 0; i < N_STAGES; ++i) {
            mbarrier_init(&mbar_load[i],    1);
            mbarrier_init(&mbar_compute[i], 1);
        }
    __syncthreads();

    int warp_id = threadIdx.x / 32;
    if (warp_id == 0) {                          // PRODUCER
        for (int k = 0; k < K_iters; ++k) {
            int slot = k % N_STAGES;
            mbarrier_wait(mbar_compute[slot]);   // 等 consumer 用完上一轮
            tma_load_async(smem + slot, K_global + k);
            mbarrier_arrive(mbar_load[slot]);
        }
    } else {                                     // CONSUMERS (warps 1..N-1)
        for (int k = 0; k < K_iters; ++k) {
            int slot = k % N_STAGES;
            mbarrier_wait(mbar_load[slot]);      // 等 producer load 完
            wgmma_async(acc, smem + slot, ...);
            wgmma_commit();
            wgmma_wait<0>();
            mbarrier_arrive(mbar_compute[slot]);
        }
    }
}

FA v3 的论文结果属于论文指定的 H100、dtype、shape 与软件栈;本章只借其 producer/consumer 结构解释 WGMMA/TMA pipeline,不把论文数字当作本仓库测量。

6.10.2 WGMMA — Hopper 的 warp-group MMA

Hopper 引入 wgmma(warp-group MMA):4 个 warp(128 lane)协作执行一个大 MMA,shape 可达 m64n256k16(fp16)。

对比:

架构指令shape每指令 FLOP
Volta sm_70mma.sync.m8n8k48×8×4512 (fp16)
Ampere sm_80mma.sync.m16n8k1616×8×164096
Hopper sm_90wgmma.async.m64n256k1664×256×16524288 (fp16)
Blackwell sm_100tcgen05.mma (2-CTA)256×256×162M+ (fp4)

关键变化:wgmma 是异步的——发射后立即返回,warp 可以继续干别的,accumulator 在后台写完。这就是软流水的硬件版。

6.10.3 ThunderKittens — 100 行写 attention 的秘诀

Stanford Hazy Research 2024 推出的 C++ 嵌入式 DSL,把上面这堆 wgmma + mbarrier + TMA 抽象成"tile 操作"

// ThunderKittens 完整 attention forward (单 head, 极简, ~80 行)
#include "kittens.cuh"
using namespace kittens;

template <int D>
__global__ void tk_attn(...) {
    auto q = make_rt<bf16, 64, D>();         // register tile (block 内)
    auto k = make_st<bf16, 64, D>();         // shared tile (block 内)
    auto v = make_st<bf16, 64, D>();
    auto o = make_rt<float, 64, D>();
    auto m = make_col_vec<float, 64>();      // running max
    auto l = make_col_vec<float, 64>();      // running sum

    load_async(q, Q_global);
    zero(o); neg_inf(m); zero(l);

    for (int kt = 0; kt < n_kv_tiles; ++kt) {
        load_async(k, K_global + kt);
        load_async(v, V_global + kt);
        kittens::wait();                       // 等 cp.async / TMA

        auto s = mm<TransB>(q, k);             // 自动 wgmma
        auto m_new = max(m, row_max(s));
        sub_row(s, m_new);
        exp(s);
        l = exp(m - m_new) * l + row_sum(s);
        o = exp(m - m_new) * o + mm(s, v);     // 自动 wgmma
        m = m_new;
    }
    div_row(o, l);
    store(O_global, o);
}

ThunderKittens 可用较紧凑的抽象表达 attention 原型;与 CUTLASS 的性能和代码量比较必须固定 feature set、shape、dtype、编译参数与 correctness gate。

6.10.4 Blackwell TMEM GEMM — TCGEN05

Blackwell(sm_100)的 GEMM 范式再次变化:

// Hopper:
wgmma_async(acc_reg, A_smem, B_smem);    // acc 在 register

// Blackwell:
tcgen05_mma_async(acc_tmem, A_smem, B_smem);   // acc 在 TMEM (新)
// ... 其他事 ...
tcgen05_ld(acc_reg, acc_tmem);           // 用之前才读到 register

TMEM 可让部分 accumulator 不长期占用 register file,但 register、occupancy 与最终吞吐的变化依赖 tile 和 compiler allocation。配合 microscaling 与 2-CTA MMA 后仍需在 Blackwell 上 profile: TODO(on GPU)

6.10.5 实战推荐路径(2026)

目标硬件推荐方式预期性能
A100 (sm_80)本章 + cp.async + register tileTODO(on GPU)
H100 (sm_90)warp spec + wgmma + TMA,用 CUTLASS 或研究 DSLTODO(on GPU)
B200 (sm_100)TCGEN05 + TMEM;选择 CUTLASS 4.6.1 C++ 或 Python CuTe DSLTODO(on GPU)

关键洞察:从 Hopper 开始,"手写 kernel 接近 cuBLAS"已经几乎不可能(cuBLAS 也是 CUTLASS 编译出来的)。生产决策:

常见坑

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

Bank 冲突诊断、memcpy_async、TMA、DSMEM、Pipeline

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

用 Nsight 量化 bank conflict:硬件 32-bank 模型与对照实验

讲完 padding 与 swizzle,怎么知道自己写的版本到底有没有冲突?必须用 Nsight Compute 的硬件计数器。先复习硬件模型:

架构Bank 数Bank 宽度同 lane 同 word同 bank 不同 word
Volta / Turing / Ampere / Hopper324 字节(默认)广播,0 代价n-way 冲突,串行 n 次
同上,启用 8-byte 模式328 字节(cudaSharedMemBankSizeEightByte广播fp64 / int2 时只用一半 bank → 冲突减半

关键计数器(Nsight Compute UI 里搜索 bank_conflicts 即可):

ncu --metrics \
      l1tex__data_bank_conflicts_pipe_lsu_mem_shared_op_ld.sum,\
      l1tex__data_bank_conflicts_pipe_lsu_mem_shared_op_st.sum,\
      l1tex__data_pipe_lsu_wavefronts_mem_shared_op_ld.sum,\
      l1tex__data_pipe_lsu_wavefronts_mem_shared_op_st.sum \
      ./my_app

每条 shared load/store 指令理想是 1 wavefront(一周期完成)。如果 conflict 数非零,wavefront 数会等比例放大。判定公式:

有效冲突 = bank_conflicts.sum / wavefronts.sum
               ↑ 0  → 完美
               ↑ 31 → 32-way 冲突(最坏情况)

在 fp64 / int2 这种 8 字节访问场景,Volta+ 还允许显式切到 8-byte bank 模式,把"两个 32-bit lane 共享同一 bank"的情况合并:

// 默认 4-byte bank:fp64 数组按列访问 → 16-way conflict
    __shared__ double tile_d[16][16];

    // 切到 8-byte bank:相邻 lane 用相邻 8-byte bank,冲突减半
    cudaDeviceSetSharedMemConfig(cudaSharedMemBankSizeEightByte);
方法论:写完一个 tile kernel 一定要跑 ncu --section MemoryWorkloadAnalysis, 看 Shared Bank Conflicts 行。任何非零都是 padding / swizzle 没做对。生产代码会把该指标加入 CI 性能门禁。

cuda::pipeline 写多阶段软流水(标准 C++ API)

6.5.2 给的 __pipeline_memcpy_async 是 C 风格底层原语,CUDA 12+ 推荐改用 <cuda/pipeline> 头里的 cuda::pipeline —— 它是 libcu++ 提供的 C++ 抽象, 可以在 block / warp / thread 任意 scope 上工作,配合 cuda::memcpy_async 自动管理"几阶段流水"。

核心数据结构:

#include <cuda/pipeline>
    #include <cooperative_groups.h>
    namespace cg = cooperative_groups;

    constexpr int N_STAGES = 4;            // 4 阶段:当前算第 0 片时,1/2/3 在路上
    constexpr int BM = 128, BN = 128, BK = 32;

    __global__ void matmul_pipeline(const float* A, const float* B, float* C,
                                    int M, int N, int K) {
        auto block = cg::this_thread_block();

        __shared__ float As[N_STAGES][BM][BK];
        __shared__ float Bs[N_STAGES][BK][BN];

        // 多阶段共享状态:每阶段占一组 barrier
        __shared__ cuda::pipeline_shared_state<cuda::thread_scope_block, N_STAGES> pstate;
        auto pipe = cuda::make_pipeline(block, &pstate);

        int n_iters = K / BK;

        // 1) 预热:先 prefetch 前 N_STAGES-1 片
        for (int s = 0; s < N_STAGES - 1 && s < n_iters; ++s) {
            pipe.producer_acquire();
            cuda::memcpy_async(block, &As[s][0][0], &A[/* tile (blockIdx.y, s) */],
                               cuda::aligned_size_t<16>(BM * BK * 4), pipe);
            cuda::memcpy_async(block, &Bs[s][0][0], &B[/* tile (s, blockIdx.x) */],
                               cuda::aligned_size_t<16>(BK * BN * 4), pipe);
            pipe.producer_commit();
        }

        float acc[8][8] = {0};

        // 2) 稳态:每轮"提交下一片 + 算当前片"完全重叠
        for (int it = 0; it < n_iters; ++it) {
            int slot_cur  = it % N_STAGES;
            int slot_next = (it + N_STAGES - 1) % N_STAGES;

            // 提交未来的一片(it + N-1)
            int future = it + N_STAGES - 1;
            if (future < n_iters) {
                pipe.producer_acquire();
                cuda::memcpy_async(block, &As[slot_next][0][0], &A[/* (by, future) */],
                                   cuda::aligned_size_t<16>(BM * BK * 4), pipe);
                cuda::memcpy_async(block, &Bs[slot_next][0][0], &B[/* (future, bx) */],
                                   cuda::aligned_size_t<16>(BK * BN * 4), pipe);
                pipe.producer_commit();
            }

            // 等当前片到位
            pipe.consumer_wait();
            block.sync();
            // ... 用 As[slot_cur] / Bs[slot_cur] 做内层 mma ...
            pipe.consumer_release();
        }
    }

对比 __pipeline_memcpy_async

特性__pipeline_memcpy_asynccuda::pipeline
cuda_pipeline.hcuda/pipeline
scopethread 隐含thread / block / cluster 可选
对齐声明必须手算cuda::aligned_size_t<N> 编译期可证
partitioned (生产者/消费者分组)不支持支持 pipeline_role::{producer, consumer}
取消pipeline::quit() 显式 early-exit
常见 Wait 陷阱cuda::memcpy_async 只有当源是 global、目标是 shared、且双方至少 4-byte 对齐时才真正异步; 不满足时 nvcc 静默回退到同步 memcpy,性能完全消失。最佳对齐是 16 byte——用 cuda::aligned_size_t<16> 让编译器在编译期检查。

动态 shared 与 cudaFuncSetAttribute 进阶:占用率 vs 容量的权衡

6.5.3 教你怎么把 shared memory 申请超 48KB;进一步要知道SM 内 L1/Shared 的统一物理 SRAM是分配出来的,多吃 shared 等于少吃 L1。cudaFuncSetAttribute 提供两个相关属性:

Attribute用途取值
cudaFuncAttributeMaxDynamicSharedMemorySize声明本 kernel 想申请的动态 shared 上限(字节)sharedMemPerBlockOptin(A100=164KB, H100=228KB)
cudaFuncAttributePreferredSharedMemoryCarveout指定 L1/Shared 切分偏好0–100(百分比 shared)或 cudaSharedmemCarveoutMaxShared / ...MaxL1 / ...Default
// 1) 解锁 100 KB 动态 shared(必须在第一次 launch 之前调)
    size_t shm_bytes = 100 * 1024;
    CUDA_CHECK(cudaFuncSetAttribute(
        matmul_pipeline,
        cudaFuncAttributeMaxDynamicSharedMemorySize,
        shm_bytes));

    // 2) 告诉 driver: 我希望尽量多 shared 而不是 L1
    CUDA_CHECK(cudaFuncSetAttribute(
        matmul_pipeline,
        cudaFuncAttributePreferredSharedMemoryCarveout,
        cudaSharedmemCarveoutMaxShared));

    // 3) launch 时第三个参数 = shm_bytes
    matmul_pipeline<<<grid, block, shm_bytes, stream>>>(...);

kernel 内部用 extern __shared__ 拿一段,自己切:

extern __shared__ __align__(16) unsigned char dyn_shm[];

    float* As = reinterpret_cast<float*>(dyn_shm);
    float* Bs = reinterpret_cast<float*>(dyn_shm + sizeof(float) * BM * BK * N_STAGES);
    // __align__(16) 保证 float4 vec-load 可用

真实数字(来自 CUTLASS profiler):

GPUSM L1+Shared 总量Max Dynamic Shared典型 GEMM 配置
V100 (sm_70)128 KB96 KBBM=128 BN=128 + 2-stage = 64 KB
A100 (sm_80)192 KB164 KBBM=128 BN=128 + 4-stage = 96 KB
H100 (sm_90)256 KB228 KBBM=256 BN=128 + 4-stage = 196 KB
常见错误:忘了 carveout
  • 申请 100 KB 但没设 ...MaxDynamicSharedMemorySize → 返回 cudaErrorInvalidValue,但 launch 时才报,新手以为是 kernel bug
  • 申请 ≥ 64 KB 但仍走默认 carveout → 实际 shared 不够,被 driver 退到下一档(reserved bytes 算法吃掉一部分),并不报错,占用率掉一半
  • 多 kernel 共用一个 .cu 文件,对其中一个 set attribute → 只影响这一个 kernel。其他 kernel 的属性不受影响(这是好事,但容易误以为是全局)

Hopper TMA 完整用法:从 Host 端 cuTensorMapEncodeTiled 到 Kernel 内 cp.async.bulk.tensor

6.5.8 把 TMA 一笔带过。这里给出"从 host 到 device 的完整最小可运行例子",让你明白这条指令背后有多少 boilerplate——这也是为什么 CUTLASS / ThunderKittens 要做封装。

第 1 步:在 Host 端构造一个 CUtensorMap(描述符)

#include <cuda.h>        // 必须用 driver API
    #include <cudaTypedefs.h>

    // global 矩阵:M × K, row-major, fp16
    constexpr int M = 4096, K = 4096;
    constexpr int BM = 128, BK = 32;          // tile / "box"

    CUtensorMap tmap_A{};
    uint64_t globalDim[2]    = { (uint64_t)K, (uint64_t)M };       // 列在前
    uint64_t globalStride[1] = { K * sizeof(__half) };             // 行步
    uint32_t boxDim[2]       = { (uint32_t)BK, (uint32_t)BM };
    uint32_t elemStride[2]   = { 1, 1 };

    CUresult res = cuTensorMapEncodeTiled(
        &tmap_A,                                     // out
        CU_TENSOR_MAP_DATA_TYPE_FLOAT16,
        /*tensorRank=*/2,
        A_global_ptr,                                // 实际 global 指针
        globalDim, globalStride, boxDim, elemStride,
        CU_TENSOR_MAP_INTERLEAVE_NONE,
        CU_TENSOR_MAP_SWIZZLE_128B,                  // ← 128B swizzle (默认 fp16 GEMM 用)
        CU_TENSOR_MAP_L2_PROMOTION_L2_128B,
        CU_TENSOR_MAP_FLOAT_OOB_FILL_NONE);
    assert(res == CUDA_SUCCESS);

第 2 步:把 CUtensorMap 作为 __grid_constant__ 传给 kernel

__global__ void gemm_tma(const __grid_constant__ CUtensorMap tmap_A,
                             const __grid_constant__ CUtensorMap tmap_B,
                             __half* C, int M, int N, int K) {
        // 1) 准备 mbarrier
        __shared__ alignas(8) uint64_t bar;
        __shared__ alignas(128) __half As[BM][BK];   // 128B 对齐, 给 swizzle 用
        __shared__ alignas(128) __half Bs[BK][BN];

        if (threadIdx.x == 0) {
            cuda::ptx::mbarrier_init(&bar, 1);   // 期待 1 次 arrive (TMA 完成)
        }
        __syncthreads();

        // 2) 一条指令发起 BM×BK 的 fp16 tile 拷贝
        int32_t coords_A[2] = { 0 /*k0*/, blockIdx.y * BM /*m0*/ };
        if (threadIdx.x == 0) {
            cuda::ptx::cp_async_bulk_tensor(
                cuda::ptx::space_shared,
                cuda::ptx::space_global,
                &As[0][0],
                &tmap_A,
                coords_A,
                cuda::device::barrier_native_handle(bar));
            // 告诉 barrier "等 BM*BK*sizeof(half) 字节"
            cuda::ptx::mbarrier_arrive_expect_tx(
                cuda::ptx::sem_release,
                cuda::ptx::scope_cta,
                cuda::ptx::space_shared,
                &bar, sizeof(As));
        }

        // 3) 等 TMA 完成 (try_wait_parity 是非阻塞 spin)
        int parity = 0;
        while (!cuda::ptx::mbarrier_try_wait_parity(&bar, parity)) {}

        // 4) 此时 As 里已经有数据 + 自带 128B swizzle 排布, 可以直接 wgmma
        // wgmma_async(acc, As, Bs);
    }

关键收益(vs cp.async)

指标cp.async (Ampere)TMA (Hopper)
发起指令数每 thread 几十条每 tile 1 条
edge / OOB 处理kernel 内手算tensor map 里声明,硬件自动 fill
swizzlekernel 写 swizzle 函数tensor map 里选 32B/64B/128B
同步__pipeline_wait_priormbarrier (硬件 barrier)
kernel 寄存器占用load 地址计算占 ~10 regs~0(TMA 引擎独立做)
三个真实约束: ① TMA 只在 sm_90+ 可用;② global 必须 128 字节对齐;③ shared 必须按 swizzle pattern 对齐(128B swizzle → 1024 字节对齐,写 __shared__ alignas(1024) ...)。任一不满足 launch 时返回错误。
TMA 不能直接读到 register:目的地必须是 shared 或 distributed shared (cluster)。 要送进 register 需要再发一条普通 ld.shared——但这是免费的因为已经 swizzle 好了。

Cluster + DSMEM:跨 block 共享 shared memory(Hopper)

Hopper 引入 Thread Block Cluster(CGA):最多 8 个 block 组成一个簇,簇内 block 之间的 shared memory 可以互相读写,称为 Distributed Shared Memory(DSMEM)。可用容量受 cluster size、每 block 动态 shared 与 occupancy 限制,应查询属性并验证 launch 配置。

用法核心三步:

  1. 在 kernel 上加 __cluster_dims__(N, 1, 1) 声明簇形状,或 launch 时通过 cudaLaunchAttributeClusterDimension
  2. cg::this_cluster() 拿到 cluster handle
  3. cluster.map_shared_rank(local_ptr, peer_rank) 把本 block 的 shared 指针映射成"peer block 上同样位置的 shared"
#include <cuda/barrier>
    #include <cooperative_groups.h>
    namespace cg = cooperative_groups;

    // 簇 = 4 个 block。每个 block 算 C 的 BM × BN,但 K 维 load 摊到 4 个 block
    __global__ __cluster_dims__(4, 1, 1)
    void cluster_gemm(const __half* A, const __half* B, __half* C,
                      int M, int N, int K) {
        using barrier_t = cuda::barrier<cuda::thread_scope_block>;

        auto cluster = cg::this_cluster();
        auto block   = cg::this_thread_block();
        int  rank    = cluster.block_rank();          // 0..3
        int  csize   = cluster.num_blocks();          // 4

        __shared__ alignas(128) __half As[BM][BK];
        __shared__ barrier_t bar;
        if (block.thread_rank() == 0) init(&bar, csize);  // 期 csize 次 arrive
        cluster.sync();   // 等所有 block 的 barrier init 完

        // 每 block 各自负责 K 维的 1/4
        int k0 = rank * (K / csize);
        int k1 = (rank + 1) * (K / csize);

        for (int kt = k0; kt < k1; kt += BK) {
            // 1) 本地 load 一片
            // (vector load 略)

            // 2) 把自己 load 完的 As "发给"右邻居 block
            int peer = (rank + 1) % csize;
            __half* As_peer = cluster.map_shared_rank(&As[0][0], peer);
            // 这里是真·跨 block 的 shared 写!硬件走 DSMEM 链路(NVLink 内部)
            for (int i = block.thread_rank(); i < BM*BK; i += block.size()) {
                As_peer[i] = As[i / BK][i % BK];
            }

            // 3) cluster-wide barrier 等大家都写完
            cluster.sync();

            // 现在 As 里有当前 block 的本地一片 + 来自左邻居的一片 → 可以做更大 K 的 mma
        }
    }
    graph TB
        subgraph "Hopper SM Cluster (4 blocks)"
          direction LR
          B0["Block 0
shared: As[0..]"] -.->|map_shared_rank| B1["Block 1
shared: As[0..]"] B1 -.->|map_shared_rank| B2["Block 2
shared: As[0..]"] B2 -.->|map_shared_rank| B3["Block 3
shared: As[0..]"] B3 -.->|map_shared_rank| B0 end note["cluster.sync() 同步整簇"] style B0 fill:#f3f1e8,stroke:#2f5d3a style B1 fill:#f3f1e8,stroke:#2f5d3a style B2 fill:#f3f1e8,stroke:#2f5d3a style B3 fill:#f3f1e8,stroke:#2f5d3a
层级容量上限 (H100)同步 API典型用途
warp__syncwarp() / shflwarp reduce
block228 KB shared__syncthreads()tile matmul / softmax
cluster (DSMEM)~3.6 MB across 16 blockscluster.sync()超大 tile / 跨 block KV cache 共享
gridHBM 全部this_grid().sync() + cooperative launch全 reduce / 全 sort
cluster 不是 silver bullet
  • cluster 内 block 必须同 SM 组(GPC,~16 SM)—— H100 一个 GPC 16 SM 刚好容下 16 block cluster
  • 实际可用 cluster size 受 cudaOccupancyMaxPotentialClusterSize 限制,超过会 launch failure
  • DSMEM 需要跨 SM 通信,是否优于本地 shared/global 路径取决于复用、tile 与 occupancy,必须用 profiler 验证

下一章导览

第 7 章把相同的 tile、warp 通信和分层合并思路用于 reduction、scan 与 histogram。