From 323d06c79c9ed35386eb1350ebe49cf2bec668bf Mon Sep 17 00:00:00 2001 From: vedithal-amd Date: Tue, 26 Aug 2025 14:15:05 -0400 Subject: [PATCH] [rocprofiler-compute] Add database output format to analyze mode (#748) Analysis data dump * Add `--output-format` and `--output-name` option to analyze mode * Remove `--output` and `-save-dfs` option to analyze mode * Add documentation on `rocpd` output format and analysis database file * Create sqlite3 database using object relation mapping (ORM) provided by sqlalchemy library * Fix metrics config to remove metrics marked as `null`, fix `Unit` header, add missing `title` * Add test cases to ensure analysis data dump work --- projects/rocprofiler-compute/CHANGELOG.md | 8 + .../docker/Dockerfile.doctest | 3 +- .../docker/docker-compose.doctest.yml | 6 +- projects/rocprofiler-compute/docs/conf.py | 2 +- .../analyze/analysis_data_dump_schema.png | Bin 0 -> 67666 bytes .../data/analyze/analysis_data_dump_views.png | Bin 0 -> 17433 bytes .../docs/how-to/analyze/cli.rst | 102 +++ .../docs/how-to/analyze/mode.rst | 2 +- .../docs/how-to/profile/mode.rst | 35 + projects/rocprofiler-compute/requirements.txt | 1 + projects/rocprofiler-compute/src/argparser.py | 32 +- .../rocprof_compute_analyze/analysis_base.py | 32 +- .../rocprof_compute_analyze/analysis_db.py | 601 ++++++++++++++++++ .../src/rocprof_compute_base.py | 6 + .../gfx908/0100_system_info.yaml | 1 + ...ssing_unit_and_data_return_path_ta_td.yaml | 5 - .../gfx908/1600_vector_l1_data_cache.yaml | 4 +- .../gfx90a/0100_system_info.yaml | 1 + .../gfx90a/1600_vector_l1_data_cache.yaml | 4 +- .../gfx940/0100_system_info.yaml | 1 + .../gfx940/0300_memory_chart.yaml | 4 - .../gfx940/1600_vector_l1_data_cache.yaml | 4 +- .../gfx941/0100_system_info.yaml | 1 + .../gfx941/0300_memory_chart.yaml | 4 - .../gfx941/1600_vector_l1_data_cache.yaml | 4 +- .../gfx942/0100_system_info.yaml | 1 + .../gfx942/0300_memory_chart.yaml | 4 - .../gfx942/1600_vector_l1_data_cache.yaml | 4 +- .../gfx950/0100_system_info.yaml | 1 + .../gfx950/1600_vector_l1_data_cache.yaml | 4 +- .../rocprof_compute_tui/utils/tui_utils.py | 2 +- .../src/utils/analysis_orm.py | 216 +++++++ .../rocprofiler-compute/src/utils/parser.py | 6 +- .../src/utils/roofline_calc.py | 3 +- projects/rocprofiler-compute/src/utils/tty.py | 30 +- .../rocprofiler-compute/src/utils/utils.py | 5 + .../tests/test_TCP_counters.py | 12 +- .../tests/test_analyze_commands.py | 16 +- .../tests/test_profile_general.py | 26 + .../utils/autogen_hash.yaml | 32 +- .../utils/unified_config.yaml | 22 +- 41 files changed, 1130 insertions(+), 117 deletions(-) create mode 100644 projects/rocprofiler-compute/docs/data/analyze/analysis_data_dump_schema.png create mode 100644 projects/rocprofiler-compute/docs/data/analyze/analysis_data_dump_views.png create mode 100644 projects/rocprofiler-compute/src/rocprof_compute_analyze/analysis_db.py create mode 100644 projects/rocprofiler-compute/src/utils/analysis_orm.py diff --git a/projects/rocprofiler-compute/CHANGELOG.md b/projects/rocprofiler-compute/CHANGELOG.md index 834cc9cc8d..c671e81a87 100644 --- a/projects/rocprofiler-compute/CHANGELOG.md +++ b/projects/rocprofiler-compute/CHANGELOG.md @@ -21,6 +21,8 @@ Full documentation for ROCm Compute Profiler is available at [https://rocm.docs. * Added interactive metric descriptions in TUI analyze mode * users can now left click on any metric cell to view detailed descriptions in the dedicated `METRIC DESCRIPTION` tab +* Add support for analysis report output as a sqlite database using ``--output-format db`` analysis mode option + ### Changed * Add notice for change in default output format to `rocpd` in a future release @@ -100,6 +102,12 @@ Full documentation for ROCm Compute Profiler is available at [https://rocm.docs. * L1I-L2 Bandwidth * sL1D-L2 BW +* Analysis output: + * Replace `-o / --output` analyze mode option with `--output-format` and `--output-name` + * Add ``--output-format`` analysis mode option to select the output format of the analysis report. + * Add ``--output-name`` analysis mode option to override the default file/folder name. + * Replace `--save-dfs` analyze mode option with `--output-format csv` + ### Resolved issues * Fixed not detecting memory clock issue when using amd-smi diff --git a/projects/rocprofiler-compute/docker/Dockerfile.doctest b/projects/rocprofiler-compute/docker/Dockerfile.doctest index 8b68b54337..b071787ac9 100644 --- a/projects/rocprofiler-compute/docker/Dockerfile.doctest +++ b/projects/rocprofiler-compute/docker/Dockerfile.doctest @@ -22,6 +22,7 @@ RUN git config --global --add safe.directory /app # Install any dependencies specified in requirements.txt # Run interactive bash shell CMD ["/bin/bash", "-c", "\ - python3 -m pip install -r docs/sphinx/requirements.txt \ + cd /app/projects/rocprofiler-compute \ + && python3 -m pip install -r docs/sphinx/requirements.txt \ && exec /bin/bash \ "] diff --git a/projects/rocprofiler-compute/docker/docker-compose.doctest.yml b/projects/rocprofiler-compute/docker/docker-compose.doctest.yml index 3ccd1b76a8..3e35543bc1 100644 --- a/projects/rocprofiler-compute/docker/docker-compose.doctest.yml +++ b/projects/rocprofiler-compute/docker/docker-compose.doctest.yml @@ -1,9 +1,9 @@ services: doctest: # service name build: - context: ../ - dockerfile: docker/Dockerfile.doctest + context: ../../../ + dockerfile: projects/rocprofiler-compute/docker/Dockerfile.doctest volumes: - - ../:/app + - ../../../:/app tty: true stdin_open: true diff --git a/projects/rocprofiler-compute/docs/conf.py b/projects/rocprofiler-compute/docs/conf.py index a52b6ed6c7..7a080b692c 100644 --- a/projects/rocprofiler-compute/docs/conf.py +++ b/projects/rocprofiler-compute/docs/conf.py @@ -213,4 +213,4 @@ extlinks = { } # Uncomment if facing rate limit exceed issue with local build -external_projects_remote_repository = "" +# external_projects_remote_repository = "" diff --git a/projects/rocprofiler-compute/docs/data/analyze/analysis_data_dump_schema.png b/projects/rocprofiler-compute/docs/data/analyze/analysis_data_dump_schema.png new file mode 100644 index 0000000000000000000000000000000000000000..7eb4813cf411a9a44f12d3fc84c465bcdca4d217 GIT binary patch literal 67666 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 I%-Hw;1L&jtm;e9( literal 0 HcmV?d00001 diff --git a/projects/rocprofiler-compute/docs/data/analyze/analysis_data_dump_views.png b/projects/rocprofiler-compute/docs/data/analyze/analysis_data_dump_views.png new file mode 100644 index 0000000000000000000000000000000000000000..e7302258f9647a8d3393f3bd3ac0e79c33a6a5f1 GIT binary patch literal 17433 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 literal 0 HcmV?d00001 diff --git a/projects/rocprofiler-compute/docs/how-to/analyze/cli.rst b/projects/rocprofiler-compute/docs/how-to/analyze/cli.rst index 767e5c0365..2c43a61a49 100644 --- a/projects/rocprofiler-compute/docs/how-to/analyze/cli.rst +++ b/projects/rocprofiler-compute/docs/how-to/analyze/cli.rst @@ -318,10 +318,12 @@ Per-kernel roofline analysis When analyzing specific kernels, the roofline analysis provides detailed metrics for each filtered kernel: .. code-block:: shell-session + $ rocprof-compute analyze -p workloads/vcopy/MI200/ -k 0 -b 4 This generates enhanced roofline output showing per-kernel performance rates and arithmetic intensity calculations: .. code-block:: text + ================================================================================ 4. Roofline ================================================================================ @@ -372,6 +374,7 @@ Per-kernel roofline analysis Analyze multiple kernels for comparison: .. code-block:: shell-session + $ rocprof-compute analyze -p workloads/vcopy/MI200/ -k 0 1 2 -b 4 Baseline comparison @@ -384,3 +387,102 @@ Baseline comparison .. code-block:: shell rocprof-compute analyze -p workload1/path/ -k 0 -p workload2/path/ -k 1 + +Analysis output format +====================== + +Use the ``--output-format `` analyze mode option to specify the output format of the +analysis report. Supported formats are ``stdout``, ``txt``, ``csv``, and ``db``. The default output +format is ``stdout``. + +* ``stdout`` format: + * Print analysis report to the terminal. + * NOTE: This option will not generate any file or folder. + +* ``txt`` format: + * Generate a file named ``rocprof_compute_.txt`` in the current working directory. + * This file contains the entire analysis report as printed on the terminal. + * This is useful in case of searching across long analysis reports. + * NOTE: This option will disable output of analysis report to terminal. + +* ``csv`` format: + * Generate a folder named ``rocprof_compute_`` in the current working directory. + * This folder contains multiple csv files representing the data in each metric table in the analysis report. + * This is useful for further programmatic analysis of analysis reports. + * NOTE: This will print the analysis report to the terminal as well. + +* ``db`` format: + * NOTE: This only works when provided workload paths are created using ``--format-rocprof-output rocpd`` profile mode option. + * Generate a file named ``rocprof_compute_.db`` in the current working directory. + * This is a SQLite database file containing all the data in the analysis report structured according to :ref:`analysis database schema `. + * This is useful for further programmatic analysis of analysis reports. + * NOTE: This option will disable output of analysis report to terminal. + +Default file/folder name ``rocprofiler_compute_`` can be overriden using ``--output-name `` analyze mode option. + +.. _analysis-database: + +Analysis database schema +======================== + +Analysis database tables + +.. image:: ../../data/analyze/analysis_data_dump_schema.png + :align: center + :alt: Analysis database tables + +Analysis database views + +.. image:: ../../data/analyze/analysis_data_dump_views.png + :align: center + :alt: Analysis database views + +Analysis database example + +.. note:: + + Some metrics cannot be calculated when corresponding counters are missing as shown in the warnings below + +.. note:: + + It is possible to merge the analysis data dump for multiple workload folders (resulting from multiple profiles) by repeating ``-p`` option for each workload + +.. code-block:: shell-session + + $ rocprof-compute analyze --verbose --db test -p workloads/vmem/MI300X_A1 -p workloads/vmem1/MI300X_A1 + DEBUG Execution mode = analyze + + __ _ + _ __ ___ ___ _ __ _ __ ___ / _| ___ ___ _ __ ___ _ __ _ _| |_ ___ + | '__/ _ \ / __| '_ \| '__/ _ \| |_ _____ / __/ _ \| '_ ` _ \| '_ \| | | | __/ _ \ + | | | (_) | (__| |_) | | | (_) | _|_____| (_| (_) | | | | | | |_) | |_| | || __/ + |_| \___/ \___| .__/|_| \___/|_| \___\___/|_| |_| |_| .__/ \__,_|\__\___| + |_| |_| + + INFO Analysis mode = db + DEBUG [omnisoc init] + DEBUG [omnisoc init] + DEBUG [analysis] prepping to do some analysis + INFO [analysis] deriving rocprofiler-compute metrics... + WARNING Roofline ceilings not found for /app/projects/rocprofiler-compute/workloads/vmem/MI300X_A1. + WARNING Roofline ceilings not found for /app/projects/rocprofiler-compute/workloads/vmem1/MI300X_A1. + WARNING PC sampling data not found for /app/projects/rocprofiler-compute/workloads/vmem/MI300X_A1. + WARNING PC sampling data not found for /app/projects/rocprofiler-compute/workloads/vmem1/MI300X_A1. + DEBUG Collected dispatch data + DEBUG Applied analysis mode filters + DEBUG Calculated dispatch data + DEBUG Collected metrics data + WARNING Failed to evaluate expression for 3.1.25 - Value: to_round(to_avg( + (pmc_df.get("TCP_TCP_LATENCY_sum") / pmc_df.get("TCP_TA_TCP_STATE_READ_sum")).where((pmc_df.get("TCP_TA_TCP_STATE_READ_sum") != 0), None)), 0) - unsupported operand type(s) for /: 'NoneType' and 'float' + WARNING Failed to evaluate expression for 3.1.39 - Value: to_round((to_avg( + (pmc_df.get("pmc_perf_ACCUM") / pmc_df.get("SQC_ICACHE_REQ")).where((pmc_df.get("SQC_ICACHE_REQ") != 0), None)) * 100), 0) - unsupported operand type(s) for /: 'NoneType' and 'float' + WARNING Failed to evaluate expression for 3.1.25 - Value: to_round(to_avg( + (pmc_df.get("TCP_TCP_LATENCY_sum") / pmc_df.get("TCP_TA_TCP_STATE_READ_sum")).where((pmc_df.get("TCP_TA_TCP_STATE_READ_sum") != 0), None)), 0) - unsupported operand type(s) for /: 'NoneType' and 'float' + WARNING Failed to evaluate expression for 3.1.39 - Value: to_round((to_avg( + (pmc_df.get("pmc_perf_ACCUM") / pmc_df.get("SQC_ICACHE_REQ")).where((pmc_df.get("SQC_ICACHE_REQ") != 0), None)) * 100), 0) - unsupported operand type(s) for /: 'NoneType' and 'float' + DEBUG Calculated metric values + DEBUG Calculated roofline data points + DEBUG [analysis] generating analysis + DEBUG SQLite database initialized with name: test.db + DEBUG Initialized database: test.db + DEBUG Completed writing database \ No newline at end of file diff --git a/projects/rocprofiler-compute/docs/how-to/analyze/mode.rst b/projects/rocprofiler-compute/docs/how-to/analyze/mode.rst index 6ca7f4a773..dc8dc11d06 100644 --- a/projects/rocprofiler-compute/docs/how-to/analyze/mode.rst +++ b/projects/rocprofiler-compute/docs/how-to/analyze/mode.rst @@ -22,7 +22,7 @@ options. * :doc:`cli` * :doc:`grafana-gui` * :doc:`standalone-gui` -* :doc:`text-based user interface (TUI)` +* :doc:`tui` .. note:: diff --git a/projects/rocprofiler-compute/docs/how-to/profile/mode.rst b/projects/rocprofiler-compute/docs/how-to/profile/mode.rst index 74640f6b45..e2d2758934 100644 --- a/projects/rocprofiler-compute/docs/how-to/profile/mode.rst +++ b/projects/rocprofiler-compute/docs/how-to/profile/mode.rst @@ -24,6 +24,15 @@ Profiling with ROCm Compute Profiler yields the following benefits. * :ref:`Automate counter collection `: ROCm Compute Profiler handles all of your profiling via pre-configured input files. +* :ref:`Profiling output format `: ROCm Compute Profile can adjust the + output format of underlying rocprof tool which changes the output format of raw performance + counter data in the workload folder created during profiling. Supported output formats are + ``json``, ``csv``, and ``rocpd``. The default output format is ``csv``. + +.. note:: + + The default output format will be changed to ``rocpd`` in a future release of ROCm Compute Profiler. + * :ref:`Filtering `: Apply runtime filters to speed up the profiling process. @@ -217,6 +226,32 @@ an Instinct MI210 vs an Instinct MI250. -rw-r--r-- 1 auser agroup 650 Mar 1 15:15 sysinfo.csv -rw-r--r-- 1 auser agroup 399 Mar 1 15:15 timestamps.csv +.. _profiling-output-format: + +Profiling output format +----------------------- + +Use the ``--format-rocprof-output `` profile mode option to specify the output format +of the underlying ``rocprof`` tool. The following formats are supported: + +* ``csv`` format: + * Ask underlying rocprof tool to dump raw performance counter data in csv format. + * The generated csv files across multiple runs of rocprof are processed and dumped into the workload directory as csv files. + * Multiple csv files are merged into single pmc_perf.csv file in workload directory. + +* ``json`` format: + * Ask underlying rocprof tool to dump raw performance counter data in json format. + * The generated json files across multiple runs of rocprof are processed and dumped into the workload directory as csv files. + * Multiple csv files are merged into single pmc_perf.csv file in workload directory. + +* ``rocpd`` format: + * Ask underlying rocprof tool to dump raw performance counter data in rocpd format. + * Multiple ``rocpd`` database files containding counter collection data are merged into a single csv under the workload folder. + The database files are then removed. + * Use ``--retain-rocpd-output`` profile mode option to preserve the ``rocpd`` database(s) in the workload folder. + This is useful for custom analysis of profiling data. + + .. _filtering: Filtering diff --git a/projects/rocprofiler-compute/requirements.txt b/projects/rocprofiler-compute/requirements.txt index ef0de785a2..464e6c3f54 100644 --- a/projects/rocprofiler-compute/requirements.txt +++ b/projects/rocprofiler-compute/requirements.txt @@ -12,6 +12,7 @@ plotille pymongo pyyaml setuptools +sqlalchemy>=2.0.42 tabulate textual textual_plotext diff --git a/projects/rocprofiler-compute/src/argparser.py b/projects/rocprofiler-compute/src/argparser.py index d54bc0e3e5..51446eae49 100644 --- a/projects/rocprofiler-compute/src/argparser.py +++ b/projects/rocprofiler-compute/src/argparser.py @@ -633,11 +633,29 @@ Examples: help="\t\tMode of spatial multiplexing.", ) analyze_group.add_argument( - "-o", - "--output", + "--output-format", metavar="", - dest="output_file", - help="\t\tSpecify an output file to save analysis results.", + dest="output_format", + choices=["stdout", "txt", "csv", "db"], + default="stdout", + help=( + "\t\tSet the format of output file or folder containing analysis data.\n" + "\t\tBy default, file or folder created will " + "have the name rocprof_compute_.\n" + "\t\tFile or folder name can be overriden using --output-name.\n" + "\t\tDefault output format is stdout which will not " + "generate any file/folder.\n" + ), + ) + analyze_group.add_argument( + "--output-name", + metavar="", + dest="output_name", + help=( + "\t\tOverride the default output file name rocprof_compue_ " + "with the specified name.\n" + "\t\tThis is only applicable when --output-format txt/csv/db is used.\n" + ), ) analyze_group.add_argument( "--gui", @@ -756,12 +774,6 @@ Examples: help="\t\tSpecify the directory of customized configs.", default=rocprof_compute_home.joinpath("rocprof_compute_soc/analysis_configs/"), ) - analyze_advanced_group.add_argument( - "--save-dfs", - dest="df_file_dir", - metavar="", - help="\t\tSpecify the dirctory to save analysis dataframe csv files.", - ) analyze_advanced_group.add_argument( "--cols", type=int, 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 654808bea1..67d64de66f 100644 --- a/projects/rocprofiler-compute/src/rocprof_compute_analyze/analysis_base.py +++ b/projects/rocprofiler-compute/src/rocprof_compute_analyze/analysis_base.py @@ -24,6 +24,7 @@ ############################################################################## import copy +import re import sys import textwrap from abc import abstractmethod @@ -41,7 +42,7 @@ from utils.logger import ( console_warning, demarcate, ) -from utils.utils import is_workload_empty, merge_counters_spatial_multiplex +from utils.utils import get_uuid, is_workload_empty, merge_counters_spatial_multiplex class OmniAnalyze_Base: @@ -284,6 +285,23 @@ class OmniAnalyze_Base: 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." + ) + # ---------------------------------------------------- # Required methods to be implemented by child classes # ---------------------------------------------------- @@ -293,11 +311,13 @@ class OmniAnalyze_Base: console_debug("analysis", "prepping to do some analysis") console_log("analysis", "deriving rocprofiler-compute metrics...") # initalize output file - self._output = ( - open(self.__args.output_file, "w+") - if self.__args.output_file - else sys.stdout - ) + if self.__args.output_format == "txt": + output_filename = self.__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": + self._output = sys.stdout # Read profiling config self._profiling_config = file_io.load_profiling_config(self.__args.path[0][0]) diff --git a/projects/rocprofiler-compute/src/rocprof_compute_analyze/analysis_db.py b/projects/rocprofiler-compute/src/rocprof_compute_analyze/analysis_db.py new file mode 100644 index 0000000000..76cc7da7b4 --- /dev/null +++ b/projects/rocprofiler-compute/src/rocprof_compute_analyze/analysis_db.py @@ -0,0 +1,601 @@ +##############################################################################bl +# MIT License +# +# Copyright (c) 2025 Advanced Micro Devices, Inc. All Rights Reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +##############################################################################el + +import ast +import json +import re +from pathlib import Path + +import astunparse +import pandas as pd + +import utils.analysis_orm as orm +from config import rocprof_compute_home +from rocprof_compute_analyze.analysis_base import OmniAnalyze_Base +from utils import rocpd_data +from utils.analysis_orm import Database, get_views +from utils.logger import console_debug, console_error, console_warning, demarcate +from utils.parser import ( + PC_SAMPLING_NOT_ISSUE_PREFIX, + CodeTransformer, + build_in_vars, + to_avg, + to_concat, + to_int, + to_max, + to_median, + to_min, + to_mod, + to_quantile, + to_round, + to_std, + to_sum, +) +from utils.roofline_calc import ( + CACHE_HIERARCHY, + MFMA_DATATYPES, + PEAK_OPS_DATATYPES, + SUPPORTED_DATATYPES, +) +from utils.utils import get_uuid, get_version + + +class db_analysis(OmniAnalyze_Base): + # ----------------------- + # Required child methods + # ----------------------- + @demarcate + def pre_processing(self): + """Perform any pre-processing steps prior to analysis.""" + super().pre_processing() + if self._profiling_config.get("format_rocprof_output") != "rocpd": + console_error( + "Creation of analysis database is only supported " + "for profiling data with rocpd output format." + ) + self._roofline_ceilings_per_workload = self.calc_roofline_ceilings() + self._pc_sampling_data_per_workload = self.calc_pc_sampling_data() + self._pmc_df_per_workload = { + workload_path: rocpd_data.process_rocpd_csv( + pd.read_csv(Path(workload_path) / "pmc_perf.csv") + ) + for workload_path in self._runs.keys() + } + self._top_kernels_per_workload = { + workload_path: pmc_df.assign( + duration=pmc_df["End_Timestamp"] - pmc_df["Start_Timestamp"] + ) + .sort_values(by="duration", ascending=False) + .drop_duplicates("Kernel_Name")["Kernel_Name"] + .to_list() + for workload_path, pmc_df in self._pmc_df_per_workload.items() + } + console_debug("Collected dispatch data") + self._pmc_df_per_workload = self.apply_pmc_filters() + self._dispatch_data_per_workload = self.calc_dispatch_data() + self._metrics_info_data_per_workload, self._values_data_per_workload = ( + self.calc_metrics_data() + ) + self._values_data_per_workload = self.calc_expressions() + self._roofline_data_per_workload = self.calc_roofline_data() + + @demarcate + def run_analysis(self): + """Run CLI analysis.""" + super().run_analysis() + + # Initialize analysis database + # Create db uuid + if self.get_args().output_name: + db_name = f"{self.get_args().output_name}.db" + else: + db_name = f"rocprof_compute_{get_uuid()}.db" + Database.init(db_name) + console_debug(f"Initialized database: {db_name}") + + for workload_path in self._runs.keys(): + workload_obj = orm.Workload( + name=workload_path.split("/")[-2], + sub_name=workload_path.split("/")[-1], + sys_info_extdata=self._runs[workload_path].sys_info.iloc[0].to_dict(), + roofline_bench_extdata=self._roofline_ceilings_per_workload.get( + workload_path + ), + profiling_config_extdata=self._profiling_config, + ) + Database.get_session().add(workload_obj) + for pc_sample in self._pc_sampling_data_per_workload.get( + workload_path, pd.DataFrame() + ).itertuples(): + Database.get_session().add( + orm.PCsampling( + source=pc_sample.source_line, + instruction=pc_sample.instruction, + count=pc_sample.count, + kernel_name=pc_sample.kernel_name, + offset=pc_sample.offset, + count_issue=pc_sample.count_issued, + count_stall=pc_sample.count_stalled, + stall_reason=pc_sample.stall_reason, + workload=workload_obj, + ) + ) + for dispatch in self._dispatch_data_per_workload.get( + workload_path, pd.DataFrame() + ).itertuples(): + Database.get_session().add( + orm.Dispatch( + dispatch_id=dispatch.dispatch_id, + kernel_name=dispatch.kernel_name, + gpu_id=dispatch.gpu_id, + duration=dispatch.duration, + workload=workload_obj, + ) + ) + for metric in self._metrics_info_data_per_workload.get( + workload_path, pd.DataFrame() + ).itertuples(): + metric_obj = orm.Metric( + name=metric.name, + metric_id=metric.metric_id, + description=metric.description, + unit=metric.unit, + table_name=metric.table_name, + sub_table_name=metric.sub_table_name, + workload=workload_obj, + ) + Database.get_session().add(metric_obj) + for value in self._values_data_per_workload.get( + workload_path, pd.DataFrame() + ).itertuples(): + if value.metric_id == metric.metric_id: + Database.get_session().add( + orm.Value( + metric=metric_obj, + value_name=value.value_name, + value=value.value, + ) + ) + + for roofline_data in self._roofline_data_per_workload.get( + workload_path, pd.DataFrame() + ).itertuples(): + Database.get_session().add( + orm.RooflineData( + kernel_name=roofline_data.kernel_name, + total_flops=roofline_data.total_flops, + l1_cache_data=roofline_data.l1_cache_data, + l2_cache_data=roofline_data.l2_cache_data, + hbm_cache_data=roofline_data.hbm_cache_data, + workload=workload_obj, + ) + ) + + version = get_version(rocprof_compute_home) + Database.get_session().add( + orm.Metadata( + compute_version=version["version"], + git_version=version["sha"], + schema_version=orm.SCHEMA_VERSION, + ) + ) + + # Create views + for view_stmt in get_views(): + Database.get_session().execute(view_stmt) + + # Write database + Database.write() + console_debug("Completed writing database") + console_warning(f"Created file: {db_name}") + + def calc_roofline_ceilings(self): + roofline_ceilings_per_workload = dict() + + for workload_path in self._runs.keys(): + if not (Path(workload_path) / "roofline.csv").exists(): + console_warning(f"Roofline ceilings not found for {workload_path}.") + continue + + roofline_dict = ( + pd.read_csv(f"{workload_path}/roofline.csv").iloc[0].to_dict() + ) + keys = list() + for mem_level in CACHE_HIERARCHY: + keys.append(f"{mem_level}Bw") + for dtype in SUPPORTED_DATATYPES[ + self._runs[workload_path].sys_info.iloc[0]["gpu_arch"] + ]: + if dtype in PEAK_OPS_DATATYPES: + if dtype.startswith("F") or dtype.startswith("B"): + keys.append(f"{dtype}Flops") + elif dtype.startswith("I"): + keys.append(f"{dtype}Ops") + if dtype in MFMA_DATATYPES: + if dtype.startswith("F") or dtype.startswith("B"): + # FP16 -> F16 + dtype = dtype.replace("FP", "F") + keys.append(f"MFMA{dtype}Flops") + elif dtype.startswith("I"): + keys.append(f"MFMA{dtype}Ops") + roofline_ceilings_per_workload[workload_path] = { + key: roofline_dict[key] for key in keys if key in roofline_dict + } + + if roofline_ceilings_per_workload: + console_debug("Collected roofline ceilings") + return roofline_ceilings_per_workload + + def calc_pc_sampling_data(self): + pc_sampling_data_per_workload = dict() + + for workload_path in self._runs.keys(): + if not (Path(workload_path) / "ps_file_results.json").exists(): + console_warning(f"PC sampling data not found for {workload_path}.") + continue + + pc_sampling_data = json.loads( + (Path(workload_path) / "ps_file_results.json").read_text() + ) + pc_sampling_data = pc_sampling_data["rocprofiler-sdk-tool"][0] + pc_sampling_stochastic = pc_sampling_data["buffer_records"][ + "pc_sample_stochastic" + ] + pc_sampling_host_trap = pc_sampling_data["buffer_records"][ + "pc_sample_host_trap" + ] + pc_sampling_instruction = pc_sampling_data["strings"][ + "pc_sample_instructions" + ] + pc_sampling_comments = pc_sampling_data["strings"]["pc_sample_comments"] + pc_sampling_kernel_name_dict = { + symbol["code_object_id"]: symbol["formatted_kernel_name"] + for symbol in pc_sampling_data["kernel_symbols"] + } + + pc_df = pd.DataFrame([ + { + "inst_index": pc_sample["inst_index"], + "code_object_id": pc_sample["record"]["pc"]["code_object_id"], + "code_object_offset": pc_sample["record"]["pc"][ + "code_object_offset" + ], + "stall_reason": pc_sample["record"] + .get("snapshot", {}) + .get("stall_reason"), + "wave_issued": pc_sample["record"].get("wave_issued"), + } + for pc_sample in pc_sampling_stochastic + pc_sampling_host_trap + ]) + + def custom_aggregator(column_name): + if column_name == "count_issued": + + def aggregator(series): + return None if series.isnull().all() else series.sum() + + return aggregator + if column_name == "count_stalled": + + def aggregator(series): + if series.isnull().all(): + return None + return series.count() - series.sum() + + return aggregator + if column_name == "stall_reason": + + def aggregator(series): + if series.isnull().all(): + return None + cleaned_series = series.dropna().str[ + len(PC_SAMPLING_NOT_ISSUE_PREFIX) : + ] + return cleaned_series.value_counts().to_dict() + + return aggregator + raise ValueError(f"Unknown column name: {column_name}") + + grouped_df = ( + pc_df.groupby(["code_object_id", "code_object_offset"]) + .agg( + count=("code_object_id", "size"), + inst_index=("inst_index", "last"), + count_issued=("wave_issued", custom_aggregator("count_issued")), + count_stalled=("wave_issued", custom_aggregator("count_stalled")), + stall_reason=("stall_reason", custom_aggregator("stall_reason")), + ) + .reset_index() + ) + + grouped_df["instruction"] = grouped_df["inst_index"].apply( + lambda x: pc_sampling_instruction[x] + if x < len(pc_sampling_instruction) + else None + ) + grouped_df["source_line"] = grouped_df["inst_index"].apply( + lambda x: pc_sampling_comments[x] + if x < len(pc_sampling_comments) + else None + ) + grouped_df["kernel_name"] = grouped_df["code_object_id"].apply( + lambda x: pc_sampling_kernel_name_dict.get(x) + ) + grouped_df = grouped_df.rename(columns={"code_object_offset": "offset"}) + grouped_df = grouped_df.drop(columns=["code_object_id", "inst_index"]) + + pc_sampling_data_per_workload[workload_path] = grouped_df + + if pc_sampling_data_per_workload: + console_debug("Collected PC sampling data") + return pc_sampling_data_per_workload + + @staticmethod + def evaluate(name, value, pmc_df, sys_info, parse=False): + if parse: + value = re.sub( + r"\$([0-9A-Za-z_]+)", + lambda m: f'sys_info["{m.group(1)}"]', + value, + ) + ast_node = ast.parse(value) + transformer = CodeTransformer() + transformer.visit(ast_node) + value = astunparse.unparse(ast_node) + value = value.replace("raw_pmc_df", "pmc_df") + value = value.replace("pmc_df['sys_info']", "sys_info") + else: + value = value.replace("raw_pmc_df['pmc_perf']", "pmc_df") + value = re.sub( + "ammolite__([0-9A-Za-z_]+)", + lambda m: f'sys_info["{m.group(1)}"]', + value, + ) + try: + return eval( + compile(value, "", "eval"), + {}, # no globals + { + # only locals + "pmc_df": pmc_df, + "sys_info": sys_info, + "to_avg": to_avg, + "to_concat": to_concat, + "to_int": to_int, + "to_max": to_max, + "to_median": to_median, + "to_min": to_min, + "to_mod": to_mod, + "to_quantile": to_quantile, + "to_round": to_round, + "to_std": to_std, + "to_sum": to_sum, + }, + ) + except Exception as e: + console_warning(f"Failed to evaluate expression for {name}: {value} - {e}") + return None + + def calc_expressions(self): + values_data_per_workload = self._values_data_per_workload.copy() + + for workload_path in self._runs.keys(): + pmc_df = self._pmc_df_per_workload[workload_path].copy() + sys_info = self._runs[workload_path].sys_info.iloc[0].to_dict() + for key, value in self._roofline_ceilings_per_workload[ + workload_path + ].items(): + sys_info[f"{key}_empirical_peak"] = value + + # Calculate PER_XCD variables first + 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(): + if "PER_XCD" not in key: + sys_info[key] = db_analysis.evaluate( + key, value, pmc_df, sys_info, parse=True + ) + + # Get name and print warning + values_data_per_workload[workload_path]["value"] = values_data_per_workload[ + workload_path + ].apply( + lambda row: db_analysis.evaluate( + f"{row['metric_id']} - {row['value_name']}", + row["value"], + pmc_df, + sys_info, + ), + axis=1, + ) + + console_debug("Calculated metric values") + return values_data_per_workload + + def calc_metrics_data(self): + metrics_info_data_per_workload = dict() + values_data_per_workload = dict() + + for workload_path in self._runs.keys(): + gfx_arch = self._runs[workload_path].sys_info.iloc[0]["gpu_arch"] + # for example 201 -> Wavefront + table_names_map = dict() + for panel_config in self._arch_configs[gfx_arch].panel_configs.values(): + table_names_map[panel_config["id"]] = panel_config["title"] + for source in panel_config["data source"]: + table_names_map[list(source.values())[0]["id"]] = list( + source.values() + )[0]["title"] + # Build metric data + non_expression_columns = [ + "Metric", + "Channel", + "Unit", + "Description", + "coll_level", + "Type", + "Xfer", + "Coherency", + "Transaction", + ] + metrics_info_df = pd.DataFrame([ + { + "name": row.get("Metric") or row["Channel"].strip(), + "metric_id": metric_id, + "description": row.get("Description"), + "unit": row.get("Unit"), + "table_name": table_names_map[int(metric_id.split(".")[0]) * 100], + "sub_table_name": table_names_map[ + int(metric_id.split(".")[0]) * 100 + + int(metric_id.split(".")[1]) + ], + } + for metric_df_id, metric_df in self._arch_configs[gfx_arch].dfs.items() + if metric_df_id + != 402 # Skip roofline data points handled in calc_roofline_data + if set(metric_df.columns).intersection({"Metric", "Channel"}) + for metric_id, row in metric_df.iterrows() + ]) + values_df = pd.DataFrame([ + { + "metric_id": metric_id, + "value_name": value_name, + "value": row[value_name].strip(), + } + for metric_df_id, metric_df in self._arch_configs[gfx_arch].dfs.items() + if metric_df_id + != 402 # Skip roofline data points handled in calc_roofline_data + if set(metric_df.columns).intersection({"Metric", "Channel"}) + for metric_id, row in metric_df.iterrows() + for value_name in metric_df.drop( + columns=non_expression_columns, errors="ignore" + ).columns + ]) + + metrics_info_data_per_workload[workload_path] = metrics_info_df + values_data_per_workload[workload_path] = values_df + + console_debug("Collected metrics data") + return metrics_info_data_per_workload, values_data_per_workload + + def calc_dispatch_data(self): + dispatch_data_per_workload = dict() + + for workload_path in self._runs.keys(): + dispatch_df = pd.DataFrame([ + { + "dispatch_id": row.Dispatch_ID, + "kernel_name": row.Kernel_Name, + "gpu_id": row.GPU_ID, + "duration": row.End_Timestamp - row.Start_Timestamp, + } + for row in self._pmc_df_per_workload[workload_path].itertuples() + ]) + dispatch_data_per_workload[workload_path] = dispatch_df + + console_debug("Calculated dispatch data") + return dispatch_data_per_workload + + def apply_pmc_filters(self): + pmc_df_per_workload = self._pmc_df_per_workload.copy() + + for workload_path, pmc_df in pmc_df_per_workload.items(): + # Filter gpu_ids + if self._runs[workload_path].filter_gpu_ids: + pmc_df = pmc_df.loc[ + pmc_df["GPU_ID"] + .astype(str) + .isin([self._runs[workload_path].filter_gpu_ids]) + ] + # Filter kernel_ids + if self._runs[workload_path].filter_kernel_ids: + pmc_df = pmc_df.loc[ + pmc_df["Kernel_Name"].isin([ + self._top_kernels_per_workload[workload_path][id] + for id in self._runs[workload_path].filter_kernel_ids + ]) + ] + # Filter dispatch_ids + if self._runs[workload_path].filter_dispatch_ids: + if ">" in self._runs[workload_path].filter_dispatch_ids[0]: + m = re.match( + r"\> (\d+)", self._runs[workload_path].filter_dispatch_ids[0] + ) + pmc_df = pmc_df[pmc_df["Dispatch_ID"] > int(m.group(1))] + else: + pmc_df = pmc_df.loc[ + pmc_df["Dispatch_ID"] + .astype(str) + .isin(self._runs[workload_path].filter_dispatch_ids) + ] + pmc_df_per_workload[workload_path] = pmc_df + + console_debug("Applied analysis mode filters") + return pmc_df_per_workload + + def calc_roofline_data(self): + roofline_data_per_workload = dict() + + for workload_path in self._runs.keys(): + pmc_df = self._pmc_df_per_workload[workload_path].copy() + sys_info = self._runs[workload_path].sys_info.iloc[0].to_dict() + gfx_arch = sys_info["gpu_arch"] + roofline_data_df = self._arch_configs[gfx_arch].dfs[402] + roofline_data_expressions = dict( + zip(roofline_data_df["Metric"], roofline_data_df["Value"]) + ) + roofline_data_expressions = { + "total_flops": roofline_data_expressions["Performance (GFLOPs)"], + "l1_cache_data": roofline_data_expressions["AI L1"], + "l2_cache_data": roofline_data_expressions["AI L2"], + "hbm_cache_data": roofline_data_expressions["AI HBM"], + } + + roofline_df = pd.DataFrame([ + { + "kernel_name": kernel_name, + **{ + metric_name: db_analysis.evaluate( + metric_name, + roofline_data_expressions[metric_name], + pmc_df[pmc_df["Kernel_Name"] == kernel_name], + sys_info, + ) + for metric_name in roofline_data_expressions + }, + } + for kernel_name in self._top_kernels_per_workload[workload_path][ + : self.get_args().max_stat_num + ] + ]) + + roofline_data_per_workload[workload_path] = roofline_df + + console_debug("Calculated roofline data points") + return roofline_data_per_workload diff --git a/projects/rocprofiler-compute/src/rocprof_compute_base.py b/projects/rocprofiler-compute/src/rocprof_compute_base.py index 3faa023134..33877c6a2a 100644 --- a/projects/rocprofiler-compute/src/rocprof_compute_base.py +++ b/projects/rocprofiler-compute/src/rocprof_compute_base.py @@ -137,6 +137,8 @@ class RocProfCompute: self.__analyze_mode = "web_ui" elif self.__args.tui: self.__analyze_mode = "tui" + elif self.__args.output_format == "db": + self.__analyze_mode = "db" else: self.__analyze_mode = "cli" return @@ -447,6 +449,10 @@ class RocProfCompute: run_tui(self.__args, self.__supported_archs) return + elif self.__analyze_mode == "db": + from rocprof_compute_analyze.analysis_db import db_analysis + + analyzer = db_analysis(self.__args, self.__supported_archs) else: console_error("Unsupported analysis mode -> %s" % self.__analyze_mode) diff --git a/projects/rocprofiler-compute/src/rocprof_compute_soc/analysis_configs/gfx908/0100_system_info.yaml b/projects/rocprofiler-compute/src/rocprof_compute_soc/analysis_configs/gfx908/0100_system_info.yaml index 8470ffbbe3..23d024fde3 100644 --- a/projects/rocprofiler-compute/src/rocprof_compute_soc/analysis_configs/gfx908/0100_system_info.yaml +++ b/projects/rocprofiler-compute/src/rocprof_compute_soc/analysis_configs/gfx908/0100_system_info.yaml @@ -6,5 +6,6 @@ Panel Config: data source: - raw_csv_table: id: 101 + title: System Info source: sysinfo.csv columnwise: true diff --git a/projects/rocprofiler-compute/src/rocprof_compute_soc/analysis_configs/gfx908/1500_address_processing_unit_and_data_return_path_ta_td.yaml b/projects/rocprofiler-compute/src/rocprof_compute_soc/analysis_configs/gfx908/1500_address_processing_unit_and_data_return_path_ta_td.yaml index 67c3aa1dfc..4c615fb0d5 100644 --- a/projects/rocprofiler-compute/src/rocprof_compute_soc/analysis_configs/gfx908/1500_address_processing_unit_and_data_return_path_ta_td.yaml +++ b/projects/rocprofiler-compute/src/rocprof_compute_soc/analysis_configs/gfx908/1500_address_processing_unit_and_data_return_path_ta_td.yaml @@ -206,11 +206,6 @@ Panel Config: min: MIN(((100 * TD_TC_STALL_sum) / ($GRBM_GUI_ACTIVE_PER_XCD * $cu_per_gpu))) max: MAX(((100 * TD_TC_STALL_sum) / ($GRBM_GUI_ACTIVE_PER_XCD * $cu_per_gpu))) unit: pct - "Workgroup manager \u2192 Data-Return Stall": - avg: null - min: null - max: null - unit: pct Coalescable Instructions: avg: AVG((TD_COALESCABLE_WAVEFRONT_sum / $denom)) min: MIN((TD_COALESCABLE_WAVEFRONT_sum / $denom)) diff --git a/projects/rocprofiler-compute/src/rocprof_compute_soc/analysis_configs/gfx908/1600_vector_l1_data_cache.yaml b/projects/rocprofiler-compute/src/rocprof_compute_soc/analysis_configs/gfx908/1600_vector_l1_data_cache.yaml index 50af33c21b..b374ea9466 100644 --- a/projects/rocprofiler-compute/src/rocprof_compute_soc/analysis_configs/gfx908/1600_vector_l1_data_cache.yaml +++ b/projects/rocprofiler-compute/src/rocprof_compute_soc/analysis_configs/gfx908/1600_vector_l1_data_cache.yaml @@ -400,7 +400,7 @@ Panel Config: avg: Avg min: Min max: Max - units: Units + units: Unit metric: Req: avg: AVG((TCP_UTCL1_REQUEST_sum / $denom)) @@ -438,5 +438,5 @@ Panel Config: avg: Avg min: Min max: Max - units: Units + units: Unit metric: {} diff --git a/projects/rocprofiler-compute/src/rocprof_compute_soc/analysis_configs/gfx90a/0100_system_info.yaml b/projects/rocprofiler-compute/src/rocprof_compute_soc/analysis_configs/gfx90a/0100_system_info.yaml index 8470ffbbe3..23d024fde3 100644 --- a/projects/rocprofiler-compute/src/rocprof_compute_soc/analysis_configs/gfx90a/0100_system_info.yaml +++ b/projects/rocprofiler-compute/src/rocprof_compute_soc/analysis_configs/gfx90a/0100_system_info.yaml @@ -6,5 +6,6 @@ Panel Config: data source: - raw_csv_table: id: 101 + title: System Info source: sysinfo.csv columnwise: true diff --git a/projects/rocprofiler-compute/src/rocprof_compute_soc/analysis_configs/gfx90a/1600_vector_l1_data_cache.yaml b/projects/rocprofiler-compute/src/rocprof_compute_soc/analysis_configs/gfx90a/1600_vector_l1_data_cache.yaml index 50af33c21b..b374ea9466 100644 --- a/projects/rocprofiler-compute/src/rocprof_compute_soc/analysis_configs/gfx90a/1600_vector_l1_data_cache.yaml +++ b/projects/rocprofiler-compute/src/rocprof_compute_soc/analysis_configs/gfx90a/1600_vector_l1_data_cache.yaml @@ -400,7 +400,7 @@ Panel Config: avg: Avg min: Min max: Max - units: Units + units: Unit metric: Req: avg: AVG((TCP_UTCL1_REQUEST_sum / $denom)) @@ -438,5 +438,5 @@ Panel Config: avg: Avg min: Min max: Max - units: Units + units: Unit metric: {} diff --git a/projects/rocprofiler-compute/src/rocprof_compute_soc/analysis_configs/gfx940/0100_system_info.yaml b/projects/rocprofiler-compute/src/rocprof_compute_soc/analysis_configs/gfx940/0100_system_info.yaml index 8470ffbbe3..23d024fde3 100644 --- a/projects/rocprofiler-compute/src/rocprof_compute_soc/analysis_configs/gfx940/0100_system_info.yaml +++ b/projects/rocprofiler-compute/src/rocprof_compute_soc/analysis_configs/gfx940/0100_system_info.yaml @@ -6,5 +6,6 @@ Panel Config: data source: - raw_csv_table: id: 101 + title: System Info source: sysinfo.csv columnwise: true diff --git a/projects/rocprofiler-compute/src/rocprof_compute_soc/analysis_configs/gfx940/0300_memory_chart.yaml b/projects/rocprofiler-compute/src/rocprof_compute_soc/analysis_configs/gfx940/0300_memory_chart.yaml index 0a6510182a..03b5606ad7 100644 --- a/projects/rocprofiler-compute/src/rocprof_compute_soc/analysis_configs/gfx940/0300_memory_chart.yaml +++ b/projects/rocprofiler-compute/src/rocprof_compute_soc/analysis_configs/gfx940/0300_memory_chart.yaml @@ -236,10 +236,6 @@ Panel Config: L2 Hit: value: ROUND(AVG((((100 * TCC_HIT_sum) / (TCC_HIT_sum + TCC_MISS_sum)) if ((TCC_HIT_sum + TCC_MISS_sum) != 0) else 0)), 0) - L2 Rd Lat: - value: null - L2 Wr Lat: - value: null Fabric_L2 Rd: value: ROUND(AVG((TCC_EA0_RDREQ_sum / $denom)), 0) Fabric_L2 Wr: diff --git a/projects/rocprofiler-compute/src/rocprof_compute_soc/analysis_configs/gfx940/1600_vector_l1_data_cache.yaml b/projects/rocprofiler-compute/src/rocprof_compute_soc/analysis_configs/gfx940/1600_vector_l1_data_cache.yaml index db745209b7..e5b5eb9e9c 100644 --- a/projects/rocprofiler-compute/src/rocprof_compute_soc/analysis_configs/gfx940/1600_vector_l1_data_cache.yaml +++ b/projects/rocprofiler-compute/src/rocprof_compute_soc/analysis_configs/gfx940/1600_vector_l1_data_cache.yaml @@ -370,7 +370,7 @@ Panel Config: avg: Avg min: Min max: Max - units: Units + units: Unit metric: Req: avg: AVG((TCP_UTCL1_REQUEST_sum / $denom)) @@ -408,5 +408,5 @@ Panel Config: avg: Avg min: Min max: Max - units: Units + units: Unit metric: {} diff --git a/projects/rocprofiler-compute/src/rocprof_compute_soc/analysis_configs/gfx941/0100_system_info.yaml b/projects/rocprofiler-compute/src/rocprof_compute_soc/analysis_configs/gfx941/0100_system_info.yaml index 8470ffbbe3..23d024fde3 100644 --- a/projects/rocprofiler-compute/src/rocprof_compute_soc/analysis_configs/gfx941/0100_system_info.yaml +++ b/projects/rocprofiler-compute/src/rocprof_compute_soc/analysis_configs/gfx941/0100_system_info.yaml @@ -6,5 +6,6 @@ Panel Config: data source: - raw_csv_table: id: 101 + title: System Info source: sysinfo.csv columnwise: true diff --git a/projects/rocprofiler-compute/src/rocprof_compute_soc/analysis_configs/gfx941/0300_memory_chart.yaml b/projects/rocprofiler-compute/src/rocprof_compute_soc/analysis_configs/gfx941/0300_memory_chart.yaml index 0a6510182a..03b5606ad7 100644 --- a/projects/rocprofiler-compute/src/rocprof_compute_soc/analysis_configs/gfx941/0300_memory_chart.yaml +++ b/projects/rocprofiler-compute/src/rocprof_compute_soc/analysis_configs/gfx941/0300_memory_chart.yaml @@ -236,10 +236,6 @@ Panel Config: L2 Hit: value: ROUND(AVG((((100 * TCC_HIT_sum) / (TCC_HIT_sum + TCC_MISS_sum)) if ((TCC_HIT_sum + TCC_MISS_sum) != 0) else 0)), 0) - L2 Rd Lat: - value: null - L2 Wr Lat: - value: null Fabric_L2 Rd: value: ROUND(AVG((TCC_EA0_RDREQ_sum / $denom)), 0) Fabric_L2 Wr: diff --git a/projects/rocprofiler-compute/src/rocprof_compute_soc/analysis_configs/gfx941/1600_vector_l1_data_cache.yaml b/projects/rocprofiler-compute/src/rocprof_compute_soc/analysis_configs/gfx941/1600_vector_l1_data_cache.yaml index db745209b7..e5b5eb9e9c 100644 --- a/projects/rocprofiler-compute/src/rocprof_compute_soc/analysis_configs/gfx941/1600_vector_l1_data_cache.yaml +++ b/projects/rocprofiler-compute/src/rocprof_compute_soc/analysis_configs/gfx941/1600_vector_l1_data_cache.yaml @@ -370,7 +370,7 @@ Panel Config: avg: Avg min: Min max: Max - units: Units + units: Unit metric: Req: avg: AVG((TCP_UTCL1_REQUEST_sum / $denom)) @@ -408,5 +408,5 @@ Panel Config: avg: Avg min: Min max: Max - units: Units + units: Unit metric: {} diff --git a/projects/rocprofiler-compute/src/rocprof_compute_soc/analysis_configs/gfx942/0100_system_info.yaml b/projects/rocprofiler-compute/src/rocprof_compute_soc/analysis_configs/gfx942/0100_system_info.yaml index 8470ffbbe3..23d024fde3 100644 --- a/projects/rocprofiler-compute/src/rocprof_compute_soc/analysis_configs/gfx942/0100_system_info.yaml +++ b/projects/rocprofiler-compute/src/rocprof_compute_soc/analysis_configs/gfx942/0100_system_info.yaml @@ -6,5 +6,6 @@ Panel Config: data source: - raw_csv_table: id: 101 + title: System Info source: sysinfo.csv columnwise: true diff --git a/projects/rocprofiler-compute/src/rocprof_compute_soc/analysis_configs/gfx942/0300_memory_chart.yaml b/projects/rocprofiler-compute/src/rocprof_compute_soc/analysis_configs/gfx942/0300_memory_chart.yaml index 0a6510182a..03b5606ad7 100644 --- a/projects/rocprofiler-compute/src/rocprof_compute_soc/analysis_configs/gfx942/0300_memory_chart.yaml +++ b/projects/rocprofiler-compute/src/rocprof_compute_soc/analysis_configs/gfx942/0300_memory_chart.yaml @@ -236,10 +236,6 @@ Panel Config: L2 Hit: value: ROUND(AVG((((100 * TCC_HIT_sum) / (TCC_HIT_sum + TCC_MISS_sum)) if ((TCC_HIT_sum + TCC_MISS_sum) != 0) else 0)), 0) - L2 Rd Lat: - value: null - L2 Wr Lat: - value: null Fabric_L2 Rd: value: ROUND(AVG((TCC_EA0_RDREQ_sum / $denom)), 0) Fabric_L2 Wr: diff --git a/projects/rocprofiler-compute/src/rocprof_compute_soc/analysis_configs/gfx942/1600_vector_l1_data_cache.yaml b/projects/rocprofiler-compute/src/rocprof_compute_soc/analysis_configs/gfx942/1600_vector_l1_data_cache.yaml index db745209b7..e5b5eb9e9c 100644 --- a/projects/rocprofiler-compute/src/rocprof_compute_soc/analysis_configs/gfx942/1600_vector_l1_data_cache.yaml +++ b/projects/rocprofiler-compute/src/rocprof_compute_soc/analysis_configs/gfx942/1600_vector_l1_data_cache.yaml @@ -370,7 +370,7 @@ Panel Config: avg: Avg min: Min max: Max - units: Units + units: Unit metric: Req: avg: AVG((TCP_UTCL1_REQUEST_sum / $denom)) @@ -408,5 +408,5 @@ Panel Config: avg: Avg min: Min max: Max - units: Units + units: Unit metric: {} diff --git a/projects/rocprofiler-compute/src/rocprof_compute_soc/analysis_configs/gfx950/0100_system_info.yaml b/projects/rocprofiler-compute/src/rocprof_compute_soc/analysis_configs/gfx950/0100_system_info.yaml index 8470ffbbe3..23d024fde3 100644 --- a/projects/rocprofiler-compute/src/rocprof_compute_soc/analysis_configs/gfx950/0100_system_info.yaml +++ b/projects/rocprofiler-compute/src/rocprof_compute_soc/analysis_configs/gfx950/0100_system_info.yaml @@ -6,5 +6,6 @@ Panel Config: data source: - raw_csv_table: id: 101 + title: System Info source: sysinfo.csv columnwise: true diff --git a/projects/rocprofiler-compute/src/rocprof_compute_soc/analysis_configs/gfx950/1600_vector_l1_data_cache.yaml b/projects/rocprofiler-compute/src/rocprof_compute_soc/analysis_configs/gfx950/1600_vector_l1_data_cache.yaml index f95e3fcb1f..2d8ac4d781 100644 --- a/projects/rocprofiler-compute/src/rocprof_compute_soc/analysis_configs/gfx950/1600_vector_l1_data_cache.yaml +++ b/projects/rocprofiler-compute/src/rocprof_compute_soc/analysis_configs/gfx950/1600_vector_l1_data_cache.yaml @@ -420,7 +420,7 @@ Panel Config: avg: Avg min: Min max: Max - units: Units + units: Unit metric: Req: avg: AVG((TCP_UTCL1_REQUEST_sum / $denom)) @@ -468,7 +468,7 @@ Panel Config: avg: Avg min: Min max: Max - units: Units + units: Unit metric: Cache Full Stall: avg: AVG((TCP_UTCL1_STALL_INFLIGHT_MAX_sum / $denom)) 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 698778fda0..629421b793 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 @@ -109,7 +109,7 @@ def process_panels_to_dataframes(args, kernel_df, archConfigs, roof_plot=None): # args.filter_metrics # args.cols # args.max_stat_num - # args.df_file_dir + # dfs file dir result_structure = {} decimal_precision = getattr(args, "decimal", 2) if args else 2 diff --git a/projects/rocprofiler-compute/src/utils/analysis_orm.py b/projects/rocprofiler-compute/src/utils/analysis_orm.py new file mode 100644 index 0000000000..23647d8133 --- /dev/null +++ b/projects/rocprofiler-compute/src/utils/analysis_orm.py @@ -0,0 +1,216 @@ +##############################################################################bl +# MIT License +# +# Copyright (c) 2025 Advanced Micro Devices, Inc. All Rights Reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +##############################################################################el + +from sqlalchemy import ( + JSON, + Column, + Float, + ForeignKey, + Integer, + String, + Text, + create_engine, + func, + select, + text, +) +from sqlalchemy.orm import declarative_base, relationship, sessionmaker + +from utils.logger import console_debug, console_error + +Base = declarative_base() + +PREFIX = "compute_" +SCHEMA_VERSION = "1.0.0" + + +class Workload(Base): + __tablename__ = f"{PREFIX}workload" + + workload_id = Column(Integer, primary_key=True) + name = Column(String) + sub_name = Column(String) + sys_info_extdata = Column(JSON) + roofline_bench_extdata = Column(JSON) + profiling_config_extdata = Column(JSON) + + # Workload can have multiple dispatches + dispatches = relationship("Dispatch", back_populates="workload") + # Workload can have multiple metrics + metrics = relationship("Metric", back_populates="workload") + # Workload can have multiple roofline data points + roofline_data_points = relationship("RooflineData", back_populates="workload") + # Workload can have multiple pc_sampling values + pc_sampling_values = relationship("PCsampling", back_populates="workload") + + +class Metric(Base): + __tablename__ = f"{PREFIX}metric" + + metric_uuid = Column(Integer, primary_key=True) + workload_id = Column( + Integer, ForeignKey(f"{PREFIX}workload.workload_id"), nullable=False + ) + name = Column(String) # e.g. Wavefronts Num + metric_id = Column(String) # e.g. 4.1.3 + description = Column(Text) # e.g. Number of wavefronts + table_name = Column(String) # e.g. Wavefront + sub_table_name = Column(String) # e.g. Wavefront stats + unit = Column(String) # e.g. Gbps + + # Metric can have one workload + workload = relationship("Workload", back_populates="metrics") + # Metric can have multiple values + values = relationship("Value", back_populates="metric") + + +class RooflineData(Base): + __tablename__ = f"{PREFIX}roofline_data" + + roofline_uuid = Column(Integer, primary_key=True) + workload_id = Column( + Integer, ForeignKey(f"{PREFIX}workload.workload_id"), nullable=False + ) + kernel_name = Column(String) + total_flops = Column(Float) + l1_cache_data = Column(Float) + l2_cache_data = Column(Float) + hbm_cache_data = Column(Float) + + # Roofline data point can have one workload + workload = relationship("Workload", back_populates="roofline_data_points") + + +class Dispatch(Base): + __tablename__ = f"{PREFIX}dispatch" + + dispatch_uuid = Column(Integer, primary_key=True) + workload_id = Column( + Integer, ForeignKey(f"{PREFIX}workload.workload_id"), nullable=False + ) + dispatch_id = Column(Integer) + kernel_name = Column(String) + gpu_id = Column(Integer) + duration = Column(Integer) + + # Dispatch can have one workload + workload = relationship("Workload", back_populates="dispatches") + + +class PCsampling(Base): + __tablename__ = f"{PREFIX}pcsampling" + + pc_sampling_uuid = Column(Integer, primary_key=True) + workload_id = Column( + Integer, ForeignKey(f"{PREFIX}workload.workload_id"), nullable=False + ) + source = Column(String) + instruction = Column(String) + count = Column(Integer) + kernel_name = Column(String) + offset = Column(Integer) + count_issue = Column(Integer) + count_stall = Column(Integer) + stall_reason = Column(JSON) + + # PCsampling can have one workload + workload = relationship("Workload", back_populates="pc_sampling_values") + + +class Value(Base): + __tablename__ = f"{PREFIX}value" + + value_uuid = Column(Integer, primary_key=True) + metric_uuid = Column( + Integer, ForeignKey(f"{PREFIX}metric.metric_uuid"), nullable=False + ) + value_name = Column(String) # e.g. min, max, avg + value = Column(Float) # e.g. 123.45 + + # Value can have one metric + metric = relationship("Metric", back_populates="values") + + +class Metadata(Base): + __tablename__ = f"{PREFIX}metadata" + + id = Column(Integer, primary_key=True) + compute_version = Column(String) + git_version = Column(String) + schema_version = Column(String) + + +class Database: + _session = None + + @classmethod + def init(cls, db_name): + engine = create_engine(f"sqlite:///{db_name}") + Base.metadata.create_all(engine) + cls._session = sessionmaker(bind=engine)() + console_debug(f"SQLite database initialized with name: {db_name}") + return db_name + + @classmethod + def get_session(cls): + return cls._session + + @classmethod + def write(self): + try: + self._session.commit() + except Exception as e: + self._session.rollback() + console_error(f"Error writing analysis database: {e}") + finally: + self._session.close() + + +def get_views(): + views = { + "kernel_view": select( + Dispatch.kernel_name, + func.count(Dispatch.dispatch_id).label("dispatch_count"), + func.sum(Dispatch.duration).label("duration_sum"), + func.avg(Dispatch.duration).label("duration_mean"), + ).group_by(Dispatch.kernel_name), + "metric_view": select( + Metric.workload_id, + Metric.name, + Metric.metric_id, + Metric.description, + Metric.table_name, + Metric.sub_table_name, + Metric.unit, + Value.value_name, + Value.value, + ).join(Value, Metric.metric_uuid == Value.metric_uuid), + } + return [ + text( + f"CREATE VIEW {PREFIX}{view_name} AS " + f"{stmt.compile(compile_kwargs={'literal_binds': True})}" + ) + for view_name, stmt in views.items() + ] diff --git a/projects/rocprofiler-compute/src/utils/parser.py b/projects/rocprofiler-compute/src/utils/parser.py index e3658b43b1..192d5db774 100755 --- a/projects/rocprofiler-compute/src/utils/parser.py +++ b/projects/rocprofiler-compute/src/utils/parser.py @@ -114,6 +114,8 @@ supported_call = { "CONCAT": "to_concat", } +PC_SAMPLING_NOT_ISSUE_PREFIX = "ROCPROFILER_PC_SAMPLING_INSTRUCTION_NOT_ISSUED_REASON_" + # ------------------------------------------------------------------------------ @@ -1283,9 +1285,7 @@ def search_pc_sampling_record(records): ) ) - rocp_inst_not_issued_prefix_len = len( - "ROCPROFILER_PC_SAMPLING_INSTRUCTION_NOT_ISSUED_REASON_" - ) + rocp_inst_not_issued_prefix_len = len(PC_SAMPLING_NOT_ISSUE_PREFIX) # Populate grouped_data for i, item in enumerate(records): diff --git a/projects/rocprofiler-compute/src/utils/roofline_calc.py b/projects/rocprofiler-compute/src/utils/roofline_calc.py index 7e05a8efc6..0c69976cef 100644 --- a/projects/rocprofiler-compute/src/utils/roofline_calc.py +++ b/projects/rocprofiler-compute/src/utils/roofline_calc.py @@ -104,6 +104,7 @@ SUPPORTED_DATATYPES = { PEAK_OPS_DATATYPES = ["FP8", "FP16", "BF16", "FP32", "FP64", "I8", "I32", "I64"] MFMA_DATATYPES = ["FP4", "FP6", "FP8", "FP16", "BF16", "FP32", "FP64", "I8"] +CACHE_HIERARCHY = ["HBM", "L2", "L1", "LDS"] TOP_N = 10 @@ -164,7 +165,7 @@ def calc_ceilings(roofline_parameters, dtype, benchmark_data): graphPoints = {"hbm": [], "l2": [], "l1": [], "lds": [], "valu": [], "mfma": []} if roofline_parameters["mem_level"] == "ALL": - cacheHierarchy = ["HBM", "L2", "L1", "LDS"] + cacheHierarchy = CACHE_HIERARCHY else: cacheHierarchy = roofline_parameters["mem_level"] diff --git a/projects/rocprofiler-compute/src/utils/tty.py b/projects/rocprofiler-compute/src/utils/tty.py index f4eb9ac318..1cec9de08b 100644 --- a/projects/rocprofiler-compute/src/utils/tty.py +++ b/projects/rocprofiler-compute/src/utils/tty.py @@ -34,7 +34,7 @@ import config from utils import mem_chart, parser 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 +from utils.utils import convert_metric_id_to_panel_info, get_uuid def string_multiple_lines(source, width, max_rows): @@ -141,6 +141,14 @@ def show_all(args, runs, archConfigs, output, profiling_config, roof_plot=None): else: hidden_cols = config.HIDDEN_COLUMNS_CLI + if args.output_format == "csv": + if args.output_name: + csv_dir = Path(f"{args.output_name}") + else: + csv_dir = Path(f"rocprof_compute_{get_uuid()}") + if not csv_dir.exists(): + csv_dir.mkdir() + for panel_id, panel in archConfigs.panel_configs.items(): # Skip panels that don't support baseline comparison if len(args.path) > 1 and panel_id in config.HIDDEN_SECTIONS: @@ -484,17 +492,15 @@ def show_all(args, runs, archConfigs, output, profiling_config, roof_plot=None): ): ss += table_id_str + " " + table_config["title"] + "\n" - if args.df_file_dir: - p = Path(args.df_file_dir) - if not p.exists(): - p.mkdir() - if p.is_dir(): - if "title" in table_config and table_config["title"]: - table_id_str += "_" + table_config["title"] - df.to_csv( - p.joinpath(table_id_str.replace(" ", "_") + ".csv"), - index=False, - ) + 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 ( diff --git a/projects/rocprofiler-compute/src/utils/utils.py b/projects/rocprofiler-compute/src/utils/utils.py index d2d8c41df0..176d89b079 100644 --- a/projects/rocprofiler-compute/src/utils/utils.py +++ b/projects/rocprofiler-compute/src/utils/utils.py @@ -36,6 +36,7 @@ import shutil import subprocess import tempfile import time +import uuid from pathlib import Path as path from typing import Optional @@ -1640,3 +1641,7 @@ def parse_sets_yaml(arch): if set_option: sets_info[set_option] = set_item return sets_info + + +def get_uuid(length=8): + return uuid.uuid4().hex[:length] diff --git a/projects/rocprofiler-compute/tests/test_TCP_counters.py b/projects/rocprofiler-compute/tests/test_TCP_counters.py index 6c6bb20f99..48012a60d9 100644 --- a/projects/rocprofiler-compute/tests/test_TCP_counters.py +++ b/projects/rocprofiler-compute/tests/test_TCP_counters.py @@ -147,7 +147,8 @@ def test_L1_cache_counters( base = Path(test_utils.get_output_dir()) for app_name in app_names: - workload_dir = str(base / app_name) + workload_dir = f"{base}/{app_name}" + workload_dir_output = f"{base}_{app_name}" # 1. profile the app return_code = binary_handler_profile_rocprof_compute( @@ -167,15 +168,17 @@ def test_L1_cache_counters( workload_dir, "-b", "16.3", - "--save-dfs", - workload_dir, + "--output-format", + "csv", + "--output-name", + workload_dir_output, ]) assert return_code == 0 # 3. save results in local # FIXME: customize file name to avoid hardcode - csv_path = workload_dir + "/16.3_vL1D_cache_access_metrics.csv" + csv_path = workload_dir_output + "/16.3_vL1D_cache_access_metrics.csv" data = load_metrics(csv_path) for metric in metrics: @@ -185,6 +188,7 @@ def test_L1_cache_counters( # 4. clean local output test_utils.clean_output_dir(config["cleanup"], workload_dir) + test_utils.clean_output_dir(config["cleanup"], workload_dir_output) test_utils.clean_output_dir(config["cleanup"], base) # 5. check results are expected diff --git a/projects/rocprofiler-compute/tests/test_analyze_commands.py b/projects/rocprofiler-compute/tests/test_analyze_commands.py index 6d9532411f..e0e63868fb 100644 --- a/projects/rocprofiler-compute/tests/test_analyze_commands.py +++ b/projects/rocprofiler-compute/tests/test_analyze_commands.py @@ -25,6 +25,7 @@ import os import shutil +from pathlib import Path from unittest.mock import Mock import pandas as pd @@ -608,14 +609,16 @@ def test_decimal_3(binary_handler_analyze_rocprof_compute): @pytest.mark.misc def test_save_dfs(binary_handler_analyze_rocprof_compute): - output_path = "tests/workloads/vcopy/saved_analysis" + output_path = test_utils.get_output_dir() for dir in indirs: workload_dir = test_utils.setup_workload_dir(dir) code = binary_handler_analyze_rocprof_compute([ "analyze", "--path", workload_dir, - "--save-dfs", + "--output-format", + "csv", + "--output-name", output_path, ]) assert code == 0 @@ -627,6 +630,7 @@ def test_save_dfs(binary_handler_analyze_rocprof_compute): shutil.rmtree(output_path) test_utils.clean_output_dir(config["cleanup"], workload_dir) + test_utils.clean_output_dir(config["cleanup"], output_path) @pytest.mark.col @@ -860,7 +864,6 @@ def test_dependency_MI100(binary_handler_analyze_rocprof_compute): def test_parser_utility_functions(): """Test parser utility functions edge cases""" import sys - from pathlib import Path sys.path.insert(0, str(Path(__file__).parent.parent / "src")) @@ -969,7 +972,6 @@ def test_parser_utility_functions(): def test_parser_error_handling(): """Test parser error handling paths""" import sys - from pathlib import Path sys.path.insert(0, str(Path(__file__).parent.parent / "src")) @@ -1009,7 +1011,6 @@ def test_missing_file_handling(binary_handler_analyze_rocprof_compute): def test_ast_transformer_edge_cases(): """Simplified test focusing on the actual code paths""" import sys - from pathlib import Path sys.path.insert(0, str(Path(__file__).parent.parent / "src")) @@ -1051,7 +1052,6 @@ def test_ast_transformer_edge_cases(): def test_analyze_with_debug_mode(binary_handler_analyze_rocprof_compute): """Test analyze to cover debug paths in eval_metric - using direct function call""" import sys - from pathlib import Path sys.path.insert(0, str(Path(__file__).parent.parent / "src")) @@ -1138,7 +1138,6 @@ def test_filter_combinations_coverage(binary_handler_analyze_rocprof_compute): def test_apply_filters_direct(): """Test apply_filters function directly to cover filter branches""" import sys - from pathlib import Path sys.path.insert(0, str(Path(__file__).parent.parent / "src")) @@ -1213,7 +1212,6 @@ def test_missing_files_scenarios(binary_handler_analyze_rocprof_compute): def test_pc_sampling_basic_coverage(): """Test PC sampling functions with minimal data""" import sys - from pathlib import Path sys.path.insert(0, str(Path(__file__).parent.parent / "src")) @@ -1245,7 +1243,6 @@ def test_pc_sampling_basic_coverage(): def test_build_dfs_edge_cases(): """Test build_dfs and gen_counter_list with various configurations""" import sys - from pathlib import Path sys.path.insert(0, str(Path(__file__).parent.parent / "src")) @@ -1275,7 +1272,6 @@ def test_build_dfs_edge_cases(): def test_update_functions_coverage(): """Test update_denom_string and update_normUnit_string branches""" import sys - from pathlib import Path sys.path.insert(0, str(Path(__file__).parent.parent / "src")) diff --git a/projects/rocprofiler-compute/tests/test_profile_general.py b/projects/rocprofiler-compute/tests/test_profile_general.py index d5c9f900da..a06aefb6a1 100644 --- a/projects/rocprofiler-compute/tests/test_profile_general.py +++ b/projects/rocprofiler-compute/tests/test_profile_general.py @@ -766,6 +766,32 @@ def test_roof_rocpd(binary_handler_profile_rocprof_compute): test_utils.clean_output_dir(config["cleanup"], workload_dir) +@pytest.mark.misc +def test_analyze_rocpd( + binary_handler_profile_rocprof_compute, binary_handler_analyze_rocprof_compute +): + workload_dir = test_utils.get_output_dir() + options = ["--device", "0", "--format-rocprof-output", "rocpd"] + binary_handler_profile_rocprof_compute(config, workload_dir, options, roof=True) + + db_name = "test" + code = binary_handler_analyze_rocprof_compute([ + "analyze", + "--output-format", + "db", + "--output-name", + f"{db_name}", + "--path", + workload_dir, + ]) + assert code == 0 + assert os.path.isfile(f"{db_name}.db") + + # Remove test.db + os.remove(f"{db_name}.db") + test_utils.clean_output_dir(config["cleanup"], workload_dir) + + @pytest.mark.misc def test_roofline_workload_dir_not_set_error(): """ diff --git a/projects/rocprofiler-compute/utils/autogen_hash.yaml b/projects/rocprofiler-compute/utils/autogen_hash.yaml index 6de2ea2fad..7079981108 100644 --- a/projects/rocprofiler-compute/utils/autogen_hash.yaml +++ b/projects/rocprofiler-compute/utils/autogen_hash.yaml @@ -5,12 +5,12 @@ src/rocprof_compute_soc/analysis_configs/gfx940/0000_top_stats.yaml: 401770cff80 src/rocprof_compute_soc/analysis_configs/gfx941/0000_top_stats.yaml: 401770cff804c6e51b78dff61390d8b5977598a2b09c6601ac593653e912535b src/rocprof_compute_soc/analysis_configs/gfx942/0000_top_stats.yaml: 401770cff804c6e51b78dff61390d8b5977598a2b09c6601ac593653e912535b src/rocprof_compute_soc/analysis_configs/gfx950/0000_top_stats.yaml: 401770cff804c6e51b78dff61390d8b5977598a2b09c6601ac593653e912535b -src/rocprof_compute_soc/analysis_configs/gfx908/0100_system_info.yaml: 739e39e69056984c277a69c17a6866effa860f56e8b1d3ea5d625582f16228ef -src/rocprof_compute_soc/analysis_configs/gfx90a/0100_system_info.yaml: 739e39e69056984c277a69c17a6866effa860f56e8b1d3ea5d625582f16228ef -src/rocprof_compute_soc/analysis_configs/gfx940/0100_system_info.yaml: 739e39e69056984c277a69c17a6866effa860f56e8b1d3ea5d625582f16228ef -src/rocprof_compute_soc/analysis_configs/gfx941/0100_system_info.yaml: 739e39e69056984c277a69c17a6866effa860f56e8b1d3ea5d625582f16228ef -src/rocprof_compute_soc/analysis_configs/gfx942/0100_system_info.yaml: 739e39e69056984c277a69c17a6866effa860f56e8b1d3ea5d625582f16228ef -src/rocprof_compute_soc/analysis_configs/gfx950/0100_system_info.yaml: 739e39e69056984c277a69c17a6866effa860f56e8b1d3ea5d625582f16228ef +src/rocprof_compute_soc/analysis_configs/gfx908/0100_system_info.yaml: b883dc360890c8d4fae49542b3362fa341598b86198cc7f2b9b9a3cf987f9576 +src/rocprof_compute_soc/analysis_configs/gfx90a/0100_system_info.yaml: b883dc360890c8d4fae49542b3362fa341598b86198cc7f2b9b9a3cf987f9576 +src/rocprof_compute_soc/analysis_configs/gfx940/0100_system_info.yaml: b883dc360890c8d4fae49542b3362fa341598b86198cc7f2b9b9a3cf987f9576 +src/rocprof_compute_soc/analysis_configs/gfx941/0100_system_info.yaml: b883dc360890c8d4fae49542b3362fa341598b86198cc7f2b9b9a3cf987f9576 +src/rocprof_compute_soc/analysis_configs/gfx942/0100_system_info.yaml: b883dc360890c8d4fae49542b3362fa341598b86198cc7f2b9b9a3cf987f9576 +src/rocprof_compute_soc/analysis_configs/gfx950/0100_system_info.yaml: b883dc360890c8d4fae49542b3362fa341598b86198cc7f2b9b9a3cf987f9576 src/rocprof_compute_soc/analysis_configs/gfx908/0200_system_speed_of_light.yaml: 2103e9d6123f473f1cb18b71c046f197b5d1d873563c4aad4933d7361255f0c1 src/rocprof_compute_soc/analysis_configs/gfx90a/0200_system_speed_of_light.yaml: e9f552ee72849dc9c4ab14fee77ecc2681f4bcf610a8649c55365ab7eea7aafc src/rocprof_compute_soc/analysis_configs/gfx940/0200_system_speed_of_light.yaml: 70716745e727d3a7e6fa706d34c346f796c241c485516da52e0c694386b3cf57 @@ -19,9 +19,9 @@ src/rocprof_compute_soc/analysis_configs/gfx942/0200_system_speed_of_light.yaml: src/rocprof_compute_soc/analysis_configs/gfx950/0200_system_speed_of_light.yaml: a2cb003c74c0a75b9fe690da4e21b46e78fdb2f3233fc4753bca9276e93d60b0 src/rocprof_compute_soc/analysis_configs/gfx908/0300_memory_chart.yaml: 190c31ddc0bc713dba8b508faf13f0630b268ed15a0d9206f30998a0a071136f src/rocprof_compute_soc/analysis_configs/gfx90a/0300_memory_chart.yaml: 8eeb4bb544eebd59aa10b51c1149ee4d015c76073c9a35e673210d9740fbf808 -src/rocprof_compute_soc/analysis_configs/gfx940/0300_memory_chart.yaml: cff5509ac8502bad6dbd75e3058159fe429aece5d93279c66b2a6a8c887b43b6 -src/rocprof_compute_soc/analysis_configs/gfx941/0300_memory_chart.yaml: cff5509ac8502bad6dbd75e3058159fe429aece5d93279c66b2a6a8c887b43b6 -src/rocprof_compute_soc/analysis_configs/gfx942/0300_memory_chart.yaml: cff5509ac8502bad6dbd75e3058159fe429aece5d93279c66b2a6a8c887b43b6 +src/rocprof_compute_soc/analysis_configs/gfx940/0300_memory_chart.yaml: 249e9ae0445de0927827ec14d20f946a07d50d92fd56e1993bbe0c17eb65bd51 +src/rocprof_compute_soc/analysis_configs/gfx941/0300_memory_chart.yaml: 249e9ae0445de0927827ec14d20f946a07d50d92fd56e1993bbe0c17eb65bd51 +src/rocprof_compute_soc/analysis_configs/gfx942/0300_memory_chart.yaml: 249e9ae0445de0927827ec14d20f946a07d50d92fd56e1993bbe0c17eb65bd51 src/rocprof_compute_soc/analysis_configs/gfx950/0300_memory_chart.yaml: 643b31ffa43bc3613d6f90b0c23d95093d0d0aa5bc8e72d9a0fbc1b739a08b67 src/rocprof_compute_soc/analysis_configs/gfx908/0400_roofline.yaml: 6406ce67cd55064f0d2db2a3511c6536cc1625314ddb31366900fbf3c60ed523 src/rocprof_compute_soc/analysis_configs/gfx90a/0400_roofline.yaml: 100d555cf9e70b892e22f92ddd9c0a5d1f914d07077c4a8d35941e8ad62b5b30 @@ -77,18 +77,18 @@ src/rocprof_compute_soc/analysis_configs/gfx940/1400_scalar_l1_data_cache.yaml: src/rocprof_compute_soc/analysis_configs/gfx941/1400_scalar_l1_data_cache.yaml: 29fac4ea38e4a018baffc4a27a720b47078fd890c10da307655d40f693e6f0e7 src/rocprof_compute_soc/analysis_configs/gfx942/1400_scalar_l1_data_cache.yaml: 29fac4ea38e4a018baffc4a27a720b47078fd890c10da307655d40f693e6f0e7 src/rocprof_compute_soc/analysis_configs/gfx950/1400_scalar_l1_data_cache.yaml: 29fac4ea38e4a018baffc4a27a720b47078fd890c10da307655d40f693e6f0e7 -src/rocprof_compute_soc/analysis_configs/gfx908/1500_address_processing_unit_and_data_return_path_ta_td.yaml: 633d59aba82b3a495b7ba33fa4b2ae4da638b58632bcc37ff18be87af68ce4d4 +src/rocprof_compute_soc/analysis_configs/gfx908/1500_address_processing_unit_and_data_return_path_ta_td.yaml: 1e7717fcbd3c8cdf87d593a33f350ca240c1db8f8065a778cca926da1f517088 src/rocprof_compute_soc/analysis_configs/gfx90a/1500_address_processing_unit_and_data_return_path_ta_td.yaml: 2bdb9d7b3bea1057b3baee29ba3b428b211808261063a97bc4b6b319f4a19fb3 src/rocprof_compute_soc/analysis_configs/gfx940/1500_address_processing_unit_and_data_return_path_ta_td.yaml: 3180c2f3266be0ff44e01d73d247ca43ae2ee18ecaf61765f58849e36c701b19 src/rocprof_compute_soc/analysis_configs/gfx941/1500_address_processing_unit_and_data_return_path_ta_td.yaml: 3180c2f3266be0ff44e01d73d247ca43ae2ee18ecaf61765f58849e36c701b19 src/rocprof_compute_soc/analysis_configs/gfx942/1500_address_processing_unit_and_data_return_path_ta_td.yaml: 3180c2f3266be0ff44e01d73d247ca43ae2ee18ecaf61765f58849e36c701b19 src/rocprof_compute_soc/analysis_configs/gfx950/1500_address_processing_unit_and_data_return_path_ta_td.yaml: 9e56cef5b066fb575a5c530bcf9400f1291dd8636b12c8a2244cdba1defafc9f -src/rocprof_compute_soc/analysis_configs/gfx908/1600_vector_l1_data_cache.yaml: 438d0f4a972dd341eb2485f51a47d6860fbb30a6169054cd8550b4b7226e199f -src/rocprof_compute_soc/analysis_configs/gfx90a/1600_vector_l1_data_cache.yaml: 438d0f4a972dd341eb2485f51a47d6860fbb30a6169054cd8550b4b7226e199f -src/rocprof_compute_soc/analysis_configs/gfx940/1600_vector_l1_data_cache.yaml: 6100b218f24de9f1433b39a093ed04b9bb9dfe656c5df77583c9db332c447230 -src/rocprof_compute_soc/analysis_configs/gfx941/1600_vector_l1_data_cache.yaml: 6100b218f24de9f1433b39a093ed04b9bb9dfe656c5df77583c9db332c447230 -src/rocprof_compute_soc/analysis_configs/gfx942/1600_vector_l1_data_cache.yaml: 6100b218f24de9f1433b39a093ed04b9bb9dfe656c5df77583c9db332c447230 -src/rocprof_compute_soc/analysis_configs/gfx950/1600_vector_l1_data_cache.yaml: 67054ec0a4c6ca147a5dd40cc91f0e8e81378e1affe7d479274747579ecc524a +src/rocprof_compute_soc/analysis_configs/gfx908/1600_vector_l1_data_cache.yaml: 360a9cd6df4e345a45f0660bc8df2003d5eb5dba2359d7e59c89933dc9fba94e +src/rocprof_compute_soc/analysis_configs/gfx90a/1600_vector_l1_data_cache.yaml: 360a9cd6df4e345a45f0660bc8df2003d5eb5dba2359d7e59c89933dc9fba94e +src/rocprof_compute_soc/analysis_configs/gfx940/1600_vector_l1_data_cache.yaml: 37c061bc9751828621a72aa6576596262b684fca7b764adbb991cd7eef58987d +src/rocprof_compute_soc/analysis_configs/gfx941/1600_vector_l1_data_cache.yaml: 37c061bc9751828621a72aa6576596262b684fca7b764adbb991cd7eef58987d +src/rocprof_compute_soc/analysis_configs/gfx942/1600_vector_l1_data_cache.yaml: 37c061bc9751828621a72aa6576596262b684fca7b764adbb991cd7eef58987d +src/rocprof_compute_soc/analysis_configs/gfx950/1600_vector_l1_data_cache.yaml: ae0388f43813302969f51a80ac58678614b993f5163083a69e1c99811d730064 src/rocprof_compute_soc/analysis_configs/gfx908/1700_l2_cache.yaml: 54ff1df4ee08206d0aa4ff9cd9f0b20cbaa3866aecb9b40a0ac5969e9e25ed20 src/rocprof_compute_soc/analysis_configs/gfx90a/1700_l2_cache.yaml: ee87b5b6cdaca98de6e5cb0d06e2e092470e0e25aac1498f8abcfc8421932ae6 src/rocprof_compute_soc/analysis_configs/gfx940/1700_l2_cache.yaml: 78f9fee5dafc83d311da1c801200c1820e16a0678dd0548fafa8a966ec6a94d5 diff --git a/projects/rocprofiler-compute/utils/unified_config.yaml b/projects/rocprofiler-compute/utils/unified_config.yaml index 4d1964dcb7..531afa847b 100644 --- a/projects/rocprofiler-compute/utils/unified_config.yaml +++ b/projects/rocprofiler-compute/utils/unified_config.yaml @@ -16,6 +16,7 @@ panels: data source: - raw_csv_table: id: 101 + title: System Info source: sysinfo.csv columnwise: true - id: 200 @@ -1878,10 +1879,6 @@ panels: L2 Hit: value: ROUND(AVG((((100 * TCC_HIT_sum) / (TCC_HIT_sum + TCC_MISS_sum)) if ((TCC_HIT_sum + TCC_MISS_sum) != 0) else 0)), 0) - L2 Rd Lat: - value: null - L2 Wr Lat: - value: null Fabric_L2 Rd: value: ROUND(AVG((TCC_EA0_RDREQ_sum / $denom)), 0) Fabric_L2 Wr: @@ -2012,10 +2009,6 @@ panels: L2 Hit: value: ROUND(AVG((((100 * TCC_HIT_sum) / (TCC_HIT_sum + TCC_MISS_sum)) if ((TCC_HIT_sum + TCC_MISS_sum) != 0) else 0)), 0) - L2 Rd Lat: - value: null - L2 Wr Lat: - value: null Fabric_L2 Rd: value: ROUND(AVG((TCC_EA0_RDREQ_sum / $denom)), 0) Fabric_L2 Wr: @@ -2146,10 +2139,6 @@ panels: L2 Hit: value: ROUND(AVG((((100 * TCC_HIT_sum) / (TCC_HIT_sum + TCC_MISS_sum)) if ((TCC_HIT_sum + TCC_MISS_sum) != 0) else 0)), 0) - L2 Rd Lat: - value: null - L2 Wr Lat: - value: null Fabric_L2 Rd: value: ROUND(AVG((TCC_EA0_RDREQ_sum / $denom)), 0) Fabric_L2 Wr: @@ -11704,11 +11693,6 @@ panels: min: MIN(((100 * TD_TC_STALL_sum) / ($GRBM_GUI_ACTIVE_PER_XCD * $cu_per_gpu))) max: MAX(((100 * TD_TC_STALL_sum) / ($GRBM_GUI_ACTIVE_PER_XCD * $cu_per_gpu))) unit: pct - "Workgroup manager \u2192 Data-Return Stall": - avg: null - min: null - max: null - unit: pct Coalescable Instructions: avg: AVG((TD_COALESCABLE_WAVEFRONT_sum / $denom)) min: MIN((TD_COALESCABLE_WAVEFRONT_sum / $denom)) @@ -13338,7 +13322,7 @@ panels: avg: Avg min: Min max: Max - units: Units + units: Unit metric: gfx90a: Req: @@ -13532,7 +13516,7 @@ panels: avg: Avg min: Min max: Max - units: Units + units: Unit metric: gfx90a: {} gfx941: {}