Compare commits

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

119 commits

Author SHA1 Message Date
Daniel Han
7bffe91e54 Merge remote-tracking branch 'origin/main' into r5945 2026-07-27 13:20:24 +00:00
Daniel Han
bba55af8aa Merge branch 'main' into woa-nvidia-wsl-fallback
Resolve three conflicts, keeping both sides in each case:

- studio/backend/tests/test_llama_cpp_update.py: the _clean_state fixture
  keeps this branch's routes.inference stub (so _run_update never blocks on a
  real Studio backend singleton) alongside main's _whisper_chain_status stub.
- studio/setup.sh: keep both llama.cpp state flags. This branch's
  _LLAMA_CPP_DEFERRED (WSL2 aarch64 background CUDA build is success, not
  degraded) and main's _LLAMA_CPP_NO_SPACE are independent and both are read
  later in the file.
- unsloth/models/_utils.py: keep the DGX Spark / N1X UMA helpers and their
  four patch calls, then main's patch_unified_memory_safetensors_load. The
  Spark block stays first so patch_dgx_spark_memory_config still sets
  PYTORCH_CUDA_ALLOC_CONF before anything can touch the allocator; the
  safetensors patch gates lazily inside its wrapper, so installing it after
  does not init CUDA.

Verified every line main added since the merge base is still present in
install.sh, install.ps1, studio/setup.sh and studio/install_llama_prebuilt.py,
and likewise for this branch's own additions.
2026-07-26 15:45:19 +00:00
Daniel Han
8d3735eddc provision: treat CUDA 13.0-13.2 as stale on glibc 2.41 and newer
The staleness gate compared only the toolkit major, so a host with
glibc >= 2.41, a CUDA 13.0/13.1/13.2 toolkit and a cu13-capable driver
kept that toolkit and skipped the CUDA 13.3 provisioning. The build then
hit the rsqrt header clash this script exists to avoid, and GGUF
inference stayed on the CPU server.

The gate now parses the toolkit minor and flags a 13.0-13.2 toolkit when
the detected glibc is 2.41 or newer (getconf first, ldd as a fallback;
an unparseable version keeps the previous major-only behavior). The
driver check still applies, so a host whose driver cannot run cu13 is
never pushed onto a 13.3 install.

Verified over the (toolkit release) x (glibc) x (driver major) matrix
with mocked nvcc output: 13.0/13.1/13.2 on glibc >= 2.41 are stale;
13.3, 13.4 and a two-digit 13.10 are kept; the same toolkits on older
glibc are kept; pre-13 stays stale on any glibc; and nothing is flagged
when the driver reports CUDA 12.x. Version parsing verified for 2.39,
2.41, 2.42, 3.0, empty and garbage inputs.
2026-07-26 11:27:28 +00:00
Daniel Han
ca641eb2da Merge remote-tracking branch 'origin/main' into r5945
# Conflicts:
#	studio/backend/tests/test_gguf_load_cache_reuse.py
2026-07-21 02:21:49 +00:00
Daniel Han
7b1ca46652 install: tighten comments in the WSL fallback paths 2026-07-20 05:22:20 +00:00
Daniel Han
51009eacbe tests: align the gguf order test with main
Main fixed the stale ordering assertion in PR 7252; adopting its
version verbatim removes this file from the branch diff entirely and
avoids a conflict on the next main merge. 32 tests pass.
2026-07-20 00:21:30 +00:00
Daniel Han
22e8e8f463 tests: anchor the inheritance order check on the call, not the definition
source.index("_resolve_inherited_extra_args(") matched the function
definition, which always precedes the endpoint, so the ordering
assertion was vacuously true. Anchoring on "= _resolve_inherited_
extra_args(" pins the first call site inside the load endpoint (line
4505), which is the statement whose position relative to the GGUF
branch the test is meant to guard. 32 tests pass.
2026-07-19 16:21:30 +00:00
Daniel Han
611183ebe0 tests: track the moved pass-through inheritance in the gguf order check
Main moved the llama_extra_args pass-through inheritance out of the
GGUF branch into _resolve_inherited_extra_args, which runs before it,
so the source-order assertion's "if request.llama_extra_args is None"
anchor no longer exists inside the branch and the check failed after
the main merge. The test now asserts the same property in the current
shape: inheritance before the GGUF branch (a carried --no-mmproj still
shapes the hub guard's companion requirement), and marker, hub guard,
unload in order within the branch. Full file passes (32 tests).
2026-07-19 15:34:44 +00:00
Daniel Han
661f73bf50 Merge remote-tracking branch 'origin/main' into r5945
# Conflicts:
#	install.sh
2026-07-19 13:21:33 +00:00
Daniel Han
2dc8b99803 install: four round-nine review fixes across installer and uninstallers
Ninth review round; each item reproduced before fixing.

The inner WSL install ran install.sh without /usr/lib/wsl/lib on PATH,
so its GPU detection (which checked PATH and /usr/bin only) could pick
CPU torch wheels on the exact Spark/N1X path this PR exists for, failing
the later torch.cuda probe. The forwarded env now appends
/usr/lib/wsl/lib to PATH (appended, so a PATH nvidia-smi still wins),
and install.sh's _has_usable_nvidia_gpu and torch-index _smi resolution
gained the same location fallback for direct WSL runs.

Three WSL failure paths in install.ps1 (WSL-not-installed deferral, the
download sentinel, and the final torch.cuda failure) set LASTEXITCODE
and returned, bypassing the round-eight Exit-InstallFailure fix, so
powershell -Command automation using the published pipe form still saw
success on those failures. All three now route through
Exit-InstallFailure, which restores the rolled-aside venv and fails the
process in every invocation mode.

The uninstall.ps1 WSL cleanup removed /root/.unsloth before killing and
matched only full argv, so cmake/nvcc children of a live CUDA build
(relative argv after the provisioner cds into the tree) survived the rm
and recreated files. The cleanup now signals each matched PID's whole
process group (guarded against the shell's own pgid, direct children
via pkill -P as fallback) before any rm; the /proc cmdline greps are
unaffected by kill order since they read process state, not files.

The round-eight same-group fallback called pkill -P without a guard;
under this script's set -e a matched provisioner with no children at
that instant (TERM pass already reaped them) aborted the whole
uninstaller before any cleanup. Now || true, like the kill beside it.
Reproduced in a dash sandbox with set -e: a childless matched PID
previously killed the harness, now dies cleanly while setsid-group and
same-group scenarios keep passing.

Verified: bash -n on both shell scripts, sh -n on the extracted WSL
clean snippet, PowerShell AST parse on both ps1 files, the three-
scenario kill sandbox, gpu-detection and installer-index pytest suites
pass, and the sh battery matches the branch baseline.
2026-07-18 16:08:38 +00:00
Daniel Han
07ccf2b233 install: reconcile branch internals with the repo test suite
Cross-platform staging CI surfaced five Repo tests (CPU) failures where
main's tests assert on script internals this branch legitimately
changed; each reconciled on its merits.

The WoA native-wheel probe still used uv's deprecated --index-url alias
that main's test suite now forbids in favor of --default-index (same
semantics, and --default-index is what overrides inherited uv index
defaults); the probe now matches the convention.

The CUDA provision gate spelled its linked-dir guard with the
:-false default form that main's prune-refactor test blacklists
file-wide. The variable is unconditionally initialized far above, so the
guard now uses the plain spelling with identical semantics. The variable
guard itself stays: unlike a symlink test, it also covers the
canonical-location reuse case where the linked dir is not a symlink.

The gpu-detection tests extract named shell functions into a sandbox,
so _setup_has_usable_nvidia_gpu's new _resolve_nvsmi dependency made the
sandboxed helper die on command-not-found and report not_usable for
usable cases; the extraction list now includes the resolver, and the
driver-version hardening assertion tracks the resolved-path spelling
while still requiring the timeout wrapper. Also hardened the resolver
assignment with an explicit empty fallback so a future non-condition
call site cannot trip set -e.

The staging run also showed the Mac Studio Update uninstall step dying
mid-run, consistent with the round-seven group kill signalling its own
process group; the round-eight self-pgid guard already fixes that and
this push carries it to CI.

Verified: the five failing tests pass locally at this head (the one
remaining local red, test_negative_control_no_tokenizers, fails
identically with these changes stashed and did not fail in CI), bash -n,
PowerShell AST parse, and the sh battery matches the branch baseline.
2026-07-18 14:53:31 +00:00
Daniel Han
7b7511fb5c install: three round-eight review fixes for exit status, kill safety, detection
Eighth review round; each item reproduced before fixing.

Exit-InstallFailure under irm-pipe-iex set LASTEXITCODE and returned, so
powershell -Command automation using the published pipe form exited 0 on
fatal installer errors (verified: a -Command run whose last call only
assigns LASTEXITCODE exits 0, while one that throws exits 1). The iex
branch now sets the var for callers that check it and then raises a
terminating error, matching the pre-existing throw behavior there:
interactive shells survive and print it, automation gets exit 1, and the
-File branch keeps carrying the specific code via exit.

The uninstall group kill could signal the uninstaller's own process
group: in a non-interactive session without job control a lingering
provisioner can share the script's pgid, and kill(-pgid) would TERM the
cleanup mid-run. The helper now compares each match's pgid against its
own and falls back to the PID plus its direct children in that case.
Both scenarios exercised in a sandbox: a setsid provisioner group still
dies whole, and a same-group provisioner dies without taking the
harness.

detect_host in install_llama_prebuilt.py resolved nvidia-smi only via
shutil.which, so the root WSL sessions this PR creates (PATH without
/usr/lib/wsl/lib) classified ARM NVIDIA WSL hosts as non-NVIDIA and took
the CPU prebuilt path before setup's provisioning logic could run. It
now falls back to /usr/lib/wsl/lib/nvidia-smi then /usr/bin/nvidia-smi,
the same order as setup.sh's resolver.

Verified: bash -n, Python AST parse, PowerShell AST parse, the pwsh
exit-code experiments above, the two-scenario kill sandbox, and the sh
test battery matches the branch baseline.
2026-07-18 14:36:08 +00:00
Daniel Han
9ca396b665 install: four round-seven review fixes across installer and uninstall
Seventh review round; each item reproduced against the live tree first.

The aarch64 bitsandbytes step gated on a bare nvidia-smi, which root login
shells cannot see under WSL2 GPU-PV (the binary lives only in
/usr/lib/wsl/lib, dropped from PATH by the /etc/profile reset), so Spark
and N1X WSL installs finished with CUDA torch but no 4-bit QLoRA. The
gate now resolves nvidia-smi explicitly with the same PATH,
/usr/lib/wsl/lib, /usr/bin order as setup.sh's resolver.

uninstall.sh's CUDA-build kill matched patterns against argv, but the
provisioner cds into the tree before `cmake --build build`, so cmake and
make children carry relative argv no pattern can match; killing only the
wrapper orphaned them mid-build. Each match's whole process group is now
signalled (TERM then KILL), with a plain PID kill as fallback when the
pgid is unreadable or shared with init. Verified in a sandbox: a child
with unmatchable argv in the wrapper's group dies with it.

The WSL shim dir was appended to user PATH while the native installer
prepends its own %USERPROFILE%\.unsloth\studio\bin, whose unsloth.exe
outlives the venv the fallback rolls aside, so on a native-to-WSL rerun
a new terminal resolved unsloth to the dead native launcher. The shim is
now prepended via Add-ToUserPath (which de-dupes and hoists), and the
dead default-root native shim is removed when the venv binary it targets
is gone; custom-root shims are left alone since the prepend outranks
them.

UNSLOTH_NPM_REGISTRY was not forwarded into the inner WSL shell even
though setup.sh threads it into every npm/bun install, so mirror-required
networks failed the frontend step (and with it the install) while the
outer installer honored the mirror. It is now forwarded with the same
strict http(s) allow-list and single-quoting as UNSLOTH_PYTORCH_MIRROR.

Verified: bash -n on both shell scripts, PowerShell AST parse on
install.ps1, the group-kill sandbox above, resolver smoke tests for the
bitsandbytes gate, and the sh test battery matches the branch baseline.
2026-07-18 13:59:55 +00:00
pre-commit-ci[bot]
6ea6c621c0 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-07-18 13:20:08 +00:00
Daniel Han
caff266689 Merge remote-tracking branch 'origin/main' into r5945
# Conflicts:
#	install.ps1
2026-07-18 13:19:07 +00:00
Daniel Han
0b6251c2af install: six round-six review fixes across installer, setup, uninstall
Sixth review round; every item reproduced against the live scripts first.

The WSL fallback tolerated a nonzero inner exit (the optional llama.cpp
step legitimately fails), so a run whose installer died mid-way could pass
the torch and CLI probes on a stale venv from a previous install and be
reported as success. setup.sh now stamps /root/.unsloth/.install-ok after
the core venv and Studio deps complete, just before its tolerated
llama-only nonzero exit; install.ps1 clears the stamp before the run and
requires it to exist afterwards (existence only, no mtime compare, so
WSL/Windows clock skew cannot bite). uninstall.sh removes the stamp and
the downloaded installer file so the trailing rmdir can still prune.

Root login shells reset PATH via /etc/profile and drop /usr/lib/wsl/lib,
the only location of nvidia-smi under WSL2 GPU-PV, so every bare
nvidia-smi probe in setup.sh and the provisioner could silently misreport
"no GPU". Both now resolve nvidia-smi explicitly (PATH, then
/usr/lib/wsl/lib, then /usr/bin) via a shared-resolver pattern, and the
provisioner's driver-major and compute_cap reads use the resolved path.

My round-five uninstall fix inserted the CUDA-build kill block after the
llama.cpp tree was already removed, so a live cmake/nvcc kept running
against deleted paths; the block now runs before the removal.

uninstall.ps1 gated its legacy marker-less WSL cleanup on the process
PROCESSOR_ARCHITECTURE, which reports AMD64 under an x64-emulated
PowerShell on ARM64, skipping exactly the machines the fallback installs
on. It now uses the same triple detection as install.ps1 (OSArchitecture,
Win32_Processor.Architecture 12, machine-level registry arch), factored
into one helper used at both gate sites.

The nvidia-smi capture helper retried twice with a 60s timeout
everywhere, so off WSL a hung nvidia-smi stalled three successive
detect_host probes for about two minutes each; the generous retry now
applies only under WSL (where GPU-PV load slowness is real) and bare
metal keeps a single short attempt.

The generated WSL Desktop launcher hardcoded port 8888 for launch, health
poll, and browser open, so with Jupyter or a second Studio on 8888 the
poll waited on the wrong server forever; it now scans 8888..8908 with a
TcpListener, mirroring the native launcher's free-port window, and passes
the winner via -p everywhere.

Verified: bash -n on all shell scripts, Python AST parse, PowerShell AST
parse on install.ps1, uninstall.ps1, and the generated launcher; the
launcher port scan exercised free, busy, and exhausted cases; the capture
helper unit-tested for WSL and bare-metal attempt/timeout splits; sh test
battery matches the branch baseline.
2026-07-18 13:06:13 +00:00
Daniel Han
af459f4673 install: six round-five review fixes across provisioner, setup, uninstall
Fifth review round; each item traced through the live scripts before fixing.

A provisioner fresh clone that failed to produce a server was left behind as
a markerless git tree; under a custom UNSLOTH_STUDIO_HOME the next run's
ownership assert refuses the unmarked dir and aborts the whole install until
the user deletes it by hand. _restore_prev now removes a clone this script
created when no server came out of it (backed-up dirs restore as before).

The CUDA provision gate ignored --with-llama-cpp-dir linked mode, so a
linked user tree with a CPU-only server could be checked out to a pinned
ref, rebuilt in place, or moved aside entirely and replaced by a fresh
clone. The gate now skips linked local dirs.

uninstall.sh removed the CUDA build artifacts without stopping a running
detached build; _pkill_studio only matches Studio roots, so live cmake/nvcc
kept burning thermals, recreated build files, and defeated the trailing
rmdir. The runner, provisioner, and llama.cpp-path processes now get
TERM-then-KILL with the same escape helper and grace the Studio kill uses.

The worker's memory-fraction guard classified Spark purely from device
props, so UNSLOTH_FORCE_DGX_SPARK=1 on an unlisted name got no fraction
guard (and the fraction env was dead), while FORCE=0 could not disable it;
the guard now honors the same force semantics as the detectors.

UNSLOTH_LLAMA_TAG / UNSLOTH_LLAMA_PR were interpolated into the runner
script's single-quoted exports unvalidated while every sibling forward has
an allow-list; they now get the INSTALL_REF ref allow-list and a digits-only
check respectively (own-machine robustness, not a trust boundary).

On WSL-fallback success with a custom UNSLOTH_STUDIO_HOME, the installer
deleted the rolled-aside custom-root venv right after telling the user that
root is not used by the WSL install; a custom root now restores the previous
venv instead (the WSL shim does not depend on the Windows venv), while the
default root keeps dropping the vestigial backup.

Verified: bash -n on all three shell scripts, AST parse on worker.py,
PowerShell AST parse on install.ps1, icon suites pass, sh battery matches
the branch baseline. Two resurfaced anchors (build/bin backup, --package
forwarding) confirmed already fixed at head.
2026-07-18 12:08:39 +00:00
Daniel Han
7faf0c7fb4 install: gate CUDA 13.3 on driver support, fail loudly on broken WSL installs
Fourth review round; each item verified against the live scripts and the CUDA
compatibility documentation before fixing.

The provisioner installed (and, since the stale-toolkit change, preferred)
CUDA 13.3 without ever consulting the driver, but cu13 binaries need a 580+
driver and minor-version compatibility never crosses majors, so a GH200-class
host on a 5xx driver got an unloadable llama-server that the structural
acceptance check then stamped as ready. The driver's supported CUDA major is
now read from nvidia-smi and enforced three ways: the stale-toolkit upgrade
only fires when the driver can run cu13, a fresh install on a sub-13 driver
bails to the existing no-toolkit message instead of installing 13.3, and a
final guard swaps a too-new selected toolkit for the newest one the driver
supports (or refuses to build). Spark-class hosts (580+ drivers) behave
exactly as before; unparseable output keeps the previous behavior.

The WSL install pipeline ended in curl | sh, so a failed download fed sh an
empty stdin and exited 0; on a rerun the stale venv then passed the torch
probe and the installer reported success without ever running. install.sh is
now downloaded to a file with exit 86 as the never-ran sentinel, checked
before any probe (rollback + non-zero). The --package splice moved onto the
file invocation.

When the Studio web-server dep repair failed its re-verify, the installer
still created shims and reported success; the missing set includes typer, so
even the plain unsloth CLI dies. A failed repair now routes to the existing
failure path (rollback + non-zero), mirroring the CLI-missing case.

If all three provision-script resolutions fail (unpackaged wheel + GitHub
unreachable), the provision block silently skipped and, with the CPU build
now deferred on native Spark hosts, the install could report success with no
GGUF server; that case is now marked degraded so the CPU-prebuilt last
resort and failure exit fire.

flex_attention.py's Spark sniff gets the same /usr/lib/wsl/lib/nvidia-smi
fallback as the other two detectors (grep confirms these are the only three),
and uninstall.sh removes the remaining WSL-side build artifacts
(run_llama_build.sh, llama_cuda_build.log, the shortcut-skip marker) so the
.unsloth directory can actually be removed.

Verified: bash -n on all three shell scripts, AST parse on flex_attention.py,
PowerShell AST parse on install.ps1, the toolkit-picker awk exercised against
a fake /usr/local tree (driver 12 picks cuda-12.8 over 13.0, driver 11 picks
none), icon suites pass, sh test battery matches the branch baseline.
2026-07-18 11:31:38 +00:00
Daniel Han
0675eb6c46 install: close four resurfaced WoA review gaps
The Spark detectors (library _is_dgx_spark_no_cuda_init and the worker's
pre-CUDA sniff) called bare nvidia-smi, but the WoA shim execs the venv
binary directly with no login shell, where /usr/lib/wsl/lib can be off PATH;
both now resolve WSL's nvidia-smi path explicitly when the bare name is not
found, so the allocator setup works on plain 'unsloth ...' launches.

An explicit UNSLOTH_PYTHON pin was lost across the WSL boundary (Windows env
vars do not cross into the distro), so the inner install.sh built the venv on
its default Python while the installer reported success; the pin is now
forwarded, gated on a strict X.Y[.Z] shape before splicing into bash -lc.

The WSL2 probe/conversion only ran for pre-existing distros; a fresh install
relied on wsl --set-default-version 2 succeeding silently and could proceed
on WSL1 all the way to the final torch.cuda failure. The probe and in-place
conversion now run for freshly installed distros too.

The fourth resurfaced item (complete Studio dependency repair set) is already
fixed at head: the repair list includes sqlite-vec, pymupdf, and python-docx.
2026-07-18 10:06:50 +00:00
Daniel Han
6c1b739c36 install: fix seven WoA/Spark review findings in provisioning and uninstall
Third review round; each item re-verified against the live scripts.

A stale CUDA < 13 toolkit was kept forever: the 13.3 install was gated on
nvcc being absent, so a host with CUDA 12.x failed the sm_121 configure (or
the glibc >= 2.41 rsqrt clash) on every rerun and always exited with the CPU
server. When apt can provide 13.3 the provisioner now installs it alongside a
stale toolkit; find_nvcc's sort -V prefers the new install, and a failed
install leaves the old toolkit as the last resort, so non-Spark hosts that
build fine on cu12x are unaffected.

llama.cpp pins only applied to fresh clones; an existing checkout rebuilt
whatever commit it had while the log claimed a release pin. Existing
checkouts now fetch and check out the pinned (or resolved-latest) ref, best
effort with the current commit as fallback, and the UNSLOTH_LLAMA_PR handling
moved out of the fresh-clone branch so it applies to both paths.

The WSL fallback silently dropped a non-default --package and reported
success with stock unsloth; it is now spliced into the curl | sh invocation
(the name is regex-validated at parse time).

setup.sh's CUDA provision gate used raw nvidia-smi and ignored the
_setup_nvidia_usable computation that honors CUDA_VISIBLE_DEVICES=""/-1, so a
mixed-GPU host that hid its NVIDIA card still got a system CUDA install; the
gate now requires the flag. On native Linux Spark hosts without nvcc, setup.sh
also no longer does the multi-minute CPU source build that the CUDA provision
in the same run immediately replaces (mirroring the existing WSL deferral
arm); provision failure still cascades to the CPU-prebuilt last resort.

uninstall.ps1's distro extraction truncated quoted names at the first space
(-d "Ubuntu Preview" matched as "Ubuntu"), wrongly keeping or removing
shortcuts; the regex now matches a full quoted token first. And the profile
icon (%USERPROFILE%\.unsloth\unsloth.ico) was removed unconditionally while
the sweep above deliberately keeps launchers for non-evidenced WSL installs,
blanking their icons; removal is now gated on no surviving Unsloth shortcut,
mirroring uninstall.sh's _drop_shared_icon_if_unused guard.

Verified: bash -n on both shell scripts, PowerShell AST parse on both ps1
files, the new distro regex proven on spaced and unspaced names, icon suites
pass, sh test battery matches the branch baseline.
2026-07-18 10:02:31 +00:00
Daniel Han
daf06e2c28 install: fix eight WoA/WSL review findings across probe, worker, provisioner
Second review round on the Windows-on-ARM + NVIDIA path; each item verified
against the live code (and torch where relevant) before fixing.

The native-CUDA probe ran uv --dry-run against the venv interpreter without
checking its architecture. uv resolves for the interpreter's platform tags, so
an x64-emulated python resolved existing win_amd64 CUDA wheels and "proved" a
native wheel WoA cannot use, skipping the WSL fallback entirely. The probe now
requires platform.machine() ARM64 from the venv python first; anything else
keeps the WSL routing.

The Studio worker appended PYTORCH_CUDA_ALLOC_CONF next to its memory-fraction
logic, 550 lines after detect_hardware() had already initialized CUDA, where
the allocator config is latched (verified on torch 2.9.1: expandable_segments
set after get_device_properties is a no-op in memory snapshots). The CUDA-free
Spark sniff now runs immediately before detect_hardware(), and it honors the
documented UNSLOTH_FORCE_DGX_SPARK=1/0 override the library detectors support,
closing the older force-flag item on the same block.

setup.sh's _have_cuda_llama_server accepted any co-located libggml-cuda.so,
re-opening the interrupted-relink hole the provisioner's completion stamp was
added to close: in exactly that state setup.sh skipped provisioning and
reported CUDA ready over the old CPU binary. The split-.so branch now also
requires the stamp; monolithic ldd-linked builds are unaffected.

The provisioner builds llama-quantize but never created the repo-root shim
that unsloth_zoo's check_llama_cpp needs (it only searches the root, which is
why setup.sh symlinks it in all three of its own paths). The success branch
now mirrors that symlink.

CMAKE_CUDA_ARCHITECTURES=native needs CMake >= 3.24, but this script installs
distro cmake (Ubuntu 22.04 apt ships 3.22), so the N/A-compute_cap fallback
aborted configure, wiped build/, and aborted again. The fallback now omits the
flag and lets ggml's version-guarded CMake defaults pick the arches.

Fresh clones tracked ggml-org master, bypassing setup.sh's newest-release pin
policy (its own header warns master bypasses the pin). An unset or "latest"
ref now resolves to the newest release tag via the GitHub API, keeping the
default-branch clone as the best-effort fallback when the API is unreachable.

install.sh writes the WSL shortcut icon to the Windows profile
(%USERPROFILE%\.unsloth\unsloth.ico) because the WoA icon broker cannot read
AppData\Local, but both uninstall.sh cleanup sites only cleaned the
AppData\Local icon. Both now clean the profile icon and drop the directory
when empty.

Verified: bash -n on all four shell scripts, AST parse on worker.py,
PowerShell AST parse on both ps1 files, the icon suites pass, and the sh test
battery matches the branch baseline (test_install_host_defaults.sh fails
identically on the clean tree).
2026-07-18 09:14:35 +00:00
Daniel Han
9ae5565803 install: close six WoA/WSL review gaps in provisioning, shortcuts, uninstall
Review round on the Windows-on-ARM + NVIDIA WSL2 path; each item reproduced
against the live scripts before fixing.

provision_llama_cuda.sh now serializes with install_llama_prebuilt.py on the
same <parent>/.<name>.install.lock file (its filelock backend is flock(2), so
shell flock interoperates; append-mode open so the Python O_EXCL fallback's
PID file is never truncated). The detached background builder could otherwise
race an installer rerun or `unsloth studio update`, both of which mv/rm -rf
inside the llama.cpp dir. Losing the 2h wait exits 0: another provisioner is
already doing the job.

The step-0 early-skip trusted a co-located libggml-cuda.so alone, which
wrongly skips one case: an in-place rebuild interrupted after the .so links
but before llama-server relinks leaves new .so + old CPU server. A completion
stamp (build/bin/.unsloth-cuda-ok) written only after the script's own final
CUDA check closes that window; skip now requires ldd evidence or the stamp.
The rejected functional --list-devices probe stays rejected: the stamp does
not gamble thermals on an env-fragile probe.

The WSL shortcut skip (install.ps1 owns the canonical WoA .lnk) was only a
transient env var, so the first `unsloth studio update`, whose wsl.exe shim
carries no env into install.sh --shortcuts-only, recreated the duplicate
blank-icon shortcut. The skip is now also persisted as
/root/.unsloth/.skip-wsl-windows-shortcut, checked by install.sh and removed
with the install by both uninstallers.

--with-llama-cpp-dir (and UNSLOTH_LOCAL_LLAMA_CPP_DIR) were parsed but
silently ignored on the WSL fallback path, which builds its own llama.cpp
inside the distro. Reject with guidance (UNSLOTH_LLAMA_TAG / UNSLOTH_LLAMA_PR
pin the WSL-side build), mirroring the --local reject.

uninstall.sh's Windows shortcut sweep only removed wsl.exe-target .lnks, so
the WoA fallback shortcuts (powershell.exe + launch-studio-wsl.ps1) survived
while their launcher dir was deleted, leaving dangling shortcuts. The
owner-matched cleanup now removes them first.

uninstall.ps1 swept every "Unsloth Studio (*.lnk" as legacy, but install.sh
creates exactly that per-distro name for current WSL installs, and the WSL
cleanup below only removes evidenced distros. The sweep now keeps a live
wsl.exe launcher whose distro is not in the same evidence set, so a surviving
WSL install keeps its shortcut; everything else is still swept.

Verified: bash -n on all three shell scripts, PowerShell AST parse on both
ps1 files, flock mutual-exclusion and stamp skip/rebuild decisions exercised
standalone, and the uninstall icon suites (sh + ps1) pass. The
test_install_host_defaults.sh failure pre-exists on the branch merge base.
2026-07-18 08:23:23 +00:00
Daniel Han
7efe4c7107 Merge remote-tracking branch 'origin/main' into merge/5945-main
# Conflicts:
#	pyproject.toml
#	scripts/uninstall.sh
#	studio/install_llama_prebuilt.py
#	studio/setup.sh
2026-07-12 01:52:13 -07:00
Daniel Han
bb2bd33943 fix: restore rolled-aside venv on WoA WSL-routing early rejects
On Windows-on-ARM + NVIDIA, an existing native Studio venv is rolled
aside (Start-StudioVenvRollback) before the WSL-routing block. The
TauriMode, --local, and invalid-UNSLOTH_INSTALL_REF rejects returned
without calling Restore-StudioVenvRollback, orphaning the user's
previous venv backup. Restore it on all three early exits, matching
the deferred-reboot / WSL1-conversion / final-failure paths that
already do. Restore-StudioVenvRollback no-ops when nothing was rolled
aside, so the fresh-install case is unaffected.

Addresses Codex review (venv-rollback ordering on the WoA reject paths).
2026-06-21 23:25:09 -07:00
Daniel Han
fa44cb8c44 fix: address Codex review on WoA deferral, uninstall port-kill, and build preservation
Three valid findings from the 06-22 Codex review:

1. provision_llama_cuda.sh: when $LLAMA_DIR holds a .git checkout (a prior CPU
   source build), the whole-dir backup was skipped, so a failed CUDA rebuild's
   'rm -rf build' destroyed the working CPU server with nothing to restore --
   leaving NO llama-server despite the 'keeps the existing server' promise (a
   thermal shutdown mid-build is a real failure mode on this hardware). Back up
   build/bin before the rebuild and restore it on total failure; idempotent and
   self-cleaning (never overwrites a freshly built server). Verified both paths.

2. uninstall.ps1: 'fuser -k 8888/tcp' killed ANY listener on 8888 (Jupyter et al.
   default to it), not just Studio. Now only kills a PID whose /proc/cmdline is
   under /root/.unsloth -- matching the adjacent pkill scoping.

3. setup.sh: the 'defer to background CUDA build' branch fired even on a direct
   in-WSL 'unsloth studio update', where install.ps1 never launched a background
   builder -- so the footer claimed a build was running while nothing built. Gate
   it on UNSLOTH_WSL_LLAMA_DEFERRED=1 (set only by install.ps1, and already read
   elsewhere in setup.sh); a direct run now falls through to a real CPU build.

bash -n + PS parse clean; the common install.ps1 WoA path (prebuilt success,
deferred flag set) is unaffected.
2026-06-21 22:24:41 -07:00
Daniel Han
75636a1909 fix(install.ps1): honor/guard --local, custom Studio root, and ref in WoA WSL fallback
Address three Codex P2s on the Windows-on-ARM + NVIDIA WSL2 fallback, all cases
where the branch silently ignored a Windows-side option while reporting success:

1. UNSLOTH_INSTALL_REF was spliced raw into the inner 'bash -lc' twice (an export
   and a GitHub raw URL); a ref with shell metacharacters (;, &, ', space) would
   break or inject the command. Validate against a strict git-ref allow-list
   (^[A-Za-z0-9][A-Za-z0-9._/-]*$) and reject loudly. Real git refs always pass.

2. --local (editable install of the Windows checkout) can't be honored by the WSL
   tunnel, which installs from PyPI/a git ref and never mounts $RepoRoot -- it would
   silently install the published package. Reject it up front and point at the
   supported pre-merge path (push the branch + UNSLOTH_INSTALL_REF).

3. A custom UNSLOTH_STUDIO_HOME / STUDIO_HOME only applies to the native Windows
   layout; the WoA install lives in WSL at /root/.unsloth. Warn clearly so the user
   isn't misled into thinking Studio landed at their custom path.

All three guard rare conditions; the default install path (no --local, default
root, normal branch/tag ref) is unaffected. install.ps1 parses clean; ref guard
verified against valid refs + metacharacter-injection cases.
2026-06-21 22:02:27 -07:00
Daniel Han
a862881f85
Merge branch 'main' into woa-nvidia-wsl-fallback 2026-06-21 22:00:07 -07:00
Daniel Han
f5aa0d75c4
Merge branch 'main' into woa-nvidia-wsl-fallback 2026-06-21 21:53:45 -07:00
Daniel Han
c34fd4e412 Merge remote-tracking branch 'origin/main' into woa-nvidia-wsl-fallback 2026-06-21 03:15:07 -07:00
Daniel Han
65ef0bfc16 revert(provision): drop the --list-devices step-0 probe (false thermal rebuilds)
The cuda_server_probe() added in 27bc44c gated the step-0 rebuild-skip on a
runtime 'llama-server --list-devices' check. In a real cold install on the N1X
this BACKFIRED: the background provision runs step-0 while the install is still
under heavy load (torch download, frontend build), and under WSL2 GPU-PV the
CUDA backend's init transiently fails under load (the same flakiness cycle-21
worked around for nvidia-smi). --list-devices then enumerated devices but no
CUDA, so the probe declared the freshly-validated PREBUILT 'broken', wiped it
(rm -rf build), and kicked off a CUDA-13.3 toolkit install + source build -- the
exact thermal-risk + wasted-prebuilt outcome cycle-21 eliminated. (Confirmed the
prebuilt is fine: --list-devices shows CUDA0 in a normal shell, even with
LD_LIBRARY_PATH stripped -- the probe failure was purely load-induced.)

Restore the load-insensitive structural check: a co-located libggml-cuda.so* is
trusted, because the prebuilt resolver validates what it installs and an
interrupted SOURCE build is already caught by the build-failure wipe+rebuild in
section 6. The Codex P2's half-linked-.so concern is real but narrow, and a
runtime probe that can gamble the machine's thermals on an env/load-fragile GPU
call is the wrong trade on this hardware.
2026-06-21 02:25:25 -07:00
Daniel Han
fbb2d9000f fix(uninstall.ps1): drop the empty ~/.unsloth left behind on WoA uninstall
The empty-dir sweep of ~/.unsloth ran before the WoA-fallback block removes
~/.unsloth\unsloth.ico, so on a Windows-on-ARM install the still-present icon
kept the dir non-empty at sweep time and it was skipped -- leaving an empty
~/.unsloth behind after a full uninstall. Re-attempt the empty-only removal
right after the icon is deleted (the last default-mode child). uninstall.sh is
unaffected: its rmdir runs as the final step.
2026-06-21 02:07:08 -07:00
Daniel Han
0a2b94de71 Merge remote-tracking branch 'origin/main' into woa-nvidia-wsl-fallback 2026-06-21 02:00:04 -07:00
Daniel Han
27bc44c460 fix(provision): functionally confirm CUDA before the step-0 rebuild-skip
is_cuda_server() treats a co-located libggml-cuda.so* as proof the server is
CUDA-ready. That's normally true (llama.cpp dlopens the backend from beside the
binary), but an *interrupted* build (thermal/power shutdown -- common on the
NVIDIA-ARM laptops this path targets) can leave a half-linked libggml-cuda.so
next to the server: present, so is_cuda_server() matches, yet the backend fails
to load at runtime. The post-build path already wipes+rebuilds such a partial
.so, but the step-0 early-skip trusted it and never rebuilt -- so Studio could
report GGUF CUDA inference ready while running a broken/non-CUDA backend.

Gate the early-skip with cuda_server_probe(): 'llama-server --list-devices'
enumerates backends and exits (cheap, no server spin-up). Only a definitive
'flag supported, ran, but no CUDA device' triggers a clean rebuild; a timeout or
an old pin without --list-devices stays inconclusive and keeps trusting the .so,
so we never force a needless, thermally-expensive rebuild. Probe logic verified
against healthy/broken/unsupported/timeout stubs (0/1/2/2).

Addresses Codex review P2 (provision_llama_cuda.sh).
2026-06-21 01:31:37 -07:00
Daniel Han
31bd20149c fix(install.ps1): forward UNSLOTH_PYTORCH_MIRROR into the WSL installer
The WoA+NVIDIA WSL2 fallback bridges UNSLOTH_NO_LLAMA_CUDA / UNSLOTH_PYTHON /
UNSLOTH_SKIP_WSL_WINDOWS_SHORTCUT into the distro, but not UNSLOTH_PYTORCH_MIRROR.
install.sh's get_torch_index_url() reads it (as does install.ps1's own native
Get-TorchIndexUrl), yet Windows env vars don't cross into WSL -- so a
mirror-required / restricted-network install silently fell back to
download.pytorch.org inside the distro even though the outer installer honored
the mirror.

Forward it alongside the other vars, guarded by a strict http(s)-URL allow-list
(no shell metacharacters) and single-quoted so the value can't break out of the
bash -lc string. Verified: legit mirror URLs (incl. host:port and query strings)
forward; space/';'/$()/quote-injection and non-http schemes are rejected.

Addresses Codex review P2 (install.ps1).
2026-06-21 01:31:27 -07:00
Daniel Han
14b7dd5ed3 fix(setup): thermal-cap the aarch64+NVIDIA foreground CUDA build
The foreground source build in setup.sh used -j(nproc), which on the
lightly-cooled NVIDIA-ARM boxes this WoA/WSL path targets (DGX Spark /
GB10, N1X RTX Spark laptops) draws enough sustained power during the
nvcc compile to trip a thermal shutdown -- the exact reason
provision_llama_cuda.sh already caps its background build.

Mirror that cap for the foreground build (only reached when no prebuilt
llama.cpp was available and a CUDA toolkit is present): gate on
aarch64/arm64 + GPU_BACKEND=cuda, then use ~half the cores, also bounded
by ~1.5 GB/nvcc job. Other platforms and CPU builds keep full -j(nproc).
Override anywhere with UNSLOTH_LLAMA_BUILD_JOBS=N.

Verified: nproc=20/29GB box -> -j10; override=6 -> -j6; CPU build and
x86_64 stay uncapped.
2026-06-21 01:18:07 -07:00
Daniel Han
c1a997b438 fix(prebuilt): don't sleep after the final nvidia-smi retry attempt
Cosmetic follow-up to a4ad50e (review nit): the retry loop slept 2s even
after the last attempt, adding ~2s only when nvidia-smi is permanently hung.
Sleep only between attempts.
2026-06-19 02:55:09 -07:00
Daniel Han
a4ad50e024 fix(prebuilt): harden nvidia-smi GPU detection against WSL GPU-PV slowness
On Windows-on-ARM + NVIDIA the Studio install runs inside WSL2, where
nvidia-smi is served over GPU-PV and can take far longer than its usual
sub-second response when the host is under heavy CPU load (the concurrent
pip / frontend / cmake work during install). detect_host probed nvidia-smi
with a single 20s timeout; under that load it raised TimeoutExpired, the GPU
was treated as ABSENT, and the host was misrouted to the ggml-org CPU prebuilt
-> rejected on an NVIDIA host -> slow (and on thermal-limited laptops, risky)
CUDA source build, even though a usable arm64 CUDA prebuilt was published.

Add _nvidia_smi_capture(): retry the three detect_host nvidia-smi probes with
a generous 60s per-attempt timeout. It is only reachable when nvidia-smi exists
on PATH, so CPU-only hosts incur no extra wait. Measured: nvidia-smi took
42-59s under a -j20 build on an N1X; with the fix the probe rides it out and
detect_host correctly reports has_usable_nvidia + compute_cap, so the CUDA
prebuilt is selected (no source build).
2026-06-19 02:29:42 -07:00
Daniel Han
6006402fa2 fix(provision): address Codex review (3 P2s on the aarch64 CUDA provisioner)
- find_nvcc now prefers the highest /usr/local/cuda-<ver> toolkit so a stale
  unversioned `cuda` symlink or an older nvcc earlier on PATH can't win and
  rebuild with CUDA 12.x (re-hitting the glibc>=2.41 / Blackwell clash this
  script avoids); falls back to a PATH nvcc only when no versioned toolkit.
- Validate the GPU compute_cap is purely numeric before using it as
  CMAKE_CUDA_ARCHITECTURES: some WSL GPU-PV / driver combos report "N/A",
  which CMake rejects (aborting an otherwise-usable build) instead of letting
  "native" autodetect.
- Gate the native-Linux aarch64 provisioner on _SKIP_GGUF_BUILD: when a non-root
  user declines the sudo prompt (or lacks sudo) for GGUF deps, don't then run a
  provisioner that does its own sudo apt-get installs.
2026-06-19 00:40:19 -07:00
Daniel Han
73bf482f65 Merge remote-tracking branch 'origin/main' into woa-nvidia-wsl-fallback
# Conflicts:
#	unsloth/models/_utils.py
2026-06-18 22:54:30 -07:00
Daniel Han
e744fb75ed test(studio): stop llama.cpp update worker tests hanging on a fully-installed host
_run_update imports routes.inference.get_llama_cpp_backend, which on a
fully-installed host pulls a real Studio singleton and blocks on its load
lock. Default the autouse fixture to a no-backend stub (the fail-open
path); the load-coordination tests still inject their own backend over it.
2026-06-18 22:52:21 -07:00
Daniel Han
aa2c4ba3eb fix(install): verify the unsloth CLI exists before declaring WSL success
The WoA WSL path gated success solely on torch.cuda.is_available(), but
install.sh can exit after PyTorch yet before the `unsloth` package/console
script (e.g. a transient `uv pip install unsloth`). torch would still import,
so the installer wrote a Windows shim pointing at
/root/.unsloth/studio/unsloth_studio/bin/unsloth and reported success even
though that binary was absent -- `unsloth studio` then fails "no such file".
$wslRc can't distinguish this (it also goes non-zero on the optional llama
prebuilt step). Now `test -x` the exact shim target; if missing, fall through
to the existing failure path (restore rollback + non-zero exit) instead of
creating a dangling shim. Verified on N1X: present->exit 0 (success kept),
absent->exit 1 (fails). Addresses Codex review 4494521902.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 21:56:45 -07:00
Daniel Han
e7468cde21 fix(install): stop the duplicate/blank WSL shortcut on Windows-on-ARM
On the WoA path install.ps1 already creates one canonical "Unsloth Studio.lnk"
with a %USERPROFILE%\.unsloth icon (renders on WoA). install.sh's
create_studio_shortcuts ALSO made a second "Unsloth Studio (WSL - <distro>).lnk"
whose icon lived under %LOCALAPPDATA% -- which the WoA shell icon broker can't
read, so it rendered BLANK. Net: two shortcuts, one blank ("blank for both").

- install.ps1: export UNSLOTH_SKIP_WSL_WINDOWS_SHORTCUT=1 into the WSL install so
  install.sh skips its own Windows .lnk (install.ps1 owns the WoA shortcut).
- install.sh: honor that flag (skip the WSL .lnk branch); and move the WSL
  shortcut icon from %LOCALAPPDATA%\Unsloth Studio to %USERPROFILE%\.unsloth so a
  DIRECT native-WSL install (no install.ps1) also renders instead of going blank.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 21:37:29 -07:00
Daniel Han
b0ee45c34d Merge remote-tracking branch 'origin/main' into woa-nvidia-wsl-fallback 2026-06-14 21:29:43 -07:00
Daniel Han
b50eb8bc71 Tighten and trim code comments 2026-06-12 08:25:40 +00:00
Daniel Han
d888184da4 fix(install): address Codex review on WoA WSL fallback
- install.ps1: force WSL2 (`wsl --set-default-version 2`) before installing a
  NEW distro, so a host whose default is WSL1 doesn't get a GPU-less distro
  that fails only at torch.cuda (the pre-existing-distro branch already
  probes/converts).
- install.ps1: forward `UNSLOTH_PYTHON` into the WSL install (install.sh reads
  it; a Windows env var isn't visible inside WSL otherwise). Numeric-only guard
  rejects shell injection.
- install.ps1: add sqlite-vec / pymupdf / python-docx to the cut-short-install
  server-deps repair so RAG/knowledge-base features aren't left broken.
- uninstall.sh: gate the Windows %LOCALAPPDATA%\Unsloth shim removal on the
  current distro owning the fallback (wsl-distro.txt), so uninstalling Studio
  from a different WSL distro no longer breaks the still-installed shim.

Disproved (no change): worker.py Spark name match is already whole-token
(commit 4cebfab, not substring); the shim's non-login WSL exec DOES have
/usr/lib/wsl/lib on PATH (nvidia-smi resolves -> Spark detector returns True),
verified on N1X.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 23:59:29 -07:00
Daniel Han
aa01e8b382 fix(uninstall): sweep legacy distro-suffixed Studio shortcuts
The Windows uninstaller removed only the exact name "Unsloth Studio.lnk",
orphaning legacy "Unsloth Studio (WSL - <distro>).lnk" shortcuts left by
pre-release dev builds. Glob "Unsloth Studio (*.lnk" across Desktop + Start
Menu so the documented "remove the shortcuts" contract holds regardless of
suffix. Validated on N1X: both canonical and suffixed .lnk removed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 23:15:22 -07:00
Daniel Han
2d4c468603 Merge commit '4af1200' into woa-nvidia-wsl-fallback 2026-06-11 23:08:19 -07:00
Daniel Han
2fa4ec504f Merge remote-tracking branch 'origin/main' into woa-nvidia-wsl-fallback 2026-06-11 23:05:11 -07:00
pre-commit-ci[bot]
4af1200dd3 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-06-12 05:22:48 +00:00
Daniel Han
4cebfabaa6 fix(spark): whole-token device-name match so GB10 != GB100/GB10X
A loose substring match ("GB10" in name) misdetected a discrete Grace+Blackwell
datacenter GPU (e.g. nvidia-smi name containing "GB100") as a unified-memory DGX
Spark, applying the UMA tuning (pin_memory off, vLLM disabled, allocator capped to
0.80) and regressing that hardware. Match each device-name token with non-alphanumeric
boundaries instead. Found by a platform x device-name gating simulation; the real N1X
(JMJWOA-Generic-GPU) still detects, GB100/B100/GB200/GH200/B200 now correctly reject.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 22:21:46 -07:00
Daniel Han
8f0b5e78da docs: tighten PR comments/docstrings (no code change; AST + non-comment-line verified) 2026-06-11 22:05:05 -07:00
Daniel Han
c3b50c4e98 Merge origin/main into woa-nvidia-wsl-fallback (94 commits)
Resolved 2 conflicts:
- studio/setup.sh: main restructured the CUDA-toolkit branch (new
  driver-vs-toolkit major-version compatibility check + an
  _CUDA_TOOLKIT_ALLOWED guard that now owns -DGGML_CUDA=ON and the
  CUDA_ARCHS detection). Took main's structure and re-injected our
  glibc>=2.41/CUDA<13.3 rsqrt diagnostic inside the guarded block so it
  runs against the final _NVCC_VER (after main's driver-compat swap).
- scripts/uninstall.sh: main expanded ~/.unsloth cleanup (.cache,
  .staging, librocdxg, rocm-smoketest, rmdir) and rewrote the WSL
  Windows-shortcut removal to per-distro, wsl.exe-target-filtered
  matching. Took main's superset + kept our provision_llama_cuda.sh
  removal and our %LOCALAPPDATA%\Unsloth shim+PATH cleanup (appended
  after main's per-distro .lnk loop).
2026-06-11 20:22:48 -07:00
Daniel Han
9bdd5436b2 Merge remote-tracking branch 'origin/main' into woa-nvidia-wsl-fallback
# Conflicts:
#	scripts/uninstall.sh
#	studio/setup.sh
2026-06-11 20:20:53 -07:00
Daniel Han
42e69031b9 Compress PR comments to essentials (comment-only; AST/token-verified)
Comment-compression sweep over comments this PR added, mirroring the
sweep already done on main. No non-comment token changed: .py verified
by AST equality (docstrings normalized), .sh by non-comment-line
equality + bash -n, .ps1 by token-stream equality minus comments.
test_spark_oom_guard.py: 13 passed before and after.

Files touched:
- install.ps1
- install.sh
- scripts/uninstall.ps1
- studio/backend/core/training/worker.py
- studio/scripts/provision_llama_cuda.sh
- studio/setup.sh
- unsloth/kernels/flex_attention.py
- unsloth/models/_utils.py

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 00:34:01 -07:00
Daniel Han
b359acc74b Merge remote-tracking branch 'origin/main' into woa-nvidia-wsl-fallback 2026-06-10 00:10:25 -07:00
Daniel Han
b3135ec7dc uninstall.ps1: scope WSL cleanup to evidenced fallback installs
The in-distro cleanup probed a hardcoded candidate set ('', Ubuntu,
Ubuntu-24.04, Ubuntu-22.04, Debian) on every Windows uninstall, wiping
/root/.unsloth in any reachable distro even when the WoA fallback never
ran -- on an x86 AMD box this deletes a ROCm-on-WSL Studio the AMD flow
installed. Use the evidence the installer already records: clean only
the wsl-distro.txt marker distro or UNSLOTH_WSL_DISTRO; keep the broad
candidate probe solely for legacy marker-less installs, which can only
exist on ARM64 hosts.

Addresses the open Codex P1 on this path. Verified gating matrix:
x86+no-marker -> no cleanup; marker/env -> that distro only;
ARM64+no-marker -> legacy broad probe unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 00:05:07 -07:00
pre-commit-ci[bot]
5f7d99b4bd [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-06-10 06:57:13 +00:00
Daniel Han
5c2aefc6d1 fix(install,studio): address review round 5 (3 real of 10; 5 PS-5.1 claims disproven on hardware)
Real fixes:

- uninstall.ps1: scope the WSL process kill to argv referencing
  /root/.unsloth/ (the fallback's install dir, which its Studio server,
  llama-server, and build runner all reference) instead of the bare
  '[l]lama-server' / '[u]nsloth_studio' name patterns -- uninstalling the
  Windows shim must not kill a user's own unrelated llama.cpp server or a
  /home Studio in a probed distro. Proven live: the path pattern matched
  exactly the three fallback processes while a planted /tmp/llama-server
  decoy matched the old pattern and not the new one. The backslash in
  '/root/\.unslot[h]/' keeps the pattern from matching the cleanup
  command's own argv.

- install.ps1: bridge UNSLOTH_LLAMA_TAG / UNSLOTH_LLAMA_PR into the
  background CUDA-build runner -- the provisioner honors both pins, but
  Windows env vars don't cross into WSL on their own, so a user's pin was
  silently ignored by the deferred build. (Deliberately NOT forwarded into
  the inner install.sh env: setup.sh skips its deferral when a PR pin is
  visible there, which would CPU-build the pin in the foreground.)

- kernels/flex_attention.py: make _flex_is_dgx_spark() CUDA-free
  (nvidia-smi device names, mirroring _is_dgx_spark_no_cuda_init) -- it
  runs at module import and called torch.cuda.get_device_name(), which
  initializes the CUDA allocator before patch_dgx_spark_memory_config()
  can set PYTORCH_CUDA_ALLOC_CONF on exactly the Spark hosts it targets
  (reachable via vision.py importing ..kernels before ._utils). Verified
  on the N1X: detects the machine with torch.cuda.is_initialized() still
  False.

- _utils.py: the TrainingArguments __post_init__ wrapper now forwards
  *args/**kwargs (robustness against future InitVar signatures).

Disproven on hardware (no change): the five "high" PS-5.1 claims --
String.TrimEnd('\', '/') with multiple char args binds fine to
params char[] (verified on PS 5.1.28000.1737, and the uninstaller's PATH
cleanup using exactly that code ran successfully this same day), and
[Text.Encoding] resolves via the System namespace prefix (the background
build dispatch using it has run in every install this week). The worker
"_sp possibly undefined" claim is false: `import subprocess as _sp` is at
worker.py line 25.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 23:56:31 -07:00
Daniel Han
ccd5b30b7e Merge remote-tracking branch 'origin/main' into woa-nvidia-wsl-fallback 2026-06-09 23:30:10 -07:00
pre-commit-ci[bot]
9af17e2aa5 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-06-10 06:13:18 +00:00
Daniel Han
c31a6e876d fix(install,studio): address Codex round-4 review (6 of 7 comments real)
- install.sh: gate the new aarch64 bitsandbytes block on SKIP_TORCH=false --
  with --no-torch/UNSLOTH_NO_TORCH (GGUF-only install) it would have pulled
  torch back into the venv through bitsandbytes' dependencies.

- studio worker: in the new Spark OOM-guard section, decide
  PYTORCH_CUDA_ALLOC_CONF (expandable_segments) BEFORE the guard's first CUDA
  touch -- get_device_properties initializes the CUDA allocator, after which
  the env var is ignored, and the later `import unsloth`
  (patch_dgx_spark_memory_config) is too late for the worker process. Uses
  the same CUDA-free nvidia-smi name sniff, append-don't-override, and
  UNSLOTH_NO_EXPANDABLE_SEGMENTS opt-out as the library patch. Live-verified
  on the N1X: env set while torch.cuda.is_initialized() is still False.

- uninstall.ps1: only run `fuser -k 8888/tcp` in a probed WSL distro when an
  Unsloth install actually exists there (checked BEFORE the rm deletes the
  marker) -- an unrelated listener on 8888 (e.g. Jupyter) in a clean distro
  must survive a Windows-side uninstall. The Unsloth-specific pkills stay
  unconditional.

- install.ps1 + uninstall.ps1: persist the chosen WSL distro to
  %LOCALAPPDATA%\Unsloth\wsl-distro.txt at install; uninstall reads it
  (before removing the directory) and prepends it to the cleanup candidates,
  so a custom UNSLOTH_WSL_DISTRO install is cleaned without the env var
  being set again at uninstall time.

- provision_llama_cuda.sh: honor UNSLOTH_LLAMA_PR (numeric-validated,
  best-effort fetch of pull/N/head after clone) so a provisioned tree
  matches a PR pin the way setup.sh does; and require only llama-server in
  the main cmake build (mirroring setup.sh), building the helper targets
  (llama-cli/quantize/mtmd-cli/gguf-split) best-effort afterwards -- an
  older UNSLOTH_LLAMA_TAG pin lacking a newer helper target no longer fails
  the whole provision.

Not changed: the "--tauri rejection doesn't restore the venv rollback"
comment is incorrect -- the rejection returns through Exit-InstallFailure,
which itself calls Restore-StudioVenvRollback.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 23:12:47 -07:00
Daniel Han
ca8c1b434d fix(install): bitsandbytes on aarch64+NVIDIA so 4-bit QLoRA works out of the box
The base unsloth package does not depend on bitsandbytes and the cuXXX
extras that normally add it are x86_64-oriented, so the Spark-class install
path (DGX Spark / GB10 / N1X, native or WSL) produced a venv where
FastLanguageModel.from_pretrained(..., load_in_4bit=True) fails with
ModuleNotFoundError -- found while benchmarking the UMA training knobs on
the N1X. bitsandbytes ships working aarch64 manylinux wheels (0.49.2
verified on sm_121 Blackwell: 4-bit Linear4bit forward runs on GPU via PTX
JIT), so install.sh now adds it best-effort on Linux aarch64 + NVIDIA after
the unsloth install, using the same version constraint as pyproject
(>=0.45.5,!=0.46.0,!=0.48.0). Platforms without a wheel just keep 16-bit
LoRA / full finetuning, with a substep saying so.

Live-tested on the N1X: with bitsandbytes removed from the venv, the block
reinstalls and imports it; benchmark suite then ran 4-bit QLoRA training in
7 configs without error.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 22:33:21 -07:00
Daniel Han
b4921e2a83 feat(studio): Spark unified-memory OOM guard in the training worker (Strix Halo parity)
PR #5301 protects ROCm unified-memory APUs (Strix Halo gfx1150/gfx1151) with
a default set_per_process_memory_fraction(0.80) at training-worker startup,
because exhausting a shared GPU+OS memory pool can stall the whole box
instead of raising a catchable OutOfMemoryError. NVIDIA Spark-class parts
(DGX Spark / GB10, N1X "RTX Spark") have the same pool topology and the same
failure mode, but only had an opt-in cap (UNSLOTH_SPARK_MEM_FRACTION).

- worker.py: new _nvidia_classify_spark_unified_memory(props) mirroring
  _rocm_classify_unified_memory: is_integrated property first (authoritative
  on native Linux), then Spark device-name tokens -- WSL2's GPU
  paravirtualization masks is_integrated to 0 and renames the device (the
  N1X reports "JMJWOA-Generic-GPU"; verified on hardware), so the property
  alone misses Spark-under-WSL. Section 1h applies the 0.80 cap on match;
  UNSLOTH_SPARK_MEM_FRACTION overrides it and any value outside (0, 1]
  disables the guard. Discrete NVIDIA GPUs and CPU-only hosts are untouched.
  The existing generic OOM handler in the training loop surfaces the
  resulting OutOfMemoryError.

- _utils.py: range-validate the opt-in UNSLOTH_SPARK_MEM_FRACTION -- "0"
  previously called set_per_process_memory_fraction(0.0), which makes every
  subsequent CUDA allocation OOM.

- tests: test_spark_oom_guard.py mirroring test_rocm_oom_guard.py (property
  path, WSL name-token path, discrete negatives). 47/47 pass alongside the
  ROCm suite.

Live-verified on the N1X (WSL2): classifier matches via JMJWOA, and with the
cap set an over-allocation raises catchable torch.OutOfMemoryError instead
of stalling the box; allocations recover after the error.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 21:58:28 -07:00
Daniel Han
76f637053f fix(install): non-zero process exit for -File failures; quote spaced distro in background-build launcher
Two Codex round-3 review fixes (both reproduced empirically before fixing):

- Exit-InstallFailure (and the WoA deferred-WSL / WSL-install-failed return
  paths) now `exit $Code` when the script runs from a file (powershell -File
  or .\install.ps1): a plain return exits the process with 0 regardless of
  $global:LASTEXITCODE, so automation treated fatal failures -- including the
  "enable WSL + reboot" deferred state -- as completed installs. Under
  `irm | iex` $PSCommandPath is empty and `exit` would kill the user's shell,
  so that context keeps the return + $LASTEXITCODE behavior. Verified: the
  old pattern exits 0 under -File, the new one exits 1, and an iex run
  survives with the session intact. Tauri behavior is unchanged (already
  exited). All Exit-InstallFailure call sites are body-level in
  Install-UnslothStudio followed by nothing but the trailing invocation, so
  control flow is unchanged -- only the process exit code.

- The detached background CUDA-build launcher now passes $_distroArg instead
  of the raw distro name: PS 5.1's Start-Process joins -ArgumentList with
  spaces WITHOUT quoting (verified: 'Ubuntu Preview' arrives as two args), so
  a spaced UNSLOTH_WSL_DISTRO never started the background builder while the
  install reported it running. $_distroArg is pre-quoted only when the name
  contains spaces, since wsl.exe rejects a quoted space-free name.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 21:01:40 -07:00
Daniel Han
80c6eb0af2 fix(install): only quote the WSL distro name when it contains spaces
wsl.exe parses its raw command line itself: invoked from the generated
unsloth.cmd shim, `wsl.exe -d "Ubuntu-24.04"` fails with
WSL_E_DISTRO_NOT_FOUND -- the quotes are treated as part of the name
(reproduced live on WSL 2.x). The blanket quoting added in 8e51d18 for
spaced UNSLOTH_WSL_DISTRO values therefore broke the shim for every
standard distro name.

Quote the name only when it actually contains whitespace: bare names keep
the proven working form, and spaced names get quoting (bare would split
after -d, so quoting is their only viable form). Applied to the shim and
the copy-paste hint commands via a single $_distroArg.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 20:12:57 -07:00
Daniel Han
8e51d18a6c fix(install): address Codex round-2 review (WSL1 distros, build-failure restore, libcurl, shim quoting, opt-out forwarding)
- install.ps1: detect a PRE-EXISTING WSL1 distro up-front (kernel string +
  libcuda probe inside the distro; encoding-proof vs UTF-16 `wsl -l -v`) and
  convert it with `wsl --set-version 2`, failing early with instructions if
  conversion does not take -- instead of completing a full install that only
  fails at the final torch.cuda check (no GPU passthrough under WSL1).
- install.ps1: quote the distro name in the generated unsloth.cmd shim and in
  the copy-pasteable hint commands so UNSLOTH_WSL_DISTRO values with spaces
  ("Ubuntu Preview") keep working.
- install.ps1: forward UNSLOTH_NO_LLAMA_CUDA=1 into the WSL install env; the
  inner setup.sh otherwise defers its llama.cpp build to a background builder
  this script then never dispatches (the same opt-out skips it), leaving no
  llama-server and a misleading "building in background" footer. Also add
  libcurl4-openssl-dev to the WSL bootstrap apt line.
- provision_llama_cuda.sh: install libcurl4-openssl-dev with the base tools --
  _cmake_configure forces -DLLAMA_CURL=ON and on the deferred WSL path this
  script is the only build path (setup.sh's GGUF dep install was skipped), so
  configure failed on fresh hosts without the headers.
- provision_llama_cuda.sh: keep the pre-existing llama.cpp backup until the
  fresh build is CONFIRMED (was: dropped right after a successful clone), and
  restore it on configure/build failure or when no server binary was produced
  -- a failed CUDA build no longer destroys a previously working (CPU) server.
- setup.sh: when provisioning fails and NO llama-server is present, set
  _LLAMA_CPP_DEGRADED=true so the arm64 CPU-prebuilt last resort and the
  installer failure exit fire instead of reporting a working install.

Round-2 comments verified already fixed in ad77ae6 (anchored to its parent
d161ff5): the torch probe already passes --reinstall; the WSL uninstall is
already scoped to /root only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 20:07:32 -07:00
Daniel Han
53de77b007 Merge remote-tracking branch 'origin/main' into woa-nvidia-wsl-fallback 2026-06-09 19:11:51 -07:00
pre-commit-ci[bot]
c30d9a73cd [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-06-08 14:07:47 +00:00
Daniel Han
ad77ae6cae fix(install): address PR review (Codex + Gemini) — exit codes, over-broad uninstall, Spark allocator, provision robustness
install.ps1 (WoA WSL fallback):
- report failure (non-zero) + restore the rolled-aside venv when the WSL GPU
  install fails (torch.cuda absent) or when WSL needs enabling+reboot, instead of
  returning success — so -File/Tauri callers don't see a broken install as complete
- on WSL success, Complete-StudioVenvRollback so the previous-venv backup isn't orphaned
- refuse under --tauri with a clear "use the CLI installer" message (the desktop
  launcher resolves a Windows-venv backend, which a WSL-only install can't provide)
- reset $LASTEXITCODE before each wsl.exe / python probe (a stale 0 could mark WSL
  ready / torch OK if the native command fails to launch)
- torch-availability probe: --reinstall so an already-installed CPU torch in a
  migrated venv isn't accepted as "satisfied" (would wrongly skip the WSL path)
- treat a null HKCU PATH as empty (fresh profile) so shim PATH update can't throw
- keep apt stderr visible inside WSL (only stdout -> /dev/null) for diagnosability

scripts/uninstall.ps1:
- scope WSL cleanup to /root (the fallback's install location); stop deleting
  /home/*/.unsloth, which could erase an unrelated WSL user's own Unsloth/cache

studio/setup.sh:
- direct (non-install.ps1) WSL installs now provision CUDA llama.cpp themselves
  instead of being left with no GGUF server: install.ps1 exports
  UNSLOTH_WSL_LLAMA_DEFERRED=1, and the aarch64+NVIDIA provision block runs under
  WSL only when that marker is absent
- mark a provisioner-built llama.cpp as Studio-owned in custom-STUDIO_HOME mode so
  the next setup's _assert_studio_owned_or_absent doesn't abort
- glibc>=2.41 check: also match a future major>2 (e.g. 3.0)

studio/scripts/provision_llama_cuda.sh:
- install base tools (cmake/git/curl) in their own apt transaction before the
  best-effort gcc-14/g++-14 (unavailable on Ubuntu 22.04 / Debian 12, where bundling
  them aborted the whole transaction and left no build tools)
- back up an existing (e.g. CPU-only) llama.cpp before the destructive clone and
  restore it on clone failure, so a failed clone doesn't leave the user with no server
- honor a pinned llama.cpp ref via UNSLOTH_LLAMA_TAG instead of always tracking main

unsloth/models/_utils.py:
- set PYTORCH_CUDA_ALLOC_CONF (expandable_segments) via a CUDA-free Spark detector
  (nvidia-smi, not torch.cuda.get_device_name) so it takes effect before CUDA/the
  caching allocator initialize — previously it was a silent no-op on auto-detected Spark

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 07:07:00 -07:00
pre-commit-ci[bot]
d161ff52a5 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-06-08 12:25:41 +00:00
Daniel Han
05f909915c Merge remote-tracking branch 'origin/main' into woa-nvidia-wsl-fallback 2026-06-08 05:25:12 -07:00
Daniel Han
01b04e4410 Merge remote-tracking branch 'origin/main' into woa-nvidia-wsl-fallback 2026-06-05 05:58:59 -07:00
Daniel Han
e3f0581e38 install.ps1: put WoA shortcut icon outside %LOCALAPPDATA% (real blank-icon fix)
Root cause (diagnosed live on an N1X WoA box, confirmed by on-screen checks):
the Windows shell's sandboxed icon-extraction broker cannot read a standalone
.ico stored under %LOCALAPPDATA% (it gets a redirected/virtualized view), so
the Desktop + Start Menu shortcuts render BLANK -- regardless of icon format
(BMP vs PNG frames), ACLs, icon cache, or shortcut-creation method, all of
which were ruled out. The IDENTICAL .ico renders correctly from a path under
the user profile. Fix: write unsloth.ico to %USERPROFILE%\.unsloth instead of
%LOCALAPPDATA%\Unsloth (shim/launcher stay in %LOCALAPPDATA%). uninstall.ps1
removes the icon at the new location. Also drops the speculative SHGetFileInfo
"image-list prime" block added while chasing the wrong (format/cache) theory.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 03:18:22 -07:00
Daniel Han
4ee9aa5ab5 install.ps1: prime shell image list for new shortcuts (blank-icon race fix)
Diagnosis this cycle: the .ico is well-formed (6 BMP frames 16-256px) and the
shell resolves the logo at every size (verified via SHGetFileInfo +
SHGetImageList/ImageList_GetIcon on the system image list, all sizes incl. the
256px jumbo slot the desktop draws). The residual blank is a first-paint race:
Explorer lazily extracts a .lnk's icon and a miss (icon not yet flushed, cache
just cleared) gets cached blank. Force the extraction at install time via
SHGetFileInfo(SHGFI_SYSICONINDEX) per .lnk, populating the per-session image
list both Desktop and Start Menu draw from. WoA path only, try/catch-wrapped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 14:13:43 -07:00
Daniel Han
fa3e5d3bd1 install.ps1: add ie4uinit -ClearIconCache before -show (blank-shortcut fix)
A same-name "Unsloth Studio.lnk" recreated across reinstalls keeps Explorer's
stale (blank) iconcache_*.db entry; -show rebuilds but does not purge, so add
-ClearIconCache first (matches PR #5940). The per-.lnk SHChangeNotify remains
the primary fix. WoA-path only -- no effect on other installs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 13:47:25 -07:00
Daniel Han
11d632307c provision: ignore junk/0 UNSLOTH_LLAMA_BUILD_JOBS (cmake -j0 = all cores)
A non-numeric or 0 override silently fell through to `cmake -j0`, which
builds with ALL cores -- the opposite of the thermal-headroom default and a
shutdown risk on NVIDIA-ARM laptops. Validate it's a positive integer; ignore
anything else and auto-compute.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 13:28:53 -07:00
Daniel Han
aaf24b5670 Merge remote-tracking branch 'origin/main' into woa-nvidia-wsl-fallback 2026-06-04 13:01:18 -07:00
Daniel Han
4d9174b9ea install.sh: redirect Windows exes from /dev/null (fix curl|sh stdin drain)
`curl https://unsloth.ai/install.sh | sh` runs install.sh from a pipe, so the
script *is* the shell's stdin. A Windows process launched via WSL interop
(powershell.exe / cmd.exe) inherits that stdin and drains the remaining
piped script, truncating it -- dash then aborts parsing the tail with
"Syntax error: Unterminated quoted string". This surfaced as a non-fatal
"sh: <line>: Unterminated quoted string" near the end of every non-tty
install (e.g. the WoA install.ps1 -> curl|sh flow). Add `</dev/null` to the
three Windows-exe invocations (create_studio_shortcuts' powershell .lnk
writer + the two browser-open helpers) so they cannot consume the script.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 00:12:53 -07:00
Daniel Han
ac62ff636c Merge remote-tracking branch 'origin/main' into woa-nvidia-wsl-fallback 2026-06-03 23:58:12 -07:00
Daniel Han
a186275bac provision_llama_cuda: retry-clean on build failure (interrupted-build recovery)
An interrupted CUDA build (e.g. a thermal/power shutdown mid-compile -- which
this machine class hits) can leave a partially-linked libggml-cuda.so. On the
next run cmake does not relink it, so linking llama-server fails with undefined
ggml_cuda_op_* references and the script gives up with no server. Mirror the
existing configure retry-clean: if `cmake --build` fails, wipe build/,
reconfigure, and rebuild clean once before giving up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 21:12:05 -07:00
Daniel Han
c7fd2cf925 provision_llama_cuda: default to ~half cores (thermal headroom)
A full -j(nproc) CUDA build trips power/thermal shutdowns on thermally
constrained NVIDIA-ARM laptops (observed on the N1X "RTX Spark": a full-core
build, especially alongside other load, shuts the machine down). nice lowers
CPU *scheduling* priority but not heat -- power/heat scale with the number of
active compile jobs -- so default to ~half the cores instead: still ~2.5x
faster than a tiny -j4, but leaves real headroom. Still mem-capped (~1.5 GB
per nvcc job) and overridable via UNSLOTH_LLAMA_BUILD_JOBS (raise on a
well-cooled box, lower if it still trips). Tiny boxes (<=4 cores) use all.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 21:06:43 -07:00
Daniel Han
61a902542a Merge origin/main into woa-nvidia-wsl-fallback
Only install.ps1 conflicted (header comment block): kept main's fuller
header (usage examples + install-dir priority + SPDX); UNSLOTH_INSTALL_REF
stays documented at Get-UnslothInstallRef. install.sh, pyproject.toml,
scripts/uninstall.ps1, scripts/uninstall.sh, unsloth/models/_utils.py
auto-merged. Verified: all WoA changes survived (--cd /root, zoo fix,
mem-aware/nice CUDA build, skip-CPU, shortcut robustness, uninstall exit-0)
and PowerShell AST / bash -n / python ast all pass.
2026-06-03 07:39:46 -07:00
Daniel Han
0946d5d37f Trim verbose PR comments to be succinct
Shorten the multi-line rationale comments added by this PR across the
remaining changed files to 1-2 lines each, preserving intent (gotchas,
workarounds, why-notes). Comment-only changes; no code, strings, or
behavior altered. Verified: PowerShell AST parser, bash -n, and python
ast.parse all pass; diffs confirmed comment-only.

Files: install.ps1, scripts/uninstall.ps1, scripts/uninstall.sh,
studio/setup.sh, unsloth/models/_utils.py, unsloth/kernels/flex_attention.py

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 06:24:07 -07:00
Daniel Han
db0df15f52 provision_llama_cuda: run the background CUDA build at idle priority
Building at -j(nproc) saturates every core (load ~25 on a 20-core box),
which starved a concurrently launched `unsloth studio` / training session
during the build's few-minute window. Wrap the cmake build in
`nice -n 19` (+ `ionice -c 3` when available): full speed when the box is
idle, but instant yield to foreground work. Also trims this file's comments.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 06:23:56 -07:00
Daniel Han
6c3453f2c0 install.sh: git-ref test path installs unsloth-zoo explicitly
unsloth_zoo is an optional extra (not a base dependency), and install.sh
always runs the studio deps step with SKIP_STUDIO_BASE=1 (which skips the
base.txt install that would otherwise add it). Every other install path
names unsloth-zoo explicitly; the pre-merge UNSLOTH_INSTALL_REF git path
did not, so a branch build left unsloth_zoo missing and `import unsloth`
failed with "Please install unsloth_zoo". Name it explicitly here too.
The default PyPI path is unaffected (released unsloth carries zoo as a
base dep). Also trims the verbose comment on this block.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 06:23:44 -07:00
Daniel Han
ac3829b381 install.ps1: run WSL Studio install/setup with --cd /root (fix CRLF setup.sh)
When install.ps1 is launched from inside a cloned unsloth repo, the WSL
subprocess inherited the Windows cwd (/mnt/c/.../unsloth). Python then
prepended that dir to sys.path and `import unsloth` resolved the LOCAL
CLONE instead of the installed package, so `unsloth studio update`'s
_find_setup_script() returned the clone's studio/setup.sh -- which has
CRLF line endings on a Windows checkout. bash aborted on line 4
($'\r': command not found / set: pipefail: invalid option name), the
deps + frontend step never ran, and Studio was left unusable (missing
packaging/structlog/fastapi).

Fix: pass `--cd /root` to the WoA-branch wsl.exe invocations (install,
self-heal repair, torch/server verifications, the desktop launcher, and
the background CUDA build) so the WSL side never starts in /mnt/c and
always imports the installed package -> resolves the LF setup.sh in
site-packages. The native `unsloth` shim is intentionally left without
--cd so relative model-path args keep resolving against the user's cwd
(the console-script entry point does not cwd-shadow at runtime).

Also: use SHCNF_IDLIST (0) for the global SHCNE_ASSOCCHANGED notify
(items are unused for that event) instead of SHCNF_PATHW.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 05:44:30 -07:00
Daniel Han
27f045a412 uninstall.ps1: exit 0 on success (do not leak WSL probe exit code)
The WSL distro-probe loop tries a candidate list that intentionally
includes distros that may not exist; the last failed `wsl -d <name> -- true`
probe left $LASTEXITCODE=255, so `& .\uninstall.ps1` returned non-zero even
when every cleanup step succeeded. Reset $global:LASTEXITCODE=0 at the end
(not `exit 0`, so the `irm ... | iex` usage does not kill the caller shell).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 05:29:31 -07:00
Daniel Han
962ab23317 install.ps1: make WSL Studio shortcuts robust against blank icons
The WSL-fallback Desktop/Start-Menu shortcuts could render blank because
the block only downloaded the .ico from GitHub (no fallback, no validation)
and only fired a single global shell notify. Now it:

  - prefers the icon bundled in the local clone (instant, reliable) and
    only falls back to a GitHub download when no bundle is present;
  - validates the ICO header (00 00 01 00) before attaching, so a partial/
    empty/404 download can never leave a non-icon attached;
  - sets IconLocation as "<path>,0" (explicit index);
  - issues a per-.lnk SHCNE_UPDATEITEM (SHCNF_PATHW) notify in addition to
    the global SHCNE_ASSOCCHANGED, forcing Explorer to re-read each new
    shortcut icon immediately and clear any stale blank cache entry.

Mirrors the icon handling already used by the native New-StudioShortcuts
path, plus the ie4uinit refresh approach from PR #5940.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 05:22:34 -07:00
Daniel Han
5d2a89a0e5 provision_llama_cuda: use all cores for CUDA build (memory-aware -j)
The CUDA llama.cpp compile is the slow step of the WSL GPU setup. The job
count now defaults to the full core count (nproc) instead of being capped,
which is ~5x faster on a 20-core box (-j4 -> -j20). To stay safe on
unified-memory machines, where nvcc jobs (~1.5 GB each) could OOM-kill a
full-parallel build, jobs are capped at mem/1.5GB when that is lower than
nproc. UNSLOTH_LLAMA_BUILD_JOBS=N still overrides for thermal throttling.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 05:21:16 -07:00
Daniel Han
d5d0858b21 install.ps1: fix detached CUDA-build launch (runner script, not bash -lc string)
The previous Start-Process passed `@(... 'bash','-lc',$buildCmd)` where $buildCmd
contained spaces (`env VAR=N bash provision.sh > log`). Start-Process's ArgumentList
array mis-quotes a space-containing element, so wsl ran just `env` -- which dumped
the environment to the log and exited; no build, only a CPU server left behind.

Fix: build a tiny runner script here, ship it as base64 (dodges every quoting
layer), and Start-Process invokes `bash /root/.unsloth/run_llama_build.sh` with
ONLY space-free args. The runner also restores PATH (/usr/lib/wsl/lib for
nvidia-smi, /usr/bin for apt) so the non-login detached shell doesn't make
provision early-exit "no nvidia-smi", then caps jobs and runs provision with
logging. Verified on a cold distro: the detached build survives install.ps1's
exit and provision runs correctly (toolkit install + CUDA build), log shows real
provision output.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 05:03:41 -07:00
Daniel Han
4a566fe7d6 UNSLOTH_INSTALL_REF: install unsloth from the ref in WSL (env, not --package)
The previous attempt passed install.sh `--package git+https://...@ref`, but
install.sh validates --package and rejects URL characters ("invalid characters")
-> the WSL install aborted (exit 127). Fix it properly and symmetrically:

- install.sh: add a gated UNSLOTH_INSTALL_REF path that installs
  `unsloth @ git+https://github.com/unslothai/unsloth@<ref>` via uv. Gated to the
  default package ("unsloth") and a non-"main" ref, so released-PyPI behavior is
  unchanged by default. Bypasses the --package name validation (fixed literal URL,
  no injection surface).
- install.ps1: when UNSLOTH_INSTALL_REF is a branch, fetch THAT ref's install.sh
  (which honors the env) and export UNSLOTH_INSTALL_REF, so the WSL studio venv
  carries the branch's studio/setup.sh + unsloth Python (e.g. the WSL CPU-build
  skip is actually exercised pre-merge). Default (ref = main) is byte-identical:
  `curl https://unsloth.ai/install.sh | sh`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 04:48:49 -07:00
Daniel Han
abb99d52ca install.ps1: UNSLOTH_INSTALL_REF also drives the WSL unsloth install
So a branch can be tested end-to-end pre-merge: when UNSLOTH_INSTALL_REF is set
(not main), pass install.sh `--package git+https://github.com/unslothai/unsloth@<ref>`
so the WSL studio venv carries THAT ref's studio/setup.sh + unsloth Python patches
(otherwise install.sh installs released PyPI unsloth and the branch's setup.sh --
e.g. the WSL CPU-build skip -- never runs). Default (ref = main) is byte-identical
to before. The git URL has no spaces, so it survives PowerShell -> wsl.exe -> bash -lc.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 04:38:21 -07:00
Daniel Han
b291c13a97 WSL CUDA llama: make the background build reliable + clean (go straight to GPU)
Three coupled fixes so the Windows-on-ARM + NVIDIA (DGX Spark / N1X) WSL path
builds the GPU llama-server reliably and never wastes time on a CPU build:

1. install.ps1 -- the post-install CUDA provision was launched as a WSL-side
   `nohup setsid ... &`. That does NOT survive: WSL shuts the distro's VM down
   once the launching wsl.exe session exits, killing the detached build (observed
   on a fresh distro: no build log, only a CPU server left behind). Fetch the
   provision script in a quick session, then run the build anchored to a DETACHED
   Windows-side process (Start-Process wsl.exe, no -Wait) that holds the VM up for
   the whole build while install.ps1 returns immediately.

2. provision_llama_cuda.sh -- a pre-existing build/ can carry an incompatible
   CMake cache (the Studio installer stages its build in llama.cpp.build.NNNN then
   relocates it, leaving a cache with stale absolute source/build paths and
   GGML_CUDA=OFF), so reconfiguring for CUDA fails ("CMakeCache directory is
   different" / "source does not match"). Try to reuse build/ first (incremental
   resume), and if configure fails, wipe build/ and configure clean once. Verified
   live on the failing scenario: stale cache detected, wiped, clean CUDA configure.

   (setup.sh's skip of the CPU source build on this path is the companion commit;
   together the fresh-install path builds only the CUDA server, in the background.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 04:33:46 -07:00
Daniel Han
9e26277cb8 setup.sh: skip CPU llama.cpp source build on WSL2 aarch64+NVIDIA (defer to background CUDA build)
On Windows-on-ARM + NVIDIA (DGX Spark / N1X "RTX Spark"), install.ps1 routes the
install through WSL2 and, after setup finishes, launches provision_llama_cuda.sh
in the BACKGROUND to install the CUDA toolkit + gcc-14 and build the real sm_121
CUDA llama-server, replacing whatever section 9 produced.

On a fresh WSL distro there is no nvcc yet, so section 9 could only ever build a
CPU-only server ("building (CPU, CUDA driver found but nvcc missing)") that the
background CUDA build immediately throws away -- slow and wasteful.

Skip the section-9 source build entirely on this exact path. Introduce a distinct
_LLAMA_CPP_DEFERRED state (NOT _LLAMA_CPP_DEGRADED) so:
  - the footer reports "GGUF engine: CUDA build running in background" (success),
    not "limited: llama.cpp unavailable";
  - the arm64 CPU-prebuilt last-resort does NOT fire (it gates on DEGRADED=true);
  - the install-failure exit 1 does NOT fire (it gates on DEGRADED=true).

Strictly gated -- defers only when ALL hold: WSL (grep microsoft /proc/version),
aarch64/arm64, an NVIDIA GPU is listed by nvidia-smi, nvcc is missing (PATH and
/usr/local/cuda*/bin), UNSLOTH_NO_LLAMA_CUDA != 1, no forced compile, no pinned
PR. Every other host (x86_64, native-Linux aarch64, nvcc-present, opt-out,
ROCm, macOS, non-NVIDIA) is byte-for-byte unaffected and still builds via
section 9 as before. install.ps1 is unchanged; it still builds CUDA in the
background, but now with no wasted CPU build first.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 04:30:36 -07:00
Daniel Han
5ecadb8512 uninstall.ps1: kill llama-server too (pkill self-match made it a no-op)
The WSL cleanup ran `pkill -9 -f unsloth_studio` then `pkill -9 -f llama-server`,
but the `bash -lc <cmd>` shell's own argv contains those literal patterns, so the
first pkill SIGKILLed the shell before the llama-server pkill (and trailing `true`)
ever ran -- leaving a running llama-server (dynamic port, not covered by
`fuser -k 8888`) alive after uninstall. Use the [x]-regex self-exclusion trick
('[u]nsloth_studio' / '[l]lama-server') so the shell's argv no longer contains the
matched substring; real target processes still match. Verified in WSL: shell
survives, both dummy processes are killed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 03:53:14 -07:00
Daniel Han
bb3676d2c8 llama.cpp CUDA detection: handle dlopen-ed backend (split build layout)
Current llama.cpp ships the CUDA backend as a dynamically-loaded plugin
(libggml-cuda.so* next to the binary), NOT a load-time dependency, so
ldd llama-server | grep libggml-cuda is a false negative: it reports no
CUDA on a perfectly good CUDA build. That made both is_cuda_server()
(provision_llama_cuda.sh) and _have_cuda_llama_server() (setup.sh) force a
needless full rebuild every run.

Fix both: keep the ldd check (old monolithic builds) and additionally treat
the presence of libggml-cuda.so* beside the binary as the CUDA signal. A
CPU-only build has no such backend, so this stays correct for the CPU case.

Verified on an N1X/sm_121 WSL build: llama-server --list-devices shows
CUDA0 JMJWOA-Generic-GPU and serves on the GPU, while ldd lists no
libggml-cuda; the new check correctly returns CUDA-present.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 03:16:06 -07:00
Daniel Han
6de39a363a Merge remote-tracking branch 'origin/main' into woa-nvidia-wsl-fallback 2026-06-03 01:08:44 -07:00
Daniel Han
f102eccaa4 install.ps1/uninstall.ps1: add UNSLOTH_INSTALL_REF + fix WSL symlink uninstall hole
install.ps1: fetch repo-versioned WSL-fallback assets (provision_llama_cuda.sh,
unsloth.ico) from a configurable git ref via new UNSLOTH_INSTALL_REF env var
(defaults to main, so existing users are byte-for-byte unaffected). Lets the
ARM64+NVIDIA WSL-fallback GPU path be exercised end-to-end on a branch before it
merges (provision_llama_cuda.sh does not exist on main until then).

uninstall.ps1: the WSL cleanup rm -rf'd /root/.unsloth but left the
~/.local/bin/unsloth launcher symlink dangling, so `unsloth` still resolved on
PATH after an uninstall. Also remove /root/.local/bin/unsloth and
/home/*/.local/bin/unsloth.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 01:08:44 -07:00
Daniel Han
cc31b87850 provision_llama_cuda: put Linux dirs first in PATH (WSL interop hygiene)
When the installer is launched from a Windows shell, WSL interop leaks the Windows
PATH (/mnt/c/... entries, with spaces) into the build environment, which can make
cmake/gcc/git resolve to Windows tools or otherwise confuse the CUDA build. Prepend
the CUDA toolkit + standard Linux dirs so the Linux toolchain always wins; keep the
original PATH after so nvidia-smi etc. still resolve.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 00:35:47 -07:00
Daniel Han
28a02ec303 install.ps1: self-heal deps as bare names (fix PS->wsl quote mangling)
The server-deps self-heal passed specs like "structlog>=24.1.0" with embedded
double-quotes through PowerShell -> wsl.exe -> bash -lc; PowerShell's native-arg
quoting drops the quotes, so bash parsed >= as a redirection and the whole install
failed ('could not auto-install Studio server deps'). Use bare package names (uv
resolves latest, satisfying the studio.txt minimums) -> no embedded quotes, no
redirection. Verified: bare-name uv install populates fastapi/uvicorn/structlog/...
and Studio starts (HTTP 200).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 23:56:54 -07:00
Daniel Han
a1cafb09b1 install.ps1: robust Windows-on-ARM detection under x64 emulation
RuntimeInformation::OSArchitecture (and $env:PROCESSOR_ARCHITECTURE) report
X64/AMD64 when install.ps1 runs under an x64-emulated PowerShell on a Windows-on-ARM
host, which mis-skips the WSL fallback and then fails the native win_arm64 torch
install. Add additive fallbacks (Win32_Processor.Architecture=12 ; machine-level
PROCESSOR_ARCHITECTURE) that read the true OS arch even under emulation. Only turns
the ARM64 path ON for genuine ARM64 hosts; x86_64/native detection unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 23:52:53 -07:00
Daniel Han
8c797daa98 install.ps1: fix WSL-fallback probe false-positive (match torch spec)
The native-CUDA-torch viability probe used a bare 'uv pip install --dry-run torch',
but the real native install pins 'torch>=2.4,<2.11.0'. The cu130 index can carry an
out-of-range torch (e.g. <2.4 or a >2.11 nightly) with a win_arm64 wheel, so the
bare probe PASSED while the pinned install FAILED -> the WSL fallback was skipped and
the install died at 'Failed to install PyTorch' on Windows-on-ARM. Use the same
pinned spec in the probe so its result exactly predicts the native install.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 23:50:01 -07:00
Daniel Han
c4071a86cb install.ps1: propagate UNSLOTH_LLAMA_BUILD_JOBS into the WSL CUDA-llama build
Windows env vars don't cross into WSL by default, so the background
provision_llama_cuda.sh always built at -j(nproc). Forward
UNSLOTH_LLAMA_BUILD_JOBS via 'env' so thermally/power-limited laptops can cap the
build's parallelism (harmless no-op passthrough when unset).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 23:47:00 -07:00
Daniel Han
689032b7b5 uninstall.ps1: encoding-proof WSL distro detection for cleanup
The WSL-distro cleanup parsed 'wsl --list --quiet', whose UTF-16 output PowerShell
often mis-parses into an EMPTY list, so the WSL install (/root/.unsloth + CUDA
llama build) was silently never removed. Probe a candidate set ('' = default
distro, Ubuntu, Ubuntu-24.04, ...) by 'wsl -d <d> -- true' exit code instead
(encoding-proof; same idiom install.ps1 uses), then run the idempotent cleanup.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 23:44:06 -07:00
Daniel Han
c87550cb3b uninstall: fix WSL rm self-kill (rm before pkill) + clean native-Linux llama.cpp
- uninstall.ps1: the WSL-distro cleanup ran 'pkill -f "unsloth studio"' before the
  rm inside a single bash -lc, but that pattern matches the bash -lc's own argv ->
  pkill SIGKILLs the shell before rm runs, so /root/.unsloth survived. Reorder: rm
  FIRST (guaranteed), then non-self-matching fuser -k 8888/tcp + pkill best-effort;
  also remove the fetched provision script + build log.
- uninstall.sh: also remove ~/.unsloth/llama.cpp (CUDA build from provision on
  native-Linux Spark) + the fetched provision_llama_cuda.sh.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 23:37:06 -07:00
Daniel Han
3906a72b08 setup.sh: exclude WSL from native-Linux Spark CUDA-llama provision
setup.sh runs during install.sh, so on WSL the new aarch64+NVIDIA provision block
would foreground-build CUDA llama.cpp during the install -- blocking it and
duplicating install.ps1's WSL background provision. Exclude WSL (grep microsoft
/proc/version, same idiom setup.sh already uses) so this block is native-Linux
(DGX Spark/GB10) only; WSL stays handled by install.ps1's background path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 23:34:01 -07:00
Daniel Han
85aee169fd DGX Spark / native Linux: auto-provision CUDA llama.cpp for GGUF inference
Native-Linux (non-WSL) aarch64+NVIDIA hosts (DGX Spark / GB10 / N1X "RTX
Spark") had a GGUF *inference* gap the Windows WSL2 fallback already closes:
setup.sh's source build only emits a CUDA llama-server when a CUDA toolkit
(nvcc) is already present. A fresh Spark ships only the driver + nvidia-smi,
so the build silently dropped to a CPU-only llama-server and Studio GGUF
inference ran without GPU.

Mirror the Windows path in the shared Linux installer (studio/setup.sh) so
ALL native Linux installs benefit, not just the Windows-specific file:

* setup.sh: after the source build, on Linux aarch64/arm64 WITH an NVIDIA GPU
  AND when no CUDA-linked llama-server exists yet, invoke the existing
  provision_llama_cuda.sh (installs CUDA 13.3 + gcc-14, builds a CUDA server
  into the same $LLAMA_CPP_DIR setup.sh validates). Best-effort, never aborts
  setup; opt out with UNSLOTH_NO_LLAMA_CUDA=1; build load via
  UNSLOTH_LLAMA_BUILD_JOBS. Resolves the script from the packaged copy, the
  local-dev repo, or the pinned GitHub raw URL (matches install.ps1).

* pyproject.toml: ship studio/scripts/*.sh in the wheel (package-data) so the
  normal `curl | sh` install has provision_llama_cuda.sh locally.

Strictly gated + additive: x86_64 NVIDIA, ROCm/AMD, Intel, macOS/MLX,
Windows-native, WSL, CPU-only ARM, and any ARM host that already built a CUDA
server are byte-for-byte unaffected. Studio web-server deps + pip seeding are
already complete on native Linux via install_python_stack.py (studio.txt
step 8 + ensurepip/uv bootstrap step 2), and the Linux .desktop launcher is
already created by install.sh create_studio_shortcuts() -- so no duplicate
self-heal/launcher was added.

bash -n setup.sh / provision_llama_cuda.sh / install.sh: pass.
pyproject.toml: valid TOML.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 23:26:30 -07:00
pre-commit-ci[bot]
2469151804 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-06-03 06:10:20 +00:00
Daniel Han
afe589282d DGX Spark / N1X: UMA training perf+memory defaults (gated, accuracy-neutral)
Four gated, is_dgx_spark()-only extensions (strict no-op on x86 NVIDIA, AMD/ROCm,
Mac/MLX, Intel, discrete aarch64, normal WSL/Windows; no computed value changes):

1. max_autotune=False on Spark, in BOTH torch_compile_options dicts
   (models/_utils.py + kernels/flex_attention.py). This 48-SM GPU is below
   inductor's hardcoded 68-SM is_big_gpu threshold, so max_autotune_gemm is
   already skipped (the 'Not enough SMs to use max_autotune_gemm mode' warning) --
   dropping it only avoids the wasted compile-time autotuning search; the produced
   Triton/inductor kernels are identical (same accuracy + steady-state speed).

2. dataloader_pin_memory=False on Spark, via an idempotent
   TrainingArguments.__post_init__ wrap (covers SFT + all TRL trainers). Pinned
   host memory is pointless on unified memory (no separate device memory) and only
   reserves non-pageable RAM from the shared pool. Mirrors transformers' own
   . Opt out:
   UNSLOTH_SPARK_KEEP_PIN_MEMORY=1.

3. UNSLOTH_DISABLE_DOUBLE_BUFFER defaulted on Spark (setdefault): unsloth-zoo's
   gradient-checkpointing double-buffer is gated on mem_get_info (undercounts on
   UMA) and overlaps a host<->device copy that is free on a shared pool.

4. Opt-in UNSLOTH_SPARK_MEM_FRACTION -> torch.cuda.set_per_process_memory_fraction
   safety valve (default unset = no cap, no capacity loss), so an over-allocation
   raises a catchable OOM instead of wedging the box.

Findings from a 5-agent code+web review (transformers/unsloth/zoo/trl + NVIDIA
DGX-Spark playbooks). Higher-impact-but-needs-validation items (device_map
max_memory sizing, GC offload short-circuit, drop_caches) deferred.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 23:09:56 -07:00
Daniel Han
23ccec6d53 provision_llama_cuda: configurable build jobs (UNSLOTH_LLAMA_BUILD_JOBS)
A full -j(nproc) CUDA build is power/thermal-heavy on laptops (e.g. N1X) and can
trip a thermal/power shutdown mid-build. Allow lowering the job count; cmake
--build is incremental so re-running resumes from where it stopped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 22:41:24 -07:00
pre-commit-ci[bot]
681f61ac6e [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-06-02 14:10:26 +00:00
Daniel Han
1144d972d0 DGX Spark / N1X: enable expandable_segments to reduce UMA fragmentation
Adds patch_dgx_spark_memory_config() (models/_utils.py), applied at import: on
Spark-class machines it sets PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True so
the CUDA caching allocator grows segments in virtual address space instead of
fragmenting the shared unified-memory pool. More of the pool stays usable for
weights/activations -> fewer fragmentation OOMs and headroom for larger models /
longer sequences. Pure memory management: computed values are unchanged, so
accuracy is unaffected (verified: gemma-3-270m losses identical with/without it).

Regression-safe: gated by is_dgx_spark() (strict no-op on x86 NVIDIA, AMD/ROCm,
Intel, Mac/MLX, discrete aarch64). Uses setdefault semantics -- only appends when
expandable_segments is absent, never overrides a user's PYTORCH_CUDA_ALLOC_CONF;
opt out with UNSLOTH_NO_EXPANDABLE_SEGMENTS=1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 07:10:07 -07:00
pre-commit-ci[bot]
4308d254f5 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-06-02 13:51:32 +00:00
Daniel Han
d8e28f6a2f DGX Spark / N1X: shared is_dgx_spark() + caching_allocator_warmup no-op
Runtime training support for NVIDIA Blackwell unified-memory (UMA) machines --
DGX Spark (GB10) and the N1X "RTX Spark" laptop. Gated so it is a strict no-op
on every non-Spark platform (x86 NVIDIA, AMD/ROCm, Intel/XPU, Mac/MLX, discrete
aarch64 GH200/GB200): those are not aarch64 and/or report non-matching device
names, so behaviour there is byte-for-byte unchanged.

models/_utils.py:
- Add is_dgx_spark(): aarch64 + NVIDIA CUDA + a known Spark device-name token
  (GB10 / JMJWOA / N1X / ...). @lru_cache; overridable via UNSLOTH_FORCE_DGX_SPARK.
  One shared detector that also catches the N1X laptop, which reports
  "JMJWOA-Generic-GPU" rather than "NVIDIA GB10".
- Add patch_dgx_spark_caching_allocator_warmup(), applied at import: no-ops
  transformers.modeling_utils.caching_allocator_warmup on Spark. HF sizes a GPU
  pre-allocation from cudaMemGetInfo() to warm the caching allocator; on Spark
  UMA cudaMemGetInfo undercounts free memory (reclaimable buffer cache shows as
  unavailable), so the warmup torch.empty() raises
  `AcceleratorError: invalid argument` and aborts any bitsandbytes 4/8-bit load.
  The warmup is only a speed hint -> dropping it on Spark lets quantized loads
  succeed. Idempotent; single call site (modeling_utils.py:4212) confirmed.
  (Patch credited to Roland [UnAI] / Daniel, Unsloth Discord.)

models/loader.py:
- Replace the two inline `"NVIDIA GB10" in get_device_name()` checks (which
  disable the currently-broken vLLM fast_inference) with is_dgx_spark(), so the
  N1X is covered too. Same behaviour on DGX Spark; no change off-Spark.

torch.compile + Triton are verified WORKING on the N1X (Triton 3.6.0; a real
gemma-3-270m-it 4-bit finetune with compile ON trains and emits the full compiled
cache), so nothing is disabled -- UNSLOTH_COMPILE_DISABLE is not set.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 06:51:04 -07:00
Daniel Han
25c11b3e98 WSL fallback: self-heal Studio server deps, seed pip, auto-build CUDA llama.cpp
A clean-slate reinstall on an ARM64+NVIDIA box surfaced three follow-on gaps in
the WSL path. All fixes are additive, best-effort, and confined to the $torchOk
success branch of the WSL fallback, so they only run on the ARM64+NVIDIA machines
that reach it -- no other platform is affected.

install.ps1:
- Self-heal Studio's web-server deps. install_python_stack.py installs the Studio
  UI deps (fastapi/uvicorn/structlog/starlette) in a late step; if that run is cut
  short, torch+unsloth land but the server stack is missing and `unsloth studio`
  dies at launch on ModuleNotFoundError. Import-check the stack after the torch.cuda
  probe and, if absent, install it WITHOUT re-pinning huggingface-hub/transformers/
  datasets, so the verified GPU torch path is never disturbed.
- Seed pip into the (uv-managed, pip-less) venv via ensurepip so save_pretrained_gguf
  -> check_pip() works regardless of how Studio is launched.
- Auto-build a CUDA llama-server for GGUF inference in the background via the new
  provision script (below), so GGUF chat/tool-calling lights up a few minutes after
  install with zero manual steps. Opt out with UNSLOTH_NO_LLAMA_CUDA=1.

studio/scripts/provision_llama_cuda.sh (new):
- Idempotent, best-effort (always exits 0). Builds a CUDA llama.cpp into
  ~/.unsloth/llama.cpp (Studio's resolver path). Generic across NVIDIA Linux/WSL
  incl. aarch64 (DGX Spark, N1X): derives the arch from the GPU's compute_cap,
  installs gcc-14 + CUDA 13.3 only when nvcc is missing (gcc-15 is rejected by nvcc;
  CUDA <13.3 hits the glibc>=2.41 rsqrt header clash), and builds the full target set
  (llama-server llama-cli llama-quantize llama-mtmd-cli llama-gguf-split) so it
  satisfies both Studio inference and save_pretrained_gguf without a later rebuild.

Validated on an NVIDIA N1X (sm_121): training, GPU inference, GGUF q4_k_m export,
`unsloth studio` via both Desktop + Start Menu shortcuts (HTTP 200), GGUF chat at
121 tok/s (BLACKWELL_NATIVE_FP4=1) and OpenAI-style tool-calling.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 06:06:25 -07:00
Daniel Han
b8f8fbffbf WSL fallback: fix blank shortcut icons + complete uninstall of WSL-fallback artifacts
- install.ps1: refresh the shell icon cache (ie4uinit -show) right after creating the
  Desktop/Start Menu shortcuts, so the (valid) .ico renders immediately instead of showing
  a blank icon (Explorer caches per-.lnk icons; programmatically-created links need a poke).
- scripts/uninstall.ps1 + scripts/uninstall.sh: also remove the WSL-fallback artifacts the
  native uninstall missed -- the %LOCALAPPDATA%\Unsloth shim/launcher/icon dir, its user-PATH
  entry, and the real Studio install inside each WSL distro (rm ~/.unsloth + any CUDA llama
  build). Previously the native uninstaller only cleaned the (empty) native venv + .lnk files.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 04:46:30 -07:00
Daniel Han
bb9b7341de WSL fallback: create Desktop + Start Menu shortcuts that launch WSL Studio
The fallback returns before install.ps1's native shortcut code, so it created no
shortcuts. Add a WSL launcher (launch-studio-wsl.ps1) plus Desktop and Start Menu
.lnk shortcuts that start `unsloth studio` inside WSL and open http://localhost:8888
in the browser once the backend is healthy. Best-effort + wrapped in try/catch so a
shortcut failure never aborts the install.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 04:23:46 -07:00
Daniel Han
8fbd8e3fa2 Harden Windows-on-Arm WSL fallback: native-CUDA probe + WSL-forwarding unsloth shim
- Future-proof: probe whether a CUDA torch wheel is installable natively for win_arm64
  (uv pip install --dry-run). If it resolves (NVIDIA ships the wheel) keep the NATIVE
  install; otherwise fall back to WSL. WSL is used ONLY when native genuinely can't.
- Create a native Windows unsloth.cmd shim (on user PATH) that forwards every
  "unsloth ..." into the WSL GPU env, so "unsloth studio" / "unsloth studio run" typed
  in PowerShell run inside WSL and stream output + the http://localhost:8888 URL back.
- Run the WSL install under Continue-EAP and verify torch.cuda before reporting success
  so the optional (aarch64) llama.cpp prebuilt failure cannot abort or mis-report.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 04:13:12 -07:00
Daniel Han
b7e9dae027 feat(install): Windows-on-Arm + NVIDIA WSL2 fallback; glibc>=2.41/CUDA<13.3 build diagnostic
Two independent, purely-additive changes for Grace-Blackwell aarch64 (NVIDIA N1X
"RTX Spark" and DGX-Spark-class) and new-glibc hosts. 78 insertions, 0 deletions.

studio/setup.sh: when the GPU llama.cpp source build runs on glibc >= 2.41 with a
CUDA toolkit < 13.3, nvcc fails on the rsqrt/rsqrtf <crt/math_functions.h> exception-
spec clash (fixed upstream in CUDA 13.3 via _NV_RSQRT_SPECIFIER) and the build silently
falls back to CPU. Add a clear diagnostic recommending CUDA >= 13.3. Diagnostic only,
strictly inside the existing NVIDIA CUDA branch (Metal/ROCm/CPU/x86 unaffected).

install.ps1: native Windows-ARM64 has no CUDA PyTorch / Triton wheels, so the native
install can't deliver GPU. When ARM64 + NVIDIA is detected, automatically set up WSL2
and run the Linux installer there (full GPU), then print the launch command. Strictly
gated on ARM64 && NVIDIA && not --no-torch; x86_64 Windows (NVIDIA/AMD) and
ARM64-without-NVIDIA are byte-for-byte unchanged. Opt out: UNSLOTH_NO_WSL_FALLBACK=1;
distro: UNSLOTH_WSL_DISTRO. Encoding-proof distro detection via 'wsl -d <d> -- true'.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 03:59:47 -07:00
15 changed files with 1909 additions and 35 deletions

View file

@ -49,6 +49,12 @@ function Install-UnslothStudio {
}
}
# raw.githubusercontent.com ref for install assets; UNSLOTH_INSTALL_REF overrides 'main' for pre-merge testing.
function Get-UnslothInstallRef {
if ($env:UNSLOTH_INSTALL_REF -and $env:UNSLOTH_INSTALL_REF.Trim()) { return $env:UNSLOTH_INSTALL_REF.Trim() }
return 'main'
}
function Get-TauriTorchIndexFamily {
param([string]$TorchIndexUrl)
if ($SkipTorch) { return "none" }
@ -93,6 +99,13 @@ function Install-UnslothStudio {
if ($TauriMode) {
exit $Code
}
# -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
}
$global:LASTEXITCODE = $Code
throw $Message
}
@ -610,7 +623,7 @@ function Install-UnslothStudio {
if ($PSScriptRoot -and $PSScriptRoot.Trim()) {
$bundledIcon = Join-Path $PSScriptRoot "studio\frontend\public\unsloth.ico"
}
$iconUrl = "https://raw.githubusercontent.com/unslothai/unsloth/main/studio/frontend/public/unsloth.ico"
$iconUrl = "https://raw.githubusercontent.com/unslothai/unsloth/$(Get-UnslothInstallRef)/studio/frontend/public/unsloth.ico"
if (-not (Test-Path -LiteralPath $appDir)) {
[System.IO.Directory]::CreateDirectory($appDir) | Out-Null
@ -2189,6 +2202,465 @@ exit 0
(-not [string]::IsNullOrWhiteSpace($env:UNSLOTH_TORCH_INDEX_FAMILY))
$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 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.
if (-not $_winArm64) {
try { if ((@(Get-CimInstance Win32_Processor -ErrorAction Stop))[0].Architecture -eq 12) { $_winArm64 = $true } } catch {}
}
if (-not $_winArm64) {
try {
$_machArch = (Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Environment' -Name PROCESSOR_ARCHITECTURE -ErrorAction Stop).PROCESSOR_ARCHITECTURE
if ($_machArch -ieq 'ARM64') { $_winArm64 = $true }
} catch {}
}
$_nativeCudaTorchOk = $false
if ($_winArm64 -and $HasNvidiaSmi -and (-not $SkipTorch)) {
# uv resolves for the interpreter's platform tags, so an x64-emulated venv
# 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 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.
$global:LASTEXITCODE = -1
try {
& uv pip install --python $VenvPython --dry-run --reinstall "torch>=2.4,<2.11.0" --default-index $TorchIndexUrl *> $null
$_nativeCudaTorchOk = ($LASTEXITCODE -eq 0)
} catch { $_nativeCudaTorchOk = $false } finally { $ErrorActionPreference = $prevEapProbe }
if ($_nativeCudaTorchOk) { step "gpu" "native CUDA PyTorch now available for win_arm64 -- keeping native install" "Green" }
}
}
if ($_winArm64 -and $HasNvidiaSmi -and (-not $_nativeCudaTorchOk) -and (-not $SkipTorch) -and ($env:UNSLOTH_NO_WSL_FALLBACK -ne '1')) {
step "wsl" "Windows on ARM + NVIDIA, native CUDA unavailable -- routing GPU setup through WSL2"
substep "no win_arm64 CUDA PyTorch/Triton yet; WSL2 delivers full GPU (DGX Spark / RTX Spark path)." "Yellow"
# 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 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, 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='<branch>'; irm https://unsloth.ai/install.ps1 | iex" 1)
}
# 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 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)
}
$wslReady = $false
if (Get-Command wsl.exe -ErrorAction SilentlyContinue) {
# 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 {}
}
if (-not $wslReady) {
$isAdmin = $false
try { $isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator) } catch {}
step "wsl" "WSL2 isn't enabled yet -- one-time setup (needs admin + reboot)" "Yellow"
if ($isAdmin) {
substep "enabling WSL2..." "Cyan"
try { & wsl.exe --install --no-launch } catch {}
substep "WSL2 enabled. REBOOT, then re-run: irm https://unsloth.ai/install.ps1 | iex" "Green"
} else {
substep "in an ADMINISTRATOR PowerShell run: wsl --install" "Cyan"
substep "reboot, then re-run: irm https://unsloth.ai/install.ps1 | iex" "Cyan"
}
# 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, 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
$global:LASTEXITCODE = -1
try { & wsl.exe -d $distro -- true *> $null; if ($LASTEXITCODE -eq 0) { $haveDistro = $true } } catch {}
if (-not $haveDistro) {
substep "installing WSL distro '$distro' (first time only)..." "Cyan"
# 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 (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
$global:LASTEXITCODE = -1
try { & wsl.exe -d $distro -u root -- bash -c $_wsl2Probe *> $null; $_isWsl2 = ($LASTEXITCODE -eq 0) } catch {}
if (-not $_isWsl2) {
substep "distro '$distro' looks like WSL1 (no GPU passthrough) -- converting to WSL2 (one-time; can take a few minutes)..." "Yellow"
$global:LASTEXITCODE = -1
try { & wsl.exe --set-version $distro 2 } catch {}
$global:LASTEXITCODE = -1
try { & wsl.exe -d $distro -u root -- bash -c $_wsl2Probe *> $null; $_isWsl2 = ($LASTEXITCODE -eq 0) } catch {}
if (-not $_isWsl2) {
Restore-StudioVenvRollback
return (Exit-InstallFailure "WSL distro '$distro' is WSL1 and automatic conversion failed; NVIDIA GPU passthrough needs WSL2. Convert it, then re-run the installer: wsl --set-version `"$distro`" 2" 1)
}
substep "'$distro' converted to WSL2." "Green"
}
substep "installing Unsloth Studio inside WSL '$distro' with full GPU (this downloads PyTorch)..." "Cyan"
# 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, 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;
# 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 (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 (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 (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 (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; 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 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 (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 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 {
$wslInstall = $_fwdEnv + 'export DEBIAN_FRONTEND=noninteractive UNSLOTH_WSL_LLAMA_DEFERRED=1; export UNSLOTH_INSTALL_REF=' + $_instRef + '; apt-get update -y >/dev/null; apt-get install -y build-essential cmake git curl pciutils libcurl4-openssl-dev >/dev/null; curl -fsSL https://raw.githubusercontent.com/unslothai/unsloth/' + $_instRef + '/install.sh -o /root/.unsloth/unsloth-install.sh || exit 86; sh /root/.unsloth/unsloth-install.sh' + $_shArgs
}
# install.sh may exit non-zero on the optional llama.cpp prebuilt step (no aarch64 prebuilt)
# even though torch + unsloth + Studio install; lower EAP so it doesn't abort under Stop.
$prevEapWsl = $ErrorActionPreference
$ErrorActionPreference = "Continue"
$global:LASTEXITCODE = -1
try {
& wsl.exe -d $distro --cd /root -u root -- bash -lc $wslInstall
$wslRc = $LASTEXITCODE
} finally {
$ErrorActionPreference = $prevEapWsl
}
Write-Host ""
# 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 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 step even on success; verify torch.cuda directly.
$torchOk = $false
$prevEapChk = $ErrorActionPreference
$ErrorActionPreference = "Continue"
# 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 }
# 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
try { & wsl.exe -d $distro --cd /root -u root -- test -x /root/.unsloth/studio/unsloth_studio/bin/unsloth *> $null } catch {}
$ErrorActionPreference = $prevEapCli
if ($LASTEXITCODE -ne 0) {
substep "WSL install incomplete: 'unsloth' CLI missing (install.sh cut short after PyTorch) -- not creating a dangling shim." "Yellow"
$torchOk = $false
}
}
# 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
try { & wsl.exe -d $distro --cd /root -u root -- test -f /root/.unsloth/.install-ok *> $null } catch {}
$ErrorActionPreference = $prevEapStamp
if ($LASTEXITCODE -ne 0) {
substep "WSL install did not complete its core steps this run (no completion stamp; inner exit $wslRc) -- the venv passing the probes is from a previous install." "Yellow"
$torchOk = $false
}
}
# Self-heal web-server deps: a cut-short "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
$prevEapS = $ErrorActionPreference; $ErrorActionPreference = "Continue"
try {
& wsl.exe -d $distro --cd /root -u root -- $_studioPy -c "import structlog, fastapi, uvicorn, starlette" *> $null
$_serverOk = ($LASTEXITCODE -eq 0)
} catch {} finally { $ErrorActionPreference = $prevEapS }
if (-not $_serverOk) {
substep "Studio web-server deps incomplete (install.sh step cut short) -- installing them now..." "Cyan"
# 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"
try { & wsl.exe -d $distro --cd /root -u root -- bash -lc $_repair } catch {} finally { $ErrorActionPreference = $prevEapR }
$prevEapS2 = $ErrorActionPreference; $ErrorActionPreference = "Continue"
try {
& wsl.exe -d $distro --cd /root -u root -- $_studioPy -c "import structlog, fastapi, uvicorn, starlette" *> $null
$_serverOk = ($LASTEXITCODE -eq 0)
} catch {} finally { $ErrorActionPreference = $prevEapS2 }
if ($_serverOk) { substep "Studio web-server deps installed." "Green" }
else {
# 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.
$prevEapP = $ErrorActionPreference; $ErrorActionPreference = "Continue"
try {
& wsl.exe -d $distro --cd /root -u root -- $_studioPy -m pip --version *> $null
if ($LASTEXITCODE -ne 0) {
& wsl.exe -d $distro --cd /root -u root -- $_studioPy -m ensurepip --upgrade *> $null
}
} catch {} finally { $ErrorActionPreference = $prevEapP }
}
if ($torchOk) {
step "done" "Unsloth Studio installed in WSL '$distro' -- GPU ready (torch.cuda available)." "Green"
# Native Windows `unsloth` shim 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
$shimLines = @(
'@echo off',
# $_distroArg: pre-quoted only when spaced (wsl.exe quoting rule above).
"wsl.exe -d $_distroArg -u root -- /root/.unsloth/studio/unsloth_studio/bin/unsloth %*"
)
Set-Content -LiteralPath (Join-Path $shimDir "unsloth.cmd") -Value $shimLines -Encoding ASCII
# Record the distro 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 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 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"
if ((Test-Path -LiteralPath $staleNativeShim) -and -not (Test-Path -LiteralPath $staleNativeTarget)) {
Remove-Item -LiteralPath $staleNativeShim -Force -ErrorAction Stop
}
} catch {}
step "shim" "created native 'unsloth' command -> forwards to WSL '$distro'" "Green"
substep "open a NEW terminal, then (no WSL knowledge needed):" "Cyan"
substep " unsloth studio # runs in WSL; opens http://localhost:8888" "Cyan"
substep " unsloth studio run # also forwarded into WSL" "Cyan"
} catch {
substep "(shim creation failed; launch manually): wsl -d $_distroArg -u root -- bash -lic 'unsloth studio -p 8888'" "Yellow"
}
# Desktop + Start Menu shortcuts: launch the WSL Studio and open the browser when ready.
try {
$appDir = Join-Path $env:LOCALAPPDATA "Unsloth"
New-Item -ItemType Directory -Force -Path $appDir *> $null
$launcher = Join-Path $appDir "launch-studio-wsl.ps1"
$L = @(
'$ErrorActionPreference = "SilentlyContinue"',
('$distro = "' + $distro + '"'),
# 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 }',
'Start-Job -ArgumentList $port { param($port) for ($i=0; $i -lt 120; $i++) { try { if ((Invoke-WebRequest "http://localhost:$port/api/health" -UseBasicParsing -TimeoutSec 2).StatusCode -eq 200) { Start-Process "http://localhost:$port"; break } } catch {}; Start-Sleep 1 } } | Out-Null',
'Write-Host "Starting Unsloth Studio in WSL ($distro); browser opens at http://localhost:$port when ready (Ctrl+C to stop)..."',
'wsl.exe -d $distro --cd /root -u root -- bash -lic "unsloth studio -p $port"'
)
Set-Content -LiteralPath $launcher -Value $L -Encoding UTF8
# Icon must live OUTSIDE %LOCALAPPDATA%: on WoA the sandboxed icon broker
# can't read a .ico 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. 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)) {
try { Copy-Item -LiteralPath $bundledIcon -Destination $icon -Force } catch {}
} elseif (-not (Test-Path -LiteralPath $icon)) {
try { Invoke-WebRequest "https://raw.githubusercontent.com/unslothai/unsloth/$(Get-UnslothInstallRef)/studio/frontend/public/unsloth.ico" -OutFile $icon -UseBasicParsing -TimeoutSec 15 *> $null } catch {}
}
$hasValidIcon = $false
if (Test-Path -LiteralPath $icon) {
try {
$ib = [System.IO.File]::ReadAllBytes($icon)
if ($ib.Length -ge 4 -and $ib[0] -eq 0 -and $ib[1] -eq 0 -and $ib[2] -eq 1 -and $ib[3] -eq 0) { $hasValidIcon = $true }
else { Remove-Item -LiteralPath $icon -Force -ErrorAction SilentlyContinue }
} catch { Remove-Item -LiteralPath $icon -Force -ErrorAction SilentlyContinue }
}
$wsh = New-Object -ComObject WScript.Shell
$lnks = @()
$dd = [Environment]::GetFolderPath("Desktop"); if ($dd -and $dd.Trim()) { $lnks += (Join-Path $dd "Unsloth Studio.lnk") }
if ($env:APPDATA -and $env:APPDATA.Trim()) { $smd = Join-Path $env:APPDATA "Microsoft\Windows\Start Menu\Programs"; New-Item -ItemType Directory -Force -Path $smd *> $null; $lnks += (Join-Path $smd "Unsloth Studio.lnk") }
foreach ($lnk in $lnks) {
$sc = $wsh.CreateShortcut($lnk)
$sc.TargetPath = (Join-Path $env:SystemRoot "System32\WindowsPowerShell\v1.0\powershell.exe")
$sc.Arguments = "-NoExit -NoProfile -ExecutionPolicy Bypass -File `"$launcher`""
$sc.WorkingDirectory = $appDir
if ($hasValidIcon) { $sc.IconLocation = "$icon,0" }
$sc.Description = "Unsloth Studio (GPU via WSL)"
$sc.Save()
}
step "shortcuts" "created Desktop + Start Menu shortcuts (launch WSL Studio + open browser)" "Green"
# 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): 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)
} catch {}
} 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. 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 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 <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, 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"
$_runnerB64 = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($_runner))
$_fetchCmd = 'mkdir -p /root/.unsloth; if curl -fsSL "' + $_llamaUrl + '" -o /root/.unsloth/provision_llama_cuda.sh && [ -s /root/.unsloth/provision_llama_cuda.sh ]; then chmod +x /root/.unsloth/provision_llama_cuda.sh; echo ' + $_runnerB64 + ' | base64 -d > /root/.unsloth/run_llama_build.sh; chmod +x /root/.unsloth/run_llama_build.sh; echo PROV_FETCHED; else echo PROV_NOSCRIPT; fi'
$_fetchOut = & wsl.exe -d $distro --cd /root -u root -- bash -lc $_fetchCmd 2>$null
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 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 {
substep "(GGUF inference needs a CUDA llama.cpp build; build later: wsl -d $_distroArg -u root -- bash ~/.unsloth/provision_llama_cuda.sh)" "Yellow"
}
} catch {} finally { $ErrorActionPreference = $prevEapL }
}
} else {
step "wsl" "WSL Studio install did not finish cleanly (torch.cuda not detected; inner exit $wslRc) -- see log above." "Yellow"
substep "retry, or launch manually: wsl -d $_distroArg -u root -- bash -lic 'unsloth studio -p 8888'" "Cyan"
}
if ($torchOk) {
# 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 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)")
}
# ── GPU arch → newest compatible Windows ROCm wheel release ──
# Wheels bundle their own ROCm runtime; the installed HIP SDK version does
# not constrain which release to use. Always picks the newest release that

View file

@ -1042,9 +1042,9 @@ _open_browser() {
elif grep -qi microsoft /proc/version 2>/dev/null; then
# WSL: xdg-open is unreliable; use Windows browser via PowerShell or cmd
if command -v powershell.exe >/dev/null 2>&1; then
powershell.exe -NoProfile -Command "Start-Process '$_url'" >/dev/null 2>&1 &
powershell.exe -NoProfile -Command "Start-Process '$_url'" </dev/null >/dev/null 2>&1 &
elif command -v cmd.exe >/dev/null 2>&1; then
cmd.exe /c start "" "$_url" >/dev/null 2>&1 &
cmd.exe /c start "" "$_url" </dev/null >/dev/null 2>&1 &
elif command -v xdg-open >/dev/null 2>&1; then
xdg-open "$_url" >/dev/null 2>&1 &
else
@ -1485,7 +1485,8 @@ STUB_EOF
fi
_css_created=1
elif [ "$_css_os" = "wsl" ]; then
elif [ "$_css_os" = "wsl" ] && [ "${UNSLOTH_SKIP_WSL_WINDOWS_SHORTCUT:-0}" != "1" ] \
&& [ ! -f "$HOME/.unsloth/.skip-wsl-windows-shortcut" ]; then
# ── WSL: create Windows Desktop and Start Menu shortcuts ──
# Detect current WSL distro for targeted shortcut
_css_distro="${WSL_DISTRO_NAME:-}"
@ -1533,9 +1534,11 @@ STUB_EOF
\$WshShell = New-Object -ComObject WScript.Shell
\$targetExe = (Get-Command '$_css_sc_target' -ErrorAction SilentlyContinue).Source
if (-not \$targetExe) { exit 1 }
# Best-effort: fetch the Unsloth icon to a stable Windows path (shared with a
# native install if one exists) so the WSL shortcut shows the proper icon.
\$iconDir = Join-Path \$env:LOCALAPPDATA 'Unsloth Studio'
# Best-effort: fetch the Unsloth icon to a stable Windows path so the shortcut
# shows the proper icon. Use %USERPROFILE%\.unsloth, NOT %LOCALAPPDATA%: on
# Windows-on-ARM the sandboxed icon broker can't read a standalone .ico under
# AppData\Local (renders blank); a profile-path icon renders everywhere.
\$iconDir = Join-Path \$env:USERPROFILE '.unsloth'
\$iconPath = Join-Path \$iconDir 'unsloth.ico'
\$preIconHash = \$null
if (Test-Path -LiteralPath \$iconPath) {
@ -1611,7 +1614,7 @@ WSLPS1_EOF
# Convert WSL path to Windows path for powershell.exe
_css_ps1_win=$(wslpath -w "$_css_ps1_tmp" 2>/dev/null)
if [ -n "$_css_ps1_win" ]; then
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "$_css_ps1_win" >/dev/null 2>&1 && _css_created=1
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "$_css_ps1_win" </dev/null >/dev/null 2>&1 && _css_created=1
fi
rm -f "$_css_ps1_tmp"
fi
@ -1799,6 +1802,11 @@ _has_usable_nvidia_gpu() {
_nvsmi=""
if command -v nvidia-smi >/dev/null 2>&1; then
_nvsmi="nvidia-smi"
elif [ -x "/usr/lib/wsl/lib/nvidia-smi" ]; then
# WSL2 GPU-PV ships nvidia-smi ONLY here, and root login shells drop
# the dir from PATH; without this fallback the WSL install detects no
# NVIDIA GPU and picks CPU torch wheels.
_nvsmi="/usr/lib/wsl/lib/nvidia-smi"
elif [ -x "/usr/bin/nvidia-smi" ]; then
_nvsmi="/usr/bin/nvidia-smi"
fi
@ -2501,6 +2509,9 @@ get_torch_index_url() {
_nvidia_detected=1
if command -v nvidia-smi >/dev/null 2>&1; then
_smi="nvidia-smi"
elif [ -x "/usr/lib/wsl/lib/nvidia-smi" ]; then
# Same WSL2 GPU-PV location fallback as _has_usable_nvidia_gpu.
_smi="/usr/lib/wsl/lib/nvidia-smi"
elif [ -x "/usr/bin/nvidia-smi" ]; then
_smi="/usr/bin/nvidia-smi"
fi
@ -3945,6 +3956,14 @@ elif [ -n "$TORCH_INDEX_URL" ]; then
run_install_cmd_retry "overlay unsloth-zoo (git main)" uv pip install --python "$_VENV_PY" \
--no-deps --reinstall-package unsloth-zoo \
"unsloth-zoo @ git+https://github.com/unslothai/unsloth-zoo"
elif [ -n "${UNSLOTH_INSTALL_REF:-}" ] && [ "${UNSLOTH_INSTALL_REF}" != "main" ] && [ "$PACKAGE_NAME" = "unsloth" ]; then
# Pre-merge testing: install unsloth from a git ref (set by install.ps1)
# so the branch's setup.sh + patches run. Name unsloth-zoo explicitly --
# not a base dep, and SKIP_STUDIO_BASE skips base.txt, so it never installs.
substep "installing unsloth from git ref '$UNSLOTH_INSTALL_REF'..."
run_install_cmd "install unsloth (@$UNSLOTH_INSTALL_REF)" uv pip install --python "$_VENV_PY" \
--upgrade-package unsloth --upgrade-package unsloth-zoo \
"unsloth @ git+https://github.com/unslothai/unsloth@${UNSLOTH_INSTALL_REF}" unsloth-zoo
else
run_install_cmd_retry "install unsloth" uv pip install --python "$_VENV_PY" \
${_UNSLOTH_TORCH_OVERRIDES:+--overrides "$_UNSLOTH_TORCH_OVERRIDES"} \
@ -3952,6 +3971,26 @@ elif [ -n "$TORCH_INDEX_URL" ]; then
fi
[ -n "$_UNSLOTH_TORCH_OVERRIDES" ] && rm -f "$_UNSLOTH_TORCH_OVERRIDES"
_UNSLOTH_TORCH_OVERRIDES=""
# aarch64 + NVIDIA (DGX Spark / GB10 / N1X): unsloth's x86_64-oriented cuXXX
# extras break 4-bit QLoRA, but aarch64 manylinux wheels work (verified on
# sm_121 via PTX JIT). Best-effort: no wheel keeps 16-bit LoRA / full finetuning.
# SKIP_TORCH gate stops a --no-torch (GGUF-only) install dragging torch back in.
# nvidia-smi may live only in /usr/lib/wsl/lib (WSL2 GPU-PV), which root login
# shells drop from PATH -- resolve explicitly (same order as setup.sh's
# _resolve_nvsmi) so the WoA/WSL install still gets 4-bit QLoRA support.
_bnb_nvsmi="$(command -v nvidia-smi 2>/dev/null || true)"
[ -z "$_bnb_nvsmi" ] && [ -x /usr/lib/wsl/lib/nvidia-smi ] && _bnb_nvsmi=/usr/lib/wsl/lib/nvidia-smi
[ -z "$_bnb_nvsmi" ] && [ -x /usr/bin/nvidia-smi ] && _bnb_nvsmi=/usr/bin/nvidia-smi
if [ "$SKIP_TORCH" = false ] \
&& { [ "$(uname -m)" = "aarch64" ] || [ "$(uname -m)" = "arm64" ]; } \
&& [ -n "$_bnb_nvsmi" ] \
&& "$_bnb_nvsmi" -L 2>/dev/null | awk '/^GPU[[:space:]]+[0-9]+:/{found=1} END{exit !found}' \
&& ! "$_VENV_PY" -c "import bitsandbytes" >/dev/null 2>&1; then
substep "installing bitsandbytes (aarch64 wheels; enables 4-bit QLoRA)..."
if ! uv pip install --python "$_VENV_PY" "bitsandbytes>=0.45.5,!=0.46.0,!=0.48.0" >/dev/null 2>&1; then
substep "(no bitsandbytes wheel for this platform; 16-bit LoRA + full finetuning still work)"
fi
fi
# AMD ROCm: repair torch if the unsloth/unsloth-zoo install pulled in
# CUDA torch from PyPI, overwriting the ROCm wheels installed in Step 1.
if [ "$SKIP_TORCH" = false ] && [ "$_torch_index_is_rocm_family" = true ]; then

View file

@ -47,6 +47,7 @@ studio = [
"*.sh",
"*.ps1",
"*.bat",
"scripts/*.sh",
"node_prebuilt_pins.json",
"frontend/dist/**/*",
"frontend/*.json",

View file

@ -16,6 +16,25 @@ 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: 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 { }
if (-not $arm) {
try { if ((@(Get-CimInstance Win32_Processor -ErrorAction Stop))[0].Architecture -eq 12) { $arm = $true } } catch { }
}
if (-not $arm) {
try {
$machArch = (Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Environment' -Name PROCESSOR_ARCHITECTURE -ErrorAction Stop).PROCESSOR_ARCHITECTURE
if ($machArch -ieq 'ARM64') { $arm = $true }
} catch { }
}
return $arm
}
# Remove a file/dir/symlink if present. Idempotent; retries since a just-killed
# process can briefly hold a handle (Windows refuses the delete until released).
function _RemovePath {
@ -399,13 +418,52 @@ function Uninstall-UnslothStudio {
}
# ── Remove desktop and Start Menu shortcuts ──
# 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 }
try {
$desktop = [Environment]::GetFolderPath("Desktop")
if ($desktop) { _RemovePath (Join-Path $desktop "Unsloth Studio.lnk") }
if ($env:LOCALAPPDATA) {
$_scDf = Join-Path (Join-Path $env:LOCALAPPDATA "Unsloth") "wsl-distro.txt"
if (Test-Path -LiteralPath $_scDf) {
$_scRd = (Get-Content -LiteralPath $_scDf -ErrorAction SilentlyContinue | Select-Object -First 1)
if ($_scRd -and $_scRd.Trim()) { $_scCands += $_scRd.Trim() }
}
}
} catch { }
if ($env:APPDATA) {
_RemovePath (Join-Path $env:APPDATA "Microsoft\Windows\Start Menu\Programs\Unsloth Studio.lnk")
if ((-not $_scCands) -and (_IsArm64Host)) {
$_scCands = @('Ubuntu', 'Ubuntu-24.04', 'Ubuntu-22.04', 'Debian')
}
$_scWs = $null
try { $_scWs = New-Object -ComObject WScript.Shell } catch { }
$shortcutDirs = @()
try { $d = [Environment]::GetFolderPath("Desktop"); if ($d) { $shortcutDirs += $d } } catch { }
if ($env:APPDATA) { $shortcutDirs += (Join-Path $env:APPDATA "Microsoft\Windows\Start Menu\Programs") }
foreach ($dir in $shortcutDirs) {
if (-not (Test-Path -LiteralPath $dir)) { continue }
_RemovePath (Join-Path $dir "Unsloth Studio.lnk")
Get-ChildItem -LiteralPath $dir -Filter "Unsloth Studio (*.lnk" -ErrorAction SilentlyContinue | ForEach-Object {
$_scKeep = $false
if ($_scWs) {
try {
$_sc = $_scWs.CreateShortcut($_.FullName)
if ("$($_sc.TargetPath) $($_sc.Arguments)" -match "wsl\.exe") {
$_scD = $null
# 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] }
}
elseif ($_.Name -match '^Unsloth Studio \(WSL - (.+)\)\.lnk$') { $_scD = $Matches[1] }
if ($_scD -and ($_scCands -notcontains $_scD)) { $_scKeep = $true }
}
} catch { }
}
if (-not $_scKeep) { _RemovePath $_.FullName }
}
}
# Invalidate the Win11 Start Menu tile cache so the removed shortcut's tile
# disappears promptly instead of lingering stale (mirrors install.ps1's
@ -480,6 +538,102 @@ function Uninstall-UnslothStudio {
Remove-Item -LiteralPath 'HKCU:\Software\Unsloth' -Recurse -Force -ErrorAction SilentlyContinue
} catch { }
# ── Windows-on-Arm WSL-fallback artifacts ──
# The ARM64+NVIDIA fallback puts Studio in WSL plus a native shim + launcher under
# %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.
$_recordedDistro = $null
if ($unslothDir) {
try {
$_distroFile = Join-Path $unslothDir "wsl-distro.txt"
if (Test-Path -LiteralPath $_distroFile) {
$_recordedDistro = (Get-Content -LiteralPath $_distroFile -ErrorAction SilentlyContinue | Select-Object -First 1)
if ($_recordedDistro) { $_recordedDistro = $_recordedDistro.Trim() }
}
} catch { }
}
if ($unslothDir) {
$shimDir = (Join-Path $unslothDir "bin").TrimEnd('\', '/')
try {
$rk = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey('Environment', $true)
if ($rk) {
try {
$rp = $rk.GetValue('Path', '', [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)
if ($rp) {
$kept = @(); $removed = $false
foreach ($e in ($rp -split ';')) {
if ([string]::IsNullOrWhiteSpace($e)) { continue }
if (([Environment]::ExpandEnvironmentVariables($e).TrimEnd('\', '/')) -ieq $shimDir) { $removed = $true; _Substep "removed PATH entry: $e" "Green"; continue }
$kept += $e
}
if ($removed) { $rk.SetValue('Path', ($kept -join ';'), [Microsoft.Win32.RegistryValueKind]::ExpandString) }
}
} finally { $rk.Close() }
}
} catch { }
_RemovePath $unslothDir
}
# The WoA shortcut icon lives under the user profile (icon broker can't read
# 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) {
if ($_icoDir -and (Test-Path -LiteralPath $_icoDir) -and
(Get-ChildItem -LiteralPath $_icoDir -Filter "Unsloth Studio*.lnk" -ErrorAction SilentlyContinue)) {
$_icoInUse = $true; break
}
}
if (-not $_icoInUse) { _RemovePath (Join-Path $env:USERPROFILE ".unsloth\unsloth.ico") }
}
# The ~/.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 (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 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 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 }
if ((-not $_cands) -and (_IsArm64Host)) {
$_cands = @('', 'Ubuntu', 'Ubuntu-24.04', 'Ubuntu-22.04', 'Debian')
}
$_done = @{}
foreach ($d in $_cands) {
if ($d) { & wsl.exe -d $d -- true *> $null } else { & wsl.exe -- true *> $null }
if ($LASTEXITCODE -ne 0) { continue }
$_label = if ($d) { $d } else { "(default)" }
if ($_done[$_label]) { continue }
$_done[$_label] = $true
if ($d) { & wsl.exe -d $d -u root -- bash -lc $_clean *> $null }
else { & wsl.exe -u root -- bash -lc $_clean *> $null }
_Substep "cleaned Unsloth from WSL distro: $_label" "Green"
}
} catch { }
}
Write-Host ""
Write-Host "Unsloth Studio uninstalled."
Write-Host "Note: Hugging Face model cache at %USERPROFILE%\.cache\huggingface was left in place."
@ -491,6 +645,10 @@ function Uninstall-UnslothStudio {
Write-Host "set to also remove that install tree, e.g.:"
Write-Host " `$env:UNSLOTH_STUDIO_HOME = 'C:\your\path'; irm https://raw.githubusercontent.com/unslothai/unsloth/main/scripts/uninstall.ps1 | iex"
}
# 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
}
Uninstall-UnslothStudio @args

View file

@ -212,10 +212,52 @@ _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 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: 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"
for _pat in "run_llama_build\.sh" "provision_llama_cuda\.sh" "$_llama_re"; do
for _pid in $(pgrep -f "$_pat" 2>/dev/null); do
_pgid=$(ps -o pgid= -p "$_pid" 2>/dev/null | tr -d '[:space:]')
case "$_pgid" in
''|0|1|"$_self_pgid")
pkill "-$_sig" -P "$_pid" 2>/dev/null || true
kill -s "$_sig" "$_pid" 2>/dev/null || true ;;
*) kill -s "$_sig" -- "-$_pgid" 2>/dev/null \
|| kill -s "$_sig" "$_pid" 2>/dev/null || true ;;
esac
done
done
}
_kill_llama_build TERM
sleep 0.5
_kill_llama_build KILL
fi
# Default-mode shared llama.cpp build + cache are siblings of studio (not removed
# by deleting it). No-op in env/custom mode (they nest under the custom root) and
# when absent. A user-set UNSLOTH_LLAMA_CPP_PATH is intentionally kept.
_remove_path "$HOME/.unsloth/llama.cpp"
# WoA/Spark CUDA-build path artifacts (provision script fetched by setup.sh,
# install.ps1's background-build runner + log, and the persisted shortcut-skip
# marker). No-ops when absent.
_remove_path "$HOME/.unsloth/provision_llama_cuda.sh"
_remove_path "$HOME/.unsloth/run_llama_build.sh"
_remove_path "$HOME/.unsloth/llama_cuda_build.log"
_remove_path "$HOME/.unsloth/.skip-wsl-windows-shortcut"
# Core-install completion stamp (written by setup.sh, checked by install.ps1's
# WSL probes). Must go, or a later reinstall could read a stale success.
_remove_path "$HOME/.unsloth/.install-ok"
_remove_path "$HOME/.unsloth/unsloth-install.sh"
_remove_path "$HOME/.unsloth/.cache"
# Isolated Node.js runtime (install_node_prebuilt.py), a sibling of studio in
# default mode. No-op in env/custom mode (nested under the custom root) and absent.
@ -281,6 +323,7 @@ case "$_os" in
# receive trailing tokens as $args. WSL distro names are safe to
# embed (no quotes/$/backtick).
# shellcheck disable=SC2016
# $env:APPDATA/$distro are PowerShell-side; $_wsl_distro is shell-injected.
powershell.exe -NoProfile -Command '$distro = "'"$_wsl_distro"'";
$dirs = @(
[Environment]::GetFolderPath("Desktop"),
@ -305,6 +348,29 @@ case "$_os" in
} catch { }
}
}
# Remove the WoA WSL-fallback native shim/launcher dir
# (%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() } }
if ($ud -and ((-not $owner) -or (-not $distro) -or ($owner -ieq $distro))) {
$shim = (Join-Path $ud "bin").TrimEnd("\","/");
$up = [Environment]::GetEnvironmentVariable("Path","User");
if ($up) { [Environment]::SetEnvironmentVariable("Path", (($up -split ";" | Where-Object { $_ -and ($_.TrimEnd("\","/") -ine $shim) }) -join ";"), "User") }
# WoA-fallback shortcuts target powershell.exe + launch-studio-wsl.ps1
# (not wsl.exe), so the sweep above keeps them; remove them here
# before their launcher dir is deleted or they would dangle.
foreach ($d in $dirs) {
if (-not $d -or -not (Test-Path -LiteralPath $d)) { continue }
$l = Join-Path $d "Unsloth Studio.lnk";
if (Test-Path -LiteralPath $l) {
try { $sc2 = $ws.CreateShortcut($l); if ($sc2.Arguments -match "launch-studio-wsl\.ps1") { Remove-Item -LiteralPath $l -Force -ErrorAction SilentlyContinue } } catch { }
}
}
if (Test-Path -LiteralPath $ud) { Remove-Item -LiteralPath $ud -Recurse -Force -ErrorAction SilentlyContinue }
}
# Keep the shared icon while any Unsloth shortcut still uses it (native
# install or another WSL distro); drop it only with the last one.
$iconInUse = $false;
@ -319,6 +385,15 @@ case "$_os" in
$ico = Join-Path $iconDir "unsloth.ico";
if ((-not $iconInUse) -and (Test-Path -LiteralPath $ico)) { Remove-Item -LiteralPath $ico -Force -ErrorAction SilentlyContinue }
if ((Test-Path -LiteralPath $iconDir) -and -not (Get-ChildItem -LiteralPath $iconDir -Force -ErrorAction SilentlyContinue)) { Remove-Item -LiteralPath $iconDir -Recurse -Force -ErrorAction SilentlyContinue }
}
# install.sh also writes the WSL shortcut icon to the Windows profile
# (%USERPROFILE%\.unsloth\unsloth.ico) 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";
if ((-not $iconInUse) -and (Test-Path -LiteralPath $pIco)) { Remove-Item -LiteralPath $pIco -Force -ErrorAction SilentlyContinue }
if ((Test-Path -LiteralPath $pIconDir) -and -not (Get-ChildItem -LiteralPath $pIconDir -Force -ErrorAction SilentlyContinue)) { Remove-Item -LiteralPath $pIconDir -Recurse -Force -ErrorAction SilentlyContinue }
}' >/dev/null 2>&1 || true
fi
# Remove $1's shared unsloth.ico only if no Unsloth shortcut (native install
@ -342,6 +417,10 @@ case "$_os" in
done
if [ "$_icon_in_use" = "0" ]; then
[ -f "$_icodir/unsloth.ico" ] && rm -f "$_icodir/unsloth.ico" 2>/dev/null || true
# install.sh also writes the icon to the Windows profile
# (%USERPROFILE%\.unsloth) for the WoA icon broker.
[ -f "$_du/.unsloth/unsloth.ico" ] && rm -f "$_du/.unsloth/unsloth.ico" 2>/dev/null || true
[ -d "$_du/.unsloth" ] && rmdir "$_du/.unsloth" 2>/dev/null || true
fi
[ -d "$_icodir" ] && rmdir "$_icodir" 2>/dev/null || true
}

View file

@ -818,6 +818,32 @@ def _rocm_classify_unified_memory(props: Any) -> tuple[str, bool]:
return gcn_arch, is_unified
def _nvidia_classify_spark_unified_memory(props: Any) -> tuple[str, bool]:
"""Classify an NVIDIA device as Spark-class unified-memory or discrete.
Returns ``(marker, is_unified)``; marker is ``"is_integrated"`` or the matched
name token, else ``""``. Spark-class parts (DGX Spark / GB10, N1X "RTX Spark")
share one memory pool with the OS, so like the ROCm APUs they need a
``set_per_process_memory_fraction`` cap -- exhausting the pool can stall the box.
``is_integrated`` is authoritative on native Linux, but WSL2 paravirtualization
masks it to 0 and renames the device (N1X reports ``JMJWOA-Generic-GPU``,
verified live) -- hence the name-token fallback. Tokens mirror
``_DGX_SPARK_DEVICE_TOKENS`` in ``unsloth/models/_utils.py`` (duplicated since
this guard runs before any ML import).
"""
if getattr(props, "is_integrated", 0):
return "is_integrated", True
name_upper = (getattr(props, "name", "") or "").upper()
import re
for token in ("GB10", "GB110", "JMJWOA", "N1X", "DGX SPARK"):
# Whole-token match so "GB10" doesn't match discrete "GB100"/"GB10X".
if re.search(r"(?<![A-Z0-9])" + re.escape(token) + r"(?![A-Z0-9])", name_upper):
return token, True
return "", False
def _tilelang_platform_supported() -> bool:
"""True iff a tilelang 0.1.8 wheel will load: Linux x86_64/aarch64, non-HIP torch.
@ -2406,6 +2432,53 @@ def run_training_process(*, event_queue: Any, stop_queue: Any, config: dict) ->
from utils.hardware import hardware as _hw
# Set PYTORCH_CUDA_ALLOC_CONF before the first CUDA touch: detect_hardware()
# below calls get_device_properties, which latches the allocator config for
# this process (verified: expandable_segments set after init is a no-op).
# CUDA-free nvidia-smi sniff (mirrors _is_dgx_spark_no_cuda_init) honoring
# UNSLOTH_FORCE_DGX_SPARK, same append-don't-override behavior and
# UNSLOTH_NO_EXPANDABLE_SEGMENTS opt-out.
try:
import platform as _plat
import re as _re
_force_spark = os.environ.get("UNSLOTH_FORCE_DGX_SPARK")
if _force_spark == "1":
_spark_smi = True
elif _force_spark == "0":
_spark_smi = False
else:
_spark_smi = False
if _plat.machine().lower() in ("aarch64", "arm64"):
import shutil as _shutil
# The WoA shim execs the venv binary directly (no login shell),
# where /usr/lib/wsl/lib can be off PATH -- resolve explicitly.
_smi_bin = "nvidia-smi"
if _shutil.which(_smi_bin) is None and os.path.exists(
"/usr/lib/wsl/lib/nvidia-smi"
):
_smi_bin = "/usr/lib/wsl/lib/nvidia-smi"
_smi = _sp.run(
[_smi_bin, "--query-gpu=name", "--format=csv,noheader"],
capture_output = True,
text = True,
timeout = 5,
)
_names_u = (_smi.stdout or "").upper()
_spark_smi = any(
_re.search(r"(?<![A-Z0-9])" + _re.escape(t) + r"(?![A-Z0-9])", _names_u)
for t in ("GB10", "GB110", "JMJWOA", "N1X", "DGX SPARK")
)
if _spark_smi and os.environ.get("UNSLOTH_NO_EXPANDABLE_SEGMENTS") != "1":
_conf = os.environ.get("PYTORCH_CUDA_ALLOC_CONF", "")
if "expandable_segments" not in _conf:
os.environ["PYTORCH_CUDA_ALLOC_CONF"] = (
_conf + "," if _conf else ""
) + "expandable_segments:True"
except Exception:
pass
_hw.detect_hardware()
if mlx_backend_requested or should_use_mlx_training_backend(device = _hw.DEVICE):
run_mlx_training_process(
@ -2903,6 +2976,57 @@ def run_training_process(*, event_queue: Any, stop_queue: Any, config: dict) ->
except Exception as _oom_guard_err:
logger.debug("Could not set GPU memory fraction: %s", _oom_guard_err)
# ── 1h. NVIDIA Spark-class unified-memory OOM guard ──
# 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:
# PYTORCH_CUDA_ALLOC_CONF is appended before detect_hardware() near the
# top of run_training_process: by here CUDA has long been initialized
# and the allocator config is latched, so only the runtime-adjustable
# memory fraction is set at this point.
import torch as _torch_mem
if _torch_mem.cuda.is_available():
_props = _torch_mem.cuda.get_device_properties(0)
# Same UNSLOTH_FORCE_DGX_SPARK override the detectors honor, so a
# forced Spark with an unlisted name still gets the fraction guard
# and FORCE=0 can disable it on a token-matched device.
_force_spark = os.environ.get("UNSLOTH_FORCE_DGX_SPARK")
if _force_spark == "1":
_marker, _is_spark_uma = "forced", True
elif _force_spark == "0":
_marker, _is_spark_uma = "forced-off", False
else:
_marker, _is_spark_uma = _nvidia_classify_spark_unified_memory(_props)
if _is_spark_uma:
_mem_fraction = 0.80
_frac_env = os.environ.get("UNSLOTH_SPARK_MEM_FRACTION")
if _frac_env:
try:
_mem_fraction = float(_frac_env)
except ValueError:
_mem_fraction = 0.80
if 0.0 < _mem_fraction <= 1.0:
_torch_mem.cuda.set_per_process_memory_fraction(_mem_fraction)
logger.info(
"Spark unified-memory OOM guard: "
"set_per_process_memory_fraction(%.2f) — %s (matched %s)",
_mem_fraction,
_props.name,
_marker,
)
else:
logger.info(
"Spark unified-memory OOM guard disabled "
"(UNSLOTH_SPARK_MEM_FRACTION=%s)",
_frac_env,
)
except Exception as _oom_guard_err:
logger.debug("Could not set GPU memory fraction: %s", _oom_guard_err)
# ── 2. Now import ML libraries (fresh in this clean process) ──
try:
_send_status(event_queue, "Importing Unsloth...")

View file

@ -119,6 +119,20 @@ 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 _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")
def _no_backend_in_tests():
raise RuntimeError("no inference backend in unit tests")
_inference_mod.get_llama_cpp_backend = _no_backend_in_tests
monkeypatch.setitem(sys.modules, "routes", _routes_pkg)
monkeypatch.setitem(sys.modules, "routes.inference", _inference_mod)
# Keep the whisper piggyback out of the llama-only tests: no host probe, no
# whisper phase (test_combined_update.py covers the chained flow).
monkeypatch.setattr(upd, "_whisper_chain_status", lambda **kwargs: None)

View file

@ -0,0 +1,92 @@
# SPDX-License-Identifier: AGPL-3.0-only
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
"""Tests for _nvidia_classify_spark_unified_memory (Spark OOM-guard classifier).
Two paths: (1) ``is_integrated`` property (authoritative on native Linux),
(2) name-token match -- needed because WSL2 GPU paravirtualization masks
``is_integrated`` to 0 and renames the device (N1X reports ``JMJWOA-Generic-GPU``;
verified live). Mirrors test_rocm_oom_guard.py, which the NVIDIA guard models.
"""
from __future__ import annotations
from types import SimpleNamespace
import pytest
from core.training.worker import _nvidia_classify_spark_unified_memory
def _props(**kwargs) -> SimpleNamespace:
"""Fake device-properties object with the given attributes."""
return SimpleNamespace(**kwargs)
# ── Path 1: is_integrated property ───────────────────────────────────────────
class TestIsIntegratedProperty:
"""``is_integrated`` truthy means unified memory, regardless of name."""
def test_integrated_native_spark(self) -> None:
props = _props(is_integrated = 1, name = "NVIDIA GB10")
marker, is_unified = _nvidia_classify_spark_unified_memory(props)
assert marker == "is_integrated"
assert is_unified is True
def test_integrated_wins_even_with_unknown_name(self) -> None:
props = _props(is_integrated = 1, name = "Some Future Unified Part")
marker, is_unified = _nvidia_classify_spark_unified_memory(props)
assert marker == "is_integrated"
assert is_unified is True
# ── Path 2: device-name token fallback (WSL masks is_integrated) ────────────
class TestDeviceNameTokenFallback:
"""is_integrated == 0 (or absent) -> classify by Spark name tokens."""
@pytest.mark.parametrize(
"name, expected_marker",
[
("JMJWOA-Generic-GPU", "JMJWOA"), # N1X under WSL2 (verified live)
("NVIDIA GB10", "GB10"), # native DGX Spark
("NVIDIA GB110", "GB110"), # "GB10" is not a substring of "GB110"
("NVIDIA DGX Spark", "DGX SPARK"),
("nvidia n1x prototype", "N1X"), # case-insensitive
],
)
def test_spark_names_unified(self, name: str, expected_marker: str) -> None:
props = _props(is_integrated = 0, name = name)
marker, is_unified = _nvidia_classify_spark_unified_memory(props)
assert is_unified is True
assert marker == expected_marker
@pytest.mark.parametrize(
"name",
[
"NVIDIA GeForce RTX 4090",
"NVIDIA H100 80GB HBM3",
"NVIDIA RTX 6000 Ada Generation",
"Tesla T4",
],
)
def test_discrete_names_not_unified(self, name: str) -> None:
props = _props(is_integrated = 0, name = name)
marker, is_unified = _nvidia_classify_spark_unified_memory(props)
assert is_unified is False
assert marker == ""
def test_missing_attrs_defaults_discrete(self) -> None:
"""No is_integrated, no name -> discrete (guard stays off)."""
marker, is_unified = _nvidia_classify_spark_unified_memory(_props())
assert is_unified is False
assert marker == ""
def test_none_name_defaults_discrete(self) -> None:
props = _props(is_integrated = 0, name = None)
marker, is_unified = _nvidia_classify_spark_unified_memory(props)
assert is_unified is False
assert marker == ""

View file

@ -10,6 +10,7 @@ import argparse
import atexit
import errno
import fnmatch
import functools
import glob
import hashlib
import json
@ -2225,6 +2226,49 @@ def _pick_rocm_gfx_target(out: str) -> str | None:
return _tokens[0]
@functools.lru_cache(maxsize = 1)
def _running_under_wsl() -> bool:
"""WSL kernels self-identify with 'microsoft' in the release string."""
try:
return "microsoft" in platform.uname().release.lower()
except Exception:
return False
def _nvidia_smi_capture(
command: list[str],
*,
attempts: int | None = None,
timeout: int | None = None,
) -> subprocess.CompletedProcess[str]:
"""run_capture for nvidia-smi probes, hardened against transient slowness.
nvidia-smi normally answers in well under a second, but under WSL2 GPU-PV it
can take far longer when the host is under heavy CPU load -- e.g. the
concurrent pip / frontend / cmake work during an `unsloth studio` install.
A single short timeout then raises TimeoutExpired, detect_host treats the
GPU as ABSENT, and the host is misrouted to a CPU prebuilt / slow source
build instead of the CUDA bundle it can actually use. Retry with a generous
per-attempt timeout there. Off WSL that slowness mode does not exist, and a
hung nvidia-smi (broken driver, revoked container GPU) would stall three
successive detect_host probes for ~2 minutes each -- so bare metal keeps a
single short attempt. Only ever reached when nvidia-smi exists on PATH, so
CPU-only hosts never incur this wait.
"""
_on_wsl = _running_under_wsl()
_attempts = max(1, attempts if attempts is not None else (2 if _on_wsl else 1))
_timeout = timeout if timeout is not None else (60 if _on_wsl else 10)
last_exc: Exception | None = None
for _attempt in range(_attempts):
try:
return run_capture(command, timeout = _timeout)
except subprocess.TimeoutExpired as exc:
last_exc = exc
if _attempt + 1 < _attempts: # don't sleep after the final attempt
time.sleep(2)
raise last_exc if last_exc is not None else RuntimeError("nvidia-smi capture failed")
# Display-adapter device class: one NNNN subkey per installed display driver
# config, each carrying the driver's DriverDesc and PCI MatchingDeviceId.
_WINDOWS_DISPLAY_CLASS_KEY = (
@ -2295,6 +2339,14 @@ def detect_host() -> HostInfo:
macos_version = parse_macos_version(platform.mac_ver()[0]) if is_macos else None
nvidia_smi = shutil.which("nvidia-smi")
if not nvidia_smi:
# Root WSL sessions drop /usr/lib/wsl/lib (the only nvidia-smi home under
# WSL2 GPU-PV) from PATH, 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
break
driver_cuda_version = None
compute_caps: list[str] = []
visible_cuda_devices = os.environ.get("CUDA_VISIBLE_DEVICES")
@ -2308,7 +2360,7 @@ def detect_host() -> HostInfo:
# container leftovers), which would otherwise misclassify an AMD
# ROCm host as NVIDIA and short-circuit the ROCm path.
try:
listing = run_capture([nvidia_smi, "-L"], timeout = 20)
listing = _nvidia_smi_capture([nvidia_smi, "-L"])
gpu_lines = [line for line in listing.stdout.splitlines() if line.startswith("GPU ")]
if gpu_lines:
has_physical_nvidia = True
@ -2317,7 +2369,7 @@ def detect_host() -> HostInfo:
pass
try:
result = run_capture([nvidia_smi], timeout = 20)
result = _nvidia_smi_capture([nvidia_smi])
merged = "\n".join(part for part in (result.stdout, result.stderr) if part)
# Newer NVIDIA drivers (e.g. 610.x on Windows) print
# "CUDA UMD Version: X.Y" instead of the legacy
@ -2335,13 +2387,12 @@ def detect_host() -> HostInfo:
pass
try:
caps = run_capture(
caps = _nvidia_smi_capture(
[
nvidia_smi,
"--query-gpu=index,uuid,compute_cap",
"--format=csv,noheader",
],
timeout = 20,
)
visible_gpu_rows: list[tuple[str, str, str]] = []
for raw in caps.stdout.splitlines():

View file

@ -0,0 +1,419 @@
#!/usr/bin/env bash
# Build CUDA llama.cpp for Studio GGUF *inference* into ~/.unsloth/llama.cpp
# (resolver checks <dir>/build/bin/llama-server). Idempotent, best-effort, exits
# 0. Exists because no aarch64+CUDA prebuilt covers NVIDIA ARM hosts (DGX Spark /
# GB10, N1X "RTX" laptops). Platform gotchas handled:
# * nvcc rejects gcc-15 -> force gcc-14 / g++-14 host compiler
# * glibc >= 2.41 vs CUDA < 13.3 -> install CUDA 13.3 (rsqrt header clash)
# * sm_121 (Blackwell) -> derive arch from the GPU's compute_cap
# Opt out with UNSLOTH_NO_LLAMA_CUDA=1 (handled by the caller).
set -uo pipefail
LLAMA_DIR="${UNSLOTH_LLAMA_CPP_PATH:-$HOME/.unsloth/llama.cpp}"
SERVER="$LLAMA_DIR/build/bin/llama-server"
log() { printf ' - %s\n' "$*"; }
# Serialize against install_llama_prebuilt.py (same lock file as its
# install_lock_path: <parent>/.<name>.install.lock; its filelock backend is
# flock(2), so this interoperates) and against a second copy of this script: the
# detached background builder can otherwise race an installer rerun or `unsloth
# studio update`, both of which mv/rm -rf inside $LLAMA_DIR. Append-mode open so
# the Python O_EXCL fallback's PID file is never truncated. 2h cap matches a
# worst-case source build; losing the wait means another provisioner is already
# doing this exact job, so exiting 0 is correct.
_LOCK_DIR="$(dirname "$LLAMA_DIR")"
mkdir -p "$_LOCK_DIR" 2>/dev/null
if command -v flock >/dev/null 2>&1; then
if exec 9>>"$_LOCK_DIR/.$(basename "$LLAMA_DIR").install.lock" 2>/dev/null; then
flock -w 7200 9 || { log "another llama.cpp install holds the lock; skipping"; exit 0; }
fi
fi
# Detect CUDA two ways: monolithic (libggml-cuda in ldd) or split (dlopen-ed
# libggml-cuda.so* beside the binary, missed by ldd). CPU-only builds ship no
# libggml-cuda.so, so its presence is the reliable signal.
is_cuda_server() {
[ -x "$1" ] || return 1
ldd "$1" 2>/dev/null | grep -qi 'libggml-cuda' && return 0
for _so in "$(dirname "$1")"/libggml-cuda.so*; do [ -e "$_so" ] && return 0; done
return 1
}
# 0. Already provisioned? Skip when the server links libggml-cuda directly (ldd)
# or when a co-located libggml-cuda.so* is paired with the completion stamp this
# script writes after its own final CUDA check. The stamp closes the one gap in
# the structural check: an in-place rebuild interrupted after libggml-cuda.so is
# linked but before llama-server relinks leaves new .so + old CPU server, which
# the bare .so test would wrongly skip. We deliberately do NOT run a functional
# `--list-devices` probe here: this script runs in a stripped-down detached shell whose
# loader path can miss /usr/lib/wsl/lib, so the CUDA backend may fail to enumerate even
# on a perfectly good server -- and a false negative would wipe a validated build and
# trigger a needless, thermally-dangerous source rebuild on the NVIDIA-ARM laptops this
# targets. Trust the .so; never gamble the machine's thermals on an env-fragile probe.
_CUDA_STAMP="$LLAMA_DIR/build/bin/.unsloth-cuda-ok"
if is_cuda_server "$SERVER"; then
if ldd "$SERVER" 2>/dev/null | grep -qi 'libggml-cuda' || [ -e "$_CUDA_STAMP" ]; then
log "CUDA llama-server already present: $SERVER"
exit 0
fi
log "CUDA .so present but the build never stamped complete (interrupted relink?); rebuilding"
fi
# 1. Require an NVIDIA GPU (this script is only meaningful with one).
# Resolve nvidia-smi explicitly: root login shells drop /usr/lib/wsl/lib from
# PATH, which is the ONLY location on WSL2 GPU-PV (mirrors setup.sh's resolver).
NVSMI="$(command -v nvidia-smi 2>/dev/null)"
[ -z "$NVSMI" ] && [ -x /usr/lib/wsl/lib/nvidia-smi ] && NVSMI=/usr/lib/wsl/lib/nvidia-smi
[ -z "$NVSMI" ] && [ -x /usr/bin/nvidia-smi ] && NVSMI=/usr/bin/nvidia-smi
if [ -z "$NVSMI" ]; then
log "no nvidia-smi found; skipping CUDA llama.cpp build"
exit 0
fi
SUDO=""; [ "$(id -u)" -ne 0 ] && SUDO="sudo"
HAVE_APT=0; command -v apt-get >/dev/null 2>&1 && HAVE_APT=1
# 2. Base toolchain first, then gcc-14 (nvcc rejects gcc-15) in a SEPARATE apt
# transaction: gcc-14 is absent from default Ubuntu 22.04 / Debian 12 sources, so
# a combined transaction would abort and lose the base build tools too.
if [ "$HAVE_APT" -eq 1 ]; then
$SUDO apt-get update -y >/dev/null 2>&1 || true
# libcurl4-openssl-dev: -DLLAMA_CURL=ON needs it, and the WSL deferred path
# skips setup.sh's GGUF dep install that would otherwise provide libcurl.
$SUDO apt-get install -y --no-install-recommends \
build-essential cmake git curl ca-certificates libcurl4-openssl-dev >/dev/null 2>&1 || true
$SUDO apt-get install -y --no-install-recommends gcc-14 g++-14 >/dev/null 2>&1 || true
fi
# 3. Locate nvcc; install the CUDA toolkit if missing. Prefer the highest
# /usr/local/cuda-<ver> toolkit: a stale unversioned `cuda` symlink or an older
# nvcc earlier on PATH could otherwise win and rebuild with CUDA 12.x, re-hitting
# the glibc>=2.41 / Blackwell clash this script exists to avoid. Fall back to a
# PATH nvcc (e.g. conda) only when no versioned system toolkit is present.
find_nvcc() {
local _v
_v="$(ls -d /usr/local/cuda-*/bin/nvcc 2>/dev/null | sort -V | tail -1)"
if [ -n "$_v" ]; then printf '%s\n' "$_v"; return 0; fi
command -v nvcc 2>/dev/null || ls /usr/local/cuda*/bin/nvcc 2>/dev/null | sort -V | tail -1
}
# The driver caps which CUDA major can RUN: cu13 binaries need a 580+ driver,
# and minor-version compatibility never crosses majors, so a server built with
# a toolkit newer than the driver loads nothing. Read the driver's supported
# major (all recent drivers print "CUDA Version: X.Y"); unparseable stays empty
# and keeps the previous install-13.3 behavior (Spark-class drivers all parse).
_DRV_CUDA_MAJOR="$("$NVSMI" 2>/dev/null | sed -n 's/.*CUDA Version: *\([0-9][0-9]*\)\..*/\1/p' | head -1)"
case "$_DRV_CUDA_MAJOR" in *[!0-9]*) _DRV_CUDA_MAJOR="" ;; esac
_nvcc_major_of() { "$1" --version 2>/dev/null | sed -n 's/.*release \([0-9][0-9]*\)\..*/\1/p' | head -1; }
_nvcc_minor_of() { "$1" --version 2>/dev/null | sed -n 's/.*release [0-9][0-9]*\.\([0-9][0-9]*\).*/\1/p' | head -1; }
# glibc >= 2.41 is the host side of the rsqrt header clash that CUDA 13.3 fixes,
# so a 13.0-13.2 toolkit is as unusable here as a pre-13 one. getconf first
# (no ldd on musl-ish images); unparseable stays 0 and keeps the major-only gate.
_glibc_ge_241=0
_glibc_ver="$(getconf GNU_LIBC_VERSION 2>/dev/null | awk '{print $NF}')"
[ -n "$_glibc_ver" ] || _glibc_ver="$(ldd --version 2>/dev/null | head -1 | awk '{print $NF}')"
case "$_glibc_ver" in
[0-9]*.[0-9]*)
_glibc_major="${_glibc_ver%%.*}"
_glibc_minor="${_glibc_ver#*.}"; _glibc_minor="${_glibc_minor%%.*}"
case "$_glibc_major$_glibc_minor" in
*[!0-9]*) ;;
*) if [ "$_glibc_major" -gt 2 ] 2>/dev/null || \
{ [ "$_glibc_major" -eq 2 ] && [ "$_glibc_minor" -ge 41 ]; } 2>/dev/null; then
_glibc_ge_241=1
fi ;;
esac
;;
esac
NVCC="$(find_nvcc)"
# A CUDA < 13 toolkit cannot build for the sm_121 Spark class (and CUDA < 13.3
# hits the glibc >= 2.41 rsqrt clash from the header) -- keeping it made every
# rerun fail configure/build and exit with the CPU server forever. When apt can
# provide 13.3 AND the driver can run cu13, upgrade past a stale toolkit;
# find_nvcc's sort -V then prefers the new install, and if the install fails
# the old toolkit remains the last resort (previous behavior, still fine on
# non-Spark hosts like GH200 + cu12x, which the driver gate now protects).
_nvcc_stale=0
if [ -n "$NVCC" ]; then
_nvcc_major="$(_nvcc_major_of "$NVCC")"
_nvcc_minor="$(_nvcc_minor_of "$NVCC")"
if [ -n "$_nvcc_major" ] && [ "$_nvcc_major" -lt 13 ] 2>/dev/null \
&& [ -n "$_DRV_CUDA_MAJOR" ] && [ "$_DRV_CUDA_MAJOR" -ge 13 ]; then
log "existing CUDA $_nvcc_major toolkit ($NVCC) predates this machine class; provisioning CUDA 13.3 alongside it"
_nvcc_stale=1
elif [ "$_glibc_ge_241" -eq 1 ] && [ -n "$_nvcc_major" ] && [ "$_nvcc_major" -eq 13 ] 2>/dev/null \
&& [ -n "$_nvcc_minor" ] && [ "$_nvcc_minor" -lt 3 ] 2>/dev/null \
&& [ -n "$_DRV_CUDA_MAJOR" ] && [ "$_DRV_CUDA_MAJOR" -ge 13 ]; then
# 13.0-13.2 compiles for sm_121 but hits the rsqrt clash on glibc >= 2.41,
# so the build fails and GGUF inference stays on the CPU server.
log "existing CUDA $_nvcc_major.$_nvcc_minor toolkit ($NVCC) hits the glibc $_glibc_ver rsqrt clash; provisioning CUDA 13.3 alongside it"
_nvcc_stale=1
fi
fi
if [ -z "$NVCC" ] && [ -n "$_DRV_CUDA_MAJOR" ] && [ "$_DRV_CUDA_MAJOR" -lt 13 ]; then
# No toolkit and the driver cannot run cu13: installing 13.3 would build an
# unloadable server. Bail to the no-toolkit message (CPU fallback stands).
log "driver supports CUDA ${_DRV_CUDA_MAJOR}.x only; not installing CUDA 13.3 (its binaries need a 580+ driver)"
elif { [ -z "$NVCC" ] || [ "$_nvcc_stale" -eq 1 ]; } && [ "$HAVE_APT" -eq 1 ]; then
[ -z "$NVCC" ] && log "CUDA toolkit (nvcc) not found - installing CUDA 13.3 (matches torch cu13x; avoids glibc>=2.41 rsqrt clash)"
# shellcheck disable=SC1091
. /etc/os-release 2>/dev/null || true
case "$(uname -m)" in
aarch64) NV_ARCH=sbsa ;;
x86_64) NV_ARCH=x86_64 ;;
*) NV_ARCH="" ;;
esac
case "${ID:-}${VERSION_ID:-}" in
ubuntu24.04) NV_DISTRO=ubuntu2404 ;;
ubuntu22.04) NV_DISTRO=ubuntu2204 ;;
debian12) NV_DISTRO=debian12 ;;
*) NV_DISTRO="" ;;
esac
if [ -n "$NV_ARCH" ] && [ -n "$NV_DISTRO" ]; then
KR=/tmp/cuda-keyring.deb
if curl -fsSL "https://developer.download.nvidia.com/compute/cuda/repos/$NV_DISTRO/$NV_ARCH/cuda-keyring_1.1-1_all.deb" -o "$KR" 2>/dev/null; then
$SUDO dpkg -i "$KR" >/dev/null 2>&1 || true
$SUDO apt-get update -y >/dev/null 2>&1 || true
$SUDO apt-get install -y cuda-toolkit-13-3 >/dev/null 2>&1 \
|| $SUDO apt-get install -y cuda-toolkit >/dev/null 2>&1 || true
fi
fi
NVCC="$(find_nvcc)"
fi
# Final sanity: never build with a toolkit whose major the driver cannot run
# (find_nvcc prefers the highest install, which may be a manually added 13.x on
# an older-driver host). Prefer the newest toolkit at or below the driver's
# major; with none, fall through to the no-toolkit exit.
if [ -n "$NVCC" ] && [ -n "$_DRV_CUDA_MAJOR" ]; then
_nvcc_major="$(_nvcc_major_of "$NVCC")"
if [ -n "$_nvcc_major" ] && [ "$_nvcc_major" -gt "$_DRV_CUDA_MAJOR" ] 2>/dev/null; then
_alt="$(ls -d /usr/local/cuda-[0-9]*/bin/nvcc 2>/dev/null | sort -V \
| awk -F'cuda-' -v m="$_DRV_CUDA_MAJOR" '{ split($2, v, /[./]/); if (v[1] + 0 <= m + 0) print }' | tail -1)"
if [ -n "$_alt" ]; then
log "CUDA $_nvcc_major toolkit exceeds the driver's supported major ($_DRV_CUDA_MAJOR); using $_alt instead"
NVCC="$_alt"
else
log "the only CUDA toolkit ($_nvcc_major.x) is newer than the driver supports (CUDA $_DRV_CUDA_MAJOR.x); a build would not load"
NVCC=""
fi
fi
fi
if [ -z "$NVCC" ]; then
log "could not provision a CUDA toolkit. Training + GGUF export still work;"
log "GGUF *inference* in Studio will be unavailable until a CUDA toolkit exists."
log "Re-run this script after installing one to enable GGUF inference."
exit 0
fi
CUDA_HOME="$(dirname "$(dirname "$NVCC")")"
# CUDA + Linux dirs FIRST so the build uses Linux cmake/gcc/git, not Windows tools
# leaked in via WSL interop (/mnt/c). Keep original PATH so nvidia-smi resolves.
export PATH="$CUDA_HOME/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$PATH"
export CUDAToolkit_ROOT="$CUDA_HOME"
# 4. Host compiler: prefer gcc-14 / g++-14 (nvcc rejects 15).
HCC=gcc; command -v gcc-14 >/dev/null 2>&1 && HCC=gcc-14
HCXX=g++; command -v g++-14 >/dev/null 2>&1 && HCXX=g++-14
export CC="$HCC" CXX="$HCXX" CUDAHOSTCXX="$HCXX"
# 5. CUDA arch from the GPU's compute capability (e.g. "12.1" -> 121). Fallback: native.
# Only a purely-numeric capability is a valid CMAKE_CUDA_ARCHITECTURES; some WSL
# GPU-PV / driver combos report "N/A". "native" needs CMake >= 3.24 (Ubuntu
# 22.04 apt ships 3.22), so the fallback omits the flag entirely and lets
# ggml's version-guarded CMake defaults pick the arches instead.
CC_CAP="$("$NVSMI" --query-gpu=compute_cap --format=csv,noheader 2>/dev/null | head -1 | tr -d ' .')"
case "$CC_CAP" in
''|*[!0-9]*) CUDA_ARCH="" ;;
*) CUDA_ARCH="$CC_CAP" ;;
esac
# 6. Clone + build into ~/.unsloth/llama.cpp, honoring a UNSLOTH_LLAMA_TAG pin
# (same var setup.sh uses) instead of always tracking ggml-org main.
mkdir -p "$(dirname "$LLAMA_DIR")"
_LLAMA_REF="${UNSLOTH_LLAMA_TAG:-}"
# setup.sh's install policy pins source builds to the newest RELEASE ("latest"
# resolved to a tag; master bypasses the pin). Mirror it: unset or literal
# "latest" resolves via the GitHub API; on API failure the empty ref keeps the
# existing default-branch clone fallback (best effort, as before).
if [ -z "$_LLAMA_REF" ] || [ "$_LLAMA_REF" = "latest" ]; then
_LLAMA_REF="$(curl -fsSL --max-time 15 https://api.github.com/repos/ggml-org/llama.cpp/releases/latest 2>/dev/null \
| grep -om1 '"tag_name": *"[^"]*"' | cut -d'"' -f4)"
[ -n "$_LLAMA_REF" ] && log "pinning llama.cpp to release $_LLAMA_REF"
fi
# Back up any existing (e.g. CPU-only) llama.cpp: restored on any failure exit,
# dropped only once the fresh build yields a server -- never leave NO server.
_LLAMA_BAK=""
_FRESH_CLONE=0
_restore_prev() {
if [ -n "$_LLAMA_BAK" ] && [ -e "$_LLAMA_BAK" ]; then
rm -rf "$LLAMA_DIR" 2>/dev/null
mv "$_LLAMA_BAK" "$LLAMA_DIR" 2>/dev/null && log "restored previous llama.cpp install"
elif [ "$_FRESH_CLONE" = "1" ] && [ ! -x "$SERVER" ]; then
# We created this clone and produced no server. Leaving a markerless git
# tree under a custom STUDIO_HOME bricks reruns: setup.sh's ownership
# assert refuses the unmarked dir and aborts the whole install.
rm -rf "$LLAMA_DIR" 2>/dev/null
fi
}
if [ ! -d "$LLAMA_DIR/.git" ]; then
if [ -e "$LLAMA_DIR" ]; then
_LLAMA_BAK="${LLAMA_DIR}.prev.$$"
rm -rf "$_LLAMA_BAK" 2>/dev/null
mv "$LLAMA_DIR" "$_LLAMA_BAK" 2>/dev/null || { rm -rf "$LLAMA_DIR" 2>/dev/null; _LLAMA_BAK=""; }
fi
_clone_ok=0
if [ -n "$_LLAMA_REF" ]; then
git clone --depth 1 --branch "$_LLAMA_REF" https://github.com/ggml-org/llama.cpp "$LLAMA_DIR" >/dev/null 2>&1 && _clone_ok=1
fi
if [ "$_clone_ok" -ne 1 ]; then
git clone --depth 1 https://github.com/ggml-org/llama.cpp "$LLAMA_DIR" >/dev/null 2>&1 && _clone_ok=1
fi
[ "$_clone_ok" -eq 1 ] && _FRESH_CLONE=1
if [ "$_clone_ok" -ne 1 ]; then
log "git clone failed"
_restore_prev
exit 0
fi
else
# Existing checkout: honor the pin on reruns too (previously all ref
# handling lived in the fresh-clone branch, so an existing tree rebuilt
# whatever commit it had regardless of the pin). Best-effort -- an
# unreachable ref keeps the current commit, matching the clone fallback.
if [ -n "$_LLAMA_REF" ]; then
_cur_head="$(git -C "$LLAMA_DIR" rev-parse HEAD 2>/dev/null)"
if git -C "$LLAMA_DIR" fetch --depth 1 origin "$_LLAMA_REF" >/dev/null 2>&1; then
_ref_head="$(git -C "$LLAMA_DIR" rev-parse FETCH_HEAD 2>/dev/null)"
if [ -n "$_ref_head" ] && [ "$_ref_head" != "$_cur_head" ]; then
git -C "$LLAMA_DIR" checkout -q FETCH_HEAD >/dev/null 2>&1 \
&& log "updated existing llama.cpp checkout to $_LLAMA_REF" \
|| log "could not check out $_LLAMA_REF; keeping the current commit"
fi
else
log "could not fetch $_LLAMA_REF; keeping the current commit"
fi
fi
fi
# Honor a UNSLOTH_LLAMA_PR pin (same var setup.sh supports) on fresh clones and
# existing checkouts alike; best-effort -- a failed fetch keeps what's there.
case "${UNSLOTH_LLAMA_PR:-}" in
''|*[!0-9]*) ;;
*)
if git -C "$LLAMA_DIR" fetch --depth 1 origin "pull/${UNSLOTH_LLAMA_PR}/head:_unsloth_pr_${UNSLOTH_LLAMA_PR}" >/dev/null 2>&1 \
&& git -C "$LLAMA_DIR" checkout "_unsloth_pr_${UNSLOTH_LLAMA_PR}" >/dev/null 2>&1; then
log "checked out llama.cpp PR #${UNSLOTH_LLAMA_PR} (UNSLOTH_LLAMA_PR)"
else
log "could not fetch llama.cpp PR #${UNSLOTH_LLAMA_PR}; building the default branch"
fi
;;
esac
cd "$LLAMA_DIR" || { _restore_prev; exit 0; }
# When rebuilding in-place over an existing git checkout, the whole-dir backup above
# was skipped (_LLAMA_BAK empty) -- but build/ may already hold a working (e.g. CPU)
# llama-server from a prior setup.sh source build. The wipe-on-failure paths below
# would destroy it with nothing to restore, leaving NO server despite the "keeps the
# existing server" promise (a thermal shutdown mid-CUDA-build is a real failure mode
# here). Back up the existing binaries so a failed rebuild can put them back. Only
# bin/ (server + dlopen-ed backends) is needed; cheap since any pre-existing server
# here is the non-CUDA fallback (a CUDA one would have exited at step 0).
_BUILD_BAK=""
if [ -z "$_LLAMA_BAK" ] && [ -x "$SERVER" ]; then
_BUILD_BAK="${LLAMA_DIR}.binbak.$$"
rm -rf "$_BUILD_BAK" 2>/dev/null
cp -a "$LLAMA_DIR/build/bin" "$_BUILD_BAK" 2>/dev/null || _BUILD_BAK=""
fi
_restore_build() {
if [ -n "$_BUILD_BAK" ] && [ -e "$_BUILD_BAK" ] && [ ! -x "$SERVER" ]; then
mkdir -p "$LLAMA_DIR/build" 2>/dev/null
rm -rf "$LLAMA_DIR/build/bin" 2>/dev/null
mv "$_BUILD_BAK" "$LLAMA_DIR/build/bin" 2>/dev/null && log "restored previous llama-server (rebuild failed)"
fi
[ -n "$_BUILD_BAK" ] && rm -rf "$_BUILD_BAK" 2>/dev/null
_BUILD_BAK=""
}
log "building CUDA llama.cpp (arch=${CUDA_ARCH:-cmake-default}, host=$HCXX) - this takes a few minutes..."
_cmake_configure() {
# Empty CUDA_ARCH (unreadable compute_cap): omit the flag so ggml's own
# CMake defaults apply -- "native" would need CMake >= 3.24.
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release \
-DGGML_CUDA=ON -DGGML_CUDA_F16=ON \
${CUDA_ARCH:+-DCMAKE_CUDA_ARCHITECTURES="$CUDA_ARCH"} \
-DCMAKE_CUDA_HOST_COMPILER="$HCXX" \
-DLLAMA_CURL=ON >/dev/null 2>&1
}
# A pre-existing build/ may carry a stale CMake cache (relocated dir: bad absolute
# paths + GGML_CUDA=OFF). Reuse first (fast incremental); wipe only on failure.
if ! _cmake_configure; then
log "stale/incompatible CMake cache detected; wiping build dir for a clean CUDA configure"
rm -rf build
_cmake_configure || { log "cmake configure failed"; cd /; _restore_build; _restore_prev; exit 0; }
fi
# Also builds the targets unsloth-zoo's GGUF exporter needs (llama-mtmd-cli,
# llama-gguf-split). Jobs default to ~half the cores (full -j(nproc) CUDA builds
# trip thermal shutdowns on NVIDIA-ARM laptops like the N1X "RTX Spark"), capped
# at ~1.5 GB/nvcc job. Tune: UNSLOTH_LLAMA_BUILD_JOBS=N; re-runs resume.
_ncpu="$(nproc 2>/dev/null || echo 4)"
# Honor a valid positive-int override; ignore junk/0 (cmake treats -j0 as all cores).
if [ -n "${UNSLOTH_LLAMA_BUILD_JOBS:-}" ] && [ "${UNSLOTH_LLAMA_BUILD_JOBS}" -ge 1 ] 2>/dev/null; then
JOBS="$UNSLOTH_LLAMA_BUILD_JOBS"
else
_half=$(( (_ncpu + 1) / 2 )) # ~half the cores for thermal headroom
if [ "$_ncpu" -le 4 ]; then _half="$_ncpu"; fi # tiny boxes: use all cores
_memkb="$(awk '/MemTotal/{print $2}' /proc/meminfo 2>/dev/null || echo 0)"
_memjobs=$(( _memkb / 1572864 )) # 1.5 GB per nvcc job
if [ "$_memjobs" -lt 1 ]; then _memjobs=1; fi
JOBS="$_half"
if [ "$_memjobs" -lt "$JOBS" ]; then JOBS="$_memjobs"; fi
fi
log "building with -j${JOBS} (cores=${_ncpu})"
# nice/ionice: full speed when idle, yields to foreground Studio/training runs.
_NICE=""
command -v nice >/dev/null 2>&1 && _NICE="nice -n 19"
command -v ionice >/dev/null 2>&1 && _NICE="$_NICE ionice -c 3"
_cmake_build() {
# Only llama-server is REQUIRED: an old UNSLOTH_LLAMA_TAG pin may predate the
# helper targets, whose absence must not fail the whole provision.
$_NICE cmake --build build -j"$JOBS" --target llama-server >/dev/null 2>&1
}
_cmake_build_extras() {
# Helper targets unsloth-zoo's GGUF exporter also uses -- best-effort each.
for _t in llama-cli llama-quantize llama-mtmd-cli llama-gguf-split; do
$_NICE cmake --build build -j"$JOBS" --target "$_t" >/dev/null 2>&1 || true
done
}
if ! _cmake_build; then
# An interrupted build (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.
log "build failed (likely interrupted/partial); wiping build dir and rebuilding clean"
rm -rf build
_cmake_configure || { log "cmake configure failed"; cd /; _restore_build; _restore_prev; exit 0; }
_cmake_build || { log "cmake build failed"; cd /; _restore_build; _restore_prev; exit 0; }
fi
_cmake_build_extras
# Drop the backup on a successful build, or restore the prior server if the rebuild
# yielded none (idempotent; only restores when $SERVER is missing).
_restore_build
if is_cuda_server "$SERVER"; then
: > "$_CUDA_STAMP" 2>/dev/null || true
# unsloth_zoo's check_llama_cpp only searches the repo root, so mirror
# setup.sh's root shim for the GGUF exporter's quantize binary.
if [ -x "$LLAMA_DIR/build/bin/llama-quantize" ] && [ ! -e "$LLAMA_DIR/llama-quantize" ]; then
ln -sf build/bin/llama-quantize "$LLAMA_DIR/llama-quantize" 2>/dev/null || true
fi
log "CUDA llama-server ready: $SERVER"
[ -n "$_LLAMA_BAK" ] && rm -rf "$_LLAMA_BAK" 2>/dev/null
elif [ -x "$SERVER" ]; then
# A server exists but isn't CUDA-confirmed; still better than the old backup.
log "build finished but CUDA llama-server could not be confirmed"
[ -n "$_LLAMA_BAK" ] && rm -rf "$_LLAMA_BAK" 2>/dev/null
else
log "build finished but no llama-server was produced"
cd /; _restore_prev
fi
exit 0

View file

@ -320,16 +320,21 @@ _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, so bare `command -v nvidia-smi` misses
# real GPUs on the flagship WoA path.
_resolve_nvsmi() {
command -v nvidia-smi 2>/dev/null && return 0
[ -x /usr/lib/wsl/lib/nvidia-smi ] && { echo /usr/lib/wsl/lib/nvidia-smi; return 0; }
[ -x /usr/bin/nvidia-smi ] && { echo /usr/bin/nvidia-smi; return 0; }
return 1
}
_setup_has_usable_nvidia_gpu() {
if _setup_cvd_hides_nvidia; then
return 1
fi
_setup_nvsmi=""
if command -v nvidia-smi >/dev/null 2>&1; then
_setup_nvsmi="nvidia-smi"
elif [ -x "/usr/bin/nvidia-smi" ]; then
_setup_nvsmi="/usr/bin/nvidia-smi"
fi
_setup_nvsmi="$(_resolve_nvsmi)" || _setup_nvsmi=""
if [ -n "$_setup_nvsmi" ]; then
if _setup_run_smi "$_setup_nvsmi" -L 2>/dev/null \
| awk '/^GPU[[:space:]]+[0-9]+:/{found=1} END{exit !found}'; then
@ -344,8 +349,8 @@ _setup_has_usable_nvidia_gpu() {
}
_cuda_driver_max_version() {
command -v nvidia-smi >/dev/null 2>&1 || return 0
_setup_run_smi nvidia-smi 2>/dev/null \
_cdm_smi="$(_resolve_nvsmi)" || return 0
_setup_run_smi "$_cdm_smi" 2>/dev/null \
| sed -nE 's/.*CUDA( UMD)? Version:[[:space:]]*([0-9]+)\.([0-9]+).*/\2.\3/p' \
| head -1 || true
}
@ -1233,6 +1238,10 @@ LLAMA_CPP_DIR="$UNSLOTH_HOME/llama.cpp"
LLAMA_SERVER_BIN="$LLAMA_CPP_DIR/build/bin/llama-server"
_NEED_LLAMA_SOURCE_BUILD=false
_LLAMA_CPP_DEGRADED=false
# 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_CPP_NO_SPACE=false
_LLAMA_FORCE_COMPILE="${UNSLOTH_LLAMA_FORCE_COMPILE:-0}"
_REQUESTED_LLAMA_TAG="${UNSLOTH_LLAMA_TAG:-${_DEFAULT_LLAMA_TAG}}"
@ -1496,6 +1505,54 @@ if [ "$_NEED_LLAMA_SOURCE_BUILD" = true ] && \
_NEED_LLAMA_SOURCE_BUILD=false
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 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" ] \
&& [ -z "$_LLAMA_PR" ] \
&& [ "${UNSLOTH_NO_LLAMA_CUDA:-0}" != "1" ] \
&& grep -qi microsoft /proc/version 2>/dev/null \
&& { [ "$_HOST_MACHINE" = "aarch64" ] || [ "$_HOST_MACHINE" = "arm64" ]; } \
&& _NVSMI_GATE="$(_resolve_nvsmi)" && [ -n "$_NVSMI_GATE" ] \
&& "$_NVSMI_GATE" -L 2>/dev/null | awk '/^GPU[[:space:]]+[0-9]+:/{found=1} END{exit !found}' \
&& ! command -v nvcc >/dev/null 2>&1 \
&& ! ls /usr/local/cuda*/bin/nvcc >/dev/null 2>&1; then
step "llama.cpp" "GGUF engine: CUDA build running in background (WSL aarch64 + NVIDIA)" "$C_WARN"
substep "skipping slow CPU build; the background CUDA llama.cpp will provide the server"
substep "(opt out / keep CPU build with UNSLOTH_NO_LLAMA_CUDA=1)"
# DEFERRED, not DEGRADED: DEGRADED triggers the CPU-prebuilt last resort + exit 1.
_NEED_LLAMA_SOURCE_BUILD=false
_LLAMA_CPP_DEFERRED=true
fi
# ── Native Linux aarch64 + NVIDIA, no nvcc yet: skip the CPU build too ──
# 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" ] \
&& [ -z "$_LLAMA_PR" ] \
&& [ "${UNSLOTH_NO_LLAMA_CUDA:-0}" != "1" ] \
&& [ "${_SKIP_GGUF_BUILD:-}" != true ] \
&& ! grep -qi microsoft /proc/version 2>/dev/null \
&& { [ "$_HOST_MACHINE" = "aarch64" ] || [ "$_HOST_MACHINE" = "arm64" ]; } \
&& _NVSMI_GATE="$(_resolve_nvsmi)" && [ -n "$_NVSMI_GATE" ] \
&& "$_NVSMI_GATE" -L 2>/dev/null | awk '/^GPU[[:space:]]+[0-9]+:/{found=1} END{exit !found}' \
&& [ "${_setup_nvidia_usable:-}" = true ] \
&& ! command -v nvcc >/dev/null 2>&1 \
&& ! ls /usr/local/cuda*/bin/nvcc >/dev/null 2>&1; then
step "llama.cpp" "GGUF engine: deferring to this run's CUDA provision (aarch64 + NVIDIA, no nvcc)" "$C_WARN"
substep "skipping the slow CPU build; the CUDA llama.cpp build below provides the server"
substep "(opt out / keep the CPU build with UNSLOTH_NO_LLAMA_CUDA=1)"
_NEED_LLAMA_SOURCE_BUILD=false
fi
# ── 8. WSL: pre-install GGUF build dependencies for fallback source builds ──
# On WSL, sudo requires a password and can't be entered during GGUF export
# (runs in a non-interactive subprocess). Install build deps here instead.
@ -1786,6 +1843,23 @@ 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), 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%%.*}"
_CU_MAJ="${_NVCC_VER%%.*}"; _CU_MIN="${_NVCC_VER#*.}"; _CU_MIN="${_CU_MIN%%.*}"
if { [ "${_GLIBC_MAJ:-0}" -gt 2 ] 2>/dev/null \
|| { [ "${_GLIBC_MAJ:-0}" -eq 2 ] 2>/dev/null && [ "${_GLIBC_MIN:-0}" -ge 41 ] 2>/dev/null; }; } \
&& { [ "${_CU_MAJ:-0}" -lt 13 ] 2>/dev/null \
|| { [ "${_CU_MAJ:-0}" -eq 13 ] 2>/dev/null && [ "${_CU_MIN:-0}" -lt 3 ] 2>/dev/null; }; }; then
substep "CUDA toolkit ${_NVCC_VER} is incompatible with glibc ${_GLIBC_VER} (rsqrt/rsqrtf header clash)." "$C_ERR"
substep "the GPU build will fail to compile and fall back to CPU -- install CUDA Toolkit >= 13.3:" "$C_WARN"
substep "https://developer.nvidia.com/cuda-downloads (setup.sh auto-selects the newest /usr/local/cuda-*)" "$C_WARN"
fi
fi
# Resolve the arch list before committing to a CUDA build;
# an empty list means CPU instead of a PTX-only binary (#5854).
_raw_caps=""
@ -1910,6 +1984,27 @@ 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 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
NCPU="$UNSLOTH_LLAMA_BUILD_JOBS"
else
_cap_half=$(( (NCPU + 1) / 2 ))
[ "$NCPU" -le 4 ] && _cap_half="$NCPU" # tiny boxes: use all cores
_cap_memkb="$(awk '/MemTotal/{print $2}' /proc/meminfo 2>/dev/null || echo 0)"
_cap_memjobs=$(( _cap_memkb / 1572864 )) # ~1.5 GB per nvcc job
[ "$_cap_memjobs" -lt 1 ] && _cap_memjobs=1
[ "$_cap_memjobs" -lt "$_cap_half" ] && _cap_half="$_cap_memjobs"
NCPU="$_cap_half"
fi
substep "thermal-capped CUDA build: -j${NCPU} (override with UNSLOTH_LLAMA_BUILD_JOBS=N)"
fi
CMAKE_GENERATOR_ARGS=""
if command -v ninja &>/dev/null; then
CMAKE_GENERATOR_ARGS="-G Ninja"
@ -2044,6 +2139,81 @@ else
}
fi # end _SKIP_GGUF_BUILD check
# ── aarch64 + NVIDIA (DGX Spark / GB10 / N1X "RTX Spark"): provision a CUDA
# llama.cpp when the source build above could not (no CUDA toolkit found) ──
# 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). 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 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
done
return 1
}
if [ "$_HOST_SYSTEM" = "Linux" ] \
&& { [ "$_HOST_MACHINE" = "aarch64" ] || [ "$_HOST_MACHINE" = "arm64" ]; } \
&& { ! grep -qi microsoft /proc/version 2>/dev/null || [ "${UNSLOTH_WSL_LLAMA_DEFERRED:-0}" != "1" ]; } \
&& [ "${UNSLOTH_NO_LLAMA_CUDA:-0}" != "1" ] \
&& [ "${_SKIP_GGUF_BUILD:-}" != true ] \
&& _NVSMI_GATE="$(_resolve_nvsmi)" && [ -n "$_NVSMI_GATE" ] \
&& "$_NVSMI_GATE" -L 2>/dev/null | awk '/^GPU[[:space:]]+[0-9]+:/{found=1} END{exit !found}' \
&& [ "${_setup_nvidia_usable:-}" = true ] \
&& [ "$_LOCAL_LLAMA_CPP_LINKED" != 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 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"
elif [ "${STUDIO_LOCAL_INSTALL:-0}" = "1" ] && [ -f "$REPO_ROOT/studio/scripts/provision_llama_cuda.sh" ]; then
_PROV_SH="$REPO_ROOT/studio/scripts/provision_llama_cuda.sh"
else
_PROV_URL="https://raw.githubusercontent.com/unslothai/unsloth/main/studio/scripts/provision_llama_cuda.sh"
_PROV_TMP="$UNSLOTH_HOME/provision_llama_cuda.sh"
if curl -fsSL "$_PROV_URL" -o "$_PROV_TMP" 2>/dev/null && [ -s "$_PROV_TMP" ]; then
_PROV_SH="$_PROV_TMP"
fi
fi
if [ -n "$_PROV_SH" ]; then
step "llama.cpp" "aarch64 + NVIDIA: provisioning CUDA toolkit + building CUDA llama.cpp for GGUF inference..." "$C_WARN"
substep "(opt out with UNSLOTH_NO_LLAMA_CUDA=1; lower load with UNSLOTH_LLAMA_BUILD_JOBS=N)"
# UNSLOTH_LLAMA_CPP_PATH routes a custom STUDIO_HOME into $LLAMA_CPP_DIR.
UNSLOTH_LLAMA_CPP_PATH="$LLAMA_CPP_DIR" bash "$_PROV_SH" || true
if _have_cuda_llama_server; then
step "llama.cpp" "CUDA llama-server ready (aarch64 + NVIDIA)"
_LLAMA_CPP_DEGRADED=false
# Claim ownership of the fresh $LLAMA_CPP_DIR, else the next custom-STUDIO_HOME
# 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
elif [ -f "$LLAMA_SERVER_BIN" ]; then
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.
_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; 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
fi
# ── arm64 Linux GPU: CPU prebuilt as a last resort ──
# An arm64 Linux GPU host source-builds for the GPU above. If that produced no
# binary, install the fork's arm64 CPU prebuilt (app-<tag>-linux-arm64-cpu.tar.gz)
@ -2176,7 +2346,9 @@ fi
if [ "$_LLAMA_ONLY" = "1" ]; then
echo ""
printf " ${C_DIM}%s${C_RST}\n" "$RULE"
if [ "$_LLAMA_CPP_DEGRADED" = true ]; then
if [ "$_LLAMA_CPP_DEFERRED" = true ]; then
printf " ${C_TITLE}%s${C_RST}\n" "llama.cpp update finished (GGUF engine: CUDA build running in background)"
elif [ "$_LLAMA_CPP_DEGRADED" = true ]; then
printf " ${C_WARN}%s${C_RST}\n" "llama.cpp update finished (limited: llama.cpp unavailable)"
else
printf " ${C_TITLE}%s${C_RST}\n" "llama.cpp update finished"
@ -2185,7 +2357,9 @@ if [ "$_LLAMA_ONLY" = "1" ]; then
elif [ "$IS_COLAB" = true ]; then
echo ""
printf " ${C_DIM}%s${C_RST}\n" "$RULE"
if [ "$_LLAMA_CPP_DEGRADED" = true ]; then
if [ "$_LLAMA_CPP_DEFERRED" = true ]; then
printf " ${C_TITLE}%s${C_RST}\n" "Unsloth Studio Setup Complete (GGUF engine: CUDA build running in background)"
elif [ "$_LLAMA_CPP_DEGRADED" = true ]; then
printf " ${C_WARN}%s${C_RST}\n" "Unsloth Studio Setup Complete (limited: llama.cpp unavailable)"
else
printf " ${C_TITLE}%s${C_RST}\n" "Unsloth Studio Setup Complete"
@ -2195,7 +2369,9 @@ elif [ "$IS_COLAB" = true ]; then
substep "start()"
else
printf " ${C_DIM}%s${C_RST}\n" "$RULE"
if [ "$_LLAMA_CPP_DEGRADED" = true ]; then
if [ "$_LLAMA_CPP_DEFERRED" = true ]; then
printf " ${C_TITLE}%s${C_RST}\n" "Unsloth Studio Installed (GGUF engine: CUDA build running in background)"
elif [ "$_LLAMA_CPP_DEGRADED" = true ]; then
printf " ${C_WARN}%s${C_RST}\n" "Unsloth Studio Installed (limited: llama.cpp unavailable)"
else
printf " ${C_TITLE}%s${C_RST}\n" "Unsloth Studio Installed"
@ -2211,6 +2387,14 @@ else
fi
echo ""
# 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
# When called from install.sh (SKIP_STUDIO_BASE=1), exit non-zero so the
# installer can report the GGUF failure after finishing PATH/shortcut setup.
# When called directly via 'unsloth studio update', keep the install

View file

@ -265,10 +265,13 @@ class TestSetupShHardening:
assert wrapped, "compute_cap probe must be wrapped in _setup_run_smi (timeout-bounded)"
def test_driver_version_probe_timeout_wrapped(self, setup_src):
# The probe resolves nvidia-smi explicitly (root WSL shells drop /usr/lib/wsl/lib
# from PATH) and must still go through the timeout wrapper with the resolved path.
start = setup_src.find("_cuda_driver_max_version()")
end = setup_src.find("\n}", start)
body = setup_src[start:end]
assert "_setup_run_smi nvidia-smi" in body
assert "_resolve_nvsmi" in body
assert '_setup_run_smi "$_cdm_smi"' in body
# TEST: install.sh -- UNSLOTH_TORCH_BACKEND classified on the final path segment
@ -457,7 +460,12 @@ class TestHiddenCvdNotUsable:
out = self._run_sh_helper(
tmp_path,
src,
["_setup_run_smi", "_setup_cvd_hides_nvidia", "_setup_has_usable_nvidia_gpu"],
[
"_resolve_nvsmi",
"_setup_run_smi",
"_setup_cvd_hides_nvidia",
"_setup_has_usable_nvidia_gpu",
],
cvd,
)
assert out == expected

View file

@ -25,6 +25,52 @@ torch_compile_options = {
"triton.cudagraphs": False,
}
def _flex_is_dgx_spark():
# CUDA-free copy of _utils._is_dgx_spark_no_cuda_init() (avoids circular import).
# Runs at module import, before ._utils -- touching torch.cuda here would init
# the allocator before patch_dgx_spark_memory_config() sets PYTORCH_CUDA_ALLOC_CONF.
_force = os.environ.get("UNSLOTH_FORCE_DGX_SPARK")
if _force == "1":
return True
if _force == "0":
return False
try:
import platform
if platform.machine().lower() not in ("aarch64", "arm64"):
return False
import subprocess
import shutil
# The WoA shim execs the venv binary directly (no login shell), where
# /usr/lib/wsl/lib can be off PATH -- resolve WSL's nvidia-smi explicitly
# (mirrors _is_dgx_spark_no_cuda_init in models/_utils.py).
_smi = "nvidia-smi"
if shutil.which(_smi) is None and os.path.exists("/usr/lib/wsl/lib/nvidia-smi"):
_smi = "/usr/lib/wsl/lib/nvidia-smi"
out = subprocess.run(
[_smi, "--query-gpu=name", "--format=csv,noheader"],
capture_output = True,
text = True,
timeout = 5,
)
names = (out.stdout or "").upper()
# Whole-token match so "GB10" doesn't match discrete "GB100"/"GB10X".
import re
return any(
re.search(r"(?<![A-Z0-9])" + re.escape(t) + r"(?![A-Z0-9])", names)
for t in ("GB10", "JMJWOA", "N1X", "DGX SPARK", "GB110")
)
except Exception:
return False
# Spark's 48 SMs are under inductor's 68-SM is_big_gpu bar; max_autotune just wastes search time.
if _flex_is_dgx_spark():
torch_compile_options["max_autotune"] = False
# Flex Attention supported from torch 2.5 onwards only
try:
from torch.nn.attention.flex_attention import (

View file

@ -1670,8 +1670,191 @@ except:
from transformers.modeling_utils import logger as transformers_logger
# NVIDIA DGX Spark (GB10) / N1X "RTX Spark" unified-memory support.
# Names vary ("NVIDIA GB10", "JMJWOA-Generic-GPU" on N1X); the aarch64 + CUDA
# gate keeps every Spark workaround a no-op elsewhere.
_DGX_SPARK_DEVICE_TOKENS = ("GB10", "JMJWOA", "N1X", "DGX SPARK", "GB110")
def _name_has_spark_token(names_upper):
# Whole-token match so "GB10" doesn't match discrete "GB100"/"GB10X".
import re
return any(
re.search(r"(?<![A-Z0-9])" + re.escape(tok) + r"(?![A-Z0-9])", names_upper)
for tok in _DGX_SPARK_DEVICE_TOKENS
)
@functools.lru_cache(maxsize = None)
def is_dgx_spark():
"""True only on DGX Spark / N1X (gate: aarch64 + CUDA + device-name token).
UNSLOTH_FORCE_DGX_SPARK=1/0 forces on/off."""
_force = os.environ.get("UNSLOTH_FORCE_DGX_SPARK")
if _force == "1":
return True
if _force == "0":
return False
try:
import platform
if platform.machine().lower() not in ("aarch64", "arm64"):
return False
if not (hasattr(torch, "cuda") and torch.cuda.is_available()):
return False
names = " ".join(
str(torch.cuda.get_device_name(i)).upper() for i in range(torch.cuda.device_count())
)
return _name_has_spark_token(names)
except Exception:
return False
@functools.lru_cache(maxsize = None)
def _is_dgx_spark_no_cuda_init():
"""Spark detection that never inits CUDA: reads 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":
return True
if _force == "0":
return False
try:
import platform
if platform.machine().lower() not in ("aarch64", "arm64"):
return False
import subprocess
import shutil
# The WoA shim execs the venv binary directly (no login shell), where
# /usr/lib/wsl/lib can be off PATH -- resolve WSL's nvidia-smi explicitly.
_smi = "nvidia-smi"
if shutil.which(_smi) is None and os.path.exists("/usr/lib/wsl/lib/nvidia-smi"):
_smi = "/usr/lib/wsl/lib/nvidia-smi"
out = subprocess.run(
[_smi, "--query-gpu=name", "--format=csv,noheader"],
capture_output = True,
text = True,
timeout = 5,
)
names = (out.stdout or "").upper()
return _name_has_spark_token(names)
except Exception:
return False
def patch_dgx_spark_caching_allocator_warmup():
"""No-op `caching_allocator_warmup` on Spark UMA.
`cudaMemGetInfo()` undercounts free UMA memory, so HF's warmup
`torch.empty(...)` raises `AcceleratorError: invalid argument`, aborting
bitsandbytes 4/8-bit loads. The warmup is only a speed hint, so skip it.
Gated by `is_dgx_spark()`; idempotent via `_unsloth_spark_noop` marker.
"""
if not is_dgx_spark():
return
try:
from transformers import modeling_utils as _mu
except Exception:
return
if not hasattr(_mu, "caching_allocator_warmup"):
return
if getattr(_mu.caching_allocator_warmup, "_unsloth_spark_noop", False):
return
def _noop(*args, **kwargs):
return None
_noop._unsloth_spark_noop = True
_mu.caching_allocator_warmup = _noop
def patch_dgx_spark_memory_config():
"""Enable allocator `expandable_segments` on Spark UMA to cut fragmentation
OOMs (no-op off-Spark).
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 init the allocator before the env var could take effect.
"""
if not _is_dgx_spark_no_cuda_init():
return
if os.environ.get("UNSLOTH_NO_EXPANDABLE_SEGMENTS") == "1":
return
conf = os.environ.get("PYTORCH_CUDA_ALLOC_CONF", "")
if "expandable_segments" in conf:
return # respect user's setting
os.environ["PYTORCH_CUDA_ALLOC_CONF"] = (
conf + "," if conf else ""
) + "expandable_segments:True"
def patch_dgx_spark_runtime_defaults():
"""Spark UMA runtime defaults (no-op off-Spark; env-overridable).
- 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 staging buffer is pure waste on a shared pool.
- UNSLOTH_SPARK_MEM_FRACTION=<0..1> (opt-in, default NO cap): caps the
allocator so over-allocation raises OutOfMemoryError early instead of
wedging the box (untracked UMA allocations may never trip a catchable OOM).
"""
if not is_dgx_spark():
return
os.environ.setdefault("UNSLOTH_DISABLE_DOUBLE_BUFFER", "1")
_frac = os.environ.get("UNSLOTH_SPARK_MEM_FRACTION")
if _frac:
try:
# Out-of-range = no cap (0 OOMs everything; torch rejects > 1).
_frac_val = float(_frac)
if 0.0 < _frac_val <= 1.0:
torch.cuda.set_per_process_memory_fraction(_frac_val)
except Exception:
pass
def patch_dgx_spark_dataloader_defaults():
"""Default `dataloader_pin_memory` to False on Spark UMA.
On one shared pool, pinning only reserves non-pageable RAM and adds a staging
copy (mirrors transformers' use_cpu precedent). Wrapping the base
`TrainingArguments.__post_init__` covers SFT + every TRL trainer in one
idempotent patch. Opt out: UNSLOTH_SPARK_KEEP_PIN_MEMORY=1. No-op off-Spark.
"""
if not is_dgx_spark():
return
if os.environ.get("UNSLOTH_SPARK_KEEP_PIN_MEMORY") == "1":
return
try:
from transformers import training_args as _ta
Base = _ta.TrainingArguments
except Exception:
return
if getattr(Base.__post_init__, "_unsloth_spark_uma", False):
return
_orig_post_init = Base.__post_init__
# *args/**kwargs: tolerate future InitVar params in __post_init__.
def __post_init__(self, *args, **kwargs):
_orig_post_init(self, *args, **kwargs)
if getattr(self, "dataloader_pin_memory", None) is True:
self.dataloader_pin_memory = False
__post_init__._unsloth_spark_uma = True
Base.__post_init__ = __post_init__
patch_dgx_spark_memory_config()
patch_dgx_spark_caching_allocator_warmup()
patch_dgx_spark_runtime_defaults()
patch_dgx_spark_dataloader_defaults()
# Faster safetensors loads on UMA (integrated) GPUs; lazy gate keeps this import
# fork-safe (no CUDA init). No-op off-UMA. Opt out: UNSLOTH_DISABLE_UMA_CLONE_LOAD=1.
# Installed after the Spark patches so patch_dgx_spark_memory_config() still lands
# its PYTORCH_CUDA_ALLOC_CONF before anything can touch the allocator.
from ._uma_safetensors import patch_unified_memory_safetensors_load
patch_unified_memory_safetensors_load()
@ -2265,6 +2448,9 @@ torch_compile_options = {
"trace.enabled": UNSLOTH_COMPILE_DEBUG,
"triton.cudagraphs": False,
}
# Spark's 48 SMs are under inductor's 68-SM is_big_gpu bar; max_autotune just wastes search time.
if is_dgx_spark():
torch_compile_options["max_autotune"] = False
import accelerate

View file

@ -16,6 +16,7 @@ from ._utils import (
_prepare_model_for_qat,
is_bfloat16_supported,
is_vLLM_available,
is_dgx_spark,
HAS_FLASH_ATTENTION,
HAS_FLASH_ATTENTION_SOFTCAPPING,
USE_MODELSCOPE,
@ -463,10 +464,10 @@ class FastLanguageModel(FastLlamaModel):
)
if DEVICE_TYPE_TORCH == "cuda":
for i in range(DEVICE_COUNT):
# [TODO] DGX Spark vLLM breaks
if "NVIDIA GB10" in str(torch.cuda.get_device_name(i)).upper():
# [TODO] DGX Spark / N1X (Spark-class) vLLM breaks
if is_dgx_spark():
print(
"Unsloth: DGX Spark detected - `fast_inference=True` is currently broken as of January 2026.\n"
"Unsloth: DGX Spark / N1X (Spark-class GPU) detected - `fast_inference=True` is currently broken as of January 2026.\n"
"Defaulting to native Unsloth inference."
)
fast_inference = False
@ -1158,10 +1159,10 @@ class FastModel(FastBaseModel):
)
if DEVICE_TYPE_TORCH == "cuda":
for i in range(DEVICE_COUNT):
# [TODO] DGX Spark vLLM breaks
if "NVIDIA GB10" in str(torch.cuda.get_device_name(i)).upper():
# [TODO] DGX Spark / N1X (Spark-class) vLLM breaks
if is_dgx_spark():
print(
"Unsloth: DGX Spark detected - `fast_inference=True` is currently broken as of January 2026.\n"
"Unsloth: DGX Spark / N1X (Spark-class GPU) detected - `fast_inference=True` is currently broken as of January 2026.\n"
"Defaulting to native Unsloth inference."
)
fast_inference = False