Skip to content

Expose MLX JIT build flag (EMILY_MLX_JIT) — closes #45 - #47

Merged
ausimian merged 4 commits into
mainfrom
mlx-jit-flag
Apr 19, 2026
Merged

Expose MLX JIT build flag (EMILY_MLX_JIT) — closes #45#47
ausimian merged 4 commits into
mainfrom
mlx-jit-flag

Conversation

@ausimian

Copy link
Copy Markdown
Owner

Summary

  • EMILY_MLX_JIT=1 at build time passes -DMLX_METAL_JIT=ON to MLX's CMake, selecting runtime JIT compilation of Metal kernels. Default (unset / 0) preserves the existing AOT path.
  • The flag is woven into the MLX install-dir cache key (mlx-<hash> vs mlx-<hash>-jit), so flipping it doesn't reuse a stale libmlx.a / metallib.
  • README "How to build" gains a new MLX JIT (optional) subsection documenting the trade-off with measured sizes.

Measured artefact sizes (M-series Mac, Release)

Mode libemily.so mlx.metallib priv/ total
JIT off (default) ~20 MB ~154 MB ~175 MB
JIT on (EMILY_MLX_JIT=1) ~22 MB ~3.5 MB ~25 MB

The ~150 MB delta on disk comes from mlx.metallib shrinking to a stub when kernels are JIT-compiled lazily at first use instead of shipped AOT-compiled. Issue #45's framing of JIT as the "bloating" mode is inverted — AOT is the bloated mode. Default is unchanged for now (per the deferred-default point in #45).

Test plan

  • Clean build EMILY_MLX_JIT=1 EMILY_CACHE=/tmp/… mix compile.emily_mlx --force succeeds; MLX install dir gets -jit suffix.
  • Clean build JIT off succeeds; no suffix.
  • mix test passes 425/425 under JIT off (default).
  • mix test passes 425/425 under JIT on (EMILY_MLX_JIT=1 EMILY_CACHE=… mix test).
  • mix precommit clean on default config.
  • Cache-key separation: ls $EMILY_CACHE shows mlx-<hash>/ and mlx-<hash>-jit/ side by side.

Notes

During investigation I initially observed 322/425 failures under JIT on — this turned out to be stale cached artefacts from iterative testing, not a real bug. Clean rebuild produced the passing run. No tests needed to be skipped or gated for JIT-only code paths.

Pipe EMILY_MLX_JIT=1 through to -DMLX_METAL_JIT=ON in the MLX CMake
configure so users can opt in to MLX's runtime JIT-compilation of
Metal kernels. Default (unset / 0) preserves the existing AOT build.

The flag is incorporated into the MLX install-dir cache key
("mlx-<hash>" vs "mlx-<hash>-jit") so toggling does not reuse a stale
artefact. All 425 tests pass under both modes.

JIT on trades a 150 MB AOT metallib for a 3.5 MB stub plus lazy
per-kernel compile at first use; priv/ footprint drops from ~175 MB
to ~25 MB. README "How to build" documents the trade-off.

Closes #45.
Add a matrix dimension over EMILY_MLX_JIT ∈ {0, 1} so both MLX build
flavours are exercised on every push / PR. _build and MLX caches are
partitioned by the flag so the two matrix entries don't clobber each
other's artefacts.
The MLX JIT branch in backend/metal/CMakeLists.txt unconditionally
runs make_jit_source on NAX headers, which #include
<MetalPerformancePrimitives/MetalPerformancePrimitives.h> — only
present in the macOS 26.2+ SDK. The AOT branch in
kernels/CMakeLists.txt already gates NAX with that SDK check; the JIT
branch needs the same gate so JIT=1 builds work on older SDKs
(e.g. the macos-14 GitHub runner).

Rather than fork the submodule, ship a patch file under patches/ and
apply it idempotently from mix.exs before cmake configure. When the
SDK gate fails, MLX_METAL_NO_NAX is defined and is_nax_available()
returns false at runtime, matching the existing AOT behaviour.
CI JIT=1 on macos-14 got past cmake configure but hit a linker
error:

  symbol not found in flat namespace '__ZN3mlx4core5metal13quantized_naxEv'

jit_kernels.cpp references mlx::core::metal::{gemm_nax,
quantized_nax, fp_quantized_nax, steel_gemm_fused_nax, …} as strong
external symbols. Those symbols are normally supplied by the
auto-generated jit/<name>.cpp files that make_jit_source() creates;
when we gate those out for old SDKs, nothing provides them and the
NIF fails to link.

Extend the patch to emit an nax_stubs.cpp with empty-string returns
under the gated-out branch. The stubs are never exercised because
is_nax_available() returns false when MLX_METAL_NO_NAX is defined.
@ausimian

Copy link
Copy Markdown
Owner Author

A PR that applies the patch can be found at ml-explore/mlx#3426

@ausimian
ausimian merged commit ccc5b4c into main Apr 19, 2026
4 checks passed
@ausimian
ausimian deleted the mlx-jit-flag branch April 19, 2026 02:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant