blob: 44c6893cf4bedc03fc460c62402e88d45f03e51a [file] [log] [blame]
#!/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())