Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/whichllm/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
)
from whichllm.data.gpu import (
_GiB,
AMD_PCI_DEVICE_NAMES,
AMD_SHARED_MEMORY_APU_MARKERS,
CURATED_GPU_SPECS,
CuratedGPUSpec,
Expand All @@ -34,6 +35,7 @@

__all__ = [
"_GiB",
"AMD_PCI_DEVICE_NAMES",
"AMD_SHARED_MEMORY_APU_MARKERS",
"CURATED_GPU_SPECS",
"CuratedGPUSpec",
Expand Down
4 changes: 4 additions & 0 deletions src/whichllm/data/gpu.py
Original file line number Diff line number Diff line change
Expand Up @@ -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] = {
Expand Down
31 changes: 26 additions & 5 deletions src/whichllm/hardware/amd.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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,
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down
46 changes: 46 additions & 0 deletions tests/test_amd_detection.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'."""
Expand Down