diff --git a/src/finn/transformation/fpgadataflow/derive_characteristic.py b/src/finn/transformation/fpgadataflow/derive_characteristic.py
index 5556399ee60d49009ba0e605d206f32e498e8024..fcec750245692808d16d9123f2c7feda72e16350 100644
--- a/src/finn/transformation/fpgadataflow/derive_characteristic.py
+++ b/src/finn/transformation/fpgadataflow/derive_characteristic.py
@@ -179,3 +179,71 @@ class DeriveCharacteristic(NodeLocalTransformation):
                     "Custom op_type %s is currently not supported." % op_type
                 )
         return (node, False)
+
+
+class DeriveFIFOSizes(NodeLocalTransformation):
+    """Prerequisite: DeriveCharacteristic already called on graph.
+    For each node in the graph, use the accumulated I/O characteristic function
+    to perform FIFO sizing, setting the in/outFIFODepth attributes of HLSCustomOp
+    nodes.
+
+    * num_workers (int or None) number of parallel workers, see documentation in
+      NodeLocalTransformation for more details.
+    """
+
+    def __init__(self, num_workers=None):
+        super().__init__(num_workers=num_workers)
+
+    def applyNodeLocal(self, node):
+        op_type = node.op_type
+        if is_fpgadataflow_node(node) is True:
+            try:
+                # lookup op_type in registry of CustomOps
+                prod = registry.getCustomOp(node)
+                assert op_type != "StreamingFIFO", "Found existing FIFOs"
+                period = prod.get_nodeattr("io_characteristic_period")
+                prod_chrc = prod.get_nodeattr("io_characteristic")
+                assert (
+                    len(prod_chrc) == 4 * period
+                ), "Found unexpected characterization attribute"
+                if prod.get_nodeattr("outFIFODepth") > 2:
+                    # FIFO depth already set, can skip this node
+                    return (node, False)
+                prod_chrc = np.asarray(prod_chrc).reshape(2, -1)[1]
+                # find consumers
+                model = self.ref_input_model
+                consumers = model.find_consumers(node.output[0])
+                # compute FIFO depth for each consumer
+                out_fifo_depth = 0
+                for cons_node in consumers:
+                    cons = registry.getCustomOp(cons_node)
+                    cons_chrc = cons.get_nodeattr("io_characteristic")
+                    cons_chrc = np.asarray(cons_chrc).reshape(2, -1)[0]
+                    # find minimum phase shift satisfying the constraint
+                    pshift_min = period
+                    for pshift_cand in range(period):
+                        pshift_condition = [
+                            (prod_chrc[i + pshift_cand] >= cons_chrc[i])
+                            for i in range(period - pshift_cand)
+                        ]
+                        if all(pshift_condition):
+                            pshift_min = pshift_cand
+                            break
+                    fifo_depth = max(
+                        [
+                            (prod_chrc[i + pshift_cand] - cons_chrc[i])
+                            for i in range(pshift_min)
+                        ]
+                    )
+                    out_fifo_depth = max(out_fifo_depth, fifo_depth)
+                # set output FIFO depth for this (producing) node
+                # InsertFIFO looks at the max of (outFIFODepth, inFIFODepth)
+                # for each tensor
+                prod.set_nodeattr("outFIFODepth", out_fifo_depth)
+
+            except KeyError:
+                # exception if op_type is not supported
+                raise Exception(
+                    "Custom op_type %s is currently not supported." % op_type
+                )
+        return (node, False)