Shared Memory and Bank Conflicts
The scratchpad is split into banks. Two lanes hitting the same bank at different addresses serialize, and the cost scales with how many.
Shared memory is fast because it is physically split into independent banks that can be read simultaneously. Consecutive 4-byte words live in consecutive banks, cycling around. If every lane in a group hits a different bank, all of the accesses complete together.
If two lanes hit different addresses in the same bank, the hardware has no choice but to serialize them. An N-way conflict costs N times as long, and a pathological access pattern can turn the fastest memory on the chip into something slower than a cache hit.
Think of shared memory as a set of parallel columns. Word 0 is in bank 0, word 1 in bank 1, and so on, wrapping back around after the last bank. A lane reading word 33 and a lane reading word 1 are hitting the same bank, because the bank is determined by the address modulo the bank count.
Two lanes reading the same address is not a conflict. The hardware broadcasts a single value to every lane that wants it, which is why reading a shared coefficient across a whole group is free. The conflict is specifically different addresses landing in the same bank.
A tile of floats declared with a power-of-two row length is the textbook trigger, because a column walk then hits the same bank every time.
__shared__ float tile[32][32];
// Each lane reads a different row, same column.
// Row stride is 32 words, so every lane lands in the same bank.
float v = tile[threadIdx.x][0]; // 32-way conflict, fully serialized
c of every row sits in the same bank. Thirty-two lanes, one bank, thirty-two serialized accesses.
The fix is one character:
__shared__ float tile[32][33]; // pad the stride by one
float v = tile[threadIdx.x][0]; // conflict-free
Bank conflicts are resolved per execution group, which means the group size participates. On hardware with 64-lane wavefronts the hardware handles the access in halves, so the conflict analysis is done over a half-wavefront rather than all 64 lanes at once.
The practical consequence is that a padding scheme tuned on 32-lane hardware is not automatically correct on 64-lane hardware, and vice versa. It is another item on the list of things that port silently and wrongly, alongside the shuffle and ballot issues.
Bank conflicts only matter in kernels that hammer shared memory in their inner loop, which in practice means tiled matrix kernels, transposes, stencils and reductions. A kernel that loads a tile once and reads it in a simple contiguous pattern is very unlikely to have a problem.
The ordering also matters: fix your global memory access pattern first. Coalescing failures cost you HBM bandwidth, which is the scarcest resource on the machine; bank conflicts cost you scratchpad bandwidth, which is plentiful. A kernel with both problems should have the global one fixed first.