| #!/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()) |