Compare commits

...
Sign in to create a new pull request.

6 commits

Author SHA1 Message Date
Roland Tannous
aa7c0b6d6c recover working install files 2026-03-19 14:18:00 +00:00
Roland Tannous
286f954da6 fix setup.sh marker removal 2026-03-19 13:36:13 +00:00
Roland Tannous
c2b0a49627 chore: comment out llama.cpp build in setup.sh 2026-03-19 09:48:16 +00:00
Roland Tannous
6e2f64b3b6 fix: persist studio home path across server restarts 2026-03-19 09:33:27 +00:00
Roland Tannous
4e69c0e415 fix: use venv_t5_root() so .venv_t5 respects UNSLOTH_STUDIO_HOME 2026-03-19 09:33:27 +00:00
Roland Tannous
bf0007c502 feat: add UNSLOTH_STUDIO_HOME env var to override studio root
Allow users to set UNSLOTH_STUDIO_HOME to relocate all studio data
(venvs, assets, outputs, exports, auth, cache, tensorboard runs).
Defaults to ~/.unsloth/studio when unset (no behaviour change).
2026-03-19 09:33:27 +00:00
11 changed files with 307 additions and 318 deletions

View file

@ -49,9 +49,30 @@ def _activate_transformers_version(model_name: str) -> None:
resolved = _resolve_base_model(model_name) resolved = _resolve_base_model(model_name)
if needs_transformers_5(resolved): if needs_transformers_5(resolved):
if not _ensure_venv_t5_exists(): from utils.paths.storage_roots import venv_t5_root
raise RuntimeError( venv_t5 = str(venv_t5_root())
f"Cannot activate transformers 5.x: .venv_t5 missing at {_VENV_T5_DIR}" if os.path.isdir(venv_t5):
sys.path.insert(0, venv_t5)
logger.info("Activated transformers 5.x from %s", venv_t5)
else:
# Fallback: pip install at runtime (slower, ~10-15s)
logger.warning(".venv_t5 not found at %s — installing at runtime", venv_t5)
import subprocess as sp
os.makedirs(venv_t5, exist_ok = True)
r1 = sp.run(
[
sys.executable,
"-m",
"pip",
"install",
"--target",
venv_t5,
"--no-deps",
"transformers==5.3.0",
],
stdout = sp.PIPE,
stderr = sp.STDOUT,
) )
if _VENV_T5_DIR not in sys.path: if _VENV_T5_DIR not in sys.path:
sys.path.insert(0, _VENV_T5_DIR) sys.path.insert(0, _VENV_T5_DIR)

View file

@ -51,9 +51,30 @@ def _activate_transformers_version(model_name: str) -> None:
resolved = _resolve_base_model(model_name) resolved = _resolve_base_model(model_name)
if needs_transformers_5(resolved): if needs_transformers_5(resolved):
if not _ensure_venv_t5_exists(): from utils.paths.storage_roots import venv_t5_root
raise RuntimeError( venv_t5 = str(venv_t5_root())
f"Cannot activate transformers 5.x: .venv_t5 missing at {_VENV_T5_DIR}" if os.path.isdir(venv_t5):
sys.path.insert(0, venv_t5)
logger.info("Activated transformers 5.x from %s", venv_t5)
else:
# Fallback: pip install at runtime (slower, ~10-15s)
logger.warning(".venv_t5 not found at %s — installing at runtime", venv_t5)
import subprocess as sp
os.makedirs(venv_t5, exist_ok = True)
r1 = sp.run(
[
sys.executable,
"-m",
"pip",
"install",
"--target",
venv_t5,
"--no-deps",
"transformers==5.3.0",
],
stdout = sp.PIPE,
stderr = sp.STDOUT,
) )
if _VENV_T5_DIR not in sys.path: if _VENV_T5_DIR not in sys.path:
sys.path.insert(0, _VENV_T5_DIR) sys.path.insert(0, _VENV_T5_DIR)

View file

@ -45,9 +45,30 @@ def _activate_transformers_version(model_name: str) -> None:
resolved = _resolve_base_model(model_name) resolved = _resolve_base_model(model_name)
if needs_transformers_5(resolved): if needs_transformers_5(resolved):
if not _ensure_venv_t5_exists(): from utils.paths.storage_roots import venv_t5_root
raise RuntimeError( venv_t5 = str(venv_t5_root())
f"Cannot activate transformers 5.x: .venv_t5 missing at {_VENV_T5_DIR}" if os.path.isdir(venv_t5):
sys.path.insert(0, venv_t5)
logger.info("Activated transformers 5.x from %s", venv_t5)
else:
# Fallback: pip install at runtime (slower, ~10-15s)
logger.warning(".venv_t5 not found at %s — installing at runtime", venv_t5)
import subprocess as sp
os.makedirs(venv_t5, exist_ok = True)
r1 = sp.run(
[
sys.executable,
"-m",
"pip",
"install",
"--target",
venv_t5,
"--no-deps",
"transformers==5.3.0",
],
stdout = sp.PIPE,
stderr = sp.STDOUT,
) )
if _VENV_T5_DIR not in sys.path: if _VENV_T5_DIR not in sys.path:
sys.path.insert(0, _VENV_T5_DIR) sys.path.insert(0, _VENV_T5_DIR)

View file

@ -427,7 +427,8 @@ _VLM_MODEL_TYPES = {
} }
# Pre-computed .venv_t5 path and backend dir for subprocess version switching. # Pre-computed .venv_t5 path and backend dir for subprocess version switching.
_VENV_T5_DIR = str(Path.home() / ".unsloth" / "studio" / ".venv_t5") from utils.paths.storage_roots import venv_t5_root
_VENV_T5_DIR = str(venv_t5_root())
_BACKEND_DIR = str(Path(__file__).resolve().parent.parent.parent) _BACKEND_DIR = str(Path(__file__).resolve().parent.parent.parent)
# Inline script executed in a subprocess with transformers 5.x activated. # Inline script executed in a subprocess with transformers 5.x activated.

View file

@ -8,6 +8,7 @@ Path utilities for model and dataset handling
from .path_utils import normalize_path, is_local_path, is_model_cached, get_cache_path from .path_utils import normalize_path, is_local_path, is_model_cached, get_cache_path
from .storage_roots import ( from .storage_roots import (
studio_root, studio_root,
venv_t5_root,
assets_root, assets_root,
datasets_root, datasets_root,
dataset_uploads_root, dataset_uploads_root,
@ -36,6 +37,7 @@ __all__ = [
"is_model_cached", "is_model_cached",
"get_cache_path", "get_cache_path",
"studio_root", "studio_root",
"venv_t5_root",
"assets_root", "assets_root",
"datasets_root", "datasets_root",
"dataset_uploads_root", "dataset_uploads_root",

View file

@ -9,12 +9,26 @@ import tempfile
def studio_root() -> Path: def studio_root() -> Path:
"""Studio root: env var > config file > default."""
custom = os.environ.get("UNSLOTH_STUDIO_HOME")
if custom:
return Path(custom).expanduser().resolve()
conf = Path.home() / ".unsloth" / "studio_home"
if conf.is_file():
saved = conf.read_text().strip()
if saved:
return Path(saved).expanduser().resolve()
return Path.home() / ".unsloth" / "studio" return Path.home() / ".unsloth" / "studio"
def venv_t5_root() -> Path:
"""Pre-installed transformers 5.x directory, respects UNSLOTH_STUDIO_HOME."""
return studio_root() / ".venv_t5"
def cache_root() -> Path: def cache_root() -> Path:
"""Central cache directory for all studio downloads (models, datasets, etc.).""" """Central cache directory for all studio downloads (models, datasets, etc.)."""
return Path.home() / ".unsloth" / "studio" / "cache" return studio_root() / "cache"
def assets_root() -> Path: def assets_root() -> Path:

View file

@ -62,7 +62,8 @@ TRANSFORMERS_5_VERSION = "5.3.0"
TRANSFORMERS_DEFAULT_VERSION = "4.57.6" TRANSFORMERS_DEFAULT_VERSION = "4.57.6"
# Pre-installed directory for transformers 5.x — created by setup.sh / setup.ps1 # Pre-installed directory for transformers 5.x — created by setup.sh / setup.ps1
_VENV_T5_DIR = str(Path.home() / ".unsloth" / "studio" / ".venv_t5") from utils.paths.storage_roots import venv_t5_root
_VENV_T5_DIR = str(venv_t5_root())
def _resolve_base_model(model_name: str) -> str: def _resolve_base_model(model_name: str) -> str:

View file

@ -140,15 +140,15 @@ def _bootstrap_uv() -> bool:
global UV_NEEDS_SYSTEM global UV_NEEDS_SYSTEM
if not shutil.which("uv"): if not shutil.which("uv"):
return False return False
# Probe: try a dry-run install targeting the current Python explicitly. # Probe: try a dry-run install without --system.
# Without --python, uv can ignore the activated venv on some platforms. # If uv can't find a venv it exits with code 2.
probe = subprocess.run( probe = subprocess.run(
["uv", "pip", "install", "--dry-run", "--python", sys.executable, "pip"], ["uv", "pip", "install", "--dry-run", "pip"],
stdout = subprocess.PIPE, stdout = subprocess.PIPE,
stderr = subprocess.STDOUT, stderr = subprocess.STDOUT,
) )
if probe.returncode != 0: if probe.returncode != 0:
# Retry with --system (some envs need it when uv can't find a venv) # Retry with --system to confirm it works
probe_sys = subprocess.run( probe_sys = subprocess.run(
["uv", "pip", "install", "--dry-run", "--system", "pip"], ["uv", "pip", "install", "--dry-run", "--system", "pip"],
stdout = subprocess.PIPE, stdout = subprocess.PIPE,
@ -204,10 +204,6 @@ def _build_uv_cmd(args: tuple[str, ...]) -> list[str]:
cmd = ["uv", "pip", "install"] cmd = ["uv", "pip", "install"]
if UV_NEEDS_SYSTEM: if UV_NEEDS_SYSTEM:
cmd.append("--system") cmd.append("--system")
# Always pass --python so uv targets the correct environment.
# Without this, uv can ignore an activated venv and install into
# the system Python (observed on Colab and similar environments).
cmd.extend(["--python", sys.executable])
cmd.extend(_translate_pip_args_for_uv(args)) cmd.extend(_translate_pip_args_for_uv(args))
cmd.append("--torch-backend=auto") cmd.append("--torch-backend=auto")
return cmd return cmd

View file

@ -966,9 +966,14 @@ if (-not $PythonCmd) {
Write-Host "[OK] Using $PythonCmd ($(& $PythonCmd --version 2>&1))" -ForegroundColor Green Write-Host "[OK] Using $PythonCmd ($(& $PythonCmd --version 2>&1))" -ForegroundColor Green
# Always create a .venv for isolation -- even for pip installs. # ── Studio home (configurable via UNSLOTH_STUDIO_HOME) ──
# Created in the repo root (parent of studio/). $StudioHome = if ($env:UNSLOTH_STUDIO_HOME) { $env:UNSLOTH_STUDIO_HOME } else { Join-Path $env:USERPROFILE ".unsloth\studio" }
$VenvDir = Join-Path $env:USERPROFILE ".unsloth\studio\.venv" # Persist for future `unsloth studio` runs (survives shell restarts)
$UnslothDir = Join-Path $env:USERPROFILE ".unsloth"
if (-not (Test-Path $UnslothDir)) { New-Item -ItemType Directory -Path $UnslothDir -Force | Out-Null }
Set-Content -Path (Join-Path $UnslothDir "studio_home") -Value $StudioHome -NoNewline
$VenvDir = Join-Path $StudioHome ".venv"
if (-not (Test-Path $VenvDir)) { if (-not (Test-Path $VenvDir)) {
Write-Host " Creating virtual environment at $VenvDir..." -ForegroundColor Cyan Write-Host " Creating virtual environment at $VenvDir..." -ForegroundColor Cyan
& $PythonCmd -m venv $VenvDir & $PythonCmd -m venv $VenvDir
@ -1090,7 +1095,7 @@ $ErrorActionPreference = $prevEAP
# The training subprocess just prepends .venv_t5/ to sys.path -- instant switch. # The training subprocess just prepends .venv_t5/ to sys.path -- instant switch.
Write-Host "" Write-Host ""
Write-Host " Pre-installing transformers 5.x for newer model support..." -ForegroundColor Cyan Write-Host " Pre-installing transformers 5.x for newer model support..." -ForegroundColor Cyan
$VenvT5Dir = Join-Path $env:USERPROFILE ".unsloth\studio\.venv_t5" $VenvT5Dir = Join-Path $StudioHome ".venv_t5"
if (Test-Path $VenvT5Dir) { Remove-Item -Recurse -Force $VenvT5Dir } if (Test-Path $VenvT5Dir) { Remove-Item -Recurse -Force $VenvT5Dir }
New-Item -ItemType Directory -Path $VenvT5Dir -Force | Out-Null New-Item -ItemType Directory -Path $VenvT5Dir -Force | Out-Null
$prevEAP_t5 = $ErrorActionPreference $prevEAP_t5 = $ErrorActionPreference

View file

@ -41,26 +41,13 @@ if [[ "$keynames" == *$'\nCOLAB_'* ]]; then
fi fi
# ── Detect whether frontend needs building ── # ── Detect whether frontend needs building ──
# Skip if dist/ exists AND no tracked input is newer than dist/. # Only skip when BOTH conditions are true:
# Checks top-level config/entry files and src/, public/ recursively. # 1. We're inside site-packages (PyPI / pip install, not editable)
# This handles: PyPI installs (dist/ bundled), repeat runs (no changes), # 2. dist/ already exists (pre-built in the wheel)
# and upgrades/pulls (source newer than dist/ triggers rebuild). # Otherwise always (re)build — handles upgrades, editable installs, and
_NEED_FRONTEND_BUILD=true # pip-from-source where dist/ was never built.
if [ -d "$SCRIPT_DIR/frontend/dist" ]; then if [[ "$SCRIPT_DIR" == */site-packages/* ]] && [ -d "$SCRIPT_DIR/frontend/dist" ]; then
# Check all top-level files (package.json, bun.lock, vite.config.ts, index.html, etc.) echo "✅ Frontend pre-built (PyPI) — skipping Node/npm check."
_changed=$(find "$SCRIPT_DIR/frontend" -maxdepth 1 -type f \
-newer "$SCRIPT_DIR/frontend/dist" -print -quit 2>/dev/null)
# Check src/ and public/ recursively (|| true guards against set -e when dirs are missing)
if [ -z "$_changed" ]; then
_changed=$(find "$SCRIPT_DIR/frontend/src" "$SCRIPT_DIR/frontend/public" \
-type f -newer "$SCRIPT_DIR/frontend/dist" -print -quit 2>/dev/null) || true
fi
if [ -z "$_changed" ]; then
_NEED_FRONTEND_BUILD=false
fi
fi
if [ "$_NEED_FRONTEND_BUILD" = false ]; then
echo "✅ Frontend already built and up to date -- skipping Node/npm check."
else else
NEED_NODE=true NEED_NODE=true
if command -v node &>/dev/null && command -v npm &>/dev/null; then if command -v node &>/dev/null && command -v npm &>/dev/null; then
@ -159,27 +146,12 @@ run_quiet "npm run build" npm run build
_restore_gitignores _restore_gitignores
trap - EXIT trap - EXIT
cd "$SCRIPT_DIR/backend/core/data_recipe/oxc-validator"
# Validate CSS output -- catch truncated Tailwind builds run_quiet "npm install (oxc validator runtime)" npm install
_MAX_CSS=$(find "$SCRIPT_DIR/frontend/dist/assets" -name '*.css' -exec wc -c {} + 2>/dev/null | sort -n | tail -1 | awk '{print $1}')
if [ -z "$_MAX_CSS" ]; then
echo "⚠️ WARNING: No CSS files were emitted. The frontend build may have failed."
elif [ "$_MAX_CSS" -lt 100000 ]; then
echo "⚠️ WARNING: Largest CSS file is only $((_MAX_CSS / 1024))KB (expected >100KB)."
echo " Tailwind may not have scanned all source files. Check for .gitignore interference."
fi
cd "$SCRIPT_DIR" cd "$SCRIPT_DIR"
echo "✅ Frontend built to frontend/dist" echo "✅ Frontend built to frontend/dist"
fi # end frontend build check fi # end frontend dist check
# ── oxc-validator runtime (needs npm -- skip if not available) ──
if [ -d "$SCRIPT_DIR/backend/core/data_recipe/oxc-validator" ] && command -v npm &>/dev/null; then
cd "$SCRIPT_DIR/backend/core/data_recipe/oxc-validator"
run_quiet "npm install (oxc validator runtime)" npm install
cd "$SCRIPT_DIR"
fi
# ── 6. Python venv + deps ── # ── 6. Python venv + deps ──
@ -251,261 +223,188 @@ install_python_stack() {
python "$SCRIPT_DIR/install_python_stack.py" python "$SCRIPT_DIR/install_python_stack.py"
} }
# Create venv under ~/.unsloth/studio/ (shared location, not in repo). if [ "$IS_COLAB" = true ]; then
# All platforms (including Colab) use the same isolated venv so that # Colab: install packages directly without venv
# studio dependencies are never installed into the system Python. install_python_stack
STUDIO_HOME="$HOME/.unsloth/studio"
VENV_DIR="$STUDIO_HOME/.venv"
VENV_T5_DIR="$STUDIO_HOME/.venv_t5"
mkdir -p "$STUDIO_HOME"
# Clean up legacy in-repo venvs if they exist
[ -d "$REPO_ROOT/.venv" ] && rm -rf "$REPO_ROOT/.venv"
[ -d "$REPO_ROOT/.venv_overlay" ] && rm -rf "$REPO_ROOT/.venv_overlay"
[ -d "$REPO_ROOT/.venv_t5" ] && rm -rf "$REPO_ROOT/.venv_t5"
rm -rf "$VENV_DIR"
rm -rf "$VENV_T5_DIR"
# Try creating venv with pip; fall back to --without-pip + bootstrap
# (some environments like Colab have broken ensurepip)
if ! "$BEST_PY" -m venv "$VENV_DIR" 2>/dev/null; then
"$BEST_PY" -m venv --without-pip "$VENV_DIR"
source "$VENV_DIR/bin/activate"
curl -sS https://bootstrap.pypa.io/get-pip.py | python > /dev/null
else else
# Local: create venv under studio home (shared location, not in repo)
# Configurable via UNSLOTH_STUDIO_HOME; defaults to ~/.unsloth/studio
STUDIO_HOME="${UNSLOTH_STUDIO_HOME:-$HOME/.unsloth/studio}"
echo " Studio home: $STUDIO_HOME"
# Persist for future `unsloth studio` runs (survives shell restarts)
mkdir -p "$HOME/.unsloth"
echo "$STUDIO_HOME" > "$HOME/.unsloth/studio_home"
VENV_DIR="$STUDIO_HOME/.venv"
VENV_T5_DIR="$STUDIO_HOME/.venv_t5"
mkdir -p "$STUDIO_HOME"
# Clean up legacy in-repo venvs if they exist
[ -d "$REPO_ROOT/.venv" ] && rm -rf "$REPO_ROOT/.venv"
[ -d "$REPO_ROOT/.venv_overlay" ] && rm -rf "$REPO_ROOT/.venv_overlay"
[ -d "$REPO_ROOT/.venv_t5" ] && rm -rf "$REPO_ROOT/.venv_t5"
rm -rf "$VENV_DIR"
rm -rf "$VENV_T5_DIR"
"$BEST_PY" -m venv "$VENV_DIR"
source "$VENV_DIR/bin/activate" source "$VENV_DIR/bin/activate"
fi cd "$SCRIPT_DIR"
install_python_stack
# ── Ensure uv is available (much faster than pip) ── # ── 6b. Pre-install transformers 5.x into .venv_t5/ ──
USE_UV=false # Models like GLM-4.7-Flash need transformers>=5.3.0. Instead of pip-installing
if command -v uv &>/dev/null; then # at runtime (slow, ~10-15s), we pre-install into a separate directory.
USE_UV=true # The training subprocess just prepends .venv_t5/ to sys.path — instant switch.
elif curl -LsSf https://astral.sh/uv/install.sh | sh > /dev/null 2>&1; then
export PATH="$HOME/.local/bin:$PATH"
command -v uv &>/dev/null && USE_UV=true
fi
# Helper: install a package, preferring uv with pip fallback
fast_install() {
if [ "$USE_UV" = true ]; then
uv pip install --python "$(command -v python)" "$@" && return 0
fi
python -m pip install "$@"
}
cd "$SCRIPT_DIR"
install_python_stack
# ── 6b. Pre-install transformers 5.x into .venv_t5/ ──
# Models like GLM-4.7-Flash need transformers>=5.3.0. Instead of pip-installing
# at runtime (slow, ~10-15s), we pre-install into a separate directory.
# The training subprocess just prepends .venv_t5/ to sys.path -- instant switch.
echo ""
echo " Pre-installing transformers 5.x for newer model support..."
mkdir -p "$VENV_T5_DIR"
run_quiet "install transformers 5.x" fast_install --target "$VENV_T5_DIR" --no-deps "transformers==5.3.0"
run_quiet "install huggingface_hub for t5" fast_install --target "$VENV_T5_DIR" --no-deps "huggingface_hub==1.7.1"
run_quiet "install hf_xet for t5" fast_install --target "$VENV_T5_DIR" --no-deps "hf_xet==1.4.2"
# tiktoken is needed by Qwen-family tokenizers. Install with deps since
# regex/requests may be missing on Windows.
run_quiet "install tiktoken for t5" fast_install --target "$VENV_T5_DIR" "tiktoken"
echo "✅ Transformers 5.x pre-installed to $VENV_T5_DIR/"
# ── 7. WSL: pre-install GGUF build dependencies ──
# On WSL, sudo requires a password and can't be entered during GGUF export
# (runs in a non-interactive subprocess). Install build deps here instead.
if grep -qi microsoft /proc/version 2>/dev/null; then
echo "" echo ""
echo "⚠️ WSL detected -- installing build dependencies for GGUF export..." echo " Pre-installing transformers 5.x for newer model support..."
_GGUF_DEPS="pciutils build-essential cmake curl git libcurl4-openssl-dev" mkdir -p "$VENV_T5_DIR"
run_quiet "pip install transformers 5.x" pip install --target "$VENV_T5_DIR" --no-deps "transformers==5.3.0"
run_quiet "pip install huggingface_hub for t5" pip install --target "$VENV_T5_DIR" --no-deps "huggingface_hub==1.3.0"
echo "✅ Transformers 5.x pre-installed to $VENV_T5_DIR/"
# Try without sudo first (works when already root) # ── 7. WSL: pre-install GGUF build dependencies ──
apt-get update -y >/dev/null 2>&1 || true # On WSL, sudo requires a password and can't be entered during GGUF export
apt-get install -y $_GGUF_DEPS >/dev/null 2>&1 || true # (runs in a non-interactive subprocess). Install build deps here instead.
if grep -qi microsoft /proc/version 2>/dev/null; then
# Check which packages are still missing echo ""
_STILL_MISSING="" echo "⚠️ WSL detected — installing build dependencies for GGUF export..."
for _pkg in $_GGUF_DEPS; do echo " You may be prompted for your password."
case "$_pkg" in sudo apt-get update -y
build-essential) command -v gcc >/dev/null 2>&1 || _STILL_MISSING="$_STILL_MISSING $_pkg" ;; sudo apt-get install -y build-essential cmake curl git libcurl4-openssl-dev
pciutils) command -v lspci >/dev/null 2>&1 || _STILL_MISSING="$_STILL_MISSING $_pkg" ;;
libcurl4-openssl-dev) dpkg -s "$_pkg" >/dev/null 2>&1 || _STILL_MISSING="$_STILL_MISSING $_pkg" ;;
*) command -v "$_pkg" >/dev/null 2>&1 || _STILL_MISSING="$_STILL_MISSING $_pkg" ;;
esac
done
_STILL_MISSING=$(echo "$_STILL_MISSING" | sed 's/^ *//')
if [ -z "$_STILL_MISSING" ]; then
echo "✅ GGUF build dependencies installed" echo "✅ GGUF build dependencies installed"
elif command -v sudo >/dev/null 2>&1; then
echo ""
echo " !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
echo " WARNING: We require sudo elevated permissions to install:"
echo " $_STILL_MISSING"
echo " If you accept, we'll run sudo now, and it'll prompt your password."
echo " !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
echo ""
printf " Accept? [Y/n] "
if [ -r /dev/tty ]; then
read -r REPLY </dev/tty || REPLY="y"
else
REPLY="y"
fi
case "$REPLY" in
[nN]*)
echo ""
echo " Please install these packages first, then re-run Unsloth Studio setup:"
echo " sudo apt-get update -y && sudo apt-get install -y $_STILL_MISSING"
_SKIP_GGUF_BUILD=true
;;
*)
sudo apt-get update -y
sudo apt-get install -y $_STILL_MISSING
echo "✅ GGUF build dependencies installed"
;;
esac
else
echo " sudo is not available on this system."
echo " Please install as root, then re-run setup:"
echo " apt-get install -y $_STILL_MISSING"
_SKIP_GGUF_BUILD=true
fi fi
fi fi
# ── 8. Build llama.cpp binaries for GGUF inference + export ── # ── 8. Build llama.cpp binaries for GGUF inference + export ──
# Builds at ~/.unsloth/llama.cpp — a single shared location under the user's # Disabled: llama.cpp build is commented out for now.
# home directory. This is used by both the inference server and the GGUF # UNCOMMENT the block below to re-enable.
# export pipeline (unsloth-zoo). #
# - llama-server: for GGUF model inference # # Builds at ~/.unsloth/llama.cpp — a single shared location under the user's
# - llama-quantize: for GGUF export quantization (symlinked to root for check_llama_cpp()) # # home directory. This is used by both the inference server and the GGUF
UNSLOTH_HOME="$HOME/.unsloth" # # export pipeline (unsloth-zoo).
mkdir -p "$UNSLOTH_HOME" # # - llama-server: for GGUF model inference
LLAMA_CPP_DIR="$UNSLOTH_HOME/llama.cpp" # # - llama-quantize: for GGUF export quantization (symlinked to root for check_llama_cpp())
LLAMA_SERVER_BIN="$LLAMA_CPP_DIR/build/bin/llama-server" # UNSLOTH_HOME="$HOME/.unsloth"
if [ "${_SKIP_GGUF_BUILD:-}" = true ]; then # mkdir -p "$UNSLOTH_HOME"
echo "" # LLAMA_CPP_DIR="$UNSLOTH_HOME/llama.cpp"
echo "Skipping llama-server build (missing dependencies)" # LLAMA_SERVER_BIN="$LLAMA_CPP_DIR/build/bin/llama-server"
echo " Install the missing packages and re-run setup to enable GGUF inference." # rm -rf "$LLAMA_CPP_DIR"
else # {
rm -rf "$LLAMA_CPP_DIR" # # Check prerequisites
{ # if ! command -v cmake &>/dev/null; then
# Check prerequisites # echo ""
if ! command -v cmake &>/dev/null; then # echo "⚠️ cmake not found — skipping llama-server build (GGUF inference won't be available)"
echo "" # echo " Install cmake and re-run setup.sh to enable GGUF inference."
echo "⚠️ cmake not found — skipping llama-server build (GGUF inference won't be available)" # elif ! command -v git &>/dev/null; then
echo " Install cmake and re-run setup.sh to enable GGUF inference." # echo ""
elif ! command -v git &>/dev/null; then # echo "⚠️ git not found — skipping llama-server build (GGUF inference won't be available)"
echo "" # else
echo "⚠️ git not found — skipping llama-server build (GGUF inference won't be available)" # echo ""
else # echo "Building llama-server for GGUF inference..."
echo "" #
echo "Building llama-server for GGUF inference..." # BUILD_OK=true
# run_quiet "clone llama.cpp" git clone --depth 1 https://github.com/ggml-org/llama.cpp.git "$LLAMA_CPP_DIR" || BUILD_OK=false
BUILD_OK=true #
run_quiet "clone llama.cpp" git clone --depth 1 https://github.com/ggml-org/llama.cpp.git "$LLAMA_CPP_DIR" || BUILD_OK=false # if [ "$BUILD_OK" = true ]; then
# # Skip tests/examples we don't need (faster build)
if [ "$BUILD_OK" = true ]; then # CMAKE_ARGS="-DLLAMA_BUILD_TESTS=OFF -DLLAMA_BUILD_EXAMPLES=OFF -DLLAMA_BUILD_SERVER=ON -DGGML_NATIVE=ON"
# Skip tests/examples we don't need (faster build) #
CMAKE_ARGS="-DLLAMA_BUILD_TESTS=OFF -DLLAMA_BUILD_EXAMPLES=OFF -DLLAMA_BUILD_SERVER=ON -DGGML_NATIVE=ON" # # Use ccache if available (dramatically faster rebuilds)
# if command -v ccache &>/dev/null; then
# Use ccache if available (dramatically faster rebuilds) # CMAKE_ARGS="$CMAKE_ARGS -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DCMAKE_CUDA_COMPILER_LAUNCHER=ccache"
if command -v ccache &>/dev/null; then # echo " Using ccache for faster compilation"
CMAKE_ARGS="$CMAKE_ARGS -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DCMAKE_CUDA_COMPILER_LAUNCHER=ccache" # fi
echo " Using ccache for faster compilation" #
fi # # Detect CUDA: check nvcc on PATH, then common install locations
# NVCC_PATH=""
# Detect CUDA: check nvcc on PATH, then common install locations # if command -v nvcc &>/dev/null; then
NVCC_PATH="" # NVCC_PATH="$(command -v nvcc)"
if command -v nvcc &>/dev/null; then # elif [ -x /usr/local/cuda/bin/nvcc ]; then
NVCC_PATH="$(command -v nvcc)" # NVCC_PATH="/usr/local/cuda/bin/nvcc"
elif [ -x /usr/local/cuda/bin/nvcc ]; then # export PATH="/usr/local/cuda/bin:$PATH"
NVCC_PATH="/usr/local/cuda/bin/nvcc" # elif ls /usr/local/cuda-*/bin/nvcc &>/dev/null 2>&1; then
export PATH="/usr/local/cuda/bin:$PATH" # # Pick the newest cuda-XX.X directory
elif ls /usr/local/cuda-*/bin/nvcc &>/dev/null 2>&1; then # NVCC_PATH="$(ls -d /usr/local/cuda-*/bin/nvcc 2>/dev/null | sort -V | tail -1)"
# Pick the newest cuda-XX.X directory # export PATH="$(dirname "$NVCC_PATH"):$PATH"
NVCC_PATH="$(ls -d /usr/local/cuda-*/bin/nvcc 2>/dev/null | sort -V | tail -1)" # fi
export PATH="$(dirname "$NVCC_PATH"):$PATH" #
fi # if [ -n "$NVCC_PATH" ]; then
# echo " Building with CUDA support (nvcc: $NVCC_PATH)..."
if [ -n "$NVCC_PATH" ]; then # CMAKE_ARGS="$CMAKE_ARGS -DGGML_CUDA=ON"
echo " Building with CUDA support (nvcc: $NVCC_PATH)..." #
CMAKE_ARGS="$CMAKE_ARGS -DGGML_CUDA=ON" # # Detect GPU compute capability and limit CUDA architectures
# # Without this, cmake builds for ALL default archs (very slow)
# Detect GPU compute capability and limit CUDA architectures # CUDA_ARCHS=""
# Without this, cmake builds for ALL default archs (very slow) # if command -v nvidia-smi &>/dev/null; then
CUDA_ARCHS="" # # Read all GPUs, deduplicate (handles mixed-GPU hosts)
if command -v nvidia-smi &>/dev/null; then # _raw_caps=$(nvidia-smi --query-gpu=compute_cap --format=csv,noheader 2>/dev/null || true)
# Read all GPUs, deduplicate (handles mixed-GPU hosts) # while IFS= read -r _cap; do
_raw_caps=$(nvidia-smi --query-gpu=compute_cap --format=csv,noheader 2>/dev/null || true) # _cap=$(echo "$_cap" | tr -d '[:space:]')
while IFS= read -r _cap; do # if [[ "$_cap" =~ ^([0-9]+)\.([0-9]+)$ ]]; then
_cap=$(echo "$_cap" | tr -d '[:space:]') # _arch="${BASH_REMATCH[1]}${BASH_REMATCH[2]}"
if [[ "$_cap" =~ ^([0-9]+)\.([0-9]+)$ ]]; then # # Append if not already present
_arch="${BASH_REMATCH[1]}${BASH_REMATCH[2]}" # case ";$CUDA_ARCHS;" in
# Append if not already present # *";$_arch;"*) ;;
case ";$CUDA_ARCHS;" in # *) CUDA_ARCHS="${CUDA_ARCHS:+$CUDA_ARCHS;}$_arch" ;;
*";$_arch;"*) ;; # esac
*) CUDA_ARCHS="${CUDA_ARCHS:+$CUDA_ARCHS;}$_arch" ;; # fi
esac # done <<< "$_raw_caps"
fi # fi
done <<< "$_raw_caps" #
fi # if [ -n "$CUDA_ARCHS" ]; then
# echo " GPU compute capabilities: ${CUDA_ARCHS//;/, } -- limiting build to detected archs"
if [ -n "$CUDA_ARCHS" ]; then # CMAKE_ARGS="$CMAKE_ARGS -DCMAKE_CUDA_ARCHITECTURES=${CUDA_ARCHS}"
echo " GPU compute capabilities: ${CUDA_ARCHS//;/, } -- limiting build to detected archs" # else
CMAKE_ARGS="$CMAKE_ARGS -DCMAKE_CUDA_ARCHITECTURES=${CUDA_ARCHS}" # echo " Could not detect GPU arch -- building for all default CUDA architectures (slower)"
else # fi
echo " Could not detect GPU arch -- building for all default CUDA architectures (slower)" #
fi # # Multi-threaded nvcc compilation (uses all CPU cores per .cu file)
# CMAKE_ARGS="$CMAKE_ARGS -DCMAKE_CUDA_FLAGS=--threads=0"
# Multi-threaded nvcc compilation (uses all CPU cores per .cu file) # elif [ -d /usr/local/cuda ] || nvidia-smi &>/dev/null; then
CMAKE_ARGS="$CMAKE_ARGS -DCMAKE_CUDA_FLAGS=--threads=0" # echo " CUDA driver detected but nvcc not found — building CPU-only"
elif [ -d /usr/local/cuda ] || nvidia-smi &>/dev/null; then # echo " To enable GPU: install cuda-toolkit or add nvcc to PATH"
echo " CUDA driver detected but nvcc not found — building CPU-only" # else
echo " To enable GPU: install cuda-toolkit or add nvcc to PATH" # echo " Building CPU-only (no CUDA detected)..."
else # fi
echo " Building CPU-only (no CUDA detected)..." #
fi # NCPU=$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4)
#
NCPU=$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4) # # Use Ninja if available (faster parallel builds than Make)
# CMAKE_GENERATOR_ARGS=""
# Use Ninja if available (faster parallel builds than Make) # if command -v ninja &>/dev/null; then
CMAKE_GENERATOR_ARGS="" # CMAKE_GENERATOR_ARGS="-G Ninja"
if command -v ninja &>/dev/null; then # fi
CMAKE_GENERATOR_ARGS="-G Ninja" #
fi # run_quiet "cmake llama.cpp" cmake $CMAKE_GENERATOR_ARGS -S "$LLAMA_CPP_DIR" -B "$LLAMA_CPP_DIR/build" $CMAKE_ARGS || BUILD_OK=false
# fi
run_quiet "cmake llama.cpp" cmake $CMAKE_GENERATOR_ARGS -S "$LLAMA_CPP_DIR" -B "$LLAMA_CPP_DIR/build" $CMAKE_ARGS || BUILD_OK=false #
fi # if [ "$BUILD_OK" = true ]; then
# run_quiet "build llama-server" cmake --build "$LLAMA_CPP_DIR/build" --config Release --target llama-server -j"$NCPU" || BUILD_OK=false
if [ "$BUILD_OK" = true ]; then # fi
run_quiet "build llama-server" cmake --build "$LLAMA_CPP_DIR/build" --config Release --target llama-server -j"$NCPU" || BUILD_OK=false #
fi # # Also build llama-quantize (needed by unsloth-zoo's GGUF export pipeline)
# if [ "$BUILD_OK" = true ]; then
# Also build llama-quantize (needed by unsloth-zoo's GGUF export pipeline) # run_quiet "build llama-quantize" cmake --build "$LLAMA_CPP_DIR/build" --config Release --target llama-quantize -j"$NCPU" || true
if [ "$BUILD_OK" = true ]; then # # Symlink to llama.cpp root — check_llama_cpp() looks for the binary there
run_quiet "build llama-quantize" cmake --build "$LLAMA_CPP_DIR/build" --config Release --target llama-quantize -j"$NCPU" || true # QUANTIZE_BIN="$LLAMA_CPP_DIR/build/bin/llama-quantize"
# Symlink to llama.cpp root — check_llama_cpp() looks for the binary there # if [ -f "$QUANTIZE_BIN" ]; then
QUANTIZE_BIN="$LLAMA_CPP_DIR/build/bin/llama-quantize" # ln -sf build/bin/llama-quantize "$LLAMA_CPP_DIR/llama-quantize"
if [ -f "$QUANTIZE_BIN" ]; then # fi
ln -sf build/bin/llama-quantize "$LLAMA_CPP_DIR/llama-quantize" # fi
fi #
fi # if [ "$BUILD_OK" = true ]; then
# if [ -f "$LLAMA_SERVER_BIN" ]; then
if [ "$BUILD_OK" = true ]; then # echo "✅ llama-server built at $LLAMA_SERVER_BIN"
if [ -f "$LLAMA_SERVER_BIN" ]; then # else
echo "✅ llama-server built at $LLAMA_SERVER_BIN" # echo "⚠️ llama-server binary not found after build — GGUF inference won't be available"
else # fi
echo "⚠️ llama-server binary not found after build — GGUF inference won't be available" # if [ -f "$LLAMA_CPP_DIR/llama-quantize" ]; then
fi # echo "✅ llama-quantize available for GGUF export"
if [ -f "$LLAMA_CPP_DIR/llama-quantize" ]; then # fi
echo "✅ llama-quantize available for GGUF export" # else
fi # echo "⚠️ llama-server build failed — GGUF inference won't be available, but everything else works"
else # fi
echo "⚠️ llama-server build failed — GGUF inference won't be available, but everything else works" # fi
fi # }
fi
}
fi # end _SKIP_GGUF_BUILD check
echo "" echo ""
if [ "$IS_COLAB" = true ]; then if [ "$IS_COLAB" = true ]; then
@ -514,9 +413,6 @@ if [ "$IS_COLAB" = true ]; then
echo "╠══════════════════════════════════════╣" echo "╠══════════════════════════════════════╣"
echo "║ Unsloth Studio is ready to start ║" echo "║ Unsloth Studio is ready to start ║"
echo "║ in your Colab notebook! ║" echo "║ in your Colab notebook! ║"
echo "║ ║"
echo "║ from colab import start ║"
echo "║ start() ║"
echo "╚══════════════════════════════════════╝" echo "╚══════════════════════════════════════╝"
else else
echo "╔══════════════════════════════════════╗" echo "╔══════════════════════════════════════╗"
@ -524,6 +420,6 @@ else
echo "╠══════════════════════════════════════╣" echo "╠══════════════════════════════════════╣"
echo "║ Launch with: ║" echo "║ Launch with: ║"
echo "║ ║" echo "║ ║"
echo "║ unsloth studio -H 0.0.0.0 -p 8888 ║" echo "║ unsloth studio -H 0.0.0.0 -p 8000 ║"
echo "╚══════════════════════════════════════╝" echo "╚══════════════════════════════════════╝"
fi fi

View file

@ -12,7 +12,18 @@ import typer
studio_app = typer.Typer(help = "Unsloth Studio commands.") studio_app = typer.Typer(help = "Unsloth Studio commands.")
STUDIO_HOME = Path.home() / ".unsloth" / "studio"
def _studio_home() -> Path:
"""Studio root: env var > config file > default."""
custom = os.environ.get("UNSLOTH_STUDIO_HOME")
if custom:
return Path(custom).expanduser().resolve()
conf = Path.home() / ".unsloth" / "studio_home"
if conf.is_file():
saved = conf.read_text().strip()
if saved:
return Path(saved).expanduser().resolve()
return Path.home() / ".unsloth" / "studio"
# __file__ is unsloth_cli/commands/studio.py -- two parents up is the package root # __file__ is unsloth_cli/commands/studio.py -- two parents up is the package root
# (either site-packages or the repo root for editable installs). # (either site-packages or the repo root for editable installs).
@ -22,9 +33,9 @@ _PACKAGE_ROOT = Path(__file__).resolve().parent.parent.parent
def _studio_venv_python() -> Optional[Path]: def _studio_venv_python() -> Optional[Path]:
"""Return the studio venv Python binary, or None if not set up.""" """Return the studio venv Python binary, or None if not set up."""
if platform.system() == "Windows": if platform.system() == "Windows":
p = STUDIO_HOME / ".venv" / "Scripts" / "python.exe" p = _studio_home() / ".venv" / "Scripts" / "python.exe"
else: else:
p = STUDIO_HOME / ".venv" / "bin" / "python" p = _studio_home() / ".venv" / "bin" / "python"
return p if p.is_file() else None return p if p.is_file() else None
@ -44,7 +55,7 @@ def _find_run_py() -> Optional[Path]:
"lib/python*/site-packages/studio/backend/run.py", "lib/python*/site-packages/studio/backend/run.py",
"Lib/site-packages/studio/backend/run.py", "Lib/site-packages/studio/backend/run.py",
): ):
for match in (STUDIO_HOME / ".venv").glob(pattern): for match in (_studio_home() / ".venv").glob(pattern):
return match return match
return None return None
@ -64,7 +75,7 @@ def _find_setup_script() -> Optional[Path]:
f"lib/python*/site-packages/studio/{name}", f"lib/python*/site-packages/studio/{name}",
f"Lib/site-packages/studio/{name}", f"Lib/site-packages/studio/{name}",
): ):
for match in (STUDIO_HOME / ".venv").glob(pattern): for match in (_studio_home() / ".venv").glob(pattern):
return match return match
return None return None
@ -85,7 +96,7 @@ def studio_default(
return return
# Always use the studio venv if it exists and we're not already in it # Always use the studio venv if it exists and we're not already in it
studio_venv_dir = STUDIO_HOME / ".venv" studio_venv_dir = _studio_home() / ".venv"
in_studio_venv = sys.prefix.startswith(str(studio_venv_dir)) in_studio_venv = sys.prefix.startswith(str(studio_venv_dir))
if not in_studio_venv: if not in_studio_venv: