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:
parent
c267895538
commit
978ae4745b
3 changed files with 714 additions and 8 deletions
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue