Add MLX_DISABLE_NAX option to skip Metal-4 nax kernels at build time#3593
Closed
1R053 wants to merge 1 commit into
Closed
Add MLX_DISABLE_NAX option to skip Metal-4 nax kernels at build time#35931R053 wants to merge 1 commit into
1R053 wants to merge 1 commit into
Conversation
Adds a top-level CMake option that downstream projects can set to OFF the Metal-4 'nax' tensor GEMM/qmm/attention kernels even when the build environment otherwise qualifies (MLX_METAL_VERSION>=400 AND MACOS_SDK_VERSION>=26.2). Motivation: on macOS 26 the bundled Metal toolchain (metalfe-32023.x at time of writing — observed at .883) miscompiles these kernels: matmuls with M > ~8 rows silently return wrong results. The symptom is 'short prompts bit-exact / medium+long prompts garbage at step 0' across every model tested (qwen3_5, gemma4, llama4, minimax_m2, nemotron_h, mamba2 in our local rig). Apple's own MLX-shipping pipeline appears to apply the equivalent workaround — the mlx.metallib shipped inside iOS-26.4 and iOS-26.5 simulator runtimes (PrivateFrameworks/GESS.framework/mlx.metallib) contains zero nax kernel symbols across three checked builds. The current MLX workaround is implicit: build with -DCMAKE_OSX_DEPLOYMENT_TARGET=14.0 so the version probe reports __METAL_VERSION__=310 and the existing CMakeLists gate fails. That works but couples the workaround to the deployment-target value, which downstream projects may want to set for other reasons. An explicit opt-out flag is friendlier to consumers who hit the bug. Implementation: - New option MLX_DISABLE_NAX, default OFF (preserves current behaviour). - Existing version/SDK gate at mlx/backend/metal/kernels/CMakeLists.txt:158 gains an AND (NOT MLX_DISABLE_NAX) clause. - The else() branch already defines MLX_METAL_NO_NAX, which the C++ dispatchers respect, so no additional source changes needed — the existing fallback codepath is the right one.
Collaborator
|
The issue has nothing to do with NAX as you get same results with versions without NAX support. |
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
Add a
MLX_DISABLE_NAXCMake option (defaultOFF, no behaviour change for existing users) that downstream projects can set to skip the Metal-4 "nax" tensor GEMM / qmm / attention kernels even when the build environment otherwise qualifies for them (MLX_METAL_VERSION >= 400ANDMACOS_SDK_VERSION >= 26.2).Motivation
On macOS 26 the bundled Metal toolchain miscompiles the nax kernels. Symptom: matmuls with M > ~8 rows silently return wrong results. Short prompts are bit-exact against the reference (numpy / older MLX pip wheel); medium and long prompts diverge at the very first step. We observed this consistently across every architecture in our local rig (qwen3_5, gemma4 dense + MoE + elastic, llama4 Scout, minimax_m2, nemotron_h, mamba2) with
metalfe-32023.883fromcom.apple.MobileAsset.MetalToolchain-v17.6.42.0(macOS 26.5 host, SDK 26.5).A minimal repro using only this repo's headers:
Reproduces against any MLX build from current
mainon macOS 26 with noCMAKE_OSX_DEPLOYMENT_TARGETpin below 26.0. The bug is in the Metal toolchain codegen forsubtile_matmad_nax; this PR is not a fix for that — it's an opt-out so users hitting it can continue to consumemlxwithout producing miscompiled kernels.Smoking-gun: Apple's own MLX-shipping pipeline already avoids the nax codepath
The
mlx.metallibshipped inside Apple's iOS simulator runtimes — atPrivateFrameworks/GESS.framework/mlx.metallib, built and signed by Apple for distribution with Xcode — contains zero nax kernel symbols across every macOS-26-era runtime we checked:strings ... | grep -ciE 'steel_gemm_fused_nax_|_qmm_n_nax_|_qmm_t_nax_|_gather_qmm_n_nax_|_gather_qmm_t_nax_'A control build from
mainon the same host withCMAKE_OSX_DEPLOYMENT_TARGETunset yields alibmlx.awith 7 such symbols and the divergent-matmul behaviour above.Corroborating evidence from
mlx-swift: that package'sPackage.swiftdefinesMETAL_PATH = "default.metallib", meaning it consumes a pre-builtdefault.metallibrather than compiling.metalsources during SwiftPM build. We have three cachedmlx-swift_Cmlx.bundle/default.metallibfiles from our own iOS / macOS app archives built between 2026-03-15 and 2026-03-28 — both after #2772 ("Add Neural Accelerator Support", 2025-11-19) and the #3271 nax refactor (2026-03-18). All three contain 0 strict nax kernel matches at ~3.82 MB each. The same nax-disabled outcome shows up in mlx-swift consumers' artifacts as in Apple's own iOS-sim runtime metallibs, even though the two artifacts have different content (different sizes — one is mlx-swift's full kernel set, the other is GESS.framework's subset).This implies Apple's MLX-shipping pipeline (across both the iOS-sim runtime and the mlx-swift package) applies the equivalent workaround. There appears to be no Metal Toolchain version where these kernels have ever shipped correctly in production — the bug is not a regression from a known-good state. The proposed
MLX_DISABLE_NAXflag is, in effect, the explicit form of what Apple's existing pre-built metallib distribution channels are already doing implicitly. Downstream projects that compile MLX from source viacmake(e.g. anyone using the C++ library or a Rust FFI wrapper) currently have no such shield and are the ones hit by the bug.Why an explicit flag (vs the existing implicit workaround)
The existing implicit workaround (
-DCMAKE_OSX_DEPLOYMENT_TARGET=14.0to make__METAL_VERSION__report 310 and trip the existing version gate toelse()) works today, but:metalvsmetalliblink-step target leakage that mlx: fix macOS 26 target leakage in v3 metallib ollama/ollama#16053 had to add a separate workaround for — when the final link step stamps the metallib with the SDK's macOS 26 target despite AIRs being compiled at 14.0, runtime dispatch can still look for nax kernels that aren't in the library and fail to load (the symptom in MLX models not working on M5 MacBook Pro lmstudio-ai/lmstudio-bug-tracker#1356).An explicit, intent-named flag is friendlier to consumers who hit the bug, easier to remove cleanly when the toolchain is fixed, and discoverable via
cmake -LA.Related reports
mx.fast.scaled_dot_product_attentionproduces incorrect attention scores on M4 / MLX 0.31.2; total model collapse on the fused MPS path, manual attention works. Same failure-mode class as the bug this PR's flag defends against; an explicitMLX_DISABLE_NAX=ONbuild closes the symptom cleanly.Unable to load function steel_gemm_fused_nax_*on M5 MacBook Pro / macOS 26.2. Different surface (runtime kernel-load failure rather than miscompiled inference output), same kernel family. WithMLX_DISABLE_NAX=ONthose symbols are never compiled and the load is never attempted.mlx: fix macOS 26 target leakage in v3 metallib. Demonstrates the failure mode where the finalmetalvsmetalliblink step leaks the macOS 26 deployment target into the library and defeats the-DCMAKE_OSX_DEPLOYMENT_TARGET=14.0workaround.MLX_DISABLE_NAX=ONis robust against that leak — the kernels are never compiled, so metadata leakage can't resurrect them.Implementation
MLX_DISABLE_NAX(defaultOFF) inCMakeLists.txt, sitting next toMLX_METAL_DEBUG.mlx/backend/metal/kernels/CMakeLists.txt:158extended withAND (NOT MLX_DISABLE_NAX).MLX_METAL_NO_NAX) is reached onON, so the existing C++ dispatcher fallback is reused — no new source code paths, no new C++ surface.Net: 8 lines added, 1 modified. Reverting the workaround is a one-line revert.
Test plan
OFFpreserves the existing version/SDK gate exactly, so default-off behaviour is unchanged by construction.strings build/.../mlx.metallib | grep -ciE 'steel_gemm_fused_nax_|_qmm_n_nax_|_qmm_t_nax_|_gather_qmm_n_nax_|_gather_qmm_t_nax_'returns 7 nax kernel matches, andoracle_runagainstqwen3_5-0.8B-4bitproduces 1/3 greedy-match (short bit-exact, medium err 21.88 at step 0, long_prefill err 30.16 at step 0).-DMLX_DISABLE_NAX=ONend-to-end build verified on this fork at the commit in this PR:MLX_DISABLE_NAXas the suppression).cmake --build . --target mlx-metallibcompletes in 40s.mlx.metallib: 0 strict nax kernel matches (pass) AND 0 broader_nax_substring matches (pass), with the non-nax steel-GEMM workaround kernels present (steel_gemm_fused_(nn|nt|tn|tt)_*: 384 instantiations) — confirming the flag suppresses nax compilation without dropping the workaround kernels.CMAKE_OSX_DEPLOYMENT_TARGETindependently and have been failing the version/SDK gate (and hitting the else() branch) all along, so this PR is a no-op for them — not expected to regress.