Skip to content

Fix JIT build with old macOS SDK - #3853

Merged
zcbenz merged 2 commits into
ml-explore:mainfrom
metascroy:fix/nax-has-include-guard
Aug 4, 2026
Merged

Fix JIT build with old macOS SDK#3853
zcbenz merged 2 commits into
ml-explore:mainfrom
metascroy:fix/nax-has-include-guard

Conversation

@metascroy

Copy link
Copy Markdown
Contributor

Summary

The NAX kernel headers unconditionally include <MetalPerformancePrimitives/MetalPerformancePrimitives.h>, which only ships in the macOS 26 / Xcode 26 SDK. With MLX_METAL_JIT=ON on an older SDK, the build fails because the JIT preamble generator preprocesses these headers and the include cannot be resolved.

Details

make_jit_sourcemake_compiled_preamble.sh runs xcrun -sdk macosx metal -x metal -E -H over each NAX header to enumerate/inline its dependencies. On an SDK without MetalPerformancePrimitives, that step fatals:

nax.h:12:10: fatal error: 'MetalPerformancePrimitives/MetalPerformancePrimitives.h' file not found
Error: Metal compiler header resolution failed for .../steel/gemm/gemm_nax.h

The metallib path already gates NAX on MLX_METAL_VERSION >= 400 && MACOS_SDK_VERSION >= 26.2 && CMAKE_OSX_DEPLOYMENT_TARGET >= 26.2 (kernels/CMakeLists.txt), but the JIT path adds the NAX make_jit_source entries unconditionally, so a JIT build on a pre-26 SDK always tries to preprocess nax.h and fails. make_compiled_preamble.sh already intends to tolerate the framework (grep -v "Xcode" strips it from the inlined preamble); this only breaks when the header is absent from the SDK entirely.

Fix

Wrap the framework include in both NAX headers with #if __has_include(...):

#if __has_include(<MetalPerformancePrimitives/MetalPerformancePrimitives.h>)
#include <MetalPerformancePrimitives/MetalPerformancePrimitives.h>
#endif

On pre-26 SDKs, preprocessing now succeeds and NAX kernels are simply never JIT-compiled (they are runtime-gated by is_nax_available(), which is already false on non-NAX hardware). On the macOS 26 SDK the include is kept and NAX is unchanged.

Testing

  • MLX_METAL_JIT=ON build with Xcode 16.4 / macOS 15.5 SDK: fails before, succeeds after.
  • NAX-capable hardware + macOS 26 SDK: NAX still compiled and used (include present).

@jagrit06

Copy link
Copy Markdown
Member

Thanks for the fix, I will find a pre-26 system as soon as I can and test it. Can you share what the jit pre-amble on the machine you tried in the meantime ?

@metascroy

Copy link
Copy Markdown
Contributor Author

Thanks for the fix, I will find a pre-26 system as soon as I can and test it. Can you share what the jit pre-amble on the machine you tried in the meantime ?

Thanks @jagrit06! I'm also on macOS 26 locally, so the preamble just includes the NAX/MPP code as before for me.

But I did apply this exact patch in ExecuTorch because our test-mlx-backend-operators job runs on Xcode 15.4 / MacOSX14.5 SDK:

  • Both nax.h patches apply cleanly
  • The JIT preamble generation over the NAX headers runs and compiles without error (see jit/steel_gemm_fused_nax.cpp, steel_attention_nax.cpp in logs); previously this step failed on MLX v32.0
  • The operator suite passes (390 passed, 77 skipped, 0 failed)

Full log: https://ossci-raw-job-status.s3.amazonaws.com/log/pytorch/executorch/89566514106

@zcbenz zcbenz left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The correct way to fix this is to only make_jit_source the nax headers when the SDK requirement is met.

The NAX GEMM and attention headers include
<MetalPerformancePrimitives/MetalPerformancePrimitives.h>, which only ships in
the macOS 26 / Xcode 26 SDK. With MLX_METAL_JIT=ON on an older SDK, the JIT
preamble generator (make_compiled_preamble.sh) runs `metal -E` over these
headers and fatals on the missing include, breaking the build. The metallib
path already gates NAX on the SDK/Metal version; the JIT path did not.

Gate the NAX make_jit_source() calls behind the same
MLX_METAL_VERSION/MACOS_SDK_VERSION/CMAKE_OSX_DEPLOYMENT_TARGET check used by the
metallib path, and define MLX_METAL_NO_NAX when the requirement is unmet. On
those SDKs NAX is already runtime-gated via is_nax_available(), so the
get_*_nax_kernel entry points in jit_kernels.cpp are unreachable; guarded empty
preamble getters keep that translation unit linking. Newer SDKs still build NAX.
@metascroy
metascroy force-pushed the fix/nax-has-include-guard branch from efdc2ec to f043322 Compare August 3, 2026 17:30
@metascroy

Copy link
Copy Markdown
Contributor Author

The correct way to fix this is to only make_jit_source the nax headers when the SDK requirement is met.

Thanks @zcbenz, I have updated the approach as suggested!

@zcbenz zcbenz left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me, thanks!

@zcbenz zcbenz changed the title Guard NAX MetalPerformancePrimitives include behind __has_include Fix JIT build with old macOS SDK Aug 4, 2026
@zcbenz
zcbenz merged commit b34e332 into ml-explore:main Aug 4, 2026
28 checks passed
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.

3 participants