FForge
← Labsmlir-basics-04MLIR anatomy●○○○

Your first MLIR program that RUNS (JIT and AOT)

mlir-basics/04_first_run.mlirneeds: mlir-opt, mlir-runner, mlir-translate, clang4 steps
0/4 steps
what this lab teaches

Reading IR is half the job. The other half is RUNNING it, because the only proof an optimisation is correct is identical output, and the only proof it's fast is a timer. Two ways to execute MLIR: JIT: mlir-runner = lower to llvm dialect, JIT-compile in-process, call @main. Fast iteration. Links "runner utils" libs that give you printMemrefF32, printF32, rtclock (a timer) etc. AOT: mlir-translate --mlir-to-llvmir -> .ll -> llc/clang -> object -> link. What a real compiler (IREE, triton-shared) does. Needed to cross-compile for AArch64 and run under qemu (lab 05_aarch64). `lower_std` (defined in tools/env.sh) is a textbook pipeline taking linalg/memref/scf/arith all the way to the llvm dialect. Lab 02_lowering takes it apart pass by pass; here we just use it.

evidence to leave with
The same kernel run through both JIT and AOT with matching results, and the asm showing fmul+fadd rather than fmla, explained
walk it, one step at a time
run this
$ lower_std 04_first_run.mlir > /tmp/first.llvm.mlir && mrun /tmp/first.llvm.mlir
what it printed on the machine that wrote this lab
Unranked Memref base@ = 0xf572d80 rank = 1 offset = 0 sizes = [8] strides = [1] data = 
[5,  5,  5,  5,  5,  5,  5,  5]
y = 3*1 + 2 = 5 everywhere. `printMemrefF32` takes an UNRANKED memref
(memref<*xf32>), which is why main does a memref.cast first — the
runtime lib is one C function that handles any rank.
run this
$ lower_std 04_first_run.mlir | mlir-opt --print-op-stats -o /dev/null 2>&1 | grep -oE '^ *[a-z_]+\.' | tr -d ' .' | sort | uniq -c
what it printed on the machine that wrote this lab
      1 builtin
     19 llvm

One builtin.module wrapper, everything else llvm.*. If anything else shows up (say `memref` or `unrealized`), mlir-translate will refuse the module.

run this
$ lower_std 04_first_run.mlir | mlir-translate --mlir-to-llvmir -o /tmp/first.ll && grep -m3 -E '^define' /tmp/first.ll
what it printed on the machine that wrote this lab
define void @axpy(float %0, ptr %1, ptr %2, i64 %3, i64 %4, i64 %5, ptr %6, ptr %7, i64 %8, i64 %9, i64 %10) {
define void @main() {
run this
$ clang -O2 /tmp/first.ll -o /tmp/first -L$MLIR_LIB -lmlir_c_runner_utils -lmlir_runner_utils -Wl,-rpath,$MLIR_LIB -Wno-override-module && /tmp/first
what it printed on the machine that wrote this lab
Unranked Memref base@ = 0x5634edbec980 rank = 1 offset = 0 sizes = [8] strides = [1] data = 
[5,  5,  5,  5,  5,  5,  5,  5]
Same numbers, now from a normal ELF binary. From here on it's "just LLVM":
everything you know from ~/debug_lab (opt, llc, llvm-mca, gdb, perf)
applies to /tmp/first.ll unchanged.
run this
$ clang --target=aarch64-linux-gnu -O2 -S /tmp/first.ll -o - -Wno-override-module | sed -n '/%vector.body/,/b.ne/p'
what it printed on the machine that wrote this lab
.LBB0_8:                                // %vector.body
                                        // =>This Inner Loop Header: Depth=1
	ldp	q1, q2, [x10, #-16]
	subs	x11, x11, #8
	ldp	q3, q4, [x9, #-16]
	add	x10, x10, #32
	fmul	v1.4s, v1.4s, v0.s[0]
	fmul	v2.4s, v2.4s, v0.s[0]
	fadd	v1.4s, v1.4s, v3.4s
	fadd	v2.4s, v2.4s, v4.4s
	stp	q1, q2, [x9, #-16]
	add	x9, x9, #32
	b.ne	.LBB0_8
run this
$ clang --target=aarch64-linux-gnu -O2 -S /tmp/first.ll -o - -Wno-override-module | grep -c 'vector.memcheck'
what it printed on the machine that wrote this lab
1

Three things to learn from twelve instructions: (a) The IR was target-independent; the backend picked AArch64 at the last moment. Whatever MLIR doesn't decide, LLVM decides. (b) MLIR emitted a SCALAR loop, and LLVM's loop vectorizer rescued it (NEON v1.4s, 8 floats/iteration). But it had to add a runtime `vector.memcheck`: x and y are two memrefs that MIGHT alias, so it checks at runtime and keeps a scalar fallback. MLIR code that is vectorized BEFORE lowering (lab 04_optimization) needs no such check, because linalg semantics already say what may be read and written. (c) `fmul` + `fadd`, not one `fmla`. C code would get a fused multiply-add here, but MLIR's arith ops default to fastmath<none> (lab 01 STEP 2), and LLVM may not CONTRACT a*b+c into fma without permission. Two instructions instead of one, in the hottest loop. Lab 04_optimization/03 turns the flag on and measures it. This is a real "fast with X / slow without X" case you'll see in a production MLIR backend.

take it further

1. Change %c8 to 1000000 and wrap @axpy in a timing loop using `func.func private @rtclock() -> f64` (lab 04_optimization shows how). 2. Replace printMemrefF32 with `vector.print` on a single loaded value. 3. Add `-O0` vs `-O2` to the clang line and count instructions in @axpy. ============================================================

read the source · mlir-basics/04_first_run.mlir
func.func private @printMemrefF32(memref<*xf32>) func.func @axpy(%a: f32, %x: memref<?xf32>, %y: memref<?xf32>) { %c0 = arith.constant 0 : index %c1 = arith.constant 1 : index %n = memref.dim %x, %c0 : memref<?xf32> scf.for %i = %c0 to %n step %c1 { %xi = memref.load %x[%i] : memref<?xf32> %yi = memref.load %y[%i] : memref<?xf32> %ax = arith.mulf %a, %xi : f32 %s = arith.addf %ax, %yi : f32 memref.store %s, %y[%i] : memref<?xf32> } return } func.func @main() { %c8 = arith.constant 8 : index %one = arith.constant 1.0 : f32 %two = arith.constant 2.0 : f32 %x = memref.alloc(%c8) : memref<?xf32> %y = memref.alloc(%c8) : memref<?xf32> linalg.fill ins(%one : f32) outs(%x : memref<?xf32>) linalg.fill ins(%two : f32) outs(%y : memref<?xf32>) %a = arith.constant 3.0 : f32 call @axpy(%a, %x, %y) : (f32, memref<?xf32>, memref<?xf32>) -> () %u = memref.cast %y : memref<?xf32> to memref<*xf32> call @printMemrefF32(%u) : (memref<*xf32>) -> () memref.dealloc %x : memref<?xf32> memref.dealloc %y : memref<?xf32> 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.