unsloth/setup.ps1
Roland Tannous 783f0caf5f add setup.bat
2026-03-01 13:05:10 +00:00

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