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

The dialect-conversion framework — and the 4 ways a lowering breaks

mlir-lowering/02_conversion_framework.mlirneeds: mlir-opt, mlir-translate; llvm-project source to read5 steps
0/5 steps
what this lab teaches

Every `convert-X-to-Y` pass is built from the same three parts. Once you see them you can read ANY conversion pass — upstream, a vendor MLIR backend's a vendor memory dialect’s conversion, IREE's `ConvertToLLVM.cpp`, or a production MLIR backend's: 1. ConversionTarget — which ops are LEGAL when we're done (LLVMConversionTarget = "only llvm.* is legal"). 2. TypeConverter — how types map (index -> i64, memref -> descriptor struct, vector<[4]xf32> -> <vscale x 4 x float>). 3. RewritePatterns — one per op: "arith.addf -> llvm.fadd". and one driver call: applyPartialConversion(op, target, patterns) leftovers are ALLOWED (almost every upstream pass) applyFullConversion(op, target, patterns) leftovers are an ERROR ("failed to legalize operation") Read the real thing (paths relative to $LLVM_SRC/mlir): lib/Conversion/ArithToLLVM/ArithToLLVM.cpp:92 AddFOpLowering = a one-line pattern arith.addf -> llvm.fadd, with AttrConvertFastMathToLLVM (THIS is how fastmath<reassoc> became `fadd reassoc` in lab 01) lib/Conversion/ArithToLLVM/ArithToLLVM.cpp:808 the pass: target + type converter + patterns lib/Conversion/ArithToLLVM/ArithToLLVM.cpp:821 applyPartialConversion include/mlir/Transforms/DialectConversion.h:1078 class ConversionTarget Because upstream passes are PARTIAL, mlir-opt succeeds even when your pipeline is wrong. The failure shows up LATER, in mlir-translate, far from the cause. That's the most confusing thing about MLIR lowering for newcomers; this lab makes you see all four shapes of it once.

evidence to leave with
Each of the four lowering failures reproduced on purpose, with the missing pass or illegal op named for each
walk it, one step at a time
run this
$ mlir-opt 02_conversion_framework.mlir --convert-scf-to-cf --finalize-memref-to-llvm --convert-func-to-llvm --convert-cf-to-llvm --reconcile-unrealized-casts > out/noarith.mlir; echo "mlir-opt exit=$?"; grep -cE 'arith\.' out/noarith.mlir
what it printed on the machine that wrote this lab
mlir-opt exit=0
7
run this
$ mlir-translate --mlir-to-llvmir out/noarith.mlir 2>&1 >/dev/null | head -3
what it printed on the machine that wrote this lab
out/noarith.mlir:9:11: error: Dialect `arith' not found for custom op 'arith.constant' 
    %c0 = arith.constant 0 : index
          ^

"Dialect `arith' not found" — mlir-translate only registers the llvm dialect (+ a few targets), so ANY leftover op is "unknown". Read the error as: "your pipeline didn't lower arith". The op name tells you which pass.

run this
$ mlir-opt 02_conversion_framework.mlir --convert-arith-to-llvm --finalize-memref-to-llvm --convert-func-to-llvm --convert-cf-to-llvm --convert-scf-to-cf --reconcile-unrealized-casts | mlir-translate --mlir-to-llvmir 2>&1 >/dev/null | head -3
what it printed on the machine that wrote this lab
<stdin>:21:5: error: Dialect `cf' not found for custom op 'cf.br' 
    cf.br ^bb1(%7, %10 : index, f32)
    ^
scf-to-cf PRODUCES cf ops, and the pass that consumes cf already ran.
Rule: a conversion must run after every pass that CREATES its source
dialect. Pipelines are ordered by "who produces what", top to bottom of
the dialect tower (lab 01/02). The same thing happens with vector: tiling
and vectorization produce vector/scf/affine ops, so their lowerings come after.
run this
$ mlir-opt 02_conversion_framework.mlir --convert-scf-to-cf --convert-arith-to-llvm --convert-func-to-llvm --convert-cf-to-llvm --reconcile-unrealized-casts | grep -E 'unrealized|memref\.' | head -3
what it printed on the machine that wrote this lab
    %6 = builtin.unrealized_conversion_cast %5 : !llvm.struct<(ptr, ptr, i64, array<1 x i64>, array<1 x i64>)> to memref<?xf32>
    %8 = builtin.unrealized_conversion_cast %7 : i64 to index
    %dim = memref.dim %6, %8 : memref<?xf32>
convert-func-to-llvm DID convert the signature to the descriptor struct —
then had to cast it BACK to memref<?xf32> for the memref.dim/load that
nobody lowered. The cast can't be reconciled because nothing on the other
side cancels it. Leftover `unrealized_conversion_cast` = a type crossed a
boundary between a converted and an unconverted op.
run this
$ mlir-opt 02_conversion_framework.mlir --convert-scf-to-cf --convert-arith-to-llvm --finalize-memref-to-llvm --convert-func-to-llvm --convert-cf-to-llvm | mlir-translate --mlir-to-llvmir 2>&1 >/dev/null | head -2
what it printed on the machine that wrote this lab
<stdin>:9:10: error: LLVM Translation failed for operation: builtin.unrealized_conversion_cast
    %6 = builtin.unrealized_conversion_cast %5 : !llvm.struct<(ptr, ptr, i64, array<1 x i64>, array<1 x i64>)> to memref<?xf32>

The pipeline was complete, but the glue casts (all of which DO cancel in pairs) were never removed. Always end a lowering with reconcile-unrealized-casts.

run this
$ mlir-opt 02_conversion_framework.mlir --convert-to-llvm | grep -cE 'scf.for'
what it printed on the machine that wrote this lab
  1
  `convert-to-llvm` asks every dialect in the IR for its to-LLVM patterns
  (ConvertToLLVMPatternInterface) and runs them together. scf has none (it
  must go through cf first), so scf.for survives. Real pipelines list their
  conversions explicitly (a vendor MLIR backend: LinalgToLLVMPass.cpp; IREE:
  LLVMCPU/ConvertToLLVM.cpp) so the order and options are visible and testable.

THE CHECKLIST WHEN mlir-translate FAILS:
  "Dialect `X' not found for custom op 'X.op'"  -> add/move the pass that lowers dialect X
  "LLVM Translation failed for ... unrealized_conversion_cast" -> type crossed an unconverted
                                                 boundary (STEP 3) or reconcile missing (STEP 4)

To find WHICH pass left it: --mlir-print-ir-after-all and search for the first dump where the op should have disappeared (lab 06_debugging/01).

the input

same sum_relu as lab 01, repeated at the bottom of this file.

take it further

1. Open ArithToLLVM.cpp:808 and change nothing — just find where `index` gets its bitwidth (hint: `indexBitwidth` option). Then run `--convert-arith-to-llvm="index-bitwidth=32"` and read the IR. 2. grep llvm-project/mlir/lib/Conversion for applyFullConversion. Which passes use it? What error do they produce on leftovers? 3. In a vendor MLIR backend, read a vendor dialect’s conversion directory: find its ConversionTarget, TypeConverter and patterns (the same three parts). ============================================================

read the source · mlir-lowering/02_conversion_framework.mlir
func.func @sum_relu(%x: memref<?xf32>) -> f32 { %c0 = arith.constant 0 : index %c1 = arith.constant 1 : index %zero = arith.constant 0.0 : f32 %n = memref.dim %x, %c0 : memref<?xf32> %r = scf.for %i = %c0 to %n step %c1 iter_args(%acc = %zero) -> (f32) { %v = memref.load %x[%i] : memref<?xf32> %relu = arith.maximumf %v, %zero : f32 %acc2 = arith.addf %acc, %relu : f32 scf.yield %acc2 : f32 } return %r : f32 }
◇
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.