/ scripts / run_benchmarks.py
run_benchmarks.py
  1  #!/usr/bin/env python3
  2  
  3  import unittest
  4  import asyncio
  5  import argparse
  6  import json
  7  import re
  8  import sys
  9  import os
 10  import subprocess
 11  import tabulate
 12  from typing import Literal
 13  from pathlib import Path
 14  
 15  topdir = Path(__file__).parent.parent
 16  sys.path.insert(0, str(topdir))
 17  
 18  import test.regression.benchmark  # noqa: E402
 19  from test.regression.benchmark import BenchmarkResult  # noqa: E402
 20  from test.regression.pysim import PySimulation  # noqa: E402
 21  
 22  
 23  def cd_to_topdir():
 24      os.chdir(str(topdir))
 25  
 26  
 27  def load_benchmarks():
 28      all_tests = test.regression.benchmark.get_all_benchmark_names()
 29      if len(all_tests) == 0:
 30          res = subprocess.run(["make", "-C", "test/external/embench"])
 31          if res.returncode != 0:
 32              print("Couldn't build benchmarks")
 33              sys.exit(1)
 34  
 35          all_tests = test.regression.benchmark.get_all_benchmark_names()
 36  
 37      exclude = {
 38          "cubic",
 39          "huffbench",
 40          "nbody",
 41          "picojpeg",
 42          "primecount",
 43          "qrduino",
 44          "sglib-combined",
 45          "st",
 46          "wikisort",
 47          "matmult-int",
 48          "edn",
 49          "nettle-aes",
 50          "md5sum",
 51          "tarfind",
 52      }
 53  
 54      ret = list(set(all_tests) - exclude)
 55      ret.sort()
 56      return ret
 57  
 58  
 59  def run_benchmarks_with_cocotb(benchmarks: list[str], traces: bool) -> bool:
 60      arglist = ["make", "-C", "test/regression/cocotb", "-f", "benchmark.Makefile", "--no-print-directory"]
 61  
 62      test_cases = ",".join(benchmarks)
 63      arglist += [f"TESTCASE={test_cases}"]
 64  
 65      verilog_code = topdir.joinpath("core.v")
 66      gen_info_path = f"{verilog_code}.json"
 67  
 68      arglist += [f"VERILOG_SOURCES={verilog_code}"]
 69      arglist += [f"_COREBLOCKS_GEN_INFO={gen_info_path}"]
 70  
 71      if traces:
 72          arglist += ["TRACES=1"]
 73  
 74      res = subprocess.run(arglist)
 75  
 76      return res.returncode == 0
 77  
 78  
 79  def run_benchmarks_with_pysim(benchmarks: list[str], traces: bool) -> bool:
 80      suite = unittest.TestSuite()
 81  
 82      def _gen_test(test_name: str):
 83          def test_fn():
 84              traces_file = None
 85              if traces:
 86                  traces_file = "benchmark." + test_name
 87              asyncio.run(test.regression.benchmark.run_benchmark(PySimulation(traces_file=traces_file), test_name))
 88  
 89          test_fn.__name__ = test_name
 90          test_fn.__qualname__ = test_name
 91  
 92          return test_fn
 93  
 94      for test_name in benchmarks:
 95          suite.addTest(unittest.FunctionTestCase(_gen_test(test_name)))
 96  
 97      runner = unittest.TextTestRunner(verbosity=2)
 98      result = runner.run(suite)
 99  
100      return result.wasSuccessful()
101  
102  
103  def run_benchmarks(benchmarks: list[str], backend: Literal["pysim", "cocotb"], traces: bool) -> bool:
104      if backend == "cocotb":
105          return run_benchmarks_with_cocotb(benchmarks, traces)
106      elif backend == "pysim":
107          return run_benchmarks_with_pysim(benchmarks, traces)
108      return False
109  
110  
111  def build_result_table(results: dict[str, BenchmarkResult], tablefmt: str) -> str:
112      if len(results) == 0:
113          return ""
114  
115      header = ["Testbench name", "Cycles", "Instructions", "IPC"]
116  
117      # First fetch all metrics names to build the header
118      result = next(iter(results.values()))
119      for metric_name in sorted(result.metric_values.keys()):
120          regs = result.metric_values[metric_name]
121          for reg_name in regs:
122              header.append(f"{metric_name}/{reg_name}")
123  
124      columns = [header]
125      for benchmark_name, result in results.items():
126          ipc = result.instr / result.cycles
127  
128          column = [benchmark_name, result.cycles, result.instr, ipc]
129  
130          for metric_name in sorted(result.metric_values.keys()):
131              regs = result.metric_values[metric_name]
132              for reg_name in regs:
133                  column.append(regs[reg_name])
134  
135          columns.append(column)
136  
137      # Transpose the table, as the library expects to get a list of rows (and we have a list of columns).
138      rows = [list(i) for i in zip(*columns)]
139  
140      return tabulate.tabulate(rows, headers="firstrow", tablefmt=tablefmt)
141  
142  
143  def main():
144      parser = argparse.ArgumentParser()
145      parser.add_argument("-l", "--list", action="store_true", help="List all benchmarks")
146      parser.add_argument("-t", "--trace", action="store_true", help="Dump waveforms")
147      parser.add_argument("--log-level", default="WARNING", action="store", help="Level of messages to display.")
148      parser.add_argument("--log-filter", default=".*", action="store", help="Regexp used to filter out logs.")
149      parser.add_argument("-p", "--profile", action="store_true", help="Write execution profiles")
150      parser.add_argument("-b", "--backend", default="cocotb", choices=["cocotb", "pysim"], help="Simulation backend")
151      parser.add_argument(
152          "-o",
153          "--output",
154          default="benchmark.json",
155          help="Selects output file to write information to. Default: %(default)s",
156      )
157      parser.add_argument("--summary", default="", action="store", help="Write Markdown summary to this file")
158      parser.add_argument("benchmark_name", nargs="?")
159  
160      args = parser.parse_args()
161  
162      benchmarks = load_benchmarks()
163  
164      if args.list:
165          for name in benchmarks:
166              print(name)
167          return
168  
169      os.environ["__TRANSACTRON_LOG_LEVEL"] = args.log_level
170      os.environ["__TRANSACTRON_LOG_FILTER"] = args.log_filter
171  
172      if args.benchmark_name:
173          pattern = re.compile(args.benchmark_name)
174          benchmarks = [name for name in benchmarks if pattern.search(name)]
175  
176          if not benchmarks:
177              print(f"Could not find benchmark '{args.benchmark_name}'")
178              sys.exit(1)
179  
180      if args.profile:
181          os.environ["__TRANSACTRON_PROFILE"] = "1"
182  
183      success = run_benchmarks(benchmarks, args.backend, args.trace)
184      if not success:
185          print("Benchmark execution failed")
186          sys.exit(1)
187  
188      ipcs = []
189  
190      results: dict[str, BenchmarkResult] = {}
191  
192      for name in benchmarks:
193          with open(f"{str(test.regression.benchmark.results_dir)}/{name}.json", "r") as f:
194              result = BenchmarkResult.from_json(f.read())  # type: ignore
195  
196          results[name] = result
197  
198          ipc = result.instr / result.cycles
199          ipcs.append({"name": name, "unit": "Instructions Per Cycle", "value": ipc})
200  
201      print(build_result_table(results, "simple_outline"))
202  
203      if args.summary != "":
204          with open(args.summary, "w") as summary_file:
205              print(build_result_table(results, "github"), file=summary_file)
206  
207      with open(args.output, "w") as benchmark_file:
208          json.dump(ipcs, benchmark_file, indent=4)
209  
210  
211  if __name__ == "__main__":
212      cd_to_topdir()
213      main()