diff --git a/src/finn/custom_op/__init__.py b/src/finn/custom_op/__init__.py
index 4ae7b9ebffaab6ca6be04b8d73f647b2db22dc78..08207a3519bf00936ac5ae4eb52e9de86d92e88c 100644
--- a/src/finn/custom_op/__init__.py
+++ b/src/finn/custom_op/__init__.py
@@ -26,101 +26,65 @@
 # 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)
+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
 
-    @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
+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/fpgadataflow/__init__.py b/src/finn/custom_op/fpgadataflow/__init__.py
index 7de6cce936ee54d58d9a526e926ff79dcd35b90d..df03a036f9c9932a820a5798336ea893cddd433f 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.custom_op import CustomOp
 from finn.util.basic import (
     CppBuilder,
     make_build_dir,
diff --git a/src/finn/transformation/streamline/__init__.py b/src/finn/transformation/streamline/__init__.py
index d7686eaadcbc800542ab96c5f45145857412b773..bf0307031b4a9158ee2eb39ee30842e6c5a686b7 100644
--- a/src/finn/transformation/streamline/__init__.py
+++ b/src/finn/transformation/streamline/__init__.py
@@ -25,6 +25,10 @@
 # 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 finn.transformation import Transformation
 from finn.transformation.infer_datatypes import InferDataTypes