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

View file

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

View file

@ -5,30 +5,25 @@
# ────────────────────────────────────────────────────────────────────────────── # ──────────────────────────────────────────────────────────────────────────────
# Enable ROCm-on-WSL for AMD Strix Halo (Radeon 8060S / gfx1151) # 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 # Installs AMD's ROCm userspace + the WSL DXG bridge (the one part install.sh
# is present; what it does NOT do is install AMD's ROCm userspace + the WSL DXG # doesn't do) on Ubuntu 24.04 WSL2. Invoked by install.sh when it sees a Strix
# bridge. This helper automates that Linux-side prerequisite on Ubuntu 24.04 # Halo APU in WSL (/dev/dxg) but no ROCm runtime. Idempotent.
# 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).
# #
# Manual, admin-gated Windows prerequisite: an AMD Adrenalin driver with # Windows prerequisite (manual, admin): Adrenalin with production ROCDXG/WSL
# production ROCDXG/WSL support (26.2.2+). install.ps1 offers to update it. Once # support (26.2.2+) -- install.ps1 offers it; after reboot /dev/dxg appears.
# installed + rebooted, /dev/dxg is exposed to WSL and this script builds the rest.
# #
# HOW ROCDXG WORKS (and why older /usr/lib/wsl/lib notes are wrong): librocdxg.so # How ROCDXG works (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 # bridges the Linux HSA runtime to the Windows driver over /dev/dxg; the
# over /dev/dxg. The STANDARD hsa-rocr runtime (NOT the gone "roc4wsl" package) # STANDARD hsa-rocr loads it when HSA_ENABLE_DXG_DETECTION=1. Nothing needs
# loads it when HSA_ENABLE_DXG_DETECTION=1. No hsa/rocm libs need injecting into # injecting into /usr/lib/wsl/lib (only d3d12/dxcore live there), so we gate
# /usr/lib/wsl/lib (it holds only d3d12/dxcore), yet rocminfo enumerates gfx1151 # on /dev/dxg, not on WSL lib injection.
# fine -- 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 # Caveat (ROCm/ROCm#6022): librocdxg can cap usable VRAM at the WSL VM's RAM
# VM's RAM (.wslconfig [wsl2] memory=) on some BIOS UMA layouts, and amd-smi # (.wslconfig [wsl2] memory=) on some BIOS UMA layouts; amd-smi doesn't work
# doesn't work in WSL. On OOM below capacity, raise memory= (then wsl --shutdown) # 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.
# #
# Verified on Ryzen AI Max+ PRO 395 / Radeon 8060S (gfx1151) with ROCm 7.2.1 + # Verified: Ryzen AI Max+ PRO 395 / Radeon 8060S, ROCm 7.2.1, Ubuntu 24.04,
# Ubuntu 24.04 + WSL2 + Adrenalin. These pins MOVE; bump + re-verify on newer ROCm. # WSL2. Pins MOVE; bump + re-verify on newer ROCm.
# ────────────────────────────────────────────────────────────────────────────── # ──────────────────────────────────────────────────────────────────────────────
set -euo pipefail set -euo pipefail
@ -38,11 +33,9 @@ GFX="gfx1151"
LIBROCDXG_REF="${UNSLOTH_LIBROCDXG_REF:-develop}" # ROCm/librocdxg git ref to build 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. # 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}/" 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 # Optional torch smoke test (throwaway venv); OFF by default since install.sh installs torch itself.
# torch itself into the real venv right after, so a duplicate download is wasteful.
SMOKE_TEST="${UNSLOTH_WSL_SMOKE_TEST:-0}" SMOKE_TEST="${UNSLOTH_WSL_SMOKE_TEST:-0}"
# REQUIRED constraint -- without it pip prefers PyPI's newer CUDA torch over the # REQUIRED: without it pip prefers PyPI's newer CUDA torch. 2.11 carries the gfx1151 fix.
# gfx1151 ROCm wheel. 2.11 carries AMD's real gfx1151 fix (matches install.sh).
TORCH_CONSTRAINT="${UNSLOTH_WSL_TORCH_CONSTRAINT:-torch>=2.11.0,<2.12.0}" TORCH_CONSTRAINT="${UNSLOTH_WSL_TORCH_CONSTRAINT:-torch>=2.11.0,<2.12.0}"
ROCM_DIR="" # resolved after install ROCM_DIR="" # resolved after install
@ -58,8 +51,7 @@ if [ "$(id -u)" -ne 0 ]; then
fi fi
# ── Windows 11 SDK (headers for the librocdxg build) ───────────────────────── # ── Windows 11 SDK (headers for the librocdxg build) ─────────────────────────
# librocdxg's cmake build needs the Windows SDK 'shared' headers, which live on # The cmake build needs the SDK 'shared' headers from the Windows host.
# the Windows HOST under C:\Program Files (x86)\Windows Kits\10\Include\<ver>\.
_WIN_SDK_INC_BASE="/mnt/c/Program Files (x86)/Windows Kits/10/Include" _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. # Print the newest installed SDK include dir with 'shared' headers, or nothing.
@ -73,15 +65,13 @@ _find_win_sdk() {
return 0 return 0
} }
# Best-effort: install the Windows 11 SDK on the Windows HOST via winget so the # Best-effort winget install of the Win11 SDK on the Windows host (ONE UAC
# build has its headers with no manual step. Elevates -> ONE UAC prompt; headers # prompt; headers appear under /mnt/c immediately, no reboot). Never fatal.
# appear under /mnt/c immediately (no reboot). Never fatal -- failure falls # Opt out: UNSLOTH_SKIP_WIN_SDK_INSTALL=1.
# through to a manual-install message. Opt out: UNSLOTH_SKIP_WIN_SDK_INSTALL=1.
_install_windows_sdk_via_winget() { _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; } [ "${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 powershell.exe >/dev/null 2>&1 || return 0
# `command -v` succeeds even with WSL interop OFF (.exe on PATH but fails # command -v passes even with interop OFF ("Exec format error"); verify it runs.
# with "Exec format error"); verify it actually executes.
powershell.exe -NoProfile -Command "exit 0" >/dev/null 2>&1 || return 0 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 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." 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" 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 "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." 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 # Newest SDK first. Header presence (not winget exit code) is the truth;
# (re-check each attempt), not winget's exit code. </dev/null so winget never # </dev/null keeps winget from eating a piped `curl | sh` stdin.
# consumes a piped `curl | sh` stdin.
for _sdk_id in Microsoft.WindowsSDK.10.0.26100 Microsoft.WindowsSDK.10.0.22621; do for _sdk_id in Microsoft.WindowsSDK.10.0.26100 Microsoft.WindowsSDK.10.0.22621; do
note "winget install ${_sdk_id} ..." note "winget install ${_sdk_id} ..."
# --source winget: pin the community source so a broken default msstore # --source winget: a broken msstore source (cert failure) can't abort resolution.
# source (the cert failure this PR fixes) can't abort SDK resolution.
powershell.exe -NoProfile -Command "winget install --id ${_sdk_id} -e --source winget --accept-source-agreements --accept-package-agreements --disable-interactivity" </dev/null || true 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 if [ -n "$(_find_win_sdk)" ]; then
note "Windows SDK headers present after install." 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)." 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 fi
note "Ubuntu 24.04 + /dev/dxg present." note "Ubuntu 24.04 + /dev/dxg present."
# Don't block on hsa/rocm libs in /usr/lib/wsl/lib: a working ROCDXG setup # /usr/lib/wsl/lib needs no hsa/rocm libs (only d3d12/dxcore); readiness is checked via rocminfo.
# doesn't need them (only d3d12/dxcore). Real readiness is checked via rocminfo.
# ── Step 1: build/runtime prerequisites ────────────────────────────────────── # ── Step 1: build/runtime prerequisites ──────────────────────────────────────
say "Installing build prerequisites" say "Installing build prerequisites"
export DEBIAN_FRONTEND=noninteractive export DEBIAN_FRONTEND=noninteractive
$SUDO apt-get update -y $SUDO apt-get update -y
# `make` is explicit: cmake shells out to it but Ubuntu only *recommends* it, so # `make` is explicit: Ubuntu only recommends it, so minimal images lack it.
# minimal images lack it and the librocdxg `make -j` build would fail.
$SUDO apt-get install -y cmake make gcc g++ git wget gpg ca-certificates python3-venv python3-pip $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) ─ # ── Step 2: ROCm ${ROCM_VER} userspace (no DKMS -- WSL uses the Windows driver) ─
say "Installing ROCm ${ROCM_VER} userspace" say "Installing ROCm ${ROCM_VER} userspace"
if ! command -v rocminfo >/dev/null 2>&1 && [ ! -x /opt/rocm/bin/rocminfo ]; then 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 # Direct apt-repo install (leaner than amdgpu-install); repo indexed by ROCm version.
# ROCm version, e.g. .../apt/7.2.1).
$SUDO mkdir -p /etc/apt/keyrings $SUDO mkdir -p /etc/apt/keyrings
wget -qO- https://repo.radeon.com/rocm/rocm.gpg.key \ wget -qO- https://repo.radeon.com/rocm/rocm.gpg.key \
| gpg --dearmor | $SUDO tee /etc/apt/keyrings/rocm.gpg >/dev/null | 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' \ printf 'Package: *\nPin: release o=repo.radeon.com\nPin-Priority: 600\n' \
| $SUDO tee /etc/apt/preferences.d/rocm-pin-600 >/dev/null | $SUDO tee /etc/apt/preferences.d/rocm-pin-600 >/dev/null
$SUDO apt-get update -y $SUDO apt-get update -y
# rocm-libs pulls everything torch links at runtime (rocblas, hipblas, # rocm-libs pulls everything torch links at runtime; hsa-rocr + rocminfo
# miopen-hip, rccl, ...); hsa-rocr + rocminfo come as deps. Large (~5 GB # come as deps. Large (~5 GB download / ~23 GB installed).
# download / ~23 GB installed).
$SUDO apt-get install -y rocm-libs rocminfo hip-runtime-amd $SUDO apt-get install -y rocm-libs rocminfo hip-runtime-amd
else else
note "ROCm already present -- skipping apt install." note "ROCm already present -- skipping apt install."
fi fi
# Resolve the real ROCm dir and ensure the canonical /opt/rocm symlink. apt lays # Resolve the real ROCm dir and ensure the /opt/rocm symlink (apt installs to
# ROCm under /opt/rocm-<ver> and rocm-core symlinks /opt/rocm -> that; repair if # /opt/rocm-<ver>); repair a partial run that left /opt/rocm as a real dir.
# an earlier partial run left /opt/rocm as a real dir blocking the symlink.
_real="$(ls -d /opt/rocm-* 2>/dev/null | sort -V | tail -1 || true)" _real="$(ls -d /opt/rocm-* 2>/dev/null | sort -V | tail -1 || true)"
if [ -n "$_real" ] && [ ! -L /opt/rocm ] && [ -d /opt/rocm ]; then 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 # Treat /opt/rocm as a stray stub only if it lacks ROCm markers (bin/rocminfo,
# stray stub if it's NOT a real ROCm install (a real one has bin/rocminfo / # bin/hipcc, .info/version) -- protects a pre-existing install. Even then,
# bin/hipcc / .info/version) -- this protects a user's pre-existing ROCm. Even # MOVE it aside, never rm -rf, so a wrong guess can't lose data.
# then we 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 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)." note "/opt/rocm is a real ROCm install -- leaving it untouched (will install librocdxg into it)."
else else
@ -181,9 +163,8 @@ say "Building librocdxg (${LIBROCDXG_REF})"
if [ -e "${ROCM_DIR}/lib/librocdxg.so" ]; then if [ -e "${ROCM_DIR}/lib/librocdxg.so" ]; then
note "librocdxg already installed -- skipping build." note "librocdxg already installed -- skipping build."
else else
# Discover the newest installed Win11 SDK (version differs per machine). If # Find the newest installed Win11 SDK; if absent, winget-install and retry,
# absent, auto-install via winget (one UAC prompt) and re-discover; only if # stopping with manual instructions only if that also fails.
# that ALSO fails do we stop with manual instructions.
_win_sdk="$(_find_win_sdk)" _win_sdk="$(_find_win_sdk)"
if [ -z "$_win_sdk" ]; then if [ -z "$_win_sdk" ]; then
note "Windows 11 SDK headers not found -- attempting automatic install..." 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 ──────────────────────────── # ── Step 5: verify the runtime enumerates the GPU ────────────────────────────
say "Verifying rocminfo sees ${GFX}" say "Verifying rocminfo sees ${GFX}"
# Capture rocminfo into a var BEFORE grepping: piping into `grep -q` SIGPIPEs # Capture rocminfo BEFORE grepping: piping into `grep -q` SIGPIPEs it, which
# rocminfo on first match, which under `set -o pipefail` turns a successful match # pipefail turns into failure on a successful match. Match the gfx1151 "Name:"
# into a pipeline failure. Match the gfx1151 ISA "Name:" agent exactly (not a # agent exactly so a generic fallback ISA or unrelated RDNA GPU can't pass.
# broad gfx1[0-9]) so a generic fallback ISA or unrelated RDNA GPU can't pass.
_rocminfo_out="$(rocminfo 2>/dev/null || true)" _rocminfo_out="$(rocminfo 2>/dev/null || true)"
if ! printf '%s\n' "$_rocminfo_out" | grep -qE "Name:[[:space:]]*${GFX}([^0-9]|$)"; then if ! printf '%s\n' "$_rocminfo_out" | grep -qE "Name:[[:space:]]*${GFX}([^0-9]|$)"; then
printf '%s\n' "$_rocminfo_out" | head -25 >&2 || true 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." 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 fi
# Display-only summary: best-effort (|| true) so head's early pipe-close under # Display-only; || true so head's pipe-close under pipefail can't fail the bootstrap.
# `set -o pipefail` can't fail the bootstrap after verification already passed.
printf '%s\n' "$_rocminfo_out" | grep -E 'Marketing Name|Device Type|Compute Unit' | grep -iE "Radeon|GPU|Compute" | head -3 || true 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}." note "ROCm-on-WSL runtime is live for ${GFX}."
@ -258,8 +237,7 @@ if [ "$SMOKE_TEST" = "1" ]; then
_venv="${HOME}/.unsloth/rocm-smoketest" _venv="${HOME}/.unsloth/rocm-smoketest"
rm -rf "$_venv"; python3 -m venv "$_venv" rm -rf "$_venv"; python3 -m venv "$_venv"
"$_venv/bin/pip" install --quiet --upgrade pip "$_venv/bin/pip" install --quiet --upgrade pip
# gfx1151 index is primary (torch + triton); PyPI only an extra for pure-py # gfx1151 index primary, PyPI only extra; the constraint keeps pip on the ROCm wheel.
# deps. The constraint keeps pip on the ROCm wheel, not a newer PyPI CUDA torch.
"$_venv/bin/pip" install --index-url "$TORCH_INDEX" \ "$_venv/bin/pip" install --index-url "$TORCH_INDEX" \
--extra-index-url https://pypi.org/simple "$TORCH_CONSTRAINT" || \ --extra-index-url https://pypi.org/simple "$TORCH_CONSTRAINT" || \
die "torch install from ${TORCH_INDEX} failed." 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 _Step { param([string]$Msg) Write-Host $Msg }
function _Substep { param([string]$Msg, [string]$Color = "Gray") Write-Host " $Msg" -ForegroundColor $Color } 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 # Remove a file/dir/symlink if present. Retries: a just-killed process can briefly hold a handle.
# process can briefly hold a handle (Windows refuses the delete until released).
function _RemovePath { function _RemovePath {
param([string]$Path) param([string]$Path)
if ([string]::IsNullOrWhiteSpace($Path)) { return } if ([string]::IsNullOrWhiteSpace($Path)) { return }
@ -240,11 +239,9 @@ function Uninstall-UnslothStudio {
} catch { } } catch { }
} }
# Stop processes that would block deleting the paths we remove. Unlike # Stop processes that would block the deletes. Unlike _StopStudioProcesses
# _StopStudioProcesses (venv exe only), this also catches llama-server/llama-cli, # (venv exe only), scans loaded modules too: catches llama-server/llama-cli,
# the unsloth.exe shim, and orphaned mp workers under SYSTEM python holding a # the unsloth.exe shim, and orphaned mp workers holding a venv DLL.
# venv DLL (an open DLL handle blocks the dir delete) -- found by scanning each
# candidate's loaded modules, not just its image path.
function _StopProcessesLockingRoots { function _StopProcessesLockingRoots {
param([string[]]$Roots) param([string[]]$Roots)
$clean = @($Roots | Where-Object { $_ } | ForEach-Object { $_.TrimEnd('\','/') }) $clean = @($Roots | Where-Object { $_ } | ForEach-Object { $_.TrimEnd('\','/') })
@ -263,8 +260,8 @@ function Uninstall-UnslothStudio {
} }
} }
} catch { } } catch { }
# 2. A loaded module under a target root (orphaned mp-fork python holding a # 2. Loaded module under a root (orphaned python holding a venv DLL);
# venv DLL). Scoped to names that load our DLLs to keep the scan fast. # scoped to known process names to keep the scan fast.
try { try {
$cands = Get-Process -Name python, pythonw, unsloth, llama-server, llama-cli -ErrorAction SilentlyContinue $cands = Get-Process -Name python, pythonw, unsloth, llama-server, llama-cli -ErrorAction SilentlyContinue
foreach ($proc in $cands) { foreach ($proc in $cands) {
@ -280,17 +277,14 @@ function Uninstall-UnslothStudio {
# Default install root + default data dir. # Default install root + default data dir.
$defaultStudioHome = if ($env:USERPROFILE) { Join-Path $env:USERPROFILE ".unsloth\studio" } else { $null } $defaultStudioHome = if ($env:USERPROFILE) { Join-Path $env:USERPROFILE ".unsloth\studio" } else { $null }
$defaultDataDir = if ($env:LOCALAPPDATA) { Join-Path $env:LOCALAPPDATA "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 # Default mode: shared llama.cpp + .cache are siblings of <studio> under
# siblings of studio (not under it), so deleting <studio> misses them -- handle # ~/.unsloth, so deleting <studio> misses them. No-op in env/custom mode;
# explicitly. No-op in env/custom mode (nested under the custom root, removed # a user-set UNSLOTH_LLAMA_CPP_PATH is left alone.
# with it). A user-set UNSLOTH_LLAMA_CPP_PATH is left alone.
$defaultUnslothHome = if ($env:USERPROFILE) { Join-Path $env:USERPROFILE ".unsloth" } else { $null } $defaultUnslothHome = if ($env:USERPROFILE) { Join-Path $env:USERPROFILE ".unsloth" } else { $null }
$defaultLlamaCpp = if ($defaultUnslothHome) { Join-Path $defaultUnslothHome "llama.cpp" } else { $null } $defaultLlamaCpp = if ($defaultUnslothHome) { Join-Path $defaultUnslothHome "llama.cpp" } else { $null }
$defaultCache = if ($defaultUnslothHome) { Join-Path $defaultUnslothHome ".cache" } else { $null } $defaultCache = if ($defaultUnslothHome) { Join-Path $defaultUnslothHome ".cache" } else { $null }
# llama.cpp atomic-install staging root (install_llama_prebuilt.py .staging, # install_llama_prebuilt.py's .staging root: an interrupted build can leave
# sibling of the install dir). Usually pruned after activate, but an interrupted # it behind, blocking the empty-dir cleanup of ~/.unsloth below.
# 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.
$defaultStaging = if ($defaultUnslothHome) { Join-Path $defaultUnslothHome ".staging" } else { $null } $defaultStaging = if ($defaultUnslothHome) { Join-Path $defaultUnslothHome ".staging" } else { $null }
# Build known-root list FIRST so the port-file kill can verify ownership. # 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 _StopByPortFile -PortFile (Join-Path $r "share\studio.port") -KnownRoots $knownRoots
} }
_StopStudioProcesses -KnownRoots $knownRoots _StopStudioProcesses -KnownRoots $knownRoots
# Also stop anything holding a handle on the exact paths we delete (llama-server, # Also stop anything holding a handle on the paths we delete (else the delete is refused).
# the CLI shim, an mp-fork python with a venv DLL) so the dir delete isn't refused.
_StopProcessesLockingRoots -Roots (@($knownRoots) + @($defaultDataDir, $defaultLlamaCpp, $defaultCache)) _StopProcessesLockingRoots -Roots (@($knownRoots) + @($defaultDataDir, $defaultLlamaCpp, $defaultCache))
# ── Remove custom-root install trees ── # ── Remove custom-root install trees ──
@ -329,8 +322,7 @@ function Uninstall-UnslothStudio {
if ($defaultStudioHome) { _RemovePath $defaultStudioHome } if ($defaultStudioHome) { _RemovePath $defaultStudioHome }
# Default data dir. # Default data dir.
if ($defaultDataDir) { _RemovePath $defaultDataDir } if ($defaultDataDir) { _RemovePath $defaultDataDir }
# Default-mode shared llama.cpp build + cache (siblings of studio under # Default-mode shared llama.cpp + cache (siblings of studio; no-op in env/custom mode).
# ~/.unsloth). No-op in env/custom mode and when absent.
if ($defaultLlamaCpp) { _RemovePath $defaultLlamaCpp } if ($defaultLlamaCpp) { _RemovePath $defaultLlamaCpp }
if ($defaultCache) { _RemovePath $defaultCache } if ($defaultCache) { _RemovePath $defaultCache }
if ($defaultStaging) { _RemovePath $defaultStaging } if ($defaultStaging) { _RemovePath $defaultStaging }
@ -349,9 +341,8 @@ function Uninstall-UnslothStudio {
if ($env:APPDATA) { if ($env:APPDATA) {
_RemovePath (Join-Path $env:APPDATA "Microsoft\Windows\Start Menu\Programs\Unsloth Studio.lnk") _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 # Invalidate the Win11 Start Menu tile cache so the removed tile doesn't
# disappears promptly instead of lingering stale (mirrors install.ps1's # linger (mirrors install.ps1). Preserves start2.bin (the pin layout).
# New-StudioShortcuts). Preserves start2.bin (the pin layout).
try { try {
$smehTemp = Join-Path $env:LOCALAPPDATA "Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\TempState" $smehTemp = Join-Path $env:LOCALAPPDATA "Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\TempState"
if (Test-Path -LiteralPath $smehTemp) { 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" _remove_path "$_custom_root"
done done
_remove_path "$HOME/.unsloth/studio" _remove_path "$HOME/.unsloth/studio"
# Default-mode shared llama.cpp build + cache are siblings of studio (not removed # Default-mode shared llama.cpp + cache are siblings of studio. No-op in
# by deleting it). No-op in env/custom mode (they nest under the custom root) and # env/custom mode; a user-set UNSLOTH_LLAMA_CPP_PATH is kept.
# when absent. A user-set UNSLOTH_LLAMA_CPP_PATH is intentionally kept.
_remove_path "$HOME/.unsloth/llama.cpp" _remove_path "$HOME/.unsloth/llama.cpp"
_remove_path "$HOME/.unsloth/.cache" _remove_path "$HOME/.unsloth/.cache"
# llama.cpp atomic-install staging root (install_llama_prebuilt.py .staging). # install_llama_prebuilt.py's .staging root: an interrupted build can leave it
# Normally pruned after activate, but an interrupted build can leave it behind; # behind, blocking the rmdir below.
# removing it lets the rmdir below succeed. No-op in env/custom mode and absent.
_remove_path "$HOME/.unsloth/.staging" _remove_path "$HOME/.unsloth/.staging"
# ROCm-on-WSL helper artifacts (librocdxg build clone + smoke-test venv). No-op # ROCm-on-WSL helper artifacts (librocdxg clone + smoke-test venv).
# where they don't exist; removing them lets the rmdir below succeed.
_remove_path "$HOME/.unsloth/librocdxg" _remove_path "$HOME/.unsloth/librocdxg"
_remove_path "$HOME/.unsloth/rocm-smoketest" _remove_path "$HOME/.unsloth/rocm-smoketest"
# Drop ~/.unsloth only if now empty (rmdir refuses non-empty, so user content is kept). # Drop ~/.unsloth only if now empty (rmdir refuses non-empty, so user content is kept).
@ -259,21 +256,17 @@ case "$_os" in
Linux) Linux)
if [ "$_is_wsl" = "1" ]; then if [ "$_is_wsl" = "1" ]; then
echo "Removing WSL Windows-side shortcuts..." echo "Removing WSL Windows-side shortcuts..."
# install.sh creates per-distro 'Unsloth Studio (WSL - <distro>).lnk' # Remove only THIS distro's 'Unsloth Studio (WSL - <distro>).lnk' so a
# on the Windows Desktop + Start Menu via powershell.exe. Scope removal # multi-distro install keeps other launchers; the wsl.exe target check
# to THIS distro (passed as $args[0]) so a multi-distro install keeps the # spares a native install's lnk. Verify powershell.exe actually EXECUTES
# other distros' launchers; the TARGET=wsl.exe check still spares a # (`command -v` passes even with interop off -- "Exec format error").
# 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).
_wsl_distro="${WSL_DISTRO_NAME:-}" _wsl_distro="${WSL_DISTRO_NAME:-}"
_ps_ran=0 _ps_ran=0
if command -v powershell.exe >/dev/null 2>&1 && \ if command -v powershell.exe >/dev/null 2>&1 && \
powershell.exe -NoProfile -Command "exit 0" >/dev/null 2>&1; then powershell.exe -NoProfile -Command "exit 0" >/dev/null 2>&1; then
_ps_ran=1 _ps_ran=1
# Inject the distro into the command: a -Command string does not # Inject the distro: -Command strings get no $args. Distro names
# receive trailing tokens as $args. WSL distro names are safe to # are safe to embed (no quotes/$/backtick).
# embed (no quotes/$/backtick).
# shellcheck disable=SC2016 # shellcheck disable=SC2016
powershell.exe -NoProfile -Command '$distro = "'"$_wsl_distro"'"; powershell.exe -NoProfile -Command '$distro = "'"$_wsl_distro"'";
$dirs = @( $dirs = @(
@ -300,9 +293,8 @@ case "$_os" in
} }
}' >/dev/null 2>&1 || true }' >/dev/null 2>&1 || true
fi fi
# Fallback when powershell.exe can't run (interop disabled): remove the # Interop disabled: remove the .lnk files via drvfs. The "(WSL..." name
# WSL .lnk files via drvfs. The "Unsloth Studio (WSL..." name is # is WSL-specific, so a native "Unsloth Studio.lnk" never matches.
# WSL-specific, so a native install's "Unsloth Studio.lnk" never matches.
if [ "$_ps_ran" = "0" ]; then if [ "$_ps_ran" = "0" ]; then
for _drive in /mnt/c /mnt/d /mnt/e; do for _drive in /mnt/c /mnt/d /mnt/e; do
[ -d "$_drive/Users" ] || continue [ -d "$_drive/Users" ] || continue
@ -329,9 +321,8 @@ case "$_os" in
done done
fi fi
# ── ROCm-on-WSL config (install_rocm_wsl_strixhalo.sh) ── # ── ROCm-on-WSL config (install_rocm_wsl_strixhalo.sh) ──
# Remove Unsloth's own ROCDXG config (the env it persisted). The system # Remove only Unsloth's persisted env; system ROCm is a shared prereq,
# ROCm userspace is a shared prereq (like CUDA) and is LEFT IN PLACE by # left in place unless UNSLOTH_UNINSTALL_ROCM=1.
# default; set UNSLOTH_UNINSTALL_ROCM=1 to remove it too.
echo "Removing ROCm-on-WSL config..." echo "Removing ROCm-on-WSL config..."
_sudo="" _sudo=""
if [ "$_uid" != "0" ] && command -v sudo >/dev/null 2>&1; then _sudo="sudo"; fi if [ "$_uid" != "0" ] && command -v sudo >/dev/null 2>&1; then _sudo="sudo"; fi

View file

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

View file

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

View file

@ -64,14 +64,11 @@ if sys.platform == "win32":
del _add_rocm_dll_dirs del _add_rocm_dll_dirs
# ── Windows AMD ROCm: make hipInfo.exe resolvable for subprocess probes ── # ── Windows AMD ROCm: make hipInfo.exe resolvable for subprocess probes ──
# bitsandbytes' get_rocm_gpu_arch() runs `hipinfo.exe` via PATH at import # bitsandbytes' get_rocm_gpu_arch() runs hipinfo.exe via PATH; the AMD wheel
# time; the AMD torch wheel ships it in the venv Scripts dir, which is on # ships it in the venv Scripts dir, on PATH only when activated -- Studio
# PATH only when the venv is activated -- Studio launches python directly. # launches python directly, so every import logs a scary (harmless) ERROR.
# Without this, every bitsandbytes import logs a scary (but harmless) # Gated on the file existing (only AMD wheels ship it). add_dll_directory
# "Could not detect ROCm GPU architecture: [WinError 2]" ERROR + WARNING. # doesn't help: subprocess PATH resolution ignores DLL search dirs.
# 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.
_scripts_dir = os.path.dirname(sys.executable) _scripts_dir = os.path.dirname(sys.executable)
if os.path.isfile(os.path.join(_scripts_dir, "hipInfo.exe")): if os.path.isfile(os.path.join(_scripts_dir, "hipInfo.exe")):
import shutil as _shutil import shutil as _shutil
@ -133,13 +130,10 @@ if sys.platform == "win32":
) )
# ── WSL AMD Strix Halo (gfx1151): enable ROCDXG before any torch import ────── # ── 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 # HSA loads the librocdxg bridge only with HSA_ENABLE_DXG_DETECTION=1 set BEFORE
# /dev/dxg), which HSA loads only when HSA_ENABLE_DXG_DETECTION=1 is set BEFORE # torch touches the GPU; a process launched outside a login shell misses the
# torch touches the GPU. A worker launched outside a login shell (e.g. # installer's persisted env and silently falls back to CPU. No-op unless both
# `wsl.exe -d Ubuntu-24.04 python ...`) misses the installer's persisted env # /dev/dxg and librocdxg.so exist.
# 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.
elif sys.platform.startswith("linux") and "HSA_ENABLE_DXG_DETECTION" not in os.environ: elif sys.platform.startswith("linux") and "HSA_ENABLE_DXG_DETECTION" not in os.environ:
try: try:
if os.path.exists("/dev/dxg") and any( 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: class _TeeStream:
"""Mirror writes to the original stream and a session log file. """Mirror writes to the original stream and a session log file.
Console behavior is unchanged (writes/returns delegate to the original Console behavior is unchanged (Tauri's structured-stdout protocol and
stream; Tauri's structured-stdout protocol and isatty probes see exactly isatty probes are unaffected); the file copy is best-effort and must
what they saw before). The file copy is best-effort: a full disk or a never break the console."""
closed handle must never break the console."""
def __init__(self, stream, log_fh): def __init__(self, stream, log_fh):
self._stream = stream self._stream = stream
@ -757,15 +756,11 @@ class _TeeStream:
def _setup_server_disk_logging(): def _setup_server_disk_logging():
"""Tee stdout/stderr to ~/.unsloth/studio/logs/server/ and aim """Tee stdout/stderr to ~/.unsloth/studio/logs/server/ and point
faulthandler at the same file so hard crashes (access violations / faulthandler there so native crashes leave a stack trace on disk.
SIGSEGV in the GPU runtime) 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
Also exports PYTHONFAULTHANDLER=1 so child Python processes (training or None."""
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.
"""
if os.environ.get("UNSLOTH_STUDIO_NO_FILE_LOG") == "1": if os.environ.get("UNSLOTH_STUDIO_NO_FILE_LOG") == "1":
return None return None
try: try:
@ -782,8 +777,7 @@ def _setup_server_disk_logging():
log_dir.mkdir(parents = True, exist_ok = True) log_dir.mkdir(parents = True, exist_ok = True)
stamp = time.strftime("%Y%m%d-%H%M%S") stamp = time.strftime("%Y%m%d-%H%M%S")
log_path = log_dir / f"server-{stamp}-pid{os.getpid()}.log" log_path = log_dir / f"server-{stamp}-pid{os.getpid()}.log"
# Line-buffered so the tail survives a hard kill; errors="replace" # Line-buffered so the tail survives a hard kill; errors="replace" guards encoding quirks.
# so a console encoding quirk can never take the server down.
log_fh = open(log_path, "w", encoding = "utf-8", errors = "replace", buffering = 1) log_fh = open(log_path, "w", encoding = "utf-8", errors = "replace", buffering = 1)
except Exception: except Exception:
return None return None
@ -794,8 +788,7 @@ def _setup_server_disk_logging():
faulthandler.enable(file = log_fh, all_threads = True) faulthandler.enable(file = log_fh, all_threads = True)
except Exception: except Exception:
pass pass
# Children (training workers) inherit: their native-crash stacks land on # Children inherit: their native-crash stacks land on captured stderr.
# the stderr the server already captures.
os.environ.setdefault("PYTHONFAULTHANDLER", "1") os.environ.setdefault("PYTHONFAULTHANDLER", "1")
sys.stdout = _TeeStream(sys.stdout, log_fh) sys.stdout = _TeeStream(sys.stdout, log_fh)
@ -844,12 +837,9 @@ def run_server(
except Exception: except Exception:
pass pass
# Persist a session log + native-crash stacks BEFORE importing main, so # Arm disk logging BEFORE importing main so import-time failures leave
# even import-time failures leave evidence on disk. Field report: Studio # evidence. Field report: native GPU-runtime crashes kill the process with
# "terminates without a warning" -- a native crash in the GPU runtime # no traceback, and the shortcut console closes before it can be read.
# kills the process with no Python traceback, and a desktop-shortcut
# console closes before anything can be read. Console-only logging made
# that undiagnosable.
_session_log = _setup_server_disk_logging() _session_log = _setup_server_disk_logging()
if _session_log is not None and not silent: if _session_log is not None and not silent:
print(f"Session log: {_session_log}") print(f"Session log: {_session_log}")

View file

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

View file

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

View file

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

View file

@ -854,10 +854,9 @@ fi
fi fi
# ── GPU detection summary (mirrors setup.ps1 step "gpu" block) ── # ── GPU detection summary (mirrors setup.ps1 step "gpu" block) ──
# WSL2 ROCDXG: the system rocminfo enumerates the GPU over /dev/dxg only when # WSL2 ROCDXG: rocminfo only sees the GPU with HSA_ENABLE_DXG_DETECTION=1
# HSA_ENABLE_DXG_DETECTION=1 (a no-op on bare metal), and /opt/rocm/bin can be # (no-op on bare metal), and /opt/rocm/bin may be off PATH in non-login shells.
# off PATH outside login shells (the profile.d drop-in). Seed both before the # Seed both or a ROCDXG WSL host is misdetected as CPU-only.
# probes or a ROCDXG WSL host is misdetected as CPU-only.
export HSA_ENABLE_DXG_DETECTION="${HSA_ENABLE_DXG_DETECTION:-1}" export HSA_ENABLE_DXG_DETECTION="${HSA_ENABLE_DXG_DETECTION:-1}"
if ! command -v rocminfo >/dev/null 2>&1 && [ -x /opt/rocm/bin/rocminfo ]; then if ! command -v rocminfo >/dev/null 2>&1 && [ -x /opt/rocm/bin/rocminfo ]; then
PATH="$PATH:/opt/rocm/bin" 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" 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) # Name-based arch inference when tools don't report gfx (mirrors setup.ps1 nameArchTable)
elif [ -z "$_setup_gfx" ] && [ -n "$_setup_mkt" ]; then elif [ -z "$_setup_gfx" ] && [ -n "$_setup_mkt" ]; then
# Kept in sync with the table in install.sh (and the PS nameArchTable). # In sync with install.sh's table (and the PS nameArchTable). gfx1102 comes
# gfx1102 matched BEFORE gfx1100 so the spaceless "RX 7700S" lands on # BEFORE gfx1100 so "RX 7700S" matches it (bash case has no negative lookahead).
# gfx1102 (bash case has no negative lookahead like the PS tables).
case "$_setup_mkt" in case "$_setup_mkt" in
*"9070 XT"*|*9080*) _setup_gfx="gfx1201" ;; # RDNA 4 *"9070 XT"*|*9080*) _setup_gfx="gfx1201" ;; # RDNA 4
*9070*|*9060*) _setup_gfx="gfx1200" ;; # 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: def _pid_alive(pid: int) -> bool:
"""Return True if a process with ``pid`` exists. """True if a process with ``pid`` exists. ``os.kill(pid, 0)`` raises
OSError (WinError 87) for every pid on Windows, so use ``tasklist``
``os.kill(pid, 0)`` raises OSError (WinError 87) for every pid on Windows, there and the signal-0 probe elsewhere."""
so use ``tasklist`` there and the signal-0 probe elsewhere.
"""
if sys.platform == "win32": if sys.platform == "win32":
try: try:
out = subprocess.run( out = subprocess.run(