From 95b50ec57af73e6e016f8636642c5338c106654a Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Fri, 12 Jun 2026 23:48:34 -0700 Subject: [PATCH] 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 --- install.ps1 | 116 ++++++++------------- install.sh | 28 ++--- scripts/install_rocm_wsl_strixhalo.sh | 106 ++++++++----------- scripts/uninstall.ps1 | 39 +++---- scripts/uninstall.sh | 39 +++---- studio/backend/core/inference/llama_cpp.py | 18 ++-- studio/backend/core/training/worker.py | 56 ++++------ studio/backend/main.py | 24 ++--- studio/backend/run.py | 36 +++---- studio/backend/utils/hardware/amd.py | 30 ++---- studio/install_llama_prebuilt.py | 28 ++--- studio/install_python_stack.py | 61 ++++------- studio/setup.ps1 | 50 ++++----- studio/setup.sh | 12 +-- unsloth_cli/commands/studio.py | 8 +- 15 files changed, 246 insertions(+), 405 deletions(-) diff --git a/install.ps1 b/install.ps1 index 88939ce627..b7075f2afd 100644 --- a/install.ps1 +++ b/install.ps1 @@ -144,9 +144,8 @@ function Install-UnslothStudio { # UNSLOTH_PYTHON pins the version (mirrors install.sh --python); default 3.13. $PythonVersion = if ($env:UNSLOTH_PYTHON) { $env:UNSLOTH_PYTHON } else { "3.13" } - # python.org fallback patch, used only when winget is unavailable/broken AND - # the live python.org listing can't be fetched. The installer URL scheme is - # stable so an older patch still installs. Bump alongside $PythonVersion. + # python.org fallback patch for when winget fails AND the live listing is + # unreachable; the URL scheme is stable. Bump with $PythonVersion. $PythonFallbackFullVersion = "3.13.13" # Resolve install destinations. Priority: UNSLOTH_STUDIO_HOME, then @@ -881,13 +880,10 @@ shell.Run cmd, 0, False } if ($createdShortcutCount -gt 0) { substep "Created Unsloth Studio shortcut" - # Force Explorer to re-read each new shortcut's icon so it renders - # immediately instead of a stale/generic entry (a same-name .lnk - # recreated across reinstalls keeps Explorer's cached per-item icon). - # The reliable, non-disruptive fix (no explorer restart) is a per-item - # SHChangeNotify SHCNE_UPDATEITEM + SHCNF_PATHW per .lnk; the global - # SHCNE_ASSOCCHANGED broadcast alone does NOT recover a stale item. - # Also clear the on-disk icon cache (covers heavier staleness). + # A same-name .lnk recreated across reinstalls keeps Explorer's stale + # per-item icon; per-item SHChangeNotify(SHCNE_UPDATEITEM, SHCNF_PATHW) + # fixes it (global SHCNE_ASSOCCHANGED alone does not). Also clear the + # on-disk icon cache. try { & "$env:SystemRoot\System32\ie4uinit.exe" -ClearIconCache 2>$null } catch {} try { & "$env:SystemRoot\System32\ie4uinit.exe" -show 2>$null } catch {} try { @@ -896,15 +892,12 @@ shell.Run cmd, 0, False foreach ($scPath in $createdShortcutPaths) { try { [UnslothShell.IconRefresh]::SHChangeNotify(0x00002000, 0x0005, $scPath, [System.IntPtr]::Zero) } catch {} } - # SHCNE_ASSOCCHANGED (0x08000000) global refresh (belt-and-suspenders) + # SHCNE_ASSOCCHANGED (0x08000000) global refresh as backup [UnslothShell.IconRefresh]::SHChangeNotify(0x08000000, 0, $null, [System.IntPtr]::Zero) } catch {} - # Win11's Start Menu (StartMenuExperienceHost) keeps its OWN - # pre-rendered tile-icon cache that ie4uinit/explorer restart do NOT - # invalidate, so a rewritten same-name shortcut shows the old tile - # until the host restarts. Drop only the render caches (NEVER - # start2.bin -- the pinned layout) and let the host rebuild. - # Best-effort; Win10 has no such host (Test-Path skips it). + # Win11 StartMenuExperienceHost keeps its own tile-icon cache that + # ie4uinit cannot invalidate: drop the render caches (NEVER start2.bin, + # the pinned layout) and restart the host. Win10: Test-Path skips it. try { $smehTemp = Join-Path $env:LOCALAPPDATA "Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\TempState" if (Test-Path -LiteralPath $smehTemp) { @@ -1030,13 +1023,10 @@ shell.Run cmd, 0, False } # ── Fallback: install CPython directly from python.org ── - # Used when winget is unavailable or fails (notably msstore cert-pinning error - # 0x8a15005e, which aborts `winget install` unless --source winget is given). - # Downloads the official installer and runs it silently as a per-user install - # (no UAC), putting python.exe + the py launcher on PATH. Mirrors the uv -> - # astral.sh fallback below. Returns @{ Version; Path } or $null. + # For when winget is missing or fails (e.g. msstore cert-pinning error + # 0x8a15005e). Silent per-user install, no UAC; puts python.exe + py + # launcher on PATH. Returns @{ Version; Path } or $null. function Install-PythonFromPythonOrg { - # python.org ships one installer per architecture. $archSuffix = switch (Get-TauriDiagArch) { "x86_64" { "-amd64" } "arm64" { "-arm64" } @@ -1048,10 +1038,8 @@ shell.Run cmd, 0, False return $null } - # Resolve the latest $PythonVersion.x patch from the python.org listing, - # falling back to a same-minor version if the listing cannot be fetched. - # Use the pinned full version only when it matches the requested minor so a - # non-default UNSLOTH_PYTHON (e.g. 3.12) doesn't silently install 3.13. + # Latest $PythonVersion.x patch from the live listing; pinned fallback only + # if it matches the requested minor (UNSLOTH_PYTHON=3.12 must not get 3.13). $full = if ($PythonFallbackFullVersion -like "$PythonVersion.*") { $PythonFallbackFullVersion } else { "$PythonVersion.0" } try { $listing = [string](Invoke-RestMethod -Uri "https://www.python.org/ftp/python/" -UseBasicParsing -TimeoutSec 20) @@ -1071,16 +1059,14 @@ shell.Run cmd, 0, False return $null } - # Per-user install => no UAC. PrependPath puts python + py on PATH; - # Include_launcher installs py.exe (preferred by Find-CompatiblePython). + # Per-user => no UAC; PrependPath + Include_launcher put python and py.exe on PATH. substep "installing Python $full (silent, per-user)..." $installArgs = @( "/quiet", "InstallAllUsers=0", "PrependPath=1", "Include_launcher=1", - # Launcher per-user too: Include_launcher defaults InstallLauncherAllUsers=1, - # which needs admin and would break this non-admin per-user fallback. + # Default InstallLauncherAllUsers=1 needs admin -- force per-user. "InstallLauncherAllUsers=0", "Include_pip=1", "AssociateFiles=0", @@ -1116,13 +1102,9 @@ shell.Run cmd, 0, False $wingetExit = $null if ($script:WingetAvailable) { - # --source winget avoids the msstore source, which can fail with - # cert-pinning error 0x8a15005e and abort the whole `winget install` - # (winget then demands --source). Python and uv both live in the - # winget source, so pinning it is correct and faster. - # - # Lower ErrorActionPreference so winget stderr (progress/warnings) is - # not a terminating error on PS 5.1 (native stderr is ErrorRecord). + # --source winget skips msstore, whose cert-pinning error 0x8a15005e + # aborts the whole `winget install`; Python and uv both live in winget. + # Lower EAP so winget stderr is not a terminating error on PS 5.1. $prevEAP = $ErrorActionPreference $ErrorActionPreference = "Continue" try { @@ -1136,10 +1118,8 @@ shell.Run cmd, 0, False $DetectedPython = Find-CompatiblePython if (-not $DetectedPython) { - # Python still not functional after winget -- force reinstall. - # This handles both real failures AND "already installed" codes where - # winget thinks Python is present but it's not actually on PATH - # (e.g. user partially uninstalled, or installed via a different method). + # Force reinstall: winget can report success/"already installed" + # while Python still is not on PATH (partial uninstall, other installer). substep "Python not found on PATH after winget. Retrying with --force..." "Yellow" $ErrorActionPreference = "Continue" try { @@ -1152,9 +1132,7 @@ shell.Run cmd, 0, False } } - # Fall back to python.org if winget is unavailable OR couldn't install a - # working Python (missing/broken winget, msstore cert errors --source - # winget can't fix). Keeps the install automatic instead of failing out. + # python.org fallback when winget is missing or could not deliver a working Python. if (-not $DetectedPython) { if ($script:WingetAvailable) { substep "winget could not install Python -- falling back to python.org..." "Yellow" @@ -1408,25 +1386,21 @@ shell.Run cmd, 0, False } # ── Helper: run amd-smi without triggering a UAC elevation prompt ── - # amd-smi on Windows auto-elevates to read GPU/APU memory, surfacing a confusing - # DiskPart UAC prompt mid-install (Studio backend amd.py hits the same). - # __COMPAT_LAYER=RunAsInvoker forces it (and helpers it spawns) to run - # un-elevated; on failure the WMI name -> gfx fallback still resolves the arch. + # amd-smi auto-elevates on Windows (confusing DiskPart UAC prompt mid-install); + # __COMPAT_LAYER=RunAsInvoker runs it un-elevated (backend amd.py does the same). function Invoke-AmdSmiNoElevate { param( [Parameter(Mandatory = $true, Position = 0)][string]$Exe, [Parameter(Position = 1)][string[]]$SmiArgs = @(), [int]$TimeoutSec = 30 ) - # RunAsInvoker blocks the auto-elevation/UAC prompt; the timeout bounds a - # flaky amd-smi that can otherwise spin for minutes (30s mirrors amd.py). + # Timeout bounds a flaky amd-smi that can spin for minutes (30s mirrors amd.py). $prevCompat = [Environment]::GetEnvironmentVariable('__COMPAT_LAYER', 'Process') $env:__COMPAT_LAYER = 'RunAsInvoker' try { - # [Process]::Start, NOT Start-Process -PassThru: the latter leaves - # .ExitCode $null after WaitForExit on PS 5.1, so $LASTEXITCODE (checked - # by callers) reads non-zero and kills detection. Async reads drain the - # pipes (no deadlock); amd-smi args have no spaces so a plain join is safe. + # NOT Start-Process -PassThru: on PS 5.1 it leaves .ExitCode $null, breaking + # callers' $LASTEXITCODE checks. Async reads avoid pipe deadlock; amd-smi + # args have no spaces so a plain join is safe. $psi = New-Object System.Diagnostics.ProcessStartInfo $psi.FileName = $Exe $psi.Arguments = ($SmiArgs -join ' ') @@ -1642,11 +1616,11 @@ shell.Run cmd, 0, False # (gfx120X/110X/1151/1150/103X); unknown names fall back to CPU. elseif ($ROCmGpuLabel) { $nameArchTable = @( - @{ P = "9070 XT|9080"; A = "gfx1201" } # RDNA 4 (RX 9070 XT / 9080) - @{ P = "9070|9060"; A = "gfx1200" } # RDNA 4 (RX 9070 / 9060) - @{ P = "8060S|8050S|8040S|Strix Halo|Ryzen AI Max|AI Max"; A = "gfx1151" } # RDNA 3.5 (Strix Halo: Radeon 8060S/8050S/8040S iGPU, Ryzen AI Max+) - @{ P = "890M|880M|860M|840M|Strix Point|Krackan|HX 37[05]|AI 9 HX|AI 9 36[05]|AI 7 35[05]|AI 5 34[05]|AI 7 PRO 35|AI 5 33"; A = "gfx1150" } # RDNA 3.5 (Strix/Krackan Point: Radeon 890M/880M iGPU, Ryzen AI 9 HX 370/375) - @{ P = "RX 7900|RX 7800|RX 7700(?!S)|PRO W7900|PRO W7800|PRO W7700"; A = "gfx1100" } # RDNA 3 desktop/workstation (Navi 31) + @{ P = "9070 XT|9080"; A = "gfx1201" } # RDNA 4 + @{ P = "9070|9060"; A = "gfx1200" } # RDNA 4 + @{ P = "8060S|8050S|8040S|Strix Halo|Ryzen AI Max|AI Max"; A = "gfx1151" } # RDNA 3.5 (Strix Halo) + @{ P = "890M|880M|860M|840M|Strix Point|Krackan|HX 37[05]|AI 9 HX|AI 9 36[05]|AI 7 35[05]|AI 5 34[05]|AI 7 PRO 35|AI 5 33"; A = "gfx1150" } # RDNA 3.5 (Strix/Krackan Point) + @{ P = "RX 7900|RX 7800|RX 7700(?!S)|PRO W7900|PRO W7800|PRO W7700"; A = "gfx1100" } # RDNA 3 (Navi 31) @{ P = "RX 7600|RX 7700S|RX 7650|PRO W7600|PRO W7500|PRO V710"; A = "gfx1102" } # RDNA 3 (Navi 33) @{ P = "780M|760M|740M|Phoenix|Hawk Point|Z1 Extreme|Z2 Extreme"; A = "gfx1103" } # RDNA 3 iGPU (Phoenix / Hawk Point) @{ P = "RX 6900|RX 6800|RX 6750|RX 6700|PRO W6800|PRO W6900"; A = "gfx1030" } # RDNA 2 (Navi 21) -- gfx103X family @@ -1707,11 +1681,9 @@ shell.Run cmd, 0, False } # ── Optional WSL-ROCm driver hint ──────────────────────────────────────── - # An AMD GPU can also be used inside WSL2, but only with Adrenalin >= 26.2.2 - # (first production ROCDXG/WSL release); native Windows GPU works with any - # recent driver. We can't auto-install it (AMD referrer-gates downloads, no - # winget package), so just point at AMD's page. Shown only when the installed - # driver predates 26.2.2 (Feb 2026); suppress with UNSLOTH_SKIP_AMD_DRIVER_HINT=1. + # WSL2 ROCm needs Adrenalin >= 26.2.2; we can't auto-install it (referrer- + # gated download, no winget package), so hint when the driver is older. + # Suppress with UNSLOTH_SKIP_AMD_DRIVER_HINT=1. function Show-AmdWslDriverHint { if ($env:UNSLOTH_SKIP_AMD_DRIVER_HINT) { return } try { @@ -1728,15 +1700,13 @@ shell.Run cmd, 0, False $drvDate = [Management.ManagementDateTimeConverter]::ToDateTime([string]$amd.DriverDate) } } catch {} - # Older than 26.2.2 (Feb 2026) => can't expose the GPU to WSL ROCm. - # Unreadable date => still show the hint (informational, suppressible). + # Pre-26.2.2 (Feb 2026) driver, or unreadable date => show the hint. if ($drvDate -and $drvDate -ge (Get-Date '2026-02-01')) { return } substep "Tip: to use this GPU inside WSL too, install AMD Adrenalin 26.2.2+ (for WSL2)." "Cyan" substep " Your current driver predates it; native Windows GPU is unaffected. Get it from AMD:" "Cyan" substep " https://www.amd.com/en/resources/support-articles/release-notes/RN-RAD-WIN-26-2-2.html" "Cyan" substep " Then reboot and run this installer inside an Ubuntu-24.04 WSL distro." "Cyan" - # If WSL isn't installed yet, point at the command that provisions it - # (best-effort; wsl.exe absent => no WSL). + # No wsl.exe => suggest installing WSL. $hasWsl = $false try { $hasWsl = [bool](Get-Command wsl.exe -ErrorAction SilentlyContinue) } catch {} if (-not $hasWsl) { @@ -1763,8 +1733,8 @@ shell.Run cmd, 0, False substep " Ensure the ROCm compute driver is installed alongside the display driver:" "Yellow" substep " https://rocm.docs.amd.com/en/latest/deploy/windows/index.html" "Yellow" } elseif ($ROCmGfxArch) { - # Known arch: Studio setup installs AMD's bundled-runtime ROCm PyTorch wheels - # (repo.amd.com), which ship their own runtime -- HIP SDK optional. + # Known arch: Studio setup installs AMD's bundled-runtime ROCm wheels + # (repo.amd.com) -- HIP SDK optional. step "gpu" "AMD ROCm ($ROCmGfxArch)" "Cyan" substep "Detected: $ROCmGpuLabel" "Cyan" substep "GPU PyTorch uses AMD's bundled-runtime ROCm wheels -- HIP SDK not required (optional)." "Cyan" @@ -1778,7 +1748,6 @@ shell.Run cmd, 0, False step "gpu" "none (chat-only / GGUF)" "Yellow" substep "Training and GPU inference require an NVIDIA or AMD ROCm GPU." "Yellow" } - # On an AMD GPU (no NVIDIA), surface the optional WSL-ROCm driver hint. if (-not $HasNvidiaSmi -and ($ROCmGfxArch -or $ROCmGpuLabel)) { Show-AmdWslDriverHint } # ── Choose the correct PyTorch index URL based on driver CUDA version ── @@ -1866,8 +1835,7 @@ shell.Run cmd, 0, False if (-not $SkipTorch -and -not $ROCmIndexUrl -and $TorchIndexUrl -like "*/cpu") { Write-Host "" if ($ROCmGfxArch) { - # Known AMD arch: install.ps1 lays down CPU PyTorch as a base, then - # setup.ps1 swaps in AMD's bundled-runtime GPU ROCm wheels (no HIP SDK). + # CPU PyTorch is only a base; setup.ps1 swaps in GPU ROCm wheels next. substep "Installing CPU PyTorch as a base -- Studio setup installs GPU ROCm" "Cyan" substep "wheels for $ROCmGfxArch next (bundled runtime; HIP SDK not required)." "Cyan" } else { diff --git a/install.sh b/install.sh index 05b8e1e92b..7d8ca83ba4 100755 --- a/install.sh +++ b/install.sh @@ -1207,8 +1207,7 @@ STUB_EOF # Escape single quotes for PowerShell single-quoted string embedding _css_sc_args_ps=$(printf '%s' "$_css_sc_args" | sed "s/'/''/g") - # DISTINCT shortcut name so the WSL launcher never clobbers a native - # install's "Unsloth Studio.lnk" in the same folder. Per-distro suffix. + # Per-distro name so the WSL launcher never clobbers a native install's "Unsloth Studio.lnk". if [ -n "$_css_distro" ]; then _css_lnk_name="Unsloth Studio (WSL - ${_css_distro}).lnk" else @@ -1283,8 +1282,7 @@ WSLPS1_EOF fi rm -f "$_css_ps1_tmp" fi - # If WSL interop is disabled (powershell.exe "Exec format error"), the - # shortcut wasn't created; tell the user how to launch / re-enable it. + # WSL interop disabled (powershell.exe "Exec format error") => no shortcut; tell the user. if [ "$_css_created" -ne 1 ]; then substep "Couldn't create the Windows shortcut (WSL interop may be disabled)." "$C_WARN" substep " Launch Studio from Windows: wsl -d \"$_css_distro\" -- bash -lc 'unsloth studio'" "$C_WARN" @@ -1708,10 +1706,9 @@ _find_no_torch_runtime() { } # ── AMD ROCm GPU detection helper ── -# WSL2 ROCDXG: the system rocminfo enumerates the GPU over /dev/dxg only when -# HSA_ENABLE_DXG_DETECTION=1 (a no-op on bare metal), and /opt/rocm/bin can be -# off PATH outside login shells (the profile.d drop-in). Seed both before any -# rocminfo probe or a ROCDXG WSL host is misdetected as CPU-only. +# WSL2 ROCDXG: rocminfo only sees the GPU with HSA_ENABLE_DXG_DETECTION=1 +# (no-op on bare metal), and /opt/rocm/bin may be off PATH in non-login shells. +# Seed both or a ROCDXG WSL host is misdetected as CPU-only. _ensure_rocm_probe_env() { export HSA_ENABLE_DXG_DETECTION="${HSA_ENABLE_DXG_DETECTION:-1}" if ! command -v rocminfo >/dev/null 2>&1 && [ -x /opt/rocm/bin/rocminfo ]; then @@ -2084,10 +2081,9 @@ _maybe_bootstrap_rocm_wsl() { [ "${UNSLOTH_SKIP_ROCM_WSL_SETUP:-0}" = "1" ] && return 0 # Leave any already-usable GPU completely alone (NVIDIA, or working ROCm). if _has_usable_nvidia_gpu; then return 0; fi - # "Usable ROCm" here = rocminfo enumerates the gfx1151 agent. Don't use the - # generic _has_amd_rocm_gpu: its broad gfx match accepts "gfx11-generic" and - # would skip this bootstrap while the real GPU is still unusable. awk consumes - # all input, so rocminfo isn't SIGPIPE'd like `grep -q` would under pipefail. + # Usable = rocminfo enumerates gfx1151. Not _has_amd_rocm_gpu: its broad + # match accepts "gfx11-generic" and would skip the bootstrap. awk consumes + # all input, avoiding the pipefail SIGPIPE a `grep -q` would cause. _ensure_rocm_probe_env if command -v rocminfo >/dev/null 2>&1 && \ rocminfo 2>/dev/null | awk '/Name:[[:space:]]*gfx1151/{found=1} END{exit !found}'; then @@ -2098,15 +2094,13 @@ _maybe_bootstrap_rocm_wsl() { _persist_rocm_wsl_dropin return 0 fi - # WSL GPU passthrough device must exist (present on any WSL2 GPU host). + # /dev/dxg exists on any WSL2 GPU host. [ -e /dev/dxg ] || return 0 - # Only Strix Halo (gfx1151): rocminfo can't tell us the arch yet, so match - # the CPU model string WSL exposes (e.g. "AMD Ryzen AI Max+ ... Radeon 8060S"). + # Strix Halo only: rocminfo can't report the arch yet, so match the CPU model string. grep -qiE 'Ryzen AI Max|Radeon 80[0-9]0S|Strix Halo' /proc/cpuinfo 2>/dev/null || return 0 command -v bash >/dev/null 2>&1 || return 0 - # Fast path: already configured (librocdxg present) but launched from a - # non-login shell so the persisted env wasn't loaded -- just load it. + # Fast path: librocdxg present but env not loaded (non-login shell) -- load it. if [ -e /opt/rocm/lib/librocdxg.so ] || [ -e /opt/rocm/lib64/librocdxg.so ]; then if [ -r /etc/profile.d/unsloth-rocm-wsl.sh ]; then # shellcheck disable=SC1091 diff --git a/scripts/install_rocm_wsl_strixhalo.sh b/scripts/install_rocm_wsl_strixhalo.sh index 5ef9ee386a..2a0149d5b5 100644 --- a/scripts/install_rocm_wsl_strixhalo.sh +++ b/scripts/install_rocm_wsl_strixhalo.sh @@ -5,30 +5,25 @@ # ────────────────────────────────────────────────────────────────────────────── # Enable ROCm-on-WSL for AMD Strix Halo (Radeon 8060S / gfx1151) # ────────────────────────────────────────────────────────────────────────────── -# install.sh already routes gfx1151 to the right ROCm wheels once a ROCm runtime -# is present; what it does NOT do is install AMD's ROCm userspace + the WSL DXG -# bridge. This helper automates that Linux-side prerequisite on Ubuntu 24.04 -# WSL2 and is invoked by install.sh when it sees a Strix Halo APU in WSL (via -# /dev/dxg) but no ROCm runtime yet. Fully idempotent (re-run just re-verifies). +# Installs AMD's ROCm userspace + the WSL DXG bridge (the one part install.sh +# doesn't do) on Ubuntu 24.04 WSL2. Invoked by install.sh when it sees a Strix +# Halo APU in WSL (/dev/dxg) but no ROCm runtime. Idempotent. # -# Manual, admin-gated Windows prerequisite: an AMD Adrenalin driver with -# production ROCDXG/WSL support (26.2.2+). install.ps1 offers to update it. Once -# installed + rebooted, /dev/dxg is exposed to WSL and this script builds the rest. +# Windows prerequisite (manual, admin): Adrenalin with production ROCDXG/WSL +# support (26.2.2+) -- install.ps1 offers it; after reboot /dev/dxg appears. # -# HOW ROCDXG WORKS (and why older /usr/lib/wsl/lib notes are wrong): librocdxg.so -# is AMD's user-mode bridge between the Linux HSA runtime and the Windows driver -# over /dev/dxg. The STANDARD hsa-rocr runtime (NOT the gone "roc4wsl" package) -# loads it when HSA_ENABLE_DXG_DETECTION=1. No hsa/rocm libs need injecting into -# /usr/lib/wsl/lib (it holds only d3d12/dxcore), yet rocminfo enumerates gfx1151 -# fine -- so we gate on /dev/dxg, not on WSL lib injection. +# How ROCDXG works (older /usr/lib/wsl/lib notes are wrong): librocdxg.so +# bridges the Linux HSA runtime to the Windows driver over /dev/dxg; the +# STANDARD hsa-rocr loads it when HSA_ENABLE_DXG_DETECTION=1. Nothing needs +# injecting into /usr/lib/wsl/lib (only d3d12/dxcore live there), so we gate +# on /dev/dxg, not on WSL lib injection. # -# KNOWN CAVEAT (ROCm/ROCm#6022): librocdxg can cap usable ROCm VRAM at the WSL -# VM's RAM (.wslconfig [wsl2] memory=) on some BIOS UMA layouts, and amd-smi -# doesn't work in WSL. On OOM below capacity, raise memory= (then wsl --shutdown) -# and watch GPU use from Windows. Large-UMA BIOS exposes the full pool regardless. +# Caveat (ROCm/ROCm#6022): librocdxg can cap usable VRAM at the WSL VM's RAM +# (.wslconfig [wsl2] memory=) on some BIOS UMA layouts; amd-smi doesn't work +# in WSL. On OOM below capacity, raise memory= then `wsl --shutdown`. # -# Verified on Ryzen AI Max+ PRO 395 / Radeon 8060S (gfx1151) with ROCm 7.2.1 + -# Ubuntu 24.04 + WSL2 + Adrenalin. These pins MOVE; bump + re-verify on newer ROCm. +# Verified: Ryzen AI Max+ PRO 395 / Radeon 8060S, ROCm 7.2.1, Ubuntu 24.04, +# WSL2. Pins MOVE; bump + re-verify on newer ROCm. # ────────────────────────────────────────────────────────────────────────────── set -euo pipefail @@ -38,11 +33,9 @@ GFX="gfx1151" LIBROCDXG_REF="${UNSLOTH_LIBROCDXG_REF:-develop}" # ROCm/librocdxg git ref to build # AMD's gfx1151 wheel index (same one install.sh uses); only for the smoke test. TORCH_INDEX="${UNSLOTH_AMD_ROCM_MIRROR:-https://repo.amd.com/rocm/whl}/${GFX}/" -# Optional torch smoke test (throwaway venv). OFF by default: install.sh installs -# torch itself into the real venv right after, so a duplicate download is wasteful. +# Optional torch smoke test (throwaway venv); OFF by default since install.sh installs torch itself. SMOKE_TEST="${UNSLOTH_WSL_SMOKE_TEST:-0}" -# REQUIRED constraint -- without it pip prefers PyPI's newer CUDA torch over the -# gfx1151 ROCm wheel. 2.11 carries AMD's real gfx1151 fix (matches install.sh). +# REQUIRED: without it pip prefers PyPI's newer CUDA torch. 2.11 carries the gfx1151 fix. TORCH_CONSTRAINT="${UNSLOTH_WSL_TORCH_CONSTRAINT:-torch>=2.11.0,<2.12.0}" ROCM_DIR="" # resolved after install @@ -58,8 +51,7 @@ if [ "$(id -u)" -ne 0 ]; then fi # ── Windows 11 SDK (headers for the librocdxg build) ───────────────────────── -# librocdxg's cmake build needs the Windows SDK 'shared' headers, which live on -# the Windows HOST under C:\Program Files (x86)\Windows Kits\10\Include\\. +# The cmake build needs the SDK 'shared' headers from the Windows host. _WIN_SDK_INC_BASE="/mnt/c/Program Files (x86)/Windows Kits/10/Include" # Print the newest installed SDK include dir with 'shared' headers, or nothing. @@ -73,15 +65,13 @@ _find_win_sdk() { return 0 } -# Best-effort: install the Windows 11 SDK on the Windows HOST via winget so the -# build has its headers with no manual step. Elevates -> ONE UAC prompt; headers -# appear under /mnt/c immediately (no reboot). Never fatal -- failure falls -# through to a manual-install message. Opt out: UNSLOTH_SKIP_WIN_SDK_INSTALL=1. +# Best-effort winget install of the Win11 SDK on the Windows host (ONE UAC +# prompt; headers appear under /mnt/c immediately, no reboot). Never fatal. +# Opt out: UNSLOTH_SKIP_WIN_SDK_INSTALL=1. _install_windows_sdk_via_winget() { [ "${UNSLOTH_SKIP_WIN_SDK_INSTALL:-0}" = "1" ] && { note "Skipping Windows SDK auto-install (UNSLOTH_SKIP_WIN_SDK_INSTALL=1)."; return 0; } command -v powershell.exe >/dev/null 2>&1 || return 0 - # `command -v` succeeds even with WSL interop OFF (.exe on PATH but fails - # with "Exec format error"); verify it actually executes. + # command -v passes even with interop OFF ("Exec format error"); verify it runs. powershell.exe -NoProfile -Command "exit 0" >/dev/null 2>&1 || return 0 if ! powershell.exe -NoProfile -Command "if (Get-Command winget -ErrorAction SilentlyContinue) { exit 0 } else { exit 1 }" >/dev/null 2>&1; then note "winget not available on the Windows host -- cannot auto-install the Windows SDK." @@ -90,13 +80,11 @@ _install_windows_sdk_via_winget() { say "Installing the Windows 11 SDK on the Windows host via winget" note "librocdxg needs its headers. Approve the UAC prompt on the Windows desktop." note "One-time (~1-3 GB download); opt out with UNSLOTH_SKIP_WIN_SDK_INSTALL=1." - # Newest SDK first, then a fallback. Header presence is the source of truth - # (re-check each attempt), not winget's exit code. /dev/null 2>&1 && [ ! -x /opt/rocm/bin/rocminfo ]; then - # Direct apt-repo install (leaner than amdgpu-install; repo is indexed by - # ROCm version, e.g. .../apt/7.2.1). + # Direct apt-repo install (leaner than amdgpu-install); repo indexed by ROCm version. $SUDO mkdir -p /etc/apt/keyrings wget -qO- https://repo.radeon.com/rocm/rocm.gpg.key \ | gpg --dearmor | $SUDO tee /etc/apt/keyrings/rocm.gpg >/dev/null @@ -144,23 +129,20 @@ if ! command -v rocminfo >/dev/null 2>&1 && [ ! -x /opt/rocm/bin/rocminfo ]; the printf 'Package: *\nPin: release o=repo.radeon.com\nPin-Priority: 600\n' \ | $SUDO tee /etc/apt/preferences.d/rocm-pin-600 >/dev/null $SUDO apt-get update -y - # rocm-libs pulls everything torch links at runtime (rocblas, hipblas, - # miopen-hip, rccl, ...); hsa-rocr + rocminfo come as deps. Large (~5 GB - # download / ~23 GB installed). + # rocm-libs pulls everything torch links at runtime; hsa-rocr + rocminfo + # come as deps. Large (~5 GB download / ~23 GB installed). $SUDO apt-get install -y rocm-libs rocminfo hip-runtime-amd else note "ROCm already present -- skipping apt install." fi -# Resolve the real ROCm dir and ensure the canonical /opt/rocm symlink. apt lays -# ROCm under /opt/rocm- and rocm-core symlinks /opt/rocm -> that; repair if -# an earlier partial run left /opt/rocm as a real dir blocking the symlink. +# Resolve the real ROCm dir and ensure the /opt/rocm symlink (apt installs to +# /opt/rocm-); repair a partial run that left /opt/rocm as a real dir. _real="$(ls -d /opt/rocm-* 2>/dev/null | sort -V | tail -1 || true)" if [ -n "$_real" ] && [ ! -L /opt/rocm ] && [ -d /opt/rocm ]; then - # /opt/rocm is a real dir blocking the symlink. Only treat it as a removable - # stray stub if it's NOT a real ROCm install (a real one has bin/rocminfo / - # bin/hipcc / .info/version) -- this protects a user's pre-existing ROCm. Even - # then we MOVE IT ASIDE, never rm -rf, so a wrong guess can't lose data. + # Treat /opt/rocm as a stray stub only if it lacks ROCm markers (bin/rocminfo, + # bin/hipcc, .info/version) -- protects a pre-existing install. Even then, + # MOVE it aside, never rm -rf, so a wrong guess can't lose data. if [ -e /opt/rocm/bin/rocminfo ] || [ -e /opt/rocm/bin/hipcc ] || [ -e /opt/rocm/.info/version ]; then note "/opt/rocm is a real ROCm install -- leaving it untouched (will install librocdxg into it)." else @@ -181,9 +163,8 @@ say "Building librocdxg (${LIBROCDXG_REF})" if [ -e "${ROCM_DIR}/lib/librocdxg.so" ]; then note "librocdxg already installed -- skipping build." else - # Discover the newest installed Win11 SDK (version differs per machine). If - # absent, auto-install via winget (one UAC prompt) and re-discover; only if - # that ALSO fails do we stop with manual instructions. + # Find the newest installed Win11 SDK; if absent, winget-install and retry, + # stopping with manual instructions only if that also fails. _win_sdk="$(_find_win_sdk)" if [ -z "$_win_sdk" ]; then note "Windows 11 SDK headers not found -- attempting automatic install..." @@ -238,17 +219,15 @@ export LD_LIBRARY_PATH="${ROCM_DIR}/lib:${LD_LIBRARY_PATH:-}" # ── Step 5: verify the runtime enumerates the GPU ──────────────────────────── say "Verifying rocminfo sees ${GFX}" -# Capture rocminfo into a var BEFORE grepping: piping into `grep -q` SIGPIPEs -# rocminfo on first match, which under `set -o pipefail` turns a successful match -# into a pipeline failure. Match the gfx1151 ISA "Name:" agent exactly (not a -# broad gfx1[0-9]) so a generic fallback ISA or unrelated RDNA GPU can't pass. +# Capture rocminfo BEFORE grepping: piping into `grep -q` SIGPIPEs it, which +# pipefail turns into failure on a successful match. Match the gfx1151 "Name:" +# agent exactly so a generic fallback ISA or unrelated RDNA GPU can't pass. _rocminfo_out="$(rocminfo 2>/dev/null || true)" if ! printf '%s\n' "$_rocminfo_out" | grep -qE "Name:[[:space:]]*${GFX}([^0-9]|$)"; then printf '%s\n' "$_rocminfo_out" | head -25 >&2 || true die "rocminfo did not enumerate a ${GFX} GPU agent. Most common cause: the Windows AMD driver predates production ROCDXG -- update Adrenalin (install.ps1 offers this), reboot, and re-run." fi -# Display-only summary: best-effort (|| true) so head's early pipe-close under -# `set -o pipefail` can't fail the bootstrap after verification already passed. +# Display-only; || true so head's pipe-close under pipefail can't fail the bootstrap. printf '%s\n' "$_rocminfo_out" | grep -E 'Marketing Name|Device Type|Compute Unit' | grep -iE "Radeon|GPU|Compute" | head -3 || true note "ROCm-on-WSL runtime is live for ${GFX}." @@ -258,8 +237,7 @@ if [ "$SMOKE_TEST" = "1" ]; then _venv="${HOME}/.unsloth/rocm-smoketest" rm -rf "$_venv"; python3 -m venv "$_venv" "$_venv/bin/pip" install --quiet --upgrade pip - # gfx1151 index is primary (torch + triton); PyPI only an extra for pure-py - # deps. The constraint keeps pip on the ROCm wheel, not a newer PyPI CUDA torch. + # gfx1151 index primary, PyPI only extra; the constraint keeps pip on the ROCm wheel. "$_venv/bin/pip" install --index-url "$TORCH_INDEX" \ --extra-index-url https://pypi.org/simple "$TORCH_CONSTRAINT" || \ die "torch install from ${TORCH_INDEX} failed." diff --git a/scripts/uninstall.ps1 b/scripts/uninstall.ps1 index f0f6e4fddc..6a7adf58a3 100644 --- a/scripts/uninstall.ps1 +++ b/scripts/uninstall.ps1 @@ -16,8 +16,7 @@ function Uninstall-UnslothStudio { function _Step { param([string]$Msg) Write-Host $Msg } function _Substep { param([string]$Msg, [string]$Color = "Gray") Write-Host " $Msg" -ForegroundColor $Color } - # Remove a file/dir/symlink if present. Idempotent; retries since a just-killed - # process can briefly hold a handle (Windows refuses the delete until released). + # Remove a file/dir/symlink if present. Retries: a just-killed process can briefly hold a handle. function _RemovePath { param([string]$Path) if ([string]::IsNullOrWhiteSpace($Path)) { return } @@ -240,11 +239,9 @@ function Uninstall-UnslothStudio { } catch { } } - # Stop processes that would block deleting the paths we remove. Unlike - # _StopStudioProcesses (venv exe only), this also catches llama-server/llama-cli, - # the unsloth.exe shim, and orphaned mp workers under SYSTEM python holding a - # venv DLL (an open DLL handle blocks the dir delete) -- found by scanning each - # candidate's loaded modules, not just its image path. + # Stop processes that would block the deletes. Unlike _StopStudioProcesses + # (venv exe only), scans loaded modules too: catches llama-server/llama-cli, + # the unsloth.exe shim, and orphaned mp workers holding a venv DLL. function _StopProcessesLockingRoots { param([string[]]$Roots) $clean = @($Roots | Where-Object { $_ } | ForEach-Object { $_.TrimEnd('\','/') }) @@ -263,8 +260,8 @@ function Uninstall-UnslothStudio { } } } catch { } - # 2. A loaded module under a target root (orphaned mp-fork python holding a - # venv DLL). Scoped to names that load our DLLs to keep the scan fast. + # 2. Loaded module under a root (orphaned python holding a venv DLL); + # scoped to known process names to keep the scan fast. try { $cands = Get-Process -Name python, pythonw, unsloth, llama-server, llama-cli -ErrorAction SilentlyContinue foreach ($proc in $cands) { @@ -280,17 +277,14 @@ function Uninstall-UnslothStudio { # Default install root + default data dir. $defaultStudioHome = if ($env:USERPROFILE) { Join-Path $env:USERPROFILE ".unsloth\studio" } else { $null } $defaultDataDir = if ($env:LOCALAPPDATA) { Join-Path $env:LOCALAPPDATA "Unsloth Studio" } else { $null } - # Default-mode ~/.unsloth holds a SHARED llama.cpp build + .cache that are - # siblings of studio (not under it), so deleting misses them -- handle - # explicitly. No-op in env/custom mode (nested under the custom root, removed - # with it). A user-set UNSLOTH_LLAMA_CPP_PATH is left alone. + # Default mode: shared llama.cpp + .cache are siblings of under + # ~/.unsloth, so deleting misses them. No-op in env/custom mode; + # a user-set UNSLOTH_LLAMA_CPP_PATH is left alone. $defaultUnslothHome = if ($env:USERPROFILE) { Join-Path $env:USERPROFILE ".unsloth" } else { $null } $defaultLlamaCpp = if ($defaultUnslothHome) { Join-Path $defaultUnslothHome "llama.cpp" } else { $null } $defaultCache = if ($defaultUnslothHome) { Join-Path $defaultUnslothHome ".cache" } else { $null } - # llama.cpp atomic-install staging root (install_llama_prebuilt.py .staging, - # sibling of the install dir). Usually pruned after activate, but an interrupted - # build can leave a ".staging-XXXX" tree; removing it lets the empty-dir - # cleanup of ~/.unsloth below succeed. No-op in env/custom mode and when absent. + # install_llama_prebuilt.py's .staging root: an interrupted build can leave + # it behind, blocking the empty-dir cleanup of ~/.unsloth below. $defaultStaging = if ($defaultUnslothHome) { Join-Path $defaultUnslothHome ".staging" } else { $null } # Build known-root list FIRST so the port-file kill can verify ownership. @@ -308,8 +302,7 @@ function Uninstall-UnslothStudio { _StopByPortFile -PortFile (Join-Path $r "share\studio.port") -KnownRoots $knownRoots } _StopStudioProcesses -KnownRoots $knownRoots - # Also stop anything holding a handle on the exact paths we delete (llama-server, - # the CLI shim, an mp-fork python with a venv DLL) so the dir delete isn't refused. + # Also stop anything holding a handle on the paths we delete (else the delete is refused). _StopProcessesLockingRoots -Roots (@($knownRoots) + @($defaultDataDir, $defaultLlamaCpp, $defaultCache)) # ── Remove custom-root install trees ── @@ -329,8 +322,7 @@ function Uninstall-UnslothStudio { if ($defaultStudioHome) { _RemovePath $defaultStudioHome } # Default data dir. if ($defaultDataDir) { _RemovePath $defaultDataDir } - # Default-mode shared llama.cpp build + cache (siblings of studio under - # ~/.unsloth). No-op in env/custom mode and when absent. + # Default-mode shared llama.cpp + cache (siblings of studio; no-op in env/custom mode). if ($defaultLlamaCpp) { _RemovePath $defaultLlamaCpp } if ($defaultCache) { _RemovePath $defaultCache } if ($defaultStaging) { _RemovePath $defaultStaging } @@ -349,9 +341,8 @@ function Uninstall-UnslothStudio { if ($env:APPDATA) { _RemovePath (Join-Path $env:APPDATA "Microsoft\Windows\Start Menu\Programs\Unsloth Studio.lnk") } - # Invalidate the Win11 Start Menu tile cache so the removed shortcut's tile - # disappears promptly instead of lingering stale (mirrors install.ps1's - # New-StudioShortcuts). Preserves start2.bin (the pin layout). + # Invalidate the Win11 Start Menu tile cache so the removed tile doesn't + # linger (mirrors install.ps1). Preserves start2.bin (the pin layout). try { $smehTemp = Join-Path $env:LOCALAPPDATA "Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\TempState" if (Test-Path -LiteralPath $smehTemp) { diff --git a/scripts/uninstall.sh b/scripts/uninstall.sh index e97b28799b..19b6fe558a 100755 --- a/scripts/uninstall.sh +++ b/scripts/uninstall.sh @@ -212,17 +212,14 @@ _custom_studio_roots | while IFS= read -r _custom_root; do _remove_path "$_custom_root" done _remove_path "$HOME/.unsloth/studio" -# Default-mode shared llama.cpp build + cache are siblings of studio (not removed -# by deleting it). No-op in env/custom mode (they nest under the custom root) and -# when absent. A user-set UNSLOTH_LLAMA_CPP_PATH is intentionally kept. +# Default-mode shared llama.cpp + cache are siblings of studio. No-op in +# env/custom mode; a user-set UNSLOTH_LLAMA_CPP_PATH is kept. _remove_path "$HOME/.unsloth/llama.cpp" _remove_path "$HOME/.unsloth/.cache" -# llama.cpp atomic-install staging root (install_llama_prebuilt.py .staging). -# Normally pruned after activate, but an interrupted build can leave it behind; -# removing it lets the rmdir below succeed. No-op in env/custom mode and absent. +# install_llama_prebuilt.py's .staging root: an interrupted build can leave it +# behind, blocking the rmdir below. _remove_path "$HOME/.unsloth/.staging" -# ROCm-on-WSL helper artifacts (librocdxg build clone + smoke-test venv). No-op -# where they don't exist; removing them lets the rmdir below succeed. +# ROCm-on-WSL helper artifacts (librocdxg clone + smoke-test venv). _remove_path "$HOME/.unsloth/librocdxg" _remove_path "$HOME/.unsloth/rocm-smoketest" # Drop ~/.unsloth only if now empty (rmdir refuses non-empty, so user content is kept). @@ -259,21 +256,17 @@ case "$_os" in Linux) if [ "$_is_wsl" = "1" ]; then echo "Removing WSL Windows-side shortcuts..." - # install.sh creates per-distro 'Unsloth Studio (WSL - ).lnk' - # on the Windows Desktop + Start Menu via powershell.exe. Scope removal - # to THIS distro (passed as $args[0]) so a multi-distro install keeps the - # other distros' launchers; the TARGET=wsl.exe check still spares a - # native install's "Unsloth Studio.lnk". Prefer powershell.exe; test it - # can EXECUTE (`command -v` succeeds even with interop OFF -- .exe then - # fails "Exec format error", common on systemd-enabled distros). + # Remove only THIS distro's 'Unsloth Studio (WSL - ).lnk' so a + # multi-distro install keeps other launchers; the wsl.exe target check + # spares a native install's lnk. Verify powershell.exe actually EXECUTES + # (`command -v` passes even with interop off -- "Exec format error"). _wsl_distro="${WSL_DISTRO_NAME:-}" _ps_ran=0 if command -v powershell.exe >/dev/null 2>&1 && \ powershell.exe -NoProfile -Command "exit 0" >/dev/null 2>&1; then _ps_ran=1 - # Inject the distro into the command: a -Command string does not - # receive trailing tokens as $args. WSL distro names are safe to - # embed (no quotes/$/backtick). + # Inject the distro: -Command strings get no $args. Distro names + # are safe to embed (no quotes/$/backtick). # shellcheck disable=SC2016 powershell.exe -NoProfile -Command '$distro = "'"$_wsl_distro"'"; $dirs = @( @@ -300,9 +293,8 @@ case "$_os" in } }' >/dev/null 2>&1 || true fi - # Fallback when powershell.exe can't run (interop disabled): remove the - # WSL .lnk files via drvfs. The "Unsloth Studio (WSL..." name is - # WSL-specific, so a native install's "Unsloth Studio.lnk" never matches. + # Interop disabled: remove the .lnk files via drvfs. The "(WSL..." name + # is WSL-specific, so a native "Unsloth Studio.lnk" never matches. if [ "$_ps_ran" = "0" ]; then for _drive in /mnt/c /mnt/d /mnt/e; do [ -d "$_drive/Users" ] || continue @@ -329,9 +321,8 @@ case "$_os" in done fi # ── ROCm-on-WSL config (install_rocm_wsl_strixhalo.sh) ── - # Remove Unsloth's own ROCDXG config (the env it persisted). The system - # ROCm userspace is a shared prereq (like CUDA) and is LEFT IN PLACE by - # default; set UNSLOTH_UNINSTALL_ROCM=1 to remove it too. + # Remove only Unsloth's persisted env; system ROCm is a shared prereq, + # left in place unless UNSLOTH_UNINSTALL_ROCM=1. echo "Removing ROCm-on-WSL config..." _sudo="" if [ "$_uid" != "0" ] && command -v sudo >/dev/null 2>&1; then _sudo="sudo"; fi diff --git a/studio/backend/core/inference/llama_cpp.py b/studio/backend/core/inference/llama_cpp.py index 16f6376aab..0f01cfef3b 100644 --- a/studio/backend/core/inference/llama_cpp.py +++ b/studio/backend/core/inference/llama_cpp.py @@ -5178,14 +5178,11 @@ class LlamaCppBackend: @staticmethod def _fit_off_retry_eligible(cmd: "list[str]", use_fit: bool) -> bool: - """Whether a llama-server startup crash may be retried with --fit off. - - Only when Studio's own VRAM math placed the model (use_fit=False) - and nothing on the command line set the fit mode explicitly - (-fit / --fit, space- or equals-form). --fit-ctx / --fit-target / - -fitc / -fitt tune the fit step but do not select the mode, so - they do not block the retry. - """ + """Whether a startup crash may be retried with --fit off: only when + Studio placed the model (use_fit=False) and no explicit fit-mode flag + (-fit/--fit, space- or equals-form) is on the command line. --fit-ctx, + --fit-target, -fitc, -fitt tune the step, not the mode, so they don't + block the retry.""" if use_fit: return False for a in cmd: @@ -5209,9 +5206,8 @@ class LlamaCppBackend: if self._stdout_thread is not None: self._stdout_thread.join(timeout = 2) output = "\n".join(self._stdout_lines[-50:]) - # Keep the TAIL: crash details (abort reason, ROCm/CUDA error - # text) print last, after the long startup banner. Head - # truncation has cut off exactly the diagnostic line before. + # Keep the TAIL: crash details print last, after the startup + # banner; head truncation has cut off the diagnostic line before. _log_hint = ( f" Full log: {self._llama_log_path}" if getattr(self, "_llama_log_path", None) diff --git a/studio/backend/core/training/worker.py b/studio/backend/core/training/worker.py index 1120744a2d..756cae0878 100644 --- a/studio/backend/core/training/worker.py +++ b/studio/backend/core/training/worker.py @@ -27,12 +27,9 @@ from pathlib import Path from typing import Any, Callable # ── WSL AMD Strix Halo (gfx1151): enable ROCDXG before any torch import ────── -# Mirrors main.py. In WSL the AMD GPU is reached via the ROCDXG bridge -# (librocdxg.so over /dev/dxg), which HSA loads only when HSA_ENABLE_DXG_ -# DETECTION=1 is set before torch touches the GPU. A worker spawned outside a -# login shell misses the installer's persisted env and falls back to CPU. -# Gated to no-op unless BOTH /dev/dxg and librocdxg.so exist, so native Linux -# ROCm, NVIDIA, macOS and Windows are unaffected. +# Mirrors main.py: HSA loads librocdxg only with HSA_ENABLE_DXG_DETECTION=1 set +# before torch touches the GPU; a worker spawned outside a login shell misses +# the persisted env and falls back to CPU. No-op unless /dev/dxg + librocdxg exist. if sys.platform.startswith("linux") and "HSA_ENABLE_DXG_DETECTION" not in os.environ: try: if os.path.exists("/dev/dxg") and any( @@ -711,13 +708,10 @@ def _rocm_classify_unified_memory(props: Any) -> tuple[str, bool]: gcn_arch = _v break - # Driver's own answer first: hipDeviceProp_t.integrated (exposed as - # props.is_integrated; same gate PR #5988's UMA safetensors fast-load - # uses). Strictly additive -- only a truthy value upgrades to unified; - # 0/absent falls through to the arch/name logic below, so a wheel that - # omits or zeroes the field can never downgrade the known APU set. This - # covers unified APUs outside the hardcoded arches (gfx1103 Phoenix - # iGPUs, future parts) with one universal signal. + # Driver's own answer first: hipDeviceProp_t.integrated (same gate as + # PR #5988's UMA fast-load). Strictly additive -- only truthy upgrades to + # unified; 0/absent falls through, so a wheel omitting the field can't + # downgrade the known APU set. Covers APUs beyond the hardcoded arches. if getattr(props, "is_integrated", 0): return gcn_arch, True @@ -2163,13 +2157,10 @@ def run_training_process(*, event_queue: Any, stop_queue: Any, config: dict) -> os.environ["TORCHDYNAMO_DISABLE"] = "1" logger.info("Windows ROCm: torch.compile (dynamo) disabled") - # bitsandbytes' import-time get_rocm_gpu_arch() probe runs - # `hipinfo.exe` from PATH; the AMD torch wheel ships it in the venv - # Scripts dir, which is on PATH only for activated venvs. Prepend - # it so the probe succeeds instead of logging a scary (harmless) - # "Could not detect ROCm GPU architecture" ERROR on every import. - # Normally inherited from main.py's env, but workers can also be - # spawned standalone (tests, CLI) -- keep the guard here too. + # bitsandbytes' get_rocm_gpu_arch() runs hipinfo.exe via PATH; the + # AMD wheel ships it in the venv Scripts dir (on PATH only when + # activated). Prepend it to silence a scary-but-harmless ERROR. + # Mirrors main.py for standalone-spawned workers (tests, CLI). _scripts_dir = os.path.dirname(sys.executable) if os.path.isfile(os.path.join(_scripts_dir, "hipInfo.exe")): import shutil as _shutil @@ -2386,18 +2377,13 @@ def run_training_process(*, event_queue: Any, stop_queue: Any, config: dict) -> "unified memory from device name %r; applying unified cap", _dev_name, ) - # Unified hosts on native Windows: mem_get_info's total is the - # WDDM budget the driver grants HIP (BIOS carve + ~half of the - # remaining RAM) -- the OS share is already outside it, so the - # Linux 0.80 starve-protection double-taxes (48.49 GiB budget → - # 38.79 allowed) and blocks loads that fit in free memory. - # 1.0 removes the double-tax. Current AMD Windows wheels only - # enforce sub-1.0 fractions (measured on gfx1151: 0.5 caps, - # 1.0 still allocates past the budget via WDDM overcommit), so - # 1.0 behaves like torch's uncapped default, with WDDM - # arbitrating residency; on wheels that do enforce it, it caps - # at exactly the driver-granted budget. On Linux the total - # spans nearly all RAM, so keep the 0.80 OS headroom there. + # Native Windows unified: mem_get_info's total is the WDDM + # budget (OS share already outside it), so the Linux 0.80 cap + # double-taxes and blocks loads that fit. 1.0 removes that; + # current AMD Windows wheels enforce only sub-1.0 fractions + # (measured on gfx1151), so it acts like torch's uncapped + # default with WDDM arbitrating residency. Linux totals span + # nearly all RAM, so keep the 0.80 OS headroom there. if _is_unified: _mem_fraction = 1.0 if sys.platform == "win32" else 0.80 else: @@ -2411,10 +2397,8 @@ def run_training_process(*, event_queue: Any, stop_queue: Any, config: dict) -> _dev_name, _gcn_arch or "unknown arch", ) - # Unified Windows APUs: the WDDM budget is user-raisable, but - # nothing on the box says so -- users see "48 GB VRAM" on a - # 96 GB machine and assume a Studio bug. Say where the limit - # comes from and how to raise it. + # The WDDM budget is user-raisable but nothing on the box says + # so; tell users where the limit comes from and how to raise it. if _is_unified and sys.platform == "win32": try: import psutil as _psutil diff --git a/studio/backend/main.py b/studio/backend/main.py index 064e261753..3d3f337077 100644 --- a/studio/backend/main.py +++ b/studio/backend/main.py @@ -64,14 +64,11 @@ if sys.platform == "win32": del _add_rocm_dll_dirs # ── Windows AMD ROCm: make hipInfo.exe resolvable for subprocess probes ── - # bitsandbytes' get_rocm_gpu_arch() runs `hipinfo.exe` via PATH at import - # time; the AMD torch wheel ships it in the venv Scripts dir, which is on - # PATH only when the venv is activated -- Studio launches python directly. - # Without this, every bitsandbytes import logs a scary (but harmless) - # "Could not detect ROCm GPU architecture: [WinError 2]" ERROR + WARNING. - # Gated on the file existing: only AMD ROCm wheels ship hipInfo.exe, so - # NVIDIA/CPU hosts are untouched. os.add_dll_directory above does not help - # here -- subprocess PATH resolution ignores DLL search directories. + # bitsandbytes' get_rocm_gpu_arch() runs hipinfo.exe via PATH; the AMD wheel + # ships it in the venv Scripts dir, on PATH only when activated -- Studio + # launches python directly, so every import logs a scary (harmless) ERROR. + # Gated on the file existing (only AMD wheels ship it). add_dll_directory + # doesn't help: subprocess PATH resolution ignores DLL search dirs. _scripts_dir = os.path.dirname(sys.executable) if os.path.isfile(os.path.join(_scripts_dir, "hipInfo.exe")): import shutil as _shutil @@ -133,13 +130,10 @@ if sys.platform == "win32": ) # ── WSL AMD Strix Halo (gfx1151): enable ROCDXG before any torch import ────── -# In WSL the AMD GPU is reached via the ROCDXG bridge (librocdxg.so over -# /dev/dxg), which HSA loads only when HSA_ENABLE_DXG_DETECTION=1 is set BEFORE -# torch touches the GPU. A worker launched outside a login shell (e.g. -# `wsl.exe -d Ubuntu-24.04 python ...`) misses the installer's persisted env -# and silently falls back to CPU. Set it here, gated to no-op unless BOTH -# /dev/dxg AND librocdxg.so exist -- native Linux ROCm, NVIDIA, macOS and -# Windows are unaffected. +# HSA loads the librocdxg bridge only with HSA_ENABLE_DXG_DETECTION=1 set BEFORE +# torch touches the GPU; a process launched outside a login shell misses the +# installer's persisted env and silently falls back to CPU. No-op unless both +# /dev/dxg and librocdxg.so exist. elif sys.platform.startswith("linux") and "HSA_ENABLE_DXG_DETECTION" not in os.environ: try: if os.path.exists("/dev/dxg") and any( diff --git a/studio/backend/run.py b/studio/backend/run.py index 84991a71bb..1ce6850c2d 100644 --- a/studio/backend/run.py +++ b/studio/backend/run.py @@ -726,10 +726,9 @@ def _resolve_frontend_path(frontend_path: Path) -> tuple[Optional[Path], list[Pa class _TeeStream: """Mirror writes to the original stream and a session log file. - Console behavior is unchanged (writes/returns delegate to the original - stream; Tauri's structured-stdout protocol and isatty probes see exactly - what they saw before). The file copy is best-effort: a full disk or a - closed handle must never break the console.""" + Console behavior is unchanged (Tauri's structured-stdout protocol and + isatty probes are unaffected); the file copy is best-effort and must + never break the console.""" def __init__(self, stream, log_fh): self._stream = stream @@ -757,15 +756,11 @@ class _TeeStream: def _setup_server_disk_logging(): - """Tee stdout/stderr to ~/.unsloth/studio/logs/server/ and aim - faulthandler at the same file so hard crashes (access violations / - SIGSEGV in the GPU runtime) leave a stack trace on disk. - - Also exports PYTHONFAULTHANDLER=1 so child Python processes (training - workers) dump native-crash stacks to their captured stderr. Keeps the - newest 20 session logs. Opt out with UNSLOTH_STUDIO_NO_FILE_LOG=1. - Returns the log path, or None when disabled/unavailable. - """ + """Tee stdout/stderr to ~/.unsloth/studio/logs/server/ and point + faulthandler there so native crashes leave a stack trace on disk. + Exports PYTHONFAULTHANDLER=1 for child workers. Keeps the newest 20 + logs; opt out with UNSLOTH_STUDIO_NO_FILE_LOG=1. Returns the log path + or None.""" if os.environ.get("UNSLOTH_STUDIO_NO_FILE_LOG") == "1": return None try: @@ -782,8 +777,7 @@ def _setup_server_disk_logging(): log_dir.mkdir(parents = True, exist_ok = True) stamp = time.strftime("%Y%m%d-%H%M%S") log_path = log_dir / f"server-{stamp}-pid{os.getpid()}.log" - # Line-buffered so the tail survives a hard kill; errors="replace" - # so a console encoding quirk can never take the server down. + # Line-buffered so the tail survives a hard kill; errors="replace" guards encoding quirks. log_fh = open(log_path, "w", encoding = "utf-8", errors = "replace", buffering = 1) except Exception: return None @@ -794,8 +788,7 @@ def _setup_server_disk_logging(): faulthandler.enable(file = log_fh, all_threads = True) except Exception: pass - # Children (training workers) inherit: their native-crash stacks land on - # the stderr the server already captures. + # Children inherit: their native-crash stacks land on captured stderr. os.environ.setdefault("PYTHONFAULTHANDLER", "1") sys.stdout = _TeeStream(sys.stdout, log_fh) @@ -844,12 +837,9 @@ def run_server( except Exception: pass - # Persist a session log + native-crash stacks BEFORE importing main, so - # even import-time failures leave evidence on disk. Field report: Studio - # "terminates without a warning" -- a native crash in the GPU runtime - # kills the process with no Python traceback, and a desktop-shortcut - # console closes before anything can be read. Console-only logging made - # that undiagnosable. + # Arm disk logging BEFORE importing main so import-time failures leave + # evidence. Field report: native GPU-runtime crashes kill the process with + # no traceback, and the shortcut console closes before it can be read. _session_log = _setup_server_disk_logging() if _session_log is not None and not silent: print(f"Session log: {_session_log}") diff --git a/studio/backend/utils/hardware/amd.py b/studio/backend/utils/hardware/amd.py index 27dfe187cc..0683d4ccfd 100644 --- a/studio/backend/utils/hardware/amd.py +++ b/studio/backend/utils/hardware/amd.py @@ -47,13 +47,9 @@ def _hip_sdk_present() -> bool: def _amd_smi_allowed() -> bool: - """Whether it is safe to spawn amd-smi here. - - On Windows without a working HIP runtime, amd-smi elevates a child at - runtime -- popping a UAC/DiskPart prompt that RunAsInvoker can't suppress - (its manifest is asInvoker). So only call it on Windows with a HIP SDK - present or UNSLOTH_ENABLE_AMD_SMI=1. Linux amd-smi never elevates. - """ + """Safe to spawn amd-smi? On Windows without a working HIP runtime it + elevates a child (UAC/DiskPart prompt RunAsInvoker can't suppress), so + require a HIP SDK or UNSLOTH_ENABLE_AMD_SMI=1 there. Linux never elevates.""" if platform.system() != "Windows": return True flag = os.environ.get("UNSLOTH_ENABLE_AMD_SMI", "").strip().lower() @@ -70,10 +66,8 @@ def _run_amd_smi(*args: str, timeout: int = _AMD_SMI_DEFAULT_TIMEOUT) -> Optiona if _amd_smi_disabled: return None if not _amd_smi_allowed(): - # Permanently skip amd-smi on Windows w/o a HIP SDK: every call would - # pop a UAC/DiskPart prompt (see _amd_smi_allowed). VRAM polling is then - # unavailable, but that beats the prompt. Opt back in with - # UNSLOTH_ENABLE_AMD_SMI=1. + # Permanently skip: every call would pop the UAC/DiskPart prompt (see + # _amd_smi_allowed). Opt back in with UNSLOTH_ENABLE_AMD_SMI=1. if not _amd_smi_disabled: logger.info( "amd-smi disabled on Windows (no HIP SDK detected) to avoid a " @@ -83,11 +77,9 @@ def _run_amd_smi(*args: str, timeout: int = _AMD_SMI_DEFAULT_TIMEOUT) -> Optiona _amd_smi_disabled = True return None if shutil.which("amd-smi") is None: - # amd-smi does not exist on Windows (neither Adrenalin nor the HIP SDK - # ship a CLI) and can be absent on minimal Linux installs. Disable the - # poller in one step instead of burning the 3-strike circuit breaker - # on guaranteed FileNotFoundError spawns. Studio's VRAM display falls - # back to torch mem_get_info. + # amd-smi doesn't exist on Windows (no AMD product ships the CLI) and + # can be absent on minimal Linux: disable in one step instead of burning + # the 3-strike breaker; VRAM display falls back to torch mem_get_info. if not _amd_smi_disabled: logger.info( "amd-smi not found on PATH; GPU utilization polling via " @@ -97,8 +89,7 @@ def _run_amd_smi(*args: str, timeout: int = _AMD_SMI_DEFAULT_TIMEOUT) -> Optiona return None _amd_env = child_env_without_native_path_secret() if platform.system() == "Windows": - # RunAsInvoker belt-and-suspenders for any manifest-elevating helper; - # the real guard is _amd_smi_allowed() above. Mirrors install scripts. + # RunAsInvoker is belt-and-suspenders; the real guard is _amd_smi_allowed(). _amd_env = {**_amd_env, "__COMPAT_LAYER": "RunAsInvoker"} try: result = subprocess.run( @@ -111,8 +102,7 @@ def _run_amd_smi(*args: str, timeout: int = _AMD_SMI_DEFAULT_TIMEOUT) -> Optiona ) except (OSError, subprocess.TimeoutExpired) as e: if isinstance(e, FileNotFoundError): - # Raced a PATH change after the which() check above; absence is - # expected on Windows (no AMD product ships an amd-smi CLI there). + # Raced a PATH change after the which() check; expected absent on Windows. logger.debug("amd-smi not found (not in PATH): %s", e) else: logger.warning("amd-smi query failed: %s", e) diff --git a/studio/install_llama_prebuilt.py b/studio/install_llama_prebuilt.py index aab6d641fe..abec4b3b27 100644 --- a/studio/install_llama_prebuilt.py +++ b/studio/install_llama_prebuilt.py @@ -46,23 +46,17 @@ EXIT_FALLBACK = 2 EXIT_ERROR = 1 EXIT_BUSY = 3 -# DiskPart-prompt suppression. RunAsInvoker does NOT stop amd-smi's runtime -# elevation (its manifest is asInvoker), so this is just harmless belt-and- -# suspenders for manifest-elevating tools. The real guard is _amd_smi_allowed(): -# we don't spawn amd-smi on Windows w/o a HIP SDK (or opt-in). +# RunAsInvoker can't stop amd-smi's runtime elevation -- harmless belt-and- +# suspenders; the real guard is _amd_smi_allowed() (no spawn w/o HIP SDK/opt-in). if platform.system() == "Windows": os.environ.setdefault("__COMPAT_LAYER", "RunAsInvoker") def _amd_smi_allowed() -> bool: - """Whether it is safe to spawn amd-smi here. - - On Windows w/o a working HIP runtime, amd-smi elevates a child and pops a - UAC/DiskPart prompt RunAsInvoker can't suppress. Only call it on Windows - when a HIP SDK is detectable (hipinfo present) or UNSLOTH_ENABLE_AMD_SMI=1; - Linux/macOS always allowed. When skipped, the gfx arch still arrives via the - forwarded --rocm-gfx, so prebuilt selection is unaffected. - """ + """Safe to spawn amd-smi? On Windows without a HIP runtime it elevates a + child (UAC/DiskPart prompt), so require a detectable HIP SDK or + UNSLOTH_ENABLE_AMD_SMI=1 there; Linux/macOS always allowed. When skipped, + --rocm-gfx still supplies the arch.""" if platform.system() != "Windows": return True flag = os.environ.get("UNSLOTH_ENABLE_AMD_SMI", "").strip().lower() @@ -2721,9 +2715,8 @@ def run_capture( check: bool = False, env: dict[str, str] | None = None, ) -> subprocess.CompletedProcess[str]: - # amd-smi on Windows auto-elevates and pops a UAC/DiskPart prompt mid-install; - # RunAsInvoker forces it un-elevated. Callers already fall back to WMI/name - # detection. Mirrors install.ps1's Invoke-AmdSmiNoElevate; Windows-only. + # amd-smi auto-elevates on Windows (UAC/DiskPart prompt mid-install); + # RunAsInvoker forces it un-elevated (mirrors install.ps1). Windows-only. if ( command and platform.system() == "Windows" @@ -2992,9 +2985,8 @@ def detect_host() -> HostInfo: _candidate = os.path.join(_root, "bin", f"{name}.exe") if os.path.isfile(_candidate): return _candidate - # AMD torch wheels ship hipInfo.exe into the venv Scripts dir - # (next to python.exe) -- resolvable on driver-only hosts where no - # SDK dir exists, so a standalone rerun can still detect the GPU. + # AMD torch wheels ship hipInfo.exe in the venv Scripts dir -- + # resolvable on driver-only hosts with no SDK dir. _venv_candidate = os.path.join(os.path.dirname(sys.executable), f"{name}.exe") if os.path.isfile(_venv_candidate): return _venv_candidate diff --git a/studio/install_python_stack.py b/studio/install_python_stack.py index d404041ca7..75dea7985e 100644 --- a/studio/install_python_stack.py +++ b/studio/install_python_stack.py @@ -42,11 +42,9 @@ IS_MAC_INTEL = IS_MACOS and platform.machine() == "x86_64" IS_MAC_ARM = IS_MACOS and platform.machine() == "arm64" IS_LINUX = sys.platform.startswith("linux") -# DiskPart-prompt suppression: amd-smi auto-elevates on Windows, popping a -# UAC/DiskPart prompt mid-install. This installer only spawns probes and pip/uv -# (none need elevation), so set __COMPAT_LAYER=RunAsInvoker process-wide -- every -# amd-smi subprocess then runs un-elevated, no per-call guard needed. setup.ps1 -# keeps per-call guards since it ALSO spawns winget installers that need elevation. +# amd-smi auto-elevates on Windows (UAC/DiskPart prompt). This installer spawns +# only probes and pip/uv, so RunAsInvoker process-wide is safe; setup.ps1 keeps +# per-call guards because it also spawns winget installers that need elevation. if IS_WINDOWS: os.environ.setdefault("__COMPAT_LAYER", "RunAsInvoker") # torchcodec ships wheels only for manylinux_2_28_x86_64, macosx_12_0_arm64, @@ -160,21 +158,17 @@ def _bnb_rocm_prerelease_url() -> str | None: def _amd_smi_env() -> dict[str, str] | None: """On Windows, env with __COMPAT_LAYER=RunAsInvoker; None elsewhere. - NB: RunAsInvoker doesn't stop amd-smi's runtime elevation (its manifest is - asInvoker -- it elevates a child via ShellExecute). The real guard is - _amd_smi_allowed() below; this is harmless belt-and-suspenders.""" + Belt-and-suspenders only -- RunAsInvoker can't stop amd-smi's runtime + elevation; the real guard is _amd_smi_allowed().""" if platform.system() != "Windows": return None return {**os.environ, "__COMPAT_LAYER": "RunAsInvoker"} def _amd_smi_allowed() -> bool: - """Whether it is safe to spawn amd-smi here. - - On Windows w/o a working HIP runtime, amd-smi elevates a child and pops a - UAC/DiskPart prompt RunAsInvoker can't suppress. Only call it on Windows with - a HIP SDK (hipinfo present) or UNSLOTH_ENABLE_AMD_SMI=1; Linux/macOS always. - """ + """Safe to spawn amd-smi? On Windows without a HIP runtime it elevates a + child (UAC/DiskPart prompt), so require a HIP SDK or + UNSLOTH_ENABLE_AMD_SMI=1 there; Linux/macOS always allowed.""" if platform.system() != "Windows": return True flag = os.environ.get("UNSLOTH_ENABLE_AMD_SMI", "").strip().lower() @@ -210,8 +204,7 @@ def _detect_rocm_version() -> tuple[int, int] | None: pass # Try amd-smi version (outputs "... | ROCm version: X.Y.Z"). - # Gated off on Windows w/o a HIP SDK (avoids the UAC/DiskPart prompt); - # hipconfig below covers that case. + # Gated off on Windows w/o a HIP SDK (UAC prompt); hipconfig below covers it. amd_smi = shutil.which("amd-smi") if _amd_smi_allowed() else None if amd_smi: try: @@ -339,10 +332,8 @@ def _detect_windows_gfx_arch() -> str | None: hipinfo = _candidate break if not hipinfo: - # 2b. AMD torch wheels ship hipInfo.exe into the venv Scripts dir - # (next to python.exe); resolvable even on driver-only hosts with no - # SDK install at all. Lets `studio update` re-detect the arch on a - # venv that already has the AMD wheel. + # 2b. AMD torch wheels ship hipInfo.exe in the venv Scripts dir -- + # lets `studio update` re-detect the arch on driver-only hosts. _venv_hipinfo = os.path.join(os.path.dirname(sys.executable), "hipInfo.exe") if os.path.isfile(_venv_hipinfo): hipinfo = _venv_hipinfo @@ -368,8 +359,7 @@ def _detect_windows_gfx_arch() -> str | None: pass # 3. amd-smi fallback -- runtime-only Radeon installs ship amd-smi but no hipinfo. - # Gated off on Windows w/o a HIP SDK (avoids the UAC/DiskPart prompt); the arch - # arrives via --rocm-gfx / name inference there, so this is only needed when safe. + # Gated off on Windows w/o a HIP SDK (UAC prompt); --rocm-gfx / name inference covers it. amd_smi = shutil.which("amd-smi") if _amd_smi_allowed() else None if amd_smi: for _args in (("static", "--asic"), ("list",)): @@ -398,11 +388,9 @@ def _detect_windows_gfx_arch() -> str | None: except Exception: continue - # 4. Last resort: GPU marketing name via WMI → arch table. Driver-only - # hosts (Adrenalin, no HIP SDK) have neither hipinfo nor amd-smi - # (amd-smi does not exist on Windows at all), but the display driver - # always knows the GPU name. Mirrors setup.ps1's $nameArchTable so a - # standalone `studio update` can repair a CPU-only venv on such hosts. + # 4. Last resort: GPU marketing name via WMI → arch table. Driver-only hosts + # have neither hipinfo nor amd-smi, but the driver knows the GPU name; + # mirrors setup.ps1's $nameArchTable so `studio update` can repair CPU venvs. try: result = subprocess.run( [ @@ -437,17 +425,17 @@ def _detect_windows_gfx_arch() -> str | None: # prebuilts / AMD Windows torch indexes support; unknown names return None # (callers then fall back cleanly to CPU). _WIN_GPU_NAME_ARCH_TABLE: "list[tuple[str, str]]" = [ - (r"9070 XT|9080", "gfx1201"), # RDNA 4 (Radeon RX 9070 XT / 9080) - (r"9070|9060", "gfx1200"), # RDNA 4 (Radeon RX 9070 / 9060) - # RDNA 3.5 (Strix Halo: Radeon 8060S/8050S/8040S iGPU, Ryzen AI Max+) + (r"9070 XT|9080", "gfx1201"), # RDNA 4 + (r"9070|9060", "gfx1200"), # RDNA 4 + # RDNA 3.5 (Strix Halo) (r"8060S|8050S|8040S|Strix Halo|Ryzen AI Max|AI Max", "gfx1151"), - # RDNA 3.5 (Strix/Krackan Point: Radeon 890M/880M iGPU, Ryzen AI 9 HX 370/375) + # RDNA 3.5 (Strix/Krackan Point) ( r"890M|880M|860M|840M|Strix Point|Krackan|HX 37[05]|AI 9 HX|AI 9 36[05]" r"|AI 7 35[05]|AI 5 34[05]|AI 7 PRO 35|AI 5 33", "gfx1150", ), - # RDNA 3 desktop / workstation (Navi 31) + # RDNA 3 (Navi 31) (r"RX 7900|RX 7800|RX 7700(?!S)|PRO W7900|PRO W7800|PRO W7700", "gfx1100"), (r"RX 7600|RX 7700S|RX 7650|PRO W7600|PRO W7500|PRO V710", "gfx1102"), # Navi 33 # RDNA 3 iGPU (Phoenix / Hawk Point) @@ -596,8 +584,7 @@ def _has_rocm_gpu() -> bool: exe = shutil.which(cmd[0]) if not exe: continue - # Skip amd-smi on Windows w/o a HIP SDK (avoids the UAC/DiskPart prompt); - # rely on rocminfo / the sysfs fallback there. + # Skip amd-smi on Windows w/o a HIP SDK (UAC prompt); rocminfo/sysfs cover it. if cmd[0] == "amd-smi" and not _amd_smi_allowed(): continue try: @@ -2021,10 +2008,8 @@ def install_python_stack() -> int: _wexe = shutil.which(_wcmd[0]) if not _wexe: continue - # Skip amd-smi on Windows w/o a HIP SDK (avoids the UAC/DiskPart - # prompt), as _has_rocm_gpu()/_detect_amd_gfx_codes do. The only loss - # is the best-effort "AMD GPU detected" note; ROCm-torch state below - # comes from the install itself. + # Skip amd-smi w/o a HIP SDK (UAC prompt). Only loss: the best-effort + # "AMD GPU detected" note; ROCm-torch state comes from the install. if _wcmd[0] == "amd-smi" and not _amd_smi_allowed(): continue try: diff --git a/studio/setup.ps1 b/studio/setup.ps1 index acc7e92875..b346634178 100644 --- a/studio/setup.ps1 +++ b/studio/setup.ps1 @@ -745,25 +745,21 @@ if (-not $HasNvidiaSmi) { } } # ── Helper: run amd-smi without triggering a UAC elevation prompt ── -# amd-smi on Windows auto-elevates to read GPU/APU memory, surfacing a confusing -# DiskPart UAC prompt mid-install (Studio backend amd.py hits the same). RunAsInvoker -# forces it (and helpers it spawns) to run un-elevated; on failure the WMI name -> -# gfx fallback still resolves the arch. +# amd-smi auto-elevates on Windows (confusing DiskPart UAC prompt mid-install); +# __COMPAT_LAYER=RunAsInvoker runs it un-elevated (backend amd.py does the same). function Invoke-AmdSmiNoElevate { param( [Parameter(Mandatory = $true, Position = 0)][string]$Exe, [Parameter(Position = 1)][string[]]$SmiArgs = @(), [int]$TimeoutSec = 30 ) - # RunAsInvoker blocks the auto-elevation/UAC prompt; the timeout bounds a flaky - # amd-smi that can otherwise spin for minutes (30s mirrors the backend amd.py). + # Timeout bounds a flaky amd-smi that can spin for minutes (30s mirrors amd.py). $prevCompat = [Environment]::GetEnvironmentVariable('__COMPAT_LAYER', 'Process') $env:__COMPAT_LAYER = 'RunAsInvoker' try { - # [Process]::Start, NOT Start-Process -PassThru: the latter leaves .ExitCode - # $null after WaitForExit on PS 5.1, so $LASTEXITCODE (checked by callers) - # reads non-zero and kills detection. Async reads drain the pipes (no - # deadlock); amd-smi args have no spaces so a plain join is safe. + # NOT Start-Process -PassThru: on PS 5.1 it leaves .ExitCode $null, breaking + # callers' $LASTEXITCODE checks. Async reads avoid pipe deadlock; amd-smi + # args have no spaces so a plain join is safe. $psi = New-Object System.Diagnostics.ProcessStartInfo $psi.FileName = $Exe $psi.Arguments = ($SmiArgs -join ' ') @@ -940,11 +936,11 @@ if (-not $HasNvidiaSmi) { # (gfx120X/110X/1151/1150/103X); unknown names fall back cleanly to CPU. elseif ($ROCmGpuLabel) { $nameArchTable = @( - @{ P = "9070 XT|9080"; A = "gfx1201" } # RDNA 4 (Radeon RX 9070 XT / 9080) - @{ P = "9070|9060"; A = "gfx1200" } # RDNA 4 (Radeon RX 9070 / 9060) - @{ P = "8060S|8050S|8040S|Strix Halo|Ryzen AI Max|AI Max"; A = "gfx1151" } # RDNA 3.5 (Strix Halo: Radeon 8060S/8050S/8040S iGPU, Ryzen AI Max+) - @{ P = "890M|880M|860M|840M|Strix Point|Krackan|HX 37[05]|AI 9 HX|AI 9 36[05]|AI 7 35[05]|AI 5 34[05]|AI 7 PRO 35|AI 5 33"; A = "gfx1150" } # RDNA 3.5 (Strix/Krackan Point: Radeon 890M/880M iGPU, Ryzen AI 9 HX 370/375) - @{ P = "RX 7900|RX 7800|RX 7700(?!S)|PRO W7900|PRO W7800|PRO W7700"; A = "gfx1100" } # RDNA 3 desktop / workstation (Navi 31) + @{ P = "9070 XT|9080"; A = "gfx1201" } # RDNA 4 + @{ P = "9070|9060"; A = "gfx1200" } # RDNA 4 + @{ P = "8060S|8050S|8040S|Strix Halo|Ryzen AI Max|AI Max"; A = "gfx1151" } # RDNA 3.5 (Strix Halo) + @{ P = "890M|880M|860M|840M|Strix Point|Krackan|HX 37[05]|AI 9 HX|AI 9 36[05]|AI 7 35[05]|AI 5 34[05]|AI 7 PRO 35|AI 5 33"; A = "gfx1150" } # RDNA 3.5 (Strix/Krackan Point) + @{ P = "RX 7900|RX 7800|RX 7700(?!S)|PRO W7900|PRO W7800|PRO W7700"; A = "gfx1100" } # RDNA 3 (Navi 31) @{ P = "RX 7600|RX 7700S|RX 7650|PRO W7600|PRO W7500|PRO V710"; A = "gfx1102" } # RDNA 3 (Navi 33) @{ P = "780M|760M|740M|Phoenix|Hawk Point|Z1 Extreme|Z2 Extreme"; A = "gfx1103" } # RDNA 3 iGPU (Phoenix / Hawk Point) @{ P = "RX 6900|RX 6800|RX 6750|RX 6700|PRO W6800|PRO W6900"; A = "gfx1030" } # RDNA 2 (Navi 21) -- gfx103X family @@ -1021,8 +1017,8 @@ if ($HasNvidiaSmi) { substep " Ensure the ROCm compute driver is installed alongside the display driver:" "Yellow" substep " https://rocm.docs.amd.com/en/latest/deploy/windows/index.html" "Yellow" } elseif ($script:ROCmGfxArch) { - # Known arch: PyTorch comes from AMD's bundled-runtime ROCm wheels (repo.amd.com), - # which ship their own runtime -- HIP SDK optional (only adds the system toolchain). + # Known arch: PyTorch comes from AMD's bundled-runtime ROCm wheels + # (repo.amd.com) -- HIP SDK optional. Write-Host "" step "gpu" "AMD ROCm ($script:ROCmGfxArch)" "Cyan" substep "Detected: $ROCmGpuLabel" "Cyan" @@ -1474,8 +1470,7 @@ if ($HasROCm) { $rocmVerLabel = if ($script:ROCmVersionFull) { "ROCm $script:ROCmVersionFull" } elseif ($script:ROCmVersion) { "ROCm $script:ROCmVersion" } else { "ROCm (version unknown)" } step "rocm" $rocmVerLabel } elseif ($script:ROCmGfxArch) { - # GPU training/inference works via AMD's bundled-runtime ROCm PyTorch wheels; - # the HIP SDK is optional (only the system ROCm toolchain). + # GPU works via AMD's bundled-runtime ROCm wheels; HIP SDK optional. step "rocm" "GPU via bundled ROCm wheels ($script:ROCmGfxArch) -- HIP SDK optional" "Cyan" } elseif ($ROCmGpuLabel) { step "rocm" "AMD GPU detected -- arch unknown; HIP SDK not found" "Yellow" @@ -2204,10 +2199,9 @@ if ($env:SKIP_STUDIO_BASE -ne "1" -and $env:STUDIO_LOCAL_INSTALL -ne "1") { if ($InstalledVer -and $LatestVer -and ($InstalledVer -eq $LatestVer)) { step "python" "$_PkgName $InstalledVer is up to date" $SkipPythonDeps = $true - # ...but not if an AMD GPU is present and installed PyTorch is CPU-only - # (host predates ROCm-wheel support, or GPU added later): the fast "up to - # date" path would leave the user on CPU torch with Train/Export disabled. - # Force the dependency pass so the ROCm wheels get installed. + # ...unless an AMD GPU is present but installed torch is CPU-only (host + # predates ROCm-wheel support): force the dependency pass so the ROCm + # wheels install instead of leaving Train/Export disabled. if ($script:ROCmGfxArch) { $_torchIsCpu = $true try { @@ -2282,12 +2276,10 @@ if ($HasNvidiaSmi) { # Wheels bundle their own ROCm runtime; HIP SDK version is irrelevant. $ROCmGfxArch = $script:ROCmGfxArch $ROCmIndexUrl = $null -# Install AMD ROCm PyTorch wheels when ROCm is confirmed OR a gfx arch is known -# (name-inferred on Adrenalin-only hosts). The per-arch wheels bundle the runtime -# (rocm-sdk-libraries-), so torch.cuda.is_available() is True without a HIP -# SDK -- which flips Studio out of chat-only (CHAT_ONLY) and enables Train/Export. -# Gating on $HasROCm alone left Strix Halo / Radeon 8060S on CPU torch; a failed -# ROCm install still falls back to CPU below, so this is safe. +# Install ROCm PyTorch when ROCm is confirmed OR a gfx arch is known (name- +# inferred, Adrenalin-only hosts): the wheels bundle the runtime, so GPU torch +# works without a HIP SDK and Studio leaves chat-only mode. Gating on $HasROCm +# alone left Strix Halo on CPU torch; a failed install still falls back to CPU. if (($HasROCm -or $ROCmGfxArch) -and $CuTag -eq "cpu") { $amdIndexBase = if ($env:UNSLOTH_ROCM_WINDOWS_MIRROR) { $env:UNSLOTH_ROCM_WINDOWS_MIRROR.TrimEnd('/') } else { "https://repo.amd.com/rocm/whl" } $archFamilyMap = @{ diff --git a/studio/setup.sh b/studio/setup.sh index 613d311708..18bdd9d2de 100755 --- a/studio/setup.sh +++ b/studio/setup.sh @@ -854,10 +854,9 @@ fi fi # ── GPU detection summary (mirrors setup.ps1 step "gpu" block) ── -# WSL2 ROCDXG: the system rocminfo enumerates the GPU over /dev/dxg only when -# HSA_ENABLE_DXG_DETECTION=1 (a no-op on bare metal), and /opt/rocm/bin can be -# off PATH outside login shells (the profile.d drop-in). Seed both before the -# probes or a ROCDXG WSL host is misdetected as CPU-only. +# WSL2 ROCDXG: rocminfo only sees the GPU with HSA_ENABLE_DXG_DETECTION=1 +# (no-op on bare metal), and /opt/rocm/bin may be off PATH in non-login shells. +# Seed both or a ROCDXG WSL host is misdetected as CPU-only. export HSA_ENABLE_DXG_DETECTION="${HSA_ENABLE_DXG_DETECTION:-1}" if ! command -v rocminfo >/dev/null 2>&1 && [ -x /opt/rocm/bin/rocminfo ]; then PATH="$PATH:/opt/rocm/bin" @@ -918,9 +917,8 @@ elif [ "$_setup_amd_detected" = true ]; then substep "gfx arch from UNSLOTH_ROCM_GFX_ARCH env override: $_setup_gfx" # Name-based arch inference when tools don't report gfx (mirrors setup.ps1 nameArchTable) elif [ -z "$_setup_gfx" ] && [ -n "$_setup_mkt" ]; then - # Kept in sync with the table in install.sh (and the PS nameArchTable). - # gfx1102 matched BEFORE gfx1100 so the spaceless "RX 7700S" lands on - # gfx1102 (bash case has no negative lookahead like the PS tables). + # In sync with install.sh's table (and the PS nameArchTable). gfx1102 comes + # BEFORE gfx1100 so "RX 7700S" matches it (bash case has no negative lookahead). case "$_setup_mkt" in *"9070 XT"*|*9080*) _setup_gfx="gfx1201" ;; # RDNA 4 *9070*|*9060*) _setup_gfx="gfx1200" ;; # RDNA 4 diff --git a/unsloth_cli/commands/studio.py b/unsloth_cli/commands/studio.py index 4608457a96..321e4fb41f 100644 --- a/unsloth_cli/commands/studio.py +++ b/unsloth_cli/commands/studio.py @@ -1252,11 +1252,9 @@ _PID_FILE = STUDIO_HOME / "studio.pid" def _pid_alive(pid: int) -> bool: - """Return True if a process with ``pid`` exists. - - ``os.kill(pid, 0)`` raises OSError (WinError 87) for every pid on Windows, - so use ``tasklist`` there and the signal-0 probe elsewhere. - """ + """True if a process with ``pid`` exists. ``os.kill(pid, 0)`` raises + OSError (WinError 87) for every pid on Windows, so use ``tasklist`` + there and the signal-0 probe elsewhere.""" if sys.platform == "win32": try: out = subprocess.run(