From 42f5ba5fccae179f86e2f7835955404e84d7c473 Mon Sep 17 00:00:00 2001 From: imagineer99 Date: Sun, 1 Mar 2026 00:02:15 +0000 Subject: [PATCH 01/36] fix: standardize OOM/TIGHT model status indicators across model dropdowns --- .../assistant-ui/model-selector/pickers.tsx | 6 -- .../components/steps/model-selection-step.tsx | 61 +++++++++++++++---- .../studio/sections/model-section.tsx | 24 ++++---- studio/frontend/src/lib/vram.ts | 29 +++++++++ 4 files changed, 91 insertions(+), 29 deletions(-) diff --git a/studio/frontend/src/components/assistant-ui/model-selector/pickers.tsx b/studio/frontend/src/components/assistant-ui/model-selector/pickers.tsx index 8a52747409..3d91cffb8c 100644 --- a/studio/frontend/src/components/assistant-ui/model-selector/pickers.tsx +++ b/studio/frontend/src/components/assistant-ui/model-selector/pickers.tsx @@ -103,9 +103,6 @@ function ModelRow({ {vramStatus === "tight" && ( TIGHT )} - {vramStatus === "fits" && ( - FIT - )} {meta ? ( {meta} ) : null} @@ -253,9 +250,6 @@ function GgufVariantExpander({ {fitStatus === "tight" && ( TIGHT )} - {fitStatus === "fits" && ( - FIT - )} {formatBytes(v.size_bytes)} diff --git a/studio/frontend/src/features/onboarding/components/steps/model-selection-step.tsx b/studio/frontend/src/features/onboarding/components/steps/model-selection-step.tsx index 5cf4ebc0b5..484a11ea70 100644 --- a/studio/frontend/src/features/onboarding/components/steps/model-selection-step.tsx +++ b/studio/frontend/src/features/onboarding/components/steps/model-selection-step.tsx @@ -33,11 +33,17 @@ import { import { MODEL_TYPE_TO_HF_TASK } from "@/config/training"; import { useDebouncedValue, + useGpuInfo, useHfModelSearch, useHfTokenValidation, useInfiniteScroll, } from "@/hooks"; import { formatCompact } from "@/lib/utils"; +import { + type TrainingMethod as VramTrainingMethod, + type VramFitStatus, + buildModelVramMap, +} from "@/lib/vram"; import { useTrainingConfigStore } from "@/features/training"; import type { TrainingMethod } from "@/types/training"; import { @@ -50,6 +56,7 @@ import { useEffect, useMemo, useRef, useState } from "react"; import { useShallow } from "zustand/react/shallow"; export function ModelSelectionStep() { + const gpu = useGpuInfo(); const { modelType, selectedModel, @@ -93,6 +100,24 @@ export function ModelSelectionStep() { const resultIds = useMemo(() => hfResults.map((r) => r.id), [hfResults]); + // Match Studio behavior: only show exception signals (OOM/TIGHT) in training flows. + const vramMap = useMemo(() => { + const fitMap = buildModelVramMap( + hfResults, + trainingMethod as VramTrainingMethod, + gpu, + ); + const map = new Map(); + for (const r of hfResults) { + const fit = fitMap.get(r.id); + map.set(r.id, { + status: fit?.status ?? null, + detail: r.totalParams ? formatCompact(r.totalParams) : null, + }); + } + return map; + }, [hfResults, gpu, trainingMethod]); + const comboboxAnchorRef = useRef(null); const { scrollRef, sentinelRef } = useInfiniteScroll( fetchMore, @@ -218,19 +243,21 @@ export function ModelSelectionStep() { > {(id: string) => { - const r = hfResults.find((r) => r.id === id); - const sizeLabel = r?.totalParams - ? formatCompact(r.totalParams) - : null; + const entry = vramMap.get(id); + const sizeLabel = entry?.detail ?? null; + const fitStatus = entry?.status ?? null; + const exceeds = fitStatus === "exceeds"; return ( - + {id} @@ -241,11 +268,23 @@ export function ModelSelectionStep() { {id} - {sizeLabel ? ( - - {sizeLabel} - - ) : null} + + {fitStatus === "exceeds" && ( + + OOM + + )} + {fitStatus === "tight" && ( + + TIGHT + + )} + {sizeLabel ? ( + + {sizeLabel} + + ) : null} + ); }} diff --git a/studio/frontend/src/features/studio/sections/model-section.tsx b/studio/frontend/src/features/studio/sections/model-section.tsx index 7a15cea02d..b2e595a4c3 100644 --- a/studio/frontend/src/features/studio/sections/model-section.tsx +++ b/studio/frontend/src/features/studio/sections/model-section.tsx @@ -37,8 +37,7 @@ import { formatCompact } from "@/lib/utils"; import { type TrainingMethod as VramTrainingMethod, type VramFitStatus, - checkVramFit, - estimateLoadingVram, + buildModelVramMap, } from "@/lib/vram"; import { listLocalModels, @@ -218,22 +217,23 @@ export function ModelSection() { // Keyed by model id so the render callback is a simple O(1) lookup. // Re-computes when the training method changes (QLoRA=4-bit vs LoRA/Full=fp16). const vramMap = useMemo(() => { - const method = trainingMethod as VramTrainingMethod; + const fitMap = buildModelVramMap( + hfResults, + trainingMethod as VramTrainingMethod, + gpu, + ); const map = new Map< string, { est: number; status: VramFitStatus | null; detail: string | null } >(); for (const r of hfResults) { const detail = r.totalParams ? formatCompact(r.totalParams) : null; - if (r.totalParams) { - const est = estimateLoadingVram(r.totalParams, method); - const status = gpu.available - ? checkVramFit(est, gpu.memoryTotalGb) - : null; - map.set(r.id, { est, status, detail }); - } else { - map.set(r.id, { est: 0, status: null, detail }); - } + const fit = fitMap.get(r.id); + map.set(r.id, { + est: fit?.est ?? 0, + status: fit?.status ?? null, + detail, + }); } return map; }, [hfResults, gpu, trainingMethod]); diff --git a/studio/frontend/src/lib/vram.ts b/studio/frontend/src/lib/vram.ts index 7aea44e850..8152104ee2 100644 --- a/studio/frontend/src/lib/vram.ts +++ b/studio/frontend/src/lib/vram.ts @@ -93,3 +93,32 @@ export function checkVramFit( if (ratio <= 1.0) return "tight"; return "exceeds"; } + +export interface ModelVramMapInput { + id: string; + totalParams?: number; +} + +export interface ModelVramMapEntry { + est: number; + status: VramFitStatus | null; +} + +export function buildModelVramMap( + models: ModelVramMapInput[], + method: TrainingMethod, + gpu: { available: boolean; memoryTotalGb: number }, +): Map { + const map = new Map(); + for (const model of models) { + if (!model.totalParams) { + map.set(model.id, { est: 0, status: null }); + continue; + } + + const est = estimateLoadingVram(model.totalParams, method); + const status = gpu.available ? checkVramFit(est, gpu.memoryTotalGb) : null; + map.set(model.id, { est, status }); + } + return map; +} From 471fc8fd90998baa8a8c7ce911800fd21f819eb5 Mon Sep 17 00:00:00 2001 From: imagineer99 Date: Sun, 1 Mar 2026 03:02:49 +0000 Subject: [PATCH 02/36] fix: prevent navbar tab shift when navigating across pages --- studio/frontend/src/components/navbar.tsx | 65 ++++++++++++----------- studio/frontend/src/index.css | 1 + 2 files changed, 34 insertions(+), 32 deletions(-) diff --git a/studio/frontend/src/components/navbar.tsx b/studio/frontend/src/components/navbar.tsx index 5f1aed832d..4a75223dc4 100644 --- a/studio/frontend/src/components/navbar.tsx +++ b/studio/frontend/src/components/navbar.tsx @@ -24,7 +24,7 @@ import { import { HugeiconsIcon } from "@hugeicons/react"; import { useTrainingRuntimeStore } from "@/features/training"; import { Link, useRouterState } from "@tanstack/react-router"; -import { AnimatePresence, motion } from "motion/react"; +import { motion } from "motion/react"; import { useState } from "react"; import { TOUR_OPEN_EVENT } from "@/features/tour"; @@ -58,9 +58,9 @@ export function Navbar() { return (
-
+
{/* Left: logo */} - + Unsloth )} - - {active && item.icon && ( - - - - )} - + + + + + {item.label} @@ -142,7 +140,7 @@ export function Navbar() { {/* Right: docs/tour desktop */} -
+
- {tourId ? ( - - ) : null} +
{/* Right: mobile */} diff --git a/studio/frontend/src/index.css b/studio/frontend/src/index.css index 4ed13bdb57..2ef1ed7d96 100644 --- a/studio/frontend/src/index.css +++ b/studio/frontend/src/index.css @@ -268,6 +268,7 @@ } html { @apply font-sans; + scrollbar-gutter: stable; } h1, h2, From ff93c970248431597a1986ff4cacb9c5cd0350bc Mon Sep 17 00:00:00 2001 From: Roland Tannous Date: Sun, 1 Mar 2026 12:55:53 +0000 Subject: [PATCH 03/36] fix: support mmproj for local vision GGUF models + fix Windows pipe deadlock --- studio/backend/core/inference/llama_cpp.py | 48 +++++++++++++- studio/backend/routes/inference.py | 1 + studio/backend/utils/models/model_config.py | 72 +++++++++++++++++++-- 3 files changed, 115 insertions(+), 6 deletions(-) diff --git a/studio/backend/core/inference/llama_cpp.py b/studio/backend/core/inference/llama_cpp.py index 68bf871590..b7b87e9cfb 100644 --- a/studio/backend/core/inference/llama_cpp.py +++ b/studio/backend/core/inference/llama_cpp.py @@ -41,6 +41,8 @@ class LlamaCppBackend: self._is_vision: bool = False self._healthy = False self._lock = threading.Lock() + self._stdout_lines: list[str] = [] + self._stdout_thread: Optional[threading.Thread] = None atexit.register(self._cleanup) @@ -115,6 +117,26 @@ class LlamaCppBackend: s.bind(("", 0)) return s.getsockname()[1] + # ── Stdout drain (prevents pipe deadlock on Windows) ───────── + + def _drain_stdout(self): + """ + Read lines from the subprocess stdout in a background thread. + + This prevents a pipe-buffer deadlock on Windows where the default + pipe buffer is only ~4 KB. Without draining, llama-server blocks + on writes and never becomes healthy. + """ + try: + for line in self._process.stdout: + line = line.rstrip() + if line: + self._stdout_lines.append(line) + logger.info(f"[llama-server] {line}") + except (ValueError, OSError): + # Pipe closed — process is terminating + pass + # ── Lifecycle ───────────────────────────────────────────────── def load_model( @@ -122,6 +144,8 @@ class LlamaCppBackend: *, # Local mode: pass a path to a .gguf file gguf_path: Optional[str] = None, + # Vision projection (mmproj) for local vision models + mmproj_path: Optional[str] = None, # HF mode: let llama-server download via -hf "repo:quant" hf_repo: Optional[str] = None, hf_variant: Optional[str] = None, @@ -186,6 +210,14 @@ class LlamaCppBackend: if n_threads is not None: cmd.extend(["--threads", str(n_threads)]) + # Append mmproj for local vision models + if mmproj_path: + if not Path(mmproj_path).is_file(): + logger.warning(f"mmproj file not found: {mmproj_path}") + else: + cmd.extend(["--mmproj", mmproj_path]) + logger.info(f"Using mmproj for vision: {mmproj_path}") + logger.info(f"Starting llama-server: {' '.join(cmd)}") # Set LD_LIBRARY_PATH so llama-server can find its shared libs @@ -196,6 +228,7 @@ class LlamaCppBackend: existing_ld = env.get("LD_LIBRARY_PATH", "") env["LD_LIBRARY_PATH"] = f"{binary_dir}:{existing_ld}" if existing_ld else binary_dir + self._stdout_lines = [] self._process = subprocess.Popen( cmd, stdout=subprocess.PIPE, @@ -204,6 +237,12 @@ class LlamaCppBackend: env=env, ) + # Start background thread to drain stdout and prevent pipe deadlock + self._stdout_thread = threading.Thread( + target=self._drain_stdout, daemon=True, name="llama-stdout" + ) + self._stdout_thread.start() + self._gguf_path = gguf_path self._hf_repo = hf_repo self._hf_variant = hf_variant @@ -256,6 +295,9 @@ class LlamaCppBackend: logger.warning(f"Error killing llama-server process: {e}") finally: self._process = None + if self._stdout_thread is not None: + self._stdout_thread.join(timeout=2) + self._stdout_thread = None def _cleanup(self): """atexit handler to ensure llama-server is terminated.""" @@ -273,8 +315,10 @@ class LlamaCppBackend: while time.monotonic() < deadline: # Check if process crashed if self._process.poll() is not None: - # Read remaining output for error info - output = self._process.stdout.read() if self._process.stdout else "" + # Give the drain thread a moment to collect final output + if self._stdout_thread is not None: + self._stdout_thread.join(timeout=2) + output = "\n".join(self._stdout_lines[-50:]) logger.error( f"llama-server exited with code {self._process.returncode}. " f"Output: {output[:2000]}" diff --git a/studio/backend/routes/inference.py b/studio/backend/routes/inference.py index 8d1c6667c0..512eb05526 100644 --- a/studio/backend/routes/inference.py +++ b/studio/backend/routes/inference.py @@ -125,6 +125,7 @@ async def load_model( # Local mode: llama-server loads via -m success = llama_backend.load_model( gguf_path=config.gguf_file, + mmproj_path=config.gguf_mmproj_file, model_identifier=config.identifier, is_vision=config.is_vision, n_ctx=request.max_seq_length, diff --git a/studio/backend/utils/models/model_config.py b/studio/backend/utils/models/model_config.py index b84a14d226..5404a198aa 100644 --- a/studio/backend/utils/models/model_config.py +++ b/studio/backend/utils/models/model_config.py @@ -422,6 +422,32 @@ def is_vision_model(model_name: str, hf_token: Optional[str] = None) -> bool: pass +def _is_mmproj(filename: str) -> bool: + """Check if a GGUF filename is a vision projection (mmproj) file.""" + return "mmproj" in filename.lower() + + +def detect_mmproj_file(path: str) -> Optional[str]: + """ + Find the mmproj (vision projection) GGUF file in a directory. + + Args: + path: Directory to search — or a .gguf file (uses its parent dir). + + Returns: + Full path to the mmproj .gguf file, or None if not found. + """ + p = Path(path) + search_dir = p.parent if p.is_file() else p + if not search_dir.is_dir(): + return None + + for f in search_dir.glob("*.gguf"): + if _is_mmproj(f.name): + return str(f.resolve()) + return None + + def detect_gguf_model(path: str) -> Optional[str]: """ Check if the given local path is or contains a GGUF model file. @@ -430,6 +456,9 @@ def detect_gguf_model(path: str) -> Optional[str]: 1. path is a direct .gguf file path 2. path is a directory containing .gguf files + Skips mmproj (vision projection) files — those must be passed via + ``--mmproj``, not ``-m``. Use :func:`detect_mmproj_file` instead. + Returns the full path to the .gguf file if found, None otherwise. For HuggingFace repo detection, use detect_gguf_model_remote() instead. """ @@ -437,11 +466,16 @@ def detect_gguf_model(path: str) -> Optional[str]: # Case 1: direct .gguf file if p.suffix == ".gguf" and p.is_file(): + if _is_mmproj(p.name): + return None return str(p.resolve()) - # Case 2: directory containing .gguf files + # Case 2: directory containing .gguf files (skip mmproj) if p.is_dir(): - gguf_files = sorted(p.glob("*.gguf"), key=lambda f: f.stat().st_size, reverse=True) + gguf_files = sorted( + (f for f in p.glob("*.gguf") if not _is_mmproj(f.name)), + key=lambda f: f.stat().st_size, reverse=True, + ) if gguf_files: return str(gguf_files[0].resolve()) @@ -677,7 +711,8 @@ def scan_exported_models(exports_dir: str = "./exports") -> List[Tuple[str, str, continue # Check for flat GGUF export (e.g. exports/gemma-3-4b-it-finetune-gguf/) - gguf_files = list(run_dir.glob("*.gguf")) + # Filter out mmproj (vision projection) files — they aren't loadable as main models + gguf_files = [f for f in run_dir.glob("*.gguf") if not _is_mmproj(f.name)] if gguf_files: base_model = None export_meta = run_dir / "export_metadata.json" @@ -907,6 +942,7 @@ class ModelConfig: is_lora: bool # Is this a lora adapter? is_gguf: bool = False # Is this a GGUF model? gguf_file: Optional[str] = None # Full path to the .gguf file (local mode) + gguf_mmproj_file: Optional[str] = None # Full path to the mmproj .gguf file (vision projection) gguf_hf_repo: Optional[str] = None # HF repo ID for -hf mode (e.g. "unsloth/gemma-3-4b-it-GGUF") gguf_variant: Optional[str] = None # Quantization variant (e.g. "Q4_K_M") base_model: Optional[str] = None # Base model (for LoRAs) @@ -1005,16 +1041,44 @@ class ModelConfig: if gguf_file: display_name = Path(gguf_file).stem logger.info(f"Detected local GGUF model: {gguf_file}") + + # Detect vision: check if base model is vision, then look for mmproj + mmproj_file = None + gguf_is_vision = False + gguf_dir = Path(gguf_file).parent + + # Determine if this is a vision model from export metadata + base_is_vision = False + meta_path = gguf_dir / "export_metadata.json" + if meta_path.exists(): + try: + meta = json.loads(meta_path.read_text()) + base = meta.get("base_model") + if base and is_vision_model(base, hf_token=hf_token): + base_is_vision = True + logger.info(f"GGUF base model '{base}' is a vision model") + except Exception as e: + logger.debug(f"Could not read export metadata: {e}") + + # If vision (or mmproj happens to exist), find the mmproj file + mmproj_file = detect_mmproj_file(gguf_file) + if mmproj_file: + gguf_is_vision = True + logger.info(f"Detected mmproj for vision: {mmproj_file}") + elif base_is_vision: + logger.warning(f"Base model is vision but no mmproj file found in {gguf_dir}") + return cls( identifier=identifier, display_name=display_name, path=path, is_local=True, is_cached=True, - is_vision=False, + is_vision=gguf_is_vision, is_lora=False, is_gguf=True, gguf_file=gguf_file, + gguf_mmproj_file=mmproj_file, ) else: # Check if the HF repo contains GGUF files From 28bac1859a133535239979f6e6b2cbffecb81563 Mon Sep 17 00:00:00 2001 From: Roland Tannous Date: Fri, 27 Feb 2026 19:45:32 +0000 Subject: [PATCH 04/36] Extract shared install_python_stack.py for cross-platform setup --- install_python_stack.py | 182 +++++++++++ setup.ps1 | 660 ++++++++++++++++++++++++++++++++++++++++ setup.sh | 25 +- 3 files changed, 843 insertions(+), 24 deletions(-) create mode 100644 install_python_stack.py create mode 100644 setup.ps1 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 From 783f0caf5fe511a8f6c48189ae21cd47f577eeaf Mon Sep 17 00:00:00 2001 From: Roland Tannous Date: Fri, 27 Feb 2026 20:31:57 +0000 Subject: [PATCH 05/36] add setup.bat --- install_python_stack.py | 72 +++++++++++++++++++++++++++++++++++++---- setup.bat | 2 ++ setup.ps1 | 55 ++++++++++++++++++++++++++++--- 3 files changed, 117 insertions(+), 12 deletions(-) create mode 100644 setup.bat diff --git a/install_python_stack.py b/install_python_stack.py index e31e96232e..b9526af71b 100644 --- a/install_python_stack.py +++ b/install_python_stack.py @@ -11,25 +11,53 @@ from __future__ import annotations import os import subprocess import sys +import tempfile import urllib.request from pathlib import Path +IS_WINDOWS = sys.platform == "win32" + # ── 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 ──────────────────────────────────────────────────────────── +# ── Color support ────────────────────────────────────────────────────── + +def _enable_colors() -> bool: + """Try to enable ANSI color support. Returns True if available.""" + if not hasattr(sys.stdout, "fileno"): + return False + try: + if not os.isatty(sys.stdout.fileno()): + return False + except Exception: + return False + if IS_WINDOWS: + try: + import ctypes + kernel32 = ctypes.windll.kernel32 + # Enable ENABLE_VIRTUAL_TERMINAL_PROCESSING (0x0004) on stdout + handle = kernel32.GetStdHandle(-11) # STD_OUTPUT_HANDLE + mode = ctypes.c_ulong() + kernel32.GetConsoleMode(handle, ctypes.byref(mode)) + kernel32.SetConsoleMode(handle, mode.value | 0x0004) + return True + except Exception: + return False + return True # Unix terminals support ANSI by default + +_HAS_COLOR = _enable_colors() def _green(msg: str) -> str: - return f"\033[92m{msg}\033[0m" + return f"\033[92m{msg}\033[0m" if _HAS_COLOR else msg def _cyan(msg: str) -> str: - return f"\033[96m{msg}\033[0m" + return f"\033[96m{msg}\033[0m" if _HAS_COLOR else msg def _red(msg: str) -> str: - return f"\033[91m{msg}\033[0m" + return f"\033[91m{msg}\033[0m" if _HAS_COLOR else msg def run(label: str, cmd: list[str], *, quiet: bool = True) -> None: @@ -47,6 +75,25 @@ def run(label: str, cmd: list[str], *, quiet: bool = True) -> None: sys.exit(result.returncode) +# Packages to skip on Windows (require special build steps) +WINDOWS_SKIP_PACKAGES = {"open_spiel"} + + +def _filter_requirements(req: Path, skip: set[str]) -> Path: + """Return a temp copy of a requirements file with certain packages removed.""" + lines = req.read_text(encoding="utf-8").splitlines(keepends=True) + filtered = [ + line for line in lines + if not any(line.strip().lower().startswith(pkg) for pkg in skip) + ] + tmp = tempfile.NamedTemporaryFile( + mode="w", suffix=".txt", delete=False, encoding="utf-8", + ) + tmp.writelines(filtered) + tmp.close() + return Path(tmp.name) + + def pip_install( label: str, *args: str, @@ -58,9 +105,20 @@ def 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) + actual_req = req + if req is not None and IS_WINDOWS and WINDOWS_SKIP_PACKAGES: + actual_req = _filter_requirements(req, WINDOWS_SKIP_PACKAGES) + if actual_req is not None: + cmd.extend(["-r", str(actual_req)]) + try: + run(label, cmd) + finally: + # Clean up temp file if we created one + if actual_req is not None and actual_req != req: + actual_req.unlink(missing_ok=True) + if req is not None and actual_req != req: + skipped = WINDOWS_SKIP_PACKAGES + print(_cyan(f" (Skipped on Windows: {', '.join(skipped)})")) def download_file(url: str, dest: Path) -> None: diff --git a/setup.bat b/setup.bat new file mode 100644 index 0000000000..ef16abd263 --- /dev/null +++ b/setup.bat @@ -0,0 +1,2 @@ +@echo off +powershell -ExecutionPolicy Bypass -File "%~dp0setup.ps1" %* diff --git a/setup.ps1 b/setup.ps1 index 357c9b0d98..9be3f2efb0 100644 --- a/setup.ps1 +++ b/setup.ps1 @@ -479,7 +479,7 @@ pip install --upgrade pip 2>&1 | Out-Null # 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" +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 @@ -645,6 +645,45 @@ if (Test-Path $LlamaServerBin) { } } +# ============================================ +# 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 # ============================================ @@ -652,9 +691,15 @@ 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 +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 "| Then run: unsloth-roland-test studio |" -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 \ No newline at end of file From 662a1eb9d530ead51010a050f9d3b5f72f153eea Mon Sep 17 00:00:00 2001 From: Roland Tannous Date: Fri, 27 Feb 2026 20:55:39 +0000 Subject: [PATCH 06/36] Fix Windows frontend build, add setup.bat, ANSI colors, aliases --- install_python_stack.py | 4 +--- setup.ps1 | 15 ++++----------- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/install_python_stack.py b/install_python_stack.py index b9526af71b..7d3ab66b20 100644 --- a/install_python_stack.py +++ b/install_python_stack.py @@ -116,9 +116,7 @@ def pip_install( # Clean up temp file if we created one if actual_req is not None and actual_req != req: actual_req.unlink(missing_ok=True) - if req is not None and actual_req != req: - skipped = WINDOWS_SKIP_PACKAGES - print(_cyan(f" (Skipped on Windows: {', '.join(skipped)})")) + def download_file(url: str, dest: Path) -> None: diff --git a/setup.ps1 b/setup.ps1 index 9be3f2efb0..c82d03615c 100644 --- a/setup.ps1 +++ b/setup.ps1 @@ -15,8 +15,8 @@ $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" +# Detect if running from pip install (no studio/frontend/ dir in repo) +$FrontendDir = Join-Path $ScriptDir "studio\frontend" $IsPipInstall = -not (Test-Path $FrontendDir) # ───────────────────────────────────────────── @@ -388,20 +388,13 @@ Write-Host "" 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") + Push-Location $FrontendDir 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 + Write-Host "[OK] Frontend built to studio/frontend/dist" -ForegroundColor Green } # ========================================================================== From 2dfe0abaa1e9b0be1400b44b5b149ed96ccd1115 Mon Sep 17 00:00:00 2001 From: Roland Tannous Date: Fri, 27 Feb 2026 20:59:10 +0000 Subject: [PATCH 07/36] Fix npm stderr crash on Windows ErrorActionPreference --- setup.ps1 | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/setup.ps1 b/setup.ps1 index c82d03615c..239238b97d 100644 --- a/setup.ps1 +++ b/setup.ps1 @@ -390,10 +390,15 @@ if ($IsPipInstall) { } else { Write-Host "" Write-Host "Building frontend..." -ForegroundColor Cyan + # npm writes warnings to stderr; lower ErrorActionPreference so PS doesn't + # treat them as terminating errors (same pattern as the pip section below). + $prevEAP_npm = $ErrorActionPreference + $ErrorActionPreference = "Continue" Push-Location $FrontendDir npm install 2>&1 | Out-Null npm run build 2>&1 | Out-Null Pop-Location + $ErrorActionPreference = $prevEAP_npm Write-Host "[OK] Frontend built to studio/frontend/dist" -ForegroundColor Green } From aba3d8e29bd8110c1c1486789b676dc3b87fa4d0 Mon Sep 17 00:00:00 2001 From: Roland Tannous Date: Fri, 27 Feb 2026 21:15:30 +0000 Subject: [PATCH 08/36] Enforce Node LTS (v20-v22), add npm error checking, clean node_modules --- setup.ps1 | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/setup.ps1 b/setup.ps1 index 239238b97d..6827b0730d 100644 --- a/setup.ps1 +++ b/setup.ps1 @@ -345,6 +345,8 @@ if ($CudaArch) { # ============================================ # 1f. Node.js / npm (always -- needed regardless of install method) # ============================================ +# setup.sh installs Node LTS (v22) via nvm. We enforce the same range here: +# Node >= 20 AND <= 22 (LTS only), npm >= 11. $NeedNode = $true try { $NodeVersion = (node -v 2>$null) @@ -353,9 +355,11 @@ try { $NodeMajor = [int]($NodeVersion -replace 'v','').Split('.')[0] $NpmMajor = [int]$NpmVersion.Split('.')[0] - if ($NodeMajor -ge 20 -and $NpmMajor -ge 11) { + if ($NodeMajor -ge 20 -and $NodeMajor -le 22 -and $NpmMajor -ge 11) { Write-Host "[OK] Node $NodeVersion and npm $NpmVersion already meet requirements." -ForegroundColor Green $NeedNode = $false + } elseif ($NodeMajor -gt 22) { + Write-Host "[WARN] Node $NodeVersion is too new (non-LTS). Installing Node LTS..." -ForegroundColor Yellow } else { Write-Host "[WARN] Node $NodeVersion / npm $NpmVersion too old." -ForegroundColor Yellow } @@ -365,13 +369,13 @@ try { } if ($NeedNode) { - Write-Host "Installing Node.js via winget..." -ForegroundColor Cyan + Write-Host "Installing Node.js LTS 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 + Write-Host "Please install Node.js LTS (v22) from https://nodejs.org/" -ForegroundColor Red exit 1 } } @@ -395,8 +399,23 @@ if ($IsPipInstall) { $prevEAP_npm = $ErrorActionPreference $ErrorActionPreference = "Continue" Push-Location $FrontendDir + # Remove stale node_modules to avoid version conflicts + if (Test-Path "node_modules") { Remove-Item -Recurse -Force "node_modules" } npm install 2>&1 | Out-Null + if ($LASTEXITCODE -ne 0) { + Pop-Location + $ErrorActionPreference = $prevEAP_npm + Write-Host "[ERROR] npm install failed (exit code $LASTEXITCODE)" -ForegroundColor Red + Write-Host " Try running 'npm install' manually in studio/frontend/ to see errors" -ForegroundColor Yellow + exit 1 + } npm run build 2>&1 | Out-Null + if ($LASTEXITCODE -ne 0) { + Pop-Location + $ErrorActionPreference = $prevEAP_npm + Write-Host "[ERROR] npm run build failed (exit code $LASTEXITCODE)" -ForegroundColor Red + exit 1 + } Pop-Location $ErrorActionPreference = $prevEAP_npm Write-Host "[OK] Frontend built to studio/frontend/dist" -ForegroundColor Green From e1cc5e61b13fa6029521e8711f5a2762ac1db2e6 Mon Sep 17 00:00:00 2001 From: Roland Tannous Date: Fri, 27 Feb 2026 21:22:38 +0000 Subject: [PATCH 09/36] Fix npm Invalid Version: delete package-lock.json, relax Node constraint --- setup.ps1 | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/setup.ps1 b/setup.ps1 index 6827b0730d..cbf636983f 100644 --- a/setup.ps1 +++ b/setup.ps1 @@ -346,7 +346,7 @@ if ($CudaArch) { # 1f. Node.js / npm (always -- needed regardless of install method) # ============================================ # setup.sh installs Node LTS (v22) via nvm. We enforce the same range here: -# Node >= 20 AND <= 22 (LTS only), npm >= 11. +# Node >= 20, npm >= 11. $NeedNode = $true try { $NodeVersion = (node -v 2>$null) @@ -355,11 +355,9 @@ try { $NodeMajor = [int]($NodeVersion -replace 'v','').Split('.')[0] $NpmMajor = [int]$NpmVersion.Split('.')[0] - if ($NodeMajor -ge 20 -and $NodeMajor -le 22 -and $NpmMajor -ge 11) { + if ($NodeMajor -ge 20 -and $NpmMajor -ge 11) { Write-Host "[OK] Node $NodeVersion and npm $NpmVersion already meet requirements." -ForegroundColor Green $NeedNode = $false - } elseif ($NodeMajor -gt 22) { - Write-Host "[WARN] Node $NodeVersion is too new (non-LTS). Installing Node LTS..." -ForegroundColor Yellow } else { Write-Host "[WARN] Node $NodeVersion / npm $NpmVersion too old." -ForegroundColor Yellow } @@ -375,7 +373,7 @@ if ($NeedNode) { Refresh-Environment } catch { Write-Host "[ERROR] Could not install Node.js automatically." -ForegroundColor Red - Write-Host "Please install Node.js LTS (v22) from https://nodejs.org/" -ForegroundColor Red + Write-Host "Please install Node.js >= 20 from https://nodejs.org/" -ForegroundColor Red exit 1 } } @@ -399,8 +397,9 @@ if ($IsPipInstall) { $prevEAP_npm = $ErrorActionPreference $ErrorActionPreference = "Continue" Push-Location $FrontendDir - # Remove stale node_modules to avoid version conflicts + # Remove stale node_modules and package-lock.json to avoid version conflicts if (Test-Path "node_modules") { Remove-Item -Recurse -Force "node_modules" } + if (Test-Path "package-lock.json") { Remove-Item -Force "package-lock.json" } npm install 2>&1 | Out-Null if ($LASTEXITCODE -ne 0) { Pop-Location From bd7c17708bf71629080f0f6b09449f36eb5271f8 Mon Sep 17 00:00:00 2001 From: Roland Tannous Date: Fri, 27 Feb 2026 21:50:54 +0000 Subject: [PATCH 10/36] Set short TORCHINDUCTOR_CACHE_DIR to fix Windows MAX_PATH crash --- setup.ps1 | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/setup.ps1 b/setup.ps1 index cbf636983f..10e7900d44 100644 --- a/setup.ps1 +++ b/setup.ps1 @@ -493,6 +493,15 @@ pip install --upgrade pip 2>&1 | Out-Null # 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. + +# Windows MAX_PATH (260 chars) causes Triton kernel compilation to fail because +# the auto-generated filenames are extremely long. Use a short cache directory. +$TorchCacheDir = "C:\tc" +if (-not (Test-Path $TorchCacheDir)) { New-Item -ItemType Directory -Path $TorchCacheDir -Force | Out-Null } +$env:TORCHINDUCTOR_CACHE_DIR = $TorchCacheDir +[Environment]::SetEnvironmentVariable('TORCHINDUCTOR_CACHE_DIR', $TorchCacheDir, 'User') +Write-Host "[OK] TORCHINDUCTOR_CACHE_DIR set to $TorchCacheDir (avoids MAX_PATH issues)" -ForegroundColor Green + $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 From 7e021886c8a43a593583d9d3cd58ba54b494f6f4 Mon Sep 17 00:00:00 2001 From: Roland Tannous Date: Fri, 27 Feb 2026 21:58:28 +0000 Subject: [PATCH 11/36] Force num_proc=1 on Windows to avoid slow spawn overhead --- studio/backend/utils/hardware/hardware.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/studio/backend/utils/hardware/hardware.py b/studio/backend/utils/hardware/hardware.py index b885e130d5..fd43e620bb 100644 --- a/studio/backend/utils/hardware/hardware.py +++ b/studio/backend/utils/hardware/hardware.py @@ -423,6 +423,11 @@ def safe_num_proc(desired: Optional[int] = None) -> int: """ Return a safe ``num_proc`` for ``dataset.map()`` calls. + On Windows, always returns 1 because Python uses ``spawn`` instead of + ``fork`` for multiprocessing — the overhead of re-importing torch, + transformers, unsloth etc. per worker is typically slower than + single-process for normal dataset sizes. + On multi-GPU machines the NVIDIA driver spawns extra background threads, making ``os.fork()`` prone to deadlocks when many workers are created. This helper caps ``num_proc`` to 4 on such machines. @@ -438,6 +443,12 @@ def safe_num_proc(desired: Optional[int] = None) -> int: A safe integer ≥ 1. """ import os + import sys + + # Windows uses 'spawn' for multiprocessing — the overhead of re-importing + # torch/transformers/unsloth per worker is typically slower than single-process. + if sys.platform == "win32": + return 1 if desired is None or not isinstance(desired, int): desired = max(1, os.cpu_count() // 3) From f036a70681c543614138c99e573085841998e37d Mon Sep 17 00:00:00 2001 From: Roland Tannous Date: Sat, 28 Feb 2026 03:07:58 +0000 Subject: [PATCH 12/36] Fix llama-server binary lookup for Windows (.exe, Release dir, ~/.unsloth) --- studio/backend/core/inference/llama_cpp.py | 29 ++++++++++++++++------ 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/studio/backend/core/inference/llama_cpp.py b/studio/backend/core/inference/llama_cpp.py index b7b87e9cfb..cf4494df30 100644 --- a/studio/backend/core/inference/llama_cpp.py +++ b/studio/backend/core/inference/llama_cpp.py @@ -78,10 +78,14 @@ class LlamaCppBackend: Search order: 1. LLAMA_SERVER_PATH environment variable 2. ./llama.cpp/build/bin/llama-server (built by setup.sh in-tree) - 3. llama-server on PATH (system install) - 4. ./bin/llama-server (legacy: extracted binary) + 3. ~/.unsloth/llama.cpp/build/bin/Release/llama-server (built by setup.ps1 on Windows) + 4. llama-server on PATH (system install) + 5. ./bin/llama-server (legacy: extracted binary) """ import os + import sys + + binary_name = "llama-server.exe" if sys.platform == "win32" else "llama-server" # 1. Env var env_path = os.environ.get("LLAMA_SERVER_PATH") @@ -91,18 +95,29 @@ class LlamaCppBackend: # Project root: llama_cpp.py → inference/ → core/ → backend/ → studio/ → root project_root = Path(__file__).resolve().parents[4] - # 2. In-tree llama.cpp build (setup.sh builds here) - build_path = project_root / "llama.cpp" / "build" / "bin" / "llama-server" + # 2. In-tree llama.cpp build (setup.sh builds here on Linux) + build_path = project_root / "llama.cpp" / "build" / "bin" / binary_name if build_path.is_file(): return str(build_path) - # 3. System PATH + # 3. Windows MSVC build (setup.ps1 builds here — Release config) + if sys.platform == "win32": + # In-tree + win_path = project_root / "llama.cpp" / "build" / "bin" / "Release" / binary_name + if win_path.is_file(): + return str(win_path) + # ~/.unsloth (setup.ps1 default location) + home_path = Path.home() / ".unsloth" / "llama.cpp" / "build" / "bin" / "Release" / binary_name + if home_path.is_file(): + return str(home_path) + + # 4. System PATH system_path = shutil.which("llama-server") if system_path: return system_path - # 4. Legacy: extracted to bin/ - bin_path = project_root / "bin" / "llama-server" + # 5. Legacy: extracted to bin/ + bin_path = project_root / "bin" / binary_name if bin_path.is_file(): return str(bin_path) From 1684e48b1eba00e706d242075f3f7c33e327e66a Mon Sep 17 00:00:00 2001 From: Roland Tannous Date: Sat, 28 Feb 2026 06:42:43 +0000 Subject: [PATCH 13/36] Add llama-cpp Windows test script, fix binary lookup paths --- test_llama_cpp.ps1 | 233 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 233 insertions(+) create mode 100644 test_llama_cpp.ps1 diff --git a/test_llama_cpp.ps1 b/test_llama_cpp.ps1 new file mode 100644 index 0000000000..5eb3b7db32 --- /dev/null +++ b/test_llama_cpp.ps1 @@ -0,0 +1,233 @@ +<# +.SYNOPSIS + Test script for llama.cpp compilation and binary validation on Windows. + Verifies that llama-server was built with CUDA support and can start. + +.USAGE + .\test_llama_cpp.ps1 + .\test_llama_cpp.ps1 -BinaryPath "C:\path\to\llama-server.exe" +#> +param( + [string]$BinaryPath = "" +) + +$ErrorActionPreference = "Continue" + +Write-Host "" +Write-Host "============================================" -ForegroundColor Cyan +Write-Host " llama.cpp Windows Build Test" -ForegroundColor Cyan +Write-Host "============================================" -ForegroundColor Cyan +Write-Host "" + +# ── Step 1: Locate the binary ────────────────────────────────────────── +Write-Host "1. Locating llama-server binary..." -ForegroundColor Yellow + +$SearchPaths = @() + +if ($BinaryPath) { + $SearchPaths += $BinaryPath +} + +# Add all known locations +$RepoRoot = $PSScriptRoot +$SearchPaths += Join-Path $RepoRoot "llama.cpp\build\bin\Release\llama-server.exe" +$SearchPaths += Join-Path $RepoRoot "llama.cpp\build\bin\llama-server.exe" +$SearchPaths += Join-Path $env:USERPROFILE ".unsloth\llama.cpp\build\bin\Release\llama-server.exe" + +# Check LLAMA_SERVER_PATH env var +$envPath = $env:LLAMA_SERVER_PATH +if ($envPath) { + $SearchPaths = @($envPath) + $SearchPaths +} + +# Also check system PATH +$systemPath = (Get-Command llama-server -ErrorAction SilentlyContinue) +if ($systemPath) { + $SearchPaths += $systemPath.Source +} + +$FoundBinary = $null +foreach ($p in $SearchPaths) { + if (Test-Path $p) { + $FoundBinary = $p + break + } +} + +if (-not $FoundBinary) { + Write-Host " [FAIL] llama-server.exe not found!" -ForegroundColor Red + Write-Host "" + Write-Host " Searched locations:" -ForegroundColor Gray + foreach ($p in $SearchPaths) { + Write-Host " - $p" -ForegroundColor Gray + } + Write-Host "" + Write-Host " To fix: Run setup.bat to build llama.cpp, or set:" -ForegroundColor Yellow + Write-Host ' $env:LLAMA_SERVER_PATH = "C:\path\to\llama-server.exe"' -ForegroundColor Yellow + exit 1 +} + +Write-Host " [OK] Found: $FoundBinary" -ForegroundColor Green + +# ── Step 2: Check file info ──────────────────────────────────────────── +Write-Host "" +Write-Host "2. Binary info..." -ForegroundColor Yellow + +$fileInfo = Get-Item $FoundBinary +$sizeMB = [math]::Round($fileInfo.Length / 1MB, 1) +Write-Host " Size: $sizeMB MB" -ForegroundColor Gray +Write-Host " Modified: $($fileInfo.LastWriteTime)" -ForegroundColor Gray + +# ── Step 3: Check for CUDA symbols ──────────────────────────────────── +Write-Host "" +Write-Host "3. Checking for CUDA support..." -ForegroundColor Yellow + +# Run with --help or -v and capture output to check for CUDA indicators +$helpOutput = & $FoundBinary --version 2>&1 | Out-String +if (-not $helpOutput) { + $helpOutput = "" +} + +# Check binary dependencies for CUDA DLLs using dumpbin if available +$dumpbin = (Get-Command dumpbin -ErrorAction SilentlyContinue) +$hasCudaDlls = $false + +if ($dumpbin) { + $deps = & dumpbin /dependents $FoundBinary 2>&1 | Out-String + if ($deps -match "cudart|cublas|cublasLt|nvcuda") { + $hasCudaDlls = $true + Write-Host " [OK] CUDA DLLs found in dependencies (dumpbin)" -ForegroundColor Green + # Extract CUDA DLL names + $cudaDlls = ($deps -split "`n") | Where-Object { $_ -match "cuda|cublas|nvcuda" } | ForEach-Object { $_.Trim() } + foreach ($dll in $cudaDlls) { + if ($dll) { Write-Host " - $dll" -ForegroundColor Gray } + } + } else { + Write-Host " [WARN] No CUDA DLLs found in dependencies!" -ForegroundColor Red + Write-Host " This binary was likely compiled WITHOUT -DGGML_CUDA=ON" -ForegroundColor Red + } +} else { + # Fallback: check file size (CUDA builds are typically > 50MB) + if ($sizeMB -gt 40) { + Write-Host " [LIKELY OK] Binary is $sizeMB MB (CUDA builds are typically > 50MB)" -ForegroundColor Green + } else { + Write-Host " [WARN] Binary is only $sizeMB MB (CPU-only builds are typically < 30MB)" -ForegroundColor Yellow + Write-Host " dumpbin not available for detailed check. Install VS Build Tools." -ForegroundColor Gray + } +} + +# ── Step 4: Quick startup test ───────────────────────────────────────── +Write-Host "" +Write-Host "4. Running startup test (will start and immediately stop)..." -ForegroundColor Yellow + +# Start llama-server on a random port with no model — just check it initializes +$testPort = Get-Random -Minimum 49152 -Maximum 65535 +$proc = $null + +try { + $proc = Start-Process -FilePath $FoundBinary ` + -ArgumentList "--port", $testPort, "--host", "127.0.0.1" ` + -PassThru -NoNewWindow -RedirectStandardError "$env:TEMP\llama_test_stderr.txt" ` + -RedirectStandardOutput "$env:TEMP\llama_test_stdout.txt" + + # Give it 3 seconds to start + Start-Sleep -Seconds 3 + + # Check if it crashed + if ($proc.HasExited) { + $exitCode = $proc.ExitCode + $stderr = "" + if (Test-Path "$env:TEMP\llama_test_stderr.txt") { + $stderr = Get-Content "$env:TEMP\llama_test_stderr.txt" -Raw + } + $stdout = "" + if (Test-Path "$env:TEMP\llama_test_stdout.txt") { + $stdout = Get-Content "$env:TEMP\llama_test_stdout.txt" -Raw + } + + $allOutput = "$stdout`n$stderr" + + if ($allOutput -match "failed to initialize CUDA") { + Write-Host " [FAIL] CUDA initialization failed!" -ForegroundColor Red + Write-Host " The binary was compiled without CUDA support or CUDA drivers are missing." -ForegroundColor Red + Write-Host "" + Write-Host " Rebuild with: cmake -DGGML_CUDA=ON ..." -ForegroundColor Yellow + } elseif ($allOutput -match "HTTPS is not supported") { + # This is expected when LLAMA_CURL=OFF — not a real failure + Write-Host " [OK] Binary started (HTTPS warning is expected — we use local files)" -ForegroundColor Green + } else { + Write-Host " [WARN] Process exited with code $exitCode" -ForegroundColor Yellow + } + + if ($allOutput.Trim()) { + Write-Host "" + Write-Host " --- Output ---" -ForegroundColor Gray + $allOutput.Trim().Split("`n") | ForEach-Object { Write-Host " $_" -ForegroundColor Gray } + } + } else { + Write-Host " [OK] llama-server started successfully on port $testPort" -ForegroundColor Green + + # Check CUDA detection from startup output + Start-Sleep -Seconds 1 + $stderr = "" + if (Test-Path "$env:TEMP\llama_test_stderr.txt") { + $stderr = Get-Content "$env:TEMP\llama_test_stderr.txt" -Raw + } + + if ($stderr -match "CUDA") { + if ($stderr -match "failed to initialize CUDA") { + Write-Host " [FAIL] CUDA init failed at runtime!" -ForegroundColor Red + } else { + Write-Host " [OK] CUDA detected at runtime" -ForegroundColor Green + } + } + + # Kill it + Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue + Write-Host " Stopped test server." -ForegroundColor Gray + } +} catch { + Write-Host " [ERROR] Could not start llama-server: $_" -ForegroundColor Red +} finally { + if ($proc -and -not $proc.HasExited) { + Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue + } + Remove-Item "$env:TEMP\llama_test_stderr.txt" -ErrorAction SilentlyContinue + Remove-Item "$env:TEMP\llama_test_stdout.txt" -ErrorAction SilentlyContinue +} + +# ── Step 5: Check llama-quantize ─────────────────────────────────────── +Write-Host "" +Write-Host "5. Checking llama-quantize..." -ForegroundColor Yellow + +$quantizePath = Join-Path (Split-Path $FoundBinary) "llama-quantize.exe" +if (Test-Path $quantizePath) { + $qSize = [math]::Round((Get-Item $quantizePath).Length / 1MB, 1) + Write-Host " [OK] Found: $quantizePath ($qSize MB)" -ForegroundColor Green +} else { + Write-Host " [WARN] llama-quantize.exe not found alongside llama-server" -ForegroundColor Yellow + Write-Host " GGUF export/quantization won't work without it" -ForegroundColor Yellow +} + +# ── Summary ──────────────────────────────────────────────────────────── +Write-Host "" +Write-Host "============================================" -ForegroundColor Cyan +Write-Host " Summary" -ForegroundColor Cyan +Write-Host "============================================" -ForegroundColor Cyan +Write-Host " Binary: $FoundBinary" -ForegroundColor Gray +Write-Host " Size: $sizeMB MB" -ForegroundColor Gray +if ($hasCudaDlls) { + Write-Host " CUDA: YES (confirmed via DLL deps)" -ForegroundColor Green +} elseif ($sizeMB -gt 40) { + Write-Host " CUDA: LIKELY (large binary size)" -ForegroundColor Yellow +} else { + Write-Host " CUDA: NO (rebuild with -DGGML_CUDA=ON)" -ForegroundColor Red +} +Write-Host "" + +if (-not $hasCudaDlls -and $sizeMB -le 40) { + Write-Host "To rebuild with CUDA:" -ForegroundColor Yellow + Write-Host ' 1. Delete the build dir: Remove-Item -Recurse -Force "$env:USERPROFILE\.unsloth\llama.cpp\build"' -ForegroundColor Gray + Write-Host ' 2. Re-run: .\setup.bat' -ForegroundColor Gray + Write-Host "" +} From bccbd26f3a932c3a499b8364e32b2eb28526c7ba Mon Sep 17 00:00:00 2001 From: Roland Tannous Date: Sat, 28 Feb 2026 07:07:46 +0000 Subject: [PATCH 14/36] Fix non-ASCII chars in test script for Windows PS 5.1 --- test_llama_cpp.ps1 | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/test_llama_cpp.ps1 b/test_llama_cpp.ps1 index 5eb3b7db32..6e16057640 100644 --- a/test_llama_cpp.ps1 +++ b/test_llama_cpp.ps1 @@ -19,7 +19,7 @@ Write-Host " llama.cpp Windows Build Test" -ForegroundColor Cyan Write-Host "============================================" -ForegroundColor Cyan Write-Host "" -# ── Step 1: Locate the binary ────────────────────────────────────────── +# -- Step 1: Locate the binary ------------------------------------------ Write-Host "1. Locating llama-server binary..." -ForegroundColor Yellow $SearchPaths = @() @@ -69,7 +69,7 @@ if (-not $FoundBinary) { Write-Host " [OK] Found: $FoundBinary" -ForegroundColor Green -# ── Step 2: Check file info ──────────────────────────────────────────── +# -- Step 2: Check file info -------------------------------------------- Write-Host "" Write-Host "2. Binary info..." -ForegroundColor Yellow @@ -78,7 +78,7 @@ $sizeMB = [math]::Round($fileInfo.Length / 1MB, 1) Write-Host " Size: $sizeMB MB" -ForegroundColor Gray Write-Host " Modified: $($fileInfo.LastWriteTime)" -ForegroundColor Gray -# ── Step 3: Check for CUDA symbols ──────────────────────────────────── +# -- Step 3: Check for CUDA symbols ------------------------------------ Write-Host "" Write-Host "3. Checking for CUDA support..." -ForegroundColor Yellow @@ -116,11 +116,11 @@ if ($dumpbin) { } } -# ── Step 4: Quick startup test ───────────────────────────────────────── +# -- Step 4: Quick startup test ----------------------------------------- Write-Host "" Write-Host "4. Running startup test (will start and immediately stop)..." -ForegroundColor Yellow -# Start llama-server on a random port with no model — just check it initializes +# Start llama-server on a random port with no model -- just check it initializes $testPort = Get-Random -Minimum 49152 -Maximum 65535 $proc = $null @@ -153,8 +153,8 @@ try { Write-Host "" Write-Host " Rebuild with: cmake -DGGML_CUDA=ON ..." -ForegroundColor Yellow } elseif ($allOutput -match "HTTPS is not supported") { - # This is expected when LLAMA_CURL=OFF — not a real failure - Write-Host " [OK] Binary started (HTTPS warning is expected — we use local files)" -ForegroundColor Green + # This is expected when LLAMA_CURL=OFF -- not a real failure + Write-Host " [OK] Binary started (HTTPS warning is expected -- we use local files)" -ForegroundColor Green } else { Write-Host " [WARN] Process exited with code $exitCode" -ForegroundColor Yellow } @@ -196,7 +196,7 @@ try { Remove-Item "$env:TEMP\llama_test_stdout.txt" -ErrorAction SilentlyContinue } -# ── Step 5: Check llama-quantize ─────────────────────────────────────── +# -- Step 5: Check llama-quantize --------------------------------------- Write-Host "" Write-Host "5. Checking llama-quantize..." -ForegroundColor Yellow @@ -209,7 +209,7 @@ if (Test-Path $quantizePath) { Write-Host " GGUF export/quantization won't work without it" -ForegroundColor Yellow } -# ── Summary ──────────────────────────────────────────────────────────── +# -- Summary ------------------------------------------------------------ Write-Host "" Write-Host "============================================" -ForegroundColor Cyan Write-Host " Summary" -ForegroundColor Cyan From 8d272ff8d560ea5a70eac4e69fc4bfd397810290 Mon Sep 17 00:00:00 2001 From: Roland Tannous Date: Sat, 28 Feb 2026 07:42:46 +0000 Subject: [PATCH 15/36] Auto-detect driver CUDA version, install compatible toolkit instead of latest --- setup.ps1 | 47 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/setup.ps1 b/setup.ps1 index 10e7900d44..c213dcbf50 100644 --- a/setup.ps1 +++ b/setup.ps1 @@ -278,14 +278,55 @@ if ($vsResult) { # ============================================ # 1e. CUDA Toolkit (nvcc for llama.cpp build + env vars) # ============================================ +# IMPORTANT: The CUDA Toolkit version must be <= the max CUDA version the +# NVIDIA driver supports. nvidia-smi reports this as "CUDA Version: X.Y". +# If we install a toolkit newer than the driver supports, llama-server will +# fail at runtime with "ggml_cuda_init: failed to initialize CUDA: (null)". + +# -- Detect max CUDA version the driver supports -- +$DriverMaxCuda = $null +try { + $smiOut = nvidia-smi 2>&1 | Out-String + if ($smiOut -match "CUDA Version:\s+([\d]+)\.([\d]+)") { + $DriverMaxCuda = "$($Matches[1]).$($Matches[2])" + Write-Host " Driver supports up to CUDA $DriverMaxCuda" -ForegroundColor Gray + } +} catch {} + $NvccPath = Find-Nvcc +# -- If toolkit is already installed, verify it's compatible with driver -- +if ($NvccPath -and $DriverMaxCuda) { + $NvccOut = & $NvccPath --version 2>&1 | Out-String + if ($NvccOut -match "release\s+([\d]+)\.([\d]+)") { + $ToolkitVersion = "$($Matches[1]).$($Matches[2])" + $tkMajor = [int]$Matches[1]; $tkMinor = [int]$Matches[2] + $drMajor = [int]$DriverMaxCuda.Split('.')[0]; $drMinor = [int]$DriverMaxCuda.Split('.')[1] + if (($tkMajor -gt $drMajor) -or ($tkMajor -eq $drMajor -and $tkMinor -gt $drMinor)) { + Write-Host "[WARN] Installed CUDA Toolkit $ToolkitVersion is NEWER than driver supports ($DriverMaxCuda)." -ForegroundColor Yellow + Write-Host " This will cause 'failed to initialize CUDA' at runtime." -ForegroundColor Yellow + Write-Host " Installing compatible CUDA Toolkit $DriverMaxCuda..." -ForegroundColor Cyan + # Force reinstall of a compatible version + $NvccPath = $null + } else { + Write-Host " [OK] CUDA Toolkit $ToolkitVersion is compatible with driver (max $DriverMaxCuda)" -ForegroundColor Green + } + } +} + if (-not $NvccPath) { - Write-Host "CUDA driver detected but toolkit (nvcc) not found -- installing via winget..." -ForegroundColor Yellow + Write-Host "CUDA driver detected but compatible 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 + # Install the version matching the driver's max supported CUDA + $WingetVersion = if ($DriverMaxCuda) { $DriverMaxCuda } else { $null } + if ($WingetVersion) { + Write-Host " Installing CUDA Toolkit $WingetVersion via winget..." -ForegroundColor Cyan + winget install --id=Nvidia.CUDA --version=$WingetVersion -e --source winget --accept-package-agreements --accept-source-agreements + } else { + Write-Host " Installing CUDA Toolkit (latest) 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) { From 3521de7040573a8253306dae3ca1d5abcb283383 Mon Sep 17 00:00:00 2001 From: Roland Tannous Date: Sat, 28 Feb 2026 07:45:40 +0000 Subject: [PATCH 16/36] Build llama.cpp in-tree, auto-detect driver CUDA version for compatible toolkit --- setup.ps1 | 7 +++---- studio/backend/core/inference/llama_cpp.py | 6 +++--- test_llama_cpp.ps1 | 1 + 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/setup.ps1 b/setup.ps1 index c213dcbf50..1269467368 100644 --- a/setup.ps1 +++ b/setup.ps1 @@ -556,12 +556,13 @@ $ErrorActionPreference = $prevEAP # ========================================================================== # PHASE 4: Build llama.cpp with CUDA for GGUF inference + export # ========================================================================== -# Builds at ~/.unsloth/llama.cpp/ (persistent across pip upgrades). +# Builds in-tree at $REPO/llama.cpp/ (same as setup.sh on Linux). +# This directory is already in .gitignore. # 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" +$LlamaCppDir = Join-Path $PSScriptRoot "llama.cpp" $BuildDir = Join-Path $LlamaCppDir "build" $LlamaServerBin = Join-Path $BuildDir "bin\Release\llama-server.exe" @@ -588,8 +589,6 @@ if (Test-Path $LlamaServerBin) { $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 diff --git a/studio/backend/core/inference/llama_cpp.py b/studio/backend/core/inference/llama_cpp.py index cf4494df30..7ae9bd142a 100644 --- a/studio/backend/core/inference/llama_cpp.py +++ b/studio/backend/core/inference/llama_cpp.py @@ -100,13 +100,13 @@ class LlamaCppBackend: if build_path.is_file(): return str(build_path) - # 3. Windows MSVC build (setup.ps1 builds here — Release config) + # 3. Windows MSVC build (Release config, in-tree — matches setup.ps1) if sys.platform == "win32": - # In-tree + # In-tree (primary — setup.ps1 now builds here) win_path = project_root / "llama.cpp" / "build" / "bin" / "Release" / binary_name if win_path.is_file(): return str(win_path) - # ~/.unsloth (setup.ps1 default location) + # Legacy: ~/.unsloth (older setup.ps1 versions built here) home_path = Path.home() / ".unsloth" / "llama.cpp" / "build" / "bin" / "Release" / binary_name if home_path.is_file(): return str(home_path) diff --git a/test_llama_cpp.ps1 b/test_llama_cpp.ps1 index 6e16057640..b98177eac6 100644 --- a/test_llama_cpp.ps1 +++ b/test_llama_cpp.ps1 @@ -32,6 +32,7 @@ if ($BinaryPath) { $RepoRoot = $PSScriptRoot $SearchPaths += Join-Path $RepoRoot "llama.cpp\build\bin\Release\llama-server.exe" $SearchPaths += Join-Path $RepoRoot "llama.cpp\build\bin\llama-server.exe" +# Legacy: older setup.ps1 built under ~/.unsloth $SearchPaths += Join-Path $env:USERPROFILE ".unsloth\llama.cpp\build\bin\Release\llama-server.exe" # Check LLAMA_SERVER_PATH env var From 75765527170c4226db84abbe9e5484d84730b757 Mon Sep 17 00:00:00 2001 From: Roland Tannous Date: Sat, 28 Feb 2026 07:50:44 +0000 Subject: [PATCH 17/36] Fix: scan side-by-side CUDA installs, pick compatible toolkit version --- setup.ps1 | 75 +++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 54 insertions(+), 21 deletions(-) diff --git a/setup.ps1 b/setup.ps1 index 1269467368..3fec5f22ed 100644 --- a/setup.ps1 +++ b/setup.ps1 @@ -42,6 +42,41 @@ function Refresh-Environment { # Find nvcc on PATH, CUDA_PATH, or standard toolkit dirs. # Returns the path to nvcc.exe, or $null if not found. function Find-Nvcc { + param([string]$MaxVersion = "") + + # If MaxVersion is set, we need to find a toolkit <= that version. + # CUDA toolkits install side-by-side under C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\vX.Y\ + + $toolkitBase = 'C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA' + + if ($MaxVersion -and (Test-Path $toolkitBase)) { + $drMajor = [int]$MaxVersion.Split('.')[0] + $drMinor = [int]$MaxVersion.Split('.')[1] + + # Get all installed CUDA dirs, sorted descending (highest first) + $cudaDirs = Get-ChildItem -Directory $toolkitBase | Where-Object { + $_.Name -match '^v(\d+)\.(\d+)' + } | Sort-Object { [version]($_.Name -replace '^v','') } -Descending + + foreach ($dir in $cudaDirs) { + if ($dir.Name -match '^v(\d+)\.(\d+)') { + $tkMajor = [int]$Matches[1]; $tkMinor = [int]$Matches[2] + $compatible = ($tkMajor -lt $drMajor) -or ($tkMajor -eq $drMajor -and $tkMinor -le $drMinor) + if ($compatible) { + $nvcc = Join-Path $dir.FullName 'bin\nvcc.exe' + if (Test-Path $nvcc) { + return $nvcc + } + } + } + } + + # No compatible side-by-side version found + return $null + } + + # Fallback: no version constraint — pick latest or whatever is available + # 1. Check nvcc on PATH $cmd = Get-Command nvcc -ErrorAction SilentlyContinue if ($cmd) { return $cmd.Source } @@ -55,7 +90,6 @@ function Find-Nvcc { } # 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'))) { @@ -293,25 +327,16 @@ try { } } catch {} -$NvccPath = Find-Nvcc - -# -- If toolkit is already installed, verify it's compatible with driver -- -if ($NvccPath -and $DriverMaxCuda) { - $NvccOut = & $NvccPath --version 2>&1 | Out-String - if ($NvccOut -match "release\s+([\d]+)\.([\d]+)") { - $ToolkitVersion = "$($Matches[1]).$($Matches[2])" - $tkMajor = [int]$Matches[1]; $tkMinor = [int]$Matches[2] - $drMajor = [int]$DriverMaxCuda.Split('.')[0]; $drMinor = [int]$DriverMaxCuda.Split('.')[1] - if (($tkMajor -gt $drMajor) -or ($tkMajor -eq $drMajor -and $tkMinor -gt $drMinor)) { - Write-Host "[WARN] Installed CUDA Toolkit $ToolkitVersion is NEWER than driver supports ($DriverMaxCuda)." -ForegroundColor Yellow - Write-Host " This will cause 'failed to initialize CUDA' at runtime." -ForegroundColor Yellow - Write-Host " Installing compatible CUDA Toolkit $DriverMaxCuda..." -ForegroundColor Cyan - # Force reinstall of a compatible version - $NvccPath = $null - } else { - Write-Host " [OK] CUDA Toolkit $ToolkitVersion is compatible with driver (max $DriverMaxCuda)" -ForegroundColor Green - } +# -- Find a toolkit that's compatible with the driver -- +if ($DriverMaxCuda) { + $NvccPath = Find-Nvcc -MaxVersion $DriverMaxCuda + if ($NvccPath) { + Write-Host " [OK] Found compatible CUDA Toolkit (nvcc: $NvccPath)" -ForegroundColor Green + } else { + Write-Host " No CUDA Toolkit <= $DriverMaxCuda found. Will install..." -ForegroundColor Yellow } +} else { + $NvccPath = Find-Nvcc } if (-not $NvccPath) { @@ -328,7 +353,11 @@ if (-not $NvccPath) { winget install --id=Nvidia.CUDA -e --source winget --accept-package-agreements --accept-source-agreements } Refresh-Environment - $NvccPath = Find-Nvcc + if ($DriverMaxCuda) { + $NvccPath = Find-Nvcc -MaxVersion $DriverMaxCuda + } else { + $NvccPath = Find-Nvcc + } if ($NvccPath) { Write-Host " [OK] CUDA Toolkit installed (nvcc: $NvccPath)" -ForegroundColor Green } @@ -337,7 +366,11 @@ if (-not $NvccPath) { 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 + if ($DriverMaxCuda) { + Write-Host " Install CUDA Toolkit $DriverMaxCuda from https://developer.nvidia.com/cuda-toolkit-archive" -ForegroundColor Yellow + } else { + Write-Host " Install CUDA Toolkit from https://developer.nvidia.com/cuda-downloads" -ForegroundColor Yellow + } exit 1 } From 7b4d074857f2c401663d76543989cef5b1208a71 Mon Sep 17 00:00:00 2001 From: Roland Tannous Date: Sat, 28 Feb 2026 07:51:31 +0000 Subject: [PATCH 18/36] Always persist compatible CUDA_PATH to User registry (overwrite stale values) --- setup.ps1 | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/setup.ps1 b/setup.ps1 index 3fec5f22ed..4c826d4bec 100644 --- a/setup.ps1 +++ b/setup.ps1 @@ -381,13 +381,10 @@ $CudaToolkitRoot = Split-Path (Split-Path $NvccPath -Parent) -Parent # 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 -} +# Always persist CUDA_PATH to User registry so the compatible toolkit is used +# in future sessions (overwrites any existing value pointing to a newer, incompatible version) +[Environment]::SetEnvironmentVariable('CUDA_PATH', $CudaToolkitRoot, 'User') +Write-Host " Persisted CUDA_PATH=$CudaToolkitRoot 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*") { From b22c5b6ed874cb7220927eac6cd3654b18746c77 Mon Sep 17 00:00:00 2001 From: Roland Tannous Date: Sat, 28 Feb 2026 07:57:46 +0000 Subject: [PATCH 19/36] Fallback: try descending CUDA versions if exact driver-max install fails --- setup.ps1 | 38 +++++++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/setup.ps1 b/setup.ps1 index 4c826d4bec..d4202dc97c 100644 --- a/setup.ps1 +++ b/setup.ps1 @@ -343,23 +343,35 @@ if (-not $NvccPath) { Write-Host "CUDA driver detected but compatible toolkit (nvcc) not found -- installing via winget..." -ForegroundColor Yellow $HasWinget = $null -ne (Get-Command winget -ErrorAction SilentlyContinue) if ($HasWinget) { - # Install the version matching the driver's max supported CUDA - $WingetVersion = if ($DriverMaxCuda) { $DriverMaxCuda } else { $null } - if ($WingetVersion) { - Write-Host " Installing CUDA Toolkit $WingetVersion via winget..." -ForegroundColor Cyan - winget install --id=Nvidia.CUDA --version=$WingetVersion -e --source winget --accept-package-agreements --accept-source-agreements + if ($DriverMaxCuda) { + # Try descending compatible versions: 12.9, 12.8, 12.6, 12.5, 12.4, 12.3 + $drMajor = [int]$DriverMaxCuda.Split('.')[0] + $drMinor = [int]$DriverMaxCuda.Split('.')[1] + $versionsToTry = @() + for ($m = $drMinor; $m -ge 0; $m--) { + $versionsToTry += "$drMajor.$m" + } + foreach ($ver in $versionsToTry) { + Write-Host " Trying CUDA Toolkit $ver via winget..." -ForegroundColor Cyan + $prevEAPCuda = $ErrorActionPreference + $ErrorActionPreference = "Continue" + winget install --id=Nvidia.CUDA --version=$ver -e --source winget --accept-package-agreements --accept-source-agreements 2>&1 | Out-Null + $ErrorActionPreference = $prevEAPCuda + Refresh-Environment + $NvccPath = Find-Nvcc -MaxVersion $DriverMaxCuda + if ($NvccPath) { + Write-Host " [OK] CUDA Toolkit installed (nvcc: $NvccPath)" -ForegroundColor Green + break + } + } } else { Write-Host " Installing CUDA Toolkit (latest) via winget..." -ForegroundColor Cyan winget install --id=Nvidia.CUDA -e --source winget --accept-package-agreements --accept-source-agreements - } - Refresh-Environment - if ($DriverMaxCuda) { - $NvccPath = Find-Nvcc -MaxVersion $DriverMaxCuda - } else { + Refresh-Environment $NvccPath = Find-Nvcc - } - if ($NvccPath) { - Write-Host " [OK] CUDA Toolkit installed (nvcc: $NvccPath)" -ForegroundColor Green + if ($NvccPath) { + Write-Host " [OK] CUDA Toolkit installed (nvcc: $NvccPath)" -ForegroundColor Green + } } } } From d79fe439edea5ab2f9dc11c1c2de8c059c581814 Mon Sep 17 00:00:00 2001 From: Roland Tannous Date: Sat, 28 Feb 2026 08:05:00 +0000 Subject: [PATCH 20/36] Warn user to uninstall incompatible CUDA toolkit instead of failed side-by-side --- setup.ps1 | 41 +++++++++++++++++++++++++++++++++-------- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/setup.ps1 b/setup.ps1 index d4202dc97c..49e7ebba0d 100644 --- a/setup.ps1 +++ b/setup.ps1 @@ -328,30 +328,55 @@ try { } catch {} # -- Find a toolkit that's compatible with the driver -- +$IncompatibleToolkit = $null if ($DriverMaxCuda) { $NvccPath = Find-Nvcc -MaxVersion $DriverMaxCuda if ($NvccPath) { Write-Host " [OK] Found compatible CUDA Toolkit (nvcc: $NvccPath)" -ForegroundColor Green } else { - Write-Host " No CUDA Toolkit <= $DriverMaxCuda found. Will install..." -ForegroundColor Yellow + # Check if there's an incompatible (too new) toolkit installed + $AnyNvcc = Find-Nvcc + if ($AnyNvcc) { + $NvccOut = & $AnyNvcc --version 2>&1 | Out-String + if ($NvccOut -match "release\s+([\d]+\.[\d]+)") { + $IncompatibleToolkit = $Matches[1] + } + } } } else { $NvccPath = Find-Nvcc } +# -- If incompatible toolkit is blocking, tell user to uninstall it -- +if (-not $NvccPath -and $IncompatibleToolkit) { + Write-Host "" -ForegroundColor Red + Write-Host "========================================================================" -ForegroundColor Red + Write-Host "[ERROR] CUDA Toolkit $IncompatibleToolkit is installed but INCOMPATIBLE" -ForegroundColor Red + Write-Host " with your NVIDIA driver (which supports up to CUDA $DriverMaxCuda)." -ForegroundColor Red + Write-Host "" -ForegroundColor Red + Write-Host " This will cause 'failed to initialize CUDA' errors at runtime." -ForegroundColor Red + Write-Host "" -ForegroundColor Red + Write-Host " To fix:" -ForegroundColor Yellow + Write-Host " 1. Open Control Panel -> Programs -> Uninstall a program" -ForegroundColor Yellow + Write-Host " 2. Uninstall 'NVIDIA CUDA Toolkit $IncompatibleToolkit'" -ForegroundColor Yellow + Write-Host " 3. Re-run setup.bat (it will install CUDA $DriverMaxCuda automatically)" -ForegroundColor Yellow + Write-Host "" -ForegroundColor Yellow + Write-Host " Alternatively, update your NVIDIA driver to one that supports CUDA $IncompatibleToolkit." -ForegroundColor Gray + Write-Host "========================================================================" -ForegroundColor Red + exit 1 +} + +# -- No toolkit at all: install via winget -- if (-not $NvccPath) { - Write-Host "CUDA driver detected but compatible toolkit (nvcc) not found -- installing via winget..." -ForegroundColor Yellow + Write-Host "CUDA toolkit (nvcc) not found -- installing via winget..." -ForegroundColor Yellow $HasWinget = $null -ne (Get-Command winget -ErrorAction SilentlyContinue) if ($HasWinget) { if ($DriverMaxCuda) { - # Try descending compatible versions: 12.9, 12.8, 12.6, 12.5, 12.4, 12.3 + # Try descending compatible versions $drMajor = [int]$DriverMaxCuda.Split('.')[0] $drMinor = [int]$DriverMaxCuda.Split('.')[1] - $versionsToTry = @() for ($m = $drMinor; $m -ge 0; $m--) { - $versionsToTry += "$drMajor.$m" - } - foreach ($ver in $versionsToTry) { + $ver = "$drMajor.$m" Write-Host " Trying CUDA Toolkit $ver via winget..." -ForegroundColor Cyan $prevEAPCuda = $ErrorActionPreference $ErrorActionPreference = "Continue" @@ -360,7 +385,7 @@ if (-not $NvccPath) { Refresh-Environment $NvccPath = Find-Nvcc -MaxVersion $DriverMaxCuda if ($NvccPath) { - Write-Host " [OK] CUDA Toolkit installed (nvcc: $NvccPath)" -ForegroundColor Green + Write-Host " [OK] CUDA Toolkit $ver installed (nvcc: $NvccPath)" -ForegroundColor Green break } } From 12867f701b87a877c57280b7dcdad644a7ddcf76 Mon Sep 17 00:00:00 2001 From: Roland Tannous Date: Sat, 28 Feb 2026 08:41:54 +0000 Subject: [PATCH 21/36] Auto-add CUDA DLLs to PATH when launching llama-server on Windows --- setup.ps1 | 28 ++++++++++------------ studio/backend/core/inference/llama_cpp.py | 26 ++++++++++++++++---- 2 files changed, 35 insertions(+), 19 deletions(-) diff --git a/setup.ps1 b/setup.ps1 index 49e7ebba0d..fae0ce7529 100644 --- a/setup.ps1 +++ b/setup.ps1 @@ -820,18 +820,16 @@ Write-Host "[OK] Batch launchers created in $BatDir (works from cmd.exe when ven # 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 \ No newline at end of file +Write-Host "+===============================================+" -ForegroundColor Green +Write-Host "| Setup Complete! |" -ForegroundColor Green +Write-Host "| |" -ForegroundColor Green +Write-Host "| IMPORTANT: Open a NEW terminal, then: |" -ForegroundColor Yellow +Write-Host "| |" -ForegroundColor Green +Write-Host "| cmd.exe: |" -ForegroundColor Green +Write-Host "| .venv\Scripts\activate.bat |" -ForegroundColor Green +Write-Host "| unsloth-studio -H 0.0.0.0 -p 8000 |" -ForegroundColor Green +Write-Host "| |" -ForegroundColor Green +Write-Host "| PowerShell: |" -ForegroundColor Green +Write-Host "| .venv\Scripts\Activate.ps1 |" -ForegroundColor Green +Write-Host "| unsloth-studio -H 0.0.0.0 -p 8000 |" -ForegroundColor Green +Write-Host "+===============================================+" -ForegroundColor Green \ No newline at end of file diff --git a/studio/backend/core/inference/llama_cpp.py b/studio/backend/core/inference/llama_cpp.py index 7ae9bd142a..023edb959b 100644 --- a/studio/backend/core/inference/llama_cpp.py +++ b/studio/backend/core/inference/llama_cpp.py @@ -235,13 +235,31 @@ class LlamaCppBackend: logger.info(f"Starting llama-server: {' '.join(cmd)}") - # Set LD_LIBRARY_PATH so llama-server can find its shared libs - # (libmtmd.so, libllama.so, etc.) which live next to the binary + # Set library paths so llama-server can find its shared libs and CUDA DLLs import os + import sys env = os.environ.copy() binary_dir = str(Path(binary).parent) - existing_ld = env.get("LD_LIBRARY_PATH", "") - env["LD_LIBRARY_PATH"] = f"{binary_dir}:{existing_ld}" if existing_ld else binary_dir + + if sys.platform == "win32": + # On Windows, CUDA DLLs (cublas64_12.dll, cudart64_12.dll, etc.) + # must be on PATH. Add CUDA_PATH\bin if available. + path_dirs = [binary_dir] + cuda_path = os.environ.get("CUDA_PATH", "") + if cuda_path: + cuda_bin = os.path.join(cuda_path, "bin") + if os.path.isdir(cuda_bin): + path_dirs.append(cuda_bin) + # Some CUDA installs put DLLs in bin\x64 + cuda_bin_x64 = os.path.join(cuda_path, "bin", "x64") + if os.path.isdir(cuda_bin_x64): + path_dirs.append(cuda_bin_x64) + existing_path = env.get("PATH", "") + env["PATH"] = ";".join(path_dirs) + ";" + existing_path + else: + # Linux: set LD_LIBRARY_PATH for shared libs next to the binary + existing_ld = env.get("LD_LIBRARY_PATH", "") + env["LD_LIBRARY_PATH"] = f"{binary_dir}:{existing_ld}" if existing_ld else binary_dir self._stdout_lines = [] self._process = subprocess.Popen( From 8e22b16bd85602d9bc556d2624ebf3c469c4c243 Mon Sep 17 00:00:00 2001 From: Roland Tannous Date: Sat, 28 Feb 2026 08:43:35 +0000 Subject: [PATCH 22/36] Simplify completion banner: no venv activation needed --- setup.ps1 | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/setup.ps1 b/setup.ps1 index fae0ce7529..07dff2090f 100644 --- a/setup.ps1 +++ b/setup.ps1 @@ -823,13 +823,8 @@ Write-Host "" Write-Host "+===============================================+" -ForegroundColor Green Write-Host "| Setup Complete! |" -ForegroundColor Green Write-Host "| |" -ForegroundColor Green -Write-Host "| IMPORTANT: Open a NEW terminal, then: |" -ForegroundColor Yellow +Write-Host "| IMPORTANT: Open a NEW terminal, then run: |" -ForegroundColor Yellow Write-Host "| |" -ForegroundColor Green -Write-Host "| cmd.exe: |" -ForegroundColor Green -Write-Host "| .venv\Scripts\activate.bat |" -ForegroundColor Green Write-Host "| unsloth-studio -H 0.0.0.0 -p 8000 |" -ForegroundColor Green Write-Host "| |" -ForegroundColor Green -Write-Host "| PowerShell: |" -ForegroundColor Green -Write-Host "| .venv\Scripts\Activate.ps1 |" -ForegroundColor Green -Write-Host "| unsloth-studio -H 0.0.0.0 -p 8000 |" -ForegroundColor Green Write-Host "+===============================================+" -ForegroundColor Green \ No newline at end of file From 9eb0ff074b505619522ea824ba618de8ef533406 Mon Sep 17 00:00:00 2001 From: Roland Tannous Date: Sat, 28 Feb 2026 08:46:09 +0000 Subject: [PATCH 23/36] Add .venv/Scripts to User PATH so unsloth-studio works without activation --- setup.ps1 | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/setup.ps1 b/setup.ps1 index 07dff2090f..265b13eb35 100644 --- a/setup.ps1 +++ b/setup.ps1 @@ -806,7 +806,7 @@ function unsloth-ui { & "$VenvPython" "$CliScript" studio -f "$FrontendDist" 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 --- +# --- cmd.exe: create batch files and ensure they're on PATH --- $BatDir = Join-Path $RepoDir ".venv\Scripts" foreach ($name in @("unsloth-studio", "unsloth-ui")) { $batPath = Join-Path $BatDir "$name.bat" @@ -814,7 +814,17 @@ foreach ($name in @("unsloth-studio", "unsloth-ui")) { 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 +# Persist .venv\Scripts to User PATH so commands work in new cmd.exe terminals without activation +$userPath = [Environment]::GetEnvironmentVariable('Path', 'User') +if (-not $userPath -or $userPath -notlike "*$BatDir*") { + if ($userPath) { + [Environment]::SetEnvironmentVariable('Path', "$BatDir;$userPath", 'User') + } else { + [Environment]::SetEnvironmentVariable('Path', "$BatDir", 'User') + } + Write-Host " Persisted $BatDir to User PATH" -ForegroundColor Gray +} +Write-Host "[OK] Batch launchers created (works from any new cmd.exe or PowerShell)" -ForegroundColor Green # ============================================ # Done From 6e5a3d17445e3969843dd5c66f232c15b3c0d6b0 Mon Sep 17 00:00:00 2001 From: Roland Tannous Date: Sat, 28 Feb 2026 09:01:00 +0000 Subject: [PATCH 24/36] Download GGUF via huggingface_hub instead of llama-server -hf (fixes HTTPS not supported on Windows) --- studio/backend/core/inference/llama_cpp.py | 55 +++++++++++++++++++--- 1 file changed, 48 insertions(+), 7 deletions(-) diff --git a/studio/backend/core/inference/llama_cpp.py b/studio/backend/core/inference/llama_cpp.py index 023edb959b..e0e2eebcf2 100644 --- a/studio/backend/core/inference/llama_cpp.py +++ b/studio/backend/core/inference/llama_cpp.py @@ -199,16 +199,58 @@ class LlamaCppBackend: # Build command based on mode if hf_repo: - hf_spec = f"{hf_repo}:{hf_variant}" if hf_variant else hf_repo + # Download the GGUF file ourselves using huggingface_hub + # (llama-server's -hf flag requires HTTPS/curl which may not + # be available, e.g. Windows builds with -DLLAMA_CURL=OFF) + try: + from huggingface_hub import hf_hub_download + except ImportError: + raise RuntimeError( + "huggingface_hub is required for HF model loading. " + "Install it with: pip install huggingface_hub" + ) + + # Determine the filename from the variant (e.g., "Q4_K_M" -> find matching file) + gguf_filename = None + if hf_variant: + # Try common naming patterns + try: + from huggingface_hub import list_repo_files + files = list_repo_files(hf_repo, token=hf_token) + variant_lower = hf_variant.lower() + for f in files: + if f.endswith(".gguf") and variant_lower in f.lower(): + gguf_filename = f + break + except Exception as e: + logger.warning(f"Could not list repo files: {e}") + + if not gguf_filename: + # Fallback: construct common filename pattern + # e.g., "unsloth/gemma-3-4b-it-GGUF" + "Q4_K_M" -> try model name + repo_name = hf_repo.split("/")[-1].replace("-GGUF", "") + gguf_filename = f"{repo_name}-{hf_variant}.gguf" + + logger.info(f"Downloading GGUF: {hf_repo}/{gguf_filename}") + try: + local_path = hf_hub_download( + repo_id=hf_repo, + filename=gguf_filename, + token=hf_token, + ) + except Exception as e: + raise RuntimeError( + f"Failed to download GGUF file '{gguf_filename}' from {hf_repo}: {e}" + ) + + logger.info(f"GGUF downloaded to: {local_path}") cmd = [ binary, - "-hf", hf_spec, + "-m", local_path, "--port", str(self._port), "-c", str(n_ctx), "-ngl", str(n_gpu_layers), ] - if hf_token: - cmd.extend(["--hf-token", hf_token]) elif gguf_path: if not Path(gguf_path).is_file(): raise FileNotFoundError(f"GGUF file not found: {gguf_path}") @@ -282,9 +324,8 @@ class LlamaCppBackend: self._is_vision = is_vision self._model_identifier = model_identifier - # HF mode: llama-server downloads before becoming healthy — need longer timeout - timeout = 600.0 if hf_repo else 120.0 - if not self._wait_for_health(timeout=timeout): + # Wait for llama-server to become healthy + if not self._wait_for_health(timeout=120.0): self._kill_process() raise RuntimeError( "llama-server failed to start. " From 2e102b683efd99db6ad8e76a328d46522192a340 Mon Sep 17 00:00:00 2001 From: Roland Tannous Date: Sat, 28 Feb 2026 09:14:22 +0000 Subject: [PATCH 25/36] Add vcpkg/curl[ssl] for HTTPS support in llama-server, enable LLAMA_CURL=ON --- .gitignore | 3 +++ setup.ps1 | 42 ++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 044775e846..633e3041c6 100755 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,9 @@ unsloth_training_checkpoints/ # llama.cpp build (built by setup.sh, shared with unsloth-zoo export) llama.cpp/ +# vcpkg (installed by setup.ps1 for curl/SSL) +vcpkg/ + # Built binaries (llama-server etc.) bin/ diff --git a/setup.ps1 b/setup.ps1 index 265b13eb35..336d20a777 100644 --- a/setup.ps1 +++ b/setup.ps1 @@ -620,13 +620,45 @@ python "$PSScriptRoot\install_python_stack.py" # Restore ErrorActionPreference after pip/python work $ErrorActionPreference = $prevEAP +# ========================================================================== +# PHASE 3.5: Install vcpkg + curl (for HTTPS support in llama-server) +# ========================================================================== +# llama-server needs curl + OpenSSL to download models from HuggingFace via -hf. +# We use vcpkg to install curl with SSL support. +$VcpkgDir = Join-Path $PSScriptRoot "vcpkg" +$VcpkgExe = Join-Path $VcpkgDir "vcpkg.exe" + +if (-not (Test-Path $VcpkgExe)) { + Write-Host "" + Write-Host "Installing vcpkg (for curl/SSL)..." -ForegroundColor Cyan + if (Test-Path $VcpkgDir) { Remove-Item -Recurse -Force $VcpkgDir } + git clone --depth 1 https://github.com/microsoft/vcpkg.git $VcpkgDir + if ($LASTEXITCODE -eq 0) { + & (Join-Path $VcpkgDir "bootstrap-vcpkg.bat") -disableMetrics + } +} + +if (Test-Path $VcpkgExe) { + # Install curl with SSL support (this also installs OpenSSL as a dependency) + $CurlInstalled = & $VcpkgExe list 2>&1 | Select-String "curl" + if (-not $CurlInstalled) { + Write-Host " Installing curl[ssl] via vcpkg (includes OpenSSL)..." -ForegroundColor Cyan + & $VcpkgExe install "curl[ssl]:x64-windows" + } + $VcpkgToolchainFile = Join-Path $VcpkgDir "scripts\buildsystems\vcpkg.cmake" + Write-Host "[OK] vcpkg ready (toolchain: $VcpkgToolchainFile)" -ForegroundColor Green +} else { + Write-Host "[WARN] vcpkg not available -- llama-server will be built without HTTPS support" -ForegroundColor Yellow + $VcpkgToolchainFile = $null +} + # ========================================================================== # PHASE 4: Build llama.cpp with CUDA for GGUF inference + export # ========================================================================== # Builds in-tree at $REPO/llama.cpp/ (same as setup.sh on Linux). # This directory is already in .gitignore. # We build: -# - llama-server: for GGUF model inference +# - llama-server: for GGUF model inference (with HTTPS if vcpkg/curl available) # - llama-quantize: for GGUF export quantization # Prerequisites (git, cmake, VS Build Tools, CUDA Toolkit) already installed in Phase 1. $LlamaCppDir = Join-Path $PSScriptRoot "llama.cpp" @@ -690,8 +722,14 @@ if (Test-Path $LlamaServerBin) { } # Common flags $CmakeArgs += '-DBUILD_SHARED_LIBS=OFF' - $CmakeArgs += '-DLLAMA_CURL=OFF' $CmakeArgs += '-DCMAKE_POLICY_DEFAULT_CMP0194=NEW' + # HTTPS support via vcpkg curl + OpenSSL + if ($VcpkgToolchainFile -and (Test-Path $VcpkgToolchainFile)) { + $CmakeArgs += "-DCMAKE_TOOLCHAIN_FILE=$VcpkgToolchainFile" + $CmakeArgs += '-DLLAMA_CURL=ON' + } else { + $CmakeArgs += '-DLLAMA_CURL=OFF' + } $CmakeArgs += '-DCMAKE_EXE_LINKER_FLAGS=/NODEFAULTLIB:LIBCMT' # CUDA flags (Unsloth-aligned) $CmakeArgs += '-DGGML_CUDA=ON' From 453f423d22c08030e69c3987ba9e6fc285de2b3a Mon Sep 17 00:00:00 2001 From: Roland Tannous Date: Sat, 28 Feb 2026 09:16:19 +0000 Subject: [PATCH 26/36] Simplify: use winget OpenSSL.Dev instead of vcpkg for HTTPS support --- .gitignore | 3 --- setup.ps1 | 67 ++++++++++++++++++++++++++++++++---------------------- 2 files changed, 40 insertions(+), 30 deletions(-) diff --git a/.gitignore b/.gitignore index 633e3041c6..044775e846 100755 --- a/.gitignore +++ b/.gitignore @@ -28,9 +28,6 @@ unsloth_training_checkpoints/ # llama.cpp build (built by setup.sh, shared with unsloth-zoo export) llama.cpp/ -# vcpkg (installed by setup.ps1 for curl/SSL) -vcpkg/ - # Built binaries (llama-server etc.) bin/ diff --git a/setup.ps1 b/setup.ps1 index 336d20a777..7b5413a519 100644 --- a/setup.ps1 +++ b/setup.ps1 @@ -621,35 +621,48 @@ python "$PSScriptRoot\install_python_stack.py" $ErrorActionPreference = $prevEAP # ========================================================================== -# PHASE 3.5: Install vcpkg + curl (for HTTPS support in llama-server) +# PHASE 3.5: Install OpenSSL dev (for HTTPS support in llama-server) # ========================================================================== -# llama-server needs curl + OpenSSL to download models from HuggingFace via -hf. -# We use vcpkg to install curl with SSL support. -$VcpkgDir = Join-Path $PSScriptRoot "vcpkg" -$VcpkgExe = Join-Path $VcpkgDir "vcpkg.exe" +# llama-server needs OpenSSL to download models from HuggingFace via -hf. +# ShiningLight.OpenSSL.Dev includes headers + libs that cmake can find. +$OpenSslAvailable = $false -if (-not (Test-Path $VcpkgExe)) { - Write-Host "" - Write-Host "Installing vcpkg (for curl/SSL)..." -ForegroundColor Cyan - if (Test-Path $VcpkgDir) { Remove-Item -Recurse -Force $VcpkgDir } - git clone --depth 1 https://github.com/microsoft/vcpkg.git $VcpkgDir - if ($LASTEXITCODE -eq 0) { - & (Join-Path $VcpkgDir "bootstrap-vcpkg.bat") -disableMetrics +# Check if OpenSSL dev is already installed (look for include dir) +$OpenSslRoots = @( + 'C:\Program Files\OpenSSL-Win64', + 'C:\Program Files\OpenSSL', + 'C:\OpenSSL-Win64' +) +$OpenSslRoot = $null +foreach ($root in $OpenSslRoots) { + if (Test-Path (Join-Path $root 'include\openssl\ssl.h')) { + $OpenSslRoot = $root + break } } -if (Test-Path $VcpkgExe) { - # Install curl with SSL support (this also installs OpenSSL as a dependency) - $CurlInstalled = & $VcpkgExe list 2>&1 | Select-String "curl" - if (-not $CurlInstalled) { - Write-Host " Installing curl[ssl] via vcpkg (includes OpenSSL)..." -ForegroundColor Cyan - & $VcpkgExe install "curl[ssl]:x64-windows" - } - $VcpkgToolchainFile = Join-Path $VcpkgDir "scripts\buildsystems\vcpkg.cmake" - Write-Host "[OK] vcpkg ready (toolchain: $VcpkgToolchainFile)" -ForegroundColor Green +if ($OpenSslRoot) { + $OpenSslAvailable = $true + Write-Host "[OK] OpenSSL dev found at $OpenSslRoot" -ForegroundColor Green } else { - Write-Host "[WARN] vcpkg not available -- llama-server will be built without HTTPS support" -ForegroundColor Yellow - $VcpkgToolchainFile = $null + Write-Host "" + Write-Host "Installing OpenSSL dev (for HTTPS in llama-server)..." -ForegroundColor Cyan + $HasWinget = $null -ne (Get-Command winget -ErrorAction SilentlyContinue) + if ($HasWinget) { + winget install -e --id ShiningLight.OpenSSL.Dev --accept-package-agreements --accept-source-agreements + # Re-check after install + foreach ($root in $OpenSslRoots) { + if (Test-Path (Join-Path $root 'include\openssl\ssl.h')) { + $OpenSslRoot = $root + $OpenSslAvailable = $true + Write-Host "[OK] OpenSSL dev installed at $OpenSslRoot" -ForegroundColor Green + break + } + } + } + if (-not $OpenSslAvailable) { + Write-Host "[WARN] OpenSSL dev not available -- llama-server will be built without HTTPS" -ForegroundColor Yellow + } } # ========================================================================== @@ -723,10 +736,10 @@ if (Test-Path $LlamaServerBin) { # Common flags $CmakeArgs += '-DBUILD_SHARED_LIBS=OFF' $CmakeArgs += '-DCMAKE_POLICY_DEFAULT_CMP0194=NEW' - # HTTPS support via vcpkg curl + OpenSSL - if ($VcpkgToolchainFile -and (Test-Path $VcpkgToolchainFile)) { - $CmakeArgs += "-DCMAKE_TOOLCHAIN_FILE=$VcpkgToolchainFile" - $CmakeArgs += '-DLLAMA_CURL=ON' + # HTTPS support via OpenSSL + if ($OpenSslAvailable -and $OpenSslRoot) { + $CmakeArgs += "-DOPENSSL_ROOT_DIR=$OpenSslRoot" + $CmakeArgs += '-DLLAMA_OPENSSL=ON' } else { $CmakeArgs += '-DLLAMA_CURL=OFF' } From 6536bfb33b02f3b4ab1ad8c9c0d62b9bd9d48955 Mon Sep 17 00:00:00 2001 From: Roland Tannous Date: Sat, 28 Feb 2026 11:28:15 +0000 Subject: [PATCH 27/36] Remove unused CMP0194 cmake policy (eliminates cmake warning) --- setup.ps1 | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.ps1 b/setup.ps1 index 7b5413a519..f892ca52ee 100644 --- a/setup.ps1 +++ b/setup.ps1 @@ -735,7 +735,6 @@ if (Test-Path $LlamaServerBin) { } # Common flags $CmakeArgs += '-DBUILD_SHARED_LIBS=OFF' - $CmakeArgs += '-DCMAKE_POLICY_DEFAULT_CMP0194=NEW' # HTTPS support via OpenSSL if ($OpenSslAvailable -and $OpenSslRoot) { $CmakeArgs += "-DOPENSSL_ROOT_DIR=$OpenSslRoot" From 0267ba0a18357df571dd055734ae8e87fdbb6b1a Mon Sep 17 00:00:00 2001 From: Roland Tannous Date: Sun, 1 Mar 2026 01:51:04 +0000 Subject: [PATCH 28/36] Auto-enable Windows Long Paths via UAC elevation during setup --- setup.ps1 | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/setup.ps1 b/setup.ps1 index f892ca52ee..0d0ce5dea2 100644 --- a/setup.ps1 +++ b/setup.ps1 @@ -226,6 +226,40 @@ if (-not $HasNvidiaSmi) { } Write-Host "[OK] NVIDIA GPU detected" -ForegroundColor Green +# ============================================ +# 1a.5. Windows Long Paths (required for deep node_modules / Python paths) +# ============================================ +$LongPathsEnabled = $false +try { + $regVal = Get-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem" -Name "LongPathsEnabled" -ErrorAction SilentlyContinue + if ($regVal -and $regVal.LongPathsEnabled -eq 1) { + $LongPathsEnabled = $true + } +} catch {} + +if ($LongPathsEnabled) { + Write-Host "[OK] Windows Long Paths enabled" -ForegroundColor Green +} else { + Write-Host "Windows Long Paths not enabled (required for Triton compilation and deep dependency paths)." -ForegroundColor Yellow + Write-Host " Requesting admin access to fix..." -ForegroundColor Yellow + try { + # Spawn an elevated process to set the registry key (triggers UAC prompt) + $proc = Start-Process -FilePath "reg.exe" ` + -ArgumentList 'add "HKLM\SYSTEM\CurrentControlSet\Control\FileSystem" /v LongPathsEnabled /t REG_DWORD /d 1 /f' ` + -Verb RunAs -Wait -PassThru -ErrorAction Stop + if ($proc.ExitCode -eq 0) { + $LongPathsEnabled = $true + Write-Host "[OK] Windows Long Paths enabled (via UAC)" -ForegroundColor Green + } else { + Write-Host "[WARN] Failed to enable Long Paths (exit code: $($proc.ExitCode))" -ForegroundColor Yellow + } + } catch { + Write-Host "[WARN] Could not enable Long Paths (UAC was declined or not available)" -ForegroundColor Yellow + Write-Host " Run this manually in an Admin terminal:" -ForegroundColor Yellow + Write-Host ' reg add "HKLM\SYSTEM\CurrentControlSet\Control\FileSystem" /v LongPathsEnabled /t REG_DWORD /d 1 /f' -ForegroundColor Cyan + } +} + # ============================================ # 1b. Git (required by pip for git+https:// deps and by npm) # ============================================ From d5644d2d0dc1d2c5c5cef64a30026f7d259a83d8 Mon Sep 17 00:00:00 2001 From: Roland Tannous Date: Sun, 1 Mar 2026 04:33:04 +0000 Subject: [PATCH 29/36] Add Python 3.12 prerequisite check with auto-install via winget --- setup.ps1 | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/setup.ps1 b/setup.ps1 index 0d0ce5dea2..05c0f1f0d9 100644 --- a/setup.ps1 +++ b/setup.ps1 @@ -522,6 +522,43 @@ if ($NeedNode) { Write-Host "[OK] Node $(node -v) | npm $(npm -v)" -ForegroundColor Green +# ============================================ +# 1g. Python (>= 3.10, prefer 3.12) +# ============================================ +$HasPython = $null -ne (Get-Command python -ErrorAction SilentlyContinue) +$NeedPython = $true + +if ($HasPython) { + $PyVer = python --version 2>&1 + if ($PyVer -match "(\d+)\.(\d+)") { + $PyMajor = [int]$Matches[1]; $PyMinor = [int]$Matches[2] + if ($PyMajor -eq 3 -and $PyMinor -ge 10) { + Write-Host "[OK] Python $PyVer" -ForegroundColor Green + $NeedPython = $false + } else { + Write-Host "[WARN] Python $PyVer is too old (need >= 3.10)" -ForegroundColor Yellow + } + } +} else { + Write-Host "[WARN] Python not found." -ForegroundColor Yellow +} + +if ($NeedPython) { + Write-Host "Installing Python 3.12 via winget..." -ForegroundColor Cyan + $HasWinget = $null -ne (Get-Command winget -ErrorAction SilentlyContinue) + if ($HasWinget) { + winget install -e --id Python.Python.3.12 --source winget --accept-package-agreements --accept-source-agreements + Refresh-Environment + } + $HasPython = $null -ne (Get-Command python -ErrorAction SilentlyContinue) + if (-not $HasPython) { + Write-Host "[ERROR] Python could not be installed automatically." -ForegroundColor Red + Write-Host " Install Python 3.12 from https://python.org/downloads/" -ForegroundColor Yellow + exit 1 + } + Write-Host "[OK] Python $(python --version)" -ForegroundColor Green +} + Write-Host "" Write-Host "--- System prerequisites ready ---" -ForegroundColor Green Write-Host "" From 674cc67d78fbdf53ee728b798b2f2d8d0675b61a Mon Sep 17 00:00:00 2001 From: Roland Tannous Date: Sun, 1 Mar 2026 04:55:59 +0000 Subject: [PATCH 30/36] Tighten Python bounds to >= 3.11, < 3.14 (matching setup.sh), only auto-install if missing --- setup.ps1 | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/setup.ps1 b/setup.ps1 index 05c0f1f0d9..fd5447b124 100644 --- a/setup.ps1 +++ b/setup.ps1 @@ -523,28 +523,27 @@ if ($NeedNode) { Write-Host "[OK] Node $(node -v) | npm $(npm -v)" -ForegroundColor Green # ============================================ -# 1g. Python (>= 3.10, prefer 3.12) +# 1g. Python (>= 3.11 and < 3.14, matching setup.sh) # ============================================ $HasPython = $null -ne (Get-Command python -ErrorAction SilentlyContinue) -$NeedPython = $true +$PythonOk = $false if ($HasPython) { $PyVer = python --version 2>&1 if ($PyVer -match "(\d+)\.(\d+)") { $PyMajor = [int]$Matches[1]; $PyMinor = [int]$Matches[2] - if ($PyMajor -eq 3 -and $PyMinor -ge 10) { + if ($PyMajor -eq 3 -and $PyMinor -ge 11 -and $PyMinor -lt 14) { Write-Host "[OK] Python $PyVer" -ForegroundColor Green - $NeedPython = $false + $PythonOk = $true } else { - Write-Host "[WARN] Python $PyVer is too old (need >= 3.10)" -ForegroundColor Yellow + Write-Host "[ERROR] Python $PyVer is outside supported range (need >= 3.11 and < 3.14)." -ForegroundColor Red + Write-Host " Install Python 3.12 from https://python.org/downloads/" -ForegroundColor Yellow + exit 1 } } } else { - Write-Host "[WARN] Python not found." -ForegroundColor Yellow -} - -if ($NeedPython) { - Write-Host "Installing Python 3.12 via winget..." -ForegroundColor Cyan + # No Python at all -- install 3.12 + Write-Host "Python not found -- installing Python 3.12 via winget..." -ForegroundColor Yellow $HasWinget = $null -ne (Get-Command winget -ErrorAction SilentlyContinue) if ($HasWinget) { winget install -e --id Python.Python.3.12 --source winget --accept-package-agreements --accept-source-agreements @@ -557,6 +556,7 @@ if ($NeedPython) { exit 1 } Write-Host "[OK] Python $(python --version)" -ForegroundColor Green + $PythonOk = $true } Write-Host "" From e280e457d1697215b00ceca1b8d7fae8ccbfa29d Mon Sep 17 00:00:00 2001 From: Roland Tannous Date: Mon, 2 Mar 2026 04:04:41 +0000 Subject: [PATCH 31/36] Move llama.cpp clone/build from in-tree to ~/.unsloth/llama.cpp - setup.sh: builds at ~/.unsloth/llama.cpp instead of ./llama.cpp - setup.ps1: builds at %USERPROFILE%/.unsloth/llama.cpp - inference llama_cpp.py: searches ~/.unsloth/ first, in-tree as legacy - export.py: updated comments (unsloth-zoo handles path natively) --- setup.ps1 | 11 ++++--- setup.sh | 9 ++++-- studio/backend/core/export/export.py | 5 ++- studio/backend/core/inference/llama_cpp.py | 36 ++++++++++++---------- 4 files changed, 35 insertions(+), 26 deletions(-) diff --git a/setup.ps1 b/setup.ps1 index fd5447b124..8a2ec56033 100644 --- a/setup.ps1 +++ b/setup.ps1 @@ -739,13 +739,16 @@ if ($OpenSslRoot) { # ========================================================================== # PHASE 4: Build llama.cpp with CUDA for GGUF inference + export # ========================================================================== -# Builds in-tree at $REPO/llama.cpp/ (same as setup.sh on Linux). -# This directory is already in .gitignore. +# Builds at ~/.unsloth/llama.cpp — a single shared location under the user's +# home directory. This is used by both the inference server and the GGUF +# export pipeline (unsloth-zoo). # We build: -# - llama-server: for GGUF model inference (with HTTPS if vcpkg/curl available) +# - llama-server: for GGUF model inference (with HTTPS if OpenSSL available) # - llama-quantize: for GGUF export quantization # Prerequisites (git, cmake, VS Build Tools, CUDA Toolkit) already installed in Phase 1. -$LlamaCppDir = Join-Path $PSScriptRoot "llama.cpp" +$UnslothHome = Join-Path $env:USERPROFILE ".unsloth" +if (-not (Test-Path $UnslothHome)) { New-Item -ItemType Directory -Force $UnslothHome | Out-Null } +$LlamaCppDir = Join-Path $UnslothHome "llama.cpp" $BuildDir = Join-Path $LlamaCppDir "build" $LlamaServerBin = Join-Path $BuildDir "bin\Release\llama-server.exe" diff --git a/setup.sh b/setup.sh index 409df0b798..07ea131125 100755 --- a/setup.sh +++ b/setup.sh @@ -197,11 +197,14 @@ else fi # ── 8. Build llama.cpp binaries for GGUF inference + export ── -# Builds in-tree at $REPO/llama.cpp/. This directory is shared with -# unsloth-zoo's GGUF export pipeline. We build: +# Builds at ~/.unsloth/llama.cpp — a single shared location under the user's +# home directory. This is used by both the inference server and the GGUF +# export pipeline (unsloth-zoo). # - llama-server: for GGUF model inference # - llama-quantize: for GGUF export quantization (symlinked to root for check_llama_cpp()) -LLAMA_CPP_DIR="$SCRIPT_DIR/llama.cpp" +UNSLOTH_HOME="$HOME/.unsloth" +mkdir -p "$UNSLOTH_HOME" +LLAMA_CPP_DIR="$UNSLOTH_HOME/llama.cpp" LLAMA_SERVER_BIN="$LLAMA_CPP_DIR/build/bin/llama-server" rm -rf "$LLAMA_CPP_DIR" { diff --git a/studio/backend/core/export/export.py b/studio/backend/core/export/export.py index bc4e267f75..3d3db2d560 100644 --- a/studio/backend/core/export/export.py +++ b/studio/backend/core/export/export.py @@ -418,9 +418,8 @@ class ExportBackend: pre_existing_ggufs = set(glob.glob(os.path.join(cwd, "*.gguf"))) # Pass absolute path — no os.chdir needed. - # unsloth saves intermediate HF model files into model_save_path, - # while check_llama_cpp("llama.cpp") resolves against cwd (repo root) - # where setup.sh already built llama.cpp with quantizer. + # unsloth saves intermediate HF model files into model_save_path. + # unsloth-zoo's check_llama_cpp() uses ~/.unsloth/llama.cpp by default. model_save_path = os.path.join(abs_save_dir, "model") self.current_model.save_pretrained_gguf( model_save_path, diff --git a/studio/backend/core/inference/llama_cpp.py b/studio/backend/core/inference/llama_cpp.py index e0e2eebcf2..09f8a2fcc9 100644 --- a/studio/backend/core/inference/llama_cpp.py +++ b/studio/backend/core/inference/llama_cpp.py @@ -77,10 +77,11 @@ class LlamaCppBackend: Search order: 1. LLAMA_SERVER_PATH environment variable - 2. ./llama.cpp/build/bin/llama-server (built by setup.sh in-tree) - 3. ~/.unsloth/llama.cpp/build/bin/Release/llama-server (built by setup.ps1 on Windows) - 4. llama-server on PATH (system install) - 5. ./bin/llama-server (legacy: extracted binary) + 2. ~/.unsloth/llama.cpp/build/bin/llama-server (Linux, built by setup.sh) + 3. ~/.unsloth/llama.cpp/build/bin/Release/llama-server.exe (Windows, built by setup.ps1) + 4. ./llama.cpp/build/bin/llama-server (legacy: in-tree build) + 5. llama-server on PATH (system install) + 6. ./bin/llama-server (legacy: extracted binary) """ import os import sys @@ -92,31 +93,34 @@ class LlamaCppBackend: if env_path and Path(env_path).is_file(): return env_path - # Project root: llama_cpp.py → inference/ → core/ → backend/ → studio/ → root - project_root = Path(__file__).resolve().parents[4] + # 2. ~/.unsloth/llama.cpp (primary — setup.sh / setup.ps1 build here) + unsloth_home = Path.home() / ".unsloth" / "llama.cpp" + home_linux = unsloth_home / "build" / "bin" / binary_name + if home_linux.is_file(): + return str(home_linux) - # 2. In-tree llama.cpp build (setup.sh builds here on Linux) + # 3. Windows MSVC build has Release subdir + if sys.platform == "win32": + home_win = unsloth_home / "build" / "bin" / "Release" / binary_name + if home_win.is_file(): + return str(home_win) + + # 4. Legacy: in-tree build (older setup.sh / setup.ps1 versions) + project_root = Path(__file__).resolve().parents[4] build_path = project_root / "llama.cpp" / "build" / "bin" / binary_name if build_path.is_file(): return str(build_path) - - # 3. Windows MSVC build (Release config, in-tree — matches setup.ps1) if sys.platform == "win32": - # In-tree (primary — setup.ps1 now builds here) win_path = project_root / "llama.cpp" / "build" / "bin" / "Release" / binary_name if win_path.is_file(): return str(win_path) - # Legacy: ~/.unsloth (older setup.ps1 versions built here) - home_path = Path.home() / ".unsloth" / "llama.cpp" / "build" / "bin" / "Release" / binary_name - if home_path.is_file(): - return str(home_path) - # 4. System PATH + # 5. System PATH system_path = shutil.which("llama-server") if system_path: return system_path - # 5. Legacy: extracted to bin/ + # 6. Legacy: extracted to bin/ bin_path = project_root / "bin" / binary_name if bin_path.is_file(): return str(bin_path) From c64e50b46f3ec9e25b1b15474927c333df9e95d7 Mon Sep 17 00:00:00 2001 From: Roland Tannous Date: Mon, 2 Mar 2026 10:45:09 +0000 Subject: [PATCH 32/36] Patch unsloth-zoo llama_cpp.py and unsloth save.py from windows-support branch --- install_python_stack.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/install_python_stack.py b/install_python_stack.py index 7d3ab66b20..8f2b5fba8c 100644 --- a/install_python_stack.py +++ b/install_python_stack.py @@ -186,20 +186,27 @@ def install_python_stack() -> int: constrain=False, ) - # 6. Patch: override llama_cpp.py with fix from unsloth-zoo main branch + # 6. Patch: override llama_cpp.py with fix from unsloth-zoo feature/llama-cpp-windows-support 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", + "https://raw.githubusercontent.com/unslothai/unsloth-zoo/refs/heads/feature/llama-cpp-windows-support/unsloth_zoo/llama_cpp.py", ) - # 7. Patch: override vision.py with fix from unsloth PR #4091 + # 7a. 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", ) + # 7b. Patch : override save.py with fix from feature/llama-cpp-windows-support + patch_package_file( + "unsloth", + os.path.join("unsloth", "save.py"), + "https://raw.githubusercontent.com/unslothai/unsloth/refs/heads/feature/llama-cpp-windows-support/unsloth/save.py", + ) + # 8. Studio dependencies pip_install( "Installing studio dependencies", From f190d5a16dec55fcbf225dc61561a9528af3ccf8 Mon Sep 17 00:00:00 2001 From: Roland Tannous Date: Tue, 3 Mar 2026 09:34:35 +0000 Subject: [PATCH 33/36] fix: make pip check non-fatal and install jedi for Colab compatibility --- setup.sh | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/setup.sh b/setup.sh index 8314ef6d75..9cc6f527bd 100755 --- a/setup.sh +++ b/setup.sh @@ -191,8 +191,14 @@ install_python_stack() { 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" + # Colab's bundled IPython 7.34 requires jedi but doesn't ship it + run_quiet "pip install jedi" pip install --no-cache-dir jedi run_quiet "patch single-env metadata" python "$SINGLE_ENV_PATCH" - run_quiet "pip check" pip check + # pip check can flag minor transitive-dependency version mismatches that + # don't actually break anything. Warn instead of aborting. + if ! pip check > /dev/null 2>&1; then + echo "⚠️ pip check reports dependency conflicts (safe to ignore)" + fi echo "✅ Python dependencies installed" } From a4d2853fbc71efe1ae0565de973b1d23bbc81502 Mon Sep 17 00:00:00 2001 From: Roland Tannous Date: Tue, 3 Mar 2026 17:03:01 +0000 Subject: [PATCH 34/36] fix: align llama-server binary discovery with upstream unsloth-zoo paths --- studio/backend/core/inference/llama_cpp.py | 53 +++++++++++++++++----- 1 file changed, 42 insertions(+), 11 deletions(-) diff --git a/studio/backend/core/inference/llama_cpp.py b/studio/backend/core/inference/llama_cpp.py index 09f8a2fcc9..f47b9132db 100644 --- a/studio/backend/core/inference/llama_cpp.py +++ b/studio/backend/core/inference/llama_cpp.py @@ -76,25 +76,51 @@ class LlamaCppBackend: Locate the llama-server binary. Search order: - 1. LLAMA_SERVER_PATH environment variable - 2. ~/.unsloth/llama.cpp/build/bin/llama-server (Linux, built by setup.sh) - 3. ~/.unsloth/llama.cpp/build/bin/Release/llama-server.exe (Windows, built by setup.ps1) - 4. ./llama.cpp/build/bin/llama-server (legacy: in-tree build) - 5. llama-server on PATH (system install) - 6. ./bin/llama-server (legacy: extracted binary) + 1. LLAMA_SERVER_PATH environment variable (direct path to binary) + 1b. UNSLOTH_LLAMA_CPP_PATH env var (custom llama.cpp install dir) + 2. ~/.unsloth/llama.cpp/llama-server (make build, root dir) + 3. ~/.unsloth/llama.cpp/build/bin/llama-server (cmake build, Linux) + 4. ~/.unsloth/llama.cpp/build/bin/Release/llama-server.exe (cmake build, Windows) + 5. ./llama.cpp/llama-server (legacy: make build, root dir) + 6. ./llama.cpp/build/bin/llama-server (legacy: cmake in-tree build) + 7. llama-server on PATH (system install) + 8. ./bin/llama-server (legacy: extracted binary) """ import os import sys binary_name = "llama-server.exe" if sys.platform == "win32" else "llama-server" - # 1. Env var + # 1. Env var — direct path to binary env_path = os.environ.get("LLAMA_SERVER_PATH") if env_path and Path(env_path).is_file(): return env_path - # 2. ~/.unsloth/llama.cpp (primary — setup.sh / setup.ps1 build here) + # 1b. UNSLOTH_LLAMA_CPP_PATH — custom llama.cpp install directory + custom_llama_cpp = os.environ.get("UNSLOTH_LLAMA_CPP_PATH") + if custom_llama_cpp: + custom_dir = Path(custom_llama_cpp) + # Root dir (make builds) + root_bin = custom_dir / binary_name + if root_bin.is_file(): + return str(root_bin) + # build/bin/ (cmake builds on Linux) + cmake_bin = custom_dir / "build" / "bin" / binary_name + if cmake_bin.is_file(): + return str(cmake_bin) + # build/bin/Release/ (cmake builds on Windows) + if sys.platform == "win32": + win_bin = custom_dir / "build" / "bin" / "Release" / binary_name + if win_bin.is_file(): + return str(win_bin) + + # 2–4. ~/.unsloth/llama.cpp (primary — setup.sh / setup.ps1 build here) unsloth_home = Path.home() / ".unsloth" / "llama.cpp" + # Root dir (make builds copy binaries here) + home_root = unsloth_home / binary_name + if home_root.is_file(): + return str(home_root) + # build/bin/ (cmake builds on Linux) home_linux = unsloth_home / "build" / "bin" / binary_name if home_linux.is_file(): return str(home_linux) @@ -105,8 +131,13 @@ class LlamaCppBackend: if home_win.is_file(): return str(home_win) - # 4. Legacy: in-tree build (older setup.sh / setup.ps1 versions) + # 5–6. Legacy: in-tree build (older setup.sh / setup.ps1 versions) project_root = Path(__file__).resolve().parents[4] + # Root dir (make builds) + root_path = project_root / "llama.cpp" / binary_name + if root_path.is_file(): + return str(root_path) + # build/bin/ (cmake builds) build_path = project_root / "llama.cpp" / "build" / "bin" / binary_name if build_path.is_file(): return str(build_path) @@ -115,12 +146,12 @@ class LlamaCppBackend: if win_path.is_file(): return str(win_path) - # 5. System PATH + # 7. System PATH system_path = shutil.which("llama-server") if system_path: return system_path - # 6. Legacy: extracted to bin/ + # 8. Legacy: extracted to bin/ bin_path = project_root / "bin" / binary_name if bin_path.is_file(): return str(bin_path) From 58b00db5cb17a8a2e7afad07422d37f346fe3f27 Mon Sep 17 00:00:00 2001 From: Roland Tannous Date: Tue, 3 Mar 2026 17:31:59 +0000 Subject: [PATCH 35/36] chore: add cross-platform Python installer with updated unsloth patch URLs --- install_python_stack.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/install_python_stack.py b/install_python_stack.py index 8f2b5fba8c..c9b73034b4 100644 --- a/install_python_stack.py +++ b/install_python_stack.py @@ -190,7 +190,7 @@ def install_python_stack() -> int: patch_package_file( "unsloth-zoo", os.path.join("unsloth_zoo", "llama_cpp.py"), - "https://raw.githubusercontent.com/unslothai/unsloth-zoo/refs/heads/feature/llama-cpp-windows-support/unsloth_zoo/llama_cpp.py", + "https://raw.githubusercontent.com/unslothai/unsloth-zoo/refs/heads/main/unsloth_zoo/llama_cpp.py", ) # 7a. Patch: override vision.py with fix from unsloth PR #4091 @@ -204,7 +204,7 @@ def install_python_stack() -> int: patch_package_file( "unsloth", os.path.join("unsloth", "save.py"), - "https://raw.githubusercontent.com/unslothai/unsloth/refs/heads/feature/llama-cpp-windows-support/unsloth/save.py", + "https://raw.githubusercontent.com/unslothai/unsloth/refs/heads/main/unsloth/save.py", ) # 8. Studio dependencies From 50b88bfb34cf0ffa35c58c8fbbe82de8e0217f8f Mon Sep 17 00:00:00 2001 From: Roland Tannous Date: Tue, 3 Mar 2026 18:42:35 +0000 Subject: [PATCH 36/36] Updated README --- README.md | 118 ++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 106 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 3d2810fae5..9656f1c634 100644 --- a/README.md +++ b/README.md @@ -30,28 +30,119 @@ ## Quick Start -### One-command setup +### Prerequisites + +| Requirement | Linux / WSL | Windows | +|---|---|---| +| **GPU** | NVIDIA GPU with working driver | NVIDIA GPU with working driver | +| **Python** | 3.11 – 3.13 | 3.11 – 3.13 | +| **Git** | Pre-installed on most distros | Auto-installed by setup script (via `winget`) | +| **CMake** | Pre-installed or `sudo apt install cmake` | Auto-installed by setup script (via `winget`) | +| **C++ compiler** | `build-essential` (auto-detected) | Visual Studio Build Tools 2022 (auto-installed by setup script) | +| **CUDA Toolkit** | Optional — setup auto-detects `nvcc` | Auto-installed by setup script (version matched to driver) | + +> [!NOTE] +> On **WSL**, the setup script will also run `sudo apt-get install build-essential cmake curl git libcurl4-openssl-dev` so that GGUF export works in non-interactive subprocesses. You may be prompted for your password during setup. + +--- + +### Linux / Windows WSL ```bash +# 1. Clone the repo +git clone https://github.com/unslothai/unsloth-studio.git +cd unsloth-studio + +# 2. Run setup (installs Node, builds frontend, creates .venv, builds llama.cpp) bash setup.sh + +# 3. Open a new terminal (or source your shell rc), then launch: +unsloth-studio -H 0.0.0.0 -p 8000 ``` -This script will: -1. Install **Node.js ≥ 20** via nvm (if needed) -2. Build the frontend to `studio/frontend/dist` -3. Create a Python virtual environment and install all dependencies (including `unsloth`) -4. Register a convenient `unsloth-ui` shell alias +
+What does setup.sh do? -### Launch the studio +1. Installs **Node.js ≥ 20** via nvm (if needed) +2. Runs `npm install && npm run build` for the React frontend +3. Detects the best **Python 3.11 – 3.13** on your system and creates a `.venv` +4. Installs all Python dependencies (unsloth, PyTorch with CUDA, triton kernels, etc.) +5. On **WSL**: pre-installs build dependencies via `apt-get` +6. Clones and builds **llama.cpp** at `~/.unsloth/llama.cpp` (GPU-accelerated if CUDA is found) +7. Registers `unsloth-studio` and `unsloth-ui` shell aliases in your shell rc (bash, zsh, fish, or ksh) + +
+ +--- + +### Windows (Native) + +> [!IMPORTANT] +> Requires an **NVIDIA GPU** — CPU-only machines are not supported on Windows. + +```powershell +# 1. Clone the repo +git clone https://github.com/unslothai/unsloth-studio.git +cd unsloth-studio + +# 2. Run setup (Right-click → "Run with PowerShell", or from a terminal): +.\setup.bat +# Or directly: +powershell -ExecutionPolicy Bypass -File setup.ps1 +``` + +After setup completes, **open a new terminal** and run: + +```powershell +# PowerShell +unsloth-studio -H 0.0.0.0 -p 8000 + +# Or cmd.exe +unsloth-studio -H 0.0.0.0 -p 8000 +``` + +
+What does setup.ps1 do? + +1. Enables **Windows Long Paths** (required for deep dependency trees — prompts for UAC) +2. Auto-installs missing system tools via `winget`: **Git**, **CMake**, **Visual Studio Build Tools 2022**, **CUDA Toolkit** (version-matched to your driver), **Node.js LTS**, **Python 3.12**, **OpenSSL dev** +3. Builds the React frontend (`npm install && npm run build`) +4. Creates a `.venv` and installs all Python dependencies (including CUDA-enabled PyTorch from the official index) +5. Sets `TORCHINDUCTOR_CACHE_DIR=C:\tc` to avoid Windows MAX_PATH issues with Triton +6. Clones and builds **llama.cpp** at `%USERPROFILE%\.unsloth\llama.cpp` with CUDA + Visual Studio +7. Registers `unsloth-studio` and `unsloth-ui` commands in both PowerShell profile and `cmd.exe` (via batch files on PATH) + +
+ +--- + +### Google Colab + +The setup script auto-detects Colab and installs everything into the existing system Python (no venv): + +```python +!bash setup.sh +``` + +--- + +### Launching the Studio + +After setup on any platform, the command is the same: ```bash -# After setup, open a new terminal (or source ~/.bashrc), then inside your working directory: -unsloth-ui -H 0.0.0.0 -p 8000 +unsloth-studio -H 0.0.0.0 -p 8000 ``` -On **first launch**, a one-time setup token is printed to the console. Use it in the browser to create your admin account. +| Flag | Description | +|---|---| +| `-H` / `--host` | Bind address (`0.0.0.0` for all interfaces, `127.0.0.1` for local only) | +| `-p` / `--port` | Port number (default: `8000`) | -As this repo is in continuous development, please make sure to run the setup.sh file everytime you pull new changes from the repo. +On **first launch**, a one-time setup token is printed to the console. Open the URL shown in your browser and use this token to create your admin account. + +> [!TIP] +> This repo is in active development. After pulling new changes, **always re-run the setup script** (`bash setup.sh` or `.\setup.bat`) to pick up dependency and build updates. ## API Reference @@ -105,7 +196,10 @@ new-ui-prototype/ │ ├── export.py │ ├── ui.py │ └── studio.py -├── setup.sh # One-command bootstrap script +├── setup.sh # Bootstrap script (Linux / WSL / Colab) +├── setup.ps1 # Bootstrap script (Windows native) +├── setup.bat # Wrapper to launch setup.ps1 via double-click +├── install_python_stack.py # Cross-platform Python dependency installer └── studio/ ├── backend/ │ ├── main.py # FastAPI app & middleware