303 lines
10 KiB
Python
303 lines
10 KiB
Python
"""Tests for AMD GPU detection fallbacks."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import subprocess
|
|
from io import StringIO
|
|
|
|
from rich.console import Console
|
|
|
|
from whichllm.hardware import amd
|
|
from whichllm.hardware.types import GPUInfo, HardwareInfo
|
|
|
|
|
|
def test_detect_amd_gpu_from_lspci_when_rocm_smi_missing(monkeypatch):
|
|
output = (
|
|
'c1:00.0 "VGA compatible controller" "Advanced Micro Devices, Inc. '
|
|
'[AMD/ATI]" "Strix Halo [Radeon 8060S]" -r00 "Framework" "Device 0001"\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)
|
|
|
|
gpus = amd.detect_amd_gpus()
|
|
|
|
assert len(gpus) == 1
|
|
assert gpus[0].vendor == "amd"
|
|
assert gpus[0].vram_bytes == 0
|
|
assert gpus[0].shared_memory is True
|
|
assert gpus[0].memory_bandwidth_gbps == 256.0
|
|
assert "Radeon 8060S" in gpus[0].name
|
|
|
|
|
|
def test_detect_strix_halo_rocm_smi_does_not_treat_aperture_as_vram(monkeypatch):
|
|
def fake_run(args, **kwargs):
|
|
if args[:2] == ["rocm-smi", "--showproductname"]:
|
|
return subprocess.CompletedProcess(
|
|
args,
|
|
0,
|
|
stdout='{"card0": {"Card SKU": "STRXLGEN"}}',
|
|
stderr="",
|
|
)
|
|
if args[:3] == ["rocm-smi", "--showmeminfo", "vram"]:
|
|
return subprocess.CompletedProcess(
|
|
args,
|
|
0,
|
|
stdout='{"card0": {"VRAM Total Memory (B)": "536870912"}}',
|
|
stderr="",
|
|
)
|
|
if args[:2] == ["rocm-smi", "--showdriverversion"]:
|
|
return subprocess.CompletedProcess(
|
|
args,
|
|
0,
|
|
stdout='{"card0": {"Driver version": "7.0.3"}}',
|
|
stderr="",
|
|
)
|
|
raise AssertionError(args)
|
|
|
|
monkeypatch.setattr(amd.subprocess, "run", fake_run)
|
|
|
|
gpus = amd.detect_amd_gpus()
|
|
|
|
assert len(gpus) == 1
|
|
assert gpus[0].name == "STRXLGEN"
|
|
assert gpus[0].vendor == "amd"
|
|
assert gpus[0].shared_memory is True
|
|
assert gpus[0].vram_bytes == 0
|
|
assert gpus[0].rocm_version == "7.0.3"
|
|
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'."""
|
|
output = (
|
|
'00:02.0 "VGA compatible controller" "Intel Corporation" '
|
|
'"Alder Lake-P GT1 [UHD Graphics]" -r0c -p00 '
|
|
'"IP3 Tech (HK) Limited" "Device 8027"\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)
|
|
# Isolate the lspci path: don't let a real sysfs probe leak in.
|
|
monkeypatch.setattr(amd, "_detect_from_sysfs", lambda: [])
|
|
|
|
assert amd.detect_amd_gpus() == []
|
|
|
|
|
|
def test_detect_amd_gpu_from_sysfs_when_lspci_missing(monkeypatch, tmp_path):
|
|
card = tmp_path / "card0" / "device"
|
|
card.mkdir(parents=True)
|
|
(card / "vendor").write_text("0x1002\n")
|
|
(card / "product_name").write_text("AMD Radeon RX 9060 XT\n")
|
|
(card / "mem_info_vram_total").write_text(str(16 * 1024**3))
|
|
|
|
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].vendor == "amd"
|
|
assert gpus[0].name == "AMD Radeon RX 9060 XT"
|
|
assert gpus[0].vram_bytes == 16 * 1024**3
|
|
assert gpus[0].shared_memory is False
|
|
|
|
|
|
def test_display_amd_shared_memory_without_zero_kb(monkeypatch):
|
|
from whichllm.output import _console as console_mod
|
|
from whichllm.output import display as display_mod
|
|
|
|
buf = StringIO()
|
|
monkeypatch.setattr(console_mod, "console", Console(file=buf, force_terminal=False))
|
|
|
|
display_mod.display_hardware(
|
|
HardwareInfo(
|
|
gpus=[
|
|
GPUInfo(
|
|
name="Strix Halo [Radeon 8060S]",
|
|
vendor="amd",
|
|
vram_bytes=0,
|
|
shared_memory=True,
|
|
)
|
|
],
|
|
cpu_name="CPU",
|
|
cpu_cores=16,
|
|
ram_bytes=128 * 1024**3,
|
|
disk_free_bytes=100 * 1024**3,
|
|
os="linux",
|
|
)
|
|
)
|
|
|
|
output = buf.getvalue()
|
|
assert "shared memory" in output
|
|
assert "256 GB/s" not in output
|
|
assert "0 KB" not in output
|
|
|
|
|
|
# ---------- Issue #61: RX 6750 XT detection ----------
|
|
|
|
|
|
def test_sysfs_generic_name_enriched_by_lspci(monkeypatch, tmp_path):
|
|
"""When sysfs gives 'AMD Graphics' and lspci gives a descriptive name,
|
|
the fallback should use the lspci name with sysfs VRAM."""
|
|
_GiB = 1024**3
|
|
|
|
# sysfs: generic name but has VRAM
|
|
card = tmp_path / "card0" / "device"
|
|
card.mkdir(parents=True)
|
|
(card / "vendor").write_text("0x1002\n")
|
|
(card / "mem_info_vram_total").write_text(str(12 * _GiB))
|
|
# no product_name → falls back to "AMD Graphics"
|
|
|
|
lspci_name = "Navi 22 [Radeon RX 6700/6700 XT/6750 XT / 6800M/6850M XT]"
|
|
original_sysfs = amd._detect_from_sysfs
|
|
monkeypatch.setattr(amd, "_detect_from_sysfs", lambda: original_sysfs(tmp_path))
|
|
monkeypatch.setattr(amd, "_detect_from_lspci", lambda: [lspci_name])
|
|
|
|
gpus = amd._detect_amd_gpus_fallback()
|
|
|
|
assert len(gpus) == 1
|
|
assert gpus[0].name == lspci_name
|
|
assert gpus[0].vram_bytes == 12 * _GiB
|
|
assert gpus[0].shared_memory is False
|
|
|
|
|
|
def test_sysfs_product_name_preferred_over_lspci(monkeypatch, tmp_path):
|
|
"""When sysfs gives a real product name, it should be used even if
|
|
lspci is also available."""
|
|
_GiB = 1024**3
|
|
|
|
card = tmp_path / "card0" / "device"
|
|
card.mkdir(parents=True)
|
|
(card / "vendor").write_text("0x1002\n")
|
|
(card / "product_name").write_text("AMD Radeon RX 6750 XT\n")
|
|
(card / "mem_info_vram_total").write_text(str(12 * _GiB))
|
|
|
|
original_sysfs = amd._detect_from_sysfs
|
|
monkeypatch.setattr(amd, "_detect_from_sysfs", lambda: original_sysfs(tmp_path))
|
|
monkeypatch.setattr(
|
|
amd,
|
|
"_detect_from_lspci",
|
|
lambda: ["Navi 22 [Radeon RX 6700/6700 XT/6750 XT / 6800M/6850M XT]"],
|
|
)
|
|
|
|
gpus = amd._detect_amd_gpus_fallback()
|
|
|
|
assert len(gpus) == 1
|
|
assert gpus[0].name == "AMD Radeon RX 6750 XT"
|
|
assert gpus[0].vram_bytes == 12 * _GiB
|
|
assert gpus[0].memory_bandwidth_gbps == 432.0
|
|
|
|
|
|
def test_lspci_enriched_with_sysfs_vram_when_sysfs_detection_fails(
|
|
monkeypatch, tmp_path
|
|
):
|
|
"""When _detect_from_sysfs returns nothing but _read_sysfs_amd_vram
|
|
succeeds, lspci names should still get VRAM data."""
|
|
_GiB = 1024**3
|
|
|
|
# _detect_from_sysfs returns nothing (e.g. product_name absent AND
|
|
# the card dir structure confuses the glob), but individual VRAM reads
|
|
# via _read_sysfs_amd_vram still work.
|
|
monkeypatch.setattr(amd, "_detect_from_sysfs", lambda: [])
|
|
monkeypatch.setattr(
|
|
amd,
|
|
"_detect_from_lspci",
|
|
lambda: ["Navi 22 [Radeon RX 6700/6700 XT/6750 XT / 6800M/6850M XT]"],
|
|
)
|
|
monkeypatch.setattr(amd, "_read_sysfs_amd_vram", lambda: [12 * _GiB])
|
|
|
|
gpus = amd._detect_amd_gpus_fallback()
|
|
|
|
assert len(gpus) == 1
|
|
assert gpus[0].vram_bytes == 12 * _GiB
|
|
assert gpus[0].shared_memory is False
|
|
|
|
|
|
def test_lookup_bandwidth_compound_lspci_name():
|
|
"""The bandwidth lookup should resolve compound lspci names by
|
|
splitting on '/' and re-applying the 'RX ' prefix."""
|
|
# Direct substring match works for clean names
|
|
assert amd._lookup_bandwidth("AMD Radeon RX 6750 XT") == 432.0
|
|
assert amd._lookup_bandwidth("AMD Radeon RX 6700 XT") == 384.0
|
|
|
|
# Compound lspci name — first matching segment wins
|
|
compound = "Navi 22 [Radeon RX 6700/6700 XT/6750 XT / 6800M/6850M XT]"
|
|
bw = amd._lookup_bandwidth(compound)
|
|
assert bw is not None
|
|
assert bw > 0
|
|
|
|
|
|
def test_display_amd_dgpu_does_not_say_shared_memory(monkeypatch):
|
|
"""A discrete AMD GPU with VRAM must NOT display 'shared memory'."""
|
|
from whichllm.output import _console as console_mod
|
|
from whichllm.output import display as display_mod
|
|
|
|
buf = StringIO()
|
|
monkeypatch.setattr(console_mod, "console", Console(file=buf, force_terminal=False))
|
|
|
|
display_mod.display_hardware(
|
|
HardwareInfo(
|
|
gpus=[
|
|
GPUInfo(
|
|
name="AMD Radeon RX 6750 XT",
|
|
vendor="amd",
|
|
vram_bytes=12 * 1024**3,
|
|
memory_bandwidth_gbps=432.0,
|
|
shared_memory=False,
|
|
)
|
|
],
|
|
cpu_name="AMD Ryzen 9 5900X",
|
|
cpu_cores=12,
|
|
ram_bytes=128 * 1024**3,
|
|
disk_free_bytes=500 * 1024**3,
|
|
os="linux",
|
|
)
|
|
)
|
|
|
|
output = buf.getvalue()
|
|
assert "12.0 GB" in output
|
|
assert "432 GB/s" in output
|
|
assert "shared memory" not in output
|
|
|
|
|
|
def test_display_amd_dgpu_zero_vram_does_not_say_shared_memory(monkeypatch):
|
|
"""An AMD dGPU with undetected VRAM should NOT be labelled
|
|
'shared memory' — that would be a false positive."""
|
|
from whichllm.output import _console as console_mod
|
|
from whichllm.output import display as display_mod
|
|
|
|
buf = StringIO()
|
|
monkeypatch.setattr(console_mod, "console", Console(file=buf, force_terminal=False))
|
|
|
|
display_mod.display_hardware(
|
|
HardwareInfo(
|
|
gpus=[
|
|
GPUInfo(
|
|
name="Navi 22 [Radeon RX 6750 XT]",
|
|
vendor="amd",
|
|
vram_bytes=0,
|
|
shared_memory=False,
|
|
)
|
|
],
|
|
cpu_name="CPU",
|
|
cpu_cores=8,
|
|
ram_bytes=32 * 1024**3,
|
|
disk_free_bytes=100 * 1024**3,
|
|
os="linux",
|
|
)
|
|
)
|
|
|
|
output = buf.getvalue()
|
|
assert "shared memory" not in output
|