Guide the agent through selecting tile sizes and work partitioning strategies for a CUDA or Triton kernel, based on shared memory budget, register pressure, occupancy targets, problem shape, and access pattern.
- Designing a new tiled kernel and the tile size and launch configuration have not yet been chosen.
- An existing kernel has suboptimal performance and the cause may be a poor tile size, low occupancy, or inefficient work partitioning.
- The problem shape has an irregular or non-power-of-2 size that makes default tile choices potentially wasteful.
- Writing a Triton kernel where
BLOCK_M,BLOCK_N,BLOCK_K,num_warps, andnum_stagesmust be chosen. - Profiling shows low SM utilization, high idle warp cycles, or memory throughput below roofline estimates.
- Using a library (cuBLAS, cuDNN, CUTLASS with auto-tuning) that handles tile size selection internally. Trust the library's tuner unless profiling shows a clear gap.
- The kernel is purely streaming (one pass, no reuse) and tiling provides no shared memory reuse benefit. In that case, work partitioning reduces to choosing a block size that achieves good occupancy and coalesced access, which is a simpler problem.
- The problem is so small (total work fits in one or two blocks) that tile size selection is irrelevant compared to launch overhead.
- Problem dimensions (e.g., M, N, K for GEMM; sequence length and head dimension for attention; reduction length for softmax).
- Element dtype: determines element size in bytes (fp16 = 2B, fp32 = 4B, bf16 = 2B, int8 = 1B).
- Target GPU architecture: shared memory capacity per SM (48 KB–228 KB depending on architecture and configuration), L2 size, number of SMs, warp size (always 32 for CUDA).
- Maximum shared memory per block (for sm_86: up to 100 KB with
cudaFuncSetAttribute(f, cudaFuncAttributeMaxDynamicSharedMemorySize, ...)). - Number of registers available per SM (65536 for most modern architectures), and target occupancy.
- Whether the problem shape is static or dynamic at kernel launch time.
- Whether the input access pattern is coalesced in the tile dimension being chosen.
- For Triton: whether autotuning will be used to sweep tile sizes, or if a fixed tile must be chosen.
-
Determine what is loaded into shared memory per block. For a GEMM-style kernel with tiles
BLOCK_M x BLOCK_Nand accumulation tileBLOCK_K:- smem per block =
(BLOCK_M * BLOCK_K + BLOCK_K * BLOCK_N) * element_size - For an elementwise kernel with no smem reuse, smem per block = 0 (tiles reside in registers only).
- For a softmax kernel (row-wise): smem per block =
BLOCK_N * element_size(one row of the softmax).
- smem per block =
-
Compute smem usage for candidate tile sizes. Start from the hardware smem limit and work downward. A common starting constraint:
- smem per block <= 48 KB (safe default, no
cudaFuncSetAttributeneeded) - smem per block <= 100 KB (requires explicit dynamic smem request on sm_80+)
- smem per block <= 228 KB (sm_90 Hopper maximum with smem partitioning)
- Reject tile sizes that exceed the smem budget.
- smem per block <= 48 KB (safe default, no
-
Estimate register usage per thread. A precise count requires compiling and inspecting with
--ptxas-info-registersorcuobjdump. As a first estimate:- Each live fp32 value requires one register. Each live fp16 value typically still uses one register.
- Tile accumulation in a GEMM with a
BLOCK_M x BLOCK_Noutput tile and 128-bit vectorized loads requires roughly(BLOCK_M * BLOCK_N) / 32registers per thread for the accumulator alone (assuming warp-level decomposition). - Total register pressure = accumulator + input tiles in registers + loop induction variables + addressing offsets. Budget ~4–8 registers for overhead per active load.
- Registers per SM = 65536. At 32 threads per warp and W warps per block: registers per thread budget = 65536 / (W * 32 * max_blocks_per_SM).
-
Compute occupancy for candidate configurations. Occupancy = (active warps per SM) / (max warps per SM). Constraints that limit occupancy:
- smem: max_blocks_per_SM = floor(smem_per_SM / smem_per_block). Warps = max_blocks_per_SM * warps_per_block.
- registers: max_blocks_per_SM = floor(regs_per_SM / (regs_per_thread * threads_per_block)). Then same warp calculation.
- hardware block limit: typically 32 blocks per SM maximum (sm_86), sometimes 16.
- Take the minimum across all constraints. This is the achieved occupancy.
- Use
cudaOccupancyMaxActiveBlocksPerMultiprocessor(CUDA) for accurate computation after writing the kernel.
-
Check warp efficiency in the last tile. If the problem dimension is not a multiple of the tile size, the last block has fewer valid elements than the tile size. The fraction of useful work in that block =
(N % TILE_SIZE) / TILE_SIZE. If this fraction is very small (e.g., < 0.1) and many blocks hit this case, the waste is significant. Pad to a tile multiple or choose a smaller tile size. -
Check alignment and divisibility for vectorized loads. Vectorized loads (float4, half2) require the pointer to be aligned to the vector width. For a tile of width W loaded with 128-bit (float4) loads, W must be a multiple of 4 (for float4). If W is not aligned, scalar fallback loads are needed, which reduces memory throughput.
-
Choose the work partitioning shape. Options:
- 1D grid: one block per output vector (row). Simple. Good for row-wise ops (softmax, layernorm). Poor if the number of rows is small relative to SM count.
- 2D grid: one block per output tile
(block_row, block_col). Standard for GEMM, attention. Grid dimensions areceil(M/BLOCK_M) x ceil(N/BLOCK_N). - 3D grid: adds a batch dimension. Standard for batched GEMM, multi-head attention. Grid =
ceil(M/BLOCK_M) x ceil(N/BLOCK_N) x batch. - Persistent kernel: one block per SM; each block loops over multiple tiles. Reduces launch overhead, improves load balance for irregular shapes. Required for reduction-then-broadcast patterns where a second kernel launch would be expensive.
- Streaming decomposition: for problems too large to hold all intermediate state, decompose along the K or sequence dimension and accumulate across kernel calls. Used in multi-pass attention or tiled reduction.
-
For Triton: choose num_warps and num_stages.
num_warps: number of warps per program instance. For compute-bound kernels, 4–8 warps is typical. For memory-bound kernels, 1–2 warps can be sufficient.num_stages: pipeline stages for the software prefetch loop. More stages hide memory latency but consume more smem for the prefetch buffers. For A100-class hardware, 3–5 stages is a common range. Verify smem budget: each additional stage adds one tile's worth of smem.- Use
triton.testing.Benchmarkto sweep configurations and confirm empirically.
- Tile sizes must be powers of 2 in each dimension when using shared memory and warp-level reductions, to avoid non-uniform warp partitioning and bank conflicts.
- The tile width (innermost dimension) must be a multiple of 32 (one warp) to avoid underutilizing warp lanes. A tile width of 16 means half the warp is idle at each step.
- For GEMM-style kernels,
BLOCK_M,BLOCK_N, andBLOCK_Kmust individually not exceed the smem budget divided by the number of tiles resident in smem simultaneously. - For Triton,
BLOCK_SIZEmust be atl.constexprto enable compile-time specialization. Do not pass it as a dynamic argument unless the kernel explicitly handles dynamic tile sizes. - Do not choose tile sizes that result in a grid with fewer blocks than the number of SMs on the target GPU — this leaves SMs idle for the entire kernel duration.
- For persistent kernels, distribute work to blocks in tile index order, not in arbitrary order, so that adjacent tiles share L2 cache lines when possible.
- When using Triton autotuning, define the autotune search space explicitly. Do not leave it as a fully open search — constrain to architecturally valid combinations (e.g., smem budget respected, num_warps divides threads evenly).
- The tile decomposition must cover all output elements exactly once. Verify that
ceil(N/TILE_SIZE) * TILE_SIZE >= N(no output position is skipped) and that no two tiles write to the same output position. - Partial tiles at the boundary must be masked correctly. Refer to the handle-boundary-conditions skill for the masking strategy.
- For reductions split across tiles or kernel calls, the accumulation across tiles must produce the same result as a single-pass reduction. Verify with small test cases where the split is at a tile boundary.
- Thread assignments within a block must not alias: two threads must not read from or write to the same shared memory location simultaneously without synchronization.
- Target at least 50% theoretical occupancy as a starting point. Below 25% occupancy, latency hiding is severely limited. Above 75% occupancy, returns on further increases are often marginal — prioritize smem and arithmetic throughput.
- For memory-bound kernels, the dominant metric is memory throughput, not occupancy. A low-occupancy kernel that achieves high memory bandwidth is preferable to a high-occupancy kernel with poor coalescing.
- For compute-bound kernels (GEMM), maximize the ratio of arithmetic to memory traffic. Large tiles improve reuse but increase register and smem pressure — find the crossover point empirically.
- Document the roofline position of the kernel: compute the arithmetic intensity (FLOPs / bytes) and compare to the hardware's compute-to-bandwidth ratio. This determines whether the bottleneck is memory or compute, and thus what tile size changes will help.
- State the expected occupancy for the chosen tile size configuration and the hardware-imposed constraint responsible for limiting it.
The agent should produce:
- Tile size analysis table: for each candidate tile size configuration, a table showing: smem per block, estimated registers per thread, occupancy estimate, warp efficiency on the last tile, alignment compatibility.
- Selected configuration with justification: the chosen tile size(s) and launch configuration, with explicit reasoning for why it was selected over alternatives.
- Work partitioning description: the grid decomposition (1D/2D/3D/persistent), grid dimensions as a function of problem size, and block/warp dimensions.
- Kernel implementation skeleton: the launch configuration and thread-to-data mapping code, even if the full kernel is not yet written.
- Autotuning plan (if applicable): the parameter space to sweep and the metric to optimize (throughput, latency).
- Tile width not a multiple of 32 (warp size): a tile width of 16, 24, or 48 leaves some warp lanes idle during each step, reducing warp efficiency. Always round tile widths up to a multiple of 32.
- smem exceeds hardware limit without explicit request: using more than 48 KB of shared memory per block on CUDA without calling
cudaFuncSetAttributecauses the kernel launch to fail silently (0 blocks launched) or error. - Grid smaller than SM count: launching fewer blocks than there are SMs on the GPU means some SMs are idle for the entire kernel. This is a hard floor on performance.
- Register pressure from large tile sizes: choosing BLOCK_M=128 x BLOCK_N=128 in a Triton GEMM requires a 128x128 register accumulator tile, which exceeds the register file on most hardware. The compiler will spill to local memory. Check the compiled kernel's register count.
- num_stages too large in Triton: each pipeline stage adds one tile's smem. With BLOCK_K=64 and fp16 (2B), each additional stage adds 64*2B = 128B per warp. For large BLOCK_M/BLOCK_N tiles, multiple stages can push over the smem budget.
- Persistent kernel with uneven work distribution: if the number of tiles is not evenly divisible by the number of blocks (SMs), some blocks will process one extra tile. This is acceptable but must be handled correctly — the last few blocks must not process past the tile count.
- Choosing tile size to avoid boundary handling rather than for performance: using BLOCK_SIZE=prime-number-close-to-N to make the problem fit in one tile is not a strategy; it produces different suboptimal behavior for different input shapes.
- Ignoring L2 reuse in tile ordering: for a 2D GEMM grid, iterating over (row, col) in row-major order reuses the A matrix tile across columns. Iterating in column-major order reuses the B matrix tile. Choosing the wrong iteration order reduces L2 hit rate.
- Shared memory per block is computed for the chosen tile size and verified to fit within the hardware limit (with or without explicit dynamic smem request as appropriate).
- Register pressure is estimated (or measured after compilation) for the chosen configuration.
- Occupancy is computed for the chosen configuration and the limiting resource is identified.
- The grid covers all output elements exactly once.
- The tile width is a multiple of 32 (warp size) in the innermost dimension.
- Partial boundary tiles are handled correctly (refer to handle-boundary-conditions skill).
- The grid size is at least equal to the number of SMs on the target GPU.
- For Triton:
num_warpsandnum_stagesare chosen with smem budget verified. - The arithmetic intensity of the kernel is computed and the roofline position is stated.
- The chosen tile size is justified by occupancy, smem reuse, or register analysis — not just by convention.
- An autotuning or benchmarking plan is included if the tile size is not verified empirically.