diff --git a/studio/backend/requirements/extras-no-deps.txt b/studio/backend/requirements/extras-no-deps.txt index 4dfbe871a3..846a4de5b3 100644 --- a/studio/backend/requirements/extras-no-deps.txt +++ b/studio/backend/requirements/extras-no-deps.txt @@ -10,20 +10,16 @@ snac peft==0.18.1 # TRL and related packages -# macOS arm64: trl 0.23.1 transitively requires huggingface-hub<1, which -# conflicts with the mlx-vlm chain's huggingface-hub>=1.5.0. Skip the pin -# here on darwin arm64 so the resolver can pick a trl version compatible -# with the realigned transformers / huggingface-hub stack. +# 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" 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: unsloth-zoo's mlx-vlm chain requires transformers>=5.1.0 (and -# >=5.5.0 for mlx-vlm 0.5.0). The base-update resolver picks a compatible -# version under --no-constraints; reinstalling 4.57.6 here with --no-deps -# would silently downgrade transformers and break mlx-vlm imports at runtime. -# constraints.txt still enforces 4.57.6 on every other platform. +# 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" 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 979593f041..ecdc8c76a5 100644 --- a/studio/backend/requirements/single-env/constraints.txt +++ b/studio/backend/requirements/single-env/constraints.txt @@ -1,16 +1,9 @@ # Single-env pins for unsloth + studio + data-designer # Keep compatible with unsloth transformers bounds. # -# macOS arm64 carve-out: every `==` pin below is gated on -# `sys_platform != "darwin" or platform_machine != "arm64"` because the -# latest unsloth-zoo's mlx-vlm chain requires newer transformers (>=5.1.0, -# >=5.5.0 for mlx-vlm 0.5.0), which in turn pulls newer huggingface-hub, -# datasets, etc. Keeping these old pins active would force the resolver -# to backtrack to a 2025-era unsloth on macOS arm64 even when the base -# step's --no-constraints fallback has already selected a working stack. -# mlx-vlm only publishes wheels for darwin arm64, so no other platform is -# affected by the carve-out. Range pins (e.g. mcp>=1.24,<2) stay active -# everywhere since they do not conflict with the mlx-vlm chain. +# 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" diff --git a/studio/backend/requirements/studio.txt b/studio/backend/requirements/studio.txt index 09cef57cd3..6016c33bfa 100644 --- a/studio/backend/requirements/studio.txt +++ b/studio/backend/requirements/studio.txt @@ -7,11 +7,8 @@ packaging matplotlib pandas nest_asyncio -# macOS arm64 carve-out: the latest unsloth-zoo's mlx-vlm chain requires -# newer transformers + huggingface-hub + datasets; the legacy single-env -# pins below would otherwise force the resolver to backtrack transformers -# to a version incompatible with mlx-vlm. mlx-vlm only publishes wheels -# for darwin arm64 so no other platform is affected. +# 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" pyjwt easydict diff --git a/studio/backend/utils/hardware/hardware.py b/studio/backend/utils/hardware/hardware.py index ede37e2953..dfd90832a3 100644 --- a/studio/backend/utils/hardware/hardware.py +++ b/studio/backend/utils/hardware/hardware.py @@ -144,9 +144,7 @@ def detect_hardware() -> DeviceType: if is_apple_silicon() and _has_mlx(): DEVICE = DeviceType.MLX CHAT_ONLY = False - # platform.processor() runs `uname -p` which returns "i386" on most - # universal2 / Rosetta-shaped Python builds even on native arm64. - # platform.machine() is "arm64" once is_apple_silicon() has gated us. + # platform.processor() returns "i386" on universal2 builds; use machine(). chip = platform.machine() or "arm64" print(f"Hardware detected: MLX — Apple Silicon ({chip})") return DEVICE @@ -282,8 +280,7 @@ def get_gpu_memory_info() -> Dict[str, Any]: try: info = mx.device_info() - # See detect_hardware(): platform.processor() can return "i386" - # on native arm64 Python builds, so prefer machine() as fallback. + # See detect_hardware(): use machine() not processor(). gpu_name = info.get("device_name") or platform.machine() or "arm64" except Exception: gpu_name = platform.machine() or "arm64" diff --git a/studio/install_python_stack.py b/studio/install_python_stack.py index a21699c12a..9c56147d02 100644 --- a/studio/install_python_stack.py +++ b/studio/install_python_stack.py @@ -430,25 +430,11 @@ NO_TORCH = _infer_no_torch() @functools.lru_cache(maxsize = 8) def _resolve_latest_pypi_version(package: str, *, timeout: float = 10.0) -> str | None: - """Return the latest published version of ``package`` on PyPI. - - Used to pin a lower-bound floor on the `unsloth studio update` upgrade - step. With base.txt's unpinned `unsloth`/`unsloth-zoo` entries, uv's - resolver will silently backtrack to an older release whenever a - transitive constraint (e.g. bitsandbytes wheel availability on macOS - arm64) makes the unpinned requirement satisfiable by an older version. - Returns None on network failure so the caller can fall back to the - historical behaviour without breaking offline installs. - - The lru_cache means the three upgrade branches share a single PyPI - round-trip per package within one ``install_python_stack`` invocation. - """ + """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) - # OSError covers socket.timeout / TimeoutError on all supported Pythons; - # URLError covers DNS / cert / refused; ValueError covers bad JSON. except (urllib.error.URLError, OSError, ValueError): return None return (data.get("info") or {}).get("version") or None @@ -457,25 +443,11 @@ def _resolve_latest_pypi_version(package: str, *, timeout: float = 10.0) -> str def _pin_floor_args( *, include_unsloth: bool = True, include_zoo: bool = True ) -> list[str]: - """Build the positional `unsloth>=LATEST` / `unsloth-zoo>=LATEST` args. + """Build `unsloth>=LATEST` / `unsloth-zoo>=LATEST` floor args. - Network failures yield an empty list so the caller falls back to the - historical unpinned behaviour. All-or-nothing: if any requested lookup - fails, the entire floor is dropped. A half-floor would let the unpinned - package backtrack while still requiring the other at latest, which - defeats the whole point of pinning both. - - ``include_unsloth=False`` is used by the no-torch branch when - ``--package`` overrides the default package name (test builds publish - to side packages that may not exist on PyPI). ``include_zoo=False`` is - used together with ``include_unsloth=False`` for the same reason: a - custom-package side build often pins its own unsloth-zoo fork via - dependency metadata, and the public PyPI floor can conflict with that - fork's published version. - - A single warning is printed when any lookup fails so the user knows - the upgrade has degraded to the pre-fix resolver semantics (e.g. - behind a corporate proxy / captive portal / firewalled PyPI mirror). + 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: @@ -897,10 +869,7 @@ def pip_install_try( return True if VERBOSE and result.stdout: print(result.stdout.decode(errors = "replace")) - # fall through to pip retry: mirrors pip_install()'s uv-to-pip - # fallback so a uv-specific failure (e.g. torch-backend probe, - # transient resolver bug) does not abandon the floor pin when - # pip itself could have applied it. + # 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, @@ -925,29 +894,11 @@ def pip_install_with_floor_fallback( req: Path | None = None, constrain: bool = True, ) -> None: - """Run pip_install with a soft lower-bound floor and fall back unpinned. + """3-level fallback: floor+constraints -> floor only -> unpinned. - Tried in order: - - 1. ``args + floor`` with ``-c constraints.txt`` -- the strict case - (works on Linux, Windows, and macos-14+ where the latest - unsloth-zoo stack is wheel-compatible). - 2. ``args + floor`` WITHOUT constraints -- macOS arm64 needs this - because the single-env constraints pin ``transformers==4.57.6`` - while ``unsloth-zoo``'s ``mlx-vlm`` dep requires - ``transformers>=5.1.0``. Skipping constraints lets the resolver - pick a transformers version that satisfies both; downstream - constrained steps still apply the pin to anything that doesn't - transitively conflict. - 3. ``args`` with no floor and the caller's ``constrain`` setting -- - the historical pre-fix code path. Last resort so the update - still completes (possibly stale) instead of failing outright. - - ``UNSLOTH_NO_PYPI_FLOOR=1`` jumps straight to step 3 -- useful for - air-gapped CI / corporate mirrors that intentionally do not expose - pypi.org directly, or for users on a private index whose mirror - lags pypi.org and so cannot serve the floor version yet (step 3 - also catches this case transparently via the unpinned fallback). + 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", @@ -959,9 +910,7 @@ def pip_install_with_floor_fallback( return if pip_install_try(label, *args, *floor, req = req, constrain = constrain): return - # Strict floor failed -- the single-env constraints.txt is the most - # common cause on macOS arm64. Retry without constraints; downstream - # steps re-apply them where they matter. + # Step 2: drop constraints (downstream steps re-apply where needed) if pip_install_try(label, *args, *floor, req = req, constrain = False): _step( "warning", @@ -1146,12 +1095,8 @@ def install_python_stack() -> int: package_name, "--upgrade-package", "unsloth-zoo", - # Force the resolver to consider transformers + mlx-vlm afresh - # so a venv carrying a stale transformers (from an older install - # that pinned 4.57.6 via constraints.txt) does not end up paired - # with a newer unsloth-zoo's mlx-vlm requirement. Both flags are - # no-ops when the package is absent (mlx-vlm only ships wheels - # on darwin arm64) so this is safe to apply unconditionally. + # 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", @@ -1199,13 +1144,9 @@ def install_python_stack() -> int: constrain = False, ) elif local_repo: - # 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). Pin a floor at PyPI latest for the same - # reason the standard update path does -- see comment below. - # The local-repo path is for `unsloth studio update --local`, - # which always operates against the public unsloth/unsloth-zoo - # PyPI distros, so the floor is unconditional here. + # 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. _progress("base packages") pip_install_with_floor_fallback( "Updating base packages", @@ -1214,8 +1155,6 @@ def install_python_stack() -> int: "unsloth", "--upgrade-package", "unsloth-zoo", - # Re-resolve transformers + mlx-vlm too -- see explanation in the - # NO_TORCH branch above (stale transformers / new mlx-vlm split). "--upgrade-package", "transformers", "--upgrade-package", @@ -1250,20 +1189,11 @@ def install_python_stack() -> int: package_name, ) else: - # 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. - # Pin a floor at the current PyPI latest so the resolver cannot - # silently backtrack to an older release when a transitive constraint - # (e.g. macOS arm64 bitsandbytes wheel availability) makes the - # unpinned `unsloth` requirement in base.txt satisfiable by a much - # older version. Mirrors the explicit floor install.sh maintains. - # Soft floor: if the resolver cannot satisfy the floor on this - # platform (e.g. macOS 13 arm64 where the latest mlx wheel - # requires macOS 14+), fall back to the unpinned resolution - # rather than erroring out -- preserves the pre-fix - # "succeed-but-stale" behaviour as a last resort with a clear - # warning to the user. + # 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. _progress("base packages") pip_install_with_floor_fallback( "Updating base packages", @@ -1272,8 +1202,6 @@ def install_python_stack() -> int: "unsloth", "--upgrade-package", "unsloth-zoo", - # Re-resolve transformers + mlx-vlm too -- see explanation in the - # NO_TORCH branch above (stale transformers / new mlx-vlm split). "--upgrade-package", "transformers", "--upgrade-package", @@ -1282,30 +1210,12 @@ def install_python_stack() -> int: req = REQ_ROOT / "base.txt", ) - # 2a. macOS arm64: realign mlx-vlm + transformers after the base step. - # The latest unsloth-zoo pulls mlx-vlm (latest: 0.5.0 requires - # transformers>=5.5.0) but `--upgrade-package transformers` alone is - # not enough on uv: when an older transformers is already installed - # and still satisfies unsloth's own range, the resolver does not - # upgrade it -- leaving mlx-vlm 0.5.0 paired with transformers 4.57.6 - # in the venv. Force a separate constraints-free install of both - # packages so the resolver picks a mutually-consistent pair. - # constrain=False because constraints.txt's old single-env pins - # would otherwise pin transformers back to 4.57.6 (the darwin-arm64 - # marker carve-out makes most pins inert, but staying constraint-free - # here keeps the contract simple). + # 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": - # Realign mlx-vlm + transformers + huggingface_hub on darwin arm64. - # Every other approach (--upgrade, --upgrade-package, --force-reinstall, - # explicit `transformers>=X` pin) failed to actually upgrade transformers - # under uv because the already-installed transformers 4.57.6 satisfies - # unsloth-zoo's range, and uv treats that as decisive even though - # mlx-vlm's stricter `>=5.5.0` is unsatisfied. Cut the resolver out - # of the loop: uninstall the conflicting trio first, then install - # them fresh with no transformers in the venv. The resolver is then - # forced to pick a version satisfying every installed package's - # requirements (unsloth + unsloth-zoo + mlx-vlm), which on darwin - # arm64 with the latest unsloth-zoo is uniquely transformers==5.5.0. _progress("mlx-vlm/transformers realign") try: subprocess.run( diff --git a/studio/setup.ps1 b/studio/setup.ps1 index 3021e8e717..f71ce30605 100644 --- a/studio/setup.ps1 +++ b/studio/setup.ps1 @@ -1725,11 +1725,9 @@ $_PkgName = if ($env:STUDIO_PACKAGE_NAME) { $env:STUDIO_PACKAGE_NAME } else { "u $SkipPythonDeps = $false if ($env:SKIP_STUDIO_BASE -ne "1" -and $env:STUDIO_LOCAL_INSTALL -ne "1") { - # Only check when NOT called from install.ps1 (which just installed the package). - # Check BOTH unsloth and unsloth-zoo -- a stale zoo while unsloth itself is - # at latest is the most common bug-trigger after the macOS arm64 resolver - # backtrack (PR #5767); the old single-package check would print - # "up to date" and skip the update entirely. + # Check unsloth + unsloth-zoo. A stale zoo with unsloth at latest is the + # common macOS arm64 backtrack symptom; single-package check would say + # "up to date" and skip the update. $InstalledVer = try { (& python -c "from importlib.metadata import version; print(version('$_PkgName'))" 2>$null | Out-String).Trim() } catch { "" } $LatestVer = "" try { @@ -1737,10 +1735,8 @@ if ($env:SKIP_STUDIO_BASE -ne "1" -and $env:STUDIO_LOCAL_INSTALL -ne "1") { $LatestVer = "$($pypiJson.info.version)".Trim() } catch { } - # Only probe public unsloth-zoo when the package being managed IS unsloth. - # Custom side packages (e.g. $env:STUDIO_PACKAGE_NAME=roland-sloth) ship - # their own zoo fork via dependency metadata and may not install public - # unsloth-zoo at all; checking it would force a no-op update. + # Only probe public unsloth-zoo when managing unsloth itself. Custom + # STUDIO_PACKAGE_NAME builds may pin their own zoo fork. $CheckZoo = ($_PkgName -eq "unsloth") $InstalledZooVer = "" diff --git a/studio/setup.sh b/studio/setup.sh index 6c825c41de..d769dd9200 100755 --- a/studio/setup.sh +++ b/studio/setup.sh @@ -536,11 +536,9 @@ if [ "$_COLAB_NO_VENV" = true ]; then fi _PKG_NAME="${STUDIO_PACKAGE_NAME:-unsloth}" if [ "$_SKIP_VERSION_CHECK" != true ] && [ "${SKIP_STUDIO_BASE:-0}" != "1" ] && [ "${STUDIO_LOCAL_INSTALL:-0}" != "1" ]; then - # Only check when NOT called from install.sh (which just installed the package). - # Check BOTH unsloth and unsloth-zoo -- a stale zoo while unsloth itself is at - # latest is the most common bug-trigger after the macOS arm64 resolver - # backtrack (PR #5767), and the old single-package check would print - # "up to date" and skip the update entirely. + # Check unsloth + unsloth-zoo. A stale zoo with unsloth at latest is the + # common macOS arm64 backtrack symptom; single-package check would say + # "up to date" and skip the update. INSTALLED_VER=$("$VENV_DIR/bin/python" -c " import sys; from importlib.metadata import version print(version(sys.argv[1])) @@ -550,10 +548,8 @@ print(version(sys.argv[1])) | "$VENV_DIR/bin/python" -c "import sys,json; print(json.load(sys.stdin)['info']['version'])" 2>/dev/null \ || echo "") - # Only probe public unsloth-zoo when the package being managed IS unsloth. - # Custom side packages (e.g. STUDIO_PACKAGE_NAME=roland-sloth) ship their - # own zoo fork via dependency metadata and may not install public - # unsloth-zoo at all; checking it would force a no-op update. + # Only probe public unsloth-zoo when managing unsloth itself. Custom + # STUDIO_PACKAGE_NAME builds may pin their own zoo fork. _CHECK_ZOO=false [ "$_PKG_NAME" = "unsloth" ] && _CHECK_ZOO=true