Navigation
πŸ”§ CUDA & Kernel EngineeringΒ· 12 min read

Tiling Strategies

Breaking large problems into shared-memory-sized tiles β€” the fundamental technique behind fast GEMM, attention, and every high-performance GPU kernel.

cudatilingshared-memorygemmdata-reusedouble-buffering

Tiling is the single most important optimization pattern in GPU programming. Every fast kernel β€” GEMM, FlashAttention, reduction, convolution β€” uses tiling to move data from slow HBM into fast shared memory and maximize data reuse. This chapter covers the principles, patterns, and tradeoffs.


1. The Core Idea

Without tiling: every operation reads from HBM (~600 cycle latency). With tiling: load a chunk into shared memory (~30 cycle latency), reuse it many times, then load the next chunk.

Without tiling:                    With tiling:
HBM β†’ Compute β†’ HBM               HBM β†’ Shared Mem β†’ Compute (reuse N times)
HBM β†’ Compute β†’ HBM                                β†’ Compute (reuse)
HBM β†’ Compute β†’ HBM                                β†’ Compute (reuse)
...                                HBM β†’ Shared Mem β†’ Compute (reuse N times)
                                   ...

The math: If each data element is used RR times, tiling reduces HBM reads by RΓ—R\times. For GEMM with tile size TT: R=TR = T, so a 32Γ—32 tile gives 32Γ— fewer HBM reads.


2. Tiling for Matrix Multiplication

The Problem

Naive GEMM: each output element CijC_{ij} reads one row of AA and one column of BB from HBM:

// Naive: O(M*N*K) HBM reads
C[i][j] = 0;
for (int k = 0; k < K; k++)
    C[i][j] += A[i][k] * B[k][j];  // each A[i][k] and B[k][j] from HBM

For M=N=K=4096M=N=K=4096: each element reads 4096 floats from A and 4096 from B. Total: 40962Γ—2Γ—4096=1374096^2 \times 2 \times 4096 = 137 billion bytes from HBM. At 3.35 TB/s, that’s 41 ms β€” far slower than the 3.4 ms achievable with tiling.

The Tiled Solution

Tiled GEMM

  1. Divide output matrix CC into tiles of size TMΓ—TNT_M \times T_N
  2. Each thread block computes one output tile
  3. Loop over K in steps of TKT_K: a. Cooperatively load a TMΓ—TKT_M \times T_K tile of AA into shared memory b. Cooperatively load a TKΓ—TNT_K \times T_N tile of BB into shared memory c. __syncthreads() β€” wait for all loads d. Compute partial dot products using shared memory data e. __syncthreads() β€” wait before loading next tile
__shared__ float As[TILE_M][TILE_K];
__shared__ float Bs[TILE_K][TILE_N];

float acc = 0.0f;
for (int t = 0; t < K; t += TILE_K) {
    // Cooperative load: each thread loads one or more elements
    As[threadIdx.y][threadIdx.x] = A[row][t + threadIdx.x];
    Bs[threadIdx.y][threadIdx.x] = B[t + threadIdx.y][col];
    __syncthreads();

    // Compute: all data in fast shared memory
    for (int k = 0; k < TILE_K; k++) {
        acc += As[threadIdx.y][k] * Bs[k][threadIdx.x];
    }
    __syncthreads();
}
C[row][col] = acc;

Data Reuse Analysis

With tile size TT:

  • Each element of AAβ€˜s tile is used by TNT_N threads (all threads in the same row of the output tile)
  • Each element of BBβ€˜s tile is used by TMT_M threads
  • Reuse factor: TT reads from shared memory instead of HBM

For T=32T=32: HBM reads reduce by 32Γ—. For T=128T=128: 128Γ— reduction.


3. Double Buffering: Overlapping Load and Compute

Naive tiling has a stall: after loading a tile and computing, all threads wait for the next load. Double buffering overlaps loading the next tile with computing the current tile:

__shared__ float As[2][TILE_M][TILE_K];  // two buffers
__shared__ float Bs[2][TILE_K][TILE_N];
int curr = 0;

// Load first tile
load_tile(As[curr], Bs[curr], t=0);
__syncthreads();

for (int t = TILE_K; t < K; t += TILE_K) {
    int next = 1 - curr;

    // Start loading NEXT tile while computing CURRENT tile
    load_tile_async(As[next], Bs[next], t);  // non-blocking

    // Compute with current tile (in shared memory)
    compute(As[curr], Bs[curr], acc);

    __syncthreads();  // wait for both load and compute
    curr = next;
}
// Process last tile
compute(As[curr], Bs[curr], acc);

Benefit: Load latency (~600 cycles) is hidden behind compute. For compute-heavy tiles, this gives near-zero load overhead.


4. Tile Size Tradeoffs

Larger tilesSmaller tiles
βœ… More data reuse β†’ fewer HBM readsβœ… Less shared memory β†’ higher occupancy
βœ… Better arithmetic intensityβœ… Less register pressure
❌ More shared memory β†’ fewer blocks per SM❌ More HBM reads
❌ More registers for accumulators❌ Lower arithmetic intensity
❌ Tail effects if matrix not divisibleβœ… Better for small/irregular matrices

Typical Tile Sizes in Practice

KernelTile sizeWhy
Simple GEMM32Γ—32Fits in shared memory, good starting point
Production GEMM (cuBLAS)128Γ—128 or 256Γ—128Maximizes reuse, uses register tiling
FlashAttention64-128 per blockBalances reuse with SRAM capacity
ReductionBlock size (256-1024)One tile per block

Register Tiling

Production kernels like cuBLAS use a two-level tiling strategy:

  1. Thread block tile: loaded into shared memory (128Γ—128)
  2. Per-thread tile: each thread accumulates a small sub-tile in registers (8Γ—8)

This maximizes reuse at both levels β€” shared memory reuse across threads, register reuse within a thread.


5. Tiling Beyond GEMM

The tiling pattern applies everywhere:

KernelWhat’s tiledWhat’s reused
GEMMA and B matrices along K dimensionEach tile element used by many output elements
FlashAttentionQ, K, V blocks along sequenceK/V blocks reused across Q rows
ConvolutionInput patches and filter weightsFilter weights reused across spatial positions
ReductionInput array chunksPartial sums accumulated in shared memory
SoftmaxInput chunks for max/sumTwo-pass (max, then normalize) on same data

The universal principle: If data is read from HBM more than once, it should be tiled into shared memory.


Self-Check

  • Explain why tiling reduces HBM reads and by how much (reuse factor)
  • Implement tiled matrix multiplication with shared memory
  • Explain double buffering and how it hides load latency
  • Analyze the tile size tradeoff (reuse vs occupancy)
  • Explain two-level tiling (shared memory + register)
  • Identify tiling opportunities in a given kernel

References