blob: a5ffacd860d59b9b3853c4945fb034a471937372 [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
"""Utilities for describing Android benchmarks.
This file provides common and structured representation of Android devices,
benchmark definitions, and benchmark result collections, so that they can be
shared between different stages of the same benchmark pipeline.
"""
import json
import re
import subprocess
from dataclasses import dataclass
from typing import Any, Dict, Sequence
__all__ = [
"AndroidDeviceInfo", "BenchmarkInfo", "BenchmarkResults", "get_output"
]
# A map for IREE driver names. This allows us to normalize driver names like
# mapping to more friendly ones and detach to keep driver names used in
# benchmark presentation stable.
IREE_DRIVER_NAME_MAP = {
"iree-dylib": "IREE-Dylib",
"iree-dylib-sync": "IREE-Dylib-Sync",
"iree-vmvx": "IREE-VMVX",
"iree-vulkan": "IREE-Vulkan",
}
def get_output(args: Sequence[str], verbose: bool = False, **kwargs) -> str:
"""Executes a command and returns its stdout."""
if verbose:
cmd = " ".join(args)
print(f"cmd: {cmd}")
return subprocess.run(args,
check=True,
stdout=subprocess.PIPE,
universal_newlines=True,
**kwargs).stdout.strip()
def get_android_device_model(verbose: bool = False) -> str:
"""Returns the Android device model."""
model = get_output(["adb", "shell", "getprop", "ro.product.model"],
verbose=verbose)
model = re.sub(r"\W+", "-", model)
return model
def get_android_cpu_abi(verbose: bool = False) -> str:
"""Returns the CPU ABI for the Android device."""
return get_output(["adb", "shell", "getprop", "ro.product.cpu.abi"],
verbose=verbose)
def get_android_cpu_features(verbose: bool = False) -> Sequence[str]:
"""Returns the CPU features for the Android device."""
cpuinfo = get_output(["adb", "shell", "cat", "/proc/cpuinfo"],
verbose=verbose)
features = []
for line in cpuinfo.splitlines():
if line.startswith("Features"):
_, features = line.split(":")
return features.strip().split()
return features
def get_android_gpu_name(verbose: bool = False) -> str:
"""Returns the GPU name for the Android device."""
vkjson = get_output(["adb", "shell", "cmd", "gpu", "vkjson"], verbose=verbose)
vkjson = json.loads(vkjson)
name = vkjson["devices"][0]["properties"]["deviceName"]
# Perform some canonicalization:
# - Adreno GPUs have raw names like "Adreno (TM) 650".
name = name.replace("(TM)", "")
# Replace all consecutive non-word characters with a single hypen.
name = re.sub(r"\W+", "-", name)
return name
@dataclass
class AndroidDeviceInfo:
"""An object describing the current Android Device.
It includes the following phone characteristics:
- model: the product model, e.g., 'Pixel-4'
- cpu_abi: the CPU ABI, e.g., 'arm64-v8a'
- cpu_features: the detailed CPU features, e.g., ['fphp', 'sve']
- gpu_name: the GPU name, e.g., 'Mali-G77'
"""
model: str
cpu_abi: str
cpu_features: Sequence[str]
gpu_name: str
def __str__(self):
features = ", ".join(self.cpu_features)
params = [
f"model='{self.model}'",
f"cpu_abi='{self.cpu_abi}'",
f"gpu_name='{self.gpu_name}'",
f"cpu_features=[{features}]",
]
params = ", ".join(params)
return f"Android device <{params}>"
def get_arm_arch_revision(self) -> str:
"""Returns the ARM architecture revision."""
if self.cpu_abi != "arm64-v8a":
raise ValueError("Unrecognized ARM CPU ABI; need to update the list")
# CPU features for ARMv8 revisions.
# From https://en.wikichip.org/wiki/arm/armv8#ARMv8_Extensions_and_Processor_Features
rev1_features = ["atomics", "asimdrdm"]
rev2_features = [
"fphp", "dcpop", "sha3", "sm3", "sm4", "asimddp", "sha512", "sve"
]
rev = "ARMv8-A"
if any([f in self.cpu_features for f in rev1_features]):
rev = "ARMv8.1-A"
if any([f in self.cpu_features for f in rev2_features]):
rev = "ARMv8.2-A"
return rev
def to_json_object(self) -> Dict[str, Any]:
return {
"model": self.model,
"cpu_abi": self.cpu_abi,
"cpu_features": self.cpu_features,
"gpu_name": self.gpu_name,
}
@staticmethod
def from_json_object(json_object: Dict[str, Any]):
return AndroidDeviceInfo(json_object["model"], json_object["cpu_abi"],
json_object["cpu_features"],
json_object["gpu_name"])
@staticmethod
def from_adb(verbose: bool = False):
return AndroidDeviceInfo(get_android_device_model(verbose),
get_android_cpu_abi(verbose),
get_android_cpu_features(verbose),
get_android_gpu_name(verbose))
@dataclass
class BenchmarkInfo:
"""An object describing the current benchmark.
It includes the following benchmark characteristics:
- model_name: the model name, e.g., 'MobileNetV2'
- model_tags: a list of tags used to describe additional model information,
e.g., ['imagenet']
- model_source: the source of the model, e.g., 'TensorFlow'
- bench_mode: a list of tags for benchmark mode,
e.g., ['1-thread', 'big-core', 'full-inference']
- runner: which runner is used for benchmarking, e.g., 'iree_vulkan', 'tflite'
- device_info: an AndroidDeviceInfo object describing the phone where
bnechmarks run
"""
model_name: str
model_tags: Sequence[str]
model_source: str
bench_mode: Sequence[str]
runner: str
device_info: AndroidDeviceInfo
def __str__(self):
# Get the target architecture and better driver name depending on the runner.
target_arch = ""
driver = ""
if self.runner == "iree-vulkan":
target_arch = "GPU-" + self.device_info.gpu_name
driver = IREE_DRIVER_NAME_MAP[self.runner]
elif (self.runner == "iree-dylib" or self.runner == "iree-dylib-sync" or
self.runner == "iree-vmvx"):
target_arch = "CPU-" + self.device_info.get_arm_arch_revision()
driver = IREE_DRIVER_NAME_MAP[self.runner]
else:
raise ValueError("Unrecognized runner; need to update the list")
if self.model_tags:
tags = ",".join(self.model_tags)
model_part = f"{self.model_name} [{tags}] ({self.model_source})"
else:
model_part = f"{self.model_name} ({self.model_source})"
phone_part = f"{self.device_info.model} ({target_arch})"
mode = ",".join(self.bench_mode)
return f"{model_part} {mode} with {driver} @ {phone_part}"
def deduce_taskset(self) -> str:
"""Deduces the CPU affinity taskset mask according to benchmark modes."""
# TODO: we actually should check the number of cores the phone have.
if "big-core" in self.bench_mode:
return "80" if "1-thread" in self.bench_mode else "f0"
if "little-core" in self.bench_mode:
return "08" if "1-thread" in self.bench_mode else "0f"
# Not specified: use the 7th core.
return "80"
def to_json_object(self) -> Dict[str, Any]:
return {
"model_name": self.model_name,
"model_tags": self.model_tags,
"model_source": self.model_source,
"bench_mode": self.bench_mode,
"runner": self.runner,
"device_info": self.device_info.to_json_object(),
}
@staticmethod
def from_json_object(json_object: Dict[str, Any]):
return BenchmarkInfo(model_name=json_object["model_name"],
model_tags=json_object["model_tags"],
model_source=json_object["model_source"],
bench_mode=json_object["bench_mode"],
runner=json_object["runner"],
device_info=AndroidDeviceInfo.from_json_object(
json_object["device_info"]))
class BenchmarkResults(object):
"""An object describing a set of benchmarks for one particular commit.
It contains the following fields:
- commit: the commit SHA for this set of benchmarks.
- benchmarks: a list of benchmarks, each with
- benchmark: a BenchmarkInfo object
- context: the context for running the benchmarks
- results: results for all benchmark runs
"""
def __init__(self):
self.commit = "<unknown>"
self.benchmarks = []
def set_commit(self, commit: str):
self.commit = commit
def append_one_benchmark(self, benchmark_info: BenchmarkInfo,
run_context: Dict[str, Any],
run_results: Sequence[Dict[str, Any]]):
"""Appends the results for one benchmark."""
self.benchmarks.append({
"benchmark": benchmark_info,
"context": run_context,
"results": run_results,
})
def merge(self, other):
if self.commit != other.commit:
raise ValueError("Inconsistent pull request commit")
self.benchmarks.extend(other.benchmarks)
def get_aggregate_time(self, benchmark_index: int, kind: str) -> int:
"""Returns the Google Benchmark aggreate time for the given kind.
Args:
- benchmark_index: the benchmark's index.
- kind: what kind of aggregate time to get; choices:
'mean', 'median', 'stddev'.
"""
time = None
for bench_case in self.benchmarks[benchmark_index]["results"]:
if bench_case["name"].endswith(f"real_time_{kind}"):
if bench_case["time_unit"] != "ms":
raise ValueError(f"Expected ms as time unit")
time = int(round(bench_case["real_time"]))
break
if time is None:
raise ValueError(f"Cannot found real_time_{kind} in benchmark results")
return time
def to_json_str(self) -> str:
json_object = {"commit": self.commit, "benchmarks": []}
for benchmark in self.benchmarks:
json_object["benchmarks"].append({
"benchmark": benchmark["benchmark"].to_json_object(),
"context": benchmark["context"],
"results": benchmark["results"],
})
return json.dumps(json_object)
@staticmethod
def from_json_str(json_str: str):
json_object = json.loads(json_str)
results = BenchmarkResults()
results.set_commit(json_object["commit"])
for benchmark in json_object["benchmarks"]:
results.append_one_benchmark(
BenchmarkInfo.from_json_object(benchmark["benchmark"]),
benchmark["context"], benchmark["results"])
return results