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]