CoolFace
Datasetpublic

echodict/llama.cpp

version https://git-lfs.github.com/spec/v1 oid sha256:cfc44b7ba25614df70e6b65e3341cae0310163bd32fd31a6b928a542df433faf size 30786

sourceHugging Faceupdated 5mo agoView on Hugging Face
0likes773downloads
compare-llama-bench.py1103 linesDownload Raw Back to scripts
1#!/usr/bin/env python32 3import argparse4import csv5import heapq6import json7import logging8import os9import sqlite310import sys11from collections.abc import Iterator, Sequence12from glob import glob13from typing import Any, Optional, Union14 15try:16    import git17    from tabulate import tabulate18except ImportError as e:19    print("the following Python libraries are required: GitPython, tabulate.") # noqa: NP10020    raise e21 22 23logger = logging.getLogger("compare-llama-bench")24 25# All llama-bench SQL fields26LLAMA_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    "use_mmap",     "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    "INTEGER", "INTEGER", "INTEGER", "INTEGER", "INTEGER", "INTEGER",42    "TEXT",    "INTEGER", "INTEGER", "REAL",    "REAL",    "INTEGER",43    "INTEGER", "INTEGER"44]45 46# All test-backend-ops SQL fields47TEST_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    "use_mmap", "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", "use_mmap", "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    "use_mmap": "Use mmap", "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 master109$ cmake -B ${BUILD_DIR} ${CMAKE_OPTS} && cmake --build ${BUILD_DIR} -t llama-bench -j $(nproc)110$ ./llama-bench -o sql | sqlite3 llama-bench.sqlite111$ git checkout some_branch112$ cmake -B ${BUILD_DIR} ${CMAKE_OPTS} && cmake --build ${BUILD_DIR} -t llama-bench -j $(nproc)113$ ./llama-bench -o sql | sqlite3 llama-bench.sqlite114$ ./scripts/compare-llama-bench.py115 116For test-backend-ops:117$ git checkout master118$ 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.sqlite120$ git checkout some_branch121$ 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.sqlite123$ ./scripts/compare-llama-bench.py --tool test-backend-ops -i test-backend-ops.sqlite124 125Performance numbers from multiple runs per commit are averaged WITHOUT being weighted by the --repetitions parameter of llama-bench.126"""127 128parser = argparse.ArgumentParser(129    description=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. "167    f"Legal values for test-backend-ops: {', '.join(TEST_BACKEND_OPS_KEY_PROPERTIES)}. "168    f"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.188    sys.exit(0)189 190if unknown_args:191    logger.error(f"Received unknown args: {unknown_args}.\n")192    parser.print_help()193    sys.exit(1)194 195input_file = known_args.input196tool = known_args.tool197 198if not input_file:199    if tool == "llama-bench" and os.path.exists("./llama-bench.sqlite"):200        input_file = ["llama-bench.sqlite"]201    elif tool == "test-backend-ops" and os.path.exists("./test-backend-ops.sqlite"):202        input_file = ["test-backend-ops.sqlite"]203 204if not input_file:205    sqlite_files = glob("*.sqlite")206    if len(sqlite_files) == 1:207        input_file = sqlite_files208 209if not input_file:210    logger.error("Cannot find a suitable input file, please provide one.\n")211    parser.print_help()212    sys.exit(1)213 214 215class LlamaBenchData:216    repo: Optional[git.Repo]217    build_len_min: int218    build_len_max: int219    build_len: int = 8220    builds: list[str] = []221    tool: str = "llama-bench"  # Tool type: "llama-bench" or "test-backend-ops"222 223    def __init__(self, tool: str = "llama-bench"):224        self.tool = tool225        try:226            self.repo = git.Repo(".", search_parent_directories=True)227        except git.InvalidGitRepositoryError:228            self.repo = None229 230        # Set schema-specific properties based on tool231        if self.tool == "llama-bench":232            self.check_keys = set(LLAMA_BENCH_KEY_PROPERTIES + ["build_commit", "test_time", "avg_ts"])233        elif self.tool == "test-backend-ops":234            self.check_keys = set(TEST_BACKEND_OPS_KEY_PROPERTIES + ["build_commit", "test_time"])235        else:236            assert False237 238    def _builds_init(self):239        self.build_len = self.build_len_min240 241    def _check_keys(self, keys: set) -> Optional[set]:242        """Private helper method that checks against required data keys and returns missing ones."""243        if not keys >= self.check_keys:244            return self.check_keys - keys245        return None246 247    def 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."""249        heap: list[tuple[int, git.Commit]] = [(0, commit)]250        seen_hexsha8 = set()251        while heap:252            depth, current_commit = heapq.heappop(heap)253            current_hexsha8 = commit.hexsha[:self.build_len]254            if current_hexsha8 in self.builds:255                return current_hexsha8256            for parent in commit.parents:257                parent_hexsha8 = parent.hexsha[:self.build_len]258                if parent_hexsha8 not in seen_hexsha8:259                    seen_hexsha8.add(parent_hexsha8)260                    heapq.heappush(heap, (depth + 1, parent))261        return None262 263    def get_all_parent_hexsha8s(self, commit: git.Commit) -> Sequence[str]:264        """Helper method to recursively get hexsha8 values for all parents of a commit."""265        unvisited = [commit]266        visited   = []267 268        while unvisited:269            current_commit = unvisited.pop(0)270            visited.append(current_commit.hexsha[:self.build_len])271            for parent in current_commit.parents:272                if parent.hexsha[:self.build_len] not in visited:273                    unvisited.append(parent)274 275        return visited276 277    def get_commit_name(self, hexsha8: str) -> str:278        """Helper method to find a human-readable name for a commit if possible."""279        if self.repo is None:280            return hexsha8281        for h in self.repo.heads:282            if h.commit.hexsha[:self.build_len] == hexsha8:283                return h.name284        for t in self.repo.tags:285            if t.commit.hexsha[:self.build_len] == hexsha8:286                return t.name287        return hexsha8288 289    def get_commit_hexsha8(self, name: str) -> Optional[str]:290        """Helper method to search for a commit given a human-readable name."""291        if self.repo is None:292            return None293        for h in self.repo.heads:294            if h.name == name:295                return h.commit.hexsha[:self.build_len]296        for t in self.repo.tags:297            if t.name == name:298                return t.commit.hexsha[:self.build_len]299        for remote in self.repo.remotes:300            for ref in remote.refs:301                if ref.name == name or ref.remote_head == name:302                    return ref.commit.hexsha[:self.build_len]303        for c in self.repo.iter_commits("--all"):304            if c.hexsha[:self.build_len] == name[:self.build_len]:305                return c.hexsha[:self.build_len]306        return None307 308    def 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."""310        return []311 312    def get_rows(self, properties: list[str], hexsha8_baseline: str, hexsha8_compare: str) -> Sequence[tuple]:313        """314        Helper method that gets table rows for some list of properties.315        Rows are created by combining those where all provided properties are equal.316        The resulting rows are then grouped by the provided properties and the t/s values are averaged.317        The returned rows are unique in terms of property combinations.318        """319        return []320 321 322class LlamaBenchDataSQLite3(LlamaBenchData):323    connection: Optional[sqlite3.Connection] = None324    cursor: sqlite3.Cursor325    table_name: str326 327    def __init__(self, tool: str = "llama-bench"):328        super().__init__(tool)329        if self.connection is None:330            self.connection = sqlite3.connect(":memory:")331            self.cursor = self.connection.cursor()332 333            # Set table name and schema based on tool334            if self.tool == "llama-bench":335                self.table_name = "llama_bench"336                db_fields = LLAMA_BENCH_DB_FIELDS337                db_types = LLAMA_BENCH_DB_TYPES338            elif self.tool == "test-backend-ops":339                self.table_name = "test_backend_ops"340                db_fields = TEST_BACKEND_OPS_DB_FIELDS341                db_types = TEST_BACKEND_OPS_DB_TYPES342            else:343                assert False344 345            self.cursor.execute(f"CREATE TABLE {self.table_name}({', '.join(' '.join(x) for x in zip(db_fields, db_types))});")346 347    def _builds_init(self):348        if self.connection:349            self.build_len_min = self.cursor.execute(f"SELECT MIN(LENGTH(build_commit)) from {self.table_name};").fetchone()[0]350            self.build_len_max = self.cursor.execute(f"SELECT MAX(LENGTH(build_commit)) from {self.table_name};").fetchone()[0]351 352            if self.build_len_min != self.build_len_max:353                logger.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.")355                self.cursor.execute(f"UPDATE {self.table_name} SET build_commit = SUBSTRING(build_commit, 1, {self.build_len_min});")356 357            builds = self.cursor.execute(f"SELECT DISTINCT build_commit FROM {self.table_name};").fetchall()358            self.builds = list(map(lambda b: b[0], builds))  # list[tuple[str]] -> list[str]359        super()._builds_init()360 361    def builds_timestamp(self, reverse: bool = False) -> Union[Iterator[tuple], Sequence[tuple]]:362        data = self.cursor.execute(363            f"SELECT build_commit, test_time FROM {self.table_name} ORDER BY test_time;").fetchall()364        return reversed(data) if reverse else data365 366    def get_rows(self, properties: list[str], hexsha8_baseline: str, hexsha8_compare: str) -> Sequence[tuple]:367        if self.tool == "llama-bench":368            return self._get_rows_llama_bench(properties, hexsha8_baseline, hexsha8_compare)369        elif self.tool == "test-backend-ops":370            return self._get_rows_test_backend_ops(properties, hexsha8_baseline, hexsha8_compare)371        else:372            assert False373 374    def _get_rows_llama_bench(self, properties: list[str], hexsha8_baseline: str, hexsha8_compare: str) -> Sequence[tuple]:375        select_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)"])377        equal_string = " AND ".join(378            [f"tb.{p} = tc.{p}" for p in LLAMA_BENCH_KEY_PROPERTIES] + [379                f"tb.build_commit = '{hexsha8_baseline}'", f"tc.build_commit = '{hexsha8_compare}'"]380        )381        group_order_string = ", ".join([f"tb.{p}" for p in properties] + ["tb.n_gen", "tb.n_prompt", "tb.n_depth"])382        query = (f"SELECT {select_string} FROM {self.table_name} tb JOIN {self.table_name} tc ON {equal_string} "383                 f"GROUP BY {group_order_string} ORDER BY {group_order_string};")384        return self.cursor.execute(query).fetchall()385 386    def _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)388        select_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            ])393        equal_string = " AND ".join(394            [f"tb.{p} = tc.{p}" for p in TEST_BACKEND_OPS_KEY_PROPERTIES] + [395                f"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 tests397        )398        group_order_string = ", ".join([f"tb.{p}" for p in properties])399        query = (f"SELECT {select_string} FROM {self.table_name} tb JOIN {self.table_name} tc ON {equal_string} "400                 f"GROUP BY {group_order_string} ORDER BY {group_order_string};")401        return self.cursor.execute(query).fetchall()402 403 404class LlamaBenchDataSQLite3File(LlamaBenchDataSQLite3):405    def __init__(self, data_file: str, tool: Any):406        self.connection = sqlite3.connect(data_file)407        self.cursor = self.connection.cursor()408 409        # Check which table exists in the database410        tables = self.cursor.execute("SELECT name FROM sqlite_master WHERE type='table';").fetchall()411        table_names = [table[0] for table in tables]412 413        # Tool selection logic414        if tool is None:415            if "llama_bench" in table_names:416                self.table_name = "llama_bench"417                tool = "llama-bench"418            elif "test_backend_ops" in table_names:419                self.table_name = "test_backend_ops"420                tool = "test-backend-ops"421            else:422                raise RuntimeError(f"No suitable table found in database. Available tables: {table_names}")423        elif tool == "llama-bench":424            if "llama_bench" in table_names:425                self.table_name = "llama_bench"426                tool = "llama-bench"427            else:428                raise RuntimeError(f"Table 'test' not found for tool 'llama-bench'. Available tables: {table_names}")429        elif tool == "test-backend-ops":430            if "test_backend_ops" in table_names:431                self.table_name = "test_backend_ops"432                tool = "test-backend-ops"433            else:434                raise RuntimeError(f"Table 'test_backend_ops' not found for tool 'test-backend-ops'. Available tables: {table_names}")435        else:436            raise RuntimeError(f"Unknown tool: {tool}")437 438        super().__init__(tool)439        self._builds_init()440 441    @staticmethod442    def valid_format(data_file: str) -> bool:443        connection = sqlite3.connect(data_file)444        cursor = connection.cursor()445 446        try:447            if cursor.execute("PRAGMA schema_version;").fetchone()[0] == 0:448                raise sqlite3.DatabaseError("The provided input file does not exist or is empty.")449        except sqlite3.DatabaseError as e:450            logger.debug(f'"{data_file}" is not a valid SQLite3 file.', exc_info=e)451            cursor = None452 453        connection.close()454        return True if cursor else False455 456 457class LlamaBenchDataJSONL(LlamaBenchDataSQLite3):458    def __init__(self, data_file: str, tool: str = "llama-bench"):459        super().__init__(tool)460 461        # Get the appropriate field list based on tool462        db_fields = LLAMA_BENCH_DB_FIELDS if tool == "llama-bench" else TEST_BACKEND_OPS_DB_FIELDS463 464        with open(data_file, "r", encoding="utf-8") as fp:465            for i, line in enumerate(fp):466                parsed = json.loads(line)467 468                for k in parsed.keys() - set(db_fields):469                    del parsed[k]470 471                if (missing_keys := self._check_keys(parsed.keys())):472                    raise RuntimeError(f"Missing required data key(s) at line {i + 1}: {', '.join(missing_keys)}")473 474                self.cursor.execute(f"INSERT INTO {self.table_name}({', '.join(parsed.keys())}) VALUES({', '.join('?' * len(parsed))});", tuple(parsed.values()))475 476        self._builds_init()477 478    @staticmethod479    def valid_format(data_file: str) -> bool:480        try:481            with open(data_file, "r", encoding="utf-8") as fp:482                for line in fp:483                    json.loads(line)484                    break485        except Exception as e:486            logger.debug(f'"{data_file}" is not a valid JSONL file.', exc_info=e)487            return False488 489        return True490 491 492class LlamaBenchDataJSON(LlamaBenchDataSQLite3):493    def __init__(self, data_files: list[str], tool: str = "llama-bench"):494        super().__init__(tool)495 496        # Get the appropriate field list based on tool497        db_fields = LLAMA_BENCH_DB_FIELDS if tool == "llama-bench" else TEST_BACKEND_OPS_DB_FIELDS498 499        for data_file in data_files:500            with open(data_file, "r", encoding="utf-8") as fp:501                parsed = json.load(fp)502 503                for i, entry in enumerate(parsed):504                    for k in entry.keys() - set(db_fields):505                        del entry[k]506 507                    if (missing_keys := self._check_keys(entry.keys())):508                        raise RuntimeError(f"Missing required data key(s) at entry {i + 1}: {', '.join(missing_keys)}")509 510                    self.cursor.execute(f"INSERT INTO {self.table_name}({', '.join(entry.keys())}) VALUES({', '.join('?' * len(entry))});", tuple(entry.values()))511 512        self._builds_init()513 514    @staticmethod515    def valid_format(data_files: list[str]) -> bool:516        if not data_files:517            return False518 519        for data_file in data_files:520            try:521                with open(data_file, "r", encoding="utf-8") as fp:522                    json.load(fp)523            except Exception as e:524                logger.debug(f'"{data_file}" is not a valid JSON file.', exc_info=e)525                return False526 527        return True528 529 530class LlamaBenchDataCSV(LlamaBenchDataSQLite3):531    def __init__(self, data_files: list[str], tool: str = "llama-bench"):532        super().__init__(tool)533 534        # Get the appropriate field list based on tool535        db_fields = LLAMA_BENCH_DB_FIELDS if tool == "llama-bench" else TEST_BACKEND_OPS_DB_FIELDS536 537        for data_file in data_files:538            with open(data_file, "r", encoding="utf-8") as fp:539                for i, parsed in enumerate(csv.DictReader(fp)):540                    keys = set(parsed.keys())541 542                    for k in keys - set(db_fields):543                        del parsed[k]544 545                    if (missing_keys := self._check_keys(keys)):546                        raise RuntimeError(f"Missing required data key(s) at line {i + 1}: {', '.join(missing_keys)}")547 548                    self.cursor.execute(f"INSERT INTO {self.table_name}({', '.join(parsed.keys())}) VALUES({', '.join('?' * len(parsed))});", tuple(parsed.values()))549 550        self._builds_init()551 552    @staticmethod553    def valid_format(data_files: list[str]) -> bool:554        if not data_files:555            return False556 557        for data_file in data_files:558            try:559                with open(data_file, "r", encoding="utf-8") as fp:560                    for parsed in csv.DictReader(fp):561                        break562            except Exception as e:563                logger.debug(f'"{data_file}" is not a valid CSV file.', exc_info=e)564                return False565 566        return True567 568 569def format_flops(flops_value: float) -> str:570    """Format FLOPS values with appropriate units for better readability."""571    if flops_value == 0:572        return "0.00"573 574    # Define unit thresholds and names575    units = [576        (1e12, "T"),   # TeraFLOPS577        (1e9, "G"),    # GigaFLOPS578        (1e6, "M"),    # MegaFLOPS579        (1e3, "k"),    # kiloFLOPS580        (1, "")        # FLOPS581    ]582 583    for threshold, unit in units:584        if abs(flops_value) >= threshold:585            formatted_value = flops_value / threshold586            if formatted_value >= 100:587                return f"{formatted_value:.1f}{unit}"588            else:589                return f"{formatted_value:.2f}{unit}"590 591    # Fallback for very small values592    return 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)."""597    if flops_value == 0:598        return "0.00"599 600    # Define unit thresholds based on target unit601    unit_divisors = {602        "TFLOPS": 1e12,603        "GFLOPS": 1e9,604        "MFLOPS": 1e6,605        "kFLOPS": 1e3,606        "FLOPS": 1607    }608 609    divisor = unit_divisors.get(target_unit, 1)610    formatted_value = flops_value / divisor611 612    if formatted_value >= 100:613        return f"{formatted_value:.1f}"614    else:615        return 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."""620    if not flops_values or all(v == 0 for v in flops_values):621        return "FLOPS"622 623    # Find the maximum absolute value to determine appropriate unit624    max_flops = max(abs(v) for v in flops_values if v != 0)625 626    if max_flops >= 1e12:627        return "TFLOPS"628    elif max_flops >= 1e9:629        return "GFLOPS"630    elif max_flops >= 1e6:631        return "MFLOPS"632    elif max_flops >= 1e3:633        return "kFLOPS"634    else:635        return "FLOPS"636 637 638bench_data = None639if len(input_file) == 1:640    if LlamaBenchDataSQLite3File.valid_format(input_file[0]):641        bench_data = LlamaBenchDataSQLite3File(input_file[0], tool)642    elif LlamaBenchDataJSON.valid_format(input_file):643        bench_data = LlamaBenchDataJSON(input_file, tool)644    elif LlamaBenchDataJSONL.valid_format(input_file[0]):645        bench_data = LlamaBenchDataJSONL(input_file[0], tool)646    elif LlamaBenchDataCSV.valid_format(input_file):647        bench_data = LlamaBenchDataCSV(input_file, tool)648else:649    if LlamaBenchDataJSON.valid_format(input_file):650        bench_data = LlamaBenchDataJSON(input_file, tool)651    elif LlamaBenchDataCSV.valid_format(input_file):652        bench_data = LlamaBenchDataCSV(input_file, tool)653 654if not bench_data:655    raise RuntimeError("No valid (or some invalid) input files found.")656 657if not bench_data.builds:658    raise 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 = None664 665# If the user specified a baseline, try to find a commit for it:666if known_args.baseline is not None:667    if known_args.baseline in bench_data.builds:668        hexsha8_baseline = known_args.baseline669    if hexsha8_baseline is None:670        hexsha8_baseline = bench_data.get_commit_hexsha8(known_args.baseline)671        name_baseline = known_args.baseline672    if hexsha8_baseline is None:673        logger.error(f"cannot find data for baseline={known_args.baseline}.")674        sys.exit(1)675# Otherwise, search for the most recent parent of master for which there is data:676elif bench_data.repo is not None:677    hexsha8_baseline = bench_data.find_parent_in_data(bench_data.repo.heads.master.commit)678 679    if hexsha8_baseline is None:680        logger.error("No baseline was provided and did not find data for any master branch commits.\n")681        parser.print_help()682        sys.exit(1)683else:684    logger.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")686    parser.print_help()687    sys.exit(1)688 689 690assert isinstance(hexsha8_baseline, str)691name_baseline = bench_data.get_commit_name(hexsha8_baseline)692 693hexsha8_compare = name_compare = None694 695# If the user has specified a compare value, try to find a corresponding commit:696if known_args.compare is not None:697    if known_args.compare in bench_data.builds:698        hexsha8_compare = known_args.compare699    if hexsha8_compare is None:700        hexsha8_compare = bench_data.get_commit_hexsha8(known_args.compare)701        name_compare = known_args.compare702    if hexsha8_compare is None:703        logger.error(f"cannot find data for compare={known_args.compare}.")704        sys.exit(1)705# Otherwise, search for the commit for llama-bench was most recently run706# and that is not a parent of master:707elif bench_data.repo is not None:708    hexsha8s_master = bench_data.get_all_parent_hexsha8s(bench_data.repo.heads.master.commit)709    for (hexsha8, _) in bench_data.builds_timestamp(reverse=True):710        if hexsha8 not in hexsha8s_master:711            hexsha8_compare = hexsha8712            break713 714    if hexsha8_compare is None:715        logger.error("No compare target was provided and did not find data for any non-master commits.\n")716        parser.print_help()717        sys.exit(1)718else:719    logger.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")721    parser.print_help()722    sys.exit(1)723 724assert isinstance(hexsha8_compare, str)725name_compare = bench_data.get_commit_name(hexsha8_compare)726 727# Get tool-specific configuration728if tool == "llama-bench":729    key_properties = LLAMA_BENCH_KEY_PROPERTIES730    bool_properties = LLAMA_BENCH_BOOL_PROPERTIES731    pretty_names = LLAMA_BENCH_PRETTY_NAMES732    default_show = DEFAULT_SHOW_LLAMA_BENCH733    default_hide = DEFAULT_HIDE_LLAMA_BENCH734elif tool == "test-backend-ops":735    key_properties = TEST_BACKEND_OPS_KEY_PROPERTIES736    bool_properties = TEST_BACKEND_OPS_BOOL_PROPERTIES737    pretty_names = TEST_BACKEND_OPS_PRETTY_NAMES738    default_show = DEFAULT_SHOW_TEST_BACKEND_OPS739    default_hide = DEFAULT_HIDE_TEST_BACKEND_OPS740else:741    assert False742 743# If the user provided columns to group the results by, use them:744if known_args.show is not None:745    show = known_args.show.split(",")746    unknown_cols = []747    for prop in show:748        valid_props = key_properties if tool == "test-backend-ops" else key_properties[:-3]  # Exclude n_prompt, n_gen, n_depth for llama-bench749        if prop not in valid_props:750            unknown_cols.append(prop)751    if unknown_cols:752        logger.error(f"Unknown values for --show: {', '.join(unknown_cols)}")753        parser.print_usage()754        sys.exit(1)755    rows_show = bench_data.get_rows(show, hexsha8_baseline, hexsha8_compare)756# Otherwise, select those columns where the values are not all the same:757else:758    rows_full = bench_data.get_rows(key_properties, hexsha8_baseline, hexsha8_compare)759    properties_different = []760 761    if tool == "llama-bench":762        # For llama-bench, skip n_prompt, n_gen, n_depth from differentiation logic763        check_properties = [kp for kp in key_properties if kp not in ["n_prompt", "n_gen", "n_depth"]]764        for i, kp_i in enumerate(key_properties):765            if kp_i in default_show or kp_i in ["n_prompt", "n_gen", "n_depth"]:766                continue767            for row_full in rows_full:768                if row_full[i] != rows_full[0][i]:769                    properties_different.append(kp_i)770                    break771    elif tool == "test-backend-ops":772        # For test-backend-ops, check all key properties773        for i, kp_i in enumerate(key_properties):774            if kp_i in default_show:775                continue776            for row_full in rows_full:777                if row_full[i] != rows_full[0][i]:778                    properties_different.append(kp_i)779                    break780    else:781        assert False782 783    show = []784 785    if tool == "llama-bench":786        # Show CPU and/or GPU by default even if the hardware for all results is the same:787        if rows_full and "n_gpu_layers" not in properties_different:788            ngl = int(rows_full[0][key_properties.index("n_gpu_layers")])789 790            if ngl != 99 and "cpu_info" not in properties_different:791                show.append("cpu_info")792 793        show += properties_different794 795        index_default = 0796        for prop in ["cpu_info", "gpu_info", "n_gpu_layers", "main_gpu"]:797            if prop in show:798                index_default += 1799        show = show[:index_default] + default_show + show[index_default:]800    elif tool == "test-backend-ops":801        show = default_show + properties_different802    else:803        assert False804 805    for prop in default_hide:806        try:807            show.remove(prop)808        except ValueError:809            pass810 811    # Add plot_x parameter to parameters to show if it's not already present:812    if known_args.plot:813        for k, v in pretty_names.items():814            if v == known_args.plot_x and k not in show:815                show.append(k)816                break817 818    rows_show = bench_data.get_rows(show, hexsha8_baseline, hexsha8_compare)819 820if not rows_show:821    logger.error(f"No comparable data was found between {name_baseline} and {name_compare}.\n")822    sys.exit(1)823 824table = []825primary_metric = "FLOPS"  # Default to FLOPS for test-backend-ops826 827if tool == "llama-bench":828    # For llama-bench, create test names and compare avg_ts values829    for row in rows_show:830        n_prompt = int(row[-5])831        n_gen    = int(row[-4])832        n_depth  = int(row[-3])833        if n_prompt != 0 and n_gen == 0:834            test_name = f"pp{n_prompt}"835        elif n_prompt == 0 and n_gen != 0:836            test_name = f"tg{n_gen}"837        else:838            test_name = f"pp{n_prompt}+tg{n_gen}"839        if n_depth != 0:840            test_name = f"{test_name}@d{n_depth}"841        #           Regular columns    test name    avg t/s values              Speedup842        #            VVVVVVVVVVVVV     VVVVVVVVV    VVVVVVVVVVVVVV              VVVVVVV843        table.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 data846    if rows_show:847        primary_metric = "FLOPS"  # Default to FLOPS848        flops_values = []849 850        # Collect all FLOPS values to determine the best unit851        for sample_row in rows_show:852            baseline_flops = float(sample_row[-4])853            compare_flops = float(sample_row[-3])854            baseline_bandwidth = float(sample_row[-2])855 856            if baseline_flops > 0:857                flops_values.extend([baseline_flops, compare_flops])858            elif baseline_bandwidth > 0 and not flops_values:859                primary_metric = "Bandwidth (GB/s)"860 861        # If we have FLOPS data, determine the appropriate unit862        if flops_values:863            primary_metric = get_flops_unit_name(flops_values)864 865    # For test-backend-ops, prioritize FLOPS > bandwidth for comparison866    for row in rows_show:867        # Extract metrics: flops, bandwidth_gb_s (baseline and compare)868        baseline_flops = float(row[-4])869        compare_flops = float(row[-3])870        baseline_bandwidth = float(row[-2])871        compare_bandwidth = float(row[-1])872 873        # Determine which metric to use for comparison (prioritize FLOPS > bandwidth)874        if baseline_flops > 0 and compare_flops > 0:875            # Use FLOPS comparison (higher is better)876            speedup = compare_flops / baseline_flops877            baseline_str = format_flops_for_table(baseline_flops, primary_metric)878            compare_str = format_flops_for_table(compare_flops, primary_metric)879        elif baseline_bandwidth > 0 and compare_bandwidth > 0:880            # Use bandwidth comparison (higher is better)881            speedup = compare_bandwidth / baseline_bandwidth882            baseline_str = f"{baseline_bandwidth:.2f}"883            compare_str = f"{compare_bandwidth:.2f}"884        else:885            # Fallback if no valid data is available886            baseline_str = "N/A"887            compare_str = "N/A"888            from math import nan889            speedup = nan890 891        table.append(list(row[:-4]) + [baseline_str, compare_str, speedup])892else:893    assert False894 895# Some a-posteriori fixes to make the table contents prettier:896for bool_property in bool_properties:897    if bool_property in show:898        ip = show.index(bool_property)899        for row_table in table:900            row_table[ip] = "Yes" if int(row_table[ip]) == 1 else "No"901 902if tool == "llama-bench":903    if "model_type" in show:904        ip = show.index("model_type")905        for (old, new) in MODEL_SUFFIX_REPLACE.items():906            for row_table in table:907                row_table[ip] = row_table[ip].replace(old, new)908 909    if "model_size" in show:910        ip = show.index("model_size")911        for row_table in table:912            row_table[ip] = float(row_table[ip]) / 1024 ** 3913 914    if "gpu_info" in show:915        ip = show.index("gpu_info")916        for row_table in table:917            for gns in GPU_NAME_STRIP:918                row_table[ip] = row_table[ip].replace(gns, "")919 920            gpu_names = row_table[ip].split(", ")921            num_gpus = len(gpu_names)922            all_names_the_same = len(set(gpu_names)) == 1923            if len(gpu_names) >= 2 and all_names_the_same:924                row_table[ip] = f"{num_gpus}x {gpu_names[0]}"925 926headers  = [pretty_names.get(p, p) for p in show]927if tool == "llama-bench":928    headers += ["Test", f"t/s {name_baseline}", f"t/s {name_compare}", "Speedup"]929elif tool == "test-backend-ops":930    headers += [f"{primary_metric} {name_baseline}", f"{primary_metric} {name_compare}", "Speedup"]931else:932    assert False933 934if known_args.plot:935    def 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"):936        try:937            import matplotlib938            import matplotlib.pyplot as plt939            matplotlib.use('Agg')940        except ImportError as e:941            logger.error("matplotlib is required for --plot.")942            raise e943 944        data_headers = headers[:-4] # Exclude the last 4 columns (Test, baseline t/s, compare t/s, Speedup)945        plot_x_index = None946        plot_x_label = plot_x_param947 948        if plot_x_param not in ["n_prompt", "n_gen", "n_depth"]:949            pretty_name = LLAMA_BENCH_PRETTY_NAMES.get(plot_x_param, plot_x_param)950            if pretty_name in data_headers:951                plot_x_index = data_headers.index(pretty_name)952                plot_x_label = pretty_name953            elif plot_x_param in data_headers:954                plot_x_index = data_headers.index(plot_x_param)955                plot_x_label = plot_x_param956            else:957                logger.error(f"Parameter '{plot_x_param}' not found in current table columns. Available columns: {', '.join(data_headers)}")958                return959 960        grouped_data = {}961 962        for i, row in enumerate(table_data):963            group_key_parts = []964            test_name = row[-4]965 966            base_test = ""967            x_value = None968 969            if plot_x_param in ["n_prompt", "n_gen", "n_depth"]:970                for j, val in enumerate(row[:-4]):971                    header_name = data_headers[j]972                    if val is not None and str(val).strip():973                        group_key_parts.append(f"{header_name}={val}")974 975                if plot_x_param == "n_prompt" and "pp" in test_name:976                    base_test = test_name.split("@")[0]977                    x_value = base_test978                elif plot_x_param == "n_gen" and "tg" in test_name:979                    x_value = test_name.split("@")[0]980                elif plot_x_param == "n_depth" and "@d" in test_name:981                    base_test = test_name.split("@d")[0]982                    x_value = int(test_name.split("@d")[1])983                else:984                    base_test = test_name985 986                if base_test.strip():987                    group_key_parts.append(f"Test={base_test}")988            else:989                for j, val in enumerate(row[:-4]):990                    if j != plot_x_index:991                        header_name = data_headers[j]992                        if val is not None and str(val).strip():993                            group_key_parts.append(f"{header_name}={val}")994                    else:995                        x_value = val996 997                group_key_parts.append(f"Test={test_name}")998 999            group_key = tuple(group_key_parts)1000 1001            if group_key not in grouped_data:1002                grouped_data[group_key] = []1003 1004            grouped_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 1011        if not grouped_data:1012            logger.error("No data available for plotting")1013            return1014 1015        def make_axes(num_groups, max_cols=2, base_size=(8, 4)):1016            from math import ceil1017            cols = 1 if num_groups == 1 else min(max_cols, num_groups)1018            rows = ceil(num_groups / cols)1019 1020            # Scale figure size by grid dimensions1021            w, h = base_size1022            fig, ax_arr = plt.subplots(rows, cols,1023                                       figsize=(w * cols, h * rows),1024                                       squeeze=False)1025 1026            axes = ax_arr.flatten()[:num_groups]1027            return fig, axes1028 1029        num_groups = len(grouped_data)1030        fig, axes = make_axes(num_groups)1031 1032        plot_idx = 01033 1034        for group_key, points in grouped_data.items():1035            if plot_idx >= len(axes):1036                break1037            ax = axes[plot_idx]1038 1039            try:1040                points_sorted = sorted(points, key=lambda p: float(p['x_value']) if p['x_value'] is not None else 0)1041                x_values = [float(p['x_value']) if p['x_value'] is not None else 0 for p in points_sorted]1042            except ValueError:1043                points_sorted = sorted(points, key=lambda p: group_key)1044                x_values = [p['x_value'] for p in points_sorted]1045 1046            baseline_vals = [p['baseline'] for p in points_sorted]1047            compare_vals = [p['compare'] for p in points_sorted]1048 1049            ax.plot(x_values, baseline_vals, 'o-', color='skyblue',1050                    label=f'{baseline_name}', linewidth=2, markersize=6)1051            ax.plot(x_values, compare_vals, 's--', color='lightcoral', alpha=0.8,1052                    label=f'{compare_name}', linewidth=2, markersize=6)1053 1054            if log_scale:1055                ax.set_xscale('log', base=2)1056                unique_x = sorted(set(x_values))1057                ax.set_xticks(unique_x)1058                ax.set_xticklabels([str(int(x)) for x in unique_x])1059 1060            title_parts = []1061            for part in group_key:1062                if '=' in part:1063                    key, value = part.split('=', 1)1064                    title_parts.append(f"{key}: {value}")1065 1066            title = ', '.join(title_parts) if title_parts else "Performance comparison"1067 1068            # Determine y-axis label based on tool type1069            if tool_type == "llama-bench":1070                y_label = "Tokens per second (t/s)"1071            elif tool_type == "test-backend-ops":1072                y_label = metric_name1073            else:1074                assert False1075 1076            ax.set_xlabel(plot_x_label, fontsize=12, fontweight='bold')1077            ax.set_ylabel(y_label, fontsize=12, fontweight='bold')1078            ax.set_title(title, fontsize=12, fontweight='bold')1079            ax.legend(loc='best', fontsize=10)1080            ax.grid(True, alpha=0.3)1081 1082            plot_idx += 11083 1084        for i in range(plot_idx, len(axes)):1085            axes[i].set_visible(False)1086 1087        fig.suptitle(f'Performance comparison: {compare_name} vs. {baseline_name}',1088                     fontsize=14, fontweight='bold')1089        fig.subplots_adjust(top=1)1090 1091        plt.tight_layout()1092        plt.savefig(output_file, dpi=300, bbox_inches='tight')1093        plt.close()1094 1095    create_performance_plot(table, headers, name_baseline, name_compare, known_args.plot, known_args.plot_x, known_args.plot_log_scale, tool, primary_metric)1096 1097print(tabulate( # noqa: NP1001098    table,1099    headers=headers,1100    floatfmt=".2f",1101    tablefmt=known_args.output1102))1103