FForge
← Labsmlir-aarch64-01MLIR on AArch64●●○○

Telling the compiler what the machine is (triple, CPU, features)

mlir-aarch64/01_target.mlirneeds: mlir-opt, mlir-translate, opt, llc4 steps
0/4 steps
what this lab teaches

Three levels of "what machine is this", and each one changes the code: TRIPLE aarch64-unknown-linux-gnu which ISA/ABI at all (lab 02_lowering/01 STEP 5b: no triple = no vector registers = no vectorization) CPU -mcpu=neoverse-v1 / cortex-x4 scheduling model, cost model, DEFAULT features FEATURES +sve +sve2 +i8mm +bf16 +dotprod which instructions may be used at all In MLIR you can set them in two places: module attributes {llvm.target_triple = "..."} -> `target triple` in the .ll func attributes {target_cpu = "...", target_features = #llvm.target_features<[...]>} -> per-function "target-cpu"/"target-features" ...or leave them to opt/llc/clang flags (-mcpu, -mattr). The real compilers do the latter: IREE passes --iree-llvmcpu-target-cpu / --iree-llvmcpu-target-cpu-features into its LLVM TargetMachine; a Triton CPU backend has to do the same from the device it runs on.

evidence to leave with
A case where the feature was present but unused, and the attribute placement that caused it
walk it, one step at a time
run this
$ mlir-opt 01_target.mlir --pass-pipeline="$LOWER_STD" | mlir-translate --mlir-to-llvmir -o out/plain.ll && grep triple out/plain.ll
what it printed on the machine that wrote this lab
target triple = "aarch64-unknown-linux-gnu"
run this
$ opt -O3 out/plain.ll -pass-remarks=loop-vectorize -o /dev/null 2>&1 | head -1
what it printed on the machine that wrote this lab
remark: <unknown>:0:0: vectorized loop (vectorization width: 4, interleaved count: 4)
Compare lab 02_lowering/01 STEP 5b, where the same loop got "not beneficial".

Put target_cpu/target_features on the func.func and look at the .ll:

run this
$ sed 's/attributes {llvm.emit_c_interface}/attributes {llvm.emit_c_interface, target_cpu = "neoverse-v1", target_features = #llvm.target_features<["+sve"]>}/' 01_target.mlir | mlir-opt --pass-pipeline="$LOWER_STD" | mlir-translate --mlir-to-llvmir -o out/tf.ll && grep -E '^define|^attributes #0' out/tf.ll
what it printed on the machine that wrote this lab
define float @sum_relu(ptr %0, ptr %1, i64 %2, i64 %3, i64 %4) {
define float @_mlir_ciface_sum_relu(ptr %0) #0 {
attributes #0 = { "target-cpu"="neoverse-v1" "target-features"="+sve" }
convert-func-to-llvm copied the attributes onto the `_mlir_ciface_` WRAPPER
(#0) but NOT onto @sum_relu itself (no #0 on its define line). Consequence:
run this
$ opt -O3 out/tf.ll -o - | llc -O3 -o out/tf.s && for f in sum_relu _mlir_ciface_sum_relu; do printf "%-24s sve=%s neon=%s\n" $f $(awk "/^$f:/,/Lfunc_end/" out/tf.s | grep -cE 'z[0-9]+\.s') $(awk "/^$f:/,/Lfunc_end/" out/tf.s | grep -cE 'v[0-9]+\.4s'); done
what it printed on the machine that wrote this lab
sum_relu                 sve=0 neon=19
_mlir_ciface_sum_relu    sve=0 neon=0
The kernel was compiled with NO SVE: the feature you asked for never
reached the function doing the work. Nothing warned. That's why
production pipelines set CPU/features on the TargetMachine (or on every
llvm.func in a late pass) instead of trusting attribute propagation. If
you ever see "I enabled +sve and nothing changed", check `^attributes`
in the .ll FIRST. (Worth checking whether this propagation is a known
upstream issue before relying on it either way.)
run this
$ for cpu in generic neoverse-n1 neoverse-v1 neoverse-v2; do opt -O3 -mcpu=$cpu out/plain.ll -o - | llc -O3 -mcpu=$cpu -o out/p_$cpu.s; printf "%-12s sve=%-3s neon=%-3s\n" $cpu $(awk '/^sum_relu:/,/Lfunc_end/' out/p_$cpu.s | grep -cE 'z[0-9]+\.s') $(awk '/^sum_relu:/,/Lfunc_end/' out/p_$cpu.s | grep -cE 'v[0-9]+\.4s'); done
what it printed on the machine that wrote this lab
generic      sve=0   neon=19 
neoverse-n1  sve=0   neon=19 
neoverse-v1  sve=16  neon=4  
neoverse-v2  sve=0   neon=19 
Read this carefully, it's counterintuitive:
  neoverse-n1  no SVE in the core at all -> NEON.
  neoverse-v1  SVE with 256-bit vectors  -> the vectorizer prefers SVE (2x NEON width).
  neoverse-v2  has SVE2, but 128-bit vectors -> SVE would be the SAME width
               as NEON, so the cost model picks NEON (no predication overhead).
"Has SVE" != "should use SVE". Most Snapdragon/Cortex-X phone cores with

SVE2 are 128-bit too, so on the devices you'll target NEON may be the right default, and SVE's value is predication (tails, masked loads), not width. Always check what the vectorizer CHOSE for the -mcpu you ship.

run this
$ awk '/^sum_relu:/,/Lfunc_end/' out/p_neoverse-v1.s | sed -n '/%vector.body/,/b.ne/p'
what it printed on the machine that wrote this lab
.LBB0_7:                                // %vector.body
                                        // =>This Inner Loop Header: Depth=1
	ldr	z5, [x12]
	ldr	z6, [x12, #1, mul vl]
	subs	x13, x13, x9
	fmax	z5.s, p0/m, z5.s, #0.0
	ldr	z7, [x12, #2, mul vl]
	ldr	z16, [x12, #3, mul vl]
	fmax	z6.s, p0/m, z6.s, #0.0
	fmax	z7.s, p0/m, z7.s, #0.0
	fmax	z16.s, p0/m, z16.s, #0.0
	add	x12, x12, x11
	fadd	z2.s, z2.s, z5.s
	fadd	z0.s, z0.s, z6.s
	fadd	z3.s, z3.s, z7.s
	fadd	z4.s, z4.s, z16.s
	b.ne	.LBB0_7
`ldr z5, [x12, #1, mul vl]`: the offset is in units of the vector length,
which the code never knows. `fmax z5.s, p0/m, ...`: predicated by p0. The
same binary runs on 128-bit and 2048-bit SVE. Lab 02 proves it under qemu.
take it further

1. Add -mcpu=cortex-x4 and -mcpu=oryon-1 (if your LLVM knows it: `llc -mtriple=aarch64 -mcpu=help 2>&1 | grep -i oryon`). 2. Force SVE on neoverse-v2: `opt -O3 -mcpu=neoverse-v2 -scalable-vectorization=preferred`. Count again. 3. Fix STEP 2 for real: after LOWER_STD, what single mlir-opt/sed step puts the attributes on EVERY llvm.func? (Or pass -mcpu/-mattr to opt/llc.) ============================================================

read the source · mlir-aarch64/01_target.mlir
module attributes {llvm.target_triple = "aarch64-unknown-linux-gnu"} { func.func @sum_relu(%x: memref<?xf32>) -> f32 attributes {llvm.emit_c_interface} { %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 fastmath<reassoc> : 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.