CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
632 lines
20 KiB
Python
632 lines
20 KiB
Python
# Copyright (c) Advanced Micro Devices, Inc.
|
|
# SPDX-License-Identifier: MIT
|
|
|
|
import re
|
|
import subprocess
|
|
import tempfile
|
|
from subprocess import CompletedProcess
|
|
from unittest.mock import patch
|
|
|
|
import common
|
|
import pytest
|
|
|
|
import utils.specs as specs
|
|
from utils.file_io import is_single_panel_config
|
|
from utils.specs import (
|
|
MachineSpecs,
|
|
MachineSpecsCDNA,
|
|
MachineSpecsRDNA35,
|
|
generate_machine_specs,
|
|
spec_family_for_arch,
|
|
)
|
|
from utils.utils_common import canonical_config_arch
|
|
|
|
# NOTE: Only testing gfx942 for now.
|
|
GFX942_CHIP_IDS_TO_NUM_XCDS = {
|
|
"29856": {"spx": 6, "tpx": 2},
|
|
"29876": {"spx": 6, "tpx": 2},
|
|
"29857": {"spx": 8, "dpx": 4, "qpx": 2, "cpx": 1},
|
|
"29877": {"spx": 8, "dpx": 4, "qpx": 2, "cpx": 1},
|
|
"29858": {"spx": 4, "dpx": 2, "cpx": 1},
|
|
"29878": {"spx": 4, "dpx": 2, "cpx": 1},
|
|
"29861": {"spx": 8, "dpx": 4, "qpx": 2, "cpx": 1},
|
|
"29881": {"spx": 8, "dpx": 4, "qpx": 2, "cpx": 1},
|
|
"29864": {"spx": 4, "dpx": 2, "cpx": 1},
|
|
"29884": {"spx": 4, "dpx": 2, "cpx": 1},
|
|
"29865": {"spx": 8, "dpx": 4, "qpx": 2, "cpx": 1},
|
|
"29885": {"spx": 8, "dpx": 4, "qpx": 2, "cpx": 1},
|
|
}
|
|
|
|
|
|
def parse_table_dict(output: str) -> dict:
|
|
"""
|
|
Parse an ASCII table into a dict mapping Spec -> Value.
|
|
"""
|
|
lines = [line for line in output.splitlines() if line.startswith("|")]
|
|
# locate header row (the one containing 'Spec' and 'Value')
|
|
header_idx = next(
|
|
(i for i, ln in enumerate(lines) if "Spec" in ln and "Value" in ln), None
|
|
)
|
|
if header_idx is None:
|
|
raise ValueError("Header row with Spec and Value not found")
|
|
|
|
header_cells = [c.strip() for c in lines[header_idx].strip("|").split("|")]
|
|
|
|
spec_i = header_cells.index("Spec")
|
|
value_i = header_cells.index("Value")
|
|
|
|
result = {}
|
|
for ln in lines[header_idx + 1 :]:
|
|
# Skip separator lines
|
|
if ln.startswith("+"):
|
|
continue
|
|
cells = [c.strip() for c in ln.strip("|").split("|")]
|
|
if len(cells) <= max(spec_i, value_i):
|
|
continue
|
|
spec = cells[spec_i]
|
|
value = cells[value_i]
|
|
if spec:
|
|
result[spec] = value
|
|
return result
|
|
|
|
|
|
def get_num_xcds():
|
|
num_xcds = None
|
|
|
|
## 1) Parse arch details from rocminfo
|
|
rocminfo = str(
|
|
# decode with utf-8 to account for rocm-smi changes in latest rocm
|
|
subprocess.run(
|
|
["rocminfo"], stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
|
).stdout.decode("utf-8")
|
|
)
|
|
rocminfo = rocminfo.split("\n")
|
|
|
|
chip_id = re.compile(r"^\s*Chip ID:\s+ ([a-zA-Z0-9]+)\s*", re.MULTILINE)
|
|
ids = list(filter(chip_id.match, rocminfo))
|
|
for id in ids:
|
|
chip_id = re.match(r"^[^()]+", id.split()[2]).group(0)
|
|
|
|
if str(chip_id) in GFX942_CHIP_IDS_TO_NUM_XCDS.keys():
|
|
num_xcds = GFX942_CHIP_IDS_TO_NUM_XCDS[str(chip_id)]
|
|
|
|
if num_xcds is None:
|
|
return
|
|
|
|
return num_xcds
|
|
|
|
|
|
@pytest.mark.num_xcds_spec_class
|
|
def test_num_xcds_spec_class(monkeypatch):
|
|
# 1. Check if gfx942 soc
|
|
gpu_arch = common.gpu_soc()[0]
|
|
if gpu_arch is None or gpu_arch.lower() != "gfx942":
|
|
pytest.skip("Skipping num xcds test for non-gfx942 socs.")
|
|
|
|
num_xcds = get_num_xcds()
|
|
|
|
# 2. load machine specs
|
|
machine_spec = generate_machine_specs(None, None)
|
|
|
|
# 3. check results are expected
|
|
assert machine_spec.compute_partition is not None
|
|
assert int(machine_spec.num_xcd) == num_xcds.get(
|
|
machine_spec.compute_partition.lower(), -1
|
|
)
|
|
|
|
|
|
@pytest.mark.num_xcds_cli_output
|
|
def test_num_xcds_cli_output():
|
|
# 1. Check if gfx942 soc
|
|
gpu_arch = common.gpu_soc()[0]
|
|
if gpu_arch is None or gpu_arch.lower() != "gfx942":
|
|
pytest.skip("Skipping num xcds test for non-gfx942 socs.")
|
|
|
|
num_xcds = get_num_xcds()
|
|
|
|
# 2. Run rocprof-compute -s and grab rocprof-compute num_xcd
|
|
try:
|
|
proc = subprocess.run(
|
|
["src/rocprof-compute", "-s"],
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
text=True,
|
|
)
|
|
except Exception:
|
|
proc = subprocess.run(
|
|
["./rocprof-compute", "-s"],
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
text=True,
|
|
)
|
|
assert proc.returncode == 0, (
|
|
f"Non-zero exit ({proc.returncode}), stderr:\n{proc.stderr}"
|
|
)
|
|
|
|
# 3. strip ANSI, parse table
|
|
clean = common.strip_ansi(proc.stdout)
|
|
return_dict = parse_table_dict(clean)
|
|
|
|
# 4. check results are expected
|
|
assert "Compute Partition" in return_dict, (
|
|
"Spec 'Compute Partition' not found in table"
|
|
)
|
|
assert "Num XCDs" in return_dict, "Spec 'Num XCDs' not found in table"
|
|
|
|
compute_partition_actual = return_dict["Compute Partition"]
|
|
num_xcd_actual = return_dict["Num XCDs"]
|
|
|
|
assert compute_partition_actual is not None
|
|
assert int(num_xcd_actual) == num_xcds.get(compute_partition_actual.lower(), -1)
|
|
|
|
|
|
@pytest.mark.misc
|
|
def test_load_yaml_file_not_found():
|
|
"""Test _load_yaml with non-existent file - covers lines 104-105"""
|
|
from src.utils.mi_gpu_spec import MIGPUSpecs
|
|
|
|
with pytest.raises(FileNotFoundError):
|
|
MIGPUSpecs._load_yaml("non_existent_file.yaml")
|
|
|
|
|
|
@pytest.mark.misc
|
|
def test_load_yaml_invalid_yaml():
|
|
"""Test _load_yaml with corrupted YAML - covers lines 106-107"""
|
|
import yaml
|
|
|
|
from src.utils.mi_gpu_spec import MIGPUSpecs
|
|
|
|
# Create invalid YAML file
|
|
with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f:
|
|
f.write("invalid: yaml: content: [\nunclosed bracket")
|
|
temp_path = f.name
|
|
|
|
with pytest.raises(yaml.YAMLError):
|
|
MIGPUSpecs._load_yaml(str(temp_path))
|
|
|
|
|
|
@pytest.mark.misc
|
|
def test_load_yaml_generic_exception():
|
|
"""Test _load_yaml generic exception handling - covers lines 108-111"""
|
|
from src.utils.mi_gpu_spec import MIGPUSpecs
|
|
|
|
with patch("builtins.open", side_effect=PermissionError("Access denied")):
|
|
with pytest.raises(PermissionError, match="Access denied"):
|
|
MIGPUSpecs._load_yaml("some_file.yaml")
|
|
|
|
|
|
@pytest.mark.misc
|
|
@pytest.mark.parametrize(
|
|
"mock_kwargs",
|
|
[
|
|
{"side_effect": FileNotFoundError("missing")},
|
|
{
|
|
"return_value": CompletedProcess(
|
|
args=["rocminfo"], returncode=1, stdout="", stderr="boom"
|
|
)
|
|
},
|
|
],
|
|
ids=["missing_binary", "nonzero_exit"],
|
|
)
|
|
def test_run_fails_fast(mock_kwargs):
|
|
with patch.object(specs.subprocess, "run", **mock_kwargs), pytest.raises(
|
|
SystemExit
|
|
):
|
|
specs._run_command(["rocminfo"])
|
|
|
|
|
|
@pytest.mark.misc
|
|
def test_get_gpu_series_dict_uninitialized():
|
|
"""Test get_gpu_series_dict when dict not populated - covers lines 182-185"""
|
|
from src.utils.mi_gpu_spec import MIGPUSpecs
|
|
|
|
with patch.object(MIGPUSpecs, "_gpu_series_dict", {}):
|
|
assert MIGPUSpecs.get_gpu_series_dict() == {}
|
|
|
|
|
|
@pytest.mark.misc
|
|
def test_get_gpu_series_uninitialized():
|
|
"""Test get_gpu_series when dict not populated - covers lines 191-194"""
|
|
from src.utils.mi_gpu_spec import MIGPUSpecs
|
|
|
|
with patch.object(MIGPUSpecs, "_gpu_series_dict", {}):
|
|
assert MIGPUSpecs.get_gpu_series_dict() == {}
|
|
|
|
|
|
@pytest.mark.misc
|
|
def test_get_perfmon_config_uninitialized():
|
|
"""Test get_perfmon_config when dict not populated - covers lines 210-213"""
|
|
from src.utils.mi_gpu_spec import MIGPUSpecs
|
|
|
|
with patch.object(MIGPUSpecs, "_perfmon_config", {}):
|
|
with pytest.raises(SystemExit):
|
|
MIGPUSpecs.get_perfmon_config("gfx942")
|
|
|
|
|
|
@pytest.mark.misc
|
|
def test_get_gpu_model_uninitialized():
|
|
"""Test get_gpu_model when dict not populated - covers lines 223-226"""
|
|
from src.utils.mi_gpu_spec import MIGPUSpecs
|
|
|
|
with patch.object(MIGPUSpecs, "_gpu_model_dict", {}):
|
|
with pytest.raises(SystemExit):
|
|
MIGPUSpecs.get_gpu_model("gfx942", "29857")
|
|
|
|
|
|
@pytest.mark.misc
|
|
def test_get_gpu_model_invalid_chip_id():
|
|
"""Test get_gpu_model with invalid chip_id - covers lines 235-236"""
|
|
from src.utils.mi_gpu_spec import MIGPUSpecs
|
|
|
|
result = MIGPUSpecs.get_gpu_model("gfx942", "99999")
|
|
assert result is None
|
|
|
|
|
|
@pytest.mark.misc
|
|
def test_get_gpu_model_invalid_arch():
|
|
"""Test get_gpu_model with invalid architecture - covers lines 243-244"""
|
|
from src.utils.mi_gpu_spec import MIGPUSpecs
|
|
|
|
result = MIGPUSpecs.get_gpu_model("gfx999", "12345")
|
|
assert result is None
|
|
|
|
|
|
@pytest.mark.misc
|
|
def test_get_gpu_model_none_result():
|
|
"""Test get_gpu_model when result is None - covers lines 246-248"""
|
|
from src.utils.mi_gpu_spec import MIGPUSpecs
|
|
|
|
with patch.object(MIGPUSpecs, "_chip_id_dict", {999: None}):
|
|
result = MIGPUSpecs.get_gpu_model("gfx942", "999")
|
|
assert result is None
|
|
|
|
|
|
@pytest.mark.misc
|
|
def test_canonical_config_arch_maps_gfx115_variants_to_shared_dir():
|
|
assert canonical_config_arch(None) is None
|
|
assert canonical_config_arch("gfx1150") == "gfx115x"
|
|
assert canonical_config_arch("gfx1151") == "gfx115x"
|
|
assert canonical_config_arch("gfx1152") == "gfx115x"
|
|
assert canonical_config_arch("gfx1153") == "gfx115x"
|
|
assert canonical_config_arch("gfx942") == "gfx942"
|
|
|
|
|
|
@pytest.mark.misc
|
|
def test_is_single_panel_config_accepts_shared_gfx115x_dir(tmp_path):
|
|
(tmp_path / "gfx115x").mkdir()
|
|
|
|
supported_archs = {
|
|
"gfx1150": "rdna35_point_1",
|
|
"gfx1151": "rdna35_halo",
|
|
"gfx1152": "rdna35_point_2",
|
|
"gfx1153": "rdna35_gorgon_point",
|
|
}
|
|
|
|
assert is_single_panel_config(str(tmp_path), supported_archs) is False
|
|
|
|
|
|
@pytest.mark.misc
|
|
def test_get_num_xcds_no_compute_partition_data():
|
|
"""Test get_num_xcds when no compute partition data found - covers lines 307-309"""
|
|
from src.utils.mi_gpu_spec import MIGPUSpecs
|
|
|
|
mock_dict = {"gfx942": None}
|
|
with patch.object(MIGPUSpecs, "_gpu_arch_to_compute_partition_dict", mock_dict):
|
|
result = MIGPUSpecs.get_num_xcds(gpu_arch="gfx942") # noqa: F841
|
|
|
|
|
|
@pytest.mark.misc
|
|
def test_get_num_xcds_uninitialized_dict():
|
|
"""Test get_num_xcds when XCD dict not populated - covers lines 315-317"""
|
|
from src.utils.mi_gpu_spec import MIGPUSpecs
|
|
|
|
with patch.object(MIGPUSpecs, "_num_xcds_dict", {}):
|
|
with pytest.raises(SystemExit):
|
|
MIGPUSpecs.get_num_xcds(gpu_arch="gfx950", gpu_model="MI350")
|
|
|
|
|
|
@pytest.mark.misc
|
|
def test_get_num_xcds_unknown_gpu_model():
|
|
"""Test get_num_xcds with unknown gpu model - covers lines 319-321"""
|
|
from src.utils.mi_gpu_spec import MIGPUSpecs
|
|
|
|
result = MIGPUSpecs.get_num_xcds( # noqa: F841
|
|
gpu_arch="gfx950", gpu_model="UNKNOWN_MODEL"
|
|
)
|
|
|
|
|
|
@pytest.mark.misc
|
|
def test_get_num_xcds_no_compute_partition():
|
|
"""Test get_num_xcds with no compute partition - covers lines 325-327"""
|
|
from src.utils.mi_gpu_spec import MIGPUSpecs
|
|
|
|
result = MIGPUSpecs.get_num_xcds( # noqa: F841
|
|
gpu_arch="gfx950", gpu_model="MI350", compute_partition=""
|
|
)
|
|
|
|
|
|
@pytest.mark.misc
|
|
def test_get_num_xcds_unknown_compute_partition():
|
|
"""Test get_num_xcds with unknown compute partition - covers lines 329-332"""
|
|
from src.utils.mi_gpu_spec import MIGPUSpecs
|
|
|
|
result = MIGPUSpecs.get_num_xcds( # noqa: F841
|
|
gpu_arch="gfx950", gpu_model="MI350", compute_partition="UNKNOWN"
|
|
)
|
|
|
|
|
|
@pytest.mark.misc
|
|
def test_get_num_xcds_none_partition_value():
|
|
"""Test get_num_xcds when partition value is None - covers lines 338-340"""
|
|
from src.utils.mi_gpu_spec import MIGPUSpecs
|
|
|
|
mock_dict = {"mi350": {"spx": None}}
|
|
with patch.object(MIGPUSpecs, "_num_xcds_dict", mock_dict):
|
|
result = MIGPUSpecs.get_num_xcds( # noqa: F841
|
|
gpu_arch="gfx950", gpu_model="MI350", compute_partition="spx"
|
|
)
|
|
|
|
|
|
@pytest.mark.misc
|
|
def test_get_num_xcds_no_gpu_model():
|
|
"""Test get_num_xcds with no gpu model - covers line 342"""
|
|
from src.utils.mi_gpu_spec import MIGPUSpecs
|
|
|
|
result = MIGPUSpecs.get_num_xcds( # noqa: F841
|
|
gpu_arch="gfx950", gpu_model="", compute_partition="spx"
|
|
)
|
|
|
|
|
|
@pytest.mark.misc
|
|
def test_get_chip_id_dict_empty():
|
|
"""Test get_chip_id_dict when dict is empty - covers line 352"""
|
|
from src.utils.mi_gpu_spec import MIGPUSpecs
|
|
|
|
with patch.object(MIGPUSpecs, "_chip_id_dict", {}):
|
|
assert MIGPUSpecs.get_chip_id_dict() == {}
|
|
|
|
|
|
@pytest.mark.misc
|
|
def test_get_num_xcds_dict_empty():
|
|
"""Test get_num_xcds_dict when dict is empty - covers line 359"""
|
|
from src.utils.mi_gpu_spec import MIGPUSpecs
|
|
|
|
with patch.object(MIGPUSpecs, "_num_xcds_dict", {}):
|
|
assert MIGPUSpecs.get_num_xcds_dict() == {}
|
|
|
|
|
|
@pytest.mark.misc
|
|
def test_set_cache_sizes_cdna_uses_l1():
|
|
"""CDNA models (e.g. mi300x_a1) should key vL1D as 'L1'."""
|
|
cache_info = {
|
|
"cache": [
|
|
{
|
|
"cache_level": 1,
|
|
"cache_properties": ["DATA_CACHE"],
|
|
"cache_size": 16,
|
|
"num_cache_instance": 304,
|
|
}
|
|
]
|
|
}
|
|
result = specs.set_cache_sizes(
|
|
"mi300x_a1", 304, cache_info, num_dies=4, num_se=8, num_sa_se=2
|
|
)
|
|
assert "L1" in result
|
|
assert "L0" not in result
|
|
assert result["L1"] == 16 * 1024
|
|
|
|
|
|
@pytest.mark.misc
|
|
def test_set_cache_sizes_rdna_uses_l0_and_l1():
|
|
"""RDNA models (e.g. rdna35_halo) populate both L0 (per-CU GL0) and L1 (per-SA GL1).
|
|
Both are reported as level-1 DATA_CACHE by amd-smi; L0 has higher instance count.
|
|
"""
|
|
cache_info = {
|
|
"cache": [
|
|
{
|
|
"cache_level": 1,
|
|
"cache_properties": ["DATA_CACHE"],
|
|
"cache_size": 32,
|
|
"num_cache_instance": 40, # GL0: one per CU (40 CUs total)
|
|
},
|
|
{
|
|
"cache_level": 1,
|
|
"cache_properties": ["DATA_CACHE"],
|
|
"cache_size": 128,
|
|
"num_cache_instance": 8, # GL1: one per SA (num_se * num_sa_se = 4*2)
|
|
},
|
|
]
|
|
}
|
|
result = specs.set_cache_sizes(
|
|
"rdna35_halo", 40, cache_info, num_dies=1, num_se=4, num_sa_se=2
|
|
)
|
|
assert result["L0"] == 32 * 1024
|
|
assert result["L1"] == 128 * 1024
|
|
|
|
|
|
@pytest.mark.misc
|
|
def test_set_cache_sizes_l2_and_mall():
|
|
"""L2 and MALL (L3) cache entries are correctly parsed and scaled by num_dies."""
|
|
cache_info = {
|
|
"cache": [
|
|
{
|
|
"cache_level": 2,
|
|
"cache_size": 512,
|
|
"cache_properties": [],
|
|
"num_cache_instance": 1,
|
|
},
|
|
{
|
|
"cache_level": 3,
|
|
"cache_size": 256,
|
|
"cache_properties": [],
|
|
"num_cache_instance": 1,
|
|
},
|
|
]
|
|
}
|
|
result = specs.set_cache_sizes(
|
|
"mi300x_a1", 304, cache_info, num_dies=4, num_se=8, num_sa_se=2
|
|
)
|
|
assert result["L2"] == 512 * 1024
|
|
assert result["MALL"] == 256 * 1024 // 4
|
|
|
|
|
|
@pytest.mark.misc
|
|
def test_set_cache_sizes_selects_vl1d_by_max_instance_count():
|
|
"""Harvested GPU: num_cu reported by rocminfo may be less than the max
|
|
num_cache_instance in amd-smi. set_cache_sizes must select vL1D by the
|
|
highest num_cache_instance, not by exact match to num_cu.
|
|
"""
|
|
cache_info = {
|
|
"cache": [
|
|
{
|
|
"cache_level": 1,
|
|
"cache_properties": ["DATA_CACHE"],
|
|
"cache_size": 16,
|
|
"num_cache_instance": 228, # max instances (some CUs harvested)
|
|
},
|
|
{
|
|
"cache_level": 1,
|
|
"cache_properties": ["DATA_CACHE"],
|
|
"cache_size": 16,
|
|
"num_cache_instance": 190, # lower instance count entry
|
|
},
|
|
]
|
|
}
|
|
# num_cu=224 simulates harvested GPU (fewer than 228 active CUs)
|
|
result = specs.set_cache_sizes(
|
|
"mi300x_a1", 224, cache_info, num_dies=4, num_se=8, num_sa_se=2
|
|
)
|
|
assert "L1" in result
|
|
assert result["L1"] == 16 * 1024
|
|
|
|
|
|
@pytest.mark.misc
|
|
def test_normal_functionality_still_works():
|
|
"""Ensure that normal paths still work after adding error handling tests"""
|
|
from src.utils.mi_gpu_spec import MIGPUSpecs
|
|
|
|
result = MIGPUSpecs.get_gpu_model("gfx90a", None)
|
|
|
|
assert result is not None
|
|
|
|
result = MIGPUSpecs.get_gpu_series("gfx90a")
|
|
assert result is not None
|
|
|
|
result = MIGPUSpecs.get_num_xcds(gpu_arch="gfx90a")
|
|
assert result == 1
|
|
|
|
|
|
@pytest.mark.misc
|
|
def test_kw_only_rejects_positional_arguments():
|
|
"""MachineSpecs construction must be keyword-only (regression for the
|
|
_kw_only decorator that previously accepted positional args silently).
|
|
"""
|
|
with pytest.raises(TypeError):
|
|
MachineSpecs("gfx942")
|
|
|
|
spec = MachineSpecs(gpu_arch="gfx942")
|
|
assert spec.gpu_arch == "gfx942"
|
|
|
|
|
|
@pytest.mark.misc
|
|
@pytest.mark.parametrize(
|
|
"series, expected_cls",
|
|
[
|
|
("RDNA3.5", MachineSpecsRDNA35),
|
|
("mi300", MachineSpecsCDNA),
|
|
(None, MachineSpecsCDNA),
|
|
],
|
|
ids=["rdna35", "cdna", "unknown"],
|
|
)
|
|
def test_spec_family_for_arch_dispatch(series, expected_cls):
|
|
with patch.object(specs.mi_gpu_specs, "get_gpu_series", return_value=series):
|
|
assert spec_family_for_arch("gfx_test") is expected_cls
|
|
|
|
|
|
@pytest.mark.misc
|
|
@pytest.mark.parametrize(
|
|
"se_per_gpu, sa_per_se, vram_bit_width, expected_gl1c, expected_channels",
|
|
[
|
|
(4, 2, 256, "8", "8"),
|
|
(4, 2, None, "8", "32"),
|
|
(None, 2, 256, None, "8"),
|
|
],
|
|
ids=["bit_width", "fallback_total_l2_chan", "missing_se"],
|
|
)
|
|
def test_rdna35_finalize_soc_fields(
|
|
se_per_gpu, sa_per_se, vram_bit_width, expected_gl1c, expected_channels
|
|
):
|
|
"""RDNA 3.5 derives num_gl1c from SE*SA and num_memory_channels from the
|
|
VRAM bus width, falling back to total_l2_chan when width is unavailable.
|
|
"""
|
|
spec = MachineSpecsRDNA35(
|
|
gpu_arch="gfx1151",
|
|
gpu_model="rdna35_halo",
|
|
se_per_gpu=se_per_gpu,
|
|
sa_per_se=sa_per_se,
|
|
l2_banks="8",
|
|
total_l2_chan="32",
|
|
)
|
|
gpu_info = {
|
|
"num_compute_units": 0,
|
|
"gpu_cache_info": {},
|
|
"vram_bit_width": vram_bit_width,
|
|
}
|
|
with patch.object(specs, "set_cache_sizes", return_value={}), patch.object(
|
|
specs.mi_gpu_specs, "get_num_dies", return_value=1
|
|
), patch.object(specs, "totall2_banks", return_value="32"):
|
|
spec.finalize_soc_fields(gpu_info)
|
|
|
|
assert spec.num_gl1c == expected_gl1c
|
|
assert spec.num_memory_channels == expected_channels
|
|
|
|
|
|
@pytest.mark.misc
|
|
def test_reconstruct_specs_from_sysinfo_round_trip():
|
|
"""generate_machine_specs reconstructs the arch-specific subclass from a
|
|
saved sysinfo dict.
|
|
"""
|
|
sysinfo = {
|
|
"version": "3",
|
|
"gpu_arch": "gfx1151",
|
|
"num_gl1c": "8",
|
|
"num_memory_channels": "8",
|
|
}
|
|
with patch.object(
|
|
specs, "get_version", return_value={"version": "3.0.0"}
|
|
), patch.object(specs.mi_gpu_specs, "get_gpu_series", return_value="RDNA3.5"):
|
|
spec = generate_machine_specs(None, sysinfo)
|
|
|
|
assert isinstance(spec, MachineSpecsRDNA35)
|
|
assert spec.num_gl1c == "8"
|
|
assert spec.num_memory_channels == "8"
|
|
|
|
|
|
@pytest.mark.misc
|
|
def test_reconstruct_specs_from_sysinfo_missing_version_raises():
|
|
"""A sysinfo dict without a version key aborts via console_error/KeyError."""
|
|
with pytest.raises((KeyError, SystemExit)):
|
|
generate_machine_specs(None, {"gpu_arch": "gfx1151"})
|
|
|
|
|
|
@pytest.mark.misc
|
|
def test_get_rocm_ver_env_override(monkeypatch, tmp_path):
|
|
"""ROCM_VER overrides detection when no .info/ directory is present."""
|
|
monkeypatch.setenv("ROCM_PATH", str(tmp_path))
|
|
monkeypatch.setenv("ROCM_VER", "6.9.9")
|
|
assert specs.get_rocm_ver() == "6.9.9"
|
|
|
|
|
|
@pytest.mark.misc
|
|
def test_get_rocm_ver_undetectable_errors(monkeypatch, tmp_path):
|
|
"""Missing .info/ and unset ROCM_VER terminates via console_error."""
|
|
monkeypatch.setenv("ROCM_PATH", str(tmp_path))
|
|
monkeypatch.delenv("ROCM_VER", raising=False)
|
|
with patch.object(specs, "console_error") as console_error_mock:
|
|
specs.get_rocm_ver()
|
|
console_error_mock.assert_called_once()
|
|
assert (
|
|
"Unable to detect a complete local ROCm installation"
|
|
in (console_error_mock.call_args.args[0])
|
|
)
|