Tiling the iteration space
Tiling splits a loop nest into outer tile loops and inner tiles; non-dividing shapes need ceilDiv and clamped tails.
One nest, two levels
Tiling splits the iteration space. Outer loops walk tile coordinates, and the body is the same op over one tile:
[M, N, K] -> [M/BM, N/BN, K/BK] outer loops, [BM, BN, BK] inner op
On memrefs, a 4x4x4 matmul tiled by 2 in M and N:
func.func @matmul_tiled(%A: memref<4x4xf32>, %B: memref<4x4xf32>, %C: memref<4x4xf32>) {
%c0 = arith.constant 0 : index
%c2 = arith.constant 2 : index
%c4 = arith.constant 4 : index
scf.for %i = %c0 to %c4 step %c2 {
scf.for %j = %c0 to %c4 step %c2 {
%sA = memref.subview %A[%i, 0] [2, 4] [1, 1]
: memref<4x4xf32> to memref<2x4xf32, strided<[4, 1], offset: ?>>
%sB = memref.subview %B[0, %j] [4, 2] [1, 1]
: memref<4x4xf32> to memref<4x2xf32, strided<[4, 1], offset: ?>>
%sC = memref.subview %C[%i, %j] [2, 2] [1, 1]
: memref<4x4xf32> to memref<2x2xf32, strided<[4, 1], offset: ?>>
linalg.matmul ins(%sA, %sB : memref<2x4xf32, strided<[4, 1], offset: ?>>,
memref<4x2xf32, strided<[4, 1], offset: ?>>)
outs(%sC : memref<2x2xf32, strided<[4, 1], offset: ?>>)
}
}
return
}
The loops are the tile grid, and the subviews keep the parent’s strides, so the inner op reads a window of the same buffers.
What the split buys and costs
The payoff is reuse at the level you target: a B tile loaded once serves every row of the current A tile, and A is reused down the columns. The costs are loop overhead per tile, address arithmetic per subview, and tail handling for shapes the tile does not divide. Shrinking a tile raises reuse until the working set fits, and raises overhead at the same time, so the useful region is found by measurement. K tiling is legal only because outs is read-modify-write, so successive K tiles accumulate into %C.
Things that break
A tile that does not divide the shape is the standard failure. memref.subview %A[3, 0] [3, 4] [1, 1] asks for a row past the end, and the wrong answer often looks like a correct one because the read stays inside the allocation. With static shapes, choose divisors or clamp the extent with affine.min and accept a dynamic inner shape. Declaring the subview as memref<2x2xf32> fails verification: it inherits stride 4 and a dynamic offset, and that strided type is what the inner op must be written against.
Sources
- MLIR linalg dialect documentationdocs · LLVM 19
- Linalg dialect rationaledocs · LLVM 19
- TVM: An Automated End-to-End Optimizing Compiler for Deep Learningpaper
A version records the documentation the claim was checked against, not a house pin. Confirm flags with --help on the release you installed.
Check yourself
Tiling `M`, `N` and `K` by `[2, 2, 2]` produces what?
A 3x3 tile on a 4x4 matmul is wrong unless you clamp. Why?
What does a tile size actually trade away?