705 lines
No EOL
30 KiB
PowerShell
705 lines
No EOL
30 KiB
PowerShell
#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/<tag>. 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" 2>&1 | Out-Null
|
|
|
|
# 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
|
|
}
|
|
}
|
|
}
|
|
|
|
# ============================================
|
|
# Add shell aliases (PowerShell profile + cmd batch files)
|
|
# ============================================
|
|
Write-Host ""
|
|
$RepoDir = $PSScriptRoot
|
|
$VenvPython = Join-Path $RepoDir ".venv\Scripts\python.exe"
|
|
$CliScript = Join-Path $RepoDir "cli.py"
|
|
$FrontendDist = Join-Path $RepoDir "studio\frontend\dist"
|
|
$AliasAdded = $false
|
|
|
|
# --- PowerShell profile: add functions ---
|
|
$ProfileDir = Split-Path $PROFILE -Parent
|
|
if (-not (Test-Path $ProfileDir)) { New-Item -ItemType Directory -Path $ProfileDir -Force | Out-Null }
|
|
if (-not (Test-Path $PROFILE)) { New-Item -ItemType File -Path $PROFILE -Force | Out-Null }
|
|
|
|
if (-not (Select-String -Path $PROFILE -Pattern "unsloth-studio" -Quiet -ErrorAction SilentlyContinue)) {
|
|
$block = @"
|
|
|
|
# Unsloth Studio launcher
|
|
function unsloth-studio { & "$VenvPython" "$CliScript" studio -f "$FrontendDist" @args }
|
|
function unsloth-ui { & "$VenvPython" "$CliScript" studio -f "$FrontendDist" @args }
|
|
"@
|
|
Add-Content -Path $PROFILE -Value $block
|
|
Write-Host "[OK] Aliases 'unsloth-studio' and 'unsloth-ui' added to $PROFILE" -ForegroundColor Green
|
|
$AliasAdded = $true
|
|
} else {
|
|
Write-Host "[OK] Aliases 'unsloth-studio' and 'unsloth-ui' already exist in $PROFILE" -ForegroundColor Green
|
|
}
|
|
|
|
# --- cmd.exe: create batch files on PATH so they work from regular terminal ---
|
|
$BatDir = Join-Path $RepoDir ".venv\Scripts"
|
|
foreach ($name in @("unsloth-studio", "unsloth-ui")) {
|
|
$batPath = Join-Path $BatDir "$name.bat"
|
|
if (-not (Test-Path $batPath)) {
|
|
Set-Content -Path $batPath -Value "@echo off`r`n`"$VenvPython`" `"$CliScript`" studio -f `"$FrontendDist`" %*"
|
|
}
|
|
}
|
|
Write-Host "[OK] Batch launchers created in $BatDir (works from cmd.exe when venv is on PATH)" -ForegroundColor Green
|
|
|
|
# ============================================
|
|
# Done
|
|
# ============================================
|
|
Write-Host ""
|
|
Write-Host "+==============================================+" -ForegroundColor Green
|
|
Write-Host "| Setup Complete! |" -ForegroundColor Green
|
|
Write-Host "| |" -ForegroundColor Green
|
|
if ($AliasAdded) {
|
|
Write-Host "| PowerShell: run '. `$PROFILE' |" -ForegroundColor Green
|
|
Write-Host "| or open a new terminal, then: |" -ForegroundColor Green
|
|
} else {
|
|
Write-Host "| Launch with: |" -ForegroundColor Green
|
|
}
|
|
Write-Host "| |" -ForegroundColor Green
|
|
Write-Host "| unsloth-studio -H 0.0.0.0 -p 8000 |" -ForegroundColor Green
|
|
Write-Host "| |" -ForegroundColor Green
|
|
Write-Host "| cmd.exe: .venv\Scripts\activate.bat |" -ForegroundColor Green
|
|
Write-Host "| unsloth-studio -H 0.0.0.0 -p 8000 |" -ForegroundColor Green
|
|
Write-Host "+==============================================+" -ForegroundColor Green |