From 7ed6000e322d1b1de2778c658e936c70ca9e3e88 Mon Sep 17 00:00:00 2001 From: xuchen-amd Date: Fri, 12 Sep 2025 13:53:24 -0400 Subject: [PATCH] [rocprofiler-compute] Refactor to add type annotation and misc (#787) --- .../.pre-commit-config.yaml | 2 +- projects/rocprofiler-compute/CONTRIBUTING.md | 35 + .../docs/archive/docs-1.x/conf.py | 1 - .../docs/archive/docs-2.x/conf.py | 1 - projects/rocprofiler-compute/docs/conf.py | 2 +- projects/rocprofiler-compute/pyproject.toml | 20 +- projects/rocprofiler-compute/src/argparser.py | 86 +- .../rocprofiler-compute/src/rocprof-compute | 118 +- .../rocprof_compute_analyze/analysis_base.py | 446 +++--- .../rocprof_compute_analyze/analysis_cli.py | 79 +- .../rocprof_compute_analyze/analysis_db.py | 6 +- .../rocprof_compute_analyze/analysis_webui.py | 409 ++--- .../src/rocprof_compute_base.py | 253 ++- .../rocprof_compute_profile/profiler_base.py | 435 +++--- .../profiler_rocprof_v3.py | 78 +- .../profiler_rocprofiler_sdk.py | 72 +- .../src/rocprof_compute_soc/soc_base.py | 455 +++--- .../src/rocprof_compute_soc/soc_gfx908.py | 19 +- .../src/rocprof_compute_soc/soc_gfx90a.py | 16 +- .../src/rocprof_compute_soc/soc_gfx940.py | 16 +- .../src/rocprof_compute_soc/soc_gfx941.py | 16 +- .../src/rocprof_compute_soc/soc_gfx942.py | 16 +- .../src/rocprof_compute_soc/soc_gfx950.py | 16 +- .../src/rocprof_compute_tui/analysis_tui.py | 32 +- .../src/rocprof_compute_tui/tui_app.py | 36 +- .../rocprof_compute_tui/utils/tui_utils.py | 48 +- .../rocprof_compute_tui/views/kernel_view.py | 26 +- .../rocprof_compute_tui/views/main_view.py | 50 +- .../widgets/center_panel/center_area.py | 5 +- .../src/rocprof_compute_tui/widgets/charts.py | 123 +- .../widgets/collapsibles.py | 29 +- .../widgets/menu_bar/menu_bar.py | 18 +- .../widgets/recent_directories.py | 3 +- .../widgets/right_panel/right.py | 7 +- .../widgets/tabs/tabs_area.py | 5 +- .../widgets/tabs/tabs_terminal.py | 36 +- projects/rocprofiler-compute/src/roofline.py | 460 +++--- .../src/utils/analysis_orm.py | 41 +- .../rocprofiler-compute/src/utils/file_io.py | 359 ++--- projects/rocprofiler-compute/src/utils/gui.py | 455 +++--- .../src/utils/gui_components/header.py | 65 +- .../src/utils/gui_components/memchart.py | 29 +- .../src/utils/kernel_name_shortener.py | 241 ++- .../rocprofiler-compute/src/utils/logger.py | 96 +- .../src/utils/mem_chart.py | 232 ++- .../src/utils/mi_gpu_spec.py | 218 ++- .../rocprofiler-compute/src/utils/parser.py | 1386 ++++++++--------- .../src/utils/rocpd_data.py | 18 +- .../src/utils/roofline_calc.py | 454 +++--- .../rocprofiler-compute/src/utils/schema.py | 38 +- .../rocprofiler-compute/src/utils/specs.py | 658 +++++--- projects/rocprofiler-compute/src/utils/tty.py | 824 +++++----- .../rocprofiler-compute/src/utils/utils.py | 768 +++++---- .../tests/test_analyze_commands.py | 118 +- .../tests/test_autogen_config.py | 2 +- .../tests/test_gpu_specs.py | 40 +- .../tests/test_profile_general.py | 24 +- .../rocprofiler-compute/tests/test_utils.py | 371 ++--- projects/rocprofiler-compute/utils/run-ci.py | 2 +- .../rocprofiler-compute/utils/split_config.py | 28 +- .../utils/update_license.py | 36 +- .../rocprofiler-compute/utils/ver_check.py | 16 +- 62 files changed, 5145 insertions(+), 4849 deletions(-) diff --git a/projects/rocprofiler-compute/.pre-commit-config.yaml b/projects/rocprofiler-compute/.pre-commit-config.yaml index 509a0a5e56..89906b129f 100644 --- a/projects/rocprofiler-compute/.pre-commit-config.yaml +++ b/projects/rocprofiler-compute/.pre-commit-config.yaml @@ -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] diff --git a/projects/rocprofiler-compute/CONTRIBUTING.md b/projects/rocprofiler-compute/CONTRIBUTING.md index c75c75c46b..5c7bda24d6 100644 --- a/projects/rocprofiler-compute/CONTRIBUTING.md +++ b/projects/rocprofiler-compute/CONTRIBUTING.md @@ -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: diff --git a/projects/rocprofiler-compute/docs/archive/docs-1.x/conf.py b/projects/rocprofiler-compute/docs/archive/docs-1.x/conf.py index 8e1f9969b9..862de9a828 100644 --- a/projects/rocprofiler-compute/docs/archive/docs-1.x/conf.py +++ b/projects/rocprofiler-compute/docs/archive/docs-1.x/conf.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Configuration file for the Sphinx documentation builder. # diff --git a/projects/rocprofiler-compute/docs/archive/docs-2.x/conf.py b/projects/rocprofiler-compute/docs/archive/docs-2.x/conf.py index 82725ad3d2..81728141c7 100644 --- a/projects/rocprofiler-compute/docs/archive/docs-2.x/conf.py +++ b/projects/rocprofiler-compute/docs/archive/docs-2.x/conf.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Configuration file for the Sphinx documentation builder. # diff --git a/projects/rocprofiler-compute/docs/conf.py b/projects/rocprofiler-compute/docs/conf.py index 7a080b692c..b812096e8b 100644 --- a/projects/rocprofiler-compute/docs/conf.py +++ b/projects/rocprofiler-compute/docs/conf.py @@ -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": { diff --git a/projects/rocprofiler-compute/pyproject.toml b/projects/rocprofiler-compute/pyproject.toml index b40f623a17..475dc24777 100644 --- a/projects/rocprofiler-compute/pyproject.toml +++ b/projects/rocprofiler-compute/pyproject.toml @@ -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 diff --git a/projects/rocprofiler-compute/src/argparser.py b/projects/rocprofiler-compute/src/argparser.py index 486bd78e43..37274eacac 100644 --- a/projects/rocprofiler-compute/src/argparser.py +++ b/projects/rocprofiler-compute/src/argparser.py @@ -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 -[profile options] [roofline options] -- ` +`rocprof-compute profile --name [profile options] [roofline options] -- ` --------------------------------------------------------------------------------- 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/)".format(os.getcwd()) + f"\t\t\tSpecify path to save workload.\n\t\t\t(DEFAULT: {os.getcwd()}/workloads/)" # 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", diff --git a/projects/rocprofiler-compute/src/rocprof-compute b/projects/rocprofiler-compute/src/rocprof-compute index eca9dba09e..e9ee695ef3 100755 --- a/projects/rocprofiler-compute/src/rocprof-compute +++ b/projects/rocprofiler-compute/src/rocprof-compute @@ -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 diff --git a/projects/rocprofiler-compute/src/rocprof_compute_analyze/analysis_base.py b/projects/rocprofiler-compute/src/rocprof_compute_analyze/analysis_base.py index 67d64de66f..7c5dd5d1cb 100644 --- a/projects/rocprofiler-compute/src/rocprof_compute_analyze/analysis_base.py +++ b/projects/rocprofiler-compute/src/rocprof_compute_analyze/analysis_base.py @@ -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") diff --git a/projects/rocprofiler-compute/src/rocprof_compute_analyze/analysis_cli.py b/projects/rocprofiler-compute/src/rocprof_compute_analyze/analysis_cli.py index 573cc5b250..59f9e8094a 100644 --- a/projects/rocprofiler-compute/src/rocprof_compute_analyze/analysis_cli.py +++ b/projects/rocprofiler-compute/src/rocprof_compute_analyze/analysis_cli.py @@ -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, diff --git a/projects/rocprofiler-compute/src/rocprof_compute_analyze/analysis_db.py b/projects/rocprofiler-compute/src/rocprof_compute_analyze/analysis_db.py index 413f0c3575..d7897bcf2d 100644 --- a/projects/rocprofiler-compute/src/rocprof_compute_analyze/analysis_db.py +++ b/projects/rocprofiler-compute/src/rocprof_compute_analyze/analysis_db.py @@ -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 diff --git a/projects/rocprofiler-compute/src/rocprof_compute_analyze/analysis_webui.py b/projects/rocprofiler-compute/src/rocprof_compute_analyze/analysis_webui.py index 2bdaaece0e..a4500f25a9 100644 --- a/projects/rocprofiler-compute/src/rocprof_compute_analyze/analysis_webui.py +++ b/projects/rocprofiler-compute/src/rocprof_compute_analyze/analysis_webui.py @@ -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( diff --git a/projects/rocprofiler-compute/src/rocprof_compute_base.py b/projects/rocprofiler-compute/src/rocprof_compute_base.py index 42ca84cb81..e814c21c8e 100644 --- a/projects/rocprofiler-compute/src/rocprof_compute_base.py +++ b/projects/rocprofiler-compute/src/rocprof_compute_base.py @@ -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 diff --git a/projects/rocprofiler-compute/src/rocprof_compute_profile/profiler_base.py b/projects/rocprofiler-compute/src/rocprof_compute_profile/profiler_base.py index 1afe6d9fca..52be6cff39 100644 --- a/projects/rocprofiler-compute/src/rocprof_compute_profile/profiler_base.py +++ b/projects/rocprofiler-compute/src/rocprof_compute_profile/profiler_base.py @@ -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() diff --git a/projects/rocprofiler-compute/src/rocprof_compute_profile/profiler_rocprof_v3.py b/projects/rocprofiler-compute/src/rocprof_compute_profile/profiler_rocprof_v3.py index ca35c2e810..b4d271495f 100644 --- a/projects/rocprofiler-compute/src/rocprof_compute_profile/profiler_rocprof_v3.py +++ b/projects/rocprofiler-compute/src/rocprof_compute_profile/profiler_rocprof_v3.py @@ -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 diff --git a/projects/rocprofiler-compute/src/rocprof_compute_profile/profiler_rocprofiler_sdk.py b/projects/rocprofiler-compute/src/rocprof_compute_profile/profiler_rocprofiler_sdk.py index 2208b9bbca..027c370a97 100644 --- a/projects/rocprofiler-compute/src/rocprof_compute_profile/profiler_rocprofiler_sdk.py +++ b/projects/rocprofiler-compute/src/rocprof_compute_profile/profiler_rocprofiler_sdk.py @@ -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 diff --git a/projects/rocprofiler-compute/src/rocprof_compute_soc/soc_base.py b/projects/rocprofiler-compute/src/rocprof_compute_soc/soc_base.py index 4efb8dbe36..bc87268996 100644 --- a/projects/rocprofiler-compute/src/rocprof_compute_soc/soc_base.py +++ b/projects/rocprofiler-compute/src/rocprof_compute_soc/soc_base.py @@ -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 diff --git a/projects/rocprofiler-compute/src/rocprof_compute_soc/soc_gfx908.py b/projects/rocprofiler-compute/src/rocprof_compute_soc/soc_gfx908.py index 26372aadaa..176970215f 100644 --- a/projects/rocprofiler-compute/src/rocprof_compute_soc/soc_gfx908.py +++ b/projects/rocprofiler-compute/src/rocprof_compute_soc/soc_gfx908.py @@ -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) diff --git a/projects/rocprofiler-compute/src/rocprof_compute_soc/soc_gfx90a.py b/projects/rocprofiler-compute/src/rocprof_compute_soc/soc_gfx90a.py index 8e21adcf64..f2e859d2ec 100644 --- a/projects/rocprofiler-compute/src/rocprof_compute_soc/soc_gfx90a.py +++ b/projects/rocprofiler-compute/src/rocprof_compute_soc/soc_gfx90a.py @@ -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) diff --git a/projects/rocprofiler-compute/src/rocprof_compute_soc/soc_gfx940.py b/projects/rocprofiler-compute/src/rocprof_compute_soc/soc_gfx940.py index 659a8509c5..759fc09803 100644 --- a/projects/rocprofiler-compute/src/rocprof_compute_soc/soc_gfx940.py +++ b/projects/rocprofiler-compute/src/rocprof_compute_soc/soc_gfx940.py @@ -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) diff --git a/projects/rocprofiler-compute/src/rocprof_compute_soc/soc_gfx941.py b/projects/rocprofiler-compute/src/rocprof_compute_soc/soc_gfx941.py index 7b26e970fe..e87ca25cdb 100644 --- a/projects/rocprofiler-compute/src/rocprof_compute_soc/soc_gfx941.py +++ b/projects/rocprofiler-compute/src/rocprof_compute_soc/soc_gfx941.py @@ -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) diff --git a/projects/rocprofiler-compute/src/rocprof_compute_soc/soc_gfx942.py b/projects/rocprofiler-compute/src/rocprof_compute_soc/soc_gfx942.py index 69c71d0532..75c858faa8 100644 --- a/projects/rocprofiler-compute/src/rocprof_compute_soc/soc_gfx942.py +++ b/projects/rocprofiler-compute/src/rocprof_compute_soc/soc_gfx942.py @@ -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) diff --git a/projects/rocprofiler-compute/src/rocprof_compute_soc/soc_gfx950.py b/projects/rocprofiler-compute/src/rocprof_compute_soc/soc_gfx950.py index 6a999dd0d1..cd5a3ad542 100644 --- a/projects/rocprofiler-compute/src/rocprof_compute_soc/soc_gfx950.py +++ b/projects/rocprofiler-compute/src/rocprof_compute_soc/soc_gfx950.py @@ -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) diff --git a/projects/rocprofiler-compute/src/rocprof_compute_tui/analysis_tui.py b/projects/rocprofiler-compute/src/rocprof_compute_tui/analysis_tui.py index c5c05803ce..fd686168ed 100644 --- a/projects/rocprofiler-compute/src/rocprof_compute_tui/analysis_tui.py +++ b/projects/rocprofiler-compute/src/rocprof_compute_tui/analysis_tui.py @@ -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) diff --git a/projects/rocprofiler-compute/src/rocprof_compute_tui/tui_app.py b/projects/rocprofiler-compute/src/rocprof_compute_tui/tui_app.py index 5caffa8e3f..c652f1851c 100644 --- a/projects/rocprofiler-compute/src/rocprof_compute_tui/tui_app.py +++ b/projects/rocprofiler-compute/src/rocprof_compute_tui/tui_app.py @@ -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() diff --git a/projects/rocprofiler-compute/src/rocprof_compute_tui/utils/tui_utils.py b/projects/rocprofiler-compute/src/rocprof_compute_tui/utils/tui_utils.py index d14689f5a2..998063569b 100644 --- a/projects/rocprofiler-compute/src/rocprof_compute_tui/utils/tui_utils.py +++ b/projects/rocprofiler-compute/src/rocprof_compute_tui/utils/tui_utils.py @@ -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) diff --git a/projects/rocprofiler-compute/src/rocprof_compute_tui/views/kernel_view.py b/projects/rocprofiler-compute/src/rocprof_compute_tui/views/kernel_view.py index b1b2b29ea5..099366266d 100644 --- a/projects/rocprofiler-compute/src/rocprof_compute_tui/views/kernel_view.py +++ b/projects/rocprofiler-compute/src/rocprof_compute_tui/views/kernel_view.py @@ -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() diff --git a/projects/rocprofiler-compute/src/rocprof_compute_tui/views/main_view.py b/projects/rocprofiler-compute/src/rocprof_compute_tui/views/main_view.py index b6b4e84100..dfa670c58d 100644 --- a/projects/rocprofiler-compute/src/rocprof_compute_tui/views/main_view.py +++ b/projects/rocprofiler-compute/src/rocprof_compute_tui/views/main_view.py @@ -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: diff --git a/projects/rocprofiler-compute/src/rocprof_compute_tui/widgets/center_panel/center_area.py b/projects/rocprofiler-compute/src/rocprof_compute_tui/widgets/center_panel/center_area.py index 41ab200126..7e07e084aa 100644 --- a/projects/rocprofiler-compute/src/rocprof_compute_tui/widgets/center_panel/center_area.py +++ b/projects/rocprofiler-compute/src/rocprof_compute_tui/widgets/center_panel/center_area.py @@ -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 diff --git a/projects/rocprofiler-compute/src/rocprof_compute_tui/widgets/charts.py b/projects/rocprofiler-compute/src/rocprof_compute_tui/widgets/charts.py index 98ff639073..fa12e7d197 100644 --- a/projects/rocprofiler-compute/src/rocprof_compute_tui/widgets/charts.py +++ b/projects/rocprofiler-compute/src/rocprof_compute_tui/widgets/charts.py @@ -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 diff --git a/projects/rocprofiler-compute/src/rocprof_compute_tui/widgets/collapsibles.py b/projects/rocprofiler-compute/src/rocprof_compute_tui/widgets/collapsibles.py index fda9916e11..716e0f5c18 100644 --- a/projects/rocprofiler-compute/src/rocprof_compute_tui/widgets/collapsibles.py +++ b/projects/rocprofiler-compute/src/rocprof_compute_tui/widgets/collapsibles.py @@ -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) diff --git a/projects/rocprofiler-compute/src/rocprof_compute_tui/widgets/menu_bar/menu_bar.py b/projects/rocprofiler-compute/src/rocprof_compute_tui/widgets/menu_bar/menu_bar.py index 05f5bf598d..2248a12602 100644 --- a/projects/rocprofiler-compute/src/rocprof_compute_tui/widgets/menu_bar/menu_bar.py +++ b/projects/rocprofiler-compute/src/rocprof_compute_tui/widgets/menu_bar/menu_bar.py @@ -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() diff --git a/projects/rocprofiler-compute/src/rocprof_compute_tui/widgets/recent_directories.py b/projects/rocprofiler-compute/src/rocprof_compute_tui/widgets/recent_directories.py index eecd4e34c3..9a0bb8dc42 100644 --- a/projects/rocprofiler-compute/src/rocprof_compute_tui/widgets/recent_directories.py +++ b/projects/rocprofiler-compute/src/rocprof_compute_tui/widgets/recent_directories.py @@ -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 diff --git a/projects/rocprofiler-compute/src/rocprof_compute_tui/widgets/right_panel/right.py b/projects/rocprofiler-compute/src/rocprof_compute_tui/widgets/right_panel/right.py index 6c7f0e43c4..8b7956c874 100644 --- a/projects/rocprofiler-compute/src/rocprof_compute_tui/widgets/right_panel/right.py +++ b/projects/rocprofiler-compute/src/rocprof_compute_tui/widgets/right_panel/right.py @@ -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") diff --git a/projects/rocprofiler-compute/src/rocprof_compute_tui/widgets/tabs/tabs_area.py b/projects/rocprofiler-compute/src/rocprof_compute_tui/widgets/tabs/tabs_area.py index 5828c8f7f5..70a00436fa 100644 --- a/projects/rocprofiler-compute/src/rocprof_compute_tui/widgets/tabs/tabs_area.py +++ b/projects/rocprofiler-compute/src/rocprof_compute_tui/widgets/tabs/tabs_area.py @@ -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) diff --git a/projects/rocprofiler-compute/src/rocprof_compute_tui/widgets/tabs/tabs_terminal.py b/projects/rocprofiler-compute/src/rocprof_compute_tui/widgets/tabs/tabs_terminal.py index dab156c819..017f059654 100644 --- a/projects/rocprofiler-compute/src/rocprof_compute_tui/widgets/tabs/tabs_terminal.py +++ b/projects/rocprofiler-compute/src/rocprof_compute_tui/widgets/tabs/tabs_terminal.py @@ -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] diff --git a/projects/rocprofiler-compute/src/roofline.py b/projects/rocprofiler-compute/src/roofline.py index 6ee3f2057d..e7cd6a6074 100644 --- a/projects/rocprofiler-compute/src/roofline.py +++ b/projects/rocprofiler-compute/src/roofline.py @@ -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
for Plotly. """ @@ -68,8 +71,19 @@ def wrap_text(text, width=92): return "
".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="%{text}", 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="%{text}", 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="%{text}", 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)) == "": - return np.nan - else: - return int(a) diff --git a/projects/rocprofiler-compute/src/utils/analysis_orm.py b/projects/rocprofiler-compute/src/utils/analysis_orm.py index 23647d8133..3b7315d9c0 100644 --- a/projects/rocprofiler-compute/src/utils/analysis_orm.py +++ b/projects/rocprofiler-compute/src/utils/analysis_orm.py @@ -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 " diff --git a/projects/rocprofiler-compute/src/utils/file_io.py b/projects/rocprofiler-compute/src/utils/file_io.py index 822326163d..adfe7e163f 100644 --- a/projects/rocprofiler-compute/src/utils/file_io.py +++ b/projects/rocprofiler-compute/src/utils/file_io.py @@ -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) diff --git a/projects/rocprofiler-compute/src/utils/gui.py b/projects/rocprofiler-compute/src/utils/gui.py index 5a6082b979..36fbd2c4bc 100644 --- a/projects/rocprofiler-compute/src/utils/gui.py +++ b/projects/rocprofiler-compute/src/utils/gui.py @@ -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) diff --git a/projects/rocprofiler-compute/src/utils/gui_components/header.py b/projects/rocprofiler-compute/src/utils/gui_components/header.py index 0fc0e07eac..ea2393590c 100644 --- a/projects/rocprofiler-compute/src/utils/gui_components/header.py +++ b/projects/rocprofiler-compute/src/utils/gui_components/header.py @@ -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"], diff --git a/projects/rocprofiler-compute/src/utils/gui_components/memchart.py b/projects/rocprofiler-compute/src/utils/gui_components/memchart.py index f7a92f8e78..ac1ad169a6 100644 --- a/projects/rocprofiler-compute/src/utils/gui_components/memchart.py +++ b/projects/rocprofiler-compute/src/utils/gui_components/memchart.py @@ -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 diff --git a/projects/rocprofiler-compute/src/utils/kernel_name_shortener.py b/projects/rocprofiler-compute/src/utils/kernel_name_shortener.py index fcc7b0cd24..f99cfe4f99 100644 --- a/projects/rocprofiler-compute/src/utils/kernel_name_shortener.py +++ b/projects/rocprofiler-compute/src/utils/kernel_name_shortener.py @@ -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[( )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[( )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 diff --git a/projects/rocprofiler-compute/src/utils/logger.py b/projects/rocprofiler-compute/src/utils/logger.py index 9233810125..cfd4dee7cc 100644 --- a/projects/rocprofiler-compute/src/utils/logger.py +++ b/projects/rocprofiler-compute/src/utils/logger.py @@ -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 diff --git a/projects/rocprofiler-compute/src/utils/mem_chart.py b/projects/rocprofiler-compute/src/utils/mem_chart.py index 1060c0e40f..22e6e10573 100644 --- a/projects/rocprofiler-compute/src/utils/mem_chart.py +++ b/projects/rocprofiler-compute/src/utils/mem_chart.py @@ -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 diff --git a/projects/rocprofiler-compute/src/utils/mi_gpu_spec.py b/projects/rocprofiler-compute/src/utils/mi_gpu_spec.py index 8cd378f1f6..1739228852 100644 --- a/projects/rocprofiler-compute/src/utils/mi_gpu_spec.py +++ b/projects/rocprofiler-compute/src/utils/mi_gpu_spec.py @@ -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 diff --git a/projects/rocprofiler-compute/src/utils/parser.py b/projects/rocprofiler-compute/src/utils/parser.py index bfc043e489..00259c0a71 100755 --- a/projects/rocprofiler-compute/src/utils/parser.py +++ b/projects/rocprofiler-compute/src/utils/parser.py @@ -22,16 +22,14 @@ # THE SOFTWARE. ############################################################################## - +import argparse import ast import json -import multiprocessing import re import sys import warnings -from collections import defaultdict from pathlib import Path -from typing import Union +from typing import Any, Optional, Union import astunparse import numpy as np @@ -39,6 +37,7 @@ import pandas as pd from utils import schema from utils.logger import console_debug, console_error, console_warning, demarcate +from utils.specs import MachineSpecs # ------------------------------------------------------------------------------ # Internal global definitions @@ -51,7 +50,7 @@ from utils.logger import console_debug, console_error, console_warning, demarcat # editor. Whenever change it to a new one, replace all appearances in this file. # 001 is ID of pmc_kernel_top.csv table -pmc_kernel_top_table_id = 1 +PMC_KERNEL_TOP_TABLE_ID: int = 1 # Build-in $denom defined in mongodb query: # "denom": { @@ -72,7 +71,7 @@ pmc_kernel_top_table_id = 1 # 1000000000]} # } # } -supported_denom = { +SUPPORTED_DENOM: dict[str, str] = { "per_wave": "SQ_WAVES", "per_cycle": "$GRBM_GUI_ACTIVE_PER_XCD", "per_second": "((End_Timestamp - Start_Timestamp) / 1000000000)", @@ -80,7 +79,7 @@ supported_denom = { } # Build-in defined in mongodb variables: -build_in_vars = { +BUILD_IN_VARS: dict[str, str] = { "GRBM_GUI_ACTIVE_PER_XCD": "(GRBM_GUI_ACTIVE / $num_xcd)", "GRBM_COUNT_PER_XCD": "(GRBM_COUNT / $num_xcd)", "GRBM_SPI_BUSY_PER_XCD": "(GRBM_SPI_BUSY / $num_xcd)", @@ -93,7 +92,7 @@ build_in_vars = { "hbmBandwidth": "($max_mclk / 1000 * 32 * $num_hbm_channels)", } -supported_call = { +SUPPORTED_CALL: dict[str, str] = { # If the below has a single arg, like(expr), it is an aggr, # in which case it turns into a pandas function. # If it has args like a list [], it turns into a Python function. @@ -119,21 +118,20 @@ PC_SAMPLING_NOT_ISSUE_PREFIX = "ROCPROFILER_PC_SAMPLING_INSTRUCTION_NOT_ISSUED_R # ------------------------------------------------------------------------------ -def to_min(*args): - if len(args) == 1 and isinstance(args[0], pd.core.series.Series): +def to_min(*args: Any) -> Union[float, None]: + if len(args) == 1 and isinstance(args[0], pd.Series): return args[0].min() - elif min(args) == None: + elif min(args) is None: return np.nan else: return min(args) -def to_max(*args): - if len(args) == 1 and isinstance(args[0], pd.core.series.Series): +def to_max(*args: Any) -> Union[float, np.ndarray, None]: + if len(args) == 1 and isinstance(args[0], pd.Series): return args[0].max() elif len(args) == 2 and ( - isinstance(args[0], pd.core.series.Series) - or isinstance(args[1], pd.core.series.Series) + isinstance(args[0], pd.Series) or isinstance(args[1], pd.Series) ): return np.maximum(args[0], args[1]) elif max(args) == None: @@ -142,10 +140,12 @@ def to_max(*args): return max(args) -def to_avg(a): - if str(type(a)) == "": +def to_avg( + a: Union[pd.Series, np.ndarray, list, int, float, str, np.number, None], +) -> Union[float, np.floating, None]: + if a is None: return np.nan - elif isinstance(a, pd.core.series.Series): + elif isinstance(a, pd.Series): if a.empty: return np.nan elif np.isnan(a).all(): @@ -165,14 +165,18 @@ def to_avg(a): return np.nan else: return float(a) + elif isinstance(a, str): + if not a: + return np.nan + return float(a) else: raise Exception(f"to_avg: unsupported type: {type(a)}") -def to_median(a): +def to_median(a: Union[pd.Series, None]) -> Union[float, None]: if a is None: return None - elif isinstance(a, pd.core.series.Series): + elif isinstance(a, pd.Series): with warnings.catch_warnings(): warnings.simplefilter("ignore", category=RuntimeWarning) return a.median() @@ -180,64 +184,67 @@ def to_median(a): raise Exception("to_median: unsupported type.") -def to_std(a): - if isinstance(a, pd.core.series.Series): +def to_std(a: pd.Series) -> float: + if isinstance(a, pd.Series): return a.std() else: raise Exception("to_std: unsupported type.") -def to_int(a): - if str(type(a)) == "": +def to_int( + a: Union[int, float, str, np.integer, pd.Series, None], +) -> Union[int, pd.Series, None]: + if a is None: return None - elif isinstance(a, (int, float, np.int64)): + elif isinstance(a, (int, float, np.integer)): + return int(a) + elif isinstance(a, pd.Series): + return a.astype(int) + elif isinstance(a, str): return int(a) - elif isinstance(a, pd.core.series.Series): - return a.astype("Int64") - # Do we need it? - # elif isinstance(a, str): - # return int(a) else: raise Exception("to_int: unsupported type.") -def to_sum(a): - if str(type(a)) == "": +def to_sum(a: Union[pd.Series, None]) -> Union[float, None]: + if a is None: return np.nan elif np.isnan(a).all(): return np.nan elif a.empty: return np.nan - elif isinstance(a, pd.core.series.Series): + elif isinstance(a, pd.Series): return a.sum() else: raise Exception("to_sum: unsupported type.") -def to_round(a, b): - if isinstance(a, pd.core.series.Series): +def to_round(a: Union[pd.Series, float], b: int) -> Union[pd.Series, float]: + if isinstance(a, pd.Series): return a.round(b) else: return round(a, b) -def to_quantile(a, b): +def to_quantile(a: Union[pd.Series, None], b: float) -> Union[float, None]: if a is None: return None - elif isinstance(a, pd.core.series.Series): + elif isinstance(a, pd.Series): return a.quantile(b) else: raise Exception("to_quantile: unsupported type.") -def to_mod(a, b): - if isinstance(a, pd.core.series.Series): +def to_mod( + a: Union[pd.Series, float], b: Union[pd.Series, float] +) -> Union[pd.Series, float]: + if isinstance(a, pd.Series): return a.mod(b) else: return a % b -def to_concat(a, b): +def to_concat(a: Any, b: Any) -> str: # noqa: ANN401 return str(a) + str(b) @@ -246,31 +253,19 @@ class CodeTransformer(ast.NodeTransformer): Python AST visitor to transform user defined equation string to df format """ - def visit_Call(self, node): + def visit_Call(self, node: ast.Call) -> ast.Call: self.generic_visit(node) - # print("--- debug visit_Call --- ", node.args, node.func) - # print(astunparse.dump(node)) - # print(astunparse.unparse(node)) if isinstance(node.func, ast.Name): - if node.func.id in supported_call: - node.func.id = supported_call[node.func.id] + if node.func.id in SUPPORTED_CALL: + node.func.id = SUPPORTED_CALL[node.func.id] else: - raise Exception( - "Unknown call:", node.func.id - ) # Could be removed if too strict + raise Exception("Unknown call:", node.func.id) return node - def visit_IfExp(self, node): + def visit_IfExp(self, node: ast.IfExp) -> ast.Expr: self.generic_visit(node) - # print( - # "visit_IfExp", - # type(node.test), - # type(node.body), - # type(node.orelse), - # dir(node), - # ) - if isinstance(node.body, ast.Num): + if isinstance(node.body, ast.Constant): raise Exception( "Don't support body of IF with number only! Has to be expr with " "df['column']." @@ -283,9 +278,6 @@ class CodeTransformer(ast.NodeTransformer): keywords=[], ) ) - # print("-------------") - # print(astunparse.dump(new_node)) - # print("-------------") return new_node @@ -299,21 +291,104 @@ class CodeTransformer(ast.NodeTransformer): # in correct way or work around. # - The 'raw_pmc_df' is hack code. For other data sources, like wavefront # data,We need to think about template or pass it as a parameter. - def visit_Name(self, node): + def visit_Name(self, node: ast.Name) -> Union[ast.Name, ast.Subscript]: self.generic_visit(node) - # print("-------------", node.id) - if (not node.id.startswith("ammolite__")) and (not node.id in supported_call): - new_node = ast.Subscript( + if (not node.id.startswith("ammolite__")) and (not node.id in SUPPORTED_CALL): + return ast.Subscript( value=ast.Name(id="raw_pmc_df", ctx=ast.Load()), - slice=ast.Index(value=ast.Str(s=node.id)), + slice=ast.Constant(value=node.id), ctx=ast.Load(), ) - node = new_node return node -def build_eval_string(equation, coll_level, config): +class MetricEvaluator: + """Encapsulates metric evaluation logic and eliminates global variables.""" + + def __init__( + self, + raw_pmc_df: Union[pd.DataFrame, dict], + sys_vars: dict[str, Any], + empirical_peaks: dict[str, Any], + ) -> None: + self.raw_pmc_df = raw_pmc_df + self.sys_vars = sys_vars + self.empirical_peaks = empirical_peaks + self._prepare_df_cache() + + def _prepare_df_cache(self) -> None: + """Prepare cached dataframe access for performance.""" + if isinstance(self.raw_pmc_df, dict): + self.df_cache = { + f"raw_pmc_df_{key}": self.raw_pmc_df[key] + for key in self.raw_pmc_df.keys() + } + elif isinstance(self.raw_pmc_df, pd.DataFrame): + raw_pmc_df_keys = set(self.raw_pmc_df.columns.get_level_values(0)) + self.df_cache = { + f"raw_pmc_df_{key}": self.raw_pmc_df[key] for key in raw_pmc_df_keys + } + else: + raise ValueError(f'Unknown `raw_pmc_df` type: "{type(self.raw_pmc_df)}".') + + def eval_expression(self, expr: str) -> Union[str, float, int]: + """Evaluate a single expression with proper local context.""" + try: + # Optimize dataframe access by replacing dict notation with dir_path + # variable access + opt_expr = re.sub(r"raw_pmc_df\['(.*?)'\]", r"raw_pmc_df_\1", expr) + + # Create comprehensive local context + local_expr_context = {} + local_expr_context.update(self.df_cache) + local_expr_context.update(self.sys_vars) + local_expr_context.update(self.empirical_peaks) + + # Add utility functions to local context + local_expr_context.update({ + "to_min": to_min, + "to_max": to_max, + "to_avg": to_avg, + "to_median": to_median, + "to_std": to_std, + "to_int": to_int, + "to_sum": to_sum, + "to_round": to_round, + "to_quantile": to_quantile, + "to_mod": to_mod, + "to_concat": to_concat, + }) + + eval_result = eval( + compile(opt_expr, "", "eval"), + {}, + local_expr_context, + ) + + if np.isnan(eval_result): + return "" + else: + return eval_result + + except (TypeError, NameError, KeyError) as exception: + if "empirical_peak" in str(exception): + console_warning( + f"Missing empirical peak data: {exception}. Using empty value." + ) + return "" + else: + return "" + + except AttributeError as attribute_error: + if str(attribute_error) == "'NoneType' object has no attribute 'get'": + return "" + else: + console_error("analysis", str(attribute_error)) + return "" + + +def build_eval_string(equation: str, coll_level: str, config: dict) -> str: """ Convert user defined equation string to eval executable string. For example, @@ -373,55 +448,59 @@ def build_eval_string(equation, coll_level, config): if not equation: return "" - s = str(equation) - # print("input:", s) + equation_string = str(equation) # build-in variable starts with '$', python can not handle it. # replace '$' with 'ammolite__'. - # TODO: pre-check there is no "ammolite__" in all config files. - s = re.sub(r"\$", "ammolite__", s) + equation_string = re.sub(r"\$", "ammolite__", equation_string) # convert equation string to intermediate expression in df array format - ast_node = ast.parse(s) - # print(astunparse.dump(ast_node)) + ast_node = ast.parse(equation_string) transformer = CodeTransformer() transformer.visit(ast_node) - s = astunparse.unparse(ast_node) + equation_string = astunparse.unparse(ast_node) # correct column name/label in df with [], such as TCC_HIT[0], # the target is df['TCC_HIT[0]'] - s = re.sub(r"\'\]\[(\d+)\]", r"[\g<1>]']", s) - # print("--- intermediate string: ", s) + equation_string = re.sub(r"\'\]\[(\d+)\]", r"[\g<1>]']", equation_string) + # apply coll_level if config.get("format_rocprof_output") == "rocpd": # Replace SQ_ACCUM_PREV_HIRES with coll_level_ACCUM then ignore coll_level df - s = re.sub("SQ_ACCUM_PREV_HIRES", f"{coll_level}_ACCUM", s) - s = re.sub( - r"raw_pmc_df", "raw_pmc_df['" + schema.pmc_perf_file_prefix + "']", s + equation_string = re.sub( + "SQ_ACCUM_PREV_HIRES", f"{coll_level}_ACCUM", equation_string + ) + equation_string = re.sub( + r"raw_pmc_df", + f"raw_pmc_df['{schema.PMC_PERF_FILE_PREFIX}']", + equation_string, ) else: - s = re.sub(r"raw_pmc_df", "raw_pmc_df['" + coll_level + "']", s) - # print("--- build_eval_string, return: ", s) - return s + equation_string = re.sub( + r"raw_pmc_df", f"raw_pmc_df['{coll_level}']", equation_string + ) + return equation_string -def update_denom_string(equation, unit): +def update_denominator_string(equation: str, normal_unit: str) -> str: """ Update $denom in equation with runtime normalization unit. """ if not equation: return "" - s = str(equation) + equation_string = str(equation) - if unit in supported_denom.keys(): - s = re.sub(r"\$denom", supported_denom[unit], s) + if normal_unit in SUPPORTED_DENOM.keys(): + equation_string = re.sub( + r"\$denom", SUPPORTED_DENOM[normal_unit], equation_string + ) - return s + return equation_string -def update_normUnit_string(equation, unit): +def update_normal_unit_string(equation: str, normal_unit: str) -> str: """ Update $normUnit in equation with runtime normalization unit. It is string replacement for display only. @@ -433,12 +512,12 @@ def update_normUnit_string(equation, unit): return re.sub( r"\((?P\w*)\s+\+\s+(\$normUnit\))", - r"\g " + re.sub("_", " ", unit), + rf"\g {re.sub('_', ' ', normal_unit)}", str(equation), ).capitalize() -def gen_counter_list(formula): +def gen_counter_list(formula: str) -> tuple[bool, list[str]]: function_filter = { "MIN": None, "MAX": None, @@ -506,20 +585,24 @@ def gen_counter_list(formula): return visited, counters -def calc_builtin_var(var, sys_info): +def calc_builtin_var(var: Union[int, str], sys_info: pd.Series) -> int: # type: ignore[return] """ Calculate build-in variable based on sys_info: """ if isinstance(var, int): return var elif isinstance(var, str) and var.startswith("$total_l2_chan"): - return sys_info.total_l2_chan + return int(sys_info.total_l2_chan) else: - console_error('Built-in var " %s " is not supported' % var) + console_error(f'Built-in var "{var}" is not supported') @demarcate -def build_dfs(archConfigs, filter_metrics, sys_info): +def build_dfs( + arch_configs: schema.ArchConfig, + filter_metrics: Optional[list[str]], + sys_info: pd.Series, +) -> None: """ - Build dataframe for each type of data source within each panel. Each dataframe will be used as a template to load data with each run later. @@ -528,11 +611,6 @@ def build_dfs(archConfigs, filter_metrics, sys_info): """ # TODO: more error checking for filter_metrics!! - # if filter_metrics: - # for metric in filter_metrics: - # if not metric in avail_ip_blocks: - # print("{} is not a valid metric to filter".format(metric)) - # exit(1) simple_box = { "Min": ["MIN(", ")"], "Q1": ["QUANTILE(", ", 0.25)"], @@ -541,11 +619,12 @@ def build_dfs(archConfigs, filter_metrics, sys_info): "Max": ["MAX(", ")"], } - d = {} + dfs = {} metric_list = {} dfs_type = {} metric_counters = {} - 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, data_config in data_source.items(): if ( @@ -553,14 +632,12 @@ def build_dfs(archConfigs, filter_metrics, sys_info): and "metric" in data_config and "placeholder_range" in data_config["metric"] ): - # print(data_config["metric"]) new_metrics = {} if sys_info is not None: # NB: support single placeholder for now!! p_range = data_config["metric"].pop("placeholder_range") metric, metric_expr = data_config["metric"].popitem() - # print(len(data_config["metric"])) - # data_config['metric'].clear() + for p, r in p_range.items(): # NB: We have to resolve placeholder range first if it # is a build-in var. It will be too late to do it in @@ -572,21 +649,17 @@ def build_dfs(archConfigs, filter_metrics, sys_info): new_val = {} for k, v in metric_expr.items(): new_val[k] = metric_expr[k].replace(p, str(i)) - # print(new_val) new_metrics[new_key] = new_val - # print(p_range) - # print(new_metrics) data_config["metric"] = new_metrics - # print(data_config) - # print(data_config["metric"]) - for panel_id, panel in archConfigs.panel_configs.items(): + for panel_id, panel in arch_configs.panel_configs.items(): for data_source in panel["data source"]: for type, data_config in data_source.items(): if type == "metric_table": headers = ["Metric_ID"] data_source_idx = str(data_config["id"] // 100) + if data_source_idx != 0 or ( filter_metrics and data_source_idx in filter_metrics ): @@ -616,25 +689,17 @@ def build_dfs(archConfigs, filter_metrics, sys_info): df = pd.DataFrame(columns=headers) - i = 0 - if not data_config["metric"]: data_source_idx = ( - str(data_config["id"] // 100) - + "." - + str(data_config["id"] % 100) + f"{data_config['id'] // 100}.{data_config['id'] % 100}" ) - metric_idx = data_source_idx + "." + str(i) metric_list[data_source_idx] = data_config["title"] - for key, entries in data_config["metric"].items(): + for i, (key, entries) in enumerate(data_config["metric"].items()): data_source_idx = ( - str(data_config["id"] // 100) - + "." - + str(data_config["id"] % 100) + f"{data_config['id'] // 100}.{data_config['id'] % 100}" ) - metric_idx = data_source_idx + "." + str(i) - values = [] + metric_idx = f"{data_source_idx}.{i}" eqn_content = [] if ( @@ -649,8 +714,7 @@ def build_dfs(archConfigs, filter_metrics, sys_info): # the whole IP block in filter (str(panel_id // 100) in filter_metrics) ): - values.append(metric_idx) - values.append(key) + values = [metric_idx, key] metric_list[data_source_idx] = data_config["title"] @@ -658,63 +722,51 @@ def build_dfs(archConfigs, filter_metrics, sys_info): "cli_style" in data_config and data_config["cli_style"] == "simple_box" ): - # print("~~~~~~~~~~~~~~~~~") - # print(entries) - # print("~~~~~~~~~~~~~~~~~") for k, v in entries.items(): if k == "expr": - for bk, bv in simple_box.items(): + for bv in simple_box.values(): values.append(bv[0] + v + bv[1]) else: - if k != "coll_level" and k != "alias": + if k not in {"coll_level", "alias"}: values.append(v) - else: for k, v in entries.items(): - if k != "coll_level" and k != "alias": + if k not in {"coll_level", "alias"}: values.append(v) eqn_content.append(v) if "alias" in entries.keys(): values.append(entries["alias"]) - if "coll_level" in entries.keys(): - values.append(entries["coll_level"]) - else: - values.append(schema.pmc_perf_file_prefix) + values.append( + entries.get("coll_level", schema.PMC_PERF_FILE_PREFIX) + ) if "metrics_description" in panel: - if key in panel["metrics_description"]: - values.append(panel["metrics_description"][key]) - else: - values.append("") + values.append(panel["metrics_description"].get(key, "")) - # print(headers, values) - # print(key, entries) df_new_row = pd.DataFrame([values], columns=headers) df = pd.concat([df, df_new_row]) # collect metric_list metric_list[metric_idx] = key + # generate mapping of counters and metrics - filter = {} - _visited = False + filtered_counters = {} + formula_visited = False + for formula in eqn_content: if formula is not None and formula != "None": visited, counters = gen_counter_list(formula) if visited: - _visited = True - for k in counters: - filter[k] = None + formula_visited = True + for counter in counters: + filtered_counters[counter] = None - if len(filter) > 0 or _visited: - metric_counters[key] = list(filter) - - i += 1 + if filtered_counters or formula_visited: + metric_counters[key] = list(filtered_counters) df.set_index("Metric_ID", inplace=True) - # df.set_index('Metric', inplace=True) - # print(tabulate(df, headers='keys', tablefmt='fancy_grid')) elif type == "raw_csv_table": data_source_idx = str(data_config["id"] // 100) if ( @@ -742,16 +794,18 @@ def build_dfs(archConfigs, filter_metrics, sys_info): else: df = pd.DataFrame() - d[data_config["id"]] = df + dfs[data_config["id"]] = df dfs_type[data_config["id"]] = type - setattr(archConfigs, "dfs", d) - setattr(archConfigs, "metric_list", metric_list) - setattr(archConfigs, "dfs_type", dfs_type) - setattr(archConfigs, "metric_counters", metric_counters) + setattr(arch_configs, "dfs", dfs) + setattr(arch_configs, "metric_list", metric_list) + setattr(arch_configs, "dfs_type", dfs_type) + setattr(arch_configs, "metric_counters", metric_counters) -def build_metric_value_string(dfs, dfs_type, normal_unit, profiling_config): +def build_metric_value_string( + dfs: dict, dfs_type: dict, normal_unit: str, profiling_config: dict +) -> None: """ Apply the real eval string to its field in the metric_table df. """ @@ -759,15 +813,16 @@ def build_metric_value_string(dfs, dfs_type, normal_unit, profiling_config): for id, df in dfs.items(): if dfs_type[id] == "metric_table": for expr in df.columns: - if expr in schema.supported_field: + if expr in schema.SUPPORTED_FIELD: # NB: apply all build-in before building the whole string - df[expr] = df[expr].apply(update_denom_string, unit=normal_unit) + df[expr] = df[expr].apply( + update_denominator_string, normal_unit=normal_unit + ) # NB: there should be a faster way to do with single apply if not df.empty: for i in range(df.shape[0]): row_idx_label = df.index.to_list()[i] - # print(i, "row_idx_label", row_idx_label, expr) if expr.lower() != "alias": df.at[row_idx_label, expr] = build_eval_string( df.at[row_idx_label, expr], @@ -776,64 +831,12 @@ def build_metric_value_string(dfs, dfs_type, normal_unit, profiling_config): ) elif expr.lower() == "unit" or expr.lower() == "units": - df[expr] = df[expr].apply(update_normUnit_string, unit=normal_unit) - - # print(tabulate(df, headers='keys', tablefmt='fancy_grid')) + df[expr] = df[expr].apply( + update_normal_unit_string, normal_unit=normal_unit + ) -def init_metric_evaluator( - raw_pmc_df: Union[pd.DataFrame, dict], ammolite_vars: dict, empirical_peaks: dict -) -> None: - if isinstance(raw_pmc_df, dict): - raw_pmc_df_keys = set(raw_pmc_df.keys()) - - elif isinstance(raw_pmc_df, pd.DataFrame): - raw_pmc_df_keys = set(raw_pmc_df.columns.get_level_values(0)) - - else: - raise ValueError(f"Unknown `raw_pmc_df` type '{type(raw_pmc_df)}'.") - - raw_pmc_df_items = {f"raw_pmc_df_{key}": raw_pmc_df[key] for key in raw_pmc_df_keys} - - # The globals here are not shared across all processes, - # they exist only within the subprocess's context, - # and their lifetime ends when the process terminates. - # The process-local globals are used for performance optimization. - globals().update(raw_pmc_df_items) - globals().update(ammolite_vars) - globals().update(empirical_peaks) - - -def run_metric_evaluator(row_expr: str) -> str: - try: - # cache dataframes of 'raw_pmc_df' - # this may replace some KeyErrors with NameErrors - # e.g. row_pmc_df['key'] -> row_pmc_df_key will throw NameError now - row_expr = re.sub(r"raw_pmc_df\['(.*?)'\]", r"raw_pmc_df_\1", row_expr) - out = eval(compile(row_expr, "", "eval")) - - if np.isnan(out): - return "" - - else: - return out - - except (TypeError, NameError, KeyError) as e: - if "empirical_peak" in str(e): - console_warning(f"Missing empirical peak data: {e}. Using empty value.") - return "" - else: - return "" - - except AttributeError as ae: - if str(ae) == "'NoneType' object has no attribute 'get'": - return "" - - else: - console_error("analysis", str(ae)) - - -def create_empirical_peaks_dict(empirical_peaks_df): +def create_empirical_peaks_dict(empirical_peaks_df: pd.DataFrame) -> dict[str, float]: """Create empirical peaks dictionary""" empirical_peaks = {} @@ -865,8 +868,103 @@ def create_empirical_peaks_dict(empirical_peaks_df): return empirical_peaks +def create_sys_vars(sys_info: pd.Series) -> dict[str, Union[int, float]]: + """Create variables from sys.info.""" + sys_vars_collection = {} + + sys_vars_config = [ + ("se_per_gpu", int, "se_per_gpu"), + ("pipes_per_gpu", int, "pipes_per_gpu"), + ("cu_per_gpu", int, "cu_per_gpu"), + ("simd_per_cu", int, "simd_per_cu"), + ("sqc_per_gpu", int, "sqc_per_gpu"), + ("lds_banks_per_cu", int, "lds_banks_per_cu"), + ("cur_sclk", float, "cur_sclk"), + ("cur_mclk", float, "cur_mclk"), + ("max_mclk", float, "max_mclk"), + ("max_sclk", float, "max_sclk"), + ("max_waves_per_cu", int, "max_waves_per_cu"), + ("num_hbm_channels", float, "num_hbm_channels"), + ("num_xcd", int, "num_xcd"), + ("wave_size", int, "wave_size"), + ] + + for var_name, var_type, attr_name in sys_vars_config: + variable_value = var_type(getattr(sys_info, attr_name)) + if np.isnan(variable_value) or variable_value == 0: + console_warning( + f"{attr_name} is not available in sysinfo.csv, please provide the " + "correct value using --specs-correction" + ) + sys_vars_collection[f"ammolite__{var_name}"] = variable_value + + # Special case for total_l2_chan + total_l2_channel_count = calc_builtin_var("$total_l2_chan", sys_info) + if np.isnan(total_l2_channel_count) or total_l2_channel_count == 0: + console_warning( + "total_l2_chan is not available in sysinfo.csv, please provide the correct " + "value using --specs-correction" + ) + sys_vars_collection["ammolite__total_l2_chan"] = total_l2_channel_count + + return sys_vars_collection + + +def calc_builtin_vars( + raw_pmc_df: Union[pd.DataFrame, dict], config: dict +) -> dict[str, Optional[Union[str, float, int]]]: + """Calculate built-in variables""" + # TODO: fix all $normUnit in Unit column or title + # build and eval all derived build-in global variables + builtin_vars_collection = {} + + # First pass: calculate per-XCD values + for variable_key, variable_value in BUILD_IN_VARS.items(): + if "PER_XCD" not in variable_key: + continue + + # NB: assume all built-in vars from pmc_perf.csv for now + eval_string = build_eval_string( + variable_value, schema.PMC_PERF_FILE_PREFIX, config + ) + try: + # Create temporary evaluator for this calculation + temporary_evaluator = MetricEvaluator(raw_pmc_df, {}, {}) + calculation_result = temporary_evaluator.eval_expression(eval_string) + builtin_vars_collection[f"ammolite__{variable_key}"] = calculation_result + except (TypeError, NameError, KeyError, AttributeError): + builtin_vars_collection[f"ammolite__{variable_key}"] = None + + # Second pass: calculate remaining variables that depend on per-XCD values + for variable_key, variable_value in BUILD_IN_VARS.items(): + if "PER_XCD" in variable_key: + continue + + eval_string = build_eval_string( + variable_value, schema.PMC_PERF_FILE_PREFIX, config + ) + try: + temporary_evaluator = MetricEvaluator( + raw_pmc_df, builtin_vars_collection, {} + ) + calculation_result = temporary_evaluator.eval_expression(eval_string) + builtin_vars_collection[f"ammolite__{variable_key}"] = calculation_result + except (TypeError, NameError, KeyError, AttributeError): + builtin_vars_collection[f"ammolite__{variable_key}"] = None + + return builtin_vars_collection + + @demarcate -def eval_metric(dfs, dfs_type, sys_info, empirical_peaks_df, raw_pmc_df, debug, config): +def eval_metric( + dfs: dict, + dfs_type: dict, + sys_info: pd.Series, + empirical_peaks_df: pd.DataFrame, + raw_pmc_df: Union[pd.DataFrame, dict], + debug: bool, + config: dict, +) -> None: """ Execute the expr string for each metric in the df. """ @@ -875,151 +973,21 @@ def eval_metric(dfs, dfs_type, sys_info, empirical_peaks_df, raw_pmc_df, debug, roof_only_run = sys_info.ip_blocks == "roofline" if ( (not roof_only_run) - and hasattr(raw_pmc_df["pmc_perf"], "GRBM_GUI_ACTIVE") + and hasattr(raw_pmc_df.get("pmc_perf", {}), "GRBM_GUI_ACTIVE") and (raw_pmc_df["pmc_perf"]["GRBM_GUI_ACTIVE"] == 0).any() ): console_warning("Dectected GRBM_GUI_ACTIVE == 0") console_error("Hauting execution for warning above.") - ammolite__se_per_gpu = int(sys_info.se_per_gpu) - if np.isnan(ammolite__se_per_gpu) or ammolite__se_per_gpu == 0: - console_warning( - "se_per_gpu is not available in sysinfo.csv, please provide the correct " - "value using --specs-correction" - ) - ammolite__pipes_per_gpu = int(sys_info.pipes_per_gpu) - if np.isnan(ammolite__pipes_per_gpu) or ammolite__pipes_per_gpu == 0: - console_warning( - "pipes_per_gpu is not available in sysinfo.csv, please provide the correct " - "value using --specs-correction" - ) - ammolite__cu_per_gpu = int(sys_info.cu_per_gpu) - if np.isnan(ammolite__cu_per_gpu) or ammolite__cu_per_gpu == 0: - console_warning( - "cu_per_gpu is not available in sysinfo.csv, please provide the correct " - "value using --specs-correction" - ) - ammolite__simd_per_cu = int(sys_info.simd_per_cu) # not used - if np.isnan(ammolite__simd_per_cu) or ammolite__simd_per_cu == 0: - console_warning( - "simd_per_cu is not available in sysinfo.csv, please provide the correct " - "value using --specs-correction" - ) - ammolite__sqc_per_gpu = int(sys_info.sqc_per_gpu) - if np.isnan(ammolite__sqc_per_gpu) or ammolite__sqc_per_gpu == 0: - console_warning( - "sqc_per_gpu is not available in sysinfo.csv, please provide the correct " - "value using --specs-correction" - ) - ammolite__lds_banks_per_cu = int(sys_info.lds_banks_per_cu) - if np.isnan(ammolite__lds_banks_per_cu) or ammolite__lds_banks_per_cu == 0: - console_warning( - "lds_banks_per_cu is not available in sysinfo.csv, please provide the " - "correct value using --specs-correction" - ) - ammolite__cur_sclk = float(sys_info.cur_sclk) # not used - if np.isnan(ammolite__cur_sclk) or ammolite__cur_sclk == 0: - console_warning( - "cur_sclk is not available in sysinfo.csv, please provide the correct " - "value using --specs-correction" - ) - ammolite__cur_mclk = float(sys_info.cur_mclk) # not used - if np.isnan(ammolite__cur_mclk) or ammolite__cur_mclk == 0: - console_warning( - "cur_mclk is not available in sysinfo.csv, please provide the correct " - "value using --specs-correction" - ) - ammolite__max_mclk = float(sys_info.max_mclk) - if np.isnan(ammolite__max_mclk) or ammolite__max_mclk == 0: - console_warning( - "max_mclk is not available in sysinfo.csv, please provide the correct " - "value using --specs-correction" - ) - ammolite__max_sclk = float(sys_info.max_sclk) - if np.isnan(ammolite__max_sclk) or ammolite__max_sclk == 0: - console_warning( - "max_sclk is not available in sysinfo.csv, please provide the correct " - "value using --specs-correction" - ) - ammolite__max_waves_per_cu = int(sys_info.max_waves_per_cu) - if np.isnan(ammolite__max_waves_per_cu) or ammolite__max_waves_per_cu == 0: - console_warning( - "max_waver_per_cu is not available in sysinfo.csv, please provide the " - "correct value using --specs-correction" - ) - ammolite__num_hbm_channels = float(sys_info.num_hbm_channels) - if np.isnan(ammolite__num_hbm_channels) or ammolite__num_hbm_channels == 0: - console_warning( - "num_hbm_channels is not available in sysinfo.csv, please provide the " - "correct value using --specs-correction" - ) - ammolite__total_l2_chan = calc_builtin_var("$total_l2_chan", sys_info) - if np.isnan(ammolite__total_l2_chan) or ammolite__total_l2_chan == 0: - console_warning( - "total_l2_chan is not available in sysinfo.csv, please provide the correct " - "value using --specs-correction" - ) - ammolite__num_xcd = int(sys_info.num_xcd) - if np.isnan(ammolite__num_xcd) or ammolite__num_xcd == 0: - console_warning( - "num_xcd is not available in sysinfo.csv, please provide the correct " - "value using --specs-correction" - ) - ammolite__wave_size = int(sys_info.wave_size) - if np.isnan(ammolite__wave_size) or ammolite__wave_size == 0: - console_warning( - "wave_size is not available in sysinfo.csv, please provide the correct " - "value using --specs-correction" - ) - + sys_vars = create_sys_vars(sys_info) empirical_peaks = create_empirical_peaks_dict(empirical_peaks_df) + builtin_vars = calc_builtin_vars(raw_pmc_df, config) + sys_vars.update(builtin_vars) - # TODO: fix all $normUnit in Unit column or title - # build and eval all derived build-in global variables - ammolite__build_in = {} + # Create metric evaluator + metric_evaluator = MetricEvaluator(raw_pmc_df, sys_vars, empirical_peaks) - # first pass, we do all per-xcd values, as these are used in subsequent builtins - for key, value in build_in_vars.items(): - if "PER_XCD" not in key: - continue - # NB: assume all built-in vars from pmc_perf.csv for now - s = build_eval_string(value, schema.pmc_perf_file_prefix, config) - try: - ammolite__build_in[key] = eval(compile(s, "", "eval")) - except TypeError: - ammolite__build_in[key] = None - except NameError: - ammolite__build_in[key] = None - except KeyError: - ammolite__build_in[key] = None - except AttributeError as ae: - if ae == "'NoneType' object has no attribute 'get'": - ammolite__build_in[key] = None - ammolite__GRBM_GUI_ACTIVE_PER_XCD = ammolite__build_in["GRBM_GUI_ACTIVE_PER_XCD"] # noqa: F841 - Ruff: var utilized during runtime - ammolite__GRBM_COUNT_PER_XCD = ammolite__build_in["GRBM_COUNT_PER_XCD"] # noqa: F841 - Ruff: var utilized during runtime - ammolite__GRBM_SPI_BUSY_PER_XCD = ammolite__build_in["GRBM_SPI_BUSY_PER_XCD"] # noqa: F841 - Ruff: var utilized during runtime - - for key, value in build_in_vars.items(): - # next pass, we evaluate the builtins the depend on the per-XCD values - if "PER_XCD" in key: - continue - # NB: assume all built-in vars from pmc_perf.csv for now - s = build_eval_string(value, schema.pmc_perf_file_prefix, config) - try: - ammolite__build_in[key] = eval(compile(s, "", "eval")) - except TypeError: - ammolite__build_in[key] = None - except KeyError: - ammolite__build_in[key] = None - except AttributeError as ae: - if ae == "'NoneType' object has no attribute 'get'": - ammolite__build_in[key] = None - ammolite__numActiveCUs = ammolite__build_in["numActiveCUs"] # noqa: F841 - Ruff: var utilized during runtime - ammolite__kernelBusyCycles = ammolite__build_in["kernelBusyCycles"] # noqa: F841 - Ruff: var utilized during runtime - ammolite__hbmBandwidth = ammolite__build_in["hbmBandwidth"] # noqa: F841 - Ruff: var utilized during runtime - - row_expr_indexes = [] - row_exprs = [] + exprs_to_eval = [] # Hmmm... apply + lambda should just work # df['Value'] = df['Value'].apply( @@ -1027,249 +995,203 @@ def eval_metric(dfs, dfs_type, sys_info, empirical_peaks_df, raw_pmc_df, debug, # compile(str(s), '', 'eval') # ) # ) - for id, df in dfs.items(): - if dfs_type[id] == "metric_table": - for idx, row in df.iterrows(): + for df_id, df in dfs.items(): + if dfs_type[df_id] == "metric_table": + for row_id, row in df.iterrows(): for expr in df.columns: - if expr in schema.supported_field: - if expr.lower() != "alias": - if row[expr]: - row_expr_indexes.append((id, idx, expr)) - row_exprs.append(row[expr]) + if expr in schema.SUPPORTED_FIELD and expr.lower() != "alias": + if row[expr]: + exprs_to_eval.append((df_id, row_id, expr, row[expr])) - if debug: # debug won't impact the regular calc - print("~" * 40 + "\nExpression:") - print(expr, "=", row[expr]) - print("Inputs:") - matched_vars = re.findall( - r"ammolite__\w+", row[expr] - ) - if matched_vars: - for v in matched_vars: - try: - value = eval( - compile(v, "", "eval") - ) - print("Var ", v, ":", value) - except NameError: - if "_empirical_peak" in v: - if v in empirical_peaks: - print( - "Var ", - v, - ":", - empirical_peaks[v], - ) - else: - print( - "Var ", - v, - ": [empirical peak not found]", # noqa - ) - else: - print( - "Var ", - v, - ": [not available in main thread]", # noqa - ) - matched_cols = re.findall( - r"raw_pmc_df\['\w+'\]\['\w+'\]", row[expr] - ) - if matched_cols: - for c in matched_cols: - m = re.match( - r"raw_pmc_df\['(\w+)'\]\['(\w+)'\]", c - ) - try: - t = raw_pmc_df[m.group(1)][ # noqa: F841 - m.group(2) - ].to_list() - print(c) - print( - raw_pmc_df[m.group(1)][ - m.group(2) - ].to_list() - ) - except KeyError as ke: - console_warning( - "Skipping entry. " - "Encountered a missing " - "key\n{}".format(str(ke)) - ) - # print( - # tabulate(raw_pmc_df[m.group(1)][ - # m.group(2)], - # headers='keys', - # tablefmt='fancy_grid')) - print("\nOutput:") - try: - print( - eval(compile(row[expr], "", "eval")) - ) - print("~" * 40) - except NameError as ne: - if "empirical_peak" in str(ne): - console_warning( - "Skipping debug evaluation. Empirical peak variables " # noqa - "not available in main thread: {}".format( # noqa - str(ne) - ) - ) - else: - console_warning( - "Skipping debug evaluation. Variable not available: {}".format( # noqa - str(ne) - ) - ) - print("~" * 40) - except TypeError: - console_warning( - "Skipping entry. Encountered a missing " - "counter\n" - "{} has been assigned to None\n{}".format( - expr, - np.nan, - ) - ) - except KeyError as ke: - # We can't guarantee that [] accesses are safe. - console_warning( - "Skipping entry. Encountered a missing " - "key\n{}".format(str(ke)) - ) - except AttributeError as ae: - if ( - str(ae) - == "'NoneType' object has no attribute " - "'get'" - ): - console_warning( - "Skipping entry. Encountered a missing " - "csv\n{}".format(np.nan) - ) - else: - console_error("analysis", str(ae)) - else: - # If not insert nan, the whole col might be treated - # as string but not nubmer if there is NONE - row[expr] = "" + if debug: + debug_evaluate_metrics( + expr, row[expr], metric_evaluator, raw_pmc_df + ) + else: + # If not insert nan, the whole col might be treated + # as string but not nubmer if there is NONE + row[expr] = "" - # print(tabulate(df, headers='keys', tablefmt='fancy_grid')) + for df_id, row_id, col, expr in exprs_to_eval: + eval_result = metric_evaluator.eval_expression(expr) + dfs[df_id].loc[row_id, col] = eval_result - ammolite_vars = { - key: val for key, val in locals().items() if key.startswith("ammolite__") - } - # Empirically, 16 is about as much as we need. - processes = min(16, multiprocessing.cpu_count() // 2) - # breakpoint() - with multiprocessing.Pool( - processes=processes, - initializer=init_metric_evaluator, - initargs=(raw_pmc_df, ammolite_vars, empirical_peaks), - ) as pool: - outs = pool.map(run_metric_evaluator, row_exprs) +def debug_evaluate_metrics( + expr: str, + row_expr: str, + metric_evaluator: MetricEvaluator, + raw_pmc_df: Union[pd.DataFrame, dict], +) -> None: + """Debug helper for expression evaluation.""" + print("~" * 40 + "\nExpression:") + print(f"{expr} = {row_expr}") + print("Inputs:") - for (df_id, row, col), out in zip(row_expr_indexes, outs): - dfs[df_id].loc[row, col] = out + # Show matched variables + matched_vars = re.findall(r"ammolite__\w+", row_expr) + if matched_vars: + for vars in matched_vars: + if vars in metric_evaluator.sys_vars: + print(f"Var {vars}: {metric_evaluator.sys_vars[vars]}") + elif vars in metric_evaluator.empirical_peaks: + print(f"Var {vars}: {metric_evaluator.empirical_peaks[vars]}") + else: + print(f"Var {vars}: [not found]") + + # Show matched columns + matched_cols = re.findall(r"raw_pmc_df\['\w+'\]\['\w+'\]", row_expr) + if matched_cols: + for cols in matched_cols: + col_match = re.match(r"raw_pmc_df\['(\w+)'\]\['(\w+)'\]", cols) + try: + if isinstance(raw_pmc_df, dict) and col_match.group(1) in raw_pmc_df: + column_data = raw_pmc_df[col_match.group(1)][ + col_match.group(2) + ].to_list() + print(f"{cols}: {column_data}") + except KeyError as key_error: + console_warning( + f"Skipping entry. Encountered a missing key: {key_error}" + ) + + print("\nOutput:") + try: + eval_result = metric_evaluator.eval_expression(row_expr) + print(eval_result) + print("~" * 40) + except Exception as e: + console_warning(f"Debug evaluation failed: {e}") + print("~" * 40) @demarcate -def apply_filters(workload, dir, is_gui, debug): +def apply_filters( + workload: schema.Workload, dir_path: str, is_gui: bool, debug: bool +) -> pd.DataFrame: """ Apply user's filters to the raw_pmc df. """ # TODO: error out properly if filters out of bound - ret_df = workload.raw_pmc + filtered_df = workload.raw_pmc + # Apply node filter if workload.filter_nodes: - ret_df = ret_df.loc[ - ret_df[schema.pmc_perf_file_prefix]["Node"] + filtered_df = filtered_df.loc[ + filtered_df[schema.PMC_PERF_FILE_PREFIX]["Node"] .astype(str) .isin([workload.filter_gpu_ids]) ] - if ret_df.empty: - console_error("analysis", "{} is invalid".format(workload.filter_nodes)) + if filtered_df.empty: + console_error("analysis", f"{workload.filter_nodes} is invalid") + # Apply GPU ID filter if workload.filter_gpu_ids: - ret_df = ret_df.loc[ - ret_df[schema.pmc_perf_file_prefix]["GPU_ID"] + filtered_df = filtered_df.loc[ + filtered_df[schema.PMC_PERF_FILE_PREFIX]["GPU_ID"] .astype(str) .isin([workload.filter_gpu_ids]) ] - if ret_df.empty: - console_error( - "analysis", "{} is an invalid gpu-id".format(workload.filter_gpu_ids) - ) + if filtered_df.empty: + console_error("analysis", f"{workload.filter_gpu_ids} is an invalid gpu-id") + # Apply kernel filter # NB: # Kernel id is unique! # We pick up kernel names from kerne ids first. # Then filter valid entries with kernel names. if workload.filter_kernel_ids: - if all(isinstance(kid, int) for kid in workload.filter_kernel_ids): - # Verify valid kernel filter - kernels_df = pd.read_csv(str(Path(dir).joinpath("pmc_kernel_top.csv"))) - for kernel_id in workload.filter_kernel_ids: - if kernel_id >= len(kernels_df["Kernel_Name"]): - console_error( - "{} is an invalid kernel id. " - "Please enter an id between 0-{}".format( - kernel_id, - len(kernels_df["Kernel_Name"]) - 1, - ) - ) - kernels = [] - # NB: mark selected kernels with "*" - # Todo: fix it for unaligned comparison - kernel_top_df = workload.dfs[pmc_kernel_top_table_id] - kernel_top_df["S"] = "" - for kernel_id in workload.filter_kernel_ids: - # print("------- ", kernel_id) - kernels.append(kernel_top_df.loc[kernel_id, "Kernel_Name"]) - kernel_top_df.loc[kernel_id, "S"] = "*" - - if kernels: - # print("fitlered df:", len(df.index)) - ret_df = ret_df.loc[ - ret_df[schema.pmc_perf_file_prefix]["Kernel_Name"].isin(kernels) - ] - elif all(isinstance(kid, str) for kid in workload.filter_kernel_ids): - df_cleaned = ret_df[schema.pmc_perf_file_prefix]["Kernel_Name"].apply( - lambda x: x.strip() if isinstance(x, str) else x - ) - ret_df = ret_df.loc[df_cleaned.isin(workload.filter_kernel_ids)] - else: - console_error( - "analyze", - "Mixing kernel indices and string filters is not currently supported", - ) + filtered_df = apply_kernel_filter(filtered_df, workload, dir_path) + # Apply dispatch filter if workload.filter_dispatch_ids: - # NB: support ignoring the 1st n dispatched execution by '> n' - # The better way may be parsing python slice string - for d in workload.filter_dispatch_ids: - if int(d) >= len(ret_df): # subtract 2 bc of the two header rows - console_error("analysis", "{} is an invalid dispatch id.".format(d)) - if ">" in workload.filter_dispatch_ids[0]: - m = re.match(r"\> (\d+)", workload.filter_dispatch_ids[0]) - ret_df = ret_df[ - ret_df[schema.pmc_perf_file_prefix]["Dispatch_ID"] > int(m.group(1)) - ] - else: - dispatches = [int(x) for x in workload.filter_dispatch_ids] - ret_df = ret_df.loc[dispatches] + filtered_df = apply_dispatch_filter(filtered_df, workload) + if debug: print("~" * 40, "\nraw pmc df info:\n") print(workload.raw_pmc.info()) print("~" * 40, "\nfiltered pmc df info:") - print(ret_df.info()) + print(filtered_df.info()) - return ret_df + return filtered_df -def find_key_recursively(data, search_key): +def apply_kernel_filter( + df: pd.DataFrame, workload: schema.Workload, dir_path_path: str +) -> pd.DataFrame: + """Apply kernel ID or name filters.""" + if all(isinstance(kernel_id, int) for kernel_id in workload.filter_kernel_ids): + # Handle integer kernel IDs + kernels_dataframe = pd.read_csv(Path(dir_path_path) / "pmc_kernel_top.csv") + + # Validate kernel IDs + for kernel_id in workload.filter_kernel_ids: + if kernel_id >= len(kernels_dataframe["Kernel_Name"]): + console_error( + f"{kernel_id} is an invalid kernel id. " + "Please enter an id between 0-" + f"{len(kernels_dataframe['Kernel_Name']) - 1}" + ) + + # Extract kernel names and mark selected kernels with "*" + # TODO: fix it for unaligned comparison + selected_kernels = [] + kernel_top_dataframe = workload.dfs[PMC_KERNEL_TOP_TABLE_ID] + kernel_top_dataframe["S"] = "" + + for kernel_id in workload.filter_kernel_ids: + selected_kernels.append(kernel_top_dataframe.loc[kernel_id, "Kernel_Name"]) + kernel_top_dataframe.loc[kernel_id, "S"] = "*" + + if selected_kernels: + df = df.loc[ + df[schema.PMC_PERF_FILE_PREFIX]["Kernel_Name"].isin(selected_kernels) + ] + + elif all(isinstance(kernel_id, str) for kernel_id in workload.filter_kernel_ids): + # Handle string kernel names + cleaned_dataframe = df[schema.PMC_PERF_FILE_PREFIX]["Kernel_Name"].apply( + lambda kernel_name: ( + kernel_name.strip() if isinstance(kernel_name, str) else kernel_name + ) + ) + df = df.loc[cleaned_dataframe.isin(workload.filter_kernel_ids)] + else: + console_error( + "analyze", + "Mixing kernel indices and string filters is not currently supported", + ) + + return df + + +def apply_dispatch_filter(df: pd.DataFrame, workload: schema.Workload) -> pd.DataFrame: + """Apply dispatch ID filters.""" + # NB: support ignoring the 1st n dispatched execution by '> n' + # The better way may be parsing python slice string + for dispatch_id in workload.filter_dispatch_ids: + if int(dispatch_id) >= len(df): # subtract 2 bc of the two header rows + console_error("analysis", f"{dispatch_id} is an invalid dispatch id.") + + if ">" in workload.filter_dispatch_ids[0]: + dispatch_match = re.match(r"\> (\d+)", workload.filter_dispatch_ids[0]) + df = df[ + df[schema.PMC_PERF_FILE_PREFIX]["Dispatch_ID"] + > int(dispatch_match.group(1)) + ] + else: + selected_dispatches = [ + int(dispatch_str) for dispatch_str in workload.filter_dispatch_ids + ] + df = df.loc[selected_dispatches] + + return df + + +def find_key_recursively( + data: Union[dict, list], search_key: str +) -> Union[list, dict, None]: """ Recursively search for the search_key in the given data (which can be a dict or list). @@ -1278,115 +1200,116 @@ def find_key_recursively(data, search_key): if isinstance(data, dict): for key, value in data.items(): if key == search_key: - # Convert JSON value to DataFrame - # return pd.read_json(StringIO(json.dumps(value))) return value elif isinstance(value, (dict, list)): result = find_key_recursively(value, search_key) - if result is not None: - return result # Return the DataFrame if found + if result: + return result elif isinstance(data, list): for item in data: result = find_key_recursively(item, search_key) - if result is not None: - return result # Return the DataFrame if found + if result: + return result return None # Return None if the key was not found -def search_key_in_json(file_path, search_key): +def search_key_in_json(file_path: Path, search_key: str) -> Union[list, dict, None]: # FIXME: # Load the entire JSON into memory. # Should not use for large file. - with open(file_path, "r") as file: + with open(file_path) as file: data = json.load(file) found = find_key_recursively(data, search_key) - if found == None: - console_error(f"Key '{search_key}' not found in the JSON file.") + if found is None: + console_error(f'Key "{search_key}" not found in the JSON file.') return found -def search_pc_sampling_record(records): +def search_pc_sampling_record( + records: Union[list[dict], dict], +) -> Optional[list[tuple]]: """ Search PC sampling records, and group and sort them """ # NB: # The field stall_reason is vailid only for HW stochastic pc sampling. + # TODO: might save wavefront count for HW stochastic pc sampling? - # Todo: might save wavefront count for HW stochastic pc sampling? - - grouped_data = defaultdict( - lambda: defaultdict( - lambda: { - "count": 0, - "count_issued": 0, - "count_stalled": 0, - "inst_index": None, - "stall_reason": { - "NONE": 0, - # No instruction available in the instruction cache. - "NO_INSTRUCTION_AVAILABLE": 0, - "ALU_DEPENDENCY": 0, # ALU dependency not resolved. - "WAITCNT": 0, - "INTERNAL_INSTRUCTION": 0, # Wave executes an internal instruction. - "BARRIER_WAIT": 0, - "ARBITER_NOT_WIN": 0, # The instruction did not win the arbiter. - "ARBITER_WIN_EX_STALL": 0, - # Arbiter issued an instruction, but the execution pipe - # pushed it back from execution. - "OTHER_WAIT": 0, - # Other types of wait (e.g., wait for XNACK acknowledgment). - "SLEEP_WAIT": 0, - "LAST": 0, - }, - } - ) - ) - - rocp_inst_not_issued_prefix_len = len(PC_SAMPLING_NOT_ISSUE_PREFIX) - - # Populate grouped_data - for i, item in enumerate(records): - pc_info = item["record"].get("pc", {}) - code_object_id = pc_info.get("code_object_id") - code_object_offset = pc_info.get("code_object_offset") - snapshot = item["record"].get("snapshot", {}) - inst_index = item.get("inst_index") - issued = item["record"].get("wave_issued") - - # Todo: opt me - if ( - code_object_id is not None - and code_object_offset is not None - and inst_index is not None - ): - grouped_data[code_object_id][code_object_offset]["count"] += 1 - # NB: the write here could be duplicated. If there is perf issue, - # We might want to opt it. - grouped_data[code_object_id][code_object_offset]["inst_index"] = inst_index - - if len(snapshot): - if issued: - grouped_data[code_object_id][code_object_offset][ - "count_issued" - ] += 1 - else: - grouped_data[code_object_id][code_object_offset][ - "count_stalled" - ] += 1 - grouped_data[code_object_id][code_object_offset]["stall_reason"][ - snapshot.get("stall_reason")[rocp_inst_not_issued_prefix_len:] - ] += 1 - # print( - # inst_index, - # grouped_data[code_object_id][code_object_offset]["stall_reason"], - # ) - - if len(grouped_data) == 0: + if not records: console_warning("PC sampling: no pc sampling record found!") return None - # print(grouped_data) + rocp_inst_not_issued_prefix_len = len(PC_SAMPLING_NOT_ISSUE_PREFIX) + + grouped_data = {} + stall_reason_keys = { + "NONE": 0, + # No instruction available in the instruction cache. + "NO_INSTRUCTION_AVAILABLE": 0, + "ALU_DEPENDENCY": 0, # ALU dependency not resolved. + "WAITCNT": 0, + "INTERNAL_INSTRUCTION": 0, # Wave executes an internal instruction. + "BARRIER_WAIT": 0, + "ARBITER_NOT_WIN": 0, # The instruction did not win the arbiter. + "ARBITER_WIN_EX_STALL": 0, + # Arbiter issued an instruction, but the execution pipe + # pushed it back from execution. + "OTHER_WAIT": 0, + # Other types of wait (e.g., wait for XNACK acknowledgment). + "SLEEP_WAIT": 0, + "LAST": 0, + } + + # Populate grouped_data + for item in records: + record = item["record"] + pc_info = record.get("pc", {}) + + code_object_id = pc_info.get("code_object_id") + code_object_offset = pc_info.get("code_object_offset") + inst_index = item.get("inst_index") + + if None in (code_object_id, code_object_offset, inst_index): + continue + + # Create composite key + key = (code_object_id, code_object_offset) + + snapshot = record.get("snapshot", {}) + issued = record.get("wave_issued") + + if key not in grouped_data: + grouped_data[key] = [0, 0, 0, inst_index, {}] + + # Update counts + entry = grouped_data[key] + entry[0] += 1 # count + entry[3] = inst_index # inst_index + + # Process snapshot data + if snapshot: + if issued: + entry[1] += 1 # count_issued + else: + entry[2] += 1 # count_stalled + + # Process stall reason only when stalled + stall_reason = snapshot.get("stall_reason") + if stall_reason: + # Extract reason key with bounds checking + if len(stall_reason) > rocp_inst_not_issued_prefix_len: + reason_key = stall_reason[rocp_inst_not_issued_prefix_len:] + # Only track known stall reasons + if reason_key in stall_reason_keys: + stall_reasons = entry[4] + stall_reasons[reason_key] = ( + stall_reasons.get(reason_key, 0) + 1 + ) + + if not grouped_data: + console_warning("PC sampling: no pc sampling record found!") + return None # Convert to sorted list of tuples: # (code_object_id, inst_index, code_object_offset, count) @@ -1456,10 +1379,10 @@ def load_pc_sampling_data_per_kernel( break if not kernel_info: - console_warning("PC sampling: can not find the kernel %s " % kernel_name) + console_warning(f"PC sampling: can not find the kernel {kernel_name}") return pd.DataFrame() else: - console_debug("PC sampling: kernel %s " % kernel_info) + console_debug(f"PC sampling: kernel {kernel_info}") filtered_sorted_list = sorted( [ @@ -1482,17 +1405,15 @@ def load_pc_sampling_data_per_kernel( kernel_info["potential_end_offset"] = sys.maxsize break - # print("kernel_info", kernel_info) - pc_sample_key_loc = ( search_key_in_json(file_name, "pc_sample_host_trap") if method == "host_trap" else search_key_in_json(file_name, "pc_sample_stochastic") ) - # print(type(pc_sample_key_loc), len(pc_sample_key_loc)) - # print(pc_sample_key_loc[0]["record"].get("pc", {}).get("code_object_offset")) - # print(search_pc_sampling_record(pc_sample_key_loc)) + if not pc_sample_key_loc: + console_warning("PC sampling: can not find pc sample.") + return pd.DataFrame() df = pd.DataFrame( search_pc_sampling_record(pc_sample_key_loc), @@ -1524,30 +1445,26 @@ def load_pc_sampling_data_per_kernel( df["offset"] = df["offset"].apply(lambda x: hex(x)) - # df["stall_reason"] = df["stall_reason"].apply( - # lambda x: ', '.join( - # f"{k}: {v}" - # for k, v in x - # ) - # ) - + # Add instruction and source line information pc_sample_instructions = search_key_in_json(file_name, "pc_sample_instructions") - # print(pc_sample_instructions) - df["instruction"] = df["inst_index"].apply( - lambda x: pc_sample_instructions[x] if x < len(pc_sample_instructions) else None - ) + if pc_sample_instructions: + df["instruction"] = df["inst_index"].apply( + lambda x: ( + pc_sample_instructions[x] if x < len(pc_sample_instructions) else None + ) + ) pc_sample_comments = search_key_in_json(file_name, "pc_sample_comments") - df["source_line"] = df["inst_index"].apply( - lambda x: ( - ".../" + Path(pc_sample_comments[x]).name - if x < len(pc_sample_instructions) - else None + if pc_sample_comments: + df["source_line"] = df["inst_index"].apply( + lambda x: ( + f".../{Path(pc_sample_comments[x]).name}" + if x < len(pc_sample_comments) + else None + ) ) - ) - - # print(df[["source_line", "instruction", "offset", "count", "stall_reason"]]) + # Return sorted data based on sorting type if sorting_type == "offset": return ( df[["source_line", "instruction", "offset", "count"]] @@ -1586,13 +1503,15 @@ def load_pc_sampling_data_per_kernel( @demarcate -def load_pc_sampling_data(workload, dir, file_prefix, sorting_type): +def load_pc_sampling_data( + workload: schema.Workload, dir_path: str, file_prefix: str, sorting_type: str +) -> pd.DataFrame: """ Load PC sampling raw data, filter and sort it by specified conditions, then return df. """ - if file_prefix.lower() == "none": + if not file_prefix or file_prefix.lower() == "none": return pd.DataFrame() pc_sampling_method = None @@ -1601,25 +1520,22 @@ def load_pc_sampling_data(workload, dir, file_prefix, sorting_type): # - The default file name is subject to changes from rocprofv3 # - Prioritize stochastic # - Alternatively, we could check pc_sampling_method in json - csv_file_path = Path.joinpath( - Path(dir), file_prefix + "_pc_sampling_stochastic.csv" - ) - if csv_file_path.exists(): - pc_sampling_method = "stochastic" - else: - csv_file_path = Path.joinpath( - Path(dir), file_prefix + "_pc_sampling_host_trap.csv" - ) - if csv_file_path.exists(): - pc_sampling_method = "host_trap" + stochastic_path = Path(dir_path) / f"{file_prefix}_pc_sampling_stochastic.csv" + host_trap_path = Path(dir_path) / f"{file_prefix}_pc_sampling_host_trap.csv" - if pc_sampling_method == None: + if stochastic_path.exists(): + pc_sampling_method = "stochastic" + csv_file_path = stochastic_path + elif host_trap_path.exists(): + pc_sampling_method = "host_trap" + csv_file_path = host_trap_path + else: console_warning( - "PC sampling: can not detect pc sampling method without %s " % csv_file_path + f"PC sampling: can not detect pc sampling method for {file_prefix}" ) return pd.DataFrame() - # No kernel filter, return grouped and sorted csv directly + # No kernel filter, return grouped and sorted csv dir_pathectly if not workload.filter_kernel_ids: df = pd.read_csv(csv_file_path) # Group by 'Instruction_Comment' and count occurrences @@ -1634,36 +1550,32 @@ def load_pc_sampling_data(workload, dir, file_prefix, sorting_type): ) grouped_counts = grouped_counts[["source_line", "instruction", "count"]] - grouped_counts["source_line"] = grouped_counts["source_line"].apply( - lambda x: (".../" + Path(x).name) + lambda x: f".../{Path(x).name}" ) # Sort by the count of occurrences - sorted_counts = grouped_counts.sort_values(by="count", ascending=False) - # print(sorted_counts.info) - - return sorted_counts + return grouped_counts.sort_values(by="count", ascending=False) elif len(workload.filter_kernel_ids) > 1: console_error( "PC sampling supports single kernel only! Please specify -k with " - "single kernel." + "single kernel.", + exit=False, ) return pd.DataFrame() elif len(workload.filter_kernel_ids) == 1: - # print("kernel id", workload.filter_kernel_ids[0]) # NB: the default file name is subject to changes from rocprofv3/rocprofiler_sdk - json_file_path = Path.joinpath(Path(dir), file_prefix + "_results.json") + json_file_path = Path(dir_path) / f"{file_prefix}_results.json" if not json_file_path.exists(): - console_error("PC sampling: can not read %s " % json_file_path) + console_error(f"PC sampling: can not read {json_file_path}", exit=False) return pd.DataFrame() else: # NB: # We should find better way to remove the dependency on kernel_top_table - kernel_top_df = workload.dfs[pmc_kernel_top_table_id] - file = Path.joinpath(Path(dir), kernel_top_df.loc[0, "from_csv"]) + kernel_top_df = workload.dfs[PMC_KERNEL_TOP_TABLE_ID] + file = Path(dir_path) / str(kernel_top_df.loc[0, "from_csv"]) kernel_name = pd.read_csv(file).loc[ workload.filter_kernel_ids[0], "Kernel_Name" ] @@ -1676,90 +1588,95 @@ def load_pc_sampling_data(workload, dir, file_prefix, sorting_type): @demarcate -def load_kernel_top(workload, dir, args): +def load_non_mertrics_table( + workload: schema.Workload, dir_path: str, args: argparse.Namespace +) -> None: # NB: # - Do pmc_kernel_top.csv loading before eval_metric because we need the # kernel names. # - There might be a better way/timing to load raw_csv_table. - # FIXME: - # the func name load_kernel_top needs to be changed to load_non_mertrics_table - # NB: # "from_csv", "from_csv_columnwise", and "from_pc_sampling" # are 3 internal symbols converted in build_dfs() for non-metrics table. # There might be better way to store these info without the orginal entry. tmp = {} - for id, df in workload.dfs.items(): + for df_id, df in workload.dfs.items(): if "from_csv" in df.columns: - file = Path.joinpath(Path(dir), df.loc[0, "from_csv"]) - if file.exists(): - tmp[id] = pd.read_csv(file) + csv_file = Path(dir_path) / str(df.loc[0, "from_csv"]) + if csv_file.exists(): + tmp[df_id] = pd.read_csv(csv_file) else: console_warning( - f"Couldn't load {file.name}. " + f"Couldn't load {csv_file.name}. " "This may result in missing analysis data." ) # NB: Special case for sysinfo. Probably room for improvement in this whole # function design elif "from_csv_columnwise" in df.columns and id == 101: - tmp[id] = workload.sys_info.transpose() + tmp[df_id] = workload.sys_info.transpose() # All transposed columns should be marked with a general header - tmp[id].columns = ["Info"] + tmp[df_id].columns = ["Info"] elif "from_csv_columnwise" in df.columns: # NB: # Another way might be doing transpose in tty like metric_table. # But we need to figure out headers and comparison properly. - file = Path.joinpath(Path(dir), df.loc[0, "from_csv_columnwise"]) - if file.exists(): - tmp[id] = pd.read_csv(file).transpose() + csv_file = Path(dir_path) / str(df.loc[0, "from_csv_columnwise"]) + if csv_file.exists(): + tmp[df_id] = pd.read_csv(csv_file).transpose() # NB: # All transposed columns should be marked with a general header, # so tty could detect them and show them correctly in comparison. - tmp[id].columns = ["Info"] + tmp[df_id].columns = ["Info"] else: console_warning( - f"Couldn't load {file.name}. " + f"Couldn't load {csv_file.name}. " "This may result in missing analysis data." ) elif "from_pc_sampling" in df.columns: - tmp[id] = load_pc_sampling_data( + tmp[df_id] = load_pc_sampling_data( workload, - dir, + dir_path, df.loc[0, "from_pc_sampling"], args.pc_sampling_sorting_type, ) - # print("table id", id, "filter_kernel_ids", workload.filter_kernel_ids) workload.dfs.update(tmp) @demarcate -def load_table_data(workload, dir, is_gui, args, config, skipKernelTop=False): +def load_table_data( + workload: schema.Workload, + dir_path: str, + is_gui: bool, + args: argparse.Namespace, + config: dict, + skip_kernel_top: bool = False, +) -> None: """ - Load data for all "raw_csv_table" - Load data for "pc_sampling_table" - Calculate mertric value for all "metric_table" """ - if not skipKernelTop: - load_kernel_top(workload, dir, args) + if not skip_kernel_top: + load_non_mertrics_table(workload, dir_path, args) eval_metric( workload.dfs, workload.dfs_type, workload.sys_info.iloc[0], workload.roofline_peaks, - apply_filters(workload, dir, is_gui, args.debug), + apply_filters(workload, dir_path, is_gui, args.debug), args.debug, config, ) -def build_comparable_columns(time_unit): +def build_comparable_columns(time_unit: str) -> list[str]: """ Build comparable columns/headers for display """ - comparable_columns = schema.supported_field + comparable_columns = schema.SUPPORTED_FIELD top_stat_base = [ "Count", "Sum", @@ -1770,25 +1687,28 @@ def build_comparable_columns(time_unit): ] for h in top_stat_base: - comparable_columns.append(h + "(" + time_unit + ")") + comparable_columns.append(f"{h}({time_unit})") return comparable_columns -def correct_sys_info(mspec, specs_correction: dict): +def correct_sys_info(mspec: MachineSpecs, specs_correction: str) -> pd.DataFrame: """ - Correct system spec items manually + Correct system spec items manually based on user-provided corrections. """ - # todo: more err checking for string specs_correction + # Parse key:value pairs + pairs: dict[str, str] = {} + for pair in specs_correction.split(","): + if ":" in pair: + key, value = pair.split(":", 1) + pairs[key.strip()] = value.strip() - pairs = dict(re.findall(r"(\w+):\s*(\d+)", specs_correction)) - - for k, v in pairs.items(): - if not hasattr(mspec, str(k)): + # Apply corrections + for key, value in pairs.items(): + if hasattr(mspec, key): + setattr(mspec, key, value) + else: console_error( - "analyze", - f"Invalid specs correction '{k}'. Please use --specs option " - f"to peak valid specs", + "analyze", f'Invalid spec "{key}". Use --specs to see valid options' ) - setattr(mspec, str(k), v) return mspec.get_class_members() diff --git a/projects/rocprofiler-compute/src/utils/rocpd_data.py b/projects/rocprofiler-compute/src/utils/rocpd_data.py index 8fe99fccd5..eb04921fd4 100644 --- a/projects/rocprofiler-compute/src/utils/rocpd_data.py +++ b/projects/rocprofiler-compute/src/utils/rocpd_data.py @@ -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", diff --git a/projects/rocprofiler-compute/src/utils/roofline_calc.py b/projects/rocprofiler-compute/src/utils/roofline_calc.py index 0c69976cef..6f0b7b0348 100644 --- a/projects/rocprofiler-compute/src/utils/roofline_calc.py +++ b/projects/rocprofiler-compute/src/utils/roofline_calc.py @@ -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) diff --git a/projects/rocprofiler-compute/src/utils/schema.py b/projects/rocprofiler-compute/src/utils/schema.py index b61a584189..b12bbfa2e3 100644 --- a/projects/rocprofiler-compute/src/utils/schema.py +++ b/projects/rocprofiler-compute/src/utils/schema.py @@ -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" diff --git a/projects/rocprofiler-compute/src/utils/specs.py b/projects/rocprofiler-compute/src/utils/specs.py index ed01fb3bef..7819583421 100644 --- a/projects/rocprofiler-compute/src/utils/specs.py +++ b/projects/rocprofiler-compute/src/utils/specs.py @@ -24,6 +24,9 @@ ############################################################################## """Get host/gpu specs.""" +from __future__ import annotations + +import argparse import importlib import json import os @@ -34,16 +37,25 @@ from dataclasses import dataclass, field, fields from datetime import datetime from math import ceil from pathlib import Path as path +from typing import Any, Optional, TypeVar import pandas as pd import config -from utils.logger import console_debug, console_error, console_log, console_warning +from utils.logger import ( + console_debug, + console_error, + console_log, + console_warning, + demarcate, +) from utils.mi_gpu_spec import mi_gpu_specs from utils.tty import get_table_string from utils.utils import get_version -VERSION_LOC = [ +T = TypeVar("T") + +VERSION_LOC: list[str] = [ "version", "version-dev", "version-hip-libraries", @@ -55,110 +67,201 @@ VERSION_LOC = [ ] -def detect_arch(_rocminfo): - for idx1, linetext in enumerate(_rocminfo): - # NOTE: currently supported socs are gfx archs only - gpu_arch = search(r"^\s*Name\s*:\s* ([Gg][Ff][Xx][a-zA-Z0-9]+).*\s*$", linetext) - if gpu_arch in mi_gpu_specs.get_gpu_series_dict().keys(): - break - if str(gpu_arch) in mi_gpu_specs.get_gpu_series_dict().keys(): - gpu_arch = str(gpu_arch) - break - if not gpu_arch in mi_gpu_specs.get_gpu_series_dict().keys(): - console_error("Cannot find a supported arch in rocminfo: " + str(gpu_arch)) +def detect_arch(rocminfo_lines: list[str]) -> Optional[tuple[str, int]]: + for idx1, line_text in enumerate(rocminfo_lines): + gpu_arch = search( + r"^\s*Name\s*:\s* ([Gg][Ff][Xx][a-zA-Z0-9]+).*\s*$", line_text + ) + if gpu_arch and gpu_arch in mi_gpu_specs.get_gpu_series_dict(): + return (gpu_arch, idx1) + + console_error("Cannot find a supported arch in rocminfo") + + +def detect_gpu_chip_id(rocminfo_lines: list[str]) -> Optional[str]: + chip_id_dict = mi_gpu_specs.get_chip_id_dict() + unknown_chips: list[str] = [] + + for idx, line_text in enumerate(rocminfo_lines): + chip_id = search(r"^\s*Chip ID\s*:\s* ([0-9]+).*\s*$", line_text) + if chip_id: + # Check if this chip ID is valid (known) + if chip_id in chip_id_dict or int(chip_id) in chip_id_dict: + return chip_id # Return first valid chip ID found + else: + unknown_chips.append(chip_id) + + # Exhausted all lines - handle the cases where no valid chip was found + if unknown_chips: + for chip_id in unknown_chips: + console_warning(f"Unknown Chip ID(s) detected: {chip_id}") else: - return (gpu_arch, idx1) + console_warning("No Chip ID detected") - -def detect_gpu_chip_id(_rocminfo): - gpu_chip_id = None - - for idx1, linetext in enumerate(_rocminfo): - # NOTE: current supported socs only have numbers in Chip ID - chip_found = search(r"^\s*Chip ID\s*:\s* ([0-9]+).*\s*$", linetext) - if chip_found: - gpu_chip_id = str(chip_found) - break - - if not gpu_chip_id: - console_warning("No Chip ID detected: " + str(gpu_chip_id)) - elif ( - gpu_chip_id not in mi_gpu_specs.get_chip_id_dict().keys() - and int(gpu_chip_id) not in mi_gpu_specs.get_chip_id_dict().keys() - ): - console_warning("Unknown Chip ID detected: " + str(gpu_chip_id)) - return gpu_chip_id + return None # Custom decorator to mimic the behavior of kw_only found in Python 3.10 -def kw_only(cls): - def __init__(self, *args, **kwargs): +def kw_only(cls: T) -> T: + def __init__(self: Any, *args: Any, **kwargs: Any) -> None: # noqa: ANN401 for name, value in kwargs.items(): setattr(self, name, value) - cls.__init__ = __init__ + cls.__init__ = __init__ # type: ignore return cls -def generate_machine_specs(args, sysinfo: dict = None): +def generate_machine_specs( + args: Optional[argparse.Namespace], sysinfo: Optional[dict[str, Any]] = None +) -> MachineSpecs: if sysinfo is not None: try: sysinfo_ver = str(sysinfo["version"]) + version = get_version(config.rocprof_compute_home)["version"] + curr_ver = version[: version.find(".")] + + if sysinfo_ver != curr_ver: + console_warning( + "Detected mismatch in sysinfo versioning. " + "You need to reprofile to update data." + ) + + return MachineSpecs(**sysinfo) except KeyError: console_error( "Detected mismatch in sysinfo versioning. You need to reprofile " "to update data." ) - version = get_version(config.rocprof_compute_home)["version"] - if sysinfo_ver != version[: version.find(".")]: - console_warning( - "Detected mismatch in sysinfo versioning. You need to reprofile " - "to update data." - ) - return MachineSpecs(**sysinfo) + # read timestamp info now = datetime.now() local_now = now.astimezone() - local_tz = local_now.tzinfo - local_tzname = local_tz.tzname(local_now) - timestamp = now.strftime("%c") + " (" + local_tzname + ")" - hostname = socket.gethostname() + local_tzname = local_now.tzinfo.tzname(local_now) # type: ignore + timestamp = f"{now.strftime('%c')} ({local_tzname})" # set specs version - vData = get_version(config.rocprof_compute_home) - version = vData["version"] + version = get_version(config.rocprof_compute_home)["version"] # NB: Just taking major as specs version. # May want to make this more specific in the future - specs_version = version[ - : version.find(".") - ] # version will always follow 'major.minor.patch' format + # version will always follow 'major.minor.patch' format + specs_version = version[: version.find(".")] ########################################## ## A. Machine Specs ########################################## - cpuinfo = path("/proc/cpuinfo").read_text() - meminfo = path("/proc/meminfo").read_text() - version = path("/proc/version").read_text() - os_release = path("/etc/os-release").read_text() - cpu_model = search(r"^model name\s*: (.*?)$", cpuinfo) - sbios = ( - path("/sys/class/dmi/id/bios_vendor").read_text().strip() - + path("/sys/class/dmi/id/bios_version").read_text().strip() - ) - linux_kernel_version = search(r"version (\S*)", version) - amd_gpu_kernel_version = "" # TODO: Extract amdgpu kernel version - cpu_memory = search(r"MemTotal:\s*(\S*)", meminfo) - gpu_memory = "" # TODO: Extract gpu memory - linux_distro = search(r'PRETTY_NAME="(.*?)"', os_release) - if linux_distro is None: - linux_distro = "" - rocm_version = get_rocm_ver().strip() + machine_info = extract_machine_info() + # FIXME: use device + # Load amd-smi data + gpu_info = extract_gpu_info() + + ########################################## + ## B. SoC Specs + ########################################## + soc_info = extract_soc_info() + + # Combine all specifications + specs = MachineSpecs( + version=specs_version, + timestamp=timestamp, + rocminfo_lines=soc_info["rocminfo_lines"], + hostname=socket.gethostname(), + cpu_model=machine_info["cpu_model"], + sbios=machine_info["sbios"], + linux_kernel_version=machine_info["linux_kernel_version"], + amd_gpu_kernel_version="", + cpu_memory=machine_info["cpu_memory"], + gpu_memory="", + linux_distro=machine_info["linux_distro"], + rocm_version=get_rocm_ver().strip(), + vbios=gpu_info["vbios"], + compute_partition=gpu_info["compute_partition"], + memory_partition=gpu_info["memory_partition"], + gpu_arch=soc_info["gpu_arch"], + gpu_chip_id=soc_info["gpu_chip_id"], + ) + + # Load above SoC specs via module import + try: + soc_module = importlib.import_module( + f"rocprof_compute_soc.soc_{specs.gpu_arch}" + ) + soc_class = getattr(soc_module, f"{specs.gpu_arch}_soc") + soc_obj = soc_class(args, specs) # noqa: F841 + except ModuleNotFoundError as e: + console_error( + f"Arch {specs.gpu_arch} marked as supported," + f"but couldn't find class implementation {e}." + ) + + # Update arch specific specs + specs.gpu_model = ( + mi_gpu_specs.get_gpu_model(specs.gpu_arch, specs.gpu_chip_id) or "" + ) + specs.num_xcd = str( + mi_gpu_specs.get_num_xcds( + specs.gpu_arch, specs.gpu_model, specs.compute_partition + ) + ) + specs.total_l2_chan = totall2_banks( + specs.gpu_arch, + specs.gpu_model, + specs.l2_banks, + specs.compute_partition, + ) + specs.num_hbm_channels = str(specs.get_hbm_channels()) + + return specs + + +@demarcate +def extract_machine_info() -> dict[str, Any]: + result: dict[str, Optional[str]] = { + "cpu_model": None, + "sbios": None, + "linux_kernel_version": None, + "cpu_memory": None, + "linux_distro": None, + } + + try: + cpuinfo = path("/proc/cpuinfo").read_text() + meminfo = path("/proc/meminfo").read_text() + version = path("/proc/version").read_text() + os_release = path("/etc/os-release").read_text() + + result["cpu_model"] = search(r"^model name\s*: (.*?)$", cpuinfo) + result["sbios"] = ( + path("/sys/class/dmi/id/bios_vendor").read_text().strip() + + path("/sys/class/dmi/id/bios_version").read_text().strip() + ) + result["linux_kernel_version"] = search(r"version (\S*)", version) + result["cpu_memory"] = search(r"MemTotal:\s*(\S*)", meminfo) + result["linux_distro"] = search(r'PRETTY_NAME="(.*?)"', os_release) or "" + + except OSError as e: + console_warning(f"Could not read system files: {e}") + return result + + +@demarcate +def extract_gpu_info() -> dict[str, Any]: + result: dict[str, Optional[str]] = { + "vbios": None, + "compute_partition": None, + "memory_partition": None, + } # Load amd-smi static data for GPU 0 - static_data = json.loads( - run(["amd-smi", "static", "--gpu=0", "--json"], exit_on_error=True) - ) + static_output = run(["amd-smi", "static", "--gpu=0", "--json"], exit_on_error=True) + if static_output is None: + return result + + try: + static_data = json.loads(static_output) + except json.JSONDecodeError as e: + console_warning(f"Failed to parse amd-smi static output: {e}") + return result # Extract GPU data gpu_list = ( @@ -167,95 +270,76 @@ def generate_machine_specs(args, sysinfo: dict = None): else static_data.get("gpu_data", []) ) gpu_data = gpu_list[0] if gpu_list else {} - - vbios = gpu_data.get("vbios", {}).get("part_number") + result["vbios"] = gpu_data.get("vbios", {}).get("part_number") # Load amd-smi partition data for GPU 0 (amd-smi >= 26.0.0) - try: - partition_data = json.loads( - run(["amd-smi", "partition", "--gpu=0", "--json"], exit_on_error=False) - ) - except json.JSONDecodeError: - partition_data = {} + partition_output = run( + ["amd-smi", "partition", "--gpu=0", "--json"], exit_on_error=False + ) + partition_data = {} + + if partition_output: + try: + partition_data = json.loads(partition_output) + except json.JSONDecodeError: + partition_data = {} current_partition = partition_data.get("current_partition", [{}])[0] # Extract partition values with gpu_data fallback (amd-smi < 26.0.0) - compute_partition = ( + result["compute_partition"] = ( current_partition.get("accelerator_type") or gpu_data.get("partition", {}).get("accelerator_partition") or gpu_data.get("partition", {}).get("compute_partition") ) - memory_partition = current_partition.get("memory") or gpu_data.get( + result["memory_partition"] = current_partition.get("memory") or gpu_data.get( "partition", {} ).get("memory_partition") # Apply defaults and warnings - if not compute_partition: + if not result["compute_partition"]: console_warning("Cannot detect accelerator partition from amd-smi.") console_warning("Applying default accelerator partition: SPX") - compute_partition = "SPX" + result["compute_partition"] = "SPX" - if not memory_partition: + if not result["memory_partition"]: console_warning("Cannot detect memory partition from amd-smi.") console_debug( - "vbios is {}, compute partition is {}, memory partition is {}".format( - vbios, compute_partition, memory_partition - ) + f"vbios is {result['vbios']}, compute partition is " + f"{result['compute_partition']}, memory partition is " + f"{result['memory_partition']}" ) - ########################################## - ## B. SoC Specs - ########################################## - # read rocminfo + return result + + +@demarcate +def extract_soc_info() -> dict[str, Any]: + result: dict[str, Any] = { + "rocminfo_lines": None, + "gpu_arch": None, + "gpu_chip_id": None, + } + + # Read rocminfo rocminfo_full = run(["rocminfo"]) - _rocminfo = rocminfo_full.split("\n") - gpu_arch, idx = detect_arch(_rocminfo) - _rocminfo = _rocminfo[idx + 1 :] # update rocminfo for target section - gpu_chip_id = detect_gpu_chip_id(_rocminfo) - specs = MachineSpecs( - version=specs_version, - timestamp=timestamp, - _rocminfo=_rocminfo, - hostname=hostname, - cpu_model=cpu_model, - sbios=sbios, - linux_kernel_version=linux_kernel_version, - amd_gpu_kernel_version=amd_gpu_kernel_version, - cpu_memory=cpu_memory, - gpu_memory=gpu_memory, - linux_distro=linux_distro, - rocm_version=rocm_version, - vbios=vbios, - compute_partition=compute_partition, - memory_partition=memory_partition, - gpu_arch=gpu_arch, - gpu_chip_id=gpu_chip_id, - ) + if rocminfo_full is None: + return result - # Load above SoC specs via module import - try: - soc_module = importlib.import_module( - "rocprof_compute_soc.soc_" + specs.gpu_arch - ) - except ModuleNotFoundError as e: - console_error( - "Arch %s marked as supported, but couldn't find class implementation %s." - % (specs.gpu_arch, e) - ) - soc_class = getattr(soc_module, specs.gpu_arch + "_soc") - soc_obj = soc_class(args, specs) # noqa: F841 - # Update arch specific specs - specs.gpu_model = mi_gpu_specs.get_gpu_model(specs.gpu_arch, specs.gpu_chip_id) - specs.num_xcd = mi_gpu_specs.get_num_xcds( - specs.gpu_arch, specs.gpu_model, specs.compute_partition - ) - specs.total_l2_chan: str = total_l2_banks( - specs.gpu_arch, specs.gpu_model, specs._l2_banks, specs.compute_partition - ) - specs.num_hbm_channels: str = str(specs.get_hbm_channels()) - return specs + rocminfo_lines = rocminfo_full.split("\n") + arch_result = detect_arch(rocminfo_lines) + + if arch_result is None: + return result + + result["gpu_arch"], arch_idx = arch_result + result["rocminfo_lines"] = rocminfo_lines[ + arch_idx + 1 : + ] # update rocminfo for target section + result["gpu_chip_id"] = detect_gpu_chip_id(rocminfo_lines) + + return result @kw_only @@ -270,23 +354,25 @@ class MachineSpecs: # _are_ included in profiling/analysis, so we mark them as 'optional' # in the metadata to avoid erroring out on missing fields on # serialization - workload_name: str = field( + workload_name: Optional[str] = field( default=None, metadata={ "doc": "The name of the workload data was collected for.", "name": "Workload Name", "optional": True, + "show_in_table": True, }, ) - command: str = field( + command: Optional[str] = field( default=None, metadata={ "doc": "The command the workload was executed with.", "name": "Command", "optional": True, + "show_in_table": True, }, ) - ip_blocks: str = field( + ip_blocks: Optional[str] = field( default=None, metadata={ "doc": "The hardware blocks profiling information was collected for.", @@ -294,62 +380,78 @@ class MachineSpecs: "optional": True, }, ) - timestamp: str = field( + timestamp: Optional[str] = field( default=None, metadata={ "doc": "The time (in local system time) when data was collected", "name": "Timestamp", + "show_in_table": True, }, ) - version: str = field( + version: Optional[str] = field( default=None, metadata={ "doc": "The version of the machine specification file format.", "name": "MachineSpecs Version", "intable": False, + "show_in_table": True, }, ) - timestamp: str = field( + timestamp: Optional[str] = field( default=None, metadata={ "doc": "The time (in local system time) when data was collected", "name": "Timestamp", + "show_in_table": True, }, ) - _rocminfo: list = field(default=None) + rocminfo_lines: Optional[list] = field( + default=None, metadata={"show_in_table": False} + ) ########################################## ## A. Machine Specs ########################################## - hostname: str = field( + hostname: Optional[str] = field( default=None, - metadata={"doc": "The hostname of the machine.", "name": "Hostname"}, + metadata={ + "doc": "The hostname of the machine.", + "name": "Hostname", + "show_in_table": True, + }, ) - cpu_model: str = field( + cpu_model: Optional[str] = field( default=None, - metadata={"doc": "The model name of the CPU used.", "name": "CPU Model"}, + metadata={ + "doc": "The model name of the CPU used.", + "name": "CPU Model", + "show_in_table": True, + }, ) - sbios: str = field( + sbios: Optional[str] = field( default=None, metadata={ "doc": "The system management bios version and vendor.", "name": "SBIOS", + "show_in_table": True, }, ) - linux_distro: str = field( + linux_distro: Optional[str] = field( default=None, metadata={ "doc": "The Linux distribution installed on the machine.", "name": "Linux Distribution", + "show_in_table": True, }, ) - linux_kernel_version: str = field( + linux_kernel_version: Optional[str] = field( default=None, metadata={ "doc": "The Linux kernel version running on the machine.", "name": "Linux Kernel Version", + "show_in_table": True, }, ) - amd_gpu_kernel_version: str = field( + amd_gpu_kernel_version: Optional[str] = field( default=None, metadata={ "doc": ( @@ -357,17 +459,19 @@ class MachineSpecs: "Unimplemented." ), "name": "AMD GPU Kernel Version", + "show_in_table": True, }, ) - cpu_memory: str = field( + cpu_memory: Optional[str] = field( default=None, metadata={ "doc": "The total amount of memory available to the CPU.", "unit": "KB", "name": "CPU Memory", + "show_in_table": True, }, ) - gpu_memory: str = field( + gpu_memory: Optional[str] = field( default=None, metadata={ "doc": ( @@ -376,23 +480,26 @@ class MachineSpecs: ), "unit": "KB", "name": "GPU Memory", + "show_in_table": True, }, ) - rocm_version: str = field( + rocm_version: Optional[str] = field( default=None, metadata={ "doc": "The ROCm version used during data-collection.", "name": "ROCm Version", + "show_in_table": True, }, ) - vbios: str = field( + vbios: Optional[str] = field( default=None, metadata={ "doc": "The version of the accelerators/GPUs video bios in the system.", "name": "VBIOS", + "show_in_table": True, }, ) - compute_partition: str = field( + compute_partition: Optional[str] = field( default=None, metadata={ "doc": ( @@ -400,9 +507,10 @@ class MachineSpecs: "system (MI300 only)." ), "name": "Compute Partition", + "show_in_table": True, }, ) - memory_partition: str = field( + memory_partition: Optional[str] = field( default=None, metadata={ "doc": ( @@ -410,43 +518,48 @@ class MachineSpecs: "system (MI300 only)." ), "name": "Memory Partition", + "show_in_table": True, }, ) ########################################## ## B. SoC Specs ########################################## - gpu_series: str = field( + gpu_series: Optional[str] = field( default=None, metadata={ "doc": "The series of the accelerators/GPUs in the system.", "name": "GPU Series", + "show_in_table": True, }, ) - gpu_model: str = field( + gpu_model: Optional[str] = field( default=None, metadata={ "doc": "The product name of the accelerators/GPUs in the system.", "name": "GPU Model", + "show_in_table": True, }, ) - gpu_arch: str = field( + gpu_arch: Optional[str] = field( default=None, metadata={ "doc": "The architecture name of the accelerators/GPUs in the system,\n" "as used by (e.g.,) the AMDGPU backed of LLVM.", "name": "GPU Arch", + "show_in_table": True, }, ) - gpu_chip_id: str = field( + gpu_chip_id: Optional[str] = field( default=None, metadata={ "doc": "The Chip ID of the accelerators/GPUs in the system.", "name": "Chip ID", "optional": True, + "show_in_table": True, }, ) - gpu_l1: str = field( + gpu_l1: Optional[str] = field( default=None, metadata={ "doc": ( @@ -455,9 +568,10 @@ class MachineSpecs: ), "name": "GPU L1", "unit": "KiB", + "show_in_table": True, }, ) - gpu_l2: str = field( + gpu_l2: Optional[str] = field( default=None, metadata={ "doc": ( @@ -466,9 +580,10 @@ class MachineSpecs: ), "name": "GPU L2", "unit": "KiB", + "show_in_table": True, }, ) - cu_per_gpu: str = field( + cu_per_gpu: Optional[str] = field( default=None, metadata={ "doc": ( @@ -477,9 +592,10 @@ class MachineSpecs: "the total number of compute units in a partition." ), "name": "CU per GPU", + "show_in_table": True, }, ) - simd_per_cu: str = field( + simd_per_cu: Optional[str] = field( default=None, metadata={ "doc": ( @@ -487,9 +603,10 @@ class MachineSpecs: "accelerators/GPUs in the system." ), "name": "SIMD per CU", + "show_in_table": True, }, ) - se_per_gpu: str = field( + se_per_gpu: Optional[str] = field( default=None, metadata={ "doc": ( @@ -498,9 +615,10 @@ class MachineSpecs: "the total number of shader engines in a partition." ), "name": "SE per GPU", + "show_in_table": True, }, ) - wave_size: str = field( + wave_size: Optional[str] = field( default=None, metadata={ "doc": ( @@ -508,9 +626,10 @@ class MachineSpecs: "the system." ), "name": "Wave Size", + "show_in_table": True, }, ) - workgroup_max_size: str = field( + workgroup_max_size: Optional[str] = field( default=None, metadata={ "doc": ( @@ -518,9 +637,10 @@ class MachineSpecs: "accelerators/GPUs in the system." ), "name": "Workgroup Max Size", + "show_in_table": True, }, ) - max_waves_per_cu: str = field( + max_waves_per_cu: Optional[str] = field( default=None, metadata={ "doc": ( @@ -528,9 +648,10 @@ class MachineSpecs: "compute unit on the accelerators/GPUs in the system" ), "name": "Max Waves per CU", + "show_in_table": True, }, ) - max_sclk: str = field( + max_sclk: Optional[str] = field( default=None, metadata={ "doc": ( @@ -539,9 +660,10 @@ class MachineSpecs: ), "name": "Max SCLK", "unit": "MHz", + "show_in_table": True, }, ) - max_mclk: str = field( + max_mclk: Optional[str] = field( default=None, metadata={ "doc": ( @@ -549,9 +671,10 @@ class MachineSpecs: ), "name": "Max MCLK", "unit": "MHz", + "show_in_table": True, }, ) - cur_sclk: str = field( + cur_sclk: Optional[str] = field( default=None, metadata={ "doc": ( @@ -560,9 +683,10 @@ class MachineSpecs: ), "name": "Cur SCLK", "unit": "MHz", + "show_in_table": True, }, ) - cur_mclk: str = field( + cur_mclk: Optional[str] = field( default=None, metadata={ "doc": ( @@ -571,10 +695,16 @@ class MachineSpecs: ), "name": "Cur MCLK", "unit": "MHz", + "show_in_table": True, }, ) - _l2_banks: str = None # NB: Only used in flatten_tcc_info_across_hbm_stacks() - total_l2_chan: str = field( + l2_banks: Optional[str] = field( + default=None, + metadata={ + "show_in_table": True, + }, + ) + total_l2_chan: Optional[str] = field( default=None, metadata={ "doc": ( @@ -584,9 +714,10 @@ class MachineSpecs: "in a partition." ), "name": "Total L2 Channels", + "show_in_table": True, }, ) - lds_banks_per_cu: str = field( + lds_banks_per_cu: Optional[str] = field( default=None, metadata={ "doc": ( @@ -594,9 +725,10 @@ class MachineSpecs: "accelerators/GPUs in the system." ), "name": "LDS Banks per CU", + "show_in_table": True, }, ) - sqc_per_gpu: str = field( + sqc_per_gpu: Optional[str] = field( default=None, metadata={ "doc": ( @@ -605,18 +737,20 @@ class MachineSpecs: "this is the total number of L1I/sL1D caches in a partition." ), "name": "SQC per GPU", + "show_in_table": True, }, ) - pipes_per_gpu: str = field( + pipes_per_gpu: Optional[str] = field( default=None, metadata={ "doc": ( "The number of scheduler-pipes on the accelerators/GPUs in the system." ), "name": "Pipes per GPU", + "show_in_table": True, }, ) - num_xcd: str = field( + num_xcd: Optional[str] = field( default=None, metadata={ "doc": ( @@ -626,56 +760,57 @@ class MachineSpecs: ), "name": "Num XCDs", "unit": "XCDs", + "show_in_table": True, }, ) - num_hbm_channels: str = field( + num_hbm_channels: Optional[str] = field( default=None, metadata={ "doc": "Number of HBM channels", "name": "HBM channels", + "show_in_table": True, }, ) - def get_hbm_channels(self): + def get_hbm_channels(self) -> Optional[str]: if self.memory_partition and self.memory_partition.lower().startswith("nps"): hbmchannels = 128 if self.memory_partition.lower() == "nps4": - hbmchannels /= 4 + hbmchannels //= 4 elif self.memory_partition.lower() == "nps8": - hbmchannels /= 8 - return hbmchannels + hbmchannels //= 8 + return str(hbmchannels) else: - return int(self.total_l2_chan) + return self.total_l2_chan - def get_class_members(self): - all_populated = True + def get_class_members(self) -> pd.DataFrame: data = {} - # dataclass uses an OrderedDict for member variables, ensuring order consistency - for class_field in fields(self): - name = class_field.name - if not name.startswith("_"): - value = getattr(self, name) - if value is None: - # check if we've marked it optional - if ( - class_field.metadata - and "optional" in class_field.metadata - and class_field.metadata["optional"] - ): - pass - else: - console_warning( - f"Incomplete class definition for {self.gpu_arch}. " - f"Expecting populated {name} but detected None." - ) - all_populated = False - data[name] = value + missing_required_fields = [] + + for class_field in fields(self): + if not class_field.metadata.get("show_in_table", True): + continue + + name = class_field.name + value = getattr(self, name) + data[name] = value + + # Check for missing required fields + if value is None and not class_field.metadata.get("optional", False): + missing_required_fields.append(name) + + # Handle warnings after processing all fields + if missing_required_fields: + for field_name in missing_required_fields: + console_warning( + f"Incomplete class definition for {self.gpu_arch}. " + f"Expecting populated {field_name} but detected None." + ) + console_warning(f"Missing specs fields for {self.gpu_arch}") - if not all_populated: - console_warning("Missing specs fields for %s" % self.gpu_arch) return pd.DataFrame(data, index=[0]) - def __repr__(self): + def __repr__(self) -> str: topstr = ( "Machine Specifications: describing the state of the machine that " "ROCm Compute Profiler data was collected on.\n" @@ -683,7 +818,7 @@ class MachineSpecs: data = [] for class_field in fields(self): name = class_field.name - if not name.startswith("_"): + if class_field.metadata.get("show_in_table", True): _data = {} value = getattr(self, name) if class_field.metadata: @@ -719,78 +854,85 @@ class MachineSpecs: return topstr + get_table_string(df, transpose=False, decimal=2) -def get_rocm_ver(): - rocm_found = False - for itr in VERSION_LOC: - _path = str(path(os.getenv("ROCM_PATH", "/opt/rocm")).joinpath(".info", itr)) - if path(_path).exists(): - rocm_ver = path(_path).read_text() - rocm_found = True - break - if not rocm_found: - # check if ROCM_VER is supplied externally - ROCM_VER_USER = os.getenv("ROCM_VER") - if ROCM_VER_USER is not None: - console_log( - "profiling", - "Overriding missing ROCm version detection with ROCM_VER = %s" - % ROCM_VER_USER, - ) - rocm_ver = ROCM_VER_USER - else: - _rocm_path = os.getenv("ROCM_PATH", "/opt/rocm") - console_warning("Unable to detect a complete local ROCm installation.") - console_warning( - "The expected %s/.info/ versioning directory is missing." % _rocm_path - ) - console_error("Ensure you have valid ROCm installation.") - return rocm_ver +def get_rocm_ver() -> str: + # Check for version files in ROCm installation + rocm_base_path = path(os.getenv("ROCM_PATH", "/opt/rocm")) + + for version_file_name in VERSION_LOC: + version_file_path = rocm_base_path / ".info" / version_file_name + if version_file_path.exists(): + return version_file_path.read_text().strip() + + # Fallback to environment variable + ROCM_VER_USER = os.getenv("ROCM_VER") + if ROCM_VER_USER: + console_log( + "profiling", + "Overriding missing ROCm version detection with " + f"ROCM_VER = {ROCM_VER_USER}", + ) + return ROCM_VER_USER + + # No version found - log error and return empty string + console_warning("Unable to detect a complete local ROCm installation.") + console_warning( + f"The expected {rocm_base_path}/.info/ versioning directory is missing." + ) + console_error("Ensure you have valid ROCm installation.", exit=False) + return "" -def run(cmd, exit_on_error=False): +def run(cmd: list[str], exit_on_error: bool = False) -> str: try: - p = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + p = subprocess.run(cmd, capture_output=True) except FileNotFoundError as e: console_error( - ( - f"Unable to parse specs. Can't find ROCm asset: {e.filename}\n" - "Try passing a path to an existing workload results in 'analyze' mode." - ) + f"Unable to parse specs. Can't find ROCm asset: {e.filename}\n" + 'Try passing a path to an existing workload results in "analyze" mode.' ) if exit_on_error: if cmd[0] == "amd-smi": - if p.returncode != 2 and p.returncode != 0: + if p.returncode != 2 and p.returncode != 0: # type: ignore console_error("No GPU detected. Unable to load amd-smi") - elif p.returncode != 0: - console_error("Command [%s] failed with non-zero exit code" % cmd) - return p.stdout.decode("utf-8") + elif p.returncode != 0: # type: ignore + console_error(f"Command {cmd} failed with non-zero exit code") + return p.stdout.decode("utf-8") # type: ignore -def search(pattern, string): +def search(pattern: str, string: str) -> Optional[str]: m = re.search(pattern, string, re.MULTILINE) if m is not None: return m.group(1) return None -def total_sqc(archname, numCUs, numSEs): - cu_per_se = float(numCUs) / float(numSEs) - sq_per_se = cu_per_se / 2 +def total_sqc(archname: str, num_compute_units: str, num_shader_engines: str) -> int: + cu_per_se = float(num_compute_units) / float(num_shader_engines) + sq_per_se = cu_per_se / 2.0 if archname.lower() in ["mi50", "mi100"]: sq_per_se = cu_per_se / 3 sq_per_se = ceil(sq_per_se) - return int(sq_per_se) * int(numSEs) + return int(sq_per_se) * int(num_shader_engines) -def total_l2_banks(gpu_arch, gpu_model, L2Banks, compute_partition): +def totall2_banks( + gpu_arch: Optional[str] = None, + gpu_model: Optional[str] = None, + L2banks: Optional[str] = None, + compute_partition: Optional[str] = None, +) -> Optional[str]: xcd_count = mi_gpu_specs.get_num_xcds(gpu_arch, gpu_model, compute_partition) # TODO: MachineSpecs and OmniSoC mspec should converge... - if L2Banks is not None and xcd_count is not None: - return int(L2Banks) * int(xcd_count) + if L2banks is not None and xcd_count is not None: + return str(int(L2banks) * int(xcd_count)) return None if __name__ == "__main__": - print(generate_machine_specs()) + specs = generate_machine_specs(None, None) + if specs: + print(specs) + else: + console_error("specs", "Failed to generate machine specifications", exit=False) diff --git a/projects/rocprofiler-compute/src/utils/tty.py b/projects/rocprofiler-compute/src/utils/tty.py index 1cec9de08b..eb122c3419 100644 --- a/projects/rocprofiler-compute/src/utils/tty.py +++ b/projects/rocprofiler-compute/src/utils/tty.py @@ -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, + ) diff --git a/projects/rocprofiler-compute/src/utils/utils.py b/projects/rocprofiler-compute/src/utils/utils.py index a17b3b9bb3..f990cb6759 100644 --- a/projects/rocprofiler-compute/src/utils/utils.py +++ b/projects/rocprofiler-compute/src/utils/utils.py @@ -23,22 +23,23 @@ ############################################################################## +import argparse import glob import io import json import locale import logging import os -import pathlib import re import selectors +import shlex import shutil import subprocess import tempfile import time import uuid -from pathlib import Path as path -from typing import Optional, Union +from pathlib import Path +from typing import Any, Optional, Union, cast import pandas as pd import yaml @@ -56,21 +57,20 @@ from utils.mi_gpu_spec import mi_gpu_specs rocprof_cmd = "" rocprof_args = "" -spi_pipe_counter_regexs = [r"SPI_CS\d+_(.*)", r"SPI_CSQ_P\d+_(.*)"] -def is_tcc_channel_counter(counter): +def is_tcc_channel_counter(counter: str) -> bool: return counter.startswith("TCC") and counter.endswith("]") def add_counter_extra_config_input_yaml( - data: dict, + data: dict[str, Any], counter_name: str, description: str, expression: str, - architectures: list, - properties: list = None, -) -> dict: + architectures: list[str], + properties: Optional[list[str]] = None, +) -> dict[str, Any]: """ Add a new counter to the rocprofiler-sdk dictionary. Initialize missing parts if data is empty or incomplete. @@ -140,7 +140,7 @@ def add_counter_extra_config_input_yaml( def extract_counter_info_extra_config_input_yaml( - data: dict, counter_name: str + data: dict[str, Any], counter_name: str ) -> Optional[dict]: """ Extract the full counter dictionary from 'data' for the given counter_name. @@ -159,11 +159,11 @@ def extract_counter_info_extra_config_input_yaml( return None -def using_v1(): +def using_v1() -> bool: return "ROCPROF" in os.environ.keys() and os.environ["ROCPROF"].endswith("rocprof") -def using_v3(): +def using_v3() -> bool: return "ROCPROF" not in os.environ.keys() or ( "ROCPROF" in os.environ.keys() and ( @@ -173,77 +173,77 @@ def using_v3(): ) -def get_version(rocprof_compute_home) -> dict: +def get_version(rocprof_compute_home: Path) -> dict[str, str]: """Return ROCm Compute Profiler versioning info""" - # symantic version info - note that version file(s) can reside in + # semantic version info - note that version file(s) can reside in # two locations depending on development vs formal install - searchDirs = [rocprof_compute_home, rocprof_compute_home.parent] + search_dirs = [rocprof_compute_home, rocprof_compute_home.parent] found = False - versionDir = None + version_dir: Optional[Path] = None + VER = "unknown" + SHA = "unknown" + MODE = "unknown" - for dir in searchDirs: - version = str(path(dir).joinpath("VERSION")) + for directory in search_dirs: + version_file = directory / "VERSION" try: - with open(version, "r") as file: + with open(version_file) as file: VER = file.read().replace("\n", "") found = True - versionDir = dir + version_dir = directory break except Exception: pass if not found: - console_error("Cannot find VERSION file at {}".format(searchDirs)) + console_error(f"Cannot find VERSION file at {search_dirs}") # git version info - try: - success, output = capture_subprocess_output( - ["git", "-C", versionDir, "log", "--pretty=format:%h", "-n", "1"], - ) - if success: - SHA = output - MODE = "dev" - else: - raise Exception(output) - except Exception: + if version_dir is not None: try: - shaFile = path(versionDir).joinpath("VERSION.sha").absolute().resolve() - with open(shaFile, "r") as file: - SHA = file.read().replace("\n", "") - MODE = "release" + success, output = capture_subprocess_output( + ["git", "-C", version_dir, "log", "--pretty=format:%h", "-n", "1"], + ) + if success: + SHA = output + MODE = "dev" + else: + raise Exception(output) except Exception: - SHA = "unknown" - MODE = "unknown" + try: + sha_file = version_dir / "VERSION.sha" + with open(sha_file) as file: + SHA = file.read().replace("\n", "") + MODE = "release" + except Exception: + pass - versionData = {"version": VER, "sha": SHA, "mode": MODE} - return versionData + return {"version": VER, "sha": SHA, "mode": MODE} -def get_version_display(version, sha, mode): +def get_version_display(version: str, sha: str, mode: str) -> str: """Pretty print versioning info""" buf = io.StringIO() print("-" * 40, file=buf) - print("rocprofiler-compute version: %s (%s)" % (version, mode), file=buf) - print("Git revision: %s" % sha, file=buf) + print(f"rocprofiler-compute version: {version} ({mode})", file=buf) + print(f"Git revision: {sha}", file=buf) print("-" * 40, file=buf) return buf.getvalue() -def detect_rocprof(args): +def detect_rocprof(args: argparse.Namespace) -> str: """Detect loaded rocprof version. Resolve path and set cmd globally.""" global rocprof_cmd if os.environ.get("ROCPROF") == "rocprofiler-sdk": - if not path(args.rocprofiler_sdk_library_path).exists(): + if not Path(args.rocprofiler_sdk_library_path).exists(): console_error( "Could not find rocprofiler-sdk library at " - + args.rocprofiler_sdk_library_path + f"{args.rocprofiler_sdk_library_path}" ) rocprof_cmd = "rocprofiler-sdk" - console_debug("rocprof_cmd is {}".format(rocprof_cmd)) - console_debug( - "rocprofiler_sdk_path is {}".format(args.rocprofiler_sdk_library_path) - ) + console_debug(f"rocprof_cmd is {rocprof_cmd}") + console_debug(f"rocprofiler_sdk_path is {args.rocprofiler_sdk_library_path}") return rocprof_cmd # detect rocprof @@ -259,35 +259,36 @@ def detect_rocprof(args): if not rocprof_path: rocprof_cmd = "rocprofv3" console_warning( - "Unable to resolve path to %s binary. Reverting to default." % rocprof_cmd + f"Unable to resolve path to {rocprof_cmd} binary. Reverting to default." ) rocprof_path = shutil.which(rocprof_cmd) if not rocprof_path: console_error( - ( - "Please verify installation or set ROCPROF environment variable " - "with full path." - ) + "Please verify installation or set ROCPROF environment variable " + "with full path." ) else: # Resolve any sym links in file path - rocprof_path = str(path(rocprof_path.rstrip("\n")).resolve()) - console_debug("ROC Profiler: " + str(rocprof_path)) + rocprof_path = str(Path(rocprof_path.rstrip("\n")).resolve()) + console_debug(f"ROC Profiler: {rocprof_path}") - console_debug("rocprof_cmd is {}".format(str(rocprof_cmd))) - # TODO: Do we still need to return this? It's not being used in the function call + console_debug(f"rocprof_cmd is {rocprof_cmd}") return rocprof_cmd -def store_app_cmd(args): +# TODO: v1/v2 function, to be removed +def store_app_cmd(args: argparse.Namespace) -> None: global rocprof_args rocprof_args = args +@demarcate def capture_subprocess_output( - subprocess_args, new_env=None, profileMode=False, enable_logging=True -): - console_debug("subprocess", "Running: " + " ".join(subprocess_args)) + subprocess_args: list[str], + new_env: Optional[dict[str, str]] = None, + profileMode: bool = False, + enable_logging: bool = True, +) -> tuple[bool, str]: # Start subprocess # bufsize = 1 means output is line buffered # universal_newlines = True is required for line buffering @@ -313,7 +314,7 @@ def capture_subprocess_output( # Create callback function for process output buf = io.StringIO() - def handle_output(stream, mask): + def handle_output(stream: io.TextIOWrapper, _mask) -> None: try: # Because the process' output is line buffered, there's only ever one # line to read when this function is called @@ -330,7 +331,8 @@ def capture_subprocess_output( # Register callback for an "available for read" event from subprocess' stdout stream selector = selectors.DefaultSelector() - selector.register(process.stdout, selectors.EVENT_READ, handle_output) + if process.stdout is not None: + selector.register(process.stdout, selectors.EVENT_READ, handle_output) # Loop until subprocess is terminated while process.poll() is None: @@ -350,14 +352,13 @@ def capture_subprocess_output( output = buf.getvalue() buf.close() - return (success, output) + return success, output -# Create a dictionary that maps agent ID to agent objects -def get_agent_dict(data): +def get_agent_dict(data: dict[str, Any]) -> dict[Any, Any]: + """Create a dictionary that maps agent ID to agent objects.""" agents = data["rocprofiler-sdk-tool"][0]["agents"] - - agent_map = {} + agent_map: dict[Any, Any] = {} for agent in agents: agent_id = agent["id"]["handle"] @@ -366,12 +367,12 @@ def get_agent_dict(data): return agent_map -# Returns a dictionary that maps agent ID to GPU ID -# starting at 0. -def get_gpuid_dict(data): +def get_gpuid_dict(data: dict[str, Any]) -> dict[Any, int]: + """ + Returns a dictionary that maps agent ID to GPU ID starting at 0. + """ agents = data["rocprofiler-sdk-tool"][0]["agents"] - - agent_list = [] + agent_list: list[tuple[Any, int]] = [] # Get agent ID and node_id for GPU agents only for agent in agents: @@ -384,46 +385,43 @@ def get_gpuid_dict(data): agent_list.sort(key=lambda x: x[1]) # Map agent ID to node id - map = {} + gpu_map: dict[Any, int] = {} gpu_id = 0 - for agent in agent_list: - map[agent[0]] = gpu_id - gpu_id = gpu_id + 1 + for agent_id, _ in agent_list: + gpu_map[agent_id] = gpu_id + gpu_id += 1 - return map + return gpu_map -# Create a dictionary that maps counter ID to counter objects -def v3_json_get_counters(data): +def v3_json_get_counters(data: dict[str, Any]) -> dict[tuple[Any, Any], Any]: + """Create a dictionary that maps (agent_id, counter_id) to counter objects.""" counters = data["rocprofiler-sdk-tool"][0]["counters"] - - counter_map = {} + counter_map: dict[tuple[Any, Any], Any] = {} for counter in counters: counter_id = counter["id"]["handle"] agent_id = counter["agent_id"]["handle"] - counter_map[(agent_id, counter_id)] = counter return counter_map -def v3_json_get_dispatches(data): +def v3_json_get_dispatches(data: dict[str, Any]) -> dict[Any, Any]: + """Create a dictionary that maps correlation_id to dispatch records.""" records = data["rocprofiler-sdk-tool"][0]["buffer_records"] - - records_map = {} + records_map: dict[Any, Any] = {} for rec in records["kernel_dispatch"]: id = rec["correlation_id"]["internal"] - records_map[id] = rec return records_map -def v3_json_to_csv(json_file_path, csv_file_path): - f = open(json_file_path, "rt") - data = json.load(f) +def v3_json_to_csv(json_file_path: str, csv_file_path: str) -> None: + with open(json_file_path) as f: + data = json.load(f) dispatch_records = v3_json_get_dispatches(data) dispatches = data["rocprofiler-sdk-tool"][0]["callback_records"][ @@ -432,48 +430,41 @@ def v3_json_to_csv(json_file_path, csv_file_path): kernel_symbols = data["rocprofiler-sdk-tool"][0]["kernel_symbols"] agents = get_agent_dict(data) pid = data["rocprofiler-sdk-tool"][0]["metadata"]["pid"] - gpuid_map = get_gpuid_dict(data) - counter_info = v3_json_get_counters(data) # CSV headers. If there are no dispatches we still end up with a valid CSV file. - csv_data = dict.fromkeys([ - "Dispatch_ID", - "GPU_ID", - "Queue_ID", - "PID", - "TID", - "Grid_Size", - "Workgroup_Size", - "LDS_Per_Workgroup", - "Scratch_Per_Workitem", - "Arch_VGPR", - "Accum_VGPR", - "SGPR", - "Wave_Size", - "Kernel_Name", - "Start_Timestamp", - "End_Timestamp", - "Correlation_ID", - ]) - - for key in csv_data: - csv_data[key] = [] + csv_data: dict[str, list[Any]] = { + key: [] + for key in [ + "Dispatch_ID", + "GPU_ID", + "Queue_ID", + "PID", + "TID", + "Grid_Size", + "Workgroup_Size", + "LDS_Per_Workgroup", + "Scratch_Per_Workitem", + "Arch_VGPR", + "Accum_VGPR", + "SGPR", + "Wave_Size", + "Kernel_Name", + "Start_Timestamp", + "End_Timestamp", + "Correlation_ID", + ] + } for d in dispatches: dispatch_info = d["dispatch_data"]["dispatch_info"] - agent_id = dispatch_info["agent_id"]["handle"] - kernel_id = dispatch_info["kernel_id"] - row = {} - + row: dict[str, Any] = {} row["Dispatch_ID"] = dispatch_info["dispatch_id"] - row["GPU_ID"] = gpuid_map[agent_id] - row["Queue_ID"] = dispatch_info["queue_id"]["handle"] row["PID"] = pid row["TID"] = d["thread_id"] @@ -485,16 +476,11 @@ def v3_json_to_csv(json_file_path, csv_file_path): row["Workgroup_Size"] = wg["x"] * wg["y"] * wg["z"] row["LDS_Per_Workgroup"] = d["lds_block_size_v"] - row["Scratch_Per_Workitem"] = kernel_symbols[kernel_id]["private_segment_size"] row["Arch_VGPR"] = d["arch_vgpr_count"] - - # TODO: Accum VGPR is missing from rocprofv3 output. - row["Accum_VGPR"] = 0 - + row["Accum_VGPR"] = 0 # TODO: Accum VGPR is missing from rocprofv3 output. row["SGPR"] = d["sgpr_count"] row["Wave_Size"] = agents[agent_id]["wave_front_size"] - row["Kernel_Name"] = kernel_symbols[kernel_id]["formatted_kernel_name"] id = d["dispatch_data"]["correlation_id"]["internal"] @@ -504,26 +490,17 @@ def v3_json_to_csv(json_file_path, csv_file_path): row["End_Timestamp"] = rec["end_timestamp"] row["Correlation_ID"] = d["dispatch_data"]["correlation_id"]["external"] - # Get counters - ctrs = {} + # Get counters, summing repeated names. + ctrs: dict[str, Any] = {} - records = d["records"] - for r in records: + for r in d["records"]: ctr_id = r["counter_id"]["handle"] value = r["value"] - name = counter_info[(agent_id, ctr_id)]["name"] - if name.endswith("_ACCUM"): - # It's an accumulate counter. Omniperf expects the accumulated value - # to be in SQ_ACCUM_PREV_HIRES. + # Omniperf expects accumulated value in SQ_ACCUM_PREV_HIRES. name = "SQ_ACCUM_PREV_HIRES" - - # Some counters appear multiple times and need to be summed - if name in ctrs: - ctrs[name] += value - else: - ctrs[name] = value + ctrs[name] = ctrs.get(name, 0) + value # Append counter values for ctr, value in ctrs.items(): @@ -533,15 +510,15 @@ def v3_json_to_csv(json_file_path, csv_file_path): for col_name, value in row.items(): if col_name not in csv_data: csv_data[col_name] = [] - csv_data[col_name].append(value) df = pd.DataFrame(csv_data) - df.to_csv(csv_file_path, index=False) -def v3_counter_csv_to_v2_csv(counter_file, agent_info_filepath, converted_csv_file): +def v3_counter_csv_to_v2_csv( + counter_file: str, agent_info_filepath: str, converted_csv_file: str +) -> None: """ Convert the counter file of csv output for a certain csv from rocprofv3 format to rocprfv2 format. @@ -581,10 +558,9 @@ def v3_counter_csv_to_v2_csv(counter_file, agent_info_filepath, converted_csv_fi # NB: Agent_Id is int in older rocporfv3, now switched to string with prefix # "Agent ". We need to make sure handle both cases. console_debug( - "The type of Agent ID from counter csv file is {}".format( - result["Agent_Id"].dtype - ) + f"The type of Agent ID from counter csv file is {result['Agent_Id'].dtype}" ) + if result["Agent_Id"].dtype == "object": # Apply the function to the 'Agent_Id' column and store it as int64 try: @@ -595,10 +571,8 @@ def v3_counter_csv_to_v2_csv(counter_file, agent_info_filepath, converted_csv_fi ) except Exception as e: console_error( - ( - 'Parsing rocprofv3 csv output: Error of getting "Agent_Id", ' - 'the error message "{}"' - ).format(e) + "v3_counter_csv_to_v2_csv", + f'Error getting "Agent_Id": {e}', ) # Grab the Wave_Front_Size column from agent info @@ -609,22 +583,16 @@ def v3_counter_csv_to_v2_csv(counter_file, agent_info_filepath, converted_csv_fi how="left", ) - # Map agent ID (Node_Id) to GPU_ID - gpu_id_map = {} - gpu_id = 0 - for idx, row in pd_agent_info.iterrows(): - if row["Agent_Type"] == "GPU": - agent_id = row["Node_Id"] - gpu_id_map[agent_id] = gpu_id - gpu_id = gpu_id + 1 + # Create GPU ID mapping from agent info + gpu_agents = pd_agent_info[pd_agent_info["Agent_Type"] == "GPU"].copy() + gpu_agents = gpu_agents.reset_index(drop=True) + gpu_id_map = dict(zip(gpu_agents["Node_Id"], gpu_agents.index)) - # Update Agent_Id for each record to match GPU ID - for idx, row in result["Agent_Id"].items(): - agent_id = result.at[idx, "Agent_Id"] - result.at[idx, "Agent_Id"] = gpu_id_map[agent_id] + # Map Agent_Id to GPU_ID using vectorized operation + result["Agent_Id"] = result["Agent_Id"].map(gpu_id_map) - # Drop the 'Node_Id' column if you don't need it in the final DataFrame - result.drop(columns="Node_Id", inplace=True) + # Drop the temporary Node_Id column + result = result.drop(columns="Node_Id") name_mapping = { "Dispatch_Id": "Dispatch_ID", @@ -673,28 +641,30 @@ def v3_counter_csv_to_v2_csv(counter_file, agent_info_filepath, converted_csv_fi index = index + remaining_column_names result = result.reindex(columns=index) - # Rename the accumulate counter to SQ_ACCUM_PREV_HIRES. - for col in result.columns: - if col.endswith("_ACCUM"): - result.rename(columns={col: "SQ_ACCUM_PREV_HIRES"}, inplace=True) + # Rename accumulate counters to standard format + accum_columns = { + col: "SQ_ACCUM_PREV_HIRES" for col in result.columns if col.endswith("_ACCUM") + } + if accum_columns: + result = result.rename(columns=accum_columns) result.to_csv(converted_csv_file, index=False) -def parse_text(text_file): +def parse_text(text_file: str) -> list[str]: """ Parse the text file to get the pmc counters. """ - def process_line(line): + def process_line(line: str) -> list[str]: if "pmc:" not in line: - return "" + return [] line = line.strip() pos = line.find("#") if pos >= 0: line = line[0:pos] - def _dedup(_line, _sep): + def _dedup(_line: str, _sep: list[str]) -> str: for itr in _sep: _line = " ".join(_line.split(itr)) return _line.strip() @@ -702,7 +672,7 @@ def parse_text(text_file): # remove tabs and duplicate spaces return _dedup(line.replace("pmc:", ""), ["\n", "\t", " "]).split(" ") - with open(text_file, "r") as file: + with open(text_file) as file: return [ counter for litr in [process_line(itr) for itr in file.readlines()] @@ -711,26 +681,26 @@ def parse_text(text_file): def run_prof( - fname, - profiler_options, - workload_dir, - mspec, - loglevel, - format_rocprof_output, - retain_rocpd_output=False, -): - fbase = path(fname).stem - - console_debug("pmc file: %s" % path(fname).name) + fname: str, + profiler_options: Union[list[str], dict[str, Union[str, list[str]]]], + workload_dir: str, + mspec: Any, # noqa: ANN401 + loglevel: int, + format_rocprof_output: str, + retain_rocpd_output: bool = False, +) -> None: + fpath = Path(fname) + fbase = fpath.stem + console_debug(f"pmc file: {fpath.name}") # standard rocprof options if rocprof_cmd == "rocprofiler-sdk": - options = profiler_options + options = cast(dict[str, Union[str, list[str]]], profiler_options) options["ROCPROF_COUNTER_COLLECTION"] = "1" - options["ROCPROF_COUNTERS"] = "pmc: " + " ".join(parse_text(fname)) + options["ROCPROF_COUNTERS"] = f"pmc: {' '.join(parse_text(fname))}" else: default_options = ["-i", fname] - options = default_options + profiler_options + options = default_options + cast(list[str], profiler_options) if using_v3(): if rocprof_cmd == "rocprofiler-sdk": @@ -747,18 +717,17 @@ def run_prof( / "rocprof_compute_soc" / "profile_configs" / "counter_defs.yaml", - "r", ) as file: counter_defs = yaml.safe_load(file) # Extra counter definitions - if path(fname).with_suffix(".yaml").exists(): - with open(path(fname).with_suffix(".yaml"), "r") as file: + if fpath.with_suffix(".yaml").exists(): + with open(fpath.with_suffix(".yaml")) as file: counter_defs["rocprofiler-sdk"]["counters"].extend( yaml.safe_load(file)["rocprofiler-sdk"]["counters"] ) # Write counter definitions to a temporary file tmpfile_path = ( - path(tempfile.mkdtemp(prefix="rocprof_counter_defs_", dir="/tmp")) + Path(tempfile.mkdtemp(prefix="rocprof_counter_defs_", dir="/tmp")) / "counter_defs.yaml" ) with open(tmpfile_path, "w") as tmpfile: @@ -766,10 +735,8 @@ def run_prof( # Set counter definitions new_env["ROCPROFILER_METRICS_PATH"] = str(tmpfile_path.parent) console_debug( - ( - "Adding env var for counter definitions: " - f"ROCPROFILER_METRICS_PATH={new_env['ROCPROFILER_METRICS_PATH']}" - ) + "Adding env var for counter definitions: " + f"ROCPROFILER_METRICS_PATH={new_env['ROCPROFILER_METRICS_PATH']}" ) # set required env var for >= mi300 @@ -783,22 +750,21 @@ def run_prof( ): new_env["ROCPROFILER_INDIVIDUAL_XCC_MODE"] = "1" - is_timestamps = False - if path(fname).name == "timestamps.txt": - is_timestamps = True + is_timestamps = Path(fname).name == "timestamps.txt" time_1 = time.time() if rocprof_cmd == "rocprofiler-sdk": app_cmd = options.pop("APP_CMD") for key, value in options.items(): new_env[key] = value - console_debug("rocprof sdk env vars: {}".format(new_env)) - console_debug("rocprof sdk user provided command: {}".format(app_cmd)) + console_debug(f"rocprof sdk env vars: {new_env}") + console_debug(f"rocprof sdk user provided command: {app_cmd}") success, output = capture_subprocess_output( app_cmd, new_env=new_env, profileMode=True ) else: - console_debug("rocprof command: {}".format([rocprof_cmd] + options)) + # print in readable format using shlex + console_debug(f"rocprof command: {shlex.join([rocprof_cmd] + options)}") # profile the app success, output = capture_subprocess_output( [rocprof_cmd] + options, new_env=new_env, profileMode=True @@ -806,9 +772,8 @@ def run_prof( time_2 = time.time() console_debug( - "Finishing subprocess of fname {}, the time it takes was {} m {} sec ".format( - fname, int((time_2 - time_1) / 60), str((time_2 - time_1) % 60) - ) + f"Finishing subprocess of fname {fname}, the time taken is " + f"{int((time_2 - time_1) / 60)} m {str((time_2 - time_1) % 60)} sec " ) # Delete counter definition temporary directory @@ -818,39 +783,37 @@ def run_prof( if not success: if loglevel > logging.INFO: for line in output.splitlines(): - console_error(output, exit=False) + console_error(line, exit=False) console_error("Profiling execution failed.") - results_files = [] + results_files: list[str] = [] if format_rocprof_output == "rocpd": if rocprof_cmd == "rocprofiler-sdk" or rocprof_cmd.endswith("v3"): # Write results_fbase.csv rocpd_data.convert_db_to_csv( - glob.glob(workload_dir + "/out/pmc_1/*/*.db")[0], - workload_dir + f"/results_{fbase}.csv", + glob.glob(f"{workload_dir}/out/pmc_1/*/*.db")[0], + f"{workload_dir}/results_{fbase}.csv", ) if retain_rocpd_output: shutil.copyfile( - glob.glob(workload_dir + "/out/pmc_1/*/*.db")[0], - workload_dir + "/" + fbase + ".db", + glob.glob(f"{workload_dir}/out/pmc_1/*/*.db")[0], + "f{workload_dir}/{fbase}.db", ) console_warning( f"Retaining large raw rocpd database: {workload_dir}/{fbase}.db" ) # Remove temp directory - shutil.rmtree(workload_dir + "/" + "out") + shutil.rmtree(f"{workload_dir}/out") return else: console_error( - ( - "rocpd output format is only supported with " - "rocprofiler-sdk or rocprofv3." - ) + "rocpd output format is only supported with " + "rocprofiler-sdk or rocprofv3." ) elif rocprof_cmd.endswith("v2"): # rocprofv2 has separate csv files for each process - results_files = glob.glob(workload_dir + "/out/pmc_1/results_*.csv") + results_files = glob.glob(f"{workload_dir}/out/pmc_1/results_*.csv") if len(results_files) == 0: return @@ -864,7 +827,7 @@ def run_prof( combined_results["Dispatch_ID"] = range(0, len(combined_results)) combined_results.to_csv( - workload_dir + "/out/pmc_1/results_" + fbase + ".csv", index=False + f"{workload_dir}/out/pmc_1/results_{fbase}.csv", index=False ) elif rocprof_cmd.endswith("v3") or rocprof_cmd == "rocprofiler-sdk": # rocprofv3 requires additional processing for each process @@ -885,44 +848,41 @@ def run_prof( elif "--hip-trace" in options: process_hip_trace_output(workload_dir, fbase) - # Combine results into single CSV file - if results_files: - combined_results = pd.concat( - [pd.read_csv(f) for f in results_files], ignore_index=True - ) - else: + if not results_files: console_warning( - ( - f"Cannot write results for {fbase}.csv due to no counter " - "csv files generated." - ) + f"Cannot write results for {fbase}.csv due to no counter " + "csv files generated." ) - return + + # Combine results into single CSV file + combined_results = pd.concat( + [pd.read_csv(f) for f in results_files], ignore_index=True + ) # Overwrite column to ensure unique IDs. combined_results["Dispatch_ID"] = range(0, len(combined_results)) combined_results.to_csv( - workload_dir + "/out/pmc_1/results_" + fbase + ".csv", index=False + f"{workload_dir}/out/pmc_1/results_{fbase}.csv", index=False ) if not using_v3() and not using_v1(): # flatten tcc for applicable mi300 input - f = path(workload_dir + "/out/pmc_1/results_" + fbase + ".csv") + f = f"{workload_dir}/out/pmc_1/results_{fbase}.csv" xcds = mi_gpu_specs.get_num_xcds( mspec.gpu_arch, mspec.gpu_model, mspec.compute_partition ) - df = flatten_tcc_info_across_xcds(f, xcds, int(mspec._l2_banks)) + df = flatten_tcc_info_across_xcds(f, xcds, int(mspec.l2_banks)) df.to_csv(f, index=False) - if path(workload_dir + "/out").exists(): + if Path(f"{workload_dir}/out").exists(): # copy and remove out directory if needed shutil.copyfile( - workload_dir + "/out/pmc_1/results_" + fbase + ".csv", - workload_dir + "/" + fbase + ".csv", + f"{workload_dir}/out/pmc_1/results_{fbase}.csv", + f"{workload_dir}/{fbase}.csv", ) # Remove temp directory - shutil.rmtree(workload_dir + "/" + "out") + shutil.rmtree(f"{workload_dir}/out") # Standardize rocprof headers via overwrite # {: } @@ -947,14 +907,19 @@ def run_prof( "SCR": "Scratch_Per_Workitem", "ACCUM_VGPR": "Accum_VGPR", } - df = pd.read_csv(workload_dir + "/" + fbase + ".csv") + csv_path = Path(workload_dir) / f"{fbase}.csv" + df = pd.read_csv(csv_path) df.rename(columns=output_headers, inplace=True) - df.to_csv(workload_dir + "/" + fbase + ".csv", index=False) + df.to_csv(csv_path, index=False) def pc_sampling_prof( - method, interval, workload_dir, appcmd, rocprofiler_sdk_library_path -): + method: str, + interval: int, + workload_dir: str, + appcmd: list[str], + rocprofiler_sdk_library_path: str, +) -> None: """ Run rocprof with pc sampling. Current support v3 only. """ @@ -964,11 +929,9 @@ def pc_sampling_prof( unit = "time" if method == "host_trap" else "cycles" if rocprof_cmd == "rocprofiler-sdk": - rocm_libdir = str(pathlib.Path(rocprofiler_sdk_library_path).parent) + rocm_libdir = str(Path(rocprofiler_sdk_library_path).parent) rocprofiler_sdk_tool_path = str( - pathlib.Path(rocm_libdir).joinpath( - "rocprofiler-sdk/librocprofiler-sdk-tool.so" - ) + Path(rocm_libdir) / "rocprofiler-sdk/librocprofiler-sdk-tool.so" ) ld_preload = [ rocprofiler_sdk_tool_path, @@ -990,10 +953,8 @@ def pc_sampling_prof( new_env = os.environ.copy() for key, value in options.items(): new_env[key] = value - console_debug("pc sampling rocprof sdk env vars: {}".format(new_env)) - console_debug( - "pc sampling rocprof sdk user provided command: {}".format(appcmd) - ) + console_debug(f"pc sampling rocprof sdk env vars: {new_env}") + console_debug(f"pc sampling rocprof sdk user provided command: {appcmd}") success, output = capture_subprocess_output( appcmd, new_env=new_env, profileMode=True ) @@ -1012,7 +973,7 @@ def pc_sampling_prof( "-d", workload_dir, "-o", - "ps_file", # todo: sync up with the name from source in 2100_.yaml + "ps_file", # TODO: sync up with the name from source in 2100_.yaml "--", ] options.extend(appcmd) @@ -1025,31 +986,33 @@ def pc_sampling_prof( console_error("PC sampling failed.") -def process_rocprofv3_output(rocprof_output, workload_dir, is_timestamps): +def process_rocprofv3_output( + rocprof_output: str, workload_dir: str, is_timestamps: bool +) -> list[str]: """ rocprofv3 specific output processing. takes care of json or csv formats, for csv format, additional processing is performed. """ - results_files_csv = {} + results_files_csv: list[str] = [] if rocprof_output == "json": - results_files_json = glob.glob(workload_dir + "/out/pmc_1/*/*.json") + results_files_json = glob.glob(f"{workload_dir}/out/pmc_1/*/*.json") for json_file in results_files_json: - csv_file = pathlib.Path(json_file).with_suffix(".csv") + csv_file = str(Path(json_file).with_suffix(".csv")) v3_json_to_csv(json_file, csv_file) - results_files_csv = glob.glob(workload_dir + "/out/pmc_1/*/*.csv") + results_files_csv = glob.glob(f"{workload_dir}/out/pmc_1/*/*.csv") elif rocprof_output == "csv": counter_info_csvs = glob.glob( - workload_dir + "/out/pmc_1/*/*_counter_collection.csv" + f"{workload_dir}/out/pmc_1/*/*_counter_collection.csv" ) - existing_counter_files_csv = [d for d in counter_info_csvs if path(d).is_file()] + existing_counter_files_csv = [f for f in counter_info_csvs if Path(f).is_file()] if existing_counter_files_csv: for counter_file in existing_counter_files_csv: - counter_path = path(counter_file) + counter_path = Path(counter_file) current_dir = counter_path.parent agent_info_filepath = current_dir / counter_path.name.replace( @@ -1058,7 +1021,7 @@ def process_rocprofv3_output(rocprof_output, workload_dir, is_timestamps): if not agent_info_filepath.is_file(): raise ValueError( - '{} has no coresponding "agent info" file'.format(counter_file) + f'{counter_file} has no corresponding "agent info" file' ) converted_csv_file = current_dir / counter_path.name.replace( @@ -1075,12 +1038,12 @@ def process_rocprofv3_output(rocprof_output, workload_dir, is_timestamps): ) return [] - results_files_csv = glob.glob(workload_dir + "/out/pmc_1/*/*_converted.csv") + results_files_csv = glob.glob(f"{workload_dir}/out/pmc_1/*/*_converted.csv") elif is_timestamps: # when the input is timestamps, we know counter csv file # is not generated and will instead parse kernel trace file results_files_csv = glob.glob( - workload_dir + "/out/pmc_1/*/*_kernel_trace.csv" + f"{workload_dir}/out/pmc_1/*/*_kernel_trace.csv" ) else: # when the input is not for timestamps, and counter csv file @@ -1094,12 +1057,12 @@ def process_rocprofv3_output(rocprof_output, workload_dir, is_timestamps): @demarcate -def process_kokkos_trace_output(workload_dir, fbase): +def process_kokkos_trace_output(workload_dir: str, fbase: str) -> None: # marker api trace csv files are generated for each process marker_api_trace_csvs = glob.glob( - workload_dir + "/out/pmc_1/*/*_marker_api_trace.csv" + f"{workload_dir}/out/pmc_1/*/*_marker_api_trace.csv" ) - existing_marker_files_csv = [d for d in marker_api_trace_csvs if path(d).is_file()] + existing_marker_files_csv = [f for f in marker_api_trace_csvs if Path(f).is_file()] # concate and output marker api trace info combined_results = pd.concat( @@ -1107,51 +1070,51 @@ def process_kokkos_trace_output(workload_dir, fbase): ) combined_results.to_csv( - workload_dir + "/out/pmc_1/results_" + fbase + "_marker_api_trace.csv", + f"{workload_dir}/out/pmc_1/results_{fbase}_marker_api_trace.csv", index=False, ) - if path(workload_dir + "/out").exists(): + if Path(f"{workload_dir}/out").exists(): shutil.copyfile( - workload_dir + "/out/pmc_1/results_" + fbase + "_marker_api_trace.csv", - workload_dir + "/" + fbase + "_marker_api_trace.csv", + f"{workload_dir}/out/pmc_1/results_{fbase}_marker_api_trace.csv", + f"{workload_dir}/{fbase}_marker_api_trace.csv", ) @demarcate -def process_hip_trace_output(workload_dir, fbase): - # marker api trace csv files are generated for each process - hip_api_trace_csvs = glob.glob(workload_dir + "/out/pmc_1/*/*_hip_api_trace.csv") - existing_hip_files_csv = [d for d in hip_api_trace_csvs if path(d).is_file()] +def process_hip_trace_output(workload_dir: str, fbase: str) -> None: + # hip api trace csv files are generated for each process + hip_api_trace_csvs = glob.glob(f"{workload_dir}/out/pmc_1/*/*_hip_api_trace.csv") + existing_hip_files_csv = [f for f in hip_api_trace_csvs if Path(f).is_file()] - # concate and output marker api trace info + # concate and output hip api trace info combined_results = pd.concat( [pd.read_csv(f) for f in existing_hip_files_csv], ignore_index=True ) combined_results.to_csv( - workload_dir + "/out/pmc_1/results_" + fbase + "_hip_api_trace.csv", + f"{workload_dir}/out/pmc_1/results_{fbase}_hip_api_trace.csv", index=False, ) - if path(workload_dir + "/out").exists(): + if Path(f"{workload_dir}/out").exists(): shutil.copyfile( - workload_dir + "/out/pmc_1/results_" + fbase + "_hip_api_trace.csv", - workload_dir + "/" + fbase + "_hip_api_trace.csv", + f"{workload_dir}/out/pmc_1/results_{fbase}_hip_api_trace.csv", + f"{workload_dir}/{fbase}_hip_api_trace.csv", ) -def replace_timestamps(workload_dir): - if not path(workload_dir, "timestamps.csv").is_file(): +def replace_timestamps(workload_dir: str) -> None: + ts_path = Path(workload_dir) / "timestamps.csv" + if not ts_path.is_file(): return - df_stamps = pd.read_csv(workload_dir + "/timestamps.csv") + df_stamps = pd.read_csv(ts_path) if "Start_Timestamp" in df_stamps.columns and "End_Timestamp" in df_stamps.columns: # Update timestamps for all *.csv output files - for fname in glob.glob(workload_dir + "/" + "*.csv"): - if path(fname).name != "sysinfo.csv": + for fname in glob.glob(f"{workload_dir}/*.csv"): + if Path(fname).name != "sysinfo.csv": df_pmc_perf = pd.read_csv(fname) - df_pmc_perf["Start_Timestamp"] = df_stamps["Start_Timestamp"] df_pmc_perf["End_Timestamp"] = df_stamps["End_Timestamp"] df_pmc_perf.to_csv(fname, index=False) @@ -1161,8 +1124,15 @@ def replace_timestamps(workload_dir): ) -def gen_sysinfo(workload_name, workload_dir, app_cmd, skip_roof, mspec, soc): - console_debug("[gen_sysinfo]") +@demarcate +def gen_sysinfo( + workload_name: str, + workload_dir: str, + app_cmd: str, + skip_roof: bool, + mspec: Any, # noqa: ANN401 + soc: Any, # noqa: ANN401 +) -> None: df = mspec.get_class_members() # Append workload information to machine specs @@ -1174,51 +1144,55 @@ def gen_sysinfo(workload_name, workload_dir, app_cmd, skip_roof, mspec, soc): blocks.append("roofline") df["ip_blocks"] = "|".join(blocks) - # Save csv df.to_csv(workload_dir + "/" + "sysinfo.csv", index=False) -def detect_roofline(mspec): +def detect_roofline(mspec: Any) -> dict[str, str]: # noqa: ANN401 from utils import specs rocm_ver = int(mspec.rocm_version[:1]) - target_binary = {"rocm_ver": rocm_ver, "distro": "override", "path": None} + target_binary: dict[str, Any] = { + "rocm_ver": rocm_ver, + "distro": "override", + "path": None, + } - os_release = path("/etc/os-release").read_text() + os_release = Path("/etc/os-release").read_text() ubuntu_distro = specs.search(r'VERSION_ID="(.*?)"', os_release) rhel_distro = specs.search(r'PLATFORM_ID="(.*?)"', os_release) sles_distro = specs.search(r'VERSION_ID="(.*?)"', os_release) if "ROOFLINE_BIN" in os.environ.keys(): rooflineBinary = os.environ["ROOFLINE_BIN"] - if path(rooflineBinary).exists(): - msg = ( - "Detected user-supplied binary --> ROOFLINE_BIN = %s\n" % rooflineBinary + if Path(rooflineBinary).exists(): + console_warning( + "roofline", + f"Detected user-supplied binary --> ROOFLINE_BIN = {rooflineBinary}\n", ) - console_warning("roofline", msg) # distro stays marked as override and path value is substituted in target_binary["path"] = rooflineBinary return target_binary else: - msg = ( - "user-supplied path to binary not accessible --> ROOFLINE_BIN = %s\n" - % rooflineBinary + console_error( + "roofline", + "user-supplied path to binary not accessible --> " + f"ROOFLINE_BIN = {rooflineBinary}\n", ) - console_error("roofline", msg) # Must be a valid RHEL machine - elif ( - rhel_distro == "platform:el8" - or rhel_distro == "platform:al8" - or rhel_distro == "platform:el9" - or rhel_distro == "platform:el10" - ): + elif rhel_distro in { + "platform:el8", + "platform:al8", + "platform:el9", + "platform:el10", + }: distro = "platform:el8" # Must be a valid SLES machine elif ( - (isinstance(sles_distro, str) and len(sles_distro) >= 3) + isinstance(sles_distro, str) + and len(sles_distro) >= 3 and sles_distro[:2] == "15" # confirm string and len and int(sles_distro[3]) >= 6 # SLES15 and SP >= 6 ): @@ -1226,9 +1200,8 @@ def detect_roofline(mspec): distro = "15.6" # Must be a valid Ubuntu machine - elif ubuntu_distro == "22.04" or ubuntu_distro == "24.04": + elif ubuntu_distro in {"22.04", "24.04"}: distro = "22.04" - else: console_error( "roofline", "Cannot find a valid binary for your operating system" @@ -1239,7 +1212,7 @@ def detect_roofline(mspec): return target_binary -def mibench(args, mspec): +def mibench(args: argparse.Namespace, mspec: Any) -> None: # noqa: ANN401 """Run roofline microbenchmark to generate peek BW and FLOP measurements.""" console_log("roofline", "No roofline data found. Generating...") @@ -1249,7 +1222,7 @@ def mibench(args, mspec): "22.04": "ubuntu22_04", } - binary_paths = [] + binary_paths: list[str] = [] target_binary = detect_roofline(mspec) if target_binary["distro"] == "override": @@ -1258,56 +1231,50 @@ def mibench(args, mspec): # check two potential locations for roofline binaries due to differences in # development usage vs formal install potential_paths = [ - "%s/utils/rooflines/roofline" % config.rocprof_compute_home, - "%s/bin/roofline" % config.rocprof_compute_home.parent.parent, + config.rocprof_compute_home / "utils" / "rooflines" / "roofline", + config.rocprof_compute_home.parent.parent / "bin" / "roofline", ] - for dir in potential_paths: + for directory in potential_paths: path_to_binary = ( - dir - + "-" - + distro_map[target_binary["distro"]] - + "-rocm" - + str(target_binary["rocm_ver"]) + f"{directory}-{distro_map[target_binary['distro']]}" + f"-rocm{target_binary['rocm_ver']}" ) binary_paths.append(path_to_binary) # Distro is valid but cant find rocm ver found = False for binary_path in binary_paths: - if pathlib.Path(binary_path).exists(): + if Path(binary_path).exists(): found = True path_to_binary = binary_path break if not found: - console_error( - "roofline", "Unable to locate expected binary (%s)." % binary_paths - ) + console_error("roofline", f"Unable to locate expected binary ({binary_paths}).") my_args = [ path_to_binary, "-o", - args.path + "/" + "roofline.csv", + f"{args.path}/roofline.csv", "-d", str(args.device), ] if args.quiet: my_args += "--quiet" - subprocess.run( - my_args, - check=True, - ) + + subprocess.run(my_args, check=True) -def flatten_tcc_info_across_xcds(file, xcds, tcc_channel_per_xcd): +def flatten_tcc_info_across_xcds( + file: str, xcds: int, tcc_channel_per_xcd: int +) -> pd.DataFrame: """ Flatten TCC per channel counters across all XCDs in partition. NB: This func highly depends on the default behavior of rocprofv2 on MI300, which might be broken anytime in the future! """ df_orig = pd.read_csv(file) - # display(df_orig.info) ### prepare column headers tcc_cols_orig = [] @@ -1317,60 +1284,50 @@ def flatten_tcc_info_across_xcds(file, xcds, tcc_channel_per_xcd): tcc_cols_orig.append(c) else: non_tcc_cols_orig.append(c) - # print(tcc_cols_orig) - cols = non_tcc_cols_orig - tcc_cols_in_group = {} - for i in range(0, xcds): - tcc_cols_in_group[i] = [] + cols = non_tcc_cols_orig[:] + tcc_cols_in_group: dict[int, list[str]] = {i: [] for i in range(xcds)} for col in tcc_cols_orig: - for i in range(0, xcds): + for i in range(xcds): # filter the channel index only p = re.compile(r"\[(\d+)\]") - # pick up the 1st element only - r = ( # noqa: E731 - lambda match: "[" - + str(int(match.group(1)) + i * tcc_channel_per_xcd) - + "]" - ) - tcc_cols_in_group[i].append(re.sub(pattern=p, repl=r, string=col)) - for i in range(0, xcds): - # print(tcc_cols_in_group[i]) + # pick up the 1st element only + def replacement(match: re.Match[str]) -> str: + return f"[{int(match.group(1)) + i * tcc_channel_per_xcd}]" + + tcc_cols_in_group[i].append(re.sub(pattern=p, repl=replacement, string=col)) + + for i in range(xcds): cols += tcc_cols_in_group[i] - # print(cols) + df = pd.DataFrame(columns=cols) ### Rearrange data with extended column names - - # print(len(df_orig.index)) for idx in range(0, len(df_orig.index), xcds): # assume the front none TCC columns are the same for all XCCs df_non_tcc = df_orig.iloc[idx].filter(regex=r"^(?!.*TCC).*$") - # display(df_non_tcc) flatten_list = df_non_tcc.tolist() # extract all tcc from one dispatch # NB: assuming default contiguous order might not be safe! df_tcc_all = df_orig.iloc[idx : (idx + xcds)].filter(regex="TCC") - # display(df_tcc_all) for idx, row in df_tcc_all.iterrows(): flatten_list += row.tolist() - # print(len(df.index), len(flatten_list), len(df.columns), flatten_list) # NB: It is not the best perf to append a row once a time df.loc[len(df.index)] = flatten_list return df -def get_submodules(package_name): +def get_submodules(package_name: str) -> list[str]: """List all submodules for a target package""" import importlib import pkgutil - submodules = [] + submodules: list[str] = [] # walk all submodules in target package package = importlib.import_module(package_name) @@ -1383,23 +1340,22 @@ def get_submodules(package_name): return submodules -def is_workload_empty(path): +def is_workload_empty(path: str) -> None: """Peek workload directory to verify valid profiling output""" - pmc_perf_path = path + "/pmc_perf.csv" - if pathlib.Path(pmc_perf_path).is_file(): + pmc_perf_path = Path(path) / "pmc_perf.csv" + if pmc_perf_path.is_file(): temp_df = pd.read_csv(pmc_perf_path) if temp_df.dropna().empty: console_error( - "profiling" - "Found empty cells in %s.\nProfiling data could be corrupt." - % pmc_perf_path + "profiling", + f"Found empty cells in {pmc_perf_path}.\n" + "Profiling data could be corrupt.", ) - else: console_error("analysis", "No profiling data found.") -def print_status(msg): +def print_status(msg: str) -> None: msg_length = len(msg) console_log("") @@ -1409,22 +1365,20 @@ def print_status(msg): console_log("") -def set_locale_encoding(): +def set_locale_encoding() -> None: try: # Attempt to set the locale to 'C.UTF-8' locale.setlocale(locale.LC_ALL, "C.UTF-8") except locale.Error: # If 'C.UTF-8' is not available, check if the current locale is UTF-8 based current_locale = locale.getdefaultlocale() - if current_locale and "UTF-8" in current_locale[1]: + if current_locale and current_locale[1] and "UTF-8" in current_locale[1]: try: locale.setlocale(locale.LC_ALL, current_locale[0]) - except locale.Error as error: + except locale.Error as e: console_error( - "Failed to set locale to the current UTF-8-based locale.", - exit=False, + f"Failed to set locale to the current UTF-8-based locale: {e}" ) - console_error(error) else: console_error( "Please ensure that a UTF-8-based locale is available on your system.", @@ -1432,33 +1386,36 @@ def set_locale_encoding(): ) -def reverse_multi_index_df_pmc(final_df): +def reverse_multi_index_df_pmc( + final_df: pd.DataFrame, +) -> tuple[list[pd.DataFrame], list[Any]]: """ Util function to decompose multi-index dataframe. """ # Check if the columns have more than one level - if len(final_df.columns.levels) < 2: + if not isinstance(final_df.columns, pd.MultiIndex) or final_df.columns.nlevels < 2: raise ValueError("Input DataFrame does not have a multi-index column.") # Extract the first level of the MultiIndex columns (the file names) coll_levels = final_df.columns.get_level_values(0).unique().tolist() # Initialize the list of DataFrames - dfs = [] + dfs: list[pd.DataFrame] = [] # Loop through each 'coll_level' and rebuild the DataFrames for level in coll_levels: # Select columns that belong to the current 'coll_level' columns_for_level = final_df.xs(level, axis=1, level=0) - # Append the DataFrame for this level + if isinstance(columns_for_level, pd.Series): + columns_for_level = columns_for_level.to_frame() dfs.append(columns_for_level) # Return the list of DataFrames and the column levels return dfs, coll_levels -def merge_counters_spatial_multiplex(df_multi_index): +def merge_counters_spatial_multiplex(df_multi_index: pd.DataFrame) -> pd.DataFrame: """ For spatial multiplexing, this merges counter values for the same kernel that runs on different devices. For time stamp, start time stamp will use median @@ -1494,7 +1451,7 @@ def merge_counters_spatial_multiplex(df_multi_index): "Queue_ID", ] - result_dfs = [] + result_dfs: list[pd.DataFrame] = [] # TODO: will need to optimize to avoid this conversion to single index format # and do merge directly on multi-index dataframe @@ -1502,21 +1459,21 @@ def merge_counters_spatial_multiplex(df_multi_index): for df in dfs: kernel_name_column_name = "Kernel_Name" - if not "Kernel_Name" in df and "Name" in df: + if "Kernel_Name" not in df and "Name" in df: kernel_name_column_name = "Name" # Find the values in Kernel_Name that occur more than once kernel_single_occurances = df[kernel_name_column_name].value_counts().index # Define a list to store the merged rows - result_data = [] + result_data: list[dict[str, Any]] = [] for kernel_name in kernel_single_occurances: # Get all rows for the current kernel_name group = df[df[kernel_name_column_name] == kernel_name] # Create a dictionary to store the merged row for the current group - merged_row = {} + merged_row: dict[str, Any] = {} # Process non-counter columns for col in [ @@ -1561,7 +1518,9 @@ def merge_counters_spatial_multiplex(df_multi_index): return final_df -def convert_metric_id_to_panel_info(metric_id): +def convert_metric_id_to_panel_info( + metric_id: str, +) -> tuple[str, Optional[int], Optional[int]]: """ Convert metric id into panel information. Output is a tuples of the form (file_id, panel_id, metric_id). @@ -1583,45 +1542,50 @@ def convert_metric_id_to_panel_info(metric_id): Raises exception for invalid metric id. """ tokens = metric_id.split(".") - if 0 < len(tokens) < 4: - # File id - file_id = str(int(tokens[0])) - # 4 -> 04 - if len(file_id) == 1: - file_id = f"0{file_id}" - # Multiply integer by 100 - file_id = f"{file_id}00" - # Panel id - if len(tokens) > 1: - panel_id = int(tokens[0]) * 100 - panel_id += int(tokens[1]) - else: - panel_id = None - # Metric id - if len(tokens) > 2: - metric_id = int(tokens[2]) - else: - metric_id = None - return (file_id, panel_id, metric_id) - else: - raise Exception(f"Invalid metric id: {metric_id}") + if not (0 < len(tokens) < 4): + raise ValueError(f"Invalid metric id: {metric_id}") + + # File id + file_id = str(int(tokens[0])) + # 4 -> 04 + if len(file_id) == 1: + file_id = f"0{file_id}" + # Multiply integer by 100 + file_id = f"{file_id}00" + + # Panel id + panel_id = None + if len(tokens) > 1: + panel_id = int(tokens[0]) * 100 + int(tokens[1]) + + # Metric id + metric_id_int = None + if len(tokens) > 2: + metric_id_int = int(tokens[2]) + + return (file_id, panel_id, metric_id_int) -def format_time(seconds): +def format_time(seconds: float) -> str: hours = int(seconds // 3600) minutes = int((seconds % 3600) // 60) secs = int(seconds % 60) - parts = [] + parts: list[str] = [] + if hours > 0: parts.append(f"{hours} hour{'s' if hours != 1 else ''}") if minutes > 0: parts.append(f"{minutes} minute{'s' if minutes != 1 else ''}") if secs > 0 or not parts: parts.append(f"{secs} second{'s' if secs != 1 else ''}") - return ", ".join(parts[:-1]) + (" and " if len(parts) > 1 else "") + parts[-1] + + if len(parts) <= 1: + return parts[0] if parts else "0 seconds" + + return ", ".join(parts[:-1]) + f" and {parts[-1]}" -def parse_sets_yaml(arch): +def parse_sets_yaml(arch: str) -> dict[str, Any]: filename = ( config.rocprof_compute_home / "rocprof_compute_soc" @@ -1629,13 +1593,13 @@ def parse_sets_yaml(arch): / "sets" / f"{arch}_sets.yaml" ) - with open(filename, "r") as file: + with open(filename) as file: content = file.read() data = yaml.safe_load(content) sets_data = data.get("sets", []) - sets_info = {} + sets_info: dict[str, Any] = {} for set_item in sets_data: set_option = set_item.get("set_option", "") if set_option: @@ -1643,7 +1607,7 @@ def parse_sets_yaml(arch): return sets_info -def get_uuid(length=8): +def get_uuid(length: int = 8) -> str: return uuid.uuid4().hex[:length] diff --git a/projects/rocprofiler-compute/tests/test_analyze_commands.py b/projects/rocprofiler-compute/tests/test_analyze_commands.py index 9a87a3d7b2..f9e134872e 100644 --- a/projects/rocprofiler-compute/tests/test_analyze_commands.py +++ b/projects/rocprofiler-compute/tests/test_analyze_commands.py @@ -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() diff --git a/projects/rocprofiler-compute/tests/test_autogen_config.py b/projects/rocprofiler-compute/tests/test_autogen_config.py index f13ca19953..acd8337520 100644 --- a/projects/rocprofiler-compute/tests/test_autogen_config.py +++ b/projects/rocprofiler-compute/tests/test_autogen_config.py @@ -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() diff --git a/projects/rocprofiler-compute/tests/test_gpu_specs.py b/projects/rocprofiler-compute/tests/test_gpu_specs.py index b970fccb7b..6fffadb0b6 100644 --- a/projects/rocprofiler-compute/tests/test_gpu_specs.py +++ b/projects/rocprofiler-compute/tests/test_gpu_specs.py @@ -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") diff --git a/projects/rocprofiler-compute/tests/test_profile_general.py b/projects/rocprofiler-compute/tests/test_profile_general.py index a961ebdfa1..96d50abccc 100644 --- a/projects/rocprofiler-compute/tests/test_profile_general.py +++ b/projects/rocprofiler-compute/tests/test_profile_general.py @@ -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" ) diff --git a/projects/rocprofiler-compute/tests/test_utils.py b/projects/rocprofiler-compute/tests/test_utils.py index 3dd5104ff8..fb45e8e26b 100644 --- a/projects/rocprofiler-compute/tests/test_utils.py +++ b/projects/rocprofiler-compute/tests/test_utils.py @@ -30,7 +30,6 @@ import json import locale import logging import os -import pathlib import re import shutil import subprocess @@ -189,7 +188,7 @@ def test_get_version_finds_version_in_home(tmp_path, monkeypatch): given directory. Args: - tmp_path (pathlib.Path): Temporary path provided by pytest for test isolation. + tmp_path (Path): Temporary path provided by pytest for test isolation. monkeypatch (pytest.MonkeyPatch): Pytest fixture to modify or simulate behavior of modules/functions. @@ -219,7 +218,7 @@ def test_get_version_finds_version_in_parent(tmp_path, monkeypatch): in the given directory. Args: - tmp_path (pathlib.Path): Temporary path provided by pytest for test isolation. + tmp_path (Path): Temporary path provided by pytest for test isolation. monkeypatch (pytest.MonkeyPatch): Pytest fixture to modify or simulate behavior of modules/functions. @@ -280,7 +279,7 @@ def test_get_version_git_success(tmp_path, monkeypatch): Test get_version returns correct version info when git command succeeds. Args: - tmp_path (pathlib.Path): Temporary directory for test files. + tmp_path (Path): Temporary directory for test files. monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching. Returns: @@ -309,7 +308,7 @@ def test_get_version_git_fails_sha_file(tmp_path, monkeypatch): Test get_version returns correct version info when git fails but VERSION.sha exists. Args: - tmp_path (pathlib.Path): Temporary directory for test files. + tmp_path (Path): Temporary directory for test files. monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching. Returns: @@ -343,7 +342,7 @@ def test_get_version_git_and_sha_fail(tmp_path, monkeypatch): Test get_version returns unknown sha and mode when both git and VERSION.sha fail. Args: - tmp_path (pathlib.Path): Temporary directory for test files. + tmp_path (Path): Temporary directory for test files. monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching. Returns: @@ -1310,7 +1309,7 @@ def test_v3_json_to_csv_basic_functionality(tmp_path, monkeypatch): Test basic functionality of v3_json_to_csv with a minimal valid JSON input. Args: - tmp_path (pathlib.Path): Temporary directory for test files + tmp_path (Path): Temporary directory for test files monkeypatch (pytest.MonkeyPatch): Pytest fixture for modifying behavior """ @@ -1425,7 +1424,7 @@ def test_v3_json_to_csv_no_dispatches(tmp_path, monkeypatch): Should create an empty CSV with headers. Args: - tmp_path (pathlib.Path): Temporary directory for test files + tmp_path (Path): Temporary directory for test files monkeypatch (pytest.MonkeyPatch): Pytest fixture for modifying behavior """ @@ -1480,7 +1479,7 @@ def test_v3_json_to_csv_accumulated_counters(tmp_path, monkeypatch): Should rename them to SQ_ACCUM_PREV_HIRES. Args: - tmp_path (pathlib.Path): Temporary directory for test files + tmp_path (Path): Temporary directory for test files monkeypatch (pytest.MonkeyPatch): Pytest fixture for modifying behavior """ @@ -1590,7 +1589,7 @@ def test_v3_json_to_csv_duplicate_counters(tmp_path, monkeypatch): Should sum the values. Args: - tmp_path (pathlib.Path): Temporary directory for test files + tmp_path (Path): Temporary directory for test files monkeypatch (pytest.MonkeyPatch): Pytest fixture for modifying behavior """ @@ -1718,7 +1717,7 @@ def test_v3_json_to_csv_invalid_json(tmp_path): Should raise JSONDecodeError. Args: - tmp_path (pathlib.Path): Temporary directory for test files + tmp_path (Path): Temporary directory for test files """ json_path = tmp_path / "invalid.json" with open(json_path, "w") as f: @@ -1736,7 +1735,7 @@ def test_v3_json_to_csv_missing_required_keys(tmp_path): Should raise KeyError. Args: - tmp_path (pathlib.Path): Temporary directory for test files + tmp_path (Path): Temporary directory for test files """ invalid_json = { @@ -1764,7 +1763,7 @@ def test_v3_json_to_csv_complex_dispatch(tmp_path, monkeypatch): multiple dispatches and 3D grid/workgroup sizes. Args: - tmp_path (pathlib.Path): Temporary directory for test files + tmp_path (Path): Temporary directory for test files monkeypatch (pytest.MonkeyPatch): Pytest fixture for modifying behavior """ @@ -1943,7 +1942,7 @@ def test_v3_json_to_csv_missing_counters_handling(tmp_path, monkeypatch): where arrays have different lengths. Args: - tmp_path (pathlib.Path): Temporary directory for test files + tmp_path (Path): Temporary directory for test files monkeypatch (pytest.MonkeyPatch): Pytest fixture for modifying behavior """ @@ -2228,7 +2227,7 @@ def test_parse_text_basic(tmp_path): """Test parse_text with a simple valid input file. Args: - tmp_path (pathlib.Path): Temporary path fixture provided by pytest. + tmp_path (Path): Temporary path fixture provided by pytest. Returns: None: Asserts that counters are correctly extracted from a simple file. @@ -2244,7 +2243,7 @@ def test_parse_text_empty_file(tmp_path): """Test parse_text with an empty file. Args: - tmp_path (pathlib.Path): Temporary path fixture provided by pytest. + tmp_path (Path): Temporary path fixture provided by pytest. Returns: None: Asserts that an empty file returns an empty list. @@ -2260,7 +2259,7 @@ def test_parse_text_no_pmc_entries(tmp_path): """Test parse_text with a file that doesn't contain any 'pmc:' entries. Args: - tmp_path (pathlib.Path): Temporary path fixture provided by pytest. + tmp_path (Path): Temporary path fixture provided by pytest. Returns: None: Asserts that a file without 'pmc:' returns an empty list. @@ -2276,7 +2275,7 @@ def test_parse_text_with_comments(tmp_path): """Test parse_text with lines that have comments after the counters. Args: - tmp_path (pathlib.Path): Temporary path fixture provided by pytest. + tmp_path (Path): Temporary path fixture provided by pytest. Returns: None: Asserts that comments are properly stripped from counter lines. @@ -2292,7 +2291,7 @@ def test_parse_text_multiple_lines(tmp_path): """Test parse_text with multiple 'pmc:' lines. Args: - tmp_path (pathlib.Path): Temporary path fixture provided by pytest. + tmp_path (Path): Temporary path fixture provided by pytest. Returns: None: Asserts counters from multiple lines are correctly combined. @@ -2308,7 +2307,7 @@ def test_parse_text_mixed_lines(tmp_path): """Test parse_text with a mix of 'pmc:' and non-'pmc:' lines. Args: - tmp_path (pathlib.Path): Temporary path fixture provided by pytest. + tmp_path (Path): Temporary path fixture provided by pytest. Returns: None: Asserts that only counters from 'pmc:' lines are extracted. @@ -2326,7 +2325,7 @@ def test_parse_text_whitespace_handling(tmp_path): """Test parse_text with various whitespace combinations. Args: - tmp_path (pathlib.Path): Temporary path fixture provided by pytest. + tmp_path (Path): Temporary path fixture provided by pytest. Returns: None: Asserts that whitespace is properly handled in counter extraction. @@ -2352,7 +2351,7 @@ def test_parse_text_edge_cases(tmp_path): """Test parse_text with edge cases like empty 'pmc:' lines. Args: - tmp_path (pathlib.Path): Temporary path fixture provided by pytest. + tmp_path (Path): Temporary path fixture provided by pytest. Returns: None: Asserts that edge cases are handled correctly. @@ -2385,7 +2384,7 @@ def test_run_prof_success_v2(tmp_path, monkeypatch): Test run_prof with rocprofv2 successful execution. Args: - tmp_path (pathlib.Path): Temporary directory for test files. + tmp_path (Path): Temporary directory for test files. monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching. Returns: @@ -2403,7 +2402,7 @@ def test_run_prof_success_v2(tmp_path, monkeypatch): class MockSpec: def __init__(self): self.gpu_model = "mi250x" - self._l2_banks = 32 + self.l2_banks = 32 self.gpu_arch = "gfx90a" self.compute_partition = "CPX" @@ -2433,7 +2432,7 @@ def test_run_prof_success_v3_csv(tmp_path, monkeypatch): Test run_prof with rocprofv3 using CSV format. Args: - tmp_path (pathlib.Path): Temporary directory for test files. + tmp_path (Path): Temporary directory for test files. monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching. Returns: @@ -2449,7 +2448,7 @@ def test_run_prof_success_v3_csv(tmp_path, monkeypatch): self.gpu_model = "mi300x" self.gpu_arch = "gfx942" self.compute_partition = "SPX" - self._l2_banks = 32 + self.l2_banks = 32 mspec = MockSpec() @@ -2481,7 +2480,7 @@ def test_run_prof_success_rocprofiler_sdk(tmp_path, monkeypatch): Test run_prof with rocprofiler-sdk execution. Args: - tmp_path (pathlib.Path): Temporary directory for test files. + tmp_path (Path): Temporary directory for test files. monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching. Returns: @@ -2496,7 +2495,7 @@ def test_run_prof_success_rocprofiler_sdk(tmp_path, monkeypatch): self.gpu_model = "mi300x" self.gpu_arch = "gfx942" self.compute_partition = "SPX" - self._l2_banks = 32 + self.l2_banks = 32 mspec = MockSpec() @@ -2530,7 +2529,7 @@ def test_run_prof_with_yaml_config(tmp_path, monkeypatch): Test run_prof with additional YAML configuration file. Args: - tmp_path (pathlib.Path): Temporary directory for test files. + tmp_path (Path): Temporary directory for test files. monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching. Returns: @@ -2547,7 +2546,7 @@ def test_run_prof_with_yaml_config(tmp_path, monkeypatch): self.gpu_model = "mi300x" self.gpu_arch = "gfx942" self.compute_partition = "SPX" - self._l2_banks = 32 + self.l2_banks = 32 mspec = MockSpec() @@ -2575,7 +2574,7 @@ def test_run_prof_failure_subprocess(tmp_path, monkeypatch): Test run_prof when subprocess execution fails. Args: - tmp_path (pathlib.Path): Temporary directory for test files. + tmp_path (Path): Temporary directory for test files. monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching. Returns: @@ -2590,7 +2589,7 @@ def test_run_prof_failure_subprocess(tmp_path, monkeypatch): self.gpu_model = "mi300x" self.gpu_arch = "gfx942" self.compute_partition = "SPX" - self._l2_banks = 32 + self.l2_banks = 32 mspec = MockSpec() @@ -2622,7 +2621,7 @@ def test_run_prof_mi300_environment_setup(tmp_path, monkeypatch): Test run_prof sets proper environment variables for MI300 series GPUs. Args: - tmp_path (pathlib.Path): Temporary directory for test files. + tmp_path (Path): Temporary directory for test files. monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching. Returns: @@ -2637,7 +2636,7 @@ def test_run_prof_mi300_environment_setup(tmp_path, monkeypatch): self.gpu_model = "mi300x" self.gpu_arch = "gfx942" self.compute_partition = "SPX" - self._l2_banks = 32 + self.l2_banks = 32 mspec = MockSpec() @@ -2672,7 +2671,7 @@ def test_run_prof_timestamps_special_case(tmp_path, monkeypatch): Test run_prof handles timestamps.txt special case correctly. Args: - tmp_path (pathlib.Path): Temporary directory for test files. + tmp_path (Path): Temporary directory for test files. monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching. Returns: @@ -2689,7 +2688,7 @@ def test_run_prof_timestamps_special_case(tmp_path, monkeypatch): self.gpu_model = "mi300x" self.gpu_arch = "gfx942" self.compute_partition = "SPX" - self._l2_banks = 32 + self.l2_banks = 32 mspec = MockSpec() @@ -2730,7 +2729,7 @@ def test_run_prof_no_results_files(tmp_path, monkeypatch): Test run_prof when no results files are generated. Args: - tmp_path (pathlib.Path): Temporary directory for test files. + tmp_path (Path): Temporary directory for test files. monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching. Returns: @@ -2745,7 +2744,7 @@ def test_run_prof_no_results_files(tmp_path, monkeypatch): self.gpu_model = "mi300x" self.gpu_arch = "gfx942" self.compute_partition = "SPX" - self._l2_banks = 32 + self.l2_banks = 32 mspec = MockSpec() @@ -2769,7 +2768,7 @@ def test_run_prof_header_standardization(tmp_path, monkeypatch): Test run_prof properly standardizes CSV headers. Args: - tmp_path (pathlib.Path): Temporary directory for test files. + tmp_path (Path): Temporary directory for test files. monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching. Returns: @@ -2786,7 +2785,7 @@ def test_run_prof_header_standardization(tmp_path, monkeypatch): self.gpu_model = "mi300x" self.gpu_arch = "gfx942" self.compute_partition = "SPX" - self._l2_banks = 32 + self.l2_banks = 32 mspec = MockSpec() @@ -2850,7 +2849,7 @@ def test_run_prof_tcc_flattening_mi300(tmp_path, monkeypatch): Test run_prof applies TCC flattening for MI300 series GPUs. Args: - tmp_path (pathlib.Path): Temporary directory for test files. + tmp_path (Path): Temporary directory for test files. monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching. Returns: @@ -2865,7 +2864,7 @@ def test_run_prof_tcc_flattening_mi300(tmp_path, monkeypatch): self.gpu_model = "mi300x" self.gpu_arch = "gfx942" self.compute_partition = "SPX" - self._l2_banks = 32 + self.l2_banks = 32 mspec = MockSpec() @@ -2921,7 +2920,7 @@ class MockMSpec: self.gpu_model = gpu_model self.gpu_arch = gpu_arch self.compute_partition = compute_partition - self._l2_banks = l2_banks + self.l2_banks = l2_banks def test_run_prof_sdk_creates_new_env_copy(tmp_path, monkeypatch): @@ -2931,7 +2930,7 @@ def test_run_prof_sdk_creates_new_env_copy(tmp_path, monkeypatch): by the mspec.gpu_model check. """ fname_str = str(tmp_path / "counters.txt") - pathlib.Path(fname_str).touch() + Path(fname_str).touch() workload_dir_str = str(tmp_path) monkeypatch.setattr("utils.utils.rocprof_cmd", "rocprofiler-sdk") @@ -2958,15 +2957,15 @@ def test_run_prof_sdk_creates_new_env_copy(tmp_path, monkeypatch): "utils.utils.parse_text", lambda *a, **k: ["COUNTER1", "COUNTER2"] ) - mock_fname_path_obj = mock.Mock(spec=pathlib.Path) + mock_fname_path_obj = mock.Mock(spec=Path) mock_fname_path_obj.stem = "counters" mock_fname_path_obj.name = "counters.txt" mock_fname_path_obj.with_suffix.return_value.exists.return_value = False - mock_out_path_obj = mock.Mock(spec=pathlib.Path) + mock_out_path_obj = mock.Mock(spec=Path) mock_out_path_obj.exists.return_value = False def path_side_effect(p_arg, *args): - if isinstance(p_arg, pathlib.Path): + if isinstance(p_arg, Path): if p_arg.name == "counters.txt": return mock_fname_path_obj return p_arg @@ -2983,7 +2982,7 @@ def test_run_prof_sdk_creates_new_env_copy(tmp_path, monkeypatch): return mock_fname_path_obj return mock_fname_path_obj - monkeypatch.setattr("utils.utils.path", path_side_effect) + monkeypatch.setattr("utils.utils.Path", path_side_effect) original_env_var = "original_value" monkeypatch.setenv("EXISTING_VAR", original_env_var) @@ -3032,7 +3031,7 @@ def test_run_prof_v3_sdk_and_cli_calls_trace_processing(tmp_path, monkeypatch): process_hip_trace_output(...) """ fname_str = str(tmp_path / "counters.txt") - pathlib.Path(fname_str).touch() + Path(fname_str).touch() fbase_str = "counters" workload_dir_str = str(tmp_path) (tmp_path / "out" / "pmc_1").mkdir(parents=True, exist_ok=True) @@ -3065,17 +3064,17 @@ def test_run_prof_v3_sdk_and_cli_calls_trace_processing(tmp_path, monkeypatch): monkeypatch.setattr("utils.utils.console_warning", lambda *a, **k: None) monkeypatch.setattr("utils.utils.parse_text", lambda *a, **k: ["C1"]) - mock_fname_path_obj = mock.MagicMock(spec=pathlib.Path) + mock_fname_path_obj = mock.MagicMock(spec=Path) mock_fname_path_obj.stem = fbase_str mock_fname_path_obj.name = "counters.txt" mock_fname_path_obj.with_suffix.return_value.exists.return_value = False - mock_fname_path_obj.__truediv__.return_value = mock.Mock(spec=pathlib.Path) + mock_fname_path_obj.__truediv__.return_value = mock.Mock(spec=Path) - mock_out_path_obj = mock.MagicMock(spec=pathlib.Path) + mock_out_path_obj = mock.MagicMock(spec=Path) mock_out_path_obj.exists.return_value = True def path_side_effect(p_arg, *args): - if isinstance(p_arg, pathlib.Path) and p_arg.name == "counters.txt": + if isinstance(p_arg, Path) and p_arg.name == "counters.txt": return mock_fname_path_obj if isinstance(p_arg, str) and p_arg.endswith("/out"): return mock_out_path_obj @@ -3089,7 +3088,7 @@ def test_run_prof_v3_sdk_and_cli_calls_trace_processing(tmp_path, monkeypatch): return mock_fname_path_obj return mock_fname_path_obj - monkeypatch.setattr("utils.utils.path", path_side_effect) + monkeypatch.setattr("utils.utils.Path", path_side_effect) dummy_df = pd.DataFrame({"Dispatch_ID": [0], "A": [1]}) monkeypatch.setattr("pandas.read_csv", lambda *a, **k: dummy_df.copy()) @@ -3170,7 +3169,7 @@ def test_process_rocprofv3_output_json_format(tmp_path, monkeypatch): Test process_rocprofv3_output with json format converts JSON files to CSV. Args: - tmp_path (pathlib.Path): Temporary directory for test files. + tmp_path (Path): Temporary directory for test files. monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching. Returns: @@ -3208,7 +3207,7 @@ def test_process_rocprofv3_output_csv_format_with_counter_files(tmp_path, monkey Test process_rocprofv3_output with csv format processes counter collection files. Args: - tmp_path (pathlib.Path): Temporary directory for test files. + tmp_path (Path): Temporary directory for test files. monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching. Returns: @@ -3254,7 +3253,7 @@ def test_process_rocprofv3_output_csv_format_conversion_error(tmp_path, monkeypa Test process_rocprofv3_output handles conversion errors gracefully. Args: - tmp_path (pathlib.Path): Temporary directory for test files. + tmp_path (Path): Temporary directory for test files. monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching. Returns: @@ -3301,7 +3300,7 @@ def test_process_rocprofv3_output_csv_format_missing_agent_file(tmp_path, monkey Test process_rocprofv3_output raises error when agent info file is missing. Args: - tmp_path (pathlib.Path): Temporary directory for test files. + tmp_path (Path): Temporary directory for test files. monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching. Returns: @@ -3323,7 +3322,7 @@ def test_process_rocprofv3_output_csv_format_missing_agent_file(tmp_path, monkey import utils.utils as utils_mod - with pytest.raises(ValueError, match='has no coresponding "agent info" file'): + with pytest.raises(ValueError, match='has no corresponding "agent info" file'): utils_mod.process_rocprofv3_output("csv", workload_dir, False) @@ -3332,7 +3331,7 @@ def test_process_rocprofv3_output_csv_format_timestamps_fallback(tmp_path, monke Test process_rocprofv3_output falls back to kernel trace files for timestamps. Args: - tmp_path (pathlib.Path): Temporary directory for test files. + tmp_path (Path): Temporary directory for test files. monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching. Returns: @@ -3370,7 +3369,7 @@ def test_process_rocprofv3_output_csv_format_no_files_non_timestamps( no files found for non-timestamps. Args: - tmp_path (pathlib.Path): Temporary directory for test files. + tmp_path (Path): Temporary directory for test files. monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching. Returns: @@ -3416,7 +3415,7 @@ def test_process_rocprofv3_output_json_format_no_files(tmp_path, monkeypatch): Test process_rocprofv3_output with json format when no JSON files exist. Args: - tmp_path (pathlib.Path): Temporary directory for test files. + tmp_path (Path): Temporary directory for test files. monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching. Returns: @@ -3440,7 +3439,7 @@ def test_process_rocprofv3_output_csv_format_multiple_counter_files( Test process_rocprofv3_output processes multiple counter collection files. Args: - tmp_path (pathlib.Path): Temporary directory for test files. + tmp_path (Path): Temporary directory for test files. monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching. Returns: @@ -3535,7 +3534,7 @@ def test_process_kokkos_trace_output_single_file(tmp_path, monkeypatch): Test process_kokkos_trace_output with a single CSV file. Args: - tmp_path (pathlib.Path): Temporary directory for test files. + tmp_path (Path): Temporary directory for test files. monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching. Returns: @@ -3626,7 +3625,7 @@ def test_process_kokkos_trace_output_no_files_found(tmp_path, monkeypatch): Should handle empty file list gracefully. Args: - tmp_path (pathlib.Path): Temporary directory for test files. + tmp_path (Path): Temporary directory for test files. monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching. Returns: @@ -3675,7 +3674,7 @@ def test_process_kokkos_trace_output_mixed_file_states(tmp_path, monkeypatch): Test process_kokkos_trace_output with a mix of valid, empty, and corrupted files. Args: - tmp_path (pathlib.Path): Temporary directory for test files. + tmp_path (Path): Temporary directory for test files. monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching. Returns: @@ -3734,7 +3733,7 @@ def test_process_kokkos_trace_output_no_out_directory(tmp_path, monkeypatch): Should not copy file to workload directory. Args: - tmp_path (pathlib.Path): Temporary directory for test files. + tmp_path (Path): Temporary directory for test files. monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching. Returns: @@ -3763,7 +3762,7 @@ def test_process_kokkos_trace_output_no_out_directory(tmp_path, monkeypatch): monkeypatch.setattr("pandas.DataFrame.to_csv", mock_to_csv) - original_path = utils.path + original_path = utils.Path def mock_path_exists(path_str): if path_str == workload_dir + "/out": @@ -3773,7 +3772,7 @@ def test_process_kokkos_trace_output_no_out_directory(tmp_path, monkeypatch): else: return original_path(path_str) - monkeypatch.setattr("utils.utils.path", mock_path_exists) + monkeypatch.setattr("utils.utils.Path", mock_path_exists) import utils.utils as utils_mod @@ -3797,7 +3796,7 @@ def test_process_kokkos_trace_output_csv_with_only_headers(tmp_path, monkeypatch only headers but no data. Args: - tmp_path (pathlib.Path): Temporary directory for test files. + tmp_path (Path): Temporary directory for test files. monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching. Returns: @@ -3834,7 +3833,7 @@ def test_process_kokkos_trace_output_large_files(tmp_path, monkeypatch): Test process_kokkos_trace_output with larger CSV files to ensure memory handling. Args: - tmp_path (pathlib.Path): Temporary directory for test files. + tmp_path (Path): Temporary directory for test files. monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching. Returns: @@ -3885,7 +3884,7 @@ def test_process_kokkos_trace_output_unicode_content(tmp_path, monkeypatch): Test process_kokkos_trace_output with CSV files containing unicode characters. Args: - tmp_path (pathlib.Path): Temporary directory for test files. + tmp_path (Path): Temporary directory for test files. monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching. Returns: @@ -3926,7 +3925,7 @@ def test_process_kokkos_trace_output_different_schemas(tmp_path, monkeypatch): Test process_kokkos_trace_output with CSV files having different column schemas. Args: - tmp_path (pathlib.Path): Temporary directory for test files. + tmp_path (Path): Temporary directory for test files. monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching. Returns: @@ -3982,7 +3981,7 @@ def test_process_kokkos_trace_output_permission_error(tmp_path, monkeypatch): errors during file operations. Args: - tmp_path (pathlib.Path): Temporary directory for test files. + tmp_path (Path): Temporary directory for test files. monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching. Returns: @@ -4319,7 +4318,7 @@ def test_process_hip_trace_output_no_out_directory(tmp_path, monkeypatch): monkeypatch.setattr("pandas.DataFrame.to_csv", mock_to_csv) - original_path = utils.path + original_path = utils.Path def mock_path_exists(path_str): if path_str == workload_dir + "/out": @@ -4329,7 +4328,7 @@ def test_process_hip_trace_output_no_out_directory(tmp_path, monkeypatch): else: return original_path(path_str) - monkeypatch.setattr("utils.utils.path", mock_path_exists) + monkeypatch.setattr("utils.utils.Path", mock_path_exists) import utils.utils as utils_mod @@ -4774,7 +4773,7 @@ def test_mibench_override_distro_success(tmp_path, monkeypatch): Test mibench with override distro that successfully finds and executes binary. Args: - tmp_path (pathlib.Path): Temporary directory for test files. + tmp_path (Path): Temporary directory for test files. monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching. Returns: @@ -4829,7 +4828,7 @@ def test_mibench_standard_distro_first_path_exists(tmp_path, monkeypatch): Test mibench with standard distro where first potential path exists. Args: - tmp_path (pathlib.Path): Temporary directory for test files. + tmp_path (Path): Temporary directory for test files. monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching. Returns: @@ -4906,7 +4905,7 @@ def test_mibench_standard_distro_second_path_exists(tmp_path, monkeypatch): Test mibench with standard distro where second potential path exists. Args: - tmp_path (pathlib.Path): Temporary directory for test files. + tmp_path (Path): Temporary directory for test files. monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching. Returns: @@ -4976,7 +4975,13 @@ def test_mibench_standard_distro_second_path_exists(tmp_path, monkeypatch): utils_mod.mibench(MockArgs(), SimpleNamespace(rocm_version="0.x.x")) assert len(subprocess_calls) == 1 - expected_args = [str(binary_path), "-o", str(tmp_path) + "/roofline.csv", "-d", "2"] # noqa + expected_args = [ # noqa: F841 + str(binary_path), + "-o", + str(tmp_path) + "/roofline.csv", + "-d", + "2", + ] def test_mibench_no_binary_found_error(tmp_path, monkeypatch): @@ -4984,7 +4989,7 @@ def test_mibench_no_binary_found_error(tmp_path, monkeypatch): Test mibench when no binary paths exist, should call console_error. Args: - tmp_path (pathlib.Path): Temporary directory for test files. + tmp_path (Path): Temporary directory for test files. monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching. Returns: @@ -5058,7 +5063,7 @@ def test_mibench_quiet_flag_handling_bug(tmp_path, monkeypatch): Test mibench quiet flag handling demonstrates the bug where += splits the string. Args: - tmp_path (pathlib.Path): Temporary directory for test files. + tmp_path (Path): Temporary directory for test files. monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching. Returns: @@ -5133,7 +5138,15 @@ def test_mibench_quiet_flag_handling_bug(tmp_path, monkeypatch): "-d", "0", ] - expected_full_args = expected_base_args + ["-", "-", "q", "u", "i", "e", "t"] # noqa + expected_full_args = expected_base_args + [ # noqa: F841 + "-", + "-", + "q", + "u", + "i", + "e", + "t", + ] subprocess_calls.clear() @@ -5147,7 +5160,13 @@ def test_mibench_quiet_flag_handling_bug(tmp_path, monkeypatch): utils_mod.mibench(MockArgsQuiet(), SimpleNamespace(rocm_version="0.x.x")) - expected_args = [str(binary_path), "-o", str(tmp_path) + "/roofline.csv", "-d", "0"] # noqa + expected_args = [ # noqa: F841 + str(binary_path), + "-o", + str(tmp_path) + "/roofline.csv", + "-d", + "0", + ] def test_mibench_sles_distro_mapping(tmp_path, monkeypatch): @@ -5155,7 +5174,7 @@ def test_mibench_sles_distro_mapping(tmp_path, monkeypatch): Test mibench with SLES distro mapping. Args: - tmp_path (pathlib.Path): Temporary directory for test files. + tmp_path (Path): Temporary directory for test files. monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching. Returns: @@ -5232,7 +5251,7 @@ def test_mibench_subprocess_run_failure(tmp_path, monkeypatch): Test mibench when subprocess.run raises an exception. Args: - tmp_path (pathlib.Path): Temporary directory for test files. + tmp_path (Path): Temporary directory for test files. monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching. Returns: @@ -5276,7 +5295,7 @@ def test_mibench_device_string_conversion(tmp_path, monkeypatch): Test mibench correctly converts device ID to string. Args: - tmp_path (pathlib.Path): Temporary directory for test files. + tmp_path (Path): Temporary directory for test files. monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching. Returns: @@ -5326,7 +5345,7 @@ def test_mibench_unknown_distro_mapping(tmp_path, monkeypatch): Test mibench behavior with unknown distro (should cause KeyError). Args: - tmp_path (pathlib.Path): Temporary directory for test files. + tmp_path (Path): Temporary directory for test files. monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching. Returns: @@ -5389,7 +5408,7 @@ def test_mibench_console_log_called(tmp_path, monkeypatch): Test mibench calls console_log with correct message. Args: - tmp_path (pathlib.Path): Temporary directory for test files. + tmp_path (Path): Temporary directory for test files. monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching. Returns: @@ -5475,7 +5494,7 @@ def test_flatten_tcc_info_across_xcds_zero_xcds(tmp_path): Test edge case with zero XCDs. Args: - tmp_path (pathlib.Path): Temporary directory for test files. + tmp_path (Path): Temporary directory for test files. Returns: None: Asserts function handles zero XCDs edge case by raising ValueError. @@ -5500,7 +5519,7 @@ def test_flatten_tcc_info_across_xcds_insufficient_data(tmp_path): Test when there's insufficient data for the specified XCDs. Args: - tmp_path (pathlib.Path): Temporary directory for test files. + tmp_path (Path): Temporary directory for test files. Returns: None: Asserts function raises ValueError when trying @@ -5526,7 +5545,7 @@ def test_flatten_tcc_info_across_xcds_irregular_tcc_column_names(tmp_path): Test with irregular TCC column naming patterns. Args: - tmp_path (pathlib.Path): Temporary directory for test files. + tmp_path (Path): Temporary directory for test files. Returns: None: Asserts function handles various TCC column name @@ -5575,7 +5594,7 @@ def test_flatten_tcc_info_across_xcds_regex_pattern_validation(tmp_path): Test that regex pattern correctly identifies channel indices. Args: - tmp_path (pathlib.Path): Temporary directory for test files. + tmp_path (Path): Temporary directory for test files. Returns: None: Asserts regex pattern works for various channel @@ -5623,7 +5642,7 @@ def test_flatten_tcc_info_across_xcds_edge_case_validation(tmp_path): flatten_tcc_info_across_xcds. Args: - tmp_path (pathlib.Path): Temporary directory for test files. + tmp_path (Path): Temporary directory for test files. Returns: None: Asserts function behavior with various edge cases. @@ -5660,7 +5679,7 @@ def test_flatten_tcc_info_across_xcds_pandas_filter_issue(tmp_path): Test demonstrating the pandas filter regex issue that causes Series ambiguity error. Args: - tmp_path (pathlib.Path): Temporary directory for test files. + tmp_path (Path): Temporary directory for test files. Returns: None: Documents the pandas boolean evaluation issue in the function. @@ -5703,7 +5722,7 @@ def test_flatten_tcc_info_across_xcds_successful_cases_only(tmp_path): Test only the cases that are expected to work successfully. Args: - tmp_path (pathlib.Path): Temporary directory for test files. + tmp_path (Path): Temporary directory for test files. Returns: None: Asserts successful operation for known working scenarios. @@ -6238,7 +6257,7 @@ Directory access issues String Formatting and Dependencies: Console error message formatting -Path handling (string vs pathlib.Path) +Path handling (string vs Path) Pandas dependency verification Return value consistency Special Scenarios: @@ -6255,7 +6274,7 @@ def test_is_workload_empty_valid_data_file(tmp_path): Test is_workload_empty with a valid pmc_perf.csv file containing data. Args: - tmp_path (pathlib.Path): Temporary directory for test files. + tmp_path (Path): Temporary directory for test files. Returns: None: Asserts function handles valid data files without errors. @@ -6290,7 +6309,7 @@ def test_is_workload_empty_file_with_nan_values(tmp_path): Test is_workload_empty with pmc_perf.csv containing NaN values. Args: - tmp_path (pathlib.Path): Temporary directory for test files. + tmp_path (Path): Temporary directory for test files. Returns: None: Asserts function detects and reports empty cells after dropping NaN. @@ -6319,9 +6338,10 @@ NaN,,,""" assert len(console_error_calls) == 1 error_args = console_error_calls[0][0] - assert "profilingFound empty cells" in error_args[0] - assert "pmc_perf.csv" in error_args[0] - assert "Profiling data could be corrupt" in error_args[0] + assert "profiling" in error_args[0] + assert "Found empty cells" in error_args[1] + assert "pmc_perf.csv" in error_args[1] + assert "Profiling data could be corrupt" in error_args[1] def test_is_workload_empty_completely_empty_csv(tmp_path): @@ -6329,7 +6349,7 @@ def test_is_workload_empty_completely_empty_csv(tmp_path): Test is_workload_empty with completely empty pmc_perf.csv file. Args: - tmp_path (pathlib.Path): Temporary directory for test files. + tmp_path (Path): Temporary directory for test files. Returns: None: Asserts function detects empty CSV file. @@ -6361,7 +6381,7 @@ def test_is_workload_empty_headers_only_csv(tmp_path): Test is_workload_empty with CSV containing only headers. Args: - tmp_path (pathlib.Path): Temporary directory for test files. + tmp_path (Path): Temporary directory for test files. Returns: None: Asserts function detects CSV with headers but no data. @@ -6387,7 +6407,8 @@ def test_is_workload_empty_headers_only_csv(tmp_path): assert len(console_error_calls) == 1 error_args = console_error_calls[0][0] - assert "profilingFound empty cells" in error_args[0] + assert "profiling" in error_args[0] + assert "Found empty cells" in error_args[1] def test_is_workload_empty_no_pmc_perf_file(tmp_path): @@ -6395,7 +6416,7 @@ def test_is_workload_empty_no_pmc_perf_file(tmp_path): Test is_workload_empty when pmc_perf.csv file doesn't exist. Args: - tmp_path (pathlib.Path): Temporary directory for test files. + tmp_path (Path): Temporary directory for test files. Returns: None: Asserts function detects missing profiling data file. @@ -6451,7 +6472,7 @@ def test_is_workload_empty_malformed_csv(tmp_path): Test is_workload_empty with malformed CSV that causes pandas read error. Args: - tmp_path (pathlib.Path): Temporary directory for test files. + tmp_path (Path): Temporary directory for test files. Returns: None: Asserts function handles pandas CSV reading errors gracefully. @@ -6487,7 +6508,7 @@ def test_is_workload_empty_mixed_valid_invalid_data(tmp_path): Test is_workload_empty with CSV containing mix of valid and invalid (NaN) data. Args: - tmp_path (pathlib.Path): Temporary directory for test files. + tmp_path (Path): Temporary directory for test files. Returns: None: Asserts function handles mixed data correctly. @@ -6523,7 +6544,7 @@ def test_is_workload_empty_large_dataset_with_nans(tmp_path): Test is_workload_empty with large dataset that becomes empty after dropping NaNs. Args: - tmp_path (pathlib.Path): Temporary directory for test files. + tmp_path (Path): Temporary directory for test files. Returns: None: Asserts function correctly processes large datasets. @@ -6553,7 +6574,8 @@ def test_is_workload_empty_large_dataset_with_nans(tmp_path): assert len(console_error_calls) == 1 error_args = console_error_calls[0][0] - assert "profilingFound empty cells" in error_args[0] + assert "profiling" in error_args[0] + assert "Found empty cells" in error_args[1] def test_is_workload_empty_unicode_content(tmp_path): @@ -6561,7 +6583,7 @@ def test_is_workload_empty_unicode_content(tmp_path): Test is_workload_empty with CSV containing Unicode characters. Args: - tmp_path (pathlib.Path): Temporary directory for test files. + tmp_path (Path): Temporary directory for test files. Returns: None: Asserts function handles Unicode content correctly. @@ -6596,7 +6618,7 @@ def test_is_workload_empty_special_path_characters(tmp_path): Test is_workload_empty with directory paths containing special characters. Args: - tmp_path (pathlib.Path): Temporary directory for test files. + tmp_path (Path): Temporary directory for test files. Returns: None: Asserts function handles special characters in paths. @@ -6629,7 +6651,7 @@ def test_is_workload_empty_csv_read_permission_error(tmp_path): Test is_workload_empty when CSV file exists but cannot be read due to permissions. Args: - tmp_path (pathlib.Path): Temporary directory for test files. + tmp_path (Path): Temporary directory for test files. Returns: None: Asserts function handles file permission errors. @@ -6665,7 +6687,7 @@ def test_is_workload_empty_csv_read_permission_error(tmp_path): def test_is_workload_empty_string_path_input(): """ - Test is_workload_empty with string path input vs pathlib.Path. + Test is_workload_empty with string path input vs Path. Returns: None: Asserts function handles different path input types. @@ -6693,7 +6715,7 @@ def test_is_workload_empty_console_error_string_formatting(tmp_path): Test is_workload_empty string formatting in console_error messages. Args: - tmp_path (pathlib.Path): Temporary directory for test files. + tmp_path (Path): Temporary directory for test files. Returns: None: Asserts console_error messages are properly formatted. @@ -6719,9 +6741,10 @@ def test_is_workload_empty_console_error_string_formatting(tmp_path): assert len(console_error_calls) == 1 error_args = console_error_calls[0][0] expected_path = str(workload_dir / "pmc_perf.csv") - assert expected_path in error_args[0] - assert "profilingFound empty cells" in error_args[0] - assert "Profiling data could be corrupt" in error_args[0] + assert expected_path in error_args[1] + assert "profiling" in error_args[0] + assert "Found empty cells" in error_args[1] + assert "Profiling data could be corrupt" in error_args[1] def test_is_workload_empty_function_return_value(tmp_path): @@ -6729,7 +6752,7 @@ def test_is_workload_empty_function_return_value(tmp_path): Test that is_workload_empty function return behavior (implicitly returns None). Args: - tmp_path (pathlib.Path): Temporary directory for test files. + tmp_path (Path): Temporary directory for test files. Returns: None: Asserts function return value consistency. @@ -6911,13 +6934,12 @@ def test_set_locale_encoding_c_utf8_fails_fallback_also_fails(): utils_mod.set_locale_encoding() - assert len(console_error_calls) == 2 + assert len(console_error_calls) == 1 assert ( - "Failed to set locale to the current UTF-8-based locale." + "Failed to set locale to the current UTF-8-based locale:" in console_error_calls[0][0][0] ) - assert console_error_calls[0][1]["exit"] == False # noqa - assert console_error_calls[1][0][0] == fallback_error + assert "Fallback locale failed" in console_error_calls[0][0][0] def test_set_locale_encoding_no_utf8_locale_available(): @@ -7171,8 +7193,8 @@ def test_set_locale_encoding_different_locale_error_types(): utils_mod.set_locale_encoding() - assert len(console_error_calls) == 2 - assert console_error_calls[1][0][0] == fallback_error + assert len(console_error_calls) == 1 + assert str(fallback_error) in console_error_calls[0][0][0] def test_set_locale_encoding_unusual_locale_names(): @@ -7469,7 +7491,7 @@ def test_set_locale_encoding_comprehensive_error_handling(): locale.Error("Fallback fail"), ], "getdefaultlocale_return": ("en_US", "UTF-8"), - "expected_errors": 2, + "expected_errors": 1, }, { "name": "No UTF-8 locale available", @@ -8669,7 +8691,7 @@ class MockArgs: @mock.patch.dict(os.environ, {"ROCPROF": "rocprofiler-sdk"}, clear=True) @mock.patch("utils.utils.console_error") -@mock.patch("utils.utils.path") +@mock.patch("utils.utils.Path") def test_detect_rocprof_calls_console_error_if_sdk_path_invalid( mock_path_constructor, mock_console_error_func ): @@ -8826,11 +8848,12 @@ def test_v3_to_v2_agent_id_parsing_success_and_error( pass mock_console_error.assert_called_once() - call_args = mock_console_error.call_args[0][0] - assert 'Parsing rocprofv3 csv output: Error of getting "Agent_Id"' in call_args + call_args = mock_console_error.call_args[0] + assert "v3_counter_csv_to_v2_csv" in call_args[0] + assert 'Error getting "Agent_Id"' in call_args[1] assert ( - "AttributeError" in call_args - or "'NoneType' object has no attribute 'group'" in call_args + "AttributeError" in call_args[1] + or "'NoneType' object has no attribute 'group'" in call_args[1] ) @@ -8960,7 +8983,7 @@ def test_pc_sampling_prof_sdk_path_nonexistent_librocprofiler_sdk_tool( sdk_lib_dir = tmp_path / "rocm_sdk" / "lib" sdk_lib_dir.mkdir(parents=True, exist_ok=True) rocprofiler_sdk_library_path = str(sdk_lib_dir / "librocprofiler_sdk.so") - pathlib.Path(rocprofiler_sdk_library_path).touch() + Path(rocprofiler_sdk_library_path).touch() expected_tool_path = str( sdk_lib_dir / "rocprofiler-sdk" / "librocprofiler-sdk-tool.so" @@ -9016,7 +9039,7 @@ def test_pc_sampling_prof_subprocess_fails( sdk_lib_dir = tmp_path / "rocm_sdk_fail" / "lib" sdk_lib_dir.mkdir(parents=True, exist_ok=True) rocprofiler_sdk_library_path_sdk = str(sdk_lib_dir / "librocprofiler_sdk.so") - pathlib.Path(rocprofiler_sdk_library_path_sdk).touch() + Path(rocprofiler_sdk_library_path_sdk).touch() tool_dir = sdk_lib_dir / "rocprofiler-sdk" tool_dir.mkdir(parents=True, exist_ok=True) @@ -9070,7 +9093,7 @@ def test_pc_sampling_prof_empty_appcmd( sdk_lib_dir = tmp_path / "rocm_sdk_empty" / "lib" sdk_lib_dir.mkdir(parents=True, exist_ok=True) rocprofiler_sdk_library_path_sdk = str(sdk_lib_dir / "librocprofiler_sdk.so") - pathlib.Path(rocprofiler_sdk_library_path_sdk).touch() + Path(rocprofiler_sdk_library_path_sdk).touch() tool_dir = sdk_lib_dir / "rocprofiler-sdk" tool_dir.mkdir(parents=True, exist_ok=True) (tool_dir / "librocprofiler-sdk-tool.so").touch() @@ -9097,9 +9120,8 @@ def create_dummy_csv(filepath, data_dict): @mock.patch("utils.utils.console_warning") -@mock.patch("utils.utils.path") def test_replace_timestamps_no_timestamps_csv_returns_early( - mock_path_util, mock_console_warning, tmp_path + mock_console_warning, tmp_path ): """ Edge Case: timestamps.csv does not exist in workload_dir. @@ -9108,25 +9130,17 @@ def test_replace_timestamps_no_timestamps_csv_returns_early( """ workload_dir = str(tmp_path) - mock_timestamps_path_obj = mock.Mock() - mock_timestamps_path_obj.is_file.return_value = False - - mock_path_util.side_effect = lambda *args: ( - mock_timestamps_path_obj if args[1] == "timestamps.csv" else mock.DEFAULT - ) - utils.replace_timestamps(workload_dir) - mock_path_util.assert_any_call(workload_dir, "timestamps.csv") - mock_timestamps_path_obj.is_file.assert_called_once() + # Since there's no timestamps.csv, function should return early + # and console_warning should not be called mock_console_warning.assert_not_called() @mock.patch("utils.utils.console_warning") -@mock.patch("glob.glob") # Mock glob.glob -@mock.patch("utils.utils.path") +@mock.patch("glob.glob") def test_replace_timestamps_timestamps_csv_missing_columns_warns( - mock_path_util, mock_glob, mock_console_warning, tmp_path + mock_glob, mock_console_warning, tmp_path ): """ Edge Case: timestamps.csv exists but is missing @@ -9137,31 +9151,23 @@ def test_replace_timestamps_timestamps_csv_missing_columns_warns( workload_dir = str(tmp_path) timestamps_csv_path_str = os.path.join(workload_dir, "timestamps.csv") + # Create the actual CSV file with missing columns create_dummy_csv(timestamps_csv_path_str, {"Some_Other_Column": [123]}) - mock_timestamps_path_obj = mock.Mock() - mock_timestamps_path_obj.is_file.return_value = True - mock_timestamps_path_obj.name = "timestamps.csv" - - mock_path_util.side_effect = lambda *args, **kwargs: ( - mock_timestamps_path_obj if args[-1] == "timestamps.csv" else mock.DEFAULT - ) - utils.replace_timestamps(workload_dir) - mock_path_util.assert_any_call(workload_dir, "timestamps.csv") - mock_timestamps_path_obj.is_file.assert_called_once() + # Verify console_warning was called mock_console_warning.assert_called_once_with( "Incomplete profiling data detected. Unable to update timestamps.\n" ) + # Verify glob wasn't called (since we return early due to missing columns) mock_glob.assert_not_called() @mock.patch("utils.utils.console_warning") @mock.patch("glob.glob") -@mock.patch("utils.utils.path") def test_replace_timestamps_updates_other_csvs_skips_sysinfo( - mock_path_util, mock_glob, mock_console_warning, tmp_path + mock_glob, mock_console_warning, tmp_path ): """ Edge Case: timestamps.csv is valid. Other CSVs exist, including sysinfo.csv. @@ -9189,26 +9195,7 @@ def test_replace_timestamps_updates_other_csvs_skips_sysinfo( {"Info": ["CPU", "MEM"], "Start_Timestamp": [5, 6], "End_Timestamp": [7, 8]}, ) - def path_side_effect(*args, **kwargs): - p_obj = mock.Mock() - full_path = args[0] if len(args) == 1 else os.path.join(args[0], args[1]) - - if full_path == timestamps_csv_path_str: - p_obj.is_file.return_value = True - p_obj.name = "timestamps.csv" - elif full_path == data_csv_path_str: - p_obj.is_file.return_value = True - p_obj.name = "data.csv" - elif full_path == sysinfo_csv_path_str: - p_obj.is_file.return_value = True - p_obj.name = "sysinfo.csv" - else: - p_obj.is_file.return_value = False - p_obj.name = os.path.basename(full_path) - return p_obj - - mock_path_util.side_effect = path_side_effect - + # Mock glob to return the CSV files we created mock_glob.return_value = [ data_csv_path_str, sysinfo_csv_path_str, @@ -9219,6 +9206,7 @@ def test_replace_timestamps_updates_other_csvs_skips_sysinfo( mock_console_warning.assert_not_called() + # Verify data.csv was updated with new timestamps df_data_updated = pd.read_csv(data_csv_path_str) pd.testing.assert_series_equal( df_data_updated["Start_Timestamp"], @@ -9228,6 +9216,7 @@ def test_replace_timestamps_updates_other_csvs_skips_sysinfo( df_data_updated["End_Timestamp"], pd.Series(new_end_ts, name="End_Timestamp") ) + # Verify sysinfo.csv was NOT updated (timestamps should remain original) df_sysinfo_original = pd.read_csv(sysinfo_csv_path_str) assert list(df_sysinfo_original["Start_Timestamp"]) == [5, 6] assert list(df_sysinfo_original["End_Timestamp"]) == [7, 8] @@ -9235,9 +9224,8 @@ def test_replace_timestamps_updates_other_csvs_skips_sysinfo( @mock.patch("utils.utils.console_warning") @mock.patch("glob.glob") -@mock.patch("utils.utils.path") def test_replace_timestamps_no_other_csvs_to_update( - mock_path_util, mock_glob, mock_console_warning, tmp_path + mock_glob, mock_console_warning, tmp_path ): """ Edge Case: timestamps.csv is valid, but no other *.csv files @@ -9257,27 +9245,14 @@ def test_replace_timestamps_no_other_csvs_to_update( {"Info": ["CPU"], "Start_Timestamp": [5], "End_Timestamp": [7]}, ) - def path_side_effect(*args, **kwargs): - p_obj = mock.Mock() - full_path = args[0] if len(args) == 1 else os.path.join(args[0], args[1]) - if full_path == timestamps_csv_path_str: - p_obj.is_file.return_value = True - p_obj.name = "timestamps.csv" - elif full_path == sysinfo_csv_path_str: - p_obj.is_file.return_value = True - p_obj.name = "sysinfo.csv" - else: - p_obj.is_file.return_value = False - p_obj.name = os.path.basename(full_path) - return p_obj - - mock_path_util.side_effect = path_side_effect - + # Mock glob to return only timestamps.csv and sysinfo.csv mock_glob.return_value = [timestamps_csv_path_str, sysinfo_csv_path_str] utils.replace_timestamps(workload_dir) mock_console_warning.assert_not_called() + + # Verify sysinfo.csv was NOT updated (timestamps should remain original) df_sysinfo_original = pd.read_csv(sysinfo_csv_path_str) assert list(df_sysinfo_original["Start_Timestamp"]) == [5] assert list(df_sysinfo_original["End_Timestamp"]) == [7] diff --git a/projects/rocprofiler-compute/utils/run-ci.py b/projects/rocprofiler-compute/utils/run-ci.py index 8c044faeda..f692fac0ee 100755 --- a/projects/rocprofiler-compute/utils/run-ci.py +++ b/projects/rocprofiler-compute/utils/run-ci.py @@ -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 diff --git a/projects/rocprofiler-compute/utils/split_config.py b/projects/rocprofiler-compute/utils/split_config.py index 89361a875c..c43e45c58d 100644 --- a/projects/rocprofiler-compute/utils/split_config.py +++ b/projects/rocprofiler-compute/utils/split_config.py @@ -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) diff --git a/projects/rocprofiler-compute/utils/update_license.py b/projects/rocprofiler-compute/utils/update_license.py index fcde37dda1..0fe75bd209 100755 --- a/projects/rocprofiler-compute/utils/update_license.py +++ b/projects/rocprofiler-compute/utils/update_license.py @@ -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) diff --git a/projects/rocprofiler-compute/utils/ver_check.py b/projects/rocprofiler-compute/utils/ver_check.py index 2f8d67c93f..0cb4684cb2 100755 --- a/projects/rocprofiler-compute/utils/ver_check.py +++ b/projects/rocprofiler-compute/utils/ver_check.py @@ -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-"):