char-fork/llama.cpp
git clone https://git.t4t.associates/char-fork/llama.cpp
e6dd0e29a
char/aliceai-t5-moe
1#!/usr/bin/env python3 2 3import argparse 4import csv 5import heapq 6import json 7import logging 8import os 9import sqlite3 10import sys 11from collections .abc import Iterator ,Sequence 12from glob import glob 13from typing import Any ,Optional ,Union 14 15try : 16import git 17from tabulate import tabulate 18except ImportError as e : 19"the following Python libraries are required: GitPython, tabulate." )# noqa: NP100 20raise e 21 22 23logger = logging .getLogger ("compare-llama-bench" ) 24 25# All llama-bench SQL fields 26LLAMA_BENCH_DB_FIELDS = [ 27"build_commit" ,"build_number" ,"cpu_info" ,"gpu_info" ,"backends" ,"model_filename" , 28"model_type" ,"model_size" ,"model_n_params" ,"n_batch" ,"n_ubatch" ,"n_threads" , 29"cpu_mask" ,"cpu_strict" ,"poll" ,"type_k" ,"type_v" ,"n_gpu_layers" , 30"split_mode" ,"main_gpu" ,"no_kv_offload" ,"flash_attn" ,"tensor_split" ,"tensor_buft_overrides" , 31"load_mode" ,"embeddings" ,"no_op_offload" ,"n_prompt" ,"n_gen" ,"n_depth" , 32"test_time" ,"avg_ns" ,"stddev_ns" ,"avg_ts" ,"stddev_ts" ,"n_cpu_moe" , 33"fit_target" ,"fit_min_ctx" 34] 35 36LLAMA_BENCH_DB_TYPES = [ 37"TEXT" ,"INTEGER" ,"TEXT" ,"TEXT" ,"TEXT" ,"TEXT" , 38"TEXT" ,"INTEGER" ,"INTEGER" ,"INTEGER" ,"INTEGER" ,"INTEGER" , 39"TEXT" ,"INTEGER" ,"INTEGER" ,"TEXT" ,"TEXT" ,"INTEGER" , 40"TEXT" ,"INTEGER" ,"INTEGER" ,"INTEGER" ,"TEXT" ,"TEXT" , 41"TEXT" ,"INTEGER" ,"INTEGER" ,"INTEGER" ,"INTEGER" ,"INTEGER" , 42"TEXT" ,"INTEGER" ,"INTEGER" ,"REAL" ,"REAL" ,"INTEGER" , 43"INTEGER" ,"INTEGER" 44] 45 46# All test-backend-ops SQL fields 47TEST_BACKEND_OPS_DB_FIELDS = [ 48"test_time" ,"build_commit" ,"backend_name" ,"op_name" ,"op_params" ,"test_mode" , 49"supported" ,"passed" ,"error_message" ,"time_us" ,"flops" ,"bandwidth_gb_s" , 50"memory_kb" ,"n_runs" 51] 52 53TEST_BACKEND_OPS_DB_TYPES = [ 54"TEXT" ,"TEXT" ,"TEXT" ,"TEXT" ,"TEXT" ,"TEXT" , 55"INTEGER" ,"INTEGER" ,"TEXT" ,"REAL" ,"REAL" ,"REAL" , 56"INTEGER" ,"INTEGER" 57] 58 59assert len (LLAMA_BENCH_DB_FIELDS )== len (LLAMA_BENCH_DB_TYPES ) 60assert len (TEST_BACKEND_OPS_DB_FIELDS )== len (TEST_BACKEND_OPS_DB_TYPES ) 61 62# Properties by which to differentiate results per commit for llama-bench: 63LLAMA_BENCH_KEY_PROPERTIES = [ 64"cpu_info" ,"gpu_info" ,"backends" ,"n_gpu_layers" ,"n_cpu_moe" ,"tensor_buft_overrides" ,"model_filename" ,"model_type" , 65"n_batch" ,"n_ubatch" ,"embeddings" ,"cpu_mask" ,"cpu_strict" ,"poll" ,"n_threads" ,"type_k" ,"type_v" , 66"load_mode" ,"no_kv_offload" ,"split_mode" ,"main_gpu" ,"tensor_split" ,"flash_attn" ,"n_prompt" ,"n_gen" ,"n_depth" , 67"fit_target" ,"fit_min_ctx" 68] 69 70# Properties by which to differentiate results per commit for test-backend-ops: 71TEST_BACKEND_OPS_KEY_PROPERTIES = [ 72"backend_name" ,"op_name" ,"op_params" ,"test_mode" 73] 74 75# Properties that are boolean and are converted to Yes/No for the table: 76LLAMA_BENCH_BOOL_PROPERTIES = ["embeddings" ,"cpu_strict" ,"no_kv_offload" ,"flash_attn" ] 77TEST_BACKEND_OPS_BOOL_PROPERTIES = ["supported" ,"passed" ] 78 79# Header names for the table (llama-bench): 80LLAMA_BENCH_PRETTY_NAMES = { 81"cpu_info" :"CPU" ,"gpu_info" :"GPU" ,"backends" :"Backends" ,"n_gpu_layers" :"GPU layers" , 82"tensor_buft_overrides" :"Tensor overrides" ,"model_filename" :"File" ,"model_type" :"Model" ,"model_size" :"Model size [GiB]" , 83"model_n_params" :"Num. of par." ,"n_batch" :"Batch size" ,"n_ubatch" :"Microbatch size" ,"embeddings" :"Embeddings" , 84"cpu_mask" :"CPU mask" ,"cpu_strict" :"CPU strict" ,"poll" :"Poll" ,"n_threads" :"Threads" ,"type_k" :"K type" ,"type_v" :"V type" , 85"load_mode" :"Load mode" ,"no_kv_offload" :"NKVO" ,"split_mode" :"Split mode" ,"main_gpu" :"Main GPU" ,"tensor_split" :"Tensor split" , 86"flash_attn" :"FlashAttention" , 87} 88 89# Header names for the table (test-backend-ops): 90TEST_BACKEND_OPS_PRETTY_NAMES = { 91"backend_name" :"Backend" ,"op_name" :"GGML op" ,"op_params" :"Op parameters" ,"test_mode" :"Mode" , 92"supported" :"Supported" ,"passed" :"Passed" ,"error_message" :"Error" , 93"flops" :"FLOPS" ,"bandwidth_gb_s" :"Bandwidth (GB/s)" ,"memory_kb" :"Memory (KB)" ,"n_runs" :"Runs" 94} 95 96DEFAULT_SHOW_LLAMA_BENCH = ["model_type" ]# Always show these properties by default. 97DEFAULT_HIDE_LLAMA_BENCH = ["model_filename" ]# Always hide these properties by default. 98 99DEFAULT_SHOW_TEST_BACKEND_OPS = ["backend_name" ,"op_name" ]# Always show these properties by default. 100DEFAULT_HIDE_TEST_BACKEND_OPS = ["error_message" ]# Always hide these properties by default. 101 102GPU_NAME_STRIP = ["NVIDIA GeForce " ,"Tesla " ,"AMD Radeon " ,"AMD Instinct " ]# Strip prefixes for smaller tables. 103MODEL_SUFFIX_REPLACE = {" - Small" :"_S" ," - Medium" :"_M" ," - Large" :"_L" } 104 105DESCRIPTION = """Creates tables from llama-bench or test-backend-ops data written to multiple JSON/CSV files, a single JSONL file or SQLite database. Example usage (Linux): 106 107For llama-bench: 108$ git checkout master 109$ cmake -B ${BUILD_DIR} ${CMAKE_OPTS} && cmake --build ${BUILD_DIR} -t llama-bench -j $(nproc) 110$ ./llama-bench -o sql | sqlite3 llama-bench.sqlite 111$ git checkout some_branch 112$ cmake -B ${BUILD_DIR} ${CMAKE_OPTS} && cmake --build ${BUILD_DIR} -t llama-bench -j $(nproc) 113$ ./llama-bench -o sql | sqlite3 llama-bench.sqlite 114$ ./scripts/compare-llama-bench.py 115 116For test-backend-ops: 117$ git checkout master 118$ cmake -B ${BUILD_DIR} ${CMAKE_OPTS} && cmake --build ${BUILD_DIR} -t test-backend-ops -j $(nproc) 119$ ./test-backend-ops perf --output sql | sqlite3 test-backend-ops.sqlite 120$ git checkout some_branch 121$ cmake -B ${BUILD_DIR} ${CMAKE_OPTS} && cmake --build ${BUILD_DIR} -t test-backend-ops -j $(nproc) 122$ ./test-backend-ops perf --output sql | sqlite3 test-backend-ops.sqlite 123$ ./scripts/compare-llama-bench.py --tool test-backend-ops -i test-backend-ops.sqlite 124 125Performance numbers from multiple runs per commit are averaged WITHOUT being weighted by the --repetitions parameter of llama-bench. 126""" 127 128parser = argparse .ArgumentParser ( 129description = DESCRIPTION ,formatter_class = argparse .RawDescriptionHelpFormatter ) 130help_b = ( 131"The baseline commit to compare performance to. " 132"Accepts either a branch name, tag name, or commit hash. " 133"Defaults to latest master commit with data." 134) 135parser .add_argument ("-b" ,"--baseline" ,help = help_b ) 136help_c = ( 137"The commit whose performance is to be compared to the baseline. " 138"Accepts either a branch name, tag name, or commit hash. " 139"Defaults to the non-master commit for which llama-bench was run most recently." 140) 141parser .add_argument ("-c" ,"--compare" ,help = help_c ) 142help_t = ( 143"The tool whose data is being compared. " 144"Either 'llama-bench' or 'test-backend-ops'. " 145"This determines the database schema and comparison logic used. " 146"If left unspecified, try to determine from the input file." 147) 148parser .add_argument ("-t" ,"--tool" ,help = help_t ,default = None ,choices = [None ,"llama-bench" ,"test-backend-ops" ]) 149help_i = ( 150"JSON/JSONL/SQLite/CSV files for comparing commits. " 151"Specify multiple times to use multiple input files (JSON/CSV only). " 152"Defaults to 'llama-bench.sqlite' in the current working directory. " 153"If no such file is found and there is exactly one .sqlite file in the current directory, " 154"that file is instead used as input." 155) 156parser .add_argument ("-i" ,"--input" ,action = "append" ,help = help_i ) 157help_o = ( 158"Output format for the table. " 159"Defaults to 'pipe' (GitHub compatible). " 160"Also supports e.g. 'latex' or 'mediawiki'. " 161"See tabulate documentation for full list." 162) 163parser .add_argument ("-o" ,"--output" ,help = help_o ,default = "pipe" ) 164help_s = ( 165"Columns to add to the table. " 166"Accepts a comma-separated list of values. " 167f"Legal values for test-backend-ops: { ', ' . join ( TEST_BACKEND_OPS_KEY_PROPERTIES ) } . " 168f"Legal values for llama-bench: { ', ' . join ( LLAMA_BENCH_KEY_PROPERTIES [: - 3 ]) } . " 169"Defaults to model name (model_type) and CPU and/or GPU name (cpu_info, gpu_info) " 170"plus any column where not all data points are the same. " 171"If the columns are manually specified, then the results for each unique combination of the " 172"specified values are averaged WITHOUT weighing by the --repetitions parameter of llama-bench." 173) 174parser .add_argument ("--check" ,action = "store_true" ,help = "check if all required Python libraries are installed" ) 175parser .add_argument ("-s" ,"--show" ,help = help_s ) 176parser .add_argument ("--verbose" ,action = "store_true" ,help = "increase output verbosity" ) 177parser .add_argument ("--plot" ,help = "generate a performance comparison plot and save to specified file (e.g., plot.png)" ) 178parser .add_argument ("--plot_x" ,help = "parameter to use as x axis for plotting (default: n_depth)" ,default = "n_depth" ) 179parser .add_argument ("--plot_log_scale" ,action = "store_true" ,help = "use log scale for x axis in plots (off by default)" ) 180 181known_args ,unknown_args = parser .parse_known_args () 182 183logging .basicConfig (level = logging .DEBUG if known_args .verbose else logging .INFO ) 184 185 186if known_args .check : 187# Check if all required Python libraries are installed. Would have failed earlier if not. 188sys .exit (0 ) 189 190if unknown_args : 191logger .error (f"Received unknown args: { unknown_args } .\n" ) 192parser .print_help () 193sys .exit (1 ) 194 195input_file = known_args .input 196tool = known_args .tool 197 198if not input_file : 199if tool == "llama-bench" and os .path .exists ("./llama-bench.sqlite" ): 200input_file = ["llama-bench.sqlite" ] 201elif tool == "test-backend-ops" and os .path .exists ("./test-backend-ops.sqlite" ): 202input_file = ["test-backend-ops.sqlite" ] 203 204if not input_file : 205sqlite_files = glob ("*.sqlite" ) 206if len (sqlite_files )== 1 : 207input_file = sqlite_files 208 209if not input_file : 210logger .error ("Cannot find a suitable input file, please provide one.\n" ) 211parser .print_help () 212sys .exit (1 ) 213 214 215class LlamaBenchData : 216repo :Optional [git .Repo ] 217build_len_min :int 218build_len_max :int 219build_len :int = 8 220builds :list [str ]= [] 221tool :str = "llama-bench" # Tool type: "llama-bench" or "test-backend-ops" 222 223def __init__ (self ,tool :str = "llama-bench" ): 224self .tool = tool 225try : 226self .repo = git .Repo ("." ,search_parent_directories = True ) 227except git .InvalidGitRepositoryError : 228self .repo = None 229 230# Set schema-specific properties based on tool 231if self .tool == "llama-bench" : 232self .check_keys = set (LLAMA_BENCH_KEY_PROPERTIES + ["build_commit" ,"test_time" ,"avg_ts" ]) 233elif self .tool == "test-backend-ops" : 234self .check_keys = set (TEST_BACKEND_OPS_KEY_PROPERTIES + ["build_commit" ,"test_time" ]) 235else : 236assert False 237 238def _builds_init (self ): 239self .build_len = self .build_len_min 240 241def _check_keys (self ,keys :set )-> Optional [set ]: 242"""Private helper method that checks against required data keys and returns missing ones.""" 243if not keys >= self .check_keys : 244return self .check_keys - keys 245return None 246 247def find_parent_in_data (self ,commit :git .Commit )-> Optional [str ]: 248"""Helper method to find the most recent parent measured in number of commits for which there is data.""" 249heap :list [tuple [int ,git .Commit ]]= [(0 ,commit )] 250seen_hexsha8 = set () 251while heap : 252depth ,current_commit = heapq .heappop (heap ) 253current_hexsha8 = commit .hexsha [:self .build_len ] 254if current_hexsha8 in self .builds : 255return current_hexsha8 256for parent in commit .parents : 257parent_hexsha8 = parent .hexsha [:self .build_len ] 258if parent_hexsha8 not in seen_hexsha8 : 259seen_hexsha8 .add (parent_hexsha8 ) 260heapq .heappush (heap , (depth + 1 ,parent )) 261return None 262 263def get_all_parent_hexsha8s (self ,commit :git .Commit )-> Sequence [str ]: 264"""Helper method to recursively get hexsha8 values for all parents of a commit.""" 265unvisited = [commit ] 266visited = [] 267 268while unvisited : 269current_commit = unvisited .pop (0 ) 270visited .append (current_commit .hexsha [:self .build_len ]) 271for parent in current_commit .parents : 272if parent .hexsha [:self .build_len ]not in visited : 273unvisited .append (parent ) 274 275return visited 276 277def get_commit_name (self ,hexsha8 :str )-> str : 278"""Helper method to find a human-readable name for a commit if possible.""" 279if self .repo is None : 280return hexsha8 281for h in self .repo .heads : 282if h .commit .hexsha [:self .build_len ]== hexsha8 : 283return h .name 284for t in self .repo .tags : 285if t .commit .hexsha [:self .build_len ]== hexsha8 : 286return t .name 287return hexsha8 288 289def get_commit_hexsha8 (self ,name :str )-> Optional [str ]: 290"""Helper method to search for a commit given a human-readable name.""" 291if self .repo is None : 292return None 293for h in self .repo .heads : 294if h .name == name : 295return h .commit .hexsha [:self .build_len ] 296for t in self .repo .tags : 297if t .name == name : 298return t .commit .hexsha [:self .build_len ] 299for remote in self .repo .remotes : 300for ref in remote .refs : 301if ref .name == name or ref .remote_head == name : 302return ref .commit .hexsha [:self .build_len ] 303for c in self .repo .iter_commits ("--all" ): 304if c .hexsha [:self .build_len ]== name [:self .build_len ]: 305return c .hexsha [:self .build_len ] 306return None 307 308def builds_timestamp (self ,reverse :bool = False )-> Union [Iterator [tuple ],Sequence [tuple ]]: 309"""Helper method that gets rows of (build_commit, test_time) sorted by the latter.""" 310return [] 311 312def get_rows (self ,properties :list [str ],hexsha8_baseline :str ,hexsha8_compare :str )-> Sequence [tuple ]: 313""" 314Helper method that gets table rows for some list of properties. 315Rows are created by combining those where all provided properties are equal. 316The resulting rows are then grouped by the provided properties and the t/s values are averaged. 317The returned rows are unique in terms of property combinations. 318""" 319return [] 320 321 322class LlamaBenchDataSQLite3 (LlamaBenchData ): 323connection :Optional [sqlite3 .Connection ]= None 324cursor :sqlite3 .Cursor 325table_name :str 326 327def __init__ (self ,tool :str = "llama-bench" ): 328super ().__init__ (tool ) 329if self .connection is None : 330self .connection = sqlite3 .connect (":memory:" ) 331self .cursor = self .connection .cursor () 332 333# Set table name and schema based on tool 334if self .tool == "llama-bench" : 335self .table_name = "llama_bench" 336db_fields = LLAMA_BENCH_DB_FIELDS 337db_types = LLAMA_BENCH_DB_TYPES 338elif self .tool == "test-backend-ops" : 339self .table_name = "test_backend_ops" 340db_fields = TEST_BACKEND_OPS_DB_FIELDS 341db_types = TEST_BACKEND_OPS_DB_TYPES 342else : 343assert False 344 345self .cursor .execute (f"CREATE TABLE { self . table_name } ( { ', ' . join ( ' ' . join ( x ) for x in zip ( db_fields , db_types )) } );" ) 346 347def _builds_init (self ): 348if self .connection : 349self .build_len_min = self .cursor .execute (f"SELECT MIN(LENGTH(build_commit)) from { self . table_name } ;" ).fetchone ()[0 ] 350self .build_len_max = self .cursor .execute (f"SELECT MAX(LENGTH(build_commit)) from { self . table_name } ;" ).fetchone ()[0 ] 351 352if self .build_len_min != self .build_len_max : 353logger .warning ("Data contains commit hashes of differing lengths. It's possible that the wrong commits will be compared. " 354"Try purging the the database of old commits." ) 355self .cursor .execute (f"UPDATE { self . table_name } SET build_commit = SUBSTRING(build_commit, 1, { self . build_len_min } );" ) 356 357builds = self .cursor .execute (f"SELECT DISTINCT build_commit FROM { self . table_name } ;" ).fetchall () 358self .builds = list (map (lambda b :b [0 ],builds ))# list[tuple[str]] -> list[str] 359super ()._builds_init () 360 361def builds_timestamp (self ,reverse :bool = False )-> Union [Iterator [tuple ],Sequence [tuple ]]: 362data = self .cursor .execute ( 363f"SELECT build_commit, test_time FROM { self . table_name } ORDER BY test_time;" ).fetchall () 364return reversed (data )if reverse else data 365 366def get_rows (self ,properties :list [str ],hexsha8_baseline :str ,hexsha8_compare :str )-> Sequence [tuple ]: 367if self .tool == "llama-bench" : 368return self ._get_rows_llama_bench (properties ,hexsha8_baseline ,hexsha8_compare ) 369elif self .tool == "test-backend-ops" : 370return self ._get_rows_test_backend_ops (properties ,hexsha8_baseline ,hexsha8_compare ) 371else : 372assert False 373 374def _get_rows_llama_bench (self ,properties :list [str ],hexsha8_baseline :str ,hexsha8_compare :str )-> Sequence [tuple ]: 375select_string = ", " .join ( 376 [f"tb. { p } " for p in properties ]+ ["tb.n_prompt" ,"tb.n_gen" ,"tb.n_depth" ,"AVG(tb.avg_ts)" ,"AVG(tc.avg_ts)" ]) 377equal_string = " AND " .join ( 378 [f"tb. { p } = tc. { p } " for p in LLAMA_BENCH_KEY_PROPERTIES ]+ [ 379f"tb.build_commit = ' { hexsha8_baseline } '" ,f"tc.build_commit = ' { hexsha8_compare } '" ] 380 ) 381group_order_string = ", " .join ([f"tb. { p } " for p in properties ]+ ["tb.n_gen" ,"tb.n_prompt" ,"tb.n_depth" ]) 382query = (f"SELECT { select_string } FROM { self . table_name } tb JOIN { self . table_name } tc ON { equal_string } " 383f"GROUP BY { group_order_string } ORDER BY { group_order_string } ;" ) 384return self .cursor .execute (query ).fetchall () 385 386def _get_rows_test_backend_ops (self ,properties :list [str ],hexsha8_baseline :str ,hexsha8_compare :str )-> Sequence [tuple ]: 387# For test-backend-ops, we compare FLOPS and bandwidth metrics (prioritizing FLOPS over bandwidth) 388select_string = ", " .join ( 389 [f"tb. { p } " for p in properties ]+ [ 390"AVG(tb.flops)" ,"AVG(tc.flops)" , 391"AVG(tb.bandwidth_gb_s)" ,"AVG(tc.bandwidth_gb_s)" 392 ]) 393equal_string = " AND " .join ( 394 [f"tb. { p } = tc. { p } " for p in TEST_BACKEND_OPS_KEY_PROPERTIES ]+ [ 395f"tb.build_commit = ' { hexsha8_baseline } '" ,f"tc.build_commit = ' { hexsha8_compare } '" , 396"tb.supported = 1" ,"tc.supported = 1" ,"tb.passed = 1" ,"tc.passed = 1" ]# Only compare successful tests 397 ) 398group_order_string = ", " .join ([f"tb. { p } " for p in properties ]) 399query = (f"SELECT { select_string } FROM { self . table_name } tb JOIN { self . table_name } tc ON { equal_string } " 400f"GROUP BY { group_order_string } ORDER BY { group_order_string } ;" ) 401return self .cursor .execute (query ).fetchall () 402 403 404class LlamaBenchDataSQLite3File (LlamaBenchDataSQLite3 ): 405def __init__ (self ,data_file :str ,tool :Any ): 406self .connection = sqlite3 .connect (data_file ) 407self .cursor = self .connection .cursor () 408 409# Check which table exists in the database 410tables = self .cursor .execute ("SELECT name FROM sqlite_master WHERE type='table';" ).fetchall () 411table_names = [table [0 ]for table in tables ] 412 413# Tool selection logic 414if tool is None : 415if "llama_bench" in table_names : 416self .table_name = "llama_bench" 417tool = "llama-bench" 418elif "test_backend_ops" in table_names : 419self .table_name = "test_backend_ops" 420tool = "test-backend-ops" 421else : 422raise RuntimeError (f"No suitable table found in database. Available tables: { table_names } " ) 423elif tool == "llama-bench" : 424if "llama_bench" in table_names : 425self .table_name = "llama_bench" 426tool = "llama-bench" 427else : 428raise RuntimeError (f"Table 'test' not found for tool 'llama-bench'. Available tables: { table_names } " ) 429elif tool == "test-backend-ops" : 430if "test_backend_ops" in table_names : 431self .table_name = "test_backend_ops" 432tool = "test-backend-ops" 433else : 434raise RuntimeError (f"Table 'test_backend_ops' not found for tool 'test-backend-ops'. Available tables: { table_names } " ) 435else : 436raise RuntimeError (f"Unknown tool: { tool } " ) 437 438super ().__init__ (tool ) 439self ._builds_init () 440 441@staticmethod 442def valid_format (data_file :str )-> bool : 443connection = sqlite3 .connect (data_file ) 444cursor = connection .cursor () 445 446try : 447if cursor .execute ("PRAGMA schema_version;" ).fetchone ()[0 ]== 0 : 448raise sqlite3 .DatabaseError ("The provided input file does not exist or is empty." ) 449except sqlite3 .DatabaseError as e : 450logger .debug (f'" { data_file } " is not a valid SQLite3 file.' ,exc_info = e ) 451cursor = None 452 453connection .close () 454return True if cursor else False 455 456 457class LlamaBenchDataJSONL (LlamaBenchDataSQLite3 ): 458def __init__ (self ,data_file :str ,tool :str = "llama-bench" ): 459super ().__init__ (tool ) 460 461# Get the appropriate field list based on tool 462db_fields = LLAMA_BENCH_DB_FIELDS if tool == "llama-bench" else TEST_BACKEND_OPS_DB_FIELDS 463 464with open (data_file ,"r" ,encoding = "utf-8" )as fp : 465for i ,line in enumerate (fp ): 466parsed = json .loads (line ) 467 468for k in parsed .keys ()- set (db_fields ): 469del parsed [k ] 470 471if (missing_keys := self ._check_keys (parsed .keys ())): 472raise RuntimeError (f"Missing required data key(s) at line { i + 1 } : { ', ' . join ( missing_keys ) } " ) 473 474self .cursor .execute (f"INSERT INTO { self . table_name } ( { ', ' . join ( parsed . keys ()) } ) VALUES( { ', ' . join ( '?' * len ( parsed )) } );" ,tuple (parsed .values ())) 475 476self ._builds_init () 477 478@staticmethod 479def valid_format (data_file :str )-> bool : 480try : 481with open (data_file ,"r" ,encoding = "utf-8" )as fp : 482for line in fp : 483json .loads (line ) 484break 485except Exception as e : 486logger .debug (f'" { data_file } " is not a valid JSONL file.' ,exc_info = e ) 487return False 488 489return True 490 491 492class LlamaBenchDataJSON (LlamaBenchDataSQLite3 ): 493def __init__ (self ,data_files :list [str ],tool :str = "llama-bench" ): 494super ().__init__ (tool ) 495 496# Get the appropriate field list based on tool 497db_fields = LLAMA_BENCH_DB_FIELDS if tool == "llama-bench" else TEST_BACKEND_OPS_DB_FIELDS 498 499for data_file in data_files : 500with open (data_file ,"r" ,encoding = "utf-8" )as fp : 501parsed = json .load (fp ) 502 503for i ,entry in enumerate (parsed ): 504for k in entry .keys ()- set (db_fields ): 505del entry [k ] 506 507if (missing_keys := self ._check_keys (entry .keys ())): 508raise RuntimeError (f"Missing required data key(s) at entry { i + 1 } : { ', ' . join ( missing_keys ) } " ) 509 510self .cursor .execute (f"INSERT INTO { self . table_name } ( { ', ' . join ( entry . keys ()) } ) VALUES( { ', ' . join ( '?' * len ( entry )) } );" ,tuple (entry .values ())) 511 512self ._builds_init () 513 514@staticmethod 515def valid_format (data_files :list [str ])-> bool : 516if not data_files : 517return False 518 519for data_file in data_files : 520try : 521with open (data_file ,"r" ,encoding = "utf-8" )as fp : 522json .load (fp ) 523except Exception as e : 524logger .debug (f'" { data_file } " is not a valid JSON file.' ,exc_info = e ) 525return False 526 527return True 528 529 530class LlamaBenchDataCSV (LlamaBenchDataSQLite3 ): 531def __init__ (self ,data_files :list [str ],tool :str = "llama-bench" ): 532super ().__init__ (tool ) 533 534# Get the appropriate field list based on tool 535db_fields = LLAMA_BENCH_DB_FIELDS if tool == "llama-bench" else TEST_BACKEND_OPS_DB_FIELDS 536 537for data_file in data_files : 538with open (data_file ,"r" ,encoding = "utf-8" )as fp : 539for i ,parsed in enumerate (csv .DictReader (fp )): 540keys = set (parsed .keys ()) 541 542for k in keys - set (db_fields ): 543del parsed [k ] 544 545if (missing_keys := self ._check_keys (keys )): 546raise RuntimeError (f"Missing required data key(s) at line { i + 1 } : { ', ' . join ( missing_keys ) } " ) 547 548self .cursor .execute (f"INSERT INTO { self . table_name } ( { ', ' . join ( parsed . keys ()) } ) VALUES( { ', ' . join ( '?' * len ( parsed )) } );" ,tuple (parsed .values ())) 549 550self ._builds_init () 551 552@staticmethod 553def valid_format (data_files :list [str ])-> bool : 554if not data_files : 555return False 556 557for data_file in data_files : 558try : 559with open (data_file ,"r" ,encoding = "utf-8" )as fp : 560for parsed in csv .DictReader (fp ): 561break 562except Exception as e : 563logger .debug (f'" { data_file } " is not a valid CSV file.' ,exc_info = e ) 564return False 565 566return True 567 568 569def format_flops (flops_value :float )-> str : 570"""Format FLOPS values with appropriate units for better readability.""" 571if flops_value == 0 : 572return "0.00" 573 574# Define unit thresholds and names 575units = [ 576 (1e12 ,"T" ),# TeraFLOPS 577 (1e9 ,"G" ),# GigaFLOPS 578 (1e6 ,"M" ),# MegaFLOPS 579 (1e3 ,"k" ),# kiloFLOPS 580 (1 ,"" )# FLOPS 581 ] 582 583for threshold ,unit in units : 584if abs (flops_value )>= threshold : 585formatted_value = flops_value / threshold 586if formatted_value >= 100 : 587return f" { formatted_value :.1f } { unit } " 588else : 589return f" { formatted_value :.2f } { unit } " 590 591# Fallback for very small values 592return f" { flops_value :.2f } " 593 594 595def format_flops_for_table (flops_value :float ,target_unit :str )-> str : 596"""Format FLOPS values for table display without unit suffix (since unit is in header).""" 597if flops_value == 0 : 598return "0.00" 599 600# Define unit thresholds based on target unit 601unit_divisors = { 602"TFLOPS" :1e12 , 603"GFLOPS" :1e9 , 604"MFLOPS" :1e6 , 605"kFLOPS" :1e3 , 606"FLOPS" :1 607 } 608 609divisor = unit_divisors .get (target_unit ,1 ) 610formatted_value = flops_value / divisor 611 612if formatted_value >= 100 : 613return f" { formatted_value :.1f } " 614else : 615return f" { formatted_value :.2f } " 616 617 618def get_flops_unit_name (flops_values :list )-> str : 619"""Determine the best FLOPS unit name based on the magnitude of values.""" 620if not flops_values or all (v == 0 for v in flops_values ): 621return "FLOPS" 622 623# Find the maximum absolute value to determine appropriate unit 624max_flops = max (abs (v )for v in flops_values if v != 0 ) 625 626if max_flops >= 1e12 : 627return "TFLOPS" 628elif max_flops >= 1e9 : 629return "GFLOPS" 630elif max_flops >= 1e6 : 631return "MFLOPS" 632elif max_flops >= 1e3 : 633return "kFLOPS" 634else : 635return "FLOPS" 636 637 638bench_data = None 639if len (input_file )== 1 : 640if LlamaBenchDataSQLite3File .valid_format (input_file [0 ]): 641bench_data = LlamaBenchDataSQLite3File (input_file [0 ],tool ) 642elif LlamaBenchDataJSON .valid_format (input_file ): 643bench_data = LlamaBenchDataJSON (input_file ,tool ) 644elif LlamaBenchDataJSONL .valid_format (input_file [0 ]): 645bench_data = LlamaBenchDataJSONL (input_file [0 ],tool ) 646elif LlamaBenchDataCSV .valid_format (input_file ): 647bench_data = LlamaBenchDataCSV (input_file ,tool ) 648else : 649if LlamaBenchDataJSON .valid_format (input_file ): 650bench_data = LlamaBenchDataJSON (input_file ,tool ) 651elif LlamaBenchDataCSV .valid_format (input_file ): 652bench_data = LlamaBenchDataCSV (input_file ,tool ) 653 654if not bench_data : 655raise RuntimeError ("No valid (or some invalid) input files found." ) 656 657if not bench_data .builds : 658raise RuntimeError (f" { input_file } does not contain any builds." ) 659 660tool = bench_data .tool # May have chosen a default if tool was None. 661 662 663hexsha8_baseline = name_baseline = None 664 665# If the user specified a baseline, try to find a commit for it: 666if known_args .baseline is not None : 667if known_args .baseline in bench_data .builds : 668hexsha8_baseline = known_args .baseline 669if hexsha8_baseline is None : 670hexsha8_baseline = bench_data .get_commit_hexsha8 (known_args .baseline ) 671name_baseline = known_args .baseline 672if hexsha8_baseline is None : 673logger .error (f"cannot find data for baseline= { known_args . baseline } ." ) 674sys .exit (1 ) 675# Otherwise, search for the most recent parent of master for which there is data: 676elif bench_data .repo is not None : 677hexsha8_baseline = bench_data .find_parent_in_data (bench_data .repo .heads .master .commit ) 678 679if hexsha8_baseline is None : 680logger .error ("No baseline was provided and did not find data for any master branch commits.\n" ) 681parser .print_help () 682sys .exit (1 ) 683else : 684logger .error ("No baseline was provided and the current working directory " 685"is not part of a git repository from which a baseline could be inferred.\n" ) 686parser .print_help () 687sys .exit (1 ) 688 689 690assert isinstance (hexsha8_baseline ,str ) 691name_baseline = bench_data .get_commit_name (hexsha8_baseline ) 692 693hexsha8_compare = name_compare = None 694 695# If the user has specified a compare value, try to find a corresponding commit: 696if known_args .compare is not None : 697if known_args .compare in bench_data .builds : 698hexsha8_compare = known_args .compare 699if hexsha8_compare is None : 700hexsha8_compare = bench_data .get_commit_hexsha8 (known_args .compare ) 701name_compare = known_args .compare 702if hexsha8_compare is None : 703logger .error (f"cannot find data for compare= { known_args . compare } ." ) 704sys .exit (1 ) 705# Otherwise, search for the commit for llama-bench was most recently run 706# and that is not a parent of master: 707elif bench_data .repo is not None : 708hexsha8s_master = bench_data .get_all_parent_hexsha8s (bench_data .repo .heads .master .commit ) 709for (hexsha8 ,_ )in bench_data .builds_timestamp (reverse = True ): 710if hexsha8 not in hexsha8s_master : 711hexsha8_compare = hexsha8 712break 713 714if hexsha8_compare is None : 715logger .error ("No compare target was provided and did not find data for any non-master commits.\n" ) 716parser .print_help () 717sys .exit (1 ) 718else : 719logger .error ("No compare target was provided and the current working directory " 720"is not part of a git repository from which a compare target could be inferred.\n" ) 721parser .print_help () 722sys .exit (1 ) 723 724assert isinstance (hexsha8_compare ,str ) 725name_compare = bench_data .get_commit_name (hexsha8_compare ) 726 727# Get tool-specific configuration 728if tool == "llama-bench" : 729key_properties = LLAMA_BENCH_KEY_PROPERTIES 730bool_properties = LLAMA_BENCH_BOOL_PROPERTIES 731pretty_names = LLAMA_BENCH_PRETTY_NAMES 732default_show = DEFAULT_SHOW_LLAMA_BENCH 733default_hide = DEFAULT_HIDE_LLAMA_BENCH 734elif tool == "test-backend-ops" : 735key_properties = TEST_BACKEND_OPS_KEY_PROPERTIES 736bool_properties = TEST_BACKEND_OPS_BOOL_PROPERTIES 737pretty_names = TEST_BACKEND_OPS_PRETTY_NAMES 738default_show = DEFAULT_SHOW_TEST_BACKEND_OPS 739default_hide = DEFAULT_HIDE_TEST_BACKEND_OPS 740else : 741assert False 742 743# If the user provided columns to group the results by, use them: 744if known_args .show is not None : 745show = known_args .show .split ("," ) 746unknown_cols = [] 747for prop in show : 748valid_props = key_properties if tool == "test-backend-ops" else key_properties [:- 3 ]# Exclude n_prompt, n_gen, n_depth for llama-bench 749if prop not in valid_props : 750unknown_cols .append (prop ) 751if unknown_cols : 752logger .error (f"Unknown values for --show: { ', ' . join ( unknown_cols ) } " ) 753parser .print_usage () 754sys .exit (1 ) 755rows_show = bench_data .get_rows (show ,hexsha8_baseline ,hexsha8_compare ) 756# Otherwise, select those columns where the values are not all the same: 757else : 758rows_full = bench_data .get_rows (key_properties ,hexsha8_baseline ,hexsha8_compare ) 759properties_different = [] 760 761if tool == "llama-bench" : 762# For llama-bench, skip n_prompt, n_gen, n_depth from differentiation logic 763check_properties = [kp for kp in key_properties if kp not in ["n_prompt" ,"n_gen" ,"n_depth" ]] 764for i ,kp_i in enumerate (key_properties ): 765if kp_i in default_show or kp_i in ["n_prompt" ,"n_gen" ,"n_depth" ]: 766continue 767for row_full in rows_full : 768if row_full [i ]!= rows_full [0 ][i ]: 769properties_different .append (kp_i ) 770break 771elif tool == "test-backend-ops" : 772# For test-backend-ops, check all key properties 773for i ,kp_i in enumerate (key_properties ): 774if kp_i in default_show : 775continue 776for row_full in rows_full : 777if row_full [i ]!= rows_full [0 ][i ]: 778properties_different .append (kp_i ) 779break 780else : 781assert False 782 783show = [] 784 785if tool == "llama-bench" : 786# Show CPU and/or GPU by default even if the hardware for all results is the same: 787if rows_full and "n_gpu_layers" not in properties_different : 788ngl = int (rows_full [0 ][key_properties .index ("n_gpu_layers" )]) 789 790if ngl != 99 and "cpu_info" not in properties_different : 791show .append ("cpu_info" ) 792 793show += properties_different 794 795index_default = 0 796for prop in ["cpu_info" ,"gpu_info" ,"n_gpu_layers" ,"main_gpu" ]: 797if prop in show : 798index_default += 1 799show = show [:index_default ]+ default_show + show [index_default :] 800elif tool == "test-backend-ops" : 801show = default_show + properties_different 802else : 803assert False 804 805for prop in default_hide : 806try : 807show .remove (prop ) 808except ValueError : 809pass 810 811# Add plot_x parameter to parameters to show if it's not already present: 812if known_args .plot : 813for k ,v in pretty_names .items (): 814if v == known_args .plot_x and k not in show : 815show .append (k ) 816break 817 818rows_show = bench_data .get_rows (show ,hexsha8_baseline ,hexsha8_compare ) 819 820if not rows_show : 821logger .error (f"No comparable data was found between { name_baseline } and { name_compare } .\n" ) 822sys .exit (1 ) 823 824table = [] 825primary_metric = "FLOPS" # Default to FLOPS for test-backend-ops 826 827if tool == "llama-bench" : 828# For llama-bench, create test names and compare avg_ts values 829for row in rows_show : 830n_prompt = int (row [- 5 ]) 831n_gen = int (row [- 4 ]) 832n_depth = int (row [- 3 ]) 833if n_prompt != 0 and n_gen == 0 : 834test_name = f"pp { n_prompt } " 835elif n_prompt == 0 and n_gen != 0 : 836test_name = f"tg { n_gen } " 837else : 838test_name = f"pp { n_prompt } +tg { n_gen } " 839if n_depth != 0 : 840test_name = f" { test_name } @d { n_depth } " 841# Regular columns test name avg t/s values Speedup 842# VVVVVVVVVVVVV VVVVVVVVV VVVVVVVVVVVVVV VVVVVVV 843table .append (list (row [:- 5 ])+ [test_name ]+ list (row [- 2 :])+ [float (row [- 1 ])/ float (row [- 2 ])]) 844elif tool == "test-backend-ops" : 845# Determine the primary metric by checking rows until we find one with valid data 846if rows_show : 847primary_metric = "FLOPS" # Default to FLOPS 848flops_values = [] 849 850# Collect all FLOPS values to determine the best unit 851for sample_row in rows_show : 852baseline_flops = float (sample_row [- 4 ]) 853compare_flops = float (sample_row [- 3 ]) 854baseline_bandwidth = float (sample_row [- 2 ]) 855 856if baseline_flops > 0 : 857flops_values .extend ([baseline_flops ,compare_flops ]) 858elif baseline_bandwidth > 0 and not flops_values : 859primary_metric = "Bandwidth (GB/s)" 860 861# If we have FLOPS data, determine the appropriate unit 862if flops_values : 863primary_metric = get_flops_unit_name (flops_values ) 864 865# For test-backend-ops, prioritize FLOPS > bandwidth for comparison 866for row in rows_show : 867# Extract metrics: flops, bandwidth_gb_s (baseline and compare) 868baseline_flops = float (row [- 4 ]) 869compare_flops = float (row [- 3 ]) 870baseline_bandwidth = float (row [- 2 ]) 871compare_bandwidth = float (row [- 1 ]) 872 873# Determine which metric to use for comparison (prioritize FLOPS > bandwidth) 874if baseline_flops > 0 and compare_flops > 0 : 875# Use FLOPS comparison (higher is better) 876speedup = compare_flops / baseline_flops 877baseline_str = format_flops_for_table (baseline_flops ,primary_metric ) 878compare_str = format_flops_for_table (compare_flops ,primary_metric ) 879elif baseline_bandwidth > 0 and compare_bandwidth > 0 : 880# Use bandwidth comparison (higher is better) 881speedup = compare_bandwidth / baseline_bandwidth 882baseline_str = f" { baseline_bandwidth :.2f } " 883compare_str = f" { compare_bandwidth :.2f } " 884else : 885# Fallback if no valid data is available 886baseline_str = "N/A" 887compare_str = "N/A" 888from math import nan 889speedup = nan 890 891table .append (list (row [:- 4 ])+ [baseline_str ,compare_str ,speedup ]) 892else : 893assert False 894 895# Some a-posteriori fixes to make the table contents prettier: 896for bool_property in bool_properties : 897if bool_property in show : 898ip = show .index (bool_property ) 899for row_table in table : 900row_table [ip ]= "Yes" if int (row_table [ip ])== 1 else "No" 901 902if tool == "llama-bench" : 903if "model_type" in show : 904ip = show .index ("model_type" ) 905for (old ,new )in MODEL_SUFFIX_REPLACE .items (): 906for row_table in table : 907row_table [ip ]= row_table [ip ].replace (old ,new ) 908 909if "model_size" in show : 910ip = show .index ("model_size" ) 911for row_table in table : 912row_table [ip ]= float (row_table [ip ])/ 1024 ** 3 913 914if "gpu_info" in show : 915ip = show .index ("gpu_info" ) 916for row_table in table : 917for gns in GPU_NAME_STRIP : 918row_table [ip ]= row_table [ip ].replace (gns ,"" ) 919 920gpu_names = row_table [ip ].split (", " ) 921num_gpus = len (gpu_names ) 922all_names_the_same = len (set (gpu_names ))== 1 923if len (gpu_names )>= 2 and all_names_the_same : 924row_table [ip ]= f" { num_gpus } x { gpu_names [ 0 ] } " 925 926headers = [pretty_names .get (p ,p )for p in show ] 927if tool == "llama-bench" : 928headers += ["Test" ,f"t/s { name_baseline } " ,f"t/s { name_compare } " ,"Speedup" ] 929elif tool == "test-backend-ops" : 930headers += [f" { primary_metric } { name_baseline } " ,f" { primary_metric } { name_compare } " ,"Speedup" ] 931else : 932assert False 933 934if known_args .plot : 935def create_performance_plot (table_data :list [list [str ]],headers :list [str ],baseline_name :str ,compare_name :str ,output_file :str ,plot_x_param :str ,log_scale :bool = False ,tool_type :str = "llama-bench" ,metric_name :str = "t/s" ): 936try : 937import matplotlib 938import matplotlib .pyplot as plt 939matplotlib .use ('Agg' ) 940except ImportError as e : 941logger .error ("matplotlib is required for --plot." ) 942raise e 943 944data_headers = headers [:- 4 ]# Exclude the last 4 columns (Test, baseline t/s, compare t/s, Speedup) 945plot_x_index = None 946plot_x_label = plot_x_param 947 948if plot_x_param not in ["n_prompt" ,"n_gen" ,"n_depth" ]: 949pretty_name = LLAMA_BENCH_PRETTY_NAMES .get (plot_x_param ,plot_x_param ) 950if pretty_name in data_headers : 951plot_x_index = data_headers .index (pretty_name ) 952plot_x_label = pretty_name 953elif plot_x_param in data_headers : 954plot_x_index = data_headers .index (plot_x_param ) 955plot_x_label = plot_x_param 956else : 957logger .error (f"Parameter ' { plot_x_param } ' not found in current table columns. Available columns: { ', ' . join ( data_headers ) } " ) 958return 959 960grouped_data = {} 961 962for i ,row in enumerate (table_data ): 963group_key_parts = [] 964test_name = row [- 4 ] 965 966base_test = "" 967x_value = None 968 969if plot_x_param in ["n_prompt" ,"n_gen" ,"n_depth" ]: 970for j ,val in enumerate (row [:- 4 ]): 971header_name = data_headers [j ] 972if val is not None and str (val ).strip (): 973group_key_parts .append (f" { header_name } = { val } " ) 974 975if plot_x_param == "n_prompt" and "pp" in test_name : 976base_test = test_name .split ("@" )[0 ] 977x_value = base_test 978elif plot_x_param == "n_gen" and "tg" in test_name : 979x_value = test_name .split ("@" )[0 ] 980elif plot_x_param == "n_depth" and "@d" in test_name : 981base_test = test_name .split ("@d" )[0 ] 982x_value = int (test_name .split ("@d" )[1 ]) 983else : 984base_test = test_name 985 986if base_test .strip (): 987group_key_parts .append (f"Test= { base_test } " ) 988else : 989for j ,val in enumerate (row [:- 4 ]): 990if j != plot_x_index : 991header_name = data_headers [j ] 992if val is not None and str (val ).strip (): 993group_key_parts .append (f" { header_name } = { val } " ) 994else : 995x_value = val 996 997group_key_parts .append (f"Test= { test_name } " ) 998 999group_key = tuple (group_key_parts ) 1000 1001if group_key not in grouped_data : 1002grouped_data [group_key ]= [] 1003 1004grouped_data [group_key ].append ({ 1005'x_value' :x_value , 1006'baseline' :float (row [- 3 ]), 1007'compare' :float (row [- 2 ]), 1008'speedup' :float (row [- 1 ]) 1009 }) 1010 1011if not grouped_data : 1012logger .error ("No data available for plotting" ) 1013return 1014 1015def make_axes (num_groups ,max_cols = 2 ,base_size = (8 ,4 )): 1016from math import ceil 1017cols = 1 if num_groups == 1 else min (max_cols ,num_groups ) 1018rows = ceil (num_groups / cols ) 1019 1020# Scale figure size by grid dimensions 1021w ,h = base_size 1022fig ,ax_arr = plt .subplots (rows ,cols , 1023figsize = (w * cols ,h * rows ), 1024squeeze = False ) 1025 1026axes = ax_arr .flatten ()[:num_groups ] 1027return fig ,axes 1028 1029num_groups = len (grouped_data ) 1030fig ,axes = make_axes (num_groups ) 1031 1032plot_idx = 0 1033 1034for group_key ,points in grouped_data .items (): 1035if plot_idx >= len (axes ): 1036break 1037ax = axes [plot_idx ] 1038 1039try : 1040points_sorted = sorted (points ,key = lambda p :float (p ['x_value' ])if p ['x_value' ]is not None else 0 ) 1041x_values = [float (p ['x_value' ])if p ['x_value' ]is not None else 0 for p in points_sorted ] 1042except ValueError : 1043points_sorted = sorted (points ,key = lambda p :group_key ) 1044x_values = [p ['x_value' ]for p in points_sorted ] 1045 1046baseline_vals = [p ['baseline' ]for p in points_sorted ] 1047compare_vals = [p ['compare' ]for p in points_sorted ] 1048 1049ax .plot (x_values ,baseline_vals ,'o-' ,color = 'skyblue' , 1050label = f' { baseline_name } ' ,linewidth = 2 ,markersize = 6 ) 1051ax .plot (x_values ,compare_vals ,'s--' ,color = 'lightcoral' ,alpha = 0.8 , 1052label = f' { compare_name } ' ,linewidth = 2 ,markersize = 6 ) 1053 1054if log_scale : 1055ax .set_xscale ('log' ,base = 2 ) 1056unique_x = sorted (set (x_values )) 1057ax .set_xticks (unique_x ) 1058ax .set_xticklabels ([str (int (x ))for x in unique_x ]) 1059 1060title_parts = [] 1061for part in group_key : 1062if '=' in part : 1063key ,value = part .split ('=' ,1 ) 1064title_parts .append (f" { key } : { value } " ) 1065 1066title = ', ' .join (title_parts )if title_parts else "Performance comparison" 1067 1068# Determine y-axis label based on tool type 1069if tool_type == "llama-bench" : 1070y_label = "Tokens per second (t/s)" 1071elif tool_type == "test-backend-ops" : 1072y_label = metric_name 1073else : 1074assert False 1075 1076ax .set_xlabel (plot_x_label ,fontsize = 12 ,fontweight = 'bold' ) 1077ax .set_ylabel (y_label ,fontsize = 12 ,fontweight = 'bold' ) 1078ax .set_title (title ,fontsize = 12 ,fontweight = 'bold' ) 1079ax .legend (loc = 'best' ,fontsize = 10 ) 1080ax .grid (True ,alpha = 0.3 ) 1081 1082plot_idx += 1 1083 1084for i in range (plot_idx ,len (axes )): 1085axes [i ].set_visible (False ) 1086 1087fig .suptitle (f'Performance comparison: { compare_name } vs. { baseline_name } ' , 1088fontsize = 14 ,fontweight = 'bold' ) 1089fig .subplots_adjust (top = 1 ) 1090 1091plt .tight_layout () 1092plt .savefig (output_file ,dpi = 300 ,bbox_inches = 'tight' ) 1093plt .close () 1094 1095create_performance_plot (table ,headers ,name_baseline ,name_compare ,known_args .plot ,known_args .plot_x ,known_args .plot_log_scale ,tool ,primary_metric ) 1096 1097tabulate (# noqa: NP100 1098table , 1099headers = headers , 1100floatfmt = ".2f" , 1101tablefmt = known_args .output 1102))