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

1169 lines
42 KiB
Python

# Copyright (c) Advanced Micro Devices, Inc.
# SPDX-License-Identifier: MIT
"""Unit tests for analysis_db.py static methods."""
import json
from pathlib import Path
from typing import Optional
from unittest.mock import MagicMock, patch
import common
import numpy as np
import pandas as pd
import pytest
from rocprof_compute_analyze.analysis_db import db_analysis
from utils import analysis_orm as orm
from utils import schema
from utils.metrics.noise_clamper import (
clear_noise_clamp_warnings,
get_noise_clamp_warnings,
)
def make_dual_issue_arch_config(metric_name: str, peak_col: str = "Peak"):
"""Build an arch_config with a metric_table carrying one VALU row."""
metric_df = pd.DataFrame(
{
"Metric": [metric_name],
"Value": ["unused_expression"],
peak_col: ["unused_peak_expression"],
},
index=pd.Index(["1.1"], name="Metric_ID"),
)
arch_config = schema.ArchConfig()
arch_config.dfs = {201: metric_df}
arch_config.dfs_type = {201: "metric_table"}
return arch_config
# =============================================================================
# db_analysis.evaluate() tests
# =============================================================================
def test_evaluate_parse_false_basic_expressions():
"""Test parse=False mode with basic expressions and substitutions."""
pmc_df = pd.DataFrame({
"Counter1": [10, 20, 30],
"Counter2": [1, 2, 3],
})
sys_info = {"numCUs": 64, "clock_speed": 1500}
# Test raw_pmc_df -> pmc_df substitution on flat single-index columns
result = db_analysis.evaluate(
"test_metric",
"raw_pmc_df['Counter1']",
pmc_df,
sys_info,
parse=False,
)
assert isinstance(result, pd.Series)
assert list(result) == [10, 20, 30]
# Test ammolite__ substitution for sys_info access
result = db_analysis.evaluate(
"test_metric",
"ammolite__numCUs * 2",
pmc_df,
sys_info,
parse=False,
)
assert result == 128
# Test expression with helper function
result = db_analysis.evaluate(
"test_metric",
"to_sum(raw_pmc_df['Counter1'])",
pmc_df,
sys_info,
parse=False,
)
assert result == 60
def test_evaluate_parse_true_basic_expressions():
"""Test parse=True mode with $ substitution and AST transformation."""
pmc_df = pd.DataFrame({
"Counter1": [10, 20, 30],
"Counter2": [2, 4, 6],
})
sys_info = {"numCUs": 64, "multiplier": 2}
# Test $variable substitution
result = db_analysis.evaluate(
"test_metric",
"$numCUs * $multiplier",
pmc_df,
sys_info,
parse=True,
)
assert result == 128
# Test AST transformation with SUPPORTED_CALL functions (SUM -> to_sum)
# and bare identifiers (Counter1 -> raw_pmc_df["Counter1"])
result = db_analysis.evaluate(
"test_metric",
"SUM(Counter1)",
pmc_df,
sys_info,
parse=True,
)
assert result == 60
# Test combined $ substitution and column access with AVG
result = db_analysis.evaluate(
"test_metric",
"AVG(Counter1) + $numCUs",
pmc_df,
sys_info,
parse=True,
)
assert result == 84 # avg(10,20,30)=20 + 64
def test_evaluate_none_and_na_handling():
"""Test evaluate() handling of None and NA values."""
pmc_df = pd.DataFrame({"Counter1": [10, 20, 30]})
sys_info = {}
# Explicit None in expression result returns None without warning
result = db_analysis.evaluate(
"test_metric",
"None",
pmc_df,
sys_info,
parse=False,
)
assert result is None
# Scalar NA values (NaN) return None
pmc_df_nan = pd.DataFrame({"Counter1": [np.nan, np.nan, np.nan]})
result = db_analysis.evaluate(
"test_metric",
"to_sum(raw_pmc_df['Counter1'])",
pmc_df_nan,
sys_info,
parse=False,
)
assert result is None
# Series with NA values are preserved (not converted to None)
pmc_df_mixed = pd.DataFrame({"Counter1": [10, np.nan, 30]})
result = db_analysis.evaluate(
"test_metric",
"raw_pmc_df['Counter1']",
pmc_df_mixed,
sys_info,
parse=False,
)
assert isinstance(result, pd.Series)
assert result.iloc[0] == 10
assert pd.isna(result.iloc[1])
assert result.iloc[2] == 30
# Exceptions return None gracefully
result = db_analysis.evaluate(
"test_metric",
"raw_pmc_df['NonExistent']",
pmc_df,
sys_info,
parse=False,
)
assert result is None
def test_evaluate_with_none_in_formula_does_not_nullify_valid_result():
"""
Test that expressions containing 'None' in formula string
still return valid results when evaluation produces a value.
This is a regression test for the bugfix where expressions like
.where(..., None) were incorrectly returning None even when
the actual result was valid.
"""
pmc_df = pd.DataFrame({
"Counter1": [10, 20, 30],
"Counter2": [1, 0, 3], # Has a zero for conditional
})
sys_info = {}
# Expression with None as fallback in .where() - should return valid result
# when condition is met for at least some values
result = db_analysis.evaluate(
"test_metric",
"(raw_pmc_df['Counter1'] / "
"raw_pmc_df['Counter2'].where("
"raw_pmc_df['Counter2'] != 0, None))",
pmc_df,
sys_info,
parse=False,
)
# Result should be a Series, not None
assert result is not None
assert isinstance(result, pd.Series)
# Expression that literally has "None" string but evaluates to a number
result = db_analysis.evaluate(
"test_metric",
"10 if True else None",
pmc_df,
sys_info,
parse=False,
)
assert result == 10
def test_evaluate_divide_by_zero_silenced_and_logged_at_debug():
"""
Divide-by-zero (x/0 -> inf, 0/0 -> NaN) emits a numpy RuntimeWarning
that is captured and logged via console_debug. The "evaluated to N/A"
console_warning must not fire when a RuntimeWarning was caught.
"""
pmc_df = pd.DataFrame({"Counter1": [10, 20, 30]})
sys_info = {}
cases = [
# x/0 yields scalar inf; evaluate() collapses to None
"to_sum(raw_pmc_df['Counter1']) / 0",
# 0/0 yields scalar NaN; evaluate() collapses to None
"(to_sum(raw_pmc_df['Counter1']) * 0) / 0",
]
for expr in cases:
with patch(
"rocprof_compute_analyze.analysis_db.console_warning"
) as mock_warning:
with patch(
"rocprof_compute_analyze.analysis_db.console_debug"
) as mock_debug:
result = db_analysis.evaluate(
"test_metric",
expr,
pmc_df,
sys_info,
parse=False,
)
assert result is None, f"Expected None for '{expr}', got {result}"
mock_warning.assert_not_called()
debug_msgs = [str(call) for call in mock_debug.call_args_list]
assert any("RuntimeWarning" in m for m in debug_msgs), (
f"Expected RuntimeWarning in console_debug output for '{expr}', "
f"got {debug_msgs}"
)
# =============================================================================
# db_analysis.calc_builtin_vars() tests
# =============================================================================
def test_calc_builtin_vars_processes_per_xcd_first():
"""
Test that PER_XCD variables are processed before non-PER_XCD variables,
allowing non-PER_XCD vars to reference PER_XCD vars via $placeholder.
"""
pmc_df = pd.DataFrame({
"Counter1": [100, 200],
})
sys_info = {"base_value": 10, "gpu_arch": "gfx942"}
# Mock BUILD_IN_VARS with dependency chain:
# - PER_XCD_VAR: computed from base_value
# - DERIVED_VAR: depends on PER_XCD_VAR via $PER_XCD_VAR
mock_builtin_vars = {
"PER_XCD_VAR": "$base_value * 2", # Should be processed first -> 20
"DERIVED_VAR": "$PER_XCD_VAR + 5", # Depends on PER_XCD_VAR -> 25
}
with patch(
"rocprof_compute_analyze.analysis_db.mi_gpu_specs.get_gpu_series",
return_value="MI300",
):
with patch(
"rocprof_compute_analyze.analysis_db.get_build_in_vars",
return_value=mock_builtin_vars,
):
with patch(
"utils.utils_counter_defs.get_build_in_vars",
return_value=mock_builtin_vars,
):
db_analysis.calc_builtin_vars(
pmc_df, sys_info, ["$PER_XCD_VAR", "$DERIVED_VAR"]
)
# Verify PER_XCD var was computed
assert sys_info["PER_XCD_VAR"] == 20
# Verify DERIVED_VAR used the computed PER_XCD_VAR value
assert sys_info["DERIVED_VAR"] == 25
def test_calc_builtin_vars_with_dataframe_expressions():
"""Test builtin vars that operate on DataFrame columns."""
pmc_df = pd.DataFrame({
"Counter1": [10, 20, 30],
})
sys_info = {"multiplier": 2, "gpu_arch": "gfx942"}
# Use SUPPORTED_CALL function names (SUM -> to_sum via CodeTransformer)
mock_builtin_vars = {
"TOTAL_COUNT": "SUM(Counter1)", # 60
"SCALED_TOTAL": "$TOTAL_COUNT * $multiplier", # 120
}
with patch(
"rocprof_compute_analyze.analysis_db.mi_gpu_specs.get_gpu_series",
return_value="MI300",
):
with patch(
"rocprof_compute_analyze.analysis_db.get_build_in_vars",
return_value=mock_builtin_vars,
):
with patch(
"utils.utils_counter_defs.get_build_in_vars",
return_value=mock_builtin_vars,
):
db_analysis.calc_builtin_vars(
pmc_df, sys_info, ["$TOTAL_COUNT", "$SCALED_TOTAL"]
)
assert sys_info["TOTAL_COUNT"] == 60
assert sys_info["SCALED_TOTAL"] == 120
# =============================================================================
# db_analysis.calc_dataframe_expressions() tests
# =============================================================================
def test_calc_dataframe_expressions_applies_evaluate_to_rows():
"""Test that expressions are evaluated for each row of expression_df."""
pmc_df = pd.DataFrame({
"Counter1": [10, 20, 30],
"Counter2": [1, 2, 3],
})
sys_info = {"scale": 100, "gpu_arch": "gfx942"}
expression_df = pd.DataFrame({
"metric_id": ["1.1", "1.2"],
"value_name": ["sum", "scaled"],
"value": [
"to_sum(raw_pmc_df['Counter1'])",
"ammolite__scale * 2",
],
})
with patch(
"rocprof_compute_analyze.analysis_db.get_build_in_vars", return_value={}
):
result = db_analysis.calc_dataframe_expressions(pmc_df, sys_info, expression_df)
assert isinstance(result, pd.Series)
assert len(result) == 2
assert result.iloc[0] == 60 # sum of Counter1
assert result.iloc[1] == 200 # 100 * 2
def test_calc_dataframe_expressions_with_builtin_vars():
"""Test that calc_dataframe_expressions calls calc_builtin_vars first."""
pmc_df = pd.DataFrame({"Counter1": [10, 20, 30]})
sys_info = {"base": 5, "gpu_arch": "gfx942"}
# Expression references a builtin var that gets computed
mock_builtin_vars = {
"COMPUTED_VAR": "$base * 10", # 50
}
expression_df = pd.DataFrame({
"metric_id": ["1.1", "1.2"],
"value_name": ["test", "none_result"],
"value": [
"ammolite__COMPUTED_VAR + 1", # Should be 51
"None",
],
})
with patch(
"rocprof_compute_analyze.analysis_db.mi_gpu_specs.get_gpu_series",
return_value="MI300",
):
with patch(
"rocprof_compute_analyze.analysis_db.get_build_in_vars",
return_value=mock_builtin_vars,
):
with patch(
"utils.utils_counter_defs.get_build_in_vars",
return_value=mock_builtin_vars,
):
result = db_analysis.calc_dataframe_expressions(
pmc_df, sys_info, expression_df
)
assert result.iloc[0] == 51
# None from evaluate becomes NaN in pandas Series
assert pd.isna(result.iloc[1])
def test_calc_dataframe_expressions_empty_returns_assignable_series():
"""An empty expression_df returns an empty Series, not a DataFrame."""
expression_df = pd.DataFrame(columns=["metric_id", "value_name", "value"])
result = db_analysis.calc_dataframe_expressions(
pd.DataFrame({"Counter1": [1, 2, 3]}),
{"gpu_arch": "gfx942"},
expression_df,
)
assert isinstance(result, pd.Series)
assert result.empty
# Reproduces the call site: assigning the result as a single column must
# not raise "Columns must be same length as key".
expression_df["value"] = result
# =============================================================================
# calc_metrics_data tests
# =============================================================================
def test_calc_metrics_data_builds_rows_and_preserves_schema():
"""Metric tables expand into rows with table-level fields resolved once;
non-metric tables are skipped and the output frames keep their columns."""
workload_path = "/fake/workload"
metric_df = pd.DataFrame(
{
"Metric": ["Grid Size"],
"Avg": [" 10 "],
"Min": [" 5 "],
"Max": [" 20 "],
"Unit": ["Work items"],
"Description": ["Grid size desc"],
},
index=pd.Index(["7.1.0"], name="Metric_ID"),
)
arch_config = schema.ArchConfig()
# Table 1 has no Metric/Channel column and is skipped; table 701 maps to
# panel 700 (table_name) and sub-table 701 (sub_table_name).
arch_config.dfs = {
1: pd.DataFrame({"from_csv": ["pmc_kernel_top.csv"]}),
701: metric_df,
}
arch_config.panel_configs = {
700: {
"id": 700,
"title": "Wavefront",
"data source": [
{"metric_table": {"id": 701, "title": "Wavefront Launch Stats"}}
],
}
}
analyzer = db_analysis(MagicMock(), {})
analyzer._pmc_df_per_workload = {workload_path: pd.DataFrame({"Counter1": [1]})}
analyzer._runs = {
workload_path: MagicMock(sys_info=pd.DataFrame([{"gpu_arch": "gfx942"}]))
}
analyzer._arch_configs = {"gfx942": arch_config}
metrics_info, expressions = analyzer.calc_metrics_data()
info = metrics_info[workload_path]
assert "pct_of_peak" in info.columns
assert list(info["metric_id"]) == ["7.1.0"]
assert list(info["name"]) == ["Grid Size"]
assert list(info["table_name"]) == ["Wavefront"]
assert list(info["sub_table_name"]) == ["Wavefront Launch Stats"]
assert not bool(info["pct_of_peak"].iloc[0])
exprs = expressions[workload_path]
assert list(exprs.columns) == ["metric_id", "value_name", "value"]
# Metric/Unit/Description are non-expression columns, so only Avg/Min/Max
# expand into expression rows, stripped and in dataframe-column order.
assert list(exprs["value_name"]) == ["Avg", "Min", "Max"]
assert list(exprs["value"]) == ["10", "5", "20"]
assert set(exprs["metric_id"]) == {"7.1.0"}
# =============================================================================
# Noise-clamp warning + summary tests
# =============================================================================
def test_calc_expressions_noise_clamp():
"""Variance warnings fire only at workload level, summary once per workload.
- evaluate(emit_variance_warnings=True) emits the per-metric warning when
to_noise_clamp advances the global counter; the False kwarg stays silent.
- calc_expressions emits exactly one variance warning per workload
(kernel-level pass is silent) and calls print_noise_clamp_summary once.
"""
workload_path = "/fake/workload"
noise_clamp_expression = (
"to_noise_clamp(to_min(raw_pmc_df['DIFF']), to_max(raw_pmc_df['REF']))"
)
# Two distinct kernels so groupby yields two kernel-level evaluate calls
# in addition to one workload-level call. Without the kwarg gate the
# unguarded code would emit three warnings; with the gate, exactly one.
pmc_df = pd.DataFrame({
"Kernel_Name": ["kernel_a", "kernel_b"],
"DIFF": [-100.0, -100.0],
"REF": [1000.0, 1000.0],
})
expression_template = pd.DataFrame({
"metric_id": ["1.1"],
"value_name": ["clamped"],
"value": [noise_clamp_expression],
})
sys_info_df = pd.DataFrame([{"placeholder": 1, "gpu_arch": "gfx942"}])
analyzer = db_analysis(MagicMock(), {})
analyzer._pmc_df_per_workload = {workload_path: pmc_df}
analyzer._metric_expression_data_per_workload = {workload_path: expression_template}
analyzer._metrics_info_data_per_workload = {}
analyzer._roofline_ceilings_per_workload = {workload_path: {}}
analyzer._runs = {workload_path: MagicMock(sys_info=sys_info_df)}
analyzer._arch_configs = MagicMock()
# Direct evaluate kwarg behavior.
clear_noise_clamp_warnings()
with patch(
"rocprof_compute_analyze.analysis_db.console_warning"
) as console_warning_mock:
db_analysis.evaluate(
"direct_test",
noise_clamp_expression,
pmc_df,
{},
emit_variance_warnings=True,
)
variance_warning_calls = [
warning_call
for warning_call in console_warning_mock.call_args_list
if "Variance corrected for metric: direct_test" in warning_call.args[0]
]
assert len(variance_warning_calls) == 1
assert get_noise_clamp_warnings()["count"] >= 1
clear_noise_clamp_warnings()
with patch(
"rocprof_compute_analyze.analysis_db.console_warning"
) as console_warning_mock:
db_analysis.evaluate(
"direct_test_off",
noise_clamp_expression,
pmc_df,
{},
emit_variance_warnings=False,
)
assert get_noise_clamp_warnings()["count"] >= 1
variance_warning_calls = [
warning_call
for warning_call in console_warning_mock.call_args_list
if "Variance corrected for metric:" in warning_call.args[0]
]
assert variance_warning_calls == []
# calc_expressions per-workload bracket.
clear_noise_clamp_warnings()
with patch(
"rocprof_compute_analyze.analysis_db.get_build_in_vars", return_value={}
):
with patch(
"rocprof_compute_analyze.analysis_db.console_warning"
) as console_warning_mock:
with patch(
"rocprof_compute_analyze.analysis_db.print_noise_clamp_summary"
) as print_noise_clamp_summary_mock:
with patch.object(db_analysis, "validate_dual_issue_metrics"):
analyzer.calc_expressions()
variance_warning_calls = [
warning_call
for warning_call in console_warning_mock.call_args_list
if "Variance corrected for metric:" in warning_call.args[0]
]
assert len(variance_warning_calls) == 1
assert "1.1 - clamped" in variance_warning_calls[0].args[0]
print_noise_clamp_summary_mock.assert_called_once()
assert get_noise_clamp_warnings()["count"] >= 1
# =============================================================================
# _derive_pct_of_peak_values tests
# =============================================================================
class TestDerivePctOfPeakValues:
"""Tests for db_analysis._derive_pct_of_peak_values."""
def _make_values_df(
self,
metric_ids: list[str],
value_names: list[str],
values: list[float],
kernel_names: Optional[list[str]] = None,
):
"""Build a long-format values DataFrame as produced by calc_expressions."""
data = {
"metric_id": metric_ids,
"value_name": value_names,
"value": values,
}
if kernel_names is not None:
data["kernel_name"] = kernel_names
return pd.DataFrame(data)
def test_pct_of_peak_true_metric_appends_percent_of_peak_row(self):
"""A pct_of_peak-enabled metric produces one new Percent of Peak row."""
values_df = self._make_values_df(
metric_ids=["1.1", "1.1"],
value_names=["Avg", "Peak"],
values=[50.0, 200.0],
)
new_rows = db_analysis._derive_pct_of_peak_values({"1.1"}, values_df)
assert len(new_rows) == 1
assert new_rows[0]["value_name"] == "Percent of Peak"
assert new_rows[0]["value"] == pytest.approx(25.0)
def test_multi_kernel_produces_one_row_per_kernel(self):
"""Calling once per kernel produces one Percent of Peak row per kernel."""
kernel_a_df = self._make_values_df(
metric_ids=["1.1", "1.1"],
value_names=["Avg", "Peak"],
values=[100.0, 200.0],
kernel_names=["kernel_a", "kernel_a"],
)
kernel_b_df = self._make_values_df(
metric_ids=["1.1", "1.1"],
value_names=["Avg", "Peak"],
values=[60.0, 300.0],
kernel_names=["kernel_b", "kernel_b"],
)
rows_a = db_analysis._derive_pct_of_peak_values({"1.1"}, kernel_a_df)
rows_b = db_analysis._derive_pct_of_peak_values({"1.1"}, kernel_b_df)
assert len(rows_a) == 1
assert rows_a[0]["value"] == pytest.approx(50.0) # 100/200*100
assert len(rows_b) == 1
assert rows_b[0]["value"] == pytest.approx(20.0) # 60/300*100
def test_pct_of_peak_false_metric_produces_no_pct_row(self):
"""A metric not in pct_of_peak_metric_ids produces no Percent of Peak row."""
values_df = self._make_values_df(
metric_ids=["1.1", "1.1"],
value_names=["Avg", "Peak"],
values=[50.0, 100.0],
)
new_rows = db_analysis._derive_pct_of_peak_values(set(), values_df)
assert new_rows == []
def test_incomplete_data_skips_metric(self):
"""A metric missing Peak or Avg/Value must be skipped gracefully."""
incomplete_cases = [
# Only "Avg" present -- no "Peak" row
self._make_values_df(
metric_ids=["1.1"], value_names=["Avg"], values=[50.0]
),
# Only "Peak" present -- no "Avg" or "Value" row
self._make_values_df(
metric_ids=["1.1"], value_names=["Peak"], values=[100.0]
),
]
for incomplete_values in incomplete_cases:
new_rows = db_analysis._derive_pct_of_peak_values(
{"1.1"}, incomplete_values
)
assert new_rows == []
# =============================================================================
# Dual-issue VALU validation tests
# =============================================================================
def test_validate_dual_issue_metrics_emits_warning_above_peak():
"""Long-format VALU Utilization above peak triggers the dual-issue warning."""
arch_config = make_dual_issue_arch_config("VALU Utilization")
workload_values_df = pd.DataFrame({
"metric_id": ["1.1", "1.1"],
"value_name": ["Value", "Peak"],
"value": [150.0, 100.0],
})
pmc_df = pd.DataFrame({"GRBM_GUI_ACTIVE": [1000]})
with patch("utils.metrics.common.console_warning") as console_warning_mock:
db_analysis.validate_dual_issue_metrics(
pmc_df,
{"gpu_arch": "gfx942"},
workload_values_df,
arch_config,
)
console_warning_mock.assert_called_once()
msg = console_warning_mock.call_args.args[0]
assert "VALU Utilization can go up to 200%" in msg
def test_validate_dual_issue_metrics_silent_below_peak():
"""Below-peak VALU Utilization stays silent."""
arch_config = make_dual_issue_arch_config("VALU Utilization")
workload_values_df = pd.DataFrame({
"metric_id": ["1.1", "1.1"],
"value_name": ["Value", "Peak"],
"value": [80.0, 100.0],
})
pmc_df = pd.DataFrame({"GRBM_GUI_ACTIVE": [1000]})
with patch("utils.metrics.common.console_warning") as console_warning_mock:
db_analysis.validate_dual_issue_metrics(
pmc_df,
{"gpu_arch": "gfx942"},
workload_values_df,
arch_config,
)
console_warning_mock.assert_not_called()
def test_validate_dual_issue_metrics_uses_peak_empirical_fallback():
"""Peak (Empirical) wins when present; falls back to Peak otherwise."""
arch_config = make_dual_issue_arch_config(
"VALU FLOPs (F64)", peak_col="Peak (Empirical)"
)
workload_values_df = pd.DataFrame({
"metric_id": ["1.1", "1.1"],
"value_name": ["Value", "Peak (Empirical)"],
"value": [600.0, 400.0],
})
pmc_df = pd.DataFrame({"GRBM_GUI_ACTIVE": [1000]})
with patch("utils.metrics.common.console_warning") as console_warning_mock:
db_analysis.validate_dual_issue_metrics(
pmc_df,
{"gpu_arch": "gfx942"},
workload_values_df,
arch_config,
)
console_warning_mock.assert_called_once()
msg = console_warning_mock.call_args.args[0]
assert "VALU FLOPs can exceed the peak value" in msg
def test_validate_dual_issue_metrics_appends_valu2_suffix_on_gfx950():
"""gfx950 with non-zero SQ_ACTIVE_INST_VALU2 appends the confirmation."""
arch_config = make_dual_issue_arch_config("VALU Utilization")
workload_values_df = pd.DataFrame({
"metric_id": ["1.1", "1.1"],
"value_name": ["Value", "Peak"],
"value": [150.0, 100.0],
})
pmc_df = pd.DataFrame({"SQ_ACTIVE_INST_VALU2": [1, 2, 3]})
with patch("utils.metrics.common.console_warning") as console_warning_mock:
db_analysis.validate_dual_issue_metrics(
pmc_df,
{"gpu_arch": "gfx950"},
workload_values_df,
arch_config,
)
msg = console_warning_mock.call_args.args[0]
assert "Dual-issue activity detected via SQ_ACTIVE_INST_VALU2 counter" in msg
def test_validate_dual_issue_metrics_skips_non_metric_table_dfs():
"""dfs entries whose dfs_type is not metric_table are ignored."""
arch_config = make_dual_issue_arch_config("VALU Utilization")
arch_config.dfs_type = {201: "raw_csv_table"}
workload_values_df = pd.DataFrame({
"metric_id": ["1.1", "1.1"],
"value_name": ["Value", "Peak"],
"value": [150.0, 100.0],
})
pmc_df = pd.DataFrame({"GRBM_GUI_ACTIVE": [1000]})
with patch("utils.metrics.common.console_warning") as console_warning_mock:
db_analysis.validate_dual_issue_metrics(
pmc_df,
{"gpu_arch": "gfx942"},
workload_values_df,
arch_config,
)
console_warning_mock.assert_not_called()
# =============================================================================
# PC-sampling population
# =============================================================================
def make_pc_sampling_tool_data():
"""Two offsets under two kernels sharing one code object, with counts."""
stall = "ROCPROFILER_PC_SAMPLING_INSTRUCTION_NOT_ISSUED_REASON_WAITCNT"
inst_type = "ROCPROFILER_PC_SAMPLING_INSTRUCTION_TYPE_VALU"
return {
"metadata": {"pid": 42},
"buffer_records": {
"pc_sample_host_trap": [],
"pc_sample_stochastic": [
{
"inst_index": 0,
"record": {
"pc": {"code_object_id": 5, "code_object_offset": 0x10},
"dispatch_id": 0,
"wave_issued": False,
"snapshot": {"stall_reason": stall},
"inst_type": inst_type,
},
},
{
"inst_index": 1,
"record": {
"pc": {"code_object_id": 5, "code_object_offset": 0x20},
"dispatch_id": 1,
"wave_issued": True,
"snapshot": {},
"inst_type": inst_type,
},
},
],
"kernel_dispatch": [
{
"start_timestamp": 0,
"end_timestamp": 0,
"dispatch_info": {
"dispatch_id": 0,
"kernel_id": 100,
"agent_id": {"handle": 1},
},
},
{
"start_timestamp": 0,
"end_timestamp": 0,
"dispatch_info": {
"dispatch_id": 1,
"kernel_id": 101,
"agent_id": {"handle": 1},
},
},
],
},
"strings": {
"pc_sample_instructions": ["v_mov", "v_add"],
"pc_sample_comments": ["/s/a.cpp:1", "/s/a.cpp:2"],
},
"kernel_symbols": [
{
"kernel_id": 100,
"code_object_id": 5,
"kernel_name": "_Z7vecCopyv.kd",
"formatted_kernel_name": "vecCopy",
},
{
"kernel_id": 101,
"code_object_id": 5,
"kernel_name": "vecAdd.kd",
"formatted_kernel_name": "vecAdd",
},
],
"code_objects": [{"code_object_id": 5, "load_base": 0x1000}],
"agents": [],
}
def test_add_pc_sampling_data_no_tool_data_is_noop(db_session):
"""A workload without tool data inserts no rows."""
workload = orm.Workload(name="w", sub_name="s")
db_session.add(workload)
analyzer = db_analysis(MagicMock(), {})
analyzer._pc_sampling_tool_data_per_workload = {"/fake/workload": None}
analyzer.add_pc_sampling_data("/fake/workload", workload, {})
db_session.commit()
assert db_session.query(orm.CodeObjectStore).count() == 0
assert db_session.query(orm.InstructionLine).count() == 0
def test_add_pc_sampling_data_populates_and_attributes_kernels(db_session):
"""Instruction lines are inserted, attributed to their dispatch kernel, and
the code object records pid/load_base."""
workload_path = "/fake/workload"
workload = orm.Workload(name="w", sub_name="s")
db_session.add(workload)
kernel_objs = {
"vecCopy": orm.Kernel(kernel_name="vecCopy", workload=workload),
"vecAdd": orm.Kernel(kernel_name="vecAdd", workload=workload),
}
for kernel in kernel_objs.values():
db_session.add(kernel)
analyzer = db_analysis(MagicMock(), {})
analyzer._pc_sampling_tool_data_per_workload = {
workload_path: make_pc_sampling_tool_data()
}
analyzer.add_pc_sampling_data(workload_path, workload, kernel_objs)
db_session.commit()
code_object = db_session.query(orm.CodeObjectStore).one()
assert code_object.pid == 42
assert code_object.load_base == 0x1000
lines = db_session.query(orm.InstructionLine).all()
kernel_by_offset = {
line.code_object_offset: line.kernel.kernel_name for line in lines
}
assert kernel_by_offset == {0x10: "vecCopy", 0x20: "vecAdd"}
# The stalled sample carries a stall-reason count; both carry an inst type.
stalled = next(line for line in lines if line.code_object_offset == 0x10)
assert stalled.pc_sample_state.stall_count == 1
assert {
r.stall_reason_lookup.text for r in stalled.pc_sample_state.stall_reasons
} == {"WAITCNT"}
def test_add_pc_sampling_data_drops_lines_without_kernel(db_session):
"""Lines whose kernel is absent from kernel_objs (filtered out) are dropped
along with their sample state and child counts, not attributed to no kernel."""
workload_path = "/fake/workload"
workload = orm.Workload(name="w", sub_name="s")
db_session.add(workload)
# Only vecCopy survives filtering; vecAdd's line must be dropped.
kernel_objs = {"vecCopy": orm.Kernel(kernel_name="vecCopy", workload=workload)}
db_session.add(kernel_objs["vecCopy"])
analyzer = db_analysis(MagicMock(), {})
analyzer._pc_sampling_tool_data_per_workload = {
workload_path: make_pc_sampling_tool_data()
}
analyzer.add_pc_sampling_data(workload_path, workload, kernel_objs)
db_session.commit()
lines = db_session.query(orm.InstructionLine).all()
assert [line.code_object_offset for line in lines] == [0x10]
assert all(line.kernel is not None for line in lines)
# No orphaned child rows for the dropped line.
assert db_session.query(orm.PCSampleState).count() == 1
# =============================================================================
# Code-object ISA ingestion (add_code_object_isa)
# =============================================================================
def make_disasm_code_object(code_object_id, instructions, symbol_name="sym"):
"""Build one code_obj_info code object with a single symbol."""
return {
"id": code_object_id,
"symbols": [{"name": symbol_name, "instructions": instructions}],
}
def test_add_code_object_isa_adds_unsampled_lines(db_session):
"""Un-sampled instructions of a dispatched kernel are added and attributed
via the mangled-name join; a disassembly offset that matches an already-
sampled offset inserts no duplicate row; ISA of an un-dispatched symbol is
not stored at all; a non-surviving pid's disassembly is skipped."""
workload_path = common.get_output_dir()
Path(workload_path).mkdir(parents=True, exist_ok=True)
load_base = 0x1000
try:
# 0x1010 -> offset 0x10 is already sampled; 0x1030 -> offset 0x30 is new
# and joins vecCopy through its mangled ELF name; 0x1040 -> offset 0x40
# belongs to a symbol never dispatched, so it is dropped entirely.
code_objects = [
{
"id": 5,
"symbols": [
{
"name": "_Z7vecCopyv",
"instructions": [
{
"virtual_address": load_base + 0x10,
"name": "v_mov",
"comment": "",
},
{
"virtual_address": load_base + 0x30,
"name": "s_nop",
"comment": "c",
},
],
},
{
"name": "_Z7unknownv",
"instructions": [
{
"virtual_address": load_base + 0x40,
"name": "s_endpgm",
"comment": "",
}
],
},
],
}
]
(Path(workload_path) / "42_code_obj_info.json").write_text(
json.dumps({"code_objects": code_objects}), encoding="utf-8"
)
# A non-surviving pid (metadata pid is 42) reuses code_object_id 5; its
# 0x50 offset must never be stored since only pid 42's load_base is known.
(Path(workload_path) / "99_code_obj_info.json").write_text(
json.dumps({
"code_objects": [
make_disasm_code_object(
5,
[
{
"virtual_address": load_base + 0x50,
"name": "s_nop",
"comment": "",
}
],
symbol_name="_Z7vecCopyv",
)
]
}),
encoding="utf-8",
)
workload = orm.Workload(name="w", sub_name="s")
db_session.add(workload)
kernel_objs = {
"vecCopy": orm.Kernel(kernel_name="vecCopy", workload=workload),
"vecAdd": orm.Kernel(kernel_name="vecAdd", workload=workload),
}
for kernel in kernel_objs.values():
db_session.add(kernel)
analyzer = db_analysis(MagicMock(), {})
analyzer._pc_sampling_tool_data_per_workload = {
workload_path: make_pc_sampling_tool_data()
}
analyzer.add_pc_sampling_data(workload_path, workload, kernel_objs)
analyzer.add_code_object_isa(workload_path, workload, kernel_objs)
db_session.commit()
lines = db_session.query(orm.InstructionLine).all()
by_offset = {line.code_object_offset: line for line in lines}
# Two sampled offsets + one new un-sampled offset; the un-dispatched
# symbol's 0x40 line and the non-surviving pid's 0x50 line are dropped,
# and 0x10 is not duplicated.
assert set(by_offset) == {0x10, 0x20, 0x30}
# Only the surviving pid (42) is stored.
assert {store.pid for store in db_session.query(orm.CodeObjectStore)} == {42}
# The disassembly-only line joins its kernel and carries no sample state.
isa_line = by_offset[0x30]
assert isa_line.kernel.kernel_name == "vecCopy"
assert isa_line.pc_sample_state is None
assert isa_line.instruction == "s_nop"
# The sampled line at 0x10 kept its kernel attribution and sample state.
assert by_offset[0x10].kernel.kernel_name == "vecCopy"
assert by_offset[0x10].pc_sample_state is not None
# Both belong to the same (reused) code object store.
assert isa_line.code_object_store is by_offset[0x10].code_object_store
finally:
common.clean_output_dir(True, workload_path)
def test_add_code_object_isa_creates_store_for_unsampled_code_object(db_session):
"""A dispatched-but-never-sampled code object present only in code_obj_info
gets a new store, using the load_base from the results' code_objects list,
and its ISA is attributed to the dispatched kernel."""
workload_path = common.get_output_dir()
Path(workload_path).mkdir(parents=True, exist_ok=True)
try:
tool_data = make_pc_sampling_tool_data()
# code object 9 has a load_base and a dispatched kernel, but no samples.
tool_data["code_objects"].append({"code_object_id": 9, "load_base": 0x2000})
tool_data["kernel_symbols"].append({
"kernel_id": 102,
"code_object_id": 9,
"kernel_name": "_Z6helperv.kd",
"formatted_kernel_name": "helper",
})
code_objects = [
make_disasm_code_object(
9,
[{"virtual_address": 0x2000 + 0x8, "name": "s_endpgm", "comment": ""}],
symbol_name="_Z6helperv",
)
]
(Path(workload_path) / "42_code_obj_info.json").write_text(
json.dumps({"code_objects": code_objects}), encoding="utf-8"
)
workload = orm.Workload(name="w", sub_name="s")
db_session.add(workload)
helper = orm.Kernel(kernel_name="helper", workload=workload)
db_session.add(helper)
analyzer = db_analysis(MagicMock(), {})
analyzer._pc_sampling_tool_data_per_workload = {workload_path: tool_data}
analyzer.add_pc_sampling_data(workload_path, workload, {"helper": helper})
analyzer.add_code_object_isa(workload_path, workload, {"helper": helper})
db_session.commit()
store = db_session.query(orm.CodeObjectStore).filter_by(code_object_id=9).one()
assert store.pid == 42
assert store.load_base == 0x2000
line = (
db_session
.query(orm.InstructionLine)
.filter_by(code_object_offset=0x8)
.one()
)
assert line.code_object_store is store
assert line.kernel is helper
finally:
common.clean_output_dir(True, workload_path)
def test_add_code_object_isa_skips_code_object_without_load_base(db_session):
"""A code object with no known load_base cannot be offset-mapped, so it is
skipped rather than stored with an inconsistent offset."""
workload_path = common.get_output_dir()
Path(workload_path).mkdir(parents=True, exist_ok=True)
try:
tool_data = make_pc_sampling_tool_data()
tool_data["code_objects"].append({"code_object_id": 9, "load_base": None})
tool_data["kernel_symbols"].append({
"kernel_id": 102,
"code_object_id": 9,
"kernel_name": "_Z6helperv.kd",
"formatted_kernel_name": "helper",
})
code_objects = [
make_disasm_code_object(
9,
[{"virtual_address": 0x500, "name": "s_endpgm", "comment": ""}],
symbol_name="_Z6helperv",
)
]
(Path(workload_path) / "42_code_obj_info.json").write_text(
json.dumps({"code_objects": code_objects}), encoding="utf-8"
)
workload = orm.Workload(name="w", sub_name="s")
db_session.add(workload)
helper = orm.Kernel(kernel_name="helper", workload=workload)
db_session.add(helper)
analyzer = db_analysis(MagicMock(), {})
analyzer._pc_sampling_tool_data_per_workload = {workload_path: tool_data}
analyzer.add_pc_sampling_data(workload_path, workload, {"helper": helper})
analyzer.add_code_object_isa(workload_path, workload, {"helper": helper})
db_session.commit()
# The store exists (its kernel was dispatched) but no ISA line was added.
store = db_session.query(orm.CodeObjectStore).filter_by(code_object_id=9).one()
assert store.instruction_lines == []
finally:
common.clean_output_dir(True, workload_path)