6.2.8 · D1GPU Architecture

Foundations — Coalesced memory access

3,330 words15 min readBack to topic

This page assumes nothing. Before you can understand why one memory pattern is 32× faster than another, you need to know what a thread, a warp, an address, a byte, a transaction, and a stride actually are — and see the picture behind each. We build them in order, each one leaning on the one before.


1. Byte — the unit everything is measured in

Picture a row of lockers. Each locker is one byte. The number written on the locker door is its position in the row — locker 0, locker 1, locker 2, and so on.

Figure — Coalesced memory access

Why we need it: every quantity in this topic — how big a float is, how big a transaction is, where a thread reads — is counted in bytes. It is the ruler.


2. Address — the number on the locker door

You will see addresses written in hexadecimal, like 0x0004. That is just another way to write a normal number:

Hex Plain number
0x0000 0
0x0004 4
0x0020 32
0x0080 128
0x1000 4096

Why hex? Because memory sizes are powers of two (32, 128, 4096…), and those come out as round, easy-to-spot numbers in base 16. 0x1000 = 4096 is tidy; "4096" in base 10 hides the roundness.

Question — what plain number is 0x0020? ::: 32 Question — why do GPU engineers write addresses in hex? ::: Memory sizes are powers of two, which look round and easy to align in base 16.


3. Element and its size — a value spread over several bytes

One byte only holds 0–255, which is tiny. To store a real decimal number (a float32) the computer glues 4 bytes together into one element.

So the address of data[i] is:

where base is the address of the very first element data[0].

Why we need : to turn an array index (data[7]) into an address (byte 28), which is the language the memory hardware actually speaks.


4. Thread — one worker running the code

Picture 32 identical workers, each holding a slip of paper with their number: 0, 1, 2, …, 31.

Why we need it: coalescing is entirely about which threads read which addresses at the same time. No threads, no story. (This page treats a thread simply as "one worker with an ID"; that is all you need here.)


5. Warp — 32 threads that move in perfect lock-step

This is the heart of the whole topic.

Figure — Coalesced memory access

The consequence that makes coalescing possible: when the shared instruction is "read from memory," all 32 threads hit memory simultaneously. The hardware sees 32 addresses arriving together and gets to decide how to serve them cleverly — as one batch, not 32 separate errands.

Question — what does SIMT force all 32 threads to do? ::: Execute the same instruction at the same time, so their memory requests arrive together.


6. Transaction size (a.k.a. "sector") — memory only sells in fixed-size bundles

Memory hardware never hands you a single byte. It always delivers a fixed-size burst.

Aligned means the burst always starts at an address that is a whole multiple of . With : transaction 0 covers bytes 0x000x1F, transaction 1 covers 0x200x3F, and so on. You cannot get a burst that starts in the middle.

Figure — Coalesced memory access

Why exists: DRAM chips read whole rows at once (see 6.2.09-Memory-hierarchy). Grabbing one byte costs almost the same as grabbing a full burst, so the controller is built to move bursts. This single fact is the entire reason coalescing matters.

The base-alignment edge case

Because transactions start only at multiples of , where the array itself begins matters.

The picture: a perfectly packed run of bytes still spills into an extra burst if it does not start on a transaction boundary. This is why libraries return memory aligned to 128 or 256 bytes — to guarantee the base sits on a boundary and the minimum transaction count is achievable.

Question — why can a perfectly sequential warp still cost 5 transactions instead of 4? ::: If the array base is not a multiple of T, the 128 useful bytes straddle an extra transaction boundary.


7. Stride — the gap between one thread's address and the next

Stride is the knob that decides everything:

  • Small stride (= element size): consecutive threads read neighbouring lockers, so many of them land inside the same transaction. → coalesced, few bursts.
  • Large stride (≥ transaction size): every thread lands in a different transaction. → non-coalesced, up to 32 bursts.
Figure — Coalesced memory access

Every case, spelled out (assume base aligned to , , , so 8 floats per transaction):

Stride What happens Transactions for the warp
(4 B) 8 floats fit per transaction; 32 floats span 128 B 4 (the minimum)
(8 B) threads step 2 floats apart; 4 threads per transaction; warp spans 256 B 8
(16 B) 2 threads per transaction; warp spans 512 B 16
(32 B) exactly 1 thread per transaction; each starts a fresh one 32 (worst case)
(e.g. 4096 B) threads even further apart, still one transaction each 32 (worst case)
stride 0 (all read same address) one transaction serves everyone (broadcast) 1

The pattern for intermediate strides (, base aligned): the number of threads that share one transaction is — you round up, because a thread whose element even partially falls inside a transaction still counts as landing there. Equivalently, threads-per-transaction . So transactions . As the stride grows from toward , transactions climb steadily from 4 up to 32 — coalescing degrades gradually, it is not an all-or-nothing cliff.

Notice the worst case saturates at 32: you can never need more than one transaction per thread, because there are only 32 threads.

Question — what stride gives perfect coalescing for float32? ::: Stride equal to the element size, 4 bytes (each thread reads the next element). Question — with stride 8 bytes (2 floats), how many transactions does the warp need? ::: 8 — four threads share each 32-byte transaction, so 32÷4 = 8. Question — how many threads share a transaction for stride s (T-byte transactions)? ::: ceil(T/s) — round up, because a partially-overlapping element still counts. Question — why can't a warp ever need more than 32 transactions? ::: There are only 32 threads; worst case is one transaction each.


8. Coalesced vs non-coalesced — the definition, stated plainly

Now that transactions and strides exist, we can define the term the whole topic is named after.

Note this is relative to the minimum, not a fixed number: 32 consecutive floats are coalesced whether that costs 1 transaction (128-byte-line GPU) or 4 transactions (32-byte-sector GPU) — in both cases it is the fewest bursts possible for 128 useful bytes.

Question — define coalesced access in one line. ::: The warp's addresses are served by the minimum possible number of transactions, so almost every fetched byte is used. Question — is "coalesced" a fixed transaction count? ::: No — it means the minimum for that architecture; the count differs between a 128-byte-line and a 32-byte-sector GPU.


9. Bandwidth and Latency — the two costs of a memory trip

Two different clocks tick when you touch memory. Confusing them is the classic beginner error.

This is exactly the tension the 6.3.02-Roofline-model captures.


10. and Efficiency — the scorecard

First, name the counter we have been using all along:

Now we can grade any pattern. Build the fraction piece by piece:

  • Bytes actually used — each of the threads genuinely wants one element of bytes, and no more. So useful work . With , that is bytes.
  • Bytes actually fetched — the hardware pulls whole bursts, each bytes, whether or not every byte is wanted. So fetched .

Every symbol in that fraction was built above: (§5), (§3), (§6), (§10). Nothing here is new — you now own the whole formula.

Question — why is "bytes used" equal to W·E? ::: Each of the W threads wants exactly one E-byte element, and nothing more. Question — why is "bytes fetched" equal to N_transactions·T? ::: The hardware always pulls whole T-byte bursts, one per transaction, wanted or not. Question — what is the efficiency of stride-32-byte access on a 32-byte-transaction GPU? ::: 12.5% — 128 useful bytes out of 1024 fetched.


How the pieces feed the topic

The map below traces the exact build order of this page: bytes give us addresses; addresses plus the element size locate each data[i]; threads grouped into a warp all read at once; the stride between their addresses and the transaction size together decide ; that count defines whether access is coalesced, and fed with latency/bandwidth it gives the time cost and, with used-vs-fetched bytes, the efficiency. Read each arrow as "is needed before you can understand."

Byte - one memory box

Address - box number

Element size E - 4 bytes per float

Thread - one worker

Warp W - 32 threads in lock-step

Stride - gap between thread addresses

Transaction size T - fixed burst

Number of transactions

Coalesced or non-coalesced

Latency L and Bandwidth B

Time cost

Efficiency = used over fetched

Coalesced Memory Access

Related prerequisites in the vault: 6.2.04-Shared-memory-and-synchronization (an alternative when coalescing is impossible), 6.2.09-Memory-hierarchy (where transactions live), and 5.1.05-Cache-coherence-protocols (cache-line ideas that echo here).


Equipment checklist

Cover the right side and answer out loud. If any line surprises you, reread its section before tackling the parent note.

A byte is... ::: one memory box holding a number 0–255; memory is a numbered row of them. An address is... ::: the number identifying which byte you mean. 0x0020 in plain digits is... ::: 32. An element is... ::: one logical data value the program treats as a single number, made of E bytes. Element size E for float32 is... ::: 4 bytes; data[i] starts at base + i×4. A thread is... ::: one independent worker with its own ID, threadIdx.x. A warp (size W) is... ::: 32 threads forced to run the same instruction at the same instant (SIMT). A transaction / sector (size T) is... ::: the smallest fixed, aligned burst the memory controller fetches (T=32 B on Volta-style); the two words mean the same thing here. "Aligned" means... ::: the burst starts at a multiple of T; you can't fetch one starting mid-burst. A misaligned array base can... ::: push a packed warp into one extra transaction, because the useful bytes straddle a boundary. Stride is... ::: the fixed address gap between consecutive threads' reads. Stride for perfect coalescing is... ::: equal to the element size (4 B), so threads read neighbouring lockers. Threads sharing one transaction for stride s is... ::: ceil(T/s) — round up, not floor. Coalesced access is... ::: a warp served by the minimum possible number of transactions, so nearly every fetched byte is used. Non-coalesced access is... ::: scattered addresses needing more than the minimum transactions, wasting most fetched bytes. N_transactions is... ::: how many T-byte bursts the hardware issues for one warp's read. Latency L is... ::: the wait before the first byte arrives (a representative ~300 ns; real GPUs 400–800 ns+), paid once per transaction. Bandwidth B is... ::: how fast bytes stream once flowing (~900 GB/s); does not include the wait. Non-coalescing is slow mainly because... ::: you pay latency L up to 32 times, once per scattered transaction. Efficiency equals... ::: bytes used over bytes fetched = W·E / (N_transactions·T).