Files
derek.holloway 7502bf64a4
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
Flatten subprojects into a single base monorepo
2026-09-02 16:34:47 -07:00

396 lines
13 KiB
Python

# Copyright (c) Advanced Micro Devices, Inc.
# SPDX-License-Identifier: MIT
"""
Unit tests for counter allocation pipeline in soc_base.py.
Tests LimitedSet, CounterFile, and the bin-packing helpers used by
perfmon_coalesce — no GPU hardware required.
"""
from types import SimpleNamespace
from typing import Any
from unittest.mock import MagicMock, patch
import pytest
from rocprof_compute_soc.soc_base import (
CounterFile,
LimitedSet,
OmniSoC_Base,
_rebuild_tcc_channel_file_map,
_trial_counter_file_with_extra,
flat_counters_in_perfmon_file,
)
# =============================================================================
# Fixtures
# =============================================================================
PERFMON_CONFIG = {
"SQ": 8,
"TA": 2,
"TD": 2,
"TCP": 4,
"TCC": 4,
"CPC": 2,
"CPF": 2,
"SPI": 6,
"GRBM": 2,
"GDS": 4,
}
@pytest.fixture
def perfmon_config():
return dict(PERFMON_CONFIG)
@pytest.fixture
def empty_counter_file(perfmon_config):
return CounterFile("0", perfmon_config)
def _make_soc(perfmon_config, arch="gfx908", num_xcd=1, l2_banks=4):
"""Build a minimal OmniSoC_Base without rocminfo or GPU access."""
mspec = MagicMock()
mspec.rocminfo_lines = None # skip populate_mspec
mspec.num_xcd = num_xcd
mspec.l2_banks = l2_banks
args = MagicMock()
args.config_dir = "/dev/null"
with patch("rocprof_compute_soc.soc_base.console_debug"):
soc = OmniSoC_Base(args, mspec)
soc.set_arch(arch)
soc.set_perfmon_config(perfmon_config)
return soc
# =============================================================================
# A. LimitedSet
# =============================================================================
def test_limited_set_basic():
ls = LimitedSet(2)
assert ls.add("SQ_WAVES") is True
assert ls.add("SQ_BUSY") is True
assert ls.add("SQ_INSTS") is False # capacity exhausted
assert ls.add("SQ_WAVES") is True # duplicate ok
assert ls.avail == 0
assert ls.elements == ["SQ_WAVES", "SQ_BUSY"]
def test_limited_set_tcc_channel_coalescing():
ls = LimitedSet(1)
assert ls.add("TCC_HIT[0]") is True
assert ls.avail == 0
# Same TCC base — bypasses capacity
assert ls.add("TCC_HIT[1]") is True
assert ls.add("TCC_HIT[2]") is True
# Different TCC base — rejected (no capacity left)
assert ls.add("TCC_MISS[0]") is False
assert len(ls.elements) == 3
def test_limited_set_reserve_succeeds_within_capacity():
"""`reserve(n)` debits avail by n and returns True when capacity remains."""
ls = LimitedSet(4)
assert ls.add("SQ_WAVES") is True
assert ls.reserve(2) is True
def test_limited_set_reserve_refuses_when_insufficient():
"""`reserve(n)` returns False and leaves avail untouched when n > avail."""
ls = LimitedSet(2)
assert ls.reserve(3) is False
assert ls.avail == 2
assert ls.elements == []
def test_limited_set_reserve_does_not_add_elements():
"""
Reservation is opaque: it does not record a counter name and leaves
subsequent add() free to use whatever capacity remains.
"""
ls = LimitedSet(3)
assert ls.reserve(2) is True
assert ls.elements == []
assert ls.avail == 1
assert ls.add("SQ_WAVES") is True
assert ls.elements == ["SQ_WAVES"]
assert ls.avail == 0
# =============================================================================
# B. CounterFile
# =============================================================================
def test_counter_file_exposes_name_attribute(perfmon_config):
"""The per-block LimitedSet map is exposed via `blocks`."""
cf = CounterFile("SQ_LEVEL_WAVES_ACCUM", perfmon_config)
assert cf.name == "SQ_LEVEL_WAVES_ACCUM"
assert set(cf.blocks.keys()) == set(perfmon_config.keys())
for block, limited_set in cf.blocks.items():
assert isinstance(limited_set, LimitedSet)
assert limited_set.avail == perfmon_config[block]
assert limited_set.elements == []
def test_counter_file_add_and_block_mapping(perfmon_config):
cf = CounterFile("0", perfmon_config)
# SQ, SQC, SP all map to the SQ block (capacity 8)
assert cf.add("SQ_WAVES") is True
assert cf.add("SQC_CACHE_HIT") is True
assert cf.add("SP_SOMETHING") is True
assert cf.blocks["SQ"].avail == 5 # 8 - 3
# TA maps to its own block (capacity 2)
assert cf.add("TA_ADDR") is True
assert cf.add("TA_DATA") is True
assert cf.add("TA_EXTRA") is False # TA full
# TCP maps to its own block (capacity 4)
assert cf.add("TCP_READ") is True
assert cf.blocks["TCP"].avail == 3
def test_counter_file_reserve_delegates_to_block(perfmon_config):
"""
`reserve(counter, n)` debits the LimitedSet for the block selected by
counter_to_block(counter) and returns the underlying boolean.
"""
cf = CounterFile("0", perfmon_config)
assert cf.add("SQ_WAVES") is True # SQ avail: 8 -> 7
assert cf.reserve("SQ_INSTS", 2) is True
assert cf.blocks["SQ"].avail == 5 # 7 - 2
# TA capacity is 2 in the fixture, so reserving 3 must fail.
assert cf.reserve("TA_EXTRA", 3) is False
assert cf.blocks["TA"].avail == 2 # unchanged after failed reserve
# Reserve must never record a counter name in the block's elements.
assert cf.blocks["SQ"].elements == ["SQ_WAVES"]
assert cf.blocks["TA"].elements == []
# =============================================================================
# C. flat_counters_in_perfmon_file
# =============================================================================
def test_flat_counters_in_perfmon_file(perfmon_config):
# Empty file returns empty list
cf = CounterFile("0", perfmon_config)
assert flat_counters_in_perfmon_file(cf) == []
# Add counters across blocks and verify flattened order
cf.add("SQ_WAVES")
cf.add("TA_ADDR")
cf.add("TCP_READ")
result = flat_counters_in_perfmon_file(cf)
assert "SQ_WAVES" in result
assert "TA_ADDR" in result
assert "TCP_READ" in result
assert len(result) == 3
# =============================================================================
# D. _trial_counter_file_with_extra
# =============================================================================
def test_trial_counter_file_with_extra_fits(perfmon_config):
basis = CounterFile("0", perfmon_config)
basis.add("SQ_WAVES")
basis.add("TA_ADDR")
extras = ["TCP_READ", "TCC_HIT[0]"]
trial = _trial_counter_file_with_extra(basis, perfmon_config, extras)
assert trial is not None
flat = flat_counters_in_perfmon_file(trial)
assert set(flat) == {"SQ_WAVES", "TA_ADDR", "TCP_READ", "TCC_HIT[0]"}
# Original basis is unchanged
assert set(flat_counters_in_perfmon_file(basis)) == {"SQ_WAVES", "TA_ADDR"}
def test_trial_counter_file_with_extra_overflow(perfmon_config):
basis = CounterFile("0", perfmon_config)
# Fill TA to capacity (2)
basis.add("TA_ADDR")
basis.add("TA_DATA")
# Try adding a third TA counter — should fail
result = _trial_counter_file_with_extra(basis, perfmon_config, ["TA_EXTRA"])
assert result is None
# Basis still has only 2 TA counters
assert len(basis.blocks["TA"].elements) == 2
# =============================================================================
# E. _rebuild_tcc_channel_file_map
# =============================================================================
def test_rebuild_tcc_channel_file_map(perfmon_config):
bucket_a = CounterFile("a", perfmon_config)
bucket_a.add("TCC_HIT[0]")
bucket_a.add("TCC_HIT[1]")
bucket_a.add("SQ_WAVES") # non-TCC, should be ignored
bucket_b = CounterFile("b", perfmon_config)
bucket_b.add("TCC_MISS[0]")
result = _rebuild_tcc_channel_file_map([bucket_a, bucket_b])
assert result["TCC_HIT"] is bucket_a
assert result["TCC_MISS"] is bucket_b
assert "SQ" not in result
# =============================================================================
# F. _allocate_perfmon_counter_files
# =============================================================================
def test_allocate_level_counters_get_dedicated_files(perfmon_config):
soc = _make_soc(perfmon_config)
counters = {
"SQ_LEVEL_WAVES_ACCUM",
"SQC_DCACHE_INFLIGHT_LEVEL_ACCUM",
"TA_ADDR",
}
with patch.object(soc, "_same_bucket_priority_metric_ids", return_value=()):
files, file_count, accu_count = soc._allocate_perfmon_counter_files(counters)
accum_files = [f for f in files if f.name.endswith("_ACCUM")]
assert accu_count == 2
assert len(accum_files) == 2
assert {f.name for f in accum_files} == {
"SQ_LEVEL_WAVES_ACCUM",
"SQC_DCACHE_INFLIGHT_LEVEL_ACCUM",
}
for af in accum_files:
assert af.name in set(flat_counters_in_perfmon_file(af))
# Both accumulators land in the SQ block (SQC routes via BLOCK_REMAP). Each
# file loses 2 SQ slots: 1 for add() + 1 for reserve(counter, 1) (the
# paired level event the hardware programs alongside the accumulator).
sq_waves_accum = next(f for f in accum_files if f.name == "SQ_LEVEL_WAVES_ACCUM")
assert sq_waves_accum.blocks["SQ"].avail == perfmon_config["SQ"] - 2
sqc_dcache_accum = next(
f for f in accum_files if f.name == "SQC_DCACHE_INFLIGHT_LEVEL_ACCUM"
)
assert sqc_dcache_accum.blocks["SQ"].avail == perfmon_config["SQ"] - 2
# TA_ADDR placed somewhere (first-fit into a LEVEL file or its own)
all_ctrs = set()
for f in files:
all_ctrs.update(flat_counters_in_perfmon_file(f))
assert "TA_ADDR" in all_ctrs
def test_allocate_first_fit_packing(perfmon_config):
soc = _make_soc(perfmon_config)
# 3 SQ counters — all fit in one bucket (SQ capacity 8)
counters = {"SQ_WAVES", "SQ_BUSY", "SQ_INSTS"}
with patch.object(soc, "_same_bucket_priority_metric_ids", return_value=()):
files, file_count, accu_count = soc._allocate_perfmon_counter_files(counters)
assert accu_count == 0
assert len(files) == 1
assert file_count == 1
flat = set(flat_counters_in_perfmon_file(files[0]))
assert flat == counters
def test_allocate_tcc_channel_coalescing(perfmon_config):
soc = _make_soc(perfmon_config)
# TCC channels with same base should land in the same bucket
counters = {"TCC_HIT[0]", "TCC_HIT[1]", "TCC_HIT[2]", "SQ_WAVES"}
with patch.object(soc, "_same_bucket_priority_metric_ids", return_value=()):
files, file_count, accu_count = soc._allocate_perfmon_counter_files(counters)
# All TCC_HIT channels should be in the same file
tcc_file = None
for f in files:
flat = flat_counters_in_perfmon_file(f)
if any("TCC_HIT" in c for c in flat):
tcc_file = f
break
assert tcc_file is not None
tcc_ctrs = [c for c in flat_counters_in_perfmon_file(tcc_file) if "TCC_HIT" in c]
assert set(tcc_ctrs) == {"TCC_HIT[0]", "TCC_HIT[1]", "TCC_HIT[2]"}
# =============================================================================
# G. _expand_tcc_template_counters
# =============================================================================
def test_expand_tcc_templates(perfmon_config):
soc = _make_soc(perfmon_config, num_xcd=2, l2_banks=3)
result = soc._expand_tcc_template_counters({"TCC_HIT[", "SQ_WAVES"})
# Template replaced with 2*3=6 indexed counters
assert "TCC_HIT[" not in result
expected_tcc = {f"TCC_HIT[{i}]" for i in range(6)}
assert expected_tcc.issubset(result)
assert "SQ_WAVES" in result
assert len(result) == 7 # 6 TCC + 1 SQ
def test_expand_tcc_no_templates(perfmon_config):
soc = _make_soc(perfmon_config, num_xcd=1, l2_banks=4)
inp = {"SQ_WAVES", "TA_ADDR", "TCC_HIT[0]"}
result = soc._expand_tcc_template_counters(inp)
# No templates — input unchanged
assert result == inp
# =============================================================================
# H. _append_analysis_yaml_for_filter_token alias handling
# =============================================================================
def _fake_soc_for_filter_token(arch: str = "gfx942") -> Any:
"""Minimal stand-in exposing only the _mspec.gpu_arch the method reads."""
return SimpleNamespace(_mspec=SimpleNamespace(gpu_arch=arch))
def test_filter_token_unknown_alias_exits_instead_of_keyerror(monkeypatch):
monkeypatch.setattr(
"rocprof_compute_soc.soc_base.get_arch_alias_to_panel_id",
lambda arch: {"lds": "10"},
)
with pytest.raises(SystemExit):
OmniSoC_Base._append_analysis_yaml_for_filter_token(
_fake_soc_for_filter_token(), "SQ", {}, "/cfg", []
)
def test_filter_token_known_alias_resolves_without_crash(monkeypatch):
monkeypatch.setattr(
"rocprof_compute_soc.soc_base.get_arch_alias_to_panel_id",
lambda arch: {"lds": "10"},
)
texts: list[str] = []
# Alias resolves to block id 10 -> file id "1000", absent from the
# empty config dict, so the token is skipped with a warning, not a crash.
OmniSoC_Base._append_analysis_yaml_for_filter_token(
_fake_soc_for_filter_token(), "lds", {}, "/cfg", texts
)
assert texts == []