Experimental MLIR (IREE llvm-cpu) backend feasibility spike - #2311
Draft
cetagostini wants to merge 3 commits into
Draft
Experimental MLIR (IREE llvm-cpu) backend feasibility spike#2311cetagostini wants to merge 3 commits into
cetagostini wants to merge 3 commits into
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Feasibility spike: can PyTensor grow an MLIR backend? This PR adds an experimental
pytensor/link/mlir/that lowers aFunctionGraphto textual MLIR (linalg+arithon dynamictensor<?x...>), compiles it with IREE'sllvm-cputarget, and executes it through the IREE runtime — end to end, with correct results on simple CPU graphs.This is not a backend proposal. It is evidence for a discussion: the working slice, the exact blockers found, and where they live.
The simple graph
scripts/mlir_demo.pyruns the full matrix (every case checked withnp.testing.assert_allcloseagainstFAST_COMPILE):Because the emitted tensor types use
?dims, one compiled function runs at multiple input lengths without retracing (exercised at 3 and 5).Design
Follows the house backend pattern (mirrors
link/mlx/link/jax):MLIRLinker(JITLinker)converts the wholeFunctionGraphvia asingledispatch mlir_funcify, emits one MLIR module, callsiree.compiler.compile_str(target_backends=["llvm-cpu"]), and invokes the loaded VM module. No Python-op fallback — unsupported Ops raiseNotImplementedErrorloudly.ElemwiseAdd/Mul/Second →linalg.generic+arith,DimShuffle/DeepCopyOp→linalg.genericaffine maps,Dot/Dot22→linalg.fill+linalg.matmul. float32/float64, rank ≤ 2.Mode("MLIR")registered incompile/mode.py;config.modeacceptsMLIR.--iree-input-demote-f64-to-f32=false.Dotinner-dim equality before IREE (IREE does not implement our runtime-broadcast policy itself).Toolchain (macOS arm64)
iree-base-compiler/iree-base-runtime3.11.0 — clean install,compile_str→ VM flatbuffer → runtime invocation. Deps are only installed in the experiment env, intentionally not declared inpyproject.mlir-python-bindings22.1.8 — imports fine but anymlir.ir.Context()aborts the process (LLVM ERROR: Attempting to attach an interface to an unregistered operation builtin.unrealized_conversion_cast), which is why the IR is string-templated rather than built through the bindings.Blockers found (why this stays a spike)
llvm-cpusilently flushes subnormal results (float32 tiny*0.5 → 0.0instead of5.877e-39; same for float64). No exposed IEEE-denormal control iniree-compile --help-hidden. Principal semantic divergence from the C/Numba/JAX backends.MappedMemory/HalBufferViewinstances per call (~9.8 MB RSS growth over a 100-call probe; reproduced in raw IREE without PyTensor). Rules out iterative workloads until fixed upstream.Test plan
scripts/mlir_demo.py— all cases green vsFAST_COMPILE(exit 0).scripts/mlir_toolchain_hello.py— raw IREE hello-world (dynamictensor<?xf64>,linalg.generic/arith.addf) prints[5. 7. 9.]and asserts float64 preservation.x*0rewrites.🤖 Explored and implemented with AI assistance (Claude Code), including adversarial/correctness/edge-case review passes and an independent fact-check of the claims above.
Metal/MPS results
Verdict: yes, with a float32-only contract. The same simple graphs now run on the M3 Max GPU through IREE's Metal HAL:
MLIR_METALselectsmetal-spirvanddriver="metal"; it is deliberately distinct from the CPUMLIRmode.scripts/mlir_demo.pyenumerates and prints the actual IREE device, rejects float64 graphs before compilation, and checks all GPU outputs againstFAST_COMPILEwithrtol=atol=1e-6.Observed run (IREE 3.11.0):
The raw IREE hello-world was also compiled outside PyTensor from the existing
linalg.genericsource after changing its element type tof32;target_backends=["metal-spirv"]plusload_vm_flatbuffer(..., driver="metal")printed[5. 7. 9.]. The originalf64source fails at the intended compiler link with:The Metal mode rejects every live float64 graph value and passes
--iree-input-demote-f64-to-f32=falseas a compiler backstop, so it does not silently change PyTensor's dtype contract. The existingMLIRCPU mode still preserves float64.What “MPS dialect / Metal backend” actually means
This working path is not MPSGraph. It is IREE's actively maintained Metal HAL target, which lowers through SPIR-V and cross-compiles to Metal Shading Language; see IREE's Metal HAL design and target implementation.
There was a real Apple proposal for an MLIR
MPSdialect targeting MetalPerformanceShadersGraph, but it was an RFC rather than a merged upstream MLIR dialect. The corresponding generic public linalg/arith -> MPSGraph Python route is not available. On this host,/usr/bin/mpsgraphtool --helpexposesconvert -coremlpackage, not a generic MLIR/bytecode input. Apple's jax-metal documentation describes a separate JAX/OpenXLA StableHLO -> MPSGraph/PJRT route; it is not an ingress for this spike and it likewise listsnp.float64as unsupported.Caveats
metal-spirvcompiler target,metalruntime driver, and Xcode's Metal toolchain. The local wheel has all of them;iree-run-module --dump_devices=metalidentifies the M3 Max as Metal 3/unified-memory.float32is supported in this slice. Float64 is intentionally rejected; complex and broader Op/dtype coverage remain outside the spike.