Skip to content

Headless/no-GPU environments crash on MLX import (NSRangeException) instead of failing gracefully #3148

Closed

Description

@jrp2014

Summary

In environments where no Metal device is visible (headless/sandboxed/virtualized/macOS automation sessions),
MLX initialization aborts the process with an uncaught Objective-C exception instead of returning
a recoverable Python error.

This makes downstream tooling fail hard, including quality/lint/test pipelines that do not need GPU execution.

Reproduction

  1. Run in a session with no visible Metal device.
  2. Execute:
python -c "import mlx.core as mx; print(mx.default_device())"

(Equivalent failure also occurs during import mlx via dependency probes.)

Actual behavior

Process exits with signal/abort (-6) and an uncaught exception similar to:

NSRangeException: -[__NSArray0 objectAtIndex:]: index 0 beyond bounds for empty array

Expected behavior

  • No hard abort.
  • Either:
    1. raise a clear Python exception (e.g., RuntimeError: No Metal device available), or
    2. allow a documented CPU/no-op mode for import-time checks.
  • Error path should be machine-detectable so CI tools can handle it gracefully.

Why this matters

Hard aborts break non-inference workflows (lint/type/test/packaging checks) when MLX is installed
but GPU is unavailable. A recoverable error would allow callers to skip MLX-dependent runtime tests
without crashing the whole process.

Suggested fix

  • Guard the zero-device path before indexing into device arrays. load_device() in mlx/backend/metal/device.cpp is the primary fix site (empty-device guard + graceful error path).
  • Convert this failure path to a typed Python exception instead of process termination.
  • Optionally provide an env flag to skip Metal probing at import time in CI/headless contexts.

The main code path is:

Related existing report: Issue #2691.

Activity

  1. awni commented on Feb 20, 2026

    @awni
    Member

    We should guard the device loading and throw a more sensible error when no device is present. CC @JakeHillion

    Regarding actually running MLX with Metal enabled on a Mac where the GPU is not accessible that is trickier.

    The simplest thing is to build the CPU-only version and run that. Use Cmake arg MLX_BUILD_METAL=OFF. If building from source is not desirable we can investigate options to make the GPU availability check more dynamic

  2. JakeHillion commented on Feb 20, 2026

    @JakeHillion

    Tools like PyInstaller expect to be able to import all the Python when bundling. If you do this in a sandboxed environment it fails, and using the CPU version of MLX wouldn't solve that. It does seem the best solution here is to not poke at hardware at import time, and instead rely on initialising anything depending on the GPU at first use.

  3. jrp2014 commented on Feb 23, 2026

    @jrp2014
    ContributorAuthor

    OK. Can we at least implement what @awni suggests (guard the device loading and throw a more sensible error when no device is present)? I suggest that rather than implementing some arbitrary default behaviour, an exception should be thrown and the calling code should decide whether to go with the cpu only or to abort or ...?

  4. dm3rdnv commented on Apr 19, 2026

    @dm3rdnv
  5. added 6 commits that reference this issue on Apr 19, 2026
    2d2749e
    1358b7b
    c71e588
    b41c754
    b715268
    7361426
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions