Skip to content

Support the free threaded build #2807

Description

@ngoldbaum

Currently, mlx and any package that depends on mlx fails to install on the free-threaded build:

goldbaum at Nathans-MBP in ~/Documents/guidance on implicit-event-loop
± python -VV
Python 3.14.0 free-threading build (main, Oct  8 2025, 10:32:29) [Clang 17.0.0 (clang-1700.3.19.1)]

goldbaum at Nathans-MBP in ~/Documents/guidance on implicit-event-loop
± pip install mlx
ERROR: Could not find a version that satisfies the requirement mlx (from versions: none)

[notice] A new release of pip is available: 25.2 -> 25.3
[notice] To update, run: pip install --upgrade pip
ERROR: No matching distribution found for mlx

And indeed, there aren't any cp314t wheels on PyPI.

It'd be really nice to be able to use mlx on the free-threaded build. Both PyTorch and jaxlib ship free-threaded wheels.

Also see https://py-free-threading.github.io for more about using the free-threaded build and https://py-free-threading.github.io/porting-extensions/ for more about updating extensions.

Myself and my team at Quansight have been actively working on adding support for the free-threaded build in community packages for the past year and a half. I can also loop in CPython core devs and other experts who can offer advice.

Currently our short-term goal is to get vLLM running on the free-threaded build. This is in support of that, since mlx is an indirect dependency on apple silicon macs, via xgrammar. See vllm-project/vllm#28762.

Activity

  1. ngoldbaum commented on Dec 5, 2025

    @ngoldbaum
    ContributorAuthor

    I'm able to build mlx and run the tests on my M3 Macbook Pro. I'm not sure why, but I had to force cmake to find the free-threaded python install I built using pyenv:

    diff --git a/CMakeLists.txt b/CMakeLists.txt
    index 3487c22c..933fc536 100644
    --- a/CMakeLists.txt
    +++ b/CMakeLists.txt
    @@ -272,6 +272,7 @@ target_link_libraries(mlx PRIVATE $<BUILD_INTERFACE:fmt::fmt-header-only>)
    
     if(MLX_BUILD_PYTHON_BINDINGS)
       message(STATUS "Building Python bindings.")
    +  set(Python_ROOT_DIR /Users/goldbaum/.pyenv/versions/3.14.1t)
       find_package(
         Python 3.8
         COMPONENTS Interpreter Development.Module
    

    After that, all but one of the tests pass:

    (.venv) ± PYTHON_GIL=0 python -m unittest discover python/tests
    .....................s......................................................F.......................s................................................................................s........sssssssssssssssss..............................................................................s..............ss..................................................................................................................................................................................................................................................................s.......................................................................s.......................transform(x)=State(a=array(11, dtype=int32), b=array(20, dtype=int32))
    .
    ======================================================================
    FAIL: test_grad_ids_pre_post (test_autograd.TestAutograd.test_grad_ids_pre_post)
    ----------------------------------------------------------------------
    Traceback (most recent call last):
      File "/Users/goldbaum/Documents/mlx/python/tests/test_autograd.py", line 785, in test_grad_ids_pre_post
        self.assertEqual(init_id, id(arrs[0]))
        ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^
    AssertionError: 4265975377344 != 4265975363904
    
    ----------------------------------------------------------------------
    Ran 657 tests in 67.071s
    

    It looks like there is a multithreaded backend for mlx.launch, I don't know offhand if the tests execute it. That said, pretty encouraging that all the C++ bindings seem to work correctly.

  2. ngoldbaum commented on Dec 8, 2025

    @ngoldbaum
    ContributorAuthor

    @awni @angeloskath hope you don't mind the ping - I think the discussion in this thread on a PR earlier this year that you two participated in is relevant to the test failure I've found.

    wjakob/nanobind@d793091 seems like it might be related, maybe? But this isn't a multithreaded test, so maybe not. All the uses of nb::cast seems to use the default automatic rv_policy as well.

    I'm a little mystified why there's any behavior difference. Any chance either of you have a little extra context?

    I guess I can also add some print debugging inside py_value_and_grad and try running with both builds to see where the divergence in behavior starts...

  3. ngoldbaum commented on Dec 9, 2025

    @ngoldbaum
    ContributorAuthor

    I'm not sure why, but I had to force cmake to find the free-threaded python install I built using pyenv

    I opened #2891 which fixes this issue for me.

  4. ngoldbaum commented on Dec 10, 2025

    @ngoldbaum
    ContributorAuthor

    I'm now thinking that the test succeeding on the GIL-enabled build is a red herring. Checking id's doesn't really verify anything, since the id can be the same for two distinct objects if the interpreter happens to re-use an object address.

    My guess is that because the free-threaded build / mimalloc has different object re-use semantics, this test has different behavaior.

    If I modify the test slightly:

    import mlx.core as mx
    
    def fun(arrs):
        return arrs[0]
    
    arrs = [mx.array(1.0)]
    arr = arrs[0]
    mx.grad(fun)(arrs)
    assert arr is arrs[0]
    

    That fails on both the free-threaded and GIL-enabled build.

    So I think it's probably safe to ignore this failure and either skip the test on the free-threaded build or simply delete the test because it's not checking something useful.

  5. awni commented on Dec 10, 2025

    @awni
    Member

    It's pretty weird to me that the modified test you posted fails. The whole point is that the objects should be the same (not distinct).

  6. ngoldbaum commented on Dec 10, 2025

    @ngoldbaum
    ContributorAuthor

    I think that's not actually happening. I think you made a mistake back here by writing the test using id instead of retaining a reference and using is and the object was never preserved, it just hapenned to work out that you always get back an object with the same id because CPython re-uses objects.

  7. awni commented on Dec 10, 2025

    @awni
    Member

    Yup sure seems like we're just getting lucky with that test. We should fix it properly

  8. ngoldbaum commented on Dec 10, 2025

    @ngoldbaum
    ContributorAuthor

    We should fix it properly

    Let me file a new issue for this. I tried to understand the object re-use semantics but am not very familiar with nanobind and didn't get much further than some vague printf debugging before I noticed the test doesn't make any sense in the first place.

  9. ngoldbaum commented on Dec 10, 2025

    @ngoldbaum
    ContributorAuthor

    Moving on to look at thread-safety, if I run the unit tests under unittest-ft to run them under a thread pool, I see:

    goldbaum at Nathans-MBP in ~/Documents/mlx on nanobind
    ± PYTHON_GIL=0 unittest-ft python/tests
    -[AGXG15XFamilyCommandBuffer tryCoalescingPreviousComputeCommandEncoderWithConfig:nextEncoderClass:]:1091: failed assertion `A command encoder is already encoding to this command buffer'
    [1]    6496 abort      PYTHON_GIL=0 unittest-ft python/tests
    

    Note that this crash happens on the GIL-enabled build and the free-threaded build, so there's some kind of use of global state that probably needs Python-level locking and this is likely a pre-existing issue on the GIL-enabled build.

  10. ngoldbaum commented on Dec 10, 2025

    @ngoldbaum
    ContributorAuthor

    #2067 has some prior discussion about multithreaded use

  11. ngoldbaum commented on Dec 11, 2025

    @ngoldbaum
    ContributorAuthor

    #2133 also has more about thread safety.

  12. ngoldbaum commented on Dec 12, 2025

    @ngoldbaum
    ContributorAuthor

    I looked at adding free-threaded CI today. There's a crash in the MPI tests on Linux that I need to chase down: https://github.com/ngoldbaum/mlx/actions/runs/20174769146/job/57919840988. I only get that far on the aarch64 runner, the Linux x86_64 runner crashes at the end of the installation step in such a way that it doesn't even produce logs in the github UI: https://github.com/ngoldbaum/mlx/actions/runs/20174769146/job/57919840967. Not sure what's happening there.

    I can't do any debugging on the Mac CI because they use self-hosted runners.

    After disabling the MPI test, I did get the CPU aarch64 Linux builds to pass with Python 3.14t: https://github.com/ngoldbaum/mlx/actions/runs/20175170976/job/57922579676.

    I think I'll try to figure out what's going wrong on the Linux tests and come back to Mac tests later. Maybe you could give my GitHub account sufficient permissions so the tests run automatically for my PRs?

    I'd also appreciate feedback on whether you'd prefer I only add one 3.14t CI job or if you'd be OK with adding one on Linux CPU, one Linux GPU, and one on MacOS. Currently all but the nightly cron tests uses Python 3.10.

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