diff --git a/install_python_stack.py b/install_python_stack.py new file mode 100644 index 0000000000..e31e96232e --- /dev/null +++ b/install_python_stack.py @@ -0,0 +1,182 @@ +#!/usr/bin/env python3 +"""Cross-platform Python dependency installer for Unsloth Studio. + +Called by both setup.sh (Linux / WSL) and setup.ps1 (Windows) after the +virtual environment is already activated. Expects `pip` and `python` on +PATH to point at the venv. +""" + +from __future__ import annotations + +import os +import subprocess +import sys +import urllib.request +from pathlib import Path + +# ── Paths ────────────────────────────────────────────────────────────── +SCRIPT_DIR = Path(__file__).resolve().parent +REQ_ROOT = SCRIPT_DIR / "studio" / "backend" / "requirements" +SINGLE_ENV = REQ_ROOT / "single-env" +CONSTRAINTS = SINGLE_ENV / "constraints.txt" + +# ── Helpers ──────────────────────────────────────────────────────────── + +def _green(msg: str) -> str: + return f"\033[92m{msg}\033[0m" + +def _cyan(msg: str) -> str: + return f"\033[96m{msg}\033[0m" + +def _red(msg: str) -> str: + return f"\033[91m{msg}\033[0m" + + +def run(label: str, cmd: list[str], *, quiet: bool = True) -> None: + """Run a command; on failure print output and exit.""" + print(_cyan(f" {label}...")) + result = subprocess.run( + cmd, + stdout=subprocess.PIPE if quiet else None, + stderr=subprocess.STDOUT if quiet else None, + ) + if result.returncode != 0: + print(_red(f"❌ {label} failed (exit code {result.returncode}):")) + if result.stdout: + print(result.stdout.decode(errors="replace")) + sys.exit(result.returncode) + + +def pip_install( + label: str, + *args: str, + req: Path | None = None, + constrain: bool = True, +) -> None: + """Build and run a pip install command.""" + cmd = [sys.executable, "-m", "pip", "install"] + cmd.extend(args) + if constrain and CONSTRAINTS.is_file(): + cmd.extend(["-c", str(CONSTRAINTS)]) + if req is not None: + cmd.extend(["-r", str(req)]) + run(label, cmd) + + +def download_file(url: str, dest: Path) -> None: + """Download a file using urllib (no curl dependency).""" + urllib.request.urlretrieve(url, dest) + + +def patch_package_file(package_name: str, relative_path: str, url: str) -> None: + """Download a file from url and overwrite a file inside an installed package.""" + result = subprocess.run( + [sys.executable, "-m", "pip", "show", package_name], + capture_output=True, text=True, + ) + if result.returncode != 0: + print(_red(f" ⚠️ Could not find package {package_name}, skipping patch")) + return + + location = None + for line in result.stdout.splitlines(): + if line.lower().startswith("location:"): + location = line.split(":", 1)[1].strip() + break + + if not location: + print(_red(f" ⚠️ Could not determine location of {package_name}")) + return + + dest = Path(location) / relative_path + print(_cyan(f" Patching {dest.name} in {package_name}...")) + download_file(url, dest) + + +# ── Main install sequence ───────────────────────────────────────────── + +def install_python_stack() -> int: + print(_cyan("── Installing Python stack ──")) + + # 1. Upgrade pip + run("Upgrading pip", [sys.executable, "-m", "pip", "install", "--upgrade", "pip"]) + + # 2. Core packages: unsloth-zoo + unsloth + pip_install( + "Installing unsloth-zoo + unsloth", + "--no-cache-dir", + req=REQ_ROOT / "base.txt", + ) + + # 3. Extra dependencies + pip_install( + "Installing additional unsloth dependencies", + "--no-cache-dir", + req=REQ_ROOT / "extras.txt", + ) + + # 4. Overrides (torchao, transformers) — force-reinstall + pip_install( + "Installing torchao + transformers overrides", + "--force-reinstall", "--no-cache-dir", + req=REQ_ROOT / "overrides.txt", + ) + + # 5. Triton kernels (no-deps, from source) + pip_install( + "Installing triton kernels", + "--no-deps", "--no-cache-dir", + req=REQ_ROOT / "triton-kernels.txt", + constrain=False, + ) + + # 6. Patch: override llama_cpp.py with fix from unsloth-zoo main branch + patch_package_file( + "unsloth-zoo", + os.path.join("unsloth_zoo", "llama_cpp.py"), + "https://raw.githubusercontent.com/unslothai/unsloth-zoo/refs/heads/main/unsloth_zoo/llama_cpp.py", + ) + + # 7. Patch: override vision.py with fix from unsloth PR #4091 + patch_package_file( + "unsloth", + os.path.join("unsloth", "models", "vision.py"), + "https://raw.githubusercontent.com/unslothai/unsloth/80e0108a684c882965a02a8ed851e3473c1145ab/unsloth/models/vision.py", + ) + + # 8. Studio dependencies + pip_install( + "Installing studio dependencies", + "--no-cache-dir", + req=REQ_ROOT / "studio.txt", + ) + + # 9. Data-designer dependencies + pip_install( + "Installing data-designer dependencies", + "--no-cache-dir", + req=SINGLE_ENV / "data-designer-deps.txt", + ) + + # 10. Data-designer packages (no-deps to avoid conflicts) + pip_install( + "Installing data-designer", + "--no-cache-dir", "--no-deps", + req=SINGLE_ENV / "data-designer.txt", + ) + + # 11. Patch metadata for single-env compatibility + run( + "Patching single-env metadata", + [sys.executable, str(SINGLE_ENV / "patch_metadata.py")], + ) + + # 12. Final check + run("Running pip check", [sys.executable, "-m", "pip", "check"], quiet=False) + + print(_green("✅ Python dependencies installed")) + return 0 + + +if __name__ == "__main__": + sys.exit(install_python_stack()) diff --git a/setup.ps1 b/setup.ps1 new file mode 100644 index 0000000000..357c9b0d98 --- /dev/null +++ b/setup.ps1 @@ -0,0 +1,660 @@ +#Requires -Version 5.1 +<# +.SYNOPSIS + Full environment setup for Unsloth Studio on Windows (bundled version). +.DESCRIPTION + Always installs Node.js if needed. When running from pip install: + skips frontend build (already bundled). When running from git repo: + full setup including frontend build. + Requires an NVIDIA GPU -- CPU-only machines are not supported. +.NOTES + Usage: powershell -ExecutionPolicy Bypass -File setup.ps1 +#> + +$ErrorActionPreference = "Stop" +$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$PackageDir = Split-Path -Parent $ScriptDir + +# Detect if running from pip install (no frontend/ dir two levels up) +$FrontendDir = Join-Path $ScriptDir "..\..\frontend" +$IsPipInstall = -not (Test-Path $FrontendDir) + +# ───────────────────────────────────────────── +# Helper functions +# ───────────────────────────────────────────── + +# Reload ALL environment variables from registry. +# Picks up changes made by installers (winget, msi, etc.) including +# Path, CUDA_PATH, CUDA_PATH_V*, and any other vars they set. +function Refresh-Environment { + foreach ($level in @('Machine', 'User')) { + $vars = [System.Environment]::GetEnvironmentVariables($level) + foreach ($key in $vars.Keys) { + if ($key -eq 'Path') { continue } + Set-Item -Path "Env:$key" -Value $vars[$key] -ErrorAction SilentlyContinue + } + } + $machinePath = [System.Environment]::GetEnvironmentVariable('Path', 'Machine') + $userPath = [System.Environment]::GetEnvironmentVariable('Path', 'User') + $env:Path = "$machinePath;$userPath" +} + +# Find nvcc on PATH, CUDA_PATH, or standard toolkit dirs. +# Returns the path to nvcc.exe, or $null if not found. +function Find-Nvcc { + # 1. Check nvcc on PATH + $cmd = Get-Command nvcc -ErrorAction SilentlyContinue + if ($cmd) { return $cmd.Source } + + # 2. Check CUDA_PATH env var + $cudaRoot = [Environment]::GetEnvironmentVariable('CUDA_PATH', 'Process') + if (-not $cudaRoot) { $cudaRoot = [Environment]::GetEnvironmentVariable('CUDA_PATH', 'Machine') } + if (-not $cudaRoot) { $cudaRoot = [Environment]::GetEnvironmentVariable('CUDA_PATH', 'User') } + if ($cudaRoot -and (Test-Path (Join-Path $cudaRoot 'bin\nvcc.exe'))) { + return (Join-Path $cudaRoot 'bin\nvcc.exe') + } + + # 3. Scan standard toolkit directory + $toolkitBase = 'C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA' + if (Test-Path $toolkitBase) { + $latest = Get-ChildItem -Directory $toolkitBase | Sort-Object Name | Select-Object -Last 1 + if ($latest -and (Test-Path (Join-Path $latest.FullName 'bin\nvcc.exe'))) { + return (Join-Path $latest.FullName 'bin\nvcc.exe') + } + } + + return $null +} + +# Detect CUDA Compute Capability via nvidia-smi. +# Returns e.g. "80" for A100 (8.0), "89" for RTX 4090 (8.9), etc. +# Returns $null if detection fails. +function Get-CudaComputeCapability { + $nvSmi = Get-Command nvidia-smi -ErrorAction SilentlyContinue + if (-not $nvSmi) { return $null } + + try { + $raw = & nvidia-smi --query-gpu=compute_cap --format=csv,noheader 2>$null + if ($LASTEXITCODE -ne 0 -or -not $raw) { return $null } + + # nvidia-smi may return multiple GPUs; take the first one + $cap = ($raw -split "`n")[0].Trim() + if ($cap -match '^(\d+)\.(\d+)$') { + $major = $Matches[1] + $minor = $Matches[2] + return "$major$minor" + } + } catch { } + + return $null +} + +# Detect driver's max CUDA version from nvidia-smi and return the highest +# compatible PyTorch CUDA index tag (e.g. "cu128"). +# PyTorch on Windows ships CPU-only by default from PyPI; CUDA wheels live at +# https://download.pytorch.org/whl/. The tag must not exceed the driver's +# capability: e.g. driver "CUDA Version: 12.9" → cu128 (not cu130). +function Get-PytorchCudaTag { + $nvSmi = Get-Command nvidia-smi -ErrorAction SilentlyContinue + if (-not $nvSmi) { return "cu124" } + + try { + # 2>&1 | Out-String merges stderr into stdout then converts to a single + # string. Plain 2>$null doesn't fully suppress stderr in PS 5.1 — + # ErrorRecord objects leak into $output and break the -match. + $output = & nvidia-smi 2>&1 | Out-String + if ($output -match 'CUDA Version:\s+(\d+)\.(\d+)') { + $major = [int]$Matches[1] + $minor = [int]$Matches[2] + # PyTorch 2.10 offers: cu124, cu126, cu128, cu130 + if ($major -ge 13) { return "cu130" } + if ($major -eq 12 -and $minor -ge 8) { return "cu128" } + if ($major -eq 12 -and $minor -ge 6) { return "cu126" } + return "cu124" + } + } catch { } + + return "cu124" +} + +# Find Visual Studio Build Tools for cmake -G flag. +# Strategy: (1) vswhere, (2) scan filesystem (handles broken vswhere registration). +# Returns @{ Generator = "Visual Studio 17 2022"; InstallPath = "C:\..."; Source = "..." } or $null. +function Find-VsBuildTools { + $map = @{ '2022' = '17'; '2019' = '16'; '2017' = '15' } + + # --- Try vswhere first (works when VS is properly registered) --- + $vsw = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" + if (Test-Path $vsw) { + $info = & $vsw -latest -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property catalog_productLineVersion 2>$null + $path = & $vsw -latest -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath 2>$null + if ($info -and $path) { + $y = $info.Trim() + $n = $map[$y] + if ($n) { + return @{ Generator = "Visual Studio $n $y"; InstallPath = $path.Trim(); Source = 'vswhere' } + } + } + } + + # --- Scan filesystem (handles broken vswhere registration after winget cycles) --- + $roots = @($env:ProgramFiles, ${env:ProgramFiles(x86)}) + $editions = @('BuildTools', 'Community', 'Professional', 'Enterprise') + $years = @('2022', '2019', '2017') + + foreach ($y in $years) { + foreach ($r in $roots) { + foreach ($ed in $editions) { + $candidate = Join-Path $r "Microsoft Visual Studio\$y\$ed" + if (Test-Path $candidate) { + $vcDir = Join-Path $candidate "VC\Tools\MSVC" + if (Test-Path $vcDir) { + $cl = Get-ChildItem -Path $vcDir -Filter "cl.exe" -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($cl) { + $n = $map[$y] + if ($n) { + return @{ Generator = "Visual Studio $n $y"; InstallPath = $candidate; Source = "filesystem ($ed)"; ClExe = $cl.FullName } + } + } + } + } + } + } + } + + return $null +} + +# ───────────────────────────────────────────── +# Banner +# ───────────────────────────────────────────── +Write-Host "+==============================================+" -ForegroundColor Green +Write-Host "| Unsloth Studio Setup (Windows) |" -ForegroundColor Green +Write-Host "+==============================================+" -ForegroundColor Green + +# ========================================================================== +# PHASE 1: System-level prerequisites (winget installs, env vars) +# All heavy system tool installs happen here BEFORE touching Python. +# ========================================================================== + +# ============================================ +# 1a. GPU requirement check +# ============================================ +$HasNvidiaSmi = $null -ne (Get-Command nvidia-smi -ErrorAction SilentlyContinue) +if (-not $HasNvidiaSmi) { + Write-Host "" + Write-Host "[ERROR] Unsloth Studio requires an NVIDIA GPU." -ForegroundColor Red + Write-Host " CPU-only machines are not supported." -ForegroundColor Red + Write-Host "" + Write-Host " If you have an NVIDIA GPU, ensure the driver is installed:" -ForegroundColor Yellow + Write-Host " https://www.nvidia.com/Download/index.aspx" -ForegroundColor Yellow + exit 1 +} +Write-Host "[OK] NVIDIA GPU detected" -ForegroundColor Green + +# ============================================ +# 1b. Git (required by pip for git+https:// deps and by npm) +# ============================================ +$HasGit = $null -ne (Get-Command git -ErrorAction SilentlyContinue) +if (-not $HasGit) { + Write-Host "Git not found -- installing via winget..." -ForegroundColor Yellow + $HasWinget = $null -ne (Get-Command winget -ErrorAction SilentlyContinue) + if ($HasWinget) { + try { + winget install Git.Git --source winget --accept-package-agreements --accept-source-agreements 2>&1 | Out-Null + Refresh-Environment + $HasGit = $null -ne (Get-Command git -ErrorAction SilentlyContinue) + } catch { } + } + if (-not $HasGit) { + Write-Host "[ERROR] Git is required but could not be installed automatically." -ForegroundColor Red + Write-Host " Install Git from https://git-scm.com/download/win and re-run." -ForegroundColor Red + exit 1 + } + Write-Host "[OK] Git installed: $(git --version)" -ForegroundColor Green +} else { + Write-Host "[OK] Git found: $(git --version)" -ForegroundColor Green +} + +# ============================================ +# 1c. CMake (required for llama.cpp build) +# ============================================ +$HasCmake = $null -ne (Get-Command cmake -ErrorAction SilentlyContinue) +if (-not $HasCmake) { + Write-Host "CMake not found -- installing via winget..." -ForegroundColor Yellow + $HasWinget = $null -ne (Get-Command winget -ErrorAction SilentlyContinue) + if ($HasWinget) { + try { + winget install Kitware.CMake --source winget --accept-package-agreements --accept-source-agreements 2>&1 | Out-Null + Refresh-Environment + $HasCmake = $null -ne (Get-Command cmake -ErrorAction SilentlyContinue) + } catch { } + } + if ($HasCmake) { + Write-Host "[OK] CMake installed" -ForegroundColor Green + } else { + Write-Host "[ERROR] CMake is required but could not be installed." -ForegroundColor Red + Write-Host " Install CMake from https://cmake.org/download/ and re-run." -ForegroundColor Red + exit 1 + } +} else { + Write-Host "[OK] CMake found: $(cmake --version | Select-Object -First 1)" -ForegroundColor Green +} + +# ============================================ +# 1d. Visual Studio Build Tools (C++ compiler for llama.cpp) +# ============================================ +$CmakeGenerator = $null +$VsInstallPath = $null +$vsResult = Find-VsBuildTools + +if (-not $vsResult) { + Write-Host "Visual Studio Build Tools not found -- installing via winget..." -ForegroundColor Yellow + Write-Host " (This is a one-time install, may take several minutes)" -ForegroundColor Gray + $HasWinget = $null -ne (Get-Command winget -ErrorAction SilentlyContinue) + if ($HasWinget) { + $prevEAPTemp = $ErrorActionPreference + $ErrorActionPreference = "Continue" + winget install Microsoft.VisualStudio.2022.BuildTools --source winget --accept-package-agreements --accept-source-agreements --override "--add Microsoft.VisualStudio.Workload.VCTools --includeRecommended --passive --wait" + $ErrorActionPreference = $prevEAPTemp + # Re-scan after install (don't trust vswhere catalog) + $vsResult = Find-VsBuildTools + } +} + +if ($vsResult) { + $CmakeGenerator = $vsResult.Generator + $VsInstallPath = $vsResult.InstallPath + Write-Host "[OK] $CmakeGenerator detected via $($vsResult.Source)" -ForegroundColor Green + if ($vsResult.ClExe) { Write-Host " cl.exe: $($vsResult.ClExe)" -ForegroundColor Gray } +} else { + Write-Host "[ERROR] Visual Studio Build Tools could not be found or installed." -ForegroundColor Red + Write-Host " Manual install:" -ForegroundColor Red + Write-Host ' 1. winget install Microsoft.VisualStudio.2022.BuildTools --source winget' -ForegroundColor Yellow + Write-Host ' 2. Open Visual Studio Installer -> Modify -> check "Desktop development with C++"' -ForegroundColor Yellow + exit 1 +} + +# ============================================ +# 1e. CUDA Toolkit (nvcc for llama.cpp build + env vars) +# ============================================ +$NvccPath = Find-Nvcc + +if (-not $NvccPath) { + Write-Host "CUDA driver detected but toolkit (nvcc) not found -- installing via winget..." -ForegroundColor Yellow + $HasWinget = $null -ne (Get-Command winget -ErrorAction SilentlyContinue) + if ($HasWinget) { + Write-Host " Installing CUDA Toolkit via winget..." -ForegroundColor Cyan + winget install --id=Nvidia.CUDA -e --source winget --accept-package-agreements --accept-source-agreements + Refresh-Environment + $NvccPath = Find-Nvcc + if ($NvccPath) { + Write-Host " [OK] CUDA Toolkit installed (nvcc: $NvccPath)" -ForegroundColor Green + } + } +} + +if (-not $NvccPath) { + Write-Host "[ERROR] CUDA Toolkit (nvcc) is required but could not be found or installed." -ForegroundColor Red + Write-Host " Install CUDA Toolkit from https://developer.nvidia.com/cuda-downloads" -ForegroundColor Yellow + exit 1 +} + +# -- Set CUDA env vars so cmake AND MSBuild can find the toolkit -- +$CudaToolkitRoot = Split-Path (Split-Path $NvccPath -Parent) -Parent +# CUDA_PATH: used by cmake's find_package(CUDAToolkit) +[Environment]::SetEnvironmentVariable('CUDA_PATH', $CudaToolkitRoot, 'Process') +# CudaToolkitDir: the MSBuild property that CUDA .targets checks directly +# Trailing backslash required -- the .targets file appends subpaths to it +[Environment]::SetEnvironmentVariable('CudaToolkitDir', "$CudaToolkitRoot\", 'Process') +# Persist CUDA_PATH to User registry if not already set +$existingSys = [Environment]::GetEnvironmentVariable('CUDA_PATH', 'Machine') +$existingUsr = [Environment]::GetEnvironmentVariable('CUDA_PATH', 'User') +if (-not $existingSys -and -not $existingUsr) { + [Environment]::SetEnvironmentVariable('CUDA_PATH', $CudaToolkitRoot, 'User') + Write-Host " Persisted CUDA_PATH to user environment" -ForegroundColor Gray +} +# Ensure nvcc's bin dir is on PATH for this process +$nvccBinDir = Split-Path $NvccPath -Parent +if ($env:PATH -notlike "*$nvccBinDir*") { + [Environment]::SetEnvironmentVariable('PATH', "$nvccBinDir;$env:PATH", 'Process') +} +# Persist nvcc bin dir to User PATH so it works in new terminals +$userPath = [Environment]::GetEnvironmentVariable('Path', 'User') +if (-not $userPath -or $userPath -notlike "*$nvccBinDir*") { + if ($userPath) { + [Environment]::SetEnvironmentVariable('Path', "$nvccBinDir;$userPath", 'User') + } else { + [Environment]::SetEnvironmentVariable('Path', "$nvccBinDir", 'User') + } + Write-Host " Persisted CUDA bin dir to user PATH" -ForegroundColor Gray +} + +Write-Host "[OK] CUDA Toolkit: $NvccPath" -ForegroundColor Green +Write-Host " CUDA_PATH = $CudaToolkitRoot" -ForegroundColor Gray +Write-Host " CudaToolkitDir = $CudaToolkitRoot\" -ForegroundColor Gray + +# Detect compute capability (used later for llama.cpp cmake) +$CudaArch = Get-CudaComputeCapability +if ($CudaArch) { + Write-Host " Compute Capability = $($CudaArch.Insert($CudaArch.Length-1, '.')) (sm_$CudaArch)" -ForegroundColor Gray +} else { + Write-Host " [WARN] Could not detect compute capability -- cmake will use defaults" -ForegroundColor Yellow +} + +# ============================================ +# 1f. Node.js / npm (always -- needed regardless of install method) +# ============================================ +$NeedNode = $true +try { + $NodeVersion = (node -v 2>$null) + $NpmVersion = (npm -v 2>$null) + if ($NodeVersion -and $NpmVersion) { + $NodeMajor = [int]($NodeVersion -replace 'v','').Split('.')[0] + $NpmMajor = [int]$NpmVersion.Split('.')[0] + + if ($NodeMajor -ge 20 -and $NpmMajor -ge 11) { + Write-Host "[OK] Node $NodeVersion and npm $NpmVersion already meet requirements." -ForegroundColor Green + $NeedNode = $false + } else { + Write-Host "[WARN] Node $NodeVersion / npm $NpmVersion too old." -ForegroundColor Yellow + } + } +} catch { + Write-Host "[WARN] Node/npm not found." -ForegroundColor Yellow +} + +if ($NeedNode) { + Write-Host "Installing Node.js via winget..." -ForegroundColor Cyan + try { + winget install OpenJS.NodeJS.LTS --source winget --accept-package-agreements --accept-source-agreements + Refresh-Environment + } catch { + Write-Host "[ERROR] Could not install Node.js automatically." -ForegroundColor Red + Write-Host "Please install Node.js >= 20 from https://nodejs.org/" -ForegroundColor Red + exit 1 + } +} + +Write-Host "[OK] Node $(node -v) | npm $(npm -v)" -ForegroundColor Green + +Write-Host "" +Write-Host "--- System prerequisites ready ---" -ForegroundColor Green +Write-Host "" + +# ========================================================================== +# PHASE 2: Frontend build (skip if pip-installed -- already bundled) +# ========================================================================== +if ($IsPipInstall) { + Write-Host "[OK] Running from pip install - frontend already bundled, skipping build" -ForegroundColor Green +} else { + $RepoRoot = (Resolve-Path (Join-Path $ScriptDir "..\..")).Path + + Write-Host "" + Write-Host "Building frontend..." -ForegroundColor Cyan + Push-Location (Join-Path $RepoRoot "frontend") + npm install 2>&1 | Out-Null + npm run build 2>&1 | Out-Null + Pop-Location + + $PackageBuildDir = Join-Path $PackageDir "studio\frontend\build" + if (Test-Path $PackageBuildDir) { Remove-Item -Recurse -Force $PackageBuildDir } + Copy-Item -Recurse (Join-Path $RepoRoot "frontend\build") $PackageBuildDir + + Write-Host "[OK] Frontend built" -ForegroundColor Green +} + +# ========================================================================== +# PHASE 3: Python environment + dependencies +# ========================================================================== +Write-Host "" +Write-Host "Setting up Python environment..." -ForegroundColor Cyan + +# Find Python +$PythonCmd = $null +foreach ($candidate in @("python3.12", "python3.11", "python3.10", "python3.9", "python3", "python")) { + try { + $ver = & $candidate --version 2>&1 + if ($ver -match 'Python 3\.(\d+)') { + $minor = [int]$Matches[1] + if ($minor -le 12) { + $PythonCmd = $candidate + break + } + } + } catch { } +} + +if (-not $PythonCmd) { + Write-Host "[ERROR] No Python <= 3.12 found." -ForegroundColor Red + exit 1 +} + +Write-Host "[OK] Using $PythonCmd ($(& $PythonCmd --version 2>&1))" -ForegroundColor Green + +# Always create a .venv for isolation -- even for pip installs. +# Created in the current working directory (where user ran the command). +$VenvDir = Join-Path (Get-Location) ".venv" +if (-not (Test-Path $VenvDir)) { + Write-Host " Creating virtual environment at $VenvDir..." -ForegroundColor Cyan + & $PythonCmd -m venv $VenvDir +} else { + Write-Host " Reusing existing virtual environment at $VenvDir" -ForegroundColor Green +} + +# pip and python write to stderr even on success (progress bars, warnings). +# With $ErrorActionPreference = "Stop" (set at top of script), PS 5.1 +# converts stderr lines into terminating ErrorRecords, breaking output. +# Lower to "Continue" for the pip/python section. +$prevEAP = $ErrorActionPreference +$ErrorActionPreference = "Continue" + +$ActivateScript = Join-Path $VenvDir "Scripts\Activate.ps1" +. $ActivateScript +pip install --upgrade pip 2>&1 | Out-Null + +# if (-not $IsPipInstall) { +# # Running from repo: copy requirements and do editable install +# $RepoRoot = (Resolve-Path (Join-Path $ScriptDir "..\..")).Path +# $ReqsSrc = Join-Path $RepoRoot "backend\requirements" +# $ReqsDst = Join-Path $PackageDir "requirements" +# if (-not (Test-Path $ReqsDst)) { New-Item -ItemType Directory -Path $ReqsDst | Out-Null } +# Copy-Item (Join-Path $ReqsSrc "*.txt") $ReqsDst -Force + +# Write-Host " Installing CLI entry point..." -ForegroundColor Cyan +# pip install -e $RepoRoot 2>&1 | Out-Null +# } else { +# # Running from pip install: the package is in system Python but not in +# # the fresh .venv. Install it so run_install() can find its modules +# # and bundled requirements files. +# Write-Host " Installing package into venv..." -ForegroundColor Cyan +# pip install unsloth-roland-test 2>&1 | Out-Null +# } + +# Pre-install PyTorch with CUDA support. +# On Windows, the default PyPI torch wheel is CPU-only. +# We need PyTorch's CUDA index to get GPU-enabled wheels. +# PyTorch bundles its own CUDA runtime, so this works regardless +# of whether the CUDA Toolkit is installed yet. +# The CUDA tag is chosen based on the driver's max supported CUDA version. +$CuTag = Get-PytorchCudaTag +Write-Host " Installing PyTorch with CUDA support ($CuTag)..." -ForegroundColor Cyan +pip install torch torchvision torchaudio --index-url "https://download.pytorch.org/whl/$CuTag" + +# Ordered heavy dependency installation — shared cross-platform script +Write-Host " Running ordered dependency installation..." -ForegroundColor Cyan +python "$PSScriptRoot\install_python_stack.py" +# Restore ErrorActionPreference after pip/python work +$ErrorActionPreference = $prevEAP + +# ========================================================================== +# PHASE 4: Build llama.cpp with CUDA for GGUF inference + export +# ========================================================================== +# Builds at ~/.unsloth/llama.cpp/ (persistent across pip upgrades). +# We build: +# - llama-server: for GGUF model inference +# - llama-quantize: for GGUF export quantization +# Prerequisites (git, cmake, VS Build Tools, CUDA Toolkit) already installed in Phase 1. +$LlamaCppDir = Join-Path $env:USERPROFILE ".unsloth\llama.cpp" +$BuildDir = Join-Path $LlamaCppDir "build" +$LlamaServerBin = Join-Path $BuildDir "bin\Release\llama-server.exe" + +if (Test-Path $LlamaServerBin) { + Write-Host "" + Write-Host "[OK] llama-server already exists at $LlamaServerBin" -ForegroundColor Green +} else { + Write-Host "" + Write-Host "Building llama.cpp with CUDA support..." -ForegroundColor Cyan + Write-Host " This typically takes 5-10 minutes on first build." -ForegroundColor Gray + Write-Host "" + + # Start total build timer + $totalSw = [System.Diagnostics.Stopwatch]::StartNew() + + # Native commands (git, cmake) write to stderr even on success. + # With $ErrorActionPreference = "Stop" (set at top of script), PS 5.1 + # converts stderr lines into terminating ErrorRecords, breaking output. + # Lower to "Continue" for the build section. + $prevEAP = $ErrorActionPreference + $ErrorActionPreference = "Continue" + + $BuildOk = $true + $FailedStep = "" + + # -- Step A: Clone or pull llama.cpp -- + $UnslothDir = Join-Path $env:USERPROFILE ".unsloth" + if (-not (Test-Path $UnslothDir)) { New-Item -ItemType Directory -Path $UnslothDir -Force | Out-Null } + + if (Test-Path (Join-Path $LlamaCppDir ".git")) { + Write-Host " llama.cpp repo already cloned, pulling latest..." -ForegroundColor Gray + git -C $LlamaCppDir pull + if ($LASTEXITCODE -ne 0) { + Write-Host " [WARN] git pull failed -- using existing source" -ForegroundColor Yellow + } + } else { + Write-Host " Cloning llama.cpp..." -ForegroundColor Gray + if (Test-Path $LlamaCppDir) { Remove-Item -Recurse -Force $LlamaCppDir } + git clone --depth 1 https://github.com/ggml-org/llama.cpp.git $LlamaCppDir + if ($LASTEXITCODE -ne 0) { + $BuildOk = $false + $FailedStep = "git clone" + } + } + + # -- Step B: cmake configure (CUDA + Unsloth flags) -- + if ($BuildOk) { + Write-Host "" + Write-Host "--- cmake configure ---" -ForegroundColor Cyan + + $CmakeArgs = @( + '-S', $LlamaCppDir, + '-B', $BuildDir, + '-G', $CmakeGenerator, + '-Wno-dev' + ) + # Tell cmake exactly where VS is (bypasses registry lookup) + if ($VsInstallPath) { + $CmakeArgs += "-DCMAKE_GENERATOR_INSTANCE=$VsInstallPath" + } + # Common flags + $CmakeArgs += '-DBUILD_SHARED_LIBS=OFF' + $CmakeArgs += '-DLLAMA_CURL=OFF' + $CmakeArgs += '-DCMAKE_POLICY_DEFAULT_CMP0194=NEW' + $CmakeArgs += '-DCMAKE_EXE_LINKER_FLAGS=/NODEFAULTLIB:LIBCMT' + # CUDA flags (Unsloth-aligned) + $CmakeArgs += '-DGGML_CUDA=ON' + $CmakeArgs += "-DCUDAToolkit_ROOT=$CudaToolkitRoot" + $CmakeArgs += "-DCMAKE_CUDA_COMPILER=$NvccPath" + $CmakeArgs += '-DGGML_CUDA_FA_ALL_QUANTS=ON' + $CmakeArgs += '-DGGML_CUDA_F16=OFF' + $CmakeArgs += '-DGGML_CUDA_GRAPHS=OFF' + $CmakeArgs += '-DGGML_CUDA_FORCE_CUBLAS=OFF' + $CmakeArgs += '-DGGML_CUDA_PEER_MAX_BATCH_SIZE=8192' + if ($CudaArch) { + $CmakeArgs += "-DCMAKE_CUDA_ARCHITECTURES=$CudaArch" + } + + Write-Host " cmake args:" -ForegroundColor Gray + foreach ($arg in $CmakeArgs) { + Write-Host " $arg" -ForegroundColor Gray + } + Write-Host "" + + cmake @CmakeArgs + if ($LASTEXITCODE -ne 0) { + $BuildOk = $false + $FailedStep = "cmake configure" + } + } + + # -- Step C: Build llama-server -- + $NumCpu = [Environment]::ProcessorCount + if ($NumCpu -lt 1) { $NumCpu = 4 } + + if ($BuildOk) { + Write-Host "" + Write-Host "--- cmake build (llama-server) ---" -ForegroundColor Cyan + Write-Host " Parallel jobs: $NumCpu" -ForegroundColor Gray + Write-Host "" + + cmake --build $BuildDir --config Release --target llama-server -j $NumCpu + if ($LASTEXITCODE -ne 0) { + $BuildOk = $false + $FailedStep = "cmake build (llama-server)" + } + } + + # -- Step D: Build llama-quantize (optional, best-effort) -- + if ($BuildOk) { + Write-Host "" + Write-Host "--- cmake build (llama-quantize) ---" -ForegroundColor Cyan + cmake --build $BuildDir --config Release --target llama-quantize -j $NumCpu + if ($LASTEXITCODE -ne 0) { + Write-Host " [WARN] llama-quantize build failed (GGUF export may be unavailable)" -ForegroundColor Yellow + } + } + + # Restore ErrorActionPreference + $ErrorActionPreference = $prevEAP + + # Stop timer + $totalSw.Stop() + $totalMin = [math]::Floor($totalSw.Elapsed.TotalMinutes) + $totalSec = [math]::Round($totalSw.Elapsed.TotalSeconds % 60, 1) + + # -- Summary -- + Write-Host "" + if ($BuildOk -and (Test-Path $LlamaServerBin)) { + Write-Host "[OK] llama-server built at $LlamaServerBin" -ForegroundColor Green + $QuantizeBin = Join-Path $BuildDir "bin\Release\llama-quantize.exe" + if (Test-Path $QuantizeBin) { + Write-Host "[OK] llama-quantize available for GGUF export" -ForegroundColor Green + } + Write-Host " Build time: ${totalMin}m ${totalSec}s" -ForegroundColor Cyan + } else { + # Check alternate paths (some cmake generators don't use Release subdir) + $altBin = Join-Path $BuildDir "bin\llama-server.exe" + if ($BuildOk -and (Test-Path $altBin)) { + Write-Host "[OK] llama-server built at $altBin" -ForegroundColor Green + Write-Host " Build time: ${totalMin}m ${totalSec}s" -ForegroundColor Cyan + } else { + Write-Host "[FAILED] llama.cpp build failed at step: $FailedStep (${totalMin}m ${totalSec}s)" -ForegroundColor Red + Write-Host " To retry: delete $LlamaCppDir and re-run setup." -ForegroundColor Yellow + exit 1 + } + } +} + +# ============================================ +# Done +# ============================================ +Write-Host "" +Write-Host "+==============================================+" -ForegroundColor Green +Write-Host "| Setup Complete! |" -ForegroundColor Green +Write-Host "| |" -ForegroundColor Green +Write-Host "| Activate venv: |" -ForegroundColor Green +Write-Host "| cmd: .venv\Scripts\activate.bat |" -ForegroundColor Green +Write-Host "| PS: .\.venv\Scripts\Activate.ps1 |" -ForegroundColor Green +Write-Host "| |" -ForegroundColor Green +Write-Host "| Then run: unsloth-roland-test studio |" -ForegroundColor Green +Write-Host "+==============================================+" -ForegroundColor Green \ No newline at end of file diff --git a/setup.sh b/setup.sh index 8314ef6d75..409df0b798 100755 --- a/setup.sh +++ b/setup.sh @@ -170,30 +170,7 @@ SINGLE_ENV_DATA_DESIGNER_DEPS="$REQ_ROOT/single-env/data-designer-deps.txt" SINGLE_ENV_PATCH="$REQ_ROOT/single-env/patch_metadata.py" install_python_stack() { - run_quiet "pip upgrade" pip install --upgrade pip - echo " Installing unsloth-zoo + unsloth..." - run_quiet "pip install unsloth" pip install --no-cache-dir -c "$SINGLE_ENV_CONSTRAINTS" -r "$REQ_ROOT/base.txt" - echo " Installing additional unsloth dependencies..." - run_quiet "pip install extras" pip install --no-cache-dir -c "$SINGLE_ENV_CONSTRAINTS" -r "$REQ_ROOT/extras.txt" - run_quiet "pip install torchao+transformers" pip install --force-reinstall --no-cache-dir -c "$SINGLE_ENV_CONSTRAINTS" -r "$REQ_ROOT/overrides.txt" - run_quiet "pip install triton_kernels" pip install --no-deps --no-cache-dir -r "$REQ_ROOT/triton-kernels.txt" - # Patch: override llama_cpp.py with fix from unsloth-zoo branch - LLAMA_CPP_DST="$(pip show unsloth-zoo | grep -i '^Location:' | awk '{print $2}')/unsloth_zoo/llama_cpp.py" - curl -sSL "https://raw.githubusercontent.com/unslothai/unsloth-zoo/refs/heads/main/unsloth_zoo/llama_cpp.py" \ - -o "$LLAMA_CPP_DST" - # Patch: override vision.py with fix from unsloth PR: https://github.com/unslothai/unsloth/pull/4091 until next pypi release - VISION_DST="$(pip show unsloth | grep -i '^Location:' | awk '{print $2}')/unsloth/models/vision.py" - curl -sSL "https://raw.githubusercontent.com/unslothai/unsloth/80e0108a684c882965a02a8ed851e3473c1145ab/unsloth/models/vision.py" \ - -o "$VISION_DST" - echo " Installing studio dependencies..." - run_quiet "pip install studio" pip install --no-cache-dir -c "$SINGLE_ENV_CONSTRAINTS" -r "$REQ_ROOT/studio.txt" - echo " Installing data-designer dependencies..." - run_quiet "pip install data-designer deps" pip install --no-cache-dir -c "$SINGLE_ENV_CONSTRAINTS" -r "$SINGLE_ENV_DATA_DESIGNER_DEPS" - echo " Installing data-designer..." - run_quiet "pip install data-designer" pip install --no-cache-dir --no-deps -c "$SINGLE_ENV_CONSTRAINTS" -r "$SINGLE_ENV_DATA_DESIGNER" - run_quiet "patch single-env metadata" python "$SINGLE_ENV_PATCH" - run_quiet "pip check" pip check - echo "✅ Python dependencies installed" + python "$SCRIPT_DIR/install_python_stack.py" } if [ "$IS_COLAB" = true ]; then