DAY 05 · AI INFRA ROADMAP · 60 DAYS

Tiling 喂饱 GPU

Day 04 的 naive GEMM 比 cuBLAS 慢 50×—— 今天解决这个问题的第一步:用 Shared Memory Tiling 把数据复用率提上去。 理解 coalesced access 和 bank conflict, 然后用 Nsight Compute 量化你的优化效果。

DURATION 3–3.5 h THEORY 1.5 h HANDS-ON 1.5 h REVIEW 0.5 h STACK CUDA C · Nsight Compute · cuBLAS
M ·

思维导图

OVERVIEW
DAY 05 · CUDA 编程入门 (2)TILING · BANK CONFLICT · COALESCED · NCU01 · TILINGShared Memory Tiling02 · ACCESS访存模式03 · PROFILINGNsight Compute04 · PRACTICETiled GEMM 实战·Global → Shared → Register·__shared__ 声明 tile·__syncthreads() 同步·数据复用率分析·Coalesced Access·Bank Conflict·32 banks × 4 bytes·Padding 消除冲突·Occupancy 分析·Memory Throughput·Compute Throughput·Roofline Model·tiled_gemm.cu·naive vs tiled 对比·ncu 报告分析·cuBLAS 基线对比DELIVERABLEStiled_gemm.cu 通过验证naive vs tiled GFLOPS 对比Nsight Compute 分析报告Bank Conflict 笔记
FIG · Day 05 全景:Shared Memory Tiling → 访存优化 → Nsight Compute 分析
01 ·

Shared Memory Tiling

30 MIN

Naive GEMM 的致命问题:每个 thread 计算 C 的一个元素时, 要从 Global Memory 读 K 次 A 和 K 次 B—— 而相邻 thread 读的数据大量重叠。 Tiling 的核心思想:把数据分块搬进 Shared Memory,让整个 Block 的 thread 复用同一份数据。

TILED GEMM — SHARED MEMORY DATA REUSEA [M × K]tile_A→ 沿 K 方向滑动 →B [K × N]tile_B↓ 沿 K 方向滑动SHARED MEMORY (per Block)As[T×T]Bs[T×T]← tile_A← tile_BT = TILE_SIZE (通常 16 或 32)TILING LOOP (K/T iterations)1. 从 Global 加载 tile → SMEM2. __syncthreads()3. 从 SMEM 计算部分和4. __syncthreads() → 下一个 tile
FIG · Tiled GEMM:将 A 和 B 的 tile 加载到 Shared Memory,Block 内所有 thread 复用数据
KEY INSIGHT

数据复用率

Naive GEMM 每个 thread 读 2K 次 Global Memory。Tiled GEMM 每个 thread 只读 2K/T 次 Global Memory(T = tile size),剩下的从 Shared Memory 读。Global 访存量降为 1/T,T=16 时即 1/16。

SYNC BARRIER

__syncthreads()

所有 thread 必须在两个点同步:(1) tile 加载完毕后、计算前;(2) 计算完毕后、加载下一个 tile 前。忘加同步 = 数据竞争 = 随机错误结果,这是最常见的 tiling bug。

关键选择: TILE_SIZE 通常取 16 或 32。取 32 时 tile 大小为 32×32×4B = 4KB,两个 tile 共 8KB,远小于 SMEM 容量。
02 ·

Coalesced Access 与 Bank Conflict

25 MIN

Tiling 解决了数据复用问题,但还有两个关键的访存陷阱: Global Memory 的 coalesced access 和 Shared Memory 的 bank conflict。 不注意这两点,tiling 的加速效果会大打折扣。

问题发生在成因后果解决方案
Non-coalesced AccessGlobal MemoryWarp 内 32 个 thread 访问不连续地址多次 128B 内存事务,带宽浪费保证连续 thread 访问连续地址
Bank ConflictShared Memory同一 Warp 的多个 thread 访问同一 bank串行化,最坏 32× 延迟Padding: __shared__ float As[T][T+1]
COALESCED ACCESS

Global Memory 合并访问

GPU 的 Global Memory 以 128 字节为粒度读取。Warp 内 32 个 thread 若访问连续 32×4B = 128B 的地址,硬件合并为一次事务。若 thread 0 访问 A[0]、thread 1 访问 A[100],则需 32 次事务——带宽利用率仅 1/32。

BANK CONFLICT

Shared Memory Bank 冲突

Shared Memory 被划分为 32 个 bank,每个 bank 宽 4 字节。地址 addr 对应 bank = (addr/4) % 32。同一 Warp 里有两个 thread 访问同一 bank 的不同地址时产生冲突,访问被串行化。广播(同一地址)不算冲突。

PADDING TRICK

Padding 消除 Bank Conflict

最简单的方法:声明 __shared__ float As[T][T+1],多加一列。这样每行起始地址偏移一个 bank,列方向访问时 thread 不再落在同一 bank。代价是浪费少量 SMEM,但几乎总是值得的。

RULE OF THUMB

访存优化三原则

① Global Memory 读写保证 coalesced。② Shared Memory 声明加 padding。③ 先用 Nsight Compute 测,不要凭感觉优化——很多"明显"的优化方向实际上不是瓶颈。

03 ·

Nsight Compute 性能分析

20 MIN

Nsight Compute(ncu)是 NVIDIA 的 kernel-level profiler, 能精确量化 occupancy、memory throughput、compute throughput, 告诉你 kernel 是 compute-bound 还是 memory-bound。

指标含义关注点
Achieved OccupancySM 上实际活跃 Warp 数 / 最大可容纳 Warp 数低 occupancy = 延迟无法隐藏
Memory Throughput实际内存吞吐 / 理论峰值带宽的百分比接近峰值 → memory-bound
Compute Throughput实际计算吞吐 / 理论峰值算力的百分比接近峰值 → compute-bound
L1/L2 Hit Rate缓存命中率tiling 后 L1 命中率应显著提升
Shared Memory Bank Conflictsbank 冲突导致的额外事务数>0 需要检查 padding

常用命令

# 基础 profiling — 收集所有指标
ncu --set full -o report ./tiled_gemm

# 只看 memory 相关指标
ncu --metrics l1tex__t_bytes_pipe_lsu_mem_global_op_ld.sum,\
l1tex__t_sectors_pipe_lsu_mem_global_op_ld.sum,\
smsp__sass_average_data_bytes_per_sector_mem_global_op_ld.pct \
./tiled_gemm

# 对比两个 kernel
ncu --set full -o naive  ./naive_gemm
ncu --set full -o tiled  ./tiled_gemm
# 用 Nsight Compute GUI 打开 .ncu-rep 文件对比

# 看 occupancy 限制因素
ncu --metrics sm__warps_active.avg.pct_of_peak_sustained_active \
./tiled_gemm
ROOFLINE

Roofline Model

Nsight Compute 内置 Roofline 视图。你的 kernel 落在屋顶线的左侧(memory-bound)还是右侧(compute-bound),决定了下一步优化方向。GEMM 在优化前通常是 memory-bound,tiling 的目标就是把它往右推。

TIP

Occupancy 不是越高越好

高 occupancy 有助于延迟隐藏,但有时降低 occupancy(用更多寄存器或 SMEM per thread)反而能减少 Global Memory 访问,总性能更好。目标是 throughput,不是 occupancy。

04 ·

动手实践 — Tiled GEMM

1.5 H

Lab — tiled_gemm.cu

在 Day 04 的 naive GEMM 基础上,加入 Shared Memory tiling。 每个 Block 负责 C 的一个 TILE_SIZE×TILE_SIZE 子矩阵, 通过循环加载 A 和 B 的 tile 到 SMEM,计算部分积,最终累加写回。

// tiled_gemm.cu — Shared Memory Tiled Matrix Multiply
#define TILE_SIZE 16

__global__ void tiled_gemm(float *A, float *B, float *C,
                            int M, int N, int K) {
    // Block 内的线程坐标
    int tx = threadIdx.x, ty = threadIdx.y;
    // 全局输出坐标
    int row = blockIdx.y * TILE_SIZE + ty;
    int col = blockIdx.x * TILE_SIZE + tx;

    // Shared Memory (padding 消除 bank conflict)
    __shared__ float As[TILE_SIZE][TILE_SIZE + 1];
    __shared__ float Bs[TILE_SIZE][TILE_SIZE + 1];

    float sum = 0.0f;

    // 沿 K 维度分块循环
    for (int t = 0; t < (K + TILE_SIZE - 1) / TILE_SIZE; t++) {
        // 1. 协作加载 tile 到 SMEM (coalesced!)
        int aCol = t * TILE_SIZE + tx;
        int bRow = t * TILE_SIZE + ty;
        As[ty][tx] = (row < M && aCol < K) ? A[row * K + aCol] : 0.0f;
        Bs[ty][tx] = (bRow < K && col < N) ? B[bRow * N + col] : 0.0f;

        // 2. 同步:确保 tile 加载完毕
        __syncthreads();

        // 3. 从 SMEM 计算部分积
        for (int k = 0; k < TILE_SIZE; k++) {
            sum += As[ty][k] * Bs[k][tx];
        }

        // 4. 同步:确保计算完毕再加载下一个 tile
        __syncthreads();
    }

    // 写回结果
    if (row < M && col < N) {
        C[row * N + col] = sum;
    }
}

// 启动配置
dim3 block(TILE_SIZE, TILE_SIZE);     // 16×16 = 256 threads
dim3 grid((N + TILE_SIZE - 1) / TILE_SIZE,
          (M + TILE_SIZE - 1) / TILE_SIZE);
tiled_gemm<<<grid, block>>>(d_A, d_B, d_C, M, N, K);
编译运行: nvcc -O2 -o tiled_gemm tiled_gemm.cu && ./tiled_gemm

性能对比实验

用 1024×1024 矩阵,分别跑 naive GEMM、tiled GEMM、cuBLAS,记录 GFLOPS。

实现典型 GFLOPS (A100)相对 cuBLAS主要瓶颈
Naive GEMM~300–500~2–3%零数据复用,memory-bound
Tiled GEMM (T=16)~2000–4000~15–25%无 register blocking,低 ILP
cuBLAS (sgemm)~15000–19000100%接近 compute-bound
BENCHMARK

GFLOPS 计算公式

GEMM 的浮点操作数 = 2 × M × N × K(每个元素一次乘法一次加法)。GFLOPS = 2·M·N·K / (time_ms × 10⁶)。对比三个版本的 GFLOPS,量化 tiling 带来的加速倍数。

NEXT OPTIMIZATION

还差什么?

Tiled GEMM 比 naive 快 5–8×,但仍比 cuBLAS 慢 5–10×。后续优化方向:register blocking(每 thread 算多个元素)、double buffering(load 和 compute 重叠)、向量化 load(float4)、warp-level MMA。

用 ncu 对比 naive 和 tiled 的 Memory Throughput:tiling 后 Global Memory 读取量应降为约 1/T。
Q ·

常见疑问

5 QUESTIONS
Q1 为什么需要两次 __syncthreads()? +
ANS

第一次同步发生在 tile 加载后、计算前:确保 Block 内所有 thread 都完成了 SMEM 写入,否则其他 thread 可能读到未初始化的值。第二次同步发生在计算后、加载下一个 tile 前:确保所有 thread 都用完了当前 tile 的数据,否则某些 thread 可能提前覆盖 SMEM。少一次 sync 都会导致间歇性计算错误,这类 bug 极难调试。

Q2 TILE_SIZE 取多大最好? +
ANS

16 或 32 是最常见的选择。T=16 时每个 Block 256 threads,两个 tile 共用 16×17×4×2 ≈ 2.2KB SMEM。T=32 时 1024 threads,SMEM 用量约 8.5KB。更大的 T 意味着更好的数据复用,但 Block 内 thread 数增多,可能降低 occupancy。实测两个版本,用 ncu 看哪个 throughput 更高。

Q3 Padding 为什么能消除 bank conflict? +
ANS

Shared Memory 有 32 个 bank,每个 bank 4 字节宽。float As[16][16] 的第 0 列全部落在 bank 0(因为行宽 64B = 16×4B 刚好整除 128B = 32 banks)。当 Warp 内 thread 按列读取时,32 个 thread 全访问同一 bank → 32-way conflict。声明 As[16][17] 后,每行多出 4 字节,列方向的 thread 落在不同 bank,冲突消失。

Q4 Tiled GEMM 已经用了 Shared Memory,为什么还比 cuBLAS 慢那么多? +
ANS

cuBLAS 在 tiling 之上还做了大量优化:(1) Register blocking——每个 thread 算 C 的 4×4 或 8×8 子块,进一步减少 SMEM 读取;(2) Double buffering——用两份 SMEM 交替,加载下一个 tile 的同时计算当前 tile,hide latency;(3) 向量化 loadfloat4LDG.128)提高内存吞吐;(4) 使用 Tensor Core MMA 指令。这些是 GEMM 优化博客(Simon Boehm)后续步骤的核心内容。

Q5 如何确认我的 kernel 是 memory-bound 还是 compute-bound? +
ANS

在 Nsight Compute 中看 Roofline 图:kernel 的点落在斜线(memory-bound 区域)还是水平线(compute-bound 区域)。或者直接看两个百分比:Memory Throughput 接近 100% → memory-bound;Compute Throughput 接近 100% → compute-bound。Naive GEMM 几乎总是 memory-bound,tiled GEMM 开始往 compute-bound 靠拢,cuBLAS 则接近 compute-bound。

R ·

复盘问题

5 QUESTIONS
  1. 画出 Tiled GEMM 的数据流:Global Memory → Shared Memory → Register → 写回 Global Memory,标注 __syncthreads() 的位置。
  2. Naive GEMM (1024×1024) 的 Global Memory 读取量是多少字节?Tiled GEMM (T=16) 呢?计算降低比例。
  3. 解释为什么 __shared__ float As[16][16] 列方向读取有 bank conflict,而 As[16][17] 没有。
  4. 你的 tiled GEMM 在 ncu 中的 Achieved Occupancy 是多少?限制因素是什么(registers / shared memory / block size)?
  5. 对比你的 naive、tiled、cuBLAS 三个版本的 GFLOPS,tiling 带来了多少倍加速?距离 cuBLAS 还差多少?
✓ ·

今日检查清单

7 ITEMS
  • 能解释 Shared Memory Tiling 的原理:为什么能减少 Global Memory 访问
  • 能说清 coalesced access 的含义,以及 non-coalesced 时的性能后果
  • 能解释 bank conflict 的成因,以及 padding 为什么能消除它
  • tiled_gemm.cu 编译通过,结果与 naive GEMM / cuBLAS 一致
  • 记录 naive / tiled / cuBLAS 三版本 GFLOPS 对比数据
  • 用 Nsight Compute 跑过 tiled GEMM,能读懂 occupancy 和 memory throughput
  • 知道 kernel 是 memory-bound 还是 compute-bound,能用 Roofline 判断
📖 ·

推荐阅读

3 ITEMS
MUST READ

How to Optimize a CUDA Matmul Kernel

Simon Boehm 的系列博客,从 naive 到接近 cuBLAS 的逐步优化。今天对应 Kernel 2 (Shared Memory Tiling)。

OFFICIAL

CUDA C Programming Guide

Chapter 5: Performance Guidelines —— Memory Coalescing、Shared Memory、Occupancy。NVIDIA 关于性能优化的官方建议。

TEXTBOOK

PMPP Chapter 5–6

Programming Massively Parallel Processors 第 5 章(Tiled Matrix Multiplication)和第 6 章(Performance Considerations),经典教材讲解 tiling。

» ·

Day 06 预告

NEXT
COMING NEXT

Profiling 工具链

系统学习 GPU profiling 工具链:nsys(系统级 timeline)、ncu(kernel 级分析)、py-spy(Python 性能分析)、torch.profiler(PyTorch 训练分析)。建立"先测量再优化"的工程习惯。

"Shared Memory is the programmer's L1 cache — use it wisely, and the hardware will reward you."

DAY 05 · AI INFRA 60-DAY ROADMAP