From 5453a0e5231ef41faad6aef91ab930c60c959247 Mon Sep 17 00:00:00 2001 From: hogeheer499-commits Date: Sun, 9 Aug 2026 04:01:43 +0200 Subject: [PATCH] fix: detect Strix Halo by AMD PCI id --- src/whichllm/constants.py | 2 ++ src/whichllm/data/gpu.py | 4 ++++ src/whichllm/hardware/amd.py | 31 ++++++++++++++++++++---- tests/test_amd_detection.py | 46 ++++++++++++++++++++++++++++++++++++ 4 files changed, 78 insertions(+), 5 deletions(-) diff --git a/src/whichllm/constants.py b/src/whichllm/constants.py index a67354d..75260bd 100644 --- a/src/whichllm/constants.py +++ b/src/whichllm/constants.py @@ -12,6 +12,7 @@ ) from whichllm.data.gpu import ( _GiB, + AMD_PCI_DEVICE_NAMES, AMD_SHARED_MEMORY_APU_MARKERS, CURATED_GPU_SPECS, CuratedGPUSpec, @@ -34,6 +35,7 @@ __all__ = [ "_GiB", + "AMD_PCI_DEVICE_NAMES", "AMD_SHARED_MEMORY_APU_MARKERS", "CURATED_GPU_SPECS", "CuratedGPUSpec", diff --git a/src/whichllm/data/gpu.py b/src/whichllm/data/gpu.py index ea87181..c090f28 100644 --- a/src/whichllm/data/gpu.py +++ b/src/whichllm/data/gpu.py @@ -37,6 +37,10 @@ class CuratedGPUSpec(NamedTuple): "RYZEN AI MAX", ) +AMD_PCI_DEVICE_NAMES: dict[str, str] = { + "0x1586": "Strix Halo [Radeon 8060S]", +} + # GPU memory bandwidth in GB/s (theoretical peak) # Key: substring matched against GPU name (case-insensitive) GPU_BANDWIDTH: dict[str, float] = { diff --git a/src/whichllm/hardware/amd.py b/src/whichllm/hardware/amd.py index 5122920..8000d62 100644 --- a/src/whichllm/hardware/amd.py +++ b/src/whichllm/hardware/amd.py @@ -4,11 +4,12 @@ import json import logging +import re import shlex import subprocess from pathlib import Path -from whichllm.constants import AMD_SHARED_MEMORY_APU_MARKERS, _GiB +from whichllm.constants import AMD_PCI_DEVICE_NAMES, AMD_SHARED_MEMORY_APU_MARKERS, _GiB from whichllm.hardware.gpu_db import _static_bandwidth, resolve_detected_bandwidth from whichllm.hardware.types import GPUInfo @@ -70,10 +71,19 @@ def _vendor_is_amd(vendor: str) -> bool: return any(marker in vendor_lower for marker in _AMD_VENDOR_MARKERS) +def _map_generic_pci_name(name: str) -> str: + if not name.strip().lower().startswith("device"): + return name + match = re.search(r"\[([0-9a-fA-F]{4})\]\s*$", name) + if not match: + return name + return AMD_PCI_DEVICE_NAMES.get(f"0x{match.group(1).lower()}", name) + + def _detect_from_lspci() -> list[str]: try: result = subprocess.run( - ["lspci", "-mm"], + ["lspci", "-mm", "-nn"], capture_output=True, text=True, timeout=5, @@ -100,11 +110,12 @@ def _detect_from_lspci() -> list[str]: if len(tokens) < 4: continue device_class, vendor, device = tokens[1], tokens[2], tokens[3] - if device_class.lower() not in _DISPLAY_CLASSES: + normalized_class = device_class.split(" [", 1)[0].lower() + if normalized_class not in _DISPLAY_CLASSES: continue if not _vendor_is_amd(vendor): continue - name = device.strip() or "AMD Graphics" + name = _map_generic_pci_name(device.strip()) or "AMD Graphics" if name not in seen: names.append(name) seen.add(name) @@ -140,9 +151,19 @@ def _detect_from_sysfs(drm_path: Path = Path("/sys/class/drm")) -> list[GPUInfo] continue name = "AMD Graphics" + known_device = False + try: + device_id = (device / "device").read_text().strip().lower() + mapped_name = AMD_PCI_DEVICE_NAMES.get(device_id) + if mapped_name: + name = mapped_name + known_device = True + except OSError: + pass + try: product_name = (device / "product_name").read_text().strip() - if product_name: + if product_name and not known_device: name = product_name except OSError: pass diff --git a/tests/test_amd_detection.py b/tests/test_amd_detection.py index f5d528c..bb1a758 100644 --- a/tests/test_amd_detection.py +++ b/tests/test_amd_detection.py @@ -72,6 +72,52 @@ def fake_run(args, **kwargs): assert gpus[0].memory_bandwidth_gbps == 256.0 +def test_detect_strix_halo_generic_pci_id_from_lspci(monkeypatch): + aperture = 512 * 1024**2 + output = ( + 'c6:00.0 "Display controller [0380]" "Advanced Micro Devices, Inc. ' + '[AMD/ATI] [1002]" "Device [1586]" -rc1 -p00 ' + '"Advanced Micro Devices, Inc. [AMD/ATI] [1002]" "Device [0124]"\n' + ) + + def fake_run(args, **kwargs): + if args[0] == "rocm-smi": + raise FileNotFoundError + return subprocess.CompletedProcess(args, 0, stdout=output, stderr="") + + monkeypatch.setattr(amd.subprocess, "run", fake_run) + monkeypatch.setattr(amd, "_detect_from_sysfs", lambda: []) + monkeypatch.setattr(amd, "_read_sysfs_amd_vram", lambda: [aperture]) + + gpus = amd.detect_amd_gpus() + + assert len(gpus) == 1 + assert gpus[0].name == "Strix Halo [Radeon 8060S]" + assert gpus[0].shared_memory is True + assert gpus[0].vram_bytes == 0 + assert gpus[0].memory_bandwidth_gbps == 256.0 + + +def test_detect_strix_halo_pci_id_from_sysfs(monkeypatch, tmp_path): + card = tmp_path / "card0" / "device" + card.mkdir(parents=True) + (card / "vendor").write_text("0x1002\n") + (card / "device").write_text("0x1586\n") + (card / "mem_info_vram_total").write_text(str(512 * 1024**2)) + + monkeypatch.setattr(amd, "_detect_from_lspci", lambda: []) + original_sysfs = amd._detect_from_sysfs + monkeypatch.setattr(amd, "_detect_from_sysfs", lambda: original_sysfs(tmp_path)) + + gpus = amd._detect_amd_gpus_fallback() + + assert len(gpus) == 1 + assert gpus[0].name == "Strix Halo [Radeon 8060S]" + assert gpus[0].shared_memory is True + assert gpus[0].vram_bytes == 0 + assert gpus[0].memory_bandwidth_gbps == 256.0 + + def test_detect_amd_gpu_ignores_intel_only_lspci(monkeypatch): """Regression: an Intel VGA row must not be reported as AMD just because 'Intel Corporation' contains the substring 'ati'."""