Files
rocm-systems/projects/rocprofiler-compute/src/utils/parser.py
T
Cole Ramos c5c44a9cba Yearly license update (#538)
* Add license template designation

Signed-off-by: coleramos425 <colramos@amd.com>

* Update license year on *.py

Signed-off-by: coleramos425 <colramos@amd.com>

* Update license year on *.sh

Signed-off-by: coleramos425 <colramos@amd.com>

* Update license year on misc files

Signed-off-by: coleramos425 <colramos@amd.com>

---------

Signed-off-by: coleramos425 <colramos@amd.com>

[ROCm/rocprofiler-compute commit: 34760ff814]
2025-01-23 13:09:32 -06:00

1017 righe
40 KiB
Python

##############################################################################bl
# MIT License
#
# 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:
#
# 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,
# 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,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
##############################################################################el
import ast
import re
import warnings
from pathlib import Path
import astunparse
import numpy as np
import pandas as pd
from utils import schema
from utils.utils import 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"]} ,
# "then": {"$divide":[{"$subtract": ["&End_Timestamp", "&Start_Timestamp" ]}, 1000000000]}
# }
# ],
# "default": 1
# }
# }
supported_denom = {
"per_wave": "SQ_WAVES",
"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 = {
"GRBM_GUI_ACTIVE_PER_XCD": "(GRBM_GUI_ACTIVE / $num_xcd)",
"GRBM_COUNT_PER_XCD": "(GRBM_COUNT / $num_xcd)",
"GRBM_SPI_BUSY_PER_XCD": "(GRBM_SPI_BUSY / $num_xcd)",
"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)",
}
supported_call = {
# If the below has single arg, like(expr), it is a aggr, in which turn to a pd function.
# If it has args like list [], in which turn to 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",
# 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",
}
# ------------------------------------------------------------------------------
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 np.isnan(a).all():
return np.nan
elif a.empty:
return np.nan
elif isinstance(a, pd.core.series.Series):
return a.mean()
else:
raise Exception("to_avg: unsupported type.")
def to_median(a):
if a is None:
return None
elif isinstance(a, pd.core.series.Series):
with warnings.catch_warnings():
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 np.nan
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_round(a, b):
if isinstance(a, pd.core.series.Series):
return a.round(b)
else:
return round(a, b)
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.")
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)
# print("visit_IfExp", type(node.test), type(node.body), type(node.orelse), dir(node))
if isinstance(node.body, ast.Num):
raise Exception(
"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):
"""
Convert user defined equation string to eval executable string
For example,
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 workaound 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.
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)
# use .get() to catch any potential KeyErrors
s = re.sub(r"raw_pmc_df\['(.*?)']", r'raw_pmc_df.get("\1")', s)
# apply coll_level
s = re.sub(r"raw_pmc_df", "raw_pmc_df.get('" + 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",
"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):
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
except:
pass
return visited, counters
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:
console_error('Built-in var " %s " is not supported' % var)
@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 = {}
# 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"])
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"]
data_source_idx = str(data_config["id"] // 100)
if data_source_idx != 0 or data_source_idx in filter_metrics:
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():
if key != "metric" and key != "tips" and key != "expr":
headers.append(tile)
else:
for key, tile in data_config["header"].items():
if key != "tips":
headers.append(tile)
# do we always need one?
headers.append("coll_level")
if "tips" in data_config["header"].keys():
headers.append(data_config["header"]["tips"])
df = pd.DataFrame(columns=headers)
i = 0
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)
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)
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:
if (
k != "tips"
and k != "coll_level"
and k != "alias"
):
values.append(v)
else:
for k, v in entries.items():
if k != "tips" and 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)
if "tips" in entries.keys():
values.append(entries["tips"])
# 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)
):
if (
"columnwise" in data_config
and data_config["columnwise"] == True
):
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()
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):
"""
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"],
)
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'))
@demarcate
def eval_metric(dfs, dfs_type, sys_info, raw_pmc_df, debug):
"""
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"
rocscope_run = sys_info.ip_blocks == "rocscope"
if (
(not rocscope_run and 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()
):
console_warning("Dectected GRBM_GUI_ACTIVE == 0")
console_error("Hauting execution for warning above.")
ammolite__se_per_gpu = sys_info.se_per_gpu
ammolite__pipes_per_gpu = sys_info.pipes_per_gpu
ammolite__cu_per_gpu = sys_info.cu_per_gpu
ammolite__simd_per_cu = sys_info.simd_per_cu # not used
ammolite__sqc_per_gpu = sys_info.sqc_per_gpu
ammolite__lds_banks_per_cu = sys_info.lds_banks_per_cu
ammolite__cur_sclk = sys_info.cur_sclk # not used
ammolite__mclk = sys_info.cur_mclk # not used
ammolite__max_sclk = sys_info.max_sclk
ammolite__max_waves_per_cu = sys_info.max_waves_per_cu
ammolite__hbm_bw = sys_info.hbm_bw
ammolite__total_l2_chan = calc_builtin_var("$total_l2_chan", sys_info)
ammolite__num_xcd = sys_info.num_xcd
ammolite__wave_size = sys_info.wave_size
# TODO: fix all $normUnit in Unit column or title
# build and eval all derived build-in global variables
ammolite__build_in = {}
# first pass, we do all per-xcd values, as these are used in subsequent builtins
for key, value in build_in_vars.items():
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)
try:
ammolite__build_in[key] = eval(compile(s, "<string>", "eval"))
except TypeError:
ammolite__build_in[key] = None
except AttributeError as ae:
if ae == "'NoneType' object has no attribute 'get'":
ammolite__build_in[key] = None
ammolite__GRBM_GUI_ACTIVE_PER_XCD = ammolite__build_in["GRBM_GUI_ACTIVE_PER_XCD"]
ammolite__GRBM_COUNT_PER_XCD = ammolite__build_in["GRBM_COUNT_PER_XCD"]
ammolite__GRBM_SPI_BUSY_PER_XCD = ammolite__build_in["GRBM_SPI_BUSY_PER_XCD"]
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)
try:
ammolite__build_in[key] = eval(compile(s, "<string>", "eval"))
except TypeError:
ammolite__build_in[key] = None
except AttributeError as ae:
if ae == "'NoneType' object has no attribute 'get'":
ammolite__build_in[key] = None
ammolite__numActiveCUs = ammolite__build_in["numActiveCUs"]
ammolite__kernelBusyCycles = ammolite__build_in["kernelBusyCycles"]
# Hmmm... apply + lambda should just work
# 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]:
if debug: # debug won't impact the regular calc
print("~" * 40 + "\nExpression:")
print(expr, "=", row[expr])
print("Inputs:")
matched_vars = re.findall(r"ammolite__\w+", row[expr])
if matched_vars:
for v in matched_vars:
print(
"Var ",
v,
":",
eval(compile(v, "<string>", "eval")),
)
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
)
t = raw_pmc_df[m.group(1)][
m.group(2)
].to_list()
print(c)
print(
raw_pmc_df[m.group(1)][
m.group(2)
].to_list()
)
# 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 TypeError:
console_warning(
"Skipping entry. Encountered a missing counter\n{} has been assigned to None\n{}".format(
expr, np.nan
)
)
except AttributeError as ae:
if (
str(ae)
== "'NoneType' object has no attribute 'get'"
):
console_warning(
"Skipping entry. Encountered a missing csv\n{}".format(
np.nan
)
)
else:
console_error("analysis", str(ae))
# print("eval_metric", id, expr)
try:
out = eval(compile(row[expr], "<string>", "eval"))
if row.name != "19.1.1" and np.isnan(
out
): # Special exception for unique format of Active CUs in mem chart
row[expr] = ""
else:
row[expr] = out
except TypeError:
row[expr] = ""
except AttributeError as ae:
if (
str(ae)
== "'NoneType' object has no attribute 'get'"
):
row[expr] = ""
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'))
@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
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(
"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:
if all(type(kid) == int for kid in workload.filter_kernel_ids):
# Verify valid kernel filter
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"]):
console_error(
"{} is an invalid kernel id. Please enter an id between 0-{}".format(
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)
]
elif all(type(kid) == str for kid in workload.filter_kernel_ids):
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)]
else:
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
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
@demarcate
def load_kernel_top(workload, dir):
# NB:
# - 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.
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(
f"Couldn't load {file.name}. This may result in missing analysis data."
)
# 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(
f"Couldn't load {file.name}. This may result in missing analysis data."
)
workload.dfs.update(tmp)
@demarcate
def load_table_data(workload, dir, is_gui, debug, verbose, skipKernelTop=False):
"""
Load data for all "raw_csv_table".
Calculate mertric value for all "metric_table".
"""
if not skipKernelTop:
load_kernel_top(workload, dir)
eval_metric(
workload.dfs,
workload.dfs_type,
workload.sys_info.iloc[0],
apply_filters(workload, dir, is_gui, debug),
debug,
)
def build_comparable_columns(time_unit):
"""
Build comparable columns/headers for display
"""
comparable_columns = schema.supported_field
top_stat_base = ["Count", "Sum", "Mean", "Median", "Standard Deviation"]
for h in top_stat_base:
comparable_columns.append(h + "(" + time_unit + ")")
return comparable_columns
def correct_sys_info(mspec, specs_correction: dict):
"""
Correct system spec items manually
"""
# todo: more err checking for string specs_correction
pairs = dict(re.findall(r"(\w+):\s*(\d+)", specs_correction))
for k, v in pairs.items():
if not hasattr(mspec, str(k)):
console_error(
"analyze",
f"Invalid specs correction '{k}'. Please use --specs option to peak valid specs",
)
setattr(mspec, str(k), v)
return mspec.get_class_members()