docs: tighten PR comments/docstrings (no code change; AST + non-comment-line verified)

This commit is contained in:
Daniel Han 2026-06-11 22:05:05 -07:00
commit 8f0b5e78da
10 changed files with 143 additions and 153 deletions

View file

@ -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 <str>`.
$_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 }

View file

@ -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 \

View file

@ -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
}

View file

@ -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("\","/");

View file

@ -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:

View file

@ -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

View file

@ -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"

View file

@ -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"

View file

@ -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

View file

@ -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():