Merge pull request #131 from antmicro/jboc/benchmark

Allow testing custom access patterns
This commit is contained in:
enjoy-digital 2020-02-06 18:12:35 +01:00 committed by GitHub
commit 08fd2960d0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 1871 additions and 299 deletions

View File

@ -185,6 +185,60 @@ class _LiteDRAMBISTGenerator(Module):
raise NotImplementedError
self.comb += dma.sink.data.eq(data_gen.o)
@ResetInserter()
class _LiteDRAMPatternGenerator(Module):
def __init__(self, dram_port, init=[]):
ashift, awidth = get_ashift_awidth(dram_port)
self.start = Signal()
self.done = Signal()
self.ticks = Signal(32)
# # #
# DMA --------------------------------------------------------------------------------------
dma = LiteDRAMDMAWriter(dram_port)
self.submodules += dma
cmd_counter = Signal(dram_port.address_width, reset_less=True)
# Data / Address FSM -----------------------------------------------------------------------
fsm = FSM(reset_state="IDLE")
self.submodules += fsm
fsm.act("IDLE",
If(self.start,
NextValue(cmd_counter, 0),
NextState("RUN")
),
NextValue(self.ticks, 0)
)
fsm.act("RUN",
dma.sink.valid.eq(1),
If(dma.sink.ready,
NextValue(cmd_counter, cmd_counter + 1),
If(cmd_counter == (len(init) - 1),
NextState("DONE")
)
),
NextValue(self.ticks, self.ticks + 1)
)
fsm.act("DONE",
self.done.eq(1)
)
if isinstance(dram_port, LiteDRAMNativePort): # addressing in dwords
dma_sink_addr = dma.sink.address
elif isinstance(dram_port, LiteDRAMAXIPort): # addressing in bytes
dma_sink_addr = dma.sink.address[ashift:]
else:
raise NotImplementedError
addr_cases = {i: dma_sink_addr.eq(addr) for i, (addr, data) in enumerate(init)}
data_cases = {i: dma.sink.data.eq(data) for i, (addr, data) in enumerate(init)}
self.comb += [
Case(cmd_counter, addr_cases),
Case(cmd_counter, data_cases),
]
# LiteDRAMBISTGenerator ----------------------------------------------------------------------------
class LiteDRAMBISTGenerator(Module, AutoCSR):
@ -367,6 +421,88 @@ class _LiteDRAMBISTChecker(Module, AutoCSR):
self.done.eq(1)
)
@ResetInserter()
class _LiteDRAMPatternChecker(Module, AutoCSR):
def __init__(self, dram_port, init=[]):
ashift, awidth = get_ashift_awidth(dram_port)
self.start = Signal()
self.done = Signal()
self.ticks = Signal(32)
self.errors = Signal(32)
# # #
# DMA --------------------------------------------------------------------------------------
dma = LiteDRAMDMAReader(dram_port)
self.submodules += dma
# Address FSM ------------------------------------------------------------------------------
cmd_counter = Signal(dram_port.address_width, reset_less=True)
cmd_fsm = FSM(reset_state="IDLE")
self.submodules += cmd_fsm
cmd_fsm.act("IDLE",
If(self.start,
NextValue(cmd_counter, 0),
NextState("RUN")
)
)
cmd_fsm.act("RUN",
dma.sink.valid.eq(1),
If(dma.sink.ready,
NextValue(cmd_counter, cmd_counter + 1),
If(cmd_counter == (len(init) - 1),
NextState("DONE")
)
)
)
cmd_fsm.act("DONE")
if isinstance(dram_port, LiteDRAMNativePort): # addressing in dwords
dma_addr_sink = dma.sink.address
elif isinstance(dram_port, LiteDRAMAXIPort): # addressing in bytes
dma_addr_sink = dma.sink.address[ashift:]
else:
raise NotImplementedError
addr_cases = {i: dma_addr_sink.eq(addr) for i, (addr, data) in enumerate(init)}
self.comb += Case(cmd_counter, addr_cases)
# Data FSM ---------------------------------------------------------------------------------
data_counter = Signal(dram_port.address_width, reset_less=True)
expected_data = Signal.like(dma.source.data)
data_cases = {i: expected_data.eq(data) for i, (addr, data) in enumerate(init)}
self.comb += Case(data_counter, data_cases)
data_fsm = FSM(reset_state="IDLE")
self.submodules += data_fsm
data_fsm.act("IDLE",
If(self.start,
NextValue(data_counter, 0),
NextValue(self.errors, 0),
NextState("RUN")
),
NextValue(self.ticks, 0)
)
data_fsm.act("RUN",
dma.source.ready.eq(1),
If(dma.source.valid,
NextValue(data_counter, data_counter + 1),
If(dma.source.data != expected_data,
NextValue(self.errors, self.errors + 1)
),
If(data_counter == (len(init) - 1),
NextState("DONE")
)
),
NextValue(self.ticks, self.ticks + 1)
)
data_fsm.act("DONE",
self.done.eq(1)
)
# LiteDRAMBISTChecker ------------------------------------------------------------------------------
class LiteDRAMBISTChecker(Module, AutoCSR):

1024
test/access_pattern.csv Normal file

File diff suppressed because it is too large Load Diff

View File

@ -3,6 +3,7 @@
# This file is Copyright (c) 2020 Florent Kermarrec <florent@enjoy-digital.fr>
# License: BSD
import csv
import argparse
from migen import *
@ -16,9 +17,8 @@ from litex.soc.integration.builder import *
from litex.tools.litex_sim import SimSoC
from litedram.frontend.bist import _LiteDRAMBISTGenerator
from litedram.frontend.bist import _LiteDRAMBISTChecker
from litedram.frontend.bist import _LiteDRAMBISTGenerator, _LiteDRAMBISTChecker, \
_LiteDRAMPatternGenerator, _LiteDRAMPatternChecker
# LiteDRAM Benchmark SoC ---------------------------------------------------------------------------
@ -29,6 +29,7 @@ class LiteDRAMBenchmarkSoC(SimSoC):
bist_base = 0x00000000,
bist_length = 1024,
bist_random = False,
pattern_init = None,
**kwargs):
# SimSoC -----------------------------------------------------------------------------------
@ -42,12 +43,34 @@ class LiteDRAMBenchmarkSoC(SimSoC):
# make sure that we perform at least one access
bist_length = max(bist_length, self.sdram.controller.interface.data_width // 8)
# BIST Generator ---------------------------------------------------------------------------
bist_generator = _LiteDRAMBISTGenerator(self.sdram.crossbar.get_port())
self.submodules.bist_generator = bist_generator
# BIST Generator / Checker -----------------------------------------------------------------
if pattern_init is None:
bist_generator = _LiteDRAMBISTGenerator(self.sdram.crossbar.get_port())
bist_checker = _LiteDRAMBISTChecker(self.sdram.crossbar.get_port())
# BIST Checker -----------------------------------------------------------------------------
bist_checker = _LiteDRAMBISTChecker(self.sdram.crossbar.get_port())
generator_config = [
bist_generator.base.eq(bist_base),
bist_generator.length.eq(bist_length),
bist_generator.random.eq(bist_random),
]
checker_config = [
bist_checker.base.eq(bist_base),
bist_checker.length.eq(bist_length),
bist_checker.random.eq(bist_random),
]
else:
# TODO: run checker in parallel to avoid overwriting previously written data
address_set = set()
for addr, _ in pattern_init:
assert addr not in address_set, \
'Duplicate address 0x%08x in pattern_init, write will overwrite previous value!' % addr
address_set.add(addr)
bist_generator = _LiteDRAMPatternGenerator(self.sdram.crossbar.get_port(), init=pattern_init)
bist_checker = _LiteDRAMPatternChecker(self.sdram.crossbar.get_port(), init=pattern_init)
generator_config = checker_config = []
self.submodules.bist_generator = bist_generator
self.submodules.bist_checker = bist_checker
# Sequencer --------------------------------------------------------------------------------
@ -68,18 +91,14 @@ class LiteDRAMBenchmarkSoC(SimSoC):
)
fsm.act("BIST-GENERATOR",
bist_generator.start.eq(1),
bist_generator.base.eq(bist_base),
bist_generator.length.eq(bist_length),
bist_generator.random.eq(bist_random),
*generator_config,
If(bist_generator.done,
NextState("BIST-CHECKER")
)
)
fsm.act("BIST-CHECKER",
bist_checker.start.eq(1),
bist_checker.base.eq(bist_base),
bist_checker.length.eq(bist_length),
bist_checker.random.eq(bist_random),
*checker_config,
If(bist_checker.done,
NextState("DISPLAY")
)
@ -109,20 +128,28 @@ class LiteDRAMBenchmarkSoC(SimSoC):
# Build --------------------------------------------------------------------------------------------
def load_access_pattern(filename):
with open(filename, newline='') as f:
reader = csv.reader(f)
pattern_init = [(int(addr, 0), int(data, 0)) for addr, data in reader]
return pattern_init
def main():
parser = argparse.ArgumentParser(description="LiteDRAM Benchmark SoC Simulation")
builder_args(parser)
soc_sdram_args(parser)
parser.add_argument("--threads", default=1, help="Set number of threads (default=1)")
parser.add_argument("--sdram-module", default="MT48LC16M16", help="Select SDRAM chip")
parser.add_argument("--sdram-data-width", default=32, help="Set SDRAM chip data width")
parser.add_argument("--trace", action="store_true", help="Enable VCD tracing")
parser.add_argument("--trace-start", default=0, help="Cycle to start VCD tracing")
parser.add_argument("--trace-end", default=-1, help="Cycle to end VCD tracing")
parser.add_argument("--opt-level", default="O0", help="Compilation optimization level")
parser.add_argument("--bist-base", default="0x00000000", help="Base address of the test (default=0)")
parser.add_argument("--bist-length", default="1024", help="Length of the test (default=1024)")
parser.add_argument("--bist-random", action="store_true", help="Use random data during the test")
parser.add_argument("--threads", default=1, help="Set number of threads (default=1)")
parser.add_argument("--sdram-module", default="MT48LC16M16", help="Select SDRAM chip")
parser.add_argument("--sdram-data-width", default=32, help="Set SDRAM chip data width")
parser.add_argument("--trace", action="store_true", help="Enable VCD tracing")
parser.add_argument("--trace-start", default=0, help="Cycle to start VCD tracing")
parser.add_argument("--trace-end", default=-1, help="Cycle to end VCD tracing")
parser.add_argument("--opt-level", default="O0", help="Compilation optimization level")
parser.add_argument("--bist-base", default="0x00000000", help="Base address of the test (default=0)")
parser.add_argument("--bist-length", default="1024", help="Length of the test (default=1024)")
parser.add_argument("--bist-random", action="store_true", help="Use random data during the test")
parser.add_argument("--access-pattern", help="Load access pattern (address, data) from CSV (ignores --bist-*)")
args = parser.parse_args()
soc_kwargs = soc_sdram_argdict(args)
@ -138,6 +165,9 @@ def main():
soc_kwargs["bist_length"] = int(args.bist_length, 0)
soc_kwargs["bist_random"] = args.bist_random
if args.access_pattern:
soc_kwargs["pattern_init"] = load_access_pattern(args.access_pattern)
# SoC ------------------------------------------------------------------------------------------
soc = LiteDRAMBenchmarkSoC(**soc_kwargs)

View File

@ -1,76 +1,193 @@
{
# sequential access
"test_0": {
"sdram_module": 'MT48LC16M16',
"sdram_data_width": 32,
"bist_length": 4096,
"bist_random": True,
"access_pattern": {
"bist_length": 4096,
"bist_random": False,
}
},
"test_1": {
"sdram_module": 'MT48LC16M16',
"sdram_data_width": 32,
"bist_length": 512,
"bist_random": False,
"access_pattern": {
"bist_length": 512,
"bist_random": False,
}
},
"test_2": {
"sdram_module": 'MT46V32M16',
"sdram_data_width": 32,
"bist_length": 512,
"bist_random": False,
"access_pattern": {
"bist_length": 512,
"bist_random": False,
}
},
"test_3": {
"sdram_module": 'MT46V32M16',
"sdram_data_width": 32,
"bist_length": 2048,
"bist_random": False,
"access_pattern": {
"bist_length": 2048,
"bist_random": False,
}
},
"test_4": {
"sdram_module": 'MT47H64M16',
"sdram_data_width": 32,
"bist_length": 1024,
"bist_random": False,
"access_pattern": {
"bist_length": 1024,
"bist_random": False,
}
},
"test_5": {
"sdram_module": 'MT47H64M16',
"sdram_data_width": 16,
"bist_length": 1024,
"bist_random": False,
"access_pattern": {
"bist_length": 1024,
"bist_random": False,
}
},
"test_6": {
"sdram_module": 'MT41K128M16',
"sdram_data_width": 16,
"bist_length": 1024,
"bist_random": False,
"access_pattern": {
"bist_length": 1024,
"bist_random": False,
}
},
"test_7": {
"sdram_module": 'MT41K128M16',
"sdram_data_width": 32,
"bist_length": 1024,
"bist_random": False,
"access_pattern": {
"bist_length": 1024,
"bist_random": False,
}
},
# latency
"test_8": {
"sdram_module": 'MT48LC16M16',
"sdram_data_width": 32,
"bist_length": 1,
"bist_random": False,
"access_pattern": {
"bist_length": 1,
"bist_random": False,
}
},
"test_9": {
"sdram_module": 'MT46V32M16',
"sdram_data_width": 32,
"bist_length": 1,
"bist_random": False,
"access_pattern": {
"bist_length": 1,
"bist_random": False,
}
},
"test_10": {
"sdram_module": 'MT47H64M16',
"sdram_data_width": 32,
"bist_length": 1,
"bist_random": False,
"access_pattern": {
"bist_length": 1,
"bist_random": False,
}
},
"test_11": {
"sdram_module": 'MT41K128M16',
"sdram_data_width": 16,
"bist_length": 1,
"bist_random": False,
"access_pattern": {
"bist_length": 1,
"bist_random": False,
}
},
# random access
"test_12": {
"sdram_module": 'MT48LC16M16',
"sdram_data_width": 32,
"access_pattern": {
"bist_length": 1024,
"bist_random": True,
}
},
"test_13": {
"sdram_module": 'MT46V32M16',
"sdram_data_width": 32,
"access_pattern": {
"bist_length": 1024,
"bist_random": True,
}
},
"test_14": {
"sdram_module": 'MT47H64M16',
"sdram_data_width": 32,
"access_pattern": {
"bist_length": 1024,
"bist_random": True,
}
},
"test_15": {
"sdram_module": 'MT41K128M16',
"sdram_data_width": 16,
"access_pattern": {
"bist_length": 1024,
"bist_random": True,
}
},
# custom access pattern
"test_16": {
"sdram_module": 'MT48LC16M16',
"sdram_data_width": 32,
"access_pattern": {
"pattern_file": "access_pattern.csv"
}
},
"test_17": {
"sdram_module": 'MT48LC16M16',
"sdram_data_width": 32,
"access_pattern": {
"pattern_file": "access_pattern.csv",
}
},
"test_18": {
"sdram_module": 'MT46V32M16',
"sdram_data_width": 32,
"access_pattern": {
"pattern_file": "access_pattern.csv",
}
},
"test_19": {
"sdram_module": 'MT46V32M16',
"sdram_data_width": 32,
"access_pattern": {
"pattern_file": "access_pattern.csv",
}
},
"test_20": {
"sdram_module": 'MT47H64M16',
"sdram_data_width": 32,
"access_pattern": {
"pattern_file": "access_pattern.csv",
}
},
"test_21": {
"sdram_module": 'MT47H64M16',
"sdram_data_width": 16,
"access_pattern": {
"pattern_file": "access_pattern.csv",
}
},
"test_22": {
"sdram_module": 'MT41K128M16',
"sdram_data_width": 16,
"access_pattern": {
"pattern_file": "access_pattern.csv",
}
},
"test_23": {
"sdram_module": 'MT41K128M16',
"sdram_data_width": 32,
"access_pattern": {
"pattern_file": "access_pattern.csv",
}
},
}

34
test/gen_access_pattern.py Executable file
View File

@ -0,0 +1,34 @@
#!/usr/bin/env python
import random
import argparse
def main():
desc = """
Generate random access pattern.
Each address in range [base, base+length) will be accessed only once,
but in random order. This ensures that no data will be overwritten.
"""
parser = argparse.ArgumentParser(description=desc)
parser.add_argument('base', help='Base address')
parser.add_argument('length', help='Number of (address, data) pairs')
parser.add_argument('data_width', help='Width of data (used to determine max value)')
parser.add_argument('--seed', help='Use given random seed (int)')
args = parser.parse_args()
if args.seed:
random.seed(int(args.seed, 0))
base = int(args.base, 0)
length = int(args.length, 0)
data_width = int(args.data_width, 0)
address = list(range(length))
random.shuffle(address)
data = [random.randrange(0, 2**data_width) for _ in range(length)]
for a, d in zip(address, data):
print('0x{:08x},0x{:08x}'.format(a, d))
if __name__ == "__main__":
main()

87
test/gen_config.py Executable file
View File

@ -0,0 +1,87 @@
#!/usr/bin/env python
import json
import argparse
import itertools
modules = [
'IS42S16160',
'IS42S16320',
'MT48LC4M16',
'MT48LC16M16',
'AS4C16M16',
'AS4C32M16',
'AS4C32M8',
'M12L64322A',
'M12L16161A',
'MT46V32M16',
'MT46H32M16',
'MT46H32M32',
'MT47H128M8',
'MT47H32M16',
'MT47H64M16',
'P3R1GE4JGF',
'MT41K64M16',
'MT41J128M16',
'MT41K128M16',
'MT41J256M16',
'MT41K256M16',
'K4B1G0446F',
'K4B2G1646F',
'H5TC4G63CFR',
'IS43TR16128B',
'MT8JTF12864',
'MT8KTF51264',
# 'MT18KSF1G72HZ',
# 'AS4C256M16D3A',
# 'MT16KTF1G64HZ',
# 'EDY4016A',
# 'MT40A1G8',
# 'MT40A512M16',
]
data_widths = [32]
bist_lengths = [1, 1024, 8192]
bist_randoms = [False]
access_patterns = ['access_pattern.csv']
def main():
parser = argparse.ArgumentParser(description='Generate configuration for all possible argument combinations.',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument('--sdram-modules', nargs='+', default=modules, help='--sdram-module options')
parser.add_argument('--sdram-data-widths', nargs='+', default=data_widths, help='--sdram-data-width options')
parser.add_argument('--bist-lengths', nargs='+', default=bist_lengths, help='--bist-length options')
parser.add_argument('--bist-randoms', nargs='+', default=bist_randoms, help='--bist-random options')
parser.add_argument('--access-patterns', nargs='+', default=access_patterns, help='--access-pattern options')
parser.add_argument('--name-format', default='test_%d', help='Name format for i-th test')
args = parser.parse_args()
bist_product = itertools.product(args.sdram_modules, args.sdram_data_widths, args.bist_lengths, args.bist_randoms)
pattern_product = itertools.product(args.sdram_modules, args.sdram_data_widths, args.access_patterns)
i = 0
configurations = {}
for module, data_width, bist_length, bist_random in bist_product:
configurations[args.name_format % i] = {
'sdram_module': module,
'sdram_data_width': data_width,
'access_pattern': {
'bist_length': bist_length,
'bist_random': bist_random,
}
}
i += 1
for module, data_width, access_pattern in pattern_product:
configurations[args.name_format % i] = {
'sdram_module': module,
'sdram_data_width': data_width,
'access_pattern': {
'pattern_file': access_pattern,
}
}
i += 1
json_str = json.dumps(configurations, indent=4)
print(json_str)
if __name__ == "__main__":
main()

View File

@ -12,22 +12,182 @@ import subprocess
from collections import defaultdict, namedtuple
import yaml
try:
import numpy as np
import pandas as pd
import matplotlib
from matplotlib.ticker import FuncFormatter, PercentFormatter, ScalarFormatter
_summary = True
except ImportError as e:
_summary = False
print('[WARNING] Results summary not available:', e, file=sys.stderr)
from litedram.common import Settings
from litedram.common import Settings as _Settings
from .benchmark import LiteDRAMBenchmarkSoC
from . import benchmark
from .benchmark import LiteDRAMBenchmarkSoC, load_access_pattern
# constructs python regex named group
def ng(name, regex):
return r'(?P<{}>{})'.format(name, regex)
def center(text, width, fillc=' '):
added = width - len(text)
left = added // 2
right = added - left
return fillc * left + text + fillc * right
# Benchmark configuration --------------------------------------------------------------------------
class Settings(_Settings):
def as_dict(self):
d = dict()
for attr, value in vars(self).items():
if attr == 'self' or attr.startswith('_'):
continue
if isinstance(value, Settings):
value = value.as_dict()
d[attr] = value
return d
class GeneratedAccess(Settings):
def __init__(self, bist_length, bist_random):
self.set_attributes(locals())
@property
def length(self):
return self.bist_length
def as_args(self):
args = ['--bist-length=%d' % self.bist_length]
if self.bist_random:
args.append('--bist-random')
return args
class CustomAccess(Settings):
def __init__(self, pattern_file):
self.set_attributes(locals())
@property
def pattern(self):
# we have to load the file to know pattern length, cache it when requested
if not hasattr(self, '_pattern'):
path = self.pattern_file
if not os.path.isabs(path):
benchmark_dir = os.path.dirname(benchmark.__file__)
path = os.path.join(benchmark_dir, path)
self._pattern = load_access_pattern(path)
return self._pattern
@property
def length(self):
return len(self.pattern)
def as_args(self):
return ['--access-pattern=%s' % self.pattern_file]
class BenchmarkConfiguration(Settings):
def __init__(self, name, sdram_module, sdram_data_width, access_pattern):
self.set_attributes(locals())
def as_args(self):
args = [
'--sdram-module=%s' % self.sdram_module,
'--sdram-data-width=%d' % self.sdram_data_width,
]
args += self.access_pattern.as_args()
return args
def __eq__(self, other):
if not isinstance(other, BenchmarkConfiguration):
return NotImplemented
return self.as_dict() == other.as_dict()
@property
def length(self):
return self.access_pattern.length
@classmethod
def from_dict(cls, d):
access_cls = CustomAccess if 'pattern_file' in d['access_pattern'] else GeneratedAccess
d['access_pattern'] = access_cls(**d['access_pattern'])
return cls(**d)
@classmethod
def load_yaml(cls, yaml_file):
with open(yaml_file) as f:
description = yaml.safe_load(f)
configs = []
for name, desc in description.items():
desc['name'] = name
configs.append(cls.from_dict(desc))
return configs
def __repr__(self):
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
# Benchmark results --------------------------------------------------------------------------------
# constructs python regex named group
def ng(name, regex):
return r'(?P<{}>{})'.format(name, regex)
def _compiled_pattern(stage, var):
pattern_fmt = r'{stage}\s+{var}:\s+{value}'
pattern = pattern_fmt.format(
stage=stage,
var=var,
value=ng('value', '[0-9]+'),
)
return re.compile(pattern)
result = re.search(pattern, benchmark_output)
class BenchmarkResult:
# pre-compiled patterns for all benchmarks
patterns = {
'generator_ticks': _compiled_pattern('BIST-GENERATOR', 'ticks'),
'checker_errors': _compiled_pattern('BIST-CHECKER', 'errors'),
'checker_ticks': _compiled_pattern('BIST-CHECKER', 'ticks'),
}
@staticmethod
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)
return int(result.group('value'))
def __init__(self, output):
self._output = output
for attr, pattern in self.patterns.items():
setattr(self, attr, self.find(pattern, output))
def __repr__(self):
d = {attr: getattr(self, attr) for attr in self.patterns.keys()}
return 'BenchmarkResult(%s)' % d
# Results summary ----------------------------------------------------------------------------------
def human_readable(value):
binary_prefixes = ['', 'k', 'M', 'G', 'T']
mult = 1.0
@ -37,262 +197,230 @@ def human_readable(value):
mult /= 1024
return mult, prefix
# Benchmark configuration --------------------------------------------------------------------------
class BenchmarkConfiguration(Settings):
def __init__(self, sdram_module, sdram_data_width, bist_length, bist_random):
self.set_attributes(locals())
self._settings = {k: v for k, v in locals().items() if k != 'self'}
def clocks_fmt(clocks):
return '{:d} clk'.format(int(clocks))
def as_args(self):
args = []
for attr, value in self._settings.items():
arg_string = '--%s' % attr.replace('_', '-')
if isinstance(value, bool):
if value:
args.append(arg_string)
else:
args.extend([arg_string, str(value)])
return args
def __eq__(self, other):
if not isinstance(other, BenchmarkConfiguration):
return NotImplemented
return all((getattr(self, setting) == getattr(other, setting)
for setting in self._settings.keys()))
def bandwidth_fmt(bw):
mult, prefix = human_readable(bw)
return '{:.1f} {}bps'.format(bw * mult, prefix)
@classmethod
def load_yaml(cls, yaml_file):
with open(yaml_file) as f:
description = yaml.safe_load(f)
configurations = {name: cls(**desc) for name, desc in description.items()}
return configurations
# Benchmark results --------------------------------------------------------------------------------
def efficiency_fmt(eff):
return '{:.1f} %'.format(eff * 100)
class BenchmarkResult:
def __init__(self, config, output):
self.config = config
self._output = output
self.parse_output(output)
# instantiate the benchmarked soc to check its configuration
self.benchmark_soc = LiteDRAMBenchmarkSoC(**self.config._settings)
def cmd_count(self):
data_width = self.benchmark_soc.sdram.controller.interface.data_width
return self.config.bist_length / (data_width // 8)
def clk_period(self):
clk_freq = self.benchmark_soc.sdrphy.module.clk_freq
return 1 / clk_freq
def write_bandwidth(self):
return (8 * self.config.bist_length) / (self.generator_ticks * self.clk_period())
def read_bandwidth(self):
return (8 * self.config.bist_length) / (self.checker_ticks * self.clk_period())
def write_efficiency(self):
return self.cmd_count() / self.generator_ticks
def read_efficiency(self):
return self.cmd_count() / self.checker_ticks
def write_latency(self):
assert self.config.bist_length == 1, 'Not a latency benchmark'
return self.generator_ticks
def read_latency(self):
assert self.config.bist_length == 1, 'Not a latency benchmark'
return self.checker_ticks
def parse_output(self, output):
bist_pattern = r'{stage}\s+{var}:\s+{value}'
def find(stage, var):
pattern = bist_pattern.format(
stage=stage,
var=var,
value=ng('value', '[0-9]+'),
)
result = re.search(pattern, output)
assert result is not None, 'Could not find pattern in output: %s, %s' % (pattern, output)
return int(result.group('value'))
self.generator_ticks = find('BIST-GENERATOR', 'ticks')
self.checker_errors = find('BIST-CHECKER', 'errors')
self.checker_ticks = find('BIST-CHECKER', 'ticks')
@classmethod
def dump_results_json(cls, results, file):
"""Save multiple results in a JSON file.
Only configurations and outpits are saved, as they can be used to reconstruct BenchmarkResult.
"""
# simply use config._settings as it defines the BenchmarkConfiguration
results_raw = [(r.config._settings, r._output) for r in results]
with open(file, 'w') as f:
json.dump(results_raw, f)
@classmethod
def load_results_json(cls, file):
"""Load results from a JSON file."""
with open(file, 'r') as f:
results_raw = json.load(f)
return [cls(BenchmarkConfiguration(**settings), output) for (settings, output) in results_raw]
# Results summary ----------------------------------------------------------------------------------
class ResultsSummary:
# value_scaling is a function: value -> (multiplier, prefix)
Fmt = namedtuple('MetricFormatting', ['name', 'unit', 'value_scaling'])
metric_formats = {
'write_bandwidth': Fmt('Write bandwidth', 'bps', lambda value: human_readable(value)),
'read_bandwidth': Fmt('Read bandwidth', 'bps', lambda value: human_readable(value)),
'write_efficiency': Fmt('Write efficiency', '', lambda value: (100, '%')),
'read_efficiency': Fmt('Read efficiency', '', lambda value: (100, '%')),
'write_latency': Fmt('Write latency', 'clk', lambda value: (1, '')),
'read_latency': Fmt('Read latency', 'clk', lambda value: (1, '')),
}
def __init__(self, run_data, plots_dir='plots'):
self.plots_dir = plots_dir
def __init__(self, results):
self.results = results
# gather results into tabular data
column_mappings = {
'name': lambda d: d.config.name,
'sdram_module': lambda d: d.config.sdram_module,
'sdram_data_width': lambda d: d.config.sdram_data_width,
'bist_length': lambda d: getattr(d.config.access_pattern, 'bist_length', None),
'bist_random': lambda d: getattr(d.config.access_pattern, 'bist_random', None),
'pattern_file': lambda d: getattr(d.config.access_pattern, 'pattern_file', None),
'length': lambda d: d.config.length,
'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,
}
columns = {name: [mapping(data) for data in run_data] for name, mapping, in column_mappings.items()}
self.df = df = pd.DataFrame(columns)
def by_metric(self, metric):
"""Returns pairs of value of the given metric and the configuration used for benchmark"""
for result in self.results:
# omit the results that should not be used to calculate given metric
if result.config.bist_length == 1 and metric not in ['read_latency', 'write_latency'] \
or result.config.bist_length != 1 and metric in ['read_latency', 'write_latency']:
continue
value = getattr(result, metric)()
yield value, result.config
# replace None with NaN
df.fillna(value=np.nan, inplace=True)
def print(self):
legend = '(module, datawidth, length, random, result)'
fmt = ' {module:15} {dwidth:2} {length:4} {random:1} {result}'
# compute other metrics based on ticks and configuration parameters
df['clk_period'] = 1 / df['clk_freq']
df['write_bandwidth'] = (8 * df['length']) / (df['generator_ticks'] * df['clk_period'])
df['read_bandwidth'] = (8 * df['length']) / (df['checker_ticks'] * df['clk_period'])
# store formatted lines per metric
metric_lines = defaultdict(list)
for metric, (_, unit, formatter) in self.metric_formats.items():
for value, config in self.by_metric(metric):
mult, prefix = formatter(value)
value_fmt = '{:5.1f} {}{}' if isinstance(value * mult, float) else '{:5d} {}{}'
result = value_fmt.format(value * mult, prefix, unit)
line = fmt.format(module=config.sdram_module,
dwidth=config.sdram_data_width,
length=config.bist_length,
random=int(config.bist_random),
result=result)
metric_lines[metric].append(line)
df['cmd_count'] = df['length'] / (df['ctrl_data_width'] / 8)
df['write_efficiency'] = df['cmd_count'] / df['generator_ticks']
df['read_efficiency'] = df['cmd_count'] / df['checker_ticks']
# find length of the longest line
max_length = max((len(l) for lines in metric_lines.values() for l in lines))
max_length = max(max_length, len(legend) + 2)
width = max_length + 2
df['write_latency'] = df[df['bist_length'] == 1]['generator_ticks']
df['read_latency'] = df[df['bist_length'] == 1]['checker_ticks']
# print the formatted summary
def header(text):
mid = center(text, width - 6, '=')
return center(mid, width, '-')
print(header(' Summary '))
print(center(legend, width))
for metric, lines in metric_lines.items():
print(center(self.metric_formats[metric].name, width))
for line in lines:
print(line)
print(header(''))
# boolean distinction between latency benchmarks and sequence benchmarks,
# as thier results differ significanly
df['is_latency'] = ~pd.isna(df['write_latency'])
assert (df['is_latency'] == ~pd.isna(df['read_latency'])).all(), \
'write_latency and read_latency should both have a value or both be NaN'
def plot(self, output_dir, backend='Agg', theme='default', save_format='png', **savefig_kwargs):
"""Create plots with benchmark results summary
# data formatting for text summary
self.text_formatters = {
'write_bandwidth': bandwidth_fmt,
'read_bandwidth': bandwidth_fmt,
'write_efficiency': efficiency_fmt,
'read_efficiency': efficiency_fmt,
'write_latency': clocks_fmt,
'read_latency': clocks_fmt,
}
Default backend is Agg, which is non-GUI backed and only allows
to save figures as files. If a GUI backed is passed, plt.show()
will be called at the end.
"""
# import locally here to be able to run benchmarks without installing matplotlib
import matplotlib
matplotlib.use(backend)
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.ticker import FuncFormatter, PercentFormatter, ScalarFormatter
plt.style.use(theme)
def bandwidth_formatter_func(value, pos):
mult, prefix = human_readable(value)
return '{:.1f}{}bps'.format(value * mult, prefix)
tick_formatters = {
'write_bandwidth': FuncFormatter(bandwidth_formatter_func),
'read_bandwidth': FuncFormatter(bandwidth_formatter_func),
# data formatting for plot summary
self.plot_xticks_formatters = {
'write_bandwidth': FuncFormatter(lambda value, pos: bandwidth_fmt(value)),
'read_bandwidth': FuncFormatter(lambda value, pos: bandwidth_fmt(value)),
'write_efficiency': PercentFormatter(1.0),
'read_efficiency': PercentFormatter(1.0),
'write_latency': ScalarFormatter(),
'read_latency': ScalarFormatter(),
}
def config_tick_name(config):
return '{}\n{}, {}, {}'.format(config.sdram_module, config.sdram_data_width,
config.bist_length, int(config.bist_random))
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(df)
for metric, (name, unit, _) in self.metric_formats.items():
fig = plt.figure()
axis = plt.gca()
def get_summary(self, mask=None, columns=None, column_formatting=None, sort_kwargs=None):
# work on a copy
df = self.df.copy()
values, configs = zip(*self.by_metric(metric))
ticks = np.arange(len(configs))
if sort_kwargs is not None:
df = df.sort_values(**sort_kwargs)
axis.barh(ticks, values, align='center')
axis.set_yticks(ticks)
axis.set_yticklabels([config_tick_name(c) for c in configs])
axis.invert_yaxis()
axis.xaxis.set_major_formatter(tick_formatters[metric])
axis.xaxis.set_tick_params(rotation=30)
axis.grid(True)
axis.spines['top'].set_visible(False)
axis.spines['right'].set_visible(False)
axis.set_axisbelow(True)
if column_formatting is not None:
for column, mapping in column_formatting.items():
old = '_{}'.format(column)
df[old] = df[column].copy()
df[column] = df[column].map(lambda value: mapping(value) if not pd.isna(value) else value)
# force xmax to 100%
if metric in ['write_efficiency', 'read_efficiency']:
axis.set_xlim(right=1.0)
df = df[mask] if mask is not None else df
df = df[columns] if columns is not None else df
title = self.metric_formats[metric].name
axis.set_title(title, fontsize=12)
return df
plt.tight_layout()
filename = '{}.{}'.format(metric, save_format)
fig.savefig(os.path.join(output_dir, filename), **savefig_kwargs)
def text_summary(self):
for title, df in self.groupped_results():
self.print_df(title, df)
print()
def groupped_results(self, formatted=True):
df = self.df
formatters = self.text_formatters if formatted else {}
common_columns = ['name', 'sdram_module', 'sdram_data_width']
latency_columns = ['write_latency', 'read_latency']
performance_columns = ['write_bandwidth', 'read_bandwidth', 'write_efficiency', 'read_efficiency']
yield 'Latency', self.get_summary(
mask=df['is_latency'] == True,
columns=common_columns + latency_columns,
column_formatting=formatters,
)
# yield 'Any access pattern', self.get_summary(
# mask=(df['is_latency'] == False),
# columns=common_columns + performance_columns + ['length', 'bist_random', 'pattern_file'],
# column_formatting=self.text_formatters,
# **kwargs,
# ),
yield 'Custom access pattern', self.get_summary(
mask=(df['is_latency'] == False) & (~pd.isna(df['pattern_file'])),
columns=common_columns + performance_columns + ['length', 'pattern_file'],
column_formatting=formatters,
),
yield 'Sequential access pattern', self.get_summary(
mask=(df['is_latency'] == False) & (pd.isna(df['pattern_file'])) & (df['bist_random'] == False),
columns=common_columns + performance_columns + ['bist_length'], # could be length
column_formatting=formatters,
),
yield 'Random access pattern', self.get_summary(
mask=(df['is_latency'] == False) & (pd.isna(df['pattern_file'])) & (df['bist_random'] == True),
columns=common_columns + performance_columns + ['bist_length'],
column_formatting=formatters,
),
def plot_summary(self, plots_dir='plots', backend='Agg', theme='default', save_format='png', **savefig_kw):
matplotlib.use(backend)
import matplotlib.pyplot as plt
plt.style.use(theme)
for title, df in self.groupped_results(formatted=False):
for column in self.plot_xticks_formatters.keys():
if column not in df.columns or df[column].empty:
continue
axis = self.plot_df(title, df, column)
# construct path
def path_name(name):
return name.lower().replace(' ', '_')
filename = '{}.{}'.format(path_name(column), save_format)
path = os.path.join(plots_dir, path_name(title), filename)
os.makedirs(os.path.dirname(path), exist_ok=True)
# save figure
axis.get_figure().savefig(path, **savefig_kw)
if backend != 'Agg':
plt.show()
def plot_df(self, title, df, column, 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)
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)
# # force xmax to 100%
# if column in ['write_efficiency', 'read_efficiency']:
# axis.set_xlim(right=1.0)
return axis
# Run ----------------------------------------------------------------------------------------------
def run_benchmark(cmd_args):
# run as separate process, because else we cannot capture all output from verilator
benchmark_script = os.path.join(os.path.dirname(__file__), 'benchmark.py')
command = ['python3', benchmark_script, *cmd_args]
proc = subprocess.run(command, stdout=subprocess.PIPE)
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]
with open(filename, 'w') as f:
json.dump(json_data, f)
@classmethod
def load_json(cls, filename):
with open(filename, 'r') as f:
json_data = json.load(f)
loaded = []
for data in json_data:
config = BenchmarkConfiguration.from_dict(data['config'])
result = BenchmarkResult(data['output'])
loaded.append(cls.RunData(config=config, result=result))
return loaded
def run_python(script, args):
command = ['python3', script, *args]
proc = subprocess.run(command, stdout=subprocess.PIPE, cwd=os.path.dirname(script))
return str(proc.stdout)
def run_benchmarks(configurations):
results = []
for name, config in configurations.items():
cmd_args = config.as_args()
print('{}: {}'.format(name, ' '.join(cmd_args)))
output = run_benchmark(cmd_args)
# exit if checker had any read error
result = BenchmarkResult(config, output)
if result.checker_errors != 0:
print('Error during benchmark "{}": checker_errors = {}'.format(
name, result.checker_errors), file=sys.stderr)
sys.exit(1)
results.append(result)
return results
def run_benchmark(config):
benchmark_script = os.path.join(os.path.dirname(__file__), 'benchmark.py')
# 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
))
return result
def main(argv=None):
@ -308,6 +436,7 @@ 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('--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
@ -315,43 +444,58 @@ def main(argv=None):
to generate different summary without having to rerun benchmarks.""")
args = parser.parse_args(argv)
if not args.results_cache and not _summary:
print('Summary not available and not running with --results-cache - run would not produce any results! Aborting.',
file=sys.stderr)
sys.exit(1)
# load and filter configurations
configurations = BenchmarkConfiguration.load_yaml(args.config)
filters = []
if args.regex:
filters.append(lambda name_value: re.search(args.regex, name_value[0]))
if args.not_regex:
filters.append(lambda name_value: not re.search(args.not_regex, name_value[0]))
if args.names:
filters.append(lambda name_value: name_value[0] in args.names)
for f in filters:
configurations = dict(filter(f, configurations.items()))
cache_exists = args.results_cache and os.path.isfile(args.results_cache)
filters = {
'regex': lambda config: re.search(args.regex, config.name),
'not_regex': lambda config: not re.search(args.not_regex, config.name),
'names': lambda config: config.name in args.names,
}
for arg, f in filters.items():
if getattr(args, arg):
configurations = filter(f, configurations)
configurations = list(configurations)
# load outputs from cache if it exsits
cache_exists = args.results_cache and os.path.isfile(args.results_cache)
if args.results_cache and cache_exists:
cached_results = BenchmarkResult.load_results_json(args.results_cache)
cache = RunCache.load_json(args.results_cache)
# take only those that match configurations
results = [r for r in cached_results if r.config in configurations.values()]
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
results = run_benchmarks(configurations)
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
# store outputs in cache
if args.results_cache and not cache_exists:
BenchmarkResult.dump_results_json(results, args.results_cache)
cache = RunCache(run_data)
cache.dump_json(args.results_cache)
# display the summary
summary = ResultsSummary(results)
summary.print()
if args.plot:
if not os.path.isdir(args.plot_output_dir):
os.makedirs(args.plot_output_dir)
summary.plot(args.plot_output_dir,
backend=args.plot_backend,
theme=args.plot_theme,
save_format=args.plot_format,
transparent=args.plot_transparent)
# display summary
if _summary:
summary = ResultsSummary(run_data)
summary.text_summary()
if args.plot:
summary.plot_summary(
plots_dir=args.plot_output_dir,
backend=args.plot_backend,
theme=args.plot_theme,
save_format=args.plot_format,
transparent=args.plot_transparent,
)
if __name__ == "__main__":