6.2.9GPU Architecture

Bank conflicts in shared memory

2,704 words12 min readdifficulty · medium

Why this matters: Shared memory is supposed to be fast (~100× faster than global memory). Bank conflicts destroy this advantage by serializing what should be parallel access.

How shared memory banks work

Derivation from first principles:

  1. Why divide by 4? Each bank is 4 bytes wide (the fixed bank width on NVIDIA hardware). A byte address must be converted to a word index. Address 0-3 → word 0, address 4-7 → word 1, etc.
  2. Why mod 32? With 32 banks, we distribute words in a round-robin fashion. Word 0 → bank 0, word 1 → bank 1, .., word 31 → bank 31, word 32 → bank 0 again.
  3. Result: Words are striped across banks. Consecutive 32-bit words live in consecutive banks.
Figure — Bank conflicts in shared memory

Types of access patterns

Why this works:

  • Thread 0 reads shared[0] → word 0 → bank 0
  • Thread 1 reads shared[1] → word 1 → bank 1
  • ...
  • Thread 31 reads shared[31] → word 31 → bank 31

All 32 threads hit different banks → 1 cycle access, perfect parallelism.

Analysis:

  • Thread 0: shared[0] → bank 0
  • Thread 1: shared[2] → bank 2
  • Thread 2: shared[4] → bank 4
  • ...
  • Thread 16: shared[32] → word 32 → bank (32mod32)=0(32 \mod 32) = 0conflict with thread 0!
  • Thread 17: shared[34] → bank 2 ← conflict with thread 1!

Result: 16 pairs of threads collide (2-way conflicts). Access takes 2 cycles instead of 1.

Why this step? With stride-2, every 16 threads we wrap around to bank 0 again. Threads separated by 16 positions request the same bank.

  • Thread 0: shared[0] → bank 0
  • Thread 1: shared[32] → word 32 → bank 0
  • Thread 2: shared[64] → word 64 → bank 0
  • All 32 threads hit bank 0!

Access time: 32 cycles (fully serialized). Shared memory is now slower than registers.

The mathematical pattern

Derivation:

  1. Thread ii accesses word index isi \cdot s.
  2. Bank for thread ii: (is)mod32(i \cdot s) \mod 32.
  3. Two threads ii and jj conflict if (is)mod32=(js)mod32(i \cdot s) \mod 32 = (j \cdot s) \mod 32.
  4. This simplifies to (ij)s0(mod32)(i - j) \cdot s \equiv 0 \pmod{32}.
  5. The smallest k=ijk = i - j satisfying this is k=32/gcd(s,32)k = 32 / \gcd(s, 32).
  6. So every 32/gcd(s,32)32/\gcd(s,32) threads, we repeat banks, meaning gcd(s,32)\gcd(s,32) threads collide per bank.

Examples:

  • Stride 1: gcd(1,32)=1\gcd(1,32) = 1no conflict
  • Stride 2: gcd(2,32)=2\gcd(2,32) = 22-way conflict
  • Stride 3: gcd(3,32)=1\gcd(3,32) = 1no conflict (odd strides are safe!)
  • Stride 16: gcd(16,32)=16\gcd(16,32) = 1616-way conflict
  • Stride 32: gcd(32,32)=32\gcd(32,32) = 3232-way conflict

Common mistake: structure-of-arrays vs array-of-structures

Why it feels wrong at first glance: "Threads are 12 bytes apart, so surely they collide on banks."

The steel-manned truth: Let's actually compute the banks for the .x access.

  • The word stride between adjacent .x fields is 3 words (12 bytes / 4).
  • Thread ii accesses word 3i3i, landing in bank (3i)mod32(3i) \mod 32.
  • Since gcd(3,32)=1\gcd(3, 32) = 1, the map i(3i)mod32i \mapsto (3i) \bmod 32 is a permutation of the 32 banks.
  • Thread 0→bank 0, T1→bank 3, T2→bank 6, ..., T10→bank 30, T11→bank 33 mod 32 = bank 1, ...

So a warp reading .x (or .y, or .z) is actually CONFLICT-FREE, because 3 is coprime to 32. The intuition that "non-unit stride = conflict" is false; only strides sharing a factor with 32 (i.e. even strides / powers of 2) cause conflicts.

When AoS does bite you: conflicts appear if the struct size shares a factor with 32 — e.g. a struct of 4 floats (16 bytes = 4 words). Then stride = 4, gcd(4,32)=4\gcd(4,32)=44-way conflict on each field. That is the real danger of AoS: you don't control the stride, and a "nice" power-of-two struct size silently creates conflicts.

The robust fix: Structure-of-arrays

__shared__ float x[32], y[32], z[32];
float px = x[threadIdx.x];  // Stride-1, guaranteed conflict-free
float py = y[threadIdx.x];
float pz = z[threadIdx.x];

SoA makes every field access stride-1, so you never depend on the struct size being coprime to 32.

Padding to avoid conflicts

Case A — same row, different columns (this is conflict-free):

float value = matrix[row][tid];   // fixed row, tid = column
  • Thread ii accesses word row32+i\text{row} \cdot 32 + i. As ii varies, the stride is 1 word.
  • Banks: (row32+i)mod32=i(\text{row}\cdot 32 + i) \bmod 32 = i (since 32032 \equiv 0). All different → no conflict.

Case B — same column, different rows (this is the bad one):

float value = matrix[tid][col];   // tid = row, fixed column
  • Thread ii accesses word i32+coli \cdot 32 + \text{col}. The stride is 32 words.
  • Banks: (i32+col)mod32=colmod32(i \cdot 32 + \text{col}) \bmod 32 = \text{col} \bmod 32, the same for every thread32-way conflict!

This column-walk pattern is exactly what happens in matrix transpose, so it matters a lot.

The fix: pad the inner dimension to 33

__shared__ float matrix[32][33];  // one extra padding column
float value = matrix[tid][col];   // column access
  • Now element [r][c][r][c] lives at word r33+cr \cdot 33 + c.
  • Thread ii (row ii, fixed column) accesses word i33+coli \cdot 33 + \text{col}.
  • Bank index (no extra /4 — we are already in word units): bank=(i33+col)mod32=(i+col)mod32\text{bank} = (i \cdot 33 + \text{col}) \bmod 32 = (i + \text{col}) \bmod 32 because 331(mod32)33 \equiv 1 \pmod{32}.
  • As ii runs 0..31, (i+col)mod32(i + \text{col}) \bmod 32 takes all 32 distinct valuesconflict-free!

Why this step? Padding to 33 makes the row stride 1(mod32)\equiv 1 \pmod{32}, so consecutive rows shift by exactly one bank instead of landing on the same bank. That is the whole trick.

Recall Explain to a 12-year-old

Imagine your school has 32 cubbyholes where students store their lunch boxes. The rule is: student 1 uses cubby 1, student 2 uses cubby 2, up to student 32 uses cubby 32. Then it repeats: student 33 uses cubby 1 again.

Now, 32 friends (a "warp") all try to grab their lunch at the same time. If they stored lunches in cubbies 1, 2, 3, .., 32, everyone gets theirs instantly—no waiting! That's conflict-free.

But what if they all stored lunch in cubby 1? Now the teacher has to hand them out one by one—super slow! That's a 32-way bank conflict.

Bank conflicts happen when multiple threads (students) want data from the same bank (cubby) at once. The GPU has to serve them one at a time instead of all together. The trick is to organize your data so each thread uses a different bank—like making sure each student has a different cubby number. Adding one "empty spare cubby" per row (padding to 33) shifts everyone by one so they stop crashing into the same cubby.

Connections

  • Shared memory architecture—the physical design enabling banks
  • Memory coalescing—similar parallelism concept for global memory
  • Occupancy—bank conflicts reduce effective occupancy by stalling warps
  • Matrix transpose—classic example where padding fixes conflicts
  • Warp divergence—another serialization bottleneck in GPU execution

#flashcards/hardware

What are shared memory banks and why do they exist? :: Banks are parallel sub-units of shared memory (typically 32) that allow multiple threads to access different addresses simultaneously. They exist to enable high-bandwidth parallel access—each bank can serve one request per cycle.

What is a bank conflict? :: When multiple threads in a warp access different addresses that map to the same bank, forcing serialized access instead of parallel access. This multiplies latency by the conflict degree.

How is bank index calculated for 4-byte words?
bank_index = (byte_address / 4) mod 32. Divide by 4 to convert byte address to word index, then mod 32 to find which of the 32 banks holds that word.

What is the conflict degree for stride-2 access? :: 2-way conflict. gcd(2, 32) = 2, so every two threads collide on the same bank, doubling access time.

Why does stride-1 avoid conflicts?
Consecutive threads access consecutive words, which map to consecutive banks (0, 1, 2, ..., 31). All 32 threads hit different banks in parallel.
What is the formula for conflict degree given stride s (in words)?
conflict_degree = gcd(s, 32). This is the number of threads that will collide on each bank.
Does a 12-byte struct (stride 3) cause bank conflicts when reading one field per thread?
No. gcd(3,32)=1, so the field accesses form a permutation of the 32 banks — conflict-free. Odd/coprime strides are safe; only strides sharing a factor with 32 (even/powers of 2) conflict.
For a shared matrix[32][32], which is bad: matrix[row][tid] or matrix[tid][col]?
matrix[tid][col] (same column, different rows) is bad — stride 32, all threads hit the same bank → 32-way conflict. matrix[row][tid] (same row) is stride-1 and conflict-free.
How does padding to [32][33] fix column access conflicts?
The row stride becomes 33 words. Since 33 ≡ 1 (mod 32), bank = (i*33 + col) mod 32 = (i + col) mod 32, giving all 32 distinct banks → conflict-free.
Do you recompute bank_index with /8 for doubles in 64-bit mode?
No. Banks are a fixed 4 bytes wide. An 8-byte double simply spans two consecutive banks; the hardware still handles a stride-1 warp of doubles conflict-free. Always reason in 4-byte banks.
What happens with stride-32 access?
32-way conflict. All threads access addresses that map to the same bank, completely serializing access (32 cycles instead of 1).
What access pattern is optimal for shared memory?
Stride-1 access where consecutive threads access consecutive 4-byte elements. This maps threads to different banks with perfect parallelism.

Concept Map

divided into

enable

each is

gives

produces

causes

forces

scaled by

hits distinct banks

spans two

stride-1 stays

Shared memory

32 banks

Parallel access

4-byte bank width

bank_index = addr/4 mod 32

Words striped across banks

Same bank, diff address

Bank conflict

Serialized access

Conflict degree

Stride-1 access

8-byte double

Hinglish (regional understanding)

Intuition Hinglish mein samjho

Hinglish (regional understanding)

Intuition Hinglish mein samjho

GPU ki shared memory ek dukaan jaisi socho jismein 32 alag-alag cash registers (banks) lage hain, taaki ek saath 32 customers (threads) ko serve kiya ja sake. Har bank ek clock cycle mein sirf ek address serve kar sakta hai. Jab warp ke saare 32 threads alag-alag banks se data maangte hain, toh sab kaam ek hi cycle mein parallel mein ho jaata hai — yahi ideal hai. Lekin jab do ya zyada threads same bank ko access karte hain (different addresses ke saath), tab "bank conflict" hota hai, aur hardware ko woh requests ek-ek karke serial mein serve karni padti hain. Isse access time conflict ki degree ke hisaab se multiply ho jaata hai.

Bank kaunsa hai yeh nikalne ka simple formula hai: bank_index = (byte_address / 4) mod 32. Divide-by-4 isliye kyunki har bank 4 bytes wide hota hai, aur mod-32 isliye kyunki 32 banks mein words round-robin fashion mein bant te hain. Iska matlab consecutive words consecutive banks mein rehte hain. Jab aap shared[threadIdx.x] jaisa stride-1 access karte ho, har thread alag bank pe jaata hai — perfect, 1 cycle. Lekin agar stride-2 (shared[tid*2]) use karo, toh thread 0 aur thread 16 dono bank 0 pe collide karte hain — 2-way conflict, 2 cycles lagte hain. Worst case stride-32 mein toh saare 32 threads bank 0 pe aa jaate hain, aur access poora serialize ho jaata hai — 32 cycles, matlab shared memory registers se bhi slow ho gayi!

Yeh baat isliye important hai kyunki shared memory ka pura fayda hi uski speed hai — yeh global memory se lagbhag 100 guna fast hoti hai. Agar tumhare access pattern mein bank conflicts hain, toh yeh speed advantage barbaad ho jaata hai kyunki jo cheez parallel honi chahiye thi woh serial ho gayi. Isliye jab bhi kernel likho, apne memory access patterns ko dekho aur strides ko aise design karo ki threads alag-alag banks pe fall karein. Ek chhoti si trick jaise padding add karna often bank conflicts ko khatam kar deti hai, aur tumhara kernel kaafi tez chalta hai.

Go deeper — visual, from zero

Test yourself — GPU Architecture

Connections