| # Copyright 2022 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 | |
| r"""Script to generate a HTML summary comparing IREE and TFLite latencies and memory usage. | |
| Example usage: | |
| python parse_tflite_benchmarks.py \ | |
| --iree_version=20220924.276 \ | |
| --tflite_version=20220924.162 \ | |
| --platform=server \ | |
| --input_csv=server_results.csv \ | |
| --output_path=/tmp/server_summary.html | |
| """ | |
| import argparse | |
| import pandas as pd | |
| import pathlib | |
| import sys | |
| from datetime import date | |
| # Add build_tools python dir to the search path. | |
| sys.path.insert(0, str(pathlib.Path(__file__).parent / ".." / ".." / "python")) | |
| from reporting.common import html_utils | |
| # Supported platforms. | |
| _PLATFORM_SERVER = "server" | |
| _PLATFORM_MOBILE = "mobile" | |
| # A map of model name to data type. | |
| _MODEL_TO_DATA_TYPE = { | |
| "albert_lite_base_squadv1_1": "fp32", | |
| "albert_lite_base_squadv1_1_fp16": "fp16", | |
| "deeplabv3": "fp32", | |
| "deeplabv3_fp16": "fp16", | |
| "efficientnet_lite0_fp32_2": "fp32", | |
| "efficientnet_lite0_fp32_2_fp16": "fp16", | |
| "efficientnet_lite0_int8_2": "int8", | |
| "inception_v4_299_fp32": "fp32", | |
| "inception_v4_299_fp32_fp16": "fp16", | |
| "inception_v4_299_uint8": "uint8", | |
| "mobilebert-baseline-tf2-quant": "int8", | |
| "mobilebert_float_384_gpu": "fp32", | |
| "mobilebert_float_384_gpu_fp16": "fp16", | |
| "mobilenet_v2_1.0_224": "fp32", | |
| "mobilenet_v2_1.0_224_fp16": "fp16", | |
| "mobilenet_v2_224_1.0_uint8": "uint8", | |
| "person_detect": "int8", | |
| "resnet_v2_101_1_default_1": "fp32", | |
| "resnet_v2_101_1_default_1_fp16": "fp16", | |
| "ssd_mobilenet_v2_static_1.0_int8": "int8", | |
| "ssd_mobilenet_v2_fpnlite_fp32": "fp32", | |
| "ssd_mobilenet_v2_fpnlite_fp32_fp16": "fp16", | |
| "ssd_mobilenet_v2_fpnlite_uint8": 'uint8', | |
| } | |
| # Column headers. | |
| _MODEL = "model" | |
| _DATA_TYPE = "data type" | |
| _RUNTIME = "runtime" | |
| _LATENCY = "latency (ms)" | |
| _TASKSET = "taskset" | |
| _MEMORY = "vmhwm (KB)" | |
| _THREADS = "threads" | |
| _CONFIG = "config" | |
| _DRIVER = "driver/delegate" | |
| _TFLITE_CONFIG = "TFLite config" | |
| _IREE_CONFIG = "IREE config" | |
| _IREE_LATENCY = "IREE latency (ms)" | |
| _TFLITE_LATENCY = "TFLite latency (ms)" | |
| _IREE_MEMORY = "IREE vmhwm (kb)" | |
| _TFLITE_MEMORY = "TFLite vmhwm (kb)" | |
| _IREE_VS_TFLITE_LATENCY = "IREE vs TFLite latency" | |
| _IREE_VS_TFLITE_MEMORY = "IREE vs TFLite memory" | |
| _PERF_COLUMNS = [_IREE_VS_TFLITE_LATENCY, _IREE_VS_TFLITE_MEMORY] | |
| _NUMBER_COLUMNS = [_IREE_LATENCY, _TFLITE_LATENCY, _IREE_MEMORY, _TFLITE_MEMORY] | |
| _CONFIG_COLUMNS = [_TFLITE_CONFIG, _IREE_CONFIG] | |
| def get_tflite_model_list(df): | |
| """Retrieves the list of TFLite models, filtering out duplicates. | |
| The .csv file includes multiple entries of the same model but under a | |
| different configuration (e.g. XNNPack enabled, XNNPack disabled). | |
| """ | |
| df = df.loc[df.runtime == "tflite"] | |
| # Remove rows where the model name ends with `noxnn` since this is a duplicate. | |
| df = df[~df.model.str.endswith("noxnn")] | |
| return df.model.unique() | |
| def get_fastest_result(model, df): | |
| """Retrieves the lowest latency result from multiple configurations. | |
| Benchmarks are run under different configurations (e.g. number of threads, | |
| Big core, LITTLE core, etc). This method retrieves the fastest configuration | |
| whilst ensuring apples to apples comparisons (e.g. FP16 results are not | |
| considered when the model is FP32). | |
| Args: | |
| model: The model name. | |
| df: The dataframe to filter through. | |
| Returns: | |
| A dataframe containing the lowest latency. | |
| """ | |
| df = df[df.model.str.startswith(model)] | |
| if not model.endswith("fp16"): | |
| df = df[~df[_MODEL].str.endswith("fp16")] | |
| df = df[df[_LATENCY] != 0] | |
| df = df[df[_LATENCY] == df[_LATENCY].min()] | |
| return df.head(1) | |
| def get_tflite_config(model, df): | |
| """Generates a configuration string from TFLite config variables.""" | |
| config = [] | |
| if _TASKSET in df.columns: | |
| taskset = df.taskset.iloc[0] | |
| config.append(f"taskset {taskset}") | |
| threads = df.threads.iloc[0] | |
| config.append(f"{threads} threads" if threads > 1 else f"{threads} thread") | |
| config.append("no xnnpack" if model.endswith("noxnn") else "xnnpack") | |
| return ", ".join(config) | |
| def generate_tflite_summary(dataframe): | |
| """Generates a dataframe containing the fastest TFLite result for each model.""" | |
| summary = pd.DataFrame(columns=[_MODEL, _LATENCY, _MEMORY, _CONFIG]) | |
| tflite_df = dataframe[dataframe.runtime == "tflite"] | |
| model_list = get_tflite_model_list(dataframe) | |
| for model in model_list: | |
| df = get_fastest_result(model, tflite_df) | |
| if df.empty: | |
| print(f"Warning: TFLite results invalid for {model}.") | |
| continue | |
| latency = df[_LATENCY].iloc[0] | |
| full_model_name = df.model.iloc[0] | |
| memory = df[_MEMORY].iloc[0] | |
| config = get_tflite_config(full_model_name, df) | |
| summary.loc[len(summary)] = [model, latency, memory, config] | |
| return summary | |
| def get_iree_model_list(df): | |
| """Retrieves the list of IREE models, filtering out duplicates. | |
| The .csv file includes multiple entries of the same model but under a | |
| different configuration (e.g. mmt4d). | |
| """ | |
| df = df.loc[df.runtime == "iree"] | |
| df = df[~df.model.str.endswith("mmt4d")] | |
| df = df[~df.model.str.endswith("padfuse")] | |
| return df.model.unique() | |
| def get_iree_config(model, df): | |
| """Generates a configuration string from IREE config variables. | |
| The configuration is embedded in the model name. | |
| """ | |
| config = [] | |
| if _TASKSET in df.columns: | |
| taskset = df.taskset.iloc[0] | |
| config.append(f"taskset {taskset}") | |
| threads = df.threads.iloc[0] | |
| config.append(f"{threads} threads" if threads > 1 else f"{threads} thread") | |
| if model.endswith("im2col_mmt4d"): | |
| config.append("im2col") | |
| config.append("mmt4d") | |
| elif model.endswith("mmt4d"): | |
| config.append("mmt4d") | |
| elif model.endswith("padfuse"): | |
| config.append("fused pad") | |
| return ", ".join(config) | |
| def generate_iree_summary(dataframe): | |
| """Generates a dataframe containing the fastest IREE result for each model.""" | |
| summary = pd.DataFrame(columns=[_MODEL, _LATENCY, _MEMORY, _CONFIG]) | |
| iree_df = dataframe[dataframe.runtime == "iree"] | |
| model_list = get_iree_model_list(dataframe) | |
| for model in model_list: | |
| df = get_fastest_result(model, iree_df) | |
| if df.empty: | |
| print(f"Warning: IREE results invalid for {model}.") | |
| continue | |
| latency = df[_LATENCY].iloc[0] | |
| full_model_name = df.model.iloc[0] | |
| memory = df[_MEMORY].iloc[0] | |
| config = get_iree_config(full_model_name, df) | |
| summary.loc[len(summary)] = [model, latency, memory, config] | |
| return summary | |
| def get_common_html_style(df, title): | |
| """Returns HTML style attributes common to both server and mobile.""" | |
| st = df.style.set_table_styles(html_utils.get_table_css()) | |
| st = st.hide(axis="index") | |
| st = st.set_caption(title) | |
| st = st.set_properties(subset=[_MODEL], | |
| **{ | |
| "width": "300px", | |
| "text-align": "left", | |
| }) | |
| st = st.set_properties(subset=[_DATA_TYPE], | |
| **{ | |
| "width": "100", | |
| "text-align": "center", | |
| }) | |
| st = st.set_properties(subset=_NUMBER_COLUMNS, | |
| **{ | |
| "width": "100", | |
| "text-align": "right", | |
| }) | |
| st = st.set_properties(subset=_PERF_COLUMNS, | |
| **{ | |
| "width": "150px", | |
| "text-align": "right", | |
| "color": "#ffffff" | |
| }) | |
| st = st.applymap(html_utils.style_latency, subset=[_IREE_VS_TFLITE_LATENCY]) | |
| st = st.applymap(html_utils.style_memory, subset=[_IREE_VS_TFLITE_MEMORY]) | |
| return st | |
| def generate_summary(dataframe, title): | |
| """Generates a table comparing latencies and memory usage between IREE and TFLite. | |
| For each model, retrieves the lowest latency configuration from both IREE and TFLite. | |
| Args: | |
| dataframe: The raw data to summarize. | |
| title: The title of the table. | |
| Returns: | |
| An HTML string containing the summarized report. | |
| """ | |
| summary = pd.DataFrame(columns=[ | |
| _MODEL, _DATA_TYPE, _TFLITE_CONFIG, _IREE_CONFIG, _TFLITE_LATENCY, | |
| _IREE_LATENCY, _IREE_VS_TFLITE_LATENCY, _TFLITE_MEMORY, _IREE_MEMORY, | |
| _IREE_VS_TFLITE_MEMORY | |
| ]) | |
| tflite_df = generate_tflite_summary(dataframe) | |
| iree_df = generate_iree_summary(dataframe) | |
| model_list = tflite_df[_MODEL].unique() | |
| for model in model_list: | |
| tflite_results = tflite_df[tflite_df.model == model] | |
| iree_results = iree_df[iree_df.model == model] | |
| if tflite_results.empty: | |
| print(f"Warning: No TFLite results found for model {model}") | |
| continue | |
| if iree_results.empty: | |
| print(f"Warning: No IREE results found for model {model}") | |
| continue | |
| iree_latency = iree_results[_LATENCY].iloc[0] | |
| tflite_latency = tflite_results[_LATENCY].iloc[0] | |
| latency_comparison = html_utils.format_latency_comparison( | |
| iree_latency, tflite_latency) | |
| iree_memory = iree_results[_MEMORY].iloc[0] | |
| tflite_memory = tflite_results[_MEMORY].iloc[0] | |
| memory_comparison = html_utils.format_memory_comparison( | |
| iree_memory, tflite_memory) | |
| iree_config = iree_results.config.iloc[0] | |
| tflite_config = tflite_results.config.iloc[0] | |
| summary.loc[len(summary)] = [ | |
| model, | |
| _MODEL_TO_DATA_TYPE[model], | |
| tflite_config, | |
| iree_config, | |
| f"{tflite_latency:.1f}", | |
| f"{iree_latency:.1f}", | |
| latency_comparison, | |
| f"{tflite_memory:,.0f}", | |
| f"{iree_memory:,.0f}", | |
| memory_comparison, | |
| ] | |
| summary = summary.round(2) | |
| st = get_common_html_style(summary, title) | |
| st = st.set_properties(subset=_CONFIG_COLUMNS, | |
| **{ | |
| "width": "300px", | |
| "text-align": "left", | |
| }) | |
| return st.to_html().replace("\\n", "<br>") + "<br/>" | |
| def generate_detail(dataframe, title, platform): | |
| """Generates a table comparing latencies and memory usage between IREE and TFLite. | |
| The table generated is more detailed than `generate_summary`. It lists latencies | |
| of all IREE configurations, using the fastest TFLite configuration as baseline. | |
| Args: | |
| dataframe: The raw data to summarize. | |
| title: The title of the table. | |
| platform: Either `server` or `mobile`. | |
| Returns: | |
| An HTML string containing the detailed report. | |
| """ | |
| summary = pd.DataFrame(columns=[ | |
| _MODEL, _DATA_TYPE, _TFLITE_CONFIG, _IREE_CONFIG, _TASKSET, _THREADS, | |
| _TFLITE_LATENCY, _IREE_LATENCY, _IREE_VS_TFLITE_LATENCY, _TFLITE_MEMORY, | |
| _IREE_MEMORY, _IREE_VS_TFLITE_MEMORY | |
| ]) | |
| model_list = get_tflite_model_list(dataframe) | |
| for model in model_list: | |
| df = dataframe[dataframe.model.str.startswith(model)] | |
| # If result does not use FP16, remove FP16 results from dataframe to | |
| # maintain apples-to-apples comparisons. | |
| if not model.endswith("fp16"): | |
| df = df[~df.model.str.endswith("fp16")] | |
| if _TASKSET in df.columns: | |
| tasksets = df.taskset.unique() | |
| else: | |
| tasksets = ["none"] | |
| for taskset in tasksets: | |
| per_taskset_df = df if taskset == "none" else df[df.taskset == taskset] | |
| threads = per_taskset_df.threads.unique() | |
| for thread in threads: | |
| per_thread_df = per_taskset_df[per_taskset_df.threads == thread] | |
| tflite_df = get_fastest_result( | |
| model, per_thread_df[per_thread_df.runtime == "tflite"]) | |
| if tflite_df.empty: | |
| continue | |
| tflite_latency = tflite_df[_LATENCY].iloc[0] | |
| tflite_memory = tflite_df[_MEMORY].iloc[0] | |
| if tflite_latency == 0 or tflite_memory == 0: | |
| continue | |
| full_model_name = tflite_df.model.iloc[0] | |
| # For TFLite config, we only want to know if XNNPack was used. The other | |
| # configuration settings are covered in other columns. | |
| tflite_config = "no xnnpack" if full_model_name.endswith( | |
| "noxnn") else "xnnpack" | |
| iree_df = per_thread_df[per_thread_df.runtime == "iree"] | |
| for _, row in iree_df.iterrows(): | |
| iree_config = row[_DRIVER] | |
| model_name = row[_MODEL] | |
| if model_name.endswith("im2col_mmt4d"): | |
| iree_config += ", im2col, mmt4d" | |
| elif model_name.endswith("mmt4d"): | |
| iree_config += ", mmt4d" | |
| elif model_name.endswith("padfuse"): | |
| iree_config += ", fused pad" | |
| iree_latency = row[_LATENCY] | |
| latency_comparison = html_utils.format_latency_comparison( | |
| iree_latency, tflite_latency) | |
| iree_memory = row[_MEMORY] | |
| memory_comparison = html_utils.format_memory_comparison( | |
| iree_memory, tflite_memory) | |
| if iree_latency == 0 or iree_memory == 0: | |
| continue | |
| summary.loc[len(summary)] = [ | |
| model, _MODEL_TO_DATA_TYPE[model], tflite_config, iree_config, | |
| taskset, thread, f"{tflite_latency:.1f}", f"{iree_latency:.1f}", | |
| latency_comparison, f"{tflite_memory:,.0f}", | |
| f"{iree_memory:,.0f}", memory_comparison | |
| ] | |
| summary = summary.round(2) | |
| st = get_common_html_style(summary, title) | |
| st = st.set_properties(subset=[_TASKSET, _THREADS], | |
| **{ | |
| "width": "100", | |
| "text-align": "center", | |
| }) | |
| st = st.set_properties(subset=[_TFLITE_CONFIG], | |
| **{ | |
| "width": "150px", | |
| "text-align": "left", | |
| }) | |
| st = st.set_properties(subset=[_IREE_CONFIG], | |
| **{ | |
| "width": "300px", | |
| "text-align": "left", | |
| }) | |
| if platform != "mobile": | |
| st.hide_columns(subset=[_TASKSET]) | |
| return st.to_html().replace("\\n", "<br>") + "<br/>" | |
| def main(args): | |
| """Summarizes IREE vs TFLite benchmark results.""" | |
| if args.platform == _PLATFORM_SERVER: | |
| cpu_drivers = ["cpu", "local-task"] | |
| gpu_drivers = ["gpu", "cuda"] | |
| else: | |
| cpu_drivers = ["cpu", "local-task"] | |
| gpu_drivers = ["gpu", "vulkan", "adreno"] | |
| version_html = (f"<i>IREE version: {args.iree_version}</i><br/>" | |
| f"<i>TFlite version: {args.tflite_version}</i><br/>" | |
| f"<i>last updated: {date.today().isoformat()}</i><br/><br/>") | |
| html = html_utils.generate_header_and_legend(version_html) | |
| df = pd.read_csv(args.input_csv) | |
| # Generate CPU Summary. | |
| results = df[df[_DRIVER].isin(cpu_drivers)] | |
| html += generate_summary(results, args.platform.capitalize() + " CPU Summary") | |
| # Generate GPU Summary. | |
| results = df[df[_DRIVER].isin(gpu_drivers)] | |
| html += generate_summary(results, args.platform.capitalize() + " GPU Summary") | |
| # Generate CPU Detailed View. | |
| results = df[df[_DRIVER].isin(cpu_drivers)] | |
| html += generate_detail(results, | |
| args.platform.capitalize() + " CPU Detailed", | |
| args.platform) | |
| # Generate GPU Detailed View. | |
| results = df[df[_DRIVER].isin(gpu_drivers)] | |
| html += generate_detail(results, | |
| args.platform.capitalize() + " GPU Detailed", | |
| args.platform) | |
| args.output_path.write_text(html) | |
| def parse_args(): | |
| parser = argparse.ArgumentParser() | |
| parser.add_argument("--iree_version", | |
| type=str, | |
| default=None, | |
| required=True, | |
| help="The IREE version.") | |
| parser.add_argument("--tflite_version", | |
| type=str, | |
| default=None, | |
| required=True, | |
| help="The TFLite version.") | |
| parser.add_argument( | |
| "--platform", | |
| action="store", | |
| type=str.lower, | |
| help= | |
| "The platform the models were benchmarked on. Either server or mobile.", | |
| required=True, | |
| choices=[_PLATFORM_SERVER, _PLATFORM_MOBILE]) | |
| parser.add_argument( | |
| "--input_csv", | |
| type=str, | |
| default=None, | |
| help= | |
| "The path to the csv file containing benchmark results for both IREE and TFLite." | |
| ) | |
| parser.add_argument( | |
| "--output_path", | |
| type=pathlib.Path, | |
| default="/tmp/summary.html", | |
| help="The path to the output html file that summarizes results.") | |
| return parser.parse_args() | |
| if __name__ == "__main__": | |
| main(parse_args()) |