Files
rocm-systems/projects/rocprofiler-compute/src/roofline.py
T
2025-09-12 16:24:44 -04:00

1010 строки
37 KiB
Python

##############################################################################
# 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.
##############################################################################
import argparse
import textwrap
import time
from abc import abstractmethod
from collections import OrderedDict
from pathlib import Path
from typing import Any, Optional, Union
import numpy as np
import pandas as pd
import plotext as plt
import plotly.graph_objects as go
from dash import dcc, html
from utils import file_io, rocpd_data, schema
from utils.logger import (
console_debug,
console_error,
console_log,
console_warning,
demarcate,
)
from utils.roofline_calc import (
MFMA_DATATYPES,
PEAK_OPS_DATATYPES,
SUPPORTED_DATATYPES,
calc_ai_analyze,
calc_ai_profile,
construct_roof,
)
from utils.specs import MachineSpecs
SYMBOLS = [0, 1, 2, 3, 4, 5, 13, 17, 18, 20]
def wrap_text(text: str, width: int = 92) -> str:
"""
Wraps text using textwrap and joins lines with <br> for Plotly.
"""
if not isinstance(text, str):
text = str(text)
wrapped_lines = textwrap.wrap(
text, width=width, break_long_words=True, replace_whitespace=False
)
return "<br>".join(wrapped_lines)
def to_int(value: Union[float, None]) -> Union[int, float]:
if value is None:
return np.nan
return int(value)
class Roofline:
def __init__(
self,
args: argparse.Namespace,
mspec: MachineSpecs,
run_parameters: Optional[dict[str, Any]] = None,
) -> None:
self.__args = args
self.__mspec = mspec
self.__run_parameters = (
run_parameters
if run_parameters
else {
"workload_dir": None, # in some cases (i.e. --specs),
# path will not be given
"device_id": 0,
"sort_type": "kernels",
"mem_level": "ALL",
"include_kernel_names": False,
"is_standalone": False,
"roofline_data_type": ["FP32"], # default to FP32
"kernel_filter": False,
}
)
self.__ai_data: Optional[dict[str, Any]] = None
self.__ceiling_data: Optional[dict[str, Any]] = None
self.__figure = go.Figure()
# Set roofline run parameters from args
if hasattr(self.__args, "path") and not run_parameters:
self.__run_parameters["workload_dir"] = self.__args.path
if hasattr(self.__args, "no_roof") and not self.__args.no_roof:
self.__run_parameters["is_standalone"] = True
if hasattr(self.__args, "kernel_names") and self.__args.kernel_names:
self.__run_parameters["include_kernel_names"] = True
if hasattr(self.__args, "mem_level") and self.__args.mem_level != "ALL":
self.__run_parameters["mem_level"] = self.__args.mem_level
if hasattr(self.__args, "sort") and self.__args.sort != "ALL":
self.__run_parameters["sort_type"] = self.__args.sort
self.__run_parameters["roofline_data_type"] = self.__args.roofline_data_type
if (hasattr(self.__args, "kernel") and self.__args.kernel) or (
hasattr(self.__args, "gpu_kernel") and self.__args.gpu_kernel
):
self.__run_parameters["kernel_filter"] = True
self.validate_parameters()
def get_args(self) -> argparse.Namespace:
return self.__args
def validate_parameters(self) -> None:
if self.__run_parameters["include_kernel_names"] and (
not self.__run_parameters["is_standalone"]
):
console_warning(
"--kernel-names is nonactionable when used with --no-roof option"
)
def roof_setup(self) -> None:
# Setup the workload directory for roofline profiling.
workload_dir_val = self.__run_parameters.get("workload_dir")
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.cwd():
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)
def apply_profile_kernel_filter(
self, df: dict[str, pd.DataFrame], args: argparse.Namespace
) -> dict[str, pd.DataFrame]:
"""Apply kernel filter for profile mode."""
df_pmc = df["pmc_perf"]
df_filtered = df_pmc.copy()
df_list = df_pmc["Kernel_Name"].tolist()
for idx in range(len(df_list)):
if df_list[idx].split("(")[0] not in args.kernel:
df_filtered.drop(index=idx, inplace=True)
# Verify that final filtered kernel df matches the kernel list requested
unique_kernels = len(df_filtered.drop_duplicates(subset=["Kernel_Name"]))
if unique_kernels != len(args.kernel):
console_debug(f"Profiled kernels: {df_list}\n`--kernel`: {args.kernel}")
console_error(
"Roofline cannot profile - kernels requested with `--kernel` missing "
"from profiling data!\n"
"\tRe-profile workload in full or specify subset of available kernels "
"using `--kernel` option.\n"
"\tComplete profiled kernels list can be found in pmc_perf file.",
exit=True,
)
df["pmc_perf"] = df_filtered
return df
def apply_analyze_kernel_filter(
self,
df: dict[str, pd.DataFrame],
path_str: Optional[str],
args: argparse.Namespace,
) -> dict[str, pd.DataFrame]:
"""Apply kernel filter for analyze mode."""
if not path_str:
console_error("roofline", "cannot locate pmc_kernel_top.csv")
top_kernels_csv = Path(path_str) / "pmc_kernel_top.csv"
if not top_kernels_csv.is_file():
console_error("roofline", f"{top_kernels_csv} does not exist")
k_df = pd.read_csv(top_kernels_csv)
k_df = k_df.loc[args.gpu_kernel[0], "Kernel_Name"]
df["pmc_perf"] = df["pmc_perf"][df["pmc_perf"]["Kernel_Name"].isin(k_df)]
return df
def validate_apply_kernel_filter(
self, df: dict[str, pd.DataFrame], path_str: Optional[str] = None
) -> dict[str, pd.DataFrame]:
if not self.__run_parameters["kernel_filter"]:
return df
args = self.get_args()
if args.mode == "profile":
return self.apply_profile_kernel_filter(df, args)
elif args.mode == "analyze":
return self.apply_analyze_kernel_filter(df, path_str, args)
return df
@demarcate
def empirical_roofline(
self, ret_df: dict[str, pd.DataFrame]
) -> Optional[html.Section]:
"""
Generate a set of empirical roofline plots given a directory containing
required profiling and benchmarking data.
"""
if (
not isinstance(self.__run_parameters["workload_dir"], list)
and self.__run_parameters["workload_dir"] != None
):
self.roof_setup()
console_debug("roofline", f"Path: {self.__run_parameters.get('workload_dir')}")
# Verify kernels have been profiled and filter the df
ret_df = self.validate_apply_kernel_filter(
df=ret_df, path_str=self.__run_parameters.get("workload_dir")
)
self.__ai_data = calc_ai_profile(
self.__mspec, self.__run_parameters.get("sort_type"), ret_df
)
msg = "AI at each mem level:"
for key, value in self.__ai_data.items():
msg += f"\n\t{key} -> {value}"
console_debug(msg)
ops_figure = flops_figure = None
ops_dt_list = flops_dt_list = kernel_list = ""
for dt in self.__run_parameters.get("roofline_data_type", []):
gpu_arch = getattr(self.__mspec, "gpu_arch", "unknown_arch")
if (
"SUPPORTED_DATATYPES" not in globals()
or gpu_arch not in SUPPORTED_DATATYPES
or str(dt) not in SUPPORTED_DATATYPES[gpu_arch]
):
console_error(
f"{dt} is not a supported datatype for roofline profiling on "
f"{getattr(self.__mspec, 'gpu_model', 'N/A')} (arch: {gpu_arch})",
exit=False,
)
continue
ops_flops = "Ops" if str(dt).startswith("I") else "Flops"
if ops_flops == "Ops":
if ops_figure:
ops_figure = self.generate_plot(
dtype=str(dt),
fig=ops_figure,
)
else:
ops_figure = self.generate_plot(dtype=str(dt))
ops_dt_list += "_" + str(dt)
if ops_flops == "Flops":
if flops_figure:
flops_figure = self.generate_plot(dtype=str(dt), fig=flops_figure)
else:
flops_figure = self.generate_plot(dtype=str(dt))
flops_dt_list += "_" + str(dt)
if self.__run_parameters.get("include_kernel_names", False):
if self.__ai_data is None:
console_error(
"Roofline Error: self.__ai_data is not populated. "
"Cannot generate kernel names info.",
exit=False,
)
original_kernel_names = []
else:
original_kernel_names = self.__ai_data.get("kernelNames", [])
num_kernels = len(original_kernel_names)
self.__figure.data = []
self.__figure.layout = {}
if num_kernels == 0:
# Create empty kernel names figure when no kernels are found
console_log(
"roofline",
"No kernel names found to generate "
"'Kernel Names and Markers' info.",
)
self.__figure.add_annotation(
text="No kernel names to display.",
showarrow=False,
xref="paper",
yref="paper",
x=0.5,
y=0.5,
)
self.__figure.update_layout(
title_text="Kernel Names and Markers",
title_x=0.5,
xaxis=dict(visible=False),
yaxis=dict(visible=False),
plot_bgcolor="white",
paper_bgcolor="white",
height=200,
width=400,
)
else:
# Create populated kernel names figure with symbols and names.
symbols_list = [SYMBOLS[i % len(SYMBOLS)] for i in range(num_kernels)]
self.__figure = go.Figure()
self.__figure.add_trace(
go.Scatter(
x=[0.1] * num_kernels,
y=list(range(num_kernels, 0, -1)),
mode="markers",
marker=dict(
symbol=symbols_list,
size=15,
color="blue",
line=dict(width=1, color="black"),
),
showlegend=False,
hoverinfo="skip",
)
)
# Add kernel name annotations
for i, kernel_name in enumerate(original_kernel_names):
self.__figure.add_annotation(
x=0.25,
y=num_kernels - i,
text=wrap_text(kernel_name),
showarrow=False,
xanchor="left",
yanchor="middle",
align="left",
font=dict(size=11, color="black"),
)
# Add formatting elements to kernel names figure.
self.__figure.add_annotation(
x=0.1,
y=num_kernels + 1,
text="<b>Symbol</b>",
showarrow=False,
xanchor="center",
yanchor="middle",
font=dict(size=12, color="black"),
)
self.__figure.add_annotation(
x=0.25,
y=num_kernels + 1,
text="<b>Kernel Name</b>",
showarrow=False,
xanchor="left",
yanchor="middle",
font=dict(size=12, color="black"),
)
# Add grid lines
for i in range(num_kernels + 1):
self.__figure.add_shape(
type="line",
x0=0,
x1=1,
y0=i + 0.5,
y1=i + 0.5,
line=dict(color="lightgray", width=1),
)
self.__figure.add_shape(
type="line",
x0=0.2,
x1=0.2,
y0=0.5,
y1=num_kernels + 1.5,
line=dict(color="lightgray", width=1),
)
self.__figure.update_layout(
title="Kernel Names and Corresponding Markers",
title_x=0.5,
xaxis=dict(visible=False, range=[0, 1]),
yaxis=dict(
visible=False, range=[0, num_kernels + 2], autorange=False
),
height=max(400, num_kernels * 40 + 150),
width=1000,
margin=dict(l=50, r=50, t=70, b=30),
plot_bgcolor="white",
paper_bgcolor="white",
)
# Output will be different depending on interaction type:
# Save PDFs if we're in "standalone roofline" mode,
# otherwise return HTML to be used in GUI output
if self.__run_parameters["is_standalone"]:
dev_id = str(self.__run_parameters["device_id"])
if self.__run_parameters.get("kernel_filter", False):
for name in sorted(self.__args.kernel):
kernel_list += "_" + name
# Re-save to remove loading MathJax pop up
for _ in range(2):
if ops_figure:
ops_figure.write_image(
f"{self.__run_parameters['workload_dir']}/empirRoof_gpu-{dev_id}{ops_dt_list}{kernel_list}.pdf"
)
if flops_figure:
flops_figure.write_image(
f"{self.__run_parameters['workload_dir']}/empirRoof_gpu-{dev_id}{flops_dt_list}{kernel_list}.pdf"
)
# only save a legend if kernel_names option is toggled
if self.__run_parameters["include_kernel_names"]:
self.__figure.write_image(
f"{self.__run_parameters['workload_dir']}/kernelName_legend{kernel_list}.pdf"
)
time.sleep(1)
console_log("roofline", "Empirical Roofline PDFs saved!")
else:
# Create HTML output for GUI mode.
ops_graph = (
html.Div(
className="float-child",
children=[
html.H3(children="Empirical Roofline Analysis (Ops)"),
dcc.Graph(figure=ops_figure),
],
)
if ops_figure
else None
)
flops_graph = (
html.Div(
className="float-child",
children=[
html.H3(children="Empirical Roofline Analysis (Flops)"),
dcc.Graph(figure=flops_figure),
],
)
if flops_figure
else None
)
return html.Section(
id="roofline",
children=[
html.Div(
className="float-container",
children=[
ops_graph,
flops_graph,
],
)
],
)
@demarcate
def generate_plot(self, dtype: str, fig: Optional[go.Figure] = None) -> go.Figure:
"""
Create graph object from ai_data (coordinate points) and ceiling_data
(peak FLOP and BW) data.
"""
if fig is None:
fig = go.Figure()
skipAI = False
else:
skipAI = True # Don't repeat AI plotting
plot_mode = "lines+text" if self.__run_parameters["is_standalone"] else "lines"
self.__ceiling_data = construct_roof(
roofline_parameters=self.__run_parameters,
dtype=dtype,
)
console_debug("roofline", f"Ceiling data:\n{self.__ceiling_data}")
ops_flops = "OP" if dtype.startswith("I") else "FLOP" # For printing purposes
#######################
# Plot Application AI
#######################
# Plot the arithmetic intensity points for each cache level
if ops_flops == "FLOP":
if not skipAI:
fig.add_trace(
go.Scatter(
x=self.__ai_data["ai_l1"][0],
y=self.__ai_data["ai_l1"][1],
name="ai_l1",
mode="markers",
marker_symbol=(
SYMBOLS
if self.__run_parameters["include_kernel_names"]
else None
),
)
)
fig.add_trace(
go.Scatter(
x=self.__ai_data["ai_l2"][0],
y=self.__ai_data["ai_l2"][1],
name="ai_l2",
mode="markers",
marker_symbol=(
SYMBOLS
if self.__run_parameters["include_kernel_names"]
else None
),
)
)
fig.add_trace(
go.Scatter(
x=self.__ai_data["ai_hbm"][0],
y=self.__ai_data["ai_hbm"][1],
name="ai_hbm",
mode="markers",
marker_symbol=(
SYMBOLS
if self.__run_parameters["include_kernel_names"]
else None
),
)
)
# Set layout
fig.update_layout(
xaxis_title="Arithmetic Intensity (FLOPs/Byte)",
yaxis_title="Performance (GFLOP/sec)",
hovermode="x unified",
margin=dict(l=50, r=50, b=50, t=50, pad=4),
)
else:
# Set layout
fig.update_layout(
xaxis_title="Bandwidth (GB/sec)",
yaxis_title="Performance (GOP/sec)",
hovermode="x unified",
margin=dict(l=50, r=50, b=50, t=50, pad=4),
)
console_debug(
"roofline",
"Roofline analysis only supports AI for "
"floating point calculations at this time",
)
#######################
# Plot ceilings
#######################
mem_level_config = self.__run_parameters.get("mem_level", "ALL")
cache_hierarchy = (
["HBM", "L2", "L1", "LDS"]
if mem_level_config == "ALL"
else (
mem_level_config
if isinstance(mem_level_config, list)
else [mem_level_config]
)
)
# Plot peak BW ceiling(s)
for cache_level in cache_hierarchy:
cache_key = cache_level.lower()
if (
not self.__ceiling_data
or cache_level.lower() not in self.__ceiling_data
or not isinstance(
self.__ceiling_data[cache_level.lower()], (list, tuple)
)
or len(self.__ceiling_data[cache_level.lower()]) < 3
):
console_error(
f"Ceiling data for {cache_level} is missing "
f"or malformed for dtype {dtype}.",
exit=False,
)
continue
fig.add_trace(
go.Scatter(
x=self.__ceiling_data[cache_level.lower()][0],
y=self.__ceiling_data[cache_level.lower()][1],
name=f"{cache_level}-{dtype}",
mode=plot_mode,
hovertemplate="<b>%{text}</b>",
text=[
f"{to_int(self.__ceiling_data[cache_key][2])} GB/s",
(
None
if self.__run_parameters.get("is_standalone")
else f"{to_int(self.__ceiling_data[cache_key][2])} GB/s"
),
],
textposition="top right",
)
)
# Plot peak VALU ceiling
if dtype in PEAK_OPS_DATATYPES:
fig.add_trace(
go.Scatter(
x=self.__ceiling_data["valu"][0],
y=self.__ceiling_data["valu"][1],
name=f"Peak VALU-{dtype}",
mode=plot_mode,
hovertemplate="<b>%{text}</b>",
text=[
(
None
if self.__run_parameters["is_standalone"]
else (
f"{to_int(self.__ceiling_data['valu'][2])} G"
f"{ops_flops}/s"
)
),
f"{to_int(self.__ceiling_data['valu'][2])} G{ops_flops}/s",
],
textposition="top left",
)
)
# Plot peak MFMA ceiling
if dtype in MFMA_DATATYPES:
fig.add_trace(
go.Scatter(
x=self.__ceiling_data["mfma"][0],
y=self.__ceiling_data["mfma"][1],
name=f"Peak MFMA-{dtype}",
mode=plot_mode,
hovertemplate="<b>%{text}</b>",
text=[
(
None
if self.__run_parameters["is_standalone"]
else (
f"{to_int(self.__ceiling_data['mfma'][2])} "
f"G{ops_flops}/s"
)
),
f"{to_int(self.__ceiling_data['mfma'][2])} G{ops_flops}/s",
],
textposition="top left",
)
)
fig.update_xaxes(type="log", autorange=True)
fig.update_yaxes(type="log", autorange=True)
return fig
def cli_generate_plot(
self,
dtype: str,
workload: Optional[schema.Workload] = None,
config: Optional[dict[str, Any]] = None,
arch_config: Optional[schema.ArchConfig] = None,
) -> Optional[str]:
"""
Plot CLI mode roofline analysis in terminal using plotext
:param dtype: The datatype to be profiled
:type method: str
:return: Build the current figure using plot.build(),
or None if datatype is not valid for the architecture
:rtype: str or None
"""
console_debug("roofline", "Generating roofline plot for CLI")
if not (str(dtype) in SUPPORTED_DATATYPES[str(self.__mspec.gpu_arch)]):
console_error(
f"{dtype} is not a supported datatype for roofline profiling on "
f"{getattr(self.__mspec, 'gpu_model', 'N/A')} (arch: "
f"{self.__mspec.gpu_arch})",
exit=False,
)
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
base_path = Path(base_dir)
roofline_csv = base_path / "roofline.csv"
if not roofline_csv.is_file():
console_log("roofline", f"{roofline_csv} does not exist")
return
# if workload is detected, utilize Roofline yamls.
# If not, fallback to legacy calc_ai
if workload and config and arch_config:
self.__ai_data = calc_ai_analyze(
workload=workload,
mspec=self.__mspec,
sort_type=str(self.__run_parameters.get("sort_type")),
config=config,
arch_config=arch_config,
)
else:
pmc_perf_csv = base_path / "pmc_perf.csv"
if not pmc_perf_csv.is_file():
console_error("roofline", f"{pmc_perf_csv} does not exist")
t_df = OrderedDict()
t_df["pmc_perf"] = pd.read_csv(pmc_perf_csv)
profiling_config = file_io.load_profiling_config(self.__args.path[0][0])
if profiling_config.get("format_rocprof_output") == "rocpd":
t_df["pmc_perf"] = rocpd_data.process_rocpd_csv(t_df["pmc_perf"])
t_df = self.validate_apply_kernel_filter(df=t_df, path_str=str(base_path))
self.__ai_data = calc_ai_profile(
self.__mspec, self.__run_parameters["sort_type"], t_df
)
self.__ceiling_data = construct_roof(
roofline_parameters=self.__run_parameters, dtype=dtype
)
console_debug(f"AI data: {self.__ai_data}")
console_debug(f"Kernel names: {self.__ai_data.get('kernelNames', [])}")
self.roof_setup()
# Check proper datatype input - takes single str
if not isinstance(dtype, str):
console_error("Unsupported datatype input - must be str")
# 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")
color_scheme = {
"HBM": "blue+",
"L2": "green+",
"L1": "red+",
"LDS": "orange+",
"VALU": "white",
"MFMA": "magenta+",
}
kernel_markers = {
0: "star",
1: "cross",
2: "sd",
3: "shamrock",
4: "at",
5: "atom",
}
plt.clf()
plt.plotsize(plt.tw(), plt.th())
ops_flops = "OP" if dtype.startswith("I") else "FLOP"
# Plot bandwidth lines
cache_hierarchy = (
["HBM", "L2", "L1", "LDS"]
if self.__run_parameters["mem_level"] == "ALL"
else self.__run_parameters["mem_level"]
)
for cache_level in cache_hierarchy:
cache_key = cache_level.lower()
plt.plot(
self.__ceiling_data[cache_key][0],
self.__ceiling_data[cache_key][1],
label=f"{cache_level}-{dtype}",
marker="braille",
color=color_scheme[cache_level],
)
plt.text(
f"{round(self.__ceiling_data[cache_key][2])} GB/s",
x=self.__ceiling_data[cache_key][0][0],
y=self.__ceiling_data[cache_key][1][0],
background="black",
color="white",
alignment="left",
)
console_debug(
"roofline",
f"{cache_level}: [{self.__ceiling_data[cache_key][0][0]},"
f"{self.__ceiling_data[cache_key][0][1]}], "
f"[{self.__ceiling_data[cache_key][1][0]},"
f"{self.__ceiling_data[cache_key][1][1]}], "
f"{self.__ceiling_data[cache_key][2]}",
)
# Plot VALU and MFMA Peak
if dtype in PEAK_OPS_DATATYPES:
plt.plot(
self.__ceiling_data["valu"][0],
[
self.__ceiling_data["valu"][1][0] - 0.1,
self.__ceiling_data["valu"][1][1] - 0.1,
],
label=f"Peak VALU-{dtype}",
marker="braille",
color=color_scheme["VALU"],
)
plt.text(
f"{round(self.__ceiling_data['valu'][2])} G{ops_flops}/s",
x=self.__ceiling_data["valu"][0][1] - 800,
y=self.__ceiling_data["valu"][1][1],
background="black",
color="white",
alignment="right",
)
console_debug(
"roofline",
f"VALU: [{self.__ceiling_data['valu'][0][0]},"
f"{self.__ceiling_data['valu'][0][1]}], "
f"[{self.__ceiling_data['valu'][1][0]},"
f"{self.__ceiling_data['valu'][1][1]}], "
f"{self.__ceiling_data['valu'][2]}",
)
else:
console_warning(f"No PEAK measurement available for {dtype}")
if dtype in MFMA_DATATYPES:
plt.plot(
self.__ceiling_data["mfma"][0],
[
self.__ceiling_data["mfma"][1][0] - 0.1,
self.__ceiling_data["mfma"][1][1] - 0.1,
],
label=f"Peak MFMA-{dtype}",
marker="braille",
color=color_scheme["MFMA"],
)
plt.text(
f"{round(self.__ceiling_data['mfma'][2])} G{ops_flops}/s",
x=self.__ceiling_data["mfma"][0][1] - 800,
y=self.__ceiling_data["mfma"][1][1],
background="black",
color="white",
alignment="right",
)
console_debug(
"roofline",
f"MFMA: [{self.__ceiling_data['mfma'][0][0]},"
f"{self.__ceiling_data['mfma'][0][1]}], "
f"[{self.__ceiling_data['mfma'][1][0]},"
f"{self.__ceiling_data['mfma'][1][1]}], "
f"{self.__ceiling_data['mfma'][2]}",
)
else:
console_warning(f"No MFMA measurement available for {dtype}")
# Plot Application AI
for cache_level in cache_hierarchy:
key = f"ai_{cache_level.lower()}"
if key not in self.__ai_data:
continue
kernel_names = self.__ai_data.get("kernelNames", [])
for i in range(len(self.__ai_data.get("kernelNames", []))):
# Zero intensity level means no data reported for this cache level
if self.__ai_data[key][0][i] > 0 and self.__ai_data[key][1][i] > 0:
plt.plot(
[self.__ai_data[key][0][i]],
[self.__ai_data[key][1][i]],
label=f"AI_{cache_level}_{kernel_names[i]}",
color=color_scheme[cache_level],
marker=kernel_markers[i % len(kernel_markers)],
)
val1 = (
self.__ai_data[key][0][i]
if i < len(self.__ai_data[key][0])
else "N/A"
)
val2 = (
self.__ai_data[key][1][i]
if i < len(self.__ai_data[key][1])
else "N/A"
)
console_debug("roofline", f"AI_{kernel_names[i]}: {val1}, {val2}")
plt.xlabel(f"Arithmetic Intensity ({ops_flops}s/Byte)")
plt.ylabel("Performance (GFLOP/sec)")
plt.title(f"Roofline ({dtype}) - {base_path}")
# Canvas config
plt.theme("pro")
plt.xscale("log")
plt.yscale("log")
# Build figure
# Print plot using `plt._utility.write(self.cli_generate_plot(dtype))`
return plt.build()
@demarcate
def standalone_roofline(self) -> None:
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")
app_path = Path(str(self.__run_parameters["workload_dir"])) / "pmc_perf.csv"
if not app_path.is_file():
console_error("roofline", f"{app_path} does not exist")
t_df = OrderedDict()
t_df["pmc_perf"] = pd.read_csv(app_path)
profiling_config = file_io.load_profiling_config(self.__args.path)
if profiling_config.get("format_rocprof_output") == "rocpd":
t_df["pmc_perf"] = rocpd_data.process_rocpd_csv(t_df["pmc_perf"])
self.empirical_roofline(ret_df=t_df)
# NB: Currently the post_prossesing() method is the only one being used by
# rocprofiler-compute, we include pre_processing() and profile() methods for
# those who wish to borrow the roofline module
@abstractmethod
def post_processing(self) -> None:
if self.__run_parameters["is_standalone"]:
self.standalone_roofline()
def get_dtype(self) -> list[str]:
return self.__run_parameters["roofline_data_type"]