Same matrix, two layouts, 6× bandwidth difference
A 2-D matrix doesn't actually live as 2-D in memory — RAM is one big 1-D array. The choice of how to flatten it changes which threads can read it efficiently.
- Row-major (C order) — addresses march along a row:
M[i][j]andM[i][j+1]are adjacent in memory. Default in C/C++, Python/NumPy, PyTorch, MLX, Metal. - Column-major (Fortran order) — addresses march down a column:
M[i][j]andM[i+1][j]are adjacent. Default in Fortran, MATLAB, R, and (because BLAS was specified in 1979) cuBLAS.
This matters because GPUs read memory in coalesced chunks: when 32 neighboring threads in a warp ask for 32 contiguous addresses, the hardware fetches them in one transaction. If those threads stride non-contiguously, the hardware needs many transactions to satisfy them — bandwidth tanks.
Concrete number from a 4096×4096 FP32 sum on RTX 4090:
- Row-major, threads scan rows (coalesced): 610 GB/s
- Column-major, threads scan rows (strided): 105 GB/s
That's a 6× swing caused entirely by memory access pattern.
Roofline: combining bandwidth + compute into one ceiling
The roofline model says: achievable performance is bounded by
min(peak FLOPs, peak bandwidth × arithmetic intensity).
Plot the kernel's intensity on the X axis and its achieved FLOPs on the Y axis; you're either under the slanted bandwidth ceiling (memory-bound, low intensity) or under the flat compute ceiling (compute-bound, high intensity). Knowing which side of the roofline knee you're on tells you which optimization to chase.