Lecture 4 — GPU Kernel Performance Basics¶
Parent: FlashAttention Course
One-line purpose: Cover the strict minimum of GPU performance hardware (memory hierarchy, warps, tensor cores, occupancy, coalescing) needed to read FlashAttention source code without getting lost.
Prerequisites: Lectures 1–3. Basic CUDA from Phase 4 Track B.
Artifact: Nsight Compute reports for a coalesced vs uncoalesced copy kernel and a warp-shuffle reduction microbenchmark.
Why it matters¶
The FlashAttention codebase is dense with CUDA primitives — __shfl_xor_sync, shared-memory bank patterns, register tile sizes, cp.async, WGMMA descriptors. If those words are noise to you, you cannot read the kernel; you are reading a foreign language. This lecture is the dictionary.
You do not need to write GEMM from scratch. You do need to know what each layer of memory costs, how warps and tensor cores execute, and what the common failure modes are.
Mental model¶
Memory hierarchy on a modern NVIDIA GPU¶
| Level | Size (Hopper SXM) | Latency | Bandwidth | Who owns it |
|---|---|---|---|---|
| HBM (global) | 80 GB | ~400-500 ns | ~3.35 TB/s | device-wide |
| L2 | 50 MB | ~150-200 ns | ~12 TB/s | device-wide, partitioned |
| Shared / L1 | 228 KB / SM | ~30 ns | ~30 TB/s per SM | per thread block |
| Registers | 64 K × 32-bit / SM | 0 (compiler-managed) | unbounded | per thread |
The whole point of a high-performance kernel is to stage data so that the inner loop only touches registers and shared memory, and HBM is touched once per element. FlashAttention is built exactly around this principle.
Warps and the SM¶
- A warp is 32 threads that execute the same instruction in lockstep (SIMT). All performance reasoning is "what does the warp do this cycle."
- An SM (Streaming Multiprocessor) runs many warps concurrently. Hopper SM has 128 KB of register file and 228 KB shared/L1.
- Occupancy = how many warps are resident on an SM. Higher occupancy hides memory latency but costs registers per thread. FlashAttention deliberately keeps occupancy modest (e.g. 2–4 warps per block, blocks per SM = 1–2) because per-thread register tiles are large.
Coalescing¶
When the 32 threads of a warp issue loads that hit a contiguous 128-byte region of HBM, the hardware combines them into one transaction. If the addresses scatter, each thread triggers its own transaction and bandwidth collapses. Rule of thumb: lay out tensors in [batch, head, seq, dim] order (NHWC-like) so the fastest-changing axis matches the warp's thread index.
Shared-memory bank conflicts¶
Shared memory is organised into 32 banks of 4-byte words. If two threads in the same warp read different addresses in the same bank, the access serialises. FlashAttention avoids this by carefully padding shared-memory tiles (+8 or +1 on the inner dimension) and by using the ldmatrix / stmatrix instructions on Hopper.
Tensor cores¶
Tensor cores execute small fixed-shape matmuls per warp / warpgroup in one instruction:
- HMMA (Volta/Turing/Ampere): one warp issues
m16n8k16fp16/bf16 MMA → 256 FMA in ~8 cycles. - WGMMA (Hopper): one warpgroup (4 warps = 128 threads) issues an asynchronous
m64n{N}k16MMA against shared memory descriptors. - TCGen5 (Blackwell): tensor cores now operate at warpgroup-cluster scale with FP4/FP6 support.
FlashAttention is a tensor-core engine wrapped in softmax glue. Most of the runtime is in MMA instructions. The rest is the rescale + exponentials around them.
Warp shuffles¶
__shfl_sync, __shfl_xor_sync, etc. let threads in a warp read each other's registers in 1 cycle — no shared memory needed. Used for warp-level reductions (m, ℓ in the softmax recurrence) and for swizzling MMA inputs.
cp.async and TMA¶
cp.async(Ampere+) issues a DMA from HBM to shared memory that the warp can wait on later, overlapping with compute.- TMA (Hopper) is the same idea at coarser granularity: a single instruction copies a multi-dimensional tile from HBM to shared memory, the whole warp / warpgroup gets it for free, and you commit/wait on a barrier.
FA2 leans on cp.async. FA3 leans on TMA. The reason FA3 is faster is largely about overlapping these copies with the MMA so the math units never stall.
Build it¶
Two microbenchmarks. Both ship as standalone CUDA files in your course working directory.
1. Coalesced vs uncoalesced copy¶
// coalesce.cu
__global__ void copy_coalesced(const float* __restrict__ x, float* __restrict__ y, int n) {
int idx = blockIdx.x * blockDim.x + threadIdx.x;
if (idx < n) y[idx] = x[idx];
}
__global__ void copy_strided(const float* __restrict__ x, float* __restrict__ y, int n, int stride) {
int idx = (blockIdx.x * blockDim.x + threadIdx.x) * stride;
if (idx < n) y[idx] = x[idx];
}
Run both at n = 2^24. The coalesced one should hit ~85–90% of HBM peak. The strided one (e.g. stride = 32) should drop by 10–20×. Compare under ncu --metrics gpu__time_duration.sum,dram__bytes.sum.
2. Warp-shuffle reduction¶
// warp_reduce.cu
__inline__ __device__ float warp_sum(float v) {
for (int off = 16; off > 0; off >>= 1)
v += __shfl_down_sync(0xffffffff, v, off);
return v;
}
__global__ void rowsum_via_shuffle(const float* x, float* out, int n) {
int row = blockIdx.x;
int tid = threadIdx.x;
float acc = 0.f;
for (int j = tid; j < n; j += blockDim.x) acc += x[row * n + j];
acc = warp_sum(acc);
if ((tid & 31) == 0) atomicAdd(&out[row], acc);
}
This is structurally the same code FlashAttention uses to reduce m and ℓ across threads of a warp before broadcasting back. Time it against a naive shared-memory reduction; on Hopper the shuffle version should be ~2× faster for warp-local accumulators.
Use it in the real stack¶
In flash-attention/csrc/flash_attn/src/:
softmax.h: search forquad_shfl_xor_sync/Allreduce— these are the warp shuffles that computemandℓreductions inside a warp.block_info.handkernel_traits.h:kBlockM,kBlockN,kNWarps,kStages— the tile sizes, warps per block, andcp.asyncstage count.mask.handsoftmax.hagain: the rescale-and-add update oftOrOafter eachtOrP— this is the FP register-level realisation of the recurrence from Lecture 2.
Pick one CUDA file and annotate, in your own notes, where each of these is happening. Do not move on to Lecture 5 until you can point at the MMA call, the shuffle reduce, and the cp.async issue / commit / wait in real source.
Measure it¶
Standard Nsight Compute metrics worth memorising:
| Metric | Meaning |
|---|---|
sm__throughput.avg.pct_of_peak_sustained_elapsed |
Overall SM utilisation |
dram__bytes.sum |
HBM traffic |
lts__t_sectors.sum |
L2 traffic |
sm__warps_active.avg.pct_of_peak_sustained_active |
Achieved occupancy |
smsp__inst_executed_pipe_tensor_op_hmma.sum |
Tensor-core HMMA instructions |
smsp__sass_average_data_bytes_per_sector_mem_global.pct |
Coalescing efficiency |
For your two microbenchmarks, report at minimum: kernel time, achieved HBM bandwidth, achieved occupancy. For the warp-shuffle reduction, also report tensor-core instruction count (should be zero — it is a pure CUDA-core kernel).
Ship it¶
Drop into flash-attn-course/:
coalesce.cuand acoalesce_bench.txtwith the two kernel times and bandwidths.warp_reduce.cuand areduce_bench.txtcomparing shuffle vs shared-memory reduce.- A one-page Markdown cheat sheet mapping each term in the FlashAttention kernel (
tOrO,tOrP,tOrS,cp.async.commit_group,__shfl_xor_sync,wgmma.mma_async) to a one-sentence explanation in your own words.
If you have those three, you are ready for Lecture 5.