Skip to content

Report the MLX backend as unavailable on the iOS simulator - #22336

Open
shoumikhin wants to merge 1 commit into
pytorch:mainfrom
shoumikhin:mlx-simulator-unavailable
Open

Report the MLX backend as unavailable on the iOS simulator#22336
shoumikhin wants to merge 1 commit into
pytorch:mainfrom
shoumikhin:mlx-simulator-unavailable

Conversation

@shoumikhin

Copy link
Copy Markdown
Contributor

Summary

Loading a model that delegates to MLX crashes the process on an iOS simulator instead of failing
cleanly. In the examples repository the demo app crashes on its MLX menu entry, and the llama and
image classifier tests crash with it.

The simulator's Metal device is real enough to build and dispatch a compute pipeline, but two of the
things MLX needs unconditionally are missing from it:

what MLX needs what the simulator does
device.architecture returns nothing, and MLX reads it without a null check
a shared storage heap refused by an assertion inside Metal

The first faults during device construction. Bypassing it reaches the second, which aborts. Neither
is reachable as an error, so nothing downstream can report it.

is_available() is the interface's own answer to whether a backend can run here, and the runtime
already asks before loading a delegate, so returning false is enough to turn the crash into
Error::NotFound. MLX's own is_available() returns a constant and checks nothing, which is why
the answer has to come from here.

This does not make MLX work on the simulator. It makes the answer honest, so a caller gets an error
it can handle.

Test plan

The macro decides everything, so it is checked on all three targets it affects:

simulator   TARGET_OS_SIMULATOR 1   is_available() false
device      TARGET_OS_SIMULATOR 0   is_available() defers to MLX
macOS       TARGET_OS_SIMULATOR 0   is_available() defers to MLX

A real GPU is unaffected. Compiling the same function with the guard disabled returns true on the
simulator again, which is the crashing path, so the guard is doing the work.

Confirmed separately that a delegate whose backend reports unavailable makes load_method return
Error::NotFound rather than crash.

The include is explicit because an undefined macro is zero in #if, which would make the guard
silently do nothing while still compiling.

Follow-ups, not in this change

  • The demo app's MLX menu entry should degrade rather than surface a load error, now that it gets
    one. That belongs in the examples repository.
  • The image classifier test can assert this error on a simulator, which pins the boundary instead
    of leaving it untested.

Loading a model that delegates to MLX crashes the process on an iOS simulator instead of failing
cleanly. The demo app in the examples repository crashes on the MLX menu entry, and the llama and
image classifier tests crash with it.

The simulator's Metal device is real enough to build and dispatch a compute pipeline, but two of
the things MLX needs unconditionally are missing from it: `architecture` reports nothing, and a
shared storage heap is refused by an assertion inside Metal. MLX reads the first without a null
check, so it faults during device construction, and if that is bypassed it aborts on the second.
Neither is reachable as an error, so nothing downstream can report it.

`is_available()` is the interface's own answer to whether a backend can run here, and the runtime
already asks before loading a delegate, so returning false is enough to turn the crash into
`Error::NotFound`. MLX's own `is_available()` returns a constant and checks nothing.

Test plan:

The macro decides everything here, so it is checked on all three targets it affects:

    simulator   TARGET_OS_SIMULATOR 1, is_available() false
    device      TARGET_OS_SIMULATOR 0, is_available() defers to MLX
    macOS       TARGET_OS_SIMULATOR 0, is_available() defers to MLX

so a real GPU is unaffected. Compiling the same function with the guard disabled returns true on
the simulator again, which is the crashing path, so the guard is doing the work.

Confirmed separately that a delegate whose backend reports unavailable makes `load_method` return
`Error::NotFound` rather than crash. The include is explicit because an undefined macro is zero in
`#if`, which would make the guard silently do nothing.

This does not make MLX work on the simulator. It makes the answer honest, so a caller gets an
error it can handle.
Copilot AI lite review requested due to automatic review settings August 30, 2026 21:22
@pytorch-bot

pytorch-bot Bot commented Aug 30, 2026

Copy link
Copy Markdown

🔗 Helpful Links

🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/22336

Note: Links to docs will display an error until the docs builds have been completed.

❌ You can merge normally! (1 Unrelated Failure), 4 Unclassified Failures

As of commit 825820c with merge base c27baa8 (image):

UNCLASSIFIED FAILURES - DrCI could not classify the following jobs because the workflow did not run on the merge base. The failures may be pre-existing on trunk or introduced by this PR:

  • MLX / test-mlx / test-mlx (gh) (this job did not run on the merge base, so DrCI cannot tell whether the failure is pre-existing)
    AssertionError: Slot for getitem already set to Slot(id_type=<IdType.Tensor: 1>, id_space=<IdSpace.Output: 3>, idx=0), trying to set to Slot(id_type=<IdType.Tensor: 1>, id_space=<IdSpace.Input: 2>, idx=3)
  • MLX / test-mlx-dflash / test-mlx-dflash (gh) (this job did not run on the merge base, so DrCI cannot tell whether the failure is pre-existing)
    AssertionError: Slot for getitem already set to Slot(id_type=<IdType.Tensor: 1>, id_space=<IdSpace.Output: 3>, idx=0), trying to set to Slot(id_type=<IdType.Tensor: 1>, id_space=<IdSpace.Input: 2>, idx=2)
  • MLX / test-mlx-qwen35-moe / test-mlx-qwen35-moe (gh) (this job did not run on the merge base, so DrCI cannot tell whether the failure is pre-existing)
    AssertionError: Slot for getitem_8 already set to Slot(id_type=<IdType.Tensor: 1>, id_space=<IdSpace.Output: 3>, idx=2), trying to set to Slot(id_type=<IdType.Tensor: 1>, id_space=<IdSpace.Input: 2>, idx=4)
  • MLX / test-mlx-stories110m / test-mlx-stories110m (gh) (this job did not run on the merge base, so DrCI cannot tell whether the failure is pre-existing)
    AssertionError: Slot for getitem already set to Slot(id_type=<IdType.Tensor: 1>, id_space=<IdSpace.Output: 3>, idx=0), trying to set to Slot(id_type=<IdType.Tensor: 1>, id_space=<IdSpace.Input: 2>, idx=2)

BROKEN TRUNK - The following job failed but were present on the merge base:

👉 Rebase onto the `viable/strict` branch to avoid these failures

This comment was automatically generated by Dr. CI and updates every 15 minutes.

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Aug 30, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@shoumikhin shoumikhin added the release notes: apple Changes to the Apple backend delegate label Aug 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. release notes: apple Changes to the Apple backend delegate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants