Files
rocm-systems/projects/rocprofiler-compute/src/utils/file_io.py
T
2025-09-18 10:48:01 -04:00

360 γραμμές
12 KiB
Python

##############################################################################
# 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.
##############################################################################
import re
from collections import OrderedDict
from pathlib import Path
from typing import Any, Optional
import pandas as pd
import yaml
import config
from utils import rocpd_data, schema
from utils.kernel_name_shortener import kernel_name_shortener
from utils.logger import console_debug, console_error, console_log, demarcate
# TODO: use pandas chunksize or dask to read really large csv file
# from dask import dataframe as dd
def load_sys_info(f: str) -> pd.DataFrame:
"""
Load sys running info from csv file to a df.
"""
return pd.read_csv(f)
def load_panel_configs(
dirs: list[str],
) -> OrderedDict[int, dict[str, Any]]:
"""
Load all panel configs from yaml file.
"""
configs: dict[int, dict[str, Any]] = {}
for dir_path in dirs:
for yaml_file in Path(dir_path).rglob("*.yaml"):
with open(yaml_file) as file:
config_yml = yaml.safe_load(file)
# metric key can be None due to some metric-
# tables not having any metrics
# metric key should be empty dict instead of None
panel_config = config_yml["Panel Config"]
for data_source in panel_config["data source"]:
metric_table = data_source.get("metric_table")
if metric_table and metric_table["metric"] is None:
metric_table["metric"] = {}
configs[panel_config["id"]] = panel_config
# TODO: sort metrics as the header order in case they-
# are not defined in the same order
return OrderedDict(sorted(configs.items()))
def load_profiling_config(config_dir: str) -> dict[str, Any]:
"""
Load profiling config from yaml file.
"""
config_path = Path(config_dir) / "profiling_config.yaml"
try:
with open(config_path) as file:
return yaml.safe_load(file) or {}
except FileNotFoundError:
console_log(f"Could not find profiling_config.yaml in {config_dir}")
return {}
@demarcate
def create_df_kernel_top_stats(
df_in: dict[str, pd.DataFrame],
raw_data_dir: str,
filter_gpu_ids: Optional[list[str]],
filter_dispatch_ids: Optional[list[str]],
filter_nodes: Optional[str],
time_unit: str,
kernel_verbose: int,
sortby: str = "sum",
) -> None:
"""
Create top stats info by grouping kernels with user's filters.
"""
df = df_in["pmc_perf"].copy()
# Demangle original KernelNames
kernel_name_shortener(df, kernel_verbose)
# The logic below for filters are the same as in parser.apply_filters(),
# which can be merged together if need it.
if filter_nodes:
df = df.loc[df["Node"].astype(str).isin([filter_nodes])]
if filter_gpu_ids:
df = df.loc[df["GPU_ID"].astype(str).isin([filter_gpu_ids])]
if filter_dispatch_ids:
# NB: support ignoring the 1st n dispatched execution by '> n'
# The better way may be parsing python slice string
first_filter = filter_dispatch_ids[0]
if isinstance(first_filter, str) and first_filter.startswith(">"):
match = re.match(r">\s*(\d+)", str(first_filter))
if match:
threshold = int(match.group(1))
df = df[df["Dispatch_ID"] > threshold]
else:
filter_strings = [str(f) for f in filter_dispatch_ids]
df = df.loc[df["Dispatch_ID"].astype(str).isin(filter_strings)]
# First, create a dispatches file used to populate global vars
dispatch_columns = (
["Node", "Dispatch_ID", "Kernel_Name", "GPU_ID"]
if "Node" in df.columns
else ["Dispatch_ID", "Kernel_Name", "GPU_ID"]
)
dispatch_info = df[dispatch_columns]
dispatch_output_path = Path(raw_data_dir) / "pmc_dispatch_info.csv"
dispatch_info.to_csv(dispatch_output_path, index=False)
# Calculate execution times
execution_times = df["End_Timestamp"] - df["Start_Timestamp"]
time_stats = pd.DataFrame({
"Kernel_Name": df["Kernel_Name"],
"ExeTime": execution_times,
})
grouped = time_stats.groupby("Kernel_Name")["ExeTime"].agg([
"count",
"sum",
"mean",
"median",
])
# Rename columns with time unit
time_unit_suffix = f"({time_unit})"
column_mapping = {
"count": "Count",
"sum": f"Sum{time_unit_suffix}",
"mean": f"Mean{time_unit_suffix}",
"median": f"Median{time_unit_suffix}",
}
grouped = grouped.rename(columns=column_mapping)
# Convert time units
time_divisor = config.TIME_UNITS[time_unit]
for col in [
f"Sum{time_unit_suffix}",
f"Mean{time_unit_suffix}",
f"Median{time_unit_suffix}",
]:
grouped[col] = grouped[col] / time_divisor
grouped = grouped.reset_index()
# Calculate percentage
sum_column = f"Sum{time_unit_suffix}"
grouped["Pct"] = grouped[sum_column] / grouped[sum_column].sum() * 100
# Sort by total time as default.
if sortby == "sum":
grouped = grouped.sort_values(sum_column, ascending=False)
grouped.to_csv(str(Path(raw_data_dir) / "pmc_kernel_top.csv"), index=False)
elif sortby == "kernel":
grouped = grouped.sort_values("Kernel_Name")
grouped.to_csv(str(Path(raw_data_dir) / "pmc_kernel_top.csv"), index=False)
@demarcate
def create_df_pmc(
raw_data_root_dir: str,
nodes: Optional[list[str]],
spatial_multiplexing: bool,
kernel_verbose: int,
verbose: int,
config_dict: dict[str, Any],
) -> pd.DataFrame:
"""
Load all raw pmc counters and join into one df.
"""
def create_single_df_pmc(
raw_data_dir: str, node_name: Optional[str], kernel_verbose: int, verbose: int
) -> pd.DataFrame:
dfs: list[pd.DataFrame] = []
coll_levels: list[str] = []
for csv_file in Path(raw_data_dir).rglob("*.csv"):
file_name = csv_file.name
is_sq_file = file_name.startswith("SQ")
is_pmc_perf = file_name == f"{schema.PMC_PERF_FILE_PREFIX}.csv"
if is_sq_file or is_pmc_perf:
tmp_df = pd.read_csv(csv_file)
if config_dict.get("format_rocprof_output") == "rocpd":
tmp_df = rocpd_data.process_rocpd_csv(tmp_df)
# Demangle original KernelNames
kernel_name_shortener(tmp_df, kernel_verbose)
# NB:
# Idealy, the Node column should be added out of
# multiindexing level. Here, we add it into pmc_perf
# as it is the main sub-df which can be handled easily
# later.
if file_name == "pmc_perf.csv" and node_name is not None:
tmp_df.insert(0, "Node", node_name)
dfs.append(tmp_df)
# Remove .csv extension for collection level
coll_levels.append(csv_file.stem)
if not dfs:
return pd.DataFrame()
# TODO: double check the case if all tmp_df.shape[0] are not on the same page
final_df = pd.concat(dfs, keys=coll_levels, axis=1, join="inner", copy=False)
if verbose >= 2:
console_debug(f"pmc_raw_data final_single_df {final_df.info}")
return final_df
root_path = Path(raw_data_root_dir)
# 1. spatial multiplexing case
if spatial_multiplexing:
dfs: list[pd.DataFrame] = []
for subdir in root_path.iterdir():
if subdir.is_dir():
new_df = create_single_df_pmc(
str(subdir), str(subdir.name), kernel_verbose, verbose
)
if not new_df.empty:
dfs.append(new_df)
return pd.concat(dfs, ignore_index=True) if dfs else pd.DataFrame()
# 2. regular single node case (nodes=None)
if nodes is None:
return create_single_df_pmc(raw_data_root_dir, None, kernel_verbose, verbose)
# 3. all nodes case (nodes=[])
if not nodes:
dfs: list[pd.DataFrame] = []
for subdir in root_path.iterdir():
if subdir.is_dir():
new_df = create_single_df_pmc(
str(subdir), str(subdir.name), kernel_verbose, verbose
)
if not new_df.empty:
dfs.append(new_df)
return pd.concat(dfs, ignore_index=True) if dfs else pd.DataFrame()
# 4. specified node list case (nodes=[...])
dfs: list[pd.DataFrame] = []
for node in nodes:
node_path = root_path / node
if node_path.exists():
new_df = create_single_df_pmc(str(node_path), node, kernel_verbose, verbose)
if not new_df.empty:
dfs.append(new_df)
return pd.concat(dfs, ignore_index=True) if dfs else pd.DataFrame()
def collect_wave_occu_per_cu(in_dir: str, out_dir: str, num_se: int) -> None:
"""
Collect wave occupancy info from in_dir csv files
and consolidate into out_dir/wave_occu_per_cu.csv.
It depends highly on wave_occu_se*.csv format.
"""
in_path = Path(in_dir)
all_data = pd.DataFrame()
for i in range(num_se):
file_path = in_path / f"wave_occu_se{i}.csv"
if not file_path.exists():
continue
tmp_df = pd.read_csv(file_path)
if tmp_df.empty:
continue
se_idx = f"SE{tmp_df.loc[0, 'SE']}"
tmp_df.rename(
columns={
"Dispatch": "Dispatch",
"SE": "SE",
"CU": "CU",
"Occupancy": se_idx,
}
)
# TODO: join instead of concat!
if i == 0:
all_data = tmp_df[{"CU", se_idx}]
all_data.sort_index(axis=1, inplace=True)
else:
all_data = pd.concat([all_data, tmp_df[se_idx]], axis=1, copy=False)
if not all_data.empty:
all_data.to_csv(Path(out_dir) / "wave_occu_per_cu.csv", index=False)
def is_single_panel_config(
root_dir: str, supported_archs: dict[str, str]
) -> Optional[bool]:
"""
Check the root configs dir structure to decide using one config set for all
archs, or one for each arch.
"""
# If not single config, verify all supported archs have defined configs
arch_names = list(supported_archs.keys())
root_path = Path(root_dir)
arch_count = sum(1 for arch in arch_names if (root_path / arch).exists())
if arch_count == 0:
return True
elif arch_count == len(arch_names):
return False
else:
console_error("Found multiple panel config sets but incomplete for all archs.")
def find_1st_sub_dir(directory: str) -> Optional[str]:
"""
Find the first sub dir in a directory
"""
dir_path = Path(directory)
try:
# Iterate over entries in the directory
for entry in dir_path.iterdir():
if entry.is_dir(): # Check if it's a directory
return str(entry)
return None
except FileNotFoundError:
console_error(f'The directory "{directory}" does not exist.', exit=False)