Compare commits
27 commits
main
...
feature/do
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c116af0a0 | ||
|
|
0627af1a3c | ||
|
|
3357d746f3 | ||
|
|
5d3fea9cdf | ||
|
|
904718abb2 | ||
|
|
cba8f4a479 | ||
|
|
a32e75d597 | ||
|
|
a804a72043 | ||
|
|
7e5bc2b586 | ||
|
|
721bdadafa | ||
|
|
41fce49d6f | ||
|
|
532d360094 | ||
|
|
8b322ec3b7 | ||
|
|
2dde3270d3 | ||
|
|
463623b8b6 | ||
|
|
ca30bd0dc4 | ||
|
|
afc6880028 | ||
|
|
20a67048ca | ||
|
|
67612861d6 | ||
|
|
170f7dc0de | ||
|
|
830c7c4f07 | ||
|
|
778a802703 | ||
|
|
fb66ed5385 | ||
|
|
95f8e77b6c | ||
|
|
2b44b85e19 | ||
|
|
ec4f2a15a0 | ||
|
|
aa17486f44 |
8 changed files with 485 additions and 436 deletions
1
.worktreeinclude
Normal file
1
.worktreeinclude
Normal file
|
|
@ -0,0 +1 @@
|
|||
CLAUDE.md
|
||||
|
|
@ -126,6 +126,97 @@ def _hipcc_gcc_install_dir() -> str | None:
|
|||
return None
|
||||
|
||||
|
||||
def _causal_conv1d_platform_tag() -> str | None:
|
||||
machine = platform.machine().lower()
|
||||
if sys.platform.startswith("linux"):
|
||||
if machine in {"x86_64", "amd64"}:
|
||||
return "linux_x86_64"
|
||||
if machine in {"aarch64", "arm64"}:
|
||||
return "linux_aarch64"
|
||||
return None
|
||||
# No prebuilt wheels published for macOS or Windows
|
||||
return None
|
||||
|
||||
|
||||
def _probe_causal_conv1d_env() -> dict[str, str] | None:
|
||||
try:
|
||||
probe = _sp.run(
|
||||
[
|
||||
sys.executable,
|
||||
"-c",
|
||||
(
|
||||
"import json, sys, re, torch; "
|
||||
"parts = torch.__version__.split('+', 1)[0].split('.')[:2]; "
|
||||
"minor = re.sub(r'[^0-9].*', '', parts[1]) if len(parts) > 1 else '0'; "
|
||||
"torch_mm = parts[0] + '.' + minor; "
|
||||
"print(json.dumps({"
|
||||
"'python_tag': f'cp{sys.version_info.major}{sys.version_info.minor}', "
|
||||
"'torch_mm': torch_mm, "
|
||||
"'cuda_major': str(int(str(torch.version.cuda).split('.', 1)[0])) if torch.version.cuda else '', "
|
||||
"'cxx11abi': str(torch._C._GLIBCXX_USE_CXX11_ABI).upper()"
|
||||
"}))"
|
||||
),
|
||||
],
|
||||
stdout = _sp.PIPE,
|
||||
stderr = _sp.PIPE,
|
||||
text = True,
|
||||
timeout = 30,
|
||||
)
|
||||
except _sp.TimeoutExpired:
|
||||
logger.warning("Torch environment probe timed out after 30s")
|
||||
return None
|
||||
if probe.returncode != 0:
|
||||
logger.warning(
|
||||
"Failed to probe torch environment for causal-conv1d wheel:\n%s",
|
||||
probe.stdout,
|
||||
)
|
||||
return None
|
||||
|
||||
try:
|
||||
return json.loads(probe.stdout.strip())
|
||||
except json.JSONDecodeError:
|
||||
logger.warning(
|
||||
"Failed to parse torch environment probe output: %s", probe.stdout
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def _direct_wheel_url(
|
||||
*,
|
||||
filename_prefix: str,
|
||||
package_version: str,
|
||||
release_tag: str,
|
||||
release_base_url: str,
|
||||
env: dict[str, str] | None = None,
|
||||
) -> str | None:
|
||||
env = env or _probe_causal_conv1d_env()
|
||||
platform_tag = _causal_conv1d_platform_tag()
|
||||
if env is None or platform_tag is None or not env.get("cuda_major"):
|
||||
return None
|
||||
|
||||
filename = (
|
||||
f"{filename_prefix}-{package_version}"
|
||||
f"+cu{env['cuda_major']}torch{env['torch_mm']}"
|
||||
f"cxx11abi{env['cxx11abi']}-{env['python_tag']}-{env['python_tag']}-{platform_tag}.whl"
|
||||
)
|
||||
return f"{release_base_url}/{release_tag}/{filename}"
|
||||
|
||||
|
||||
def _url_exists(url: str) -> bool:
|
||||
try:
|
||||
request = urllib.request.Request(url, method = "HEAD")
|
||||
with urllib.request.urlopen(request, timeout = 10):
|
||||
return True
|
||||
except urllib.error.HTTPError as exc:
|
||||
if exc.code == 404:
|
||||
return False
|
||||
logger.warning("Unexpected HTTP error while probing %s: %s", url, exc)
|
||||
return False
|
||||
except Exception as exc:
|
||||
logger.warning("Failed to probe %s: %s", url, exc)
|
||||
return False
|
||||
|
||||
|
||||
def _install_package_wheel_first(
|
||||
*,
|
||||
event_queue: Any,
|
||||
|
|
|
|||
|
|
@ -235,10 +235,11 @@ def _setup_cache_env() -> None:
|
|||
os.environ.get("XDG_CACHE_HOME", Path.home() / ".cache")
|
||||
).expanduser()
|
||||
hf_default = xdg_cache / "huggingface"
|
||||
hf_home = Path(os.environ.get("HF_HOME", str(hf_default)))
|
||||
defaults: dict[str, str] = {
|
||||
"HF_HOME": str(hf_default),
|
||||
"HF_HUB_CACHE": str(hf_default / "hub"),
|
||||
"HF_XET_CACHE": str(hf_default / "xet"),
|
||||
"HF_HUB_CACHE": str(hf_home / "hub"),
|
||||
"HF_XET_CACHE": str(hf_home / "xet"),
|
||||
"UV_CACHE_DIR": str(root / "uv"),
|
||||
"VLLM_CACHE_ROOT": str(root / "vllm"),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -917,9 +917,9 @@ def install_python_stack() -> int:
|
|||
base_total += 3
|
||||
_TOTAL = (base_total - 1) if skip_base else base_total
|
||||
|
||||
# 1. Try to use uv for faster installs (must happen before pip upgrade
|
||||
# because uv venvs don't include pip by default)
|
||||
USE_UV = _bootstrap_uv()
|
||||
# # 1. Try to use uv for faster installs (must happen before pip upgrade
|
||||
# # because uv venvs don't include pip by default)
|
||||
# USE_UV = _bootstrap_uv()
|
||||
|
||||
# 2. Ensure pip is available (uv venvs created by install.sh don't include pip)
|
||||
_progress("pip bootstrap")
|
||||
|
|
@ -948,17 +948,115 @@ def install_python_stack() -> int:
|
|||
).returncode
|
||||
== 0
|
||||
)
|
||||
# # 2. Ensure pip is available (uv venvs created by install.sh don't include pip)
|
||||
# _progress("pip bootstrap")
|
||||
# if USE_UV:
|
||||
# run(
|
||||
# "Bootstrapping pip via uv",
|
||||
# [
|
||||
# "uv",
|
||||
# "pip",
|
||||
# "install",
|
||||
# "--python",
|
||||
# sys.executable,
|
||||
# "pip",
|
||||
# ],
|
||||
# )
|
||||
# else:
|
||||
# # pip may not exist yet (uv-created venvs omit it). Try ensurepip
|
||||
# # first, then upgrade. Only fall back to a direct upgrade when pip
|
||||
# # is already present.
|
||||
# _has_pip = (
|
||||
# subprocess.run(
|
||||
# [sys.executable, "-m", "pip", "--version"],
|
||||
# stdout = subprocess.DEVNULL,
|
||||
# stderr = subprocess.DEVNULL,
|
||||
# ).returncode
|
||||
# == 0
|
||||
# )
|
||||
#
|
||||
# if not _has_pip:
|
||||
# run(
|
||||
# "Bootstrapping pip via ensurepip",
|
||||
# [sys.executable, "-m", "ensurepip", "--upgrade"],
|
||||
# )
|
||||
# else:
|
||||
# run(
|
||||
# "Upgrading pip",
|
||||
# [sys.executable, "-m", "pip", "install", "--upgrade", "pip"],
|
||||
# )
|
||||
|
||||
if not _has_pip:
|
||||
run(
|
||||
"Bootstrapping pip via ensurepip",
|
||||
[sys.executable, "-m", "ensurepip", "--upgrade"],
|
||||
)
|
||||
else:
|
||||
run(
|
||||
"Upgrading pip",
|
||||
[sys.executable, "-m", "pip", "install", "--upgrade", "pip"],
|
||||
)
|
||||
# # 3. Core packages: unsloth-zoo + unsloth (or custom package name)
|
||||
# if skip_base:
|
||||
# print(_green(f"✅ {package_name} already installed — skipping base packages"))
|
||||
# elif NO_TORCH:
|
||||
# # No-torch update path: install unsloth + unsloth-zoo with --no-deps
|
||||
# # (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(
|
||||
# f"Updating {package_name} + unsloth-zoo (no-torch mode)",
|
||||
# "--no-cache-dir",
|
||||
# "--no-deps",
|
||||
# "--upgrade-package",
|
||||
# package_name,
|
||||
# "--upgrade-package",
|
||||
# "unsloth-zoo",
|
||||
# package_name,
|
||||
# "unsloth-zoo",
|
||||
# )
|
||||
# pip_install(
|
||||
# "Installing no-torch runtime deps",
|
||||
# "--no-cache-dir",
|
||||
# "--no-deps",
|
||||
# req = REQ_ROOT / "no-torch-runtime.txt",
|
||||
# )
|
||||
# if local_repo:
|
||||
# pip_install(
|
||||
# "Overlaying local repo (editable)",
|
||||
# "--no-cache-dir",
|
||||
# "--no-deps",
|
||||
# "-e",
|
||||
# local_repo,
|
||||
# constrain = False,
|
||||
# )
|
||||
# elif local_repo:
|
||||
# _progress("base packages")
|
||||
# pip_install(
|
||||
# "Updating base packages",
|
||||
# "--no-cache-dir",
|
||||
# "--upgrade-package",
|
||||
# "unsloth",
|
||||
# "--upgrade-package",
|
||||
# "unsloth-zoo",
|
||||
# req = REQ_ROOT / "base.txt",
|
||||
# )
|
||||
# pip_install(
|
||||
# "Overlaying local repo (editable)",
|
||||
# "--no-cache-dir",
|
||||
# "--no-deps",
|
||||
# "-e",
|
||||
# local_repo,
|
||||
# constrain = False,
|
||||
# )
|
||||
# elif package_name != "unsloth":
|
||||
# _progress("base packages")
|
||||
# pip_install(
|
||||
# f"Installing {package_name}",
|
||||
# "--no-cache-dir",
|
||||
# package_name,
|
||||
# )
|
||||
# else:
|
||||
# _progress("base packages")
|
||||
# pip_install(
|
||||
# "Updating base packages",
|
||||
# "--no-cache-dir",
|
||||
# "--upgrade-package",
|
||||
# "unsloth",
|
||||
# "--upgrade-package",
|
||||
# "unsloth-zoo",
|
||||
# req = REQ_ROOT / "base.txt",
|
||||
# )
|
||||
|
||||
# 3. Core packages: unsloth-zoo + unsloth (or custom package name)
|
||||
if skip_base:
|
||||
|
|
@ -1008,10 +1106,17 @@ def install_python_stack() -> int:
|
|||
# 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).
|
||||
# --no-deps on the base.txt install too: this branch is for Docker
|
||||
# builds where torch + every CUDA-adjacent package is already pinned
|
||||
# and installed; resolving base.txt's deps risks pulling PyPI's
|
||||
# CPU-only torch over our local-version cu* build (the +cu130 etc.
|
||||
# local segment is non-canonical PEP 440 and uv may treat it as
|
||||
# not-satisfying an unconstrained `torch` requirement).
|
||||
_progress("base packages")
|
||||
pip_install(
|
||||
"Updating base packages",
|
||||
"--no-cache-dir",
|
||||
"--no-deps",
|
||||
"--upgrade-package",
|
||||
"unsloth",
|
||||
"--upgrade-package",
|
||||
|
|
@ -1048,110 +1153,60 @@ def install_python_stack() -> int:
|
|||
# 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.
|
||||
# --no-deps: this branch is for Docker builds where torch + every
|
||||
# CUDA-adjacent package is already pinned and installed by the
|
||||
# Dockerfile; resolving base.txt's transitive deps risks pulling
|
||||
# PyPI's CPU-only torch over our local-version cu* build (the
|
||||
# +cu130 etc. local segment is non-canonical PEP 440 and uv may
|
||||
# treat it as not-satisfying an unconstrained `torch` requirement).
|
||||
_progress("base packages")
|
||||
pip_install(
|
||||
"Updating base packages",
|
||||
"--no-cache-dir",
|
||||
"--no-deps",
|
||||
"--upgrade-package",
|
||||
"unsloth",
|
||||
"--upgrade-package",
|
||||
"unsloth-zoo",
|
||||
req = REQ_ROOT / "base.txt",
|
||||
)
|
||||
# pip_install(
|
||||
# "Installing additional unsloth dependencies",
|
||||
# "--no-cache-dir",
|
||||
# req = REQ_ROOT / "extras.txt",
|
||||
# )
|
||||
|
||||
# 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.
|
||||
if not IS_WINDOWS and not IS_MACOS and not NO_TORCH:
|
||||
_progress("ROCm torch check")
|
||||
_ensure_rocm_torch()
|
||||
# pip_install(
|
||||
# "Installing extras (no-deps)",
|
||||
# "--no-deps",
|
||||
# "--no-cache-dir",
|
||||
# req = REQ_ROOT / "extras-no-deps.txt",
|
||||
# )
|
||||
|
||||
# Windows + AMD GPU: PyTorch does not publish ROCm wheels for Windows.
|
||||
# Detect and warn so users know manual steps are needed for GPU training.
|
||||
if IS_WINDOWS and not NO_TORCH and not _has_usable_nvidia_gpu():
|
||||
# Validate actual AMD GPU presence (not just tool existence)
|
||||
import re as _re_win
|
||||
# # 4. Overrides (torchao, transformers) -- force-reinstall
|
||||
# _progress("dependency overrides")
|
||||
# pip_install(
|
||||
# "Installing dependency overrides",
|
||||
# "--force-reinstall",
|
||||
# "--no-cache-dir",
|
||||
# req = REQ_ROOT / "overrides.txt",
|
||||
# )
|
||||
|
||||
def _win_amd_smi_has_gpu(stdout: str) -> bool:
|
||||
return bool(_re_win.search(r"(?im)^gpu\s*[:\[]\s*\d", stdout))
|
||||
# # 5. Triton kernels (no-deps, from source)
|
||||
# # Skip on Windows (no support) and macOS (no support).
|
||||
# if not IS_WINDOWS and not IS_MACOS:
|
||||
# _progress("triton kernels")
|
||||
# pip_install(
|
||||
# "Installing triton kernels",
|
||||
# "--no-deps",
|
||||
# "--no-cache-dir",
|
||||
# req = REQ_ROOT / "triton-kernels.txt",
|
||||
# constrain = False,
|
||||
# )
|
||||
|
||||
_win_amd_gpu = False
|
||||
for _wcmd, _check_fn in (
|
||||
(["hipinfo"], lambda out: "gcnarchname" in out.lower()),
|
||||
(["amd-smi", "list"], _win_amd_smi_has_gpu),
|
||||
):
|
||||
_wexe = shutil.which(_wcmd[0])
|
||||
if not _wexe:
|
||||
continue
|
||||
try:
|
||||
_wr = subprocess.run(
|
||||
[_wexe, *_wcmd[1:]],
|
||||
stdout = subprocess.PIPE,
|
||||
stderr = subprocess.DEVNULL,
|
||||
text = True,
|
||||
timeout = 10,
|
||||
)
|
||||
except Exception:
|
||||
continue
|
||||
if _wr.returncode == 0 and _check_fn(_wr.stdout):
|
||||
_win_amd_gpu = True
|
||||
break
|
||||
if _win_amd_gpu:
|
||||
_safe_print(
|
||||
_dim(" Note:"),
|
||||
"AMD GPU detected on Windows. ROCm-enabled PyTorch must be",
|
||||
)
|
||||
_safe_print(
|
||||
" " * 8,
|
||||
"installed manually. See: https://docs.unsloth.ai/get-started/install-and-update/amd",
|
||||
)
|
||||
|
||||
# 3. Extra dependencies
|
||||
_progress("unsloth extras")
|
||||
pip_install(
|
||||
"Installing additional unsloth dependencies",
|
||||
"--no-cache-dir",
|
||||
req = REQ_ROOT / "extras.txt",
|
||||
)
|
||||
|
||||
# 3b. Extra dependencies (no-deps) -- audio model support etc.
|
||||
_progress("extra codecs")
|
||||
pip_install(
|
||||
"Installing extras (no-deps)",
|
||||
"--no-deps",
|
||||
"--no-cache-dir",
|
||||
req = REQ_ROOT / "extras-no-deps.txt",
|
||||
)
|
||||
|
||||
# 4. Overrides (torchao, transformers) -- force-reinstall
|
||||
# Skip entirely when torch is unavailable (e.g. Intel Mac GGUF-only mode)
|
||||
# because overrides.txt contains torchao which requires torch.
|
||||
if NO_TORCH:
|
||||
_progress("dependency overrides (skipped, no torch)")
|
||||
else:
|
||||
_progress("dependency overrides")
|
||||
pip_install(
|
||||
"Installing dependency overrides",
|
||||
"--force-reinstall",
|
||||
"--no-cache-dir",
|
||||
req = REQ_ROOT / "overrides.txt",
|
||||
)
|
||||
|
||||
# 5. Triton kernels (no-deps, from source)
|
||||
# Skip on Windows (no support) and macOS (no support).
|
||||
if not IS_WINDOWS and not IS_MACOS:
|
||||
_progress("triton kernels")
|
||||
pip_install(
|
||||
"Installing triton kernels",
|
||||
"--no-deps",
|
||||
"--no-cache-dir",
|
||||
req = REQ_ROOT / "triton-kernels.txt",
|
||||
constrain = False,
|
||||
)
|
||||
|
||||
if not IS_WINDOWS and not IS_MACOS and not NO_TORCH:
|
||||
_progress("flash-attn")
|
||||
_ensure_flash_attn()
|
||||
#if not IS_WINDOWS and not IS_MACOS and not NO_TORCH:
|
||||
# _progress("flash-attn")
|
||||
# _ensure_flash_attn()
|
||||
|
||||
# # 6. Patch: override llama_cpp.py with fix from unsloth-zoo feature/llama-cpp-windows-support branch
|
||||
# patch_package_file(
|
||||
|
|
|
|||
|
|
@ -1891,6 +1891,89 @@ if ($stackExit -ne 0) {
|
|||
exit 1
|
||||
}
|
||||
|
||||
# ── Pre-install transformers 5.x into .venv_t5_530/ and .venv_t5_550/ ──
|
||||
# Models like GLM-4.7-Flash, Qwen3 MoE need transformers>=5.3.0.
|
||||
# Gemma 4 models need transformers>=5.5.0.
|
||||
# Pre-install into separate directories to avoid runtime pip overhead.
|
||||
# The training subprocess prepends the appropriate dir to sys.path.
|
||||
Write-Host ""
|
||||
|
||||
# Clean up legacy single .venv_t5 directory
|
||||
$VenvT5Legacy = Join-Path $env:USERPROFILE ".unsloth\studio\.venv_t5"
|
||||
if (Test-Path $VenvT5Legacy) { Remove-Item -Recurse -Force $VenvT5Legacy }
|
||||
|
||||
$prevEAP_t5 = $ErrorActionPreference
|
||||
$ErrorActionPreference = "Continue"
|
||||
|
||||
# --- .venv_t5_530 (transformers 5.3.0) ---
|
||||
substep "pre-installing transformers 5.3.0 for newer model support..."
|
||||
$VenvT5_530Dir = Join-Path $env:USERPROFILE ".unsloth\studio\.venv_t5_530"
|
||||
if (Test-Path $VenvT5_530Dir) { Remove-Item -Recurse -Force $VenvT5_530Dir }
|
||||
New-Item -ItemType Directory -Path $VenvT5_530Dir -Force | Out-Null
|
||||
foreach ($pkg in @("transformers==5.3.0", "huggingface_hub==1.8.0", "hf_xet==1.4.2")) {
|
||||
if ($script:UnslothVerbose) {
|
||||
Fast-Install --target $VenvT5_530Dir --no-deps $pkg
|
||||
$t5PkgExit = $LASTEXITCODE
|
||||
$output = ""
|
||||
} else {
|
||||
$output = Fast-Install --target $VenvT5_530Dir --no-deps $pkg | Out-String
|
||||
$t5PkgExit = $LASTEXITCODE
|
||||
}
|
||||
if ($t5PkgExit -ne 0) {
|
||||
Write-Host "[FAIL] Could not install $pkg into .venv_t5_530/" -ForegroundColor Red
|
||||
Write-Host $output -ForegroundColor Red
|
||||
$ErrorActionPreference = $prevEAP_t5
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
if ($script:UnslothVerbose) {
|
||||
Fast-Install --target $VenvT5_530Dir tiktoken
|
||||
$tiktokenInstallExit = $LASTEXITCODE
|
||||
$output = ""
|
||||
} else {
|
||||
$output = Fast-Install --target $VenvT5_530Dir tiktoken | Out-String
|
||||
$tiktokenInstallExit = $LASTEXITCODE
|
||||
}
|
||||
if ($tiktokenInstallExit -ne 0) {
|
||||
substep "Could not install tiktoken into .venv_t5_530/ -- Qwen tokenizers may fail" "Yellow"
|
||||
}
|
||||
step "transformers" "5.3.0 pre-installed"
|
||||
|
||||
# --- .venv_t5_550 (transformers 5.5.0) ---
|
||||
substep "pre-installing transformers 5.5.0 for Gemma 4 support..."
|
||||
$VenvT5_550Dir = Join-Path $env:USERPROFILE ".unsloth\studio\.venv_t5_550"
|
||||
if (Test-Path $VenvT5_550Dir) { Remove-Item -Recurse -Force $VenvT5_550Dir }
|
||||
New-Item -ItemType Directory -Path $VenvT5_550Dir -Force | Out-Null
|
||||
foreach ($pkg in @("transformers==5.5.0", "huggingface_hub==1.8.0", "hf_xet==1.4.2")) {
|
||||
if ($script:UnslothVerbose) {
|
||||
Fast-Install --target $VenvT5_550Dir --no-deps $pkg
|
||||
$t5PkgExit = $LASTEXITCODE
|
||||
$output = ""
|
||||
} else {
|
||||
$output = Fast-Install --target $VenvT5_550Dir --no-deps $pkg | Out-String
|
||||
$t5PkgExit = $LASTEXITCODE
|
||||
}
|
||||
if ($t5PkgExit -ne 0) {
|
||||
Write-Host "[FAIL] Could not install $pkg into .venv_t5_550/" -ForegroundColor Red
|
||||
Write-Host $output -ForegroundColor Red
|
||||
$ErrorActionPreference = $prevEAP_t5
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
if ($script:UnslothVerbose) {
|
||||
Fast-Install --target $VenvT5_550Dir tiktoken
|
||||
$tiktokenInstallExit = $LASTEXITCODE
|
||||
$output = ""
|
||||
} else {
|
||||
$output = Fast-Install --target $VenvT5_550Dir tiktoken | Out-String
|
||||
$tiktokenInstallExit = $LASTEXITCODE
|
||||
}
|
||||
if ($tiktokenInstallExit -ne 0) {
|
||||
substep "Could not install tiktoken into .venv_t5_550/ -- Qwen tokenizers may fail" "Yellow"
|
||||
}
|
||||
$ErrorActionPreference = $prevEAP_t5
|
||||
step "transformers" "5.5.0 pre-installed"
|
||||
|
||||
} else {
|
||||
step "python" "dependencies up to date"
|
||||
# Restore ErrorActionPreference (was lowered for pip/python section)
|
||||
|
|
|
|||
121
studio/setup.sh
121
studio/setup.sh
|
|
@ -417,36 +417,7 @@ if [ -d "$SCRIPT_DIR/backend/core/data_recipe/oxc-validator" ] && command -v npm
|
|||
fi
|
||||
|
||||
# ── Python venv + deps ──
|
||||
# UNSLOTH_STUDIO_HOME (or STUDIO_HOME alias) overrides the install root
|
||||
# (mirrors install.sh). UNSLOTH_STUDIO_HOME wins when both are set.
|
||||
_studio_override_var=""
|
||||
_studio_override="${UNSLOTH_STUDIO_HOME:-}"
|
||||
if [ -n "$_studio_override" ]; then
|
||||
_studio_override_var="UNSLOTH_STUDIO_HOME"
|
||||
else
|
||||
_studio_override="${STUDIO_HOME:-}"
|
||||
[ -n "$_studio_override" ] && _studio_override_var="STUDIO_HOME"
|
||||
fi
|
||||
# Strip whitespace so " " is treated as unset (matches Python .strip()).
|
||||
_studio_override=$(printf '%s' "$_studio_override" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
|
||||
case "$_studio_override" in
|
||||
"~") _studio_override="$HOME" ;;
|
||||
"~/"*) _studio_override="$HOME/${_studio_override#'~/'}" ;;
|
||||
esac
|
||||
if [ -n "$_studio_override" ]; then
|
||||
# setup.sh runs against an existing install (via 'unsloth studio update');
|
||||
# a typo in the override must fail fast instead of materializing an
|
||||
# empty workspace dir. Mirrors setup.ps1 behavior.
|
||||
if [ ! -d "$_studio_override" ]; then
|
||||
echo "ERROR: $_studio_override_var=$_studio_override does not exist." >&2
|
||||
echo " Run install.sh to create the install root before 'unsloth studio update'." >&2
|
||||
exit 1
|
||||
fi
|
||||
[ -w "$_studio_override" ] || { echo "ERROR: $_studio_override_var=$_studio_override is not writable." >&2; exit 1; }
|
||||
STUDIO_HOME="$(CDPATH= cd -P -- "$_studio_override" && pwd -P)" || exit 1
|
||||
else
|
||||
STUDIO_HOME="$HOME/.unsloth/studio"
|
||||
fi
|
||||
STUDIO_HOME="${UNSLOTH_STUDIO_HOME:-$HOME/.unsloth/studio}"
|
||||
VENV_DIR="$STUDIO_HOME/unsloth_studio"
|
||||
VENV_T5_530_DIR="$STUDIO_HOME/.venv_t5_530"
|
||||
VENV_T5_550_DIR="$STUDIO_HOME/.venv_t5_550"
|
||||
|
|
@ -459,7 +430,12 @@ VENV_T5_550_DIR="$STUDIO_HOME/.venv_t5_550"
|
|||
# Note: do NOT delete $STUDIO_HOME/.venv here — install.sh handles migration
|
||||
|
||||
_COLAB_NO_VENV=false
|
||||
if [ ! -x "$VENV_DIR/bin/python" ]; then
|
||||
_DOCKER_NO_VENV=false
|
||||
if [ -n "$UNSLOTH_DOCKER" ]; then
|
||||
# Docker: packages already in /opt/conda — skip venv entirely.
|
||||
# Only pre-install .venv_t5 for transformers 5.x switching (handled below).
|
||||
_DOCKER_NO_VENV=true
|
||||
elif [ ! -x "$VENV_DIR/bin/python" ]; then
|
||||
if [ "$IS_COLAB" = true ]; then
|
||||
# On Colab there is no Studio venv -- install backend deps into system Python.
|
||||
# Strip all version constraints so pip keeps Colab's pre-installed
|
||||
|
|
@ -524,6 +500,52 @@ if [ "$_COLAB_NO_VENV" = true ]; then
|
|||
substep "continuing to llama.cpp install for GGUF inference support"
|
||||
fi
|
||||
|
||||
# In Docker, packages are pre-installed in /opt/conda — only install missing
|
||||
# studio/data-designer deps and pre-install .venv_t5 for transformers 5.x.
|
||||
if [ "$_DOCKER_NO_VENV" = true ]; then
|
||||
echo " Docker detected — skipping venv activation."
|
||||
|
||||
# Install branch's unsloth/unsloth_cli/studio into /opt/conda
|
||||
# (overwrites PyPI version with Docker-aware code)
|
||||
echo " Installing local unsloth from branch..."
|
||||
pip install --force-reinstall --no-deps "$REPO_ROOT"
|
||||
|
||||
# Install only missing deps (studio, data-designer, plugin, metadata patch).
|
||||
# Heavy packages (torch, unsloth, vllm, etc.) are already in /opt/conda.
|
||||
# install_python_stack.py has steps 1-5 commented out for this branch.
|
||||
python "$SCRIPT_DIR/install_python_stack.py"
|
||||
|
||||
# Pre-install transformers 5.x into .venv_t5_530/ and .venv_t5_550/
|
||||
echo ""
|
||||
echo " Pre-installing transformers 5.x for newer model support..."
|
||||
|
||||
# Clean up legacy single .venv_t5 directory
|
||||
[ -d "$STUDIO_HOME/.venv_t5" ] && rm -rf "$STUDIO_HOME/.venv_t5"
|
||||
|
||||
[ -d "$VENV_T5_530_DIR" ] && rm -rf "$VENV_T5_530_DIR"
|
||||
mkdir -p "$VENV_T5_530_DIR"
|
||||
pip install --target "$VENV_T5_530_DIR" --no-deps "transformers==5.3.0" 2>/dev/null
|
||||
pip install --target "$VENV_T5_530_DIR" --no-deps "huggingface_hub==1.8.0" 2>/dev/null
|
||||
pip install --target "$VENV_T5_530_DIR" --no-deps "hf_xet==1.4.2" 2>/dev/null
|
||||
pip install --target "$VENV_T5_530_DIR" "tiktoken" 2>/dev/null
|
||||
|
||||
[ -d "$VENV_T5_550_DIR" ] && rm -rf "$VENV_T5_550_DIR"
|
||||
mkdir -p "$VENV_T5_550_DIR"
|
||||
pip install --target "$VENV_T5_550_DIR" --no-deps "transformers==5.5.0" 2>/dev/null
|
||||
pip install --target "$VENV_T5_550_DIR" --no-deps "huggingface_hub==1.8.0" 2>/dev/null
|
||||
pip install --target "$VENV_T5_550_DIR" --no-deps "hf_xet==1.4.2" 2>/dev/null
|
||||
pip install --target "$VENV_T5_550_DIR" "tiktoken" 2>/dev/null
|
||||
|
||||
echo "✅ Transformers 5.3.0 pre-installed to $VENV_T5_530_DIR/"
|
||||
echo "✅ Transformers 5.5.0 pre-installed to $VENV_T5_550_DIR/"
|
||||
|
||||
echo ""
|
||||
echo "╔══════════════════════════════════════╗"
|
||||
echo "║ Docker Studio Setup Complete! ║"
|
||||
echo "╚══════════════════════════════════════╝"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ── Check if Python deps need updating ──
|
||||
# Compare installed package version against PyPI latest.
|
||||
# Skip all Python dependency work if versions match (fast update path).
|
||||
|
|
@ -558,6 +580,31 @@ fi
|
|||
|
||||
if [ "$_SKIP_PYTHON_DEPS" = false ]; then
|
||||
install_python_stack
|
||||
|
||||
# ── 6b. Pre-install transformers 5.x into .venv_t5_530/ and .venv_t5_550/ ──
|
||||
# Models like GLM-4.7-Flash, Qwen3 MoE need transformers>=5.3.0.
|
||||
# Gemma 4 models need transformers>=5.5.0.
|
||||
# Pre-install into separate directories to avoid runtime pip overhead.
|
||||
# The training subprocess prepends the appropriate dir to sys.path.
|
||||
|
||||
# Clean up legacy single .venv_t5 directory
|
||||
[ -d "$STUDIO_HOME/.venv_t5" ] && rm -rf "$STUDIO_HOME/.venv_t5"
|
||||
|
||||
[ -d "$VENV_T5_530_DIR" ] && rm -rf "$VENV_T5_530_DIR"
|
||||
mkdir -p "$VENV_T5_530_DIR"
|
||||
run_quiet "install transformers 5.3.0" fast_install --target "$VENV_T5_530_DIR" --no-deps "transformers==5.3.0"
|
||||
run_quiet "install huggingface_hub for t5_530" fast_install --target "$VENV_T5_530_DIR" --no-deps "huggingface_hub==1.8.0"
|
||||
run_quiet "install hf_xet for t5_530" fast_install --target "$VENV_T5_530_DIR" --no-deps "hf_xet==1.4.2"
|
||||
run_quiet "install tiktoken for t5_530" fast_install --target "$VENV_T5_530_DIR" "tiktoken"
|
||||
step "transformers" "5.3.0 pre-installed"
|
||||
|
||||
[ -d "$VENV_T5_550_DIR" ] && rm -rf "$VENV_T5_550_DIR"
|
||||
mkdir -p "$VENV_T5_550_DIR"
|
||||
run_quiet "install transformers 5.5.0" fast_install --target "$VENV_T5_550_DIR" --no-deps "transformers==5.5.0"
|
||||
run_quiet "install huggingface_hub for t5_550" fast_install --target "$VENV_T5_550_DIR" --no-deps "huggingface_hub==1.8.0"
|
||||
run_quiet "install hf_xet for t5_550" fast_install --target "$VENV_T5_550_DIR" --no-deps "hf_xet==1.4.2"
|
||||
run_quiet "install tiktoken for t5_550" fast_install --target "$VENV_T5_550_DIR" "tiktoken"
|
||||
step "transformers" "5.5.0 pre-installed"
|
||||
else
|
||||
step "python" "dependencies up to date"
|
||||
verbose_substep "python deps check: installed=$_PKG_NAME@${INSTALLED_VER:-unknown} latest=${LATEST_VER:-unknown}"
|
||||
|
|
@ -636,13 +683,10 @@ fi
|
|||
fi
|
||||
|
||||
# ── 7. Prefer prebuilt llama.cpp bundles before any source build path ──
|
||||
# Nest llama.cpp under $STUDIO_HOME only for real env-overrides; legacy
|
||||
# default keeps ~/.unsloth/llama.cpp so pre-PR builds are still discovered.
|
||||
if [ "$_STUDIO_HOME_IS_CUSTOM" = true ]; then
|
||||
UNSLOTH_HOME="$STUDIO_HOME"
|
||||
else
|
||||
UNSLOTH_HOME="$HOME/.unsloth"
|
||||
fi
|
||||
if [ "$_DOCKER_NO_VENV" = true ]; then
|
||||
step "llama.cpp" "skipped (Docker)"
|
||||
else # begin non-Docker llama.cpp block
|
||||
UNSLOTH_HOME="$HOME/.unsloth"
|
||||
mkdir -p "$UNSLOTH_HOME"
|
||||
LLAMA_CPP_DIR="$UNSLOTH_HOME/llama.cpp"
|
||||
LLAMA_SERVER_BIN="$LLAMA_CPP_DIR/build/bin/llama-server"
|
||||
|
|
@ -1155,6 +1199,7 @@ else
|
|||
fi
|
||||
}
|
||||
fi # end _SKIP_GGUF_BUILD check
|
||||
fi # end non-Docker llama.cpp block
|
||||
|
||||
# ── Footer ──
|
||||
if [ "$_LLAMA_ONLY" = "1" ]; then
|
||||
|
|
|
|||
|
|
@ -1584,7 +1584,6 @@ class FastModel(FastBaseModel):
|
|||
|
||||
if _clippable_linear_cls is not None:
|
||||
from peft.tuners.lora.model import LoraModel as _LoraModel
|
||||
|
||||
_original_car = _LoraModel._create_and_replace
|
||||
|
||||
def _patched_car(
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
|
||||
|
||||
import importlib.util
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
|
|
@ -11,7 +8,6 @@ import secrets
|
|||
import sqlite3
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
import types
|
||||
import urllib.error
|
||||
|
|
@ -98,6 +94,7 @@ API_KEY_PBKDF2_SALT_KEY = "api_key_pbkdf2_salt"
|
|||
DESKTOP_SECRET_HASH_KEY = "desktop_secret_hash"
|
||||
DESKTOP_SECRET_CREATED_AT_KEY = "desktop_secret_created_at"
|
||||
PBKDF2_ITERATIONS = 100_000
|
||||
STUDIO_HOME = Path.home() / ".unsloth" / "studio"
|
||||
|
||||
# __file__ is unsloth_cli/commands/studio.py -- two parents up is the package root
|
||||
# (either site-packages or the repo root for editable installs).
|
||||
|
|
@ -234,228 +231,12 @@ def _create_api_key_inprocess(name: str) -> str:
|
|||
``POST /api/auth/api-keys`` on fresh installs. Safe because the
|
||||
CLI already has filesystem access to ``~/.unsloth/studio``.
|
||||
"""
|
||||
storage = _load_backend_auth_storage()
|
||||
from auth.storage import create_api_key, DEFAULT_ADMIN_USERNAME
|
||||
|
||||
raw_key, _row = storage.create_api_key(
|
||||
username = storage.DEFAULT_ADMIN_USERNAME,
|
||||
name = name,
|
||||
)
|
||||
raw_key, _row = create_api_key(username = DEFAULT_ADMIN_USERNAME, name = name)
|
||||
return raw_key
|
||||
|
||||
|
||||
def _load_backend_auth_storage():
|
||||
run_py = _find_run_py()
|
||||
backend_dir = (
|
||||
run_py.parent if run_py is not None else _PACKAGE_ROOT / "studio" / "backend"
|
||||
)
|
||||
if backend_dir.is_dir() and str(backend_dir) not in sys.path:
|
||||
sys.path.insert(0, str(backend_dir))
|
||||
|
||||
auth_dir = backend_dir / "auth"
|
||||
storage_py = auth_dir / "storage.py"
|
||||
loaded = sys.modules.get("auth.storage")
|
||||
loaded_path = Path(getattr(loaded, "__file__", "")).resolve()
|
||||
if loaded is not None and loaded_path == storage_py:
|
||||
return loaded
|
||||
|
||||
package = sys.modules.get("auth")
|
||||
package_paths = [Path(path).resolve() for path in getattr(package, "__path__", [])]
|
||||
if package is None or auth_dir.resolve() not in package_paths:
|
||||
package = types.ModuleType("auth")
|
||||
package.__path__ = [str(auth_dir)]
|
||||
package.__package__ = "auth"
|
||||
package.__file__ = str(auth_dir / "__init__.py")
|
||||
sys.modules["auth"] = package
|
||||
|
||||
spec = importlib.util.spec_from_file_location("auth.storage", storage_py)
|
||||
if spec is None or spec.loader is None:
|
||||
raise ImportError(f"Could not load backend auth storage from {storage_py}")
|
||||
storage = importlib.util.module_from_spec(spec)
|
||||
sys.modules["auth.storage"] = storage
|
||||
spec.loader.exec_module(storage)
|
||||
|
||||
return storage
|
||||
|
||||
|
||||
def _write_auth_secret(path: Path, secret: str) -> None:
|
||||
path.parent.mkdir(parents = True, exist_ok = True)
|
||||
fd, tmp_name = tempfile.mkstemp(prefix = f".{path.name}.", dir = path.parent)
|
||||
tmp_path = Path(tmp_name)
|
||||
try:
|
||||
try:
|
||||
os.chmod(tmp_path, 0o600)
|
||||
except OSError:
|
||||
pass
|
||||
with os.fdopen(fd, "w") as f:
|
||||
fd = -1
|
||||
f.write(secret)
|
||||
os.replace(tmp_path, path)
|
||||
except Exception:
|
||||
if fd >= 0:
|
||||
os.close(fd)
|
||||
tmp_path.unlink(missing_ok = True)
|
||||
raise
|
||||
try:
|
||||
os.chmod(path, 0o600)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def _connect_auth_db() -> sqlite3.Connection:
|
||||
auth_dir = STUDIO_HOME / "auth"
|
||||
auth_dir.mkdir(parents = True, exist_ok = True)
|
||||
conn = sqlite3.connect(auth_dir / "auth.db")
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS auth_user (
|
||||
id INTEGER PRIMARY KEY,
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
password_salt TEXT NOT NULL,
|
||||
password_hash TEXT NOT NULL,
|
||||
jwt_secret TEXT NOT NULL,
|
||||
must_change_password INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS refresh_tokens (
|
||||
id INTEGER PRIMARY KEY,
|
||||
token_hash TEXT NOT NULL,
|
||||
username TEXT NOT NULL,
|
||||
expires_at TEXT NOT NULL,
|
||||
is_desktop INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS api_keys (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT NOT NULL,
|
||||
key_prefix TEXT NOT NULL,
|
||||
key_hash TEXT NOT NULL UNIQUE,
|
||||
name TEXT NOT NULL DEFAULT '',
|
||||
created_at TEXT NOT NULL,
|
||||
last_used_at TEXT,
|
||||
expires_at TEXT,
|
||||
is_active INTEGER NOT NULL DEFAULT 1
|
||||
);
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS app_secrets (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL
|
||||
);
|
||||
"""
|
||||
)
|
||||
auth_columns = {row[1] for row in conn.execute("PRAGMA table_info(auth_user)")}
|
||||
if "must_change_password" not in auth_columns:
|
||||
conn.execute(
|
||||
"ALTER TABLE auth_user ADD COLUMN must_change_password INTEGER NOT NULL DEFAULT 0"
|
||||
)
|
||||
refresh_columns = {
|
||||
row[1] for row in conn.execute("PRAGMA table_info(refresh_tokens)")
|
||||
}
|
||||
if "is_desktop" not in refresh_columns:
|
||||
conn.execute(
|
||||
"ALTER TABLE refresh_tokens ADD COLUMN is_desktop INTEGER NOT NULL DEFAULT 0"
|
||||
)
|
||||
conn.commit()
|
||||
return conn
|
||||
|
||||
|
||||
def _pbkdf2_hex(value: str, salt: bytes) -> str:
|
||||
return hashlib.pbkdf2_hmac(
|
||||
"sha256",
|
||||
value.encode("utf-8"),
|
||||
salt,
|
||||
PBKDF2_ITERATIONS,
|
||||
).hex()
|
||||
|
||||
|
||||
def _hash_password(password: str) -> tuple[str, str]:
|
||||
salt = secrets.token_hex(16)
|
||||
pwd_hash = _pbkdf2_hex(password, salt.encode("utf-8"))
|
||||
return salt, pwd_hash
|
||||
|
||||
|
||||
def _get_or_create_api_key_pbkdf2_salt(conn: sqlite3.Connection) -> bytes:
|
||||
row = conn.execute(
|
||||
"SELECT value FROM app_secrets WHERE key = ?",
|
||||
(API_KEY_PBKDF2_SALT_KEY,),
|
||||
).fetchone()
|
||||
if row is None:
|
||||
salt_hex = secrets.token_hex(32)
|
||||
conn.execute(
|
||||
"INSERT OR IGNORE INTO app_secrets (key, value) VALUES (?, ?)",
|
||||
(API_KEY_PBKDF2_SALT_KEY, salt_hex),
|
||||
)
|
||||
row = conn.execute(
|
||||
"SELECT value FROM app_secrets WHERE key = ?",
|
||||
(API_KEY_PBKDF2_SALT_KEY,),
|
||||
).fetchone()
|
||||
return bytes.fromhex(row[0])
|
||||
|
||||
|
||||
def _ensure_cli_default_admin(conn: sqlite3.Connection) -> None:
|
||||
row = conn.execute(
|
||||
"SELECT 1 FROM auth_user WHERE username = ?",
|
||||
(DEFAULT_ADMIN_USERNAME,),
|
||||
).fetchone()
|
||||
if row is not None:
|
||||
return
|
||||
|
||||
bootstrap_password = secrets.token_urlsafe(32)
|
||||
password_salt, password_hash = _hash_password(bootstrap_password)
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT INTO auth_user (
|
||||
username,
|
||||
password_salt,
|
||||
password_hash,
|
||||
jwt_secret,
|
||||
must_change_password
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
DEFAULT_ADMIN_USERNAME,
|
||||
password_salt,
|
||||
password_hash,
|
||||
secrets.token_urlsafe(64),
|
||||
1,
|
||||
),
|
||||
)
|
||||
_write_auth_secret(
|
||||
STUDIO_HOME / "auth" / BOOTSTRAP_PASSWORD_FILE,
|
||||
bootstrap_password,
|
||||
)
|
||||
|
||||
|
||||
def _create_desktop_secret_in_cli() -> str:
|
||||
raw_secret = DESKTOP_SECRET_PREFIX + secrets.token_urlsafe(48)
|
||||
now = datetime.now(timezone.utc).isoformat()
|
||||
conn = _connect_auth_db()
|
||||
try:
|
||||
_ensure_cli_default_admin(conn)
|
||||
secret_hash = _pbkdf2_hex(raw_secret, _get_or_create_api_key_pbkdf2_salt(conn))
|
||||
conn.execute(
|
||||
"INSERT OR REPLACE INTO app_secrets (key, value) VALUES (?, ?)",
|
||||
(DESKTOP_SECRET_HASH_KEY, secret_hash),
|
||||
)
|
||||
conn.execute(
|
||||
"INSERT OR REPLACE INTO app_secrets (key, value) VALUES (?, ?)",
|
||||
(DESKTOP_SECRET_CREATED_AT_KEY, now),
|
||||
)
|
||||
conn.commit()
|
||||
return raw_secret
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def _load_model_via_http(
|
||||
port: int,
|
||||
api_key: str,
|
||||
|
|
@ -509,11 +290,6 @@ def studio_default(
|
|||
host: str = typer.Option("127.0.0.1", "--host", "-H"),
|
||||
frontend: Optional[Path] = typer.Option(None, "--frontend", "-f"),
|
||||
silent: bool = typer.Option(False, "--silent", "-q"),
|
||||
api_only: bool = typer.Option(
|
||||
False,
|
||||
"--api-only",
|
||||
help = "Run API server only, no frontend serving (for Tauri desktop app)",
|
||||
),
|
||||
):
|
||||
"""Launch the Unsloth Studio server."""
|
||||
# Runs before any subcommand; covers run/setup/update/etc in one place.
|
||||
|
|
@ -521,58 +297,58 @@ def studio_default(
|
|||
if ctx.invoked_subcommand is not None:
|
||||
return
|
||||
|
||||
# Always use the studio venv if it exists and we're not already in it
|
||||
studio_venv_dir = STUDIO_HOME / "unsloth_studio"
|
||||
in_studio_venv = sys.prefix.startswith(str(studio_venv_dir))
|
||||
# In Docker, packages live in /opt/conda — skip venv re-exec entirely.
|
||||
if not os.environ.get("UNSLOTH_DOCKER"):
|
||||
# Always use the studio venv if it exists and we're not already in it
|
||||
studio_venv_dir = STUDIO_HOME / "unsloth_studio"
|
||||
in_studio_venv = sys.prefix.startswith(str(studio_venv_dir))
|
||||
|
||||
if not in_studio_venv:
|
||||
studio_python = _studio_venv_python()
|
||||
run_py = _find_run_py()
|
||||
if studio_python and run_py:
|
||||
if not silent:
|
||||
typer.echo("Launching Unsloth Studio... Please wait...")
|
||||
args = [
|
||||
str(studio_python),
|
||||
str(run_py),
|
||||
"--host",
|
||||
host,
|
||||
"--port",
|
||||
str(port),
|
||||
]
|
||||
if frontend:
|
||||
args.extend(["--frontend", str(frontend)])
|
||||
if silent:
|
||||
args.append("--silent")
|
||||
if api_only:
|
||||
args.append("--api-only")
|
||||
# On Windows, os.execvp() spawns a child but the parent lingers,
|
||||
# so Ctrl+C only kills the parent leaving the child orphaned.
|
||||
# Use subprocess.run() on Windows so the parent waits for the child.
|
||||
if sys.platform == "win32":
|
||||
import subprocess as _sp
|
||||
if not in_studio_venv:
|
||||
studio_python = _studio_venv_python()
|
||||
run_py = _find_run_py()
|
||||
if studio_python and run_py:
|
||||
if not silent:
|
||||
typer.echo("Launching Unsloth Studio... Please wait...")
|
||||
args = [
|
||||
str(studio_python),
|
||||
str(run_py),
|
||||
"--host",
|
||||
host,
|
||||
"--port",
|
||||
str(port),
|
||||
]
|
||||
if frontend:
|
||||
args.extend(["--frontend", str(frontend)])
|
||||
if silent:
|
||||
args.append("--silent")
|
||||
# On Windows, os.execvp() spawns a child but the parent lingers,
|
||||
# so Ctrl+C only kills the parent leaving the child orphaned.
|
||||
# Use subprocess.run() on Windows so the parent waits for the child.
|
||||
if sys.platform == "win32":
|
||||
import subprocess as _sp
|
||||
|
||||
proc = _sp.Popen(args, **_windows_hidden_subprocess_kwargs())
|
||||
try:
|
||||
rc = proc.wait()
|
||||
except KeyboardInterrupt:
|
||||
# Child has its own signal handler — let it finish
|
||||
rc = proc.wait()
|
||||
if rc != 0:
|
||||
typer.echo(
|
||||
f"\nError: Studio server exited unexpectedly (code {rc}).",
|
||||
err = True,
|
||||
)
|
||||
typer.echo(
|
||||
"Check the error above. If a package is missing, "
|
||||
"re-run: unsloth studio setup",
|
||||
err = True,
|
||||
)
|
||||
raise typer.Exit(rc)
|
||||
proc = _sp.Popen(args)
|
||||
try:
|
||||
rc = proc.wait()
|
||||
except KeyboardInterrupt:
|
||||
# Child has its own signal handler — let it finish
|
||||
rc = proc.wait()
|
||||
if rc != 0:
|
||||
typer.echo(
|
||||
f"\nError: Studio server exited unexpectedly (code {rc}).",
|
||||
err = True,
|
||||
)
|
||||
typer.echo(
|
||||
"Check the error above. If a package is missing, "
|
||||
"re-run: unsloth studio setup",
|
||||
err = True,
|
||||
)
|
||||
raise typer.Exit(rc)
|
||||
else:
|
||||
os.execvp(str(studio_python), args)
|
||||
else:
|
||||
os.execvp(str(studio_python), args)
|
||||
else:
|
||||
typer.echo("Studio not set up. Run install.sh first.")
|
||||
raise typer.Exit(1)
|
||||
typer.echo("Studio not set up. Run install.sh first.")
|
||||
raise typer.Exit(1)
|
||||
|
||||
from studio.backend.run import run_server
|
||||
|
||||
|
|
@ -582,7 +358,7 @@ def studio_default(
|
|||
display_host = _resolve_external_ip() if host == "0.0.0.0" else host
|
||||
typer.echo(f"Starting Unsloth Studio on http://{display_host}:{port}")
|
||||
|
||||
run_kwargs = dict(host = host, port = port, silent = silent, api_only = api_only)
|
||||
run_kwargs = dict(host = host, port = port, silent = silent)
|
||||
if frontend is not None:
|
||||
run_kwargs["frontend_path"] = frontend
|
||||
run_server(**run_kwargs)
|
||||
|
|
@ -1053,12 +829,15 @@ def _run_setup_script(*, verbose: bool = False) -> None:
|
|||
# CREATE_NO_WINDOW. Empty update.log on the windows-latest
|
||||
# CI was the smoking gun (run 25533694490 and 25534292239).
|
||||
result = subprocess.run(
|
||||
powershell_args,
|
||||
["powershell", "-ExecutionPolicy", "Bypass", "-File", str(script)],
|
||||
env = env,
|
||||
stdin = _stream_for_subprocess(sys.stdin),
|
||||
stdout = _stream_for_subprocess(sys.stdout),
|
||||
stderr = _stream_for_subprocess(sys.stderr),
|
||||
**_windows_hidden_subprocess_kwargs(),
|
||||
result = subprocess.run(
|
||||
["powershell", "-ExecutionPolicy", "Bypass", "-File", str(script)],
|
||||
env = env,
|
||||
)
|
||||
else:
|
||||
result = subprocess.run(["bash", str(script)], env = env)
|
||||
|
|
@ -1412,18 +1191,13 @@ def reset_password():
|
|||
"""
|
||||
auth_dir = STUDIO_HOME / "auth"
|
||||
db_file = auth_dir / "auth.db"
|
||||
stale_files = [
|
||||
auth_dir / BOOTSTRAP_PASSWORD_FILE,
|
||||
auth_dir / DESKTOP_SECRET_FILE,
|
||||
]
|
||||
had_db = db_file.exists()
|
||||
pw_file = auth_dir / ".bootstrap_password"
|
||||
|
||||
db_file.unlink(missing_ok = True)
|
||||
for path in stale_files:
|
||||
path.unlink(missing_ok = True)
|
||||
|
||||
if not had_db:
|
||||
if not db_file.exists():
|
||||
typer.echo("No auth database found -- nothing to reset.")
|
||||
raise typer.Exit(0)
|
||||
|
||||
db_file.unlink(missing_ok = True)
|
||||
pw_file.unlink(missing_ok = True)
|
||||
|
||||
typer.echo("Auth database deleted. Restart Unsloth Studio to get a new password.")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue