From 1dba3ef264e6e8a71acc8e01cf277ae79d101865 Mon Sep 17 00:00:00 2001 From: Vignesh Edithal Date: Fri, 1 Aug 2025 12:06:40 -0400 Subject: [PATCH 1/4] Analysis data dump * Add `--db` option to analyze mode which creates database file containiing analysis result * 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 works * Update CHANGELOG --- projects/rocprofiler-compute/CHANGELOG.md | 2 + .../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/data-dump.rst | 104 ++++ .../docs/how-to/analyze/mode.rst | 3 +- .../docs/how-to/profile/mode.rst | 15 + .../rocprofiler-compute/docs/how-to/use.rst | 14 +- .../docs/sphinx/_toc.yml.in | 1 + projects/rocprofiler-compute/requirements.txt | 1 + projects/rocprofiler-compute/src/argparser.py | 11 + .../rocprof_compute_analyze/analysis_base.py | 11 + .../rocprof_compute_analyze/analysis_db.py | 550 ++++++++++++++++++ .../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 +- .../src/utils/analysis_orm.py | 185 ++++++ .../rocprofiler-compute/src/utils/parser.py | 6 +- .../src/utils/roofline_calc.py | 3 +- .../tests/test_profile_general.py | 22 + .../utils/autogen_hash.yaml | 32 +- .../utils/unified_config.yaml | 22 +- 38 files changed, 969 insertions(+), 77 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/docs/how-to/analyze/data-dump.rst 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 bf99e58006ef..17464ade8138 100644 --- a/projects/rocprofiler-compute/CHANGELOG.md +++ b/projects/rocprofiler-compute/CHANGELOG.md @@ -18,6 +18,8 @@ Full documentation for ROCm Compute Profiler is available at [https://rocm.docs. * Enable SQC_DCACHE_INFLIGHT_LEVEL counter and associated metrics * Enable TCP_TCP_LATENCY counter and associated counter for all GPUs except MI300 +* Add `--db` option to analyze mode which creates analysis database file for further reporting + ### Changed * Add notice for change in default output format to `rocpd` in a future release diff --git a/projects/rocprofiler-compute/docker/Dockerfile.doctest b/projects/rocprofiler-compute/docker/Dockerfile.doctest index 8b68b54337aa..b071787ac977 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 3ccd1b76a87c..3e35543bc14e 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 a52b6ed6c72a..7a080b692c9b 100644 --- a/projects/rocprofiler-compute/docs/conf.py +++ b/projects/rocprofiler-compute/docs/conf.py @@ -213,4 +213,4 @@ } # 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/data-dump.rst b/projects/rocprofiler-compute/docs/how-to/analyze/data-dump.rst new file mode 100644 index 000000000000..ba0b876846b2 --- /dev/null +++ b/projects/rocprofiler-compute/docs/how-to/analyze/data-dump.rst @@ -0,0 +1,104 @@ +.. meta:: + :description: ROCm Compute Profiler analysis: Data dump analysis + :keywords: ROCm Compute Profiler, ROCm, profiler, tool, Instinct, accelerator, analyze, filtering, metrics, baseline, comparison, post analysis, database, sqlite, data, dump, data dump + +****************** +Data dump analysis +****************** + +This section provides an overview of how the data generated in analysis mode can be dumped to a SQLite3 database for custom report generation. + +.. _overview: + +Overview +================= + +.. note:: + + Data dump analysis only works when the given workload(s) folder are created using ``--format-rocprof-output rocpd`` option in profile mode. + +In addition to applying various analysis mode options mentioned in :doc:`cli` to perform kernel, dispatch and metric filtering; Add ``--db `` option to the command. +This will do the following: + +* Create ``.db`` SQLite3 database file in the current working directory + + * Will fail if the file already exists to avoid overwriting + +* Will populate system information, roofline data, PC sampling data and calculate metrics data into the database file + + * Warning will be shown if any information cannot be calculated due to missing data + + * For example, attempting to analyze workload profiled with ``--no-roof`` option will throw a warning for unable to calculate roofline data. + +.. _schema: + +Data dump schema +================ + +Analysis data dump schema +------------------------- + +.. image:: ../../data/analyze/analysis_data_dump_schema.png + :align: center + :alt: Analysis data dump schema + +Analysis data dump views +------------------------ + +.. image:: ../../data/analyze/analysis_data_dump_views.png + :align: center + :alt: Analysis data dump views + + +.. _example: + +Data dump command 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 diff --git a/projects/rocprofiler-compute/docs/how-to/analyze/mode.rst b/projects/rocprofiler-compute/docs/how-to/analyze/mode.rst index 6ca7f4a773d1..04651ad592b0 100644 --- a/projects/rocprofiler-compute/docs/how-to/analyze/mode.rst +++ b/projects/rocprofiler-compute/docs/how-to/analyze/mode.rst @@ -22,7 +22,8 @@ options. * :doc:`cli` * :doc:`grafana-gui` * :doc:`standalone-gui` -* :doc:`text-based user interface (TUI)` +* :doc:`tui` +* :doc:`data-dump` .. note:: diff --git a/projects/rocprofiler-compute/docs/how-to/profile/mode.rst b/projects/rocprofiler-compute/docs/how-to/profile/mode.rst index 74640f6b450e..5a0ea4e07361 100644 --- a/projects/rocprofiler-compute/docs/how-to/profile/mode.rst +++ b/projects/rocprofiler-compute/docs/how-to/profile/mode.rst @@ -24,6 +24,9 @@ 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 `: Collect raw data in ``rocpd`` database + output format and merge them into a single csv file containing counter collection data. + * :ref:`Filtering `: Apply runtime filters to speed up the profiling process. @@ -217,6 +220,18 @@ 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 +----------------------- + +* Provide ``--format-rocprof-output rocpd`` profile mode option to have the underlying ``rocprof`` collect raw performance counter data in ``rocpd`` database format. + + * ROCm Compute Profiler takes care of merging multiple ``rocpd`` databases into a single csv file containing counter collection data in the workload folder. + + * 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/docs/how-to/use.rst b/projects/rocprofiler-compute/docs/how-to/use.rst index 594939521f11..764fed63e0f6 100644 --- a/projects/rocprofiler-compute/docs/how-to/use.rst +++ b/projects/rocprofiler-compute/docs/how-to/use.rst @@ -164,12 +164,20 @@ Analyze mode is great if you want immediate access to a hardware component you’re already familiar with. + Use the ``--db `` option in analyze mode to have the post analysis data dumped into a SQLite database + named ``db_name.db``. This can be used to generate custom analysis reports. Note that using this option + will disable any analysis report generation such as CLI analysis report. + .. code-block:: shell - $ rocprof-compute analyze --help + $ rocprof-compute analyze --db db_name -p workload_path_1 -p workload_path_2 + + See :doc:`analyze/mode` to learn about this mode in depth and to get started + with analysis using ROCm Compute Profiler. -See :doc:`analyze/mode` to learn about this mode in depth and to get started -with analysis using ROCm Compute Profiler. + .. code-block:: shell + + $ rocprof-compute analyze --help .. _modes-database: diff --git a/projects/rocprofiler-compute/docs/sphinx/_toc.yml.in b/projects/rocprofiler-compute/docs/sphinx/_toc.yml.in index 483bf1c0f584..4456146f7136 100644 --- a/projects/rocprofiler-compute/docs/sphinx/_toc.yml.in +++ b/projects/rocprofiler-compute/docs/sphinx/_toc.yml.in @@ -27,6 +27,7 @@ subtrees: - file: how-to/analyze/grafana-gui.rst - file: how-to/analyze/standalone-gui.rst - file: how-to/analyze/tui.rst + - file: how-to/analyze/data-dump.rst - caption: Conceptual entries: diff --git a/projects/rocprofiler-compute/requirements.txt b/projects/rocprofiler-compute/requirements.txt index ef0de785a29a..9fb8f0f1bef1 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 ec8569e3df74..3587f9bea34f 100644 --- a/projects/rocprofiler-compute/src/argparser.py +++ b/projects/rocprofiler-compute/src/argparser.py @@ -654,6 +654,17 @@ def validate_block(value): help="\t\tActivate a Textual User Interface (TUI) to " "interact with rocprofiler-compute metrics.", ) + analyze_group.add_argument( + "--db", + type=str, + required=False, + metavar="", + help=( + "\t\tStore the analysis data in a sqlite3 database file\n" + "\t\twith the given name and .db suffix added to it.\n" + "\t\tThis option will disable generation of analysis report in CLI/GUI/TUI." + ), + ) analyze_group.add_argument( "-R", "--roofline-data-type", 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 d1d09428c74b..fd9668d5bf05 100644 --- a/projects/rocprofiler-compute/src/rocprof_compute_analyze/analysis_base.py +++ b/projects/rocprofiler-compute/src/rocprof_compute_analyze/analysis_base.py @@ -258,6 +258,17 @@ def sanitize(self): print("Node list:", " ".join(nodes)) sys.exit(0) + # Ensure analysis db file only contains alphanumeric characters and the file does not exist + if self.__args.db: + if not self.__args.db.isalnum(): + console_error( + "Analysis database file name must contain only alphanumeric characters." + ) + if Path(f"{self.__args.db}.db").exists(): + console_error( + f"Analysis database file {self.__args.db}.db already exists. Please choose a different name." + ) + # ---------------------------------------------------- # Required methods to be implemented by child classes # ---------------------------------------------------- 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 000000000000..5980e91d3875 --- /dev/null +++ b/projects/rocprofiler-compute/src/rocprof_compute_analyze/analysis_db.py @@ -0,0 +1,550 @@ +##############################################################################bl +# MIT License +# +# Copyright (c) 2021 - 2025 Advanced Micro Devices, Inc. All Rights Reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +##############################################################################el + +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 +import utils.analysis_orm as orm +from utils.logger import demarcate, console_debug, console_warning, console_error +from utils.parser import PC_SAMPLING_NOT_ISSUE_PREFIX, build_in_vars, CodeTransformer, to_avg, to_concat, to_int, to_max, to_median, to_min, to_mod, to_quantile, to_round, to_std +from utils.roofline_calc import SUPPORTED_DATATYPES, PEAK_OPS_DATATYPES, MFMA_DATATYPES, CACHE_HIERARCHY +from utils.utils import get_version +import pandas as pd +import json +from pathlib import Path +import re +import ast +import astunparse + + +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 + db_name = Database.init(self.get_args().db) + 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") + + 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"): + keys.append(f"{dtype}Flops") + elif dtype.startswith("I"): + keys.append(f"{dtype}Ops") + elif dtype in MFMA_DATATYPES: + if dtype.startswith("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 + + 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() + + def evaluate(name, value, parse=False): + if parse: + value = re.sub(f"\$([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.get('pmc_perf')", "pmc_df") + value = re.sub(f"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, + }, + ) + except Exception as e: + console_warning(f"Failed to evaluate expression for {name}: {value} - {e}") + return None + + # Calculate PER_XCD variables first + for key, value in build_in_vars.items(): + if "PER_XCD" in key: + sys_info[key] = evaluate(key, value, 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] = evaluate(key, value, parse=True) + + # Get name and print warning + values_data_per_workload[workload_path]["value"] = values_data_per_workload[workload_path].apply( + lambda row: evaluate(f"{row['metric_id']} - {row['value_name']}", row['value']), 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 in self._arch_configs[gfx_arch].dfs.values() + 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 in self._arch_configs[gfx_arch].dfs.values() + 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() + + def evaluate_roofline_metric(pmc_df, sys_info, metric_name): + if metric_name == "total_flops": + return ( + ( + 64 + * ( + pmc_df["SQ_INSTS_VALU_ADD_F16"] + + pmc_df["SQ_INSTS_VALU_MUL_F16"] + + (2 * pmc_df["SQ_INSTS_VALU_FMA_F16"]) + + pmc_df["SQ_INSTS_VALU_TRANS_F16"] + ) + ) + + ( + 64 + * ( + pmc_df["SQ_INSTS_VALU_ADD_F32"] + + pmc_df["SQ_INSTS_VALU_MUL_F32"] + + (2 * pmc_df["SQ_INSTS_VALU_FMA_F32"]) + + pmc_df["SQ_INSTS_VALU_TRANS_F32"] + ) + ) + + ( + 64 + * ( + pmc_df["SQ_INSTS_VALU_ADD_F64"] + + pmc_df["SQ_INSTS_VALU_MUL_F64"] + + (2 * pmc_df["SQ_INSTS_VALU_FMA_F64"]) + + pmc_df["SQ_INSTS_VALU_TRANS_F64"] + ) + ) + + (pmc_df["SQ_INSTS_VALU_MFMA_MOPS_F16"] * 512) + + (pmc_df["SQ_INSTS_VALU_MFMA_MOPS_BF16"] * 512) + + (pmc_df["SQ_INSTS_VALU_MFMA_MOPS_F32"] * 512) + + (pmc_df["SQ_INSTS_VALU_MFMA_MOPS_F64"] * 512) + + ( + pmc_df["SQ_INSTS_VALU_MFMA_MOPS_F8"] * 512 + if "FP8" in SUPPORTED_DATATYPES[sys_info["gpu_arch"]] + else 0 + ) + + ( + pmc_df["SQ_INSTS_VALU_MFMA_MOPS_F6F4"] * 512 + if ( + "FP4" in SUPPORTED_DATATYPES[sys_info["gpu_arch"]] + or "FP6" in SUPPORTED_DATATYPES[sys_info["gpu_arch"]] + ) + else 0 + ) + ).sum() + elif metric_name == "l1_cache_data": + return pmc_df["TCP_TOTAL_CACHE_ACCESSES_sum"].sum() * 64 + elif metric_name == "l2_cache_data": + return ( + pmc_df["TCP_TCC_WRITE_REQ_sum"] + + pmc_df["TCP_TCC_ATOMIC_WITH_RET_REQ_sum"] + + pmc_df["TCP_TCC_ATOMIC_WITHOUT_RET_REQ_sum"] + + pmc_df["TCP_TCC_READ_REQ_sum"] + ).sum() * 64 + elif metric_name == "hbm_cache_data": + if sys_info["gpu_series"] == "MI200": + return ( + (pmc_df["TCC_EA_RDREQ_32B_sum"] * 32) + + ( + (pmc_df["TCC_EA_RDREQ_sum"] - pmc_df["TCC_EA_RDREQ_32B_sum"]) + * 64 + ) + + (pmc_df["TCC_EA_WRREQ_64B_sum"] * 64) + + ( + (pmc_df["TCC_EA_WRREQ_sum"] - pmc_df["TCC_EA_WRREQ_64B_sum"]) + * 32 + ) + ).sum() + else: + return ( + (pmc_df["TCC_BUBBLE_sum"] * 128) + + (pmc_df["TCC_EA0_RDREQ_32B_sum"] * 32) + + ( + ( + pmc_df["TCC_EA0_RDREQ_sum"] + - pmc_df["TCC_BUBBLE_sum"] + - pmc_df["TCC_EA0_RDREQ_32B_sum"] + ) + * 64 + ) + + ( + ( + pmc_df["TCC_EA0_WRREQ_sum"] + - pmc_df["TCC_EA0_WRREQ_64B_sum"] + ) + * 32 + ) + + (pmc_df["TCC_EA0_WRREQ_64B_sum"] * 64) + ).sum() + return None + + roofline_df = pd.DataFrame([ + { + "kernel_name": kernel_name, + **{ + metric_name: evaluate_roofline_metric(pmc_df[pmc_df["Kernel_Name"] == kernel_name], sys_info, metric_name) + for metric_name in [ + "total_flops", + "l1_cache_data", + "l2_cache_data", + "hbm_cache_data" + ] + } + } + 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 + \ No newline at end of file diff --git a/projects/rocprofiler-compute/src/rocprof_compute_base.py b/projects/rocprofiler-compute/src/rocprof_compute_base.py index a2b06c82636d..08004049dfc6 100644 --- a/projects/rocprofiler-compute/src/rocprof_compute_base.py +++ b/projects/rocprofiler-compute/src/rocprof_compute_base.py @@ -137,6 +137,8 @@ def detect_analyze(self): self.__analyze_mode = "web_ui" elif self.__args.tui: self.__analyze_mode = "tui" + elif self.__args.db: + self.__analyze_mode = "db" else: self.__analyze_mode = "cli" return @@ -445,6 +447,10 @@ def run_analysis(self): 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 8470ffbbe3e3..23d024fde376 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 67c3aa1dfc28..4c615fb0d5b3 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 50af33c21b1d..b374ea946677 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 8470ffbbe3e3..23d024fde376 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 50af33c21b1d..b374ea946677 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 8470ffbbe3e3..23d024fde376 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 0a6510182a7b..03b5606ad780 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 db745209b746..e5b5eb9e9cfc 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 8470ffbbe3e3..23d024fde376 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 0a6510182a7b..03b5606ad780 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 db745209b746..e5b5eb9e9cfc 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 8470ffbbe3e3..23d024fde376 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 0a6510182a7b..03b5606ad780 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 db745209b746..e5b5eb9e9cfc 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 8470ffbbe3e3..23d024fde376 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 f95e3fcb1f3f..2d8ac4d781d6 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/utils/analysis_orm.py b/projects/rocprofiler-compute/src/utils/analysis_orm.py new file mode 100644 index 000000000000..9bda80dc86c7 --- /dev/null +++ b/projects/rocprofiler-compute/src/utils/analysis_orm.py @@ -0,0 +1,185 @@ +##############################################################################bl +# MIT License +# +# Copyright (c) 2021 - 2025 Advanced Micro Devices, Inc. All Rights Reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +##############################################################################el + +from sqlalchemy import create_engine, Column, String, JSON, Integer, ForeignKey, Text, Float, select, func, text +from sqlalchemy.orm import declarative_base, sessionmaker, relationship + +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, name): + db_name = f"{name}.db" + 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 {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 d1d75abaa6af..a18e7bbb9cc5 100644 --- a/projects/rocprofiler-compute/src/utils/parser.py +++ b/projects/rocprofiler-compute/src/utils/parser.py @@ -111,6 +111,8 @@ "CONCAT": "to_concat", } +PC_SAMPLING_NOT_ISSUE_PREFIX = "ROCPROFILER_PC_SAMPLING_INSTRUCTION_NOT_ISSUED_REASON_" + # ------------------------------------------------------------------------------ @@ -1172,9 +1174,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 3a670d2578ff..7e124bc47271 100644 --- a/projects/rocprofiler-compute/src/utils/roofline_calc.py +++ b/projects/rocprofiler-compute/src/utils/roofline_calc.py @@ -100,6 +100,7 @@ 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 @@ -160,7 +161,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/tests/test_profile_general.py b/projects/rocprofiler-compute/tests/test_profile_general.py index 51dcdd50c7f9..630cc7cfca33 100644 --- a/projects/rocprofiler-compute/tests/test_profile_general.py +++ b/projects/rocprofiler-compute/tests/test_profile_general.py @@ -743,6 +743,28 @@ 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", + "--db", + 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 3a4e66320ce9..c24be726c576 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: 57f95dcd487dfcdf24e1c2d8eb16d14dc3462df83d11a08e7de2b06343b48c3e src/rocprof_compute_soc/analysis_configs/gfx90a/0400_roofline.yaml: 57f95dcd487dfcdf24e1c2d8eb16d14dc3462df83d11a08e7de2b06343b48c3e @@ -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 d214d3148d83..7637b7510900 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: @@ -10750,11 +10739,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)) @@ -12384,7 +12368,7 @@ panels: avg: Avg min: Min max: Max - units: Units + units: Unit metric: gfx90a: Req: @@ -12578,7 +12562,7 @@ panels: avg: Avg min: Min max: Max - units: Units + units: Unit metric: gfx90a: {} gfx941: {} From e904aa350c8302dcea3cab0501de6767d362d2f6 Mon Sep 17 00:00:00 2001 From: Vignesh Edithal Date: Thu, 14 Aug 2025 14:18:02 -0400 Subject: [PATCH 2/4] run ruff format and linting --- projects/rocprofiler-compute/src/argparser.py | 7 +- .../rocprof_compute_analyze/analysis_base.py | 9 +- .../rocprof_compute_analyze/analysis_db.py | 270 +++++++++++++----- .../src/rocprof_compute_base.py | 8 +- .../src/rocprof_compute_soc/soc_base.py | 15 +- .../rocprof_compute_tui/views/main_view.py | 4 +- .../src/utils/analysis_orm.py | 88 ++++-- .../rocprofiler-compute/src/utils/parser.py | 8 +- .../rocprofiler-compute/src/utils/utils.py | 2 +- .../tests/test_profile_general.py | 14 +- .../rocprofiler-compute/tests/test_utils.py | 37 ++- .../rocprofiler-compute/utils/split_config.py | 16 +- 12 files changed, 331 insertions(+), 147 deletions(-) diff --git a/projects/rocprofiler-compute/src/argparser.py b/projects/rocprofiler-compute/src/argparser.py index 3587f9bea34f..18b5cf95911a 100644 --- a/projects/rocprofiler-compute/src/argparser.py +++ b/projects/rocprofiler-compute/src/argparser.py @@ -228,7 +228,10 @@ def validate_block(value): "--set", default=None, dest="set_selected", - help="\t\t\tProfile a set of metrics of topic of interest by collecting counters in a single pass.\n\t\t\tFor available sets, see --list-sets", + help=( + "\t\t\tProfile a set of metrics of topic of interest by collecting counters" + " in a single pass.\n\t\t\tFor available sets, see --list-sets" + ), ) profile_group.add_argument( @@ -307,7 +310,7 @@ def validate_block(value): "\t\t\t For stochastic sampling, the interval is in cycles.\n" "\t\t\t For host_trap sampling, the interval is in microsecond " "(DEFAULT: 1048576)." - ) + ), ) profile_group.add_argument( 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 fd9668d5bf05..dbafd5656d63 100644 --- a/projects/rocprofiler-compute/src/rocprof_compute_analyze/analysis_base.py +++ b/projects/rocprofiler-compute/src/rocprof_compute_analyze/analysis_base.py @@ -258,15 +258,18 @@ def sanitize(self): print("Node list:", " ".join(nodes)) sys.exit(0) - # Ensure analysis db file only contains alphanumeric characters and the file does not exist + # Ensure analysis db file only contains alphanumeric + # characters and the file does not exist if self.__args.db: if not self.__args.db.isalnum(): console_error( - "Analysis database file name must contain only alphanumeric characters." + "Analysis database file name must contain only alphanumeric " + "characters." ) if Path(f"{self.__args.db}.db").exists(): console_error( - f"Analysis database file {self.__args.db}.db already exists. Please choose a different name." + f"Analysis database file {self.__args.db}.db already exists. " + "Please choose a different name." ) # ---------------------------------------------------- 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 5980e91d3875..68e4447409bb 100644 --- a/projects/rocprofiler-compute/src/rocprof_compute_analyze/analysis_db.py +++ b/projects/rocprofiler-compute/src/rocprof_compute_analyze/analysis_db.py @@ -22,21 +22,42 @@ # 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 -import utils.analysis_orm as orm -from utils.logger import demarcate, console_debug, console_warning, console_error -from utils.parser import PC_SAMPLING_NOT_ISSUE_PREFIX, build_in_vars, CodeTransformer, to_avg, to_concat, to_int, to_max, to_median, to_min, to_mod, to_quantile, to_round, to_std -from utils.roofline_calc import SUPPORTED_DATATYPES, PEAK_OPS_DATATYPES, MFMA_DATATYPES, CACHE_HIERARCHY +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, +) +from utils.roofline_calc import ( + CACHE_HIERARCHY, + MFMA_DATATYPES, + PEAK_OPS_DATATYPES, + SUPPORTED_DATATYPES, +) from utils.utils import get_version -import pandas as pd -import json -from pathlib import Path -import re -import ast -import astunparse class db_analysis(OmniAnalyze_Base): @@ -48,27 +69,33 @@ 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.") + 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") + 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() + ) + .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._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() @@ -86,11 +113,15 @@ def run_analysis(self): 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), + 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(): + 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, @@ -104,7 +135,9 @@ def run_analysis(self): workload=workload_obj, ) ) - for dispatch in self._dispatch_data_per_workload.get(workload_path, pd.DataFrame()).itertuples(): + 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, @@ -114,7 +147,9 @@ def run_analysis(self): workload=workload_obj, ) ) - for metric in self._metrics_info_data_per_workload.get(workload_path, pd.DataFrame()).itertuples(): + 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, @@ -125,7 +160,9 @@ def run_analysis(self): workload=workload_obj, ) Database.get_session().add(metric_obj) - for value in self._values_data_per_workload.get(workload_path, pd.DataFrame()).itertuples(): + 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( @@ -135,7 +172,9 @@ def run_analysis(self): ) ) - for roofline_data in self._roofline_data_per_workload.get(workload_path, pd.DataFrame()).itertuples(): + 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, @@ -150,9 +189,9 @@ def run_analysis(self): 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, + compute_version=version["version"], + git_version=version["sha"], + schema_version=orm.SCHEMA_VERSION, ) ) @@ -168,15 +207,19 @@ 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(): + 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() + 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"]]: + 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"): keys.append(f"{dtype}Flops") @@ -188,9 +231,7 @@ def calc_roofline_ceilings(self): 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 + key: roofline_dict[key] for key in keys if key in roofline_dict } if roofline_ceilings_per_workload: @@ -201,15 +242,23 @@ 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(): + 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 = 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_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"] @@ -220,8 +269,12 @@ def calc_pc_sampling_data(self): { "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"), + "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 @@ -229,37 +282,53 @@ def calc_pc_sampling_data(self): 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):] + 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 = ( + 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 + 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 + 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) @@ -282,7 +351,11 @@ def calc_expressions(self): def evaluate(name, value, parse=False): if parse: - value = re.sub(f"\$([0-9A-Za-z_]+)", lambda m: f"sys_info[\"{m.group(1)}\"]", value) + 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) @@ -291,11 +364,15 @@ def evaluate(name, value, parse=False): value = value.replace("pmc_df['sys_info']", "sys_info") else: value = value.replace("raw_pmc_df.get('pmc_perf')", "pmc_df") - value = re.sub(f"ammolite__([0-9A-Za-z_]+)", lambda m: f"sys_info[\"{m.group(1)}\"]", value) + 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 + {}, # no globals { # only locals "pmc_df": pmc_df, @@ -313,7 +390,9 @@ def evaluate(name, value, parse=False): }, ) except Exception as e: - console_warning(f"Failed to evaluate expression for {name}: {value} - {e}") + console_warning( + f"Failed to evaluate expression for {name}: {value} - {e}" + ) return None # Calculate PER_XCD variables first @@ -327,8 +406,13 @@ def evaluate(name, value, parse=False): sys_info[key] = evaluate(key, value, parse=True) # Get name and print warning - values_data_per_workload[workload_path]["value"] = values_data_per_workload[workload_path].apply( - lambda row: evaluate(f"{row['metric_id']} - {row['value_name']}", row['value']), axis=1 + values_data_per_workload[workload_path]["value"] = values_data_per_workload[ + workload_path + ].apply( + lambda row: evaluate( + f"{row['metric_id']} - {row['value_name']}", row["value"] + ), + axis=1, ) console_debug("Calculated metric values") @@ -345,9 +429,21 @@ def calc_metrics_data(self): 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"] + 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"] + 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(), @@ -355,7 +451,10 @@ def calc_metrics_data(self): "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])], + "sub_table_name": table_names_map[ + int(metric_id.split(".")[0]) * 100 + + int(metric_id.split(".")[1]) + ], } for metric_df in self._arch_configs[gfx_arch].dfs.values() if set(metric_df.columns).intersection({"Metric", "Channel"}) @@ -370,7 +469,9 @@ def calc_metrics_data(self): for metric_df in self._arch_configs[gfx_arch].dfs.values() 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 + for value_name in metric_df.drop( + columns=non_expression_columns, errors="ignore" + ).columns ]) metrics_info_data_per_workload[workload_path] = metrics_info_df @@ -403,23 +504,31 @@ def apply_pmc_filters(self): 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])] + 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 - ])] + 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)) - ] + 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["Dispatch_ID"] + .astype(str) + .isin(self._runs[workload_path].filter_dispatch_ids) ] pmc_df_per_workload[workload_path] = pmc_df @@ -495,12 +604,18 @@ def evaluate_roofline_metric(pmc_df, sys_info, metric_name): return ( (pmc_df["TCC_EA_RDREQ_32B_sum"] * 32) + ( - (pmc_df["TCC_EA_RDREQ_sum"] - pmc_df["TCC_EA_RDREQ_32B_sum"]) + ( + pmc_df["TCC_EA_RDREQ_sum"] + - pmc_df["TCC_EA_RDREQ_32B_sum"] + ) * 64 ) + (pmc_df["TCC_EA_WRREQ_64B_sum"] * 64) + ( - (pmc_df["TCC_EA_WRREQ_sum"] - pmc_df["TCC_EA_WRREQ_64B_sum"]) + ( + pmc_df["TCC_EA_WRREQ_sum"] + - pmc_df["TCC_EA_WRREQ_64B_sum"] + ) * 32 ) ).sum() @@ -531,20 +646,25 @@ def evaluate_roofline_metric(pmc_df, sys_info, metric_name): { "kernel_name": kernel_name, **{ - metric_name: evaluate_roofline_metric(pmc_df[pmc_df["Kernel_Name"] == kernel_name], sys_info, metric_name) + metric_name: evaluate_roofline_metric( + pmc_df[pmc_df["Kernel_Name"] == kernel_name], + sys_info, + metric_name, + ) for metric_name in [ "total_flops", "l1_cache_data", "l2_cache_data", - "hbm_cache_data" + "hbm_cache_data", ] - } + }, } - for kernel_name in self._top_kernels_per_workload[workload_path][:self.get_args().max_stat_num] + 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 - \ No newline at end of file diff --git a/projects/rocprofiler-compute/src/rocprof_compute_base.py b/projects/rocprofiler-compute/src/rocprof_compute_base.py index 08004049dfc6..dda5c8068f4f 100644 --- a/projects/rocprofiler-compute/src/rocprof_compute_base.py +++ b/projects/rocprofiler-compute/src/rocprof_compute_base.py @@ -261,7 +261,8 @@ def list_sets(self): # Print header print( - f"{'Set Option':<35} {'Set Title':<35} {'Metric Name':<30} {'Metric ID':<10}" + f"{'Set Option':<35} {'Set Title':<35} {'Metric Name':<30} " + f"{'Metric ID':<10}" ) print("-" * 115) @@ -281,7 +282,8 @@ def list_sets(self): title_display = title if first_row else "" print( - f"{set_display:<35} {title_display:<35} {metric_name:<30} {metric_id:<10}" + f"{set_display:<35} {title_display:<35} {metric_name:<30} " + f"{metric_id:<10}" ) first_row = False # Empty line between sets @@ -291,7 +293,7 @@ def list_sets(self): if sets_info: first_set = next(iter(sets_info.keys())) print(f" rocprof-compute profile --set {first_set} # Profile this set") - print(f" rocprof-compute profile --list-sets # Show this help") + print(" rocprof-compute profile --list-sets # Show this help") print() sys.exit(0) 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 f5790ffb484b..52645b0e1a1a 100644 --- a/projects/rocprofiler-compute/src/rocprof_compute_soc/soc_base.py +++ b/projects/rocprofiler-compute/src/rocprof_compute_soc/soc_base.py @@ -300,7 +300,8 @@ def detect_counters(self): sets_info = parse_sets_yaml(self.__arch) if set_selected not in set(sets_info.keys()): console_error( - f"argument --set: invalid choice: '{set_selected}' (choose from {sets_info.keys()})" + f"argument --set: invalid choice: '{set_selected}' " + f"(choose from {sets_info.keys()})" ) self.__args.filter_blocks = [ next(iter(metric.keys())) @@ -377,14 +378,10 @@ def detect_counters(self): 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) - ) - } - ) + counters = counters.union({ + f"{counter_name}[{i}]" + for i in range(num_xcd_for_pmc_file * int(self._mspec._l2_banks)) + }) return counters 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 4e38dad5577d..dd0f9c2a9e38 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 @@ -57,9 +57,7 @@ def __init__(self): super().__init__(id="main-container") self.start_path = ( # NOTE: is cwd the best choice? - Path.cwd() - if DEFAULT_START_PATH is None - else Path(DEFAULT_START_PATH) + Path.cwd() if DEFAULT_START_PATH is None else Path(DEFAULT_START_PATH) ) self.logger = Logger() diff --git a/projects/rocprofiler-compute/src/utils/analysis_orm.py b/projects/rocprofiler-compute/src/utils/analysis_orm.py index 9bda80dc86c7..1638e6d6e25a 100644 --- a/projects/rocprofiler-compute/src/utils/analysis_orm.py +++ b/projects/rocprofiler-compute/src/utils/analysis_orm.py @@ -22,8 +22,20 @@ # SOFTWARE. ##############################################################################el -from sqlalchemy import create_engine, Column, String, JSON, Integer, ForeignKey, Text, Float, select, func, text -from sqlalchemy.orm import declarative_base, sessionmaker, relationship +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 @@ -32,8 +44,9 @@ PREFIX = "compute_" SCHEMA_VERSION = "1.0.0" + class Workload(Base): - __tablename__ = f'{PREFIX}workload' + __tablename__ = f"{PREFIX}workload" workload_id = Column(Integer, primary_key=True) name = Column(String) @@ -51,28 +64,34 @@ class Workload(Base): # Workload can have multiple pc_sampling values pc_sampling_values = relationship("PCsampling", back_populates="workload") + class Metric(Base): - __tablename__ = f'{PREFIX}metric' + __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 + 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' + __tablename__ = f"{PREFIX}roofline_data" roofline_uuid = Column(Integer, primary_key=True) - workload_id = Column(Integer, ForeignKey(f'{PREFIX}workload.workload_id'), nullable=False) + 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) @@ -82,11 +101,14 @@ class RooflineData(Base): # Roofline data point can have one workload workload = relationship("Workload", back_populates="roofline_data_points") + class Dispatch(Base): - __tablename__ = f'{PREFIX}dispatch' + __tablename__ = f"{PREFIX}dispatch" dispatch_uuid = Column(Integer, primary_key=True) - workload_id = Column(Integer, ForeignKey(f'{PREFIX}workload.workload_id'), nullable=False) + workload_id = Column( + Integer, ForeignKey(f"{PREFIX}workload.workload_id"), nullable=False + ) dispatch_id = Column(Integer) kernel_name = Column(String) gpu_id = Column(Integer) @@ -95,11 +117,14 @@ class Dispatch(Base): # Dispatch can have one workload workload = relationship("Workload", back_populates="dispatches") + class PCsampling(Base): - __tablename__ = f'{PREFIX}pcsampling' + __tablename__ = f"{PREFIX}pcsampling" pc_sampling_uuid = Column(Integer, primary_key=True) - workload_id = Column(Integer, ForeignKey(f'{PREFIX}workload.workload_id'), nullable=False) + workload_id = Column( + Integer, ForeignKey(f"{PREFIX}workload.workload_id"), nullable=False + ) source = Column(String) instruction = Column(String) count = Column(Integer) @@ -112,19 +137,23 @@ class PCsampling(Base): # PCsampling can have one workload workload = relationship("Workload", back_populates="pc_sampling_values") + class Value(Base): - __tablename__ = f'{PREFIX}value' + __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 + 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' + __tablename__ = f"{PREFIX}metadata" id = Column(Integer, primary_key=True) compute_version = Column(String) @@ -158,15 +187,15 @@ def write(self): 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') + 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, @@ -176,10 +205,13 @@ def get_views(): Metric.sub_table_name, Metric.unit, Value.value_name, - Value.value + Value.value, ).join(Value, Metric.metric_uuid == Value.metric_uuid), } return [ - text(f"CREATE VIEW {PREFIX}{view_name} AS {stmt.compile(compile_kwargs={'literal_binds': True})}") + 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 a18e7bbb9cc5..fa8a50dab6dc 100644 --- a/projects/rocprofiler-compute/src/utils/parser.py +++ b/projects/rocprofiler-compute/src/utils/parser.py @@ -960,8 +960,8 @@ def eval_metric(dfs, dfs_type, sys_info, raw_pmc_df, debug, config): except TypeError: console_warning( "Skipping entry. Encountered a missing " - "counter\n{} has been assigned to None\n{}" - .format( + "counter\n{} has been assigned " + "to None\n{}".format( expr, np.nan, ) @@ -1045,8 +1045,8 @@ def apply_filters(workload, dir, is_gui, debug): 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( + "{} is an invalid kernel id. Please " + "enter an id between 0-{}".format( kernel_id, len(kernels_df["Kernel_Name"]) - 1, ) diff --git a/projects/rocprofiler-compute/src/utils/utils.py b/projects/rocprofiler-compute/src/utils/utils.py index e0d20539433a..d2d8c41df02b 100644 --- a/projects/rocprofiler-compute/src/utils/utils.py +++ b/projects/rocprofiler-compute/src/utils/utils.py @@ -745,7 +745,7 @@ def run_prof( config.rocprof_compute_home / "rocprof_compute_soc" / "profile_configs" - / f"counter_defs.yaml", + / "counter_defs.yaml", "r", ) as file: counter_defs = yaml.safe_load(file) diff --git a/projects/rocprofiler-compute/tests/test_profile_general.py b/projects/rocprofiler-compute/tests/test_profile_general.py index 630cc7cfca33..fad3512fe8cb 100644 --- a/projects/rocprofiler-compute/tests/test_profile_general.py +++ b/projects/rocprofiler-compute/tests/test_profile_general.py @@ -744,7 +744,9 @@ def test_roof_rocpd(binary_handler_profile_rocprof_compute): @pytest.mark.misc -def test_analyze_rocpd(binary_handler_profile_rocprof_compute, binary_handler_analyze_rocprof_compute): +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) @@ -1698,9 +1700,9 @@ def test_memory_throughput_set(self, binary_handler_profile_rocprof_compute): 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() - ), f"Expected memory metric {metric_id} not found" + assert metric_id in open(Path(workload_dir) / "log.txt", "r").read(), ( + f"Expected memory metric {metric_id} not found" + ) test_utils.clean_output_dir(config["cleanup"], workload_dir) @@ -1767,7 +1769,9 @@ def test_invalid_set_error_handling(self, binary_handler_profile_rocprof_compute assert returncode == 1 test_utils.clean_output_dir(config["cleanup"], workload_dir) - def test_set_and_block_mutual_exclusion(self, binary_handler_profile_rocprof_compute): + def test_set_and_block_mutual_exclusion( + self, binary_handler_profile_rocprof_compute + ): options = ["--set", "compute_thruput_util", "--block", "12"] workload_dir = test_utils.get_output_dir() diff --git a/projects/rocprofiler-compute/tests/test_utils.py b/projects/rocprofiler-compute/tests/test_utils.py index 4b530ce333bc..20662672c274 100644 --- a/projects/rocprofiler-compute/tests/test_utils.py +++ b/projects/rocprofiler-compute/tests/test_utils.py @@ -30,18 +30,17 @@ import locale import logging import os -import tempfile import pathlib import re import shutil import subprocess +import tempfile from pathlib import Path from types import SimpleNamespace from unittest import mock import pandas as pd import pytest -import yaml import utils.utils as utils @@ -173,7 +172,9 @@ def get_num_pmc_file(output_dir): """ perfmon_path = Path(output_dir) / "perfmon" - return len([f for f in perfmon_path.iterdir() if f.is_file() and f.suffix == ".txt"]) + return len([ + f for f in perfmon_path.iterdir() if f.is_file() and f.suffix == ".txt" + ]) # ============================================================================= @@ -1770,8 +1771,18 @@ def test_v3_json_to_csv_complex_dispatch(tmp_path, monkeypatch): { "metadata": {"pid": 12345}, "agents": [ - {"id": {"handle": 1}, "type": 2, "node_id": 0, "wave_front_size": 64}, - {"id": {"handle": 2}, "type": 2, "node_id": 1, "wave_front_size": 32}, + { + "id": {"handle": 1}, + "type": 2, + "node_id": 0, + "wave_front_size": 64, + }, + { + "id": {"handle": 2}, + "type": 2, + "node_id": 1, + "wave_front_size": 32, + }, ], "counters": [ { @@ -1924,8 +1935,10 @@ def test_v3_json_to_csv_complex_dispatch(tmp_path, monkeypatch): def test_v3_json_to_csv_missing_counters_handling(tmp_path, monkeypatch): """ - Test v3_json_to_csv handles cases where different dispatches have different sets of counters. - This addresses the DataFrame creation issue where arrays have different lengths. + Test v3_json_to_csv handles cases where + different dispatches have different sets of counters. + This addresses the DataFrame creation issue + where arrays have different lengths. Args: tmp_path (pathlib.Path): Temporary directory for test files @@ -1937,7 +1950,12 @@ def test_v3_json_to_csv_missing_counters_handling(tmp_path, monkeypatch): { "metadata": {"pid": 12345}, "agents": [ - {"id": {"handle": 1}, "type": 2, "node_id": 0, "wave_front_size": 64} + { + "id": {"handle": 1}, + "type": 2, + "node_id": 0, + "wave_front_size": 64, + } ], "counters": [ { @@ -2073,7 +2091,8 @@ def test_v3_json_to_csv_missing_counters_handling(tmp_path, monkeypatch): except ValueError as e: if "All arrays must be of the same length" in str(e): pytest.skip( - "v3_json_to_csv does not currently handle missing counters gracefully - arrays have different lengths" + "v3_json_to_csv does not currently handle missing " + "counters gracefully - arrays have different lengths" ) else: raise diff --git a/projects/rocprofiler-compute/utils/split_config.py b/projects/rocprofiler-compute/utils/split_config.py index 0c9f8647021a..1c8ae1dbd1a2 100644 --- a/projects/rocprofiler-compute/utils/split_config.py +++ b/projects/rocprofiler-compute/utils/split_config.py @@ -34,7 +34,11 @@ def get_autogen_text(config_file="utils/unified_config.yaml"): - return f"# AUTOGENERATED FILE. Only edit for testing purposes, not for development. Generated from {config_file}. Generated by utils/split_config.py\n" + return ( + f"# AUTOGENERATED FILE. Only edit for testing purposes, not " + f"for development. Generated from {config_file}. " + "Generated by utils/split_config.py\n" + ) def update_analysis_config(): @@ -87,7 +91,9 @@ def update_analysis_config(): data_source_config["metric_table"]["metric"], gfx_version, ) - new_panel_config["Panel Config"]["data source"].append(data_source_config) + new_panel_config["Panel Config"]["data source"].append( + data_source_config + ) # Write panel config to file filename = Path( TARGET_DIR.joinpath(gfx_version, f"{panel_id}_{panel_title}.yaml") @@ -134,9 +140,9 @@ def update_sets_config(): } for metric_id in sets["metric"][gfx_version]: - current_set["metric"].append( - {metric_id: METRIC_ID_TO_NAME_MAP[gfx_version][str(metric_id)]} - ) + current_set["metric"].append({ + metric_id: METRIC_ID_TO_NAME_MAP[gfx_version][str(metric_id)] + }) new_sets["sets"].append(current_set) From c994150cf88b8feb2826ed6dcdc2d92490b894c2 Mon Sep 17 00:00:00 2001 From: Vignesh Edithal Date: Tue, 19 Aug 2025 14:11:17 -0400 Subject: [PATCH 3/4] Address review comments --- projects/rocprofiler-compute/CHANGELOG.md | 6 + .../docs/how-to/analyze/cli.rst | 99 +++++++++++++++++ .../docs/how-to/analyze/data-dump.rst | 104 ------------------ .../docs/how-to/analyze/mode.rst | 1 - .../docs/how-to/profile/mode.rst | 34 ++++-- .../rocprofiler-compute/docs/how-to/use.rst | 10 +- .../docs/sphinx/_toc.yml.in | 1 - projects/rocprofiler-compute/src/argparser.py | 41 ++++--- .../rocprof_compute_analyze/analysis_base.py | 32 +++--- .../rocprof_compute_analyze/analysis_db.py | 12 +- .../src/rocprof_compute_base.py | 2 +- .../rocprof_compute_tui/utils/tui_utils.py | 2 +- .../src/utils/analysis_orm.py | 5 +- projects/rocprofiler-compute/src/utils/tty.py | 17 +-- .../rocprofiler-compute/src/utils/utils.py | 4 + 15 files changed, 198 insertions(+), 172 deletions(-) delete mode 100644 projects/rocprofiler-compute/docs/how-to/analyze/data-dump.rst diff --git a/projects/rocprofiler-compute/CHANGELOG.md b/projects/rocprofiler-compute/CHANGELOG.md index 17464ade8138..bf194d5bf879 100644 --- a/projects/rocprofiler-compute/CHANGELOG.md +++ b/projects/rocprofiler-compute/CHANGELOG.md @@ -55,6 +55,12 @@ Full documentation for ROCm Compute Profiler is available at [https://rocm.docs. * sL1D-L2 BW Utilization (section 1401) * Bandwidth Utilization (section 1601) +* Analysis output: + * Remove ``-o`` or ``--output`` analyze mode option. + * Remove ``--save-dfs`` analyze mode option. + * 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. + ### Resolved issues * Fixed not detecting memory clock issue when using amd-smi diff --git a/projects/rocprofiler-compute/docs/how-to/analyze/cli.rst b/projects/rocprofiler-compute/docs/how-to/analyze/cli.rst index 2c494840e309..b5466a2b072e 100644 --- a/projects/rocprofiler-compute/docs/how-to/analyze/cli.rst +++ b/projects/rocprofiler-compute/docs/how-to/analyze/cli.rst @@ -320,3 +320,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/data-dump.rst b/projects/rocprofiler-compute/docs/how-to/analyze/data-dump.rst deleted file mode 100644 index ba0b876846b2..000000000000 --- a/projects/rocprofiler-compute/docs/how-to/analyze/data-dump.rst +++ /dev/null @@ -1,104 +0,0 @@ -.. meta:: - :description: ROCm Compute Profiler analysis: Data dump analysis - :keywords: ROCm Compute Profiler, ROCm, profiler, tool, Instinct, accelerator, analyze, filtering, metrics, baseline, comparison, post analysis, database, sqlite, data, dump, data dump - -****************** -Data dump analysis -****************** - -This section provides an overview of how the data generated in analysis mode can be dumped to a SQLite3 database for custom report generation. - -.. _overview: - -Overview -================= - -.. note:: - - Data dump analysis only works when the given workload(s) folder are created using ``--format-rocprof-output rocpd`` option in profile mode. - -In addition to applying various analysis mode options mentioned in :doc:`cli` to perform kernel, dispatch and metric filtering; Add ``--db `` option to the command. -This will do the following: - -* Create ``.db`` SQLite3 database file in the current working directory - - * Will fail if the file already exists to avoid overwriting - -* Will populate system information, roofline data, PC sampling data and calculate metrics data into the database file - - * Warning will be shown if any information cannot be calculated due to missing data - - * For example, attempting to analyze workload profiled with ``--no-roof`` option will throw a warning for unable to calculate roofline data. - -.. _schema: - -Data dump schema -================ - -Analysis data dump schema -------------------------- - -.. image:: ../../data/analyze/analysis_data_dump_schema.png - :align: center - :alt: Analysis data dump schema - -Analysis data dump views ------------------------- - -.. image:: ../../data/analyze/analysis_data_dump_views.png - :align: center - :alt: Analysis data dump views - - -.. _example: - -Data dump command 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 diff --git a/projects/rocprofiler-compute/docs/how-to/analyze/mode.rst b/projects/rocprofiler-compute/docs/how-to/analyze/mode.rst index 04651ad592b0..dc8dc11d06ab 100644 --- a/projects/rocprofiler-compute/docs/how-to/analyze/mode.rst +++ b/projects/rocprofiler-compute/docs/how-to/analyze/mode.rst @@ -23,7 +23,6 @@ options. * :doc:`grafana-gui` * :doc:`standalone-gui` * :doc:`tui` -* :doc:`data-dump` .. note:: diff --git a/projects/rocprofiler-compute/docs/how-to/profile/mode.rst b/projects/rocprofiler-compute/docs/how-to/profile/mode.rst index 5a0ea4e07361..2e1408d8acf3 100644 --- a/projects/rocprofiler-compute/docs/how-to/profile/mode.rst +++ b/projects/rocprofiler-compute/docs/how-to/profile/mode.rst @@ -24,8 +24,14 @@ 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 `: Collect raw data in ``rocpd`` database - output format and merge them into a single csv file containing counter collection data. +* :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 the future releases of ROCm Compute Profiler. * :ref:`Filtering `: Apply runtime filters to speed up the profiling process. @@ -225,11 +231,25 @@ an Instinct MI210 vs an Instinct MI250. Profiling output format ----------------------- -* Provide ``--format-rocprof-output rocpd`` profile mode option to have the underlying ``rocprof`` collect raw performance counter data in ``rocpd`` database format. - - * ROCm Compute Profiler takes care of merging multiple ``rocpd`` databases into a single csv file containing counter collection data in the workload folder. - - * 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. +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: diff --git a/projects/rocprofiler-compute/docs/how-to/use.rst b/projects/rocprofiler-compute/docs/how-to/use.rst index 764fed63e0f6..4347b5daca8c 100644 --- a/projects/rocprofiler-compute/docs/how-to/use.rst +++ b/projects/rocprofiler-compute/docs/how-to/use.rst @@ -164,21 +164,13 @@ Analyze mode is great if you want immediate access to a hardware component you’re already familiar with. - Use the ``--db `` option in analyze mode to have the post analysis data dumped into a SQLite database - named ``db_name.db``. This can be used to generate custom analysis reports. Note that using this option - will disable any analysis report generation such as CLI analysis report. - .. code-block:: shell - $ rocprof-compute analyze --db db_name -p workload_path_1 -p workload_path_2 + $ rocprof-compute analyze --help See :doc:`analyze/mode` to learn about this mode in depth and to get started with analysis using ROCm Compute Profiler. - .. code-block:: shell - - $ rocprof-compute analyze --help - .. _modes-database: Database mode diff --git a/projects/rocprofiler-compute/docs/sphinx/_toc.yml.in b/projects/rocprofiler-compute/docs/sphinx/_toc.yml.in index 4456146f7136..483bf1c0f584 100644 --- a/projects/rocprofiler-compute/docs/sphinx/_toc.yml.in +++ b/projects/rocprofiler-compute/docs/sphinx/_toc.yml.in @@ -27,7 +27,6 @@ subtrees: - file: how-to/analyze/grafana-gui.rst - file: how-to/analyze/standalone-gui.rst - file: how-to/analyze/tui.rst - - file: how-to/analyze/data-dump.rst - caption: Conceptual entries: diff --git a/projects/rocprofiler-compute/src/argparser.py b/projects/rocprofiler-compute/src/argparser.py index 18b5cf95911a..774032bc1f67 100644 --- a/projects/rocprofiler-compute/src/argparser.py +++ b/projects/rocprofiler-compute/src/argparser.py @@ -635,11 +635,27 @@ def validate_block(value): 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.", + 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", @@ -657,17 +673,6 @@ def validate_block(value): help="\t\tActivate a Textual User Interface (TUI) to " "interact with rocprofiler-compute metrics.", ) - analyze_group.add_argument( - "--db", - type=str, - required=False, - metavar="", - help=( - "\t\tStore the analysis data in a sqlite3 database file\n" - "\t\twith the given name and .db suffix added to it.\n" - "\t\tThis option will disable generation of analysis report in CLI/GUI/TUI." - ), - ) analyze_group.add_argument( "-R", "--roofline-data-type", @@ -769,12 +774,6 @@ def validate_block(value): 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 dbafd5656d63..80e69e51d102 100644 --- a/projects/rocprofiler-compute/src/rocprof_compute_analyze/analysis_base.py +++ b/projects/rocprofiler-compute/src/rocprof_compute_analyze/analysis_base.py @@ -32,7 +32,7 @@ from utils import file_io, parser, schema from utils.logger import console_debug, console_error, console_log, demarcate -from utils.utils import is_workload_empty, merge_counters_spatial_multiplex +from utils.utils import is_workload_empty, merge_counters_spatial_multiplex, get_uuid class OmniAnalyze_Base: @@ -258,17 +258,18 @@ def sanitize(self): print("Node list:", " ".join(nodes)) sys.exit(0) - # Ensure analysis db file only contains alphanumeric - # characters and the file does not exist - if self.__args.db: - if not self.__args.db.isalnum(): + # Ensure analysis output does not overwrite existing files + if self.__args.output_name: + if not self.__args.output_name.isalnum(): console_error( - "Analysis database file name must contain only alphanumeric " - "characters." + "Analysis output file/folder name must contain only alphanumeric characters." ) - if Path(f"{self.__args.db}.db").exists(): + 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 database file {self.__args.db}.db already exists. " + f"Analysis output file/folder {path_to_check} already exists. " "Please choose a different name." ) @@ -281,11 +282,14 @@ def pre_processing(self): 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": + if self.__args.output_name: + self._output = open(f"{self.__args.output_name}.txt", "w+") + print(f"Created file: {self.__args.output_name}.txt") + else: + self._output = open(f"rocprof_compute_{get_uuid()}.txt", "w+") + 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 index 68e4447409bb..3872fdfa3ab1 100644 --- a/projects/rocprofiler-compute/src/rocprof_compute_analyze/analysis_db.py +++ b/projects/rocprofiler-compute/src/rocprof_compute_analyze/analysis_db.py @@ -1,7 +1,7 @@ ##############################################################################bl # MIT License # -# Copyright (c) 2021 - 2025 Advanced Micro Devices, Inc. All Rights Reserved. +# 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 @@ -57,7 +57,7 @@ PEAK_OPS_DATATYPES, SUPPORTED_DATATYPES, ) -from utils.utils import get_version +from utils.utils import get_version, get_uuid class db_analysis(OmniAnalyze_Base): @@ -105,7 +105,12 @@ def run_analysis(self): super().run_analysis() # Initialize analysis database - db_name = Database.init(self.get_args().db) + # 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(): @@ -202,6 +207,7 @@ def run_analysis(self): # Write database Database.write() console_debug("Completed writing database") + print(f"Created file: {db_name}") def calc_roofline_ceilings(self): roofline_ceilings_per_workload = dict() diff --git a/projects/rocprofiler-compute/src/rocprof_compute_base.py b/projects/rocprofiler-compute/src/rocprof_compute_base.py index dda5c8068f4f..a3a90926310f 100644 --- a/projects/rocprofiler-compute/src/rocprof_compute_base.py +++ b/projects/rocprofiler-compute/src/rocprof_compute_base.py @@ -137,7 +137,7 @@ def detect_analyze(self): self.__analyze_mode = "web_ui" elif self.__args.tui: self.__analyze_mode = "tui" - elif self.__args.db: + elif self.__args.output_format == "db": self.__analyze_mode = "db" else: self.__analyze_mode = "cli" 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 fe9e34f5b346..85ec653f0136 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 @@ -123,7 +123,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 = defaultdict(dict) diff --git a/projects/rocprofiler-compute/src/utils/analysis_orm.py b/projects/rocprofiler-compute/src/utils/analysis_orm.py index 1638e6d6e25a..23647d8133ca 100644 --- a/projects/rocprofiler-compute/src/utils/analysis_orm.py +++ b/projects/rocprofiler-compute/src/utils/analysis_orm.py @@ -1,7 +1,7 @@ ##############################################################################bl # MIT License # -# Copyright (c) 2021 - 2025 Advanced Micro Devices, Inc. All Rights Reserved. +# 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 @@ -165,8 +165,7 @@ class Database: _session = None @classmethod - def init(cls, name): - db_name = f"{name}.db" + def init(cls, db_name): engine = create_engine(f"sqlite:///{db_name}") Base.metadata.create_all(engine) cls._session = sessionmaker(bind=engine)() diff --git a/projects/rocprofiler-compute/src/utils/tty.py b/projects/rocprofiler-compute/src/utils/tty.py index fbedf7c8808f..f394222aad73 100644 --- a/projects/rocprofiler-compute/src/utils/tty.py +++ b/projects/rocprofiler-compute/src/utils/tty.py @@ -33,7 +33,7 @@ import config from utils import mem_chart, parser 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): @@ -388,17 +388,20 @@ 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 args.output_format == "csv": + if args.output_name: + p = Path(f"{args.output_name}") + else: + p = Path(f"rocprof_compute_{get_uuid()}") 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, - ) + csv_filename = p.joinpath(table_id_str.replace(" ", "_") + ".csv"), + df.to_csv(csv_filename, index=False) + print(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 d2d8c41df02b..c96886947925 100644 --- a/projects/rocprofiler-compute/src/utils/utils.py +++ b/projects/rocprofiler-compute/src/utils/utils.py @@ -38,6 +38,7 @@ import time from pathlib import Path as path from typing import Optional +import uuid import pandas as pd import yaml @@ -1640,3 +1641,6 @@ 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] From d948e810e436f0422beb59057818b11a1c0ffea9 Mon Sep 17 00:00:00 2001 From: Vignesh Edithal Date: Tue, 19 Aug 2025 15:44:56 -0400 Subject: [PATCH 4/4] fix bugs --- projects/rocprofiler-compute/CHANGELOG.md | 2 +- projects/rocprofiler-compute/docs/how-to/use.rst | 4 ++-- projects/rocprofiler-compute/tests/test_profile_general.py | 5 ++++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/projects/rocprofiler-compute/CHANGELOG.md b/projects/rocprofiler-compute/CHANGELOG.md index bf194d5bf879..84bbe6acaa1e 100644 --- a/projects/rocprofiler-compute/CHANGELOG.md +++ b/projects/rocprofiler-compute/CHANGELOG.md @@ -18,7 +18,7 @@ Full documentation for ROCm Compute Profiler is available at [https://rocm.docs. * Enable SQC_DCACHE_INFLIGHT_LEVEL counter and associated metrics * Enable TCP_TCP_LATENCY counter and associated counter for all GPUs except MI300 -* Add `--db` option to analyze mode which creates analysis database file for further reporting +* Add support for analysis report output as a sqlite database using ``--output-format db`` analysis mode option ### Changed diff --git a/projects/rocprofiler-compute/docs/how-to/use.rst b/projects/rocprofiler-compute/docs/how-to/use.rst index 4347b5daca8c..594939521f11 100644 --- a/projects/rocprofiler-compute/docs/how-to/use.rst +++ b/projects/rocprofiler-compute/docs/how-to/use.rst @@ -168,8 +168,8 @@ Analyze mode $ rocprof-compute analyze --help - See :doc:`analyze/mode` to learn about this mode in depth and to get started - with analysis using ROCm Compute Profiler. +See :doc:`analyze/mode` to learn about this mode in depth and to get started +with analysis using ROCm Compute Profiler. .. _modes-database: diff --git a/projects/rocprofiler-compute/tests/test_profile_general.py b/projects/rocprofiler-compute/tests/test_profile_general.py index fad3512fe8cb..06b6684cd011 100644 --- a/projects/rocprofiler-compute/tests/test_profile_general.py +++ b/projects/rocprofiler-compute/tests/test_profile_general.py @@ -754,7 +754,10 @@ def test_analyze_rocpd( db_name = "test" code = binary_handler_analyze_rocprof_compute([ "analyze", - "--db", + "--output-format", + "db", + "--output-name", + f"{db_name}", db_name, "--path", workload_dir,