FForge
← Labsmlir-lowering-03MLIR to LLVM IR●●○○

The pass manager — pipelines, nesting, options, timing

mlir-lowering/03_pass_manager.mlirneeds: mlir-opt6 steps
0/6 steps
what this lab teaches

A compiler like a production backend IS a pass pipeline. In C++ it looks like (a production backend’s Linalg→LLVM pass): pm.addPass(createCanonicalizerPass()); // runs on the module pm.addNestedPass<func::FuncOp>(createLowerTTXPass()); // runs on EACH func and on the command line the SAME thing is a textual pipeline: --pass-pipeline='builtin.module(canonicalize, func.func(lower-ttx))' Being able to go back and forth between the two is how you reproduce "what the compiler did" with mlir-opt, one pass at a time. Nesting matters because passes are ANCHORED on an op type. A func-level pass runs independently on every function (and in PARALLEL across them — the pass manager multithreads over sibling ops). A module-level pass sees everything but runs once.

evidence to leave with
A pipeline string you wrote by hand that reproduces LOWER_STD, plus its timing tree
walk it, one step at a time
run this
$ mlir-opt 03_pass_manager.mlir --canonicalize --convert-scf-to-cf --cse --dump-pass-pipeline -o /dev/null 2>&1
what it printed on the machine that wrote this lab
Pass Manager with 3 passes:
builtin.module(
  canonicalize{cse-between-iterations=false    max-iterations=10 max-num-rewrites=-1 region-simplify=normal test-convergence=false top-down=true},
  convert-scf-to-cf{allow-pattern-rollback=true},
  cse
)

The "--foo --bar" flag form is sugar for builtin.module(foo, bar). The dump shows every option with its value: paste it into --pass-pipeline to replay EXACTLY. When a colleague says "it works for me", compare dumps.

run this
$ mlir-opt 03_pass_manager.mlir --pass-pipeline='builtin.module(affine-loop-unroll)' -o /dev/null 2>&1 | head -1
what it printed on the machine that wrote this lab
<unknown>:0: error: unable to schedule pass 'AffineLoopUnroll' on a PassManager intended to run on 'builtin.module'!
run this
$ mlir-opt 03_pass_manager.mlir --pass-pipeline='builtin.module(func.func(affine-loop-unroll))' -o /dev/null && echo scheduled-ok
what it printed on the machine that wrote this lab
scheduled-ok
affine-loop-unroll is anchored on func.func. You must say "for each
function inside the module, run it". This is the #1 error when you
translate a C++ `addNestedPass<func::FuncOp>(...)` into a string.
run this
$ mlir-opt 03_pass_manager.mlir --pass-pipeline='builtin.module(func.func(canonicalize{max-iterations=1}), cse)' --dump-pass-pipeline -o /dev/null 2>&1
what it printed on the machine that wrote this lab
Pass Manager with 2 passes:
builtin.module(
  func.func(
    canonicalize{cse-between-iterations=false    max-iterations=1 max-num-rewrites=-1 region-simplify=normal test-convergence=false top-down=true}
  ),
  cse
)
run this
$ mlir-opt 03_pass_manager.mlir --pass-pipeline='builtin.module(canonicalize{max-iteratons=1})' 2>&1 | head -1
what it printed on the machine that wrote this lab
<unknown>:0: error: <Pass-Options-Parser>: no such option max-iteratons
Options are {key=value key2=value}, space separated. Find them with
`mlir-opt --help | grep -A12 -- '--canonicalize '`. production backends
passes have options too (tile sizes, enable flags): these are the knobs
you'll turn during perf work, so learn to set them from the command line.
run this
$ mlir-opt ../01_basics/04_first_run.mlir --pass-pipeline="$LOWER_STD" --mlir-timing -o /dev/null 2>&1 | sed -n '6,14p'
what it printed on the machine that wrote this lab
  ----User Time----  ----Wall Time----  ----Name----
    0.0027 ( 21.5%)    0.0027 ( 23.3%)  Parser
    0.0009 (  7.0%)    0.0009 (  7.6%)  OneShotBufferizePass
    0.0010 (  7.6%)    0.0005 (  4.5%)  'func.func' Pipeline
    0.0009 (  7.3%)    0.0005 (  4.4%)    ConvertLinalgToLoopsPass
    0.0003 (  2.2%)    0.0003 (  2.3%)  ConvertVectorToSCF
    0.0004 (  2.9%)    0.0004 (  3.1%)  SCFToControlFlowPass
    0.0002 (  1.8%)    0.0002 (  2.0%)  ExpandStridedMetadataPass
    0.0002 (  1.3%)    0.0002 (  1.4%)  LowerAffinePass

Compile time matters for Triton (JIT compile on first call). On big models the tree view (--mlir-timing-display=tree) shows which nested pipeline is slow. The `(A)` rows are ANALYSES, computed on demand and cached until a pass invalidates them.

run this
$ mlir-opt 03_pass_manager.mlir --pass-pipeline="builtin.module(func.func(convert-linalg-to-loops), convert-scf-to-cf)" --mlir-print-ir-after=convert-scf-to-cf -o /dev/null 2>&1 | head -4
what it printed on the machine that wrote this lab
// -----// IR Dump After SCFToControlFlowPass: convert-scf-to-cf{allow-pattern-rollback=true} //----- //
module {
  func.func @scale(%arg0: memref<16xf32>, %arg1: f32) {
    %c0 = arith.constant 0 : index
--mlir-print-ir-before=PASS / --mlir-print-ir-after=PASS / -all variants
are your microscope. Track 06_debugging is built on them.

A production vendor backend's Linalg->LLVM pass is ONE function with about 126 addPass/addNestedPass calls in it, many behind `if (options.X)` guards (those guards are the on/off switches for perf experiments). Count them in whichever backend you have checked out: grep -cE 'addNestedPass<func::FuncOp>|addPass\(' <its LinalgTo*Pass.cpp>

run this
$ grep -nE 'void add[A-Za-z0-9]*PassPipeline' $IREE_LLVMCPU/Passes.cpp
what it printed on the machine that wrote this lab
170:void addMultiTilingExpertPassPipeline(
270:void addConvTileAndDecomposeExpertPassPipeline(
328:void addMmt4dTilingExpertPassPipeline(
477:void addCPUDefaultPassPipeline(OpPassManager &funcPassManager,

A vendor backend: one long hand-written pipeline, with `if (options.X)` guards (those guards are the on/off switches for perf experiments). IREE: SEVERAL pipelines, and a strategy-selection pass (KernelDispatch.cpp) picks one PER DISPATCH based on the op (matmul? conv? elementwise?) and the CPU features. That "pick a pipeline per kernel" idea is what a backend’s pass-ordering plan turns into.

take it further

1. Run LOWER_STD with --mlir-timing-display=tree. 2. Add --mlir-disable-threading and compare timing on a file with 50 functions (generate one with a shell loop). When is threading a win? 3. Translate a production backend’s Linalg→LLVM pass lines 199-230 into a --pass-pipeline string using only UPSTREAM passes that exist in mlir-opt (skip the vendor-specific ones). Does it parse? ============================================================

read the source · mlir-lowering/03_pass_manager.mlir
func.func @scale(%x: memref<16xf32>, %s: f32) { linalg.generic {indexing_maps = [affine_map<(i) -> (i)>], iterator_types = ["parallel"]} outs(%x : memref<16xf32>) { ^bb0(%v: f32): %m = arith.mulf %v, %s : f32 linalg.yield %m : f32 } return }
◇
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.