GEMM Kernel Optimization
Overview
General Matrix Multiply (C = A @ B) is the most compute-intensive operation in deep learning.
GEMM is compute-bound: the key metric is TFLOPS (target: >60% of hardware peak).
| Bound |
Metric |
Target (H100) |
Target (4090) |
Target (MI300X) |
| Compute |
TFLOPS |
>500 FP16 |
>150 FP16 |
>600 FP16 |
Core Technique
Algorithm: Tiled Blocked GEMM
- Partition output C into
[BLOCK_M, BLOCK_N] tiles, one per program instance
- Accumulate in FP32: iterate K dimension in
BLOCK_K chunks
- Each iteration: load A tile
[BLOCK_M, BLOCK_K], B tile [BLOCK_K, BLOCK_N], call tl.dot
- Store result tile back to C (cast to output dtype)
L2 Cache Grouping
Naive row-major block ordering causes excessive HBM reads. Group GROUP_SIZE_M rows of blocks
before moving to the next column to maximize L2 reuse of B tiles.
Empirically: GROUP_SIZE_M=8 gives 10-15% improvement on A100/H100.
Tensor Core Utilization
tl.dot(a, b, acc) maps to WMMA (Ampere) or WGMMA (Hopper) or MFMA (CDNA)
- Accumulator must be FP32 for accuracy
- BLOCK_K must be >= 16 for tensor core eligibility
- On MI300X, MFMA prefers BLOCK_K=32 with wavefront-64
Autotune Configs
NVIDIA (Ampere/Ada/Hopper)
NVIDIA_CONFIGS = [
triton.Config({'BLOCK_SIZE_M': 128, 'BLOCK_SIZE_N': 256, 'BLOCK_SIZE_K': 64, 'GROUP_SIZE_M': 8}, num_stages=3, num_warps=8),
triton.Config({'BLOCK_SIZE_M': 128, 'BLOCK_SIZE_N': 128, 'BLOCK_SIZE_K': 32, 'GROUP_SIZE_M': 8}, num_stages=4, num_warps=4),
triton.Config({'BLOCK_SIZE_M': 64, 'BLOCK_SIZE_N': 128, 'BLOCK_SIZE_K': 32, 'GROUP_SIZE_M': 8}, num_stages=4, num_warps=4),
triton.Config({'BLOCK_SIZE_M': 64, 'BLOCK_SIZE_N': 64, 'BLOCK_SIZE_K': 32, 'GROUP_SIZE_M': 8}, num_stages=5, num_warps=2),
]
AMD MI300X (CDNA3, gfx942)
AMD_CONFIGS = [
triton.Config({'BLOCK_SIZE_M': 128, 'BLOCK_SIZE_N': 128, 'BLOCK_SIZE_K': 32, 'GROUP_SIZE_M': 8}, num_warps=4, num_stages=2),
triton.Config({'BLOCK_SIZE_M': 128, 'BLOCK_SIZE_N': 256, 'BLOCK_SIZE_K': 32, 'GROUP_SIZE_M': 8}, num_warps=8, num_stages=2),
triton.Config({'BLOCK_SIZE_M': 256, 'BLOCK_SIZE_N': 128, 'BLOCK_SIZE_K': 32, 'GROUP_SIZE_M': 8}, num_warps=8, num_stages=2),
triton.Config({'BLOCK_SIZE_M': 64, 'BLOCK_SIZE_N': 64, 'BLOCK_SIZE_K': 32, 'GROUP_SIZE_M': 8, 'waves_per_eu': 8}, num_warps=4, num_stages=2),
]
Key AMD differences:
- Wavefront = 64 threads (not 32);
num_warps=4 means 256 threads
num_stages must be >= 1 (0 crashes on HIP)
waves_per_eu hint for occupancy tuning (2-4 compute-bound, 4-8 memory-bound)
Verification
python skills/kernels/gemm/test_gemm.py
- Correctness:
torch.allclose(triton_result, torch.matmul(A, B), atol=1e-2, rtol=1e-2) for FP16
- Performance: report TFLOPS =
2*M*N*K / latency_s / 1e12
Common Pitfalls
- Missing mask on K boundary: when K is not divisible by BLOCK_K, unmasked loads read garbage
- Wrong accumulator dtype: using FP16 accumulator causes catastrophic precision loss
- GROUP_SIZE_M=1: disables L2 grouping, 10-20% perf regression on large matrices
- BLOCK_K too small on CDNA: MI300X MFMA prefers BLOCK_K >= 32
- Forgetting
.to(output_dtype) before store: silent precision issues
Agent Instructions
When optimizing a GEMM kernel:
- Start with
triton_template.py as baseline
- Profile with NCU/rocprof to determine if memory or compute bound
- If compute-bound and < 60% peak: check tensor core utilization, increase BLOCK_K
- If memory-bound: check L2 hit rate, adjust GROUP_SIZE_M, consider split-K
- Run
test_gemm.py after every change to verify correctness + measure TFLOPS
References
1---2name: gemm-kernel-optimization3description: Optimize dense matrix multiplication (GEMM) kernels in Triton for NVIDIA and AMD GPUs. Covers tiled blocking, L2 cache grouping, tensor core utilization, and dual-platform autotune. Use when writing or optimizing matmul, linear layers, or any GEMM-based kernel.4---56# GEMM Kernel Optimization78## Overview910General Matrix Multiply (C = A @ B) is the most compute-intensive operation in deep learning.11GEMM is **compute-bound**: the key metric is **TFLOPS** (target: >60% of hardware peak).1213| Bound | Metric | Target (H100) | Target (4090) | Target (MI300X) |14|-------|--------|---------------|---------------|-----------------|15| Compute | TFLOPS | >500 FP16 | >150 FP16 | >600 FP16 |1617## Core Technique1819### Algorithm: Tiled Blocked GEMM20211. Partition output C into `[BLOCK_M, BLOCK_N]` tiles, one per program instance222. Accumulate in FP32: iterate K dimension in `BLOCK_K` chunks233. Each iteration: load A tile `[BLOCK_M, BLOCK_K]`, B tile `[BLOCK_K, BLOCK_N]`, call `tl.dot`244. Store result tile back to C (cast to output dtype)2526### L2 Cache Grouping2728Naive row-major block ordering causes excessive HBM reads. Group `GROUP_SIZE_M` rows of blocks29before moving to the next column to maximize L2 reuse of B tiles.3031Empirically: GROUP_SIZE_M=8 gives 10-15% improvement on A100/H100.3233### Tensor Core Utilization3435- `tl.dot(a, b, acc)` maps to WMMA (Ampere) or WGMMA (Hopper) or MFMA (CDNA)36- Accumulator must be FP32 for accuracy37- BLOCK_K must be >= 16 for tensor core eligibility38- On MI300X, MFMA prefers BLOCK_K=32 with wavefront-643940## Autotune Configs4142### NVIDIA (Ampere/Ada/Hopper)4344```python45NVIDIA_CONFIGS = [46 triton.Config({'BLOCK_SIZE_M': 128, 'BLOCK_SIZE_N': 256, 'BLOCK_SIZE_K': 64, 'GROUP_SIZE_M': 8}, num_stages=3, num_warps=8),47 triton.Config({'BLOCK_SIZE_M': 128, 'BLOCK_SIZE_N': 128, 'BLOCK_SIZE_K': 32, 'GROUP_SIZE_M': 8}, num_stages=4, num_warps=4),48 triton.Config({'BLOCK_SIZE_M': 64, 'BLOCK_SIZE_N': 128, 'BLOCK_SIZE_K': 32, 'GROUP_SIZE_M': 8}, num_stages=4, num_warps=4),49 triton.Config({'BLOCK_SIZE_M': 64, 'BLOCK_SIZE_N': 64, 'BLOCK_SIZE_K': 32, 'GROUP_SIZE_M': 8}, num_stages=5, num_warps=2),50]51```5253### AMD MI300X (CDNA3, gfx942)5455```python56AMD_CONFIGS = [57 triton.Config({'BLOCK_SIZE_M': 128, 'BLOCK_SIZE_N': 128, 'BLOCK_SIZE_K': 32, 'GROUP_SIZE_M': 8}, num_warps=4, num_stages=2),58 triton.Config({'BLOCK_SIZE_M': 128, 'BLOCK_SIZE_N': 256, 'BLOCK_SIZE_K': 32, 'GROUP_SIZE_M': 8}, num_warps=8, num_stages=2),59 triton.Config({'BLOCK_SIZE_M': 256, 'BLOCK_SIZE_N': 128, 'BLOCK_SIZE_K': 32, 'GROUP_SIZE_M': 8}, num_warps=8, num_stages=2),60 triton.Config({'BLOCK_SIZE_M': 64, 'BLOCK_SIZE_N': 64, 'BLOCK_SIZE_K': 32, 'GROUP_SIZE_M': 8, 'waves_per_eu': 8}, num_warps=4, num_stages=2),61]62```6364Key AMD differences:65- Wavefront = 64 threads (not 32); `num_warps=4` means 256 threads66- `num_stages` must be >= 1 (0 crashes on HIP)67- `waves_per_eu` hint for occupancy tuning (2-4 compute-bound, 4-8 memory-bound)6869## Verification7071```bash72python skills/kernels/gemm/test_gemm.py73```7475- Correctness: `torch.allclose(triton_result, torch.matmul(A, B), atol=1e-2, rtol=1e-2)` for FP1676- Performance: report TFLOPS = `2*M*N*K / latency_s / 1e12`7778## Common Pitfalls79801. **Missing mask on K boundary**: when K is not divisible by BLOCK_K, unmasked loads read garbage812. **Wrong accumulator dtype**: using FP16 accumulator causes catastrophic precision loss823. **GROUP_SIZE_M=1**: disables L2 grouping, 10-20% perf regression on large matrices834. **BLOCK_K too small on CDNA**: MI300X MFMA prefers BLOCK_K >= 32845. **Forgetting `.to(output_dtype)` before store**: silent precision issues8586## Agent Instructions8788When optimizing a GEMM kernel:89901. Start with `triton_template.py` as baseline912. Profile with NCU/rocprof to determine if memory or compute bound923. If compute-bound and < 60% peak: check tensor core utilization, increase BLOCK_K934. If memory-bound: check L2 hit rate, adjust GROUP_SIZE_M, consider split-K945. Run `test_gemm.py` after every change to verify correctness + measure TFLOPS9596## References9798- [Triton Matrix Multiplication Tutorial](https://triton-lang.org/main/getting-started/tutorials/03-matrix-multiplication.html)99- [AutoKernel program.md Tier 1](https://github.com/RightNow-AI/autokernel)100- [CUTLASS: Persistent Kernels and Stream-K](https://research.colfax-intl.com/cutlass-tutorial-persistent-kernels-and-stream-k/)