| #!/usr/bin/env python3 | |
| # Copyright 2021 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 | |
| """Runs all matched benchmark suites on an Android device. | |
| This script probes the Android phone via `adb` and uses the device information | |
| to filter and run suitable benchmarks and optionally captures Tracy traces on | |
| the Android phone. | |
| It expects that `adb` is installed, and there is iree tools cross-compiled | |
| towards Android. If to capture traces, another set of tracing-enabled iree | |
| tools and the Tracy `capture` tool should be cross-compiled towards Android. | |
| Example usages: | |
| # Without trace generation | |
| python3 run_benchmarks.py \ | |
| --normal_benchmark_tool_dir=/path/to/normal/android/target/tools/dir \ | |
| /path/to/host/build/dir | |
| # With trace generation | |
| python3 run_benchmarks.py \ | |
| --normal_benchmark_tool_dir=/path/to/normal/android/target/tools/dir \ | |
| --traced_benchmark_tool_dir=/path/to/tracy/android/target/tools/dir \ | |
| --trace_capture_tool=/path/to/host/build/tracy/capture \ | |
| /path/to/host/build/dir | |
| """ | |
| import sys | |
| import pathlib | |
| # Add build_tools python dir to the search path. | |
| sys.path.insert(0, str(pathlib.Path(__file__).parent.with_name("python"))) | |
| import atexit | |
| import json | |
| import shutil | |
| import subprocess | |
| import tarfile | |
| from typing import Any, List, Optional, Sequence, Tuple | |
| from common.benchmark_config import BenchmarkConfig | |
| from common.benchmark_driver import BenchmarkDriver | |
| from common.benchmark_definition import ( | |
| DriverInfo, | |
| execute_cmd, | |
| execute_cmd_and_get_stdout, | |
| execute_cmd_and_get_output, | |
| get_git_commit_hash, | |
| get_iree_benchmark_module_arguments, | |
| wait_for_iree_benchmark_module_start, | |
| parse_iree_benchmark_metrics, | |
| ) | |
| from common.benchmark_suite import MODEL_FLAGFILE_NAME, BenchmarkCase, BenchmarkSuite | |
| from common.android_device_utils import ( | |
| get_android_device_model, | |
| get_android_device_info, | |
| get_android_gpu_name, | |
| ) | |
| import common.common_arguments | |
| from e2e_test_artifacts import iree_artifacts | |
| from e2e_test_framework import serialization | |
| from e2e_test_framework.definitions import common_definitions, iree_definitions | |
| from e2e_test_framework.device_specs import device_parameters | |
| # Root directory to perform benchmarks in on the Android device. | |
| ANDROID_TMPDIR = pathlib.PurePosixPath("/data/local/tmp/iree-benchmarks") | |
| NORMAL_TOOL_REL_DIR = pathlib.PurePosixPath("normal-tools") | |
| TRACED_TOOL_REL_DIR = pathlib.PurePosixPath("traced-tools") | |
| def adb_push_to_tmp_dir( | |
| content: pathlib.Path, | |
| relative_dir: pathlib.PurePosixPath = pathlib.PurePosixPath(), | |
| verbose: bool = False, | |
| ) -> pathlib.PurePosixPath: | |
| """Pushes content onto the Android device. | |
| Args: | |
| content: the full path to the source file. | |
| relative_dir: the directory to push to; relative to ANDROID_TMPDIR. | |
| Returns: | |
| The full path to the content on the Android device. | |
| """ | |
| filename = content.name | |
| android_path = ANDROID_TMPDIR / relative_dir / filename | |
| # When the output is a TTY, keep the default progress info output. | |
| # In other cases, redirect progress info to null to avoid bloating log files. | |
| stdout_redirect = None if sys.stdout.isatty() else subprocess.DEVNULL | |
| execute_cmd( | |
| ["adb", "push", content.resolve(), android_path], | |
| verbose=verbose, | |
| stdout=stdout_redirect, | |
| ) | |
| return android_path | |
| def adb_execute_and_get_output( | |
| cmd_args: Sequence[str], | |
| relative_dir: pathlib.PurePosixPath = pathlib.PurePosixPath(), | |
| verbose: bool = False, | |
| ) -> Tuple[str, str]: | |
| """Executes command with adb shell. | |
| Switches to `relative_dir` relative to the android tmp directory before | |
| executing. Waits for completion and returns the command stdout. | |
| Args: | |
| cmd_args: a list containing the command to execute and its parameters | |
| relative_dir: the directory to execute the command in; relative to | |
| ANDROID_TMPDIR. | |
| Returns: | |
| Strings for stdout and stderr. | |
| """ | |
| cmd = ["adb", "shell", "cd", ANDROID_TMPDIR / relative_dir, "&&"] | |
| cmd.extend(cmd_args) | |
| return execute_cmd_and_get_output(cmd, verbose=verbose) | |
| def adb_execute( | |
| cmd_args: Sequence[str], | |
| relative_dir: pathlib.PurePosixPath = pathlib.PurePosixPath(), | |
| verbose: bool = False, | |
| ) -> subprocess.CompletedProcess: | |
| """Executes command with adb shell. | |
| Switches to `relative_dir` relative to the android tmp directory before | |
| executing. Waits for completion. Output is streamed to the terminal. | |
| Args: | |
| cmd_args: a list containing the command to execute and its parameters | |
| relative_dir: the directory to execute the command in; relative to | |
| ANDROID_TMPDIR. | |
| Returns: | |
| The completed process. | |
| """ | |
| cmd = ["adb", "shell", "cd", ANDROID_TMPDIR / relative_dir, "&&"] | |
| cmd.extend(cmd_args) | |
| return execute_cmd(cmd, verbose=verbose) | |
| def is_magisk_su(): | |
| """Returns true if the Android device has a Magisk SU binary.""" | |
| stdout, _ = adb_execute_and_get_output(["su", "--help"]) | |
| return "MagiskSU" in stdout | |
| def adb_execute_as_root(cmd_args: Sequence[Any]) -> subprocess.CompletedProcess: | |
| """Executes the given command as root.""" | |
| cmd = ["su", "-c" if is_magisk_su() else "root"] | |
| cmd.extend(cmd_args) | |
| return adb_execute(cmd) | |
| def adb_start_cmd( | |
| cmd_args: Sequence[str], | |
| relative_dir: pathlib.PurePosixPath = pathlib.PurePosixPath(), | |
| verbose: bool = False, | |
| ) -> subprocess.Popen: | |
| """Executes command with adb shell in a directory and returns the handle | |
| without waiting for completion. | |
| Args: | |
| cmd_args: a list containing the command to execute and its parameters | |
| relative_dir: the directory to execute the command in; relative to | |
| ANDROID_TMPDIR. | |
| Returns: | |
| A Popen object for the started command. | |
| """ | |
| cmd = ["adb", "shell", "cd", ANDROID_TMPDIR / relative_dir, "&&"] | |
| cmd.extend(cmd_args) | |
| if verbose: | |
| print(f"cmd: {cmd}") | |
| return subprocess.Popen(cmd, stdout=subprocess.PIPE, text=True) | |
| def get_vmfb_full_path_for_benchmark_case( | |
| benchmark_case_dir: pathlib.Path, | |
| ) -> pathlib.Path: | |
| flagfile = benchmark_case_dir / MODEL_FLAGFILE_NAME | |
| for line in flagfile.read_text().splitlines(): | |
| flag_name, flag_value = line.strip().split("=") | |
| if flag_name == "--module": | |
| # Realpath canonicalization matters. The caller may rely on that to track | |
| # which files it already pushed. | |
| return (benchmark_case_dir / flag_value).resolve() | |
| raise ValueError(f"{flagfile} does not contain a --module flag") | |
| class AndroidBenchmarkDriver(BenchmarkDriver): | |
| """Android benchmark driver.""" | |
| def __init__(self, *args, **kwargs): | |
| super().__init__(*args, **kwargs) | |
| self.already_pushed_files = {} | |
| def run_benchmark_case( | |
| self, | |
| benchmark_case: BenchmarkCase, | |
| benchmark_results_filename: Optional[pathlib.Path], | |
| capture_filename: Optional[pathlib.Path], | |
| ) -> None: | |
| benchmark_case_dir = benchmark_case.benchmark_case_dir | |
| android_case_dir = pathlib.PurePosixPath( | |
| benchmark_case_dir.relative_to(self.config.root_benchmark_dir) | |
| ) | |
| run_config = benchmark_case.run_config | |
| self.__check_and_push_file( | |
| benchmark_case_dir / iree_artifacts.MODULE_FILENAME, android_case_dir | |
| ) | |
| taskset = self.__deduce_taskset_from_run_config(run_config) | |
| run_args = run_config.materialize_run_flags() | |
| run_args.append(f"--module={iree_artifacts.MODULE_FILENAME}") | |
| if benchmark_results_filename is not None: | |
| self.__run_benchmark( | |
| android_case_dir=android_case_dir, | |
| tool_name=benchmark_case.benchmark_tool_name, | |
| driver_info=benchmark_case.driver_info, | |
| run_args=run_args, | |
| results_filename=benchmark_results_filename, | |
| taskset=taskset, | |
| ) | |
| if capture_filename is not None: | |
| self.__run_capture( | |
| android_case_dir=android_case_dir, | |
| tool_name=benchmark_case.benchmark_tool_name, | |
| run_args=run_args, | |
| capture_filename=capture_filename, | |
| taskset=taskset, | |
| ) | |
| def __run_benchmark( | |
| self, | |
| android_case_dir: pathlib.PurePosixPath, | |
| tool_name: str, | |
| driver_info: DriverInfo, | |
| run_args: Sequence[str], | |
| results_filename: pathlib.Path, | |
| taskset: str, | |
| ): | |
| if self.config.normal_benchmark_tool_dir is None: | |
| raise ValueError("normal_benchmark_tool_dir can't be None.") | |
| host_tool_path = self.config.normal_benchmark_tool_dir / tool_name | |
| android_tool = self.__check_and_push_file(host_tool_path, NORMAL_TOOL_REL_DIR) | |
| cmd = ["taskset", taskset, android_tool] | |
| cmd += run_args | |
| if tool_name == "iree-benchmark-module": | |
| cmd += get_iree_benchmark_module_arguments( | |
| results_filename=f"'{results_filename.name}'", | |
| driver_info=driver_info, | |
| benchmark_min_time=self.config.benchmark_min_time, | |
| ) | |
| benchmark_stdout, benchmark_stderr = adb_execute_and_get_output( | |
| cmd, android_case_dir, verbose=self.verbose | |
| ) | |
| benchmark_metrics = parse_iree_benchmark_metrics( | |
| benchmark_stdout, benchmark_stderr | |
| ) | |
| if self.verbose: | |
| print(benchmark_metrics) | |
| results_filename.write_text(json.dumps(benchmark_metrics.to_json_object())) | |
| def __run_capture( | |
| self, | |
| android_case_dir: pathlib.PurePosixPath, | |
| tool_name: str, | |
| capture_filename: pathlib.Path, | |
| run_args: Sequence[str], | |
| taskset: str, | |
| ): | |
| capture_config = self.config.trace_capture_config | |
| if capture_config is None: | |
| raise ValueError("capture_config can't be None.") | |
| host_tool_path = capture_config.traced_benchmark_tool_dir / tool_name | |
| android_tool = self.__check_and_push_file(host_tool_path, TRACED_TOOL_REL_DIR) | |
| run_cmd = [ | |
| "TRACY_NO_EXIT=1", | |
| f"IREE_PRESERVE_DYLIB_TEMP_FILES={ANDROID_TMPDIR}", | |
| "taskset", | |
| taskset, | |
| android_tool, | |
| ] | |
| run_cmd += run_args | |
| # Just launch the traced benchmark tool with TRACY_NO_EXIT=1 without | |
| # waiting for the adb command to complete as that won't happen. | |
| process = adb_start_cmd(run_cmd, android_case_dir, verbose=self.verbose) | |
| wait_for_iree_benchmark_module_start(process, self.verbose) | |
| # Now it's okay to collect the trace via the capture tool. This will | |
| # send the signal to let the previously waiting benchmark tool to | |
| # complete. | |
| capture_cmd = [capture_config.trace_capture_tool, "-f", "-o", capture_filename] | |
| # If verbose, just let the subprocess print its output. The subprocess | |
| # may need to detect if the output is a TTY to decide whether to log | |
| # verbose progress info and use ANSI colors, so it's better to use | |
| # stdout redirection than to capture the output in a string. | |
| stdout_redirect = None if self.verbose else subprocess.DEVNULL | |
| execute_cmd(capture_cmd, verbose=self.verbose, stdout=stdout_redirect) | |
| # TODO(#13187): These logics are inherited from the legacy benchmark suites, | |
| # which only work for a few specific phones. We should define the topology | |
| # in their device specs. | |
| def __deduce_taskset_from_run_config( | |
| self, run_config: iree_definitions.E2EModelRunConfig | |
| ) -> str: | |
| """Deduces the CPU mask according to device and execution config.""" | |
| device_spec = run_config.target_device_spec | |
| # For GPU benchmarks, use the most performant core. | |
| if device_spec.architecture.type == common_definitions.ArchitectureType.GPU: | |
| return "80" | |
| device_params = device_spec.device_parameters | |
| single_thread = "1-thread" in run_config.module_execution_config.tags | |
| if device_parameters.ARM_BIG_CORES in device_params: | |
| return "80" if single_thread else "f0" | |
| elif device_parameters.ARM_LITTLE_CORES in device_params: | |
| return "08" if single_thread else "0f" | |
| raise ValueError(f"Unsupported config to deduce taskset: '{run_config}'.") | |
| def __check_and_push_file( | |
| self, host_path: pathlib.Path, relative_dir: pathlib.PurePosixPath | |
| ): | |
| """Checks if the file has been pushed and pushes it if not.""" | |
| android_path = self.already_pushed_files.get(host_path) | |
| if android_path is not None: | |
| return android_path | |
| android_path = adb_push_to_tmp_dir( | |
| host_path, relative_dir=relative_dir, verbose=self.verbose | |
| ) | |
| self.already_pushed_files[host_path] = android_path | |
| return android_path | |
| def set_cpu_frequency_scaling_governor(governor: str): | |
| git_root = execute_cmd_and_get_stdout(["git", "rev-parse", "--show-toplevel"]) | |
| cpu_script = ( | |
| pathlib.Path(git_root) | |
| / "build_tools" | |
| / "benchmarks" | |
| / "set_android_scaling_governor.sh" | |
| ) | |
| android_path = adb_push_to_tmp_dir(cpu_script) | |
| adb_execute_as_root([android_path, governor]) | |
| def set_gpu_frequency_scaling_policy(policy: str): | |
| git_root = execute_cmd_and_get_stdout(["git", "rev-parse", "--show-toplevel"]) | |
| device_model = get_android_device_model() | |
| gpu_name = get_android_gpu_name() | |
| benchmarks_tool_dir = pathlib.Path(git_root) / "build_tools" / "benchmarks" | |
| if device_model == "Pixel-6" or device_model == "Pixel-6-Pro": | |
| gpu_script = benchmarks_tool_dir / "set_pixel6_gpu_scaling_policy.sh" | |
| elif gpu_name.lower().startswith("adreno"): | |
| gpu_script = benchmarks_tool_dir / "set_adreno_gpu_scaling_policy.sh" | |
| else: | |
| raise RuntimeError( | |
| f"Unsupported device '{device_model}' for setting GPU scaling policy" | |
| ) | |
| android_path = adb_push_to_tmp_dir(gpu_script) | |
| adb_execute_as_root([android_path, policy]) | |
| def main(args): | |
| device_info = get_android_device_info(args.verbose) | |
| if args.verbose: | |
| print(device_info) | |
| commit = get_git_commit_hash("HEAD") | |
| benchmark_config = BenchmarkConfig.build_from_args(args, commit) | |
| benchmark_groups = json.loads(args.execution_benchmark_config.read_text()) | |
| benchmark_group = benchmark_groups.get(args.target_device_name) | |
| if benchmark_group is None: | |
| raise ValueError("Target device not found in the benchmark config.") | |
| run_configs = serialization.unpack_and_deserialize( | |
| data=benchmark_group["run_configs"], | |
| root_type=List[iree_definitions.E2EModelRunConfig], | |
| ) | |
| benchmark_suite = BenchmarkSuite.load_from_run_configs( | |
| run_configs=run_configs, root_benchmark_dir=benchmark_config.root_benchmark_dir | |
| ) | |
| benchmark_driver = AndroidBenchmarkDriver( | |
| device_info=device_info, | |
| benchmark_config=benchmark_config, | |
| benchmark_suite=benchmark_suite, | |
| benchmark_grace_time=1.0, | |
| verbose=args.verbose, | |
| ) | |
| if args.pin_cpu_freq: | |
| set_cpu_frequency_scaling_governor("performance") | |
| atexit.register(set_cpu_frequency_scaling_governor, "schedutil") | |
| if args.pin_gpu_freq: | |
| set_gpu_frequency_scaling_policy("performance") | |
| atexit.register(set_gpu_frequency_scaling_policy, "default") | |
| # Clear the benchmark directory on the Android device first just in case | |
| # there are leftovers from manual or failed runs. | |
| execute_cmd_and_get_stdout( | |
| ["adb", "shell", "rm", "-rf", ANDROID_TMPDIR], verbose=args.verbose | |
| ) | |
| if not args.no_clean: | |
| # Clear the benchmark directory on the Android device. | |
| atexit.register( | |
| execute_cmd_and_get_stdout, | |
| ["adb", "shell", "rm", "-rf", ANDROID_TMPDIR], | |
| verbose=args.verbose, | |
| ) | |
| # Also clear temporary directory on the host device. | |
| atexit.register(shutil.rmtree, args.tmp_dir) | |
| # Tracy client and server communicate over port 8086 by default. If we want | |
| # to capture traces along the way, forward port via adb. | |
| trace_capture_config = benchmark_config.trace_capture_config | |
| if trace_capture_config: | |
| execute_cmd_and_get_stdout( | |
| ["adb", "forward", "tcp:8086", "tcp:8086"], verbose=args.verbose | |
| ) | |
| atexit.register( | |
| execute_cmd_and_get_stdout, | |
| ["adb", "forward", "--remove", "tcp:8086"], | |
| verbose=args.verbose, | |
| ) | |
| benchmark_driver.run() | |
| benchmark_results = benchmark_driver.get_benchmark_results() | |
| if args.output is not None: | |
| with open(args.output, "w") as f: | |
| f.write(benchmark_results.to_json_str()) | |
| if args.verbose: | |
| print(benchmark_results.commit) | |
| print(benchmark_results.benchmarks) | |
| if trace_capture_config: | |
| # Put all captures in a tarball and remove the original files. | |
| with tarfile.open(trace_capture_config.capture_tarball, "w:gz") as tar: | |
| for capture_filename in benchmark_driver.get_capture_filenames(): | |
| tar.add(capture_filename) | |
| benchmark_errors = benchmark_driver.get_benchmark_errors() | |
| if benchmark_errors: | |
| print("Benchmarking completed with errors", file=sys.stderr) | |
| raise RuntimeError(benchmark_errors) | |
| if __name__ == "__main__": | |
| main(common.common_arguments.Parser().parse_args()) |