|  | #!/usr/bin/env python3 | 
|  | # Copyright 2023 The IREE Authors | 
|  | # | 
|  | # Licensed under the Apache License v2.0 with LLVM Exceptions. | 
|  | # See https://llvm.org/LICENSE.txt for license information. | 
|  | # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception | 
|  | """Miscellaneous tool to help work with benchmark suite and benchmark CI.""" | 
|  |  | 
|  | import pathlib | 
|  | import sys | 
|  |  | 
|  | # Add build_tools python dir to the search path. | 
|  | sys.path.insert(0, str(pathlib.Path(__file__).parent.with_name("python"))) | 
|  |  | 
|  | import argparse | 
|  | import json | 
|  | import os | 
|  | import shlex | 
|  | import subprocess | 
|  | from typing import Dict, List, Optional, Sequence | 
|  | import functools | 
|  |  | 
|  | from e2e_test_artifacts import model_artifacts, iree_artifacts | 
|  | from e2e_test_framework import serialization | 
|  | from e2e_test_framework.definitions import iree_definitions | 
|  |  | 
|  | IREE_COMPILER_NAME = "iree-compile" | 
|  |  | 
|  |  | 
|  | def _convert_to_cmd_string(cmds: Sequence[str]) -> str: | 
|  | if os.name == "nt": | 
|  | # list2cmdline is an undocumented method for Windows command lines. Python | 
|  | # doesn't provide an official method for quoting Windows command lines and | 
|  | # the correct implementation is slightly non-trivial. Use the undocumented | 
|  | # method for now and can be rewritten with our own implementation later. | 
|  | # See https://learn.microsoft.com/en-us/archive/blogs/twistylittlepassagesallalike/everyone-quotes-command-line-arguments-the-wrong-way | 
|  | return subprocess.list2cmdline(cmds) | 
|  |  | 
|  | return " ".join(shlex.quote(cmd) for cmd in cmds) | 
|  |  | 
|  |  | 
|  | def _dump_cmds_of_generation_config( | 
|  | gen_config: iree_definitions.ModuleGenerationConfig, | 
|  | root_path: pathlib.PurePath = pathlib.PurePath(), | 
|  | ): | 
|  | imported_model = gen_config.imported_model | 
|  | imported_model_path = iree_artifacts.get_imported_model_path( | 
|  | imported_model=imported_model, root_path=root_path | 
|  | ) | 
|  | module_dir_path = iree_artifacts.get_module_dir_path( | 
|  | module_generation_config=gen_config, root_path=root_path | 
|  | ) | 
|  | module_path = module_dir_path / iree_artifacts.MODULE_FILENAME | 
|  | compile_cmds = [ | 
|  | IREE_COMPILER_NAME, | 
|  | str(imported_model_path), | 
|  | "-o", | 
|  | str(module_path), | 
|  | ] | 
|  | compile_cmds += gen_config.materialize_compile_flags( | 
|  | module_dir_path=module_dir_path | 
|  | ) | 
|  | compile_cmd_str = _convert_to_cmd_string(compile_cmds) | 
|  |  | 
|  | if imported_model.import_config.tool == iree_definitions.ImportTool.NONE: | 
|  | import_cmd_str = "# (Source model is already in MLIR)" | 
|  | else: | 
|  | source_model_path = model_artifacts.get_model_path( | 
|  | model=imported_model.model, root_path=root_path | 
|  | ) | 
|  | import_cmds = [ | 
|  | imported_model.import_config.tool.value, | 
|  | str(source_model_path), | 
|  | "-o", | 
|  | str(imported_model_path), | 
|  | ] | 
|  | import_cmds += imported_model.import_config.materialize_import_flags( | 
|  | model=imported_model.model | 
|  | ) | 
|  | import_cmd_str = _convert_to_cmd_string(import_cmds) | 
|  |  | 
|  | # Insert a blank line after each command to help read with line wrap. | 
|  | return ["Compile Module:", compile_cmd_str, "", "Import Model:", import_cmd_str, ""] | 
|  |  | 
|  |  | 
|  | def _dump_cmds_from_run_config( | 
|  | run_config: iree_definitions.E2EModelRunConfig, | 
|  | root_path: pathlib.PurePath = pathlib.PurePath(), | 
|  | ): | 
|  | gen_config = run_config.module_generation_config | 
|  | module_path = ( | 
|  | iree_artifacts.get_module_dir_path( | 
|  | module_generation_config=gen_config, root_path=root_path | 
|  | ) | 
|  | / iree_artifacts.MODULE_FILENAME | 
|  | ) | 
|  |  | 
|  | run_cmds = [run_config.tool.value, f"--module={module_path}"] | 
|  | run_cmds += run_config.materialize_run_flags() | 
|  | # Insert a blank line after the command to help read with line wrap. | 
|  | lines = ["Run Module:", _convert_to_cmd_string(run_cmds), ""] | 
|  | lines += _dump_cmds_of_generation_config(gen_config=gen_config, root_path=root_path) | 
|  | return lines | 
|  |  | 
|  |  | 
|  | def _dump_cmds_handler( | 
|  | e2e_test_artifacts_dir: pathlib.Path, | 
|  | execution_benchmark_config: Optional[pathlib.Path], | 
|  | compilation_benchmark_config: Optional[pathlib.Path], | 
|  | benchmark_id: Optional[str], | 
|  | **_unused_args, | 
|  | ): | 
|  | lines = [] | 
|  |  | 
|  | if execution_benchmark_config is not None: | 
|  | benchmark_groups = json.loads(execution_benchmark_config.read_text()) | 
|  | for target_device, benchmark_group in benchmark_groups.items(): | 
|  | shard_count = len(benchmark_group["shards"]) | 
|  | for shard in benchmark_group["shards"]: | 
|  | run_configs = serialization.unpack_and_deserialize( | 
|  | data=shard["run_configs"], | 
|  | root_type=List[iree_definitions.E2EModelRunConfig], | 
|  | ) | 
|  | for run_config in run_configs: | 
|  | if ( | 
|  | benchmark_id is not None | 
|  | and benchmark_id != run_config.composite_id | 
|  | ): | 
|  | continue | 
|  |  | 
|  | lines.append("################") | 
|  | lines.append("") | 
|  | lines.append(f"Execution Benchmark ID: {run_config.composite_id}") | 
|  | lines.append(f"Name: {run_config}") | 
|  | lines.append(f"Target Device: {target_device}") | 
|  | lines.append(f"Shard: {shard['index']} / {shard_count}") | 
|  | lines.append("") | 
|  | lines += _dump_cmds_from_run_config( | 
|  | run_config=run_config, root_path=e2e_test_artifacts_dir | 
|  | ) | 
|  |  | 
|  | if compilation_benchmark_config is not None: | 
|  | benchmark_config = json.loads(compilation_benchmark_config.read_text()) | 
|  | gen_configs = serialization.unpack_and_deserialize( | 
|  | data=benchmark_config["generation_configs"], | 
|  | root_type=List[iree_definitions.ModuleGenerationConfig], | 
|  | ) | 
|  | for gen_config in gen_configs: | 
|  | if benchmark_id is not None and benchmark_id != gen_config.composite_id: | 
|  | continue | 
|  |  | 
|  | lines.append("################") | 
|  | lines.append("") | 
|  | lines.append(f"Compilation Benchmark ID: {gen_config.composite_id}") | 
|  | lines.append(f"Name: {gen_config}") | 
|  | lines.append("") | 
|  | lines += _dump_cmds_of_generation_config( | 
|  | gen_config=gen_config, root_path=e2e_test_artifacts_dir | 
|  | ) | 
|  |  | 
|  | print(*lines, sep="\n") | 
|  |  | 
|  |  | 
|  | # Represents a benchmark results file with the data already loaded from a JSON file. | 
|  | class JSONBackedBenchmarkData: | 
|  | def __init__(self, source_filepath: pathlib.PurePath, data: Dict): | 
|  | if not isinstance(data, dict): | 
|  | raise ValueError( | 
|  | f"'{source_filepath}' seems not to be a valid benchmark-results-file (No JSON struct as root element)." | 
|  | ) | 
|  | if "commit" not in data: | 
|  | raise ValueError( | 
|  | f"'{source_filepath}' seems not to be a valid benchmark-results-file ('commit' field not found)." | 
|  | ) | 
|  | if "benchmarks" not in data: | 
|  | raise ValueError( | 
|  | f"'{source_filepath}' seems not to be a valid benchmark-results-file ('benchmarks' field not found)." | 
|  | ) | 
|  |  | 
|  | self.source_filepath: pathlib.PurePath = source_filepath | 
|  | self.data: Dict = data | 
|  |  | 
|  | # Parses a JSON benchmark results file and makes some sanity checks | 
|  | @staticmethod | 
|  | def load_from_file(filepath: pathlib.Path): | 
|  | try: | 
|  | data = json.loads(filepath.read_bytes()) | 
|  | except json.JSONDecodeError as e: | 
|  | raise ValueError(f"'{filepath}' seems not to be a valid JSON file: {e.msg}") | 
|  | return JSONBackedBenchmarkData(filepath, data) | 
|  |  | 
|  | # A convenience wrapper around `loadFromFile` that accepts a sequence of paths and returns a sequence of JSONBackedBenchmarkData objects as a generator. | 
|  | @staticmethod | 
|  | def load_many_from_files(filepaths: Sequence[pathlib.Path]): | 
|  | return ( | 
|  | JSONBackedBenchmarkData.load_from_file(filepath) for filepath in filepaths | 
|  | ) | 
|  |  | 
|  |  | 
|  | # Merges the benchmark results from `right` into `left` and returns the updated `left` | 
|  | def _merge_two_resultsets( | 
|  | left: JSONBackedBenchmarkData, right: JSONBackedBenchmarkData | 
|  | ) -> JSONBackedBenchmarkData: | 
|  | if left.data["commit"] != right.data["commit"]: | 
|  | raise ValueError( | 
|  | f"'{right.source_filepath}' and the previous files are based on different commits ({left.data['commit']} != {right.data['commit']}). Merging not supported." | 
|  | ) | 
|  | left.data["benchmarks"].extend(right.data["benchmarks"]) | 
|  | return left | 
|  |  | 
|  |  | 
|  | def merge_results(benchmark_results: Sequence[JSONBackedBenchmarkData]): | 
|  | return functools.reduce(_merge_two_resultsets, benchmark_results) | 
|  |  | 
|  |  | 
|  | def _merge_results_handler( | 
|  | benchmark_results_files: Sequence[pathlib.Path], **_unused_args | 
|  | ): | 
|  | print( | 
|  | json.dumps( | 
|  | merge_results( | 
|  | JSONBackedBenchmarkData.load_many_from_files(benchmark_results_files) | 
|  | ) | 
|  | ) | 
|  | ) | 
|  |  | 
|  |  | 
|  | def _parse_arguments() -> argparse.Namespace: | 
|  | parser = argparse.ArgumentParser( | 
|  | description="Miscellaneous tool to help work with benchmark suite and benchmark CI." | 
|  | ) | 
|  |  | 
|  | subparser = parser.add_subparsers( | 
|  | required=True, title="operation", dest="operation" | 
|  | ) | 
|  | dump_cmds_parser = subparser.add_parser( | 
|  | "dump-cmds", help="Dump the commands to compile and run benchmarks manually." | 
|  | ) | 
|  | dump_cmds_parser.add_argument( | 
|  | "--e2e_test_artifacts_dir", | 
|  | type=pathlib.PurePath, | 
|  | default=pathlib.Path(), | 
|  | help="E2E test artifacts root path used in the outputs of artifact paths", | 
|  | ) | 
|  | dump_cmds_parser.add_argument( | 
|  | "--benchmark_id", type=str, help="Only dump the benchmark with this id" | 
|  | ) | 
|  | dump_cmds_parser.add_argument( | 
|  | "--execution_benchmark_config", | 
|  | type=pathlib.Path, | 
|  | help="Config file exported from export_benchmark_config.py execution", | 
|  | ) | 
|  | dump_cmds_parser.add_argument( | 
|  | "--compilation_benchmark_config", | 
|  | type=pathlib.Path, | 
|  | help="Config file exported from export_benchmark_config.py compilation", | 
|  | ) | 
|  | dump_cmds_parser.set_defaults(handler=_dump_cmds_handler) | 
|  |  | 
|  | merge_results_parser = subparser.add_parser( | 
|  | "merge-results", | 
|  | help="Merges the results from multiple benchmark results JSON files into a single JSON structure.", | 
|  | ) | 
|  | merge_results_parser.add_argument( | 
|  | "benchmark_results_files", | 
|  | type=pathlib.Path, | 
|  | nargs="+", | 
|  | help="One or more benchmark results JSON file paths", | 
|  | ) | 
|  | merge_results_parser.set_defaults(handler=_merge_results_handler) | 
|  |  | 
|  | args = parser.parse_args() | 
|  | if ( | 
|  | args.operation == "dump-cmds" | 
|  | and args.execution_benchmark_config is None | 
|  | and args.compilation_benchmark_config is None | 
|  | ): | 
|  | parser.error( | 
|  | "At least one of --execution_benchmark_config or " | 
|  | "--compilation_benchmark_config must be set." | 
|  | ) | 
|  |  | 
|  | return args | 
|  |  | 
|  |  | 
|  | def main(args: argparse.Namespace): | 
|  | args.handler(**vars(args)) | 
|  |  | 
|  |  | 
|  | if __name__ == "__main__": | 
|  | main(_parse_arguments()) |