Simplify mac-arm64 fix: install MLX stack with --no-deps

The previous approach (PyPI floor pin + 3-level fallback + macOS arm64
realign step + marker carve-outs on every == pin) was fighting symptoms.
The root cause is that unsloth-zoo declares mlx-vlm>=0.4.4 as a darwin
arm64 dep, and mlx-vlm 0.5.0's metadata pulls in transformers>=5.5.0,
which conflicts with the main venv's transformers==4.57.6 pin and forces
the resolver to backtrack unsloth.

Severing that chain at its source: install mlx + mlx-metal + mlx-lm +
mlx-vlm with --no-deps BEFORE unsloth-zoo. The resolver sees mlx-vlm
already installed (>=0.4.4) and never inspects its transformers metadata.
Per-model transformers version routing is already handled at runtime by
the side-car venvs in utils/transformers_version.py (.venv_t5_530 for
Ministral/GLM/Qwen3 MoE, .venv_t5_550 for Gemma 4).

Net change: -224 / +71 lines across install.sh, install_python_stack.py
and the three requirements files.

Reverted:
- _resolve_latest_pypi_version + _pin_floor_args + pip_install_with_floor_fallback
- macOS arm64 realign step (pip uninstall + reinstall)
- --upgrade-package transformers --upgrade-package mlx-vlm in base steps
- All ; sys_platform != "darwin" or platform_machine != "arm64" markers
  in constraints.txt, studio.txt, extras-no-deps.txt
- pip_install_try restored to its pre-PR signature

Added:
- install.sh: Apple Silicon MLX --no-deps install before unsloth (both
  fresh and migrated branches)
- install_python_stack.py: same step gated on IS_MAC_ARM and not skip_base

Kept (independent bugs):
- setup.sh / setup.ps1 dual-package zoo version check
- platform.processor() -> platform.machine() hardware-detect fix
This commit is contained in:
Daniel Han 2026-05-26 13:02:54 +00:00
commit f8a43b87e8
5 changed files with 71 additions and 224 deletions

View file

@ -1858,6 +1858,13 @@ if [ "$_MIGRATED" = true ]; then
# Migrated env: force-reinstall unsloth+unsloth-zoo to ensure clean state
# in the new venv location, while preserving existing torch/CUDA
substep "upgrading unsloth in migrated environment..."
# Apple Silicon: ensure MLX stack is present --no-deps before reinstalling
# unsloth-zoo (see comment in fresh-install branch below for rationale).
if [ "$OS" = "macos" ] && [ "$_ARCH" = "arm64" ]; then
substep "installing MLX stack (mlx + mlx-lm + mlx-vlm, --no-deps)..."
run_install_cmd "install MLX stack" uv pip install --python "$_VENV_PY" \
--no-deps --upgrade mlx mlx-metal mlx-lm mlx-vlm
fi
if [ "$SKIP_TORCH" = true ]; then
# No-torch: install unsloth + unsloth-zoo with --no-deps (current
# PyPI metadata still declares torch as a hard dep), then install
@ -2038,6 +2045,16 @@ elif [ -n "$TORCH_INDEX_URL" ]; then
;;
esac
fi
# Fresh: Apple Silicon -- install MLX stack --no-deps BEFORE unsloth so
# unsloth-zoo's mlx-vlm>=0.4.4 dep is already satisfied. Stops the resolver
# from inspecting mlx-vlm's transformers>=5.5.0 requirement (which would
# conflict with the venv's transformers==4.57.6 pin and backtrack unsloth).
# Per-model transformers routing happens at runtime via side-car venvs.
if [ "$OS" = "macos" ] && [ "$_ARCH" = "arm64" ]; then
substep "installing MLX stack (mlx + mlx-lm + mlx-vlm, --no-deps)..."
run_install_cmd "install MLX stack" uv pip install --python "$_VENV_PY" \
--no-deps --upgrade mlx mlx-metal mlx-lm mlx-vlm
fi
# Fresh: Step 2 - install unsloth, preserving pre-installed torch
tauri_log "STEP" "Installing Unsloth"
substep "installing unsloth (this may take a few minutes)..."
@ -2108,12 +2125,6 @@ else
fi
fi
# ── Install mlx-vlm on Apple Silicon (optional, for VLM training) ──
if [ "$OS" = "macos" ] && [ "$_ARCH" = "arm64" ]; then
substep "installing mlx-vlm (VLM training support)..."
run_install_cmd "install mlx-vlm" uv pip install --python "$_VENV_PY" mlx-vlm
fi
# ── Run studio setup ──
tauri_log "STEP" "Running Studio setup"
# When --local, use the repo's own setup.sh directly.

View file

@ -10,16 +10,11 @@ snac
peft==0.18.1
# TRL and related packages
# macOS arm64: trl 0.23.1 needs huggingface-hub<1, conflicts with mlx-vlm's
# huggingface-hub>=1.5.0. Pin gated off so resolver picks a compatible trl.
trl==0.23.1 ; sys_platform != "darwin" or platform_machine != "arm64"
trl==0.23.1
git+https://github.com/meta-pytorch/OpenEnv.git
# executorch>=1.0.1 # 41.5 MB - no imports in unsloth/zoo/studio
torch-c-dlpack-ext
sentence_transformers==5.2.0
# macOS arm64: gated off because mlx-vlm 0.5.0 needs transformers>=5.5.0.
# Reinstalling 4.57.6 with --no-deps here would silently downgrade and break
# mlx-vlm imports at runtime. Other platforms still get 4.57.6 via constraints.
transformers==4.57.6 ; sys_platform != "darwin" or platform_machine != "arm64"
transformers==4.57.6
pytorch_tokenizers
kernels==0.12.1

View file

@ -1,20 +1,16 @@
# Single-env pins for unsloth + studio + data-designer
# Keep compatible with unsloth transformers bounds.
#
# macOS arm64 carve-out: == pins are marker-gated off because unsloth-zoo's
# mlx-vlm chain (darwin arm64 only) needs transformers>=5.5.0. Range pins
# stay active everywhere -- they don't conflict.
transformers==4.57.6 ; sys_platform != "darwin" or platform_machine != "arm64"
trl==0.23.1 ; sys_platform != "darwin" or platform_machine != "arm64"
huggingface-hub==0.36.2 ; sys_platform != "darwin" or platform_machine != "arm64"
transformers==4.57.6
trl==0.23.1
huggingface-hub==0.36.2
# Studio stack
datasets==4.3.0 ; sys_platform != "darwin" or platform_machine != "arm64"
pyarrow==23.0.1 ; sys_platform != "darwin" or platform_machine != "arm64"
datasets==4.3.0
pyarrow==23.0.1
# FastMCP/OpenEnv compat
fastmcp>=3.0.2
mcp>=1.24,<2
websockets>=15.0.1
pandas==2.3.3 ; sys_platform != "darwin" or platform_machine != "arm64"
pandas==2.3.3

View file

@ -7,14 +7,12 @@ packaging
matplotlib
pandas
nest_asyncio
# macOS arm64: gated off -- mlx-vlm chain needs newer datasets/hub. Other
# platforms still get the pins below.
datasets==4.3.0 ; sys_platform != "darwin" or platform_machine != "arm64"
datasets==4.3.0
pyjwt
easydict
addict
# gradio>=4.0.0 # 148 MB - Studio uses React + FastAPI, not Gradio
huggingface-hub==0.36.2 ; sys_platform != "darwin" or platform_machine != "arm64"
huggingface-hub==0.36.2
structlog>=24.1.0
diceware
ddgs

View file

@ -12,15 +12,12 @@ PATH to point at the venv.
from __future__ import annotations
import functools
import json
import os
import platform
import shutil
import subprocess
import sys
import tempfile
import urllib.error
import urllib.request
from pathlib import Path
@ -428,48 +425,6 @@ def _infer_no_torch() -> bool:
NO_TORCH = _infer_no_torch()
@functools.lru_cache(maxsize = 8)
def _resolve_latest_pypi_version(package: str, *, timeout: float = 10.0) -> str | None:
"""Latest PyPI version, or None on network failure (caller falls back unpinned)."""
url = f"https://pypi.org/pypi/{package}/json"
try:
with urllib.request.urlopen(url, timeout = timeout) as response:
data = json.load(response)
except (urllib.error.URLError, OSError, ValueError):
return None
return (data.get("info") or {}).get("version") or None
def _pin_floor_args(
*, include_unsloth: bool = True, include_zoo: bool = True
) -> list[str]:
"""Build `unsloth>=LATEST` / `unsloth-zoo>=LATEST` floor args.
All-or-nothing: if any lookup fails, return `[]` so the caller falls back
unpinned. A half-floor would defeat the purpose. Custom STUDIO_PACKAGE_NAME
builds skip both (they may not publish to public PyPI).
"""
requested: list[str] = []
if include_unsloth:
requested.append("unsloth")
if include_zoo:
requested.append("unsloth-zoo")
versions: dict[str, str] = {}
for package in requested:
latest = _resolve_latest_pypi_version(package)
if not latest:
_step(
"warning",
"PyPI unreachable; skipping latest-version floor pin "
"(resolver may pick an older release on platforms where a "
"transitive dep restricts wheel availability)",
_cyan,
)
return []
versions[package] = latest
return [f"{pkg}>={version}" for pkg, version in versions.items()]
# -- Verbosity control ----------------------------------------------------------
# By default the installer shows a minimal progress bar (one line, in-place).
# Set UNSLOTH_VERBOSE=1 in the environment to restore full per-step output:
@ -828,7 +783,6 @@ def _build_uv_cmd(args: tuple[str, ...]) -> list[str]:
def pip_install_try(
label: str,
*args: str,
req: Path | None = None,
constrain: bool = True,
) -> bool:
"""Like pip_install but returns False on failure instead of exiting.
@ -840,94 +794,23 @@ def pip_install_try(
constraint_args_pip = ["-c", str(CONSTRAINTS)]
constraint_args_uv = ["-c", _uv_safe_path(CONSTRAINTS)]
actual_req = req
temp_reqs: list[Path] = []
if req is not None and IS_WINDOWS and WINDOWS_SKIP_PACKAGES:
actual_req = _filter_requirements(req, WINDOWS_SKIP_PACKAGES)
temp_reqs.append(actual_req)
if actual_req is not None and NO_TORCH and NO_TORCH_SKIP_PACKAGES:
actual_req = _filter_requirements(actual_req, NO_TORCH_SKIP_PACKAGES)
temp_reqs.append(actual_req)
req_args_pip: list[str] = []
req_args_uv: list[str] = []
if actual_req is not None:
req_args_pip = ["-r", str(actual_req)]
req_args_uv = ["-r", _uv_safe_path(actual_req)]
if USE_UV:
cmd = _build_uv_cmd(args) + constraint_args_uv
else:
cmd = _build_pip_cmd(args) + constraint_args_pip
try:
if VERBOSE:
_step(_LABEL, f"{label}...", _dim)
if USE_UV:
uv_cmd = _build_uv_cmd(args) + constraint_args_uv + req_args_uv
result = subprocess.run(
uv_cmd,
stdout = subprocess.PIPE,
stderr = subprocess.STDOUT,
**_windows_hidden_subprocess_kwargs(),
)
if result.returncode == 0:
return True
if VERBOSE and result.stdout:
print(result.stdout.decode(errors = "replace"))
# fall through to pip retry (mirrors pip_install)
pip_cmd = _build_pip_cmd(args) + constraint_args_pip + req_args_pip
result = subprocess.run(
pip_cmd,
stdout = subprocess.PIPE,
stderr = subprocess.STDOUT,
**_windows_hidden_subprocess_kwargs(),
)
if result.returncode == 0:
return True
if VERBOSE and result.stdout:
print(result.stdout.decode(errors = "replace"))
return False
finally:
for temp_req in temp_reqs:
temp_req.unlink(missing_ok = True)
def pip_install_with_floor_fallback(
label: str,
*args: str,
floor: list[str],
req: Path | None = None,
constrain: bool = True,
) -> None:
"""3-level fallback: floor+constraints -> floor only -> unpinned.
Step 2 catches macOS arm64 where constraints.txt pins transformers==4.57.6
but mlx-vlm needs >=5.1.0. Step 3 catches air-gapped / lagging mirrors.
UNSLOTH_NO_PYPI_FLOOR=1 jumps straight to step 3.
"""
skip_floor = os.environ.get("UNSLOTH_NO_PYPI_FLOOR", "").strip().lower() in (
"1",
"true",
"yes",
if VERBOSE:
_step(_LABEL, f"{label}...", _dim)
result = subprocess.run(
cmd,
stdout = subprocess.PIPE,
stderr = subprocess.STDOUT,
)
if skip_floor or not floor:
pip_install(label, *args, req = req, constrain = constrain)
return
if pip_install_try(label, *args, *floor, req = req, constrain = constrain):
return
# Step 2: drop constraints (downstream steps re-apply where needed)
if pip_install_try(label, *args, *floor, req = req, constrain = False):
_step(
"warning",
"Floor pin needed --no-constraints to resolve on this "
"platform; subsequent steps re-apply the single-env pins",
_cyan,
)
return
_step(
"warning",
"Floor pin unsatisfiable on this platform; falling back to "
"the unpinned resolution (you may end up on an older release "
"if a transitive dep restricts wheel availability -- check the "
"post-update version with `unsloth --version`)",
_cyan,
)
pip_install(label, *args, req = req, constrain = constrain)
if result.returncode == 0:
return True
if VERBOSE and result.stdout:
print(result.stdout.decode(errors = "replace"))
return False
def pip_install(
@ -1079,6 +962,26 @@ def install_python_stack() -> int:
[sys.executable, "-m", "pip", "install", "--upgrade", "pip"],
)
# 3a. macOS arm64: install MLX stack --no-deps BEFORE unsloth-zoo so its
# mlx-vlm>=0.4.4 dep is already satisfied. This stops the resolver from
# inspecting mlx-vlm's transformers>=5.5.0 metadata (which would conflict
# with the venv's transformers==4.57.6 pin and backtrack unsloth). Per-
# model transformers routing is handled at runtime by the side-car venvs
# in utils/transformers_version.py.
if IS_MAC_ARM and not skip_base:
_progress("MLX stack (Apple Silicon)")
pip_install(
"Installing MLX stack (mlx + mlx-lm + mlx-vlm)",
"--no-cache-dir",
"--no-deps",
"--upgrade",
"mlx",
"mlx-metal",
"mlx-lm",
"mlx-vlm",
constrain = False,
)
# 3. Core packages: unsloth-zoo + unsloth (or custom package name)
if skip_base:
pass
@ -1087,7 +990,7 @@ def install_python_stack() -> int:
# (current PyPI metadata still declares torch as a hard dep), then
# runtime deps with --no-deps (avoids transitive torch).
_progress("base packages (no torch)")
pip_install_with_floor_fallback(
pip_install(
f"Updating {package_name} + unsloth-zoo (no-torch mode)",
"--no-cache-dir",
"--no-deps",
@ -1095,18 +998,8 @@ def install_python_stack() -> int:
package_name,
"--upgrade-package",
"unsloth-zoo",
# Re-resolve transformers + mlx-vlm so a stale transformers does
# not stay paired with a newer unsloth-zoo (no-op off darwin arm64).
"--upgrade-package",
"transformers",
"--upgrade-package",
"mlx-vlm",
package_name,
"unsloth-zoo",
floor = _pin_floor_args(
include_unsloth = package_name == "unsloth",
include_zoo = package_name == "unsloth",
),
)
# Resolve pydantic WITH deps so pip pins pydantic-core to the
# exact version pydantic's metadata declares. Under --no-deps
@ -1144,22 +1037,17 @@ def install_python_stack() -> int:
constrain = False,
)
elif local_repo:
# Local dev install (`unsloth studio update --local`): update deps from
# base.txt then overlay the local checkout as editable (--no-deps so
# torch is preserved). Floor is unconditional -- always uses public PyPI.
# Local dev install: update deps from base.txt, then overlay the
# local checkout as an editable install (--no-deps so torch is
# never re-resolved).
_progress("base packages")
pip_install_with_floor_fallback(
pip_install(
"Updating base packages",
"--no-cache-dir",
"--upgrade-package",
"unsloth",
"--upgrade-package",
"unsloth-zoo",
"--upgrade-package",
"transformers",
"--upgrade-package",
"mlx-vlm",
floor = _pin_floor_args(),
req = REQ_ROOT / "base.txt",
)
_step(_LABEL, f"overlaying local repo (editable): {local_repo}")
@ -1189,61 +1077,20 @@ def install_python_stack() -> int:
package_name,
)
else:
# Update path: upgrade unsloth + unsloth-zoo while preserving existing
# torch (--upgrade-package targets only base pkgs). PyPI floor blocks
# the resolver from silently backtracking when a transitive constraint
# (e.g. macOS arm64 bitsandbytes wheel availability) makes the unpinned
# base.txt entry satisfiable by an older release.
# Update path: upgrade only unsloth + unsloth-zoo while preserving
# existing torch/CUDA installations. Torch is pre-installed by
# install.sh / setup.ps1; --upgrade-package targets only base pkgs.
_progress("base packages")
pip_install_with_floor_fallback(
pip_install(
"Updating base packages",
"--no-cache-dir",
"--upgrade-package",
"unsloth",
"--upgrade-package",
"unsloth-zoo",
"--upgrade-package",
"transformers",
"--upgrade-package",
"mlx-vlm",
floor = _pin_floor_args(),
req = REQ_ROOT / "base.txt",
)
# 2a. macOS arm64: realign mlx-vlm + transformers + huggingface_hub.
# uv's incumbent bias keeps transformers 4.57.6 even with
# --upgrade-package because it satisfies unsloth-zoo's range, ignoring
# mlx-vlm 0.5.0's stricter >=5.5.0. Uninstall the trio then reinstall
# with no constraints so the resolver picks the unique consistent set.
if IS_MAC_ARM and not skip_base and package_name == "unsloth":
_progress("mlx-vlm/transformers realign")
try:
subprocess.run(
[
sys.executable,
"-m",
"pip",
"uninstall",
"-y",
"transformers",
"mlx-vlm",
"huggingface_hub",
],
stdout = subprocess.PIPE,
stderr = subprocess.STDOUT,
**_windows_hidden_subprocess_kwargs(),
)
except Exception:
pass
pip_install(
"Realigning mlx-vlm + transformers (macOS arm64)",
"--no-cache-dir",
"mlx-vlm",
"transformers",
"huggingface_hub",
constrain = False,
)
# 2b. AMD ROCm: reinstall torch with HIP wheels if the host has ROCm but the
# venv received CPU-only torch (common when pip resolves torch from PyPI).
# Must come immediately after base packages so torch is present for inspection.