Hand-lower matmul to scf.for
Hand-lowering a fixed 4x4x4 linalg.matmul to scf.for exposes exactly what the generic lowering must reproduce.
The nest the pass writes
mlir/matmul4.mlir holds a fixed 4x4x4 linalg.matmul on memrefs. Write the nest it must become before running any pass:
func.func @matmul_4x4x4(%A: memref<4x4xf32>, %B: memref<4x4xf32>, %C: memref<4x4xf32>) {
%c0 = arith.constant 0 : index
%c1 = arith.constant 1 : index
%c4 = arith.constant 4 : index
scf.for %i = %c0 to %c4 step %c1 {
scf.for %j = %c0 to %c4 step %c1 {
%init = memref.load %C[%i, %j] : memref<4x4xf32>
%acc = scf.for %k = %c0 to %c4 step %c1 iter_args(%sum = %init) -> (f32) {
%a = memref.load %A[%i, %k] : memref<4x4xf32>
%b = memref.load %B[%k, %j] : memref<4x4xf32>
%p = arith.mulf %a, %b : f32
%s = arith.addf %sum, %p : f32
scf.yield %s : f32
}
memref.store %acc, %C[%i, %j] : memref<4x4xf32>
}
}
return
}
Three details carry the meaning. The k loop carries the accumulator through iter_args, so each element of %C is read once before its k loop and written once after it. That read exists because linalg.matmul accumulates into its outs operand. And each subscript list is that operand’s indexing map read off in order: (i, k) for %A, (k, j) for %B, (i, j) for %C.
Diff against the machine
Lower the same op with the stock pass and compare:
mlir-opt matmul4.mlir --convert-linalg-to-loops
The printed nest should match yours up to SSA numbering. Where it does not, the difference is the part of the lowering you had not understood, and it is cheaper to find here than in a disassembly three passes later. Confirm the pass name on your build with mlir-opt --help and record it in VERSIONS.md.
The hand version is a specification, not an optimisation: the pass erases the matmul, so nothing downstream can still recognise it. That is why every tiling decision has to happen before this lowering runs.
Things that break
Feed the pass tensors and it exits zero having lowered nothing, because convert-linalg-to-loops requires buffer semantics. If the nest matches the pass but the numbers disagree with the scalar kernel at 4x4, suspect the operands rather than the lowering: swapping the %A subscripts computes A transposed, which agrees only when A is symmetric, so test with inputs that are not.
Sources
- MLIR linalg dialect documentationdocs · LLVM 19
- MLIR pass referencedocs · LLVM 19
- MLIR: using mlir-optdocs · LLVM 19
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
You drop the `iter_args` and load and store `%C` inside the `k` loop. The result is still numerically correct. What did you lose?
`mlir-opt --convert-linalg-to-loops` leaves a `linalg.matmul` on `tensor<4x4xf32>` untouched. Why?
The hand-written nest and the pass output match exactly. What does that prove?