Integrate roofline in tui (#762)
* Update changelog, remove unused code. * enable roofline in TUI. * Remove roofline section when data not available. * Fix workload dir path.
Этот коммит содержится в:
+1
-1
@@ -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`
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
@@ -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"))
|
||||
|
||||
+84
-35
@@ -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+",
|
||||
|
||||
@@ -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
|
||||
# -----------------------------------------------------
|
||||
|
||||
Ссылка в новой задаче
Block a user