Files
rocm-systems/projects/rocprofiler-compute/src/utils/parser.py
T

1795 строки
68 KiB
Python
Исходник Обычный вид История

2025-08-01 10:14:39 -06:00
##############################################################################
# MIT License
#
2025-01-23 13:09:32 -06:00
# Copyright (c) 2021 - 2025 Advanced Micro Devices, Inc. All Rights Reserved.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
2025-08-01 10:14:39 -06:00
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
2025-08-01 10:14:39 -06:00
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
2025-08-01 10:14:39 -06:00
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
##############################################################################
import ast
import json
import multiprocessing
import re
import sys
2024-01-30 17:25:16 -06:00
import warnings
from collections import defaultdict
2025-01-02 13:29:47 -08:00
from pathlib import Path
from typing import Union
2025-01-02 13:29:47 -08:00
import astunparse
import numpy as np
2025-01-02 13:29:47 -08:00
import pandas as pd
from utils import schema
from utils.logger import console_debug, console_error, console_warning, demarcate
# ------------------------------------------------------------------------------
# Internal global definitions
# NB:
# Ammolite is unique gemstone from the Rocky Mountains.
# "ammolite__" is a special internal prefix to mark build-in global variables
# calculated or parsed from raw data sources. Its range is only in this file.
# Any other general prefixes string, like "buildin__", might be used by the
# editor. Whenever change it to a new one, replace all appearances in this file.
# 001 is ID of pmc_kernel_top.csv table
pmc_kernel_top_table_id = 1
# Build-in $denom defined in mongodb query:
# "denom": {
# "$switch" : {
# "branches": [
# {
# "case": { "$eq": [ $normUnit, "per Wave"]} ,
# "then": "&SQ_WAVES"
# },
# {
# "case": { "$eq": [ $normUnit, "per Cycle"]} ,
# "then": "&GRBM_GUI_ACTIVE"
# },
# {
# "case": { "$eq": [ $normUnit, "per Sec"]} ,
2025-08-08 15:32:30 -04:00
# "then": {"$divide":[{"$subtract": ["&End_Timestamp",
# "&Start_Timestamp" ]},
# 1000000000]}
# }
# }
supported_denom = {
"per_wave": "SQ_WAVES",
2024-02-27 13:41:35 -05:00
"per_cycle": "$GRBM_GUI_ACTIVE_PER_XCD",
"per_second": "((End_Timestamp - Start_Timestamp) / 1000000000)",
"per_kernel": "1",
}
# Build-in defined in mongodb variables:
build_in_vars = {
2024-02-27 13:52:44 -05:00
"GRBM_GUI_ACTIVE_PER_XCD": "(GRBM_GUI_ACTIVE / $num_xcd)",
"GRBM_COUNT_PER_XCD": "(GRBM_COUNT / $num_xcd)",
2024-03-01 11:52:31 -06:00
"GRBM_SPI_BUSY_PER_XCD": "(GRBM_SPI_BUSY / $num_xcd)",
2025-08-08 15:32:30 -04:00
"numActiveCUs": "TO_INT(MIN((((ROUND(AVG(((4 * SQ_BUSY_CU_CYCLES) / \
$GRBM_GUI_ACTIVE_PER_XCD)), 0) / $max_waves_per_cu) * 8) + \
MIN(MOD(ROUND(AVG(((4 * SQ_BUSY_CU_CYCLES) / \
$GRBM_GUI_ACTIVE_PER_XCD)), 0), $max_waves_per_cu), 8)), $cu_per_gpu))",
"kernelBusyCycles": "ROUND(AVG((((End_Timestamp - Start_Timestamp) / \
1000) * $max_sclk)), 0)",
2025-04-03 02:21:18 -04:00
"hbmBandwidth": "($max_mclk / 1000 * 32 * $num_hbm_channels)",
}
supported_call = {
2025-08-08 15:32:30 -04:00
# If the below has a single arg, like(expr), it is an aggr,
# in which case it turns into a pandas function.
# If it has args like a list [], it turns into a Python function.
"MIN": "to_min",
"MAX": "to_max",
# simple aggr
"AVG": "to_avg",
"MEDIAN": "to_median",
"STD": "to_std",
# functions apply to whole column of df or a single value
"TO_INT": "to_int",
"SUM": "to_sum",
# Support the below with 2 inputs
"ROUND": "to_round",
"QUANTILE": "to_quantile",
"MOD": "to_mod",
# Concat operation from the memory chart "active cus"
"CONCAT": "to_concat",
}
PC_SAMPLING_NOT_ISSUE_PREFIX = "ROCPROFILER_PC_SAMPLING_INSTRUCTION_NOT_ISSUED_REASON_"
# ------------------------------------------------------------------------------
def to_min(*args):
if len(args) == 1 and isinstance(args[0], pd.core.series.Series):
return args[0].min()
elif min(args) == None:
return np.nan
else:
return min(args)
def to_max(*args):
if len(args) == 1 and isinstance(args[0], pd.core.series.Series):
return args[0].max()
elif len(args) == 2 and (
isinstance(args[0], pd.core.series.Series)
or isinstance(args[1], pd.core.series.Series)
):
return np.maximum(args[0], args[1])
elif max(args) == None:
return np.nan
else:
return max(args)
def to_avg(a):
if str(type(a)) == "<class 'NoneType'>":
return np.nan
elif isinstance(a, pd.core.series.Series):
2025-08-01 11:31:43 -04:00
if a.empty:
return np.nan
elif np.isnan(a).all():
return np.nan
else:
return a.mean()
elif isinstance(a, (np.ndarray, list)):
arr = np.array(a)
if arr.size == 0:
return np.nan
elif np.isnan(arr).all():
return np.nan
else:
return np.nanmean(arr)
elif isinstance(a, (int, float, np.number)):
if np.isnan(a):
return np.nan
else:
return float(a)
else:
2025-08-01 11:31:43 -04:00
raise Exception(f"to_avg: unsupported type: {type(a)}")
def to_median(a):
if a is None:
return None
elif isinstance(a, pd.core.series.Series):
with warnings.catch_warnings():
2024-03-01 18:00:17 -06:00
warnings.simplefilter("ignore", category=RuntimeWarning)
return a.median()
else:
raise Exception("to_median: unsupported type.")
def to_std(a):
if isinstance(a, pd.core.series.Series):
return a.std()
else:
raise Exception("to_std: unsupported type.")
def to_int(a):
if str(type(a)) == "<class 'NoneType'>":
return None
elif isinstance(a, (int, float, np.int64)):
return int(a)
elif isinstance(a, pd.core.series.Series):
return a.astype("Int64")
# Do we need it?
# elif isinstance(a, str):
# return int(a)
else:
raise Exception("to_int: unsupported type.")
def to_sum(a):
if str(type(a)) == "<class 'NoneType'>":
return np.nan
elif np.isnan(a).all():
return np.nan
elif a.empty:
return np.nan
elif isinstance(a, pd.core.series.Series):
return a.sum()
else:
raise Exception("to_sum: unsupported type.")
def to_round(a, b):
if isinstance(a, pd.core.series.Series):
return a.round(b)
else:
return round(a, b)
2024-02-16 15:34:28 -06:00
def to_quantile(a, b):
if a is None:
return None
elif isinstance(a, pd.core.series.Series):
return a.quantile(b)
else:
raise Exception("to_quantile: unsupported type.")
2024-02-16 15:34:28 -06:00
def to_mod(a, b):
if isinstance(a, pd.core.series.Series):
return a.mod(b)
else:
return a % b
def to_concat(a, b):
return str(a) + str(b)
class CodeTransformer(ast.NodeTransformer):
"""
Python AST visitor to transform user defined equation string to df format
"""
def visit_Call(self, node):
self.generic_visit(node)
# print("--- debug visit_Call --- ", node.args, node.func)
# print(astunparse.dump(node))
# print(astunparse.unparse(node))
if isinstance(node.func, ast.Name):
if node.func.id in supported_call:
node.func.id = supported_call[node.func.id]
else:
raise Exception(
"Unknown call:", node.func.id
) # Could be removed if too strict
return node
def visit_IfExp(self, node):
self.generic_visit(node)
2025-08-08 15:32:30 -04:00
# print(
# "visit_IfExp",
# type(node.test),
# type(node.body),
# type(node.orelse),
# dir(node),
# )
if isinstance(node.body, ast.Num):
raise Exception(
2025-08-08 15:32:30 -04:00
"Don't support body of IF with number only! Has to be expr with "
"df['column']."
)
new_node = ast.Expr(
value=ast.Call(
func=ast.Attribute(value=node.body, attr="where", ctx=ast.Load()),
args=[node.test, node.orelse],
keywords=[],
)
)
# print("-------------")
# print(astunparse.dump(new_node))
# print("-------------")
return new_node
# NB:
# visit_Name is for replacing HW counter to its df expr. In this way, we
# could support any HW counter names, which is easier than regex.
#
# There are 2 limitations:
# - It is not straightforward to support types other than simple column
# in df, such as [], (). If we need to support those, have to implement
# in correct way or work around.
# - The 'raw_pmc_df' is hack code. For other data sources, like wavefront
# data,We need to think about template or pass it as a parameter.
def visit_Name(self, node):
self.generic_visit(node)
# print("-------------", node.id)
if (not node.id.startswith("ammolite__")) and (not node.id in supported_call):
new_node = ast.Subscript(
value=ast.Name(id="raw_pmc_df", ctx=ast.Load()),
slice=ast.Index(value=ast.Str(s=node.id)),
ctx=ast.Load(),
)
node = new_node
return node
def build_eval_string(equation, coll_level, config):
"""
2025-08-08 15:32:30 -04:00
Convert user defined equation string to eval executable string.
For example,
2025-08-08 15:32:30 -04:00
input:
AVG(100 * SQ_ACTIVE_INST_SCA / ( GRBM_GUI_ACTIVE * $numCU ))
output:
to_avg(
100 * raw_pmc_df["pmc_perf"]["SQ_ACTIVE_INST_SCA"] /
(
raw_pmc_df["pmc_perf"]["GRBM_GUI_ACTIVE"] *
numCU
)
)
input:
AVG(
(
TCC_EA_RDREQ_LEVEL_31 / TCC_EA_RDREQ_31
)
if (TCC_EA_RDREQ_31 != 0)
else (0)
)
output:
to_avg(
(
raw_pmc_df["pmc_perf"]["TCC_EA_RDREQ_LEVEL_31"] /
raw_pmc_df["pmc_perf"]["TCC_EA_RDREQ_31"]
).where(
raw_pmc_df["pmc_perf"]["TCC_EA_RDREQ_31"] != 0,
0
)
)
We can not handle the below for now:
input:
AVG(
(
0
if (TCC_EA_RDREQ_31 == 0)
else (
TCC_EA_RDREQ_LEVEL_31 /
TCC_EA_RDREQ_31
)
)
)
But potential workaround is:
output:
to_avg(
raw_pmc_df["pmc_perf"]["TCC_EA_RDREQ_31"].where(
raw_pmc_df["pmc_perf"]["TCC_EA_RDREQ_31"] == 0,
raw_pmc_df["pmc_perf"]["TCC_EA_RDREQ_LEVEL_31"] /
raw_pmc_df["pmc_perf"]["TCC_EA_RDREQ_31"]
)
)
"""
if coll_level is None:
raise Exception("Error: coll_level can not be None.")
if not equation:
return ""
s = str(equation)
# print("input:", s)
# build-in variable starts with '$', python can not handle it.
# replace '$' with 'ammolite__'.
# TODO: pre-check there is no "ammolite__" in all config files.
2024-02-21 16:47:32 -06:00
s = re.sub(r"\$", "ammolite__", s)
# convert equation string to intermediate expression in df array format
ast_node = ast.parse(s)
# print(astunparse.dump(ast_node))
transformer = CodeTransformer()
transformer.visit(ast_node)
s = astunparse.unparse(ast_node)
# correct column name/label in df with [], such as TCC_HIT[0],
# the target is df['TCC_HIT[0]']
s = re.sub(r"\'\]\[(\d+)\]", r"[\g<1>]']", s)
2025-08-01 11:31:43 -04:00
# print("--- intermediate string: ", s)
# apply coll_level
if config.get("format_rocprof_output") == "rocpd":
# Replace SQ_ACCUM_PREV_HIRES with coll_level_ACCUM then ignore coll_level df
2025-08-08 15:32:30 -04:00
s = re.sub("SQ_ACCUM_PREV_HIRES", f"{coll_level}_ACCUM", s)
s = re.sub(
r"raw_pmc_df", "raw_pmc_df['" + schema.pmc_perf_file_prefix + "']", s
)
else:
s = re.sub(r"raw_pmc_df", "raw_pmc_df['" + coll_level + "']", s)
# print("--- build_eval_string, return: ", s)
return s
def update_denom_string(equation, unit):
"""
Update $denom in equation with runtime normalization unit.
"""
if not equation:
return ""
s = str(equation)
if unit in supported_denom.keys():
s = re.sub(r"\$denom", supported_denom[unit], s)
return s
def update_normUnit_string(equation, unit):
"""
Update $normUnit in equation with runtime normalization unit.
It is string replacement for display only.
"""
# TODO: We might want to do it for subtitle contains $normUnit
if not equation:
return ""
return re.sub(
r"\((?P<PREFIX>\w*)\s+\+\s+(\$normUnit\))",
r"\g<PREFIX> " + re.sub("_", " ", unit),
str(equation),
).capitalize()
def gen_counter_list(formula):
function_filter = {
"MIN": None,
"MAX": None,
"AVG": None,
"ROUND": None,
"TO_INT": None,
"GB": None,
"STD": None,
"GFLOP": None,
"GOP": None,
"OP": None,
"CU": None,
"NC": None,
"UC": None,
"CC": None,
"RW": None,
"GIOP": None,
"GFLOPs": None,
"CONCAT": None,
"MOD": None,
}
built_in_counter = [
"LDS_Per_Workgroup",
"Grid_Size",
"Workgroup_Size",
"Arch_VGPR",
"Accum_VGPR",
"SGPR",
"Scratch_Per_Workitem",
"Start_Timestamp",
"End_Timestamp",
]
visited = False
counters = []
if not isinstance(formula, str):
return visited, counters
try:
tree = ast.parse(
formula.replace("$normUnit", "SQ_WAVES")
.replace("$denom", "SQ_WAVES")
.replace(
"$numActiveCUs",
2025-08-08 15:32:30 -04:00
"TO_INT(MIN((((ROUND(AVG(((4 * SQ_BUSY_CU_CYCLES) / "
"$GRBM_GUI_ACTIVE_PER_XCD})), 0) / $maxWavesPerCU) * 8) + "
"MIN(MOD(ROUND(AVG(((4 * SQ_BUSY_CU_CYCLES) / "
"$GRBM_GUI_ACTIVE_PER_XCD)), 0), $maxWavesPerCU), 8)), $numCU))",
)
.replace("$", "")
)
for node in ast.walk(tree):
if isinstance(node, ast.Name):
2025-08-08 15:32:30 -04:00
val = (
str(node.id)[:-4] if str(node.id).endswith("_sum") else str(node.id)
)
if val.isupper() and val not in function_filter:
counters.append(val)
visited = True
if val in built_in_counter:
visited = True
2025-08-08 15:32:30 -04:00
except Exception:
pass
return visited, counters
2024-02-16 15:34:28 -06:00
def calc_builtin_var(var, sys_info):
"""
Calculate build-in variable based on sys_info:
"""
if isinstance(var, int):
return var
elif isinstance(var, str) and var.startswith("$total_l2_chan"):
return sys_info.total_l2_chan
else:
2024-03-04 12:57:25 -06:00
console_error('Built-in var " %s " is not supported' % var)
2024-02-16 15:34:28 -06:00
@demarcate
def build_dfs(archConfigs, filter_metrics, sys_info):
"""
- Build dataframe for each type of data source within each panel.
Each dataframe will be used as a template to load data with each run later.
For now, support "metric_table" and "raw_csv_table". Otherwise, put an empty df.
- Collect/build metric_list to suport customrized metrics profiling.
"""
# TODO: more error checking for filter_metrics!!
# if filter_metrics:
# for metric in filter_metrics:
# if not metric in avail_ip_blocks:
# print("{} is not a valid metric to filter".format(metric))
# exit(1)
simple_box = {
"Min": ["MIN(", ")"],
"Q1": ["QUANTILE(", ", 0.25)"],
"Median": ["MEDIAN(", ")"],
"Q3": ["QUANTILE(", ", 0.75)"],
"Max": ["MAX(", ")"],
}
d = {}
metric_list = {}
dfs_type = {}
metric_counters = {}
for panel_id, panel in archConfigs.panel_configs.items():
for data_source in panel["data source"]:
for type, data_config in data_source.items():
if (
type == "metric_table"
and "metric" in data_config
and "placeholder_range" in data_config["metric"]
):
# print(data_config["metric"])
new_metrics = {}
if sys_info is not None:
# NB: support single placeholder for now!!
p_range = data_config["metric"].pop("placeholder_range")
metric, metric_expr = data_config["metric"].popitem()
# print(len(data_config["metric"]))
# data_config['metric'].clear()
for p, r in p_range.items():
# NB: We have to resolve placeholder range first if it
# is a build-in var. It will be too late to do it in
# eval_metric(). This is the only reason we need
# sys_info at this stage.
var = calc_builtin_var(r, sys_info)
for i in range(var):
new_key = metric.replace(p, str(i))
new_val = {}
for k, v in metric_expr.items():
new_val[k] = metric_expr[k].replace(p, str(i))
# print(new_val)
new_metrics[new_key] = new_val
# print(p_range)
# print(new_metrics)
data_config["metric"] = new_metrics
# print(data_config)
# print(data_config["metric"])
2024-02-16 15:34:28 -06:00
for panel_id, panel in archConfigs.panel_configs.items():
for data_source in panel["data source"]:
for type, data_config in data_source.items():
if type == "metric_table":
headers = ["Metric_ID"]
2024-01-19 15:29:26 -06:00
data_source_idx = str(data_config["id"] // 100)
if data_source_idx != 0 or (
filter_metrics and data_source_idx in filter_metrics
):
2024-01-19 15:29:26 -06:00
metric_list[data_source_idx] = panel["title"]
if (
"cli_style" in data_config
and data_config["cli_style"] == "simple_box"
):
headers.append(data_config["header"]["metric"])
for k in simple_box.keys():
headers.append(k)
for key, tile in data_config["header"].items():
2025-07-25 14:01:34 -04:00
if key != "metric" and key != "expr":
headers.append(tile)
else:
2025-07-25 14:01:34 -04:00
headers.append(data_config["header"]["metric"])
for key, tile in data_config["header"].items():
2025-07-25 14:01:34 -04:00
if key != "metric":
headers.append(tile)
headers.append("coll_level")
2025-07-25 14:01:34 -04:00
# Only add Metrics Description column if it is defined in the panel
if "metrics_description" in panel:
headers.append("Description")
2024-02-16 15:34:28 -06:00
df = pd.DataFrame(columns=headers)
2024-02-16 15:34:28 -06:00
i = 0
if not data_config["metric"]:
data_source_idx = (
str(data_config["id"] // 100)
+ "."
+ str(data_config["id"] % 100)
)
metric_idx = data_source_idx + "." + str(i)
metric_list[data_source_idx] = data_config["title"]
for key, entries in data_config["metric"].items():
data_source_idx = (
str(data_config["id"] // 100)
+ "."
+ str(data_config["id"] % 100)
)
metric_idx = data_source_idx + "." + str(i)
values = []
eqn_content = []
if (
(not filter_metrics)
2024-03-15 16:40:13 -05:00
or (
metric_idx in filter_metrics
) # no filter # metric in filter
or
# the whole table in filter
(data_source_idx in filter_metrics)
or
# the whole IP block in filter
(str(panel_id // 100) in filter_metrics)
):
values.append(metric_idx)
values.append(key)
2024-02-16 15:34:28 -06:00
2024-01-19 15:29:26 -06:00
metric_list[data_source_idx] = data_config["title"]
if (
"cli_style" in data_config
and data_config["cli_style"] == "simple_box"
):
# print("~~~~~~~~~~~~~~~~~")
# print(entries)
# print("~~~~~~~~~~~~~~~~~")
for k, v in entries.items():
if k == "expr":
for bk, bv in simple_box.items():
values.append(bv[0] + v + bv[1])
else:
2025-07-25 14:01:34 -04:00
if k != "coll_level" and k != "alias":
values.append(v)
else:
for k, v in entries.items():
2025-07-25 14:01:34 -04:00
if k != "coll_level" and k != "alias":
values.append(v)
eqn_content.append(v)
if "alias" in entries.keys():
values.append(entries["alias"])
if "coll_level" in entries.keys():
values.append(entries["coll_level"])
else:
values.append(schema.pmc_perf_file_prefix)
2025-07-25 14:01:34 -04:00
if "metrics_description" in panel:
if key in panel["metrics_description"]:
values.append(panel["metrics_description"][key])
else:
values.append("")
# print(headers, values)
# print(key, entries)
df_new_row = pd.DataFrame([values], columns=headers)
df = pd.concat([df, df_new_row])
# collect metric_list
metric_list[metric_idx] = key
# generate mapping of counters and metrics
filter = {}
_visited = False
for formula in eqn_content:
if formula is not None and formula != "None":
visited, counters = gen_counter_list(formula)
if visited:
_visited = True
for k in counters:
filter[k] = None
if len(filter) > 0 or _visited:
metric_counters[key] = list(filter)
i += 1
df.set_index("Metric_ID", inplace=True)
# df.set_index('Metric', inplace=True)
# print(tabulate(df, headers='keys', tablefmt='fancy_grid'))
elif type == "raw_csv_table":
data_source_idx = str(data_config["id"] // 100)
if (
(not filter_metrics)
or (data_source_idx == "0") # no filter
or (data_source_idx in filter_metrics)
):
2025-08-08 15:32:30 -04:00
if "columnwise" in data_config and data_config["columnwise"]:
df = pd.DataFrame(
[data_config["source"]], columns=["from_csv_columnwise"]
)
else:
df = pd.DataFrame(
[data_config["source"]], columns=["from_csv"]
)
metric_list[data_source_idx] = panel["title"]
else:
df = pd.DataFrame()
elif type == "pc_sampling_table":
data_source_idx = str(data_config["id"] // 100)
2025-06-09 11:29:10 -06:00
df = pd.DataFrame(
[data_config["source"]], columns=["from_pc_sampling"]
)
metric_list[data_source_idx] = panel["title"]
else:
df = pd.DataFrame()
d[data_config["id"]] = df
dfs_type[data_config["id"]] = type
setattr(archConfigs, "dfs", d)
setattr(archConfigs, "metric_list", metric_list)
setattr(archConfigs, "dfs_type", dfs_type)
setattr(archConfigs, "metric_counters", metric_counters)
def build_metric_value_string(dfs, dfs_type, normal_unit, profiling_config):
"""
Apply the real eval string to its field in the metric_table df.
"""
for id, df in dfs.items():
if dfs_type[id] == "metric_table":
for expr in df.columns:
if expr in schema.supported_field:
# NB: apply all build-in before building the whole string
df[expr] = df[expr].apply(update_denom_string, unit=normal_unit)
# NB: there should be a faster way to do with single apply
if not df.empty:
for i in range(df.shape[0]):
row_idx_label = df.index.to_list()[i]
# print(i, "row_idx_label", row_idx_label, expr)
if expr.lower() != "alias":
df.at[row_idx_label, expr] = build_eval_string(
df.at[row_idx_label, expr],
df.at[row_idx_label, "coll_level"],
profiling_config,
)
elif expr.lower() == "unit" or expr.lower() == "units":
df[expr] = df[expr].apply(update_normUnit_string, unit=normal_unit)
# print(tabulate(df, headers='keys', tablefmt='fancy_grid'))
def init_metric_evaluator(
raw_pmc_df: Union[pd.DataFrame, dict], ammolite_vars: dict, empirical_peaks: dict
) -> None:
if isinstance(raw_pmc_df, dict):
raw_pmc_df_keys = set(raw_pmc_df.keys())
elif isinstance(raw_pmc_df, pd.DataFrame):
raw_pmc_df_keys = set(raw_pmc_df.columns.get_level_values(0))
else:
raise ValueError(f"Unknown `raw_pmc_df` type '{type(raw_pmc_df)}'.")
raw_pmc_df_items = {f"raw_pmc_df_{key}": raw_pmc_df[key] for key in raw_pmc_df_keys}
# The globals here are not shared across all processes,
# they exist only within the subprocess's context,
# and their lifetime ends when the process terminates.
# The process-local globals are used for performance optimization.
globals().update(raw_pmc_df_items)
globals().update(ammolite_vars)
globals().update(empirical_peaks)
def run_metric_evaluator(row_expr: str) -> str:
try:
# cache dataframes of 'raw_pmc_df'
# this may replace some KeyErrors with NameErrors
# e.g. row_pmc_df['key'] -> row_pmc_df_key will throw NameError now
row_expr = re.sub(r"raw_pmc_df\['(.*?)'\]", r"raw_pmc_df_\1", row_expr)
out = eval(compile(row_expr, "<string>", "eval"))
if np.isnan(out):
return ""
else:
return out
except (TypeError, NameError, KeyError) as e:
if "empirical_peak" in str(e):
console_warning(f"Missing empirical peak data: {e}. Using empty value.")
return ""
else:
return ""
except AttributeError as ae:
if str(ae) == "'NoneType' object has no attribute 'get'":
return ""
else:
console_error("analysis", str(ae))
def create_empirical_peaks_dict(empirical_peaks_df):
"""Create empirical peaks dictionary"""
empirical_peaks = {}
if not empirical_peaks_df.empty:
peak_data_row = empirical_peaks_df.iloc[0]
for col in empirical_peaks_df.columns:
empirical_peaks[f"ammolite__{col}_empirical_peak"] = peak_data_row[col]
else:
peak_names = [
"FP16Flops",
"FP32Flops",
"FP64Flops",
"MFMAF64Flops",
"MFMAF32Flops",
"MFMAF16Flops",
"MFMABF16Flops",
"MFMAF8Flops",
"MFMAI8Ops",
"HBMBw",
"L2Bw",
"L1Bw",
"LDSBw",
"MFMA_FLOPs_F6F4",
]
# initialize peaks to 0
for peak_name in peak_names:
empirical_peaks[f"ammolite__{peak_name}_empirical_peak"] = 0
return empirical_peaks
@demarcate
def eval_metric(dfs, dfs_type, sys_info, empirical_peaks_df, raw_pmc_df, debug, config):
"""
Execute the expr string for each metric in the df.
"""
# confirm no illogical counter values (only consider non-roofline runs)
roof_only_run = sys_info.ip_blocks == "roofline"
if (
(not roof_only_run)
and hasattr(raw_pmc_df["pmc_perf"], "GRBM_GUI_ACTIVE")
and (raw_pmc_df["pmc_perf"]["GRBM_GUI_ACTIVE"] == 0).any()
):
2024-01-30 17:25:16 -06:00
console_warning("Dectected GRBM_GUI_ACTIVE == 0")
console_error("Hauting execution for warning above.")
2025-03-06 12:52:54 -07:00
ammolite__se_per_gpu = int(sys_info.se_per_gpu)
2025-04-03 02:21:18 -04:00
if np.isnan(ammolite__se_per_gpu) or ammolite__se_per_gpu == 0:
console_warning(
2025-08-08 15:32:30 -04:00
"se_per_gpu is not available in sysinfo.csv, please provide the correct "
"value using --specs-correction"
2025-04-03 02:21:18 -04:00
)
2025-03-06 12:52:54 -07:00
ammolite__pipes_per_gpu = int(sys_info.pipes_per_gpu)
2025-04-03 02:21:18 -04:00
if np.isnan(ammolite__pipes_per_gpu) or ammolite__pipes_per_gpu == 0:
console_warning(
2025-08-08 15:32:30 -04:00
"pipes_per_gpu is not available in sysinfo.csv, please provide the correct "
"value using --specs-correction"
2025-04-03 02:21:18 -04:00
)
2025-03-06 12:52:54 -07:00
ammolite__cu_per_gpu = int(sys_info.cu_per_gpu)
2025-04-03 02:21:18 -04:00
if np.isnan(ammolite__cu_per_gpu) or ammolite__cu_per_gpu == 0:
console_warning(
2025-08-08 15:32:30 -04:00
"cu_per_gpu is not available in sysinfo.csv, please provide the correct "
"value using --specs-correction"
2025-04-03 02:21:18 -04:00
)
2025-03-06 12:52:54 -07:00
ammolite__simd_per_cu = int(sys_info.simd_per_cu) # not used
2025-04-03 02:21:18 -04:00
if np.isnan(ammolite__simd_per_cu) or ammolite__simd_per_cu == 0:
console_warning(
2025-08-08 15:32:30 -04:00
"simd_per_cu is not available in sysinfo.csv, please provide the correct "
"value using --specs-correction"
2025-04-03 02:21:18 -04:00
)
2025-03-06 12:52:54 -07:00
ammolite__sqc_per_gpu = int(sys_info.sqc_per_gpu)
2025-04-03 02:21:18 -04:00
if np.isnan(ammolite__sqc_per_gpu) or ammolite__sqc_per_gpu == 0:
console_warning(
2025-08-08 15:32:30 -04:00
"sqc_per_gpu is not available in sysinfo.csv, please provide the correct "
"value using --specs-correction"
2025-04-03 02:21:18 -04:00
)
2025-03-06 12:52:54 -07:00
ammolite__lds_banks_per_cu = int(sys_info.lds_banks_per_cu)
2025-04-03 02:21:18 -04:00
if np.isnan(ammolite__lds_banks_per_cu) or ammolite__lds_banks_per_cu == 0:
console_warning(
2025-08-08 15:32:30 -04:00
"lds_banks_per_cu is not available in sysinfo.csv, please provide the "
"correct value using --specs-correction"
2025-04-03 02:21:18 -04:00
)
2025-03-06 12:52:54 -07:00
ammolite__cur_sclk = float(sys_info.cur_sclk) # not used
2025-04-03 02:21:18 -04:00
if np.isnan(ammolite__cur_sclk) or ammolite__cur_sclk == 0:
console_warning(
2025-08-08 15:32:30 -04:00
"cur_sclk is not available in sysinfo.csv, please provide the correct "
"value using --specs-correction"
2025-04-03 02:21:18 -04:00
)
ammolite__cur_mclk = float(sys_info.cur_mclk) # not used
if np.isnan(ammolite__cur_mclk) or ammolite__cur_mclk == 0:
console_warning(
2025-08-08 15:32:30 -04:00
"cur_mclk is not available in sysinfo.csv, please provide the correct "
"value using --specs-correction"
2025-04-03 02:21:18 -04:00
)
ammolite__max_mclk = float(sys_info.max_mclk)
if np.isnan(ammolite__max_mclk) or ammolite__max_mclk == 0:
console_warning(
2025-08-08 15:32:30 -04:00
"max_mclk is not available in sysinfo.csv, please provide the correct "
"value using --specs-correction"
2025-04-03 02:21:18 -04:00
)
2025-03-06 12:52:54 -07:00
ammolite__max_sclk = float(sys_info.max_sclk)
2025-04-03 02:21:18 -04:00
if np.isnan(ammolite__max_sclk) or ammolite__max_sclk == 0:
console_warning(
2025-08-08 15:32:30 -04:00
"max_sclk is not available in sysinfo.csv, please provide the correct "
"value using --specs-correction"
2025-04-03 02:21:18 -04:00
)
2025-03-06 12:52:54 -07:00
ammolite__max_waves_per_cu = int(sys_info.max_waves_per_cu)
2025-04-03 02:21:18 -04:00
if np.isnan(ammolite__max_waves_per_cu) or ammolite__max_waves_per_cu == 0:
console_warning(
2025-08-08 15:32:30 -04:00
"max_waver_per_cu is not available in sysinfo.csv, please provide the "
"correct value using --specs-correction"
2025-04-03 02:21:18 -04:00
)
ammolite__num_hbm_channels = float(sys_info.num_hbm_channels)
if np.isnan(ammolite__num_hbm_channels) or ammolite__num_hbm_channels == 0:
console_warning(
2025-08-08 15:32:30 -04:00
"num_hbm_channels is not available in sysinfo.csv, please provide the "
"correct value using --specs-correction"
2025-04-03 02:21:18 -04:00
)
ammolite__total_l2_chan = calc_builtin_var("$total_l2_chan", sys_info)
2025-04-03 02:21:18 -04:00
if np.isnan(ammolite__total_l2_chan) or ammolite__total_l2_chan == 0:
console_warning(
2025-08-08 15:32:30 -04:00
"total_l2_chan is not available in sysinfo.csv, please provide the correct "
"value using --specs-correction"
2025-04-03 02:21:18 -04:00
)
2025-03-06 12:52:54 -07:00
ammolite__num_xcd = int(sys_info.num_xcd)
2025-04-03 02:21:18 -04:00
if np.isnan(ammolite__num_xcd) or ammolite__num_xcd == 0:
console_warning(
2025-08-08 15:32:30 -04:00
"num_xcd is not available in sysinfo.csv, please provide the correct "
"value using --specs-correction"
2025-04-03 02:21:18 -04:00
)
2025-03-06 12:52:54 -07:00
ammolite__wave_size = int(sys_info.wave_size)
2025-04-03 02:21:18 -04:00
if np.isnan(ammolite__wave_size) or ammolite__wave_size == 0:
console_warning(
2025-08-08 15:32:30 -04:00
"wave_size is not available in sysinfo.csv, please provide the correct "
"value using --specs-correction"
2025-04-03 02:21:18 -04:00
)
empirical_peaks = create_empirical_peaks_dict(empirical_peaks_df)
# TODO: fix all $normUnit in Unit column or title
# build and eval all derived build-in global variables
ammolite__build_in = {}
2024-02-27 13:52:44 -05:00
# first pass, we do all per-xcd values, as these are used in subsequent builtins
for key, value in build_in_vars.items():
2024-02-27 13:52:44 -05:00
if "PER_XCD" not in key:
continue
# NB: assume all built-in vars from pmc_perf.csv for now
s = build_eval_string(value, schema.pmc_perf_file_prefix, config)
try:
ammolite__build_in[key] = eval(compile(s, "<string>", "eval"))
except TypeError:
ammolite__build_in[key] = None
except NameError:
ammolite__build_in[key] = None
except KeyError:
ammolite__build_in[key] = None
except AttributeError as ae:
if ae == "'NoneType' object has no attribute 'get'":
ammolite__build_in[key] = None
2025-08-08 15:32:30 -04:00
ammolite__GRBM_GUI_ACTIVE_PER_XCD = ammolite__build_in["GRBM_GUI_ACTIVE_PER_XCD"] # noqa: F841 - Ruff: var utilized during runtime
ammolite__GRBM_COUNT_PER_XCD = ammolite__build_in["GRBM_COUNT_PER_XCD"] # noqa: F841 - Ruff: var utilized during runtime
ammolite__GRBM_SPI_BUSY_PER_XCD = ammolite__build_in["GRBM_SPI_BUSY_PER_XCD"] # noqa: F841 - Ruff: var utilized during runtime
2024-02-27 13:52:44 -05:00
for key, value in build_in_vars.items():
# next pass, we evaluate the builtins the depend on the per-XCD values
if "PER_XCD" in key:
continue
# NB: assume all built-in vars from pmc_perf.csv for now
s = build_eval_string(value, schema.pmc_perf_file_prefix, config)
2024-02-27 13:52:44 -05:00
try:
ammolite__build_in[key] = eval(compile(s, "<string>", "eval"))
except TypeError:
ammolite__build_in[key] = None
except KeyError:
ammolite__build_in[key] = None
2024-02-27 13:52:44 -05:00
except AttributeError as ae:
if ae == "'NoneType' object has no attribute 'get'":
ammolite__build_in[key] = None
2025-08-08 15:32:30 -04:00
ammolite__numActiveCUs = ammolite__build_in["numActiveCUs"] # noqa: F841 - Ruff: var utilized during runtime
ammolite__kernelBusyCycles = ammolite__build_in["kernelBusyCycles"] # noqa: F841 - Ruff: var utilized during runtime
ammolite__hbmBandwidth = ammolite__build_in["hbmBandwidth"] # noqa: F841 - Ruff: var utilized during runtime
2024-02-27 13:52:44 -05:00
row_expr_indexes = []
row_exprs = []
# Hmmm... apply + lambda should just work
2025-08-08 15:32:30 -04:00
# df['Value'] = df['Value'].apply(
# lambda s: eval(
# compile(str(s), '<string>', 'eval')
# )
# )
for id, df in dfs.items():
if dfs_type[id] == "metric_table":
for idx, row in df.iterrows():
for expr in df.columns:
if expr in schema.supported_field:
if expr.lower() != "alias":
if row[expr]:
row_expr_indexes.append((id, idx, expr))
row_exprs.append(row[expr])
if debug: # debug won't impact the regular calc
print("~" * 40 + "\nExpression:")
print(expr, "=", row[expr])
print("Inputs:")
2025-08-08 15:32:30 -04:00
matched_vars = re.findall(
r"ammolite__\w+", row[expr]
)
if matched_vars:
for v in matched_vars:
try:
value = eval(
compile(v, "<string>", "eval")
)
print("Var ", v, ":", value)
except NameError:
if "_empirical_peak" in v:
if v in empirical_peaks:
print(
"Var ",
v,
":",
empirical_peaks[v],
)
else:
print(
"Var ",
v,
": [empirical peak not found]", # noqa
)
else:
print(
"Var ",
v,
": [not available in main thread]", # noqa
)
matched_cols = re.findall(
r"raw_pmc_df\['\w+'\]\['\w+'\]", row[expr]
)
if matched_cols:
for c in matched_cols:
m = re.match(
r"raw_pmc_df\['(\w+)'\]\['(\w+)'\]", c
)
try:
t = raw_pmc_df[m.group(1)][ # noqa: F841
m.group(2)
].to_list()
print(c)
print(
raw_pmc_df[m.group(1)][
m.group(2)
].to_list()
)
except KeyError as ke:
console_warning(
"Skipping entry. "
"Encountered a missing "
"key\n{}".format(str(ke))
)
# print(
# tabulate(raw_pmc_df[m.group(1)][
# m.group(2)],
# headers='keys',
# tablefmt='fancy_grid'))
print("\nOutput:")
try:
print(
eval(compile(row[expr], "<string>", "eval"))
)
print("~" * 40)
except NameError as ne:
if "empirical_peak" in str(ne):
console_warning(
"Skipping debug evaluation. Empirical peak variables " # noqa
"not available in main thread: {}".format( # noqa
str(ne)
)
)
else:
console_warning(
"Skipping debug evaluation. Variable not available: {}".format( # noqa
str(ne)
)
)
print("~" * 40)
except TypeError:
2024-03-04 12:57:25 -06:00
console_warning(
2025-08-08 15:32:30 -04:00
"Skipping entry. Encountered a missing "
2025-08-22 12:22:17 -04:00
"counter\n"
"{} has been assigned to None\n{}".format(
2025-08-08 15:32:30 -04:00
expr,
np.nan,
2024-03-04 12:57:25 -06:00
)
)
except KeyError as ke:
# We can't guarantee that [] accesses are safe.
console_warning(
"Skipping entry. Encountered a missing "
"key\n{}".format(str(ke))
)
except AttributeError as ae:
if (
str(ae)
2025-08-08 15:32:30 -04:00
== "'NoneType' object has no attribute "
"'get'"
):
2024-03-04 12:57:25 -06:00
console_warning(
2025-08-08 15:32:30 -04:00
"Skipping entry. Encountered a missing "
"csv\n{}".format(np.nan)
)
2024-03-04 12:57:25 -06:00
else:
console_error("analysis", str(ae))
else:
# If not insert nan, the whole col might be treated
# as string but not nubmer if there is NONE
row[expr] = ""
# print(tabulate(df, headers='keys', tablefmt='fancy_grid'))
ammolite_vars = {
key: val for key, val in locals().items() if key.startswith("ammolite__")
}
# Empirically, 16 is about as much as we need.
processes = min(16, multiprocessing.cpu_count() // 2)
# breakpoint()
with multiprocessing.Pool(
processes=processes,
initializer=init_metric_evaluator,
initargs=(raw_pmc_df, ammolite_vars, empirical_peaks),
) as pool:
outs = pool.map(run_metric_evaluator, row_exprs)
for (df_id, row, col), out in zip(row_expr_indexes, outs):
dfs[df_id].loc[row, col] = out
@demarcate
def apply_filters(workload, dir, is_gui, debug):
"""
Apply user's filters to the raw_pmc df.
"""
# TODO: error out properly if filters out of bound
ret_df = workload.raw_pmc
2025-01-02 13:29:47 -08:00
if workload.filter_nodes:
ret_df = ret_df.loc[
ret_df[schema.pmc_perf_file_prefix]["Node"]
.astype(str)
.isin([workload.filter_gpu_ids])
]
if ret_df.empty:
console_error("analysis", "{} is invalid".format(workload.filter_nodes))
if workload.filter_gpu_ids:
ret_df = ret_df.loc[
ret_df[schema.pmc_perf_file_prefix]["GPU_ID"]
.astype(str)
.isin([workload.filter_gpu_ids])
]
if ret_df.empty:
console_error(
2024-03-04 12:57:25 -06:00
"analysis", "{} is an invalid gpu-id".format(workload.filter_gpu_ids)
)
# NB:
# Kernel id is unique!
# We pick up kernel names from kerne ids first.
# Then filter valid entries with kernel names.
if workload.filter_kernel_ids:
2025-08-08 15:32:30 -04:00
if all(isinstance(kid, int) for kid in workload.filter_kernel_ids):
# Verify valid kernel filter
2025-01-02 13:29:47 -08:00
kernels_df = pd.read_csv(str(Path(dir).joinpath("pmc_kernel_top.csv")))
for kernel_id in workload.filter_kernel_ids:
if kernel_id >= len(kernels_df["Kernel_Name"]):
2024-01-30 17:25:16 -06:00
console_error(
2025-08-22 12:22:17 -04:00
"{} is an invalid kernel id. "
"Please enter an id between 0-{}".format(
2025-08-08 15:32:30 -04:00
kernel_id,
len(kernels_df["Kernel_Name"]) - 1,
)
)
kernels = []
# NB: mark selected kernels with "*"
# Todo: fix it for unaligned comparison
kernel_top_df = workload.dfs[pmc_kernel_top_table_id]
kernel_top_df["S"] = ""
for kernel_id in workload.filter_kernel_ids:
# print("------- ", kernel_id)
kernels.append(kernel_top_df.loc[kernel_id, "Kernel_Name"])
kernel_top_df.loc[kernel_id, "S"] = "*"
if kernels:
# print("fitlered df:", len(df.index))
ret_df = ret_df.loc[
ret_df[schema.pmc_perf_file_prefix]["Kernel_Name"].isin(kernels)
]
2025-08-08 15:32:30 -04:00
elif all(isinstance(kid, str) for kid in workload.filter_kernel_ids):
2024-03-03 15:20:21 -06:00
df_cleaned = ret_df[schema.pmc_perf_file_prefix]["Kernel_Name"].apply(
lambda x: x.strip() if isinstance(x, str) else x
)
ret_df = ret_df.loc[df_cleaned.isin(workload.filter_kernel_ids)]
2024-02-29 15:12:01 -06:00
else:
2024-03-04 12:57:25 -06:00
console_error(
"analyze",
"Mixing kernel indices and string filters is not currently supported",
)
if workload.filter_dispatch_ids:
# NB: support ignoring the 1st n dispatched execution by '> n'
# The better way may be parsing python slice string
for d in workload.filter_dispatch_ids:
if int(d) >= len(ret_df): # subtract 2 bc of the two header rows
2024-03-04 12:57:25 -06:00
console_error("analysis", "{} is an invalid dispatch id.".format(d))
if ">" in workload.filter_dispatch_ids[0]:
m = re.match(r"\> (\d+)", workload.filter_dispatch_ids[0])
ret_df = ret_df[
ret_df[schema.pmc_perf_file_prefix]["Dispatch_ID"] > int(m.group(1))
]
else:
dispatches = [int(x) for x in workload.filter_dispatch_ids]
ret_df = ret_df.loc[dispatches]
if debug:
print("~" * 40, "\nraw pmc df info:\n")
print(workload.raw_pmc.info())
print("~" * 40, "\nfiltered pmc df info:")
print(ret_df.info())
return ret_df
def find_key_recursively(data, search_key):
"""
2025-08-08 15:32:30 -04:00
Recursively search for the search_key in the given data
(which can be a dict or list).
If the key is found, returns the value as a DataFrame.
"""
if isinstance(data, dict):
for key, value in data.items():
if key == search_key:
# Convert JSON value to DataFrame
# return pd.read_json(StringIO(json.dumps(value)))
return value
elif isinstance(value, (dict, list)):
result = find_key_recursively(value, search_key)
if result is not None:
return result # Return the DataFrame if found
elif isinstance(data, list):
for item in data:
result = find_key_recursively(item, search_key)
if result is not None:
return result # Return the DataFrame if found
return None # Return None if the key was not found
def search_key_in_json(file_path, search_key):
# FIXME:
# Load the entire JSON into memory.
# Should not use for large file.
with open(file_path, "r") as file:
data = json.load(file)
found = find_key_recursively(data, search_key)
if found == None:
console_error(f"Key '{search_key}' not found in the JSON file.")
return found
def search_pc_sampling_record(records):
"""
Search PC sampling records, and group and sort them
"""
2025-06-06 12:43:52 -06:00
# NB:
# The field stall_reason is vailid only for HW stochastic pc sampling.
# Todo: might save wavefront count for HW stochastic pc sampling?
grouped_data = defaultdict(
2025-06-06 12:43:52 -06:00
lambda: defaultdict(
lambda: {
"count": 0,
"count_issued": 0,
"count_stalled": 0,
2025-06-06 12:43:52 -06:00
"inst_index": None,
"stall_reason": {
"NONE": 0,
2025-08-08 15:32:30 -04:00
# No instruction available in the instruction cache.
"NO_INSTRUCTION_AVAILABLE": 0,
2025-06-06 12:43:52 -06:00
"ALU_DEPENDENCY": 0, # ALU dependency not resolved.
"WAITCNT": 0,
"INTERNAL_INSTRUCTION": 0, # Wave executes an internal instruction.
"BARRIER_WAIT": 0,
"ARBITER_NOT_WIN": 0, # The instruction did not win the arbiter.
2025-08-08 15:32:30 -04:00
"ARBITER_WIN_EX_STALL": 0,
# Arbiter issued an instruction, but the execution pipe
# pushed it back from execution.
"OTHER_WAIT": 0,
# Other types of wait (e.g., wait for XNACK acknowledgment).
2025-06-06 12:43:52 -06:00
"SLEEP_WAIT": 0,
"LAST": 0,
},
}
)
)
rocp_inst_not_issued_prefix_len = len(PC_SAMPLING_NOT_ISSUE_PREFIX)
# Populate grouped_data
2025-06-06 12:43:52 -06:00
for i, item in enumerate(records):
pc_info = item["record"].get("pc", {})
code_object_id = pc_info.get("code_object_id")
code_object_offset = pc_info.get("code_object_offset")
2025-06-06 12:43:52 -06:00
snapshot = item["record"].get("snapshot", {})
inst_index = item.get("inst_index")
issued = item["record"].get("wave_issued")
2025-06-06 12:43:52 -06:00
# Todo: opt me
if (
code_object_id is not None
and code_object_offset is not None
and inst_index is not None
):
grouped_data[code_object_id][code_object_offset]["count"] += 1
2025-08-08 15:32:30 -04:00
# NB: the write here could be duplicated. If there is perf issue,
# We might want to opt it.
grouped_data[code_object_id][code_object_offset]["inst_index"] = inst_index
2025-06-06 12:43:52 -06:00
if len(snapshot):
if issued:
2025-08-08 15:32:30 -04:00
grouped_data[code_object_id][code_object_offset][
"count_issued"
] += 1
else:
2025-08-08 15:32:30 -04:00
grouped_data[code_object_id][code_object_offset][
"count_stalled"
] += 1
grouped_data[code_object_id][code_object_offset]["stall_reason"][
snapshot.get("stall_reason")[rocp_inst_not_issued_prefix_len:]
] += 1
2025-06-06 12:43:52 -06:00
# print(
# inst_index,
# grouped_data[code_object_id][code_object_offset]["stall_reason"],
# )
if len(grouped_data) == 0:
console_warning("PC sampling: no pc sampling record found!")
return None
2025-06-06 12:43:52 -06:00
# print(grouped_data)
2025-08-08 15:32:30 -04:00
# Convert to sorted list of tuples:
# (code_object_id, inst_index, code_object_offset, count)
sorted_counts = sorted(
[
2025-06-06 12:43:52 -06:00
(
code_object_id,
info["inst_index"],
offset,
info["count"],
info["count_issued"],
info["count_stalled"],
2025-08-08 15:32:30 -04:00
# For info["stall_reason"], remove the zero entries,
# sorting the remaining items by their values in descending order
2025-06-06 12:43:52 -06:00
sorted(
((k, v) for k, v in info["stall_reason"].items() if v > 0),
key=lambda item: item[1],
reverse=True,
),
)
for code_object_id, offsets in grouped_data.items()
for offset, info in offsets.items()
],
key=lambda x: (
x[0],
x[2],
), # Sort by code_object_id, then by code_object_offset
)
return sorted_counts
@demarcate
2025-06-06 12:43:52 -06:00
def load_pc_sampling_data_per_kernel(
method: str, file_name: Path, kernel_name: str, sorting_type: str
) -> pd.DataFrame:
"""
2025-06-06 12:43:52 -06:00
Load PC sampling raw data from json file with given method and kernel name,
2025-08-08 15:32:30 -04:00
count pc sampling and sort it in the order of compiled asm and associate with
kernel source code if available,
then return df.
2025-06-06 12:43:52 -06:00
:param method: "host_trap" or "stochastic".
:type method: str
:param file_name: The pc sampling json file.
:type file_name: Path
:param kernel_name: The kernel name to be filtered out.
:type kernel_name: str
:param sorting_type: "offset" or "count".
:type sorting_type: str
:return: The counted and reordering pc sampling info.
:rtype: pd.DataFrame:
"""
kernel_info_list = search_key_in_json(file_name, "kernel_symbols")
kernel_info = {}
if kernel_info_list:
for item in kernel_info_list:
if (
item["formatted_kernel_name"] == kernel_name
or item["demangled_kernel_name"] == kernel_name
or item["truncated_kernel_name"] == kernel_name
):
# kernel_info["kernel_id"] = item["kernel_id"]
kernel_info["code_object_id"] = item["code_object_id"]
kernel_info["entry_byte_offset"] = item["kernel_code_entry_byte_offset"]
break
if not kernel_info:
console_warning("PC sampling: can not find the kernel %s " % kernel_name)
return pd.DataFrame()
else:
console_debug("PC sampling: kernel %s " % kernel_info)
filtered_sorted_list = sorted(
[
item
for item in kernel_info_list
if item["code_object_id"] == kernel_info["code_object_id"]
],
key=lambda x: x["kernel_code_entry_byte_offset"],
)
for i, item in enumerate(filtered_sorted_list):
if item["kernel_code_entry_byte_offset"] == kernel_info["entry_byte_offset"]:
next_index = i + 1
if next_index < len(filtered_sorted_list): # Ensure the next item exists
next_item = filtered_sorted_list[next_index]
kernel_info["potential_end_offset"] = next_item[
"kernel_code_entry_byte_offset"
]
else:
kernel_info["potential_end_offset"] = sys.maxsize
break
# print("kernel_info", kernel_info)
2025-06-06 12:43:52 -06:00
pc_sample_key_loc = (
search_key_in_json(file_name, "pc_sample_host_trap")
if method == "host_trap"
else search_key_in_json(file_name, "pc_sample_stochastic")
)
2025-06-06 12:43:52 -06:00
# print(type(pc_sample_key_loc), len(pc_sample_key_loc))
# print(pc_sample_key_loc[0]["record"].get("pc", {}).get("code_object_offset"))
# print(search_pc_sampling_record(pc_sample_key_loc))
df = pd.DataFrame(
2025-06-06 12:43:52 -06:00
search_pc_sampling_record(pc_sample_key_loc),
columns=[
"code_object_id",
"inst_index",
"offset",
"count",
"count_issued",
"count_stalled",
"stall_reason",
],
)
df = df[
(df["code_object_id"] == kernel_info["code_object_id"])
& (df["offset"] > kernel_info["entry_byte_offset"])
& (df["offset"] < kernel_info["potential_end_offset"])
2025-08-08 15:32:30 -04:00
][
[
"inst_index",
"offset",
"count",
"count_issued",
"count_stalled",
"stall_reason",
]
]
df["offset"] = df["offset"].apply(lambda x: hex(x))
2025-08-08 15:32:30 -04:00
# df["stall_reason"] = df["stall_reason"].apply(
# lambda x: ', '.join(
# f"{k}: {v}"
# for k, v in x
# )
# )
2025-06-06 12:43:52 -06:00
pc_sample_instructions = search_key_in_json(file_name, "pc_sample_instructions")
# print(pc_sample_instructions)
df["instruction"] = df["inst_index"].apply(
lambda x: pc_sample_instructions[x] if x < len(pc_sample_instructions) else None
)
pc_sample_comments = search_key_in_json(file_name, "pc_sample_comments")
df["source_line"] = df["inst_index"].apply(
lambda x: (
".../" + Path(pc_sample_comments[x]).name
if x < len(pc_sample_instructions)
else None
)
)
2025-06-06 12:43:52 -06:00
# print(df[["source_line", "instruction", "offset", "count", "stall_reason"]])
if sorting_type == "offset":
return (
df[["source_line", "instruction", "offset", "count"]]
if method == "host_trap"
else df[
[
"source_line",
"instruction",
"offset",
"count",
"count_issued",
"count_stalled",
"stall_reason",
]
]
2025-06-06 12:43:52 -06:00
)
else: # sort by "count"
return (
df[["source_line", "instruction", "offset", "count"]].sort_values(
by="count", ascending=False
)
if method == "host_trap"
else df[
[
"source_line",
"instruction",
"offset",
"count",
"count_issued",
"count_stalled",
"stall_reason",
]
2025-06-06 12:43:52 -06:00
].sort_values(by="count", ascending=False)
)
# might support sort by stall reason in the future
@demarcate
2025-06-06 12:43:52 -06:00
def load_pc_sampling_data(workload, dir, file_prefix, sorting_type):
"""
Load PC sampling raw data, filter and sort it by specified conditions,
then return df.
"""
if file_prefix.lower() == "none":
return pd.DataFrame()
2025-06-06 12:43:52 -06:00
pc_sampling_method = None
# NB:
# - The default file name is subject to changes from rocprofv3
# - Prioritize stochastic
# - Alternatively, we could check pc_sampling_method in json
2025-08-08 15:32:30 -04:00
csv_file_path = Path.joinpath(
Path(dir), file_prefix + "_pc_sampling_stochastic.csv"
)
2025-06-06 12:43:52 -06:00
if csv_file_path.exists():
pc_sampling_method = "stochastic"
else:
csv_file_path = Path.joinpath(
Path(dir), file_prefix + "_pc_sampling_host_trap.csv"
)
2025-06-06 12:43:52 -06:00
if csv_file_path.exists():
pc_sampling_method = "host_trap"
2025-06-06 12:43:52 -06:00
if pc_sampling_method == None:
2025-06-09 11:29:10 -06:00
console_warning(
"PC sampling: can not detect pc sampling method without %s " % csv_file_path
)
2025-06-06 12:43:52 -06:00
return pd.DataFrame()
2025-06-06 12:43:52 -06:00
# No kernel filter, return grouped and sorted csv directly
if not workload.filter_kernel_ids:
df = pd.read_csv(csv_file_path)
# Group by 'Instruction_Comment' and count occurrences
grouped_counts = (
df.groupby("Instruction_Comment")
.agg(
count=("Instruction_Comment", "count"),
instruction=("Instruction", "first"),
)
2025-06-06 12:43:52 -06:00
.reset_index()
.rename(columns={"Instruction_Comment": "source_line"})
)
grouped_counts = grouped_counts[["source_line", "instruction", "count"]]
2025-06-06 12:43:52 -06:00
grouped_counts["source_line"] = grouped_counts["source_line"].apply(
lambda x: (".../" + Path(x).name)
)
# Sort by the count of occurrences
sorted_counts = grouped_counts.sort_values(by="count", ascending=False)
# print(sorted_counts.info)
2025-06-06 12:43:52 -06:00
return sorted_counts
elif len(workload.filter_kernel_ids) > 1:
console_error(
2025-08-08 15:32:30 -04:00
"PC sampling supports single kernel only! Please specify -k with "
"single kernel."
)
return pd.DataFrame()
elif len(workload.filter_kernel_ids) == 1:
# print("kernel id", workload.filter_kernel_ids[0])
# NB: the default file name is subject to changes from rocprofv3/rocprofiler_sdk
json_file_path = Path.joinpath(Path(dir), file_prefix + "_results.json")
if not json_file_path.exists():
console_error("PC sampling: can not read %s " % json_file_path)
return pd.DataFrame()
else:
# NB:
# We should find better way to remove the dependency on kernel_top_table
kernel_top_df = workload.dfs[pmc_kernel_top_table_id]
file = Path.joinpath(Path(dir), kernel_top_df.loc[0, "from_csv"])
kernel_name = pd.read_csv(file).loc[
workload.filter_kernel_ids[0], "Kernel_Name"
]
2025-06-06 12:43:52 -06:00
return load_pc_sampling_data_per_kernel(
pc_sampling_method, json_file_path, kernel_name, sorting_type
)
else:
console_warning("PC sampling: No data")
return pd.DataFrame()
@demarcate
2025-06-06 12:43:52 -06:00
def load_kernel_top(workload, dir, args):
# NB:
2025-08-08 15:32:30 -04:00
# - Do pmc_kernel_top.csv loading before eval_metric because we need the
# kernel names.
# - There might be a better way/timing to load raw_csv_table.
# FIXME:
# the func name load_kernel_top needs to be changed to load_non_mertrics_table
# NB:
# "from_csv", "from_csv_columnwise", and "from_pc_sampling"
# are 3 internal symbols converted in build_dfs() for non-metrics table.
# There might be better way to store these info without the orginal entry.
tmp = {}
for id, df in workload.dfs.items():
if "from_csv" in df.columns:
file = Path.joinpath(Path(dir), df.loc[0, "from_csv"])
if file.exists():
tmp[id] = pd.read_csv(file)
else:
console_warning(
2025-08-08 15:32:30 -04:00
f"Couldn't load {file.name}. "
"This may result in missing analysis data."
)
2025-08-08 15:32:30 -04:00
# NB: Special case for sysinfo. Probably room for improvement in this whole
# function design
elif "from_csv_columnwise" in df.columns and id == 101:
tmp[id] = workload.sys_info.transpose()
# All transposed columns should be marked with a general header
tmp[id].columns = ["Info"]
elif "from_csv_columnwise" in df.columns:
# NB:
# Another way might be doing transpose in tty like metric_table.
# But we need to figure out headers and comparison properly.
file = Path.joinpath(Path(dir), df.loc[0, "from_csv_columnwise"])
if file.exists():
tmp[id] = pd.read_csv(file).transpose()
# NB:
# All transposed columns should be marked with a general header,
# so tty could detect them and show them correctly in comparison.
tmp[id].columns = ["Info"]
else:
console_warning(
2025-08-08 15:32:30 -04:00
f"Couldn't load {file.name}. "
"This may result in missing analysis data."
)
elif "from_pc_sampling" in df.columns:
2025-06-06 12:43:52 -06:00
tmp[id] = load_pc_sampling_data(
workload,
dir,
df.loc[0, "from_pc_sampling"],
args.pc_sampling_sorting_type,
)
# print("table id", id, "filter_kernel_ids", workload.filter_kernel_ids)
workload.dfs.update(tmp)
@demarcate
def load_table_data(workload, dir, is_gui, args, config, skipKernelTop=False):
"""
- Load data for all "raw_csv_table"
2025-08-01 11:31:43 -04:00
- Load data for "pc_sampling_table"
- Calculate mertric value for all "metric_table"
"""
if not skipKernelTop:
2025-06-06 12:43:52 -06:00
load_kernel_top(workload, dir, args)
eval_metric(
workload.dfs,
workload.dfs_type,
workload.sys_info.iloc[0],
workload.roofline_peaks,
2025-06-06 12:43:52 -06:00
apply_filters(workload, dir, is_gui, args.debug),
args.debug,
config,
)
def build_comparable_columns(time_unit):
"""
Build comparable columns/headers for display
"""
comparable_columns = schema.supported_field
2025-07-25 14:01:34 -04:00
top_stat_base = [
"Count",
"Sum",
"Mean",
"Median",
"Standard Deviation",
"Description",
]
for h in top_stat_base:
comparable_columns.append(h + "(" + time_unit + ")")
return comparable_columns
2024-01-15 12:27:18 -06:00
2024-03-01 11:52:31 -06:00
def correct_sys_info(mspec, specs_correction: dict):
2024-01-15 12:27:18 -06:00
"""
Correct system spec items manually
"""
# todo: more err checking for string specs_correction
2024-01-15 12:27:18 -06:00
pairs = dict(re.findall(r"(\w+):\s*(\d+)", specs_correction))
2024-01-15 12:27:18 -06:00
for k, v in pairs.items():
if not hasattr(mspec, str(k)):
2024-01-30 17:25:16 -06:00
console_error(
2024-03-04 12:57:25 -06:00
"analyze",
2025-08-08 15:32:30 -04:00
f"Invalid specs correction '{k}'. Please use --specs option "
f"to peak valid specs",
2024-03-01 11:52:31 -06:00
)
setattr(mspec, str(k), v)
return mspec.get_class_members()