Skip to content

Commit

Permalink
Merge pull request #32 from thesps/fp_emulation
Browse files Browse the repository at this point in the history
Backend agreement
  • Loading branch information
thesps committed Nov 25, 2022
2 parents 01743d7 + 2455c2b commit 6d6d721
Show file tree
Hide file tree
Showing 7 changed files with 218 additions and 120 deletions.
3 changes: 2 additions & 1 deletion conifer/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@

from conifer import converters
from conifer import backends
from conifer.model import Model
from conifer.model import Model
from conifer import utils
5 changes: 3 additions & 2 deletions conifer/backends/vhdl/__init__.py
Original file line number Diff line number Diff line change
@@ -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
99 changes: 99 additions & 0 deletions conifer/backends/vhdl/simulators.py
Original file line number Diff line number Diff line change
@@ -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)
136 changes: 20 additions & 116 deletions conifer/backends/vhdl/writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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']

Expand All @@ -79,23 +79,20 @@ 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
if field == 'threshold' or field == 'value':
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
Expand All @@ -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'])))
Expand Down Expand Up @@ -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
Expand All @@ -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()

3 changes: 2 additions & 1 deletion conifer/utils/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
from conifer.utils.misc import _ap_include, _json_include
from .fixed_point import FixedPointConverter
from conifer.utils.misc import _ap_include, _json_include
64 changes: 64 additions & 0 deletions conifer/utils/fixed_point.py
Original file line number Diff line number Diff line change
@@ -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)
Loading

0 comments on commit 6d6d721

Please sign in to comment.