TRACK: aarch64 | DIFFICULTY: 4 | NEEDS: mlir-opt, clang, qemu (~/debug_lab/tools/a64-setup.sh)
FILES: sched_sve.mlir (schedule), matmul.mlir + bench_matmul.c (copied from track 04)
In track 04 the N tile was a number (16). For SVE it becomes [4], read as
"4 × vscale": one z-register of f32 whatever the hardware width is (128 bits
on most phone cores, 256 on Neoverse-V1, up to 2048). Three consequences, all
visible in this lab:
%c4_vscale, and
the last tile may be partial, so vectorization needs explicit, masked
vector sizes: vectorize %t vector_sizes [TM, [4], 1].convert-vector-to-llvm{enable-arm-sve},
plus arm-sve-legalize-vector-storage. This is $LOWER_SVE in tools/env.sh.~/debug_lab/aarch64 lab 02).This lab also documents three real failures hit while building it. They are the most valuable part: each is a bug class you'll meet in a production MLIR backend.
sed 's/TM/2/g; s|HOIST||' sched_sve.mlir > out/s.mlir mlir-opt matmul.mlir --transform-preload-library="transform-library-paths=out/s.mlir" \ --transform-interpreter --test-transform-dialect-erase-schedule -o out/sve.t.mlir grep -oE 'vector<[^>]*\[[0-9]+\][^>]*>' out/sve.t.mlir | sort | uniq -c grep -m1 -o 'step %c4_vscale' out/sve.t.mlir
Types like vector<2x[4]xf32> (data) and vector<2x[4]xi1> (masks), and a
loop that steps by 4 * vscale.
mlir-opt out/sve.t.mlir --pass-pipeline="$LOWER_SVE" | mlir-translate --mlir-to-llvmir -o out/sve.ll clang --target=aarch64-linux-gnu -O2 -march=armv8.2-a+sve -c out/sve.ll -o out/sve.o -Wno-override-module a64cc -O2 bench_matmul.c out/sve.o -o out/mm_sve -lm for vl in 16 32 64; do printf "VL=%4d bits: " $((vl*8)); a64sve $vl ./out/mm_sve 1 sve 2>&1 | grep -oE 'maxerr.*'; done
Real output (about 75 s under qemu; the timings it prints are meaningless under emulation):
VL= 128 bits: maxerr 0.0e+00 OK VL= 256 bits: maxerr 0.0e+00 OK VL= 512 bits: maxerr 0.0e+00 OK
clang --target=aarch64-linux-gnu -O2 -march=armv8.2-a+sve -S out/sve.ll -o out/sve.s -Wno-override-module awk '/Inner Loop Header: Depth=3/{p=1} p{print} p&&/b\.ne/{exit}' out/sve.s
ld1w { z0.s }, p1/z, [x14, x15, lsl #2] B row slice (predicated by p1) ld1w { z1.s }, p1/z, [x16] C row 0 <-- loaded every k ld1rw { z2.s }, p0/z, [x18] A[m0][k] broadcast to all lanes ld1w { z3.s }, p1/z, [x17] C row 1 <-- loaded every k ld1rw { z4.s }, p0/z, [x0] A[m1][k] broadcast fmla z1.s, p1/m, z2.s, z0.s C0 += A0 * B fmla z3.s, p1/m, z4.s, z0.s C1 += A1 * B st1w { z1.s }, p1, [x16] C row 0 <-- stored every k st1w { z3.s }, p1, [x17] C row 1 <-- stored every k
It's correct and vector-length-agnostic, but it's lab 04/01's variant C (no hoisting): C goes through memory on every k step. Adding hoisting for masked, scalable transfers is the exercise below, and it's exactly the kind of gap you'll close in a production MLIR backend.
Failure 1 — Dialect 'ub' not found for custom op 'ub.poison' in mlir-translate.
Masked vector code introduces ub.poison, and the original LOWER_STD had no
convert-ub-to-llvm. This is lab 02_lowering/02, STEP 1, for real:
new IR shapes need new conversion passes. Fixed in tools/env.sh.
Failure 2 — the AArch64 backend crashed:
fatal error: error in backend: Cannot select: 0x...: nxv4i1,ch = load<(load (<vscale x 1 x s8>) ... In function: matmul PLEASE submit a bug report ... /tmp/sve-XXXX.ll /tmp/sve-XXXX.sh
Cause: pass order. convert-vector-to-scf spills masks to stack
temporaries (memref<vector<...xi1>>), and arm-sve-legalize-vector-storage
must run after it to make those stores legal for SVE predicates. In the
first version it ran before. The wrong order is kept as $LOWER_SVE_BAD so
you can reproduce it; lab 06_debugging/04 walks through minimizing it.
Lesson: an LLVM backend crash on MLIR-generated IR is usually an MLIR
pipeline bug, i.e. IR the target can't legalize. Check the pipeline against an
upstream test that does the same thing (here
mlir/test/Integration/Dialect/Linalg/CPU/ArmSVE/matmul.mlir) before filing an
LLVM bug.
Failure 3 — correct build, then qemu: uncaught target signal 11 (Segmentation fault) after 5 s.
mlir-opt out/sve.t.mlir --pass-pipeline="builtin.module(func.func(convert-linalg-to-loops), canonicalize, cse, convert-vector-to-scf)" | grep -nE 'alloca|scf.for' | head -8
19: scf.for %arg3 = %c0 to %c512 step %c2 { 20: scf.for %arg4 = %c0 to %c512 step %c4_vscale { 21: scf.for %arg5 = %c0 to %c512 step %c1 { 22: %alloca = memref.alloca() : memref<vector<2x1xf32>> 23: %alloca_1 = memref.alloca() : memref<vector<1x[4]xf32>> ...
memref.alloca inside the innermost loop. Each iteration grows the stack;
512 × 128 × 512 iterations blow it. The upstream test passes only because its
matrices are 5×15. The fix is convert-vector-to-scf{full-unroll=true}, which
lowers the transfers without temporaries (0 allocas). Lesson: tests with
tiny shapes hide resource bugs. Always run at least one realistic size.
HOIST with
%fh = transform.structured.hoist_redundant_vector_transfers %func : (!transform.any_op) -> !transform.any_op
and make the following apply_patterns use %fh. Do the st1ws leave the loop?
If not, why? (Hint: masked transfers, and whether the mask is loop-invariant.)TM=4 and TM=8. Compare instruction counts in the inner loop.mlir-opt out/sve.t.mlir --pass-pipeline="$LOWER_SVE_BAD" | mlir-translate --mlir-to-llvmir | clang --target=aarch64-linux-gnu -O2 -march=armv8.2-a+sve -x ir -c - -o /dev/nullA lab is done when you can say what it showed on your machine — not when you have read it.
It decides which problems come first, which drills open, which labs you see and how the Learn surface is organised. Change it any time from the switch in the top bar, or press Escape to look around first.