AI Infra Interviews logo
CUDA, Triton & Kernel Engineering / 02
easyNewNVIDIAFireworks

What is memory coalescing, why does a strided access pattern hurt, and how do you see it in a profiler?

A warp issues one load instruction and the memory system turns it into some number of 32-byte sector requests; that number is the whole story. The arithmetic for contiguous, stride-2 and stride-32 access, the row-major matrix where a loop order change gives 8x, and the two Nsight Compute counters that show the waste.

Updated Sep 2026 · Grounded in real AI infrastructure interview loops and written to a senior-engineer editorial bar, with every number worked and every diagram hand-built.

TL;DR: A warp's 32 threads execute a load together; the memory system services it in 32-byte sectors (four per 128-byte cache line). If the 32 threads read 32 consecutive floats (128 bytes), the request touches 4 sectors and every byte fetched is used: 100% efficiency. If thread i reads element i × 2 (stride 2), the same instruction touches 8 sectors and uses half of each byte fetched: 50%. At stride 32 floats (128 bytes) each thread's load lands in its own sector: 32 sectors for 128 useful bytes, 12.5% efficiency, and the kernel runs at an eighth of the bandwidth. The classic case is a row-major matrix accessed down a column, where consecutive threads step by the row length; swapping which index the thread id maps to fixes it. In Nsight Compute, the number to read is sectors per request (ideal 4 for 4-byte loads) and the L1 and L2 "bytes requested versus bytes transferred" ratio.

How to approach it

Describe the mechanism (a warp's load becomes sector requests) and give the three cases with numbers. Then the matrix example with code. Then the profiler metrics and what values mean. Close with the fixes and the reversal (when strided is unavoidable and what to do).

A strong answer

A typical situation: a kernel that reads a 4,096 × 4,096 float matrix column-wise runs at 400 GB/s on an H100 rated at 3.35 TB/s; the profiler shows 32 sectors per request; a one-line index swap brings it to 2.8 TB/s.

The mechanism, with the arithmetic:

a warp: 32 threads issue one 4-byte load each → 128 bytes wanted
memory: served in 32-byte sectors; the ideal is 4 sectors for the 128 bytes

case A, contiguous: thread t reads a[base + t]
  addresses span 128 contiguous bytes (aligned) → 4 sectors → 128 bytes fetched, 128 used → 100%
case B, stride 2: thread t reads a[base + 2t]
  addresses span 256 bytes → 8 sectors → 256 fetched, 128 used → 50%; twice the traffic per useful byte
case C, stride 32 (a column of a 32-wide row-major matrix, or any stride ≥ 8 floats): thread t reads a[base + 32t]
  each address is in a different sector → 32 sectors → 1,024 fetched, 128 used → 12.5%; 8× the traffic
case D, misaligned contiguous: a[base + t] with base not a multiple of 32 bytes
  128 bytes spanning 5 sectors → 160 fetched → 80%; a minor cost, worth aligning allocations to avoid
sanity: the bandwidth a strided kernel achieves is the peak times the efficiency: 3.35 TB/s × 12.5% ≈ 420 GB/s,
        which is the scenario's number.

Memory Coalescing has the sector model and the cache-line details; GPU Memory Hierarchy explains where the sectors come from (L2 and HBM granularity).

The matrix example:

// C[i][j] = A[i][j] * 2 for an N×N row-major matrix (A[i*N + j])

// version 1: thread id maps to the ROW; consecutive threads in a warp read consecutive rows → stride N
__global__ void scale_bad(const float* A, float* C, int N) {
    int i = blockIdx.x * blockDim.x + threadIdx.x;   // row
    if (i < N)
        for (int j = 0; j < N; ++j) C[i * N + j] = 2.0f * A[i * N + j];  // warp: 32 rows, same j → 32 sectors
}

// version 2: thread id maps to the COLUMN; consecutive threads read consecutive elements of one row → contiguous
__global__ void scale_good(const float* A, float* C, int N) {
    int j = blockIdx.x * blockDim.x + threadIdx.x;   // column
    if (j < N)
        for (int i = 0; i < N; ++i) C[i * N + j] = 2.0f * A[i * N + j];  // warp: 32 columns, same i → 4 sectors
}

// version 3 (2D grid): both indices from thread ids, x fastest → contiguous, and no loop
__global__ void scale_2d(const float* A, float* C, int N) {
    int j = blockIdx.x * blockDim.x + threadIdx.x;
    int i = blockIdx.y * blockDim.y + threadIdx.y;
    if (i < N && j < N) C[i * N + j] = 2.0f * A[i * N + j];
}

The rule behind it: the fastest-varying thread index (threadIdx.x) should map to the fastest-varying memory index (the last dimension in row-major layout). Version 1 has each warp touching 32 rows at the same column, stride N floats, 32 sectors per request. Version 2 has each warp on 32 consecutive columns of one row, 4 sectors. Version 3 is the idiomatic 2D form with a 32 × 8 block (256 threads) whose x dimension is a warp reading 128 contiguous bytes.

The profiler view:

Nsight Compute, Memory Workload Analysis section:
  "L1/TEX: sectors per request" for global loads: 4.0 is ideal for 4-byte accesses (8.0 for 8-byte, 16 for float4);
    version 1 shows 32.0
  "Memory throughput" vs "DRAM throughput %": a kernel at 12% of peak DRAM bandwidth with high L2 sector traffic is
    fetching bytes it does not use
  "L2: bytes requested / bytes transferred" (or the "global load efficiency" derived metric in older tools): the
    fraction of fetched bytes the warp actually used; 12.5% in version 1
  the source view: per-line sectors per request, which points at the exact load
Nsight Systems shows only that the kernel is slow; the sector counts are Compute's job

Profiling with Nsight walks the sections; Roofline Model is where the 12.5% shows up as a kernel far below the bandwidth roof at low intensity.

The fixes, in order of preference: change the thread-to-data mapping (free); change the data layout (store the matrix transposed, or as structure-of-arrays instead of array-of-structures, so the access becomes contiguous); use shared memory as a staging tile (read a tile contiguously, then access it in the pattern the algorithm needs, which is the transpose kernel's trick; Shared Memory and Bank Conflicts covers what that introduces); vectorize with float4 when access is contiguous, so each thread asks for 16 bytes and a warp asks for 512 in 16 sectors per request, which reduces instruction count and helps reach peak on wide buses.

ONE WARP, ONE LOAD, 128 B OF USEFUL DATA contiguous 32 lanes × 4 B, adjacent 4 sectors strided by 2 every 2nd element 8 sectors strided by 32 one element per sector 32 sectors Same useful bytes, 8x the traffic. The sector count is the whole of coalescing. Nsight reports the sector count directly, so this is a lookup rather than a source-reading exercise.

The reversal condition: a gather with random indices (an embedding lookup, a sparse operation) cannot be coalesced; its efficiency is set by the data, and the design responses are to sort or bucket indices before the gather, to make the gathered rows at least 128 bytes so each row is its own efficient request, and to accept that the kernel is bound by sector traffic rather than by bytes used.

What interviewers probe next

  • "Does the same apply to stores?" Yes; a warp's store becomes sector writes, and partial sectors cost a read-modify-write at L2; strided stores are as bad as strided loads.
  • "What about 8-byte and 16-byte loads?" A warp of 16-byte loads wants 512 bytes in 16 sectors; still 100% efficient if contiguous; the ideal sectors per request scales with the access width.
  • "How does the L1 cache change this?" Reuse within a block can hit L1 and avoid L2 traffic, but the sector granularity is the same; a strided pattern with no reuse gets no help.
  • "Can the compiler fix it?" No; the mapping from threads to addresses is the program's; the compiler can vectorize adjacent accesses within a thread, not reorganize across threads.

Common mistakes

  • Mapping threadIdx.x to the row of a row-major matrix.
  • Reading "GPU utilization" or SM activity and concluding the kernel is fine while it fetches 8× the bytes it uses.
  • Fixing coalescing with shared memory and introducing bank conflicts of the same magnitude.
  • Ignoring alignment on hand-computed offsets.

Key takeaways

  • A warp's load becomes sector requests; 4 sectors per 128 bytes is ideal; efficiency = useful bytes ÷ fetched bytes.
  • Stride 2 halves efficiency; stride 32 floats gives 12.5% and an 8× slowdown.
  • Fastest thread index to fastest memory index; swap the mapping or the layout; stage through shared memory when the algorithm needs the other order.
  • Read sectors per request and the requested-versus-transferred ratio in Nsight Compute.
That one was free — and so are 10 answers per topic without an account. Signing in doubles that to 20, opens the Plus lessons in the courses, and remembers which topics you keep getting wrong.no card · Google sign-in · nothing to cancel
HOW DID IT GO?
0
READING SIGNED OUT

Signing in doubles your free answers, from 10 to 20 per topic, and the site starts remembering you: mastery per topic, bookmarks, and a next-focus recommendation. Free, no card.

Sign in free

The concepts behind this question

Ranked by how closely each one overlaps this question's topic, so the first card is the thing to read if the answer above moved too fast.

Core
Kernels & CompilersSign in
Memory CoalescingA warp's 32 threads issue one memory request together, and the hardware serves it in 32-byte sectors. Coalescing is arranging addresses so those sectors are full of bytes the warp will use. It decides whether a bandwidth-bound kernel moves at the HBM rate or at an eighth of it, and it is the pattern NVIDIA's trace-classification interview question tests.
Advanced
Kernels & Compilers🔒 Premium
Profiling with NsightNsight Systems answers where wall-clock time goes across CPU, kernels and copies; Nsight Compute answers why one kernel is slow, from hardware counters. The skill interviewers test is the order: timeline first, then the Speed of Light section, then the two or three metrics that name the bottleneck, so that a memory-bound kernel is recognized from its profile in under a minute and the fix is bytes, not occupancy.
Foundational
🧩 GPU & Accelerator Architecture
GPU Execution ModelA GPU hides memory latency with parallelism instead of caches: thousands of threads in flight, scheduled in warps of 32, pinned to streaming multiprocessors that switch between warps for free whenever one stalls. Every performance conversation in an AI infra loop, from occupancy to why decode is slow, rests on this one mechanism.
Foundational
Kernels & Compilers
Kernel FusionAn elementwise or reduction kernel does a few FLOPs per byte and runs at HBM speed, so a chain of five of them costs five trips through HBM for work that needs one. Fusion collapses the chain into a single kernel that keeps intermediates in registers. It is the first lever for anything memory-bound, and knowing what it cannot fix (weight reads in decode, the GEMMs themselves) is what the interview is really testing.
UP NEXT ON YOUR JOURNEY
FEDITOR'S NOTE

Scored on explaining coalescing as sectors per warp request with the numbers for contiguous and strided cases, on the row-versus-column matrix example, and on naming the profiler metrics that expose it.

DISCUSSION · 0

No comments yet — be the first to share your approach.