fix: robust gfx arch detection for Strix Halo / HIP-runtime-only installs

Covers users who have the HIP runtime (amd-smi available) but not the
full HIP SDK (no hipinfo), which is common on Strix Halo iGPU systems.
Without this, $ROCmGfxArch stays null and the installer silently falls
back to CPU-only PyTorch despite a working GPU.

Detection waterfall (setup.ps1 + install.ps1):
  1. hipinfo gcnArchName          -- full HIP SDK (existing, unchanged)
  2. amd-smi list gfx pattern     -- newer amd-smi versions embed arch
  3. amd-smi static --asic        -- ROCm 6+ ASIC details with GFX target
  4. UNSLOTH_ROCM_GFX_ARCH env    -- manual override escape hatch
  5. GPU name → arch table        -- best-effort from marketing name:
       890M / Strix Halo  → gfx1151 (RDNA 3.5 iGPU, Strix Halo)
       880M / Strix Point → gfx1150 (RDNA 3.5 iGPU, Strix Point)
       780M / Phoenix     → gfx1103 (RDNA 3 iGPU)
       RX 7900/7800/7700  → gfx1100 (RDNA 3 desktop)
       RX 9070 XT / 9080  → gfx1201 (RDNA 4)
       RX 9070 / 9060 XT  → gfx1200 (RDNA 4)

When arch is inferred from name, a Cyan substep tells the user to set
UNSLOTH_ROCM_GFX_ARCH to skip inference on future installs.
WMI block intentionally does not set $HasROCm (no runtime confirmation).

Tests: 11 new tests in TestStrixHaloGfxArchDetection covering all five
detection levels, WMI safety, and gfx regex in both ps1 files.
This commit is contained in:
LeoBorcherding 2026-05-16 14:45:33 -05:00
commit 7a5e93b42c
3 changed files with 194 additions and 4 deletions

View file

@ -1235,7 +1235,24 @@ shell.Run cmd, 0, False
$smiOut = & $amdSmiExe.Source list 2>&1 | Out-String
if ($LASTEXITCODE -eq 0 -and $smiOut -match "(?im)^GPU\s*[:\[]\s*\d") {
$HasROCm = $true
$ROCmGpuLabel = "AMD ROCm"
# Attempt 1: newer amd-smi versions embed the gfx arch in list output
if ($smiOut -match "(?i)\b(gfx\d+[a-z]?)\b") {
$ROCmGfxArch = $Matches[1].ToLower()
$ROCmGpuLabel = "AMD ROCm ($ROCmGfxArch)"
} else {
# Attempt 2: 'static --asic' exposes ASIC details on ROCm 6+,
# including the GFX target needed for wheel index selection.
$smiAsicOut = ""
try { $smiAsicOut = & $amdSmiExe.Source static --asic 2>&1 | Out-String } catch {}
if ($smiAsicOut -match "(?i)\b(gfx\d+[a-z]?)\b") {
$ROCmGfxArch = $Matches[1].ToLower()
$ROCmGpuLabel = "AMD ROCm ($ROCmGfxArch)"
} elseif ($smiAsicOut -match "(?im)Market.?Name\s*[:\|]\s*([^\r\n]+)") {
$ROCmGpuLabel = "AMD ROCm ($($Matches[1].Trim()))"
} else {
$ROCmGpuLabel = "AMD ROCm"
}
}
}
} catch {}
}
@ -1248,6 +1265,38 @@ shell.Run cmd, 0, False
if ($wmiGpu) { $ROCmGpuLabel = $wmiGpu.Name }
} catch {}
}
# ── Arch resolution: env-var override → name inference ──────────────
# Covers users whose amd-smi is too old to report the GFX target and
# who don't have hipinfo (HIP-runtime-only, common on Strix Halo / iGPU).
if ($HasROCm -and -not $ROCmGfxArch) {
# 1. Manual override: set UNSLOTH_ROCM_GFX_ARCH=gfx1151 before running.
if ($env:UNSLOTH_ROCM_GFX_ARCH) {
$ROCmGfxArch = $env:UNSLOTH_ROCM_GFX_ARCH.Trim().ToLower()
$ROCmGpuLabel = "AMD ROCm ($ROCmGfxArch)"
substep "gfx arch from UNSLOTH_ROCM_GFX_ARCH env override: $ROCmGfxArch" "Cyan"
}
# 2. Best-effort name → arch lookup from marketing name (amd-smi / WMI).
elseif ($ROCmGpuLabel) {
$nameArchTable = @(
@{ P = "9070 XT|9080"; A = "gfx1201" } # RDNA 4
@{ P = "9070|9060"; A = "gfx1200" } # RDNA 4
@{ P = "890M|Strix Halo|HX 37[05]|HX 38[05]|AI 9 HX"; A = "gfx1151" } # RDNA 3.5 iGPU (Strix Halo)
@{ P = "880M|Strix Point|AI 9 36[05]|AI 7 35[05]|AI 5 34[05]"; A = "gfx1150" } # RDNA 3.5 iGPU (Strix Point)
@{ P = "RX 7900|RX 7800|RX 7700(?! S)"; A = "gfx1100" } # RDNA 3 desktop
@{ P = "RX 7600"; A = "gfx1102" } # RDNA 3
@{ P = "780M|760M|740M|Phoenix"; A = "gfx1103" } # RDNA 3 iGPU (Phoenix)
)
foreach ($row in $nameArchTable) {
if ($ROCmGpuLabel -match $row.P) {
$ROCmGfxArch = $row.A
$ROCmGpuLabel = "AMD ROCm ($ROCmGfxArch)"
substep "gfx arch inferred from GPU name: $ROCmGfxArch" "Cyan"
substep "Tip: set UNSLOTH_ROCM_GFX_ARCH=$ROCmGfxArch to skip inference next time" "Cyan"
break
}
}
}
}
# Capture ROCm version for wheel selection (hipconfig, then amd-smi)
if ($HasROCm) {
$hipConfigExe = Get-Command hipconfig -ErrorAction SilentlyContinue

View file

@ -703,7 +703,10 @@ if (-not $HasNvidiaSmi) {
}
} catch {}
}
# amd-smi list fallback: look for "GPU: <digit>" data rows
# amd-smi fallback: HIP runtime present but hipinfo unavailable (no full HIP SDK).
# Confirms GPU visibility via 'list', then attempts 'static --asic' to extract
# the gfx arch that hipinfo would have provided. Critical for Strix Halo
# (gfx1151) and other iGPUs where only the HIP runtime is installed.
if (-not $HasROCm) {
$amdSmiExe = Get-Command "amd-smi" -ErrorAction SilentlyContinue
if ($amdSmiExe) {
@ -711,12 +714,33 @@ if (-not $HasNvidiaSmi) {
$smiOut = & $amdSmiExe.Source list 2>&1 | Out-String
if ($LASTEXITCODE -eq 0 -and $smiOut -match "(?im)^GPU\s*[:\[]\s*\d") {
$HasROCm = $true
$ROCmGpuLabel = "AMD ROCm"
# Attempt 1: newer amd-smi versions embed the gfx arch in list output
if ($smiOut -match "(?i)\b(gfx\d+[a-z]?)\b") {
$script:ROCmGfxArch = $Matches[1].ToLower()
$ROCmGpuLabel = "AMD ROCm ($script:ROCmGfxArch)"
} else {
# Attempt 2: 'static --asic' exposes ASIC details on ROCm 6+,
# including the GFX target needed for wheel index selection.
$smiAsicOut = ""
try { $smiAsicOut = & $amdSmiExe.Source static --asic 2>&1 | Out-String } catch {}
if ($smiAsicOut -match "(?i)\b(gfx\d+[a-z]?)\b") {
$script:ROCmGfxArch = $Matches[1].ToLower()
$ROCmGpuLabel = "AMD ROCm ($script:ROCmGfxArch)"
} elseif ($smiAsicOut -match "(?im)Market.?Name\s*[:\|]\s*([^\r\n]+)") {
$ROCmGpuLabel = "AMD ROCm ($($Matches[1].Trim()))"
} else {
$ROCmGpuLabel = "AMD ROCm"
}
}
}
} catch {}
}
}
# WMI fallback: AMD GPU in device list but no HIP SDK → guide the user
# WMI fallback: AMD GPU in device list but no HIP SDK → guide the user.
# WMI gives a marketing name (e.g. "AMD Radeon 890M") but never a gfx arch.
# $HasROCm is intentionally NOT set here — we cannot confirm ROCm runtime
# support without hipinfo or amd-smi. The name is saved to $ROCmGpuLabel
# so the name-based inference below can still attempt an arch lookup.
if (-not $HasROCm) {
try {
$wmiGpu = Get-WmiObject Win32_VideoController -ErrorAction SilentlyContinue |
@ -725,6 +749,40 @@ if (-not $HasNvidiaSmi) {
if ($wmiGpu) { $ROCmGpuLabel = $wmiGpu.Name }
} catch {}
}
# ── Arch resolution: env-var override → name inference ──────────────────
# Runs after all probe methods. Covers users whose amd-smi version is too
# old to report the GFX target and who don't have hipinfo (HIP-runtime-only
# installs, common on Strix Halo / iGPU systems).
if ($HasROCm -and -not $script:ROCmGfxArch) {
# 1. Manual override: set UNSLOTH_ROCM_GFX_ARCH=gfx1151 before running.
if ($env:UNSLOTH_ROCM_GFX_ARCH) {
$script:ROCmGfxArch = $env:UNSLOTH_ROCM_GFX_ARCH.Trim().ToLower()
$ROCmGpuLabel = "AMD ROCm ($script:ROCmGfxArch)"
substep "gfx arch from UNSLOTH_ROCM_GFX_ARCH env override: $script:ROCmGfxArch" "Cyan"
}
# 2. Best-effort name → arch lookup from marketing name (amd-smi / WMI).
# Ordered most-specific first; first match wins.
elseif ($ROCmGpuLabel) {
$nameArchTable = @(
@{ P = "9070 XT|9080"; A = "gfx1201" } # RDNA 4
@{ P = "9070|9060"; A = "gfx1200" } # RDNA 4
@{ P = "890M|Strix Halo|HX 37[05]|HX 38[05]|AI 9 HX"; A = "gfx1151" } # RDNA 3.5 iGPU (Strix Halo)
@{ P = "880M|Strix Point|AI 9 36[05]|AI 7 35[05]|AI 5 34[05]"; A = "gfx1150" } # RDNA 3.5 iGPU (Strix Point)
@{ P = "RX 7900|RX 7800|RX 7700(?! S)"; A = "gfx1100" } # RDNA 3 desktop
@{ P = "RX 7600"; A = "gfx1102" } # RDNA 3
@{ P = "780M|760M|740M|Phoenix"; A = "gfx1103" } # RDNA 3 iGPU (Phoenix)
)
foreach ($row in $nameArchTable) {
if ($ROCmGpuLabel -match $row.P) {
$script:ROCmGfxArch = $row.A
$ROCmGpuLabel = "AMD ROCm ($script:ROCmGfxArch)"
substep "gfx arch inferred from GPU name: $script:ROCmGfxArch" "Cyan"
substep "Tip: set UNSLOTH_ROCM_GFX_ARCH=$script:ROCmGfxArch to skip inference next time" "Cyan"
break
}
}
}
}
# Capture ROCm version early for display and wheel selection
if ($HasROCm) {
$script:ROCmVersion = $null

View file

@ -1993,5 +1993,88 @@ class TestRocmTorchPkgSpecs:
assert mapping.get(arch) == "gfx110X-all", f"{arch} missing from mapping"
# =============================================================================
# TEST: setup.ps1 / install.ps1 -- Strix Halo gfx arch detection
# =============================================================================
_SETUP_PS1_PATH = PACKAGE_ROOT / "studio" / "setup.ps1"
_INSTALL_PS1_PATH = PACKAGE_ROOT / "install.ps1"
class TestStrixHaloGfxArchDetection:
"""Verify that setup.ps1 and install.ps1 have robust gfx arch detection
for Strix Halo / iGPU users who only have the HIP runtime (no hipinfo)."""
def test_amd_smi_static_asic_attempted_in_setup(self):
"""setup.ps1 must try 'amd-smi static --asic' when list output lacks gfx arch."""
source = _SETUP_PS1_PATH.read_text(encoding = "utf-8")
assert "static --asic" in source
def test_amd_smi_static_asic_attempted_in_install(self):
"""install.ps1 must try 'amd-smi static --asic' when list output lacks gfx arch."""
source = _INSTALL_PS1_PATH.read_text(encoding = "utf-8")
assert "static --asic" in source
def test_env_var_override_in_setup(self):
"""setup.ps1 must honour UNSLOTH_ROCM_GFX_ARCH as a manual arch override."""
source = _SETUP_PS1_PATH.read_text(encoding = "utf-8")
assert "UNSLOTH_ROCM_GFX_ARCH" in source
def test_env_var_override_in_install(self):
"""install.ps1 must honour UNSLOTH_ROCM_GFX_ARCH as a manual arch override."""
source = _INSTALL_PS1_PATH.read_text(encoding = "utf-8")
assert "UNSLOTH_ROCM_GFX_ARCH" in source
def test_name_arch_table_covers_strix_halo_in_setup(self):
"""setup.ps1 name→arch table must map 890M / Strix Halo to gfx1151."""
source = _SETUP_PS1_PATH.read_text(encoding = "utf-8")
assert "gfx1151" in source
assert "890M" in source or "Strix Halo" in source
def test_name_arch_table_covers_strix_halo_in_install(self):
"""install.ps1 name→arch table must map 890M / Strix Halo to gfx1151."""
source = _INSTALL_PS1_PATH.read_text(encoding = "utf-8")
assert "gfx1151" in source
assert "890M" in source or "Strix Halo" in source
def test_name_arch_table_covers_strix_point_in_setup(self):
"""setup.ps1 name→arch table must map 880M / Strix Point to gfx1150."""
source = _SETUP_PS1_PATH.read_text(encoding = "utf-8")
assert "gfx1150" in source
assert "880M" in source or "Strix Point" in source
def test_name_arch_table_covers_strix_point_in_install(self):
"""install.ps1 name→arch table must map 880M / Strix Point to gfx1150."""
source = _INSTALL_PS1_PATH.read_text(encoding = "utf-8")
assert "gfx1150" in source
assert "880M" in source or "Strix Point" in source
def test_name_arch_table_covers_rdna3_phoenix_in_setup(self):
"""setup.ps1 name→arch table must map 780M / Phoenix to gfx1103."""
source = _SETUP_PS1_PATH.read_text(encoding = "utf-8")
assert "gfx1103" in source
assert "780M" in source or "Phoenix" in source
def test_wmi_does_not_set_hasrocm_in_setup(self):
"""WMI block in setup.ps1 must NOT set $HasROCm = $true (no runtime confirmation)."""
source = _SETUP_PS1_PATH.read_text(encoding = "utf-8")
# Find the WMI block and confirm HasROCm is not set inside it
wmi_idx = source.find("Win32_VideoController")
assert wmi_idx != -1, "WMI block not found in setup.ps1"
# The nearest HasROCm = $true must not appear between the WMI block
# and the closing brace of that if-block. We check by confirming
# $HasROCm = $true does NOT appear within 300 chars of the WMI call.
wmi_context = source[wmi_idx : wmi_idx + 300]
assert "$HasROCm = $true" not in wmi_context
def test_gfx_arch_regex_parses_from_amd_smi_output(self):
"""Both files must use the gfx\\d+[a-z]? regex to parse arch from amd-smi output."""
for path in (_SETUP_PS1_PATH, _INSTALL_PS1_PATH):
source = path.read_text(encoding = "utf-8")
# The regex pattern used to match gfx arches
assert "gfx\\d+" in source or r"gfx\d+" in source, \
f"gfx arch regex not found in {path.name}"
if __name__ == "__main__":
pytest.main([__file__, "-v"])