Coalescing
How a warp's 32 addresses become 32-byte memory transactions, and what stride and alignment cost.
Loading the animation…
Concept
A warp's load instruction carries 32 addresses, one per lane. The memory system does not fetch 32 separate words: "global memory is accessed via 32-byte memory transactions", and the warp's request is coalesced into as many 32-byte sectors as its addresses touch (CUDA Programming Guide §2.3.4.1). Caches keep sectors in 128-byte lines of four (Nsight Compute Profiling Guide, "sector").
- Consecutive 4-byte words (stride 1): 32 lanes × 4 bytes = 128 bytes = exactly four sectors. Everything fetched is used: 100%. This is the default in the animation.
- A stride of 8 or more words (32 bytes or more): every lane lands in its own sector. The warp fetches 32 × 32 = 1,024 bytes to use 128, the guide's 12.5%.
- A misaligned start (offset 4): the same 128 bytes straddle a sector boundary, so five sectors and two lines: 80%.
- Wider elements (8 or 16 bytes per lane, such as
doubleorfloat4) still use every byte at stride 1, and need fewer instructions for the same data. - Stride 0: every lane reads the same word; one sector serves the whole warp.
Watch the memory panel as the lanes step through: at stride 1 lanes 0–7 share sector 0 and only every eighth lane opens a new one; at stride 8 every lane opens a new sector and the filled words are islands in mostly empty sectors. That empty space is HBM bandwidth spent on nothing.
The usual culprit is a two-dimensional array walked the wrong way. In row-major storage, consecutive threadIdx.x should index consecutive columns; if they index rows, the stride is the row length and every lane gets its own sector. A matrix transpose has to do one of the two, which is why it stages a tile through shared memory (chapter 5).
Maths
Lane of the warp reads bytes starting at byte , for start offset and stride elements. It touches sectors
and the warp fetches the union of them, sectors. It asked for bytes and the memory moved , so
With aligned 4-byte words, up to , then 32: the efficiency falls as until every lane has its own sector.
Code
The model's sector count, cut from src/lib/gpu/model.ts:
const addr = offset + lane * stride * elemBytes;
const first = idiv(addr, sector);
const last = idiv(addr + elemBytes - 1, sector);
And the pattern it describes, in CUDA. This kernel is illustrative and not compiled by this site's CI (no GPU there); it follows the guide's matrix examples:
// row-major a[rows][cols]: consecutive threads read consecutive columns
float good = a[row * cols + threadIdx.x]; // stride 1: coalesced
// consecutive threads read consecutive rows: stride = cols
float bad = a[threadIdx.x * cols + col]; // a sector per lane