diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c1e0fc4db..f7b142e353 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ Full documentation for ROCm Compute Profiler is available at [https://rocm.docs. * Sorting of PC sampling by type: offset or count. -* Add rocprof-compute Text User Interface (TUI) support for analyze mode +* Add rocprof-compute Text User Interface (TUI) support for analyze mode (beta version) * A command line based user interface to support interactive single-run analysis * launch with `--tui` option in analyze mode. i.e., `rocprof-compute analyze --tui` diff --git a/src/rocprof_compute_tui/analysis_tui.py b/src/rocprof_compute_tui/analysis_tui.py index ffe755c550..b47755b4d0 100644 --- a/src/rocprof_compute_tui/analysis_tui.py +++ b/src/rocprof_compute_tui/analysis_tui.py @@ -37,6 +37,7 @@ class tui_analysis(OmniAnalyze_Base): def __init__(self, args, supported_archs, path): super().__init__(args, supported_archs) self.path = str(path) + self.arch = None # ----------------------- # Required child methods @@ -95,10 +96,10 @@ class tui_analysis(OmniAnalyze_Base): # load required configs sysinfo_path = Path(self.path) sys_info = file_io.load_sys_info(sysinfo_path.joinpath("sysinfo.csv")) - arch = sys_info.iloc[0]["gpu_arch"] + self.arch = sys_info.iloc[0]["gpu_arch"] args = self.get_args() self.generate_configs( - arch, + self.arch, args.config_dir, args.list_stats, args.filter_metrics, @@ -112,15 +113,13 @@ class tui_analysis(OmniAnalyze_Base): # For regular single node case, load sysinfo.csv directly # For multi-node, either the default "all", or specified some, # pick up the one in the 1st sub_dir. We could fix it properly later. - sysinfo_path = Path(self.path) w.sys_info = file_io.load_sys_info(sysinfo_path.joinpath("sysinfo.csv")) - arch = w.sys_info.iloc[0]["gpu_arch"] - mspec = self.get_socs()[arch]._mspec + 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[arch].dfs) - w.dfs_type = self._arch_configs[arch].dfs_type + 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 return self._runs @@ -130,10 +129,39 @@ class tui_analysis(OmniAnalyze_Base): """Run TUI analysis.""" super().run_analysis() + roof_plot = None + # 1. check if not baseline && compatible soc: + if self.arch in [ + # >= MI200 + "gfx90a", + "gfx940", + "gfx941", + "gfx942", + "gfx950", + ]: + # add roofline plot to cli output + self.get_socs()[self.arch].analysis_setup( + roofline_parameters={ + "workload_dir": self.path, + "device_id": 0, + "sort_type": "kernels", + "mem_level": "ALL", + "include_kernel_names": False, + "is_standalone": False, + "roofline_data_type": "FP32", + } + ) + roof_obj = self.get_socs()[self.arch].roofline_obj + + if roof_obj: + # NOTE: using default data type + roof_plot = roof_obj.cli_generate_plot(roof_obj.get_dtype()[0]) + results = process_panels_to_dataframes( self.get_args(), self._runs, - self._arch_configs[self._runs[self.path].sys_info.iloc[0]["gpu_arch"]], + self._arch_configs[self.arch], self._profiling_config, + roof_plot=roof_plot, ) return results diff --git a/src/rocprof_compute_tui/utils/tui_utils.py b/src/rocprof_compute_tui/utils/tui_utils.py index ef3f87e6d8..c4179c2c3d 100644 --- a/src/rocprof_compute_tui/utils/tui_utils.py +++ b/src/rocprof_compute_tui/utils/tui_utils.py @@ -287,7 +287,9 @@ def get_table_dfs(): return section_dfs -def process_panels_to_dataframes(args, runs, archConfigs, profiling_config): +def process_panels_to_dataframes( + args, runs, archConfigs, profiling_config, roof_plot=None +): """ Process panel data into pandas DataFrames. Returns a nested dictionary structure with DataFrames and tui_style information. @@ -416,7 +418,7 @@ def process_panels_to_dataframes(args, runs, archConfigs, profiling_config): # Save to CSV if requested if args.df_file_dir: save_dataframe_to_csv(df, table_id_str, table_config, args) - + result_structure["roofline"] = roof_plot return dict(result_structure) diff --git a/src/rocprof_compute_tui/views/main_view.py b/src/rocprof_compute_tui/views/main_view.py index f797e3a90a..dbd28720de 100644 --- a/src/rocprof_compute_tui/views/main_view.py +++ b/src/rocprof_compute_tui/views/main_view.py @@ -237,6 +237,10 @@ class MainView(Horizontal): else: self.app.call_from_thread(self.refresh_results) self.logger.info("Step 8: Analysis completed successfully") + if self.dfs["roofline"]: + self.logger.info("Step 8: Roofline data available") + else: + self.logger.info("Step 8: Roofline data not available") except Exception as e: self.logger.error(f"Step 8 failed - Error running analysis: {str(e)}") raise diff --git a/src/rocprof_compute_tui/widgets/charts.py b/src/rocprof_compute_tui/widgets/charts.py index 806e615748..c1c791ba12 100644 --- a/src/rocprof_compute_tui/widgets/charts.py +++ b/src/rocprof_compute_tui/widgets/charts.py @@ -263,6 +263,45 @@ def px_simple_multi_bar(df, title=None, id=None): return dfigs +class RooflinePlot(Static): + """Roofline Plot visualization widget.""" + + DEFAULT_CSS = """ + RooflinePlot { + border: solid $accent; + padding: 0; + width: auto; + height: auto; + overflow-y: auto; + overflow-x: auto; + background: $surface; + color: $text; + } + """ + + def __init__(self, df: pd.DataFrame, **kwargs): + """Initialize the roofline plot""" + super().__init__("", classes="roofline", **kwargs) + self.df = df + + # Disable markup rendering + self._render_markup = False + + try: + plot_str = "" + try: + result = self.df["roofline"] + if result: + plot_str = str(result) + except: + plot_str = "No roofline data generated" + + self.update(plot_str) + except Exception as e: + error_message = f"Roofline plot error: {str(e)}\n{traceback.format_exc()}" + self.update(error_message) + + class MemoryChart(Static): """Memory chart visualization widget.""" @@ -317,95 +356,6 @@ class MemoryChart(Static): self.update(f"Error: {str(error_message)}") -class RooflinePlot(PlotextPlot): - """ - HACK: will be replaced with real roof line plot - Roofline plot visualization widget. - """ - - DEFAULT_CSS = """ - RooflinePlot { - padding: 1; - width: auto; - height: auto; - background: $surface; - color: $text; - border: solid $accent; - } - """ - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.styles.height = "75%" - self.styles.width = "75%" - self.plot_initialized = False - self.plt.theme("pro") - - def on_mount(self): - self.refresh_plot() - - def refresh_plot(self): - # Get the current size of this widget - plot_width, plot_height = self.size - - # Configure the plot size - self.plt.plot_size(plot_width, plot_height) - self.create_roofline() - self.plot_initialized = True - - def on_resize(self): - if self.plot_initialized: - self.refresh_plot() - - def create_roofline(self): - """Generate the roofline plot data and visualization.""" - # Roofline model parameters - peak_performance = 1000.0 # GFLOPS - memory_bandwidth = 100.0 # GB/s - - # For memory-bound region (diagonal line) - x_mem = [0.1, 0.5, 1.0, 5.0, 10.0] - y_mem = [x * memory_bandwidth for x in x_mem] - - # For compute-bound region (horizontal line) - x_comp = [10.0, 20.0, 50.0, 100.0] - y_comp = [peak_performance] * len(x_comp) - - # Example workloads with safe values - workloads = [ - (0.5, 45.0, "Workload A"), # Memory bound - (2.0, 180.0, "Workload B"), # Memory bound - (15.0, 950.0, "Workload C"), # Compute bound - (30.0, 980.0, "Workload D"), # Compute bound - ] - - # Clear the plot and set properties - self.plt.clear_figure() - self.plt.title("Roofline Model (🚧 Under Construction)") - self.plt.xlabel("Arithmetic Intensity (FLOPs/Byte)") - self.plt.ylabel("Performance (GFLOP/sec)") - - # Plot memory-bound and compute-bound lines - self.plt.plot(x_mem, y_mem, label="Memory Bound") - self.plt.plot(x_comp, y_comp, label="Compute Bound") - - # Add workload points - workload_x = [w[0] for w in workloads] - workload_y = [w[1] for w in workloads] - workload_names = [w[2] for w in workloads] - - # Plot workload points one by one to avoid errors - for i in range(len(workload_x)): - self.plt.scatter([workload_x[i]], [workload_y[i]], label=workload_names[i]) - - # Set a reasonable view range - self.plt.xlim(0, 100) - self.plt.ylim(0, 1100) - - # Draw the plot - self.refresh() - - class SimpleBar(Static): """Simple Bar visualization widget.""" diff --git a/src/rocprof_compute_tui/widgets/collapsibles.py b/src/rocprof_compute_tui/widgets/collapsibles.py index 7690653a21..efe1d97dd3 100644 --- a/src/rocprof_compute_tui/widgets/collapsibles.py +++ b/src/rocprof_compute_tui/widgets/collapsibles.py @@ -93,10 +93,6 @@ def create_widget_from_data(df: pd.DataFrame, tui_style: Optional[str] = None) - case "mem_chart": return MemoryChart(df) - case "roofline": - # TODO: implement real roofline plot - pass - case "simple_bar": return SimpleBar(df) @@ -150,12 +146,12 @@ def build_subsection( widgets.append(widget) collapsible = Collapsible(*widgets, title=title, collapsed=collapsed) - - # HACK: only because no real roofline data right now elif tui_style == "roofline": - widget = VerticalScroll(RooflinePlot()) - collapsible = Collapsible(widget, title=title, collapsed=collapsed) - + if dfs["roofline"]: + widget = RooflinePlot(dfs) + collapsible = Collapsible(widget, title=title, collapsed=collapsed) + else: + return None # Fallback for subsections without data or style else: collapsible = Collapsible( @@ -228,7 +224,8 @@ def build_section_from_config( for subsection_config in section_config["subsections"]: try: subsection = build_subsection(subsection_config, dfs) - children.append(subsection) + if subsection: + children.append(subsection) except Exception as e: error_msg = f"{subsection_config.get('title', 'Unknown')} error: {str(e)}" children.append(Label(error_msg, classes="warning")) diff --git a/src/roofline.py b/src/roofline.py index 37b0e7f3af..8f4327a881 100644 --- a/src/roofline.py +++ b/src/roofline.py @@ -109,27 +109,57 @@ class Roofline: def roof_setup(self): # Setup the workload directory for roofline profiling. workload_dir_val = self.__run_parameters.get("workload_dir") - if ( - workload_dir_val - and Path(workload_dir_val).name == "workloads" - and Path(workload_dir_val).parent == Path(os.getcwd()) - ): - app_name = getattr(self.__args, "name", "default_app_name") - gpu_model_name = getattr(self.__mspec, "gpu_model", "default_gpu_model") - self.__run_parameters["workload_dir"] = str( - Path(workload_dir_val).joinpath( - app_name, - gpu_model_name, - ) - ) - current_workload_dir = self.__run_parameters.get("workload_dir") - if current_workload_dir: - Path(current_workload_dir).mkdir(parents=True, exist_ok=True) - else: + if not workload_dir_val: console_error( "Workload directory is not set. Cannot perform setup.", exit=False ) + return + + if isinstance(workload_dir_val, list): + if not workload_dir_val or not workload_dir_val[0]: + console_error( + "Workload directory list is empty or invalid. Cannot perform setup.", + exit=False, + ) + return + # Handle nested list structure [0][0] or simple list [0] + base_dir = ( + workload_dir_val[0][0] + if isinstance(workload_dir_val[0], (list, tuple)) + else workload_dir_val[0] + ) + else: + # workload_dir_val is a string + base_dir = workload_dir_val + + base_path = Path(base_dir) + + if base_path.name == "workloads" and base_path.parent == Path(os.getcwd()): + + app_name = getattr(self.__args, "name", "default_app_name") + gpu_model_name = getattr(self.__mspec, "gpu_model", "default_gpu_model") + + # Create the new path + new_path = base_path / app_name / gpu_model_name + + # Update workload_dir with the new path, maintaining original data structure + if isinstance(workload_dir_val, list): + # Update the nested list structure + if isinstance(workload_dir_val[0], (list, tuple)): + self.__run_parameters["workload_dir"][0][0] = str(new_path) + else: + self.__run_parameters["workload_dir"][0] = str(new_path) + else: + # Update string value + self.__run_parameters["workload_dir"] = str(new_path) + + final_dir = str(new_path) + else: + final_dir = base_dir + + # Create the directory + Path(final_dir).mkdir(parents=True, exist_ok=True) @demarcate def empirical_roofline( @@ -593,37 +623,56 @@ class Roofline: ) return + # Normalize workload_dir to get the base directory + workload_dir = self.__run_parameters.get("workload_dir") + if workload_dir is None: + console_error( + "workload_dir is not set", + exit=False, + ) + return + + # Extract base directory path regardless of whether workload_dir is list or string + if isinstance(workload_dir, list): + if not workload_dir or not workload_dir[0]: + console_error( + "workload_dir list is empty or contains invalid entries", + exit=False, + ) + return + # Handle nested list structure [0][0] or simple list [0] + base_dir = ( + workload_dir[0][0] + if isinstance(workload_dir[0], (list, tuple)) + else workload_dir[0] + ) + else: + # workload_dir is a string + base_dir = workload_dir + self.roof_setup() + + # Convert to Path object for easier manipulation + base_path = Path(base_dir) + # Check proper datatype input - takes single str if not isinstance(dtype, str): console_error("Unsupported datatype input - must be str") - if ( - not isinstance(self.__run_parameters["workload_dir"], list) - and self.__run_parameters["workload_dir"] != None - ): - self.roof_setup() - # Change vL1D to a interpretable str, if required if "vL1D" in self.__run_parameters["mem_level"]: self.__run_parameters["mem_level"].remove("vL1D") self.__run_parameters["mem_level"].append("L1") - roofline_csv = str( - Path(self.__run_parameters["workload_dir"][0][0]).joinpath("roofline.csv") - ) - roofline_csv_exists = Path(roofline_csv).is_file() - if not roofline_csv_exists: + roofline_csv = base_path / "roofline.csv" + if not roofline_csv.is_file(): console_log("roofline", "{} does not exist".format(roofline_csv)) return - app_path = str( - Path(self.__run_parameters["workload_dir"][0][0]).joinpath("pmc_perf.csv") - ) - roofline_exists = Path(app_path).is_file() - if not roofline_exists: - console_error("roofline", "{} does not exist".format(app_path)) + pmc_perf_csv = base_path / "pmc_perf.csv" + if not pmc_perf_csv.is_file(): + console_error("roofline", "{} does not exist".format(pmc_perf_csv)) t_df = OrderedDict() - t_df["pmc_perf"] = pd.read_csv(app_path) + t_df["pmc_perf"] = pd.read_csv(pmc_perf_csv) color_scheme = { "HBM": "blue+", diff --git a/src/utils/roofline_calc.py b/src/utils/roofline_calc.py index b599f10f3c..c141c349ad 100644 --- a/src/utils/roofline_calc.py +++ b/src/utils/roofline_calc.py @@ -590,9 +590,18 @@ def calc_ai(mspec, sort_type, ret_df): def constuct_roof(roofline_parameters, dtype): - benchmark_results = str( - Path(roofline_parameters["workload_dir"][0][0]).joinpath("roofline.csv") - ) + workload_dir = roofline_parameters.get("workload_dir") + if isinstance(workload_dir, list): + base_dir = ( + workload_dir[0][0] + if isinstance(workload_dir[0], (list, tuple)) + else workload_dir[0] + ) + else: + base_dir = workload_dir + + benchmark_results = str(Path(base_dir) / "roofline.csv") + # ----------------------------------------------------- # Initialize roofline data dictionary from roofline.csv # -----------------------------------------------------