Compare commits

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

1 commit

Author SHA1 Message Date
Daniel Han
95b50ec57a Compress installer/Studio comments to essentials (comment-only)
Follow-up to the merged #5940. Trims verbose comments across the
Windows/WSL installer and Studio backend to their load-bearing content
(constraints, env-var names, issue refs, magic-value rationale),
removing restated-code narration and multi-sentence justifications.

Comment-only and machine-verified: every .py file is AST-dump-identical
(docstrings normalized), every .ps1 is non-comment-token identical, and
every .sh has zero non-comment-line changes with bash -n clean, all
checked against main. Install suites (340 passed) and backend suites
(282 passed) unchanged. Net -159 comment lines across 15 files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 02:37:08 -07:00
15 changed files with 246 additions and 405 deletions

View file

@ -144,9 +144,8 @@ function Install-UnslothStudio {
# UNSLOTH_PYTHON pins the version (mirrors install.sh --python); default 3.13.
$PythonVersion = if ($env:UNSLOTH_PYTHON) { $env:UNSLOTH_PYTHON } else { "3.13" }
# python.org fallback patch, used only when winget is unavailable/broken AND
# the live python.org listing can't be fetched. The installer URL scheme is
# stable so an older patch still installs. Bump alongside $PythonVersion.
# python.org fallback patch for when winget fails AND the live listing is
# unreachable; the URL scheme is stable. Bump with $PythonVersion.
$PythonFallbackFullVersion = "3.13.13"
# Resolve install destinations. Priority: UNSLOTH_STUDIO_HOME, then
@ -881,13 +880,10 @@ shell.Run cmd, 0, False
}
if ($createdShortcutCount -gt 0) {
substep "Created Unsloth Studio shortcut"
# Force Explorer to re-read each new shortcut's icon so it renders
# immediately instead of a stale/generic entry (a same-name .lnk
# recreated across reinstalls keeps Explorer's cached per-item icon).
# The reliable, non-disruptive fix (no explorer restart) is a per-item
# SHChangeNotify SHCNE_UPDATEITEM + SHCNF_PATHW per .lnk; the global
# SHCNE_ASSOCCHANGED broadcast alone does NOT recover a stale item.
# Also clear the on-disk icon cache (covers heavier staleness).
# A same-name .lnk recreated across reinstalls keeps Explorer's stale
# per-item icon; per-item SHChangeNotify(SHCNE_UPDATEITEM, SHCNF_PATHW)
# fixes it (global SHCNE_ASSOCCHANGED alone does not). Also clear the
# on-disk icon cache.
try { & "$env:SystemRoot\System32\ie4uinit.exe" -ClearIconCache 2>$null } catch {}
try { & "$env:SystemRoot\System32\ie4uinit.exe" -show 2>$null } catch {}
try {
@ -896,15 +892,12 @@ shell.Run cmd, 0, False
foreach ($scPath in $createdShortcutPaths) {
try { [UnslothShell.IconRefresh]::SHChangeNotify(0x00002000, 0x0005, $scPath, [System.IntPtr]::Zero) } catch {}
}
# SHCNE_ASSOCCHANGED (0x08000000) global refresh (belt-and-suspenders)
# SHCNE_ASSOCCHANGED (0x08000000) global refresh as backup
[UnslothShell.IconRefresh]::SHChangeNotify(0x08000000, 0, $null, [System.IntPtr]::Zero)
} catch {}
# Win11's Start Menu (StartMenuExperienceHost) keeps its OWN
# pre-rendered tile-icon cache that ie4uinit/explorer restart do NOT
# invalidate, so a rewritten same-name shortcut shows the old tile
# until the host restarts. Drop only the render caches (NEVER
# start2.bin -- the pinned layout) and let the host rebuild.
# Best-effort; Win10 has no such host (Test-Path skips it).
# Win11 StartMenuExperienceHost keeps its own tile-icon cache that
# ie4uinit cannot invalidate: drop the render caches (NEVER start2.bin,
# the pinned layout) and restart the host. Win10: Test-Path skips it.
try {
$smehTemp = Join-Path $env:LOCALAPPDATA "Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\TempState"
if (Test-Path -LiteralPath $smehTemp) {
@ -1030,13 +1023,10 @@ shell.Run cmd, 0, False
}
# ── Fallback: install CPython directly from python.org ──
# Used when winget is unavailable or fails (notably msstore cert-pinning error
# 0x8a15005e, which aborts `winget install` unless --source winget is given).
# Downloads the official installer and runs it silently as a per-user install
# (no UAC), putting python.exe + the py launcher on PATH. Mirrors the uv ->
# astral.sh fallback below. Returns @{ Version; Path } or $null.
# For when winget is missing or fails (e.g. msstore cert-pinning error
# 0x8a15005e). Silent per-user install, no UAC; puts python.exe + py
# launcher on PATH. Returns @{ Version; Path } or $null.
function Install-PythonFromPythonOrg {
# python.org ships one installer per architecture.
$archSuffix = switch (Get-TauriDiagArch) {
"x86_64" { "-amd64" }
"arm64" { "-arm64" }
@ -1048,10 +1038,8 @@ shell.Run cmd, 0, False
return $null
}
# Resolve the latest $PythonVersion.x patch from the python.org listing,
# falling back to a same-minor version if the listing cannot be fetched.
# Use the pinned full version only when it matches the requested minor so a
# non-default UNSLOTH_PYTHON (e.g. 3.12) doesn't silently install 3.13.
# Latest $PythonVersion.x patch from the live listing; pinned fallback only
# if it matches the requested minor (UNSLOTH_PYTHON=3.12 must not get 3.13).
$full = if ($PythonFallbackFullVersion -like "$PythonVersion.*") { $PythonFallbackFullVersion } else { "$PythonVersion.0" }
try {
$listing = [string](Invoke-RestMethod -Uri "https://www.python.org/ftp/python/" -UseBasicParsing -TimeoutSec 20)
@ -1071,16 +1059,14 @@ shell.Run cmd, 0, False
return $null
}
# Per-user install => no UAC. PrependPath puts python + py on PATH;
# Include_launcher installs py.exe (preferred by Find-CompatiblePython).
# Per-user => no UAC; PrependPath + Include_launcher put python and py.exe on PATH.
substep "installing Python $full (silent, per-user)..."
$installArgs = @(
"/quiet",
"InstallAllUsers=0",
"PrependPath=1",
"Include_launcher=1",
# Launcher per-user too: Include_launcher defaults InstallLauncherAllUsers=1,
# which needs admin and would break this non-admin per-user fallback.
# Default InstallLauncherAllUsers=1 needs admin -- force per-user.
"InstallLauncherAllUsers=0",
"Include_pip=1",
"AssociateFiles=0",
@ -1116,13 +1102,9 @@ shell.Run cmd, 0, False
$wingetExit = $null
if ($script:WingetAvailable) {
# --source winget avoids the msstore source, which can fail with
# cert-pinning error 0x8a15005e and abort the whole `winget install`
# (winget then demands --source). Python and uv both live in the
# winget source, so pinning it is correct and faster.
#
# Lower ErrorActionPreference so winget stderr (progress/warnings) is
# not a terminating error on PS 5.1 (native stderr is ErrorRecord).
# --source winget skips msstore, whose cert-pinning error 0x8a15005e
# aborts the whole `winget install`; Python and uv both live in winget.
# Lower EAP so winget stderr is not a terminating error on PS 5.1.
$prevEAP = $ErrorActionPreference
$ErrorActionPreference = "Continue"
try {
@ -1136,10 +1118,8 @@ shell.Run cmd, 0, False
$DetectedPython = Find-CompatiblePython
if (-not $DetectedPython) {
# Python still not functional after winget -- force reinstall.
# This handles both real failures AND "already installed" codes where
# winget thinks Python is present but it's not actually on PATH
# (e.g. user partially uninstalled, or installed via a different method).
# Force reinstall: winget can report success/"already installed"
# while Python still is not on PATH (partial uninstall, other installer).
substep "Python not found on PATH after winget. Retrying with --force..." "Yellow"
$ErrorActionPreference = "Continue"
try {
@ -1152,9 +1132,7 @@ shell.Run cmd, 0, False
}
}
# Fall back to python.org if winget is unavailable OR couldn't install a
# working Python (missing/broken winget, msstore cert errors --source
# winget can't fix). Keeps the install automatic instead of failing out.
# python.org fallback when winget is missing or could not deliver a working Python.
if (-not $DetectedPython) {
if ($script:WingetAvailable) {
substep "winget could not install Python -- falling back to python.org..." "Yellow"
@ -1408,25 +1386,21 @@ shell.Run cmd, 0, False
}
# ── Helper: run amd-smi without triggering a UAC elevation prompt ──
# amd-smi on Windows auto-elevates to read GPU/APU memory, surfacing a confusing
# DiskPart UAC prompt mid-install (Studio backend amd.py hits the same).
# __COMPAT_LAYER=RunAsInvoker forces it (and helpers it spawns) to run
# un-elevated; on failure the WMI name -> gfx fallback still resolves the arch.
# amd-smi auto-elevates on Windows (confusing DiskPart UAC prompt mid-install);
# __COMPAT_LAYER=RunAsInvoker runs it un-elevated (backend amd.py does the same).
function Invoke-AmdSmiNoElevate {
param(
[Parameter(Mandatory = $true, Position = 0)][string]$Exe,
[Parameter(Position = 1)][string[]]$SmiArgs = @(),
[int]$TimeoutSec = 30
)
# RunAsInvoker blocks the auto-elevation/UAC prompt; the timeout bounds a
# flaky amd-smi that can otherwise spin for minutes (30s mirrors amd.py).
# Timeout bounds a flaky amd-smi that can spin for minutes (30s mirrors amd.py).
$prevCompat = [Environment]::GetEnvironmentVariable('__COMPAT_LAYER', 'Process')
$env:__COMPAT_LAYER = 'RunAsInvoker'
try {
# [Process]::Start, NOT Start-Process -PassThru: the latter leaves
# .ExitCode $null after WaitForExit on PS 5.1, so $LASTEXITCODE (checked
# by callers) reads non-zero and kills detection. Async reads drain the
# pipes (no deadlock); amd-smi args have no spaces so a plain join is safe.
# NOT Start-Process -PassThru: on PS 5.1 it leaves .ExitCode $null, breaking
# callers' $LASTEXITCODE checks. Async reads avoid pipe deadlock; amd-smi
# args have no spaces so a plain join is safe.
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $Exe
$psi.Arguments = ($SmiArgs -join ' ')
@ -1642,11 +1616,11 @@ shell.Run cmd, 0, False
# (gfx120X/110X/1151/1150/103X); unknown names fall back to CPU.
elseif ($ROCmGpuLabel) {
$nameArchTable = @(
@{ P = "9070 XT|9080"; A = "gfx1201" } # RDNA 4 (RX 9070 XT / 9080)
@{ P = "9070|9060"; A = "gfx1200" } # RDNA 4 (RX 9070 / 9060)
@{ P = "8060S|8050S|8040S|Strix Halo|Ryzen AI Max|AI Max"; A = "gfx1151" } # RDNA 3.5 (Strix Halo: Radeon 8060S/8050S/8040S iGPU, Ryzen AI Max+)
@{ P = "890M|880M|860M|840M|Strix Point|Krackan|HX 37[05]|AI 9 HX|AI 9 36[05]|AI 7 35[05]|AI 5 34[05]|AI 7 PRO 35|AI 5 33"; A = "gfx1150" } # RDNA 3.5 (Strix/Krackan Point: Radeon 890M/880M iGPU, Ryzen AI 9 HX 370/375)
@{ P = "RX 7900|RX 7800|RX 7700(?!S)|PRO W7900|PRO W7800|PRO W7700"; A = "gfx1100" } # RDNA 3 desktop/workstation (Navi 31)
@{ P = "9070 XT|9080"; A = "gfx1201" } # RDNA 4
@{ P = "9070|9060"; A = "gfx1200" } # RDNA 4
@{ P = "8060S|8050S|8040S|Strix Halo|Ryzen AI Max|AI Max"; A = "gfx1151" } # RDNA 3.5 (Strix Halo)
@{ P = "890M|880M|860M|840M|Strix Point|Krackan|HX 37[05]|AI 9 HX|AI 9 36[05]|AI 7 35[05]|AI 5 34[05]|AI 7 PRO 35|AI 5 33"; A = "gfx1150" } # RDNA 3.5 (Strix/Krackan Point)
@{ P = "RX 7900|RX 7800|RX 7700(?!S)|PRO W7900|PRO W7800|PRO W7700"; A = "gfx1100" } # RDNA 3 (Navi 31)
@{ P = "RX 7600|RX 7700S|RX 7650|PRO W7600|PRO W7500|PRO V710"; A = "gfx1102" } # RDNA 3 (Navi 33)
@{ P = "780M|760M|740M|Phoenix|Hawk Point|Z1 Extreme|Z2 Extreme"; A = "gfx1103" } # RDNA 3 iGPU (Phoenix / Hawk Point)
@{ P = "RX 6900|RX 6800|RX 6750|RX 6700|PRO W6800|PRO W6900"; A = "gfx1030" } # RDNA 2 (Navi 21) -- gfx103X family
@ -1707,11 +1681,9 @@ shell.Run cmd, 0, False
}
# ── Optional WSL-ROCm driver hint ────────────────────────────────────────
# An AMD GPU can also be used inside WSL2, but only with Adrenalin >= 26.2.2
# (first production ROCDXG/WSL release); native Windows GPU works with any
# recent driver. We can't auto-install it (AMD referrer-gates downloads, no
# winget package), so just point at AMD's page. Shown only when the installed
# driver predates 26.2.2 (Feb 2026); suppress with UNSLOTH_SKIP_AMD_DRIVER_HINT=1.
# WSL2 ROCm needs Adrenalin >= 26.2.2; we can't auto-install it (referrer-
# gated download, no winget package), so hint when the driver is older.
# Suppress with UNSLOTH_SKIP_AMD_DRIVER_HINT=1.
function Show-AmdWslDriverHint {
if ($env:UNSLOTH_SKIP_AMD_DRIVER_HINT) { return }
try {
@ -1728,15 +1700,13 @@ shell.Run cmd, 0, False
$drvDate = [Management.ManagementDateTimeConverter]::ToDateTime([string]$amd.DriverDate)
}
} catch {}
# Older than 26.2.2 (Feb 2026) => can't expose the GPU to WSL ROCm.
# Unreadable date => still show the hint (informational, suppressible).
# Pre-26.2.2 (Feb 2026) driver, or unreadable date => show the hint.
if ($drvDate -and $drvDate -ge (Get-Date '2026-02-01')) { return }
substep "Tip: to use this GPU inside WSL too, install AMD Adrenalin 26.2.2+ (for WSL2)." "Cyan"
substep " Your current driver predates it; native Windows GPU is unaffected. Get it from AMD:" "Cyan"
substep " https://www.amd.com/en/resources/support-articles/release-notes/RN-RAD-WIN-26-2-2.html" "Cyan"
substep " Then reboot and run this installer inside an Ubuntu-24.04 WSL distro." "Cyan"
# If WSL isn't installed yet, point at the command that provisions it
# (best-effort; wsl.exe absent => no WSL).
# No wsl.exe => suggest installing WSL.
$hasWsl = $false
try { $hasWsl = [bool](Get-Command wsl.exe -ErrorAction SilentlyContinue) } catch {}
if (-not $hasWsl) {
@ -1763,8 +1733,8 @@ shell.Run cmd, 0, False
substep " Ensure the ROCm compute driver is installed alongside the display driver:" "Yellow"
substep " https://rocm.docs.amd.com/en/latest/deploy/windows/index.html" "Yellow"
} elseif ($ROCmGfxArch) {
# Known arch: Studio setup installs AMD's bundled-runtime ROCm PyTorch wheels
# (repo.amd.com), which ship their own runtime -- HIP SDK optional.
# Known arch: Studio setup installs AMD's bundled-runtime ROCm wheels
# (repo.amd.com) -- HIP SDK optional.
step "gpu" "AMD ROCm ($ROCmGfxArch)" "Cyan"
substep "Detected: $ROCmGpuLabel" "Cyan"
substep "GPU PyTorch uses AMD's bundled-runtime ROCm wheels -- HIP SDK not required (optional)." "Cyan"
@ -1778,7 +1748,6 @@ shell.Run cmd, 0, False
step "gpu" "none (chat-only / GGUF)" "Yellow"
substep "Training and GPU inference require an NVIDIA or AMD ROCm GPU." "Yellow"
}
# On an AMD GPU (no NVIDIA), surface the optional WSL-ROCm driver hint.
if (-not $HasNvidiaSmi -and ($ROCmGfxArch -or $ROCmGpuLabel)) { Show-AmdWslDriverHint }
# ── Choose the correct PyTorch index URL based on driver CUDA version ──
@ -1866,8 +1835,7 @@ shell.Run cmd, 0, False
if (-not $SkipTorch -and -not $ROCmIndexUrl -and $TorchIndexUrl -like "*/cpu") {
Write-Host ""
if ($ROCmGfxArch) {
# Known AMD arch: install.ps1 lays down CPU PyTorch as a base, then
# setup.ps1 swaps in AMD's bundled-runtime GPU ROCm wheels (no HIP SDK).
# CPU PyTorch is only a base; setup.ps1 swaps in GPU ROCm wheels next.
substep "Installing CPU PyTorch as a base -- Studio setup installs GPU ROCm" "Cyan"
substep "wheels for $ROCmGfxArch next (bundled runtime; HIP SDK not required)." "Cyan"
} else {

View file

@ -1207,8 +1207,7 @@ STUB_EOF
# Escape single quotes for PowerShell single-quoted string embedding
_css_sc_args_ps=$(printf '%s' "$_css_sc_args" | sed "s/'/''/g")
# DISTINCT shortcut name so the WSL launcher never clobbers a native
# install's "Unsloth Studio.lnk" in the same folder. Per-distro suffix.
# Per-distro name so the WSL launcher never clobbers a native install's "Unsloth Studio.lnk".
if [ -n "$_css_distro" ]; then
_css_lnk_name="Unsloth Studio (WSL - ${_css_distro}).lnk"
else
@ -1283,8 +1282,7 @@ WSLPS1_EOF
fi
rm -f "$_css_ps1_tmp"
fi
# If WSL interop is disabled (powershell.exe "Exec format error"), the
# shortcut wasn't created; tell the user how to launch / re-enable it.
# WSL interop disabled (powershell.exe "Exec format error") => no shortcut; tell the user.
if [ "$_css_created" -ne 1 ]; then
substep "Couldn't create the Windows shortcut (WSL interop may be disabled)." "$C_WARN"
substep " Launch Studio from Windows: wsl -d \"$_css_distro\" -- bash -lc 'unsloth studio'" "$C_WARN"
@ -1708,10 +1706,9 @@ _find_no_torch_runtime() {
}
# ── AMD ROCm GPU detection helper ──
# WSL2 ROCDXG: the system rocminfo enumerates the GPU over /dev/dxg only when
# HSA_ENABLE_DXG_DETECTION=1 (a no-op on bare metal), and /opt/rocm/bin can be
# off PATH outside login shells (the profile.d drop-in). Seed both before any
# rocminfo probe or a ROCDXG WSL host is misdetected as CPU-only.
# WSL2 ROCDXG: rocminfo only sees the GPU with HSA_ENABLE_DXG_DETECTION=1
# (no-op on bare metal), and /opt/rocm/bin may be off PATH in non-login shells.
# Seed both or a ROCDXG WSL host is misdetected as CPU-only.
_ensure_rocm_probe_env() {
export HSA_ENABLE_DXG_DETECTION="${HSA_ENABLE_DXG_DETECTION:-1}"
if ! command -v rocminfo >/dev/null 2>&1 && [ -x /opt/rocm/bin/rocminfo ]; then
@ -2084,10 +2081,9 @@ _maybe_bootstrap_rocm_wsl() {
[ "${UNSLOTH_SKIP_ROCM_WSL_SETUP:-0}" = "1" ] && return 0
# Leave any already-usable GPU completely alone (NVIDIA, or working ROCm).
if _has_usable_nvidia_gpu; then return 0; fi
# "Usable ROCm" here = rocminfo enumerates the gfx1151 agent. Don't use the
# generic _has_amd_rocm_gpu: its broad gfx match accepts "gfx11-generic" and
# would skip this bootstrap while the real GPU is still unusable. awk consumes
# all input, so rocminfo isn't SIGPIPE'd like `grep -q` would under pipefail.
# Usable = rocminfo enumerates gfx1151. Not _has_amd_rocm_gpu: its broad
# match accepts "gfx11-generic" and would skip the bootstrap. awk consumes
# all input, avoiding the pipefail SIGPIPE a `grep -q` would cause.
_ensure_rocm_probe_env
if command -v rocminfo >/dev/null 2>&1 && \
rocminfo 2>/dev/null | awk '/Name:[[:space:]]*gfx1151/{found=1} END{exit !found}'; then
@ -2098,15 +2094,13 @@ _maybe_bootstrap_rocm_wsl() {
_persist_rocm_wsl_dropin
return 0
fi
# WSL GPU passthrough device must exist (present on any WSL2 GPU host).
# /dev/dxg exists on any WSL2 GPU host.
[ -e /dev/dxg ] || return 0
# Only Strix Halo (gfx1151): rocminfo can't tell us the arch yet, so match
# the CPU model string WSL exposes (e.g. "AMD Ryzen AI Max+ ... Radeon 8060S").
# Strix Halo only: rocminfo can't report the arch yet, so match the CPU model string.
grep -qiE 'Ryzen AI Max|Radeon 80[0-9]0S|Strix Halo' /proc/cpuinfo 2>/dev/null || return 0
command -v bash >/dev/null 2>&1 || return 0
# Fast path: already configured (librocdxg present) but launched from a
# non-login shell so the persisted env wasn't loaded -- just load it.
# Fast path: librocdxg present but env not loaded (non-login shell) -- load it.
if [ -e /opt/rocm/lib/librocdxg.so ] || [ -e /opt/rocm/lib64/librocdxg.so ]; then
if [ -r /etc/profile.d/unsloth-rocm-wsl.sh ]; then
# shellcheck disable=SC1091

View file

@ -5,30 +5,25 @@
# ──────────────────────────────────────────────────────────────────────────────
# Enable ROCm-on-WSL for AMD Strix Halo (Radeon 8060S / gfx1151)
# ──────────────────────────────────────────────────────────────────────────────
# install.sh already routes gfx1151 to the right ROCm wheels once a ROCm runtime
# is present; what it does NOT do is install AMD's ROCm userspace + the WSL DXG
# bridge. This helper automates that Linux-side prerequisite on Ubuntu 24.04
# WSL2 and is invoked by install.sh when it sees a Strix Halo APU in WSL (via
# /dev/dxg) but no ROCm runtime yet. Fully idempotent (re-run just re-verifies).
# Installs AMD's ROCm userspace + the WSL DXG bridge (the one part install.sh
# doesn't do) on Ubuntu 24.04 WSL2. Invoked by install.sh when it sees a Strix
# Halo APU in WSL (/dev/dxg) but no ROCm runtime. Idempotent.
#
# Manual, admin-gated Windows prerequisite: an AMD Adrenalin driver with
# production ROCDXG/WSL support (26.2.2+). install.ps1 offers to update it. Once
# installed + rebooted, /dev/dxg is exposed to WSL and this script builds the rest.
# Windows prerequisite (manual, admin): Adrenalin with production ROCDXG/WSL
# support (26.2.2+) -- install.ps1 offers it; after reboot /dev/dxg appears.
#
# HOW ROCDXG WORKS (and why older /usr/lib/wsl/lib notes are wrong): librocdxg.so
# is AMD's user-mode bridge between the Linux HSA runtime and the Windows driver
# over /dev/dxg. The STANDARD hsa-rocr runtime (NOT the gone "roc4wsl" package)
# loads it when HSA_ENABLE_DXG_DETECTION=1. No hsa/rocm libs need injecting into
# /usr/lib/wsl/lib (it holds only d3d12/dxcore), yet rocminfo enumerates gfx1151
# fine -- so we gate on /dev/dxg, not on WSL lib injection.
# How ROCDXG works (older /usr/lib/wsl/lib notes are wrong): librocdxg.so
# bridges the Linux HSA runtime to the Windows driver over /dev/dxg; the
# STANDARD hsa-rocr loads it when HSA_ENABLE_DXG_DETECTION=1. Nothing needs
# injecting into /usr/lib/wsl/lib (only d3d12/dxcore live there), so we gate
# on /dev/dxg, not on WSL lib injection.
#
# KNOWN CAVEAT (ROCm/ROCm#6022): librocdxg can cap usable ROCm VRAM at the WSL
# VM's RAM (.wslconfig [wsl2] memory=) on some BIOS UMA layouts, and amd-smi
# doesn't work in WSL. On OOM below capacity, raise memory= (then wsl --shutdown)
# and watch GPU use from Windows. Large-UMA BIOS exposes the full pool regardless.
# Caveat (ROCm/ROCm#6022): librocdxg can cap usable VRAM at the WSL VM's RAM
# (.wslconfig [wsl2] memory=) on some BIOS UMA layouts; amd-smi doesn't work
# in WSL. On OOM below capacity, raise memory= then `wsl --shutdown`.
#
# Verified on Ryzen AI Max+ PRO 395 / Radeon 8060S (gfx1151) with ROCm 7.2.1 +
# Ubuntu 24.04 + WSL2 + Adrenalin. These pins MOVE; bump + re-verify on newer ROCm.
# Verified: Ryzen AI Max+ PRO 395 / Radeon 8060S, ROCm 7.2.1, Ubuntu 24.04,
# WSL2. Pins MOVE; bump + re-verify on newer ROCm.
# ──────────────────────────────────────────────────────────────────────────────
set -euo pipefail
@ -38,11 +33,9 @@ GFX="gfx1151"
LIBROCDXG_REF="${UNSLOTH_LIBROCDXG_REF:-develop}" # ROCm/librocdxg git ref to build
# AMD's gfx1151 wheel index (same one install.sh uses); only for the smoke test.
TORCH_INDEX="${UNSLOTH_AMD_ROCM_MIRROR:-https://repo.amd.com/rocm/whl}/${GFX}/"
# Optional torch smoke test (throwaway venv). OFF by default: install.sh installs
# torch itself into the real venv right after, so a duplicate download is wasteful.
# Optional torch smoke test (throwaway venv); OFF by default since install.sh installs torch itself.
SMOKE_TEST="${UNSLOTH_WSL_SMOKE_TEST:-0}"
# REQUIRED constraint -- without it pip prefers PyPI's newer CUDA torch over the
# gfx1151 ROCm wheel. 2.11 carries AMD's real gfx1151 fix (matches install.sh).
# REQUIRED: without it pip prefers PyPI's newer CUDA torch. 2.11 carries the gfx1151 fix.
TORCH_CONSTRAINT="${UNSLOTH_WSL_TORCH_CONSTRAINT:-torch>=2.11.0,<2.12.0}"
ROCM_DIR="" # resolved after install
@ -58,8 +51,7 @@ if [ "$(id -u)" -ne 0 ]; then
fi
# ── Windows 11 SDK (headers for the librocdxg build) ─────────────────────────
# librocdxg's cmake build needs the Windows SDK 'shared' headers, which live on
# the Windows HOST under C:\Program Files (x86)\Windows Kits\10\Include\<ver>\.
# The cmake build needs the SDK 'shared' headers from the Windows host.
_WIN_SDK_INC_BASE="/mnt/c/Program Files (x86)/Windows Kits/10/Include"
# Print the newest installed SDK include dir with 'shared' headers, or nothing.
@ -73,15 +65,13 @@ _find_win_sdk() {
return 0
}
# Best-effort: install the Windows 11 SDK on the Windows HOST via winget so the
# build has its headers with no manual step. Elevates -> ONE UAC prompt; headers
# appear under /mnt/c immediately (no reboot). Never fatal -- failure falls
# through to a manual-install message. Opt out: UNSLOTH_SKIP_WIN_SDK_INSTALL=1.
# Best-effort winget install of the Win11 SDK on the Windows host (ONE UAC
# prompt; headers appear under /mnt/c immediately, no reboot). Never fatal.
# Opt out: UNSLOTH_SKIP_WIN_SDK_INSTALL=1.
_install_windows_sdk_via_winget() {
[ "${UNSLOTH_SKIP_WIN_SDK_INSTALL:-0}" = "1" ] && { note "Skipping Windows SDK auto-install (UNSLOTH_SKIP_WIN_SDK_INSTALL=1)."; return 0; }
command -v powershell.exe >/dev/null 2>&1 || return 0
# `command -v` succeeds even with WSL interop OFF (.exe on PATH but fails
# with "Exec format error"); verify it actually executes.
# command -v passes even with interop OFF ("Exec format error"); verify it runs.
powershell.exe -NoProfile -Command "exit 0" >/dev/null 2>&1 || return 0
if ! powershell.exe -NoProfile -Command "if (Get-Command winget -ErrorAction SilentlyContinue) { exit 0 } else { exit 1 }" >/dev/null 2>&1; then
note "winget not available on the Windows host -- cannot auto-install the Windows SDK."
@ -90,13 +80,11 @@ _install_windows_sdk_via_winget() {
say "Installing the Windows 11 SDK on the Windows host via winget"
note "librocdxg needs its headers. Approve the UAC prompt on the Windows desktop."
note "One-time (~1-3 GB download); opt out with UNSLOTH_SKIP_WIN_SDK_INSTALL=1."
# Newest SDK first, then a fallback. Header presence is the source of truth
# (re-check each attempt), not winget's exit code. </dev/null so winget never
# consumes a piped `curl | sh` stdin.
# Newest SDK first. Header presence (not winget exit code) is the truth;
# </dev/null keeps winget from eating a piped `curl | sh` stdin.
for _sdk_id in Microsoft.WindowsSDK.10.0.26100 Microsoft.WindowsSDK.10.0.22621; do
note "winget install ${_sdk_id} ..."
# --source winget: pin the community source so a broken default msstore
# source (the cert failure this PR fixes) can't abort SDK resolution.
# --source winget: a broken msstore source (cert failure) can't abort resolution.
powershell.exe -NoProfile -Command "winget install --id ${_sdk_id} -e --source winget --accept-source-agreements --accept-package-agreements --disable-interactivity" </dev/null || true
if [ -n "$(_find_win_sdk)" ]; then
note "Windows SDK headers present after install."
@ -120,22 +108,19 @@ if [ ! -e /dev/dxg ]; then
die "/dev/dxg missing -- WSL GPU paravirtualization not present. Ensure this is WSL2 (not WSL1) on a recent Windows build, and that an AMD GPU + ROCDXG-capable Adrenalin driver is installed on the Windows host (then reboot)."
fi
note "Ubuntu 24.04 + /dev/dxg present."
# Don't block on hsa/rocm libs in /usr/lib/wsl/lib: a working ROCDXG setup
# doesn't need them (only d3d12/dxcore). Real readiness is checked via rocminfo.
# /usr/lib/wsl/lib needs no hsa/rocm libs (only d3d12/dxcore); readiness is checked via rocminfo.
# ── Step 1: build/runtime prerequisites ──────────────────────────────────────
say "Installing build prerequisites"
export DEBIAN_FRONTEND=noninteractive
$SUDO apt-get update -y
# `make` is explicit: cmake shells out to it but Ubuntu only *recommends* it, so
# minimal images lack it and the librocdxg `make -j` build would fail.
# `make` is explicit: Ubuntu only recommends it, so minimal images lack it.
$SUDO apt-get install -y cmake make gcc g++ git wget gpg ca-certificates python3-venv python3-pip
# ── Step 2: ROCm ${ROCM_VER} userspace (no DKMS -- WSL uses the Windows driver) ─
say "Installing ROCm ${ROCM_VER} userspace"
if ! command -v rocminfo >/dev/null 2>&1 && [ ! -x /opt/rocm/bin/rocminfo ]; then
# Direct apt-repo install (leaner than amdgpu-install; repo is indexed by
# ROCm version, e.g. .../apt/7.2.1).
# Direct apt-repo install (leaner than amdgpu-install); repo indexed by ROCm version.
$SUDO mkdir -p /etc/apt/keyrings
wget -qO- https://repo.radeon.com/rocm/rocm.gpg.key \
| gpg --dearmor | $SUDO tee /etc/apt/keyrings/rocm.gpg >/dev/null
@ -144,23 +129,20 @@ if ! command -v rocminfo >/dev/null 2>&1 && [ ! -x /opt/rocm/bin/rocminfo ]; the
printf 'Package: *\nPin: release o=repo.radeon.com\nPin-Priority: 600\n' \
| $SUDO tee /etc/apt/preferences.d/rocm-pin-600 >/dev/null
$SUDO apt-get update -y
# rocm-libs pulls everything torch links at runtime (rocblas, hipblas,
# miopen-hip, rccl, ...); hsa-rocr + rocminfo come as deps. Large (~5 GB
# download / ~23 GB installed).
# rocm-libs pulls everything torch links at runtime; hsa-rocr + rocminfo
# come as deps. Large (~5 GB download / ~23 GB installed).
$SUDO apt-get install -y rocm-libs rocminfo hip-runtime-amd
else
note "ROCm already present -- skipping apt install."
fi
# Resolve the real ROCm dir and ensure the canonical /opt/rocm symlink. apt lays
# ROCm under /opt/rocm-<ver> and rocm-core symlinks /opt/rocm -> that; repair if
# an earlier partial run left /opt/rocm as a real dir blocking the symlink.
# Resolve the real ROCm dir and ensure the /opt/rocm symlink (apt installs to
# /opt/rocm-<ver>); repair a partial run that left /opt/rocm as a real dir.
_real="$(ls -d /opt/rocm-* 2>/dev/null | sort -V | tail -1 || true)"
if [ -n "$_real" ] && [ ! -L /opt/rocm ] && [ -d /opt/rocm ]; then
# /opt/rocm is a real dir blocking the symlink. Only treat it as a removable
# stray stub if it's NOT a real ROCm install (a real one has bin/rocminfo /
# bin/hipcc / .info/version) -- this protects a user's pre-existing ROCm. Even
# then we MOVE IT ASIDE, never rm -rf, so a wrong guess can't lose data.
# Treat /opt/rocm as a stray stub only if it lacks ROCm markers (bin/rocminfo,
# bin/hipcc, .info/version) -- protects a pre-existing install. Even then,
# MOVE it aside, never rm -rf, so a wrong guess can't lose data.
if [ -e /opt/rocm/bin/rocminfo ] || [ -e /opt/rocm/bin/hipcc ] || [ -e /opt/rocm/.info/version ]; then
note "/opt/rocm is a real ROCm install -- leaving it untouched (will install librocdxg into it)."
else
@ -181,9 +163,8 @@ say "Building librocdxg (${LIBROCDXG_REF})"
if [ -e "${ROCM_DIR}/lib/librocdxg.so" ]; then
note "librocdxg already installed -- skipping build."
else
# Discover the newest installed Win11 SDK (version differs per machine). If
# absent, auto-install via winget (one UAC prompt) and re-discover; only if
# that ALSO fails do we stop with manual instructions.
# Find the newest installed Win11 SDK; if absent, winget-install and retry,
# stopping with manual instructions only if that also fails.
_win_sdk="$(_find_win_sdk)"
if [ -z "$_win_sdk" ]; then
note "Windows 11 SDK headers not found -- attempting automatic install..."
@ -238,17 +219,15 @@ export LD_LIBRARY_PATH="${ROCM_DIR}/lib:${LD_LIBRARY_PATH:-}"
# ── Step 5: verify the runtime enumerates the GPU ────────────────────────────
say "Verifying rocminfo sees ${GFX}"
# Capture rocminfo into a var BEFORE grepping: piping into `grep -q` SIGPIPEs
# rocminfo on first match, which under `set -o pipefail` turns a successful match
# into a pipeline failure. Match the gfx1151 ISA "Name:" agent exactly (not a
# broad gfx1[0-9]) so a generic fallback ISA or unrelated RDNA GPU can't pass.
# Capture rocminfo BEFORE grepping: piping into `grep -q` SIGPIPEs it, which
# pipefail turns into failure on a successful match. Match the gfx1151 "Name:"
# agent exactly so a generic fallback ISA or unrelated RDNA GPU can't pass.
_rocminfo_out="$(rocminfo 2>/dev/null || true)"
if ! printf '%s\n' "$_rocminfo_out" | grep -qE "Name:[[:space:]]*${GFX}([^0-9]|$)"; then
printf '%s\n' "$_rocminfo_out" | head -25 >&2 || true
die "rocminfo did not enumerate a ${GFX} GPU agent. Most common cause: the Windows AMD driver predates production ROCDXG -- update Adrenalin (install.ps1 offers this), reboot, and re-run."
fi
# Display-only summary: best-effort (|| true) so head's early pipe-close under
# `set -o pipefail` can't fail the bootstrap after verification already passed.
# Display-only; || true so head's pipe-close under pipefail can't fail the bootstrap.
printf '%s\n' "$_rocminfo_out" | grep -E 'Marketing Name|Device Type|Compute Unit' | grep -iE "Radeon|GPU|Compute" | head -3 || true
note "ROCm-on-WSL runtime is live for ${GFX}."
@ -258,8 +237,7 @@ if [ "$SMOKE_TEST" = "1" ]; then
_venv="${HOME}/.unsloth/rocm-smoketest"
rm -rf "$_venv"; python3 -m venv "$_venv"
"$_venv/bin/pip" install --quiet --upgrade pip
# gfx1151 index is primary (torch + triton); PyPI only an extra for pure-py
# deps. The constraint keeps pip on the ROCm wheel, not a newer PyPI CUDA torch.
# gfx1151 index primary, PyPI only extra; the constraint keeps pip on the ROCm wheel.
"$_venv/bin/pip" install --index-url "$TORCH_INDEX" \
--extra-index-url https://pypi.org/simple "$TORCH_CONSTRAINT" || \
die "torch install from ${TORCH_INDEX} failed."

View file

@ -16,8 +16,7 @@ function Uninstall-UnslothStudio {
function _Step { param([string]$Msg) Write-Host $Msg }
function _Substep { param([string]$Msg, [string]$Color = "Gray") Write-Host " $Msg" -ForegroundColor $Color }
# 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).
# Remove a file/dir/symlink if present. Retries: a just-killed process can briefly hold a handle.
function _RemovePath {
param([string]$Path)
if ([string]::IsNullOrWhiteSpace($Path)) { return }
@ -240,11 +239,9 @@ function Uninstall-UnslothStudio {
} catch { }
}
# Stop processes that would block deleting the paths we remove. Unlike
# _StopStudioProcesses (venv exe only), this also catches llama-server/llama-cli,
# the unsloth.exe shim, and orphaned mp workers under SYSTEM python holding a
# venv DLL (an open DLL handle blocks the dir delete) -- found by scanning each
# candidate's loaded modules, not just its image path.
# Stop processes that would block the deletes. Unlike _StopStudioProcesses
# (venv exe only), scans loaded modules too: catches llama-server/llama-cli,
# the unsloth.exe shim, and orphaned mp workers holding a venv DLL.
function _StopProcessesLockingRoots {
param([string[]]$Roots)
$clean = @($Roots | Where-Object { $_ } | ForEach-Object { $_.TrimEnd('\','/') })
@ -263,8 +260,8 @@ function Uninstall-UnslothStudio {
}
}
} catch { }
# 2. A loaded module under a target root (orphaned mp-fork python holding a
# venv DLL). Scoped to names that load our DLLs to keep the scan fast.
# 2. Loaded module under a root (orphaned python holding a venv DLL);
# scoped to known process names to keep the scan fast.
try {
$cands = Get-Process -Name python, pythonw, unsloth, llama-server, llama-cli -ErrorAction SilentlyContinue
foreach ($proc in $cands) {
@ -280,17 +277,14 @@ function Uninstall-UnslothStudio {
# Default install root + default data dir.
$defaultStudioHome = if ($env:USERPROFILE) { Join-Path $env:USERPROFILE ".unsloth\studio" } else { $null }
$defaultDataDir = if ($env:LOCALAPPDATA) { Join-Path $env:LOCALAPPDATA "Unsloth Studio" } else { $null }
# Default-mode ~/.unsloth holds a SHARED llama.cpp build + .cache that are
# siblings of studio (not under it), so deleting <studio> misses them -- handle
# explicitly. No-op in env/custom mode (nested under the custom root, removed
# with it). A user-set UNSLOTH_LLAMA_CPP_PATH is left alone.
# Default mode: shared llama.cpp + .cache are siblings of <studio> under
# ~/.unsloth, so deleting <studio> misses them. No-op in env/custom mode;
# a user-set UNSLOTH_LLAMA_CPP_PATH is left alone.
$defaultUnslothHome = if ($env:USERPROFILE) { Join-Path $env:USERPROFILE ".unsloth" } else { $null }
$defaultLlamaCpp = if ($defaultUnslothHome) { Join-Path $defaultUnslothHome "llama.cpp" } else { $null }
$defaultCache = if ($defaultUnslothHome) { Join-Path $defaultUnslothHome ".cache" } else { $null }
# llama.cpp atomic-install staging root (install_llama_prebuilt.py .staging,
# sibling of the install dir). Usually pruned after activate, but an interrupted
# build can leave a "<name>.staging-XXXX" tree; removing it lets the empty-dir
# cleanup of ~/.unsloth below succeed. No-op in env/custom mode and when absent.
# install_llama_prebuilt.py's .staging root: an interrupted build can leave
# it behind, blocking the empty-dir cleanup of ~/.unsloth below.
$defaultStaging = if ($defaultUnslothHome) { Join-Path $defaultUnslothHome ".staging" } else { $null }
# Build known-root list FIRST so the port-file kill can verify ownership.
@ -308,8 +302,7 @@ function Uninstall-UnslothStudio {
_StopByPortFile -PortFile (Join-Path $r "share\studio.port") -KnownRoots $knownRoots
}
_StopStudioProcesses -KnownRoots $knownRoots
# Also stop anything holding a handle on the exact paths we delete (llama-server,
# the CLI shim, an mp-fork python with a venv DLL) so the dir delete isn't refused.
# Also stop anything holding a handle on the paths we delete (else the delete is refused).
_StopProcessesLockingRoots -Roots (@($knownRoots) + @($defaultDataDir, $defaultLlamaCpp, $defaultCache))
# ── Remove custom-root install trees ──
@ -329,8 +322,7 @@ function Uninstall-UnslothStudio {
if ($defaultStudioHome) { _RemovePath $defaultStudioHome }
# Default data dir.
if ($defaultDataDir) { _RemovePath $defaultDataDir }
# Default-mode shared llama.cpp build + cache (siblings of studio under
# ~/.unsloth). No-op in env/custom mode and when absent.
# Default-mode shared llama.cpp + cache (siblings of studio; no-op in env/custom mode).
if ($defaultLlamaCpp) { _RemovePath $defaultLlamaCpp }
if ($defaultCache) { _RemovePath $defaultCache }
if ($defaultStaging) { _RemovePath $defaultStaging }
@ -349,9 +341,8 @@ function Uninstall-UnslothStudio {
if ($env:APPDATA) {
_RemovePath (Join-Path $env:APPDATA "Microsoft\Windows\Start Menu\Programs\Unsloth Studio.lnk")
}
# Invalidate the Win11 Start Menu tile cache so the removed shortcut's tile
# disappears promptly instead of lingering stale (mirrors install.ps1's
# New-StudioShortcuts). Preserves start2.bin (the pin layout).
# Invalidate the Win11 Start Menu tile cache so the removed tile doesn't
# linger (mirrors install.ps1). Preserves start2.bin (the pin layout).
try {
$smehTemp = Join-Path $env:LOCALAPPDATA "Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\TempState"
if (Test-Path -LiteralPath $smehTemp) {

View file

@ -212,17 +212,14 @@ _custom_studio_roots | while IFS= read -r _custom_root; do
_remove_path "$_custom_root"
done
_remove_path "$HOME/.unsloth/studio"
# Default-mode shared llama.cpp build + cache are siblings of studio (not removed
# by deleting it). No-op in env/custom mode (they nest under the custom root) and
# when absent. A user-set UNSLOTH_LLAMA_CPP_PATH is intentionally kept.
# Default-mode shared llama.cpp + cache are siblings of studio. No-op in
# env/custom mode; a user-set UNSLOTH_LLAMA_CPP_PATH is kept.
_remove_path "$HOME/.unsloth/llama.cpp"
_remove_path "$HOME/.unsloth/.cache"
# llama.cpp atomic-install staging root (install_llama_prebuilt.py .staging).
# Normally pruned after activate, but an interrupted build can leave it behind;
# removing it lets the rmdir below succeed. No-op in env/custom mode and absent.
# install_llama_prebuilt.py's .staging root: an interrupted build can leave it
# behind, blocking the rmdir below.
_remove_path "$HOME/.unsloth/.staging"
# ROCm-on-WSL helper artifacts (librocdxg build clone + smoke-test venv). No-op
# where they don't exist; removing them lets the rmdir below succeed.
# ROCm-on-WSL helper artifacts (librocdxg clone + smoke-test venv).
_remove_path "$HOME/.unsloth/librocdxg"
_remove_path "$HOME/.unsloth/rocm-smoketest"
# Drop ~/.unsloth only if now empty (rmdir refuses non-empty, so user content is kept).
@ -259,21 +256,17 @@ case "$_os" in
Linux)
if [ "$_is_wsl" = "1" ]; then
echo "Removing WSL Windows-side shortcuts..."
# install.sh creates per-distro 'Unsloth Studio (WSL - <distro>).lnk'
# on the Windows Desktop + Start Menu via powershell.exe. Scope removal
# to THIS distro (passed as $args[0]) so a multi-distro install keeps the
# other distros' launchers; the TARGET=wsl.exe check still spares a
# native install's "Unsloth Studio.lnk". Prefer powershell.exe; test it
# can EXECUTE (`command -v` succeeds even with interop OFF -- .exe then
# fails "Exec format error", common on systemd-enabled distros).
# Remove only THIS distro's 'Unsloth Studio (WSL - <distro>).lnk' so a
# multi-distro install keeps other launchers; the wsl.exe target check
# spares a native install's lnk. Verify powershell.exe actually EXECUTES
# (`command -v` passes even with interop off -- "Exec format error").
_wsl_distro="${WSL_DISTRO_NAME:-}"
_ps_ran=0
if command -v powershell.exe >/dev/null 2>&1 && \
powershell.exe -NoProfile -Command "exit 0" >/dev/null 2>&1; then
_ps_ran=1
# Inject the distro into the command: a -Command string does not
# receive trailing tokens as $args. WSL distro names are safe to
# embed (no quotes/$/backtick).
# Inject the distro: -Command strings get no $args. Distro names
# are safe to embed (no quotes/$/backtick).
# shellcheck disable=SC2016
powershell.exe -NoProfile -Command '$distro = "'"$_wsl_distro"'";
$dirs = @(
@ -300,9 +293,8 @@ case "$_os" in
}
}' >/dev/null 2>&1 || true
fi
# Fallback when powershell.exe can't run (interop disabled): remove the
# WSL .lnk files via drvfs. The "Unsloth Studio (WSL..." name is
# WSL-specific, so a native install's "Unsloth Studio.lnk" never matches.
# Interop disabled: remove the .lnk files via drvfs. The "(WSL..." name
# is WSL-specific, so a native "Unsloth Studio.lnk" never matches.
if [ "$_ps_ran" = "0" ]; then
for _drive in /mnt/c /mnt/d /mnt/e; do
[ -d "$_drive/Users" ] || continue
@ -329,9 +321,8 @@ case "$_os" in
done
fi
# ── ROCm-on-WSL config (install_rocm_wsl_strixhalo.sh) ──
# Remove Unsloth's own ROCDXG config (the env it persisted). The system
# ROCm userspace is a shared prereq (like CUDA) and is LEFT IN PLACE by
# default; set UNSLOTH_UNINSTALL_ROCM=1 to remove it too.
# Remove only Unsloth's persisted env; system ROCm is a shared prereq,
# left in place unless UNSLOTH_UNINSTALL_ROCM=1.
echo "Removing ROCm-on-WSL config..."
_sudo=""
if [ "$_uid" != "0" ] && command -v sudo >/dev/null 2>&1; then _sudo="sudo"; fi

View file

@ -5178,14 +5178,11 @@ class LlamaCppBackend:
@staticmethod
def _fit_off_retry_eligible(cmd: "list[str]", use_fit: bool) -> bool:
"""Whether a llama-server startup crash may be retried with --fit off.
Only when Studio's own VRAM math placed the model (use_fit=False)
and nothing on the command line set the fit mode explicitly
(-fit / --fit, space- or equals-form). --fit-ctx / --fit-target /
-fitc / -fitt tune the fit step but do not select the mode, so
they do not block the retry.
"""
"""Whether a startup crash may be retried with --fit off: only when
Studio placed the model (use_fit=False) and no explicit fit-mode flag
(-fit/--fit, space- or equals-form) is on the command line. --fit-ctx,
--fit-target, -fitc, -fitt tune the step, not the mode, so they don't
block the retry."""
if use_fit:
return False
for a in cmd:
@ -5209,9 +5206,8 @@ class LlamaCppBackend:
if self._stdout_thread is not None:
self._stdout_thread.join(timeout = 2)
output = "\n".join(self._stdout_lines[-50:])
# Keep the TAIL: crash details (abort reason, ROCm/CUDA error
# text) print last, after the long startup banner. Head
# truncation has cut off exactly the diagnostic line before.
# Keep the TAIL: crash details print last, after the startup
# banner; head truncation has cut off the diagnostic line before.
_log_hint = (
f" Full log: {self._llama_log_path}"
if getattr(self, "_llama_log_path", None)

View file

@ -27,12 +27,9 @@ from pathlib import Path
from typing import Any, Callable
# ── WSL AMD Strix Halo (gfx1151): enable ROCDXG before any torch import ──────
# Mirrors main.py. In WSL the AMD GPU is reached via the ROCDXG bridge
# (librocdxg.so over /dev/dxg), which HSA loads only when HSA_ENABLE_DXG_
# DETECTION=1 is set before torch touches the GPU. A worker spawned outside a
# login shell misses the installer's persisted env and falls back to CPU.
# Gated to no-op unless BOTH /dev/dxg and librocdxg.so exist, so native Linux
# ROCm, NVIDIA, macOS and Windows are unaffected.
# Mirrors main.py: HSA loads librocdxg only with HSA_ENABLE_DXG_DETECTION=1 set
# before torch touches the GPU; a worker spawned outside a login shell misses
# the persisted env and falls back to CPU. No-op unless /dev/dxg + librocdxg exist.
if sys.platform.startswith("linux") and "HSA_ENABLE_DXG_DETECTION" not in os.environ:
try:
if os.path.exists("/dev/dxg") and any(
@ -711,13 +708,10 @@ def _rocm_classify_unified_memory(props: Any) -> tuple[str, bool]:
gcn_arch = _v
break
# Driver's own answer first: hipDeviceProp_t.integrated (exposed as
# props.is_integrated; same gate PR #5988's UMA safetensors fast-load
# uses). Strictly additive -- only a truthy value upgrades to unified;
# 0/absent falls through to the arch/name logic below, so a wheel that
# omits or zeroes the field can never downgrade the known APU set. This
# covers unified APUs outside the hardcoded arches (gfx1103 Phoenix
# iGPUs, future parts) with one universal signal.
# Driver's own answer first: hipDeviceProp_t.integrated (same gate as
# PR #5988's UMA fast-load). Strictly additive -- only truthy upgrades to
# unified; 0/absent falls through, so a wheel omitting the field can't
# downgrade the known APU set. Covers APUs beyond the hardcoded arches.
if getattr(props, "is_integrated", 0):
return gcn_arch, True
@ -2163,13 +2157,10 @@ def run_training_process(*, event_queue: Any, stop_queue: Any, config: dict) ->
os.environ["TORCHDYNAMO_DISABLE"] = "1"
logger.info("Windows ROCm: torch.compile (dynamo) disabled")
# bitsandbytes' import-time get_rocm_gpu_arch() probe runs
# `hipinfo.exe` from PATH; the AMD torch wheel ships it in the venv
# Scripts dir, which is on PATH only for activated venvs. Prepend
# it so the probe succeeds instead of logging a scary (harmless)
# "Could not detect ROCm GPU architecture" ERROR on every import.
# Normally inherited from main.py's env, but workers can also be
# spawned standalone (tests, CLI) -- keep the guard here too.
# bitsandbytes' get_rocm_gpu_arch() runs hipinfo.exe via PATH; the
# AMD wheel ships it in the venv Scripts dir (on PATH only when
# activated). Prepend it to silence a scary-but-harmless ERROR.
# Mirrors main.py for standalone-spawned workers (tests, CLI).
_scripts_dir = os.path.dirname(sys.executable)
if os.path.isfile(os.path.join(_scripts_dir, "hipInfo.exe")):
import shutil as _shutil
@ -2386,18 +2377,13 @@ def run_training_process(*, event_queue: Any, stop_queue: Any, config: dict) ->
"unified memory from device name %r; applying unified cap",
_dev_name,
)
# Unified hosts on native Windows: mem_get_info's total is the
# WDDM budget the driver grants HIP (BIOS carve + ~half of the
# remaining RAM) -- the OS share is already outside it, so the
# Linux 0.80 starve-protection double-taxes (48.49 GiB budget →
# 38.79 allowed) and blocks loads that fit in free memory.
# 1.0 removes the double-tax. Current AMD Windows wheels only
# enforce sub-1.0 fractions (measured on gfx1151: 0.5 caps,
# 1.0 still allocates past the budget via WDDM overcommit), so
# 1.0 behaves like torch's uncapped default, with WDDM
# arbitrating residency; on wheels that do enforce it, it caps
# at exactly the driver-granted budget. On Linux the total
# spans nearly all RAM, so keep the 0.80 OS headroom there.
# Native Windows unified: mem_get_info's total is the WDDM
# budget (OS share already outside it), so the Linux 0.80 cap
# double-taxes and blocks loads that fit. 1.0 removes that;
# current AMD Windows wheels enforce only sub-1.0 fractions
# (measured on gfx1151), so it acts like torch's uncapped
# default with WDDM arbitrating residency. Linux totals span
# nearly all RAM, so keep the 0.80 OS headroom there.
if _is_unified:
_mem_fraction = 1.0 if sys.platform == "win32" else 0.80
else:
@ -2411,10 +2397,8 @@ def run_training_process(*, event_queue: Any, stop_queue: Any, config: dict) ->
_dev_name,
_gcn_arch or "unknown arch",
)
# Unified Windows APUs: the WDDM budget is user-raisable, but
# nothing on the box says so -- users see "48 GB VRAM" on a
# 96 GB machine and assume a Studio bug. Say where the limit
# comes from and how to raise it.
# The WDDM budget is user-raisable but nothing on the box says
# so; tell users where the limit comes from and how to raise it.
if _is_unified and sys.platform == "win32":
try:
import psutil as _psutil

View file

@ -64,14 +64,11 @@ if sys.platform == "win32":
del _add_rocm_dll_dirs
# ── Windows AMD ROCm: make hipInfo.exe resolvable for subprocess probes ──
# bitsandbytes' get_rocm_gpu_arch() runs `hipinfo.exe` via PATH at import
# time; the AMD torch wheel ships it in the venv Scripts dir, which is on
# PATH only when the venv is activated -- Studio launches python directly.
# Without this, every bitsandbytes import logs a scary (but harmless)
# "Could not detect ROCm GPU architecture: [WinError 2]" ERROR + WARNING.
# Gated on the file existing: only AMD ROCm wheels ship hipInfo.exe, so
# NVIDIA/CPU hosts are untouched. os.add_dll_directory above does not help
# here -- subprocess PATH resolution ignores DLL search directories.
# bitsandbytes' get_rocm_gpu_arch() runs hipinfo.exe via PATH; the AMD wheel
# ships it in the venv Scripts dir, on PATH only when activated -- Studio
# launches python directly, so every import logs a scary (harmless) ERROR.
# Gated on the file existing (only AMD wheels ship it). add_dll_directory
# doesn't help: subprocess PATH resolution ignores DLL search dirs.
_scripts_dir = os.path.dirname(sys.executable)
if os.path.isfile(os.path.join(_scripts_dir, "hipInfo.exe")):
import shutil as _shutil
@ -133,13 +130,10 @@ if sys.platform == "win32":
)
# ── WSL AMD Strix Halo (gfx1151): enable ROCDXG before any torch import ──────
# In WSL the AMD GPU is reached via the ROCDXG bridge (librocdxg.so over
# /dev/dxg), which HSA loads only when HSA_ENABLE_DXG_DETECTION=1 is set BEFORE
# torch touches the GPU. A worker launched outside a login shell (e.g.
# `wsl.exe -d Ubuntu-24.04 python ...`) misses the installer's persisted env
# and silently falls back to CPU. Set it here, gated to no-op unless BOTH
# /dev/dxg AND librocdxg.so exist -- native Linux ROCm, NVIDIA, macOS and
# Windows are unaffected.
# HSA loads the librocdxg bridge only with HSA_ENABLE_DXG_DETECTION=1 set BEFORE
# torch touches the GPU; a process launched outside a login shell misses the
# installer's persisted env and silently falls back to CPU. No-op unless both
# /dev/dxg and librocdxg.so exist.
elif sys.platform.startswith("linux") and "HSA_ENABLE_DXG_DETECTION" not in os.environ:
try:
if os.path.exists("/dev/dxg") and any(

View file

@ -726,10 +726,9 @@ def _resolve_frontend_path(frontend_path: Path) -> tuple[Optional[Path], list[Pa
class _TeeStream:
"""Mirror writes to the original stream and a session log file.
Console behavior is unchanged (writes/returns delegate to the original
stream; Tauri's structured-stdout protocol and isatty probes see exactly
what they saw before). The file copy is best-effort: a full disk or a
closed handle must never break the console."""
Console behavior is unchanged (Tauri's structured-stdout protocol and
isatty probes are unaffected); the file copy is best-effort and must
never break the console."""
def __init__(self, stream, log_fh):
self._stream = stream
@ -757,15 +756,11 @@ class _TeeStream:
def _setup_server_disk_logging():
"""Tee stdout/stderr to ~/.unsloth/studio/logs/server/ and aim
faulthandler at the same file so hard crashes (access violations /
SIGSEGV in the GPU runtime) leave a stack trace on disk.
Also exports PYTHONFAULTHANDLER=1 so child Python processes (training
workers) dump native-crash stacks to their captured stderr. Keeps the
newest 20 session logs. Opt out with UNSLOTH_STUDIO_NO_FILE_LOG=1.
Returns the log path, or None when disabled/unavailable.
"""
"""Tee stdout/stderr to ~/.unsloth/studio/logs/server/ and point
faulthandler there so native crashes leave a stack trace on disk.
Exports PYTHONFAULTHANDLER=1 for child workers. Keeps the newest 20
logs; opt out with UNSLOTH_STUDIO_NO_FILE_LOG=1. Returns the log path
or None."""
if os.environ.get("UNSLOTH_STUDIO_NO_FILE_LOG") == "1":
return None
try:
@ -782,8 +777,7 @@ def _setup_server_disk_logging():
log_dir.mkdir(parents = True, exist_ok = True)
stamp = time.strftime("%Y%m%d-%H%M%S")
log_path = log_dir / f"server-{stamp}-pid{os.getpid()}.log"
# Line-buffered so the tail survives a hard kill; errors="replace"
# so a console encoding quirk can never take the server down.
# Line-buffered so the tail survives a hard kill; errors="replace" guards encoding quirks.
log_fh = open(log_path, "w", encoding = "utf-8", errors = "replace", buffering = 1)
except Exception:
return None
@ -794,8 +788,7 @@ def _setup_server_disk_logging():
faulthandler.enable(file = log_fh, all_threads = True)
except Exception:
pass
# Children (training workers) inherit: their native-crash stacks land on
# the stderr the server already captures.
# Children inherit: their native-crash stacks land on captured stderr.
os.environ.setdefault("PYTHONFAULTHANDLER", "1")
sys.stdout = _TeeStream(sys.stdout, log_fh)
@ -844,12 +837,9 @@ def run_server(
except Exception:
pass
# Persist a session log + native-crash stacks BEFORE importing main, so
# even import-time failures leave evidence on disk. Field report: Studio
# "terminates without a warning" -- a native crash in the GPU runtime
# kills the process with no Python traceback, and a desktop-shortcut
# console closes before anything can be read. Console-only logging made
# that undiagnosable.
# Arm disk logging BEFORE importing main so import-time failures leave
# evidence. Field report: native GPU-runtime crashes kill the process with
# no traceback, and the shortcut console closes before it can be read.
_session_log = _setup_server_disk_logging()
if _session_log is not None and not silent:
print(f"Session log: {_session_log}")

View file

@ -47,13 +47,9 @@ def _hip_sdk_present() -> bool:
def _amd_smi_allowed() -> bool:
"""Whether it is safe to spawn amd-smi here.
On Windows without a working HIP runtime, amd-smi elevates a child at
runtime -- popping a UAC/DiskPart prompt that RunAsInvoker can't suppress
(its manifest is asInvoker). So only call it on Windows with a HIP SDK
present or UNSLOTH_ENABLE_AMD_SMI=1. Linux amd-smi never elevates.
"""
"""Safe to spawn amd-smi? On Windows without a working HIP runtime it
elevates a child (UAC/DiskPart prompt RunAsInvoker can't suppress), so
require a HIP SDK or UNSLOTH_ENABLE_AMD_SMI=1 there. Linux never elevates."""
if platform.system() != "Windows":
return True
flag = os.environ.get("UNSLOTH_ENABLE_AMD_SMI", "").strip().lower()
@ -70,10 +66,8 @@ def _run_amd_smi(*args: str, timeout: int = _AMD_SMI_DEFAULT_TIMEOUT) -> Optiona
if _amd_smi_disabled:
return None
if not _amd_smi_allowed():
# Permanently skip amd-smi on Windows w/o a HIP SDK: every call would
# pop a UAC/DiskPart prompt (see _amd_smi_allowed). VRAM polling is then
# unavailable, but that beats the prompt. Opt back in with
# UNSLOTH_ENABLE_AMD_SMI=1.
# Permanently skip: every call would pop the UAC/DiskPart prompt (see
# _amd_smi_allowed). Opt back in with UNSLOTH_ENABLE_AMD_SMI=1.
if not _amd_smi_disabled:
logger.info(
"amd-smi disabled on Windows (no HIP SDK detected) to avoid a "
@ -83,11 +77,9 @@ def _run_amd_smi(*args: str, timeout: int = _AMD_SMI_DEFAULT_TIMEOUT) -> Optiona
_amd_smi_disabled = True
return None
if shutil.which("amd-smi") is None:
# amd-smi does not exist on Windows (neither Adrenalin nor the HIP SDK
# ship a CLI) and can be absent on minimal Linux installs. Disable the
# poller in one step instead of burning the 3-strike circuit breaker
# on guaranteed FileNotFoundError spawns. Studio's VRAM display falls
# back to torch mem_get_info.
# amd-smi doesn't exist on Windows (no AMD product ships the CLI) and
# can be absent on minimal Linux: disable in one step instead of burning
# the 3-strike breaker; VRAM display falls back to torch mem_get_info.
if not _amd_smi_disabled:
logger.info(
"amd-smi not found on PATH; GPU utilization polling via "
@ -97,8 +89,7 @@ def _run_amd_smi(*args: str, timeout: int = _AMD_SMI_DEFAULT_TIMEOUT) -> Optiona
return None
_amd_env = child_env_without_native_path_secret()
if platform.system() == "Windows":
# RunAsInvoker belt-and-suspenders for any manifest-elevating helper;
# the real guard is _amd_smi_allowed() above. Mirrors install scripts.
# RunAsInvoker is belt-and-suspenders; the real guard is _amd_smi_allowed().
_amd_env = {**_amd_env, "__COMPAT_LAYER": "RunAsInvoker"}
try:
result = subprocess.run(
@ -111,8 +102,7 @@ def _run_amd_smi(*args: str, timeout: int = _AMD_SMI_DEFAULT_TIMEOUT) -> Optiona
)
except (OSError, subprocess.TimeoutExpired) as e:
if isinstance(e, FileNotFoundError):
# Raced a PATH change after the which() check above; absence is
# expected on Windows (no AMD product ships an amd-smi CLI there).
# Raced a PATH change after the which() check; expected absent on Windows.
logger.debug("amd-smi not found (not in PATH): %s", e)
else:
logger.warning("amd-smi query failed: %s", e)

View file

@ -46,23 +46,17 @@ EXIT_FALLBACK = 2
EXIT_ERROR = 1
EXIT_BUSY = 3
# DiskPart-prompt suppression. RunAsInvoker does NOT stop amd-smi's runtime
# elevation (its manifest is asInvoker), so this is just harmless belt-and-
# suspenders for manifest-elevating tools. The real guard is _amd_smi_allowed():
# we don't spawn amd-smi on Windows w/o a HIP SDK (or opt-in).
# RunAsInvoker can't stop amd-smi's runtime elevation -- harmless belt-and-
# suspenders; the real guard is _amd_smi_allowed() (no spawn w/o HIP SDK/opt-in).
if platform.system() == "Windows":
os.environ.setdefault("__COMPAT_LAYER", "RunAsInvoker")
def _amd_smi_allowed() -> bool:
"""Whether it is safe to spawn amd-smi here.
On Windows w/o a working HIP runtime, amd-smi elevates a child and pops a
UAC/DiskPart prompt RunAsInvoker can't suppress. Only call it on Windows
when a HIP SDK is detectable (hipinfo present) or UNSLOTH_ENABLE_AMD_SMI=1;
Linux/macOS always allowed. When skipped, the gfx arch still arrives via the
forwarded --rocm-gfx, so prebuilt selection is unaffected.
"""
"""Safe to spawn amd-smi? On Windows without a HIP runtime it elevates a
child (UAC/DiskPart prompt), so require a detectable HIP SDK or
UNSLOTH_ENABLE_AMD_SMI=1 there; Linux/macOS always allowed. When skipped,
--rocm-gfx still supplies the arch."""
if platform.system() != "Windows":
return True
flag = os.environ.get("UNSLOTH_ENABLE_AMD_SMI", "").strip().lower()
@ -2721,9 +2715,8 @@ def run_capture(
check: bool = False,
env: dict[str, str] | None = None,
) -> subprocess.CompletedProcess[str]:
# amd-smi on Windows auto-elevates and pops a UAC/DiskPart prompt mid-install;
# RunAsInvoker forces it un-elevated. Callers already fall back to WMI/name
# detection. Mirrors install.ps1's Invoke-AmdSmiNoElevate; Windows-only.
# amd-smi auto-elevates on Windows (UAC/DiskPart prompt mid-install);
# RunAsInvoker forces it un-elevated (mirrors install.ps1). Windows-only.
if (
command
and platform.system() == "Windows"
@ -2992,9 +2985,8 @@ def detect_host() -> HostInfo:
_candidate = os.path.join(_root, "bin", f"{name}.exe")
if os.path.isfile(_candidate):
return _candidate
# AMD torch wheels ship hipInfo.exe into the venv Scripts dir
# (next to python.exe) -- resolvable on driver-only hosts where no
# SDK dir exists, so a standalone rerun can still detect the GPU.
# AMD torch wheels ship hipInfo.exe in the venv Scripts dir --
# resolvable on driver-only hosts with no SDK dir.
_venv_candidate = os.path.join(os.path.dirname(sys.executable), f"{name}.exe")
if os.path.isfile(_venv_candidate):
return _venv_candidate

View file

@ -42,11 +42,9 @@ IS_MAC_INTEL = IS_MACOS and platform.machine() == "x86_64"
IS_MAC_ARM = IS_MACOS and platform.machine() == "arm64"
IS_LINUX = sys.platform.startswith("linux")
# DiskPart-prompt suppression: amd-smi auto-elevates on Windows, popping a
# UAC/DiskPart prompt mid-install. This installer only spawns probes and pip/uv
# (none need elevation), so set __COMPAT_LAYER=RunAsInvoker process-wide -- every
# amd-smi subprocess then runs un-elevated, no per-call guard needed. setup.ps1
# keeps per-call guards since it ALSO spawns winget installers that need elevation.
# amd-smi auto-elevates on Windows (UAC/DiskPart prompt). This installer spawns
# only probes and pip/uv, so RunAsInvoker process-wide is safe; setup.ps1 keeps
# per-call guards because it also spawns winget installers that need elevation.
if IS_WINDOWS:
os.environ.setdefault("__COMPAT_LAYER", "RunAsInvoker")
# torchcodec ships wheels only for manylinux_2_28_x86_64, macosx_12_0_arm64,
@ -160,21 +158,17 @@ def _bnb_rocm_prerelease_url() -> str | None:
def _amd_smi_env() -> dict[str, str] | None:
"""On Windows, env with __COMPAT_LAYER=RunAsInvoker; None elsewhere.
NB: RunAsInvoker doesn't stop amd-smi's runtime elevation (its manifest is
asInvoker -- it elevates a child via ShellExecute). The real guard is
_amd_smi_allowed() below; this is harmless belt-and-suspenders."""
Belt-and-suspenders only -- RunAsInvoker can't stop amd-smi's runtime
elevation; the real guard is _amd_smi_allowed()."""
if platform.system() != "Windows":
return None
return {**os.environ, "__COMPAT_LAYER": "RunAsInvoker"}
def _amd_smi_allowed() -> bool:
"""Whether it is safe to spawn amd-smi here.
On Windows w/o a working HIP runtime, amd-smi elevates a child and pops a
UAC/DiskPart prompt RunAsInvoker can't suppress. Only call it on Windows with
a HIP SDK (hipinfo present) or UNSLOTH_ENABLE_AMD_SMI=1; Linux/macOS always.
"""
"""Safe to spawn amd-smi? On Windows without a HIP runtime it elevates a
child (UAC/DiskPart prompt), so require a HIP SDK or
UNSLOTH_ENABLE_AMD_SMI=1 there; Linux/macOS always allowed."""
if platform.system() != "Windows":
return True
flag = os.environ.get("UNSLOTH_ENABLE_AMD_SMI", "").strip().lower()
@ -210,8 +204,7 @@ def _detect_rocm_version() -> tuple[int, int] | None:
pass
# Try amd-smi version (outputs "... | ROCm version: X.Y.Z").
# Gated off on Windows w/o a HIP SDK (avoids the UAC/DiskPart prompt);
# hipconfig below covers that case.
# Gated off on Windows w/o a HIP SDK (UAC prompt); hipconfig below covers it.
amd_smi = shutil.which("amd-smi") if _amd_smi_allowed() else None
if amd_smi:
try:
@ -339,10 +332,8 @@ def _detect_windows_gfx_arch() -> str | None:
hipinfo = _candidate
break
if not hipinfo:
# 2b. AMD torch wheels ship hipInfo.exe into the venv Scripts dir
# (next to python.exe); resolvable even on driver-only hosts with no
# SDK install at all. Lets `studio update` re-detect the arch on a
# venv that already has the AMD wheel.
# 2b. AMD torch wheels ship hipInfo.exe in the venv Scripts dir --
# lets `studio update` re-detect the arch on driver-only hosts.
_venv_hipinfo = os.path.join(os.path.dirname(sys.executable), "hipInfo.exe")
if os.path.isfile(_venv_hipinfo):
hipinfo = _venv_hipinfo
@ -368,8 +359,7 @@ def _detect_windows_gfx_arch() -> str | None:
pass
# 3. amd-smi fallback -- runtime-only Radeon installs ship amd-smi but no hipinfo.
# Gated off on Windows w/o a HIP SDK (avoids the UAC/DiskPart prompt); the arch
# arrives via --rocm-gfx / name inference there, so this is only needed when safe.
# Gated off on Windows w/o a HIP SDK (UAC prompt); --rocm-gfx / name inference covers it.
amd_smi = shutil.which("amd-smi") if _amd_smi_allowed() else None
if amd_smi:
for _args in (("static", "--asic"), ("list",)):
@ -398,11 +388,9 @@ def _detect_windows_gfx_arch() -> str | None:
except Exception:
continue
# 4. Last resort: GPU marketing name via WMI → arch table. Driver-only
# hosts (Adrenalin, no HIP SDK) have neither hipinfo nor amd-smi
# (amd-smi does not exist on Windows at all), but the display driver
# always knows the GPU name. Mirrors setup.ps1's $nameArchTable so a
# standalone `studio update` can repair a CPU-only venv on such hosts.
# 4. Last resort: GPU marketing name via WMI → arch table. Driver-only hosts
# have neither hipinfo nor amd-smi, but the driver knows the GPU name;
# mirrors setup.ps1's $nameArchTable so `studio update` can repair CPU venvs.
try:
result = subprocess.run(
[
@ -437,17 +425,17 @@ def _detect_windows_gfx_arch() -> str | None:
# prebuilts / AMD Windows torch indexes support; unknown names return None
# (callers then fall back cleanly to CPU).
_WIN_GPU_NAME_ARCH_TABLE: "list[tuple[str, str]]" = [
(r"9070 XT|9080", "gfx1201"), # RDNA 4 (Radeon RX 9070 XT / 9080)
(r"9070|9060", "gfx1200"), # RDNA 4 (Radeon RX 9070 / 9060)
# RDNA 3.5 (Strix Halo: Radeon 8060S/8050S/8040S iGPU, Ryzen AI Max+)
(r"9070 XT|9080", "gfx1201"), # RDNA 4
(r"9070|9060", "gfx1200"), # RDNA 4
# RDNA 3.5 (Strix Halo)
(r"8060S|8050S|8040S|Strix Halo|Ryzen AI Max|AI Max", "gfx1151"),
# RDNA 3.5 (Strix/Krackan Point: Radeon 890M/880M iGPU, Ryzen AI 9 HX 370/375)
# RDNA 3.5 (Strix/Krackan Point)
(
r"890M|880M|860M|840M|Strix Point|Krackan|HX 37[05]|AI 9 HX|AI 9 36[05]"
r"|AI 7 35[05]|AI 5 34[05]|AI 7 PRO 35|AI 5 33",
"gfx1150",
),
# RDNA 3 desktop / workstation (Navi 31)
# RDNA 3 (Navi 31)
(r"RX 7900|RX 7800|RX 7700(?!S)|PRO W7900|PRO W7800|PRO W7700", "gfx1100"),
(r"RX 7600|RX 7700S|RX 7650|PRO W7600|PRO W7500|PRO V710", "gfx1102"), # Navi 33
# RDNA 3 iGPU (Phoenix / Hawk Point)
@ -596,8 +584,7 @@ def _has_rocm_gpu() -> bool:
exe = shutil.which(cmd[0])
if not exe:
continue
# Skip amd-smi on Windows w/o a HIP SDK (avoids the UAC/DiskPart prompt);
# rely on rocminfo / the sysfs fallback there.
# Skip amd-smi on Windows w/o a HIP SDK (UAC prompt); rocminfo/sysfs cover it.
if cmd[0] == "amd-smi" and not _amd_smi_allowed():
continue
try:
@ -2021,10 +2008,8 @@ def install_python_stack() -> int:
_wexe = shutil.which(_wcmd[0])
if not _wexe:
continue
# Skip amd-smi on Windows w/o a HIP SDK (avoids the UAC/DiskPart
# prompt), as _has_rocm_gpu()/_detect_amd_gfx_codes do. The only loss
# is the best-effort "AMD GPU detected" note; ROCm-torch state below
# comes from the install itself.
# Skip amd-smi w/o a HIP SDK (UAC prompt). Only loss: the best-effort
# "AMD GPU detected" note; ROCm-torch state comes from the install.
if _wcmd[0] == "amd-smi" and not _amd_smi_allowed():
continue
try:

View file

@ -745,25 +745,21 @@ if (-not $HasNvidiaSmi) {
}
}
# ── Helper: run amd-smi without triggering a UAC elevation prompt ──
# amd-smi on Windows auto-elevates to read GPU/APU memory, surfacing a confusing
# DiskPart UAC prompt mid-install (Studio backend amd.py hits the same). RunAsInvoker
# forces it (and helpers it spawns) to run un-elevated; on failure the WMI name ->
# gfx fallback still resolves the arch.
# amd-smi auto-elevates on Windows (confusing DiskPart UAC prompt mid-install);
# __COMPAT_LAYER=RunAsInvoker runs it un-elevated (backend amd.py does the same).
function Invoke-AmdSmiNoElevate {
param(
[Parameter(Mandatory = $true, Position = 0)][string]$Exe,
[Parameter(Position = 1)][string[]]$SmiArgs = @(),
[int]$TimeoutSec = 30
)
# RunAsInvoker blocks the auto-elevation/UAC prompt; the timeout bounds a flaky
# amd-smi that can otherwise spin for minutes (30s mirrors the backend amd.py).
# Timeout bounds a flaky amd-smi that can spin for minutes (30s mirrors amd.py).
$prevCompat = [Environment]::GetEnvironmentVariable('__COMPAT_LAYER', 'Process')
$env:__COMPAT_LAYER = 'RunAsInvoker'
try {
# [Process]::Start, NOT Start-Process -PassThru: the latter leaves .ExitCode
# $null after WaitForExit on PS 5.1, so $LASTEXITCODE (checked by callers)
# reads non-zero and kills detection. Async reads drain the pipes (no
# deadlock); amd-smi args have no spaces so a plain join is safe.
# NOT Start-Process -PassThru: on PS 5.1 it leaves .ExitCode $null, breaking
# callers' $LASTEXITCODE checks. Async reads avoid pipe deadlock; amd-smi
# args have no spaces so a plain join is safe.
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $Exe
$psi.Arguments = ($SmiArgs -join ' ')
@ -940,11 +936,11 @@ if (-not $HasNvidiaSmi) {
# (gfx120X/110X/1151/1150/103X); unknown names fall back cleanly to CPU.
elseif ($ROCmGpuLabel) {
$nameArchTable = @(
@{ P = "9070 XT|9080"; A = "gfx1201" } # RDNA 4 (Radeon RX 9070 XT / 9080)
@{ P = "9070|9060"; A = "gfx1200" } # RDNA 4 (Radeon RX 9070 / 9060)
@{ P = "8060S|8050S|8040S|Strix Halo|Ryzen AI Max|AI Max"; A = "gfx1151" } # RDNA 3.5 (Strix Halo: Radeon 8060S/8050S/8040S iGPU, Ryzen AI Max+)
@{ P = "890M|880M|860M|840M|Strix Point|Krackan|HX 37[05]|AI 9 HX|AI 9 36[05]|AI 7 35[05]|AI 5 34[05]|AI 7 PRO 35|AI 5 33"; A = "gfx1150" } # RDNA 3.5 (Strix/Krackan Point: Radeon 890M/880M iGPU, Ryzen AI 9 HX 370/375)
@{ P = "RX 7900|RX 7800|RX 7700(?!S)|PRO W7900|PRO W7800|PRO W7700"; A = "gfx1100" } # RDNA 3 desktop / workstation (Navi 31)
@{ P = "9070 XT|9080"; A = "gfx1201" } # RDNA 4
@{ P = "9070|9060"; A = "gfx1200" } # RDNA 4
@{ P = "8060S|8050S|8040S|Strix Halo|Ryzen AI Max|AI Max"; A = "gfx1151" } # RDNA 3.5 (Strix Halo)
@{ P = "890M|880M|860M|840M|Strix Point|Krackan|HX 37[05]|AI 9 HX|AI 9 36[05]|AI 7 35[05]|AI 5 34[05]|AI 7 PRO 35|AI 5 33"; A = "gfx1150" } # RDNA 3.5 (Strix/Krackan Point)
@{ P = "RX 7900|RX 7800|RX 7700(?!S)|PRO W7900|PRO W7800|PRO W7700"; A = "gfx1100" } # RDNA 3 (Navi 31)
@{ P = "RX 7600|RX 7700S|RX 7650|PRO W7600|PRO W7500|PRO V710"; A = "gfx1102" } # RDNA 3 (Navi 33)
@{ P = "780M|760M|740M|Phoenix|Hawk Point|Z1 Extreme|Z2 Extreme"; A = "gfx1103" } # RDNA 3 iGPU (Phoenix / Hawk Point)
@{ P = "RX 6900|RX 6800|RX 6750|RX 6700|PRO W6800|PRO W6900"; A = "gfx1030" } # RDNA 2 (Navi 21) -- gfx103X family
@ -1021,8 +1017,8 @@ if ($HasNvidiaSmi) {
substep " Ensure the ROCm compute driver is installed alongside the display driver:" "Yellow"
substep " https://rocm.docs.amd.com/en/latest/deploy/windows/index.html" "Yellow"
} elseif ($script:ROCmGfxArch) {
# Known arch: PyTorch comes from AMD's bundled-runtime ROCm wheels (repo.amd.com),
# which ship their own runtime -- HIP SDK optional (only adds the system toolchain).
# Known arch: PyTorch comes from AMD's bundled-runtime ROCm wheels
# (repo.amd.com) -- HIP SDK optional.
Write-Host ""
step "gpu" "AMD ROCm ($script:ROCmGfxArch)" "Cyan"
substep "Detected: $ROCmGpuLabel" "Cyan"
@ -1474,8 +1470,7 @@ if ($HasROCm) {
$rocmVerLabel = if ($script:ROCmVersionFull) { "ROCm $script:ROCmVersionFull" } elseif ($script:ROCmVersion) { "ROCm $script:ROCmVersion" } else { "ROCm (version unknown)" }
step "rocm" $rocmVerLabel
} elseif ($script:ROCmGfxArch) {
# GPU training/inference works via AMD's bundled-runtime ROCm PyTorch wheels;
# the HIP SDK is optional (only the system ROCm toolchain).
# GPU works via AMD's bundled-runtime ROCm wheels; HIP SDK optional.
step "rocm" "GPU via bundled ROCm wheels ($script:ROCmGfxArch) -- HIP SDK optional" "Cyan"
} elseif ($ROCmGpuLabel) {
step "rocm" "AMD GPU detected -- arch unknown; HIP SDK not found" "Yellow"
@ -2204,10 +2199,9 @@ if ($env:SKIP_STUDIO_BASE -ne "1" -and $env:STUDIO_LOCAL_INSTALL -ne "1") {
if ($InstalledVer -and $LatestVer -and ($InstalledVer -eq $LatestVer)) {
step "python" "$_PkgName $InstalledVer is up to date"
$SkipPythonDeps = $true
# ...but not if an AMD GPU is present and installed PyTorch is CPU-only
# (host predates ROCm-wheel support, or GPU added later): the fast "up to
# date" path would leave the user on CPU torch with Train/Export disabled.
# Force the dependency pass so the ROCm wheels get installed.
# ...unless an AMD GPU is present but installed torch is CPU-only (host
# predates ROCm-wheel support): force the dependency pass so the ROCm
# wheels install instead of leaving Train/Export disabled.
if ($script:ROCmGfxArch) {
$_torchIsCpu = $true
try {
@ -2282,12 +2276,10 @@ if ($HasNvidiaSmi) {
# Wheels bundle their own ROCm runtime; HIP SDK version is irrelevant.
$ROCmGfxArch = $script:ROCmGfxArch
$ROCmIndexUrl = $null
# Install AMD ROCm PyTorch wheels when ROCm is confirmed OR a gfx arch is known
# (name-inferred on Adrenalin-only hosts). The per-arch wheels bundle the runtime
# (rocm-sdk-libraries-<gfx>), so torch.cuda.is_available() is True without a HIP
# SDK -- which flips Studio out of chat-only (CHAT_ONLY) and enables Train/Export.
# Gating on $HasROCm alone left Strix Halo / Radeon 8060S on CPU torch; a failed
# ROCm install still falls back to CPU below, so this is safe.
# Install ROCm PyTorch when ROCm is confirmed OR a gfx arch is known (name-
# inferred, Adrenalin-only hosts): the wheels bundle the runtime, so GPU torch
# works without a HIP SDK and Studio leaves chat-only mode. Gating on $HasROCm
# alone left Strix Halo on CPU torch; a failed install still falls back to CPU.
if (($HasROCm -or $ROCmGfxArch) -and $CuTag -eq "cpu") {
$amdIndexBase = if ($env:UNSLOTH_ROCM_WINDOWS_MIRROR) { $env:UNSLOTH_ROCM_WINDOWS_MIRROR.TrimEnd('/') } else { "https://repo.amd.com/rocm/whl" }
$archFamilyMap = @{

View file

@ -854,10 +854,9 @@ fi
fi
# ── GPU detection summary (mirrors setup.ps1 step "gpu" block) ──
# WSL2 ROCDXG: the system rocminfo enumerates the GPU over /dev/dxg only when
# HSA_ENABLE_DXG_DETECTION=1 (a no-op on bare metal), and /opt/rocm/bin can be
# off PATH outside login shells (the profile.d drop-in). Seed both before the
# probes or a ROCDXG WSL host is misdetected as CPU-only.
# WSL2 ROCDXG: rocminfo only sees the GPU with HSA_ENABLE_DXG_DETECTION=1
# (no-op on bare metal), and /opt/rocm/bin may be off PATH in non-login shells.
# Seed both or a ROCDXG WSL host is misdetected as CPU-only.
export HSA_ENABLE_DXG_DETECTION="${HSA_ENABLE_DXG_DETECTION:-1}"
if ! command -v rocminfo >/dev/null 2>&1 && [ -x /opt/rocm/bin/rocminfo ]; then
PATH="$PATH:/opt/rocm/bin"
@ -918,9 +917,8 @@ elif [ "$_setup_amd_detected" = true ]; then
substep "gfx arch from UNSLOTH_ROCM_GFX_ARCH env override: $_setup_gfx"
# Name-based arch inference when tools don't report gfx (mirrors setup.ps1 nameArchTable)
elif [ -z "$_setup_gfx" ] && [ -n "$_setup_mkt" ]; then
# Kept in sync with the table in install.sh (and the PS nameArchTable).
# gfx1102 matched BEFORE gfx1100 so the spaceless "RX 7700S" lands on
# gfx1102 (bash case has no negative lookahead like the PS tables).
# In sync with install.sh's table (and the PS nameArchTable). gfx1102 comes
# BEFORE gfx1100 so "RX 7700S" matches it (bash case has no negative lookahead).
case "$_setup_mkt" in
*"9070 XT"*|*9080*) _setup_gfx="gfx1201" ;; # RDNA 4
*9070*|*9060*) _setup_gfx="gfx1200" ;; # RDNA 4

View file

@ -1252,11 +1252,9 @@ _PID_FILE = STUDIO_HOME / "studio.pid"
def _pid_alive(pid: int) -> bool:
"""Return True if a process with ``pid`` exists.
``os.kill(pid, 0)`` raises OSError (WinError 87) for every pid on Windows,
so use ``tasklist`` there and the signal-0 probe elsewhere.
"""
"""True if a process with ``pid`` exists. ``os.kill(pid, 0)`` raises
OSError (WinError 87) for every pid on Windows, so use ``tasklist``
there and the signal-0 probe elsewhere."""
if sys.platform == "win32":
try:
out = subprocess.run(