unsloth/.github/workflows/mlx-ci.yml
Daniel Han 30ddc7cb8b ci(mlx): version-pin every pip install, consolidate to one matrix job
Pin every explicit pip install to an exact released version (latest
as of 2026-05-07 within each project's existing constraint range)
to reduce supply-chain surface and make rebuilds reproducible.
unsloth-zoo on Linux is the pinned PyPI release; on macOS it stays
on git main (PR-A is not yet on PyPI).

Also fold the previously separate mlx-dispatch (Linux) and
mlx-real-apple-silicon (macOS) jobs into a single matrix job with
labels linux-cpu-spoof and macos-m1-real, sharing the dispatch
test step so adding new MLX dispatch tests applies to both runners
automatically. The Mac-only smoke steps (verify _IS_MLX flips True
on real Apple Silicon, smoke-import every PR-A MLX-only module)
remain gated on if: matrix.real_mlx.

Validated locally against .macsim_venv3 with the pinned package
set: 35 passed + 1 skipped, matching the prior unpinned run.
2026-05-07 03:23:31 +00:00

250 lines
11 KiB
YAML

# 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. One job, two matrix
# variants:
#
# - linux-cpu-spoof ubuntu-latest with hardware probes spoofed via
# monkeypatch (no Apple Silicon, no real GPU, no
# real MLX install required).
# - macos-m1-real macos-14 (M1, 3 vCPU / 7 GB / Apple Silicon
# standard runner -- FREE for public repositories
# per the GitHub Actions billing reference, larger
# variants like macos-14-large/-xlarge are paid
# so we deliberately avoid those). Adds two
# Mac-only steps before the dispatch tests:
# 1. verify unsloth._IS_MLX flips True on real
# Apple Silicon (no spoof);
# 2. smoke-import every PR-A MLX-only module
# (mlx_loader, mlx_trainer, mlx_compile,
# mlx_utils, mlx_cce, gated_delta_vjp). Each
# does `import mlx.core as mx` at module top
# level, so this catches a future change that
# breaks the real `mlx` PyPI wheel without
# needing a Mac developer in the loop.
#
# Both variants then run the SAME three dispatch test files documented
# in tests/studio/README.md:
# - 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
#
# Surfaces two PR checks ("MLX dispatch (linux-cpu-spoof)" and
# "MLX dispatch (macos-m1-real)") sharing one workflow definition so
# adding new dispatch tests applies to both runners automatically.
#
# Security audit footprint: every package this workflow installs is
# already covered by .github/workflows/security-audit.yml -- the deps
# come from studio/backend/requirements/studio.txt and unsloth-zoo's
# pyproject (resolved transitively). The git+ install of unsloth-zoo
# is intentionally skipped by the audit (pip-audit cannot resolve a
# git URL through PyPI metadata; the audit comment in security-audit.yml
# documents this). No new package is introduced solely by MLX CI.
name: MLX CI
on:
pull_request:
paths:
- 'unsloth/__init__.py'
- 'unsloth/_gpu_init.py'
- 'studio/backend/utils/hardware/**'
- 'studio/backend/core/training/worker.py'
- 'studio/backend/core/inference/mlx_inference.py'
- 'tests/studio/test_hardware_dispatch_matrix.py'
- 'tests/studio/test_is_mlx_dispatch_gate.py'
- 'tests/studio/test_mlx_training_worker_behaviors.py'
- 'tests/conftest.py'
- '.github/workflows/mlx-ci.yml'
push:
branches: [main, pip]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
dispatch:
name: MLX dispatch (${{ matrix.label }})
runs-on: ${{ matrix.os }}
timeout-minutes: ${{ matrix.timeout }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
label: linux-cpu-spoof
real_mlx: false
timeout: 10
- os: macos-14
label: macos-m1-real
real_mlx: true
timeout: 15
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: '3.12'
cache: 'pip'
# Linux variant: spoofed hardware. Mirrors the Repo tests (CPU)
# dep set from studio-backend-ci.yml so unsloth's import chain
# succeeds on a runner without any GPU. CPU torch from the
# PyTorch index, transformers + datasets + bitsandbytes from
# the standard PyPI index, unsloth-zoo from PyPI.
# All explicit pip installs are version-pinned to a single
# released version. unsloth-zoo on Linux is the latest PyPI
# release; on macOS it is sourced from git main (PR-A is not
# yet on PyPI). The pin set was the latest as of 2026-05-07
# within each project's existing constraint ranges; bump
# alongside the rest of the security audit when a new release
# of any of these lands.
- name: Install deps (Linux+CPU spoof)
if: matrix.real_mlx == false
run: |
python -m pip install --upgrade pip
pip install -r studio/backend/requirements/studio.txt
pip install \
'python-multipart==0.0.27' \
'aiofiles==25.1.0' \
'sqlalchemy==2.0.49' \
'cryptography==48.0.0' \
'pyyaml==6.0.3' \
'jinja2==3.1.6' \
'mammoth==1.12.0' \
'unpdf==1.0.0' \
'requests==2.33.1' \
'typer==0.25.1' \
'numpy==2.4.4' \
'pytest==9.0.3' \
'pytest-asyncio==1.3.0' \
'httpx==0.28.1'
pip install --index-url https://download.pytorch.org/whl/cpu \
'torch==2.10.0' 'torchvision==0.25.0'
pip install 'transformers==5.5.0'
pip install 'bitsandbytes==0.49.2'
pip install 'unsloth_zoo==2026.5.1'
pip install -e . --no-deps
# macOS variant: real Apple Silicon. Install ladder validated
# locally against a Linux mac-sim venv (platform spoofed +
# mlx_simulation shim + real datasets/transformers/structlog).
#
# 1. studio/backend/requirements/studio.txt brings structlog,
# fastapi, etc. The hardware probe imports structlog at
# module top level.
# 2. Same pytest / numpy / httpx stack as the Linux variant.
# 3. torch is explicitly installed: unsloth-zoo's pyproject
# deliberately excludes torch on darwin+arm64 (mlx replaces
# it for runtime use), but the dispatch tests spoof
# torch.cuda / torch.xpu / torch.backends.mps via monkeypatch
# and so the test process needs torch importable. We pull
# from the PyTorch CPU index for both Linux and macOS so
# Apple Silicon gets the explicit cpu+MPS arm64 wheel rather
# than something the default PyPI resolver might pick up.
# https://download.pytorch.org/whl/cpu hosts
# torch-x.y.z-cp312-...-macosx_*_arm64.whl alongside the
# Linux x86_64 wheels.
# 4. unsloth-zoo from git main (NOT PyPI), WITH deps. PR-A's
# MLX support landed after the most recent unsloth-zoo PyPI
# release; the wheel still raises NotImplementedError on
# Apple Silicon when device_type.get_device_type() runs
# unguarded. Studio's own install.sh overlays unsloth-zoo
# from git main for the same reason. Pulling deps lets pip
# resolve the platform-conditional MLX-only wheels (mlx,
# mlx-lm, mlx-vlm gated on darwin+arm64 in unsloth-zoo's
# pyproject) AND the shared deps (datasets, transformers,
# sentencepiece, ...) that unsloth's MLX branch loads via
# dataprep/raw_text.py.
# 5. unsloth -e . --no-deps so the editable install does not
# fight the unsloth-zoo dep set.
# See the comment on the Linux variant -- same pin set, with
# two macOS-specific adjustments: no torchvision (zoo's mlx-vlm
# replaces it on Apple Silicon), no bitsandbytes (CUDA-only),
# no transformers explicit pin (zoo's deps already constrain
# it), and unsloth-zoo from git main rather than PyPI.
- name: Install deps (macOS real Apple Silicon)
if: matrix.real_mlx
run: |
python -m pip install --upgrade pip
pip install -r studio/backend/requirements/studio.txt
pip install \
'python-multipart==0.0.27' \
'aiofiles==25.1.0' \
'sqlalchemy==2.0.49' \
'cryptography==48.0.0' \
'pyyaml==6.0.3' \
'jinja2==3.1.6' \
'mammoth==1.12.0' \
'unpdf==1.0.0' \
'requests==2.33.1' \
'typer==0.25.1' \
'numpy==2.4.4' \
'pytest==9.0.3' \
'pytest-asyncio==1.3.0' \
'httpx==0.28.1'
pip install --index-url https://download.pytorch.org/whl/cpu \
'torch==2.10.0'
pip install "unsloth_zoo @ git+https://github.com/unslothai/unsloth-zoo"
pip install -e . --no-deps
# Mac-only sanity: confirm _IS_MLX activates on real Apple
# Silicon hardware with no platform spoof.
- name: Verify _IS_MLX flips True on real Apple Silicon
if: matrix.real_mlx
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')
"
# Mac-only sanity: confirm every PR-A MLX-only module loads
# against real mlx + mlx-lm + mlx-vlm wheels.
- name: Smoke-import every MLX-only unsloth_zoo module
if: matrix.real_mlx
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')
"
# Both variants run the same dispatch tests. The monkeypatch
# spoofs in test_hardware_dispatch_matrix.py override
# platform.system / platform.machine / torch.cuda /
# torch.xpu / torch.backends.mps for each test, so they
# behave identically on Linux and on real Apple Silicon. The
# macOS variant additionally proves the spoofs do not collide
# with the real environment.
- name: MLX dispatch tests (3 files, 36 tests)
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