Skip to content

Commit 87b9d07

Browse files
lkomalinv-hwoo
authored andcommitted
Extend genai perf plots to compare across multiple runs (#635)
* Modify PlotManager and plots classes * Support plots for multiple runs -draft * Fix default plot visualization * Remove artifact * Set default compare directory * Support generating parquet files * Remove annotations and fix heatmap * Fix errors * Fix pre-commit * Fix CodeQL warning * Remove unused comments * remove x axis tick label for boxplot * Add logging and label for heatmap subplots * Allow users to adjust width and height * fix grammer --------- Co-authored-by: Hyunjae Woo <[email protected]>
1 parent af09cf8 commit 87b9d07

File tree

12 files changed

+223
-261
lines changed

12 files changed

+223
-261
lines changed

src/c++/perf_analyzer/genai-perf/genai_perf/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,5 @@
3434

3535

3636
DEFAULT_ARTIFACT_DIR = "artifacts"
37+
DEFAULT_COMPARE_DIR = "compare"
3738
DEFAULT_PARQUET_FILE = "all_data"

src/c++/perf_analyzer/genai-perf/genai_perf/logging.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,16 @@ def init_logging() -> None:
7070
"level": "DEBUG",
7171
"propagate": False,
7272
},
73+
"genai_perf.plots.plot_config_parser": {
74+
"handlers": ["console"],
75+
"level": "DEBUG",
76+
"propagate": False,
77+
},
78+
"genai_perf.plots.plot_manager": {
79+
"handlers": ["console"],
80+
"level": "DEBUG",
81+
"propagate": False,
82+
},
7383
},
7484
}
7585
logging.config.dictConfig(LOGGING_CONFIG)

src/c++/perf_analyzer/genai-perf/genai_perf/main.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -118,10 +118,8 @@ def create_plots(filename: Path) -> None:
118118
PlotConfigParser.create_init_yaml_config([filename], output_dir)
119119
config_parser = PlotConfigParser(output_dir / "config.yaml")
120120
plot_configs = config_parser.generate_configs()
121-
122-
# TODO (harshini): plug-in configs to plot manager
123-
# plot_manager = PlotManager(stats)
124-
# plot_manager.create_default_plots()
121+
plot_manager = PlotManager(plot_configs)
122+
plot_manager.generate_plots()
125123

126124

127125
def finalize(profile_export_file: Path):

src/c++/perf_analyzer/genai-perf/genai_perf/parser.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,13 @@
2525
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
2626

2727
import argparse
28+
import os
2829
import sys
2930
from pathlib import Path
3031

3132
import genai_perf.logging as logging
3233
import genai_perf.utils as utils
33-
from genai_perf.constants import CNN_DAILY_MAIL, OPEN_ORCA
34+
from genai_perf.constants import CNN_DAILY_MAIL, DEFAULT_COMPARE_DIR, OPEN_ORCA
3435
from genai_perf.llm_inputs.llm_inputs import LlmInputs, OutputFormat, PromptSource
3536
from genai_perf.plots.plot_config_parser import PlotConfigParser
3637
from genai_perf.plots.plot_manager import PlotManager
@@ -483,6 +484,11 @@ def _parse_compare_args(subparsers) -> argparse.ArgumentParser:
483484
### Handlers ###
484485

485486

487+
def create_compare_dir() -> None:
488+
if not os.path.exists(DEFAULT_COMPARE_DIR):
489+
os.mkdir(DEFAULT_COMPARE_DIR)
490+
491+
486492
def profile_handler(args, extra_args):
487493
from genai_perf.wrapper import Profiler
488494

@@ -492,15 +498,15 @@ def profile_handler(args, extra_args):
492498
def compare_handler(args: argparse.Namespace):
493499
"""Handles `compare` subcommand workflow."""
494500
if args.files:
495-
PlotConfigParser.create_init_yaml_config(args.files, Path("."))
496-
args.config = Path("config.yaml")
501+
create_compare_dir()
502+
output_dir = Path(f"{DEFAULT_COMPARE_DIR}")
503+
PlotConfigParser.create_init_yaml_config(args.files, output_dir)
504+
args.config = output_dir / "config.yaml"
497505

498506
config_parser = PlotConfigParser(args.config)
499507
plot_configs = config_parser.generate_configs()
500-
501-
# TODO (harshini): plug-in configs to PlotManager
502-
# plot_manager = PlotManager(plot_configs)
503-
# plot_manager.generate_plots()
508+
plot_manager = PlotManager(plot_configs)
509+
plot_manager.generate_plots()
504510

505511

506512
### Entrypoint ###

src/c++/perf_analyzer/genai-perf/genai_perf/plots/base_plot.py

Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,11 @@
2525
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
2626
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
2727

28+
from pathlib import Path
2829

29-
from copy import deepcopy
30-
from typing import Dict
31-
32-
from genai_perf.constants import DEFAULT_ARTIFACT_DIR
30+
import pandas as pd
3331
from genai_perf.exceptions import GenAIPerfException
34-
from genai_perf.llm_metrics import Statistics
35-
from pandas import DataFrame
32+
from genai_perf.plots.plot_config import ProfileRunData
3633
from plotly.graph_objects import Figure
3734

3835

@@ -41,40 +38,44 @@ class BasePlot:
4138
Base class for plots
4239
"""
4340

44-
def __init__(self, stats: Statistics, extra_data: Dict | None = None) -> None:
45-
self._stats = stats
46-
self._metrics_data = deepcopy(stats.metrics.data)
47-
if extra_data:
48-
self._metrics_data = self._metrics_data | extra_data
41+
def __init__(self, data: list[ProfileRunData]) -> None:
42+
self._profile_data = data
4943

5044
def create_plot(
5145
self,
52-
x_key: str,
53-
y_key: str,
54-
x_metric: str,
55-
y_metric: str,
5646
graph_title: str,
5747
x_label: str,
5848
y_label: str,
49+
width: int,
50+
height: int,
5951
filename_root: str,
52+
output_dir: Path,
6053
) -> None:
6154
"""
6255
Create plot for specific graph type
6356
"""
6457
raise NotImplementedError
6558

66-
def _generate_parquet(self, dataframe: DataFrame, file: str) -> None:
67-
dataframe.to_parquet(
68-
f"{DEFAULT_ARTIFACT_DIR}/data/{file}.gzip", compression="gzip"
59+
def _create_dataframe(self, x_label: str, y_label: str) -> pd.DataFrame:
60+
return pd.DataFrame(
61+
{
62+
x_label: [prd.x_metric for prd in self._profile_data],
63+
y_label: [prd.y_metric for prd in self._profile_data],
64+
"Run Name": [prd.name for prd in self._profile_data],
65+
}
6966
)
7067

71-
def _generate_graph_file(self, fig: Figure, file: str, title: str) -> None:
68+
def _generate_parquet(self, df: pd.DataFrame, output_dir: Path, file: str) -> None:
69+
filepath = output_dir / f"{file}.gzip"
70+
df.to_parquet(filepath, compression="gzip")
71+
72+
def _generate_graph_file(self, fig: Figure, output_dir: Path, file: str) -> None:
7273
if file.endswith("jpeg"):
73-
print(f"Generating '{title}' jpeg")
74-
fig.write_image(f"{DEFAULT_ARTIFACT_DIR}/plots/{file}")
74+
filepath = output_dir / f"{file}"
75+
fig.write_image(filepath)
7576
elif file.endswith("html"):
76-
print(f"Generating '{title}' html")
77-
fig.write_html(f"{DEFAULT_ARTIFACT_DIR}/plots/{file}")
77+
filepath = output_dir / f"{file}"
78+
fig.write_html(filepath)
7879
else:
7980
extension = file.split(".")[-1]
8081
raise GenAIPerfException(f"image file type {extension} is not supported")

src/c++/perf_analyzer/genai-perf/genai_perf/plots/box_plot.py

Lines changed: 29 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -25,105 +25,52 @@
2525
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
2626
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
2727

28+
from pathlib import Path
2829

29-
import copy
30-
from typing import Dict
31-
32-
import pandas as pd
33-
import plotly.express as px
34-
from genai_perf.llm_metrics import Statistics
30+
import plotly.graph_objects as go
3531
from genai_perf.plots.base_plot import BasePlot
36-
from genai_perf.utils import scale
37-
from plotly.graph_objects import Figure
32+
from genai_perf.plots.plot_config import ProfileRunData
3833

3934

4035
class BoxPlot(BasePlot):
4136
"""
4237
Generate a box plot in jpeg and html format.
4338
"""
4439

45-
def __init__(self, stats: Statistics, extra_data: Dict | None = None) -> None:
46-
super().__init__(stats, extra_data)
40+
def __init__(self, data: list[ProfileRunData]) -> None:
41+
super().__init__(data)
4742

4843
def create_plot(
4944
self,
50-
x_key: str = "",
51-
y_key: str = "",
52-
x_metric: str = "",
53-
y_metric: str = "",
5445
graph_title: str = "",
5546
x_label: str = "",
5647
y_label: str = "",
48+
width: int = 700,
49+
height: int = 450,
5750
filename_root: str = "",
51+
output_dir: Path = Path(""),
5852
) -> None:
59-
df = pd.DataFrame({y_metric: self._metrics_data[y_key]})
60-
fig = px.box(
61-
df,
62-
y=y_metric,
63-
points="all",
64-
title=graph_title,
53+
fig = go.Figure()
54+
for pd in self._profile_data:
55+
fig.add_trace(go.Box(y=pd.y_metric, name=pd.name))
56+
57+
# Update layout and axis labels
58+
fig.update_layout(
59+
title={
60+
"text": f"{graph_title}",
61+
"xanchor": "center",
62+
"x": 0.5,
63+
},
64+
width=width,
65+
height=height,
6566
)
66-
fig.update_layout(title_x=0.5)
67-
fig.update_xaxes(title_text=x_label)
68-
69-
fig.update_yaxes(title_text="")
70-
71-
# create a copy to avoid annotations on html file
72-
fig_jpeg = copy.deepcopy(fig)
73-
self._add_annotations(fig_jpeg, y_metric)
74-
75-
self._generate_parquet(df, filename_root)
76-
self._generate_graph_file(fig, filename_root + ".html", graph_title)
77-
self._generate_graph_file(fig_jpeg, filename_root + ".jpeg", graph_title)
67+
fig.update_traces(boxpoints="all")
68+
fig.update_xaxes(title_text=x_label, showticklabels=False)
69+
fig.update_yaxes(title_text=y_label)
7870

79-
def _add_annotations(self, fig: Figure, y_metric: str) -> None:
80-
"""
81-
Add annotations to the non html version of the box plot
82-
to replace the missing hovertext
83-
"""
84-
stat_root_name = self._stats.metrics.get_base_name(y_metric)
71+
# Save dataframe as parquet file
72+
df = self._create_dataframe(x_label, y_label)
73+
self._generate_parquet(df, output_dir, filename_root)
8574

86-
val = scale(self._stats.data[f"max_{stat_root_name}"], (1 / 1e9))
87-
fig.add_annotation(
88-
x=0.5,
89-
y=val,
90-
text=f"max: {round(val, 2)}",
91-
showarrow=False,
92-
yshift=10,
93-
)
94-
95-
val = scale(self._stats.data[f"p75_{stat_root_name}"], (1 / 1e9))
96-
fig.add_annotation(
97-
x=0.5,
98-
y=val,
99-
text=f"q3: {round(val, 2)}",
100-
showarrow=False,
101-
yshift=10,
102-
)
103-
104-
val = scale(self._stats.data[f"p50_{stat_root_name}"], (1 / 1e9))
105-
fig.add_annotation(
106-
x=0.5,
107-
y=val,
108-
text=f"median: {round(val, 2)}",
109-
showarrow=False,
110-
yshift=10,
111-
)
112-
113-
val = scale(self._stats.data[f"p25_{stat_root_name}"], (1 / 1e9))
114-
fig.add_annotation(
115-
x=0.5,
116-
y=val,
117-
text=f"q1: {round(val, 2)}",
118-
showarrow=False,
119-
yshift=10,
120-
)
121-
122-
val = scale(self._stats.data[f"min_{stat_root_name}"], (1 / 1e9))
123-
fig.add_annotation(
124-
x=0.5,
125-
y=val,
126-
text=f"min: {round(val, 2)}",
127-
showarrow=False,
128-
yshift=10,
129-
)
75+
self._generate_graph_file(fig, output_dir, filename_root + ".html")
76+
self._generate_graph_file(fig, output_dir, filename_root + ".jpeg")

0 commit comments

Comments
 (0)