From 03e7cbafc2ff4f2848c42ce4a3dc424e040df66d Mon Sep 17 00:00:00 2001
From: Tobi-Alonso <tobi.alonso@gmail.com>
Date: Wed, 24 Jun 2020 08:49:57 +0100
Subject: [PATCH] [FPGADataflow] Fix InferChannelwiseLinearLayer. Correct
 output datatype inference from idt and parameters. Add parameter type
 inference. Check parameter shape. Check if retrived intializer is None
 (dynamic input)

---
 .../fpgadataflow/convert_to_hls_layers.py     | 78 +++++++++++++------
 1 file changed, 54 insertions(+), 24 deletions(-)

diff --git a/src/finn/transformation/fpgadataflow/convert_to_hls_layers.py b/src/finn/transformation/fpgadataflow/convert_to_hls_layers.py
index c34a7ef23..c480a6001 100644
--- a/src/finn/transformation/fpgadataflow/convert_to_hls_layers.py
+++ b/src/finn/transformation/fpgadataflow/convert_to_hls_layers.py
@@ -496,6 +496,22 @@ class InferThresholdingLayer(Transformation):
 class InferChannelwiseLinearLayer(Transformation):
     """Convert any channel-wise Add/Mul into a HLS layer."""
 
+    def get_smallest_possible(self, vals):
+        """Returns smallest (fewest bits) possible DataType that can represent
+        value. Prefers unsigned integers where possible."""
+        vals = np.array(vals)
+        for v in vals:
+            assert int(v) == v, "Error float value"
+
+        for k in DataType.__members__:
+            dt = DataType[k]
+            if dt in [DataType.BIPOLAR, DataType.BIPOLAR]:
+                # not currently supported
+                continue
+
+            if (dt.min() <= vals).all() and (vals <= dt.max()).all():
+                return dt
+
     def apply(self, model):
         graph = model.graph
         node_ind = 0
@@ -503,45 +519,51 @@ class InferChannelwiseLinearLayer(Transformation):
         for node in graph.node:
             node_ind += 1
             if node.op_type == "Add" or node.op_type == "Mul":
+                # assuming input[0] is dynamic
                 ll_input = node.input[0]
                 ll_output = node.output[0]
                 ll_in_shape = model.get_tensor_shape(ll_input)
-                # check that:
-                # input 1 is an initializer
-                # with shape (ch, 1)
-                # and initializer is integers
+
+                # check if input 1 has an initializer
                 ll_const = node.input[1]
                 if ll_const is not None:
                     ll_cinit = model.get_initializer(ll_const)
+                    if ll_cinit is None:
+                        # input 1 is also dynamic
+                        continue
                 else:
                     continue
 
-                ll_cinit_shape = list(ll_cinit.shape)
-                # get number of channels from input
+                # get number of channels and channel index from input
                 ll_in_layout = model.get_tensor_layout(ll_input)
                 if ll_in_layout == DataLayout.NHWC or ll_in_layout == DataLayout.NC:
+                    ch_index = -1
                     ch = ll_in_shape[-1]
                 elif ll_in_layout == DataLayout.NCHW:
+                    ch_index = 1
                     ch = ll_in_shape[1]
                 else:
                     continue
 
-                # check if the shape of initializer is compatible with
-                # number of channels, e.g. (ch,1) or (ch)
-                # TODO: verify plausible shapes
-                if np.prod(ll_cinit_shape) != ch:
+                # check if the shape of initializer is compatible
+                ll_cinit_shape = list(ll_cinit.shape)
+                if np.prod(ll_cinit_shape) == 1:
+                    # TODO broadcast
+                    pass
+
+                if np.prod(ll_cinit_shape) != ch or ll_cinit_shape[ch_index] != ch:
+                    # parameter shape not compatible with Channelwise_batch
                     continue
 
                 # check initializer contains integers as floats
                 if not (ll_cinit.astype(np.int32) == ll_cinit).all():
                     continue
-
                 # all initializer conditions are met
+
                 # check inputs
                 idt = model.get_tensor_datatype(ll_input)
-
-                # skip conversion for layers with float input
                 if not idt.is_integer():
+                    # skip conversion for layers with float input
                     continue
 
                 # check layout of inputs/outputs, and convert if needed
@@ -559,25 +581,32 @@ class InferChannelwiseLinearLayer(Transformation):
                     ll_output = nchw_to_nhwc(ll_output, model, node_ind, reverse=True)
                     node_ind += 1
 
-                # create node with no parallelization first
-                pe = 1
-                assert ch % pe == 0, "Requirement IFC divisable by PE is violated."
+                # get parameter data type
+                param_min = min(ll_cinit.flatten())
+                param_max = max(ll_cinit.flatten())
+                pdt = self.get_smallest_possible([param_min, param_max])
 
                 # set function and determine output data type
                 if node.op_type == "Add":
                     func = "add"
-                    if idt.signed():
-                        odt = DataType.get_smallest_possible(2 * idt.min())
-                    else:
-                        odt = DataType.get_smallest_possible(2 * idt.max())
+                    out_min = idt.min() + param_min
+                    out_max = idt.max() + param_max
+                    odt = self.get_smallest_possible([out_min, out_max])
                 elif node.op_type == "Mul":
                     func = "mul"
-                    if idt.signed():
-                        odt = DataType.get_smallest_possible(abs(idt.min()) * idt.min())
-                    else:
-                        odt = DataType.get_smallest_possible(idt.max() * idt.max())
+                    possible_limits = []
+                    possible_limits += [idt.min() * param_min]
+                    possible_limits += [idt.min() * param_max]
+                    possible_limits += [idt.max() * param_min]
+                    possible_limits += [idt.max() * param_max]
+                    odt = self.get_smallest_possible(possible_limits)
+
                 model.set_initializer(ll_const, ll_cinit.reshape(ch))
                 model.set_tensor_datatype(ll_output, odt)
+
+                # create node with no parallelization first
+                pe = 1
+                assert ch % pe == 0, "Requirement IFC divisable by PE is violated."
                 # create and insert node
                 new_node = helper.make_node(
                     "ChannelwiseOp_Batch",
@@ -589,6 +618,7 @@ class InferChannelwiseLinearLayer(Transformation):
                     NumChannels=ch,
                     PE=pe,
                     inputDataType=idt.name,
+                    paramDataType=pdt.name,
                     outputDataType=odt.name,
                     numInputVectors=list(ll_in_shape[:-1]),
                 )
-- 
GitLab