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.
This commit is contained in:
Daniel Han 2026-07-18 13:59:55 +00:00
commit 9ca396b665
3 changed files with 52 additions and 16 deletions

View file

@ -2223,6 +2223,13 @@ exit 0
if ($env:UNSLOTH_PYTORCH_MIRROR -and ($env:UNSLOTH_PYTORCH_MIRROR -match '^https?://[A-Za-z0-9._~:/?#@%+=&-]+$')) {
$_fwdEnv += "export UNSLOTH_PYTORCH_MIRROR='$($env:UNSLOTH_PYTORCH_MIRROR)'; "
}
# Forward the npm mirror the same way: setup.sh threads UNSLOTH_NPM_REGISTRY
# into every npm/bun install, and on mirror-required networks the WSL
# frontend/OXC steps would otherwise hit registry.npmjs.org and fail the
# install. Same strict http(s) allow-list + single-quote as above.
if ($env:UNSLOTH_NPM_REGISTRY -and ($env:UNSLOTH_NPM_REGISTRY -match '^https?://[A-Za-z0-9._~:/?#@%+=&-]+$')) {
$_fwdEnv += "export UNSLOTH_NPM_REGISTRY='$($env:UNSLOTH_NPM_REGISTRY)'; "
}
# Forward an explicit UNSLOTH_PYTHON pin: Windows env vars do not cross into
# WSL, so without this the inner install.sh silently built the venv on its
# default Python while the installer reported success. Strict version shape
@ -2379,14 +2386,23 @@ exit 0
# Record the distro so the uninstaller can clean a custom UNSLOTH_WSL_DISTRO install
# without the env var set.
try { Set-Content -LiteralPath (Join-Path (Split-Path $shimDir -Parent) "wsl-distro.txt") -Value $distro -Encoding ASCII } catch {}
# A fresh profile may have no HKCU 'Path'; null would make TrimEnd() throw.
$userPath = [Environment]::GetEnvironmentVariable("Path", "User")
if (-not $userPath) { $userPath = "" }
if (($userPath -split ';') -notcontains $shimDir) {
$newUserPath = if ($userPath.Trim()) { $userPath.TrimEnd(';') + ";" + $shimDir } else { $shimDir }
[Environment]::SetEnvironmentVariable("Path", $newUserPath, "User")
}
$env:Path = $env:Path.TrimEnd(';') + ";" + $shimDir
# PREPEND (not append): a previous NATIVE install prepended
# %USERPROFILE%\.unsloth\studio\bin (unsloth.exe) to user PATH,
# and that exe outlives the venv this fallback just rolled aside
# -- an appended shim would lose to the dead native launcher in
# every new terminal. Add-ToUserPath de-dupes and hoists.
$null = Add-ToUserPath -Directory $shimDir -Position 'Prepend'
$env:Path = $shimDir + ";" + $env:Path.TrimStart(';')
# Drop the dead default-root native shim outright when the venv
# binary it launches is gone (custom-root shims are left alone;
# the PATH prepend above already outranks them).
try {
$staleNativeShim = Join-Path $env:USERPROFILE ".unsloth\studio\bin\unsloth.exe"
$staleNativeTarget = Join-Path $env:USERPROFILE ".unsloth\studio\unsloth_studio\Scripts\unsloth.exe"
if ((Test-Path -LiteralPath $staleNativeShim) -and -not (Test-Path -LiteralPath $staleNativeTarget)) {
Remove-Item -LiteralPath $staleNativeShim -Force -ErrorAction Stop
}
} catch {}
step "shim" "created native 'unsloth' command -> forwards to WSL '$distro'" "Green"
substep "open a NEW terminal, then (no WSL knowledge needed):" "Cyan"
substep " unsloth studio # runs in WSL; opens http://localhost:8888" "Cyan"

View file

@ -2979,10 +2979,16 @@ elif [ -n "$TORCH_INDEX_URL" ]; then
# extras break 4-bit QLoRA, but aarch64 manylinux wheels work (verified on
# sm_121 via PTX JIT). Best-effort: no wheel keeps 16-bit LoRA / full finetuning.
# SKIP_TORCH gate stops a --no-torch (GGUF-only) install dragging torch back in.
# nvidia-smi may live only in /usr/lib/wsl/lib (WSL2 GPU-PV), which root login
# shells drop from PATH -- resolve explicitly (same order as setup.sh's
# _resolve_nvsmi) so the WoA/WSL install still gets 4-bit QLoRA support.
_bnb_nvsmi="$(command -v nvidia-smi 2>/dev/null || true)"
[ -z "$_bnb_nvsmi" ] && [ -x /usr/lib/wsl/lib/nvidia-smi ] && _bnb_nvsmi=/usr/lib/wsl/lib/nvidia-smi
[ -z "$_bnb_nvsmi" ] && [ -x /usr/bin/nvidia-smi ] && _bnb_nvsmi=/usr/bin/nvidia-smi
if [ "$SKIP_TORCH" = false ] \
&& { [ "$(uname -m)" = "aarch64" ] || [ "$(uname -m)" = "arm64" ]; } \
&& command -v nvidia-smi >/dev/null 2>&1 \
&& nvidia-smi -L 2>/dev/null | awk '/^GPU[[:space:]]+[0-9]+:/{found=1} END{exit !found}' \
&& [ -n "$_bnb_nvsmi" ] \
&& "$_bnb_nvsmi" -L 2>/dev/null | awk '/^GPU[[:space:]]+[0-9]+:/{found=1} END{exit !found}' \
&& ! "$_VENV_PY" -c "import bitsandbytes" >/dev/null 2>&1; then
substep "installing bitsandbytes (aarch64 wheels; enables 4-bit QLoRA)..."
if ! uv pip install --python "$_VENV_PY" "bitsandbytes>=0.45.5,!=0.46.0,!=0.48.0" >/dev/null 2>&1; then

View file

@ -219,13 +219,27 @@ _remove_path "$HOME/.unsloth/studio"
# _pkill_studio uses.
if command -v pkill >/dev/null 2>&1; then
_llama_re=$(_pkill_escape "$HOME/.unsloth/llama.cpp")
for _pat in "run_llama_build\.sh" "provision_llama_cuda\.sh" "$_llama_re"; do
pkill -TERM -f "$_pat" 2>/dev/null || true
done
# Signal the whole process GROUP of each match, not just the matching PID:
# the provisioner cds into the tree before `cmake --build build`, so cmake/
# make children carry relative argv that no pattern can match, and killing
# only the wrapper orphans them mid-build. Group kill sweeps the tree; PID
# kill remains the fallback when pgid is unreadable or shared with init.
_kill_llama_build() {
_sig="$1"
for _pat in "run_llama_build\.sh" "provision_llama_cuda\.sh" "$_llama_re"; do
for _pid in $(pgrep -f "$_pat" 2>/dev/null); do
_pgid=$(ps -o pgid= -p "$_pid" 2>/dev/null | tr -d '[:space:]')
case "$_pgid" in
''|0|1) kill -s "$_sig" "$_pid" 2>/dev/null || true ;;
*) kill -s "$_sig" -- "-$_pgid" 2>/dev/null \
|| kill -s "$_sig" "$_pid" 2>/dev/null || true ;;
esac
done
done
}
_kill_llama_build TERM
sleep 0.5
for _pat in "run_llama_build\.sh" "provision_llama_cuda\.sh" "$_llama_re"; do
pkill -KILL -f "$_pat" 2>/dev/null || true
done
_kill_llama_build KILL
fi
# Default-mode shared llama.cpp build + cache are siblings of studio (not removed
# by deleting it). No-op in env/custom mode (they nest under the custom root) and