FForge
← Labsmlir-debugging-01Debugging MLIR pipelines●●○○

"Which pass did that?" in one run

mlir-debugging/01_find_the_pass.mdneeds: mlir-opt (an assertions build for `-debug-only`) | **TOOL:** `tools/who_did_it.sh`
evidence to leave with
The single pass that introduced a given IR change, named and proved with its before/after diff

DEBUGGING LAB #01 — "Which pass did that?" in one run

TRACK: debugging | DIFFICULTY: 2 | NEEDS: mlir-opt (an assertions build for -debug-only) | TOOL: tools/who_did_it.sh

What this teaches

~/debug_lab/passwork bisected LLVM's 201 passes with -opt-bisect-limit. MLIR has no bisect limit, but it has something better: it can write the IR after every pass to a directory tree, one file per pass, in order:

mlir-opt IN.mlir --pass-pipeline=... --mlir-print-ir-after-all --mlir-print-ir-tree-dir=DIR

tools/who_did_it.sh REGEX <mlir-opt command> does that and prints, in pipeline order, how many lines match REGEX after each pass. The first row where the count changes names the pass. One run, no bisection, any property you can grep.

STEP 1 — Who put memref.alloca inside the SVE k loop? (lab 05_aarch64/02, failure 3)

$ ../tools/who_did_it.sh 'memref.alloca' mlir-opt ../05_aarch64/out/sve.t.mlir --pass-pipeline="builtin.module(func.func(convert-linalg-to-loops), canonicalize, cse, convert-vector-to-scf, convert-scf-to-cf)" order after pass matches 0 (input) 0 1 func_func_matmul/0_0_convert-linalg-to-loops.mlir 0 2 builtin_module_no-symbol-name/1_canonicalize.mlir 0 3 builtin_module_no-symbol-name/2_cse.mlir 0 4 builtin_module_no-symbol-name/3_convert-vector-to-scf.mlir 7 5 builtin_module_no-symbol-name/4_convert-scf-to-cf.mlir 7

0 → 7 at convert-vector-to-scf. Then read its options (mlir-opt --help | grep -A5 -- '--convert-vector-to-scf '), find full-unroll, and the fix follows. Nested func.func(...) passes write N_M_ files and restart their numbering, so the tool orders rows by write time (true execution order).

STEP 2 — Who inserted the 64 MiB memref.copy? (lab 04_optimization/02)

$ ../tools/who_did_it.sh 'memref.copy' mlir-opt ../04_optimization/fusion.mlir --pass-pipeline='builtin.module(linalg-fuse-elementwise-ops,one-shot-bufferize{bufferize-function-boundaries function-boundary-type-conversion=identity-layout-map},canonicalize)' order after pass matches 0 (input) 0 1 builtin_module_no-symbol-name/0_linalg-fuse-elementwise-ops.mlir 0 2 builtin_module_no-symbol-name/1_one-shot-bufferize.mlir 1 3 builtin_module_no-symbol-name/2_canonicalize.mlir 1

one-shot-bufferize. It made the copy because it couldn't prove in-place was safe. The follow-up question is why, which lab 03_linalg/02's test-analysis-only print-conflicts answers.

The other print flags, and when to use them

flaguse it when
--mlir-print-ir-after-allsmall input, you want to read everything (stderr, huge)
--mlir-print-ir-after-changecombine with the above: skip passes that changed nothing
--mlir-print-ir-before=PASS / --mlir-print-ir-after=PASSyou already know the suspect
--mlir-print-ir-after-failurea pass fails; print the IR it left behind
--mlir-print-ir-tree-dir=DIRroute all of the above to per-pass files (what who_did_it.sh uses)
--mlir-print-ir-module-scopea func-level dump isn't enough; show the whole module
--mlir-elide-elementsattrs-if-larger=16weights/constants make dumps unreadable
--mlir-print-debuginfomap an op back to its source loc(...) (Triton → Python line)
--mlir-print-op-generica custom printer hides something (lab 01_basics/01)

For Triton, the equivalent switch is MLIR_ENABLE_DUMP=1 (a vendor MLIR backend developer guide), which dumps before every pass of Triton's own pipelines.

What you can NOT do with this build: -debug-only

mlir-opt x.mlir -debug-only=dialect-conversion ... # mlir-opt: Unknown command line argument '-debug-only=dialect-conversion'.

-debug-only=<DEBUG_TYPE> prints a pass's internal reasoning (every pattern tried in a conversion, every fusion candidate rejected). It needs an LLVM built with -DLLVM_ENABLE_ASSERTIONS=ON. Both local builds (build, build-full) have it OFF. For compiler development on a production MLIR backend, make your dev build an assertions build: you get -debug-only, and verifier checks inside passes that catch bugs at the pass instead of three passes later. Keep a Release build for timing.

Try this yourself

  1. Which pass turns vector<8x16xf32> into vector<16xf32> in lab 04's D variant? who_did_it.sh 'vector<8x16xf32>' mlir-opt ... on the pre-lowering file with $LOWER_STD.
  2. Which pass removes the last unrealized_conversion_cast in $LOWER_STD?
  3. Use --mlir-print-ir-after-change with --mlir-print-ir-after-all on $LOWER_STD and count how many passes actually changed lab 01/04's program.
◇
Finish the lab: write the evidence note

A lab is done when you can say what it showed on your machine — not when you have read it.