diff --git a/conifer/__init__.py b/conifer/__init__.py index efefae72..31a79691 100644 --- a/conifer/__init__.py +++ b/conifer/__init__.py @@ -9,4 +9,5 @@ from conifer import converters from conifer import backends -from conifer.model import Model \ No newline at end of file +from conifer.model import Model +from conifer import utils diff --git a/conifer/backends/vhdl/__init__.py b/conifer/backends/vhdl/__init__.py index 4b198818..8307a926 100644 --- a/conifer/backends/vhdl/__init__.py +++ b/conifer/backends/vhdl/__init__.py @@ -1,2 +1,3 @@ -from .writer import write, auto_config, sim_compile, decision_function, build, Simulators -simulator = Simulators.xsim +from conifer.backends.vhdl.writer import write, auto_config, sim_compile, decision_function, build +from conifer.backends.vhdl.simulators import Modelsim, GHDL, Xsim +simulator = Xsim diff --git a/conifer/backends/vhdl/simulators.py b/conifer/backends/vhdl/simulators.py new file mode 100644 index 00000000..3a222dbc --- /dev/null +++ b/conifer/backends/vhdl/simulators.py @@ -0,0 +1,99 @@ +import os +import logging +logger = logging.getLogger(__name__) + + +def _compile_sim(simulator, odir): + logger.info(f'Compiling simulation for {simulator.__name__.lower()} simulator') + logger.debug(f'Compiling simulation with command "{simulator._compile_cmd}"') + cwd = os.getcwd() + os.chdir(odir) + success = os.system(simulator._compile_cmd) + os.chdir(cwd) + if(success > 0): + logger.error(f"'sim_compile' failed, check {simulator.__name__.lower()}_compile.log") + return success == 0 + +def _run_sim(simulator, odir): + logger.info(f'Running simulation for {simulator.__name__.lower()} simulator') + logger.debug(f'Running simulation with command "{simulator._run_cmd}"') + cwd = os.getcwd() + os.chdir(odir) + success = os.system(simulator._run_cmd) + os.chdir(cwd) + if(success > 0): + logger.error(f"'sim_compile' failed, check {simulator.__name__.lower()}.log") + return success == 0 + +class Modelsim: + _compile_cmd = 'sh modelsim_compile.sh > modelsim_compile.log' + _run_cmd = 'vsim -c -do "vsim -L BDT -L xil_defaultlib xil_defaultlib.testbench; run -all; quit -f" > vsim.log' + + def write_scripts(outputdir, filedir, n_classes): + f = open(os.path.join(filedir,'./scripts/modelsim_compile.sh'),'r') + fout = open(f'{outputdir}/modelsim_compile.sh','w') + for line in f.readlines(): + if 'insert arrays' in line: + for i in range(n_classes): + newline = f'vcom -2008 -work BDT ./firmware/Arrays{i}.vhd\n' + fout.write(newline) + else: + fout.write(line) + f.close() + fout.close() + + f = open(f'{outputdir}/test.tcl', 'w') + f.write('vsim -L BDT -L xil_defaultlib xil_defaultlib.testbench\n') + f.write('run 100 ns\n') + f.write('quit -f\n') + f.close() + + def compile(odir): + return _compile_sim(Modelsim, odir) + + def run_sim(odir): + return _run_sim(Modelsim, odir) + +class GHDL: + _compile_cmd = 'sh ghdl_compile.sh > ghdl_compile.log' + _run_cmd = 'ghdl -r --std=08 --work=xil_defaultlib testbench > ghdl.log' + def write_scripts(outputdir, filedir, n_classes): + f = open(os.path.join(filedir, './scripts/ghdl_compile.sh'), 'r') + fout = open(f'{outputdir}/ghdl_compile.sh', 'w') + for line in f.readlines(): + if 'insert arrays' in line: + for i in range(n_classes): + newline = f'ghdl -a --std=08 --work=BDT ./firmware/Arrays{i}.vhd\n' + fout.write(newline) + else: + fout.write(line) + f.close() + fout.close() + + def compile(odir): + return _compile_sim(GHDL, odir) + + def run_sim(odir): + return _run_sim(GHDL, odir) + +class Xsim: + _compile_cmd = 'sh xsim_compile.sh > xsim_compile.log' + _run_cmd = 'xsim -R bdt_tb > xsim.log' + def write_scripts(outputdir, filedir, n_classes): + f = open(os.path.join(filedir, './scripts/xsim_compile.sh'), 'r') + fout = open(f'{outputdir}/xsim_compile.sh', 'w') + for line in f.readlines(): + if 'insert arrays' in line: + for i in range(n_classes): + newline = f'xvhdl -2008 -work BDT ./firmware/Arrays{i}.vhd\n' + fout.write(newline) + else: + fout.write(line) + f.close() + fout.close() + + def compile(odir): + return _compile_sim(Xsim, odir) + + def run_sim(odir): + return _run_sim(Xsim, odir) diff --git a/conifer/backends/vhdl/writer.py b/conifer/backends/vhdl/writer.py index 491ebbae..b537c91f 100644 --- a/conifer/backends/vhdl/writer.py +++ b/conifer/backends/vhdl/writer.py @@ -2,16 +2,12 @@ from shutil import copyfile import numpy as np from enum import Enum +from conifer.utils import FixedPointConverter import copy import datetime import logging logger = logging.getLogger(__name__) -class Simulators(Enum): - modelsim = 0 - xsim = 1 - ghdl = 2 - def write(model): model.save() @@ -70,6 +66,10 @@ def write(model): dtype_frac = dtype_n - dtype_int # number of fractional bits mult = 2**dtype_frac + fp = FixedPointConverter(cfg['Precision']) + # TODO this should be attached to the model differently + model._fp_converter = fp + # binary classification only uses one set of trees n_classes = 1 if ensembleDict['n_classes'] == 2 else ensembleDict['n_classes'] @@ -79,15 +79,12 @@ def write(model): for i in range(n_classes): fout[i].write(array_header_text) fout[i].write('package Arrays{} is\n\n'.format(i)) - fout[i].write(' constant initPredict : ty := to_ty({});\n'.format(int(np.round(ensembleDict['init_predict'][i] * mult)))) - #fout[i].write(' constant initPredict : ty := to_ty({});\n'.format(int(np.round(ensembleDict['init_predict'][i] * mult)))) - + fout[i].write(' constant initPredict : ty := to_ty({});\n'.format(fp.to_int(np.float64(ensembleDict['init_predict'][i])))) # Loop over fields (childrenLeft, childrenRight, threshold...) for field in ensembleDict['trees'][0][0].keys(): # Write the type for this field to each classes' file for iclass in range(n_classes): - #dtype = 'txArray2D' if field == 'threshold' else 'tyArray2D' if field == 'value' else 'intArray2D' fieldName = field # The threshold and value arrays are declared as integers, then cast # So need a separate constant @@ -95,7 +92,7 @@ def write(model): fieldName += '_int' # Convert the floating point values to integers for ii, trees in enumerate(ensembleDict['trees']): - ensembleDict['trees'][ii][iclass][field] = np.round(np.array(ensembleDict['trees'][ii][iclass][field]) * mult).astype('int') + ensembleDict['trees'][ii][iclass][field] = np.array([fp.to_int(x) for x in ensembleDict['trees'][ii][iclass][field]]) nElem = 'nLeaves' if field == 'iLeaf' else 'nNodes' fout[iclass].write(' constant {} : intArray2D{}(0 to nTrees - 1) := ('.format(fieldName, nElem)) # Loop over the trees within the class @@ -112,7 +109,8 @@ def write(model): fout[i].write('end Arrays{};'.format(i)) fout[i].close() - write_sim_scripts(cfg, filedir, n_classes) + from conifer.backends.vhdl import simulator + simulator.write_scripts(cfg['OutputDir'], filedir, n_classes) f = open('{}/SimulationInput.txt'.format(cfg['OutputDir']), 'w') f.write(' '.join(map(str, [0] * ensembleDict['n_features']))) @@ -188,67 +186,24 @@ def auto_config(): def sim_compile(model): from conifer.backends.vhdl import simulator - config = copy.deepcopy(model.config) - xsim_cmd = 'sh xsim_compile.sh > xsim_compile.log' - msim_cmd = 'sh modelsim_compile.sh > modelsim_compile.log' - ghdl_cmd = 'sh ghdl_compile.sh > ghdl_compile.log' - cmdmap = {Simulators.modelsim : msim_cmd, - Simulators.xsim : xsim_cmd, - Simulators.ghdl : ghdl_cmd} - cmd = cmdmap[simulator] - logger.info(f'Compiling simulation for {simulator} simulator') - logger.debug(f'Compiling simulation with command "{cmd}"') - cwd = os.getcwd() - os.chdir(config['OutputDir']) - success = os.system(cmd) - os.chdir(cwd) - if(success > 0): - logger.error("'sim_compile' failed, check {}_compile.log".format(simulator.name)) - return + return simulator.compile(model.config['OutputDir']) def decision_function(X, model, trees=False): from conifer.backends.vhdl import simulator config = copy.deepcopy(model.config) - msim_cmd = 'vsim -c -do "vsim -L BDT -L xil_defaultlib xil_defaultlib.testbench; run -all; quit -f" > vsim.log' - xsim_cmd = 'xsim -R bdt_tb > xsim.log' - ghdl_cmd = 'ghdl -r --std=08 --work=xil_defaultlib testbench > ghdl.log' - cmdmap = {Simulators.modelsim : msim_cmd, - Simulators.xsim : xsim_cmd, - Simulators.ghdl : ghdl_cmd} - cmd = cmdmap[simulator] - msim_log = 'vsim.log' - xsim_log = 'xsim.log' - ghdl_log = 'ghdl.log' - logmap = {Simulators.modelsim : msim_log, - Simulators.xsim : xsim_log, - Simulators.ghdl : ghdl_log} - logfile = logmap[simulator] - - logger.info(f'Running simulation for {simulator} simulator') - dtype = config['Precision'] - if not 'ap_fixed' in dtype: - logger.error("Only ap_fixed is currently supported, exiting") - return - dtype = dtype.replace('ap_fixed<', '').replace('>', '') - dtype_n = int(dtype.split(',')[0].strip()) # total number of bits - dtype_int = int(dtype.split(',')[1].strip()) # number of integer bits - dtype_frac = dtype_n - dtype_int # number of fractional bits - mult = 2**dtype_frac - Xint = (X * mult).astype('int') - logger.debug(f'Converting X ({X.dtype}), to integers with scale factor {mult} from {config["Precision"]}') + Xint = np.array([model._fp_converter.to_int(x) for x in X.ravel()]).reshape(X.shape) np.savetxt('{}/SimulationInput.txt'.format(config['OutputDir']), Xint, delimiter=' ', fmt='%d') - cwd = os.getcwd() - os.chdir(config['OutputDir']) - logger.debug(f'Running simulation with command "{cmd}"') - success = os.system(cmd) - os.chdir(cwd) - if(success > 0): - logger.error("'decision_function' failed, see {}.log".format(logfile)) - return - y = np.loadtxt('{}/SimulationOutput.txt'.format(config['OutputDir'])) * 1. / mult + success = simulator.run_sim(config['OutputDir']) + if not success: + return + y = np.loadtxt('{}/SimulationOutput.txt'.format(config['OutputDir'])).astype(np.int32) + y = np.array([model._fp_converter.from_int(yi) for yi in y.ravel()]).reshape(y.shape) + if np.ndim(y) == 1: + y = np.expand_dims(y, 1) + if trees: logger.warn("Individual tree output (trees=True) not yet implemented for this backend") return y @@ -268,55 +223,4 @@ def build(config, **kwargs): logger.error("build failed, check build.log") return False return True - -def write_sim_scripts(cfg, filedir, n_classes): - from conifer.backends.vhdl import simulator - fmap = {Simulators.modelsim : write_modelsim_scripts, - Simulators.xsim : write_xsim_scripts, - Simulators.ghdl : write_ghdl_scripts,} - fmap[simulator](cfg, filedir, n_classes) - -def write_modelsim_scripts(cfg, filedir, n_classes): - f = open(os.path.join(filedir,'./scripts/modelsim_compile.sh'),'r') - fout = open('{}/modelsim_compile.sh'.format(cfg['OutputDir']),'w') - for line in f.readlines(): - if 'insert arrays' in line: - for i in range(n_classes): - newline = 'vcom -2008 -work BDT ./firmware/Arrays{}.vhd\n'.format(i) - fout.write(newline) - else: - fout.write(line) - f.close() - fout.close() - - f = open('{}/test.tcl'.format(cfg['OutputDir']), 'w') - f.write('vsim -L BDT -L xil_defaultlib xil_defaultlib.testbench\n') - f.write('run 100 ns\n') - f.write('quit -f\n') - f.close() - -def write_xsim_scripts(cfg, filedir, n_classes): - f = open(os.path.join(filedir, './scripts/xsim_compile.sh'), 'r') - fout = open('{}/xsim_compile.sh'.format(cfg['OutputDir']), 'w') - for line in f.readlines(): - if 'insert arrays' in line: - for i in range(n_classes): - newline = 'xvhdl -2008 -work BDT ./firmware/Arrays{}.vhd\n'.format(i) - fout.write(newline) - else: - fout.write(line) - f.close() - fout.close() - -def write_ghdl_scripts(cfg, filedir, n_classes): - f = open(os.path.join(filedir, './scripts/ghdl_compile.sh'), 'r') - fout = open('{}/ghdl_compile.sh'.format(cfg['OutputDir']), 'w') - for line in f.readlines(): - if 'insert arrays' in line: - for i in range(n_classes): - newline = 'ghdl -a --std=08 --work=BDT ./firmware/Arrays{}.vhd\n'.format(i) - fout.write(newline) - else: - fout.write(line) - f.close() - fout.close() \ No newline at end of file + \ No newline at end of file diff --git a/conifer/utils/__init__.py b/conifer/utils/__init__.py index 782a1948..18b21bf6 100644 --- a/conifer/utils/__init__.py +++ b/conifer/utils/__init__.py @@ -1 +1,2 @@ -from conifer.utils.misc import _ap_include, _json_include \ No newline at end of file +from .fixed_point import FixedPointConverter +from conifer.utils.misc import _ap_include, _json_include diff --git a/conifer/utils/fixed_point.py b/conifer/utils/fixed_point.py new file mode 100644 index 00000000..1df67445 --- /dev/null +++ b/conifer/utils/fixed_point.py @@ -0,0 +1,64 @@ +import os +import logging +logger = logging.getLogger(__name__) + +class FixedPointConverter: + ''' + A Python wrapper around ap_fixed types to easily emulate the correct number representations + ''' + + def __init__(self, type_string): + ''' + Construct the FixedPointConverter. Compiles the c++ library to use for conversions + args: + type_string : string for the ap_ type, e.g. ap_fixed<16,6,AP_RND,AP_SAT> + ''' + logger.info(f'Constructing converter for {type_string}') + self.type_string = type_string + self.sani_type = type_string.replace('<','_').replace('>','').replace(',','_') + filedir = os.path.dirname(os.path.abspath(__file__)) + cpp_filedir = f"./.fp_converter_{self.sani_type}" + cpp_filename = cpp_filedir + f'/{self.sani_type}.cpp' + os.makedirs(cpp_filedir, exist_ok=True) + + fin = open(f'{filedir}/fixed_point_conversions.cpp', 'r') + fout = open(cpp_filename, 'w') + for line in fin.readlines(): + newline = line + if '// conifer insert typedef' in line: + newline = f"typedef {type_string} T;\n" + fout.write(newline) + fin.close() + fout.close() + + curr_dir = os.getcwd() + os.chdir(cpp_filedir) + cmd = f"g++ -O3 -shared -std=c++11 -fPIC $(python3 -m pybind11 --includes) -I/cvmfs/cms.cern.ch/slc7_amd64_gcc900/external/hls/2019.08/include/ {self.sani_type}.cpp -o {self.sani_type}.so" + logger.debug(f'Compiling with command {cmd}') + try: + ret_val = os.system(cmd) + if ret_val != 0: + raise Exception(f'Failed to compile FixedPointConverter {self.sani_type}.cpp') + finally: + os.chdir(curr_dir) + + os.chdir(cpp_filedir) + logger.debug(f'Importing compiled module {self.sani_type}.so') + try: + import importlib.util + spec = importlib.util.spec_from_file_location('fixed_point', f'{self.sani_type}.so') + self.lib = importlib.util.module_from_spec(spec) + spec.loader.exec_module(self.lib) + except ImportError: + os.chdir(curr_dir) + raise Exception("Can't import pybind11 bridge, is it compiled?") + os.chdir(curr_dir) + + def to_int(self, x): + return self.lib.to_int(x) + + def to_double(self, x): + return self.lib.to_double(x) + + def from_int(self, x): + return self.lib.from_int(x) \ No newline at end of file diff --git a/conifer/utils/fixed_point_conversions.cpp b/conifer/utils/fixed_point_conversions.cpp new file mode 100644 index 00000000..f9e7419b --- /dev/null +++ b/conifer/utils/fixed_point_conversions.cpp @@ -0,0 +1,28 @@ +#include "ap_fixed.h" +#include + +// conifer insert typedef + +int to_int(double x){ + T y = (T) x; + return y.V; +} + +double to_double(double x){ + T y = (T) x; + return y.to_double(); +} + +double from_int(int x){ + T y; + y.V = x; + return y.to_double(); +} + +namespace py = pybind11; +PYBIND11_MODULE(fixed_point, m) { + m.doc() = "fixed point conversion"; + m.def("to_int", &to_int, "Get the integer representation of the ap_fixed"); + m.def("to_double", &to_double, "Get the double representation of the ap_fixed"); + m.def("from_int", &from_int, "Set the underlying bits of the ap_fixed from int, return the double"); +} \ No newline at end of file