Compare commits

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

10 commits

Author SHA1 Message Date
Daniel Han
67f0ab592e studio: add CUDA lib paths to LD_LIBRARY_PATH for llama-server
When llama-server is built with shared libs (setup.sh default),
it needs libcudart.so.12 and other CUDA runtime libs. Add
/usr/local/cuda/lib64 and targets path to LD_LIBRARY_PATH so
the server starts correctly even when CUDA isn't on the system path.
2026-03-17 01:12:55 +00:00
pre-commit-ci[bot]
12bc242ca2 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-03-16 15:04:09 +00:00
Daniel Han
eb15b31e3e studio: improve training progress messages for large datasets
Add granular status updates through the full preprocessing pipeline:
- "Downloading dataset: Open-Orca/OpenOrca..." before HF download
- "Downloaded Open-Orca/OpenOrca (4,233,923 rows)" after download
- "Formatting dataset (4,233,923 rows)..." before format step
- "Applying chat template to chatml_conversations (4,233,923 rows)..."
- "Dataset ready (4,233,923 samples, chatml_conversations format)"

Shows detected format name and row counts at each stage so users
can see progress through large dataset preprocessing instead of
a static "Loading and formatting dataset..." for minutes.
2026-03-16 15:03:58 +00:00
pre-commit-ci[bot]
251e3d2011 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-03-16 14:22:06 +00:00
Daniel Han
2b7a787075 studio: cross-platform llama.cpp builder (Linux, macOS, Windows)
- Linux: CUDA detection via /usr/local/cuda*, Ninja preferred
- macOS: Metal backend auto-enabled (llama.cpp default), no CUDA
- Windows: CUDA via CUDA_PATH env and toolkit dirs, VS generator
  fallback, binaries in build/bin/Release/, copy instead of symlink
- Reuse existing source (don't re-clone if CMakeLists.txt present)
- Both llama-server and llama-quantize verified and built
2026-03-16 14:21:54 +00:00
Roland Tannous
de7ddc704e fix: Resolve CUDA toolkit mismatch on multi-CUDA Windows systems (#4324)
* fix: prefer existing CUDA_PATH toolkit to avoid version mismatch on multi-CUDA systems

* fix: validate GPU arch support before accepting CUDA toolkit (sm_120 + CUDA 12.4 fallback)

* debug: add temporary CUDA compatibility check print

* fix: auto-copy CUDA VS integration files when missing (No CUDA toolset found)

* fix: return false when nvcc --list-gpu-arch unavailable (reject old toolkit, scan for newer)

* fix: re-sanitize CUDA env vars before cmake build (survives Refresh-Environment)

* fix: use --list-gpu-code (sm_*) instead of --list-gpu-arch (compute_*) for arch probing
2026-03-16 14:21:37 +00:00
pre-commit-ci[bot]
fc6110d491 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-03-16 14:15:26 +00:00
Daniel Han
d7a78599e6 studio: optimize llama.cpp build with static linking, ccache, ninja
Benchmarked BUILD_SHARED_LIBS ON vs OFF:
- Build time identical (~55s both on 192-core B200)
- Static: 75MB self-contained binary, no LD_LIBRARY_PATH needed
- Shared: 7.4MB + .so deps, needs lib path management
- Both work correctly

Optimizations applied to LlamaCppBuilder:
- BUILD_SHARED_LIBS=OFF: static binary, simpler deployment
- ccache: 27x faster rebuilds (55s -> 2s with warm cache)
- Ninja generator: parallel builds
- CMAKE_CUDA_ARCHITECTURES: build only for detected GPU arch
- CMAKE_CUDA_FLAGS=--threads=0: multi-threaded CUDA compilation
- GGML_NATIVE=ON: native CPU optimizations
- LLAMA_BUILD_TESTS=OFF, LLAMA_BUILD_EXAMPLES=OFF: skip unused targets
- LLAMA_BUILD_SERVER=ON: build only what we need
2026-03-16 14:15:14 +00:00
pre-commit-ci[bot]
056be824b2 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-03-16 14:02:50 +00:00
Daniel Han
34cdbf42bf studio: training progress visibility + deferred llama.cpp compilation
Training progress:
- Show row counts in status messages: "Loaded dataset from HuggingFace:
  Open-Orca/OpenOrca (4,233,923 rows)" instead of just the dataset name
- Emit "Formatting dataset (N rows)..." and "Applying chat template
  (N rows)..." status updates so users see progress during the
  preprocessing stages that previously appeared stuck

Deferred llama.cpp compilation:
- Add LlamaCppBuilder that runs cmake build in a background thread
  at server startup if the llama-server binary is missing
- Studio starts immediately and is usable for training/non-GGUF tasks
  while llama.cpp compiles in the background
- GGUF model loads wait for the build to finish with a helpful message
- Add /api/inference/llama-cpp-status endpoint for build status
- Frontend shows "Waiting for llama.cpp to compile..." toast when
  loading a GGUF while build is in progress
2026-03-16 14:02:20 +00:00
9 changed files with 601 additions and 22 deletions

View file

@ -888,9 +888,18 @@ class LlamaCppBackend:
env["PATH"] = ";".join(path_dirs) + ";" + existing_path
else:
# Linux: set LD_LIBRARY_PATH for shared libs next to the binary
# and CUDA runtime libs (libcudart, libcublas, etc.)
lib_dirs = [binary_dir]
for cuda_lib in [
"/usr/local/cuda/lib64",
"/usr/local/cuda/targets/x86_64-linux/lib",
]:
if os.path.isdir(cuda_lib):
lib_dirs.append(cuda_lib)
existing_ld = env.get("LD_LIBRARY_PATH", "")
new_ld = ":".join(lib_dirs)
env["LD_LIBRARY_PATH"] = (
f"{binary_dir}:{existing_ld}" if existing_ld else binary_dir
f"{new_ld}:{existing_ld}" if existing_ld else new_ld
)
# Pin to selected GPU(s) via CUDA_VISIBLE_DEVICES

View file

@ -0,0 +1,333 @@
# SPDX-License-Identifier: AGPL-3.0-only
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
"""
Background llama.cpp compilation.
On server startup, if the llama-server binary is not found, a background
thread clones and builds llama.cpp from source. Features that need
llama.cpp (GGUF chat, export, AI Assist) can await `wait_for_ready()`
which blocks until compilation finishes or fails.
Cross-platform: Linux, macOS, Windows.
"""
import os
import shutil
import subprocess
import sys
import threading
from pathlib import Path
from typing import Optional
import structlog
from loggers import get_logger
logger = get_logger(__name__)
_IS_WIN = sys.platform == "win32"
_IS_MAC = sys.platform == "darwin"
class LlamaCppBuilder:
"""Manages background llama.cpp compilation."""
def __init__(self):
self._ready = threading.Event()
self._building = False
self._error: Optional[str] = None
self._thread: Optional[threading.Thread] = None
self._binary_path: Optional[str] = None
@property
def is_ready(self) -> bool:
return self._ready.is_set()
@property
def is_building(self) -> bool:
return self._building
@property
def error(self) -> Optional[str]:
return self._error
@property
def binary_path(self) -> Optional[str]:
return self._binary_path
def check_and_build(self) -> None:
"""Check if llama-server exists. If not, start background build."""
from core.inference.llama_cpp import LlamaCppBackend
binary = LlamaCppBackend._find_llama_server_binary()
if binary:
self._binary_path = binary
self._ready.set()
logger.info(f"llama-server binary found: {binary}")
return
logger.info("llama-server binary not found, starting background build...")
self._building = True
self._thread = threading.Thread(
target = self._build, daemon = True, name = "llama-cpp-build"
)
self._thread.start()
def wait_for_ready(self, timeout: Optional[float] = None) -> bool:
"""Block until llama.cpp is ready. Returns True if ready, False on timeout."""
return self._ready.wait(timeout = timeout)
# ── Platform helpers ─────────────────────────────────────────
@staticmethod
def _find_nvcc() -> Optional[str]:
"""Find nvcc across platforms."""
nvcc = shutil.which("nvcc")
if nvcc:
return nvcc
if _IS_WIN:
# Windows: check CUDA_PATH env, then standard install dirs
cuda_path = os.environ.get("CUDA_PATH", "")
if cuda_path:
candidate = Path(cuda_path) / "bin" / "nvcc.exe"
if candidate.is_file():
return str(candidate)
toolkit_base = Path(r"C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA")
if toolkit_base.is_dir():
for d in sorted(toolkit_base.iterdir(), reverse = True):
candidate = d / "bin" / "nvcc.exe"
if candidate.is_file():
return str(candidate)
else:
# Linux: check standard locations
if Path("/usr/local/cuda/bin/nvcc").is_file():
return "/usr/local/cuda/bin/nvcc"
for cuda_dir in sorted(
Path("/usr/local").glob("cuda-*/bin/nvcc"), reverse = True
):
return str(cuda_dir)
return None
@staticmethod
def _detect_cuda_architectures() -> Optional[str]:
"""Detect GPU compute capabilities via nvidia-smi."""
nvidia_smi = "nvidia-smi"
if _IS_WIN:
# nvidia-smi is typically in System32 on Windows
win_path = Path(r"C:\Windows\System32\nvidia-smi.exe")
if win_path.is_file():
nvidia_smi = str(win_path)
try:
result = subprocess.run(
[nvidia_smi, "--query-gpu=compute_cap", "--format=csv,noheader"],
capture_output = True,
text = True,
timeout = 10,
)
if result.returncode == 0:
caps = set()
for line in result.stdout.strip().split("\n"):
cap = line.strip().replace(".", "")
if cap:
caps.add(cap)
if caps:
return ";".join(sorted(caps))
except Exception:
pass
return None
# ── Build ────────────────────────────────────────────────────
def _build(self) -> None:
"""Clone and build llama.cpp in the background."""
try:
llama_dir = Path.home() / ".unsloth" / "llama.cpp"
binary_name = "llama-server.exe" if _IS_WIN else "llama-server"
# Don't rebuild if binary appeared while we were starting
from core.inference.llama_cpp import LlamaCppBackend
binary = LlamaCppBackend._find_llama_server_binary()
if binary:
self._binary_path = binary
self._building = False
self._ready.set()
return
# Check prerequisites
if not shutil.which("cmake"):
self._error = "cmake not found. Install cmake to enable GGUF inference."
self._building = False
logger.warning(self._error)
return
if not shutil.which("git"):
self._error = "git not found. Install git to enable GGUF inference."
self._building = False
logger.warning(self._error)
return
# Clone (don't delete if already exists -- might be a partial build)
if not (llama_dir / "CMakeLists.txt").is_file():
if llama_dir.exists():
shutil.rmtree(llama_dir, ignore_errors = True)
logger.info("Cloning llama.cpp...")
subprocess.run(
[
"git",
"clone",
"--depth",
"1",
"https://github.com/ggml-org/llama.cpp.git",
str(llama_dir),
],
check = True,
capture_output = True,
)
else:
logger.info("llama.cpp source already present, building...")
# ── CMake arguments ──────────────────────────────────
cmake_args = [
"-DBUILD_SHARED_LIBS=OFF", # Self-contained binary
"-DGGML_NATIVE=ON", # Native CPU optimizations
"-DLLAMA_BUILD_TESTS=OFF", # Skip tests
"-DLLAMA_BUILD_EXAMPLES=OFF", # Skip examples
"-DLLAMA_BUILD_SERVER=ON", # Ensure server target
]
# ccache (27x faster rebuilds when available)
ccache = shutil.which("ccache")
if ccache:
cmake_args.extend(
[
f"-DCMAKE_C_COMPILER_LAUNCHER={ccache}",
f"-DCMAKE_CXX_COMPILER_LAUNCHER={ccache}",
f"-DCMAKE_CUDA_COMPILER_LAUNCHER={ccache}",
]
)
logger.info("Using ccache for faster compilation")
# ── GPU backend ──────────────────────────────────────
if _IS_MAC:
# macOS: Metal is enabled by default in llama.cpp, no extra flags needed
logger.info("macOS detected, Metal backend enabled by default")
else:
nvcc = self._find_nvcc()
if nvcc:
logger.info(f"CUDA detected: {nvcc}")
cmake_args.append("-DGGML_CUDA=ON")
# Build only for the detected GPU architecture(s)
archs = self._detect_cuda_architectures()
if archs:
cmake_args.append(f"-DCMAKE_CUDA_ARCHITECTURES={archs}")
# Multi-threaded CUDA compilation
cmake_args.append("-DCMAKE_CUDA_FLAGS=--threads=0")
else:
logger.info("No CUDA detected, building CPU-only llama.cpp")
# ── Generator ────────────────────────────────────────
generator_args = []
if _IS_WIN:
# Windows: prefer Ninja, fall back to VS generator
if shutil.which("ninja"):
generator_args = ["-G", "Ninja"]
# else: cmake will use default VS generator
else:
# Linux/Mac: prefer Ninja
if shutil.which("ninja"):
generator_args = ["-G", "Ninja"]
# ── Configure ────────────────────────────────────────
build_dir = llama_dir / "build"
logger.info("Configuring llama.cpp build...")
subprocess.run(
["cmake", "-B", str(build_dir), "-S", str(llama_dir)]
+ generator_args
+ cmake_args,
check = True,
capture_output = True,
)
# ── Build ────────────────────────────────────────────
logger.info("Building llama.cpp (this may take a few minutes)...")
subprocess.run(
[
"cmake",
"--build",
str(build_dir),
"--config",
"Release",
"--target",
"llama-server",
"llama-quantize",
"-j",
],
check = True,
capture_output = True,
)
# ── Verify binaries ──────────────────────────────────
# Windows VS generator puts binaries in build/bin/Release/
if _IS_WIN:
bin_dir = build_dir / "bin" / "Release"
if not (bin_dir / binary_name).is_file():
bin_dir = build_dir / "bin" # Ninja puts them here
else:
bin_dir = build_dir / "bin"
server_bin = bin_dir / binary_name
quantize_name = "llama-quantize.exe" if _IS_WIN else "llama-quantize"
quantize_bin = bin_dir / quantize_name
if server_bin.is_file():
self._binary_path = str(server_bin)
logger.info(f"llama-server built: {self._binary_path}")
else:
self._error = (
f"Build completed but llama-server not found at {server_bin}"
)
logger.error(self._error)
if quantize_bin.is_file():
logger.info(f"llama-quantize built: {quantize_bin}")
# Create symlink/copy for unsloth-zoo's check_llama_cpp()
quantize_link = llama_dir / quantize_name
if not quantize_link.exists():
try:
if _IS_WIN:
shutil.copy2(str(quantize_bin), str(quantize_link))
else:
quantize_link.symlink_to(quantize_bin)
except Exception:
pass
else:
logger.warning(f"llama-quantize not found at {quantize_bin}")
except subprocess.CalledProcessError as e:
stderr = (
e.stderr.decode() if isinstance(e.stderr, bytes) else e.stderr
) or ""
self._error = f"llama.cpp build failed: {stderr[-500:]}"
logger.error(self._error)
except Exception as e:
self._error = f"llama.cpp build error: {e}"
logger.error(self._error)
finally:
self._building = False
self._ready.set() # Unblock waiters even on failure
# ── Singleton ────────────────────────────────────────────────────
_builder: Optional[LlamaCppBuilder] = None
def get_llama_cpp_builder() -> LlamaCppBuilder:
global _builder
if _builder is None:
_builder = LlamaCppBuilder()
return _builder

View file

@ -2345,6 +2345,9 @@ class UnslothTrainer:
status_message = f"Streamed {len(dataset)} rows from HuggingFace"
)
else:
self._update_progress(
status_message = f"Downloading dataset: {dataset_source}..."
)
dataset = load_dataset(**load_kwargs)
# Check if stopped during dataset loading
@ -2352,11 +2355,12 @@ class UnslothTrainer:
logger.info("Stopped during dataset loading\n")
return None
n_rows = len(dataset) if hasattr(dataset, "__len__") else 0
self._update_progress(
status_message = f"Loaded dataset from HuggingFace: {dataset_source}"
status_message = f"Downloaded {dataset_source} ({n_rows:,} rows)"
)
logger.info(
f"Loaded dataset from Hugging Face: {dataset_source} ({len(dataset)} rows)\n"
f"Loaded dataset from Hugging Face: {dataset_source} ({n_rows:,} rows)\n"
)
# Resolve eval split from a separate HF split (explicit or auto-detected)
@ -2481,10 +2485,15 @@ class UnslothTrainer:
self._update_progress(error = error_msg)
return None
detected = dataset_info.get("detected_format", "unknown")
final_ds = dataset_info.get("dataset")
final_n = len(final_ds) if hasattr(final_ds, "__len__") else "?"
self._update_progress(
status_message = f"Dataset formatted and ready for training"
status_message = f"Dataset ready ({final_n:,} samples, {detected} format)"
)
logger.info(
f"Dataset formatted successfully ({final_n} samples, {detected})\n"
)
logger.info(f"Dataset formatted successfully\n")
# ========== THEN SPLIT ==========
if has_separate_eval_source and eval_dataset is not None:

View file

@ -75,6 +75,12 @@ async def lifespan(app: FastAPI):
threading.Thread(target = _precache, daemon = True).start()
# Build llama.cpp in the background if the binary is missing.
# Studio starts immediately; GGUF features wait for the build to finish.
from core.inference.llama_cpp_builder import get_llama_cpp_builder
get_llama_cpp_builder().check_and_build()
if storage.ensure_default_admin():
bootstrap_pw = storage.get_bootstrap_password()
app.state.bootstrap_password = bootstrap_pw

View file

@ -111,6 +111,20 @@ async def load_model(
# ── GGUF path: load via llama-server ──────────────────────
if config.is_gguf:
# Wait for llama.cpp to be compiled if a background build is in progress
from core.inference.llama_cpp_builder import get_llama_cpp_builder
builder = get_llama_cpp_builder()
if not builder.is_ready:
logger.info("Waiting for llama.cpp build to finish...")
ready = await asyncio.to_thread(builder.wait_for_ready, timeout = 600)
if not ready or builder.error:
raise HTTPException(
status_code = 503,
detail = builder.error
or "llama.cpp is still compiling. Please wait.",
)
llama_backend = get_llama_cpp_backend()
unsloth_backend = get_inference_backend()
@ -538,6 +552,19 @@ async def get_status(
raise HTTPException(status_code = 500, detail = f"Failed to get status: {str(e)}")
@router.get("/llama-cpp-status")
async def llama_cpp_status():
"""Check if llama.cpp is ready (built and available)."""
from core.inference.llama_cpp_builder import get_llama_cpp_builder
builder = get_llama_cpp_builder()
return {
"ready": builder.is_ready and builder.error is None,
"building": builder.is_building,
"error": builder.error,
}
# =====================================================================
# Audio (TTS) Generation (/audio/generate)
# =====================================================================

View file

@ -1092,6 +1092,9 @@ def format_and_template_dataset(
# LLM FLOW (Existing code)
else:
# Step 1: Format the dataset
n_rows = len(dataset) if hasattr(dataset, "__len__") else None
if progress_callback and n_rows:
progress_callback(status_message = f"Formatting dataset ({n_rows:,} rows)...")
dataset_info = format_dataset(
dataset,
format_type = format_type,
@ -1106,6 +1109,11 @@ def format_and_template_dataset(
)
# Step 2: Apply chat template
detected = dataset_info.get("detected_format", "unknown")
if progress_callback and n_rows:
progress_callback(
status_message = f"Applying chat template to {detected} ({n_rows:,} rows)..."
)
# Gemma emits a leading <bos> that must be stripped for text-only chatml/sharegpt.
is_alpaca = format_type == "alpaca" or (
format_type == "auto" and dataset_info["detected_format"] == "alpaca"

View file

@ -62,6 +62,15 @@ export async function getInferenceStatus(): Promise<InferenceStatusResponse> {
return parseJsonOrThrow<InferenceStatusResponse>(response);
}
export async function getLlamaCppStatus(): Promise<{
ready: boolean;
building: boolean;
error: string | null;
}> {
const response = await authFetch("/api/inference/llama-cpp-status");
return parseJsonOrThrow(response);
}
export async function loadModel(
payload: LoadModelRequest,
): Promise<LoadModelResponse> {

View file

@ -9,6 +9,7 @@ import {
getDownloadProgress,
getGgufDownloadProgress,
getInferenceStatus,
getLlamaCppStatus,
listLoras,
listModels,
loadModel,
@ -318,6 +319,25 @@ export function useChatModelRuntime() {
try {
async function performLoad(): Promise<void> {
if (abortCtrl.signal.aborted) throw new Error("Cancelled");
// For GGUF models, check if llama.cpp is still compiling
if (ggufVariant) {
try {
const llamaStatus = await getLlamaCppStatus();
if (llamaStatus.building) {
toast.info("Waiting for llama.cpp to compile...", {
description: "This is a one-time build. Studio is still usable for non-GGUF tasks.",
duration: 8000,
});
} else if (llamaStatus.error) {
throw new Error(llamaStatus.error);
}
} catch (e) {
if (e instanceof Error && e.message.includes("not found")) throw e;
// Endpoint might not exist on older backends, ignore
}
}
let previousWasUnloaded = false;
const currentCheckpoint =
useChatRuntimeStore.getState().params.checkpoint;

View file

@ -126,6 +126,42 @@ function Get-CudaComputeCapability {
return $null
}
# Check if an nvcc binary supports a given sm_ architecture.
# Uses `nvcc --list-gpu-code` which outputs sm_* tokens (--list-gpu-arch
# outputs compute_* tokens instead). Available since CUDA 11.6.
# Returns $false if the flag isn't supported (old toolkit) — safer to reject
# and fall back to scanning/PTX than to assume support and fail later.
function Test-NvccArchSupport {
param([string]$NvccExe, [string]$Arch)
try {
$listCode = & $NvccExe --list-gpu-code 2>&1 | Out-String
if ($LASTEXITCODE -ne 0) { return $false }
return ($listCode -match "sm_$Arch")
} catch {
return $false
}
}
# Given an nvcc binary, return the highest sm_ architecture it supports.
# Returns e.g. "90" for CUDA 12.4. Returns $null if detection fails.
function Get-NvccMaxArch {
param([string]$NvccExe)
try {
$listCode = & $NvccExe --list-gpu-code 2>&1 | Out-String
if ($LASTEXITCODE -ne 0) { return $null }
$arches = @()
foreach ($line in $listCode -split "`n") {
if ($line.Trim() -match '^sm_(\d+)') {
$arches += [int]$Matches[1]
}
}
if ($arches.Count -gt 0) {
return ($arches | Sort-Object | Select-Object -Last 1).ToString()
}
} 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
@ -368,19 +404,76 @@ try {
}
} catch {}
# -- Find a toolkit that's compatible with the driver --
# Detect compute capability early so we can validate toolkit support
$CudaArch = Get-CudaComputeCapability
if ($CudaArch) {
Write-Host " GPU Compute Capability = $($CudaArch.Insert($CudaArch.Length-1, '.')) (sm_$CudaArch)" -ForegroundColor Gray
}
# -- Find a toolkit that's compatible with the driver AND the GPU --
# Strategy: prefer the toolkit at CUDA_PATH (user's existing setup) if it's
# compatible with the driver AND supports the GPU architecture. Only fall back
# to scanning side-by-side installs if CUDA_PATH is missing, points to an
# incompatible version, or can't compile for the GPU. This avoids
# header/binary mismatches when multiple toolkits are installed.
$IncompatibleToolkit = $null
$NvccPath = $null
if ($DriverMaxCuda) {
$NvccPath = Find-Nvcc -MaxVersion $DriverMaxCuda
if ($NvccPath) {
Write-Host " [OK] Found compatible CUDA Toolkit (nvcc: $NvccPath)" -ForegroundColor Green
} else {
# 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]
$drMajorCuda = [int]$DriverMaxCuda.Split('.')[0]
$drMinorCuda = [int]$DriverMaxCuda.Split('.')[1]
# --- Step 1: Check existing CUDA_PATH first ---
$existingCudaPath = [Environment]::GetEnvironmentVariable('CUDA_PATH', 'Machine')
if (-not $existingCudaPath) {
$existingCudaPath = [Environment]::GetEnvironmentVariable('CUDA_PATH', 'User')
}
if ($existingCudaPath -and (Test-Path (Join-Path $existingCudaPath 'bin\nvcc.exe'))) {
$candidateNvcc = Join-Path $existingCudaPath 'bin\nvcc.exe'
$verOut = & $candidateNvcc --version 2>&1 | Out-String
if ($verOut -match 'release\s+(\d+)\.(\d+)') {
$tkMaj = [int]$Matches[1]; $tkMin = [int]$Matches[2]
$isCompat = ($tkMaj -lt $drMajorCuda) -or ($tkMaj -eq $drMajorCuda -and $tkMin -le $drMinorCuda)
if ($isCompat) {
# Also verify the toolkit supports our GPU architecture
Write-Host " [DEBUG] Checking CUDA compatibility: toolkit=$tkMaj.$tkMin arch=sm_$CudaArch" -ForegroundColor Magenta
$archOk = $true
if ($CudaArch) {
$archOk = Test-NvccArchSupport -NvccExe $candidateNvcc -Arch $CudaArch
if (-not $archOk) {
Write-Host " [INFO] CUDA_PATH toolkit (CUDA $tkMaj.$tkMin) does not support GPU arch sm_$CudaArch" -ForegroundColor Yellow
Write-Host " Looking for a newer toolkit..." -ForegroundColor Yellow
}
}
if ($archOk) {
$NvccPath = $candidateNvcc
Write-Host " [OK] Using existing CUDA Toolkit at CUDA_PATH (nvcc: $NvccPath)" -ForegroundColor Green
}
} else {
Write-Host " [INFO] CUDA_PATH ($existingCudaPath) has CUDA $tkMaj.$tkMin which exceeds driver max $DriverMaxCuda" -ForegroundColor Yellow
}
}
}
# --- Step 2: Fall back to scanning side-by-side installs ---
if (-not $NvccPath) {
$NvccPath = Find-Nvcc -MaxVersion $DriverMaxCuda
if ($NvccPath) {
Write-Host " [OK] Found compatible CUDA Toolkit (nvcc: $NvccPath)" -ForegroundColor Green
if ($existingCudaPath) {
$selectedRoot = Split-Path (Split-Path $NvccPath -Parent) -Parent
if ($existingCudaPath.TrimEnd('\') -ne $selectedRoot.TrimEnd('\')) {
Write-Host " [INFO] Overriding CUDA_PATH from $existingCudaPath to $selectedRoot" -ForegroundColor Yellow
}
}
} else {
# 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]
}
}
}
}
@ -487,6 +580,19 @@ $CudaToolkitRoot = Split-Path (Split-Path $NvccPath -Parent) -Parent
# 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
# Clear all versioned CUDA_PATH_V* env vars in this process to prevent
# cmake/MSBuild from discovering a conflicting CUDA installation.
$cudaPathVars = @([Environment]::GetEnvironmentVariables('Process').Keys | Where-Object { $_ -match '^CUDA_PATH_V' })
foreach ($v in $cudaPathVars) {
[Environment]::SetEnvironmentVariable($v, $null, 'Process')
}
# Set only the versioned var matching the selected toolkit (e.g. CUDA_PATH_V13_0)
$tkDirName = Split-Path $CudaToolkitRoot -Leaf
if ($tkDirName -match '^v(\d+)\.(\d+)') {
$cudaPathVerVar = "CUDA_PATH_V$($Matches[1])_$($Matches[2])"
[Environment]::SetEnvironmentVariable($cudaPathVerVar, $CudaToolkitRoot, 'Process')
Write-Host " Set $cudaPathVerVar (cleared other CUDA_PATH_V* vars)" -ForegroundColor Gray
}
# Ensure nvcc's bin dir is on PATH for this process
$nvccBinDir = Split-Path $NvccPath -Parent
if ($env:PATH -notlike "*$nvccBinDir*") {
@ -503,15 +609,38 @@ if (-not $userPath -or $userPath -notlike "*$nvccBinDir*") {
Write-Host " Persisted CUDA bin dir to user PATH" -ForegroundColor Gray
}
# -- Ensure CUDA ↔ Visual Studio integration files exist --
# When CUDA is installed before VS Build Tools (or VS is reinstalled after CUDA),
# the MSBuild .targets/.props files that let VS compile .cu files are missing.
# cmake fails with "No CUDA toolset found". Fix: copy from CUDA extras dir.
if ($VsInstallPath -and $CudaToolkitRoot) {
$vsCustomizations = Join-Path $VsInstallPath "MSBuild\Microsoft\VC\v170\BuildCustomizations"
$cudaExtras = Join-Path $CudaToolkitRoot "extras\visual_studio_integration\MSBuildExtensions"
if ((Test-Path $cudaExtras) -and (Test-Path $vsCustomizations)) {
$hasTargets = Get-ChildItem $vsCustomizations -Filter "CUDA *.targets" -ErrorAction SilentlyContinue
if (-not $hasTargets) {
Write-Host " [INFO] CUDA VS integration missing -- copying .targets files..." -ForegroundColor Yellow
try {
Copy-Item "$cudaExtras\*" $vsCustomizations -Force -ErrorAction Stop
Write-Host " [OK] CUDA VS integration files installed" -ForegroundColor Green
} catch {
Write-Host " [WARN] Could not copy CUDA VS integration files (may need admin)" -ForegroundColor Yellow
Write-Host " Manual fix: copy contents of" -ForegroundColor Yellow
Write-Host " $cudaExtras" -ForegroundColor Cyan
Write-Host " into:" -ForegroundColor Yellow
Write-Host " $vsCustomizations" -ForegroundColor Cyan
}
}
}
}
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 {
# $CudaArch was detected earlier (before toolkit selection) so it could
# influence which toolkit we picked. Just log the final state here.
if (-not $CudaArch) {
Write-Host " [WARN] Could not detect compute capability -- cmake will use defaults" -ForegroundColor Yellow
}
@ -875,6 +1004,21 @@ if (Test-Path $LlamaServerBin) {
$BuildOk = $true
$FailedStep = ""
# Re-sanitize CUDA_PATH_V* vars — Refresh-Environment (called during
# Node/Python installs above) may have repopulated conflicting versioned
# vars from the Machine registry.
$cudaPathVars2 = @([Environment]::GetEnvironmentVariables('Process').Keys | Where-Object { $_ -match '^CUDA_PATH_V' })
foreach ($v2 in $cudaPathVars2) {
[Environment]::SetEnvironmentVariable($v2, $null, 'Process')
}
$tkDirName2 = Split-Path $CudaToolkitRoot -Leaf
if ($tkDirName2 -match '^v(\d+)\.(\d+)') {
[Environment]::SetEnvironmentVariable("CUDA_PATH_V$($Matches[1])_$($Matches[2])", $CudaToolkitRoot, 'Process')
}
# Also re-assert CUDA_PATH and CudaToolkitDir in case they were overwritten
[Environment]::SetEnvironmentVariable('CUDA_PATH', $CudaToolkitRoot, 'Process')
[Environment]::SetEnvironmentVariable('CudaToolkitDir', "$CudaToolkitRoot\", 'Process')
# -- Step A: Clone or pull llama.cpp --
if (Test-Path (Join-Path $LlamaCppDir ".git")) {
@ -921,6 +1065,7 @@ if (Test-Path $LlamaServerBin) {
# CUDA flags (Unsloth-aligned)
$CmakeArgs += '-DGGML_CUDA=ON'
$CmakeArgs += "-DCUDAToolkit_ROOT=$CudaToolkitRoot"
$CmakeArgs += "-DCUDA_TOOLKIT_ROOT_DIR=$CudaToolkitRoot"
$CmakeArgs += "-DCMAKE_CUDA_COMPILER=$NvccPath"
$CmakeArgs += '-DGGML_CUDA_FA_ALL_QUANTS=ON'
$CmakeArgs += '-DGGML_CUDA_F16=OFF'
@ -928,7 +1073,20 @@ if (Test-Path $LlamaServerBin) {
$CmakeArgs += '-DGGML_CUDA_FORCE_CUBLAS=OFF'
$CmakeArgs += '-DGGML_CUDA_PEER_MAX_BATCH_SIZE=8192'
if ($CudaArch) {
$CmakeArgs += "-DCMAKE_CUDA_ARCHITECTURES=$CudaArch"
# Validate nvcc actually supports this architecture
if (Test-NvccArchSupport -NvccExe $NvccPath -Arch $CudaArch) {
$CmakeArgs += "-DCMAKE_CUDA_ARCHITECTURES=$CudaArch"
} else {
# GPU arch too new for this toolkit — fall back to highest supported.
# PTX forward-compatibility will JIT-compile for the actual GPU at runtime.
$maxArch = Get-NvccMaxArch -NvccExe $NvccPath
if ($maxArch) {
$CmakeArgs += "-DCMAKE_CUDA_ARCHITECTURES=$maxArch"
Write-Host " [WARN] GPU is sm_$CudaArch but nvcc only supports up to sm_$maxArch" -ForegroundColor Yellow
Write-Host " Building with sm_$maxArch (PTX will JIT for your GPU at runtime)" -ForegroundColor Yellow
}
# else: omit flag entirely, let cmake pick defaults
}
}
cmake @CmakeArgs 2>&1 | Out-Null