Skip to content

Commit f1dfdc6

Browse files
jaredoconnellsjmonsonmarkurtz
authored
Option to re-display a benchmark file (#185)
closes #175 This adds a command to re-display a prior benchmarks file in the CLI. Before marking this as ready for review, we need to decide what command format we want to use. During the call with Mark we discussed this being an option within the benchmark command. Also, let me know if the stripped down results file is a good one to use. I manually removed data from a large results file. --------- Co-authored-by: Samuel Monson <smonson@redhat.com> Co-authored-by: Mark Kurtz <mark.j.kurtz@gmail.com>
1 parent 4d46368 commit f1dfdc6

13 files changed

+1337
-13
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ cython_debug/
168168
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
169169
# and can be added to the global gitignore or merged into this file. For a more nuclear
170170
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
171-
#.idea/
171+
.idea/
172172

173173

174174
# MacOS files

.pre-commit-config.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ repos:
33
rev: v4.6.0
44
hooks:
55
- id: trailing-whitespace
6+
exclude: ^tests/?.*/assets/.+
67
- id: end-of-file-fixer
8+
exclude: ^tests/?.*/assets/.+
79
- repo: https://github.com/astral-sh/ruff-pre-commit
810
rev: v0.11.7
911
hooks:

src/guidellm/__main__.py

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,16 @@
77
from pydantic import ValidationError
88

99
from guidellm.backend import BackendType
10-
from guidellm.benchmark import ProfileType
10+
from guidellm.benchmark import (
11+
ProfileType,
12+
reimport_benchmarks_report,
13+
)
1114
from guidellm.benchmark.entrypoints import benchmark_with_scenario
1215
from guidellm.benchmark.scenario import GenerativeTextScenario, get_builtin_scenarios
1316
from guidellm.config import print_config
1417
from guidellm.preprocess.dataset import ShortPromptStrategy, process_dataset
1518
from guidellm.scheduler import StrategyType
19+
from guidellm.utils import DefaultGroupHandler
1620
from guidellm.utils import cli as cli_tools
1721

1822
STRATEGY_PROFILE_CHOICES = set(
@@ -25,7 +29,17 @@ def cli():
2529
pass
2630

2731

28-
@cli.command(
32+
@cli.group(
33+
help="Commands to run a new benchmark or load a prior one.",
34+
cls=DefaultGroupHandler,
35+
default="run",
36+
)
37+
def benchmark():
38+
pass
39+
40+
41+
@benchmark.command(
42+
"run",
2943
help="Run a benchmark against a generative model using the specified arguments.",
3044
context_settings={"auto_envvar_prefix": "GUIDELLM"},
3145
)
@@ -230,7 +244,7 @@ def cli():
230244
type=int,
231245
help="The random seed to use for benchmarking to ensure reproducibility.",
232246
)
233-
def benchmark(
247+
def run(
234248
scenario,
235249
target,
236250
backend_type,
@@ -306,6 +320,37 @@ def benchmark(
306320
)
307321

308322

323+
@benchmark.command(
324+
"from-file",
325+
help="Load a saved benchmark report."
326+
)
327+
@click.argument(
328+
"path",
329+
type=click.Path(file_okay=True, dir_okay=False, exists=True),
330+
default=Path.cwd() / "benchmarks.json",
331+
)
332+
@click.option(
333+
"--output-path",
334+
type=click.Path(file_okay=True, dir_okay=True, exists=False),
335+
default=None,
336+
is_flag=False,
337+
flag_value=Path.cwd() / "benchmarks_reexported.json",
338+
help=(
339+
"Allows re-exporting the benchmarks to another format. "
340+
"The path to save the output to. If it is a directory, "
341+
"it will save benchmarks.json under it. "
342+
"Otherwise, json, yaml, or csv files are supported for output types "
343+
"which will be read from the extension for the file path. "
344+
"This input is optional. If the output path flag is not provided, "
345+
"the benchmarks will not be reexported. If the flag is present but "
346+
"no value is specified, it will default to the current directory "
347+
"with the file name `benchmarks_reexported.json`."
348+
),
349+
)
350+
def from_file(path, output_path):
351+
reimport_benchmarks_report(path, output_path)
352+
353+
309354
def decode_escaped_str(_ctx, _param, value):
310355
"""
311356
Click auto adds characters. For example, when using --pad-char "\n",
@@ -321,10 +366,11 @@ def decode_escaped_str(_ctx, _param, value):
321366

322367

323368
@cli.command(
369+
short_help="Prints environment variable settings.",
324370
help=(
325371
"Print out the available configuration settings that can be set "
326372
"through environment variables."
327-
)
373+
),
328374
)
329375
def config():
330376
print_config()

src/guidellm/benchmark/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
StatusBreakdown,
1313
)
1414
from .benchmarker import Benchmarker, BenchmarkerResult, GenerativeBenchmarker
15-
from .entrypoints import benchmark_generative_text
15+
from .entrypoints import benchmark_generative_text, reimport_benchmarks_report
1616
from .output import GenerativeBenchmarksConsole, GenerativeBenchmarksReport
1717
from .profile import (
1818
AsyncProfile,
@@ -63,4 +63,5 @@
6363
"ThroughputProfile",
6464
"benchmark_generative_text",
6565
"create_profile",
66+
"reimport_benchmarks_report",
6667
]

src/guidellm/benchmark/entrypoints.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -133,13 +133,8 @@ async def benchmark_generative_text(
133133
)
134134

135135
if output_console:
136-
orig_enabled = console.enabled
137-
console.enabled = True
138136
console.benchmarks = report.benchmarks
139-
console.print_benchmarks_metadata()
140-
console.print_benchmarks_info()
141-
console.print_benchmarks_stats()
142-
console.enabled = orig_enabled
137+
console.print_full_report()
143138

144139
if output_path:
145140
console.print_line("\nSaving benchmarks report...")
@@ -151,3 +146,20 @@ async def benchmark_generative_text(
151146
console.print_line("\nBenchmarking complete.")
152147

153148
return report, saved_path
149+
150+
151+
def reimport_benchmarks_report(file: Path, output_path: Optional[Path]) -> None:
152+
"""
153+
The command-line entry point for re-importing and displaying an
154+
existing benchmarks report. Can also specify
155+
Assumes the file provided exists.
156+
"""
157+
console = GenerativeBenchmarksConsole(enabled=True)
158+
report = GenerativeBenchmarksReport.load_file(file)
159+
console.benchmarks = report.benchmarks
160+
console.print_full_report()
161+
162+
if output_path:
163+
console.print_line("\nSaving benchmarks report...")
164+
saved_path = report.save_file(output_path)
165+
console.print_line(f"Benchmarks report saved to {saved_path}")

src/guidellm/benchmark/output.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,10 @@ def _file_setup(
242242
if path_suffix in [".csv"]:
243243
return path, "csv"
244244

245-
raise ValueError(f"Unsupported file extension: {path_suffix} for {path}.")
245+
raise ValueError(
246+
f"Unsupported file extension: {path_suffix} for {path}; "
247+
"expected json, yaml, or csv."
248+
)
246249

247250
@staticmethod
248251
def _benchmark_desc_headers_and_values(
@@ -944,3 +947,20 @@ def print_benchmarks_stats(self):
944947
title="Benchmarks Stats",
945948
sections=sections,
946949
)
950+
951+
def print_full_report(self):
952+
"""
953+
Print out the benchmark statistics to the console.
954+
Temporarily enables the console if it's disabled.
955+
956+
Format:
957+
- Metadata
958+
- Info
959+
- Stats
960+
"""
961+
orig_enabled = self.enabled
962+
self.enabled = True
963+
self.print_benchmarks_metadata()
964+
self.print_benchmarks_info()
965+
self.print_benchmarks_stats()
966+
self.enabled = orig_enabled

src/guidellm/utils/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from .colors import Colors
2+
from .default_group import DefaultGroupHandler
23
from .hf_datasets import (
34
SUPPORTED_TYPES,
45
save_dataset_to_file,
@@ -20,6 +21,7 @@
2021
__all__ = [
2122
"SUPPORTED_TYPES",
2223
"Colors",
24+
"DefaultGroupHandler",
2325
"EndlessTextCreator",
2426
"IntegerRangeSampler",
2527
"check_load_processor",

src/guidellm/utils/default_group.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
"""
2+
File uses code adapted from code with the following license:
3+
4+
Copyright (c) 2015-2023, Heungsub Lee
5+
All rights reserved.
6+
7+
Redistribution and use in source and binary forms, with or without modification,
8+
are permitted provided that the following conditions are met:
9+
10+
Redistributions of source code must retain the above copyright notice, this
11+
list of conditions and the following disclaimer.
12+
13+
Redistributions in binary form must reproduce the above copyright notice, this
14+
list of conditions and the following disclaimer in the documentation and/or
15+
other materials provided with the distribution.
16+
17+
Neither the name of the copyright holder nor the names of its
18+
contributors may be used to endorse or promote products derived from
19+
this software without specific prior written permission.
20+
21+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
22+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
23+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
24+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
25+
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
26+
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
27+
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
28+
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
30+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31+
"""
32+
33+
__all__ = ["DefaultGroupHandler"]
34+
35+
import collections.abc as cabc
36+
37+
import click
38+
39+
40+
class DefaultGroupHandler(click.Group):
41+
"""
42+
Allows the migration to a new sub-command by allowing the group to run
43+
one of its sub-commands as the no-args default command.
44+
"""
45+
46+
def __init__(self, *args, **kwargs):
47+
# To resolve as the default command.
48+
if not kwargs.get("ignore_unknown_options", True):
49+
raise ValueError("Default group accepts unknown options")
50+
self.ignore_unknown_options = True
51+
self.default_cmd_name = kwargs.pop("default", None)
52+
self.default_if_no_args = kwargs.pop("default_if_no_args", False)
53+
super().__init__(*args, **kwargs)
54+
55+
def parse_args(self, ctx, args):
56+
if not args and self.default_if_no_args:
57+
args.insert(0, self.default_cmd_name)
58+
return super().parse_args(ctx, args)
59+
60+
def get_command(self, ctx, cmd_name):
61+
if cmd_name not in self.commands:
62+
# If it doesn't match an existing command, use the default command name.
63+
ctx.arg0 = cmd_name
64+
cmd_name = self.default_cmd_name
65+
return super().get_command(ctx, cmd_name)
66+
67+
def resolve_command(self, ctx, args):
68+
cmd_name, cmd, args = super().resolve_command(ctx, args)
69+
if hasattr(ctx, "arg0"):
70+
args.insert(0, ctx.arg0)
71+
cmd_name = cmd.name
72+
return cmd_name, cmd, args
73+
74+
def format_commands(self, ctx, formatter):
75+
"""
76+
Used to wrap the default formatter to clarify which command is the default.
77+
"""
78+
formatter = DefaultCommandFormatter(self, formatter, mark=" (default)")
79+
return super().format_commands(ctx, formatter)
80+
81+
82+
class DefaultCommandFormatter:
83+
"""
84+
Wraps a formatter to edit the line for the default command to mark it
85+
with the specified mark string.
86+
"""
87+
88+
def __init__(self, group, formatter, mark="*"):
89+
self.group = group
90+
self.formatter = formatter
91+
self.mark = mark
92+
super().__init__()
93+
94+
def __getattr__(self, attr):
95+
return getattr(self.formatter, attr)
96+
97+
def write_dl(self, rows: cabc.Sequence[tuple[str, str]], *args, **kwargs):
98+
rows_: list[tuple[str, str]] = []
99+
for cmd_name, help_msg in rows:
100+
if cmd_name == self.group.default_cmd_name:
101+
rows_.insert(0, (cmd_name + self.mark, help_msg))
102+
else:
103+
rows_.append((cmd_name, help_msg))
104+
return self.formatter.write_dl(rows_, *args, **kwargs)

tests/unit/entrypoints/__init__.py

Whitespace-only changes.

tests/unit/entrypoints/assets/benchmarks_stripped.json

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)