Learn › Numerical Computing
·FREE·8 min read

The Cost of Matrix Multiplication — Where Almost All of AI's Compute Goes

Why GEMM is everything: the anatomy of O(n³), memory bandwidth and arithmetic intensity, what actually makes a GPU fast, and the intuition behind tiling — ending with you able to estimate a model's training and inference FLOPs yourself.

Key points

  • Modern computers have arithmetic units far faster than memory bandwidth, so runtime depends on how many bytes you move, not just how many operations you perform.
  • Matrix multiplication (GEMM) of m×k by k×n costs 2mkn FLOPs, or 2n^3 for square matrices, and dominates AI compute.
  • Arithmetic intensity I = FLOPs / bytes moved decides memory bound versus compute bound, capping performance at min(peak compute, I × memory bandwidth).
  • GEMM has I ≈ n/6 and improves with size, while a matrix-vector product is fixed at I ≈ 0.5, which is why batching helps.
  • Tiling raises effective intensity to I ≈ T/6, and the estimates are about 2N FLOPs per token for inference, 6N for training, so C ≈ 6ND.

Sources: Attention Is All You Need · cuDNN: Efficient Primitives for Deep Learning · BLAS (Basic Linear Algebra Subprograms) · Parallel Thread Execution ISA (NVIDIA CUDA Documentation) and 2 more (below)

ModalitytextTasknumerical

The metaphor: the machine and the trucks

A factory installs a ferociously fast machine tool: it can cut a thousand parts per second. The trucks delivering raw stock, however, arrive with ten parts per second. This factory's output is set by the number of trucks, not by the machine. The machine stands idle 99% of the time.

Modern computers are that factory. Arithmetic units are ferociously fast; the bandwidth that carries data out of memory has not kept pace. Counting how many operations you perform therefore does not predict runtime. You have to count how many bytes you move at the same time.

And in AI, the overwhelming majority of that time goes into a single operation: matrix multiplication.

Why matrix multiplication is everything

Take a Transformer apart and what falls out is almost entirely matrix products: the projections to Q, K and V, the attention scores, the weighted sum over values, the output projection, and the two linear layers of the FFN. Convolutions, too, are typically lowered to a matrix product via im2col or an equivalent rearrangement.

That is why the library world gave this one operation a dedicated name — GEMM, general matrix multiply — and has been optimizing it for decades. Strip the marketing off "making AI faster" and a large fraction of it is "making GEMM faster".

The anatomy of O(n3)O(n^3)

Multiply an m×km \times k matrix AA by a k×nk \times n matrix BB to get an m×nm \times n matrix CC. One output cell is defined as

Cij=∑p=1kAipBpjC_{ij} = \sum_{p=1}^{k} A_{ip} B_{pj}
(1)

Read in words: "row ii of AA against column jj of BB, multiplied elementwise and summed" — a single dot product. There are mnmn cells, each costing kk multiplications and roughly kk additions. Counting a multiply and an add as one operation each,

FLOPs=2 m k n\text{FLOPs} = 2\,m\,k\,n

which says nothing more exotic than: cells in the output (mnmn), times multiply-adds needed to fill one cell (kk), times two operations per multiply-add.

That is 2n32n^3 for square matrices. Double nn and the work grows eightfold. That is the content of O(n3)O(n^3), and it is also what people mean by a model's "compute".

Numbers give it scale. A square product at n=4096n = 4096 costs about 1.4×10111.4 \times 10^{11} FLOPs — over a hundred billion operations for one multiplication. A Transformer stacks dozens of layers of those and repeats them per token.

It is worth feeling, with your hands, that the atom of all this is a dot product.

FIG 1A dot product collapses one row and one column into a single number. GEMM repeats it m×n times, each of length k

That covers the operation count. But runtime is not determined by 2n32n^3 alone — the trucks from the opening are about to matter.

Arithmetic intensity: how much work per byte delivered

Here is the factory, written as a formula. Define the arithmetic intensity:

I=FLOPs performedbytes moved from memoryI = \frac{\text{FLOPs performed}}{\text{bytes moved from memory}}
(2)

Read in words: the numerator is how much work you did, the denominator is how many bytes you paid to move in order to do it. Think of II as fuel economy for data. It is the ratio of work done to data delivered — how many times you managed to reuse each byte you paid to fetch. An operation with low II stalls on bandwidth no matter how fast the arithmetic units are (memory bound). With high II you get to use the units at full tilt (compute bound). Achievable performance is capped by

performance≤min⁡(peak compute,  I×memory bandwidth)\text{performance} \le \min\big(\text{peak compute},\ \ I \times \text{memory bandwidth}\big)

which says your real speed is pinned by whichever is lower: what the arithmetic units are capable of, or what the bandwidth can keep them fed with. The first term is the machine's ceiling, the second is "bytes per second times work per byte". Plotting those two lines gives you the roofline model.

Run the numbers for GEMM. For square n×nn \times n matrices you move three of them, 3n23n^2 elements, which in fp32 is 12n212n^2 bytes. The work is 2n32n^3, so

I=2n312n2=n6I = \frac{2n^3}{12n^2} = \frac{n}{6}

Put in words, the arithmetic intensity of a matrix product is nothing but its side length divided by six. Intensity grows linearly with nn: the bigger the matrix, the more times each fetched byte gets reused. That is the distinguishing property of matrix multiplication. At n=4096n = 4096, I≈680I \approx 680 — comfortably compute bound.

Contrast a matrix-vector product (GEMV): 2n22n^2 FLOPs against roughly 4n24n^2 bytes, so I≈0.5I \approx 0.5, fixed. Each element of the matrix is used exactly once, leaving nothing to reuse. This is why single-stream LLM token generation is slow and batching helps so dramatically: batching converts GEMV into GEMM and raises II.

What actually makes a GPU fast

Two things: sheer count of arithmetic units, and instructions cut to the shape of a matrix product.

A CPU is built to finish one instruction as quickly as possible, spending enormous silicon area on branch prediction and speculation. A GPU assumes the opposite workload — the same operation applied across a flood of data — so it strips control logic and packs the area with arithmetic units instead. Recent accelerators go further, with dedicated units that perform a small matrix multiply-accumulate as a single instruction. It is a deliberate bet on operations with high II.

Which is where intensity bites back. Multiply your arithmetic units by a thousand and any low-II operation still stalls on bandwidth. A GPU is fast because matrix multiplication has high arithmetic intensity, not because computation in general got faster. That asymmetry drives real implementation decisions.

Tiling: cut the matrix into cache-sized blocks

I=n/6I = n/6 assumed the ideal: AA, BB and CC all resident in fast memory. In reality a 4096×40964096 \times 4096 matrix does not fit in cache. A naive triple loop walks a column of BB on every pass of the inner loop, going out to main memory each time, and the effective intensity collapses to around 0.25.

The fix is tiling (blocking). Cut the matrices into T×TT \times T blocks so that one block each of AA, BB and CC live in fast memory simultaneously. Once they are resident, that block performs T3T^3 operations on 3T23T^2 elements of data. Across the whole matrix this gives

I≈T6I \approx \frac{T}{6}

which says the number governing your fuel economy is no longer the side of the whole matrix, nn, but the side of the block you managed to keep resident, TT. Bigger TT is better, bounded by the capacity of cache — or shared memory on a GPU. What BLAS implementations have polished for decades is, at bottom, exactly this partitioning, applied at several levels down to registers. The algorithm stays O(n3)O(n^3) while data movement alone changes runtime by factors of several to an order of magnitude — the most blatant instance of the "outside the complexity class" effects from the complexity article.

There are also ways to attack the O(n3)O(n^3) itself. Strassen's algorithm uses divide and conquer to reach O(n2.807)O(n^{2.807}) and does get used for very large matrices, at some cost in numerical stability. The theoretical exponent has been pushed below 2.4, but with constants so vast that those are "galactic algorithms" — provably better, never run.

Estimating FLOPs yourself

This is the destination. First, a tool.

def gemm_cost(m, k, n, bytes_per_elem=4):
    flops = 2 * m * k * n                          # one multiply-add = 2 FLOP
    moved = (m * k + k * n + m * n) * bytes_per_elem
    return flops, flops / moved                    # (FLOPs, arithmetic intensity)

print(gemm_cost(4096, 4096, 4096))   # → intensity 682.7 FLOP/byte
print(gemm_cost(4096, 4096, 1))      # → intensity   0.5 FLOP/byte

Same matrix AA, and the intensity differs by more than a factor of a thousand depending on whether the other operand is a matrix or a vector. The value of batching falls straight out of the arithmetic.

For a whole model, estimate from the parameter count NN. Each parameter contributes a multiply and an add, so a forward pass costs about 2N2N FLOPs per token. Training adds a backward pass that computes gradients with respect to both inputs and weights, costing roughly twice the forward, for a total of about 6N6N FLOPs per token (backpropagation). Training on DD tokens therefore costs

C≈6NDC \approx 6 N D
(3)

which says, in words: about six operations for every pairing of one parameter with one token. The bill for a training run is essentially model size NN multiplied by how much text DD you push through it. This approximation ignores attention's s2s^2 term and normalization, and holds while dmodel>s/12d_{\text{model}} > s/12, i.e. while the sequence length ss stays below about 12 dmodel12\,d_{\text{model}}. At long context the attention term stops being negligible — remember that caveat.

Put numbers in. With N=7×109N = 7 \times 10^9 and D=1012D = 10^{12} tokens, C≈4.2×1022C \approx 4.2 \times 10^{22} FLOPs. On a setup sustaining 101510^{15} FLOP/s — one PFLOP/s — that is 4.2×1074.2 \times 10^{7} seconds, about 1.3 years. One line of arithmetic explains why training runs are spread across hundreds or thousands of accelerators.

Four things that matter in practice

1. Suspect the batch size When inference is slow, look at II first. Batch-1 generation is GEMV and memory bound, so adding compute does nothing. Batching is the only lever that works.

2. Track effective FLOPs over peak FLOPs Back out effective performance from measured time and divide by peak. A low ratio tells you the units are idling, and that the problem is data movement rather than arithmetic.

3. Lowering precision is buying bandwidth Moving from fp32 to fp16 or int8 does not only speed up arithmetic — it halves or quarters the bytes moved, which raises II. That is why quantization pays off most in memory-bound regimes.

4. Do not write your own GEMM Tiling, prefetching and register allocation carry decades of accumulated tuning inside existing BLAS implementations. What you should be shaping is the form in which you call GEMM.

Summary

Next: how this computation gets split across many units — the limits of parallelism and the communication cost of distributed training.

Sources

  1. Attention Is All You Need. arXiv:1706.03762Open(retrieved 2026-10-20)
  2. cuDNN: Efficient Primitives for Deep Learning. arXiv:1410.0759Open(retrieved 2026-10-20)
  3. BLAS (Basic Linear Algebra Subprograms). netlib.orgOpen(retrieved 2026-10-20)
  4. Parallel Thread Execution ISA (NVIDIA CUDA Documentation). docs.nvidia.comOpen(retrieved 2026-10-20)
  5. Powers of Tensors and Fast Matrix Multiplication. arXiv:1401.7714Open(retrieved 2026-10-20)
  6. Scaling Laws for Neural Language Models. arXiv:2001.08361Open(retrieved 2026-10-20)

Definitions and figures in this article were checked against the primary sources above. Where they differ, the source is authoritative.

Published by TechAthletes · Editorial policy · How we cite · Corrections

Comments

Sign in to comment