[rocprofiler-compute] Refactor to add type annotation and misc (#787)

This commit is contained in:
xuchen-amd
2025-09-12 13:53:24 -04:00
committed by GitHub
parent 37f8da676a
commit 7ed6000e32
62 changed files with 5145 additions and 4849 deletions
@@ -11,7 +11,7 @@ repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version. Check https://github.com/astral-sh/ruff-pre-commit#version-compatibility,
# for the latest ruff version supported by the hook.
rev: v0.12.7
rev: v0.12.12
hooks:
- id: ruff-check
args: [--fix, --exit-non-zero-on-fix]
@@ -82,6 +82,41 @@ ruff format .
-----
```markdown
## Type Annotations
This project enforces type annotations using Ruff's `flake8-annotations` rules. All new code must include proper type annotations.
### Requirements
- All function arguments must have type annotations (except `self` and `cls`)
- All function return types must be annotated
- Class attributes should have type annotations where applicable
### Examples
```python
# Good - properly annotated
def process_kernel_data(kernel_name: str, metrics: list[float]) -> dict[str, Any]:
"""Process kernel performance metrics."""
return {"kernel": kernel_name, "avg": sum(metrics) / len(metrics)}
# Bad - missing annotations (will be caught by Ruff)
def process_kernel_data(kernel_name, metrics):
return {"kernel": kernel_name, "avg": sum(metrics) / len(metrics)}
```
### Checking Type Annotations
To specifically check for missing type annotations:
```bash
ruff check --select ANN .
```
For existing code, we're gradually adding type annotations. When modifying existing functions, please add type annotations to any code you touch.
```
## Disabling Formatting for Specific Sections
There may be instances where you need to disable Ruff's formatting on a specific block of code. You can do this using special comments:
@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
#
# Configuration file for the Sphinx documentation builder.
#
@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
#
# Configuration file for the Sphinx documentation builder.
#
+1 -1
View File
@@ -60,7 +60,7 @@ exclude_patterns = ["archive", "*/includes"]
html_static_path = ["sphinx/static/css"]
html_css_files = ["o_custom.css"]
with open("data/metrics_description.yaml", "r") as f:
with open("data/metrics_description.yaml") as f:
metrics_data = yaml.safe_load(f)
jinja_contexts = {
"wavefront-launch-stats": {
+17 -3
View File
@@ -21,15 +21,29 @@ extend-exclude = [
".misc",
"external",
"build-rocprof_compute",
# deprecated files
"src/rocprof_compute_profile/profiler_rocprof_v1.py",
"src/rocprof_compute_profile/profiler_rocprof_v2.py",
"src/rocprof_compute_analyze/analysis_db.py",
"src/utils/db_connector.py",
# WIP files
"src/rocprof_compute_tui/widgets/splitter.py"
]
[tool.ruff.lint]
# Enable Pyflakes (F), pycodestyle (E, W for PEP8), and isort (I) rules.
select = ["E", "W", "F", "I"]
ignore = ["E713", "E711"]
# Enable Pyflakes (F), pycodestyle (E, W for PEP8), aisort (I),
# type annotation (ANN), and f-string (UP) rules.
select = ["E", "W", "F", "I", "ANN","UP"]
ignore = ["E713", "E711", "ANN001", "ANN401", "UP045"]
fixable = ["ALL"]
unfixable = []
[tool.ruff.lint.flake8-annotations]
allow-star-arg-any = true #allow Any for *args
ignore-fully-untyped = false #require type annotations
suppress-dummy-args = true #don't require annotation for "_" arguments
suppress-none-returning = false #require explicit None return types
[tool.ruff.format]
preview = true
+40 -46
View File
@@ -27,18 +27,22 @@ import argparse
import os
import re
from pathlib import Path
from typing import Optional
def print_avail_arch(avail_arch: list):
def print_avail_arch(avail_arch: list[str]) -> str:
ret_str = "List all available metrics for analysis on specified arch:"
for arch in avail_arch:
ret_str += "\n {}".format(arch)
ret_str += f"\n {arch}"
return ret_str
def add_general_group(
parser, rocprof_compute_version, supported_archs, rocprof_compute_home
):
parser: argparse.ArgumentParser,
rocprof_compute_home: Path,
supported_archs: dict[str, str],
rocprof_compute_version: dict[str, Optional[str]],
) -> None:
general_group = parser.add_argument_group("General Options")
general_group.add_argument(
@@ -62,25 +66,28 @@ def add_general_group(
dest="list_metrics",
metavar="",
choices=supported_archs.keys(), # ["gfx908", "gfx90a"],
help=print_avail_arch(supported_archs.keys()),
help=print_avail_arch(list(supported_archs.keys())),
)
general_group.add_argument(
"--config-dir",
dest="config_dir",
metavar="",
help="Specify the directory of customized report section configs.",
default=rocprof_compute_home.joinpath("rocprof_compute_soc/analysis_configs/"),
default=rocprof_compute_home / "rocprof_compute_soc/analysis_configs/",
)
# Nowhere to load specs from in db mode
if "database" not in parser.usage:
if parser.usage and "database" not in parser.usage:
general_group.add_argument(
"-s", "--specs", action="store_true", help="Print system specs and exit."
)
def omniarg_parser(
parser, rocprof_compute_home, supported_archs, rocprof_compute_version
):
parser: argparse.ArgumentParser,
rocprof_compute_home: Path,
supported_archs: dict[str, str],
rocprof_compute_version: dict[str, Optional[str]],
) -> None:
# -----------------------------------------
# Parse arguments (dependent on mode)
# -----------------------------------------
@@ -88,7 +95,7 @@ def omniarg_parser(
## General Command Line Options
## ----------------------------
add_general_group(
parser, rocprof_compute_version, supported_archs, rocprof_compute_home
parser, rocprof_compute_home, supported_archs, rocprof_compute_version
)
parser._positionals.title = "Modes"
parser._optionals.title = "Help"
@@ -104,8 +111,7 @@ def omniarg_parser(
help="Profile the target application",
usage="""
`rocprof-compute profile --name <workload_name>
[profile options] [roofline options] -- <profile_cmd>`
`rocprof-compute profile --name <workload_name> [profile options] [roofline options] -- <workload_cmd>`
---------------------------------------------------------------------------------
Examples:
@@ -115,7 +121,7 @@ Examples:
\trocprof-compute profile -n vcopy_disp -d 0 -- ./vcopy -n 1048576 -b 256
\trocprof-compute profile -n vcopy_roof --roof-only -- ./vcopy -n 1048576 -b 256
---------------------------------------------------------------------------------
""",
""", # noqa: E501
prog="tool",
allow_abbrev=False,
formatter_class=lambda prog: argparse.RawTextHelpFormatter(
@@ -125,7 +131,7 @@ Examples:
profile_parser._optionals.title = "Help"
add_general_group(
profile_parser, rocprof_compute_version, supported_archs, rocprof_compute_home
profile_parser, rocprof_compute_home, supported_archs, rocprof_compute_version
)
profile_group = profile_parser.add_argument_group("Profile Options")
roofline_group = profile_parser.add_argument_group("Standalone Roofline Options")
@@ -147,11 +153,10 @@ Examples:
metavar="",
type=str,
dest="path",
default=str(Path(os.getcwd()).joinpath("workloads")),
default=str(Path(os.getcwd()) / "workloads"),
required=False,
help=(
"\t\t\tSpecify path to save workload.\n\t\t\t"
"(DEFAULT: {}/workloads/<name>)".format(os.getcwd())
f"\t\t\tSpecify path to save workload.\n\t\t\t(DEFAULT: {os.getcwd()}/workloads/<name>)" # noqa: E501
),
)
profile_group.add_argument(
@@ -207,10 +212,9 @@ Examples:
help="\t\t\tDispatch ID filtering.",
)
def validate_block(value):
def validate_block(value: str) -> str:
# Metric id is of the form I or I.I or I.I.I where I is two digit number.
metric_id_pattern = re.compile(r"^\d{1,2}(?:\.\d{1,2}){0,2}$")
if metric_id_pattern.match(value):
if re.compile(r"^\d{1,2}(?:\.\d{1,2}){0,2}$").match(value):
return value
raise argparse.ArgumentTypeError(f"Invalid metric id: {value}")
@@ -252,7 +256,6 @@ Examples:
"\t\t\tCannot be used with --block or --roof-only"
),
)
profile_group.add_argument(
"--join-type",
metavar="",
@@ -298,7 +301,6 @@ Examples:
default="csv",
help="\t\t\tSet the format of output file of rocprof.",
)
profile_group.add_argument(
"--pc-sampling-method",
required=False,
@@ -310,7 +312,6 @@ Examples:
"Support stochastic only >= MI300"
),
)
profile_group.add_argument(
"--pc-sampling-interval",
required=False,
@@ -324,7 +325,6 @@ Examples:
"(DEFAULT: 1048576)."
),
)
profile_group.add_argument(
"--rocprofiler-sdk-library-path",
type=str,
@@ -400,7 +400,6 @@ Examples:
action="store_true",
help="\t\t\tInclude kernel names in roofline plot.",
)
roofline_group.add_argument(
"-R",
"--roofline-data-type",
@@ -468,12 +467,10 @@ Examples:
\n\n-------------------------------------------------------------------------------
\nExamples:
\n\trocprof-compute database --import -H pavii1 -u temp -t asw -w "
"workloads/vcopy/mi200/"
"\n\trocprof-compute database --remove -H pavii1 -u temp -w "
"rocprofiler-compute_asw_sample_mi200"
"\n-------------------------------------------------------------------------------\n"
""",
\n\trocprof-compute database --import -H pavii1 -u temp -t asw -w "workloads/vcopy/mi200/"
\n\trocprof-compute database --remove -H pavii1 -u temp -w "rocprofiler-compute_asw_sample_mi200"
\n-------------------------------------------------------------------------------\n
""", # noqa: E501
prog="tool",
allow_abbrev=False,
formatter_class=lambda prog: argparse.RawTextHelpFormatter(
@@ -483,7 +480,7 @@ Examples:
db_parser._optionals.title = "Help"
add_general_group(
db_parser, rocprof_compute_version, supported_archs, rocprof_compute_home
db_parser, rocprof_compute_home, supported_archs, rocprof_compute_version
)
interaction_group = db_parser.add_argument_group("Interaction Type")
connection_group = db_parser.add_argument_group("Connection Options")
@@ -494,7 +491,7 @@ Examples:
required=False,
dest="upload",
action="store_true",
help="\t\t\t\tImport workload to rocprofiler-compute DB",
help="\t\tImport workload to rocprofiler-compute DB",
)
interaction_group.add_argument(
"-r",
@@ -502,7 +499,7 @@ Examples:
required=False,
dest="remove",
action="store_true",
help="\t\t\t\tRemove a workload from rocprofiler-compute DB",
help="\t\tRemove a workload from rocprofiler-compute DB",
)
connection_group.add_argument(
@@ -510,14 +507,14 @@ Examples:
"--host",
required=True,
metavar="",
help="\t\t\t\tName or IP address of the server host.",
help="\t\tName or IP address of the server host.",
)
connection_group.add_argument(
"-P",
"--port",
required=False,
metavar="",
help="\t\t\t\tTCP/IP Port. (DEFAULT: 27018)",
help="\t\tTCP/IP Port. (DEFAULT: 27018)",
default=27018,
)
connection_group.add_argument(
@@ -525,17 +522,17 @@ Examples:
"--username",
required=True,
metavar="",
help="\t\t\t\tUsername for authentication.",
help="\t\tUsername for authentication.",
)
connection_group.add_argument(
"-p",
"--password",
metavar="",
help="\t\t\t\tThe user's password. (will be requested later if it's not set)",
help="\t\tThe user's password. (will be requested later if it's not set)",
default="",
)
connection_group.add_argument(
"-t", "--team", required=False, metavar="", help="\t\t\t\tSpecify Team prefix."
"-t", "--team", required=False, metavar="", help="\t\tSpecify Team prefix."
)
connection_group.add_argument(
"-w",
@@ -544,8 +541,7 @@ Examples:
metavar="",
dest="workload",
help=(
"\t\t\t\tSpecify name of workload (to remove) or path to workload "
"(to import)"
"\t\tSpecify name of workload (to remove) or path to workload (to import)"
),
)
connection_group.add_argument(
@@ -585,7 +581,7 @@ Examples:
analyze_parser._optionals.title = "Help"
add_general_group(
analyze_parser, rocprof_compute_version, supported_archs, rocprof_compute_home
analyze_parser, rocprof_compute_home, supported_archs, rocprof_compute_version
)
analyze_group = analyze_parser.add_argument_group("Analyze Options")
analyze_advanced_group = analyze_parser.add_argument_group("Advanced Options")
@@ -728,7 +724,6 @@ Examples:
"\t\t\t I64\n\t\t\t "
),
)
analyze_group.add_argument(
"--pc-sampling-sorting-type",
required=False,
@@ -806,7 +801,7 @@ Examples:
nargs="+",
help=(
"\t\tSpecify which hidden column names should be included in cli output.\n"
"\t\tFor example, to show 'Description' column which is hidden by "
'\t\tFor example, to show "Description" column which is hidden by '
"default in cli output,\n"
"\t\tuse the option --include-cols Description."
),
@@ -837,8 +832,7 @@ Examples:
type=str,
metavar="",
help="\t\tSpecify the specs to correct. e.g. "
"--specs-correction='specname1:specvalue1,"
"specname2:specvalue2'",
'--specs-correction="specname1:specvalue1,specname2:specvalue2"',
)
analyze_advanced_group.add_argument(
"--list-nodes",
@@ -25,11 +25,8 @@
# SOFTWARE.
##############################################################################el
import os
import re
import sys
# import logging
from pathlib import Path
try:
@@ -38,112 +35,109 @@ try:
from rocprof_compute_base import RocProfCompute
from utils.logger import console_error
except ImportError as e:
print (f"{e}")
# In wheel package, softlink is not supported.
# rocprof-compute will get installed in bin and libexec/rocprofiler-compute.
# When invoked from bin folder, the extra paths are required to import the dependent scripts
# When invoked from bin folder, the extra paths are required to import the
# dependent scripts
try:
current_path = os.path.dirname(os.path.abspath(__file__))
additional_path = f"{current_path}/../libexec/rocprofiler-compute"
sys.path.append(os.path.abspath(additional_path))
current_path = Path(__file__).resolve().parent
additional_path = current_path / "../libexec/rocprofiler-compute"
sys.path.append(str(additional_path.resolve()))
from importlib import metadata
from rocprof_compute_base import RocProfCompute
from utils.utils import console_error
except ImportError as e:
# print("Failed to import required modules: " + str(e))
except ImportError:
pass
def verify_deps_version(localVer, desiredVer, operator):
def check_version(local_ver, desired_ver, operator) -> bool:
"""Check package version strings with simple operators used in companion
requirements.txt file"""
if operator == "==":
return localVer == desiredVer
elif operator == ">=":
return localVer >= desiredVer
elif operator == "<=":
return localVer <= desiredVer
elif operator == ">":
return localVer > desiredVer
elif operator == "<":
return localVer < desiredVer
else:
return True
ops = {
"==": lambda loc, des: loc == des,
">=": lambda loc, des: loc >= des,
"<=": lambda loc, des: loc <= des,
">": lambda loc, des: loc > des,
"<": lambda loc, des: loc < des,
}
return ops.get(operator, lambda loc, des: True)(local_ver, desired_ver)
def verify_deps():
def verify_deps() -> None:
"""Utility to read library dependencies from requirements.txt and endeavor
to load them within current execution environment.
Used in top-level rocprofiler-compute to provide error messages if necessary
dependencies are not available."""
# Check which version of python is being used
if sys.version_info[0] < 3 or (sys.version_info[0] == 3
and sys.version_info[1] < 8):
print("[ERROR] Python 3.8 or higher is required to run rocprofiler-compute."
f" The current version is {sys.version_info[0]}.{sys.version_info[1]}.")
if sys.version_info < (3, 8):
print(
f"[ERROR] Python 3.8 or higher is required to run rocprofiler-compute. "
f"The current version is {sys.version_info[0]}.{sys.version_info[1]}."
)
sys.exit(1)
bindir = str(Path(__file__).resolve().parent)
depsLocation = ["requirements.txt", "../requirements.txt"]
deps_locations = ["requirements.txt", "../requirements.txt"]
for location in depsLocation:
checkFile = str(Path(bindir).joinpath(location))
if Path(checkFile).exists():
with open(checkFile, "r", encoding="utf-8") as file_in:
dependencies = file_in.read().splitlines()
for location in deps_locations:
check_file = Path(bindir).joinpath(location)
if check_file.exists():
dependencies = check_file.read_text(encoding="utf-8").splitlines()
error = False
version_pattern = r"^([^=<>]+)([=<>]+)(.*)$"
version_pattern = re.compile(r"^([^=<>]+)([=<>]+)(.*)$")
for dependency in dependencies:
desiredVersion = None
match = re.match(version_pattern, dependency)
match = version_pattern.match(dependency)
if match:
package = match.group(1)
operator = match.group(2) or None
desiredVersion = match.group(3) or None
package, operator, desired_version = match.groups()
else:
package = dependency
package, operator, desired_version = dependency, None, None
try:
localVersion = metadata.distribution(package).version
local_version = metadata.distribution(package).version
except metadata.PackageNotFoundError:
error = True
print(f"[ERROR] The '{dependency}' package was not found "
"in the current execution environment.")
print(
f"[ERROR] The '{dependency}' package was not found "
"in the current execution environment."
)
continue
# check version requirement
if not error:
if desiredVersion:
if not verify_deps_version(localVersion, desiredVersion,
operator):
print(
f"[ERROR] the '{dependency}' distribution does "
"not meet version requirements to use rocprofiler-compute."
)
print(" --> version installed :", localVersion)
error = True
# Check version requirement
if desired_version and not check_version(
local_version, desired_version, operator
):
print(
f"[ERROR] the '{dependency}' distribution does not meet "
"version requirements to use rocprofiler-compute."
)
print(f" --> version installed : {local_version}")
error = True
if error:
print("")
print("Please verify all of the python dependencies called out "
"in the requirements file")
print(
"\nPlease verify all of the python dependencies called out "
"in the requirements file"
)
print("are installed locally prior to running rocprofiler-compute.")
print("")
print(f"See: {checkFile}")
print(f"\nSee: {check_file}")
sys.exit(1)
return
return
def main():
def main() -> None:
"""Main function for rocprofiler-compute"""
# verify required python dependencies
verify_deps()
rocprof_compute = RocProfCompute()
mode = rocprof_compute.get_mode()
# major rocprofiler-compute execution modes
@@ -23,6 +23,7 @@
##############################################################################
import argparse
import copy
import re
import sys
@@ -30,10 +31,12 @@ import textwrap
from abc import abstractmethod
from collections import OrderedDict
from pathlib import Path
from typing import Any, Optional, TextIO
import pandas as pd
import config
from rocprof_compute_soc.soc_base import OmniSoC_Base
from utils import file_io, parser, schema
from utils.logger import (
console_debug,
@@ -44,146 +47,104 @@ from utils.logger import (
)
from utils.utils import get_uuid, is_workload_empty, merge_counters_spatial_multiplex
# the build-in config to list kernel names purpose only
TOP_STATS_BUILD_IN_CONFIG: OrderedDict[int, dict[str, Any]] = OrderedDict([
(
0,
{
"id": 0,
"title": "Top Kernels",
"data source": [
{"raw_csv_table": {"id": 1, "source": "pmc_kernel_top.csv"}}
],
},
),
(
1,
{
"id": 1,
"title": "Dispatch List",
"data source": [
{"raw_csv_table": {"id": 2, "source": "pmc_dispatch_info.csv"}}
],
},
),
])
class OmniAnalyze_Base:
def __init__(self, args, supported_archs):
def __init__(
self, args: argparse.Namespace, supported_archs: dict[str, str]
) -> None:
self.__args = args
self._runs = OrderedDict()
self._arch_configs = {}
self._profiling_config = dict()
self._runs: OrderedDict[str, schema.Workload] = OrderedDict()
self._arch_configs: dict[str, schema.ArchConfig] = {}
self._profiling_config: dict[str, Any] = {}
self.__supported_archs = supported_archs
self._output = None
self.__socs: dict = None # available OmniSoC objs
self._output: Optional[TextIO] = None
self.__socs: Optional[dict[str, OmniSoC_Base]] = None
def get_args(self):
def get_args(self) -> argparse.Namespace:
return self.__args
def set_soc(self, omni_socs):
def set_soc(self, omni_socs: dict[str, OmniSoC_Base]) -> None:
self.__socs = omni_socs
def get_socs(self):
def get_socs(self) -> Optional[dict[str, OmniSoC_Base]]:
return self.__socs
@demarcate
def spatial_multiplex_merge_counters(self, df):
def spatial_multiplex_merge_counters(self, df: pd.DataFrame) -> pd.DataFrame:
return merge_counters_spatial_multiplex(df)
@demarcate
def generate_configs(self, arch, config_dir, list_stats, filter_metrics, sys_info):
def generate_configs(
self,
arch: str,
config_dir: str,
list_stats: bool,
filter_metrics: Optional[list[str]],
sys_info: pd.Series,
) -> dict[str, schema.ArchConfig]:
single_panel_config = file_io.is_single_panel_config(
Path(config_dir), self.__supported_archs
config_dir, self.__supported_archs
)
ac = schema.ArchConfig()
if list_stats:
ac.panel_configs = file_io.top_stats_build_in_config
ac.panel_configs = TOP_STATS_BUILD_IN_CONFIG
else:
arch_panel_config = [
config_dir if single_panel_config else config_dir.joinpath(arch)
config_dir if single_panel_config else str(f"{config_dir}/{arch}")
]
# Use restructured perf metrics in TUI analyze mode
if self.__args.tui and arch in ["gfx942", "gfx950"]:
if self.get_args().tui and arch in ["gfx942", "gfx950"]:
arch_panel_config.append(
f"{config.rocprof_compute_home}/rocprof_compute_tui/utils/{arch}"
str(
config.rocprof_compute_home
/ "rocprof_compute_tui"
/ "utils"
/ arch
)
)
ac.panel_configs = file_io.load_panel_configs(arch_panel_config)
# TODO: filter_metrics should/might be one per arch
# print(ac)
parser.build_dfs(
archConfigs=ac, filter_metrics=filter_metrics, sys_info=sys_info
arch_configs=ac, filter_metrics=filter_metrics, sys_info=sys_info
)
self._arch_configs[arch] = ac
return self._arch_configs
@demarcate
def list_metrics(self):
args = self.__args
if args.list_metrics in self.__supported_archs.keys():
arch = args.list_metrics
if arch not in self._arch_configs.keys():
sys_info = file_io.load_sys_info(
Path(self.__args.path[0][0], "sysinfo.csv")
)
self.generate_configs(
arch,
args.config_dir,
args.list_stats,
args.filter_metrics,
sys_info.iloc[0],
)
def list_metrics(self) -> None:
args = self.get_args()
arch = args.list_metrics
metric_descriptions = {
k: v
for dfs in self._arch_configs[args.list_metrics].dfs.values()
for k, v in dfs.to_dict().get("Description", {}).items()
}
for key, value in self._arch_configs[args.list_metrics].metric_list.items():
prefix = ""
description = ""
if "." not in str(key):
prefix = ""
elif str(key).count(".") == 1:
prefix = "\t"
else:
prefix = "\t\t"
description = metric_descriptions.get(key, "")
print(prefix + key, "->", value + "\n")
if description:
print(
prefix
+ f"\n{prefix}".join(textwrap.wrap(description, width=40))
+ "\n"
)
sys.exit(0)
else:
console_error("Unsupported arch")
@demarcate
def load_options(self, normalization_filter):
if not normalization_filter:
for k, v in self._arch_configs.items():
parser.build_metric_value_string(
v.dfs, v.dfs_type, self.__args.normal_unit, self._profiling_config
)
else:
for k, v in self._arch_configs.items():
parser.build_metric_value_string(
v.dfs, v.dfs_type, normalization_filter, self._profiling_config
)
args = self.__args
# Error checking for multiple runs and multiple kernel filters
if args.gpu_kernel and (len(args.path) != len(args.gpu_kernel)):
if len(args.gpu_kernel) == 1:
for i in range(len(args.path) - 1):
args.gpu_kernel.extend(args.gpu_kernel)
else:
console_error(
"analysis"
"The number of -k/--kernel doesn't match the number of --dir."
)
@demarcate
def initalize_runs(self, normalization_filter=None):
if self.__args.list_metrics:
self.list_metrics()
def get_sysinfo_path(data_path):
return (
Path(data_path)
if self.__args.nodes is None
and self.__args.spatial_multiplexing is not True
else file_io.find_1st_sub_dir(data_path)
)
# load required configs
for d in self.__args.path:
sysinfo_path = get_sysinfo_path(d[0])
sys_info = file_io.load_sys_info(sysinfo_path.joinpath("sysinfo.csv"))
arch = sys_info.iloc[0]["gpu_arch"]
args = self.__args
if arch not in self.__supported_archs:
console_error("analysis", "Unsupported arch")
if arch not in self._arch_configs:
sys_info = file_io.load_sys_info(f"{args.path[0][0]}/sysinfo.csv")
self.generate_configs(
arch,
args.config_dir,
@@ -192,163 +153,244 @@ class OmniAnalyze_Base:
sys_info.iloc[0],
)
metric_descriptions = {
k: v
for dfs in self._arch_configs[arch].dfs.values()
for k, v in dfs.to_dict().get("Description", {}).items()
}
for key, value in self._arch_configs[arch].metric_list.items():
dot_count = str(key).count(".")
if dot_count == 0:
prefix = ""
elif dot_count == 1:
prefix = "\t"
else:
prefix = "\t\t"
description = metric_descriptions.get(key, "") if dot_count > 1 else ""
print(f"{prefix}{key} -> {value}\n")
if description:
formatted_desc = f"\n{prefix}".join(
textwrap.wrap(description, width=40)
)
print(f"{prefix}{formatted_desc}\n")
sys.exit(0)
@demarcate
def load_options(self, normalization_filter: Optional[str]) -> None:
args = self.get_args()
target_filter = normalization_filter or args.normal_unit
for arch_config in self._arch_configs.values():
parser.build_metric_value_string(
arch_config.dfs,
arch_config.dfs_type,
target_filter,
self._profiling_config,
)
# Error checking for multiple runs and multiple kernel filters
if args.gpu_kernel and (len(args.path) != len(args.gpu_kernel)):
if len(args.gpu_kernel) == 1:
args.gpu_kernel *= len(args.path)
else:
console_error(
"analysis"
"The number of -k/--kernel doesn't match the number of --dir."
)
@demarcate
def initalize_runs(
self, normalization_filter: Optional[str] = None
) -> OrderedDict[str, schema.Workload]:
args = self.get_args()
if args.list_metrics:
self.list_metrics()
def get_sysinfo_path(data_path: str) -> Optional[str]:
return (
data_path
if args.nodes is None and not args.spatial_multiplexing
else file_io.find_1st_sub_dir(data_path)
)
# load required configs
for path_info in args.path:
sysinfo_path = get_sysinfo_path(path_info[0])
if sysinfo_path:
sys_info = file_io.load_sys_info(f"{sysinfo_path}/sysinfo.csv")
arch = sys_info.iloc[0]["gpu_arch"]
self.generate_configs(
arch,
args.config_dir,
args.list_stats,
args.filter_metrics,
sys_info.iloc[0],
)
self.load_options(normalization_filter)
for d in self.__args.path:
w = schema.Workload()
for path_info in args.path:
# FIXME:
# For regular single node case, load sysinfo.csv directly
# For multi-node, either the default "all", or specified some,
# pick up the one in the 1st sub_dir. We could fix it properly later.
w = schema.Workload()
sysinfo_path = get_sysinfo_path(d[0])
w.sys_info = file_io.load_sys_info(sysinfo_path.joinpath("sysinfo.csv"))
if not getattr(self.get_args(), "no_roof", False):
try:
roofline_csv_path = sysinfo_path / "roofline.csv"
roofline_df = pd.read_csv(roofline_csv_path)
w.roofline_peaks = roofline_df
except FileNotFoundError:
console_warning("roofline.csv not found.")
sysinfo_path = get_sysinfo_path(path_info[0])
if sysinfo_path:
w.sys_info = file_io.load_sys_info(f"{sysinfo_path}/sysinfo.csv")
if not getattr(args, "no_roof", False):
try:
roofline_df = pd.read_csv(f"{sysinfo_path}/roofline.csv")
w.roofline_peaks = roofline_df
except FileNotFoundError:
console_warning("roofline.csv not found.")
w.roofline_peaks = pd.DataFrame()
else:
w.roofline_peaks = pd.DataFrame()
else:
w.roofline_peaks = pd.DataFrame()
arch = w.sys_info.iloc[0]["gpu_arch"]
mspec = self.get_socs()[arch]._mspec
if self.__args.specs_correction:
w.sys_info = parser.correct_sys_info(
mspec, self.__args.specs_correction
)
w.avail_ips = w.sys_info["ip_blocks"].item().split("|")
w.dfs = copy.deepcopy(self._arch_configs[arch].dfs)
w.dfs_type = self._arch_configs[arch].dfs_type
self._runs[d[0]] = w
arch = w.sys_info.iloc[0]["gpu_arch"]
socs = self.get_socs()
if socs and arch in socs:
mspec = socs[arch]._mspec
if args.specs_correction:
w.sys_info = parser.correct_sys_info(
mspec, args.specs_correction
)
w.avail_ips = w.sys_info["ip_blocks"].item().split("|")
w.dfs = copy.deepcopy(self._arch_configs[arch].dfs)
w.dfs_type = self._arch_configs[arch].dfs_type
self._runs[path_info[0]] = w
return self._runs
@demarcate
def sanitize(self):
def sanitize(self) -> None:
"""Perform sanitization of inputs"""
if self.__args.tui:
args = self.get_args()
if args.tui:
return
if not self.__args.path:
if not args.path:
console_error("The following arguments are required: -p/--path")
# verify not accessing parent directories
if ".." in str(self.__args.path):
if ".." in str(args.path):
console_error(
"Access denied. Cannot access parent directories in path (i.e. ../)"
)
# ensure absolute path
for dir in self.__args.path:
full_path = str(Path(dir[0]).absolute().resolve())
dir[0] = full_path
if not Path(dir[0]).is_dir():
console_error("Invalid directory {}\nPlease try again.".format(dir[0]))
seen_paths: set[str] = set()
for dir_info in args.path:
full_path = Path(dir_info[0]).absolute().resolve()
dir_info[0] = str(full_path)
if not full_path.is_dir():
console_error(
"analysis", f"Invalid directory {full_path}\nPlease try again."
)
# validate profiling data
# Todo: more err check
if not (
self.__args.nodes is not None
or self.__args.list_nodes
or self.__args.spatial_multiplexing
):
is_workload_empty(dir[0])
# else:
if dir_info[0] in seen_paths:
console_error("analysis", "You cannot provide the same path twice.")
seen_paths.add(dir_info[0])
# no using same paths
occurances = set()
for dir in self.__args.path:
dir = dir[0]
if dir in occurances:
console_error("You cannot provide the same path twice.")
else:
occurances.add(dir)
if not any([
args.nodes,
args.list_nodes,
args.spatial_multiplexing,
]):
is_workload_empty(dir_info[0])
# FIXME:
# The proper location of this func should be in pre_processing().
# However, because of reading soc depends on sys spec, and sys
# spec depends on sys_info. And we read sys_info too early so we
# . can not do it now. There should be a way to make it simpler.
if self.__args.list_nodes:
nodes = []
if args.list_nodes:
# NB:
# There are 2 ways to do it: one is doing like the below, checking
# sub dirs only as we assume the profiling stage generate sub dirs
# with node name. The 2nd way would be checkign host name in each
# sub dir and very those.
for subdir in Path(self.__args.path[0][0]).iterdir():
if subdir.is_dir():
nodes.append(str(subdir.name))
nodes = [
subdir.name
for subdir in Path(args.path[0][0]).iterdir()
if subdir.is_dir()
]
print("Node list:", " ".join(nodes))
sys.exit(0)
# Ensure analysis output does not overwrite existing files
if self.__args.output_name:
if not re.match(r"^[A-Za-z0-9_-]+$", self.__args.output_name):
console_error(
"Analysis output file/folder name must "
"contain only alphanumeric characters "
"or underscores (_), hyphens (-)."
)
path_to_check = self.__args.output_name
if self.__args.output_format in ("txt", "db"):
path_to_check += f".{self.__args.output_format}"
if Path(path_to_check).exists():
console_error(
f"Analysis output file/folder {path_to_check} already exists. "
"Please choose a different name."
)
if not args.output_name:
return
if not re.match(r"^[A-Za-z0-9_-]+$", args.output_name):
console_error(
"analysis",
"Analysis output file/folder name must "
"contain only alphanumeric characters "
"or underscores (_), hyphens (-).",
)
path_to_check = args.output_name
if args.output_format in ("txt", "db"):
path_to_check += f".{args.output_format}"
if Path(path_to_check).exists():
console_error(
f"Analysis output file/folder {path_to_check} already exists. "
"Please choose a different name."
)
# ----------------------------------------------------
# Required methods to be implemented by child classes
# ----------------------------------------------------
@abstractmethod
def pre_processing(self):
def pre_processing(self) -> None:
"""Perform initialization prior to analysis."""
console_debug("analysis", "prepping to do some analysis")
console_log("analysis", "deriving rocprofiler-compute metrics...")
args = self.get_args()
# initalize output file
if self.__args.output_format == "txt":
output_filename = self.__args.output_name or f"rocprof_compute_{get_uuid()}"
if args.output_format == "txt":
output_filename = args.output_name or f"rocprof_compute_{get_uuid()}"
output_filename += ".txt"
self._output = open(output_filename, "w+")
console_warning(f"Created file: {output_filename}")
elif self.__args.output_format == "stdout":
console_warning("analysis", f"Created file: {output_filename}")
elif args.output_format == "stdout":
self._output = sys.stdout
# Read profiling config
self._profiling_config = file_io.load_profiling_config(self.__args.path[0][0])
self._profiling_config = file_io.load_profiling_config(args.path[0][0])
# initalize runs
self._runs = self.initalize_runs()
# set filters
if self.__args.gpu_kernel:
for d, gk in zip(self.__args.path, self.__args.gpu_kernel):
self._runs[d[0]].filter_kernel_ids = gk
if self.__args.gpu_id:
if len(self.__args.gpu_id) == 1 and len(self.__args.path) != 1:
for i in range(len(self.__args.path) - 1):
self.__args.gpu_id.extend(self.__args.gpu_id)
for d, gi in zip(self.__args.path, self.__args.gpu_id):
self._runs[d[0]].filter_gpu_ids = gi
if self.__args.gpu_dispatch_id:
if len(self.__args.gpu_dispatch_id) == 1 and len(self.__args.path) != 1:
for i in range(len(self.__args.path) - 1):
self.__args.gpu_dispatch_id.extend(self.__args.gpu_dispatch_id)
for d, gd in zip(self.__args.path, self.__args.gpu_dispatch_id):
self._runs[d[0]].filter_dispatch_ids = gd
if self.__args.nodes:
if len(self.__args.nodes) == 1 and len(self.__args.path) != 1:
for i in range(len(self.__args.path) - 1):
self.__args.nodes.extend(self.__args.nodes)
for d, gd in zip(self.__args.path, self.__args.nodes):
self._runs[d[0]].nodes = gd
filter_configs = [
(args.gpu_kernel, "filter_kernel_ids"),
(args.gpu_id, "filter_gpu_ids"),
(args.gpu_dispatch_id, "filter_dispatch_ids"),
(args.nodes, "nodes"),
]
for filter_list, attr_name in filter_configs:
if not filter_list:
continue
# Extend single filter to match all paths
if len(filter_list) == 1 and len(args.path) > 1:
filter_list *= len(args.path)
# Apply filters to workloads
for path_info, filter_value in zip(args.path, filter_list):
setattr(self._runs[path_info[0]], attr_name, filter_value)
@abstractmethod
def run_analysis(self):
def run_analysis(self) -> None:
"""Run analysis."""
console_debug("analysis", "generating analysis")
@@ -23,6 +23,7 @@
##############################################################################
from rocprof_compute_analyze.analysis_base import OmniAnalyze_Base
from utils import file_io, parser, tty
from utils.kernel_name_shortener import kernel_name_shortener
@@ -34,89 +35,97 @@ class cli_analysis(OmniAnalyze_Base):
# Required child methods
# -----------------------
@demarcate
def pre_processing(self):
def pre_processing(self) -> None:
"""Perform any pre-processing steps prior to analysis."""
super().pre_processing()
if self.get_args().random_port:
args = self.get_args()
if args.random_port:
console_error("--gui flag is required to enable --random-port")
for d in self.get_args().path:
workload = self._runs[d[0]]
for path_info in args.path:
workload = self._runs[path_info[0]]
# create 'mega dataframe'
workload.raw_pmc = file_io.create_df_pmc(
d[0],
self.get_args().nodes,
self.get_args().spatial_multiplexing,
self.get_args().kernel_verbose,
self.get_args().verbose,
path_info[0],
args.nodes,
args.spatial_multiplexing,
args.kernel_verbose,
args.verbose,
self._profiling_config,
)
if self.get_args().spatial_multiplexing:
if args.spatial_multiplexing:
workload.raw_pmc = self.spatial_multiplex_merge_counters(
workload.raw_pmc
)
file_io.create_df_kernel_top_stats(
df_in=workload.raw_pmc,
raw_data_dir=d[0],
raw_data_dir=path_info[0],
filter_gpu_ids=workload.filter_gpu_ids,
filter_dispatch_ids=workload.filter_dispatch_ids,
filter_nodes=workload.filter_nodes,
time_unit=self.get_args().time_unit,
max_stat_num=self.get_args().max_stat_num,
kernel_verbose=self.get_args().kernel_verbose,
time_unit=args.time_unit,
kernel_verbose=args.kernel_verbose,
)
# demangle and overwrite original 'Kernel_Name'
kernel_name_shortener(workload.raw_pmc, self.get_args().kernel_verbose)
kernel_name_shortener(workload.raw_pmc, args.kernel_verbose)
# create the loaded table
parser.load_table_data(
workload=workload,
dir=d[0],
dir_path=path_info[0],
is_gui=False,
args=self.get_args(),
args=args,
config=self._profiling_config,
)
@demarcate
def run_analysis(self):
def run_analysis(self) -> None:
"""Run CLI analysis."""
super().run_analysis()
workload_path = self.get_args().path[0][0]
args = self.get_args()
workload_path = args.path[0][0]
workload = self._runs[workload_path]
gpu_arch = workload.sys_info.iloc[0]["gpu_arch"]
arch_config = self._arch_configs[gpu_arch]
if self.get_args().list_stats:
if args.list_stats:
tty.show_kernel_stats(
self.get_args(),
args,
self._runs,
arch_config,
self._output,
)
else:
roof_plot = None
# 1. check if not baseline && compatible soc:
if (len(self.get_args().path)) == 1:
# Generate roofline plot for single-path, compatible architectures
if (len(args.path)) == 1:
if gpu_arch in ["gfx90a", "gfx940", "gfx941", "gfx942", "gfx950"]:
roof_obj = self.get_socs()[gpu_arch].roofline_obj
soc = self.get_socs()
if soc and gpu_arch in soc:
roof_obj = soc[gpu_arch].roofline_obj
if roof_obj:
# store path in workload for calc_ai_analyze
workload.path = workload_path
if roof_obj:
# store path in workload for calc_ai_analyze
workload.path = workload_path
# NOTE: using default data type
roof_plot = roof_obj.cli_generate_plot(
dtype=roof_obj.get_dtype()[0],
workload=workload,
config=self._profiling_config,
arch_config=arch_config,
)
# NOTE: using default data type
roof_plot = roof_obj.cli_generate_plot(
dtype=roof_obj.get_dtype()[0],
workload=workload,
config=self._profiling_config,
arch_config=arch_config,
)
tty.show_all(
self.get_args(),
args,
self._runs,
arch_config,
self._output,
@@ -39,7 +39,7 @@ from utils.logger import console_debug, console_error, console_warning, demarcat
from utils.parser import (
PC_SAMPLING_NOT_ISSUE_PREFIX,
CodeTransformer,
build_in_vars,
BUILD_IN_VARS,
to_avg,
to_concat,
to_int,
@@ -409,14 +409,14 @@ class db_analysis(OmniAnalyze_Base):
sys_info[f"{key}_empirical_peak"] = value
# Calculate PER_XCD variables first
for key, value in build_in_vars.items():
for key, value in BUILD_IN_VARS.items():
if "PER_XCD" in key:
sys_info[key] = db_analysis.evaluate(
key, value, pmc_df, sys_info, parse=True
)
# variable dependent on PER_XCD variables
for key, value in build_in_vars.items():
for key, value in BUILD_IN_VARS.items():
if "PER_XCD" not in key:
sys_info[key] = db_analysis.evaluate(
key, value, pmc_df, sys_info, parse=True
@@ -23,35 +23,41 @@
##############################################################################
import argparse
import copy
import random
from pathlib import Path
from typing import Any, Optional
import dash
import dash_bootstrap_components as dbc
import pandas as pd
from dash import dcc, html
from dash.dependencies import Input, Output, State
from config import HIDDEN_COLUMNS, PROJECT_NAME
from rocprof_compute_analyze.analysis_base import OmniAnalyze_Base
from utils import file_io, parser
from utils import file_io, parser, schema
from utils.gui import build_bar_chart, build_table_chart
from utils.logger import console_debug, console_error, console_warning, demarcate
class webui_analysis(OmniAnalyze_Base):
def __init__(self, args, supported_archs):
def __init__(
self, args: argparse.Namespace, supported_archs: dict[str, str]
) -> None:
super().__init__(args, supported_archs)
self.app = dash.Dash(
__name__, title=PROJECT_NAME, external_stylesheets=[dbc.themes.CYBORG]
)
self.dest_dir = str(Path(args.path[0][0]).absolute().resolve())
self.arch = None
self.arch: Optional[str] = None
self.__hidden_sections = ["Memory Chart"]
self.__hidden_columns = HIDDEN_COLUMNS
# define different types of bar charts
self.__barchart_elements = {
self.__barchart_elements: dict[str, list[int]] = {
"instr_mix": [1001, 1002],
# 1604: L1D - L2 Transactions
# 1705: L2 - Fabric Interface Stalls
@@ -59,27 +65,33 @@ class webui_analysis(OmniAnalyze_Base):
"sol": [1101, 1201, 1301, 1401, 1601, 1701],
# "l2_cache_per_chan": [1802, 1803]
}
# define any elements which will have full width
self.__full_width_elements = {1801}
# define any elements which will have full width
self.__full_width_elements: set[int] = {1801}
self.__roofline_data_type = args.roofline_data_type
@demarcate
def build_layout(self, input_filters, arch_configs):
def build_layout(
self, input_filters: dict[str, Any], arch_configs: schema.ArchConfig
) -> None:
"""
Build gui layout
"""
from utils.gui_components.header import get_header
from utils.gui_components.memchart import get_memchart
args = self.get_args()
comparable_columns = parser.build_comparable_columns(self.get_args().time_unit)
comparable_columns = parser.build_comparable_columns(args.time_unit)
base_run, base_data = next(iter(self._runs.items()))
self.app.layout = html.Div(style={"backgroundColor": "rgb(50, 50, 50)"})
filt_kernel_names = []
# get filtered kernel names from kernel ids
filt_kernel_names: list[str] = []
kernel_top_df = base_data.dfs[1]
for kernel_id in base_data.filter_kernel_ids:
filt_kernel_names.append(kernel_top_df.loc[kernel_id, "Kernel_Name"])
filt_kernel_names.append(str(kernel_top_df.loc[kernel_id, "Kernel_Name"]))
# setup app layout
from utils.gui_components.header import get_header
self.app.layout.children = html.Div(
children=[
@@ -105,36 +117,43 @@ class webui_analysis(OmniAnalyze_Base):
[State("container", "children")],
)
def generate_from_filter(
disp_filt, kernel_filter, gcd_filter, norm_filt, top_n_filt, div_children
):
console_debug("analysis", "gui normalization is %s" % norm_filt)
disp_filt: str,
kernel_filter: str,
gcd_filter: str,
norm_filt: str,
top_n_filt: str,
div_children: list[html.Section],
) -> list[html.Section]:
console_debug("analysis", f"gui normalization is {norm_filt}")
# Re-initalizes everything
base_data = self.initalize_runs(normalization_filter=norm_filt)
panel_configs = copy.deepcopy(arch_configs.panel_configs)
# Generate original raw df
base_data[base_run].raw_pmc = file_io.create_df_pmc(
self.dest_dir,
self.get_args().nodes,
self.get_args().spatial_multiplexing,
self.get_args().kernel_verbose,
self.get_args().verbose,
args.nodes,
args.spatial_multiplexing,
args.kernel_verbose,
args.verbose,
self._profiling_config,
)
if self.get_args().spatial_multiplexing:
if args.spatial_multiplexing:
base_data[base_run].raw_pmc = self.spatial_multiplex_merge_counters(
base_data[base_run].raw_pmc
)
console_debug("analysis", "gui dispatch filter is %s" % disp_filt)
console_debug("analysis", "gui kernel filter is %s" % kernel_filter)
console_debug("analysis", "gui gpu filter is %s" % gcd_filter)
console_debug("analysis", "gui top-n filter is %s" % top_n_filt)
base_data[base_run].filter_kernel_ids = kernel_filter
base_data[base_run].filter_gpu_ids = gcd_filter
base_data[base_run].filter_dispatch_ids = disp_filt
# Apply filters to workload data
console_debug("analysis", f"gui dispatch filter is {disp_filt}")
console_debug("analysis", f"gui kernel filter is {kernel_filter}")
console_debug("analysis", f"gui gpu filter is {gcd_filter}")
console_debug("analysis", f"gui top-n filter is {top_n_filt}")
base_data[base_run].filter_kernel_ids = [int(kernel_filter)]
base_data[base_run].filter_gpu_ids = [int(gcd_filter)]
base_data[base_run].filter_dispatch_ids = [int(disp_filt)]
base_data[base_run].filter_top_n = top_n_filt
# Reload the pmc_kernel_top.csv for Top Stats panel
@@ -144,31 +163,35 @@ class webui_analysis(OmniAnalyze_Base):
filter_gpu_ids=base_data[base_run].filter_gpu_ids,
filter_dispatch_ids=base_data[base_run].filter_dispatch_ids,
filter_nodes=self._runs[self.dest_dir].filter_nodes,
time_unit=self.get_args().time_unit,
max_stat_num=base_data[base_run].filter_top_n,
kernel_verbose=self.get_args().kernel_verbose,
time_unit=args.time_unit,
kernel_verbose=args.kernel_verbose,
)
# Only display basic metrics if no filters are applied
if not (disp_filt or kernel_filter or gcd_filter):
temp = {}
keep = [1, 2, 101, 201, 301, 401, 402]
for key in base_data[base_run].dfs:
if keep.count(key) != 0:
temp[key] = base_data[base_run].dfs[key]
basic_dfs_keep = [1, 2, 101, 201, 301, 401, 402]
basic_panels_keep = [0, 100, 200, 300, 400]
# Filter dataframes
filtered_dfs = {
key: base_data[base_run].dfs[key]
for key in base_data[base_run].dfs
if key in basic_dfs_keep
}
base_data[base_run].dfs = filtered_dfs
panel_configs = {
key: panel_configs[key]
for key in panel_configs
if key in basic_panels_keep
}
base_data[base_run].dfs = temp
temp = {}
keep = [0, 100, 200, 300, 400]
for key in panel_configs:
if keep.count(key) != 0:
temp[key] = panel_configs[key]
panel_configs = temp
# All filtering will occur here
parser.load_table_data(
workload=base_data[base_run],
dir=self.dest_dir,
dir_path=self.dest_dir,
is_gui=True,
args=self.get_args(),
args=args,
config=self._profiling_config,
)
@@ -178,39 +201,46 @@ class webui_analysis(OmniAnalyze_Base):
div_children = []
# Append memory chart and roofline
from utils.gui_components.memchart import get_memchart
div_children.append(
get_memchart(panel_configs[300]["data source"], base_data[base_run])
)
has_roofline = Path(self.dest_dir).joinpath("roofline.csv").is_file()
if has_roofline and hasattr(self.get_socs()[self.arch], "roofline_obj"):
# update roofline for visualization in GUI
self.get_socs()[self.arch].analysis_setup(
roofline_parameters={
"workload_dir": self.dest_dir,
"device_id": 0,
"sort_type": "kernels",
"mem_level": "ALL",
"include_kernel_names": False,
"is_standalone": False,
"roofline_data_type": self.__roofline_data_type,
"kernel_filter": False,
}
)
roof_obj = self.get_socs()[self.arch].roofline_obj
div_children.append(
roof_obj.empirical_roofline(
ret_df=parser.apply_filters(
workload=base_data[base_run],
dir=self.dest_dir,
is_gui=True,
debug=self.get_args().debug,
has_roofline = (Path(self.dest_dir) / "roofline.csv").is_file()
soc = self.get_socs()
if soc and self.arch in soc:
if has_roofline and hasattr(soc[self.arch], "roofline_obj"):
# update roofline for visualization in GUI
soc[self.arch].analysis_setup(
roofline_parameters={
"workload_dir": self.dest_dir,
"device_id": 0,
"sort_type": "kernels",
"mem_level": "ALL",
"include_kernel_names": False,
"is_standalone": False,
"roofline_data_type": self.__roofline_data_type,
"kernel_filter": False,
}
)
roof_obj = soc[self.arch].roofline_obj
div_children.append(
roof_obj.empirical_roofline(
ret_df=parser.apply_filters(
workload=base_data[base_run],
dir=self.dest_dir,
is_gui=True,
debug=args.debug,
)
)
)
)
# Iterate over each section as defined in panel configs
for panel_id, panel in panel_configs.items():
title = str(panel_id // 100) + ". " + panel["title"]
if panel["title"] in self.__hidden_sections:
continue
title = f"{panel_id // 100}. {panel['title']}"
section_title = (
panel["title"]
.replace("(", "")
@@ -219,43 +249,40 @@ class webui_analysis(OmniAnalyze_Base):
.replace(" ", "_")
.lower()
)
# Build content for a single panel
html_section = []
if panel["title"] not in self.__hidden_sections:
# Iterate over each table per section
for data_source in panel["data source"]:
for t_type, table_config in data_source.items():
original_df = base_data[base_run].dfs[table_config["id"]]
# The sys info table need to add index back
if (
t_type == "raw_csv_table"
and "Info" in original_df.keys()
):
original_df.reset_index(inplace=True)
# Iterate over each table per section
for data_source in panel["data source"]:
for t_type, table_config in data_source.items():
original_df = base_data[base_run].dfs[table_config["id"]]
content = determine_chart_type(
original_df=original_df,
table_config=table_config,
hidden_columns=self.__hidden_columns,
barchart_elements=self.__barchart_elements,
norm_filt=norm_filt,
comparable_columns=comparable_columns,
decimal=self.get_args().decimal,
# The sys info table need to add index back
if t_type == "raw_csv_table" and "Info" in original_df.keys():
original_df.reset_index(inplace=True)
content = determine_chart_type(
original_df=original_df,
table_config=table_config,
hidden_columns=self.__hidden_columns,
barchart_elements=self.__barchart_elements,
comparable_columns=comparable_columns,
decimal=args.decimal,
)
# Update content for this section
div_style = (
{"width": "100%"}
if table_config["id"] in self.__full_width_elements
else {}
)
html_section.append(
html.Div(
className="float-child",
children=content,
style=div_style,
)
# Update content for this section
if table_config["id"] in self.__full_width_elements:
# Optionally override default (50%) width
html_section.append(
html.Div(
className="float-child",
children=content,
style={"width": "100%"},
)
)
else:
html_section.append(
html.Div(className="float-child", children=content)
)
)
# Append the new section with all of it's contents
div_children.append(
@@ -298,56 +325,54 @@ class webui_analysis(OmniAnalyze_Base):
# Required child methods
# -----------------------
@demarcate
def pre_processing(self):
def pre_processing(self) -> None:
"""Perform any pre-processing steps prior to analysis."""
super().pre_processing()
if len(self._runs) == 1:
args = self.get_args()
# create 'mega dataframe'
self._runs[self.dest_dir].raw_pmc = file_io.create_df_pmc(
self.dest_dir,
self.get_args().nodes,
self.get_args().spatial_multiplexing,
self.get_args().kernel_verbose,
args.verbose,
self._profiling_config,
)
if self.get_args().spatial_multiplexing:
self._runs[
self.dest_dir
].raw_pmc = self.spatial_multiplex_merge_counters(
self._runs[self.dest_dir].raw_pmc
)
file_io.create_df_kernel_top_stats(
df_in=self._runs[self.dest_dir].raw_pmc,
raw_data_dir=self.dest_dir,
filter_gpu_ids=self._runs[self.dest_dir].filter_gpu_ids,
filter_dispatch_ids=self._runs[self.dest_dir].filter_dispatch_ids,
filter_nodes=self._runs[self.dest_dir].filter_nodes,
time_unit=args.time_unit,
max_stat_num=args.max_stat_num,
kernel_verbose=self.get_args().kernel_verbose,
)
# create the loaded kernel stats
parser.load_kernel_top(
self._runs[self.dest_dir], self.dest_dir, self.get_args()
)
# set architecture
self.arch = self._runs[self.dest_dir].sys_info.iloc[0]["gpu_arch"]
else:
if len(self._runs) != 1:
console_error(
"Multiple runs not yet supported in GUI. Retry without --gui flag."
"analysis",
"Multiple runs not yet supported in GUI. Retry without --gui flag.",
)
args = self.get_args()
# create 'mega dataframe'
self._runs[self.dest_dir].raw_pmc = file_io.create_df_pmc(
self.dest_dir,
args.nodes,
args.spatial_multiplexing,
args.kernel_verbose,
args.verbose,
self._profiling_config,
)
if args.spatial_multiplexing:
self._runs[self.dest_dir].raw_pmc = self.spatial_multiplex_merge_counters(
self._runs[self.dest_dir].raw_pmc
)
file_io.create_df_kernel_top_stats(
df_in=self._runs[self.dest_dir].raw_pmc,
raw_data_dir=self.dest_dir,
filter_gpu_ids=self._runs[self.dest_dir].filter_gpu_ids,
filter_dispatch_ids=self._runs[self.dest_dir].filter_dispatch_ids,
filter_nodes=self._runs[self.dest_dir].filter_nodes,
time_unit=args.time_unit,
kernel_verbose=args.kernel_verbose,
)
# create the loaded kernel stats
parser.load_non_mertrics_table(self._runs[self.dest_dir], self.dest_dir, args)
# set architecture
self.arch = self._runs[self.dest_dir].sys_info.iloc[0]["gpu_arch"]
@demarcate
def run_analysis(self):
"""Run CLI analysis."""
def run_analysis(self) -> None:
"""Run webui analysis."""
super().run_analysis()
args = self.get_args()
input_filters = {
"kernel": self._runs[self.dest_dir].filter_kernel_ids,
"gpu": self._runs[self.dest_dir].filter_gpu_ids,
@@ -356,63 +381,60 @@ class webui_analysis(OmniAnalyze_Base):
"top_n": args.max_stat_num,
}
self.build_layout(
input_filters,
self._arch_configs[self.arch],
)
if args.random_port:
self.app.run(debug=False, host="0.0.0.0", port=random.randint(1024, 49151))
else:
self.app.run(debug=False, host="0.0.0.0", port=args.gui)
if self.arch and self.arch in self._arch_configs:
self.build_layout(
input_filters,
self._arch_configs[self.arch],
)
port = random.randint(1024, 49151) if args.random_port else args.gui
self.app.run(debug=False, host="0.0.0.0", port=port)
@demarcate
def determine_chart_type(
original_df,
table_config,
hidden_columns,
barchart_elements,
norm_filt,
comparable_columns,
decimal,
):
original_df: pd.DataFrame,
table_config: dict[str, Any],
hidden_columns: list[str],
barchart_elements: dict[str, list[int]],
comparable_columns: list[str],
decimal: int,
) -> list[html.Div]:
content = []
display_columns = original_df.columns.values.tolist().copy()
# Remove hidden columns. Better way to do it?
for col in hidden_columns:
if col in display_columns:
display_columns.remove(col)
if original_df.empty:
console_warning(
"analysis",
f"The dataframe with id={table_config['id']} is empty! Not displaying it.",
)
return content
display_columns = [
col for col in original_df.columns.values.tolist() if col not in hidden_columns
]
display_df = original_df[display_columns]
# Determine chart type:
# a) Barchart
if original_df.empty:
console_warning(
f"The dataframe with id={table_config['id']} is empty! Not displaying it."
)
elif table_config["id"] in [x for i in barchart_elements.values() for x in i]:
d_figs = build_bar_chart(display_df, table_config, barchart_elements, norm_filt)
if table_config["id"] in [x for i in barchart_elements.values() for x in i]:
d_figs = build_bar_chart(display_df, table_config, barchart_elements)
# Smaller formatting if barchart yeilds several graphs
if (
len(d_figs) > 2
# and not table_config["id"]
# in barchart_elements["l2_cache_per_chan"]
):
temp_obj = []
for fig in d_figs:
temp_obj.append(
html.Div(
className="float-child",
children=[dcc.Graph(figure=fig, style={"margin": "2%"})],
)
if len(d_figs) > 2:
temp_obj = [
html.Div(
className="float-child",
children=[dcc.Graph(figure=fig, style={"margin": "2%"})],
)
for fig in d_figs
]
content.append(html.Div(className="float-container", children=temp_obj))
# Normal formatting if < 2 graphs
else:
for fig in d_figs:
content.append(dcc.Graph(figure=fig, style={"margin": "2%"}))
# B) Tablechart
content.extend([
dcc.Graph(figure=fig, style={"margin": "2%"}) for fig in d_figs
])
# b) Tablechart
else:
d_figs = build_table_chart(
display_df,
@@ -422,18 +444,13 @@ def determine_chart_type(
comparable_columns,
decimal,
)
for fig in d_figs:
content.append(html.Div([fig], style={"margin": "2%"}))
content.extend([html.Div([fig], style={"margin": "2%"}) for fig in d_figs])
# subtitle for each table in a panel if existing
if "title" in table_config and table_config["title"]:
if table_config.get("title"):
subtitle = (
str(table_config["id"] // 100)
+ "."
+ str(table_config["id"] % 100)
+ " "
+ table_config["title"]
+ "\n"
f"{table_config['id'] // 100}.{table_config['id'] % 100} "
f"{table_config['title']}\n"
)
content.insert(
@@ -30,9 +30,11 @@ import socket
import sys
import time
from pathlib import Path
from typing import Optional
import config
from argparser import omniarg_parser
from rocprof_compute_soc.soc_base import OmniSoC_Base
from utils import file_io, parser, schema
from utils.logger import (
console_debug,
@@ -57,25 +59,21 @@ from utils.utils import (
class RocProfCompute:
def __init__(self):
self.__args = None
def __init__(self) -> None:
self.__args: Optional[argparse.Namespace] = None
self.__profiler_mode = None
self.__analyze_mode = None
self.__soc_name = (
set()
) # gpu name, or in case of analyze mode, all loaded gpu name(s)
self.__soc = dict() # set of key, value pairs. Where arch->OmniSoc() obj
self.__version = {
"ver": None,
"ver_pretty": None,
}
self.__options = {}
self.__soc: dict[str, OmniSoC_Base] = {}
self.__version: dict[str, Optional[str]] = {"ver": None, "ver_pretty": None}
self.__supported_archs = mi_gpu_specs.get_gpu_series_dict()
self.__mspec: MachineSpecs = None # to be initalized in load_soc_specs()
self.__mspec: MachineSpecs # to be initialized in load_soc_specs()
setup_console_handler()
self.set_version()
self.parse_args()
assert self.__args is not None
self.__mode = self.__args.mode
gui_value = getattr(self.__args, "gui", None)
self.__loglevel = setup_logging_priority(
self.__args.verbose, self.__args.quiet, self.__mode, gui_value
@@ -90,11 +88,11 @@ class RocProfCompute:
elif self.__mode == "analyze":
self.detect_analyze()
console_debug("Execution mode = %s" % self.__mode)
console_debug(f"Execution mode = {self.__mode}")
def print_graphic(self):
"""Log program name as ascii art to terminal."""
ascii_art = r"""
def print_graphic(self) -> None:
print(
r"""
__ _
_ __ ___ ___ _ __ _ __ ___ / _| ___ ___ _ __ ___ _ __ _ _| |_ ___
| '__/ _ \ / __| '_ \| '__/ _ \| |_ _____ / __/ _ \| '_ ` _ \| '_ \| | | | __/ _ \
@@ -102,20 +100,19 @@ class RocProfCompute:
|_| \___/ \___| .__/|_| \___/|_| \___\___/|_| |_| |_| .__/ \__,_|\__\___|
|_| |_|
"""
print(ascii_art)
)
def get_mode(self):
def get_mode(self) -> Optional[str]:
return self.__mode
def set_version(self):
def set_version(self) -> None:
vData = get_version(config.rocprof_compute_home)
self.__version["ver"] = vData["version"]
self.__version["ver_pretty"] = get_version_display(
vData["version"], vData["sha"], vData["mode"]
)
return
def detect_profiler(self):
def detect_profiler(self) -> None:
profiler_mode = detect_rocprof(self.__args)
if str(profiler_mode).endswith("rocprof"):
self.__profiler_mode = "rocprofv1"
@@ -127,12 +124,11 @@ class RocProfCompute:
self.__profiler_mode = "rocprofiler-sdk"
else:
console_error(
"Incompatible profiler: %s. Supported profilers include: %s"
% (profiler_mode, get_submodules("rocprof_compute_profile"))
f"Incompatible profiler: {profiler_mode}. Supported profilers "
f"include: {get_submodules('rocprof_compute_profile')}"
)
return
def detect_analyze(self):
def detect_analyze(self) -> None:
if self.__args.gui:
self.__analyze_mode = "web_ui"
elif self.__args.tui:
@@ -141,9 +137,8 @@ class RocProfCompute:
self.__analyze_mode = "db"
else:
self.__analyze_mode = "cli"
return
def sanitize(self):
def sanitize(self) -> None:
block = False
if (hasattr(self.__args, "filter_metrics") and self.__args.filter_metrics) or (
hasattr(self.__args, "filter_blocks") and self.__args.filter_blocks
@@ -159,21 +154,19 @@ class RocProfCompute:
console_error("Cannot use --list-available-metrics with --blocks")
@demarcate
def load_soc_specs(self, sysinfo: dict = None):
def load_soc_specs(self, sysinfo: Optional[dict] = None) -> None:
"""Load OmniSoC instance for RocProfCompute run"""
self.__mspec = generate_machine_specs(self.__args, sysinfo)
if self.__args.specs:
if self.__args and self.__args.specs:
print(self.__mspec)
sys.exit(0)
arch = self.__mspec.gpu_arch
soc_module = importlib.import_module("rocprof_compute_soc.soc_" + arch)
soc_class = getattr(soc_module, arch + "_soc")
soc_module = importlib.import_module(f"rocprof_compute_soc.soc_{arch}")
soc_class = getattr(soc_module, f"{arch}_soc")
self.__soc[arch] = soc_class(self.__args, self.__mspec)
return
def parse_args(self):
def parse_args(self) -> None:
parser = argparse.ArgumentParser(
description=(
"Command line interface for AMD's GPU profiler, ROCm Compute Profiler"
@@ -190,18 +183,16 @@ class RocProfCompute:
self.__args = parser.parse_args()
if (
"format_rocprof_output" in self.__args
hasattr(self.__args, "format_rocprof_output")
and self.__args.format_rocprof_output != "rocpd"
):
console_warning(
(
f"The option --format-rocprof-output currently set to "
f"{self.__args.format_rocprof_output} will default to rocpd "
"in a future release."
)
f"The option --format-rocprof-output currently set to "
f"{self.__args.format_rocprof_output} will default to rocpd "
"in a future release."
)
if self.__args.mode == None:
if self.__args.mode is None:
if self.__args.specs:
print(generate_machine_specs(self.__args))
sys.exit(0)
@@ -219,36 +210,42 @@ class RocProfCompute:
"rocprof-compute requires you to pass a valid mode. Detected None."
)
elif self.__args.mode == "profile":
# Add --name to workload path if --path is not given
if self.__args.path == str(Path(os.getcwd()) / "workloads"):
self.__args.path = str(Path(self.__args.path) / self.__args.name)
# Add node name to workload path
if self.__args.subpath == "node_name":
self.__args.path = str(Path(self.__args.path) / socket.gethostname())
# Or, add gpu model name to workload path
elif self.__args.subpath == "gpu_model":
self.__args.path = str(Path(self.__args.path) / self.__mspec.gpu_model)
# Create workload directory if it does not exist
p = Path(self.__args.path)
if not p.exists():
try:
p.mkdir(parents=True, exist_ok=False)
except FileExistsError:
console_error("Directory already exists.")
self.handle_profile_args()
elif self.__args.mode == "analyze":
# block all filters during spatial-multiplexing
if self.__args.spatial_multiplexing:
self.__args.gpu_id = None
self.__args.gpu_kernel = None
self.__args.gpu_dispatch_id = None
self.__args.nodes = None
self.handle_analyze_args()
return
def handle_profile_args(self) -> None:
# Add --name to workload path if --path is not given
if self.__args.path == str(Path(os.getcwd()) / "workloads"):
if not hasattr(self.__args, "name") or not self.__args.name:
console_error("-n/--name is required")
self.__args.path = str(Path(self.__args.path) / self.__args.name)
# Add node name to workload path
if self.__args.subpath == "node_name":
self.__args.path = str(Path(self.__args.path) / socket.gethostname())
# Or, add gpu model name to workload path
elif self.__args.subpath == "gpu_model":
self.__args.path = str(Path(self.__args.path) / self.__mspec.gpu_model)
# Create workload directory if it does not exist
p = Path(self.__args.path)
if not p.exists():
try:
p.mkdir(parents=True, exist_ok=False)
except FileExistsError:
console_error("Directory already exists.")
def handle_analyze_args(self) -> None:
"""Handle analyze-specific argument processing"""
# Block all filters during spatial-multiplexing
if self.__args.spatial_multiplexing:
self.__args.gpu_id = None
self.__args.gpu_kernel = None
self.__args.gpu_dispatch_id = None
self.__args.nodes = None
@demarcate
def list_metrics(self):
def list_metrics(self) -> None:
self.load_soc_specs()
for_current_arch = False
@@ -271,7 +268,7 @@ class RocProfCompute:
sys_info = (
self.__mspec.get_class_members().iloc[0] if for_current_arch else None
)
parser.build_dfs(archConfigs=ac, filter_metrics=[], sys_info=sys_info)
parser.build_dfs(arch_configs=ac, filter_metrics=[], sys_info=sys_info)
for key, value in ac.metric_list.items():
prefix = ""
if "." not in str(key):
@@ -286,7 +283,7 @@ class RocProfCompute:
console_error("Unsupported arch")
@demarcate
def list_sets(self):
def list_sets(self) -> None:
sets_info = parse_sets_yaml(self.__mspec.gpu_arch)
if not sets_info:
@@ -334,8 +331,41 @@ class RocProfCompute:
sys.exit(0)
def create_profiler(self) -> object:
profiler_classes = {
"rocprofv1": (
"rocprof_compute_profile.profiler_rocprof_v1",
"rocprof_v1_profiler",
),
"rocprofv2": (
"rocprof_compute_profile.profiler_rocprof_v2",
"rocprof_v2_profiler",
),
"rocprofv3": (
"rocprof_compute_profile.profiler_rocprof_v3",
"rocprof_v3_profiler",
),
"rocprofiler-sdk": (
"rocprof_compute_profile.profiler_rocprofiler_sdk",
"rocprofiler_sdk_profiler",
),
}
if self.__profiler_mode not in profiler_classes:
console_error("Unsupported profiler")
module_name, class_name = profiler_classes[self.__profiler_mode]
module = importlib.import_module(module_name)
profiler_class = getattr(module, class_name)
return profiler_class(
self.__args,
self.__profiler_mode,
self.__soc[self.__mspec.gpu_arch],
)
@demarcate
def run_profiler(self):
def run_profiler(self) -> None:
self.print_graphic()
self.load_soc_specs()
@@ -346,51 +376,15 @@ class RocProfCompute:
elif self.__args.name is None:
sys.exit("Either --list-name or --name is required")
if self.__args.name.find("/") != -1:
console_error("'/' not permitted in profile name")
if "/" in self.__args.name:
console_error('"/" is not permitted in profile name')
# instantiate desired profiler
if self.__profiler_mode == "rocprofv1":
from rocprof_compute_profile.profiler_rocprof_v1 import rocprof_v1_profiler
profiler = rocprof_v1_profiler(
self.__args,
self.__profiler_mode,
self.__soc[self.__mspec.gpu_arch],
)
elif self.__profiler_mode == "rocprofv2":
from rocprof_compute_profile.profiler_rocprof_v2 import rocprof_v2_profiler
profiler = rocprof_v2_profiler(
self.__args,
self.__profiler_mode,
self.__soc[self.__mspec.gpu_arch],
)
elif self.__profiler_mode == "rocprofv3":
from rocprof_compute_profile.profiler_rocprof_v3 import rocprof_v3_profiler
profiler = rocprof_v3_profiler(
self.__args,
self.__profiler_mode,
self.__soc[self.__mspec.gpu_arch],
)
elif self.__profiler_mode == "rocprofiler-sdk":
from rocprof_compute_profile.profiler_rocprofiler_sdk import (
rocprofiler_sdk_profiler,
)
profiler = rocprofiler_sdk_profiler(
self.__args,
self.__profiler_mode,
self.__soc[self.__mspec.gpu_arch],
)
else:
console_error("Unsupported profiler")
profiler = self.create_profiler()
# -----------------------
# run profiling workflow
# -----------------------
profiler.sanitize()
# enable file-based logging
@@ -398,28 +392,26 @@ class RocProfCompute:
profiler.pre_processing()
console_debug('starting "run_profiling" and about to start rocprof\'s workload')
time_start_prof = time.time()
profiler.run_profiling(self.__version["ver"], config.PROJECT_NAME)
time_end_prof = time.time()
prof_duration = time_end_prof - time_start_prof
console_debug(
(
'finished "run_profiling" and finished rocprof\'s workload, '
"time taken was {} m {} sec"
).format(
int((time_end_prof - time_start_prof) / 60),
str((time_end_prof - time_start_prof) % 60),
)
)
profiler.post_processing()
time_end_post = time.time()
console_debug(
'time taken for "post_processing" was {} seconds'.format(
int(time_end_post - time_end_prof)
)
f'finished "run_profiling" and finished rocprof\'s workload, '
f"time taken was {int(prof_duration / 60)} m {prof_duration % 60} sec"
)
profiler.post_processing()
time_end_post = time.time()
post_duration = int(time_end_post - time_end_prof)
console_debug(f'time taken for "post_processing" was {post_duration} seconds')
self.__soc[self.__mspec.gpu_arch].post_profiling()
@demarcate
def update_db(self):
def update_db(self) -> None:
self.print_graphic()
console_warning(
@@ -444,10 +436,9 @@ class RocProfCompute:
return
@demarcate
def run_analysis(self):
def run_analysis(self) -> None:
self.print_graphic()
console_log("Analysis mode = %s" % self.__analyze_mode)
console_log(f"Analysis mode = {self.__analyze_mode}")
if self.__analyze_mode == "cli":
from rocprof_compute_analyze.analysis_cli import cli_analysis
@@ -467,7 +458,7 @@ class RocProfCompute:
analyzer = db_analysis(self.__args, self.__supported_archs)
else:
console_error("Unsupported analysis mode -> %s" % self.__analyze_mode)
console_error(f"Unsupported analysis mode -> {self.__analyze_mode}")
# -----------------------
# run analysis workflow
@@ -476,12 +467,10 @@ class RocProfCompute:
# Load required SoC(s) from input
for d in analyzer.get_args().path:
# FIXME
# sys_info = pd.read_csv(Path(d[0], "sysinfo.csv"))
sysinfo_path = (
Path(d[0])
if analyzer.get_args().nodes is None
and analyzer.get_args().spatial_multiplexing is not True
and not analyzer.get_args().spatial_multiplexing
else file_io.find_1st_sub_dir(d[0])
)
sys_info = file_io.load_sys_info(sysinfo_path / "sysinfo.csv")
@@ -493,5 +482,3 @@ class RocProfCompute:
analyzer.set_soc(self.__soc)
analyzer.pre_processing()
analyzer.run_analysis()
return
@@ -23,6 +23,7 @@
##############################################################################
import argparse
import csv
import glob
import os
@@ -32,10 +33,13 @@ import shutil
import time
from abc import abstractmethod
from pathlib import Path
from typing import Any, Optional, Union
import numpy as np
import pandas as pd
import yaml
from rocprof_compute_soc.soc_base import OmniSoC_Base
from utils.logger import (
console_debug,
console_error,
@@ -54,21 +58,28 @@ from utils.utils import (
class RocProfCompute_Base:
def __init__(self, args, profiler_mode, soc):
def __init__(
self,
args: argparse.Namespace,
profiler_mode: str,
soc: OmniSoC_Base,
) -> None:
self.__args = args
self.__profiler = profiler_mode
self._soc = soc # OmniSoC obj
def get_args(self):
def get_args(self) -> argparse.Namespace:
return self.__args
def get_profiler_options(self, fname, soc):
def get_profiler_options(
self, fname: str, soc: OmniSoC_Base
) -> Union[list[str], dict[str, Any]]:
"""Fetch any version specific arguments required by profiler"""
# assume no SoC specific options and return empty list by default
return []
@demarcate
def sanitize(self):
def sanitize(self) -> None:
"""Perform sanitization of inputs"""
args = self.get_args()
@@ -103,26 +114,27 @@ class RocProfCompute_Base:
args.remaining = " ".join(args.remaining)
else:
console_error(
(
"Profiling command required. Pass application executable after -- "
"at the end of options.\n"
"\t\ti.e. rocprof-compute profile -n vcopy -- "
"./vcopy -n 1048576 -b 256"
)
"Profiling command required. Pass application executable after -- "
"at the end of options.\n"
"\ti.e. rocprof-compute profile -n vcopy -- "
"./vcopy -n 1048576 -b 256"
)
@demarcate
def join_prof(self, out=None):
def join_prof(self, out: Optional[str] = None) -> Optional[pd.DataFrame]:
"""Manually join separated rocprof runs"""
if self.get_args().format_rocprof_output == "rocpd":
args = self.get_args()
output_file = out or f"{args.path}/pmc_perf.csv"
# handle rocpd format
if args.format_rocprof_output == "rocpd":
# Vertically concat (by rows) results_*.csv into pmc_perf.csv
result_files = glob.glob(self.get_args().path + "/results_*.csv")
if out is None:
out = self.__args.path + "/pmc_perf.csv"
with open(out, "w", newline="") as outfile:
result_files = glob.glob(f"{args.path}/results_*.csv")
with open(output_file, "w", newline="") as outfile:
writer = None
for file in result_files:
with open(file, "r", newline="") as infile:
with open(file, newline="") as infile:
reader = csv.reader(infile)
header = next(reader)
# Write header only once
@@ -131,7 +143,9 @@ class RocProfCompute_Base:
writer.writerow(header)
for row in reader:
writer.writerow(row)
console_debug(f"Created file: {out}")
console_debug(f"Created file: {output_file}")
# Delete results_*.csv files
for file in result_files:
os.remove(file)
@@ -139,14 +153,15 @@ class RocProfCompute_Base:
return
# Set default output directory if not specified
if isinstance(self.__args.path, str):
if out is None:
out = self.__args.path + "/pmc_perf.csv"
files = glob.glob(self.__args.path + "/" + "pmc_perf_*.csv")
files.extend(glob.glob(self.__args.path + "/" + "SQ_*.csv"))
files.extend(glob.glob(self.__args.path + "/" + "SQC_*.csv"))
if isinstance(args.path, str):
csv_patterns = ["pmc_perf_*.csv", "SQ_*.csv", "SQC_*.csv"]
files = [
file
for pattern in csv_patterns
for file in glob.glob(f"{args.path}/{pattern}")
]
if self.get_args().hip_trace:
if args.hip_trace:
# remove hip api trace ouputs from this list
files = [
f
@@ -156,7 +171,7 @@ class RocProfCompute_Base:
)
]
if self.get_args().kokkos_trace:
if args.kokkos_trace:
# remove marker api trace ouputs from this list
files = [
f
@@ -165,39 +180,39 @@ class RocProfCompute_Base:
os.path.basename(f)
)
]
elif isinstance(self.__args.path, list):
files = self.__args.path
elif isinstance(args.path, list):
files = args.path
else:
console_error(
"Invalid workload directory. Cannot resolve %s" % self.__args.path
)
console_error(f"Invalid workload directory. Cannot resolve {args.path}")
# Process files and create joined dataframe
df = None
for i, file in enumerate(files):
_df = pd.read_csv(file) if isinstance(self.__args.path, str) else file
if self.__args.join_type == "kernel":
key = _df.groupby("Kernel_Name").cumcount()
_df["key"] = _df.Kernel_Name + " - " + key.astype(str)
elif self.__args.join_type == "grid":
key = _df.groupby(["Kernel_Name", "Grid_Size"]).cumcount()
_df["key"] = (
_df["Kernel_Name"]
current_df = pd.read_csv(file)
if args.join_type == "kernel":
key = current_df.groupby("Kernel_Name").cumcount()
current_df["key"] = current_df.Kernel_Name + " - " + key.astype(str)
elif args.join_type == "grid":
key = current_df.groupby(["Kernel_Name", "Grid_Size"]).cumcount()
current_df["key"] = (
current_df["Kernel_Name"].astype(str)
+ " - "
+ _df["Grid_Size"].astype(str)
+ current_df["Grid_Size"].astype(str)
+ " - "
+ key.astype(str)
)
else:
console_error(
"%s is an unrecognized option for --join-type"
% self.__args.join_type
f"{args.join_type} is an unrecognized option for --join-type"
)
if df is None:
df = _df
df = current_df
else:
# join by unique index of kernel
df = pd.merge(df, _df, how="inner", on="key", suffixes=("", f"_{i}"))
df = pd.merge(
df, current_df, how="inner", on="key", suffixes=("", f"_{i}")
)
if df is None or df.empty:
return
@@ -217,6 +232,7 @@ class RocProfCompute_Base:
],
"SGPR": [col for col in df.columns if col.startswith("SGPR")],
}
# Check for vgpr counter in ROCm < 5.3
if "vgpr" in df.columns:
duplicate_cols["vgpr"] = [
@@ -230,108 +246,106 @@ class RocProfCompute_Base:
duplicate_cols["Accum_VGPR"] = [
col for col in df.columns if col.startswith("Accum_VGPR")
]
for key, cols in duplicate_cols.items():
_df = df[cols]
if not test_df_column_equality(_df):
msg = "Detected differing {} values while joining pmc_perf.csv".format(
key
current_df = df[cols]
if not test_df_column_equality(current_df):
console_warning(
f"Detected differing {key} values while joining pmc_perf.csv"
)
console_warning(msg)
else:
msg = "Successfully joined {} in pmc_perf.csv".format(key)
console_debug(msg)
console_debug(f"Successfully joined {key} in pmc_perf.csv")
# now, we can:
#   A) throw away any of the "boring" duplicates
columns_to_remove = [
# rocprofv2 headers
"GPU_ID_",
"Grid_Size_",
"Workgroup_Size_",
"LDS_Per_Workgroup_",
"Scratch_Per_Workitem_",
"vgpr_",
"Arch_VGPR_",
"Accum_VGPR_",
"SGPR_",
"Dispatch_ID_",
"Queue_ID",
"Queue_Index",
"PID",
"TID",
"SIG",
"OBJ",
"Correlation_ID_",
"Wave_Size_",
# rocscope specific merged counters, keep original
"dispatch_",
# extras
"sig",
"queue-id",
"queue-index",
"pid",
"tid",
"fbar",
]
df = df[
[
k
for k in df.keys()
if not any(
k.startswith(check)
for check in [
# rocprofv2 headers
"GPU_ID_",
"Grid_Size_",
"Workgroup_Size_",
"LDS_Per_Workgroup_",
"Scratch_Per_Workitem_",
"vgpr_",
"Arch_VGPR_",
"Accum_VGPR_",
"SGPR_",
"Dispatch_ID_",
"Queue_ID",
"Queue_Index",
"PID",
"TID",
"SIG",
"OBJ",
"Correlation_ID_",
"Wave_Size_",
# rocscope specific merged counters, keep original
"dispatch_",
# extras
"sig",
"queue-id",
"queue-index",
"pid",
"tid",
"fbar",
]
)
col
for col in df.columns
if not any(col.startswith(prefix) for prefix in columns_to_remove)
]
]
# B) any timestamps that are _not_ the duration,
# which is the one we care about
timestamp_patterns = ["DispatchNs", "CompleteNs", "HostDuration"]
df = df[
[
k
for k in df.keys()
if not any(
check in k
for check in [
"DispatchNs",
"CompleteNs",
# rocscope specific timestamp
"HostDuration",
]
)
col
for col in df.columns
if not any(pattern in col for pattern in timestamp_patterns)
]
]
#   C) sanity check the name and key
namekeys = [k for k in df.keys() if "Kernel_Name" in k]
assert len(namekeys)
for k in namekeys[1:]:
assert (df[namekeys[0]] == df[k]).all()
df = df.drop(columns=namekeys[1:])
name_cols = [col for col in df.columns if "Kernel_Name" in col]
if not name_cols:
return df
for col in name_cols[1:]:
assert (df[name_cols[0]] == df[col]).all()
df = df.drop(columns=name_cols[1:])
# now take the median of the durations
bkeys = []
ekeys = []
for k in df.keys():
if "Start_Timestamp" in k:
bkeys.append(k)
if "End_Timestamp" in k:
ekeys.append(k)
# compute mean begin and end timestamps
endNs = df[ekeys].mean(axis=1)
beginNs = df[bkeys].mean(axis=1)
# and replace
df = df.drop(columns=bkeys)
df = df.drop(columns=ekeys)
df["Start_Timestamp"] = beginNs
df["End_Timestamp"] = endNs
start_cols = [col for col in df.columns if "Start_Timestamp" in col]
end_cols = [col for col in df.columns if "End_Timestamp" in col]
# compute mean mean timestamps
if start_cols and end_cols:
mean_start = df[start_cols].mean(axis=1)
mean_end = df[end_cols].mean(axis=1)
# Replace with consolidated timestamps
df = df.drop(columns=start_cols + end_cols)
df["Start_Timestamp"] = mean_start
df["End_Timestamp"] = mean_end
# finally, join the drop key
df = df.drop(columns=["key"])
# save to file and delete old file(s)
# skip if we're being called outside of rocprof-compute
if isinstance(self.__args.path, str):
df.to_csv(out, index=False)
if not self.__args.verbose:
if isinstance(args.path, str):
df.to_csv(output_file, index=False)
if not args.verbose:
for file in files:
# Do not remove accumulate counter files
if "SQ_" not in file or "SQC_" not in file:
os.remove(file)
return None
else:
return df
@@ -339,14 +353,15 @@ class RocProfCompute_Base:
# Required methods to be implemented by child classes
# ----------------------------------------------------
@abstractmethod
def pre_processing(self):
def pre_processing(self) -> None:
"""Perform any pre-processing steps prior to profiling."""
console_debug("profiling", "pre-processing using %s profiler" % self.__profiler)
args = self.get_args()
console_debug("profiling", f"pre-processing using {self.__profiler} profiler")
self._filter_blocks = self._soc.profiling_setup()
# Write profiling configuration as yaml file
with open(Path(self.__args.path).joinpath("profiling_config.yaml"), "w") as f:
with open(f"{self.__args.path}/profiling_config.yaml", "w") as f:
args_dict = vars(self.__args)
# Override filter_blocks when writing profiling config yaml
args_dict["filter_blocks"] = self._filter_blocks
@@ -356,63 +371,56 @@ class RocProfCompute_Base:
# verify soc compatibility
if self.__profiler not in self._soc.get_compatible_profilers():
console_error(
"%s is not enabled in %s. Available profilers include: %s"
% (
self._soc.get_arch(),
self.__profiler,
self._soc.get_compatible_profilers(),
)
f"{self._soc.get_arch()} is not enabled in {self.__profiler}. "
f"Available profilers include: {self._soc.get_compatible_profilers()}"
)
gen_sysinfo(
workload_name=self.__args.name,
workload_dir=self.get_args().path,
app_cmd=self.__args.remaining,
skip_roof=self.__args.no_roof,
workload_name=args.name,
workload_dir=args.path,
app_cmd=args.remaining,
skip_roof=args.no_roof,
mspec=self._soc._mspec,
soc=self._soc,
)
@abstractmethod
def run_profiling(self, version: str, prog: str):
def run_profiling(self, version: str, prog: str) -> None:
"""Run profiling."""
console_debug(
"profiling", "performing profiling using %s profiler" % self.__profiler
"profiling", f"performing profiling using {self.__profiler} profiler"
)
args = self.get_args()
# log basic info
console_log(str(prog).title() + " version: " + str(version))
console_log("Profiler choice: %s" % self.__profiler)
console_log("Path: " + str(Path(self.__args.path).absolute().resolve()))
console_log("Target: " + str(self._soc._mspec.gpu_model))
console_log("Command: " + str(self.__args.remaining))
console_log("Kernel Selection: " + str(self.__args.kernel))
console_log("Dispatch Selection: " + str(self.__args.dispatch))
console_log(f"{str(prog).title()} version: {version}")
console_log(f"Profiler choice: {self.__profiler}")
console_log(f"Path: {Path(self.__args.path).absolute().resolve()}")
console_log(f"Target: {self._soc._mspec.gpu_model}")
console_log(f"Command: {args.remaining}")
console_log(f"Kernel Selection: {args.kernel}")
console_log(f"Dispatch Selection: {args.dispatch}")
if self._filter_blocks:
console_log(f"Filtered sections: {str(self._filter_blocks)}")
else:
console_log("Filtered sections: All")
msg = "Collecting Performance Counters"
(
print_status(msg)
if not self.__args.roof_only
else print_status(msg + " (Roofline Only)")
)
status_msg = f"{msg} (Roofline Only)" if self.__args.roof_only else msg
print_status(status_msg)
# Run profiling on each input file
input_files = glob.glob(self.get_args().path + "/perfmon/*.txt")
input_files.sort()
input_files = sorted(glob.glob(f"{args.path}/perfmon/*.txt"))
total_runs = len(input_files)
total_profiling_time_so_far = 0
avg_profiling_time = 0
total_profiling_time = 0.0
for i, fname in enumerate(input_files):
run_number = i + 1
# Log progress and time estimation
if i > 0:
avg_profiling_time = total_profiling_time_so_far / i
time_left_seconds = (total_runs - run_number) * avg_profiling_time
avg_time = total_profiling_time / i
time_left_seconds = (total_runs - run_number) * avg_time
time_left = format_time(time_left_seconds)
console_log(
f"[Run {run_number}/{total_runs}]"
@@ -426,15 +434,12 @@ class RocProfCompute_Base:
)
# Kernel filtering (in-place replacement)
if not self.__args.kernel == None:
if not args.kernel == None:
success, output = capture_subprocess_output([
"sed",
"-i",
"-r",
"s%^(kernel:).*%"
+ "kernel: "
+ ",".join(self.__args.kernel)
+ "%g",
f"s%^(kernel:).*%kernel: {','.join(self.__args.kernel)}%g",
fname,
])
# log output from profile filtering
@@ -444,15 +449,12 @@ class RocProfCompute_Base:
console_debug(output)
# Dispatch filtering (inplace replacement)
if not self.__args.dispatch == None:
if not args.dispatch == None:
success, output = capture_subprocess_output([
"sed",
"-i",
"-r",
"s%^(range:).*%"
+ "range: "
+ " ".join(self.__args.dispatch)
+ "%g",
f"s%^(range:).*%range: {' '.join(self.__args.dispatch)}%g",
fname,
])
# log output from profile filtering
@@ -460,73 +462,76 @@ class RocProfCompute_Base:
console_error(output)
else:
console_debug(output)
console_log("profiling", "Current input file: %s" % fname)
options = self.get_profiler_options(fname, self._soc)
if (
self.__profiler == "rocprofv1"
or self.__profiler == "rocprofv2"
or self.__profiler == "rocprofv3"
or self.__profiler == "rocprofiler-sdk"
console_log("profiling", f"Current input file: {fname}")
if self.__profiler in (
"rocprofv1",
"rocprofv2",
"rocprofv3",
"rocprofiler-sdk",
):
start_run_prof = time.time()
options = self.get_profiler_options(fname, self._soc)
start_time = time.time()
run_prof(
fname=fname,
profiler_options=options,
workload_dir=self.get_args().path,
workload_dir=args.path,
mspec=self._soc._mspec,
loglevel=self.get_args().loglevel,
format_rocprof_output=self.get_args().format_rocprof_output,
retain_rocpd_output=self.get_args().retain_rocpd_output,
loglevel=args.loglevel,
format_rocprof_output=args.format_rocprof_output,
retain_rocpd_output=args.retain_rocpd_output,
)
end_run_prof = time.time()
actual_profiling_duration = end_run_prof - start_run_prof
end_time = time.time()
duration = end_time - start_time
total_profiling_time += duration
console_debug(
"The time of run_prof of {} is {} m {} sec".format(
fname,
int((end_run_prof - start_run_prof) / 60),
str((end_run_prof - start_run_prof) % 60),
)
f"The time of run_prof of {fname} is {int(duration / 60)} min"
f" {duration % 60} sec"
)
else:
console_error("Profiler not supported")
total_profiling_time_so_far += actual_profiling_duration
# PC sampling data is only collected when block "21" is specified
if "21" in self.get_args().filter_blocks and self.__profiler in (
"rocprofv3",
"rocprofiler-sdk",
):
console_log(
f"[Run {total_runs + 1}/{total_runs + 1}][PC sampling profile run]"
)
start_run_prof = time.time()
pc_sampling_prof(
method=self.get_args().pc_sampling_method,
interval=self.get_args().pc_sampling_interval,
workload_dir=self.get_args().path,
appcmd=shlex.split(
self.get_args().remaining
), # FIXME: the right solution is applying it when argparsing once!
rocprofiler_sdk_library_path=self.get_args().rocprofiler_sdk_library_path,
)
end_run_prof = time.time()
pc_sampling_duration = end_run_prof - start_run_prof
console_debug(
"The time of pc sampling profiling is {} m {} sec".format(
int((pc_sampling_duration) / 60),
str((pc_sampling_duration) % 60),
)
)
@abstractmethod
def post_processing(self):
"""Perform any post-processing steps prior to profiling."""
# PC sampling data is only collected when block "21" is specified
if not (
"21" in args.filter_blocks
and self.__profiler in ("rocprofv3", "rocprofiler-sdk")
):
return
input_files = glob.glob(f"{args.path}/perfmon/*.txt")
total_runs = len(input_files)
console_log(f"[Run {total_runs + 1}/{total_runs + 1}][PC sampling profile run]")
start_time = time.time()
pc_sampling_prof(
method=args.pc_sampling_method,
interval=args.pc_sampling_interval,
workload_dir=args.path,
appcmd=shlex.split(
args.remaining
), # FIXME: the right solution is applying it when argparsing once!
rocprofiler_sdk_library_path=args.rocprofiler_sdk_library_path,
)
end_time = time.time()
duration = end_time - start_time
console_debug(
"profiling",
"performing post-processing using %s profiler" % self.__profiler,
f"The time of pc sampling profiling is {int(duration / 60)} m "
f"{duration % 60} sec",
)
@abstractmethod
def post_processing(self) -> None:
"""Perform any post-processing steps prior to profiling."""
console_debug(
"profiling", f"performing post-processing using {self.__profiler} profiler"
)
self._soc.post_profiling()
def test_df_column_equality(df):
def test_df_column_equality(df: pd.DataFrame) -> np.bool:
return df.eq(df.iloc[:, 0], axis=0).all(1).all()
@@ -23,26 +23,34 @@
##############################################################################
import argparse
import shlex
from pathlib import Path
from rocprof_compute_profile.profiler_base import RocProfCompute_Base
from rocprof_compute_soc.soc_base import OmniSoC_Base
from utils.logger import console_error, console_log, demarcate
class rocprof_v3_profiler(RocProfCompute_Base):
def __init__(self, profiling_args, profiler_mode, soc):
def __init__(
self,
profiling_args: argparse.Namespace,
profiler_mode: str,
soc: OmniSoC_Base,
) -> None:
super().__init__(profiling_args, profiler_mode, soc)
self.ready_to_profile = (
self.get_args().roof_only
and not Path(self.get_args().path).joinpath("pmc_perf.csv").is_file()
and not (Path(self.get_args().path) / "pmc_perf.csv").is_file()
or not self.get_args().roof_only
)
def get_profiler_options(self, fname, soc):
app_cmd = shlex.split(self.get_args().remaining)
trace_option = "--kernel-trace"
if self.get_args().kokkos_trace:
def get_profiler_options(self, fname: str, soc: OmniSoC_Base) -> list[str]:
args = self.get_args()
app_cmd = shlex.split(args.remaining)
if args.kokkos_trace:
trace_option = "--kokkos-trace"
# NOTE: --kokkos-trace feature is incomplete and is disabled for now.
console_error(
@@ -50,61 +58,69 @@ class rocprof_v3_profiler(RocProfCompute_Base):
"version of rocprof-compute. This functionality is planned for a "
"future release. Please adjust your profiling options accordingly."
)
if self.get_args().hip_trace:
elif args.hip_trace:
trace_option = "--hip-trace"
else:
trace_option = "--kernel-trace"
args = [
profiling_options = [
# v3 requires output directory argument
"-d",
self.get_args().path + "/" + "out",
f"{self.get_args().path}/out",
trace_option,
"--output-format",
self.get_args().format_rocprof_output,
args.format_rocprof_output,
]
# Kernel filtering
if self.get_args().kernel:
args.extend(["--kernel-include-regex", "|".join(self.get_args().kernel)])
if args.kernel:
profiling_options.extend(["--kernel-include-regex", "|".join(args.kernel)])
# Dispatch filtering
dispatch = []
# rocprofv3 dispatch indexing is inclusive and starts from 1
if self.get_args().dispatch:
for dispatch_id in self.get_args().dispatch:
if args.dispatch:
for dispatch_id in args.dispatch:
if ":" in dispatch_id:
tokens = dispatch_id.split(":")
# 4:7 -> 5-7
dispatch.append(f"{int(tokens[0]) + 1}-{tokens[1]}")
start, end = dispatch_id.split(":")
dispatch.append(f"{int(start) + 1}-{end}")
else:
# 4 -> 5
dispatch.append(f"{int(dispatch_id) + 1}")
if dispatch:
args.extend(["--kernel-iteration-range", f"[{','.join(dispatch)}]"])
args.append("--")
args.extend(app_cmd)
return args
profiling_options.extend([
"--kernel-iteration-range",
f"[{','.join(dispatch)}]",
])
profiling_options.append("--")
profiling_options.extend(app_cmd)
return profiling_options
# -----------------------
# Required child methods
# -----------------------
@demarcate
def pre_processing(self):
def pre_processing(self) -> None:
"""Perform any pre-processing steps prior to profiling."""
super().pre_processing()
@demarcate
def run_profiling(self, version, prog):
def run_profiling(self, version: str, prog: str) -> None:
"""Run profiling."""
if self.ready_to_profile:
if self.get_args().roof_only:
console_log(
"roofline", "Generating pmc_perf.csv (roofline counters only)."
)
# Log profiling options and setup filtering
super().run_profiling(version, prog)
else:
if not self.ready_to_profile:
console_log("roofline", "Detected existing pmc_perf.csv")
return
if self.get_args().roof_only:
console_log("roofline", "Generating pmc_perf.csv (roofline counters only).")
# Log profiling options and setup filtering
super().run_profiling(version, prog)
@demarcate
def post_processing(self):
def post_processing(self) -> None:
"""Perform any post-processing steps prior to profiling."""
if self.ready_to_profile:
# Manually join each pmc_perf*.csv output
@@ -23,31 +23,43 @@
##############################################################################
import argparse
import shlex
from pathlib import Path
from typing import Union
from rocprof_compute_profile.profiler_base import RocProfCompute_Base
from rocprof_compute_soc.soc_base import OmniSoC_Base
from utils.logger import console_error, console_log, demarcate
class rocprofiler_sdk_profiler(RocProfCompute_Base):
def __init__(self, profiling_args, profiler_mode, soc):
def __init__(
self,
profiling_args: argparse.Namespace,
profiler_mode: str,
soc: OmniSoC_Base,
) -> None:
super().__init__(profiling_args, profiler_mode, soc)
self.ready_to_profile = (
self.get_args().roof_only
and not Path(self.get_args().path).joinpath("pmc_perf.csv").is_file()
and not (Path(self.get_args().path) / "pmc_perf.csv").is_file()
or not self.get_args().roof_only
)
def get_profiler_options(self, fname, soc):
app_cmd = shlex.split(self.get_args().remaining)
rocm_libdir = str(Path(self.get_args().rocprofiler_sdk_library_path).parent)
def get_profiler_options(
self, fname: str, soc: OmniSoC_Base
) -> dict[str, Union[str, list[str]]]:
args = self.get_args()
app_cmd = shlex.split(args.remaining)
rocm_libdir = Path(args.rocprofiler_sdk_library_path).parent
rocprofiler_sdk_tool_path = str(
Path(rocm_libdir).joinpath("rocprofiler-sdk/librocprofiler-sdk-tool.so")
rocm_libdir / "rocprofiler-sdk" / "librocprofiler-sdk-tool.so"
)
ld_preload = [
rocprofiler_sdk_tool_path,
self.get_args().rocprofiler_sdk_library_path,
args.rocprofiler_sdk_library_path,
]
options = {
"ROCPROFILER_LIBRARY_CTOR": "1",
@@ -55,38 +67,38 @@ class rocprofiler_sdk_profiler(RocProfCompute_Base):
"ROCP_TOOL_LIBRARIES": rocprofiler_sdk_tool_path,
"LD_LIBRARY_PATH": rocm_libdir,
"ROCPROF_KERNEL_TRACE": "1",
"ROCPROF_OUTPUT_FORMAT": self.get_args().format_rocprof_output,
"ROCPROF_OUTPUT_PATH": self.get_args().path + "/out/pmc_1",
"ROCPROF_OUTPUT_FORMAT": args.format_rocprof_output,
"ROCPROF_OUTPUT_PATH": f"{args.path}/out/pmc_1",
}
if self.get_args().kokkos_trace:
if args.kokkos_trace:
# NOTE: --kokkos-trace feature is incomplete and is disabled for now.
console_error(
"The option '--kokkos-trace' is not supported in the current "
"version of rocprof-compute. This functionality is planned for a "
"future release. Please adjust your profiling options accordingly."
)
if self.get_args().hip_trace:
if args.hip_trace:
options["ROCPROF_HIP_COMPILER_API_TRACE"] = "1"
options["ROCPROF_HIP_RUNTIME_API_TRACE"] = "1"
# Kernel filtering
if self.get_args().kernel:
options["ROCPROF_KERNEL_FILTER_INCLUDE_REGEX"] = "|".join(
self.get_args().kernel
)
if args.kernel:
options["ROCPROF_KERNEL_FILTER_INCLUDE_REGEX"] = "|".join(args.kernel)
# Dispatch filtering
dispatch = []
# rocprof sdk dispatch indexing is inclusive and starts from 1
if self.get_args().dispatch:
for dispatch_id in self.get_args().dispatch:
if args.dispatch:
for dispatch_id in args.dispatch:
if ":" in dispatch_id:
tokens = dispatch_id.split(":")
# 4:7 -> 5-7
dispatch.append(f"{int(tokens[0]) + 1}-{tokens[1]}")
start, end = dispatch_id.split(":")
dispatch.append(f"{int(start) + 1}-{end}")
else:
# 4 -> 5
dispatch.append(f"{int(dispatch_id) + 1}")
if dispatch:
options["ROCPROF_KERNEL_FILTER_RANGE"] = f"[{','.join(dispatch)}]"
options["APP_CMD"] = app_cmd
@@ -96,25 +108,25 @@ class rocprofiler_sdk_profiler(RocProfCompute_Base):
# Required child methods
# -----------------------
@demarcate
def pre_processing(self):
def pre_processing(self) -> None:
"""Perform any pre-processing steps prior to profiling."""
super().pre_processing()
@demarcate
def run_profiling(self, version, prog):
def run_profiling(self, version: str, prog: str) -> None:
"""Run profiling."""
if self.ready_to_profile:
if self.get_args().roof_only:
console_log(
"roofline", "Generating pmc_perf.csv (roofline counters only)."
)
# Log profiling options and setup filtering
super().run_profiling(version, prog)
else:
if not self.ready_to_profile:
console_log("roofline", "Detected existing pmc_perf.csv")
return
if self.get_args().roof_only:
console_log("roofline", "Generating pmc_perf.csv (roofline counters only).")
# Log profiling options and setup filtering
super().run_profiling(version, prog)
@demarcate
def post_processing(self):
def post_processing(self) -> None:
"""Perform any post-processing steps prior to profiling."""
if self.ready_to_profile:
# Manually join each pmc_perf*.csv output
@@ -23,6 +23,7 @@
##############################################################################
import argparse
import glob
import json
import math
@@ -31,6 +32,7 @@ import re
import sys
from abc import abstractmethod
from pathlib import Path
from typing import Any, Optional
import yaml
@@ -44,7 +46,8 @@ from utils.logger import (
demarcate,
)
from utils.mi_gpu_spec import mi_gpu_specs
from utils.parser import build_in_vars, supported_denom
from utils.parser import BUILD_IN_VARS, SUPPORTED_DENOM
from utils.specs import MachineSpecs
from utils.utils import (
add_counter_extra_config_input_yaml,
capture_subprocess_output,
@@ -59,111 +62,111 @@ from utils.utils import (
class OmniSoC_Base:
def __init__(
self, args, mspec
): # new info field will contain rocminfo or sysinfo to populate properties
def __init__(self, args: argparse.Namespace, mspec: MachineSpecs) -> None:
# new info field will contain rocminfo or sysinfo to populate properties
console_debug("[omnisoc init]")
self.__args = args
self.__arch = None
self.__arch: Optional[str] = None
self._mspec = mspec
# Per IP block, max number of simultaneous counters. GFX IP Blocks.
self.__perfmon_config = {}
self.__soc_params = {} # SoC specifications
self.__compatible_profilers = [] # Store profilers compatible with SoC
self.__perfmon_config: dict[str, int] = {}
self.__compatible_profilers: list[str] = [] # Store SoC compatible profilers
self.populate_mspec()
# Create roofline object if mode is provided; skip for --specs
if hasattr(self.__args, "mode") and self.__args.mode:
self.roofline_obj = Roofline(args, self._mspec)
def __hash__(self):
def __hash__(self) -> int:
return hash(self.__arch)
def __eq__(self, other):
def __eq__(self, other: object) -> bool:
if not isinstance(other, type(self)):
return NotImplemented
return self.__arch == other.get_soc()
def set_perfmon_config(self, config: dict):
def set_perfmon_config(self, config: dict[str, int]) -> None:
self.__perfmon_config = config
def get_soc_param(self):
return self.__soc_params
def set_arch(self, arch: str):
def set_arch(self, arch: str) -> None:
self.__arch = arch
def get_arch(self):
return self.__arch
def get_args(self):
return self.__args
def set_compatible_profilers(self, profiler_names: list):
def set_compatible_profilers(self, profiler_names: list[str]) -> None:
self.__compatible_profilers = profiler_names
def get_compatible_profilers(self):
def get_arch(self) -> Optional[str]:
return self.__arch
def get_args(self) -> argparse.Namespace:
return self.__args
def get_compatible_profilers(self) -> list[str]:
return self.__compatible_profilers
def populate_mspec(self):
def populate_mspec(self) -> None:
from utils.specs import run, search, total_sqc
if not hasattr(self._mspec, "_rocminfo") or self._mspec._rocminfo is None:
if (
not hasattr(self._mspec, "rocminfo_lines")
or self._mspec.rocminfo_lines is None
):
return
# load stats from rocminfo
self._mspec.gpu_l1 = ""
self._mspec.gpu_l2 = ""
for idx2, linetext in enumerate(self._mspec._rocminfo):
for linetext in self._mspec.rocminfo_lines:
key = search(r"^\s*L1:\s+ ([a-zA-Z0-9]+)\s*", linetext)
if key != None:
if key is not None:
self._mspec.gpu_l1 = key
continue
key = search(r"^\s*L2:\s+ ([a-zA-Z0-9]+)\s*", linetext)
if key != None:
if key is not None:
self._mspec.gpu_l2 = key
continue
key = search(r"^\s*Max Clock Freq\. \(MHz\):\s+([0-9]+)", linetext)
if key != None:
if key is not None:
self._mspec.max_sclk = key
continue
key = search(r"^\s*Compute Unit:\s+ ([a-zA-Z0-9]+)\s*", linetext)
if key != None:
if key is not None:
self._mspec.cu_per_gpu = key
continue
key = search(r"^\s*SIMDs per CU:\s+ ([a-zA-Z0-9]+)\s*", linetext)
if key != None:
if key is not None:
self._mspec.simd_per_cu = key
continue
key = search(r"^\s*Shader Engines:\s+ ([a-zA-Z0-9]+)\s*", linetext)
if key != None:
if key is not None:
self._mspec.se_per_gpu = key
continue
key = search(r"^\s*Wavefront Size:\s+ ([a-zA-Z0-9]+)\s*", linetext)
if key != None:
if key is not None:
self._mspec.wave_size = key
continue
key = search(r"^\s*Workgroup Max Size:\s+ ([a-zA-Z0-9]+)\s*", linetext)
if key != None:
if key is not None:
self._mspec.workgroup_max_size = key
continue
key = search(r"^\s*Max Waves Per CU:\s+ ([a-zA-Z0-9]+)\s*", linetext)
if key != None:
if key is not None:
self._mspec.max_waves_per_cu = key
break
self._mspec.sqc_per_gpu = str(
total_sqc(
self._mspec.gpu_arch, self._mspec.cu_per_gpu, self._mspec.se_per_gpu
if self._mspec.gpu_arch and self._mspec.cu_per_gpu and self._mspec.se_per_gpu:
self._mspec.sqc_per_gpu = str(
total_sqc(
self._mspec.gpu_arch, self._mspec.cu_per_gpu, self._mspec.se_per_gpu
)
)
)
# Parse json from amd-smi static --clock
static_data = json.loads(
@@ -187,14 +190,15 @@ class OmniSoC_Base:
# 100 Mhz -> 100
self._mspec.max_mclk = amd_smi_mclk.split()[0]
console_debug("max mem clock is {}".format(self._mspec.max_mclk))
console_debug(f"max mem clock is {self._mspec.max_mclk}")
# These are just max values now, because the parsing was broken and this was
# inconsistent with how we use the clocks elsewhere (all max, all the time)
self._mspec.cur_sclk = self._mspec.max_sclk
self._mspec.cur_mclk = self._mspec.max_mclk
self._mspec.gpu_series = mi_gpu_specs.get_gpu_series(self._mspec.gpu_arch)
if self._mspec.gpu_arch:
self._mspec.gpu_series = mi_gpu_specs.get_gpu_series(self._mspec.gpu_arch)
# specify gpu model name for gfx942 hardware
self._mspec.gpu_model = mi_gpu_specs.get_gpu_model(
self._mspec.gpu_arch, self._mspec.gpu_chip_id
@@ -212,7 +216,7 @@ class OmniSoC_Base:
)
@demarcate
def detect_gpu_model(self, gpu_arch):
def detect_gpu_model(self, gpu_arch: str) -> Optional[str]:
"""
Detects the GPU model using various identifiers from 'amd-smi static'.
Falls back through multiple methods if the primary method fails.
@@ -225,11 +229,15 @@ class OmniSoC_Base:
static_data = run(
["amd-smi", "static", "--gpu=0", "--json"], exit_on_error=True
)
gpu_list = (
static_data
if isinstance(static_data, list)
else static_data.get("gpu_data", [])
)
try:
parsed_data = json.loads(static_data)
gpu_list = (
parsed_data
if isinstance(parsed_data, list)
else parsed_data.get("gpu_data", [])
)
except json.JSONDecodeError:
gpu_list = []
gpu_data = gpu_list[0] if gpu_list else {}
# Try detection methods until we find a match
@@ -244,7 +252,7 @@ class OmniSoC_Base:
detected_name = gpu_data.get(section, {}).get(field, "").lower()
for model in mi_gpu_specs.get_all_gpu_models():
if model in detected_name:
console_log(f"GPU model '{model}' detected using {section}.{field}")
console_log(f'GPU model "{model}" detected using {section}.{field}')
gpu_model = model
break
@@ -255,12 +263,12 @@ class OmniSoC_Base:
gpu_model = self._adjust_mi300_model(gpu_model.lower(), gpu_arch.lower())
if gpu_model.lower() not in mi_gpu_specs.get_num_xcds_dict().keys():
console_warning(f"Unknown GPU model detected: '{gpu_model}'.")
console_warning(f'Unknown GPU model detected: "{gpu_model}".')
return
return gpu_model.upper()
def _adjust_mi300_model(self, gpu_model, gpu_arch):
def _adjust_mi300_model(self, gpu_model: str, gpu_arch: str) -> str:
"""
Applies specific adjustments for MI300 series GPU models based on architecture.
"""
@@ -274,7 +282,7 @@ class OmniSoC_Base:
return gpu_model
@demarcate
def detect_counters(self):
def detect_counters(self) -> tuple[set[str], list[str]]:
"""
Create a set of counters required for the selected report sections.
Parse analysis report configuration files based on the selected report
@@ -290,11 +298,11 @@ class OmniSoC_Base:
}
filter_blocks = args.filter_blocks
if args.set_selected:
if args.set_selected and self.__arch:
sets_info = parse_sets_yaml(self.__arch)
if args.set_selected not in set(sets_info.keys()):
console_error(
f"argument --set: invalid choice: '{args.set_selected}' "
f'argument --set: invalid choice: "{args.set_selected}" '
f"(choose from {sets_info.keys()})"
)
filter_blocks = [
@@ -304,25 +312,24 @@ class OmniSoC_Base:
elif args.roof_only:
filter_blocks = ["4"]
texts = list()
texts: list[str] = []
if not filter_blocks:
# Select all sections by default
for filename in config_filename_dict.values():
with open(filename, "r") as stream:
with open(filename) as stream:
texts.append(stream.read())
for block_id in filter_blocks:
file_id, panel_id, metric_id = convert_metric_id_to_panel_info(block_id)
# File id filtering
if file_id not in config_filename_dict:
console_warning(
(
f"Skipping {block_id}: file id {file_id} not found in "
f"{config_root_dir}"
)
f"Skipping {block_id}: file id {file_id} not found in "
f"{config_root_dir}"
)
continue
with open(config_filename_dict[file_id], "r") as stream:
with open(config_filename_dict[file_id]) as stream:
file_config = yaml.safe_load(stream)
if panel_id is None:
# If no panel id level filtering, then read the whole file
@@ -337,10 +344,8 @@ class OmniSoC_Base:
}
if panel_id not in panel_dict:
console_warning(
(
f"Skipping {block_id}: metric table {panel_id} not found in "
f"{config_filename_dict[file_id]}"
)
f"Skipping {block_id}: metric table {panel_id} not found in "
f"{config_filename_dict[file_id]}"
)
continue
if metric_id is None:
@@ -355,10 +360,8 @@ class OmniSoC_Base:
}
if metric_id not in metric_dict:
console_warning(
(
f"Skipping {block_id}: metric id {metric_id} not found in "
f"panel id {panel_id}"
)
f"Skipping {block_id}: metric id {metric_id} not found in "
f"panel id {panel_id}"
)
continue
texts.append(yaml.dump(metric_dict[metric_id], sort_keys=False))
@@ -368,23 +371,21 @@ class OmniSoC_Base:
# Handle TCC channel counters: if hw_counter_matches has elems ending with '['
# Expand and interleve the TCC channel counters
# e.g. TCC_HIT[0] TCC_ATOMIC[0] ... TCC_HIT[1] TCC_ATOMIC[1] ...
if using_v3():
num_xcd_for_pmc_file = int(self._mspec.num_xcd)
else:
num_xcd_for_pmc_file = 1
num_xcd_for_pmc_file = int(self._mspec.num_xcd) if using_v3() else 1
for counter_name in counters.copy():
if counter_name.startswith("TCC") and counter_name.endswith("["):
counters.remove(counter_name)
counter_name = counter_name.split("[")[0]
counters = counters.union({
f"{counter_name}[{i}]"
for i in range(num_xcd_for_pmc_file * int(self._mspec._l2_banks))
for i in range(num_xcd_for_pmc_file * int(self._mspec.l2_banks))
})
return counters, filter_blocks
@demarcate
def perfmon_filter(self):
def perfmon_filter(self) -> list[str]:
"""Filter default performance counter set based on user arguments"""
counters, filter_blocks = self.detect_counters()
@@ -413,7 +414,7 @@ class OmniSoC_Base:
return filter_blocks
@demarcate
def parse_counters(self, config_text):
def parse_counters(self, config_text: str) -> set[str]:
"""
Create a set of all hardware counters mentioned in the given config file
content string.
@@ -421,7 +422,7 @@ class OmniSoC_Base:
hw_counter_matches, variable_matches = self.parse_counters_text(config_text)
# get hw counters and variables for all supported denominators
for formula in supported_denom.values():
for formula in SUPPORTED_DENOM.values():
hw_counter_matches_denom, variable_matches_denom = self.parse_counters_text(
formula
)
@@ -430,13 +431,13 @@ class OmniSoC_Base:
# get hw counters corresponding to variables recursively
while variable_matches:
subvariable_matches = set()
subvariable_matches: set[str] = set()
for var in variable_matches:
if var in build_in_vars:
if var in BUILD_IN_VARS:
(
hw_counter_matches_vars,
variable_matches_vars,
) = self.parse_counters_text(build_in_vars[var])
) = self.parse_counters_text(BUILD_IN_VARS[var])
hw_counter_matches.update(hw_counter_matches_vars)
subvariable_matches.update(variable_matches_vars)
# process new found variables
@@ -444,7 +445,7 @@ class OmniSoC_Base:
return hw_counter_matches
def parse_counters_text(self, text):
def parse_counters_text(self, text: str) -> tuple[set[str], set[str]]:
"""Parse out hardware counters and variables from given text"""
# hw counter name should start with ip block name
# hw counter name should have all capital letters or digits
@@ -461,8 +462,9 @@ class OmniSoC_Base:
hw_counter_matches = hw_counter_matches - variable_matches
return hw_counter_matches, variable_matches
def get_rocprof_supported_counters(self):
rocprof_cmd = detect_rocprof(self.get_args())
def get_rocprof_supported_counters(self) -> set[str]:
args = self.get_args()
rocprof_cmd = detect_rocprof(args)
if rocprof_cmd != "rocprofiler-sdk":
console_warning(
@@ -473,67 +475,55 @@ class OmniSoC_Base:
"--rocprofiler-sdk-library-path."
)
rocprof_counters = set()
rocprof_counters: set[str] = set()
if str(rocprof_cmd).endswith("rocprof"):
command = [rocprof_cmd, "--list-basic"]
success, output = capture_subprocess_output(command, enable_logging=False)
# return code should be 1 so success should be False
if success:
console_error(
"Failed to list rocprof supported counters using command: %s"
% command
if rocprof_cmd.endswith("rocprof"):
for list_type in ["--list-basic", "--list-derived"]:
command = [rocprof_cmd, list_type]
success, output = capture_subprocess_output(
command, enable_logging=False
)
for line in output.splitlines():
if "gpu-agent" in line:
counters, _ = self.parse_counters_text(line.split(":")[1].strip())
rocprof_counters.update(counters)
# return code should be 1 so success should be False
if success:
console_error(
"Failed to list rocprof supported counters using command: "
f"{command}"
)
command = [rocprof_cmd, "--list-derived"]
success, output = capture_subprocess_output(command, enable_logging=False)
# return code should be 1 so success should be False
if success:
console_error(
"Failed to list rocprof supported counters using command: %s"
% command
)
for line in output.splitlines():
if "gpu-agent" in line:
counters, _ = self.parse_counters_text(line.split(":")[1].strip())
rocprof_counters.update(counters)
elif str(rocprof_cmd).endswith("rocprofv2"):
for line in output.splitlines():
if "gpu-agent" in line:
counters, _ = self.parse_counters_text(
line.split(":")[1].strip()
)
rocprof_counters.update(counters)
elif rocprof_cmd.endswith("rocprofv2"):
command = [rocprof_cmd, "--list-counters"]
success, output = capture_subprocess_output(command, enable_logging=False)
# return code should be 1 so success should be False
if success:
console_error(
"Failed to list rocprof supported counters using command: %s"
% command
"Failed to list rocprof supported counters using command: "
f"{command}"
)
for line in output.splitlines():
if "gfx" in line:
counters, _ = self.parse_counters_text(line.split(":")[2].strip())
rocprof_counters.update(counters)
elif (
str(rocprof_cmd).endswith("rocprofv3")
or str(rocprof_cmd) == "rocprofiler-sdk"
):
elif rocprof_cmd.endswith("rocprofv3") or rocprof_cmd == "rocprofiler-sdk":
# Point to counter definition
old_rocprofiler_metrics_path = os.environ.get("ROCPROFILER_METRICS_PATH")
os.environ["ROCPROFILER_METRICS_PATH"] = str(
config.rocprof_compute_home / "rocprof_compute_soc" / "profile_configs"
)
sys.path.append(
str(
Path(self.get_args().rocprofiler_sdk_library_path).parent.parent
/ "bin"
)
str(Path(args.rocprofiler_sdk_library_path).parent.parent / "bin")
)
from rocprofv3_avail_module import avail
avail.loadLibrary.libname = str(
Path(self.get_args().rocprofiler_sdk_library_path).parent.parent
Path(args.rocprofiler_sdk_library_path).parent.parent
/ "lib"
/ "rocprofiler-sdk"
/ "librocprofv3-list-avail.so"
@@ -552,20 +542,20 @@ class OmniSoC_Base:
else:
console_error(
"Incompatible profiler: %s. Supported profilers include: %s"
% (rocprof_cmd, get_submodules("rocprof_compute_profile"))
f"Incompatible profiler: {rocprof_cmd}. Supported profilers include: "
f"{get_submodules('rocprof_compute_profile')}"
)
return rocprof_counters
@demarcate
def perfmon_coalesce(self, counters):
def perfmon_coalesce(self, counters: set[str]) -> None:
"""
Sort and bucket all related performance counters to minimize required
application passes
"""
workload_perfmon_dir = self.get_args().path + "/perfmon"
Path(workload_perfmon_dir).mkdir(parents=True, exist_ok=True)
workload_perfmon_dir = Path(self.get_args().path) / "perfmon"
workload_perfmon_dir.mkdir(parents=True, exist_ok=True)
# Sanity check whether counters are supported by underlying rocprof tool
rocprof_counters = self.get_rocprof_supported_counters()
@@ -575,27 +565,27 @@ class OmniSoC_Base:
counter.split("[")[0] if is_tcc_channel_counter(counter) else counter
for counter in counters
} - rocprof_counters
if not_supported_counters:
console_warning(
"Following counters might not be supported by rocprof: %s"
% ", ".join(not_supported_counters)
"Following counters might not be supported by rocprof: "
f"{', '.join(not_supported_counters)}"
)
# We might be providing definitions of unsupported counters, so still try to
# collect them
if not counters:
console_error(
"profiling",
(
"No performance counters to collect, "
"please check the provided profiling filters"
),
"No performance counters to collect, "
"please check the provided profiling filters",
)
else:
console_debug(f"Collecting following counters: {', '.join(counters)} ")
output_files = []
console_debug(f"Collecting following counters: {', '.join(counters)} ")
output_files: list[CounterFile] = []
accu_file_count = 0
# Create separate perfmon file for LEVEL counters without _sum suffix
# TCC LEVEL counters are handled channel wise, so ignore them
for counter in counters.copy():
@@ -609,6 +599,7 @@ class OmniSoC_Base:
CounterFile(counter + ".txt", self.__perfmon_config)
)
output_files[-1].add(counter)
if using_v3():
# v3 does not support SQ_ACCUM_PREV_HIRES. Use custom counters
# defined in counter_defs.yaml that utilize accumulate(),
@@ -620,7 +611,8 @@ class OmniSoC_Base:
file_count = 0
# Store all channels for a TCC channel counter in the same file
tcc_channel_counter_file_map = dict()
tcc_channel_counter_file_map: dict[str, CounterFile] = {}
for ctr in counters:
# Store all channels for a TCC channel counter in the same file
if is_tcc_channel_counter(ctr):
@@ -628,29 +620,26 @@ class OmniSoC_Base:
if output_file:
output_file.add(ctr)
continue
# Add counter to first file that has room
added = False
for i in range(len(output_files)):
if output_files[i].add(ctr):
for output_file in output_files:
if output_file.add(ctr):
added = True
# Store all channels for a TCC channel counter in the same file
if is_tcc_channel_counter(ctr):
tcc_channel_counter_file_map[ctr.split("[")[0]] = output_files[
i
]
tcc_channel_counter_file_map[ctr.split("[")[0]] = output_file
break
# All files are full, create a new file
if not added:
output_files.append(
CounterFile(
"pmc_perf_{}.txt".format(file_count), self.__perfmon_config
)
CounterFile(f"pmc_perf_{file_count}.txt", self.__perfmon_config)
)
file_count += 1
output_files[-1].add(ctr)
console_debug("profiling", "perfmon_coalesce file_count %s" % file_count)
console_debug("profiling", f"perfmon_coalesce file_count {file_count}")
# TODO: rewrite the above logic for spatial_multiplexing later
if self.get_args().spatial_multiplexing:
@@ -661,9 +650,9 @@ class OmniSoC_Base:
"multiplexing need provide node_idx node_count and gpu_count",
)
node_idx = int(self.get_args().spatial_multiplexing[0])
node_count = int(self.get_args().spatial_multiplexing[1])
gpu_count = int(self.get_args().spatial_multiplexing[2])
node_idx, node_count, gpu_count = map(
int, self.get_args().spatial_multiplexing
)
old_group_num = file_count + accu_file_count
new_bucket_count = node_count * gpu_count
@@ -677,34 +666,17 @@ class OmniSoC_Base:
console_debug(
"profiling",
(
"spatial_multiplexing node_idx %s, node_count %s, gpu_count: %s,\n"
"old_group_num %s, new_bucket_count %s, groups_per_bucket %s,\n"
"max_groups_per_node %s, group_start %s, group_end %s"
)
% (
node_idx,
node_count,
gpu_count,
old_group_num,
new_bucket_count,
groups_per_bucket,
max_groups_per_node,
group_start,
group_end,
),
f"spatial_multiplexing node_idx {node_idx}, node_count {node_count}, "
f"gpu_count: {gpu_count},\n"
f"old_group_num {old_group_num}, new_bucket_count {new_bucket_count}, "
f"groups_per_bucket {groups_per_bucket},\n"
f"max_groups_per_node {max_groups_per_node}, "
f"group_start {group_start}, group_end {group_end}",
)
for f_idx in range(groups_per_bucket):
file_name = str(
Path(workload_perfmon_dir).joinpath(
"pmc_perf_"
+ "node_"
+ str(node_idx)
+ "_"
+ str(f_idx)
+ ".txt"
)
file_name = (
Path(workload_perfmon_dir) / f"pmc_perf_node_{node_idx}_{f_idx}.txt"
)
pmc = []
@@ -715,65 +687,55 @@ class OmniSoC_Base:
gpu_idx = g_idx % gpu_count
for block_name in output_files[g_idx].blocks.keys():
for ctr in output_files[g_idx].blocks[block_name].elements:
pmc.append(ctr + ":device=" + str(gpu_idx))
stext = "pmc: " + " ".join(pmc)
pmc.append(f"{ctr}:device={gpu_idx}")
# Write counters to file
fd = open(file_name, "w")
fd.write(stext + "\n\n")
fd.close()
with open(file_name, "w") as fd:
fd.write(f"pmc: {' '.join(pmc)}\n\n")
else:
# Output to files
for f in output_files:
file_name_txt = str(
Path(workload_perfmon_dir).joinpath(f.file_name_txt)
)
file_name_yaml = str(
Path(workload_perfmon_dir).joinpath(f.file_name_yaml)
)
file_name_txt = workload_perfmon_dir / f.file_name_txt
file_name_yaml = workload_perfmon_dir / f.file_name_yaml
pmc = []
counter_def = dict()
counter_def: dict[str, Any] = {}
for ctr in [
ctr
for block_name in f.blocks
for ctr in f.blocks[block_name].elements
]:
pmc.append(ctr)
if using_v3():
# Add TCC channel counters definitions
if is_tcc_channel_counter(ctr):
counter_name = ctr.split("[")[0]
idx = int(ctr.split("[")[1].split("]")[0])
xcd_idx = idx // int(self._mspec._l2_banks)
channel_idx = idx % int(self._mspec._l2_banks)
expression = (
f"select({counter_name},"
f"[DIMENSION_XCC=[{xcd_idx}], "
f"DIMENSION_INSTANCE=[{channel_idx}]])"
)
description = (
f"{counter_name} on {xcd_idx}th XCC and "
f"{channel_idx}th channel"
)
counter_def = add_counter_extra_config_input_yaml(
counter_def,
ctr,
description,
expression,
[self.__arch],
)
if using_v3() and is_tcc_channel_counter(ctr):
counter_name = ctr.split("[")[0]
idx = int(ctr.split("[")[1].split("]")[0])
xcd_idx = idx // int(self._mspec.l2_banks)
channel_idx = idx % int(self._mspec.l2_banks)
expression = (
f"select({counter_name},"
f"[DIMENSION_XCC=[{xcd_idx}], "
f"DIMENSION_INSTANCE=[{channel_idx}]])"
)
description = (
f"{counter_name} on {xcd_idx}th XCC and "
f"{channel_idx}th channel"
)
counter_def = add_counter_extra_config_input_yaml(
counter_def,
ctr,
description,
expression,
[self.__arch],
)
stext = "pmc: " + " ".join(pmc)
# Write counters to file
fd = open(file_name_txt, "w")
fd.write(stext + "\n\n")
fd.write("gpu:\n")
fd.write("range:\n")
fd.write("kernel:\n")
fd.close()
with open(file_name_txt, "w") as fd:
fd.write(f"pmc: {' '.join(pmc)}\n\n")
fd.write("gpu:\n")
fd.write("range:\n")
fd.write("kernel:\n")
# Write counter definitions to file
if counter_def:
@@ -783,25 +745,26 @@ class OmniSoC_Base:
# Add a timestamp file
# TODO: Does v3 need this?
if not using_v3():
fd = open(str(Path(workload_perfmon_dir).joinpath("timestamps.txt")), "w")
fd.write("pmc:\n\n")
fd.write("gpu:\n")
fd.write("range:\n")
fd.write("kernel:\n")
fd.close()
timestamp_file = workload_perfmon_dir / "timestamps.txt"
with open(timestamp_file, "w") as fd:
fd.write("pmc:\n\n")
fd.write("gpu:\n")
fd.write("range:\n")
fd.write("kernel:\n")
# ----------------------------------------------------
# Required methods to be implemented by child classes
# ----------------------------------------------------
@abstractmethod
def profiling_setup(self):
def profiling_setup(self) -> Optional[list[str]]:
"""Perform any SoC-specific setup prior to profiling."""
console_debug("profiling", "perform SoC profiling setup for %s" % self.__arch)
console_debug("profiling", f"perform SoC profiling setup for {self.__arch}")
@abstractmethod
def post_profiling(self):
def post_profiling(self) -> None:
"""Perform any SoC-specific post profiling activities."""
console_debug("profiling", "perform SoC post processing for %s" % self.__arch)
console_debug("profiling", f"perform SoC post processing for {self.__arch}")
# Roofline can be skipped via --no-roof
# Roofline not supported on MI 100
# If --filter-blocks is provided, roofline block (block 4) should be mentioned
@@ -815,23 +778,23 @@ class OmniSoC_Base:
):
console_log("roofline", "Skipping roofline")
else:
pmc_path = str(Path(self.get_args().path).joinpath("pmc_perf.csv"))
if not Path(pmc_path).is_file():
pmc_path = Path(self.get_args().path) / "pmc_perf.csv"
if not pmc_path.is_file():
console_warning(
"Incomplete or missing profiling data. Skipping roofline."
)
return
console_log(
"roofline", "Checking for roofline.csv in " + str(self.get_args().path)
"roofline", f"Checking for roofline.csv in {self.get_args().path}"
)
if not Path(self.get_args().path).joinpath("roofline.csv").is_file():
if not (Path(self.get_args().path) / "roofline.csv").is_file():
mibench(self.get_args(), self._mspec)
self.roofline_obj.post_processing()
@abstractmethod
def analysis_setup(self, roofline_parameters=None):
def analysis_setup(self, roofline_parameters: Optional[dict[str, Any]]) -> None:
"""Perform any SoC-specific setup prior to analysis."""
console_debug("analysis", "perform SoC analysis setup for %s" % self.__arch)
console_debug("analysis", f"perform SoC analysis setup for {self.__arch}")
if roofline_parameters:
self.roofline_obj = Roofline(
self.get_args(), self._mspec, roofline_parameters
@@ -840,20 +803,20 @@ class OmniSoC_Base:
# Set with limited size
class LimitedSet:
def __init__(self, maxsize) -> None:
self.avail = maxsize
self.elements = []
def __init__(self, maxsize: int) -> None:
self.avail: int = maxsize
self.elements: list[str] = []
def add(self, e) -> None:
if e in self.elements:
def add(self, element: str) -> bool:
if element in self.elements:
return True
# Store all channels for a TCC channel counter in the same file
if e.split("[")[0] in {element.split("[")[0] for element in self.elements}:
self.elements.append(e)
if element.split("[")[0] in {elem.split("[")[0] for elem in self.elements}:
self.elements.append(element)
return True
if self.avail > 0:
self.avail -= 1
self.elements.append(e)
self.elements.append(element)
return True
return False
@@ -861,13 +824,15 @@ class LimitedSet:
# Represents a file that lists PMC counters. Number of counters for each
# block limited according to perfmon config.
class CounterFile:
def __init__(self, name, perfmon_config) -> None:
def __init__(self, name: str, perfmon_config: dict[str, int]) -> None:
name_no_extension = name.split(".")[0]
self.file_name_txt = name_no_extension + ".txt"
self.file_name_yaml = name_no_extension + ".yaml"
self.blocks = {b: LimitedSet(v) for b, v in perfmon_config.items()}
self.file_name_txt: str = name_no_extension + ".txt"
self.file_name_yaml: str = name_no_extension + ".yaml"
self.blocks: dict[str, LimitedSet] = {
block: LimitedSet(capacity) for block, capacity in perfmon_config.items()
}
def add(self, counter) -> bool:
def add(self, counter: str) -> bool:
block = counter.split("_")[0]
# SQ and SQC belong to the same IP block
@@ -22,22 +22,26 @@
# THE SOFTWARE.
##############################################################################
import argparse
from typing import Any, Optional
from rocprof_compute_soc.soc_base import OmniSoC_Base
from utils.logger import console_error, demarcate
from utils.mi_gpu_spec import mi_gpu_specs
from utils.specs import MachineSpecs
class gfx908_soc(OmniSoC_Base):
def __init__(self, args, mspec):
def __init__(self, args: argparse.Namespace, mspec: MachineSpecs) -> None:
super().__init__(args, mspec)
self.set_arch("gfx908")
self.set_compatible_profilers(["rocprofv1", "rocprofv3", "rocprofiler-sdk"])
# Per IP block max number of simultaneous counters. GFX IP Blocks
self.set_perfmon_config(mi_gpu_specs.get_perfmon_config("gfx908"))
# Set arch specific specs
self._mspec._l2_banks = 32
self._mspec.l2_banks = 32
self._mspec.lds_banks_per_cu = 32
self._mspec.pipes_per_gpu = 4
@@ -45,21 +49,24 @@ class gfx908_soc(OmniSoC_Base):
# Required child methods
# -----------------------
@demarcate
def profiling_setup(self):
def profiling_setup(self) -> Optional[list[str]]:
"""Perform any SoC-specific setup prior to profiling."""
super().profiling_setup()
if self.get_args().roof_only:
console_error("%s does not support roofline analysis" % self.get_arch())
console_error(f"{self.get_arch()} does not support roofline analysis")
# Perfmon filtering
filter_blocks = self.perfmon_filter()
return filter_blocks
@demarcate
def post_profiling(self):
def post_profiling(self) -> None:
"""Perform any SoC-specific post profiling activities."""
super().post_profiling()
@demarcate
def analysis_setup(self, roofline_parameters=None):
def analysis_setup(
self, roofline_parameters: Optional[dict[str, Any]] = None
) -> None:
"""Perform any SoC-specific setup prior to analysis."""
super().analysis_setup(roofline_parameters=roofline_parameters)
@@ -22,15 +22,17 @@
# THE SOFTWARE.
##############################################################################
import argparse
from typing import Any, Optional
from rocprof_compute_soc.soc_base import OmniSoC_Base
from utils.logger import demarcate
from utils.mi_gpu_spec import mi_gpu_specs
from utils.specs import MachineSpecs
class gfx90a_soc(OmniSoC_Base):
def __init__(self, args, mspec):
def __init__(self, args: argparse.Namespace, mspec: MachineSpecs) -> None:
super().__init__(args, mspec)
self.set_arch("gfx90a")
self.set_compatible_profilers([
@@ -43,7 +45,7 @@ class gfx90a_soc(OmniSoC_Base):
self.set_perfmon_config(mi_gpu_specs.get_perfmon_config("gfx90a"))
# Set arch specific specs
self._mspec._l2_banks = 32
self._mspec.l2_banks = 32
self._mspec.lds_banks_per_cu = 32
self._mspec.pipes_per_gpu = 4
@@ -51,7 +53,7 @@ class gfx90a_soc(OmniSoC_Base):
# Required child methods
# -----------------------
@demarcate
def profiling_setup(self):
def profiling_setup(self) -> None:
"""Perform any SoC-specific setup prior to profiling."""
super().profiling_setup()
# Performance counter filtering
@@ -59,11 +61,13 @@ class gfx90a_soc(OmniSoC_Base):
return filter_blocks
@demarcate
def post_profiling(self):
def post_profiling(self) -> None:
"""Perform any SoC-specific post profiling activities."""
super().post_profiling()
@demarcate
def analysis_setup(self, roofline_parameters=None):
def analysis_setup(
self, roofline_parameters: Optional[dict[str, Any]] = None
) -> None:
"""Perform any SoC-specific setup prior to analysis."""
super().analysis_setup(roofline_parameters=roofline_parameters)
@@ -22,15 +22,17 @@
# THE SOFTWARE.
##############################################################################
import argparse
from typing import Any, Optional
from rocprof_compute_soc.soc_base import OmniSoC_Base
from utils.logger import demarcate
from utils.mi_gpu_spec import mi_gpu_specs
from utils.specs import MachineSpecs
class gfx940_soc(OmniSoC_Base):
def __init__(self, args, mspec):
def __init__(self, args: argparse.Namespace, mspec: MachineSpecs) -> None:
super().__init__(args, mspec)
self.set_arch("gfx940")
self.set_compatible_profilers([
@@ -43,7 +45,7 @@ class gfx940_soc(OmniSoC_Base):
self.set_perfmon_config(mi_gpu_specs.get_perfmon_config("gfx940"))
# Set arch specific specs
self._mspec._l2_banks = 16
self._mspec.l2_banks = 16
self._mspec.lds_banks_per_cu = 32
self._mspec.pipes_per_gpu = 4
@@ -51,7 +53,7 @@ class gfx940_soc(OmniSoC_Base):
# Required child methods
# -----------------------
@demarcate
def profiling_setup(self):
def profiling_setup(self) -> Optional[list[str]]:
"""Perform any SoC-specific setup prior to profiling."""
super().profiling_setup()
# Performance counter filtering
@@ -59,11 +61,13 @@ class gfx940_soc(OmniSoC_Base):
return filter_blocks
@demarcate
def post_profiling(self):
def post_profiling(self) -> None:
"""Perform any SoC-specific post profiling activities."""
super().post_profiling()
@demarcate
def analysis_setup(self, roofline_parameters=None):
def analysis_setup(
self, roofline_parameters: Optional[dict[str, Any]] = None
) -> None:
"""Perform any SoC-specific setup prior to analysis."""
super().analysis_setup(roofline_parameters=roofline_parameters)
@@ -22,15 +22,17 @@
# THE SOFTWARE.
##############################################################################
import argparse
from typing import Any, Optional
from rocprof_compute_soc.soc_base import OmniSoC_Base
from utils.logger import demarcate
from utils.mi_gpu_spec import mi_gpu_specs
from utils.specs import MachineSpecs
class gfx941_soc(OmniSoC_Base):
def __init__(self, args, mspec):
def __init__(self, args: argparse.Namespace, mspec: MachineSpecs) -> None:
super().__init__(args, mspec)
self.set_arch("gfx941")
self.set_compatible_profilers([
@@ -43,7 +45,7 @@ class gfx941_soc(OmniSoC_Base):
self.set_perfmon_config(mi_gpu_specs.get_perfmon_config("gfx941"))
# Set arch specific specs
self._mspec._l2_banks = 16
self._mspec.l2_banks = 16
self._mspec.lds_banks_per_cu = 32
self._mspec.pipes_per_gpu = 4
@@ -51,7 +53,7 @@ class gfx941_soc(OmniSoC_Base):
# Required child methods
# -----------------------
@demarcate
def profiling_setup(self):
def profiling_setup(self) -> Optional[list[str]]:
"""Perform any SoC-specific setup prior to profiling."""
super().profiling_setup()
# Performance counter filtering
@@ -59,11 +61,13 @@ class gfx941_soc(OmniSoC_Base):
return filter_blocks
@demarcate
def post_profiling(self):
def post_profiling(self) -> None:
"""Perform any SoC-specific post profiling activities."""
super().post_profiling()
@demarcate
def analysis_setup(self, roofline_parameters=None):
def analysis_setup(
self, roofline_parameters: Optional[dict[str, Any]] = None
) -> None:
"""Perform any SoC-specific setup prior to analysis."""
super().analysis_setup(roofline_parameters=roofline_parameters)
@@ -22,15 +22,17 @@
# THE SOFTWARE.
##############################################################################
import argparse
from typing import Any, Optional
from rocprof_compute_soc.soc_base import OmniSoC_Base
from utils.logger import demarcate
from utils.mi_gpu_spec import mi_gpu_specs
from utils.specs import MachineSpecs
class gfx942_soc(OmniSoC_Base):
def __init__(self, args, mspec):
def __init__(self, args: argparse.Namespace, mspec: MachineSpecs) -> None:
super().__init__(args, mspec)
self.set_arch("gfx942")
self.set_compatible_profilers([
@@ -43,7 +45,7 @@ class gfx942_soc(OmniSoC_Base):
self.set_perfmon_config(mi_gpu_specs.get_perfmon_config("gfx942"))
# Set arch specific specs
self._mspec._l2_banks = 16
self._mspec.l2_banks = 16
self._mspec.lds_banks_per_cu = 32
self._mspec.pipes_per_gpu = 4
@@ -51,7 +53,7 @@ class gfx942_soc(OmniSoC_Base):
# Required child methods
# -----------------------
@demarcate
def profiling_setup(self):
def profiling_setup(self) -> Optional[list[str]]:
"""Perform any SoC-specific setup prior to profiling."""
super().profiling_setup()
# Performance counter filtering
@@ -59,11 +61,13 @@ class gfx942_soc(OmniSoC_Base):
return filter_blocks
@demarcate
def post_profiling(self):
def post_profiling(self) -> None:
"""Perform any SoC-specific post profiling activities."""
super().post_profiling()
@demarcate
def analysis_setup(self, roofline_parameters=None):
def analysis_setup(
self, roofline_parameters: Optional[dict[str, Any]] = None
) -> None:
"""Perform any SoC-specific setup prior to analysis."""
super().analysis_setup(roofline_parameters=roofline_parameters)
@@ -22,15 +22,17 @@
# THE SOFTWARE.
##############################################################################
import argparse
from typing import Any, Optional
from rocprof_compute_soc.soc_base import OmniSoC_Base
from utils.logger import demarcate
from utils.mi_gpu_spec import mi_gpu_specs
from utils.specs import MachineSpecs
class gfx950_soc(OmniSoC_Base):
def __init__(self, args, mspec):
def __init__(self, args: argparse.Namespace, mspec: MachineSpecs) -> None:
super().__init__(args, mspec)
self.set_arch("gfx950")
self.set_compatible_profilers(["rocprofv3", "rocprofiler-sdk"])
@@ -38,7 +40,7 @@ class gfx950_soc(OmniSoC_Base):
self.set_perfmon_config(mi_gpu_specs.get_perfmon_config("gfx950"))
# Set arch specific specs
self._mspec._l2_banks = 16
self._mspec.l2_banks = 16
self._mspec.lds_banks_per_cu = 32
self._mspec.pipes_per_gpu = 4
@@ -46,7 +48,7 @@ class gfx950_soc(OmniSoC_Base):
# Required child methods
# -----------------------
@demarcate
def profiling_setup(self):
def profiling_setup(self) -> Optional[list[str]]:
"""Perform any SoC-specific setup prior to profiling."""
super().profiling_setup()
# Performance counter filtering
@@ -54,11 +56,13 @@ class gfx950_soc(OmniSoC_Base):
return filter_blocks
@demarcate
def post_profiling(self):
def post_profiling(self) -> None:
"""Perform any SoC-specific post profiling activities."""
super().post_profiling()
@demarcate
def analysis_setup(self, roofline_parameters=None):
def analysis_setup(
self, roofline_parameters: Optional[dict[str, Any]] = None
) -> None:
"""Perform any SoC-specific setup prior to analysis."""
super().analysis_setup(roofline_parameters=roofline_parameters)
@@ -23,8 +23,10 @@
##############################################################################
import argparse
import copy
from pathlib import Path
from typing import Any, Hashable, Optional, OrderedDict
import pandas as pd
@@ -39,17 +41,19 @@ from utils.logger import console_error, demarcate
class tui_analysis(OmniAnalyze_Base):
def __init__(self, args, supported_archs, path):
def __init__(
self, args: argparse.Namespace, supported_archs: dict[str, str], path: Path
) -> None:
super().__init__(args, supported_archs)
self.path = str(path)
self.args = self.get_args()
self.raw_dfs = {}
self.raw_dfs: dict[str, dict] = {}
# -----------------------
# Required child methods
# -----------------------
@demarcate
def pre_processing(self):
def pre_processing(self) -> None:
self._profiling_config = file_io.load_profiling_config(self.path)
self._runs = self.initalize_runs()
@@ -78,13 +82,12 @@ class tui_analysis(OmniAnalyze_Base):
filter_dispatch_ids=workload.filter_dispatch_ids,
filter_nodes=workload.filter_nodes,
time_unit=self.args.time_unit,
max_stat_num=self.args.max_stat_num,
kernel_verbose=self.args.kernel_verbose,
)
kernel_name_shortener(self._runs[self.path].raw_pmc, self.args.kernel_verbose)
# 1. load top kernel
parser.load_kernel_top(
parser.load_non_mertrics_table(
workload=self._runs[self.path], dir=self.path, args=self.args
)
@@ -92,7 +95,7 @@ class tui_analysis(OmniAnalyze_Base):
self.raw_dfs = {}
for idx in workload.raw_pmc.index:
kernel_df = workload.raw_pmc.loc[[idx]]
kernel_name = kernel_df.pmc_perf["Kernel_Name"].loc[idx]
kernel_name = str(kernel_df.pmc_perf["Kernel_Name"].loc[idx])
kernel_dfs = copy.deepcopy(workload.dfs)
parser.eval_metric(
@@ -107,9 +110,11 @@ class tui_analysis(OmniAnalyze_Base):
self.raw_dfs[kernel_name] = kernel_dfs
def initalize_runs(self, normalization_filter=None):
def initalize_runs(
self, normalization_filter: Optional[str] = None
) -> OrderedDict[str, schema.Workload]:
# Load system info and configure
sys_info = file_io.load_sys_info(Path(self.path) / "sysinfo.csv")
sys_info = file_io.load_sys_info(str(Path(self.path) / "sysinfo.csv"))
arch = sys_info.iloc[0]["gpu_arch"]
self.generate_configs(
@@ -132,11 +137,8 @@ class tui_analysis(OmniAnalyze_Base):
)
roofline_path = Path(self.path) / "roofline.csv"
w.roofline_peaks = (
pd.read_csv(roofline_path)
if not getattr(self.args, "no_roof", False) and roofline_path.exists()
else pd.DataFrame()
)
if roofline_path.is_file() and not getattr(self.args, "no_roof", False):
w.roofline_peaks = pd.read_csv(roofline_path)
w.avail_ips = w.sys_info["ip_blocks"].item().split("|")
w.dfs = copy.deepcopy(self._arch_configs[arch].dfs)
@@ -145,7 +147,7 @@ class tui_analysis(OmniAnalyze_Base):
self._runs[self.path] = w
return self._runs
def run_kernel_analysis(self):
def run_kernel_analysis(self) -> dict[str, Any]:
arch = list(self._arch_configs.keys())[0]
return {
kernel_name: process_panels_to_dataframes(
@@ -154,5 +156,5 @@ class tui_analysis(OmniAnalyze_Base):
for kernel_name, df in self.raw_dfs.items()
}
def run_top_kernel(self):
def run_top_kernel(self) -> Optional[list[dict[Hashable, Any]]]:
return get_top_kernels_and_dispatch_ids(self._runs)
@@ -27,12 +27,14 @@ ROCm Compute Profiler TUI - Main Application with Analysis Methods
----------------------------------------------------------------
"""
import argparse
import importlib
import json
from pathlib import Path
from typing import Any, Optional
from textual import on, work
from textual.app import App
from textual.app import App, ComposeResult
from textual.binding import Binding
from textual.widgets import Button, Footer, Header
from textual_fspicker import SelectDirectory
@@ -60,7 +62,11 @@ class RocprofTUIApp(App):
# Binding(key="a", action="analyze", description="Analyze"),
]
def __init__(self, args=None, supported_archs=None):
def __init__(
self,
args: argparse.Namespace,
supported_archs: Optional[dict[str, Any]] = None,
) -> None:
super().__init__()
self.main_view = MainView()
self.recent_dirs = self._load_recent_dirs()
@@ -68,37 +74,37 @@ class RocprofTUIApp(App):
# Analysis attributes
self.args = args
self.supported_archs = supported_archs or {}
self.soc = {}
self.mspec = None
self.soc: dict[str, Any] = {}
self.mspec: Optional[Any] = None
def compose(self):
def compose(self) -> ComposeResult:
yield Header()
yield self.main_view
yield Footer()
def action_refresh(self):
def action_refresh(self) -> None:
self.main_view.refresh_view()
def load_soc_specs(self, sysinfo=None):
def load_soc_specs(self, sysinfo: Optional[dict] = None) -> None:
self.mspec = generate_machine_specs(self.args, sysinfo)
arch = self.mspec.gpu_arch
soc_module = importlib.import_module(f"rocprof_compute_soc.soc_{arch}")
soc_class = getattr(soc_module, f"{arch}_soc")
self.soc[arch] = soc_class(self.args, self.mspec)
def _load_recent_dirs(self):
def _load_recent_dirs(self) -> list[str]:
recent_file = Path.home() / ".textual_browser_recent.json"
if recent_file.exists():
with open(recent_file, "r") as f:
with open(recent_file) as f:
return json.load(f)
return []
def _save_recent_dirs(self):
def _save_recent_dirs(self) -> None:
recent_file = Path.home() / ".textual_browser_recent.json"
with open(recent_file, "w") as f:
json.dump(self.recent_dirs, f, indent=2)
def add_recent_dir(self, directory):
def add_recent_dir(self, directory: str) -> None:
directory = str(Path(directory).absolute())
# Remove if exists, add to front, keep max 5
@@ -108,14 +114,14 @@ class RocprofTUIApp(App):
self.recent_dirs = self.recent_dirs[:5]
self._save_recent_dirs()
def on_recent_selected(self, selected_dir):
def on_recent_selected(self, selected_dir: Optional[str]) -> None:
if selected_dir:
self.main_view.selected_path = selected_dir
self.main_view.run_analysis()
@on(Button.Pressed, "#menu-open-workload")
@work
async def pick_directory(self):
async def pick_directory(self) -> None:
if opened := await self.push_screen_wait(SelectDirectory()):
self.add_recent_dir(str(opened))
self.main_view.selected_path = opened
@@ -123,7 +129,9 @@ class RocprofTUIApp(App):
self.main_view.run_analysis()
def run_tui(args=None, supported_archs=None):
def run_tui(
args: argparse.Namespace, supported_archs: Optional[dict[str, Any]] = None
) -> None:
"""Run the TUI application."""
app = RocprofTUIApp(args, supported_archs)
app.run()
@@ -22,14 +22,17 @@
# THE SOFTWARE.
##############################################################################
import argparse
import logging
from datetime import datetime
from enum import Enum
from typing import Any, Hashable, Optional
import pandas as pd
from textual.widgets import TextArea
import config
from utils import schema
class LogLevel(str, Enum):
@@ -40,11 +43,11 @@ class LogLevel(str, Enum):
class Logger:
def __init__(self, output_area=None):
def __init__(self, output_area: Optional[TextArea] = None) -> None:
self.output_area = output_area
self._setup_logger()
def _setup_logger(self):
def _setup_logger(self) -> None:
self.logger = logging.getLogger("app")
self.logger.setLevel(logging.INFO)
@@ -56,21 +59,23 @@ class Logger:
handler.setFormatter(formatter)
self.logger.addHandler(handler)
def set_output_area(self, output_area):
def set_output_area(self, output_area: TextArea) -> None:
self.output_area = output_area
def log(self, message, level="INFO", update_ui=True):
def log(
self, message: str, log_level: str = "INFO", update_ui: bool = True
) -> None:
level_map = {
"INFO": logging.INFO,
"SUCCESS": logging.INFO,
"WARNING": logging.WARNING,
"ERROR": logging.ERROR,
}
self.logger.log(level_map[level], message)
self.logger.log(level_map[log_level], message)
if update_ui and self.output_area and hasattr(self.output_area, "text"):
timestamp = datetime.now().strftime("%H:%M:%S")
formatted_msg = f"[{timestamp}] [{level}] {message}"
formatted_msg = f"[{timestamp}] [{log_level}] {message}"
self.output_area.text = (
f"{self.output_area.text}\n{formatted_msg}"
if self.output_area.text
@@ -78,20 +83,22 @@ class Logger:
)
self.output_area.cursor_location = (999999, 0)
def info(self, message, update_ui=True):
def info(self, message: str, update_ui: bool = True) -> None:
self.log(message, "INFO", update_ui)
def success(self, message, update_ui=True):
def success(self, message: str, update_ui: bool = True) -> None:
self.log(message, "SUCCESS", update_ui)
def warning(self, message, update_ui=True):
def warning(self, message: str, update_ui: bool = True) -> None:
self.log(message, "WARNING", update_ui)
def error(self, message, update_ui=True):
def error(self, message: str, update_ui: bool = True) -> None:
self.log(message, "ERROR", update_ui)
def get_top_kernels_and_dispatch_ids(runs):
def get_top_kernels_and_dispatch_ids(
runs: dict[str, Any],
) -> Optional[list[dict[Hashable, Any]]]:
if not runs:
return None
@@ -113,7 +120,12 @@ def get_top_kernels_and_dispatch_ids(runs):
return merged_df.to_dict("records")
def process_panels_to_dataframes(args, kernel_df, archConfigs, roof_plot=None):
def process_panels_to_dataframes(
args: argparse.Namespace,
kernel_df: dict[int, pd.DataFrame],
arch_configs: schema.ArchConfig,
roof_plot: Optional[str] = None,
) -> dict[str, dict[str, dict[str, Any]]]:
"""
Process panel data into pandas DataFrames.
Returns a nested dictionary structure with DataFrames and tui_style information.
@@ -139,7 +151,7 @@ def process_panels_to_dataframes(args, kernel_df, archConfigs, roof_plot=None):
result_structure = {}
decimal_precision = getattr(args, "decimal", 2) if args else 2
for panel_id, panel in archConfigs.panel_configs.items():
for panel_id, panel in arch_configs.panel_configs.items():
if panel_id in config.HIDDEN_SECTIONS:
continue
@@ -173,7 +185,7 @@ def process_panels_to_dataframes(args, kernel_df, archConfigs, roof_plot=None):
f"{table_config['id'] // 100}.{table_config['id'] % 100}"
)
if table_config.get("title"):
subsection_name += " " + table_config["title"]
subsection_name += f" {table_config['title']}"
section_data[subsection_name] = {
"df": df,
@@ -190,7 +202,7 @@ def process_panels_to_dataframes(args, kernel_df, archConfigs, roof_plot=None):
return result_structure
def apply_rounding_logic(df, decimal_precision):
def apply_rounding_logic(df: pd.DataFrame, decimal_precision: int) -> pd.DataFrame:
if df.empty:
return df
@@ -200,8 +212,8 @@ def apply_rounding_logic(df, decimal_precision):
if len(float_cols) > 0:
df_rounded[float_cols] = df_rounded[float_cols].round(decimal_precision)
other_cols = df_rounded.select_dtypes(exclude=["float"]).columns
for col in other_cols:
non_float_cols = df_rounded.select_dtypes(exclude=["float"]).columns
for col in non_float_cols:
numeric_series = pd.to_numeric(df_rounded[col], errors="coerce")
if numeric_series.notna().any():
df_rounded[col] = numeric_series.round(decimal_precision)
@@ -29,9 +29,10 @@ Panel Widget Modules
Contains the panel widgets used in the main layout.
"""
from typing import Optional
from typing import Any, Optional
from textual import on
from textual.app import ComposeResult
from textual.containers import Container, VerticalScroll
from textual.widgets import Label, RadioButton, RadioSet
@@ -78,13 +79,14 @@ class KernelView(Container):
}
"""
def __init__(self, config_path: Optional[str] = None):
def __init__(self, config_path: Optional[str] = None) -> None:
super().__init__(id="kernel-view")
self.kernel_to_df_dict = {}
self.top_kernel_to_df_list = []
self.current_selection = None
self.kernel_to_df_dict: dict[str, dict[str, Any]] = {}
self.top_kernel_to_df_list: list[dict[str, Any]] = []
self.current_selection: Optional[str] = None
self.status_label: Optional[Label] = None
self.config_path = config_path or (
self.config_path = config_path or str(
rocprof_compute_home
/ "rocprof_compute_tui"
/ "utils"
@@ -93,7 +95,7 @@ class KernelView(Container):
else None
)
def compose(self):
def compose(self) -> ComposeResult:
"""
Compose the split panel layout with two scrollable containers.
"""
@@ -110,7 +112,11 @@ class KernelView(Container):
# empty on init
pass
def update_results(self, kernel_to_df_dict, top_kernel_to_df_list) -> None:
def update_results(
self,
kernel_to_df_dict: dict[str, dict[str, Any]],
top_kernel_to_df_list: list[dict[str, Any]],
) -> None:
self.kernel_to_df_dict = kernel_to_df_dict
self.top_kernel_to_df_list = top_kernel_to_df_list
@@ -151,7 +157,7 @@ class KernelView(Container):
self.status_label.update(message)
self.status_label.set_classes(log_level)
def new_perf_metric(self):
def new_perf_metric(self) -> None:
new_metrics = ["VGPRs", "Grid Size", "Workgroup Size"]
for new_metric in new_metrics:
for i, kernel in enumerate(self.top_kernel_to_df_list):
@@ -171,7 +177,7 @@ class KernelView(Container):
self.current_selection = kernel_data["Kernel_Name"]
self.update_bottom_content()
def update_bottom_content(self):
def update_bottom_content(self) -> None:
bottom_container = self.query_one("#bottom-container", VerticalScroll)
bottom_container.remove_children()
@@ -29,8 +29,10 @@ Contains the main view layout and organization for the application.
"""
from pathlib import Path
from typing import Any, Optional
from textual import on, work
from textual.app import ComposeResult
from textual.containers import Horizontal, Vertical
from textual.reactive import reactive
from textual.widgets import DataTable
@@ -48,21 +50,21 @@ from utils import file_io
class MainView(Horizontal):
"""Main view layout for the application."""
selected_path = reactive(None)
kernel_to_df_dict = reactive({})
top_kernel_to_df_list = reactive([])
selected_path: reactive[Optional[Path]] = reactive(None)
kernel_to_df_dict: reactive[dict[str, dict[str, Any]]] = reactive({})
top_kernel_to_df_list: reactive[list[dict[str, Any]]] = reactive([])
def __init__(self):
def __init__(self) -> None:
super().__init__(id="main-container")
self.start_path = Path(DEFAULT_START_PATH) if DEFAULT_START_PATH else Path.cwd()
self.logger = Logger()
self.logger.info("MainView initialized", update_ui=False)
def flush(self):
def flush(self) -> None:
"""Required for stdout compatibility."""
pass
def compose(self):
def compose(self) -> ComposeResult:
self.logger.info("Composing main view layout", update_ui=False)
yield MenuBar()
@@ -87,25 +89,31 @@ class MainView(Horizontal):
yield RightPanel()
@on(DataTable.CellSelected)
def on_data_table_cell_selected(self, event):
def on_data_table_cell_selected(self, event: DataTable.CellSelected) -> None:
table = event.data_table
row_idx = event.coordinate.row
visible_data = table.get_row_at(row_idx)
description = (
table._df.iloc[row_idx].get("Description", "No description")
if hasattr(table, "_df")
else "N/A"
)
description = self._get_row_description(table, row_idx)
self.metric_description.text = (
f"Selected Metric ID: {visible_data[0]}\n"
f"Selected Metric: {visible_data[1]}\n"
f"Description: {description}"
)
if self.metric_description is not None:
self.metric_description.text = (
f"Selected Metric ID: {visible_data[0]}\n"
f"Selected Metric: {visible_data[1]}\n"
f"Description: {description}"
)
def _get_row_description(self, table: DataTable, row_idx: int) -> str:
"""Get description for a table row with safe attribute access."""
try:
if hasattr(table, "_df") and table._df is not None:
return str(table._df.iloc[row_idx].get("Description", "No description"))
except (IndexError, AttributeError, KeyError):
pass
return "N/A"
@work(thread=True)
def run_analysis(self):
def run_analysis(self) -> None:
self.kernel_to_df_dict = {}
self.top_kernel_to_df_list = []
@@ -158,12 +166,12 @@ class MainView(Horizontal):
self.logger.error(f"{error_msg}\n{traceback.format_exc()}")
self._update_kernel_view(error_msg, LogLevel.ERROR)
def _update_kernel_view(self, message, log_level):
def _update_kernel_view(self, message: str, log_level: LogLevel) -> None:
self.app.call_from_thread(
lambda: self.query_one("#kernel-view").update_view(message, log_level)
)
def refresh_results(self):
def refresh_results(self) -> None:
kernel_view = self.query_one("#kernel-view")
if kernel_view:
kernel_view.update_results(
@@ -173,7 +181,7 @@ class MainView(Horizontal):
else:
self.logger.error("Kernel view not found or no data available")
def refresh_view(self):
def refresh_view(self) -> None:
if self.kernel_to_df_dict and self.top_kernel_to_df_list:
self.refresh_results()
else:
@@ -28,6 +28,7 @@ Panel Widget Modules
Contains the panel widgets used in the main layout.
"""
from textual.app import ComposeResult
from textual.containers import Vertical
from textual.widgets import TabPane
@@ -44,13 +45,13 @@ class CenterPanel(Vertical):
"border-title-status",
}
def __init__(self):
def __init__(self) -> None:
super().__init__()
self.default_tab = "center-analyze"
self.kernel_view = KernelView()
def compose(self):
def compose(self) -> ComposeResult:
with TabsTabbedContent(initial="tab-kernel"):
with TabPane("Basic View", id="tab-kernel"):
yield self.kernel_view
@@ -29,16 +29,30 @@ import math
import sys
import traceback
from io import StringIO
from typing import Any, Optional
import pandas as pd
import plotext as plt
import plotly.express as px
import plotly.graph_objects as go
from textual.widgets import Static
from utils.mem_chart import plot_mem_chart
# Constants
MIN_PLOT_WIDTH = 20
WIDTH_MULTIPLIER_SMALL = 3
WIDTH_MULTIPLIER_TINY = 100
HEIGHT_MULTIPLIER_SMALL = 10
HEIGHT_MULTIPLIER_TINY = 300
WIDTH_THRESHOLD_SMALL = 20
WIDTH_THRESHOLD_TINY = 1
HEIGHT_THRESHOLD_SMALL = 20
HEIGHT_THRESHOLD_TINY = 0.5
DEFAULT_WIDTH_OFFSET = 40
def simple_bar(df, title=None):
def simple_bar(df: pd.DataFrame, title: Optional[str] = None) -> Optional[str]:
"""
Plot data with simple bar chart
"""
@@ -63,20 +77,20 @@ def simple_bar(df, title=None):
plt.clear_figure()
# adjust plot size along x axis based on the max value
w = max(list(metric_dict.values())) - 40
if w < 20 and w > 1:
w *= 3
elif w < 1:
w *= 100
w = max(list(metric_dict.values())) - DEFAULT_WIDTH_OFFSET
if w < WIDTH_THRESHOLD_SMALL and w > WIDTH_THRESHOLD_TINY:
w *= WIDTH_MULTIPLIER_SMALL
elif w < WIDTH_THRESHOLD_TINY:
w *= WIDTH_MULTIPLIER_TINY
plt.simple_bar(list(metric_dict.keys()), list(metric_dict.values()), width=w)
# plt.show()
plot_content = plt.build()
if not plot_content or plot_content.strip() == "":
return None
return "\n" + plot_content + "\n"
return f"\n{plot_content}\n"
def simple_multiple_bar(df, title=None):
def simple_multiple_bar(df: pd.DataFrame, title: Optional[str] = None) -> Optional[str]:
"""
Plot data with simple multiple bar chart
"""
@@ -92,51 +106,35 @@ def simple_multiple_bar(df, title=None):
data = t_df.transpose().to_dict("split")["data"]
labels = data.pop(0)
# plt.simple_multiple_bar(labels, data, labels = sub_labels) #, width=w)
# print(data)
plt.theme("pro")
# adjust plot size along y axis based on the max value
h = max(max(y) for y in data)
# print(h)
if h < 20 and h > 0.5:
h *= 10
elif h < 0.5 or math.isclose(h, 0.5):
h *= 300
if h < HEIGHT_THRESHOLD_SMALL and h > HEIGHT_THRESHOLD_TINY:
h *= HEIGHT_MULTIPLIER_SMALL
elif h < HEIGHT_THRESHOLD_TINY or math.isclose(h, HEIGHT_THRESHOLD_TINY):
h *= HEIGHT_MULTIPLIER_TINY
plt.plot_size(height=h)
plt.multiple_bar(labels, data)
# plt.show()
plot_content = plt.build()
if not plot_content or plot_content.strip() == "":
return None
return "\n" + plot_content + "\n"
return f"\n{plot_content}\n"
def simple_box(df, orientation="v", title=None):
def simple_box(
df: pd.DataFrame, orientation: str = "v", title: Optional[str] = None
) -> Optional[str]:
"""
Plot data with simple box/whisker chart.
Accept pre-calculated data only for now.
"""
# Example:
# labels = ["apple", "bee", "cat", "dog"]
# datas = [
# # max, q3, q2, q1, min
# [10, 7, 5, 3, 1.5],
# [19, 12.3, 9, 7, 4],
# [15, 14, 11, 9, 8],
# [13, 12, 11, 10, 6]]
# plt.box(labels, datas, width=0.1, hint='hint')
# plt.theme("pro")
# plt.title("Most Favored Pizzas in the World")
# plt.show()
plt.clear_figure()
labels = []
data = []
labels: list[str] = []
data: list[list[float]] = []
# TODO:
# handle Nan and None properly
@@ -147,7 +145,7 @@ def simple_box(df, orientation="v", title=None):
t_df = (
df.fillna(0).replace("", 0).replace(float("inf"), -1).replace(float("-inf"), -1)
)
for index, row in t_df.iterrows():
for _, row in t_df.iterrows():
column_name = row.get("Metric") or row.get("Channel")
if column_name is None:
@@ -159,14 +157,6 @@ def simple_box(df, orientation="v", title=None):
data.append([row["Max"], row["Q3"], row["Median"], row["Q1"], row["Min"]])
# TODO: need better fix for horizontal overflow
# labels_length *= 0.80
# print("~~~~~~~~~~~~~~~~~~~~")
# print(labels)
# print(labels_length)
# print(data)
# print("~~~~~~~~~~~~~~~~~~~~")
# print(plt.bar.__doc__)
if orientation == "v":
# adjust plot size along x axis based on total labels length
plt.plot_size(labels_length, 30)
@@ -180,14 +170,19 @@ def simple_box(df, orientation="v", title=None):
)
plt.theme("pro")
# plt.show()
plot_content = plt.build()
if not plot_content or plot_content.strip() == "":
return None
return "\n" + plot_content + "\n"
return f"\n{plot_content}\n"
def px_simple_bar(df, title: str = None, id=None, style: dict = None, orientation="h"):
def px_simple_bar(
df: pd.DataFrame,
title: Optional[str] = None,
id: Optional[int] = None,
style: Optional[dict[str, Any]] = None,
orientation: str = "h",
) -> go.Figure:
"""
Plot data with simple bar chart
"""
@@ -196,15 +191,15 @@ def px_simple_bar(df, title: str = None, id=None, style: dict = None, orientatio
if "Metric" in df.columns and ("Count" in df.columns or "Value" in df.columns):
detected_label = "Count" if "Count" in df.columns else "Value"
df[detected_label] = [
x.astype(int) if x != "" else int(0) for x in df[detected_label]
x.astype(int) if x != "" else 0 for x in df[detected_label]
]
else:
raise NameError("simple_bar: No Metric or Count in df columns!")
# Assign figure characteristics
range_color = style.get("range_color", None)
label_txt = style.get("label_txt", None)
xrange = style.get("xrange", None)
range_color = style.get("range_color", None) if style else None
label_txt = style.get("label_txt", None) if style else None
xrange = style.get("xrange", None) if style else None
if label_txt is not None:
label_txt = label_txt.strip("()")
try:
@@ -236,27 +231,29 @@ def px_simple_bar(df, title: str = None, id=None, style: dict = None, orientatio
return fig
def px_simple_multi_bar(df, title=None, id=None):
def px_simple_multi_bar(
df: pd.DataFrame, title: Optional[str] = None, id: Optional[int] = None
) -> list[go.Figure]:
"""
Plot data with simple multiple bar chart
"""
# TODO: handle Nan and None properly
if "Metric" in df.columns and "Avg" in df.columns:
df["Avg"] = [x.astype(int) if x != "" else int(0) for x in df["Avg"]]
df["Avg"] = [x.astype(int) if x != "" else 0 for x in df["Avg"]]
else:
raise NameError("simple_multi_bar: No Metric or Count in df columns!")
dfigs = []
nested_bar = {}
dfigs: list[go.Figure] = []
nested_bar: dict[str, dict[str, Any]] = {}
df_unit = df["Unit"][0]
if id == 1604:
nested_bar = {"NC": {}, "UC": {}, "RW": {}, "CC": {}}
for index, row in df.iterrows():
for _, row in df.iterrows():
nested_bar[row["Coherency"]][row["Xfer"]] = row["Avg"]
if id == 1704:
nested_bar = {"Read": {}, "Write": {}}
for index, row in df.iterrows():
for _, row in df.iterrows():
nested_bar[row["Transaction"]][row["Type"]] = row["Avg"]
for group, metric in nested_bar.items():
@@ -290,7 +287,7 @@ class RooflinePlot(Static):
}
"""
def __init__(self, df: pd.DataFrame, **kwargs):
def __init__(self, df: pd.DataFrame, **kwargs: Any) -> None:
"""Initialize the roofline plot"""
super().__init__("", classes="roofline", **kwargs)
self.df = df
@@ -319,7 +316,7 @@ class MemoryChart(Static):
}
"""
def __init__(self, df: pd.DataFrame, **kwargs):
def __init__(self, df: pd.DataFrame, **kwargs: Any) -> None:
super().__init__("", classes="mem-chart", **kwargs)
self.df = df
@@ -368,7 +365,7 @@ class SimpleBar(Static):
}
"""
def __init__(self, df: pd.DataFrame, **kwargs):
def __init__(self, df: pd.DataFrame, **kwargs: Any) -> None:
super().__init__("", classes="simple-bar", **kwargs)
self.df = df
@@ -401,7 +398,7 @@ class SimpleBox(Static):
}
"""
def __init__(self, df: pd.DataFrame, **kwargs):
def __init__(self, df: pd.DataFrame, **kwargs: Any) -> None:
super().__init__("", classes="simple-box", **kwargs)
self.df = df
@@ -436,7 +433,7 @@ class SimpleMultiBar(Static):
}
"""
def __init__(self, df: pd.DataFrame, **kwargs):
def __init__(self, df: pd.DataFrame, **kwargs: Any) -> None:
super().__init__("", classes="simple-multi-bar", **kwargs)
self.df = df
@@ -22,10 +22,11 @@
# THE SOFTWARE.
##############################################################################
from typing import Any, Optional, Union
import pandas as pd
import yaml
from textual.widgets import Collapsible, DataTable, Label
from textual.widgets import Collapsible, DataTable, Label, Static
from rocprof_compute_tui.widgets.charts import (
MemoryChart,
@@ -35,7 +36,9 @@ from rocprof_compute_tui.widgets.charts import (
)
def create_table(df, hidden_columns=[]):
def create_table(
df: pd.DataFrame, hidden_columns: Optional[list[str]] = []
) -> Union[DataTable, Label]:
table = DataTable(zebra_stripes=True)
df = df.reset_index().dropna()
@@ -44,8 +47,8 @@ def create_table(df, hidden_columns=[]):
if df.empty:
return Label("No table data generated")
table._df = df
table._visible_cols = [col for col in df.columns if col not in hidden_columns]
table._df = df # type: ignore[attr-defined]
table._visible_cols = [col for col in df.columns if col not in hidden_columns] # type: ignore[attr-defined]
table.add_columns(*table._visible_cols)
for _, row in df.iterrows():
@@ -54,7 +57,9 @@ def create_table(df, hidden_columns=[]):
return table
def create_widget_from_data(df, tui_style=None, context=""):
def create_widget_from_data(
df: Optional[pd.DataFrame], tui_style: Optional[str] = None, context: str = ""
) -> Union[Label, Static, DataTable]:
if df is None or df.empty:
return Label(
f"Data not available{f' for {context}' if context else ''}",
@@ -75,15 +80,17 @@ def create_widget_from_data(df, tui_style=None, context=""):
return Label(f"Unknown display type: {tui_style}")
def load_config(config_path):
with open(config_path, "r") as file:
def load_config(config_path: str) -> dict[str, Any]:
with open(config_path) as file:
return yaml.safe_load(file)
def build_section_from_config(dfs, section_config):
def build_section_from_config(
dfs: dict[str, Any], section_config: dict[str, Any]
) -> Collapsible:
title = section_config["title"]
collapsed = section_config.get("collapsed", True)
children = []
children: list[Collapsible] = []
for subsection_config in section_config["subsections"]:
subsection_title = subsection_config.get("title", "Untitled")
@@ -139,7 +146,7 @@ def build_section_from_config(dfs, section_config):
return Collapsible(*children, title=title, collapsed=collapsed)
def build_all_sections(dfs, config_path):
def build_all_sections(dfs: dict[str, Any], config_path: str) -> list[Collapsible]:
config = load_config(config_path)
return [
build_section_from_config(dfs, section_config)
@@ -22,8 +22,10 @@
# THE SOFTWARE.
##############################################################################
from typing import Any, Optional
from textual import on
from textual.app import ComposeResult
from textual.containers import Container, Horizontal
from textual.reactive import reactive
from textual.widgets import Button
@@ -32,7 +34,7 @@ from rocprof_compute_tui.widgets.recent_directories import RecentDirectoriesScre
class DropdownMenu(Container):
def compose(self):
def compose(self) -> ComposeResult:
"""Compose the dropdown menu with menu items."""
yield Button("Open Workload", id="menu-open-workload", classes="menu-item")
yield Button("Open Recent", id="menu-open-recent", classes="menu-item")
@@ -47,11 +49,11 @@ class DropdownMenu(Container):
class MenuButton(Button):
is_open = reactive(False)
def __init__(self, label, menu_id, *args, **kwargs):
def __init__(self, label: str, menu_id: str, *args: Any, **kwargs: Any) -> None:
super().__init__(label, *args, **kwargs)
self.menu_id = menu_id
def on_click(self):
def on_click(self) -> None:
self.is_open = not self.is_open
dropdown = self.app.query_one(f"#{self.menu_id}", DropdownMenu)
@@ -64,7 +66,7 @@ class MenuButton(Button):
class MenuBar(Container):
"""A menu bar that spans the width of the app."""
def compose(self):
def compose(self) -> ComposeResult:
yield Horizontal(
MenuButton("File", "file-dropdown", id="menu-file"), id="menu-buttons"
)
@@ -72,18 +74,18 @@ class MenuBar(Container):
with Container(id="dropdown-container"):
yield DropdownMenu(id="file-dropdown")
def on_mount(self):
def on_mount(self) -> None:
self.border_title = "MENU BAR"
self.add_class("section")
self.parent_main_view = self.screen.query_one("#main-container", Horizontal)
@on(Button.Pressed, "#menu-open-recent")
def show_recent(self):
def show_recent(self) -> None:
if not self.app.recent_dirs:
self.notify("No recent directories found", severity="warning")
return
def on_recent_selected(selected_dir):
def on_recent_selected(selected_dir: Optional[str]) -> None:
if selected_dir:
self.parent_main_view.selected_path = selected_dir
self.query_one("#file-dropdown", DropdownMenu).add_class("hidden")
@@ -94,5 +96,5 @@ class MenuBar(Container):
)
@on(Button.Pressed, "#menu-exit")
def exit_app(self):
def exit_app(self) -> None:
self.app.exit()
@@ -23,7 +23,6 @@
##############################################################################
from typing import List
from textual.app import ComposeResult
from textual.containers import Container, Horizontal
@@ -34,7 +33,7 @@ from textual.widgets import Button, Label, ListItem, ListView
class RecentDirectoriesScreen(ModalScreen):
"""Modal screen to display recent directories."""
def __init__(self, recent_dirs: List[str]) -> None:
def __init__(self, recent_dirs: list[str]) -> None:
super().__init__()
self.recent_dirs = recent_dirs
@@ -28,6 +28,7 @@ Panel Widget Modules
Contains the panel widgets used in the main layout.
"""
from textual.app import ComposeResult
from textual.containers import Vertical
from textual.widgets import Label
@@ -35,14 +36,14 @@ from textual.widgets import Label
class RightPanel(Vertical):
"""Right panel for additional tools."""
def __init__(self):
def __init__(self) -> None:
"""Initialize the right panel."""
super().__init__()
def compose(self):
def compose(self) -> ComposeResult:
"""Compose the right panel."""
yield Label("🚧 Under Construction")
def _on_mount(self):
def _on_mount(self) -> None:
self.border_title = "🚧 UNDER CONSTRUCTION"
self.add_class("section")
@@ -28,6 +28,7 @@ Panel Widget Modules
Contains the panel widgets used in the main layout.
"""
from textual.app import ComposeResult
from textual.containers import Vertical
from textual.widgets import TabPane, TextArea
@@ -44,7 +45,7 @@ class TabsArea(Vertical):
"border-title-status",
}
def __init__(self):
def __init__(self) -> None:
"""Initialize the bottom panel."""
super().__init__()
@@ -56,7 +57,7 @@ class TabsArea(Vertical):
# Set initial tab
self.default_tab = "tab-output"
def compose(self):
def compose(self) -> ComposeResult:
with TabsTabbedContent(initial="tab-output"):
with TabPane("METRIC DESCRIPTION", id="tab-description"):
yield (self.description_area)
@@ -41,23 +41,23 @@ class Terimnal(Container):
name: Optional[str] = None,
id: Optional[str] = None,
classes: Optional[str] = None,
):
) -> None:
super().__init__(name=name, id=id, classes=classes)
self.current_directory = os.getcwd()
self.output_text = ""
self.input_text = ""
self.input_prompt = ""
self.has_focus = True
self.output_text: str = ""
self.input_text: str = ""
self.input_prompt: str = ""
self.has_focus: bool = True
# Command history
self.command_history = []
self.history_index = -1
self.current_command = ""
self.command_history: list[str] = []
self.history_index: int = -1
self.current_command: str = ""
# Tab completion
self.tab_completions = []
self.tab_index = -1
self.tab_prefix = ""
self.tab_completions: list[str] = []
self.tab_index: int = -1
self.tab_prefix: str = ""
def compose(self) -> ComposeResult:
# Output area with scroll wrapper
@@ -94,8 +94,8 @@ class Terimnal(Container):
def add_output(self, text: str) -> None:
"""Add text to the terminal output."""
self.output_text += text
output = self.query_one("#terminal-output")
output.update(Text.from_ansi(self.output_text))
scroll = self.query_one("#term-output-scroll", VerticalScroll)
scroll.scroll_end(animate=False)
# Ensure scroll to bottom
scroll = self.query_one("#term-output-scroll")
@@ -104,7 +104,7 @@ class Terimnal(Container):
def action_clear(self) -> None:
"""Clear the terminal output."""
self.output_text = ""
output = self.query_one("#terminal-output")
output = self.query_one("#terminal-output", Static)
output.update(Text.from_ansi(""))
def action_interrupt(self) -> None:
@@ -125,7 +125,7 @@ class Terimnal(Container):
else:
# If no process is running, just show ^C and clear the input
self.add_output("\n^C\n")
self.query_one("#terminal-input").value = ""
self.query_one("#terminal-input", Input).value = ""
def run_command(self, command: str) -> None:
"""Run a system command and display its output."""
@@ -137,7 +137,7 @@ class Terimnal(Container):
self.history_index = len(self.command_history)
# Show the command in the output
prompt = self.query_one("#terminal-input").placeholder
prompt = self.query_one("#terminal-input", Input).placeholder
self.add_output(f"{prompt}{command}\n")
if not command.strip():
@@ -207,7 +207,7 @@ class Terimnal(Container):
"""Handle key events for history navigation."""
# Handle arrow keys for command history
if event.key == "up" and self.command_history:
input_widget = self.query_one("#terminal-input")
input_widget = self.query_one("#terminal-input", Input)
if self.history_index > 0:
self.history_index -= 1
input_widget.value = self.command_history[self.history_index]
@@ -215,7 +215,7 @@ class Terimnal(Container):
event.prevent_default()
elif event.key == "down" and self.command_history:
input_widget = self.query_one("#terminal-input")
input_widget = self.query_one("#terminal-input", Input)
if self.history_index < len(self.command_history) - 1:
self.history_index += 1
input_widget.value = self.command_history[self.history_index]
+243 -217
View File
@@ -23,12 +23,14 @@
##############################################################################
import argparse
import os
import textwrap
import time
from abc import abstractmethod
from collections import OrderedDict
from pathlib import Path
from typing import Any, Optional, Union
import numpy as np
import pandas as pd
@@ -36,7 +38,7 @@ import plotext as plt
import plotly.graph_objects as go
from dash import dcc, html
from utils import file_io, rocpd_data
from utils import file_io, rocpd_data, schema
from utils.logger import (
console_debug,
console_error,
@@ -50,13 +52,14 @@ from utils.roofline_calc import (
SUPPORTED_DATATYPES,
calc_ai_analyze,
calc_ai_profile,
constuct_roof,
construct_roof,
)
from utils.specs import MachineSpecs
SYMBOLS = [0, 1, 2, 3, 4, 5, 13, 17, 18, 20]
def wrap_text(text, width=92):
def wrap_text(text: str, width: int = 92) -> str:
"""
Wraps text using textwrap and joins lines with <br> for Plotly.
"""
@@ -68,8 +71,19 @@ def wrap_text(text, width=92):
return "<br>".join(wrapped_lines)
def to_int(value: Union[float, None]) -> Union[int, float]:
if value is None:
return np.nan
return int(value)
class Roofline:
def __init__(self, args, mspec, run_parameters=None):
def __init__(
self,
args: argparse.Namespace,
mspec: MachineSpecs,
run_parameters: Optional[dict[str, Any]] = None,
) -> None:
self.__args = args
self.__mspec = mspec
self.__run_parameters = (
@@ -87,9 +101,10 @@ class Roofline:
"kernel_filter": False,
}
)
self.__ai_data = None
self.__ceiling_data = None
self.__ai_data: Optional[dict[str, Any]] = None
self.__ceiling_data: Optional[dict[str, Any]] = None
self.__figure = go.Figure()
# Set roofline run parameters from args
if hasattr(self.__args, "path") and not run_parameters:
self.__run_parameters["workload_dir"] = self.__args.path
@@ -108,7 +123,10 @@ class Roofline:
self.__run_parameters["kernel_filter"] = True
self.validate_parameters()
def validate_parameters(self):
def get_args(self) -> argparse.Namespace:
return self.__args
def validate_parameters(self) -> None:
if self.__run_parameters["include_kernel_names"] and (
not self.__run_parameters["is_standalone"]
):
@@ -116,7 +134,7 @@ class Roofline:
"--kernel-names is nonactionable when used with --no-roof option"
)
def roof_setup(self):
def roof_setup(self) -> None:
# Setup the workload directory for roofline profiling.
workload_dir_val = self.__run_parameters.get("workload_dir")
@@ -171,53 +189,72 @@ class Roofline:
# Create the directory
Path(final_dir).mkdir(parents=True, exist_ok=True)
def validate_apply_kernel_filter(self, df, path=None):
if self.__run_parameters["kernel_filter"] is True:
if self.__args.mode == "profile":
df_pmc = df["pmc_perf"]
df_filtered = df_pmc.copy()
df_list = (df_pmc.loc[:, "Kernel_Name"]).to_list()
for idx in range(0, len(df_list)):
if df_list[idx].split("(")[0] not in self.__args.kernel:
# Drop row from dataframe if kernel has not been requested
df_filtered.drop(index=idx, inplace=True)
# Verify that final filtered kernel df matches the kernel list requested
if len(df_filtered.drop_duplicates(subset=["Kernel_Name"])) != len(
self.__args.kernel
):
console_debug(
"Profiled kernels: {}\n`--kernel`: {}".format(
df_list, self.__args.kernel
)
)
console_error(
"Roofline cannot profile - kernels requested with `--kernel` missing from profiling data!" # noqa: E501
"\n\tRe-profile workload in full or specify subset of available kernels using `--kernel` option." # noqa: E501
"\n\tComplete profiled kernels list can be found in pmc_perf file.", # noqa: E501
exit=True,
)
# Fix df structure to resemble same df arg passed in
df["pmc_perf"] = df_filtered
elif self.__args.mode == "analyze":
top_kernels_csv = Path(path).joinpath("pmc_kernel_top.csv")
if not top_kernels_csv.is_file():
console_error(
"roofline", "{} does not exist".format(top_kernels_csv)
)
k_df = pd.read_csv(top_kernels_csv)
k_df = k_df.loc[self.__args.gpu_kernel[0], "Kernel_Name"]
def apply_profile_kernel_filter(
self, df: dict[str, pd.DataFrame], args: argparse.Namespace
) -> dict[str, pd.DataFrame]:
"""Apply kernel filter for profile mode."""
df_pmc = df["pmc_perf"]
df_filtered = df_pmc.copy()
df_list = df_pmc["Kernel_Name"].tolist()
df["pmc_perf"] = df["pmc_perf"][
df["pmc_perf"]["Kernel_Name"].isin(k_df)
]
for idx in range(len(df_list)):
if df_list[idx].split("(")[0] not in args.kernel:
df_filtered.drop(index=idx, inplace=True)
# Verify that final filtered kernel df matches the kernel list requested
unique_kernels = len(df_filtered.drop_duplicates(subset=["Kernel_Name"]))
if unique_kernels != len(args.kernel):
console_debug(f"Profiled kernels: {df_list}\n`--kernel`: {args.kernel}")
console_error(
"Roofline cannot profile - kernels requested with `--kernel` missing "
"from profiling data!\n"
"\tRe-profile workload in full or specify subset of available kernels "
"using `--kernel` option.\n"
"\tComplete profiled kernels list can be found in pmc_perf file.",
exit=True,
)
df["pmc_perf"] = df_filtered
return df
def apply_analyze_kernel_filter(
self,
df: dict[str, pd.DataFrame],
path_str: Optional[str],
args: argparse.Namespace,
) -> dict[str, pd.DataFrame]:
"""Apply kernel filter for analyze mode."""
if not path_str:
console_error("roofline", "cannot locate pmc_kernel_top.csv")
top_kernels_csv = Path(path_str) / "pmc_kernel_top.csv"
if not top_kernels_csv.is_file():
console_error("roofline", f"{top_kernels_csv} does not exist")
k_df = pd.read_csv(top_kernels_csv)
k_df = k_df.loc[args.gpu_kernel[0], "Kernel_Name"]
df["pmc_perf"] = df["pmc_perf"][df["pmc_perf"]["Kernel_Name"].isin(k_df)]
return df
def validate_apply_kernel_filter(
self, df: dict[str, pd.DataFrame], path_str: Optional[str] = None
) -> dict[str, pd.DataFrame]:
if not self.__run_parameters["kernel_filter"]:
return df
args = self.get_args()
if args.mode == "profile":
return self.apply_profile_kernel_filter(df, args)
elif args.mode == "analyze":
return self.apply_analyze_kernel_filter(df, path_str, args)
return df
@demarcate
def empirical_roofline(
self,
ret_df,
):
self, ret_df: dict[str, pd.DataFrame]
) -> Optional[html.Section]:
"""
Generate a set of empirical roofline plots given a directory containing
required profiling and benchmarking data.
@@ -228,19 +265,20 @@ class Roofline:
):
self.roof_setup()
console_debug(
"roofline", "Path: %s" % self.__run_parameters.get("workload_dir")
)
console_debug("roofline", f"Path: {self.__run_parameters.get('workload_dir')}")
# Verify kernels have been profiled and filter the df
ret_df = self.validate_apply_kernel_filter(
df=ret_df, path=self.__run_parameters.get("workload_dir")
df=ret_df, path_str=self.__run_parameters.get("workload_dir")
)
self.__ai_data = calc_ai_profile(
self.__mspec, self.__run_parameters.get("sort_type"), ret_df
)
msg = "AI at each mem level:"
for i in self.__ai_data:
msg += "\n\t%s -> %s" % (i, self.__ai_data[i])
for key, value in self.__ai_data.items():
msg += f"\n\t{key} -> {value}"
console_debug(msg)
ops_figure = flops_figure = None
@@ -254,35 +292,27 @@ class Roofline:
or str(dt) not in SUPPORTED_DATATYPES[gpu_arch]
):
console_error(
"{} is not a supported datatype for roofline profiling on {} "
"(arch: {})".format(
str(dt),
getattr(self.__mspec, "gpu_model", "N/A"),
gpu_arch,
),
f"{dt} is not a supported datatype for roofline profiling on "
f"{getattr(self.__mspec, 'gpu_model', 'N/A')} (arch: {gpu_arch})",
exit=False,
)
continue
ops_flops = "Ops" if (str(dt[:1]) == "I") else "Flops"
ops_flops = "Ops" if str(dt).startswith("I") else "Flops"
if ops_flops == "Ops":
if ops_figure:
ops_combo_figure = self.generate_plot(
ops_figure = self.generate_plot(
dtype=str(dt),
fig=ops_figure,
)
ops_figure = ops_combo_figure
else:
ops_figure = self.generate_plot(dtype=str(dt))
ops_dt_list += "_" + str(dt)
if ops_flops == "Flops":
if flops_figure:
flops_combo_figure = self.generate_plot(
dtype=str(dt),
fig=flops_figure,
)
flops_figure = flops_combo_figure
flops_figure = self.generate_plot(dtype=str(dt), fig=flops_figure)
else:
flops_figure = self.generate_plot(dtype=str(dt))
flops_dt_list += "_" + str(dt)
@@ -299,11 +329,11 @@ class Roofline:
original_kernel_names = self.__ai_data.get("kernelNames", [])
num_kernels = len(original_kernel_names)
self.__figure.data = []
self.__figure.layout = {}
if num_kernels == 0:
# Create empty kernel names figure when no kernels are found
console_log(
"roofline",
"No kernel names found to generate "
@@ -328,15 +358,10 @@ class Roofline:
width=400,
)
else:
symbols_list = []
kernel_names_list = []
for i in range(num_kernels):
symbols_list.append(SYMBOLS[i % len(SYMBOLS)])
kernel_names_list.append(original_kernel_names[i])
# Create populated kernel names figure with symbols and names.
symbols_list = [SYMBOLS[i % len(SYMBOLS)] for i in range(num_kernels)]
self.__figure = go.Figure()
self.__figure.add_trace(
go.Scatter(
x=[0.1] * num_kernels,
@@ -353,7 +378,8 @@ class Roofline:
)
)
for i, kernel_name in enumerate(kernel_names_list):
# Add kernel name annotations
for i, kernel_name in enumerate(original_kernel_names):
self.__figure.add_annotation(
x=0.25,
y=num_kernels - i,
@@ -365,6 +391,7 @@ class Roofline:
font=dict(size=11, color="black"),
)
# Add formatting elements to kernel names figure.
self.__figure.add_annotation(
x=0.1,
y=num_kernels + 1,
@@ -384,6 +411,7 @@ class Roofline:
font=dict(size=12, color="black"),
)
# Add grid lines
for i in range(num_kernels + 1):
self.__figure.add_shape(
type="line",
@@ -427,51 +455,50 @@ class Roofline:
kernel_list += "_" + name
# Re-save to remove loading MathJax pop up
for i in range(2):
for _ in range(2):
if ops_figure:
ops_figure.write_image(
self.__run_parameters["workload_dir"]
+ "/empirRoof_gpu-{}{}{}.pdf".format(
dev_id, ops_dt_list, kernel_list
)
f"{self.__run_parameters['workload_dir']}/empirRoof_gpu-{dev_id}{ops_dt_list}{kernel_list}.pdf"
)
if flops_figure:
flops_figure.write_image(
self.__run_parameters["workload_dir"]
+ "/empirRoof_gpu-{}{}{}.pdf".format(
dev_id, flops_dt_list, kernel_list
)
f"{self.__run_parameters['workload_dir']}/empirRoof_gpu-{dev_id}{flops_dt_list}{kernel_list}.pdf"
)
# only save a legend if kernel_names option is toggled
if self.__run_parameters["include_kernel_names"]:
self.__figure.write_image(
self.__run_parameters["workload_dir"]
+ "/kernelName_legend{}.pdf".format(kernel_list)
f"{self.__run_parameters['workload_dir']}/kernelName_legend{kernel_list}.pdf"
)
time.sleep(1)
console_log("roofline", "Empirical Roofline PDFs saved!")
else:
if ops_figure:
ops_graph = html.Div(
# Create HTML output for GUI mode.
ops_graph = (
html.Div(
className="float-child",
children=[
html.H3(children="Empirical Roofline Analysis (Ops)"),
dcc.Graph(figure=ops_figure),
],
)
else:
ops_graph = None
if flops_figure:
flops_graph = html.Div(
if ops_figure
else None
)
flops_graph = (
html.Div(
className="float-child",
children=[
html.H3(children="Empirical Roofline Analysis (Flops)"),
dcc.Graph(figure=flops_figure),
],
)
else:
flops_graph = None
if flops_figure
else None
)
return html.Section(
id="roofline",
children=[
@@ -486,7 +513,7 @@ class Roofline:
)
@demarcate
def generate_plot(self, dtype, fig=None) -> go.Figure():
def generate_plot(self, dtype: str, fig: Optional[go.Figure] = None) -> go.Figure:
"""
Create graph object from ai_data (coordinate points) and ceiling_data
(peak FLOP and BW) data.
@@ -498,12 +525,14 @@ class Roofline:
skipAI = True # Don't repeat AI plotting
plot_mode = "lines+text" if self.__run_parameters["is_standalone"] else "lines"
self.__ceiling_data = constuct_roof(
self.__ceiling_data = construct_roof(
roofline_parameters=self.__run_parameters,
dtype=dtype,
)
console_debug("roofline", "Ceiling data:\n%s" % self.__ceiling_data)
ops_flops = "OP" if (dtype[:1] == "I") else "FLOP" # For printing purposes
console_debug("roofline", f"Ceiling data:\n{self.__ceiling_data}")
ops_flops = "OP" if dtype.startswith("I") else "FLOP" # For printing purposes
#######################
# Plot Application AI
@@ -576,17 +605,21 @@ class Roofline:
# Plot ceilings
#######################
mem_level_config = self.__run_parameters.get("mem_level", "ALL")
if mem_level_config == "ALL":
cache_hierarchy = ["HBM", "L2", "L1", "LDS"]
else:
cache_hierarchy = (
cache_hierarchy = (
["HBM", "L2", "L1", "LDS"]
if mem_level_config == "ALL"
else (
mem_level_config
if isinstance(mem_level_config, list)
else [mem_level_config]
)
)
# Plot peak BW ceiling(s)
for cache_level in cache_hierarchy:
cache_key = cache_level.lower()
if (
not self.__ceiling_data
or cache_level.lower() not in self.__ceiling_data
@@ -606,19 +639,15 @@ class Roofline:
go.Scatter(
x=self.__ceiling_data[cache_level.lower()][0],
y=self.__ceiling_data[cache_level.lower()][1],
name="{}-{}".format(cache_level, dtype),
name=f"{cache_level}-{dtype}",
mode=plot_mode,
hovertemplate="<b>%{text}</b>",
text=[
"{} GB/s".format(
to_int(self.__ceiling_data[cache_level.lower()][2])
),
f"{to_int(self.__ceiling_data[cache_key][2])} GB/s",
(
None
if self.__run_parameters.get("is_standalone")
else "{} GB/s".format(
to_int(self.__ceiling_data[cache_level.lower()][2])
)
else f"{to_int(self.__ceiling_data[cache_key][2])} GB/s"
),
],
textposition="top right",
@@ -631,20 +660,19 @@ class Roofline:
go.Scatter(
x=self.__ceiling_data["valu"][0],
y=self.__ceiling_data["valu"][1],
name="Peak VALU-{}".format(dtype),
name=f"Peak VALU-{dtype}",
mode=plot_mode,
hovertemplate="<b>%{text}</b>",
text=[
(
None
if self.__run_parameters["is_standalone"]
else "{} G{}/s".format(
to_int(self.__ceiling_data["valu"][2]), ops_flops
else (
f"{to_int(self.__ceiling_data['valu'][2])} G"
f"{ops_flops}/s"
)
),
"{} G{}/s".format(
to_int(self.__ceiling_data["valu"][2]), ops_flops
),
f"{to_int(self.__ceiling_data['valu'][2])} G{ops_flops}/s",
],
textposition="top left",
)
@@ -656,20 +684,19 @@ class Roofline:
go.Scatter(
x=self.__ceiling_data["mfma"][0],
y=self.__ceiling_data["mfma"][1],
name="Peak MFMA-{}".format(dtype),
name=f"Peak MFMA-{dtype}",
mode=plot_mode,
hovertemplate="<b>%{text}</b>",
text=[
(
None
if self.__run_parameters["is_standalone"]
else "{} G{}/s".format(
to_int(self.__ceiling_data["mfma"][2]), ops_flops
else (
f"{to_int(self.__ceiling_data['mfma'][2])} "
f"G{ops_flops}/s"
)
),
"{} G{}/s".format(
to_int(self.__ceiling_data["mfma"][2]), ops_flops
),
f"{to_int(self.__ceiling_data['mfma'][2])} G{ops_flops}/s",
],
textposition="top left",
)
@@ -680,7 +707,13 @@ class Roofline:
return fig
def cli_generate_plot(self, dtype, workload=None, config=None, arch_config=None):
def cli_generate_plot(
self,
dtype: str,
workload: Optional[schema.Workload] = None,
config: Optional[dict[str, Any]] = None,
arch_config: Optional[schema.ArchConfig] = None,
) -> Optional[str]:
"""
Plot CLI mode roofline analysis in terminal using plotext
@@ -692,11 +725,11 @@ class Roofline:
"""
console_debug("roofline", "Generating roofline plot for CLI")
if not (str(dtype) in SUPPORTED_DATATYPES[self.__mspec.gpu_arch]):
if not (str(dtype) in SUPPORTED_DATATYPES[str(self.__mspec.gpu_arch)]):
console_error(
"{} is not a supported datatype for roofline profiling on {}".format(
str(dtype), self.__mspec.gpu_model
),
f"{dtype} is not a supported datatype for roofline profiling on "
f"{getattr(self.__mspec, 'gpu_model', 'N/A')} (arch: "
f"{self.__mspec.gpu_arch})",
exit=False,
)
return
@@ -728,21 +761,20 @@ class Roofline:
else:
# workload_dir is a string
base_dir = workload_dir
# Convert to Path object for easier manipulation
base_path = Path(base_dir)
base_path = Path(base_dir)
roofline_csv = base_path / "roofline.csv"
if not roofline_csv.is_file():
console_log("roofline", "{} does not exist".format(roofline_csv))
console_log("roofline", f"{roofline_csv} does not exist")
return
# if workload is detected, utilize Roofline yamls.
# If not, fallback to legacy calc_ai
if workload is not None:
if workload and config and arch_config:
self.__ai_data = calc_ai_analyze(
workload=workload,
mspec=self.__mspec,
sort_type=self.__run_parameters.get("sort_type"),
sort_type=str(self.__run_parameters.get("sort_type")),
config=config,
arch_config=arch_config,
)
@@ -750,21 +782,24 @@ class Roofline:
else:
pmc_perf_csv = base_path / "pmc_perf.csv"
if not pmc_perf_csv.is_file():
console_error("roofline", "{} does not exist".format(pmc_perf_csv))
console_error("roofline", f"{pmc_perf_csv} does not exist")
t_df = OrderedDict()
t_df["pmc_perf"] = pd.read_csv(pmc_perf_csv)
profiling_config = file_io.load_profiling_config(self.__args.path[0][0])
if profiling_config.get("format_rocprof_output") == "rocpd":
t_df["pmc_perf"] = rocpd_data.process_rocpd_csv(t_df["pmc_perf"])
t_df = self.validate_apply_kernel_filter(df=t_df, path=base_path)
t_df = self.validate_apply_kernel_filter(df=t_df, path_str=str(base_path))
self.__ai_data = calc_ai_profile(
self.__mspec, self.__run_parameters["sort_type"], t_df
)
self.__ceiling_data = constuct_roof(
self.__ceiling_data = construct_roof(
roofline_parameters=self.__run_parameters, dtype=dtype
)
console_debug(f"AI data: {self.__ai_data}")
console_debug(f"Kernel names: {self.__ai_data.get('kernelNames', [])}")
@@ -800,40 +835,39 @@ class Roofline:
plt.clf()
plt.plotsize(plt.tw(), plt.th())
ops_flops = "OP" if (dtype[:1] == "I") else "FLOP" # For printing purposes
ops_flops = "OP" if dtype.startswith("I") else "FLOP"
# Plot BW Lines
if self.__run_parameters["mem_level"] == "ALL":
cache_hierarchy = ["HBM", "L2", "L1", "LDS"]
else:
cache_hierarchy = self.__run_parameters["mem_level"]
# Plot bandwidth lines
cache_hierarchy = (
["HBM", "L2", "L1", "LDS"]
if self.__run_parameters["mem_level"] == "ALL"
else self.__run_parameters["mem_level"]
)
for cache_level in cache_hierarchy:
cache_key = cache_level.lower()
plt.plot(
self.__ceiling_data[cache_level.lower()][0],
self.__ceiling_data[cache_level.lower()][1],
label="{}-{}".format(cache_level, dtype),
self.__ceiling_data[cache_key][0],
self.__ceiling_data[cache_key][1],
label=f"{cache_level}-{dtype}",
marker="braille",
color=color_scheme[cache_level],
)
plt.text(
str(round(self.__ceiling_data[cache_level.lower()][2])) + " GB/s",
x=self.__ceiling_data[cache_level.lower()][0][0],
y=self.__ceiling_data[cache_level.lower()][1][0],
f"{round(self.__ceiling_data[cache_key][2])} GB/s",
x=self.__ceiling_data[cache_key][0][0],
y=self.__ceiling_data[cache_key][1][0],
background="black",
color="white",
alignment="left",
)
console_debug(
"roofline",
cache_level
+ ": [{},{}], [{},{}], {}".format(
str(self.__ceiling_data[cache_level.lower()][0][0]),
str(self.__ceiling_data[cache_level.lower()][0][1]),
str(self.__ceiling_data[cache_level.lower()][1][0]),
str(self.__ceiling_data[cache_level.lower()][1][1]),
str(self.__ceiling_data[cache_level.lower()][2]),
),
f"{cache_level}: [{self.__ceiling_data[cache_key][0][0]},"
f"{self.__ceiling_data[cache_key][0][1]}], "
f"[{self.__ceiling_data[cache_key][1][0]},"
f"{self.__ceiling_data[cache_key][1][1]}], "
f"{self.__ceiling_data[cache_key][2]}",
)
# Plot VALU and MFMA Peak
@@ -844,12 +878,12 @@ class Roofline:
self.__ceiling_data["valu"][1][0] - 0.1,
self.__ceiling_data["valu"][1][1] - 0.1,
],
label="Peak VALU-{}".format(dtype),
label=f"Peak VALU-{dtype}",
marker="braille",
color=color_scheme["VALU"],
)
plt.text(
str(round(self.__ceiling_data["valu"][2])) + " G{}/s".format(ops_flops),
f"{round(self.__ceiling_data['valu'][2])} G{ops_flops}/s",
x=self.__ceiling_data["valu"][0][1] - 800,
y=self.__ceiling_data["valu"][1][1],
background="black",
@@ -858,16 +892,14 @@ class Roofline:
)
console_debug(
"roofline",
"VALU: [{},{}], [{},{}], {}".format(
str(self.__ceiling_data["valu"][0][0]),
str(self.__ceiling_data["valu"][0][1]),
str(self.__ceiling_data["valu"][1][0]),
str(self.__ceiling_data["valu"][1][1]),
str(self.__ceiling_data["valu"][2]),
),
f"VALU: [{self.__ceiling_data['valu'][0][0]},"
f"{self.__ceiling_data['valu'][0][1]}], "
f"[{self.__ceiling_data['valu'][1][0]},"
f"{self.__ceiling_data['valu'][1][1]}], "
f"{self.__ceiling_data['valu'][2]}",
)
else:
console_warning("No PEAK measurement available for {}".format(dtype))
console_warning(f"No PEAK measurement available for {dtype}")
if dtype in MFMA_DATATYPES:
plt.plot(
@@ -876,12 +908,12 @@ class Roofline:
self.__ceiling_data["mfma"][1][0] - 0.1,
self.__ceiling_data["mfma"][1][1] - 0.1,
],
label="Peak MFMA-{}".format(dtype),
label=f"Peak MFMA-{dtype}",
marker="braille",
color=color_scheme["MFMA"],
)
plt.text(
str(round(self.__ceiling_data["mfma"][2])) + " G{}/s".format(ops_flops),
f"{round(self.__ceiling_data['mfma'][2])} G{ops_flops}/s",
x=self.__ceiling_data["mfma"][0][1] - 800,
y=self.__ceiling_data["mfma"][1][1],
background="black",
@@ -890,45 +922,46 @@ class Roofline:
)
console_debug(
"roofline",
"MFMA: [{},{}], [{},{}], {}".format(
str(self.__ceiling_data["mfma"][0][0]),
str(self.__ceiling_data["mfma"][0][1]),
str(self.__ceiling_data["mfma"][1][0]),
str(self.__ceiling_data["mfma"][1][1]),
str(self.__ceiling_data["mfma"][2]),
),
f"MFMA: [{self.__ceiling_data['mfma'][0][0]},"
f"{self.__ceiling_data['mfma'][0][1]}], "
f"[{self.__ceiling_data['mfma'][1][0]},"
f"{self.__ceiling_data['mfma'][1][1]}], "
f"{self.__ceiling_data['mfma'][2]}",
)
else:
console_warning("No MFMA measurement available for {}".format(dtype))
console_warning(f"No MFMA measurement available for {dtype}")
# Plot Application AI
for cache_level in cache_hierarchy:
key = "ai_" + cache_level.lower()
if key in self.__ai_data:
for i in range(len(self.__ai_data["kernelNames"])):
# Zero intensity level means no data reported for this cache level
if self.__ai_data[key][0][i] > 0 and self.__ai_data[key][1][i] > 0:
plt.plot(
[self.__ai_data[key][0][i]],
[self.__ai_data[key][1][i]],
label="AI_"
+ cache_level
+ "_{}".format(self.__ai_data["kernelNames"][i]),
color=color_scheme[cache_level],
marker=kernel_markers[i % len(kernel_markers)],
)
console_debug(
"roofline",
"AI_{}: {}, {}".format(
self.__ai_data["kernelNames"][i],
self.__ai_data[key][0][i],
self.__ai_data[key][1][i],
),
)
key = f"ai_{cache_level.lower()}"
if key not in self.__ai_data:
continue
plt.xlabel("Arithmetic Intensity ({})s/Byte)".format(ops_flops))
kernel_names = self.__ai_data.get("kernelNames", [])
for i in range(len(self.__ai_data.get("kernelNames", []))):
# Zero intensity level means no data reported for this cache level
if self.__ai_data[key][0][i] > 0 and self.__ai_data[key][1][i] > 0:
plt.plot(
[self.__ai_data[key][0][i]],
[self.__ai_data[key][1][i]],
label=f"AI_{cache_level}_{kernel_names[i]}",
color=color_scheme[cache_level],
marker=kernel_markers[i % len(kernel_markers)],
)
val1 = (
self.__ai_data[key][0][i]
if i < len(self.__ai_data[key][0])
else "N/A"
)
val2 = (
self.__ai_data[key][1][i]
if i < len(self.__ai_data[key][1])
else "N/A"
)
console_debug("roofline", f"AI_{kernel_names[i]}: {val1}, {val2}")
plt.xlabel(f"Arithmetic Intensity ({ops_flops}s/Byte)")
plt.ylabel("Performance (GFLOP/sec)")
plt.title("Roofline ({}) - {}".format(dtype, base_path))
plt.title(f"Roofline ({dtype}) - {base_path}")
# Canvas config
plt.theme("pro")
@@ -940,7 +973,7 @@ class Roofline:
return plt.build()
@demarcate
def standalone_roofline(self):
def standalone_roofline(self) -> None:
if (
not isinstance(self.__run_parameters["workload_dir"], list)
and self.__run_parameters["workload_dir"] != None
@@ -952,33 +985,26 @@ class Roofline:
self.__run_parameters["mem_level"].remove("vL1D")
self.__run_parameters["mem_level"].append("L1")
app_path = str(
Path(self.__run_parameters["workload_dir"]).joinpath("pmc_perf.csv")
)
roofline_exists = Path(app_path).is_file()
if not roofline_exists:
console_error("roofline", "{} does not exist".format(app_path))
app_path = Path(str(self.__run_parameters["workload_dir"])) / "pmc_perf.csv"
if not app_path.is_file():
console_error("roofline", f"{app_path} does not exist")
t_df = OrderedDict()
t_df["pmc_perf"] = pd.read_csv(app_path)
profiling_config = file_io.load_profiling_config(self.__args.path)
if profiling_config.get("format_rocprof_output") == "rocpd":
t_df["pmc_perf"] = rocpd_data.process_rocpd_csv(t_df["pmc_perf"])
self.empirical_roofline(ret_df=t_df)
# NB: Currently the post_prossesing() method is the only one being used by
# rocprofiler-compute, we include pre_processing() and profile() methods for
# those who wish to borrow the roofline module
@abstractmethod
def post_processing(self):
def post_processing(self) -> None:
if self.__run_parameters["is_standalone"]:
self.standalone_roofline()
def get_dtype(self):
def get_dtype(self) -> list[str]:
return self.__run_parameters["roofline_data_type"]
def to_int(a):
if str(type(a)) == "<class 'NoneType'>":
return np.nan
else:
return int(a)
@@ -21,6 +21,7 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
##############################################################################el
from typing import Any, Optional
from sqlalchemy import (
JSON,
@@ -30,21 +31,25 @@ from sqlalchemy import (
Integer,
String,
Text,
TextClause,
create_engine,
func,
select,
text,
)
from sqlalchemy.orm import declarative_base, relationship, sessionmaker
from sqlalchemy.engine import Engine
from sqlalchemy.orm import Session, declarative_base, relationship, sessionmaker
from sqlalchemy.sql import Select
from utils.logger import console_debug, console_error
Base = declarative_base()
PREFIX = "compute_"
SCHEMA_VERSION = "1.0.0"
Base = declarative_base()
class Workload(Base):
__tablename__ = f"{PREFIX}workload"
@@ -162,33 +167,38 @@ class Metadata(Base):
class Database:
_session = None
_session: Optional[Session] = None
_engine: Optional[Engine] = None
@classmethod
def init(cls, db_name):
engine = create_engine(f"sqlite:///{db_name}")
Base.metadata.create_all(engine)
cls._session = sessionmaker(bind=engine)()
def init(cls, db_name: str) -> str:
cls._engine = create_engine(f"sqlite:///{db_name}")
Base.metadata.create_all(cls._engine)
cls._session = sessionmaker(bind=cls._engine)()
console_debug(f"SQLite database initialized with name: {db_name}")
return db_name
@classmethod
def get_session(cls):
def get_session(cls) -> Optional[Session]:
return cls._session
@classmethod
def write(self):
def write(cls) -> None:
if cls._session is None:
console_error("No active database session")
try:
self._session.commit()
cls._session.commit()
except Exception as e:
self._session.rollback()
cls._session.rollback()
console_error(f"Error writing analysis database: {e}")
finally:
self._session.close()
cls._session.close()
cls._session = None
def get_views():
views = {
def get_views() -> list[TextClause]:
views: dict[str, Select[Any]] = {
"kernel_view": select(
Dispatch.kernel_name,
func.count(Dispatch.dispatch_id).label("dispatch_count"),
@@ -207,6 +217,7 @@ def get_views():
Value.value,
).join(Value, Metric.metric_uuid == Value.metric_uuid),
}
return [
text(
f"CREATE VIEW {PREFIX}{view_name} AS "
+184 -175
View File
@@ -27,6 +27,7 @@ import os
import re
from collections import OrderedDict
from pathlib import Path
from typing import Any, Optional
import pandas as pd
import yaml
@@ -39,90 +40,71 @@ 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
# the build-in config to list kernel names purpose only
top_stats_build_in_config = {
0: {
"id": 0,
"title": "Top Kernels",
"data source": [{"raw_csv_table": {"id": 1, "source": "pmc_kernel_top.csv"}}],
},
1: {
"id": 1,
"title": "Dispatch List",
"data source": [
{"raw_csv_table": {"id": 2, "source": "pmc_dispatch_info.csv"}}
],
},
}
def load_sys_info(f):
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):
def load_panel_configs(
dirs: list[str],
) -> OrderedDict[int, dict[str, Any]]:
"""
Load all panel configs from yaml file.
"""
d = {}
for dir in dirs:
for root, _, files in os.walk(dir):
for f in files:
if f.endswith(".yaml"):
with open(Path(root) / f) as file:
configs: dict[int, dict[str, Any]] = {}
for dir_path in dirs:
for root, _, files in os.walk(dir_path):
for file_name in files:
if file_name.endswith(".yaml"):
with open(Path(root) / file_name) 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
for data_source in config_yml["Panel Config"]["data source"]:
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"] = {}
d[config_yml["Panel Config"]["id"]] = config_yml["Panel Config"]
configs[panel_config["id"]] = panel_config
# TODO: sort metrics as the header order in case they-
# are not defined in the same order
od = OrderedDict(sorted(d.items()))
# for key, value in od.items():
# print(key, value)
return od
return OrderedDict(sorted(configs.items()))
def load_profiling_config(config_dir):
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(Path(config_dir).joinpath("profiling_config.yaml")) as file:
prof_config = yaml.safe_load(file)
return prof_config
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 dict()
return {}
@demarcate
def create_df_kernel_top_stats(
df_in,
raw_data_dir,
filter_gpu_ids,
filter_dispatch_ids,
filter_nodes,
time_unit,
max_stat_num,
kernel_verbose,
sortby="sum",
):
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.
"""
# NB: think about df = pd.DataFrame(df_in["pmc_perf"].copy())
df = df_in["pmc_perf"]
df = df_in["pmc_perf"].copy()
# Demangle original KernelNames
kernel_name_shortener(df, kernel_verbose)
@@ -139,87 +121,105 @@ def create_df_kernel_top_stats(
if filter_dispatch_ids:
# NB: support ignoring the 1st n dispatched execution by '> n'
# The better way may be parsing python slice string
if ">" in filter_dispatch_ids[0]:
m = re.match(r"\> (\d+)", filter_dispatch_ids[0])
df = df[df["Dispatch_ID"] > int(m.group(1))]
first_filter = filter_dispatch_ids[0]
if first_filter.startswith(">"):
match = re.match(r">\s*(\d+)", first_filter)
if match:
threshold = int(match.group(1))
df = df[df["Dispatch_ID"] > threshold]
else:
df = df.loc[df["Dispatch_ID"].astype(str).isin(filter_dispatch_ids)]
# First, create a dispatches file used to populate global vars
dispatch_info = (
df.loc[:, ["Node", "Dispatch_ID", "Kernel_Name", "GPU_ID"]]
dispatch_columns = (
["Node", "Dispatch_ID", "Kernel_Name", "GPU_ID"]
if "Node" in df.columns
else df.loc[:, ["Dispatch_ID", "Kernel_Name", "GPU_ID"]]
)
dispatch_info.to_csv(
str(Path(raw_data_dir).joinpath("pmc_dispatch_info.csv")), index=False
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)
time_stats = pd.concat(
[df["Kernel_Name"], (df["End_Timestamp"] - df["Start_Timestamp"])],
keys=["Kernel_Name", "ExeTime"],
axis=1,
)
grouped = time_stats.groupby(by=["Kernel_Name"]).agg({
"ExeTime": ["count", "sum", "mean", "median"]
# Calculate execution times
execution_times = df["End_Timestamp"] - df["Start_Timestamp"]
time_stats = pd.DataFrame({
"Kernel_Name": df["Kernel_Name"],
"ExeTime": execution_times,
})
time_unit_str = "(" + time_unit + ")"
grouped.columns = [
x.capitalize() + time_unit_str if x != "count" else x.capitalize()
for x in grouped.columns.get_level_values(1)
]
grouped = time_stats.groupby("Kernel_Name")["ExeTime"].agg([
"count",
"sum",
"mean",
"median",
])
key = "Sum" + time_unit_str
grouped[key] = grouped[key].div(config.TIME_UNITS[time_unit])
key = "Mean" + time_unit_str
grouped[key] = grouped[key].div(config.TIME_UNITS[time_unit])
key = "Median" + time_unit_str
grouped[key] = grouped[key].div(config.TIME_UNITS[time_unit])
# 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)
grouped = grouped.reset_index() # Remove special group indexing
# 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
key = "Sum" + time_unit_str
grouped["Pct"] = grouped[key] / grouped[key].sum() * 100
grouped = grouped.reset_index()
# Calculate percentage
sum_column = f"Sum{time_unit_suffix}"
grouped["Pct"] = grouped[sum_column] / grouped[sum_column].sum() * 100
# NB:
# Sort by total time as default.
if sortby == "sum":
grouped = grouped.sort_values(by=("Sum" + time_unit_str), ascending=False)
grouped.to_csv(
str(Path(raw_data_dir).joinpath("pmc_kernel_top.csv")), index=False
)
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).joinpath("pmc_kernel_top.csv")), index=False
)
grouped.to_csv(str(Path(raw_data_dir) / "pmc_kernel_top.csv"), index=False)
@demarcate
def create_df_pmc(
raw_data_root_dir, nodes, spatial_multiplexing, kernel_verbose, verbose, config
):
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, node_name, kernel_verbose, verbose):
dfs = []
coll_levels = []
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] = []
df = pd.DataFrame() # noqa: F841
new_df = pd.DataFrame() # noqa: F841
for root, dirs, files in os.walk(raw_data_dir):
for f in files:
# print("file ", f)
if (f.endswith(".csv") and f.startswith("SQ")) or (
f == schema.pmc_perf_file_prefix + ".csv"
):
tmp_df = pd.read_csv(str(Path(root).joinpath(f)))
if config.get("format_rocprof_output") == "rocpd":
for root, _, files in os.walk(raw_data_dir):
for file_name in files:
# Process SQ*.csv or pmc_perf.csv files
is_sq_file = file_name.endswith(".csv") and file_name.startswith("SQ")
is_pmc_perf = file_name == f"{schema.PMC_PERF_FILE_PREFIX}.csv"
if is_sq_file or is_pmc_perf:
file_path = Path(root) / file_name
tmp_df = pd.read_csv(file_path)
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)
@@ -228,117 +228,126 @@ def create_df_pmc(
# multiindexing level. Here, we add it into pmc_perf
# as it is the main sub-df which can be handled easily
# later.
if f == "pmc_perf.csv" and node_name != None:
if file_name == "pmc_perf.csv" and node_name is not None:
tmp_df.insert(0, "Node", node_name)
dfs.append(tmp_df)
coll_levels.append(f[:-4])
# Remove .csv extension for collection level
coll_levels.append(file_name[:-4])
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("pmc_raw_data final_single_df %s" % final_df.info)
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:
df = pd.DataFrame()
# todo: more err check
for subdir in Path(raw_data_root_dir).iterdir():
dfs: list[pd.DataFrame] = []
for subdir in root_path.iterdir():
if subdir.is_dir():
new_df = create_single_df_pmc(
subdir, str(subdir.name), kernel_verbose, verbose
str(subdir), str(subdir.name), kernel_verbose, verbose
)
df = pd.concat([df, new_df])
return df
if not new_df.empty:
dfs.append(new_df)
return pd.concat(dfs, ignore_index=True) if dfs else pd.DataFrame()
# specified node list
else:
# regular single node case
if nodes is None:
return create_single_df_pmc(
raw_data_root_dir, None, kernel_verbose, verbose
)
# 2. regular single node case (nodes=None)
if nodes is None:
return create_single_df_pmc(raw_data_root_dir, None, kernel_verbose, verbose)
# "empty list" means all nodes
elif not nodes:
df = pd.DataFrame()
# todo: more err check
for subdir in Path(raw_data_root_dir).iterdir():
if subdir.is_dir():
new_df = create_single_df_pmc(
subdir, str(subdir.name), kernel_verbose, verbose
)
df = pd.concat([df, new_df])
return df
# 3. all nodes case (nodes=[])
if not nodes:
dfs: list[pd.DataFrame] = []
# specified node list
else:
df = pd.DataFrame()
# todo: more err check
for subdir in nodes:
p = Path(raw_data_root_dir)
for subdir in root_path.iterdir():
if subdir.is_dir():
new_df = create_single_df_pmc(
p.joinpath(subdir), subdir, kernel_verbose, verbose
str(subdir), str(subdir.name), kernel_verbose, verbose
)
df = pd.concat([df, new_df])
return df
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, out_dir, numSE):
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()
all = pd.DataFrame()
for i in range(num_se):
file_path = in_path / f"wave_occu_se{i}.csv"
if not file_path.exists():
continue
for i in range(numSE):
p = Path(in_dir, "wave_occu_se" + str(i) + ".csv")
if p.exists():
tmp_df = pd.read_csv(p)
SE_idx = "SE" + str(tmp_df.loc[0, "SE"])
tmp_df.rename(
columns={
"Dispatch": "Dispatch",
"SE": "SE",
"CU": "CU",
"Occupancy": SE_idx,
},
inplace=True,
)
tmp_df = pd.read_csv(file_path)
if tmp_df.empty:
continue
# TODO: join instead of concat!
if i == 0:
all = tmp_df[{"CU", SE_idx}]
all.sort_index(axis=1, inplace=True)
else:
all = pd.concat([all, tmp_df[SE_idx]], axis=1, copy=False)
se_idx = f"SE{tmp_df.loc[0, 'SE']}"
tmp_df.rename(
columns={
"Dispatch": "Dispatch",
"SE": "SE",
"CU": "CU",
"Occupancy": se_idx,
}
)
if not all.empty:
# print(all.transpose())
all.to_csv(Path(out_dir, "wave_occu_per_cu.csv"), index=False)
# 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, supported_archs):
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
supported_archs = supported_archs.keys()
counter = 0
for arch in supported_archs:
if root_dir.joinpath(arch).exists():
counter += 1
if counter == 0:
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 counter == len(supported_archs):
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):
def find_1st_sub_dir(directory: str) -> Optional[str]:
"""
Find the first sub dir in a directory
"""
@@ -347,7 +356,7 @@ def find_1st_sub_dir(directory):
# Iterate over entries in the directory
for entry in dir_path.iterdir():
if entry.is_dir(): # Check if it's a directory
return entry
return str(entry)
return None
except FileNotFoundError:
print(f"The directory '{directory}' does not exist.")
return None
console_error(f'The directory "{directory}" does not exist.', exit=False)
+235 -220
View File
@@ -23,10 +23,12 @@
##############################################################################
import colorlover
from typing import Any
import colorlover # type: ignore
import pandas as pd
import plotly.express as px
from dash import dash_table, html
import plotly.express as px # type: ignore
from dash import dash_table, html # type: ignore
from utils import schema
from utils.logger import console_error
@@ -41,73 +43,79 @@ IS_DARK = True # TODO: Remove hardcoded in favor of class property
##################
# HELPER FUNCTIONS
##################
def filter_df(column, df, filt):
filt_df = df
if filt != []:
filt_df = df.loc[df[schema.pmc_perf_file_prefix][column].astype(str).isin(filt)]
return filt_df
def filter_df(column: str, df: pd.DataFrame, filt: list[str]) -> pd.DataFrame:
if not filt:
return df
return df.loc[df[schema.PMC_PERF_FILE_PREFIX][column].astype(str).isin(filt)]
def multi_bar_chart(table_id, display_df):
def multi_bar_chart(
table_id: int, display_df: pd.DataFrame
) -> dict[str, dict[str, Any]]:
nested_bar: dict[str, dict[str, Any]] = {}
if table_id == 1604:
nested_bar = {}
for index, row in display_df.iterrows():
if not row["Coherency"] in nested_bar:
nested_bar[row["Coherency"]] = {}
nested_bar[row["Coherency"]][row["Xfer"]] = row["Avg"]
if table_id == 1705: # L2 - Fabric Interface Stalls
nested_bar = {}
for index, row in display_df.iterrows():
if not row["Transaction"] in nested_bar:
nested_bar[row["Transaction"]] = {}
nested_bar[row["Transaction"]][row["Type"]] = row["Avg"]
for _, row in display_df.iterrows():
coherency = row["Coherency"]
if coherency not in nested_bar:
nested_bar[coherency] = {}
nested_bar[coherency][row["Xfer"]] = row["Avg"]
elif table_id == 1705: # L2 - Fabric Interface Stalls
for _, row in display_df.iterrows():
transaction = row["Transaction"]
if transaction not in nested_bar:
nested_bar[transaction] = {}
nested_bar[transaction][row["Type"]] = row["Avg"]
return nested_bar
def discrete_background_color_bins(df, n_bins=5, columns="all"):
def discrete_background_color_bins(
df: pd.DataFrame, n_bins: int = 5, columns: str | list[str] = "all"
) -> tuple[list[dict[str, Any]], html.Div]:
bounds = [i * (1.0 / n_bins) for i in range(n_bins + 1)]
if columns == "all":
if "id" in df:
df_numeric_columns = df.select_dtypes("number").drop(["id"], axis=1)
else:
df_numeric_columns = df.select_dtypes("number")
df_numeric_columns = (
df.select_dtypes("number").drop(["id"], axis=1)
if "id" in df.columns
else df.select_dtypes("number")
)
else:
df_numeric_columns = df[columns]
df_max = df_numeric_columns.max().max()
df_min = df_numeric_columns.min().min()
ranges = [((df_max - df_min) * i) + df_min for i in bounds]
styles = []
legend = []
styles: list[dict[str, Any]] = []
legend: list[html.Div] = []
for i in range(1, len(bounds)):
min_bound = ranges[i - 1]
max_bound = ranges[i]
backgroundColor = colorlover.scales[str(n_bins)]["seq"]["Blues"][i - 1]
background_color = colorlover.scales[str(n_bins)]["seq"]["Blues"][i - 1]
color = "white" if i > len(bounds) / 2.0 else "inherit"
for column in df_numeric_columns:
for column in df_numeric_columns.columns:
filter_query = f"{{{column}}} >= {min_bound}" + (
f" && {{{column}}} < {max_bound}" if i < len(bounds) - 1 else ""
)
styles.append({
"if": {
"filter_query": (
"{{{column}}} >= {min_bound}"
+ (
" && {{{column}}} < {max_bound}"
if (i < len(bounds) - 1)
else ""
)
).format(column=column, min_bound=min_bound, max_bound=max_bound),
"filter_query": filter_query,
"column_id": column,
},
"backgroundColor": backgroundColor,
"backgroundColor": background_color,
"color": color,
})
legend.append(
html.Div(
style={"display": "inline-block", "width": "60px"},
children=[
html.Div(
style={
"backgroundColor": backgroundColor,
"backgroundColor": background_color,
"borderLeft": "1px rgb(50, 50, 50) solid",
"height": "10px",
}
@@ -117,112 +125,84 @@ def discrete_background_color_bins(df, n_bins=5, columns="all"):
)
)
return (styles, html.Div(legend, style={"padding": "5px 0 5px 0"}))
return styles, html.Div(legend, style={"padding": "5px 0 5px 0"})
####################
# GRAPHICAL ELEMENTS
####################
def build_bar_chart(display_df, table_config, barchart_elements, norm_filt):
"""
Read data into a bar chart. ID will determine which subtype of barchart.
"""
d_figs = []
def create_instruction_mix_bar_chart(display_df: pd.DataFrame, df_unit: str) -> px.bar:
display_df = display_df.copy()
display_df["Avg"] = display_df["Avg"].apply(lambda x: int(x) if x != "" else 0)
# Insr Mix bar chart
if table_config["id"] in barchart_elements["instr_mix"]:
display_df["Avg"] = [
x.astype(int) if x != "" else int(0) for x in display_df["Avg"]
]
df_unit = display_df["Unit"].iloc[0]
d_figs.append(
return px.bar(
display_df,
x="Avg",
y="Metric",
color="Avg",
labels={"Avg": f"# of {df_unit.lower()}"},
height=400,
orientation="h",
)
def create_multi_bar_charts(
display_df: pd.DataFrame, table_id: int, df_unit: str
) -> list[px.bar]:
display_df = display_df.copy()
display_df["Avg"] = display_df["Avg"].apply(lambda x: int(x) if x != "" else 0)
nested_bar = multi_bar_chart(table_id, display_df)
charts = []
for group, metric in nested_bar.items():
chart = px.bar(
title=group,
x=list(metric.values()),
y=list(metric.keys()),
labels={"x": df_unit, "y": ""},
text=list(metric.values()),
orientation="h",
height=200,
)
chart.update_xaxes(showgrid=False, rangemode="nonnegative")
chart.update_yaxes(showgrid=False)
chart.update_layout(title_x=0.5)
charts.append(chart)
return charts
def create_sol_charts(display_df: pd.DataFrame, table_id: int) -> list[px.bar]:
display_df = display_df.copy()
display_df["Avg"] = display_df["Avg"].apply(lambda x: float(x) if x != "" else 0.0)
charts = []
if table_id == 1701:
# Special layout for L2 Cache SOL
pct_data = display_df[display_df["Unit"] == "Pct"]
charts.append(
px.bar(
display_df,
pct_data,
x="Avg",
y="Metric",
color="Avg",
labels={"Avg": "# of {}".format(df_unit.lower())},
height=400,
range_color=[0, 100],
labels={"Avg": "%"},
height=220,
orientation="h",
)
).update_xaxes(range=[0, 110], ticks="inside", title="%")
)
# Multi bar chart
elif table_config["id"] in barchart_elements["multi_bar"]:
display_df["Avg"] = [
x.astype(int) if x != "" else int(0) for x in display_df["Avg"]
]
df_unit = display_df["Unit"].iloc[0]
nested_bar = multi_bar_chart(table_config["id"], display_df)
# generate chart for each coherency
for group, metric in nested_bar.items():
d_figs.append(
# HBM Bandwidth chart
hbm_row = display_df[display_df["Metric"] == "HBM Bandwidth"]
if not hbm_row.empty:
hbm_bw = float(hbm_row["Avg"].iloc[0])
gb_data = display_df[display_df["Unit"] == "Gb/s"]
charts.append(
px.bar(
title=group,
x=metric.values(),
y=metric.keys(),
labels={"x": df_unit, "y": ""},
text=metric.values(),
orientation="h",
height=200,
)
.update_xaxes(showgrid=False, rangemode="nonnegative")
.update_yaxes(showgrid=False)
.update_layout(title_x=0.5)
)
# L2 Cache per channel
# elif table_config["id"] in barchart_elements["l2_cache_per_chan"]:
# nested_bar = {}
# channels = []
# for colName, colData in display_df.items():
# if colName == "Channel":
# channels = list(colData.values)
# else:
# display_df[colName] = [
# x.astype(float) if x != "" and x != None else float(0)
# for x in display_df[colName]
# ]
# nested_bar[colName] = list(display_df[colName])
# for group, metric in nested_bar.items():
# d_figs.append(
# px.bar(
# title=group[0 : group.rfind("(")],
# x=channels,
# y=metric,
# labels={
# "x": "Channel",
# "y": group[group.rfind("(") + 1 : len(group) - 1].replace(
# "per", norm_filt
# ),
# },
# ).update_yaxes(rangemode="nonnegative")
# )
# Speed-of-light bar chart
elif table_config["id"] in barchart_elements["sol"]:
display_df["Avg"] = [
float(x) if x != "" else float(0) for x in display_df["Avg"]
]
if table_config["id"] == 1701:
# special layout for L2 Cache SOL
d_figs.append(
px.bar(
display_df[display_df["Unit"] == "Pct"],
x="Avg",
y="Metric",
color="Avg",
range_color=[0, 100],
labels={"Avg": "%"},
height=220,
orientation="h",
).update_xaxes(range=[0, 110], ticks="inside", title="%")
) # append first % chart
hbm_bw = float(
display_df[display_df["Metric"] == "HBM Bandwidth"]["Avg"].iloc[0]
)
d_figs.append(
px.bar(
display_df[display_df["Unit"] == "Gb/s"],
gb_data,
x="Avg",
y="Metric",
color="Avg",
@@ -231,88 +211,145 @@ def build_bar_chart(display_df, table_config, barchart_elements, norm_filt):
height=220,
orientation="h",
).update_xaxes(range=[0, hbm_bw])
) # append second GB/s chart
elif table_config["id"] == 1101:
# Special formatting reference 'Pct of Peak' value
display_df["Pct of Peak"] = [
x.astype(float) if x != "" else float(0)
for x in display_df["Pct of Peak"]
]
d_figs.append(
px.bar(
display_df,
x="Pct of Peak",
y="Metric",
color="Pct of Peak",
range_color=[0, 100],
labels={"Avg": "%"},
height=400,
orientation="h",
).update_xaxes(range=[0, 110])
)
else:
d_figs.append(
px.bar(
display_df,
x="Avg",
y="Metric",
color="Avg",
range_color=[0, 100],
labels={"Avg": "%"},
height=400,
orientation="h",
).update_xaxes(range=[0, 110])
)
elif table_id == 1101:
# Special formatting reference 'Pct of Peak' value
display_df["Pct of Peak"] = display_df["Pct of Peak"].apply(
lambda x: float(x) if x != "" else 0.0
)
charts.append(
px.bar(
display_df,
x="Pct of Peak",
y="Metric",
color="Pct of Peak",
range_color=[0, 100],
labels={"Avg": "%"},
height=400,
orientation="h",
).update_xaxes(range=[0, 110])
)
else:
console_error(
"Table id %s. Cannot determine barchart type." % table_config["id"]
charts.append(
px.bar(
display_df,
x="Avg",
y="Metric",
color="Avg",
range_color=[0, 100],
labels={"Avg": "%"},
height=400,
orientation="h",
).update_xaxes(range=[0, 110])
)
# update layout for each of the charts
for fig in d_figs:
return charts
def build_bar_chart(
display_df: pd.DataFrame,
table_config: dict[str, Any],
barchart_elements: dict[str, Any],
) -> list:
"""
Read data into a bar chart. ID will determine which subtype of barchart.
"""
table_id = table_config["id"]
charts: list[px.bar] = []
# Get unit from first row if available
df_unit = display_df["Unit"].iloc[0] if "Unit" in display_df.columns else ""
# Instruction Mix bar chart
if table_id in barchart_elements["instr_mix"]:
charts.append(create_instruction_mix_bar_chart(display_df, df_unit))
# Multi bar chart
elif table_id in barchart_elements["multi_bar"]:
charts.extend(create_multi_bar_charts(display_df, table_id, df_unit))
# Speed-of-light bar chart
elif table_id in barchart_elements["sol"]:
charts.extend(create_sol_charts(display_df, table_id))
else:
console_error(
f"Table id {table_id}. Cannot determine barchart type.", exit=False
)
return []
# Apply consistent styling to all charts
for fig in charts:
fig.update_layout(
margin=dict(l=50, r=50, b=50, t=50, pad=4),
paper_bgcolor="rgba(0,0,0,0)",
plot_bgcolor="rgba(0,0,0,0)",
font={"color": "#ffffff"},
)
return d_figs
return charts
def get_dark_mode_styles() -> tuple[
dict[str, Any], dict[str, Any], list[dict[str, Any]]
]:
if not IS_DARK:
return {}, {}, []
style_header = {
"backgroundColor": "rgb(30, 30, 30)",
"color": "white",
"fontWeight": "bold",
}
style_data = {
"backgroundColor": "rgb(50, 50, 50)",
"color": "white",
"whiteSpace": "normal",
"height": "auto",
}
style_data_conditional = [
{"if": {"row_index": "odd"}, "backgroundColor": "rgb(60, 60, 60)"}
]
return style_header, style_data, style_data_conditional
def build_table_chart(
display_df, table_config, original_df, display_columns, comparable_columns, decimal
):
display_df: pd.DataFrame,
table_config: dict[str, Any],
original_df: pd.DataFrame,
display_columns: list[str],
comparable_columns: list[str],
decimal: int,
) -> list[dash_table.DataTable]:
"""
Read data into a DashTable
"""
d_figs = []
# build comlumns/header with formatting
formatted_columns = []
for col in display_df.columns:
if (
str(col).lower() == "pct"
or str(col).lower() == "pop"
or str(col).lower() == "percentage"
):
formatted_columns.append(
dict(
id=col,
name=col,
type="numeric",
format={"specifier": ".{}f".format(decimal)},
)
)
col_lower = str(col).lower()
if col_lower in {"pct", "pop", "percentage"}:
formatted_columns.append({
"id": col,
"name": col,
"type": "numeric",
"format": {"specifier": f".{decimal}f"},
})
elif col in comparable_columns:
formatted_columns.append(
dict(
id=col,
name=col,
type="numeric",
format={"specifier": ".{}f".format(decimal)},
)
)
formatted_columns.append({
"id": col,
"name": col,
"type": "numeric",
"format": {"specifier": f".{decimal}f"},
})
else:
formatted_columns.append(dict(id=col, name=col, type="text"))
formatted_columns.append({"id": col, "name": col, "type": "text"})
# tooltip shows only on the 1st col for now if 'Metric Description' available
table_tooltip = (
@@ -326,7 +363,7 @@ def build_table_chart(
),
"type": "markdown",
}
for column, value in row.items()
for column in row.keys()
}
for row in original_df.to_dict("records")
]
@@ -334,6 +371,9 @@ def build_table_chart(
else None
)
# Get styling based on dark mode
style_header, style_data, style_data_conditional = get_dark_mode_styles()
# build data table with columns, tooltip, df and other properties
d_t = dash_table.DataTable(
id=str(table_config["id"]),
@@ -348,36 +388,11 @@ def build_table_chart(
# style cell
style_cell={"maxWidth": "500px"},
# display style
style_header=(
{
"backgroundColor": "rgb(30, 30, 30)",
"color": "white",
"fontWeight": "bold",
}
if IS_DARK
else {}
),
style_data=(
{
"backgroundColor": "rgb(50, 50, 50)",
"color": "white",
"whiteSpace": "normal",
"height": "auto",
}
if IS_DARK
else {}
),
style_data_conditional=(
[
{"if": {"row_index": "odd"}, "backgroundColor": "rgb(60, 60, 60)"},
]
if IS_DARK
else []
),
style_header=style_header,
style_data=style_data,
style_data_conditional=style_data_conditional,
# the df to display
data=display_df.to_dict("records"),
)
# print("DATA: \n", display_df.to_dict('records'))
d_figs.append(d_t)
return d_figs
# print(d_t.columns)
@@ -23,16 +23,19 @@
##############################################################################
from typing import Any, Union
import dash_bootstrap_components as dbc
import pandas as pd
from dash import dcc, html
from utils import schema
avail_normalizations = ["per_wave", "per_cycle", "per_second", "per_kernel"]
AVAIL_NORMALIZATIONS = ["per_wave", "per_cycle", "per_second", "per_kernel"]
# List all the unique column values for desired column in df, 'target_col'
def list_unique(orig_list, is_numeric):
def list_unique(orig_list: list[str], is_numeric: bool) -> list[str]:
list_set = set(orig_list)
unique_list = list(list_set)
if is_numeric:
@@ -40,18 +43,23 @@ def list_unique(orig_list, is_numeric):
return unique_list
def create_span(input):
return {"label": html.Span(str(input), title=str(input)), "value": str(input)}
def create_span(input_value: str) -> dict[str, Union[html.Span, str]]:
return {
"label": html.Span(str(input_value), title=str(input_value)),
"value": str(input_value),
}
def get_header(raw_pmc, input_filters, kernel_names):
kernel_names = list(
map(
str,
raw_pmc[schema.pmc_perf_file_prefix]["Kernel_Name"],
)
)
kernel_names = [x.strip() for x in kernel_names]
def get_header(
raw_pmc: pd.DataFrame, input_filters: dict[str, Any], kernel_names: list[str]
) -> html.Header:
pmc_data = raw_pmc[schema.PMC_PERF_FILE_PREFIX]
kernel_names = [str(name).strip() for name in pmc_data["Kernel_Name"]]
# Extract GPU and Dispatch IDs
gpu_ids = [str(gpu_id) for gpu_id in pmc_data["GPU_ID"]]
dispatch_ids = [str(dispatch_id) for dispatch_id in pmc_data["Dispatch_ID"]]
return html.Header(
id="home",
children=[
@@ -175,7 +183,7 @@ def get_header(raw_pmc, input_filters, kernel_names):
children=["Normalization:"],
),
dcc.Dropdown(
avail_normalizations,
AVAIL_NORMALIZATIONS,
id="norm-filt",
value=input_filters["normalization"],
clearable=False,
@@ -196,14 +204,7 @@ def get_header(raw_pmc, input_filters, kernel_names):
),
dcc.Dropdown(
list_unique(
list(
map(
str,
raw_pmc[
schema.pmc_perf_file_prefix
]["GPU_ID"],
)
),
gpu_ids,
True,
), # list avail gcd ids
id="gcd-filt",
@@ -229,14 +230,7 @@ def get_header(raw_pmc, input_filters, kernel_names):
children=["Dispatch Filter:"],
),
dcc.Dropdown(
list(
map(
str,
raw_pmc[
schema.pmc_perf_file_prefix
]["Dispatch_ID"],
)
),
dispatch_ids,
id="disp-filt",
multi=True,
# default to any dispatch
@@ -282,15 +276,12 @@ def get_header(raw_pmc, input_filters, kernel_names):
children=["Kernels:"],
),
dcc.Dropdown(
list(
map(
create_span,
list_unique(
orig_list=kernel_names,
is_numeric=False,
), # list avail kernel names
[
create_span(name)
for name in list_unique(
kernel_names, False
)
),
],
id="kernel-filt",
multi=True,
value=input_filters["kernel"],
@@ -22,29 +22,34 @@
# THE SOFTWARE.
##############################################################################
from typing import Any
from dash import html
from dash_svg import G, Path, Rect, Svg, Text
from utils import schema
from utils.logger import console_error
from utils.utils import format_scientific_notation_if_needed
# Constants for display formatting
DEFAULT_MAX_LENGTH = 6
DEFAULT_PRECISION = 1
DEFAULT_SCIENTIFIC_WIDTH = 8
def insert_chart_data(mem_data, base_data):
def insert_chart_data(mem_data: list[dict[str, Any]], base_data: schema.Workload) -> G:
if len(mem_data) != 1:
console_error("Memory Chart config doesn't follow expected formatting")
table_config = mem_data[0]["metric_table"]
original_df = base_data.dfs[table_config["id"]]
display_columns = original_df.columns.values.tolist().copy()
display_df = original_df[display_columns]
alias = display_df["Metric"].values
values = display_df["Value"].values
memchart_values = {}
memchart_values: dict[str, Any] = {}
for i in range(0, len(alias)):
memchart_values[alias[i]] = values[i]
@@ -521,7 +526,9 @@ def insert_chart_data(mem_data, base_data):
)
def get_memchart(mem_data, base_data):
def get_memchart(
mem_data: list[dict[str, Any]], base_data: schema.Workload
) -> html.Section:
return html.Section(
id="memchart",
children=[
@@ -2039,7 +2046,7 @@ def get_memchart(mem_data, base_data):
)
def format_value_for_display(value, max_length=6):
def format_value_for_display(value: Any, max_length: int = DEFAULT_MAX_LENGTH) -> str: # noqa: ANN401
"""
Format a value (int, float, or str) into a concise string suitable for display.
@@ -2097,8 +2104,8 @@ def format_value_for_display(value, max_length=6):
sci = format_scientific_notation_if_needed(
abs_val,
align=">",
width_align=8,
precision=1,
width_align=DEFAULT_SCIENTIFIC_WIDTH,
precision=DEFAULT_PRECISION,
fmt_type_align="e",
max_length=max_length,
).strip()
@@ -2110,7 +2117,7 @@ def format_value_for_display(value, max_length=6):
value = normal
if is_negative:
value = "-" + value
value = f"-{value}"
else:
value = str(value)
@@ -2123,11 +2130,11 @@ def format_value_for_display(value, max_length=6):
exponent = value[e_index:]
max_mantissa_len = max_length - len(exponent)
if max_mantissa_len < 1:
value = exponent[: max_length - 1] + ""
value = f"{exponent[: max_length - 1]}"
else:
truncated_mantissa = mantissa[:max_mantissa_len]
value = truncated_mantissa + exponent
else:
value = value[: max_length - 1] + ""
value = f"{value[: max_length - 1]}"
return value
@@ -26,112 +26,183 @@
import re
import subprocess
from pathlib import Path
from typing import Optional
import pandas as pd
from utils.logger import console_debug, console_error, console_log
cache = dict()
# Module-level cache for demangled kernel names
_NAME_CACHE: dict[str, str] = {}
# Constants
# NOTE: c++filt is a Linux-only solution for demangling C++ symbols.
# TODO: We need to think about Windows support in the future.
# Windows equivalent might be undname.exe or using llvm-cxxfilt.
# CONCERN: Using absolute path here is brittle - c++filt location may vary
# across distributions. TODO: Consider using shutil.which() or PATH lookup instead.
CPP_FILT_PATH = "/usr/bin/c++filt"
MAX_SHORTENING_LEVEL = 5
KERNEL_NAME_COLUMNS = ["Kernel_Name", "Name"]
# Note: shortener is now dependent on a rocprof install with llvm
def kernel_name_shortener(df, level):
def shorten_file(df, level):
global cache
def validate_cpp_filt(cpp_filt_path: str = CPP_FILT_PATH) -> bool:
"""Validate that c++filt binary exists and is executable."""
if not Path(cpp_filt_path).is_file():
console_error(
f"Could not resolve c++filt in expected directory: {cpp_filt_path}"
)
return False
return True
column_name = ""
if "Kernel_Name" in df:
column_name = "Kernel_Name"
if "Name" in df:
column_name = "Name"
if column_name == "Kernel_Name" or column_name == "Name":
# loop through all indices
for index in df.index:
original_name = df.loc[index, column_name]
if original_name in cache:
continue
def demangle_kernel_name(original_name: str, cpp_filt_path: str = CPP_FILT_PATH) -> str:
cmd = [cpp_filt_path, original_name]
cmd = [cpp_filt, original_name]
try:
proc = subprocess.Popen(
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
)
demangled_name, error = proc.communicate()
proc = subprocess.Popen(
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
if proc.returncode != 0:
console_error(f"c++filt failed for {original_name}: {error}", exit=False)
return original_name
demangled_name, e = proc.communicate()
demangled_name = str(demangled_name, "UTF-8").strip()
return demangled_name.strip()
# cache miss, add the shortened name to the dictionary
new_name = ""
matches = ""
except (subprocess.SubprocessError, OSError) as e:
console_error(f"Error running c++filt: {e}", exit=False)
return original_name
names_and_args = re.compile(
r"(?P<name>[( )A-Za-z0-9_]+)([ ,*<>()]+)(::)?"
)
# works for name:
# Kokkos::namespace::init_lock_array_kernel_threadid(int) [clone .kd]
if names_and_args.search(demangled_name):
matches = names_and_args.findall(demangled_name)
else:
# Works for first case '__amd_rocclr_fillBuffer.kd'
cache[original_name] = new_name
if new_name == None or new_name == "":
cache[original_name] = demangled_name
continue
def parse_template_depth(text: str, level: int, current_level: int) -> tuple[str, int]:
result = ""
curr_index = 0
current_level = 0
for name in matches:
# can cause errors if a function name-
# or argument is equal to 'clone'
if name[0] == "clone":
continue
if len(name) == 3:
if name[2] == "::":
continue
while curr_index < len(text) and ">" in text:
if current_level < level:
result += text[curr_index:]
current_level -= text[curr_index:].count(">")
break
elif text[curr_index] == ">":
current_level -= 1
curr_index += 1
if current_level < level:
new_name += name[0]
# closing '>' is to be taken account by the while loop
if name[1].count(">") == 0:
if current_level < level:
if not (
current_level == level - 1 and name[1].count("<") > 0
):
new_name += name[1]
current_level += name[1].count("<")
return result, current_level
curr_index = 0
# cases include '>' '> >, ' have to go in depth here to-
# not lose account of commas and current level
while name[1].count(">") > 0 and curr_index < len(name[1]):
if current_level < level:
new_name += name[1][curr_index:]
current_level -= name[1][curr_index:].count(">")
curr_index = len(name[1])
elif name[1][curr_index] == (">"):
current_level -= 1
curr_index += 1
cache[original_name] = new_name
if new_name == None or new_name == "":
cache[original_name] = demangled_name
def shorten_demangled_name(demangled_name: str, level: int) -> str:
names_and_args_pattern = re.compile(r"(?P<name>[( )A-Za-z0-9_]+)([ ,*<>()]+)(::)?")
df[column_name] = df[column_name].map(cache)
matches = names_and_args_pattern.findall(demangled_name)
if not matches:
# Handle cases like '__amd_rocclr_fillBuffer.kd'
return demangled_name
shortened_name = ""
current_level = 0
for name_part, args_part, scope_op in matches:
# Skip 'clone' parts as they can cause errors
if name_part == "clone":
continue
# Skip scope operators
if scope_op == "::":
continue
# Add name part if within level limit
if current_level < level:
shortened_name += name_part
# Handle template arguments
if ">" not in args_part:
if current_level < level:
# Don't add opening brackets at the deepest level
if not (current_level == level - 1 and "<" in args_part):
shortened_name += args_part
current_level += args_part.count("<")
else:
# Handle closing template brackets
if current_level < level:
shortened_name += args_part
current_level -= args_part.count(">")
else:
_, current_level = parse_template_depth(args_part, level, current_level)
return shortened_name if shortened_name else demangled_name
def process_single_kernel_name(
original_name: str, level: int, cpp_filt_path: str = CPP_FILT_PATH
) -> str:
if original_name in _NAME_CACHE:
return _NAME_CACHE[original_name]
demangled_name = demangle_kernel_name(original_name, cpp_filt_path)
shortened_name = shorten_demangled_name(demangled_name, level)
final_name = shortened_name if shortened_name else demangled_name
_NAME_CACHE[original_name] = final_name
return final_name
def get_kernel_column_name(df: pd.DataFrame) -> Optional[str]:
for column_name in KERNEL_NAME_COLUMNS:
if column_name in df.columns:
return column_name
return None
def shorten_file(
df: pd.DataFrame, level: int, cpp_filt_path: str = CPP_FILT_PATH
) -> pd.DataFrame:
column_name = get_kernel_column_name(df)
if not column_name:
console_debug("No kernel name column found")
return df
# Only shorten if valid shortening level
if level < 5:
cpp_filt = str(Path("/usr").joinpath("bin", "c++filt"))
if not Path(cpp_filt).is_file():
console_error(
"Could not resolve c++filt in expected directory: %s" % cpp_filt
)
df_copy = df.copy()
try:
modified_df = shorten_file(df, level)
console_log("profiling", "Kernel_Name shortening complete.")
return modified_df
except pd.errors.EmptyDataError:
console_debug("profiling", "Skipping shortening on empty csv")
df_copy[column_name] = df_copy[column_name].apply(
lambda name: process_single_kernel_name(name, level, cpp_filt_path)
)
return df_copy
def kernel_name_shortener(df: pd.DataFrame, level: int) -> Optional[pd.DataFrame]:
"""Shorten kernel names in a DataFrame.
NOTE: shortener is now dependent on a rocprof install with llvm
Args:
df: DataFrame containing kernel names
level: Shortening level (0-4)
Returns:
DataFrame with shortened kernel names, or None if processing fails
"""
if level >= MAX_SHORTENING_LEVEL:
console_debug("profiling", "Skipping kernel name shortening: level >= 5")
return df
cpp_filt = CPP_FILT_PATH
if not validate_cpp_filt(cpp_filt):
return df
try:
modified_df = shorten_file(df, level, cpp_filt)
console_log("profiling", "Kernel_Name shortening complete.")
return modified_df
except pd.errors.EmptyDataError:
console_debug("profiling", "Skipping shortening on empty csv")
return df
except Exception as e:
console_error(f"Error during kernel name shortening: {e}")
return df
@@ -27,6 +27,9 @@ import logging
import os
import sys
from pathlib import Path
from typing import Any, Callable, Optional, TypeVar
R = TypeVar("R")
# Define the colors
BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8)
@@ -42,58 +45,80 @@ COLORS = {
"TRACE": MAGENTA,
}
# Constants
TRACE_LEVEL = logging.DEBUG - 5
def demarcate(function):
def wrap_function(*args, **kwargs):
logging.trace("----- [entering function] -> %s()" % (function.__qualname__))
LOG_LEVEL_MAPPING = {
"DEBUG": logging.DEBUG,
"debug": logging.DEBUG,
"TRACE": TRACE_LEVEL,
"trace": TRACE_LEVEL,
"INFO": logging.INFO,
"info": logging.INFO,
"ERROR": logging.ERROR,
"error": logging.ERROR,
}
def demarcate(function: Callable[..., R]) -> Callable[..., R]:
def wrap_function(*args: Any, **kwargs: Any) -> R:
trace_logger(f"----- [entering function] -> {function.__qualname__}()")
result = function(*args, **kwargs)
logging.trace("----- [exiting function] -> %s()" % function.__qualname__)
trace_logger(f"----- [exiting function] -> {function.__qualname__}()")
return result
return wrap_function
def console_error(*argv, exit=True):
def console_error(*argv: Any, exit: bool = True) -> None:
if len(argv) > 1:
logging.error(f"[{argv[0]}] {argv[1]}")
else:
elif len(argv) == 1:
logging.error(f"{argv[0]}")
else:
logging.error("Empty error message")
if exit:
sys.exit(1)
def console_log(*argv, indent_level=0):
def console_log(*argv: Any, indent_level: int = 0) -> None:
indent = ""
if indent_level >= 1:
indent = " " * 3 * indent_level + "|-> " # spaces per indent level
indent = " " * (3 * indent_level) + "|-> " # spaces per indent level
if len(argv) > 1:
logging.info(indent + f"[{argv[0]}] {argv[1]}")
else:
elif len(argv) == 1:
logging.info(indent + f"{argv[0]}")
else:
logging.info(indent + "Empty log message")
def console_debug(*argv):
def console_debug(*argv: Any) -> None:
if len(argv) > 1:
logging.debug(f"[{argv[0]}] {argv[1]}")
else:
elif len(argv) == 1:
logging.debug(f"{argv[0]}")
else:
logging.debug("Empty debug message")
def console_warning(*argv):
def console_warning(*argv: Any) -> None:
if len(argv) > 1:
logging.warning(f"[{argv[0]}] {argv[1]}")
else:
elif len(argv) == 1:
logging.warning(f"{argv[0]}")
else:
logging.warning("Empty warning message")
def trace_logger(message, *args, **kwargs):
logging.log(logging.TRACE, message, *args, **kwargs)
def trace_logger(message: str, *args: Any, **kwargs: Any) -> None:
logging.log(TRACE_LEVEL, message, *args, **kwargs)
# Define the formatter
class ColoredFormatter(logging.Formatter):
def format(self, record):
def format(self, record: logging.LogRecord) -> str:
levelname = record.levelname
if levelname in COLORS:
levelname_color = (
@@ -104,7 +129,7 @@ class ColoredFormatter(logging.Formatter):
class ColoredFormatterAll(logging.Formatter):
def format(self, record):
def format(self, record: logging.LogRecord) -> str:
levelname = record.levelname
if levelname in COLORS:
if levelname == "INFO":
@@ -115,11 +140,12 @@ class ColoredFormatterAll(logging.Formatter):
f"%(levelname)s: %(message)s{RESET_SEQ}"
)
formatter = logging.Formatter(log_fmt)
return formatter.format(record)
return formatter.format(record)
return super().format(record)
class PlainFormatter(logging.Formatter):
def format(self, record):
def format(self, record: logging.LogRecord) -> str:
if record.levelno == logging.ERROR:
self._style._fmt = "%(levelname)s %(message)s"
else:
@@ -129,12 +155,11 @@ class PlainFormatter(logging.Formatter):
# Setup console handler - provided as separate function to be called
# prior to argument parsing
def setup_console_handler():
def setup_console_handler() -> None:
logging.getLogger().handlers.clear()
# register a trace level logger
logging.TRACE = logging.DEBUG - 5
logging.addLevelName(logging.TRACE, "TRACE")
setattr(logging, "TRACE", logging.TRACE)
logging.addLevelName(TRACE_LEVEL, "TRACE")
setattr(logging, "TRACE", TRACE_LEVEL)
setattr(logging, "trace", trace_logger)
color_setting = 1
@@ -164,8 +189,8 @@ def setup_console_handler():
# Setup file handler - enabled in profile mode
def setup_file_handler(loglevel, workload_dir):
filename = str(Path(workload_dir).joinpath("log.txt"))
def setup_file_handler(loglevel: int, workload_dir: str) -> None:
filename = str(Path(workload_dir) / "log.txt")
file_handler = logging.FileHandler(filename, "w")
file_loglevel = min([loglevel, logging.INFO])
file_handler.setLevel(file_loglevel)
@@ -174,9 +199,11 @@ def setup_file_handler(loglevel, workload_dir):
# Setup logger priority - called after argument parsing
def setup_logging_priority(verbosity, quietmode, appmode, guimode):
def setup_logging_priority(
verbosity: int, quietmode: bool, appmode: str, guimode: Optional[bool] = None
) -> int:
# set loglevel based on selected verbosity and quietmode
levels = [logging.INFO, logging.DEBUG, logging.TRACE]
levels = [logging.INFO, logging.DEBUG, TRACE_LEVEL]
if quietmode:
loglevel = logging.ERROR
@@ -191,18 +218,11 @@ def setup_logging_priority(verbosity, quietmode, appmode, guimode):
# optional: override of default loglevel via env variable which takes precedence
if "ROCPROFCOMPUTE_LOGLEVEL" in os.environ.keys():
loglevel = os.environ["ROCPROFCOMPUTE_LOGLEVEL"]
if loglevel in {"DEBUG", "debug"}:
loglevel = logging.DEBUG
elif loglevel in {"TRACE", "trace"}:
loglevel = logging.TRACE
elif loglevel in {"INFO", "info"}:
loglevel = logging.INFO
elif loglevel in {"ERROR", "error"}:
loglevel = logging.ERROR
if loglevel in LOG_LEVEL_MAPPING:
loglevel = LOG_LEVEL_MAPPING[loglevel]
else:
print(
"Ignoring unsupported ROCPROFCOMPUTE_LOGLEVEL setting (%s)" % loglevel
)
print(f"Ignoring unsupported ROCPROFCOMPUTE_LOGLEVEL setting ({loglevel})")
sys.exit(1)
# update console loglevel based on command-line args/env settings
@@ -25,14 +25,12 @@
import re
from dataclasses import dataclass, field
from decimal import Decimal
from typing import Dict
from typing import Any, Optional, Union
from plotille import Canvas
from .utils import format_scientific_notation_if_needed
from plotille import Canvas # type: ignore
def make_format_spec(num, align=">"):
def make_format_spec(num: Union[int, float], align: str = ">") -> str:
"""
Generate alignment string for a given input
"""
@@ -45,6 +43,11 @@ def make_format_spec(num, align=">"):
int_part = str(d.to_integral_value())
# Handle special cases where exponent is not an integer (NaN, Infinity, etc.)
if not isinstance(exponent, int):
# For special values, just return basic format
return f"{align}{str(num)}f"
if exponent >= 0:
# Pure integer, or float like 6.0, 6.00 (no decimal places)
if isinstance(num, int):
@@ -60,7 +63,7 @@ def make_format_spec(num, align=">"):
return f"{align}{num_str}f"
def is_value_valid(value):
def is_value_valid(value: Union[int, float, str, None]) -> bool:
"""
Check if a value is valid and display N/A if not
(to be valid, it needs to be not None, and be int or float)
@@ -75,15 +78,15 @@ def is_value_valid(value):
def format_text(
value,
key=None,
value: Union[int, float, str, None],
key: Union[str, Union[int, float], None] = None,
mark_between: str = ": ",
post_description_with_space: str = "",
value_step_prec_rightalign=0,
key_step_prec_leftalign=0,
key_align="<",
value_align=">",
):
value_step_prec_rightalign: Union[int, float] = 0,
key_step_prec_leftalign: Union[int, float] = 0,
key_align: str = "<",
value_align: str = ">",
) -> str:
"""
Format a text string for canvas to display according to
input key-value pair and make proper alignment.
@@ -94,61 +97,25 @@ def format_text(
# Step 1: Build format spec using make_format_spec
value_format = make_format_spec(value_step_prec_rightalign, value_align)
# Step 2: Extract width and precision as integer
match = re.match(r"([<>=^])(\d+)(?:\.(\d+))?([a-zA-Z])?", value_format)
if match:
align_char = match.group(1)
width_align = int(match.group(2))
precision_digits = match.group(3)
fmt_type_align = match.group(4) or "f"
precision = int(precision_digits) if precision_digits else 0
else:
# Fallback to default values
align_char = value_align
width_align = 6
precision = 2
fmt_type_align = "f"
# Step 3: Format the key using make_format_spec
key_format = (
make_format_spec(key_step_prec_leftalign, key_align)
if key is not None
else None
)
key_str = (
"{key:{key_format}}".format(key=key, key_format=key_format)
if key is not None and isinstance(key, (int, float))
else str(key)
if key is not None
else None
)
# Step 4: Format the value or fallback to N/A
if is_value_valid(value):
formatted_value = format_scientific_notation_if_needed(
value,
align=align_char,
width_align=width_align,
precision=precision,
fmt_type_align=fmt_type_align,
max_length=width_align,
sci_lower_bound=1e-3,
sci_upper_bound=1e3,
)
value_str = formatted_value
value_str = f"{value:{value_format}}"
else:
value_str = f"{'N/A':{align_char}{width_align}}"
match = re.search(r"[<>=^](\d+)", value_format)
width = int(match.group(1)) if match else 6
# Step 5: Unit and Final Output
unit_string = post_description_with_space if "N/A" not in value_str else ""
# Use same alignment as in value_format (first char)
align = value_format[0]
value_str = f"{'N/A':{align}{width}}"
if key_str is not None:
if key is not None:
key_format = make_format_spec(key_step_prec_leftalign, key_align)
key_str = f"{key:{key_format}}" if isinstance(key, (int, float)) else str(key)
result_str_no_unit = f"{key_str}{mark_between}{value_str}"
else:
result_str_no_unit = value_str
result_str_no_unit = f"{value_str}"
result_str = result_str_no_unit + unit_string
return result_str
unit_string = post_description_with_space if "N/A" not in value_str else ""
return result_str_no_unit + unit_string
# A basic rect frame for any block or group of wires where all its elements should
@@ -166,11 +133,10 @@ class RectFrame:
# Instr Buff Block
@dataclass
class InstrBuff(RectFrame):
wave_occupancy: int = None
wave_life: int = None
wave_occupancy: Optional[int] = None
wave_life: Optional[int] = None
def draw(self, canvas):
# print("---------", self.x_min, self.y_min, self.x_max, self.y_max)
def draw(self, canvas: Canvas) -> None:
canvas.text(self.x_min, self.y_max + 1.0, self.label)
canvas.rect(self.x_min, self.y_min, self.x_max - 2.0, self.y_max - 1.0)
@@ -208,8 +174,8 @@ class InstrBuff(RectFrame):
# Wires between Instr Buff and Instr Dispatch
@dataclass
class Wire_InstrBuff_InstrDispatch(RectFrame):
def draw(self, canvas):
# Todo: finer wires for connections
def draw(self, canvas: Canvas) -> None:
# TODO: finer wires for connections
canvas.line(self.x_min + 2, self.y_min, self.x_min + 2, self.y_max)
canvas.line(self.x_max, self.y_min + 1.5, self.x_max, self.y_max - 1.5)
canvas.line(self.x_min + 2, self.y_min, self.x_max, self.y_min + 1.5)
@@ -227,9 +193,9 @@ class InstrDispatch(RectFrame):
text_y_offset: float = 0.5
line_y_offset: float = 0.5
rect_y_offset: float = 3.0
instrs: Dict[str, int] = field(default_factory=dict)
instrs: dict[str, int] = field(default_factory=dict)
def draw(self, canvas):
def draw(self, canvas: Canvas) -> None:
canvas.text(self.x_min, self.y_max + 1.0, self.label)
self.top_rect_x_min = self.x_min + 2.0
@@ -237,9 +203,7 @@ class InstrDispatch(RectFrame):
self.top_rect_y_min = self.y_max - 1.5
self.top_rect_y_max = self.y_max
i = 0
for k, v in self.instrs.items():
# print(k,v)
for i, (k, v) in enumerate(self.instrs.items()):
text = format_text(
key=k,
value=v,
@@ -258,7 +222,6 @@ class InstrDispatch(RectFrame):
self.top_rect_y_min - self.rect_y_offset * i,
"------------------>",
)
i = i + 1
# Exec Block
@@ -273,7 +236,7 @@ class Exec(RectFrame):
wavefronts: int = 0
workgroups: int = 0
def draw(self, canvas):
def draw(self, canvas: Canvas) -> None:
canvas.text(self.x_min, self.y_max + 1.0, self.label)
canvas.rect(self.x_min, self.y_min, self.x_max, self.y_max)
@@ -378,13 +341,13 @@ class Exec(RectFrame):
class Wire_E_GLVS(RectFrame):
text_x_offset: float = 3.0
lds_req: int = None
vl1_rd: int = None
vl1_wr: int = None
vl1_atomic: int = None
sl1_rd: int = None
lds_req: Optional[int] = None
vl1_rd: Optional[int] = None
vl1_wr: Optional[int] = None
vl1_atomic: Optional[int] = None
sl1_rd: Optional[int] = None
def draw(self, canvas):
def draw(self, canvas: Canvas) -> None:
canvas.text(
self.x_min + self.text_x_offset,
self.y_max - 2.0,
@@ -459,7 +422,7 @@ class Wire_E_GLVS(RectFrame):
class Wire_InstrBuff_IL1Cache(RectFrame):
il1_fetch: int = 0
def draw(self, canvas):
def draw(self, canvas: Canvas) -> None:
end_col = int(self.y_max - self.y_min)
canvas.text(self.x_min, self.y_max - 1, "^")
for i in range(2, end_col):
@@ -482,10 +445,10 @@ class Wire_InstrBuff_IL1Cache(RectFrame):
# GDS Block
@dataclass
class GDS(RectFrame):
gws: int = None
latency: int = None
gws: Optional[int] = None
latency: Optional[int] = None
def draw(self, canvas):
def draw(self, canvas: Canvas) -> None:
canvas.text(self.x_min, self.y_max + 1.0, self.label)
canvas.rect(self.x_min, self.y_min, self.x_max, self.y_max)
@@ -523,10 +486,10 @@ class GDS(RectFrame):
# LDS Block
@dataclass
class LDS(RectFrame):
util: int = None
latency: int = None
util: Optional[int] = None
latency: Optional[int] = None
def draw(self, canvas):
def draw(self, canvas: Canvas) -> None:
canvas.text(self.x_min, self.y_max + 1.0, self.label)
canvas.rect(self.x_min, self.y_min, self.x_max, self.y_max)
canvas.text(
@@ -556,12 +519,12 @@ class LDS(RectFrame):
# Vector L1 Cache Block
@dataclass
class VectorL1Cache(RectFrame):
hit: int = None
latency: int = None
coales: int = None
stall: int = None
hit: Optional[int] = None
latency: Optional[int] = None
coales: Optional[int] = None
stall: Optional[int] = None
def draw(self, canvas):
def draw(self, canvas: Canvas) -> None:
canvas.text(self.x_min, self.y_max + 1.0, self.label)
canvas.rect(self.x_min, self.y_min, self.x_max, self.y_max)
@@ -614,10 +577,10 @@ class VectorL1Cache(RectFrame):
# Scalar L1D Cache
@dataclass
class ScalarL1DCache(RectFrame):
hit: int = None
latency: int = None
hit: Optional[int] = None
latency: Optional[int] = None
def draw(self, canvas):
def draw(self, canvas: Canvas) -> None:
canvas.text(self.x_min, self.y_max + 1.0, self.label)
canvas.rect(self.x_min, self.y_min, self.x_max, self.y_max)
@@ -648,10 +611,10 @@ class ScalarL1DCache(RectFrame):
# Instr L1 Cache
@dataclass
class InstrL1Cache(RectFrame):
hit: int = None
latency: int = None
hit: Optional[int] = None
latency: Optional[int] = None
def draw(self, canvas):
def draw(self, canvas: Canvas) -> None:
canvas.text(self.x_min, self.y_max + 1.0, self.label)
canvas.rect(self.x_min, self.y_min, self.x_max, self.y_max)
@@ -684,15 +647,15 @@ class InstrL1Cache(RectFrame):
class Wires_L1_L2(RectFrame):
text_v_x_offset: float = 0.0
vl1_l2_rd: int = None
vl1_l2_wr: int = None
vl1_l2_atomic: int = None
sl1_l2_rd: int = None
sl1_l2_wr: int = None
sl1_l2_atomic: int = None
il1_l2_req: int = None
vl1_l2_rd: Optional[int] = None
vl1_l2_wr: Optional[int] = None
vl1_l2_atomic: Optional[int] = None
sl1_l2_rd: Optional[int] = None
sl1_l2_wr: Optional[int] = None
sl1_l2_atomic: Optional[int] = None
il1_l2_req: Optional[int] = None
def draw(self, canvas):
def draw(self, canvas: Canvas) -> None:
canvas.text(
self.x_min + self.text_v_x_offset,
self.y_max - 2.0,
@@ -783,14 +746,14 @@ class Wires_L1_L2(RectFrame):
# L2 Cache
@dataclass
class L2Cache(RectFrame):
rd: int = None
wr: int = None
atomic: int = None
hit: int = None
rd_lat: int = None
wr_lat: int = None
rd: Optional[int] = None
wr: Optional[int] = None
atomic: Optional[int] = None
hit: Optional[int] = None
rd_lat: Optional[int] = None
wr_lat: Optional[int] = None
def draw(self, canvas):
def draw(self, canvas: Canvas) -> None:
canvas.text(self.x_min, self.y_max + 1.0, self.label)
canvas.rect(self.x_min, self.y_min, self.x_max, self.y_max)
@@ -876,11 +839,11 @@ class L2Cache(RectFrame):
class Wire_L2_Fabric(RectFrame):
text_x_offset: float = 3.0
rd: int = None
wr: int = None
atomic: int = None
rd: Optional[int] = None
wr: Optional[int] = None
atomic: Optional[int] = None
def draw(self, canvas):
def draw(self, canvas: Canvas) -> None:
canvas.text(
self.x_min + self.text_x_offset,
self.y_max - 2.0,
@@ -925,7 +888,7 @@ class Wire_L2_Fabric(RectFrame):
# xGMI/PCIe block with wires to fabric
@dataclass
class xGMI_PCIe(RectFrame):
def draw(self, canvas):
def draw(self, canvas: Canvas) -> None:
canvas.rect(self.x_min, self.y_min, self.x_max, self.y_max)
canvas.text(self.x_min + 1.0, self.y_max - 2.0, self.label)
canvas.text(self.x_min + 3.0, self.y_max - 5.0, "^ |")
@@ -937,9 +900,9 @@ class xGMI_PCIe(RectFrame):
# Fabric Cache Block
@dataclass
class Fabric(RectFrame):
lat: Dict[str, int] = field(default_factory=dict)
lat: dict[str, int] = field(default_factory=dict)
def draw(self, canvas):
def draw(self, canvas: Canvas) -> None:
canvas.rect(self.x_min, self.y_min, self.x_max, self.y_max)
canvas.text(self.x_min + 6.0, self.y_max - 2.0, " " + self.label)
canvas.text(self.x_min + 2.0, self.y_max - 4.0, "Latency (cycles)")
@@ -947,24 +910,20 @@ class Fabric(RectFrame):
self.x_min + 2.0, self.y_max - 9, self.x_max - 2.0, self.y_max - 4.5
)
i = 1
for k, v in self.lat.items():
# print(k,v)
for i, (k, v) in enumerate(self.lat.items(), 1):
text = format_text(
key=k,
value=v,
key_step_prec_leftalign=6,
value_step_prec_rightalign=6.0,
)
canvas.text(self.x_min + 4.0, self.y_max - 4.5 - i, text)
i = i + 1
# GMI block with wires to fabric
@dataclass
class GMI(RectFrame):
def draw(self, canvas):
def draw(self, canvas: Canvas) -> None:
canvas.text(self.x_min + 3.0, self.y_max + 4.0, "^ |")
canvas.text(self.x_min + 3.0, self.y_max + 3.0, "| |")
canvas.text(self.x_min + 3.0, self.y_max + 2.0, "| |")
@@ -981,7 +940,7 @@ class Wire_Fabric_HBM(RectFrame):
rd: int = 0
wr: int = 0
def draw(self, canvas):
def draw(self, canvas: Canvas) -> None:
canvas.text(
self.x_min + self.text_x_offset,
self.y_max,
@@ -1013,30 +972,31 @@ class Wire_Fabric_HBM(RectFrame):
# HBM
@dataclass
class HBM(RectFrame):
def draw(self, canvas):
def draw(self, canvas: Canvas) -> None:
canvas.rect(self.x_min, self.y_min, self.x_max, self.y_max)
canvas.text(self.x_min + 4.0, self.y_max - 2.0, self.label)
# Memory chart pannel for 1 instance
class MemChart:
def __init__(self, x_min, y_min, x_max, y_max):
def __init__(self, x_min: float, y_min: float, x_max: float, y_max: float) -> None:
self.x_min = x_min
self.x_max = x_max
self.y_min = y_min
self.y_max = y_max
def draw(self, canvas, normal_unit, metric_dict):
def draw(
self, canvas: Canvas, normal_unit: str, metric_dict: dict[str, Any]
) -> None:
# ----------------------------------------
# Overall rect and title
canvas.rect(self.x_min, self.y_min, self.x_max, self.y_max)
canvas.text(
self.x_min + 2.0, self.y_max - 2.0, "(Normalization: " + normal_unit + ")"
self.x_min + 2.0, self.y_max - 2.0, f"(Normalization: {normal_unit})"
)
# Fixme: this is temp solution to filter out non-numeric string
# FIXME: this is temp solution to filter out non-numeric string
for k, v in metric_dict.items():
# print(k, type(v))
metric_dict[k] = None if isinstance(v, str) else v
# Typically, the drawing order would be: left->right, top->down
@@ -1317,20 +1277,18 @@ class MemChart:
block_hbm.draw(canvas)
def plot_mem_chart(arch, normal_unit, metric_dict):
"""plot memory chart from an arch with given metrics dict"""
def plot_mem_chart(arch: str, normal_unit: str, metric_dict: dict[str, Any]) -> str:
# TODO: verify metrics dict for given arch first
canvas = Canvas(width=234, height=42, xmax=234, ymax=42)
mc = MemChart(0, 0, 233, 41)
mc.draw(canvas, normal_unit, metric_dict)
# return the plot string stream
return canvas.plot()
if __name__ == "__main__":
# TODO: unit test should be moved to tests/*
# Unit test
metric_dict = {}
metric_dict["Wavefront Occupancy"] = 1
@@ -24,60 +24,42 @@
##############################################################################
import os
from dataclasses import dataclass
from typing import Any, Dict
from typing import Any, Optional
import yaml
from utils.logger import console_debug, console_error, console_warning
# Constants for MI series
# NOTE: Currently supports MI50, MI100, MI200, MI300
MI50 = 0
MI100 = 1
MI200 = 2
MI300 = 3
MI350 = 4
MI_CONSTANS = {
MI50: "mi50",
MI100: "mi100",
MI200: "mi200",
MI300: "mi300",
MI350: "mi350",
}
# ----------------------------
# Data Class handling to preserve the hierarchical gpu information
# ----------------------------
@dataclass
class MIGPUSpecs:
_instance = None
_instance: Optional["MIGPUSpecs"] = None
_gpu_series_dict = {} # key: gpu_arch
_gpu_model_dict = {} # key: gpu_arch
_num_xcds_dict = {} # key: gpu_model
_chip_id_dict = {} # key: chip_id (int)
_perfmon_config = {} # key: gpu_arch
_gpu_series_dict: dict[str, str] = {} # key: gpu_arch
_gpu_model_dict: dict[str, list[str]] = {} # key: gpu_arch
_num_xcds_dict: dict[str, dict[str, int]] = {} # key: gpu_model
_chip_id_dict: dict[int, str] = {} # key: chip_id (int)
_perfmon_config: dict[str, Any] = {} # key: gpu_arch
_gpu_arch_to_compute_partition_dict = {} # key: gpu_arch, used for gpu archs
# containing only one gpu model and
# key: gpu_arch, used for gpu archs containing only one gpu model and
# thus one compute partition
_gpu_arch_to_compute_partition_dict: dict[str, dict[str, int]] = {}
_all_gpu_models = []
_all_gpu_models: list[str] = []
_initialized = False
def __new__(cls):
def __new__(cls) -> "MIGPUSpecs":
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._initialize()
return cls._instance
@classmethod
def _initialize(cls):
def _initialize(cls) -> None:
if not cls._initialized:
cls._parse_mi_gpu_spec()
cls._initialized = True
@@ -87,7 +69,7 @@ class MIGPUSpecs:
# ----------------------------
@classmethod
def _load_yaml(cls, file_path: str) -> Dict[str, Any]:
def _load_yaml(cls, file_path: str) -> dict[str, Any]:
"""
Loads MI GPU YAML data /util into a Python dictionary.
@@ -98,23 +80,14 @@ class MIGPUSpecs:
Dict[str, Any]: Parsed YAML data as a nested dictionary.
Exit with console error if an error occurs.
"""
console_debug("[load_yaml]")
try:
with open(file_path, "r") as file:
data = yaml.safe_load(file)
return data
except FileNotFoundError:
console_error(f"Error: The file '{file_path}' was not found.")
except yaml.YAMLError as exc:
console_error(f"Error parsing YAML file '{file_path}': {exc}")
except Exception as e:
console_error(
f"An unexpected error occurred while loading YAML "
f"file '{file_path}': {e}"
)
console_debug("mi_gpu_spec", "[load_yaml]")
with open(file_path) as file:
data = yaml.safe_load(file)
return data or {}
@classmethod
def _parse_mi_gpu_spec(cls):
def _parse_mi_gpu_spec(cls) -> None:
"""
Parse out mi gpu data from yaml file and store in memory.
MI GPUs
@@ -136,16 +109,26 @@ class MIGPUSpecs:
# Load the YAML data
yaml_data = cls._load_yaml(yaml_file_path)
for series in yaml_data["mi_gpu_spec"]:
curr_gpu_series = series["gpu_series"]
console_debug("[parse_mi_gpu_spec] Processing series: %s" % curr_gpu_series)
for archs in series["gpu_archs"]:
curr_gpu_arch = archs["gpu_arch"]
for series in yaml_data.get("mi_gpu_spec", []):
curr_gpu_series = series.get("gpu_series")
if not curr_gpu_series:
continue
console_debug(
"mi_gpu_spec",
f"[parse_mi_gpu_spec] Processing series: {curr_gpu_series}",
)
for archs in series.get("gpu_archs", []):
curr_gpu_arch = archs.get("gpu_arch")
cls._gpu_series_dict[curr_gpu_arch] = curr_gpu_series
cls._perfmon_config[curr_gpu_arch] = archs["perfmon_config"]
cls._perfmon_config[curr_gpu_arch] = archs.get("perfmon_config", {})
cls._gpu_model_dict[curr_gpu_arch] = []
for models in archs["models"]:
for models in archs.get("models", []):
curr_gpu_model = models["gpu_model"]
cls._all_gpu_models.append(curr_gpu_model)
cls._gpu_model_dict[curr_gpu_arch].append(curr_gpu_model)
cls._num_xcds_dict[curr_gpu_model] = (
@@ -153,6 +136,7 @@ class MIGPUSpecs:
.get("compute_partition_mode", {})
.get("num_xcds", {})
)
if "chip_ids" in models and "physical" in models["chip_ids"]:
cls._chip_id_dict[models["chip_ids"]["physical"]] = (
curr_gpu_model
@@ -166,7 +150,7 @@ class MIGPUSpecs:
cls._populate_gpu_arch_to_compute_partition_dict()
@classmethod
def _populate_gpu_arch_to_compute_partition_dict(cls):
def _populate_gpu_arch_to_compute_partition_dict(cls) -> None:
"""
This creates a mapping of gpu_arch -> compute_partition for architectures
where there's only one model (and therefore one partition configuration).
@@ -181,53 +165,55 @@ class MIGPUSpecs:
compute_partition
)
console_debug(
"[populate_single_arch_partition_dict] Single model "
"arch found: %s -> %s (partition: %s)"
% (gpu_arch, single_model, compute_partition)
f"[populate_single_arch_partition_dict] Single model "
f"arch found: {gpu_arch} -> {single_model} (partition:"
f" {compute_partition})"
)
@classmethod
def get_gpu_series_dict(cls):
def get_gpu_series_dict(cls) -> dict[str, str]:
if not cls._gpu_series_dict:
console_error(
"gpu_series_dict not yet populated, did you run parse_mi_gpu_spec()?"
"gpu_series_dict not yet populated, did you run parse_mi_gpu_spec()?",
exit=False,
)
return None
return cls._gpu_series_dict
@classmethod
def get_gpu_series(cls, gpu_arch_):
def get_gpu_series(cls, gpu_arch: str) -> Optional[str]:
if not cls._gpu_series_dict:
console_error(
"gpu_series_dict not yet populated, did you run parse_mi_gpu_spec()?"
"gpu_series_dict not yet populated, did you run parse_mi_gpu_spec()?",
exit=False,
)
return None
# Normalize the key by checking both the raw and lowercase versions
gpu_series = cls._gpu_series_dict.get(gpu_arch_) or cls._gpu_series_dict.get(
gpu_arch_.lower()
gpu_series = cls._gpu_series_dict.get(gpu_arch) or cls._gpu_series_dict.get(
gpu_arch.lower()
)
if gpu_series:
return gpu_series.upper()
console_warning(f"No matching gpu series found for gpu arch: {gpu_arch_}")
console_warning(f"No matching gpu series found for gpu arch: {gpu_arch}")
return None
@classmethod
def get_perfmon_config(cls, gpu_arch_):
def get_perfmon_config(cls, gpu_arch: str) -> dict[Any, Any]:
# Check that gpu_model_dict is populated first
if not cls._perfmon_config:
console_error(
"gpu_model_dict not yet populated. Did you run parse_mi_gpu_spec()?"
)
return None
gpu_arch_lower = gpu_arch_.lower()
return cls._perfmon_config.get(gpu_arch_lower, None)
return cls._perfmon_config.get(gpu_arch.lower(), {})
@classmethod
def get_gpu_model(cls, gpu_arch_, chip_id_):
def get_gpu_model(
cls, gpu_arch: Optional[str], chip_id: Optional[str] = None
) -> Optional[str]:
if not gpu_arch and not chip_id:
return None
# Check that gpu_model_dict is populated first
if not cls._gpu_model_dict:
console_error(
@@ -235,43 +221,55 @@ class MIGPUSpecs:
)
return None
gpu_arch_lower = gpu_arch_.lower()
gpu_arch_lower = gpu_arch.lower()
# Handle gfx942 with chip_id mapping
if gpu_arch_lower not in ("gfx908", "gfx90a"):
if chip_id_ and int(chip_id_) in cls._chip_id_dict:
gpu_model = cls._chip_id_dict.get(int(chip_id_))
if chip_id and chip_id.isdigit():
chip_id_int = int(chip_id)
if chip_id_int in cls._chip_id_dict:
gpu_model = cls._chip_id_dict[chip_id_int]
else:
console_warning(f"No gpu model found for chip id: {chip_id}")
return None
else:
console_warning(f"No gpu model found for chip id: {chip_id_}")
console_warning(f"No valid chip id provided: {chip_id}")
return None
# Otherwise use gpu_model_dict mapping for other mi architectures
elif gpu_arch_lower in cls._gpu_model_dict:
# NOTE: take the first element works for now
gpu_model = cls._gpu_model_dict[gpu_arch_lower][0]
gpu_models = cls._gpu_model_dict[gpu_arch_lower]
if gpu_models:
gpu_model = gpu_models[0]
else:
console_warning(f"No gpu models found for gpu arch: {gpu_arch_lower}")
return None
else:
console_warning(f"No gpu model found for gpu arch: {gpu_arch_lower}")
return None
if not gpu_model:
console_warning(f"No gpu model found for gpu arch: {gpu_arch_lower}")
return None
return gpu_model.upper()
return gpu_model.upper() if gpu_model else None
@classmethod
def set_default_gpu_settings(self, gpu_arch, gpu_model, compute_partition):
def set_default_gpu_settings(
cls,
gpu_arch: Optional[str],
gpu_model: Optional[str],
compute_partition: Optional[str],
) -> int:
"""
Set default GPU settings when model is unknown or cannot be
determined. NOTE: This is a fallback to gfx942 settings -
consider making this architecture-specific.
"""
DEFAULT_COMPUTE_PARTITION = "SPX"
DEFAULT_NUM_XCD = 8
console_warning(
"Unable to determine xcd count from:\n\t"
f"GPU arch: '{gpu_arch}', model: '{gpu_model}',\n\t"
f"partition: '{compute_partition}'"
f'GPU arch: "{gpu_arch}", model: "{gpu_model}",\n\t'
f'partition: "{compute_partition}"'
)
console_warning(
f"Applying default gfx942 settings:\n"
@@ -283,8 +281,11 @@ class MIGPUSpecs:
@classmethod
def get_num_xcds(
cls, gpu_arch: str = None, gpu_model: str = None, compute_partition: str = None
):
cls,
gpu_arch: Optional[str] = None,
gpu_model: Optional[str] = None,
compute_partition: Optional[str] = None,
) -> int:
"""
Retrieve the number of XCDs based on GPU architecture, model,
and compute partition.
@@ -310,22 +311,22 @@ class MIGPUSpecs:
return 1
# 2. Try architecture-based lookup first (preferred method)
if gpu_arch_norm and hasattr(cls, "_gpu_arch_to_compute_partition_dict"):
arch_dict = cls._gpu_arch_to_compute_partition_dict
if gpu_arch_norm in arch_dict:
num_xcds = arch_dict[gpu_arch_norm]
if gpu_arch_norm and gpu_arch_norm in cls._gpu_arch_to_compute_partition_dict:
arch_dict = cls._gpu_arch_to_compute_partition_dict[gpu_arch_norm]
if partition_norm and partition_norm in arch_dict:
num_xcds = arch_dict[partition_norm]
if num_xcds is not None:
return num_xcds
else:
console_warning(
f"No compute partition data found for "
f"architecture '{gpu_arch.upper()}'"
)
else:
console_warning(
f"No compute partition data found for "
f"architecture: {gpu_arch.upper() if gpu_arch else None}"
)
# 3. Fall back to model + partition-based lookup
if gpu_model_norm:
# Validate XCD dictionary is populated
if not hasattr(cls, "_num_xcds_dict") or not cls._num_xcds_dict:
if not cls._num_xcds_dict:
console_error(
"mi300_num_xcds_dict not populated. "
"Did you run parse_mi_gpu_spec()?"
@@ -343,9 +344,8 @@ class MIGPUSpecs:
)
elif partition_norm not in model_dict:
console_warning(
f"Unknown compute partition "
f"'{compute_partition}' for model "
f"'{gpu_model}'"
f"Unknown compute partition: "
f"{compute_partition} for model: {gpu_model}"
)
else:
num_xcds = model_dict[partition_norm]
@@ -364,25 +364,19 @@ class MIGPUSpecs:
return cls.set_default_gpu_settings(gpu_arch, gpu_model, compute_partition)
@classmethod
def get_chip_id_dict(cls):
if cls._chip_id_dict:
return cls._chip_id_dict
else:
console_error()
def get_chip_id_dict(cls) -> dict[int, str]:
return cls._chip_id_dict
@classmethod
def get_num_xcds_dict(cls):
if cls._num_xcds_dict:
return cls._num_xcds_dict
else:
console_error()
def get_num_xcds_dict(cls) -> dict[str, dict[str, int]]:
return cls._num_xcds_dict
@classmethod
def get_gpu_arch_to_compute_partition_dict(cls):
def get_gpu_arch_to_compute_partition_dict(cls) -> dict[str, dict[str, int]]:
return cls._gpu_arch_to_compute_partition_dict
@classmethod
def get_all_gpu_models(cls):
def get_all_gpu_models(cls) -> list:
return cls._all_gpu_models
File diff suppressed because it is too large Load Diff
@@ -26,6 +26,9 @@
import csv
import sqlite3
from contextlib import closing
from typing import Any
import pandas as pd
from utils.logger import console_error
@@ -70,19 +73,22 @@ def convert_db_to_csv(
])
for row in cursor:
writer.writerow(row)
except (sqlite3.DatabaseError, IOError) as e:
console_error(f"Error converting database to CSV: {e}")
except OSError as e:
console_error(f"Database error while converting to CSV: {e}")
except Exception as e:
console_error(f"Unexpected error converting database to CSV: {e}")
def process_rocpd_csv(df):
def process_rocpd_csv(df: pd.DataFrame) -> pd.DataFrame:
"""
Merge counters across unique dispatches from the
input dataframe and return processed dataframe.
"""
# Only import pandas if needed
import pandas as pd
if df.empty:
return df
data: list[dict[str, Any]] = []
data = list()
# Group by unique kernel and merge into a single row
for _, group_df in df.groupby([
"Dispatch_ID",
@@ -27,18 +27,18 @@
import csv
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Union
import pandas as pd
from utils import schema
from utils.logger import console_debug, console_warning
from utils.parser import apply_filters, eval_metric
from utils.specs import MachineSpecs
################################################
# Global vars
################################################
IMGNAME = "empirRoof"
XMIN = 0.01
XMAX = 1000
@@ -48,7 +48,7 @@ FONT_WEIGHT = "bold"
# SUPPORTED_DATATYPES table is based on datatype support in rocm-amdgpu-bench repository
# Indicates which datatypes per gpu arch can be generated by the roofline binary
SUPPORTED_DATATYPES = {
SUPPORTED_DATATYPES: dict[str, list[str]] = {
"gfx90a": [
"FP16",
"BF16",
@@ -135,7 +135,49 @@ class AI_Data:
avgDuration: float
def get_font():
@dataclass
class PlotPoints:
"""Data structure for storing roofline plot points."""
ai_l1: list[list[float]]
ai_l2: list[list[float]]
ai_hbm: list[list[float]]
kernelNames: list[str]
@classmethod
def empty(cls) -> "PlotPoints":
"""Create empty plot points structure."""
return cls(ai_l1=[[], []], ai_l2=[[], []], ai_hbm=[[], []], kernelNames=[])
@dataclass
class GraphPoints:
"""Data structure for storing roofline graph ceiling points."""
hbm: list[Union[list[float], float, None]]
l2: list[Union[list[float], float, None]]
l1: list[Union[list[float], float, None]]
lds: list[Union[list[float], float, None]]
valu: list[Union[list[float], float, None]]
mfma: list[Union[list[float], float, None]]
@classmethod
def empty(cls) -> "GraphPoints":
"""Create empty graph points structure."""
return cls(
hbm=[None, None, None],
l2=[None, None, None],
l1=[None, None, None],
lds=[None, None, None],
valu=[None, None, None],
mfma=[None, None, None],
)
################################################
# Helper functions
################################################
def get_font() -> dict[str, Union[int, str]]:
return {
"size": FONT_SIZE,
"color": FONT_COLOR,
@@ -144,160 +186,164 @@ def get_font():
}
def get_color(catagory):
if catagory == "ai_l1":
return "green"
elif catagory == "ai_l2":
return "blue"
elif catagory == "ai_hbm":
return "red"
else:
raise RuntimeError("Invalid catagory passed to get_color()")
def get_color(category: str) -> str:
color_map = {"ai_l1": "green", "ai_l2": "blue", "ai_hbm": "red"}
if category not in color_map:
raise RuntimeError(f"Invalid category passed to get_color(): {category}")
return color_map[category]
# -------------------------------------------------------------------------------------
# Plot BW at each cache level
# -------------------------------------------------------------------------------------
def calc_ceilings(roofline_parameters, dtype, benchmark_data):
def calc_ceilings(
roofline_parameters: dict[str, Any],
dtype: str,
benchmark_data: dict[str, list[str]],
) -> dict[str, list[Union[list[float], float, None]]]:
"""Given benchmarking data, calculate ceilings (or peak performance) for
empirical roofline"""
# TODO: This is where filtering by memory level will need to occur for standalone
graphPoints = {"hbm": [], "l2": [], "l1": [], "lds": [], "valu": [], "mfma": []}
graph_points: dict[str, list[Union[list[float], float, None]]] = {
"hbm": [],
"l2": [],
"l1": [],
"lds": [],
"valu": [],
"mfma": [],
}
if roofline_parameters["mem_level"] == "ALL":
cacheHierarchy = CACHE_HIERARCHY
else:
cacheHierarchy = roofline_parameters["mem_level"]
cache_hierarchy = (
CACHE_HIERARCHY
if roofline_parameters["mem_level"] == "ALL"
else roofline_parameters["mem_level"]
)
x1 = y1 = x2 = y2 = -1
x1_mfma = y1_mfma = x2_mfma = y2_mfma = -1
ops_flops = "Ops" if (dtype[:1] == "I") else "Flops"
ops_flops = "Ops" if dtype.startswith("I") else "Flops"
peak_ops = 0.0
if dtype in PEAK_OPS_DATATYPES:
peakOps = float(
benchmark_data[dtype + "{}".format(ops_flops)][
roofline_parameters["device_id"]
]
peak_ops = float(
benchmark_data[f"{dtype}{ops_flops}"][roofline_parameters["device_id"]]
)
for i in range(0, len(cacheHierarchy)):
for cache_level in cache_hierarchy:
# Plot BW line
console_debug("roofline", "Current cache level is %s" % cacheHierarchy[i])
curr_bw = cacheHierarchy[i] + "Bw"
peakBw = float(benchmark_data[curr_bw][roofline_parameters["device_id"]])
console_debug("roofline", f"Current cache level is {cache_level}")
curr_bw = f"{cache_level}Bw"
peak_bw = float(benchmark_data[curr_bw][roofline_parameters["device_id"]])
x1 = float(XMIN)
y1 = float(XMIN) * peakBw
y1 = float(XMIN) * peak_bw
if dtype in PEAK_OPS_DATATYPES:
x2 = peakOps / peakBw
y2 = peakOps # noqa
x2 = peak_ops / peak_bw
y2 = peak_ops # noqa
# Plot MFMA lines (NOTE: Assuming MI200 soc)
x1_mfma = peakOps / peakBw
y1_mfma = peakOps
x1_mfma = peak_ops / peak_bw
y1_mfma = peak_ops
peak_mfma = 0.0
if dtype in MFMA_DATATYPES:
target_precision = (dtype) if (dtype[:1] == "I") else ("F" + dtype[2:])
target_precision = dtype if dtype.startswith("I") else f"F{dtype[2:]}"
peakMFMA = float(
benchmark_data["MFMA{}{}".format(target_precision, ops_flops)][
peak_mfma = float(
benchmark_data[f"MFMA{target_precision}{ops_flops}"][
roofline_parameters["device_id"]
]
)
x2_mfma = peakMFMA / peakBw
y2_mfma = peakMFMA
x2_mfma = peak_mfma / peak_bw
y2_mfma = peak_mfma
# Check which peak is higher for formatting bandwidth lines
if y2_mfma > y1_mfma: # peakMFMA
peakX = x2_mfma
peakY = y2_mfma
if y2_mfma > y1_mfma: # peak_mfma
peak_x = x2_mfma
peak_y = y2_mfma
else: # peakVALU
peakX = x1_mfma
peakY = y1_mfma
peak_x = x1_mfma
peak_y = y1_mfma
# These are the points to use:
console_debug("roofline", "coordinate points:")
console_debug("x = [{}, {}]".format(x1, peakX))
console_debug("y = [{}, {}]".format(y1, peakY))
console_debug(f"x = [{x1}, {peak_x}]")
console_debug(f"y = [{y1}, {peak_y}]")
graphPoints[cacheHierarchy[i].lower()].append([x1, peakX])
graphPoints[cacheHierarchy[i].lower()].append([y1, peakY])
graphPoints[cacheHierarchy[i].lower()].append(peakBw)
cache_key = cache_level.lower()
graph_points[cache_key].extend([[x1, peak_x], [y1, peak_y], peak_bw])
# ----------------------------------------------------------------------------------
# Plot computing roof
# ----------------------------------------------------------------------------------
if dtype in PEAK_OPS_DATATYPES:
# Plot FMA roof
x0 = XMAX
if x2 < x0:
x0 = x2
x0 = min(x2, XMAX) if x2 < XMAX else XMAX
console_debug("FMA ROOF [{}, {}], [{},{}]".format(x0, XMAX, peakOps, peakOps))
graphPoints["valu"].append([x0, XMAX])
graphPoints["valu"].append([peakOps, peakOps])
graphPoints["valu"].append(peakOps)
console_debug(f"FMA ROOF [{x0}, {XMAX}], [{peak_ops},{peak_ops}]")
graph_points["valu"].extend([[x0, XMAX], [peak_ops, peak_ops], peak_ops])
# Plot MFMA roof
if dtype in MFMA_DATATYPES: # assert that mfma has been assigned
x0_mfma = XMAX
if x2_mfma < x0_mfma:
x0_mfma = x2_mfma
x0_mfma = min(x2_mfma, XMAX) if x2_mfma < XMAX else XMAX
console_debug(
"MFMA ROOF [{}, {}], [{},{}]".format(x0_mfma, XMAX, peakMFMA, peakMFMA)
)
graphPoints["mfma"].append([x0_mfma, XMAX])
graphPoints["mfma"].append([peakMFMA, peakMFMA])
graphPoints["mfma"].append(peakMFMA)
console_debug(f"MFMA ROOF [{x0_mfma}, {XMAX}], [{peak_mfma},{peak_mfma}]")
graph_points["mfma"].extend([
[x0_mfma, XMAX],
[peak_mfma, peak_mfma],
peak_mfma,
])
return graphPoints
return graph_points
# -------------------------------------------------------------------------------------
# Overlay application performance
# -------------------------------------------------------------------------------------
# Calculate relevant metrics for ai calculation
def calc_ai_analyze(workload, mspec, sort_type, config, arch_config):
def calc_ai_analyze(
workload: schema.Workload,
mspec: MachineSpecs,
sort_type: str,
config: dict[str, Any],
arch_config: schema.ArchConfig,
) -> dict[str, Union[list[list[float]], list[str]]]:
"""
Calculate per-kernel metrics and AI points with Roofline yamls using eval_metric.
"""
console_debug("calc_ai_analyze: Starting calc_ai analysis using Roofline yamls")
plot_points = {
"ai_l1": [[], []],
"ai_l2": [[], []],
"ai_hbm": [[], []],
"kernelNames": [],
}
console_debug("calc_ai_analyze", "Starting calc_ai analysis using Roofline yamls")
plot_points = PlotPoints.empty()
workload.roofline_metrics = {}
filtered_pmc = apply_filters(workload, workload.path, is_gui=False, debug=False)
kernel_ids_to_process = []
kernel_ids_to_process: list[int] = []
kernel_top_table_id = 1
if workload.filter_kernel_ids:
kernel_ids_to_process = workload.filter_kernel_ids
else:
if kernel_top_table_id in workload.dfs:
kernel_top_df = workload.dfs[kernel_top_table_id]
kernel_ids_to_process = kernel_top_df.index.tolist()
console_debug(
"roofline", f"Found {len(kernel_ids_to_process)} kernels to process"
)
elif kernel_top_table_id in workload.dfs:
kernel_top_df = workload.dfs[kernel_top_table_id]
kernel_ids_to_process = kernel_top_df.index.tolist()
console_debug(
"roofline", f"Found {len(kernel_ids_to_process)} kernels to process"
)
if not kernel_ids_to_process:
console_warning("No kernels found to process for roofline")
return plot_points
return plot_points.__dict__
for kernel_id in kernel_ids_to_process:
kernel_name = ""
if kernel_top_table_id in workload.dfs:
kernel_top_df = workload.dfs[kernel_top_table_id]
if kernel_id in kernel_top_df.index:
kernel_name = kernel_top_df.loc[kernel_id, "Kernel_Name"]
else:
if kernel_id not in kernel_top_df.index:
continue
kernel_name = kernel_top_df.loc[kernel_id, "Kernel_Name"]
else:
continue
@@ -314,8 +360,8 @@ def calc_ai_analyze(workload, mspec, sort_type, config, arch_config):
kernel_only_data = {"pmc_perf": kernel_pmc_df["pmc_perf"]}
kernel_dfs = {}
kernel_dfs_type = {}
kernel_dfs: dict[int, pd.DataFrame] = {}
kernel_dfs_type: dict[int, str] = {}
for table_id in [401, 402]:
if table_id in arch_config.dfs:
@@ -368,16 +414,16 @@ def calc_ai_analyze(workload, mspec, sort_type, config, arch_config):
# add to plot points if we have valid data
if performance > 0:
if ai_hbm > 0:
plot_points["ai_hbm"][0].append(ai_hbm)
plot_points["ai_hbm"][1].append(performance)
plot_points.ai_hbm[0].append(ai_hbm)
plot_points.ai_hbm[1].append(performance)
if ai_l2 > 0:
plot_points["ai_l2"][0].append(ai_l2)
plot_points["ai_l2"][1].append(performance)
plot_points.ai_l2[0].append(ai_l2)
plot_points.ai_l2[1].append(performance)
if ai_l1 > 0:
plot_points["ai_l1"][0].append(ai_l1)
plot_points["ai_l1"][1].append(performance)
plot_points.ai_l1[0].append(ai_l1)
plot_points.ai_l1[1].append(performance)
plot_points["kernelNames"].append(f"K{kernel_id}")
plot_points.kernelNames.append(f"K{kernel_id}")
console_debug("roofline", f"Added kernel {kernel_id} to plot points")
else:
console_debug(
@@ -391,14 +437,14 @@ def calc_ai_analyze(workload, mspec, sort_type, config, arch_config):
"calc_table": kernel_dfs.get(402, pd.DataFrame()),
}
console_debug(
"roofline", f"Generated {len(plot_points['kernelNames'])} plot points"
)
console_debug("roofline", f"Generated {len(plot_points.kernelNames)} plot points")
console_debug("roofline", f"Plot points: {plot_points}")
return plot_points
return plot_points.__dict__
def calc_ai_profile(mspec, sort_type, ret_df):
def calc_ai_profile(
mspec: MachineSpecs, sort_type: str, ret_df: dict[str, pd.DataFrame]
) -> dict[str, Union[list[list[float]], list[str]]]:
"""Given counter data, calculate arithmetic intensity for each kernel
in the application. Leverage hard-coded equations to calculate AI values.
@@ -410,8 +456,7 @@ def calc_ai_profile(mspec, sort_type, ret_df):
)
df = ret_df["pmc_perf"]
# Sort by top kernels or top dispatches?
df = df.sort_values(by=["Kernel_Name"])
df = df.reset_index(drop=True)
df = df.sort_values(by=["Kernel_Name"]).reset_index(drop=True)
total_flops = valu_flops = mfma_flops_f6f4 = mfma_flops_f8 = mfma_flops_bf16 = (
mfma_flops_f16
@@ -419,25 +464,24 @@ def calc_ai_profile(mspec, sort_type, ret_df):
L2cache_data
) = hbm_data = calls = totalDuration = avgDuration = 0.0
kernelName = ""
kernel_name = ""
my_list: list[AI_Data] = []
myList = []
at_end = False
next_kernelName = ""
supported_dt = SUPPORTED_DATATYPES[mspec.gpu_arch]
supported_dt = (
SUPPORTED_DATATYPES[mspec.gpu_arch]
if mspec.gpu_arch in SUPPORTED_DATATYPES
else None
)
for idx in df.index:
# CASE: Top kernels
# Calculate + append AI data if
# a) current KernelName is different than previous OR
# b) We've reached the end of list
if idx + 1 == df.shape[0]:
at_end = True
else:
next_kernelName = df["Kernel_Name"][idx + 1]
at_end = idx + 1 == df.shape[0]
next_kernel_name = df["Kernel_Name"][idx + 1] if not at_end else ""
kernel_name = df["Kernel_Name"][idx]
kernelName = df["Kernel_Name"][idx]
try:
total_flops += (
(
@@ -476,10 +520,10 @@ def calc_ai_profile(mspec, sort_type, ret_df):
total_flops += df["SQ_INSTS_VALU_MFMA_MOPS_F8"][idx] * 512
if ("FP4" in supported_dt) or ("FP6" in supported_dt):
total_flops += df["SQ_INSTS_VALU_MFMA_MOPS_F6F4"][idx] * 512
except KeyError:
except KeyError as e:
console_debug(
"roofline",
"{}: Skipped total_flops at index {}".format(kernelName[:35], idx),
f"{kernel_name[:35]}: Skipped total_flops at index {idx} due to {e}",
)
pass
try:
@@ -506,10 +550,10 @@ def calc_ai_profile(mspec, sort_type, ret_df):
+ df["SQ_INSTS_VALU_TRANS_F64"][idx]
)
)
except KeyError:
except KeyError as e:
console_debug(
"roofline",
"{}: Skipped valu_flops at index {}".format(kernelName[:35], idx),
f"{kernel_name[:35]}: Skipped valu_flops at index {idx} due to {e}",
)
pass
@@ -523,10 +567,10 @@ def calc_ai_profile(mspec, sort_type, ret_df):
mfma_flops_f32 += df["SQ_INSTS_VALU_MFMA_MOPS_F32"][idx] * 512
mfma_flops_f64 += df["SQ_INSTS_VALU_MFMA_MOPS_F64"][idx] * 512
mfma_iops_i8 += df["SQ_INSTS_VALU_MFMA_MOPS_I8"][idx] * 512
except KeyError:
except KeyError as e:
console_debug(
"roofline",
"{}: Skipped mfma ops at index {}".format(kernelName[:35], idx),
f"{kernel_name[:35]}: Skipped mfma ops at index {idx} due to {e}",
)
pass
@@ -536,19 +580,19 @@ def calc_ai_profile(mspec, sort_type, ret_df):
* 4
* (mspec.lds_banks_per_cu)
)
except KeyError:
except KeyError as e:
console_debug(
"roofline",
"{}: Skipped lds_data at index {}".format(kernelName[:35], idx),
f"{kernel_name[:35]}: Skipped lds_data at index {idx} due to {e}",
)
pass
try:
L1cache_data += df["TCP_TOTAL_CACHE_ACCESSES_sum"][idx] * 64
except KeyError:
except KeyError as e:
console_debug(
"roofline",
"{}: Skipped L1cache_data at index {}".format(kernelName[:35], idx),
f"{kernel_name[:35]}: Skipped L1cache_data at index {idx} due to {e}",
)
pass
@@ -559,10 +603,10 @@ def calc_ai_profile(mspec, sort_type, ret_df):
+ df["TCP_TCC_ATOMIC_WITHOUT_RET_REQ_sum"][idx] * 64
+ df["TCP_TCC_READ_REQ_sum"][idx] * 64
)
except KeyError:
except KeyError as e:
console_debug(
"roofline",
"{}: Skipped L2cache_data at index {}".format(kernelName[:35], idx),
f"{kernel_name[:35]}: Skipped L2cache_data at index {idx} due to {e}",
)
pass
try:
@@ -602,22 +646,21 @@ def calc_ai_profile(mspec, sort_type, ret_df):
)
+ (df["TCC_EA0_WRREQ_64B_sum"][idx] * 64)
)
except KeyError:
except KeyError as e:
console_debug(
"roofline",
"{}: Skipped hbm_data at index {}".format(kernelName[:35], idx),
f"{kernel_name[:35]}: Skipped hbm_data at index {idx} due to {e}",
)
pass
totalDuration += df["End_Timestamp"][idx] - df["Start_Timestamp"][idx]
avgDuration += df["End_Timestamp"][idx] - df["Start_Timestamp"][idx]
calls += 1
if sort_type == "kernels" and (at_end or (kernelName != next_kernelName)):
myList.append(
if sort_type == "kernels" and (at_end or (kernel_name != next_kernel_name)):
my_list.append(
AI_Data(
kernelName,
kernel_name,
calls,
total_flops / calls,
valu_flops / calls,
@@ -636,11 +679,8 @@ def calc_ai_profile(mspec, sort_type, ret_df):
avgDuration / calls,
)
)
console_debug(
"Just added {} to AI_Data at index {}. # of calls: {}".format(
kernelName, idx, calls
)
)
console_debug(f"Just added {kernel_name} to AI_Data. # of calls: {calls}")
total_flops = valu_flops = mfma_flops_f6f4 = mfma_flops_f8 = (
mfma_flops_bf16
) = mfma_flops_f16 = mfma_iops_i8 = mfma_flops_f32 = mfma_flops_f64 = (
@@ -650,9 +690,9 @@ def calc_ai_profile(mspec, sort_type, ret_df):
) = 0.0
if sort_type == "dispatches":
myList.append(
my_list.append(
AI_Data(
kernelName,
kernel_name,
calls,
total_flops,
valu_flops,
@@ -679,73 +719,64 @@ def calc_ai_profile(mspec, sort_type, ret_df):
avgDuration
) = 0.0
myList.sort(key=lambda x: x.totalDuration, reverse=True)
my_list.sort(key=lambda x: x.totalDuration, reverse=True)
intensities = {"ai_l1": [], "ai_l2": [], "ai_hbm": []}
curr_perf = []
kernelNames = []
i = 0
# Create list of top 5 intensities
while i < TOP_N and i != len(myList):
if myList[i].total_flops == 0:
intensities: dict[str, list[float]] = {"ai_l1": [], "ai_l2": [], "ai_hbm": []}
curr_perf: list[float] = []
kernel_names: list[str] = []
# Create list of top N intensities
for i in range(min(TOP_N, len(my_list))):
kernel_data = my_list[i]
if my_list[i].total_flops == 0:
console_debug(
f"No flops counted for {myList[i].KernelName}, "
f"No flops counted for {my_list[i].KernelName}, "
"arithmetic intensities will not display on plots."
)
kernelNames.append(myList[i].KernelName)
(
intensities["ai_l1"].append(myList[i].total_flops / myList[i].L1cache_data)
if myList[i].L1cache_data
else intensities["ai_l1"].append(0)
kernel_names.append(my_list[i].KernelName)
# Calculate arithmetic intensities
intensities["ai_l1"].append(
kernel_data.total_flops / kernel_data.L1cache_data
if kernel_data.L1cache_data
else 0
)
# print("cur_ai_L1", myList[i].total_flops/myList[i].L1cache_data) if myList[i].L1cache_data else print("null") #noqa
# print()
(
intensities["ai_l2"].append(myList[i].total_flops / myList[i].L2cache_data)
if myList[i].L2cache_data
else intensities["ai_l2"].append(0)
intensities["ai_l2"].append(
kernel_data.total_flops / kernel_data.L2cache_data
if kernel_data.L2cache_data
else 0
)
# print("cur_ai_L2", myList[i].total_flops/myList[i].L2cache_data) if myList[i].L2cache_data else print("null") #noqa
# print()
(
intensities["ai_hbm"].append(myList[i].total_flops / myList[i].hbm_data)
if myList[i].hbm_data
else intensities["ai_hbm"].append(0)
intensities["ai_hbm"].append(
kernel_data.total_flops / kernel_data.hbm_data
if kernel_data.hbm_data
else 0
)
# print("cur_ai_hbm", myList[i].total_flops/myList[i].hbm_data) if myList[i].hbm_data else print("null") #noqa
# print()
(
curr_perf.append(myList[i].total_flops / myList[i].avgDuration)
if myList[i].avgDuration
else curr_perf.append(0)
curr_perf.append(
kernel_data.total_flops / kernel_data.avgDuration
if kernel_data.avgDuration
else 0
)
# print("cur_perf", myList[i].total_flops/myList[i].avgDuration) if myList[i].avgDuration else print("null") #noqa
i += 1
# Create intensity points for plotting
intensity_points: dict[str, Union[list[list[float]], list[str]]] = {}
intensityPoints = {"ai_l1": [], "ai_l2": [], "ai_hbm": []}
for ai_type in intensities:
values = intensities[ai_type]
for i in intensities:
values = intensities[i]
x = values
y = curr_perf[: len(values)]
intensity_points[ai_type] = [x, y]
color = get_color(i) # noqa
x = []
y = []
for entryIndx in range(0, len(values)):
x.append(values[entryIndx])
y.append(curr_perf[entryIndx])
intensityPoints[i].append(x)
intensityPoints[i].append(y)
# Add an entry for kernel names
intensityPoints["kernelNames"] = kernelNames
return intensityPoints
# Add kernel names
intensity_points["kernelNames"] = kernel_names
return intensity_points
def constuct_roof(roofline_parameters, dtype):
def construct_roof(
roofline_parameters: dict[str, Any], dtype: str
) -> dict[str, list[Union[list[float], float, None]]]:
workload_dir = roofline_parameters.get("workload_dir")
if isinstance(workload_dir, list):
base_dir = (
@@ -756,44 +787,35 @@ def constuct_roof(roofline_parameters, dtype):
else:
base_dir = workload_dir
benchmark_results = str(Path(base_dir) / "roofline.csv")
benchmark_results = Path(base_dir) / "roofline.csv"
# -----------------------------------------------------
# Initialize roofline data dictionary from roofline.csv
# -----------------------------------------------------
# TODO: consider changing this to an ordered dict for consistency over py versions
benchmark_data = {}
headers = []
benchmark_data: dict[str, list[str]] = {}
headers: list[str] = []
try:
with open(benchmark_results, "r") as csvfile:
csvReader = csv.reader(csvfile, delimiter=",")
rowCount = 0
for row in csvReader:
with open(benchmark_results) as csvfile:
csv_reader = csv.reader(csvfile, delimiter=",")
row_count = 0
for row in csv_reader:
row.pop(0) # remove devID
if rowCount == 0:
if row_count == 0:
headers = row
for i in headers:
benchmark_data[i] = []
for header in headers:
benchmark_data[header] = []
else:
for i, key in enumerate(headers):
benchmark_data[key].append(row[i])
rowCount += 1
csvfile.close()
except Exception:
graphPoints = {
"hbm": [None, None, None],
"l2": [None, None, None],
"l1": [None, None, None],
"lds": [None, None, None],
"valu": [None, None, None],
"mfma": [None, None, None],
}
return graphPoints
row_count += 1
except Exception as e:
console_debug("roofline", f"Failed to read benchmark results: {e}")
return GraphPoints.empty().__dict__
# ------------------
# Generate Roofline
# ------------------
results = calc_ceilings(roofline_parameters, dtype, benchmark_data)
return results
return calc_ceilings(roofline_parameters, dtype, benchmark_data)
@@ -30,7 +30,7 @@
from collections import OrderedDict
from dataclasses import dataclass, field
from typing import Dict, List
from typing import Any
import pandas as pd
@@ -38,10 +38,10 @@ import pandas as pd
@dataclass
class ArchConfig:
# [id: panel_config] pairs
panel_configs: OrderedDict = field(default=dict)
panel_configs: OrderedDict[int, Any] = field(default_factory=OrderedDict)
# [id: df] pairs
dfs: Dict[int, pd.DataFrame] = field(default_factory=dict)
dfs: dict[int, pd.DataFrame] = field(default_factory=dict)
# NB:
# dfs_type should be a meta info embeded into df.
@@ -49,30 +49,34 @@ class ArchConfig:
# So do it as below for now.
# [id: df_type] pairs
dfs_type: Dict[int, str] = field(default_factory=dict)
dfs_type: dict[int, str] = field(default_factory=dict)
# [Index: Metric name] pairs
metric_list: Dict[str, str] = field(default_factory=dict)
metric_list: dict[str, str] = field(default_factory=dict)
# [Metric name: Counters] pairs
metric_counters: Dict[str, list] = field(default_factory=dict)
metric_counters: dict[str, list] = field(default_factory=dict)
@dataclass
class Workload:
sys_info: pd.DataFrame = None
raw_pmc: pd.DataFrame = None
dfs: Dict[int, pd.DataFrame] = field(default_factory=dict)
dfs_type: Dict[int, str] = field(default_factory=dict)
filter_kernel_ids: List[int] = field(default_factory=list)
filter_gpu_ids: List[int] = field(default_factory=list)
filter_dispatch_ids: List[int] = field(default_factory=list)
filter_nodes: List[str] = field(default_factory=list)
avail_ips: List[int] = field(default_factory=list)
sys_info: pd.DataFrame = field(default_factory=pd.DataFrame)
raw_pmc: pd.DataFrame = field(default_factory=pd.DataFrame)
dfs: dict[int, pd.DataFrame] = field(default_factory=dict)
dfs_type: dict[int, str] = field(default_factory=dict)
filter_kernel_ids: list[int] = field(default_factory=list)
filter_gpu_ids: list[int] = field(default_factory=list)
filter_dispatch_ids: list[int] = field(default_factory=list)
filter_nodes: list[str] = field(default_factory=list)
avail_ips: list[int] = field(default_factory=list)
roofline_peaks: pd.DataFrame = field(default_factory=pd.DataFrame)
roofline_metrics: dict[int, dict[str, Any]] = field(default_factory=dict)
path: str = field(default_factory=str)
filter_top_n: str = field(default_factory=str)
# Metrics will be calculated ONLY when the header(key) is in below list
supported_field = [
SUPPORTED_FIELD = [
"Value",
"Minimum",
"Maximum",
@@ -121,4 +125,4 @@ supported_field = [
]
# The prefix of raw pmc_perf.csv
pmc_perf_file_prefix = "pmc_perf"
PMC_PERF_FILE_PREFIX = "pmc_perf"
File diff suppressed because it is too large Load Diff
+416 -408
View File
@@ -23,41 +23,46 @@
##############################################################################
import argparse
import copy
import textwrap
from pathlib import Path
from typing import Any, Optional, TextIO
import pandas as pd
from tabulate import tabulate
import config
from utils import mem_chart, parser
from utils import mem_chart, parser, schema
from utils.kernel_name_shortener import kernel_name_shortener
from utils.logger import console_error, console_log, console_warning
from utils.utils import convert_metric_id_to_panel_info, get_uuid
def string_multiple_lines(source, width, max_rows):
def string_multiple_lines(source: str, width: int, max_rows: int) -> str:
"""
Adjust string with multiple lines by inserting '\n'
"""
idx = 0
lines = []
while idx < len(source) and len(lines) < max_rows:
lines.append(source[idx : idx + width])
idx += width
lines: list[str] = []
for i in range(0, len(source), width):
if len(lines) >= max_rows:
break
lines.append(source[i : i + width])
if len(lines) == max_rows and len(source) > max_rows * width:
lines[-1] = lines[-1][:-3] + "..."
if idx < len(source):
last = lines[-1]
lines[-1] = last[0:-3] + "..."
return "\n".join(lines)
def get_table_string(df, transpose=False, decimal=2):
def get_table_string(
df: pd.DataFrame, transpose: bool = False, decimal: int = 2
) -> str:
"""
Convert DataFrame to a formatted table string, wrapping specified columns.
"""
df_to_show = df.transpose() if transpose else df
wrap_columns = ["Description"]
wrap_width = 40
for col in wrap_columns:
@@ -67,42 +72,40 @@ def get_table_string(df, transpose=False, decimal=2):
.astype(str)
.apply(lambda x: textwrap.fill(x, width=wrap_width))
)
df_with_index = df_to_show.reset_index()
return tabulate(
df_to_show,
headers="keys",
df_with_index.values,
headers=list(df_with_index.columns),
tablefmt="fancy_grid",
floatfmt="." + str(decimal) + "f",
floatfmt=f".{decimal}f",
)
def convert_time_columns(df, time_unit):
def convert_time_columns(df: pd.DataFrame, time_unit: str) -> pd.DataFrame:
"""
Convert time column values based on the specified time unit.
Uses the Unit column to identify which columns contain time data.
"""
if time_unit not in config.TIME_UNITS or "Unit" not in df.columns:
return df
# Avoid modifying the original
df_copy = df.copy()
time_rows = df_copy["Unit"].str.lower().str.contains("ns", na=False)
time_value_columns = ["Avg", "Min", "Max"]
for col in time_value_columns:
if col in df_copy.columns:
mask = time_rows
if mask.any():
try:
numeric_values = pd.to_numeric(
df_copy.loc[mask, col], errors="coerce"
)
df_copy.loc[mask, col] = (
numeric_values / config.TIME_UNITS[time_unit]
)
except Exception:
pass
if col in df_copy.columns and time_rows.any():
try:
numeric_values = pd.to_numeric(
df_copy.loc[time_rows, col], errors="coerce"
)
df_copy.loc[time_rows, col] = (
numeric_values / config.TIME_UNITS[time_unit]
)
except Exception:
pass
# Update the Unit column
if time_rows.any():
@@ -111,31 +114,341 @@ def convert_time_columns(df, time_unit):
return df_copy
def has_time_data(df):
def has_time_data(df: pd.DataFrame) -> bool:
"""
Check if the dataframe contains time data by looking at the Unit column.
"""
if "Unit" not in df.columns:
return False
# NOTE: "ns" / "NS" / "nS" / "Ns" are reserved for Nanosec time unit
return df["Unit"].str.lower().str.contains("ns", na=False).any()
return bool(df["Unit"].str.lower().str.contains("ns", na=False).any())
def show_all(args, runs, archConfigs, output, profiling_config, roof_plot=None):
def is_roofline_shown(
args: argparse.Namespace,
runs: dict[str, Any],
output: Optional[TextIO],
panel: dict[str, Any],
roof_plot: Optional[str],
hidden_cols: list[str],
) -> bool:
has_roofline_style = any(
data_source.get(table_type, {}).get("cli_style") == "Roofline"
for data_source in panel["data source"]
for table_type in data_source
)
if not has_roofline_style or (
args.filter_metrics and "4" not in args.filter_metrics
):
return False
print(f"\n{'=' * 80}", file=output)
print("4. Roofline", file=output)
print("=" * 80, file=output)
# Display roofline metrics for each run
for run_path, workload in runs.items():
if hasattr(workload, "roofline_metrics") and workload.roofline_metrics:
print(
"\n(4.1) Per-Kernel Roofline Metrics and (4.2) AI Plot Points",
file=output,
)
print("-" * 80, file=output)
kernel_top_df = workload.dfs.get(1, pd.DataFrame())
if not kernel_top_df.empty:
kernel_name_shortener(kernel_top_df, args.kernel_verbose)
# Display roofline metrics
for kernel_id, metrics in workload.roofline_metrics.items():
if not kernel_top_df.empty and kernel_id in kernel_top_df.index:
kernel_name = kernel_top_df.loc[kernel_id, "Kernel_Name"]
kernel_pct = (
kernel_top_df.loc[kernel_id, "Pct"]
if "Pct" in kernel_top_df.columns
else 0
)
else:
kernel_name = metrics.get("name", f"Kernel {kernel_id}")
kernel_pct = 0
display_name = (
kernel_name[:80] + "..." if len(kernel_name) > 80 else kernel_name
)
print(
f"\nKernel {kernel_id}: {display_name} ({kernel_pct:.1f}%)",
file=output,
)
base_indent = " "
table_indent_prefix = f"{base_indent}| "
print(f"{base_indent}|", file=output)
tables = {
401: (
"4.1 Roofline Rate Metrics:",
metrics.get("ai_table", pd.DataFrame()),
),
402: (
"4.2 Roofline AI Plot Points:",
metrics.get("calc_table", pd.DataFrame()),
),
}
for table_id, (table_name, df) in tables.items():
if df.empty:
continue
print(f"{base_indent}├─ {table_name}", file=output)
# Remove hidden columns
display_df = df.copy()
for col in hidden_cols:
if col in display_df.columns:
display_df = display_df.drop(columns=[col])
table_string = get_table_string(
display_df, transpose=False, decimal=args.decimal
)
indented_table = textwrap.indent(table_string, table_indent_prefix)
print(indented_table, file=output)
else:
print("\nNo per-kernel metrics available", file=output)
# Show the roofline plot
if roof_plot:
show_roof_plot(roof_plot)
return True
def process_table_data(
args: argparse.Namespace,
runs: dict[str, Any],
table_config: dict[str, Any],
table_type: str,
comparable_columns: list[str],
hidden_cols: list[str],
) -> pd.DataFrame:
# take the 1st run as baseline
base_run, base_data = next(iter(runs.items()))
base_df = base_data.dfs[table_config["id"]]
if args.time_unit and has_time_data(base_df):
base_df = convert_time_columns(base_df, args.time_unit)
result_df = pd.DataFrame(index=base_df.index)
for header in base_df.columns:
# Skip filtered columns
if (
table_type != "raw_csv_table"
and args.cols
and base_df.columns.get_loc(header) not in args.cols
):
continue
if header in hidden_cols:
continue
if header not in comparable_columns:
# Process columns that are not comparable across runs.
if (
table_type == "raw_csv_table"
and table_config["source"]
in ["pmc_kernel_top.csv", "pmc_dispatch_info.csv"]
and header == "Kernel_Name"
):
# NB: the width of kernel name might depend
# on the header of the table.
width = 40 if table_config["source"] == "pmc_kernel_top.csv" else 80
max_rows = 3 if table_config["source"] == "pmc_kernel_top.csv" else 4
adjusted_names = base_df["Kernel_Name"].apply(
lambda x: string_multiple_lines(x, width, max_rows)
)
result_df = pd.concat([result_df, adjusted_names], axis=1)
elif table_type == "raw_csv_table" and header == "Info":
for run_data in runs.values():
cur_df = run_data.dfs[table_config["id"]]
result_df = pd.concat([result_df, cur_df[header]], axis=1)
else:
result_df = pd.concat([result_df, base_df[header]], axis=1)
else:
# Process columns that can be compared across runs.
for run_name, run_data in runs.items():
cur_df = run_data.dfs[table_config["id"]]
if args.time_unit and has_time_data(base_df):
cur_df = convert_time_columns(cur_df, args.time_unit)
if (table_type == "raw_csv_table") or (
table_type == "metric_table" and header not in hidden_cols
):
if run_name != base_run:
# Calculate percentage difference between current and
# base dataframe.
base_series = pd.to_numeric(
base_df[header], errors="coerce"
).fillna(0.0)
cur_series = pd.to_numeric(
cur_df[header], errors="coerce"
).fillna(0.0)
# Calculate absolute and percentage differences
absolute_diff = (cur_series - base_series).round(args.decimal)
percentage_diff = (
absolute_diff / base_series.replace(0, 1) * 100
).round(args.decimal)
if args.verbose >= 2:
console_log("---------", header, percentage_diff)
# Format as "value (percentage%)"
formatted_diff = (
cur_series.round(args.decimal).astype(str)
+ " ("
+ percentage_diff.astype(str)
+ "%)"
)
result_df = pd.concat([result_df, formatted_diff], axis=1)
# DEBUG: When in a CI setting and flag is set,
# then verify metrics meet threshold
# requirement
if (
header in ["Value", "Count", "Avg"]
and percentage_diff.abs().gt(args.report_diff).any()
):
result_df["Abs Diff"] = absolute_diff
if args.report_diff:
violation_idx = percentage_diff.index[
percentage_diff.abs() > args.report_diff
]
console_warning(
f"Dataframe diff exceeds {args.report_diff}% "
"threshold requirement\n"
f"See metric {violation_idx.to_numpy()}"
)
console_warning(result_df)
else:
# Base run - just add the rounded values
cur_df_copy = copy.deepcopy(cur_df)
cur_df_copy[header] = [
(round(float(x), args.decimal) if x != "" else x)
for x in base_df[header]
]
result_df = pd.concat([result_df, cur_df_copy[header]], axis=1)
return result_df
def format_table_output(
args: argparse.Namespace,
table_config: dict[str, Any],
df: pd.DataFrame,
table_type: str,
runs: dict[str, Any],
csv_dir: Optional[Path] = None,
) -> str:
"""Format table for output, handling special cases and saving to files if needed."""
table_id_str = f"{table_config['id'] // 100}.{table_config['id'] % 100}"
content = ""
# Check if any column in df is empty
is_empty_columns_exist = any(
df.replace("", None).iloc[:, col_idx].isnull().all()
for col_idx in range(len(df.columns))
)
# Do not print the table if any column is empty
if is_empty_columns_exist:
title = table_config.get("title", "")
console_log(f"Not showing table with empty column(s): {table_id_str} {title}")
return content
if "title" in table_config and table_config["title"]:
content += f"{table_id_str} {table_config['title']}\n"
if args.output_format == "csv" and csv_dir and csv_dir.is_dir():
if "title" in table_config and table_config["title"]:
table_id_str += f"_{table_config['title']}"
csv_filename = csv_dir / f"{table_id_str.replace(' ', '_')}.csv"
df.to_csv(csv_filename, index=False)
console_warning(f"Created file: {csv_filename}")
# Only show top N kernels (as specified in --max-kernel-num)
# in "Top Stats" section
if table_type == "raw_csv_table" and table_config["source"] in [
"pmc_kernel_top.csv",
"pmc_dispatch_info.csv",
]:
df = df.head(args.max_stat_num)
# NB:
# "columnwise: True" is a special attr of a table/df
# For raw_csv_table, such as system_info, we transpose the
# df when load it, because we need those items in column.
# For metric_table, we only need to show the data in column
# fash for now.
transpose = table_type != "raw_csv_table" and table_config.get("columnwise", False)
# enable mem_chart only with single run
if (
table_config.get("cli_style") == "mem_chart"
and len(runs) == 1
and "Metric" in df.columns
and "Value" in df.columns
):
mem_data = (
pd.DataFrame([df["Metric"], df["Value"]])
.transpose()
.set_index("Metric")
.to_dict()["Value"]
)
content += mem_chart.plot_mem_chart("", args.normal_unit, mem_data) + "\n"
else:
content += (
get_table_string(df, transpose=transpose, decimal=args.decimal) + "\n"
)
return content
def show_all(
args: argparse.Namespace,
runs: dict[str, Any],
arch_configs: schema.ArchConfig,
output: Optional[TextIO],
profiling_config: dict[str, Any],
roof_plot: Optional[str] = None,
) -> None:
"""
Show all panels with their data in plain text mode.
"""
comparable_columns = parser.build_comparable_columns(args.time_unit)
filter_panel_ids = profiling_config.get("filter_blocks", [])
csv_dir = None
if isinstance(filter_panel_ids, dict):
# For backward compatibility
filter_panel_ids = [
name for name, type in filter_panel_ids.items() if type == "metric_id"
name
for name, table_type in filter_panel_ids.items()
if table_type == "metric_id"
]
filter_panel_ids = [
int(convert_metric_id_to_panel_info(metric_id)[0])
int(result[0])
for metric_id in filter_panel_ids
if (result := convert_metric_id_to_panel_info(metric_id)) is not None
]
if args.include_cols:
hidden_cols = list(set(config.HIDDEN_COLUMNS_CLI) - set(args.include_cols))
else:
@@ -149,119 +462,19 @@ def show_all(args, runs, archConfigs, output, profiling_config, roof_plot=None):
if not csv_dir.exists():
csv_dir.mkdir()
for panel_id, panel in archConfigs.panel_configs.items():
for panel_id, panel in arch_configs.panel_configs.items():
# Skip panels that don't support baseline comparison
if len(args.path) > 1 and panel_id in config.HIDDEN_SECTIONS:
continue
ss = "" # store content of all data_source from one panel
panel_content = "" # store content of all data_source from one panel
if panel_id == 400:
has_roofline_style = any(
data_source.get(type, {}).get("cli_style") == "Roofline"
for data_source in panel["data source"]
for type in data_source
)
if has_roofline_style and (
not args.filter_metrics or "4" in args.filter_metrics
):
print("\n" + "=" * 80, file=output)
print("4. Roofline", file=output)
print("=" * 80, file=output)
for run_path, workload in runs.items():
if (
hasattr(workload, "roofline_metrics")
and workload.roofline_metrics
):
print(
"\n(4.1) Per-Kernel Roofline Metrics and "
"(4.2) AI Plot Points",
file=output,
)
print("-" * 80, file=output)
kernel_top_df = workload.dfs.get(1, pd.DataFrame())
if not kernel_top_df.empty:
kernel_name_shortener(kernel_top_df, args.kernel_verbose)
for i, (kernel_id, metrics) in enumerate(
workload.roofline_metrics.items()
):
if (
not kernel_top_df.empty
and kernel_id in kernel_top_df.index
):
kernel_name = kernel_top_df.loc[
kernel_id, "Kernel_Name"
]
kernel_pct = (
kernel_top_df.loc[kernel_id, "Pct"]
if "Pct" in kernel_top_df.columns
else 0
)
else:
kernel_name = metrics.get("name", f"Kernel {kernel_id}")
kernel_pct = 0
display_name = (
kernel_name[:80] + "..."
if len(kernel_name) > 80
else kernel_name
)
print(
f"\nKernel {kernel_id}: "
f"{display_name} "
f"({kernel_pct:.1f}%)",
file=output,
)
base_indent = " "
table_indent_prefix = f"{base_indent}| "
tables = {
401: (
"4.1 Roofline Rate Metrics:",
metrics.get("ai_table", pd.DataFrame()),
),
402: (
"4.2 Roofline AI Plot Points:",
metrics.get("calc_table", pd.DataFrame()),
),
}
print(f"{base_indent}|")
for table_id, (table_name, df) in tables.items():
if df.empty:
continue
print(f"{base_indent}├─ {table_name}", file=output)
display_df = df.copy()
for col in hidden_cols:
if col in display_df.columns:
display_df = display_df.drop(columns=[col])
table_string = get_table_string(
display_df, transpose=False, decimal=args.decimal
)
indented_table_string = textwrap.indent(
table_string, table_indent_prefix
)
print(indented_table_string, file=output)
else:
print("\nNo per-kernel metrics available", file=output)
# Show the roofline plot
if roof_plot:
show_roof_plot(roof_plot)
if is_roofline_shown(args, runs, output, panel, roof_plot, hidden_cols):
continue
for data_source in panel["data source"]:
for type, table_config in data_source.items():
for table_type, table_config in data_source.items():
# If block filtering was used during analysis, then don't use profiling
# config. If block filtering was used in profiling config, only show
# those panels. If block filtering not used in profiling config, show
@@ -275,14 +488,12 @@ def show_all(args, runs, archConfigs, output, profiling_config, roof_plot=None):
and panel_id > 100
):
table_id_str = (
str(table_config["id"] // 100)
+ "."
+ str(table_config["id"] % 100)
f"{table_config['id'] // 100}.{table_config['id'] % 100}"
)
console_log(
f"Not showing table not selected during profiling: "
f"{table_id_str} "
f"{table_config['title']}"
f"{table_id_str} {table_config['title']}"
)
continue
@@ -290,273 +501,58 @@ def show_all(args, runs, archConfigs, output, profiling_config, roof_plot=None):
# We cannot guarantee that all runs have the same metrics.
# Only show common metrics.
if (
type == "metric_table"
table_type == "metric_table"
and "Metric" in table_config["header"].values()
and len(runs) > 1
):
# Common metrics across all runs
common_metrics = set()
for _, data in runs.items():
if not common_metrics:
common_metrics = set(data.dfs[table_config["id"]]["Metric"])
else:
common_metrics &= set(
data.dfs[table_config["id"]]["Metric"]
)
# Find common metrics across all runs
common_metrics: set[str] = set()
for run_data in runs.values():
run_metrics = set(run_data.dfs[table_config["id"]]["Metric"])
common_metrics = (
run_metrics
if not common_metrics
else common_metrics & run_metrics
)
# Apply common metrics across all runs
# Reindex all runs based on first run
initial_index = None
for key in runs.keys():
runs[key].dfs[table_config["id"]] = (
runs[key]
.dfs[table_config["id"]]
.loc[lambda d: d["Metric"].isin(common_metrics)]
)
for run_data in runs.values():
run_data.dfs[table_config["id"]] = run_data.dfs[
table_config["id"]
].loc[lambda df: df["Metric"].isin(common_metrics)]
if initial_index is None:
initial_index = runs[key].dfs[table_config["id"]].index
initial_index = run_data.dfs[table_config["id"]].index
else:
runs[key].dfs[table_config["id"]].index = initial_index
run_data.dfs[table_config["id"]].index = initial_index
# take the 1st run as baseline
base_run, base_data = next(iter(runs.items()))
base_df = base_data.dfs[table_config["id"]]
processed_df = process_table_data(
args,
runs,
table_config,
table_type,
comparable_columns,
hidden_cols,
)
if args.time_unit and has_time_data(base_df):
base_df = convert_time_columns(base_df, args.time_unit)
df = pd.DataFrame(index=base_df.index)
for header in list(base_df.keys()):
# For raw csv table, columns cannot be filtered
# If columns are filtered, then skip the headers not in
# filtered columns
if (
type == "raw_csv_table"
or not args.cols
or base_df.columns.get_loc(header) in args.cols
):
if header in hidden_cols:
pass
elif header not in comparable_columns:
if (
type == "raw_csv_table"
and (
table_config["source"] == "pmc_kernel_top.csv"
or table_config["source"] == "pmc_dispatch_info.csv"
)
and header == "Kernel_Name"
):
# NB: the width of kernel name might depend
# on the header of the table.
if table_config["source"] == "pmc_kernel_top.csv":
adjusted_name = base_df["Kernel_Name"].apply(
lambda x: string_multiple_lines(x, 40, 3)
)
else:
adjusted_name = base_df["Kernel_Name"].apply(
lambda x: string_multiple_lines(x, 80, 4)
)
df = pd.concat([df, adjusted_name], axis=1)
elif type == "raw_csv_table" and header == "Info":
for run, data in runs.items():
cur_df = data.dfs[table_config["id"]]
df = pd.concat([df, cur_df[header]], axis=1)
else:
df = pd.concat([df, base_df[header]], axis=1)
else:
for run, data in runs.items():
cur_df = data.dfs[table_config["id"]]
if args.time_unit and has_time_data(base_df):
cur_df = convert_time_columns(
cur_df, args.time_unit
)
if (type == "raw_csv_table") or (
type == "metric_table"
and (not header in hidden_cols)
):
if run != base_run:
# calc percentage over the baseline
base_df[header] = [
float(x) if x != "" else float(0)
for x in base_df[header]
]
cur_df[header] = [
float(x) if x != "" else float(0)
for x in cur_df[header]
]
t_df = pd.concat(
[
base_df[header],
cur_df[header],
],
axis=1,
)
absolute_diff = (
t_df.iloc[:, 1] - t_df.iloc[:, 0]
).round(args.decimal)
t_df = absolute_diff / t_df.iloc[:, 0].replace(
0, 1
)
if args.verbose >= 2:
console_log("---------", header, t_df)
t_df_pretty = (
t_df.astype(float)
.mul(100)
.round(args.decimal)
)
# show value + percentage
# TODO: better alignment
t_df = (
cur_df[header]
.astype(float)
.round(args.decimal)
.map(str)
.astype(str)
+ " ("
+ t_df_pretty.map(str)
+ "%)"
)
df = pd.concat([df, t_df], axis=1)
# DEBUG: When in a CI setting and flag is set,
# then verify metrics meet threshold
# requirement
if (
header in ["Value", "Count", "Avg"]
and t_df_pretty.abs()
.gt(args.report_diff)
.any()
):
df["Abs Diff"] = absolute_diff
if args.report_diff:
violation_idx = t_df_pretty.index[
t_df_pretty.abs() > args.report_diff
]
console_warning(
"Dataframe diff exceeds %s "
"threshold requirement\n"
"See metric %s"
% (
str(args.report_diff) + "%",
violation_idx.to_numpy(),
)
)
console_warning(df)
else:
cur_df_copy = copy.deepcopy(cur_df)
cur_df_copy[header] = [
(
round(float(x), args.decimal)
if x != ""
else x
)
for x in base_df[header]
]
df = pd.concat(
[df, cur_df_copy[header]], axis=1
)
if not df.empty:
# subtitle for each table in a panel if existing
table_id_str = (
str(table_config["id"] // 100)
+ "."
+ str(table_config["id"] % 100)
if not processed_df.empty:
panel_content += format_table_output(
args, table_config, processed_df, table_type, runs, csv_dir
)
# Check if any column in df is empty
is_empty_columns_exist = any([
df.columns[col_idx]
for col_idx in range(len(df.columns))
if df.replace("", None).iloc[:, col_idx].isnull().all()
])
# Do not print the table if any column is empty
if is_empty_columns_exist:
if "title" in table_config:
console_log(
f"Not showing table with empty column(s): "
f"{table_id_str} "
f"{table_config['title']}"
)
else:
console_log(
f"Not showing table with empty column(s): "
f"{table_id_str}"
)
if (
"title" in table_config
and table_config["title"]
and not is_empty_columns_exist
):
ss += table_id_str + " " + table_config["title"] + "\n"
if args.output_format == "csv" and csv_dir.is_dir():
if "title" in table_config and table_config["title"]:
table_id_str += "_" + table_config["title"]
csv_filename = str(
csv_dir.joinpath(table_id_str.replace(" ", "_") + ".csv"),
)
df.to_csv(csv_filename, index=False)
console_warning(f"Created file: {csv_filename}")
# Only show top N kernels (as specified in --max-kernel-num)
# in "Top Stats" section
if type == "raw_csv_table" and (
table_config["source"] == "pmc_kernel_top.csv"
or table_config["source"] == "pmc_dispatch_info.csv"
):
df = df.head(args.max_stat_num)
# NB:
# "columnwise: True" is a special attr of a table/df
# For raw_csv_table, such as system_info, we transpose the
# df when load it, because we need those items in column.
# For metric_table, we only need to show the data in column
# fash for now.
transpose = (
type != "raw_csv_table"
and "columnwise" in table_config
and table_config["columnwise"]
)
if not is_empty_columns_exist:
# enable mem_chart only with single run
if (
"cli_style" in table_config
and table_config["cli_style"] == "mem_chart"
and len(runs) == 1
):
# NB: to avoid broken test with
# arbitrary number with "--cols" option
if "Metric" in df.columns and "Value" in df.columns:
ss += mem_chart.plot_mem_chart(
"",
args.normal_unit,
pd.DataFrame([df["Metric"], df["Value"]])
.transpose()
.set_index("Metric")
.to_dict()["Value"],
)
ss += "\n"
else:
ss += (
get_table_string(
df, transpose=transpose, decimal=args.decimal
)
+ "\n"
)
if ss:
print("\n" + "-" * 80, file=output)
print(str(panel_id // 100) + ". " + panel["title"], file=output)
print(ss, file=output)
if panel_content:
print(f"\n{'-' * 80}", file=output)
print(f"{panel_id // 100}. {panel['title']}", file=output)
print(panel_content, file=output)
def show_roof_plot(roof_plot):
def show_roof_plot(roof_plot: str) -> None:
# TODO: short term solution to display roofline plot
print("\n" + "-" * 80)
print(f"\n{'-' * 80}")
print("4. Roofline")
print("4.3 Roofline Plot")
if roof_plot:
print(roof_plot)
else:
@@ -567,35 +563,47 @@ def show_roof_plot(roof_plot):
)
def show_kernel_stats(args, runs, archConfigs, output):
def show_kernel_stats(
args: argparse.Namespace,
runs: dict[str, Any],
arch_configs: schema.ArchConfig,
output: Optional[TextIO],
) -> None:
"""
Show the kernels and dispatches from "Top Stats" section.
"""
df = pd.DataFrame()
for panel_id, panel in archConfigs.panel_configs.items():
for panel_id, panel in arch_configs.panel_configs.items():
for data_source in panel["data source"]:
for type, table_config in data_source.items():
for table_type, table_config in data_source.items():
for run, data in runs.items():
df = pd.DataFrame()
single_df = data.dfs[table_config["id"]]
# NB:
# For pmc_kernel_top.csv, have to sort here if not
# sorted when load_table_data.
if table_config["id"] == 1:
print("\n" + "-" * 80, file=output)
print(f"\n{'-' * 80}", file=output)
print(
"Detected Kernels (sorted descending by duration)",
file=output,
)
df = pd.concat([df, single_df["Kernel_Name"]], axis=1)
display_df = pd.DataFrame()
display_df = pd.concat(
[display_df, single_df["Kernel_Name"]], axis=1
)
print(
get_table_string(
display_df, transpose=False, decimal=args.decimal
),
file=output,
)
if table_config["id"] == 2:
print("\n" + "-" * 80, file=output)
print(f"\n{'-' * 80}", file=output)
print("Dispatch list", file=output)
df = single_df
print(
get_table_string(df, transpose=False, decimal=args.decimal),
file=output,
)
print(
get_table_string(
single_df, transpose=False, decimal=args.decimal
),
file=output,
)
File diff suppressed because it is too large Load Diff
@@ -57,7 +57,7 @@ def test_valid_path(binary_handler_analyze_rocprof_compute):
])
assert code == 0
test_utils.clean_output_dir(config["cleanup"], workload_dir)
test_utils.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.misc
@@ -71,7 +71,7 @@ def test_list_kernels(binary_handler_analyze_rocprof_compute):
"--list-stats",
])
assert code == 0
test_utils.clean_output_dir(config["cleanup"], workload_dir)
test_utils.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.list_metrics
@@ -94,7 +94,7 @@ def test_list_metrics_gfx90a(binary_handler_analyze_rocprof_compute):
])
assert code == 0
test_utils.clean_output_dir(config["cleanup"], workload_dir)
test_utils.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.list_metrics
@@ -117,7 +117,7 @@ def test_list_metrics_gfx908(binary_handler_analyze_rocprof_compute):
])
assert code == 0
test_utils.clean_output_dir(config["cleanup"], workload_dir)
test_utils.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.list_metrics
@@ -213,7 +213,7 @@ def test_filter_block_1(binary_handler_analyze_rocprof_compute):
])
assert code == 0
test_utils.clean_output_dir(config["cleanup"], workload_dir)
test_utils.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.filter_block
@@ -229,7 +229,7 @@ def test_filter_block_2(binary_handler_analyze_rocprof_compute):
])
assert code == 0
test_utils.clean_output_dir(config["cleanup"], workload_dir)
test_utils.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.filter_block
@@ -245,7 +245,7 @@ def test_filter_block_3(binary_handler_analyze_rocprof_compute):
])
assert code == 0
test_utils.clean_output_dir(config["cleanup"], workload_dir)
test_utils.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.filter_block
@@ -261,7 +261,7 @@ def test_filter_block_4(binary_handler_analyze_rocprof_compute):
])
assert code == 0
test_utils.clean_output_dir(config["cleanup"], workload_dir)
test_utils.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.filter_block
@@ -277,7 +277,7 @@ def test_filter_block_5(binary_handler_analyze_rocprof_compute):
])
assert code == 0
test_utils.clean_output_dir(config["cleanup"], workload_dir)
test_utils.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.filter_block
@@ -293,7 +293,7 @@ def test_filter_block_6(binary_handler_analyze_rocprof_compute):
])
assert code == 0
test_utils.clean_output_dir(config["cleanup"], workload_dir)
test_utils.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.serial
@@ -309,7 +309,7 @@ def test_filter_kernel_1(binary_handler_analyze_rocprof_compute):
])
assert code == 0
test_utils.clean_output_dir(config["cleanup"], workload_dir)
test_utils.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.serial
@@ -325,7 +325,7 @@ def test_filter_kernel_2(binary_handler_analyze_rocprof_compute):
])
assert code == 1
test_utils.clean_output_dir(config["cleanup"], workload_dir)
test_utils.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.serial
@@ -342,7 +342,7 @@ def test_filter_kernel_3(binary_handler_analyze_rocprof_compute):
])
assert code == 1
test_utils.clean_output_dir(config["cleanup"], workload_dir)
test_utils.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.serial
@@ -358,7 +358,7 @@ def test_dispatch_1(binary_handler_analyze_rocprof_compute):
])
assert code == 0
test_utils.clean_output_dir(config["cleanup"], workload_dir)
test_utils.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.serial
@@ -374,7 +374,7 @@ def test_dispatch_2(binary_handler_analyze_rocprof_compute):
])
assert code == 0
test_utils.clean_output_dir(config["cleanup"], workload_dir)
test_utils.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.serial
@@ -390,7 +390,7 @@ def test_dispatch_3(binary_handler_analyze_rocprof_compute):
])
assert code == 0
test_utils.clean_output_dir(config["cleanup"], workload_dir)
test_utils.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.serial
@@ -407,7 +407,7 @@ def test_dispatch_4(binary_handler_analyze_rocprof_compute):
])
assert code == 1
test_utils.clean_output_dir(config["cleanup"], workload_dir)
test_utils.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.serial
@@ -424,7 +424,7 @@ def test_dispatch_5(binary_handler_analyze_rocprof_compute):
])
assert code == 1
test_utils.clean_output_dir(config["cleanup"], workload_dir)
test_utils.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.misc
@@ -444,7 +444,7 @@ def test_gpu_ids(binary_handler_analyze_rocprof_compute):
])
assert code == 0
test_utils.clean_output_dir(config["cleanup"], workload_dir)
test_utils.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.normal_unit
@@ -460,7 +460,7 @@ def test_normal_unit_per_wave(binary_handler_analyze_rocprof_compute):
])
assert code == 0
test_utils.clean_output_dir(config["cleanup"], workload_dir)
test_utils.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.normal_unit
@@ -476,7 +476,7 @@ def test_normal_unit_per_cycle(binary_handler_analyze_rocprof_compute):
])
assert code == 0
test_utils.clean_output_dir(config["cleanup"], workload_dir)
test_utils.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.normal_unit
@@ -492,7 +492,7 @@ def test_normal_unit_per_second(binary_handler_analyze_rocprof_compute):
])
assert code == 0
test_utils.clean_output_dir(config["cleanup"], workload_dir)
test_utils.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.normal_unit
@@ -508,7 +508,7 @@ def test_normal_unit_per_kernel(binary_handler_analyze_rocprof_compute):
])
assert code == 0
test_utils.clean_output_dir(config["cleanup"], workload_dir)
test_utils.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.max_stat
@@ -524,7 +524,7 @@ def test_max_stat_num_1(binary_handler_analyze_rocprof_compute):
])
assert code == 0
test_utils.clean_output_dir(config["cleanup"], workload_dir)
test_utils.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.max_stat
@@ -540,7 +540,7 @@ def test_max_stat_num_2(binary_handler_analyze_rocprof_compute):
])
assert code == 0
test_utils.clean_output_dir(config["cleanup"], workload_dir)
test_utils.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.max_stat
@@ -556,7 +556,7 @@ def test_max_stat_num_3(binary_handler_analyze_rocprof_compute):
])
assert code == 0
test_utils.clean_output_dir(config["cleanup"], workload_dir)
test_utils.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.max_stat
@@ -572,7 +572,7 @@ def test_max_stat_num_4(binary_handler_analyze_rocprof_compute):
])
assert code == 0
test_utils.clean_output_dir(config["cleanup"], workload_dir)
test_utils.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.time_unit
@@ -588,7 +588,7 @@ def test_time_unit_s(binary_handler_analyze_rocprof_compute):
])
assert code == 0
test_utils.clean_output_dir(config["cleanup"], workload_dir)
test_utils.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.time_unit
@@ -604,7 +604,7 @@ def test_time_unit_ms(binary_handler_analyze_rocprof_compute):
])
assert code == 0
test_utils.clean_output_dir(config["cleanup"], workload_dir)
test_utils.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.time_unit
@@ -620,7 +620,7 @@ def test_time_unit_us(binary_handler_analyze_rocprof_compute):
])
assert code == 0
test_utils.clean_output_dir(config["cleanup"], workload_dir)
test_utils.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.time_unit
@@ -636,7 +636,7 @@ def test_time_unit_ns(binary_handler_analyze_rocprof_compute):
])
assert code == 0
test_utils.clean_output_dir(config["cleanup"], workload_dir)
test_utils.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.decimal
@@ -652,7 +652,7 @@ def test_decimal_1(binary_handler_analyze_rocprof_compute):
])
assert code == 0
test_utils.clean_output_dir(config["cleanup"], workload_dir)
test_utils.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.decimal
@@ -668,7 +668,7 @@ def test_decimal_2(binary_handler_analyze_rocprof_compute):
])
assert code == 0
test_utils.clean_output_dir(config["cleanup"], workload_dir)
test_utils.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.decimal
@@ -684,7 +684,7 @@ def test_decimal_3(binary_handler_analyze_rocprof_compute):
])
assert code == 0
test_utils.clean_output_dir(config["cleanup"], workload_dir)
test_utils.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.misc
@@ -709,7 +709,7 @@ def test_save_dfs(binary_handler_analyze_rocprof_compute):
assert len(df.index) >= 1
shutil.rmtree(output_path)
test_utils.clean_output_dir(config["cleanup"], workload_dir)
test_utils.clean_output_dir(config["cleanup"], workload_dir)
test_utils.clean_output_dir(config["cleanup"], output_path)
@@ -726,7 +726,7 @@ def test_col_1(binary_handler_analyze_rocprof_compute):
])
assert code == 0
test_utils.clean_output_dir(config["cleanup"], workload_dir)
test_utils.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.col
@@ -744,7 +744,7 @@ def test_col_2(binary_handler_analyze_rocprof_compute):
])
assert code == 0
test_utils.clean_output_dir(config["cleanup"], workload_dir)
test_utils.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.col
@@ -761,7 +761,7 @@ def test_col_3(binary_handler_analyze_rocprof_compute):
])
assert code == 0
test_utils.clean_output_dir(config["cleanup"], workload_dir)
test_utils.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.misc
@@ -776,7 +776,7 @@ def test_g(binary_handler_analyze_rocprof_compute):
])
assert code == 0
test_utils.clean_output_dir(config["cleanup"], workload_dir)
test_utils.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.kernel_verbose
@@ -792,7 +792,7 @@ def test_kernel_verbose_0(binary_handler_analyze_rocprof_compute):
])
assert code == 0
test_utils.clean_output_dir(config["cleanup"], workload_dir)
test_utils.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.kernel_verbose
@@ -808,7 +808,7 @@ def test_kernel_verbose_1(binary_handler_analyze_rocprof_compute):
])
assert code == 0
test_utils.clean_output_dir(config["cleanup"], workload_dir)
test_utils.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.kernel_verbose
@@ -824,7 +824,7 @@ def test_kernel_verbose_2(binary_handler_analyze_rocprof_compute):
])
assert code == 0
test_utils.clean_output_dir(config["cleanup"], workload_dir)
test_utils.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.kernel_verbose
@@ -840,7 +840,7 @@ def test_kernel_verbose_3(binary_handler_analyze_rocprof_compute):
])
assert code == 0
test_utils.clean_output_dir(config["cleanup"], workload_dir)
test_utils.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.kernel_verbose
@@ -856,7 +856,7 @@ def test_kernel_verbose_4(binary_handler_analyze_rocprof_compute):
])
assert code == 0
test_utils.clean_output_dir(config["cleanup"], workload_dir)
test_utils.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.kernel_verbose
@@ -872,7 +872,7 @@ def test_kernel_verbose_5(binary_handler_analyze_rocprof_compute):
])
assert code == 0
test_utils.clean_output_dir(config["cleanup"], workload_dir)
test_utils.clean_output_dir(config["cleanup"], workload_dir)
@pytest.mark.kernel_verbose
@@ -1055,7 +1055,11 @@ def test_parser_error_handling():
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
from utils.parser import build_eval_string, calc_builtin_var, update_denom_string
from utils.parser import (
build_eval_string,
calc_builtin_var,
update_denominator_string,
)
try:
build_eval_string("AVG(SQ_WAVES)", None, config={})
@@ -1064,7 +1068,7 @@ def test_parser_error_handling():
assert "coll_level can not be None" in str(e)
assert build_eval_string("", "pmc_perf", config={}) == ""
assert update_denom_string("", "per_wave") == ""
assert update_denominator_string("", "per_wave") == ""
class MockSysInfo:
total_l2_chan = 32
@@ -1115,14 +1119,14 @@ def test_ast_transformer_edge_cases():
f"Expected 'Unknown call' in error, got: {str(e)}"
)
supported_call = ast.Call(
SUPPORTED_CALL = ast.Call(
func=ast.Name(id="MIN", ctx=ast.Load()),
args=[ast.Constant(value=5) if hasattr(ast, "Constant") else ast.Num(n=5)],
keywords=[],
)
try:
result = transformer.visit_Call(supported_call)
result = transformer.visit_Call(SUPPORTED_CALL)
assert result.func.id == "to_min", f"Expected 'to_min', got: {result.func.id}"
except Exception as e:
assert False, f"Supported function call should not raise exception: {e}"
@@ -1280,7 +1284,7 @@ def test_missing_files_scenarios(binary_handler_analyze_rocprof_compute):
if os.path.exists(csv_path):
os.remove(csv_path)
code = binary_handler_analyze_rocprof_compute([ # noqa: F841
binary_handler_analyze_rocprof_compute([
"analyze",
"--path",
workload_dir,
@@ -1350,27 +1354,27 @@ def test_build_dfs_edge_cases():
@pytest.mark.misc
def test_update_functions_coverage():
"""Test update_denom_string and update_normUnit_string branches"""
"""Test update_denominator_string and update_norm_unit_string branches"""
import sys
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
from utils.parser import update_denom_string, update_normUnit_string
from utils.parser import update_denominator_string, update_normal_unit_string
result = update_denom_string("AVG(SQ_WAVES / $denom)", "per_wave")
result = update_denominator_string("AVG(SQ_WAVES / $denom)", "per_wave")
assert "$denom" not in result
assert "SQ_WAVES" in result
result = update_denom_string("AVG(DATA / $denom)", "per_cycle")
result = update_denominator_string("AVG(DATA / $denom)", "per_cycle")
assert "$GRBM_GUI_ACTIVE_PER_XCD" in result
result = update_denom_string("AVG(DATA / $denom)", "per_second")
result = update_denominator_string("AVG(DATA / $denom)", "per_second")
assert "End_Timestamp - Start_Timestamp" in result
result = update_denom_string("AVG(DATA / $denom)", "unsupported_unit")
result = update_denominator_string("AVG(DATA / $denom)", "unsupported_unit")
assert "$denom" in result
result = update_normUnit_string("(Prefix + $normUnit)", "per_wave")
result = update_normal_unit_string("(Prefix + $normUnit)", "per_wave")
assert "per wave" in result.lower()
assert result[0].isupper()
@@ -32,7 +32,7 @@ import yaml
def test_modification_time():
# Ensure hash map consistency
hash_path = Path("utils/autogen_hash.yaml")
with open(hash_path, "r") as f:
with open(hash_path) as f:
hash_map = yaml.safe_load(f)
for file, hash in hash_map.items():
file_hash = hashlib.sha256(Path(file).read_bytes()).hexdigest()
@@ -23,7 +23,6 @@
##############################################################################
import os
import re
import subprocess
import tempfile
@@ -152,7 +151,7 @@ def test_num_xcds_spec_class(monkeypatch):
num_xcds = get_num_xcds()
# 2. load machine specs
machine_spec = generate_machine_specs(None)
machine_spec = generate_machine_specs(None, None)
# 3. check results are expected
assert machine_spec.compute_partition is not None
@@ -203,26 +202,24 @@ def test_load_yaml_file_not_found():
"""Test _load_yaml with non-existent file - covers lines 104-105"""
from src.utils.mi_gpu_spec import MIGPUSpecs
non_existent_path = "/path/that/does/not/exist/file.yaml"
with pytest.raises(SystemExit):
MIGPUSpecs._load_yaml(non_existent_path)
with pytest.raises(FileNotFoundError):
MIGPUSpecs._load_yaml("non_existent_file.yaml")
@pytest.mark.misc
def test_load_yaml_invalid_yaml():
"""Test _load_yaml with corrupted YAML - covers lines 106-107"""
import yaml
from src.utils.mi_gpu_spec import MIGPUSpecs
# Create invalid YAML file
with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f:
f.write("invalid: yaml: content: [\nunclosed bracket")
temp_path = f.name
try:
with pytest.raises(SystemExit):
MIGPUSpecs._load_yaml(temp_path)
finally:
os.unlink(temp_path)
with pytest.raises(yaml.YAMLError):
MIGPUSpecs._load_yaml(str(temp_path))
@pytest.mark.misc
@@ -231,7 +228,7 @@ def test_load_yaml_generic_exception():
from src.utils.mi_gpu_spec import MIGPUSpecs
with patch("builtins.open", side_effect=PermissionError("Access denied")):
with pytest.raises(SystemExit):
with pytest.raises(PermissionError, match="Access denied"):
MIGPUSpecs._load_yaml("some_file.yaml")
@@ -241,8 +238,7 @@ def test_get_gpu_series_dict_uninitialized():
from src.utils.mi_gpu_spec import MIGPUSpecs
with patch.object(MIGPUSpecs, "_gpu_series_dict", {}):
with pytest.raises(SystemExit):
MIGPUSpecs.get_gpu_series_dict()
assert MIGPUSpecs.get_gpu_series_dict() == {}
@pytest.mark.misc
@@ -251,8 +247,7 @@ def test_get_gpu_series_uninitialized():
from src.utils.mi_gpu_spec import MIGPUSpecs
with patch.object(MIGPUSpecs, "_gpu_series_dict", {}):
with pytest.raises(SystemExit):
result = MIGPUSpecs.get_gpu_series("gfx942") # noqa: F841
assert MIGPUSpecs.get_gpu_series_dict() == {}
@pytest.mark.misc
@@ -328,7 +323,9 @@ def test_get_num_xcds_unknown_gpu_model():
"""Test get_num_xcds with unknown gpu model - covers lines 319-321"""
from src.utils.mi_gpu_spec import MIGPUSpecs
result = MIGPUSpecs.get_num_xcds(gpu_arch="gfx950", gpu_model="UNKNOWN_MODEL") # noqa: F841
result = MIGPUSpecs.get_num_xcds( # noqa: F841
gpu_arch="gfx950", gpu_model="UNKNOWN_MODEL"
)
@pytest.mark.misc
@@ -379,9 +376,7 @@ def test_get_chip_id_dict_empty():
from src.utils.mi_gpu_spec import MIGPUSpecs
with patch.object(MIGPUSpecs, "_chip_id_dict", {}):
with patch("src.utils.mi_gpu_spec.console_error") as mock_error:
result = MIGPUSpecs.get_chip_id_dict() # noqa: F841
mock_error.assert_called_once()
assert MIGPUSpecs.get_chip_id_dict() == {}
@pytest.mark.misc
@@ -390,9 +385,7 @@ def test_get_num_xcds_dict_empty():
from src.utils.mi_gpu_spec import MIGPUSpecs
with patch.object(MIGPUSpecs, "_num_xcds_dict", {}):
with patch("src.utils.mi_gpu_spec.console_error") as mock_error:
result = MIGPUSpecs.get_num_xcds_dict() # noqa: F841
mock_error.assert_called_once()
assert MIGPUSpecs.get_num_xcds_dict() == {}
@pytest.mark.misc
@@ -401,6 +394,7 @@ def test_normal_functionality_still_works():
from src.utils.mi_gpu_spec import MIGPUSpecs
result = MIGPUSpecs.get_gpu_model("gfx90a", None)
assert result is not None
result = MIGPUSpecs.get_gpu_series("gfx90a")
@@ -580,7 +580,7 @@ def test_path(binary_handler_profile_rocprof_compute):
elif "MI350" in soc:
assert sorted(list(file_dict.keys())) == sorted(ALL_CSVS_MI350)
else:
print("This test is not supported for {}".format(soc))
print(f"This test is not supported for {soc}")
assert 0
validate(inspect.stack()[0][3], workload_dir, file_dict)
@@ -821,7 +821,7 @@ def test_roofline_workload_dir_not_set_error():
self.roofline_data_type = ["FP32"]
args = MockArgs()
mspec = generate_machine_specs(None)
mspec = generate_machine_specs(None, None)
run_parameters = {
"workload_dir": None,
@@ -1090,7 +1090,7 @@ def test_roofline_missing_file_handling(binary_handler_profile_rocprof_compute):
self.roofline_data_type = ["FP32"]
args = MockArgs()
mspec = generate_machine_specs(None)
mspec = generate_machine_specs(None, None)
workload_dir = test_utils.get_output_dir()
@@ -1144,7 +1144,7 @@ def test_roofline_invalid_datatype_cli(binary_handler_profile_rocprof_compute):
self.roofline_data_type = ["FP32"]
args = MockArgs()
mspec = generate_machine_specs(None)
mspec = generate_machine_specs(None, None)
run_parameters = {
"workload_dir": test_utils.get_output_dir(),
@@ -1212,7 +1212,7 @@ def test_device_filter(binary_handler_profile_rocprof_compute):
elif "MI350" in soc:
assert sorted(list(file_dict.keys())) == sorted(ALL_CSVS_MI350)
else:
print("Testing isn't supported yet for {}".format(soc))
print(f"Testing isn't supported yet for {soc}")
assert 0
# TODO - verify expected device id in results
@@ -1250,7 +1250,7 @@ def test_kernel(binary_handler_profile_rocprof_compute):
elif "MI350" in soc:
assert sorted(list(file_dict.keys())) == sorted(ALL_CSVS_MI350)
else:
print("Testing isn't supported yet for {}".format(soc))
print(f"Testing isn't supported yet for {soc}")
assert 0
validate(
@@ -1286,7 +1286,7 @@ def test_dispatch_0(binary_handler_profile_rocprof_compute):
elif "MI350" in soc:
assert sorted(list(file_dict.keys())) == sorted(ALL_CSVS_MI350)
else:
print("Testing isn't supported yet for {}".format(soc))
print(f"Testing isn't supported yet for {soc}")
assert 0
validate(
@@ -1326,7 +1326,7 @@ def test_dispatch_0_1(binary_handler_profile_rocprof_compute):
elif "MI350" in soc:
assert sorted(list(file_dict.keys())) == sorted(ALL_CSVS_MI350)
else:
print("Testing isn't supported yet for {}".format(soc))
print(f"Testing isn't supported yet for {soc}")
assert 0
validate(
@@ -1363,7 +1363,7 @@ def test_dispatch_2(binary_handler_profile_rocprof_compute):
elif "MI350" in soc:
assert sorted(list(file_dict.keys())) == sorted(ALL_CSVS_MI350)
else:
print("Testing isn't supported yet for {}".format(soc))
print(f"Testing isn't supported yet for {soc}")
assert 0
validate(
@@ -1403,7 +1403,7 @@ def test_join_type_grid(binary_handler_profile_rocprof_compute):
elif "MI350" in soc:
assert sorted(list(file_dict.keys())) == sorted(ALL_CSVS_MI350)
else:
print("Testing isn't supported yet for {}".format(soc))
print(f"Testing isn't supported yet for {soc}")
assert 0
validate(
@@ -1440,7 +1440,7 @@ def test_join_type_kernel(binary_handler_profile_rocprof_compute):
elif "MI350" in soc:
assert sorted(list(file_dict.keys())) == sorted(ALL_CSVS_MI350)
else:
print("Testing isn't supported yet for {}".format(soc))
print(f"Testing isn't supported yet for {soc}")
assert 0
validate(
@@ -1887,7 +1887,7 @@ class TestSetsIntegration:
memory_metrics = ["16.1.2", "17.1.0"]
for metric_id in memory_metrics:
assert metric_id in open(Path(workload_dir) / "log.txt", "r").read(), (
assert metric_id in open(Path(workload_dir) / "log.txt").read(), (
f"Expected memory metric {metric_id} not found"
)
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -49,7 +49,7 @@ def detect_repo_structure():
return True, monorepo_root, project_root
if (cwd / "CMakeLists.txt").exists():
with open(cwd / "CMakeLists.txt", "r") as f:
with open(cwd / "CMakeLists.txt") as f:
content = f.read()
if (
"project(rocprofiler-compute" in content
@@ -42,17 +42,15 @@ import yaml
# Get root directory of the project
ROOT_DIR = Path(__file__).parent.parent
SOURCE_DIR = ROOT_DIR.joinpath("utils")
TARGET_DIR = ROOT_DIR.joinpath("src", "rocprof_compute_soc", "analysis_configs")
SETS_TARGET_DIR = ROOT_DIR.joinpath(
"src", "rocprof_compute_soc", "profile_configs", "sets"
)
DOC_TARGET_DIR = ROOT_DIR.joinpath("docs", "data")
SOURCE_DIR = ROOT_DIR / "utils"
TARGET_DIR = ROOT_DIR / "src" / "rocprof_compute_soc" / "analysis_configs"
SETS_TARGET_DIR = ROOT_DIR / "src" / "rocprof_compute_soc" / "profile_configs" / "sets"
DOC_TARGET_DIR = ROOT_DIR / "docs" / "data"
AUTOGEN_TEXT = (
"# AUTOGENERATED FILE. Only edit for testing purposes, not for development. "
"Generated from utils/unified_config.yaml. Generated by utils/split_config.py\n"
)
HASH_FILE = ROOT_DIR.joinpath("utils", "autogen_hash.yaml")
HASH_FILE = ROOT_DIR / "utils" / "autogen_hash.yaml"
HASH_FILE_MAP = {}
GFX_VERSIONS = ["gfx908", "gfx90a", "gfx940", "gfx941", "gfx942", "gfx950"]
METRIC_ID_TO_NAME_MAP = {gfx_version: {} for gfx_version in GFX_VERSIONS}
@@ -70,7 +68,7 @@ def update_analysis_config():
global METRIC_ID_TO_NAME_MAP
# Read the unified config file
with open(SOURCE_DIR.joinpath("unified_config.yaml")) as file:
with open(SOURCE_DIR / "unified_config.yaml") as file:
unified_config = yaml.safe_load(file)
# Create per panel config file
@@ -94,7 +92,7 @@ def update_analysis_config():
for gfx_version in GFX_VERSIONS:
# Create per gfx architecture folder
gfx_dir = TARGET_DIR.joinpath(gfx_version)
gfx_dir = TARGET_DIR / gfx_version
# Create directory if it doesn't exist
if not gfx_dir.exists():
gfx_dir.mkdir()
@@ -120,9 +118,7 @@ def update_analysis_config():
data_source_config
)
# Write panel config to file
filename = Path(
TARGET_DIR.joinpath(gfx_version, f"{panel_id}_{panel_title}.yaml")
)
filename = TARGET_DIR / gfx_version / f"{panel_id}_{panel_title}.yaml"
with open(filename, "w") as file:
file.write(get_autogen_text())
yaml.dump(new_panel_config, file, sort_keys=False)
@@ -148,7 +144,7 @@ def update_sets_config():
print(f"Created directory: {SETS_TARGET_DIR}")
# Read the unified config file
with open(SOURCE_DIR.joinpath("unified_sets.yaml")) as file:
with open(SOURCE_DIR / "unified_sets.yaml") as file:
unified_sets = yaml.safe_load(file)
# Create per gfx version file
@@ -172,7 +168,7 @@ def update_sets_config():
new_sets["sets"].append(current_set)
# Write gfx version sets to file
filename = Path(SETS_TARGET_DIR.joinpath(f"{gfx_version}_sets.yaml"))
filename = SETS_TARGET_DIR / f"{gfx_version}_sets.yaml"
with open(filename, "w") as file:
file.write(get_autogen_text("utils/unified_sets.yaml"))
yaml.dump(new_sets, file, sort_keys=False)
@@ -223,7 +219,7 @@ def update_documentation():
}
# Read the unified config file
with open(SOURCE_DIR.joinpath("unified_config.yaml")) as file:
with open(SOURCE_DIR / "unified_config.yaml") as file:
unified_config = yaml.safe_load(file)
panel_metric_map = {}
@@ -258,7 +254,7 @@ def update_documentation():
section_metric_map[section] = panel_metric_map[panel_id]
# Write documentation metrics description file
filename = Path(DOC_TARGET_DIR.joinpath("metrics_description.yaml"))
filename = DOC_TARGET_DIR / "metrics_description.yaml"
with open(filename, "w") as file:
file.write(get_autogen_text())
yaml.dump(section_metric_map, file, sort_keys=False)
@@ -45,11 +45,11 @@ maxHeaderLines = 200
def cacheLicenseFile(infile, comment="#"):
if not Path(infile).is_file():
logging.error("Unable to access license file - >%s" % infile)
logging.error(f"Unable to access license file - >{infile}")
sys.exit(1)
license = ""
with open(infile, "r") as file_in:
with open(infile) as file_in:
for line in file_in:
license += comment
if line.strip() != "":
@@ -80,17 +80,17 @@ if args.files:
specificFiles = args.files.split(",")
print("")
logging.info("Source directory = %s" % srcDir)
logging.info(f"Source directory = {srcDir}")
if fileExtension:
logging.info("File extension = %s" % fileExtension)
logging.info(f"File extension = {fileExtension}")
if specificFiles:
logging.info("Specific files = %s" % specificFiles)
logging.info(f"Specific files = {specificFiles}")
# cache license file
license = cacheLicenseFile(args.license)
# Scan files in provided source directory...
for filename in glob.iglob(srcDir + "/**", recursive=True):
for filename in glob.iglob(f"{srcDir}/**", recursive=True):
# skip directories
if Path(filename).is_dir():
continue
@@ -106,22 +106,22 @@ for filename in glob.iglob(srcDir + "/**", recursive=True):
if specificFiles:
found = False
for file in specificFiles:
fullPath = str(Path(srcDir).joinpath(file))
fullPath = str(Path(srcDir) / file)
if fullPath == filename:
found = True
break
if not found:
continue
logging.debug("Examining %s for license..." % filename)
logging.debug(f"Examining {filename} for license...")
# Update license header contents if delimiters are found
with open(filename, "r") as file_in:
baseName = Path(filename).name
dirName = str(Path(filename).parent)
tmpFile = dirName + "/." + baseName + ".tmp"
with open(filename) as file_in:
base_name = Path(filename).name
dir_name = Path(filename).parent
tmp_file = dir_name / f".{base_name}.tmp"
file_out = open(tmpFile, "w")
file_out = open(tmp_file, "w")
for line in file_in:
if re.search(begDelim, line):
logging.debug("Found beginning delimiter")
@@ -147,10 +147,10 @@ for filename in glob.iglob(srcDir + "/**", recursive=True):
file_out.close()
# Check if file changed and update
if not filecmp.cmp(filename, tmpFile, shallow=False):
logging.info("%s changed" % filename)
shutil.copystat(filename, tmpFile)
if not filecmp.cmp(filename, tmp_file, shallow=False):
logging.info(f"{filename} changed")
shutil.copystat(filename, tmp_file)
if not args.dryrun:
os.rename(tmpFile, filename)
os.rename(tmp_file, filename)
else:
os.unlink(tmpFile)
os.unlink(tmp_file)
@@ -35,20 +35,20 @@ parser = argparse.ArgumentParser()
parser.add_argument("--tag", type=str, required=True, help="tagname to check")
args = parser.parse_args()
execPath = str(Path(__file__).parent)
with open(execPath + "/../VERSION") as f:
repoVer = f.readline().strip()
exec_path = Path(__file__).parent
with open(exec_path / "../VERSION") as f:
repo_ver = f.readline().strip()
repoCheck = "v" + repoVer
repo_check = f"v{repo_ver}"
tag = args.tag
print("Current repository version = %s" % repoVer)
print("--> tagname = %s" % tag)
print(f"Current repository version = {repo_ver}")
print(f"--> tagname = {tag}")
if repoCheck == tag:
if repo_check == tag:
print("OK: exact match")
exit(0)
elif tag.startswith(repoCheck + "-"):
elif tag.startswith(repo_check + "-"):
print("OK: allowed match with extra delimiter")
exit(0)
elif tag.startswith("rocm-"):