LESSON READING // 01
MEMORY HIERARCHY
Once enough threads exist, stop adding more. Shape each warp's memory requests and stage reused data in the right memory space.
Write down answers as you go; the estimate includes code tracing and practice.
PLAY THE INTERACTIVE LESSON→- 01ORIENT + OBJECTIVES05 MIN
- 02MENTAL MODEL08 MIN
- 03DEEP DIVES15 MIN
- 04CODE TRACE12 MIN
- 05HISTORY + ECOSYSTEM08 MIN
- 06PRACTICE + REVIEW12 MIN
Move beyond running code: use industry tools to locate memory bottlenecks and let evidence drive optimization.
The lesson visualizes coalescing, reuse, and shared memory; the lab requires profiler evidence for transactions, throughput, and bank behavior.
- Lesson 00 or a basic kernel
- Warp and global-index concepts
- Reference-output comparison
- Nsight Compute
- CUDA C++
- CUDA C++ Best Practices Guide
ORIENT + OBJECTIVES
REMEMBER THIS FIRST
GPU performance depends not only on how much you compute, but on the shape in which data reaches the compute units.
BY THE END, YOU SHOULD BE ABLE TO
- 01
Inspect a warp's address set and predict excess global-memory transactions.
- 02
Choose registers, shared memory, or global memory from reuse scope.
- 03
Explain halos, cooperative loading, and synchronization in neighborhood work.
- 04
Recognize bank conflicts, unsafe barriers, and over-caching.
BUILD THE MENTAL MODEL
- 01ADDRESS
A warp issues 32 addresses together; hardware coalesces them into as few memory transactions as possible.
- 02REUSE
When a block reuses neighboring values, its threads can cooperatively stage them in shared memory.
- 03SYNC
Readers can safely consume the complete tile only after all participating threads finish loading it.
CONCEPT DEEP DIVES
COALESCED ACCESS
Adjacent threads reading adjacent, suitably aligned data commonly reduces the global transactions needed to serve a warp.
SCOPE CHOOSES THE SPACE
Registers belong to a thread, shared memory to a block, and global memory is visible across the grid and host.
SHARED MEMORY IS MANAGED
It is fast but finite, can suffer bank conflicts, and requires explicit loading, layout, and synchronization.
COUNT TRANSACTIONS, NOT JUST ADDRESSES
When a warp executes a global load, it presents a set of addresses together. Hardware services the required memory segments, so the useful question is not merely '32 values were read' but 'how many bytes moved to deliver them?' Contiguous addresses commonly improve useful-byte efficiency.
Exact coalescing rules vary with compute capability, data width, and cache path. The durable method is to inspect address distribution, alignment, and useful bytes, then confirm with a profiler such as Nsight Compute.
PAUSE AND REASONWhich usually wastes more transfer: contiguous floats or floats 4 KB apart?+
REFERENCE ANSWERThe 4 KB stride usually touches many more segments and transfers more unused data; contiguous addresses coalesce more readily.
TILES TURN REPEATED MOVEMENT INTO COOPERATION
Neighborhood, convolution, and matrix algorithms often make adjacent threads reread the same input. A shared-memory tile lets a block load a region cooperatively, then reuse it on chip. Halos provide the extra edge values needed by boundary threads in the tile.
Tile size trades reuse against shared-memory capacity, block size, and halo overhead. Tiny tiles spend more on halos; large tiles can reduce resident blocks. Compare saved global traffic with added barriers, indexing, and resource cost.
PAUSE AND REASONHow can 66 input values serve 64 outputs in a three-point stencil?+
REFERENCE ANSWERThe 64 central values plus two halos cover every neighbor. Central values are reused rather than loaded three times per thread.
THE WHOLE PARTICIPATING GROUP MUST REACH A BARRIER
__syncthreads() is a block barrier. If only some threads enter a branch containing the barrier, others may never arrive. A common safe pattern conditionally performs load duties but reaches the barrier unconditionally.
Shared memory is divided into banks. Threads mapping different addresses to the same bank can serialize; padding a 2D tile is a common remedy. A barrier ensures visibility but does not repair bank conflicts or bad indexing.
PAUSE AND REASONWhy is if (threadIdx.x < 16) __syncthreads(); unsafe?+
REFERENCE ANSWEROnly part of the block reaches the barrier, producing undefined behavior and possible deadlock.
PUT IT BACK INTO CODE
01__shared__ float tile[BLOCK_SIZE + 2];02int i = blockIdx.x * blockDim.x + threadIdx.x;03tile[threadIdx.x + 1] = input[i];04// boundary threads also load left/right halos05__syncthreads();06output[i] = tile[t] + tile[t + 1] + tile[t + 2];TRACE THE CODE
- 01ALLOCRESERVE CENTRAL VALUES + HALOS
BLOCK_SIZE + 2 leaves one location on each side; 2D kernels commonly add borders in both dimensions.
- 02LOADCOALESCE THE CENTRAL LOAD
Adjacent threads map to adjacent central values; boundary threads take a small number of halo duties.
- 03BARRIERFINISH COOPERATION BEFORE USE
Every thread reaches __syncthreads() after completing its assigned loads.
- 04REUSECOMBINE OUTPUTS ON CHIP
Each global input can serve several neighboring outputs, with global boundaries still guarded.
KNOWLEDGE ATLAS
OPTIONAL REFERENCE · REVISIT AS NEEDED · OUTSIDE THE CORE 60 MINUTES
CORE VOCABULARY
- Memory Transaction
- A memory segment moved to satisfy a warp request; requested bytes and transferred bytes are not identical.
- Coalescing
- Arranging warp addresses into as few aligned segments as practical to raise useful-byte efficiency.
- Shared Memory
- Program-managed on-chip storage shared within a block for cooperative loading and reuse.
- Halo
- Extra input around a tile that lets edge threads compute neighborhood outputs.
- Bank Conflict
- Threads request different addresses in one bank, forcing a shared-memory request to split or serialize.
WORKED EXAMPLE
- 01DIRECT LOADS
64 × 3 = 192 scalar requests, with neighboring threads asking for many of the same values.
- 02COOPERATIVE TILE
Load 64 body values plus one halo value on each side: 66 values total.
- 03ESTIMATE REUSE
192 ÷ 66 ≈ 2.9, so the source-level request count falls to roughly one third.
DIAGNOSTIC PLAYBOOK
Low bandwidth with many load requests
- INSPECT FIRST
- Warp continuity, alignment, and whether adjacent threads touch adjacent data
- EVIDENCE
- Nsight Compute transactions, throughput, and warp-address shape
Shared memory made it slower
- INSPECT FIRST
- Reuse, barriers, halo ratio, capacity, and occupancy
- EVIDENCE
- Before/after global bytes, barrier stalls, and active warps
Unexpected latency in a 2D tile
- INSPECT FIRST
- Whether row stride maps many lanes to one bank
- EVIDENCE
- Bank-conflict metrics and a controlled +1-padding comparison
HARDWARE → ECOSYSTEM
- 01GLOBAL-MEMORY DISCIPLINE
Early GPGPU demanded careful address organization.
Coalescing became a foundational optimization. - 02CACHE + SHARED MEMORY
Cache hierarchy, ECC, and on-chip reuse grew stronger.
Reasoning expanded to reuse, reliability, and resources. - 03ASYNCHRONOUS MOVEMENT
Newer architectures strengthened asynchronous tile movement.
Software pipelines load the next tile while computing the current one.
HARDWARE + ECOSYSTEM COORDINATE
Fermi strengthened caches, ECC, and the memory system required for general-purpose computing. Data-center GPU programming increasingly became a question of data movement and reliability, not just arithmetic-unit count.
PRACTICE + REVIEW
A warp reads input[i * 16]. What should you inspect first?
- A. Whether addresses spread across many memory transactions
- B. Whether the block count always equals 16
- C. Whether to add more CPU threads
REVEAL ANSWER+
The stride spreads addresses within the warp. Inspect the request shape before reordering data or introducing shared memory.
A warp reads 32 consecutive floats starting one float off alignment. Is it always as efficient as aligned access?
HINT+
Consider whether an extra segment is touched.
REFERENCE ANSWER+
Not always. Misalignment can add a transaction, although cache reuse may soften the cost. Check alignment and measure.
64 threads each read left, center, and right. How many scalar reads versus tile values?
HINT+
Ignore caches for the conceptual count.
REFERENCE ANSWER+
192 scalar reads versus 66 tile inputs: 64 centers and two halos. Hardware transaction count still depends on access shape.
Only halo-loading threads execute __syncthreads(). How do you fix it?
HINT+
Separate load duty from barrier participation.
REFERENCE ANSWER+
Keep conditional halo loads in branches, then move __syncthreads() after the branches so the full block reaches it.
Should a value used once by one thread go into shared memory?
HINT+
Look at scope and reuse.
REFERENCE ANSWER+
Usually no. A short-lived thread-private value belongs in a register; shared memory is for block-wide sharing or meaningful reuse.
OPTIONAL HANDS-ON LAB
OUTSIDE THE CORE 60 MINUTES · REQUIRES A SUITABLE CUDA / GPU ENVIRONMENT
LAB GOAL
MEASURE CONTIGUOUS, STRIDED, AND SHARED-MEMORY PATHS
Profile three correct variants and connect address shape to global-memory efficiency and shared-memory behavior.PROCEDURE
- 01
Implement input[i] and input[(i * stride) % n] with fixed size and warmup.
- 02
Collect global load/store, memory-throughput, and warp metrics.
- 03
Add a three-point shared-memory tile with halos and a safe barrier.
- 04
Compare direct and tiled code; explain unchanged results rather than forcing a speedup.
Provide correctness, a metric table, and one causal explanation linking address/reuse behavior to profiler evidence.
Compare a 2D transpose tile with and without +1 padding for bank conflicts.