Part P · Mapping › The mapping problem, stated precisely

Module 5·Part P — Mapping·18 min

The mapping problem, stated precisely

A mapping is four decisions: tiling, permutation, spatial assignment, binding. Which makes arithmetic intensity a property of the mapping rather than the algorithm — and makes the search space astronomically large.

The core mental model

Every workload in this series is a perfectly nested loop over an iteration space, with tensors read and written through affine index expressions. A GEMM is three loops over (m,n,k)(m, n, k); a 2D convolution is seven over (n,k,c,p,q,r,s)(n, k, c, p, q, r, s); attention is a GEMM, a softmax and another GEMM. The iteration space says what must be computed and says nothing whatever about order, placement or parallelism — those are free, subject only to the dependences. A mapping is a choice of all of them, and it decomposes cleanly into exactly four decisions:

  1. Tiling — how each loop is split into a factor per memory level. A loop of extent 1024 becomes 4×8×324 \times 8 \times 32 across DRAM, SRAM and registers. The tile factors determine the working set at each level, which must fit its capacity.
  2. Permutation — the loop order within each level. Which loop is outermost decides which tensor is re-fetched and which stays put, so permutation is where reuse is actually decided.
  3. Spatial assignment — which loop dimensions are unrolled across parallel hardware rather than iterated in time. This is what maps onto PE arrays, warps, cores and devices.
  4. Binding — which tensor lives at which level, and for how long. Weights resident in SRAM, activations streaming, partial sums in registers.

Everything a compiler, a kernel author, or a distributed-training strategy does is a choice of these four. torch.compile picking a fusion group and a tile size is a mapping. FlashAttention is a mapping. Tensor parallelism is a mapping, with the interconnect as the memory level. Once you have this vocabulary the field stops being a list of tricks.

The consequence worth putting in bold: arithmetic intensity is a property of the mapping, not of the algorithm. A GEMM performs 2MNK2MNK FLOPs no matter what, but the bytes it moves through any given level depend entirely on the tile sizes chosen at that level — from O(MNK)O(MNK) if you re-fetch everything to O(MN+MK+NK)O(MN + MK + NK) if it all fits. This is why the roofline picture, taken alone, misleads: it invites you to read operational intensity off the algorithm, when in fact it is the output of a design decision you have not made yet. Move the x-axis position, and you have changed which resource binds.

And the reason this is hard rather than merely tedious: the space of legal mappings is enormous. For a single convolution layer the count of (tiling × permutation × spatial × binding) choices routinely exceeds 101010^{10}, the objective is non-convex with hard cliffs at capacity boundaries, and the evaluation function is either an approximate analytic model or an expensive measurement. That is Module 8. This module is about what is being searched.

.11101001k1101001karithmetic intensity — FLOP per byteachieved TFLOP/sridge 295989 TFLOP/s peak3.35 TB/smemory-boundcompute-boundtilenaive — nothing residenttiled, T = 128everything resident
One GEMM, three mappings, on an H100. Identical arithmetic — 2N³ FLOPs in every case. Only the tile size changed, and it moved the operation across the entire plot, from hopelessly memory-bound to comfortably compute-bound. This is why intensity cannot be read off an algorithm.

The design space, quantified

The notation used throughout the rest of the series. A GEMM C[M,N]=A[M,K]×B[K,N]C[M,N] = A[M,K] \times B[K,N], mapped to three levels:

for m2 in range(M2):              # DRAM level  (temporal)
  for n2 in range(N2):
    for k2 in range(K2):
      for m1 in range(M1):        # SRAM level  (temporal)
        for n1 in range(N1):
          parallel_for k1 in range(K1):   # spatial: across PEs
            for m0 in range(M0):  # register level
              for n0 in range(N0):
                C[m,n] += A[m,k] * B[k,n]

with M=M2M1M0M = M2 \cdot M1 \cdot M0 and so on. The mapping is the tuple of factors, the loop order at each level, which loops carry parallel_for, and the binding of AA, BB, CC to levels.

What each decision controls:

DecisionControlsConstrained by
Tile factorsworking set per level, reuse depthcapacity at each level
Permutationwhich tensor is re-fetcheddependences (reductions last)
Spatial assignmentutilization of the arraynative operation shape (Module 2)
Bindingtraffic between levelscapacity, and lifetime

The size of the space, for one modest convolution layer:

FactorCount (order)
Tile factorisations, 7 loops × 3 levels10610^{6}10810^{8}
Permutations, 7! per level, 3 levelsup to 101010^{10}
Spatial assignment choices10110^{1}10210^{2}
Bindings10110^{1}10210^{2}
Legal mappings after pruning10810^{8}101210^{12}

Traffic bounds for GEMM at one level of capacity CC, which is where the whole thing pays off:

Mapping qualityDRAM trafficIntensity
No reuse (naive)O(MNK)O(MNK)O(1)O(1)
One operand residentO(MNK/T)O(MNK/T)O(T)O(T)
Square tiles at capacity CCO(MNK/C)O(MNK/\sqrt{C})O(C)O(\sqrt{C})
Everything fitsO(MN+MK+NK)O(MN + MK + NK)O(min(M,N,K))O(\min(M,N,K))

Critical thinking

Why is arithmetic intensity a property of the mapping, and what breaks if you forget?

Because intensity is FLOPs divided by bytes moved, and only the numerator is fixed by the algorithm.

Take C=ABC = AB with all matrices N×NN \times N. FLOPs are 2N32N^3, always. Bytes through DRAM depend entirely on tiling:

  • Naive triple loop, nothing resident. Each of the N3N^3 multiply-accumulates fetches its operands. Traffic 3N34\sim 3N^3 \cdot 4 bytes. Intensity 1/6\approx 1/6 FLOP per byte. Hopelessly memory-bound.
  • Tile with square tiles of side TT that fit in capacity CC, so 3T2C3T^2 \le C. Each tile of CC is computed from N/TN/T pairs of tiles, and total traffic is 2N3/T4\sim 2N^3/T \cdot 4 bytes. Intensity T/4\approx T/4. With T=128T = 128, that is 32 FLOP/byte — a hundred-fold improvement from a decision that changed no arithmetic whatsoever.
  • Everything fits on-chip. Traffic 3N243N^2 \cdot 4. Intensity N/6\approx N/6, unbounded in NN.

Three mappings, one algorithm, intensities spanning several orders of magnitude. On a roofline plot these are three different points, one memory-bound and one compute-bound, for identical code.

What breaks if you forget:

  • You misdiagnose. “This operation is memory-bound” leads to buying bandwidth, when the actual problem was a tile size and the fix was free. This is the most common expensive mistake in the field.
  • You provision hardware wrongly. If you size a chip’s bandwidth from the intensity of today’s mappings, and the compiler subsequently improves, you have bought bandwidth that is now idle — or, worse, if you assumed mappings your compiler never achieves, a permanent ceiling. Module 8’s cost model is the only defence, and Module 14 is what happens when it is wrong.
  • You mis-attribute wins. FlashAttention is routinely described as an algorithmic breakthrough. It performs the same FLOPs as standard attention. What it changed was the binding of the S×SS \times S score matrix — from HBM to SRAM — plus the tiling that makes the softmax reduction streamable. It is a mapping result, and calling it an algorithm result obscures that the same technique generalises to any operation with a materialised intermediate.

The honest statement of roofline is therefore: it is a diagram of the hardware, on which a mapping is a point. The hardware fixes the roof and the slope. You choose where on the x-axis to stand.

Walk through why permutation, not tiling, is where reuse is decided.

Because tile sizes set how much can be reused, and loop order sets what actually is.

Consider a GEMM tile level holding tiles of AA, BB and CC, and ask what happens as the outer loops advance. The innermost loop of the enclosing level determines which tensor’s tile changes most often, and a tile that changes on every iteration must be re-fetched:

  • k innermost (order m, n, k): C[m,n]C[m,n] is invariant across the inner loop, so the output tile stays in registers and accumulates. AA and BB tiles both change every step. This is output-stationary — partial sums never leave the accumulator, which matters because in a reduction psums are the highest-traffic tensor.
  • n innermost (order k, m, n): A[m,k]A[m,k] is invariant, so the AA tile is reused across the inner loop while CC tiles are read, updated and written back each step. This is input-stationary, and it is usually worse, because you have converted psum traffic — the thing you most want to avoid — into memory traffic.
  • m innermost (order k, n, m): B[k,n]B[k,n] is invariant. Weight-stationary in the neural network reading, where BB is the weight matrix. Excellent when the weight tile is expensive to load and reused across many rows of activations, which is precisely the large-batch case.

Same tile sizes in all three. Same capacity used. Radically different traffic — and note that the choice of which is best depends on the shape: weight-stationary wins when MM is large (many rows amortise the weight load), output-stationary wins when KK is large (long reduction, so psum traffic would dominate). That shape dependence is why one fixed choice cannot be right for everything, which is Module 6’s argument about dataflow.

Two refinements:

  • Permutation is constrained by dependences. A reduction loop cannot be moved outside the loops that produce its partial sums without materialising them, which is exactly the trade: hoisting k outward turns register accumulation into memory traffic. Legality and cost are entangled here in a way they are not for tiling.
  • Permutation and tiling interact, so they cannot be chosen sequentially. The best permutation depends on the relative tile sizes (whichever tensor’s tile is largest is the one you least want to re-fetch), and the best tile sizes depend on which tensor you decided to keep resident. This coupling is the main reason the mapspace cannot be decomposed into independent subproblems and greedy search does badly.

If the mapspace has ten billion points, how does any compiler ship?

By exploiting structure rather than by searching harder — and the structures used are worth knowing, because each is a claim about the problem that is true in ML and false in general.

Prune to legality and non-dominance first. Most of the raw space is illegal (tiles exceeding capacity) or trivially dominated (permutations differing only in unit-extent loops, which change nothing). Timeloop’s linear_pruned search does exactly this, and the reduction is orders of magnitude before any evaluation happens.

Use the closed forms where they exist. For a plain GEMM you do not search: the optimal tiling is known analytically — square tiles at C/3\sqrt{C/3} — and the residual search is over a handful of hardware-specific alignment choices. A great deal of production kernel code is closed form plus a small tuned table, not search.

Restrict the space by construction. This is the big one, and it is a design decision rather than an algorithm. Fix the dataflow in hardware and one whole dimension of the mapspace disappears — a systolic array is weight-stationary, so no permutation choice exists at that level. Templates like CUTLASS expose a few dozen parameterised tile shapes rather than the full space. Triton exposes block sizes and stages, not arbitrary loop nests. Every one of these trades reachable performance for a tractable search, and mostly the trade is good because the space is heavily dominated by a small number of good regions.

Then search what is left, by one of two methods with opposite properties:

Analytic (Timeloop, MAESTRO)Empirical (Ansor, Triton autotune)
Evaluationmodel, microsecondsmeasurement, milliseconds to seconds
Points explored10510^{5}10710^{7}10310^{3}10410^{4}
Accuracysystematically wrong in known waysexact, for that machine
Works on hardware that does not existyesno

That last row is the codesign-specific point and it is the reason analytic models matter far more here than in ordinary compiler work. When you are designing a chip, measurement is not available — the silicon is two years away — so the mapping study that sizes your SRAM and your bandwidth is run entirely on a model. The quality of that model is therefore the quality of your chip, and errors in it are discovered after tape-out. Module 8 is about how much to trust it.

The pragmatic summary of how this actually goes in industry: closed forms and hand-written templates cover the dominant operations, autotuning fills in the shapes those miss, and full mapspace search is used mostly by hardware architects rather than by production compilers. Which is itself a comment on the state of the field — the search problem is not solved, it is avoided, by restricting the space until the answer is obvious.

Which of the four decisions is hardest to change after tape-out, and what follows?

Spatial assignment, because the array’s shape and dataflow are cast in RTL, and everything else must then bend around them.

Rank them by how late they can be changed:

DecisionFixed whenChangeable by
Tile factorscompile time, or runtimerecompiling; often a tuning parameter
Bindingcompile timerecompiling
Permutationcompile time — if the hardware allowsrecompiling, where not fixed in RTL
Spatial assignmenttape-outnothing

A 128×128 systolic array has decided that two loop dimensions are unrolled spatially, in a fixed pattern, with a fixed operand flow, forever. If your workload’s natural parallel dimensions do not match — a GEMV, a depthwise convolution where the channel dimension does not reduce, a small expert in an MoE — you cannot re-map your way out. You pad and waste. That is why Module 2’s filling condition is a hardware property that software inherits rather than a tuning problem.

What follows, and this is the design rule that Module 14 closes on:

  • Fix the numbers, keep the mapping soft. Capacities, bandwidths and array sizes must be committed; the mapping should not be. Any decision you can defer to the compiler is one you can revise after learning what the workload actually became.
  • Flexibility costs area, and the price is knowable. Configurable-dataflow designs (Eyeriss v2’s flexible NoC, and every “reconfigurable” accelerator) spend area on interconnect and control to keep permutation choices alive. Typically 10–30% overhead. Whether that is worth it is a bet on workload drift over the chip’s life, and the historical record — conv nets to transformers to MoE in under a decade — argues it usually is.
  • The array shape should be chosen for the worst workload you must not fail on, not the best one you hope to run. A 256×256 array doubles peak and doubles the shape penalty on anything narrow, which is why the trend toward larger arrays makes decode and small-batch inference structurally harder even as the datasheet number improves.

This is the same three-part rule as ML Hardware Module 12 — put a function in hardware if it is on the critical path, stable over months, and bounded in time — applied to a mapping decision rather than a function. Spatial assignment fails the stability test more often than teams expect.

Self-check

What are the four decisions that constitute a mapping?

Tiling (how each loop splits into a factor per memory level, setting the working set), permutation (loop order at each level, which decides what is re-fetched), spatial assignment (which dimensions unroll across parallel hardware rather than iterating in time), and binding (which tensor lives at which level, for how long). Every compiler decision, kernel, and parallelism strategy is a choice of these four.

Show that arithmetic intensity belongs to the mapping, using GEMM.

2N32N^3 FLOPs regardless. Naive with nothing resident: 3N3\sim 3N^3 words of traffic, intensity 1/6\approx 1/6 FLOP/byte. Tiled at side TT: traffic 2N3/T\sim 2N^3/T words, intensity T/4\approx T/4 — 32 FLOP/byte at T=128T = 128. Everything resident: traffic 3N23N^2, intensity N/6\approx N/6. Three points spanning orders of magnitude on a roofline plot, for identical arithmetic. Roofline draws the hardware; the mapping picks the point on the x-axis.

Why is permutation where reuse is actually decided?

Tiling sets how much can be reused; loop order sets what is. With k innermost, CC is invariant and accumulates in registers — output-stationary, which keeps psums (the highest-traffic tensor in a reduction) out of memory. With m innermost, BB is invariant — weight-stationary, best when MM is large. Same tiles, same capacity, radically different traffic. And the best choice depends on shape, which is why no single fixed dataflow is right for everything.

Name the four ways compilers avoid searching ten billion mappings.

Prune to legality and non-dominance; use closed forms where they exist (square tiles at C/3\sqrt{C/3} for GEMM); restrict the space by construction — fixed dataflow in RTL, CUTLASS templates, Triton’s block-size parameters — which trades reachable performance for tractability; then search the remainder analytically or empirically. In practice full mapspace search is mostly used by hardware architects, not production compilers: the problem is avoided rather than solved.

Which mapping decision is fixed at tape-out, and what design rule follows?

Spatial assignment — the array shape and its operand flow are cast in RTL, so a workload whose natural parallel dimensions do not match cannot be re-mapped, only padded. Tiling, binding and (usually) permutation are compile-time. The rule: fix the numbers, keep the mapping soft. Flexible-dataflow designs cost 10–30% area to preserve permutation choices, and given the conv → transformer → MoE drift within a decade, that bet has usually paid.