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

Calling MLIR from C — and running it on AArch64 (qemu)

mlir-lowering/04_kernel.mlirneeds: mlir-opt, mlir-translate, clang, qemu (~/debug_lab/tools/a64-setup.sh)4 steps
0/4 steps
what this lab teaches

mlir-runner is for experiments. A real compiler produces an OBJECT FILE that someone else calls: the Triton launcher (a vendor MLIR backend: the generated Triton launcher), IREE's runtime, or a C benchmark harness you write. This lab is that path, and it's the template for every benchmark in track 04 and every AArch64 run in track 05: kernel.mlir --(LOWER_STD)--> llvm dialect --(mlir-translate)--> .ll --(clang --target=X -c)--> .o + harness.c --> executable for X The ABI rule: a memref argument becomes 3+2N scalars (lab 01/02). That's painful from C, so `attributes {llvm.emit_c_interface}` asks MLIR to ALSO emit `_mlir_ciface_<name>` which takes each memref as ONE POINTER to a descriptor struct. The harness declares that struct by hand.

evidence to leave with
A C program calling an MLIR kernel, producing identical results natively and under qemu-aarch64
walk it, one step at a time
run this
$ mlir-opt 04_kernel.mlir --pass-pipeline="$LOWER_STD" | mlir-translate --mlir-to-llvmir -o out/saxpy.ll && grep -E '^define' out/saxpy.ll
what it printed on the machine that wrote this lab
define void @saxpy(float %0, ptr %1, ptr %2, i64 %3, i64 %4, i64 %5, ptr %6, ptr %7, i64 %8, i64 %9, i64 %10) {
define void @_mlir_ciface_saxpy(float %0, ptr %1, ptr %2) {
run this
$ sed -n '/_mlir_ciface_saxpy/,/^}/p' out/saxpy.ll | sed -n '1,3p;15,17p'
what it printed on the machine that wrote this lab
define void @_mlir_ciface_saxpy(float %0, ptr %1, ptr %2) {
  %4 = load { ptr, ptr, i64, [1 x i64], [1 x i64] }, ptr %1, align 8
  %5 = extractvalue { ptr, ptr, i64, [1 x i64], [1 x i64] } %4, 0
  ret void
}

The wrapper loads each descriptor struct, splats it into 5 scalars, and calls the real function. After -O2 inlining it costs nothing.

run this
$ clang -O2 -c out/saxpy.ll -o out/saxpy.x86.o -Wno-override-module && clang -O2 04_harness.c out/saxpy.x86.o -o out/saxpy.x86 && ./out/saxpy.x86
what it printed on the machine that wrote this lab
1 3 5 7 9 11 13 15 17 19 
PASS
run this
$ clang --target=aarch64-linux-gnu -O2 -c out/saxpy.ll -o out/saxpy.a64.o -Wno-override-module && a64cc -O2 04_harness.c out/saxpy.a64.o -o out/saxpy.a64 && a64run ./out/saxpy.a64
what it printed on the machine that wrote this lab
1 3 5 7 9 11 13 15 17 19 
PASS
run this
$ file out/saxpy.a64 | cut -d, -f1-2
what it printed on the machine that wrote this lab
out/saxpy.a64: ELF 64-bit LSB pie executable, ARM aarch64
Remember debug_lab's rule: qemu is for CORRECTNESS. Never time under qemu.
For AArch64 performance use llvm-mca (lab 05_aarch64/03) or real hardware
(the Snapdragon device / Arm server the team uses).
run this
$ sed 's/ attributes {llvm.emit_c_interface}//' 04_kernel.mlir | mlir-opt --pass-pipeline="$LOWER_STD" | mlir-translate --mlir-to-llvmir | clang -x ir -O2 -c - -o out/noc.o -Wno-override-module && clang 04_harness.c out/noc.o -o out/noc 2>&1 | grep -m1 undefined
what it printed on the machine that wrote this lab
04_harness.c:(.text+0x8c): undefined reference to `_mlir_ciface_saxpy'
Without the attribute there's no _mlir_ciface_ symbol. The other classic:
harness struct doesn't match the memref rank (MemRef1D for a 2-D memref)
— that doesn't fail to link, it reads garbage strides. If a kernel is
correct under mlir-runner and wrong from C, check the struct first.
files

04_kernel.mlir (this), 04_harness.c ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

take it further

1. Make the kernel 2-D (memref<?x?xf32>) and write MemRef2D in the harness. 2. Pass a NON-contiguous view from C: stride 2 over a bigger array. (LOWER_STD uses identity-layout-map at function boundaries, so what does the kernel ASSUME about strides? Read the .ll: is stride used?) 3. Use `a64sve 32 ./out/saxpy.a64` (256-bit SVE). Does anything change? Why not? (No SVE instructions in it — lab 05_aarch64/02 changes that.) ============================================================ A kernel with the C interface attribute: MLIR will ALSO emit `_mlir_ciface_saxpy(float, MemRef1D *x, MemRef1D *y)` that takes descriptors by pointer.

read the source · mlir-lowering/04_kernel.mlir
func.func @saxpy(%a: f32, %x: memref<?xf32>, %y: memref<?xf32>) attributes {llvm.emit_c_interface} { linalg.generic {indexing_maps = [affine_map<(i) -> (i)>, affine_map<(i) -> (i)>], iterator_types = ["parallel"]} ins(%x : memref<?xf32>) outs(%y : memref<?xf32>) { ^bb0(%xi: f32, %yi: f32): %m = arith.mulf %a, %xi : f32 %s = arith.addf %m, %yi : f32 linalg.yield %s : 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.