Skip to content

Commit b9641d0

Browse files
nv-hwoomc-nv
authored andcommitted
Add compare subcommand (#623)
* Move for better visibility * Add compare subparser * Add subcommand compare * Fix test * Add ticket * add --files option and minor fix * Fix tests * Add unit tests * Address feedback * Fix minor error and add section header
1 parent 3daf2e3 commit b9641d0

File tree

4 files changed

+143
-19
lines changed

4 files changed

+143
-19
lines changed

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

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -135,15 +135,19 @@ def finalize(profile_export_file: Path):
135135
# to assert correct errors and messages.
136136
def run():
137137
try:
138+
# TMA-1900: refactor CLI handler
138139
init_logging()
139140
args, extra_args = parser.parse_args()
140-
create_artifacts_dirs(args.generate_plots)
141-
tokenizer = get_tokenizer(args.tokenizer)
142-
generate_inputs(args, tokenizer)
143-
args.func(args, extra_args)
144-
data_parser = calculate_metrics(args, tokenizer)
145-
report_output(data_parser, args)
146-
finalize(args.profile_export_file)
141+
if args.subcommand == "compare":
142+
args.func(args)
143+
else:
144+
create_artifacts_dirs(args.generate_plots)
145+
tokenizer = get_tokenizer(args.tokenizer)
146+
generate_inputs(args, tokenizer)
147+
args.func(args, extra_args)
148+
data_parser = calculate_metrics(args, tokenizer)
149+
report_output(data_parser, args)
150+
finalize(args.profile_export_file)
147151
except Exception as e:
148152
raise GenAIPerfException(e)
149153

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

Lines changed: 74 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,29 @@
4141
_endpoint_type_map = {"chat": "v1/chat/completions", "completions": "v1/completions"}
4242

4343

44+
def _check_model_args(
45+
parser: argparse.ArgumentParser, args: argparse.Namespace
46+
) -> argparse.Namespace:
47+
"""
48+
Check if model name is provided.
49+
"""
50+
if not args.subcommand and not args.model:
51+
parser.error("The -m/--model option is required and cannot be empty.")
52+
return args
53+
54+
55+
def _check_compare_args(
56+
parser: argparse.ArgumentParser, args: argparse.Namespace
57+
) -> argparse.Namespace:
58+
"""
59+
Check compare subcommand args
60+
"""
61+
if args.subcommand == "compare":
62+
if not args.config and not args.files:
63+
parser.error("Either the --config or --files option must be specified.")
64+
return args
65+
66+
4467
def _check_conditional_args(
4568
parser: argparse.ArgumentParser, args: argparse.Namespace
4669
) -> argparse.Namespace:
@@ -132,15 +155,6 @@ def _convert_str_to_enum_entry(args, option, enum):
132155
return args
133156

134157

135-
### Handlers ###
136-
137-
138-
def handler(args, extra_args):
139-
from genai_perf.wrapper import Profiler
140-
141-
Profiler.run(args=args, extra_args=extra_args)
142-
143-
144158
### Parsers ###
145159

146160

@@ -286,7 +300,7 @@ def _add_endpoint_args(parser):
286300
"-m",
287301
"--model",
288302
type=str,
289-
required=True,
303+
default=None,
290304
help=f"The name of the model to benchmark.",
291305
)
292306

@@ -437,6 +451,47 @@ def get_extra_inputs_as_dict(args: argparse.Namespace) -> dict:
437451
return request_inputs
438452

439453

454+
def _parse_compare_args(subparsers) -> argparse.ArgumentParser:
455+
compare = subparsers.add_parser(
456+
"compare",
457+
description="Subcommand to generate plots that compare multiple profile runs.",
458+
)
459+
compare_group = compare.add_argument_group("Compare")
460+
mx_group = compare_group.add_mutually_exclusive_group(required=False)
461+
mx_group.add_argument(
462+
"--config",
463+
type=Path,
464+
default=None,
465+
help="The path to the YAML file that specifies plot configurations for "
466+
"comparing multiple runs.",
467+
)
468+
mx_group.add_argument(
469+
"-f",
470+
"--files",
471+
nargs="+",
472+
default=[],
473+
help="List of paths to the profile export JSON files. Users can specify "
474+
"this option instead of the `--config` option if they would like "
475+
"GenAI-Perf to generate default plots as well as initial YAML config file.",
476+
)
477+
compare.set_defaults(func=compare_handler)
478+
return compare
479+
480+
481+
### Handlers ###
482+
483+
484+
def profile_handler(args, extra_args):
485+
from genai_perf.wrapper import Profiler
486+
487+
Profiler.run(args=args, extra_args=extra_args)
488+
489+
490+
def compare_handler(args: argparse.Namespace):
491+
# TMA-1893: parse yaml file
492+
pass
493+
494+
440495
### Entrypoint ###
441496

442497

@@ -448,7 +503,7 @@ def parse_args():
448503
description="CLI to profile LLMs and Generative AI models with Perf Analyzer",
449504
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
450505
)
451-
parser.set_defaults(func=handler)
506+
parser.set_defaults(func=profile_handler)
452507

453508
# Conceptually group args for easier visualization
454509
_add_endpoint_args(parser)
@@ -457,6 +512,12 @@ def parse_args():
457512
_add_output_args(parser)
458513
_add_other_args(parser)
459514

515+
# Add subcommands
516+
subparsers = parser.add_subparsers(
517+
help="List of subparser commands.", dest="subcommand"
518+
)
519+
compare_parser = _parse_compare_args(subparsers)
520+
460521
# Check for passthrough args
461522
if "--" in argv:
462523
passthrough_index = argv.index("--")
@@ -466,7 +527,9 @@ def parse_args():
466527

467528
args = parser.parse_args(argv[1:passthrough_index])
468529
args = _infer_prompt_source(args)
530+
args = _check_model_args(parser, args)
469531
args = _check_conditional_args(parser, args)
532+
args = _check_compare_args(compare_parser, args)
470533
args = _update_load_manager_args(args)
471534

472535
return args, argv[passthrough_index + 1 :]

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ def build_cmd(args: Namespace, extra_args: list[str] | None = None) -> list[str]
7676
"tokenizer",
7777
"endpoint_type",
7878
"generate_plots",
79+
"subcommand",
7980
]
8081

8182
utils.remove_file(args.profile_export_file)

src/c++/perf_analyzer/genai-perf/tests/test_cli.py

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@
3232

3333

3434
class TestCLIArguments:
35+
# ================================================
36+
# GENAI-PERF COMMAND
37+
# ================================================
3538
expected_help_output = (
3639
"CLI to profile LLMs and Generative AI models with Perf Analyzer"
3740
)
@@ -217,7 +220,7 @@ def test_load_level_mutually_exclusive(self, monkeypatch, capsys):
217220

218221
def test_model_not_provided(self, monkeypatch, capsys):
219222
monkeypatch.setattr("sys.argv", ["genai-perf"])
220-
expected_output = "the following arguments are required: -m/--model"
223+
expected_output = "The -m/--model option is required and cannot be empty."
221224

222225
with pytest.raises(SystemExit) as excinfo:
223226
parser.parse_args()
@@ -437,3 +440,56 @@ def test_prompt_source_assertions(self, monkeypatch, mocker, capsys):
437440
assert excinfo.value.code != 0
438441
captured = capsys.readouterr()
439442
assert expected_output in captured.err
443+
444+
# ================================================
445+
# COMPARE SUBCOMMAND
446+
# ================================================
447+
expected_compare_help_output = (
448+
"Subcommand to generate plots that compare multiple profile runs."
449+
)
450+
451+
@pytest.mark.parametrize(
452+
"args, expected_output",
453+
[
454+
(["-h"], expected_compare_help_output),
455+
(["--help"], expected_compare_help_output),
456+
],
457+
)
458+
def test_compare_help_arguments_output_and_exit(
459+
self, monkeypatch, args, expected_output, capsys
460+
):
461+
monkeypatch.setattr("sys.argv", ["genai-perf", "compare"] + args)
462+
463+
with pytest.raises(SystemExit) as excinfo:
464+
_ = parser.parse_args()
465+
466+
# Check that the exit was successful
467+
assert excinfo.value.code == 0
468+
469+
# Capture that the correct message was displayed
470+
captured = capsys.readouterr()
471+
assert expected_output in captured.out
472+
473+
def test_compare_mutually_exclusive(self, monkeypatch, capsys):
474+
args = ["genai-perf", "compare", "--config", "hello", "--files", "a", "b", "c"]
475+
monkeypatch.setattr("sys.argv", args)
476+
expected_output = "argument -f/--files: not allowed with argument --config"
477+
478+
with pytest.raises(SystemExit) as excinfo:
479+
parser.parse_args()
480+
481+
assert excinfo.value.code != 0
482+
captured = capsys.readouterr()
483+
assert expected_output in captured.err
484+
485+
def test_compare_not_provided(self, monkeypatch, capsys):
486+
args = ["genai-perf", "compare"]
487+
monkeypatch.setattr("sys.argv", args)
488+
expected_output = "Either the --config or --files option must be specified."
489+
490+
with pytest.raises(SystemExit) as excinfo:
491+
parser.parse_args()
492+
493+
assert excinfo.value.code != 0
494+
captured = capsys.readouterr()
495+
assert expected_output in captured.err

0 commit comments

Comments
 (0)