Phase 1, week 4retrospective

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

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?

0 / 3 correct

← back to the map