diff --git a/docker/Dockerfile.finn_ci b/docker/Dockerfile.finn_ci
index 4518fe564670364b1ad464099d880c958e780c24..b9c71df58b1869cec7a49d2e5f4dd19d09949097 100644
--- a/docker/Dockerfile.finn_ci
+++ b/docker/Dockerfile.finn_ci
@@ -47,6 +47,8 @@ RUN bash xrtdeps.sh
 RUN rm xrtdeps.sh
 
 # cloning dependency repos
+# finn-base
+RUN git clone https://github.com/maltanar/finn-base.git /workspace/finn-base
 # Brevitas
 RUN git clone https://github.com/Xilinx/brevitas.git /workspace/brevitas
 # CNPY
diff --git a/docker/Dockerfile.finn_dev b/docker/Dockerfile.finn_dev
index 4be975442665c680ccbb8aefd1d0bb8d07b81cc9..fe7f45b461f517ae736b11ab7871b56ec8f9061a 100644
--- a/docker/Dockerfile.finn_dev
+++ b/docker/Dockerfile.finn_dev
@@ -71,6 +71,8 @@ RUN chown -R $UNAME:$GNAME /home/$UNAME
 USER $UNAME
 
 # cloning dependency repos (as user)
+# finn-base
+RUN git clone https://github.com/maltanar/finn-base.git /workspace/finn-base
 # Brevitas
 RUN git clone https://github.com/Xilinx/brevitas.git /workspace/brevitas
 # CNPY
diff --git a/docker/finn_entrypoint.sh b/docker/finn_entrypoint.sh
index 4569e5be77e9d3f6906f94e279938af9abe28345..6dcf99e7589e4e0e6c50b626ea53948f6153ae3c 100644
--- a/docker/finn_entrypoint.sh
+++ b/docker/finn_entrypoint.sh
@@ -12,6 +12,7 @@ gecho () {
 
 # checkout the correct dependency repo commits
 # the repos themselves are cloned in the Dockerfile
+FINN_BASE_COMMIT=ceb1219b5aba396dde41967a929e1f08887653ce
 BREVITAS_COMMIT=6ffefa8dbf37fdb0f44c994f34604c29fadb16b0
 CNPY_COMMIT=4e8810b1a8637695171ed346ce68f6984e585ef4
 HLSLIB_COMMIT=cfafe11a93b79ab1af7529d68f08886913a6466e
@@ -19,6 +20,11 @@ PYVERILATOR_COMMIT=c97a5ba41bbc7c419d6f25c74cdf3bdc3393174f
 OMX_COMMIT=1bae737669901e762f581af73348332b5c4b2ada
 
 gecho "Setting up known-good commit versions for FINN dependencies"
+# finn-base
+gecho "finn-base @ $FINN_BASE_COMMIT"
+git -C /workspace/finn-base pull --quiet
+git -C /workspace/finn-base checkout $FINN_BASE_COMMIT --quiet
+pip install --user -e /workspace/finn-base
 # Brevitas
 gecho "brevitas @ $BREVITAS_COMMIT"
 git -C /workspace/brevitas pull --quiet
diff --git a/notebooks/advanced/1_custom_transformation_pass.ipynb b/notebooks/advanced/1_custom_transformation_pass.ipynb
index a9345401e28b89ba3a206b81e7c3b022bedae023..9c54d6f26913e558867b2f800b424f4157f47491 100644
--- a/notebooks/advanced/1_custom_transformation_pass.ipynb
+++ b/notebooks/advanced/1_custom_transformation_pass.ipynb
@@ -120,7 +120,7 @@
     }
    ],
    "source": [
-    "from finn.transformation import Transformation\n",
+    "from finn.transformation.base import Transformation\n",
     "\n",
     "showSrc(Transformation)"
    ]
@@ -199,7 +199,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "from finn.transformation import Transformation\n",
+    "from finn.transformation.base import Transformation\n",
     "\n",
     "class ConvertSubToAdd(Transformation):\n",
     "    def apply(self, model):\n",
@@ -352,7 +352,7 @@
     }
    ],
    "source": [
-    "from finn.transformation import NodeLocalTransformation\n",
+    "from finn.transformation.base import NodeLocalTransformation\n",
     "\n",
     "showSrc(NodeLocalTransformation)"
    ]
diff --git a/requirements.txt b/requirements.txt
index ba7bc716b741911820e67f1455aeca4c05e6e005..71eaa3224213f0ff021107ce5090a9fb1901234a 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -3,8 +3,8 @@ docrep==0.2.7
 future==0.18.2
 gspread==3.6.0
 numpy==1.18.0
-onnx==1.6.0
-onnxruntime==1.2.0
+onnx==1.7.0
+onnxruntime==1.4.0
 pre-commit==2.6.0
 scipy==1.5.2
 toposort==1.5
diff --git a/src/__init__.py b/src/__init__.py
deleted file mode 100644
index 83c8e8bed70797f7d6c0138968f750f72e790386..0000000000000000000000000000000000000000
--- a/src/__init__.py
+++ /dev/null
@@ -1,27 +0,0 @@
-# Copyright (c) 2020, Xilinx
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-#
-# * Redistributions of source code must retain the above copyright notice, this
-#   list of conditions and the following disclaimer.
-#
-# * Redistributions in binary form must reproduce the above copyright notice,
-#   this list of conditions and the following disclaimer in the documentation
-#   and/or other materials provided with the distribution.
-#
-# * Neither the name of FINN nor the names of its
-#   contributors may be used to endorse or promote products derived from
-#   this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/src/finn/__init__.py b/src/finn/__init__.py
deleted file mode 100644
index 76b4dacddb18404218dcb842bddf79b5f72eeb8e..0000000000000000000000000000000000000000
--- a/src/finn/__init__.py
+++ /dev/null
@@ -1,39 +0,0 @@
-# Copyright (c) 2020, Xilinx
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-#
-# * Redistributions of source code must retain the above copyright notice, this
-#   list of conditions and the following disclaimer.
-#
-# * Redistributions in binary form must reproduce the above copyright notice,
-#   this list of conditions and the following disclaimer in the documentation
-#   and/or other materials provided with the distribution.
-#
-# * Neither the name of FINN nor the names of its
-#   contributors may be used to endorse or promote products derived from
-#   this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-# -*- coding: utf-8 -*-
-from pkg_resources import get_distribution, DistributionNotFound
-
-try:
-    # Change here if project is renamed and does not equal the package name
-    dist_name = "FINN"
-    __version__ = get_distribution(dist_name).version
-except DistributionNotFound:
-    __version__ = "unknown"
-finally:
-    del get_distribution, DistributionNotFound
diff --git a/src/finn/analysis/__init__.py b/src/finn/analysis/__init__.py
deleted file mode 100644
index c3f810e6f658c248d3aee26a4e174baa5cf44ce5..0000000000000000000000000000000000000000
--- a/src/finn/analysis/__init__.py
+++ /dev/null
@@ -1,36 +0,0 @@
-# Copyright (c) 2020, Xilinx
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-#
-# * Redistributions of source code must retain the above copyright notice, this
-#   list of conditions and the following disclaimer.
-#
-# * Redistributions in binary form must reproduce the above copyright notice,
-#   this list of conditions and the following disclaimer in the documentation
-#   and/or other materials provided with the distribution.
-#
-# * Neither the name of FINN nor the names of its
-#   contributors may be used to endorse or promote products derived from
-#   this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-"""
-How to write an analysis pass for FINN
---------------------------------------
-
-An analysis pass traverses the graph structure and produces information about
-certain properties. The convention is to take in a ModelWrapper, and return
-a dictionary of named properties that the analysis extracts.
-"""
diff --git a/src/finn/analysis/topology.py b/src/finn/analysis/topology.py
deleted file mode 100644
index acdb8ed7fcf41fd041c3601b2ee4fe67b6dc5f19..0000000000000000000000000000000000000000
--- a/src/finn/analysis/topology.py
+++ /dev/null
@@ -1,104 +0,0 @@
-# Copyright (c) 2020, Xilinx
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-#
-# * Redistributions of source code must retain the above copyright notice, this
-#   list of conditions and the following disclaimer.
-#
-# * Redistributions in binary form must reproduce the above copyright notice,
-#   this list of conditions and the following disclaimer in the documentation
-#   and/or other materials provided with the distribution.
-#
-# * Neither the name of FINN nor the names of its
-#   contributors may be used to endorse or promote products derived from
-#   this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-import numpy as np
-
-
-def is_linear(model):
-    """Checks whether the given model graph is linear. This is done by looking
-    at the fan-out of each tensor. All tensors have a fan-out <= 1 in a linear
-    graph.
-
-    Returns {"is_linear": Bool}."""
-    per_tensor_fanouts = get_per_tensor_fanouts(model)
-    # check for tensors that have fanout > 1
-    multi_fanouts = list(filter(lambda x: x[1] > 1, per_tensor_fanouts.items()))
-    return {"is_linear": len(multi_fanouts) == 0}
-
-
-def get_per_tensor_fanouts(model):
-    """Returns a dictionary of {tensor_name: tensor_fanout} for the model."""
-    # make execution context to get a list of tensors
-    per_tensor_fanouts = model.make_empty_exec_context()
-    # replace every tensor with its fanout
-    for tensor_name in per_tensor_fanouts.keys():
-        per_tensor_fanouts[tensor_name] = model.get_tensor_fanout(tensor_name)
-    return per_tensor_fanouts
-
-
-def all_tensors_f32(model):
-    """Checks whether all tensors have a float32 dtype, extra quantization
-    annotations notwithstanding.
-
-    Returns {"all_tensors_f32": Bool}."""
-    all_tensors = model.make_empty_exec_context().items()
-    non_f32_tensors = filter(lambda x: x[1].dtype != np.float32, all_tensors)
-    return {"all_tensors_f32": len(list(non_f32_tensors)) == 0}
-
-
-def node_inputs_in_expected_order(model):
-    """Verifies that the node inputs are ordered in the way that FINN expects
-    them. When a node has a mixture of static (= constant, initialized) inputs
-    and dynamic inputs, the dynamic input should come first, followed by the
-    static one. Only verifiable for a small subset of op_types for now.
-
-    Returns {"node_inputs_in_expected_order": Bool}."""
-    op_types = ["MatMul", "Conv", "Add", "Mul"]
-    nodes = filter(lambda x: x.op_type in op_types, model.graph.node)
-    all_OK = True
-    for n in nodes:
-        all_OK = all_OK and len(list(n.input)) == 2
-        # input 0 should be dynamic, no initializer
-        all_OK = all_OK and (model.get_initializer(n.input[0]) is None)
-        # input 1 should be static (unless eltwise add)
-        if n.op_type != "Add":
-            all_OK = all_OK and (model.get_initializer(n.input[1]) is not None)
-    return {"node_inputs_in_expected_order": all_OK}
-
-
-def nodes_topologically_sorted(model):
-    """Verifies that graph.node is topologically sorted. This is required by the
-    ONNX specification.
-
-    Returns {"nodes_topologically_sorted": Bool}."""
-
-    # get successors of every node and check that
-    # successor index > current node index
-
-    all_OK = True
-    for n in model.graph.node:
-        successors = model.find_direct_successors(n)
-        if successors is not None:
-            for successor in successors:
-                # check the condition by checking the antithesis
-                index_n = model.get_node_index(n)
-                index_suc = model.get_node_index(successor)
-                if index_n > index_suc:
-                    all_OK = False
-
-    return {"nodes_topologically_sorted": all_OK}
diff --git a/src/finn/core/__init__.py b/src/finn/core/__init__.py
deleted file mode 100644
index 83c8e8bed70797f7d6c0138968f750f72e790386..0000000000000000000000000000000000000000
--- a/src/finn/core/__init__.py
+++ /dev/null
@@ -1,27 +0,0 @@
-# Copyright (c) 2020, Xilinx
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-#
-# * Redistributions of source code must retain the above copyright notice, this
-#   list of conditions and the following disclaimer.
-#
-# * Redistributions in binary form must reproduce the above copyright notice,
-#   this list of conditions and the following disclaimer in the documentation
-#   and/or other materials provided with the distribution.
-#
-# * Neither the name of FINN nor the names of its
-#   contributors may be used to endorse or promote products derived from
-#   this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/src/finn/core/data_layout.py b/src/finn/core/data_layout.py
deleted file mode 100644
index 3971d221527d3862346c06cf415831c27e5cba8b..0000000000000000000000000000000000000000
--- a/src/finn/core/data_layout.py
+++ /dev/null
@@ -1,35 +0,0 @@
-# Copyright (c) 2020, Xilinx
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-#
-# * Redistributions of source code must retain the above copyright notice, this
-#   list of conditions and the following disclaimer.
-#
-# * Redistributions in binary form must reproduce the above copyright notice,
-#   this list of conditions and the following disclaimer in the documentation
-#   and/or other materials provided with the distribution.
-#
-# * Neither the name of FINN nor the names of its
-#   contributors may be used to endorse or promote products derived from
-#   this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-# predefined lists of strings to have a cannonical way of expresing data layout
-# annotations
-
-NHWC = ["N", "H", "W", "C"]
-NCHW = ["N", "C", "H", "W"]
-NC = ["N", "C"]
-UNKNOWN = []
diff --git a/src/finn/core/datatype.py b/src/finn/core/datatype.py
deleted file mode 100644
index df895a1ad446d6b2cc3ebb24f1179944f4cfe9ab..0000000000000000000000000000000000000000
--- a/src/finn/core/datatype.py
+++ /dev/null
@@ -1,229 +0,0 @@
-# Copyright (c) 2020, Xilinx
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-#
-# * Redistributions of source code must retain the above copyright notice, this
-#   list of conditions and the following disclaimer.
-#
-# * Redistributions in binary form must reproduce the above copyright notice,
-#   this list of conditions and the following disclaimer in the documentation
-#   and/or other materials provided with the distribution.
-#
-# * Neither the name of FINN nor the names of its
-#   contributors may be used to endorse or promote products derived from
-#   this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-
-from enum import Enum, auto
-
-import numpy as np
-
-
-class DataType(Enum):
-    """Enum class that contains FINN data types to set the quantization annotation.
-    ONNX does not support data types smaller than 8-bit integers, whereas in FINN we are
-    interested in smaller integers down to ternary and bipolar.
-
-    Assignment of DataTypes to indices based on following ordering:
-
-    * unsigned to signed
-
-    * fewer to more bits
-
-    Currently supported DataTypes: """
-
-    # important: the get_smallest_possible() member function is dependent on ordering.
-    BINARY = auto()
-    UINT2 = auto()
-    UINT3 = auto()
-    UINT4 = auto()
-    UINT5 = auto()
-    UINT6 = auto()
-    UINT7 = auto()
-    UINT8 = auto()
-    UINT9 = auto()
-    UINT10 = auto()
-    UINT11 = auto()
-    UINT12 = auto()
-    UINT13 = auto()
-    UINT14 = auto()
-    UINT15 = auto()
-    UINT16 = auto()
-    UINT17 = auto()
-    UINT18 = auto()
-    UINT19 = auto()
-    UINT20 = auto()
-    UINT21 = auto()
-    UINT22 = auto()
-    UINT23 = auto()
-    UINT24 = auto()
-    UINT25 = auto()
-    UINT26 = auto()
-    UINT27 = auto()
-    UINT28 = auto()
-    UINT29 = auto()
-    UINT30 = auto()
-    UINT31 = auto()
-    UINT32 = auto()
-    UINT64 = auto()
-    BIPOLAR = auto()
-    TERNARY = auto()
-    INT2 = auto()
-    INT3 = auto()
-    INT4 = auto()
-    INT5 = auto()
-    INT6 = auto()
-    INT7 = auto()
-    INT8 = auto()
-    INT9 = auto()
-    INT10 = auto()
-    INT11 = auto()
-    INT12 = auto()
-    INT13 = auto()
-    INT14 = auto()
-    INT15 = auto()
-    INT16 = auto()
-    INT17 = auto()
-    INT18 = auto()
-    INT19 = auto()
-    INT20 = auto()
-    INT21 = auto()
-    INT22 = auto()
-    INT23 = auto()
-    INT24 = auto()
-    INT25 = auto()
-    INT26 = auto()
-    INT27 = auto()
-    INT28 = auto()
-    INT29 = auto()
-    INT30 = auto()
-    INT31 = auto()
-    INT32 = auto()
-    INT64 = auto()
-    FLOAT32 = auto()
-
-    def bitwidth(self):
-        """Returns the number of bits required for this DataType."""
-
-        if self.name.startswith("UINT"):
-            return int(self.name.strip("UINT"))
-        elif self.name.startswith("INT"):
-            return int(self.name.strip("INT"))
-        elif "FLOAT" in self.name:
-            return int(self.name.strip("FLOAT"))
-        elif self.name in ["BINARY", "BIPOLAR"]:
-            return 1
-        elif self.name == "TERNARY":
-            return 2
-        else:
-            raise Exception("Unrecognized data type: %s" % self.name)
-
-    def min(self):
-        """Returns the smallest possible value allowed by this DataType."""
-
-        if self.name.startswith("UINT") or self.name == "BINARY":
-            return 0
-        elif self.name.startswith("INT"):
-            return -(2 ** (self.bitwidth() - 1))
-        elif self.name == "FLOAT32":
-            return np.finfo(np.float32).min
-        elif self.name == "BIPOLAR":
-            return -1
-        elif self.name == "TERNARY":
-            return -1
-        else:
-            raise Exception("Unrecognized data type: %s" % self.name)
-
-    def max(self):
-        """Returns the largest possible value allowed by this DataType."""
-
-        if self.name.startswith("UINT"):
-            return (2 ** (self.bitwidth())) - 1
-        elif self.name == "BINARY":
-            return +1
-        elif self.name.startswith("INT"):
-            return (2 ** (self.bitwidth() - 1)) - 1
-        elif self.name == "FLOAT32":
-            return np.finfo(np.float32).max
-        elif self.name == "BIPOLAR":
-            return +1
-        elif self.name == "TERNARY":
-            return +1
-        else:
-            raise Exception("Unrecognized data type: %s" % self.name)
-
-    def allowed(self, value):
-        """Check whether given value is allowed for this DataType.
-
-        * value (float32): value to be checked"""
-
-        if "FLOAT" in self.name:
-            return True
-        elif "INT" in self.name:
-            return (
-                (self.min() <= value)
-                and (value <= self.max())
-                and float(value).is_integer()
-            )
-        elif self.name == "BINARY":
-            return value in [0, 1]
-        elif self.name == "BIPOLAR":
-            return value in [-1, +1]
-        elif self.name == "TERNARY":
-            return value in [-1, 0, +1]
-        else:
-            raise Exception("Unrecognized data type: %s" % self.name)
-
-    def get_num_possible_values(self):
-        """Returns the number of possible values this DataType can take. Only
-        implemented for integer types for now."""
-        assert self.is_integer(), """This function only works for integers for now,
-        not for the DataType you used this function with."""
-        if "INT" in self.name:
-            return abs(self.min()) + abs(self.max()) + 1
-        elif self.name == "BINARY" or self.name == "BIPOLAR":
-            return 2
-        elif self.name == "TERNARY":
-            return 3
-
-    def get_smallest_possible(value):
-        """Returns smallest (fewest bits) possible DataType that can represent
-      value. Prefers unsigned integers where possible."""
-        if not int(value) == value:
-            return DataType["FLOAT32"]
-        for k in DataType.__members__:
-            dt = DataType[k]
-            if (dt.min() <= value) and (value <= dt.max()):
-                return dt
-
-    def signed(self):
-        """Returns whether this DataType can represent negative numbers."""
-        return self.min() < 0
-
-    def is_integer(self):
-        """Returns whether this DataType represents integer values only."""
-        # only FLOAT32 is noninteger for now
-        return self != DataType.FLOAT32
-
-    def get_hls_datatype_str(self):
-        """Returns 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/execute_custom_node.py b/src/finn/core/execute_custom_node.py
deleted file mode 100644
index 86f7114a700c31e93d1d980693410c0e16dd128a..0000000000000000000000000000000000000000
--- a/src/finn/core/execute_custom_node.py
+++ /dev/null
@@ -1,42 +0,0 @@
-# Copyright (c) 2020, Xilinx
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-#
-# * Redistributions of source code must retain the above copyright notice, this
-#   list of conditions and the following disclaimer.
-#
-# * Redistributions in binary form must reproduce the above copyright notice,
-#   this list of conditions and the following disclaimer in the documentation
-#   and/or other materials provided with the distribution.
-#
-# * Neither the name of FINN nor the names of its
-#   contributors may be used to endorse or promote products derived from
-#   this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-import finn.custom_op.registry as registry
-
-
-def execute_custom_node(node, context, graph):
-    """Call custom implementation to execute a single custom node.
-    Input/output provided via context."""
-    op_type = node.op_type
-    try:
-        # lookup op_type in registry of CustomOps
-        inst = registry.custom_op[op_type](node)
-        inst.execute_node(context, graph)
-    except KeyError:
-        # exception if op_type is not supported
-        raise Exception("Custom op_type %s is currently not supported." % op_type)
diff --git a/src/finn/core/modelwrapper.py b/src/finn/core/modelwrapper.py
deleted file mode 100644
index 11fda3da5a65c89f3e5d5b7bcb1da4cd4be056b9..0000000000000000000000000000000000000000
--- a/src/finn/core/modelwrapper.py
+++ /dev/null
@@ -1,585 +0,0 @@
-# Copyright (c) 2020, Xilinx
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-#
-# * Redistributions of source code must retain the above copyright notice, this
-#   list of conditions and the following disclaimer.
-#
-# * Redistributions in binary form must reproduce the above copyright notice,
-#   this list of conditions and the following disclaimer in the documentation
-#   and/or other materials provided with the distribution.
-#
-# * Neither the name of FINN nor the names of its
-#   contributors may be used to endorse or promote products derived from
-#   this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-import copy
-import os
-import onnx
-import onnx.helper as oh
-import onnx.numpy_helper as np_helper
-from onnx import TensorProto
-
-import finn.util.basic as util
-import finn.util.onnx as onnxutil
-from finn.core.datatype import DataType
-from finn.transformation.general import (
-    RemoveUnusedTensors,
-    RemoveStaticGraphInputs,
-    SortGraph,
-)
-from finn.transformation.double_to_single_float import DoubleToSingleFloat
-
-
-class ModelWrapper:
-    """A wrapper around ONNX ModelProto that exposes some useful utility
-    functions for graph manipulation and exploration."""
-
-    def __init__(self, onnx_model_proto, make_deepcopy=False):
-        """Creates a ModelWrapper instance.
-        onnx_model_proto can be either a ModelProto instance, or a string
-        with the path to a stored .onnx file on disk, or serialized bytes.
-
-        - make_deepcopy : controls whether a deep copy of the ModelProto
-        is made internally.
-        """
-        if isinstance(onnx_model_proto, str):
-            assert os.path.isfile(onnx_model_proto)
-            self._model_proto = onnx.load(onnx_model_proto)
-        elif isinstance(onnx_model_proto, bytes):
-            self._model_proto = onnx.load_from_string(onnx_model_proto)
-        else:
-            if make_deepcopy:
-                self._model_proto = copy.deepcopy(onnx_model_proto)
-            else:
-                self._model_proto = onnx_model_proto
-
-    @property
-    def graph(self):
-        """Returns the graph of the model."""
-        return self._model_proto.graph
-
-    @graph.setter
-    def graph(self, value):
-        """Sets the graph of the model according to value"""
-        self._model_proto.graph = value
-
-    @property
-    def model(self):
-        """Returns the model."""
-        return self._model_proto
-
-    @model.setter
-    def model(self, value):
-        """Sets the model according to value."""
-        self._model_proto = value
-
-    def save(self, filename):
-        """Saves the wrapper ONNX ModelProto into a file with given name."""
-        onnx.save(self._model_proto, filename)
-
-    def analysis(self, analysis_fxn):
-        """Runs given anaylsis_fxn on this model and return resulting dict."""
-        return analysis_fxn(self)
-
-    def transform(
-        self, transformation, make_deepcopy=True, cleanup=True, fix_float64=True
-    ):
-        """Applies given Transformation repeatedly until no more changes can be made
-        and returns a transformed ModelWrapper instance.
-
-        - make_deepcopy : operates on a new (deep)copy of model.
-        - fix_float64 : DoubleToSingleFloat correction before starting
-        - cleanup : execute cleanup transformations before returning
-        """
-        transformed_model = self
-        if make_deepcopy:
-            transformed_model = copy.deepcopy(self)
-        if fix_float64:
-            (transformed_model, model_was_changed) = DoubleToSingleFloat().apply(
-                transformed_model
-            )
-        model_was_changed = True
-        while model_was_changed:
-            (transformed_model, model_was_changed) = transformation.apply(
-                transformed_model
-            )
-        if cleanup:
-            transformed_model.cleanup()
-        return transformed_model
-
-    def cleanup(self):
-        "Run cleanup transformations on the model."
-        transformed_model = self
-        cleanup_transforms = [
-            RemoveUnusedTensors(),
-            RemoveStaticGraphInputs(),
-            SortGraph(),
-        ]
-        for trn in cleanup_transforms:
-            transformed_model = transformed_model.transform(
-                trn, cleanup=False, make_deepcopy=False
-            )
-        return transformed_model
-
-    def check_compatibility(self):
-        """Checks this model for FINN compatibility:
-
-        * no embedded subgraphs
-
-        * all tensor shapes are specified, including activations
-
-        * all constants are initializers
-        """
-        # TODO check for no embedded subgraphs
-        # TODO check that all shapes are inferred
-        # TODO check that all constants are initializers
-        return True
-
-    def get_tensor_datatype(self, tensor_name):
-        """Returns the FINN DataType of tensor with given name."""
-        graph = self._model_proto.graph
-        qnt_annotations = graph.quantization_annotation
-        ret = util.get_by_name(qnt_annotations, tensor_name, "tensor_name")
-        if ret is not None:
-            ret = util.get_by_name(
-                ret.quant_parameter_tensor_names, "finn_datatype", "key"
-            )
-            if ret is not None:
-                return DataType[ret.value]
-        # TODO maybe use native ONNX tensor type instead of assuming fp32?
-        return DataType["FLOAT32"]
-
-    def set_tensor_datatype(self, tensor_name, datatype):
-        """Sets the FINN DataType of tensor with given name."""
-        graph = self._model_proto.graph
-        qnt_annotations = graph.quantization_annotation
-        ret = util.get_by_name(qnt_annotations, tensor_name, "tensor_name")
-        if ret is not None:
-            ret_dt = util.get_by_name(
-                ret.quant_parameter_tensor_names, "finn_datatype", "key"
-            )
-            if ret_dt is not None:
-                ret_dt.value = datatype.name
-            else:
-                dt = onnx.StringStringEntryProto()
-                dt.key = "finn_datatype"
-                dt.value = datatype.name
-                ret.quant_parameter_tensor_names.append(dt)
-        else:
-            qa = onnx.TensorAnnotation()
-            dt = onnx.StringStringEntryProto()
-            dt.key = "finn_datatype"
-            dt.value = datatype.name
-            qa.tensor_name = tensor_name
-            qa.quant_parameter_tensor_names.append(dt)
-            qnt_annotations.append(qa)
-
-    def get_tensor_valueinfo(self, tensor_name):
-        """Returns ValueInfoProto of tensor with given name, if it has one."""
-        graph = self._model_proto.graph
-        vi_names = [(x.name, x) for x in graph.input]
-        vi_names += [(x.name, x) for x in graph.output]
-        vi_names += [(x.name, x) for x in graph.value_info]
-        try:
-            vi_ind = [x[0] for x in vi_names].index(tensor_name)
-            vi = vi_names[vi_ind][1]
-            return vi
-        except ValueError:
-            return None
-
-    def get_tensor_shape(self, tensor_name):
-        """Returns the shape of tensor with given name, if it has ValueInfoProto."""
-        graph = self._model_proto.graph
-        vi_names = [(x.name, x) for x in graph.input]
-        vi_names += [(x.name, x) for x in graph.output]
-        vi_names += [(x.name, x) for x in graph.value_info]
-        try:
-            vi_ind = [x[0] for x in vi_names].index(tensor_name)
-            vi = vi_names[vi_ind][1]
-            dims = [x.dim_value for x in vi.type.tensor_type.shape.dim]
-            return dims
-        except ValueError:
-            return None
-
-    def set_tensor_shape(self, tensor_name, tensor_shape, dtype=TensorProto.FLOAT):
-        """Assigns shape in ValueInfoProto for tensor with given name."""
-        new_vi = oh.make_tensor_value_info(tensor_name, dtype, tensor_shape)
-        # find what container tis tensor's ValueInfo lives in
-        # if not found anywhere, we assume it's a new value_info
-        target_container = self.graph.value_info
-        if util.get_by_name(self.graph.input, tensor_name) is not None:
-            target_container = self.graph.input
-        if util.get_by_name(self.graph.output, tensor_name) is not None:
-            target_container = self.graph.output
-        # remove from target container and add new
-        util.remove_by_name(target_container, tensor_name)
-        target_container.append(new_vi)
-
-    def set_initializer(self, tensor_name, tensor_value):
-        """Sets the initializer value for tensor with given name."""
-        graph = self._model_proto.graph
-        # convert tensor_value (numpy array) into TensorProto w/ correct name
-        tensor_init_proto = np_helper.from_array(tensor_value)
-        tensor_init_proto.name = tensor_name
-        # first, remove if an initializer already exists
-        init_names = [x.name for x in graph.initializer]
-        try:
-            init_ind = init_names.index(tensor_name)
-            init_old = graph.initializer[init_ind]
-            graph.initializer.remove(init_old)
-        except ValueError:
-            pass
-        # create and insert new initializer
-        graph.initializer.append(tensor_init_proto)
-        # set shape
-        dtype = tensor_init_proto.data_type
-        self.set_tensor_shape(tensor_name, list(tensor_value.shape), dtype)
-
-    def rename_tensor(self, old_name, new_name):
-        """Renames a tensor from old_name to new_name."""
-        graph = self.graph
-        # sweep over inputs
-        if util.get_by_name(graph.input, old_name) is not None:
-            util.get_by_name(graph.input, old_name).name = new_name
-        # sweep over outputs
-        if util.get_by_name(graph.output, old_name) is not None:
-            util.get_by_name(graph.output, old_name).name = new_name
-        # sweep over value_info
-        if util.get_by_name(graph.value_info, old_name) is not None:
-            util.get_by_name(graph.value_info, old_name).name = new_name
-        # sweep over initializers
-        if util.get_by_name(graph.initializer, old_name) is not None:
-            util.get_by_name(graph.initializer, old_name).name = new_name
-        # sweep over quantization annotations
-        if (
-            util.get_by_name(graph.quantization_annotation, old_name, "tensor_name")
-            is not None
-        ):
-            util.get_by_name(
-                graph.quantization_annotation, old_name, "tensor_name"
-            ).tensor_name = new_name
-        # sweep over node i/o
-        for n in graph.node:
-            if old_name in n.input:
-                n.input[list(n.input).index(old_name)] = new_name
-            if old_name in n.output:
-                n.output[list(n.output).index(old_name)] = new_name
-
-    def get_initializer(self, tensor_name):
-        """Gets the initializer value for tensor with given name, if any."""
-        graph = self._model_proto.graph
-        init_names = [x.name for x in graph.initializer]
-        try:
-            init_ind = init_names.index(tensor_name)
-            return np_helper.to_array(graph.initializer[init_ind])
-        except ValueError:
-            return None
-
-    def find_producer(self, tensor_name):
-        """Finds and returns the node that produces the tensor with given name."""
-        for x in self._model_proto.graph.node:
-            if tensor_name in x.output:
-                return x
-        return None
-
-    def find_upstream(self, tensor_name, finder_fxn):
-        """Follow the producer chain upstream, calling finder_fxn on each upstream
-        node until it returns True or there are no nodes left. Returns the list
-        of nodes visited, or None if finder_fxn did not return True."""
-        visit_list = []
-        current_tensor = tensor_name
-        while True:
-            current_producer = self.find_producer(current_tensor)
-            if current_producer is None:
-                return []
-            else:
-                found = finder_fxn(current_producer)
-                visit_list.append(current_producer)
-                if found:
-                    return visit_list
-                else:
-                    current_tensor = current_producer.input[0]
-
-    def find_consumer(self, tensor_name):
-        """Finds and returns the node that consumes the tensor with given name.
-        Currently only works for linear graphs."""
-        all_inputs = [x.input[0] for x in self._model_proto.graph.node]
-        try:
-            consumer_ind = all_inputs.index(tensor_name)
-            return self._model_proto.graph.node[consumer_ind]
-        except ValueError:
-            return None
-
-    def find_consumers(self, tensor_name):
-        """Finds and returns a list of the nodes that consume tensor with
-        given name."""
-        consumers = []
-        for n in self._model_proto.graph.node:
-            for inp_tensor in n.input:
-                if inp_tensor == tensor_name:
-                    consumers.append(n)
-        if consumers != []:
-            return consumers
-        else:
-            return None
-
-    def find_direct_successors(self, node):
-        """Finds and returns a list of the nodes that are successors of
-        given node."""
-        successors = []
-        for outp_tensor in node.output:
-            tensor_consumer_list = self.find_consumers(outp_tensor)
-            if tensor_consumer_list is not None:
-                for consumer in tensor_consumer_list:
-                    successors.append(consumer)
-        if successors != []:
-            return successors
-        else:
-            return None
-
-    def find_direct_predecessors(self, node):
-        """Finds and returns a list of the nodes that are predecessors of
-        given node."""
-        predecessors = []
-        for inp_tensor in node.input:
-            producer = self.find_producer(inp_tensor)
-            if producer is not None:
-                predecessors.append(producer)
-        if predecessors != []:
-            return predecessors
-        else:
-            return None
-
-    def is_fork_node(self, node):
-        """Checks if the given node is a fork, that is, the node has multiple
-        direct successors"""
-        direct_successors = self.find_direct_successors(node)
-        is_fork = False if direct_successors is None else (len(direct_successors) > 1)
-        return is_fork
-
-    def is_join_node(self, node):
-        """Checks if the given node is a join, that is, the node has multiple
-        direct predecessors"""
-        direct_predecessors = self.find_direct_predecessors(node)
-        is_join = (
-            False if direct_predecessors is None else (len(direct_predecessors) > 1)
-        )
-        return is_join
-
-    def get_all_tensor_names(self):
-        """Returns a list of all (input, output and value_info) tensor names
-        in the graph."""
-        graph = self.graph
-        names = [x.name for x in graph.value_info]
-        names += [x.name for x in graph.input]
-        names += [x.name for x in graph.output]
-        return names
-
-    def make_new_valueinfo_name(self):
-        """Returns a name that can be used for a new value_info."""
-        names = self.get_all_tensor_names()
-        candidate = util.random_string()
-        while candidate in names:
-            candidate = util.random_string()
-        return candidate
-
-    def make_empty_exec_context(self):
-        """Creates an empty execution context for this model.
-
-        The execution context is a dictionary of all tensors used for the
-        inference computation. Any initializer values will be taken into
-        account, all other tensors will be zero."""
-        execution_context = dict()
-        graph = self._model_proto.graph
-        # make empty tensors for all the graph inputs and outputs
-        for vi in graph.input:
-            new_tensor = onnxutil.valueinfo_to_tensor(vi)
-            execution_context[vi.name] = new_tensor
-        for vi in graph.output:
-            new_tensor = onnxutil.valueinfo_to_tensor(vi)
-            execution_context[vi.name] = new_tensor
-        # make empty tensors for all intermediate buffers
-        for vi in graph.value_info:
-            new_tensor = onnxutil.valueinfo_to_tensor(vi)
-            execution_context[vi.name] = new_tensor
-        # fill in the constants provided by the initializers (TensorProto to npy)
-        for t in graph.initializer:
-            execution_context[t.name] = np_helper.to_array(t)
-        return execution_context
-
-    def check_all_tensor_shapes_specified(self):
-        """Checks whether all tensors have a specified shape (ValueInfo).
-        The ONNX standard allows for intermediate activations to have no
-        associated ValueInfo, but FINN expects this."""
-        graph = self._model_proto.graph
-        ret = True
-        for n in graph.node:
-            for i in n.input:
-                ret = ret and (self.get_tensor_shape(i) is not None)
-            for o in n.output:
-                ret = ret and (self.get_tensor_shape(o) is not None)
-        return ret
-
-    def get_tensor_fanout(self, tensor_name):
-        """Returns the number of nodes for which the tensor with given name is
-        as input."""
-        graph = self.graph
-        fanout = 0
-        for n in graph.node:
-            if tensor_name in n.input:
-                fanout += 1
-        return fanout
-
-    def get_metadata_prop(self, key):
-        """Returns the value associated with metadata_prop with given key,
-        or None otherwise."""
-        metadata_prop = util.get_by_name(self.model.metadata_props, key, "key")
-        if metadata_prop is None:
-            return None
-        else:
-            return metadata_prop.value
-
-    def set_metadata_prop(self, key, value):
-        """Sets metadata property with given key to the given value."""
-        metadata_prop = util.get_by_name(self.model.metadata_props, key, "key")
-        if metadata_prop is None:
-            metadata_prop = onnx.StringStringEntryProto()
-            metadata_prop.key = key
-            metadata_prop.value = value
-            self.model.metadata_props.append(metadata_prop)
-        else:
-            metadata_prop.value = value
-            
-    def get_nodes_by_name(self, op_name):
-        """Returns a list of nodes with specified name."""
-        return list(filter(lambda x: x.name == op_name, self.graph.node))
-    
-    def get_nodes_by_op_type(self, op_type):
-        """Returns a list of nodes with specified op_type."""
-        return list(filter(lambda x: x.op_type == op_type, self.graph.node))
-
-    def get_finn_nodes(self):
-        """Returns a list of nodes where domain == 'finn'."""
-        return list(filter(lambda x: x.domain == "finn", self.graph.node))
-
-    def get_non_finn_nodes(self):
-        """Returns a list of nodes where domain != 'finn'."""
-        return list(filter(lambda x: x.domain != "finn", self.graph.node))
-
-    def get_node_index(self, node):
-        """Returns current index of given node."""
-        n_ind = 0
-        try:
-            for n in self.graph.node:
-                if n == node:
-                    return n_ind
-                n_ind += 1
-        except ValueError:
-            return None
-
-    def get_tensor_layout(self, tensor_name):
-        """Returns the data layout annotation of tensor with given name.
-        The data layout is expressed as a list of strings with as many
-        elements as the number of dimensions in the tensor shape. Each
-        string annotates what is contained in that dimension. If there is no
-        data layout annotation, None will be returned.
-        Examples of data layout annotations:
-        ["N", "C"] is tensor[batch][channel]
-        ["N", "C", "H", "W"] is tensor[batch][channel][height][width]
-        ["N", "H", "W", "C"] is tensor[batch][height][width][channel]
-        """
-        graph = self._model_proto.graph
-        qnt_annotations = graph.quantization_annotation
-        ret = util.get_by_name(qnt_annotations, tensor_name, "tensor_name")
-        if ret is not None:
-            ret = util.get_by_name(
-                ret.quant_parameter_tensor_names, "tensor_layout", "key"
-            )
-            if ret is not None:
-                return eval(ret.value)
-        return None
-
-    def set_tensor_layout(self, tensor_name, data_layout):
-        """Sets the data layout annotation of tensor with given name. See
-        get_tensor_layout for examples."""
-        tensor_shape = self.get_tensor_shape(tensor_name)
-        assert type(data_layout) == list, "data_layout must be a list"
-        if tensor_shape is not None:
-            assert len(tensor_shape) == len(
-                data_layout
-            ), """Mismatch between number
-            of dimensions of tensor shape and data layout annotation."""
-        graph = self._model_proto.graph
-        qnt_annotations = graph.quantization_annotation
-        ret = util.get_by_name(qnt_annotations, tensor_name, "tensor_name")
-        if ret is not None:
-            ret_tl = util.get_by_name(
-                ret.quant_parameter_tensor_names, "tensor_layout", "key"
-            )
-            if ret_tl is not None:
-                ret_tl.value = str(data_layout)
-            else:
-                tl = onnx.StringStringEntryProto()
-                tl.key = "tensor_layout"
-                tl.value = str(data_layout)
-                ret.quant_parameter_tensor_names.append(tl)
-        else:
-            qa = onnx.TensorAnnotation()
-            dt = onnx.StringStringEntryProto()
-            dt.key = "tensor_layout"
-            dt.value = str(data_layout)
-            qa.tensor_name = tensor_name
-            qa.quant_parameter_tensor_names.append(dt)
-            qnt_annotations.append(qa)
-
-    def get_tensor_sparsity(self, tensor_name):
-        """Returns the sparsity of a given tensor as dictionary."""
-        graph = self._model_proto.graph
-        qnt_annotations = graph.quantization_annotation
-        ret = util.get_by_name(qnt_annotations, tensor_name, "tensor_name")
-        if ret is not None:
-            ret = util.get_by_name(
-                ret.quant_parameter_tensor_names, "tensor_sparsity", "key"
-            )
-            if ret is not None:
-                return eval(ret.value)
-        return None
-
-    def set_tensor_sparsity(self, tensor_name, sparsity_dict):
-        """Sets the sparsity annotation of a tensor with given name."""
-        graph = self._model_proto.graph
-        qnt_annotations = graph.quantization_annotation
-        ret = util.get_by_name(qnt_annotations, tensor_name, "tensor_name")
-        if ret is not None:
-            ret_ts = util.get_by_name(
-                ret.quant_parameter_tensor_names, "tensor_sparsity", "key"
-            )
-            if ret_ts is not None:
-                ret_ts.value = str(sparsity_dict)
-            else:
-                ts = onnx.StringStringEntryProto()
-                ts.key = "tensor_sparsity"
-                ts.value = str(sparsity_dict)
-                ret.quant_parameter_tensor_names.append(ts)
-        else:
-            qa = onnx.TensorAnnotation()
-            dt = onnx.StringStringEntryProto()
-            dt.key = "tensor_sparsity"
-            dt.value = str(sparsity_dict)
-            qa.tensor_name = tensor_name
-            qa.quant_parameter_tensor_names.append(dt)
-            qnt_annotations.append(qa)
diff --git a/src/finn/core/onnx_exec.py b/src/finn/core/onnx_exec.py
deleted file mode 100644
index 85b52c0f33baac609b4dad4df59f8442f737ffc2..0000000000000000000000000000000000000000
--- a/src/finn/core/onnx_exec.py
+++ /dev/null
@@ -1,266 +0,0 @@
-# Copyright (c) 2020, Xilinx
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-#
-# * Redistributions of source code must retain the above copyright notice, this
-#   list of conditions and the following disclaimer.
-#
-# * Redistributions in binary form must reproduce the above copyright notice,
-#   this list of conditions and the following disclaimer in the documentation
-#   and/or other materials provided with the distribution.
-#
-# * Neither the name of FINN nor the names of its
-#   contributors may be used to endorse or promote products derived from
-#   this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-
-import copy
-
-import numpy as np
-import onnx.helper as helper
-import onnxruntime as rt
-
-import finn.core.execute_custom_node as ex_cu_node
-from finn.core.modelwrapper import ModelWrapper
-from finn.core.remote_exec import remote_exec
-from finn.core.rtlsim_exec import rtlsim_exec
-from finn.custom_op.registry import getCustomOp
-import finn.analysis.topology as ta
-from finn.util.basic import sanitize_quant_values, get_sanitize_quant_tensors
-
-
-def execute_node(node, context, graph, return_full_exec_context=False):
-    """Executes a single node by using onnxruntime, with custom function or
-    if dataflow partition by using remote execution or rtlsim.
-
-    Input/output provided via context."""
-
-    if node.op_type == "StreamingDataflowPartition":
-        sdp_node = getCustomOp(node)
-        model = ModelWrapper(sdp_node.get_nodeattr("model"))
-        inp_ctx = dict(filter(lambda x: x[0] in node.input, context.items()))
-        # input may have been renamed in partition
-        assert len(inp_ctx) == 1
-        old_iname = node.input[0]
-        new_iname = model.graph.input[0].name
-        if old_iname != new_iname:
-            inp_ctx[new_iname] = inp_ctx[old_iname]
-            del inp_ctx[old_iname]
-        ret = execute_onnx(model, inp_ctx, return_full_exec_context)
-        # if the model was in ip-stitched rtlsim mode, may get annotation
-        # for numbet of elapsed cycles, save again
-        if model.get_metadata_prop("exec_mode") == "rtlsim":
-            model.save(sdp_node.get_nodeattr("model"))
-        # output may have been renamed in partition
-        assert len(model.graph.output) == 1
-        node_oname = node.output[0]
-        model_oname = model.graph.output[0].name
-        context[node_oname] = ret[model_oname]
-        # prefix and insert exec context entries
-        if return_full_exec_context:
-            for tname in ret.keys():
-                if tname != model_oname:
-                    context[node.name + "_" + tname] = ret[tname]
-    else:
-        if node.domain == "finn":
-
-            ex_cu_node.execute_custom_node(node, context, graph)
-
-        else:
-
-            # onnxruntime unfortunately does not implement run_node as defined by ONNX,
-            # it can only execute entire models -- so we create a model which solely
-            # consists of our current node.
-            # note: ensure that the same ValueInfo does not appear both in
-            # graph.value_info as well as graph.output or graph.input
-            # nodes with multiple outputs that are a mix of value_info and
-            # input/outputs may get them reordered below
-            node_inputs = list(filter(lambda x: x.name in node.input, graph.input))
-            node_inputs += list(
-                filter(lambda x: x.name in node.input, graph.value_info)
-            )
-            node_outputs = list(filter(lambda x: x.name in node.output, graph.output))
-            node_outputs += list(
-                filter(lambda x: x.name in node.output, graph.value_info)
-            )
-            node_graph = helper.make_graph(
-                nodes=[node],
-                name="single-node-exec",
-                inputs=node_inputs,
-                outputs=node_outputs,
-            )
-            node_model = helper.make_model(node_graph)
-            input_dict = dict()
-            for inp in node.input:
-                input_dict[inp] = context[inp]
-
-            sess = rt.InferenceSession(node_model.SerializeToString())
-            output_list = sess.run(None, input_dict)
-
-            for output_ind in range(len(node.output)):
-                # get the name of the target buffer from node.output
-                outp = node.output[output_ind]
-
-                # retrieve the index of that name in node_outputs
-                for i in range(len(node_outputs)):
-                    if outp == node_outputs[i].name:
-                        list_ind = i
-
-                # use that index to index output_list
-                if output_list[list_ind].shape != context[outp].shape:
-                    raise Exception(
-                        """Output shapes disagree after node execution:
-                        found %s vs expected %s"""
-                        % (str(output_list[list_ind].shape), str(context[outp].shape))
-                    )
-                context[outp] = output_list[list_ind]
-
-
-def execute_onnx(
-    model, input_dict, return_full_exec_context=False, start_node=None, end_node=None
-):
-    """Executes given ONNX ModelWrapper with given named inputs.
-
-    If return_full_exec_context is False, a dict of named outputs is returned
-    as indicated by the model.graph.output.
-
-    If return return_full_exec_context is True, the full set of tensors used by
-    the execution (including inputs, weights, activations and final outputs)
-    will be returned as a dict.
-
-    When start_node and end_node are set to None, the whole graph is executed.
-    If they are set to particular ONNX nodes, only the subgraph between (and
-    including) those nodes is executed.
-    """
-
-    if not model.check_all_tensor_shapes_specified():
-        raise Exception("Found unspecified tensor shapes, try infer_shapes")
-    ret = model.analysis(ta.nodes_topologically_sorted)
-    assert (
-        ret["nodes_topologically_sorted"] is True
-    ), """Nodes must be
-    topologically sorted."""
-
-    graph = model.graph
-    # first, we need to make sure that every variable required by the graph has
-    # some buffer associated with it. this includes graph inputs (which includes
-    # the input data as well as the trained parameters) and the graph ValueInfo
-    # (intermediate tensors between layers)
-    # this is provided by the execution_context, which is a dict of np.ndarray
-    execution_context = model.make_empty_exec_context()
-    # fill in any inputs provided to this function
-    for inp_name in input_dict.keys():
-        if inp_name in execution_context:
-            if execution_context[inp_name].shape == input_dict[inp_name].shape:
-                execution_context[inp_name] = input_dict[inp_name]
-            else:
-                raise Exception(
-                    "Shape mismatch for provided input %s: found %s expected %s "
-                    % (
-                        inp_name,
-                        str(execution_context[inp_name].shape),
-                        str(input_dict[inp_name].shape),
-                    )
-                )
-        # else:
-        # raise Exception("Provided input not found in graph context: %s" % inp_name)
-
-    # check if model has an execution mode set
-    # if None, execute model node by node using execute_node()
-    # if set to "remote_pynq" execute model on PYNQ board
-    # if set to "rtlsim" execute model using pyverilator
-    model_exec_mode = model.get_metadata_prop("exec_mode")
-    if (model_exec_mode is None) or (model_exec_mode == ""):
-        # execute the model node by node
-        # we can simply walk down the list since the ONNX spec guarantees that it is
-        # topologically sorted
-        subgraph = []
-        if start_node is None:
-            start_node = model.graph.node[0]
-        if end_node is None:
-            end_node = model.graph.node[-1]
-        # select the nodes between specified start/end nodes
-        start_ind = model.get_node_index(start_node)
-        end_ind = model.get_node_index(end_node) + 1
-        assert end_ind >= start_ind, "Start/end nodes must define valid subgraph"
-        subgraph = graph.node[start_ind:end_ind]
-        for node in subgraph:
-            if get_sanitize_quant_tensors() != 0:
-                # round input values to match quantization annotation
-                execution_context = sanitize_quant_values(
-                    model, node.input, execution_context
-                )
-            execute_node(node, execution_context, graph, return_full_exec_context)
-            if get_sanitize_quant_tensors() != 0:
-                # round output values to quantization annotation
-                execution_context = sanitize_quant_values(
-                    model, node.output, execution_context
-                )
-    elif model_exec_mode == "remote_pynq":
-        # use remote exec metadata built into model to execute on a remote PYNQ
-        remote_exec(model, execution_context)
-    elif model_exec_mode == "rtlsim":
-        # use stitched IP for rtlsim
-        rtlsim_exec(model, execution_context)
-    else:
-        raise Exception(
-            """Metadata property "exec_mode" is set to an unknown value.
-        Can be left unset or has to be set to "remote_pynq" for remote execution
-        on PYNQ board or "rtlsim" for execution using pyverilator!"""
-        )
-
-    if return_full_exec_context:
-        return execution_context
-    else:
-        # provide outputs as dict
-        output_dict = dict()
-        for out_tensor in graph.output:
-            out_name = out_tensor.name
-            output_dict[out_name] = execution_context[out_name]
-        return output_dict
-
-
-def execute_onnx_and_make_model(model, input_dict):
-    """Executes given ONNX ModelWrapper with given named inputs and return a new
-    ModelWrapper where an initializer is provided for each tensor as taken from
-    the execution. This new model is useful for debugging, since it contains
-    all the intermediate activation values."""
-
-    # retrieve the full execution context
-    execution_context = execute_onnx(model, input_dict, True)
-    new_model = copy.deepcopy(model)
-    # create value_info entries and initializers for everything
-    for i in execution_context.keys():
-        new_model.set_initializer(i, execution_context[i])
-    for vi in new_model.graph.value_info:
-        new_model.graph.output.append(vi)
-    # import pdb; pdb.set_trace()
-    return new_model
-
-
-def compare_execution(
-    model_a,
-    model_b,
-    input_dict,
-    compare_fxn=lambda x, y: np.isclose(x, y, atol=1e-3).all(),
-):
-    """Executes two ONNX models and compare their outputs using given function.
-
-    compare_fxn should take in two tensors and return a Boolean"""
-    # compare values from first output tensors produced
-    res_a = list(execute_onnx(model_a, input_dict).items())[0][1]
-    res_b = list(execute_onnx(model_b, input_dict).items())[0][1]
-    return compare_fxn(res_a, res_b)
diff --git a/src/finn/core/remote_exec.py b/src/finn/core/remote_exec.py
deleted file mode 100644
index 2e139065ec0eff8cdbdb402f80113b039deed4da..0000000000000000000000000000000000000000
--- a/src/finn/core/remote_exec.py
+++ /dev/null
@@ -1,119 +0,0 @@
-# Copyright (c) 2020, Xilinx
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-#
-# * Redistributions of source code must retain the above copyright notice, this
-#   list of conditions and the following disclaimer.
-#
-# * Redistributions in binary form must reproduce the above copyright notice,
-#   this list of conditions and the following disclaimer in the documentation
-#   and/or other materials provided with the distribution.
-#
-# * Neither the name of FINN nor the names of its
-#   contributors may be used to endorse or promote products derived from
-#   this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-import os
-import subprocess
-import warnings
-import numpy as np
-
-
-def remote_exec(model, execution_context):
-    """Executes the given model remotely on the pynq board. The metadata properties
-    related to the pynq board have to be set. The execution context contains the
-    input values."""
-    # TODO fix for multi input-output
-    pynq_ip = model.get_metadata_prop("pynq_ip")
-    pynq_port = int(model.get_metadata_prop("pynq_port"))
-    pynq_username = model.get_metadata_prop("pynq_username")
-    pynq_password = model.get_metadata_prop("pynq_password")
-    pynq_target_dir = model.get_metadata_prop("pynq_target_dir")
-    deployment_dir = model.get_metadata_prop("pynq_deploy_dir")
-    platform = model.get_metadata_prop("platform")
-    assert platform in ["alveo", "zynq-iodma"]
-    bitfile = model.get_metadata_prop("bitfile")
-    bitfile = os.path.basename(bitfile)
-    if pynq_password == "":
-        if "zynq" in platform:
-            raise Exception("PYNQ board remote exec needs password for sudo")
-        else:
-            local_prefix = ""  # assume we are using an ssh key
-            warnings.warn("Empty password, make sure you've set up an ssh key")
-    else:
-        local_prefix = "sshpass -p %s " % pynq_password
-
-    if platform == "alveo":
-        # Alveo can run without sudo
-        remote_prefix = ""
-    elif "zynq" in platform:
-        # PYNQ Zynq boards need to execute with sudo
-        remote_prefix = "echo %s | sudo -S " % pynq_password
-
-    inp = execution_context[model.graph.input[0].name]
-    # make copy of array before saving it
-    inp = inp.copy()
-    batchsize = inp.shape[0]
-    np.save(os.path.join(deployment_dir, "input.npy"), inp)
-    # extracting last folder of absolute path (deployment_dir)
-    deployment_folder = os.path.basename(os.path.normpath(deployment_dir))
-    # copy input to PYNQ board
-    cmd = local_prefix + "scp -P{} -r {}/input.npy {}@{}:{}/{}".format(
-        pynq_port,
-        deployment_dir,
-        pynq_username,
-        pynq_ip,
-        pynq_target_dir,
-        deployment_folder,
-    )
-    bash_command = ["/bin/bash", "-c", cmd]
-    process_scp_in = subprocess.Popen(bash_command, stdout=subprocess.PIPE)
-    process_scp_in.communicate()
-
-    # use platform attribute for correct remote execution
-    if platform == "alveo":
-        remote_cmd = "bash -ic 'bash alveo_run.sh execute %d' \"" % batchsize
-    else:
-        remote_cmd = (
-            "python3.6 driver.py --exec_mode=execute --batchsize={} "
-            "--bitfile={} --inputfile=input.npy --outputfile=output.npy "
-            '--platform={} "'
-        ).format(batchsize, bitfile, platform)
-    cmd = (
-        local_prefix + 'ssh {}@{} -p {} "cd {}/{}; ' + remote_prefix + remote_cmd
-    ).format(pynq_username, pynq_ip, pynq_port, pynq_target_dir, deployment_folder)
-    bash_command = ["/bin/bash", "-c", cmd]
-    process_exec_accel = subprocess.Popen(bash_command, stdout=subprocess.PIPE)
-    process_exec_accel.communicate()
-    # remove stale output file from local dir, if any
-    try:
-        os.remove("{}/output.npy".format(deployment_dir))
-    except FileNotFoundError:
-        pass
-    # copy generated output to local
-    cmd = local_prefix + "scp -P{} {}@{}:{}/{}/output.npy {}".format(
-        pynq_port,
-        pynq_username,
-        pynq_ip,
-        pynq_target_dir,
-        deployment_folder,
-        deployment_dir,
-    )
-    bash_command = ["/bin/bash", "-c", cmd]
-    process_scp_out = subprocess.Popen(bash_command, stdout=subprocess.PIPE)
-    process_scp_out.communicate()
-    outp = np.load("{}/output.npy".format(deployment_dir))
-    execution_context[model.graph.output[0].name] = outp
diff --git a/src/finn/core/rtlsim_exec.py b/src/finn/core/rtlsim_exec.py
deleted file mode 100644
index d83bcd3a75dd0d2fc02315c72784e57348901a04..0000000000000000000000000000000000000000
--- a/src/finn/core/rtlsim_exec.py
+++ /dev/null
@@ -1,194 +0,0 @@
-# Copyright (c) 2020, Xilinx
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-#
-# * Redistributions of source code must retain the above copyright notice, this
-#   list of conditions and the following disclaimer.
-#
-# * Redistributions in binary form must reproduce the above copyright notice,
-#   this list of conditions and the following disclaimer in the documentation
-#   and/or other materials provided with the distribution.
-#
-# * Neither the name of FINN nor the names of its
-#   contributors may be used to endorse or promote products derived from
-#   this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-import os
-
-from finn.custom_op.registry import getCustomOp
-from finn.util.data_packing import npy_to_rtlsim_input, rtlsim_output_to_npy
-from finn.util.fpgadataflow import (
-    pyverilate_get_liveness_threshold_cycles,
-    pyverilate_stitched_ip,
-)
-
-try:
-    from pyverilator import PyVerilator
-except ModuleNotFoundError:
-    PyVerilator = None
-
-
-def rtlsim_exec(model, execution_context):
-    """Use PyVerilator to execute given model with stitched IP. The execution
-    context contains the input values."""
-
-    if PyVerilator is None:
-        raise ImportError("Installation of PyVerilator is required.")
-    # ensure stitched ip project already exists
-    assert os.path.isfile(
-        model.get_metadata_prop("wrapper_filename")
-    ), """The
-    file name from metadata property "wrapper_filename" doesn't exist."""
-    assert os.path.isdir(
-        model.get_metadata_prop("vivado_stitch_proj")
-    ), """The
-    directory from metadata property "vivado_stitch_proj" doesn't exist"""
-    trace_file = model.get_metadata_prop("rtlsim_trace")
-    # extract input shape
-    # TODO extend for multiple inputs
-    i_name = model.graph.input[0].name
-    i_tensor = execution_context[i_name]
-    i_dt = model.get_tensor_datatype(i_name)
-    first_node = getCustomOp(model.find_consumer(i_name))
-    i_stream_w = first_node.get_instream_width()
-    # convert input into time multiplexed shape
-    i_folded_shape = first_node.get_folded_input_shape()
-    batchsize = i_tensor.shape[0]
-    # override batch size for input
-    i_folded_shape = list(i_folded_shape)
-    i_folded_shape[0] = batchsize
-    i_folded_shape = tuple(i_folded_shape)
-    # TODO any other layout transformations need to happen here!
-    i_tensor = i_tensor.reshape(i_folded_shape)
-    # extract output shape
-    o_name = model.graph.output[0].name
-    o_shape = model.get_tensor_shape(o_name)
-    o_dt = model.get_tensor_datatype(o_name)
-    last_node = getCustomOp(model.find_producer(o_name))
-    o_folded_shape = last_node.get_folded_output_shape()
-    # override batch size from actual input
-    o_shape = list(o_shape)
-    o_shape[0] = batchsize
-    o_shape = tuple(o_shape)
-    o_folded_shape = list(o_folded_shape)
-    o_folded_shape[0] = batchsize
-    o_folded_shape = tuple(o_folded_shape)
-    o_stream_w = last_node.get_outstream_width()
-    packedBits = o_stream_w
-    targetBits = o_dt.bitwidth()
-    # pack input
-    packed_input = npy_to_rtlsim_input(i_tensor, i_dt, i_stream_w)
-    num_out_values = last_node.get_number_output_values()
-    num_out_values *= batchsize
-    # prepare pyverilator model
-    rtlsim_so = model.get_metadata_prop("rtlsim_so")
-    if (rtlsim_so is None) or (not os.path.isfile(rtlsim_so)):
-        sim = pyverilate_stitched_ip(model)
-        model.set_metadata_prop("rtlsim_so", sim.lib._name)
-    else:
-        sim = PyVerilator(rtlsim_so, auto_eval=False)
-    ret = _run_rtlsim(sim, packed_input, num_out_values, trace_file)
-    packed_output = ret[0]
-    model.set_metadata_prop("cycles_rtlsim", str(ret[1]))
-    # unpack output and put into context
-    o_folded_tensor = rtlsim_output_to_npy(
-        packed_output, None, o_dt, o_folded_shape, packedBits, targetBits
-    )
-    execution_context[o_name] = o_folded_tensor.reshape(o_shape)
-
-
-# TODO move the rtlsim functions below into a common location such as utils
-def _reset_rtlsim(sim):
-    """Sets reset input in pyverilator to zero, toggles the clock and set it
-    back to one"""
-    sim.io.ap_rst_n = 0
-    _toggle_clk(sim)
-    _toggle_clk(sim)
-    sim.io.ap_rst_n = 1
-    _toggle_clk(sim)
-    _toggle_clk(sim)
-
-
-def _toggle_clk(sim):
-    """Toggles the clock input in pyverilator once."""
-    sim.io.ap_clk = 0
-    sim.eval()
-    sim.io.ap_clk = 1
-    sim.eval()
-
-
-def _run_rtlsim(sim, inp, num_out_values, trace_file=None, reset=True):
-    """Runs the pyverilator simulation by passing the input values to the simulation,
-    toggle the clock and observing the execution time. Argument num_out_values contains
-    the number of expected output values, so the simulation is closed after all
-    outputs are calculated. Function contains also an observation loop that can
-    abort the simulation if no output value is produced after a certain time
-    (liveness_threshold from function pyverilate_get_liveness_threshold_cycles()
-    from finn.util.fpgadataflow)"""
-    inputs = inp
-    outputs = []
-    sim.io.m_axis_0_tready = 1
-
-    # observe if output is completely calculated
-    # observation_count will contain the number of cycles the calculation ran
-    output_observed = False
-    observation_count = 0
-
-    # avoid infinite looping of simulation by aborting when there is no change in
-    # output values after LIVENESS_THRESHOLD cycles
-    no_change_count = 0
-    old_outputs = outputs
-    liveness_threshold = pyverilate_get_liveness_threshold_cycles()
-
-    if trace_file is not None:
-        sim.start_vcd_trace(trace_file)
-    if reset:
-        _reset_rtlsim(sim)
-
-    while not (output_observed):
-        sim.io.s_axis_0_tvalid = 1 if len(inputs) > 0 else 0
-        sim.io.s_axis_0_tdata = inputs[0] if len(inputs) > 0 else 0
-        if sim.io.s_axis_0_tready == 1 and sim.io.s_axis_0_tvalid == 1:
-            inputs = inputs[1:]
-        if sim.io.m_axis_0_tvalid == 1 and sim.io.m_axis_0_tready == 1:
-            outputs = outputs + [sim.io.m_axis_0_tdata]
-        _toggle_clk(sim)
-
-        observation_count = observation_count + 1
-        no_change_count = no_change_count + 1
-
-        if len(outputs) == num_out_values:
-            cycles_rtlsim = observation_count
-            output_observed = True
-
-        if no_change_count == liveness_threshold:
-            if old_outputs == outputs:
-                if trace_file is not None:
-                    sim.flush_vcd_trace()
-                    sim.stop_vcd_trace()
-                raise Exception(
-                    "Error in simulation! Takes too long to produce output."
-                    "Consider setting the LIVENESS_THRESHOLD env.var. to a "
-                    "larger value."
-                )
-            else:
-                no_change_count = 0
-                old_outputs = outputs
-    if trace_file is not None:
-        sim.flush_vcd_trace()
-        sim.stop_vcd_trace()
-
-    return (outputs, cycles_rtlsim)
diff --git a/src/finn/core/throughput_test.py b/src/finn/core/throughput_test.py
deleted file mode 100644
index 1306edfa23a9b25de41d0592796b4a03ad4e6508..0000000000000000000000000000000000000000
--- a/src/finn/core/throughput_test.py
+++ /dev/null
@@ -1,157 +0,0 @@
-# Copyright (c) 2020, Xilinx
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-#
-# * Redistributions of source code must retain the above copyright notice, this
-#   list of conditions and the following disclaimer.
-#
-# * Redistributions in binary form must reproduce the above copyright notice,
-#   this list of conditions and the following disclaimer in the documentation
-#   and/or other materials provided with the distribution.
-#
-# * Neither the name of FINN nor the names of its
-#   contributors may be used to endorse or promote products derived from
-#   this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-import os
-import subprocess
-import numpy as np
-import warnings
-from finn.util.basic import gen_finn_dt_tensor
-from finn.core.rtlsim_exec import rtlsim_exec
-
-
-def throughput_test_remote(model, batchsize=1000):
-    """Runs the throughput test for the given model remotely on the pynq board.
-    The metadata properties related to the pynq board have to be set.
-    Returns a dictionary with results of the throughput test. Returns None
-    if the test fails."""
-
-    pynq_ip = model.get_metadata_prop("pynq_ip")
-    pynq_port = int(model.get_metadata_prop("pynq_port"))
-    pynq_username = model.get_metadata_prop("pynq_username")
-    pynq_password = model.get_metadata_prop("pynq_password")
-    pynq_target_dir = model.get_metadata_prop("pynq_target_dir")
-    deployment_dir = model.get_metadata_prop("pynq_deploy_dir")
-    # extracting last folder of absolute path (deployment_dir)
-    deployment_folder = os.path.basename(os.path.normpath(deployment_dir))
-    platform = model.get_metadata_prop("platform")
-    assert platform in ["alveo", "zynq-iodma"]
-    bitfile = model.get_metadata_prop("bitfile")
-    bitfile = os.path.basename(bitfile)
-    if pynq_password == "":
-        if "zynq" in platform:
-            raise Exception("PYNQ board remote exec needs password for sudo")
-        else:
-            local_prefix = ""  # assume we are using an ssh key
-            warnings.warn("Empty password, make sure you've set up an ssh key")
-    else:
-        local_prefix = "sshpass -p %s " % pynq_password
-
-    if platform == "alveo":
-        # Alveo can run without sudo but needs correct environment
-        remote_prefix = "conda activate finn-pynq-alveo; "
-    elif "zynq" in platform:
-        # PYNQ Zynq boards need to execute with sudo
-        remote_prefix = "echo %s | sudo -S " % pynq_password
-
-    # use platform attribute for correct remote execution
-    if platform == "alveo":
-        remote_cmd = "bash -ic 'bash alveo_run.sh throughput_test %d' \"" % batchsize
-    else:
-        remote_cmd = (
-            "python3.6 driver.py --exec_mode=throughput_test --batchsize={} "
-            "--bitfile={} --inputfile=input.npy --outputfile=output.npy "
-            '--platform={} "'
-        ).format(batchsize, bitfile, platform)
-    cmd = (
-        local_prefix + 'ssh {}@{} -p {} "cd {}/{}; ' + remote_prefix + remote_cmd
-    ).format(pynq_username, pynq_ip, pynq_port, pynq_target_dir, deployment_folder)
-    bash_command = ["/bin/bash", "-c", cmd]
-    process_throughput_test = subprocess.Popen(bash_command, stdout=subprocess.PIPE)
-    process_throughput_test.communicate()
-
-    # remove any pre-existing metrics file
-    try:
-        os.remove("{}/nw_metrics.txt".format(deployment_dir))
-    except FileNotFoundError:
-        pass
-
-    cmd = local_prefix + "scp -P{} {}@{}:{}/{}/nw_metrics.txt {}".format(
-        pynq_port,
-        pynq_username,
-        pynq_ip,
-        pynq_target_dir,
-        deployment_folder,
-        deployment_dir,
-    )
-    bash_command = ["/bin/bash", "-c", cmd]
-    process_compile = subprocess.Popen(bash_command, stdout=subprocess.PIPE)
-    process_compile.communicate()
-
-    try:
-        with open("{}/nw_metrics.txt".format(deployment_dir), "r") as file:
-            res = eval(file.read())
-        return res
-    except FileNotFoundError:
-        return None
-
-
-def throughput_test_rtlsim(model, batchsize=100):
-    """Runs a throughput test for the given IP-stitched model. When combined
-    with tracing, useful to determine bottlenecks and required FIFO sizes."""
-
-    assert (
-        model.get_metadata_prop("exec_mode") == "rtlsim"
-    ), """Top-level exec_mode
-    metadata_prop must be set to rtlsim"""
-
-    # create random input
-    iname = model.graph.input[0].name
-    ishape = model.get_tensor_shape(iname)
-    ishape_batch = ishape
-    ishape_batch[0] = batchsize
-    idt = model.get_tensor_datatype(iname)
-    dummy_input = gen_finn_dt_tensor(idt, ishape_batch)
-    # compute input/output sizes
-    oname = model.graph.output[0].name
-    oshape = model.get_tensor_shape(oname)
-    oshape_batch = oshape
-    oshape_batch[0] = batchsize
-    odt = model.get_tensor_datatype(oname)
-    i_bytes = (np.prod(ishape_batch) * idt.bitwidth()) / 8
-    o_bytes = (np.prod(oshape_batch) * odt.bitwidth()) / 8
-    # make empty exec context and insert input
-    ctx = model.make_empty_exec_context()
-    ctx[iname] = dummy_input
-    # remove liveness threshold, launch rtlsim
-    os.environ["LIVENESS_THRESHOLD"] = "-1"
-    rtlsim_exec(model, ctx)
-    # extract metrics
-    cycles = int(model.get_metadata_prop("cycles_rtlsim"))
-    clk_ns = float(model.get_metadata_prop("clk_ns"))
-    fclk_mhz = 1 / (clk_ns * 0.001)
-    runtime_s = (cycles * clk_ns) * (10 ** -9)
-    res = dict()
-    res["cycles"] = cycles
-    res["runtime[ms]"] = runtime_s * 1000
-    res["throughput[images/s]"] = batchsize / runtime_s
-    res["DRAM_in_bandwidth[Mb/s]"] = i_bytes * 0.000001 / runtime_s
-    res["DRAM_out_bandwidth[Mb/s]"] = o_bytes * 0.000001 / runtime_s
-    res["fclk[mhz]"] = fclk_mhz
-    res["N"] = batchsize
-
-    return res
diff --git a/src/finn/custom_op/__init__.py b/src/finn/custom_op/__init__.py
index 4ae7b9ebffaab6ca6be04b8d73f647b2db22dc78..06fc7e5659d8f55f63fe40380abac70dc74c0a4d 100644
--- a/src/finn/custom_op/__init__.py
+++ b/src/finn/custom_op/__init__.py
@@ -26,101 +26,53 @@
 # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
-from abc import ABC, abstractmethod
-from finn.util.basic import get_by_name
-import onnx.helper as helper
+from pkgutil import extend_path
 
+__path__ = extend_path(__path__, __name__)
 
-class CustomOp(ABC):
-    """CustomOp class all custom op nodes are based on. Contains different functions
-    every custom node should have. Some as abstract methods, these have to be
-    filled when writing a new custom op node."""
+from finn.custom_op.registry import custom_op
 
-    def __init__(self, onnx_node):
-        super().__init__()
-        self.onnx_node = onnx_node
+# make sure new CustomOp subclasses are imported here so that they get
+# registered and plug in correctly into the infrastructure
+from finn.custom_op.fpgadataflow.convolutioninputgenerator import (
+    ConvolutionInputGenerator,
+)
+from finn.custom_op.fpgadataflow.downsampler import DownSampler
+from finn.custom_op.fpgadataflow.streamingfclayer_batch import StreamingFCLayer_Batch
+from finn.custom_op.fpgadataflow.streamingmaxpool_batch import StreamingMaxPool_Batch
+from finn.custom_op.fpgadataflow.streamingfifo import StreamingFIFO
+from finn.custom_op.fpgadataflow.tlastmarker import TLastMarker
+from finn.custom_op.fpgadataflow.streamingdatawidthconverter_batch import (
+    StreamingDataWidthConverter_Batch,
+)
+from finn.custom_op.fpgadataflow.globalaccpool_batch import GlobalAccPool_Batch
+from finn.custom_op.fpgadataflow.pool_batch import Pool_Batch
+from finn.custom_op.fpgadataflow.fmpadding_batch import FMPadding_Batch
+from finn.custom_op.fpgadataflow.thresholding_batch import Thresholding_Batch
+from finn.custom_op.fpgadataflow.addstreams_batch import AddStreams_Batch
+from finn.custom_op.fpgadataflow.labelselect_batch import LabelSelect_Batch
+from finn.custom_op.fpgadataflow.duplicatestreams_batch import DuplicateStreams_Batch
+from finn.custom_op.fpgadataflow.vector_vector_activate_batch import (
+    Vector_Vector_Activate_Batch,
+)
+from finn.custom_op.fpgadataflow.channelwise_op_batch import ChannelwiseOp_Batch
+from finn.custom_op.fpgadataflow.iodma import IODMA
 
-    def get_nodeattr(self, name):
-        """Get a node attribute by name. Data is stored inside the ONNX node's
-        AttributeProto container. Attribute must be part of get_nodeattr_types.
-        Default value is returned if attribute is not set."""
-        try:
-            (dtype, req, def_val) = self.get_nodeattr_types()[name]
-            attr = get_by_name(self.onnx_node.attribute, name)
-            if attr is not None:
-                # dtype indicates which ONNX Attribute member to use
-                # (such as i, f, s...)
-                ret = attr.__getattribute__(dtype)
-                if dtype == "s":
-                    # decode string attributes
-                    ret = ret.decode("utf-8")
-                return ret
-            else:
-                if req:
-                    raise Exception(
-                        """Required attribute %s unspecified in
-                    a %s node"""
-                        % (name, self.onnx_node.op_type)
-                    )
-                else:
-                    # not set, return default value
-                    return def_val
-        except KeyError:
-            raise AttributeError("Op has no such attribute: " + name)
 
-    def set_nodeattr(self, name, value):
-        """Set a node attribute by name. Data is stored inside the ONNX node's
-        AttributeProto container. Attribute must be part of get_nodeattr_types."""
-        try:
-            (dtype, req, def_val) = self.get_nodeattr_types()[name]
-            attr = get_by_name(self.onnx_node.attribute, name)
-            if attr is not None:
-                # dtype indicates which ONNX Attribute member to use
-                # (such as i, f, s...)
-                if dtype == "s":
-                    # encode string attributes
-                    value = value.encode("utf-8")
-                attr.__setattr__(dtype, value)
-            else:
-                # not set, create and insert AttributeProto
-                attr_proto = helper.make_attribute(name, value)
-                self.onnx_node.attribute.append(attr_proto)
-        except KeyError:
-            raise AttributeError("Op has no such attribute: " + name)
-
-    @abstractmethod
-    def get_nodeattr_types(self):
-        """Returns a dict of permitted attributes for node, where:
-            returned_dict[attribute_name] = (dtype, require, default_value)
-            - dtype indicates which member of the ONNX AttributeProto
-            will be utilized
-            - require indicates whether this attribute is required
-            - default_val indicates the default value that will be used if the
-            attribute is not set
-        """
-        pass
-
-    @abstractmethod
-    def make_shape_compatible_op(self, model):
-        """Returns a standard ONNX op which is compatible with this CustomOp
-        for performing shape inference."""
-        pass
-
-    @abstractmethod
-    def infer_node_datatype(self, model):
-        """Set the DataType annotations corresponding to the outputs of this
-        node."""
-        pass
-
-    @abstractmethod
-    def execute_node(self, context, graph):
-        """Execute this CustomOp instance, given the execution context and
-        ONNX graph."""
-        pass
-
-    @abstractmethod
-    def verify_node(self):
-        """Verifies that all attributes the node needs are there and
-        that particular attributes are set correctly. Also checks if
-        the number of inputs is equal to the expected number."""
-        pass
+custom_op["DownSampler"] = DownSampler
+custom_op["StreamingMaxPool_Batch"] = StreamingMaxPool_Batch
+custom_op["StreamingFCLayer_Batch"] = StreamingFCLayer_Batch
+custom_op["ConvolutionInputGenerator"] = ConvolutionInputGenerator
+custom_op["TLastMarker"] = TLastMarker
+custom_op["StreamingDataWidthConverter_Batch"] = StreamingDataWidthConverter_Batch
+custom_op["StreamingFIFO"] = StreamingFIFO
+custom_op["GlobalAccPool_Batch"] = GlobalAccPool_Batch
+custom_op["Pool_Batch"] = Pool_Batch
+custom_op["FMPadding_Batch"] = FMPadding_Batch
+custom_op["Thresholding_Batch"] = Thresholding_Batch
+custom_op["AddStreams_Batch"] = AddStreams_Batch
+custom_op["LabelSelect_Batch"] = LabelSelect_Batch
+custom_op["DuplicateStreams_Batch"] = DuplicateStreams_Batch
+custom_op["Vector_Vector_Activate_Batch"] = Vector_Vector_Activate_Batch
+custom_op["ChannelwiseOp_Batch"] = ChannelwiseOp_Batch
+custom_op["IODMA"] = IODMA
diff --git a/src/finn/custom_op/debugmarker.py b/src/finn/custom_op/debugmarker.py
deleted file mode 100644
index 6c02f0dc81295dfc5c3060d549b6853eac1d0bac..0000000000000000000000000000000000000000
--- a/src/finn/custom_op/debugmarker.py
+++ /dev/null
@@ -1,66 +0,0 @@
-# Copyright (c) 2020, Xilinx
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-#
-# * Redistributions of source code must retain the above copyright notice, this
-#   list of conditions and the following disclaimer.
-#
-# * Redistributions in binary form must reproduce the above copyright notice,
-#   this list of conditions and the following disclaimer in the documentation
-#   and/or other materials provided with the distribution.
-#
-# * Neither the name of FINN nor the names of its
-#   contributors may be used to endorse or promote products derived from
-#   this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-from finn.custom_op import CustomOp
-from onnx import helper
-
-
-class DebugMarker(CustomOp):
-    def get_nodeattr_types(self):
-        return {"export_debug_name": ("s", True, "")}
-
-    def make_shape_compatible_op(self, model):
-        node = self.onnx_node
-        return helper.make_node("Identity", [node.input[0]], [node.output[0]])
-
-    def infer_node_datatype(self, model):
-        node = self.onnx_node
-        # data type stays the same
-        dtype = model.get_tensor_datatype(node.input[0])
-        model.set_tensor_datatype(node.output[0], dtype)
-        # create quantization annotation for debug marker
-        model.set_tensor_datatype(self.get_nodeattr("export_debug_name"), dtype)
-
-    def execute_node(self, context, graph):
-        node = self.onnx_node
-        inp_name = node.input[0]
-        out_name = node.output[0]
-        inp = context[inp_name]
-        context[out_name] = inp
-        # insert debug marker output as separate tensor
-        context[self.get_nodeattr("export_debug_name")] = inp
-
-    def verify_node(self):
-        info_messages = []
-        # verify that "domain" is set to "finn"
-        domain_value = self.onnx_node.domain
-        if domain_value == "finn":
-            info_messages.append("Attribute domain is set correctly")
-        else:
-            info_messages.append('Attribute domain should be set to "finn"')
-        return info_messages
diff --git a/src/finn/custom_op/fpgadataflow/__init__.py b/src/finn/custom_op/fpgadataflow/__init__.py
index 7de6cce936ee54d58d9a526e926ff79dcd35b90d..d093a410e5b30a039688ec1a5264e59b92edfd8a 100644
--- a/src/finn/custom_op/fpgadataflow/__init__.py
+++ b/src/finn/custom_op/fpgadataflow/__init__.py
@@ -25,12 +25,16 @@
 # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+# namespace package, extend path
+from pkgutil import extend_path
+
+__path__ = extend_path(__path__, __name__)
 
 from abc import abstractmethod
 import numpy as np
 import os
 import subprocess
-from finn.custom_op import CustomOp
+from finn.custom_op.base import CustomOp
 from finn.util.basic import (
     CppBuilder,
     make_build_dir,
@@ -321,7 +325,7 @@ class HLSCustomOp(CustomOp):
         builder = CppBuilder()
         # to enable additional debug features please uncommand the next line
         # builder.append_includes("-DDEBUG")
-        builder.append_includes("-I/workspace/finn/src/finn/data/cpp")
+        builder.append_includes("-I/workspace/finn/src/finn/qnn-data/cpp")
         builder.append_includes("-I/workspace/cnpy/")
         builder.append_includes("-I/workspace/finn-hlslib")
         builder.append_includes("-I{}/include".format(os.environ["VIVADO_PATH"]))
diff --git a/src/finn/custom_op/im2col.py b/src/finn/custom_op/im2col.py
deleted file mode 100644
index 8ed0041704d421dab587f08bcbcd9e739e8434e9..0000000000000000000000000000000000000000
--- a/src/finn/custom_op/im2col.py
+++ /dev/null
@@ -1,203 +0,0 @@
-import numpy as np
-from onnx import TensorProto, helper
-
-from finn.custom_op import CustomOp
-import finn.util.basic as util
-from finn.core.datatype import DataType
-
-# adapted from A. Karpathy's CS231 im2col code
-# utilities to generate a patch matrix from a multichannel image
-# of shape (batches, channels, height, width)
-
-
-def compute_conv_output_dim(ifm_dim, k, stride, pad=0):
-    """Returns spatial output dimension size for convolution with given params."""
-    return int(((ifm_dim + 2 * pad - k) / stride) + 1)
-
-
-def get_im2col_indices_nchw(
-    x_shape, field_height, field_width, padding=0, stride_y=1, stride_x=1
-):
-    """Returns im2col indices."""
-    # First figure out what the size of the output should be
-    N, C, H, W = x_shape
-    out_height = compute_conv_output_dim(H, field_height, stride_y, padding)
-    out_width = compute_conv_output_dim(W, field_width, stride_x, padding)
-
-    i0 = np.repeat(np.arange(field_height), field_width)
-    i0 = np.tile(i0, C)
-    i1 = stride_y * np.repeat(np.arange(out_height), out_width)
-    j0 = np.tile(np.arange(field_width), field_height * C)
-    j1 = stride_x * np.tile(np.arange(out_width), out_height)
-    i = i0.reshape(-1, 1) + i1.reshape(1, -1)
-    j = j0.reshape(-1, 1) + j1.reshape(1, -1)
-
-    k = np.repeat(np.arange(C), field_height * field_width).reshape(-1, 1)
-
-    return (k, i, j)
-
-
-def im2col_indices_nchw(
-    x, field_height, field_width, padding=0, stride_y=1, stride_x=1, pad_val=0
-):
-    """Performs im2col on x with given field height and width, as well as values
-    for padding and stride size.
-    Returns result of im2col."""
-    # Zero-pad the input
-    p = padding
-    x_padded = np.pad(
-        x, ((0, 0), (0, 0), (p, p), (p, p)), mode="constant", constant_values=pad_val
-    )
-
-    k, i, j = get_im2col_indices_nchw(
-        x.shape, field_height, field_width, padding, stride_y, stride_x
-    )
-
-    cols = x_padded[:, k, i, j]
-    C = x.shape[1]
-    cols = cols.transpose(1, 2, 0).reshape(field_height * field_width * C, -1)
-    return cols
-
-
-# ONNX i/o tensor shape assumptions for Im2Col:
-# input 0 is the input vector, shape (1, ih, iw, ifm)
-# output 0 is the output vector, shape (1, oh, ow, k*k*ifm)
-# where:
-# * ih, iw are the height and width of the input image
-# * oh, ow are the height and width of the output (lowered) image
-# * ifm is the number of input channels
-# * k is the convolutional kernel size
-
-# note: for the innermost (dot product) dimension of k*k*ifm, we
-# assume an internal ordering (k, k, ifm)
-
-
-class Im2Col(CustomOp):
-    def get_nodeattr_types(self):
-        return {
-            "stride": ("i", True, 1),
-            "kernel_size": ("i", True, 1),
-            "input_shape": ("s", True, ""),
-            "pad_amount": ("i", False, 0),
-            "pad_value": ("i", False, 0),
-            # depthwise: if != 0, infer ConvolutionInputGenerator with depthwise == 1
-            "depthwise": ("i", False, 0),
-        }
-
-    def make_shape_compatible_op(self, model):
-        k = self.get_nodeattr("kernel_size")
-        stride = self.get_nodeattr("stride")
-        ishape = self.get_nodeattr("input_shape")
-        pad = self.get_nodeattr("pad_amount")
-
-        # convert string into list of integers
-        ishape = ishape.strip("(")
-        ishape = ishape.strip(")")
-        ishape = ishape.split(",")
-        for i in range(0, len(ishape)):
-            ishape[i] = int(ishape[i])
-
-        # extract all necessary information and determine output dimensions
-        ifm_ch = ishape[-1]
-        assert len(ishape) == 4, "Unexpected input shape for Im2Col"
-        assert ishape[1] == ishape[2], "Im2Col for non-square images unsupported"
-        ifm_dim = ishape[1]
-        ofm_dim = compute_conv_output_dim(ifm_dim, k, stride, pad)
-
-        # implement tensor with correct shape
-        values = np.random.randn(1, ofm_dim, ofm_dim, k * k * ifm_ch).astype(np.float32)
-        return helper.make_node(
-            "Constant",
-            inputs=[],
-            outputs=[self.onnx_node.output[0]],
-            value=helper.make_tensor(
-                name="const_tensor",
-                data_type=TensorProto.FLOAT,
-                dims=values.shape,
-                vals=values.flatten().astype(float),
-            ),
-        )
-
-    def infer_node_datatype(self, model):
-        node = self.onnx_node
-        # data type stays the same
-        dtype = model.get_tensor_datatype(node.input[0])
-        model.set_tensor_datatype(node.output[0], dtype)
-
-    def execute_node(self, context, graph):
-        node = self.onnx_node
-        k = self.get_nodeattr("kernel_size")
-        stride = self.get_nodeattr("stride")
-        pad = self.get_nodeattr("pad_amount")
-        pad_val = self.get_nodeattr("pad_value")
-        iname = node.input[0]
-        x = context[iname]
-        qnt_annotations = graph.quantization_annotation
-        ret = util.get_by_name(qnt_annotations, iname, "tensor_name")
-        ret = util.get_by_name(ret.quant_parameter_tensor_names, "finn_datatype", "key")
-        idt = DataType[ret.value]
-        if pad != 0:
-            assert idt.allowed(pad_val), "Im2Col dtype must allow pad_val"
-        # check that input is NHWC
-        assert x.ndim == 4, "Unexpected number of input dims for Im2Col"
-        N, H, W, C = x.shape
-        assert H == W, "Unexpected input shape for Im2Col"
-        out_dim = compute_conv_output_dim(H, k, stride, pad)
-        # internally convert input to NCHW
-        x = x.transpose(0, 3, 1, 2)
-        # call NCHW im2col implementation
-        ret = im2col_indices_nchw(x, k, k, pad, stride, stride, pad_val=pad_val)
-        # result shape is (k*k*N, out_dim*out_dim), convert to NCHW
-        ret = ret.reshape(N, C, k, k, out_dim, out_dim)
-        # (N=0,C=1,kh=2,kw=3,H=4,W=5) -> (N=0,H=4,W=5,kh=2,kw=3,C=1)
-        ret = ret.transpose(0, 4, 5, 2, 3, 1)
-        ret = ret.reshape(N, out_dim, out_dim, k * k * C)
-
-        # ret = ret.reshape(N, k * k * C, out_dim, out_dim)
-        # convert output back to NHWC
-        # ret = ret.transpose(0, 2, 3, 1)
-        context[node.output[0]] = ret
-
-    def verify_node(self):
-        node = self.onnx_node
-
-        info_messages = []
-
-        # verify number of attributes
-        num_of_attr = 3
-        if len(node.attribute) == num_of_attr:
-            info_messages.append("The number of attributes is correct")
-        else:
-            info_messages.append(
-                """The number of attributes is incorrect,
-            {} should have {} attributes""".format(
-                    node.op_type, num_of_attr
-                )
-            )
-
-        # verify that "domain" is set to "finn"
-        domain_value = node.domain
-        if domain_value == "finn":
-            info_messages.append("Attribute domain is set correctly")
-        else:
-            info_messages.append('Attribute domain should be set to "finn"')
-
-        # verify that all necessary attributes exist
-        try:
-            self.get_nodeattr("stride")
-            self.get_nodeattr("kernel_size")
-            info_messages.append("All necessary attributes exist")
-        except Exception:
-            info_messages.append(
-                """The necessary attributes do not exist.
-                Im2Col needs the following attributes:
-                stride, kernel_size"""
-            )
-
-        # verify the number of inputs
-        if len(node.input) == 1:
-            info_messages.append("The number of inputs is correct")
-        else:
-            info_messages.append("{} needs 1 data input".format(node.op_type))
-
-        return info_messages
diff --git a/src/finn/custom_op/maxpoolnhwc.py b/src/finn/custom_op/maxpoolnhwc.py
deleted file mode 100644
index c623e40075e0ed6836dc9494ee5effb4539a46af..0000000000000000000000000000000000000000
--- a/src/finn/custom_op/maxpoolnhwc.py
+++ /dev/null
@@ -1,126 +0,0 @@
-# Copyright (c) 2020, Xilinx
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-#
-# * Redistributions of source code must retain the above copyright notice, this
-#   list of conditions and the following disclaimer.
-#
-# * Redistributions in binary form must reproduce the above copyright notice,
-#   this list of conditions and the following disclaimer in the documentation
-#   and/or other materials provided with the distribution.
-#
-# * Neither the name of FINN nor the names of its
-#   contributors may be used to endorse or promote products derived from
-#   this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-from finn.custom_op import CustomOp
-import numpy as np
-from onnx import helper, TensorProto
-from finn.core.modelwrapper import ModelWrapper
-
-
-def compute_pool_output_dim(ifm_dim, k, stride, pad=0):
-    "Return spatial output dimension size for pooling with given params."
-    return int(((ifm_dim + 2 * pad - k) / stride) + 1)
-
-
-class MaxPoolNHWC(CustomOp):
-    # a MaxPool node, but using the NHWC data layout
-
-    def get_nodeattr_types(self):
-        # no specific attributes for MaxPoolNHWC
-        return {
-            "kernel_shape": ("ints", True, []),
-            "pads": ("ints", True, []),
-            "strides": ("ints", True, []),
-        }
-
-    def make_shape_compatible_op(self, model):
-        node = self.onnx_node
-        iname = node.input[0]
-        ishape = model.get_tensor_shape(iname)
-        kernel_shape = self.get_nodeattr("kernel_shape")
-        pads = self.get_nodeattr("pads")
-        strides = self.get_nodeattr("strides")
-        assert len(kernel_shape) == 2, "Non-2D MaxPoolNHWC not supported"
-        assert pads[0] == pads[2], "Uneven padding not supported"
-        assert pads[1] == pads[3], "Uneven padding not supported"
-        (n, hi, wi, c) = ishape
-        ho = compute_pool_output_dim(hi, kernel_shape[0], strides[0], pads[0])
-        wo = compute_pool_output_dim(wi, kernel_shape[1], strides[1], pads[2])
-        oshape = (n, ho, wo, c)
-        # implement tensor with correct shape
-        values = np.random.randn(*oshape).astype(np.float32)
-        return helper.make_node(
-            "Constant",
-            inputs=[],
-            outputs=[self.onnx_node.output[0]],
-            value=helper.make_tensor(
-                name="const_tensor",
-                data_type=TensorProto.FLOAT,
-                dims=values.shape,
-                vals=values.flatten().astype(float),
-            ),
-        )
-
-    def infer_node_datatype(self, model):
-        node = self.onnx_node
-        # data type stays the same
-        dtype = model.get_tensor_datatype(node.input[0])
-        model.set_tensor_datatype(node.output[0], dtype)
-
-    def execute_node(self, context, graph):
-        node = self.onnx_node
-        inp_name = node.input[0]
-        out_name = node.output[0]
-        inp = context[inp_name]
-        dummy_out = context[out_name]
-        # convert i/o NHWC -> NCHW
-        inp = np.transpose(inp, (0, 3, 1, 2))
-        dummy_out = np.transpose(dummy_out, (0, 3, 1, 2))
-        # execute as regular MaxPool
-        node.domain = ""
-        node.op_type = "MaxPool"
-        inp_vi = helper.make_tensor_value_info(inp_name, TensorProto.FLOAT, inp.shape)
-        out_vi = helper.make_tensor_value_info(
-            out_name, TensorProto.FLOAT, dummy_out.shape
-        )
-        tmp_graph = helper.make_graph(
-            nodes=[node], name="tmp_graph", inputs=[inp_vi], outputs=[out_vi]
-        )
-        tmp_model = helper.make_model(tmp_graph, producer_name="finn")
-        tmp_model = ModelWrapper(tmp_model)
-        new_ctx = {inp_name: inp}
-        from finn.core.onnx_exec import execute_onnx
-
-        ret = execute_onnx(tmp_model, new_ctx)
-        # restore original node props
-        node.domain = "finn"
-        node.op_type = "MaxPoolNHWC"
-        outp = ret[out_name]
-        # convert output NCHW -> NHWC
-        outp = np.transpose(outp, (0, 2, 3, 1))
-        context[out_name] = outp
-
-    def verify_node(self):
-        info_messages = []
-        # verify that "domain" is set to "finn"
-        domain_value = self.onnx_node.domain
-        if domain_value == "finn":
-            info_messages.append("Attribute domain is set correctly")
-        else:
-            info_messages.append('Attribute domain should be set to "finn"')
-        return info_messages
diff --git a/src/finn/custom_op/multithreshold.py b/src/finn/custom_op/multithreshold.py
deleted file mode 100644
index bc0a454cdf847d124b12c940b029f51bf2d3e778..0000000000000000000000000000000000000000
--- a/src/finn/custom_op/multithreshold.py
+++ /dev/null
@@ -1,176 +0,0 @@
-# Copyright (c) 2020, Xilinx
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-#
-# * Redistributions of source code must retain the above copyright notice, this
-#   list of conditions and the following disclaimer.
-#
-# * Redistributions in binary form must reproduce the above copyright notice,
-#   this list of conditions and the following disclaimer in the documentation
-#   and/or other materials provided with the distribution.
-#
-# * Neither the name of FINN nor the names of its
-#   contributors may be used to endorse or promote products derived from
-#   this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-import numpy as np
-import onnx.helper as helper
-
-from finn.core.datatype import DataType
-from finn.custom_op import CustomOp
-
-
-def multithreshold(v, thresholds, out_scale=None, out_bias=None):
-    """Given a set of threshold values t={t_0, t_1 ... t_n} the successive
-    thresholding maps any real number x to an integer in the interval [0, n],
-    where the returned integer is the number of thresholds x is greater than
-    or equal to.
-
-    The output tensor will be scaled by out_scale and biased by out_bias."""
-    # the inputs are expected to be in the shape (N,C,H,W) or (N, C)
-    # the MultiThreshold node supports a data_layout attribute that can be set
-    # to 'NHWC' to support (N,H,W,C) data layout mode for in-out as well
-    # N : Batch size
-    # C : Number of channels
-    # H : Heigth of the input images
-    # W : Width of the input images
-    #
-    # the thresholds are expected to be in the shape (C, B)
-    # C : Number of channels (must be the same value as C in input tensor
-    #     or 1 if all channels use the same threshold value)
-    # B : Desired activation steps => i.e. for 4-bit activation,
-    #     B=7 (2^(n)-1 and n=4)
-    # the output tensor will be scaled by out_scale and biased by out_bias
-    # assert threshold shape
-    is_global_threshold = thresholds.shape[0] == 1
-    assert (
-        v.shape[1] == thresholds.shape[0]
-    ) or is_global_threshold, """"Threshold
-    shape incorrect"""
-    # save the required shape sizes for the loops (N, C and B)
-    num_batch = v.shape[0]
-    num_channel = v.shape[1]
-    num_act = thresholds.shape[1]
-    # reshape inputs to enable channel-wise reading
-    vr = v.reshape((v.shape[0], v.shape[1], -1))
-    # initiate output tensor
-    ret = np.zeros_like(vr)
-    # iterate over thresholds channel-wise
-    for t in range(num_channel):
-        channel_thresh = thresholds[0] if is_global_threshold else thresholds[t]
-        # iterate over batches
-        for b in range(num_batch):
-            # iterate over the different thresholds for one channel
-            for a in range(num_act):
-                ret[b][t] += (vr[b][t] >= channel_thresh[a]).astype(int)
-
-    if out_scale is None:
-        out_scale = 1.0
-    if out_bias is None:
-        out_bias = 0.0
-    return out_scale * ret.reshape(v.shape) + out_bias
-
-
-class MultiThreshold(CustomOp):
-    """Class that corresponds to a multithresholding node."""
-
-    def get_nodeattr_types(self):
-        return {
-            "out_dtype": ("s", True, ""),
-            "out_scale": ("f", False, 1.0),
-            "out_bias": ("f", False, 0.0),
-            "data_layout": ("s", False, "NCHW"),
-        }
-
-    def make_shape_compatible_op(self, model):
-        node = self.onnx_node
-        return helper.make_node("Relu", [node.input[0]], [node.output[0]])
-
-    def infer_node_datatype(self, model):
-        node = self.onnx_node
-        odt = self.get_nodeattr("out_dtype")
-        model.set_tensor_datatype(node.output[0], DataType[odt])
-
-    def execute_node(self, context, graph):
-        node = self.onnx_node
-        # save inputs
-        v = context[node.input[0]]
-        thresholds = context[node.input[1]]
-        # retrieve attributes if output scaling is used
-        out_scale = self.get_nodeattr("out_scale")
-        out_bias = self.get_nodeattr("out_bias")
-        # transpose input if NHWC data layout is chosen
-        data_layout = self.get_nodeattr("data_layout")
-        if data_layout == "NHWC":
-            if v.ndim == 4:
-                # NHWC -> NCHW
-                v = np.transpose(v, (0, 3, 1, 2))
-            elif v.ndim == 2:
-                # no HW dimension means NHWC and NCHW layouts are equivalent
-                pass
-            else:
-                raise Exception(
-                    "Unknown data_layout and input ndim"
-                    " combination for MultiThreshold."
-                )
-        # calculate output
-        output = multithreshold(v, thresholds, out_scale, out_bias)
-        # setting context according to output
-        if data_layout == "NHWC":
-            if output.ndim == 4:
-                # NCHW -> NHWC
-                output = np.transpose(output, (0, 2, 3, 1))
-            elif output.ndim == 2:
-                # no HW dimension means NHWC and NCHW layouts are equivalent
-                pass
-            else:
-                raise Exception(
-                    "Unknown data_layout and output ndim"
-                    " combination for MultiThreshold."
-                )
-        context[node.output[0]] = output
-
-    def verify_node(self):
-        info_messages = []
-
-        # verify that "domain" is set to "finn"
-        domain_value = self.onnx_node.domain
-        if domain_value == "finn":
-            info_messages.append("Attribute domain is set correctly")
-        else:
-            info_messages.append('Attribute domain should be set to "finn"')
-
-        # verify that all necessary attributes exist
-        try:
-            self.get_nodeattr("out_dtype")
-            info_messages.append("All necessary attributes exist")
-        except Exception:
-            info_messages.append(
-                """The necessary attributes do not exist.
-                MultiThreshold needs the following attributes:
-                out_scale, out_bias, out_dtype"""
-            )
-
-        # verify the number of inputs
-        if len(self.onnx_node.input) == 2:
-            info_messages.append("The number of inputs is correct")
-        else:
-            info_messages.append(
-                """MultiThreshold needs 2 inputs
-                    (data input and threshold values)"""
-            )
-
-        return info_messages
diff --git a/src/finn/custom_op/quantavgpool2d.py b/src/finn/custom_op/quantavgpool2d.py
deleted file mode 100644
index 28d01069264d883f3afc400808470f5f303be799..0000000000000000000000000000000000000000
--- a/src/finn/custom_op/quantavgpool2d.py
+++ /dev/null
@@ -1,136 +0,0 @@
-import numpy as np
-from onnx import TensorProto, helper
-import onnxruntime as rt
-
-from finn.custom_op import CustomOp
-from finn.core.datatype import DataType
-from finn.custom_op.maxpoolnhwc import compute_pool_output_dim
-
-
-class QuantAvgPool2d(CustomOp):
-    """Class that corresponds to the quantized average pooling
-    layer from brevitas"""
-
-    def get_nodeattr_types(self):
-        return {
-            "stride": ("i", True, 1),
-            "kernel": ("i", True, 1),
-            "ibits": ("i", True, 1),
-            "obits": ("i", True, 1),
-            # determines if values are signed (set to "1") or unsigned ("0")
-            "signed": ("i", True, 0),
-            # data layout attribute can be set to "NCHW" or "NHWC"
-            "data_layout": ("s", False, "NCHW"),
-        }
-
-    def make_shape_compatible_op(self, model):
-        node = self.onnx_node
-        k = self.get_nodeattr("kernel")
-        s = self.get_nodeattr("stride")
-        data_layout = self.get_nodeattr("data_layout")
-        if data_layout == "NCHW":
-            return helper.make_node(
-                "AveragePool",
-                inputs=[node.input[0]],
-                outputs=[node.output[0]],
-                kernel_shape=[k, k],
-                strides=[s, s],
-            )
-        elif data_layout == "NHWC":
-            iname = node.input[0]
-            ishape = model.get_tensor_shape(iname)
-            (n, hi, wi, c) = ishape
-            ho = compute_pool_output_dim(hi, k, s)
-            wo = compute_pool_output_dim(wi, k, s)
-            oshape = (n, ho, wo, c)
-            # implement tensor with correct shape
-            values = np.random.randn(*oshape).astype(np.float32)
-            return helper.make_node(
-                "Constant",
-                inputs=[],
-                outputs=[node.output[0]],
-                value=helper.make_tensor(
-                    name="const_tensor",
-                    data_type=TensorProto.FLOAT,
-                    dims=values.shape,
-                    vals=values.flatten().astype(float),
-                ),
-            )
-
-        else:
-            raise Exception(
-                """Datalayout for QuantAvgPool2d is set to an invalid value.
-                    Has to be set to "NCHW" or "NHWC"."""
-            )
-
-    def infer_node_datatype(self, model):
-        node = self.onnx_node
-        bw = self.get_nodeattr("obits")
-        if bw in [2, 4, 8, 16, 32]:
-            if self.get_nodeattr("signed") == 0:
-                dtype = DataType["UINT%d" % bw]
-            else:
-                dtype = DataType["INT%d" % bw]
-        else:
-            raise Exception("Unsupported output datatype for QuantAvgPool2d")
-        model.set_tensor_datatype(node.output[0], dtype)
-
-    def get_accum_size(self):
-        ibits = self.get_nodeattr("ibits")
-        k = self.get_nodeattr("kernel")
-        max_value = 2 ** ibits - 1
-        max_value = max_value * k * k
-        max_bit_width = int(max_value).bit_length()
-        return max_bit_width
-
-    def get_shifts(self):
-        shift_bits = self.get_accum_size() - self.get_nodeattr("obits")
-        shift_bits = shift_bits if shift_bits >= 0 else 0
-        return shift_bits
-
-    def execute_node(self, context, graph):
-        # create a standard average pooling node to help calculate the result
-        node = self.onnx_node
-        k = self.get_nodeattr("kernel")
-        s = self.get_nodeattr("stride")
-        inp_values = context[node.input[0]]
-        oshape = context[node.output[0]].shape
-        if self.get_nodeattr("data_layout") == "NHWC":
-            inp_values = inp_values.transpose(0, 3, 1, 2)
-            oshape = (context[node.output[0]]).transpose(0, 3, 1, 2).shape
-        ishape = inp_values.shape
-        inp = helper.make_tensor_value_info(node.input[0], TensorProto.FLOAT, ishape)
-        outp = helper.make_tensor_value_info(node.output[0], TensorProto.FLOAT, oshape)
-        node_avgpool = helper.make_node(
-            "AveragePool",
-            inputs=[node.input[0]],
-            outputs=[node.output[0]],
-            kernel_shape=[k, k],
-            strides=[s, s],
-        )
-        graph_avgpool = helper.make_graph(
-            nodes=[node_avgpool],
-            name="single-avgpool-exec",
-            inputs=[inp],
-            outputs=[outp],
-        )
-        model_avgpool = helper.make_model(graph_avgpool)
-        idict = {node.input[0]: inp_values}
-        sess = rt.InferenceSession(model_avgpool.SerializeToString())
-        result_temp = sess.run(None, idict)
-        # remove scaling introduced by average
-        result_temp = result_temp[0] * (k * k)
-        result = np.right_shift(result_temp.astype(int), self.get_shifts())
-        if self.get_nodeattr("data_layout") == "NHWC":
-            result = result.transpose(0, 2, 3, 1)
-        context[node.output[0]] = result.astype(np.float32)
-
-    def verify_node(self):
-        info_messages = []
-        # verify that "domain" is set to "finn"
-        domain_value = self.onnx_node.domain
-        if domain_value == "finn":
-            info_messages.append("Attribute domain is set correctly")
-        else:
-            info_messages.append('Attribute domain should be set to "finn"')
-        return info_messages
diff --git a/src/finn/custom_op/registry.py b/src/finn/custom_op/registry.py
deleted file mode 100644
index ecf2a711f17ac35c9bf8cb081fb4dc6d9bb6c01e..0000000000000000000000000000000000000000
--- a/src/finn/custom_op/registry.py
+++ /dev/null
@@ -1,100 +0,0 @@
-# Copyright (c) 2020, Xilinx
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-#
-# * Redistributions of source code must retain the above copyright notice, this
-#   list of conditions and the following disclaimer.
-#
-# * Redistributions in binary form must reproduce the above copyright notice,
-#   this list of conditions and the following disclaimer in the documentation
-#   and/or other materials provided with the distribution.
-#
-# * Neither the name of FINN nor the names of its
-#   contributors may be used to endorse or promote products derived from
-#   this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-# make sure new CustomOp subclasses are imported here so that they get
-# registered and plug in correctly into the infrastructure
-from finn.custom_op.fpgadataflow.convolutioninputgenerator import (
-    ConvolutionInputGenerator,
-)
-from finn.custom_op.fpgadataflow.downsampler import DownSampler
-from finn.custom_op.fpgadataflow.streamingfclayer_batch import StreamingFCLayer_Batch
-from finn.custom_op.fpgadataflow.streamingmaxpool_batch import StreamingMaxPool_Batch
-from finn.custom_op.fpgadataflow.streamingfifo import StreamingFIFO
-from finn.custom_op.im2col import Im2Col
-from finn.custom_op.fpgadataflow.tlastmarker import TLastMarker
-from finn.custom_op.multithreshold import MultiThreshold
-from finn.custom_op.streamingdataflowpartition import StreamingDataflowPartition
-from finn.custom_op.xnorpopcount import XnorPopcountMatMul
-from finn.custom_op.maxpoolnhwc import MaxPoolNHWC
-from finn.custom_op.fpgadataflow.streamingdatawidthconverter_batch import (
-    StreamingDataWidthConverter_Batch,
-)
-from finn.custom_op.fpgadataflow.globalaccpool_batch import GlobalAccPool_Batch
-from finn.custom_op.fpgadataflow.pool_batch import Pool_Batch
-from finn.custom_op.fpgadataflow.fmpadding_batch import FMPadding_Batch
-from finn.custom_op.fpgadataflow.thresholding_batch import Thresholding_Batch
-from finn.custom_op.fpgadataflow.addstreams_batch import AddStreams_Batch
-from finn.custom_op.fpgadataflow.labelselect_batch import LabelSelect_Batch
-from finn.custom_op.quantavgpool2d import QuantAvgPool2d
-from finn.custom_op.fpgadataflow.duplicatestreams_batch import DuplicateStreams_Batch
-from finn.custom_op.fpgadataflow.vector_vector_activate_batch import (
-    Vector_Vector_Activate_Batch,
-)
-from finn.custom_op.fpgadataflow.channelwise_op_batch import ChannelwiseOp_Batch
-from finn.custom_op.fpgadataflow.iodma import IODMA
-from finn.custom_op.debugmarker import DebugMarker
-
-# create a mapping of all known CustomOp names and classes
-custom_op = {}
-
-custom_op["MultiThreshold"] = MultiThreshold
-custom_op["DownSampler"] = DownSampler
-custom_op["XnorPopcountMatMul"] = XnorPopcountMatMul
-custom_op["Im2Col"] = Im2Col
-custom_op["StreamingMaxPool_Batch"] = StreamingMaxPool_Batch
-custom_op["StreamingFCLayer_Batch"] = StreamingFCLayer_Batch
-custom_op["ConvolutionInputGenerator"] = ConvolutionInputGenerator
-custom_op["TLastMarker"] = TLastMarker
-custom_op["StreamingDataflowPartition"] = StreamingDataflowPartition
-custom_op["MaxPoolNHWC"] = MaxPoolNHWC
-custom_op["StreamingDataWidthConverter_Batch"] = StreamingDataWidthConverter_Batch
-custom_op["StreamingFIFO"] = StreamingFIFO
-custom_op["GlobalAccPool_Batch"] = GlobalAccPool_Batch
-custom_op["Pool_Batch"] = Pool_Batch
-custom_op["FMPadding_Batch"] = FMPadding_Batch
-custom_op["Thresholding_Batch"] = Thresholding_Batch
-custom_op["AddStreams_Batch"] = AddStreams_Batch
-custom_op["LabelSelect_Batch"] = LabelSelect_Batch
-custom_op["QuantAvgPool2d"] = QuantAvgPool2d
-custom_op["DuplicateStreams_Batch"] = DuplicateStreams_Batch
-custom_op["Vector_Vector_Activate_Batch"] = Vector_Vector_Activate_Batch
-custom_op["ChannelwiseOp_Batch"] = ChannelwiseOp_Batch
-custom_op["IODMA"] = IODMA
-custom_op["DebugMarker"] = DebugMarker
-
-
-def getCustomOp(node):
-    "Return a FINN CustomOp instance for the given ONNX node, if it exists."
-    op_type = node.op_type
-    try:
-        # lookup op_type in registry of CustomOps
-        inst = custom_op[op_type](node)
-        return inst
-    except KeyError:
-        # exception if op_type is not supported
-        raise Exception("Custom op_type %s is currently not supported." % op_type)
diff --git a/src/finn/custom_op/streamingdataflowpartition.py b/src/finn/custom_op/streamingdataflowpartition.py
deleted file mode 100644
index 31cd38fea3c5a9e88084c3332d46aebdb065f800..0000000000000000000000000000000000000000
--- a/src/finn/custom_op/streamingdataflowpartition.py
+++ /dev/null
@@ -1,96 +0,0 @@
-# Copyright (c) 2020, Xilinx
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-#
-# * Redistributions of source code must retain the above copyright notice, this
-#   list of conditions and the following disclaimer.
-#
-# * Redistributions in binary form must reproduce the above copyright notice,
-#   this list of conditions and the following disclaimer in the documentation
-#   and/or other materials provided with the distribution.
-#
-# * Neither the name of FINN nor the names of its
-#   contributors may be used to endorse or promote products derived from
-#   this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-from finn.custom_op import CustomOp
-
-
-class StreamingDataflowPartition(CustomOp):
-    """Class that corresponds to the meta/container node StreamingDataflowPartition
-    which is a placeholder for a group of fpgadataflow nodes that have been separated
-    out into a FINN-ONNX model of its own. Note that is does not produce any HLS or
-    bitfile by itself."""
-
-    def get_nodeattr_types(self):
-        return {
-            "model": ("s", True, ""),
-            "res_estimate": ("s", False, ""),
-            "res_hls": ("s", False, ""),
-            "res_synth": ("s", False, ""),
-        }
-
-    def make_shape_compatible_op(self, model):
-        pass
-
-    def infer_node_datatype(self, model):
-        pass
-
-    def execute_node(self, context, graph):
-        # TODO add RPC execution with synthesized bitfile?
-        # whole-design rtlsim with PyVerilator may also be an alternative
-        pass
-
-    def verify_node(self):
-        info_messages = []
-
-        # verify number of attributes
-        num_of_attr = 1
-        if len(self.onnx_node.attribute) == num_of_attr:
-            info_messages.append("The number of attributes is correct")
-        else:
-            info_messages.append(
-                """The number of attributes is incorrect,
-            {} should have {} attributes""".format(
-                    self.onnx_node.op_type, num_of_attr
-                )
-            )
-
-        # verify that "domain" is set to "finn"
-        domain_value = self.onnx_node.domain
-        if domain_value == "finn":
-            info_messages.append("Attribute domain is set correctly")
-        else:
-            info_messages.append('Attribute domain should be set to "finn"')
-
-        # verify that all necessary attributes exist
-        try:
-            self.get_nodeattr("model")
-            info_messages.append("All necessary attributes exist")
-        except Exception:
-            info_messages.append(
-                """The necessary attributes do not exist.
-                StreamingDataflowPartition needs the following attribute(s):
-                model"""
-            )
-
-        # verify the number of inputs
-        if len(self.onnx_node.input) >= 1:
-            info_messages.append("The number of inputs is correct")
-        else:
-            info_messages.append("StreamingDataflowPartition needs 1 data input")
-
-        return info_messages
diff --git a/src/finn/custom_op/xnorpopcount.py b/src/finn/custom_op/xnorpopcount.py
deleted file mode 100644
index 199b7a5d9938b662b8f23e30275768b427fd87a7..0000000000000000000000000000000000000000
--- a/src/finn/custom_op/xnorpopcount.py
+++ /dev/null
@@ -1,131 +0,0 @@
-# Copyright (c) 2020, Xilinx
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-#
-# * Redistributions of source code must retain the above copyright notice, this
-#   list of conditions and the following disclaimer.
-#
-# * Redistributions in binary form must reproduce the above copyright notice,
-#   this list of conditions and the following disclaimer in the documentation
-#   and/or other materials provided with the distribution.
-#
-# * Neither the name of FINN nor the names of its
-#   contributors may be used to endorse or promote products derived from
-#   this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-import numpy as np
-import onnx.helper as helper
-
-from finn.core.datatype import DataType
-from finn.custom_op import CustomOp
-
-
-def xnorpopcountmatmul(inp0, inp1):
-    """Simulates XNOR-popcount matrix multiplication as a regular bipolar
-    matrix multiplication followed by some post processing."""
-    # extract the operand shapes
-    # (M, K0) = inp0.shape
-    # (K1, N) = inp1.shape
-    K0 = inp0.shape[-1]
-    K1 = inp1.shape[0]
-    # make sure shapes are compatible with matmul
-    assert K0 == K1, "Matrix shapes are not compatible with matmul."
-    K = K0
-    # convert binary inputs to bipolar
-    inp0_bipolar = 2.0 * inp0 - 1.0
-    inp1_bipolar = 2.0 * inp1 - 1.0
-    # call regular numpy matrix multiplication
-    out = np.matmul(inp0_bipolar, inp1_bipolar)
-    # XNOR-popcount does not produce the regular dot product result --
-    # it returns the number of +1s after XNOR. let P be the number of +1s
-    # and N be the number of -1s. XNOR-popcount returns P, whereas the
-    # regular dot product result from numpy is P-N, so we need to apply
-    # some correction.
-    # out = P-N
-    # K = P+N
-    # out + K = 2P, so P = (out + K)/2
-    return (out + K) * 0.5
-
-
-class XnorPopcountMatMul(CustomOp):
-    """Class that corresponds to a XNOR-popcount matrix
-    multiplication node."""
-
-    def get_nodeattr_types(self):
-        return {}
-
-    def make_shape_compatible_op(self, model):
-        node = self.onnx_node
-        return helper.make_node(
-            "MatMul", [node.input[0], node.input[1]], [node.output[0]]
-        )
-
-    def infer_node_datatype(self, model):
-        node = self.onnx_node
-        # ensure inputs are binary
-        assert (
-            model.get_tensor_datatype(node.input[0]) == DataType["BINARY"]
-        ), """FINN
-        DataType of first input is not set to BINARY as it should be."""
-        assert (
-            model.get_tensor_datatype(node.input[1]) == DataType["BINARY"]
-        ), """FINN
-        DataTypes of second input is not set to BINARY as it should be."""
-        # XNOR-popcount produces unsigned integers, assume uint32
-        model.set_tensor_datatype(node.output[0], DataType["UINT32"])
-
-    def execute_node(self, context, graph):
-        node = self.onnx_node
-        # save inputs
-        inp0 = context[node.input[0]]
-        inp1 = context[node.input[1]]
-        # calculate output
-        output = xnorpopcountmatmul(inp0, inp1)
-        # set context according to output name
-        context[node.output[0]] = output
-
-    def verify_node(self):
-        info_messages = []
-
-        # verify number of attributes
-        num_of_attr = 0
-        if len(self.onnx_node.attribute) == num_of_attr:
-            info_messages.append("The number of attributes is correct")
-        else:
-            info_messages.append(
-                """The number of attributes is incorrect,
-            {} should have {} attributes""".format(
-                    self.onnx_node.op_type, num_of_attr
-                )
-            )
-
-        # verify that "domain" is set to "finn"
-        domain_value = self.onnx_node.domain
-        if domain_value == "finn":
-            info_messages.append("Attribute domain is set correctly")
-        else:
-            info_messages.append('Attribute domain should be set to "finn"')
-
-        # verify that all necessary attributes exist
-        info_messages.append("XnorPopcountMatMul should not have any attributes")
-
-        # verify the number of inputs
-        if len(self.onnx_node.input) == 2:
-            info_messages.append("The number of inputs is correct")
-        else:
-            info_messages.append("XnorPopcountMatMul needs 2 data inputs")
-
-        return info_messages
diff --git a/src/finn/data/onnx/mnist-conv/model.onnx b/src/finn/data/onnx/mnist-conv/model.onnx
deleted file mode 100644
index fc1a3f733c6e6243dd23dacb125b7a372de55a50..0000000000000000000000000000000000000000
Binary files a/src/finn/data/onnx/mnist-conv/model.onnx and /dev/null differ
diff --git a/src/finn/data/onnx/mnist-conv/test_data_set_0/input_0.pb b/src/finn/data/onnx/mnist-conv/test_data_set_0/input_0.pb
deleted file mode 100644
index f0072d51a480af615e92312608f75993be9f1136..0000000000000000000000000000000000000000
Binary files a/src/finn/data/onnx/mnist-conv/test_data_set_0/input_0.pb and /dev/null differ
diff --git a/src/finn/data/onnx/mnist-conv/test_data_set_0/output_0.pb b/src/finn/data/onnx/mnist-conv/test_data_set_0/output_0.pb
deleted file mode 100644
index a6f4cdf92e27aaab21e44098b5b28e16048098e2..0000000000000000000000000000000000000000
--- a/src/finn/data/onnx/mnist-conv/test_data_set_0/output_0.pb
+++ /dev/null
@@ -1,2 +0,0 @@
-
-J(ãêsDU®ÄŒtÍEÚ'DWQeÄYôÐÄQôÄ3vÂNKBÄñ³Ä
\ No newline at end of file
diff --git a/src/finn/analysis/fpgadataflow/__init__.py b/src/finn/qnn-data/__init__.py
similarity index 100%
rename from src/finn/analysis/fpgadataflow/__init__.py
rename to src/finn/qnn-data/__init__.py
diff --git a/src/finn/data/cifar10/cifar10-test-data-class3.npz b/src/finn/qnn-data/cifar10/cifar10-test-data-class3.npz
similarity index 100%
rename from src/finn/data/cifar10/cifar10-test-data-class3.npz
rename to src/finn/qnn-data/cifar10/cifar10-test-data-class3.npz
diff --git a/src/finn/data/cpp/npy2apintstream.hpp b/src/finn/qnn-data/cpp/npy2apintstream.hpp
similarity index 100%
rename from src/finn/data/cpp/npy2apintstream.hpp
rename to src/finn/qnn-data/cpp/npy2apintstream.hpp
diff --git a/src/finn/data/onnx/finn-hls-model/finn-hls-onnx-model.onnx b/src/finn/qnn-data/onnx/finn-hls-model/finn-hls-onnx-model.onnx
similarity index 100%
rename from src/finn/data/onnx/finn-hls-model/finn-hls-onnx-model.onnx
rename to src/finn/qnn-data/onnx/finn-hls-model/finn-hls-onnx-model.onnx
diff --git a/src/finn/data/onnx/finn-hls-model/tfc_w1_a1_after_conv_to_hls.onnx b/src/finn/qnn-data/onnx/finn-hls-model/tfc_w1_a1_after_conv_to_hls.onnx
similarity index 100%
rename from src/finn/data/onnx/finn-hls-model/tfc_w1_a1_after_conv_to_hls.onnx
rename to src/finn/qnn-data/onnx/finn-hls-model/tfc_w1_a1_after_conv_to_hls.onnx
diff --git a/src/finn/transformation/__init__.py b/src/finn/transformation/__init__.py
deleted file mode 100644
index e9f5fe15f6bdefe1e739394495f67a972ccff669..0000000000000000000000000000000000000000
--- a/src/finn/transformation/__init__.py
+++ /dev/null
@@ -1,115 +0,0 @@
-# Copyright (c) 2020, Xilinx
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-#
-# * Redistributions of source code must retain the above copyright notice, this
-#   list of conditions and the following disclaimer.
-#
-# * Redistributions in binary form must reproduce the above copyright notice,
-#   this list of conditions and the following disclaimer in the documentation
-#   and/or other materials provided with the distribution.
-#
-# * Neither the name of FINN nor the names of its
-#   contributors may be used to endorse or promote products derived from
-#   this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-"""
-Guide to writing FINN transformations
--------------------------------------
-
-* Your transformation must inherit the Transformation abstract base class.
-* Your transformation's apply function should take in a ModelWrapper, and return
-  a tuple with (transformed_model: ModelWrapper, model_was_changed: Bool)
-* The transformations are meant to be applied using the .transform function
-  in ModelWrapper. This makes a deep copy of the input model by default, so
-  you don't have to.
-* model_was_changed indicates whether your transformation made any changes to
-  the model. If you know your transformation needs to be called only once and
-  repeated calls have no further effect, you can return False even if the model
-  was changed.
-* You MUST return model_was_changed=False at some point when your transformation
-  is called multiple times, otherwise apply_repeated() will loop infinitely.
-* If you cannot guarantee that the transformation will reach a fixed point,
-  you must declare this, return model_was_changed = False and let the user
-  manually re-apply the transform.
-"""
-
-from abc import ABC, abstractmethod
-from finn.util.basic import get_num_default_workers
-import multiprocessing as mp
-
-
-class Transformation(ABC):
-    """Transformation class all transformations are based on. Contains only
-    abstract method apply() every transformation has to fill."""
-
-    def __init__(self):
-        super().__init__()
-
-    @abstractmethod
-    def apply(self, model):
-        pass
-
-
-class NodeLocalTransformation(Transformation):
-    """
-    Parent class for transformations, which can be executed locally to one node
-    by accessing and modifying the attributes of only that node.
-    This class can then automatically parallelize the transformation.
-    Transformations sublcassing NodeLocalTransformation must implement the
-    abstract method applyNodeLocal().
-
-    To control the degree of parallelization, specify the num_workers argument
-    in the constructor, using one of the following values:
-    * None: use NUM_DEFAULT_WORKERS environment variable
-    * 0: use all available CPU cores
-    * (any other int>0): set number of parallel workers
-    """
-
-    def __init__(self, num_workers=None):
-        super().__init__()
-        if num_workers is None:
-            self._num_workers = get_num_default_workers()
-        else:
-            self._num_workers = num_workers
-        assert self._num_workers >= 0, "Number of workers must be nonnegative."
-        if self._num_workers == 0:
-            self._num_workers = mp.cpu_count()
-
-    @abstractmethod
-    def applyNodeLocal(self, node):
-        pass
-
-    def apply(self, model):
-        # Remove old nodes from the current model
-        old_nodes = []
-        for i in range(len(model.graph.node)):
-            old_nodes.append(model.graph.node.pop())
-
-        # Execute transformation in parallel
-        with mp.Pool(self._num_workers) as p:
-            new_nodes_and_bool = p.map(self.applyNodeLocal, old_nodes, chunksize=1)
-
-        # extract nodes and check if the transformation needs to run again
-        # Note: .pop() had initially reversed the node order
-        run_again = False
-        for node, run in reversed(new_nodes_and_bool):
-            # Reattach new nodes to old model
-            model.graph.node.append(node)
-            if run is True:
-                run_again = True
-
-        return (model, run_again)
diff --git a/src/finn/transformation/batchnorm_to_affine.py b/src/finn/transformation/batchnorm_to_affine.py
deleted file mode 100644
index 401c5916415cd327a52a43f89c076bd7abd40647..0000000000000000000000000000000000000000
--- a/src/finn/transformation/batchnorm_to_affine.py
+++ /dev/null
@@ -1,114 +0,0 @@
-# Copyright (c) 2020, Xilinx
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-#
-# * Redistributions of source code must retain the above copyright notice, this
-#   list of conditions and the following disclaimer.
-#
-# * Redistributions in binary form must reproduce the above copyright notice,
-#   this list of conditions and the following disclaimer in the documentation
-#   and/or other materials provided with the distribution.
-#
-# * Neither the name of FINN nor the names of its
-#   contributors may be used to endorse or promote products derived from
-#   this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-import numpy as np
-from onnx import TensorProto
-from onnx import helper as oh
-
-from finn.transformation import Transformation
-from finn.transformation.infer_shapes import InferShapes
-
-
-class BatchNormToAffine(Transformation):
-    """Replaces any test-time BatchNorm layers with Mul-Add layers."""
-
-    def apply(self, model):
-        graph = model.graph
-        node_ind = 0
-        graph_modified = False
-        for n in graph.node:
-            node_ind += 1
-            if n.op_type == "BatchNormalization":
-                graph_modified = True
-                bn_input = n.input[0]
-                bn_output = n.output[0]
-                # extract batchnorm parameters as numpy arrays
-                scale = model.get_initializer(n.input[1])
-                bias = model.get_initializer(n.input[2])
-                mean = model.get_initializer(n.input[3])
-                variance = model.get_initializer(n.input[4])
-                epsilon = 1e-5
-                # find A and B to compute batchnorm as affine transpose Ax+B
-                # TODO is a division by moving avg factor needed for variance?
-                A = scale / np.sqrt(epsilon + variance)
-                B = bias - (A * mean)
-                # see if we have surrounding Unsqueeze/Squeeze nodes we can remove
-                producer = model.find_producer(bn_input)
-                if producer is not None:
-                    if producer.op_type == "Unsqueeze":
-                        bn_input = producer.input[0]
-                consumer = model.find_consumer(bn_output)
-                if consumer is not None:
-                    if consumer.op_type == "Squeeze":
-                        bn_output = consumer.output[0]
-                data_shape = model.get_tensor_shape(bn_input)
-                assert A.ndim == B.ndim, "Unexpected mul/add dims in BatchNormToAffine"
-                assert (
-                    len(data_shape) >= A.ndim
-                ), "Unexpected number of dims found in BatchNormToAffine"
-                # reshape the mul/add constants to match the data shape/dims
-                # by adding (1,) dimensions to the right
-                n_spatial_dims = len(data_shape) - 2
-                target_shape = (1, -1) + tuple(1 for i in range(n_spatial_dims))
-                A = A.reshape(target_shape)
-                B = B.reshape(target_shape)
-                # create value_info and initializers for Mul and Add constants
-                mul_const = oh.make_tensor_value_info(
-                    model.make_new_valueinfo_name(), TensorProto.FLOAT, A.shape
-                )
-                graph.value_info.append(mul_const)
-                model.set_initializer(mul_const.name, A)
-                mul_output = oh.make_tensor_value_info(
-                    model.make_new_valueinfo_name(), TensorProto.FLOAT, data_shape
-                )
-                graph.value_info.append(mul_output)
-                add_const = oh.make_tensor_value_info(
-                    model.make_new_valueinfo_name(), TensorProto.FLOAT, B.shape
-                )
-                graph.value_info.append(add_const)
-                model.set_initializer(add_const.name, B)
-                # create Mul and Add nodes to replace the batchnorm
-                mul_node = oh.make_node(
-                    "Mul", [bn_input, mul_const.name], [mul_output.name]
-                )
-                add_node = oh.make_node(
-                    "Add", [mul_output.name, add_const.name], [bn_output]
-                )
-                # insert where the batchnorm is to preserve topological ordering
-                graph.node.insert(node_ind, mul_node)
-                graph.node.insert(node_ind + 1, add_node)
-                # remove old nodes
-                graph.node.remove(n)
-                if consumer is not None:
-                    if consumer.op_type == "Squeeze":
-                        graph.node.remove(consumer)
-                if producer is not None:
-                    if producer.op_type == "Unsqueeze":
-                        graph.node.remove(producer)
-        model = model.transform(InferShapes())
-        return (model, graph_modified)
diff --git a/src/finn/transformation/bipolar_to_xnor.py b/src/finn/transformation/bipolar_to_xnor.py
deleted file mode 100644
index 80f2a73351f8548c99efd8dedd8a04d44c8558a3..0000000000000000000000000000000000000000
--- a/src/finn/transformation/bipolar_to_xnor.py
+++ /dev/null
@@ -1,153 +0,0 @@
-# Copyright (c) 2020, Xilinx
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-#
-# * Redistributions of source code must retain the above copyright notice, this
-#   list of conditions and the following disclaimer.
-#
-# * Redistributions in binary form must reproduce the above copyright notice,
-#   this list of conditions and the following disclaimer in the documentation
-#   and/or other materials provided with the distribution.
-#
-# * Neither the name of FINN nor the names of its
-#   contributors may be used to endorse or promote products derived from
-#   this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-import numpy as np
-import warnings
-from onnx import TensorProto
-from onnx import helper as oh
-
-from finn.core.datatype import DataType
-from finn.transformation import Transformation
-from finn.transformation.infer_shapes import InferShapes
-from finn.transformation.infer_datatypes import InferDataTypes
-from finn.util.basic import get_by_name
-from finn.custom_op.registry import getCustomOp
-
-
-class ConvertBipolarMatMulToXnorPopcount(Transformation):
-    """Convert MatMul nodes with all-bipolar inputs to XnorPopcountMatMul
-    and associated result correction."""
-
-    def apply(self, model):
-        graph = model.graph
-        node_ind = 0
-        graph_modified = False
-        for n in graph.node:
-            node_ind += 1
-            if n.op_type == "MatMul":
-                mm_input = n.input[0]
-                mm_weight = n.input[1]
-                mm_output = n.output[0]
-                i_bp = model.get_tensor_datatype(mm_input) == DataType.BIPOLAR
-                w_bp = model.get_tensor_datatype(mm_weight) == DataType.BIPOLAR
-                if i_bp and w_bp:
-                    # find producing threshold node and adjust output to binary
-                    def find_prod_mt(x):
-                        is_mt = x.op_type == "MultiThreshold"
-                        is_bp = False
-                        if is_mt:
-                            dt = get_by_name(x.attribute, "out_dtype").s
-                            is_bp = dt.decode("utf-8") == "BIPOLAR"
-                        return is_mt and is_bp
-
-                    mt_chain = model.find_upstream(mm_input, find_prod_mt)
-                    if len(mt_chain) == 0:
-                        if mm_input == graph.input[0].name:
-                            # change input datatype to BINARY
-                            model.set_tensor_datatype(mm_input, DataType.BINARY)
-                            graph_modified = True
-                            warnings.warn(
-                                """IMPORTANT: Changing graph input DataType
-                            to BINARY instead of BIPOLAR. Ensure this is respected
-                            when checking for correctness.
-                            """
-                            )
-                        else:
-                            raise Exception(
-                                """Could not find upstream bipolar
-                                   MultiThreshold, and the MatMul is not the
-                                   first node on graph input. Unable to convert
-                                   input tensor from BIPOLAR to BINARY."""
-                            )
-                    else:
-                        graph_modified = True
-                        mt = mt_chain[-1]
-                        mt_inst = getCustomOp(mt)
-                        # ensure old scale/bias were correct for BIPOLAR
-                        scale_ok = mt_inst.get_nodeattr("out_scale") == 2.0
-                        bias_ok = mt_inst.get_nodeattr("out_bias") == -1.0
-                        assert (
-                            scale_ok and bias_ok
-                        ), """Unexpected scale/bias
-                        attributes for BIPOLAR MultiThreshold node."""
-                        # start conversion, set MT output to binary
-                        # (this is what XnorPopcountMatMul expects)
-                        mt_inst.set_nodeattr("out_dtype", "BINARY")
-                        mt_inst.set_nodeattr("out_scale", 1.0)
-                        mt_inst.set_nodeattr("out_bias", 0.0)
-                        model.set_tensor_datatype(mm_input, DataType.BINARY)
-                    # change node type and domain
-                    n.op_type = "XnorPopcountMatMul"
-                    n.domain = "finn"
-                    # convert weights into binary (-1,+1) -> (0,1)
-                    Wbin = (model.get_initializer(mm_weight) + 1) / 2
-                    # extract vector length (common matrix dim)
-                    K = Wbin.shape[0]
-                    model.set_initializer(mm_weight, Wbin)
-                    model.set_tensor_datatype(mm_weight, DataType.BINARY)
-                    # make new output node with correct shape
-                    mm_out_shape = model.get_tensor_shape(mm_output)
-                    xnorpcout = oh.make_tensor_value_info(
-                        model.make_new_valueinfo_name(), TensorProto.FLOAT, mm_out_shape
-                    )
-                    n.output[0] = xnorpcout.name
-                    model.set_tensor_datatype(xnorpcout.name, DataType.UINT32)
-                    # add mul-add nodes to produce correct dot product result
-                    # need to derive P-N from P and K = P+N
-                    # so we need 2*P-K
-                    A = np.asarray([2.0], dtype=np.float32)
-                    B = np.asarray([-K], dtype=np.float32)
-                    # create value_info and initializers for Mul and Add constants
-                    mul_const = oh.make_tensor_value_info(
-                        model.make_new_valueinfo_name(), TensorProto.FLOAT, A.shape
-                    )
-                    graph.value_info.append(mul_const)
-                    model.set_initializer(mul_const.name, A)
-                    mul_output = oh.make_tensor_value_info(
-                        model.make_new_valueinfo_name(), TensorProto.FLOAT, mm_out_shape
-                    )
-                    graph.value_info.append(mul_output)
-                    add_const = oh.make_tensor_value_info(
-                        model.make_new_valueinfo_name(), TensorProto.FLOAT, B.shape
-                    )
-                    graph.value_info.append(add_const)
-                    model.set_initializer(add_const.name, B)
-                    # create Mul and Add nodes to replace the batchnorm
-                    mul_node = oh.make_node(
-                        "Mul", [xnorpcout.name, mul_const.name], [mul_output.name]
-                    )
-                    add_node = oh.make_node(
-                        "Add", [mul_output.name, add_const.name], [mm_output]
-                    )
-                    # insert where the batchnorm is to preserve topological ordering
-                    graph.node.insert(node_ind, mul_node)
-                    graph.node.insert(node_ind + 1, add_node)
-        if graph_modified:
-            model = model.transform(InferShapes())
-            model = model.transform(InferDataTypes())
-        return (model, graph_modified)
diff --git a/src/finn/transformation/change_datalayout.py b/src/finn/transformation/change_datalayout.py
deleted file mode 100644
index d5b393a25e57122b059a44f70904a6dbe5bbaa3f..0000000000000000000000000000000000000000
--- a/src/finn/transformation/change_datalayout.py
+++ /dev/null
@@ -1,110 +0,0 @@
-# Copyright (c) 2020, Xilinx
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-#
-# * Redistributions of source code must retain the above copyright notice, this
-#   list of conditions and the following disclaimer.
-#
-# * Redistributions in binary form must reproduce the above copyright notice,
-#   this list of conditions and the following disclaimer in the documentation
-#   and/or other materials provided with the distribution.
-#
-# * Neither the name of FINN nor the names of its
-#   contributors may be used to endorse or promote products derived from
-#   this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-from onnx import helper, TensorProto
-
-from finn.transformation import Transformation
-from finn.transformation.infer_shapes import InferShapes
-from finn.util.basic import get_by_name
-
-
-class ChangeDataLayoutQuantAvgPool2d(Transformation):
-    """Replace QuantAvgPool2d with datalayout (N,C,H,W) with Transpose nodes
-    and QuantAvgPool2dNHWC with datalayout (N,H,W,C)"""
-
-    def apply(self, model):
-        graph = model.graph
-        node_ind = 0
-        graph_modified = False
-        for n in graph.node:
-            node_ind += 1
-            if n.op_type == "QuantAvgPool2d" and (
-                get_by_name(n.attribute, "data_layout") is None
-                or get_by_name(n.attribute, "data_layout").s.decode("UTF-8") == "NCHW"
-            ):
-                graph_modified = True
-                node_input = n.input[0]
-                node_output = n.output[0]
-                s = get_by_name(n.attribute, "stride").i
-                k = get_by_name(n.attribute, "kernel").i
-                ibits = get_by_name(n.attribute, "ibits").i
-                obits = get_by_name(n.attribute, "obits").i
-                signed = get_by_name(n.attribute, "signed").i
-                batchsize = model.get_tensor_shape(n.input[0])[0]  # assume NCHW
-                channels = model.get_tensor_shape(n.input[0])[1]  # assume NCHW
-                idim = model.get_tensor_shape(n.input[0])[-1]  # assume NCHW
-                odim = model.get_tensor_shape(n.output[0])[-1]  # assume NCHW
-
-                # create new nodes
-                # NCHW -> NHWC
-                # create new intermediate values
-                inp_trans_out = helper.make_tensor_value_info(
-                    model.make_new_valueinfo_name(),
-                    TensorProto.FLOAT,
-                    (batchsize, idim, idim, channels),  # NHWC
-                )
-                graph.value_info.append(inp_trans_out)
-                inp_trans_out = inp_trans_out.name
-                quantavg_out = helper.make_tensor_value_info(
-                    model.make_new_valueinfo_name(),
-                    TensorProto.FLOAT,
-                    (batchsize, odim, odim, channels),
-                )
-                graph.value_info.append(quantavg_out)
-                quantavg_out = quantavg_out.name
-                inp_trans_node = helper.make_node(
-                    "Transpose", [node_input], [inp_trans_out], perm=[0, 2, 3, 1]
-                )
-                quantavg_node = helper.make_node(
-                    "QuantAvgPool2d",
-                    [inp_trans_out],
-                    [quantavg_out],
-                    domain="finn",
-                    stride=s,
-                    kernel=k,
-                    ibits=ibits,
-                    obits=obits,
-                    signed=signed,
-                    data_layout="NHWC",
-                )
-                # NHWC -> NCHW
-                out_trans_node = helper.make_node(
-                    "Transpose", [quantavg_out], [node_output], perm=[0, 3, 1, 2]
-                )
-                # insert nodes
-                graph.node.insert(node_ind, inp_trans_node)
-                graph.node.insert(node_ind + 1, quantavg_node)
-                graph.node.insert(node_ind + 2, out_trans_node)
-                # remove old nodes
-                graph.node.remove(n)
-
-                # set shapes
-                model.set_tensor_shape(inp_trans_out, (batchsize, idim, idim, channels))
-                model.set_tensor_shape(quantavg_out, (batchsize, odim, odim, channels))
-        model = model.transform(InferShapes())
-        return (model, graph_modified)
diff --git a/src/finn/transformation/double_to_single_float.py b/src/finn/transformation/double_to_single_float.py
deleted file mode 100644
index 4f7eb1cc82b346321eb6ee711b87c2c98ccfdaee..0000000000000000000000000000000000000000
--- a/src/finn/transformation/double_to_single_float.py
+++ /dev/null
@@ -1,45 +0,0 @@
-# Copyright (c) 2020, Xilinx
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-#
-# * Redistributions of source code must retain the above copyright notice, this
-#   list of conditions and the following disclaimer.
-#
-# * Redistributions in binary form must reproduce the above copyright notice,
-#   this list of conditions and the following disclaimer in the documentation
-#   and/or other materials provided with the distribution.
-#
-# * Neither the name of FINN nor the names of its
-#   contributors may be used to endorse or promote products derived from
-#   this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-from finn.transformation import Transformation
-import numpy as np
-
-
-class DoubleToSingleFloat(Transformation):
-    """Convert any float64 initializers to float32."""
-
-    def apply(self, model):
-        graph_modified = False
-        init_names = [x.name for x in model.graph.initializer]
-        for nm in init_names:
-            init = model.get_initializer(nm)
-            if init.dtype == np.float64:
-                init_f32 = init.astype(np.float32)
-                model.set_initializer(nm, init_f32)
-                graph_modified = True
-        return (model, graph_modified)
diff --git a/src/finn/transformation/fold_constants.py b/src/finn/transformation/fold_constants.py
deleted file mode 100644
index a73035e571fe0ce0425c3d8da288f755984268d4..0000000000000000000000000000000000000000
--- a/src/finn/transformation/fold_constants.py
+++ /dev/null
@@ -1,62 +0,0 @@
-# Copyright (c) 2020, Xilinx
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-#
-# * Redistributions of source code must retain the above copyright notice, this
-#   list of conditions and the following disclaimer.
-#
-# * Redistributions in binary form must reproduce the above copyright notice,
-#   this list of conditions and the following disclaimer in the documentation
-#   and/or other materials provided with the distribution.
-#
-# * Neither the name of FINN nor the names of its
-#   contributors may be used to endorse or promote products derived from
-#   this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-import finn.core.onnx_exec as oxe
-from finn.transformation import Transformation
-from finn.transformation.infer_shapes import InferShapes
-
-
-class FoldConstants(Transformation):
-    """Replace the output of a node with const-only inputs with a precomputed
-    result."""
-
-    def apply(self, model):
-        graph = model.graph
-        node_ind = 0
-        graph_modified = False
-        execution_context = model.make_empty_exec_context()
-        for n in graph.node:
-            node_ind += 1
-            node_inp_inits = list(map(lambda x: model.get_initializer(x), n.input))
-            node_inp_dyn = list(filter(lambda x: x is None, node_inp_inits))
-            node_out = n.output[0]
-            is_all_constant_inputs = len(node_inp_dyn) == 0
-            ishape = model.get_tensor_shape(n.input[0])
-            is_const_shape = (n.op_type == "Shape") and (ishape is not None)
-            if is_all_constant_inputs or is_const_shape:
-                # this node has no dynamic inputs, only constant ones -- so we can
-                # do constant folding.
-                oxe.execute_node(n, execution_context, graph)
-                # use the execution result as an initializer
-                model.set_initializer(node_out, execution_context[node_out])
-                # remove old node
-                graph.node.remove(n)
-                graph_modified = True
-        if graph_modified:
-            model = model.transform(InferShapes())
-        return (model, graph_modified)
diff --git a/src/finn/transformation/fpgadataflow/__init__.py b/src/finn/transformation/fpgadataflow/__init__.py
deleted file mode 100644
index 83c8e8bed70797f7d6c0138968f750f72e790386..0000000000000000000000000000000000000000
--- a/src/finn/transformation/fpgadataflow/__init__.py
+++ /dev/null
@@ -1,27 +0,0 @@
-# Copyright (c) 2020, Xilinx
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-#
-# * Redistributions of source code must retain the above copyright notice, this
-#   list of conditions and the following disclaimer.
-#
-# * Redistributions in binary form must reproduce the above copyright notice,
-#   this list of conditions and the following disclaimer in the documentation
-#   and/or other materials provided with the distribution.
-#
-# * Neither the name of FINN nor the names of its
-#   contributors may be used to endorse or promote products derived from
-#   this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/src/finn/transformation/fpgadataflow/annotate_cycles.py b/src/finn/transformation/fpgadataflow/annotate_cycles.py
index 521c84952daf25982e574421dfba3ff0f7df91ae..2c547203df94308b929a7989f1a9102bd3002fed 100644
--- a/src/finn/transformation/fpgadataflow/annotate_cycles.py
+++ b/src/finn/transformation/fpgadataflow/annotate_cycles.py
@@ -27,7 +27,7 @@
 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
 import finn.custom_op.registry as registry
-from finn.transformation import Transformation
+from finn.transformation.base import Transformation
 from finn.transformation.move_reshape import _is_fpgadataflow_node
 from finn.core.modelwrapper import ModelWrapper
 from finn.custom_op.registry import getCustomOp
diff --git a/src/finn/transformation/fpgadataflow/annotate_resources.py b/src/finn/transformation/fpgadataflow/annotate_resources.py
index d6ff058848700b50dadb7a6ed0ff6c07b7eeb4a3..4e501510110ef724c6a67f6214654b5454b30a77 100644
--- a/src/finn/transformation/fpgadataflow/annotate_resources.py
+++ b/src/finn/transformation/fpgadataflow/annotate_resources.py
@@ -27,7 +27,7 @@
 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
 import finn.custom_op.registry as registry
-from finn.transformation import Transformation
+from finn.transformation.base import Transformation
 from finn.transformation.move_reshape import _is_fpgadataflow_node
 from finn.analysis.fpgadataflow.res_estimation import res_estimation
 from finn.analysis.fpgadataflow.hls_synth_res_estimation import hls_synth_res_estimation
diff --git a/src/finn/transformation/fpgadataflow/cleanup.py b/src/finn/transformation/fpgadataflow/cleanup.py
index 248a99b57aed7f38f63cc25ad7ecf93bd1930e63..f089317074eb2bded4675f6fd2e22fdaeb4b6a82 100644
--- a/src/finn/transformation/fpgadataflow/cleanup.py
+++ b/src/finn/transformation/fpgadataflow/cleanup.py
@@ -31,7 +31,7 @@ import shutil
 
 import finn.custom_op.registry as registry
 from finn.util.fpgadataflow import is_fpgadataflow_node
-from finn.transformation import Transformation
+from finn.transformation.base import Transformation
 
 
 class CleanUp(Transformation):
diff --git a/src/finn/transformation/fpgadataflow/compile_cppsim.py b/src/finn/transformation/fpgadataflow/compile_cppsim.py
index ddf00c799b8a53c428d0854551d0078a6e264111..e17feb4683189ad2f8174f0564a877f84870b51d 100644
--- a/src/finn/transformation/fpgadataflow/compile_cppsim.py
+++ b/src/finn/transformation/fpgadataflow/compile_cppsim.py
@@ -28,7 +28,7 @@
 
 import finn.custom_op.registry as registry
 from finn.util.fpgadataflow import is_fpgadataflow_node
-from finn.transformation import NodeLocalTransformation
+from finn.transformation.base import NodeLocalTransformation
 
 
 class CompileCppSim(NodeLocalTransformation):
diff --git a/src/finn/transformation/fpgadataflow/convert_to_hls_layers.py b/src/finn/transformation/fpgadataflow/convert_to_hls_layers.py
index d4d5b006493b8db1da0184e98ba35493d3e6ccbd..d6f2e04f762a6b67ace989fa802829fd3e5a6fb5 100644
--- a/src/finn/transformation/fpgadataflow/convert_to_hls_layers.py
+++ b/src/finn/transformation/fpgadataflow/convert_to_hls_layers.py
@@ -32,7 +32,7 @@ import numpy as np
 import warnings
 
 from finn.core.datatype import DataType
-from finn.transformation import Transformation
+from finn.transformation.base import Transformation
 from finn.custom_op.registry import getCustomOp
 from finn.transformation.infer_shapes import InferShapes
 from finn.transformation.infer_datatypes import InferDataTypes
diff --git a/src/finn/transformation/fpgadataflow/create_dataflow_partition.py b/src/finn/transformation/fpgadataflow/create_dataflow_partition.py
index fb8b4358abd772d13c355f797649dc3b51975b4d..90a92d11ce621897e3e6c687f57b1cdf77d08fba 100644
--- a/src/finn/transformation/fpgadataflow/create_dataflow_partition.py
+++ b/src/finn/transformation/fpgadataflow/create_dataflow_partition.py
@@ -30,7 +30,7 @@ import copy
 
 from onnx import helper
 
-from finn.transformation import Transformation
+from finn.transformation.base import Transformation
 from finn.util.basic import get_by_name, make_build_dir
 
 
diff --git a/src/finn/transformation/fpgadataflow/create_stitched_ip.py b/src/finn/transformation/fpgadataflow/create_stitched_ip.py
index 0def25d8429f5d3f6c02a9db656650bc1baba6ee..3470e9525d590303eca1f8700fe4b79c4e03d38f 100644
--- a/src/finn/transformation/fpgadataflow/create_stitched_ip.py
+++ b/src/finn/transformation/fpgadataflow/create_stitched_ip.py
@@ -30,7 +30,7 @@ import os
 import warnings
 import subprocess
 
-from finn.transformation import Transformation
+from finn.transformation.base import Transformation
 from finn.util.basic import get_by_name, make_build_dir
 from finn.custom_op.registry import getCustomOp
 from finn.util.basic import get_num_default_workers
diff --git a/src/finn/transformation/fpgadataflow/floorplan.py b/src/finn/transformation/fpgadataflow/floorplan.py
index 1d9a51875499d77f384c03f54009a9dd1001dea0..eaade4a335258bb2ffd7ce0f87da8ae7c3fd00b3 100644
--- a/src/finn/transformation/fpgadataflow/floorplan.py
+++ b/src/finn/transformation/fpgadataflow/floorplan.py
@@ -27,7 +27,7 @@
 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
 from finn.custom_op.registry import getCustomOp
-from finn.transformation import Transformation
+from finn.transformation.base import Transformation
 from finn.util.basic import get_by_name
 
 
diff --git a/src/finn/transformation/fpgadataflow/hlssynth_ip.py b/src/finn/transformation/fpgadataflow/hlssynth_ip.py
index 8315b6ec11e3d0bc5e2bf97e7c11817ae8b5a5b1..e79d70544c5e8d2b9060e354d7713b8405ae9c7f 100644
--- a/src/finn/transformation/fpgadataflow/hlssynth_ip.py
+++ b/src/finn/transformation/fpgadataflow/hlssynth_ip.py
@@ -29,7 +29,7 @@
 import os
 import finn.custom_op.registry as registry
 from finn.util.fpgadataflow import is_fpgadataflow_node
-from finn.transformation import NodeLocalTransformation
+from finn.transformation.base import NodeLocalTransformation
 import warnings
 
 
diff --git a/src/finn/transformation/fpgadataflow/insert_dwc.py b/src/finn/transformation/fpgadataflow/insert_dwc.py
index 3fe60292e8f54a8cdf394b5e09f8a3d2bca7605c..195a005ff87b43c6b64017354895693cd811a48e 100644
--- a/src/finn/transformation/fpgadataflow/insert_dwc.py
+++ b/src/finn/transformation/fpgadataflow/insert_dwc.py
@@ -2,7 +2,7 @@ from onnx import TensorProto
 from onnx import helper as oh
 
 from finn.custom_op.registry import getCustomOp
-from finn.transformation import Transformation
+from finn.transformation.base import Transformation
 from finn.util.fpgadataflow import is_fpgadataflow_node
 
 
diff --git a/src/finn/transformation/fpgadataflow/insert_fifo.py b/src/finn/transformation/fpgadataflow/insert_fifo.py
index 6f7fde0c4faba09e584eb578819f44c18639bc9d..1c26642888f29e8dd08046e4de01ae8fa62b10e7 100644
--- a/src/finn/transformation/fpgadataflow/insert_fifo.py
+++ b/src/finn/transformation/fpgadataflow/insert_fifo.py
@@ -2,7 +2,7 @@ from onnx import TensorProto
 from onnx import helper as oh
 
 from finn.custom_op.registry import getCustomOp
-from finn.transformation import Transformation
+from finn.transformation.base import Transformation
 from finn.util.fpgadataflow import is_fpgadataflow_node
 import numpy as np
 
diff --git a/src/finn/transformation/fpgadataflow/insert_iodma.py b/src/finn/transformation/fpgadataflow/insert_iodma.py
index 72e5ec4fdd721ecf549adaf7ddd38db4636bce27..feaa534e1e9d2fb527293a617cc622a5f71c24cb 100644
--- a/src/finn/transformation/fpgadataflow/insert_iodma.py
+++ b/src/finn/transformation/fpgadataflow/insert_iodma.py
@@ -31,7 +31,7 @@ from onnx import helper as oh
 
 from finn.util.basic import get_by_name
 from finn.custom_op.registry import getCustomOp
-from finn.transformation import Transformation
+from finn.transformation.base import Transformation
 from finn.transformation.general import SortGraph
 import finn.core.data_layout as DataLayout
 import math
diff --git a/src/finn/transformation/fpgadataflow/insert_tlastmarker.py b/src/finn/transformation/fpgadataflow/insert_tlastmarker.py
index bbb0e43fda464e919a7d8c9dcd25e08a49b33cec..8ffb083217bb3a7e379112b3da102487c0cd50c2 100644
--- a/src/finn/transformation/fpgadataflow/insert_tlastmarker.py
+++ b/src/finn/transformation/fpgadataflow/insert_tlastmarker.py
@@ -30,7 +30,7 @@ from onnx import TensorProto
 from onnx import helper as oh
 
 from finn.custom_op.registry import getCustomOp
-from finn.transformation import Transformation
+from finn.transformation.base import Transformation
 from finn.util.basic import get_by_name
 
 import numpy as np
diff --git a/src/finn/transformation/fpgadataflow/make_deployment.py b/src/finn/transformation/fpgadataflow/make_deployment.py
index 2880e4aba20564f50a0acdff5e8c728714c84b5c..6d37f567c9a20cf692df126c1c3560324b61d06d 100644
--- a/src/finn/transformation/fpgadataflow/make_deployment.py
+++ b/src/finn/transformation/fpgadataflow/make_deployment.py
@@ -32,7 +32,7 @@ import subprocess
 from distutils.dir_util import copy_tree
 from shutil import copy
 
-from finn.transformation import Transformation
+from finn.transformation.base import Transformation
 from finn.util.basic import make_build_dir
 import finn.transformation.fpgadataflow.templates as templates
 
diff --git a/src/finn/transformation/fpgadataflow/make_pynq_driver.py b/src/finn/transformation/fpgadataflow/make_pynq_driver.py
index 813b40698d1beec54e6ba3fa5344a8d0bb715a00..e8e3059240556296ca635ffcc36665fb18beda3b 100644
--- a/src/finn/transformation/fpgadataflow/make_pynq_driver.py
+++ b/src/finn/transformation/fpgadataflow/make_pynq_driver.py
@@ -28,9 +28,10 @@
 
 
 import shutil
-from finn.transformation import Transformation
-from finn.util.basic import gen_finn_dt_tensor, get_finn_root, make_build_dir
-from finn.util.data_packing import finnpy_to_packed_bytearray
+from finn.transformation.base import Transformation
+from finn.util.basic import gen_finn_dt_tensor, make_build_dir
+import finn.util.data_packing as dpk
+import finn.core.datatype as dtp
 
 from . import templates
 
@@ -77,10 +78,10 @@ class MakePYNQDriver(Transformation):
         # generate dummy folded i/o tensors and their packed versions
         i_tensor_dummy_folded = gen_finn_dt_tensor(i_tensor_dt, i_tensor_shape_folded)
         o_tensor_dummy_folded = gen_finn_dt_tensor(o_tensor_dt, o_tensor_shape_folded)
-        i_tensor_dummy_packed = finnpy_to_packed_bytearray(
+        i_tensor_dummy_packed = dpk.finnpy_to_packed_bytearray(
             i_tensor_dummy_folded, i_tensor_dt
         )
-        o_tensor_dummy_packed = finnpy_to_packed_bytearray(
+        o_tensor_dummy_packed = dpk.finnpy_to_packed_bytearray(
             o_tensor_dummy_folded, o_tensor_dt
         )
         i_tensor_shape_packed = i_tensor_dummy_packed.shape
@@ -132,11 +133,17 @@ class MakePYNQDriver(Transformation):
             f.write(validate_src)
 
         # copy all the dependencies into the driver folder
-        shutil.copytree(
-            get_finn_root() + "/src/finn/util", pynq_driver_dir + "/finn/util"
-        )
-        shutil.copytree(
-            get_finn_root() + "/src/finn/core", pynq_driver_dir + "/finn/core"
-        )
+        # driver imports utils/data_packing and core/datatype
+        # both of which are in finn-base
+        # e.g. /workspace/finn-base/src/finn/util/data_packing.py
+        dpk_root = dpk.__file__
+        # e.g. /workspace/finn-base/src/finn/util
+        dpk_root = dpk_root.replace("data_packing.py", "")
+        # e.g. /workspace/finn-base/src/finn/core/datatype.py
+        dtp_root = dtp.__file__
+        # e.g. /workspace/finn-base/src/finn/core
+        dtp_root = dtp_root.replace("datatype.py", "")
+        shutil.copytree(dpk_root, pynq_driver_dir + "/finn/util")
+        shutil.copytree(dtp_root, pynq_driver_dir + "/finn/core")
 
         return (model, False)
diff --git a/src/finn/transformation/fpgadataflow/make_zynq_proj.py b/src/finn/transformation/fpgadataflow/make_zynq_proj.py
index e263c450af5d2dca09a79b7757a0c0d67bdf86ff..1a4b67d1e7adb86b5a7515eb0ff14b780eea0585 100644
--- a/src/finn/transformation/fpgadataflow/make_zynq_proj.py
+++ b/src/finn/transformation/fpgadataflow/make_zynq_proj.py
@@ -30,7 +30,7 @@ import os
 import subprocess
 
 from finn.custom_op.registry import getCustomOp
-from finn.transformation import Transformation
+from finn.transformation.base import Transformation
 from finn.core.modelwrapper import ModelWrapper
 from finn.util.basic import get_by_name, make_build_dir
 from finn.util.basic import get_num_default_workers
diff --git a/src/finn/transformation/fpgadataflow/minimize_accumulator_width.py b/src/finn/transformation/fpgadataflow/minimize_accumulator_width.py
index 2c54a5efbd3b28f0fbfd074b512929edab234e78..0a0c45b6bedeabe7cfa1c7209ea74b6876b828b2 100644
--- a/src/finn/transformation/fpgadataflow/minimize_accumulator_width.py
+++ b/src/finn/transformation/fpgadataflow/minimize_accumulator_width.py
@@ -27,7 +27,7 @@
 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
 from finn.custom_op.registry import getCustomOp
-from finn.transformation import Transformation
+from finn.transformation.base import Transformation
 from finn.util.fpgadataflow import is_fpgadataflow_node
 
 
diff --git a/src/finn/transformation/fpgadataflow/prepare_cppsim.py b/src/finn/transformation/fpgadataflow/prepare_cppsim.py
index 6eae560e1191642cfaf85d92c6d0fcf644630973..26354bdf70e10bfcddfbaf732a214865c6feb8f5 100644
--- a/src/finn/transformation/fpgadataflow/prepare_cppsim.py
+++ b/src/finn/transformation/fpgadataflow/prepare_cppsim.py
@@ -31,7 +31,7 @@ import os
 import finn.custom_op.registry as registry
 from finn.util.basic import make_build_dir
 from finn.util.fpgadataflow import is_fpgadataflow_node
-from finn.transformation import Transformation
+from finn.transformation.base import Transformation
 from finn.util.basic import get_num_default_workers
 import multiprocessing as mp
 import copy
diff --git a/src/finn/transformation/fpgadataflow/prepare_ip.py b/src/finn/transformation/fpgadataflow/prepare_ip.py
index 21f8e0052d5f2d60f11f33846b483d3f556d1188..53cb0af163b853c0a0352d8562cca66b3ecf6068 100644
--- a/src/finn/transformation/fpgadataflow/prepare_ip.py
+++ b/src/finn/transformation/fpgadataflow/prepare_ip.py
@@ -28,7 +28,7 @@
 
 import os
 import finn.custom_op.registry as registry
-from finn.transformation import Transformation
+from finn.transformation.base import Transformation
 from finn.util.basic import make_build_dir
 from finn.util.fpgadataflow import is_fpgadataflow_node
 import warnings
diff --git a/src/finn/transformation/fpgadataflow/prepare_rtlsim.py b/src/finn/transformation/fpgadataflow/prepare_rtlsim.py
index 8c28ab7e2376de392b0fdd628c70e854011dc406..d2ec5561a349d5fc83f02870c1a682dba8433e43 100644
--- a/src/finn/transformation/fpgadataflow/prepare_rtlsim.py
+++ b/src/finn/transformation/fpgadataflow/prepare_rtlsim.py
@@ -31,7 +31,7 @@ from finn.util.fpgadataflow import is_fpgadataflow_node
 from finn.transformation.fpgadataflow.replace_verilog_relpaths import (
     ReplaceVerilogRelPaths,
 )
-from finn.transformation import NodeLocalTransformation
+from finn.transformation.base import NodeLocalTransformation
 
 try:
     from pyverilator import PyVerilator
diff --git a/src/finn/transformation/fpgadataflow/replace_verilog_relpaths.py b/src/finn/transformation/fpgadataflow/replace_verilog_relpaths.py
index e63ae4e0203188d9664f432f75e36994e8a71ac5..c577704129fa564f5e0e1e256623ff10125cf5ac 100644
--- a/src/finn/transformation/fpgadataflow/replace_verilog_relpaths.py
+++ b/src/finn/transformation/fpgadataflow/replace_verilog_relpaths.py
@@ -30,7 +30,7 @@ import os
 
 import finn.custom_op.registry as registry
 from finn.util.fpgadataflow import is_fpgadataflow_node
-from finn.transformation import Transformation
+from finn.transformation.base import Transformation
 
 
 class ReplaceVerilogRelPaths(Transformation):
diff --git a/src/finn/transformation/fpgadataflow/set_exec_mode.py b/src/finn/transformation/fpgadataflow/set_exec_mode.py
index 40996e5f64fb812ea3766b71a9a8275514dec4a0..6a76031f4c76831f514b77aee6cd3c560b3b9910 100644
--- a/src/finn/transformation/fpgadataflow/set_exec_mode.py
+++ b/src/finn/transformation/fpgadataflow/set_exec_mode.py
@@ -28,7 +28,7 @@
 
 import finn.custom_op.registry as registry
 from finn.util.fpgadataflow import is_fpgadataflow_node
-from finn.transformation import Transformation
+from finn.transformation.base import Transformation
 
 
 class SetExecMode(Transformation):
diff --git a/src/finn/transformation/fpgadataflow/synth_ooc.py b/src/finn/transformation/fpgadataflow/synth_ooc.py
index 8fd7e4724ef7f255b1435d5ab5e680d155d39487..acc20e4ad0d0f4a17b20fd77625e9954f09e69aa 100644
--- a/src/finn/transformation/fpgadataflow/synth_ooc.py
+++ b/src/finn/transformation/fpgadataflow/synth_ooc.py
@@ -29,7 +29,7 @@
 import os
 from shutil import copy2
 
-from finn.transformation import Transformation
+from finn.transformation.base import Transformation
 from finn.util.vivado import out_of_context_synth
 from finn.util.basic import make_build_dir
 
diff --git a/src/finn/transformation/fpgadataflow/templates.py b/src/finn/transformation/fpgadataflow/templates.py
index 2b3789dc21cb4fe3f62d6b2a2ea0888329c9db66..253fd462fa6601bcc34dd3d2aa2f42463c0ae308 100644
--- a/src/finn/transformation/fpgadataflow/templates.py
+++ b/src/finn/transformation/fpgadataflow/templates.py
@@ -214,7 +214,7 @@ class FINNAccelDriver():
 if __name__ == "__main__":
     parser = argparse.ArgumentParser(description='Set exec mode, batchsize N, bitfile name, inputfile name and outputfile name')
     parser.add_argument('--exec_mode', help='Please select functional verification ("execute") or throughput test ("throughput_test")', default="execute")
-    parser.add_argument('--platform', help='Target platform: zynq-iodma alveo', default="zynq")
+    parser.add_argument('--platform', help='Target platform: zynq-iodma alveo', default="$PLATFORM$")
     parser.add_argument('--batchsize', help='number of samples for inference', type=int, default=1)
     parser.add_argument('--bitfile', help='name of bitfile (i.e. "resizer.bit")', default="resizer.bit")
     parser.add_argument('--inputfile', help='name of input npy file (i.e. "input.npy")', default="input.npy")
diff --git a/src/finn/transformation/fpgadataflow/vitis_build.py b/src/finn/transformation/fpgadataflow/vitis_build.py
index 482dc8d784c66faef9392093c7c857630304eef3..2e1f3fee2a3ba06f1feb192b9c21e6a5671ad48b 100644
--- a/src/finn/transformation/fpgadataflow/vitis_build.py
+++ b/src/finn/transformation/fpgadataflow/vitis_build.py
@@ -30,7 +30,7 @@ import os
 import subprocess
 
 from finn.core.modelwrapper import ModelWrapper
-from finn.transformation import Transformation
+from finn.transformation.base import Transformation
 from finn.custom_op.registry import getCustomOp
 
 from finn.transformation.fpgadataflow.create_dataflow_partition import (
diff --git a/src/finn/transformation/general.py b/src/finn/transformation/general.py
deleted file mode 100644
index 02f95b14e7944b828ae4c71ebf26851dc90b755d..0000000000000000000000000000000000000000
--- a/src/finn/transformation/general.py
+++ /dev/null
@@ -1,257 +0,0 @@
-# Copyright (c) 2020, Xilinx
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-#
-# * Redistributions of source code must retain the above copyright notice, this
-#   list of conditions and the following disclaimer.
-#
-# * Redistributions in binary form must reproduce the above copyright notice,
-#   this list of conditions and the following disclaimer in the documentation
-#   and/or other materials provided with the distribution.
-#
-# * Neither the name of FINN nor the names of its
-#   contributors may be used to endorse or promote products derived from
-#   this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-import finn.util.basic as util
-from finn.transformation import Transformation
-from toposort import toposort_flatten
-
-
-class RemoveUnusedTensors(Transformation):
-    """Remove any unused tensors in the graph by removing any initializers,
-    ValueInfo and tensor annotations associated with it. Unused tensors do not
-    appear as any input/output for any graph nodes.
-    """
-
-    def apply(self, model):
-        graph_modified = False
-        onnx_graph = model.model.graph
-        # build a set of tensors that we actually use in the graph nodes
-        used_tensors = set()
-        for node in model.graph.node:
-            for i in node.input:
-                used_tensors.add(i)
-            for o in node.output:
-                used_tensors.add(o)
-        # remove initializers, value_info and annotations that are not in the
-        # used set of tensors, as determined by the graph node i/o
-        for init in onnx_graph.initializer:
-            if init.name not in used_tensors:
-                onnx_graph.initializer.remove(init)
-                graph_modified = True
-        for vi in onnx_graph.value_info:
-            if vi.name not in used_tensors:
-                onnx_graph.value_info.remove(vi)
-                graph_modified = True
-        for qa in onnx_graph.quantization_annotation:
-            if qa.tensor_name not in used_tensors:
-                onnx_graph.quantization_annotation.remove(qa)
-                graph_modified = True
-
-        return (model, graph_modified)
-
-
-class RemoveStaticGraphInputs(Transformation):
-    "Remove any top-level graph inputs that have initializers."
-
-    def apply(self, model):
-        graph_modified = False
-        for i in model.graph.input:
-            if model.get_initializer(i.name) is not None:
-                # move ValueInfo to internal (value_info) container
-                model.graph.value_info.append(i)
-                model.graph.input.remove(i)
-                graph_modified = True
-
-        return (model, graph_modified)
-
-
-class GiveUniqueNodeNames(Transformation):
-    """Give unique names to each node in the graph using enumeration, starting
-    with given prefix (if specified in the constructor)."""
-
-    def __init__(self, prefix=""):
-        super().__init__()
-        self.prefix = prefix
-
-    def apply(self, model):
-        optype_count = {}
-        for n in model.graph.node:
-            if n.op_type not in optype_count.keys():
-                optype_count[n.op_type] = 0
-            n.name = "%s%s_%d" % (self.prefix, n.op_type, optype_count[n.op_type])
-            optype_count[n.op_type] += 1
-        # return model_was_changed = False as single iteration is always enough
-        return (model, False)
-
-
-class GiveRandomTensorNames(Transformation):
-    """Give random tensor names to all tensors."""
-
-    def apply(self, model):
-        names = model.get_all_tensor_names()
-        for name in names:
-            model.rename_tensor(name, util.random_string())
-        # return model_was_changed = False as single iteration is always enough
-        return (model, False)
-
-
-class GiveReadableTensorNames(Transformation):
-    """Give more human-readable names to all internal tensors. You should
-    apply GiveUniqueNodeNames prior to this transform to avoid empty node names,
-    as the readable names are based on the node names."""
-
-    def apply(self, model):
-        # to ensure we can use rename_tensor safely (without renaming existing
-        # tensors) we start by giving random names to all tensors
-        model = model.transform(GiveRandomTensorNames())
-        graph = model.graph
-        for n in graph.node:
-            assert n.name != "", "Found empty node name"
-            out_num = 0
-            for o in n.output:
-                model.rename_tensor(o, "%s_out%d" % (n.name, out_num))
-                out_num += 1
-            init_in_num = 0
-            for i in n.input:
-                if model.get_initializer(i) is not None:
-                    model.rename_tensor(i, "%s_param%d" % (n.name, init_in_num))
-                    init_in_num += 1
-        # give special names to the main model input and output
-        model.rename_tensor(model.graph.input[0].name, "global_in")
-        model.rename_tensor(model.graph.output[0].name, "global_out")
-        # return model_was_changed = False as single iteration is always enough
-        return (model, False)
-
-
-class GiveUniqueParameterTensors(Transformation):
-    """Make every parameter tensor unique. The aim is to avoid affecting
-    other nodes apart from the one the system is currently operating on."""
-
-    def apply(self, model):
-        graph = model.graph
-        graph_modified = False
-        seen_parameters = []
-        for n in graph.node:
-            # copy inputs since they may be modified
-            node_inputs_list = [x for x in n.input]
-            for input_idx, node_input in enumerate(node_inputs_list):
-                # check if it's a parameter
-                input_init = model.get_initializer(node_input)
-                if input_init is None:
-                    # dynamic input
-                    continue
-
-                # check if repeated
-                if node_input not in seen_parameters:
-                    # first occurance
-                    seen_parameters += [node_input]
-                    continue
-
-                new_param_name = model.make_new_valueinfo_name()
-
-                model.set_initializer(new_param_name, input_init)
-                model.set_tensor_datatype(
-                    new_param_name, model.get_tensor_datatype(node_input)
-                )
-
-                # point node input to new tensor
-                n.input[input_idx] = new_param_name
-
-        return (model, graph_modified)
-
-
-class SortGraph(Transformation):
-    """ Returns the model with its node list sorted topologically.
-    Any ONNX graph to be executed must have a topologically sorted node list,
-    as dictated by the ONNX standard.
-    """
-
-    # Notes on SortGraph performance:
-    # benchmark in  tests/transformation/test_sort_graph.py
-    # The algorithm doesn't move initializers so its performance should only depend on
-    # the number of nodes
-    #
-    # Relative order of magnitudes for time per step:
-    # - Gather graph structure:       base
-    # - Sort nodes:                   0.1 of base
-    # - Remove and insert in order :  0.001 of base
-    #
-    # Notes:
-    # Remove nodes and insert them in order:
-    # Probably this is faster than copying initializers and more robust in general
-
-    def apply(self, model):
-        if len(model.graph.node) == 1:
-            # single-node graph, nothing to sort
-            return (model, False)
-        # Gather graph structure
-        graph_dependencies = {}
-        node_list = [
-            n for n in model.graph.node
-        ]  # I also need the list to remove the nodes
-        for node_idx, n in enumerate(node_list):
-            node_pred = model.find_direct_predecessors(n)
-            if node_pred is None:
-                # Will also eliminate nodes that are floating around for some reason
-                continue
-
-            node_dependencies = [node_list.index(pred) for pred in node_pred]
-            graph_dependencies[node_idx] = set(node_dependencies)
-
-        # Sort nodes
-        sorted_node_indexes = toposort_flatten(graph_dependencies)
-
-        # Remove nodes and insert them in order
-        # Can't remove nodes before if I want to use model.find_direct_predecessors()
-        for n in node_list:
-            model.graph.node.remove(n)
-
-        for new_idx, sorted_idx in enumerate(sorted_node_indexes):
-            model.graph.node.insert(new_idx, node_list[sorted_idx])
-
-        return (model, False)
-
-
-class ConvertSubToAdd(Transformation):
-    """Convert subtract-a-constant nodes to add-a-constant nodes."""
-
-    def apply(self, model):
-        graph = model.graph
-        for n in graph.node:
-            if n.op_type == "Sub":
-                A = model.get_initializer(n.input[1])
-                if A is not None:
-                    n.op_type = "Add"
-                    model.set_initializer(n.input[1], -A)
-        # return model_was_changed = False as single iteration is always enough
-        return (model, False)
-
-
-class ConvertDivToMul(Transformation):
-    """Convert divide by constant nodes to multiply by constant nodes."""
-
-    def apply(self, model):
-        graph = model.graph
-        for n in graph.node:
-            if n.op_type == "Div":
-                A = model.get_initializer(n.input[1])
-                if A is not None:
-                    n.op_type = "Mul"
-                    model.set_initializer(n.input[1], 1.0 / A)
-        # return model_was_changed = False as single iteration is always enough
-        return (model, False)
diff --git a/src/finn/transformation/infer_data_layouts.py b/src/finn/transformation/infer_data_layouts.py
deleted file mode 100644
index d07162fa049bd016e91b8c5b01ea56eda6267655..0000000000000000000000000000000000000000
--- a/src/finn/transformation/infer_data_layouts.py
+++ /dev/null
@@ -1,127 +0,0 @@
-# Copyright (c) 2020, Xilinx
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-#
-# * Redistributions of source code must retain the above copyright notice, this
-#   list of conditions and the following disclaimer.
-#
-# * Redistributions in binary form must reproduce the above copyright notice,
-#   this list of conditions and the following disclaimer in the documentation
-#   and/or other materials provided with the distribution.
-#
-# * Neither the name of FINN nor the names of its
-#   contributors may be used to endorse or promote products derived from
-#   this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-import finn.custom_op.registry as registry
-import finn.core.data_layout as DataLayout
-from finn.transformation import Transformation
-import warnings
-from finn.util.basic import get_by_name
-
-
-def _dims_to_layout(model, node, ndims):
-    if ndims == 2:
-        return DataLayout.NC
-    else:
-        if node.domain == "finn":
-            if node.op_type == "MultiThreshold" or node.op_type == "QuantAvgPool2d":
-                mt_inst = registry.getCustomOp(node)
-                layout = mt_inst.get_nodeattr("data_layout")
-                if layout == "NHWC" and ndims == 4:
-                    return DataLayout.NHWC
-                elif layout == "NCHW" and ndims == 4:
-                    return DataLayout.NCHW
-                else:
-                    return DataLayout.UNKNOWN
-            else:
-                if ndims == 4:
-                    return DataLayout.NHWC
-                else:
-                    return DataLayout.UNKNOWN
-        else:
-            # propagate input layout to output
-            # TODO this won't work for concat, squeeze/unsqueeze/reshape...
-            return model.get_tensor_layout(node.input[0])
-
-
-def _infer_node_data_layout(model, node):
-    """Infer output data layout annotation(s) for a particular node.
-    Returns True if any changes were made."""
-    old_layouts = list(map(lambda x: model.get_tensor_layout(x), node.output))
-    if node.domain == "finn":
-        # try to guess based on number of output dims
-        for o in node.output:
-            ndims = len(model.get_tensor_shape(o))
-            new_layout = _dims_to_layout(model, node, ndims)
-            model.set_tensor_layout(o, new_layout)
-    else:
-        if node.op_type == "Transpose":
-            # grab input annotation and switch it around using perm
-            perm = get_by_name(node.attribute, "perm").ints
-            inp_layout = model.get_tensor_layout(node.input[0])
-            out_layout = [inp_layout[i] for i in perm]
-            model.set_tensor_layout(node.output[0], out_layout)
-        elif node.op_type == "Unsqueeze":
-            inp_layout = model.get_tensor_layout(node.input[0])
-            # add dummy dimension at the output
-            out_layout = inp_layout + ["x"]
-            model.set_tensor_layout(node.output[0], out_layout)
-        elif node.op_type == "Squeeze":
-            inp_layout = model.get_tensor_layout(node.input[0])
-            assert inp_layout[-1] == "x"
-            # remove dummy dimension
-            out_layout = inp_layout[:-1]
-            model.set_tensor_layout(node.output[0], out_layout)
-        else:
-            # try to guess based on number of output dims
-            for o in node.output:
-                ndims = len(model.get_tensor_shape(o))
-                model.set_tensor_layout(o, _dims_to_layout(model, node, ndims))
-    # compare old and new output dtypes to see if anything changed
-    new_layouts = list(map(lambda x: model.get_tensor_layout(x), node.output))
-    graph_modified = new_layouts != old_layouts
-    return graph_modified
-
-
-class InferDataLayouts(Transformation):
-    """Try to infer data layout annotations info for all input/intermediate/output
-    tensors based on inputs and node type."""
-
-    def apply(self, model):
-        graph = model.graph
-        graph_modified = False
-        # first, make sure that the global input has an annotation
-        # this is really hard to do in general, so we do some bad guesswork
-        inp_name = graph.input[0].name
-        if model.get_tensor_layout(inp_name) is None:
-            inp_shape = model.get_tensor_shape(inp_name)
-            if len(inp_shape) == 4:
-                warnings.warn("Assuming 4D input is NCHW")
-                model.set_tensor_layout(inp_name, DataLayout.NCHW)
-                graph_modified = True
-            elif len(inp_shape) == 2:
-                graph_modified = True
-                warnings.warn("Assuming 2D input is NC")
-                model.set_tensor_layout(inp_name, DataLayout.NC)
-            else:
-                raise Exception(
-                    """Unknown number of dims for input, don't know
-                how to annotate"""
-                )
-        for node in graph.node:
-            graph_modified |= _infer_node_data_layout(model, node)
-        return (model, graph_modified)
diff --git a/src/finn/transformation/infer_datatypes.py b/src/finn/transformation/infer_datatypes.py
deleted file mode 100644
index 39b7a787be8c725e7b6d474757dd96fc4848dfe0..0000000000000000000000000000000000000000
--- a/src/finn/transformation/infer_datatypes.py
+++ /dev/null
@@ -1,96 +0,0 @@
-# Copyright (c) 2020, Xilinx
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-#
-# * Redistributions of source code must retain the above copyright notice, this
-#   list of conditions and the following disclaimer.
-#
-# * Redistributions in binary form must reproduce the above copyright notice,
-#   this list of conditions and the following disclaimer in the documentation
-#   and/or other materials provided with the distribution.
-#
-# * Neither the name of FINN nor the names of its
-#   contributors may be used to endorse or promote products derived from
-#   this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-import finn.custom_op.registry as registry
-from finn.core.datatype import DataType
-from finn.transformation import Transformation
-
-
-def _infer_node_datatype(model, node):
-    """Infer output datatype(s) for a particular node. Returns True if any
-    changes were made."""
-    dt_identity_optypes = ["Reshape", "Transpose"]
-    idtypes = list(map(lambda x: model.get_tensor_datatype(x), node.input))
-    odtypes = list(map(lambda x: model.get_tensor_datatype(x), node.output))
-    op_type = node.op_type
-    if node.domain == "finn":
-        # handle DataType inference for CustomOp
-        try:
-            # lookup op_type in registry of CustomOps
-            inst = registry.custom_op[op_type](node)
-            inst.infer_node_datatype(model)
-        except KeyError:
-            # exception if op_type is not supported
-            raise Exception("Custom op_type %s is currently not supported." % op_type)
-    else:
-        if node.op_type == "Sign":
-            # always produces bipolar outputs
-            model.set_tensor_datatype(node.output[0], DataType.BIPOLAR)
-        elif node.op_type == "MatMul":
-            if len(list(filter(lambda x: x == DataType.FLOAT32, idtypes))) != 0:
-                # node has at least one float input, output is also float
-                model.set_tensor_datatype(node.output[0], DataType.FLOAT32)
-            else:
-                # TODO compute minimum / maximum result to minimize bitwidth
-                # use (u)int32 accumulators for now
-                has_signed_inp = len(list(filter(lambda x: x.signed(), idtypes))) != 0
-                if has_signed_inp:
-                    odtype = DataType.INT32
-                else:
-                    odtype = DataType.UINT32
-                model.set_tensor_datatype(node.output[0], odtype)
-        elif node.op_type in dt_identity_optypes:
-            # set output dtype = input dtype
-            idtype = model.get_tensor_datatype(node.input[0])
-            model.set_tensor_datatype(node.output[0], idtype)
-        else:
-            # unknown, assume node produces float32 outputs
-            for o in node.output:
-                # check if output datatype is already set to a value != FLOAT32
-                odtype = model.get_tensor_datatype(o)
-                if odtype is not None and odtype != DataType.FLOAT32:
-                    # don't change data type
-                    model.set_tensor_datatype(o, odtype)
-                else:
-                    model.set_tensor_datatype(o, DataType.FLOAT32)
-    # compare old and new output dtypes to see if anything changed
-    new_odtypes = list(map(lambda x: model.get_tensor_datatype(x), node.output))
-    graph_modified = new_odtypes != odtypes
-    return graph_modified
-
-
-class InferDataTypes(Transformation):
-    """Infer FINN DataType info for all intermediate/output tensors based on
-    inputs and node type."""
-
-    def apply(self, model):
-        graph = model.graph
-        graph_modified = False
-        for node in graph.node:
-            graph_modified |= _infer_node_datatype(model, node)
-        return (model, graph_modified)
diff --git a/src/finn/transformation/infer_shapes.py b/src/finn/transformation/infer_shapes.py
deleted file mode 100644
index 361ef7f6ad46c23faa48d50b34d78e38d0823796..0000000000000000000000000000000000000000
--- a/src/finn/transformation/infer_shapes.py
+++ /dev/null
@@ -1,90 +0,0 @@
-# Copyright (c) 2020, Xilinx
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-#
-# * Redistributions of source code must retain the above copyright notice, this
-#   list of conditions and the following disclaimer.
-#
-# * Redistributions in binary form must reproduce the above copyright notice,
-#   this list of conditions and the following disclaimer in the documentation
-#   and/or other materials provided with the distribution.
-#
-# * Neither the name of FINN nor the names of its
-#   contributors may be used to endorse or promote products derived from
-#   this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-import onnx.shape_inference as si
-
-import finn.custom_op.registry as registry
-from finn.core.modelwrapper import ModelWrapper
-from finn.transformation import Transformation
-
-
-def _make_shape_compatible_op(node, model):
-    """Return a shape-compatible non-FINN op for a given FINN op. Used for
-    shape inference with custom ops."""
-    assert node.domain == "finn", 'Node domain is not set to "finn".'
-    op_type = node.op_type
-    try:
-        # lookup op_type in registry of CustomOps
-        inst = registry.custom_op[op_type](node)
-        return inst.make_shape_compatible_op(model)
-    except KeyError:
-        # exception if op_type is not supported
-        raise Exception("Custom op_type %s is currently not supported." % op_type)
-
-
-def _hide_finn_ops(model):
-    """Replace any FINN ops by shape-compatible ones, and return a dict that
-    can be used to map the string representations of the new (shape-compatible)
-    ops back to the old ops."""
-    hidden_ops = {}
-    node_ind = 0
-    for node in model.graph.node:
-        node_ind += 1
-        if node.domain == "finn":
-            new_node = _make_shape_compatible_op(node, model)
-            hidden_ops[str(new_node)] = node
-            model.graph.node.insert(node_ind, new_node)
-            model.graph.node.remove(node)
-    return hidden_ops
-
-
-def _restore_finn_ops(model, hidden_ops):
-    """Replace any shape-compatible ops with the FINN ops that originally
-    generated them."""
-    node_ind = 0
-    for node in model.graph.node:
-        node_ind += 1
-        try:
-            old_node = hidden_ops[str(node)]
-            model.graph.node.insert(node_ind, old_node)
-            model.graph.node.remove(node)
-        except KeyError:
-            pass
-
-
-class InferShapes(Transformation):
-    """Ensure every tensor in the model has a specified shape (ValueInfo)."""
-
-    def apply(self, model):
-        # hide your riches!
-        hidden_ops = _hide_finn_ops(model)
-        # call regular ONNX shape inference
-        model = ModelWrapper(si.infer_shapes(model.model))
-        # bring back hidden ops
-        _restore_finn_ops(model, hidden_ops)
-        return (model, False)
diff --git a/src/finn/transformation/insert_topk.py b/src/finn/transformation/insert_topk.py
deleted file mode 100644
index 213d2cedf92c0276e33fcf2b50e6966aeee8c847..0000000000000000000000000000000000000000
--- a/src/finn/transformation/insert_topk.py
+++ /dev/null
@@ -1,96 +0,0 @@
-# Copyright (c) 2020, Xilinx
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-#
-# * Redistributions of source code must retain the above copyright notice, this
-#   list of conditions and the following disclaimer.
-#
-# * Redistributions in binary form must reproduce the above copyright notice,
-#   this list of conditions and the following disclaimer in the documentation
-#   and/or other materials provided with the distribution.
-#
-# * Neither the name of FINN nor the names of its
-#   contributors may be used to endorse or promote products derived from
-#   this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-import numpy as np
-
-from onnx import TensorProto
-from onnx import helper as oh
-
-from finn.transformation import Transformation
-from finn.core.datatype import DataType
-
-
-class InsertTopK(Transformation):
-    """Add TopK node at the network output and replace the graph output with
-    the TopK indices."""
-
-    def __init__(self, k=5, axis=-1, largest=1, sorted=1):
-        super().__init__()
-        self.k = k
-        self.axis = axis
-        self.largest = largest
-        self.sorted = sorted
-
-    def apply(self, model):
-        # get name of output tensor
-        graph_out_name = model.graph.output[0].name
-        # find final node
-        final_node = model.find_producer(graph_out_name)
-        # if a top-select op is already present, do nothing
-        if final_node.op_type == "TopK":
-            return (model, False)
-        else:
-            out_shape = model.get_tensor_shape(graph_out_name)
-            out_dtype = model.get_tensor_datatype(graph_out_name)
-            # adjust shape
-            out_shape[self.axis] = self.k
-            # make new buffer
-            k_tensor = np.array([self.k]).astype(np.int64)
-            k_value = oh.make_tensor_value_info(
-                model.make_new_valueinfo_name(), TensorProto.INT64, [1]
-            )
-            topk_values = oh.make_tensor_value_info(
-                model.make_new_valueinfo_name(), TensorProto.FLOAT, out_shape
-            )
-            topk_indices = oh.make_tensor_value_info(
-                model.make_new_valueinfo_name(), TensorProto.INT64, out_shape
-            )
-            model.graph.value_info.append(k_value)
-            model.set_tensor_datatype(k_value.name, out_dtype)  # TODO set to int64
-            model.graph.value_info.append(topk_values)
-            model.set_tensor_datatype(topk_values.name, out_dtype)
-            # create and append topk node
-            model.set_initializer(k_value.name, k_tensor)
-            topk_node = oh.make_node(
-                "TopK",
-                inputs=[graph_out_name, k_value.name],
-                outputs=[topk_values.name, topk_indices.name],
-                axis=self.axis,
-                largest=self.largest,
-                sorted=self.sorted,
-            )
-            model.graph.node.append(topk_node)
-            # replace the existing output definition with topk indices
-            model.graph.output.insert(0, topk_indices)
-            model.graph.output.pop(1)
-            # set quantization annotation for indices
-            # minimal output dtype for TopK indices dependens on num. classes
-            # assuming UINT32 is large enough for now (FINN has currently no
-            # DataType.INT64)
-            model.set_tensor_datatype(topk_indices.name, DataType.UINT32)
-            return (model, True)
diff --git a/src/finn/transformation/lower_convs_to_matmul.py b/src/finn/transformation/lower_convs_to_matmul.py
deleted file mode 100644
index e5a1f778d0cac48925ecd97ae8b970f7bdab9c4f..0000000000000000000000000000000000000000
--- a/src/finn/transformation/lower_convs_to_matmul.py
+++ /dev/null
@@ -1,171 +0,0 @@
-# Copyright (c) 2020, Xilinx
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-#
-# * Redistributions of source code must retain the above copyright notice, this
-#   list of conditions and the following disclaimer.
-#
-# * Redistributions in binary form must reproduce the above copyright notice,
-#   this list of conditions and the following disclaimer in the documentation
-#   and/or other materials provided with the distribution.
-#
-# * Neither the name of FINN nor the names of its
-#   contributors may be used to endorse or promote products derived from
-#   this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-import numpy as np
-from onnx import TensorProto
-from onnx import helper
-
-from finn.transformation import Transformation
-from finn.transformation.infer_shapes import InferShapes
-from finn.util.basic import get_by_name
-
-
-class LowerConvsToMatMul(Transformation):
-    """Replace Conv layers with pairs of Im2Col-MatMul layers, plus Transpose
-    layers to keep the original data layout."""
-
-    def apply(self, model):
-        graph = model.graph
-        node_ind = 0
-        graph_modified = False
-        for n in graph.node:
-            node_ind += 1
-            if n.op_type == "Conv":
-                graph_modified = True
-                cnv_input = n.input[0]
-                cnv_output = n.output[0]
-                idt = model.get_tensor_datatype(cnv_input)
-                odt = model.get_tensor_datatype(cnv_output)
-                # extract conv parameters
-                k = get_by_name(n.attribute, "kernel_shape").ints[-1]
-                pad = get_by_name(n.attribute, "pads").ints[-1]
-                stride = get_by_name(n.attribute, "strides").ints[-1]
-                group = get_by_name(n.attribute, "group").i
-                weight_name = n.input[1]
-                W_conv = model.get_initializer(weight_name)
-                ifm_ch = model.get_tensor_shape(n.input[0])[1]  # assume NCHW
-                ofm_ch = model.get_tensor_shape(n.output[0])[1]  # assume NCHW
-                ifm_dim = model.get_tensor_shape(n.input[0])[-1]  # assume NCHW
-                ofm_dim = model.get_tensor_shape(n.output[0])[-1]  # assume NCHW
-
-                # if depthwise conv create sparse matrix and variable "dw"
-                # to store as attribute in Im2Col that indicates that the created
-                # Im2Col node belongs to a depthwise convolution
-                dw = False
-                if group == ifm_ch and ofm_ch == ifm_ch:
-                    W_sparse = np.zeros((ofm_ch, ifm_ch, k, k))
-                    for ch in range(ifm_ch):
-                        W_sparse[ch][ch] = W_conv[ch][0]
-                    W_conv = W_sparse.astype(np.float32)
-                    # we need to store information of the
-                    # sparsity of the weight matrix. For this
-                    # we use the sparsity annotation of the
-                    # weight tensor
-                    sparsity = {"dw": {"kernel_shape": k}}
-                    model.set_tensor_sparsity(weight_name, sparsity)
-                    # additionally create variable "dw" to store
-                    # as attribute in Im2Col that indicates that the created
-                    # Im2Col node belongs to a depthwise convolution
-                    dw = True
-
-                # reuse conv weights for new matmul weights
-                # conv weights are [OFM][IFM][k][k]
-                # first convert to [OFM][k][k][IFM] (to remain compatible with
-                # finn-hlslib and how it does im2col/sliding window)
-                W_matmul = W_conv.transpose(0, 2, 3, 1)
-                # reshape into [OFM][k*k*IFM] matrix
-                W_matmul = W_matmul.reshape(ofm_ch, ifm_ch * k * k)
-                # transpose to get ONNX-compatible [k*k*IFM][OFM] matrix
-                W_matmul = W_matmul.T
-                model.set_initializer(weight_name, W_matmul)
-
-                # create new intermediate values
-                inp_trans_out = helper.make_tensor_value_info(
-                    model.make_new_valueinfo_name(),
-                    TensorProto.FLOAT,
-                    (1, ifm_dim, ifm_dim, ifm_ch),  # NHWC
-                )
-                graph.value_info.append(inp_trans_out)
-                inp_trans_out = inp_trans_out.name
-                model.set_tensor_datatype(inp_trans_out, idt)
-
-                need_im2col = True
-                if k == 1 and pad == 0 and stride == 1:
-                    need_im2col = False
-
-                if need_im2col:
-                    im2col_out = helper.make_tensor_value_info(
-                        model.make_new_valueinfo_name(),
-                        TensorProto.FLOAT,
-                        (1, ofm_dim, ofm_dim, ifm_ch * k * k),
-                    )
-                    graph.value_info.append(im2col_out)
-                    im2col_out = im2col_out.name
-                    model.set_tensor_datatype(im2col_out, idt)
-
-                matmul_out = helper.make_tensor_value_info(
-                    model.make_new_valueinfo_name(),
-                    TensorProto.FLOAT,
-                    (1, ofm_dim, ofm_dim, ofm_ch),
-                )
-                graph.value_info.append(matmul_out)
-                matmul_out = matmul_out.name
-                model.set_tensor_datatype(matmul_out, odt)
-
-                # create new nodes
-                # NCHW -> NHWC
-                inp_trans_node = helper.make_node(
-                    "Transpose", [cnv_input], [inp_trans_out], perm=[0, 2, 3, 1]
-                )
-                # lower input tensor
-                matmul_input = inp_trans_out
-                if need_im2col:
-                    matmul_input = im2col_out
-                    im2col_node = helper.make_node(
-                        "Im2Col",
-                        [inp_trans_out],
-                        [im2col_out],
-                        domain="finn",
-                        stride=stride,
-                        kernel_size=k,
-                        pad_amount=pad,
-                        input_shape="(1,{},{},{})".format(ifm_dim, ifm_dim, ifm_ch),
-                        depthwise=dw,
-                    )
-
-                # do matmul
-                matmul_node = helper.make_node(
-                    "MatMul", [matmul_input, weight_name], [matmul_out]
-                )
-                # NHWC -> NCHW
-                out_trans_node = helper.make_node(
-                    "Transpose", [matmul_out], [cnv_output], perm=[0, 3, 1, 2]
-                )
-                # insert nodes where the conv is to preserve topological ordering
-                graph.node.insert(node_ind, inp_trans_node)
-                if need_im2col:
-                    graph.node.insert(node_ind + 1, im2col_node)
-                    graph.node.insert(node_ind + 2, matmul_node)
-                    graph.node.insert(node_ind + 3, out_trans_node)
-                else:
-                    graph.node.insert(node_ind + 1, matmul_node)
-                    graph.node.insert(node_ind + 2, out_trans_node)
-                # remove old nodes
-                graph.node.remove(n)
-        model = model.transform(InferShapes())
-        return (model, graph_modified)
diff --git a/src/finn/transformation/merge_onnx_models.py b/src/finn/transformation/merge_onnx_models.py
deleted file mode 100644
index 2a84910832fc55b9383b2d731c05c53f42368631..0000000000000000000000000000000000000000
--- a/src/finn/transformation/merge_onnx_models.py
+++ /dev/null
@@ -1,164 +0,0 @@
-# Copyright (c) 2020, Xilinx
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-#
-# * Redistributions of source code must retain the above copyright notice, this
-#   list of conditions and the following disclaimer.
-#
-# * Redistributions in binary form must reproduce the above copyright notice,
-#   this list of conditions and the following disclaimer in the documentation
-#   and/or other materials provided with the distribution.
-#
-# * Neither the name of FINN nor the names of its
-#   contributors may be used to endorse or promote products derived from
-#   this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-import copy
-import warnings
-from onnx import helper
-
-from finn.transformation import Transformation
-from finn.core.modelwrapper import ModelWrapper
-from finn.transformation.infer_shapes import InferShapes
-from finn.transformation.infer_datatypes import InferDataTypes
-from finn.transformation.infer_data_layouts import InferDataLayouts
-from finn.transformation.general import (
-    GiveReadableTensorNames,
-    GiveRandomTensorNames,
-    GiveUniqueNodeNames,
-    GiveUniqueParameterTensors,
-)
-
-
-class MergeONNXModels(Transformation):
-    """Merges two models. The model passed in the transformation will be inserted before
-    the model the transformation is applied on, the resulting model is returned.
-    This transformation will try to connect graph.output[0] of the pre model and
-    graph.input[0] of the post model.
-    If more than one input or output exists, a warning is raised."""
-
-    def __init__(self, pre_model):
-        super().__init__()
-        # use deep copy of model that should be inserted in the beginning of
-        # the other model to ensure that it stays unchanged
-        self.pre_model = copy.deepcopy(pre_model)
-
-    def apply(self, model):
-        graph_modified = False
-        pre_model = self.pre_model
-        post_model = copy.deepcopy(model)
-        # to avoid mix-ups, start by giving all tensors random names
-        pre_model = pre_model.transform(GiveRandomTensorNames())
-        post_model = post_model.transform(GiveRandomTensorNames())
-
-        # check for dynamic outputs of pre model
-        dyn_outp = []
-        for outp in pre_model.graph.output:
-            init_val = pre_model.get_initializer(outp.name)
-            if init_val is None:
-                dyn_outp.append(outp)
-
-        if len(dyn_outp) != 1:
-            warnings.warn(
-                "The pre model has more than one dynamic output! The transformation "
-                "tries to connect the first dynamic output to the first dynamic input "
-                "of the post model."
-            )
-
-        # check for dynamic inputs of post model
-        dyn_inp = []
-        for inp in post_model.graph.input:
-            init_val = post_model.get_initializer(inp.name)
-            if init_val is None:
-                dyn_inp.append(inp)
-
-        if len(dyn_inp) != 1:
-            warnings.warn(
-                "The post model has more than one dynamic input! The transformation "
-                "tries to connect the first dynamic input to the first dynamic output "
-                "of the pre model."
-            )
-
-        # erase all node names to avoid conflict
-        for n in pre_model.graph.node:
-            n.name = ""
-        for n in post_model.graph.node:
-            n.name = ""
-
-        # check if models can be merged
-        output_model_a = dyn_outp[0].name
-        input_model_b = dyn_inp[0].name
-        output_a_shape = pre_model.get_tensor_shape(output_model_a)
-        input_b_shape = post_model.get_tensor_shape(input_model_b)
-        assert (
-            output_a_shape == input_b_shape
-        ), "Models can't be merged! Shapes don't match."
-
-        # connect output of one model to input of the other
-        for n in pre_model.graph.node:
-            if output_model_a == n.output[0]:
-                n.output[0] = input_model_b
-
-        # extract information for new model
-
-        # nodes
-        node_pre = [node for node in pre_model.graph.node]
-        node_post = [node for node in post_model.graph.node]
-        node_new = node_pre + node_post
-
-        # in and output
-        inp = pre_model.graph.input[0]
-        outp = post_model.graph.output[0]
-
-        vi_pre = [x for x in pre_model.graph.value_info]
-        out_pre = [x for x in pre_model.graph.output]
-        qa_pre = [x for x in pre_model.graph.quantization_annotation]
-        init_pre = [x for x in pre_model.graph.initializer]
-
-        vi_post = [x for x in post_model.graph.value_info]
-        qa_post = [x for x in post_model.graph.quantization_annotation]
-        init_post = [x for x in post_model.graph.initializer]
-
-        vi_new = vi_pre + vi_post + out_pre
-        qa_new = qa_pre + qa_post
-        init_new = init_pre + init_post
-
-        # create new graph and model
-        new_graph = helper.make_graph(
-            nodes=node_new,
-            name="fuse-graph",
-            inputs=[inp],
-            outputs=[outp],
-            value_info=vi_new,
-        )
-
-        new_model = helper.make_model(new_graph, producer_name="fuse_model")
-        new_model = ModelWrapper(new_model)
-
-        for i in init_new:
-            new_model.graph.initializer.append(i)
-        for qa in qa_new:
-            new_model.graph.quantization_annotation.append(qa)
-
-        # tidy-up new model
-        model = new_model
-        model = model.transform(InferShapes())
-        model = model.transform(InferDataTypes())
-        model = model.transform(InferDataLayouts())
-        model = model.transform(GiveUniqueNodeNames())
-        model = model.transform(GiveUniqueParameterTensors())
-        model = model.transform(GiveReadableTensorNames())
-
-        return (model, graph_modified)
diff --git a/src/finn/transformation/move_reshape.py b/src/finn/transformation/move_reshape.py
index 9943d371dad79a977b61810bcddafdcba505d6cc..a07eaf142293487237b3f2b93460ba492eb5368d 100644
--- a/src/finn/transformation/move_reshape.py
+++ b/src/finn/transformation/move_reshape.py
@@ -1,4 +1,4 @@
-from finn.transformation import Transformation
+from finn.transformation.base import Transformation
 from finn.util.basic import get_by_name
 
 
diff --git a/src/finn/transformation/streamline/__init__.py b/src/finn/transformation/streamline/__init__.py
index d7686eaadcbc800542ab96c5f45145857412b773..e78b798ff6f31ff705b733d47dcfe7bcdc6aa127 100644
--- a/src/finn/transformation/streamline/__init__.py
+++ b/src/finn/transformation/streamline/__init__.py
@@ -25,8 +25,12 @@
 # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+# namespace package, extend path
+from pkgutil import extend_path
 
-from finn.transformation import Transformation
+__path__ = extend_path(__path__, __name__)
+
+from finn.transformation.base import Transformation
 from finn.transformation.infer_datatypes import InferDataTypes
 from finn.transformation.general import (
     ConvertSubToAdd,
diff --git a/src/finn/transformation/streamline/absorb.py b/src/finn/transformation/streamline/absorb.py
index 0f2c5525d91263b44002677b505087d38408333a..fa2d7a714ad894ebb19099c7ed73e42e12ffdf44 100644
--- a/src/finn/transformation/streamline/absorb.py
+++ b/src/finn/transformation/streamline/absorb.py
@@ -32,7 +32,7 @@ import warnings
 
 from finn.core.datatype import DataType
 import finn.core.data_layout as DataLayout
-from finn.transformation import Transformation
+from finn.transformation.base import Transformation
 from finn.util.basic import get_by_name
 from finn.custom_op.registry import getCustomOp
 from finn.transformation.infer_shapes import InferShapes
diff --git a/src/finn/transformation/streamline/collapse_repeated.py b/src/finn/transformation/streamline/collapse_repeated.py
index 769bed841ce07c1c9c62f762de4b2c0937a6d68f..19f1ec3e836f3ec38aa4b716f5d6a25ca0782197 100644
--- a/src/finn/transformation/streamline/collapse_repeated.py
+++ b/src/finn/transformation/streamline/collapse_repeated.py
@@ -28,7 +28,7 @@
 
 from onnx import helper as oh
 
-from finn.transformation import Transformation
+from finn.transformation.base import Transformation
 from finn.transformation.infer_shapes import InferShapes
 from finn.core.datatype import DataType
 
diff --git a/src/finn/transformation/streamline/remove.py b/src/finn/transformation/streamline/remove.py
index ddc4233ddafbc70c4d20d316ea72ea6bba1b82a8..12c6984c6e66e1917d2a1e0a74c8620ccb6afabc 100644
--- a/src/finn/transformation/streamline/remove.py
+++ b/src/finn/transformation/streamline/remove.py
@@ -27,10 +27,11 @@
 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
 
-from finn.transformation import Transformation
+from finn.transformation.base import Transformation
 from finn.transformation.infer_shapes import InferShapes
 import numpy as np
 
+
 class RemoveIdentityOps(Transformation):
     """Remove identity ops like Add/Sub with zero or Mul/Div with one"""
 
diff --git a/src/finn/transformation/streamline/reorder.py b/src/finn/transformation/streamline/reorder.py
index f4c1dc1306b67e5807c25cfb08c961729dbfbdf6..bae3c9f22f4e5b2a525f15d1d948e42a4087953a 100644
--- a/src/finn/transformation/streamline/reorder.py
+++ b/src/finn/transformation/streamline/reorder.py
@@ -31,7 +31,7 @@ import warnings
 from onnx import helper as oh
 from onnx import TensorProto
 
-from finn.transformation import Transformation
+from finn.transformation.base import Transformation
 import finn.core.data_layout as DataLayout
 from finn.transformation.infer_shapes import InferShapes
 from finn.transformation.infer_datatypes import InferDataTypes
diff --git a/src/finn/transformation/streamline/round_thresholds.py b/src/finn/transformation/streamline/round_thresholds.py
index 8626ef40619b067c6672c9017ddcb747998c3f2c..ba476504a4213a7d004113c39e2285beeecdddec 100644
--- a/src/finn/transformation/streamline/round_thresholds.py
+++ b/src/finn/transformation/streamline/round_thresholds.py
@@ -28,7 +28,7 @@
 
 import numpy as np
 
-from finn.transformation import Transformation
+from finn.transformation.base import Transformation
 
 
 class RoundAndClipThresholds(Transformation):
diff --git a/src/finn/transformation/streamline/sign_to_thres.py b/src/finn/transformation/streamline/sign_to_thres.py
index d2b51df7a43c830516897e6bf7d2210698269da8..4e35012ceb4f84284ff2a96a60e4a9bd58a65cce 100644
--- a/src/finn/transformation/streamline/sign_to_thres.py
+++ b/src/finn/transformation/streamline/sign_to_thres.py
@@ -30,7 +30,7 @@ import numpy as np
 from onnx import helper as oh
 
 from finn.core.datatype import DataType
-from finn.transformation import Transformation
+from finn.transformation.base import Transformation
 
 
 class ConvertSignToThres(Transformation):
diff --git a/src/finn/util/__init__.py b/src/finn/util/__init__.py
deleted file mode 100644
index 83c8e8bed70797f7d6c0138968f750f72e790386..0000000000000000000000000000000000000000
--- a/src/finn/util/__init__.py
+++ /dev/null
@@ -1,27 +0,0 @@
-# Copyright (c) 2020, Xilinx
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-#
-# * Redistributions of source code must retain the above copyright notice, this
-#   list of conditions and the following disclaimer.
-#
-# * Redistributions in binary form must reproduce the above copyright notice,
-#   this list of conditions and the following disclaimer in the documentation
-#   and/or other materials provided with the distribution.
-#
-# * Neither the name of FINN nor the names of its
-#   contributors may be used to endorse or promote products derived from
-#   this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/src/finn/util/basic.py b/src/finn/util/basic.py
deleted file mode 100644
index cc759bebb1b856a84e25978d442e460332092d23..0000000000000000000000000000000000000000
--- a/src/finn/util/basic.py
+++ /dev/null
@@ -1,442 +0,0 @@
-# Copyright (c) 2020, Xilinx
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-#
-# * Redistributions of source code must retain the above copyright notice, this
-#   list of conditions and the following disclaimer.
-#
-# * Redistributions in binary form must reproduce the above copyright notice,
-#   this list of conditions and the following disclaimer in the documentation
-#   and/or other materials provided with the distribution.
-#
-# * Neither the name of FINN nor the names of its
-#   contributors may be used to endorse or promote products derived from
-#   this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-import os
-import random
-import string
-import subprocess
-import tempfile
-import warnings
-
-import numpy as np
-
-from finn.core.datatype import DataType
-
-# mapping from PYNQ board names to FPGA part names
-pynq_part_map = dict()
-pynq_part_map["Ultra96"] = "xczu3eg-sbva484-1-e"
-pynq_part_map["Pynq-Z1"] = "xc7z020clg400-1"
-pynq_part_map["Pynq-Z2"] = "xc7z020clg400-1"
-pynq_part_map["ZCU102"] = "xczu9eg-ffvb1156-2-e"
-pynq_part_map["ZCU104"] = "xczu7ev-ffvc1156-2-e"
-
-# native AXI HP port width (in bits) for PYNQ boards
-pynq_native_port_width = dict()
-pynq_native_port_width["Pynq-Z1"] = 64
-pynq_native_port_width["Pynq-Z2"] = 64
-pynq_native_port_width["Ultra96"] = 128
-pynq_native_port_width["ZCU102"] = 128
-pynq_native_port_width["ZCU104"] = 128
-
-# Alveo device and platform mappings
-alveo_part_map = dict()
-alveo_part_map["U50"] = "xcu50-fsvh2104-2L-e"
-alveo_part_map["U200"] = "xcu200-fsgd2104-2-e"
-alveo_part_map["U250"] = "xcu250-figd2104-2L-e"
-alveo_part_map["U280"] = "xcu280-fsvh2892-2L-e"
-
-alveo_default_platform = dict()
-alveo_default_platform["U50"] = "xilinx_u50_gen3x16_xdma_201920_3"
-alveo_default_platform["U200"] = "xilinx_u200_xdma_201830_2"
-alveo_default_platform["U250"] = "xilinx_u250_xdma_201830_2"
-alveo_default_platform["U280"] = "xilinx_u280_xdma_201920_3"
-
-
-def get_rtlsim_trace_depth():
-    """Return the trace depth for rtlsim via PyVerilator. Controllable
-    via the RTLSIM_TRACE_DEPTH environment variable. If the env.var. is
-    undefined, the default value of 1 is returned. A trace depth of 1
-    will only show top-level signals and yield smaller .vcd files.
-
-    The following depth values are of interest for whole-network stitched IP
-    rtlsim:
-    - level 1 shows top-level input/output streams
-    - level 2 shows per-layer input/output streams
-    - level 3 shows per full-layer I/O including FIFO count signals
-    """
-
-    try:
-        return int(os.environ["RTLSIM_TRACE_DEPTH"])
-    except KeyError:
-        return 1
-
-
-def get_remote_vivado():
-    """Return the address of the remote Vivado synthesis server as set by the,
-    REMOTE_VIVADO environment variable, otherwise return None"""
-
-    try:
-        return os.environ["REMOTE_VIVADO"]
-    except KeyError:
-        return None
-
-
-def get_num_default_workers():
-    """Return the number of workers for parallel transformations. Controllable
-    via the NUM_DEFAULT_WORKERS environment variable. If the env.var. is
-    undefined, the default value of 1 is returned.
-    """
-
-    try:
-        return int(os.environ["NUM_DEFAULT_WORKERS"])
-    except KeyError:
-        return 1
-
-
-def get_finn_root():
-    "Return the root directory that FINN is cloned into."
-
-    try:
-        return os.environ["FINN_ROOT"]
-    except KeyError:
-        raise Exception(
-            """Environment variable FINN_ROOT must be set
-        correctly. Please ensure you have launched the Docker contaier correctly.
-        """
-        )
-
-
-def get_execution_error_thresh():
-    "Return the max error that is allowed for rounding in FINN execution."
-    try:
-        return float(os.environ["ERROR_THRESH"])
-    except KeyError:
-        return 1e-2
-
-
-def get_sanitize_quant_tensors():
-    """Return whether tensors with quantization annotations should be sanitized.
-    Enabled by default, disabling will yield faster ONNX execution but may give
-    incorrect results. Use with caution."""
-    try:
-        return int(os.environ["SANITIZE_QUANT_TENSORS"])
-    except KeyError:
-        # enabled by default
-        return 1
-
-
-def make_build_dir(prefix=""):
-    """Creates a temporary folder with given prefix to be used as a build dir.
-    Use this function instead of tempfile.mkdtemp to ensure any generated files
-    will survive on the host after the FINN Docker container exits."""
-    try:
-        inst_prefix = os.environ["FINN_INST_NAME"] + "/"
-        return tempfile.mkdtemp(prefix=inst_prefix + prefix)
-    except KeyError:
-        raise Exception(
-            """Environment variable FINN_INST_NAME must be set
-        correctly. Please ensure you have launched the Docker contaier correctly.
-        """
-        )
-
-
-def get_by_name(container, name, name_field="name"):
-    """Return item from container by .name field if it exists, None otherwise.
-    Will throw an Exception if multiple items are found, since this violates the
-    ONNX standard."""
-    names = [getattr(x, name_field) for x in container]
-
-    inds = [i for i, e in enumerate(names) if e == name]
-    if len(inds) > 1:
-        raise Exception("Found multiple get_by_name matches, undefined behavior")
-    elif len(inds) == 0:
-        return None
-    else:
-        ind = inds[0]
-        return container[ind]
-
-
-def remove_by_name(container, name, name_field="name"):
-    """Remove item from container by .name field if it exists."""
-    item = get_by_name(container, name, name_field)
-    if item is not None:
-        container.remove(item)
-
-
-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 interleave_matrix_outer_dim_from_partitions(matrix, n_partitions):
-    """Interleave the outermost dimension of a matrix from given
-    partitions (n_partitions)."""
-    if type(matrix) != np.ndarray or matrix.dtype != np.float32:
-        # try to convert to a float numpy array (container dtype is float)
-        matrix = np.asarray(matrix, dtype=np.float32)
-    shp = matrix.shape
-    ndim = matrix.ndim
-    # ensure # partitions evenly divide the outermost dimension
-    assert (
-        shp[0] % n_partitions == 0
-    ), """The outermost dimension is not divisable
-    by the number of partitions."""
-    # only tested for matrices
-    assert (
-        ndim == 2
-    ), """The dimension of the matrix is not 2. Currently this function
-    only works for matrices."""
-    # interleave rows between PEs using reshape + transpose
-    matrix_r = matrix.reshape(-1, n_partitions, shp[1]).transpose((1, 0, 2))
-    matrix_r = matrix_r.reshape(n_partitions, -1, shp[1])
-    return matrix_r
-
-
-def roundup_to_integer_multiple(x, factor):
-    """Round up integer x to the nearest integer multiple of integer factor.
-    Returns x if factor is set to -1. Both x and factor must otherwise be
-    positive."""
-    # ensure integers
-    assert int(x) == x, "The input x is not an integer."
-    assert int(factor) == factor, "The input factor is not an integer."
-    # use -1 to indicate no padding needed
-    if factor == -1:
-        return x
-    # ensure positive values
-    assert factor > 0 and x > 0, "Factor and x are <= 0."
-    if x < factor:
-        return factor
-    else:
-        if x % factor == 0:
-            return x
-        else:
-            return x + (factor - (x % factor))
-
-
-def pad_tensor_to_multiple_of(ndarray, pad_to_dims, val=0, distr_pad=False):
-    """Pad each dimension of given NumPy ndarray using val, so that each
-    dimension is a multiple of the respective value in pad_to_dims. -1 means
-    do not pad that particular dimension. If distr_pad is False, all padding
-    will be inserted after the existing values; otherwise it will be split
-    evenly between before and after the existing values, with one extra value
-    inserted after if the padding amount is not divisible by two."""
-    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)
-    assert ndarray.ndim == len(
-        pad_to_dims
-    ), """The dimensions of the input
-    array don't match the length of the pad_to_dims value."""
-    # compute the desired shape
-    desired = zip(list(ndarray.shape), list(pad_to_dims))
-    desired = map(lambda x: roundup_to_integer_multiple(x[0], x[1]), desired)
-    desired = np.asarray(list(desired), dtype=np.int32)
-    current = np.asarray(ndarray.shape, dtype=np.int32)
-    pad_amt = desired - current
-    # add padding to get to the desired shape
-    if distr_pad:
-        pad_before = (pad_amt // 2).astype(np.int32)
-        pad_after = pad_amt - pad_before
-        pad_amt = list(zip(pad_before, pad_after))
-    else:
-        # all padding is added after the existing values
-        pad_amt = list(map(lambda x: (0, x), pad_amt))
-    ret = np.pad(ndarray, pad_amt, mode="constant", constant_values=val)
-    assert (
-        np.asarray(ret.shape, dtype=np.int32) == desired
-    ).all(), """The
-    calculated output array doesn't match the desired/expected one."""
-    return ret
-
-
-def calculate_matvec_accumulator_range(matrix, vec_dt):
-    """Calculate the minimum and maximum possible result (accumulator) values
-    for a dot product x * A, given matrix A of dims (MW, MH), and vector (1, MW)
-    with datatype vec_dt. Returns (acc_min, acc_max).
-    """
-    min_weight = matrix.min()
-    max_weight = matrix.max()
-    perceptive_field_elems = matrix.shape[0]
-    min_input = vec_dt.min()
-    max_input = vec_dt.max()
-    # calculate minimum and maximum values of accumulator
-    # assume inputs span the whole range of the input datatype
-    acc_min = perceptive_field_elems * min(
-        min_weight * max_input,
-        min_weight * min_input,
-        max_weight * max_input,
-        max_weight * min_input,
-    )
-    acc_max = perceptive_field_elems * max(
-        min_weight * max_input,
-        min_weight * min_input,
-        max_weight * max_input,
-        max_weight * min_input,
-    )
-    return (acc_min, acc_max)
-
-
-def gen_finn_dt_tensor(finn_dt, tensor_shape):
-    """Generates random tensor in given shape and with given FINN DataType."""
-    if type(tensor_shape) == list:
-        tensor_shape = tuple(tensor_shape)
-    if finn_dt == DataType.BIPOLAR:
-        tensor_values = np.random.randint(2, size=tensor_shape)
-        tensor_values = 2 * tensor_values - 1
-    elif finn_dt == DataType.BINARY:
-        tensor_values = np.random.randint(2, size=tensor_shape)
-    elif "INT" in finn_dt.name or finn_dt == DataType.TERNARY:
-        tensor_values = np.random.randint(
-            finn_dt.min(), high=finn_dt.max() + 1, size=tensor_shape
-        )
-    else:
-        raise ValueError(
-            "Datatype {} is not supported, no tensor could be generated".format(finn_dt)
-        )
-    # always use float type as container
-    return tensor_values.astype(np.float32)
-
-
-def calculate_signed_dot_prod_range(dt_a, dt_b, len):
-    """Returns the (min,max) values a dot product between two signed vectors of
-    types dt_a and dt_b of len elements can take."""
-    assert (
-        dt_a.signed() and dt_b.signed()
-    ), """The input values are not both
-    signed vectors."""
-    min_prod = 2 ** 30
-    max_prod = -(2 ** 30)
-    for a_val in [dt_a.min(), dt_a.max()]:
-        for b_val in [dt_b.min(), dt_b.max()]:
-            prod = a_val * b_val * len
-            if prod < min_prod:
-                min_prod = prod
-            if prod > max_prod:
-                max_prod = prod
-    return (min_prod, max_prod)
-
-
-def sanitize_quant_values(model, node_tensors, execution_context, check_values=False):
-    """ Sanitize given list of tensors in execution_context by rounding values
-    that are supposed to be integers (as indicated by their quantization
-    annotation). Will raise an assertion if the amount of rounding is too large.
-    Returns the sanitized execution context.
-
-    If check_values is specified, an extra DataType.allowed() check will be
-    performed on any rounded tensors.
-
-    Background:
-    FINN uses floating point tensors as a carrier data type to represent
-    integers. Floating point arithmetic can introduce rounding errors, e.g.
-    (int_num * float_scale) / float_scale is not always equal to int_num.
-    We use this function to ensure that the values that are supposed to be
-    integers are indeed integers.
-    """
-
-    for tensor in node_tensors:
-        dtype = model.get_tensor_datatype(tensor)
-        # floats don't need sanitization, skip to next
-        # introduces less quicker runtime
-        if dtype == DataType.FLOAT32:
-            continue
-        current_values = execution_context[tensor]
-        updated_values = current_values
-        has_to_be_rounded = False
-        # TODO: vectorize with numpy
-        for value in np.nditer(current_values):
-            if not dtype.allowed(value):
-                has_to_be_rounded = True
-                break
-        if has_to_be_rounded:
-            updated_values = np.round(current_values)
-            warnings.warn(
-                "The values of tensor {} can't be represented "
-                "with the set FINN datatype ({}), they will be rounded to match the "
-                "FINN datatype.".format(tensor, dtype)
-            )
-        # check if rounded values are not too far from original values
-        max_error = max(np.abs(current_values - updated_values).flatten())
-        if max_error <= get_execution_error_thresh():
-            if check_values is True:
-                # check again if values can now be represented with set finn datatype
-                # TODO: vectorize with numpy
-                for value in np.nditer(updated_values):
-                    if not dtype.allowed(value):
-                        raise Exception(
-                            """Values can't be represented with set
-                                finn datatype ({}) for input {}""".format(
-                                dtype, tensor
-                            )
-                        )
-            execution_context[tensor] = updated_values
-        else:
-            raise Exception(
-                """Rounding error is too high to match set FINN
-            datatype ({}) for input {}""".format(
-                    dtype, tensor
-                )
-            )
-    return execution_context
-
-
-class CppBuilder:
-    """Builds the g++ compiler command to produces the executable of the c++ code
-    in code_gen_dir which is passed to the function build() of this class."""
-
-    def __init__(self):
-        self.include_paths = []
-        self.cpp_files = []
-        self.executable_path = ""
-        self.code_gen_dir = ""
-        self.compile_components = []
-        self.compile_script = ""
-
-    def append_includes(self, library_path):
-        """Adds given library path to include_paths list."""
-        self.include_paths.append(library_path)
-
-    def append_sources(self, cpp_file):
-        """Adds given c++ file to cpp_files list."""
-        self.cpp_files.append(cpp_file)
-
-    def set_executable_path(self, path):
-        """Sets member variable "executable_path" to given path."""
-        self.executable_path = path
-
-    def build(self, code_gen_dir):
-        """Builds the g++ compiler command according to entries in include_paths
-        and cpp_files lists. Saves it in bash script in given folder and
-        executes it."""
-        # raise error if includes are empty
-        self.code_gen_dir = code_gen_dir
-        self.compile_components.append("g++ -o " + str(self.executable_path))
-        for cpp_file in self.cpp_files:
-            self.compile_components.append(cpp_file)
-        for lib in self.include_paths:
-            self.compile_components.append(lib)
-        bash_compile = ""
-        for component in self.compile_components:
-            bash_compile += str(component) + " "
-        self.compile_script = str(self.code_gen_dir) + "/compile.sh"
-        with open(self.compile_script, "w") as f:
-            f.write("#!/bin/bash \n")
-            f.write(bash_compile + "\n")
-        bash_command = ["bash", self.compile_script]
-        process_compile = subprocess.Popen(bash_command, stdout=subprocess.PIPE)
-        process_compile.communicate()
diff --git a/src/finn/util/create.py b/src/finn/util/create.py
deleted file mode 100644
index 853cdd0d44a05426b34bf1db3caa58d9289b2e9e..0000000000000000000000000000000000000000
--- a/src/finn/util/create.py
+++ /dev/null
@@ -1,178 +0,0 @@
-# Copyright (c) 2020, Xilinx
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-#
-# * Redistributions of source code must retain the above copyright notice, this
-#   list of conditions and the following disclaimer.
-#
-# * Redistributions in binary form must reproduce the above copyright notice,
-#   this list of conditions and the following disclaimer in the documentation
-#   and/or other materials provided with the distribution.
-#
-# * Neither the name of FINN nor the names of its
-#   contributors may be used to endorse or promote products derived from
-#   this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-import numpy as np
-from finn.core.modelwrapper import ModelWrapper
-from onnx import TensorProto, helper
-from finn.core.datatype import DataType
-from finn.util.basic import calculate_signed_dot_prod_range, gen_finn_dt_tensor
-
-
-def hls_random_mlp_maker(layer_spec):
-    """Create an MLP of given specification using HLSCustomOp instances.
-    Generate random weights/thresholds of appropriate size."""
-    ret = []
-    for l in layer_spec:
-        idt = l["idt"]
-        wdt = l["wdt"]
-        mw = l["mw"]
-        mh = l["mh"]
-        act = l["act"]
-        l["W"] = gen_finn_dt_tensor(wdt, (mw, mh))
-        if act is None:
-            # no activation, produce accumulators
-            T = None
-            tdt = None
-            if wdt == DataType.BIPOLAR and idt == DataType.BIPOLAR:
-                odt = DataType.UINT32
-            else:
-                odt = DataType.INT32
-        else:
-            odt = act
-            (min, max) = calculate_signed_dot_prod_range(idt, wdt, mw)
-            n_steps = act.get_num_possible_values() - 1
-            T = np.random.randint(min, max - 1, (mh, n_steps)).astype(np.float32)
-            # provide non-decreasing thresholds
-            T = np.sort(T, axis=1)
-            # generate thresholds for activation
-            if wdt == DataType.BIPOLAR and idt == DataType.BIPOLAR:
-                tdt = DataType.UINT32
-                # bias thresholds to be positive
-                T = np.ceil((T + mw) / 2)
-                assert (T >= 0).all()
-            else:
-                tdt = DataType.INT32
-        l["T"] = T
-        l["tdt"] = tdt
-        l["odt"] = odt
-        ret.append(l)
-
-    return hls_mlp_maker(ret)
-
-
-def hls_mlp_maker(layer_spec):
-    """Create an MLP of given specification using HLSCustomOp instances."""
-
-    current_in_name = ""
-    current_out_name = ""
-    i = 0
-
-    graph = helper.make_graph(nodes=[], name="mlp", inputs=[], outputs=[])
-
-    model = helper.make_model(graph, producer_name="finn")
-    model = ModelWrapper(model)
-
-    for l in layer_spec:
-        current_W_name = "W_%d" % i
-        current_T_name = "T_%d" % i
-        current_in_name = "act_%d" % i
-        current_out_name = "act_%d" % (i + 1)
-
-        W = l["W"]
-        (mw, mh) = W.shape
-        T = l["T"]
-        pe = l["pe"]
-        simd = l["simd"]
-        wdt = l["wdt"]
-        idt = l["idt"]
-        tdt = l["tdt"]
-        odt = l["odt"]
-
-        if i == 0:
-            global_in = helper.make_tensor_value_info(
-                current_in_name, TensorProto.FLOAT, [1, mw]
-            )
-            model.graph.input.append(global_in)
-
-        if i == len(layer_spec) - 1:
-            global_out = helper.make_tensor_value_info(
-                current_out_name, TensorProto.FLOAT, [1, mh]
-            )
-            model.graph.output.append(global_out)
-
-        # there are two ways to implement bipolar weights and inputs for
-        # StreamingFC:
-        # - specify their datatypes as such
-        # - specify their datatypes as BINARY as use binaryXnorMode
-        if wdt == DataType.BIPOLAR and idt == DataType.BIPOLAR:
-            # we'll internally convert weights/inputs to binary and specify the
-            # datatypes as such, and also set the binaryXnorMode attribute to 1
-            export_wdt = DataType.BINARY
-            export_idt = DataType.BINARY
-            binary_xnor_mode = 1
-        else:
-            export_wdt = wdt
-            export_idt = idt
-            binary_xnor_mode = 0
-
-        if T is not None:
-            no_act = 0
-            node_inp_list = [current_in_name, current_W_name, current_T_name]
-            if odt == DataType.BIPOLAR:
-                actval = 0
-            else:
-                actval = odt.min()
-        else:
-            # no thresholds
-            node_inp_list = [current_in_name, current_W_name]
-            actval = 0
-            no_act = 1
-        FCLayer_node = helper.make_node(
-            "StreamingFCLayer_Batch",
-            node_inp_list,
-            [current_out_name],
-            domain="finn",
-            backend="fpgadataflow",
-            resType="ap_resource_lut()",
-            MW=mw,
-            MH=mh,
-            SIMD=simd,
-            PE=pe,
-            inputDataType=export_idt.name,
-            weightDataType=export_wdt.name,
-            outputDataType=odt.name,
-            ActVal=actval,
-            binaryXnorMode=binary_xnor_mode,
-            noActivation=no_act,
-        )
-
-        model.graph.node.append(FCLayer_node)
-        model.set_tensor_datatype(current_in_name, idt)
-        model.set_tensor_datatype(current_out_name, odt)
-        model.set_tensor_datatype(current_W_name, wdt)
-        if binary_xnor_mode:
-            # convert bipolar to binary
-            model.set_initializer(current_W_name, (W + 1) / 2)
-        else:
-            model.set_initializer(current_W_name, W)
-        if T is not None:
-            model.set_tensor_datatype(current_T_name, tdt)
-            model.set_initializer(current_T_name, T)
-        i += 1
-
-    return model
diff --git a/src/finn/util/data_packing.py b/src/finn/util/data_packing.py
deleted file mode 100644
index a087fd2ff2f8a3db8a6caa8332810be9b6188367..0000000000000000000000000000000000000000
--- a/src/finn/util/data_packing.py
+++ /dev/null
@@ -1,397 +0,0 @@
-# Copyright (c) 2020, Xilinx
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-#
-# * Redistributions of source code must retain the above copyright notice, this
-#   list of conditions and the following disclaimer.
-#
-# * Redistributions in binary form must reproduce the above copyright notice,
-#   this list of conditions and the following disclaimer in the documentation
-#   and/or other materials provided with the distribution.
-#
-# * Neither the name of FINN nor the names of its
-#   contributors may be used to endorse or promote products derived from
-#   this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-import binascii
-import os
-import sys
-
-import numpy as np
-from bitstring import BitArray
-
-from finn.core.datatype import DataType
-from finn.util.basic import roundup_to_integer_multiple
-
-
-def array2hexstring(array, dtype, pad_to_nbits, prefix="0x", reverse=False):
-    """
-    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. reverse can be used to reverse the array prior to
-    packing.
-
-    Examples:
-
-    array2hexstring([1, 1, 1, 0], DataType.BINARY, 4) = "0xe"
-
-    array2hexstring([1, 1, 1, 0], DataType.BINARY, 8) = "0x0e"
-
-    array2hexstring([1, 1, 0, 1], DataType.BINARY, 4, reverse=True) = "0xb"
-
-    array2hexstring([1, 1, 1, 0], DataType.BINARY, 8, reverse=True) = "0x07"
-    """
-    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, "The given array is not one-dimensional."
-    if dtype == DataType.BIPOLAR:
-        # convert bipolar values to binary
-        array = (array + 1) / 2
-        dtype = DataType.BINARY
-    # reverse prior to packing, if desired
-    if reverse:
-        array = np.flip(array, -1)
-    lineval = BitArray(length=0)
-    bw = dtype.bitwidth()
-    for val in array:
-        # ensure that this value is permitted by chosen dtype
-        assert dtype.allowed(val), "This value is not permitted by chosen dtype."
-        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 prefix + lineval.hex
-
-
-def hexstring2npbytearray(hexstring, remove_prefix="0x"):
-    """Convert a hex string into a NumPy array of dtype uint8.
-
-    Example:
-
-    hexstring2npbytearray("0f01") = array([15,  1], dtype=uint8)
-    """
-    # remove prefix if found
-    if hexstring.startswith(remove_prefix):
-        lrp = len(remove_prefix)
-        hexstring = hexstring[lrp:]
-    # use Python's built-in bytearray
-    return np.asarray(bytearray.fromhex(hexstring), dtype=np.uint8)
-
-
-def npbytearray2hexstring(npbytearray, prefix="0x"):
-    """Convert a NumPy array of uint8 dtype into a hex string.
-
-    Example:
-
-    npbytearray2hexstring(array([15,  1], dtype=uint8)) = "0x0f01"
-    """
-    return prefix + binascii.hexlify(bytearray(npbytearray)).decode("utf-8")
-
-
-def pack_innermost_dim_as_hex_string(
-    ndarray, dtype, pad_to_nbits, reverse_inner=False, prefix="0x"
-):
-    """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, reverse=reverse_inner, prefix=prefix
-        )
-
-    return np.apply_along_axis(fun, ndarray.ndim - 1, ndarray)
-
-
-def unpack_innermost_dim_from_hex_string(
-    ndarray, dtype, out_shape, packedBits, reverse_inner=False
-):
-    """Convert a NumPy array of hex strings into a FINN NumPy array by unpacking
-    the hex strings into the specified data type. out_shape can be specified
-    such that any padding in the packing dimension is removed. If reverse_inner
-    is set, the innermost unpacked dimension will be reversed."""
-
-    if type(ndarray) != np.ndarray:
-        raise Exception(
-            """unpack_innermost_dim_from_hex_string needs ndarray
-        as input"""
-        )
-    if ndarray.dtype.kind not in {"U", "S"}:
-        raise Exception(
-            """unpack_innermost_dim_from_hex_string needs ndarray of
-        hex strings as input"""
-        )
-    # convert ndarray into flattened list
-    data = ndarray.flatten().tolist()
-    targetBits = dtype.bitwidth()
-    # calculate outer and inner dim shapes
-    outer_dim_elems = 1
-    for dim in range(len(out_shape) - 1):
-        outer_dim_elems = outer_dim_elems * out_shape[dim]
-    inner_dim_elems = out_shape[-1]
-
-    array = []
-    for outer_elem in range(outer_dim_elems):
-        ar_list = []
-        ar_elem = data[0]
-        data.pop(0)
-        ar_elem = ar_elem.split("x")
-        ar_elem_bin = bin(int(ar_elem[1], 16))[2:].zfill(packedBits)
-        ar_elem_bin = [int(x) for x in ar_elem_bin]
-
-        ar_elem_bin.reverse()
-        for i in range(inner_dim_elems):
-            upper_limit = (i + 1) * targetBits
-            lower_limit = i * targetBits
-            elem = ar_elem_bin[lower_limit:upper_limit]
-            elem.reverse()
-            elem_str = "".join(map(str, elem))
-            ar_list.append(int(elem_str, 2))
-        # reverse inner dimension back to "normal" positions
-        if reverse_inner is False:
-            ar_list.reverse()
-
-        # interpret output values correctly
-
-        # interpret values as bipolar
-        if dtype == DataType.BIPOLAR:
-            ar_list = [2 * x - 1 for x in ar_list]
-        # interpret values as signed values
-        elif dtype.name.startswith("INT"):
-            mask = 2 ** (dtype.bitwidth() - 1)
-            ar_list = [-(x & mask) + (x & ~mask) for x in ar_list]
-
-        array.append(ar_list)
-    array = np.asarray(array, dtype=np.float32).reshape(out_shape)
-    return array
-
-
-def numpy_to_hls_code(
-    ndarray, dtype, hls_var_name, pack_innermost_dim=True, no_decl=False
-):
-    """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. If no_decl is
-    set to True, no variable name and type will be generated as part of the
-    emitted string.
-    """
-    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()
-        idimbits = roundup_to_integer_multiple(idimbits, 4)
-        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("]", "}")
-    if no_decl:
-        ret = strarr + ";"
-    else:
-        ret = ret + " = \n" + strarr + ";"
-    return ret
-
-
-def npy_to_rtlsim_input(input_file, input_dtype, pad_to_nbits, reverse_inner=True):
-    """Convert the multidimensional NumPy array of integers (stored as floats)
-    from input_file into a flattened sequence of Python arbitrary-precision
-    integers, packing the innermost dimension. See
-    finn.util.basic.pack_innermost_dim_as_hex_string() for more info on how the
-    packing works. If reverse_inner is set, the innermost dimension will be
-    reversed prior to packing."""
-    pad_to_nbits = roundup_to_integer_multiple(pad_to_nbits, 4)
-    if issubclass(type(input_file), np.ndarray):
-        inp = input_file
-    elif os.path.isfile(input_file):
-        inp = np.load(input_file)
-    else:
-        raise Exception("input_file must be ndarray or filename for .npy")
-    packed_data = pack_innermost_dim_as_hex_string(
-        inp, input_dtype, pad_to_nbits, reverse_inner=reverse_inner
-    )
-    packed_data = packed_data.flatten()
-    packed_data = [int(x[2:], 16) for x in packed_data]
-    return packed_data
-
-
-def rtlsim_output_to_npy(
-    output, path, dtype, shape, packedBits, targetBits, reverse_inner=True
-):
-    """Convert a flattened sequence of Python arbitrary-precision integers
-    output into a NumPy array, saved as npy file at path. Each arbitrary-precision
-    integer is assumed to be a packed array of targetBits-bit elements, which
-    will be unpacked as the innermost dimension of the NumPy array. If path is
-    not None it will also be saved as a npy file."""
-
-    # TODO should have its own testbench?
-    output = np.asarray([hex(int(x)) for x in output])
-    out_array = unpack_innermost_dim_from_hex_string(
-        output, dtype, shape, packedBits=packedBits, reverse_inner=reverse_inner
-    )
-    # make copy before saving the array
-    out_array = out_array.copy()
-    if path is not None:
-        np.save(path, out_array)
-    return out_array
-
-
-def finnpy_to_packed_bytearray(
-    ndarray, dtype, reverse_inner=False, reverse_endian=False
-):
-    """Given a numpy ndarray with FINN DataType dtype, pack the innermost
-    dimension and return the packed representation as an ndarray of uint8.
-    The packed innermost dimension will be padded to the nearest multiple
-    of 8 bits. The returned ndarray has the same number of dimensions as the
-    input.
-    """
-
-    if (not issubclass(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)
-    # pack innermost dim to hex strings padded to 8 bits
-    bits = dtype.bitwidth() * ndarray.shape[-1]
-    bits_padded = roundup_to_integer_multiple(bits, 8)
-    packed_hexstring = pack_innermost_dim_as_hex_string(
-        ndarray, dtype, bits_padded, reverse_inner=reverse_inner
-    )
-
-    def fn(x):
-        return np.asarray(list(map(hexstring2npbytearray, x)))
-
-    if packed_hexstring.ndim == 0:
-        # scalar, call hexstring2npbytearray directly
-        ret = hexstring2npbytearray(np.asscalar(packed_hexstring))
-    else:
-        # convert ndarray of hex strings to byte array
-        ret = np.apply_along_axis(fn, packed_hexstring.ndim - 1, packed_hexstring)
-    if reverse_endian:
-        # reverse the endianness of packing dimension
-        ret = np.flip(ret, axis=-1)
-    return ret
-
-
-def packed_bytearray_to_finnpy(
-    packed_bytearray,
-    dtype,
-    output_shape=None,
-    reverse_inner=False,
-    reverse_endian=False,
-):
-    """Given a packed numpy uint8 ndarray, unpack it into a FINN array of
-    given DataType.
-
-    output_shape can be specified to remove padding from the
-    packed dimension, or set to None to be inferred from the input."""
-
-    if (
-        not issubclass(type(packed_bytearray), np.ndarray)
-    ) or packed_bytearray.dtype != np.uint8:
-        raise Exception("packed_bytearray_to_finnpy needs NumPy uint8 arrays")
-    if packed_bytearray.ndim == 0:
-        raise Exception("packed_bytearray_to_finnpy expects at least 1D ndarray")
-    packed_dim = packed_bytearray.ndim - 1
-    packed_bits = packed_bytearray.shape[packed_dim] * 8
-    target_bits = dtype.bitwidth()
-    if output_shape is None:
-        # determine output shape from input shape
-        assert (
-            packed_bits % target_bits == 0
-        ), """packed_bits are not divisable by
-        target_bits."""
-        n_target_elems = packed_bits // target_bits
-        output_shape = packed_bytearray.shape[:-1] + (n_target_elems,)
-    # if reverse_endian and target_bits > 8:
-    #     # revse the endianness of each element
-    #     orig_shape = packed_bytearray.shape
-    #     assert target_bits % 8 == 0, "target_bits are not a multiple of 8."
-    #     target_bytes = target_bits // 8
-    #     new_shape = orig_shape[:-1] + (-1, target_bytes)
-    #     packed_bytearray = np.flip(packed_bytearray.reshape(new_shape), axis=-1)
-    #     packed_bytearray = packed_bytearray.reshape(orig_shape)
-    if reverse_endian:
-        packed_bytearray = np.flip(packed_bytearray, axis=-1)
-    # convert innermost dim of byte array to hex strings
-    packed_hexstring = np.apply_along_axis(
-        npbytearray2hexstring, packed_dim, packed_bytearray
-    )
-    ret = unpack_innermost_dim_from_hex_string(
-        packed_hexstring, dtype, output_shape, packed_bits, reverse_inner
-    )
-
-    return ret
diff --git a/src/finn/util/fpgadataflow.py b/src/finn/util/fpgadataflow.py
deleted file mode 100644
index 3fe747a84985b2702ffb1e5855d9071362efebda..0000000000000000000000000000000000000000
--- a/src/finn/util/fpgadataflow.py
+++ /dev/null
@@ -1,218 +0,0 @@
-# Copyright (c) 2020, Xilinx
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-#
-# * Redistributions of source code must retain the above copyright notice, this
-#   list of conditions and the following disclaimer.
-#
-# * Redistributions in binary form must reproduce the above copyright notice,
-#   this list of conditions and the following disclaimer in the documentation
-#   and/or other materials provided with the distribution.
-#
-# * Neither the name of FINN nor the names of its
-#   contributors may be used to endorse or promote products derived from
-#   this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-import os
-import subprocess
-
-try:
-    from pyverilator import PyVerilator
-except ModuleNotFoundError:
-    PyVerilator = None
-from finn.util.basic import get_by_name, make_build_dir, get_rtlsim_trace_depth
-
-
-class IPGenBuilder:
-    """Builds the bash script to generate IP blocks using Vivado HLS."""
-
-    def __init__(self):
-        self.tcl_script = ""
-        self.ipgen_path = ""
-        self.code_gen_dir = ""
-        self.ipgen_script = ""
-
-    def append_tcl(self, tcl_script):
-        """Sets member variable "tcl_script" to given tcl script."""
-        self.tcl_script = tcl_script
-
-    def set_ipgen_path(self, path):
-        """Sets member variable ipgen_path to given path."""
-        self.ipgen_path = path
-
-    def build(self, code_gen_dir):
-        """Builds the bash script with given parameters and saves it in given folder.
-        To guarantee the generation in the correct folder the bash script contains a
-        cd command."""
-        self.code_gen_dir = code_gen_dir
-        self.ipgen_script = str(self.code_gen_dir) + "/ipgen.sh"
-        working_dir = os.environ["PWD"]
-        f = open(self.ipgen_script, "w")
-        f.write("#!/bin/bash \n")
-        f.write("cd {}\n".format(code_gen_dir))
-        f.write("vivado_hls {}\n".format(self.tcl_script))
-        f.write("cd {}\n".format(working_dir))
-        f.close()
-        bash_command = ["bash", self.ipgen_script]
-        process_compile = subprocess.Popen(bash_command, stdout=subprocess.PIPE)
-        process_compile.communicate()
-
-
-def pyverilate_stitched_ip(model):
-    "Given a model with stitched IP, return a PyVerilator sim object."
-    if PyVerilator is None:
-        raise ImportError("Installation of PyVerilator is required.")
-
-    vivado_stitch_proj_dir = model.get_metadata_prop("vivado_stitch_proj")
-    with open(vivado_stitch_proj_dir + "/all_verilog_srcs.txt", "r") as f:
-        all_verilog_srcs = f.read().split()
-
-    def file_to_dir(x):
-        return os.path.dirname(os.path.realpath(x))
-
-    def file_to_basename(x):
-        return os.path.basename(os.path.realpath(x))
-
-    all_verilog_dirs = list(map(file_to_dir, all_verilog_srcs))
-    all_verilog_files = list(
-        set(
-            filter(
-                lambda x: x.endswith(".v"),
-                list(map(file_to_basename, all_verilog_srcs)),
-            )
-        )
-    )
-    top_module_name = model.get_metadata_prop("wrapper_filename")
-    top_module_name = file_to_basename(top_module_name).strip(".v")
-    build_dir = make_build_dir("pyverilator_ipstitched_")
-    sim = PyVerilator.build(
-        all_verilog_files,
-        verilog_path=all_verilog_dirs,
-        build_dir=build_dir,
-        trace_depth=get_rtlsim_trace_depth(),
-        top_module_name=top_module_name,
-        auto_eval=False,
-    )
-    return sim
-
-
-def pyverilate_get_liveness_threshold_cycles():
-    """Return the number of no-output cycles rtlsim will wait before assuming
-    the simulation is not finishing and throwing an exception."""
-
-    return int(os.getenv("LIVENESS_THRESHOLD", 10000))
-
-
-def is_fpgadataflow_node(node):
-    """Returns True if given node is fpgadataflow node. Otherwise False."""
-    is_node = False
-    if node is not None:
-        if node.domain == "finn":
-            n_backend = get_by_name(node.attribute, "backend")
-            if n_backend is not None:
-                backend_value = n_backend.s.decode("UTF-8")
-                if backend_value == "fpgadataflow":
-                    is_node = True
-
-    return is_node
-
-
-def rtlsim_multi_io(sim, io_dict, num_out_values, trace_file=""):
-    """Runs the pyverilator simulation by passing the input values to the simulation,
-    toggle the clock and observing the execution time. Function contains also an
-    observation loop that can abort the simulation if no output value is produced
-    after a set number of cycles. Can handle multiple i/o streams. See function
-    implementation for details on how the top-level signals should be named.
-
-    sim: the PyVerilator object for simulation
-    io_dict: a dict of dicts in the following format:
-            {"inputs" : {"in0" : <input_data>, "in1" : <input_data>},
-             "outputs" : {"out0" : [], "out1" : []} }
-            <input_data> is a list of Python arbitrary-precision ints indicating
-            what data to push into the simulation, and the output lists are
-            similarly filled when the simulation is complete
-    num_out_values: number of total values to be read from the simulation to
-                    finish the simulation and return.
-
-    returns: number of clock cycles elapsed for completion
-
-    """
-
-    if trace_file != "":
-        sim.start_vcd_trace(trace_file)
-
-    for outp in io_dict["outputs"]:
-        sim.io[outp + "_V_V_TREADY"] = 1
-
-    # observe if output is completely calculated
-    # total_cycle_count will contain the number of cycles the calculation ran
-    output_done = False
-    total_cycle_count = 0
-    output_count = 0
-    old_output_count = 0
-
-    # avoid infinite looping of simulation by aborting when there is no change in
-    # output values after 100 cycles
-    no_change_count = 0
-    liveness_threshold = pyverilate_get_liveness_threshold_cycles()
-
-    while not (output_done):
-        for inp in io_dict["inputs"]:
-            inputs = io_dict["inputs"][inp]
-            sim.io[inp + "_V_V_TVALID"] = 1 if len(inputs) > 0 else 0
-            sim.io[inp + "_V_V_TDATA"] = inputs[0] if len(inputs) > 0 else 0
-            if sim.io[inp + "_V_V_TREADY"] == 1 and sim.io[inp + "_V_V_TVALID"] == 1:
-                inputs = inputs[1:]
-            io_dict["inputs"][inp] = inputs
-
-        for outp in io_dict["outputs"]:
-            outputs = io_dict["outputs"][outp]
-            if sim.io[outp + "_V_V_TVALID"] == 1 and sim.io[outp + "_V_V_TREADY"] == 1:
-                outputs = outputs + [sim.io[outp + "_V_V_TDATA"]]
-                output_count += 1
-            io_dict["outputs"][outp] = outputs
-
-        sim.io.ap_clk = 1
-        sim.io.ap_clk = 0
-
-        total_cycle_count = total_cycle_count + 1
-
-        if output_count == old_output_count:
-            no_change_count = no_change_count + 1
-        else:
-            no_change_count = 0
-            old_output_count = output_count
-
-        # check if all expected output words received
-        if output_count == num_out_values:
-            output_done = True
-
-        # end sim on timeout
-        if no_change_count == liveness_threshold:
-            if trace_file != "":
-                sim.flush_vcd_trace()
-                sim.stop_vcd_trace()
-            raise Exception(
-                "Error in simulation! Takes too long to produce output. "
-                "Consider setting the LIVENESS_THRESHOLD env.var. to a "
-                "larger value."
-            )
-
-    if trace_file != "":
-        sim.flush_vcd_trace()
-        sim.stop_vcd_trace()
-
-    return total_cycle_count
diff --git a/src/finn/util/onnx.py b/src/finn/util/onnx.py
deleted file mode 100644
index 4d7cdd126ededac887639a932c2021ef5f081c02..0000000000000000000000000000000000000000
--- a/src/finn/util/onnx.py
+++ /dev/null
@@ -1,75 +0,0 @@
-# Copyright (c) 2020, Xilinx
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-#
-# * Redistributions of source code must retain the above copyright notice, this
-#   list of conditions and the following disclaimer.
-#
-# * Redistributions in binary form must reproduce the above copyright notice,
-#   this list of conditions and the following disclaimer in the documentation
-#   and/or other materials provided with the distribution.
-#
-# * Neither the name of FINN nor the names of its
-#   contributors may be used to endorse or promote products derived from
-#   this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-import numpy as np
-import onnx
-import finn.core.data_layout as DataLayout
-
-
-def valueinfo_to_tensor(vi):
-    """Creates an all-zeroes numpy tensor from a ValueInfoProto."""
-
-    dims = [x.dim_value for x in vi.type.tensor_type.shape.dim]
-    return np.zeros(
-        dims, dtype=onnx.mapping.TENSOR_TYPE_TO_NP_TYPE[vi.type.tensor_type.elem_type]
-    )
-
-
-def nchw_to_nhwc(t, model, idx, reverse=False):
-    """Converts between NCHW <-> NHWC layouts for tensor t by inserting a transpose. 
-    If reverse=False, t is assumed NCHW and we insert transpose to convert NCHW -> NHWC
-    If reverse=True, t is assumed NHWC and we insert transpose to convert NHWC -> NCHW.
-    """
-    graph = model.graph
-    # create new NHWC tensor
-    t_shape = model.get_tensor_shape(t)
-    bs = t_shape[0]
-    ch = t_shape[1]
-    height = t_shape[2]
-    width = t_shape[3]
-    t_trans = onnx.helper.make_tensor_value_info(
-        model.make_new_valueinfo_name(),
-        onnx.TensorProto.FLOAT,
-        (bs, height, width, ch),  # NHWC
-    )
-    graph.value_info.append(t_trans)
-    dt = model.get_tensor_datatype(t)
-    t_trans = t_trans.name
-    model.set_tensor_datatype(t_trans, dt)
-    model.set_tensor_layout(t_trans, DataLayout.NHWC)
-    # NCHW <-> NHWC transpose
-    if reverse:
-        t_trans_node = onnx.helper.make_node(
-            "Transpose", [t_trans], [t], perm=[0, 3, 1, 2]
-        )
-    else:
-        t_trans_node = onnx.helper.make_node(
-            "Transpose", [t], [t_trans], perm=[0, 2, 3, 1]
-        )
-    graph.node.insert(idx, t_trans_node)
-    return t_trans
diff --git a/src/finn/util/test.py b/src/finn/util/test.py
index 32c6a0a3a3bb19b95590181dbe447e82cf9966a2..03a9d435a6d04659b2891815b33172586a7f0a96 100644
--- a/src/finn/util/test.py
+++ b/src/finn/util/test.py
@@ -139,11 +139,13 @@ def get_example_input(topology):
     "Get example numpy input tensor for given topology."
 
     if "fc" in topology:
-        raw_i = get_data("finn", "data/onnx/mnist-conv/test_data_set_0/input_0.pb")
+        raw_i = get_data("finn.data", "onnx/mnist-conv/test_data_set_0/input_0.pb")
         onnx_tensor = onnx.load_tensor_from_string(raw_i)
         return nph.to_array(onnx_tensor)
     elif topology == "cnv":
-        fn = pk.resource_filename("finn", "data/cifar10/cifar10-test-data-class3.npz")
+        fn = pk.resource_filename(
+            "finn.qnn-data", "cifar10/cifar10-test-data-class3.npz"
+        )
         input_tensor = np.load(fn)["arr_0"].astype(np.float32)
         return input_tensor
     else:
diff --git a/tests/analysis/test_is_linear.py b/tests/analysis/test_is_linear.py
deleted file mode 100644
index 6afe9bb9c57f5f20486a9a35bab9902e2d952b02..0000000000000000000000000000000000000000
--- a/tests/analysis/test_is_linear.py
+++ /dev/null
@@ -1,85 +0,0 @@
-# Copyright (c) 2020, Xilinx
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-#
-# * Redistributions of source code must retain the above copyright notice, this
-#   list of conditions and the following disclaimer.
-#
-# * Redistributions in binary form must reproduce the above copyright notice,
-#   this list of conditions and the following disclaimer in the documentation
-#   and/or other materials provided with the distribution.
-#
-# * Neither the name of FINN nor the names of its
-#   contributors may be used to endorse or promote products derived from
-#   this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-import onnx.helper as oh
-from onnx import TensorProto
-
-import finn.analysis.topology as ta
-from finn.core.modelwrapper import ModelWrapper
-from finn.transformation.infer_shapes import InferShapes
-
-
-def test_is_linear_linear():
-    top_in = oh.make_tensor_value_info("top_in", TensorProto.FLOAT, [2])
-    add_param = oh.make_tensor_value_info("add_param", TensorProto.FLOAT, [2])
-    mul_param = oh.make_tensor_value_info("mul_param", TensorProto.FLOAT, [2])
-    top_out = oh.make_tensor_value_info("top_out", TensorProto.FLOAT, [2])
-    modelproto = oh.make_model(
-        oh.make_graph(
-            name="test",
-            inputs=[top_in],
-            outputs=[top_out],
-            value_info=[add_param, mul_param],
-            nodes=[
-                oh.make_node("Add", ["top_in", "add_param"], ["middle"]),
-                oh.make_node("Mul", ["middle", "mul_param"], ["top_out"]),
-            ],
-        )
-    )
-    model = ModelWrapper(modelproto)
-    model = model.transform(InferShapes())
-    ret = model.analysis(ta.is_linear)
-    assert ret["is_linear"] is True
-
-
-def test_is_linear_forked_node_output():
-    top_in = oh.make_tensor_value_info("top_in", TensorProto.FLOAT, [2])
-    add_param = oh.make_tensor_value_info("add_param", TensorProto.FLOAT, [2])
-    mul0_param = oh.make_tensor_value_info("mul0_param", TensorProto.FLOAT, [2])
-    mul1_param = oh.make_tensor_value_info("mul1_param", TensorProto.FLOAT, [2])
-    mul0_res = oh.make_tensor_value_info("mul0_res", TensorProto.FLOAT, [2])
-    mul1_res = oh.make_tensor_value_info("mul1_res", TensorProto.FLOAT, [2])
-    top_out = oh.make_tensor_value_info("top_out", TensorProto.FLOAT, [2])
-    modelproto = oh.make_model(
-        oh.make_graph(
-            name="test",
-            inputs=[top_in],
-            outputs=[top_out],
-            value_info=[add_param, mul0_param, mul1_param, mul0_res, mul1_res],
-            nodes=[
-                oh.make_node("Add", ["top_in", "add_param"], ["middle"]),
-                oh.make_node("Mul", ["middle", "mul0_param"], ["mul0_res"]),
-                oh.make_node("Mul", ["middle", "mul1_param"], ["mul1_res"]),
-                oh.make_node("Add", ["mul0_res", "mul1_res"], ["top_out"]),
-            ],
-        )
-    )
-    model = ModelWrapper(modelproto)
-    model = model.transform(InferShapes())
-    ret = model.analysis(ta.is_linear)
-    assert ret["is_linear"] is False
diff --git a/tests/analysis/test_topology_checks.py b/tests/analysis/test_topology_checks.py
deleted file mode 100644
index 7f7f800da05e38fefa9350928ab6ddc94acbe2b6..0000000000000000000000000000000000000000
--- a/tests/analysis/test_topology_checks.py
+++ /dev/null
@@ -1,205 +0,0 @@
-# Copyright (c) 2020, Xilinx
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-#
-# * Redistributions of source code must retain the above copyright notice, this
-#   list of conditions and the following disclaimer.
-#
-# * Redistributions in binary form must reproduce the above copyright notice,
-#   this list of conditions and the following disclaimer in the documentation
-#   and/or other materials provided with the distribution.
-#
-# * Neither the name of FINN nor the names of its
-#   contributors may be used to endorse or promote products derived from
-#   this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-import os
-from pkgutil import get_data
-
-import onnx.helper as oh
-from onnx import TensorProto
-import brevitas.onnx as bo
-from finn.util.test import get_test_model_trained
-import finn.analysis.topology as ta
-from finn.core.modelwrapper import ModelWrapper
-from finn.transformation.infer_shapes import InferShapes
-
-
-def test_all_tensors_f32():
-    top_in = oh.make_tensor_value_info("top_in", TensorProto.FLOAT, [2])
-    add_param = oh.make_tensor_value_info("add_param", TensorProto.FLOAT, [2])
-    mul_param = oh.make_tensor_value_info("mul_param", TensorProto.FLOAT, [2])
-    top_out = oh.make_tensor_value_info("top_out", TensorProto.FLOAT, [2])
-    modelproto = oh.make_model(
-        oh.make_graph(
-            name="test",
-            inputs=[top_in],
-            outputs=[top_out],
-            value_info=[add_param, mul_param],
-            nodes=[
-                oh.make_node("Add", ["top_in", "add_param"], ["middle"]),
-                oh.make_node("Mul", ["middle", "mul_param"], ["top_out"]),
-            ],
-        )
-    )
-    model = ModelWrapper(modelproto)
-    model = model.transform(InferShapes())
-    ret = model.analysis(ta.all_tensors_f32)
-    assert ret["all_tensors_f32"] is True
-
-    top_in = oh.make_tensor_value_info("top_in", TensorProto.FLOAT, [2])
-    add_param = oh.make_tensor_value_info("add_param", TensorProto.INT8, [2])
-    mul_param = oh.make_tensor_value_info("mul_param", TensorProto.FLOAT, [2])
-    top_out = oh.make_tensor_value_info("top_out", TensorProto.FLOAT, [2])
-    modelproto = oh.make_model(
-        oh.make_graph(
-            name="test",
-            inputs=[top_in],
-            outputs=[top_out],
-            value_info=[add_param, mul_param],
-            nodes=[
-                oh.make_node("Add", ["top_in", "add_param"], ["middle"]),
-                oh.make_node("Mul", ["middle", "mul_param"], ["top_out"]),
-            ],
-        )
-    )
-    model = ModelWrapper(modelproto)
-    model = model.transform(InferShapes())
-    ret = model.analysis(ta.all_tensors_f32)
-    assert ret["all_tensors_f32"] is False
-
-
-def test_node_inputs_in_expected_order():
-    raw_m = get_data("finn", "data/onnx/mnist-conv/model.onnx")
-    model = ModelWrapper(raw_m)
-    model = model.transform(InferShapes())
-    ret = model.analysis(ta.node_inputs_in_expected_order)
-    # this model has an (unnecessary) dynamic reshape for its weight tensor
-    # and so it fails the check
-    assert ret["node_inputs_in_expected_order"] is False
-
-
-def test_nodes_topologically_sorted():
-    # test analysis pass (nodes_topologically_sorted) with different models
-
-    # test with data/onnx/finn-hls-model/tfc_w1_a1_after_conv_to_hls.onnx
-    raw_m = get_data(
-        "finn", "data/onnx/finn-hls-model/tfc_w1_a1_after_conv_to_hls.onnx"
-    )
-    model = ModelWrapper(raw_m)
-    ret = model.analysis(ta.nodes_topologically_sorted)
-    assert ret["nodes_topologically_sorted"] is True
-
-    # remove first node and add it at the end
-    graph = model.graph
-    first_node = graph.node[0]
-    graph.node.remove(first_node)
-    graph.node.append(first_node)
-    ret = model.analysis(ta.nodes_topologically_sorted)
-    assert ret["nodes_topologically_sorted"] is False
-
-    # test with data/onnx/mnist-conv/model.onnx
-    raw_m = get_data("finn", "data/onnx/mnist-conv/model.onnx")
-    model = ModelWrapper(raw_m)
-    ret = model.analysis(ta.nodes_topologically_sorted)
-    assert ret["nodes_topologically_sorted"] is True
-
-    # remove first node and add it at the end
-    graph = model.graph
-    first_node = graph.node[0]
-    graph.node.remove(first_node)
-    graph.node.append(first_node)
-    ret = model.analysis(ta.nodes_topologically_sorted)
-    assert ret["nodes_topologically_sorted"] is False
-
-    # test with manually created small network
-    Neg_node = oh.make_node("Neg", inputs=["in1"], outputs=["neg1"])
-    Round_node = oh.make_node("Round", inputs=["neg1"], outputs=["round1"])
-
-    Ceil_node = oh.make_node("Ceil", inputs=["neg1"], outputs=["ceil1"])
-    Add_node = oh.make_node("Add", inputs=["round1", "ceil1"], outputs=["out1"])
-
-    in1 = oh.make_tensor_value_info("in1", TensorProto.FLOAT, [4, 4])
-    out1 = oh.make_tensor_value_info("out1", TensorProto.FLOAT, [4, 4])
-
-    graph = oh.make_graph(
-        nodes=[Neg_node, Round_node, Ceil_node, Add_node],
-        name="simple_graph",
-        inputs=[in1],
-        outputs=[out1],
-        value_info=[
-            oh.make_tensor_value_info("neg1", TensorProto.FLOAT, [4, 4]),
-            oh.make_tensor_value_info("round1", TensorProto.FLOAT, [4, 4]),
-            oh.make_tensor_value_info("ceil1", TensorProto.FLOAT, [4, 4]),
-        ],
-    )
-
-    onnx_model = oh.make_model(graph, producer_name="simple-model")
-    model = ModelWrapper(onnx_model)
-
-    ret = model.analysis(ta.nodes_topologically_sorted)
-    assert ret["nodes_topologically_sorted"] is True
-
-    # create same graph but with "wrong" node order
-    graph = oh.make_graph(
-        nodes=[Round_node, Ceil_node, Neg_node, Add_node],
-        name="simple_graph",
-        inputs=[in1],
-        outputs=[out1],
-        value_info=[
-            oh.make_tensor_value_info("neg1", TensorProto.FLOAT, [4, 4]),
-            oh.make_tensor_value_info("round1", TensorProto.FLOAT, [4, 4]),
-            oh.make_tensor_value_info("ceil1", TensorProto.FLOAT, [4, 4]),
-        ],
-    )
-
-    onnx_model = oh.make_model(graph, producer_name="simple-model")
-    model = ModelWrapper(onnx_model)
-
-    ret = model.analysis(ta.nodes_topologically_sorted)
-    assert ret["nodes_topologically_sorted"] is False
-
-    # test with data/onnx/finn-hls-model/finn-hls-onnx-model.onnx
-    raw_m = get_data("finn", "data/onnx/finn-hls-model/finn-hls-onnx-model.onnx")
-    model = ModelWrapper(raw_m)
-    ret = model.analysis(ta.nodes_topologically_sorted)
-    assert ret["nodes_topologically_sorted"] is True
-
-    # remove first node and add it at the end
-    graph = model.graph
-    first_node = graph.node[0]
-    graph.node.remove(first_node)
-    graph.node.append(first_node)
-    ret = model.analysis(ta.nodes_topologically_sorted)
-    assert ret["nodes_topologically_sorted"] is False
-
-    # test with cnv_w1a1
-    build_dir = "/tmp/" + os.environ["FINN_INST_NAME"]
-    cnv = get_test_model_trained("CNV", 1, 1)
-    bo.export_finn_onnx(
-        cnv, (1, 3, 32, 32), build_dir + "/end2end_cnv_w1a1_export.onnx"
-    )
-    model = ModelWrapper(build_dir + "/end2end_cnv_w1a1_export.onnx")
-    ret = model.analysis(ta.nodes_topologically_sorted)
-    assert ret["nodes_topologically_sorted"] is True
-
-    # remove first node and add it at the end
-    graph = model.graph
-    first_node = graph.node[0]
-    graph.node.remove(first_node)
-    graph.node.append(first_node)
-    ret = model.analysis(ta.nodes_topologically_sorted)
-    assert ret["nodes_topologically_sorted"] is False
diff --git a/tests/brevitas/test_brevitas_cnv.py b/tests/brevitas/test_brevitas_cnv.py
index 120c67646de08a1a9875b76bedd3a0130792b487..4b072535bdfe102a6c59ebd4c730de9ae827c00e 100644
--- a/tests/brevitas/test_brevitas_cnv.py
+++ b/tests/brevitas/test_brevitas_cnv.py
@@ -58,7 +58,7 @@ def test_brevitas_cnv_export_exec(wbits, abits):
     model = model.transform(RemoveStaticGraphInputs())
     assert len(model.graph.input) == 1
     assert len(model.graph.output) == 1
-    fn = pk.resource_filename("finn", "data/cifar10/cifar10-test-data-class3.npz")
+    fn = pk.resource_filename("finn.qnn-data", "cifar10/cifar10-test-data-class3.npz")
     input_tensor = np.load(fn)["arr_0"].astype(np.float32)
     input_tensor = input_tensor / 255
     assert input_tensor.shape == (1, 3, 32, 32)
diff --git a/tests/brevitas/test_brevitas_debug.py b/tests/brevitas/test_brevitas_debug.py
index 50d0ca44cd0befe5d08b5c1b45edf602457bda19..9115352796b0b90257d64ce9b14163ad372c9c98 100644
--- a/tests/brevitas/test_brevitas_debug.py
+++ b/tests/brevitas/test_brevitas_debug.py
@@ -55,7 +55,7 @@ def test_brevitas_debug():
     assert len(model.graph.input) == 1
     assert len(model.graph.output) == 1
     # load one of the test vectors
-    raw_i = get_data("finn", "data/onnx/mnist-conv/test_data_set_0/input_0.pb")
+    raw_i = get_data("finn.data", "onnx/mnist-conv/test_data_set_0/input_0.pb")
     input_tensor = onnx.load_tensor_from_string(raw_i)
     # run using FINN-based execution
     input_dict = {"0": nph.to_array(input_tensor)}
diff --git a/tests/brevitas/test_brevitas_fc.py b/tests/brevitas/test_brevitas_fc.py
index 9369b25385080875efcb286c02291fc579a15a34..24a453007515ba2eba4369a6b76829099f722168 100644
--- a/tests/brevitas/test_brevitas_fc.py
+++ b/tests/brevitas/test_brevitas_fc.py
@@ -68,7 +68,7 @@ def test_brevitas_fc_onnx_export_and_exec(size, wbits, abits):
     assert len(model.graph.input) == 1
     assert len(model.graph.output) == 1
     # load one of the test vectors
-    raw_i = get_data("finn", "data/onnx/mnist-conv/test_data_set_0/input_0.pb")
+    raw_i = get_data("finn.data", "onnx/mnist-conv/test_data_set_0/input_0.pb")
     input_tensor = onnx.load_tensor_from_string(raw_i)
     # run using FINN-based execution
     input_dict = {"0": nph.to_array(input_tensor)}
diff --git a/tests/core/test_basic_onnx_exec.py b/tests/core/test_basic_onnx_exec.py
deleted file mode 100644
index ddb2cbfc40c7647970f0c51ecb95340e7d1dddae..0000000000000000000000000000000000000000
--- a/tests/core/test_basic_onnx_exec.py
+++ /dev/null
@@ -1,98 +0,0 @@
-# Copyright (c) 2020, Xilinx
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-#
-# * Redistributions of source code must retain the above copyright notice, this
-#   list of conditions and the following disclaimer.
-#
-# * Redistributions in binary form must reproduce the above copyright notice,
-#   this list of conditions and the following disclaimer in the documentation
-#   and/or other materials provided with the distribution.
-#
-# * Neither the name of FINN nor the names of its
-#   contributors may be used to endorse or promote products derived from
-#   this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-from pkgutil import get_data
-
-import numpy as np
-import onnx
-import onnx.numpy_helper as np_helper
-
-import finn.core.onnx_exec as oxe
-from finn.core.modelwrapper import ModelWrapper
-from finn.transformation.infer_shapes import InferShapes
-from finn.core.datatype import DataType
-from finn.util.basic import gen_finn_dt_tensor
-
-
-def test_mnist_onnx_download_extract_run():
-    # load the onnx model
-    raw_m = get_data("finn", "data/onnx/mnist-conv/model.onnx")
-    model = ModelWrapper(raw_m)
-    model = model.transform(InferShapes())
-    # load one of the test vectors
-    raw_i = get_data("finn", "data/onnx/mnist-conv/test_data_set_0/input_0.pb")
-    raw_o = get_data("finn", "data/onnx/mnist-conv/test_data_set_0/output_0.pb")
-    input_tensor = onnx.load_tensor_from_string(raw_i)
-    output_tensor = onnx.load_tensor_from_string(raw_o)
-    # run using FINN-based execution (full graph)
-    input_dict = {"Input3": np_helper.to_array(input_tensor)}
-    output_dict = oxe.execute_onnx(model, input_dict, return_full_exec_context=True)
-    assert np.isclose(
-        np_helper.to_array(output_tensor), output_dict["Plus214_Output_0"], atol=1e-3
-    ).all()
-    # test subgraph execution
-    start_node = model.graph.node[1]
-    end_node = model.graph.node[3]
-    subgraph_i_dict = {start_node.input[0]: output_dict[start_node.input[0]]}
-    subgraph_o_dict = oxe.execute_onnx(
-        model,
-        subgraph_i_dict,
-        return_full_exec_context=True,
-        start_node=start_node,
-        end_node=end_node,
-    )
-    assert np.isclose(
-        subgraph_o_dict[end_node.output[0]], output_dict[end_node.output[0]], atol=1e-3
-    ).all()
-
-
-def test_onnx_exec_internal_rounding():
-    inp0 = onnx.helper.make_tensor_value_info("inp0", onnx.TensorProto.FLOAT, [2, 2])
-    inp1 = onnx.helper.make_tensor_value_info("inp1", onnx.TensorProto.FLOAT, [1])
-    outp = onnx.helper.make_tensor_value_info("outp", onnx.TensorProto.FLOAT, [2, 2])
-    mul_node = onnx.helper.make_node("Mul", inputs=["inp0", "inp1"], outputs=["outp"])
-    graph = onnx.helper.make_graph(
-        nodes=[mul_node], name="mul_graph", inputs=[inp0, inp1], outputs=[outp]
-    )
-
-    model = onnx.helper.make_model(graph, producer_name="mul-model")
-    model = ModelWrapper(model)
-    idt = DataType.INT2
-    model.set_tensor_datatype("inp0", idt)
-    model.set_tensor_datatype("inp1", idt)
-    model.transform(InferShapes())
-
-    mul_value = np.asarray([-1], dtype=np.float32)
-    inp_int = gen_finn_dt_tensor(idt, [2, 2])
-    scale = np.random.uniform(low=0, high=1, size=(2, 2)).astype(np.float32)
-    inp_rounded = (inp_int * scale) / (scale + 1e-7)
-    input_dict = {"inp0": inp_rounded, "inp1": mul_value}
-    output_dict = oxe.execute_onnx(model, input_dict)
-    produced = output_dict["outp"]
-    expected = np.multiply(inp_int, mul_value)
-    assert (produced == expected).all()
diff --git a/tests/core/test_custom_onnx_exec.py b/tests/core/test_custom_onnx_exec.py
deleted file mode 100644
index 086681dde0ff029ceaa7d3274bad4d3f15bd32fc..0000000000000000000000000000000000000000
--- a/tests/core/test_custom_onnx_exec.py
+++ /dev/null
@@ -1,277 +0,0 @@
-# Copyright (c) 2020, Xilinx
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-#
-# * Redistributions of source code must retain the above copyright notice, this
-#   list of conditions and the following disclaimer.
-#
-# * Redistributions in binary form must reproduce the above copyright notice,
-#   this list of conditions and the following disclaimer in the documentation
-#   and/or other materials provided with the distribution.
-#
-# * Neither the name of FINN nor the names of its
-#   contributors may be used to endorse or promote products derived from
-#   this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-import numpy as np
-from onnx import TensorProto, helper
-
-import finn.core.execute_custom_node as ex_cu_node
-
-
-def test_execute_custom_node_multithreshold():
-    inputs = np.ndarray(
-        shape=(6, 3, 2, 2),
-        buffer=np.array(
-            [
-                4.8,
-                3.2,
-                1.2,
-                4.9,
-                7.8,
-                2.4,
-                3.1,
-                4.7,
-                6.2,
-                5.1,
-                4.9,
-                2.2,
-                6.2,
-                0.0,
-                0.8,
-                4.7,
-                0.2,
-                5.6,
-                8.9,
-                9.2,
-                9.1,
-                4.0,
-                3.3,
-                4.9,
-                2.3,
-                1.7,
-                1.3,
-                2.2,
-                4.6,
-                3.4,
-                3.7,
-                9.8,
-                4.7,
-                4.9,
-                2.8,
-                2.7,
-                8.3,
-                6.7,
-                4.2,
-                7.1,
-                2.8,
-                3.1,
-                0.8,
-                0.6,
-                4.4,
-                2.7,
-                6.3,
-                6.1,
-                1.4,
-                5.3,
-                2.3,
-                1.9,
-                4.7,
-                8.1,
-                9.3,
-                3.7,
-                2.7,
-                5.1,
-                4.2,
-                1.8,
-                4.1,
-                7.3,
-                7.1,
-                0.4,
-                0.2,
-                1.3,
-                4.3,
-                8.9,
-                1.4,
-                1.6,
-                8.3,
-                9.4,
-            ]
-        ),
-    )
-
-    threshold_values = np.ndarray(
-        shape=(3, 7),
-        buffer=np.array(
-            [
-                0.8,
-                1.4,
-                1.7,
-                3.5,
-                5.2,
-                6.8,
-                8.2,
-                0.2,
-                2.2,
-                3.5,
-                4.5,
-                6.6,
-                8.6,
-                9.2,
-                1.3,
-                4.1,
-                4.5,
-                6.5,
-                7.8,
-                8.1,
-                8.9,
-            ]
-        ),
-    )
-
-    v = helper.make_tensor_value_info("v", TensorProto.FLOAT, [6, 3, 2, 2])
-    thresholds = helper.make_tensor_value_info("thresholds", TensorProto.FLOAT, [3, 7])
-    out = helper.make_tensor_value_info("out", TensorProto.FLOAT, [6, 3, 2, 2])
-
-    node_def = helper.make_node(
-        "MultiThreshold", ["v", "thresholds"], ["out"], domain="finn"
-    )
-
-    graph_def = helper.make_graph([node_def], "test_model", [v, thresholds], [out])
-
-    execution_context = {}
-    execution_context["v"] = inputs
-    execution_context["thresholds"] = threshold_values
-
-    ex_cu_node.execute_custom_node(node_def, execution_context, graph_def)
-
-    outputs = np.ndarray(
-        shape=(6, 3, 2, 2),
-        buffer=np.array(
-            [
-                4.0,
-                3.0,
-                1.0,
-                4.0,
-                5.0,
-                2.0,
-                2.0,
-                4.0,
-                3.0,
-                3.0,
-                3.0,
-                1.0,
-                5.0,
-                0.0,
-                1.0,
-                4.0,
-                1.0,
-                4.0,
-                6.0,
-                7.0,
-                7.0,
-                1.0,
-                1.0,
-                3.0,
-                3.0,
-                3.0,
-                1.0,
-                3.0,
-                4.0,
-                2.0,
-                3.0,
-                7.0,
-                3.0,
-                3.0,
-                1.0,
-                1.0,
-                7.0,
-                5.0,
-                4.0,
-                6.0,
-                2.0,
-                2.0,
-                1.0,
-                1.0,
-                2.0,
-                1.0,
-                3.0,
-                3.0,
-                2.0,
-                5.0,
-                3.0,
-                3.0,
-                4.0,
-                5.0,
-                7.0,
-                3.0,
-                1.0,
-                3.0,
-                2.0,
-                1.0,
-                4.0,
-                6.0,
-                6.0,
-                0.0,
-                1.0,
-                1.0,
-                3.0,
-                6.0,
-                1.0,
-                1.0,
-                6.0,
-                7.0,
-            ]
-        ),
-    )
-
-    assert (execution_context["out"] == outputs).all()
-
-    # test the optional output scaling features on MultiThreshold
-    node_def = helper.make_node(
-        "MultiThreshold",
-        ["v", "thresholds"],
-        ["out"],
-        domain="finn",
-        out_scale=2.0,
-        out_bias=-1.0,
-    )
-
-    graph_def = helper.make_graph([node_def], "test_model", [v, thresholds], [out])
-    ex_cu_node.execute_custom_node(node_def, execution_context, graph_def)
-    outputs_scaled = 2.0 * outputs - 1.0
-    assert (execution_context["out"] == outputs_scaled).all()
-
-    # test the optional data layout option for MultiThreshold
-    node_def = helper.make_node(
-        "MultiThreshold",
-        ["v", "thresholds"],
-        ["out"],
-        domain="finn",
-        data_layout="NHWC",
-    )
-
-    v_nhwc = helper.make_tensor_value_info("v", TensorProto.FLOAT, [6, 2, 2, 3])
-    out_nhwc = helper.make_tensor_value_info("out", TensorProto.FLOAT, [6, 2, 2, 3])
-    inputs_nhwc = np.transpose(inputs, (0, 2, 3, 1))  # NCHW -> NHWC
-    outputs_nhwc = np.transpose(outputs, (0, 2, 3, 1))  # NCHW -> NHWC
-    execution_context["v"] = inputs_nhwc
-
-    graph_def = helper.make_graph(
-        [node_def], "test_model", [v_nhwc, thresholds], [out_nhwc]
-    )
-    ex_cu_node.execute_custom_node(node_def, execution_context, graph_def)
-    assert (execution_context["out"] == outputs_nhwc).all()
diff --git a/tests/core/test_datatypes.py b/tests/core/test_datatypes.py
deleted file mode 100644
index f1d34923c5f8b05dc21a0d5b7781879deabae379..0000000000000000000000000000000000000000
--- a/tests/core/test_datatypes.py
+++ /dev/null
@@ -1,75 +0,0 @@
-# Copyright (c) 2020, Xilinx
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-#
-# * Redistributions of source code must retain the above copyright notice, this
-#   list of conditions and the following disclaimer.
-#
-# * Redistributions in binary form must reproduce the above copyright notice,
-#   this list of conditions and the following disclaimer in the documentation
-#   and/or other materials provided with the distribution.
-#
-# * Neither the name of FINN nor the names of its
-#   contributors may be used to endorse or promote products derived from
-#   this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-from finn.core.datatype import DataType
-
-
-def test_datatypes():
-    assert DataType.BIPOLAR.allowed(-1)
-    assert DataType.BIPOLAR.allowed(0) is False
-    assert DataType.BINARY.allowed(-1) is False
-    assert DataType.BINARY.allowed(1)
-    assert DataType.TERNARY.allowed(2) is False
-    assert DataType.TERNARY.allowed(-1)
-    assert DataType.UINT2.allowed(2)
-    assert DataType.UINT2.allowed(10) is False
-    assert DataType.UINT3.allowed(5)
-    assert DataType.UINT3.allowed(-7) is False
-    assert DataType.UINT4.allowed(15)
-    assert DataType.UINT4.allowed(150) is False
-    assert DataType.UINT8.allowed(150)
-    assert DataType.UINT8.allowed(777) is False
-    assert DataType.UINT16.allowed(14500)
-    assert DataType.UINT16.allowed(-1) is False
-    assert DataType.UINT32.allowed(2 ** 10)
-    assert DataType.UINT32.allowed(-1) is False
-    assert DataType.INT2.allowed(-1)
-    assert DataType.INT2.allowed(-10) is False
-    assert DataType.INT3.allowed(5) is False
-    assert DataType.INT3.allowed(-2)
-    assert DataType.INT4.allowed(15) is False
-    assert DataType.INT4.allowed(-5)
-    assert DataType.INT8.allowed(150) is False
-    assert DataType.INT8.allowed(-127)
-    assert DataType.INT16.allowed(-1.04) is False
-    assert DataType.INT16.allowed(-7777)
-    assert DataType.INT32.allowed(7.77) is False
-    assert DataType.INT32.allowed(-5)
-    assert DataType.INT32.allowed(5)
-    assert DataType.BINARY.signed() is False
-    assert DataType.FLOAT32.signed()
-    assert DataType.BIPOLAR.signed()
-    assert DataType.TERNARY.signed()
-
-
-def test_smallest_possible():
-    assert DataType.get_smallest_possible(1) == DataType.BINARY
-    assert DataType.get_smallest_possible(1.1) == DataType.FLOAT32
-    assert DataType.get_smallest_possible(-1) == DataType.BIPOLAR
-    assert DataType.get_smallest_possible(-3) == DataType.INT3
-    assert DataType.get_smallest_possible(-3.2) == DataType.FLOAT32
diff --git a/tests/core/test_mixed_onnx_exec.py b/tests/core/test_mixed_onnx_exec.py
deleted file mode 100644
index d8754105e3001f3ef40b1df23e4d5a22aa176ba4..0000000000000000000000000000000000000000
--- a/tests/core/test_mixed_onnx_exec.py
+++ /dev/null
@@ -1,252 +0,0 @@
-# Copyright (c) 2020, Xilinx
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-#
-# * Redistributions of source code must retain the above copyright notice, this
-#   list of conditions and the following disclaimer.
-#
-# * Redistributions in binary form must reproduce the above copyright notice,
-#   this list of conditions and the following disclaimer in the documentation
-#   and/or other materials provided with the distribution.
-#
-# * Neither the name of FINN nor the names of its
-#   contributors may be used to endorse or promote products derived from
-#   this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-import numpy as np
-from onnx import TensorProto, helper
-
-import finn.core.onnx_exec as oxe
-from finn.core.modelwrapper import ModelWrapper
-from finn.transformation.infer_shapes import InferShapes
-
-
-def test_execute_mixed_model():
-
-    out0 = helper.make_tensor_value_info("out0", TensorProto.FLOAT, [6, 3, 2, 2])
-
-    graph_def = helper.make_graph(
-        nodes=[
-            helper.make_node(
-                "MultiThreshold", ["v", "thresholds"], ["out0"], domain="finn"
-            ),
-            helper.make_node("Relu", ["out0"], ["out1"]),
-        ],
-        name="test-model",
-        inputs=[
-            helper.make_tensor_value_info("v", TensorProto.FLOAT, [6, 3, 2, 2]),
-            helper.make_tensor_value_info("thresholds", TensorProto.FLOAT, [3, 7]),
-        ],
-        outputs=[
-            helper.make_tensor_value_info("out1", TensorProto.FLOAT, [6, 3, 2, 2])
-        ],
-        value_info=[out0],
-    )
-    model_def = helper.make_model(graph_def, producer_name="onnx-example")
-
-    model = ModelWrapper(model_def)
-    model = model.transform(InferShapes())
-
-    inputs = np.asarray(
-        [
-            4.8,
-            3.2,
-            1.2,
-            4.9,
-            7.8,
-            2.4,
-            3.1,
-            4.7,
-            6.2,
-            5.1,
-            4.9,
-            2.2,
-            6.2,
-            0.0,
-            0.8,
-            4.7,
-            0.2,
-            5.6,
-            8.9,
-            9.2,
-            9.1,
-            4.0,
-            3.3,
-            4.9,
-            2.3,
-            1.7,
-            1.3,
-            2.2,
-            4.6,
-            3.4,
-            3.7,
-            9.8,
-            4.7,
-            4.9,
-            2.8,
-            2.7,
-            8.3,
-            6.7,
-            4.2,
-            7.1,
-            2.8,
-            3.1,
-            0.8,
-            0.6,
-            4.4,
-            2.7,
-            6.3,
-            6.1,
-            1.4,
-            5.3,
-            2.3,
-            1.9,
-            4.7,
-            8.1,
-            9.3,
-            3.7,
-            2.7,
-            5.1,
-            4.2,
-            1.8,
-            4.1,
-            7.3,
-            7.1,
-            0.4,
-            0.2,
-            1.3,
-            4.3,
-            8.9,
-            1.4,
-            1.6,
-            8.3,
-            9.4,
-        ],
-        dtype=np.float32,
-    ).reshape(6, 3, 2, 2)
-
-    threshold_values = np.asarray(
-        [
-            0.8,
-            1.4,
-            1.7,
-            3.5,
-            5.2,
-            6.8,
-            8.2,
-            0.2,
-            2.2,
-            3.5,
-            4.5,
-            6.6,
-            8.6,
-            9.2,
-            1.3,
-            4.1,
-            4.5,
-            6.5,
-            7.8,
-            8.1,
-            8.9,
-        ],
-        dtype=np.float32,
-    ).reshape(3, 7)
-
-    input_dict = {}
-    input_dict["v"] = inputs
-    input_dict["thresholds"] = threshold_values
-
-    output_dict = oxe.execute_onnx(model, input_dict)
-
-    outputs = np.asarray(
-        [
-            4.0,
-            3.0,
-            1.0,
-            4.0,
-            5.0,
-            2.0,
-            2.0,
-            4.0,
-            3.0,
-            3.0,
-            3.0,
-            1.0,
-            5.0,
-            0.0,
-            1.0,
-            4.0,
-            1.0,
-            4.0,
-            6.0,
-            7.0,
-            7.0,
-            1.0,
-            1.0,
-            3.0,
-            3.0,
-            3.0,
-            1.0,
-            3.0,
-            4.0,
-            2.0,
-            3.0,
-            7.0,
-            3.0,
-            3.0,
-            1.0,
-            1.0,
-            7.0,
-            5.0,
-            4.0,
-            6.0,
-            2.0,
-            2.0,
-            1.0,
-            1.0,
-            2.0,
-            1.0,
-            3.0,
-            3.0,
-            2.0,
-            5.0,
-            3.0,
-            3.0,
-            4.0,
-            5.0,
-            7.0,
-            3.0,
-            1.0,
-            3.0,
-            2.0,
-            1.0,
-            4.0,
-            6.0,
-            6.0,
-            0.0,
-            1.0,
-            1.0,
-            3.0,
-            6.0,
-            1.0,
-            1.0,
-            6.0,
-            7.0,
-        ],
-        dtype=np.float32,
-    ).reshape(6, 3, 2, 2)
-
-    assert (output_dict["out1"] == outputs).all()
diff --git a/tests/core/test_modelwrapper.py b/tests/core/test_modelwrapper.py
deleted file mode 100644
index 0fb7ae42f3bd556755f81a02be6c71fd73ffc519..0000000000000000000000000000000000000000
--- a/tests/core/test_modelwrapper.py
+++ /dev/null
@@ -1,176 +0,0 @@
-# Copyright (c) 2020, Xilinx
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-#
-# * Redistributions of source code must retain the above copyright notice, this
-#   list of conditions and the following disclaimer.
-#
-# * Redistributions in binary form must reproduce the above copyright notice,
-#   this list of conditions and the following disclaimer in the documentation
-#   and/or other materials provided with the distribution.
-#
-# * Neither the name of FINN nor the names of its
-#   contributors may be used to endorse or promote products derived from
-#   this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-import os
-import onnx
-from collections import Counter
-import brevitas.onnx as bo
-import numpy as np
-import finn.core.data_layout as DataLayout
-
-from finn.core.modelwrapper import ModelWrapper
-from finn.util.test import get_test_model_trained
-
-export_onnx_path = "test_modelwrapper.onnx"
-
-
-def test_modelwrapper():
-    lfc = get_test_model_trained("LFC", 1, 1)
-    bo.export_finn_onnx(lfc, (1, 1, 28, 28), export_onnx_path)
-    model = ModelWrapper(export_onnx_path)
-    assert model.check_all_tensor_shapes_specified() is False
-    inp_name = model.graph.input[0].name
-    inp_shape = model.get_tensor_shape(inp_name)
-    assert inp_shape == [1, 1, 28, 28]
-    # find first matmul node
-    l0_mat_tensor_name = ""
-    l0_inp_tensor_name = ""
-    for node in model.graph.node:
-        if node.op_type == "MatMul":
-            l0_inp_tensor_name = node.input[0]
-            l0_mat_tensor_name = node.input[1]
-            break
-    assert l0_mat_tensor_name != ""
-    l0_weights = model.get_initializer(l0_mat_tensor_name)
-    assert l0_weights.shape == (784, 1024)
-    l0_weights_hist = Counter(l0_weights.flatten())
-    assert (l0_weights_hist[1.0] + l0_weights_hist[-1.0]) == 784 * 1024
-    l0_weights_rand = np.random.randn(784, 1024)
-    model.set_initializer(l0_mat_tensor_name, l0_weights_rand)
-    assert (model.get_initializer(l0_mat_tensor_name) == l0_weights_rand).all()
-    assert l0_inp_tensor_name != ""
-    inp_cons = model.find_consumer(l0_inp_tensor_name)
-    assert inp_cons.op_type == "MatMul"
-    out_prod = model.find_producer(l0_inp_tensor_name)
-    assert out_prod.op_type == "MultiThreshold"
-    inp_layout = model.get_tensor_layout(inp_name)
-    assert inp_layout is None
-    inp_layout = DataLayout.NCHW
-    model.set_tensor_layout(inp_name, inp_layout)
-    assert model.get_tensor_layout(inp_name) == inp_layout
-    inp_sparsity = model.get_tensor_sparsity(inp_name)
-    assert inp_sparsity is None
-    inp_sparsity = {"dw": {"kernel_shape": 3}}
-    model.set_tensor_sparsity(inp_name, inp_sparsity)
-    assert model.get_tensor_sparsity(inp_name) == inp_sparsity
-    os.remove(export_onnx_path)
-
-
-def test_modelwrapper_graph_order():
-    # create small network with properties to be tested
-    Neg_node = onnx.helper.make_node("Neg", inputs=["in1"], outputs=["neg1"])
-    Round_node = onnx.helper.make_node("Round", inputs=["neg1"], outputs=["round1"])
-
-    Ceil_node = onnx.helper.make_node("Ceil", inputs=["neg1"], outputs=["ceil1"])
-    Add_node = onnx.helper.make_node(
-        "Add", inputs=["round1", "ceil1"], outputs=["out1"]
-    )
-
-    in1 = onnx.helper.make_tensor_value_info("in1", onnx.TensorProto.FLOAT, [4, 4])
-    out1 = onnx.helper.make_tensor_value_info("out1", onnx.TensorProto.FLOAT, [4, 4])
-
-    graph = onnx.helper.make_graph(
-        nodes=[Neg_node, Round_node, Ceil_node, Add_node],
-        name="simple_graph",
-        inputs=[in1],
-        outputs=[out1],
-        value_info=[
-            onnx.helper.make_tensor_value_info("neg1", onnx.TensorProto.FLOAT, [4, 4]),
-            onnx.helper.make_tensor_value_info(
-                "round1", onnx.TensorProto.FLOAT, [4, 4]
-            ),
-            onnx.helper.make_tensor_value_info("ceil1", onnx.TensorProto.FLOAT, [4, 4]),
-        ],
-    )
-
-    onnx_model = onnx.helper.make_model(graph, producer_name="simple-model")
-    model = ModelWrapper(onnx_model)
-
-    # test graph order functions
-    assert model.find_consumers("in1") == [Neg_node]
-    assert model.find_consumers("neg1") == [Round_node, Ceil_node]
-    assert model.find_consumers("round1") == [Add_node]
-    assert model.find_consumers("ceil1") == [Add_node]
-    assert model.find_consumers("out1") is None
-
-    assert model.find_direct_successors(Neg_node) == [Round_node, Ceil_node]
-    assert model.find_direct_successors(Round_node) == [Add_node]
-    assert model.find_direct_successors(Ceil_node) == [Add_node]
-    assert model.find_direct_successors(Add_node) is None
-
-    assert model.find_direct_predecessors(Neg_node) is None
-    assert model.find_direct_predecessors(Round_node) == [Neg_node]
-    assert model.find_direct_predecessors(Ceil_node) == [Neg_node]
-    assert model.find_direct_predecessors(Add_node) == [Round_node, Ceil_node]
-
-    assert model.get_node_index(Neg_node) == 0
-    assert model.get_node_index(Round_node) == 1
-    assert model.get_node_index(Ceil_node) == 2
-    assert model.get_node_index(Add_node) == 3
-
-
-def test_modelwrapper_detect_forks_n_joins():
-    # create small network with properties to be tested
-    Neg_node = onnx.helper.make_node("Neg", inputs=["in1"], outputs=["neg1"])
-    Round_node = onnx.helper.make_node("Round", inputs=["neg1"], outputs=["round1"])
-
-    Ceil_node = onnx.helper.make_node("Ceil", inputs=["neg1"], outputs=["ceil1"])
-    Add_node = onnx.helper.make_node(
-        "Add", inputs=["round1", "ceil1"], outputs=["out1"]
-    )
-
-    in1 = onnx.helper.make_tensor_value_info("in1", onnx.TensorProto.FLOAT, [4, 4])
-    out1 = onnx.helper.make_tensor_value_info("out1", onnx.TensorProto.FLOAT, [4, 4])
-
-    graph = onnx.helper.make_graph(
-        nodes=[Neg_node, Round_node, Ceil_node, Add_node],
-        name="simple_graph",
-        inputs=[in1],
-        outputs=[out1],
-        value_info=[
-            onnx.helper.make_tensor_value_info("neg1", onnx.TensorProto.FLOAT, [4, 4]),
-            onnx.helper.make_tensor_value_info(
-                "round1", onnx.TensorProto.FLOAT, [4, 4]
-            ),
-            onnx.helper.make_tensor_value_info("ceil1", onnx.TensorProto.FLOAT, [4, 4]),
-        ],
-    )
-
-    onnx_model = onnx.helper.make_model(graph, producer_name="simple-model")
-    model = ModelWrapper(onnx_model)
-
-    # test
-    assert model.is_fork_node(Neg_node)
-    assert not model.is_fork_node(Round_node)
-    assert not model.is_fork_node(Ceil_node)
-    assert not model.is_fork_node(Add_node)
-
-    assert not model.is_join_node(Neg_node)
-    assert not model.is_join_node(Round_node)
-    assert not model.is_join_node(Ceil_node)
-    assert model.is_join_node(Add_node)
diff --git a/tests/custom_op/test_im2col.py b/tests/custom_op/test_im2col.py
deleted file mode 100644
index 0b148145bd6f7d9819e4b72da5333d2556de94e4..0000000000000000000000000000000000000000
--- a/tests/custom_op/test_im2col.py
+++ /dev/null
@@ -1,320 +0,0 @@
-import numpy as np
-from onnx import TensorProto, helper
-
-import finn.core.onnx_exec as oxe
-from finn.core.datatype import DataType
-from finn.core.modelwrapper import ModelWrapper
-from finn.transformation.infer_datatypes import InferDataTypes
-from finn.transformation.infer_shapes import InferShapes
-from finn.custom_op.im2col import compute_conv_output_dim
-
-
-def check_two_dict_for_equality(dict1, dict2):
-    for key in dict1:
-        assert key in dict2, "Key: {} is not in both dictionaries".format(key)
-        assert (
-            dict1[key] == dict2[key]
-        ), """Values for key {} are not the same
-        in both dictionaries""".format(
-            key
-        )
-
-    return True
-
-
-def execution_im2col(x, idt, k, stride, ifm_ch, ifm_dim, pad_amt=0, pad_val=0):
-    ofm_dim = compute_conv_output_dim(ifm_dim, k, stride, pad_amt)
-
-    # set up onnx model
-    inp = helper.make_tensor_value_info(
-        "inp", TensorProto.FLOAT, [1, ifm_dim, ifm_dim, ifm_ch]
-    )
-    outp = helper.make_tensor_value_info(
-        "outp", TensorProto.FLOAT, [1, ofm_dim, ofm_dim, k * k * ifm_ch]
-    )
-
-    Im2Col_node = helper.make_node(
-        "Im2Col",
-        ["inp"],
-        ["outp"],
-        domain="finn",
-        stride=stride,
-        kernel_size=k,
-        pad_amount=pad_amt,
-        pad_value=pad_val,
-        input_shape="(1,{},{},{})".format(ifm_dim, ifm_dim, ifm_ch),
-    )
-
-    graph = helper.make_graph(
-        nodes=[Im2Col_node], name="im2col_graph", inputs=[inp], outputs=[outp]
-    )
-
-    model = helper.make_model(graph, producer_name="im2col-model")
-    model = ModelWrapper(model)
-
-    model.set_tensor_datatype("inp", idt)
-
-    # test shape inference
-    model.transform(InferShapes())
-    assert model.get_tensor_shape("outp") == [1, ofm_dim, ofm_dim, k * k * ifm_ch]
-
-    # test datatype inference
-    assert model.get_tensor_datatype("outp") is DataType.FLOAT32
-    model = model.transform(InferDataTypes())
-    assert model.get_tensor_datatype("outp") is idt
-
-    # prepare input data
-    input_dict = {"inp": x}
-
-    # execute model
-    y_produced = oxe.execute_onnx(model, input_dict)["outp"]
-
-    return y_produced
-
-
-def test_im2col():
-    # bipolar inputs with following im2col parameters
-    idt = DataType.BIPOLAR
-    k = 2
-    stride = 1
-    ifm_ch = 1
-    ifm_dim = 4
-    pad_amt = 0
-    pad_val = 0
-    ofm_dim = compute_conv_output_dim(ifm_dim, k, stride, pad_amt)
-
-    x = np.asarray(
-        [
-            -1.0,
-            -1.0,
-            1.0,
-            1.0,
-            1.0,
-            -1.0,
-            1.0,
-            -1.0,
-            -1.0,
-            1.0,
-            -1.0,
-            -1.0,
-            1.0,
-            1.0,
-            1.0,
-            1.0,
-        ],
-        dtype=np.float32,
-    ).reshape(1, ifm_dim, ifm_dim, ifm_ch)
-
-    expected = np.asarray(
-        [
-            -1.0,
-            -1.0,
-            1.0,
-            -1.0,
-            -1.0,
-            1.0,
-            -1.0,
-            1.0,
-            1.0,
-            1.0,
-            1.0,
-            -1.0,
-            1.0,
-            -1.0,
-            -1.0,
-            1.0,
-            -1.0,
-            1.0,
-            1.0,
-            -1.0,
-            1.0,
-            -1.0,
-            -1.0,
-            -1.0,
-            -1.0,
-            1.0,
-            1.0,
-            1.0,
-            1.0,
-            -1.0,
-            1.0,
-            1.0,
-            -1.0,
-            -1.0,
-            1.0,
-            1.0,
-        ],
-        dtype=np.float32,
-    ).reshape(1, ofm_dim, ofm_dim, k * k * ifm_ch)
-
-    produced = execution_im2col(x, idt, k, stride, ifm_ch, ifm_dim, pad_amt, pad_val)
-    assert (produced == expected).all()
-
-    idt = DataType.INT8
-    k = 2
-    stride = 1
-    ifm_ch = 2
-    ifm_dim = 4
-    pad_amt = 0
-    pad_val = 0
-    ofm_dim = compute_conv_output_dim(ifm_dim, k, stride, pad_amt)
-
-    x = np.asarray(
-        [
-            [
-                [[1, -1], [2, -2], [3, -3], [4, -4]],
-                [[5, -5], [6, -6], [7, -7], [8, -8]],
-                [[9, -9], [10, -10], [11, -11], [12, -12]],
-                [[13, -13], [14, -14], [15, -15], [16, -16]],
-            ]
-        ],
-        dtype=np.float32,
-    )
-
-    expected = np.asarray(
-        [
-            [
-                [
-                    [1.0, -1.0, 2.0, -2.0, 5.0, -5.0, 6.0, -6.0],
-                    [2.0, -2.0, 3.0, -3.0, 6.0, -6.0, 7.0, -7.0],
-                    [3.0, -3.0, 4.0, -4.0, 7.0, -7.0, 8.0, -8.0],
-                ],
-                [
-                    [5.0, -5.0, 6.0, -6.0, 9.0, -9.0, 10.0, -10.0],
-                    [6.0, -6.0, 7.0, -7.0, 10.0, -10.0, 11.0, -11.0],
-                    [7.0, -7.0, 8.0, -8.0, 11.0, -11.0, 12.0, -12.0],
-                ],
-                [
-                    [9.0, -9.0, 10.0, -10.0, 13.0, -13.0, 14.0, -14.0],
-                    [10.0, -10.0, 11.0, -11.0, 14.0, -14.0, 15.0, -15.0],
-                    [11.0, -11.0, 12.0, -12.0, 15.0, -15.0, 16.0, -16.0],
-                ],
-            ]
-        ],
-        dtype=np.float32,
-    )
-
-    produced = execution_im2col(x, idt, k, stride, ifm_ch, ifm_dim, pad_amt, pad_val)
-    assert (produced == expected).all()
-
-    idt = DataType.INT8
-    k = 2
-    stride = 1
-    ifm_ch = 2
-    ifm_dim = 4
-    pad_amt = 1
-    pad_val = 0
-    ofm_dim = compute_conv_output_dim(ifm_dim, k, stride, pad_amt)
-
-    x = np.asarray(
-        [
-            [
-                [[1, -1], [2, -2], [3, -3], [4, -4]],
-                [[5, -5], [6, -6], [7, -7], [8, -8]],
-                [[9, -9], [10, -10], [11, -11], [12, -12]],
-                [[13, -13], [14, -14], [15, -15], [16, -16]],
-            ]
-        ],
-        dtype=np.float32,
-    )
-
-    expected = np.asarray(
-        [
-            [
-                [
-                    [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, -1.0],
-                    [0.0, 0.0, 0.0, 0.0, 1.0, -1.0, 2.0, -2.0],
-                    [0.0, 0.0, 0.0, 0.0, 2.0, -2.0, 3.0, -3.0],
-                    [0.0, 0.0, 0.0, 0.0, 3.0, -3.0, 4.0, -4.0],
-                    [0.0, 0.0, 0.0, 0.0, 4.0, -4.0, 0.0, 0.0],
-                ],
-                [
-                    [0.0, 0.0, 1.0, -1.0, 0.0, 0.0, 5.0, -5.0],
-                    [1.0, -1.0, 2.0, -2.0, 5.0, -5.0, 6.0, -6.0],
-                    [2.0, -2.0, 3.0, -3.0, 6.0, -6.0, 7.0, -7.0],
-                    [3.0, -3.0, 4.0, -4.0, 7.0, -7.0, 8.0, -8.0],
-                    [4.0, -4.0, 0.0, 0.0, 8.0, -8.0, 0.0, 0.0],
-                ],
-                [
-                    [0.0, 0.0, 5.0, -5.0, 0.0, 0.0, 9.0, -9.0],
-                    [5.0, -5.0, 6.0, -6.0, 9.0, -9.0, 10.0, -10.0],
-                    [6.0, -6.0, 7.0, -7.0, 10.0, -10.0, 11.0, -11.0],
-                    [7.0, -7.0, 8.0, -8.0, 11.0, -11.0, 12.0, -12.0],
-                    [8.0, -8.0, 0.0, 0.0, 12.0, -12.0, 0.0, 0.0],
-                ],
-                [
-                    [0.0, 0.0, 9.0, -9.0, 0.0, 0.0, 13.0, -13.0],
-                    [9.0, -9.0, 10.0, -10.0, 13.0, -13.0, 14.0, -14.0],
-                    [10.0, -10.0, 11.0, -11.0, 14.0, -14.0, 15.0, -15.0],
-                    [11.0, -11.0, 12.0, -12.0, 15.0, -15.0, 16.0, -16.0],
-                    [12.0, -12.0, 0.0, 0.0, 16.0, -16.0, 0.0, 0.0],
-                ],
-                [
-                    [0.0, 0.0, 13.0, -13.0, 0.0, 0.0, 0.0, 0.0],
-                    [13.0, -13.0, 14.0, -14.0, 0.0, 0.0, 0.0, 0.0],
-                    [14.0, -14.0, 15.0, -15.0, 0.0, 0.0, 0.0, 0.0],
-                    [15.0, -15.0, 16.0, -16.0, 0.0, 0.0, 0.0, 0.0],
-                    [16.0, -16.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
-                ],
-            ]
-        ],
-        dtype=np.float32,
-    )
-
-    produced = execution_im2col(x, idt, k, stride, ifm_ch, ifm_dim, pad_amt, pad_val)
-    assert (produced == expected).all()
-
-
-def test_im2col_infer_shapes():
-    idt = DataType.BIPOLAR
-    k = 2
-    stride = 1
-    ifm_ch = 1
-    ifm_dim = 4
-    ofm_dim = int(((ifm_dim - k) / stride) + 1)
-
-    # set up onnx model
-    inp = helper.make_tensor_value_info(
-        "inp", TensorProto.FLOAT, [1, ifm_dim, ifm_dim, ifm_ch]
-    )
-    outp = helper.make_tensor_value_info(
-        "outp", TensorProto.FLOAT, [1, ofm_dim, ofm_dim, k * k * ifm_ch]
-    )
-
-    abs_node = helper.make_node("Abs", inputs=["inp"], outputs=["abs"])
-
-    Im2Col_node = helper.make_node(
-        "Im2Col",
-        ["abs"],
-        ["im2col"],
-        domain="finn",
-        stride=stride,
-        kernel_size=k,
-        input_shape="(1,{},{},{})".format(ifm_dim, ifm_dim, ifm_ch),
-    )
-
-    abs1_node = helper.make_node("Abs", inputs=["im2col"], outputs=["outp"])
-
-    graph = helper.make_graph(
-        nodes=[abs_node, Im2Col_node, abs1_node],
-        name="shape_graph",
-        inputs=[inp],
-        outputs=[outp],
-        value_info=[
-            helper.make_tensor_value_info(
-                "abs", TensorProto.FLOAT, [1, ifm_dim, ifm_dim, ifm_ch]
-            ),
-            helper.make_tensor_value_info(
-                "im2col", TensorProto.FLOAT, [1, ofm_dim, ofm_dim, k * k * ifm_ch]
-            ),
-        ],
-    )
-
-    model = helper.make_model(graph, producer_name="shape-model")
-    model = ModelWrapper(model)
-
-    model.set_tensor_datatype("inp", idt)
-
-    # test shape inference
-    model.transform(InferShapes())
-    assert model.get_tensor_shape("im2col") == [1, ofm_dim, ofm_dim, k * k * ifm_ch]
diff --git a/tests/custom_op/test_multithreshold.py b/tests/custom_op/test_multithreshold.py
deleted file mode 100644
index 7e6ad4fe08517290dd22a2c74b2847d007b74b1f..0000000000000000000000000000000000000000
--- a/tests/custom_op/test_multithreshold.py
+++ /dev/null
@@ -1,322 +0,0 @@
-# Copyright (c) 2020, Xilinx
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-#
-# * Redistributions of source code must retain the above copyright notice, this
-#   list of conditions and the following disclaimer.
-#
-# * Redistributions in binary form must reproduce the above copyright notice,
-#   this list of conditions and the following disclaimer in the documentation
-#   and/or other materials provided with the distribution.
-#
-# * Neither the name of FINN nor the names of its
-#   contributors may be used to endorse or promote products derived from
-#   this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-import numpy as np
-import time
-from finn.custom_op.multithreshold import multithreshold
-
-
-def compare(x, y):
-    """Comparison helper function for multithresholding.
-
-    Gets two values and returns 1.0 if x>=y otherwise 0.0."""
-    if x >= y:
-        return 1.0
-    else:
-        return 0.0
-
-# naive implementation of thresholding for performance comparison
-def multithreshold_elementwise(v, thresholds, out_scale=None, out_bias=None):
-    """Given a set of threshold values t={t_0, t_1 ... t_n} the successive
-    thresholding maps any real number x to an integer in the interval [0, n],
-    where the returned integer is the number of thresholds x is greater than
-    or equal to.
-
-    The output tensor will be scaled by out_scale and biased by out_bias."""
-    # the inputs are expected to be in the shape (N,C,H,W) or (N, C)
-    # the MultiThreshold node supports a data_layout attribute that can be set
-    # to 'NHWC' to support (N,H,W,C) data layout mode for in-out as well
-    # N : Batch size
-    # C : Number of channels
-    # H : Heigth of the input images
-    # W : Width of the input images
-    #
-    # the thresholds are expected to be in the shape (C, B)
-    # C : Number of channels (must be the same value as C in input tensor
-    #     or 1 if all channels use the same threshold value)
-    # B : Desired activation steps => i.e. for 4-bit activation,
-    #     B=7 (2^(n)-1 and n=4)
-    # the output tensor will be scaled by out_scale and biased by out_bias
-    # assert threshold shape
-    is_global_threshold = thresholds.shape[0] == 1
-    assert (
-        v.shape[1] == thresholds.shape[0]
-    ) or is_global_threshold, """"Threshold
-    shape incorrect"""
-    # save the required shape sizes for the loops (N, C and B)
-    num_batch = v.shape[0]
-    num_channel = v.shape[1]
-    num_act = thresholds.shape[1]
-    # reshape inputs to enable channel-wise reading
-    vr = v.reshape((v.shape[0], v.shape[1], -1))
-    # save the new shape size of the images
-    num_img_elem = vr.shape[2]
-    # initiate output tensor
-    ret = np.zeros_like(vr)
-    # iterate over thresholds channel-wise
-    for t in range(num_channel):
-        channel_thresh = thresholds[0] if is_global_threshold else thresholds[t]
-        # iterate over batches
-        for b in range(num_batch):
-            # iterate over image elements on which the thresholds will be applied
-            for elem in range(num_img_elem):
-                # iterate over the different thresholds for one channel
-                for a in range(num_act):
-                    # apply successive thresholding to every element
-                    ret[b][t][elem] += compare(vr[b][t][elem], channel_thresh[a])
-    if out_scale is None:
-        out_scale = 1.0
-    if out_bias is None:
-        out_bias = 0.0
-    return out_scale * ret.reshape(v.shape) + out_bias
-
-
-def test_multithreshold():
-
-    inputs = np.ndarray(
-        shape=(6, 3, 2, 2),
-        buffer=np.array(
-            [
-                4.8,
-                3.2,
-                1.2,
-                4.9,
-                7.8,
-                2.4,
-                3.1,
-                4.7,
-                6.2,
-                5.1,
-                4.9,
-                2.2,
-                6.2,
-                0.0,
-                0.8,
-                4.7,
-                0.2,
-                5.6,
-                8.9,
-                9.2,
-                9.1,
-                4.0,
-                3.3,
-                4.9,
-                2.3,
-                1.7,
-                1.3,
-                2.2,
-                4.6,
-                3.4,
-                3.7,
-                9.8,
-                4.7,
-                4.9,
-                2.8,
-                2.7,
-                8.3,
-                6.7,
-                4.2,
-                7.1,
-                2.8,
-                3.1,
-                0.8,
-                0.6,
-                4.4,
-                2.7,
-                6.3,
-                6.1,
-                1.4,
-                5.3,
-                2.3,
-                1.9,
-                4.7,
-                8.1,
-                9.3,
-                3.7,
-                2.7,
-                5.1,
-                4.2,
-                1.8,
-                4.1,
-                7.3,
-                7.1,
-                0.4,
-                0.2,
-                1.3,
-                4.3,
-                8.9,
-                1.4,
-                1.6,
-                8.3,
-                9.4,
-            ]
-        ),
-    )
-
-    thresholds = np.ndarray(
-        shape=(3, 7),
-        buffer=np.array(
-            [
-                0.8,
-                1.4,
-                1.7,
-                3.5,
-                5.2,
-                6.8,
-                8.2,
-                0.2,
-                2.2,
-                3.5,
-                4.5,
-                6.6,
-                8.6,
-                9.2,
-                1.3,
-                4.1,
-                4.5,
-                6.5,
-                7.8,
-                8.1,
-                8.9,
-            ]
-        ),
-    )
-
-    outputs = np.ndarray(
-        shape=(6, 3, 2, 2),
-        buffer=np.array(
-            [
-                4.0,
-                3.0,
-                1.0,
-                4.0,
-                5.0,
-                2.0,
-                2.0,
-                4.0,
-                3.0,
-                3.0,
-                3.0,
-                1.0,
-                5.0,
-                0.0,
-                1.0,
-                4.0,
-                1.0,
-                4.0,
-                6.0,
-                7.0,
-                7.0,
-                1.0,
-                1.0,
-                3.0,
-                3.0,
-                3.0,
-                1.0,
-                3.0,
-                4.0,
-                2.0,
-                3.0,
-                7.0,
-                3.0,
-                3.0,
-                1.0,
-                1.0,
-                7.0,
-                5.0,
-                4.0,
-                6.0,
-                2.0,
-                2.0,
-                1.0,
-                1.0,
-                2.0,
-                1.0,
-                3.0,
-                3.0,
-                2.0,
-                5.0,
-                3.0,
-                3.0,
-                4.0,
-                5.0,
-                7.0,
-                3.0,
-                1.0,
-                3.0,
-                2.0,
-                1.0,
-                4.0,
-                6.0,
-                6.0,
-                0.0,
-                1.0,
-                1.0,
-                3.0,
-                6.0,
-                1.0,
-                1.0,
-                6.0,
-                7.0,
-            ]
-        ),
-    )
-
-    results = multithreshold(inputs, thresholds)
-    assert (results == outputs).all()
-
-    results_scaled = multithreshold(inputs, thresholds, 2.0, -1.0)
-    outputs_scaled = 2.0 * outputs - 1.0
-    assert (results_scaled == outputs_scaled).all()
-
-    # performance and random test
-    np.random.seed(0)
-    inputs = np.random.random((1, 256, 64, 64))
-    thresholds = (np.array([[1, 2, 3, 4, 5, 6]]) - 0.5) / 6
-
-    before = time.time()
-    vec_results = multithreshold(inputs, thresholds)
-    after = time.time()
-    vector_runtime = after - before
-
-    before = time.time()
-    nonvec_results = multithreshold_elementwise(inputs, thresholds)
-    after = time.time()
-    non_vector_runtime = after - before
-
-    assert (vec_results == nonvec_results).all()
-
-    return vector_runtime, non_vector_runtime
-
-
-if __name__ == "__main__":
-    vector_runtime, non_vector_runtime = test_multithreshold()
-
-    print("Runtime non-vectorized: ", non_vector_runtime, "s")
-    print("Runtime vectorized: ", vector_runtime, "s")
-    print("Speed-up: ", non_vector_runtime / vector_runtime)
diff --git a/tests/custom_op/test_xnorpopcountmatmul.py b/tests/custom_op/test_xnorpopcountmatmul.py
deleted file mode 100644
index 745b782d418129d96e21c327a49de04d53aa7c48..0000000000000000000000000000000000000000
--- a/tests/custom_op/test_xnorpopcountmatmul.py
+++ /dev/null
@@ -1,108 +0,0 @@
-# Copyright (c) 2020, Xilinx
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-#
-# * Redistributions of source code must retain the above copyright notice, this
-#   list of conditions and the following disclaimer.
-#
-# * Redistributions in binary form must reproduce the above copyright notice,
-#   this list of conditions and the following disclaimer in the documentation
-#   and/or other materials provided with the distribution.
-#
-# * Neither the name of FINN nor the names of its
-#   contributors may be used to endorse or promote products derived from
-#   this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-import os
-from pkgutil import get_data
-
-import brevitas.onnx as bo
-import numpy as np
-import onnx
-import onnx.helper as helper
-import onnx.numpy_helper as nph
-from onnx import TensorProto
-
-import finn.core.onnx_exec as oxe
-from finn.core.datatype import DataType
-from finn.core.modelwrapper import ModelWrapper
-from finn.transformation.bipolar_to_xnor import ConvertBipolarMatMulToXnorPopcount
-from finn.transformation.fold_constants import FoldConstants
-from finn.transformation.general import GiveReadableTensorNames, GiveUniqueNodeNames
-from finn.transformation.infer_datatypes import InferDataTypes
-from finn.transformation.infer_shapes import InferShapes
-from finn.transformation.streamline.sign_to_thres import ConvertSignToThres
-from finn.util.test import get_test_model_trained
-
-export_onnx_path = "test_xnorpopcountmatmul.onnx"
-
-
-def test_xnorpopcountmatmul():
-    M = 1
-    K = 3
-    N = 3
-    x = helper.make_tensor_value_info("x", TensorProto.FLOAT, [M, K])
-    W = helper.make_tensor_value_info("W", TensorProto.FLOAT, [K, N])
-    out = helper.make_tensor_value_info("out", TensorProto.FLOAT, ["x", "y"])
-    node_def = helper.make_node(
-        "XnorPopcountMatMul", ["x", "W"], ["out"], domain="finn"
-    )
-    modelproto = helper.make_model(
-        helper.make_graph([node_def], "test_model", [x], [out], value_info=[W])
-    )
-    model = ModelWrapper(modelproto)
-    model.set_tensor_datatype("x", DataType.BINARY)
-    model.set_tensor_datatype("W", DataType.BINARY)
-    W_data = np.asarray([[1, 0, 0], [0, 1, 0], [0, 0, 1]], dtype=np.float32)
-    model.set_initializer("W", W_data)
-    # test shape inference
-    model = model.transform(InferShapes())
-    assert model.get_tensor_shape("out") == [M, N]
-    # test datatype inference
-    assert model.get_tensor_datatype("out") is DataType.FLOAT32
-    model = model.transform(InferDataTypes())
-    assert model.get_tensor_datatype("out") is DataType.UINT32
-    # test execution
-    x_data = np.asarray([[1, 0, 0]], dtype=np.float32)
-    inp_dict = {"x": x_data}
-    out_dict = oxe.execute_onnx(model, inp_dict)
-    Wb = 2 * W_data - 1
-    xb = 2 * x_data - 1
-    rb = np.matmul(xb, Wb)
-    assert (2 * out_dict["out"] - K == rb).all()
-
-
-def test_convert_bipolar_matmul_to_xnorpopcountmatmul():
-    lfc = get_test_model_trained("LFC", 1, 1)
-    bo.export_finn_onnx(lfc, (1, 1, 28, 28), export_onnx_path)
-    model = ModelWrapper(export_onnx_path)
-    model = model.transform(InferShapes())
-    model = model.transform(FoldConstants())
-    model = model.transform(GiveUniqueNodeNames())
-    model = model.transform(GiveReadableTensorNames())
-    model = model.transform(ConvertSignToThres())
-    # load one of the test vectors
-    raw_i = get_data("finn", "data/onnx/mnist-conv/test_data_set_0/input_0.pb")
-    input_tensor = onnx.load_tensor_from_string(raw_i)
-    # run using FINN-based execution
-    input_dict = {"global_in": nph.to_array(input_tensor)}
-    expected_ctx = oxe.execute_onnx(model, input_dict, True)
-    expected = expected_ctx[model.graph.output[0].name]
-    model = model.transform(ConvertBipolarMatMulToXnorPopcount())
-    produced_ctx = oxe.execute_onnx(model, input_dict, True)
-    produced = produced_ctx[model.graph.output[0].name]
-    assert np.isclose(expected, produced, atol=1e-3).all()
-    os.remove(export_onnx_path)
diff --git a/tests/fpgadataflow/test_convert_to_hls_layers_cnv.py b/tests/fpgadataflow/test_convert_to_hls_layers_cnv.py
index e8b50efef0723c1394c2bdd438a87e090071507d..20751a5877a879eeabf1ed6b67a7573208cf9367 100644
--- a/tests/fpgadataflow/test_convert_to_hls_layers_cnv.py
+++ b/tests/fpgadataflow/test_convert_to_hls_layers_cnv.py
@@ -73,7 +73,7 @@ def test_convert_to_hls_layers_cnv_w1a1(fused_activation):
     model = model.transform(InferDataLayouts())
     # model.save("golden.onnx")
     # load one of the test vectors
-    fn = pk.resource_filename("finn", "data/cifar10/cifar10-test-data-class3.npz")
+    fn = pk.resource_filename("finn.qnn-data", "cifar10/cifar10-test-data-class3.npz")
     input_tensor = np.load(fn)["arr_0"].astype(np.float32)
     input_tensor = input_tensor / 255
     assert input_tensor.shape == (1, 3, 32, 32)
diff --git a/tests/fpgadataflow/test_convert_to_hls_layers_fc.py b/tests/fpgadataflow/test_convert_to_hls_layers_fc.py
index bd600c6c57d00d5fc03152f75b9f2f8c6beeeb2c..cb66fa7237416579b509aa4f508c9105d386d08a 100644
--- a/tests/fpgadataflow/test_convert_to_hls_layers_fc.py
+++ b/tests/fpgadataflow/test_convert_to_hls_layers_fc.py
@@ -110,7 +110,7 @@ def test_convert_to_hls_layers_tfc_w1a1():
     model = model.transform(CompileCppSim())
     model = model.transform(SetExecMode("cppsim"))
 
-    raw_i = get_data("finn", "data/onnx/mnist-conv/test_data_set_0/input_0.pb")
+    raw_i = get_data("finn.data", "onnx/mnist-conv/test_data_set_0/input_0.pb")
     input_tensor = onnx.load_tensor_from_string(raw_i)
     # run using FINN-based execution
     input_dict = {"global_in": nph.to_array(input_tensor)}
@@ -175,7 +175,7 @@ def test_convert_to_hls_layers_tfc_w1a2():
     model = model.transform(PrepareCppSim())
     model = model.transform(CompileCppSim())
     model = model.transform(SetExecMode("cppsim"))
-    raw_i = get_data("finn", "data/onnx/mnist-conv/test_data_set_0/input_0.pb")
+    raw_i = get_data("finn.data", "onnx/mnist-conv/test_data_set_0/input_0.pb")
     input_tensor = onnx.load_tensor_from_string(raw_i)
     # run using FINN-based execution
     input_dict = {"global_in": nph.to_array(input_tensor)}
diff --git a/tests/fpgadataflow/test_create_dataflow_partition.py b/tests/fpgadataflow/test_create_dataflow_partition.py
index c4f748051ff038371353574298580f3bf9e05e9f..6732b92ae0865e390002bd3c65dfefe3890610e2 100644
--- a/tests/fpgadataflow/test_create_dataflow_partition.py
+++ b/tests/fpgadataflow/test_create_dataflow_partition.py
@@ -45,7 +45,7 @@ build_dir = make_build_dir("test_dataflow_partition_")
 def test_dataflow_partition_create():
     # load the onnx model
     raw_m = get_data(
-        "finn", "data/onnx/finn-hls-model/tfc_w1_a1_after_conv_to_hls.onnx"
+        "finn.qnn-data", "onnx/finn-hls-model/tfc_w1_a1_after_conv_to_hls.onnx"
     )
     model = ModelWrapper(raw_m)
     model = model.transform(CreateDataflowPartition())
diff --git a/tests/util/test_shape_utils.py b/tests/test_enforce_import_order.py
similarity index 78%
rename from tests/util/test_shape_utils.py
rename to tests/test_enforce_import_order.py
index ab58f591f39234b659de7c05f2592c76ebb73a3e..3859a6ca38c9d69e9d0b2ab5dd331077fe5a7f36 100644
--- a/tests/util/test_shape_utils.py
+++ b/tests/test_enforce_import_order.py
@@ -26,16 +26,18 @@
 # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
-import numpy as np
 
-import finn.util.basic as util
+# IMPORTANT:
+# - do not move this file to subfolders
+#      pytest must discover it before other FINN tests, can be checked with:
+#        pytest --collect-only
+# - do not change the order of imports below
+# this is to workaround the onnx 1.6.0/pytorch bug
+# https://github.com/onnx/onnx/issues/2394#issuecomment-581638840
 
+import onnx  # noqa
+import torch  # noqa
 
-def test_interleave_matrix_outer_dim_from_partitions():
-    A = np.eye(10)
-    n_parts = 2
-    Ax = util.interleave_matrix_outer_dim_from_partitions(A, n_parts)
-    part_size = 10 // n_parts
-    assert Ax.shape == (n_parts, part_size, 10)
-    for r_ind in range(A.shape[0]):
-        assert (A[r_ind] == Ax[r_ind % n_parts][r_ind // n_parts]).all()
+
+def test_enforce_import_order():
+    assert True
diff --git a/tests/transformation/test_absorb_mul_into_topk.py b/tests/transformation/streamline/test_absorb_mul_into_topk.py
similarity index 100%
rename from tests/transformation/test_absorb_mul_into_topk.py
rename to tests/transformation/streamline/test_absorb_mul_into_topk.py
diff --git a/tests/transformation/test_absorb_opposite_transposes.py b/tests/transformation/streamline/test_absorb_opposite_transposes.py
similarity index 100%
rename from tests/transformation/test_absorb_opposite_transposes.py
rename to tests/transformation/streamline/test_absorb_opposite_transposes.py
diff --git a/tests/transformation/test_absorb_transp_into_flatten.py b/tests/transformation/streamline/test_absorb_transp_into_flatten.py
similarity index 100%
rename from tests/transformation/test_absorb_transp_into_flatten.py
rename to tests/transformation/streamline/test_absorb_transp_into_flatten.py
diff --git a/tests/transformation/test_collapse_repeated_op.py b/tests/transformation/streamline/test_collapse_repeated_op.py
similarity index 100%
rename from tests/transformation/test_collapse_repeated_op.py
rename to tests/transformation/streamline/test_collapse_repeated_op.py
diff --git a/tests/transformation/test_factor_out_mul_sign_magnitude.py b/tests/transformation/streamline/test_factor_out_mul_sign_magnitude.py
similarity index 100%
rename from tests/transformation/test_factor_out_mul_sign_magnitude.py
rename to tests/transformation/streamline/test_factor_out_mul_sign_magnitude.py
diff --git a/tests/transformation/test_linear_past_eltwise.py b/tests/transformation/streamline/test_linear_past_eltwise.py
similarity index 100%
rename from tests/transformation/test_linear_past_eltwise.py
rename to tests/transformation/streamline/test_linear_past_eltwise.py
diff --git a/tests/transformation/test_move_add_past_mul.py b/tests/transformation/streamline/test_move_add_past_mul.py
similarity index 100%
rename from tests/transformation/test_move_add_past_mul.py
rename to tests/transformation/streamline/test_move_add_past_mul.py
diff --git a/tests/transformation/test_move_chw_add_past_conv.py b/tests/transformation/streamline/test_move_chw_add_past_conv.py
similarity index 100%
rename from tests/transformation/test_move_chw_add_past_conv.py
rename to tests/transformation/streamline/test_move_chw_add_past_conv.py
diff --git a/tests/transformation/test_move_flatten_past_affine.py b/tests/transformation/streamline/test_move_flatten_past_affine.py
similarity index 100%
rename from tests/transformation/test_move_flatten_past_affine.py
rename to tests/transformation/streamline/test_move_flatten_past_affine.py
diff --git a/tests/transformation/test_move_flatten_past_topk.py b/tests/transformation/streamline/test_move_flatten_past_topk.py
similarity index 100%
rename from tests/transformation/test_move_flatten_past_topk.py
rename to tests/transformation/streamline/test_move_flatten_past_topk.py
diff --git a/tests/transformation/test_move_maxpool_past_multithreshold.py b/tests/transformation/streamline/test_move_maxpool_past_multithreshold.py
similarity index 100%
rename from tests/transformation/test_move_maxpool_past_multithreshold.py
rename to tests/transformation/streamline/test_move_maxpool_past_multithreshold.py
diff --git a/tests/transformation/test_move_mul_past_dw_conv.py b/tests/transformation/streamline/test_move_mul_past_dw_conv.py
similarity index 100%
rename from tests/transformation/test_move_mul_past_dw_conv.py
rename to tests/transformation/streamline/test_move_mul_past_dw_conv.py
diff --git a/tests/transformation/test_move_past_fork.py b/tests/transformation/streamline/test_move_past_fork.py
similarity index 100%
rename from tests/transformation/test_move_past_fork.py
rename to tests/transformation/streamline/test_move_past_fork.py
diff --git a/tests/transformation/test_move_scalar_past_conv.py b/tests/transformation/streamline/test_move_scalar_past_conv.py
similarity index 100%
rename from tests/transformation/test_move_scalar_past_conv.py
rename to tests/transformation/streamline/test_move_scalar_past_conv.py
diff --git a/tests/transformation/test_move_scalar_past_matmul.py b/tests/transformation/streamline/test_move_scalar_past_matmul.py
similarity index 100%
rename from tests/transformation/test_move_scalar_past_matmul.py
rename to tests/transformation/streamline/test_move_scalar_past_matmul.py
diff --git a/tests/transformation/test_move_transpose_past_scalar_mul.py b/tests/transformation/streamline/test_move_transpose_past_scalar_mul.py
similarity index 100%
rename from tests/transformation/test_move_transpose_past_scalar_mul.py
rename to tests/transformation/streamline/test_move_transpose_past_scalar_mul.py
diff --git a/tests/transformation/test_remove_identity_ops.py b/tests/transformation/streamline/test_remove_identity_ops.py
similarity index 100%
rename from tests/transformation/test_remove_identity_ops.py
rename to tests/transformation/streamline/test_remove_identity_ops.py
diff --git a/tests/transformation/test_round_thresholds.py b/tests/transformation/streamline/test_round_thresholds.py
similarity index 100%
rename from tests/transformation/test_round_thresholds.py
rename to tests/transformation/streamline/test_round_thresholds.py
diff --git a/tests/transformation/test_sign_to_thres.py b/tests/transformation/streamline/test_sign_to_thres.py
similarity index 97%
rename from tests/transformation/test_sign_to_thres.py
rename to tests/transformation/streamline/test_sign_to_thres.py
index a92f839e5f6ca8b45eadf939fa35973ac153e0b1..4618dffc43f5cee848b580b77cf418c612b48f3e 100644
--- a/tests/transformation/test_sign_to_thres.py
+++ b/tests/transformation/streamline/test_sign_to_thres.py
@@ -52,7 +52,7 @@ def test_sign_to_thres():
     new_model = model.transform(ConvertSignToThres())
     assert new_model.graph.node[3].op_type == "MultiThreshold"
     # load one of the test vectors
-    raw_i = get_data("finn", "data/onnx/mnist-conv/test_data_set_0/input_0.pb")
+    raw_i = get_data("finn.data", "onnx/mnist-conv/test_data_set_0/input_0.pb")
     input_tensor = onnx.load_tensor_from_string(raw_i)
     input_dict = {"0": nph.to_array(input_tensor)}
     assert oxe.compare_execution(model, new_model, input_dict)
diff --git a/tests/transformation/streamline/test_streamline_cnv.py b/tests/transformation/streamline/test_streamline_cnv.py
index 82a38636e3927e17e5e2a3e8714f46082bba10e4..ca8cf3b1ceba6943828f47bcbcf974aa5b368c4e 100644
--- a/tests/transformation/streamline/test_streamline_cnv.py
+++ b/tests/transformation/streamline/test_streamline_cnv.py
@@ -67,7 +67,7 @@ def test_streamline_cnv(size, wbits, abits):
     model = model.transform(GiveReadableTensorNames())
     model = model.transform(RemoveStaticGraphInputs())
     # load one of the test vectors
-    fn = pk.resource_filename("finn", "data/cifar10/cifar10-test-data-class3.npz")
+    fn = pk.resource_filename("finn.qnn-data", "cifar10/cifar10-test-data-class3.npz")
     input_tensor = np.load(fn)["arr_0"].astype(np.float32)
     input_tensor = input_tensor / 255
     assert input_tensor.shape == (1, 3, 32, 32)
diff --git a/tests/transformation/streamline/test_streamline_fc.py b/tests/transformation/streamline/test_streamline_fc.py
index 9ce98066cfbf9d1c64514b957d8a260705fd0d7c..d88bf14913d2551cd7347c5617895998a7d56799 100644
--- a/tests/transformation/streamline/test_streamline_fc.py
+++ b/tests/transformation/streamline/test_streamline_fc.py
@@ -72,7 +72,7 @@ def test_streamline_fc(size, wbits, abits):
     model = model.transform(GiveReadableTensorNames())
     model = model.transform(RemoveStaticGraphInputs())
     # load one of the test vectors
-    raw_i = get_data("finn", "data/onnx/mnist-conv/test_data_set_0/input_0.pb")
+    raw_i = get_data("finn.data", "onnx/mnist-conv/test_data_set_0/input_0.pb")
     input_tensor = onnx.load_tensor_from_string(raw_i)
     # run using FINN-based execution
     input_dict = {"global_in": nph.to_array(input_tensor)}
diff --git a/tests/transformation/test_batchnorm_to_affine.py b/tests/transformation/test_batchnorm_to_affine_bnn_pynq.py
similarity index 96%
rename from tests/transformation/test_batchnorm_to_affine.py
rename to tests/transformation/test_batchnorm_to_affine_bnn_pynq.py
index a3df5ae9bbd3f99bc29bc088a5f461122af06d81..7e894c078b15c16f29dec60d694f8b6892e84a8a 100644
--- a/tests/transformation/test_batchnorm_to_affine.py
+++ b/tests/transformation/test_batchnorm_to_affine_bnn_pynq.py
@@ -51,7 +51,7 @@ def test_batchnorm_to_affine_cnv_w1a1():
     model = ModelWrapper(export_onnx_path)
     model = model.transform(InferShapes())
     model = model.transform(FoldConstants())
-    fn = pk.resource_filename("finn", "data/cifar10/cifar10-test-data-class3.npz")
+    fn = pk.resource_filename("finn.qnn-data", "cifar10/cifar10-test-data-class3.npz")
     input_tensor = np.load(fn)["arr_0"].astype(np.float32)
     input_tensor = input_tensor / 255
     assert input_tensor.shape == (1, 3, 32, 32)
@@ -77,7 +77,7 @@ def test_batchnorm_to_affine_lfc_w1a1():
     model = model.transform(FoldConstants())
     new_model = model.transform(BatchNormToAffine())
     # load one of the test vectors
-    raw_i = get_data("finn", "data/onnx/mnist-conv/test_data_set_0/input_0.pb")
+    raw_i = get_data("finn.data", "onnx/mnist-conv/test_data_set_0/input_0.pb")
     input_tensor = onnx.load_tensor_from_string(raw_i)
     input_dict = {"0": nph.to_array(input_tensor)}
     assert oxe.compare_execution(model, new_model, input_dict)
diff --git a/tests/transformation/test_change_datalayout.py b/tests/transformation/test_change_datalayout.py
deleted file mode 100644
index 66459d574957575e61ec1bec631fb7030a27cca1..0000000000000000000000000000000000000000
--- a/tests/transformation/test_change_datalayout.py
+++ /dev/null
@@ -1,112 +0,0 @@
-# Copyright (c) 2020, Xilinx
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-#
-# * Redistributions of source code must retain the above copyright notice, this
-#   list of conditions and the following disclaimer.
-#
-# * Redistributions in binary form must reproduce the above copyright notice,
-#   this list of conditions and the following disclaimer in the documentation
-#   and/or other materials provided with the distribution.
-#
-# * Neither the name of FINN nor the names of its
-#   contributors may be used to endorse or promote products derived from
-#   this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-import pytest
-from onnx import helper, TensorProto
-
-from finn.custom_op.maxpoolnhwc import compute_pool_output_dim
-from finn.core.modelwrapper import ModelWrapper
-from finn.core.datatype import DataType
-import finn.core.data_layout as DataLayout
-from finn.transformation.change_datalayout import ChangeDataLayoutQuantAvgPool2d
-from finn.transformation.infer_shapes import InferShapes
-from finn.transformation.infer_datatypes import InferDataTypes
-from finn.transformation.infer_data_layouts import InferDataLayouts
-from finn.transformation.general import GiveUniqueNodeNames, GiveReadableTensorNames
-from finn.util.basic import gen_finn_dt_tensor
-from finn.util.basic import get_by_name
-import finn.core.onnx_exec as oxe
-
-# stride
-@pytest.mark.parametrize("s", [1, 2])
-# kernel
-@pytest.mark.parametrize("k", [3, 4])
-# ibits
-@pytest.mark.parametrize("ibits", [4, 8])
-# obits
-@pytest.mark.parametrize("obits", [2, 4])
-# signed
-@pytest.mark.parametrize("signed", [False, True])
-# channels
-@pytest.mark.parametrize("c", [2, 3])
-# input dimension
-@pytest.mark.parametrize("idim", [6, 7])
-def test_change_datalayout_quantavgpool(s, k, ibits, obits, signed, c, idim):
-    n = 1
-    odim = compute_pool_output_dim(idim, k, s)
-    # determine input FINN datatype
-    if signed is True:
-        prefix = "INT"
-    else:
-        prefix = "UINT"
-    dt_name = prefix + str(ibits)
-    dtype = DataType[dt_name]
-
-    inp = helper.make_tensor_value_info("inp", TensorProto.FLOAT, [n, c, idim, idim])
-    outp = helper.make_tensor_value_info("outp", TensorProto.FLOAT, [n, c, odim, odim])
-
-    node = helper.make_node(
-        "QuantAvgPool2d",
-        ["inp"],
-        ["outp"],
-        domain="finn",
-        stride=s,
-        kernel=k,
-        ibits=ibits,
-        obits=obits,
-        signed=signed,
-        data_layout="NCHW",
-    )
-    graph = helper.make_graph(
-        nodes=[node], name="single-quantavgpool", inputs=[inp], outputs=[outp]
-    )
-
-    model = helper.make_model(graph)
-    model = ModelWrapper(model)
-    model = model.transform(InferShapes())
-    model = model.transform(InferDataTypes())
-    model = model.transform(InferDataLayouts())
-    model = model.transform(GiveUniqueNodeNames())
-    model = model.transform(GiveReadableTensorNames())
-    model_transformed = model.transform(ChangeDataLayoutQuantAvgPool2d())
-    model_transformed = model_transformed.transform(InferShapes())
-    model_transformed = model_transformed.transform(InferDataTypes())
-    model_transformed = model_transformed.transform(InferDataLayouts())
-    model_transformed = model_transformed.transform(GiveUniqueNodeNames())
-    model_transformed = model_transformed.transform(GiveReadableTensorNames())
-    inp_values = gen_finn_dt_tensor(dtype, [n, c, idim, idim])
-    idict = {"inp": inp_values}
-    assert oxe.compare_execution(model, model_transformed, idict)
-    assert len(model.graph.node) + 2 == len(model_transformed.graph.node)
-    assert model_transformed.graph.node[-1].op_type == "Transpose"
-    assert model_transformed.graph.node[0].op_type == "Transpose"
-    # check if QuantAvgPool2d node has datalayout set correctly
-    node = model_transformed.graph.node[1]
-    d_layout = get_by_name(node.attribute, "data_layout").s.decode("UTF-8")
-    assert d_layout == "NHWC"
-    assert model_transformed.get_tensor_layout(node.input[0]) == DataLayout.NHWC
-    assert model_transformed.get_tensor_layout(node.output[0]) == DataLayout.NHWC
diff --git a/tests/transformation/test_conv_lowering.py b/tests/transformation/test_conv_lowering.py
deleted file mode 100644
index b6ab634b374dea3ba309bbf12654c73c0a90e36c..0000000000000000000000000000000000000000
--- a/tests/transformation/test_conv_lowering.py
+++ /dev/null
@@ -1,189 +0,0 @@
-# Copyright (c) 2020, Xilinx
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-#
-# * Redistributions of source code must retain the above copyright notice, this
-#   list of conditions and the following disclaimer.
-#
-# * Redistributions in binary form must reproduce the above copyright notice,
-#   this list of conditions and the following disclaimer in the documentation
-#   and/or other materials provided with the distribution.
-#
-# * Neither the name of FINN nor the names of its
-#   contributors may be used to endorse or promote products derived from
-#   this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-import pytest
-import onnx.helper as oh
-from onnx import TensorProto
-import os
-import pkg_resources as pk
-import brevitas.onnx as bo
-import numpy as np
-
-from finn.core.modelwrapper import ModelWrapper
-from finn.core.datatype import DataType
-from finn.transformation.fold_constants import FoldConstants
-from finn.transformation.infer_shapes import InferShapes
-from finn.util.test import get_test_model_trained
-from finn.transformation.lower_convs_to_matmul import LowerConvsToMatMul
-import finn.core.onnx_exec as oxe
-from finn.custom_op.im2col import compute_conv_output_dim
-from finn.util.basic import gen_finn_dt_tensor
-from finn.custom_op.registry import getCustomOp
-
-export_onnx_path = "test_conv_lowering.onnx"
-
-
-def test_conv_lowering_cnv_w1a1():
-    cnv = get_test_model_trained("CNV", 1, 1)
-    bo.export_finn_onnx(cnv, (1, 3, 32, 32), export_onnx_path)
-    model = ModelWrapper(export_onnx_path)
-    model = model.transform(InferShapes())
-    model = model.transform(FoldConstants())
-    fn = pk.resource_filename("finn", "data/cifar10/cifar10-test-data-class3.npz")
-    input_tensor = np.load(fn)["arr_0"].astype(np.float32)
-    input_tensor = input_tensor / 255
-    assert input_tensor.shape == (1, 3, 32, 32)
-    # execute imported model to get expected answer
-    input_dict = {"0": input_tensor}
-    output_dict_e = oxe.execute_onnx(model, input_dict)
-    expected = output_dict_e[list(output_dict_e.keys())[0]]
-    # execute transformed model and compare
-    model = model.transform(LowerConvsToMatMul())
-    output_dict_p = oxe.execute_onnx(model, input_dict)
-    produced = output_dict_p[list(output_dict_p.keys())[0]]
-    assert np.isclose(produced, expected).all()
-    assert np.argmax(produced) == 3
-    os.remove(export_onnx_path)
-
-
-# input datatype
-@pytest.mark.parametrize("idt", [DataType.INT2, DataType.INT4])
-# kernel size
-@pytest.mark.parametrize("k", [2, 4])
-# input dimension
-@pytest.mark.parametrize("ifm_dim", [4, 6])
-# input channels
-@pytest.mark.parametrize("ifm_ch", [2, 3])
-# stride
-@pytest.mark.parametrize("stride", [1, 2])
-# padding
-@pytest.mark.parametrize("padding", [[0, 0, 0, 0], [1, 1, 1, 1]])
-def test_depthwise_conv_lowering(idt, k, ifm_dim, ifm_ch, stride, padding):
-    wdt = idt
-    odt = DataType.INT32
-    ofm_ch = ifm_ch
-    ofm_dim = compute_conv_output_dim(ifm_dim, k, stride, pad=padding[0])
-
-    # set up onnx model
-    inp = oh.make_tensor_value_info(
-        "inp", TensorProto.FLOAT, [1, ifm_ch, ifm_dim, ifm_dim]
-    )
-    outp = oh.make_tensor_value_info(
-        "outp", TensorProto.FLOAT, [1, ofm_ch, ofm_dim, ofm_dim]
-    )
-
-    W = oh.make_tensor_value_info("W", TensorProto.FLOAT, [ofm_ch, 1, k, k])
-
-    dw_cnv = oh.make_node(
-        "Conv",
-        inputs=["inp", "W"],
-        outputs=["outp"],
-        kernel_shape=[k, k],
-        pads=padding,
-        strides=[stride, stride],
-        group=ifm_ch,
-    )
-    graph = oh.make_graph(
-        nodes=[dw_cnv],
-        name="dw_cnv_graph",
-        inputs=[inp],
-        outputs=[outp],
-        value_info=[W],
-    )
-
-    model = oh.make_model(graph, producer_name="dws_cnv-model")
-    model = ModelWrapper(model)
-    model.set_tensor_datatype("inp", idt)
-    model.set_tensor_datatype("outp", odt)
-    model.set_tensor_datatype("W", wdt)
-    w_tensor = gen_finn_dt_tensor(wdt, [ofm_ch, 1, k, k])
-    model.set_initializer("W", w_tensor)
-    model = model.transform(InferShapes())
-
-    input_tensor = gen_finn_dt_tensor(idt, [1, ifm_ch, ifm_dim, ifm_dim])
-    input_dict = {"inp": input_tensor}
-    output_dict = oxe.execute_onnx(model, input_dict)
-    expected = output_dict["outp"]
-
-    model = model.transform(LowerConvsToMatMul())
-    output_dict = oxe.execute_onnx(model, input_dict)
-    produced = output_dict["outp"]
-    assert (produced == expected).all()
-
-    # check if created nodes have attributes that indicate depthwise conv
-    assert model.get_tensor_sparsity("W") is not None
-    im2col_node = getCustomOp(model.graph.node[1])
-    assert im2col_node.get_nodeattr("depthwise") == 1
-
-
-def test_conv_lowering_conv_1x1():
-    np.random.seed(0)
-
-    in_feature_dim = 7
-    in_chn = 3
-    kernel_size = 1
-    out_feature_dim = in_feature_dim
-
-    input_shape = [1, in_chn, in_feature_dim, in_feature_dim]
-    output_shape = [1, in_chn, out_feature_dim, out_feature_dim]
-
-    conv_param_shape = [in_chn, in_chn, kernel_size, kernel_size]
-
-    conv_config = {}
-    conv_config["dilations"] = [1, 1]
-    conv_config["group"] = 1
-    conv_config["kernel_shape"] = [kernel_size, kernel_size]
-    conv_config["pads"] = [0, 0, 0, 0]
-    conv_config["strides"] = [1, 1]
-
-    top_in = oh.make_tensor_value_info("top_in", TensorProto.FLOAT, input_shape)
-    top_out = oh.make_tensor_value_info("top_out", TensorProto.FLOAT, output_shape)
-
-    value_info = [oh.make_tensor_value_info("p1", TensorProto.FLOAT, conv_param_shape)]
-
-    modelproto = oh.make_model(
-        oh.make_graph(
-            name="test",
-            inputs=[top_in],
-            outputs=[top_out],
-            value_info=value_info,
-            nodes=[oh.make_node("Conv", ["top_in", "p1"], ["top_out"], **conv_config)],
-        )
-    )
-    model = ModelWrapper(modelproto)
-    model = model.transform(InferShapes())
-    model.set_initializer("p1", np.random.rand(*conv_param_shape).astype(np.float32))
-
-    new_model = model.transform(LowerConvsToMatMul())
-    inp_dict = {"top_in": np.random.rand(*input_shape).astype(np.float32)}
-
-    assert oxe.compare_execution(model, new_model, inp_dict)
-    assert new_model.graph.node[0].op_type == "Transpose"
-    assert new_model.graph.node[1].op_type == "MatMul"
-    assert new_model.graph.node[2].op_type == "Transpose"
-    assert len(new_model.graph.node) == 3
diff --git a/tests/transformation/test_fold_constants.py b/tests/transformation/test_fold_constants.py
deleted file mode 100644
index a976ffd62bce744a474a6fac2a61a6478526777f..0000000000000000000000000000000000000000
--- a/tests/transformation/test_fold_constants.py
+++ /dev/null
@@ -1,72 +0,0 @@
-# Copyright (c) 2020, Xilinx
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-#
-# * Redistributions of source code must retain the above copyright notice, this
-#   list of conditions and the following disclaimer.
-#
-# * Redistributions in binary form must reproduce the above copyright notice,
-#   this list of conditions and the following disclaimer in the documentation
-#   and/or other materials provided with the distribution.
-#
-# * Neither the name of FINN nor the names of its
-#   contributors may be used to endorse or promote products derived from
-#   this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-import os
-from pkgutil import get_data
-
-import brevitas.onnx as bo
-import numpy as np
-import onnx
-import onnx.numpy_helper as np_helper
-
-import finn.core.onnx_exec as oxe
-from finn.core.modelwrapper import ModelWrapper
-from finn.transformation.fold_constants import FoldConstants
-from finn.transformation.infer_shapes import InferShapes
-from finn.util.test import get_test_model_untrained
-
-export_onnx_path = "test_fold_constants.onnx"
-
-
-def test_const_folding():
-    raw_m = get_data("finn", "data/onnx/mnist-conv/model.onnx")
-    model = ModelWrapper(raw_m)
-    model = model.transform(InferShapes())
-    model = model.transform(FoldConstants())
-    raw_i = get_data("finn", "data/onnx/mnist-conv/test_data_set_0/input_0.pb")
-    raw_o = get_data("finn", "data/onnx/mnist-conv/test_data_set_0/output_0.pb")
-    input_tensor = onnx.load_tensor_from_string(raw_i)
-    output_tensor = onnx.load_tensor_from_string(raw_o)
-    input_dict = {"Input3": np_helper.to_array(input_tensor)}
-    output_dict = oxe.execute_onnx(model, input_dict)
-    assert np.isclose(
-        np_helper.to_array(output_tensor), output_dict["Plus214_Output_0"], atol=1e-3
-    ).all()
-
-
-def test_const_folding_shapes():
-    lfc = get_test_model_untrained("LFC", 1, 1)
-    bo.export_finn_onnx(lfc, (1, 1, 28, 28), export_onnx_path)
-    model = ModelWrapper(export_onnx_path)
-    model = model.transform(InferShapes())
-    model = model.transform(FoldConstants())
-    reshape_node = model.graph.node[0]
-    assert reshape_node.op_type == "Reshape"
-    assert list(model.get_tensor_shape(reshape_node.input[0])) == [1, 1, 28, 28]
-    assert list(model.get_tensor_shape(reshape_node.output[0])) == [1, 784]
-    os.remove(export_onnx_path)
diff --git a/tests/transformation/test_general_transformation.py b/tests/transformation/test_general_transformation.py
deleted file mode 100644
index 153af378eb3e07d5824f114fd194730048fb4953..0000000000000000000000000000000000000000
--- a/tests/transformation/test_general_transformation.py
+++ /dev/null
@@ -1,120 +0,0 @@
-# Copyright (c) 2020, Xilinx
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-#
-# * Redistributions of source code must retain the above copyright notice, this
-#   list of conditions and the following disclaimer.
-#
-# * Redistributions in binary form must reproduce the above copyright notice,
-#   this list of conditions and the following disclaimer in the documentation
-#   and/or other materials provided with the distribution.
-#
-# * Neither the name of FINN nor the names of its
-#   contributors may be used to endorse or promote products derived from
-#   this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-from pkgutil import get_data
-
-from finn.core.modelwrapper import ModelWrapper
-from finn.transformation.general import GiveUniqueNodeNames
-
-import numpy as np
-import onnx
-import finn.core.onnx_exec as oxe
-from finn.transformation.infer_shapes import InferShapes
-from finn.transformation.general import GiveUniqueParameterTensors
-
-
-def test_give_unique_node_names():
-    raw_m = get_data("finn", "data/onnx/mnist-conv/model.onnx")
-    model = ModelWrapper(raw_m)
-    model = model.transform(GiveUniqueNodeNames())
-    assert model.graph.node[0].name == "Reshape_0"
-    assert model.graph.node[1].name == "Conv_0"
-    assert model.graph.node[11].name == "Add_2"
-
-
-def test_give_unique_parameter_tensors():
-
-    # Create model
-    input_shape = [4, 4]
-    in1 = onnx.helper.make_tensor_value_info("in1", onnx.TensorProto.FLOAT, input_shape)
-    out1 = onnx.helper.make_tensor_value_info(
-        "out1", onnx.TensorProto.FLOAT, input_shape
-    )
-
-    graph_nodes = []
-    graph_nodes += [
-        onnx.helper.make_node("Add", inputs=["in1", "param1"], outputs=["t1"])
-    ]
-
-    graph_nodes += [
-        onnx.helper.make_node("Sum", inputs=["t1", "param1", "param1"], outputs=["t2"])
-    ]
-
-    graph_nodes += [
-        onnx.helper.make_node("Sum", inputs=["t2", "param2", "param1"], outputs=["t3"])
-    ]
-
-    graph_nodes += [
-        onnx.helper.make_node("Add", inputs=["t3", "param1"], outputs=["out1"])
-    ]
-
-    onnx_graph = onnx.helper.make_graph(
-        nodes=graph_nodes, name="simple_graph", inputs=[in1], outputs=[out1],
-    )
-
-    onnx_model = onnx.helper.make_model(onnx_graph, producer_name="simple-model")
-    model = ModelWrapper(onnx_model)
-
-    # Set param values
-    np.random.seed(0)
-    param1 = np.random.rand(*input_shape).astype(np.float32)
-    param2 = np.random.rand(*input_shape).astype(np.float32)
-    model.set_initializer("param1", param1)
-    model.set_initializer("param2", param2)
-    model = model.transform(InferShapes())
-
-    # Apply transformation
-    new_model = model.transform(GiveUniqueParameterTensors())
-    new_model = new_model.transform(InferShapes())
-
-    # Test
-    # Breaks the model?
-    input_tensor = np.random.rand(*input_shape).astype(np.float32)
-    input_dict = {"in1": input_tensor}
-
-    # run original
-    expected_context = oxe.execute_onnx(model, input_dict)
-    expected_output = expected_context[model.graph.output[0].name]
-
-    # run modified
-    produced_context = oxe.execute_onnx(new_model, input_dict)
-    produced_output = produced_context[new_model.graph.output[0].name]
-
-    assert np.isclose(
-        expected_output, produced_output, atol=1e-8
-    ).all(), " GiveUniqueParameterTensors() transform breaks the model"
-
-    # Does the job?
-    param_set = set()
-    param_cnt = 0
-    for n in new_model.graph.node:
-        for i in range(1, len(n.input)):
-            param_set |= {n.input[i]}
-            param_cnt += 1
-
-    assert len(param_set) == param_cnt, " There are still parameters reused"
diff --git a/tests/transformation/test_infer_data_layouts.py b/tests/transformation/test_infer_data_layouts_cnv.py
similarity index 99%
rename from tests/transformation/test_infer_data_layouts.py
rename to tests/transformation/test_infer_data_layouts_cnv.py
index 0bc30ea0eb48087606545c86e705328217b004ca..a8ba81dff608994b8e5efb33ec23bd0e3f894175 100644
--- a/tests/transformation/test_infer_data_layouts.py
+++ b/tests/transformation/test_infer_data_layouts_cnv.py
@@ -46,7 +46,7 @@ import finn.core.data_layout as DataLayout
 export_onnx_path_cnv = "test_infer_data_layouts.onnx"
 
 
-def test_infer_data_layouts():
+def test_infer_data_layouts_cnv():
     cnv = get_test_model_trained("CNV", 1, 1)
     bo.export_finn_onnx(cnv, (1, 3, 32, 32), export_onnx_path_cnv)
     model = ModelWrapper(export_onnx_path_cnv)
diff --git a/tests/transformation/test_infer_datatypes.py b/tests/transformation/test_infer_datatypes_lfc.py
similarity index 98%
rename from tests/transformation/test_infer_datatypes.py
rename to tests/transformation/test_infer_datatypes_lfc.py
index 097ae03f6153843fbb7956a72b38431559d5d0f1..0802c50c7d15a649182529a4e6897b9bbe273336 100644
--- a/tests/transformation/test_infer_datatypes.py
+++ b/tests/transformation/test_infer_datatypes_lfc.py
@@ -41,7 +41,7 @@ from finn.util.test import get_test_model_trained
 export_onnx_path = "test_infer_datatypes.onnx"
 
 
-def test_infer_datatypes():
+def test_infer_datatypes_lfc():
     lfc = get_test_model_trained("LFC", 1, 1)
     bo.export_finn_onnx(lfc, (1, 1, 28, 28), export_onnx_path)
     model = ModelWrapper(export_onnx_path)
diff --git a/tests/transformation/test_infer_shapes.py b/tests/transformation/test_infer_shapes.py
deleted file mode 100644
index a6ebe540bb5e081178704ec0493d511277562acb..0000000000000000000000000000000000000000
--- a/tests/transformation/test_infer_shapes.py
+++ /dev/null
@@ -1,89 +0,0 @@
-# Copyright (c) 2020, Xilinx
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-#
-# * Redistributions of source code must retain the above copyright notice, this
-#   list of conditions and the following disclaimer.
-#
-# * Redistributions in binary form must reproduce the above copyright notice,
-#   this list of conditions and the following disclaimer in the documentation
-#   and/or other materials provided with the distribution.
-#
-# * Neither the name of FINN nor the names of its
-#   contributors may be used to endorse or promote products derived from
-#   this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-from pkgutil import get_data
-
-import numpy as np
-from onnx import TensorProto, helper
-
-import finn.util.basic as util
-from finn.core.modelwrapper import ModelWrapper
-from finn.transformation.infer_shapes import InferShapes
-
-
-def test_infer_shapes():
-    # load the onnx model
-    raw_m = get_data("finn", "data/onnx/mnist-conv/model.onnx")
-    model = ModelWrapper(raw_m)
-    graph = model.graph
-
-    # multi-thresholding node to be inserted between the first Relu and MaxPool node
-
-    # get Relu node to use data
-    Relu_node = graph.node[3]
-    assert Relu_node.op_type == "Relu", "The wrong model was chosen for the check"
-
-    # create thresholds tensor as constant
-    mt_thresh0 = helper.make_tensor_value_info("mt_thresh0", TensorProto.FLOAT, [8, 7])
-
-    # random numbers for the thresholds
-    # thresholds for one channel have to be sorted to guarantee the correct behavior
-    mt_thresh0_values = np.empty([8, 7], dtype=np.float32)
-    for i in range(len(mt_thresh0_values)):
-        mt_thresh0_values[i] = np.sort(np.random.random_sample(7) * 10)
-
-    model.set_initializer(mt_thresh0.name, mt_thresh0_values)
-
-    # add multi-thresholding node and change Relu node
-    mt_node = helper.make_node(
-        "MultiThreshold", ["mt_v0", "mt_thresh0"], [Relu_node.output[0]], domain="finn"
-    )
-    Relu_node.output[0] = "mt_v0"
-
-    # explicitly remove any present shape from ReLU and MultiThreshold outputs
-    util.remove_by_name(model.graph.value_info, Relu_node.output[0])
-    util.remove_by_name(model.graph.value_info, mt_node.output[0])
-    graph.node.insert(4, mt_node)
-
-    # first check routine
-    # check if at least one shape is not specified
-    assert not (
-        model.check_all_tensor_shapes_specified()
-    ), "All tensors are already specified before the shape inference execution"
-
-    # perform shape inference on mixed model
-    model = model.transform(InferShapes())
-
-    # second check routine
-    # now all shapes should be specified and mt_node output shape is (1,8,28,28)
-    assert (
-        model.check_all_tensor_shapes_specified()
-    ), "There are still tensors that are not specified"
-    assert (model.get_tensor_shape(mt_node.output[0])) == (
-        [1, 8, 28, 28]
-    ), "output of multi-thresholding node has wrong shape"
diff --git a/tests/transformation/test_merge_onnx_models.py b/tests/transformation/test_merge_onnx_models.py
deleted file mode 100644
index db7c990baddfb50a39603937a9c5b73f512a0e59..0000000000000000000000000000000000000000
--- a/tests/transformation/test_merge_onnx_models.py
+++ /dev/null
@@ -1,126 +0,0 @@
-# Copyright (c) 2020, Xilinx
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-#
-# * Redistributions of source code must retain the above copyright notice, this
-#   list of conditions and the following disclaimer.
-#
-# * Redistributions in binary form must reproduce the above copyright notice,
-#   this list of conditions and the following disclaimer in the documentation
-#   and/or other materials provided with the distribution.
-#
-# * Neither the name of FINN nor the names of its
-#   contributors may be used to endorse or promote products derived from
-#   this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-from pkgutil import get_data
-
-import numpy as np
-import onnx
-import onnx.numpy_helper as np_helper
-from onnx import TensorProto, helper
-
-from finn.core.modelwrapper import ModelWrapper
-from finn.core.datatype import DataType
-from finn.transformation.infer_shapes import InferShapes
-from finn.transformation.infer_datatypes import InferDataTypes
-from finn.transformation.infer_data_layouts import InferDataLayouts
-from finn.transformation.general import GiveReadableTensorNames, GiveUniqueNodeNames
-from finn.transformation.merge_onnx_models import MergeONNXModels
-import finn.core.onnx_exec as oxe
-
-
-def test_merge_onnx_models():
-    # load pre model
-    raw_m = get_data("finn", "data/onnx/mnist-conv/model.onnx")
-    model1 = ModelWrapper(raw_m)
-    # the input for model1 comes from a uint8 vector so we set the finn datatype
-    # of the input tensor to DataType.UINT8 to verify that the datatypes are correctly
-    # preserved in the transformed model
-    model1.set_tensor_datatype(model1.graph.input[0].name, DataType.UINT8)
-    model1 = model1.transform(InferShapes())
-    model1 = model1.transform(GiveUniqueNodeNames())
-    model1 = model1.transform(GiveReadableTensorNames())
-
-    # set up post model
-    shape = [1, 10]
-    inp = helper.make_tensor_value_info("inp", TensorProto.FLOAT, shape)
-    a0 = helper.make_tensor_value_info("a0", TensorProto.FLOAT, [])
-    a1 = helper.make_tensor_value_info("a1", TensorProto.FLOAT, [])
-    outp = helper.make_tensor_value_info("outp", TensorProto.FLOAT, shape)
-
-    mul_node = helper.make_node("Mul", ["inp", "a0"], ["mul_out"])
-    div_node = helper.make_node("Div", ["mul_out", "a1"], ["outp"])
-
-    graph = helper.make_graph(
-        nodes=[mul_node, div_node],
-        name="model2-graph",
-        inputs=[inp],
-        outputs=[outp],
-        value_info=[a0, a1],
-    )
-
-    model2 = helper.make_model(graph, producer_name="model2")
-    model2 = ModelWrapper(model2)
-    # initialize model2
-    a0_value = np.random.uniform(low=0, high=1, size=(1)).astype(np.float32)
-    model2.set_initializer("a0", a0_value)
-    a1_value = np.random.uniform(low=0.1, high=1, size=(1)).astype(np.float32)
-    model2.set_initializer("a1", a1_value)
-    # set a dummy sparsity annotation to check if it gets correctly transferred
-    # to the merged model
-    sparsity = {"dw": {"kernel_shape": 0}}
-    model2.set_tensor_sparsity("a1", sparsity)
-    model2 = model2.transform(InferShapes())
-    model2 = model2.transform(InferDataTypes())
-    model2 = model2.transform(InferDataLayouts())
-    model2 = model2.transform(GiveUniqueNodeNames())
-    model2 = model2.transform(GiveReadableTensorNames())
-
-    # simulate the models before the merging and pass the output of model1 to model2
-    # load one of the test vectors
-    raw_i = get_data("finn", "data/onnx/mnist-conv/test_data_set_0/input_0.pb")
-    inp_values = onnx.load_tensor_from_string(raw_i)
-    inp_values = np_helper.to_array(inp_values)
-    idict = {model1.graph.input[0].name: inp_values}
-    odict = oxe.execute_onnx(model1, idict)
-    temp = odict[model1.graph.output[0].name]
-
-    idict = {model2.graph.input[0].name: temp}
-    odict = oxe.execute_onnx(model2, idict)
-    outp = odict[model2.graph.output[0].name]
-    # merge models
-    model_transformed = model2.transform(MergeONNXModels(model1))
-
-    idict = {model_transformed.graph.input[0].name: inp_values}
-    odict = oxe.execute_onnx(model_transformed, idict)
-    outp_transformed = odict[model_transformed.graph.output[0].name]
-
-    assert (outp == outp_transformed).all()
-    assert len(model_transformed.graph.node) == len(model1.graph.node) + len(
-        model2.graph.node
-    )
-    # to test if the value is preserved we set the sparsity annotation of input[1]
-    # of the division block to a dummy value, we can now look for the division block
-    # and check if the sparsity annotation is still the same
-    for n in model_transformed.graph.node:
-        if n.op_type == "Div":
-            tensor_name = n.input[1]
-            set_sparsity = model_transformed.get_tensor_sparsity(tensor_name)
-            assert sparsity == set_sparsity
-
-    # check if finn datatype of graph.input[0] is still set to UINT8
-    assert model_transformed.get_tensor_datatype("global_in") == DataType.UINT8
diff --git a/tests/transformation/test_renaming.py b/tests/transformation/test_renaming.py
deleted file mode 100644
index db8b8410e27f881b15abc426537b153348f38206..0000000000000000000000000000000000000000
--- a/tests/transformation/test_renaming.py
+++ /dev/null
@@ -1,75 +0,0 @@
-# Copyright (c) 2020, Xilinx
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-#
-# * Redistributions of source code must retain the above copyright notice, this
-#   list of conditions and the following disclaimer.
-#
-# * Redistributions in binary form must reproduce the above copyright notice,
-#   this list of conditions and the following disclaimer in the documentation
-#   and/or other materials provided with the distribution.
-#
-# * Neither the name of FINN nor the names of its
-#   contributors may be used to endorse or promote products derived from
-#   this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-from pkgutil import get_data
-
-import numpy as np
-import onnx
-import onnx.numpy_helper as np_helper
-
-import finn.core.onnx_exec as oxe
-from finn.core.modelwrapper import ModelWrapper
-from finn.transformation.general import GiveReadableTensorNames, GiveUniqueNodeNames
-from finn.transformation.infer_shapes import InferShapes
-
-
-def test_renaming():
-    # load the onnx model
-    raw_m = get_data("finn", "data/onnx/mnist-conv/model.onnx")
-    model = ModelWrapper(raw_m)
-    model = model.transform(InferShapes())
-    model = model.transform(GiveUniqueNodeNames())
-    model = model.transform(GiveReadableTensorNames())
-    # do some basic checks
-    assert model.graph.input[0].name == "global_in"
-    assert model.graph.output[0].name == "global_out"
-    assert model.graph.node[1].op_type == "Conv"
-    assert model.graph.node[1].name == "Conv_0"
-    assert model.graph.node[1].input[1] == "Conv_0_param0"
-    assert model.graph.node[6].op_type == "Add"
-    assert model.graph.node[6].name == "Add_1"
-    assert model.graph.node[6].input[1] == "Add_1_param0"
-    # ensure running renaming twice still yields the same names
-    model = model.transform(GiveUniqueNodeNames())
-    model = model.transform(GiveReadableTensorNames())
-    assert model.graph.node[1].op_type == "Conv"
-    assert model.graph.node[1].name == "Conv_0"
-    assert model.graph.node[1].input[1] == "Conv_0_param0"
-    assert model.graph.node[6].op_type == "Add"
-    assert model.graph.node[6].name == "Add_1"
-    assert model.graph.node[6].input[1] == "Add_1_param0"
-    # run renamed model to make sure we did not mess up the topology
-    raw_i = get_data("finn", "data/onnx/mnist-conv/test_data_set_0/input_0.pb")
-    raw_o = get_data("finn", "data/onnx/mnist-conv/test_data_set_0/output_0.pb")
-    input_tensor = onnx.load_tensor_from_string(raw_i)
-    output_tensor = onnx.load_tensor_from_string(raw_o)
-    input_dict = {"global_in": np_helper.to_array(input_tensor)}
-    output_dict = oxe.execute_onnx(model, input_dict)
-    assert np.isclose(
-        np_helper.to_array(output_tensor), output_dict["global_out"], atol=1e-3
-    ).all()
diff --git a/tests/transformation/test_sort_graph.py b/tests/transformation/test_sort_graph.py
deleted file mode 100644
index 05842504c13b144bb34e8084fb12b5086fa84115..0000000000000000000000000000000000000000
--- a/tests/transformation/test_sort_graph.py
+++ /dev/null
@@ -1,150 +0,0 @@
-from onnx import TensorProto, helper
-import numpy as np
-
-from finn.core.modelwrapper import ModelWrapper
-from finn.transformation.general import SortGraph
-from finn.transformation.infer_shapes import InferShapes
-import pytest
-import finn.analysis.topology as ta
-
-
-def make_randomly_sorted_linear_model(num_of_nodes, seed=None):
-    if seed is not None:
-        np.random.seed(seed)
-
-    ch = 2
-    ifmdim = 16
-    input_shape = (1, ch, ifmdim, ifmdim)
-
-    top_in = helper.make_tensor_value_info("t0", TensorProto.FLOAT, input_shape)
-    top_out = helper.make_tensor_value_info(
-        "t" + str(num_of_nodes), TensorProto.FLOAT, input_shape
-    )
-
-    value_info = []
-    nodes = []
-    for i in range(num_of_nodes):
-        nodes += [
-            helper.make_node("Add", ["t" + str(i), "p" + str(i)], ["t" + str(i + 1)])
-        ]
-        value_info += [
-            helper.make_tensor_value_info("p" + str(i), TensorProto.FLOAT, input_shape)
-        ]
-
-    nodes = np.random.permutation(nodes)
-
-    modelproto = helper.make_model(
-        helper.make_graph(
-            name="test",
-            inputs=[top_in],
-            outputs=[top_out],
-            value_info=value_info,
-            nodes=nodes,
-        )
-    )
-    model = ModelWrapper(modelproto)
-    model = model.transform(InferShapes())
-
-    for i in range(num_of_nodes):
-        model.set_initializer(
-            "p" + str(i), np.random.rand(*input_shape).astype(np.float32)
-        )
-
-    return model
-
-
-@pytest.mark.parametrize("num_of_nodes", [64])
-def test_sort_linear_graph(num_of_nodes):
-    model = make_randomly_sorted_linear_model(num_of_nodes, seed=0)
-    new_model = model.transform(SortGraph())
-
-    # Test
-    ret = new_model.analysis(ta.nodes_topologically_sorted)
-    assert ret["nodes_topologically_sorted"], "Nodes are not topologically sorted."
-
-
-def test_sort_nonlinear_graph():
-    ch = 2
-    ifmdim = 16
-    input_shape = (1, ch, ifmdim, ifmdim)
-
-    top_in = helper.make_tensor_value_info("top_in", TensorProto.FLOAT, input_shape)
-    top_out = helper.make_tensor_value_info("top_out", TensorProto.FLOAT, input_shape)
-
-    num_of_params = 8
-    value_info = []
-    for i in range(num_of_params):
-        value_info += [
-            helper.make_tensor_value_info("p" + str(i), TensorProto.FLOAT, input_shape)
-        ]
-
-    modelproto = helper.make_model(
-        helper.make_graph(
-            name="test",
-            inputs=[top_in],
-            outputs=[top_out],
-            value_info=value_info,
-            nodes=[
-                # Not sorted nodes
-                helper.make_node("Mul", ["fork1", "p2"], ["t3"]),
-                helper.make_node("Add", ["t4", "p3"], ["t5"]),
-                helper.make_node("Add", ["t2", "t3"], ["t4"]),
-                helper.make_node("Add", ["t6", "t7"], ["t8"]),
-                helper.make_node("Add", ["fork3", "fork3"], ["top_out"]),
-                helper.make_node("Mul", ["t5", "p4"], ["fork2"]),
-                helper.make_node("Add", ["top_in", "p0"], ["fork1"]),
-                helper.make_node("Mul", ["fork1", "p1"], ["t2"]),
-                helper.make_node("Add", ["fork2", "p5"], ["t6"]),
-                helper.make_node("Add", ["fork2", "p6"], ["t7"]),
-                helper.make_node("Mul", ["t8", "p7"], ["fork3"]),
-            ],
-        )
-    )
-    model = ModelWrapper(modelproto)
-    model = model.transform(InferShapes())
-
-    np.random.seed(0)
-    for i in range(num_of_params):
-        model.set_initializer(
-            "p" + str(i), np.random.rand(*input_shape).astype(np.float32)
-        )
-
-    new_model = model.transform(SortGraph())
-
-    # Test
-    ret = new_model.analysis(ta.nodes_topologically_sorted)
-    assert ret["nodes_topologically_sorted"], "Nodes are not topologically sorted."
-
-
-if __name__ == "__main__":
-    import time
-
-    sizes = [10, 50, 100, 500, 1000]
-    times = []
-    reps = 10
-
-    print("SortGraph performance test:")
-    print("Test sizes", sizes)
-    print("Repetitions per size:", reps)
-    for sz in sizes:
-        acc_time = 0
-        print(" Testing size ", sz)
-        for i in range(reps):
-            # it should take the same time even with the sorted one
-            # but better new model each time as it is a more general approach
-            model = make_randomly_sorted_linear_model(sz)  # new model as seed is None
-            bef = time.time()
-            new_model = model.transform(SortGraph(), make_deepcopy=False)
-            acc_time += time.time() - bef
-
-        times += [acc_time / reps]
-
-    # print csv
-    print("\nnum_of_nodes,  seconds")
-    for sz, tm in zip(sizes, times):
-        print("{:12d}, {:6.4e}".format(sz, tm))
-
-    # plot
-    # import matplotlib.pyplot as plt
-    # plt.plot(sizes,times,"--o")
-    # plt.grid(True)
diff --git a/tests/transformation/test_topk_insert.py b/tests/transformation/test_topk_insert.py
deleted file mode 100644
index a9faac4df0caf973d9aae6430e007eac349a7c43..0000000000000000000000000000000000000000
--- a/tests/transformation/test_topk_insert.py
+++ /dev/null
@@ -1,60 +0,0 @@
-import os
-import onnx
-from finn.util.test import get_test_model_trained
-import brevitas.onnx as bo
-import numpy as np
-import onnx.numpy_helper as nph
-import torch
-
-from finn.core.modelwrapper import ModelWrapper
-from finn.transformation.general import GiveReadableTensorNames, GiveUniqueNodeNames
-from finn.transformation.infer_shapes import InferShapes
-from finn.transformation.infer_datatypes import InferDataTypes
-from finn.transformation.fold_constants import FoldConstants
-from finn.transformation.insert_topk import InsertTopK
-
-import finn.core.onnx_exec as oxe
-from pkgutil import get_data
-
-import pytest
-
-export_onnx_path = "test_topk_insert.onnx"
-
-
-@pytest.mark.parametrize("k", [1, 2])
-def test_topk_insert(k):
-    tfc = get_test_model_trained("TFC", 1, 1)
-    bo.export_finn_onnx(tfc, (1, 1, 28, 28), export_onnx_path)
-    model = ModelWrapper(export_onnx_path)
-
-    # do transformations (no topk)
-    model = model.transform(InferShapes())
-    model = model.transform(FoldConstants())
-    model = model.transform(GiveUniqueNodeNames())
-    model = model.transform(GiveReadableTensorNames())
-    model = model.transform(InferDataTypes())
-
-    # verification: generate random input, run through net, streamline,
-    # run again, check that output is top-k
-    raw_i = get_data("finn", "data/onnx/mnist-conv/test_data_set_0/input_0.pb")
-    input_tensor = onnx.load_tensor_from_string(raw_i)
-    input_brevitas = torch.from_numpy(nph.to_array(input_tensor)).float()
-    output_golden = tfc.forward(input_brevitas).detach().numpy()
-    output_golden_topk = np.flip(output_golden.flatten().argsort())[:k]
-    output_golden_topk = output_golden_topk.flatten()
-
-    input_dict = {"global_in": nph.to_array(input_tensor)}
-
-    # insert top-k
-    model = model.transform(InsertTopK(k))
-    model = model.transform(GiveUniqueNodeNames())
-    model = model.transform(GiveReadableTensorNames())
-    model = model.transform(InferShapes())
-
-    # verify output of top-k
-    output_dict_topk = oxe.execute_onnx(model, input_dict)
-    output_pysim_topk = output_dict_topk[list(output_dict_topk.keys())[0]]
-    output_pysim_topk = output_pysim_topk.astype(np.int).flatten()
-
-    assert np.array_equal(output_golden_topk, output_pysim_topk)
-    os.remove(export_onnx_path)
diff --git a/tests/util/test_create.py b/tests/util/test_create.py
deleted file mode 100644
index 4e236978592b02e1c18b03aba56ff8b2369311a6..0000000000000000000000000000000000000000
--- a/tests/util/test_create.py
+++ /dev/null
@@ -1,64 +0,0 @@
-# Copyright (c) 2020, Xilinx
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-#
-# * Redistributions of source code must retain the above copyright notice, this
-#   list of conditions and the following disclaimer.
-#
-# * Redistributions in binary form must reproduce the above copyright notice,
-#   this list of conditions and the following disclaimer in the documentation
-#   and/or other materials provided with the distribution.
-#
-# * Neither the name of FINN nor the names of its
-#   contributors may be used to endorse or promote products derived from
-#   this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-import pytest
-import finn.util.create as create
-from finn.core.datatype import DataType
-
-
-@pytest.mark.parametrize("bitwidth", [DataType.BIPOLAR, DataType.INT2, DataType.INT4])
-def test_hls_random_mlp_maker(bitwidth):
-    w = bitwidth
-    a = bitwidth
-    layer_spec = [
-        {
-            "mw": 185,
-            "mh": 100,
-            "simd": 185,
-            "pe": 100,
-            "idt": DataType.BIPOLAR,
-            "wdt": w,
-            "act": a,
-        },
-        {"mw": 100, "mh": 100, "simd": 100, "pe": 100, "idt": a, "wdt": w, "act": a},
-        {"mw": 100, "mh": 100, "simd": 100, "pe": 100, "idt": a, "wdt": w, "act": a},
-        {"mw": 100, "mh": 100, "simd": 100, "pe": 100, "idt": a, "wdt": w, "act": a},
-        {
-            "mw": 100,
-            "mh": 1,
-            "simd": 100,
-            "pe": 1,
-            "idt": a,
-            "wdt": w,
-            "act": DataType.BIPOLAR,
-        },
-    ]
-
-    ret = create.hls_random_mlp_maker(layer_spec)
-    assert len(ret.graph.node) == 5
-    # ret.save("mlp-%s.onnx" % str(bitwidth))
diff --git a/tests/util/test_data_packing.py b/tests/util/test_data_packing_hls.py
similarity index 56%
rename from tests/util/test_data_packing.py
rename to tests/util/test_data_packing_hls.py
index 7b77c4be20c1f41c11b53a9b65b79441c9bbbe47..a926bc4068831a552ccfb728511ddda4a8670ca8 100644
--- a/tests/util/test_data_packing.py
+++ b/tests/util/test_data_packing_hls.py
@@ -36,13 +36,7 @@ import numpy as np
 
 import finn.util.basic as cutil
 from finn.core.datatype import DataType
-from finn.util.data_packing import (
-    array2hexstring,
-    finnpy_to_packed_bytearray,
-    numpy_to_hls_code,
-    pack_innermost_dim_as_hex_string,
-    packed_bytearray_to_finnpy,
-)
+from finn.util.data_packing import numpy_to_hls_code
 
 
 @pytest.mark.parametrize("dtype", [DataType.BINARY, DataType.INT2, DataType.INT32])
@@ -95,7 +89,7 @@ def test_npy2apintstream(test_shape, dtype):
         f.write("\n".join(test_app_string))
     cmd_compile = """
 g++ -o test_npy2apintstream test.cpp /workspace/cnpy/cnpy.cpp \
--I/workspace/cnpy/ -I{}/include -I/workspace/finn/src/finn/data/cpp \
+-I/workspace/cnpy/ -I{}/include -I/workspace/finn/src/finn/qnn-data/cpp \
 --std=c++11 -lz""".format(
         os.environ["VIVADO_PATH"]
     )
@@ -121,34 +115,6 @@ g++ -o test_npy2apintstream test.cpp /workspace/cnpy/cnpy.cpp \
     assert success
 
 
-def test_array2hexstring():
-    assert array2hexstring([1, 1, 1, 0], DataType.BINARY, 4) == "0xe"
-    assert array2hexstring([1, 1, 1, 0], DataType.BINARY, 8) == "0x0e"
-    assert array2hexstring([1, 1, 1, -1], DataType.BIPOLAR, 8) == "0x0e"
-    assert array2hexstring([3, 3, 3, 3], DataType.UINT2, 8) == "0xff"
-    assert array2hexstring([1, 3, 3, 1], DataType.UINT2, 8) == "0x7d"
-    assert array2hexstring([1, -1, 1, -1], DataType.INT2, 8) == "0x77"
-    assert array2hexstring([1, 1, 1, -1], DataType.INT4, 16) == "0x111f"
-    assert array2hexstring([-1], DataType.FLOAT32, 32) == "0xbf800000"
-    assert array2hexstring([17.125], DataType.FLOAT32, 32) == "0x41890000"
-    assert array2hexstring([1, 1, 0, 1], DataType.BINARY, 4, reverse=True) == "0xb"
-    assert array2hexstring([1, 1, 1, 0], DataType.BINARY, 8, reverse=True) == "0x07"
-
-
-def test_pack_innermost_dim_as_hex_string():
-    A = [[1, 1, 1, 0], [0, 1, 1, 0]]
-    eA = np.asarray(["0x0e", "0x06"])
-    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([["0x0f", "0x0f"], ["0x07", "0x0d"]])
-    assert (pack_innermost_dim_as_hex_string(B, DataType.UINT2, 8) == eB).all()
-    C = [[[3, 3], [3, 3]], [[1, 3], [3, 1]]]
-    eC = np.asarray([["0x0f", "0x0f"], ["0x0d", "0x07"]])
-    assert (
-        pack_innermost_dim_as_hex_string(C, DataType.UINT2, 8, reverse_inner=True) == eC
-    ).all()
-
-
 def test_numpy_to_hls_code():
     def remove_all_whitespace(s):
         return "".join(s.split())
@@ -168,68 +134,3 @@ def test_numpy_to_hls_code():
     eB = """{{ap_uint<4>("0xf", 16), ap_uint<4>("0xf", 16)},
      {ap_uint<4>("0x7", 16), ap_uint<4>("0xd", 16)}};"""
     assert remove_all_whitespace(ret) == remove_all_whitespace(eB)
-
-
-def test_finnpy_to_packed_bytearray():
-    A = [[1, 1, 1, 0], [0, 1, 1, 0]]
-    eA = np.asarray([[14], [6]], dtype=np.uint8)
-    assert (finnpy_to_packed_bytearray(A, DataType.BINARY) == eA).all()
-    B = [[[3, 3], [3, 3]], [[1, 3], [3, 1]]]
-    eB = np.asarray([[[15], [15]], [[7], [13]]], dtype=np.uint8)
-    assert (finnpy_to_packed_bytearray(B, DataType.UINT2) == eB).all()
-    C = [1, 7, 2, 5]
-    eC = np.asarray([23, 37], dtype=np.uint8)
-    assert (finnpy_to_packed_bytearray(C, DataType.UINT4) == eC).all()
-    D = [[1, 7, 2, 5], [2, 5, 1, 7]]
-    eD = np.asarray([[23, 37], [37, 23]], dtype=np.uint8)
-    assert (finnpy_to_packed_bytearray(D, DataType.UINT4) == eD).all()
-    E = [[-4, 0, -4, -4]]
-    eE = np.asarray(
-        [[255, 255, 255, 252, 0, 0, 0, 0, 255, 255, 255, 252, 255, 255, 255, 252]],
-        dtype=np.uint8,
-    )
-    assert (finnpy_to_packed_bytearray(E, DataType.INT32) == eE).all()
-
-
-def test_packed_bytearray_to_finnpy():
-    A = np.asarray([[14], [6]], dtype=np.uint8)
-    eA = [[1, 1, 1, 0], [0, 1, 1, 0]]
-    eA = np.asarray(eA, dtype=np.float32)
-    shapeA = eA.shape
-    assert (packed_bytearray_to_finnpy(A, DataType.BINARY, shapeA) == eA).all()
-    B = np.asarray([[[15], [15]], [[7], [13]]], dtype=np.uint8)
-    eB = [[[3, 3], [3, 3]], [[1, 3], [3, 1]]]
-    eB = np.asarray(eB, dtype=np.float32)
-    shapeB = eB.shape
-    assert (packed_bytearray_to_finnpy(B, DataType.UINT2, shapeB) == eB).all()
-    C = np.asarray([23, 37], dtype=np.uint8)
-    eC = [1, 7, 2, 5]
-    eC = np.asarray(eC, dtype=np.float32)
-    shapeC = eC.shape
-    assert (packed_bytearray_to_finnpy(C, DataType.UINT4, shapeC) == eC).all()
-    D = np.asarray([[23, 37], [37, 23]], dtype=np.uint8)
-    eD = [[1, 7, 2, 5], [2, 5, 1, 7]]
-    eD = np.asarray(eD, dtype=np.float32)
-    shapeD = eD.shape
-    assert (packed_bytearray_to_finnpy(D, DataType.UINT4, shapeD) == eD).all()
-    E = np.asarray(
-        [[255, 255, 255, 252, 0, 0, 0, 0, 255, 255, 255, 252, 255, 255, 255, 252]],
-        dtype=np.uint8,
-    )
-    eE = [[-4, 0, -4, -4]]
-    eE = np.asarray(eE, dtype=np.float32)
-    shapeE = eE.shape
-    assert (packed_bytearray_to_finnpy(E, DataType.INT32, shapeE) == eE).all()
-    F = np.asarray(
-        [[252, 255, 255, 255, 0, 0, 0, 0, 252, 255, 255, 255, 252, 255, 255, 255]],
-        dtype=np.uint8,
-    )
-    eF = [[-4, 0, -4, -4]]
-    eF = np.asarray(eE, dtype=np.float32)
-    shapeF = eF.shape
-    assert (
-        packed_bytearray_to_finnpy(
-            F, DataType.INT32, shapeF, reverse_inner=True, reverse_endian=True
-        )
-        == eF
-    ).all()
diff --git a/tests/util/test_gen_finn_dt_tensor.py b/tests/util/test_gen_finn_dt_tensor.py
deleted file mode 100644
index f9944e7f5283725d4c7c3b70ead899c9e1d4ea49..0000000000000000000000000000000000000000
--- a/tests/util/test_gen_finn_dt_tensor.py
+++ /dev/null
@@ -1,105 +0,0 @@
-# Copyright (c) 2020, Xilinx
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-#
-# * Redistributions of source code must retain the above copyright notice, this
-#   list of conditions and the following disclaimer.
-#
-# * Redistributions in binary form must reproduce the above copyright notice,
-#   this list of conditions and the following disclaimer in the documentation
-#   and/or other materials provided with the distribution.
-#
-# * Neither the name of FINN nor the names of its
-#   contributors may be used to endorse or promote products derived from
-#   this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-import finn.util.basic as util
-from finn.core.datatype import DataType
-
-
-def test_finn_tensor_generator():
-    # bipolar
-    shape_bp = [2, 2]
-    dt_bp = DataType.BIPOLAR
-    tensor_bp = util.gen_finn_dt_tensor(dt_bp, shape_bp)
-    # test shape
-    for i in range(len(shape_bp)):
-        assert (
-            shape_bp[i] == tensor_bp.shape[i]
-        ), """Shape of generated tensor
-            does not match the desired shape"""
-    # test if elements are FINN datatype
-    for value in tensor_bp.flatten():
-        assert dt_bp.allowed(
-            value
-        ), """Data type of generated tensor
-            does not match the desired Data type"""
-
-    # binary
-    shape_b = [4, 2, 3]
-    dt_b = DataType.BINARY
-    tensor_b = util.gen_finn_dt_tensor(dt_b, shape_b)
-    # test shape
-    for i in range(len(shape_b)):
-        assert (
-            shape_b[i] == tensor_b.shape[i]
-        ), """Shape of generated tensor
-            does not match the desired shape"""
-    # test if elements are FINN datatype
-    for value in tensor_b.flatten():
-        assert dt_b.allowed(
-            value
-        ), """Data type of generated tensor
-            does not match the desired Data type"""
-
-    # ternary
-    shape_t = [7, 1, 3, 1]
-    dt_t = DataType.TERNARY
-    tensor_t = util.gen_finn_dt_tensor(dt_t, shape_t)
-    # test shape
-    for i in range(len(shape_t)):
-        assert (
-            shape_t[i] == tensor_t.shape[i]
-        ), """Shape of generated tensor
-            does not match the desired shape"""
-    # test if elements are FINN datatype
-    for value in tensor_t.flatten():
-        assert dt_t.allowed(
-            value
-        ), """Data type of generated tensor
-            does not match the desired Data type"""
-
-    # int2
-    shape_int2 = [7, 4]
-    dt_int2 = DataType.INT2
-    tensor_int2 = util.gen_finn_dt_tensor(dt_int2, shape_int2)
-    # test shape
-    for i in range(len(shape_int2)):
-        assert (
-            shape_int2[i] == tensor_int2.shape[i]
-        ), """Shape of generated tensor
-            does not match the desired shape"""
-    # test if elements are FINN datatype
-    for value in tensor_int2.flatten():
-        assert value in [
-            -2,
-            -1,
-            0,
-            1,
-        ], """Data type of generated tensor
-            does not match the desired Data type"""
-
-    # import pdb; pdb.set_trace()
diff --git a/tests/util/test_padding.py b/tests/util/test_padding.py
deleted file mode 100644
index 4e49acf12badc28bd231e990a5d02dc25d3a2006..0000000000000000000000000000000000000000
--- a/tests/util/test_padding.py
+++ /dev/null
@@ -1,57 +0,0 @@
-# Copyright (c) 2020, Xilinx
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-#
-# * Redistributions of source code must retain the above copyright notice, this
-#   list of conditions and the following disclaimer.
-#
-# * Redistributions in binary form must reproduce the above copyright notice,
-#   this list of conditions and the following disclaimer in the documentation
-#   and/or other materials provided with the distribution.
-#
-# * Neither the name of FINN nor the names of its
-#   contributors may be used to endorse or promote products derived from
-#   this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-import numpy as np
-
-from finn.util.basic import pad_tensor_to_multiple_of
-
-
-def test_pad_tensor_to_multiple_of():
-    A = np.eye(3)
-    B = pad_tensor_to_multiple_of(A, [2, 2], val=-1)
-    assert B.shape == (4, 4)
-    assert (B[:3, :3] == A).all()
-    assert (B[3, :] == -1).all()
-    assert (B[:, 3] == -1).all()
-    B = pad_tensor_to_multiple_of(A, [5, 5], val=-1, distr_pad=True)
-    assert B.shape == (5, 5)
-    assert (B[1:4, 1:4] == A).all()
-    assert (B[0, :] == -1).all()
-    assert (B[:, 0] == -1).all()
-    assert (B[4, :] == -1).all()
-    assert (B[:, 4] == -1).all()
-    # using -1 in pad_to parameter should give an unpadded dimension
-    B = pad_tensor_to_multiple_of(A, [-1, 5], val=-1, distr_pad=True)
-    assert B.shape == (3, 5)
-    assert (B[:, 1:4] == A).all()
-    assert (B[:, 0] == -1).all()
-    assert (B[:, 4] == -1).all()
-    # if odd number of padding pixels required, 1 more should go after existing
-    B = pad_tensor_to_multiple_of(A, [6, 6], val=-1, distr_pad=True)
-    assert B.shape == (6, 6)
-    assert (B[1:4, 1:4] == A).all()
diff --git a/tests/util/test_rtlsim2npy.py b/tests/util/test_rtlsim2npy.py
deleted file mode 100644
index 87ea5c2c57b10360ba64ac0593d4a7f5badef735..0000000000000000000000000000000000000000
--- a/tests/util/test_rtlsim2npy.py
+++ /dev/null
@@ -1,107 +0,0 @@
-# Copyright (c) 2020, Xilinx
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-#
-# * Redistributions of source code must retain the above copyright notice, this
-#   list of conditions and the following disclaimer.
-#
-# * Redistributions in binary form must reproduce the above copyright notice,
-#   this list of conditions and the following disclaimer in the documentation
-#   and/or other materials provided with the distribution.
-#
-# * Neither the name of FINN nor the names of its
-#   contributors may be used to endorse or promote products derived from
-#   this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-import numpy as np
-
-from finn.core.datatype import DataType
-from finn.util.data_packing import unpack_innermost_dim_from_hex_string
-
-
-def test_unpack_innermost_dim_from_hex_string():
-    # BINARY
-    A = np.asarray(["0x0e", "0x06"])
-    dtype = DataType.BINARY
-    shape = (1, 2, 4)
-    eA = [[1, 1, 1, 0], [0, 1, 1, 0]]
-    A_unpacked = unpack_innermost_dim_from_hex_string(A, dtype, shape, 8)
-    assert (A_unpacked == eA).all()
-
-    A = np.asarray(["0x0e", "0x06"])
-    eA_flipped = [[0, 1, 1, 1], [0, 1, 1, 0]]
-    A_unpacked_flipped = unpack_innermost_dim_from_hex_string(
-        A, dtype, shape, 8, reverse_inner=True
-    )
-    assert (A_unpacked_flipped == eA_flipped).all()
-
-    # UINT2
-    B = np.asarray([["0x0f", "0x0f"], ["0x07", "0x0d"]])
-    dtype = DataType.UINT2
-    shape = (1, 2, 2, 2)
-    eB = [[[3, 3], [3, 3]], [[1, 3], [3, 1]]]
-    B_unpacked = unpack_innermost_dim_from_hex_string(B, dtype, shape, 8)
-    assert (B_unpacked == eB).all()
-
-    B = np.asarray([["0x0f", "0x0f"], ["0x07", "0x0d"]])
-    eB_flipped = [[[3, 3], [3, 3]], [[3, 1], [1, 3]]]
-    B_unpacked_flipped = unpack_innermost_dim_from_hex_string(
-        B, dtype, shape, 8, reverse_inner=True
-    )
-    assert (B_unpacked_flipped == eB_flipped).all()
-
-    # INT2
-    C = np.asarray([["0x0f", "0x0f"], ["0x07", "0x0d"]])
-    dtype = DataType.INT2
-    shape = (1, 2, 2, 2)
-    eC = [[[-1, -1], [-1, -1]], [[1, -1], [-1, 1]]]
-    C_unpacked = unpack_innermost_dim_from_hex_string(C, dtype, shape, 8)
-    assert (C_unpacked == eC).all()
-
-    C = np.asarray([["0x0f", "0x0f"], ["0x07", "0x0d"]])
-    dtype = DataType.INT2
-    shape = (1, 2, 2, 2)
-    eC = [[[-1, -1], [-1, -1]], [[-1, 1], [1, -1]]]
-    C_unpacked = unpack_innermost_dim_from_hex_string(
-        C, dtype, shape, 8, reverse_inner=True
-    )
-    assert (C_unpacked == eC).all()
-
-    # INT4
-    D = np.asarray(["0x0e", "0x06"])
-    dtype = DataType.INT4
-    shape = (2, 1)
-    eD = [[-2], [6]]
-    D_unpacked = unpack_innermost_dim_from_hex_string(D, dtype, shape, 8)
-    assert (D_unpacked == eD).all()
-
-    D_unpacked = unpack_innermost_dim_from_hex_string(
-        D, dtype, shape, 8, reverse_inner=True
-    )
-    assert (D_unpacked == eD).all()
-
-    # INT32
-    E = np.asarray(["0xffffffff", "0xfffffffe", "0x02", "0xffffffef"])
-    dtype = DataType.INT32
-    shape = (1, 4, 1)
-    eE = [[[-1], [-2], [2], [-17]]]
-    E_unpacked = unpack_innermost_dim_from_hex_string(E, dtype, shape, 32)
-    assert (E_unpacked == eE).all()
-
-    E_unpacked = unpack_innermost_dim_from_hex_string(
-        E, dtype, shape, 32, reverse_inner=True
-    )
-    assert (E_unpacked == eE).all()