From b7e9dae027aec338c0017cf6143f099cd64dfab0 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Tue, 2 Jun 2026 03:43:50 -0700 Subject: [PATCH 01/95] feat(install): Windows-on-Arm + NVIDIA WSL2 fallback; glibc>=2.41/CUDA<13.3 build diagnostic Two independent, purely-additive changes for Grace-Blackwell aarch64 (NVIDIA N1X "RTX Spark" and DGX-Spark-class) and new-glibc hosts. 78 insertions, 0 deletions. studio/setup.sh: when the GPU llama.cpp source build runs on glibc >= 2.41 with a CUDA toolkit < 13.3, nvcc fails on the rsqrt/rsqrtf exception- spec clash (fixed upstream in CUDA 13.3 via _NV_RSQRT_SPECIFIER) and the build silently falls back to CPU. Add a clear diagnostic recommending CUDA >= 13.3. Diagnostic only, strictly inside the existing NVIDIA CUDA branch (Metal/ROCm/CPU/x86 unaffected). install.ps1: native Windows-ARM64 has no CUDA PyTorch / Triton wheels, so the native install can't deliver GPU. When ARM64 + NVIDIA is detected, automatically set up WSL2 and run the Linux installer there (full GPU), then print the launch command. Strictly gated on ARM64 && NVIDIA && not --no-torch; x86_64 Windows (NVIDIA/AMD) and ARM64-without-NVIDIA are byte-for-byte unchanged. Opt out: UNSLOTH_NO_WSL_FALLBACK=1; distro: UNSLOTH_WSL_DISTRO. Encoding-proof distro detection via 'wsl -d -- true'. Co-Authored-By: Claude Opus 4.8 --- install.ps1 | 76 +++++++++++++++++++++++++++++++++++++++++++++++++ studio/setup.sh | 20 +++++++++++++ 2 files changed, 96 insertions(+) diff --git a/install.ps1 b/install.ps1 index 47c72bcdc1..d836d2ad69 100644 --- a/install.ps1 +++ b/install.ps1 @@ -1457,6 +1457,82 @@ shell.Run cmd, 0, False } $TorchIndexUrl = Get-TorchIndexUrl + # ===== Windows-on-ARM + NVIDIA GPU -> automatic WSL2 fallback (N1X "RTX Spark" / DGX Spark-class) ===== + # Native Windows-ARM64 has no CUDA PyTorch wheel and no Triton wheel for win_arm64, so the GPU + # training/inference stack cannot run natively. When an NVIDIA GPU is present on ARM64, transparently + # set up the supported path for an average user: enable/install WSL2 and run the Linux installer there + # (full GPU). STRICTLY gated on ARM64 + NVIDIA -> normal x86_64 Windows (NVIDIA or AMD) and + # ARM64-without-NVIDIA are byte-for-byte unaffected and continue the native install below. + # Opt out with UNSLOTH_NO_WSL_FALLBACK=1; choose the distro with UNSLOTH_WSL_DISTRO. + try { $_winArm64 = ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString() -ieq 'Arm64') } catch { $_winArm64 = $false } + if ($_winArm64 -and $HasNvidiaSmi -and (-not $SkipTorch) -and ($env:UNSLOTH_NO_WSL_FALLBACK -ne '1')) { + step "wsl" "Windows on ARM + NVIDIA detected -- routing GPU setup through WSL2 (supported path)" + substep "native Windows-ARM64 has no CUDA PyTorch/Triton yet; WSL2 delivers full GPU." "Yellow" + + $wslReady = $false + if (Get-Command wsl.exe -ErrorAction SilentlyContinue) { + try { & wsl.exe --status *> $null; if ($LASTEXITCODE -eq 0) { $wslReady = $true } } catch {} + } + + if (-not $wslReady) { + # Enabling WSL2 is a one-time operation that requires admin + a reboot. + $isAdmin = $false + try { $isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator) } catch {} + step "wsl" "WSL2 isn't enabled yet -- one-time setup (needs admin + reboot)" "Yellow" + if ($isAdmin) { + substep "enabling WSL2..." "Cyan" + try { & wsl.exe --install --no-launch } catch {} + substep "WSL2 enabled. REBOOT, then re-run: irm https://unsloth.ai/install.ps1 | iex" "Green" + } else { + substep "in an ADMINISTRATOR PowerShell run: wsl --install" "Cyan" + substep "reboot, then re-run: irm https://unsloth.ai/install.ps1 | iex" "Cyan" + } + return + } + + $distro = if ($env:UNSLOTH_WSL_DISTRO) { $env:UNSLOTH_WSL_DISTRO } else { "Ubuntu-24.04" } + # Detect the distro by exit code (encoding-proof; wsl --list emits UTF-16 that PS mis-parses). + $haveDistro = $false + try { & wsl.exe -d $distro -- true *> $null; if ($LASTEXITCODE -eq 0) { $haveDistro = $true } } catch {} + if (-not $haveDistro) { + substep "installing WSL distro '$distro' (first time only)..." "Cyan" + try { & wsl.exe --install -d $distro --no-launch } catch {} + } + substep "installing Unsloth Studio inside WSL '$distro' with full GPU (this downloads PyTorch)..." "Cyan" + $wslInstall = 'export DEBIAN_FRONTEND=noninteractive; apt-get update -y >/dev/null 2>&1; apt-get install -y build-essential cmake git curl pciutils >/dev/null 2>&1; curl -fsSL https://unsloth.ai/install.sh | sh' + # install.sh writes diagnostics to stderr and may exit non-zero on the optional llama.cpp + # prebuilt step (no aarch64 prebuilt exists) -- that must NOT abort us under -ErrorAction Stop, + # since torch + unsloth + Studio still install. Lower EAP around the call (same idiom as above). + $prevEapWsl = $ErrorActionPreference + $ErrorActionPreference = "Continue" + try { + & wsl.exe -d $distro -u root -- bash -lc $wslInstall + $wslRc = $LASTEXITCODE + } finally { + $ErrorActionPreference = $prevEapWsl + } + Write-Host "" + # The optional llama.cpp prebuilt step exits non-zero on aarch64 (no prebuilt) even when + # torch + unsloth + Studio installed fine -- so verify torch.cuda directly instead of trusting $wslRc. + $torchOk = $false + $prevEapChk = $ErrorActionPreference + $ErrorActionPreference = "Continue" + try { + & wsl.exe -d $distro -u root -- /root/.unsloth/studio/unsloth_studio/bin/python -c "import torch,sys; sys.exit(0 if torch.cuda.is_available() else 3)" *> $null + $torchOk = ($LASTEXITCODE -eq 0) + } catch {} finally { $ErrorActionPreference = $prevEapChk } + if ($torchOk) { + step "done" "Unsloth Studio installed in WSL '$distro' -- GPU ready (torch.cuda available)." "Green" + } else { + step "wsl" "WSL Studio install did not finish cleanly (torch.cuda not detected; inner exit $wslRc) -- see log above." "Yellow" + } + substep "Launch it from Windows (then open http://localhost:8888):" "Cyan" + substep " wsl -d $distro -u root -- bash -lic 'unsloth studio -p 8888'" "Cyan" + substep "GPU training + GGUF export run inside WSL. (GGUF *inference* additionally needs a CUDA llama.cpp build.)" "Yellow" + if ($torchOk) { $global:LASTEXITCODE = 0 } + return + } + # ── GPU arch → newest compatible Windows ROCm wheel release ── # Wheels bundle their own ROCm runtime; the installed HIP SDK version does # not constrain which release to use. Always picks the newest release that diff --git a/studio/setup.sh b/studio/setup.sh index 9b29def859..309e3c4460 100755 --- a/studio/setup.sh +++ b/studio/setup.sh @@ -1164,6 +1164,26 @@ else else CMAKE_ARGS="$CMAKE_ARGS -DGGML_CUDA=ON" + # glibc >= 2.41 added rsqrt()/rsqrtf() (gated by __GLIBC_USE(IEC_60559_FUNCS_EXT_C23), + # which g++ enables via _GNU_SOURCE). CUDA Toolkits < 13.3 declare these in + # without a matching exception specifier -> every .cu fails + # "exception specification is incompatible", and the GPU build silently drops to CPU. + # -allow-unsupported-compiler does NOT fix this (header clash, not the GNU-version + # #error); no host gcc avoids it. NVIDIA fixed it in CUDA 13.3 (_NV_RSQRT_SPECIFIER). + # Diagnostic only: never changes flags / never aborts -> cannot regress any platform. + _GLIBC_VER="$(getconf GNU_LIBC_VERSION 2>/dev/null | awk '{print $2}')" || _GLIBC_VER="" + if [ -n "$_GLIBC_VER" ]; then + _GLIBC_MAJ="${_GLIBC_VER%%.*}"; _GLIBC_MIN="${_GLIBC_VER#*.}"; _GLIBC_MIN="${_GLIBC_MIN%%.*}" + _CU_MAJ="${_NVCC_VER%%.*}"; _CU_MIN="${_NVCC_VER#*.}"; _CU_MIN="${_CU_MIN%%.*}" + if [ "${_GLIBC_MAJ:-0}" -eq 2 ] 2>/dev/null && [ "${_GLIBC_MIN:-0}" -ge 41 ] 2>/dev/null \ + && { [ "${_CU_MAJ:-0}" -lt 13 ] 2>/dev/null \ + || { [ "${_CU_MAJ:-0}" -eq 13 ] 2>/dev/null && [ "${_CU_MIN:-0}" -lt 3 ] 2>/dev/null; }; }; then + substep "CUDA toolkit ${_NVCC_VER} is incompatible with glibc ${_GLIBC_VER} (rsqrt/rsqrtf header clash)." "$C_ERR" + substep "the GPU build will fail to compile and fall back to CPU -- install CUDA Toolkit >= 13.3:" "$C_WARN" + substep "https://developer.nvidia.com/cuda-downloads (setup.sh auto-selects the newest /usr/local/cuda-*)" "$C_WARN" + fi + fi + CUDA_ARCHS="" if command -v nvidia-smi &>/dev/null; then _raw_caps=$(nvidia-smi --query-gpu=compute_cap --format=csv,noheader 2>/dev/null || true) From 8fbd8e3fa27140a9fe678b385419f371cfef6c25 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Tue, 2 Jun 2026 04:13:12 -0700 Subject: [PATCH 02/95] Harden Windows-on-Arm WSL fallback: native-CUDA probe + WSL-forwarding unsloth shim - Future-proof: probe whether a CUDA torch wheel is installable natively for win_arm64 (uv pip install --dry-run). If it resolves (NVIDIA ships the wheel) keep the NATIVE install; otherwise fall back to WSL. WSL is used ONLY when native genuinely can't. - Create a native Windows unsloth.cmd shim (on user PATH) that forwards every "unsloth ..." into the WSL GPU env, so "unsloth studio" / "unsloth studio run" typed in PowerShell run inside WSL and stream output + the http://localhost:8888 URL back. - Run the WSL install under Continue-EAP and verify torch.cuda before reporting success so the optional (aarch64) llama.cpp prebuilt failure cannot abort or mis-report. Co-Authored-By: Claude Opus 4.8 --- install.ps1 | 52 +++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 43 insertions(+), 9 deletions(-) diff --git a/install.ps1 b/install.ps1 index d836d2ad69..79bcbe0e39 100644 --- a/install.ps1 +++ b/install.ps1 @@ -1459,15 +1459,27 @@ shell.Run cmd, 0, False # ===== Windows-on-ARM + NVIDIA GPU -> automatic WSL2 fallback (N1X "RTX Spark" / DGX Spark-class) ===== # Native Windows-ARM64 has no CUDA PyTorch wheel and no Triton wheel for win_arm64, so the GPU - # training/inference stack cannot run natively. When an NVIDIA GPU is present on ARM64, transparently - # set up the supported path for an average user: enable/install WSL2 and run the Linux installer there - # (full GPU). STRICTLY gated on ARM64 + NVIDIA -> normal x86_64 Windows (NVIDIA or AMD) and - # ARM64-without-NVIDIA are byte-for-byte unaffected and continue the native install below. + # stack can't run natively today. When an NVIDIA GPU is present on ARM64 AND native CUDA PyTorch is + # NOT installable for this platform, set up the supported path: enable/install WSL2, run the Linux + # installer there (full GPU), and create a Windows `unsloth` shim that forwards into WSL. + # STRICTLY gated -> normal x86_64 Windows (NVIDIA or AMD) and ARM64-without-NVIDIA are byte-for-byte + # unaffected and continue the native install below. FUTURE-PROOF: if NVIDIA ships a win_arm64 CUDA + # torch wheel, the probe below passes and the native install is kept automatically. # Opt out with UNSLOTH_NO_WSL_FALLBACK=1; choose the distro with UNSLOTH_WSL_DISTRO. try { $_winArm64 = ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString() -ieq 'Arm64') } catch { $_winArm64 = $false } - if ($_winArm64 -and $HasNvidiaSmi -and (-not $SkipTorch) -and ($env:UNSLOTH_NO_WSL_FALLBACK -ne '1')) { - step "wsl" "Windows on ARM + NVIDIA detected -- routing GPU setup through WSL2 (supported path)" - substep "native Windows-ARM64 has no CUDA PyTorch/Triton yet; WSL2 delivers full GPU." "Yellow" + $_nativeCudaTorchOk = $false + if ($_winArm64 -and $HasNvidiaSmi -and (-not $SkipTorch)) { + # Future-proof check: can a CUDA-capable torch wheel be resolved natively for this platform/index? + $prevEapProbe = $ErrorActionPreference; $ErrorActionPreference = "Continue" + try { + & uv pip install --python $VenvPython --dry-run torch --index-url $TorchIndexUrl *> $null + $_nativeCudaTorchOk = ($LASTEXITCODE -eq 0) + } catch { $_nativeCudaTorchOk = $false } finally { $ErrorActionPreference = $prevEapProbe } + if ($_nativeCudaTorchOk) { step "gpu" "native CUDA PyTorch now available for win_arm64 -- keeping native install" "Green" } + } + if ($_winArm64 -and $HasNvidiaSmi -and (-not $_nativeCudaTorchOk) -and (-not $SkipTorch) -and ($env:UNSLOTH_NO_WSL_FALLBACK -ne '1')) { + step "wsl" "Windows on ARM + NVIDIA, native CUDA unavailable -- routing GPU setup through WSL2" + substep "no win_arm64 CUDA PyTorch/Triton yet; WSL2 delivers full GPU (DGX Spark / RTX Spark path)." "Yellow" $wslReady = $false if (Get-Command wsl.exe -ErrorAction SilentlyContinue) { @@ -1523,11 +1535,33 @@ shell.Run cmd, 0, False } catch {} finally { $ErrorActionPreference = $prevEapChk } if ($torchOk) { step "done" "Unsloth Studio installed in WSL '$distro' -- GPU ready (torch.cuda available)." "Green" + # Native Windows `unsloth` shim: forward every `unsloth ...` into the WSL GPU env so the user + # never has to touch WSL. `unsloth studio` runs inside WSL and streams output + URL back here; + # WSL2 forwards 127.0.0.1, so http://localhost:8888 works in the Windows browser. + try { + $shimDir = Join-Path $env:LOCALAPPDATA "Unsloth\bin" + New-Item -ItemType Directory -Force -Path $shimDir *> $null + $shimLines = @( + '@echo off', + "wsl.exe -d $distro -u root -- /root/.unsloth/studio/unsloth_studio/bin/unsloth %*" + ) + Set-Content -LiteralPath (Join-Path $shimDir "unsloth.cmd") -Value $shimLines -Encoding ASCII + $userPath = [Environment]::GetEnvironmentVariable("Path", "User") + if (($userPath -split ';') -notcontains $shimDir) { + [Environment]::SetEnvironmentVariable("Path", ($userPath.TrimEnd(';') + ";" + $shimDir), "User") + } + $env:Path = $env:Path.TrimEnd(';') + ";" + $shimDir + step "shim" "created native 'unsloth' command -> forwards to WSL '$distro'" "Green" + substep "open a NEW terminal, then (no WSL knowledge needed):" "Cyan" + substep " unsloth studio # runs in WSL; opens http://localhost:8888" "Cyan" + substep " unsloth studio run # also forwarded into WSL" "Cyan" + } catch { + substep "(shim creation failed; launch manually): wsl -d $distro -u root -- bash -lic 'unsloth studio -p 8888'" "Yellow" + } } else { step "wsl" "WSL Studio install did not finish cleanly (torch.cuda not detected; inner exit $wslRc) -- see log above." "Yellow" + substep "retry, or launch manually: wsl -d $distro -u root -- bash -lic 'unsloth studio -p 8888'" "Cyan" } - substep "Launch it from Windows (then open http://localhost:8888):" "Cyan" - substep " wsl -d $distro -u root -- bash -lic 'unsloth studio -p 8888'" "Cyan" substep "GPU training + GGUF export run inside WSL. (GGUF *inference* additionally needs a CUDA llama.cpp build.)" "Yellow" if ($torchOk) { $global:LASTEXITCODE = 0 } return From bb9b7341de4f9f87bcae4cae530fc9c7379a7c7c Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Tue, 2 Jun 2026 04:23:46 -0700 Subject: [PATCH 03/95] WSL fallback: create Desktop + Start Menu shortcuts that launch WSL Studio The fallback returns before install.ps1's native shortcut code, so it created no shortcuts. Add a WSL launcher (launch-studio-wsl.ps1) plus Desktop and Start Menu .lnk shortcuts that start `unsloth studio` inside WSL and open http://localhost:8888 in the browser once the backend is healthy. Best-effort + wrapped in try/catch so a shortcut failure never aborts the install. Co-Authored-By: Claude Opus 4.8 --- install.ps1 | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/install.ps1 b/install.ps1 index 79bcbe0e39..d9063e832c 100644 --- a/install.ps1 +++ b/install.ps1 @@ -1558,6 +1558,38 @@ shell.Run cmd, 0, False } catch { substep "(shim creation failed; launch manually): wsl -d $distro -u root -- bash -lic 'unsloth studio -p 8888'" "Yellow" } + # Desktop + Start Menu shortcuts: launch the WSL Studio and open the browser when ready. + try { + $appDir = Join-Path $env:LOCALAPPDATA "Unsloth" + New-Item -ItemType Directory -Force -Path $appDir *> $null + $launcher = Join-Path $appDir "launch-studio-wsl.ps1" + $L = @( + '$ErrorActionPreference = "SilentlyContinue"', + ('$distro = "' + $distro + '"'), + 'Start-Job { for ($i=0; $i -lt 120; $i++) { try { if ((Invoke-WebRequest "http://localhost:8888/api/health" -UseBasicParsing -TimeoutSec 2).StatusCode -eq 200) { Start-Process "http://localhost:8888"; break } } catch {}; Start-Sleep 1 } } | Out-Null', + 'Write-Host "Starting Unsloth Studio in WSL ($distro); browser opens at http://localhost:8888 when ready (Ctrl+C to stop)..."', + 'wsl.exe -d $distro -u root -- bash -lic "unsloth studio -p 8888"' + ) + Set-Content -LiteralPath $launcher -Value $L -Encoding UTF8 + $icon = Join-Path $appDir "unsloth.ico" + try { if (-not (Test-Path -LiteralPath $icon)) { Invoke-WebRequest "https://raw.githubusercontent.com/unslothai/unsloth/main/studio/frontend/public/unsloth.ico" -OutFile $icon -UseBasicParsing -TimeoutSec 15 *> $null } } catch {} + $wsh = New-Object -ComObject WScript.Shell + $lnks = @() + $dd = [Environment]::GetFolderPath("Desktop"); if ($dd -and $dd.Trim()) { $lnks += (Join-Path $dd "Unsloth Studio.lnk") } + if ($env:APPDATA -and $env:APPDATA.Trim()) { $smd = Join-Path $env:APPDATA "Microsoft\Windows\Start Menu\Programs"; New-Item -ItemType Directory -Force -Path $smd *> $null; $lnks += (Join-Path $smd "Unsloth Studio.lnk") } + foreach ($lnk in $lnks) { + $sc = $wsh.CreateShortcut($lnk) + $sc.TargetPath = (Join-Path $env:SystemRoot "System32\WindowsPowerShell\v1.0\powershell.exe") + $sc.Arguments = "-NoExit -NoProfile -ExecutionPolicy Bypass -File `"$launcher`"" + $sc.WorkingDirectory = $appDir + if (Test-Path -LiteralPath $icon) { $sc.IconLocation = $icon } + $sc.Description = "Unsloth Studio (GPU via WSL)" + $sc.Save() + } + step "shortcuts" "created Desktop + Start Menu shortcuts (launch WSL Studio + open browser)" "Green" + } catch { + substep "(could not create shortcuts: $($_.Exception.Message))" "Yellow" + } } else { step "wsl" "WSL Studio install did not finish cleanly (torch.cuda not detected; inner exit $wslRc) -- see log above." "Yellow" substep "retry, or launch manually: wsl -d $distro -u root -- bash -lic 'unsloth studio -p 8888'" "Cyan" From b8f8fbffbf98558bbd93fbd8ce14e3695775cdf9 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Tue, 2 Jun 2026 04:46:30 -0700 Subject: [PATCH 04/95] WSL fallback: fix blank shortcut icons + complete uninstall of WSL-fallback artifacts - install.ps1: refresh the shell icon cache (ie4uinit -show) right after creating the Desktop/Start Menu shortcuts, so the (valid) .ico renders immediately instead of showing a blank icon (Explorer caches per-.lnk icons; programmatically-created links need a poke). - scripts/uninstall.ps1 + scripts/uninstall.sh: also remove the WSL-fallback artifacts the native uninstall missed -- the %LOCALAPPDATA%\Unsloth shim/launcher/icon dir, its user-PATH entry, and the real Studio install inside each WSL distro (rm ~/.unsloth + any CUDA llama build). Previously the native uninstaller only cleaned the (empty) native venv + .lnk files. Co-Authored-By: Claude Opus 4.8 --- install.ps1 | 3 +++ scripts/uninstall.ps1 | 38 ++++++++++++++++++++++++++++++++++++++ scripts/uninstall.sh | 9 ++++++++- 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/install.ps1 b/install.ps1 index d9063e832c..f5768ac0cc 100644 --- a/install.ps1 +++ b/install.ps1 @@ -1587,6 +1587,9 @@ shell.Run cmd, 0, False $sc.Save() } step "shortcuts" "created Desktop + Start Menu shortcuts (launch WSL Studio + open browser)" "Green" + # Refresh the shell icon cache so the brand-new .lnk icons render immediately instead of + # showing blank (Explorer caches per-.lnk icons; programmatically-created links often need a poke). + try { & "$env:SystemRoot\System32\ie4uinit.exe" -show 2>$null } catch {} } catch { substep "(could not create shortcuts: $($_.Exception.Message))" "Yellow" } diff --git a/scripts/uninstall.ps1 b/scripts/uninstall.ps1 index 6174e7e494..16f309f687 100644 --- a/scripts/uninstall.ps1 +++ b/scripts/uninstall.ps1 @@ -336,6 +336,44 @@ function Uninstall-UnslothStudio { Remove-Item -LiteralPath 'HKCU:\Software\Unsloth' -Recurse -Force -ErrorAction SilentlyContinue } catch { } + # ── Windows-on-Arm WSL-fallback artifacts ── + # The ARM64+NVIDIA fallback installs Studio INSIDE WSL and drops a native shim + launcher under + # %LOCALAPPDATA%\Unsloth (note: "Unsloth", not "Unsloth Studio") with a PATH entry, while the real + # install lives in the WSL distro(s). The native cleanup above misses all of that -- handle it here. + _Step "Removing WSL-fallback artifacts (shim, launcher, PATH entry, WSL install)..." + $unslothDir = if ($env:LOCALAPPDATA) { Join-Path $env:LOCALAPPDATA "Unsloth" } else { $null } + if ($unslothDir) { + $shimDir = (Join-Path $unslothDir "bin").TrimEnd('\', '/') + try { + $rk = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey('Environment', $true) + if ($rk) { + try { + $rp = $rk.GetValue('Path', '', [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames) + if ($rp) { + $kept = @(); $removed = $false + foreach ($e in ($rp -split ';')) { + if ([string]::IsNullOrWhiteSpace($e)) { continue } + if (([Environment]::ExpandEnvironmentVariables($e).TrimEnd('\', '/')) -ieq $shimDir) { $removed = $true; _Substep "removed PATH entry: $e" "Green"; continue } + $kept += $e + } + if ($removed) { $rk.SetValue('Path', ($kept -join ';'), [Microsoft.Win32.RegistryValueKind]::ExpandString) } + } + } finally { $rk.Close() } + } + } catch { } + _RemovePath $unslothDir + } + # Remove the Studio install inside each WSL distro (the real GPU install + any CUDA llama.cpp build). + if (Get-Command wsl.exe -ErrorAction SilentlyContinue) { + try { + $distros = @(((& wsl.exe --list --quiet 2>$null) -join "`n").Replace([char]0, '') -split "`r?`n" | ForEach-Object { $_.Trim() } | Where-Object { $_ }) + foreach ($d in $distros) { + & wsl.exe -d $d -u root -- bash -lc 'pkill -9 -f "unsloth studio" 2>/dev/null; pkill -9 -f "llama-server" 2>/dev/null; rm -rf /root/.unsloth /home/*/.unsloth /root/llama-cuda 2>/dev/null; true' 2>$null + if ($LASTEXITCODE -eq 0) { _Substep "cleaned Unsloth from WSL distro: $d" "Green" } + } + } catch { } + } + Write-Host "" Write-Host "Unsloth Studio uninstalled." Write-Host "Note: Hugging Face model cache at %USERPROFILE%\.cache\huggingface was left in place." diff --git a/scripts/uninstall.sh b/scripts/uninstall.sh index 94ca04b204..699cff9d3c 100755 --- a/scripts/uninstall.sh +++ b/scripts/uninstall.sh @@ -247,7 +247,6 @@ case "$_os" in # shellcheck disable=SC2016 # $env:APPDATA is a PowerShell expansion; intentionally literal at shell level. powershell.exe -NoProfile -Command ' - $names = @("Desktop","StartMenu"); $dirs = @( [Environment]::GetFolderPath("Desktop"), (Join-Path $env:APPDATA "Microsoft\Windows\Start Menu\Programs") @@ -256,6 +255,14 @@ case "$_os" in if (-not $d) { continue } $p = Join-Path $d "Unsloth Studio.lnk"; if (Test-Path -LiteralPath $p) { Remove-Item -LiteralPath $p -Force } + } + # WSL-fallback native shim/launcher dir (%LOCALAPPDATA%\Unsloth) + its PATH entry. + $ud = if ($env:LOCALAPPDATA) { Join-Path $env:LOCALAPPDATA "Unsloth" } else { $null }; + if ($ud) { + $shim = (Join-Path $ud "bin").TrimEnd("\","/"); + $up = [Environment]::GetEnvironmentVariable("Path","User"); + if ($up) { [Environment]::SetEnvironmentVariable("Path", (($up -split ";" | Where-Object { $_ -and ($_.TrimEnd("\","/") -ine $shim) }) -join ";"), "User") } + if (Test-Path -LiteralPath $ud) { Remove-Item -LiteralPath $ud -Recurse -Force -ErrorAction SilentlyContinue } }' >/dev/null 2>&1 || true fi fi From 25c11b3e983ffbe9bd6baa9fd1ea1cb774c44e85 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Tue, 2 Jun 2026 06:06:25 -0700 Subject: [PATCH 05/95] WSL fallback: self-heal Studio server deps, seed pip, auto-build CUDA llama.cpp A clean-slate reinstall on an ARM64+NVIDIA box surfaced three follow-on gaps in the WSL path. All fixes are additive, best-effort, and confined to the $torchOk success branch of the WSL fallback, so they only run on the ARM64+NVIDIA machines that reach it -- no other platform is affected. install.ps1: - Self-heal Studio's web-server deps. install_python_stack.py installs the Studio UI deps (fastapi/uvicorn/structlog/starlette) in a late step; if that run is cut short, torch+unsloth land but the server stack is missing and `unsloth studio` dies at launch on ModuleNotFoundError. Import-check the stack after the torch.cuda probe and, if absent, install it WITHOUT re-pinning huggingface-hub/transformers/ datasets, so the verified GPU torch path is never disturbed. - Seed pip into the (uv-managed, pip-less) venv via ensurepip so save_pretrained_gguf -> check_pip() works regardless of how Studio is launched. - Auto-build a CUDA llama-server for GGUF inference in the background via the new provision script (below), so GGUF chat/tool-calling lights up a few minutes after install with zero manual steps. Opt out with UNSLOTH_NO_LLAMA_CUDA=1. studio/scripts/provision_llama_cuda.sh (new): - Idempotent, best-effort (always exits 0). Builds a CUDA llama.cpp into ~/.unsloth/llama.cpp (Studio's resolver path). Generic across NVIDIA Linux/WSL incl. aarch64 (DGX Spark, N1X): derives the arch from the GPU's compute_cap, installs gcc-14 + CUDA 13.3 only when nvcc is missing (gcc-15 is rejected by nvcc; CUDA <13.3 hits the glibc>=2.41 rsqrt header clash), and builds the full target set (llama-server llama-cli llama-quantize llama-mtmd-cli llama-gguf-split) so it satisfies both Studio inference and save_pretrained_gguf without a later rebuild. Validated on an NVIDIA N1X (sm_121): training, GPU inference, GGUF q4_k_m export, `unsloth studio` via both Desktop + Start Menu shortcuts (HTTP 200), GGUF chat at 121 tok/s (BLACKWELL_NATIVE_FP4=1) and OpenAI-style tool-calling. Co-Authored-By: Claude Opus 4.8 --- install.ps1 | 75 ++++++++++++++- studio/scripts/provision_llama_cuda.sh | 125 +++++++++++++++++++++++++ 2 files changed, 198 insertions(+), 2 deletions(-) create mode 100644 studio/scripts/provision_llama_cuda.sh diff --git a/install.ps1 b/install.ps1 index f5768ac0cc..855151cbbc 100644 --- a/install.ps1 +++ b/install.ps1 @@ -1533,6 +1533,50 @@ shell.Run cmd, 0, False & wsl.exe -d $distro -u root -- /root/.unsloth/studio/unsloth_studio/bin/python -c "import torch,sys; sys.exit(0 if torch.cuda.is_available() else 3)" *> $null $torchOk = ($LASTEXITCODE -eq 0) } catch {} finally { $ErrorActionPreference = $prevEapChk } + # Self-heal Studio's web-server deps. install.sh installs them in a late step + # (install_python_stack.py "studio deps", step 8). If that step is cut short -- + # an interrupted download, or a transient resolver hiccup -- torch + unsloth still + # land, but the server stack (fastapi/uvicorn/structlog/starlette) is missing and + # `unsloth studio` dies at launch with ModuleNotFoundError. If the stack can't + # import, install it WITHOUT disturbing the working ML stack: we deliberately do + # NOT pin huggingface-hub / transformers / datasets here, so the GPU torch path + # we just verified stays intact (those pins live in studio.txt for a fresh env). + if ($torchOk) { + $_studioPy = "/root/.unsloth/studio/unsloth_studio/bin/python" + $_serverOk = $false + $prevEapS = $ErrorActionPreference; $ErrorActionPreference = "Continue" + try { + & wsl.exe -d $distro -u root -- $_studioPy -c "import structlog, fastapi, uvicorn, starlette" *> $null + $_serverOk = ($LASTEXITCODE -eq 0) + } catch {} finally { $ErrorActionPreference = $prevEapS } + if (-not $_serverOk) { + substep "Studio web-server deps incomplete (install.sh step cut short) -- installing them now..." "Cyan" + # Mirrors studio/backend/requirements/studio.txt MINUS the huggingface-hub + # pin (protected above). Prefer uv (matches install.sh); fall back to pip. + $_deps = 'typer fastapi uvicorn matplotlib pandas nest_asyncio pyjwt easydict addict "structlog>=24.1.0" diceware ddgs "cryptography>=42.0.0" "httpx>=0.27.0" "fastmcp>=3.0.2"' + $_repair = 'PY=/root/.unsloth/studio/unsloth_studio/bin/python; UV="$(command -v uv 2>/dev/null || echo /root/.local/bin/uv)"; if [ -x "$UV" ] || command -v uv >/dev/null 2>&1; then "$UV" pip install --python "$PY" ' + $_deps + '; else "$PY" -m pip install ' + $_deps + '; fi' + $prevEapR = $ErrorActionPreference; $ErrorActionPreference = "Continue" + try { & wsl.exe -d $distro -u root -- bash -lc $_repair } catch {} finally { $ErrorActionPreference = $prevEapR } + $prevEapS2 = $ErrorActionPreference; $ErrorActionPreference = "Continue" + try { + & wsl.exe -d $distro -u root -- $_studioPy -c "import structlog, fastapi, uvicorn, starlette" *> $null + $_serverOk = ($LASTEXITCODE -eq 0) + } catch {} finally { $ErrorActionPreference = $prevEapS2 } + if ($_serverOk) { substep "Studio web-server deps installed." "Green" } + else { substep "(could not auto-install Studio server deps; 'unsloth studio' may fail to start)" "Yellow" } + } + # GGUF export robustness: the venv is uv-managed and ships no `pip`, but + # unsloth-zoo's exporter calls check_pip() and only finds `uv pip` when uv is + # on PATH (true for the login-shell launcher, not for every code path). + # Seeding pip into the venv makes `save_pretrained_gguf` work regardless. + $prevEapP = $ErrorActionPreference; $ErrorActionPreference = "Continue" + try { + & wsl.exe -d $distro -u root -- $_studioPy -m pip --version *> $null + if ($LASTEXITCODE -ne 0) { + & wsl.exe -d $distro -u root -- $_studioPy -m ensurepip --upgrade *> $null + } + } catch {} finally { $ErrorActionPreference = $prevEapP } + } if ($torchOk) { step "done" "Unsloth Studio installed in WSL '$distro' -- GPU ready (torch.cuda available)." "Green" # Native Windows `unsloth` shim: forward every `unsloth ...` into the WSL GPU env so the user @@ -1587,12 +1631,39 @@ shell.Run cmd, 0, False $sc.Save() } step "shortcuts" "created Desktop + Start Menu shortcuts (launch WSL Studio + open browser)" "Green" - # Refresh the shell icon cache so the brand-new .lnk icons render immediately instead of - # showing blank (Explorer caches per-.lnk icons; programmatically-created links often need a poke). + # Make the brand-new .lnk icons render immediately instead of blank. Explorer caches + # per-.lnk icons, so a freshly-created shortcut often shows blank until the shell is told + # to re-read it. ie4uinit -show alone is unreliable; also broadcast SHChangeNotify so + # Explorer refreshes the icons without needing a restart or re-login. try { & "$env:SystemRoot\System32\ie4uinit.exe" -show 2>$null } catch {} + try { + if (-not ("UnslothShell.Notify" -as [type])) { + Add-Type -Namespace UnslothShell -Name Notify -MemberDefinition '[System.Runtime.InteropServices.DllImport("shell32.dll")] public static extern void SHChangeNotify(int eventId, uint flags, System.IntPtr item1, System.IntPtr item2);' + } + # SHCNE_ASSOCCHANGED (0x08000000) with SHCNF_IDLIST (0) -> flush shell icon associations. + [UnslothShell.Notify]::SHChangeNotify(0x08000000, 0, [System.IntPtr]::Zero, [System.IntPtr]::Zero) + } catch {} } catch { substep "(could not create shortcuts: $($_.Exception.Message))" "Yellow" } + # GGUF *inference* needs a CUDA-linked llama-server. There is no published + # aarch64+CUDA llama.cpp prebuilt (NVIDIA DGX Spark / N1X), so build one into + # ~/.unsloth/llama.cpp (Studio's resolver path) IN THE BACKGROUND: the user gets + # Studio + training immediately, and GGUF inference lights up a few minutes later + # with zero manual steps. Best-effort; opt out with UNSLOTH_NO_LLAMA_CUDA=1. + if ($env:UNSLOTH_NO_LLAMA_CUDA -ne '1') { + $prevEapL = $ErrorActionPreference; $ErrorActionPreference = "Continue" + try { + $_llamaUrl = "https://raw.githubusercontent.com/unslothai/unsloth/main/studio/scripts/provision_llama_cuda.sh" + $_provCmd = 'mkdir -p /root/.unsloth; if curl -fsSL "' + $_llamaUrl + '" -o /root/.unsloth/provision_llama_cuda.sh && [ -s /root/.unsloth/provision_llama_cuda.sh ]; then chmod +x /root/.unsloth/provision_llama_cuda.sh; nohup setsid bash /root/.unsloth/provision_llama_cuda.sh > /root/.unsloth/llama_cuda_build.log 2>&1 < /dev/null & echo PROV_STARTED; else echo PROV_NOSCRIPT; fi' + $_provOut = & wsl.exe -d $distro -u root -- bash -lc $_provCmd 2>$null + if ("$_provOut" -match 'PROV_STARTED') { + step "llama.cpp" "building CUDA llama.cpp for GGUF inference in the background (a few min); log: ~/.unsloth/llama_cuda_build.log" "Green" + } else { + substep "(GGUF inference needs a CUDA llama.cpp build; build later: wsl -d $distro -u root -- bash ~/.unsloth/provision_llama_cuda.sh)" "Yellow" + } + } catch {} finally { $ErrorActionPreference = $prevEapL } + } } else { step "wsl" "WSL Studio install did not finish cleanly (torch.cuda not detected; inner exit $wslRc) -- see log above." "Yellow" substep "retry, or launch manually: wsl -d $distro -u root -- bash -lic 'unsloth studio -p 8888'" "Cyan" diff --git a/studio/scripts/provision_llama_cuda.sh b/studio/scripts/provision_llama_cuda.sh new file mode 100644 index 0000000000..cea357a595 --- /dev/null +++ b/studio/scripts/provision_llama_cuda.sh @@ -0,0 +1,125 @@ +#!/usr/bin/env bash +# Provision a CUDA-enabled llama.cpp for Unsloth Studio GGUF *inference*. +# +# Builds into ~/.unsloth/llama.cpp (the dir Unsloth Studio's llama-server +# resolver checks: /build/bin/llama-server). Best-effort and idempotent: +# safe to re-run, never hard-fails the caller (always exits 0). +# +# Why this exists: torch ships its own bundled CUDA runtime, so training + +# GGUF *export* work without a system CUDA toolkit. But GGUF *inference* needs +# a CUDA-linked llama-server, and on NVIDIA ARM machines (NVIDIA DGX Spark / +# GB10, N1X "RTX" laptops) there is no published aarch64+CUDA prebuilt, so we +# build one. Handles the known gotchas on these platforms: +# * nvcc rejects gcc-15 -> force gcc-14 / g++-14 as the host compiler +# * glibc >= 2.41 vs CUDA < 13.3 -> install CUDA 13.3 (rsqrt header clash) +# * sm_121 (Blackwell) GPUs -> derive arch from the GPU's compute_cap +# +# Opt out entirely with UNSLOTH_NO_LLAMA_CUDA=1 (handled by the caller). +set -uo pipefail + +LLAMA_DIR="${UNSLOTH_LLAMA_CPP_PATH:-$HOME/.unsloth/llama.cpp}" +SERVER="$LLAMA_DIR/build/bin/llama-server" +log() { printf ' - %s\n' "$*"; } + +is_cuda_server() { [ -x "$1" ] && ldd "$1" 2>/dev/null | grep -qi 'libggml-cuda'; } + +# 0. Already provisioned? +if is_cuda_server "$SERVER"; then + log "CUDA llama-server already present: $SERVER" + exit 0 +fi + +# 1. Require an NVIDIA GPU (this script is only meaningful with one). +if ! command -v nvidia-smi >/dev/null 2>&1; then + log "no nvidia-smi found; skipping CUDA llama.cpp build" + exit 0 +fi + +SUDO=""; [ "$(id -u)" -ne 0 ] && SUDO="sudo" +HAVE_APT=0; command -v apt-get >/dev/null 2>&1 && HAVE_APT=1 + +# 2. Base toolchain. gcc-14 is required because nvcc rejects gcc-15. +if [ "$HAVE_APT" -eq 1 ]; then + $SUDO apt-get update -y >/dev/null 2>&1 || true + $SUDO apt-get install -y --no-install-recommends \ + build-essential cmake git curl ca-certificates gcc-14 g++-14 >/dev/null 2>&1 || true +fi + +# 3. Locate nvcc; install the CUDA toolkit if missing. +find_nvcc() { command -v nvcc 2>/dev/null || ls /usr/local/cuda*/bin/nvcc 2>/dev/null | sort -V | tail -1; } +NVCC="$(find_nvcc)" +if [ -z "$NVCC" ] && [ "$HAVE_APT" -eq 1 ]; then + log "CUDA toolkit (nvcc) not found - installing CUDA 13.3 (matches torch cu13x; avoids glibc>=2.41 rsqrt clash)" + # shellcheck disable=SC1091 + . /etc/os-release 2>/dev/null || true + case "$(uname -m)" in + aarch64) NV_ARCH=sbsa ;; + x86_64) NV_ARCH=x86_64 ;; + *) NV_ARCH="" ;; + esac + case "${ID:-}${VERSION_ID:-}" in + ubuntu24.04) NV_DISTRO=ubuntu2404 ;; + ubuntu22.04) NV_DISTRO=ubuntu2204 ;; + debian12) NV_DISTRO=debian12 ;; + *) NV_DISTRO="" ;; + esac + if [ -n "$NV_ARCH" ] && [ -n "$NV_DISTRO" ]; then + KR=/tmp/cuda-keyring.deb + if curl -fsSL "https://developer.download.nvidia.com/compute/cuda/repos/$NV_DISTRO/$NV_ARCH/cuda-keyring_1.1-1_all.deb" -o "$KR" 2>/dev/null; then + $SUDO dpkg -i "$KR" >/dev/null 2>&1 || true + $SUDO apt-get update -y >/dev/null 2>&1 || true + $SUDO apt-get install -y cuda-toolkit-13-3 >/dev/null 2>&1 \ + || $SUDO apt-get install -y cuda-toolkit >/dev/null 2>&1 || true + fi + fi + NVCC="$(find_nvcc)" +fi + +if [ -z "$NVCC" ]; then + log "could not provision a CUDA toolkit. Training + GGUF export still work;" + log "GGUF *inference* in Studio will be unavailable until a CUDA toolkit exists." + log "Re-run this script after installing one to enable GGUF inference." + exit 0 +fi + +CUDA_HOME="$(dirname "$(dirname "$NVCC")")" +export PATH="$CUDA_HOME/bin:$PATH" +export CUDAToolkit_ROOT="$CUDA_HOME" + +# 4. Host compiler: prefer gcc-14 / g++-14 (nvcc rejects 15). +HCC=gcc; command -v gcc-14 >/dev/null 2>&1 && HCC=gcc-14 +HCXX=g++; command -v g++-14 >/dev/null 2>&1 && HCXX=g++-14 +export CC="$HCC" CXX="$HCXX" CUDAHOSTCXX="$HCXX" + +# 5. CUDA arch from the GPU's compute capability (e.g. "12.1" -> 121). Fallback: native. +CC_CAP="$(nvidia-smi --query-gpu=compute_cap --format=csv,noheader 2>/dev/null | head -1 | tr -d ' .')" +if [ -n "$CC_CAP" ]; then CUDA_ARCH="$CC_CAP"; else CUDA_ARCH="native"; fi + +# 6. Clone + build into ~/.unsloth/llama.cpp. +mkdir -p "$(dirname "$LLAMA_DIR")" +if [ ! -d "$LLAMA_DIR/.git" ]; then + rm -rf "$LLAMA_DIR" + git clone --depth 1 https://github.com/ggml-org/llama.cpp "$LLAMA_DIR" >/dev/null 2>&1 \ + || { log "git clone failed"; exit 0; } +fi +cd "$LLAMA_DIR" || exit 0 + +log "building CUDA llama.cpp (arch=$CUDA_ARCH, host=$HCXX) - this takes a few minutes..." +cmake -S . -B build -DCMAKE_BUILD_TYPE=Release \ + -DGGML_CUDA=ON -DGGML_CUDA_F16=ON \ + -DCMAKE_CUDA_ARCHITECTURES="$CUDA_ARCH" \ + -DCMAKE_CUDA_HOST_COMPILER="$HCXX" \ + -DLLAMA_CURL=ON >/dev/null 2>&1 || { log "cmake configure failed"; exit 0; } +# Build the full set unsloth-zoo's GGUF exporter expects too (llama-mtmd-cli, +# llama-gguf-split), so a pre-provisioned build satisfies both Studio inference +# AND save_pretrained_gguf without triggering a --clean-first rebuild later. +cmake --build build -j"$(nproc)" --target \ + llama-server llama-cli llama-quantize llama-mtmd-cli llama-gguf-split >/dev/null 2>&1 \ + || { log "cmake build failed"; exit 0; } + +if is_cuda_server "$SERVER"; then + log "CUDA llama-server ready: $SERVER" +else + log "build finished but CUDA llama-server could not be confirmed" +fi +exit 0 From d8e28f6a2f60cf25999a7143f39d92bbdcaa5a7f Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Tue, 2 Jun 2026 06:51:04 -0700 Subject: [PATCH 06/95] DGX Spark / N1X: shared is_dgx_spark() + caching_allocator_warmup no-op Runtime training support for NVIDIA Blackwell unified-memory (UMA) machines -- DGX Spark (GB10) and the N1X "RTX Spark" laptop. Gated so it is a strict no-op on every non-Spark platform (x86 NVIDIA, AMD/ROCm, Intel/XPU, Mac/MLX, discrete aarch64 GH200/GB200): those are not aarch64 and/or report non-matching device names, so behaviour there is byte-for-byte unchanged. models/_utils.py: - Add is_dgx_spark(): aarch64 + NVIDIA CUDA + a known Spark device-name token (GB10 / JMJWOA / N1X / ...). @lru_cache; overridable via UNSLOTH_FORCE_DGX_SPARK. One shared detector that also catches the N1X laptop, which reports "JMJWOA-Generic-GPU" rather than "NVIDIA GB10". - Add patch_dgx_spark_caching_allocator_warmup(), applied at import: no-ops transformers.modeling_utils.caching_allocator_warmup on Spark. HF sizes a GPU pre-allocation from cudaMemGetInfo() to warm the caching allocator; on Spark UMA cudaMemGetInfo undercounts free memory (reclaimable buffer cache shows as unavailable), so the warmup torch.empty() raises `AcceleratorError: invalid argument` and aborts any bitsandbytes 4/8-bit load. The warmup is only a speed hint -> dropping it on Spark lets quantized loads succeed. Idempotent; single call site (modeling_utils.py:4212) confirmed. (Patch credited to Roland [UnAI] / Daniel, Unsloth Discord.) models/loader.py: - Replace the two inline `"NVIDIA GB10" in get_device_name()` checks (which disable the currently-broken vLLM fast_inference) with is_dgx_spark(), so the N1X is covered too. Same behaviour on DGX Spark; no change off-Spark. torch.compile + Triton are verified WORKING on the N1X (Triton 3.6.0; a real gemma-3-270m-it 4-bit finetune with compile ON trains and emits the full compiled cache), so nothing is disabled -- UNSLOTH_COMPILE_DISABLE is not set. Co-Authored-By: Claude Opus 4.8 --- unsloth/models/_utils.py | 66 ++++++++++++++++++++++++++++++++++++++++ unsloth/models/loader.py | 13 ++++---- 2 files changed, 73 insertions(+), 6 deletions(-) diff --git a/unsloth/models/_utils.py b/unsloth/models/_utils.py index abb6969a7e..4e0b5d8f63 100644 --- a/unsloth/models/_utils.py +++ b/unsloth/models/_utils.py @@ -939,6 +939,72 @@ except: from transformers.modeling_utils import logger as transformers_logger +# ---- NVIDIA DGX Spark (GB10) / N1X "RTX Spark" (Blackwell unified-memory) support ---- +# These Blackwell unified-memory (UMA) machines report different device names: +# "NVIDIA GB10" on DGX Spark, "JMJWOA-Generic-GPU" on the pre-launch N1X laptop. +# One shared detector so every Spark-specific workaround uses the same definition. +# The aarch64 + CUDA gate makes this a strict no-op on x86_64 NVIDIA, AMD/ROCm, +# Intel/XPU, Mac/MLX, and discrete aarch64 GPUs (GH200/GB200) -- those report +# non-matching names and/or are not aarch64, so behaviour there is unchanged. +_DGX_SPARK_DEVICE_TOKENS = ("GB10", "JMJWOA", "N1X", "DGX SPARK", "GB110") + +@functools.lru_cache(maxsize = None) +def is_dgx_spark(): + """True only on a DGX Spark / N1X Spark-class machine. + + Gate: aarch64 + NVIDIA CUDA + a known Spark device-name token. Overridable for + testing via UNSLOTH_FORCE_DGX_SPARK=1 (force on) / =0 (force off). + """ + _force = os.environ.get("UNSLOTH_FORCE_DGX_SPARK") + if _force == "1": return True + if _force == "0": return False + try: + import platform + if platform.machine().lower() not in ("aarch64", "arm64"): + return False + if not (hasattr(torch, "cuda") and torch.cuda.is_available()): + return False + names = " ".join( + str(torch.cuda.get_device_name(i)).upper() + for i in range(torch.cuda.device_count()) + ) + return any(token in names for token in _DGX_SPARK_DEVICE_TOKENS) + except Exception: + return False +pass + + +def patch_dgx_spark_caching_allocator_warmup(): + """No-op `transformers.modeling_utils.caching_allocator_warmup` on Spark UMA. + + HF sizes a GPU pre-allocation from `cudaMemGetInfo()` to warm the caching + allocator. On Spark unified memory `cudaMemGetInfo` undercounts free memory + (reclaimable buffer cache is reported unavailable), so the warmup + `torch.empty(...)` raises `AcceleratorError: invalid argument` and aborts any + runtime-quantized (bitsandbytes 4/8-bit) load. The warmup is only a speed hint, + so skipping it on Spark merely forgoes a minor warmup while letting loads + succeed. No-op on every non-Spark platform (gated by `is_dgx_spark()`). + Idempotent: re-applying is a no-op (marked via `_unsloth_spark_noop`). + """ + if not is_dgx_spark(): + return + try: + from transformers import modeling_utils as _mu + except Exception: + return + if not hasattr(_mu, "caching_allocator_warmup"): + return + if getattr(_mu.caching_allocator_warmup, "_unsloth_spark_noop", False): + return + def _noop(*args, **kwargs): + return None + _noop._unsloth_spark_noop = True + _mu.caching_allocator_warmup = _noop +pass + +patch_dgx_spark_caching_allocator_warmup() + + class _RaiseUninitialized(logging.Handler): def __init__(self): super().__init__() diff --git a/unsloth/models/loader.py b/unsloth/models/loader.py index 0808138ac9..f808d2b6ae 100644 --- a/unsloth/models/loader.py +++ b/unsloth/models/loader.py @@ -16,6 +16,7 @@ from ._utils import ( _prepare_model_for_qat, is_bfloat16_supported, is_vLLM_available, + is_dgx_spark, HAS_FLASH_ATTENTION, HAS_FLASH_ATTENTION_SOFTCAPPING, USE_MODELSCOPE, @@ -379,10 +380,10 @@ class FastLanguageModel(FastLlamaModel): ) if DEVICE_TYPE_TORCH == "cuda": for i in range(DEVICE_COUNT): - # [TODO] DGX Spark vLLM breaks - if "NVIDIA GB10" in str(torch.cuda.get_device_name(i)).upper(): + # [TODO] DGX Spark / N1X (Spark-class) vLLM breaks + if is_dgx_spark(): print( - "Unsloth: DGX Spark detected - `fast_inference=True` is currently broken as of January 2026.\n" + "Unsloth: DGX Spark / N1X (Spark-class GPU) detected - `fast_inference=True` is currently broken as of January 2026.\n" "Defaulting to native Unsloth inference." ) fast_inference = False @@ -1005,10 +1006,10 @@ class FastModel(FastBaseModel): ) if DEVICE_TYPE_TORCH == "cuda": for i in range(DEVICE_COUNT): - # [TODO] DGX Spark vLLM breaks - if "NVIDIA GB10" in str(torch.cuda.get_device_name(i)).upper(): + # [TODO] DGX Spark / N1X (Spark-class) vLLM breaks + if is_dgx_spark(): print( - "Unsloth: DGX Spark detected - `fast_inference=True` is currently broken as of January 2026.\n" + "Unsloth: DGX Spark / N1X (Spark-class GPU) detected - `fast_inference=True` is currently broken as of January 2026.\n" "Defaulting to native Unsloth inference." ) fast_inference = False From 4308d254f56327e13116322376f5225a764bed44 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2026 13:51:29 +0000 Subject: [PATCH 07/95] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- unsloth/models/_utils.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/unsloth/models/_utils.py b/unsloth/models/_utils.py index 4e0b5d8f63..c977312854 100644 --- a/unsloth/models/_utils.py +++ b/unsloth/models/_utils.py @@ -948,6 +948,7 @@ from transformers.modeling_utils import logger as transformers_logger # non-matching names and/or are not aarch64, so behaviour there is unchanged. _DGX_SPARK_DEVICE_TOKENS = ("GB10", "JMJWOA", "N1X", "DGX SPARK", "GB110") + @functools.lru_cache(maxsize = None) def is_dgx_spark(): """True only on a DGX Spark / N1X Spark-class machine. @@ -956,10 +957,13 @@ def is_dgx_spark(): testing via UNSLOTH_FORCE_DGX_SPARK=1 (force on) / =0 (force off). """ _force = os.environ.get("UNSLOTH_FORCE_DGX_SPARK") - if _force == "1": return True - if _force == "0": return False + if _force == "1": + return True + if _force == "0": + return False try: import platform + if platform.machine().lower() not in ("aarch64", "arm64"): return False if not (hasattr(torch, "cuda") and torch.cuda.is_available()): @@ -971,7 +975,8 @@ def is_dgx_spark(): return any(token in names for token in _DGX_SPARK_DEVICE_TOKENS) except Exception: return False -pass + + def patch_dgx_spark_caching_allocator_warmup(): @@ -996,11 +1001,14 @@ def patch_dgx_spark_caching_allocator_warmup(): return if getattr(_mu.caching_allocator_warmup, "_unsloth_spark_noop", False): return + def _noop(*args, **kwargs): return None + _noop._unsloth_spark_noop = True _mu.caching_allocator_warmup = _noop -pass + + patch_dgx_spark_caching_allocator_warmup() From 1144d972d0403fae84f646ad1c1b186efb0133b1 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Tue, 2 Jun 2026 07:10:07 -0700 Subject: [PATCH 08/95] DGX Spark / N1X: enable expandable_segments to reduce UMA fragmentation Adds patch_dgx_spark_memory_config() (models/_utils.py), applied at import: on Spark-class machines it sets PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True so the CUDA caching allocator grows segments in virtual address space instead of fragmenting the shared unified-memory pool. More of the pool stays usable for weights/activations -> fewer fragmentation OOMs and headroom for larger models / longer sequences. Pure memory management: computed values are unchanged, so accuracy is unaffected (verified: gemma-3-270m losses identical with/without it). Regression-safe: gated by is_dgx_spark() (strict no-op on x86 NVIDIA, AMD/ROCm, Intel, Mac/MLX, discrete aarch64). Uses setdefault semantics -- only appends when expandable_segments is absent, never overrides a user's PYTORCH_CUDA_ALLOC_CONF; opt out with UNSLOTH_NO_EXPANDABLE_SEGMENTS=1. Co-Authored-By: Claude Opus 4.8 --- unsloth/models/_utils.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/unsloth/models/_utils.py b/unsloth/models/_utils.py index c977312854..d5eccb9b0a 100644 --- a/unsloth/models/_utils.py +++ b/unsloth/models/_utils.py @@ -1009,7 +1009,32 @@ def patch_dgx_spark_caching_allocator_warmup(): _mu.caching_allocator_warmup = _noop +def patch_dgx_spark_memory_config(): + """Memory-efficiency default for Spark UMA (accuracy-neutral, gated). + Enables the CUDA caching allocator's `expandable_segments` mode so segments can + grow in virtual address space instead of fragmenting the shared unified-memory + pool -- more of the pool stays usable for weights/activations (fewer + fragmentation OOMs; headroom for larger models / longer sequences). Pure memory + management: it never changes any computed value, so accuracy is unaffected. + + Strictly no-op off-Spark (gated by `is_dgx_spark()`). Respects an existing + PYTORCH_CUDA_ALLOC_CONF (only appends `expandable_segments` when absent, never + overrides a user's setting) and an explicit opt-out + (UNSLOTH_NO_EXPANDABLE_SEGMENTS=1). Must run before the first CUDA allocation; + `import unsloth` precedes model load, so it is set in time for normal use. + """ + if not is_dgx_spark(): + return + if os.environ.get("UNSLOTH_NO_EXPANDABLE_SEGMENTS") == "1": + return + conf = os.environ.get("PYTORCH_CUDA_ALLOC_CONF", "") + if "expandable_segments" in conf: + return # user already configured it -- do not override + os.environ["PYTORCH_CUDA_ALLOC_CONF"] = (conf + "," if conf else "") + "expandable_segments:True" + + +patch_dgx_spark_memory_config() patch_dgx_spark_caching_allocator_warmup() From 681f61ac6e35b695ef20575c68725a4ddc6b83ab Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2026 14:10:23 +0000 Subject: [PATCH 09/95] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- unsloth/models/_utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/unsloth/models/_utils.py b/unsloth/models/_utils.py index d5eccb9b0a..c9abbce183 100644 --- a/unsloth/models/_utils.py +++ b/unsloth/models/_utils.py @@ -977,8 +977,6 @@ def is_dgx_spark(): return False - - def patch_dgx_spark_caching_allocator_warmup(): """No-op `transformers.modeling_utils.caching_allocator_warmup` on Spark UMA. @@ -1031,7 +1029,9 @@ def patch_dgx_spark_memory_config(): conf = os.environ.get("PYTORCH_CUDA_ALLOC_CONF", "") if "expandable_segments" in conf: return # user already configured it -- do not override - os.environ["PYTORCH_CUDA_ALLOC_CONF"] = (conf + "," if conf else "") + "expandable_segments:True" + os.environ["PYTORCH_CUDA_ALLOC_CONF"] = ( + conf + "," if conf else "" + ) + "expandable_segments:True" patch_dgx_spark_memory_config() From 23ccec6d53979d87125e7963a487b5750ed055ed Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Tue, 2 Jun 2026 22:41:24 -0700 Subject: [PATCH 10/95] provision_llama_cuda: configurable build jobs (UNSLOTH_LLAMA_BUILD_JOBS) A full -j(nproc) CUDA build is power/thermal-heavy on laptops (e.g. N1X) and can trip a thermal/power shutdown mid-build. Allow lowering the job count; cmake --build is incremental so re-running resumes from where it stopped. Co-Authored-By: Claude Opus 4.8 --- studio/scripts/provision_llama_cuda.sh | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/studio/scripts/provision_llama_cuda.sh b/studio/scripts/provision_llama_cuda.sh index cea357a595..fda37b445c 100644 --- a/studio/scripts/provision_llama_cuda.sh +++ b/studio/scripts/provision_llama_cuda.sh @@ -113,7 +113,11 @@ cmake -S . -B build -DCMAKE_BUILD_TYPE=Release \ # Build the full set unsloth-zoo's GGUF exporter expects too (llama-mtmd-cli, # llama-gguf-split), so a pre-provisioned build satisfies both Studio inference # AND save_pretrained_gguf without triggering a --clean-first rebuild later. -cmake --build build -j"$(nproc)" --target \ +# Job count is overridable (UNSLOTH_LLAMA_BUILD_JOBS) -- lower it on thermally / +# power-constrained laptops (e.g. N1X) where a full -j(nproc) CUDA build can trip +# thermal/power shutdowns. cmake --build is incremental, so re-running resumes. +JOBS="${UNSLOTH_LLAMA_BUILD_JOBS:-$(nproc)}" +cmake --build build -j"$JOBS" --target \ llama-server llama-cli llama-quantize llama-mtmd-cli llama-gguf-split >/dev/null 2>&1 \ || { log "cmake build failed"; exit 0; } From afe589282d477dd1bd0aab86f9bf3ff208511953 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Tue, 2 Jun 2026 23:09:56 -0700 Subject: [PATCH 11/95] DGX Spark / N1X: UMA training perf+memory defaults (gated, accuracy-neutral) Four gated, is_dgx_spark()-only extensions (strict no-op on x86 NVIDIA, AMD/ROCm, Mac/MLX, Intel, discrete aarch64, normal WSL/Windows; no computed value changes): 1. max_autotune=False on Spark, in BOTH torch_compile_options dicts (models/_utils.py + kernels/flex_attention.py). This 48-SM GPU is below inductor's hardcoded 68-SM is_big_gpu threshold, so max_autotune_gemm is already skipped (the 'Not enough SMs to use max_autotune_gemm mode' warning) -- dropping it only avoids the wasted compile-time autotuning search; the produced Triton/inductor kernels are identical (same accuracy + steady-state speed). 2. dataloader_pin_memory=False on Spark, via an idempotent TrainingArguments.__post_init__ wrap (covers SFT + all TRL trainers). Pinned host memory is pointless on unified memory (no separate device memory) and only reserves non-pageable RAM from the shared pool. Mirrors transformers' own . Opt out: UNSLOTH_SPARK_KEEP_PIN_MEMORY=1. 3. UNSLOTH_DISABLE_DOUBLE_BUFFER defaulted on Spark (setdefault): unsloth-zoo's gradient-checkpointing double-buffer is gated on mem_get_info (undercounts on UMA) and overlaps a host<->device copy that is free on a shared pool. 4. Opt-in UNSLOTH_SPARK_MEM_FRACTION -> torch.cuda.set_per_process_memory_fraction safety valve (default unset = no cap, no capacity loss), so an over-allocation raises a catchable OOM instead of wedging the box. Findings from a 5-agent code+web review (transformers/unsloth/zoo/trl + NVIDIA DGX-Spark playbooks). Higher-impact-but-needs-validation items (device_map max_memory sizing, GC offload short-circuit, drop_caches) deferred. Co-Authored-By: Claude Opus 4.8 --- unsloth/kernels/flex_attention.py | 34 ++++++++++++++ unsloth/models/_utils.py | 74 +++++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+) diff --git a/unsloth/kernels/flex_attention.py b/unsloth/kernels/flex_attention.py index b94ff56dec..c5cd537b89 100644 --- a/unsloth/kernels/flex_attention.py +++ b/unsloth/kernels/flex_attention.py @@ -25,6 +25,40 @@ torch_compile_options = { "triton.cudagraphs": False, } + +def _flex_is_dgx_spark(): + # Mirror of unsloth.models._utils.is_dgx_spark(), inlined to avoid importing + # `unsloth.models` from this low-level `kernels` module (circular at import). + # DGX Spark / N1X = aarch64 + NVIDIA CUDA + a Spark device-name token. + _force = os.environ.get("UNSLOTH_FORCE_DGX_SPARK") + if _force == "1": + return True + if _force == "0": + return False + try: + import platform + + if platform.machine().lower() not in ("aarch64", "arm64"): + return False + if not (hasattr(torch, "cuda") and torch.cuda.is_available()): + return False + names = " ".join( + str(torch.cuda.get_device_name(i)).upper() + for i in range(torch.cuda.device_count()) + ) + return any( + t in names for t in ("GB10", "JMJWOA", "N1X", "DGX SPARK", "GB110") + ) + except Exception: + return False + + +# DGX Spark / N1X has 48 SMs (< inductor's 68-SM is_big_gpu threshold), so +# max_autotune_gemm is already skipped; dropping max_autotune only saves the +# wasted compile-time search -- identical kernels, no accuracy/throughput change. +if _flex_is_dgx_spark(): + torch_compile_options["max_autotune"] = False + # Flex Attention supported from torch 2.5 onwards only try: from torch.nn.attention.flex_attention import ( diff --git a/unsloth/models/_utils.py b/unsloth/models/_utils.py index c9abbce183..8965cac7e0 100644 --- a/unsloth/models/_utils.py +++ b/unsloth/models/_utils.py @@ -1034,8 +1034,74 @@ def patch_dgx_spark_memory_config(): ) + "expandable_segments:True" +def patch_dgx_spark_runtime_defaults(): + """Spark UMA runtime defaults (accuracy-neutral, gated, env-overridable). + + - `UNSLOTH_DISABLE_DOUBLE_BUFFER=1`: unsloth-zoo's gradient-checkpointing + double-buffer is enabled via a `torch.cuda.mem_get_info` free-memory check + that UNDERCOUNTS on UMA, and it stages an extra GPU buffer to overlap a + host<->device copy that is physically free on a shared pool. Default it off + on Spark (`setdefault`, so a user can still force it back on). Must be set + before unsloth-zoo initializes gradient checkpointing -- `import unsloth` + precedes that, so this is in time. + - `set_per_process_memory_fraction`: OPT-IN safety valve. On Spark UMA an + over-allocation can wedge the box (untracked UMA allocations may never trip + a catchable OOM). If the user sets `UNSLOTH_SPARK_MEM_FRACTION=<0..1>`, cap + the caching allocator so it raises OutOfMemoryError early. Default unset -> + NO cap (no capacity loss); purely opt-in. + Strict no-op off-Spark. + """ + if not is_dgx_spark(): + return + os.environ.setdefault("UNSLOTH_DISABLE_DOUBLE_BUFFER", "1") + _frac = os.environ.get("UNSLOTH_SPARK_MEM_FRACTION") + if _frac: + try: + torch.cuda.set_per_process_memory_fraction(float(_frac)) + except Exception: + pass + + +def patch_dgx_spark_dataloader_defaults(): + """On Spark UMA, default `dataloader_pin_memory` to False (accuracy-neutral). + + Page-locked host memory exists to speed host->device DMA; on unified memory + there is no separate device memory, so pinning only reserves non-pageable RAM + from the shared pool and adds a staging copy -- pure waste. Mirrors + transformers' own `if self.use_cpu: self.dataloader_pin_memory = False` + precedent. Wraps the base `TrainingArguments.__post_init__`, so SFT + every + TRL trainer (whose configs call `super().__post_init__()`) are covered with + one idempotent patch. Only flips the library default `True`; opt out with + `UNSLOTH_SPARK_KEEP_PIN_MEMORY=1`. Strict no-op off-Spark; never changes any + computed value, so accuracy is unaffected. + """ + if not is_dgx_spark(): + return + if os.environ.get("UNSLOTH_SPARK_KEEP_PIN_MEMORY") == "1": + return + try: + from transformers import training_args as _ta + + Base = _ta.TrainingArguments + except Exception: + return + if getattr(Base.__post_init__, "_unsloth_spark_uma", False): + return + _orig_post_init = Base.__post_init__ + + def __post_init__(self): + _orig_post_init(self) + if getattr(self, "dataloader_pin_memory", None) is True: + self.dataloader_pin_memory = False + + __post_init__._unsloth_spark_uma = True + Base.__post_init__ = __post_init__ + + patch_dgx_spark_memory_config() patch_dgx_spark_caching_allocator_warmup() +patch_dgx_spark_runtime_defaults() +patch_dgx_spark_dataloader_defaults() class _RaiseUninitialized(logging.Handler): @@ -1620,6 +1686,14 @@ torch_compile_options = { "trace.enabled": UNSLOTH_COMPILE_DEBUG, "triton.cudagraphs": False, } +# DGX Spark / N1X: this GPU has 48 SMs, below inductor's hardcoded 68-SM +# `is_big_gpu` threshold, so `max_autotune_gemm` is already skipped by inductor +# (the "Not enough SMs to use max_autotune_gemm mode" warning). Dropping +# max_autotune on Spark only avoids the wasted compile-time autotuning search -- +# the produced Triton/inductor kernels are identical, so steady-state throughput +# and accuracy are unchanged. Strict no-op off-Spark (gated by is_dgx_spark()). +if is_dgx_spark(): + torch_compile_options["max_autotune"] = False import accelerate From 2469151804d82f7d017e79902d35d562ae350b15 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 3 Jun 2026 06:10:17 +0000 Subject: [PATCH 12/95] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- unsloth/kernels/flex_attention.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/unsloth/kernels/flex_attention.py b/unsloth/kernels/flex_attention.py index c5cd537b89..bd8ec43348 100644 --- a/unsloth/kernels/flex_attention.py +++ b/unsloth/kernels/flex_attention.py @@ -46,9 +46,7 @@ def _flex_is_dgx_spark(): str(torch.cuda.get_device_name(i)).upper() for i in range(torch.cuda.device_count()) ) - return any( - t in names for t in ("GB10", "JMJWOA", "N1X", "DGX SPARK", "GB110") - ) + return any(t in names for t in ("GB10", "JMJWOA", "N1X", "DGX SPARK", "GB110")) except Exception: return False From 85aee169fd96c0842fea1da670c330216840e007 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Tue, 2 Jun 2026 23:22:10 -0700 Subject: [PATCH 13/95] DGX Spark / native Linux: auto-provision CUDA llama.cpp for GGUF inference Native-Linux (non-WSL) aarch64+NVIDIA hosts (DGX Spark / GB10 / N1X "RTX Spark") had a GGUF *inference* gap the Windows WSL2 fallback already closes: setup.sh's source build only emits a CUDA llama-server when a CUDA toolkit (nvcc) is already present. A fresh Spark ships only the driver + nvidia-smi, so the build silently dropped to a CPU-only llama-server and Studio GGUF inference ran without GPU. Mirror the Windows path in the shared Linux installer (studio/setup.sh) so ALL native Linux installs benefit, not just the Windows-specific file: * setup.sh: after the source build, on Linux aarch64/arm64 WITH an NVIDIA GPU AND when no CUDA-linked llama-server exists yet, invoke the existing provision_llama_cuda.sh (installs CUDA 13.3 + gcc-14, builds a CUDA server into the same $LLAMA_CPP_DIR setup.sh validates). Best-effort, never aborts setup; opt out with UNSLOTH_NO_LLAMA_CUDA=1; build load via UNSLOTH_LLAMA_BUILD_JOBS. Resolves the script from the packaged copy, the local-dev repo, or the pinned GitHub raw URL (matches install.ps1). * pyproject.toml: ship studio/scripts/*.sh in the wheel (package-data) so the normal `curl | sh` install has provision_llama_cuda.sh locally. Strictly gated + additive: x86_64 NVIDIA, ROCm/AMD, Intel, macOS/MLX, Windows-native, WSL, CPU-only ARM, and any ARM host that already built a CUDA server are byte-for-byte unaffected. Studio web-server deps + pip seeding are already complete on native Linux via install_python_stack.py (studio.txt step 8 + ensurepip/uv bootstrap step 2), and the Linux .desktop launcher is already created by install.sh create_studio_shortcuts() -- so no duplicate self-heal/launcher was added. bash -n setup.sh / provision_llama_cuda.sh / install.sh: pass. pyproject.toml: valid TOML. Co-Authored-By: Claude Opus 4.8 --- pyproject.toml | 1 + studio/setup.sh | 60 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index acc65f12cf..2100d934dd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,6 +45,7 @@ studio = [ "*.sh", "*.ps1", "*.bat", + "scripts/*.sh", "frontend/dist/**/*", "frontend/*.json", "frontend/*.ts", diff --git a/studio/setup.sh b/studio/setup.sh index 309e3c4460..30a448c513 100755 --- a/studio/setup.sh +++ b/studio/setup.sh @@ -1393,6 +1393,66 @@ else } fi # end _SKIP_GGUF_BUILD check +# ── aarch64 + NVIDIA (DGX Spark / GB10 / N1X "RTX Spark"): provision a CUDA +# llama.cpp when the source build above could not (no CUDA toolkit found) ── +# There is no published aarch64+CUDA llama.cpp prebuilt, so these hosts always +# source-build for the GPU above. But that build only emits a CUDA llama-server +# when a CUDA toolkit (nvcc) is already present; on a fresh Spark that ships only +# the driver + nvidia-smi, the build silently falls back to CPU. The Windows path +# closes this exact gap from its WSL2 fallback by invoking provision_llama_cuda.sh +# (installs CUDA 13.3 + gcc-14, then builds a CUDA-linked server). Mirror that here +# so native-Linux Spark users get the same GGUF *inference* robustness, shared by +# every Linux install instead of bolted onto the Windows installer. +# +# Strictly gated + additive: only fires on Linux aarch64/arm64 WITH an NVIDIA GPU +# AND when we do NOT already have a CUDA-linked llama-server. x86_64 (CUDA prebuilt +# or its own source build), ROCm, macOS/Metal, CPU-only ARM, and any ARM host that +# already built a CUDA server are byte-for-byte unaffected. Opt out with +# UNSLOTH_NO_LLAMA_CUDA=1. Best-effort: never aborts setup (provision script always +# exits 0; failures leave the prior CPU/degraded state for the fallback below). +_have_cuda_llama_server() { + [ -x "$LLAMA_SERVER_BIN" ] && ldd "$LLAMA_SERVER_BIN" 2>/dev/null | grep -qi 'libggml-cuda' +} +if [ "$_HOST_SYSTEM" = "Linux" ] \ + && { [ "$_HOST_MACHINE" = "aarch64" ] || [ "$_HOST_MACHINE" = "arm64" ]; } \ + && [ "${UNSLOTH_NO_LLAMA_CUDA:-0}" != "1" ] \ + && command -v nvidia-smi >/dev/null 2>&1 \ + && nvidia-smi -L 2>/dev/null | awk '/^GPU[[:space:]]+[0-9]+:/{found=1} END{exit !found}' \ + && ! _have_cuda_llama_server; then + # Resolve provision_llama_cuda.sh: prefer the copy shipped beside setup.sh + # (packaged via studio/scripts/*.sh), then the local-dev repo, else fetch + # the pinned raw copy from GitHub (mirrors install.ps1's WSL fetch) so the + # normal `curl | sh` install works even on an older wheel without the script. + _PROV_SH="" + if [ -f "$SCRIPT_DIR/scripts/provision_llama_cuda.sh" ]; then + _PROV_SH="$SCRIPT_DIR/scripts/provision_llama_cuda.sh" + elif [ "${STUDIO_LOCAL_INSTALL:-0}" = "1" ] && [ -f "$REPO_ROOT/studio/scripts/provision_llama_cuda.sh" ]; then + _PROV_SH="$REPO_ROOT/studio/scripts/provision_llama_cuda.sh" + else + _PROV_URL="https://raw.githubusercontent.com/unslothai/unsloth/main/studio/scripts/provision_llama_cuda.sh" + _PROV_TMP="$UNSLOTH_HOME/provision_llama_cuda.sh" + if curl -fsSL "$_PROV_URL" -o "$_PROV_TMP" 2>/dev/null && [ -s "$_PROV_TMP" ]; then + _PROV_SH="$_PROV_TMP" + fi + fi + if [ -n "$_PROV_SH" ]; then + step "llama.cpp" "aarch64 + NVIDIA: provisioning CUDA toolkit + building CUDA llama.cpp for GGUF inference..." "$C_WARN" + substep "(opt out with UNSLOTH_NO_LLAMA_CUDA=1; lower load with UNSLOTH_LLAMA_BUILD_JOBS=N)" + # provision_llama_cuda.sh installs the toolkit + gcc-14 and builds into + # $LLAMA_CPP_DIR. It always exits 0; honor UNSLOTH_LLAMA_CPP_PATH so a + # custom STUDIO_HOME build lands in the same dir setup.sh validates. + UNSLOTH_LLAMA_CPP_PATH="$LLAMA_CPP_DIR" bash "$_PROV_SH" || true + if _have_cuda_llama_server; then + step "llama.cpp" "CUDA llama-server ready (aarch64 + NVIDIA)" + _LLAMA_CPP_DEGRADED=false + elif [ -f "$LLAMA_SERVER_BIN" ]; then + substep "CUDA build unavailable; keeping existing (CPU) llama-server" "$C_WARN" + else + substep "CUDA build unavailable; see ~/.unsloth/llama.cpp build output" "$C_WARN" + fi + fi +fi + # ── arm64 Linux GPU: CPU prebuilt as a last resort ── # arm64 Linux with a GPU has no CUDA prebuilt anywhere (the unslothai fork is # x64 only; ggml-org ships no Linux CUDA build), so it source-builds for the From 3906a72b08b006cabc58c28218166e35c2955f2f Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Tue, 2 Jun 2026 23:34:01 -0700 Subject: [PATCH 14/95] setup.sh: exclude WSL from native-Linux Spark CUDA-llama provision setup.sh runs during install.sh, so on WSL the new aarch64+NVIDIA provision block would foreground-build CUDA llama.cpp during the install -- blocking it and duplicating install.ps1's WSL background provision. Exclude WSL (grep microsoft /proc/version, same idiom setup.sh already uses) so this block is native-Linux (DGX Spark/GB10) only; WSL stays handled by install.ps1's background path. Co-Authored-By: Claude Opus 4.8 --- studio/setup.sh | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/studio/setup.sh b/studio/setup.sh index 30a448c513..5aa7f0497a 100755 --- a/studio/setup.sh +++ b/studio/setup.sh @@ -1415,10 +1415,16 @@ _have_cuda_llama_server() { } if [ "$_HOST_SYSTEM" = "Linux" ] \ && { [ "$_HOST_MACHINE" = "aarch64" ] || [ "$_HOST_MACHINE" = "arm64" ]; } \ + && ! grep -qi microsoft /proc/version 2>/dev/null \ && [ "${UNSLOTH_NO_LLAMA_CUDA:-0}" != "1" ] \ && command -v nvidia-smi >/dev/null 2>&1 \ && nvidia-smi -L 2>/dev/null | awk '/^GPU[[:space:]]+[0-9]+:/{found=1} END{exit !found}' \ && ! _have_cuda_llama_server; then + # NOTE: WSL2 is intentionally excluded above (grep microsoft /proc/version) -- + # under WSL the Windows installer (install.ps1) provisions the CUDA llama.cpp + # in the BACKGROUND after setup completes, so doing it here too would (a) run a + # heavy build in the FOREGROUND during install and (b) duplicate that work. + # This block is for NATIVE Linux (DGX Spark / GB10) only. # Resolve provision_llama_cuda.sh: prefer the copy shipped beside setup.sh # (packaged via studio/scripts/*.sh), then the local-dev repo, else fetch # the pinned raw copy from GitHub (mirrors install.ps1's WSL fetch) so the From c87550cb3bf11ede3b7e320900a8d8d679625912 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Tue, 2 Jun 2026 23:37:06 -0700 Subject: [PATCH 15/95] uninstall: fix WSL rm self-kill (rm before pkill) + clean native-Linux llama.cpp - uninstall.ps1: the WSL-distro cleanup ran 'pkill -f "unsloth studio"' before the rm inside a single bash -lc, but that pattern matches the bash -lc's own argv -> pkill SIGKILLs the shell before rm runs, so /root/.unsloth survived. Reorder: rm FIRST (guaranteed), then non-self-matching fuser -k 8888/tcp + pkill best-effort; also remove the fetched provision script + build log. - uninstall.sh: also remove ~/.unsloth/llama.cpp (CUDA build from provision on native-Linux Spark) + the fetched provision_llama_cuda.sh. Co-Authored-By: Claude Opus 4.8 --- scripts/uninstall.ps1 | 6 +++++- scripts/uninstall.sh | 6 ++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/scripts/uninstall.ps1 b/scripts/uninstall.ps1 index 16f309f687..eb059c763f 100644 --- a/scripts/uninstall.ps1 +++ b/scripts/uninstall.ps1 @@ -368,7 +368,11 @@ function Uninstall-UnslothStudio { try { $distros = @(((& wsl.exe --list --quiet 2>$null) -join "`n").Replace([char]0, '') -split "`r?`n" | ForEach-Object { $_.Trim() } | Where-Object { $_ }) foreach ($d in $distros) { - & wsl.exe -d $d -u root -- bash -lc 'pkill -9 -f "unsloth studio" 2>/dev/null; pkill -9 -f "llama-server" 2>/dev/null; rm -rf /root/.unsloth /home/*/.unsloth /root/llama-cuda 2>/dev/null; true' 2>$null + # IMPORTANT: rm runs FIRST. `pkill -f ""` matches this very + # bash -lc (its argv contains the pattern) and would SIGKILL the shell + # before a trailing rm could run. So remove files first (guaranteed), + # then best-effort kill: fuser by port (does not self-match) + pkill. + & wsl.exe -d $d -u root -- bash -lc 'rm -rf /root/.unsloth /home/*/.unsloth /root/llama-cuda /root/provision_llama_cuda.sh /root/llama_cuda_build.log 2>/dev/null; fuser -k 8888/tcp 2>/dev/null; pkill -9 -f unsloth_studio 2>/dev/null; pkill -9 -f llama-server 2>/dev/null; true' 2>$null if ($LASTEXITCODE -eq 0) { _Substep "cleaned Unsloth from WSL distro: $d" "Green" } } } catch { } diff --git a/scripts/uninstall.sh b/scripts/uninstall.sh index 699cff9d3c..d7cda3be96 100755 --- a/scripts/uninstall.sh +++ b/scripts/uninstall.sh @@ -209,6 +209,12 @@ _custom_studio_roots | while IFS= read -r _custom_root; do _remove_path "$_custom_root" done _remove_path "$HOME/.unsloth/studio" +# CUDA llama.cpp built by provision_llama_cuda.sh on native-Linux Spark/aarch64 +# (and the fetched provision script). On WSL ~/.unsloth/llama.cpp is a symlink to +# the real build, which install.ps1's uninstall removes; here rm -rf clears the +# native-Linux build dir / the symlink. +_remove_path "$HOME/.unsloth/llama.cpp" +_remove_path "$HOME/.unsloth/provision_llama_cuda.sh" _remove_path "$HOME/.local/share/unsloth" # CLI shim: only the symlink Studio created, never a pip-installed file. _remove_cli_shim From 689032b7b557473147a7832b28e77a3010a747aa Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Tue, 2 Jun 2026 23:44:06 -0700 Subject: [PATCH 16/95] uninstall.ps1: encoding-proof WSL distro detection for cleanup The WSL-distro cleanup parsed 'wsl --list --quiet', whose UTF-16 output PowerShell often mis-parses into an EMPTY list, so the WSL install (/root/.unsloth + CUDA llama build) was silently never removed. Probe a candidate set ('' = default distro, Ubuntu, Ubuntu-24.04, ...) by 'wsl -d -- true' exit code instead (encoding-proof; same idiom install.ps1 uses), then run the idempotent cleanup. Co-Authored-By: Claude Opus 4.8 --- scripts/uninstall.ps1 | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/scripts/uninstall.ps1 b/scripts/uninstall.ps1 index eb059c763f..01fffad737 100644 --- a/scripts/uninstall.ps1 +++ b/scripts/uninstall.ps1 @@ -366,14 +366,26 @@ function Uninstall-UnslothStudio { # Remove the Studio install inside each WSL distro (the real GPU install + any CUDA llama.cpp build). if (Get-Command wsl.exe -ErrorAction SilentlyContinue) { try { - $distros = @(((& wsl.exe --list --quiet 2>$null) -join "`n").Replace([char]0, '') -split "`r?`n" | ForEach-Object { $_.Trim() } | Where-Object { $_ }) - foreach ($d in $distros) { - # IMPORTANT: rm runs FIRST. `pkill -f ""` matches this very - # bash -lc (its argv contains the pattern) and would SIGKILL the shell - # before a trailing rm could run. So remove files first (guaranteed), - # then best-effort kill: fuser by port (does not self-match) + pkill. - & wsl.exe -d $d -u root -- bash -lc 'rm -rf /root/.unsloth /home/*/.unsloth /root/llama-cuda /root/provision_llama_cuda.sh /root/llama_cuda_build.log 2>/dev/null; fuser -k 8888/tcp 2>/dev/null; pkill -9 -f unsloth_studio 2>/dev/null; pkill -9 -f llama-server 2>/dev/null; true' 2>$null - if ($LASTEXITCODE -eq 0) { _Substep "cleaned Unsloth from WSL distro: $d" "Green" } + # `wsl --list` emits UTF-16 that PowerShell frequently mis-parses (yielding an + # EMPTY list -> the cleanup silently skipped, leaving the WSL install behind). + # So probe a candidate set by exit code instead ('' = the default distro), + # which is encoding-proof. In the cleanup, rm runs FIRST: `pkill -f ""` + # matches this very bash -lc (its argv contains the pattern) and would SIGKILL + # the shell before a trailing rm -- so remove files first (guaranteed), then + # best-effort kill via fuser by port (does not self-match) + pkill. + $_clean = 'rm -rf /root/.unsloth /home/*/.unsloth /root/llama-cuda /root/provision_llama_cuda.sh /root/llama_cuda_build.log 2>/dev/null; fuser -k 8888/tcp 2>/dev/null; pkill -9 -f unsloth_studio 2>/dev/null; pkill -9 -f llama-server 2>/dev/null; true' + $_cands = @('', 'Ubuntu', 'Ubuntu-24.04', 'Ubuntu-22.04', 'Debian') + if ($env:UNSLOTH_WSL_DISTRO) { $_cands = @($env:UNSLOTH_WSL_DISTRO) + $_cands } + $_done = @{} + foreach ($d in $_cands) { + if ($d) { & wsl.exe -d $d -- true *> $null } else { & wsl.exe -- true *> $null } + if ($LASTEXITCODE -ne 0) { continue } + $_label = if ($d) { $d } else { "(default)" } + if ($_done[$_label]) { continue } + $_done[$_label] = $true + if ($d) { & wsl.exe -d $d -u root -- bash -lc $_clean *> $null } + else { & wsl.exe -u root -- bash -lc $_clean *> $null } + _Substep "cleaned Unsloth from WSL distro: $_label" "Green" } } catch { } } From c4071a86cbb2e4015b2e8d92aaba007d02d318b9 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Tue, 2 Jun 2026 23:47:00 -0700 Subject: [PATCH 17/95] install.ps1: propagate UNSLOTH_LLAMA_BUILD_JOBS into the WSL CUDA-llama build Windows env vars don't cross into WSL by default, so the background provision_llama_cuda.sh always built at -j(nproc). Forward UNSLOTH_LLAMA_BUILD_JOBS via 'env' so thermally/power-limited laptops can cap the build's parallelism (harmless no-op passthrough when unset). Co-Authored-By: Claude Opus 4.8 --- install.ps1 | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/install.ps1 b/install.ps1 index 855151cbbc..0f7cd7de8a 100644 --- a/install.ps1 +++ b/install.ps1 @@ -1655,7 +1655,12 @@ shell.Run cmd, 0, False $prevEapL = $ErrorActionPreference; $ErrorActionPreference = "Continue" try { $_llamaUrl = "https://raw.githubusercontent.com/unslothai/unsloth/main/studio/scripts/provision_llama_cuda.sh" - $_provCmd = 'mkdir -p /root/.unsloth; if curl -fsSL "' + $_llamaUrl + '" -o /root/.unsloth/provision_llama_cuda.sh && [ -s /root/.unsloth/provision_llama_cuda.sh ]; then chmod +x /root/.unsloth/provision_llama_cuda.sh; nohup setsid bash /root/.unsloth/provision_llama_cuda.sh > /root/.unsloth/llama_cuda_build.log 2>&1 < /dev/null & echo PROV_STARTED; else echo PROV_NOSCRIPT; fi' + # Propagate UNSLOTH_LLAMA_BUILD_JOBS into the WSL build (Windows env + # vars do not cross into WSL by default), so thermally/power-limited + # laptops can cap the CUDA build's parallelism (`env` with no + # assignment is a harmless passthrough when the var is unset). + $_jobsEnv = if ($env:UNSLOTH_LLAMA_BUILD_JOBS) { "UNSLOTH_LLAMA_BUILD_JOBS=$($env:UNSLOTH_LLAMA_BUILD_JOBS) " } else { "" } + $_provCmd = 'mkdir -p /root/.unsloth; if curl -fsSL "' + $_llamaUrl + '" -o /root/.unsloth/provision_llama_cuda.sh && [ -s /root/.unsloth/provision_llama_cuda.sh ]; then chmod +x /root/.unsloth/provision_llama_cuda.sh; nohup setsid env ' + $_jobsEnv + 'bash /root/.unsloth/provision_llama_cuda.sh > /root/.unsloth/llama_cuda_build.log 2>&1 < /dev/null & echo PROV_STARTED; else echo PROV_NOSCRIPT; fi' $_provOut = & wsl.exe -d $distro -u root -- bash -lc $_provCmd 2>$null if ("$_provOut" -match 'PROV_STARTED') { step "llama.cpp" "building CUDA llama.cpp for GGUF inference in the background (a few min); log: ~/.unsloth/llama_cuda_build.log" "Green" From 8c797daa98495b62b69b6ebc7d618f5a2a2053bf Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Tue, 2 Jun 2026 23:50:01 -0700 Subject: [PATCH 18/95] install.ps1: fix WSL-fallback probe false-positive (match torch spec) The native-CUDA-torch viability probe used a bare 'uv pip install --dry-run torch', but the real native install pins 'torch>=2.4,<2.11.0'. The cu130 index can carry an out-of-range torch (e.g. <2.4 or a >2.11 nightly) with a win_arm64 wheel, so the bare probe PASSED while the pinned install FAILED -> the WSL fallback was skipped and the install died at 'Failed to install PyTorch' on Windows-on-ARM. Use the same pinned spec in the probe so its result exactly predicts the native install. Co-Authored-By: Claude Opus 4.8 --- install.ps1 | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/install.ps1 b/install.ps1 index 0f7cd7de8a..7f93ac6b2b 100644 --- a/install.ps1 +++ b/install.ps1 @@ -1470,9 +1470,13 @@ shell.Run cmd, 0, False $_nativeCudaTorchOk = $false if ($_winArm64 -and $HasNvidiaSmi -and (-not $SkipTorch)) { # Future-proof check: can a CUDA-capable torch wheel be resolved natively for this platform/index? + # MUST use the SAME spec as the real native install below ("torch>=2.4,<2.11.0"). A bare `torch` + # probe is too loose -- the cu130 index can carry an out-of-range version (e.g. torch<2.4 or a + # nightly >2.11) whose win_arm64 wheel makes the dry-run pass, giving a FALSE POSITIVE that skips + # the WSL fallback and then fails the real install at the pinned range. $prevEapProbe = $ErrorActionPreference; $ErrorActionPreference = "Continue" try { - & uv pip install --python $VenvPython --dry-run torch --index-url $TorchIndexUrl *> $null + & uv pip install --python $VenvPython --dry-run "torch>=2.4,<2.11.0" --index-url $TorchIndexUrl *> $null $_nativeCudaTorchOk = ($LASTEXITCODE -eq 0) } catch { $_nativeCudaTorchOk = $false } finally { $ErrorActionPreference = $prevEapProbe } if ($_nativeCudaTorchOk) { step "gpu" "native CUDA PyTorch now available for win_arm64 -- keeping native install" "Green" } From a1cafb09b11d1da67921d3c40d49199b271a2262 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Tue, 2 Jun 2026 23:52:53 -0700 Subject: [PATCH 19/95] install.ps1: robust Windows-on-ARM detection under x64 emulation RuntimeInformation::OSArchitecture (and $env:PROCESSOR_ARCHITECTURE) report X64/AMD64 when install.ps1 runs under an x64-emulated PowerShell on a Windows-on-ARM host, which mis-skips the WSL fallback and then fails the native win_arm64 torch install. Add additive fallbacks (Win32_Processor.Architecture=12 ; machine-level PROCESSOR_ARCHITECTURE) that read the true OS arch even under emulation. Only turns the ARM64 path ON for genuine ARM64 hosts; x86_64/native detection unchanged. Co-Authored-By: Claude Opus 4.8 --- install.ps1 | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/install.ps1 b/install.ps1 index 7f93ac6b2b..e30d8c29fc 100644 --- a/install.ps1 +++ b/install.ps1 @@ -1467,6 +1467,20 @@ shell.Run cmd, 0, False # torch wheel, the probe below passes and the native install is kept automatically. # Opt out with UNSLOTH_NO_WSL_FALLBACK=1; choose the distro with UNSLOTH_WSL_DISTRO. try { $_winArm64 = ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString() -ieq 'Arm64') } catch { $_winArm64 = $false } + # Robust against x64-EMULATED PowerShell on Windows-on-ARM: under emulation .NET's + # OSArchitecture and $env:PROCESSOR_ARCHITECTURE both report X64/AMD64, which would + # mis-skip the WSL fallback. Win32_Processor.Architecture (12 = ARM64) and the + # machine-level PROCESSOR_ARCHITECTURE read the true OS arch even under emulation. + # Additive: can only turn $_winArm64 ON for genuine ARM64 hosts; x86_64 is unaffected. + if (-not $_winArm64) { + try { if ((@(Get-CimInstance Win32_Processor -ErrorAction Stop))[0].Architecture -eq 12) { $_winArm64 = $true } } catch {} + } + if (-not $_winArm64) { + try { + $_machArch = (Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Environment' -Name PROCESSOR_ARCHITECTURE -ErrorAction Stop).PROCESSOR_ARCHITECTURE + if ($_machArch -ieq 'ARM64') { $_winArm64 = $true } + } catch {} + } $_nativeCudaTorchOk = $false if ($_winArm64 -and $HasNvidiaSmi -and (-not $SkipTorch)) { # Future-proof check: can a CUDA-capable torch wheel be resolved natively for this platform/index? From 28a02ec303698589b1982452fc791ce7ee336290 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Tue, 2 Jun 2026 23:56:54 -0700 Subject: [PATCH 20/95] install.ps1: self-heal deps as bare names (fix PS->wsl quote mangling) The server-deps self-heal passed specs like "structlog>=24.1.0" with embedded double-quotes through PowerShell -> wsl.exe -> bash -lc; PowerShell's native-arg quoting drops the quotes, so bash parsed >= as a redirection and the whole install failed ('could not auto-install Studio server deps'). Use bare package names (uv resolves latest, satisfying the studio.txt minimums) -> no embedded quotes, no redirection. Verified: bare-name uv install populates fastapi/uvicorn/structlog/... and Studio starts (HTTP 200). Co-Authored-By: Claude Opus 4.8 --- install.ps1 | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/install.ps1 b/install.ps1 index e30d8c29fc..81bff7e45e 100644 --- a/install.ps1 +++ b/install.ps1 @@ -1571,7 +1571,13 @@ shell.Run cmd, 0, False substep "Studio web-server deps incomplete (install.sh step cut short) -- installing them now..." "Cyan" # Mirrors studio/backend/requirements/studio.txt MINUS the huggingface-hub # pin (protected above). Prefer uv (matches install.sh); fall back to pip. - $_deps = 'typer fastapi uvicorn matplotlib pandas nest_asyncio pyjwt easydict addict "structlog>=24.1.0" diceware ddgs "cryptography>=42.0.0" "httpx>=0.27.0" "fastmcp>=3.0.2"' + # Bare package names only -- NO version specifiers / embedded quotes. + # The whole repair string is passed PowerShell -> wsl.exe -> bash -lc, and + # PowerShell's native-arg quoting mangles embedded double-quotes, so a + # "structlog>=24.1.0" loses its quotes and bash parses `>=` as a redirection, + # failing the whole install. uv resolves the latest of each (which satisfies + # the studio.txt minimums anyway), so bare names are sufficient and safe. + $_deps = 'typer fastapi uvicorn matplotlib pandas nest_asyncio pyjwt easydict addict structlog diceware ddgs cryptography httpx fastmcp' $_repair = 'PY=/root/.unsloth/studio/unsloth_studio/bin/python; UV="$(command -v uv 2>/dev/null || echo /root/.local/bin/uv)"; if [ -x "$UV" ] || command -v uv >/dev/null 2>&1; then "$UV" pip install --python "$PY" ' + $_deps + '; else "$PY" -m pip install ' + $_deps + '; fi' $prevEapR = $ErrorActionPreference; $ErrorActionPreference = "Continue" try { & wsl.exe -d $distro -u root -- bash -lc $_repair } catch {} finally { $ErrorActionPreference = $prevEapR } From cc31b87850f45869225661a9af8fb93862573dfd Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Wed, 3 Jun 2026 00:35:47 -0700 Subject: [PATCH 21/95] provision_llama_cuda: put Linux dirs first in PATH (WSL interop hygiene) When the installer is launched from a Windows shell, WSL interop leaks the Windows PATH (/mnt/c/... entries, with spaces) into the build environment, which can make cmake/gcc/git resolve to Windows tools or otherwise confuse the CUDA build. Prepend the CUDA toolkit + standard Linux dirs so the Linux toolchain always wins; keep the original PATH after so nvidia-smi etc. still resolve. Co-Authored-By: Claude Opus 4.8 --- studio/scripts/provision_llama_cuda.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/studio/scripts/provision_llama_cuda.sh b/studio/scripts/provision_llama_cuda.sh index fda37b445c..694979ab46 100644 --- a/studio/scripts/provision_llama_cuda.sh +++ b/studio/scripts/provision_llama_cuda.sh @@ -83,7 +83,12 @@ if [ -z "$NVCC" ]; then fi CUDA_HOME="$(dirname "$(dirname "$NVCC")")" -export PATH="$CUDA_HOME/bin:$PATH" +# Put the CUDA toolkit + standard Linux dirs FIRST so the build always uses the +# Linux cmake/gcc/git, never a Windows tool that leaked into PATH via WSL interop +# when the installer is launched from a Windows shell (those /mnt/c entries also +# contain spaces that can confuse the build). Original PATH kept after so things +# like nvidia-smi still resolve. +export PATH="$CUDA_HOME/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$PATH" export CUDAToolkit_ROOT="$CUDA_HOME" # 4. Host compiler: prefer gcc-14 / g++-14 (nvcc rejects 15). From f102eccaa442b12411bba5029e85587b076d9fff Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Wed, 3 Jun 2026 01:08:44 -0700 Subject: [PATCH 22/95] install.ps1/uninstall.ps1: add UNSLOTH_INSTALL_REF + fix WSL symlink uninstall hole install.ps1: fetch repo-versioned WSL-fallback assets (provision_llama_cuda.sh, unsloth.ico) from a configurable git ref via new UNSLOTH_INSTALL_REF env var (defaults to main, so existing users are byte-for-byte unaffected). Lets the ARM64+NVIDIA WSL-fallback GPU path be exercised end-to-end on a branch before it merges (provision_llama_cuda.sh does not exist on main until then). uninstall.ps1: the WSL cleanup rm -rf'd /root/.unsloth but left the ~/.local/bin/unsloth launcher symlink dangling, so `unsloth` still resolved on PATH after an uninstall. Also remove /root/.local/bin/unsloth and /home/*/.local/bin/unsloth. Co-Authored-By: Claude Opus 4.8 --- install.ps1 | 18 +++++++++++++++--- scripts/uninstall.ps1 | 6 +++++- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/install.ps1 b/install.ps1 index 81bff7e45e..c4e09adc27 100644 --- a/install.ps1 +++ b/install.ps1 @@ -8,6 +8,9 @@ # UNSLOTH_STUDIO_HOME / STUDIO_HOME = path -> install under that path # (DataDir nests inside; user PATH not modified persistently). # Default ($USERPROFILE\.unsloth\studio) is preserved when no env var is set. +# UNSLOTH_INSTALL_REF = branch/tag/sha to fetch repo-versioned install assets +# from (provision_llama_cuda.sh, the .ico); defaults to 'main'. Lets the +# WSL-fallback GPU path be tested on a branch before it merges. function Install-UnslothStudio { $ErrorActionPreference = "Stop" @@ -42,6 +45,15 @@ function Install-UnslothStudio { } } + # Git ref (branch/tag/sha) used to fetch repo-versioned install assets from + # raw.githubusercontent.com: provision_llama_cuda.sh and the .ico. Defaults to + # 'main' so existing users are byte-for-byte unaffected; set UNSLOTH_INSTALL_REF + # to a branch to exercise the WSL-fallback path end-to-end before a PR merges. + function Get-UnslothInstallRef { + if ($env:UNSLOTH_INSTALL_REF -and $env:UNSLOTH_INSTALL_REF.Trim()) { return $env:UNSLOTH_INSTALL_REF.Trim() } + return 'main' + } + function Get-TauriTorchIndexFamily { param([string]$TorchIndexUrl) if ($SkipTorch) { return "none" } @@ -519,7 +531,7 @@ function Install-UnslothStudio { if ($PSScriptRoot -and $PSScriptRoot.Trim()) { $bundledIcon = Join-Path $PSScriptRoot "studio\frontend\public\unsloth.ico" } - $iconUrl = "https://raw.githubusercontent.com/unslothai/unsloth/main/studio/frontend/public/unsloth.ico" + $iconUrl = "https://raw.githubusercontent.com/unslothai/unsloth/$(Get-UnslothInstallRef)/studio/frontend/public/unsloth.ico" if (-not (Test-Path -LiteralPath $appDir)) { [System.IO.Directory]::CreateDirectory($appDir) | Out-Null @@ -1640,7 +1652,7 @@ shell.Run cmd, 0, False ) Set-Content -LiteralPath $launcher -Value $L -Encoding UTF8 $icon = Join-Path $appDir "unsloth.ico" - try { if (-not (Test-Path -LiteralPath $icon)) { Invoke-WebRequest "https://raw.githubusercontent.com/unslothai/unsloth/main/studio/frontend/public/unsloth.ico" -OutFile $icon -UseBasicParsing -TimeoutSec 15 *> $null } } catch {} + try { if (-not (Test-Path -LiteralPath $icon)) { Invoke-WebRequest "https://raw.githubusercontent.com/unslothai/unsloth/$(Get-UnslothInstallRef)/studio/frontend/public/unsloth.ico" -OutFile $icon -UseBasicParsing -TimeoutSec 15 *> $null } } catch {} $wsh = New-Object -ComObject WScript.Shell $lnks = @() $dd = [Environment]::GetFolderPath("Desktop"); if ($dd -and $dd.Trim()) { $lnks += (Join-Path $dd "Unsloth Studio.lnk") } @@ -1678,7 +1690,7 @@ shell.Run cmd, 0, False if ($env:UNSLOTH_NO_LLAMA_CUDA -ne '1') { $prevEapL = $ErrorActionPreference; $ErrorActionPreference = "Continue" try { - $_llamaUrl = "https://raw.githubusercontent.com/unslothai/unsloth/main/studio/scripts/provision_llama_cuda.sh" + $_llamaUrl = "https://raw.githubusercontent.com/unslothai/unsloth/$(Get-UnslothInstallRef)/studio/scripts/provision_llama_cuda.sh" # Propagate UNSLOTH_LLAMA_BUILD_JOBS into the WSL build (Windows env # vars do not cross into WSL by default), so thermally/power-limited # laptops can cap the CUDA build's parallelism (`env` with no diff --git a/scripts/uninstall.ps1 b/scripts/uninstall.ps1 index 01fffad737..782cc39290 100644 --- a/scripts/uninstall.ps1 +++ b/scripts/uninstall.ps1 @@ -373,7 +373,11 @@ function Uninstall-UnslothStudio { # matches this very bash -lc (its argv contains the pattern) and would SIGKILL # the shell before a trailing rm -- so remove files first (guaranteed), then # best-effort kill via fuser by port (does not self-match) + pkill. - $_clean = 'rm -rf /root/.unsloth /home/*/.unsloth /root/llama-cuda /root/provision_llama_cuda.sh /root/llama_cuda_build.log 2>/dev/null; fuser -k 8888/tcp 2>/dev/null; pkill -9 -f unsloth_studio 2>/dev/null; pkill -9 -f llama-server 2>/dev/null; true' + # Also remove the `unsloth` launcher symlink install.sh drops at + # ~/.local/bin/unsloth -> /bin/unsloth. rm -rf of ~/.unsloth + # above deletes its target but leaves the symlink dangling, so the + # `unsloth` command still resolves on PATH after an uninstall. + $_clean = 'rm -rf /root/.unsloth /home/*/.unsloth /root/llama-cuda /root/provision_llama_cuda.sh /root/llama_cuda_build.log 2>/dev/null; rm -f /root/.local/bin/unsloth /home/*/.local/bin/unsloth 2>/dev/null; fuser -k 8888/tcp 2>/dev/null; pkill -9 -f unsloth_studio 2>/dev/null; pkill -9 -f llama-server 2>/dev/null; true' $_cands = @('', 'Ubuntu', 'Ubuntu-24.04', 'Ubuntu-22.04', 'Debian') if ($env:UNSLOTH_WSL_DISTRO) { $_cands = @($env:UNSLOTH_WSL_DISTRO) + $_cands } $_done = @{} From bb3676d2c8c869bb4c903b43b086e639185bd692 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Wed, 3 Jun 2026 03:16:06 -0700 Subject: [PATCH 23/95] llama.cpp CUDA detection: handle dlopen-ed backend (split build layout) Current llama.cpp ships the CUDA backend as a dynamically-loaded plugin (libggml-cuda.so* next to the binary), NOT a load-time dependency, so ldd llama-server | grep libggml-cuda is a false negative: it reports no CUDA on a perfectly good CUDA build. That made both is_cuda_server() (provision_llama_cuda.sh) and _have_cuda_llama_server() (setup.sh) force a needless full rebuild every run. Fix both: keep the ldd check (old monolithic builds) and additionally treat the presence of libggml-cuda.so* beside the binary as the CUDA signal. A CPU-only build has no such backend, so this stays correct for the CPU case. Verified on an N1X/sm_121 WSL build: llama-server --list-devices shows CUDA0 JMJWOA-Generic-GPU and serves on the GPU, while ldd lists no libggml-cuda; the new check correctly returns CUDA-present. Co-Authored-By: Claude Opus 4.8 --- studio/scripts/provision_llama_cuda.sh | 14 +++++++++++++- studio/setup.sh | 10 +++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/studio/scripts/provision_llama_cuda.sh b/studio/scripts/provision_llama_cuda.sh index 694979ab46..3f20527288 100644 --- a/studio/scripts/provision_llama_cuda.sh +++ b/studio/scripts/provision_llama_cuda.sh @@ -21,7 +21,19 @@ LLAMA_DIR="${UNSLOTH_LLAMA_CPP_PATH:-$HOME/.unsloth/llama.cpp}" SERVER="$LLAMA_DIR/build/bin/llama-server" log() { printf ' - %s\n' "$*"; } -is_cuda_server() { [ -x "$1" ] && ldd "$1" 2>/dev/null | grep -qi 'libggml-cuda'; } +# A llama-server is CUDA-capable in either of two build layouts: +# * old monolithic build -> libggml-cuda is a direct load-time dependency (ldd shows it) +# * current split build -> CUDA ships as a dlopen-ed backend plugin, libggml-cuda.so*, +# sitting next to the binary; ldd will NOT list it +# Checking only ldd (the old behaviour) is a false negative on current llama.cpp and would +# force a pointless full rebuild every run. A CPU-only build has no libggml-cuda.so at all, +# so the presence of that backend beside the binary is the reliable CUDA signal. +is_cuda_server() { + [ -x "$1" ] || return 1 + ldd "$1" 2>/dev/null | grep -qi 'libggml-cuda' && return 0 + for _so in "$(dirname "$1")"/libggml-cuda.so*; do [ -e "$_so" ] && return 0; done + return 1 +} # 0. Already provisioned? if is_cuda_server "$SERVER"; then diff --git a/studio/setup.sh b/studio/setup.sh index 5aa7f0497a..3e17444244 100755 --- a/studio/setup.sh +++ b/studio/setup.sh @@ -1410,8 +1410,16 @@ fi # end _SKIP_GGUF_BUILD check # already built a CUDA server are byte-for-byte unaffected. Opt out with # UNSLOTH_NO_LLAMA_CUDA=1. Best-effort: never aborts setup (provision script always # exits 0; failures leave the prior CPU/degraded state for the fallback below). +# CUDA-capable in either build layout: old monolithic (libggml-cuda is a direct +# ldd dependency) or current split build (CUDA is a dlopen-ed backend, libggml-cuda.so*, +# beside the binary -- ldd will NOT list it). Checking only ldd is a false negative on +# current llama.cpp and would force a needless rebuild; a CPU-only build has no +# libggml-cuda.so at all, so its presence beside the binary is the reliable signal. _have_cuda_llama_server() { - [ -x "$LLAMA_SERVER_BIN" ] && ldd "$LLAMA_SERVER_BIN" 2>/dev/null | grep -qi 'libggml-cuda' + [ -x "$LLAMA_SERVER_BIN" ] || return 1 + ldd "$LLAMA_SERVER_BIN" 2>/dev/null | grep -qi 'libggml-cuda' && return 0 + for _so in "$(dirname "$LLAMA_SERVER_BIN")"/libggml-cuda.so*; do [ -e "$_so" ] && return 0; done + return 1 } if [ "$_HOST_SYSTEM" = "Linux" ] \ && { [ "$_HOST_MACHINE" = "aarch64" ] || [ "$_HOST_MACHINE" = "arm64" ]; } \ From 5ecadb8512e6b28384ad8c9dd516bda177593353 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Wed, 3 Jun 2026 03:53:14 -0700 Subject: [PATCH 24/95] uninstall.ps1: kill llama-server too (pkill self-match made it a no-op) The WSL cleanup ran `pkill -9 -f unsloth_studio` then `pkill -9 -f llama-server`, but the `bash -lc ` shell's own argv contains those literal patterns, so the first pkill SIGKILLed the shell before the llama-server pkill (and trailing `true`) ever ran -- leaving a running llama-server (dynamic port, not covered by `fuser -k 8888`) alive after uninstall. Use the [x]-regex self-exclusion trick ('[u]nsloth_studio' / '[l]lama-server') so the shell's argv no longer contains the matched substring; real target processes still match. Verified in WSL: shell survives, both dummy processes are killed. Co-Authored-By: Claude Opus 4.8 --- scripts/uninstall.ps1 | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/scripts/uninstall.ps1 b/scripts/uninstall.ps1 index 782cc39290..5dc4052db9 100644 --- a/scripts/uninstall.ps1 +++ b/scripts/uninstall.ps1 @@ -377,7 +377,16 @@ function Uninstall-UnslothStudio { # ~/.local/bin/unsloth -> /bin/unsloth. rm -rf of ~/.unsloth # above deletes its target but leaves the symlink dangling, so the # `unsloth` command still resolves on PATH after an uninstall. - $_clean = 'rm -rf /root/.unsloth /home/*/.unsloth /root/llama-cuda /root/provision_llama_cuda.sh /root/llama_cuda_build.log 2>/dev/null; rm -f /root/.local/bin/unsloth /home/*/.local/bin/unsloth 2>/dev/null; fuser -k 8888/tcp 2>/dev/null; pkill -9 -f unsloth_studio 2>/dev/null; pkill -9 -f llama-server 2>/dev/null; true' + # The pkill patterns use the [x]-regex self-exclusion trick: this very + # `bash -lc ` shell's own argv contains the literal pattern text, so a + # plain `pkill -f unsloth_studio` would match (and SIGKILL) the shell itself + # before the next command runs -- which is why rm goes first AND why the second + # pkill (llama-server, a dynamic port not covered by `fuser -k 8888`) never + # fired. Writing the pattern as '[u]nsloth_studio' means the shell's argv holds + # "[u]nsloth_studio" (no literal "unsloth_studio" substring) so it no longer + # self-matches, while real target processes (cmdline contains "unsloth_studio") + # still match. Same for '[l]lama-server'. + $_clean = 'rm -rf /root/.unsloth /home/*/.unsloth /root/llama-cuda /root/provision_llama_cuda.sh /root/llama_cuda_build.log 2>/dev/null; rm -f /root/.local/bin/unsloth /home/*/.local/bin/unsloth 2>/dev/null; fuser -k 8888/tcp 2>/dev/null; pkill -9 -f ''[u]nsloth_studio'' 2>/dev/null; pkill -9 -f ''[l]lama-server'' 2>/dev/null; true' $_cands = @('', 'Ubuntu', 'Ubuntu-24.04', 'Ubuntu-22.04', 'Debian') if ($env:UNSLOTH_WSL_DISTRO) { $_cands = @($env:UNSLOTH_WSL_DISTRO) + $_cands } $_done = @{} From 9e26277cb8e3934f35985ac6b29fb797f8e68c56 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Wed, 3 Jun 2026 04:22:43 -0700 Subject: [PATCH 25/95] setup.sh: skip CPU llama.cpp source build on WSL2 aarch64+NVIDIA (defer to background CUDA build) On Windows-on-ARM + NVIDIA (DGX Spark / N1X "RTX Spark"), install.ps1 routes the install through WSL2 and, after setup finishes, launches provision_llama_cuda.sh in the BACKGROUND to install the CUDA toolkit + gcc-14 and build the real sm_121 CUDA llama-server, replacing whatever section 9 produced. On a fresh WSL distro there is no nvcc yet, so section 9 could only ever build a CPU-only server ("building (CPU, CUDA driver found but nvcc missing)") that the background CUDA build immediately throws away -- slow and wasteful. Skip the section-9 source build entirely on this exact path. Introduce a distinct _LLAMA_CPP_DEFERRED state (NOT _LLAMA_CPP_DEGRADED) so: - the footer reports "GGUF engine: CUDA build running in background" (success), not "limited: llama.cpp unavailable"; - the arm64 CPU-prebuilt last-resort does NOT fire (it gates on DEGRADED=true); - the install-failure exit 1 does NOT fire (it gates on DEGRADED=true). Strictly gated -- defers only when ALL hold: WSL (grep microsoft /proc/version), aarch64/arm64, an NVIDIA GPU is listed by nvidia-smi, nvcc is missing (PATH and /usr/local/cuda*/bin), UNSLOTH_NO_LLAMA_CUDA != 1, no forced compile, no pinned PR. Every other host (x86_64, native-Linux aarch64, nvcc-present, opt-out, ROCm, macOS, non-NVIDIA) is byte-for-byte unaffected and still builds via section 9 as before. install.ps1 is unchanged; it still builds CUDA in the background, but now with no wasted CPU build first. Co-Authored-By: Claude Opus 4.8 --- studio/setup.sh | 59 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 56 insertions(+), 3 deletions(-) diff --git a/studio/setup.sh b/studio/setup.sh index 3e17444244..095428b39e 100755 --- a/studio/setup.sh +++ b/studio/setup.sh @@ -756,6 +756,12 @@ LLAMA_CPP_DIR="$UNSLOTH_HOME/llama.cpp" LLAMA_SERVER_BIN="$LLAMA_CPP_DIR/build/bin/llama-server" _NEED_LLAMA_SOURCE_BUILD=false _LLAMA_CPP_DEGRADED=false +# Distinct from _LLAMA_CPP_DEGRADED: on WSL2 aarch64+NVIDIA with no nvcc yet, the +# CPU source build is skipped because install.ps1 builds the real CUDA server in +# the BACKGROUND. There is temporarily no llama-server, but that is a SUCCESS +# (CUDA build in progress), NOT a degraded/failed install -- so it must not trip +# the arm64 CPU-prebuilt last-resort or the install-failure exit 1. +_LLAMA_CPP_DEFERRED=false _LLAMA_FORCE_COMPILE="${UNSLOTH_LLAMA_FORCE_COMPILE:-0}" _REQUESTED_LLAMA_TAG="${UNSLOTH_LLAMA_TAG:-${_DEFAULT_LLAMA_TAG}}" _HOST_SYSTEM="$(uname -s 2>/dev/null || true)" @@ -927,6 +933,47 @@ if [ "$_NEED_LLAMA_SOURCE_BUILD" = true ] && \ _NEED_LLAMA_SOURCE_BUILD=false fi +# ── WSL2 aarch64 + NVIDIA, no nvcc yet: defer to the background CUDA build ── +# On Windows-on-ARM + NVIDIA (DGX Spark / N1X "RTX Spark"), install.ps1 routes +# through WSL2 and, after this install finishes, launches provision_llama_cuda.sh +# in the BACKGROUND (installs CUDA 13.3 + gcc-14, builds the real sm_121 CUDA +# llama-server into ~/.unsloth/llama.cpp, replacing whatever is here). On a fresh +# WSL distro there is no CUDA toolkit (nvcc) yet, so the section-9 source build +# below can only produce a CPU-only server ("building (CPU, CUDA driver found but +# nvcc missing)") -- which is SLOW and immediately thrown away by that background +# CUDA build. So skip the source build entirely on this exact path: the +# background CUDA provision is the sole builder, and the CPU build is pure waste. +# +# Strictly gated. ALL must hold: +# - running under WSL (grep microsoft /proc/version) +# - aarch64/arm64 ($_HOST_MACHINE) +# - an NVIDIA GPU is present (nvidia-smi lists a GPU) +# - nvcc is MISSING (no nvcc on PATH, none under /usr/local/cuda*) +# - the CUDA provision is NOT opted out (UNSLOTH_NO_LLAMA_CUDA != 1) +# - user did not force a compile / pin a PR (_LLAMA_FORCE_COMPILE != 1, no _LLAMA_PR) +# If nvcc IS already present we fall through to section 9 and build CUDA directly. +# If UNSLOTH_NO_LLAMA_CUDA=1 the background build never runs, so we KEEP the CPU +# source build as the user's only llama-server (do not defer). +if [ "$_NEED_LLAMA_SOURCE_BUILD" = true ] \ + && [ "$_LLAMA_FORCE_COMPILE" != "1" ] \ + && [ -z "$_LLAMA_PR" ] \ + && [ "${UNSLOTH_NO_LLAMA_CUDA:-0}" != "1" ] \ + && grep -qi microsoft /proc/version 2>/dev/null \ + && { [ "$_HOST_MACHINE" = "aarch64" ] || [ "$_HOST_MACHINE" = "arm64" ]; } \ + && command -v nvidia-smi >/dev/null 2>&1 \ + && nvidia-smi -L 2>/dev/null | awk '/^GPU[[:space:]]+[0-9]+:/{found=1} END{exit !found}' \ + && ! command -v nvcc >/dev/null 2>&1 \ + && ! ls /usr/local/cuda*/bin/nvcc >/dev/null 2>&1; then + step "llama.cpp" "GGUF engine: CUDA build running in background (WSL aarch64 + NVIDIA)" "$C_WARN" + substep "skipping slow CPU build; the background CUDA llama.cpp will provide the server" + substep "(opt out / keep CPU build with UNSLOTH_NO_LLAMA_CUDA=1)" + # Sole builder is install.ps1's background provision_llama_cuda.sh. Do NOT set + # _LLAMA_CPP_DEGRADED (that would trigger the arm64 CPU-prebuilt last resort + # and the install-failure exit 1); use the distinct DEFERRED state instead. + _NEED_LLAMA_SOURCE_BUILD=false + _LLAMA_CPP_DEFERRED=true +fi + # ── 8. WSL: pre-install GGUF build dependencies for fallback source builds ── # On WSL, sudo requires a password and can't be entered during GGUF export # (runs in a non-interactive subprocess). Install build deps here instead. @@ -1497,7 +1544,9 @@ fi if [ "$_LLAMA_ONLY" = "1" ]; then echo "" printf " ${C_DIM}%s${C_RST}\n" "$RULE" - if [ "$_LLAMA_CPP_DEGRADED" = true ]; then + if [ "$_LLAMA_CPP_DEFERRED" = true ]; then + printf " ${C_TITLE}%s${C_RST}\n" "llama.cpp update finished (GGUF engine: CUDA build running in background)" + elif [ "$_LLAMA_CPP_DEGRADED" = true ]; then printf " ${C_WARN}%s${C_RST}\n" "llama.cpp update finished (limited: llama.cpp unavailable)" else printf " ${C_TITLE}%s${C_RST}\n" "llama.cpp update finished" @@ -1506,7 +1555,9 @@ if [ "$_LLAMA_ONLY" = "1" ]; then elif [ "$IS_COLAB" = true ]; then echo "" printf " ${C_DIM}%s${C_RST}\n" "$RULE" - if [ "$_LLAMA_CPP_DEGRADED" = true ]; then + if [ "$_LLAMA_CPP_DEFERRED" = true ]; then + printf " ${C_TITLE}%s${C_RST}\n" "Unsloth Studio Setup Complete (GGUF engine: CUDA build running in background)" + elif [ "$_LLAMA_CPP_DEGRADED" = true ]; then printf " ${C_WARN}%s${C_RST}\n" "Unsloth Studio Setup Complete (limited: llama.cpp unavailable)" else printf " ${C_TITLE}%s${C_RST}\n" "Unsloth Studio Setup Complete" @@ -1516,7 +1567,9 @@ elif [ "$IS_COLAB" = true ]; then substep "start()" else printf " ${C_DIM}%s${C_RST}\n" "$RULE" - if [ "$_LLAMA_CPP_DEGRADED" = true ]; then + if [ "$_LLAMA_CPP_DEFERRED" = true ]; then + printf " ${C_TITLE}%s${C_RST}\n" "Unsloth Studio Installed (GGUF engine: CUDA build running in background)" + elif [ "$_LLAMA_CPP_DEGRADED" = true ]; then printf " ${C_WARN}%s${C_RST}\n" "Unsloth Studio Installed (limited: llama.cpp unavailable)" else printf " ${C_TITLE}%s${C_RST}\n" "Unsloth Studio Installed" From b291c13a97ddfebdb5214177317d614048324336 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Wed, 3 Jun 2026 04:33:46 -0700 Subject: [PATCH 26/95] WSL CUDA llama: make the background build reliable + clean (go straight to GPU) Three coupled fixes so the Windows-on-ARM + NVIDIA (DGX Spark / N1X) WSL path builds the GPU llama-server reliably and never wastes time on a CPU build: 1. install.ps1 -- the post-install CUDA provision was launched as a WSL-side `nohup setsid ... &`. That does NOT survive: WSL shuts the distro's VM down once the launching wsl.exe session exits, killing the detached build (observed on a fresh distro: no build log, only a CPU server left behind). Fetch the provision script in a quick session, then run the build anchored to a DETACHED Windows-side process (Start-Process wsl.exe, no -Wait) that holds the VM up for the whole build while install.ps1 returns immediately. 2. provision_llama_cuda.sh -- a pre-existing build/ can carry an incompatible CMake cache (the Studio installer stages its build in llama.cpp.build.NNNN then relocates it, leaving a cache with stale absolute source/build paths and GGML_CUDA=OFF), so reconfiguring for CUDA fails ("CMakeCache directory is different" / "source does not match"). Try to reuse build/ first (incremental resume), and if configure fails, wipe build/ and configure clean once. Verified live on the failing scenario: stale cache detected, wiped, clean CUDA configure. (setup.sh's skip of the CPU source build on this path is the companion commit; together the fresh-install path builds only the CUDA server, in the background.) Co-Authored-By: Claude Opus 4.8 --- install.ps1 | 18 ++++++++++++++---- studio/scripts/provision_llama_cuda.sh | 24 +++++++++++++++++++----- 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/install.ps1 b/install.ps1 index c4e09adc27..064b32ea3c 100644 --- a/install.ps1 +++ b/install.ps1 @@ -1695,10 +1695,20 @@ shell.Run cmd, 0, False # vars do not cross into WSL by default), so thermally/power-limited # laptops can cap the CUDA build's parallelism (`env` with no # assignment is a harmless passthrough when the var is unset). - $_jobsEnv = if ($env:UNSLOTH_LLAMA_BUILD_JOBS) { "UNSLOTH_LLAMA_BUILD_JOBS=$($env:UNSLOTH_LLAMA_BUILD_JOBS) " } else { "" } - $_provCmd = 'mkdir -p /root/.unsloth; if curl -fsSL "' + $_llamaUrl + '" -o /root/.unsloth/provision_llama_cuda.sh && [ -s /root/.unsloth/provision_llama_cuda.sh ]; then chmod +x /root/.unsloth/provision_llama_cuda.sh; nohup setsid env ' + $_jobsEnv + 'bash /root/.unsloth/provision_llama_cuda.sh > /root/.unsloth/llama_cuda_build.log 2>&1 < /dev/null & echo PROV_STARTED; else echo PROV_NOSCRIPT; fi' - $_provOut = & wsl.exe -d $distro -u root -- bash -lc $_provCmd 2>$null - if ("$_provOut" -match 'PROV_STARTED') { + # Step 1: fetch the provision script (quick; a transient WSL session is fine). + $_fetchCmd = 'mkdir -p /root/.unsloth; if curl -fsSL "' + $_llamaUrl + '" -o /root/.unsloth/provision_llama_cuda.sh && [ -s /root/.unsloth/provision_llama_cuda.sh ]; then chmod +x /root/.unsloth/provision_llama_cuda.sh; echo PROV_FETCHED; else echo PROV_NOSCRIPT; fi' + $_fetchOut = & wsl.exe -d $distro -u root -- bash -lc $_fetchCmd 2>$null + if ("$_fetchOut" -match 'PROV_FETCHED') { + # Step 2: run the build anchored to a DETACHED WINDOWS process. A WSL-side + # `nohup setsid ... &` does NOT survive here: WSL shuts the distro's VM down + # once the launching wsl.exe session exits, killing any backgrounded build + # (observed: no build log, only the CPU server left behind). A persistent + # Windows-side wsl.exe (Start-Process, no -Wait) holds the VM up for the whole + # build while install.ps1 returns immediately. Job count crosses via `env`. + # No double-quotes in the bash string -> clean through Start-Process arg array. + $_jobsPrefix = if ($env:UNSLOTH_LLAMA_BUILD_JOBS) { "env UNSLOTH_LLAMA_BUILD_JOBS=$($env:UNSLOTH_LLAMA_BUILD_JOBS) " } else { "" } + $_buildCmd = $_jobsPrefix + 'bash /root/.unsloth/provision_llama_cuda.sh > /root/.unsloth/llama_cuda_build.log 2>&1' + Start-Process -WindowStyle Hidden -FilePath 'wsl.exe' -ArgumentList @('-d', $distro, '-u', 'root', '--', 'bash', '-lc', $_buildCmd) | Out-Null step "llama.cpp" "building CUDA llama.cpp for GGUF inference in the background (a few min); log: ~/.unsloth/llama_cuda_build.log" "Green" } else { substep "(GGUF inference needs a CUDA llama.cpp build; build later: wsl -d $distro -u root -- bash ~/.unsloth/provision_llama_cuda.sh)" "Yellow" diff --git a/studio/scripts/provision_llama_cuda.sh b/studio/scripts/provision_llama_cuda.sh index 3f20527288..cf23f71efc 100644 --- a/studio/scripts/provision_llama_cuda.sh +++ b/studio/scripts/provision_llama_cuda.sh @@ -122,11 +122,25 @@ fi cd "$LLAMA_DIR" || exit 0 log "building CUDA llama.cpp (arch=$CUDA_ARCH, host=$HCXX) - this takes a few minutes..." -cmake -S . -B build -DCMAKE_BUILD_TYPE=Release \ - -DGGML_CUDA=ON -DGGML_CUDA_F16=ON \ - -DCMAKE_CUDA_ARCHITECTURES="$CUDA_ARCH" \ - -DCMAKE_CUDA_HOST_COMPILER="$HCXX" \ - -DLLAMA_CURL=ON >/dev/null 2>&1 || { log "cmake configure failed"; exit 0; } +_cmake_configure() { + cmake -S . -B build -DCMAKE_BUILD_TYPE=Release \ + -DGGML_CUDA=ON -DGGML_CUDA_F16=ON \ + -DCMAKE_CUDA_ARCHITECTURES="$CUDA_ARCH" \ + -DCMAKE_CUDA_HOST_COMPILER="$HCXX" \ + -DLLAMA_CURL=ON >/dev/null 2>&1 +} +# A pre-existing build/ may carry an INCOMPATIBLE CMake cache. The common case: the +# Studio installer (setup.sh / install_llama_prebuilt.py) stages its llama.cpp build in +# a versioned dir (llama.cpp.build.NNNN) then RELOCATES it here, leaving a cache whose +# baked-in absolute source/build paths no longer match (and GGML_CUDA=OFF). Re-running +# CUDA configure over that fails ("CMakeCache directory is different" / "source does not +# match"). So try to reuse build/ first (fast incremental resume on a re-run after a +# partial CUDA build), and only if configure fails, wipe build/ and configure clean once. +if ! _cmake_configure; then + log "stale/incompatible CMake cache detected; wiping build dir for a clean CUDA configure" + rm -rf build + _cmake_configure || { log "cmake configure failed"; exit 0; } +fi # Build the full set unsloth-zoo's GGUF exporter expects too (llama-mtmd-cli, # llama-gguf-split), so a pre-provisioned build satisfies both Studio inference # AND save_pretrained_gguf without triggering a --clean-first rebuild later. From abb99d52ca281c540806442320ffa7e10cdbf9c0 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Wed, 3 Jun 2026 04:38:21 -0700 Subject: [PATCH 27/95] install.ps1: UNSLOTH_INSTALL_REF also drives the WSL unsloth install So a branch can be tested end-to-end pre-merge: when UNSLOTH_INSTALL_REF is set (not main), pass install.sh `--package git+https://github.com/unslothai/unsloth@` so the WSL studio venv carries THAT ref's studio/setup.sh + unsloth Python patches (otherwise install.sh installs released PyPI unsloth and the branch's setup.sh -- e.g. the WSL CPU-build skip -- never runs). Default (ref = main) is byte-identical to before. The git URL has no spaces, so it survives PowerShell -> wsl.exe -> bash -lc. Co-Authored-By: Claude Opus 4.8 --- install.ps1 | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/install.ps1 b/install.ps1 index 064b32ea3c..cd98f56064 100644 --- a/install.ps1 +++ b/install.ps1 @@ -1541,7 +1541,15 @@ shell.Run cmd, 0, False try { & wsl.exe --install -d $distro --no-launch } catch {} } substep "installing Unsloth Studio inside WSL '$distro' with full GPU (this downloads PyTorch)..." "Cyan" - $wslInstall = 'export DEBIAN_FRONTEND=noninteractive; apt-get update -y >/dev/null 2>&1; apt-get install -y build-essential cmake git curl pciutils >/dev/null 2>&1; curl -fsSL https://unsloth.ai/install.sh | sh' + # When UNSLOTH_INSTALL_REF is a branch/tag/sha (not "main"), install unsloth FROM that + # ref via install.sh's --package, so the WSL studio package carries THIS ref's + # studio/setup.sh + unsloth Python patches. Otherwise install.sh pulls released PyPI + # unsloth and the branch's setup.sh (e.g. the WSL CPU-build skip) would not run + # pre-merge. Default (ref = main) is byte-identical to before: plain `... | sh`. + # The git URL has no spaces, so it passes cleanly PowerShell -> wsl.exe -> bash -lc. + $_instRef = Get-UnslothInstallRef + $_pkgArg = if ($_instRef -eq 'main') { '' } else { ' -s -- --package git+https://github.com/unslothai/unsloth@' + $_instRef } + $wslInstall = 'export DEBIAN_FRONTEND=noninteractive; apt-get update -y >/dev/null 2>&1; apt-get install -y build-essential cmake git curl pciutils >/dev/null 2>&1; curl -fsSL https://unsloth.ai/install.sh | sh' + $_pkgArg # install.sh writes diagnostics to stderr and may exit non-zero on the optional llama.cpp # prebuilt step (no aarch64 prebuilt exists) -- that must NOT abort us under -ErrorAction Stop, # since torch + unsloth + Studio still install. Lower EAP around the call (same idiom as above). From 4a566fe7d6fd269e4b51c169b51c69e0367c5a15 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Wed, 3 Jun 2026 04:48:49 -0700 Subject: [PATCH 28/95] UNSLOTH_INSTALL_REF: install unsloth from the ref in WSL (env, not --package) The previous attempt passed install.sh `--package git+https://...@ref`, but install.sh validates --package and rejects URL characters ("invalid characters") -> the WSL install aborted (exit 127). Fix it properly and symmetrically: - install.sh: add a gated UNSLOTH_INSTALL_REF path that installs `unsloth @ git+https://github.com/unslothai/unsloth@` via uv. Gated to the default package ("unsloth") and a non-"main" ref, so released-PyPI behavior is unchanged by default. Bypasses the --package name validation (fixed literal URL, no injection surface). - install.ps1: when UNSLOTH_INSTALL_REF is a branch, fetch THAT ref's install.sh (which honors the env) and export UNSLOTH_INSTALL_REF, so the WSL studio venv carries the branch's studio/setup.sh + unsloth Python (e.g. the WSL CPU-build skip is actually exercised pre-merge). Default (ref = main) is byte-identical: `curl https://unsloth.ai/install.sh | sh`. Co-Authored-By: Claude Opus 4.8 --- install.ps1 | 21 +++++++++++++-------- install.sh | 9 +++++++++ 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/install.ps1 b/install.ps1 index cd98f56064..097602f34b 100644 --- a/install.ps1 +++ b/install.ps1 @@ -1541,15 +1541,20 @@ shell.Run cmd, 0, False try { & wsl.exe --install -d $distro --no-launch } catch {} } substep "installing Unsloth Studio inside WSL '$distro' with full GPU (this downloads PyTorch)..." "Cyan" - # When UNSLOTH_INSTALL_REF is a branch/tag/sha (not "main"), install unsloth FROM that - # ref via install.sh's --package, so the WSL studio package carries THIS ref's - # studio/setup.sh + unsloth Python patches. Otherwise install.sh pulls released PyPI - # unsloth and the branch's setup.sh (e.g. the WSL CPU-build skip) would not run - # pre-merge. Default (ref = main) is byte-identical to before: plain `... | sh`. - # The git URL has no spaces, so it passes cleanly PowerShell -> wsl.exe -> bash -lc. + # When UNSLOTH_INSTALL_REF is a branch/tag/sha (not "main"), fetch THAT ref's + # install.sh (which honors UNSLOTH_INSTALL_REF) and export the ref, so install.sh + # installs unsloth from that ref -> the WSL studio venv carries this ref's + # studio/setup.sh + unsloth Python patches (e.g. the WSL CPU-build skip). Otherwise + # install.sh would pull released PyPI unsloth and the branch's setup.sh would never + # run pre-merge. Default (ref = main) is byte-identical to before: plain + # `curl https://unsloth.ai/install.sh | sh`. The ref is a bare git ref (no spaces), + # so it passes cleanly PowerShell -> wsl.exe -> bash -lc. $_instRef = Get-UnslothInstallRef - $_pkgArg = if ($_instRef -eq 'main') { '' } else { ' -s -- --package git+https://github.com/unslothai/unsloth@' + $_instRef } - $wslInstall = 'export DEBIAN_FRONTEND=noninteractive; apt-get update -y >/dev/null 2>&1; apt-get install -y build-essential cmake git curl pciutils >/dev/null 2>&1; curl -fsSL https://unsloth.ai/install.sh | sh' + $_pkgArg + if ($_instRef -eq 'main') { + $wslInstall = 'export DEBIAN_FRONTEND=noninteractive; apt-get update -y >/dev/null 2>&1; apt-get install -y build-essential cmake git curl pciutils >/dev/null 2>&1; curl -fsSL https://unsloth.ai/install.sh | sh' + } else { + $wslInstall = 'export DEBIAN_FRONTEND=noninteractive; export UNSLOTH_INSTALL_REF=' + $_instRef + '; apt-get update -y >/dev/null 2>&1; apt-get install -y build-essential cmake git curl pciutils >/dev/null 2>&1; curl -fsSL https://raw.githubusercontent.com/unslothai/unsloth/' + $_instRef + '/install.sh | sh' + } # install.sh writes diagnostics to stderr and may exit non-zero on the optional llama.cpp # prebuilt step (no aarch64 prebuilt exists) -- that must NOT abort us under -ErrorAction Stop, # since torch + unsloth + Studio still install. Lower EAP around the call (same idiom as above). diff --git a/install.sh b/install.sh index 15755c589f..ef1a468e43 100755 --- a/install.sh +++ b/install.sh @@ -2325,6 +2325,15 @@ elif [ -n "$TORCH_INDEX_URL" ]; then run_install_cmd "overlay unsloth-zoo (git main)" uv pip install --python "$_VENV_PY" \ --no-deps --reinstall-package unsloth-zoo \ "unsloth-zoo @ git+https://github.com/unslothai/unsloth-zoo" + elif [ -n "${UNSLOTH_INSTALL_REF:-}" ] && [ "${UNSLOTH_INSTALL_REF}" != "main" ] && [ "$PACKAGE_NAME" = "unsloth" ]; then + # Pre-merge branch testing: install unsloth from the requested git ref so its + # bundled studio/setup.sh + unsloth Python patches are exercised (released PyPI + # would not have them yet). Gated: only the default package ("unsloth") and only a + # non-"main" ref; install.ps1 sets UNSLOTH_INSTALL_REF when testing a branch. The + # ref is a bare git ref; the URL is a fixed literal -- no --package injection path. + substep "installing unsloth from git ref '$UNSLOTH_INSTALL_REF'..." + run_install_cmd "install unsloth (@$UNSLOTH_INSTALL_REF)" uv pip install --python "$_VENV_PY" \ + --upgrade-package unsloth "unsloth @ git+https://github.com/unslothai/unsloth@${UNSLOTH_INSTALL_REF}" else run_install_cmd "install unsloth" uv pip install --python "$_VENV_PY" \ --upgrade-package unsloth -- "$PACKAGE_NAME" From d5d0858b21f8e436268715179e6d9a0ae13a1906 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Wed, 3 Jun 2026 05:03:41 -0700 Subject: [PATCH 29/95] install.ps1: fix detached CUDA-build launch (runner script, not bash -lc string) The previous Start-Process passed `@(... 'bash','-lc',$buildCmd)` where $buildCmd contained spaces (`env VAR=N bash provision.sh > log`). Start-Process's ArgumentList array mis-quotes a space-containing element, so wsl ran just `env` -- which dumped the environment to the log and exited; no build, only a CPU server left behind. Fix: build a tiny runner script here, ship it as base64 (dodges every quoting layer), and Start-Process invokes `bash /root/.unsloth/run_llama_build.sh` with ONLY space-free args. The runner also restores PATH (/usr/lib/wsl/lib for nvidia-smi, /usr/bin for apt) so the non-login detached shell doesn't make provision early-exit "no nvidia-smi", then caps jobs and runs provision with logging. Verified on a cold distro: the detached build survives install.ps1's exit and provision runs correctly (toolkit install + CUDA build), log shows real provision output. Co-Authored-By: Claude Opus 4.8 --- install.ps1 | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/install.ps1 b/install.ps1 index 097602f34b..16253d3b5a 100644 --- a/install.ps1 +++ b/install.ps1 @@ -1708,20 +1708,28 @@ shell.Run cmd, 0, False # vars do not cross into WSL by default), so thermally/power-limited # laptops can cap the CUDA build's parallelism (`env` with no # assignment is a harmless passthrough when the var is unset). - # Step 1: fetch the provision script (quick; a transient WSL session is fine). - $_fetchCmd = 'mkdir -p /root/.unsloth; if curl -fsSL "' + $_llamaUrl + '" -o /root/.unsloth/provision_llama_cuda.sh && [ -s /root/.unsloth/provision_llama_cuda.sh ]; then chmod +x /root/.unsloth/provision_llama_cuda.sh; echo PROV_FETCHED; else echo PROV_NOSCRIPT; fi' + # Step 1: fetch the provision script + write a small runner (quick session). + # The runner is built here and shipped as base64 (dodges every quoting layer). + # It (a) restores a sane PATH so a NON-login shell still finds nvidia-smi + # (/usr/lib/wsl/lib) and apt (/usr/bin) -- otherwise provision would early-exit + # "no nvidia-smi"; (b) caps build jobs; (c) runs provision with logging. Using a + # runner FILE lets the detached launcher below pass ONLY space-free args, avoiding + # Start-Process arg-quoting (a space-containing `bash -lc ` gets mis-split and + # silently runs just `env`). + $_pathLine = 'export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/lib/wsl/lib:$PATH"' + "`n" + $_jobsLine = if ($env:UNSLOTH_LLAMA_BUILD_JOBS) { "export UNSLOTH_LLAMA_BUILD_JOBS=$($env:UNSLOTH_LLAMA_BUILD_JOBS)`n" } else { "" } + $_runner = "#!/usr/bin/env bash`n" + $_pathLine + $_jobsLine + "exec bash /root/.unsloth/provision_llama_cuda.sh > /root/.unsloth/llama_cuda_build.log 2>&1`n" + $_runnerB64 = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($_runner)) + $_fetchCmd = 'mkdir -p /root/.unsloth; if curl -fsSL "' + $_llamaUrl + '" -o /root/.unsloth/provision_llama_cuda.sh && [ -s /root/.unsloth/provision_llama_cuda.sh ]; then chmod +x /root/.unsloth/provision_llama_cuda.sh; echo ' + $_runnerB64 + ' | base64 -d > /root/.unsloth/run_llama_build.sh; chmod +x /root/.unsloth/run_llama_build.sh; echo PROV_FETCHED; else echo PROV_NOSCRIPT; fi' $_fetchOut = & wsl.exe -d $distro -u root -- bash -lc $_fetchCmd 2>$null if ("$_fetchOut" -match 'PROV_FETCHED') { # Step 2: run the build anchored to a DETACHED WINDOWS process. A WSL-side - # `nohup setsid ... &` does NOT survive here: WSL shuts the distro's VM down - # once the launching wsl.exe session exits, killing any backgrounded build - # (observed: no build log, only the CPU server left behind). A persistent - # Windows-side wsl.exe (Start-Process, no -Wait) holds the VM up for the whole - # build while install.ps1 returns immediately. Job count crosses via `env`. - # No double-quotes in the bash string -> clean through Start-Process arg array. - $_jobsPrefix = if ($env:UNSLOTH_LLAMA_BUILD_JOBS) { "env UNSLOTH_LLAMA_BUILD_JOBS=$($env:UNSLOTH_LLAMA_BUILD_JOBS) " } else { "" } - $_buildCmd = $_jobsPrefix + 'bash /root/.unsloth/provision_llama_cuda.sh > /root/.unsloth/llama_cuda_build.log 2>&1' - Start-Process -WindowStyle Hidden -FilePath 'wsl.exe' -ArgumentList @('-d', $distro, '-u', 'root', '--', 'bash', '-lc', $_buildCmd) | Out-Null + # `nohup setsid ... &` does NOT survive: WSL shuts the distro's VM down once + # the launching wsl.exe session exits, killing any backgrounded build + # (observed: no log, only a CPU server left behind). A persistent Windows-side + # wsl.exe (Start-Process, no -Wait) holds the VM up for the whole build while + # install.ps1 returns immediately. All ArgumentList tokens are space-free. + Start-Process -WindowStyle Hidden -FilePath 'wsl.exe' -ArgumentList @('-d', $distro, '-u', 'root', '--', 'bash', '/root/.unsloth/run_llama_build.sh') | Out-Null step "llama.cpp" "building CUDA llama.cpp for GGUF inference in the background (a few min); log: ~/.unsloth/llama_cuda_build.log" "Green" } else { substep "(GGUF inference needs a CUDA llama.cpp build; build later: wsl -d $distro -u root -- bash ~/.unsloth/provision_llama_cuda.sh)" "Yellow" From 5d2a89a0e5c08c424afe7277759e713e3e6e2687 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Wed, 3 Jun 2026 05:21:04 -0700 Subject: [PATCH 30/95] provision_llama_cuda: use all cores for CUDA build (memory-aware -j) The CUDA llama.cpp compile is the slow step of the WSL GPU setup. The job count now defaults to the full core count (nproc) instead of being capped, which is ~5x faster on a 20-core box (-j4 -> -j20). To stay safe on unified-memory machines, where nvcc jobs (~1.5 GB each) could OOM-kill a full-parallel build, jobs are capped at mem/1.5GB when that is lower than nproc. UNSLOTH_LLAMA_BUILD_JOBS=N still overrides for thermal throttling. Co-Authored-By: Claude Opus 4.8 --- studio/scripts/provision_llama_cuda.sh | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/studio/scripts/provision_llama_cuda.sh b/studio/scripts/provision_llama_cuda.sh index cf23f71efc..829557a415 100644 --- a/studio/scripts/provision_llama_cuda.sh +++ b/studio/scripts/provision_llama_cuda.sh @@ -144,10 +144,24 @@ fi # Build the full set unsloth-zoo's GGUF exporter expects too (llama-mtmd-cli, # llama-gguf-split), so a pre-provisioned build satisfies both Studio inference # AND save_pretrained_gguf without triggering a --clean-first rebuild later. -# Job count is overridable (UNSLOTH_LLAMA_BUILD_JOBS) -- lower it on thermally / -# power-constrained laptops (e.g. N1X) where a full -j(nproc) CUDA build can trip -# thermal/power shutdowns. cmake --build is incremental, so re-running resumes. -JOBS="${UNSLOTH_LLAMA_BUILD_JOBS:-$(nproc)}" +# Build parallelism: use ALL cores by default. The single-arch CUDA compile is the +# slow step, and -j(nproc) is dramatically faster than a conservative cap (e.g. -j4 +# is ~5x slower on a 20-core box). We only back off when RAM is tight: nvcc jobs are +# memory-hungry (~1.5 GB each), so on a unified-memory machine we cap jobs at +# mem/1.5GB to avoid an OOM-kill mid-build. Override explicitly with +# UNSLOTH_LLAMA_BUILD_JOBS=N (e.g. to throttle a thermally limited laptop). +# cmake --build is incremental, so a re-run simply resumes where it left off. +_ncpu="$(nproc 2>/dev/null || echo 4)" +if [ -n "${UNSLOTH_LLAMA_BUILD_JOBS:-}" ]; then + JOBS="$UNSLOTH_LLAMA_BUILD_JOBS" +else + _memkb="$(awk '/MemTotal/{print $2}' /proc/meminfo 2>/dev/null || echo 0)" + _memjobs=$(( _memkb / 1572864 )) # 1.5 GB per nvcc job + if [ "$_memjobs" -lt 1 ]; then _memjobs=1; fi + JOBS="$_ncpu" + if [ "$_memjobs" -lt "$JOBS" ]; then JOBS="$_memjobs"; fi +fi +log "building with -j${JOBS} (cores=${_ncpu})" cmake --build build -j"$JOBS" --target \ llama-server llama-cli llama-quantize llama-mtmd-cli llama-gguf-split >/dev/null 2>&1 \ || { log "cmake build failed"; exit 0; } From 962ab23317b9edc9823e28cf96276bf3832fb6fe Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Wed, 3 Jun 2026 05:22:04 -0700 Subject: [PATCH 31/95] install.ps1: make WSL Studio shortcuts robust against blank icons The WSL-fallback Desktop/Start-Menu shortcuts could render blank because the block only downloaded the .ico from GitHub (no fallback, no validation) and only fired a single global shell notify. Now it: - prefers the icon bundled in the local clone (instant, reliable) and only falls back to a GitHub download when no bundle is present; - validates the ICO header (00 00 01 00) before attaching, so a partial/ empty/404 download can never leave a non-icon attached; - sets IconLocation as ",0" (explicit index); - issues a per-.lnk SHCNE_UPDATEITEM (SHCNF_PATHW) notify in addition to the global SHCNE_ASSOCCHANGED, forcing Explorer to re-read each new shortcut icon immediately and clear any stale blank cache entry. Mirrors the icon handling already used by the native New-StudioShortcuts path, plus the ie4uinit refresh approach from PR #5940. Co-Authored-By: Claude Opus 4.8 --- install.ps1 | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/install.ps1 b/install.ps1 index 16253d3b5a..99570ddc20 100644 --- a/install.ps1 +++ b/install.ps1 @@ -1665,7 +1665,25 @@ shell.Run cmd, 0, False ) Set-Content -LiteralPath $launcher -Value $L -Encoding UTF8 $icon = Join-Path $appDir "unsloth.ico" - try { if (-not (Test-Path -LiteralPath $icon)) { Invoke-WebRequest "https://raw.githubusercontent.com/unslothai/unsloth/$(Get-UnslothInstallRef)/studio/frontend/public/unsloth.ico" -OutFile $icon -UseBasicParsing -TimeoutSec 15 *> $null } } catch {} + # Prefer the icon bundled in the local clone (instant + reliable); fall back to a + # best-effort GitHub download only when no bundle is present. Then validate the ICO + # header (00 00 01 00) before attaching it: a partial/empty/HTML-404 download must + # never leave the shortcut pointing at a non-icon (which renders blank). + $bundledIcon = $null + if ($PSScriptRoot -and $PSScriptRoot.Trim()) { $bundledIcon = Join-Path $PSScriptRoot "studio\frontend\public\unsloth.ico" } + if ($bundledIcon -and (Test-Path -LiteralPath $bundledIcon)) { + try { Copy-Item -LiteralPath $bundledIcon -Destination $icon -Force } catch {} + } elseif (-not (Test-Path -LiteralPath $icon)) { + try { Invoke-WebRequest "https://raw.githubusercontent.com/unslothai/unsloth/$(Get-UnslothInstallRef)/studio/frontend/public/unsloth.ico" -OutFile $icon -UseBasicParsing -TimeoutSec 15 *> $null } catch {} + } + $hasValidIcon = $false + if (Test-Path -LiteralPath $icon) { + try { + $ib = [System.IO.File]::ReadAllBytes($icon) + if ($ib.Length -ge 4 -and $ib[0] -eq 0 -and $ib[1] -eq 0 -and $ib[2] -eq 1 -and $ib[3] -eq 0) { $hasValidIcon = $true } + else { Remove-Item -LiteralPath $icon -Force -ErrorAction SilentlyContinue } + } catch { Remove-Item -LiteralPath $icon -Force -ErrorAction SilentlyContinue } + } $wsh = New-Object -ComObject WScript.Shell $lnks = @() $dd = [Environment]::GetFolderPath("Desktop"); if ($dd -and $dd.Trim()) { $lnks += (Join-Path $dd "Unsloth Studio.lnk") } @@ -1675,7 +1693,7 @@ shell.Run cmd, 0, False $sc.TargetPath = (Join-Path $env:SystemRoot "System32\WindowsPowerShell\v1.0\powershell.exe") $sc.Arguments = "-NoExit -NoProfile -ExecutionPolicy Bypass -File `"$launcher`"" $sc.WorkingDirectory = $appDir - if (Test-Path -LiteralPath $icon) { $sc.IconLocation = $icon } + if ($hasValidIcon) { $sc.IconLocation = "$icon,0" } $sc.Description = "Unsloth Studio (GPU via WSL)" $sc.Save() } @@ -1687,10 +1705,14 @@ shell.Run cmd, 0, False try { & "$env:SystemRoot\System32\ie4uinit.exe" -show 2>$null } catch {} try { if (-not ("UnslothShell.Notify" -as [type])) { - Add-Type -Namespace UnslothShell -Name Notify -MemberDefinition '[System.Runtime.InteropServices.DllImport("shell32.dll")] public static extern void SHChangeNotify(int eventId, uint flags, System.IntPtr item1, System.IntPtr item2);' + Add-Type -Namespace UnslothShell -Name Notify -MemberDefinition '[System.Runtime.InteropServices.DllImport("shell32.dll", CharSet = System.Runtime.InteropServices.CharSet.Unicode)] public static extern void SHChangeNotify(int eventId, uint flags, string item1, System.IntPtr item2);' } - # SHCNE_ASSOCCHANGED (0x08000000) with SHCNF_IDLIST (0) -> flush shell icon associations. - [UnslothShell.Notify]::SHChangeNotify(0x08000000, 0, [System.IntPtr]::Zero, [System.IntPtr]::Zero) + # Per-.lnk SHCNE_UPDATEITEM (0x00002000) + SHCNF_PATHW (0x0005): force Explorer to + # re-read each shortcut's icon NOW, clearing any stale "blank" entry cached for that + # exact path (the global notify alone often does not refresh an existing .lnk). + foreach ($lnk in $lnks) { try { [UnslothShell.Notify]::SHChangeNotify(0x00002000, 0x0005, $lnk, [System.IntPtr]::Zero) } catch {} } + # SHCNE_ASSOCCHANGED (0x08000000) -> flush global shell icon associations. + [UnslothShell.Notify]::SHChangeNotify(0x08000000, 0x0005, $null, [System.IntPtr]::Zero) } catch {} } catch { substep "(could not create shortcuts: $($_.Exception.Message))" "Yellow" From 27f045a4123bccd1f841b718497ea6ae645ed17b Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Wed, 3 Jun 2026 05:29:31 -0700 Subject: [PATCH 32/95] uninstall.ps1: exit 0 on success (do not leak WSL probe exit code) The WSL distro-probe loop tries a candidate list that intentionally includes distros that may not exist; the last failed `wsl -d -- true` probe left $LASTEXITCODE=255, so `& .\uninstall.ps1` returned non-zero even when every cleanup step succeeded. Reset $global:LASTEXITCODE=0 at the end (not `exit 0`, so the `irm ... | iex` usage does not kill the caller shell). Co-Authored-By: Claude Opus 4.8 --- scripts/uninstall.ps1 | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/scripts/uninstall.ps1 b/scripts/uninstall.ps1 index 5dc4052db9..51d13b2c3d 100644 --- a/scripts/uninstall.ps1 +++ b/scripts/uninstall.ps1 @@ -414,6 +414,15 @@ function Uninstall-UnslothStudio { Write-Host "set to also remove that install tree, e.g.:" Write-Host " `$env:UNSLOTH_STUDIO_HOME = 'C:\your\path'; irm https://raw.githubusercontent.com/unslothai/unsloth/main/scripts/uninstall.ps1 | iex" } + + # A successful uninstall must report success. The WSL distro-probe loop above + # leaves $LASTEXITCODE set by the last `wsl -d -- true` probe, and the + # candidate list intentionally includes distros that may not exist (their + # probes fail by design) -- so without this reset `& .\uninstall.ps1` would + # exit non-zero (255) even though every cleanup step succeeded. Set the var + # rather than calling `exit 0` so the `irm ... | iex` usage does not terminate + # the caller's shell. + $global:LASTEXITCODE = 0 } Uninstall-UnslothStudio @args From ac3829b3819e1e80415361de1a2422c6b2266885 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Wed, 3 Jun 2026 05:44:30 -0700 Subject: [PATCH 33/95] install.ps1: run WSL Studio install/setup with --cd /root (fix CRLF setup.sh) When install.ps1 is launched from inside a cloned unsloth repo, the WSL subprocess inherited the Windows cwd (/mnt/c/.../unsloth). Python then prepended that dir to sys.path and `import unsloth` resolved the LOCAL CLONE instead of the installed package, so `unsloth studio update`'s _find_setup_script() returned the clone's studio/setup.sh -- which has CRLF line endings on a Windows checkout. bash aborted on line 4 ($'\r': command not found / set: pipefail: invalid option name), the deps + frontend step never ran, and Studio was left unusable (missing packaging/structlog/fastapi). Fix: pass `--cd /root` to the WoA-branch wsl.exe invocations (install, self-heal repair, torch/server verifications, the desktop launcher, and the background CUDA build) so the WSL side never starts in /mnt/c and always imports the installed package -> resolves the LF setup.sh in site-packages. The native `unsloth` shim is intentionally left without --cd so relative model-path args keep resolving against the user's cwd (the console-script entry point does not cwd-shadow at runtime). Also: use SHCNF_IDLIST (0) for the global SHCNE_ASSOCCHANGED notify (items are unused for that event) instead of SHCNF_PATHW. Co-Authored-By: Claude Opus 4.8 --- install.ps1 | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/install.ps1 b/install.ps1 index 99570ddc20..7835773f85 100644 --- a/install.ps1 +++ b/install.ps1 @@ -1561,7 +1561,7 @@ shell.Run cmd, 0, False $prevEapWsl = $ErrorActionPreference $ErrorActionPreference = "Continue" try { - & wsl.exe -d $distro -u root -- bash -lc $wslInstall + & wsl.exe -d $distro --cd /root -u root -- bash -lc $wslInstall $wslRc = $LASTEXITCODE } finally { $ErrorActionPreference = $prevEapWsl @@ -1573,7 +1573,7 @@ shell.Run cmd, 0, False $prevEapChk = $ErrorActionPreference $ErrorActionPreference = "Continue" try { - & wsl.exe -d $distro -u root -- /root/.unsloth/studio/unsloth_studio/bin/python -c "import torch,sys; sys.exit(0 if torch.cuda.is_available() else 3)" *> $null + & wsl.exe -d $distro --cd /root -u root -- /root/.unsloth/studio/unsloth_studio/bin/python -c "import torch,sys; sys.exit(0 if torch.cuda.is_available() else 3)" *> $null $torchOk = ($LASTEXITCODE -eq 0) } catch {} finally { $ErrorActionPreference = $prevEapChk } # Self-heal Studio's web-server deps. install.sh installs them in a late step @@ -1589,7 +1589,7 @@ shell.Run cmd, 0, False $_serverOk = $false $prevEapS = $ErrorActionPreference; $ErrorActionPreference = "Continue" try { - & wsl.exe -d $distro -u root -- $_studioPy -c "import structlog, fastapi, uvicorn, starlette" *> $null + & wsl.exe -d $distro --cd /root -u root -- $_studioPy -c "import structlog, fastapi, uvicorn, starlette" *> $null $_serverOk = ($LASTEXITCODE -eq 0) } catch {} finally { $ErrorActionPreference = $prevEapS } if (-not $_serverOk) { @@ -1605,10 +1605,10 @@ shell.Run cmd, 0, False $_deps = 'typer fastapi uvicorn matplotlib pandas nest_asyncio pyjwt easydict addict structlog diceware ddgs cryptography httpx fastmcp' $_repair = 'PY=/root/.unsloth/studio/unsloth_studio/bin/python; UV="$(command -v uv 2>/dev/null || echo /root/.local/bin/uv)"; if [ -x "$UV" ] || command -v uv >/dev/null 2>&1; then "$UV" pip install --python "$PY" ' + $_deps + '; else "$PY" -m pip install ' + $_deps + '; fi' $prevEapR = $ErrorActionPreference; $ErrorActionPreference = "Continue" - try { & wsl.exe -d $distro -u root -- bash -lc $_repair } catch {} finally { $ErrorActionPreference = $prevEapR } + try { & wsl.exe -d $distro --cd /root -u root -- bash -lc $_repair } catch {} finally { $ErrorActionPreference = $prevEapR } $prevEapS2 = $ErrorActionPreference; $ErrorActionPreference = "Continue" try { - & wsl.exe -d $distro -u root -- $_studioPy -c "import structlog, fastapi, uvicorn, starlette" *> $null + & wsl.exe -d $distro --cd /root -u root -- $_studioPy -c "import structlog, fastapi, uvicorn, starlette" *> $null $_serverOk = ($LASTEXITCODE -eq 0) } catch {} finally { $ErrorActionPreference = $prevEapS2 } if ($_serverOk) { substep "Studio web-server deps installed." "Green" } @@ -1620,9 +1620,9 @@ shell.Run cmd, 0, False # Seeding pip into the venv makes `save_pretrained_gguf` work regardless. $prevEapP = $ErrorActionPreference; $ErrorActionPreference = "Continue" try { - & wsl.exe -d $distro -u root -- $_studioPy -m pip --version *> $null + & wsl.exe -d $distro --cd /root -u root -- $_studioPy -m pip --version *> $null if ($LASTEXITCODE -ne 0) { - & wsl.exe -d $distro -u root -- $_studioPy -m ensurepip --upgrade *> $null + & wsl.exe -d $distro --cd /root -u root -- $_studioPy -m ensurepip --upgrade *> $null } } catch {} finally { $ErrorActionPreference = $prevEapP } } @@ -1661,7 +1661,7 @@ shell.Run cmd, 0, False ('$distro = "' + $distro + '"'), 'Start-Job { for ($i=0; $i -lt 120; $i++) { try { if ((Invoke-WebRequest "http://localhost:8888/api/health" -UseBasicParsing -TimeoutSec 2).StatusCode -eq 200) { Start-Process "http://localhost:8888"; break } } catch {}; Start-Sleep 1 } } | Out-Null', 'Write-Host "Starting Unsloth Studio in WSL ($distro); browser opens at http://localhost:8888 when ready (Ctrl+C to stop)..."', - 'wsl.exe -d $distro -u root -- bash -lic "unsloth studio -p 8888"' + 'wsl.exe -d $distro --cd /root -u root -- bash -lic "unsloth studio -p 8888"' ) Set-Content -LiteralPath $launcher -Value $L -Encoding UTF8 $icon = Join-Path $appDir "unsloth.ico" @@ -1711,8 +1711,9 @@ shell.Run cmd, 0, False # re-read each shortcut's icon NOW, clearing any stale "blank" entry cached for that # exact path (the global notify alone often does not refresh an existing .lnk). foreach ($lnk in $lnks) { try { [UnslothShell.Notify]::SHChangeNotify(0x00002000, 0x0005, $lnk, [System.IntPtr]::Zero) } catch {} } - # SHCNE_ASSOCCHANGED (0x08000000) -> flush global shell icon associations. - [UnslothShell.Notify]::SHChangeNotify(0x08000000, 0x0005, $null, [System.IntPtr]::Zero) + # SHCNE_ASSOCCHANGED (0x08000000) with SHCNF_IDLIST (0) -> flush global icon + # associations. Both item args are unused for this event, so pass NULL/IDLIST. + [UnslothShell.Notify]::SHChangeNotify(0x08000000, 0, $null, [System.IntPtr]::Zero) } catch {} } catch { substep "(could not create shortcuts: $($_.Exception.Message))" "Yellow" @@ -1743,7 +1744,7 @@ shell.Run cmd, 0, False $_runner = "#!/usr/bin/env bash`n" + $_pathLine + $_jobsLine + "exec bash /root/.unsloth/provision_llama_cuda.sh > /root/.unsloth/llama_cuda_build.log 2>&1`n" $_runnerB64 = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($_runner)) $_fetchCmd = 'mkdir -p /root/.unsloth; if curl -fsSL "' + $_llamaUrl + '" -o /root/.unsloth/provision_llama_cuda.sh && [ -s /root/.unsloth/provision_llama_cuda.sh ]; then chmod +x /root/.unsloth/provision_llama_cuda.sh; echo ' + $_runnerB64 + ' | base64 -d > /root/.unsloth/run_llama_build.sh; chmod +x /root/.unsloth/run_llama_build.sh; echo PROV_FETCHED; else echo PROV_NOSCRIPT; fi' - $_fetchOut = & wsl.exe -d $distro -u root -- bash -lc $_fetchCmd 2>$null + $_fetchOut = & wsl.exe -d $distro --cd /root -u root -- bash -lc $_fetchCmd 2>$null if ("$_fetchOut" -match 'PROV_FETCHED') { # Step 2: run the build anchored to a DETACHED WINDOWS process. A WSL-side # `nohup setsid ... &` does NOT survive: WSL shuts the distro's VM down once @@ -1751,7 +1752,7 @@ shell.Run cmd, 0, False # (observed: no log, only a CPU server left behind). A persistent Windows-side # wsl.exe (Start-Process, no -Wait) holds the VM up for the whole build while # install.ps1 returns immediately. All ArgumentList tokens are space-free. - Start-Process -WindowStyle Hidden -FilePath 'wsl.exe' -ArgumentList @('-d', $distro, '-u', 'root', '--', 'bash', '/root/.unsloth/run_llama_build.sh') | Out-Null + Start-Process -WindowStyle Hidden -FilePath 'wsl.exe' -ArgumentList @('-d', $distro, '--cd', '/root', '-u', 'root', '--', 'bash', '/root/.unsloth/run_llama_build.sh') | Out-Null step "llama.cpp" "building CUDA llama.cpp for GGUF inference in the background (a few min); log: ~/.unsloth/llama_cuda_build.log" "Green" } else { substep "(GGUF inference needs a CUDA llama.cpp build; build later: wsl -d $distro -u root -- bash ~/.unsloth/provision_llama_cuda.sh)" "Yellow" From 6c3453f2c0fa56f0a9c68945bd9effd5d68d2166 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Wed, 3 Jun 2026 06:23:44 -0700 Subject: [PATCH 34/95] install.sh: git-ref test path installs unsloth-zoo explicitly unsloth_zoo is an optional extra (not a base dependency), and install.sh always runs the studio deps step with SKIP_STUDIO_BASE=1 (which skips the base.txt install that would otherwise add it). Every other install path names unsloth-zoo explicitly; the pre-merge UNSLOTH_INSTALL_REF git path did not, so a branch build left unsloth_zoo missing and `import unsloth` failed with "Please install unsloth_zoo". Name it explicitly here too. The default PyPI path is unaffected (released unsloth carries zoo as a base dep). Also trims the verbose comment on this block. Co-Authored-By: Claude Opus 4.8 --- install.sh | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/install.sh b/install.sh index ef1a468e43..b8c2e5b32f 100755 --- a/install.sh +++ b/install.sh @@ -2326,14 +2326,15 @@ elif [ -n "$TORCH_INDEX_URL" ]; then --no-deps --reinstall-package unsloth-zoo \ "unsloth-zoo @ git+https://github.com/unslothai/unsloth-zoo" elif [ -n "${UNSLOTH_INSTALL_REF:-}" ] && [ "${UNSLOTH_INSTALL_REF}" != "main" ] && [ "$PACKAGE_NAME" = "unsloth" ]; then - # Pre-merge branch testing: install unsloth from the requested git ref so its - # bundled studio/setup.sh + unsloth Python patches are exercised (released PyPI - # would not have them yet). Gated: only the default package ("unsloth") and only a - # non-"main" ref; install.ps1 sets UNSLOTH_INSTALL_REF when testing a branch. The - # ref is a bare git ref; the URL is a fixed literal -- no --package injection path. + # Pre-merge branch testing: install unsloth from a git ref so its bundled + # setup.sh + Python patches are exercised (not yet on PyPI). install.ps1 sets + # UNSLOTH_INSTALL_REF; gated to the "unsloth" package and a non-"main" ref. + # unsloth-zoo is an optional extra (not a base dep) and SKIP_STUDIO_BASE skips + # the studio base.txt step, so name it explicitly or it never gets installed. substep "installing unsloth from git ref '$UNSLOTH_INSTALL_REF'..." run_install_cmd "install unsloth (@$UNSLOTH_INSTALL_REF)" uv pip install --python "$_VENV_PY" \ - --upgrade-package unsloth "unsloth @ git+https://github.com/unslothai/unsloth@${UNSLOTH_INSTALL_REF}" + --upgrade-package unsloth --upgrade-package unsloth-zoo \ + "unsloth @ git+https://github.com/unslothai/unsloth@${UNSLOTH_INSTALL_REF}" unsloth-zoo else run_install_cmd "install unsloth" uv pip install --python "$_VENV_PY" \ --upgrade-package unsloth -- "$PACKAGE_NAME" From db0df15f52e3bb49c1f37b6beb2b964dcdb51bd7 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Wed, 3 Jun 2026 06:23:56 -0700 Subject: [PATCH 35/95] provision_llama_cuda: run the background CUDA build at idle priority Building at -j(nproc) saturates every core (load ~25 on a 20-core box), which starved a concurrently launched `unsloth studio` / training session during the build's few-minute window. Wrap the cmake build in `nice -n 19` (+ `ionice -c 3` when available): full speed when the box is idle, but instant yield to foreground work. Also trims this file's comments. Co-Authored-By: Claude Opus 4.8 --- studio/scripts/provision_llama_cuda.sh | 69 +++++++++++--------------- 1 file changed, 28 insertions(+), 41 deletions(-) diff --git a/studio/scripts/provision_llama_cuda.sh b/studio/scripts/provision_llama_cuda.sh index 829557a415..fea0583fcc 100644 --- a/studio/scripts/provision_llama_cuda.sh +++ b/studio/scripts/provision_llama_cuda.sh @@ -1,33 +1,25 @@ #!/usr/bin/env bash -# Provision a CUDA-enabled llama.cpp for Unsloth Studio GGUF *inference*. +# Build a CUDA llama.cpp for Unsloth Studio GGUF *inference* into +# ~/.unsloth/llama.cpp (resolver checks /build/bin/llama-server). +# Idempotent, best-effort: safe to re-run, always exits 0. # -# Builds into ~/.unsloth/llama.cpp (the dir Unsloth Studio's llama-server -# resolver checks: /build/bin/llama-server). Best-effort and idempotent: -# safe to re-run, never hard-fails the caller (always exits 0). -# -# Why this exists: torch ships its own bundled CUDA runtime, so training + -# GGUF *export* work without a system CUDA toolkit. But GGUF *inference* needs -# a CUDA-linked llama-server, and on NVIDIA ARM machines (NVIDIA DGX Spark / -# GB10, N1X "RTX" laptops) there is no published aarch64+CUDA prebuilt, so we -# build one. Handles the known gotchas on these platforms: +# Needed because no aarch64+CUDA llama.cpp prebuilt exists for NVIDIA ARM hosts +# (DGX Spark / GB10, N1X "RTX" laptops). Handles the platform gotchas: # * nvcc rejects gcc-15 -> force gcc-14 / g++-14 as the host compiler # * glibc >= 2.41 vs CUDA < 13.3 -> install CUDA 13.3 (rsqrt header clash) # * sm_121 (Blackwell) GPUs -> derive arch from the GPU's compute_cap # -# Opt out entirely with UNSLOTH_NO_LLAMA_CUDA=1 (handled by the caller). +# Opt out with UNSLOTH_NO_LLAMA_CUDA=1 (handled by the caller). set -uo pipefail LLAMA_DIR="${UNSLOTH_LLAMA_CPP_PATH:-$HOME/.unsloth/llama.cpp}" SERVER="$LLAMA_DIR/build/bin/llama-server" log() { printf ' - %s\n' "$*"; } -# A llama-server is CUDA-capable in either of two build layouts: -# * old monolithic build -> libggml-cuda is a direct load-time dependency (ldd shows it) -# * current split build -> CUDA ships as a dlopen-ed backend plugin, libggml-cuda.so*, -# sitting next to the binary; ldd will NOT list it -# Checking only ldd (the old behaviour) is a false negative on current llama.cpp and would -# force a pointless full rebuild every run. A CPU-only build has no libggml-cuda.so at all, -# so the presence of that backend beside the binary is the reliable CUDA signal. +# CUDA-capable in two layouts: old monolithic (libggml-cuda is a direct ldd dep) +# or current split build (CUDA is a dlopen-ed backend libggml-cuda.so* beside the +# binary, not shown by ldd). ldd alone false-negatives on current llama.cpp; a +# CPU-only build has no libggml-cuda.so, so its presence is the reliable signal. is_cuda_server() { [ -x "$1" ] || return 1 ldd "$1" 2>/dev/null | grep -qi 'libggml-cuda' && return 0 @@ -95,11 +87,9 @@ if [ -z "$NVCC" ]; then fi CUDA_HOME="$(dirname "$(dirname "$NVCC")")" -# Put the CUDA toolkit + standard Linux dirs FIRST so the build always uses the -# Linux cmake/gcc/git, never a Windows tool that leaked into PATH via WSL interop -# when the installer is launched from a Windows shell (those /mnt/c entries also -# contain spaces that can confuse the build). Original PATH kept after so things -# like nvidia-smi still resolve. +# CUDA toolkit + Linux dirs FIRST so the build uses Linux cmake/gcc/git, not a +# Windows tool leaked into PATH via WSL interop (/mnt/c, also has spaces). Keep +# the original PATH after so nvidia-smi etc. still resolve. export PATH="$CUDA_HOME/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$PATH" export CUDAToolkit_ROOT="$CUDA_HOME" @@ -129,28 +119,20 @@ _cmake_configure() { -DCMAKE_CUDA_HOST_COMPILER="$HCXX" \ -DLLAMA_CURL=ON >/dev/null 2>&1 } -# A pre-existing build/ may carry an INCOMPATIBLE CMake cache. The common case: the -# Studio installer (setup.sh / install_llama_prebuilt.py) stages its llama.cpp build in -# a versioned dir (llama.cpp.build.NNNN) then RELOCATES it here, leaving a cache whose -# baked-in absolute source/build paths no longer match (and GGML_CUDA=OFF). Re-running -# CUDA configure over that fails ("CMakeCache directory is different" / "source does not -# match"). So try to reuse build/ first (fast incremental resume on a re-run after a -# partial CUDA build), and only if configure fails, wipe build/ and configure clean once. +# A pre-existing build/ may carry an incompatible CMake cache (e.g. the installer +# relocates a versioned build dir here, leaving stale absolute paths + GGML_CUDA=OFF), +# making CUDA configure fail. Try to reuse build/ first (fast incremental resume); +# only wipe and configure clean if that fails. if ! _cmake_configure; then log "stale/incompatible CMake cache detected; wiping build dir for a clean CUDA configure" rm -rf build _cmake_configure || { log "cmake configure failed"; exit 0; } fi -# Build the full set unsloth-zoo's GGUF exporter expects too (llama-mtmd-cli, -# llama-gguf-split), so a pre-provisioned build satisfies both Studio inference -# AND save_pretrained_gguf without triggering a --clean-first rebuild later. -# Build parallelism: use ALL cores by default. The single-arch CUDA compile is the -# slow step, and -j(nproc) is dramatically faster than a conservative cap (e.g. -j4 -# is ~5x slower on a 20-core box). We only back off when RAM is tight: nvcc jobs are -# memory-hungry (~1.5 GB each), so on a unified-memory machine we cap jobs at -# mem/1.5GB to avoid an OOM-kill mid-build. Override explicitly with -# UNSLOTH_LLAMA_BUILD_JOBS=N (e.g. to throttle a thermally limited laptop). -# cmake --build is incremental, so a re-run simply resumes where it left off. +# Build the full target set unsloth-zoo's GGUF exporter also needs (llama-mtmd-cli, +# llama-gguf-split) so one build serves both Studio inference and save_pretrained_gguf. +# Parallelism: all cores by default (-j(nproc) is far faster than a conservative cap), +# but cap at mem/1.5GB when RAM is tight (nvcc uses ~1.5 GB/job) to avoid OOM-kill. +# Override with UNSLOTH_LLAMA_BUILD_JOBS=N. Incremental: a re-run resumes. _ncpu="$(nproc 2>/dev/null || echo 4)" if [ -n "${UNSLOTH_LLAMA_BUILD_JOBS:-}" ]; then JOBS="$UNSLOTH_LLAMA_BUILD_JOBS" @@ -162,7 +144,12 @@ else if [ "$_memjobs" -lt "$JOBS" ]; then JOBS="$_memjobs"; fi fi log "building with -j${JOBS} (cores=${_ncpu})" -cmake --build build -j"$JOBS" --target \ +# Lowest CPU + idle I/O priority so this background build keeps full speed when the +# box is idle but instantly yields to a foreground `unsloth studio` / training run. +_NICE="" +command -v nice >/dev/null 2>&1 && _NICE="nice -n 19" +command -v ionice >/dev/null 2>&1 && _NICE="$_NICE ionice -c 3" +$_NICE cmake --build build -j"$JOBS" --target \ llama-server llama-cli llama-quantize llama-mtmd-cli llama-gguf-split >/dev/null 2>&1 \ || { log "cmake build failed"; exit 0; } From 0946d5d37f71b02057e41a42c2f9ffcb7824cf7d Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Wed, 3 Jun 2026 06:24:07 -0700 Subject: [PATCH 36/95] Trim verbose PR comments to be succinct Shorten the multi-line rationale comments added by this PR across the remaining changed files to 1-2 lines each, preserving intent (gotchas, workarounds, why-notes). Comment-only changes; no code, strings, or behavior altered. Verified: PowerShell AST parser, bash -n, and python ast.parse all pass; diffs confirmed comment-only. Files: install.ps1, scripts/uninstall.ps1, scripts/uninstall.sh, studio/setup.sh, unsloth/models/_utils.py, unsloth/kernels/flex_attention.py Co-Authored-By: Claude Opus 4.8 --- install.ps1 | 149 +++++++++++------------------- scripts/uninstall.ps1 | 42 +++------ scripts/uninstall.sh | 6 +- studio/setup.sh | 101 +++++++------------- unsloth/kernels/flex_attention.py | 10 +- unsloth/models/_utils.py | 19 ++-- 6 files changed, 113 insertions(+), 214 deletions(-) diff --git a/install.ps1 b/install.ps1 index 7835773f85..1a884a7621 100644 --- a/install.ps1 +++ b/install.ps1 @@ -9,8 +9,7 @@ # (DataDir nests inside; user PATH not modified persistently). # Default ($USERPROFILE\.unsloth\studio) is preserved when no env var is set. # UNSLOTH_INSTALL_REF = branch/tag/sha to fetch repo-versioned install assets -# from (provision_llama_cuda.sh, the .ico); defaults to 'main'. Lets the -# WSL-fallback GPU path be tested on a branch before it merges. +# from (provision_llama_cuda.sh, the .ico); defaults to 'main'. function Install-UnslothStudio { $ErrorActionPreference = "Stop" @@ -45,10 +44,9 @@ function Install-UnslothStudio { } } - # Git ref (branch/tag/sha) used to fetch repo-versioned install assets from - # raw.githubusercontent.com: provision_llama_cuda.sh and the .ico. Defaults to - # 'main' so existing users are byte-for-byte unaffected; set UNSLOTH_INSTALL_REF - # to a branch to exercise the WSL-fallback path end-to-end before a PR merges. + # Git ref for fetching repo-versioned install assets (provision_llama_cuda.sh, + # the .ico) from raw.githubusercontent.com. Defaults to 'main' (unchanged for + # existing users); set UNSLOTH_INSTALL_REF to a branch to test pre-merge. function Get-UnslothInstallRef { if ($env:UNSLOTH_INSTALL_REF -and $env:UNSLOTH_INSTALL_REF.Trim()) { return $env:UNSLOTH_INSTALL_REF.Trim() } return 'main' @@ -1470,20 +1468,16 @@ shell.Run cmd, 0, False $TorchIndexUrl = Get-TorchIndexUrl # ===== Windows-on-ARM + NVIDIA GPU -> automatic WSL2 fallback (N1X "RTX Spark" / DGX Spark-class) ===== - # Native Windows-ARM64 has no CUDA PyTorch wheel and no Triton wheel for win_arm64, so the GPU - # stack can't run natively today. When an NVIDIA GPU is present on ARM64 AND native CUDA PyTorch is - # NOT installable for this platform, set up the supported path: enable/install WSL2, run the Linux - # installer there (full GPU), and create a Windows `unsloth` shim that forwards into WSL. - # STRICTLY gated -> normal x86_64 Windows (NVIDIA or AMD) and ARM64-without-NVIDIA are byte-for-byte - # unaffected and continue the native install below. FUTURE-PROOF: if NVIDIA ships a win_arm64 CUDA - # torch wheel, the probe below passes and the native install is kept automatically. + # win_arm64 has no CUDA PyTorch/Triton wheel, so the GPU stack can't run natively. On ARM64 with an + # NVIDIA GPU and no installable native CUDA torch, route GPU setup through WSL2: enable/install WSL2, + # run the Linux installer there (full GPU), and add a Windows `unsloth` shim that forwards into WSL. + # Strictly gated: x86_64 and ARM64-without-NVIDIA are unaffected. Future-proof: if a win_arm64 CUDA + # torch wheel ships, the probe below passes and native install is kept automatically. # Opt out with UNSLOTH_NO_WSL_FALLBACK=1; choose the distro with UNSLOTH_WSL_DISTRO. try { $_winArm64 = ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString() -ieq 'Arm64') } catch { $_winArm64 = $false } - # Robust against x64-EMULATED PowerShell on Windows-on-ARM: under emulation .NET's - # OSArchitecture and $env:PROCESSOR_ARCHITECTURE both report X64/AMD64, which would - # mis-skip the WSL fallback. Win32_Processor.Architecture (12 = ARM64) and the - # machine-level PROCESSOR_ARCHITECTURE read the true OS arch even under emulation. - # Additive: can only turn $_winArm64 ON for genuine ARM64 hosts; x86_64 is unaffected. + # Under x64-emulated PowerShell on ARM, .NET OSArchitecture and $env:PROCESSOR_ARCHITECTURE report + # X64/AMD64; Win32_Processor.Architecture (12=ARM64) and machine-level PROCESSOR_ARCHITECTURE read + # the true arch. Additive: only turns $_winArm64 ON for genuine ARM64 hosts. if (-not $_winArm64) { try { if ((@(Get-CimInstance Win32_Processor -ErrorAction Stop))[0].Architecture -eq 12) { $_winArm64 = $true } } catch {} } @@ -1495,11 +1489,9 @@ shell.Run cmd, 0, False } $_nativeCudaTorchOk = $false if ($_winArm64 -and $HasNvidiaSmi -and (-not $SkipTorch)) { - # Future-proof check: can a CUDA-capable torch wheel be resolved natively for this platform/index? - # MUST use the SAME spec as the real native install below ("torch>=2.4,<2.11.0"). A bare `torch` - # probe is too loose -- the cu130 index can carry an out-of-range version (e.g. torch<2.4 or a - # nightly >2.11) whose win_arm64 wheel makes the dry-run pass, giving a FALSE POSITIVE that skips - # the WSL fallback and then fails the real install at the pinned range. + # Can a native CUDA torch wheel be resolved for this platform/index? Must use the SAME spec + # as the real install ("torch>=2.4,<2.11.0"): a bare `torch` probe can match an out-of-range + # wheel on the index, a false positive that skips WSL then fails the real pinned install. $prevEapProbe = $ErrorActionPreference; $ErrorActionPreference = "Continue" try { & uv pip install --python $VenvPython --dry-run "torch>=2.4,<2.11.0" --index-url $TorchIndexUrl *> $null @@ -1541,23 +1533,17 @@ shell.Run cmd, 0, False try { & wsl.exe --install -d $distro --no-launch } catch {} } substep "installing Unsloth Studio inside WSL '$distro' with full GPU (this downloads PyTorch)..." "Cyan" - # When UNSLOTH_INSTALL_REF is a branch/tag/sha (not "main"), fetch THAT ref's - # install.sh (which honors UNSLOTH_INSTALL_REF) and export the ref, so install.sh - # installs unsloth from that ref -> the WSL studio venv carries this ref's - # studio/setup.sh + unsloth Python patches (e.g. the WSL CPU-build skip). Otherwise - # install.sh would pull released PyPI unsloth and the branch's setup.sh would never - # run pre-merge. Default (ref = main) is byte-identical to before: plain - # `curl https://unsloth.ai/install.sh | sh`. The ref is a bare git ref (no spaces), - # so it passes cleanly PowerShell -> wsl.exe -> bash -lc. + # For a non-main ref, fetch + export THAT ref so the WSL venv gets the branch's + # setup.sh + unsloth patches (otherwise install.sh pulls released PyPI unsloth and the + # branch never runs pre-merge). main is byte-identical to plain unsloth.ai/install.sh. $_instRef = Get-UnslothInstallRef if ($_instRef -eq 'main') { $wslInstall = 'export DEBIAN_FRONTEND=noninteractive; apt-get update -y >/dev/null 2>&1; apt-get install -y build-essential cmake git curl pciutils >/dev/null 2>&1; curl -fsSL https://unsloth.ai/install.sh | sh' } else { $wslInstall = 'export DEBIAN_FRONTEND=noninteractive; export UNSLOTH_INSTALL_REF=' + $_instRef + '; apt-get update -y >/dev/null 2>&1; apt-get install -y build-essential cmake git curl pciutils >/dev/null 2>&1; curl -fsSL https://raw.githubusercontent.com/unslothai/unsloth/' + $_instRef + '/install.sh | sh' } - # install.sh writes diagnostics to stderr and may exit non-zero on the optional llama.cpp - # prebuilt step (no aarch64 prebuilt exists) -- that must NOT abort us under -ErrorAction Stop, - # since torch + unsloth + Studio still install. Lower EAP around the call (same idiom as above). + # install.sh may exit non-zero on the optional llama.cpp prebuilt step (no aarch64 prebuilt) + # though torch + unsloth + Studio still install, so lower EAP so it doesn't abort under Stop. $prevEapWsl = $ErrorActionPreference $ErrorActionPreference = "Continue" try { @@ -1567,8 +1553,7 @@ shell.Run cmd, 0, False $ErrorActionPreference = $prevEapWsl } Write-Host "" - # The optional llama.cpp prebuilt step exits non-zero on aarch64 (no prebuilt) even when - # torch + unsloth + Studio installed fine -- so verify torch.cuda directly instead of trusting $wslRc. + # $wslRc can be non-zero from the llama.cpp prebuilt step even on success, so verify torch.cuda directly. $torchOk = $false $prevEapChk = $ErrorActionPreference $ErrorActionPreference = "Continue" @@ -1576,14 +1561,10 @@ shell.Run cmd, 0, False & wsl.exe -d $distro --cd /root -u root -- /root/.unsloth/studio/unsloth_studio/bin/python -c "import torch,sys; sys.exit(0 if torch.cuda.is_available() else 3)" *> $null $torchOk = ($LASTEXITCODE -eq 0) } catch {} finally { $ErrorActionPreference = $prevEapChk } - # Self-heal Studio's web-server deps. install.sh installs them in a late step - # (install_python_stack.py "studio deps", step 8). If that step is cut short -- - # an interrupted download, or a transient resolver hiccup -- torch + unsloth still - # land, but the server stack (fastapi/uvicorn/structlog/starlette) is missing and - # `unsloth studio` dies at launch with ModuleNotFoundError. If the stack can't - # import, install it WITHOUT disturbing the working ML stack: we deliberately do - # NOT pin huggingface-hub / transformers / datasets here, so the GPU torch path - # we just verified stays intact (those pins live in studio.txt for a fresh env). + # Self-heal Studio's web-server deps: if install.sh's late "studio deps" step was cut short, + # torch + unsloth land but fastapi/uvicorn/structlog/starlette are missing and `unsloth studio` + # dies with ModuleNotFoundError. Reinstall those without pinning huggingface-hub/transformers/ + # datasets, so the verified GPU torch stack stays intact. if ($torchOk) { $_studioPy = "/root/.unsloth/studio/unsloth_studio/bin/python" $_serverOk = $false @@ -1594,14 +1575,10 @@ shell.Run cmd, 0, False } catch {} finally { $ErrorActionPreference = $prevEapS } if (-not $_serverOk) { substep "Studio web-server deps incomplete (install.sh step cut short) -- installing them now..." "Cyan" - # Mirrors studio/backend/requirements/studio.txt MINUS the huggingface-hub - # pin (protected above). Prefer uv (matches install.sh); fall back to pip. - # Bare package names only -- NO version specifiers / embedded quotes. - # The whole repair string is passed PowerShell -> wsl.exe -> bash -lc, and - # PowerShell's native-arg quoting mangles embedded double-quotes, so a - # "structlog>=24.1.0" loses its quotes and bash parses `>=` as a redirection, - # failing the whole install. uv resolves the latest of each (which satisfies - # the studio.txt minimums anyway), so bare names are sufficient and safe. + # Mirrors studio.txt minus the huggingface-hub pin (protected above); uv preferred, + # pip fallback. Bare names only -- a version spec's quotes get mangled through + # PowerShell -> wsl.exe -> bash -lc and `>=` becomes a redirection. uv resolves the + # latest of each, which satisfies the studio.txt minimums anyway. $_deps = 'typer fastapi uvicorn matplotlib pandas nest_asyncio pyjwt easydict addict structlog diceware ddgs cryptography httpx fastmcp' $_repair = 'PY=/root/.unsloth/studio/unsloth_studio/bin/python; UV="$(command -v uv 2>/dev/null || echo /root/.local/bin/uv)"; if [ -x "$UV" ] || command -v uv >/dev/null 2>&1; then "$UV" pip install --python "$PY" ' + $_deps + '; else "$PY" -m pip install ' + $_deps + '; fi' $prevEapR = $ErrorActionPreference; $ErrorActionPreference = "Continue" @@ -1614,10 +1591,8 @@ shell.Run cmd, 0, False if ($_serverOk) { substep "Studio web-server deps installed." "Green" } else { substep "(could not auto-install Studio server deps; 'unsloth studio' may fail to start)" "Yellow" } } - # GGUF export robustness: the venv is uv-managed and ships no `pip`, but - # unsloth-zoo's exporter calls check_pip() and only finds `uv pip` when uv is - # on PATH (true for the login-shell launcher, not for every code path). - # Seeding pip into the venv makes `save_pretrained_gguf` work regardless. + # The uv-managed venv ships no `pip`, but unsloth-zoo's exporter calls check_pip() and only + # finds `uv pip` when uv is on PATH. Seed pip so `save_pretrained_gguf` works regardless. $prevEapP = $ErrorActionPreference; $ErrorActionPreference = "Continue" try { & wsl.exe -d $distro --cd /root -u root -- $_studioPy -m pip --version *> $null @@ -1628,9 +1603,8 @@ shell.Run cmd, 0, False } if ($torchOk) { step "done" "Unsloth Studio installed in WSL '$distro' -- GPU ready (torch.cuda available)." "Green" - # Native Windows `unsloth` shim: forward every `unsloth ...` into the WSL GPU env so the user - # never has to touch WSL. `unsloth studio` runs inside WSL and streams output + URL back here; - # WSL2 forwards 127.0.0.1, so http://localhost:8888 works in the Windows browser. + # Native Windows `unsloth` shim forwards every `unsloth ...` into the WSL GPU env so the user + # never touches WSL. WSL2 forwards 127.0.0.1, so http://localhost:8888 opens in the Windows browser. try { $shimDir = Join-Path $env:LOCALAPPDATA "Unsloth\bin" New-Item -ItemType Directory -Force -Path $shimDir *> $null @@ -1665,10 +1639,8 @@ shell.Run cmd, 0, False ) Set-Content -LiteralPath $launcher -Value $L -Encoding UTF8 $icon = Join-Path $appDir "unsloth.ico" - # Prefer the icon bundled in the local clone (instant + reliable); fall back to a - # best-effort GitHub download only when no bundle is present. Then validate the ICO - # header (00 00 01 00) before attaching it: a partial/empty/HTML-404 download must - # never leave the shortcut pointing at a non-icon (which renders blank). + # Prefer the bundled icon; fall back to a GitHub download. Validate the ICO header + # (00 00 01 00) before attaching, so a partial/HTML-404 download never makes a blank icon. $bundledIcon = $null if ($PSScriptRoot -and $PSScriptRoot.Trim()) { $bundledIcon = Join-Path $PSScriptRoot "studio\frontend\public\unsloth.ico" } if ($bundledIcon -and (Test-Path -LiteralPath $bundledIcon)) { @@ -1698,47 +1670,36 @@ shell.Run cmd, 0, False $sc.Save() } step "shortcuts" "created Desktop + Start Menu shortcuts (launch WSL Studio + open browser)" "Green" - # Make the brand-new .lnk icons render immediately instead of blank. Explorer caches - # per-.lnk icons, so a freshly-created shortcut often shows blank until the shell is told - # to re-read it. ie4uinit -show alone is unreliable; also broadcast SHChangeNotify so - # Explorer refreshes the icons without needing a restart or re-login. + # Force the new .lnk icons to render now instead of blank: Explorer caches per-.lnk + # icons. ie4uinit -show alone is unreliable, so also broadcast SHChangeNotify below. try { & "$env:SystemRoot\System32\ie4uinit.exe" -show 2>$null } catch {} try { if (-not ("UnslothShell.Notify" -as [type])) { Add-Type -Namespace UnslothShell -Name Notify -MemberDefinition '[System.Runtime.InteropServices.DllImport("shell32.dll", CharSet = System.Runtime.InteropServices.CharSet.Unicode)] public static extern void SHChangeNotify(int eventId, uint flags, string item1, System.IntPtr item2);' } - # Per-.lnk SHCNE_UPDATEITEM (0x00002000) + SHCNF_PATHW (0x0005): force Explorer to - # re-read each shortcut's icon NOW, clearing any stale "blank" entry cached for that - # exact path (the global notify alone often does not refresh an existing .lnk). + # Per-.lnk SHCNE_UPDATEITEM (0x00002000), SHCNF_PATHW (0x0005): force Explorer to + # re-read each shortcut's icon now (the global notify alone often misses existing .lnks). foreach ($lnk in $lnks) { try { [UnslothShell.Notify]::SHChangeNotify(0x00002000, 0x0005, $lnk, [System.IntPtr]::Zero) } catch {} } - # SHCNE_ASSOCCHANGED (0x08000000) with SHCNF_IDLIST (0) -> flush global icon - # associations. Both item args are unused for this event, so pass NULL/IDLIST. + # SHCNE_ASSOCCHANGED (0x08000000), SHCNF_IDLIST (0): flush global icon associations + # (item args unused for this event). [UnslothShell.Notify]::SHChangeNotify(0x08000000, 0, $null, [System.IntPtr]::Zero) } catch {} } catch { substep "(could not create shortcuts: $($_.Exception.Message))" "Yellow" } - # GGUF *inference* needs a CUDA-linked llama-server. There is no published - # aarch64+CUDA llama.cpp prebuilt (NVIDIA DGX Spark / N1X), so build one into - # ~/.unsloth/llama.cpp (Studio's resolver path) IN THE BACKGROUND: the user gets - # Studio + training immediately, and GGUF inference lights up a few minutes later - # with zero manual steps. Best-effort; opt out with UNSLOTH_NO_LLAMA_CUDA=1. + # GGUF *inference* needs a CUDA-linked llama-server and no aarch64+CUDA prebuilt exists, so + # build one into ~/.unsloth/llama.cpp in the BACKGROUND: Studio + training are usable now and + # GGUF inference lights up minutes later. Best-effort; opt out with UNSLOTH_NO_LLAMA_CUDA=1. if ($env:UNSLOTH_NO_LLAMA_CUDA -ne '1') { $prevEapL = $ErrorActionPreference; $ErrorActionPreference = "Continue" try { $_llamaUrl = "https://raw.githubusercontent.com/unslothai/unsloth/$(Get-UnslothInstallRef)/studio/scripts/provision_llama_cuda.sh" - # Propagate UNSLOTH_LLAMA_BUILD_JOBS into the WSL build (Windows env - # vars do not cross into WSL by default), so thermally/power-limited - # laptops can cap the CUDA build's parallelism (`env` with no - # assignment is a harmless passthrough when the var is unset). - # Step 1: fetch the provision script + write a small runner (quick session). - # The runner is built here and shipped as base64 (dodges every quoting layer). - # It (a) restores a sane PATH so a NON-login shell still finds nvidia-smi - # (/usr/lib/wsl/lib) and apt (/usr/bin) -- otherwise provision would early-exit - # "no nvidia-smi"; (b) caps build jobs; (c) runs provision with logging. Using a - # runner FILE lets the detached launcher below pass ONLY space-free args, avoiding - # Start-Process arg-quoting (a space-containing `bash -lc ` gets mis-split and - # silently runs just `env`). + # Step 1: fetch the provision script + write a small runner, shipped as base64 to + # dodge quoting layers. The runner (a) restores PATH so a non-login shell finds + # nvidia-smi (/usr/lib/wsl/lib) and apt -- else provision early-exits "no nvidia-smi"; + # (b) caps build jobs from UNSLOTH_LLAMA_BUILD_JOBS (Windows env vars don't cross into + # WSL); (c) runs provision with logging. A runner FILE lets the detached launcher below + # pass only space-free args, avoiding Start-Process mis-splitting `bash -lc `. $_pathLine = 'export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/lib/wsl/lib:$PATH"' + "`n" $_jobsLine = if ($env:UNSLOTH_LLAMA_BUILD_JOBS) { "export UNSLOTH_LLAMA_BUILD_JOBS=$($env:UNSLOTH_LLAMA_BUILD_JOBS)`n" } else { "" } $_runner = "#!/usr/bin/env bash`n" + $_pathLine + $_jobsLine + "exec bash /root/.unsloth/provision_llama_cuda.sh > /root/.unsloth/llama_cuda_build.log 2>&1`n" @@ -1746,12 +1707,10 @@ shell.Run cmd, 0, False $_fetchCmd = 'mkdir -p /root/.unsloth; if curl -fsSL "' + $_llamaUrl + '" -o /root/.unsloth/provision_llama_cuda.sh && [ -s /root/.unsloth/provision_llama_cuda.sh ]; then chmod +x /root/.unsloth/provision_llama_cuda.sh; echo ' + $_runnerB64 + ' | base64 -d > /root/.unsloth/run_llama_build.sh; chmod +x /root/.unsloth/run_llama_build.sh; echo PROV_FETCHED; else echo PROV_NOSCRIPT; fi' $_fetchOut = & wsl.exe -d $distro --cd /root -u root -- bash -lc $_fetchCmd 2>$null if ("$_fetchOut" -match 'PROV_FETCHED') { - # Step 2: run the build anchored to a DETACHED WINDOWS process. A WSL-side - # `nohup setsid ... &` does NOT survive: WSL shuts the distro's VM down once - # the launching wsl.exe session exits, killing any backgrounded build - # (observed: no log, only a CPU server left behind). A persistent Windows-side - # wsl.exe (Start-Process, no -Wait) holds the VM up for the whole build while - # install.ps1 returns immediately. All ArgumentList tokens are space-free. + # Step 2: anchor the build to a detached Windows process. A WSL-side `nohup &` + # doesn't survive -- WSL stops the VM when the launching session exits, killing + # the build. A persistent Windows-side wsl.exe (Start-Process, no -Wait) keeps the + # VM up for the whole build while install.ps1 returns. All tokens are space-free. Start-Process -WindowStyle Hidden -FilePath 'wsl.exe' -ArgumentList @('-d', $distro, '--cd', '/root', '-u', 'root', '--', 'bash', '/root/.unsloth/run_llama_build.sh') | Out-Null step "llama.cpp" "building CUDA llama.cpp for GGUF inference in the background (a few min); log: ~/.unsloth/llama_cuda_build.log" "Green" } else { diff --git a/scripts/uninstall.ps1 b/scripts/uninstall.ps1 index 51d13b2c3d..35636f712a 100644 --- a/scripts/uninstall.ps1 +++ b/scripts/uninstall.ps1 @@ -337,9 +337,8 @@ function Uninstall-UnslothStudio { } catch { } # ── Windows-on-Arm WSL-fallback artifacts ── - # The ARM64+NVIDIA fallback installs Studio INSIDE WSL and drops a native shim + launcher under - # %LOCALAPPDATA%\Unsloth (note: "Unsloth", not "Unsloth Studio") with a PATH entry, while the real - # install lives in the WSL distro(s). The native cleanup above misses all of that -- handle it here. + # The ARM64+NVIDIA fallback puts Studio inside WSL plus a native shim + launcher under + # %LOCALAPPDATA%\Unsloth (not "Unsloth Studio") with a PATH entry -- all missed by the cleanup above. _Step "Removing WSL-fallback artifacts (shim, launcher, PATH entry, WSL install)..." $unslothDir = if ($env:LOCALAPPDATA) { Join-Path $env:LOCALAPPDATA "Unsloth" } else { $null } if ($unslothDir) { @@ -366,26 +365,13 @@ function Uninstall-UnslothStudio { # Remove the Studio install inside each WSL distro (the real GPU install + any CUDA llama.cpp build). if (Get-Command wsl.exe -ErrorAction SilentlyContinue) { try { - # `wsl --list` emits UTF-16 that PowerShell frequently mis-parses (yielding an - # EMPTY list -> the cleanup silently skipped, leaving the WSL install behind). - # So probe a candidate set by exit code instead ('' = the default distro), - # which is encoding-proof. In the cleanup, rm runs FIRST: `pkill -f ""` - # matches this very bash -lc (its argv contains the pattern) and would SIGKILL - # the shell before a trailing rm -- so remove files first (guaranteed), then - # best-effort kill via fuser by port (does not self-match) + pkill. - # Also remove the `unsloth` launcher symlink install.sh drops at - # ~/.local/bin/unsloth -> /bin/unsloth. rm -rf of ~/.unsloth - # above deletes its target but leaves the symlink dangling, so the - # `unsloth` command still resolves on PATH after an uninstall. - # The pkill patterns use the [x]-regex self-exclusion trick: this very - # `bash -lc ` shell's own argv contains the literal pattern text, so a - # plain `pkill -f unsloth_studio` would match (and SIGKILL) the shell itself - # before the next command runs -- which is why rm goes first AND why the second - # pkill (llama-server, a dynamic port not covered by `fuser -k 8888`) never - # fired. Writing the pattern as '[u]nsloth_studio' means the shell's argv holds - # "[u]nsloth_studio" (no literal "unsloth_studio" substring) so it no longer - # self-matches, while real target processes (cmdline contains "unsloth_studio") - # still match. Same for '[l]lama-server'. + # `wsl --list` emits UTF-16 PowerShell mis-parses (empty list -> cleanup skipped), so probe a + # candidate set by exit code instead ('' = default distro), which is encoding-proof. + # rm runs FIRST (guaranteed) since the kills could SIGKILL this shell. Also rm the dangling + # ~/.local/bin/unsloth symlink (its target under ~/.unsloth is gone but the link still resolves + # on PATH). pkill patterns use the [x]-regex self-exclusion trick: '[u]nsloth_studio' keeps the + # shell's own argv from matching (no literal "unsloth_studio" substring) while real processes + # still match. Same for '[l]lama-server' (a dynamic port not covered by fuser -k 8888). $_clean = 'rm -rf /root/.unsloth /home/*/.unsloth /root/llama-cuda /root/provision_llama_cuda.sh /root/llama_cuda_build.log 2>/dev/null; rm -f /root/.local/bin/unsloth /home/*/.local/bin/unsloth 2>/dev/null; fuser -k 8888/tcp 2>/dev/null; pkill -9 -f ''[u]nsloth_studio'' 2>/dev/null; pkill -9 -f ''[l]lama-server'' 2>/dev/null; true' $_cands = @('', 'Ubuntu', 'Ubuntu-24.04', 'Ubuntu-22.04', 'Debian') if ($env:UNSLOTH_WSL_DISTRO) { $_cands = @($env:UNSLOTH_WSL_DISTRO) + $_cands } @@ -415,13 +401,9 @@ function Uninstall-UnslothStudio { Write-Host " `$env:UNSLOTH_STUDIO_HOME = 'C:\your\path'; irm https://raw.githubusercontent.com/unslothai/unsloth/main/scripts/uninstall.ps1 | iex" } - # A successful uninstall must report success. The WSL distro-probe loop above - # leaves $LASTEXITCODE set by the last `wsl -d -- true` probe, and the - # candidate list intentionally includes distros that may not exist (their - # probes fail by design) -- so without this reset `& .\uninstall.ps1` would - # exit non-zero (255) even though every cleanup step succeeded. Set the var - # rather than calling `exit 0` so the `irm ... | iex` usage does not terminate - # the caller's shell. + # The distro-probe loop leaves $LASTEXITCODE from its last probe, which fails by design for + # absent distros -- reset it so a successful uninstall exits 0. Set the var rather than `exit 0` + # so `irm ... | iex` doesn't terminate the caller's shell. $global:LASTEXITCODE = 0 } diff --git a/scripts/uninstall.sh b/scripts/uninstall.sh index d7cda3be96..1c50b814c1 100755 --- a/scripts/uninstall.sh +++ b/scripts/uninstall.sh @@ -209,10 +209,8 @@ _custom_studio_roots | while IFS= read -r _custom_root; do _remove_path "$_custom_root" done _remove_path "$HOME/.unsloth/studio" -# CUDA llama.cpp built by provision_llama_cuda.sh on native-Linux Spark/aarch64 -# (and the fetched provision script). On WSL ~/.unsloth/llama.cpp is a symlink to -# the real build, which install.ps1's uninstall removes; here rm -rf clears the -# native-Linux build dir / the symlink. +# CUDA llama.cpp from provision_llama_cuda.sh (+ the fetched script). Clears the +# native-Linux build dir, or on WSL the symlink to the build install.ps1 removes. _remove_path "$HOME/.unsloth/llama.cpp" _remove_path "$HOME/.unsloth/provision_llama_cuda.sh" _remove_path "$HOME/.local/share/unsloth" diff --git a/studio/setup.sh b/studio/setup.sh index 095428b39e..9d5053b6af 100755 --- a/studio/setup.sh +++ b/studio/setup.sh @@ -756,11 +756,10 @@ LLAMA_CPP_DIR="$UNSLOTH_HOME/llama.cpp" LLAMA_SERVER_BIN="$LLAMA_CPP_DIR/build/bin/llama-server" _NEED_LLAMA_SOURCE_BUILD=false _LLAMA_CPP_DEGRADED=false -# Distinct from _LLAMA_CPP_DEGRADED: on WSL2 aarch64+NVIDIA with no nvcc yet, the -# CPU source build is skipped because install.ps1 builds the real CUDA server in -# the BACKGROUND. There is temporarily no llama-server, but that is a SUCCESS -# (CUDA build in progress), NOT a degraded/failed install -- so it must not trip -# the arm64 CPU-prebuilt last-resort or the install-failure exit 1. +# Distinct from _LLAMA_CPP_DEGRADED: on WSL2 aarch64+NVIDIA with no nvcc, the CPU +# build is skipped because install.ps1 builds the real CUDA server in the background. +# A temporarily-absent server here is success, not failure, so it must not trip the +# arm64 CPU-prebuilt last-resort or the exit 1. _LLAMA_CPP_DEFERRED=false _LLAMA_FORCE_COMPILE="${UNSLOTH_LLAMA_FORCE_COMPILE:-0}" _REQUESTED_LLAMA_TAG="${UNSLOTH_LLAMA_TAG:-${_DEFAULT_LLAMA_TAG}}" @@ -934,26 +933,12 @@ if [ "$_NEED_LLAMA_SOURCE_BUILD" = true ] && \ fi # ── WSL2 aarch64 + NVIDIA, no nvcc yet: defer to the background CUDA build ── -# On Windows-on-ARM + NVIDIA (DGX Spark / N1X "RTX Spark"), install.ps1 routes -# through WSL2 and, after this install finishes, launches provision_llama_cuda.sh -# in the BACKGROUND (installs CUDA 13.3 + gcc-14, builds the real sm_121 CUDA -# llama-server into ~/.unsloth/llama.cpp, replacing whatever is here). On a fresh -# WSL distro there is no CUDA toolkit (nvcc) yet, so the section-9 source build -# below can only produce a CPU-only server ("building (CPU, CUDA driver found but -# nvcc missing)") -- which is SLOW and immediately thrown away by that background -# CUDA build. So skip the source build entirely on this exact path: the -# background CUDA provision is the sole builder, and the CPU build is pure waste. -# -# Strictly gated. ALL must hold: -# - running under WSL (grep microsoft /proc/version) -# - aarch64/arm64 ($_HOST_MACHINE) -# - an NVIDIA GPU is present (nvidia-smi lists a GPU) -# - nvcc is MISSING (no nvcc on PATH, none under /usr/local/cuda*) -# - the CUDA provision is NOT opted out (UNSLOTH_NO_LLAMA_CUDA != 1) -# - user did not force a compile / pin a PR (_LLAMA_FORCE_COMPILE != 1, no _LLAMA_PR) -# If nvcc IS already present we fall through to section 9 and build CUDA directly. -# If UNSLOTH_NO_LLAMA_CUDA=1 the background build never runs, so we KEEP the CPU -# source build as the user's only llama-server (do not defer). +# On Windows-on-ARM + NVIDIA, install.ps1 builds the real CUDA llama-server in the +# background after this install. Without nvcc yet the section-9 build can only make a +# slow CPU server that the background build throws away, so skip it on this exact path. +# Gated: WSL + aarch64/arm64 + NVIDIA GPU + nvcc missing + CUDA not opted out +# (UNSLOTH_NO_LLAMA_CUDA!=1) + no forced compile / PR pin. If nvcc is present we fall +# through to section 9; if opted out we keep the CPU build as the only server. if [ "$_NEED_LLAMA_SOURCE_BUILD" = true ] \ && [ "$_LLAMA_FORCE_COMPILE" != "1" ] \ && [ -z "$_LLAMA_PR" ] \ @@ -967,9 +952,8 @@ if [ "$_NEED_LLAMA_SOURCE_BUILD" = true ] \ step "llama.cpp" "GGUF engine: CUDA build running in background (WSL aarch64 + NVIDIA)" "$C_WARN" substep "skipping slow CPU build; the background CUDA llama.cpp will provide the server" substep "(opt out / keep CPU build with UNSLOTH_NO_LLAMA_CUDA=1)" - # Sole builder is install.ps1's background provision_llama_cuda.sh. Do NOT set - # _LLAMA_CPP_DEGRADED (that would trigger the arm64 CPU-prebuilt last resort - # and the install-failure exit 1); use the distinct DEFERRED state instead. + # Use DEFERRED, not DEGRADED: DEGRADED would trigger the CPU-prebuilt last + # resort + exit 1, but install.ps1's background build is the intended builder. _NEED_LLAMA_SOURCE_BUILD=false _LLAMA_CPP_DEFERRED=true fi @@ -1211,13 +1195,10 @@ else else CMAKE_ARGS="$CMAKE_ARGS -DGGML_CUDA=ON" - # glibc >= 2.41 added rsqrt()/rsqrtf() (gated by __GLIBC_USE(IEC_60559_FUNCS_EXT_C23), - # which g++ enables via _GNU_SOURCE). CUDA Toolkits < 13.3 declare these in - # without a matching exception specifier -> every .cu fails - # "exception specification is incompatible", and the GPU build silently drops to CPU. - # -allow-unsupported-compiler does NOT fix this (header clash, not the GNU-version - # #error); no host gcc avoids it. NVIDIA fixed it in CUDA 13.3 (_NV_RSQRT_SPECIFIER). - # Diagnostic only: never changes flags / never aborts -> cannot regress any platform. + # glibc >= 2.41 vs CUDA < 13.3: rsqrt/rsqrtf header clash makes every .cu + # fail "exception specification is incompatible" and the GPU build drops to + # CPU. No workaround but CUDA >= 13.3. Diagnostic only: never changes flags + # or aborts, so it cannot regress any platform. _GLIBC_VER="$(getconf GNU_LIBC_VERSION 2>/dev/null | awk '{print $2}')" || _GLIBC_VER="" if [ -n "$_GLIBC_VER" ]; then _GLIBC_MAJ="${_GLIBC_VER%%.*}"; _GLIBC_MIN="${_GLIBC_VER#*.}"; _GLIBC_MIN="${_GLIBC_MIN%%.*}" @@ -1442,26 +1423,19 @@ fi # end _SKIP_GGUF_BUILD check # ── aarch64 + NVIDIA (DGX Spark / GB10 / N1X "RTX Spark"): provision a CUDA # llama.cpp when the source build above could not (no CUDA toolkit found) ── -# There is no published aarch64+CUDA llama.cpp prebuilt, so these hosts always -# source-build for the GPU above. But that build only emits a CUDA llama-server -# when a CUDA toolkit (nvcc) is already present; on a fresh Spark that ships only -# the driver + nvidia-smi, the build silently falls back to CPU. The Windows path -# closes this exact gap from its WSL2 fallback by invoking provision_llama_cuda.sh -# (installs CUDA 13.3 + gcc-14, then builds a CUDA-linked server). Mirror that here -# so native-Linux Spark users get the same GGUF *inference* robustness, shared by -# every Linux install instead of bolted onto the Windows installer. +# No aarch64+CUDA prebuilt exists, and the source build above only emits a CUDA +# server when nvcc is already present (a fresh Spark ships only driver + nvidia-smi, +# so it falls back to CPU). The Windows/WSL path closes this gap via +# provision_llama_cuda.sh; mirror it here so native-Linux Spark gets the same. # -# Strictly gated + additive: only fires on Linux aarch64/arm64 WITH an NVIDIA GPU -# AND when we do NOT already have a CUDA-linked llama-server. x86_64 (CUDA prebuilt -# or its own source build), ROCm, macOS/Metal, CPU-only ARM, and any ARM host that -# already built a CUDA server are byte-for-byte unaffected. Opt out with -# UNSLOTH_NO_LLAMA_CUDA=1. Best-effort: never aborts setup (provision script always -# exits 0; failures leave the prior CPU/degraded state for the fallback below). -# CUDA-capable in either build layout: old monolithic (libggml-cuda is a direct -# ldd dependency) or current split build (CUDA is a dlopen-ed backend, libggml-cuda.so*, -# beside the binary -- ldd will NOT list it). Checking only ldd is a false negative on -# current llama.cpp and would force a needless rebuild; a CPU-only build has no -# libggml-cuda.so at all, so its presence beside the binary is the reliable signal. +# Gated + additive: only on Linux aarch64/arm64 + NVIDIA GPU with no CUDA server +# yet (opt out via UNSLOTH_NO_LLAMA_CUDA=1). x86_64, ROCm, Metal, CPU-only ARM, and +# ARM hosts that already built CUDA are unaffected. Best-effort: provision always +# exits 0; on failure the prior CPU/degraded state stands for the fallback below. +# CUDA-capable in two layouts: old monolithic (libggml-cuda is a direct ldd dep) or +# split build (dlopen-ed backend libggml-cuda.so* beside the binary, not in ldd). ldd +# alone false-negatives; a CPU-only build has no libggml-cuda.so, so its presence is +# the reliable signal. _have_cuda_llama_server() { [ -x "$LLAMA_SERVER_BIN" ] || return 1 ldd "$LLAMA_SERVER_BIN" 2>/dev/null | grep -qi 'libggml-cuda' && return 0 @@ -1475,15 +1449,11 @@ if [ "$_HOST_SYSTEM" = "Linux" ] \ && command -v nvidia-smi >/dev/null 2>&1 \ && nvidia-smi -L 2>/dev/null | awk '/^GPU[[:space:]]+[0-9]+:/{found=1} END{exit !found}' \ && ! _have_cuda_llama_server; then - # NOTE: WSL2 is intentionally excluded above (grep microsoft /proc/version) -- - # under WSL the Windows installer (install.ps1) provisions the CUDA llama.cpp - # in the BACKGROUND after setup completes, so doing it here too would (a) run a - # heavy build in the FOREGROUND during install and (b) duplicate that work. - # This block is for NATIVE Linux (DGX Spark / GB10) only. - # Resolve provision_llama_cuda.sh: prefer the copy shipped beside setup.sh - # (packaged via studio/scripts/*.sh), then the local-dev repo, else fetch - # the pinned raw copy from GitHub (mirrors install.ps1's WSL fetch) so the - # normal `curl | sh` install works even on an older wheel without the script. + # WSL2 is excluded above: there install.ps1 runs this in the background after + # setup, so doing it here would duplicate the work in the foreground. Native + # Linux (DGX Spark / GB10) only. + # Resolve provision_llama_cuda.sh: copy beside setup.sh, then local-dev repo, + # else fetch from GitHub so `curl | sh` works on an older wheel without it. _PROV_SH="" if [ -f "$SCRIPT_DIR/scripts/provision_llama_cuda.sh" ]; then _PROV_SH="$SCRIPT_DIR/scripts/provision_llama_cuda.sh" @@ -1499,9 +1469,8 @@ if [ "$_HOST_SYSTEM" = "Linux" ] \ if [ -n "$_PROV_SH" ]; then step "llama.cpp" "aarch64 + NVIDIA: provisioning CUDA toolkit + building CUDA llama.cpp for GGUF inference..." "$C_WARN" substep "(opt out with UNSLOTH_NO_LLAMA_CUDA=1; lower load with UNSLOTH_LLAMA_BUILD_JOBS=N)" - # provision_llama_cuda.sh installs the toolkit + gcc-14 and builds into - # $LLAMA_CPP_DIR. It always exits 0; honor UNSLOTH_LLAMA_CPP_PATH so a - # custom STUDIO_HOME build lands in the same dir setup.sh validates. + # Builds into $LLAMA_CPP_DIR (via UNSLOTH_LLAMA_CPP_PATH so a custom + # STUDIO_HOME lands where setup.sh validates); always exits 0. UNSLOTH_LLAMA_CPP_PATH="$LLAMA_CPP_DIR" bash "$_PROV_SH" || true if _have_cuda_llama_server; then step "llama.cpp" "CUDA llama-server ready (aarch64 + NVIDIA)" diff --git a/unsloth/kernels/flex_attention.py b/unsloth/kernels/flex_attention.py index bd8ec43348..d14a663e17 100644 --- a/unsloth/kernels/flex_attention.py +++ b/unsloth/kernels/flex_attention.py @@ -27,9 +27,8 @@ torch_compile_options = { def _flex_is_dgx_spark(): - # Mirror of unsloth.models._utils.is_dgx_spark(), inlined to avoid importing - # `unsloth.models` from this low-level `kernels` module (circular at import). - # DGX Spark / N1X = aarch64 + NVIDIA CUDA + a Spark device-name token. + # Inlined copy of _utils.is_dgx_spark() to avoid a circular import. + # Spark = aarch64 + NVIDIA CUDA + a Spark device-name token. _force = os.environ.get("UNSLOTH_FORCE_DGX_SPARK") if _force == "1": return True @@ -51,9 +50,8 @@ def _flex_is_dgx_spark(): return False -# DGX Spark / N1X has 48 SMs (< inductor's 68-SM is_big_gpu threshold), so -# max_autotune_gemm is already skipped; dropping max_autotune only saves the -# wasted compile-time search -- identical kernels, no accuracy/throughput change. +# Spark's 48 SMs are below inductor's 68-SM is_big_gpu threshold, so max_autotune +# is already skipped; disabling it just avoids a wasted compile-time search. if _flex_is_dgx_spark(): torch_compile_options["max_autotune"] = False diff --git a/unsloth/models/_utils.py b/unsloth/models/_utils.py index 8965cac7e0..aab6364022 100644 --- a/unsloth/models/_utils.py +++ b/unsloth/models/_utils.py @@ -940,12 +940,9 @@ from transformers.modeling_utils import logger as transformers_logger # ---- NVIDIA DGX Spark (GB10) / N1X "RTX Spark" (Blackwell unified-memory) support ---- -# These Blackwell unified-memory (UMA) machines report different device names: -# "NVIDIA GB10" on DGX Spark, "JMJWOA-Generic-GPU" on the pre-launch N1X laptop. -# One shared detector so every Spark-specific workaround uses the same definition. -# The aarch64 + CUDA gate makes this a strict no-op on x86_64 NVIDIA, AMD/ROCm, -# Intel/XPU, Mac/MLX, and discrete aarch64 GPUs (GH200/GB200) -- those report -# non-matching names and/or are not aarch64, so behaviour there is unchanged. +# Shared detector for Spark-class UMA machines, which report varying device names +# ("NVIDIA GB10" on DGX Spark, "JMJWOA-Generic-GPU" on the N1X laptop). The +# aarch64 + CUDA gate keeps every Spark workaround a strict no-op elsewhere. _DGX_SPARK_DEVICE_TOKENS = ("GB10", "JMJWOA", "N1X", "DGX SPARK", "GB110") @@ -1028,7 +1025,7 @@ def patch_dgx_spark_memory_config(): return conf = os.environ.get("PYTORCH_CUDA_ALLOC_CONF", "") if "expandable_segments" in conf: - return # user already configured it -- do not override + return # respect user's setting os.environ["PYTORCH_CUDA_ALLOC_CONF"] = ( conf + "," if conf else "" ) + "expandable_segments:True" @@ -1686,12 +1683,8 @@ torch_compile_options = { "trace.enabled": UNSLOTH_COMPILE_DEBUG, "triton.cudagraphs": False, } -# DGX Spark / N1X: this GPU has 48 SMs, below inductor's hardcoded 68-SM -# `is_big_gpu` threshold, so `max_autotune_gemm` is already skipped by inductor -# (the "Not enough SMs to use max_autotune_gemm mode" warning). Dropping -# max_autotune on Spark only avoids the wasted compile-time autotuning search -- -# the produced Triton/inductor kernels are identical, so steady-state throughput -# and accuracy are unchanged. Strict no-op off-Spark (gated by is_dgx_spark()). +# Spark's 48 SMs are below inductor's 68-SM is_big_gpu threshold, so max_autotune +# is already skipped; disabling it just avoids a wasted compile-time search. if is_dgx_spark(): torch_compile_options["max_autotune"] = False From c7fd2cf925b915e2a825c9c09826277b47d69b56 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Wed, 3 Jun 2026 21:06:43 -0700 Subject: [PATCH 37/95] provision_llama_cuda: default to ~half cores (thermal headroom) A full -j(nproc) CUDA build trips power/thermal shutdowns on thermally constrained NVIDIA-ARM laptops (observed on the N1X "RTX Spark": a full-core build, especially alongside other load, shuts the machine down). nice lowers CPU *scheduling* priority but not heat -- power/heat scale with the number of active compile jobs -- so default to ~half the cores instead: still ~2.5x faster than a tiny -j4, but leaves real headroom. Still mem-capped (~1.5 GB per nvcc job) and overridable via UNSLOTH_LLAMA_BUILD_JOBS (raise on a well-cooled box, lower if it still trips). Tiny boxes (<=4 cores) use all. Co-Authored-By: Claude Opus 4.8 --- studio/scripts/provision_llama_cuda.sh | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/studio/scripts/provision_llama_cuda.sh b/studio/scripts/provision_llama_cuda.sh index fea0583fcc..3b11f8749f 100644 --- a/studio/scripts/provision_llama_cuda.sh +++ b/studio/scripts/provision_llama_cuda.sh @@ -130,17 +130,21 @@ if ! _cmake_configure; then fi # Build the full target set unsloth-zoo's GGUF exporter also needs (llama-mtmd-cli, # llama-gguf-split) so one build serves both Studio inference and save_pretrained_gguf. -# Parallelism: all cores by default (-j(nproc) is far faster than a conservative cap), -# but cap at mem/1.5GB when RAM is tight (nvcc uses ~1.5 GB/job) to avoid OOM-kill. -# Override with UNSLOTH_LLAMA_BUILD_JOBS=N. Incremental: a re-run resumes. +# Parallelism default = ~half the cores: much faster than a tiny -j4, but leaves +# thermal/power headroom -- a full -j(nproc) CUDA build trips shutdowns on +# thermally constrained NVIDIA-ARM laptops (e.g. N1X "RTX Spark"). Also cap by RAM +# (~1.5 GB per nvcc job) to avoid OOM. Tune with UNSLOTH_LLAMA_BUILD_JOBS=N (raise +# on a well-cooled box, lower if it still trips). Incremental: a re-run resumes. _ncpu="$(nproc 2>/dev/null || echo 4)" if [ -n "${UNSLOTH_LLAMA_BUILD_JOBS:-}" ]; then JOBS="$UNSLOTH_LLAMA_BUILD_JOBS" else + _half=$(( (_ncpu + 1) / 2 )) # ~half the cores for thermal headroom + if [ "$_ncpu" -le 4 ]; then _half="$_ncpu"; fi # tiny boxes: use all cores _memkb="$(awk '/MemTotal/{print $2}' /proc/meminfo 2>/dev/null || echo 0)" - _memjobs=$(( _memkb / 1572864 )) # 1.5 GB per nvcc job + _memjobs=$(( _memkb / 1572864 )) # 1.5 GB per nvcc job if [ "$_memjobs" -lt 1 ]; then _memjobs=1; fi - JOBS="$_ncpu" + JOBS="$_half" if [ "$_memjobs" -lt "$JOBS" ]; then JOBS="$_memjobs"; fi fi log "building with -j${JOBS} (cores=${_ncpu})" From a186275bacab345e45870a572e69bb831636d53a Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Wed, 3 Jun 2026 21:12:05 -0700 Subject: [PATCH 38/95] provision_llama_cuda: retry-clean on build failure (interrupted-build recovery) An interrupted CUDA build (e.g. a thermal/power shutdown mid-compile -- which this machine class hits) can leave a partially-linked libggml-cuda.so. On the next run cmake does not relink it, so linking llama-server fails with undefined ggml_cuda_op_* references and the script gives up with no server. Mirror the existing configure retry-clean: if `cmake --build` fails, wipe build/, reconfigure, and rebuild clean once before giving up. Co-Authored-By: Claude Opus 4.8 --- studio/scripts/provision_llama_cuda.sh | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/studio/scripts/provision_llama_cuda.sh b/studio/scripts/provision_llama_cuda.sh index 3b11f8749f..cda2bb1dcb 100644 --- a/studio/scripts/provision_llama_cuda.sh +++ b/studio/scripts/provision_llama_cuda.sh @@ -153,9 +153,20 @@ log "building with -j${JOBS} (cores=${_ncpu})" _NICE="" command -v nice >/dev/null 2>&1 && _NICE="nice -n 19" command -v ionice >/dev/null 2>&1 && _NICE="$_NICE ionice -c 3" -$_NICE cmake --build build -j"$JOBS" --target \ - llama-server llama-cli llama-quantize llama-mtmd-cli llama-gguf-split >/dev/null 2>&1 \ - || { log "cmake build failed"; exit 0; } +_cmake_build() { + $_NICE cmake --build build -j"$JOBS" --target \ + llama-server llama-cli llama-quantize llama-mtmd-cli llama-gguf-split >/dev/null 2>&1 +} +if ! _cmake_build; then + # An interrupted build (e.g. a thermal/power shutdown mid-compile, which this + # machine class is prone to) can leave a partially-linked libggml-cuda.so that + # then fails to link llama-server on resume (undefined ggml_cuda_op_* refs). + # Wipe build/ and rebuild clean once before giving up. + log "build failed (likely interrupted/partial); wiping build dir and rebuilding clean" + rm -rf build + _cmake_configure || { log "cmake configure failed"; exit 0; } + _cmake_build || { log "cmake build failed"; exit 0; } +fi if is_cuda_server "$SERVER"; then log "CUDA llama-server ready: $SERVER" From 4d9174b9ea1289e131ace2f45d00546ba170c979 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Thu, 4 Jun 2026 00:12:53 -0700 Subject: [PATCH 39/95] install.sh: redirect Windows exes from /dev/null (fix curl|sh stdin drain) `curl https://unsloth.ai/install.sh | sh` runs install.sh from a pipe, so the script *is* the shell's stdin. A Windows process launched via WSL interop (powershell.exe / cmd.exe) inherits that stdin and drains the remaining piped script, truncating it -- dash then aborts parsing the tail with "Syntax error: Unterminated quoted string". This surfaced as a non-fatal "sh: : Unterminated quoted string" near the end of every non-tty install (e.g. the WoA install.ps1 -> curl|sh flow). Add ` --- install.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/install.sh b/install.sh index bd8aca56fc..8cc69a3132 100755 --- a/install.sh +++ b/install.sh @@ -732,9 +732,9 @@ _open_browser() { elif grep -qi microsoft /proc/version 2>/dev/null; then # WSL: xdg-open is unreliable; use Windows browser via PowerShell or cmd if command -v powershell.exe >/dev/null 2>&1; then - powershell.exe -NoProfile -Command "Start-Process '$_url'" >/dev/null 2>&1 & + powershell.exe -NoProfile -Command "Start-Process '$_url'" /dev/null 2>&1 & elif command -v cmd.exe >/dev/null 2>&1; then - cmd.exe /c start "" "$_url" >/dev/null 2>&1 & + cmd.exe /c start "" "$_url" /dev/null 2>&1 & elif command -v xdg-open >/dev/null 2>&1; then xdg-open "$_url" >/dev/null 2>&1 & else @@ -1232,7 +1232,7 @@ WSLPS1_EOF # Convert WSL path to Windows path for powershell.exe _css_ps1_win=$(wslpath -w "$_css_ps1_tmp" 2>/dev/null) if [ -n "$_css_ps1_win" ]; then - powershell.exe -NoProfile -ExecutionPolicy Bypass -File "$_css_ps1_win" >/dev/null 2>&1 && _css_created=1 + powershell.exe -NoProfile -ExecutionPolicy Bypass -File "$_css_ps1_win" /dev/null 2>&1 && _css_created=1 fi rm -f "$_css_ps1_tmp" fi From 11d632307c705ccef4f176901c02db890200dbc2 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Thu, 4 Jun 2026 13:28:53 -0700 Subject: [PATCH 40/95] provision: ignore junk/0 UNSLOTH_LLAMA_BUILD_JOBS (cmake -j0 = all cores) A non-numeric or 0 override silently fell through to `cmake -j0`, which builds with ALL cores -- the opposite of the thermal-headroom default and a shutdown risk on NVIDIA-ARM laptops. Validate it's a positive integer; ignore anything else and auto-compute. Co-Authored-By: Claude Opus 4.8 --- studio/scripts/provision_llama_cuda.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/studio/scripts/provision_llama_cuda.sh b/studio/scripts/provision_llama_cuda.sh index cda2bb1dcb..5be96aa89b 100644 --- a/studio/scripts/provision_llama_cuda.sh +++ b/studio/scripts/provision_llama_cuda.sh @@ -136,7 +136,8 @@ fi # (~1.5 GB per nvcc job) to avoid OOM. Tune with UNSLOTH_LLAMA_BUILD_JOBS=N (raise # on a well-cooled box, lower if it still trips). Incremental: a re-run resumes. _ncpu="$(nproc 2>/dev/null || echo 4)" -if [ -n "${UNSLOTH_LLAMA_BUILD_JOBS:-}" ]; then +# Honor a valid positive-int override; ignore junk/0 (cmake reads -j0 as "all cores"). +if [ -n "${UNSLOTH_LLAMA_BUILD_JOBS:-}" ] && [ "${UNSLOTH_LLAMA_BUILD_JOBS}" -ge 1 ] 2>/dev/null; then JOBS="$UNSLOTH_LLAMA_BUILD_JOBS" else _half=$(( (_ncpu + 1) / 2 )) # ~half the cores for thermal headroom From fa3e5d3bd18586d2c095c1cb80319385c0404ce3 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Thu, 4 Jun 2026 13:47:25 -0700 Subject: [PATCH 41/95] install.ps1: add ie4uinit -ClearIconCache before -show (blank-shortcut fix) A same-name "Unsloth Studio.lnk" recreated across reinstalls keeps Explorer's stale (blank) iconcache_*.db entry; -show rebuilds but does not purge, so add -ClearIconCache first (matches PR #5940). The per-.lnk SHChangeNotify remains the primary fix. WoA-path only -- no effect on other installs. Co-Authored-By: Claude Opus 4.8 --- install.ps1 | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/install.ps1 b/install.ps1 index a3c9a4a996..a39a68f6fa 100644 --- a/install.ps1 +++ b/install.ps1 @@ -1682,7 +1682,10 @@ shell.Run cmd, 0, False } step "shortcuts" "created Desktop + Start Menu shortcuts (launch WSL Studio + open browser)" "Green" # Force the new .lnk icons to render now instead of blank: Explorer caches per-.lnk - # icons. ie4uinit -show alone is unreliable, so also broadcast SHChangeNotify below. + # icons in iconcache_*.db, and a same-name .lnk recreated across reinstalls keeps the + # stale (blank) entry. -ClearIconCache purges that db, -show rebuilds; both are still + # unreliable alone, so the per-.lnk SHChangeNotify below is the real fix. + try { & "$env:SystemRoot\System32\ie4uinit.exe" -ClearIconCache 2>$null } catch {} try { & "$env:SystemRoot\System32\ie4uinit.exe" -show 2>$null } catch {} try { if (-not ("UnslothShell.Notify" -as [type])) { From 4ee9aa5ab543ddb974bde6651cd9d9650ea14eac Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Thu, 4 Jun 2026 14:13:43 -0700 Subject: [PATCH 42/95] install.ps1: prime shell image list for new shortcuts (blank-icon race fix) Diagnosis this cycle: the .ico is well-formed (6 BMP frames 16-256px) and the shell resolves the logo at every size (verified via SHGetFileInfo + SHGetImageList/ImageList_GetIcon on the system image list, all sizes incl. the 256px jumbo slot the desktop draws). The residual blank is a first-paint race: Explorer lazily extracts a .lnk's icon and a miss (icon not yet flushed, cache just cleared) gets cached blank. Force the extraction at install time via SHGetFileInfo(SHGFI_SYSICONINDEX) per .lnk, populating the per-session image list both Desktop and Start Menu draw from. WoA path only, try/catch-wrapped. Co-Authored-By: Claude Opus 4.8 --- install.ps1 | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/install.ps1 b/install.ps1 index a39a68f6fa..3fec2f2242 100644 --- a/install.ps1 +++ b/install.ps1 @@ -1698,6 +1698,17 @@ shell.Run cmd, 0, False # (item args unused for this event). [UnslothShell.Notify]::SHChangeNotify(0x08000000, 0, $null, [System.IntPtr]::Zero) } catch {} + # Prime the shell system image list NOW so Desktop + Start Menu extract the real + # icon immediately. A lazy first-paint extraction that misses (icon not yet flushed, + # cache just cleared) is what gets cached as a blank; SHGFI_SYSICONINDEX forces the + # extraction here, while the .ico is known-present. (WoA path only; both views draw + # from this one per-session list.) + try { + if (-not ("UnslothShell.IconPrime" -as [type])) { + Add-Type -TypeDefinition 'using System; using System.Runtime.InteropServices; namespace UnslothShell { public class IconPrime { [StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)] struct FI { public IntPtr h; public int i; public uint a; [MarshalAs(UnmanagedType.ByValTStr, SizeConst=260)] public string n; [MarshalAs(UnmanagedType.ByValTStr, SizeConst=80)] public string t; } [DllImport("shell32.dll", CharSet=CharSet.Unicode)] static extern IntPtr SHGetFileInfo(string p, uint a, ref FI i, uint cb, uint f); public static void Prime(string path){ FI fi=new FI(); SHGetFileInfo(path,0,ref fi,(uint)Marshal.SizeOf(fi),0x4000); } } }' + } + foreach ($lnk in $lnks) { try { [UnslothShell.IconPrime]::Prime($lnk) } catch {} } + } catch {} } catch { substep "(could not create shortcuts: $($_.Exception.Message))" "Yellow" } From e3f0581e388c89805aee1ad978e952f6a2b8991c Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Fri, 5 Jun 2026 03:18:22 -0700 Subject: [PATCH 43/95] install.ps1: put WoA shortcut icon outside %LOCALAPPDATA% (real blank-icon fix) Root cause (diagnosed live on an N1X WoA box, confirmed by on-screen checks): the Windows shell's sandboxed icon-extraction broker cannot read a standalone .ico stored under %LOCALAPPDATA% (it gets a redirected/virtualized view), so the Desktop + Start Menu shortcuts render BLANK -- regardless of icon format (BMP vs PNG frames), ACLs, icon cache, or shortcut-creation method, all of which were ruled out. The IDENTICAL .ico renders correctly from a path under the user profile. Fix: write unsloth.ico to %USERPROFILE%\.unsloth instead of %LOCALAPPDATA%\Unsloth (shim/launcher stay in %LOCALAPPDATA%). uninstall.ps1 removes the icon at the new location. Also drops the speculative SHGetFileInfo "image-list prime" block added while chasing the wrong (format/cache) theory. Co-Authored-By: Claude Opus 4.8 --- install.ps1 | 27 +++++++++++---------------- scripts/uninstall.ps1 | 3 +++ 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/install.ps1 b/install.ps1 index 3fec2f2242..7768343239 100644 --- a/install.ps1 +++ b/install.ps1 @@ -1649,7 +1649,14 @@ shell.Run cmd, 0, False 'wsl.exe -d $distro --cd /root -u root -- bash -lic "unsloth studio -p 8888"' ) Set-Content -LiteralPath $launcher -Value $L -Encoding UTF8 - $icon = Join-Path $appDir "unsloth.ico" + # Icon must live OUTSIDE %LOCALAPPDATA%: on Windows-on-ARM the shell's sandboxed + # icon-extraction broker can't read a standalone .ico under AppData\Local (it gets a + # redirected/virtualized view), so the shortcut renders BLANK -- while the identical + # file under the user profile renders fine (verified on N1X). Keep the shim/launcher + # in $appDir; only the icon needs the profile location. + $iconDir = Join-Path $env:USERPROFILE ".unsloth" + New-Item -ItemType Directory -Force -Path $iconDir *> $null + $icon = Join-Path $iconDir "unsloth.ico" # Prefer the bundled icon; fall back to a GitHub download. Validate the ICO header # (00 00 01 00) before attaching, so a partial/HTML-404 download never makes a blank icon. $bundledIcon = $null @@ -1681,10 +1688,9 @@ shell.Run cmd, 0, False $sc.Save() } step "shortcuts" "created Desktop + Start Menu shortcuts (launch WSL Studio + open browser)" "Green" - # Force the new .lnk icons to render now instead of blank: Explorer caches per-.lnk - # icons in iconcache_*.db, and a same-name .lnk recreated across reinstalls keeps the - # stale (blank) entry. -ClearIconCache purges that db, -show rebuilds; both are still - # unreliable alone, so the per-.lnk SHChangeNotify below is the real fix. + # Nudge Explorer to pick up the new/changed shortcuts now: clear+rebuild the icon + # cache, then per-.lnk SHCNE_UPDATEITEM + a global SHCNE_ASSOCCHANGED. (The real + # blank-icon cause on WoA was the AppData\Local icon path, fixed above.) try { & "$env:SystemRoot\System32\ie4uinit.exe" -ClearIconCache 2>$null } catch {} try { & "$env:SystemRoot\System32\ie4uinit.exe" -show 2>$null } catch {} try { @@ -1698,17 +1704,6 @@ shell.Run cmd, 0, False # (item args unused for this event). [UnslothShell.Notify]::SHChangeNotify(0x08000000, 0, $null, [System.IntPtr]::Zero) } catch {} - # Prime the shell system image list NOW so Desktop + Start Menu extract the real - # icon immediately. A lazy first-paint extraction that misses (icon not yet flushed, - # cache just cleared) is what gets cached as a blank; SHGFI_SYSICONINDEX forces the - # extraction here, while the .ico is known-present. (WoA path only; both views draw - # from this one per-session list.) - try { - if (-not ("UnslothShell.IconPrime" -as [type])) { - Add-Type -TypeDefinition 'using System; using System.Runtime.InteropServices; namespace UnslothShell { public class IconPrime { [StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)] struct FI { public IntPtr h; public int i; public uint a; [MarshalAs(UnmanagedType.ByValTStr, SizeConst=260)] public string n; [MarshalAs(UnmanagedType.ByValTStr, SizeConst=80)] public string t; } [DllImport("shell32.dll", CharSet=CharSet.Unicode)] static extern IntPtr SHGetFileInfo(string p, uint a, ref FI i, uint cb, uint f); public static void Prime(string path){ FI fi=new FI(); SHGetFileInfo(path,0,ref fi,(uint)Marshal.SizeOf(fi),0x4000); } } }' - } - foreach ($lnk in $lnks) { try { [UnslothShell.IconPrime]::Prime($lnk) } catch {} } - } catch {} } catch { substep "(could not create shortcuts: $($_.Exception.Message))" "Yellow" } diff --git a/scripts/uninstall.ps1 b/scripts/uninstall.ps1 index 3b842ecae4..5013e4a85b 100644 --- a/scripts/uninstall.ps1 +++ b/scripts/uninstall.ps1 @@ -365,6 +365,9 @@ function Uninstall-UnslothStudio { } catch { } _RemovePath $unslothDir } + # The WoA shortcut icon lives under the user profile (the shell icon broker can't read a .ico + # under AppData\Local), so remove it here too. + if ($env:USERPROFILE) { _RemovePath (Join-Path $env:USERPROFILE ".unsloth\unsloth.ico") } # Remove the Studio install inside each WSL distro (the real GPU install + any CUDA llama.cpp build). if (Get-Command wsl.exe -ErrorAction SilentlyContinue) { try { From d161ff52a527a0168569a5c6d5f313cbf7656d78 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2026 12:25:38 +0000 Subject: [PATCH 44/95] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- unsloth/kernels/flex_attention.py | 3 +-- unsloth/models/_utils.py | 4 +--- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/unsloth/kernels/flex_attention.py b/unsloth/kernels/flex_attention.py index 7a0ad25142..e1e0dc399b 100644 --- a/unsloth/kernels/flex_attention.py +++ b/unsloth/kernels/flex_attention.py @@ -42,8 +42,7 @@ def _flex_is_dgx_spark(): if not (hasattr(torch, "cuda") and torch.cuda.is_available()): return False names = " ".join( - str(torch.cuda.get_device_name(i)).upper() - for i in range(torch.cuda.device_count()) + str(torch.cuda.get_device_name(i)).upper() for i in range(torch.cuda.device_count()) ) return any(t in names for t in ("GB10", "JMJWOA", "N1X", "DGX SPARK", "GB110")) except Exception: diff --git a/unsloth/models/_utils.py b/unsloth/models/_utils.py index 72fe785010..f21863c6b2 100644 --- a/unsloth/models/_utils.py +++ b/unsloth/models/_utils.py @@ -929,8 +929,7 @@ def is_dgx_spark(): if not (hasattr(torch, "cuda") and torch.cuda.is_available()): return False names = " ".join( - str(torch.cuda.get_device_name(i)).upper() - for i in range(torch.cuda.device_count()) + str(torch.cuda.get_device_name(i)).upper() for i in range(torch.cuda.device_count()) ) return any(token in names for token in _DGX_SPARK_DEVICE_TOKENS) except Exception: @@ -1041,7 +1040,6 @@ def patch_dgx_spark_dataloader_defaults(): return try: from transformers import training_args as _ta - Base = _ta.TrainingArguments except Exception: return From ad77ae6cae1a9e357b394437c3156f4ca884d641 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Mon, 8 Jun 2026 07:06:10 -0700 Subject: [PATCH 45/95] =?UTF-8?q?fix(install):=20address=20PR=20review=20(?= =?UTF-8?q?Codex=20+=20Gemini)=20=E2=80=94=20exit=20codes,=20over-broad=20?= =?UTF-8?q?uninstall,=20Spark=20allocator,=20provision=20robustness?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit install.ps1 (WoA WSL fallback): - report failure (non-zero) + restore the rolled-aside venv when the WSL GPU install fails (torch.cuda absent) or when WSL needs enabling+reboot, instead of returning success — so -File/Tauri callers don't see a broken install as complete - on WSL success, Complete-StudioVenvRollback so the previous-venv backup isn't orphaned - refuse under --tauri with a clear "use the CLI installer" message (the desktop launcher resolves a Windows-venv backend, which a WSL-only install can't provide) - reset $LASTEXITCODE before each wsl.exe / python probe (a stale 0 could mark WSL ready / torch OK if the native command fails to launch) - torch-availability probe: --reinstall so an already-installed CPU torch in a migrated venv isn't accepted as "satisfied" (would wrongly skip the WSL path) - treat a null HKCU PATH as empty (fresh profile) so shim PATH update can't throw - keep apt stderr visible inside WSL (only stdout -> /dev/null) for diagnosability scripts/uninstall.ps1: - scope WSL cleanup to /root (the fallback's install location); stop deleting /home/*/.unsloth, which could erase an unrelated WSL user's own Unsloth/cache studio/setup.sh: - direct (non-install.ps1) WSL installs now provision CUDA llama.cpp themselves instead of being left with no GGUF server: install.ps1 exports UNSLOTH_WSL_LLAMA_DEFERRED=1, and the aarch64+NVIDIA provision block runs under WSL only when that marker is absent - mark a provisioner-built llama.cpp as Studio-owned in custom-STUDIO_HOME mode so the next setup's _assert_studio_owned_or_absent doesn't abort - glibc>=2.41 check: also match a future major>2 (e.g. 3.0) studio/scripts/provision_llama_cuda.sh: - install base tools (cmake/git/curl) in their own apt transaction before the best-effort gcc-14/g++-14 (unavailable on Ubuntu 22.04 / Debian 12, where bundling them aborted the whole transaction and left no build tools) - back up an existing (e.g. CPU-only) llama.cpp before the destructive clone and restore it on clone failure, so a failed clone doesn't leave the user with no server - honor a pinned llama.cpp ref via UNSLOTH_LLAMA_TAG instead of always tracking main unsloth/models/_utils.py: - set PYTORCH_CUDA_ALLOC_CONF (expandable_segments) via a CUDA-free Spark detector (nvidia-smi, not torch.cuda.get_device_name) so it takes effect before CUDA/the caching allocator initialize — previously it was a silent no-op on auto-detected Spark Co-Authored-By: Claude Opus 4.8 --- install.ps1 | 57 +++++++++++++++++++++++--- scripts/uninstall.ps1 | 11 ++--- studio/scripts/provision_llama_cuda.sh | 39 +++++++++++++++--- studio/setup.sh | 20 ++++++--- unsloth/models/_utils.py | 46 ++++++++++++++++++--- 5 files changed, 145 insertions(+), 28 deletions(-) diff --git a/install.ps1 b/install.ps1 index 7768343239..f4d4599d30 100644 --- a/install.ps1 +++ b/install.ps1 @@ -1504,8 +1504,12 @@ shell.Run cmd, 0, False # as the real install ("torch>=2.4,<2.11.0"): a bare `torch` probe can match an out-of-range # wheel on the index, a false positive that skips WSL then fails the real pinned install. $prevEapProbe = $ErrorActionPreference; $ErrorActionPreference = "Continue" + # --reinstall forces resolution from the index instead of accepting an already-installed + # (e.g. CPU-only) torch in a migrated venv as "satisfied" -- otherwise the probe could pass + # without proving a native win_arm64 CUDA wheel exists, wrongly skipping the WSL path. + $global:LASTEXITCODE = -1 try { - & uv pip install --python $VenvPython --dry-run "torch>=2.4,<2.11.0" --index-url $TorchIndexUrl *> $null + & uv pip install --python $VenvPython --dry-run --reinstall "torch>=2.4,<2.11.0" --index-url $TorchIndexUrl *> $null $_nativeCudaTorchOk = ($LASTEXITCODE -eq 0) } catch { $_nativeCudaTorchOk = $false } finally { $ErrorActionPreference = $prevEapProbe } if ($_nativeCudaTorchOk) { step "gpu" "native CUDA PyTorch now available for win_arm64 -- keeping native install" "Green" } @@ -1514,8 +1518,19 @@ shell.Run cmd, 0, False step "wsl" "Windows on ARM + NVIDIA, native CUDA unavailable -- routing GPU setup through WSL2" substep "no win_arm64 CUDA PyTorch/Triton yet; WSL2 delivers full GPU (DGX Spark / RTX Spark path)." "Yellow" + # The bundled desktop app passes --tauri and launches its backend from a Windows venv + # (resolve_backend_binary), not from WSL -- so a WSL-only install would report complete yet + # fail to start. Until the Tauri launcher can drive a WSL backend, send desktop-app users to + # the CLI installer rather than leaving them with a broken-looking app. + if ($TauriMode) { + return (Exit-InstallFailure "Windows-on-ARM + NVIDIA GPU needs the WSL2 GPU install, which the desktop app can't launch yet. Install from PowerShell instead: irm https://unsloth.ai/install.ps1 | iex" 1) + } + $wslReady = $false if (Get-Command wsl.exe -ErrorAction SilentlyContinue) { + # Reset first: if wsl.exe throws/fails to start, $LASTEXITCODE keeps its prior value + # (a stale 0 from an earlier command would wrongly mark WSL ready). + $global:LASTEXITCODE = -1 try { & wsl.exe --status *> $null; if ($LASTEXITCODE -eq 0) { $wslReady = $true } } catch {} } @@ -1532,12 +1547,18 @@ shell.Run cmd, 0, False substep "in an ADMINISTRATOR PowerShell run: wsl --install" "Cyan" substep "reboot, then re-run: irm https://unsloth.ai/install.ps1 | iex" "Cyan" } + # WSL2 must be enabled + the machine rebooted before anything can install. Restore any + # rolled-aside previous venv and signal not-complete so -File callers don't treat this + # deferred state as a successful install. + Restore-StudioVenvRollback + $global:LASTEXITCODE = 1 return } $distro = if ($env:UNSLOTH_WSL_DISTRO) { $env:UNSLOTH_WSL_DISTRO } else { "Ubuntu-24.04" } # Detect the distro by exit code (encoding-proof; wsl --list emits UTF-16 that PS mis-parses). $haveDistro = $false + $global:LASTEXITCODE = -1 try { & wsl.exe -d $distro -- true *> $null; if ($LASTEXITCODE -eq 0) { $haveDistro = $true } } catch {} if (-not $haveDistro) { substep "installing WSL distro '$distro' (first time only)..." "Cyan" @@ -1548,15 +1569,22 @@ shell.Run cmd, 0, False # setup.sh + unsloth patches (otherwise install.sh pulls released PyPI unsloth and the # branch never runs pre-merge). main is byte-identical to plain unsloth.ai/install.sh. $_instRef = Get-UnslothInstallRef + # UNSLOTH_WSL_LLAMA_DEFERRED=1 tells the inner setup.sh that install.ps1 will build the CUDA + # llama.cpp in the background after install -- so setup.sh skips its own foreground build. + # (A user who runs install.sh DIRECTLY inside WSL won't set it, so setup.sh provisions CUDA + # itself instead of leaving them with no GGUF server.) + # apt stderr is kept visible (only stdout -> /dev/null) so network/DNS/repo failures inside + # WSL are diagnosable rather than silently swallowed. if ($_instRef -eq 'main') { - $wslInstall = 'export DEBIAN_FRONTEND=noninteractive; apt-get update -y >/dev/null 2>&1; apt-get install -y build-essential cmake git curl pciutils >/dev/null 2>&1; curl -fsSL https://unsloth.ai/install.sh | sh' + $wslInstall = 'export DEBIAN_FRONTEND=noninteractive UNSLOTH_WSL_LLAMA_DEFERRED=1; apt-get update -y >/dev/null; apt-get install -y build-essential cmake git curl pciutils >/dev/null; curl -fsSL https://unsloth.ai/install.sh | sh' } else { - $wslInstall = 'export DEBIAN_FRONTEND=noninteractive; export UNSLOTH_INSTALL_REF=' + $_instRef + '; apt-get update -y >/dev/null 2>&1; apt-get install -y build-essential cmake git curl pciutils >/dev/null 2>&1; curl -fsSL https://raw.githubusercontent.com/unslothai/unsloth/' + $_instRef + '/install.sh | sh' + $wslInstall = 'export DEBIAN_FRONTEND=noninteractive UNSLOTH_WSL_LLAMA_DEFERRED=1; export UNSLOTH_INSTALL_REF=' + $_instRef + '; apt-get update -y >/dev/null; apt-get install -y build-essential cmake git curl pciutils >/dev/null; curl -fsSL https://raw.githubusercontent.com/unslothai/unsloth/' + $_instRef + '/install.sh | sh' } # install.sh may exit non-zero on the optional llama.cpp prebuilt step (no aarch64 prebuilt) # though torch + unsloth + Studio still install, so lower EAP so it doesn't abort under Stop. $prevEapWsl = $ErrorActionPreference $ErrorActionPreference = "Continue" + $global:LASTEXITCODE = -1 try { & wsl.exe -d $distro --cd /root -u root -- bash -lc $wslInstall $wslRc = $LASTEXITCODE @@ -1568,6 +1596,8 @@ shell.Run cmd, 0, False $torchOk = $false $prevEapChk = $ErrorActionPreference $ErrorActionPreference = "Continue" + # Reset first so a stale 0 from a prior command can't mark torch OK if this fails to launch. + $global:LASTEXITCODE = -1 try { & wsl.exe -d $distro --cd /root -u root -- /root/.unsloth/studio/unsloth_studio/bin/python -c "import torch,sys; sys.exit(0 if torch.cuda.is_available() else 3)" *> $null $torchOk = ($LASTEXITCODE -eq 0) @@ -1624,9 +1654,13 @@ shell.Run cmd, 0, False "wsl.exe -d $distro -u root -- /root/.unsloth/studio/unsloth_studio/bin/unsloth %*" ) Set-Content -LiteralPath (Join-Path $shimDir "unsloth.cmd") -Value $shimLines -Encoding ASCII + # A fresh Windows profile may have no HKCU 'Path' value at all -> $userPath is null + # and $userPath.TrimEnd() would throw, losing the shim. Treat null as empty. $userPath = [Environment]::GetEnvironmentVariable("Path", "User") + if (-not $userPath) { $userPath = "" } if (($userPath -split ';') -notcontains $shimDir) { - [Environment]::SetEnvironmentVariable("Path", ($userPath.TrimEnd(';') + ";" + $shimDir), "User") + $newUserPath = if ($userPath.Trim()) { $userPath.TrimEnd(';') + ";" + $shimDir } else { $shimDir } + [Environment]::SetEnvironmentVariable("Path", $newUserPath, "User") } $env:Path = $env:Path.TrimEnd(';') + ";" + $shimDir step "shim" "created native 'unsloth' command -> forwards to WSL '$distro'" "Green" @@ -1742,8 +1776,19 @@ shell.Run cmd, 0, False step "wsl" "WSL Studio install did not finish cleanly (torch.cuda not detected; inner exit $wslRc) -- see log above." "Yellow" substep "retry, or launch manually: wsl -d $distro -u root -- bash -lic 'unsloth studio -p 8888'" "Cyan" } - substep "GPU training + GGUF export run inside WSL. (GGUF *inference* additionally needs a CUDA llama.cpp build.)" "Yellow" - if ($torchOk) { $global:LASTEXITCODE = 0 } + if ($torchOk) { + # WSL GPU install succeeded. On this path the Windows venv is vestigial (everything + # runs in WSL), so drop the rolled-aside previous-venv backup instead of orphaning it. + Complete-StudioVenvRollback + substep "GPU training + GGUF export run inside WSL. (GGUF *inference* additionally needs a CUDA llama.cpp build.)" "Yellow" + $global:LASTEXITCODE = 0 + return + } + # WSL GPU install failed (torch.cuda unavailable). Restore any rolled-aside previous venv so + # a reinstall-over-existing isn't left worse off, and report non-zero so -File callers don't + # treat a broken install as success. + Restore-StudioVenvRollback + $global:LASTEXITCODE = 1 return } diff --git a/scripts/uninstall.ps1 b/scripts/uninstall.ps1 index 5013e4a85b..46f9a87a77 100644 --- a/scripts/uninstall.ps1 +++ b/scripts/uninstall.ps1 @@ -374,11 +374,12 @@ function Uninstall-UnslothStudio { # `wsl --list` emits UTF-16 PowerShell mis-parses (empty list -> cleanup skipped), so probe a # candidate set by exit code instead ('' = default distro), which is encoding-proof. # rm runs FIRST (guaranteed) since the kills could SIGKILL this shell. Also rm the dangling - # ~/.local/bin/unsloth symlink (its target under ~/.unsloth is gone but the link still resolves - # on PATH). pkill patterns use the [x]-regex self-exclusion trick: '[u]nsloth_studio' keeps the - # shell's own argv from matching (no literal "unsloth_studio" substring) while real processes - # still match. Same for '[l]lama-server' (a dynamic port not covered by fuser -k 8888). - $_clean = 'rm -rf /root/.unsloth /home/*/.unsloth /root/llama-cuda /root/provision_llama_cuda.sh /root/llama_cuda_build.log 2>/dev/null; rm -f /root/.local/bin/unsloth /home/*/.local/bin/unsloth 2>/dev/null; fuser -k 8888/tcp 2>/dev/null; pkill -9 -f ''[u]nsloth_studio'' 2>/dev/null; pkill -9 -f ''[l]lama-server'' 2>/dev/null; true' + # /root/.local/bin/unsloth symlink (its target under /root/.unsloth is gone but the link still + # resolves on PATH). Scope STRICTLY to /root: the WoA fallback installs there (wsl -u root), so + # touching /home/*/.unsloth would erase an unrelated WSL user's own Unsloth/cache that this + # installer never created. pkill patterns use the [x]-regex self-exclusion trick: '[u]nsloth_studio' + # keeps the shell's own argv from matching while real processes still match. Same for '[l]lama-server'. + $_clean = 'rm -rf /root/.unsloth /root/llama-cuda /root/provision_llama_cuda.sh /root/llama_cuda_build.log 2>/dev/null; rm -f /root/.local/bin/unsloth 2>/dev/null; fuser -k 8888/tcp 2>/dev/null; pkill -9 -f ''[u]nsloth_studio'' 2>/dev/null; pkill -9 -f ''[l]lama-server'' 2>/dev/null; true' $_cands = @('', 'Ubuntu', 'Ubuntu-24.04', 'Ubuntu-22.04', 'Debian') if ($env:UNSLOTH_WSL_DISTRO) { $_cands = @($env:UNSLOTH_WSL_DISTRO) + $_cands } $_done = @{} diff --git a/studio/scripts/provision_llama_cuda.sh b/studio/scripts/provision_llama_cuda.sh index 5be96aa89b..a8c15fbbd0 100644 --- a/studio/scripts/provision_llama_cuda.sh +++ b/studio/scripts/provision_llama_cuda.sh @@ -42,11 +42,16 @@ fi SUDO=""; [ "$(id -u)" -ne 0 ] && SUDO="sudo" HAVE_APT=0; command -v apt-get >/dev/null 2>&1 && HAVE_APT=1 -# 2. Base toolchain. gcc-14 is required because nvcc rejects gcc-15. +# 2. Base toolchain (must succeed) THEN gcc-14 (best-effort, separate transaction). +# gcc-14 is preferred because nvcc rejects gcc-15, but it isn't in the default apt +# sources on Ubuntu 22.04 / Debian 12 -- installing it in the SAME transaction as +# cmake/git/curl would make apt abort the whole transaction there, leaving the box +# without the basic build tools needed to clone/configure llama.cpp. if [ "$HAVE_APT" -eq 1 ]; then $SUDO apt-get update -y >/dev/null 2>&1 || true $SUDO apt-get install -y --no-install-recommends \ - build-essential cmake git curl ca-certificates gcc-14 g++-14 >/dev/null 2>&1 || true + build-essential cmake git curl ca-certificates >/dev/null 2>&1 || true + $SUDO apt-get install -y --no-install-recommends gcc-14 g++-14 >/dev/null 2>&1 || true fi # 3. Locate nvcc; install the CUDA toolkit if missing. @@ -102,12 +107,34 @@ export CC="$HCC" CXX="$HCXX" CUDAHOSTCXX="$HCXX" CC_CAP="$(nvidia-smi --query-gpu=compute_cap --format=csv,noheader 2>/dev/null | head -1 | tr -d ' .')" if [ -n "$CC_CAP" ]; then CUDA_ARCH="$CC_CAP"; else CUDA_ARCH="native"; fi -# 6. Clone + build into ~/.unsloth/llama.cpp. +# 6. Clone + build into ~/.unsloth/llama.cpp. Honor a pinned llama.cpp ref +# (UNSLOTH_LLAMA_TAG, the same var setup.sh uses) so a provisioner-built tree matches +# the user's request instead of always tracking ggml-org main. mkdir -p "$(dirname "$LLAMA_DIR")" +_LLAMA_REF="${UNSLOTH_LLAMA_TAG:-}" if [ ! -d "$LLAMA_DIR/.git" ]; then - rm -rf "$LLAMA_DIR" - git clone --depth 1 https://github.com/ggml-org/llama.cpp "$LLAMA_DIR" >/dev/null 2>&1 \ - || { log "git clone failed"; exit 0; } + # Preserve any existing (e.g. CPU-only) llama.cpp so a FAILED clone doesn't leave the user + # with NO server -- restore it on clone failure. A successful clone makes it obsolete (the + # fresh CUDA build replaces it), so the backup is dropped then. + _LLAMA_BAK="" + if [ -e "$LLAMA_DIR" ]; then + _LLAMA_BAK="${LLAMA_DIR}.prev.$$" + rm -rf "$_LLAMA_BAK" 2>/dev/null + mv "$LLAMA_DIR" "$_LLAMA_BAK" 2>/dev/null || { rm -rf "$LLAMA_DIR" 2>/dev/null; _LLAMA_BAK=""; } + fi + _clone_ok=0 + if [ -n "$_LLAMA_REF" ]; then + git clone --depth 1 --branch "$_LLAMA_REF" https://github.com/ggml-org/llama.cpp "$LLAMA_DIR" >/dev/null 2>&1 && _clone_ok=1 + fi + if [ "$_clone_ok" -ne 1 ]; then + git clone --depth 1 https://github.com/ggml-org/llama.cpp "$LLAMA_DIR" >/dev/null 2>&1 && _clone_ok=1 + fi + if [ "$_clone_ok" -ne 1 ]; then + log "git clone failed" + [ -n "$_LLAMA_BAK" ] && mv "$_LLAMA_BAK" "$LLAMA_DIR" 2>/dev/null # restore previous server + exit 0 + fi + [ -n "$_LLAMA_BAK" ] && rm -rf "$_LLAMA_BAK" 2>/dev/null fi cd "$LLAMA_DIR" || exit 0 diff --git a/studio/setup.sh b/studio/setup.sh index 9d5053b6af..6ad42f0ff6 100755 --- a/studio/setup.sh +++ b/studio/setup.sh @@ -1203,7 +1203,8 @@ else if [ -n "$_GLIBC_VER" ]; then _GLIBC_MAJ="${_GLIBC_VER%%.*}"; _GLIBC_MIN="${_GLIBC_VER#*.}"; _GLIBC_MIN="${_GLIBC_MIN%%.*}" _CU_MAJ="${_NVCC_VER%%.*}"; _CU_MIN="${_NVCC_VER#*.}"; _CU_MIN="${_CU_MIN%%.*}" - if [ "${_GLIBC_MAJ:-0}" -eq 2 ] 2>/dev/null && [ "${_GLIBC_MIN:-0}" -ge 41 ] 2>/dev/null \ + if { [ "${_GLIBC_MAJ:-0}" -gt 2 ] 2>/dev/null \ + || { [ "${_GLIBC_MAJ:-0}" -eq 2 ] 2>/dev/null && [ "${_GLIBC_MIN:-0}" -ge 41 ] 2>/dev/null; }; } \ && { [ "${_CU_MAJ:-0}" -lt 13 ] 2>/dev/null \ || { [ "${_CU_MAJ:-0}" -eq 13 ] 2>/dev/null && [ "${_CU_MIN:-0}" -lt 3 ] 2>/dev/null; }; }; then substep "CUDA toolkit ${_NVCC_VER} is incompatible with glibc ${_GLIBC_VER} (rsqrt/rsqrtf header clash)." "$C_ERR" @@ -1444,14 +1445,17 @@ _have_cuda_llama_server() { } if [ "$_HOST_SYSTEM" = "Linux" ] \ && { [ "$_HOST_MACHINE" = "aarch64" ] || [ "$_HOST_MACHINE" = "arm64" ]; } \ - && ! grep -qi microsoft /proc/version 2>/dev/null \ + && { ! grep -qi microsoft /proc/version 2>/dev/null || [ "${UNSLOTH_WSL_LLAMA_DEFERRED:-0}" != "1" ]; } \ && [ "${UNSLOTH_NO_LLAMA_CUDA:-0}" != "1" ] \ && command -v nvidia-smi >/dev/null 2>&1 \ && nvidia-smi -L 2>/dev/null | awk '/^GPU[[:space:]]+[0-9]+:/{found=1} END{exit !found}' \ && ! _have_cuda_llama_server; then - # WSL2 is excluded above: there install.ps1 runs this in the background after - # setup, so doing it here would duplicate the work in the foreground. Native - # Linux (DGX Spark / GB10) only. + # Native Linux (DGX Spark / GB10) runs this. Under WSL it runs ONLY for a DIRECT + # `install.sh` invocation: when install.ps1 drives the WSL install it exports + # UNSLOTH_WSL_LLAMA_DEFERRED=1 and builds the CUDA llama.cpp in the background after + # setup, so this foreground build is skipped to avoid duplicating it. A user who runs + # install.sh themselves inside WSL has no background builder, so we provision here + # rather than leave them with no GGUF server. # Resolve provision_llama_cuda.sh: copy beside setup.sh, then local-dev repo, # else fetch from GitHub so `curl | sh` works on an older wheel without it. _PROV_SH="" @@ -1475,6 +1479,12 @@ if [ "$_HOST_SYSTEM" = "Linux" ] \ if _have_cuda_llama_server; then step "llama.cpp" "CUDA llama-server ready (aarch64 + NVIDIA)" _LLAMA_CPP_DEGRADED=false + # The provisioner just created $LLAMA_CPP_DIR. In custom-STUDIO_HOME mode the next + # setup/update runs _assert_studio_owned_or_absent on it, so claim ownership now or + # that assert would abort on a directory this installer made. + if [ "$_STUDIO_HOME_IS_CUSTOM" = true ]; then + : > "$LLAMA_CPP_DIR/$_STUDIO_OWNED_MARKER" 2>/dev/null || true + fi elif [ -f "$LLAMA_SERVER_BIN" ]; then substep "CUDA build unavailable; keeping existing (CPU) llama-server" "$C_WARN" else diff --git a/unsloth/models/_utils.py b/unsloth/models/_utils.py index f21863c6b2..25cc4d32a3 100644 --- a/unsloth/models/_utils.py +++ b/unsloth/models/_utils.py @@ -936,6 +936,38 @@ def is_dgx_spark(): return False +@functools.lru_cache(maxsize = None) +def _is_dgx_spark_no_cuda_init(): + """Spark detection that never initializes a CUDA context. + + `is_dgx_spark()` calls `torch.cuda.get_device_name()`, which lazily initializes CUDA + (and the caching allocator). Settings consumed at allocator-init time -- + `PYTORCH_CUDA_ALLOC_CONF` (expandable_segments) -- must be decided BEFORE that, so this + variant reads the GPU name from `nvidia-smi` (a separate process) instead of torch. + Honors the same UNSLOTH_FORCE_DGX_SPARK override. Falls back to False on any error. + """ + _force = os.environ.get("UNSLOTH_FORCE_DGX_SPARK") + if _force == "1": + return True + if _force == "0": + return False + try: + import platform + + if platform.machine().lower() not in ("aarch64", "arm64"): + return False + import subprocess + + out = subprocess.run( + ["nvidia-smi", "--query-gpu=name", "--format=csv,noheader"], + capture_output = True, text = True, timeout = 5, + ) + names = (out.stdout or "").upper() + return any(token in names for token in _DGX_SPARK_DEVICE_TOKENS) + except Exception: + return False + + def patch_dgx_spark_caching_allocator_warmup(): """No-op `transformers.modeling_utils.caching_allocator_warmup` on Spark UMA. @@ -975,13 +1007,15 @@ def patch_dgx_spark_memory_config(): fragmentation OOMs; headroom for larger models / longer sequences). Pure memory management: it never changes any computed value, so accuracy is unaffected. - Strictly no-op off-Spark (gated by `is_dgx_spark()`). Respects an existing - PYTORCH_CUDA_ALLOC_CONF (only appends `expandable_segments` when absent, never - overrides a user's setting) and an explicit opt-out - (UNSLOTH_NO_EXPANDABLE_SEGMENTS=1). Must run before the first CUDA allocation; - `import unsloth` precedes model load, so it is set in time for normal use. + Strictly no-op off-Spark. Respects an existing PYTORCH_CUDA_ALLOC_CONF (only appends + `expandable_segments` when absent, never overrides a user's setting) and an explicit + opt-out (UNSLOTH_NO_EXPANDABLE_SEGMENTS=1). Must run before the first CUDA allocation, + so it gates on the CUDA-free `_is_dgx_spark_no_cuda_init()` -- the regular + `is_dgx_spark()` calls `torch.cuda.get_device_name()`, which would initialize CUDA (and + the allocator) before this env var could take effect. `import unsloth` precedes model + load, so it is set in time for normal use. """ - if not is_dgx_spark(): + if not _is_dgx_spark_no_cuda_init(): return if os.environ.get("UNSLOTH_NO_EXPANDABLE_SEGMENTS") == "1": return From c30d9a73cd04b741a5d9a687fae733e4cf9ff29a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2026 14:07:44 +0000 Subject: [PATCH 46/95] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- unsloth/models/_utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/unsloth/models/_utils.py b/unsloth/models/_utils.py index 25cc4d32a3..38202bded1 100644 --- a/unsloth/models/_utils.py +++ b/unsloth/models/_utils.py @@ -960,7 +960,9 @@ def _is_dgx_spark_no_cuda_init(): out = subprocess.run( ["nvidia-smi", "--query-gpu=name", "--format=csv,noheader"], - capture_output = True, text = True, timeout = 5, + capture_output = True, + text = True, + timeout = 5, ) names = (out.stdout or "").upper() return any(token in names for token in _DGX_SPARK_DEVICE_TOKENS) From 8e51d18a6cf346fb1e9ab029ea17f9be147fbff7 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Tue, 9 Jun 2026 20:07:32 -0700 Subject: [PATCH 47/95] fix(install): address Codex round-2 review (WSL1 distros, build-failure restore, libcurl, shim quoting, opt-out forwarding) - install.ps1: detect a PRE-EXISTING WSL1 distro up-front (kernel string + libcuda probe inside the distro; encoding-proof vs UTF-16 `wsl -l -v`) and convert it with `wsl --set-version 2`, failing early with instructions if conversion does not take -- instead of completing a full install that only fails at the final torch.cuda check (no GPU passthrough under WSL1). - install.ps1: quote the distro name in the generated unsloth.cmd shim and in the copy-pasteable hint commands so UNSLOTH_WSL_DISTRO values with spaces ("Ubuntu Preview") keep working. - install.ps1: forward UNSLOTH_NO_LLAMA_CUDA=1 into the WSL install env; the inner setup.sh otherwise defers its llama.cpp build to a background builder this script then never dispatches (the same opt-out skips it), leaving no llama-server and a misleading "building in background" footer. Also add libcurl4-openssl-dev to the WSL bootstrap apt line. - provision_llama_cuda.sh: install libcurl4-openssl-dev with the base tools -- _cmake_configure forces -DLLAMA_CURL=ON and on the deferred WSL path this script is the only build path (setup.sh's GGUF dep install was skipped), so configure failed on fresh hosts without the headers. - provision_llama_cuda.sh: keep the pre-existing llama.cpp backup until the fresh build is CONFIRMED (was: dropped right after a successful clone), and restore it on configure/build failure or when no server binary was produced -- a failed CUDA build no longer destroys a previously working (CPU) server. - setup.sh: when provisioning fails and NO llama-server is present, set _LLAMA_CPP_DEGRADED=true so the arm64 CPU-prebuilt last resort and the installer failure exit fire instead of reporting a working install. Round-2 comments verified already fixed in ad77ae6 (anchored to its parent d161ff5): the torch probe already passes --reinstall; the WSL uninstall is already scoped to /root only. Co-Authored-By: Claude Opus 4.8 --- install.ps1 | 43 ++++++++++++++++++++++---- studio/scripts/provision_llama_cuda.sh | 38 ++++++++++++++++------- studio/setup.sh | 6 +++- 3 files changed, 68 insertions(+), 19 deletions(-) diff --git a/install.ps1 b/install.ps1 index f4d4599d30..69d0db6c7c 100644 --- a/install.ps1 +++ b/install.ps1 @@ -1563,6 +1563,29 @@ shell.Run cmd, 0, False if (-not $haveDistro) { substep "installing WSL distro '$distro' (first time only)..." "Cyan" try { & wsl.exe --install -d $distro --no-launch } catch {} + } else { + # A PRE-EXISTING distro may be WSL1, which has no GPU passthrough: the existence + # probe passes but the full install would only fail at the final torch.cuda + # check. Detect WSL1 up-front from inside the distro (kernel string + libcuda -- + # encoding-proof, unlike parsing UTF-16 `wsl -l -v` output) and convert in place; + # `wsl --set-version` preserves the distro's files. Freshly installed distros + # are WSL2 (default version 2), so only the pre-existing case needs this. + $_wsl2Probe = 'grep -qiE ''microsoft-standard|WSL2'' /proc/version 2>/dev/null || test -e /usr/lib/wsl/lib/libcuda.so' + $_isWsl2 = $false + $global:LASTEXITCODE = -1 + try { & wsl.exe -d $distro -u root -- bash -c $_wsl2Probe *> $null; $_isWsl2 = ($LASTEXITCODE -eq 0) } catch {} + if (-not $_isWsl2) { + substep "distro '$distro' looks like WSL1 (no GPU passthrough) -- converting to WSL2 (one-time; can take a few minutes)..." "Yellow" + $global:LASTEXITCODE = -1 + try { & wsl.exe --set-version $distro 2 } catch {} + $global:LASTEXITCODE = -1 + try { & wsl.exe -d $distro -u root -- bash -c $_wsl2Probe *> $null; $_isWsl2 = ($LASTEXITCODE -eq 0) } catch {} + if (-not $_isWsl2) { + Restore-StudioVenvRollback + return (Exit-InstallFailure "WSL distro '$distro' is WSL1 and automatic conversion failed; NVIDIA GPU passthrough needs WSL2. Convert it, then re-run the installer: wsl --set-version `"$distro`" 2" 1) + } + substep "'$distro' converted to WSL2." "Green" + } } substep "installing Unsloth Studio inside WSL '$distro' with full GPU (this downloads PyTorch)..." "Cyan" # For a non-main ref, fetch + export THAT ref so the WSL venv gets the branch's @@ -1575,10 +1598,16 @@ shell.Run cmd, 0, False # itself instead of leaving them with no GGUF server.) # apt stderr is kept visible (only stdout -> /dev/null) so network/DNS/repo failures inside # WSL are diagnosable rather than silently swallowed. + # Forward the CUDA llama.cpp opt-out into WSL: without it the inner setup.sh would + # defer its build to a background builder this script then never starts (the same + # opt-out skips the dispatch below), leaving no llama-server and a misleading + # "building in background" footer. Forwarded, setup.sh keeps its own build instead. + $_fwdEnv = '' + if ($env:UNSLOTH_NO_LLAMA_CUDA -eq '1') { $_fwdEnv = 'export UNSLOTH_NO_LLAMA_CUDA=1; ' } if ($_instRef -eq 'main') { - $wslInstall = 'export DEBIAN_FRONTEND=noninteractive UNSLOTH_WSL_LLAMA_DEFERRED=1; apt-get update -y >/dev/null; apt-get install -y build-essential cmake git curl pciutils >/dev/null; curl -fsSL https://unsloth.ai/install.sh | sh' + $wslInstall = $_fwdEnv + 'export DEBIAN_FRONTEND=noninteractive UNSLOTH_WSL_LLAMA_DEFERRED=1; apt-get update -y >/dev/null; apt-get install -y build-essential cmake git curl pciutils libcurl4-openssl-dev >/dev/null; curl -fsSL https://unsloth.ai/install.sh | sh' } else { - $wslInstall = 'export DEBIAN_FRONTEND=noninteractive UNSLOTH_WSL_LLAMA_DEFERRED=1; export UNSLOTH_INSTALL_REF=' + $_instRef + '; apt-get update -y >/dev/null; apt-get install -y build-essential cmake git curl pciutils >/dev/null; curl -fsSL https://raw.githubusercontent.com/unslothai/unsloth/' + $_instRef + '/install.sh | sh' + $wslInstall = $_fwdEnv + 'export DEBIAN_FRONTEND=noninteractive UNSLOTH_WSL_LLAMA_DEFERRED=1; export UNSLOTH_INSTALL_REF=' + $_instRef + '; apt-get update -y >/dev/null; apt-get install -y build-essential cmake git curl pciutils libcurl4-openssl-dev >/dev/null; curl -fsSL https://raw.githubusercontent.com/unslothai/unsloth/' + $_instRef + '/install.sh | sh' } # install.sh may exit non-zero on the optional llama.cpp prebuilt step (no aarch64 prebuilt) # though torch + unsloth + Studio still install, so lower EAP so it doesn't abort under Stop. @@ -1651,7 +1680,9 @@ shell.Run cmd, 0, False New-Item -ItemType Directory -Force -Path $shimDir *> $null $shimLines = @( '@echo off', - "wsl.exe -d $distro -u root -- /root/.unsloth/studio/unsloth_studio/bin/unsloth %*" + # Quote the distro: an UNSLOTH_WSL_DISTRO with spaces (e.g. "Ubuntu Preview") + # would otherwise split after -d and break every `unsloth ...` invocation. + "wsl.exe -d `"$distro`" -u root -- /root/.unsloth/studio/unsloth_studio/bin/unsloth %*" ) Set-Content -LiteralPath (Join-Path $shimDir "unsloth.cmd") -Value $shimLines -Encoding ASCII # A fresh Windows profile may have no HKCU 'Path' value at all -> $userPath is null @@ -1668,7 +1699,7 @@ shell.Run cmd, 0, False substep " unsloth studio # runs in WSL; opens http://localhost:8888" "Cyan" substep " unsloth studio run # also forwarded into WSL" "Cyan" } catch { - substep "(shim creation failed; launch manually): wsl -d $distro -u root -- bash -lic 'unsloth studio -p 8888'" "Yellow" + substep "(shim creation failed; launch manually): wsl -d `"$distro`" -u root -- bash -lic 'unsloth studio -p 8888'" "Yellow" } # Desktop + Start Menu shortcuts: launch the WSL Studio and open the browser when ready. try { @@ -1768,13 +1799,13 @@ shell.Run cmd, 0, False Start-Process -WindowStyle Hidden -FilePath 'wsl.exe' -ArgumentList @('-d', $distro, '--cd', '/root', '-u', 'root', '--', 'bash', '/root/.unsloth/run_llama_build.sh') | Out-Null step "llama.cpp" "building CUDA llama.cpp for GGUF inference in the background (a few min); log: ~/.unsloth/llama_cuda_build.log" "Green" } else { - substep "(GGUF inference needs a CUDA llama.cpp build; build later: wsl -d $distro -u root -- bash ~/.unsloth/provision_llama_cuda.sh)" "Yellow" + substep "(GGUF inference needs a CUDA llama.cpp build; build later: wsl -d `"$distro`" -u root -- bash ~/.unsloth/provision_llama_cuda.sh)" "Yellow" } } catch {} finally { $ErrorActionPreference = $prevEapL } } } else { step "wsl" "WSL Studio install did not finish cleanly (torch.cuda not detected; inner exit $wslRc) -- see log above." "Yellow" - substep "retry, or launch manually: wsl -d $distro -u root -- bash -lic 'unsloth studio -p 8888'" "Cyan" + substep "retry, or launch manually: wsl -d `"$distro`" -u root -- bash -lic 'unsloth studio -p 8888'" "Cyan" } if ($torchOk) { # WSL GPU install succeeded. On this path the Windows venv is vestigial (everything diff --git a/studio/scripts/provision_llama_cuda.sh b/studio/scripts/provision_llama_cuda.sh index a8c15fbbd0..a15d69e1d2 100644 --- a/studio/scripts/provision_llama_cuda.sh +++ b/studio/scripts/provision_llama_cuda.sh @@ -49,8 +49,11 @@ HAVE_APT=0; command -v apt-get >/dev/null 2>&1 && HAVE_APT=1 # without the basic build tools needed to clone/configure llama.cpp. if [ "$HAVE_APT" -eq 1 ]; then $SUDO apt-get update -y >/dev/null 2>&1 || true + # libcurl4-openssl-dev: _cmake_configure forces -DLLAMA_CURL=ON, and on the WSL + # deferred path this script is the only build path -- setup.sh's GGUF dep install + # (which covers libcurl) was skipped, so configure would fail without the headers. $SUDO apt-get install -y --no-install-recommends \ - build-essential cmake git curl ca-certificates >/dev/null 2>&1 || true + build-essential cmake git curl ca-certificates libcurl4-openssl-dev >/dev/null 2>&1 || true $SUDO apt-get install -y --no-install-recommends gcc-14 g++-14 >/dev/null 2>&1 || true fi @@ -112,11 +115,17 @@ if [ -n "$CC_CAP" ]; then CUDA_ARCH="$CC_CAP"; else CUDA_ARCH="native"; fi # the user's request instead of always tracking ggml-org main. mkdir -p "$(dirname "$LLAMA_DIR")" _LLAMA_REF="${UNSLOTH_LLAMA_TAG:-}" +# Preserve any existing (e.g. CPU-only) llama.cpp so a failed clone OR a failed CUDA +# build doesn't leave the user with NO server: the backup is restored on any failure +# exit and only dropped once a server from the fresh build is confirmed. +_LLAMA_BAK="" +_restore_prev() { + if [ -n "$_LLAMA_BAK" ] && [ -e "$_LLAMA_BAK" ]; then + rm -rf "$LLAMA_DIR" 2>/dev/null + mv "$_LLAMA_BAK" "$LLAMA_DIR" 2>/dev/null && log "restored previous llama.cpp install" + fi +} if [ ! -d "$LLAMA_DIR/.git" ]; then - # Preserve any existing (e.g. CPU-only) llama.cpp so a FAILED clone doesn't leave the user - # with NO server -- restore it on clone failure. A successful clone makes it obsolete (the - # fresh CUDA build replaces it), so the backup is dropped then. - _LLAMA_BAK="" if [ -e "$LLAMA_DIR" ]; then _LLAMA_BAK="${LLAMA_DIR}.prev.$$" rm -rf "$_LLAMA_BAK" 2>/dev/null @@ -131,12 +140,11 @@ if [ ! -d "$LLAMA_DIR/.git" ]; then fi if [ "$_clone_ok" -ne 1 ]; then log "git clone failed" - [ -n "$_LLAMA_BAK" ] && mv "$_LLAMA_BAK" "$LLAMA_DIR" 2>/dev/null # restore previous server + _restore_prev exit 0 fi - [ -n "$_LLAMA_BAK" ] && rm -rf "$_LLAMA_BAK" 2>/dev/null fi -cd "$LLAMA_DIR" || exit 0 +cd "$LLAMA_DIR" || { _restore_prev; exit 0; } log "building CUDA llama.cpp (arch=$CUDA_ARCH, host=$HCXX) - this takes a few minutes..." _cmake_configure() { @@ -153,7 +161,7 @@ _cmake_configure() { if ! _cmake_configure; then log "stale/incompatible CMake cache detected; wiping build dir for a clean CUDA configure" rm -rf build - _cmake_configure || { log "cmake configure failed"; exit 0; } + _cmake_configure || { log "cmake configure failed"; cd /; _restore_prev; exit 0; } fi # Build the full target set unsloth-zoo's GGUF exporter also needs (llama-mtmd-cli, # llama-gguf-split) so one build serves both Studio inference and save_pretrained_gguf. @@ -192,13 +200,19 @@ if ! _cmake_build; then # Wipe build/ and rebuild clean once before giving up. log "build failed (likely interrupted/partial); wiping build dir and rebuilding clean" rm -rf build - _cmake_configure || { log "cmake configure failed"; exit 0; } - _cmake_build || { log "cmake build failed"; exit 0; } + _cmake_configure || { log "cmake configure failed"; cd /; _restore_prev; exit 0; } + _cmake_build || { log "cmake build failed"; cd /; _restore_prev; exit 0; } fi if is_cuda_server "$SERVER"; then log "CUDA llama-server ready: $SERVER" -else + [ -n "$_LLAMA_BAK" ] && rm -rf "$_LLAMA_BAK" 2>/dev/null +elif [ -x "$SERVER" ]; then + # A server exists but isn't CUDA-confirmed; still better than the old backup. log "build finished but CUDA llama-server could not be confirmed" + [ -n "$_LLAMA_BAK" ] && rm -rf "$_LLAMA_BAK" 2>/dev/null +else + log "build finished but no llama-server was produced" + cd /; _restore_prev fi exit 0 diff --git a/studio/setup.sh b/studio/setup.sh index 6ad42f0ff6..a6e92c7b6f 100755 --- a/studio/setup.sh +++ b/studio/setup.sh @@ -1488,7 +1488,11 @@ if [ "$_HOST_SYSTEM" = "Linux" ] \ elif [ -f "$LLAMA_SERVER_BIN" ]; then substep "CUDA build unavailable; keeping existing (CPU) llama-server" "$C_WARN" else - substep "CUDA build unavailable; see ~/.unsloth/llama.cpp build output" "$C_WARN" + substep "CUDA build unavailable and no llama-server present; see $LLAMA_CPP_DIR build output" "$C_WARN" + # No server at all (e.g. the provisioner replaced a previous build and then + # failed): mark degraded so the arm64 CPU-prebuilt last resort below and the + # installer failure exit fire instead of reporting a working install. + _LLAMA_CPP_DEGRADED=true fi fi fi From 80c6eb0af29ae944f1fc59e41c96b80ec55aea3f Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Tue, 9 Jun 2026 20:12:57 -0700 Subject: [PATCH 48/95] fix(install): only quote the WSL distro name when it contains spaces wsl.exe parses its raw command line itself: invoked from the generated unsloth.cmd shim, `wsl.exe -d "Ubuntu-24.04"` fails with WSL_E_DISTRO_NOT_FOUND -- the quotes are treated as part of the name (reproduced live on WSL 2.x). The blanket quoting added in 8e51d18 for spaced UNSLOTH_WSL_DISTRO values therefore broke the shim for every standard distro name. Quote the name only when it actually contains whitespace: bare names keep the proven working form, and spaced names get quoting (bare would split after -d, so quoting is their only viable form). Applied to the shim and the copy-paste hint commands via a single $_distroArg. Co-Authored-By: Claude Opus 4.8 --- install.ps1 | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/install.ps1 b/install.ps1 index 69d0db6c7c..7bc5ae787f 100644 --- a/install.ps1 +++ b/install.ps1 @@ -1556,6 +1556,12 @@ shell.Run cmd, 0, False } $distro = if ($env:UNSLOTH_WSL_DISTRO) { $env:UNSLOTH_WSL_DISTRO } else { "Ubuntu-24.04" } + # For cmd-context uses of the name (the generated .cmd shim, copy-paste hints): + # wsl.exe parses its raw command line itself, and a QUOTED space-free name + # ('wsl -d "Ubuntu-24.04"') fails with WSL_E_DISTRO_NOT_FOUND (verified live on + # 2.x) -- while a bare spaced name would split after -d. So quote ONLY when the + # name contains whitespace. + $_distroArg = if ($distro -match '\s') { '"' + $distro + '"' } else { $distro } # Detect the distro by exit code (encoding-proof; wsl --list emits UTF-16 that PS mis-parses). $haveDistro = $false $global:LASTEXITCODE = -1 @@ -1680,9 +1686,9 @@ shell.Run cmd, 0, False New-Item -ItemType Directory -Force -Path $shimDir *> $null $shimLines = @( '@echo off', - # Quote the distro: an UNSLOTH_WSL_DISTRO with spaces (e.g. "Ubuntu Preview") - # would otherwise split after -d and break every `unsloth ...` invocation. - "wsl.exe -d `"$distro`" -u root -- /root/.unsloth/studio/unsloth_studio/bin/unsloth %*" + # $_distroArg: quoted only if the name has spaces -- wsl.exe rejects a + # quoted space-free name (WSL_E_DISTRO_NOT_FOUND) but splits a bare spaced one. + "wsl.exe -d $_distroArg -u root -- /root/.unsloth/studio/unsloth_studio/bin/unsloth %*" ) Set-Content -LiteralPath (Join-Path $shimDir "unsloth.cmd") -Value $shimLines -Encoding ASCII # A fresh Windows profile may have no HKCU 'Path' value at all -> $userPath is null @@ -1699,7 +1705,7 @@ shell.Run cmd, 0, False substep " unsloth studio # runs in WSL; opens http://localhost:8888" "Cyan" substep " unsloth studio run # also forwarded into WSL" "Cyan" } catch { - substep "(shim creation failed; launch manually): wsl -d `"$distro`" -u root -- bash -lic 'unsloth studio -p 8888'" "Yellow" + substep "(shim creation failed; launch manually): wsl -d $_distroArg -u root -- bash -lic 'unsloth studio -p 8888'" "Yellow" } # Desktop + Start Menu shortcuts: launch the WSL Studio and open the browser when ready. try { @@ -1799,13 +1805,13 @@ shell.Run cmd, 0, False Start-Process -WindowStyle Hidden -FilePath 'wsl.exe' -ArgumentList @('-d', $distro, '--cd', '/root', '-u', 'root', '--', 'bash', '/root/.unsloth/run_llama_build.sh') | Out-Null step "llama.cpp" "building CUDA llama.cpp for GGUF inference in the background (a few min); log: ~/.unsloth/llama_cuda_build.log" "Green" } else { - substep "(GGUF inference needs a CUDA llama.cpp build; build later: wsl -d `"$distro`" -u root -- bash ~/.unsloth/provision_llama_cuda.sh)" "Yellow" + substep "(GGUF inference needs a CUDA llama.cpp build; build later: wsl -d $_distroArg -u root -- bash ~/.unsloth/provision_llama_cuda.sh)" "Yellow" } } catch {} finally { $ErrorActionPreference = $prevEapL } } } else { step "wsl" "WSL Studio install did not finish cleanly (torch.cuda not detected; inner exit $wslRc) -- see log above." "Yellow" - substep "retry, or launch manually: wsl -d `"$distro`" -u root -- bash -lic 'unsloth studio -p 8888'" "Cyan" + substep "retry, or launch manually: wsl -d $_distroArg -u root -- bash -lic 'unsloth studio -p 8888'" "Cyan" } if ($torchOk) { # WSL GPU install succeeded. On this path the Windows venv is vestigial (everything From 76f637053f1b2c15b84d0c264867735813c60226 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Tue, 9 Jun 2026 21:01:40 -0700 Subject: [PATCH 49/95] fix(install): non-zero process exit for -File failures; quote spaced distro in background-build launcher Two Codex round-3 review fixes (both reproduced empirically before fixing): - Exit-InstallFailure (and the WoA deferred-WSL / WSL-install-failed return paths) now `exit $Code` when the script runs from a file (powershell -File or .\install.ps1): a plain return exits the process with 0 regardless of $global:LASTEXITCODE, so automation treated fatal failures -- including the "enable WSL + reboot" deferred state -- as completed installs. Under `irm | iex` $PSCommandPath is empty and `exit` would kill the user's shell, so that context keeps the return + $LASTEXITCODE behavior. Verified: the old pattern exits 0 under -File, the new one exits 1, and an iex run survives with the session intact. Tauri behavior is unchanged (already exited). All Exit-InstallFailure call sites are body-level in Install-UnslothStudio followed by nothing but the trailing invocation, so control flow is unchanged -- only the process exit code. - The detached background CUDA-build launcher now passes $_distroArg instead of the raw distro name: PS 5.1's Start-Process joins -ArgumentList with spaces WITHOUT quoting (verified: 'Ubuntu Preview' arrives as two args), so a spaced UNSLOTH_WSL_DISTRO never started the background builder while the install reported it running. $_distroArg is pre-quoted only when the name contains spaces, since wsl.exe rejects a quoted space-free name. Co-Authored-By: Claude Opus 4.8 --- install.ps1 | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/install.ps1 b/install.ps1 index 7bc5ae787f..f8e0041496 100644 --- a/install.ps1 +++ b/install.ps1 @@ -98,6 +98,15 @@ function Install-UnslothStudio { if ($TauriMode) { exit $Code } + # File-based runs (powershell -File / .\install.ps1) exit 0 on a plain return no + # matter what $LASTEXITCODE says, so automation would treat a fatal failure as a + # completed install -- `exit` carries the code there. Under `irm | iex` there is + # no $PSCommandPath and `exit` would kill the user's shell, so fall through and + # let the caller return (the message is the signal). + if ($PSCommandPath) { + exit $Code + } + $global:LASTEXITCODE = $Code } # ── Parse flags ── @@ -1549,9 +1558,12 @@ shell.Run cmd, 0, False } # WSL2 must be enabled + the machine rebooted before anything can install. Restore any # rolled-aside previous venv and signal not-complete so -File callers don't treat this - # deferred state as a successful install. + # deferred state as a successful install. A plain return exits 0 for `-File` runs no + # matter what $LASTEXITCODE says, so exit explicitly there; under `irm | iex` + # ($PSCommandPath empty) exit would kill the user's shell, so return instead. Restore-StudioVenvRollback $global:LASTEXITCODE = 1 + if ($PSCommandPath) { exit 1 } return } @@ -1801,8 +1813,11 @@ shell.Run cmd, 0, False # Step 2: anchor the build to a detached Windows process. A WSL-side `nohup &` # doesn't survive -- WSL stops the VM when the launching session exits, killing # the build. A persistent Windows-side wsl.exe (Start-Process, no -Wait) keeps the - # VM up for the whole build while install.ps1 returns. All tokens are space-free. - Start-Process -WindowStyle Hidden -FilePath 'wsl.exe' -ArgumentList @('-d', $distro, '--cd', '/root', '-u', 'root', '--', 'bash', '/root/.unsloth/run_llama_build.sh') | Out-Null + # VM up for the whole build while install.ps1 returns. PS 5.1's Start-Process + # joins -ArgumentList with spaces WITHOUT quoting, so a spaced distro name would + # split after -d -- pass $_distroArg (pre-quoted only when spaced; wsl.exe + # rejects a quoted space-free name). All other tokens are space-free. + Start-Process -WindowStyle Hidden -FilePath 'wsl.exe' -ArgumentList @('-d', $_distroArg, '--cd', '/root', '-u', 'root', '--', 'bash', '/root/.unsloth/run_llama_build.sh') | Out-Null step "llama.cpp" "building CUDA llama.cpp for GGUF inference in the background (a few min); log: ~/.unsloth/llama_cuda_build.log" "Green" } else { substep "(GGUF inference needs a CUDA llama.cpp build; build later: wsl -d $_distroArg -u root -- bash ~/.unsloth/provision_llama_cuda.sh)" "Yellow" @@ -1823,9 +1838,10 @@ shell.Run cmd, 0, False } # WSL GPU install failed (torch.cuda unavailable). Restore any rolled-aside previous venv so # a reinstall-over-existing isn't left worse off, and report non-zero so -File callers don't - # treat a broken install as success. + # treat a broken install as success (plain return exits 0 for -File; iex must not exit). Restore-StudioVenvRollback $global:LASTEXITCODE = 1 + if ($PSCommandPath) { exit 1 } return } From b4921e2a835b08f2ec5fe26fec39897bc5f18041 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Tue, 9 Jun 2026 21:58:28 -0700 Subject: [PATCH 50/95] feat(studio): Spark unified-memory OOM guard in the training worker (Strix Halo parity) PR #5301 protects ROCm unified-memory APUs (Strix Halo gfx1150/gfx1151) with a default set_per_process_memory_fraction(0.80) at training-worker startup, because exhausting a shared GPU+OS memory pool can stall the whole box instead of raising a catchable OutOfMemoryError. NVIDIA Spark-class parts (DGX Spark / GB10, N1X "RTX Spark") have the same pool topology and the same failure mode, but only had an opt-in cap (UNSLOTH_SPARK_MEM_FRACTION). - worker.py: new _nvidia_classify_spark_unified_memory(props) mirroring _rocm_classify_unified_memory: is_integrated property first (authoritative on native Linux), then Spark device-name tokens -- WSL2's GPU paravirtualization masks is_integrated to 0 and renames the device (the N1X reports "JMJWOA-Generic-GPU"; verified on hardware), so the property alone misses Spark-under-WSL. Section 1h applies the 0.80 cap on match; UNSLOTH_SPARK_MEM_FRACTION overrides it and any value outside (0, 1] disables the guard. Discrete NVIDIA GPUs and CPU-only hosts are untouched. The existing generic OOM handler in the training loop surfaces the resulting OutOfMemoryError. - _utils.py: range-validate the opt-in UNSLOTH_SPARK_MEM_FRACTION -- "0" previously called set_per_process_memory_fraction(0.0), which makes every subsequent CUDA allocation OOM. - tests: test_spark_oom_guard.py mirroring test_rocm_oom_guard.py (property path, WSL name-token path, discrete negatives). 47/47 pass alongside the ROCm suite. Live-verified on the N1X (WSL2): classifier matches via JMJWOA, and with the cap set an over-allocation raises catchable torch.OutOfMemoryError instead of stalling the box; allocations recover after the error. Co-Authored-By: Claude Opus 4.8 --- studio/backend/core/training/worker.py | 72 +++++++++++++++ studio/backend/tests/test_spark_oom_guard.py | 95 ++++++++++++++++++++ unsloth/models/_utils.py | 6 +- 3 files changed, 172 insertions(+), 1 deletion(-) create mode 100644 studio/backend/tests/test_spark_oom_guard.py diff --git a/studio/backend/core/training/worker.py b/studio/backend/core/training/worker.py index 50323478b2..0390e6745e 100644 --- a/studio/backend/core/training/worker.py +++ b/studio/backend/core/training/worker.py @@ -703,6 +703,36 @@ def _rocm_classify_unified_memory(props: Any) -> tuple[str, bool]: return gcn_arch, is_unified +def _nvidia_classify_spark_unified_memory(props: Any) -> tuple[str, bool]: + """Classify an NVIDIA device as Spark-class unified-memory or discrete. + + Returns ``(marker, is_unified)``: + - ``marker``: the signal that matched (``"is_integrated"`` or the matching + device-name token), else ``""``. + - ``is_unified``: ``True`` for Spark-class parts that share one memory pool + with the OS (DGX Spark / GB10, N1X "RTX Spark", Grace-Blackwell desksides) + — these need the same lower ``set_per_process_memory_fraction`` cap as the + ROCm APUs: exhausting the shared pool can stall the whole box instead of + raising a catchable OutOfMemoryError. + + Classification priority: + 1. ``is_integrated`` device property (authoritative on native Linux). + 2. Device-name token match — WSL2's GPU paravirtualization masks + ``is_integrated`` to 0 and renames the device (the N1X reports + ``JMJWOA-Generic-GPU`` with ``is_integrated == 0``, verified on + hardware), so the property alone misses Spark-under-WSL. Tokens mirror + ``_DGX_SPARK_DEVICE_TOKENS`` in ``unsloth/models/_utils.py`` (duplicated + because this guard runs before any ML import). + """ + if getattr(props, "is_integrated", 0): + return "is_integrated", True + name_upper = (getattr(props, "name", "") or "").upper() + for token in ("GB10", "GB110", "JMJWOA", "N1X", "DGX SPARK"): + if token in name_upper: + return token, True + return "", False + + def _tilelang_platform_supported() -> bool: """True iff a tilelang 0.1.8 wheel will load: Linux x86_64/aarch64, non-HIP torch. @@ -2194,6 +2224,48 @@ def run_training_process(*, event_queue: Any, stop_queue: Any, config: dict) -> except Exception as _oom_guard_err: logger.debug("Could not set GPU memory fraction: %s", _oom_guard_err) + # ── 1h. NVIDIA Spark-class unified-memory OOM guard ── + # Same failure mode as the ROCm APU guard above, NVIDIA flavor: Spark-class + # parts (DGX Spark / GB10, N1X "RTX Spark") share one memory pool with the + # OS, so over-allocation can stall the whole box instead of raising a + # catchable OutOfMemoryError. Cap the allocator at 0.80 like Strix Halo — + # the pool is shared with the host OS and page cache, so 20% headroom stays + # with the system. UNSLOTH_SPARK_MEM_FRACTION overrides the cap; any value + # outside (0, 1] disables the guard. Discrete NVIDIA GPUs are untouched + # (they already raise a graceful OOM). The generic OOM handler in the + # training loop surfaces the resulting OutOfMemoryError with remediation. + else: + try: + import torch as _torch_mem + if _torch_mem.cuda.is_available(): + _props = _torch_mem.cuda.get_device_properties(0) + _marker, _is_spark_uma = _nvidia_classify_spark_unified_memory(_props) + if _is_spark_uma: + _mem_fraction = 0.80 + _frac_env = os.environ.get("UNSLOTH_SPARK_MEM_FRACTION") + if _frac_env: + try: + _mem_fraction = float(_frac_env) + except ValueError: + _mem_fraction = 0.80 + if 0.0 < _mem_fraction <= 1.0: + _torch_mem.cuda.set_per_process_memory_fraction(_mem_fraction) + logger.info( + "Spark unified-memory OOM guard: " + "set_per_process_memory_fraction(%.2f) — %s (matched %s)", + _mem_fraction, + _props.name, + _marker, + ) + else: + logger.info( + "Spark unified-memory OOM guard disabled " + "(UNSLOTH_SPARK_MEM_FRACTION=%s)", + _frac_env, + ) + except Exception as _oom_guard_err: + logger.debug("Could not set GPU memory fraction: %s", _oom_guard_err) + # ── 2. Now import ML libraries (fresh in this clean process) ── try: _send_status(event_queue, "Importing Unsloth...") diff --git a/studio/backend/tests/test_spark_oom_guard.py b/studio/backend/tests/test_spark_oom_guard.py new file mode 100644 index 0000000000..74610d4369 --- /dev/null +++ b/studio/backend/tests/test_spark_oom_guard.py @@ -0,0 +1,95 @@ +# SPDX-License-Identifier: AGPL-3.0-only +# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 + +"""Unit tests for _nvidia_classify_spark_unified_memory (Spark OOM-guard classifier). + +Two paths: (1) the ``is_integrated`` device property (authoritative on native +Linux), (2) device-name token match — needed because WSL2's GPU +paravirtualization masks ``is_integrated`` to 0 and renames the device (the N1X +reports ``JMJWOA-Generic-GPU``; verified on hardware). + +Mirrors test_rocm_oom_guard.py for the ROCm/Strix-Halo classifier the NVIDIA +guard was modeled on. +""" + +from __future__ import annotations + +from types import SimpleNamespace + +import pytest + +from core.training.worker import _nvidia_classify_spark_unified_memory + + +def _props(**kwargs) -> SimpleNamespace: + """Build a fake device-properties object with the given attributes.""" + return SimpleNamespace(**kwargs) + + +# ── Path 1: is_integrated property ─────────────────────────────────────────── + + +class TestIsIntegratedProperty: + """``is_integrated`` truthy means unified memory, regardless of name.""" + + def test_integrated_native_spark(self) -> None: + props = _props(is_integrated = 1, name = "NVIDIA GB10") + marker, is_unified = _nvidia_classify_spark_unified_memory(props) + assert marker == "is_integrated" + assert is_unified is True + + def test_integrated_wins_even_with_unknown_name(self) -> None: + props = _props(is_integrated = 1, name = "Some Future Unified Part") + marker, is_unified = _nvidia_classify_spark_unified_memory(props) + assert marker == "is_integrated" + assert is_unified is True + + +# ── Path 2: device-name token fallback (WSL masks is_integrated) ──────────── + + +class TestDeviceNameTokenFallback: + """is_integrated == 0 (or absent) -> classify by Spark name tokens.""" + + @pytest.mark.parametrize( + "name, expected_marker", + [ + ("JMJWOA-Generic-GPU", "JMJWOA"), # N1X under WSL2 (verified live) + ("NVIDIA GB10", "GB10"), # native DGX Spark + ("NVIDIA GB110", "GB110"), # "GB10" is not a substring of "GB110" + ("NVIDIA DGX Spark", "DGX SPARK"), + ("nvidia n1x prototype", "N1X"), # case-insensitive + ], + ) + def test_spark_names_unified(self, name: str, expected_marker: str) -> None: + props = _props(is_integrated = 0, name = name) + marker, is_unified = _nvidia_classify_spark_unified_memory(props) + assert is_unified is True + assert marker == expected_marker + + @pytest.mark.parametrize( + "name", + [ + "NVIDIA GeForce RTX 4090", + "NVIDIA H100 80GB HBM3", + "NVIDIA RTX 6000 Ada Generation", + "Tesla T4", + ], + ) + def test_discrete_names_not_unified(self, name: str) -> None: + props = _props(is_integrated = 0, name = name) + marker, is_unified = _nvidia_classify_spark_unified_memory(props) + assert is_unified is False + assert marker == "" + + def test_missing_attrs_defaults_discrete(self) -> None: + """No is_integrated, no name -> discrete (guard stays off).""" + marker, is_unified = _nvidia_classify_spark_unified_memory(_props()) + assert is_unified is False + assert marker == "" + + def test_none_name_defaults_discrete(self) -> None: + props = _props(is_integrated = 0, name = None) + marker, is_unified = _nvidia_classify_spark_unified_memory(props) + assert is_unified is False + assert marker == "" diff --git a/unsloth/models/_utils.py b/unsloth/models/_utils.py index 270eb14fd6..c9e77336e0 100644 --- a/unsloth/models/_utils.py +++ b/unsloth/models/_utils.py @@ -1049,7 +1049,11 @@ def patch_dgx_spark_runtime_defaults(): _frac = os.environ.get("UNSLOTH_SPARK_MEM_FRACTION") if _frac: try: - torch.cuda.set_per_process_memory_fraction(float(_frac)) + # Only (0, 1] is a usable cap: 0 would make EVERY allocation OOM + # and values > 1 are rejected by torch. Out-of-range = no cap. + _frac_val = float(_frac) + if 0.0 < _frac_val <= 1.0: + torch.cuda.set_per_process_memory_fraction(_frac_val) except Exception: pass From ca8c1b434de6a766b30f08f64fe3343f47847255 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Tue, 9 Jun 2026 22:33:21 -0700 Subject: [PATCH 51/95] fix(install): bitsandbytes on aarch64+NVIDIA so 4-bit QLoRA works out of the box The base unsloth package does not depend on bitsandbytes and the cuXXX extras that normally add it are x86_64-oriented, so the Spark-class install path (DGX Spark / GB10 / N1X, native or WSL) produced a venv where FastLanguageModel.from_pretrained(..., load_in_4bit=True) fails with ModuleNotFoundError -- found while benchmarking the UMA training knobs on the N1X. bitsandbytes ships working aarch64 manylinux wheels (0.49.2 verified on sm_121 Blackwell: 4-bit Linear4bit forward runs on GPU via PTX JIT), so install.sh now adds it best-effort on Linux aarch64 + NVIDIA after the unsloth install, using the same version constraint as pyproject (>=0.45.5,!=0.46.0,!=0.48.0). Platforms without a wheel just keep 16-bit LoRA / full finetuning, with a substep saying so. Live-tested on the N1X: with bitsandbytes removed from the venv, the block reinstalls and imports it; benchmark suite then ran 4-bit QLoRA training in 7 configs without error. Co-Authored-By: Claude Opus 4.8 --- install.sh | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/install.sh b/install.sh index 8cc69a3132..09e637aede 100755 --- a/install.sh +++ b/install.sh @@ -2373,6 +2373,21 @@ elif [ -n "$TORCH_INDEX_URL" ]; then run_install_cmd "install unsloth" uv pip install --python "$_VENV_PY" \ --upgrade-package unsloth -- "$PACKAGE_NAME" fi + # aarch64 + NVIDIA (DGX Spark / GB10 / N1X, native or WSL): the base unsloth + # package does not depend on bitsandbytes and the cuXXX extras that normally + # add it are x86_64-oriented, so 4-bit QLoRA fails with ModuleNotFoundError + # out of the box. bitsandbytes ships working aarch64 manylinux wheels + # (verified on sm_121 Blackwell via PTX JIT), so add it best-effort -- a + # platform without a wheel just keeps 16-bit LoRA / full finetuning. + if { [ "$(uname -m)" = "aarch64" ] || [ "$(uname -m)" = "arm64" ]; } \ + && command -v nvidia-smi >/dev/null 2>&1 \ + && nvidia-smi -L 2>/dev/null | awk '/^GPU[[:space:]]+[0-9]+:/{found=1} END{exit !found}' \ + && ! "$_VENV_PY" -c "import bitsandbytes" >/dev/null 2>&1; then + substep "installing bitsandbytes (aarch64 wheels; enables 4-bit QLoRA)..." + if ! uv pip install --python "$_VENV_PY" "bitsandbytes>=0.45.5,!=0.46.0,!=0.48.0" >/dev/null 2>&1; then + substep "(no bitsandbytes wheel for this platform; 16-bit LoRA + full finetuning still work)" + fi + fi # AMD ROCm: repair torch if the unsloth/unsloth-zoo install pulled in # CUDA torch from PyPI, overwriting the ROCm wheels installed in Step 1. if [ "$SKIP_TORCH" = false ]; then From c31a6e876d66c0dcc9cd5c26b10e9784e589fae7 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Tue, 9 Jun 2026 23:12:47 -0700 Subject: [PATCH 52/95] fix(install,studio): address Codex round-4 review (6 of 7 comments real) - install.sh: gate the new aarch64 bitsandbytes block on SKIP_TORCH=false -- with --no-torch/UNSLOTH_NO_TORCH (GGUF-only install) it would have pulled torch back into the venv through bitsandbytes' dependencies. - studio worker: in the new Spark OOM-guard section, decide PYTORCH_CUDA_ALLOC_CONF (expandable_segments) BEFORE the guard's first CUDA touch -- get_device_properties initializes the CUDA allocator, after which the env var is ignored, and the later `import unsloth` (patch_dgx_spark_memory_config) is too late for the worker process. Uses the same CUDA-free nvidia-smi name sniff, append-don't-override, and UNSLOTH_NO_EXPANDABLE_SEGMENTS opt-out as the library patch. Live-verified on the N1X: env set while torch.cuda.is_initialized() is still False. - uninstall.ps1: only run `fuser -k 8888/tcp` in a probed WSL distro when an Unsloth install actually exists there (checked BEFORE the rm deletes the marker) -- an unrelated listener on 8888 (e.g. Jupyter) in a clean distro must survive a Windows-side uninstall. The Unsloth-specific pkills stay unconditional. - install.ps1 + uninstall.ps1: persist the chosen WSL distro to %LOCALAPPDATA%\Unsloth\wsl-distro.txt at install; uninstall reads it (before removing the directory) and prepends it to the cleanup candidates, so a custom UNSLOTH_WSL_DISTRO install is cleaned without the env var being set again at uninstall time. - provision_llama_cuda.sh: honor UNSLOTH_LLAMA_PR (numeric-validated, best-effort fetch of pull/N/head after clone) so a provisioned tree matches a PR pin the way setup.sh does; and require only llama-server in the main cmake build (mirroring setup.sh), building the helper targets (llama-cli/quantize/mtmd-cli/gguf-split) best-effort afterwards -- an older UNSLOTH_LLAMA_TAG pin lacking a newer helper target no longer fails the whole provision. Not changed: the "--tauri rejection doesn't restore the venv rollback" comment is incorrect -- the rejection returns through Exit-InstallFailure, which itself calls Restore-StudioVenvRollback. Co-Authored-By: Claude Opus 4.8 --- install.ps1 | 3 +++ install.sh | 5 ++++- scripts/uninstall.ps1 | 20 +++++++++++++++++- studio/backend/core/training/worker.py | 28 ++++++++++++++++++++++++++ studio/scripts/provision_llama_cuda.sh | 27 +++++++++++++++++++++++-- 5 files changed, 79 insertions(+), 4 deletions(-) diff --git a/install.ps1 b/install.ps1 index f8e0041496..8ae84f97bb 100644 --- a/install.ps1 +++ b/install.ps1 @@ -1703,6 +1703,9 @@ shell.Run cmd, 0, False "wsl.exe -d $_distroArg -u root -- /root/.unsloth/studio/unsloth_studio/bin/unsloth %*" ) Set-Content -LiteralPath (Join-Path $shimDir "unsloth.cmd") -Value $shimLines -Encoding ASCII + # Record the distro for the uninstaller: a custom UNSLOTH_WSL_DISTRO install + # must be cleanable without the env var being set again at uninstall time. + try { Set-Content -LiteralPath (Join-Path (Split-Path $shimDir -Parent) "wsl-distro.txt") -Value $distro -Encoding ASCII } catch {} # A fresh Windows profile may have no HKCU 'Path' value at all -> $userPath is null # and $userPath.TrimEnd() would throw, losing the shim. Treat null as empty. $userPath = [Environment]::GetEnvironmentVariable("Path", "User") diff --git a/install.sh b/install.sh index 09e637aede..7595255a39 100755 --- a/install.sh +++ b/install.sh @@ -2379,7 +2379,10 @@ elif [ -n "$TORCH_INDEX_URL" ]; then # out of the box. bitsandbytes ships working aarch64 manylinux wheels # (verified on sm_121 Blackwell via PTX JIT), so add it best-effort -- a # platform without a wheel just keeps 16-bit LoRA / full finetuning. - if { [ "$(uname -m)" = "aarch64" ] || [ "$(uname -m)" = "arm64" ]; } \ + # Gated on SKIP_TORCH: a --no-torch/UNSLOTH_NO_TORCH (GGUF-only) install must + # not have bitsandbytes drag torch back into the venv via its dependencies. + if [ "$SKIP_TORCH" = false ] \ + && { [ "$(uname -m)" = "aarch64" ] || [ "$(uname -m)" = "arm64" ]; } \ && command -v nvidia-smi >/dev/null 2>&1 \ && nvidia-smi -L 2>/dev/null | awk '/^GPU[[:space:]]+[0-9]+:/{found=1} END{exit !found}' \ && ! "$_VENV_PY" -c "import bitsandbytes" >/dev/null 2>&1; then diff --git a/scripts/uninstall.ps1 b/scripts/uninstall.ps1 index 46f9a87a77..2cdf01e5d8 100644 --- a/scripts/uninstall.ps1 +++ b/scripts/uninstall.ps1 @@ -344,6 +344,19 @@ function Uninstall-UnslothStudio { # %LOCALAPPDATA%\Unsloth (not "Unsloth Studio") with a PATH entry -- all missed by the cleanup above. _Step "Removing WSL-fallback artifacts (shim, launcher, PATH entry, WSL install)..." $unslothDir = if ($env:LOCALAPPDATA) { Join-Path $env:LOCALAPPDATA "Unsloth" } else { $null } + # The installer records its WSL distro in wsl-distro.txt so a custom + # UNSLOTH_WSL_DISTRO install is cleanable without the env var being set again + # at uninstall time. Read it BEFORE the directory is removed below. + $_recordedDistro = $null + if ($unslothDir) { + try { + $_distroFile = Join-Path $unslothDir "wsl-distro.txt" + if (Test-Path -LiteralPath $_distroFile) { + $_recordedDistro = (Get-Content -LiteralPath $_distroFile -ErrorAction SilentlyContinue | Select-Object -First 1) + if ($_recordedDistro) { $_recordedDistro = $_recordedDistro.Trim() } + } + } catch { } + } if ($unslothDir) { $shimDir = (Join-Path $unslothDir "bin").TrimEnd('\', '/') try { @@ -379,8 +392,13 @@ function Uninstall-UnslothStudio { # touching /home/*/.unsloth would erase an unrelated WSL user's own Unsloth/cache that this # installer never created. pkill patterns use the [x]-regex self-exclusion trick: '[u]nsloth_studio' # keeps the shell's own argv from matching while real processes still match. Same for '[l]lama-server'. - $_clean = 'rm -rf /root/.unsloth /root/llama-cuda /root/provision_llama_cuda.sh /root/llama_cuda_build.log 2>/dev/null; rm -f /root/.local/bin/unsloth 2>/dev/null; fuser -k 8888/tcp 2>/dev/null; pkill -9 -f ''[u]nsloth_studio'' 2>/dev/null; pkill -9 -f ''[l]lama-server'' 2>/dev/null; true' + # The port-8888 kill is gated on an Unsloth install actually existing in the + # distro (checked BEFORE the rm deletes the marker): a probed distro with an + # unrelated listener on 8888 (Jupyter etc.) must not lose it. The pkills are + # already Unsloth-specific, so they stay unconditional. + $_clean = '_had=0; if [ -d /root/.unsloth ] || [ -L /root/.local/bin/unsloth ]; then _had=1; fi; rm -rf /root/.unsloth /root/llama-cuda /root/provision_llama_cuda.sh /root/llama_cuda_build.log 2>/dev/null; rm -f /root/.local/bin/unsloth 2>/dev/null; if [ $_had -eq 1 ]; then fuser -k 8888/tcp 2>/dev/null; fi; pkill -9 -f ''[u]nsloth_studio'' 2>/dev/null; pkill -9 -f ''[l]lama-server'' 2>/dev/null; true' $_cands = @('', 'Ubuntu', 'Ubuntu-24.04', 'Ubuntu-22.04', 'Debian') + if ($_recordedDistro) { $_cands = @($_recordedDistro) + $_cands } if ($env:UNSLOTH_WSL_DISTRO) { $_cands = @($env:UNSLOTH_WSL_DISTRO) + $_cands } $_done = @{} foreach ($d in $_cands) { diff --git a/studio/backend/core/training/worker.py b/studio/backend/core/training/worker.py index 0390e6745e..15f9e6ff51 100644 --- a/studio/backend/core/training/worker.py +++ b/studio/backend/core/training/worker.py @@ -2236,6 +2236,34 @@ def run_training_process(*, event_queue: Any, stop_queue: Any, config: dict) -> # training loop surfaces the resulting OutOfMemoryError with remediation. else: try: + # The Spark allocator config must be decided BEFORE this guard's first + # CUDA touch: get_device_properties below initializes the CUDA allocator, + # after which PYTORCH_CUDA_ALLOC_CONF changes are ignored -- and the later + # `import unsloth` (patch_dgx_spark_memory_config) would be too late for + # THIS worker process even though it is in time for a plain + # `import unsloth`. CUDA-free sniff via nvidia-smi device names (mirrors + # _is_dgx_spark_no_cuda_init), with the same append-don't-override and + # UNSLOTH_NO_EXPANDABLE_SEGMENTS opt-out semantics as the library patch. + try: + import platform as _plat + _spark_smi = False + if _plat.machine().lower() in ("aarch64", "arm64"): + _smi = _sp.run( + ["nvidia-smi", "--query-gpu=name", "--format=csv,noheader"], + capture_output = True, text = True, timeout = 5, + ) + _names_u = (_smi.stdout or "").upper() + _spark_smi = any( + t in _names_u for t in ("GB10", "GB110", "JMJWOA", "N1X", "DGX SPARK") + ) + if _spark_smi and os.environ.get("UNSLOTH_NO_EXPANDABLE_SEGMENTS") != "1": + _conf = os.environ.get("PYTORCH_CUDA_ALLOC_CONF", "") + if "expandable_segments" not in _conf: + os.environ["PYTORCH_CUDA_ALLOC_CONF"] = ( + (_conf + "," if _conf else "") + "expandable_segments:True" + ) + except Exception: + pass import torch as _torch_mem if _torch_mem.cuda.is_available(): _props = _torch_mem.cuda.get_device_properties(0) diff --git a/studio/scripts/provision_llama_cuda.sh b/studio/scripts/provision_llama_cuda.sh index a15d69e1d2..d89e192dc4 100644 --- a/studio/scripts/provision_llama_cuda.sh +++ b/studio/scripts/provision_llama_cuda.sh @@ -143,6 +143,20 @@ if [ ! -d "$LLAMA_DIR/.git" ]; then _restore_prev exit 0 fi + # Honor a llama.cpp PR pin (UNSLOTH_LLAMA_PR, the same var setup.sh supports) + # so a provisioned tree matches the user's request instead of silently building + # the default branch. Best-effort: a failed fetch keeps the default branch. + case "${UNSLOTH_LLAMA_PR:-}" in + ''|*[!0-9]*) ;; + *) + if git -C "$LLAMA_DIR" fetch --depth 1 origin "pull/${UNSLOTH_LLAMA_PR}/head:_unsloth_pr_${UNSLOTH_LLAMA_PR}" >/dev/null 2>&1 \ + && git -C "$LLAMA_DIR" checkout "_unsloth_pr_${UNSLOTH_LLAMA_PR}" >/dev/null 2>&1; then + log "checked out llama.cpp PR #${UNSLOTH_LLAMA_PR} (UNSLOTH_LLAMA_PR)" + else + log "could not fetch llama.cpp PR #${UNSLOTH_LLAMA_PR}; building the default branch" + fi + ;; + esac fi cd "$LLAMA_DIR" || { _restore_prev; exit 0; } @@ -190,8 +204,16 @@ _NICE="" command -v nice >/dev/null 2>&1 && _NICE="nice -n 19" command -v ionice >/dev/null 2>&1 && _NICE="$_NICE ionice -c 3" _cmake_build() { - $_NICE cmake --build build -j"$JOBS" --target \ - llama-server llama-cli llama-quantize llama-mtmd-cli llama-gguf-split >/dev/null 2>&1 + # Only llama-server is REQUIRED (mirrors setup.sh's source path): an older + # UNSLOTH_LLAMA_TAG pin may predate newer helper targets (llama-mtmd-cli, + # llama-gguf-split), and those missing must not fail the whole provision. + $_NICE cmake --build build -j"$JOBS" --target llama-server >/dev/null 2>&1 +} +_cmake_build_extras() { + # Helper targets unsloth-zoo's GGUF exporter also uses -- best-effort each. + for _t in llama-cli llama-quantize llama-mtmd-cli llama-gguf-split; do + $_NICE cmake --build build -j"$JOBS" --target "$_t" >/dev/null 2>&1 || true + done } if ! _cmake_build; then # An interrupted build (e.g. a thermal/power shutdown mid-compile, which this @@ -203,6 +225,7 @@ if ! _cmake_build; then _cmake_configure || { log "cmake configure failed"; cd /; _restore_prev; exit 0; } _cmake_build || { log "cmake build failed"; cd /; _restore_prev; exit 0; } fi +_cmake_build_extras if is_cuda_server "$SERVER"; then log "CUDA llama-server ready: $SERVER" From 9af17e2aa58f05b2256b63cf6173d10bf29fb0f7 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 10 Jun 2026 06:13:15 +0000 Subject: [PATCH 53/95] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- studio/backend/core/training/worker.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/studio/backend/core/training/worker.py b/studio/backend/core/training/worker.py index 15f9e6ff51..317b2301f8 100644 --- a/studio/backend/core/training/worker.py +++ b/studio/backend/core/training/worker.py @@ -2246,11 +2246,14 @@ def run_training_process(*, event_queue: Any, stop_queue: Any, config: dict) -> # UNSLOTH_NO_EXPANDABLE_SEGMENTS opt-out semantics as the library patch. try: import platform as _plat + _spark_smi = False if _plat.machine().lower() in ("aarch64", "arm64"): _smi = _sp.run( ["nvidia-smi", "--query-gpu=name", "--format=csv,noheader"], - capture_output = True, text = True, timeout = 5, + capture_output = True, + text = True, + timeout = 5, ) _names_u = (_smi.stdout or "").upper() _spark_smi = any( @@ -2260,8 +2263,8 @@ def run_training_process(*, event_queue: Any, stop_queue: Any, config: dict) -> _conf = os.environ.get("PYTORCH_CUDA_ALLOC_CONF", "") if "expandable_segments" not in _conf: os.environ["PYTORCH_CUDA_ALLOC_CONF"] = ( - (_conf + "," if _conf else "") + "expandable_segments:True" - ) + _conf + "," if _conf else "" + ) + "expandable_segments:True" except Exception: pass import torch as _torch_mem From 5c2aefc6d17c97cebeb1eeffc06fd458f0421e51 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Tue, 9 Jun 2026 23:56:31 -0700 Subject: [PATCH 54/95] fix(install,studio): address review round 5 (3 real of 10; 5 PS-5.1 claims disproven on hardware) Real fixes: - uninstall.ps1: scope the WSL process kill to argv referencing /root/.unsloth/ (the fallback's install dir, which its Studio server, llama-server, and build runner all reference) instead of the bare '[l]lama-server' / '[u]nsloth_studio' name patterns -- uninstalling the Windows shim must not kill a user's own unrelated llama.cpp server or a /home Studio in a probed distro. Proven live: the path pattern matched exactly the three fallback processes while a planted /tmp/llama-server decoy matched the old pattern and not the new one. The backslash in '/root/\.unslot[h]/' keeps the pattern from matching the cleanup command's own argv. - install.ps1: bridge UNSLOTH_LLAMA_TAG / UNSLOTH_LLAMA_PR into the background CUDA-build runner -- the provisioner honors both pins, but Windows env vars don't cross into WSL on their own, so a user's pin was silently ignored by the deferred build. (Deliberately NOT forwarded into the inner install.sh env: setup.sh skips its deferral when a PR pin is visible there, which would CPU-build the pin in the foreground.) - kernels/flex_attention.py: make _flex_is_dgx_spark() CUDA-free (nvidia-smi device names, mirroring _is_dgx_spark_no_cuda_init) -- it runs at module import and called torch.cuda.get_device_name(), which initializes the CUDA allocator before patch_dgx_spark_memory_config() can set PYTORCH_CUDA_ALLOC_CONF on exactly the Spark hosts it targets (reachable via vision.py importing ..kernels before ._utils). Verified on the N1X: detects the machine with torch.cuda.is_initialized() still False. - _utils.py: the TrainingArguments __post_init__ wrapper now forwards *args/**kwargs (robustness against future InitVar signatures). Disproven on hardware (no change): the five "high" PS-5.1 claims -- String.TrimEnd('\', '/') with multiple char args binds fine to params char[] (verified on PS 5.1.28000.1737, and the uninstaller's PATH cleanup using exactly that code ran successfully this same day), and [Text.Encoding] resolves via the System namespace prefix (the background build dispatch using it has run in every install this week). The worker "_sp possibly undefined" claim is false: `import subprocess as _sp` is at worker.py line 25. Co-Authored-By: Claude Opus 4.8 --- install.ps1 | 8 +++++++- scripts/uninstall.ps1 | 14 +++++++++----- unsloth/kernels/flex_attention.py | 18 ++++++++++++------ unsloth/models/_utils.py | 6 ++++-- 4 files changed, 32 insertions(+), 14 deletions(-) diff --git a/install.ps1 b/install.ps1 index 8ae84f97bb..c1714b104a 100644 --- a/install.ps1 +++ b/install.ps1 @@ -1808,7 +1808,13 @@ shell.Run cmd, 0, False # pass only space-free args, avoiding Start-Process mis-splitting `bash -lc `. $_pathLine = 'export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/lib/wsl/lib:$PATH"' + "`n" $_jobsLine = if ($env:UNSLOTH_LLAMA_BUILD_JOBS) { "export UNSLOTH_LLAMA_BUILD_JOBS=$($env:UNSLOTH_LLAMA_BUILD_JOBS)`n" } else { "" } - $_runner = "#!/usr/bin/env bash`n" + $_pathLine + $_jobsLine + "exec bash /root/.unsloth/provision_llama_cuda.sh > /root/.unsloth/llama_cuda_build.log 2>&1`n" + # Bridge llama.cpp pins into WSL: the provisioner honors UNSLOTH_LLAMA_TAG / + # UNSLOTH_LLAMA_PR, but Windows env vars don't cross into WSL on their own -- + # without these exports a user's pin would be silently ignored by the + # deferred background build. sh-single-quoted (tags/PRs are simple tokens). + $_tagLine = if ($env:UNSLOTH_LLAMA_TAG) { "export UNSLOTH_LLAMA_TAG='$($env:UNSLOTH_LLAMA_TAG)'`n" } else { "" } + $_prLine = if ($env:UNSLOTH_LLAMA_PR) { "export UNSLOTH_LLAMA_PR='$($env:UNSLOTH_LLAMA_PR)'`n" } else { "" } + $_runner = "#!/usr/bin/env bash`n" + $_pathLine + $_jobsLine + $_tagLine + $_prLine + "exec bash /root/.unsloth/provision_llama_cuda.sh > /root/.unsloth/llama_cuda_build.log 2>&1`n" $_runnerB64 = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($_runner)) $_fetchCmd = 'mkdir -p /root/.unsloth; if curl -fsSL "' + $_llamaUrl + '" -o /root/.unsloth/provision_llama_cuda.sh && [ -s /root/.unsloth/provision_llama_cuda.sh ]; then chmod +x /root/.unsloth/provision_llama_cuda.sh; echo ' + $_runnerB64 + ' | base64 -d > /root/.unsloth/run_llama_build.sh; chmod +x /root/.unsloth/run_llama_build.sh; echo PROV_FETCHED; else echo PROV_NOSCRIPT; fi' $_fetchOut = & wsl.exe -d $distro --cd /root -u root -- bash -lc $_fetchCmd 2>$null diff --git a/scripts/uninstall.ps1 b/scripts/uninstall.ps1 index 2cdf01e5d8..8a5ccaf185 100644 --- a/scripts/uninstall.ps1 +++ b/scripts/uninstall.ps1 @@ -390,13 +390,17 @@ function Uninstall-UnslothStudio { # /root/.local/bin/unsloth symlink (its target under /root/.unsloth is gone but the link still # resolves on PATH). Scope STRICTLY to /root: the WoA fallback installs there (wsl -u root), so # touching /home/*/.unsloth would erase an unrelated WSL user's own Unsloth/cache that this - # installer never created. pkill patterns use the [x]-regex self-exclusion trick: '[u]nsloth_studio' - # keeps the shell's own argv from matching while real processes still match. Same for '[l]lama-server'. + # installer never created. # The port-8888 kill is gated on an Unsloth install actually existing in the # distro (checked BEFORE the rm deletes the marker): a probed distro with an - # unrelated listener on 8888 (Jupyter etc.) must not lose it. The pkills are - # already Unsloth-specific, so they stay unconditional. - $_clean = '_had=0; if [ -d /root/.unsloth ] || [ -L /root/.local/bin/unsloth ]; then _had=1; fi; rm -rf /root/.unsloth /root/llama-cuda /root/provision_llama_cuda.sh /root/llama_cuda_build.log 2>/dev/null; rm -f /root/.local/bin/unsloth 2>/dev/null; if [ $_had -eq 1 ]; then fuser -k 8888/tcp 2>/dev/null; fi; pkill -9 -f ''[u]nsloth_studio'' 2>/dev/null; pkill -9 -f ''[l]lama-server'' 2>/dev/null; true' + # unrelated listener on 8888 (Jupyter etc.) must not lose it. The process kill is + # scoped to argv referencing /root/.unsloth/ -- the fallback's install dir, which + # its Studio server, llama-server, and build runner all reference -- instead of + # bare name patterns that would also kill a user's own unrelated llama-server or + # a /home Studio in that distro. The backslash in '/root/\.unslot[h]/' keeps the + # pattern from matching this command's own argv (whose literal text contains the + # escaped form, not the resolved path) -- same idea as the [x]-bracket trick. + $_clean = '_had=0; if [ -d /root/.unsloth ] || [ -L /root/.local/bin/unsloth ]; then _had=1; fi; rm -rf /root/.unsloth /root/llama-cuda /root/provision_llama_cuda.sh /root/llama_cuda_build.log 2>/dev/null; rm -f /root/.local/bin/unsloth 2>/dev/null; if [ $_had -eq 1 ]; then fuser -k 8888/tcp 2>/dev/null; fi; pkill -9 -f ''/root/\.unslot[h]/'' 2>/dev/null; true' $_cands = @('', 'Ubuntu', 'Ubuntu-24.04', 'Ubuntu-22.04', 'Debian') if ($_recordedDistro) { $_cands = @($_recordedDistro) + $_cands } if ($env:UNSLOTH_WSL_DISTRO) { $_cands = @($env:UNSLOTH_WSL_DISTRO) + $_cands } diff --git a/unsloth/kernels/flex_attention.py b/unsloth/kernels/flex_attention.py index e1e0dc399b..512e6361ff 100644 --- a/unsloth/kernels/flex_attention.py +++ b/unsloth/kernels/flex_attention.py @@ -27,8 +27,12 @@ torch_compile_options = { def _flex_is_dgx_spark(): - # Inlined copy of _utils.is_dgx_spark() to avoid a circular import. - # Spark = aarch64 + NVIDIA CUDA + a Spark device-name token. + # Inlined CUDA-free copy of _utils._is_dgx_spark_no_cuda_init() (kept local to + # avoid a circular import). Spark = aarch64 + a Spark device name via nvidia-smi. + # Must NOT touch torch.cuda: this runs at module import, and vision.py imports + # ..kernels before ._utils -- a device-name query here would initialize the CUDA + # allocator before patch_dgx_spark_memory_config() can set PYTORCH_CUDA_ALLOC_CONF + # on the very Spark hosts this check targets. _force = os.environ.get("UNSLOTH_FORCE_DGX_SPARK") if _force == "1": return True @@ -39,11 +43,13 @@ def _flex_is_dgx_spark(): if platform.machine().lower() not in ("aarch64", "arm64"): return False - if not (hasattr(torch, "cuda") and torch.cuda.is_available()): - return False - names = " ".join( - str(torch.cuda.get_device_name(i)).upper() for i in range(torch.cuda.device_count()) + import subprocess + + out = subprocess.run( + ["nvidia-smi", "--query-gpu=name", "--format=csv,noheader"], + capture_output = True, text = True, timeout = 5, ) + names = (out.stdout or "").upper() return any(t in names for t in ("GB10", "JMJWOA", "N1X", "DGX SPARK", "GB110")) except Exception: return False diff --git a/unsloth/models/_utils.py b/unsloth/models/_utils.py index c13e46bfce..e990b32737 100644 --- a/unsloth/models/_utils.py +++ b/unsloth/models/_utils.py @@ -1177,8 +1177,10 @@ def patch_dgx_spark_dataloader_defaults(): return _orig_post_init = Base.__post_init__ - def __post_init__(self): - _orig_post_init(self) + # Forward *args/**kwargs so a future TrainingArguments (or a subclass) that + # adds InitVar parameters to __post_init__ keeps working through the wrapper. + def __post_init__(self, *args, **kwargs): + _orig_post_init(self, *args, **kwargs) if getattr(self, "dataloader_pin_memory", None) is True: self.dataloader_pin_memory = False From 5f7d99b4bded0b116eaafdc06cadcd6ce72fff5a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 10 Jun 2026 06:57:10 +0000 Subject: [PATCH 55/95] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- unsloth/kernels/flex_attention.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/unsloth/kernels/flex_attention.py b/unsloth/kernels/flex_attention.py index 512e6361ff..7e9c80cf13 100644 --- a/unsloth/kernels/flex_attention.py +++ b/unsloth/kernels/flex_attention.py @@ -47,7 +47,9 @@ def _flex_is_dgx_spark(): out = subprocess.run( ["nvidia-smi", "--query-gpu=name", "--format=csv,noheader"], - capture_output = True, text = True, timeout = 5, + capture_output = True, + text = True, + timeout = 5, ) names = (out.stdout or "").upper() return any(t in names for t in ("GB10", "JMJWOA", "N1X", "DGX SPARK", "GB110")) From b3135ec7dccbe143fa45cbc076d3e04b09a66dd2 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Wed, 10 Jun 2026 00:05:07 -0700 Subject: [PATCH 56/95] uninstall.ps1: scope WSL cleanup to evidenced fallback installs The in-distro cleanup probed a hardcoded candidate set ('', Ubuntu, Ubuntu-24.04, Ubuntu-22.04, Debian) on every Windows uninstall, wiping /root/.unsloth in any reachable distro even when the WoA fallback never ran -- on an x86 AMD box this deletes a ROCm-on-WSL Studio the AMD flow installed. Use the evidence the installer already records: clean only the wsl-distro.txt marker distro or UNSLOTH_WSL_DISTRO; keep the broad candidate probe solely for legacy marker-less installs, which can only exist on ARM64 hosts. Addresses the open Codex P1 on this path. Verified gating matrix: x86+no-marker -> no cleanup; marker/env -> that distro only; ARM64+no-marker -> legacy broad probe unchanged. Co-Authored-By: Claude Fable 5 --- scripts/uninstall.ps1 | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/scripts/uninstall.ps1 b/scripts/uninstall.ps1 index 8a5ccaf185..f92990f763 100644 --- a/scripts/uninstall.ps1 +++ b/scripts/uninstall.ps1 @@ -401,9 +401,19 @@ function Uninstall-UnslothStudio { # pattern from matching this command's own argv (whose literal text contains the # escaped form, not the resolved path) -- same idea as the [x]-bracket trick. $_clean = '_had=0; if [ -d /root/.unsloth ] || [ -L /root/.local/bin/unsloth ]; then _had=1; fi; rm -rf /root/.unsloth /root/llama-cuda /root/provision_llama_cuda.sh /root/llama_cuda_build.log 2>/dev/null; rm -f /root/.local/bin/unsloth 2>/dev/null; if [ $_had -eq 1 ]; then fuser -k 8888/tcp 2>/dev/null; fi; pkill -9 -f ''/root/\.unslot[h]/'' 2>/dev/null; true' - $_cands = @('', 'Ubuntu', 'Ubuntu-24.04', 'Ubuntu-22.04', 'Debian') - if ($_recordedDistro) { $_cands = @($_recordedDistro) + $_cands } - if ($env:UNSLOTH_WSL_DISTRO) { $_cands = @($env:UNSLOTH_WSL_DISTRO) + $_cands } + # Scope the in-distro cleanup to evidence the WoA fallback actually + # installed there: the recorded wsl-distro.txt marker (written by + # install.ps1) or an explicit UNSLOTH_WSL_DISTRO. Only legacy + # marker-less fallback installs need the broad candidate probe, and + # those can only exist on ARM64 hosts -- on x86 machines the probe + # would reach into distros this installer never touched (e.g. an + # AMD ROCm-on-WSL Studio under /root) and delete them. + $_cands = @() + if ($env:UNSLOTH_WSL_DISTRO) { $_cands += $env:UNSLOTH_WSL_DISTRO } + if ($_recordedDistro) { $_cands += $_recordedDistro } + if ((-not $_cands) -and ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64')) { + $_cands = @('', 'Ubuntu', 'Ubuntu-24.04', 'Ubuntu-22.04', 'Debian') + } $_done = @{} foreach ($d in $_cands) { if ($d) { & wsl.exe -d $d -- true *> $null } else { & wsl.exe -- true *> $null } From 42e69031b9c4ca1fd5196ec5c5f476e20ec5c10c Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Wed, 10 Jun 2026 00:32:41 -0700 Subject: [PATCH 57/95] Compress PR comments to essentials (comment-only; AST/token-verified) Comment-compression sweep over comments this PR added, mirroring the sweep already done on main. No non-comment token changed: .py verified by AST equality (docstrings normalized), .sh by non-comment-line equality + bash -n, .ps1 by token-stream equality minus comments. test_spark_oom_guard.py: 13 passed before and after. Files touched: - install.ps1 - install.sh - scripts/uninstall.ps1 - studio/backend/core/training/worker.py - studio/scripts/provision_llama_cuda.sh - studio/setup.sh - unsloth/kernels/flex_attention.py - unsloth/models/_utils.py Co-Authored-By: Claude Fable 5 --- install.ps1 | 180 ++++++++++--------------- install.sh | 22 ++- scripts/uninstall.ps1 | 48 +++---- studio/backend/core/training/worker.py | 53 +++----- studio/scripts/provision_llama_cuda.sh | 89 +++++------- studio/setup.sh | 72 ++++------ unsloth/kernels/flex_attention.py | 13 +- unsloth/models/_utils.py | 103 +++++--------- 8 files changed, 218 insertions(+), 362 deletions(-) diff --git a/install.ps1 b/install.ps1 index c1714b104a..57887a1ed4 100644 --- a/install.ps1 +++ b/install.ps1 @@ -48,9 +48,8 @@ function Install-UnslothStudio { } } - # Git ref for fetching repo-versioned install assets (provision_llama_cuda.sh, - # the .ico) from raw.githubusercontent.com. Defaults to 'main' (unchanged for - # existing users); set UNSLOTH_INSTALL_REF to a branch to test pre-merge. + # Ref for fetching install assets (provision_llama_cuda.sh, the .ico) from + # raw.githubusercontent.com; UNSLOTH_INSTALL_REF overrides 'main' for pre-merge testing. function Get-UnslothInstallRef { if ($env:UNSLOTH_INSTALL_REF -and $env:UNSLOTH_INSTALL_REF.Trim()) { return $env:UNSLOTH_INSTALL_REF.Trim() } return 'main' @@ -98,11 +97,9 @@ function Install-UnslothStudio { if ($TauriMode) { exit $Code } - # File-based runs (powershell -File / .\install.ps1) exit 0 on a plain return no - # matter what $LASTEXITCODE says, so automation would treat a fatal failure as a - # completed install -- `exit` carries the code there. Under `irm | iex` there is - # no $PSCommandPath and `exit` would kill the user's shell, so fall through and - # let the caller return (the message is the signal). + # -File runs exit 0 on a plain return regardless of $LASTEXITCODE, so `exit` + # must carry the code there; under `irm | iex` (no $PSCommandPath) `exit` + # would kill the user's shell, so fall through. if ($PSCommandPath) { exit $Code } @@ -1488,16 +1485,13 @@ shell.Run cmd, 0, False $TorchIndexUrl = Get-TorchIndexUrl # ===== Windows-on-ARM + NVIDIA GPU -> automatic WSL2 fallback (N1X "RTX Spark" / DGX Spark-class) ===== - # win_arm64 has no CUDA PyTorch/Triton wheel, so the GPU stack can't run natively. On ARM64 with an - # NVIDIA GPU and no installable native CUDA torch, route GPU setup through WSL2: enable/install WSL2, - # run the Linux installer there (full GPU), and add a Windows `unsloth` shim that forwards into WSL. - # Strictly gated: x86_64 and ARM64-without-NVIDIA are unaffected. Future-proof: if a win_arm64 CUDA - # torch wheel ships, the probe below passes and native install is kept automatically. - # Opt out with UNSLOTH_NO_WSL_FALLBACK=1; choose the distro with UNSLOTH_WSL_DISTRO. + # win_arm64 has no CUDA PyTorch/Triton wheel, so run the Linux installer inside WSL2 (full GPU) and + # add a Windows `unsloth` shim that forwards into it. x86_64 / ARM64-without-NVIDIA unaffected; if a + # win_arm64 CUDA torch wheel ever ships, the probe below keeps the native install automatically. + # Opt out: UNSLOTH_NO_WSL_FALLBACK=1; choose the distro with UNSLOTH_WSL_DISTRO. try { $_winArm64 = ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString() -ieq 'Arm64') } catch { $_winArm64 = $false } - # Under x64-emulated PowerShell on ARM, .NET OSArchitecture and $env:PROCESSOR_ARCHITECTURE report - # X64/AMD64; Win32_Processor.Architecture (12=ARM64) and machine-level PROCESSOR_ARCHITECTURE read - # the true arch. Additive: only turns $_winArm64 ON for genuine ARM64 hosts. + # x64-emulated PS on ARM reports X64/AMD64 via .NET and $env:; Win32_Processor.Architecture + # (12=ARM64) and machine-level PROCESSOR_ARCHITECTURE read the true arch. Only ever turns $_winArm64 ON. if (-not $_winArm64) { try { if ((@(Get-CimInstance Win32_Processor -ErrorAction Stop))[0].Architecture -eq 12) { $_winArm64 = $true } } catch {} } @@ -1509,13 +1503,11 @@ shell.Run cmd, 0, False } $_nativeCudaTorchOk = $false if ($_winArm64 -and $HasNvidiaSmi -and (-not $SkipTorch)) { - # Can a native CUDA torch wheel be resolved for this platform/index? Must use the SAME spec - # as the real install ("torch>=2.4,<2.11.0"): a bare `torch` probe can match an out-of-range - # wheel on the index, a false positive that skips WSL then fails the real pinned install. + # Probe with the SAME spec as the real install ("torch>=2.4,<2.11.0"): a bare `torch` probe + # can match an out-of-range wheel, skipping WSL only to fail the real pinned install. $prevEapProbe = $ErrorActionPreference; $ErrorActionPreference = "Continue" - # --reinstall forces resolution from the index instead of accepting an already-installed - # (e.g. CPU-only) torch in a migrated venv as "satisfied" -- otherwise the probe could pass - # without proving a native win_arm64 CUDA wheel exists, wrongly skipping the WSL path. + # --reinstall: an already-installed (e.g. CPU-only) torch must not satisfy the probe -- + # it has to prove a native win_arm64 CUDA wheel exists on the index. $global:LASTEXITCODE = -1 try { & uv pip install --python $VenvPython --dry-run --reinstall "torch>=2.4,<2.11.0" --index-url $TorchIndexUrl *> $null @@ -1527,24 +1519,21 @@ shell.Run cmd, 0, False step "wsl" "Windows on ARM + NVIDIA, native CUDA unavailable -- routing GPU setup through WSL2" substep "no win_arm64 CUDA PyTorch/Triton yet; WSL2 delivers full GPU (DGX Spark / RTX Spark path)." "Yellow" - # The bundled desktop app passes --tauri and launches its backend from a Windows venv - # (resolve_backend_binary), not from WSL -- so a WSL-only install would report complete yet - # fail to start. Until the Tauri launcher can drive a WSL backend, send desktop-app users to - # the CLI installer rather than leaving them with a broken-looking app. + # The Tauri desktop app launches its backend from a Windows venv (resolve_backend_binary), + # not WSL, so a WSL-only install would report complete yet fail to start -- send those + # users to the CLI installer. if ($TauriMode) { return (Exit-InstallFailure "Windows-on-ARM + NVIDIA GPU needs the WSL2 GPU install, which the desktop app can't launch yet. Install from PowerShell instead: irm https://unsloth.ai/install.ps1 | iex" 1) } $wslReady = $false if (Get-Command wsl.exe -ErrorAction SilentlyContinue) { - # Reset first: if wsl.exe throws/fails to start, $LASTEXITCODE keeps its prior value - # (a stale 0 from an earlier command would wrongly mark WSL ready). + # Reset first: a stale 0 in $LASTEXITCODE would wrongly mark WSL ready if wsl.exe fails to start. $global:LASTEXITCODE = -1 try { & wsl.exe --status *> $null; if ($LASTEXITCODE -eq 0) { $wslReady = $true } } catch {} } if (-not $wslReady) { - # Enabling WSL2 is a one-time operation that requires admin + a reboot. $isAdmin = $false try { $isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator) } catch {} step "wsl" "WSL2 isn't enabled yet -- one-time setup (needs admin + reboot)" "Yellow" @@ -1556,11 +1545,9 @@ shell.Run cmd, 0, False substep "in an ADMINISTRATOR PowerShell run: wsl --install" "Cyan" substep "reboot, then re-run: irm https://unsloth.ai/install.ps1 | iex" "Cyan" } - # WSL2 must be enabled + the machine rebooted before anything can install. Restore any - # rolled-aside previous venv and signal not-complete so -File callers don't treat this - # deferred state as a successful install. A plain return exits 0 for `-File` runs no - # matter what $LASTEXITCODE says, so exit explicitly there; under `irm | iex` - # ($PSCommandPath empty) exit would kill the user's shell, so return instead. + # Deferred until reboot: restore any rolled-aside previous venv and signal not-complete. + # A plain return exits 0 for -File runs regardless of $LASTEXITCODE, so `exit 1` there; + # under `irm | iex` ($PSCommandPath empty) exit would kill the user's shell, so return. Restore-StudioVenvRollback $global:LASTEXITCODE = 1 if ($PSCommandPath) { exit 1 } @@ -1568,11 +1555,9 @@ shell.Run cmd, 0, False } $distro = if ($env:UNSLOTH_WSL_DISTRO) { $env:UNSLOTH_WSL_DISTRO } else { "Ubuntu-24.04" } - # For cmd-context uses of the name (the generated .cmd shim, copy-paste hints): - # wsl.exe parses its raw command line itself, and a QUOTED space-free name - # ('wsl -d "Ubuntu-24.04"') fails with WSL_E_DISTRO_NOT_FOUND (verified live on - # 2.x) -- while a bare spaced name would split after -d. So quote ONLY when the - # name contains whitespace. + # For cmd-context uses (.cmd shim, copy-paste hints): wsl.exe rejects a QUOTED space-free + # name (WSL_E_DISTRO_NOT_FOUND, verified on 2.x) yet splits a bare spaced one after -d -- + # so quote ONLY when the name contains whitespace. $_distroArg = if ($distro -match '\s') { '"' + $distro + '"' } else { $distro } # Detect the distro by exit code (encoding-proof; wsl --list emits UTF-16 that PS mis-parses). $haveDistro = $false @@ -1582,12 +1567,10 @@ shell.Run cmd, 0, False substep "installing WSL distro '$distro' (first time only)..." "Cyan" try { & wsl.exe --install -d $distro --no-launch } catch {} } else { - # A PRE-EXISTING distro may be WSL1, which has no GPU passthrough: the existence - # probe passes but the full install would only fail at the final torch.cuda - # check. Detect WSL1 up-front from inside the distro (kernel string + libcuda -- - # encoding-proof, unlike parsing UTF-16 `wsl -l -v` output) and convert in place; - # `wsl --set-version` preserves the distro's files. Freshly installed distros - # are WSL2 (default version 2), so only the pre-existing case needs this. + # A PRE-EXISTING distro may be WSL1 (no GPU passthrough; would only fail at the final + # torch.cuda check). Detect from inside the distro (encoding-proof, unlike UTF-16 + # `wsl -l -v`) and convert in place -- `wsl --set-version` preserves the files. + # Fresh installs default to WSL2, so only the pre-existing case needs this. $_wsl2Probe = 'grep -qiE ''microsoft-standard|WSL2'' /proc/version 2>/dev/null || test -e /usr/lib/wsl/lib/libcuda.so' $_isWsl2 = $false $global:LASTEXITCODE = -1 @@ -1606,20 +1589,14 @@ shell.Run cmd, 0, False } } substep "installing Unsloth Studio inside WSL '$distro' with full GPU (this downloads PyTorch)..." "Cyan" - # For a non-main ref, fetch + export THAT ref so the WSL venv gets the branch's - # setup.sh + unsloth patches (otherwise install.sh pulls released PyPI unsloth and the - # branch never runs pre-merge). main is byte-identical to plain unsloth.ai/install.sh. + # Non-main ref: fetch + export THAT ref so the WSL venv gets the branch's setup.sh + + # patches (else install.sh pulls PyPI unsloth). main == plain unsloth.ai/install.sh. $_instRef = Get-UnslothInstallRef - # UNSLOTH_WSL_LLAMA_DEFERRED=1 tells the inner setup.sh that install.ps1 will build the CUDA - # llama.cpp in the background after install -- so setup.sh skips its own foreground build. - # (A user who runs install.sh DIRECTLY inside WSL won't set it, so setup.sh provisions CUDA - # itself instead of leaving them with no GGUF server.) - # apt stderr is kept visible (only stdout -> /dev/null) so network/DNS/repo failures inside - # WSL are diagnosable rather than silently swallowed. - # Forward the CUDA llama.cpp opt-out into WSL: without it the inner setup.sh would - # defer its build to a background builder this script then never starts (the same - # opt-out skips the dispatch below), leaving no llama-server and a misleading - # "building in background" footer. Forwarded, setup.sh keeps its own build instead. + # UNSLOTH_WSL_LLAMA_DEFERRED=1: setup.sh skips its foreground CUDA llama.cpp build because + # install.ps1 builds it in the background (a DIRECT install.sh run in WSL doesn't set it). + # apt stderr stays visible (only stdout -> /dev/null) so network/repo failures are diagnosable. + # Forward UNSLOTH_NO_LLAMA_CUDA into WSL: the same opt-out skips the dispatch below, so + # unforwarded, setup.sh would defer to a background builder that never starts (no llama-server). $_fwdEnv = '' if ($env:UNSLOTH_NO_LLAMA_CUDA -eq '1') { $_fwdEnv = 'export UNSLOTH_NO_LLAMA_CUDA=1; ' } if ($_instRef -eq 'main') { @@ -1649,10 +1626,9 @@ shell.Run cmd, 0, False & wsl.exe -d $distro --cd /root -u root -- /root/.unsloth/studio/unsloth_studio/bin/python -c "import torch,sys; sys.exit(0 if torch.cuda.is_available() else 3)" *> $null $torchOk = ($LASTEXITCODE -eq 0) } catch {} finally { $ErrorActionPreference = $prevEapChk } - # Self-heal Studio's web-server deps: if install.sh's late "studio deps" step was cut short, - # torch + unsloth land but fastapi/uvicorn/structlog/starlette are missing and `unsloth studio` - # dies with ModuleNotFoundError. Reinstall those without pinning huggingface-hub/transformers/ - # datasets, so the verified GPU torch stack stays intact. + # Self-heal web-server deps: a cut-short install.sh "studio deps" step leaves torch + unsloth + # but no fastapi/uvicorn/structlog/starlette (`unsloth studio` dies). Reinstall them without + # pinning huggingface-hub/transformers/datasets so the verified GPU torch stack stays intact. if ($torchOk) { $_studioPy = "/root/.unsloth/studio/unsloth_studio/bin/python" $_serverOk = $false @@ -1663,10 +1639,9 @@ shell.Run cmd, 0, False } catch {} finally { $ErrorActionPreference = $prevEapS } if (-not $_serverOk) { substep "Studio web-server deps incomplete (install.sh step cut short) -- installing them now..." "Cyan" - # Mirrors studio.txt minus the huggingface-hub pin (protected above); uv preferred, - # pip fallback. Bare names only -- a version spec's quotes get mangled through - # PowerShell -> wsl.exe -> bash -lc and `>=` becomes a redirection. uv resolves the - # latest of each, which satisfies the studio.txt minimums anyway. + # studio.txt minus the huggingface-hub pin; uv preferred, pip fallback. Bare names + # only: `>=` becomes a redirection through PowerShell -> wsl.exe -> bash -lc, and + # latest-of-each satisfies the studio.txt minimums anyway. $_deps = 'typer fastapi uvicorn matplotlib pandas nest_asyncio pyjwt easydict addict structlog diceware ddgs cryptography httpx fastmcp' $_repair = 'PY=/root/.unsloth/studio/unsloth_studio/bin/python; UV="$(command -v uv 2>/dev/null || echo /root/.local/bin/uv)"; if [ -x "$UV" ] || command -v uv >/dev/null 2>&1; then "$UV" pip install --python "$PY" ' + $_deps + '; else "$PY" -m pip install ' + $_deps + '; fi' $prevEapR = $ErrorActionPreference; $ErrorActionPreference = "Continue" @@ -1698,16 +1673,14 @@ shell.Run cmd, 0, False New-Item -ItemType Directory -Force -Path $shimDir *> $null $shimLines = @( '@echo off', - # $_distroArg: quoted only if the name has spaces -- wsl.exe rejects a - # quoted space-free name (WSL_E_DISTRO_NOT_FOUND) but splits a bare spaced one. + # $_distroArg: pre-quoted only when spaced (wsl.exe quoting rule above). "wsl.exe -d $_distroArg -u root -- /root/.unsloth/studio/unsloth_studio/bin/unsloth %*" ) Set-Content -LiteralPath (Join-Path $shimDir "unsloth.cmd") -Value $shimLines -Encoding ASCII - # Record the distro for the uninstaller: a custom UNSLOTH_WSL_DISTRO install - # must be cleanable without the env var being set again at uninstall time. + # Record the distro so the uninstaller can clean a custom UNSLOTH_WSL_DISTRO + # install without the env var being set again. try { Set-Content -LiteralPath (Join-Path (Split-Path $shimDir -Parent) "wsl-distro.txt") -Value $distro -Encoding ASCII } catch {} - # A fresh Windows profile may have no HKCU 'Path' value at all -> $userPath is null - # and $userPath.TrimEnd() would throw, losing the shim. Treat null as empty. + # A fresh profile may have no HKCU 'Path' at all; null would make TrimEnd() throw. $userPath = [Environment]::GetEnvironmentVariable("Path", "User") if (-not $userPath) { $userPath = "" } if (($userPath -split ';') -notcontains $shimDir) { @@ -1735,11 +1708,9 @@ shell.Run cmd, 0, False 'wsl.exe -d $distro --cd /root -u root -- bash -lic "unsloth studio -p 8888"' ) Set-Content -LiteralPath $launcher -Value $L -Encoding UTF8 - # Icon must live OUTSIDE %LOCALAPPDATA%: on Windows-on-ARM the shell's sandboxed - # icon-extraction broker can't read a standalone .ico under AppData\Local (it gets a - # redirected/virtualized view), so the shortcut renders BLANK -- while the identical - # file under the user profile renders fine (verified on N1X). Keep the shim/launcher - # in $appDir; only the icon needs the profile location. + # Icon must live OUTSIDE %LOCALAPPDATA%: on WoA the shell's sandboxed icon broker + # can't read a .ico under AppData\Local, so the shortcut renders BLANK -- the same + # file under the user profile renders fine (verified on N1X). Only the icon moves. $iconDir = Join-Path $env:USERPROFILE ".unsloth" New-Item -ItemType Directory -Force -Path $iconDir *> $null $icon = Join-Path $iconDir "unsloth.ico" @@ -1774,44 +1745,37 @@ shell.Run cmd, 0, False $sc.Save() } step "shortcuts" "created Desktop + Start Menu shortcuts (launch WSL Studio + open browser)" "Green" - # Nudge Explorer to pick up the new/changed shortcuts now: clear+rebuild the icon - # cache, then per-.lnk SHCNE_UPDATEITEM + a global SHCNE_ASSOCCHANGED. (The real - # blank-icon cause on WoA was the AppData\Local icon path, fixed above.) + # Nudge Explorer: clear+rebuild icon cache, per-.lnk SHCNE_UPDATEITEM, global + # SHCNE_ASSOCCHANGED. (The real WoA blank-icon cause was the icon path, fixed above.) try { & "$env:SystemRoot\System32\ie4uinit.exe" -ClearIconCache 2>$null } catch {} try { & "$env:SystemRoot\System32\ie4uinit.exe" -show 2>$null } catch {} try { if (-not ("UnslothShell.Notify" -as [type])) { Add-Type -Namespace UnslothShell -Name Notify -MemberDefinition '[System.Runtime.InteropServices.DllImport("shell32.dll", CharSet = System.Runtime.InteropServices.CharSet.Unicode)] public static extern void SHChangeNotify(int eventId, uint flags, string item1, System.IntPtr item2);' } - # Per-.lnk SHCNE_UPDATEITEM (0x00002000), SHCNF_PATHW (0x0005): force Explorer to - # re-read each shortcut's icon now (the global notify alone often misses existing .lnks). + # SHCNE_UPDATEITEM (0x00002000), SHCNF_PATHW (0x0005): the global notify alone often misses existing .lnks. foreach ($lnk in $lnks) { try { [UnslothShell.Notify]::SHChangeNotify(0x00002000, 0x0005, $lnk, [System.IntPtr]::Zero) } catch {} } - # SHCNE_ASSOCCHANGED (0x08000000), SHCNF_IDLIST (0): flush global icon associations - # (item args unused for this event). + # SHCNE_ASSOCCHANGED (0x08000000), SHCNF_IDLIST (0): flush global icon associations. [UnslothShell.Notify]::SHChangeNotify(0x08000000, 0, $null, [System.IntPtr]::Zero) } catch {} } catch { substep "(could not create shortcuts: $($_.Exception.Message))" "Yellow" } - # GGUF *inference* needs a CUDA-linked llama-server and no aarch64+CUDA prebuilt exists, so - # build one into ~/.unsloth/llama.cpp in the BACKGROUND: Studio + training are usable now and - # GGUF inference lights up minutes later. Best-effort; opt out with UNSLOTH_NO_LLAMA_CUDA=1. + # GGUF *inference* needs a CUDA llama-server (no aarch64+CUDA prebuilt exists), so build one + # into ~/.unsloth/llama.cpp in the BACKGROUND. Best-effort; opt out: UNSLOTH_NO_LLAMA_CUDA=1. if ($env:UNSLOTH_NO_LLAMA_CUDA -ne '1') { $prevEapL = $ErrorActionPreference; $ErrorActionPreference = "Continue" try { $_llamaUrl = "https://raw.githubusercontent.com/unslothai/unsloth/$(Get-UnslothInstallRef)/studio/scripts/provision_llama_cuda.sh" - # Step 1: fetch the provision script + write a small runner, shipped as base64 to - # dodge quoting layers. The runner (a) restores PATH so a non-login shell finds - # nvidia-smi (/usr/lib/wsl/lib) and apt -- else provision early-exits "no nvidia-smi"; - # (b) caps build jobs from UNSLOTH_LLAMA_BUILD_JOBS (Windows env vars don't cross into - # WSL); (c) runs provision with logging. A runner FILE lets the detached launcher below - # pass only space-free args, avoiding Start-Process mis-splitting `bash -lc `. + # Step 1: fetch the provision script + write a runner (base64 to dodge quoting layers). + # The runner restores PATH (non-login shells miss /usr/lib/wsl/lib nvidia-smi -> + # provision early-exits) and exports the env knobs below (Windows env vars don't cross + # into WSL). A runner FILE lets the detached launcher pass only space-free args, + # avoiding Start-Process mis-splitting `bash -lc `. $_pathLine = 'export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/lib/wsl/lib:$PATH"' + "`n" $_jobsLine = if ($env:UNSLOTH_LLAMA_BUILD_JOBS) { "export UNSLOTH_LLAMA_BUILD_JOBS=$($env:UNSLOTH_LLAMA_BUILD_JOBS)`n" } else { "" } - # Bridge llama.cpp pins into WSL: the provisioner honors UNSLOTH_LLAMA_TAG / - # UNSLOTH_LLAMA_PR, but Windows env vars don't cross into WSL on their own -- - # without these exports a user's pin would be silently ignored by the - # deferred background build. sh-single-quoted (tags/PRs are simple tokens). + # Bridge UNSLOTH_LLAMA_TAG / UNSLOTH_LLAMA_PR pins into WSL too, else the deferred + # build silently ignores them. sh-single-quoted (tags/PRs are simple tokens). $_tagLine = if ($env:UNSLOTH_LLAMA_TAG) { "export UNSLOTH_LLAMA_TAG='$($env:UNSLOTH_LLAMA_TAG)'`n" } else { "" } $_prLine = if ($env:UNSLOTH_LLAMA_PR) { "export UNSLOTH_LLAMA_PR='$($env:UNSLOTH_LLAMA_PR)'`n" } else { "" } $_runner = "#!/usr/bin/env bash`n" + $_pathLine + $_jobsLine + $_tagLine + $_prLine + "exec bash /root/.unsloth/provision_llama_cuda.sh > /root/.unsloth/llama_cuda_build.log 2>&1`n" @@ -1819,13 +1783,10 @@ shell.Run cmd, 0, False $_fetchCmd = 'mkdir -p /root/.unsloth; if curl -fsSL "' + $_llamaUrl + '" -o /root/.unsloth/provision_llama_cuda.sh && [ -s /root/.unsloth/provision_llama_cuda.sh ]; then chmod +x /root/.unsloth/provision_llama_cuda.sh; echo ' + $_runnerB64 + ' | base64 -d > /root/.unsloth/run_llama_build.sh; chmod +x /root/.unsloth/run_llama_build.sh; echo PROV_FETCHED; else echo PROV_NOSCRIPT; fi' $_fetchOut = & wsl.exe -d $distro --cd /root -u root -- bash -lc $_fetchCmd 2>$null if ("$_fetchOut" -match 'PROV_FETCHED') { - # Step 2: anchor the build to a detached Windows process. A WSL-side `nohup &` - # doesn't survive -- WSL stops the VM when the launching session exits, killing - # the build. A persistent Windows-side wsl.exe (Start-Process, no -Wait) keeps the - # VM up for the whole build while install.ps1 returns. PS 5.1's Start-Process - # joins -ArgumentList with spaces WITHOUT quoting, so a spaced distro name would - # split after -d -- pass $_distroArg (pre-quoted only when spaced; wsl.exe - # rejects a quoted space-free name). All other tokens are space-free. + # Step 2: a detached Windows-side wsl.exe keeps the WSL VM up for the whole build + # (a WSL-side `nohup &` dies: WSL stops the VM when the launching session exits). + # PS 5.1 Start-Process joins -ArgumentList WITHOUT quoting, so pass $_distroArg + # (pre-quoted only when spaced); all other tokens are space-free. Start-Process -WindowStyle Hidden -FilePath 'wsl.exe' -ArgumentList @('-d', $_distroArg, '--cd', '/root', '-u', 'root', '--', 'bash', '/root/.unsloth/run_llama_build.sh') | Out-Null step "llama.cpp" "building CUDA llama.cpp for GGUF inference in the background (a few min); log: ~/.unsloth/llama_cuda_build.log" "Green" } else { @@ -1838,16 +1799,15 @@ shell.Run cmd, 0, False substep "retry, or launch manually: wsl -d $_distroArg -u root -- bash -lic 'unsloth studio -p 8888'" "Cyan" } if ($torchOk) { - # WSL GPU install succeeded. On this path the Windows venv is vestigial (everything - # runs in WSL), so drop the rolled-aside previous-venv backup instead of orphaning it. + # Success: the Windows venv is vestigial here (everything runs in WSL), so drop the + # rolled-aside previous-venv backup instead of orphaning it. Complete-StudioVenvRollback substep "GPU training + GGUF export run inside WSL. (GGUF *inference* additionally needs a CUDA llama.cpp build.)" "Yellow" $global:LASTEXITCODE = 0 return } - # WSL GPU install failed (torch.cuda unavailable). Restore any rolled-aside previous venv so - # a reinstall-over-existing isn't left worse off, and report non-zero so -File callers don't - # treat a broken install as success (plain return exits 0 for -File; iex must not exit). + # Failed (torch.cuda unavailable): restore any rolled-aside previous venv and report + # non-zero (plain return exits 0 for -File; under iex `exit` would kill the caller's shell). Restore-StudioVenvRollback $global:LASTEXITCODE = 1 if ($PSCommandPath) { exit 1 } diff --git a/install.sh b/install.sh index 7595255a39..6b43bde722 100755 --- a/install.sh +++ b/install.sh @@ -2360,11 +2360,10 @@ elif [ -n "$TORCH_INDEX_URL" ]; then --no-deps --reinstall-package unsloth-zoo \ "unsloth-zoo @ git+https://github.com/unslothai/unsloth-zoo" elif [ -n "${UNSLOTH_INSTALL_REF:-}" ] && [ "${UNSLOTH_INSTALL_REF}" != "main" ] && [ "$PACKAGE_NAME" = "unsloth" ]; then - # Pre-merge branch testing: install unsloth from a git ref so its bundled - # setup.sh + Python patches are exercised (not yet on PyPI). install.ps1 sets - # UNSLOTH_INSTALL_REF; gated to the "unsloth" package and a non-"main" ref. - # unsloth-zoo is an optional extra (not a base dep) and SKIP_STUDIO_BASE skips - # the studio base.txt step, so name it explicitly or it never gets installed. + # Pre-merge testing: install unsloth from a git ref (install.ps1 sets + # UNSLOTH_INSTALL_REF) so the branch's setup.sh + patches run. unsloth-zoo + # is not a base dep and SKIP_STUDIO_BASE skips studio base.txt, so name it + # explicitly or it never gets installed. substep "installing unsloth from git ref '$UNSLOTH_INSTALL_REF'..." run_install_cmd "install unsloth (@$UNSLOTH_INSTALL_REF)" uv pip install --python "$_VENV_PY" \ --upgrade-package unsloth --upgrade-package unsloth-zoo \ @@ -2373,14 +2372,11 @@ elif [ -n "$TORCH_INDEX_URL" ]; then run_install_cmd "install unsloth" uv pip install --python "$_VENV_PY" \ --upgrade-package unsloth -- "$PACKAGE_NAME" fi - # aarch64 + NVIDIA (DGX Spark / GB10 / N1X, native or WSL): the base unsloth - # package does not depend on bitsandbytes and the cuXXX extras that normally - # add it are x86_64-oriented, so 4-bit QLoRA fails with ModuleNotFoundError - # out of the box. bitsandbytes ships working aarch64 manylinux wheels - # (verified on sm_121 Blackwell via PTX JIT), so add it best-effort -- a - # platform without a wheel just keeps 16-bit LoRA / full finetuning. - # Gated on SKIP_TORCH: a --no-torch/UNSLOTH_NO_TORCH (GGUF-only) install must - # not have bitsandbytes drag torch back into the venv via its dependencies. + # aarch64 + NVIDIA (DGX Spark / GB10 / N1X): base unsloth lacks bitsandbytes + # (the cuXXX extras are x86_64-oriented), so 4-bit QLoRA fails out of the box. + # aarch64 manylinux wheels work (verified on sm_121 via PTX JIT); best-effort, + # no wheel just keeps 16-bit LoRA / full finetuning. SKIP_TORCH gate: a + # --no-torch (GGUF-only) install must not let bitsandbytes drag torch back in. if [ "$SKIP_TORCH" = false ] \ && { [ "$(uname -m)" = "aarch64" ] || [ "$(uname -m)" = "arm64" ]; } \ && command -v nvidia-smi >/dev/null 2>&1 \ diff --git a/scripts/uninstall.ps1 b/scripts/uninstall.ps1 index f92990f763..bcec58caf1 100644 --- a/scripts/uninstall.ps1 +++ b/scripts/uninstall.ps1 @@ -344,9 +344,8 @@ function Uninstall-UnslothStudio { # %LOCALAPPDATA%\Unsloth (not "Unsloth Studio") with a PATH entry -- all missed by the cleanup above. _Step "Removing WSL-fallback artifacts (shim, launcher, PATH entry, WSL install)..." $unslothDir = if ($env:LOCALAPPDATA) { Join-Path $env:LOCALAPPDATA "Unsloth" } else { $null } - # The installer records its WSL distro in wsl-distro.txt so a custom - # UNSLOTH_WSL_DISTRO install is cleanable without the env var being set again - # at uninstall time. Read it BEFORE the directory is removed below. + # wsl-distro.txt records a custom UNSLOTH_WSL_DISTRO install so it is cleanable + # without the env var set; read it BEFORE the directory is removed below. $_recordedDistro = $null if ($unslothDir) { try { @@ -378,36 +377,24 @@ function Uninstall-UnslothStudio { } catch { } _RemovePath $unslothDir } - # The WoA shortcut icon lives under the user profile (the shell icon broker can't read a .ico - # under AppData\Local), so remove it here too. + # The WoA shortcut icon lives under the user profile (icon broker can't read AppData\Local). if ($env:USERPROFILE) { _RemovePath (Join-Path $env:USERPROFILE ".unsloth\unsloth.ico") } # Remove the Studio install inside each WSL distro (the real GPU install + any CUDA llama.cpp build). if (Get-Command wsl.exe -ErrorAction SilentlyContinue) { try { - # `wsl --list` emits UTF-16 PowerShell mis-parses (empty list -> cleanup skipped), so probe a - # candidate set by exit code instead ('' = default distro), which is encoding-proof. - # rm runs FIRST (guaranteed) since the kills could SIGKILL this shell. Also rm the dangling - # /root/.local/bin/unsloth symlink (its target under /root/.unsloth is gone but the link still - # resolves on PATH). Scope STRICTLY to /root: the WoA fallback installs there (wsl -u root), so - # touching /home/*/.unsloth would erase an unrelated WSL user's own Unsloth/cache that this - # installer never created. - # The port-8888 kill is gated on an Unsloth install actually existing in the - # distro (checked BEFORE the rm deletes the marker): a probed distro with an - # unrelated listener on 8888 (Jupyter etc.) must not lose it. The process kill is - # scoped to argv referencing /root/.unsloth/ -- the fallback's install dir, which - # its Studio server, llama-server, and build runner all reference -- instead of - # bare name patterns that would also kill a user's own unrelated llama-server or - # a /home Studio in that distro. The backslash in '/root/\.unslot[h]/' keeps the - # pattern from matching this command's own argv (whose literal text contains the - # escaped form, not the resolved path) -- same idea as the [x]-bracket trick. + # `wsl --list` emits UTF-16 PS mis-parses, so probe candidates by exit code instead + # ('' = default distro). rm runs FIRST (the kills could SIGKILL this shell) and also + # drops the dangling /root/.local/bin/unsloth symlink. Scope STRICTLY to /root (where + # the fallback installs): /home/*/.unsloth may be an unrelated user's. The port-8888 + # kill is gated on an Unsloth install existing (checked BEFORE rm deletes the marker) + # so an unrelated 8888 listener survives; pkill matches argv containing /root/.unsloth/ + # rather than bare names that would kill a user's own llama-server, and the backslash + # + [h]-bracket in '/root/\.unslot[h]/' keep it from matching this command's own argv. $_clean = '_had=0; if [ -d /root/.unsloth ] || [ -L /root/.local/bin/unsloth ]; then _had=1; fi; rm -rf /root/.unsloth /root/llama-cuda /root/provision_llama_cuda.sh /root/llama_cuda_build.log 2>/dev/null; rm -f /root/.local/bin/unsloth 2>/dev/null; if [ $_had -eq 1 ]; then fuser -k 8888/tcp 2>/dev/null; fi; pkill -9 -f ''/root/\.unslot[h]/'' 2>/dev/null; true' - # Scope the in-distro cleanup to evidence the WoA fallback actually - # installed there: the recorded wsl-distro.txt marker (written by - # install.ps1) or an explicit UNSLOTH_WSL_DISTRO. Only legacy - # marker-less fallback installs need the broad candidate probe, and - # those can only exist on ARM64 hosts -- on x86 machines the probe - # would reach into distros this installer never touched (e.g. an - # AMD ROCm-on-WSL Studio under /root) and delete them. + # Clean only distros with evidence of a fallback install: the wsl-distro.txt marker + # or an explicit UNSLOTH_WSL_DISTRO. The broad candidate probe is only for legacy + # marker-less installs, which exist only on ARM64 hosts -- on x86 it would delete + # distros this installer never touched (e.g. a ROCm-on-WSL Studio under /root). $_cands = @() if ($env:UNSLOTH_WSL_DISTRO) { $_cands += $env:UNSLOTH_WSL_DISTRO } if ($_recordedDistro) { $_cands += $_recordedDistro } @@ -440,9 +427,8 @@ function Uninstall-UnslothStudio { Write-Host " `$env:UNSLOTH_STUDIO_HOME = 'C:\your\path'; irm https://raw.githubusercontent.com/unslothai/unsloth/main/scripts/uninstall.ps1 | iex" } - # The distro-probe loop leaves $LASTEXITCODE from its last probe, which fails by design for - # absent distros -- reset it so a successful uninstall exits 0. Set the var rather than `exit 0` - # so `irm ... | iex` doesn't terminate the caller's shell. + # The distro probes leave a failing $LASTEXITCODE; reset it so success exits 0. Set the + # var rather than `exit 0` so `irm ... | iex` doesn't terminate the caller's shell. $global:LASTEXITCODE = 0 } diff --git a/studio/backend/core/training/worker.py b/studio/backend/core/training/worker.py index 317b2301f8..fc2b5a7343 100644 --- a/studio/backend/core/training/worker.py +++ b/studio/backend/core/training/worker.py @@ -706,23 +706,17 @@ def _rocm_classify_unified_memory(props: Any) -> tuple[str, bool]: def _nvidia_classify_spark_unified_memory(props: Any) -> tuple[str, bool]: """Classify an NVIDIA device as Spark-class unified-memory or discrete. - Returns ``(marker, is_unified)``: - - ``marker``: the signal that matched (``"is_integrated"`` or the matching - device-name token), else ``""``. - - ``is_unified``: ``True`` for Spark-class parts that share one memory pool - with the OS (DGX Spark / GB10, N1X "RTX Spark", Grace-Blackwell desksides) - — these need the same lower ``set_per_process_memory_fraction`` cap as the - ROCm APUs: exhausting the shared pool can stall the whole box instead of - raising a catchable OutOfMemoryError. + Returns ``(marker, is_unified)``; marker is ``"is_integrated"`` or the matched + device-name token, else ``""``. Spark-class parts (DGX Spark / GB10, N1X "RTX + Spark") share one memory pool with the OS, so like the ROCm APUs they need a + ``set_per_process_memory_fraction`` cap -- exhausting the pool can stall the + box instead of raising a catchable OutOfMemoryError. - Classification priority: - 1. ``is_integrated`` device property (authoritative on native Linux). - 2. Device-name token match — WSL2's GPU paravirtualization masks - ``is_integrated`` to 0 and renames the device (the N1X reports - ``JMJWOA-Generic-GPU`` with ``is_integrated == 0``, verified on - hardware), so the property alone misses Spark-under-WSL. Tokens mirror - ``_DGX_SPARK_DEVICE_TOKENS`` in ``unsloth/models/_utils.py`` (duplicated - because this guard runs before any ML import). + ``is_integrated`` is authoritative on native Linux, but WSL2 paravirtualization + masks it to 0 and renames the device (the N1X reports ``JMJWOA-Generic-GPU``, + verified on hardware) -- hence the name-token fallback. Tokens mirror + ``_DGX_SPARK_DEVICE_TOKENS`` in ``unsloth/models/_utils.py`` (duplicated + because this guard runs before any ML import). """ if getattr(props, "is_integrated", 0): return "is_integrated", True @@ -2225,25 +2219,18 @@ def run_training_process(*, event_queue: Any, stop_queue: Any, config: dict) -> logger.debug("Could not set GPU memory fraction: %s", _oom_guard_err) # ── 1h. NVIDIA Spark-class unified-memory OOM guard ── - # Same failure mode as the ROCm APU guard above, NVIDIA flavor: Spark-class - # parts (DGX Spark / GB10, N1X "RTX Spark") share one memory pool with the - # OS, so over-allocation can stall the whole box instead of raising a - # catchable OutOfMemoryError. Cap the allocator at 0.80 like Strix Halo — - # the pool is shared with the host OS and page cache, so 20% headroom stays - # with the system. UNSLOTH_SPARK_MEM_FRACTION overrides the cap; any value - # outside (0, 1] disables the guard. Discrete NVIDIA GPUs are untouched - # (they already raise a graceful OOM). The generic OOM handler in the - # training loop surfaces the resulting OutOfMemoryError with remediation. + # NVIDIA flavor of the ROCm APU guard above: Spark-class parts share one + # memory pool with the OS, so over-allocation can stall the box instead of + # raising a catchable OutOfMemoryError. Cap at 0.80 like Strix Halo (20% + # headroom stays with the OS/page cache). UNSLOTH_SPARK_MEM_FRACTION + # overrides; outside (0, 1] disables. Discrete NVIDIA GPUs untouched. else: try: - # The Spark allocator config must be decided BEFORE this guard's first - # CUDA touch: get_device_properties below initializes the CUDA allocator, - # after which PYTORCH_CUDA_ALLOC_CONF changes are ignored -- and the later - # `import unsloth` (patch_dgx_spark_memory_config) would be too late for - # THIS worker process even though it is in time for a plain - # `import unsloth`. CUDA-free sniff via nvidia-smi device names (mirrors - # _is_dgx_spark_no_cuda_init), with the same append-don't-override and - # UNSLOTH_NO_EXPANDABLE_SEGMENTS opt-out semantics as the library patch. + # Set PYTORCH_CUDA_ALLOC_CONF before get_device_properties below inits + # the CUDA allocator -- the later `import unsloth` patch is too late for + # THIS worker process. CUDA-free nvidia-smi sniff (mirrors + # _is_dgx_spark_no_cuda_init), same append-don't-override and + # UNSLOTH_NO_EXPANDABLE_SEGMENTS opt-out as the library patch. try: import platform as _plat diff --git a/studio/scripts/provision_llama_cuda.sh b/studio/scripts/provision_llama_cuda.sh index d89e192dc4..8c593e7950 100644 --- a/studio/scripts/provision_llama_cuda.sh +++ b/studio/scripts/provision_llama_cuda.sh @@ -1,14 +1,11 @@ #!/usr/bin/env bash -# Build a CUDA llama.cpp for Unsloth Studio GGUF *inference* into -# ~/.unsloth/llama.cpp (resolver checks /build/bin/llama-server). -# Idempotent, best-effort: safe to re-run, always exits 0. -# -# Needed because no aarch64+CUDA llama.cpp prebuilt exists for NVIDIA ARM hosts -# (DGX Spark / GB10, N1X "RTX" laptops). Handles the platform gotchas: -# * nvcc rejects gcc-15 -> force gcc-14 / g++-14 as the host compiler -# * glibc >= 2.41 vs CUDA < 13.3 -> install CUDA 13.3 (rsqrt header clash) -# * sm_121 (Blackwell) GPUs -> derive arch from the GPU's compute_cap -# +# Build CUDA llama.cpp for Studio GGUF *inference* into ~/.unsloth/llama.cpp +# (resolver checks /build/bin/llama-server). Idempotent, best-effort, always +# exits 0. Exists because no aarch64+CUDA prebuilt covers NVIDIA ARM hosts +# (DGX Spark / GB10, N1X "RTX" laptops). Platform gotchas handled: +# * nvcc rejects gcc-15 -> force gcc-14 / g++-14 host compiler +# * glibc >= 2.41 vs CUDA < 13.3 -> install CUDA 13.3 (rsqrt header clash) +# * sm_121 (Blackwell) -> derive arch from the GPU's compute_cap # Opt out with UNSLOTH_NO_LLAMA_CUDA=1 (handled by the caller). set -uo pipefail @@ -16,10 +13,9 @@ LLAMA_DIR="${UNSLOTH_LLAMA_CPP_PATH:-$HOME/.unsloth/llama.cpp}" SERVER="$LLAMA_DIR/build/bin/llama-server" log() { printf ' - %s\n' "$*"; } -# CUDA-capable in two layouts: old monolithic (libggml-cuda is a direct ldd dep) -# or current split build (CUDA is a dlopen-ed backend libggml-cuda.so* beside the -# binary, not shown by ldd). ldd alone false-negatives on current llama.cpp; a -# CPU-only build has no libggml-cuda.so, so its presence is the reliable signal. +# CUDA shows up two ways: old monolithic (libggml-cuda in ldd) or current split +# build (dlopen-ed libggml-cuda.so* beside the binary, missed by ldd). CPU-only +# builds ship no libggml-cuda.so, so its presence is the reliable signal. is_cuda_server() { [ -x "$1" ] || return 1 ldd "$1" 2>/dev/null | grep -qi 'libggml-cuda' && return 0 @@ -42,16 +38,13 @@ fi SUDO=""; [ "$(id -u)" -ne 0 ] && SUDO="sudo" HAVE_APT=0; command -v apt-get >/dev/null 2>&1 && HAVE_APT=1 -# 2. Base toolchain (must succeed) THEN gcc-14 (best-effort, separate transaction). -# gcc-14 is preferred because nvcc rejects gcc-15, but it isn't in the default apt -# sources on Ubuntu 22.04 / Debian 12 -- installing it in the SAME transaction as -# cmake/git/curl would make apt abort the whole transaction there, leaving the box -# without the basic build tools needed to clone/configure llama.cpp. +# 2. Base toolchain first, then gcc-14 (nvcc rejects gcc-15) in a SEPARATE apt +# transaction: gcc-14 is absent from default Ubuntu 22.04 / Debian 12 sources, +# which would abort a combined transaction and lose the base build tools too. if [ "$HAVE_APT" -eq 1 ]; then $SUDO apt-get update -y >/dev/null 2>&1 || true - # libcurl4-openssl-dev: _cmake_configure forces -DLLAMA_CURL=ON, and on the WSL - # deferred path this script is the only build path -- setup.sh's GGUF dep install - # (which covers libcurl) was skipped, so configure would fail without the headers. + # libcurl4-openssl-dev: -DLLAMA_CURL=ON needs it, and on the WSL deferred path + # setup.sh's GGUF dep install (which covers libcurl) was skipped. $SUDO apt-get install -y --no-install-recommends \ build-essential cmake git curl ca-certificates libcurl4-openssl-dev >/dev/null 2>&1 || true $SUDO apt-get install -y --no-install-recommends gcc-14 g++-14 >/dev/null 2>&1 || true @@ -95,9 +88,8 @@ if [ -z "$NVCC" ]; then fi CUDA_HOME="$(dirname "$(dirname "$NVCC")")" -# CUDA toolkit + Linux dirs FIRST so the build uses Linux cmake/gcc/git, not a -# Windows tool leaked into PATH via WSL interop (/mnt/c, also has spaces). Keep -# the original PATH after so nvidia-smi etc. still resolve. +# CUDA + Linux dirs FIRST so the build uses Linux cmake/gcc/git, not Windows tools +# leaked in via WSL interop (/mnt/c); original PATH kept so nvidia-smi resolves. export PATH="$CUDA_HOME/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$PATH" export CUDAToolkit_ROOT="$CUDA_HOME" @@ -110,14 +102,12 @@ export CC="$HCC" CXX="$HCXX" CUDAHOSTCXX="$HCXX" CC_CAP="$(nvidia-smi --query-gpu=compute_cap --format=csv,noheader 2>/dev/null | head -1 | tr -d ' .')" if [ -n "$CC_CAP" ]; then CUDA_ARCH="$CC_CAP"; else CUDA_ARCH="native"; fi -# 6. Clone + build into ~/.unsloth/llama.cpp. Honor a pinned llama.cpp ref -# (UNSLOTH_LLAMA_TAG, the same var setup.sh uses) so a provisioner-built tree matches -# the user's request instead of always tracking ggml-org main. +# 6. Clone + build into ~/.unsloth/llama.cpp, honoring a UNSLOTH_LLAMA_TAG pin +# (same var setup.sh uses) instead of always tracking ggml-org main. mkdir -p "$(dirname "$LLAMA_DIR")" _LLAMA_REF="${UNSLOTH_LLAMA_TAG:-}" -# Preserve any existing (e.g. CPU-only) llama.cpp so a failed clone OR a failed CUDA -# build doesn't leave the user with NO server: the backup is restored on any failure -# exit and only dropped once a server from the fresh build is confirmed. +# Back up any existing (e.g. CPU-only) llama.cpp: restored on any failure exit, +# dropped only once the fresh build yields a server -- never leave NO server. _LLAMA_BAK="" _restore_prev() { if [ -n "$_LLAMA_BAK" ] && [ -e "$_LLAMA_BAK" ]; then @@ -143,9 +133,8 @@ if [ ! -d "$LLAMA_DIR/.git" ]; then _restore_prev exit 0 fi - # Honor a llama.cpp PR pin (UNSLOTH_LLAMA_PR, the same var setup.sh supports) - # so a provisioned tree matches the user's request instead of silently building - # the default branch. Best-effort: a failed fetch keeps the default branch. + # Honor a UNSLOTH_LLAMA_PR pin (same var setup.sh supports); best-effort -- + # a failed fetch keeps the default branch. case "${UNSLOTH_LLAMA_PR:-}" in ''|*[!0-9]*) ;; *) @@ -168,22 +157,17 @@ _cmake_configure() { -DCMAKE_CUDA_HOST_COMPILER="$HCXX" \ -DLLAMA_CURL=ON >/dev/null 2>&1 } -# A pre-existing build/ may carry an incompatible CMake cache (e.g. the installer -# relocates a versioned build dir here, leaving stale absolute paths + GGML_CUDA=OFF), -# making CUDA configure fail. Try to reuse build/ first (fast incremental resume); -# only wipe and configure clean if that fails. +# A pre-existing build/ may carry a stale CMake cache (relocated dir: bad absolute +# paths + GGML_CUDA=OFF). Reuse it first (fast incremental); wipe only on failure. if ! _cmake_configure; then log "stale/incompatible CMake cache detected; wiping build dir for a clean CUDA configure" rm -rf build _cmake_configure || { log "cmake configure failed"; cd /; _restore_prev; exit 0; } fi -# Build the full target set unsloth-zoo's GGUF exporter also needs (llama-mtmd-cli, -# llama-gguf-split) so one build serves both Studio inference and save_pretrained_gguf. -# Parallelism default = ~half the cores: much faster than a tiny -j4, but leaves -# thermal/power headroom -- a full -j(nproc) CUDA build trips shutdowns on -# thermally constrained NVIDIA-ARM laptops (e.g. N1X "RTX Spark"). Also cap by RAM -# (~1.5 GB per nvcc job) to avoid OOM. Tune with UNSLOTH_LLAMA_BUILD_JOBS=N (raise -# on a well-cooled box, lower if it still trips). Incremental: a re-run resumes. +# Also builds the targets unsloth-zoo's GGUF exporter needs (llama-mtmd-cli, +# llama-gguf-split). Jobs default to ~half the cores -- full -j(nproc) CUDA builds +# trip thermal shutdowns on NVIDIA-ARM laptops (N1X "RTX Spark") -- and are +# RAM-capped (~1.5 GB/nvcc job). Tune: UNSLOTH_LLAMA_BUILD_JOBS=N; re-runs resume. _ncpu="$(nproc 2>/dev/null || echo 4)" # Honor a valid positive-int override; ignore junk/0 (cmake reads -j0 as "all cores"). if [ -n "${UNSLOTH_LLAMA_BUILD_JOBS:-}" ] && [ "${UNSLOTH_LLAMA_BUILD_JOBS}" -ge 1 ] 2>/dev/null; then @@ -198,15 +182,13 @@ else if [ "$_memjobs" -lt "$JOBS" ]; then JOBS="$_memjobs"; fi fi log "building with -j${JOBS} (cores=${_ncpu})" -# Lowest CPU + idle I/O priority so this background build keeps full speed when the -# box is idle but instantly yields to a foreground `unsloth studio` / training run. +# nice/ionice: full speed when idle, yields to foreground Studio/training runs. _NICE="" command -v nice >/dev/null 2>&1 && _NICE="nice -n 19" command -v ionice >/dev/null 2>&1 && _NICE="$_NICE ionice -c 3" _cmake_build() { - # Only llama-server is REQUIRED (mirrors setup.sh's source path): an older - # UNSLOTH_LLAMA_TAG pin may predate newer helper targets (llama-mtmd-cli, - # llama-gguf-split), and those missing must not fail the whole provision. + # Only llama-server is REQUIRED: an old UNSLOTH_LLAMA_TAG pin may predate the + # helper targets, and those missing must not fail the whole provision. $_NICE cmake --build build -j"$JOBS" --target llama-server >/dev/null 2>&1 } _cmake_build_extras() { @@ -216,10 +198,9 @@ _cmake_build_extras() { done } if ! _cmake_build; then - # An interrupted build (e.g. a thermal/power shutdown mid-compile, which this - # machine class is prone to) can leave a partially-linked libggml-cuda.so that - # then fails to link llama-server on resume (undefined ggml_cuda_op_* refs). - # Wipe build/ and rebuild clean once before giving up. + # An interrupted build (thermal/power shutdown -- this machine class is prone) + # can leave a half-linked libggml-cuda.so that breaks the resume link + # (undefined ggml_cuda_op_* refs); wipe and rebuild clean once. log "build failed (likely interrupted/partial); wiping build dir and rebuilding clean" rm -rf build _cmake_configure || { log "cmake configure failed"; cd /; _restore_prev; exit 0; } diff --git a/studio/setup.sh b/studio/setup.sh index a6e92c7b6f..fcec6f72fa 100755 --- a/studio/setup.sh +++ b/studio/setup.sh @@ -756,10 +756,9 @@ LLAMA_CPP_DIR="$UNSLOTH_HOME/llama.cpp" LLAMA_SERVER_BIN="$LLAMA_CPP_DIR/build/bin/llama-server" _NEED_LLAMA_SOURCE_BUILD=false _LLAMA_CPP_DEGRADED=false -# Distinct from _LLAMA_CPP_DEGRADED: on WSL2 aarch64+NVIDIA with no nvcc, the CPU -# build is skipped because install.ps1 builds the real CUDA server in the background. -# A temporarily-absent server here is success, not failure, so it must not trip the -# arm64 CPU-prebuilt last-resort or the exit 1. +# Deferred != degraded: on WSL2 aarch64+NVIDIA install.ps1 builds the real CUDA +# server in the background, so a temporarily-absent server is success and must not +# trip the arm64 CPU-prebuilt last-resort or the exit 1. _LLAMA_CPP_DEFERRED=false _LLAMA_FORCE_COMPILE="${UNSLOTH_LLAMA_FORCE_COMPILE:-0}" _REQUESTED_LLAMA_TAG="${UNSLOTH_LLAMA_TAG:-${_DEFAULT_LLAMA_TAG}}" @@ -933,12 +932,10 @@ if [ "$_NEED_LLAMA_SOURCE_BUILD" = true ] && \ fi # ── WSL2 aarch64 + NVIDIA, no nvcc yet: defer to the background CUDA build ── -# On Windows-on-ARM + NVIDIA, install.ps1 builds the real CUDA llama-server in the -# background after this install. Without nvcc yet the section-9 build can only make a -# slow CPU server that the background build throws away, so skip it on this exact path. -# Gated: WSL + aarch64/arm64 + NVIDIA GPU + nvcc missing + CUDA not opted out -# (UNSLOTH_NO_LLAMA_CUDA!=1) + no forced compile / PR pin. If nvcc is present we fall -# through to section 9; if opted out we keep the CPU build as the only server. +# install.ps1 builds the real CUDA llama-server in the background after install; +# without nvcc, section 9 could only make a slow CPU server that build discards. +# With nvcc we fall through to section 9; opted out (UNSLOTH_NO_LLAMA_CUDA=1) the +# CPU build is kept as the only server. if [ "$_NEED_LLAMA_SOURCE_BUILD" = true ] \ && [ "$_LLAMA_FORCE_COMPILE" != "1" ] \ && [ -z "$_LLAMA_PR" ] \ @@ -952,8 +949,7 @@ if [ "$_NEED_LLAMA_SOURCE_BUILD" = true ] \ step "llama.cpp" "GGUF engine: CUDA build running in background (WSL aarch64 + NVIDIA)" "$C_WARN" substep "skipping slow CPU build; the background CUDA llama.cpp will provide the server" substep "(opt out / keep CPU build with UNSLOTH_NO_LLAMA_CUDA=1)" - # Use DEFERRED, not DEGRADED: DEGRADED would trigger the CPU-prebuilt last - # resort + exit 1, but install.ps1's background build is the intended builder. + # DEFERRED, not DEGRADED: DEGRADED would trigger the CPU-prebuilt last resort + exit 1. _NEED_LLAMA_SOURCE_BUILD=false _LLAMA_CPP_DEFERRED=true fi @@ -1195,10 +1191,9 @@ else else CMAKE_ARGS="$CMAKE_ARGS -DGGML_CUDA=ON" - # glibc >= 2.41 vs CUDA < 13.3: rsqrt/rsqrtf header clash makes every .cu - # fail "exception specification is incompatible" and the GPU build drops to - # CPU. No workaround but CUDA >= 13.3. Diagnostic only: never changes flags - # or aborts, so it cannot regress any platform. + # glibc >= 2.41 + CUDA < 13.3: rsqrt/rsqrtf header clash fails every .cu + # ("exception specification is incompatible") -> CPU fallback; only fix is + # CUDA >= 13.3. Diagnostic only -- never changes flags or aborts. _GLIBC_VER="$(getconf GNU_LIBC_VERSION 2>/dev/null | awk '{print $2}')" || _GLIBC_VER="" if [ -n "$_GLIBC_VER" ]; then _GLIBC_MAJ="${_GLIBC_VER%%.*}"; _GLIBC_MIN="${_GLIBC_VER#*.}"; _GLIBC_MIN="${_GLIBC_MIN%%.*}" @@ -1424,19 +1419,14 @@ fi # end _SKIP_GGUF_BUILD check # ── aarch64 + NVIDIA (DGX Spark / GB10 / N1X "RTX Spark"): provision a CUDA # llama.cpp when the source build above could not (no CUDA toolkit found) ── -# No aarch64+CUDA prebuilt exists, and the source build above only emits a CUDA -# server when nvcc is already present (a fresh Spark ships only driver + nvidia-smi, -# so it falls back to CPU). The Windows/WSL path closes this gap via -# provision_llama_cuda.sh; mirror it here so native-Linux Spark gets the same. -# -# Gated + additive: only on Linux aarch64/arm64 + NVIDIA GPU with no CUDA server -# yet (opt out via UNSLOTH_NO_LLAMA_CUDA=1). x86_64, ROCm, Metal, CPU-only ARM, and -# ARM hosts that already built CUDA are unaffected. Best-effort: provision always -# exits 0; on failure the prior CPU/degraded state stands for the fallback below. -# CUDA-capable in two layouts: old monolithic (libggml-cuda is a direct ldd dep) or -# split build (dlopen-ed backend libggml-cuda.so* beside the binary, not in ldd). ldd -# alone false-negatives; a CPU-only build has no libggml-cuda.so, so its presence is -# the reliable signal. +# No aarch64+CUDA prebuilt exists and a fresh Spark ships only driver + nvidia-smi, +# so the build above fell back to CPU; mirror the Windows/WSL fix +# (provision_llama_cuda.sh) for native Linux. Gated to Linux aarch64 + NVIDIA with +# no CUDA server yet (opt out: UNSLOTH_NO_LLAMA_CUDA=1); best-effort -- provision +# always exits 0 and on failure the prior CPU/degraded state stands. +# CUDA detection covers both layouts: old monolithic (libggml-cuda in ldd) and +# split build (dlopen-ed libggml-cuda.so* beside the binary, missed by ldd -- +# CPU-only builds ship no libggml-cuda.so, so its presence is the signal). _have_cuda_llama_server() { [ -x "$LLAMA_SERVER_BIN" ] || return 1 ldd "$LLAMA_SERVER_BIN" 2>/dev/null | grep -qi 'libggml-cuda' && return 0 @@ -1450,14 +1440,11 @@ if [ "$_HOST_SYSTEM" = "Linux" ] \ && command -v nvidia-smi >/dev/null 2>&1 \ && nvidia-smi -L 2>/dev/null | awk '/^GPU[[:space:]]+[0-9]+:/{found=1} END{exit !found}' \ && ! _have_cuda_llama_server; then - # Native Linux (DGX Spark / GB10) runs this. Under WSL it runs ONLY for a DIRECT - # `install.sh` invocation: when install.ps1 drives the WSL install it exports - # UNSLOTH_WSL_LLAMA_DEFERRED=1 and builds the CUDA llama.cpp in the background after - # setup, so this foreground build is skipped to avoid duplicating it. A user who runs - # install.sh themselves inside WSL has no background builder, so we provision here - # rather than leave them with no GGUF server. + # Under WSL this runs ONLY for a DIRECT `install.sh` run: install.ps1 exports + # UNSLOTH_WSL_LLAMA_DEFERRED=1 and builds CUDA llama.cpp in the background, but + # a direct run has no background builder, so provision here. # Resolve provision_llama_cuda.sh: copy beside setup.sh, then local-dev repo, - # else fetch from GitHub so `curl | sh` works on an older wheel without it. + # else fetch from GitHub (so `curl | sh` works on an older wheel without it). _PROV_SH="" if [ -f "$SCRIPT_DIR/scripts/provision_llama_cuda.sh" ]; then _PROV_SH="$SCRIPT_DIR/scripts/provision_llama_cuda.sh" @@ -1473,15 +1460,13 @@ if [ "$_HOST_SYSTEM" = "Linux" ] \ if [ -n "$_PROV_SH" ]; then step "llama.cpp" "aarch64 + NVIDIA: provisioning CUDA toolkit + building CUDA llama.cpp for GGUF inference..." "$C_WARN" substep "(opt out with UNSLOTH_NO_LLAMA_CUDA=1; lower load with UNSLOTH_LLAMA_BUILD_JOBS=N)" - # Builds into $LLAMA_CPP_DIR (via UNSLOTH_LLAMA_CPP_PATH so a custom - # STUDIO_HOME lands where setup.sh validates); always exits 0. + # UNSLOTH_LLAMA_CPP_PATH routes a custom STUDIO_HOME into $LLAMA_CPP_DIR; always exits 0. UNSLOTH_LLAMA_CPP_PATH="$LLAMA_CPP_DIR" bash "$_PROV_SH" || true if _have_cuda_llama_server; then step "llama.cpp" "CUDA llama-server ready (aarch64 + NVIDIA)" _LLAMA_CPP_DEGRADED=false - # The provisioner just created $LLAMA_CPP_DIR. In custom-STUDIO_HOME mode the next - # setup/update runs _assert_studio_owned_or_absent on it, so claim ownership now or - # that assert would abort on a directory this installer made. + # Claim ownership of the fresh $LLAMA_CPP_DIR or the next custom-STUDIO_HOME + # run's _assert_studio_owned_or_absent would abort on it. if [ "$_STUDIO_HOME_IS_CUSTOM" = true ]; then : > "$LLAMA_CPP_DIR/$_STUDIO_OWNED_MARKER" 2>/dev/null || true fi @@ -1489,9 +1474,8 @@ if [ "$_HOST_SYSTEM" = "Linux" ] \ substep "CUDA build unavailable; keeping existing (CPU) llama-server" "$C_WARN" else substep "CUDA build unavailable and no llama-server present; see $LLAMA_CPP_DIR build output" "$C_WARN" - # No server at all (e.g. the provisioner replaced a previous build and then - # failed): mark degraded so the arm64 CPU-prebuilt last resort below and the - # installer failure exit fire instead of reporting a working install. + # No server at all: mark degraded so the arm64 CPU-prebuilt last resort + # and the failure exit fire instead of reporting a working install. _LLAMA_CPP_DEGRADED=true fi fi diff --git a/unsloth/kernels/flex_attention.py b/unsloth/kernels/flex_attention.py index 7e9c80cf13..044e8911f2 100644 --- a/unsloth/kernels/flex_attention.py +++ b/unsloth/kernels/flex_attention.py @@ -27,12 +27,10 @@ torch_compile_options = { def _flex_is_dgx_spark(): - # Inlined CUDA-free copy of _utils._is_dgx_spark_no_cuda_init() (kept local to - # avoid a circular import). Spark = aarch64 + a Spark device name via nvidia-smi. - # Must NOT touch torch.cuda: this runs at module import, and vision.py imports - # ..kernels before ._utils -- a device-name query here would initialize the CUDA - # allocator before patch_dgx_spark_memory_config() can set PYTORCH_CUDA_ALLOC_CONF - # on the very Spark hosts this check targets. + # Local CUDA-free copy of _utils._is_dgx_spark_no_cuda_init() (avoids a circular + # import). Runs at module import, before ._utils -- touching torch.cuda here would + # init the allocator before patch_dgx_spark_memory_config() can set + # PYTORCH_CUDA_ALLOC_CONF on the very Spark hosts this targets. _force = os.environ.get("UNSLOTH_FORCE_DGX_SPARK") if _force == "1": return True @@ -57,8 +55,7 @@ def _flex_is_dgx_spark(): return False -# Spark's 48 SMs are below inductor's 68-SM is_big_gpu threshold, so max_autotune -# is already skipped; disabling it just avoids a wasted compile-time search. +# Spark's 48 SMs are under inductor's 68-SM is_big_gpu bar; max_autotune would only waste search time. if _flex_is_dgx_spark(): torch_compile_options["max_autotune"] = False diff --git a/unsloth/models/_utils.py b/unsloth/models/_utils.py index e990b32737..c8dfc6cf67 100644 --- a/unsloth/models/_utils.py +++ b/unsloth/models/_utils.py @@ -992,20 +992,16 @@ except: from transformers.modeling_utils import logger as transformers_logger -# ---- NVIDIA DGX Spark (GB10) / N1X "RTX Spark" (Blackwell unified-memory) support ---- -# Shared detector for Spark-class UMA machines, which report varying device names -# ("NVIDIA GB10" on DGX Spark, "JMJWOA-Generic-GPU" on the N1X laptop). The -# aarch64 + CUDA gate keeps every Spark workaround a strict no-op elsewhere. +# ---- NVIDIA DGX Spark (GB10) / N1X "RTX Spark" unified-memory support ---- +# Device names vary ("NVIDIA GB10" on DGX Spark, "JMJWOA-Generic-GPU" on N1X); +# the aarch64 + CUDA gate keeps every Spark workaround a no-op elsewhere. _DGX_SPARK_DEVICE_TOKENS = ("GB10", "JMJWOA", "N1X", "DGX SPARK", "GB110") @functools.lru_cache(maxsize = None) def is_dgx_spark(): - """True only on a DGX Spark / N1X Spark-class machine. - - Gate: aarch64 + NVIDIA CUDA + a known Spark device-name token. Overridable for - testing via UNSLOTH_FORCE_DGX_SPARK=1 (force on) / =0 (force off). - """ + """True only on DGX Spark / N1X Spark-class machines (gate: aarch64 + NVIDIA + CUDA + known device-name token). UNSLOTH_FORCE_DGX_SPARK=1/0 forces on/off.""" _force = os.environ.get("UNSLOTH_FORCE_DGX_SPARK") if _force == "1": return True @@ -1028,14 +1024,10 @@ def is_dgx_spark(): @functools.lru_cache(maxsize = None) def _is_dgx_spark_no_cuda_init(): - """Spark detection that never initializes a CUDA context. - - `is_dgx_spark()` calls `torch.cuda.get_device_name()`, which lazily initializes CUDA - (and the caching allocator). Settings consumed at allocator-init time -- - `PYTORCH_CUDA_ALLOC_CONF` (expandable_segments) -- must be decided BEFORE that, so this - variant reads the GPU name from `nvidia-smi` (a separate process) instead of torch. - Honors the same UNSLOTH_FORCE_DGX_SPARK override. Falls back to False on any error. - """ + """Spark detection that never initializes CUDA: reads device names via + `nvidia-smi` instead of torch, so allocator-init-time settings + (PYTORCH_CUDA_ALLOC_CONF) can still be set after calling it. Same + UNSLOTH_FORCE_DGX_SPARK override; False on any error.""" _force = os.environ.get("UNSLOTH_FORCE_DGX_SPARK") if _force == "1": return True @@ -1063,14 +1055,10 @@ def _is_dgx_spark_no_cuda_init(): def patch_dgx_spark_caching_allocator_warmup(): """No-op `transformers.modeling_utils.caching_allocator_warmup` on Spark UMA. - HF sizes a GPU pre-allocation from `cudaMemGetInfo()` to warm the caching - allocator. On Spark unified memory `cudaMemGetInfo` undercounts free memory - (reclaimable buffer cache is reported unavailable), so the warmup - `torch.empty(...)` raises `AcceleratorError: invalid argument` and aborts any - runtime-quantized (bitsandbytes 4/8-bit) load. The warmup is only a speed hint, - so skipping it on Spark merely forgoes a minor warmup while letting loads - succeed. No-op on every non-Spark platform (gated by `is_dgx_spark()`). - Idempotent: re-applying is a no-op (marked via `_unsloth_spark_noop`). + `cudaMemGetInfo()` undercounts free memory on Spark unified memory, so HF's + warmup `torch.empty(...)` raises `AcceleratorError: invalid argument` and + aborts bitsandbytes 4/8-bit loads. The warmup is only a speed hint, so skip + it. Gated by `is_dgx_spark()`; idempotent (`_unsloth_spark_noop` marker). """ if not is_dgx_spark(): return @@ -1091,21 +1079,13 @@ def patch_dgx_spark_caching_allocator_warmup(): def patch_dgx_spark_memory_config(): - """Memory-efficiency default for Spark UMA (accuracy-neutral, gated). + """Enable allocator `expandable_segments` on Spark UMA to cut fragmentation + OOMs (accuracy-neutral; strict no-op off-Spark). - Enables the CUDA caching allocator's `expandable_segments` mode so segments can - grow in virtual address space instead of fragmenting the shared unified-memory - pool -- more of the pool stays usable for weights/activations (fewer - fragmentation OOMs; headroom for larger models / longer sequences). Pure memory - management: it never changes any computed value, so accuracy is unaffected. - - Strictly no-op off-Spark. Respects an existing PYTORCH_CUDA_ALLOC_CONF (only appends - `expandable_segments` when absent, never overrides a user's setting) and an explicit - opt-out (UNSLOTH_NO_EXPANDABLE_SEGMENTS=1). Must run before the first CUDA allocation, - so it gates on the CUDA-free `_is_dgx_spark_no_cuda_init()` -- the regular - `is_dgx_spark()` calls `torch.cuda.get_device_name()`, which would initialize CUDA (and - the allocator) before this env var could take effect. `import unsloth` precedes model - load, so it is set in time for normal use. + Appends to PYTORCH_CUDA_ALLOC_CONF only when absent; opt out with + UNSLOTH_NO_EXPANDABLE_SEGMENTS=1. Must run before the first CUDA allocation, + hence the CUDA-free `_is_dgx_spark_no_cuda_init()` gate -- `is_dgx_spark()` + would initialize the allocator before the env var could take effect. """ if not _is_dgx_spark_no_cuda_init(): return @@ -1120,21 +1100,14 @@ def patch_dgx_spark_memory_config(): def patch_dgx_spark_runtime_defaults(): - """Spark UMA runtime defaults (accuracy-neutral, gated, env-overridable). + """Spark UMA runtime defaults (no-op off-Spark; env-overridable). - - `UNSLOTH_DISABLE_DOUBLE_BUFFER=1`: unsloth-zoo's gradient-checkpointing - double-buffer is enabled via a `torch.cuda.mem_get_info` free-memory check - that UNDERCOUNTS on UMA, and it stages an extra GPU buffer to overlap a - host<->device copy that is physically free on a shared pool. Default it off - on Spark (`setdefault`, so a user can still force it back on). Must be set - before unsloth-zoo initializes gradient checkpointing -- `import unsloth` - precedes that, so this is in time. - - `set_per_process_memory_fraction`: OPT-IN safety valve. On Spark UMA an - over-allocation can wedge the box (untracked UMA allocations may never trip - a catchable OOM). If the user sets `UNSLOTH_SPARK_MEM_FRACTION=<0..1>`, cap - the caching allocator so it raises OutOfMemoryError early. Default unset -> - NO cap (no capacity loss); purely opt-in. - Strict no-op off-Spark. + - UNSLOTH_DISABLE_DOUBLE_BUFFER=1 (setdefault): zoo's grad-checkpointing + double-buffer gates on a mem_get_info check that UNDERCOUNTS on UMA, and + its extra staging buffer is pure waste on a shared pool. + - UNSLOTH_SPARK_MEM_FRACTION=<0..1> (opt-in, default NO cap): caps the + allocator so over-allocation raises OutOfMemoryError early instead of + wedging the box (untracked UMA allocations may never trip a catchable OOM). """ if not is_dgx_spark(): return @@ -1142,8 +1115,7 @@ def patch_dgx_spark_runtime_defaults(): _frac = os.environ.get("UNSLOTH_SPARK_MEM_FRACTION") if _frac: try: - # Only (0, 1] is a usable cap: 0 would make EVERY allocation OOM - # and values > 1 are rejected by torch. Out-of-range = no cap. + # 0 would OOM every allocation; torch rejects > 1. Out-of-range = no cap. _frac_val = float(_frac) if 0.0 < _frac_val <= 1.0: torch.cuda.set_per_process_memory_fraction(_frac_val) @@ -1152,17 +1124,12 @@ def patch_dgx_spark_runtime_defaults(): def patch_dgx_spark_dataloader_defaults(): - """On Spark UMA, default `dataloader_pin_memory` to False (accuracy-neutral). + """Default `dataloader_pin_memory` to False on Spark UMA (accuracy-neutral). - Page-locked host memory exists to speed host->device DMA; on unified memory - there is no separate device memory, so pinning only reserves non-pageable RAM - from the shared pool and adds a staging copy -- pure waste. Mirrors - transformers' own `if self.use_cpu: self.dataloader_pin_memory = False` - precedent. Wraps the base `TrainingArguments.__post_init__`, so SFT + every - TRL trainer (whose configs call `super().__post_init__()`) are covered with - one idempotent patch. Only flips the library default `True`; opt out with - `UNSLOTH_SPARK_KEEP_PIN_MEMORY=1`. Strict no-op off-Spark; never changes any - computed value, so accuracy is unaffected. + With one shared memory pool, pinning only reserves non-pageable RAM and adds + a staging copy (mirrors transformers' own use_cpu precedent). Wrapping the + base `TrainingArguments.__post_init__` covers SFT + every TRL trainer in one + idempotent patch. Opt out: UNSLOTH_SPARK_KEEP_PIN_MEMORY=1. No-op off-Spark. """ if not is_dgx_spark(): return @@ -1177,8 +1144,7 @@ def patch_dgx_spark_dataloader_defaults(): return _orig_post_init = Base.__post_init__ - # Forward *args/**kwargs so a future TrainingArguments (or a subclass) that - # adds InitVar parameters to __post_init__ keeps working through the wrapper. + # *args/**kwargs: tolerate future InitVar parameters in __post_init__. def __post_init__(self, *args, **kwargs): _orig_post_init(self, *args, **kwargs) if getattr(self, "dataloader_pin_memory", None) is True: @@ -1744,8 +1710,7 @@ torch_compile_options = { "trace.enabled": UNSLOTH_COMPILE_DEBUG, "triton.cudagraphs": False, } -# Spark's 48 SMs are below inductor's 68-SM is_big_gpu threshold, so max_autotune -# is already skipped; disabling it just avoids a wasted compile-time search. +# Spark's 48 SMs are under inductor's 68-SM is_big_gpu bar; max_autotune would only waste search time. if is_dgx_spark(): torch_compile_options["max_autotune"] = False From c3b50c4e98c3505d7738442c94c5b72a5bdf46b7 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Thu, 11 Jun 2026 20:22:48 -0700 Subject: [PATCH 58/95] Merge origin/main into woa-nvidia-wsl-fallback (94 commits) Resolved 2 conflicts: - studio/setup.sh: main restructured the CUDA-toolkit branch (new driver-vs-toolkit major-version compatibility check + an _CUDA_TOOLKIT_ALLOWED guard that now owns -DGGML_CUDA=ON and the CUDA_ARCHS detection). Took main's structure and re-injected our glibc>=2.41/CUDA<13.3 rsqrt diagnostic inside the guarded block so it runs against the final _NVCC_VER (after main's driver-compat swap). - scripts/uninstall.sh: main expanded ~/.unsloth cleanup (.cache, .staging, librocdxg, rocm-smoketest, rmdir) and rewrote the WSL Windows-shortcut removal to per-distro, wsl.exe-target-filtered matching. Took main's superset + kept our provision_llama_cuda.sh removal and our %LOCALAPPDATA%\Unsloth shim+PATH cleanup (appended after main's per-distro .lnk loop). --- studio/setup.sh | 54 +++++++++++++++++-------------------------------- 1 file changed, 18 insertions(+), 36 deletions(-) diff --git a/studio/setup.sh b/studio/setup.sh index 0e32289819..3a55c1607a 100755 --- a/studio/setup.sh +++ b/studio/setup.sh @@ -1430,41 +1430,6 @@ else GPU_BACKEND="" _BUILD_DESC="building (CPU, CUDA toolkit < 12.4)" else -<<<<<<< ours - CMAKE_ARGS="$CMAKE_ARGS -DGGML_CUDA=ON" - - # glibc >= 2.41 + CUDA < 13.3: rsqrt/rsqrtf header clash fails every .cu - # ("exception specification is incompatible") -> CPU fallback; only fix is - # CUDA >= 13.3. Diagnostic only -- never changes flags or aborts. - _GLIBC_VER="$(getconf GNU_LIBC_VERSION 2>/dev/null | awk '{print $2}')" || _GLIBC_VER="" - if [ -n "$_GLIBC_VER" ]; then - _GLIBC_MAJ="${_GLIBC_VER%%.*}"; _GLIBC_MIN="${_GLIBC_VER#*.}"; _GLIBC_MIN="${_GLIBC_MIN%%.*}" - _CU_MAJ="${_NVCC_VER%%.*}"; _CU_MIN="${_NVCC_VER#*.}"; _CU_MIN="${_CU_MIN%%.*}" - if { [ "${_GLIBC_MAJ:-0}" -gt 2 ] 2>/dev/null \ - || { [ "${_GLIBC_MAJ:-0}" -eq 2 ] 2>/dev/null && [ "${_GLIBC_MIN:-0}" -ge 41 ] 2>/dev/null; }; } \ - && { [ "${_CU_MAJ:-0}" -lt 13 ] 2>/dev/null \ - || { [ "${_CU_MAJ:-0}" -eq 13 ] 2>/dev/null && [ "${_CU_MIN:-0}" -lt 3 ] 2>/dev/null; }; }; then - substep "CUDA toolkit ${_NVCC_VER} is incompatible with glibc ${_GLIBC_VER} (rsqrt/rsqrtf header clash)." "$C_ERR" - substep "the GPU build will fail to compile and fall back to CPU -- install CUDA Toolkit >= 13.3:" "$C_WARN" - substep "https://developer.nvidia.com/cuda-downloads (setup.sh auto-selects the newest /usr/local/cuda-*)" "$C_WARN" - fi - fi - - CUDA_ARCHS="" - if command -v nvidia-smi &>/dev/null; then - _raw_caps=$(nvidia-smi --query-gpu=compute_cap --format=csv,noheader 2>/dev/null || true) - while IFS= read -r _cap; do - _cap=$(echo "$_cap" | tr -d '[:space:]') - if [[ "$_cap" =~ ^([0-9]+)\.([0-9]+)$ ]]; then - _arch="${BASH_REMATCH[1]}${BASH_REMATCH[2]}" - # Append if not already present - case ";$CUDA_ARCHS;" in - *";$_arch;"*) ;; - *) CUDA_ARCHS="${CUDA_ARCHS:+$CUDA_ARCHS;}$_arch" ;; - esac - fi - done <<< "$_raw_caps" -======= _DRIVER_MAX_CUDA="$(_cuda_driver_max_version)" _CUDA_TOOLKIT_ALLOWED=true if [ -n "$_NVCC_VER" ] && [ -n "$_DRIVER_MAX_CUDA" ] && \ @@ -1484,12 +1449,29 @@ else _BUILD_DESC="building (CPU, CUDA toolkit major > driver)" _CUDA_TOOLKIT_ALLOWED=false fi ->>>>>>> theirs fi if [ "$_CUDA_TOOLKIT_ALLOWED" = true ]; then CMAKE_ARGS="$CMAKE_ARGS -DGGML_CUDA=ON" + # glibc >= 2.41 + CUDA < 13.3: rsqrt/rsqrtf header clash fails every .cu + # ("exception specification is incompatible") -> CPU fallback; only fix is + # CUDA >= 13.3. Diagnostic only -- never changes flags or aborts. Checked + # against the final _NVCC_VER (after the driver-compat swap above). + _GLIBC_VER="$(getconf GNU_LIBC_VERSION 2>/dev/null | awk '{print $2}')" || _GLIBC_VER="" + if [ -n "$_GLIBC_VER" ]; then + _GLIBC_MAJ="${_GLIBC_VER%%.*}"; _GLIBC_MIN="${_GLIBC_VER#*.}"; _GLIBC_MIN="${_GLIBC_MIN%%.*}" + _CU_MAJ="${_NVCC_VER%%.*}"; _CU_MIN="${_NVCC_VER#*.}"; _CU_MIN="${_CU_MIN%%.*}" + if { [ "${_GLIBC_MAJ:-0}" -gt 2 ] 2>/dev/null \ + || { [ "${_GLIBC_MAJ:-0}" -eq 2 ] 2>/dev/null && [ "${_GLIBC_MIN:-0}" -ge 41 ] 2>/dev/null; }; } \ + && { [ "${_CU_MAJ:-0}" -lt 13 ] 2>/dev/null \ + || { [ "${_CU_MAJ:-0}" -eq 13 ] 2>/dev/null && [ "${_CU_MIN:-0}" -lt 3 ] 2>/dev/null; }; }; then + substep "CUDA toolkit ${_NVCC_VER} is incompatible with glibc ${_GLIBC_VER} (rsqrt/rsqrtf header clash)." "$C_ERR" + substep "the GPU build will fail to compile and fall back to CPU -- install CUDA Toolkit >= 13.3:" "$C_WARN" + substep "https://developer.nvidia.com/cuda-downloads (setup.sh auto-selects the newest /usr/local/cuda-*)" "$C_WARN" + fi + fi + CUDA_ARCHS="" if command -v nvidia-smi &>/dev/null; then _raw_caps=$(_setup_run_smi nvidia-smi --query-gpu=compute_cap --format=csv,noheader 2>/dev/null || true) From 8f0b5e78da1d519d18cb41f8d86a626b2feed4a7 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Thu, 11 Jun 2026 22:05:05 -0700 Subject: [PATCH 59/95] docs: tighten PR comments/docstrings (no code change; AST + non-comment-line verified) --- install.ps1 | 116 +++++++++---------- install.sh | 16 +-- scripts/uninstall.ps1 | 36 +++--- scripts/uninstall.sh | 6 +- studio/backend/core/training/worker.py | 21 ++-- studio/backend/tests/test_spark_oom_guard.py | 12 +- studio/scripts/provision_llama_cuda.sh | 12 +- studio/setup.sh | 43 ++++--- unsloth/kernels/flex_attention.py | 7 +- unsloth/models/_utils.py | 27 +++-- 10 files changed, 143 insertions(+), 153 deletions(-) diff --git a/install.ps1 b/install.ps1 index d2312c98ec..af51ae3604 100644 --- a/install.ps1 +++ b/install.ps1 @@ -48,8 +48,8 @@ function Install-UnslothStudio { } } - # Ref for fetching install assets (provision_llama_cuda.sh, the .ico) from - # raw.githubusercontent.com; UNSLOTH_INSTALL_REF overrides 'main' for pre-merge testing. + # raw.githubusercontent.com ref for install assets (provision_llama_cuda.sh, .ico). + # UNSLOTH_INSTALL_REF overrides 'main' for pre-merge testing. function Get-UnslothInstallRef { if ($env:UNSLOTH_INSTALL_REF -and $env:UNSLOTH_INSTALL_REF.Trim()) { return $env:UNSLOTH_INSTALL_REF.Trim() } return 'main' @@ -97,9 +97,8 @@ function Install-UnslothStudio { if ($TauriMode) { exit $Code } - # -File runs exit 0 on a plain return regardless of $LASTEXITCODE, so `exit` - # must carry the code there; under `irm | iex` (no $PSCommandPath) `exit` - # would kill the user's shell, so fall through. + # -File ignores $LASTEXITCODE on plain return, so `exit` must carry the code; + # under `irm | iex` (no $PSCommandPath) `exit` would kill the user's shell. if ($PSCommandPath) { exit $Code } @@ -1774,13 +1773,13 @@ shell.Run cmd, 0, False $TorchIndexUrl = Get-TorchIndexUrl # ===== Windows-on-ARM + NVIDIA GPU -> automatic WSL2 fallback (N1X "RTX Spark" / DGX Spark-class) ===== - # win_arm64 has no CUDA PyTorch/Triton wheel, so run the Linux installer inside WSL2 (full GPU) and - # add a Windows `unsloth` shim that forwards into it. x86_64 / ARM64-without-NVIDIA unaffected; if a - # win_arm64 CUDA torch wheel ever ships, the probe below keeps the native install automatically. - # Opt out: UNSLOTH_NO_WSL_FALLBACK=1; choose the distro with UNSLOTH_WSL_DISTRO. + # win_arm64 has no CUDA PyTorch/Triton wheel, so run the Linux installer inside WSL2 (full GPU) plus + # a Windows `unsloth` shim that forwards into it. x86_64 / ARM64-without-NVIDIA unaffected; the probe + # below keeps the native install if a win_arm64 CUDA torch wheel ever ships. + # Opt out: UNSLOTH_NO_WSL_FALLBACK=1; pick distro with UNSLOTH_WSL_DISTRO. try { $_winArm64 = ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString() -ieq 'Arm64') } catch { $_winArm64 = $false } - # x64-emulated PS on ARM reports X64/AMD64 via .NET and $env:; Win32_Processor.Architecture - # (12=ARM64) and machine-level PROCESSOR_ARCHITECTURE read the true arch. Only ever turns $_winArm64 ON. + # x64-emulated PS on ARM reports X64/AMD64; Win32_Processor.Architecture (12=ARM64) and machine-level + # PROCESSOR_ARCHITECTURE read the true arch. Only ever turns $_winArm64 ON. if (-not $_winArm64) { try { if ((@(Get-CimInstance Win32_Processor -ErrorAction Stop))[0].Architecture -eq 12) { $_winArm64 = $true } } catch {} } @@ -1793,10 +1792,10 @@ shell.Run cmd, 0, False $_nativeCudaTorchOk = $false if ($_winArm64 -and $HasNvidiaSmi -and (-not $SkipTorch)) { # Probe with the SAME spec as the real install ("torch>=2.4,<2.11.0"): a bare `torch` probe - # can match an out-of-range wheel, skipping WSL only to fail the real pinned install. + # could match an out-of-range wheel, skipping WSL only to fail the real pinned install. $prevEapProbe = $ErrorActionPreference; $ErrorActionPreference = "Continue" - # --reinstall: an already-installed (e.g. CPU-only) torch must not satisfy the probe -- - # it has to prove a native win_arm64 CUDA wheel exists on the index. + # --reinstall: an installed (e.g. CPU-only) torch mustn't satisfy the probe -- it must + # prove a native win_arm64 CUDA wheel exists on the index. $global:LASTEXITCODE = -1 try { & uv pip install --python $VenvPython --dry-run --reinstall "torch>=2.4,<2.11.0" --index-url $TorchIndexUrl *> $null @@ -1809,15 +1808,14 @@ shell.Run cmd, 0, False substep "no win_arm64 CUDA PyTorch/Triton yet; WSL2 delivers full GPU (DGX Spark / RTX Spark path)." "Yellow" # The Tauri desktop app launches its backend from a Windows venv (resolve_backend_binary), - # not WSL, so a WSL-only install would report complete yet fail to start -- send those - # users to the CLI installer. + # not WSL, so a WSL-only install would start nothing -- send those users to the CLI installer. if ($TauriMode) { return (Exit-InstallFailure "Windows-on-ARM + NVIDIA GPU needs the WSL2 GPU install, which the desktop app can't launch yet. Install from PowerShell instead: irm https://unsloth.ai/install.ps1 | iex" 1) } $wslReady = $false if (Get-Command wsl.exe -ErrorAction SilentlyContinue) { - # Reset first: a stale 0 in $LASTEXITCODE would wrongly mark WSL ready if wsl.exe fails to start. + # Reset: a stale 0 would wrongly mark WSL ready if wsl.exe fails to start. $global:LASTEXITCODE = -1 try { & wsl.exe --status *> $null; if ($LASTEXITCODE -eq 0) { $wslReady = $true } } catch {} } @@ -1835,8 +1833,8 @@ shell.Run cmd, 0, False substep "reboot, then re-run: irm https://unsloth.ai/install.ps1 | iex" "Cyan" } # Deferred until reboot: restore any rolled-aside previous venv and signal not-complete. - # A plain return exits 0 for -File runs regardless of $LASTEXITCODE, so `exit 1` there; - # under `irm | iex` ($PSCommandPath empty) exit would kill the user's shell, so return. + # `exit 1` for -File (plain return exits 0); under `irm | iex` (no $PSCommandPath) return + # instead, since exit would kill the user's shell. Restore-StudioVenvRollback $global:LASTEXITCODE = 1 if ($PSCommandPath) { exit 1 } @@ -1844,9 +1842,8 @@ shell.Run cmd, 0, False } $distro = if ($env:UNSLOTH_WSL_DISTRO) { $env:UNSLOTH_WSL_DISTRO } else { "Ubuntu-24.04" } - # For cmd-context uses (.cmd shim, copy-paste hints): wsl.exe rejects a QUOTED space-free - # name (WSL_E_DISTRO_NOT_FOUND, verified on 2.x) yet splits a bare spaced one after -d -- - # so quote ONLY when the name contains whitespace. + # For cmd-context uses (.cmd shim, copy-paste hints): wsl.exe rejects a QUOTED space-free name + # (WSL_E_DISTRO_NOT_FOUND on 2.x) yet splits a bare spaced one after -d, so quote ONLY when spaced. $_distroArg = if ($distro -match '\s') { '"' + $distro + '"' } else { $distro } # Detect the distro by exit code (encoding-proof; wsl --list emits UTF-16 that PS mis-parses). $haveDistro = $false @@ -1857,9 +1854,8 @@ shell.Run cmd, 0, False try { & wsl.exe --install -d $distro --no-launch } catch {} } else { # A PRE-EXISTING distro may be WSL1 (no GPU passthrough; would only fail at the final - # torch.cuda check). Detect from inside the distro (encoding-proof, unlike UTF-16 - # `wsl -l -v`) and convert in place -- `wsl --set-version` preserves the files. - # Fresh installs default to WSL2, so only the pre-existing case needs this. + # torch.cuda check). Detect from inside (encoding-proof, unlike UTF-16 `wsl -l -v`) and + # convert in place -- `wsl --set-version` preserves files. Fresh installs default to WSL2. $_wsl2Probe = 'grep -qiE ''microsoft-standard|WSL2'' /proc/version 2>/dev/null || test -e /usr/lib/wsl/lib/libcuda.so' $_isWsl2 = $false $global:LASTEXITCODE = -1 @@ -1878,14 +1874,14 @@ shell.Run cmd, 0, False } } substep "installing Unsloth Studio inside WSL '$distro' with full GPU (this downloads PyTorch)..." "Cyan" - # Non-main ref: fetch + export THAT ref so the WSL venv gets the branch's setup.sh + - # patches (else install.sh pulls PyPI unsloth). main == plain unsloth.ai/install.sh. + # Non-main ref: fetch + export THAT ref so the WSL venv gets the branch's setup.sh + patches + # (else install.sh pulls PyPI unsloth). main == plain unsloth.ai/install.sh. $_instRef = Get-UnslothInstallRef - # UNSLOTH_WSL_LLAMA_DEFERRED=1: setup.sh skips its foreground CUDA llama.cpp build because - # install.ps1 builds it in the background (a DIRECT install.sh run in WSL doesn't set it). - # apt stderr stays visible (only stdout -> /dev/null) so network/repo failures are diagnosable. - # Forward UNSLOTH_NO_LLAMA_CUDA into WSL: the same opt-out skips the dispatch below, so - # unforwarded, setup.sh would defer to a background builder that never starts (no llama-server). + # UNSLOTH_WSL_LLAMA_DEFERRED=1: setup.sh skips its foreground CUDA llama.cpp build since we build + # it in the background (a DIRECT install.sh run in WSL doesn't set it). apt stderr stays visible + # (only stdout -> /dev/null) so network/repo failures are diagnosable. + # Forward UNSLOTH_NO_LLAMA_CUDA into WSL: it also skips the dispatch below, so unforwarded + # setup.sh would defer to a background builder that never starts (no llama-server). $_fwdEnv = '' if ($env:UNSLOTH_NO_LLAMA_CUDA -eq '1') { $_fwdEnv = 'export UNSLOTH_NO_LLAMA_CUDA=1; ' } if ($_instRef -eq 'main') { @@ -1894,7 +1890,7 @@ shell.Run cmd, 0, False $wslInstall = $_fwdEnv + 'export DEBIAN_FRONTEND=noninteractive UNSLOTH_WSL_LLAMA_DEFERRED=1; export UNSLOTH_INSTALL_REF=' + $_instRef + '; apt-get update -y >/dev/null; apt-get install -y build-essential cmake git curl pciutils libcurl4-openssl-dev >/dev/null; curl -fsSL https://raw.githubusercontent.com/unslothai/unsloth/' + $_instRef + '/install.sh | sh' } # install.sh may exit non-zero on the optional llama.cpp prebuilt step (no aarch64 prebuilt) - # though torch + unsloth + Studio still install, so lower EAP so it doesn't abort under Stop. + # though torch + unsloth + Studio still install; lower EAP so it doesn't abort under Stop. $prevEapWsl = $ErrorActionPreference $ErrorActionPreference = "Continue" $global:LASTEXITCODE = -1 @@ -1909,15 +1905,15 @@ shell.Run cmd, 0, False $torchOk = $false $prevEapChk = $ErrorActionPreference $ErrorActionPreference = "Continue" - # Reset first so a stale 0 from a prior command can't mark torch OK if this fails to launch. + # Reset so a stale 0 can't mark torch OK if this fails to launch. $global:LASTEXITCODE = -1 try { & wsl.exe -d $distro --cd /root -u root -- /root/.unsloth/studio/unsloth_studio/bin/python -c "import torch,sys; sys.exit(0 if torch.cuda.is_available() else 3)" *> $null $torchOk = ($LASTEXITCODE -eq 0) } catch {} finally { $ErrorActionPreference = $prevEapChk } # Self-heal web-server deps: a cut-short install.sh "studio deps" step leaves torch + unsloth - # but no fastapi/uvicorn/structlog/starlette (`unsloth studio` dies). Reinstall them without - # pinning huggingface-hub/transformers/datasets so the verified GPU torch stack stays intact. + # but no fastapi/uvicorn/structlog/starlette (`unsloth studio` dies). Reinstall them unpinned + # (no huggingface-hub/transformers/datasets) so the verified GPU torch stack stays intact. if ($torchOk) { $_studioPy = "/root/.unsloth/studio/unsloth_studio/bin/python" $_serverOk = $false @@ -1928,8 +1924,8 @@ shell.Run cmd, 0, False } catch {} finally { $ErrorActionPreference = $prevEapS } if (-not $_serverOk) { substep "Studio web-server deps incomplete (install.sh step cut short) -- installing them now..." "Cyan" - # studio.txt minus the huggingface-hub pin; uv preferred, pip fallback. Bare names - # only: `>=` becomes a redirection through PowerShell -> wsl.exe -> bash -lc, and + # studio.txt minus the huggingface-hub pin; uv preferred, pip fallback. Bare names only: + # `>=` would become a redirection through PowerShell -> wsl.exe -> bash -lc, and # latest-of-each satisfies the studio.txt minimums anyway. $_deps = 'typer fastapi uvicorn matplotlib pandas nest_asyncio pyjwt easydict addict structlog diceware ddgs cryptography httpx fastmcp' $_repair = 'PY=/root/.unsloth/studio/unsloth_studio/bin/python; UV="$(command -v uv 2>/dev/null || echo /root/.local/bin/uv)"; if [ -x "$UV" ] || command -v uv >/dev/null 2>&1; then "$UV" pip install --python "$PY" ' + $_deps + '; else "$PY" -m pip install ' + $_deps + '; fi' @@ -1943,8 +1939,8 @@ shell.Run cmd, 0, False if ($_serverOk) { substep "Studio web-server deps installed." "Green" } else { substep "(could not auto-install Studio server deps; 'unsloth studio' may fail to start)" "Yellow" } } - # The uv-managed venv ships no `pip`, but unsloth-zoo's exporter calls check_pip() and only - # finds `uv pip` when uv is on PATH. Seed pip so `save_pretrained_gguf` works regardless. + # The uv-managed venv ships no `pip`, but unsloth-zoo's exporter's check_pip() finds `uv pip` + # only when uv is on PATH. Seed pip so `save_pretrained_gguf` works regardless. $prevEapP = $ErrorActionPreference; $ErrorActionPreference = "Continue" try { & wsl.exe -d $distro --cd /root -u root -- $_studioPy -m pip --version *> $null @@ -1956,7 +1952,7 @@ shell.Run cmd, 0, False if ($torchOk) { step "done" "Unsloth Studio installed in WSL '$distro' -- GPU ready (torch.cuda available)." "Green" # Native Windows `unsloth` shim forwards every `unsloth ...` into the WSL GPU env so the user - # never touches WSL. WSL2 forwards 127.0.0.1, so http://localhost:8888 opens in the Windows browser. + # never touches WSL. WSL2 forwards 127.0.0.1, so http://localhost:8888 opens in Windows. try { $shimDir = Join-Path $env:LOCALAPPDATA "Unsloth\bin" New-Item -ItemType Directory -Force -Path $shimDir *> $null @@ -1966,10 +1962,10 @@ shell.Run cmd, 0, False "wsl.exe -d $_distroArg -u root -- /root/.unsloth/studio/unsloth_studio/bin/unsloth %*" ) Set-Content -LiteralPath (Join-Path $shimDir "unsloth.cmd") -Value $shimLines -Encoding ASCII - # Record the distro so the uninstaller can clean a custom UNSLOTH_WSL_DISTRO - # install without the env var being set again. + # Record the distro so the uninstaller can clean a custom UNSLOTH_WSL_DISTRO install + # without the env var set again. try { Set-Content -LiteralPath (Join-Path (Split-Path $shimDir -Parent) "wsl-distro.txt") -Value $distro -Encoding ASCII } catch {} - # A fresh profile may have no HKCU 'Path' at all; null would make TrimEnd() throw. + # A fresh profile may have no HKCU 'Path'; null would make TrimEnd() throw. $userPath = [Environment]::GetEnvironmentVariable("Path", "User") if (-not $userPath) { $userPath = "" } if (($userPath -split ';') -notcontains $shimDir) { @@ -1997,14 +1993,14 @@ shell.Run cmd, 0, False 'wsl.exe -d $distro --cd /root -u root -- bash -lic "unsloth studio -p 8888"' ) Set-Content -LiteralPath $launcher -Value $L -Encoding UTF8 - # Icon must live OUTSIDE %LOCALAPPDATA%: on WoA the shell's sandboxed icon broker - # can't read a .ico under AppData\Local, so the shortcut renders BLANK -- the same - # file under the user profile renders fine (verified on N1X). Only the icon moves. + # Icon must live OUTSIDE %LOCALAPPDATA%: on WoA the sandboxed icon broker can't read a + # .ico under AppData\Local, so the shortcut renders BLANK; under the user profile it + # renders fine (verified on N1X). Only the icon moves. $iconDir = Join-Path $env:USERPROFILE ".unsloth" New-Item -ItemType Directory -Force -Path $iconDir *> $null $icon = Join-Path $iconDir "unsloth.ico" - # Prefer the bundled icon; fall back to a GitHub download. Validate the ICO header - # (00 00 01 00) before attaching, so a partial/HTML-404 download never makes a blank icon. + # Prefer the bundled icon, else download from GitHub. Validate the ICO header (00 00 01 00) + # before attaching, so a partial/HTML-404 download never makes a blank icon. $bundledIcon = $null if ($PSScriptRoot -and $PSScriptRoot.Trim()) { $bundledIcon = Join-Path $PSScriptRoot "studio\frontend\public\unsloth.ico" } if ($bundledIcon -and (Test-Path -LiteralPath $bundledIcon)) { @@ -2050,21 +2046,21 @@ shell.Run cmd, 0, False } catch { substep "(could not create shortcuts: $($_.Exception.Message))" "Yellow" } - # GGUF *inference* needs a CUDA llama-server (no aarch64+CUDA prebuilt exists), so build one - # into ~/.unsloth/llama.cpp in the BACKGROUND. Best-effort; opt out: UNSLOTH_NO_LLAMA_CUDA=1. + # GGUF *inference* needs a CUDA llama-server (no aarch64+CUDA prebuilt), so build one into + # ~/.unsloth/llama.cpp in the BACKGROUND. Best-effort; opt out: UNSLOTH_NO_LLAMA_CUDA=1. if ($env:UNSLOTH_NO_LLAMA_CUDA -ne '1') { $prevEapL = $ErrorActionPreference; $ErrorActionPreference = "Continue" try { $_llamaUrl = "https://raw.githubusercontent.com/unslothai/unsloth/$(Get-UnslothInstallRef)/studio/scripts/provision_llama_cuda.sh" # Step 1: fetch the provision script + write a runner (base64 to dodge quoting layers). - # The runner restores PATH (non-login shells miss /usr/lib/wsl/lib nvidia-smi -> + # The runner restores PATH (non-login shells miss /usr/lib/wsl/lib nvidia-smi, so # provision early-exits) and exports the env knobs below (Windows env vars don't cross # into WSL). A runner FILE lets the detached launcher pass only space-free args, # avoiding Start-Process mis-splitting `bash -lc `. $_pathLine = 'export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/lib/wsl/lib:$PATH"' + "`n" $_jobsLine = if ($env:UNSLOTH_LLAMA_BUILD_JOBS) { "export UNSLOTH_LLAMA_BUILD_JOBS=$($env:UNSLOTH_LLAMA_BUILD_JOBS)`n" } else { "" } - # Bridge UNSLOTH_LLAMA_TAG / UNSLOTH_LLAMA_PR pins into WSL too, else the deferred - # build silently ignores them. sh-single-quoted (tags/PRs are simple tokens). + # Bridge UNSLOTH_LLAMA_TAG / UNSLOTH_LLAMA_PR pins into WSL, else the deferred build + # ignores them. sh-single-quoted (tags/PRs are simple tokens). $_tagLine = if ($env:UNSLOTH_LLAMA_TAG) { "export UNSLOTH_LLAMA_TAG='$($env:UNSLOTH_LLAMA_TAG)'`n" } else { "" } $_prLine = if ($env:UNSLOTH_LLAMA_PR) { "export UNSLOTH_LLAMA_PR='$($env:UNSLOTH_LLAMA_PR)'`n" } else { "" } $_runner = "#!/usr/bin/env bash`n" + $_pathLine + $_jobsLine + $_tagLine + $_prLine + "exec bash /root/.unsloth/provision_llama_cuda.sh > /root/.unsloth/llama_cuda_build.log 2>&1`n" @@ -2073,9 +2069,9 @@ shell.Run cmd, 0, False $_fetchOut = & wsl.exe -d $distro --cd /root -u root -- bash -lc $_fetchCmd 2>$null if ("$_fetchOut" -match 'PROV_FETCHED') { # Step 2: a detached Windows-side wsl.exe keeps the WSL VM up for the whole build - # (a WSL-side `nohup &` dies: WSL stops the VM when the launching session exits). - # PS 5.1 Start-Process joins -ArgumentList WITHOUT quoting, so pass $_distroArg - # (pre-quoted only when spaced); all other tokens are space-free. + # (a WSL-side `nohup &` dies when the launching session exits). PS 5.1 Start-Process + # joins -ArgumentList WITHOUT quoting, so pass $_distroArg (pre-quoted only when + # spaced); all other tokens are space-free. Start-Process -WindowStyle Hidden -FilePath 'wsl.exe' -ArgumentList @('-d', $_distroArg, '--cd', '/root', '-u', 'root', '--', 'bash', '/root/.unsloth/run_llama_build.sh') | Out-Null step "llama.cpp" "building CUDA llama.cpp for GGUF inference in the background (a few min); log: ~/.unsloth/llama_cuda_build.log" "Green" } else { @@ -2088,15 +2084,15 @@ shell.Run cmd, 0, False substep "retry, or launch manually: wsl -d $_distroArg -u root -- bash -lic 'unsloth studio -p 8888'" "Cyan" } if ($torchOk) { - # Success: the Windows venv is vestigial here (everything runs in WSL), so drop the + # Success: the Windows venv is vestigial (everything runs in WSL), so drop the # rolled-aside previous-venv backup instead of orphaning it. Complete-StudioVenvRollback substep "GPU training + GGUF export run inside WSL. (GGUF *inference* additionally needs a CUDA llama.cpp build.)" "Yellow" $global:LASTEXITCODE = 0 return } - # Failed (torch.cuda unavailable): restore any rolled-aside previous venv and report - # non-zero (plain return exits 0 for -File; under iex `exit` would kill the caller's shell). + # Failed (torch.cuda unavailable): restore any rolled-aside previous venv and report non-zero + # (plain return exits 0 for -File; under iex `exit` would kill the caller's shell). Restore-StudioVenvRollback $global:LASTEXITCODE = 1 if ($PSCommandPath) { exit 1 } diff --git a/install.sh b/install.sh index 4f062b9f44..13d93531e7 100755 --- a/install.sh +++ b/install.sh @@ -2649,9 +2649,9 @@ elif [ -n "$TORCH_INDEX_URL" ]; then "unsloth-zoo @ git+https://github.com/unslothai/unsloth-zoo" elif [ -n "${UNSLOTH_INSTALL_REF:-}" ] && [ "${UNSLOTH_INSTALL_REF}" != "main" ] && [ "$PACKAGE_NAME" = "unsloth" ]; then # Pre-merge testing: install unsloth from a git ref (install.ps1 sets - # UNSLOTH_INSTALL_REF) so the branch's setup.sh + patches run. unsloth-zoo - # is not a base dep and SKIP_STUDIO_BASE skips studio base.txt, so name it - # explicitly or it never gets installed. + # UNSLOTH_INSTALL_REF) so the branch's setup.sh + patches run. Name + # unsloth-zoo explicitly: it's not a base dep and SKIP_STUDIO_BASE skips + # base.txt, so otherwise it never installs. substep "installing unsloth from git ref '$UNSLOTH_INSTALL_REF'..." run_install_cmd "install unsloth (@$UNSLOTH_INSTALL_REF)" uv pip install --python "$_VENV_PY" \ --upgrade-package unsloth --upgrade-package unsloth-zoo \ @@ -2660,11 +2660,11 @@ elif [ -n "$TORCH_INDEX_URL" ]; then run_install_cmd "install unsloth" uv pip install --python "$_VENV_PY" \ --upgrade-package unsloth -- "$PACKAGE_NAME" fi - # aarch64 + NVIDIA (DGX Spark / GB10 / N1X): base unsloth lacks bitsandbytes - # (the cuXXX extras are x86_64-oriented), so 4-bit QLoRA fails out of the box. - # aarch64 manylinux wheels work (verified on sm_121 via PTX JIT); best-effort, - # no wheel just keeps 16-bit LoRA / full finetuning. SKIP_TORCH gate: a - # --no-torch (GGUF-only) install must not let bitsandbytes drag torch back in. + # aarch64 + NVIDIA (DGX Spark / GB10 / N1X): unsloth's cuXXX extras are + # x86_64-oriented, so 4-bit QLoRA fails out of the box. aarch64 manylinux + # wheels work (verified on sm_121 via PTX JIT); best-effort, no wheel just + # keeps 16-bit LoRA / full finetuning. SKIP_TORCH gate: a --no-torch + # (GGUF-only) install must not let bitsandbytes drag torch back in. if [ "$SKIP_TORCH" = false ] \ && { [ "$(uname -m)" = "aarch64" ] || [ "$(uname -m)" = "arm64" ]; } \ && command -v nvidia-smi >/dev/null 2>&1 \ diff --git a/scripts/uninstall.ps1 b/scripts/uninstall.ps1 index e70b8d413f..287531fd71 100644 --- a/scripts/uninstall.ps1 +++ b/scripts/uninstall.ps1 @@ -418,12 +418,12 @@ function Uninstall-UnslothStudio { } catch { } # ── Windows-on-Arm WSL-fallback artifacts ── - # The ARM64+NVIDIA fallback puts Studio inside WSL plus a native shim + launcher under - # %LOCALAPPDATA%\Unsloth (not "Unsloth Studio") with a PATH entry -- all missed by the cleanup above. + # The ARM64+NVIDIA fallback puts Studio in WSL plus a native shim + launcher under + # %LOCALAPPDATA%\Unsloth (not "Unsloth Studio") with a PATH entry -- all missed above. _Step "Removing WSL-fallback artifacts (shim, launcher, PATH entry, WSL install)..." $unslothDir = if ($env:LOCALAPPDATA) { Join-Path $env:LOCALAPPDATA "Unsloth" } else { $null } - # wsl-distro.txt records a custom UNSLOTH_WSL_DISTRO install so it is cleanable - # without the env var set; read it BEFORE the directory is removed below. + # wsl-distro.txt records a custom UNSLOTH_WSL_DISTRO install so it's cleanable without the + # env var set; read it BEFORE the directory is removed below. $_recordedDistro = $null if ($unslothDir) { try { @@ -460,19 +460,19 @@ function Uninstall-UnslothStudio { # Remove the Studio install inside each WSL distro (the real GPU install + any CUDA llama.cpp build). if (Get-Command wsl.exe -ErrorAction SilentlyContinue) { try { - # `wsl --list` emits UTF-16 PS mis-parses, so probe candidates by exit code instead - # ('' = default distro). rm runs FIRST (the kills could SIGKILL this shell) and also - # drops the dangling /root/.local/bin/unsloth symlink. Scope STRICTLY to /root (where - # the fallback installs): /home/*/.unsloth may be an unrelated user's. The port-8888 - # kill is gated on an Unsloth install existing (checked BEFORE rm deletes the marker) - # so an unrelated 8888 listener survives; pkill matches argv containing /root/.unsloth/ - # rather than bare names that would kill a user's own llama-server, and the backslash - # + [h]-bracket in '/root/\.unslot[h]/' keep it from matching this command's own argv. + # Probe candidates by exit code ('' = default distro) since `wsl --list` emits UTF-16 PS + # mis-parses. rm runs FIRST (the kills could SIGKILL this shell) and drops the dangling + # /root/.local/bin/unsloth symlink. Scope STRICTLY to /root (where the fallback installs); + # /home/*/.unsloth may be another user's. The 8888 kill is gated on an Unsloth install + # existing (checked BEFORE rm deletes the marker) so an unrelated listener survives; pkill + # matches argv containing /root/.unsloth/ (not bare names that would hit a user's own + # llama-server), and the backslash + [h]-bracket in '/root/\.unslot[h]/' keep it from + # matching this command's own argv. $_clean = '_had=0; if [ -d /root/.unsloth ] || [ -L /root/.local/bin/unsloth ]; then _had=1; fi; rm -rf /root/.unsloth /root/llama-cuda /root/provision_llama_cuda.sh /root/llama_cuda_build.log 2>/dev/null; rm -f /root/.local/bin/unsloth 2>/dev/null; if [ $_had -eq 1 ]; then fuser -k 8888/tcp 2>/dev/null; fi; pkill -9 -f ''/root/\.unslot[h]/'' 2>/dev/null; true' - # Clean only distros with evidence of a fallback install: the wsl-distro.txt marker - # or an explicit UNSLOTH_WSL_DISTRO. The broad candidate probe is only for legacy - # marker-less installs, which exist only on ARM64 hosts -- on x86 it would delete - # distros this installer never touched (e.g. a ROCm-on-WSL Studio under /root). + # Clean only distros with evidence of a fallback install: the wsl-distro.txt marker or an + # explicit UNSLOTH_WSL_DISTRO. The broad candidate probe is only for legacy marker-less + # installs, which exist only on ARM64 -- on x86 it would delete distros this installer + # never touched (e.g. a ROCm-on-WSL Studio under /root). $_cands = @() if ($env:UNSLOTH_WSL_DISTRO) { $_cands += $env:UNSLOTH_WSL_DISTRO } if ($_recordedDistro) { $_cands += $_recordedDistro } @@ -505,8 +505,8 @@ function Uninstall-UnslothStudio { Write-Host " `$env:UNSLOTH_STUDIO_HOME = 'C:\your\path'; irm https://raw.githubusercontent.com/unslothai/unsloth/main/scripts/uninstall.ps1 | iex" } - # The distro probes leave a failing $LASTEXITCODE; reset it so success exits 0. Set the - # var rather than `exit 0` so `irm ... | iex` doesn't terminate the caller's shell. + # The distro probes leave a failing $LASTEXITCODE; reset so success exits 0. Set the var + # rather than `exit 0` so `irm ... | iex` doesn't terminate the caller's shell. $global:LASTEXITCODE = 0 } diff --git a/scripts/uninstall.sh b/scripts/uninstall.sh index 8abc8bf3b9..20285da856 100755 --- a/scripts/uninstall.sh +++ b/scripts/uninstall.sh @@ -303,9 +303,9 @@ case "$_os" in } catch { } } } - # WoA WSL-fallback (install.ps1) native shim/launcher dir - # (%LOCALAPPDATA%\Unsloth) + its PATH entry. install.ps1 created the - # shim; clean it here too so a WSL-side bash uninstall is complete. + # Remove the WoA WSL-fallback native shim/launcher dir + # (%LOCALAPPDATA%\Unsloth) + its PATH entry that install.ps1 + # created, so a WSL-side bash uninstall is complete. $ud = if ($env:LOCALAPPDATA) { Join-Path $env:LOCALAPPDATA "Unsloth" } else { $null }; if ($ud) { $shim = (Join-Path $ud "bin").TrimEnd("\","/"); diff --git a/studio/backend/core/training/worker.py b/studio/backend/core/training/worker.py index 3ed83024c8..0d1e51343d 100644 --- a/studio/backend/core/training/worker.py +++ b/studio/backend/core/training/worker.py @@ -738,11 +738,10 @@ def _nvidia_classify_spark_unified_memory(props: Any) -> tuple[str, bool]: Returns ``(marker, is_unified)``; marker is ``"is_integrated"`` or the matched device-name token, else ``""``. Spark-class parts (DGX Spark / GB10, N1X "RTX Spark") share one memory pool with the OS, so like the ROCm APUs they need a - ``set_per_process_memory_fraction`` cap -- exhausting the pool can stall the - box instead of raising a catchable OutOfMemoryError. + ``set_per_process_memory_fraction`` cap -- exhausting the pool can stall the box. ``is_integrated`` is authoritative on native Linux, but WSL2 paravirtualization - masks it to 0 and renames the device (the N1X reports ``JMJWOA-Generic-GPU``, + masks it to 0 and renames the device (N1X reports ``JMJWOA-Generic-GPU``, verified on hardware) -- hence the name-token fallback. Tokens mirror ``_DGX_SPARK_DEVICE_TOKENS`` in ``unsloth/models/_utils.py`` (duplicated because this guard runs before any ML import). @@ -2432,16 +2431,16 @@ def run_training_process(*, event_queue: Any, stop_queue: Any, config: dict) -> logger.debug("Could not set GPU memory fraction: %s", _oom_guard_err) # ── 1h. NVIDIA Spark-class unified-memory OOM guard ── - # NVIDIA flavor of the ROCm APU guard above: Spark-class parts share one - # memory pool with the OS, so over-allocation can stall the box instead of - # raising a catchable OutOfMemoryError. Cap at 0.80 like Strix Halo (20% - # headroom stays with the OS/page cache). UNSLOTH_SPARK_MEM_FRACTION - # overrides; outside (0, 1] disables. Discrete NVIDIA GPUs untouched. + # NVIDIA flavor of the ROCm APU guard above: Spark-class parts share one pool + # with the OS, so over-allocation can stall the box instead of raising a + # catchable OutOfMemoryError. Cap at 0.80 like Strix Halo (20% headroom for + # OS/page cache). UNSLOTH_SPARK_MEM_FRACTION overrides; outside (0, 1] disables. + # Discrete NVIDIA GPUs untouched. else: try: - # Set PYTORCH_CUDA_ALLOC_CONF before get_device_properties below inits - # the CUDA allocator -- the later `import unsloth` patch is too late for - # THIS worker process. CUDA-free nvidia-smi sniff (mirrors + # Set PYTORCH_CUDA_ALLOC_CONF before get_device_properties below inits the + # CUDA allocator -- the later `import unsloth` patch is too late for THIS + # worker process. CUDA-free nvidia-smi sniff (mirrors # _is_dgx_spark_no_cuda_init), same append-don't-override and # UNSLOTH_NO_EXPANDABLE_SEGMENTS opt-out as the library patch. try: diff --git a/studio/backend/tests/test_spark_oom_guard.py b/studio/backend/tests/test_spark_oom_guard.py index 74610d4369..4e61dfd3af 100644 --- a/studio/backend/tests/test_spark_oom_guard.py +++ b/studio/backend/tests/test_spark_oom_guard.py @@ -3,13 +3,11 @@ """Unit tests for _nvidia_classify_spark_unified_memory (Spark OOM-guard classifier). -Two paths: (1) the ``is_integrated`` device property (authoritative on native -Linux), (2) device-name token match — needed because WSL2's GPU -paravirtualization masks ``is_integrated`` to 0 and renames the device (the N1X -reports ``JMJWOA-Generic-GPU``; verified on hardware). - -Mirrors test_rocm_oom_guard.py for the ROCm/Strix-Halo classifier the NVIDIA -guard was modeled on. +Two paths: (1) ``is_integrated`` device property (authoritative on native Linux), +(2) device-name token match -- needed because WSL2's GPU paravirtualization masks +``is_integrated`` to 0 and renames the device (N1X reports ``JMJWOA-Generic-GPU``; +verified on hardware). Mirrors test_rocm_oom_guard.py for the ROCm/Strix-Halo +classifier the NVIDIA guard was modeled on. """ from __future__ import annotations diff --git a/studio/scripts/provision_llama_cuda.sh b/studio/scripts/provision_llama_cuda.sh index 8c593e7950..ef4c63bb50 100644 --- a/studio/scripts/provision_llama_cuda.sh +++ b/studio/scripts/provision_llama_cuda.sh @@ -13,9 +13,9 @@ LLAMA_DIR="${UNSLOTH_LLAMA_CPP_PATH:-$HOME/.unsloth/llama.cpp}" SERVER="$LLAMA_DIR/build/bin/llama-server" log() { printf ' - %s\n' "$*"; } -# CUDA shows up two ways: old monolithic (libggml-cuda in ldd) or current split -# build (dlopen-ed libggml-cuda.so* beside the binary, missed by ldd). CPU-only -# builds ship no libggml-cuda.so, so its presence is the reliable signal. +# CUDA shows up two ways: monolithic (libggml-cuda in ldd) or split (dlopen-ed +# libggml-cuda.so* beside the binary, missed by ldd). CPU-only builds ship no +# libggml-cuda.so, so its presence is the reliable signal. is_cuda_server() { [ -x "$1" ] || return 1 ldd "$1" 2>/dev/null | grep -qi 'libggml-cuda' && return 0 @@ -165,8 +165,8 @@ if ! _cmake_configure; then _cmake_configure || { log "cmake configure failed"; cd /; _restore_prev; exit 0; } fi # Also builds the targets unsloth-zoo's GGUF exporter needs (llama-mtmd-cli, -# llama-gguf-split). Jobs default to ~half the cores -- full -j(nproc) CUDA builds -# trip thermal shutdowns on NVIDIA-ARM laptops (N1X "RTX Spark") -- and are +# llama-gguf-split). Jobs default to ~half the cores (full -j(nproc) CUDA builds +# trip thermal shutdowns on NVIDIA-ARM laptops like the N1X "RTX Spark") and are # RAM-capped (~1.5 GB/nvcc job). Tune: UNSLOTH_LLAMA_BUILD_JOBS=N; re-runs resume. _ncpu="$(nproc 2>/dev/null || echo 4)" # Honor a valid positive-int override; ignore junk/0 (cmake reads -j0 as "all cores"). @@ -198,7 +198,7 @@ _cmake_build_extras() { done } if ! _cmake_build; then - # An interrupted build (thermal/power shutdown -- this machine class is prone) + # An interrupted build (thermal/power shutdown, common on this machine class) # can leave a half-linked libggml-cuda.so that breaks the resume link # (undefined ggml_cuda_op_* refs); wipe and rebuild clean once. log "build failed (likely interrupted/partial); wiping build dir and rebuilding clean" diff --git a/studio/setup.sh b/studio/setup.sh index 3a55c1607a..c8d9a630d8 100755 --- a/studio/setup.sh +++ b/studio/setup.sh @@ -974,9 +974,9 @@ LLAMA_CPP_DIR="$UNSLOTH_HOME/llama.cpp" LLAMA_SERVER_BIN="$LLAMA_CPP_DIR/build/bin/llama-server" _NEED_LLAMA_SOURCE_BUILD=false _LLAMA_CPP_DEGRADED=false -# Deferred != degraded: on WSL2 aarch64+NVIDIA install.ps1 builds the real CUDA -# server in the background, so a temporarily-absent server is success and must not -# trip the arm64 CPU-prebuilt last-resort or the exit 1. +# Deferred != degraded: on WSL2 aarch64+NVIDIA install.ps1 builds the CUDA server +# in the background, so an absent server is success and must not trip the arm64 +# CPU-prebuilt last-resort or exit 1. _LLAMA_CPP_DEFERRED=false _LLAMA_FORCE_COMPILE="${UNSLOTH_LLAMA_FORCE_COMPILE:-0}" _REQUESTED_LLAMA_TAG="${UNSLOTH_LLAMA_TAG:-${_DEFAULT_LLAMA_TAG}}" @@ -1161,10 +1161,10 @@ if [ "$_NEED_LLAMA_SOURCE_BUILD" = true ] && \ fi # ── WSL2 aarch64 + NVIDIA, no nvcc yet: defer to the background CUDA build ── -# install.ps1 builds the real CUDA llama-server in the background after install; -# without nvcc, section 9 could only make a slow CPU server that build discards. -# With nvcc we fall through to section 9; opted out (UNSLOTH_NO_LLAMA_CUDA=1) the -# CPU build is kept as the only server. +# install.ps1 builds the CUDA llama-server in the background; without nvcc, +# section 9 could only make a slow CPU server that build discards. With nvcc we +# fall through to section 9; opted out (UNSLOTH_NO_LLAMA_CUDA=1) the CPU build is +# kept as the only server. if [ "$_NEED_LLAMA_SOURCE_BUILD" = true ] \ && [ "$_LLAMA_FORCE_COMPILE" != "1" ] \ && [ -z "$_LLAMA_PR" ] \ @@ -1454,10 +1454,10 @@ else if [ "$_CUDA_TOOLKIT_ALLOWED" = true ]; then CMAKE_ARGS="$CMAKE_ARGS -DGGML_CUDA=ON" - # glibc >= 2.41 + CUDA < 13.3: rsqrt/rsqrtf header clash fails every .cu - # ("exception specification is incompatible") -> CPU fallback; only fix is - # CUDA >= 13.3. Diagnostic only -- never changes flags or aborts. Checked - # against the final _NVCC_VER (after the driver-compat swap above). + # glibc >= 2.41 + CUDA < 13.3: rsqrt/rsqrtf header clash fails + # every .cu -> CPU fallback; only fix is CUDA >= 13.3. Diagnostic + # only (never changes flags or aborts). Checks the final _NVCC_VER + # (after the driver-compat swap above). _GLIBC_VER="$(getconf GNU_LIBC_VERSION 2>/dev/null | awk '{print $2}')" || _GLIBC_VER="" if [ -n "$_GLIBC_VER" ]; then _GLIBC_MAJ="${_GLIBC_VER%%.*}"; _GLIBC_MIN="${_GLIBC_VER#*.}"; _GLIBC_MIN="${_GLIBC_MIN%%.*}" @@ -1683,12 +1683,11 @@ fi # end _SKIP_GGUF_BUILD check # llama.cpp when the source build above could not (no CUDA toolkit found) ── # No aarch64+CUDA prebuilt exists and a fresh Spark ships only driver + nvidia-smi, # so the build above fell back to CPU; mirror the Windows/WSL fix -# (provision_llama_cuda.sh) for native Linux. Gated to Linux aarch64 + NVIDIA with -# no CUDA server yet (opt out: UNSLOTH_NO_LLAMA_CUDA=1); best-effort -- provision -# always exits 0 and on failure the prior CPU/degraded state stands. -# CUDA detection covers both layouts: old monolithic (libggml-cuda in ldd) and -# split build (dlopen-ed libggml-cuda.so* beside the binary, missed by ldd -- -# CPU-only builds ship no libggml-cuda.so, so its presence is the signal). +# (provision_llama_cuda.sh) for native Linux. Best-effort: provision always exits +# 0, and on failure the prior CPU/degraded state stands. +# CUDA detection covers both layouts: monolithic (libggml-cuda in ldd) and split +# (dlopen-ed libggml-cuda.so* beside the binary, missed by ldd). CPU-only builds +# ship no libggml-cuda.so, so its presence is the signal. _have_cuda_llama_server() { [ -x "$LLAMA_SERVER_BIN" ] || return 1 ldd "$LLAMA_SERVER_BIN" 2>/dev/null | grep -qi 'libggml-cuda' && return 0 @@ -1702,11 +1701,11 @@ if [ "$_HOST_SYSTEM" = "Linux" ] \ && command -v nvidia-smi >/dev/null 2>&1 \ && nvidia-smi -L 2>/dev/null | awk '/^GPU[[:space:]]+[0-9]+:/{found=1} END{exit !found}' \ && ! _have_cuda_llama_server; then - # Under WSL this runs ONLY for a DIRECT `install.sh` run: install.ps1 exports - # UNSLOTH_WSL_LLAMA_DEFERRED=1 and builds CUDA llama.cpp in the background, but - # a direct run has no background builder, so provision here. - # Resolve provision_llama_cuda.sh: copy beside setup.sh, then local-dev repo, - # else fetch from GitHub (so `curl | sh` works on an older wheel without it). + # Under WSL this runs ONLY for a DIRECT `install.sh` run: install.ps1 sets + # UNSLOTH_WSL_LLAMA_DEFERRED=1 and builds in the background, but a direct run + # has no background builder, so provision here. + # Resolve provision_llama_cuda.sh: beside setup.sh, then local-dev repo, else + # fetch from GitHub (so `curl | sh` works on an older wheel without it). _PROV_SH="" if [ -f "$SCRIPT_DIR/scripts/provision_llama_cuda.sh" ]; then _PROV_SH="$SCRIPT_DIR/scripts/provision_llama_cuda.sh" diff --git a/unsloth/kernels/flex_attention.py b/unsloth/kernels/flex_attention.py index 044e8911f2..8cd1e82141 100644 --- a/unsloth/kernels/flex_attention.py +++ b/unsloth/kernels/flex_attention.py @@ -27,10 +27,9 @@ torch_compile_options = { def _flex_is_dgx_spark(): - # Local CUDA-free copy of _utils._is_dgx_spark_no_cuda_init() (avoids a circular - # import). Runs at module import, before ._utils -- touching torch.cuda here would - # init the allocator before patch_dgx_spark_memory_config() can set - # PYTORCH_CUDA_ALLOC_CONF on the very Spark hosts this targets. + # CUDA-free copy of _utils._is_dgx_spark_no_cuda_init() (avoids a circular import). + # Runs at module import, before ._utils -- touching torch.cuda here would init the + # allocator before patch_dgx_spark_memory_config() can set PYTORCH_CUDA_ALLOC_CONF. _force = os.environ.get("UNSLOTH_FORCE_DGX_SPARK") if _force == "1": return True diff --git a/unsloth/models/_utils.py b/unsloth/models/_utils.py index ab7f19133c..7cbe5e1aba 100644 --- a/unsloth/models/_utils.py +++ b/unsloth/models/_utils.py @@ -993,8 +993,8 @@ from transformers.modeling_utils import logger as transformers_logger # ---- NVIDIA DGX Spark (GB10) / N1X "RTX Spark" unified-memory support ---- -# Device names vary ("NVIDIA GB10" on DGX Spark, "JMJWOA-Generic-GPU" on N1X); -# the aarch64 + CUDA gate keeps every Spark workaround a no-op elsewhere. +# Device names vary ("NVIDIA GB10", "JMJWOA-Generic-GPU" on N1X); the +# aarch64 + CUDA gate keeps every Spark workaround a no-op elsewhere. _DGX_SPARK_DEVICE_TOKENS = ("GB10", "JMJWOA", "N1X", "DGX SPARK", "GB110") @@ -1024,9 +1024,8 @@ def is_dgx_spark(): @functools.lru_cache(maxsize = None) def _is_dgx_spark_no_cuda_init(): - """Spark detection that never initializes CUDA: reads device names via - `nvidia-smi` instead of torch, so allocator-init-time settings - (PYTORCH_CUDA_ALLOC_CONF) can still be set after calling it. Same + """Spark detection that never inits CUDA: reads device names via `nvidia-smi`, + not torch, so PYTORCH_CUDA_ALLOC_CONF can still be set afterwards. Same UNSLOTH_FORCE_DGX_SPARK override; False on any error.""" _force = os.environ.get("UNSLOTH_FORCE_DGX_SPARK") if _force == "1": @@ -1055,10 +1054,10 @@ def _is_dgx_spark_no_cuda_init(): def patch_dgx_spark_caching_allocator_warmup(): """No-op `transformers.modeling_utils.caching_allocator_warmup` on Spark UMA. - `cudaMemGetInfo()` undercounts free memory on Spark unified memory, so HF's - warmup `torch.empty(...)` raises `AcceleratorError: invalid argument` and - aborts bitsandbytes 4/8-bit loads. The warmup is only a speed hint, so skip - it. Gated by `is_dgx_spark()`; idempotent (`_unsloth_spark_noop` marker). + `cudaMemGetInfo()` undercounts free UMA memory, so HF's warmup + `torch.empty(...)` raises `AcceleratorError: invalid argument` and aborts + bitsandbytes 4/8-bit loads. The warmup is only a speed hint, so skip it. + Gated by `is_dgx_spark()`; idempotent (`_unsloth_spark_noop` marker). """ if not is_dgx_spark(): return @@ -1085,7 +1084,7 @@ def patch_dgx_spark_memory_config(): Appends to PYTORCH_CUDA_ALLOC_CONF only when absent; opt out with UNSLOTH_NO_EXPANDABLE_SEGMENTS=1. Must run before the first CUDA allocation, hence the CUDA-free `_is_dgx_spark_no_cuda_init()` gate -- `is_dgx_spark()` - would initialize the allocator before the env var could take effect. + would init the allocator before the env var could take effect. """ if not _is_dgx_spark_no_cuda_init(): return @@ -1104,7 +1103,7 @@ def patch_dgx_spark_runtime_defaults(): - UNSLOTH_DISABLE_DOUBLE_BUFFER=1 (setdefault): zoo's grad-checkpointing double-buffer gates on a mem_get_info check that UNDERCOUNTS on UMA, and - its extra staging buffer is pure waste on a shared pool. + its staging buffer is pure waste on a shared pool. - UNSLOTH_SPARK_MEM_FRACTION=<0..1> (opt-in, default NO cap): caps the allocator so over-allocation raises OutOfMemoryError early instead of wedging the box (untracked UMA allocations may never trip a catchable OOM). @@ -1126,9 +1125,9 @@ def patch_dgx_spark_runtime_defaults(): def patch_dgx_spark_dataloader_defaults(): """Default `dataloader_pin_memory` to False on Spark UMA (accuracy-neutral). - With one shared memory pool, pinning only reserves non-pageable RAM and adds - a staging copy (mirrors transformers' own use_cpu precedent). Wrapping the - base `TrainingArguments.__post_init__` covers SFT + every TRL trainer in one + On one shared pool, pinning only reserves non-pageable RAM and adds a staging + copy (mirrors transformers' own use_cpu precedent). Wrapping the base + `TrainingArguments.__post_init__` covers SFT + every TRL trainer in one idempotent patch. Opt out: UNSLOTH_SPARK_KEEP_PIN_MEMORY=1. No-op off-Spark. """ if not is_dgx_spark(): From 4cebfabaa686be2fa46212a5680a6562e56ce77f Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Thu, 11 Jun 2026 22:21:46 -0700 Subject: [PATCH 60/95] fix(spark): whole-token device-name match so GB10 != GB100/GB10X A loose substring match ("GB10" in name) misdetected a discrete Grace+Blackwell datacenter GPU (e.g. nvidia-smi name containing "GB100") as a unified-memory DGX Spark, applying the UMA tuning (pin_memory off, vLLM disabled, allocator capped to 0.80) and regressing that hardware. Match each device-name token with non-alphanumeric boundaries instead. Found by a platform x device-name gating simulation; the real N1X (JMJWOA-Generic-GPU) still detects, GB100/B100/GB200/GH200/B200 now correctly reject. Co-Authored-By: Claude Opus 4.8 --- studio/backend/core/training/worker.py | 8 ++++++-- unsloth/kernels/flex_attention.py | 7 ++++++- unsloth/models/_utils.py | 14 ++++++++++++-- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/studio/backend/core/training/worker.py b/studio/backend/core/training/worker.py index 0d1e51343d..f52dfc48f1 100644 --- a/studio/backend/core/training/worker.py +++ b/studio/backend/core/training/worker.py @@ -749,8 +749,10 @@ def _nvidia_classify_spark_unified_memory(props: Any) -> tuple[str, bool]: if getattr(props, "is_integrated", 0): return "is_integrated", True name_upper = (getattr(props, "name", "") or "").upper() + import re for token in ("GB10", "GB110", "JMJWOA", "N1X", "DGX SPARK"): - if token in name_upper: + # Whole-token match so "GB10" does not match a discrete "GB100"/"GB10X". + if re.search(r"(? timeout = 5, ) _names_u = (_smi.stdout or "").upper() + import re as _re _spark_smi = any( - t in _names_u for t in ("GB10", "GB110", "JMJWOA", "N1X", "DGX SPARK") + _re.search(r"(? Date: Fri, 12 Jun 2026 05:22:45 +0000 Subject: [PATCH 61/95] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- studio/backend/core/training/worker.py | 2 ++ unsloth/kernels/flex_attention.py | 1 + 2 files changed, 3 insertions(+) diff --git a/studio/backend/core/training/worker.py b/studio/backend/core/training/worker.py index f52dfc48f1..77628e4a44 100644 --- a/studio/backend/core/training/worker.py +++ b/studio/backend/core/training/worker.py @@ -750,6 +750,7 @@ def _nvidia_classify_spark_unified_memory(props: Any) -> tuple[str, bool]: return "is_integrated", True name_upper = (getattr(props, "name", "") or "").upper() import re + for token in ("GB10", "GB110", "JMJWOA", "N1X", "DGX SPARK"): # Whole-token match so "GB10" does not match a discrete "GB100"/"GB10X". if re.search(r"(? ) _names_u = (_smi.stdout or "").upper() import re as _re + _spark_smi = any( _re.search(r"(? Date: Thu, 11 Jun 2026 23:15:22 -0700 Subject: [PATCH 62/95] fix(uninstall): sweep legacy distro-suffixed Studio shortcuts The Windows uninstaller removed only the exact name "Unsloth Studio.lnk", orphaning legacy "Unsloth Studio (WSL - ).lnk" shortcuts left by pre-release dev builds. Glob "Unsloth Studio (*.lnk" across Desktop + Start Menu so the documented "remove the shortcuts" contract holds regardless of suffix. Validated on N1X: both canonical and suffixed .lnk removed. Co-Authored-By: Claude Opus 4.8 (1M context) --- scripts/uninstall.ps1 | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/scripts/uninstall.ps1 b/scripts/uninstall.ps1 index 287531fd71..81f4dd58cb 100644 --- a/scripts/uninstall.ps1 +++ b/scripts/uninstall.ps1 @@ -341,13 +341,17 @@ function Uninstall-UnslothStudio { } # ── Remove desktop and Start Menu shortcuts ── + # Canonical name is "Unsloth Studio.lnk"; also sweep legacy distro-suffixed + # names ("Unsloth Studio (WSL - ).lnk") left by pre-release dev builds. _Step "Removing desktop and Start Menu shortcuts..." - try { - $desktop = [Environment]::GetFolderPath("Desktop") - if ($desktop) { _RemovePath (Join-Path $desktop "Unsloth Studio.lnk") } - } catch { } - if ($env:APPDATA) { - _RemovePath (Join-Path $env:APPDATA "Microsoft\Windows\Start Menu\Programs\Unsloth Studio.lnk") + $shortcutDirs = @() + try { $d = [Environment]::GetFolderPath("Desktop"); if ($d) { $shortcutDirs += $d } } catch { } + if ($env:APPDATA) { $shortcutDirs += (Join-Path $env:APPDATA "Microsoft\Windows\Start Menu\Programs") } + foreach ($dir in $shortcutDirs) { + if (-not (Test-Path -LiteralPath $dir)) { continue } + _RemovePath (Join-Path $dir "Unsloth Studio.lnk") + Get-ChildItem -LiteralPath $dir -Filter "Unsloth Studio (*.lnk" -ErrorAction SilentlyContinue | + ForEach-Object { _RemovePath $_.FullName } } # Invalidate the Win11 Start Menu tile cache so the removed shortcut's tile # disappears promptly instead of lingering stale (mirrors install.ps1's From d888184da48e945026d57297ed9ca6be628e22e1 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Thu, 11 Jun 2026 23:59:29 -0700 Subject: [PATCH 63/95] fix(install): address Codex review on WoA WSL fallback - install.ps1: force WSL2 (`wsl --set-default-version 2`) before installing a NEW distro, so a host whose default is WSL1 doesn't get a GPU-less distro that fails only at torch.cuda (the pre-existing-distro branch already probes/converts). - install.ps1: forward `UNSLOTH_PYTHON` into the WSL install (install.sh reads it; a Windows env var isn't visible inside WSL otherwise). Numeric-only guard rejects shell injection. - install.ps1: add sqlite-vec / pymupdf / python-docx to the cut-short-install server-deps repair so RAG/knowledge-base features aren't left broken. - uninstall.sh: gate the Windows %LOCALAPPDATA%\Unsloth shim removal on the current distro owning the fallback (wsl-distro.txt), so uninstalling Studio from a different WSL distro no longer breaks the still-installed shim. Disproved (no change): worker.py Spark name match is already whole-token (commit 4cebfab, not substring); the shim's non-login WSL exec DOES have /usr/lib/wsl/lib on PATH (nvidia-smi resolves -> Spark detector returns True), verified on N1X. Co-Authored-By: Claude Opus 4.8 (1M context) --- install.ps1 | 9 ++++++++- scripts/uninstall.sh | 8 ++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/install.ps1 b/install.ps1 index af51ae3604..b2486ad67d 100644 --- a/install.ps1 +++ b/install.ps1 @@ -1851,6 +1851,10 @@ shell.Run cmd, 0, False try { & wsl.exe -d $distro -- true *> $null; if ($LASTEXITCODE -eq 0) { $haveDistro = $true } } catch {} if (-not $haveDistro) { substep "installing WSL distro '$distro' (first time only)..." "Cyan" + # New distros install at the global default WSL version; force 2 so a host + # whose default is WSL1 doesn't get a GPU-less distro (fails only at torch.cuda). + $global:LASTEXITCODE = -1 + try { & wsl.exe --set-default-version 2 *> $null } catch {} try { & wsl.exe --install -d $distro --no-launch } catch {} } else { # A PRE-EXISTING distro may be WSL1 (no GPU passthrough; would only fail at the final @@ -1884,6 +1888,9 @@ shell.Run cmd, 0, False # setup.sh would defer to a background builder that never starts (no llama-server). $_fwdEnv = '' if ($env:UNSLOTH_NO_LLAMA_CUDA -eq '1') { $_fwdEnv = 'export UNSLOTH_NO_LLAMA_CUDA=1; ' } + # Forward a user Python pin: install.sh reads UNSLOTH_PYTHON, but a Windows env var + # isn't visible inside WSL unless bridged. Numeric-only guard (e.g. 3.12) = no injection. + if ($env:UNSLOTH_PYTHON -and ($env:UNSLOTH_PYTHON -match '^[0-9][0-9.]*$')) { $_fwdEnv += "export UNSLOTH_PYTHON=$($env:UNSLOTH_PYTHON); " } if ($_instRef -eq 'main') { $wslInstall = $_fwdEnv + 'export DEBIAN_FRONTEND=noninteractive UNSLOTH_WSL_LLAMA_DEFERRED=1; apt-get update -y >/dev/null; apt-get install -y build-essential cmake git curl pciutils libcurl4-openssl-dev >/dev/null; curl -fsSL https://unsloth.ai/install.sh | sh' } else { @@ -1927,7 +1934,7 @@ shell.Run cmd, 0, False # studio.txt minus the huggingface-hub pin; uv preferred, pip fallback. Bare names only: # `>=` would become a redirection through PowerShell -> wsl.exe -> bash -lc, and # latest-of-each satisfies the studio.txt minimums anyway. - $_deps = 'typer fastapi uvicorn matplotlib pandas nest_asyncio pyjwt easydict addict structlog diceware ddgs cryptography httpx fastmcp' + $_deps = 'typer fastapi uvicorn matplotlib pandas nest_asyncio pyjwt easydict addict structlog diceware ddgs cryptography httpx fastmcp sqlite-vec pymupdf python-docx' $_repair = 'PY=/root/.unsloth/studio/unsloth_studio/bin/python; UV="$(command -v uv 2>/dev/null || echo /root/.local/bin/uv)"; if [ -x "$UV" ] || command -v uv >/dev/null 2>&1; then "$UV" pip install --python "$PY" ' + $_deps + '; else "$PY" -m pip install ' + $_deps + '; fi' $prevEapR = $ErrorActionPreference; $ErrorActionPreference = "Continue" try { & wsl.exe -d $distro --cd /root -u root -- bash -lc $_repair } catch {} finally { $ErrorActionPreference = $prevEapR } diff --git a/scripts/uninstall.sh b/scripts/uninstall.sh index 20285da856..ab02c67974 100755 --- a/scripts/uninstall.sh +++ b/scripts/uninstall.sh @@ -305,9 +305,13 @@ case "$_os" in } # Remove the WoA WSL-fallback native shim/launcher dir # (%LOCALAPPDATA%\Unsloth) + its PATH entry that install.ps1 - # created, so a WSL-side bash uninstall is complete. + # created, so a WSL-side bash uninstall is complete. Only when THIS + # distro owns the fallback (wsl-distro.txt) -- else uninstalling a + # different distro would break the still-installed shim. $ud = if ($env:LOCALAPPDATA) { Join-Path $env:LOCALAPPDATA "Unsloth" } else { $null }; - if ($ud) { + $owner = $null; + if ($ud) { $of = Join-Path $ud "wsl-distro.txt"; if (Test-Path -LiteralPath $of) { $owner = (Get-Content -LiteralPath $of | Select-Object -First 1).Trim() } } + if ($ud -and ((-not $owner) -or (-not $distro) -or ($owner -ieq $distro))) { $shim = (Join-Path $ud "bin").TrimEnd("\","/"); $up = [Environment]::GetEnvironmentVariable("Path","User"); if ($up) { [Environment]::SetEnvironmentVariable("Path", (($up -split ";" | Where-Object { $_ -and ($_.TrimEnd("\","/") -ine $shim) }) -join ";"), "User") } From b50eb8bc7182e883ef1165848b4c8f9bda5c0888 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Fri, 12 Jun 2026 08:25:40 +0000 Subject: [PATCH 64/95] Tighten and trim code comments --- install.ps1 | 86 ++++++++++---------- install.sh | 16 ++-- scripts/uninstall.ps1 | 22 ++--- scripts/uninstall.sh | 5 +- studio/backend/core/training/worker.py | 21 +++-- studio/backend/tests/test_spark_oom_guard.py | 11 ++- studio/scripts/provision_llama_cuda.sh | 30 +++---- studio/setup.sh | 22 ++--- unsloth/kernels/flex_attention.py | 10 +-- unsloth/models/_utils.py | 35 ++++---- 10 files changed, 125 insertions(+), 133 deletions(-) diff --git a/install.ps1 b/install.ps1 index b2486ad67d..eba41dfbe2 100644 --- a/install.ps1 +++ b/install.ps1 @@ -48,8 +48,7 @@ function Install-UnslothStudio { } } - # raw.githubusercontent.com ref for install assets (provision_llama_cuda.sh, .ico). - # UNSLOTH_INSTALL_REF overrides 'main' for pre-merge testing. + # raw.githubusercontent.com ref for install assets; UNSLOTH_INSTALL_REF overrides 'main' for pre-merge testing. function Get-UnslothInstallRef { if ($env:UNSLOTH_INSTALL_REF -and $env:UNSLOTH_INSTALL_REF.Trim()) { return $env:UNSLOTH_INSTALL_REF.Trim() } return 'main' @@ -97,8 +96,8 @@ function Install-UnslothStudio { if ($TauriMode) { exit $Code } - # -File ignores $LASTEXITCODE on plain return, so `exit` must carry the code; - # under `irm | iex` (no $PSCommandPath) `exit` would kill the user's shell. + # -File ignores $LASTEXITCODE on plain return, so `exit` carries the code; under + # `irm | iex` (no $PSCommandPath) `exit` would kill the user's shell, so set the var. if ($PSCommandPath) { exit $Code } @@ -1773,9 +1772,9 @@ shell.Run cmd, 0, False $TorchIndexUrl = Get-TorchIndexUrl # ===== Windows-on-ARM + NVIDIA GPU -> automatic WSL2 fallback (N1X "RTX Spark" / DGX Spark-class) ===== - # win_arm64 has no CUDA PyTorch/Triton wheel, so run the Linux installer inside WSL2 (full GPU) plus - # a Windows `unsloth` shim that forwards into it. x86_64 / ARM64-without-NVIDIA unaffected; the probe - # below keeps the native install if a win_arm64 CUDA torch wheel ever ships. + # win_arm64 has no CUDA PyTorch/Triton wheel, so run the Linux installer inside WSL2 (full GPU) plus a + # Windows `unsloth` shim forwarding into it; x86_64 / ARM64-without-NVIDIA unaffected, and the probe + # below keeps the native install if a win_arm64 CUDA wheel ever ships. # Opt out: UNSLOTH_NO_WSL_FALLBACK=1; pick distro with UNSLOTH_WSL_DISTRO. try { $_winArm64 = ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString() -ieq 'Arm64') } catch { $_winArm64 = $false } # x64-emulated PS on ARM reports X64/AMD64; Win32_Processor.Architecture (12=ARM64) and machine-level @@ -1791,11 +1790,11 @@ shell.Run cmd, 0, False } $_nativeCudaTorchOk = $false if ($_winArm64 -and $HasNvidiaSmi -and (-not $SkipTorch)) { - # Probe with the SAME spec as the real install ("torch>=2.4,<2.11.0"): a bare `torch` probe - # could match an out-of-range wheel, skipping WSL only to fail the real pinned install. + # Probe the SAME spec as the real install ("torch>=2.4,<2.11.0"); a bare `torch` probe could + # match an out-of-range wheel, skipping WSL only to fail the real pinned install. $prevEapProbe = $ErrorActionPreference; $ErrorActionPreference = "Continue" - # --reinstall: an installed (e.g. CPU-only) torch mustn't satisfy the probe -- it must - # prove a native win_arm64 CUDA wheel exists on the index. + # --reinstall: an installed (e.g. CPU-only) torch mustn't satisfy the probe -- it must prove + # a native win_arm64 CUDA wheel exists on the index. $global:LASTEXITCODE = -1 try { & uv pip install --python $VenvPython --dry-run --reinstall "torch>=2.4,<2.11.0" --index-url $TorchIndexUrl *> $null @@ -1807,8 +1806,8 @@ shell.Run cmd, 0, False step "wsl" "Windows on ARM + NVIDIA, native CUDA unavailable -- routing GPU setup through WSL2" substep "no win_arm64 CUDA PyTorch/Triton yet; WSL2 delivers full GPU (DGX Spark / RTX Spark path)." "Yellow" - # The Tauri desktop app launches its backend from a Windows venv (resolve_backend_binary), - # not WSL, so a WSL-only install would start nothing -- send those users to the CLI installer. + # The Tauri desktop app launches its backend from a Windows venv (resolve_backend_binary), not + # WSL, so a WSL-only install would start nothing -- send those users to the CLI installer. if ($TauriMode) { return (Exit-InstallFailure "Windows-on-ARM + NVIDIA GPU needs the WSL2 GPU install, which the desktop app can't launch yet. Install from PowerShell instead: irm https://unsloth.ai/install.ps1 | iex" 1) } @@ -1833,8 +1832,8 @@ shell.Run cmd, 0, False substep "reboot, then re-run: irm https://unsloth.ai/install.ps1 | iex" "Cyan" } # Deferred until reboot: restore any rolled-aside previous venv and signal not-complete. - # `exit 1` for -File (plain return exits 0); under `irm | iex` (no $PSCommandPath) return - # instead, since exit would kill the user's shell. + # `exit 1` for -File (plain return exits 0); under `irm | iex` (no $PSCommandPath) return, + # since exit would kill the user's shell. Restore-StudioVenvRollback $global:LASTEXITCODE = 1 if ($PSCommandPath) { exit 1 } @@ -1843,7 +1842,7 @@ shell.Run cmd, 0, False $distro = if ($env:UNSLOTH_WSL_DISTRO) { $env:UNSLOTH_WSL_DISTRO } else { "Ubuntu-24.04" } # For cmd-context uses (.cmd shim, copy-paste hints): wsl.exe rejects a QUOTED space-free name - # (WSL_E_DISTRO_NOT_FOUND on 2.x) yet splits a bare spaced one after -d, so quote ONLY when spaced. + # (WSL_E_DISTRO_NOT_FOUND on 2.x) but splits a bare spaced one after -d, so quote ONLY when spaced. $_distroArg = if ($distro -match '\s') { '"' + $distro + '"' } else { $distro } # Detect the distro by exit code (encoding-proof; wsl --list emits UTF-16 that PS mis-parses). $haveDistro = $false @@ -1851,15 +1850,15 @@ shell.Run cmd, 0, False try { & wsl.exe -d $distro -- true *> $null; if ($LASTEXITCODE -eq 0) { $haveDistro = $true } } catch {} if (-not $haveDistro) { substep "installing WSL distro '$distro' (first time only)..." "Cyan" - # New distros install at the global default WSL version; force 2 so a host - # whose default is WSL1 doesn't get a GPU-less distro (fails only at torch.cuda). + # New distros install at the global default WSL version; force 2 so a WSL1-default + # host doesn't get a GPU-less distro (would fail only at torch.cuda). $global:LASTEXITCODE = -1 try { & wsl.exe --set-default-version 2 *> $null } catch {} try { & wsl.exe --install -d $distro --no-launch } catch {} } else { - # A PRE-EXISTING distro may be WSL1 (no GPU passthrough; would only fail at the final + # A PRE-EXISTING distro may be WSL1 (no GPU passthrough; would fail only at the final # torch.cuda check). Detect from inside (encoding-proof, unlike UTF-16 `wsl -l -v`) and - # convert in place -- `wsl --set-version` preserves files. Fresh installs default to WSL2. + # convert in place -- `wsl --set-version` preserves files. $_wsl2Probe = 'grep -qiE ''microsoft-standard|WSL2'' /proc/version 2>/dev/null || test -e /usr/lib/wsl/lib/libcuda.so' $_isWsl2 = $false $global:LASTEXITCODE = -1 @@ -1881,15 +1880,14 @@ shell.Run cmd, 0, False # Non-main ref: fetch + export THAT ref so the WSL venv gets the branch's setup.sh + patches # (else install.sh pulls PyPI unsloth). main == plain unsloth.ai/install.sh. $_instRef = Get-UnslothInstallRef - # UNSLOTH_WSL_LLAMA_DEFERRED=1: setup.sh skips its foreground CUDA llama.cpp build since we build - # it in the background (a DIRECT install.sh run in WSL doesn't set it). apt stderr stays visible - # (only stdout -> /dev/null) so network/repo failures are diagnosable. - # Forward UNSLOTH_NO_LLAMA_CUDA into WSL: it also skips the dispatch below, so unforwarded - # setup.sh would defer to a background builder that never starts (no llama-server). + # UNSLOTH_WSL_LLAMA_DEFERRED=1: setup.sh skips its foreground CUDA llama.cpp build since we build it + # in the background. apt stderr stays visible (only stdout -> /dev/null) so failures are diagnosable. + # Forward UNSLOTH_NO_LLAMA_CUDA into WSL: it also skips the dispatch below, so unforwarded setup.sh + # would defer to a background builder that never starts (no llama-server). $_fwdEnv = '' if ($env:UNSLOTH_NO_LLAMA_CUDA -eq '1') { $_fwdEnv = 'export UNSLOTH_NO_LLAMA_CUDA=1; ' } - # Forward a user Python pin: install.sh reads UNSLOTH_PYTHON, but a Windows env var - # isn't visible inside WSL unless bridged. Numeric-only guard (e.g. 3.12) = no injection. + # Forward a user Python pin (install.sh reads UNSLOTH_PYTHON, but Windows env vars don't cross + # into WSL unless bridged). Numeric-only guard (e.g. 3.12) prevents injection. if ($env:UNSLOTH_PYTHON -and ($env:UNSLOTH_PYTHON -match '^[0-9][0-9.]*$')) { $_fwdEnv += "export UNSLOTH_PYTHON=$($env:UNSLOTH_PYTHON); " } if ($_instRef -eq 'main') { $wslInstall = $_fwdEnv + 'export DEBIAN_FRONTEND=noninteractive UNSLOTH_WSL_LLAMA_DEFERRED=1; apt-get update -y >/dev/null; apt-get install -y build-essential cmake git curl pciutils libcurl4-openssl-dev >/dev/null; curl -fsSL https://unsloth.ai/install.sh | sh' @@ -1897,7 +1895,7 @@ shell.Run cmd, 0, False $wslInstall = $_fwdEnv + 'export DEBIAN_FRONTEND=noninteractive UNSLOTH_WSL_LLAMA_DEFERRED=1; export UNSLOTH_INSTALL_REF=' + $_instRef + '; apt-get update -y >/dev/null; apt-get install -y build-essential cmake git curl pciutils libcurl4-openssl-dev >/dev/null; curl -fsSL https://raw.githubusercontent.com/unslothai/unsloth/' + $_instRef + '/install.sh | sh' } # install.sh may exit non-zero on the optional llama.cpp prebuilt step (no aarch64 prebuilt) - # though torch + unsloth + Studio still install; lower EAP so it doesn't abort under Stop. + # even though torch + unsloth + Studio install; lower EAP so it doesn't abort under Stop. $prevEapWsl = $ErrorActionPreference $ErrorActionPreference = "Continue" $global:LASTEXITCODE = -1 @@ -1918,9 +1916,9 @@ shell.Run cmd, 0, False & wsl.exe -d $distro --cd /root -u root -- /root/.unsloth/studio/unsloth_studio/bin/python -c "import torch,sys; sys.exit(0 if torch.cuda.is_available() else 3)" *> $null $torchOk = ($LASTEXITCODE -eq 0) } catch {} finally { $ErrorActionPreference = $prevEapChk } - # Self-heal web-server deps: a cut-short install.sh "studio deps" step leaves torch + unsloth - # but no fastapi/uvicorn/structlog/starlette (`unsloth studio` dies). Reinstall them unpinned - # (no huggingface-hub/transformers/datasets) so the verified GPU torch stack stays intact. + # Self-heal web-server deps: a cut-short install.sh "studio deps" step leaves torch + unsloth but + # no fastapi/uvicorn/structlog/starlette (`unsloth studio` dies). Reinstall them unpinned (no + # huggingface-hub/transformers/datasets) so the verified GPU torch stack stays intact. if ($torchOk) { $_studioPy = "/root/.unsloth/studio/unsloth_studio/bin/python" $_serverOk = $false @@ -1932,8 +1930,8 @@ shell.Run cmd, 0, False if (-not $_serverOk) { substep "Studio web-server deps incomplete (install.sh step cut short) -- installing them now..." "Cyan" # studio.txt minus the huggingface-hub pin; uv preferred, pip fallback. Bare names only: - # `>=` would become a redirection through PowerShell -> wsl.exe -> bash -lc, and - # latest-of-each satisfies the studio.txt minimums anyway. + # `>=` would become a redirection through PowerShell -> wsl.exe -> bash -lc, and latest-of-each + # satisfies the studio.txt minimums anyway. $_deps = 'typer fastapi uvicorn matplotlib pandas nest_asyncio pyjwt easydict addict structlog diceware ddgs cryptography httpx fastmcp sqlite-vec pymupdf python-docx' $_repair = 'PY=/root/.unsloth/studio/unsloth_studio/bin/python; UV="$(command -v uv 2>/dev/null || echo /root/.local/bin/uv)"; if [ -x "$UV" ] || command -v uv >/dev/null 2>&1; then "$UV" pip install --python "$PY" ' + $_deps + '; else "$PY" -m pip install ' + $_deps + '; fi' $prevEapR = $ErrorActionPreference; $ErrorActionPreference = "Continue" @@ -1946,8 +1944,8 @@ shell.Run cmd, 0, False if ($_serverOk) { substep "Studio web-server deps installed." "Green" } else { substep "(could not auto-install Studio server deps; 'unsloth studio' may fail to start)" "Yellow" } } - # The uv-managed venv ships no `pip`, but unsloth-zoo's exporter's check_pip() finds `uv pip` - # only when uv is on PATH. Seed pip so `save_pretrained_gguf` works regardless. + # The uv-managed venv ships no `pip`, but unsloth-zoo's check_pip() finds `uv pip` only + # when uv is on PATH. Seed pip so `save_pretrained_gguf` works regardless. $prevEapP = $ErrorActionPreference; $ErrorActionPreference = "Continue" try { & wsl.exe -d $distro --cd /root -u root -- $_studioPy -m pip --version *> $null @@ -1970,7 +1968,7 @@ shell.Run cmd, 0, False ) Set-Content -LiteralPath (Join-Path $shimDir "unsloth.cmd") -Value $shimLines -Encoding ASCII # Record the distro so the uninstaller can clean a custom UNSLOTH_WSL_DISTRO install - # without the env var set again. + # without the env var set. try { Set-Content -LiteralPath (Join-Path (Split-Path $shimDir -Parent) "wsl-distro.txt") -Value $distro -Encoding ASCII } catch {} # A fresh profile may have no HKCU 'Path'; null would make TrimEnd() throw. $userPath = [Environment]::GetEnvironmentVariable("Path", "User") @@ -2000,9 +1998,9 @@ shell.Run cmd, 0, False 'wsl.exe -d $distro --cd /root -u root -- bash -lic "unsloth studio -p 8888"' ) Set-Content -LiteralPath $launcher -Value $L -Encoding UTF8 - # Icon must live OUTSIDE %LOCALAPPDATA%: on WoA the sandboxed icon broker can't read a - # .ico under AppData\Local, so the shortcut renders BLANK; under the user profile it - # renders fine (verified on N1X). Only the icon moves. + # Icon must live OUTSIDE %LOCALAPPDATA%: on WoA the sandboxed icon broker can't read a .ico + # under AppData\Local, so the shortcut renders BLANK; under the user profile it renders + # fine (verified on N1X). Only the icon moves. $iconDir = Join-Path $env:USERPROFILE ".unsloth" New-Item -ItemType Directory -Force -Path $iconDir *> $null $icon = Join-Path $iconDir "unsloth.ico" @@ -2060,14 +2058,14 @@ shell.Run cmd, 0, False try { $_llamaUrl = "https://raw.githubusercontent.com/unslothai/unsloth/$(Get-UnslothInstallRef)/studio/scripts/provision_llama_cuda.sh" # Step 1: fetch the provision script + write a runner (base64 to dodge quoting layers). - # The runner restores PATH (non-login shells miss /usr/lib/wsl/lib nvidia-smi, so - # provision early-exits) and exports the env knobs below (Windows env vars don't cross - # into WSL). A runner FILE lets the detached launcher pass only space-free args, - # avoiding Start-Process mis-splitting `bash -lc `. + # The runner restores PATH (non-login shells miss /usr/lib/wsl/lib nvidia-smi, so provision + # early-exits) and exports the env knobs below (Windows env vars don't cross into WSL). A + # runner FILE lets the detached launcher pass only space-free args, avoiding Start-Process + # mis-splitting `bash -lc `. $_pathLine = 'export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/lib/wsl/lib:$PATH"' + "`n" $_jobsLine = if ($env:UNSLOTH_LLAMA_BUILD_JOBS) { "export UNSLOTH_LLAMA_BUILD_JOBS=$($env:UNSLOTH_LLAMA_BUILD_JOBS)`n" } else { "" } # Bridge UNSLOTH_LLAMA_TAG / UNSLOTH_LLAMA_PR pins into WSL, else the deferred build - # ignores them. sh-single-quoted (tags/PRs are simple tokens). + # ignores them. sh-single-quoted since tags/PRs are simple tokens. $_tagLine = if ($env:UNSLOTH_LLAMA_TAG) { "export UNSLOTH_LLAMA_TAG='$($env:UNSLOTH_LLAMA_TAG)'`n" } else { "" } $_prLine = if ($env:UNSLOTH_LLAMA_PR) { "export UNSLOTH_LLAMA_PR='$($env:UNSLOTH_LLAMA_PR)'`n" } else { "" } $_runner = "#!/usr/bin/env bash`n" + $_pathLine + $_jobsLine + $_tagLine + $_prLine + "exec bash /root/.unsloth/provision_llama_cuda.sh > /root/.unsloth/llama_cuda_build.log 2>&1`n" diff --git a/install.sh b/install.sh index 13d93531e7..2f8b3c0ff4 100755 --- a/install.sh +++ b/install.sh @@ -2648,10 +2648,9 @@ elif [ -n "$TORCH_INDEX_URL" ]; then --no-deps --reinstall-package unsloth-zoo \ "unsloth-zoo @ git+https://github.com/unslothai/unsloth-zoo" elif [ -n "${UNSLOTH_INSTALL_REF:-}" ] && [ "${UNSLOTH_INSTALL_REF}" != "main" ] && [ "$PACKAGE_NAME" = "unsloth" ]; then - # Pre-merge testing: install unsloth from a git ref (install.ps1 sets - # UNSLOTH_INSTALL_REF) so the branch's setup.sh + patches run. Name - # unsloth-zoo explicitly: it's not a base dep and SKIP_STUDIO_BASE skips - # base.txt, so otherwise it never installs. + # Pre-merge testing: install unsloth from a git ref (set by install.ps1) + # so the branch's setup.sh + patches run. Name unsloth-zoo explicitly -- + # not a base dep, and SKIP_STUDIO_BASE skips base.txt, so it never installs. substep "installing unsloth from git ref '$UNSLOTH_INSTALL_REF'..." run_install_cmd "install unsloth (@$UNSLOTH_INSTALL_REF)" uv pip install --python "$_VENV_PY" \ --upgrade-package unsloth --upgrade-package unsloth-zoo \ @@ -2660,11 +2659,10 @@ elif [ -n "$TORCH_INDEX_URL" ]; then run_install_cmd "install unsloth" uv pip install --python "$_VENV_PY" \ --upgrade-package unsloth -- "$PACKAGE_NAME" fi - # aarch64 + NVIDIA (DGX Spark / GB10 / N1X): unsloth's cuXXX extras are - # x86_64-oriented, so 4-bit QLoRA fails out of the box. aarch64 manylinux - # wheels work (verified on sm_121 via PTX JIT); best-effort, no wheel just - # keeps 16-bit LoRA / full finetuning. SKIP_TORCH gate: a --no-torch - # (GGUF-only) install must not let bitsandbytes drag torch back in. + # aarch64 + NVIDIA (DGX Spark / GB10 / N1X): unsloth's x86_64-oriented cuXXX + # extras break 4-bit QLoRA, but aarch64 manylinux wheels work (verified on + # sm_121 via PTX JIT). Best-effort: no wheel keeps 16-bit LoRA / full finetuning. + # SKIP_TORCH gate stops a --no-torch (GGUF-only) install dragging torch back in. if [ "$SKIP_TORCH" = false ] \ && { [ "$(uname -m)" = "aarch64" ] || [ "$(uname -m)" = "arm64" ]; } \ && command -v nvidia-smi >/dev/null 2>&1 \ diff --git a/scripts/uninstall.ps1 b/scripts/uninstall.ps1 index 81f4dd58cb..7ff9ee3f66 100644 --- a/scripts/uninstall.ps1 +++ b/scripts/uninstall.ps1 @@ -341,8 +341,8 @@ function Uninstall-UnslothStudio { } # ── Remove desktop and Start Menu shortcuts ── - # Canonical name is "Unsloth Studio.lnk"; also sweep legacy distro-suffixed - # names ("Unsloth Studio (WSL - ).lnk") left by pre-release dev builds. + # Canonical name is "Unsloth Studio.lnk"; also sweep legacy distro-suffixed names + # ("Unsloth Studio (WSL - ).lnk") left by pre-release dev builds. _Step "Removing desktop and Start Menu shortcuts..." $shortcutDirs = @() try { $d = [Environment]::GetFolderPath("Desktop"); if ($d) { $shortcutDirs += $d } } catch { } @@ -423,11 +423,11 @@ function Uninstall-UnslothStudio { # ── Windows-on-Arm WSL-fallback artifacts ── # The ARM64+NVIDIA fallback puts Studio in WSL plus a native shim + launcher under - # %LOCALAPPDATA%\Unsloth (not "Unsloth Studio") with a PATH entry -- all missed above. + # %LOCALAPPDATA%\Unsloth (not "Unsloth Studio") with a PATH entry -- none caught above. _Step "Removing WSL-fallback artifacts (shim, launcher, PATH entry, WSL install)..." $unslothDir = if ($env:LOCALAPPDATA) { Join-Path $env:LOCALAPPDATA "Unsloth" } else { $null } - # wsl-distro.txt records a custom UNSLOTH_WSL_DISTRO install so it's cleanable without the - # env var set; read it BEFORE the directory is removed below. + # wsl-distro.txt records a custom UNSLOTH_WSL_DISTRO install so it's cleanable without the env + # var set; read it BEFORE the directory is removed below. $_recordedDistro = $null if ($unslothDir) { try { @@ -466,17 +466,17 @@ function Uninstall-UnslothStudio { try { # Probe candidates by exit code ('' = default distro) since `wsl --list` emits UTF-16 PS # mis-parses. rm runs FIRST (the kills could SIGKILL this shell) and drops the dangling - # /root/.local/bin/unsloth symlink. Scope STRICTLY to /root (where the fallback installs); + # /root/.local/bin/unsloth symlink. Scope STRICTLY to /root (the fallback's install dir); # /home/*/.unsloth may be another user's. The 8888 kill is gated on an Unsloth install - # existing (checked BEFORE rm deletes the marker) so an unrelated listener survives; pkill + # existing (checked BEFORE rm deletes the marker) so an unrelated listener survives. pkill # matches argv containing /root/.unsloth/ (not bare names that would hit a user's own - # llama-server), and the backslash + [h]-bracket in '/root/\.unslot[h]/' keep it from - # matching this command's own argv. + # llama-server); the backslash + [h]-bracket in '/root/\.unslot[h]/' keep it from matching + # this command's own argv. $_clean = '_had=0; if [ -d /root/.unsloth ] || [ -L /root/.local/bin/unsloth ]; then _had=1; fi; rm -rf /root/.unsloth /root/llama-cuda /root/provision_llama_cuda.sh /root/llama_cuda_build.log 2>/dev/null; rm -f /root/.local/bin/unsloth 2>/dev/null; if [ $_had -eq 1 ]; then fuser -k 8888/tcp 2>/dev/null; fi; pkill -9 -f ''/root/\.unslot[h]/'' 2>/dev/null; true' # Clean only distros with evidence of a fallback install: the wsl-distro.txt marker or an # explicit UNSLOTH_WSL_DISTRO. The broad candidate probe is only for legacy marker-less - # installs, which exist only on ARM64 -- on x86 it would delete distros this installer - # never touched (e.g. a ROCm-on-WSL Studio under /root). + # installs (ARM64 only); on x86 it would delete distros this installer never touched + # (e.g. a ROCm-on-WSL Studio under /root). $_cands = @() if ($env:UNSLOTH_WSL_DISTRO) { $_cands += $env:UNSLOTH_WSL_DISTRO } if ($_recordedDistro) { $_cands += $_recordedDistro } diff --git a/scripts/uninstall.sh b/scripts/uninstall.sh index ab02c67974..e6ca561743 100755 --- a/scripts/uninstall.sh +++ b/scripts/uninstall.sh @@ -216,8 +216,7 @@ _remove_path "$HOME/.unsloth/studio" # by deleting it). No-op in env/custom mode (they nest under the custom root) and # when absent. A user-set UNSLOTH_LLAMA_CPP_PATH is intentionally kept. _remove_path "$HOME/.unsloth/llama.cpp" -# provision_llama_cuda.sh fetched by the WoA/Spark CUDA-build path (install.ps1 -# background build + direct-WSL setup.sh). No-op when absent. +# provision_llama_cuda.sh fetched by the WoA/Spark CUDA-build path. No-op when absent. _remove_path "$HOME/.unsloth/provision_llama_cuda.sh" _remove_path "$HOME/.unsloth/.cache" # llama.cpp atomic-install staging root (install_llama_prebuilt.py .staging). @@ -278,7 +277,7 @@ case "$_os" in # receive trailing tokens as $args. WSL distro names are safe to # embed (no quotes/$/backtick). # shellcheck disable=SC2016 - # $env:APPDATA/$distro are PowerShell-side; $_wsl_distro is injected from shell. + # $env:APPDATA/$distro are PowerShell-side; $_wsl_distro is shell-injected. powershell.exe -NoProfile -Command '$distro = "'"$_wsl_distro"'"; $dirs = @( [Environment]::GetFolderPath("Desktop"), diff --git a/studio/backend/core/training/worker.py b/studio/backend/core/training/worker.py index 77628e4a44..13d56e8c42 100644 --- a/studio/backend/core/training/worker.py +++ b/studio/backend/core/training/worker.py @@ -736,15 +736,15 @@ def _nvidia_classify_spark_unified_memory(props: Any) -> tuple[str, bool]: """Classify an NVIDIA device as Spark-class unified-memory or discrete. Returns ``(marker, is_unified)``; marker is ``"is_integrated"`` or the matched - device-name token, else ``""``. Spark-class parts (DGX Spark / GB10, N1X "RTX - Spark") share one memory pool with the OS, so like the ROCm APUs they need a + name token, else ``""``. Spark-class parts (DGX Spark / GB10, N1X "RTX Spark") + share one memory pool with the OS, so like the ROCm APUs they need a ``set_per_process_memory_fraction`` cap -- exhausting the pool can stall the box. ``is_integrated`` is authoritative on native Linux, but WSL2 paravirtualization masks it to 0 and renames the device (N1X reports ``JMJWOA-Generic-GPU``, - verified on hardware) -- hence the name-token fallback. Tokens mirror - ``_DGX_SPARK_DEVICE_TOKENS`` in ``unsloth/models/_utils.py`` (duplicated - because this guard runs before any ML import). + verified live) -- hence the name-token fallback. Tokens mirror + ``_DGX_SPARK_DEVICE_TOKENS`` in ``unsloth/models/_utils.py`` (duplicated since + this guard runs before any ML import). """ if getattr(props, "is_integrated", 0): return "is_integrated", True @@ -752,7 +752,7 @@ def _nvidia_classify_spark_unified_memory(props: Any) -> tuple[str, bool]: import re for token in ("GB10", "GB110", "JMJWOA", "N1X", "DGX SPARK"): - # Whole-token match so "GB10" does not match a discrete "GB100"/"GB10X". + # Whole-token match so "GB10" doesn't match discrete "GB100"/"GB10X". if re.search(r"(? # Discrete NVIDIA GPUs untouched. else: try: - # Set PYTORCH_CUDA_ALLOC_CONF before get_device_properties below inits the - # CUDA allocator -- the later `import unsloth` patch is too late for THIS - # worker process. CUDA-free nvidia-smi sniff (mirrors - # _is_dgx_spark_no_cuda_init), same append-don't-override and - # UNSLOTH_NO_EXPANDABLE_SEGMENTS opt-out as the library patch. + # Set PYTORCH_CUDA_ALLOC_CONF before get_device_properties below inits + # the allocator -- the later `import unsloth` patch is too late for THIS + # worker. CUDA-free nvidia-smi sniff (mirrors _is_dgx_spark_no_cuda_init), + # same append-don't-override and UNSLOTH_NO_EXPANDABLE_SEGMENTS opt-out. try: import platform as _plat diff --git a/studio/backend/tests/test_spark_oom_guard.py b/studio/backend/tests/test_spark_oom_guard.py index 4e61dfd3af..a0e13f0bd6 100644 --- a/studio/backend/tests/test_spark_oom_guard.py +++ b/studio/backend/tests/test_spark_oom_guard.py @@ -1,13 +1,12 @@ # SPDX-License-Identifier: AGPL-3.0-only # Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 -"""Unit tests for _nvidia_classify_spark_unified_memory (Spark OOM-guard classifier). +"""Tests for _nvidia_classify_spark_unified_memory (Spark OOM-guard classifier). -Two paths: (1) ``is_integrated`` device property (authoritative on native Linux), -(2) device-name token match -- needed because WSL2's GPU paravirtualization masks +Two paths: (1) ``is_integrated`` property (authoritative on native Linux), +(2) name-token match -- needed because WSL2 GPU paravirtualization masks ``is_integrated`` to 0 and renames the device (N1X reports ``JMJWOA-Generic-GPU``; -verified on hardware). Mirrors test_rocm_oom_guard.py for the ROCm/Strix-Halo -classifier the NVIDIA guard was modeled on. +verified live). Mirrors test_rocm_oom_guard.py, which the NVIDIA guard models. """ from __future__ import annotations @@ -20,7 +19,7 @@ from core.training.worker import _nvidia_classify_spark_unified_memory def _props(**kwargs) -> SimpleNamespace: - """Build a fake device-properties object with the given attributes.""" + """Fake device-properties object with the given attributes.""" return SimpleNamespace(**kwargs) diff --git a/studio/scripts/provision_llama_cuda.sh b/studio/scripts/provision_llama_cuda.sh index ef4c63bb50..1f36b8b90f 100644 --- a/studio/scripts/provision_llama_cuda.sh +++ b/studio/scripts/provision_llama_cuda.sh @@ -1,8 +1,8 @@ #!/usr/bin/env bash # Build CUDA llama.cpp for Studio GGUF *inference* into ~/.unsloth/llama.cpp -# (resolver checks /build/bin/llama-server). Idempotent, best-effort, always -# exits 0. Exists because no aarch64+CUDA prebuilt covers NVIDIA ARM hosts -# (DGX Spark / GB10, N1X "RTX" laptops). Platform gotchas handled: +# (resolver checks /build/bin/llama-server). Idempotent, best-effort, exits +# 0. Exists because no aarch64+CUDA prebuilt covers NVIDIA ARM hosts (DGX Spark / +# GB10, N1X "RTX" laptops). Platform gotchas handled: # * nvcc rejects gcc-15 -> force gcc-14 / g++-14 host compiler # * glibc >= 2.41 vs CUDA < 13.3 -> install CUDA 13.3 (rsqrt header clash) # * sm_121 (Blackwell) -> derive arch from the GPU's compute_cap @@ -13,7 +13,7 @@ LLAMA_DIR="${UNSLOTH_LLAMA_CPP_PATH:-$HOME/.unsloth/llama.cpp}" SERVER="$LLAMA_DIR/build/bin/llama-server" log() { printf ' - %s\n' "$*"; } -# CUDA shows up two ways: monolithic (libggml-cuda in ldd) or split (dlopen-ed +# Detect CUDA two ways: monolithic (libggml-cuda in ldd) or split (dlopen-ed # libggml-cuda.so* beside the binary, missed by ldd). CPU-only builds ship no # libggml-cuda.so, so its presence is the reliable signal. is_cuda_server() { @@ -39,12 +39,12 @@ SUDO=""; [ "$(id -u)" -ne 0 ] && SUDO="sudo" HAVE_APT=0; command -v apt-get >/dev/null 2>&1 && HAVE_APT=1 # 2. Base toolchain first, then gcc-14 (nvcc rejects gcc-15) in a SEPARATE apt -# transaction: gcc-14 is absent from default Ubuntu 22.04 / Debian 12 sources, -# which would abort a combined transaction and lose the base build tools too. +# transaction: gcc-14 is absent from default Ubuntu 22.04 / Debian 12 sources, so +# a combined transaction would abort and lose the base build tools too. if [ "$HAVE_APT" -eq 1 ]; then $SUDO apt-get update -y >/dev/null 2>&1 || true - # libcurl4-openssl-dev: -DLLAMA_CURL=ON needs it, and on the WSL deferred path - # setup.sh's GGUF dep install (which covers libcurl) was skipped. + # libcurl4-openssl-dev: -DLLAMA_CURL=ON needs it, and the WSL deferred path + # skips setup.sh's GGUF dep install that would otherwise provide libcurl. $SUDO apt-get install -y --no-install-recommends \ build-essential cmake git curl ca-certificates libcurl4-openssl-dev >/dev/null 2>&1 || true $SUDO apt-get install -y --no-install-recommends gcc-14 g++-14 >/dev/null 2>&1 || true @@ -89,7 +89,7 @@ fi CUDA_HOME="$(dirname "$(dirname "$NVCC")")" # CUDA + Linux dirs FIRST so the build uses Linux cmake/gcc/git, not Windows tools -# leaked in via WSL interop (/mnt/c); original PATH kept so nvidia-smi resolves. +# leaked in via WSL interop (/mnt/c). Keep original PATH so nvidia-smi resolves. export PATH="$CUDA_HOME/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$PATH" export CUDAToolkit_ROOT="$CUDA_HOME" @@ -158,7 +158,7 @@ _cmake_configure() { -DLLAMA_CURL=ON >/dev/null 2>&1 } # A pre-existing build/ may carry a stale CMake cache (relocated dir: bad absolute -# paths + GGML_CUDA=OFF). Reuse it first (fast incremental); wipe only on failure. +# paths + GGML_CUDA=OFF). Reuse first (fast incremental); wipe only on failure. if ! _cmake_configure; then log "stale/incompatible CMake cache detected; wiping build dir for a clean CUDA configure" rm -rf build @@ -166,10 +166,10 @@ if ! _cmake_configure; then fi # Also builds the targets unsloth-zoo's GGUF exporter needs (llama-mtmd-cli, # llama-gguf-split). Jobs default to ~half the cores (full -j(nproc) CUDA builds -# trip thermal shutdowns on NVIDIA-ARM laptops like the N1X "RTX Spark") and are -# RAM-capped (~1.5 GB/nvcc job). Tune: UNSLOTH_LLAMA_BUILD_JOBS=N; re-runs resume. +# trip thermal shutdowns on NVIDIA-ARM laptops like the N1X "RTX Spark"), capped +# at ~1.5 GB/nvcc job. Tune: UNSLOTH_LLAMA_BUILD_JOBS=N; re-runs resume. _ncpu="$(nproc 2>/dev/null || echo 4)" -# Honor a valid positive-int override; ignore junk/0 (cmake reads -j0 as "all cores"). +# Honor a valid positive-int override; ignore junk/0 (cmake treats -j0 as all cores). if [ -n "${UNSLOTH_LLAMA_BUILD_JOBS:-}" ] && [ "${UNSLOTH_LLAMA_BUILD_JOBS}" -ge 1 ] 2>/dev/null; then JOBS="$UNSLOTH_LLAMA_BUILD_JOBS" else @@ -188,7 +188,7 @@ command -v nice >/dev/null 2>&1 && _NICE="nice -n 19" command -v ionice >/dev/null 2>&1 && _NICE="$_NICE ionice -c 3" _cmake_build() { # Only llama-server is REQUIRED: an old UNSLOTH_LLAMA_TAG pin may predate the - # helper targets, and those missing must not fail the whole provision. + # helper targets, whose absence must not fail the whole provision. $_NICE cmake --build build -j"$JOBS" --target llama-server >/dev/null 2>&1 } _cmake_build_extras() { @@ -200,7 +200,7 @@ _cmake_build_extras() { if ! _cmake_build; then # An interrupted build (thermal/power shutdown, common on this machine class) # can leave a half-linked libggml-cuda.so that breaks the resume link - # (undefined ggml_cuda_op_* refs); wipe and rebuild clean once. + # (undefined ggml_cuda_op_* refs); wipe and rebuild clean. log "build failed (likely interrupted/partial); wiping build dir and rebuilding clean" rm -rf build _cmake_configure || { log "cmake configure failed"; cd /; _restore_prev; exit 0; } diff --git a/studio/setup.sh b/studio/setup.sh index c8d9a630d8..44b790ffce 100755 --- a/studio/setup.sh +++ b/studio/setup.sh @@ -975,7 +975,7 @@ LLAMA_SERVER_BIN="$LLAMA_CPP_DIR/build/bin/llama-server" _NEED_LLAMA_SOURCE_BUILD=false _LLAMA_CPP_DEGRADED=false # Deferred != degraded: on WSL2 aarch64+NVIDIA install.ps1 builds the CUDA server -# in the background, so an absent server is success and must not trip the arm64 +# in the background, so an absent server is success -- must not trip the arm64 # CPU-prebuilt last-resort or exit 1. _LLAMA_CPP_DEFERRED=false _LLAMA_FORCE_COMPILE="${UNSLOTH_LLAMA_FORCE_COMPILE:-0}" @@ -1164,7 +1164,7 @@ fi # install.ps1 builds the CUDA llama-server in the background; without nvcc, # section 9 could only make a slow CPU server that build discards. With nvcc we # fall through to section 9; opted out (UNSLOTH_NO_LLAMA_CUDA=1) the CPU build is -# kept as the only server. +# the only server. if [ "$_NEED_LLAMA_SOURCE_BUILD" = true ] \ && [ "$_LLAMA_FORCE_COMPILE" != "1" ] \ && [ -z "$_LLAMA_PR" ] \ @@ -1178,7 +1178,7 @@ if [ "$_NEED_LLAMA_SOURCE_BUILD" = true ] \ step "llama.cpp" "GGUF engine: CUDA build running in background (WSL aarch64 + NVIDIA)" "$C_WARN" substep "skipping slow CPU build; the background CUDA llama.cpp will provide the server" substep "(opt out / keep CPU build with UNSLOTH_NO_LLAMA_CUDA=1)" - # DEFERRED, not DEGRADED: DEGRADED would trigger the CPU-prebuilt last resort + exit 1. + # DEFERRED, not DEGRADED: DEGRADED triggers the CPU-prebuilt last resort + exit 1. _NEED_LLAMA_SOURCE_BUILD=false _LLAMA_CPP_DEFERRED=true fi @@ -1456,7 +1456,7 @@ else # glibc >= 2.41 + CUDA < 13.3: rsqrt/rsqrtf header clash fails # every .cu -> CPU fallback; only fix is CUDA >= 13.3. Diagnostic - # only (never changes flags or aborts). Checks the final _NVCC_VER + # only (never changes flags or aborts), against the final _NVCC_VER # (after the driver-compat swap above). _GLIBC_VER="$(getconf GNU_LIBC_VERSION 2>/dev/null | awk '{print $2}')" || _GLIBC_VER="" if [ -n "$_GLIBC_VER" ]; then @@ -1686,8 +1686,8 @@ fi # end _SKIP_GGUF_BUILD check # (provision_llama_cuda.sh) for native Linux. Best-effort: provision always exits # 0, and on failure the prior CPU/degraded state stands. # CUDA detection covers both layouts: monolithic (libggml-cuda in ldd) and split -# (dlopen-ed libggml-cuda.so* beside the binary, missed by ldd). CPU-only builds -# ship no libggml-cuda.so, so its presence is the signal. +# (dlopen-ed libggml-cuda.so* beside the binary, missed by ldd). Its presence is +# the signal -- CPU-only builds ship no libggml-cuda.so. _have_cuda_llama_server() { [ -x "$LLAMA_SERVER_BIN" ] || return 1 ldd "$LLAMA_SERVER_BIN" 2>/dev/null | grep -qi 'libggml-cuda' && return 0 @@ -1702,8 +1702,8 @@ if [ "$_HOST_SYSTEM" = "Linux" ] \ && nvidia-smi -L 2>/dev/null | awk '/^GPU[[:space:]]+[0-9]+:/{found=1} END{exit !found}' \ && ! _have_cuda_llama_server; then # Under WSL this runs ONLY for a DIRECT `install.sh` run: install.ps1 sets - # UNSLOTH_WSL_LLAMA_DEFERRED=1 and builds in the background, but a direct run - # has no background builder, so provision here. + # UNSLOTH_WSL_LLAMA_DEFERRED=1 and builds in the background; a direct run has + # no background builder, so provision here. # Resolve provision_llama_cuda.sh: beside setup.sh, then local-dev repo, else # fetch from GitHub (so `curl | sh` works on an older wheel without it). _PROV_SH="" @@ -1721,13 +1721,13 @@ if [ "$_HOST_SYSTEM" = "Linux" ] \ if [ -n "$_PROV_SH" ]; then step "llama.cpp" "aarch64 + NVIDIA: provisioning CUDA toolkit + building CUDA llama.cpp for GGUF inference..." "$C_WARN" substep "(opt out with UNSLOTH_NO_LLAMA_CUDA=1; lower load with UNSLOTH_LLAMA_BUILD_JOBS=N)" - # UNSLOTH_LLAMA_CPP_PATH routes a custom STUDIO_HOME into $LLAMA_CPP_DIR; always exits 0. + # UNSLOTH_LLAMA_CPP_PATH routes a custom STUDIO_HOME into $LLAMA_CPP_DIR. UNSLOTH_LLAMA_CPP_PATH="$LLAMA_CPP_DIR" bash "$_PROV_SH" || true if _have_cuda_llama_server; then step "llama.cpp" "CUDA llama-server ready (aarch64 + NVIDIA)" _LLAMA_CPP_DEGRADED=false - # Claim ownership of the fresh $LLAMA_CPP_DIR or the next custom-STUDIO_HOME - # run's _assert_studio_owned_or_absent would abort on it. + # Claim ownership of the fresh $LLAMA_CPP_DIR, else the next custom-STUDIO_HOME + # run's _assert_studio_owned_or_absent aborts on it. if [ "$_STUDIO_HOME_IS_CUSTOM" = true ]; then : > "$LLAMA_CPP_DIR/$_STUDIO_OWNED_MARKER" 2>/dev/null || true fi diff --git a/unsloth/kernels/flex_attention.py b/unsloth/kernels/flex_attention.py index 71428b27b1..294a54e21d 100644 --- a/unsloth/kernels/flex_attention.py +++ b/unsloth/kernels/flex_attention.py @@ -27,9 +27,9 @@ torch_compile_options = { def _flex_is_dgx_spark(): - # CUDA-free copy of _utils._is_dgx_spark_no_cuda_init() (avoids a circular import). - # Runs at module import, before ._utils -- touching torch.cuda here would init the - # allocator before patch_dgx_spark_memory_config() can set PYTORCH_CUDA_ALLOC_CONF. + # CUDA-free copy of _utils._is_dgx_spark_no_cuda_init() (avoids circular import). + # Runs at module import, before ._utils -- touching torch.cuda here would init + # the allocator before patch_dgx_spark_memory_config() sets PYTORCH_CUDA_ALLOC_CONF. _force = os.environ.get("UNSLOTH_FORCE_DGX_SPARK") if _force == "1": return True @@ -49,7 +49,7 @@ def _flex_is_dgx_spark(): timeout = 5, ) names = (out.stdout or "").upper() - # Whole-token match so "GB10" does not match a discrete "GB100"/"GB10X". + # Whole-token match so "GB10" doesn't match discrete "GB100"/"GB10X". import re return any( @@ -60,7 +60,7 @@ def _flex_is_dgx_spark(): return False -# Spark's 48 SMs are under inductor's 68-SM is_big_gpu bar; max_autotune would only waste search time. +# Spark's 48 SMs are under inductor's 68-SM is_big_gpu bar; max_autotune just wastes search time. if _flex_is_dgx_spark(): torch_compile_options["max_autotune"] = False diff --git a/unsloth/models/_utils.py b/unsloth/models/_utils.py index 8df76767a8..af96d1a85b 100644 --- a/unsloth/models/_utils.py +++ b/unsloth/models/_utils.py @@ -992,15 +992,14 @@ except: from transformers.modeling_utils import logger as transformers_logger -# ---- NVIDIA DGX Spark (GB10) / N1X "RTX Spark" unified-memory support ---- -# Device names vary ("NVIDIA GB10", "JMJWOA-Generic-GPU" on N1X); the -# aarch64 + CUDA gate keeps every Spark workaround a no-op elsewhere. +# NVIDIA DGX Spark (GB10) / N1X "RTX Spark" unified-memory support. +# Names vary ("NVIDIA GB10", "JMJWOA-Generic-GPU" on N1X); the aarch64 + CUDA +# gate keeps every Spark workaround a no-op elsewhere. _DGX_SPARK_DEVICE_TOKENS = ("GB10", "JMJWOA", "N1X", "DGX SPARK", "GB110") def _name_has_spark_token(names_upper): - # Whole-token match so "GB10" does NOT match "GB100"/"GB10X" -- a discrete - # Grace+Blackwell datacenter GPU must not be misread as a unified-memory Spark. + # Whole-token match so "GB10" doesn't match discrete "GB100"/"GB10X". import re return any( re.search(r"(? 1. Out-of-range = no cap. + # Out-of-range = no cap (0 OOMs everything; torch rejects > 1). _frac_val = float(_frac) if 0.0 < _frac_val <= 1.0: torch.cuda.set_per_process_memory_fraction(_frac_val) @@ -1133,10 +1132,10 @@ def patch_dgx_spark_runtime_defaults(): def patch_dgx_spark_dataloader_defaults(): - """Default `dataloader_pin_memory` to False on Spark UMA (accuracy-neutral). + """Default `dataloader_pin_memory` to False on Spark UMA. On one shared pool, pinning only reserves non-pageable RAM and adds a staging - copy (mirrors transformers' own use_cpu precedent). Wrapping the base + copy (mirrors transformers' use_cpu precedent). Wrapping the base `TrainingArguments.__post_init__` covers SFT + every TRL trainer in one idempotent patch. Opt out: UNSLOTH_SPARK_KEEP_PIN_MEMORY=1. No-op off-Spark. """ @@ -1153,7 +1152,7 @@ def patch_dgx_spark_dataloader_defaults(): return _orig_post_init = Base.__post_init__ - # *args/**kwargs: tolerate future InitVar parameters in __post_init__. + # *args/**kwargs: tolerate future InitVar params in __post_init__. def __post_init__(self, *args, **kwargs): _orig_post_init(self, *args, **kwargs) if getattr(self, "dataloader_pin_memory", None) is True: @@ -1731,7 +1730,7 @@ torch_compile_options = { "trace.enabled": UNSLOTH_COMPILE_DEBUG, "triton.cudagraphs": False, } -# Spark's 48 SMs are under inductor's 68-SM is_big_gpu bar; max_autotune would only waste search time. +# Spark's 48 SMs are under inductor's 68-SM is_big_gpu bar; max_autotune just wastes search time. if is_dgx_spark(): torch_compile_options["max_autotune"] = False From e7468cde21c7a53b9c3098789c2167d1dc452a27 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Sun, 14 Jun 2026 21:37:29 -0700 Subject: [PATCH 65/95] fix(install): stop the duplicate/blank WSL shortcut on Windows-on-ARM On the WoA path install.ps1 already creates one canonical "Unsloth Studio.lnk" with a %USERPROFILE%\.unsloth icon (renders on WoA). install.sh's create_studio_shortcuts ALSO made a second "Unsloth Studio (WSL - ).lnk" whose icon lived under %LOCALAPPDATA% -- which the WoA shell icon broker can't read, so it rendered BLANK. Net: two shortcuts, one blank ("blank for both"). - install.ps1: export UNSLOTH_SKIP_WSL_WINDOWS_SHORTCUT=1 into the WSL install so install.sh skips its own Windows .lnk (install.ps1 owns the WoA shortcut). - install.sh: honor that flag (skip the WSL .lnk branch); and move the WSL shortcut icon from %LOCALAPPDATA%\Unsloth Studio to %USERPROFILE%\.unsloth so a DIRECT native-WSL install (no install.ps1) also renders instead of going blank. Co-Authored-By: Claude Opus 4.8 (1M context) --- install.ps1 | 4 ++++ install.sh | 10 ++++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/install.ps1 b/install.ps1 index c502d95683..2c52c44ed4 100644 --- a/install.ps1 +++ b/install.ps1 @@ -1936,6 +1936,10 @@ shell.Run cmd, 0, False # Forward a user Python pin (install.sh reads UNSLOTH_PYTHON, but Windows env vars don't cross # into WSL unless bridged). Numeric-only guard (e.g. 3.12) prevents injection. if ($env:UNSLOTH_PYTHON -and ($env:UNSLOTH_PYTHON -match '^[0-9][0-9.]*$')) { $_fwdEnv += "export UNSLOTH_PYTHON=$($env:UNSLOTH_PYTHON); " } + # install.ps1 owns the WoA shortcut (one canonical "Unsloth Studio.lnk" with a + # %USERPROFILE%\.unsloth icon that renders on WoA). Tell install.sh to skip its own + # WSL .lnk so we don't get a duplicate whose %LOCALAPPDATA% icon renders blank. + $_fwdEnv += 'export UNSLOTH_SKIP_WSL_WINDOWS_SHORTCUT=1; ' if ($_instRef -eq 'main') { $wslInstall = $_fwdEnv + 'export DEBIAN_FRONTEND=noninteractive UNSLOTH_WSL_LLAMA_DEFERRED=1; apt-get update -y >/dev/null; apt-get install -y build-essential cmake git curl pciutils libcurl4-openssl-dev >/dev/null; curl -fsSL https://unsloth.ai/install.sh | sh' } else { diff --git a/install.sh b/install.sh index 200e01688e..59f4c80e9c 100755 --- a/install.sh +++ b/install.sh @@ -1175,7 +1175,7 @@ STUB_EOF fi _css_created=1 - elif [ "$_css_os" = "wsl" ]; then + elif [ "$_css_os" = "wsl" ] && [ "${UNSLOTH_SKIP_WSL_WINDOWS_SHORTCUT:-0}" != "1" ]; then # ── WSL: create Windows Desktop and Start Menu shortcuts ── # Detect current WSL distro for targeted shortcut _css_distro="${WSL_DISTRO_NAME:-}" @@ -1223,9 +1223,11 @@ STUB_EOF \$WshShell = New-Object -ComObject WScript.Shell \$targetExe = (Get-Command '$_css_sc_target' -ErrorAction SilentlyContinue).Source if (-not \$targetExe) { exit 1 } -# Best-effort: fetch the Unsloth icon to a stable Windows path (shared with a -# native install if one exists) so the WSL shortcut shows the proper icon. -\$iconDir = Join-Path \$env:LOCALAPPDATA 'Unsloth Studio' +# Best-effort: fetch the Unsloth icon to a stable Windows path so the shortcut +# shows the proper icon. Use %USERPROFILE%\.unsloth, NOT %LOCALAPPDATA%: on +# Windows-on-ARM the sandboxed icon broker can't read a standalone .ico under +# AppData\Local (renders blank); a profile-path icon renders everywhere. +\$iconDir = Join-Path \$env:USERPROFILE '.unsloth' \$iconPath = Join-Path \$iconDir 'unsloth.ico' if (-not (Test-Path -LiteralPath \$iconPath)) { try { From aa2c4ba3ebc740ac9be052456308e061eda837a0 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Sun, 14 Jun 2026 21:56:45 -0700 Subject: [PATCH 66/95] fix(install): verify the unsloth CLI exists before declaring WSL success The WoA WSL path gated success solely on torch.cuda.is_available(), but install.sh can exit after PyTorch yet before the `unsloth` package/console script (e.g. a transient `uv pip install unsloth`). torch would still import, so the installer wrote a Windows shim pointing at /root/.unsloth/studio/unsloth_studio/bin/unsloth and reported success even though that binary was absent -- `unsloth studio` then fails "no such file". $wslRc can't distinguish this (it also goes non-zero on the optional llama prebuilt step). Now `test -x` the exact shim target; if missing, fall through to the existing failure path (restore rollback + non-zero exit) instead of creating a dangling shim. Verified on N1X: present->exit 0 (success kept), absent->exit 1 (fails). Addresses Codex review 4494521902. Co-Authored-By: Claude Opus 4.8 (1M context) --- install.ps1 | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/install.ps1 b/install.ps1 index 2c52c44ed4..396398d347 100644 --- a/install.ps1 +++ b/install.ps1 @@ -1967,6 +1967,20 @@ shell.Run cmd, 0, False & wsl.exe -d $distro --cd /root -u root -- /root/.unsloth/studio/unsloth_studio/bin/python -c "import torch,sys; sys.exit(0 if torch.cuda.is_available() else 3)" *> $null $torchOk = ($LASTEXITCODE -eq 0) } catch {} finally { $ErrorActionPreference = $prevEapChk } + # torch.cuda alone isn't success: install.sh can exit after PyTorch but before the `unsloth` + # package/console script (e.g. a transient `uv pip install unsloth`), and $wslRc can't tell + # (it also goes non-zero on the optional llama prebuilt step). Verify the exact binary the shim + # execs exists -- else we'd write a dangling shim and report a broken install as success. + if ($torchOk) { + $prevEapCli = $ErrorActionPreference; $ErrorActionPreference = "Continue" + $global:LASTEXITCODE = -1 + try { & wsl.exe -d $distro --cd /root -u root -- test -x /root/.unsloth/studio/unsloth_studio/bin/unsloth *> $null } catch {} + $ErrorActionPreference = $prevEapCli + if ($LASTEXITCODE -ne 0) { + substep "WSL install incomplete: 'unsloth' CLI missing (install.sh cut short after PyTorch) -- not creating a dangling shim." "Yellow" + $torchOk = $false + } + } # Self-heal web-server deps: a cut-short install.sh "studio deps" step leaves torch + unsloth but # no fastapi/uvicorn/structlog/starlette (`unsloth studio` dies). Reinstall them unpinned (no # huggingface-hub/transformers/datasets) so the verified GPU torch stack stays intact. From e744fb75ededc9ef0e82202e47820240e1178b1a Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Thu, 18 Jun 2026 22:52:21 -0700 Subject: [PATCH 67/95] test(studio): stop llama.cpp update worker tests hanging on a fully-installed host _run_update imports routes.inference.get_llama_cpp_backend, which on a fully-installed host pulls a real Studio singleton and blocks on its load lock. Default the autouse fixture to a no-backend stub (the fail-open path); the load-coordination tests still inject their own backend over it. --- studio/backend/tests/test_llama_cpp_update.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/studio/backend/tests/test_llama_cpp_update.py b/studio/backend/tests/test_llama_cpp_update.py index 326b3dc6aa..3755756158 100644 --- a/studio/backend/tests/test_llama_cpp_update.py +++ b/studio/backend/tests/test_llama_cpp_update.py @@ -121,6 +121,21 @@ def _clean_state(monkeypatch, tmp_path): monkeypatch.delenv("UNSLOTH_LLAMA_CPP_PATH", raising = False) # Never hit the network in these tests. monkeypatch.setattr(freshness, "_fetch_latest_release_tag", lambda repo, timeout = 5.0: None) + # Default to no live inference backend: on a fully-installed host + # `from routes.inference import get_llama_cpp_backend` (inside _run_update) + # imports a real Studio singleton and blocks on its load lock, making the + # worker tests hang/flake by host. This is the CI/fail-open path; the + # load-coordination tests inject their own backend over this default. + _routes_pkg = ModuleType("routes") + _routes_pkg.__path__ = [] + _inference_mod = ModuleType("routes.inference") + + def _no_backend_in_tests(): + raise RuntimeError("no inference backend in unit tests") + + _inference_mod.get_llama_cpp_backend = _no_backend_in_tests + monkeypatch.setitem(sys.modules, "routes", _routes_pkg) + monkeypatch.setitem(sys.modules, "routes.inference", _inference_mod) yield freshness.reset_caches() upd._reset_job_for_tests() From 6006402fa2f15a8a8ea5be9af194058cc1a68666 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Fri, 19 Jun 2026 00:40:19 -0700 Subject: [PATCH 68/95] fix(provision): address Codex review (3 P2s on the aarch64 CUDA provisioner) - find_nvcc now prefers the highest /usr/local/cuda- toolkit so a stale unversioned `cuda` symlink or an older nvcc earlier on PATH can't win and rebuild with CUDA 12.x (re-hitting the glibc>=2.41 / Blackwell clash this script avoids); falls back to a PATH nvcc only when no versioned toolkit. - Validate the GPU compute_cap is purely numeric before using it as CMAKE_CUDA_ARCHITECTURES: some WSL GPU-PV / driver combos report "N/A", which CMake rejects (aborting an otherwise-usable build) instead of letting "native" autodetect. - Gate the native-Linux aarch64 provisioner on _SKIP_GGUF_BUILD: when a non-root user declines the sudo prompt (or lacks sudo) for GGUF deps, don't then run a provisioner that does its own sudo apt-get installs. --- studio/scripts/provision_llama_cuda.sh | 21 ++++++++++++++++++--- studio/setup.sh | 1 + 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/studio/scripts/provision_llama_cuda.sh b/studio/scripts/provision_llama_cuda.sh index 1f36b8b90f..a7fc34103c 100644 --- a/studio/scripts/provision_llama_cuda.sh +++ b/studio/scripts/provision_llama_cuda.sh @@ -50,8 +50,17 @@ if [ "$HAVE_APT" -eq 1 ]; then $SUDO apt-get install -y --no-install-recommends gcc-14 g++-14 >/dev/null 2>&1 || true fi -# 3. Locate nvcc; install the CUDA toolkit if missing. -find_nvcc() { command -v nvcc 2>/dev/null || ls /usr/local/cuda*/bin/nvcc 2>/dev/null | sort -V | tail -1; } +# 3. Locate nvcc; install the CUDA toolkit if missing. Prefer the highest +# /usr/local/cuda- toolkit: a stale unversioned `cuda` symlink or an older +# nvcc earlier on PATH could otherwise win and rebuild with CUDA 12.x, re-hitting +# the glibc>=2.41 / Blackwell clash this script exists to avoid. Fall back to a +# PATH nvcc (e.g. conda) only when no versioned system toolkit is present. +find_nvcc() { + local _v + _v="$(ls -d /usr/local/cuda-*/bin/nvcc 2>/dev/null | sort -V | tail -1)" + if [ -n "$_v" ]; then printf '%s\n' "$_v"; return 0; fi + command -v nvcc 2>/dev/null || ls /usr/local/cuda*/bin/nvcc 2>/dev/null | sort -V | tail -1 +} NVCC="$(find_nvcc)" if [ -z "$NVCC" ] && [ "$HAVE_APT" -eq 1 ]; then log "CUDA toolkit (nvcc) not found - installing CUDA 13.3 (matches torch cu13x; avoids glibc>=2.41 rsqrt clash)" @@ -99,8 +108,14 @@ HCXX=g++; command -v g++-14 >/dev/null 2>&1 && HCXX=g++-14 export CC="$HCC" CXX="$HCXX" CUDAHOSTCXX="$HCXX" # 5. CUDA arch from the GPU's compute capability (e.g. "12.1" -> 121). Fallback: native. +# Only a purely-numeric capability is a valid CMAKE_CUDA_ARCHITECTURES; some WSL +# GPU-PV / driver combos report "N/A", which CMake would reject (aborting an +# otherwise-usable build) instead of letting "native" autodetect. CC_CAP="$(nvidia-smi --query-gpu=compute_cap --format=csv,noheader 2>/dev/null | head -1 | tr -d ' .')" -if [ -n "$CC_CAP" ]; then CUDA_ARCH="$CC_CAP"; else CUDA_ARCH="native"; fi +case "$CC_CAP" in + ''|*[!0-9]*) CUDA_ARCH="native" ;; + *) CUDA_ARCH="$CC_CAP" ;; +esac # 6. Clone + build into ~/.unsloth/llama.cpp, honoring a UNSLOTH_LLAMA_TAG pin # (same var setup.sh uses) instead of always tracking ggml-org main. diff --git a/studio/setup.sh b/studio/setup.sh index fc1a7e121b..bd3d7a1304 100755 --- a/studio/setup.sh +++ b/studio/setup.sh @@ -1731,6 +1731,7 @@ if [ "$_HOST_SYSTEM" = "Linux" ] \ && { [ "$_HOST_MACHINE" = "aarch64" ] || [ "$_HOST_MACHINE" = "arm64" ]; } \ && { ! grep -qi microsoft /proc/version 2>/dev/null || [ "${UNSLOTH_WSL_LLAMA_DEFERRED:-0}" != "1" ]; } \ && [ "${UNSLOTH_NO_LLAMA_CUDA:-0}" != "1" ] \ + && [ "${_SKIP_GGUF_BUILD:-}" != true ] \ && command -v nvidia-smi >/dev/null 2>&1 \ && nvidia-smi -L 2>/dev/null | awk '/^GPU[[:space:]]+[0-9]+:/{found=1} END{exit !found}' \ && ! _have_cuda_llama_server; then From a4ad50e0242029e9cba01d1d891dcf07b29879e6 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Fri, 19 Jun 2026 02:29:42 -0700 Subject: [PATCH 69/95] fix(prebuilt): harden nvidia-smi GPU detection against WSL GPU-PV slowness On Windows-on-ARM + NVIDIA the Studio install runs inside WSL2, where nvidia-smi is served over GPU-PV and can take far longer than its usual sub-second response when the host is under heavy CPU load (the concurrent pip / frontend / cmake work during install). detect_host probed nvidia-smi with a single 20s timeout; under that load it raised TimeoutExpired, the GPU was treated as ABSENT, and the host was misrouted to the ggml-org CPU prebuilt -> rejected on an NVIDIA host -> slow (and on thermal-limited laptops, risky) CUDA source build, even though a usable arm64 CUDA prebuilt was published. Add _nvidia_smi_capture(): retry the three detect_host nvidia-smi probes with a generous 60s per-attempt timeout. It is only reachable when nvidia-smi exists on PATH, so CPU-only hosts incur no extra wait. Measured: nvidia-smi took 42-59s under a -j20 build on an N1X; with the fix the probe rides it out and detect_host correctly reports has_usable_nvidia + compute_cap, so the CUDA prebuilt is selected (no source build). --- studio/install_llama_prebuilt.py | 34 ++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/studio/install_llama_prebuilt.py b/studio/install_llama_prebuilt.py index 4edab0b8ab..e89b142cb1 100644 --- a/studio/install_llama_prebuilt.py +++ b/studio/install_llama_prebuilt.py @@ -2855,6 +2855,33 @@ def _pick_rocm_gfx_target(out: str) -> str | None: return _tokens[0] +def _nvidia_smi_capture( + command: list[str], + *, + attempts: int = 2, + timeout: int = 60, +) -> subprocess.CompletedProcess[str]: + """run_capture for nvidia-smi probes, hardened against transient slowness. + + nvidia-smi normally answers in well under a second, but under WSL2 GPU-PV it + can take far longer when the host is under heavy CPU load -- e.g. the + concurrent pip / frontend / cmake work during an `unsloth studio` install. + A single short timeout then raises TimeoutExpired, detect_host treats the + GPU as ABSENT, and the host is misrouted to a CPU prebuilt / slow source + build instead of the CUDA bundle it can actually use. Retry with a generous + per-attempt timeout. Only ever reached when nvidia-smi exists on PATH, so + CPU-only hosts never incur this wait. + """ + last_exc: Exception | None = None + for _attempt in range(max(1, attempts)): + try: + return run_capture(command, timeout = timeout) + except subprocess.TimeoutExpired as exc: + last_exc = exc + time.sleep(2) + raise last_exc if last_exc is not None else RuntimeError("nvidia-smi capture failed") + + def detect_host() -> HostInfo: system = platform.system() machine = platform.machine().lower() @@ -2880,7 +2907,7 @@ def detect_host() -> HostInfo: # container leftovers), which would otherwise misclassify an AMD # ROCm host as NVIDIA and short-circuit the ROCm path. try: - listing = run_capture([nvidia_smi, "-L"], timeout = 20) + listing = _nvidia_smi_capture([nvidia_smi, "-L"]) gpu_lines = [line for line in listing.stdout.splitlines() if line.startswith("GPU ")] if gpu_lines: has_physical_nvidia = True @@ -2889,7 +2916,7 @@ def detect_host() -> HostInfo: pass try: - result = run_capture([nvidia_smi], timeout = 20) + result = _nvidia_smi_capture([nvidia_smi]) merged = "\n".join(part for part in (result.stdout, result.stderr) if part) # Newer NVIDIA drivers (e.g. 610.x on Windows) print # "CUDA UMD Version: X.Y" instead of the legacy @@ -2907,13 +2934,12 @@ def detect_host() -> HostInfo: pass try: - caps = run_capture( + caps = _nvidia_smi_capture( [ nvidia_smi, "--query-gpu=index,uuid,compute_cap", "--format=csv,noheader", ], - timeout = 20, ) visible_gpu_rows: list[tuple[str, str, str]] = [] for raw in caps.stdout.splitlines(): From c1a997b438ba43c3da6abde27937d6562adcd00e Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Fri, 19 Jun 2026 02:55:09 -0700 Subject: [PATCH 70/95] fix(prebuilt): don't sleep after the final nvidia-smi retry attempt Cosmetic follow-up to a4ad50e (review nit): the retry loop slept 2s even after the last attempt, adding ~2s only when nvidia-smi is permanently hung. Sleep only between attempts. --- studio/install_llama_prebuilt.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/studio/install_llama_prebuilt.py b/studio/install_llama_prebuilt.py index e89b142cb1..39f6ce0406 100644 --- a/studio/install_llama_prebuilt.py +++ b/studio/install_llama_prebuilt.py @@ -2873,12 +2873,14 @@ def _nvidia_smi_capture( CPU-only hosts never incur this wait. """ last_exc: Exception | None = None - for _attempt in range(max(1, attempts)): + _attempts = max(1, attempts) + for _attempt in range(_attempts): try: return run_capture(command, timeout = timeout) except subprocess.TimeoutExpired as exc: last_exc = exc - time.sleep(2) + if _attempt + 1 < _attempts: # don't sleep after the final attempt + time.sleep(2) raise last_exc if last_exc is not None else RuntimeError("nvidia-smi capture failed") From 14b7dd5ed356b17929bff624fb17f6f465e14bcf Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Sun, 21 Jun 2026 01:18:07 -0700 Subject: [PATCH 71/95] fix(setup): thermal-cap the aarch64+NVIDIA foreground CUDA build The foreground source build in setup.sh used -j(nproc), which on the lightly-cooled NVIDIA-ARM boxes this WoA/WSL path targets (DGX Spark / GB10, N1X RTX Spark laptops) draws enough sustained power during the nvcc compile to trip a thermal shutdown -- the exact reason provision_llama_cuda.sh already caps its background build. Mirror that cap for the foreground build (only reached when no prebuilt llama.cpp was available and a CUDA toolkit is present): gate on aarch64/arm64 + GPU_BACKEND=cuda, then use ~half the cores, also bounded by ~1.5 GB/nvcc job. Other platforms and CPU builds keep full -j(nproc). Override anywhere with UNSLOTH_LLAMA_BUILD_JOBS=N. Verified: nproc=20/29GB box -> -j10; override=6 -> -j6; CPU build and x86_64 stay uncapped. --- studio/setup.sh | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/studio/setup.sh b/studio/setup.sh index bd3d7a1304..833dd2598f 100755 --- a/studio/setup.sh +++ b/studio/setup.sh @@ -1616,6 +1616,30 @@ else substep "$_BUILD_DESC..." NCPU=$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4) + # Thermal cap for the aarch64 + NVIDIA foreground CUDA build. A full + # -j(nproc) nvcc compile draws enough sustained power to trip a + # thermal shutdown on the lightly-cooled NVIDIA-ARM boxes this path + # targets (DGX Spark / GB10, N1X "RTX Spark" laptops) -- the same + # reason provision_llama_cuda.sh caps its background build. Mirror + # that cap here (this foreground build only runs when no prebuilt was + # available and a CUDA toolkit is already present): ~half the cores, + # also bounded by ~1.5 GB/nvcc job. Other platforms keep full + # -j(nproc); override on any host with UNSLOTH_LLAMA_BUILD_JOBS=N. + if { [ "$_HOST_MACHINE" = "aarch64" ] || [ "$_HOST_MACHINE" = "arm64" ]; } \ + && [ "${GPU_BACKEND:-}" = "cuda" ]; then + if [ -n "${UNSLOTH_LLAMA_BUILD_JOBS:-}" ] && [ "${UNSLOTH_LLAMA_BUILD_JOBS}" -ge 1 ] 2>/dev/null; then + NCPU="$UNSLOTH_LLAMA_BUILD_JOBS" + else + _cap_half=$(( (NCPU + 1) / 2 )) + [ "$NCPU" -le 4 ] && _cap_half="$NCPU" # tiny boxes: use all cores + _cap_memkb="$(awk '/MemTotal/{print $2}' /proc/meminfo 2>/dev/null || echo 0)" + _cap_memjobs=$(( _cap_memkb / 1572864 )) # ~1.5 GB per nvcc job + [ "$_cap_memjobs" -lt 1 ] && _cap_memjobs=1 + [ "$_cap_memjobs" -lt "$_cap_half" ] && _cap_half="$_cap_memjobs" + NCPU="$_cap_half" + fi + substep "thermal-capped CUDA build: -j${NCPU} (override with UNSLOTH_LLAMA_BUILD_JOBS=N)" + fi CMAKE_GENERATOR_ARGS="" if command -v ninja &>/dev/null; then CMAKE_GENERATOR_ARGS="-G Ninja" From 31bd20149c56728ea0240c09f365df193df5515a Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Sun, 21 Jun 2026 01:31:27 -0700 Subject: [PATCH 72/95] fix(install.ps1): forward UNSLOTH_PYTORCH_MIRROR into the WSL installer The WoA+NVIDIA WSL2 fallback bridges UNSLOTH_NO_LLAMA_CUDA / UNSLOTH_PYTHON / UNSLOTH_SKIP_WSL_WINDOWS_SHORTCUT into the distro, but not UNSLOTH_PYTORCH_MIRROR. install.sh's get_torch_index_url() reads it (as does install.ps1's own native Get-TorchIndexUrl), yet Windows env vars don't cross into WSL -- so a mirror-required / restricted-network install silently fell back to download.pytorch.org inside the distro even though the outer installer honored the mirror. Forward it alongside the other vars, guarded by a strict http(s)-URL allow-list (no shell metacharacters) and single-quoted so the value can't break out of the bash -lc string. Verified: legit mirror URLs (incl. host:port and query strings) forward; space/';'/$()/quote-injection and non-http schemes are rejected. Addresses Codex review P2 (install.ps1). --- install.ps1 | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/install.ps1 b/install.ps1 index f797b80296..089ba2b8bb 100644 --- a/install.ps1 +++ b/install.ps1 @@ -2093,6 +2093,14 @@ exit 0 # Forward a user Python pin (install.sh reads UNSLOTH_PYTHON, but Windows env vars don't cross # into WSL unless bridged). Numeric-only guard (e.g. 3.12) prevents injection. if ($env:UNSLOTH_PYTHON -and ($env:UNSLOTH_PYTHON -match '^[0-9][0-9.]*$')) { $_fwdEnv += "export UNSLOTH_PYTHON=$($env:UNSLOTH_PYTHON); " } + # Forward a custom PyTorch wheel mirror. install.sh reads UNSLOTH_PYTORCH_MIRROR (get_torch_index_url) + # but, like the other Windows env vars, it doesn't cross into WSL -- so a mirror-required / restricted- + # network install would silently fall back to download.pytorch.org inside the distro even though the + # outer installer honored the mirror. Strict http(s)-URL allow-list (no shell metachars) + single-quote + # so the value can't break out of the bash -lc string. + if ($env:UNSLOTH_PYTORCH_MIRROR -and ($env:UNSLOTH_PYTORCH_MIRROR -match '^https?://[A-Za-z0-9._~:/?#@%+=&-]+$')) { + $_fwdEnv += "export UNSLOTH_PYTORCH_MIRROR='$($env:UNSLOTH_PYTORCH_MIRROR)'; " + } # install.ps1 owns the WoA shortcut (one canonical "Unsloth Studio.lnk" with a # %USERPROFILE%\.unsloth icon that renders on WoA). Tell install.sh to skip its own # WSL .lnk so we don't get a duplicate whose %LOCALAPPDATA% icon renders blank. From 27bc44c460a96722f251f0858128789c75d94b4d Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Sun, 21 Jun 2026 01:31:37 -0700 Subject: [PATCH 73/95] fix(provision): functionally confirm CUDA before the step-0 rebuild-skip is_cuda_server() treats a co-located libggml-cuda.so* as proof the server is CUDA-ready. That's normally true (llama.cpp dlopens the backend from beside the binary), but an *interrupted* build (thermal/power shutdown -- common on the NVIDIA-ARM laptops this path targets) can leave a half-linked libggml-cuda.so next to the server: present, so is_cuda_server() matches, yet the backend fails to load at runtime. The post-build path already wipes+rebuilds such a partial .so, but the step-0 early-skip trusted it and never rebuilt -- so Studio could report GGUF CUDA inference ready while running a broken/non-CUDA backend. Gate the early-skip with cuda_server_probe(): 'llama-server --list-devices' enumerates backends and exits (cheap, no server spin-up). Only a definitive 'flag supported, ran, but no CUDA device' triggers a clean rebuild; a timeout or an old pin without --list-devices stays inconclusive and keeps trusting the .so, so we never force a needless, thermally-expensive rebuild. Probe logic verified against healthy/broken/unsupported/timeout stubs (0/1/2/2). Addresses Codex review P2 (provision_llama_cuda.sh). --- studio/scripts/provision_llama_cuda.sh | 30 ++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/studio/scripts/provision_llama_cuda.sh b/studio/scripts/provision_llama_cuda.sh index a7fc34103c..585f0baf01 100644 --- a/studio/scripts/provision_llama_cuda.sh +++ b/studio/scripts/provision_llama_cuda.sh @@ -23,10 +23,36 @@ is_cuda_server() { return 1 } +# Functional confirmation that the server's CUDA backend actually loads, used only +# to gate the step-0 early-skip. is_cuda_server() trusts a co-located +# libggml-cuda.so*, but an *interrupted* build (thermal/power shutdown -- common on +# this hardware) can leave a half-linked libggml-cuda.so beside the binary: present, +# so is_cuda_server() matches, yet the backend fails to dlopen at runtime. The +# post-build path already wipes+rebuilds such a partial .so, but the early-skip would +# trust it and never rebuild. `--list-devices` enumerates backends and exits, so it's +# a cheap probe (no server spin-up). Returns: 0 = a CUDA device is listed; 1 = the +# flag is supported and ran but no CUDA device appeared (broken/partial backend -> +# rebuild); 2 = inconclusive (timed out, or an old pin without --list-devices) -> keep +# trusting the .so so we never force a needless, thermally-expensive rebuild. +cuda_server_probe() { + local _to="" _out _rc + command -v timeout >/dev/null 2>&1 && _to="timeout 60" + _out="$( $_to "$1" --list-devices 2>&1 )"; _rc=$? + [ "$_rc" -eq 124 ] && return 2 # timed out + printf '%s\n' "$_out" | grep -qiE 'CUDA[0-9]' && return 0 # CUDA device listed + printf '%s\n' "$_out" | grep -qi 'available devices' && return 1 # ran, but none is CUDA + return 2 # flag unsupported / couldn't run +} + # 0. Already provisioned? if is_cuda_server "$SERVER"; then - log "CUDA llama-server already present: $SERVER" - exit 0 + cuda_server_probe "$SERVER"; _probe=$? + if [ "$_probe" -ne 1 ]; then + log "CUDA llama-server already present: $SERVER" + exit 0 + fi + log "existing llama-server has libggml-cuda.so but lists no CUDA device (partial/broken build); rebuilding clean" + rm -rf "$LLAMA_DIR/build" # force a clean reconfigure+build below fi # 1. Require an NVIDIA GPU (this script is only meaningful with one). From fbb2d9000f2b17c3a993f3a83f3ab2c1ceaada47 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Sun, 21 Jun 2026 02:07:08 -0700 Subject: [PATCH 74/95] fix(uninstall.ps1): drop the empty ~/.unsloth left behind on WoA uninstall The empty-dir sweep of ~/.unsloth ran before the WoA-fallback block removes ~/.unsloth\unsloth.ico, so on a Windows-on-ARM install the still-present icon kept the dir non-empty at sweep time and it was skipped -- leaving an empty ~/.unsloth behind after a full uninstall. Re-attempt the empty-only removal right after the icon is deleted (the last default-mode child). uninstall.sh is unaffected: its rmdir runs as the final step. --- scripts/uninstall.ps1 | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/scripts/uninstall.ps1 b/scripts/uninstall.ps1 index 7ff9ee3f66..020fb85781 100644 --- a/scripts/uninstall.ps1 +++ b/scripts/uninstall.ps1 @@ -461,6 +461,13 @@ function Uninstall-UnslothStudio { } # The WoA shortcut icon lives under the user profile (icon broker can't read AppData\Local). if ($env:USERPROFILE) { _RemovePath (Join-Path $env:USERPROFILE ".unsloth\unsloth.ico") } + # The empty-dir sweep of ~/.unsloth above ran BEFORE this icon removal, so on a WoA install + # the still-present unsloth.ico kept ~/.unsloth non-empty then and it was skipped -- leaving an + # empty ~/.unsloth behind. Re-attempt now that the icon (the last default-mode child) is gone. + if ($defaultUnslothHome -and (Test-Path -LiteralPath $defaultUnslothHome) -and + -not (Get-ChildItem -LiteralPath $defaultUnslothHome -Force -ErrorAction SilentlyContinue)) { + _RemovePath $defaultUnslothHome + } # Remove the Studio install inside each WSL distro (the real GPU install + any CUDA llama.cpp build). if (Get-Command wsl.exe -ErrorAction SilentlyContinue) { try { From 65ef0bfc16b4d1fa9c30adcd6cdfaed4072e89ee Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Sun, 21 Jun 2026 02:25:25 -0700 Subject: [PATCH 75/95] revert(provision): drop the --list-devices step-0 probe (false thermal rebuilds) The cuda_server_probe() added in 27bc44c gated the step-0 rebuild-skip on a runtime 'llama-server --list-devices' check. In a real cold install on the N1X this BACKFIRED: the background provision runs step-0 while the install is still under heavy load (torch download, frontend build), and under WSL2 GPU-PV the CUDA backend's init transiently fails under load (the same flakiness cycle-21 worked around for nvidia-smi). --list-devices then enumerated devices but no CUDA, so the probe declared the freshly-validated PREBUILT 'broken', wiped it (rm -rf build), and kicked off a CUDA-13.3 toolkit install + source build -- the exact thermal-risk + wasted-prebuilt outcome cycle-21 eliminated. (Confirmed the prebuilt is fine: --list-devices shows CUDA0 in a normal shell, even with LD_LIBRARY_PATH stripped -- the probe failure was purely load-induced.) Restore the load-insensitive structural check: a co-located libggml-cuda.so* is trusted, because the prebuilt resolver validates what it installs and an interrupted SOURCE build is already caught by the build-failure wipe+rebuild in section 6. The Codex P2's half-linked-.so concern is real but narrow, and a runtime probe that can gamble the machine's thermals on an env/load-fragile GPU call is the wrong trade on this hardware. --- studio/scripts/provision_llama_cuda.sh | 40 +++++++------------------- 1 file changed, 11 insertions(+), 29 deletions(-) diff --git a/studio/scripts/provision_llama_cuda.sh b/studio/scripts/provision_llama_cuda.sh index 585f0baf01..29ffc0052e 100644 --- a/studio/scripts/provision_llama_cuda.sh +++ b/studio/scripts/provision_llama_cuda.sh @@ -23,36 +23,18 @@ is_cuda_server() { return 1 } -# Functional confirmation that the server's CUDA backend actually loads, used only -# to gate the step-0 early-skip. is_cuda_server() trusts a co-located -# libggml-cuda.so*, but an *interrupted* build (thermal/power shutdown -- common on -# this hardware) can leave a half-linked libggml-cuda.so beside the binary: present, -# so is_cuda_server() matches, yet the backend fails to dlopen at runtime. The -# post-build path already wipes+rebuilds such a partial .so, but the early-skip would -# trust it and never rebuild. `--list-devices` enumerates backends and exits, so it's -# a cheap probe (no server spin-up). Returns: 0 = a CUDA device is listed; 1 = the -# flag is supported and ran but no CUDA device appeared (broken/partial backend -> -# rebuild); 2 = inconclusive (timed out, or an old pin without --list-devices) -> keep -# trusting the .so so we never force a needless, thermally-expensive rebuild. -cuda_server_probe() { - local _to="" _out _rc - command -v timeout >/dev/null 2>&1 && _to="timeout 60" - _out="$( $_to "$1" --list-devices 2>&1 )"; _rc=$? - [ "$_rc" -eq 124 ] && return 2 # timed out - printf '%s\n' "$_out" | grep -qiE 'CUDA[0-9]' && return 0 # CUDA device listed - printf '%s\n' "$_out" | grep -qi 'available devices' && return 1 # ran, but none is CUDA - return 2 # flag unsupported / couldn't run -} - -# 0. Already provisioned? +# 0. Already provisioned? A co-located libggml-cuda.so* is the trusted signal: the +# prebuilt resolver validates the server it installs, and a *source* build that gets +# interrupted is caught by the build-failure wipe+rebuild below (section 6), so the +# early-skip can rely on the structural check. We deliberately do NOT run a functional +# `--list-devices` probe here: this script runs in a stripped-down detached shell whose +# loader path can miss /usr/lib/wsl/lib, so the CUDA backend may fail to enumerate even +# on a perfectly good server -- and a false negative would wipe a validated build and +# trigger a needless, thermally-dangerous source rebuild on the NVIDIA-ARM laptops this +# targets. Trust the .so; never gamble the machine's thermals on an env-fragile probe. if is_cuda_server "$SERVER"; then - cuda_server_probe "$SERVER"; _probe=$? - if [ "$_probe" -ne 1 ]; then - log "CUDA llama-server already present: $SERVER" - exit 0 - fi - log "existing llama-server has libggml-cuda.so but lists no CUDA device (partial/broken build); rebuilding clean" - rm -rf "$LLAMA_DIR/build" # force a clean reconfigure+build below + log "CUDA llama-server already present: $SERVER" + exit 0 fi # 1. Require an NVIDIA GPU (this script is only meaningful with one). From 75636a1909eaf9c8c63880511c12b0b78f78885c Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Sun, 21 Jun 2026 22:01:36 -0700 Subject: [PATCH 76/95] fix(install.ps1): honor/guard --local, custom Studio root, and ref in WoA WSL fallback Address three Codex P2s on the Windows-on-ARM + NVIDIA WSL2 fallback, all cases where the branch silently ignored a Windows-side option while reporting success: 1. UNSLOTH_INSTALL_REF was spliced raw into the inner 'bash -lc' twice (an export and a GitHub raw URL); a ref with shell metacharacters (;, &, ', space) would break or inject the command. Validate against a strict git-ref allow-list (^[A-Za-z0-9][A-Za-z0-9._/-]*$) and reject loudly. Real git refs always pass. 2. --local (editable install of the Windows checkout) can't be honored by the WSL tunnel, which installs from PyPI/a git ref and never mounts $RepoRoot -- it would silently install the published package. Reject it up front and point at the supported pre-merge path (push the branch + UNSLOTH_INSTALL_REF). 3. A custom UNSLOTH_STUDIO_HOME / STUDIO_HOME only applies to the native Windows layout; the WoA install lives in WSL at /root/.unsloth. Warn clearly so the user isn't misled into thinking Studio landed at their custom path. All three guard rare conditions; the default install path (no --local, default root, normal branch/tag ref) is unaffected. install.ps1 parses clean; ref guard verified against valid refs + metacharacter-injection cases. --- install.ps1 | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/install.ps1 b/install.ps1 index b73f07f070..30db2e9065 100644 --- a/install.ps1 +++ b/install.ps1 @@ -2016,6 +2016,21 @@ exit 0 return (Exit-InstallFailure "Windows-on-ARM + NVIDIA GPU needs the WSL2 GPU install, which the desktop app can't launch yet. Install from PowerShell instead: irm https://unsloth.ai/install.ps1 | iex" 1) } + # --local installs the Windows checkout editably (uv pip install -e $RepoRoot) on the native + # path, but the WSL tunnel installs from PyPI / a git ref and never mounts $RepoRoot -- so a + # --local run here would silently install the published package inside WSL and report success. + # Reject it and point at the supported pre-merge mechanism (push the branch + UNSLOTH_INSTALL_REF). + if ($StudioLocalInstall) { + return (Exit-InstallFailure "--local can't be honored on Windows-on-ARM + NVIDIA: the GPU install runs inside WSL2 and installs from a published/git ref, not this Windows checkout. For pre-merge testing, push your branch and set UNSLOTH_INSTALL_REF, e.g.: `$env:UNSLOTH_INSTALL_REF=''; irm https://unsloth.ai/install.ps1 | iex" 1) + } + # A custom Studio root (UNSLOTH_STUDIO_HOME / STUDIO_HOME) only applies to the native Windows + # layout; the WoA GPU install lives inside WSL at /root/.unsloth and the shim/verification paths + # are fixed there. Don't pretend to honor it -- warn so the user isn't misled into thinking Studio + # landed at their custom path (the uninstaller still cleans the WSL install regardless). + if ($envOverride) { + substep "note: $envOverrideVar='$envOverride' is not used for the Windows-on-ARM WSL install -- Studio installs inside WSL at /root/.unsloth." "Yellow" + } + $wslReady = $false if (Get-Command wsl.exe -ErrorAction SilentlyContinue) { # Reset: a stale 0 would wrongly mark WSL ready if wsl.exe fails to start. @@ -2084,6 +2099,13 @@ exit 0 # Non-main ref: fetch + export THAT ref so the WSL venv gets the branch's setup.sh + patches # (else install.sh pulls PyPI unsloth). main == plain unsloth.ai/install.sh. $_instRef = Get-UnslothInstallRef + # The ref is spliced into the inner `bash -lc` twice (an `export` and a raw GitHub URL), so a + # hand-set UNSLOTH_INSTALL_REF containing shell metacharacters (`;`, `&`, `'`, spaces) would + # break/inject the command. git refs can't contain those anyway; enforce a strict allow-list + # (letters, digits, `.` `_` `/` `-`) and reject loudly rather than silently mangle the install. + if ($_instRef -ne 'main' -and ($_instRef -notmatch '^[A-Za-z0-9][A-Za-z0-9._/-]*$')) { + return (Exit-InstallFailure "UNSLOTH_INSTALL_REF='$_instRef' is not a valid git ref (allowed: letters, digits, '.', '_', '/', '-'). Set it to a real branch or tag name." 1) + } # UNSLOTH_WSL_LLAMA_DEFERRED=1: setup.sh skips its foreground CUDA llama.cpp build since we build it # in the background. apt stderr stays visible (only stdout -> /dev/null) so failures are diagnosable. # Forward UNSLOTH_NO_LLAMA_CUDA into WSL: it also skips the dispatch below, so unforwarded setup.sh From fa44cb8c44238fc2cfdbd79eb6be81306c741ff5 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Sun, 21 Jun 2026 22:24:41 -0700 Subject: [PATCH 77/95] fix: address Codex review on WoA deferral, uninstall port-kill, and build preservation Three valid findings from the 06-22 Codex review: 1. provision_llama_cuda.sh: when $LLAMA_DIR holds a .git checkout (a prior CPU source build), the whole-dir backup was skipped, so a failed CUDA rebuild's 'rm -rf build' destroyed the working CPU server with nothing to restore -- leaving NO llama-server despite the 'keeps the existing server' promise (a thermal shutdown mid-build is a real failure mode on this hardware). Back up build/bin before the rebuild and restore it on total failure; idempotent and self-cleaning (never overwrites a freshly built server). Verified both paths. 2. uninstall.ps1: 'fuser -k 8888/tcp' killed ANY listener on 8888 (Jupyter et al. default to it), not just Studio. Now only kills a PID whose /proc/cmdline is under /root/.unsloth -- matching the adjacent pkill scoping. 3. setup.sh: the 'defer to background CUDA build' branch fired even on a direct in-WSL 'unsloth studio update', where install.ps1 never launched a background builder -- so the footer claimed a build was running while nothing built. Gate it on UNSLOTH_WSL_LLAMA_DEFERRED=1 (set only by install.ps1, and already read elsewhere in setup.sh); a direct run now falls through to a real CPU build. bash -n + PS parse clean; the common install.ps1 WoA path (prebuilt success, deferred flag set) is unaffected. --- scripts/uninstall.ps1 | 13 +++++----- studio/scripts/provision_llama_cuda.sh | 33 +++++++++++++++++++++++--- studio/setup.sh | 12 ++++++---- 3 files changed, 45 insertions(+), 13 deletions(-) diff --git a/scripts/uninstall.ps1 b/scripts/uninstall.ps1 index 020fb85781..34e60fbc93 100644 --- a/scripts/uninstall.ps1 +++ b/scripts/uninstall.ps1 @@ -474,12 +474,13 @@ function Uninstall-UnslothStudio { # Probe candidates by exit code ('' = default distro) since `wsl --list` emits UTF-16 PS # mis-parses. rm runs FIRST (the kills could SIGKILL this shell) and drops the dangling # /root/.local/bin/unsloth symlink. Scope STRICTLY to /root (the fallback's install dir); - # /home/*/.unsloth may be another user's. The 8888 kill is gated on an Unsloth install - # existing (checked BEFORE rm deletes the marker) so an unrelated listener survives. pkill - # matches argv containing /root/.unsloth/ (not bare names that would hit a user's own - # llama-server); the backslash + [h]-bracket in '/root/\.unslot[h]/' keep it from matching - # this command's own argv. - $_clean = '_had=0; if [ -d /root/.unsloth ] || [ -L /root/.local/bin/unsloth ]; then _had=1; fi; rm -rf /root/.unsloth /root/llama-cuda /root/provision_llama_cuda.sh /root/llama_cuda_build.log 2>/dev/null; rm -f /root/.local/bin/unsloth 2>/dev/null; if [ $_had -eq 1 ]; then fuser -k 8888/tcp 2>/dev/null; fi; pkill -9 -f ''/root/\.unslot[h]/'' 2>/dev/null; true' + # /home/*/.unsloth may be another user's. The 8888 kill only targets a listener whose + # process cmdline is under /root/.unsloth (Studio's bind), so an unrelated service on 8888 + # -- Jupyter et al. default to it -- is NOT killed; it's also gated on an Unsloth install + # having existed (checked BEFORE rm deletes the marker). pkill matches argv containing + # /root/.unsloth/ (not bare names that would hit a user's own llama-server); the backslash + # + [h]-bracket in '/root/\.unslot[h]/' keep it from matching this command's own argv. + $_clean = '_had=0; if [ -d /root/.unsloth ] || [ -L /root/.local/bin/unsloth ]; then _had=1; fi; rm -rf /root/.unsloth /root/llama-cuda /root/provision_llama_cuda.sh /root/llama_cuda_build.log 2>/dev/null; rm -f /root/.local/bin/unsloth 2>/dev/null; if [ $_had -eq 1 ]; then for _p in $(fuser 8888/tcp 2>/dev/null); do grep -qa /root/\.unsloth/ /proc/$_p/cmdline 2>/dev/null && kill -9 $_p 2>/dev/null; done; fi; pkill -9 -f ''/root/\.unslot[h]/'' 2>/dev/null; true' # Clean only distros with evidence of a fallback install: the wsl-distro.txt marker or an # explicit UNSLOTH_WSL_DISTRO. The broad candidate probe is only for legacy marker-less # installs (ARM64 only); on x86 it would delete distros this installer never touched diff --git a/studio/scripts/provision_llama_cuda.sh b/studio/scripts/provision_llama_cuda.sh index 29ffc0052e..43779c0d10 100644 --- a/studio/scripts/provision_llama_cuda.sh +++ b/studio/scripts/provision_llama_cuda.sh @@ -172,6 +172,30 @@ if [ ! -d "$LLAMA_DIR/.git" ]; then fi cd "$LLAMA_DIR" || { _restore_prev; exit 0; } +# When rebuilding in-place over an existing git checkout, the whole-dir backup above +# was skipped (_LLAMA_BAK empty) -- but build/ may already hold a working (e.g. CPU) +# llama-server from a prior setup.sh source build. The wipe-on-failure paths below +# would destroy it with nothing to restore, leaving NO server despite the "keeps the +# existing server" promise (a thermal shutdown mid-CUDA-build is a real failure mode +# here). Back up the existing binaries so a failed rebuild can put them back. Only +# bin/ (server + dlopen-ed backends) is needed; cheap since any pre-existing server +# here is the non-CUDA fallback (a CUDA one would have exited at step 0). +_BUILD_BAK="" +if [ -z "$_LLAMA_BAK" ] && [ -x "$SERVER" ]; then + _BUILD_BAK="${LLAMA_DIR}.binbak.$$" + rm -rf "$_BUILD_BAK" 2>/dev/null + cp -a "$LLAMA_DIR/build/bin" "$_BUILD_BAK" 2>/dev/null || _BUILD_BAK="" +fi +_restore_build() { + if [ -n "$_BUILD_BAK" ] && [ -e "$_BUILD_BAK" ] && [ ! -x "$SERVER" ]; then + mkdir -p "$LLAMA_DIR/build" 2>/dev/null + rm -rf "$LLAMA_DIR/build/bin" 2>/dev/null + mv "$_BUILD_BAK" "$LLAMA_DIR/build/bin" 2>/dev/null && log "restored previous llama-server (rebuild failed)" + fi + [ -n "$_BUILD_BAK" ] && rm -rf "$_BUILD_BAK" 2>/dev/null + _BUILD_BAK="" +} + log "building CUDA llama.cpp (arch=$CUDA_ARCH, host=$HCXX) - this takes a few minutes..." _cmake_configure() { cmake -S . -B build -DCMAKE_BUILD_TYPE=Release \ @@ -185,7 +209,7 @@ _cmake_configure() { if ! _cmake_configure; then log "stale/incompatible CMake cache detected; wiping build dir for a clean CUDA configure" rm -rf build - _cmake_configure || { log "cmake configure failed"; cd /; _restore_prev; exit 0; } + _cmake_configure || { log "cmake configure failed"; cd /; _restore_build; _restore_prev; exit 0; } fi # Also builds the targets unsloth-zoo's GGUF exporter needs (llama-mtmd-cli, # llama-gguf-split). Jobs default to ~half the cores (full -j(nproc) CUDA builds @@ -226,10 +250,13 @@ if ! _cmake_build; then # (undefined ggml_cuda_op_* refs); wipe and rebuild clean. log "build failed (likely interrupted/partial); wiping build dir and rebuilding clean" rm -rf build - _cmake_configure || { log "cmake configure failed"; cd /; _restore_prev; exit 0; } - _cmake_build || { log "cmake build failed"; cd /; _restore_prev; exit 0; } + _cmake_configure || { log "cmake configure failed"; cd /; _restore_build; _restore_prev; exit 0; } + _cmake_build || { log "cmake build failed"; cd /; _restore_build; _restore_prev; exit 0; } fi _cmake_build_extras +# Drop the backup on a successful build, or restore the prior server if the rebuild +# yielded none (idempotent; only restores when $SERVER is missing). +_restore_build if is_cuda_server "$SERVER"; then log "CUDA llama-server ready: $SERVER" diff --git a/studio/setup.sh b/studio/setup.sh index 87e99acf53..bf22bea51e 100755 --- a/studio/setup.sh +++ b/studio/setup.sh @@ -1253,11 +1253,15 @@ if [ "$_NEED_LLAMA_SOURCE_BUILD" = true ] && \ fi # ── WSL2 aarch64 + NVIDIA, no nvcc yet: defer to the background CUDA build ── -# install.ps1 builds the CUDA llama-server in the background; without nvcc, -# section 9 could only make a slow CPU server that build discards. With nvcc we -# fall through to section 9; opted out (UNSLOTH_NO_LLAMA_CUDA=1) the CPU build is -# the only server. +# install.ps1 builds the CUDA llama-server in the background AND signals that by +# exporting UNSLOTH_WSL_LLAMA_DEFERRED=1 into this install. ONLY defer when that +# flag is set: a direct in-WSL `unsloth studio update` has no background builder, +# so deferring there would report "CUDA build running in background" while nothing +# builds -- hiding a no-server state. Without the flag we fall through to section 9 +# (a slow CPU server, still better than a phantom background build). With nvcc we +# fall through too; opted out (UNSLOTH_NO_LLAMA_CUDA=1) the CPU build is the only server. if [ "$_NEED_LLAMA_SOURCE_BUILD" = true ] \ + && [ "${UNSLOTH_WSL_LLAMA_DEFERRED:-0}" = "1" ] \ && [ "$_LLAMA_FORCE_COMPILE" != "1" ] \ && [ -z "$_LLAMA_PR" ] \ && [ "${UNSLOTH_NO_LLAMA_CUDA:-0}" != "1" ] \ From bb2bd339431e21948daf43ab707de492f26f9418 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Sun, 21 Jun 2026 23:25:09 -0700 Subject: [PATCH 78/95] fix: restore rolled-aside venv on WoA WSL-routing early rejects On Windows-on-ARM + NVIDIA, an existing native Studio venv is rolled aside (Start-StudioVenvRollback) before the WSL-routing block. The TauriMode, --local, and invalid-UNSLOTH_INSTALL_REF rejects returned without calling Restore-StudioVenvRollback, orphaning the user's previous venv backup. Restore it on all three early exits, matching the deferred-reboot / WSL1-conversion / final-failure paths that already do. Restore-StudioVenvRollback no-ops when nothing was rolled aside, so the fresh-install case is unaffected. Addresses Codex review (venv-rollback ordering on the WoA reject paths). --- install.ps1 | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/install.ps1 b/install.ps1 index 30db2e9065..9877bb18a0 100644 --- a/install.ps1 +++ b/install.ps1 @@ -2013,6 +2013,10 @@ exit 0 # The Tauri desktop app launches its backend from a Windows venv (resolve_backend_binary), not # WSL, so a WSL-only install would start nothing -- send those users to the CLI installer. if ($TauriMode) { + # A prior native Studio venv was already rolled aside (Start-StudioVenvRollback, + # ~L1444) before we got here; restore it so rejecting this path doesn't orphan + # the user's working install. No-op when nothing was rolled aside. + Restore-StudioVenvRollback return (Exit-InstallFailure "Windows-on-ARM + NVIDIA GPU needs the WSL2 GPU install, which the desktop app can't launch yet. Install from PowerShell instead: irm https://unsloth.ai/install.ps1 | iex" 1) } @@ -2021,6 +2025,7 @@ exit 0 # --local run here would silently install the published package inside WSL and report success. # Reject it and point at the supported pre-merge mechanism (push the branch + UNSLOTH_INSTALL_REF). if ($StudioLocalInstall) { + Restore-StudioVenvRollback # see TauriMode note above: don't orphan a rolled-aside venv return (Exit-InstallFailure "--local can't be honored on Windows-on-ARM + NVIDIA: the GPU install runs inside WSL2 and installs from a published/git ref, not this Windows checkout. For pre-merge testing, push your branch and set UNSLOTH_INSTALL_REF, e.g.: `$env:UNSLOTH_INSTALL_REF=''; irm https://unsloth.ai/install.ps1 | iex" 1) } # A custom Studio root (UNSLOTH_STUDIO_HOME / STUDIO_HOME) only applies to the native Windows @@ -2104,6 +2109,7 @@ exit 0 # break/inject the command. git refs can't contain those anyway; enforce a strict allow-list # (letters, digits, `.` `_` `/` `-`) and reject loudly rather than silently mangle the install. if ($_instRef -ne 'main' -and ($_instRef -notmatch '^[A-Za-z0-9][A-Za-z0-9._/-]*$')) { + Restore-StudioVenvRollback # see TauriMode note above: don't orphan a rolled-aside venv return (Exit-InstallFailure "UNSLOTH_INSTALL_REF='$_instRef' is not a valid git ref (allowed: letters, digits, '.', '_', '/', '-'). Set it to a real branch or tag name." 1) } # UNSLOTH_WSL_LLAMA_DEFERRED=1: setup.sh skips its foreground CUDA llama.cpp build since we build it From 9ae556580374744b8a34b5a4fbdb1c9b7b100301 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Sat, 18 Jul 2026 08:23:23 +0000 Subject: [PATCH 79/95] install: close six WoA/WSL review gaps in provisioning, shortcuts, uninstall Review round on the Windows-on-ARM + NVIDIA WSL2 path; each item reproduced against the live scripts before fixing. provision_llama_cuda.sh now serializes with install_llama_prebuilt.py on the same /..install.lock file (its filelock backend is flock(2), so shell flock interoperates; append-mode open so the Python O_EXCL fallback's PID file is never truncated). The detached background builder could otherwise race an installer rerun or `unsloth studio update`, both of which mv/rm -rf inside the llama.cpp dir. Losing the 2h wait exits 0: another provisioner is already doing the job. The step-0 early-skip trusted a co-located libggml-cuda.so alone, which wrongly skips one case: an in-place rebuild interrupted after the .so links but before llama-server relinks leaves new .so + old CPU server. A completion stamp (build/bin/.unsloth-cuda-ok) written only after the script's own final CUDA check closes that window; skip now requires ldd evidence or the stamp. The rejected functional --list-devices probe stays rejected: the stamp does not gamble thermals on an env-fragile probe. The WSL shortcut skip (install.ps1 owns the canonical WoA .lnk) was only a transient env var, so the first `unsloth studio update`, whose wsl.exe shim carries no env into install.sh --shortcuts-only, recreated the duplicate blank-icon shortcut. The skip is now also persisted as /root/.unsloth/.skip-wsl-windows-shortcut, checked by install.sh and removed with the install by both uninstallers. --with-llama-cpp-dir (and UNSLOTH_LOCAL_LLAMA_CPP_DIR) were parsed but silently ignored on the WSL fallback path, which builds its own llama.cpp inside the distro. Reject with guidance (UNSLOTH_LLAMA_TAG / UNSLOTH_LLAMA_PR pin the WSL-side build), mirroring the --local reject. uninstall.sh's Windows shortcut sweep only removed wsl.exe-target .lnks, so the WoA fallback shortcuts (powershell.exe + launch-studio-wsl.ps1) survived while their launcher dir was deleted, leaving dangling shortcuts. The owner-matched cleanup now removes them first. uninstall.ps1 swept every "Unsloth Studio (*.lnk" as legacy, but install.sh creates exactly that per-distro name for current WSL installs, and the WSL cleanup below only removes evidenced distros. The sweep now keeps a live wsl.exe launcher whose distro is not in the same evidence set, so a surviving WSL install keeps its shortcut; everything else is still swept. Verified: bash -n on all three shell scripts, PowerShell AST parse on both ps1 files, flock mutual-exclusion and stamp skip/rebuild decisions exercised standalone, and the uninstall icon suites (sh + ps1) pass. The test_install_host_defaults.sh failure pre-exists on the branch merge base. --- install.ps1 | 12 +++++++- install.sh | 3 +- scripts/uninstall.ps1 | 41 +++++++++++++++++++++++--- scripts/uninstall.sh | 10 +++++++ studio/scripts/provision_llama_cuda.sh | 35 ++++++++++++++++++---- 5 files changed, 89 insertions(+), 12 deletions(-) diff --git a/install.ps1 b/install.ps1 index 900cc0ba46..70d449b83f 100644 --- a/install.ps1 +++ b/install.ps1 @@ -2111,6 +2111,13 @@ exit 0 if ($envOverride) { substep "note: $envOverrideVar='$envOverride' is not used for the Windows-on-ARM WSL install -- Studio installs inside WSL at /root/.unsloth." "Yellow" } + # --with-llama-cpp-dir names a Windows-side llama.cpp, but this install runs + # llama.cpp inside WSL2 and would silently ignore the user's explicit binary + # choice. Reject like --local and point at the supported WSL-side pins. + if ($WithLlamaCppDir -or $env:UNSLOTH_LOCAL_LLAMA_CPP_DIR) { + Restore-StudioVenvRollback + return (Exit-InstallFailure "--with-llama-cpp-dir / UNSLOTH_LOCAL_LLAMA_CPP_DIR can't be honored on Windows-on-ARM + NVIDIA: llama.cpp runs inside WSL2 and can't use a Windows path. Remove it, or pin the WSL-side build with UNSLOTH_LLAMA_TAG or UNSLOTH_LLAMA_PR instead." 1) + } $wslReady = $false if (Get-Command wsl.exe -ErrorAction SilentlyContinue) { @@ -2208,7 +2215,10 @@ exit 0 # install.ps1 owns the WoA shortcut (one canonical "Unsloth Studio.lnk" with a # %USERPROFILE%\.unsloth icon that renders on WoA). Tell install.sh to skip its own # WSL .lnk so we don't get a duplicate whose %LOCALAPPDATA% icon renders blank. - $_fwdEnv += 'export UNSLOTH_SKIP_WSL_WINDOWS_SHORTCUT=1; ' + # Persist the skip as a marker file too: `unsloth studio update` reruns + # install.sh --shortcuts-only through the wsl.exe shim, which carries no env, + # so without the marker the first update would recreate the duplicate .lnk. + $_fwdEnv += 'export UNSLOTH_SKIP_WSL_WINDOWS_SHORTCUT=1; mkdir -p /root/.unsloth; touch /root/.unsloth/.skip-wsl-windows-shortcut; ' if ($_instRef -eq 'main') { $wslInstall = $_fwdEnv + 'export DEBIAN_FRONTEND=noninteractive UNSLOTH_WSL_LLAMA_DEFERRED=1; apt-get update -y >/dev/null; apt-get install -y build-essential cmake git curl pciutils libcurl4-openssl-dev >/dev/null; curl -fsSL https://unsloth.ai/install.sh | sh' } else { diff --git a/install.sh b/install.sh index 02cd50b5d4..abc745076f 100755 --- a/install.sh +++ b/install.sh @@ -1234,7 +1234,8 @@ STUB_EOF fi _css_created=1 - elif [ "$_css_os" = "wsl" ] && [ "${UNSLOTH_SKIP_WSL_WINDOWS_SHORTCUT:-0}" != "1" ]; then + elif [ "$_css_os" = "wsl" ] && [ "${UNSLOTH_SKIP_WSL_WINDOWS_SHORTCUT:-0}" != "1" ] \ + && [ ! -f "$HOME/.unsloth/.skip-wsl-windows-shortcut" ]; then # ── WSL: create Windows Desktop and Start Menu shortcuts ── # Detect current WSL distro for targeted shortcut _css_distro="${WSL_DISTRO_NAME:-}" diff --git a/scripts/uninstall.ps1 b/scripts/uninstall.ps1 index 6aef26d437..fb30e2dd1e 100644 --- a/scripts/uninstall.ps1 +++ b/scripts/uninstall.ps1 @@ -399,17 +399,50 @@ function Uninstall-UnslothStudio { } # ── Remove desktop and Start Menu shortcuts ── - # Canonical name is "Unsloth Studio.lnk"; also sweep legacy distro-suffixed names - # ("Unsloth Studio (WSL - ).lnk") left by pre-release dev builds. + # Canonical name is "Unsloth Studio.lnk". Distro-suffixed names + # ("Unsloth Studio (WSL - ).lnk") belong to per-distro WSL installs, which + # the WSL-fallback section below only cleans for evidenced distros (env var, + # wsl-distro.txt marker, or the legacy ARM64 probe) -- scope this sweep to the + # same set so a surviving WSL install keeps its launcher. Anything that is not a + # live wsl.exe launcher (pre-release leftovers) is still swept. _Step "Removing desktop and Start Menu shortcuts..." + $_scCands = @() + if ($env:UNSLOTH_WSL_DISTRO) { $_scCands += $env:UNSLOTH_WSL_DISTRO } + try { + if ($env:LOCALAPPDATA) { + $_scDf = Join-Path (Join-Path $env:LOCALAPPDATA "Unsloth") "wsl-distro.txt" + if (Test-Path -LiteralPath $_scDf) { + $_scRd = (Get-Content -LiteralPath $_scDf -ErrorAction SilentlyContinue | Select-Object -First 1) + if ($_scRd -and $_scRd.Trim()) { $_scCands += $_scRd.Trim() } + } + } + } catch { } + if ((-not $_scCands) -and ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64')) { + $_scCands = @('Ubuntu', 'Ubuntu-24.04', 'Ubuntu-22.04', 'Debian') + } + $_scWs = $null + try { $_scWs = New-Object -ComObject WScript.Shell } catch { } $shortcutDirs = @() try { $d = [Environment]::GetFolderPath("Desktop"); if ($d) { $shortcutDirs += $d } } catch { } if ($env:APPDATA) { $shortcutDirs += (Join-Path $env:APPDATA "Microsoft\Windows\Start Menu\Programs") } foreach ($dir in $shortcutDirs) { if (-not (Test-Path -LiteralPath $dir)) { continue } _RemovePath (Join-Path $dir "Unsloth Studio.lnk") - Get-ChildItem -LiteralPath $dir -Filter "Unsloth Studio (*.lnk" -ErrorAction SilentlyContinue | - ForEach-Object { _RemovePath $_.FullName } + Get-ChildItem -LiteralPath $dir -Filter "Unsloth Studio (*.lnk" -ErrorAction SilentlyContinue | ForEach-Object { + $_scKeep = $false + if ($_scWs) { + try { + $_sc = $_scWs.CreateShortcut($_.FullName) + if ("$($_sc.TargetPath) $($_sc.Arguments)" -match "wsl\.exe") { + $_scD = $null + if ($_sc.Arguments -match '-d\s+"?([^"\s]+)"?') { $_scD = $Matches[1] } + elseif ($_.Name -match '^Unsloth Studio \(WSL - (.+)\)\.lnk$') { $_scD = $Matches[1] } + if ($_scD -and ($_scCands -notcontains $_scD)) { $_scKeep = $true } + } + } catch { } + } + if (-not $_scKeep) { _RemovePath $_.FullName } + } } # Invalidate the Win11 Start Menu tile cache so the removed shortcut's tile # disappears promptly instead of lingering stale (mirrors install.ps1's diff --git a/scripts/uninstall.sh b/scripts/uninstall.sh index b67c013870..5509e18a4e 100755 --- a/scripts/uninstall.sh +++ b/scripts/uninstall.sh @@ -320,6 +320,16 @@ case "$_os" in $shim = (Join-Path $ud "bin").TrimEnd("\","/"); $up = [Environment]::GetEnvironmentVariable("Path","User"); if ($up) { [Environment]::SetEnvironmentVariable("Path", (($up -split ";" | Where-Object { $_ -and ($_.TrimEnd("\","/") -ine $shim) }) -join ";"), "User") } + # The WoA-fallback shortcuts target powershell.exe + launch-studio-wsl.ps1 + # (not wsl.exe), so the sweep above keeps them; remove them here before + # their launcher dir is deleted or they would dangle. + foreach ($d in $dirs) { + if (-not $d -or -not (Test-Path -LiteralPath $d)) { continue } + $l = Join-Path $d "Unsloth Studio.lnk"; + if (Test-Path -LiteralPath $l) { + try { $sc2 = $ws.CreateShortcut($l); if ($sc2.Arguments -match "launch-studio-wsl\.ps1") { Remove-Item -LiteralPath $l -Force -ErrorAction SilentlyContinue } } catch { } + } + } if (Test-Path -LiteralPath $ud) { Remove-Item -LiteralPath $ud -Recurse -Force -ErrorAction SilentlyContinue } } # Keep the shared icon while any Unsloth shortcut still uses it (native diff --git a/studio/scripts/provision_llama_cuda.sh b/studio/scripts/provision_llama_cuda.sh index 43779c0d10..def6d61d6e 100644 --- a/studio/scripts/provision_llama_cuda.sh +++ b/studio/scripts/provision_llama_cuda.sh @@ -13,6 +13,22 @@ LLAMA_DIR="${UNSLOTH_LLAMA_CPP_PATH:-$HOME/.unsloth/llama.cpp}" SERVER="$LLAMA_DIR/build/bin/llama-server" log() { printf ' - %s\n' "$*"; } +# Serialize against install_llama_prebuilt.py (same lock file as its +# install_lock_path: /..install.lock; its filelock backend is +# flock(2), so this interoperates) and against a second copy of this script: the +# detached background builder can otherwise race an installer rerun or `unsloth +# studio update`, both of which mv/rm -rf inside $LLAMA_DIR. Append-mode open so +# the Python O_EXCL fallback's PID file is never truncated. 2h cap matches a +# worst-case source build; losing the wait means another provisioner is already +# doing this exact job, so exiting 0 is correct. +_LOCK_DIR="$(dirname "$LLAMA_DIR")" +mkdir -p "$_LOCK_DIR" 2>/dev/null +if command -v flock >/dev/null 2>&1; then + if exec 9>>"$_LOCK_DIR/.$(basename "$LLAMA_DIR").install.lock" 2>/dev/null; then + flock -w 7200 9 || { log "another llama.cpp install holds the lock; skipping"; exit 0; } + fi +fi + # Detect CUDA two ways: monolithic (libggml-cuda in ldd) or split (dlopen-ed # libggml-cuda.so* beside the binary, missed by ldd). CPU-only builds ship no # libggml-cuda.so, so its presence is the reliable signal. @@ -23,18 +39,24 @@ is_cuda_server() { return 1 } -# 0. Already provisioned? A co-located libggml-cuda.so* is the trusted signal: the -# prebuilt resolver validates the server it installs, and a *source* build that gets -# interrupted is caught by the build-failure wipe+rebuild below (section 6), so the -# early-skip can rely on the structural check. We deliberately do NOT run a functional +# 0. Already provisioned? Skip when the server links libggml-cuda directly (ldd) +# or when a co-located libggml-cuda.so* is paired with the completion stamp this +# script writes after its own final CUDA check. The stamp closes the one gap in +# the structural check: an in-place rebuild interrupted after libggml-cuda.so is +# linked but before llama-server relinks leaves new .so + old CPU server, which +# the bare .so test would wrongly skip. We deliberately do NOT run a functional # `--list-devices` probe here: this script runs in a stripped-down detached shell whose # loader path can miss /usr/lib/wsl/lib, so the CUDA backend may fail to enumerate even # on a perfectly good server -- and a false negative would wipe a validated build and # trigger a needless, thermally-dangerous source rebuild on the NVIDIA-ARM laptops this # targets. Trust the .so; never gamble the machine's thermals on an env-fragile probe. +_CUDA_STAMP="$LLAMA_DIR/build/bin/.unsloth-cuda-ok" if is_cuda_server "$SERVER"; then - log "CUDA llama-server already present: $SERVER" - exit 0 + if ldd "$SERVER" 2>/dev/null | grep -qi 'libggml-cuda' || [ -e "$_CUDA_STAMP" ]; then + log "CUDA llama-server already present: $SERVER" + exit 0 + fi + log "CUDA .so present but the build never stamped complete (interrupted relink?); rebuilding" fi # 1. Require an NVIDIA GPU (this script is only meaningful with one). @@ -259,6 +281,7 @@ _cmake_build_extras _restore_build if is_cuda_server "$SERVER"; then + : > "$_CUDA_STAMP" 2>/dev/null || true log "CUDA llama-server ready: $SERVER" [ -n "$_LLAMA_BAK" ] && rm -rf "$_LLAMA_BAK" 2>/dev/null elif [ -x "$SERVER" ]; then From daf06e2c28e9a218c0466917070e0570ba1ee835 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Sat, 18 Jul 2026 09:14:35 +0000 Subject: [PATCH 80/95] install: fix eight WoA/WSL review findings across probe, worker, provisioner Second review round on the Windows-on-ARM + NVIDIA path; each item verified against the live code (and torch where relevant) before fixing. The native-CUDA probe ran uv --dry-run against the venv interpreter without checking its architecture. uv resolves for the interpreter's platform tags, so an x64-emulated python resolved existing win_amd64 CUDA wheels and "proved" a native wheel WoA cannot use, skipping the WSL fallback entirely. The probe now requires platform.machine() ARM64 from the venv python first; anything else keeps the WSL routing. The Studio worker appended PYTORCH_CUDA_ALLOC_CONF next to its memory-fraction logic, 550 lines after detect_hardware() had already initialized CUDA, where the allocator config is latched (verified on torch 2.9.1: expandable_segments set after get_device_properties is a no-op in memory snapshots). The CUDA-free Spark sniff now runs immediately before detect_hardware(), and it honors the documented UNSLOTH_FORCE_DGX_SPARK=1/0 override the library detectors support, closing the older force-flag item on the same block. setup.sh's _have_cuda_llama_server accepted any co-located libggml-cuda.so, re-opening the interrupted-relink hole the provisioner's completion stamp was added to close: in exactly that state setup.sh skipped provisioning and reported CUDA ready over the old CPU binary. The split-.so branch now also requires the stamp; monolithic ldd-linked builds are unaffected. The provisioner builds llama-quantize but never created the repo-root shim that unsloth_zoo's check_llama_cpp needs (it only searches the root, which is why setup.sh symlinks it in all three of its own paths). The success branch now mirrors that symlink. CMAKE_CUDA_ARCHITECTURES=native needs CMake >= 3.24, but this script installs distro cmake (Ubuntu 22.04 apt ships 3.22), so the N/A-compute_cap fallback aborted configure, wiped build/, and aborted again. The fallback now omits the flag and lets ggml's version-guarded CMake defaults pick the arches. Fresh clones tracked ggml-org master, bypassing setup.sh's newest-release pin policy (its own header warns master bypasses the pin). An unset or "latest" ref now resolves to the newest release tag via the GitHub API, keeping the default-branch clone as the best-effort fallback when the API is unreachable. install.sh writes the WSL shortcut icon to the Windows profile (%USERPROFILE%\.unsloth\unsloth.ico) because the WoA icon broker cannot read AppData\Local, but both uninstall.sh cleanup sites only cleaned the AppData\Local icon. Both now clean the profile icon and drop the directory when empty. Verified: bash -n on all four shell scripts, AST parse on worker.py, PowerShell AST parse on both ps1 files, the icon suites pass, and the sh test battery matches the branch baseline (test_install_host_defaults.sh fails identically on the clean tree). --- install.ps1 | 30 +++++++---- scripts/uninstall.sh | 13 +++++ studio/backend/core/training/worker.py | 72 +++++++++++++++----------- studio/scripts/provision_llama_cuda.sh | 27 ++++++++-- studio/setup.sh | 9 +++- 5 files changed, 104 insertions(+), 47 deletions(-) diff --git a/install.ps1 b/install.ps1 index 70d449b83f..a4f55d99a7 100644 --- a/install.ps1 +++ b/install.ps1 @@ -2070,17 +2070,25 @@ exit 0 } $_nativeCudaTorchOk = $false if ($_winArm64 -and $HasNvidiaSmi -and (-not $SkipTorch)) { - # Probe the SAME spec as the real install ("torch>=2.4,<2.11.0"); a bare `torch` probe could - # match an out-of-range wheel, skipping WSL only to fail the real pinned install. - $prevEapProbe = $ErrorActionPreference; $ErrorActionPreference = "Continue" - # --reinstall: an installed (e.g. CPU-only) torch mustn't satisfy the probe -- it must prove - # a native win_arm64 CUDA wheel exists on the index. - $global:LASTEXITCODE = -1 - try { - & uv pip install --python $VenvPython --dry-run --reinstall "torch>=2.4,<2.11.0" --index-url $TorchIndexUrl *> $null - $_nativeCudaTorchOk = ($LASTEXITCODE -eq 0) - } catch { $_nativeCudaTorchOk = $false } finally { $ErrorActionPreference = $prevEapProbe } - if ($_nativeCudaTorchOk) { step "gpu" "native CUDA PyTorch now available for win_arm64 -- keeping native install" "Green" } + # uv resolves for the interpreter's platform tags, so an x64-emulated venv + # python resolves the existing win_amd64 CUDA wheels and would "prove" a + # native wheel WoA can't actually use. Only a real win_arm64 interpreter + # can prove a win_arm64 CUDA wheel; anything else keeps the WSL fallback. + $_pyArch = "" + try { $_pyArch = (& $VenvPython -c "import platform; print(platform.machine())" 2>$null | Select-Object -First 1) } catch {} + if ("$_pyArch" -imatch 'ARM64') { + # Probe the SAME spec as the real install ("torch>=2.4,<2.11.0"); a bare `torch` probe could + # match an out-of-range wheel, skipping WSL only to fail the real pinned install. + $prevEapProbe = $ErrorActionPreference; $ErrorActionPreference = "Continue" + # --reinstall: an installed (e.g. CPU-only) torch mustn't satisfy the probe -- it must prove + # a native win_arm64 CUDA wheel exists on the index. + $global:LASTEXITCODE = -1 + try { + & uv pip install --python $VenvPython --dry-run --reinstall "torch>=2.4,<2.11.0" --index-url $TorchIndexUrl *> $null + $_nativeCudaTorchOk = ($LASTEXITCODE -eq 0) + } catch { $_nativeCudaTorchOk = $false } finally { $ErrorActionPreference = $prevEapProbe } + if ($_nativeCudaTorchOk) { step "gpu" "native CUDA PyTorch now available for win_arm64 -- keeping native install" "Green" } + } } if ($_winArm64 -and $HasNvidiaSmi -and (-not $_nativeCudaTorchOk) -and (-not $SkipTorch) -and ($env:UNSLOTH_NO_WSL_FALLBACK -ne '1')) { step "wsl" "Windows on ARM + NVIDIA, native CUDA unavailable -- routing GPU setup through WSL2" diff --git a/scripts/uninstall.sh b/scripts/uninstall.sh index 5509e18a4e..eeaa0a3169 100755 --- a/scripts/uninstall.sh +++ b/scripts/uninstall.sh @@ -346,6 +346,15 @@ case "$_os" in $ico = Join-Path $iconDir "unsloth.ico"; if ((-not $iconInUse) -and (Test-Path -LiteralPath $ico)) { Remove-Item -LiteralPath $ico -Force -ErrorAction SilentlyContinue } if ((Test-Path -LiteralPath $iconDir) -and -not (Get-ChildItem -LiteralPath $iconDir -Force -ErrorAction SilentlyContinue)) { Remove-Item -LiteralPath $iconDir -Recurse -Force -ErrorAction SilentlyContinue } + } + # install.sh also writes the WSL shortcut icon to the Windows + # profile (%USERPROFILE%\.unsloth\unsloth.ico) because the WoA + # icon broker cannot read AppData\Local; clean it the same way. + if (-not [string]::IsNullOrWhiteSpace($env:USERPROFILE)) { + $pIconDir = Join-Path $env:USERPROFILE ".unsloth"; + $pIco = Join-Path $pIconDir "unsloth.ico"; + if ((-not $iconInUse) -and (Test-Path -LiteralPath $pIco)) { Remove-Item -LiteralPath $pIco -Force -ErrorAction SilentlyContinue } + if ((Test-Path -LiteralPath $pIconDir) -and -not (Get-ChildItem -LiteralPath $pIconDir -Force -ErrorAction SilentlyContinue)) { Remove-Item -LiteralPath $pIconDir -Recurse -Force -ErrorAction SilentlyContinue } }' >/dev/null 2>&1 || true fi # Remove $1's shared unsloth.ico only if no Unsloth shortcut (native install @@ -369,6 +378,10 @@ case "$_os" in done if [ "$_icon_in_use" = "0" ]; then [ -f "$_icodir/unsloth.ico" ] && rm -f "$_icodir/unsloth.ico" 2>/dev/null || true + # install.sh also writes the icon to the Windows profile + # (%USERPROFILE%\.unsloth) for the WoA icon broker. + [ -f "$_du/.unsloth/unsloth.ico" ] && rm -f "$_du/.unsloth/unsloth.ico" 2>/dev/null || true + [ -d "$_du/.unsloth" ] && rmdir "$_du/.unsloth" 2>/dev/null || true fi [ -d "$_icodir" ] && rmdir "$_icodir" 2>/dev/null || true } diff --git a/studio/backend/core/training/worker.py b/studio/backend/core/training/worker.py index bccf8b3ce7..1bf71eae18 100644 --- a/studio/backend/core/training/worker.py +++ b/studio/backend/core/training/worker.py @@ -2294,6 +2294,44 @@ def run_training_process(*, event_queue: Any, stop_queue: Any, config: dict) -> from utils.hardware import hardware as _hw + # Set PYTORCH_CUDA_ALLOC_CONF before the first CUDA touch: detect_hardware() + # below calls get_device_properties, which latches the allocator config for + # this process (verified: expandable_segments set after init is a no-op). + # CUDA-free nvidia-smi sniff (mirrors _is_dgx_spark_no_cuda_init) honoring + # UNSLOTH_FORCE_DGX_SPARK, same append-don't-override behavior and + # UNSLOTH_NO_EXPANDABLE_SEGMENTS opt-out. + try: + import platform as _plat + import re as _re + + _force_spark = os.environ.get("UNSLOTH_FORCE_DGX_SPARK") + if _force_spark == "1": + _spark_smi = True + elif _force_spark == "0": + _spark_smi = False + else: + _spark_smi = False + if _plat.machine().lower() in ("aarch64", "arm64"): + _smi = _sp.run( + ["nvidia-smi", "--query-gpu=name", "--format=csv,noheader"], + capture_output = True, + text = True, + timeout = 5, + ) + _names_u = (_smi.stdout or "").upper() + _spark_smi = any( + _re.search(r"(? # Discrete NVIDIA GPUs untouched. else: try: - # Set PYTORCH_CUDA_ALLOC_CONF before get_device_properties below inits - # the allocator -- the later `import unsloth` patch is too late for THIS - # worker. CUDA-free nvidia-smi sniff (mirrors _is_dgx_spark_no_cuda_init), - # same append-don't-override and UNSLOTH_NO_EXPANDABLE_SEGMENTS opt-out. - try: - import platform as _plat - - _spark_smi = False - if _plat.machine().lower() in ("aarch64", "arm64"): - _smi = _sp.run( - ["nvidia-smi", "--query-gpu=name", "--format=csv,noheader"], - capture_output = True, - text = True, - timeout = 5, - ) - _names_u = (_smi.stdout or "").upper() - import re as _re - - _spark_smi = any( - _re.search(r"(? 121). Fallback: native. # Only a purely-numeric capability is a valid CMAKE_CUDA_ARCHITECTURES; some WSL -# GPU-PV / driver combos report "N/A", which CMake would reject (aborting an -# otherwise-usable build) instead of letting "native" autodetect. +# GPU-PV / driver combos report "N/A". "native" needs CMake >= 3.24 (Ubuntu +# 22.04 apt ships 3.22), so the fallback omits the flag entirely and lets +# ggml's version-guarded CMake defaults pick the arches instead. CC_CAP="$(nvidia-smi --query-gpu=compute_cap --format=csv,noheader 2>/dev/null | head -1 | tr -d ' .')" case "$CC_CAP" in - ''|*[!0-9]*) CUDA_ARCH="native" ;; + ''|*[!0-9]*) CUDA_ARCH="" ;; *) CUDA_ARCH="$CC_CAP" ;; esac @@ -151,6 +152,15 @@ esac # (same var setup.sh uses) instead of always tracking ggml-org main. mkdir -p "$(dirname "$LLAMA_DIR")" _LLAMA_REF="${UNSLOTH_LLAMA_TAG:-}" +# setup.sh's install policy pins source builds to the newest RELEASE ("latest" +# resolved to a tag; master bypasses the pin). Mirror it: unset or literal +# "latest" resolves via the GitHub API; on API failure the empty ref keeps the +# existing default-branch clone fallback (best effort, as before). +if [ -z "$_LLAMA_REF" ] || [ "$_LLAMA_REF" = "latest" ]; then + _LLAMA_REF="$(curl -fsSL --max-time 15 https://api.github.com/repos/ggml-org/llama.cpp/releases/latest 2>/dev/null \ + | grep -om1 '"tag_name": *"[^"]*"' | cut -d'"' -f4)" + [ -n "$_LLAMA_REF" ] && log "pinning llama.cpp to release $_LLAMA_REF" +fi # Back up any existing (e.g. CPU-only) llama.cpp: restored on any failure exit, # dropped only once the fresh build yields a server -- never leave NO server. _LLAMA_BAK="" @@ -218,11 +228,13 @@ _restore_build() { _BUILD_BAK="" } -log "building CUDA llama.cpp (arch=$CUDA_ARCH, host=$HCXX) - this takes a few minutes..." +log "building CUDA llama.cpp (arch=${CUDA_ARCH:-cmake-default}, host=$HCXX) - this takes a few minutes..." _cmake_configure() { + # Empty CUDA_ARCH (unreadable compute_cap): omit the flag so ggml's own + # CMake defaults apply -- "native" would need CMake >= 3.24. cmake -S . -B build -DCMAKE_BUILD_TYPE=Release \ -DGGML_CUDA=ON -DGGML_CUDA_F16=ON \ - -DCMAKE_CUDA_ARCHITECTURES="$CUDA_ARCH" \ + ${CUDA_ARCH:+-DCMAKE_CUDA_ARCHITECTURES="$CUDA_ARCH"} \ -DCMAKE_CUDA_HOST_COMPILER="$HCXX" \ -DLLAMA_CURL=ON >/dev/null 2>&1 } @@ -282,6 +294,11 @@ _restore_build if is_cuda_server "$SERVER"; then : > "$_CUDA_STAMP" 2>/dev/null || true + # unsloth_zoo's check_llama_cpp only searches the repo root, so mirror + # setup.sh's root shim for the GGUF exporter's quantize binary. + if [ -x "$LLAMA_DIR/build/bin/llama-quantize" ] && [ ! -e "$LLAMA_DIR/llama-quantize" ]; then + ln -sf build/bin/llama-quantize "$LLAMA_DIR/llama-quantize" 2>/dev/null || true + fi log "CUDA llama-server ready: $SERVER" [ -n "$_LLAMA_BAK" ] && rm -rf "$_LLAMA_BAK" 2>/dev/null elif [ -x "$SERVER" ]; then diff --git a/studio/setup.sh b/studio/setup.sh index 656c8b5204..3db9a0ebb1 100755 --- a/studio/setup.sh +++ b/studio/setup.sh @@ -1979,7 +1979,14 @@ fi # end _SKIP_GGUF_BUILD check _have_cuda_llama_server() { [ -x "$LLAMA_SERVER_BIN" ] || return 1 ldd "$LLAMA_SERVER_BIN" 2>/dev/null | grep -qi 'libggml-cuda' && return 0 - for _so in "$(dirname "$LLAMA_SERVER_BIN")"/libggml-cuda.so*; do [ -e "$_so" ] && return 0; done + # Split-.so builds on this path come from provision_llama_cuda.sh, which stamps + # .unsloth-cuda-ok only after its final CUDA check. Requiring the stamp keeps + # the interrupted-relink state (new .so + old CPU server) provisioning instead + # of being reported as ready. + _stamp="$(dirname "$LLAMA_SERVER_BIN")/.unsloth-cuda-ok" + for _so in "$(dirname "$LLAMA_SERVER_BIN")"/libggml-cuda.so*; do + [ -e "$_so" ] && [ -e "$_stamp" ] && return 0 + done return 1 } if [ "$_HOST_SYSTEM" = "Linux" ] \ From 6c1b739c36e7e3413ed55a4256bcd5dbde81b492 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Sat, 18 Jul 2026 10:02:31 +0000 Subject: [PATCH 81/95] install: fix seven WoA/Spark review findings in provisioning and uninstall Third review round; each item re-verified against the live scripts. A stale CUDA < 13 toolkit was kept forever: the 13.3 install was gated on nvcc being absent, so a host with CUDA 12.x failed the sm_121 configure (or the glibc >= 2.41 rsqrt clash) on every rerun and always exited with the CPU server. When apt can provide 13.3 the provisioner now installs it alongside a stale toolkit; find_nvcc's sort -V prefers the new install, and a failed install leaves the old toolkit as the last resort, so non-Spark hosts that build fine on cu12x are unaffected. llama.cpp pins only applied to fresh clones; an existing checkout rebuilt whatever commit it had while the log claimed a release pin. Existing checkouts now fetch and check out the pinned (or resolved-latest) ref, best effort with the current commit as fallback, and the UNSLOTH_LLAMA_PR handling moved out of the fresh-clone branch so it applies to both paths. The WSL fallback silently dropped a non-default --package and reported success with stock unsloth; it is now spliced into the curl | sh invocation (the name is regex-validated at parse time). setup.sh's CUDA provision gate used raw nvidia-smi and ignored the _setup_nvidia_usable computation that honors CUDA_VISIBLE_DEVICES=""/-1, so a mixed-GPU host that hid its NVIDIA card still got a system CUDA install; the gate now requires the flag. On native Linux Spark hosts without nvcc, setup.sh also no longer does the multi-minute CPU source build that the CUDA provision in the same run immediately replaces (mirroring the existing WSL deferral arm); provision failure still cascades to the CPU-prebuilt last resort. uninstall.ps1's distro extraction truncated quoted names at the first space (-d "Ubuntu Preview" matched as "Ubuntu"), wrongly keeping or removing shortcuts; the regex now matches a full quoted token first. And the profile icon (%USERPROFILE%\.unsloth\unsloth.ico) was removed unconditionally while the sweep above deliberately keeps launchers for non-evidenced WSL installs, blanking their icons; removal is now gated on no surviving Unsloth shortcut, mirroring uninstall.sh's _drop_shared_icon_if_unused guard. Verified: bash -n on both shell scripts, PowerShell AST parse on both ps1 files, the new distro regex proven on spaced and unspaced names, icon suites pass, sh test battery matches the branch baseline. --- install.ps1 | 9 +++- scripts/uninstall.ps1 | 23 ++++++++-- studio/scripts/provision_llama_cuda.sh | 60 ++++++++++++++++++++------ studio/setup.sh | 25 +++++++++++ 4 files changed, 98 insertions(+), 19 deletions(-) diff --git a/install.ps1 b/install.ps1 index a4f55d99a7..047e7f1fbe 100644 --- a/install.ps1 +++ b/install.ps1 @@ -2227,10 +2227,15 @@ exit 0 # install.sh --shortcuts-only through the wsl.exe shim, which carries no env, # so without the marker the first update would recreate the duplicate .lnk. $_fwdEnv += 'export UNSLOTH_SKIP_WSL_WINDOWS_SHORTCUT=1; mkdir -p /root/.unsloth; touch /root/.unsloth/.skip-wsl-windows-shortcut; ' + # Forward a non-default --package into the WSL install (already validated + # against ^[a-zA-Z0-9][a-zA-Z0-9._-]*$ at parse time, so splicing is safe); + # previously it was silently dropped and the user got stock unsloth. + $_shArgs = '' + if ($PackageName -ne 'unsloth') { $_shArgs = ' -s -- --package ' + $PackageName } if ($_instRef -eq 'main') { - $wslInstall = $_fwdEnv + 'export DEBIAN_FRONTEND=noninteractive UNSLOTH_WSL_LLAMA_DEFERRED=1; apt-get update -y >/dev/null; apt-get install -y build-essential cmake git curl pciutils libcurl4-openssl-dev >/dev/null; curl -fsSL https://unsloth.ai/install.sh | sh' + $wslInstall = $_fwdEnv + 'export DEBIAN_FRONTEND=noninteractive UNSLOTH_WSL_LLAMA_DEFERRED=1; apt-get update -y >/dev/null; apt-get install -y build-essential cmake git curl pciutils libcurl4-openssl-dev >/dev/null; curl -fsSL https://unsloth.ai/install.sh | sh' + $_shArgs } else { - $wslInstall = $_fwdEnv + 'export DEBIAN_FRONTEND=noninteractive UNSLOTH_WSL_LLAMA_DEFERRED=1; export UNSLOTH_INSTALL_REF=' + $_instRef + '; apt-get update -y >/dev/null; apt-get install -y build-essential cmake git curl pciutils libcurl4-openssl-dev >/dev/null; curl -fsSL https://raw.githubusercontent.com/unslothai/unsloth/' + $_instRef + '/install.sh | sh' + $wslInstall = $_fwdEnv + 'export DEBIAN_FRONTEND=noninteractive UNSLOTH_WSL_LLAMA_DEFERRED=1; export UNSLOTH_INSTALL_REF=' + $_instRef + '; apt-get update -y >/dev/null; apt-get install -y build-essential cmake git curl pciutils libcurl4-openssl-dev >/dev/null; curl -fsSL https://raw.githubusercontent.com/unslothai/unsloth/' + $_instRef + '/install.sh | sh' + $_shArgs } # install.sh may exit non-zero on the optional llama.cpp prebuilt step (no aarch64 prebuilt) # even though torch + unsloth + Studio install; lower EAP so it doesn't abort under Stop. diff --git a/scripts/uninstall.ps1 b/scripts/uninstall.ps1 index fb30e2dd1e..6b88992eaf 100644 --- a/scripts/uninstall.ps1 +++ b/scripts/uninstall.ps1 @@ -435,7 +435,11 @@ function Uninstall-UnslothStudio { $_sc = $_scWs.CreateShortcut($_.FullName) if ("$($_sc.TargetPath) $($_sc.Arguments)" -match "wsl\.exe") { $_scD = $null - if ($_sc.Arguments -match '-d\s+"?([^"\s]+)"?') { $_scD = $Matches[1] } + # install.sh quotes spaced distro names (-d "Ubuntu Preview"), so match a + # full quoted token first; a naive [^"\s]+ would truncate at the space. + if ($_sc.Arguments -match '-d\s+(?:"([^"]+)"|(\S+))') { + $_scD = if ($Matches[1]) { $Matches[1] } else { $Matches[2] } + } elseif ($_.Name -match '^Unsloth Studio \(WSL - (.+)\)\.lnk$') { $_scD = $Matches[1] } if ($_scD -and ($_scCands -notcontains $_scD)) { $_scKeep = $true } } @@ -555,8 +559,21 @@ function Uninstall-UnslothStudio { } catch { } _RemovePath $unslothDir } - # The WoA shortcut icon lives under the user profile (icon broker can't read AppData\Local). - if ($env:USERPROFILE) { _RemovePath (Join-Path $env:USERPROFILE ".unsloth\unsloth.ico") } + # The WoA shortcut icon lives under the user profile (icon broker can't read + # AppData\Local). The shortcut sweep above deliberately keeps launchers for + # WSL installs it has no evidence for; those .lnks point at this icon, so + # only remove it when no Unsloth shortcut survives anywhere (mirrors the + # _drop_shared_icon_if_unused guard on the WSL-side uninstaller). + if ($env:USERPROFILE) { + $_icoInUse = $false + foreach ($_icoDir in $shortcutDirs) { + if ($_icoDir -and (Test-Path -LiteralPath $_icoDir) -and + (Get-ChildItem -LiteralPath $_icoDir -Filter "Unsloth Studio*.lnk" -ErrorAction SilentlyContinue)) { + $_icoInUse = $true; break + } + } + if (-not $_icoInUse) { _RemovePath (Join-Path $env:USERPROFILE ".unsloth\unsloth.ico") } + } # The empty-dir sweep of ~/.unsloth above ran BEFORE this icon removal, so on a WoA install # the still-present unsloth.ico kept ~/.unsloth non-empty then and it was skipped -- leaving an # empty ~/.unsloth behind. Re-attempt now that the icon (the last default-mode child) is gone. diff --git a/studio/scripts/provision_llama_cuda.sh b/studio/scripts/provision_llama_cuda.sh index 3049f8335b..a7fb04baee 100644 --- a/studio/scripts/provision_llama_cuda.sh +++ b/studio/scripts/provision_llama_cuda.sh @@ -92,8 +92,22 @@ find_nvcc() { command -v nvcc 2>/dev/null || ls /usr/local/cuda*/bin/nvcc 2>/dev/null | sort -V | tail -1 } NVCC="$(find_nvcc)" -if [ -z "$NVCC" ] && [ "$HAVE_APT" -eq 1 ]; then - log "CUDA toolkit (nvcc) not found - installing CUDA 13.3 (matches torch cu13x; avoids glibc>=2.41 rsqrt clash)" +# A CUDA < 13 toolkit cannot build for the sm_121 Spark class (and CUDA < 13.3 +# hits the glibc >= 2.41 rsqrt clash from the header) -- keeping it made every +# rerun fail configure/build and exit with the CPU server forever. When apt can +# provide 13.3, upgrade past a stale toolkit; find_nvcc's sort -V then prefers +# the new install, and if the install fails the old toolkit remains the last +# resort (previous behavior, still fine on non-Spark hosts like GH200 + cu12x). +_nvcc_stale=0 +if [ -n "$NVCC" ]; then + _nvcc_major="$("$NVCC" --version 2>/dev/null | sed -n 's/.*release \([0-9][0-9]*\)\..*/\1/p' | head -1)" + if [ -n "$_nvcc_major" ] && [ "$_nvcc_major" -lt 13 ] 2>/dev/null; then + log "existing CUDA $_nvcc_major toolkit ($NVCC) predates this machine class; provisioning CUDA 13.3 alongside it" + _nvcc_stale=1 + fi +fi +if { [ -z "$NVCC" ] || [ "$_nvcc_stale" -eq 1 ]; } && [ "$HAVE_APT" -eq 1 ]; then + [ -z "$NVCC" ] && log "CUDA toolkit (nvcc) not found - installing CUDA 13.3 (matches torch cu13x; avoids glibc>=2.41 rsqrt clash)" # shellcheck disable=SC1091 . /etc/os-release 2>/dev/null || true case "$(uname -m)" in @@ -188,20 +202,38 @@ if [ ! -d "$LLAMA_DIR/.git" ]; then _restore_prev exit 0 fi - # Honor a UNSLOTH_LLAMA_PR pin (same var setup.sh supports); best-effort -- - # a failed fetch keeps the default branch. - case "${UNSLOTH_LLAMA_PR:-}" in - ''|*[!0-9]*) ;; - *) - if git -C "$LLAMA_DIR" fetch --depth 1 origin "pull/${UNSLOTH_LLAMA_PR}/head:_unsloth_pr_${UNSLOTH_LLAMA_PR}" >/dev/null 2>&1 \ - && git -C "$LLAMA_DIR" checkout "_unsloth_pr_${UNSLOTH_LLAMA_PR}" >/dev/null 2>&1; then - log "checked out llama.cpp PR #${UNSLOTH_LLAMA_PR} (UNSLOTH_LLAMA_PR)" - else - log "could not fetch llama.cpp PR #${UNSLOTH_LLAMA_PR}; building the default branch" +else + # Existing checkout: honor the pin on reruns too (previously all ref + # handling lived in the fresh-clone branch, so an existing tree rebuilt + # whatever commit it had regardless of the pin). Best-effort -- an + # unreachable ref keeps the current commit, matching the clone fallback. + if [ -n "$_LLAMA_REF" ]; then + _cur_head="$(git -C "$LLAMA_DIR" rev-parse HEAD 2>/dev/null)" + if git -C "$LLAMA_DIR" fetch --depth 1 origin "$_LLAMA_REF" >/dev/null 2>&1; then + _ref_head="$(git -C "$LLAMA_DIR" rev-parse FETCH_HEAD 2>/dev/null)" + if [ -n "$_ref_head" ] && [ "$_ref_head" != "$_cur_head" ]; then + git -C "$LLAMA_DIR" checkout -q FETCH_HEAD >/dev/null 2>&1 \ + && log "updated existing llama.cpp checkout to $_LLAMA_REF" \ + || log "could not check out $_LLAMA_REF; keeping the current commit" fi - ;; - esac + else + log "could not fetch $_LLAMA_REF; keeping the current commit" + fi + fi fi +# Honor a UNSLOTH_LLAMA_PR pin (same var setup.sh supports) on fresh clones and +# existing checkouts alike; best-effort -- a failed fetch keeps what's there. +case "${UNSLOTH_LLAMA_PR:-}" in + ''|*[!0-9]*) ;; + *) + if git -C "$LLAMA_DIR" fetch --depth 1 origin "pull/${UNSLOTH_LLAMA_PR}/head:_unsloth_pr_${UNSLOTH_LLAMA_PR}" >/dev/null 2>&1 \ + && git -C "$LLAMA_DIR" checkout "_unsloth_pr_${UNSLOTH_LLAMA_PR}" >/dev/null 2>&1; then + log "checked out llama.cpp PR #${UNSLOTH_LLAMA_PR} (UNSLOTH_LLAMA_PR)" + else + log "could not fetch llama.cpp PR #${UNSLOTH_LLAMA_PR}; building the default branch" + fi + ;; +esac cd "$LLAMA_DIR" || { _restore_prev; exit 0; } # When rebuilding in-place over an existing git checkout, the whole-dir backup above diff --git a/studio/setup.sh b/studio/setup.sh index 3db9a0ebb1..f9553a8989 100755 --- a/studio/setup.sh +++ b/studio/setup.sh @@ -1436,6 +1436,30 @@ if [ "$_NEED_LLAMA_SOURCE_BUILD" = true ] \ _LLAMA_CPP_DEFERRED=true fi +# ── Native Linux aarch64 + NVIDIA, no nvcc yet: skip the CPU build too ── +# The aarch64+NVIDIA provision block below installs the CUDA toolkit and does +# the only build this host needs; a CPU source build first would burn minutes +# (and thermal headroom on Spark-class machines) on a binary the CUDA rebuild +# replaces in the same run. Provision failure still cascades to the +# CPU-prebuilt last resort via _LLAMA_CPP_DEGRADED, so no-server states surface. +if [ "$_NEED_LLAMA_SOURCE_BUILD" = true ] \ + && [ "$_LLAMA_FORCE_COMPILE" != "1" ] \ + && [ -z "$_LLAMA_PR" ] \ + && [ "${UNSLOTH_NO_LLAMA_CUDA:-0}" != "1" ] \ + && [ "${_SKIP_GGUF_BUILD:-}" != true ] \ + && ! grep -qi microsoft /proc/version 2>/dev/null \ + && { [ "$_HOST_MACHINE" = "aarch64" ] || [ "$_HOST_MACHINE" = "arm64" ]; } \ + && command -v nvidia-smi >/dev/null 2>&1 \ + && nvidia-smi -L 2>/dev/null | awk '/^GPU[[:space:]]+[0-9]+:/{found=1} END{exit !found}' \ + && [ "${_setup_nvidia_usable:-}" = true ] \ + && ! command -v nvcc >/dev/null 2>&1 \ + && ! ls /usr/local/cuda*/bin/nvcc >/dev/null 2>&1; then + step "llama.cpp" "GGUF engine: deferring to this run's CUDA provision (aarch64 + NVIDIA, no nvcc)" "$C_WARN" + substep "skipping the slow CPU build; the CUDA llama.cpp build below provides the server" + substep "(opt out / keep the CPU build with UNSLOTH_NO_LLAMA_CUDA=1)" + _NEED_LLAMA_SOURCE_BUILD=false +fi + # ── 8. WSL: pre-install GGUF build dependencies for fallback source builds ── # On WSL, sudo requires a password and can't be entered during GGUF export # (runs in a non-interactive subprocess). Install build deps here instead. @@ -1996,6 +2020,7 @@ if [ "$_HOST_SYSTEM" = "Linux" ] \ && [ "${_SKIP_GGUF_BUILD:-}" != true ] \ && command -v nvidia-smi >/dev/null 2>&1 \ && nvidia-smi -L 2>/dev/null | awk '/^GPU[[:space:]]+[0-9]+:/{found=1} END{exit !found}' \ + && [ "${_setup_nvidia_usable:-}" = true ] \ && ! _have_cuda_llama_server; then # Under WSL this runs ONLY for a DIRECT `install.sh` run: install.ps1 sets # UNSLOTH_WSL_LLAMA_DEFERRED=1 and builds in the background; a direct run has From 0675eb6c463493c80fb228834b12f6b7ecdf35aa Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Sat, 18 Jul 2026 10:06:50 +0000 Subject: [PATCH 82/95] install: close four resurfaced WoA review gaps The Spark detectors (library _is_dgx_spark_no_cuda_init and the worker's pre-CUDA sniff) called bare nvidia-smi, but the WoA shim execs the venv binary directly with no login shell, where /usr/lib/wsl/lib can be off PATH; both now resolve WSL's nvidia-smi path explicitly when the bare name is not found, so the allocator setup works on plain 'unsloth ...' launches. An explicit UNSLOTH_PYTHON pin was lost across the WSL boundary (Windows env vars do not cross into the distro), so the inner install.sh built the venv on its default Python while the installer reported success; the pin is now forwarded, gated on a strict X.Y[.Z] shape before splicing into bash -lc. The WSL2 probe/conversion only ran for pre-existing distros; a fresh install relied on wsl --set-default-version 2 succeeding silently and could proceed on WSL1 all the way to the final torch.cuda failure. The probe and in-place conversion now run for freshly installed distros too. The fourth resurfaced item (complete Studio dependency repair set) is already fixed at head: the repair list includes sqlite-vec, pymupdf, and python-docx. --- install.ps1 | 39 +++++++++++++++----------- studio/backend/core/training/worker.py | 9 +++++- unsloth/models/_utils.py | 8 +++++- 3 files changed, 38 insertions(+), 18 deletions(-) diff --git a/install.ps1 b/install.ps1 index 047e7f1fbe..65fa5a1f88 100644 --- a/install.ps1 +++ b/install.ps1 @@ -2170,26 +2170,26 @@ exit 0 $global:LASTEXITCODE = -1 try { & wsl.exe --set-default-version 2 *> $null } catch {} try { & wsl.exe --install -d $distro --no-launch } catch {} - } else { - # A PRE-EXISTING distro may be WSL1 (no GPU passthrough; would fail only at the final - # torch.cuda check). Detect from inside (encoding-proof, unlike UTF-16 `wsl -l -v`) and - # convert in place -- `wsl --set-version` preserves files. - $_wsl2Probe = 'grep -qiE ''microsoft-standard|WSL2'' /proc/version 2>/dev/null || test -e /usr/lib/wsl/lib/libcuda.so' - $_isWsl2 = $false + } + # Verify WSL2 for pre-existing AND freshly installed distros: set-default-version + # can fail silently (old WSL builds), leaving a fresh WSL1 distro that would only + # fail at the final torch.cuda check. Detect from inside (encoding-proof, unlike + # UTF-16 `wsl -l -v`) and convert in place -- `wsl --set-version` preserves files. + $_wsl2Probe = 'grep -qiE ''microsoft-standard|WSL2'' /proc/version 2>/dev/null || test -e /usr/lib/wsl/lib/libcuda.so' + $_isWsl2 = $false + $global:LASTEXITCODE = -1 + try { & wsl.exe -d $distro -u root -- bash -c $_wsl2Probe *> $null; $_isWsl2 = ($LASTEXITCODE -eq 0) } catch {} + if (-not $_isWsl2) { + substep "distro '$distro' looks like WSL1 (no GPU passthrough) -- converting to WSL2 (one-time; can take a few minutes)..." "Yellow" + $global:LASTEXITCODE = -1 + try { & wsl.exe --set-version $distro 2 } catch {} $global:LASTEXITCODE = -1 try { & wsl.exe -d $distro -u root -- bash -c $_wsl2Probe *> $null; $_isWsl2 = ($LASTEXITCODE -eq 0) } catch {} if (-not $_isWsl2) { - substep "distro '$distro' looks like WSL1 (no GPU passthrough) -- converting to WSL2 (one-time; can take a few minutes)..." "Yellow" - $global:LASTEXITCODE = -1 - try { & wsl.exe --set-version $distro 2 } catch {} - $global:LASTEXITCODE = -1 - try { & wsl.exe -d $distro -u root -- bash -c $_wsl2Probe *> $null; $_isWsl2 = ($LASTEXITCODE -eq 0) } catch {} - if (-not $_isWsl2) { - Restore-StudioVenvRollback - return (Exit-InstallFailure "WSL distro '$distro' is WSL1 and automatic conversion failed; NVIDIA GPU passthrough needs WSL2. Convert it, then re-run the installer: wsl --set-version `"$distro`" 2" 1) - } - substep "'$distro' converted to WSL2." "Green" + Restore-StudioVenvRollback + return (Exit-InstallFailure "WSL distro '$distro' is WSL1 and automatic conversion failed; NVIDIA GPU passthrough needs WSL2. Convert it, then re-run the installer: wsl --set-version `"$distro`" 2" 1) } + substep "'$distro' converted to WSL2." "Green" } substep "installing Unsloth Studio inside WSL '$distro' with full GPU (this downloads PyTorch)..." "Cyan" # Non-main ref: fetch + export THAT ref so the WSL venv gets the branch's setup.sh + patches @@ -2220,6 +2220,13 @@ exit 0 if ($env:UNSLOTH_PYTORCH_MIRROR -and ($env:UNSLOTH_PYTORCH_MIRROR -match '^https?://[A-Za-z0-9._~:/?#@%+=&-]+$')) { $_fwdEnv += "export UNSLOTH_PYTORCH_MIRROR='$($env:UNSLOTH_PYTORCH_MIRROR)'; " } + # Forward an explicit UNSLOTH_PYTHON pin: Windows env vars do not cross into + # WSL, so without this the inner install.sh silently built the venv on its + # default Python while the installer reported success. Strict version shape + # so the splice into bash -lc cannot break out; the default stays install.sh's. + if ($env:UNSLOTH_PYTHON -and ($env:UNSLOTH_PYTHON -match '^\d+\.\d+(\.\d+)?$')) { + $_fwdEnv += "export UNSLOTH_PYTHON='$($env:UNSLOTH_PYTHON)'; " + } # install.ps1 owns the WoA shortcut (one canonical "Unsloth Studio.lnk" with a # %USERPROFILE%\.unsloth icon that renders on WoA). Tell install.sh to skip its own # WSL .lnk so we don't get a duplicate whose %LOCALAPPDATA% icon renders blank. diff --git a/studio/backend/core/training/worker.py b/studio/backend/core/training/worker.py index 1bf71eae18..68b06b57e2 100644 --- a/studio/backend/core/training/worker.py +++ b/studio/backend/core/training/worker.py @@ -2312,8 +2312,15 @@ def run_training_process(*, event_queue: Any, stop_queue: Any, config: dict) -> else: _spark_smi = False if _plat.machine().lower() in ("aarch64", "arm64"): + import shutil as _shutil + + # The WoA shim execs the venv binary directly (no login shell), + # where /usr/lib/wsl/lib can be off PATH -- resolve explicitly. + _smi_bin = "nvidia-smi" + if _shutil.which(_smi_bin) is None and os.path.exists("/usr/lib/wsl/lib/nvidia-smi"): + _smi_bin = "/usr/lib/wsl/lib/nvidia-smi" _smi = _sp.run( - ["nvidia-smi", "--query-gpu=name", "--format=csv,noheader"], + [_smi_bin, "--query-gpu=name", "--format=csv,noheader"], capture_output = True, text = True, timeout = 5, diff --git a/unsloth/models/_utils.py b/unsloth/models/_utils.py index 5956dda90e..e868c8cc6d 100644 --- a/unsloth/models/_utils.py +++ b/unsloth/models/_utils.py @@ -1725,9 +1725,15 @@ def _is_dgx_spark_no_cuda_init(): if platform.machine().lower() not in ("aarch64", "arm64"): return False import subprocess + import shutil + # The WoA shim execs the venv binary directly (no login shell), where + # /usr/lib/wsl/lib can be off PATH -- resolve WSL's nvidia-smi explicitly. + _smi = "nvidia-smi" + if shutil.which(_smi) is None and os.path.exists("/usr/lib/wsl/lib/nvidia-smi"): + _smi = "/usr/lib/wsl/lib/nvidia-smi" out = subprocess.run( - ["nvidia-smi", "--query-gpu=name", "--format=csv,noheader"], + [_smi, "--query-gpu=name", "--format=csv,noheader"], capture_output = True, text = True, timeout = 5, From 7faf0c7fb45d15cfa4365e0840d2434222366e56 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Sat, 18 Jul 2026 11:31:38 +0000 Subject: [PATCH 83/95] install: gate CUDA 13.3 on driver support, fail loudly on broken WSL installs Fourth review round; each item verified against the live scripts and the CUDA compatibility documentation before fixing. The provisioner installed (and, since the stale-toolkit change, preferred) CUDA 13.3 without ever consulting the driver, but cu13 binaries need a 580+ driver and minor-version compatibility never crosses majors, so a GH200-class host on a 5xx driver got an unloadable llama-server that the structural acceptance check then stamped as ready. The driver's supported CUDA major is now read from nvidia-smi and enforced three ways: the stale-toolkit upgrade only fires when the driver can run cu13, a fresh install on a sub-13 driver bails to the existing no-toolkit message instead of installing 13.3, and a final guard swaps a too-new selected toolkit for the newest one the driver supports (or refuses to build). Spark-class hosts (580+ drivers) behave exactly as before; unparseable output keeps the previous behavior. The WSL install pipeline ended in curl | sh, so a failed download fed sh an empty stdin and exited 0; on a rerun the stale venv then passed the torch probe and the installer reported success without ever running. install.sh is now downloaded to a file with exit 86 as the never-ran sentinel, checked before any probe (rollback + non-zero). The --package splice moved onto the file invocation. When the Studio web-server dep repair failed its re-verify, the installer still created shims and reported success; the missing set includes typer, so even the plain unsloth CLI dies. A failed repair now routes to the existing failure path (rollback + non-zero), mirroring the CLI-missing case. If all three provision-script resolutions fail (unpackaged wheel + GitHub unreachable), the provision block silently skipped and, with the CPU build now deferred on native Spark hosts, the install could report success with no GGUF server; that case is now marked degraded so the CPU-prebuilt last resort and failure exit fire. flex_attention.py's Spark sniff gets the same /usr/lib/wsl/lib/nvidia-smi fallback as the other two detectors (grep confirms these are the only three), and uninstall.sh removes the remaining WSL-side build artifacts (run_llama_build.sh, llama_cuda_build.log, the shortcut-skip marker) so the .unsloth directory can actually be removed. Verified: bash -n on all three shell scripts, AST parse on flex_attention.py, PowerShell AST parse on install.ps1, the toolkit-picker awk exercised against a fake /usr/local tree (driver 12 picks cuda-12.8 over 13.0, driver 11 picks none), icon suites pass, sh test battery matches the branch baseline. --- install.ps1 | 30 ++++++++++++++--- scripts/uninstall.sh | 7 +++- studio/scripts/provision_llama_cuda.sh | 46 ++++++++++++++++++++++---- studio/setup.sh | 7 ++++ unsloth/kernels/flex_attention.py | 9 ++++- 5 files changed, 87 insertions(+), 12 deletions(-) diff --git a/install.ps1 b/install.ps1 index 65fa5a1f88..6811bd3301 100644 --- a/install.ps1 +++ b/install.ps1 @@ -2238,11 +2238,17 @@ exit 0 # against ^[a-zA-Z0-9][a-zA-Z0-9._-]*$ at parse time, so splicing is safe); # previously it was silently dropped and the user got stock unsloth. $_shArgs = '' - if ($PackageName -ne 'unsloth') { $_shArgs = ' -s -- --package ' + $PackageName } + if ($PackageName -ne 'unsloth') { $_shArgs = ' --package ' + $PackageName } + # Download to a file instead of `curl | sh`: a failed download feeds sh an + # empty stdin (exit 0), and on a rerun the stale venv then passes the torch + # probe below, reporting success without the installer ever running. Exit 86 + # is the "download failed, installer never ran" sentinel checked after the + # run. /root/.unsloth already exists (skip-marker mkdir above) and the file + # is removed with it on uninstall. if ($_instRef -eq 'main') { - $wslInstall = $_fwdEnv + 'export DEBIAN_FRONTEND=noninteractive UNSLOTH_WSL_LLAMA_DEFERRED=1; apt-get update -y >/dev/null; apt-get install -y build-essential cmake git curl pciutils libcurl4-openssl-dev >/dev/null; curl -fsSL https://unsloth.ai/install.sh | sh' + $_shArgs + $wslInstall = $_fwdEnv + 'export DEBIAN_FRONTEND=noninteractive UNSLOTH_WSL_LLAMA_DEFERRED=1; apt-get update -y >/dev/null; apt-get install -y build-essential cmake git curl pciutils libcurl4-openssl-dev >/dev/null; curl -fsSL https://unsloth.ai/install.sh -o /root/.unsloth/unsloth-install.sh || exit 86; sh /root/.unsloth/unsloth-install.sh' + $_shArgs } else { - $wslInstall = $_fwdEnv + 'export DEBIAN_FRONTEND=noninteractive UNSLOTH_WSL_LLAMA_DEFERRED=1; export UNSLOTH_INSTALL_REF=' + $_instRef + '; apt-get update -y >/dev/null; apt-get install -y build-essential cmake git curl pciutils libcurl4-openssl-dev >/dev/null; curl -fsSL https://raw.githubusercontent.com/unslothai/unsloth/' + $_instRef + '/install.sh | sh' + $_shArgs + $wslInstall = $_fwdEnv + 'export DEBIAN_FRONTEND=noninteractive UNSLOTH_WSL_LLAMA_DEFERRED=1; export UNSLOTH_INSTALL_REF=' + $_instRef + '; apt-get update -y >/dev/null; apt-get install -y build-essential cmake git curl pciutils libcurl4-openssl-dev >/dev/null; curl -fsSL https://raw.githubusercontent.com/unslothai/unsloth/' + $_instRef + '/install.sh -o /root/.unsloth/unsloth-install.sh || exit 86; sh /root/.unsloth/unsloth-install.sh' + $_shArgs } # install.sh may exit non-zero on the optional llama.cpp prebuilt step (no aarch64 prebuilt) # even though torch + unsloth + Studio install; lower EAP so it doesn't abort under Stop. @@ -2256,6 +2262,15 @@ exit 0 $ErrorActionPreference = $prevEapWsl } Write-Host "" + # Sentinel from the download step above: the installer never ran, so the + # probes below would only re-validate a stale venv from a previous install. + if ($wslRc -eq 86) { + step "wsl" "could not download install.sh inside WSL (network or bad ref) -- the installer never ran." "Yellow" + Restore-StudioVenvRollback + $global:LASTEXITCODE = 1 + if ($PSCommandPath) { exit 1 } + return + } # $wslRc can be non-zero from the llama.cpp prebuilt step even on success, so verify torch.cuda directly. $torchOk = $false $prevEapChk = $ErrorActionPreference @@ -2306,7 +2321,14 @@ exit 0 $_serverOk = ($LASTEXITCODE -eq 0) } catch {} finally { $ErrorActionPreference = $prevEapS2 } if ($_serverOk) { substep "Studio web-server deps installed." "Green" } - else { substep "(could not auto-install Studio server deps; 'unsloth studio' may fail to start)" "Yellow" } + else { + # The missing set includes typer, so even the plain unsloth CLI + # dies; creating shims and reporting success over that state + # advertises commands that cannot run. Route to the failure + # path (rollback + non-zero), like the CLI-missing case above. + substep "Studio server deps missing and the repair failed -- not reporting success over a broken install." "Yellow" + $torchOk = $false + } } # The uv-managed venv ships no `pip`, but unsloth-zoo's check_pip() finds `uv pip` only # when uv is on PATH. Seed pip so `save_pretrained_gguf` works regardless. diff --git a/scripts/uninstall.sh b/scripts/uninstall.sh index eeaa0a3169..3c450c8d93 100755 --- a/scripts/uninstall.sh +++ b/scripts/uninstall.sh @@ -216,8 +216,13 @@ _remove_path "$HOME/.unsloth/studio" # by deleting it). No-op in env/custom mode (they nest under the custom root) and # when absent. A user-set UNSLOTH_LLAMA_CPP_PATH is intentionally kept. _remove_path "$HOME/.unsloth/llama.cpp" -# provision_llama_cuda.sh fetched by the WoA/Spark CUDA-build path. No-op when absent. +# WoA/Spark CUDA-build path artifacts (provision script fetched by setup.sh, +# install.ps1's background-build runner + log, and the persisted shortcut-skip +# marker). No-ops when absent. _remove_path "$HOME/.unsloth/provision_llama_cuda.sh" +_remove_path "$HOME/.unsloth/run_llama_build.sh" +_remove_path "$HOME/.unsloth/llama_cuda_build.log" +_remove_path "$HOME/.unsloth/.skip-wsl-windows-shortcut" _remove_path "$HOME/.unsloth/.cache" # Isolated Node.js runtime (install_node_prebuilt.py), a sibling of studio in # default mode. No-op in env/custom mode (nested under the custom root) and absent. diff --git a/studio/scripts/provision_llama_cuda.sh b/studio/scripts/provision_llama_cuda.sh index a7fb04baee..b9041199c5 100644 --- a/studio/scripts/provision_llama_cuda.sh +++ b/studio/scripts/provision_llama_cuda.sh @@ -91,22 +91,37 @@ find_nvcc() { if [ -n "$_v" ]; then printf '%s\n' "$_v"; return 0; fi command -v nvcc 2>/dev/null || ls /usr/local/cuda*/bin/nvcc 2>/dev/null | sort -V | tail -1 } +# The driver caps which CUDA major can RUN: cu13 binaries need a 580+ driver, +# and minor-version compatibility never crosses majors, so a server built with +# a toolkit newer than the driver loads nothing. Read the driver's supported +# major (all recent drivers print "CUDA Version: X.Y"); unparseable stays empty +# and keeps the previous install-13.3 behavior (Spark-class drivers all parse). +_DRV_CUDA_MAJOR="$(nvidia-smi 2>/dev/null | sed -n 's/.*CUDA Version: *\([0-9][0-9]*\)\..*/\1/p' | head -1)" +case "$_DRV_CUDA_MAJOR" in *[!0-9]*) _DRV_CUDA_MAJOR="" ;; esac +_nvcc_major_of() { "$1" --version 2>/dev/null | sed -n 's/.*release \([0-9][0-9]*\)\..*/\1/p' | head -1; } + NVCC="$(find_nvcc)" # A CUDA < 13 toolkit cannot build for the sm_121 Spark class (and CUDA < 13.3 # hits the glibc >= 2.41 rsqrt clash from the header) -- keeping it made every # rerun fail configure/build and exit with the CPU server forever. When apt can -# provide 13.3, upgrade past a stale toolkit; find_nvcc's sort -V then prefers -# the new install, and if the install fails the old toolkit remains the last -# resort (previous behavior, still fine on non-Spark hosts like GH200 + cu12x). +# provide 13.3 AND the driver can run cu13, upgrade past a stale toolkit; +# find_nvcc's sort -V then prefers the new install, and if the install fails +# the old toolkit remains the last resort (previous behavior, still fine on +# non-Spark hosts like GH200 + cu12x, which the driver gate now protects). _nvcc_stale=0 if [ -n "$NVCC" ]; then - _nvcc_major="$("$NVCC" --version 2>/dev/null | sed -n 's/.*release \([0-9][0-9]*\)\..*/\1/p' | head -1)" - if [ -n "$_nvcc_major" ] && [ "$_nvcc_major" -lt 13 ] 2>/dev/null; then + _nvcc_major="$(_nvcc_major_of "$NVCC")" + if [ -n "$_nvcc_major" ] && [ "$_nvcc_major" -lt 13 ] 2>/dev/null \ + && [ -n "$_DRV_CUDA_MAJOR" ] && [ "$_DRV_CUDA_MAJOR" -ge 13 ]; then log "existing CUDA $_nvcc_major toolkit ($NVCC) predates this machine class; provisioning CUDA 13.3 alongside it" _nvcc_stale=1 fi fi -if { [ -z "$NVCC" ] || [ "$_nvcc_stale" -eq 1 ]; } && [ "$HAVE_APT" -eq 1 ]; then +if [ -z "$NVCC" ] && [ -n "$_DRV_CUDA_MAJOR" ] && [ "$_DRV_CUDA_MAJOR" -lt 13 ]; then + # No toolkit and the driver cannot run cu13: installing 13.3 would build an + # unloadable server. Bail to the no-toolkit message (CPU fallback stands). + log "driver supports CUDA ${_DRV_CUDA_MAJOR}.x only; not installing CUDA 13.3 (its binaries need a 580+ driver)" +elif { [ -z "$NVCC" ] || [ "$_nvcc_stale" -eq 1 ]; } && [ "$HAVE_APT" -eq 1 ]; then [ -z "$NVCC" ] && log "CUDA toolkit (nvcc) not found - installing CUDA 13.3 (matches torch cu13x; avoids glibc>=2.41 rsqrt clash)" # shellcheck disable=SC1091 . /etc/os-release 2>/dev/null || true @@ -133,6 +148,25 @@ if { [ -z "$NVCC" ] || [ "$_nvcc_stale" -eq 1 ]; } && [ "$HAVE_APT" -eq 1 ]; the NVCC="$(find_nvcc)" fi +# Final sanity: never build with a toolkit whose major the driver cannot run +# (find_nvcc prefers the highest install, which may be a manually added 13.x on +# an older-driver host). Prefer the newest toolkit at or below the driver's +# major; with none, fall through to the no-toolkit exit. +if [ -n "$NVCC" ] && [ -n "$_DRV_CUDA_MAJOR" ]; then + _nvcc_major="$(_nvcc_major_of "$NVCC")" + if [ -n "$_nvcc_major" ] && [ "$_nvcc_major" -gt "$_DRV_CUDA_MAJOR" ] 2>/dev/null; then + _alt="$(ls -d /usr/local/cuda-[0-9]*/bin/nvcc 2>/dev/null | sort -V \ + | awk -F'cuda-' -v m="$_DRV_CUDA_MAJOR" '{ split($2, v, /[./]/); if (v[1] + 0 <= m + 0) print }' | tail -1)" + if [ -n "$_alt" ]; then + log "CUDA $_nvcc_major toolkit exceeds the driver's supported major ($_DRV_CUDA_MAJOR); using $_alt instead" + NVCC="$_alt" + else + log "the only CUDA toolkit ($_nvcc_major.x) is newer than the driver supports (CUDA $_DRV_CUDA_MAJOR.x); a build would not load" + NVCC="" + fi + fi +fi + if [ -z "$NVCC" ]; then log "could not provision a CUDA toolkit. Training + GGUF export still work;" log "GGUF *inference* in Studio will be unavailable until a CUDA toolkit exists." diff --git a/studio/setup.sh b/studio/setup.sh index f9553a8989..f5d797f16e 100755 --- a/studio/setup.sh +++ b/studio/setup.sh @@ -2060,6 +2060,13 @@ if [ "$_HOST_SYSTEM" = "Linux" ] \ # and the failure exit fire instead of reporting a working install. _LLAMA_CPP_DEGRADED=true fi + else + # Provisioner unreachable (not packaged and the GitHub fetch failed). The + # native deferral above may have skipped the CPU source build expecting + # this block to build; without a server that must surface as degraded so + # the CPU-prebuilt last resort fires instead of reporting success. + substep "CUDA provision script unavailable (offline?); cannot build CUDA llama.cpp" "$C_WARN" + [ -f "$LLAMA_SERVER_BIN" ] || _LLAMA_CPP_DEGRADED=true fi fi diff --git a/unsloth/kernels/flex_attention.py b/unsloth/kernels/flex_attention.py index 294a54e21d..115cdd8f57 100644 --- a/unsloth/kernels/flex_attention.py +++ b/unsloth/kernels/flex_attention.py @@ -41,9 +41,16 @@ def _flex_is_dgx_spark(): if platform.machine().lower() not in ("aarch64", "arm64"): return False import subprocess + import shutil + # The WoA shim execs the venv binary directly (no login shell), where + # /usr/lib/wsl/lib can be off PATH -- resolve WSL's nvidia-smi explicitly + # (mirrors _is_dgx_spark_no_cuda_init in models/_utils.py). + _smi = "nvidia-smi" + if shutil.which(_smi) is None and os.path.exists("/usr/lib/wsl/lib/nvidia-smi"): + _smi = "/usr/lib/wsl/lib/nvidia-smi" out = subprocess.run( - ["nvidia-smi", "--query-gpu=name", "--format=csv,noheader"], + [_smi, "--query-gpu=name", "--format=csv,noheader"], capture_output = True, text = True, timeout = 5, From af459f4673daffbbf14dd46acf2b4adecfa08ef5 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Sat, 18 Jul 2026 12:08:39 +0000 Subject: [PATCH 84/95] install: six round-five review fixes across provisioner, setup, uninstall Fifth review round; each item traced through the live scripts before fixing. A provisioner fresh clone that failed to produce a server was left behind as a markerless git tree; under a custom UNSLOTH_STUDIO_HOME the next run's ownership assert refuses the unmarked dir and aborts the whole install until the user deletes it by hand. _restore_prev now removes a clone this script created when no server came out of it (backed-up dirs restore as before). The CUDA provision gate ignored --with-llama-cpp-dir linked mode, so a linked user tree with a CPU-only server could be checked out to a pinned ref, rebuilt in place, or moved aside entirely and replaced by a fresh clone. The gate now skips linked local dirs. uninstall.sh removed the CUDA build artifacts without stopping a running detached build; _pkill_studio only matches Studio roots, so live cmake/nvcc kept burning thermals, recreated build files, and defeated the trailing rmdir. The runner, provisioner, and llama.cpp-path processes now get TERM-then-KILL with the same escape helper and grace the Studio kill uses. The worker's memory-fraction guard classified Spark purely from device props, so UNSLOTH_FORCE_DGX_SPARK=1 on an unlisted name got no fraction guard (and the fraction env was dead), while FORCE=0 could not disable it; the guard now honors the same force semantics as the detectors. UNSLOTH_LLAMA_TAG / UNSLOTH_LLAMA_PR were interpolated into the runner script's single-quoted exports unvalidated while every sibling forward has an allow-list; they now get the INSTALL_REF ref allow-list and a digits-only check respectively (own-machine robustness, not a trust boundary). On WSL-fallback success with a custom UNSLOTH_STUDIO_HOME, the installer deleted the rolled-aside custom-root venv right after telling the user that root is not used by the WSL install; a custom root now restores the previous venv instead (the WSL shim does not depend on the Windows venv), while the default root keeps dropping the vestigial backup. Verified: bash -n on all three shell scripts, AST parse on worker.py, PowerShell AST parse on install.ps1, icon suites pass, sh battery matches the branch baseline. Two resurfaced anchors (build/bin backup, --package forwarding) confirmed already fixed at head. --- install.ps1 | 14 ++++++++++---- scripts/uninstall.sh | 14 ++++++++++++++ studio/backend/core/training/worker.py | 11 ++++++++++- studio/scripts/provision_llama_cuda.sh | 7 +++++++ studio/setup.sh | 1 + 5 files changed, 42 insertions(+), 5 deletions(-) diff --git a/install.ps1 b/install.ps1 index 6811bd3301..410e9af2a0 100644 --- a/install.ps1 +++ b/install.ps1 @@ -2452,8 +2452,10 @@ exit 0 $_jobsLine = if ($env:UNSLOTH_LLAMA_BUILD_JOBS) { "export UNSLOTH_LLAMA_BUILD_JOBS=$($env:UNSLOTH_LLAMA_BUILD_JOBS)`n" } else { "" } # Bridge UNSLOTH_LLAMA_TAG / UNSLOTH_LLAMA_PR pins into WSL, else the deferred build # ignores them. sh-single-quoted since tags/PRs are simple tokens. - $_tagLine = if ($env:UNSLOTH_LLAMA_TAG) { "export UNSLOTH_LLAMA_TAG='$($env:UNSLOTH_LLAMA_TAG)'`n" } else { "" } - $_prLine = if ($env:UNSLOTH_LLAMA_PR) { "export UNSLOTH_LLAMA_PR='$($env:UNSLOTH_LLAMA_PR)'`n" } else { "" } + # Same allow-lists as the other forwarded knobs: a quote in the + # value would break out of the single-quoted export in the runner. + $_tagLine = if ($env:UNSLOTH_LLAMA_TAG -and ($env:UNSLOTH_LLAMA_TAG -match '^[A-Za-z0-9][A-Za-z0-9._/-]*$')) { "export UNSLOTH_LLAMA_TAG='$($env:UNSLOTH_LLAMA_TAG)'`n" } else { "" } + $_prLine = if ($env:UNSLOTH_LLAMA_PR -and ($env:UNSLOTH_LLAMA_PR -match '^\d+$')) { "export UNSLOTH_LLAMA_PR='$($env:UNSLOTH_LLAMA_PR)'`n" } else { "" } $_runner = "#!/usr/bin/env bash`n" + $_pathLine + $_jobsLine + $_tagLine + $_prLine + "exec bash /root/.unsloth/provision_llama_cuda.sh > /root/.unsloth/llama_cuda_build.log 2>&1`n" $_runnerB64 = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($_runner)) $_fetchCmd = 'mkdir -p /root/.unsloth; if curl -fsSL "' + $_llamaUrl + '" -o /root/.unsloth/provision_llama_cuda.sh && [ -s /root/.unsloth/provision_llama_cuda.sh ]; then chmod +x /root/.unsloth/provision_llama_cuda.sh; echo ' + $_runnerB64 + ' | base64 -d > /root/.unsloth/run_llama_build.sh; chmod +x /root/.unsloth/run_llama_build.sh; echo PROV_FETCHED; else echo PROV_NOSCRIPT; fi' @@ -2476,8 +2478,12 @@ exit 0 } if ($torchOk) { # Success: the Windows venv is vestigial (everything runs in WSL), so drop the - # rolled-aside previous-venv backup instead of orphaning it. - Complete-StudioVenvRollback + # rolled-aside previous-venv backup instead of orphaning it. EXCEPT for a + # custom UNSLOTH_STUDIO_HOME: the installer told the user above that their + # custom root is not used by the WSL install, so deleting the venv that + # lived there would contradict that disclaimer -- put it back instead + # (the WSL shim does not depend on the Windows venv). + if ($envOverride) { Restore-StudioVenvRollback } else { Complete-StudioVenvRollback } substep "GPU training + GGUF export run inside WSL. (GGUF *inference* additionally needs a CUDA llama.cpp build.)" "Yellow" $global:LASTEXITCODE = 0 return diff --git a/scripts/uninstall.sh b/scripts/uninstall.sh index 3c450c8d93..911d5914cf 100755 --- a/scripts/uninstall.sh +++ b/scripts/uninstall.sh @@ -216,6 +216,20 @@ _remove_path "$HOME/.unsloth/studio" # by deleting it). No-op in env/custom mode (they nest under the custom root) and # when absent. A user-set UNSLOTH_LLAMA_CPP_PATH is intentionally kept. _remove_path "$HOME/.unsloth/llama.cpp" +# Stop a detached CUDA llama.cpp build before deleting its tree: _pkill_studio +# only matches Studio roots, and a live cmake/nvcc under ~/.unsloth/llama.cpp +# would keep burning CPU/thermals, recreate build/ files, and defeat the +# trailing rmdir. TERM first, then KILL after the same grace _pkill_studio uses. +if command -v pkill >/dev/null 2>&1; then + _llama_re=$(_pkill_escape "$HOME/.unsloth/llama.cpp") + for _pat in "run_llama_build\.sh" "provision_llama_cuda\.sh" "$_llama_re"; do + pkill -TERM -f "$_pat" 2>/dev/null || true + done + sleep 0.5 + for _pat in "run_llama_build\.sh" "provision_llama_cuda\.sh" "$_llama_re"; do + pkill -KILL -f "$_pat" 2>/dev/null || true + done +fi # WoA/Spark CUDA-build path artifacts (provision script fetched by setup.sh, # install.ps1's background-build runner + log, and the persisted shortcut-skip # marker). No-ops when absent. diff --git a/studio/backend/core/training/worker.py b/studio/backend/core/training/worker.py index 68b06b57e2..f8d3b0affd 100644 --- a/studio/backend/core/training/worker.py +++ b/studio/backend/core/training/worker.py @@ -2876,7 +2876,16 @@ def run_training_process(*, event_queue: Any, stop_queue: Any, config: dict) -> import torch as _torch_mem if _torch_mem.cuda.is_available(): _props = _torch_mem.cuda.get_device_properties(0) - _marker, _is_spark_uma = _nvidia_classify_spark_unified_memory(_props) + # Same UNSLOTH_FORCE_DGX_SPARK override the detectors honor, so a + # forced Spark with an unlisted name still gets the fraction guard + # and FORCE=0 can disable it on a token-matched device. + _force_spark = os.environ.get("UNSLOTH_FORCE_DGX_SPARK") + if _force_spark == "1": + _marker, _is_spark_uma = "forced", True + elif _force_spark == "0": + _marker, _is_spark_uma = "forced-off", False + else: + _marker, _is_spark_uma = _nvidia_classify_spark_unified_memory(_props) if _is_spark_uma: _mem_fraction = 0.80 _frac_env = os.environ.get("UNSLOTH_SPARK_MEM_FRACTION") diff --git a/studio/scripts/provision_llama_cuda.sh b/studio/scripts/provision_llama_cuda.sh index b9041199c5..f540e99c86 100644 --- a/studio/scripts/provision_llama_cuda.sh +++ b/studio/scripts/provision_llama_cuda.sh @@ -212,10 +212,16 @@ fi # Back up any existing (e.g. CPU-only) llama.cpp: restored on any failure exit, # dropped only once the fresh build yields a server -- never leave NO server. _LLAMA_BAK="" +_FRESH_CLONE=0 _restore_prev() { if [ -n "$_LLAMA_BAK" ] && [ -e "$_LLAMA_BAK" ]; then rm -rf "$LLAMA_DIR" 2>/dev/null mv "$_LLAMA_BAK" "$LLAMA_DIR" 2>/dev/null && log "restored previous llama.cpp install" + elif [ "$_FRESH_CLONE" = "1" ] && [ ! -x "$SERVER" ]; then + # We created this clone and produced no server. Leaving a markerless git + # tree under a custom STUDIO_HOME bricks reruns: setup.sh's ownership + # assert refuses the unmarked dir and aborts the whole install. + rm -rf "$LLAMA_DIR" 2>/dev/null fi } if [ ! -d "$LLAMA_DIR/.git" ]; then @@ -231,6 +237,7 @@ if [ ! -d "$LLAMA_DIR/.git" ]; then if [ "$_clone_ok" -ne 1 ]; then git clone --depth 1 https://github.com/ggml-org/llama.cpp "$LLAMA_DIR" >/dev/null 2>&1 && _clone_ok=1 fi + [ "$_clone_ok" -eq 1 ] && _FRESH_CLONE=1 if [ "$_clone_ok" -ne 1 ]; then log "git clone failed" _restore_prev diff --git a/studio/setup.sh b/studio/setup.sh index f5d797f16e..115a76282a 100755 --- a/studio/setup.sh +++ b/studio/setup.sh @@ -2021,6 +2021,7 @@ if [ "$_HOST_SYSTEM" = "Linux" ] \ && command -v nvidia-smi >/dev/null 2>&1 \ && nvidia-smi -L 2>/dev/null | awk '/^GPU[[:space:]]+[0-9]+:/{found=1} END{exit !found}' \ && [ "${_setup_nvidia_usable:-}" = true ] \ + && [ "${_LOCAL_LLAMA_CPP_LINKED:-false}" != true ] \ && ! _have_cuda_llama_server; then # Under WSL this runs ONLY for a DIRECT `install.sh` run: install.ps1 sets # UNSLOTH_WSL_LLAMA_DEFERRED=1 and builds in the background; a direct run has From 0b6251c2afc585c2f20a6134fe3725ab84186eb8 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Sat, 18 Jul 2026 13:06:13 +0000 Subject: [PATCH 85/95] install: six round-six review fixes across installer, setup, uninstall Sixth review round; every item reproduced against the live scripts first. The WSL fallback tolerated a nonzero inner exit (the optional llama.cpp step legitimately fails), so a run whose installer died mid-way could pass the torch and CLI probes on a stale venv from a previous install and be reported as success. setup.sh now stamps /root/.unsloth/.install-ok after the core venv and Studio deps complete, just before its tolerated llama-only nonzero exit; install.ps1 clears the stamp before the run and requires it to exist afterwards (existence only, no mtime compare, so WSL/Windows clock skew cannot bite). uninstall.sh removes the stamp and the downloaded installer file so the trailing rmdir can still prune. Root login shells reset PATH via /etc/profile and drop /usr/lib/wsl/lib, the only location of nvidia-smi under WSL2 GPU-PV, so every bare nvidia-smi probe in setup.sh and the provisioner could silently misreport "no GPU". Both now resolve nvidia-smi explicitly (PATH, then /usr/lib/wsl/lib, then /usr/bin) via a shared-resolver pattern, and the provisioner's driver-major and compute_cap reads use the resolved path. My round-five uninstall fix inserted the CUDA-build kill block after the llama.cpp tree was already removed, so a live cmake/nvcc kept running against deleted paths; the block now runs before the removal. uninstall.ps1 gated its legacy marker-less WSL cleanup on the process PROCESSOR_ARCHITECTURE, which reports AMD64 under an x64-emulated PowerShell on ARM64, skipping exactly the machines the fallback installs on. It now uses the same triple detection as install.ps1 (OSArchitecture, Win32_Processor.Architecture 12, machine-level registry arch), factored into one helper used at both gate sites. The nvidia-smi capture helper retried twice with a 60s timeout everywhere, so off WSL a hung nvidia-smi stalled three successive detect_host probes for about two minutes each; the generous retry now applies only under WSL (where GPU-PV load slowness is real) and bare metal keeps a single short attempt. The generated WSL Desktop launcher hardcoded port 8888 for launch, health poll, and browser open, so with Jupyter or a second Studio on 8888 the poll waited on the wrong server forever; it now scans 8888..8908 with a TcpListener, mirroring the native launcher's free-port window, and passes the winner via -p everywhere. Verified: bash -n on all shell scripts, Python AST parse, PowerShell AST parse on install.ps1, uninstall.ps1, and the generated launcher; the launcher port scan exercised free, busy, and exhausted cases; the capture helper unit-tested for WSL and bare-metal attempt/timeout splits; sh test battery matches the branch baseline. --- install.ps1 | 38 +++++++++++++++++++--- scripts/uninstall.ps1 | 24 ++++++++++++-- scripts/uninstall.sh | 19 +++++++---- studio/install_llama_prebuilt.py | 25 ++++++++++++--- studio/scripts/provision_llama_cuda.sh | 11 +++++-- studio/setup.sh | 44 ++++++++++++++++++-------- 6 files changed, 126 insertions(+), 35 deletions(-) diff --git a/install.ps1 b/install.ps1 index 410e9af2a0..6c030e104b 100644 --- a/install.ps1 +++ b/install.ps1 @@ -2233,7 +2233,11 @@ exit 0 # Persist the skip as a marker file too: `unsloth studio update` reruns # install.sh --shortcuts-only through the wsl.exe shim, which carries no env, # so without the marker the first update would recreate the duplicate .lnk. - $_fwdEnv += 'export UNSLOTH_SKIP_WSL_WINDOWS_SHORTCUT=1; mkdir -p /root/.unsloth; touch /root/.unsloth/.skip-wsl-windows-shortcut; ' + # Clear the completion stamp from any previous install: setup.sh rewrites + # it only after the core venv + Studio deps finish, and the post-run gate + # below requires it, so a run that dies mid-install can no longer coast on + # a stale venv passing the torch/CLI probes. + $_fwdEnv += 'export UNSLOTH_SKIP_WSL_WINDOWS_SHORTCUT=1; mkdir -p /root/.unsloth; touch /root/.unsloth/.skip-wsl-windows-shortcut; rm -f /root/.unsloth/.install-ok; ' # Forward a non-default --package into the WSL install (already validated # against ^[a-zA-Z0-9][a-zA-Z0-9._-]*$ at parse time, so splicing is safe); # previously it was silently dropped and the user got stock unsloth. @@ -2295,6 +2299,22 @@ exit 0 $torchOk = $false } } + # Require the completion stamp setup.sh writes after the core venv + + # Studio deps finish (cleared above before the run). torch + CLI alone + # can both come from a stale venv left by a PREVIOUS install while this + # run's installer died mid-way; the tolerated nonzero $wslRc (optional + # llama.cpp step) makes that indistinguishable by exit code. Existence + # only, no mtime compare: WSL and Windows clocks can skew. + if ($torchOk) { + $prevEapStamp = $ErrorActionPreference; $ErrorActionPreference = "Continue" + $global:LASTEXITCODE = -1 + try { & wsl.exe -d $distro --cd /root -u root -- test -f /root/.unsloth/.install-ok *> $null } catch {} + $ErrorActionPreference = $prevEapStamp + if ($LASTEXITCODE -ne 0) { + substep "WSL install did not complete its core steps this run (no completion stamp; inner exit $wslRc) -- the venv passing the probes is from a previous install." "Yellow" + $torchOk = $false + } + } # Self-heal web-server deps: a cut-short install.sh "studio deps" step leaves torch + unsloth but # no fastapi/uvicorn/structlog/starlette (`unsloth studio` dies). Reinstall them unpinned (no # huggingface-hub/transformers/datasets) so the verified GPU torch stack stays intact. @@ -2379,9 +2399,19 @@ exit 0 $L = @( '$ErrorActionPreference = "SilentlyContinue"', ('$distro = "' + $distro + '"'), - 'Start-Job { for ($i=0; $i -lt 120; $i++) { try { if ((Invoke-WebRequest "http://localhost:8888/api/health" -UseBasicParsing -TimeoutSec 2).StatusCode -eq 200) { Start-Process "http://localhost:8888"; break } } catch {}; Start-Sleep 1 } } | Out-Null', - 'Write-Host "Starting Unsloth Studio in WSL ($distro); browser opens at http://localhost:8888 when ready (Ctrl+C to stop)..."', - 'wsl.exe -d $distro --cd /root -u root -- bash -lic "unsloth studio -p 8888"' + # Port 8888 may already be taken on the Windows side (Jupyter, a + # second Studio): Studio inside WSL would bind a different port + # while the poll below waits on 8888 forever and the browser + # never opens. Scan the same 8888..8908 window the native + # launcher uses (Find-FreeLaunchPort) and pass the winner via + # -p. WSL2 localhost forwarding mirrors the WSL port onto + # Windows, so probing with a Windows-side TcpListener is valid. + '$port = 0', + 'foreach ($p in 8888..8908) { $l = $null; try { $l = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Any, $p); $l.Start(); $port = $p } catch {} finally { if ($l) { try { $l.Stop() } catch {} } }; if ($port) { break } }', + 'if (-not $port) { Write-Host "No free port in 8888-8908; close one of the apps using them and relaunch."; Start-Sleep 10; exit 1 }', + 'Start-Job -ArgumentList $port { param($port) for ($i=0; $i -lt 120; $i++) { try { if ((Invoke-WebRequest "http://localhost:$port/api/health" -UseBasicParsing -TimeoutSec 2).StatusCode -eq 200) { Start-Process "http://localhost:$port"; break } } catch {}; Start-Sleep 1 } } | Out-Null', + 'Write-Host "Starting Unsloth Studio in WSL ($distro); browser opens at http://localhost:$port when ready (Ctrl+C to stop)..."', + 'wsl.exe -d $distro --cd /root -u root -- bash -lic "unsloth studio -p $port"' ) Set-Content -LiteralPath $launcher -Value $L -Encoding UTF8 # Icon must live OUTSIDE %LOCALAPPDATA%: on WoA the sandboxed icon broker can't read a .ico diff --git a/scripts/uninstall.ps1 b/scripts/uninstall.ps1 index 6b88992eaf..cbee753dbd 100644 --- a/scripts/uninstall.ps1 +++ b/scripts/uninstall.ps1 @@ -16,6 +16,26 @@ function Uninstall-UnslothStudio { function _Step { param([string]$Msg) Write-Host $Msg } function _Substep { param([string]$Msg, [string]$Color = "Gray") Write-Host " $Msg" -ForegroundColor $Color } + # True host architecture, mirroring install.ps1's WSL-fallback gate: an + # x64-emulated PowerShell on ARM64 reports AMD64 in PROCESSOR_ARCHITECTURE, + # which made the legacy marker-less WSL cleanup below skip exactly the + # machines the fallback installed on. Each probe only ever turns the answer + # ON; Win32_Processor.Architecture 12 = ARM64. + function _IsArm64Host { + $arm = $false + try { $arm = ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString() -ieq 'Arm64') } catch { } + if (-not $arm) { + try { if ((@(Get-CimInstance Win32_Processor -ErrorAction Stop))[0].Architecture -eq 12) { $arm = $true } } catch { } + } + if (-not $arm) { + try { + $machArch = (Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Environment' -Name PROCESSOR_ARCHITECTURE -ErrorAction Stop).PROCESSOR_ARCHITECTURE + if ($machArch -ieq 'ARM64') { $arm = $true } + } catch { } + } + return $arm + } + # Remove a file/dir/symlink if present. Idempotent; retries since a just-killed # process can briefly hold a handle (Windows refuses the delete until released). function _RemovePath { @@ -417,7 +437,7 @@ function Uninstall-UnslothStudio { } } } catch { } - if ((-not $_scCands) -and ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64')) { + if ((-not $_scCands) -and (_IsArm64Host)) { $_scCands = @('Ubuntu', 'Ubuntu-24.04', 'Ubuntu-22.04', 'Debian') } $_scWs = $null @@ -601,7 +621,7 @@ function Uninstall-UnslothStudio { $_cands = @() if ($env:UNSLOTH_WSL_DISTRO) { $_cands += $env:UNSLOTH_WSL_DISTRO } if ($_recordedDistro) { $_cands += $_recordedDistro } - if ((-not $_cands) -and ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64')) { + if ((-not $_cands) -and (_IsArm64Host)) { $_cands = @('', 'Ubuntu', 'Ubuntu-24.04', 'Ubuntu-22.04', 'Debian') } $_done = @{} diff --git a/scripts/uninstall.sh b/scripts/uninstall.sh index 911d5914cf..52e5def945 100755 --- a/scripts/uninstall.sh +++ b/scripts/uninstall.sh @@ -212,14 +212,11 @@ _custom_studio_roots | while IFS= read -r _custom_root; do _remove_path "$_custom_root" done _remove_path "$HOME/.unsloth/studio" -# Default-mode shared llama.cpp build + cache are siblings of studio (not removed -# by deleting it). No-op in env/custom mode (they nest under the custom root) and -# when absent. A user-set UNSLOTH_LLAMA_CPP_PATH is intentionally kept. -_remove_path "$HOME/.unsloth/llama.cpp" -# Stop a detached CUDA llama.cpp build before deleting its tree: _pkill_studio +# Stop a detached CUDA llama.cpp build BEFORE deleting its tree: _pkill_studio # only matches Studio roots, and a live cmake/nvcc under ~/.unsloth/llama.cpp -# would keep burning CPU/thermals, recreate build/ files, and defeat the -# trailing rmdir. TERM first, then KILL after the same grace _pkill_studio uses. +# would keep burning CPU/thermals, recreate build/ files between the rm and the +# rmdir, and leave a partial tree. TERM first, then KILL after the same grace +# _pkill_studio uses. if command -v pkill >/dev/null 2>&1; then _llama_re=$(_pkill_escape "$HOME/.unsloth/llama.cpp") for _pat in "run_llama_build\.sh" "provision_llama_cuda\.sh" "$_llama_re"; do @@ -230,6 +227,10 @@ if command -v pkill >/dev/null 2>&1; then pkill -KILL -f "$_pat" 2>/dev/null || true done fi +# Default-mode shared llama.cpp build + cache are siblings of studio (not removed +# by deleting it). No-op in env/custom mode (they nest under the custom root) and +# when absent. A user-set UNSLOTH_LLAMA_CPP_PATH is intentionally kept. +_remove_path "$HOME/.unsloth/llama.cpp" # WoA/Spark CUDA-build path artifacts (provision script fetched by setup.sh, # install.ps1's background-build runner + log, and the persisted shortcut-skip # marker). No-ops when absent. @@ -237,6 +238,10 @@ _remove_path "$HOME/.unsloth/provision_llama_cuda.sh" _remove_path "$HOME/.unsloth/run_llama_build.sh" _remove_path "$HOME/.unsloth/llama_cuda_build.log" _remove_path "$HOME/.unsloth/.skip-wsl-windows-shortcut" +# Core-install completion stamp (written by setup.sh, checked by install.ps1's +# WSL probes). Must go, or a later reinstall could read a stale success. +_remove_path "$HOME/.unsloth/.install-ok" +_remove_path "$HOME/.unsloth/unsloth-install.sh" _remove_path "$HOME/.unsloth/.cache" # Isolated Node.js runtime (install_node_prebuilt.py), a sibling of studio in # default mode. No-op in env/custom mode (nested under the custom root) and absent. diff --git a/studio/install_llama_prebuilt.py b/studio/install_llama_prebuilt.py index 193c12a6d4..5bbec4c4a1 100644 --- a/studio/install_llama_prebuilt.py +++ b/studio/install_llama_prebuilt.py @@ -10,6 +10,7 @@ import argparse import atexit import errno import fnmatch +import functools import glob import hashlib import json @@ -2762,11 +2763,20 @@ def _pick_rocm_gfx_target(out: str) -> str | None: return _tokens[0] +@functools.lru_cache(maxsize = 1) +def _running_under_wsl() -> bool: + """WSL kernels self-identify with 'microsoft' in the release string.""" + try: + return "microsoft" in platform.uname().release.lower() + except Exception: + return False + + def _nvidia_smi_capture( command: list[str], *, - attempts: int = 2, - timeout: int = 60, + attempts: int | None = None, + timeout: int | None = None, ) -> subprocess.CompletedProcess[str]: """run_capture for nvidia-smi probes, hardened against transient slowness. @@ -2776,14 +2786,19 @@ def _nvidia_smi_capture( A single short timeout then raises TimeoutExpired, detect_host treats the GPU as ABSENT, and the host is misrouted to a CPU prebuilt / slow source build instead of the CUDA bundle it can actually use. Retry with a generous - per-attempt timeout. Only ever reached when nvidia-smi exists on PATH, so + per-attempt timeout there. Off WSL that slowness mode does not exist, and a + hung nvidia-smi (broken driver, revoked container GPU) would stall three + successive detect_host probes for ~2 minutes each -- so bare metal keeps a + single short attempt. Only ever reached when nvidia-smi exists on PATH, so CPU-only hosts never incur this wait. """ + _on_wsl = _running_under_wsl() + _attempts = max(1, attempts if attempts is not None else (2 if _on_wsl else 1)) + _timeout = timeout if timeout is not None else (60 if _on_wsl else 10) last_exc: Exception | None = None - _attempts = max(1, attempts) for _attempt in range(_attempts): try: - return run_capture(command, timeout = timeout) + return run_capture(command, timeout = _timeout) except subprocess.TimeoutExpired as exc: last_exc = exc if _attempt + 1 < _attempts: # don't sleep after the final attempt diff --git a/studio/scripts/provision_llama_cuda.sh b/studio/scripts/provision_llama_cuda.sh index f540e99c86..102c431cdd 100644 --- a/studio/scripts/provision_llama_cuda.sh +++ b/studio/scripts/provision_llama_cuda.sh @@ -60,7 +60,12 @@ if is_cuda_server "$SERVER"; then fi # 1. Require an NVIDIA GPU (this script is only meaningful with one). -if ! command -v nvidia-smi >/dev/null 2>&1; then +# Resolve nvidia-smi explicitly: root login shells drop /usr/lib/wsl/lib from +# PATH, which is the ONLY location on WSL2 GPU-PV (mirrors setup.sh's resolver). +NVSMI="$(command -v nvidia-smi 2>/dev/null)" +[ -z "$NVSMI" ] && [ -x /usr/lib/wsl/lib/nvidia-smi ] && NVSMI=/usr/lib/wsl/lib/nvidia-smi +[ -z "$NVSMI" ] && [ -x /usr/bin/nvidia-smi ] && NVSMI=/usr/bin/nvidia-smi +if [ -z "$NVSMI" ]; then log "no nvidia-smi found; skipping CUDA llama.cpp build" exit 0 fi @@ -96,7 +101,7 @@ find_nvcc() { # a toolkit newer than the driver loads nothing. Read the driver's supported # major (all recent drivers print "CUDA Version: X.Y"); unparseable stays empty # and keeps the previous install-13.3 behavior (Spark-class drivers all parse). -_DRV_CUDA_MAJOR="$(nvidia-smi 2>/dev/null | sed -n 's/.*CUDA Version: *\([0-9][0-9]*\)\..*/\1/p' | head -1)" +_DRV_CUDA_MAJOR="$("$NVSMI" 2>/dev/null | sed -n 's/.*CUDA Version: *\([0-9][0-9]*\)\..*/\1/p' | head -1)" case "$_DRV_CUDA_MAJOR" in *[!0-9]*) _DRV_CUDA_MAJOR="" ;; esac _nvcc_major_of() { "$1" --version 2>/dev/null | sed -n 's/.*release \([0-9][0-9]*\)\..*/\1/p' | head -1; } @@ -190,7 +195,7 @@ export CC="$HCC" CXX="$HCXX" CUDAHOSTCXX="$HCXX" # GPU-PV / driver combos report "N/A". "native" needs CMake >= 3.24 (Ubuntu # 22.04 apt ships 3.22), so the fallback omits the flag entirely and lets # ggml's version-guarded CMake defaults pick the arches instead. -CC_CAP="$(nvidia-smi --query-gpu=compute_cap --format=csv,noheader 2>/dev/null | head -1 | tr -d ' .')" +CC_CAP="$("$NVSMI" --query-gpu=compute_cap --format=csv,noheader 2>/dev/null | head -1 | tr -d ' .')" case "$CC_CAP" in ''|*[!0-9]*) CUDA_ARCH="" ;; *) CUDA_ARCH="$CC_CAP" ;; diff --git a/studio/setup.sh b/studio/setup.sh index 115a76282a..b473cdfdb8 100755 --- a/studio/setup.sh +++ b/studio/setup.sh @@ -265,16 +265,22 @@ _setup_cvd_hides_nvidia() { # via CUDA_VISIBLE_DEVICES=""/-1 counts as NOT usable (matches # install_llama_prebuilt.py has_usable_nvidia), so the AMD probes still run # and a mixed host steered to its AMD card keeps the ROCm route. +# nvidia-smi resolver: on WSL2 GPU-PV the binary lives ONLY in +# /usr/lib/wsl/lib, which root login shells drop from PATH (/etc/profile +# resets it), and /proc/driver/nvidia is not populated under the dxg driver -- +# so bare `command -v nvidia-smi` misses real GPUs on the flagship WoA path. +_resolve_nvsmi() { + command -v nvidia-smi 2>/dev/null && return 0 + [ -x /usr/lib/wsl/lib/nvidia-smi ] && { echo /usr/lib/wsl/lib/nvidia-smi; return 0; } + [ -x /usr/bin/nvidia-smi ] && { echo /usr/bin/nvidia-smi; return 0; } + return 1 +} + _setup_has_usable_nvidia_gpu() { if _setup_cvd_hides_nvidia; then return 1 fi - _setup_nvsmi="" - if command -v nvidia-smi >/dev/null 2>&1; then - _setup_nvsmi="nvidia-smi" - elif [ -x "/usr/bin/nvidia-smi" ]; then - _setup_nvsmi="/usr/bin/nvidia-smi" - fi + _setup_nvsmi="$(_resolve_nvsmi)" if [ -n "$_setup_nvsmi" ]; then if _setup_run_smi "$_setup_nvsmi" -L 2>/dev/null \ | awk '/^GPU[[:space:]]+[0-9]+:/{found=1} END{exit !found}'; then @@ -289,8 +295,8 @@ _setup_has_usable_nvidia_gpu() { } _cuda_driver_max_version() { - command -v nvidia-smi >/dev/null 2>&1 || return 0 - _setup_run_smi nvidia-smi 2>/dev/null \ + _cdm_smi="$(_resolve_nvsmi)" || return 0 + _setup_run_smi "$_cdm_smi" 2>/dev/null \ | sed -nE 's/.*CUDA( UMD)? Version:[[:space:]]*([0-9]+)\.([0-9]+).*/\2.\3/p' \ | head -1 || true } @@ -1424,8 +1430,8 @@ if [ "$_NEED_LLAMA_SOURCE_BUILD" = true ] \ && [ "${UNSLOTH_NO_LLAMA_CUDA:-0}" != "1" ] \ && grep -qi microsoft /proc/version 2>/dev/null \ && { [ "$_HOST_MACHINE" = "aarch64" ] || [ "$_HOST_MACHINE" = "arm64" ]; } \ - && command -v nvidia-smi >/dev/null 2>&1 \ - && nvidia-smi -L 2>/dev/null | awk '/^GPU[[:space:]]+[0-9]+:/{found=1} END{exit !found}' \ + && _NVSMI_GATE="$(_resolve_nvsmi)" && [ -n "$_NVSMI_GATE" ] \ + && "$_NVSMI_GATE" -L 2>/dev/null | awk '/^GPU[[:space:]]+[0-9]+:/{found=1} END{exit !found}' \ && ! command -v nvcc >/dev/null 2>&1 \ && ! ls /usr/local/cuda*/bin/nvcc >/dev/null 2>&1; then step "llama.cpp" "GGUF engine: CUDA build running in background (WSL aarch64 + NVIDIA)" "$C_WARN" @@ -1449,8 +1455,8 @@ if [ "$_NEED_LLAMA_SOURCE_BUILD" = true ] \ && [ "${_SKIP_GGUF_BUILD:-}" != true ] \ && ! grep -qi microsoft /proc/version 2>/dev/null \ && { [ "$_HOST_MACHINE" = "aarch64" ] || [ "$_HOST_MACHINE" = "arm64" ]; } \ - && command -v nvidia-smi >/dev/null 2>&1 \ - && nvidia-smi -L 2>/dev/null | awk '/^GPU[[:space:]]+[0-9]+:/{found=1} END{exit !found}' \ + && _NVSMI_GATE="$(_resolve_nvsmi)" && [ -n "$_NVSMI_GATE" ] \ + && "$_NVSMI_GATE" -L 2>/dev/null | awk '/^GPU[[:space:]]+[0-9]+:/{found=1} END{exit !found}' \ && [ "${_setup_nvidia_usable:-}" = true ] \ && ! command -v nvcc >/dev/null 2>&1 \ && ! ls /usr/local/cuda*/bin/nvcc >/dev/null 2>&1; then @@ -2018,8 +2024,8 @@ if [ "$_HOST_SYSTEM" = "Linux" ] \ && { ! grep -qi microsoft /proc/version 2>/dev/null || [ "${UNSLOTH_WSL_LLAMA_DEFERRED:-0}" != "1" ]; } \ && [ "${UNSLOTH_NO_LLAMA_CUDA:-0}" != "1" ] \ && [ "${_SKIP_GGUF_BUILD:-}" != true ] \ - && command -v nvidia-smi >/dev/null 2>&1 \ - && nvidia-smi -L 2>/dev/null | awk '/^GPU[[:space:]]+[0-9]+:/{found=1} END{exit !found}' \ + && _NVSMI_GATE="$(_resolve_nvsmi)" && [ -n "$_NVSMI_GATE" ] \ + && "$_NVSMI_GATE" -L 2>/dev/null | awk '/^GPU[[:space:]]+[0-9]+:/{found=1} END{exit !found}' \ && [ "${_setup_nvidia_usable:-}" = true ] \ && [ "${_LOCAL_LLAMA_CPP_LINKED:-false}" != true ] \ && ! _have_cuda_llama_server; then @@ -2141,6 +2147,16 @@ else fi echo "" +# Core install (venv + torch + Studio deps) is complete here; only the +# optional llama.cpp engine can still be missing. Stamp that fact BEFORE the +# tolerated nonzero exit below: install.ps1's WSL fallback cannot tell that +# exit apart from a real mid-install failure at the process level, so it +# removes this file before the run and requires it to exist afterwards -- +# otherwise its torch/CLI probes can pass on a stale venv from a previous +# install. Removed by scripts/uninstall.sh with the rest of ~/.unsloth. +mkdir -p "$HOME/.unsloth" 2>/dev/null || true +: > "$HOME/.unsloth/.install-ok" 2>/dev/null || true + # When called from install.sh (SKIP_STUDIO_BASE=1), exit non-zero so the # installer can report the GGUF failure after finishing PATH/shortcut setup. # When called directly via 'unsloth studio update', keep the install From 6ea6c621c0b7e694a65aa0c57e5632e3d3c91620 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 18 Jul 2026 13:20:04 +0000 Subject: [PATCH 86/95] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- studio/backend/core/training/worker.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/studio/backend/core/training/worker.py b/studio/backend/core/training/worker.py index 6fbea4cf0f..db8dfad2a8 100644 --- a/studio/backend/core/training/worker.py +++ b/studio/backend/core/training/worker.py @@ -2321,7 +2321,9 @@ def run_training_process(*, event_queue: Any, stop_queue: Any, config: dict) -> # The WoA shim execs the venv binary directly (no login shell), # where /usr/lib/wsl/lib can be off PATH -- resolve explicitly. _smi_bin = "nvidia-smi" - if _shutil.which(_smi_bin) is None and os.path.exists("/usr/lib/wsl/lib/nvidia-smi"): + if _shutil.which(_smi_bin) is None and os.path.exists( + "/usr/lib/wsl/lib/nvidia-smi" + ): _smi_bin = "/usr/lib/wsl/lib/nvidia-smi" _smi = _sp.run( [_smi_bin, "--query-gpu=name", "--format=csv,noheader"], From 9ca396b665d805b1e7c1ea6529c924dbbffb543d Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Sat, 18 Jul 2026 13:59:55 +0000 Subject: [PATCH 87/95] install: four round-seven review fixes across installer and uninstall Seventh review round; each item reproduced against the live tree first. The aarch64 bitsandbytes step gated on a bare nvidia-smi, which root login shells cannot see under WSL2 GPU-PV (the binary lives only in /usr/lib/wsl/lib, dropped from PATH by the /etc/profile reset), so Spark and N1X WSL installs finished with CUDA torch but no 4-bit QLoRA. The gate now resolves nvidia-smi explicitly with the same PATH, /usr/lib/wsl/lib, /usr/bin order as setup.sh's resolver. uninstall.sh's CUDA-build kill matched patterns against argv, but the provisioner cds into the tree before `cmake --build build`, so cmake and make children carry relative argv no pattern can match; killing only the wrapper orphaned them mid-build. Each match's whole process group is now signalled (TERM then KILL), with a plain PID kill as fallback when the pgid is unreadable or shared with init. Verified in a sandbox: a child with unmatchable argv in the wrapper's group dies with it. The WSL shim dir was appended to user PATH while the native installer prepends its own %USERPROFILE%\.unsloth\studio\bin, whose unsloth.exe outlives the venv the fallback rolls aside, so on a native-to-WSL rerun a new terminal resolved unsloth to the dead native launcher. The shim is now prepended via Add-ToUserPath (which de-dupes and hoists), and the dead default-root native shim is removed when the venv binary it targets is gone; custom-root shims are left alone since the prepend outranks them. UNSLOTH_NPM_REGISTRY was not forwarded into the inner WSL shell even though setup.sh threads it into every npm/bun install, so mirror-required networks failed the frontend step (and with it the install) while the outer installer honored the mirror. It is now forwarded with the same strict http(s) allow-list and single-quoting as UNSLOTH_PYTORCH_MIRROR. Verified: bash -n on both shell scripts, PowerShell AST parse on install.ps1, the group-kill sandbox above, resolver smoke tests for the bitsandbytes gate, and the sh test battery matches the branch baseline. --- install.ps1 | 32 ++++++++++++++++++++++++-------- install.sh | 10 ++++++++-- scripts/uninstall.sh | 26 ++++++++++++++++++++------ 3 files changed, 52 insertions(+), 16 deletions(-) diff --git a/install.ps1 b/install.ps1 index 079176bd60..4b350c915b 100644 --- a/install.ps1 +++ b/install.ps1 @@ -2223,6 +2223,13 @@ exit 0 if ($env:UNSLOTH_PYTORCH_MIRROR -and ($env:UNSLOTH_PYTORCH_MIRROR -match '^https?://[A-Za-z0-9._~:/?#@%+=&-]+$')) { $_fwdEnv += "export UNSLOTH_PYTORCH_MIRROR='$($env:UNSLOTH_PYTORCH_MIRROR)'; " } + # Forward the npm mirror the same way: setup.sh threads UNSLOTH_NPM_REGISTRY + # into every npm/bun install, and on mirror-required networks the WSL + # frontend/OXC steps would otherwise hit registry.npmjs.org and fail the + # install. Same strict http(s) allow-list + single-quote as above. + if ($env:UNSLOTH_NPM_REGISTRY -and ($env:UNSLOTH_NPM_REGISTRY -match '^https?://[A-Za-z0-9._~:/?#@%+=&-]+$')) { + $_fwdEnv += "export UNSLOTH_NPM_REGISTRY='$($env:UNSLOTH_NPM_REGISTRY)'; " + } # Forward an explicit UNSLOTH_PYTHON pin: Windows env vars do not cross into # WSL, so without this the inner install.sh silently built the venv on its # default Python while the installer reported success. Strict version shape @@ -2379,14 +2386,23 @@ exit 0 # Record the distro so the uninstaller can clean a custom UNSLOTH_WSL_DISTRO install # without the env var set. try { Set-Content -LiteralPath (Join-Path (Split-Path $shimDir -Parent) "wsl-distro.txt") -Value $distro -Encoding ASCII } catch {} - # A fresh profile may have no HKCU 'Path'; null would make TrimEnd() throw. - $userPath = [Environment]::GetEnvironmentVariable("Path", "User") - if (-not $userPath) { $userPath = "" } - if (($userPath -split ';') -notcontains $shimDir) { - $newUserPath = if ($userPath.Trim()) { $userPath.TrimEnd(';') + ";" + $shimDir } else { $shimDir } - [Environment]::SetEnvironmentVariable("Path", $newUserPath, "User") - } - $env:Path = $env:Path.TrimEnd(';') + ";" + $shimDir + # PREPEND (not append): a previous NATIVE install prepended + # %USERPROFILE%\.unsloth\studio\bin (unsloth.exe) to user PATH, + # and that exe outlives the venv this fallback just rolled aside + # -- an appended shim would lose to the dead native launcher in + # every new terminal. Add-ToUserPath de-dupes and hoists. + $null = Add-ToUserPath -Directory $shimDir -Position 'Prepend' + $env:Path = $shimDir + ";" + $env:Path.TrimStart(';') + # Drop the dead default-root native shim outright when the venv + # binary it launches is gone (custom-root shims are left alone; + # the PATH prepend above already outranks them). + try { + $staleNativeShim = Join-Path $env:USERPROFILE ".unsloth\studio\bin\unsloth.exe" + $staleNativeTarget = Join-Path $env:USERPROFILE ".unsloth\studio\unsloth_studio\Scripts\unsloth.exe" + if ((Test-Path -LiteralPath $staleNativeShim) -and -not (Test-Path -LiteralPath $staleNativeTarget)) { + Remove-Item -LiteralPath $staleNativeShim -Force -ErrorAction Stop + } + } catch {} step "shim" "created native 'unsloth' command -> forwards to WSL '$distro'" "Green" substep "open a NEW terminal, then (no WSL knowledge needed):" "Cyan" substep " unsloth studio # runs in WSL; opens http://localhost:8888" "Cyan" diff --git a/install.sh b/install.sh index a30b6b57aa..17baa76550 100755 --- a/install.sh +++ b/install.sh @@ -2979,10 +2979,16 @@ elif [ -n "$TORCH_INDEX_URL" ]; then # extras break 4-bit QLoRA, but aarch64 manylinux wheels work (verified on # sm_121 via PTX JIT). Best-effort: no wheel keeps 16-bit LoRA / full finetuning. # SKIP_TORCH gate stops a --no-torch (GGUF-only) install dragging torch back in. + # nvidia-smi may live only in /usr/lib/wsl/lib (WSL2 GPU-PV), which root login + # shells drop from PATH -- resolve explicitly (same order as setup.sh's + # _resolve_nvsmi) so the WoA/WSL install still gets 4-bit QLoRA support. + _bnb_nvsmi="$(command -v nvidia-smi 2>/dev/null || true)" + [ -z "$_bnb_nvsmi" ] && [ -x /usr/lib/wsl/lib/nvidia-smi ] && _bnb_nvsmi=/usr/lib/wsl/lib/nvidia-smi + [ -z "$_bnb_nvsmi" ] && [ -x /usr/bin/nvidia-smi ] && _bnb_nvsmi=/usr/bin/nvidia-smi if [ "$SKIP_TORCH" = false ] \ && { [ "$(uname -m)" = "aarch64" ] || [ "$(uname -m)" = "arm64" ]; } \ - && command -v nvidia-smi >/dev/null 2>&1 \ - && nvidia-smi -L 2>/dev/null | awk '/^GPU[[:space:]]+[0-9]+:/{found=1} END{exit !found}' \ + && [ -n "$_bnb_nvsmi" ] \ + && "$_bnb_nvsmi" -L 2>/dev/null | awk '/^GPU[[:space:]]+[0-9]+:/{found=1} END{exit !found}' \ && ! "$_VENV_PY" -c "import bitsandbytes" >/dev/null 2>&1; then substep "installing bitsandbytes (aarch64 wheels; enables 4-bit QLoRA)..." if ! uv pip install --python "$_VENV_PY" "bitsandbytes>=0.45.5,!=0.46.0,!=0.48.0" >/dev/null 2>&1; then diff --git a/scripts/uninstall.sh b/scripts/uninstall.sh index 52e5def945..3cc49b3ab7 100755 --- a/scripts/uninstall.sh +++ b/scripts/uninstall.sh @@ -219,13 +219,27 @@ _remove_path "$HOME/.unsloth/studio" # _pkill_studio uses. if command -v pkill >/dev/null 2>&1; then _llama_re=$(_pkill_escape "$HOME/.unsloth/llama.cpp") - for _pat in "run_llama_build\.sh" "provision_llama_cuda\.sh" "$_llama_re"; do - pkill -TERM -f "$_pat" 2>/dev/null || true - done + # Signal the whole process GROUP of each match, not just the matching PID: + # the provisioner cds into the tree before `cmake --build build`, so cmake/ + # make children carry relative argv that no pattern can match, and killing + # only the wrapper orphans them mid-build. Group kill sweeps the tree; PID + # kill remains the fallback when pgid is unreadable or shared with init. + _kill_llama_build() { + _sig="$1" + for _pat in "run_llama_build\.sh" "provision_llama_cuda\.sh" "$_llama_re"; do + for _pid in $(pgrep -f "$_pat" 2>/dev/null); do + _pgid=$(ps -o pgid= -p "$_pid" 2>/dev/null | tr -d '[:space:]') + case "$_pgid" in + ''|0|1) kill -s "$_sig" "$_pid" 2>/dev/null || true ;; + *) kill -s "$_sig" -- "-$_pgid" 2>/dev/null \ + || kill -s "$_sig" "$_pid" 2>/dev/null || true ;; + esac + done + done + } + _kill_llama_build TERM sleep 0.5 - for _pat in "run_llama_build\.sh" "provision_llama_cuda\.sh" "$_llama_re"; do - pkill -KILL -f "$_pat" 2>/dev/null || true - done + _kill_llama_build KILL fi # Default-mode shared llama.cpp build + cache are siblings of studio (not removed # by deleting it). No-op in env/custom mode (they nest under the custom root) and From 7b7511fb5cf6a0c6e022f86a8703d13276b8cd31 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Sat, 18 Jul 2026 14:36:08 +0000 Subject: [PATCH 88/95] install: three round-eight review fixes for exit status, kill safety, detection Eighth review round; each item reproduced before fixing. Exit-InstallFailure under irm-pipe-iex set LASTEXITCODE and returned, so powershell -Command automation using the published pipe form exited 0 on fatal installer errors (verified: a -Command run whose last call only assigns LASTEXITCODE exits 0, while one that throws exits 1). The iex branch now sets the var for callers that check it and then raises a terminating error, matching the pre-existing throw behavior there: interactive shells survive and print it, automation gets exit 1, and the -File branch keeps carrying the specific code via exit. The uninstall group kill could signal the uninstaller's own process group: in a non-interactive session without job control a lingering provisioner can share the script's pgid, and kill(-pgid) would TERM the cleanup mid-run. The helper now compares each match's pgid against its own and falls back to the PID plus its direct children in that case. Both scenarios exercised in a sandbox: a setsid provisioner group still dies whole, and a same-group provisioner dies without taking the harness. detect_host in install_llama_prebuilt.py resolved nvidia-smi only via shutil.which, so the root WSL sessions this PR creates (PATH without /usr/lib/wsl/lib) classified ARM NVIDIA WSL hosts as non-NVIDIA and took the CPU prebuilt path before setup's provisioning logic could run. It now falls back to /usr/lib/wsl/lib/nvidia-smi then /usr/bin/nvidia-smi, the same order as setup.sh's resolver. Verified: bash -n, Python AST parse, PowerShell AST parse, the pwsh exit-code experiments above, the two-scenario kill sandbox, and the sh test battery matches the branch baseline. --- install.ps1 | 6 +++++- scripts/uninstall.sh | 9 ++++++++- studio/install_llama_prebuilt.py | 8 ++++++++ 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/install.ps1 b/install.ps1 index 4b350c915b..0286d0fd8a 100644 --- a/install.ps1 +++ b/install.ps1 @@ -98,11 +98,15 @@ function Install-UnslothStudio { exit $Code } # -File ignores $LASTEXITCODE on plain return, so `exit` carries the code; under - # `irm | iex` (no $PSCommandPath) `exit` would kill the user's shell, so set the var. + # `irm | iex` (no $PSCommandPath) `exit` would kill the user's shell. There, set + # the var for callers that check it, then raise a terminating error: interactive + # shells survive a throw and just print it, while `-Command "irm ... | iex"` + # automation exits 1 (a plain return would report success on fatal errors). if ($PSCommandPath) { exit $Code } $global:LASTEXITCODE = $Code + throw $Message } # ── Parse flags ── diff --git a/scripts/uninstall.sh b/scripts/uninstall.sh index 3cc49b3ab7..eb8512e758 100755 --- a/scripts/uninstall.sh +++ b/scripts/uninstall.sh @@ -224,13 +224,20 @@ if command -v pkill >/dev/null 2>&1; then # make children carry relative argv that no pattern can match, and killing # only the wrapper orphans them mid-build. Group kill sweeps the tree; PID # kill remains the fallback when pgid is unreadable or shared with init. + # Never group-kill our own group: in a non-interactive session (no job + # control) a lingering provisioner can share the uninstaller's pgid, and + # kill(-pgid) would TERM this script and its caller mid-cleanup. Fall back + # to the PID plus its direct children in that case. + _self_pgid=$(ps -o pgid= -p $$ 2>/dev/null | tr -d '[:space:]') _kill_llama_build() { _sig="$1" for _pat in "run_llama_build\.sh" "provision_llama_cuda\.sh" "$_llama_re"; do for _pid in $(pgrep -f "$_pat" 2>/dev/null); do _pgid=$(ps -o pgid= -p "$_pid" 2>/dev/null | tr -d '[:space:]') case "$_pgid" in - ''|0|1) kill -s "$_sig" "$_pid" 2>/dev/null || true ;; + ''|0|1|"$_self_pgid") + pkill "-$_sig" -P "$_pid" 2>/dev/null + kill -s "$_sig" "$_pid" 2>/dev/null || true ;; *) kill -s "$_sig" -- "-$_pgid" 2>/dev/null \ || kill -s "$_sig" "$_pid" 2>/dev/null || true ;; esac diff --git a/studio/install_llama_prebuilt.py b/studio/install_llama_prebuilt.py index d4126200de..2e5cedbc10 100644 --- a/studio/install_llama_prebuilt.py +++ b/studio/install_llama_prebuilt.py @@ -3024,6 +3024,14 @@ def detect_host() -> HostInfo: macos_version = parse_macos_version(platform.mac_ver()[0]) if is_macos else None nvidia_smi = shutil.which("nvidia-smi") + if not nvidia_smi: + # Root WSL sessions drop /usr/lib/wsl/lib (the only nvidia-smi home + # under WSL2 GPU-PV) from PATH, which would misroute an ARM NVIDIA WSL + # host to the CPU prebuilt; mirror setup.sh's resolver order. + for _cand in ("/usr/lib/wsl/lib/nvidia-smi", "/usr/bin/nvidia-smi"): + if os.access(_cand, os.X_OK): + nvidia_smi = _cand + break driver_cuda_version = None compute_caps: list[str] = [] visible_cuda_devices = os.environ.get("CUDA_VISIBLE_DEVICES") From 07ccf2b2339c119af9359ba730c0862f8326fec3 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Sat, 18 Jul 2026 14:53:31 +0000 Subject: [PATCH 89/95] install: reconcile branch internals with the repo test suite Cross-platform staging CI surfaced five Repo tests (CPU) failures where main's tests assert on script internals this branch legitimately changed; each reconciled on its merits. The WoA native-wheel probe still used uv's deprecated --index-url alias that main's test suite now forbids in favor of --default-index (same semantics, and --default-index is what overrides inherited uv index defaults); the probe now matches the convention. The CUDA provision gate spelled its linked-dir guard with the :-false default form that main's prune-refactor test blacklists file-wide. The variable is unconditionally initialized far above, so the guard now uses the plain spelling with identical semantics. The variable guard itself stays: unlike a symlink test, it also covers the canonical-location reuse case where the linked dir is not a symlink. The gpu-detection tests extract named shell functions into a sandbox, so _setup_has_usable_nvidia_gpu's new _resolve_nvsmi dependency made the sandboxed helper die on command-not-found and report not_usable for usable cases; the extraction list now includes the resolver, and the driver-version hardening assertion tracks the resolved-path spelling while still requiring the timeout wrapper. Also hardened the resolver assignment with an explicit empty fallback so a future non-condition call site cannot trip set -e. The staging run also showed the Mac Studio Update uninstall step dying mid-run, consistent with the round-seven group kill signalling its own process group; the round-eight self-pgid guard already fixes that and this push carries it to CI. Verified: the five failing tests pass locally at this head (the one remaining local red, test_negative_control_no_tokenizers, fails identically with these changes stashed and did not fail in CI), bash -n, PowerShell AST parse, and the sh battery matches the branch baseline. --- install.ps1 | 2 +- studio/setup.sh | 4 ++-- .../studio/install/test_gpu_detection_followups.py | 13 +++++++++++-- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/install.ps1 b/install.ps1 index 0286d0fd8a..1482bf574f 100644 --- a/install.ps1 +++ b/install.ps1 @@ -2091,7 +2091,7 @@ exit 0 # a native win_arm64 CUDA wheel exists on the index. $global:LASTEXITCODE = -1 try { - & uv pip install --python $VenvPython --dry-run --reinstall "torch>=2.4,<2.11.0" --index-url $TorchIndexUrl *> $null + & uv pip install --python $VenvPython --dry-run --reinstall "torch>=2.4,<2.11.0" --default-index $TorchIndexUrl *> $null $_nativeCudaTorchOk = ($LASTEXITCODE -eq 0) } catch { $_nativeCudaTorchOk = $false } finally { $ErrorActionPreference = $prevEapProbe } if ($_nativeCudaTorchOk) { step "gpu" "native CUDA PyTorch now available for win_arm64 -- keeping native install" "Green" } diff --git a/studio/setup.sh b/studio/setup.sh index a6d219fd25..6aba8bf4f7 100755 --- a/studio/setup.sh +++ b/studio/setup.sh @@ -290,7 +290,7 @@ _setup_has_usable_nvidia_gpu() { if _setup_cvd_hides_nvidia; then return 1 fi - _setup_nvsmi="$(_resolve_nvsmi)" + _setup_nvsmi="$(_resolve_nvsmi)" || _setup_nvsmi="" if [ -n "$_setup_nvsmi" ]; then if _setup_run_smi "$_setup_nvsmi" -L 2>/dev/null \ | awk '/^GPU[[:space:]]+[0-9]+:/{found=1} END{exit !found}'; then @@ -2041,7 +2041,7 @@ if [ "$_HOST_SYSTEM" = "Linux" ] \ && _NVSMI_GATE="$(_resolve_nvsmi)" && [ -n "$_NVSMI_GATE" ] \ && "$_NVSMI_GATE" -L 2>/dev/null | awk '/^GPU[[:space:]]+[0-9]+:/{found=1} END{exit !found}' \ && [ "${_setup_nvidia_usable:-}" = true ] \ - && [ "${_LOCAL_LLAMA_CPP_LINKED:-false}" != true ] \ + && [ "$_LOCAL_LLAMA_CPP_LINKED" != true ] \ && ! _have_cuda_llama_server; then # Under WSL this runs ONLY for a DIRECT `install.sh` run: install.ps1 sets # UNSLOTH_WSL_LLAMA_DEFERRED=1 and builds in the background; a direct run has diff --git a/tests/studio/install/test_gpu_detection_followups.py b/tests/studio/install/test_gpu_detection_followups.py index f5ad9566d7..5c1339d556 100644 --- a/tests/studio/install/test_gpu_detection_followups.py +++ b/tests/studio/install/test_gpu_detection_followups.py @@ -265,10 +265,14 @@ class TestSetupShHardening: assert wrapped, "compute_cap probe must be wrapped in _setup_run_smi (timeout-bounded)" def test_driver_version_probe_timeout_wrapped(self, setup_src): + # The probe resolves nvidia-smi explicitly (root WSL shells drop + # /usr/lib/wsl/lib from PATH) and must still go through the timeout + # wrapper with the resolved path. start = setup_src.find("_cuda_driver_max_version()") end = setup_src.find("\n}", start) body = setup_src[start:end] - assert "_setup_run_smi nvidia-smi" in body + assert "_resolve_nvsmi" in body + assert '_setup_run_smi "$_cdm_smi"' in body # TEST: install.sh -- UNSLOTH_TORCH_BACKEND classified on the final path segment @@ -455,7 +459,12 @@ class TestHiddenCvdNotUsable: out = self._run_sh_helper( tmp_path, src, - ["_setup_run_smi", "_setup_cvd_hides_nvidia", "_setup_has_usable_nvidia_gpu"], + [ + "_resolve_nvsmi", + "_setup_run_smi", + "_setup_cvd_hides_nvidia", + "_setup_has_usable_nvidia_gpu", + ], cvd, ) assert out == expected From 2dc8b99803b2a36119a564f2e36e5820fa400a1f Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Sat, 18 Jul 2026 16:08:38 +0000 Subject: [PATCH 90/95] install: four round-nine review fixes across installer and uninstallers Ninth review round; each item reproduced before fixing. The inner WSL install ran install.sh without /usr/lib/wsl/lib on PATH, so its GPU detection (which checked PATH and /usr/bin only) could pick CPU torch wheels on the exact Spark/N1X path this PR exists for, failing the later torch.cuda probe. The forwarded env now appends /usr/lib/wsl/lib to PATH (appended, so a PATH nvidia-smi still wins), and install.sh's _has_usable_nvidia_gpu and torch-index _smi resolution gained the same location fallback for direct WSL runs. Three WSL failure paths in install.ps1 (WSL-not-installed deferral, the download sentinel, and the final torch.cuda failure) set LASTEXITCODE and returned, bypassing the round-eight Exit-InstallFailure fix, so powershell -Command automation using the published pipe form still saw success on those failures. All three now route through Exit-InstallFailure, which restores the rolled-aside venv and fails the process in every invocation mode. The uninstall.ps1 WSL cleanup removed /root/.unsloth before killing and matched only full argv, so cmake/nvcc children of a live CUDA build (relative argv after the provisioner cds into the tree) survived the rm and recreated files. The cleanup now signals each matched PID's whole process group (guarded against the shell's own pgid, direct children via pkill -P as fallback) before any rm; the /proc cmdline greps are unaffected by kill order since they read process state, not files. The round-eight same-group fallback called pkill -P without a guard; under this script's set -e a matched provisioner with no children at that instant (TERM pass already reaped them) aborted the whole uninstaller before any cleanup. Now || true, like the kill beside it. Reproduced in a dash sandbox with set -e: a childless matched PID previously killed the harness, now dies cleanly while setsid-group and same-group scenarios keep passing. Verified: bash -n on both shell scripts, sh -n on the extracted WSL clean snippet, PowerShell AST parse on both ps1 files, the three- scenario kill sandbox, gpu-detection and installer-index pytest suites pass, and the sh battery matches the branch baseline. --- install.ps1 | 33 ++++++++++++++++----------------- install.sh | 8 ++++++++ scripts/uninstall.ps1 | 21 ++++++++++++--------- scripts/uninstall.sh | 2 +- 4 files changed, 37 insertions(+), 27 deletions(-) diff --git a/install.ps1 b/install.ps1 index 1482bf574f..e2d69390f6 100644 --- a/install.ps1 +++ b/install.ps1 @@ -2153,13 +2153,10 @@ exit 0 substep "in an ADMINISTRATOR PowerShell run: wsl --install" "Cyan" substep "reboot, then re-run: irm https://unsloth.ai/install.ps1 | iex" "Cyan" } - # Deferred until reboot: restore any rolled-aside previous venv and signal not-complete. - # `exit 1` for -File (plain return exits 0); under `irm | iex` (no $PSCommandPath) return, - # since exit would kill the user's shell. - Restore-StudioVenvRollback - $global:LASTEXITCODE = 1 - if ($PSCommandPath) { exit 1 } - return + # Deferred until reboot: signal not-complete through Exit-InstallFailure + # (restores the rolled-aside venv, exits 1 for -File, throws under iex so + # -Command automation cannot see a deferred setup as success). + return (Exit-InstallFailure "WSL setup deferred: enable WSL2 and reboot, then re-run the installer") } $distro = if ($env:UNSLOTH_WSL_DISTRO) { $env:UNSLOTH_WSL_DISTRO } else { "Ubuntu-24.04" } @@ -2252,6 +2249,11 @@ exit 0 # below requires it, so a run that dies mid-install can no longer coast on # a stale venv passing the torch/CLI probes. $_fwdEnv += 'export UNSLOTH_SKIP_WSL_WINDOWS_SHORTCUT=1; mkdir -p /root/.unsloth; touch /root/.unsloth/.skip-wsl-windows-shortcut; rm -f /root/.unsloth/.install-ok; ' + # Root login shells reset PATH via /etc/profile and can drop /usr/lib/wsl/lib, + # the only nvidia-smi location under WSL2 GPU-PV; without it install.sh's GPU + # detection picks CPU torch wheels and the torch.cuda probe then fails the + # whole install. Appended (not prepended) so a PATH nvidia-smi still wins. + $_fwdEnv += 'export PATH="$PATH:/usr/lib/wsl/lib"; ' # Forward a non-default --package into the WSL install (already validated # against ^[a-zA-Z0-9][a-zA-Z0-9._-]*$ at parse time, so splicing is safe); # previously it was silently dropped and the user got stock unsloth. @@ -2284,10 +2286,9 @@ exit 0 # probes below would only re-validate a stale venv from a previous install. if ($wslRc -eq 86) { step "wsl" "could not download install.sh inside WSL (network or bad ref) -- the installer never ran." "Yellow" - Restore-StudioVenvRollback - $global:LASTEXITCODE = 1 - if ($PSCommandPath) { exit 1 } - return + # Exit-InstallFailure restores the rollback and fails the process in every + # invocation mode (exit for -File, throw for iex/-Command automation). + return (Exit-InstallFailure "could not download install.sh inside WSL; the installer never ran") } # $wslRc can be non-zero from the llama.cpp prebuilt step even on success, so verify torch.cuda directly. $torchOk = $false @@ -2541,12 +2542,10 @@ exit 0 $global:LASTEXITCODE = 0 return } - # Failed (torch.cuda unavailable): restore any rolled-aside previous venv and report non-zero - # (plain return exits 0 for -File; under iex `exit` would kill the caller's shell). - Restore-StudioVenvRollback - $global:LASTEXITCODE = 1 - if ($PSCommandPath) { exit 1 } - return + # Failed (torch.cuda unavailable): Exit-InstallFailure restores the rolled-aside + # venv and fails the process in every invocation mode, so iex/-Command + # automation cannot read this as success. + return (Exit-InstallFailure "WSL Studio install did not finish cleanly (torch.cuda not detected; inner exit $wslRc)") } # ── GPU arch → newest compatible Windows ROCm wheel release ── diff --git a/install.sh b/install.sh index 17baa76550..0980f539a4 100755 --- a/install.sh +++ b/install.sh @@ -1554,6 +1554,11 @@ _has_usable_nvidia_gpu() { _nvsmi="" if command -v nvidia-smi >/dev/null 2>&1; then _nvsmi="nvidia-smi" + elif [ -x "/usr/lib/wsl/lib/nvidia-smi" ]; then + # WSL2 GPU-PV ships nvidia-smi ONLY here, and root login shells drop + # the dir from PATH; without this fallback the WSL install detects no + # NVIDIA GPU and picks CPU torch wheels. + _nvsmi="/usr/lib/wsl/lib/nvidia-smi" elif [ -x "/usr/bin/nvidia-smi" ]; then _nvsmi="/usr/bin/nvidia-smi" fi @@ -2074,6 +2079,9 @@ get_torch_index_url() { _nvidia_detected=1 if command -v nvidia-smi >/dev/null 2>&1; then _smi="nvidia-smi" + elif [ -x "/usr/lib/wsl/lib/nvidia-smi" ]; then + # Same WSL2 GPU-PV location fallback as _has_usable_nvidia_gpu. + _smi="/usr/lib/wsl/lib/nvidia-smi" elif [ -x "/usr/bin/nvidia-smi" ]; then _smi="/usr/bin/nvidia-smi" fi diff --git a/scripts/uninstall.ps1 b/scripts/uninstall.ps1 index cbee753dbd..0b9ad96ae8 100644 --- a/scripts/uninstall.ps1 +++ b/scripts/uninstall.ps1 @@ -605,15 +605,18 @@ function Uninstall-UnslothStudio { if (Get-Command wsl.exe -ErrorAction SilentlyContinue) { try { # Probe candidates by exit code ('' = default distro) since `wsl --list` emits UTF-16 PS - # mis-parses. rm runs FIRST (the kills could SIGKILL this shell) and drops the dangling - # /root/.local/bin/unsloth symlink. Scope STRICTLY to /root (the fallback's install dir); - # /home/*/.unsloth may be another user's. The 8888 kill only targets a listener whose - # process cmdline is under /root/.unsloth (Studio's bind), so an unrelated service on 8888 - # -- Jupyter et al. default to it -- is NOT killed; it's also gated on an Unsloth install - # having existed (checked BEFORE rm deletes the marker). pkill matches argv containing - # /root/.unsloth/ (not bare names that would hit a user's own llama-server); the backslash - # + [h]-bracket in '/root/\.unslot[h]/' keep it from matching this command's own argv. - $_clean = '_had=0; if [ -d /root/.unsloth ] || [ -L /root/.local/bin/unsloth ]; then _had=1; fi; rm -rf /root/.unsloth /root/llama-cuda /root/provision_llama_cuda.sh /root/llama_cuda_build.log 2>/dev/null; rm -f /root/.local/bin/unsloth 2>/dev/null; if [ $_had -eq 1 ]; then for _p in $(fuser 8888/tcp 2>/dev/null); do grep -qa /root/\.unsloth/ /proc/$_p/cmdline 2>/dev/null && kill -9 $_p 2>/dev/null; done; fi; pkill -9 -f ''/root/\.unslot[h]/'' 2>/dev/null; true' + # mis-parses. Kills run BEFORE rm: a live CUDA build (cmake/nvcc under + # /root/.unsloth/llama.cpp) would otherwise keep burning CPU/GPU and recreate files after + # the rm. Each matched PID's whole process GROUP is signalled (cmake --build children carry + # relative argv no pattern can match), guarded against this shell's own pgid, plus direct + # children via pkill -P; this shell cannot self-match (its argv carries an extra backslash + # and the [h]-bracket in the pkill pattern). Scope STRICTLY to /root (the fallback's + # install dir); /home/*/.unsloth may be another user's. The 8888 kill only targets a + # listener whose process cmdline is under /root/.unsloth (Studio's bind), so an unrelated + # service on 8888 -- Jupyter et al. default to it -- is NOT killed; it's also gated on an + # Unsloth install having existed. /proc cmdline greps still work after kills since they + # read process state, not files. + $_clean = '_had=0; if [ -d /root/.unsloth ] || [ -L /root/.local/bin/unsloth ]; then _had=1; fi; _mypg=$(ps -o pgid= -p $$ 2>/dev/null | tr -d " "); for _p in $(pgrep -f ''/root/\.unslot[h]/'' 2>/dev/null); do _pg=$(ps -o pgid= -p $_p 2>/dev/null | tr -d " "); case "$_pg" in ""|0|1|"$_mypg") pkill -9 -P $_p 2>/dev/null; kill -9 $_p 2>/dev/null ;; *) kill -9 -- -$_pg 2>/dev/null || kill -9 $_p 2>/dev/null ;; esac; done; if [ $_had -eq 1 ]; then for _p in $(fuser 8888/tcp 2>/dev/null); do grep -qa /root/\.unsloth/ /proc/$_p/cmdline 2>/dev/null && kill -9 $_p 2>/dev/null; done; fi; rm -rf /root/.unsloth /root/llama-cuda /root/provision_llama_cuda.sh /root/llama_cuda_build.log 2>/dev/null; rm -f /root/.local/bin/unsloth 2>/dev/null; true' # Clean only distros with evidence of a fallback install: the wsl-distro.txt marker or an # explicit UNSLOTH_WSL_DISTRO. The broad candidate probe is only for legacy marker-less # installs (ARM64 only); on x86 it would delete distros this installer never touched diff --git a/scripts/uninstall.sh b/scripts/uninstall.sh index eb8512e758..137034e392 100755 --- a/scripts/uninstall.sh +++ b/scripts/uninstall.sh @@ -236,7 +236,7 @@ if command -v pkill >/dev/null 2>&1; then _pgid=$(ps -o pgid= -p "$_pid" 2>/dev/null | tr -d '[:space:]') case "$_pgid" in ''|0|1|"$_self_pgid") - pkill "-$_sig" -P "$_pid" 2>/dev/null + pkill "-$_sig" -P "$_pid" 2>/dev/null || true kill -s "$_sig" "$_pid" 2>/dev/null || true ;; *) kill -s "$_sig" -- "-$_pgid" 2>/dev/null \ || kill -s "$_sig" "$_pid" 2>/dev/null || true ;; From 611183ebe0722ddfb28a173d9c4b2f5e715effe6 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Sun, 19 Jul 2026 15:34:44 +0000 Subject: [PATCH 91/95] tests: track the moved pass-through inheritance in the gguf order check Main moved the llama_extra_args pass-through inheritance out of the GGUF branch into _resolve_inherited_extra_args, which runs before it, so the source-order assertion's "if request.llama_extra_args is None" anchor no longer exists inside the branch and the check failed after the main merge. The test now asserts the same property in the current shape: inheritance before the GGUF branch (a carried --no-mmproj still shapes the hub guard's companion requirement), and marker, hub guard, unload in order within the branch. Full file passes (32 tests). --- studio/backend/tests/test_gguf_load_cache_reuse.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/studio/backend/tests/test_gguf_load_cache_reuse.py b/studio/backend/tests/test_gguf_load_cache_reuse.py index 15d91cd324..6e707f6c76 100644 --- a/studio/backend/tests/test_gguf_load_cache_reuse.py +++ b/studio/backend/tests/test_gguf_load_cache_reuse.py @@ -728,9 +728,11 @@ class TestLoadHubDownloadExclusion: source = (Path(__file__).resolve().parent.parent / "routes" / "inference.py").read_text() gguf_branch = source[source.index("if config.is_gguf:") :] + # Pass-through inheritance runs before the GGUF branch, so a carried + # --no-mmproj shapes the hub guard's companion requirement. + assert source.index("_resolve_inherited_extra_args(") < source.index("if config.is_gguf:") assert ( gguf_branch.index("enter_context(gguf_load_in_flight") - < gguf_branch.index("if request.llama_extra_args is None") < gguf_branch.index("_hub_download_blocks_gguf_load") < gguf_branch.index("unsloth_backend.unload_model") ) From 22e8e8f463cd5b51124ea5db4551b22b620eec00 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Sun, 19 Jul 2026 16:21:30 +0000 Subject: [PATCH 92/95] tests: anchor the inheritance order check on the call, not the definition source.index("_resolve_inherited_extra_args(") matched the function definition, which always precedes the endpoint, so the ordering assertion was vacuously true. Anchoring on "= _resolve_inherited_ extra_args(" pins the first call site inside the load endpoint (line 4505), which is the statement whose position relative to the GGUF branch the test is meant to guard. 32 tests pass. --- studio/backend/tests/test_gguf_load_cache_reuse.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/studio/backend/tests/test_gguf_load_cache_reuse.py b/studio/backend/tests/test_gguf_load_cache_reuse.py index 6e707f6c76..6c39f813b1 100644 --- a/studio/backend/tests/test_gguf_load_cache_reuse.py +++ b/studio/backend/tests/test_gguf_load_cache_reuse.py @@ -730,7 +730,7 @@ class TestLoadHubDownloadExclusion: # Pass-through inheritance runs before the GGUF branch, so a carried # --no-mmproj shapes the hub guard's companion requirement. - assert source.index("_resolve_inherited_extra_args(") < source.index("if config.is_gguf:") + assert source.index("= _resolve_inherited_extra_args(") < source.index("if config.is_gguf:") assert ( gguf_branch.index("enter_context(gguf_load_in_flight") < gguf_branch.index("_hub_download_blocks_gguf_load") From 51009eacbe7ce8e7be3910759d56e73fbe191fc3 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Mon, 20 Jul 2026 00:21:30 +0000 Subject: [PATCH 93/95] tests: align the gguf order test with main Main fixed the stale ordering assertion in PR 7252; adopting its version verbatim removes this file from the branch diff entirely and avoids a conflict on the next main merge. 32 tests pass. --- studio/backend/tests/test_gguf_load_cache_reuse.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/studio/backend/tests/test_gguf_load_cache_reuse.py b/studio/backend/tests/test_gguf_load_cache_reuse.py index 6c39f813b1..62596fcc8a 100644 --- a/studio/backend/tests/test_gguf_load_cache_reuse.py +++ b/studio/backend/tests/test_gguf_load_cache_reuse.py @@ -728,9 +728,11 @@ class TestLoadHubDownloadExclusion: source = (Path(__file__).resolve().parent.parent / "routes" / "inference.py").read_text() gguf_branch = source[source.index("if config.is_gguf:") :] - # Pass-through inheritance runs before the GGUF branch, so a carried - # --no-mmproj shapes the hub guard's companion requirement. - assert source.index("= _resolve_inherited_extra_args(") < source.index("if config.is_gguf:") + # The gguf_load_in_flight marker must be entered before the hub-download + # guard and the unload so a concurrent load can't race the download + # manager. The llama_extra_args inheritance that used to sit between the + # marker and the guard now runs in _guard_chat_load_against_training, ahead + # of the GGUF branch, so it is no longer a landmark inside this slice. assert ( gguf_branch.index("enter_context(gguf_load_in_flight") < gguf_branch.index("_hub_download_blocks_gguf_load") From 7b1ca46652784b1ed20e867367153d6ee05fb7d6 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Mon, 20 Jul 2026 05:22:20 +0000 Subject: [PATCH 94/95] install: tighten comments in the WSL fallback paths --- install.ps1 | 301 ++++++++---------- scripts/uninstall.ps1 | 77 ++--- scripts/uninstall.sh | 42 ++- .../tests/test_gguf_load_cache_reuse.py | 9 +- studio/backend/tests/test_llama_cpp_update.py | 9 +- studio/install_llama_prebuilt.py | 6 +- studio/setup.sh | 103 +++--- .../install/test_gpu_detection_followups.py | 5 +- 8 files changed, 244 insertions(+), 308 deletions(-) diff --git a/install.ps1 b/install.ps1 index 3998f9b9e9..5d82c003f1 100644 --- a/install.ps1 +++ b/install.ps1 @@ -97,11 +97,9 @@ function Install-UnslothStudio { if ($TauriMode) { exit $Code } - # -File ignores $LASTEXITCODE on plain return, so `exit` carries the code; under - # `irm | iex` (no $PSCommandPath) `exit` would kill the user's shell. There, set - # the var for callers that check it, then raise a terminating error: interactive - # shells survive a throw and just print it, while `-Command "irm ... | iex"` - # automation exits 1 (a plain return would report success on fatal errors). + # -File: `exit` carries the code. Under `irm | iex` (no $PSCommandPath) `exit` + # would kill the user's shell, so set the var then throw: interactive shells + # survive it, `-Command "irm ... | iex"` automation exits 1 (return would look OK). if ($PSCommandPath) { exit $Code } @@ -2059,13 +2057,13 @@ exit 0 $TorchIndexUrl = Get-TorchIndexUrl # ===== Windows-on-ARM + NVIDIA GPU -> automatic WSL2 fallback (N1X "RTX Spark" / DGX Spark-class) ===== - # win_arm64 has no CUDA PyTorch/Triton wheel, so run the Linux installer inside WSL2 (full GPU) plus a - # Windows `unsloth` shim forwarding into it; x86_64 / ARM64-without-NVIDIA unaffected, and the probe - # below keeps the native install if a win_arm64 CUDA wheel ever ships. + # win_arm64 has no CUDA PyTorch/Triton wheel, so run the Linux installer inside WSL2 (full + # GPU) plus a Windows `unsloth` shim into it; x86_64 / ARM64-without-NVIDIA unaffected, and + # the probe below keeps the native install if a win_arm64 CUDA wheel ever ships. # Opt out: UNSLOTH_NO_WSL_FALLBACK=1; pick distro with UNSLOTH_WSL_DISTRO. try { $_winArm64 = ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString() -ieq 'Arm64') } catch { $_winArm64 = $false } - # x64-emulated PS on ARM reports X64/AMD64; Win32_Processor.Architecture (12=ARM64) and machine-level - # PROCESSOR_ARCHITECTURE read the true arch. Only ever turns $_winArm64 ON. + # x64-emulated PS on ARM reports X64/AMD64; Win32_Processor.Architecture (12=ARM64) and + # machine-level PROCESSOR_ARCHITECTURE read the true arch. Only ever turns $_winArm64 ON. if (-not $_winArm64) { try { if ((@(Get-CimInstance Win32_Processor -ErrorAction Stop))[0].Architecture -eq 12) { $_winArm64 = $true } } catch {} } @@ -2078,17 +2076,16 @@ exit 0 $_nativeCudaTorchOk = $false if ($_winArm64 -and $HasNvidiaSmi -and (-not $SkipTorch)) { # uv resolves for the interpreter's platform tags, so an x64-emulated venv - # python resolves the existing win_amd64 CUDA wheels and would "prove" a - # native wheel WoA can't actually use. Only a real win_arm64 interpreter - # can prove a win_arm64 CUDA wheel; anything else keeps the WSL fallback. + # python would match the win_amd64 CUDA wheels WoA can't use. Only a real + # win_arm64 interpreter proves a win_arm64 CUDA wheel; else keep the WSL fallback. $_pyArch = "" try { $_pyArch = (& $VenvPython -c "import platform; print(platform.machine())" 2>$null | Select-Object -First 1) } catch {} if ("$_pyArch" -imatch 'ARM64') { - # Probe the SAME spec as the real install ("torch>=2.4,<2.11.0"); a bare `torch` probe could - # match an out-of-range wheel, skipping WSL only to fail the real pinned install. + # Probe the real install's exact spec; a bare `torch` could match an + # out-of-range wheel, skipping WSL only to fail the real pinned install. $prevEapProbe = $ErrorActionPreference; $ErrorActionPreference = "Continue" - # --reinstall: an installed (e.g. CPU-only) torch mustn't satisfy the probe -- it must prove - # a native win_arm64 CUDA wheel exists on the index. + # --reinstall: an installed (e.g. CPU-only) torch mustn't satisfy the probe; + # it must prove a native win_arm64 CUDA wheel exists on the index. $global:LASTEXITCODE = -1 try { & uv pip install --python $VenvPython --dry-run --reinstall "torch>=2.4,<2.11.0" --default-index $TorchIndexUrl *> $null @@ -2101,34 +2098,33 @@ exit 0 step "wsl" "Windows on ARM + NVIDIA, native CUDA unavailable -- routing GPU setup through WSL2" substep "no win_arm64 CUDA PyTorch/Triton yet; WSL2 delivers full GPU (DGX Spark / RTX Spark path)." "Yellow" - # The Tauri desktop app launches its backend from a Windows venv (resolve_backend_binary), not - # WSL, so a WSL-only install would start nothing -- send those users to the CLI installer. + # The Tauri desktop app launches its backend from a Windows venv, not WSL, so a + # WSL-only install would start nothing -- send those users to the CLI installer. if ($TauriMode) { - # A prior native Studio venv was already rolled aside (Start-StudioVenvRollback, - # ~L1444) before we got here; restore it so rejecting this path doesn't orphan - # the user's working install. No-op when nothing was rolled aside. + # A prior native Studio venv was rolled aside (Start-StudioVenvRollback) before + # here; restore it so rejecting this path doesn't orphan the user's working + # install. No-op when nothing was rolled aside. Restore-StudioVenvRollback return (Exit-InstallFailure "Windows-on-ARM + NVIDIA GPU needs the WSL2 GPU install, which the desktop app can't launch yet. Install from PowerShell instead: irm https://unsloth.ai/install.ps1 | iex" 1) } - # --local installs the Windows checkout editably (uv pip install -e $RepoRoot) on the native - # path, but the WSL tunnel installs from PyPI / a git ref and never mounts $RepoRoot -- so a - # --local run here would silently install the published package inside WSL and report success. - # Reject it and point at the supported pre-merge mechanism (push the branch + UNSLOTH_INSTALL_REF). + # --local installs the Windows checkout editably, but the WSL tunnel installs from + # PyPI / a git ref and never mounts $RepoRoot -- so --local here would silently + # install the published package in WSL and report success. Reject it and point at + # the supported pre-merge mechanism (push the branch + UNSLOTH_INSTALL_REF). if ($StudioLocalInstall) { Restore-StudioVenvRollback # see TauriMode note above: don't orphan a rolled-aside venv return (Exit-InstallFailure "--local can't be honored on Windows-on-ARM + NVIDIA: the GPU install runs inside WSL2 and installs from a published/git ref, not this Windows checkout. For pre-merge testing, push your branch and set UNSLOTH_INSTALL_REF, e.g.: `$env:UNSLOTH_INSTALL_REF=''; irm https://unsloth.ai/install.ps1 | iex" 1) } - # A custom Studio root (UNSLOTH_STUDIO_HOME / STUDIO_HOME) only applies to the native Windows - # layout; the WoA GPU install lives inside WSL at /root/.unsloth and the shim/verification paths - # are fixed there. Don't pretend to honor it -- warn so the user isn't misled into thinking Studio - # landed at their custom path (the uninstaller still cleans the WSL install regardless). + # A custom Studio root only applies to the native Windows layout; the WoA GPU + # install lives in WSL at /root/.unsloth with fixed shim/verification paths. Warn + # rather than pretend to honor it (the uninstaller still cleans the WSL install). if ($envOverride) { substep "note: $envOverrideVar='$envOverride' is not used for the Windows-on-ARM WSL install -- Studio installs inside WSL at /root/.unsloth." "Yellow" } - # --with-llama-cpp-dir names a Windows-side llama.cpp, but this install runs - # llama.cpp inside WSL2 and would silently ignore the user's explicit binary - # choice. Reject like --local and point at the supported WSL-side pins. + # --with-llama-cpp-dir names a Windows-side llama.cpp, but this install runs it + # inside WSL2 and would silently ignore the choice. Reject like --local and point + # at the supported WSL-side pins. if ($WithLlamaCppDir -or $env:UNSLOTH_LOCAL_LLAMA_CPP_DIR) { Restore-StudioVenvRollback return (Exit-InstallFailure "--with-llama-cpp-dir / UNSLOTH_LOCAL_LLAMA_CPP_DIR can't be honored on Windows-on-ARM + NVIDIA: llama.cpp runs inside WSL2 and can't use a Windows path. Remove it, or pin the WSL-side build with UNSLOTH_LLAMA_TAG or UNSLOTH_LLAMA_PR instead." 1) @@ -2153,15 +2149,14 @@ exit 0 substep "in an ADMINISTRATOR PowerShell run: wsl --install" "Cyan" substep "reboot, then re-run: irm https://unsloth.ai/install.ps1 | iex" "Cyan" } - # Deferred until reboot: signal not-complete through Exit-InstallFailure - # (restores the rolled-aside venv, exits 1 for -File, throws under iex so - # -Command automation cannot see a deferred setup as success). + # Deferred until reboot: fail via Exit-InstallFailure so automation can't + # read a deferred setup as success (restores venv, exits 1 / throws). return (Exit-InstallFailure "WSL setup deferred: enable WSL2 and reboot, then re-run the installer") } $distro = if ($env:UNSLOTH_WSL_DISTRO) { $env:UNSLOTH_WSL_DISTRO } else { "Ubuntu-24.04" } - # For cmd-context uses (.cmd shim, copy-paste hints): wsl.exe rejects a QUOTED space-free name - # (WSL_E_DISTRO_NOT_FOUND on 2.x) but splits a bare spaced one after -d, so quote ONLY when spaced. + # For cmd-context uses (.cmd shim, hints): wsl.exe rejects a QUOTED space-free name + # but splits a bare spaced one after -d, so quote ONLY when spaced. $_distroArg = if ($distro -match '\s') { '"' + $distro + '"' } else { $distro } # Detect the distro by exit code (encoding-proof; wsl --list emits UTF-16 that PS mis-parses). $haveDistro = $false @@ -2169,15 +2164,14 @@ exit 0 try { & wsl.exe -d $distro -- true *> $null; if ($LASTEXITCODE -eq 0) { $haveDistro = $true } } catch {} if (-not $haveDistro) { substep "installing WSL distro '$distro' (first time only)..." "Cyan" - # New distros install at the global default WSL version; force 2 so a WSL1-default - # host doesn't get a GPU-less distro (would fail only at torch.cuda). + # Force version 2 so a WSL1-default host doesn't get a GPU-less distro + # (would fail only at torch.cuda). $global:LASTEXITCODE = -1 try { & wsl.exe --set-default-version 2 *> $null } catch {} try { & wsl.exe --install -d $distro --no-launch } catch {} } - # Verify WSL2 for pre-existing AND freshly installed distros: set-default-version - # can fail silently (old WSL builds), leaving a fresh WSL1 distro that would only - # fail at the final torch.cuda check. Detect from inside (encoding-proof, unlike + # Verify WSL2 (set-default-version can fail silently on old builds, leaving a WSL1 + # distro that only fails at torch.cuda). Detect from inside (encoding-proof, unlike # UTF-16 `wsl -l -v`) and convert in place -- `wsl --set-version` preserves files. $_wsl2Probe = 'grep -qiE ''microsoft-standard|WSL2'' /proc/version 2>/dev/null || test -e /usr/lib/wsl/lib/libcuda.so' $_isWsl2 = $false @@ -2196,75 +2190,62 @@ exit 0 substep "'$distro' converted to WSL2." "Green" } substep "installing Unsloth Studio inside WSL '$distro' with full GPU (this downloads PyTorch)..." "Cyan" - # Non-main ref: fetch + export THAT ref so the WSL venv gets the branch's setup.sh + patches - # (else install.sh pulls PyPI unsloth). main == plain unsloth.ai/install.sh. + # Non-main ref: fetch + export THAT ref so the WSL venv gets the branch's setup.sh + # + patches (else install.sh pulls PyPI unsloth). main == plain install.sh. $_instRef = Get-UnslothInstallRef - # The ref is spliced into the inner `bash -lc` twice (an `export` and a raw GitHub URL), so a - # hand-set UNSLOTH_INSTALL_REF containing shell metacharacters (`;`, `&`, `'`, spaces) would - # break/inject the command. git refs can't contain those anyway; enforce a strict allow-list - # (letters, digits, `.` `_` `/` `-`) and reject loudly rather than silently mangle the install. + # The ref is spliced into the inner `bash -lc` twice, so shell metacharacters would + # inject. git refs can't contain those anyway; enforce a strict allow-list and + # reject loudly rather than silently mangle the install. if ($_instRef -ne 'main' -and ($_instRef -notmatch '^[A-Za-z0-9][A-Za-z0-9._/-]*$')) { Restore-StudioVenvRollback # see TauriMode note above: don't orphan a rolled-aside venv return (Exit-InstallFailure "UNSLOTH_INSTALL_REF='$_instRef' is not a valid git ref (allowed: letters, digits, '.', '_', '/', '-'). Set it to a real branch or tag name." 1) } - # UNSLOTH_WSL_LLAMA_DEFERRED=1: setup.sh skips its foreground CUDA llama.cpp build since we build it - # in the background. apt stderr stays visible (only stdout -> /dev/null) so failures are diagnosable. - # Forward UNSLOTH_NO_LLAMA_CUDA into WSL: it also skips the dispatch below, so unforwarded setup.sh - # would defer to a background builder that never starts (no llama-server). + # UNSLOTH_WSL_LLAMA_DEFERRED=1: setup.sh skips its foreground CUDA llama.cpp build; + # we build it in the background. apt stderr stays visible so failures are diagnosable. + # Forward UNSLOTH_NO_LLAMA_CUDA (it also skips the dispatch below, so unforwarded + # setup.sh would defer to a background builder that never starts). $_fwdEnv = '' if ($env:UNSLOTH_NO_LLAMA_CUDA -eq '1') { $_fwdEnv = 'export UNSLOTH_NO_LLAMA_CUDA=1; ' } - # Forward a user Python pin (install.sh reads UNSLOTH_PYTHON, but Windows env vars don't cross - # into WSL unless bridged). Numeric-only guard (e.g. 3.12) prevents injection. + # Forward a user Python pin (Windows env vars don't cross into WSL unless bridged). + # Numeric-only guard (e.g. 3.12) prevents injection. if ($env:UNSLOTH_PYTHON -and ($env:UNSLOTH_PYTHON -match '^[0-9][0-9.]*$')) { $_fwdEnv += "export UNSLOTH_PYTHON=$($env:UNSLOTH_PYTHON); " } - # Forward a custom PyTorch wheel mirror. install.sh reads UNSLOTH_PYTORCH_MIRROR (get_torch_index_url) - # but, like the other Windows env vars, it doesn't cross into WSL -- so a mirror-required / restricted- - # network install would silently fall back to download.pytorch.org inside the distro even though the - # outer installer honored the mirror. Strict http(s)-URL allow-list (no shell metachars) + single-quote - # so the value can't break out of the bash -lc string. + # Forward a custom PyTorch wheel mirror (doesn't cross into WSL, so a restricted- + # network install would silently fall back to download.pytorch.org). Strict http(s) + # allow-list + single-quote so the value can't break out of the bash -lc string. if ($env:UNSLOTH_PYTORCH_MIRROR -and ($env:UNSLOTH_PYTORCH_MIRROR -match '^https?://[A-Za-z0-9._~:/?#@%+=&-]+$')) { $_fwdEnv += "export UNSLOTH_PYTORCH_MIRROR='$($env:UNSLOTH_PYTORCH_MIRROR)'; " } - # Forward the npm mirror the same way: setup.sh threads UNSLOTH_NPM_REGISTRY - # into every npm/bun install, and on mirror-required networks the WSL - # frontend/OXC steps would otherwise hit registry.npmjs.org and fail the - # install. Same strict http(s) allow-list + single-quote as above. + # Forward the npm mirror the same way (else the WSL frontend/OXC steps hit + # registry.npmjs.org and fail on mirror-required networks). Same allow-list as above. if ($env:UNSLOTH_NPM_REGISTRY -and ($env:UNSLOTH_NPM_REGISTRY -match '^https?://[A-Za-z0-9._~:/?#@%+=&-]+$')) { $_fwdEnv += "export UNSLOTH_NPM_REGISTRY='$($env:UNSLOTH_NPM_REGISTRY)'; " } - # Forward an explicit UNSLOTH_PYTHON pin: Windows env vars do not cross into - # WSL, so without this the inner install.sh silently built the venv on its - # default Python while the installer reported success. Strict version shape - # so the splice into bash -lc cannot break out; the default stays install.sh's. + # Forward an explicit UNSLOTH_PYTHON pin (env vars don't cross into WSL, so without + # this install.sh silently built the venv on its default Python). Strict version + # shape so the splice into bash -lc can't break out; default stays install.sh's. if ($env:UNSLOTH_PYTHON -and ($env:UNSLOTH_PYTHON -match '^\d+\.\d+(\.\d+)?$')) { $_fwdEnv += "export UNSLOTH_PYTHON='$($env:UNSLOTH_PYTHON)'; " } - # install.ps1 owns the WoA shortcut (one canonical "Unsloth Studio.lnk" with a - # %USERPROFILE%\.unsloth icon that renders on WoA). Tell install.sh to skip its own - # WSL .lnk so we don't get a duplicate whose %LOCALAPPDATA% icon renders blank. - # Persist the skip as a marker file too: `unsloth studio update` reruns - # install.sh --shortcuts-only through the wsl.exe shim, which carries no env, - # so without the marker the first update would recreate the duplicate .lnk. - # Clear the completion stamp from any previous install: setup.sh rewrites - # it only after the core venv + Studio deps finish, and the post-run gate - # below requires it, so a run that dies mid-install can no longer coast on - # a stale venv passing the torch/CLI probes. + # install.ps1 owns the WoA shortcut; tell install.sh to skip its own WSL .lnk so we + # don't get a duplicate whose %LOCALAPPDATA% icon renders blank. Persist a marker + # too: `unsloth studio update` reruns install.sh through the wsl.exe shim (no env), + # so without it the first update recreates the duplicate .lnk. + # Clear any previous completion stamp: setup.sh rewrites it only after the core venv + # + Studio deps finish and the post-run gate below requires it, so a run that dies + # mid-install can no longer coast on a stale venv passing the torch/CLI probes. $_fwdEnv += 'export UNSLOTH_SKIP_WSL_WINDOWS_SHORTCUT=1; mkdir -p /root/.unsloth; touch /root/.unsloth/.skip-wsl-windows-shortcut; rm -f /root/.unsloth/.install-ok; ' - # Root login shells reset PATH via /etc/profile and can drop /usr/lib/wsl/lib, - # the only nvidia-smi location under WSL2 GPU-PV; without it install.sh's GPU - # detection picks CPU torch wheels and the torch.cuda probe then fails the - # whole install. Appended (not prepended) so a PATH nvidia-smi still wins. + # Root login shells reset PATH and can drop /usr/lib/wsl/lib, the only nvidia-smi + # location under WSL2 GPU-PV; without it install.sh picks CPU torch wheels and the + # torch.cuda probe fails. Appended (not prepended) so a PATH nvidia-smi still wins. $_fwdEnv += 'export PATH="$PATH:/usr/lib/wsl/lib"; ' - # Forward a non-default --package into the WSL install (already validated - # against ^[a-zA-Z0-9][a-zA-Z0-9._-]*$ at parse time, so splicing is safe); + # Forward a non-default --package (validated at parse time, so splicing is safe); # previously it was silently dropped and the user got stock unsloth. $_shArgs = '' if ($PackageName -ne 'unsloth') { $_shArgs = ' --package ' + $PackageName } - # Download to a file instead of `curl | sh`: a failed download feeds sh an - # empty stdin (exit 0), and on a rerun the stale venv then passes the torch - # probe below, reporting success without the installer ever running. Exit 86 - # is the "download failed, installer never ran" sentinel checked after the - # run. /root/.unsloth already exists (skip-marker mkdir above) and the file - # is removed with it on uninstall. + # Download to a file instead of `curl | sh`: a failed download feeds sh empty + # stdin (exit 0) and a rerun's stale venv then passes the torch probe, faking + # success without the installer running. Exit 86 is the "download failed" sentinel + # checked after the run. /root/.unsloth exists already and is removed on uninstall. if ($_instRef -eq 'main') { $wslInstall = $_fwdEnv + 'export DEBIAN_FRONTEND=noninteractive UNSLOTH_WSL_LLAMA_DEFERRED=1; apt-get update -y >/dev/null; apt-get install -y build-essential cmake git curl pciutils libcurl4-openssl-dev >/dev/null; curl -fsSL https://unsloth.ai/install.sh -o /root/.unsloth/unsloth-install.sh || exit 86; sh /root/.unsloth/unsloth-install.sh' + $_shArgs } else { @@ -2282,15 +2263,14 @@ exit 0 $ErrorActionPreference = $prevEapWsl } Write-Host "" - # Sentinel from the download step above: the installer never ran, so the - # probes below would only re-validate a stale venv from a previous install. + # Download sentinel: the installer never ran, so the probes below would only + # re-validate a stale venv from a previous install. if ($wslRc -eq 86) { step "wsl" "could not download install.sh inside WSL (network or bad ref) -- the installer never ran." "Yellow" - # Exit-InstallFailure restores the rollback and fails the process in every - # invocation mode (exit for -File, throw for iex/-Command automation). + # Exit-InstallFailure restores the rollback and fails in every invocation mode. return (Exit-InstallFailure "could not download install.sh inside WSL; the installer never ran") } - # $wslRc can be non-zero from the llama.cpp prebuilt step even on success, so verify torch.cuda directly. + # $wslRc can be non-zero from the llama.cpp step even on success; verify torch.cuda directly. $torchOk = $false $prevEapChk = $ErrorActionPreference $ErrorActionPreference = "Continue" @@ -2300,10 +2280,9 @@ exit 0 & wsl.exe -d $distro --cd /root -u root -- /root/.unsloth/studio/unsloth_studio/bin/python -c "import torch,sys; sys.exit(0 if torch.cuda.is_available() else 3)" *> $null $torchOk = ($LASTEXITCODE -eq 0) } catch {} finally { $ErrorActionPreference = $prevEapChk } - # torch.cuda alone isn't success: install.sh can exit after PyTorch but before the `unsloth` - # package/console script (e.g. a transient `uv pip install unsloth`), and $wslRc can't tell - # (it also goes non-zero on the optional llama prebuilt step). Verify the exact binary the shim - # execs exists -- else we'd write a dangling shim and report a broken install as success. + # torch.cuda alone isn't success: install.sh can exit after PyTorch but before the + # `unsloth` console script, and $wslRc can't tell. Verify the exact binary the shim + # execs exists -- else we'd write a dangling shim and report a broken install as OK. if ($torchOk) { $prevEapCli = $ErrorActionPreference; $ErrorActionPreference = "Continue" $global:LASTEXITCODE = -1 @@ -2314,12 +2293,10 @@ exit 0 $torchOk = $false } } - # Require the completion stamp setup.sh writes after the core venv + - # Studio deps finish (cleared above before the run). torch + CLI alone - # can both come from a stale venv left by a PREVIOUS install while this - # run's installer died mid-way; the tolerated nonzero $wslRc (optional - # llama.cpp step) makes that indistinguishable by exit code. Existence - # only, no mtime compare: WSL and Windows clocks can skew. + # Require the completion stamp setup.sh writes after the core venv + Studio deps + # finish (cleared before the run). torch + CLI alone can both come from a stale + # PREVIOUS install while this run died mid-way, which the tolerated nonzero $wslRc + # can't distinguish. Existence only, no mtime: WSL/Windows clocks can skew. if ($torchOk) { $prevEapStamp = $ErrorActionPreference; $ErrorActionPreference = "Continue" $global:LASTEXITCODE = -1 @@ -2330,9 +2307,9 @@ exit 0 $torchOk = $false } } - # Self-heal web-server deps: a cut-short install.sh "studio deps" step leaves torch + unsloth but - # no fastapi/uvicorn/structlog/starlette (`unsloth studio` dies). Reinstall them unpinned (no - # huggingface-hub/transformers/datasets) so the verified GPU torch stack stays intact. + # Self-heal web-server deps: a cut-short "studio deps" step leaves torch + unsloth + # but no fastapi/uvicorn/structlog/starlette (`unsloth studio` dies). Reinstall them + # unpinned (no hf-hub/transformers/datasets) so the verified GPU torch stack stays. if ($torchOk) { $_studioPy = "/root/.unsloth/studio/unsloth_studio/bin/python" $_serverOk = $false @@ -2343,9 +2320,9 @@ exit 0 } catch {} finally { $ErrorActionPreference = $prevEapS } if (-not $_serverOk) { substep "Studio web-server deps incomplete (install.sh step cut short) -- installing them now..." "Cyan" - # studio.txt minus the huggingface-hub pin; uv preferred, pip fallback. Bare names only: - # `>=` would become a redirection through PowerShell -> wsl.exe -> bash -lc, and latest-of-each - # satisfies the studio.txt minimums anyway. + # studio.txt minus the hf-hub pin; uv preferred, pip fallback. Bare names + # only: `>=` would become a redirection through PS -> wsl.exe -> bash -lc, + # and latest-of-each satisfies the studio.txt minimums anyway. $_deps = 'typer fastapi uvicorn matplotlib pandas nest_asyncio pyjwt easydict addict structlog diceware ddgs cryptography httpx fastmcp sqlite-vec pymupdf python-docx' $_repair = 'PY=/root/.unsloth/studio/unsloth_studio/bin/python; UV="$(command -v uv 2>/dev/null || echo /root/.local/bin/uv)"; if [ -x "$UV" ] || command -v uv >/dev/null 2>&1; then "$UV" pip install --python "$PY" ' + $_deps + '; else "$PY" -m pip install ' + $_deps + '; fi' $prevEapR = $ErrorActionPreference; $ErrorActionPreference = "Continue" @@ -2357,16 +2334,15 @@ exit 0 } catch {} finally { $ErrorActionPreference = $prevEapS2 } if ($_serverOk) { substep "Studio web-server deps installed." "Green" } else { - # The missing set includes typer, so even the plain unsloth CLI - # dies; creating shims and reporting success over that state - # advertises commands that cannot run. Route to the failure - # path (rollback + non-zero), like the CLI-missing case above. + # The missing set includes typer, so even the plain unsloth CLI dies; + # reporting success would advertise commands that can't run. Route to + # the failure path, like the CLI-missing case above. substep "Studio server deps missing and the repair failed -- not reporting success over a broken install." "Yellow" $torchOk = $false } } - # The uv-managed venv ships no `pip`, but unsloth-zoo's check_pip() finds `uv pip` only - # when uv is on PATH. Seed pip so `save_pretrained_gguf` works regardless. + # The uv-managed venv ships no `pip`, but unsloth-zoo's check_pip() finds `uv + # pip` only when uv is on PATH. Seed pip so `save_pretrained_gguf` works regardless. $prevEapP = $ErrorActionPreference; $ErrorActionPreference = "Continue" try { & wsl.exe -d $distro --cd /root -u root -- $_studioPy -m pip --version *> $null @@ -2377,8 +2353,8 @@ exit 0 } if ($torchOk) { step "done" "Unsloth Studio installed in WSL '$distro' -- GPU ready (torch.cuda available)." "Green" - # Native Windows `unsloth` shim forwards every `unsloth ...` into the WSL GPU env so the user - # never touches WSL. WSL2 forwards 127.0.0.1, so http://localhost:8888 opens in Windows. + # Native Windows `unsloth` shim forwards every `unsloth ...` into the WSL GPU + # env. WSL2 forwards 127.0.0.1, so http://localhost:8888 opens in Windows. try { $shimDir = Join-Path $env:LOCALAPPDATA "Unsloth\bin" New-Item -ItemType Directory -Force -Path $shimDir *> $null @@ -2388,19 +2364,16 @@ exit 0 "wsl.exe -d $_distroArg -u root -- /root/.unsloth/studio/unsloth_studio/bin/unsloth %*" ) Set-Content -LiteralPath (Join-Path $shimDir "unsloth.cmd") -Value $shimLines -Encoding ASCII - # Record the distro so the uninstaller can clean a custom UNSLOTH_WSL_DISTRO install - # without the env var set. + # Record the distro so the uninstaller can clean a custom + # UNSLOTH_WSL_DISTRO install without the env var set. try { Set-Content -LiteralPath (Join-Path (Split-Path $shimDir -Parent) "wsl-distro.txt") -Value $distro -Encoding ASCII } catch {} - # PREPEND (not append): a previous NATIVE install prepended - # %USERPROFILE%\.unsloth\studio\bin (unsloth.exe) to user PATH, - # and that exe outlives the venv this fallback just rolled aside - # -- an appended shim would lose to the dead native launcher in - # every new terminal. Add-ToUserPath de-dupes and hoists. + # PREPEND (not append): a previous NATIVE install prepended its + # unsloth.exe to user PATH, and that exe outlives the rolled-aside venv -- + # an appended shim would lose to the dead launcher. Add-ToUserPath de-dupes. $null = Add-ToUserPath -Directory $shimDir -Position 'Prepend' $env:Path = $shimDir + ";" + $env:Path.TrimStart(';') - # Drop the dead default-root native shim outright when the venv - # binary it launches is gone (custom-root shims are left alone; - # the PATH prepend above already outranks them). + # Drop the dead default-root native shim when the venv binary it launches + # is gone (custom-root shims are left alone; the PATH prepend outranks them). try { $staleNativeShim = Join-Path $env:USERPROFILE ".unsloth\studio\bin\unsloth.exe" $staleNativeTarget = Join-Path $env:USERPROFILE ".unsloth\studio\unsloth_studio\Scripts\unsloth.exe" @@ -2423,13 +2396,11 @@ exit 0 $L = @( '$ErrorActionPreference = "SilentlyContinue"', ('$distro = "' + $distro + '"'), - # Port 8888 may already be taken on the Windows side (Jupyter, a - # second Studio): Studio inside WSL would bind a different port - # while the poll below waits on 8888 forever and the browser - # never opens. Scan the same 8888..8908 window the native - # launcher uses (Find-FreeLaunchPort) and pass the winner via - # -p. WSL2 localhost forwarding mirrors the WSL port onto - # Windows, so probing with a Windows-side TcpListener is valid. + # Port 8888 may be taken on the Windows side (Jupyter, a second + # Studio): WSL Studio would bind another port while the poll waits on + # 8888 forever. Scan the same 8888..8908 window the native launcher uses + # and pass the winner via -p. WSL2 mirrors the port onto Windows, so a + # Windows-side TcpListener probe is valid. '$port = 0', 'foreach ($p in 8888..8908) { $l = $null; try { $l = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Any, $p); $l.Start(); $port = $p } catch {} finally { if ($l) { try { $l.Stop() } catch {} } }; if ($port) { break } }', 'if (-not $port) { Write-Host "No free port in 8888-8908; close one of the apps using them and relaunch."; Start-Sleep 10; exit 1 }', @@ -2438,13 +2409,13 @@ exit 0 'wsl.exe -d $distro --cd /root -u root -- bash -lic "unsloth studio -p $port"' ) Set-Content -LiteralPath $launcher -Value $L -Encoding UTF8 - # Icon must live OUTSIDE %LOCALAPPDATA%: on WoA the sandboxed icon broker can't read a .ico - # under AppData\Local, so the shortcut renders BLANK; under the user profile it renders - # fine (verified on N1X). Only the icon moves. + # Icon must live OUTSIDE %LOCALAPPDATA%: on WoA the sandboxed icon broker + # can't read a .ico under AppData\Local, so the shortcut renders BLANK; + # under the user profile it renders fine (verified on N1X). Only the icon moves. $iconDir = Join-Path $env:USERPROFILE ".unsloth" New-Item -ItemType Directory -Force -Path $iconDir *> $null $icon = Join-Path $iconDir "unsloth.ico" - # Prefer the bundled icon, else download from GitHub. Validate the ICO header (00 00 01 00) + # Prefer the bundled icon, else download. Validate the ICO header (00 00 01 00) # before attaching, so a partial/HTML-404 download never makes a blank icon. $bundledIcon = $null if ($PSScriptRoot -and $PSScriptRoot.Trim()) { $bundledIcon = Join-Path $PSScriptRoot "studio\frontend\public\unsloth.ico" } @@ -2475,15 +2446,15 @@ exit 0 $sc.Save() } step "shortcuts" "created Desktop + Start Menu shortcuts (launch WSL Studio + open browser)" "Green" - # Nudge Explorer: clear+rebuild icon cache, per-.lnk SHCNE_UPDATEITEM, global - # SHCNE_ASSOCCHANGED. (The real WoA blank-icon cause was the icon path, fixed above.) + # Nudge Explorer: clear icon cache, per-.lnk SHCNE_UPDATEITEM, global + # SHCNE_ASSOCCHANGED. (The real WoA blank-icon cause was the icon path.) try { & "$env:SystemRoot\System32\ie4uinit.exe" -ClearIconCache 2>$null } catch {} try { & "$env:SystemRoot\System32\ie4uinit.exe" -show 2>$null } catch {} try { if (-not ("UnslothShell.Notify" -as [type])) { Add-Type -Namespace UnslothShell -Name Notify -MemberDefinition '[System.Runtime.InteropServices.DllImport("shell32.dll", CharSet = System.Runtime.InteropServices.CharSet.Unicode)] public static extern void SHChangeNotify(int eventId, uint flags, string item1, System.IntPtr item2);' } - # SHCNE_UPDATEITEM (0x00002000), SHCNF_PATHW (0x0005): the global notify alone often misses existing .lnks. + # SHCNE_UPDATEITEM (0x00002000), SHCNF_PATHW (0x0005): global notify alone often misses existing .lnks. foreach ($lnk in $lnks) { try { [UnslothShell.Notify]::SHChangeNotify(0x00002000, 0x0005, $lnk, [System.IntPtr]::Zero) } catch {} } # SHCNE_ASSOCCHANGED (0x08000000), SHCNF_IDLIST (0): flush global icon associations. [UnslothShell.Notify]::SHChangeNotify(0x08000000, 0, $null, [System.IntPtr]::Zero) @@ -2491,23 +2462,22 @@ exit 0 } catch { substep "(could not create shortcuts: $($_.Exception.Message))" "Yellow" } - # GGUF *inference* needs a CUDA llama-server (no aarch64+CUDA prebuilt), so build one into - # ~/.unsloth/llama.cpp in the BACKGROUND. Best-effort; opt out: UNSLOTH_NO_LLAMA_CUDA=1. + # GGUF *inference* needs a CUDA llama-server (no aarch64+CUDA prebuilt), so build + # one into ~/.unsloth/llama.cpp in the BACKGROUND. Opt out: UNSLOTH_NO_LLAMA_CUDA=1. if ($env:UNSLOTH_NO_LLAMA_CUDA -ne '1') { $prevEapL = $ErrorActionPreference; $ErrorActionPreference = "Continue" try { $_llamaUrl = "https://raw.githubusercontent.com/unslothai/unsloth/$(Get-UnslothInstallRef)/studio/scripts/provision_llama_cuda.sh" - # Step 1: fetch the provision script + write a runner (base64 to dodge quoting layers). - # The runner restores PATH (non-login shells miss /usr/lib/wsl/lib nvidia-smi, so provision - # early-exits) and exports the env knobs below (Windows env vars don't cross into WSL). A - # runner FILE lets the detached launcher pass only space-free args, avoiding Start-Process - # mis-splitting `bash -lc `. + # Step 1: fetch the provision script + write a runner (base64 to dodge + # quoting layers). The runner restores PATH (non-login shells miss the + # /usr/lib/wsl/lib nvidia-smi) and exports the env knobs below. A runner + # FILE lets the detached launcher pass only space-free args, avoiding + # Start-Process mis-splitting `bash -lc `. $_pathLine = 'export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/lib/wsl/lib:$PATH"' + "`n" $_jobsLine = if ($env:UNSLOTH_LLAMA_BUILD_JOBS) { "export UNSLOTH_LLAMA_BUILD_JOBS=$($env:UNSLOTH_LLAMA_BUILD_JOBS)`n" } else { "" } - # Bridge UNSLOTH_LLAMA_TAG / UNSLOTH_LLAMA_PR pins into WSL, else the deferred build - # ignores them. sh-single-quoted since tags/PRs are simple tokens. - # Same allow-lists as the other forwarded knobs: a quote in the - # value would break out of the single-quoted export in the runner. + # Bridge UNSLOTH_LLAMA_TAG / UNSLOTH_LLAMA_PR pins into WSL, else the + # deferred build ignores them. Same allow-lists as the other forwarded + # knobs so a quote can't break out of the single-quoted export. $_tagLine = if ($env:UNSLOTH_LLAMA_TAG -and ($env:UNSLOTH_LLAMA_TAG -match '^[A-Za-z0-9][A-Za-z0-9._/-]*$')) { "export UNSLOTH_LLAMA_TAG='$($env:UNSLOTH_LLAMA_TAG)'`n" } else { "" } $_prLine = if ($env:UNSLOTH_LLAMA_PR -and ($env:UNSLOTH_LLAMA_PR -match '^\d+$')) { "export UNSLOTH_LLAMA_PR='$($env:UNSLOTH_LLAMA_PR)'`n" } else { "" } $_runner = "#!/usr/bin/env bash`n" + $_pathLine + $_jobsLine + $_tagLine + $_prLine + "exec bash /root/.unsloth/provision_llama_cuda.sh > /root/.unsloth/llama_cuda_build.log 2>&1`n" @@ -2515,10 +2485,10 @@ exit 0 $_fetchCmd = 'mkdir -p /root/.unsloth; if curl -fsSL "' + $_llamaUrl + '" -o /root/.unsloth/provision_llama_cuda.sh && [ -s /root/.unsloth/provision_llama_cuda.sh ]; then chmod +x /root/.unsloth/provision_llama_cuda.sh; echo ' + $_runnerB64 + ' | base64 -d > /root/.unsloth/run_llama_build.sh; chmod +x /root/.unsloth/run_llama_build.sh; echo PROV_FETCHED; else echo PROV_NOSCRIPT; fi' $_fetchOut = & wsl.exe -d $distro --cd /root -u root -- bash -lc $_fetchCmd 2>$null if ("$_fetchOut" -match 'PROV_FETCHED') { - # Step 2: a detached Windows-side wsl.exe keeps the WSL VM up for the whole build - # (a WSL-side `nohup &` dies when the launching session exits). PS 5.1 Start-Process - # joins -ArgumentList WITHOUT quoting, so pass $_distroArg (pre-quoted only when - # spaced); all other tokens are space-free. + # Step 2: a detached Windows-side wsl.exe keeps the WSL VM up for the + # whole build (a WSL-side `nohup &` dies when the session exits). PS 5.1 + # Start-Process joins -ArgumentList WITHOUT quoting, so pass $_distroArg + # (pre-quoted only when spaced); other tokens are space-free. Start-Process -WindowStyle Hidden -FilePath 'wsl.exe' -ArgumentList @('-d', $_distroArg, '--cd', '/root', '-u', 'root', '--', 'bash', '/root/.unsloth/run_llama_build.sh') | Out-Null step "llama.cpp" "building CUDA llama.cpp for GGUF inference in the background (a few min); log: ~/.unsloth/llama_cuda_build.log" "Green" } else { @@ -2531,20 +2501,17 @@ exit 0 substep "retry, or launch manually: wsl -d $_distroArg -u root -- bash -lic 'unsloth studio -p 8888'" "Cyan" } if ($torchOk) { - # Success: the Windows venv is vestigial (everything runs in WSL), so drop the - # rolled-aside previous-venv backup instead of orphaning it. EXCEPT for a - # custom UNSLOTH_STUDIO_HOME: the installer told the user above that their - # custom root is not used by the WSL install, so deleting the venv that - # lived there would contradict that disclaimer -- put it back instead - # (the WSL shim does not depend on the Windows venv). + # Success: the Windows venv is vestigial (all runs in WSL), so drop the + # rolled-aside backup instead of orphaning it. EXCEPT a custom + # UNSLOTH_STUDIO_HOME: we told the user their custom root isn't used by the WSL + # install, so restore its venv rather than delete it (the shim doesn't need it). if ($envOverride) { Restore-StudioVenvRollback } else { Complete-StudioVenvRollback } substep "GPU training + GGUF export run inside WSL. (GGUF *inference* additionally needs a CUDA llama.cpp build.)" "Yellow" $global:LASTEXITCODE = 0 return } - # Failed (torch.cuda unavailable): Exit-InstallFailure restores the rolled-aside - # venv and fails the process in every invocation mode, so iex/-Command - # automation cannot read this as success. + # Failed (torch.cuda unavailable): Exit-InstallFailure restores the venv and fails + # in every invocation mode, so automation cannot read this as success. return (Exit-InstallFailure "WSL Studio install did not finish cleanly (torch.cuda not detected; inner exit $wslRc)") } diff --git a/scripts/uninstall.ps1 b/scripts/uninstall.ps1 index 8afee9667d..3c2666f51d 100644 --- a/scripts/uninstall.ps1 +++ b/scripts/uninstall.ps1 @@ -16,11 +16,10 @@ function Uninstall-UnslothStudio { function _Step { param([string]$Msg) Write-Host $Msg } function _Substep { param([string]$Msg, [string]$Color = "Gray") Write-Host " $Msg" -ForegroundColor $Color } - # True host architecture, mirroring install.ps1's WSL-fallback gate: an - # x64-emulated PowerShell on ARM64 reports AMD64 in PROCESSOR_ARCHITECTURE, - # which made the legacy marker-less WSL cleanup below skip exactly the - # machines the fallback installed on. Each probe only ever turns the answer - # ON; Win32_Processor.Architecture 12 = ARM64. + # True host architecture, mirroring install.ps1's WSL-fallback gate: x64-emulated + # PowerShell on ARM64 reports AMD64, which made the legacy marker-less WSL cleanup + # skip the machines the fallback installed on. Each probe only turns the answer ON; + # Win32_Processor.Architecture 12 = ARM64. function _IsArm64Host { $arm = $false try { $arm = ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString() -ieq 'Arm64') } catch { } @@ -419,12 +418,10 @@ function Uninstall-UnslothStudio { } # ── Remove desktop and Start Menu shortcuts ── - # Canonical name is "Unsloth Studio.lnk". Distro-suffixed names - # ("Unsloth Studio (WSL - ).lnk") belong to per-distro WSL installs, which - # the WSL-fallback section below only cleans for evidenced distros (env var, - # wsl-distro.txt marker, or the legacy ARM64 probe) -- scope this sweep to the - # same set so a surviving WSL install keeps its launcher. Anything that is not a - # live wsl.exe launcher (pre-release leftovers) is still swept. + # Canonical name is "Unsloth Studio.lnk". Distro-suffixed names belong to per-distro + # WSL installs, which the section below only cleans for evidenced distros (env var, + # wsl-distro.txt, or legacy ARM64 probe) -- scope this sweep to the same set so a + # surviving WSL install keeps its launcher. Non-wsl.exe launchers are still swept. _Step "Removing desktop and Start Menu shortcuts..." $_scCands = @() if ($env:UNSLOTH_WSL_DISTRO) { $_scCands += $env:UNSLOTH_WSL_DISTRO } @@ -455,8 +452,8 @@ function Uninstall-UnslothStudio { $_sc = $_scWs.CreateShortcut($_.FullName) if ("$($_sc.TargetPath) $($_sc.Arguments)" -match "wsl\.exe") { $_scD = $null - # install.sh quotes spaced distro names (-d "Ubuntu Preview"), so match a - # full quoted token first; a naive [^"\s]+ would truncate at the space. + # install.sh quotes spaced distro names, so match a full quoted + # token first; a naive [^"\s]+ would truncate at the space. if ($_sc.Arguments -match '-d\s+(?:"([^"]+)"|(\S+))') { $_scD = if ($Matches[1]) { $Matches[1] } else { $Matches[2] } } @@ -543,11 +540,11 @@ function Uninstall-UnslothStudio { # ── Windows-on-Arm WSL-fallback artifacts ── # The ARM64+NVIDIA fallback puts Studio in WSL plus a native shim + launcher under - # %LOCALAPPDATA%\Unsloth (not "Unsloth Studio") with a PATH entry -- none caught above. + # %LOCALAPPDATA%\Unsloth with a PATH entry -- none caught above. _Step "Removing WSL-fallback artifacts (shim, launcher, PATH entry, WSL install)..." $unslothDir = if ($env:LOCALAPPDATA) { Join-Path $env:LOCALAPPDATA "Unsloth" } else { $null } - # wsl-distro.txt records a custom UNSLOTH_WSL_DISTRO install so it's cleanable without the env - # var set; read it BEFORE the directory is removed below. + # wsl-distro.txt records a custom UNSLOTH_WSL_DISTRO install so it's cleanable without + # the env var set; read it BEFORE the directory is removed below. $_recordedDistro = $null if ($unslothDir) { try { @@ -580,10 +577,9 @@ function Uninstall-UnslothStudio { _RemovePath $unslothDir } # The WoA shortcut icon lives under the user profile (icon broker can't read - # AppData\Local). The shortcut sweep above deliberately keeps launchers for - # WSL installs it has no evidence for; those .lnks point at this icon, so - # only remove it when no Unsloth shortcut survives anywhere (mirrors the - # _drop_shared_icon_if_unused guard on the WSL-side uninstaller). + # AppData\Local). The sweep above keeps launchers for WSL installs it has no evidence + # for, and those .lnks point at this icon, so only remove it when no Unsloth shortcut + # survives anywhere (mirrors _drop_shared_icon_if_unused on the WSL-side uninstaller). if ($env:USERPROFILE) { $_icoInUse = $false foreach ($_icoDir in $shortcutDirs) { @@ -594,33 +590,30 @@ function Uninstall-UnslothStudio { } if (-not $_icoInUse) { _RemovePath (Join-Path $env:USERPROFILE ".unsloth\unsloth.ico") } } - # The empty-dir sweep of ~/.unsloth above ran BEFORE this icon removal, so on a WoA install - # the still-present unsloth.ico kept ~/.unsloth non-empty then and it was skipped -- leaving an - # empty ~/.unsloth behind. Re-attempt now that the icon (the last default-mode child) is gone. + # The ~/.unsloth empty-dir sweep above ran BEFORE this icon removal, so the still-present + # unsloth.ico kept it non-empty and it was skipped. Re-attempt now that the icon (the + # last default-mode child) is gone. if ($defaultUnslothHome -and (Test-Path -LiteralPath $defaultUnslothHome) -and -not (Get-ChildItem -LiteralPath $defaultUnslothHome -Force -ErrorAction SilentlyContinue)) { _RemovePath $defaultUnslothHome } - # Remove the Studio install inside each WSL distro (the real GPU install + any CUDA llama.cpp build). + # Remove the Studio install inside each WSL distro (GPU install + any CUDA llama.cpp build). if (Get-Command wsl.exe -ErrorAction SilentlyContinue) { try { - # Probe candidates by exit code ('' = default distro) since `wsl --list` emits UTF-16 PS - # mis-parses. Kills run BEFORE rm: a live CUDA build (cmake/nvcc under - # /root/.unsloth/llama.cpp) would otherwise keep burning CPU/GPU and recreate files after - # the rm. Each matched PID's whole process GROUP is signalled (cmake --build children carry - # relative argv no pattern can match), guarded against this shell's own pgid, plus direct - # children via pkill -P; this shell cannot self-match (its argv carries an extra backslash - # and the [h]-bracket in the pkill pattern). Scope STRICTLY to /root (the fallback's - # install dir); /home/*/.unsloth may be another user's. The 8888 kill only targets a - # listener whose process cmdline is under /root/.unsloth (Studio's bind), so an unrelated - # service on 8888 -- Jupyter et al. default to it -- is NOT killed; it's also gated on an - # Unsloth install having existed. /proc cmdline greps still work after kills since they - # read process state, not files. + # Probe candidates by exit code ('' = default distro) since `wsl --list` emits + # UTF-16 PS mis-parses. Kills run BEFORE rm so a live CUDA build (cmake/nvcc under + # /root/.unsloth/llama.cpp) can't recreate files after the rm. Each matched PID's + # whole process GROUP is signalled (cmake children carry relative argv), guarded + # against this shell's pgid, plus direct children via pkill -P; this shell can't + # self-match (extra backslash + [h]-bracket). Scope STRICTLY to /root; /home/* + # may be another user's. The 8888 kill only targets a listener whose cmdline is + # under /root/.unsloth, so an unrelated service on 8888 isn't killed, and is gated + # on an Unsloth install having existed. /proc greps still work after kills. $_clean = '_had=0; if [ -d /root/.unsloth ] || [ -L /root/.local/bin/unsloth ]; then _had=1; fi; _mypg=$(ps -o pgid= -p $$ 2>/dev/null | tr -d " "); for _p in $(pgrep -f ''/root/\.unslot[h]/'' 2>/dev/null); do _pg=$(ps -o pgid= -p $_p 2>/dev/null | tr -d " "); case "$_pg" in ""|0|1|"$_mypg") pkill -9 -P $_p 2>/dev/null; kill -9 $_p 2>/dev/null ;; *) kill -9 -- -$_pg 2>/dev/null || kill -9 $_p 2>/dev/null ;; esac; done; if [ $_had -eq 1 ]; then for _p in $(fuser 8888/tcp 2>/dev/null); do grep -qa /root/\.unsloth/ /proc/$_p/cmdline 2>/dev/null && kill -9 $_p 2>/dev/null; done; fi; rm -rf /root/.unsloth /root/llama-cuda /root/provision_llama_cuda.sh /root/llama_cuda_build.log 2>/dev/null; rm -f /root/.local/bin/unsloth 2>/dev/null; true' - # Clean only distros with evidence of a fallback install: the wsl-distro.txt marker or an - # explicit UNSLOTH_WSL_DISTRO. The broad candidate probe is only for legacy marker-less - # installs (ARM64 only); on x86 it would delete distros this installer never touched - # (e.g. a ROCm-on-WSL Studio under /root). + # Clean only distros with fallback-install evidence: wsl-distro.txt or an + # explicit UNSLOTH_WSL_DISTRO. The broad candidate probe is only for legacy + # marker-less installs (ARM64 only); on x86 it would delete distros this + # installer never touched (e.g. a ROCm-on-WSL Studio under /root). $_cands = @() if ($env:UNSLOTH_WSL_DISTRO) { $_cands += $env:UNSLOTH_WSL_DISTRO } if ($_recordedDistro) { $_cands += $_recordedDistro } @@ -653,8 +646,8 @@ function Uninstall-UnslothStudio { Write-Host " `$env:UNSLOTH_STUDIO_HOME = 'C:\your\path'; irm https://raw.githubusercontent.com/unslothai/unsloth/main/scripts/uninstall.ps1 | iex" } - # The distro probes leave a failing $LASTEXITCODE; reset so success exits 0. Set the var - # rather than `exit 0` so `irm ... | iex` doesn't terminate the caller's shell. + # The distro probes leave a failing $LASTEXITCODE; reset so success exits 0. Set the + # var rather than `exit 0` so `irm ... | iex` doesn't terminate the caller's shell. $global:LASTEXITCODE = 0 } diff --git a/scripts/uninstall.sh b/scripts/uninstall.sh index 3f889a855e..8b960f2dac 100755 --- a/scripts/uninstall.sh +++ b/scripts/uninstall.sh @@ -212,22 +212,17 @@ _custom_studio_roots | while IFS= read -r _custom_root; do _remove_path "$_custom_root" done _remove_path "$HOME/.unsloth/studio" -# Stop a detached CUDA llama.cpp build BEFORE deleting its tree: _pkill_studio -# only matches Studio roots, and a live cmake/nvcc under ~/.unsloth/llama.cpp -# would keep burning CPU/thermals, recreate build/ files between the rm and the -# rmdir, and leave a partial tree. TERM first, then KILL after the same grace -# _pkill_studio uses. +# Stop a detached CUDA llama.cpp build BEFORE deleting its tree: _pkill_studio only +# matches Studio roots, and a live cmake/nvcc under ~/.unsloth/llama.cpp would recreate +# build/ files between the rm and the rmdir. TERM first, then KILL after the same grace. if command -v pkill >/dev/null 2>&1; then _llama_re=$(_pkill_escape "$HOME/.unsloth/llama.cpp") - # Signal the whole process GROUP of each match, not just the matching PID: - # the provisioner cds into the tree before `cmake --build build`, so cmake/ - # make children carry relative argv that no pattern can match, and killing - # only the wrapper orphans them mid-build. Group kill sweeps the tree; PID - # kill remains the fallback when pgid is unreadable or shared with init. - # Never group-kill our own group: in a non-interactive session (no job - # control) a lingering provisioner can share the uninstaller's pgid, and - # kill(-pgid) would TERM this script and its caller mid-cleanup. Fall back - # to the PID plus its direct children in that case. + # Signal the whole process GROUP of each match: the provisioner cds into the tree + # before `cmake --build build`, so cmake/make children carry relative argv no pattern + # matches, and killing only the wrapper orphans them. PID kill is the fallback when + # pgid is unreadable or shared. Never group-kill our own group: a lingering provisioner + # in a non-interactive session can share our pgid, and kill(-pgid) would TERM this + # script mid-cleanup; fall back to the PID plus its direct children then. _self_pgid=$(ps -o pgid= -p $$ 2>/dev/null | tr -d '[:space:]') _kill_llama_build() { _sig="$1" @@ -354,10 +349,9 @@ case "$_os" in } } # Remove the WoA WSL-fallback native shim/launcher dir - # (%LOCALAPPDATA%\Unsloth) + its PATH entry that install.ps1 - # created, so a WSL-side bash uninstall is complete. Only when THIS - # distro owns the fallback (wsl-distro.txt) -- else uninstalling a - # different distro would break the still-installed shim. + # (%LOCALAPPDATA%\Unsloth) + its PATH entry, so a WSL-side bash uninstall + # is complete. Only when THIS distro owns the fallback (wsl-distro.txt), + # else uninstalling a different distro breaks the still-installed shim. $ud = if ($env:LOCALAPPDATA) { Join-Path $env:LOCALAPPDATA "Unsloth" } else { $null }; $owner = $null; if ($ud) { $of = Join-Path $ud "wsl-distro.txt"; if (Test-Path -LiteralPath $of) { $owner = (Get-Content -LiteralPath $of | Select-Object -First 1).Trim() } } @@ -365,9 +359,9 @@ case "$_os" in $shim = (Join-Path $ud "bin").TrimEnd("\","/"); $up = [Environment]::GetEnvironmentVariable("Path","User"); if ($up) { [Environment]::SetEnvironmentVariable("Path", (($up -split ";" | Where-Object { $_ -and ($_.TrimEnd("\","/") -ine $shim) }) -join ";"), "User") } - # The WoA-fallback shortcuts target powershell.exe + launch-studio-wsl.ps1 - # (not wsl.exe), so the sweep above keeps them; remove them here before - # their launcher dir is deleted or they would dangle. + # WoA-fallback shortcuts target powershell.exe + launch-studio-wsl.ps1 + # (not wsl.exe), so the sweep above keeps them; remove them here + # before their launcher dir is deleted or they would dangle. foreach ($d in $dirs) { if (-not $d -or -not (Test-Path -LiteralPath $d)) { continue } $l = Join-Path $d "Unsloth Studio.lnk"; @@ -392,9 +386,9 @@ case "$_os" in if ((-not $iconInUse) -and (Test-Path -LiteralPath $ico)) { Remove-Item -LiteralPath $ico -Force -ErrorAction SilentlyContinue } if ((Test-Path -LiteralPath $iconDir) -and -not (Get-ChildItem -LiteralPath $iconDir -Force -ErrorAction SilentlyContinue)) { Remove-Item -LiteralPath $iconDir -Recurse -Force -ErrorAction SilentlyContinue } } - # install.sh also writes the WSL shortcut icon to the Windows - # profile (%USERPROFILE%\.unsloth\unsloth.ico) because the WoA - # icon broker cannot read AppData\Local; clean it the same way. + # install.sh also writes the WSL shortcut icon to the Windows profile + # (%USERPROFILE%\.unsloth\unsloth.ico) since the WoA icon broker cannot + # read AppData\Local; clean it the same way. if (-not [string]::IsNullOrWhiteSpace($env:USERPROFILE)) { $pIconDir = Join-Path $env:USERPROFILE ".unsloth"; $pIco = Join-Path $pIconDir "unsloth.ico"; diff --git a/studio/backend/tests/test_gguf_load_cache_reuse.py b/studio/backend/tests/test_gguf_load_cache_reuse.py index 62596fcc8a..2452af7b31 100644 --- a/studio/backend/tests/test_gguf_load_cache_reuse.py +++ b/studio/backend/tests/test_gguf_load_cache_reuse.py @@ -728,11 +728,10 @@ class TestLoadHubDownloadExclusion: source = (Path(__file__).resolve().parent.parent / "routes" / "inference.py").read_text() gguf_branch = source[source.index("if config.is_gguf:") :] - # The gguf_load_in_flight marker must be entered before the hub-download - # guard and the unload so a concurrent load can't race the download - # manager. The llama_extra_args inheritance that used to sit between the - # marker and the guard now runs in _guard_chat_load_against_training, ahead - # of the GGUF branch, so it is no longer a landmark inside this slice. + # The gguf_load_in_flight marker must be entered before the hub-download guard + # and the unload so a concurrent load can't race the download manager. The + # llama_extra_args inheritance that used to sit between them now runs in + # _guard_chat_load_against_training, ahead of the GGUF branch, so it's gone here. assert ( gguf_branch.index("enter_context(gguf_load_in_flight") < gguf_branch.index("_hub_download_blocks_gguf_load") diff --git a/studio/backend/tests/test_llama_cpp_update.py b/studio/backend/tests/test_llama_cpp_update.py index d52b746149..3cbccd4311 100644 --- a/studio/backend/tests/test_llama_cpp_update.py +++ b/studio/backend/tests/test_llama_cpp_update.py @@ -116,11 +116,10 @@ def _clean_state(monkeypatch, tmp_path): monkeypatch.delenv("UNSLOTH_LLAMA_CPP_PATH", raising = False) # Never hit the network in these tests. monkeypatch.setattr(freshness, "_fetch_latest_release_tag", lambda repo, timeout = 5.0: None) - # Default to no live inference backend: on a fully-installed host - # `from routes.inference import get_llama_cpp_backend` (inside _run_update) - # imports a real Studio singleton and blocks on its load lock, making the - # worker tests hang/flake by host. This is the CI/fail-open path; the - # load-coordination tests inject their own backend over this default. + # Default to no live inference backend: on a fully-installed host _run_update's + # `from routes.inference import get_llama_cpp_backend` imports a real Studio + # singleton and blocks on its load lock, hanging/flaking the worker tests. This is + # the fail-open path; load-coordination tests inject their own backend over it. _routes_pkg = ModuleType("routes") _routes_pkg.__path__ = [] _inference_mod = ModuleType("routes.inference") diff --git a/studio/install_llama_prebuilt.py b/studio/install_llama_prebuilt.py index 191d4d014e..89feca1fc3 100644 --- a/studio/install_llama_prebuilt.py +++ b/studio/install_llama_prebuilt.py @@ -3025,9 +3025,9 @@ def detect_host() -> HostInfo: nvidia_smi = shutil.which("nvidia-smi") if not nvidia_smi: - # Root WSL sessions drop /usr/lib/wsl/lib (the only nvidia-smi home - # under WSL2 GPU-PV) from PATH, which would misroute an ARM NVIDIA WSL - # host to the CPU prebuilt; mirror setup.sh's resolver order. + # Root WSL sessions drop /usr/lib/wsl/lib (the only nvidia-smi home under + # WSL2 GPU-PV) from PATH, misrouting an ARM NVIDIA WSL host to the CPU + # prebuilt; mirror setup.sh's resolver order. for _cand in ("/usr/lib/wsl/lib/nvidia-smi", "/usr/bin/nvidia-smi"): if os.access(_cand, os.X_OK): nvidia_smi = _cand diff --git a/studio/setup.sh b/studio/setup.sh index afb0f217f0..e30d58bc23 100755 --- a/studio/setup.sh +++ b/studio/setup.sh @@ -275,10 +275,9 @@ _setup_cvd_hides_nvidia() { # via CUDA_VISIBLE_DEVICES=""/-1 counts as NOT usable (matches # install_llama_prebuilt.py has_usable_nvidia), so the AMD probes still run # and a mixed host steered to its AMD card keeps the ROCm route. -# nvidia-smi resolver: on WSL2 GPU-PV the binary lives ONLY in -# /usr/lib/wsl/lib, which root login shells drop from PATH (/etc/profile -# resets it), and /proc/driver/nvidia is not populated under the dxg driver -- -# so bare `command -v nvidia-smi` misses real GPUs on the flagship WoA path. +# nvidia-smi resolver: on WSL2 GPU-PV the binary lives ONLY in /usr/lib/wsl/lib, +# which root login shells drop from PATH, so bare `command -v nvidia-smi` misses +# real GPUs on the flagship WoA path. _resolve_nvsmi() { command -v nvidia-smi 2>/dev/null && return 0 [ -x /usr/lib/wsl/lib/nvidia-smi ] && { echo /usr/lib/wsl/lib/nvidia-smi; return 0; } @@ -1191,9 +1190,9 @@ LLAMA_CPP_DIR="$UNSLOTH_HOME/llama.cpp" LLAMA_SERVER_BIN="$LLAMA_CPP_DIR/build/bin/llama-server" _NEED_LLAMA_SOURCE_BUILD=false _LLAMA_CPP_DEGRADED=false -# Deferred != degraded: on WSL2 aarch64+NVIDIA install.ps1 builds the CUDA server -# in the background, so an absent server is success -- must not trip the arm64 -# CPU-prebuilt last-resort or exit 1. +# Deferred != degraded: on WSL2 aarch64+NVIDIA install.ps1 builds the CUDA server in +# the background, so an absent server is success -- must not trip the arm64 CPU-prebuilt +# last-resort or exit 1. _LLAMA_CPP_DEFERRED=false _LLAMA_FORCE_COMPILE="${UNSLOTH_LLAMA_FORCE_COMPILE:-0}" _REQUESTED_LLAMA_TAG="${UNSLOTH_LLAMA_TAG:-${_DEFAULT_LLAMA_TAG}}" @@ -1430,13 +1429,11 @@ if [ "$_NEED_LLAMA_SOURCE_BUILD" = true ] && \ fi # ── WSL2 aarch64 + NVIDIA, no nvcc yet: defer to the background CUDA build ── -# install.ps1 builds the CUDA llama-server in the background AND signals that by -# exporting UNSLOTH_WSL_LLAMA_DEFERRED=1 into this install. ONLY defer when that -# flag is set: a direct in-WSL `unsloth studio update` has no background builder, -# so deferring there would report "CUDA build running in background" while nothing -# builds -- hiding a no-server state. Without the flag we fall through to section 9 -# (a slow CPU server, still better than a phantom background build). With nvcc we -# fall through too; opted out (UNSLOTH_NO_LLAMA_CUDA=1) the CPU build is the only server. +# install.ps1 builds the CUDA llama-server in the background and signals it via +# UNSLOTH_WSL_LLAMA_DEFERRED=1. ONLY defer when that flag is set: a direct in-WSL +# `unsloth studio update` has no background builder, so deferring there would claim +# "CUDA build running in background" while nothing builds. Without the flag (or with +# nvcc) we fall through to a slow CPU server; opted out the CPU build is the only server. if [ "$_NEED_LLAMA_SOURCE_BUILD" = true ] \ && [ "${UNSLOTH_WSL_LLAMA_DEFERRED:-0}" = "1" ] \ && [ "$_LLAMA_FORCE_COMPILE" != "1" ] \ @@ -1457,10 +1454,9 @@ if [ "$_NEED_LLAMA_SOURCE_BUILD" = true ] \ fi # ── Native Linux aarch64 + NVIDIA, no nvcc yet: skip the CPU build too ── -# The aarch64+NVIDIA provision block below installs the CUDA toolkit and does -# the only build this host needs; a CPU source build first would burn minutes -# (and thermal headroom on Spark-class machines) on a binary the CUDA rebuild -# replaces in the same run. Provision failure still cascades to the +# The provision block below installs the CUDA toolkit and does the only build this host +# needs; a CPU source build first would burn minutes (and thermal headroom on Spark-class +# machines) on a binary the CUDA rebuild replaces. Provision failure still cascades to the # CPU-prebuilt last resort via _LLAMA_CPP_DEGRADED, so no-server states surface. if [ "$_NEED_LLAMA_SOURCE_BUILD" = true ] \ && [ "$_LLAMA_FORCE_COMPILE" != "1" ] \ @@ -1749,10 +1745,9 @@ else fi if [ "$_CUDA_TOOLKIT_ALLOWED" = true ]; then - # glibc >= 2.41 + CUDA < 13.3: rsqrt/rsqrtf header clash fails - # every .cu -> CPU fallback; only fix is CUDA >= 13.3. Diagnostic - # only (never changes flags or aborts), against the final _NVCC_VER - # (after the driver-compat swap above). + # glibc >= 2.41 + CUDA < 13.3: rsqrt/rsqrtf header clash fails every + # .cu -> CPU fallback; only fix is CUDA >= 13.3. Diagnostic only (never + # changes flags), against the final _NVCC_VER (post driver-compat swap). _GLIBC_VER="$(getconf GNU_LIBC_VERSION 2>/dev/null | awk '{print $2}')" || _GLIBC_VER="" if [ -n "$_GLIBC_VER" ]; then _GLIBC_MAJ="${_GLIBC_VER%%.*}"; _GLIBC_MIN="${_GLIBC_VER#*.}"; _GLIBC_MIN="${_GLIBC_MIN%%.*}" @@ -1891,15 +1886,12 @@ else substep "$_BUILD_DESC..." NCPU=$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4) - # Thermal cap for the aarch64 + NVIDIA foreground CUDA build. A full - # -j(nproc) nvcc compile draws enough sustained power to trip a - # thermal shutdown on the lightly-cooled NVIDIA-ARM boxes this path - # targets (DGX Spark / GB10, N1X "RTX Spark" laptops) -- the same - # reason provision_llama_cuda.sh caps its background build. Mirror - # that cap here (this foreground build only runs when no prebuilt was - # available and a CUDA toolkit is already present): ~half the cores, - # also bounded by ~1.5 GB/nvcc job. Other platforms keep full - # -j(nproc); override on any host with UNSLOTH_LLAMA_BUILD_JOBS=N. + # Thermal cap for the aarch64 + NVIDIA foreground CUDA build. A full -j(nproc) + # nvcc compile can trip a thermal shutdown on the lightly-cooled NVIDIA-ARM boxes + # this targets (DGX Spark / GB10, N1X "RTX Spark") -- same reason + # provision_llama_cuda.sh caps its background build. Mirror it here: ~half the + # cores, also bounded by ~1.5 GB/nvcc job. Other platforms keep full -j(nproc); + # override anywhere with UNSLOTH_LLAMA_BUILD_JOBS=N. if { [ "$_HOST_MACHINE" = "aarch64" ] || [ "$_HOST_MACHINE" = "arm64" ]; } \ && [ "${GPU_BACKEND:-}" = "cuda" ]; then if [ -n "${UNSLOTH_LLAMA_BUILD_JOBS:-}" ] && [ "${UNSLOTH_LLAMA_BUILD_JOBS}" -ge 1 ] 2>/dev/null; then @@ -2013,20 +2005,17 @@ fi # end _SKIP_GGUF_BUILD check # ── aarch64 + NVIDIA (DGX Spark / GB10 / N1X "RTX Spark"): provision a CUDA # llama.cpp when the source build above could not (no CUDA toolkit found) ── -# No aarch64+CUDA prebuilt exists and a fresh Spark ships only driver + nvidia-smi, -# so the build above fell back to CPU; mirror the Windows/WSL fix -# (provision_llama_cuda.sh) for native Linux. Best-effort: provision always exits -# 0, and on failure the prior CPU/degraded state stands. +# No aarch64+CUDA prebuilt exists and a fresh Spark ships only driver + nvidia-smi, so the +# build above fell back to CPU; mirror the Windows/WSL fix (provision_llama_cuda.sh) for +# native Linux. Best-effort: on failure the prior CPU/degraded state stands. # CUDA detection covers both layouts: monolithic (libggml-cuda in ldd) and split -# (dlopen-ed libggml-cuda.so* beside the binary, missed by ldd). Its presence is -# the signal -- CPU-only builds ship no libggml-cuda.so. +# (dlopen-ed libggml-cuda.so* beside the binary, missed by ldd). CPU-only builds ship none. _have_cuda_llama_server() { [ -x "$LLAMA_SERVER_BIN" ] || return 1 ldd "$LLAMA_SERVER_BIN" 2>/dev/null | grep -qi 'libggml-cuda' && return 0 - # Split-.so builds on this path come from provision_llama_cuda.sh, which stamps - # .unsloth-cuda-ok only after its final CUDA check. Requiring the stamp keeps - # the interrupted-relink state (new .so + old CPU server) provisioning instead - # of being reported as ready. + # Split-.so builds here come from provision_llama_cuda.sh, which stamps + # .unsloth-cuda-ok only after its final CUDA check. Requiring the stamp keeps an + # interrupted-relink state (new .so + old CPU server) provisioning, not "ready". _stamp="$(dirname "$LLAMA_SERVER_BIN")/.unsloth-cuda-ok" for _so in "$(dirname "$LLAMA_SERVER_BIN")"/libggml-cuda.so*; do [ -e "$_so" ] && [ -e "$_stamp" ] && return 0 @@ -2044,10 +2033,9 @@ if [ "$_HOST_SYSTEM" = "Linux" ] \ && [ "$_LOCAL_LLAMA_CPP_LINKED" != true ] \ && ! _have_cuda_llama_server; then # Under WSL this runs ONLY for a DIRECT `install.sh` run: install.ps1 sets - # UNSLOTH_WSL_LLAMA_DEFERRED=1 and builds in the background; a direct run has - # no background builder, so provision here. - # Resolve provision_llama_cuda.sh: beside setup.sh, then local-dev repo, else - # fetch from GitHub (so `curl | sh` works on an older wheel without it). + # UNSLOTH_WSL_LLAMA_DEFERRED=1 and builds in the background; a direct run has none. + # Resolve provision_llama_cuda.sh: beside setup.sh, then local-dev repo, else fetch + # from GitHub (so `curl | sh` works on an older wheel without it). _PROV_SH="" if [ -f "$SCRIPT_DIR/scripts/provision_llama_cuda.sh" ]; then _PROV_SH="$SCRIPT_DIR/scripts/provision_llama_cuda.sh" @@ -2069,7 +2057,7 @@ if [ "$_HOST_SYSTEM" = "Linux" ] \ step "llama.cpp" "CUDA llama-server ready (aarch64 + NVIDIA)" _LLAMA_CPP_DEGRADED=false # Claim ownership of the fresh $LLAMA_CPP_DIR, else the next custom-STUDIO_HOME - # run's _assert_studio_owned_or_absent aborts on it. + # run's _assert_studio_owned_or_absent aborts. if [ "$_STUDIO_HOME_IS_CUSTOM" = true ]; then : > "$LLAMA_CPP_DIR/$_STUDIO_OWNED_MARKER" 2>/dev/null || true fi @@ -2077,15 +2065,14 @@ if [ "$_HOST_SYSTEM" = "Linux" ] \ substep "CUDA build unavailable; keeping existing (CPU) llama-server" "$C_WARN" else substep "CUDA build unavailable and no llama-server present; see $LLAMA_CPP_DIR build output" "$C_WARN" - # No server at all: mark degraded so the arm64 CPU-prebuilt last resort - # and the failure exit fire instead of reporting a working install. + # No server at all: mark degraded so the arm64 CPU-prebuilt last resort and + # the failure exit fire instead of reporting a working install. _LLAMA_CPP_DEGRADED=true fi else - # Provisioner unreachable (not packaged and the GitHub fetch failed). The - # native deferral above may have skipped the CPU source build expecting - # this block to build; without a server that must surface as degraded so - # the CPU-prebuilt last resort fires instead of reporting success. + # Provisioner unreachable (not packaged and the GitHub fetch failed). The native + # deferral above may have skipped the CPU source build expecting this block; without + # a server, surface degraded so the CPU-prebuilt last resort fires, not success. substep "CUDA provision script unavailable (offline?); cannot build CUDA llama.cpp" "$C_WARN" [ -f "$LLAMA_SERVER_BIN" ] || _LLAMA_CPP_DEGRADED=true fi @@ -2169,13 +2156,11 @@ else fi echo "" -# Core install (venv + torch + Studio deps) is complete here; only the -# optional llama.cpp engine can still be missing. Stamp that fact BEFORE the -# tolerated nonzero exit below: install.ps1's WSL fallback cannot tell that -# exit apart from a real mid-install failure at the process level, so it -# removes this file before the run and requires it to exist afterwards -- -# otherwise its torch/CLI probes can pass on a stale venv from a previous -# install. Removed by scripts/uninstall.sh with the rest of ~/.unsloth. +# Core install (venv + torch + Studio deps) is complete here; only the optional llama.cpp +# engine can still be missing. Stamp that BEFORE the tolerated nonzero exit below: +# install.ps1's WSL fallback can't tell that exit from a real mid-install failure, so it +# removes this file before the run and requires it afterwards -- else its torch/CLI probes +# could pass on a stale venv. Removed by scripts/uninstall.sh with the rest of ~/.unsloth. mkdir -p "$HOME/.unsloth" 2>/dev/null || true : > "$HOME/.unsloth/.install-ok" 2>/dev/null || true diff --git a/tests/studio/install/test_gpu_detection_followups.py b/tests/studio/install/test_gpu_detection_followups.py index 5c1339d556..aae11a305c 100644 --- a/tests/studio/install/test_gpu_detection_followups.py +++ b/tests/studio/install/test_gpu_detection_followups.py @@ -265,9 +265,8 @@ class TestSetupShHardening: assert wrapped, "compute_cap probe must be wrapped in _setup_run_smi (timeout-bounded)" def test_driver_version_probe_timeout_wrapped(self, setup_src): - # The probe resolves nvidia-smi explicitly (root WSL shells drop - # /usr/lib/wsl/lib from PATH) and must still go through the timeout - # wrapper with the resolved path. + # The probe resolves nvidia-smi explicitly (root WSL shells drop /usr/lib/wsl/lib + # from PATH) and must still go through the timeout wrapper with the resolved path. start = setup_src.find("_cuda_driver_max_version()") end = setup_src.find("\n}", start) body = setup_src[start:end] From 8d3735eddc294709afd60a9d4426abc988e0a055 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Sun, 26 Jul 2026 11:27:28 +0000 Subject: [PATCH 95/95] provision: treat CUDA 13.0-13.2 as stale on glibc 2.41 and newer The staleness gate compared only the toolkit major, so a host with glibc >= 2.41, a CUDA 13.0/13.1/13.2 toolkit and a cu13-capable driver kept that toolkit and skipped the CUDA 13.3 provisioning. The build then hit the rsqrt header clash this script exists to avoid, and GGUF inference stayed on the CPU server. The gate now parses the toolkit minor and flags a 13.0-13.2 toolkit when the detected glibc is 2.41 or newer (getconf first, ldd as a fallback; an unparseable version keeps the previous major-only behavior). The driver check still applies, so a host whose driver cannot run cu13 is never pushed onto a 13.3 install. Verified over the (toolkit release) x (glibc) x (driver major) matrix with mocked nvcc output: 13.0/13.1/13.2 on glibc >= 2.41 are stale; 13.3, 13.4 and a two-digit 13.10 are kept; the same toolkits on older glibc are kept; pre-13 stays stale on any glibc; and nothing is flagged when the driver reports CUDA 12.x. Version parsing verified for 2.39, 2.41, 2.42, 3.0, empty and garbage inputs. --- studio/scripts/provision_llama_cuda.sh | 29 ++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/studio/scripts/provision_llama_cuda.sh b/studio/scripts/provision_llama_cuda.sh index 102c431cdd..2d51826ac9 100644 --- a/studio/scripts/provision_llama_cuda.sh +++ b/studio/scripts/provision_llama_cuda.sh @@ -104,6 +104,27 @@ find_nvcc() { _DRV_CUDA_MAJOR="$("$NVSMI" 2>/dev/null | sed -n 's/.*CUDA Version: *\([0-9][0-9]*\)\..*/\1/p' | head -1)" case "$_DRV_CUDA_MAJOR" in *[!0-9]*) _DRV_CUDA_MAJOR="" ;; esac _nvcc_major_of() { "$1" --version 2>/dev/null | sed -n 's/.*release \([0-9][0-9]*\)\..*/\1/p' | head -1; } +_nvcc_minor_of() { "$1" --version 2>/dev/null | sed -n 's/.*release [0-9][0-9]*\.\([0-9][0-9]*\).*/\1/p' | head -1; } + +# glibc >= 2.41 is the host side of the rsqrt header clash that CUDA 13.3 fixes, +# so a 13.0-13.2 toolkit is as unusable here as a pre-13 one. getconf first +# (no ldd on musl-ish images); unparseable stays 0 and keeps the major-only gate. +_glibc_ge_241=0 +_glibc_ver="$(getconf GNU_LIBC_VERSION 2>/dev/null | awk '{print $NF}')" +[ -n "$_glibc_ver" ] || _glibc_ver="$(ldd --version 2>/dev/null | head -1 | awk '{print $NF}')" +case "$_glibc_ver" in + [0-9]*.[0-9]*) + _glibc_major="${_glibc_ver%%.*}" + _glibc_minor="${_glibc_ver#*.}"; _glibc_minor="${_glibc_minor%%.*}" + case "$_glibc_major$_glibc_minor" in + *[!0-9]*) ;; + *) if [ "$_glibc_major" -gt 2 ] 2>/dev/null || \ + { [ "$_glibc_major" -eq 2 ] && [ "$_glibc_minor" -ge 41 ]; } 2>/dev/null; then + _glibc_ge_241=1 + fi ;; + esac + ;; +esac NVCC="$(find_nvcc)" # A CUDA < 13 toolkit cannot build for the sm_121 Spark class (and CUDA < 13.3 @@ -116,10 +137,18 @@ NVCC="$(find_nvcc)" _nvcc_stale=0 if [ -n "$NVCC" ]; then _nvcc_major="$(_nvcc_major_of "$NVCC")" + _nvcc_minor="$(_nvcc_minor_of "$NVCC")" if [ -n "$_nvcc_major" ] && [ "$_nvcc_major" -lt 13 ] 2>/dev/null \ && [ -n "$_DRV_CUDA_MAJOR" ] && [ "$_DRV_CUDA_MAJOR" -ge 13 ]; then log "existing CUDA $_nvcc_major toolkit ($NVCC) predates this machine class; provisioning CUDA 13.3 alongside it" _nvcc_stale=1 + elif [ "$_glibc_ge_241" -eq 1 ] && [ -n "$_nvcc_major" ] && [ "$_nvcc_major" -eq 13 ] 2>/dev/null \ + && [ -n "$_nvcc_minor" ] && [ "$_nvcc_minor" -lt 3 ] 2>/dev/null \ + && [ -n "$_DRV_CUDA_MAJOR" ] && [ "$_DRV_CUDA_MAJOR" -ge 13 ]; then + # 13.0-13.2 compiles for sm_121 but hits the rsqrt clash on glibc >= 2.41, + # so the build fails and GGUF inference stays on the CPU server. + log "existing CUDA $_nvcc_major.$_nvcc_minor toolkit ($NVCC) hits the glibc $_glibc_ver rsqrt clash; provisioning CUDA 13.3 alongside it" + _nvcc_stale=1 fi fi if [ -z "$NVCC" ] && [ -n "$_DRV_CUDA_MAJOR" ] && [ "$_DRV_CUDA_MAJOR" -lt 13 ]; then