Merge pull request #135 from antmicro/jboc/benchmark
Improve speed of benchmark runner
This commit is contained in:
commit
ab8661405a
|
@ -3,6 +3,10 @@
|
|||
# This file is Copyright (c) 2020 Jędrzej Boczar <jboczar@antmicro.com>
|
||||
# License: BSD
|
||||
|
||||
# Limitations/TODO
|
||||
# - add configurable sdram_clk_freq - using hardcoded value now
|
||||
# - sdram_controller_data_width - try to expose the value from litex_sim to avoid duplicated code
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
@ -22,17 +26,12 @@ except ImportError as e:
|
|||
_summary = False
|
||||
print('[WARNING] Results summary not available:', e, file=sys.stderr)
|
||||
|
||||
from litex.tools.litex_sim import get_sdram_phy_settings, sdram_module_nphases
|
||||
from litedram import modules as litedram_modules
|
||||
from litedram.common import Settings as _Settings
|
||||
|
||||
from . import benchmark
|
||||
from .benchmark import LiteDRAMBenchmarkSoC, load_access_pattern
|
||||
|
||||
|
||||
def center(text, width, fillc=' '):
|
||||
added = width - len(text)
|
||||
left = added // 2
|
||||
right = added - left
|
||||
return fillc * left + text + fillc * right
|
||||
from .benchmark import load_access_pattern
|
||||
|
||||
|
||||
# Benchmark configuration --------------------------------------------------------------------------
|
||||
|
@ -128,21 +127,17 @@ class BenchmarkConfiguration(Settings):
|
|||
return 'BenchmarkConfiguration(%s)' % self.as_dict()
|
||||
|
||||
@property
|
||||
def soc(self):
|
||||
if not hasattr(self, '_soc'):
|
||||
kwargs = dict(
|
||||
sdram_module=self.sdram_module,
|
||||
sdram_data_width=self.sdram_data_width,
|
||||
)
|
||||
if isinstance(self.access_pattern, GeneratedAccess):
|
||||
kwargs['bist_length'] = self.access_pattern.bist_length
|
||||
kwargs['bist_random'] = self.access_pattern.bist_random
|
||||
elif isinstance(self.access_pattern, CustomAccess):
|
||||
kwargs['pattern_init'] = self.access_pattern.pattern
|
||||
else:
|
||||
raise ValueError(self.access_pattern)
|
||||
self._soc = LiteDRAMBenchmarkSoC(**kwargs)
|
||||
return self._soc
|
||||
def sdram_clk_freq(self):
|
||||
return 100e6 # FIXME: value of 100MHz is hardcoded in litex_sim
|
||||
|
||||
@property
|
||||
def sdram_controller_data_width(self):
|
||||
# use values from module class (no need to instantiate it)
|
||||
sdram_module_cls = getattr(litedram_modules, self.sdram_module)
|
||||
memtype = sdram_module_cls.memtype
|
||||
nphases = sdram_module_nphases[memtype]
|
||||
dfi_databits = self.sdram_data_width * (1 if memtype == 'SDR' else 2)
|
||||
return dfi_databits * nphases
|
||||
|
||||
# Benchmark results --------------------------------------------------------------------------------
|
||||
|
||||
|
@ -174,7 +169,7 @@ class BenchmarkResult:
|
|||
def find(pattern, output):
|
||||
result = pattern.search(output)
|
||||
assert result is not None, \
|
||||
'Could not find pattern "%s" in output:\n%s' % (pattern, benchmark_output)
|
||||
'Could not find pattern "%s" in output' % (pattern)
|
||||
return int(result.group('value'))
|
||||
|
||||
def __init__(self, output):
|
||||
|
@ -215,6 +210,10 @@ class ResultsSummary:
|
|||
def __init__(self, run_data, plots_dir='plots'):
|
||||
self.plots_dir = plots_dir
|
||||
|
||||
# filter out failures
|
||||
self.failed_configs = [data.config for data in run_data if data.result is None]
|
||||
run_data = [data for data in run_data if data.result is not None]
|
||||
|
||||
# gather results into tabular data
|
||||
column_mappings = {
|
||||
'name': lambda d: d.config.name,
|
||||
|
@ -227,8 +226,8 @@ class ResultsSummary:
|
|||
'generator_ticks': lambda d: d.result.generator_ticks,
|
||||
'checker_errors': lambda d: d.result.checker_errors,
|
||||
'checker_ticks': lambda d: d.result.checker_ticks,
|
||||
'ctrl_data_width': lambda d: d.config.soc.sdram.controller.interface.data_width,
|
||||
'clk_freq': lambda d: d.config.soc.sdrphy.module.clk_freq,
|
||||
'ctrl_data_width': lambda d: d.config.sdram_controller_data_width,
|
||||
'clk_freq': lambda d: d.config.sdram_clk_freq,
|
||||
}
|
||||
columns = {name: [mapping(data) for data in run_data] for name, mapping, in column_mappings.items()}
|
||||
self.df = df = pd.DataFrame(columns)
|
||||
|
@ -274,10 +273,13 @@ class ResultsSummary:
|
|||
'read_latency': ScalarFormatter(),
|
||||
}
|
||||
|
||||
def header(self, text):
|
||||
return '===> {}'.format(text)
|
||||
|
||||
def print_df(self, title, df):
|
||||
# make sure all data will be shown
|
||||
with pd.option_context('display.max_rows', None, 'display.max_columns', None, 'display.width', None):
|
||||
print('===> {}:'.format(title))
|
||||
print(self.header(title + ':'))
|
||||
print(df)
|
||||
|
||||
def get_summary(self, mask=None, columns=None, column_formatting=None, sort_kwargs=None):
|
||||
|
@ -364,31 +366,47 @@ class ResultsSummary:
|
|||
if backend != 'Agg':
|
||||
plt.show()
|
||||
|
||||
def plot_df(self, title, df, column, save_format='png', save_filename=None):
|
||||
def plot_df(self, title, df, column, fig_width=6.4, fig_min_height=2.2, save_format='png', save_filename=None):
|
||||
if save_filename is None:
|
||||
save_filename = os.path.join(self.plots_dir, title.lower().replace(' ', '_'))
|
||||
|
||||
axis = df.plot(kind='barh', x='name', y=column, title=title, grid=True, legend=False)
|
||||
fig = axis.get_figure()
|
||||
|
||||
if column in self.plot_xticks_formatters:
|
||||
axis.xaxis.set_major_formatter(self.plot_xticks_formatters[column])
|
||||
axis.xaxis.set_tick_params(rotation=15)
|
||||
axis.spines['top'].set_visible(False)
|
||||
axis.spines['right'].set_visible(False)
|
||||
axis.set_axisbelow(True)
|
||||
axis.set_ylabel('') # no need for label as we have only one series
|
||||
|
||||
# # force xmax to 100%
|
||||
# if column in ['write_efficiency', 'read_efficiency']:
|
||||
# axis.set_xlim(right=1.0)
|
||||
# for large number of rows, the bar labels start overlapping
|
||||
# use fixed ratio between number of rows and height of figure
|
||||
n_ok = 16
|
||||
new_height = (fig_width / n_ok) * len(df)
|
||||
fig.set_size_inches(fig_width, max(fig_min_height, new_height))
|
||||
|
||||
# remove empty spaces
|
||||
fig.tight_layout()
|
||||
|
||||
return axis
|
||||
|
||||
def failures_summary(self):
|
||||
if len(self.failed_configs) > 0:
|
||||
print(self.header('Failures:'))
|
||||
for config in self.failed_configs:
|
||||
print(' {}: {}'.format(config.name, config.as_args()))
|
||||
else:
|
||||
print(self.header('All benchmarks ok.'))
|
||||
|
||||
# Run ----------------------------------------------------------------------------------------------
|
||||
|
||||
class RunCache(list):
|
||||
RunData = namedtuple('RunData', ['config', 'result'])
|
||||
|
||||
def dump_json(self, filename):
|
||||
json_data = [{'config': data.config.as_dict(), 'output': data.result._output} for data in self]
|
||||
json_data = [{'config': data.config.as_dict(), 'output': getattr(data.result, '_output', None) } for data in self]
|
||||
with open(filename, 'w') as f:
|
||||
json.dump(json_data, f)
|
||||
|
||||
|
@ -399,7 +417,7 @@ class RunCache(list):
|
|||
loaded = []
|
||||
for data in json_data:
|
||||
config = BenchmarkConfiguration.from_dict(data['config'])
|
||||
result = BenchmarkResult(data['output'])
|
||||
result = BenchmarkResult(data['output']) if data['output'] is not None else None
|
||||
loaded.append(cls.RunData(config=config, result=result))
|
||||
return loaded
|
||||
|
||||
|
@ -410,19 +428,45 @@ def run_python(script, args):
|
|||
return str(proc.stdout)
|
||||
|
||||
|
||||
def run_benchmark(config):
|
||||
benchmark_script = os.path.join(os.path.dirname(__file__), 'benchmark.py')
|
||||
def run_single_benchmark(func_args):
|
||||
config, output_dir, ignore_failures = func_args
|
||||
# run as separate process, because else we cannot capture all output from verilator
|
||||
output = run_python(benchmark_script, config.as_args())
|
||||
result = BenchmarkResult(output)
|
||||
# exit if checker had any read error
|
||||
if result.checker_errors != 0:
|
||||
raise RuntimeError('Error during benchmark: checker_errors = {}, args = {}'.format(
|
||||
result.checker_errors, args
|
||||
))
|
||||
print(' {}: {}'.format(config.name, ' '.join(config.as_args())))
|
||||
try:
|
||||
output = run_python(benchmark.__file__, config.as_args() + ['--output-dir', output_dir])
|
||||
result = BenchmarkResult(output)
|
||||
# exit if checker had any read error
|
||||
if result.checker_errors != 0:
|
||||
raise RuntimeError('Error during benchmark: checker_errors = {}, args = {}'.format(
|
||||
result.checker_errors, args
|
||||
))
|
||||
except Exception as e:
|
||||
if ignore_failures:
|
||||
print(' {}: ERROR: {}'.format(config.name, e))
|
||||
return None
|
||||
else:
|
||||
raise
|
||||
print(' {}: ok'.format(config.name))
|
||||
return result
|
||||
|
||||
|
||||
def run_benchmarks(configurations, output_base_dir, njobs, ignore_failures):
|
||||
print('Running {:d} benchmarks ...'.format(len(configurations)))
|
||||
if njobs == 1:
|
||||
results = [run_single_benchmark((config, output_base_dir, ignore_failures)) for config in configurations]
|
||||
else:
|
||||
import multiprocessing
|
||||
func_args = [(config, os.path.join(output_base_dir, config.name.replace(' ', '_')), ignore_failures)
|
||||
for config in configurations]
|
||||
if njobs == 0:
|
||||
njobs = os.cpu_count()
|
||||
print('Using {:d} parallel jobs'.format(njobs))
|
||||
with multiprocessing.Pool(processes=njobs) as pool:
|
||||
results = pool.map(run_single_benchmark, func_args)
|
||||
run_data = [RunCache.RunData(config, result) for config, result in zip(configurations, results)]
|
||||
return run_data
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Run LiteDRAM benchmarks and collect the results.')
|
||||
|
@ -436,7 +480,9 @@ def main(argv=None):
|
|||
parser.add_argument('--plot-transparent', action='store_true', help='Use transparent background when saving plots')
|
||||
parser.add_argument('--plot-output-dir', default='plots', help='Specify where to save the plots')
|
||||
parser.add_argument('--plot-theme', default='default', help='Use different matplotlib theme')
|
||||
parser.add_argument('--ignore-failures', action='store_true', help='Ignore failuers during benchmarking, continue using successful runs only')
|
||||
parser.add_argument('--fail-fast', action='store_true', help='Exit on any benchmark error, do not continue')
|
||||
parser.add_argument('--output-dir', default='build', help='Directory to store benchmark build output')
|
||||
parser.add_argument('--njobs', default=0, type=int, help='Use N parallel jobs to run benchmarks (default=0, which uses CPU count)')
|
||||
parser.add_argument('--results-cache', help="""Use given JSON file as results cache. If the file exists,
|
||||
it will be loaded instead of running actual benchmarks,
|
||||
else benchmarks will be run normally, and then saved
|
||||
|
@ -470,14 +516,7 @@ def main(argv=None):
|
|||
names_to_load = [c.name for c in configurations]
|
||||
run_data = [data for data in cache if data.config.name in names_to_load]
|
||||
else: # run all the benchmarks normally
|
||||
run_data = []
|
||||
for config in configurations:
|
||||
print(' {}: {}'.format(config.name, ' '.join(config.as_args())))
|
||||
try:
|
||||
run_data.append(RunCache.RunData(config, run_benchmark(config)))
|
||||
except:
|
||||
if not args.ignore_failures:
|
||||
raise
|
||||
run_data = run_benchmarks(configurations, args.output_dir, args.njobs, not args.fail_fast)
|
||||
|
||||
# store outputs in cache
|
||||
if args.results_cache and not cache_exists:
|
||||
|
@ -488,6 +527,7 @@ def main(argv=None):
|
|||
if _summary:
|
||||
summary = ResultsSummary(run_data)
|
||||
summary.text_summary()
|
||||
summary.failures_summary()
|
||||
if args.plot:
|
||||
summary.plot_summary(
|
||||
plots_dir=args.plot_output_dir,
|
||||
|
|
Loading…
Reference in New Issue