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
396 lines
13 KiB
Python
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 == []
|