diff --git a/backends/mlx/runtime/MLXBackend.cpp b/backends/mlx/runtime/MLXBackend.cpp index d6dfc463b76..615c4f1c6ce 100644 --- a/backends/mlx/runtime/MLXBackend.cpp +++ b/backends/mlx/runtime/MLXBackend.cpp @@ -31,6 +31,8 @@ #include #include +#include + namespace executorch { namespace backends { namespace mlx { @@ -217,7 +219,16 @@ class MLXBackend final : public ::executorch::runtime::BackendInterface { ~MLXBackend() override = default; bool is_available() const override { +#if TARGET_OS_SIMULATOR + // The simulator's Metal device reports no architecture, which MLX reads + // without a null check while constructing its device. Past that, requesting + // a shared storage heap traps inside Metal itself, so MLX never gets a + // value it could fall back from. This is a build switch rather than a + // probe: it can go once the simulator has a usable Metal device. + return false; +#else return ::mlx::core::metal::is_available(); +#endif } Result init( diff --git a/docs/source/backends-overview.md b/docs/source/backends-overview.md index 049b7d443dc..ab74fe8edd8 100644 --- a/docs/source/backends-overview.md +++ b/docs/source/backends-overview.md @@ -23,7 +23,7 @@ Backends are the bridge between your exported model and the hardware it runs on. | [XNNPACK](backends/xnnpack/xnnpack-overview.md) | All | CPU | General-purpose, fallback | | [CUDA](backends/cuda/cuda-overview.md) | Linux/Windows | GPU | NVIDIA GPU acceleration | | [Core ML](backends/coreml/coreml-overview.md) | iOS, macOS | NPU/GPU/CPU | Apple devices, high performance | -| [MLX](/backends/mlx/mlx-overview.md) | macOS | GPU | Apple Silicon GPU (MLX) | +| [MLX](/backends/mlx/mlx-overview.md) | iOS (experimental), macOS | GPU | Apple Silicon GPU (MLX) | | [Vulkan](backends/vulkan/vulkan-overview.md) | Android, Linux, Windows | GPU | Android devices (mature); Desktops (experimental) | | [WebGPU](backends/webgpu/webgpu-overview.md) | Browser, Linux, macOS | GPU | Cross-platform and browser GPU execution (experimental) | | [Qualcomm](backends-qualcomm) | Android | NPU | Qualcomm SoCs | diff --git a/docs/source/backends/mlx/mlx-overview.md b/docs/source/backends/mlx/mlx-overview.md index c22e4d7b67d..ccd73c42b03 100644 --- a/docs/source/backends/mlx/mlx-overview.md +++ b/docs/source/backends/mlx/mlx-overview.md @@ -16,8 +16,14 @@ The MLX delegate is experimental and under active development. ## Target Requirements -- Apple Silicon Mac (M1 or later) -- [macOS](https://developer.apple.com/macos) >= 14.0 +One of: + +- [macOS](https://developer.apple.com/macos) >= 14.0 on an Apple Silicon Mac (M1 or later) +- [iOS](https://developer.apple.com/ios) or [iPadOS](https://developer.apple.com/ipados) >= 17.0 + on a real device (experimental). The backend is built and shipped for iOS, but + running a model on a physical device is not yet covered by CI. The iOS simulator + has no Metal device MLX can use, so the backend reports itself unavailable there + and a model delegated to it will not load. ## Development Requirements diff --git a/docs/source/using-executorch-ios.md b/docs/source/using-executorch-ios.md index e319ff0a60c..7053c28fd76 100644 --- a/docs/source/using-executorch-ios.md +++ b/docs/source/using-executorch-ios.md @@ -23,6 +23,8 @@ Link your binary with the ExecuTorch runtime and any backends or kernels used by **Note:** To access logs, link against the Debug build of the ExecuTorch runtime, i.e., the `executorch_debug` framework. For optimal performance, always link against the Release version of the deliverables (those without the `_debug` suffix), which have all logging overhead removed. See the [Logging](#Logging) section for more details. +**Note:** The MLX backend links and registers on the iOS simulator, so an app builds for both destinations, but it reports itself unavailable there because the simulator has no Metal device it can use. A model delegated to MLX will not load on the simulator. Use a real device or a Mac to run one. + ### Swift Package Manager The prebuilt ExecuTorch runtime, backend, and kernels are available as a [Swift PM](https://www.swift.org/documentation/package-manager/) package. diff --git a/runtime/executor/test/backend_integration_test.cpp b/runtime/executor/test/backend_integration_test.cpp index 90b1b80c768..89f3b530416 100644 --- a/runtime/executor/test/backend_integration_test.cpp +++ b/runtime/executor/test/backend_integration_test.cpp @@ -347,6 +347,28 @@ TEST_P(BackendIntegrationTest, BasicInitSucceeds) { EXPECT_EQ(method_res.error(), Error::Ok); } +TEST_P(BackendIntegrationTest, UnavailableBackendFailsToLoad) { + // A backend that reports itself unavailable must make load_method return + // NotFound rather than being initialized anyway. Backends that cannot run on + // the current platform rely on this to fail instead of faulting. + StubBackend::singleton().install_is_available([]() { return false; }); + + Result loader = FileDataLoader::from(program_path()); + ASSERT_EQ(loader.error(), Error::Ok); + Result program = Program::load(&loader.get()); + ASSERT_EQ(program.error(), Error::Ok); + + // The gate only means something if this method actually delegates to the + // stub, so confirm that before asserting the load fails on availability. + EXPECT_TRUE( + program->method_meta("forward")->uses_backend(StubBackend::kName)); + + ManagedMemoryManager mmm(kDefaultNonConstMemBytes, kDefaultRuntimeMemBytes); + + Result method_res = program->load_method("forward", &mmm.get()); + EXPECT_EQ(method_res.error(), Error::NotFound); +} + TEST_P(BackendIntegrationTest, GetBackendNamesSuccess) { // Load the program from file. Result loader = FileDataLoader::from(program_path());