mirror of
https://github.com/unslothai/unsloth.git
synced 2026-05-22 02:50:03 +00:00
* refactor: consolidate dual venvs into single ~/.unsloth/studio/unsloth_studio
* refactor: separate install.sh (first-time) from setup.sh (smart update with PyPI version check)
* fix: install.sh calls setup.sh directly, keep both setup and update CLI commands
* fix: use importlib.resources.files() directly without _path attribute
* fix: bootstrap uv before pip upgrade to handle uv venvs without pip
* fix: frontend 404 when launched via CLI, add global symlink to ~/.local/bin
* feat: add --local flag to install.sh and unsloth studio update for branch testing
* fix: resolve repo root from script location for --local installs
* feat: add --package flag to install.sh for testing with custom package names
* feat: add --package flag to unsloth studio update
* fix: always nuke venv in install.sh for clean installs
* revert: remove Windows changes, will handle in separate PR
* fix: error when --package is passed without an argument
* revert: restore Windows scripts to current main
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* fix: always explicitly set STUDIO_LOCAL_INSTALL and STUDIO_PACKAGE_NAME env vars
* fix: pass explicit STUDIO_LOCAL_REPO env var for --local installs
* fix: align banner box for Setup vs Update labels
* deprecate: hide 'unsloth studio setup' command, point users to update/install.sh
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* fix: check stdout not stdin for auto-launch detection (curl pipe fix)
* fix: update install URL to unsloth.ai/install.sh
* fix: update install.sh usage comments to unsloth.ai/install.sh
* fix: use --upgrade-package for base deps to preserve existing torch/CUDA installs
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* fix: --local install now also installs unsloth-zoo via base.txt before editable overlay
* fix: don't skip base packages for --local installs (editable needs unsloth-zoo)
* refactor: move --local full dep install to install.sh, keep SKIP_STUDIO_BASE for all paths
* feat: add migration support for old .venv and CWD-based installs in setup.sh
* Revert "feat: add migration support for old .venv and CWD-based installs in setup.sh"
This reverts commit 301291d002.
* feat: migrate old .venv layout in install.sh instead of always nuking
* feat: validate old .venv with torch CUDA test before migration, recovery message on launch failure
* fix: try CUDA then fall back to CPU for migration validation
* fix: upgrade unsloth/unsloth-zoo with --reinstall-package on migration to preserve torch
* remove: delete unused unsloth ui command (use unsloth studio instead)
* Fix Windows venv path mismatch between install.ps1, setup.ps1, and studio.py
install.ps1 was creating the venv CWD-relative ($VenvName = "unsloth_studio"),
setup.ps1 was using an absolute path to ".unsloth\studio\.venv", and studio.py
looks for ".unsloth\studio\unsloth_studio". All three paths were different, so
the Windows installer would never produce a working Studio setup.
install.ps1:
- Use absolute $StudioHome + $VenvDir matching the Linux install.sh layout
- Add 3-way migration: old .venv at STUDIO_HOME, CWD-relative ~/unsloth_studio
from the previous install.ps1, or fresh creation with torch validation
- For migrated envs, upgrade unsloth while preserving existing torch/CUDA wheels
- Set SKIP_STUDIO_BASE=1 before calling setup.ps1 (matches install.sh behavior)
- Fix launch instructions to use the absolute venv path
setup.ps1:
- Change $VenvDir from ".unsloth\studio\.venv" to ".unsloth\studio\unsloth_studio"
- Add SKIP_STUDIO_BASE guard: error out if venv is missing when called from
install.ps1 (which should have already created it)
- Differentiate "Setup" vs "Update" in banners based on SKIP_STUDIO_BASE
* setup.ps1: unconditionally error if venv missing, matching setup.sh
setup.sh always errors out if the venv does not exist (line 224-228),
telling the user to run install.sh first. setup.ps1 was conditionally
creating a bare venv with python -m venv when SKIP_STUDIO_BASE was not
set, which would produce an empty venv with no torch or unsloth. Now
setup.ps1 matches setup.sh: always error, always point to install.ps1.
* Fix --torch-backend=auto CPU solver dead-end on Linux, macOS, and Windows
On CPU-only machines, `uv pip install unsloth --torch-backend=auto`
falls back to unsloth==2024.8 because the CPU solver cannot satisfy
newer unsloth's dependencies. install.ps1 already solved this with a
two-step approach; this applies the same fix to install.sh and
install_python_stack.py.
install.sh: add get_torch_index_url() that detects GPU via nvidia-smi
and maps CUDA versions to PyTorch index URLs (matching install.ps1's
Get-TorchIndexUrl). Fresh installs now install torch first via explicit
--index-url, then install unsloth with --upgrade-package to preserve
the pre-installed torch. All 5 --torch-backend=auto removed from
primary paths.
install.ps1: add fallback else-branch when TorchIndexUrl is empty,
using --torch-backend=auto as last resort (matching install.sh).
install_python_stack.py: remove unconditional --torch-backend=auto
from _build_uv_cmd. Torch is pre-installed by install.sh/setup.ps1
by the time this runs. Callers that need it can set UV_TORCH_BACKEND.
Both install.sh and install.ps1 now share the same three-branch logic:
migrated env (upgrade-package only), normal (torch-first + index-url),
and fallback (--torch-backend=auto if URL detection fails).
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Use --reinstall-package for migrated envs on both Linux and Windows
For migrated environments (moved from legacy venv location),
--reinstall-package is better than --upgrade-package because it forces
a clean reinstall even if the same version is already installed. This
ensures proper .dist-info and .pyc state in the new venv location.
--upgrade-package remains correct for the fresh install path where
torch is already installed and we just want to add unsloth without
re-resolving torch.
* Address review findings: portability, parity, and stale comments
- Replace grep -oP (GNU Perl regex) with POSIX sed in
get_torch_index_url() so the script works on BSD grep (macOS is
already guarded by the Darwin early-return, but Alpine/BusyBox
would silently get the wrong CUDA tag)
- Add LC_ALL=C before nvidia-smi invocation to prevent locale-dependent
output parsing issues
- Add warning on stderr when nvidia-smi output is unparseable, matching
install.ps1's [WARN] message
- Add explicit unsloth-zoo positional arg to install.ps1 migrated path,
matching install.sh (--reinstall-package alone won't install it if it
was never present in the migrated env)
- Fix stale comment in install_python_stack.py line 392 that still
claimed --torch-backend=auto is added by _build_uv_cmd
- Add sed to test tools directory (function now uses sed instead of grep)
* Add --index-url to migrated env path to prevent CPU torch resolution
The migrated path runs uv pip install with --reinstall-package for
unsloth/unsloth-zoo. While uv should keep existing torch as satisfied,
the resolver could still re-resolve torch as a transitive dependency.
Without --index-url pointing at the correct CUDA wheel index, the
resolver would fall back to plain PyPI and potentially pull CPU-only
torch. Adding --index-url $TORCH_INDEX_URL ensures CUDA wheels are
available if the resolver needs them.
Applied to both install.sh and install.ps1.
* Revert --index-url on migrated env path
The original install.ps1 on main already handles the migrated path
without --index-url and it works correctly. --reinstall-package only
forces reinstall of the named packages while uv keeps existing torch
as satisfied. No need for the extra flag.
* Fix unsloth studio update --local not installing local checkout
studio.py sets STUDIO_LOCAL_REPO when --local is passed, but
install_python_stack.py never read it. The update path always
installed from PyPI regardless of the --local flag.
Add a local_repo branch that first updates deps from base.txt
(with --upgrade-package to preserve torch), then overlays the
local checkout as an editable install with --no-deps.
---------
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Daniel Han <danielhanchen@gmail.com>
137 lines
4.9 KiB
Python
137 lines
4.9 KiB
Python
"""Cross-platform parity tests between install.sh and install.ps1."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
INSTALL_SH = REPO_ROOT / "install.sh"
|
|
INSTALL_PS1 = REPO_ROOT / "install.ps1"
|
|
|
|
|
|
class TestNoTorchBackendAutoInInstallSh:
|
|
"""install.sh primary install paths must not use --torch-backend=auto.
|
|
|
|
The fallback else-branch (when TORCH_INDEX_URL is empty) is allowed to
|
|
use --torch-backend=auto since that is the last-resort recovery path.
|
|
"""
|
|
|
|
def test_no_torch_backend_auto_outside_fallback(self):
|
|
lines = INSTALL_SH.read_text().splitlines()
|
|
# Find the fallback block: starts with the "else" after the
|
|
# TORCH_INDEX_URL check and ends at the next "fi".
|
|
fallback_start = None
|
|
fallback_end = None
|
|
for i, line in enumerate(lines):
|
|
if fallback_start is None and "GPU detection failed" in line:
|
|
fallback_start = i
|
|
elif (
|
|
fallback_start is not None
|
|
and fallback_end is None
|
|
and line.strip() == "fi"
|
|
):
|
|
fallback_end = i
|
|
break
|
|
fallback_range = (
|
|
range(fallback_start or 0, (fallback_end or 0) + 1)
|
|
if fallback_start
|
|
else range(0)
|
|
)
|
|
|
|
matches = [
|
|
(i + 1, line)
|
|
for i, line in enumerate(lines)
|
|
if "--torch-backend=auto" in line
|
|
and not line.lstrip().startswith("#")
|
|
and i not in fallback_range
|
|
]
|
|
assert matches == [], (
|
|
f"install.sh contains --torch-backend=auto outside the fallback block at lines: "
|
|
f"{[m[0] for m in matches]}"
|
|
)
|
|
|
|
def test_fallback_uses_torch_backend_auto(self):
|
|
"""The fallback branch should use --torch-backend=auto as recovery."""
|
|
text = INSTALL_SH.read_text()
|
|
assert (
|
|
"GPU detection failed" in text
|
|
), "install.sh should have a fallback branch for when GPU detection fails"
|
|
|
|
|
|
class TestInstallShHasGpuDetection:
|
|
"""install.sh must contain the get_torch_index_url function."""
|
|
|
|
def test_function_exists(self):
|
|
text = INSTALL_SH.read_text()
|
|
assert (
|
|
"get_torch_index_url()" in text
|
|
), "install.sh is missing the get_torch_index_url() function"
|
|
|
|
def test_torch_index_url_assigned(self):
|
|
text = INSTALL_SH.read_text()
|
|
assert (
|
|
"TORCH_INDEX_URL=$(get_torch_index_url)" in text
|
|
), "install.sh should assign TORCH_INDEX_URL from get_torch_index_url()"
|
|
|
|
|
|
class TestCudaMappingParity:
|
|
"""CUDA version thresholds must match between install.sh and install.ps1."""
|
|
|
|
@staticmethod
|
|
def _extract_cuda_thresholds_sh(text: str) -> list[str]:
|
|
"""Extract cu* suffixes from the major/minor comparison chain in install.sh."""
|
|
# Only match lines in the if/elif chain that compare _major/_minor
|
|
in_func = False
|
|
results = []
|
|
for line in text.splitlines():
|
|
if "get_torch_index_url()" in line:
|
|
in_func = True
|
|
continue
|
|
if in_func and line.startswith("}"):
|
|
break
|
|
if in_func and ("_major" in line or "_minor" in line):
|
|
m = re.search(r"/(cu\d+|cpu)", line)
|
|
if m:
|
|
results.append(m.group(1))
|
|
return results
|
|
|
|
@staticmethod
|
|
def _extract_cuda_thresholds_ps1(text: str) -> list[str]:
|
|
"""Extract cu* suffixes from the major/minor comparison chain in install.ps1."""
|
|
in_func = False
|
|
depth = 0
|
|
results = []
|
|
for line in text.splitlines():
|
|
if "function Get-TorchIndexUrl" in line:
|
|
in_func = True
|
|
depth = 1
|
|
continue
|
|
if in_func:
|
|
depth += line.count("{") - line.count("}")
|
|
if depth <= 0:
|
|
break
|
|
# Only match the if-chain lines that compare $major/$minor
|
|
if "$major" in line or "$minor" in line:
|
|
m = re.search(r"/(cu\d+|cpu)", line)
|
|
if m:
|
|
results.append(m.group(1))
|
|
return results
|
|
|
|
def test_same_cuda_suffixes(self):
|
|
"""Both scripts should produce the same ordered list of CUDA index suffixes."""
|
|
sh_text = INSTALL_SH.read_text()
|
|
ps1_text = INSTALL_PS1.read_text()
|
|
|
|
sh_thresholds = self._extract_cuda_thresholds_sh(sh_text)
|
|
ps1_thresholds = self._extract_cuda_thresholds_ps1(ps1_text)
|
|
|
|
assert len(sh_thresholds) > 0, "Could not extract thresholds from install.sh"
|
|
assert len(ps1_thresholds) > 0, "Could not extract thresholds from install.ps1"
|
|
assert sh_thresholds == ps1_thresholds, (
|
|
f"CUDA mapping mismatch:\n"
|
|
f" install.sh: {sh_thresholds}\n"
|
|
f" install.ps1: {ps1_thresholds}"
|
|
)
|