From 22b6eb8926d159fcf7ef36b0eeb87ead84afdef8 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Thu, 7 May 2026 03:00:21 +0000 Subject: [PATCH] ci(mlx): add real Apple Silicon job on free macos-14 runner GitHub-hosted macos-14 is the M1 standard runner (3 vCPU, 7 GB RAM, 14 GB storage) and is FREE for public repositories per the GitHub Actions billing reference. Larger variants (macos-14-large, macos-14-xlarge) are billed; we deliberately avoid those. unslothai/unsloth and unslothai/unsloth-zoo are both public, so adding a single macos-14 job to MLX CI costs zero minutes against the org's billing quota while closing the only remaining gap the spoofed Linux job cannot reach: the actual Apple Silicon dispatch path. Specifically the new mlx-real-apple-silicon job: - Installs the real mlx and mlx-lm packages from PyPI. - Verifies platform.system()=='Darwin' and platform.machine()=='arm64' naturally, with no monkeypatch. - Imports unsloth and asserts unsloth._IS_MLX is True so the gate flips on real hardware as it is supposed to. - Smoke-imports every PR-A MLX-only module: mlx_loader, mlx_trainer, mlx_compile, mlx_utils, mlx_cce, gated_delta_vjp. These all do `import mlx.core as mx` at module level; this is the test that catches a future change to those modules that would only surface on a real Mac. - Re-runs the same three dispatch test files the Linux job runs. The monkeypatch spoofs still apply on real hardware, so this is also the canary that the spoofs do not collide with the real environment. The Linux job is unchanged. Both jobs trigger on the same path filter; mlx-real-apple-silicon caps at 15 minutes since the mlx install is heavier than the Linux dep set. --- .github/workflows/mlx-ci.yml | 96 ++++++++++++++++++++++++++++++++---- 1 file changed, 86 insertions(+), 10 deletions(-) diff --git a/.github/workflows/mlx-ci.yml b/.github/workflows/mlx-ci.yml index 9dae97c30c..7b105a23c5 100644 --- a/.github/workflows/mlx-ci.yml +++ b/.github/workflows/mlx-ci.yml @@ -1,17 +1,27 @@ # SPDX-License-Identifier: AGPL-3.0-only # Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. -# Focused PR gate for the MLX dispatch surface. Runs the three test files -# documented in tests/studio/README.md against a Linux+CPU runner with -# hardware probes spoofed (no Apple Silicon, no real GPU, no real MLX -# install required): +# Focused PR gate for the MLX dispatch surface. Two jobs: # -# - test_hardware_dispatch_matrix.py parametrized 7-profile matrix -# + 2 dispatch-priority canaries -# - test_is_mlx_dispatch_gate.py AST + runtime guard on -# unsloth._IS_MLX -# - test_mlx_training_worker_behaviors.py AST contract checks on -# studio/backend/core/training/worker.py +# 1. mlx-dispatch (Linux+CPU) +# Runs the three test files documented in tests/studio/README.md with +# hardware probes spoofed (no Apple Silicon, no real GPU, no real MLX +# install required): +# - test_hardware_dispatch_matrix.py parametrized 7-profile matrix +# + 2 dispatch-priority canaries +# - test_is_mlx_dispatch_gate.py AST + runtime guard on +# unsloth._IS_MLX +# - test_mlx_training_worker_behaviors.py AST contract checks on +# studio/backend/core/training/worker.py +# +# 2. mlx-real-apple-silicon (macOS-14, M1) +# Runs the SAME tests on a real Apple Silicon runner with a real mlx +# install. macos-14 is GitHub-hosted standard (3 vCPU / 7GB / M1) and +# is FREE for public repositories per the GitHub Actions billing +# docs, so adding this job costs zero minutes. Catches the residual +# gap the spoofed Linux job cannot: real `import mlx`, real +# `_IS_MLX = True` activation, real `unsloth_zoo.mlx_loader` +# submodule load, and a sanity import of every PR-A MLX module. # # Surfaces "MLX dispatch broke" as its own check in the PR UI rather than # burying it inside the broader Backend CI. The Backend CI Repo tests @@ -86,3 +96,69 @@ jobs: tests/studio/test_hardware_dispatch_matrix.py \ tests/studio/test_is_mlx_dispatch_gate.py \ tests/studio/test_mlx_training_worker_behaviors.py + + mlx-real-apple-silicon: + name: MLX real Apple Silicon (macos-14, M1) + runs-on: macos-14 + timeout-minutes: 15 + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + with: + python-version: '3.12' + cache: 'pip' + + # Real mlx install. The GitHub-hosted macos-14 runner is M1 / arm64 + # so platform.system() == 'Darwin' and platform.machine() == 'arm64' + # without any spoofing -- exactly the conditions unsloth._IS_MLX + # checks for. Once mlx is on the path the gate flips True and the + # MLX import branch in unsloth/__init__.py runs. + - name: Install deps (real mlx + unsloth + unsloth-zoo) + run: | + python -m pip install --upgrade pip + pip install mlx mlx-lm + pip install pytest pytest-asyncio + pip install 'unsloth_zoo>=2026.5.1' + pip install -e . --no-deps + + - name: Verify _IS_MLX flips True on real Apple Silicon + run: | + python -c " + import platform + assert platform.system() == 'Darwin', platform.system() + assert platform.machine() == 'arm64', platform.machine() + import unsloth + assert unsloth._IS_MLX is True, f'expected _IS_MLX=True on real Apple Silicon, got {unsloth._IS_MLX}' + print('OK: _IS_MLX activated on real Apple Silicon') + " + + - name: Smoke-import every MLX-only unsloth_zoo module + run: | + python -c " + import importlib + for name in [ + 'unsloth_zoo.mlx_loader', + 'unsloth_zoo.mlx_trainer', + 'unsloth_zoo.mlx_compile', + 'unsloth_zoo.mlx_utils', + 'unsloth_zoo.mlx_cce', + 'unsloth_zoo.gated_delta_vjp', + ]: + importlib.import_module(name) + print('OK:', name) + from unsloth_zoo.mlx_loader import FastMLXModel + from unsloth_zoo.mlx_trainer import MLXTrainer, MLXTrainingConfig + assert hasattr(FastMLXModel, 'from_pretrained') + print('OK: FastMLXModel + MLXTrainer surface present') + " + + - name: MLX dispatch tests on real Apple Silicon (spoofs still apply) + env: + PYTHONPATH: ${{ github.workspace }}/studio + UNSLOTH_COMPILE_DISABLE: '1' + run: | + python -m pytest -v --tb=short \ + tests/studio/test_hardware_dispatch_matrix.py \ + tests/studio/test_is_mlx_dispatch_gate.py \ + tests/studio/test_mlx_training_worker_behaviors.py