From f8a43b87e8b46fd5e3b7942d38f15b58f3844aae Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Tue, 26 May 2026 13:02:54 +0000 Subject: [PATCH] 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 --- install.sh | 23 +- .../backend/requirements/extras-no-deps.txt | 9 +- .../requirements/single-env/constraints.txt | 16 +- studio/backend/requirements/studio.txt | 6 +- studio/install_python_stack.py | 241 ++++-------------- 5 files changed, 71 insertions(+), 224 deletions(-) diff --git a/install.sh b/install.sh index cc92fd52c2..d7c4c3c51e 100755 --- a/install.sh +++ b/install.sh @@ -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. diff --git a/studio/backend/requirements/extras-no-deps.txt b/studio/backend/requirements/extras-no-deps.txt index 846a4de5b3..23c61baa44 100644 --- a/studio/backend/requirements/extras-no-deps.txt +++ b/studio/backend/requirements/extras-no-deps.txt @@ -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 diff --git a/studio/backend/requirements/single-env/constraints.txt b/studio/backend/requirements/single-env/constraints.txt index ecdc8c76a5..156f78567e 100644 --- a/studio/backend/requirements/single-env/constraints.txt +++ b/studio/backend/requirements/single-env/constraints.txt @@ -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 diff --git a/studio/backend/requirements/studio.txt b/studio/backend/requirements/studio.txt index 6016c33bfa..96f8816b57 100644 --- a/studio/backend/requirements/studio.txt +++ b/studio/backend/requirements/studio.txt @@ -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 diff --git a/studio/install_python_stack.py b/studio/install_python_stack.py index 9c56147d02..a77e8138f9 100644 --- a/studio/install_python_stack.py +++ b/studio/install_python_stack.py @@ -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.