fix(install): infer Strix gfx when ROCm runtime is absent (#7305)

* fix(install): infer Strix gfx when ROCm runtime is absent

When /dev/kfd and rocminfo are missing on Linux (e.g. Arch/CachyOS Strix
Halo), route to AMD per-arch wheels via cpuinfo/lspci inference instead
of CPU-only PyTorch. Mirrors install.ps1 Windows behavior and fixes
studio update via install_python_stack.py (unslothai#7301).

* Map Radeon 8065S to gfx1151 in the Linux gfx inference (Codex P2)

install.sh _infer_amd_gfx_arch_from_gpu_name missed 8065S, so a Strix Halo host that only exposes 'AMD Radeon 8065S' via lspci (no Ryzen AI Max branding in /proc/cpuinfo) was left on CPU torch. setup.sh and setup.ps1 already list 8065S -> gfx1151. Added it, and widened the cpuinfo regexes (install.sh and install_python_stack.py) from Radeon 80[0-9]0S to 80[0-9][05]S to match the 80X5S naming, consistent with the display-side check already in install.sh. Tests cover the 8065S name and the cpuinfo-only case.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Gate the Linux gfx inference out of WSL without the ROCDXG runtime for PR #7305

On WSL /proc/cpuinfo and lspci still see the host APU, so a standalone
'unsloth studio update' could infer gfx1151 and install per-arch ROCm wheels
into a WSL env whose ROCDXG bridge (librocdxg) was never bootstrapped, i.e. one
that cannot expose the GPU. Skip the cpuinfo/lspci inference on WSL unless
librocdxg is present; an explicit UNSLOTH_ROCM_GFX_ARCH override still wins.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Address Codex review on PR #7305 (WSL runtime gate, Linux mirror, arch guard)

- install.sh _infer_linux_amd_gfx_arch: skip the cpuinfo/lspci inference on WSL
  unless librocdxg is present (the ROCDXG bridge), mirroring the Python fix, so a
  WSL box whose ROCm bootstrap was skipped keeps the CPU fallback instead of
  installing AMD wheels that cannot reach the GPU. The explicit UNSLOTH_ROCM_GFX_ARCH
  override still returns first, so it stays authoritative.
- install.sh: guard the inferred-gfx reroute on x86_64|amd64. ROCm torch wheels are
  not published for arm64, so an inferred/overridden gfx no longer pushes an arm64
  host to the AMD arch index (get_torch_index_url returns CPU there).
- install_python_stack.py _amd_arch_index_url: honour UNSLOTH_AMD_ROCM_MIRROR on
  Linux (the same var install.sh uses) instead of the Windows mirror var, so a
  mirrored/air-gapped Linux 'unsloth studio update' reaches the index install.sh
  chose. Windows still delegates unchanged; both default to repo.amd.com.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Scan all AMD display controllers in the lspci fallback for PR #7305 (Codex P2)

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fix(studio): keep inferred AMD wheels from being overwritten

After a successful inferred-gfx install, skip the generic pytorch.org
ROCm reinstall so readable ROCm userland without /dev/kfd cannot undo
the per-arch repair (Codex P1 on #7305). Also merge latest main.

* Only take the inferred-gfx install when the runtime sees no GPU for PR #7305 (Codex P1)

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Isolate three updater tests from the host cpuinfo for PR #7305 (Strix dev box leak)

* Gate the reroute on invisible ROCm and forward the inferred gfx to setup.sh for PR #7305 (Codex P2s)

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Require AMD PCI display evidence for cpuinfo inference; honor gfx override with visible ROCm for PR #7305 (Codex P2s)

---------

Co-authored-by: Daniel Han <danielhanchen@gmail.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: LeoBorcherding <borchborchmail@gmail.com>
This commit is contained in:
Souravrajvi0 2026-07-23 06:46:45 +05:30 committed by GitHub
commit 978ae4745b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 714 additions and 8 deletions

View file

@ -769,6 +769,142 @@ def _gfx_arch_from_gpu_name(name: str) -> "str | None":
return None
def _linux_amd_gfx_from_cpuinfo() -> "str | None":
"""Infer gfx arch from /proc/cpuinfo on integrated AMD APUs (Strix Halo/Point)."""
try:
text = Path("/proc/cpuinfo").read_text(encoding = "utf-8", errors = "replace")
except OSError:
return None
if re.search(r"Ryzen AI Max|Radeon 80[0-9][05]S|Strix Halo", text, re.IGNORECASE):
return "gfx1151"
if re.search(
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",
text,
re.IGNORECASE,
):
return "gfx1150"
return None
def _linux_amd_gfx_from_lspci() -> "str | None":
"""First AMD display-class lspci line mapping to a known gfx arch. A non-AMD
controller can enumerate first (Intel/ASPEED before an AMD dGPU), so scan
them all. The vendor guard is case-SENSITIVE: a -i "ATI" would match
"CorporATIon" on every Intel/NVIDIA line. Whole-line matching also survives
the 0000: PCI domain prefix."""
lspci = shutil.which("lspci")
if not lspci:
return None
try:
result = subprocess.run(
[lspci, "-nn"],
stdout = subprocess.PIPE,
stderr = subprocess.DEVNULL,
text = True,
timeout = 10,
)
except Exception:
return None
if result.returncode != 0:
return None
for line in result.stdout.splitlines():
if not re.search(r"VGA compatible controller|3D controller|Display controller", line, re.I):
continue
if not re.search(r"AMD|ATI", line):
continue
arch = _gfx_arch_from_gpu_name(line)
if arch:
return arch
return None
def _is_wsl() -> bool:
"""True on WSL, where the AMD GPU is reached via /dev/dxg (not /dev/kfd)."""
if os.path.exists("/dev/dxg"):
return True
try:
with open("/proc/version", encoding = "utf-8", errors = "replace") as fh:
return "microsoft" in fh.read().lower()
except OSError:
return False
def _wsl_rocm_runtime_present() -> bool:
"""librocdxg (the WSL ROCDXG bridge that lets HIP reach the GPU over /dev/dxg)
under a ROCm lib dir. Its absence marks a WSL box whose ROCm was never set up."""
dirs = ["/opt/rocm/lib", "/opt/rocm/lib64"]
dirs += glob.glob("/opt/rocm-*/lib") + glob.glob("/opt/rocm-*/lib64")
return any(
os.path.exists(os.path.join(d, so))
for d in dirs
for so in ("librocdxg.so", "librocdxg.so.1")
)
def _linux_amd_display_device_present() -> bool:
"""Any AMD (vendor 0x1002) PCI display-class (0x03*) device in sysfs.
/proc/cpuinfo leaks the HOST CPU model into VMs/containers that received no
AMD GPU, so the CPU-model text alone is not GPU evidence; this is the
device-level check (mirrors install.sh _amd_gpu_present_via_pci)."""
try:
for dev in Path("/sys/bus/pci/devices").iterdir():
try:
if (dev / "vendor").read_text().strip() != "0x1002":
continue
if (dev / "class").read_text().strip().startswith("0x03"):
return True
except OSError:
continue
except OSError:
pass
return False
def _infer_linux_amd_gfx_arch() -> "str | None":
"""Infer gfx when ROCm runtime is absent but the host is a known AMD arch (unslothai#7301)."""
override = (os.environ.get("UNSLOTH_ROCM_GFX_ARCH") or "").strip().lower()
if override:
return override
if _is_wsl():
# cpuinfo/lspci see the host APU even on a WSL box whose ROCDXG runtime
# was never bootstrapped; inferring there would install per-arch ROCm
# wheels into an env that still can't expose the GPU. Skip unless that
# runtime is present -- WSL enumerates no PCI display device, so
# /dev/dxg + librocdxg IS the GPU evidence there.
if not _wsl_rocm_runtime_present():
return None
elif not _linux_amd_display_device_present():
# Native Linux: a VM/container on a Strix host still shows the host CPU
# model in /proc/cpuinfo while receiving no AMD GPU, so require an AMD
# display device before trusting the CPU-model inference. The lspci
# fallback reads the same PCI space and would find nothing here either.
return None
cpu_gfx = _linux_amd_gfx_from_cpuinfo()
if cpu_gfx:
return cpu_gfx
return _linux_amd_gfx_from_lspci()
def _amd_arch_index_url(gfx_arch: str | None) -> str | None:
"""Return the AMD per-arch pip index URL for a gfx arch (Linux + Windows).
Windows honors UNSLOTH_ROCM_WINDOWS_MIRROR (via _windows_rocm_index_url);
Linux honors UNSLOTH_AMD_ROCM_MIRROR -- the same var install.sh uses -- so a
mirrored/air-gapped Linux repair reaches the index install.sh chose rather
than falling back to repo.amd.com. Both default to repo.amd.com when unset.
"""
if IS_WINDOWS:
return _windows_rocm_index_url(gfx_arch)
arch_family = _GFX_TO_AMD_INDEX_ARCH.get(gfx_arch or "")
if arch_family is None:
return None
base = (os.environ.get("UNSLOTH_AMD_ROCM_MIRROR") or "https://repo.amd.com/rocm/whl").rstrip(
"/"
)
return f"{base}/{arch_family}/"
def _windows_rocm_index_url(gfx_arch: str | None) -> str | None:
"""Return the AMD pip index URL for the given GPU arch, or None if unsupported."""
arch_family = _GFX_TO_AMD_INDEX_ARCH.get(gfx_arch or "")
@ -1647,22 +1783,24 @@ def _ensure_rocm_torch() -> None:
# An explicit ROCm pin commits to ROCm wheels regardless of the visible GPU (headless / CI).
# Mirror _ensure_cuda_torch: skip the NVIDIA/no-AMD/unreadable gates.
_rocm_pin = _explicit_rocm_torch_index_url()
_inferred_linux_gfx = (
_infer_linux_amd_gfx_arch() if (_rocm_pin is None and not IS_WINDOWS) else None
)
if _rocm_pin is None:
# NVIDIA takes precedence on mixed hosts (only if a GPU is usable).
if _has_usable_nvidia_gpu():
return
# _has_rocm_gpu() (rocminfo / amd-smi rows) is the authoritative AMD-host signal;
# the old /opt/rocm-or-hipcc gate broke runtime-only ROCm installs.
if not _has_rocm_gpu():
if not _has_rocm_gpu() and not _inferred_linux_gfx:
return # no AMD GPU visible
ver = _detect_rocm_version()
if ver is None:
if _rocm_pin is None:
if _rocm_pin is None and not _inferred_linux_gfx:
print(" ROCm detected but version unreadable -- skipping torch reinstall")
return
# Explicit pin: the pinned leaf drives the install, so an unreadable host version
# is fine (sentinel keeps ver comparisons defined).
# Explicit pin or inferred gfx: the index drives the install.
ver = (0, 0)
# Probe whether torch links against HIP, capturing the installed ROCm tag for pin-mismatch
@ -1712,6 +1850,44 @@ def _ensure_rocm_torch() -> None:
rocm_torch_ready = has_hip_torch and not _rocm_pin_mismatch
# Inferred-gfx path: ROCm runtime missing but install.sh would route to AMD wheels.
# Gated on the runtime NOT enumerating a GPU: when it can, the runtime-visible
# arch (Strix override / generic below) decides, not cpuinfo -- a mixed Strix
# APU + dGPU box with HIP_VISIBLE_DEVICES on the dGPU must not get APU wheels.
# An explicit UNSLOTH_ROCM_GFX_ARCH is exempt from that runtime gate (mirrors
# install.sh): a visible GPU with an unreadable/unsupported ROCm version must
# not silently discard the user's named arch and leave CPU torch in place.
_gfx_override_env = (os.environ.get("UNSLOTH_ROCM_GFX_ARCH") or "").strip().lower()
if (
_inferred_linux_gfx
and not has_hip_torch
and _rocm_pin is None
and (_gfx_override_env or not _has_rocm_gpu())
):
index_url = _amd_arch_index_url(_inferred_linux_gfx)
if index_url is not None:
_torch_pkg, _vision_pkg, _audio_pkg = _WINDOWS_ROCM_TORCH_PKG_SPECS.get(
_inferred_linux_gfx, ("torch", "torchvision", "torchaudio")
)
print(
f"\n {_inferred_linux_gfx} inferred (ROCm runtime not visible) -- "
f"installing torch from {_strip_index_url_credentials(index_url)}\n"
f" AMD wheels bundle their own ROCm runtime; install the kernel stack "
f"for native GPU compute.\n"
)
pip_install(
f"ROCm torch (inferred {_inferred_linux_gfx})",
"--force-reinstall",
"--no-cache-dir",
_torch_pkg,
_vision_pkg,
_audio_pkg,
"--index-url",
index_url,
constrain = False,
)
rocm_torch_ready = True
# Strix Halo / Point (gfx1151 / gfx1150) need torch from AMD's per-gfx index
# (2.11+rocm7.13); any generic pytorch.org rocm index lacks the fixes (ROCm 7.1
# segfaults in _grouped_mm). See _strix_needs_amd_arch_index for the floor gate.
@ -1776,8 +1952,11 @@ def _ensure_rocm_torch() -> None:
constrain = False,
)
rocm_torch_ready = True
elif not has_hip_torch or _rocm_pin_mismatch:
elif not rocm_torch_ready:
# Reinstall when torch is not ROCm yet, OR a ROCm build's family differs from a pin.
# Gate on rocm_torch_ready (not has_hip_torch alone) so a successful inferred-gfx
# install above is not overwritten by the generic pytorch.org/rocmX.Y path -- that
# would undo the fresh-ROCm/no-/dev/kfd repair this path exists for (Codex P1 #7305).
# Honour a ROCm pin verbatim; else pick the newest wheel tag <= host.
_override_idx = _explicit_rocm_torch_index_url()
if _override_idx is not None: