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