From ed877433f3a398328dd08a4c9dc7113e829a19ed Mon Sep 17 00:00:00 2001 From: xuchen-amd Date: Fri, 22 Aug 2025 11:02:32 -0400 Subject: [PATCH] [rocprofiler-compute][TUI] Updates and refactor. (#703) --- .../src/rocprof_compute_base.py | 2 +- .../src/rocprof_compute_tui/analysis_tui.py | 128 ++++++++++-------- .../src/rocprof_compute_tui/tui_app.py | 125 +++++------------ .../utils/kernel_view_config.yaml | 3 + 4 files changed, 108 insertions(+), 150 deletions(-) diff --git a/projects/rocprofiler-compute/src/rocprof_compute_base.py b/projects/rocprofiler-compute/src/rocprof_compute_base.py index ab9021d5d2..d2920f5a82 100644 --- a/projects/rocprofiler-compute/src/rocprof_compute_base.py +++ b/projects/rocprofiler-compute/src/rocprof_compute_base.py @@ -230,7 +230,7 @@ class RocProfCompute: if arch in self.__supported_archs.keys(): ac = schema.ArchConfig() ac.panel_configs = file_io.load_panel_configs( - self.__args.config_dir.joinpath(arch) + [self.__args.config_dir.joinpath(arch)] ) sys_info = self.__mspec.get_class_members().iloc[0] parser.build_dfs(archConfigs=ac, filter_metrics=[], sys_info=sys_info) diff --git a/projects/rocprofiler-compute/src/rocprof_compute_tui/analysis_tui.py b/projects/rocprofiler-compute/src/rocprof_compute_tui/analysis_tui.py index 7f0fe62039..c5c05803ce 100644 --- a/projects/rocprofiler-compute/src/rocprof_compute_tui/analysis_tui.py +++ b/projects/rocprofiler-compute/src/rocprof_compute_tui/analysis_tui.py @@ -26,6 +26,8 @@ import copy from pathlib import Path +import pandas as pd + from rocprof_compute_analyze.analysis_base import OmniAnalyze_Base from rocprof_compute_tui.utils.tui_utils import ( get_top_kernels_and_dispatch_ids, @@ -40,9 +42,8 @@ class tui_analysis(OmniAnalyze_Base): def __init__(self, args, supported_archs, path): super().__init__(args, supported_archs) self.path = str(path) - self.arch = None + self.args = self.get_args() self.raw_dfs = {} - self.kernel_dfs = {} # ----------------------- # Required child methods @@ -50,99 +51,108 @@ class tui_analysis(OmniAnalyze_Base): @demarcate def pre_processing(self): self._profiling_config = file_io.load_profiling_config(self.path) - self._runs = self.initalize_runs() - if self.get_args().random_port: + if self.args.random_port: console_error("--gui flag is required to enable --random-port") - self._runs[self.path].raw_pmc = file_io.create_df_pmc( + # Process PMC data + workload = self._runs[self.path] + + workload.raw_pmc = file_io.create_df_pmc( self.path, - self.get_args().nodes, - self.get_args().spatial_multiplexing, - self.get_args().kernel_verbose, - self.get_args().verbose, + self.args.nodes, + self.args.spatial_multiplexing, + self.args.kernel_verbose, + self.args.verbose, self._profiling_config, ) - if self.get_args().spatial_multiplexing: - self._runs[self.path].raw_pmc = self.spatial_multiplex_merge_counters( - self._runs[self.path].raw_pmc - ) + if self.args.spatial_multiplexing: + workload.raw_pmc = self.spatial_multiplex_merge_counters(workload.raw_pmc) file_io.create_df_kernel_top_stats( - df_in=self._runs[self.path].raw_pmc, + df_in=workload.raw_pmc, raw_data_dir=self.path, - filter_gpu_ids=self._runs[self.path].filter_gpu_ids, - filter_dispatch_ids=self._runs[self.path].filter_dispatch_ids, - filter_nodes=self._runs[self.path].filter_nodes, - time_unit=self.get_args().time_unit, - max_stat_num=self.get_args().max_stat_num, - kernel_verbose=self.get_args().kernel_verbose, - ) - - kernel_name_shortener( - self._runs[self.path].raw_pmc, self.get_args().kernel_verbose + filter_gpu_ids=workload.filter_gpu_ids, + filter_dispatch_ids=workload.filter_dispatch_ids, + filter_nodes=workload.filter_nodes, + time_unit=self.args.time_unit, + max_stat_num=self.args.max_stat_num, + kernel_verbose=self.args.kernel_verbose, ) + kernel_name_shortener(self._runs[self.path].raw_pmc, self.args.kernel_verbose) # 1. load top kernel parser.load_kernel_top( - workload=self._runs[self.path], dir=self.path, args=self.get_args() + workload=self._runs[self.path], dir=self.path, args=self.args ) - # 2. load table data for each kernel - self.raw_dfs.clear() - for idx in self._runs[self.path].raw_pmc.index: - kernel_df = self._runs[self.path].raw_pmc.loc[[idx]] + # 2. Generate kernel-specific dataframes + self.raw_dfs = {} + for idx in workload.raw_pmc.index: + kernel_df = workload.raw_pmc.loc[[idx]] kernel_name = kernel_df.pmc_perf["Kernel_Name"].loc[idx] - this_dfs = copy.deepcopy(self._runs[self.path].dfs) + kernel_dfs = copy.deepcopy(workload.dfs) + parser.eval_metric( - this_dfs, - self._runs[self.path].dfs_type, - self._runs[self.path].sys_info.iloc[0], + kernel_dfs, + workload.dfs_type, + workload.sys_info.iloc[0], + workload.roofline_peaks, kernel_df, - self.get_args().debug, + self.args.debug, self._profiling_config, ) - self.raw_dfs[kernel_name] = this_dfs + self.raw_dfs[kernel_name] = kernel_dfs def initalize_runs(self, normalization_filter=None): - sysinfo_path = Path(self.path) - sys_info = file_io.load_sys_info(sysinfo_path.joinpath("sysinfo.csv")) - self.arch = sys_info.iloc[0]["gpu_arch"] - args = self.get_args() + # Load system info and configure + sys_info = file_io.load_sys_info(Path(self.path) / "sysinfo.csv") + arch = sys_info.iloc[0]["gpu_arch"] + self.generate_configs( - self.arch, - args.config_dir, - args.list_stats, - args.filter_metrics, + arch, + self.args.config_dir, + self.args.list_stats, + self.args.filter_metrics, sys_info.iloc[0], ) - self.load_options(normalization_filter) + # Create workload with system and roofline data w = schema.Workload() - w.sys_info = file_io.load_sys_info(sysinfo_path.joinpath("sysinfo.csv")) - mspec = self.get_socs()[self.arch]._mspec - if args.specs_correction: - w.sys_info = parser.correct_sys_info(mspec, args.specs_correction) - w.avail_ips = w.sys_info["ip_blocks"].item().split("|") - w.dfs = copy.deepcopy(self._arch_configs[self.arch].dfs) - w.dfs_type = self._arch_configs[self.arch].dfs_type - self._runs[self.path] = w + w.sys_info = ( + parser.correct_sys_info( + self.get_socs()[arch]._mspec, self.args.specs_correction + ) + if self.args.specs_correction + else sys_info + ) + roofline_path = Path(self.path) / "roofline.csv" + w.roofline_peaks = ( + pd.read_csv(roofline_path) + if not getattr(self.args, "no_roof", False) and roofline_path.exists() + else pd.DataFrame() + ) + + w.avail_ips = w.sys_info["ip_blocks"].item().split("|") + w.dfs = copy.deepcopy(self._arch_configs[arch].dfs) + w.dfs_type = self._arch_configs[arch].dfs_type + + self._runs[self.path] = w return self._runs - @demarcate def run_kernel_analysis(self): - self.kernel_dfs.clear() - for kernel_name, df in self.raw_dfs.items(): - self.kernel_dfs[kernel_name] = process_panels_to_dataframes( - self.get_args(), df, self._arch_configs[self.arch], roof_plot=None + arch = list(self._arch_configs.keys())[0] + return { + kernel_name: process_panels_to_dataframes( + self.args, df, self._arch_configs[arch], roof_plot=None ) - return self.kernel_dfs + for kernel_name, df in self.raw_dfs.items() + } - @demarcate def run_top_kernel(self): return get_top_kernels_and_dispatch_ids(self._runs) diff --git a/projects/rocprofiler-compute/src/rocprof_compute_tui/tui_app.py b/projects/rocprofiler-compute/src/rocprof_compute_tui/tui_app.py index eda40d8aca..5caffa8e3f 100644 --- a/projects/rocprofiler-compute/src/rocprof_compute_tui/tui_app.py +++ b/projects/rocprofiler-compute/src/rocprof_compute_tui/tui_app.py @@ -29,12 +29,10 @@ ROCm Compute Profiler TUI - Main Application with Analysis Methods import importlib import json -import os from pathlib import Path -from typing import Any, Dict, List, Optional from textual import on, work -from textual.app import App, ComposeResult +from textual.app import App from textual.binding import Binding from textual.widgets import Button, Footer, Header from textual_fspicker import SelectDirectory @@ -43,7 +41,7 @@ import config from rocprof_compute_tui.config import APP_TITLE from rocprof_compute_tui.views.main_view import MainView from rocprof_compute_tui.widgets.menu_bar.menu_bar import DropdownMenu -from utils.specs import MachineSpecs, generate_machine_specs +from utils.specs import generate_machine_specs from utils.utils import get_version @@ -62,123 +60,70 @@ class RocprofTUIApp(App): # Binding(key="a", action="analyze", description="Analyze"), ] - def __init__( - self, args: Optional[Any] = None, supported_archs: Optional[Dict] = None - ) -> None: - """ - Initialize the application. - """ + def __init__(self, args=None, supported_archs=None): super().__init__() self.main_view = MainView() + self.recent_dirs = self._load_recent_dirs() - self.recent_file = Path.home() / ".textual_browser_recent.json" - self.recent_dirs: List[str] = [] - self.current_path = "" - self.load_recent_directories() - - # Initialize analysis-related attributes + # Analysis attributes self.args = args self.supported_archs = supported_archs or {} - self.soc: Dict = {} - self.mspec: Optional[MachineSpecs] = None + self.soc = {} + self.mspec = None - def compose(self) -> ComposeResult: - """Compose the application layout.""" + def compose(self): yield Header() yield self.main_view yield Footer() - def action_refresh(self) -> None: - """Refresh the view.""" - try: - self.main_view.refresh_view() - except Exception as e: - self.notify(f"Refresh failed: {str(e)}", severity="error") + def action_refresh(self): + self.main_view.refresh_view() - def load_soc_specs(self, sysinfo: dict = None) -> None: - """ - Load OmniSoC instance for analysis. - """ + def load_soc_specs(self, sysinfo=None): self.mspec = generate_machine_specs(self.args, sysinfo) - - if self.args and self.args.specs: - print(self.mspec) - return - arch = self.mspec.gpu_arch - - # Dynamically import and instantiate the SoC class - soc_module = importlib.import_module("rocprof_compute_soc.soc_" + arch) - soc_class = getattr(soc_module, arch + "_soc") + soc_module = importlib.import_module(f"rocprof_compute_soc.soc_{arch}") + soc_class = getattr(soc_module, f"{arch}_soc") self.soc[arch] = soc_class(self.args, self.mspec) - def get_soc(self) -> Dict: - """Get the SoC dictionary.""" - return self.soc + def _load_recent_dirs(self): + recent_file = Path.home() / ".textual_browser_recent.json" + if recent_file.exists(): + with open(recent_file, "r") as f: + return json.load(f) + return [] - def get_mspec(self) -> Optional[MachineSpecs]: - """Get the machine specifications.""" - return self.mspec + def _save_recent_dirs(self): + recent_file = Path.home() / ".textual_browser_recent.json" + with open(recent_file, "w") as f: + json.dump(self.recent_dirs, f, indent=2) - def load_recent_directories(self) -> None: - """Load recent directories from file.""" - try: - if self.recent_file.exists(): - with open(self.recent_file, "r") as f: - self.recent_dirs = json.load(f) - except (json.JSONDecodeError, FileNotFoundError): - self.recent_dirs = [] + def add_recent_dir(self, directory): + directory = str(Path(directory).absolute()) - def save_recent_directories(self) -> None: - """Save recent directories to file.""" - try: - with open(self.recent_file, "w") as f: - json.dump(self.recent_dirs, f, indent=2) - except Exception as e: - self.notify(f"Failed to save recent directories: {e}", severity="error") - - def add_to_recent(self, directory: str) -> None: - """Add directory to recent list (FIFO, max 5 items).""" - directory = os.path.abspath(directory) - - # Remove if already exists + # Remove if exists, add to front, keep max 5 if directory in self.recent_dirs: self.recent_dirs.remove(directory) - - # Add to front - # TODO: should we check to if workload dir can be successfully loaded? self.recent_dirs.insert(0, directory) - - # Keep only last 5 self.recent_dirs = self.recent_dirs[:5] + self._save_recent_dirs() - # Save to file - self.save_recent_directories() - - def on_recent_selected(self, selected_dir: str) -> None: + def on_recent_selected(self, selected_dir): if selected_dir: self.main_view.selected_path = selected_dir self.main_view.run_analysis() @on(Button.Pressed, "#menu-open-workload") @work - async def pick_a_directory(self) -> None: + async def pick_directory(self): if opened := await self.push_screen_wait(SelectDirectory()): - self.add_to_recent(str(opened)) + self.add_recent_dir(str(opened)) self.main_view.selected_path = opened - dropdown = self.query_one("#file-dropdown", DropdownMenu) - dropdown.add_class("hidden") + self.query_one("#file-dropdown", DropdownMenu).add_class("hidden") self.main_view.run_analysis() -def run_tui(args: Optional[Any] = None, supported_archs: Optional[list] = None) -> None: - """ - Run the TUI application. - """ - try: - app = RocprofTUIApp(args, supported_archs) - app.run() - except KeyboardInterrupt: - pass - except Exception as e: - raise RuntimeError(f"Failed to run TUI application: {str(e)}") from e +def run_tui(args=None, supported_archs=None): + """Run the TUI application.""" + app = RocprofTUIApp(args, supported_archs) + app.run() diff --git a/projects/rocprofiler-compute/src/rocprof_compute_tui/utils/kernel_view_config.yaml b/projects/rocprofiler-compute/src/rocprof_compute_tui/utils/kernel_view_config.yaml index c5dbee67f0..654b43e373 100644 --- a/projects/rocprofiler-compute/src/rocprof_compute_tui/utils/kernel_view_config.yaml +++ b/projects/rocprofiler-compute/src/rocprof_compute_tui/utils/kernel_view_config.yaml @@ -32,6 +32,9 @@ sections: - "2. System Speed-of-Light" - "3. Memory Chart" - "4. Roofline" + - "32. GPU Speed-of-Light" + - "33. Compute Throughput" + - "34. Memory Throughput" collapsed: true - title: "Source Level Analysis"