diff --git a/requirements.txt b/requirements.txt index a2247fea500d67f6dfb762f0214ddbfb921d1b00..daefe6b51c395e2707d63246ccba62d40ad34fd7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +bitstring docrep future numpy diff --git a/src/finn/backend/fpgadataflow/utils.py b/src/finn/backend/fpgadataflow/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..b0268237739476e3a8fb99377fda9f76a6a8c5c2 --- /dev/null +++ b/src/finn/backend/fpgadataflow/utils.py @@ -0,0 +1,51 @@ +import sys + +import numpy as np + +from finn.core.datatype import DataType +from finn.core.utils import pack_innermost_dim_as_hex_string + + +def numpy_to_hls_code(ndarray, dtype, hls_var_name, pack_innermost_dim=True): + """Return C++ code representation of a numpy ndarray with FINN DataType + dtype, using hls_var_name as the resulting C++ variable name. If + pack_innermost_dim is specified, the innermost dimension of the ndarray + will be packed into a hex string using array2hexstring. + """ + hls_dtype = dtype.get_hls_datatype_str() + if type(ndarray) != np.ndarray or ndarray.dtype != np.float32: + # try to convert to a float numpy array (container dtype is float) + ndarray = np.asarray(ndarray, dtype=np.float32) + if pack_innermost_dim: + idimlen = ndarray.shape[-1] + idimbits = idimlen * dtype.bitwidth() + ndarray = pack_innermost_dim_as_hex_string(ndarray, dtype, idimbits) + hls_dtype = "ap_uint<%d>" % idimbits + ndims = ndarray.ndim + # add type string and variable name + # e.g. "const ap_uint<64>" "weightMem0" + ret = "%s %s" % (hls_dtype, hls_var_name) + # add dimensions + for d in range(ndims): + ret += "[%d]" % ndarray.shape[d] + orig_printops = np.get_printoptions() + np.set_printoptions(threshold=sys.maxsize) + + # define a function to convert a single element into a C++ init string + # a single element can be a hex string if we are using packing + def elem2str(x): + if type(x) == str or type(x) == np.str_ or type(x) == np.str: + return '%s("%s", 16)' % (hls_dtype, x) + elif type(x) == np.float32: + if dtype == DataType.FLOAT32: + return str(x) + else: + return str(int(x)) + else: + raise Exception("Unsupported type for numpy_to_hls_code") + + strarr = np.array2string(ndarray, separator=", ", formatter={"all": elem2str}) + np.set_printoptions(**orig_printops) + strarr = strarr.replace("[", "{").replace("]", "}") + ret = ret + " = \n" + strarr + ";" + return ret diff --git a/src/finn/core/datatype.py b/src/finn/core/datatype.py index 42a366aafcc002a433d0e03c03ef6a9bed6adede..34eb5abc247c2d44d33a6a57406fba6887316b98 100644 --- a/src/finn/core/datatype.py +++ b/src/finn/core/datatype.py @@ -130,3 +130,13 @@ class DataType(Enum): """Return whether this DataType represents integer values only.""" # only FLOAT32 is noninteger for now return self != DataType.FLOAT32 + + def get_hls_datatype_str(self): + """Return the corresponding Vivado HLS datatype name.""" + if self.is_integer(): + if self.signed(): + return "ap_int<%d>" % self.bitwidth() + else: + return "ap_uint<%d>" % self.bitwidth() + else: + return "float" diff --git a/src/finn/core/utils.py b/src/finn/core/utils.py index 00dad9655036e496870d60adaa358e2156859599..19c51f65990902898683a492811b83c495d73b38 100644 --- a/src/finn/core/utils.py +++ b/src/finn/core/utils.py @@ -3,6 +3,9 @@ import string import numpy as np import onnx +from bitstring import BitArray + +from finn.core.datatype import DataType def valueinfo_to_tensor(vi): @@ -35,3 +38,72 @@ def random_string(stringLength=6): """Randomly generate a string of letters and digits.""" lettersAndDigits = string.ascii_letters + string.digits return "".join(random.choice(lettersAndDigits) for i in range(stringLength)) + + +def array2hexstring(array, dtype, pad_to_nbits): + """ + Pack given one-dimensional NumPy array with FINN DataType dtype into a hex + string. + Any BIPOLAR values will be converted to a single bit with a 0 representing + -1. + pad_to_nbits is used to prepend leading zeros to ensure packed strings of + fixed width. The minimum value for pad_to_nbits is 4, since a single hex + digit is four bits. + + Examples: + array2hexstring([1, 1, 1, 0], DataType.BINARY, 4) = "e" + array2hexstring([1, 1, 1, 0], DataType.BINARY, 8) = "0e" + """ + if pad_to_nbits < 4: + pad_to_nbits = 4 + # ensure input is a numpy array with float values + if type(array) != np.ndarray or array.dtype != np.float32: + # try to convert to a float numpy array (container dtype is float) + array = np.asarray(array, dtype=np.float32) + # ensure one-dimensional array to pack + assert array.ndim == 1 + if dtype == DataType.BIPOLAR: + # convert bipolar values to binary + array = (array + 1) / 2 + dtype = DataType.BINARY + lineval = BitArray(length=0) + bw = dtype.bitwidth() + for val in array: + # ensure that this value is permitted by chosen dtype + assert dtype.allowed(val) + if dtype.is_integer(): + if dtype.signed(): + lineval.append(BitArray(int=int(val), length=bw)) + else: + lineval.append(BitArray(uint=int(val), length=bw)) + else: + lineval.append(BitArray(float=val, length=bw)) + if pad_to_nbits >= lineval.len: + # extend to the desired output width (a minimum of 4 bits) + lineval.prepend(BitArray(length=pad_to_nbits - lineval.len)) + else: + raise Exception("Number of bits is greater than pad_to_nbits") + # represent as hex + return lineval.hex + + +def pack_innermost_dim_as_hex_string(ndarray, dtype, pad_to_nbits): + """Pack the innermost dimension of the given numpy ndarray into hex + strings using array2hexstring. Examples: + + A = [[1, 1, 1, 0], [0, 1, 1, 0]] + eA = ["0e", "06"] + pack_innermost_dim_as_hex_string(A, DataType.BINARY, 8) == eA + B = [[[3, 3], [3, 3]], [[1, 3], [3, 1]]] + eB = [[ "0f", "0f"], ["07", "0d"]] + pack_innermost_dim_as_hex_string(B, DataType.UINT2, 8) == eB + """ + + if type(ndarray) != np.ndarray or ndarray.dtype != np.float32: + # try to convert to a float numpy array (container dtype is float) + ndarray = np.asarray(ndarray, dtype=np.float32) + + def fun(x): + return array2hexstring(x, dtype, pad_to_nbits) + + return np.apply_along_axis(fun, ndarray.ndim - 1, ndarray) diff --git a/tests/test_npy2hls.py b/tests/test_npy2hls.py new file mode 100644 index 0000000000000000000000000000000000000000..ea54d900a8e3a06b53cb4074072b15747c6c598c --- /dev/null +++ b/tests/test_npy2hls.py @@ -0,0 +1,43 @@ +import numpy as np + +from finn.backend.fpgadataflow.utils import numpy_to_hls_code +from finn.core.datatype import DataType +from finn.core.utils import array2hexstring, pack_innermost_dim_as_hex_string + + +def test_array2hexstring(): + assert array2hexstring([1, 1, 1, 0], DataType.BINARY, 4) == "e" + assert array2hexstring([1, 1, 1, 0], DataType.BINARY, 8) == "0e" + assert array2hexstring([1, 1, 1, -1], DataType.BIPOLAR, 8) == "0e" + assert array2hexstring([3, 3, 3, 3], DataType.UINT2, 8) == "ff" + assert array2hexstring([1, 3, 3, 1], DataType.UINT2, 8) == "7d" + assert array2hexstring([1, -1, 1, -1], DataType.INT2, 8) == "77" + assert array2hexstring([1, 1, 1, -1], DataType.INT4, 16) == "111f" + assert array2hexstring([-1], DataType.FLOAT32, 32) == "bf800000" + assert array2hexstring([17.125], DataType.FLOAT32, 32) == "41890000" + + +def test_pack_innermost_dim_as_hex_string(): + A = [[1, 1, 1, 0], [0, 1, 1, 0]] + eA = np.asarray(["0e", "06"]) + assert (pack_innermost_dim_as_hex_string(A, DataType.BINARY, 8) == eA).all() + B = [[[3, 3], [3, 3]], [[1, 3], [3, 1]]] + eB = np.asarray([["0f", "0f"], ["07", "0d"]]) + assert (pack_innermost_dim_as_hex_string(B, DataType.UINT2, 8) == eB).all() + + +def test_numpy_to_hls_code(): + def remove_all_whitespace(s): + return "".join(s.split()) + + A = [[1, 1, 1, 0], [0, 1, 1, 0]] + ret = numpy_to_hls_code(A, DataType.BINARY, "test", True) + eA = """ap_uint<4> test[2] = + {ap_uint<4>("e", 16), ap_uint<4>("6", 16)};""" + assert remove_all_whitespace(ret) == remove_all_whitespace(eA) + B = [[[3, 3], [3, 3]], [[1, 3], [3, 1]]] + ret = numpy_to_hls_code(B, DataType.UINT2, "test", True) + eB = """ap_uint<4> test[2][2] = + {{ap_uint<4>("f", 16), ap_uint<4>("f", 16)}, + {ap_uint<4>("7", 16), ap_uint<4>("d", 16)}};""" + assert remove_all_whitespace(ret) == remove_all_whitespace(eB)