diff --git a/src/finn/analysis/fpgadataflow/post_synth_res.py b/src/finn/analysis/fpgadataflow/post_synth_res.py
index 508c34aaed50f2935f4915cdcea29a3e92641b3c..72533b54ea7b2193a3a9e0d7ee7c8c60646ead0e 100644
--- a/src/finn/analysis/fpgadataflow/post_synth_res.py
+++ b/src/finn/analysis/fpgadataflow/post_synth_res.py
@@ -30,15 +30,20 @@ import os
 import xml.etree.ElementTree as ET
 
 from finn.transformation.move_reshape import _is_fpgadataflow_node
+from finn.core.modelwrapper import ModelWrapper
+from finn.custom_op.registry import getCustomOp
 
 
-def post_synth_res(model):
+def post_synth_res(model, override_synth_report_filename=None):
     """Extracts the FPGA resource results from the Vivado synthesis.
 
     Returns {node name : resources_dict}."""
 
     res_dict = {}
-    synth_report_filename = model.get_metadata_prop("vivado_synth_rpt")
+    if override_synth_report_filename is not None:
+        synth_report_filename = override_synth_report_filename
+    else:
+        synth_report_filename = model.get_metadata_prop("vivado_synth_rpt")
     if os.path.isfile(synth_report_filename):
         tree = ET.parse(synth_report_filename)
         root = tree.getroot()
@@ -51,30 +56,35 @@ def post_synth_res(model):
 
     for node in model.graph.node:
         if _is_fpgadataflow_node(node):
-            row = root.findall(".//*[@contents='%s']/.." % node.name)
-            if row != []:
-                node_dict = {}
-                row = row[0].getchildren()
-                """ Expected XML structure:
-<tablerow class="" suppressoutput="0" wordwrap="0">
-    <tableheader class="" contents="Instance" halign="3" width="-1"/>
-    <tableheader class="" contents="Module" halign="3" width="-1"/>
-    <tableheader class="" contents="Total LUTs" halign="3" width="-1"/>
-    <tableheader class="" contents="Logic LUTs" halign="3" width="-1"/>
-    <tableheader class="" contents="LUTRAMs" halign="3" width="-1"/>
-    <tableheader class="" contents="SRLs" halign="3" width="-1"/>
-    <tableheader class="" contents="FFs" halign="3" width="-1"/>
-    <tableheader class="" contents="RAMB36" halign="3" width="-1"/>
-    <tableheader class="" contents="RAMB18" halign="3" width="-1"/>
-    <tableheader class="" contents="DSP48 Blocks" halign="3" width="-1"/>
-</tablerow>
-                """
-                node_dict["LUT"] = int(row[2].attrib["contents"])
-                node_dict["SRL"] = int(row[5].attrib["contents"])
-                node_dict["FF"] = int(row[6].attrib["contents"])
-                node_dict["BRAM_36K"] = int(row[7].attrib["contents"])
-                node_dict["BRAM_18K"] = int(row[8].attrib["contents"])
-                node_dict["DSP48"] = int(row[9].attrib["contents"])
-                res_dict[node.name] = node_dict
+            if node.op_type == "StreamingDataflowPartition":
+                sdp_model = ModelWrapper(getCustomOp(node).get_nodeattr("model"))
+                sdp_res_dict = post_synth_res(sdp_model, synth_report_filename)
+                res_dict.update(sdp_res_dict)
+            else:
+                row = root.findall(".//*[@contents='%s']/.." % node.name)
+                if row != []:
+                    node_dict = {}
+                    row = row[0].getchildren()
+                    """ Expected XML structure:
+    <tablerow class="" suppressoutput="0" wordwrap="0">
+        <tableheader class="" contents="Instance" halign="3" width="-1"/>
+        <tableheader class="" contents="Module" halign="3" width="-1"/>
+        <tableheader class="" contents="Total LUTs" halign="3" width="-1"/>
+        <tableheader class="" contents="Logic LUTs" halign="3" width="-1"/>
+        <tableheader class="" contents="LUTRAMs" halign="3" width="-1"/>
+        <tableheader class="" contents="SRLs" halign="3" width="-1"/>
+        <tableheader class="" contents="FFs" halign="3" width="-1"/>
+        <tableheader class="" contents="RAMB36" halign="3" width="-1"/>
+        <tableheader class="" contents="RAMB18" halign="3" width="-1"/>
+        <tableheader class="" contents="DSP48 Blocks" halign="3" width="-1"/>
+    </tablerow>
+                    """
+                    node_dict["LUT"] = int(row[2].attrib["contents"])
+                    node_dict["SRL"] = int(row[5].attrib["contents"])
+                    node_dict["FF"] = int(row[6].attrib["contents"])
+                    node_dict["BRAM_36K"] = int(row[7].attrib["contents"])
+                    node_dict["BRAM_18K"] = int(row[8].attrib["contents"])
+                    node_dict["DSP48"] = int(row[9].attrib["contents"])
+                    res_dict[node.name] = node_dict
 
     return res_dict
diff --git a/src/finn/transformation/fpgadataflow/annotate_resources.py b/src/finn/transformation/fpgadataflow/annotate_resources.py
index 62ee92df54eee2b63d84657515d7fbc3a8808b81..aba22d16caf715f3495055489c185d925a33f7e0 100644
--- a/src/finn/transformation/fpgadataflow/annotate_resources.py
+++ b/src/finn/transformation/fpgadataflow/annotate_resources.py
@@ -32,6 +32,8 @@ 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
 from finn.analysis.fpgadataflow.post_synth_res import post_synth_res
+from finn.core.modelwrapper import ModelWrapper
+from finn.custom_op.registry import getCustomOp
 
 
 class AnnotateResources(Transformation):
@@ -44,9 +46,10 @@ class AnnotateResources(Transformation):
     chosen mode (e.g. HLSSynthIP for hls) was previously run.
     """
 
-    def __init__(self, mode):
+    def __init__(self, mode, override_res_dict=None):
         super().__init__()
         self.mode = mode
+        self.res_dict = override_res_dict
 
     def apply(self, model):
         graph = model.graph
@@ -58,10 +61,34 @@ class AnnotateResources(Transformation):
             res_fxn = post_synth_res
         else:
             raise Exception("Unrecognized mode for AnnotateResources")
-        res_dict = model.analysis(res_fxn)
+        if self.res_dict is None:
+            self.res_dict = model.analysis(res_fxn)
+        children_dict = {}
+        # annotate node resources
+        for node in graph.node:
+            if _is_fpgadataflow_node(node):
+                if node.name in self.res_dict.keys():
+                    op_inst = registry.getCustomOp(node)
+                    op_inst.set_nodeattr(
+                        "res_" + self.mode, str(self.res_dict[node.name])
+                    )
+                    children_dict[node.name] = self.res_dict[node.name]
+                elif node.op_type == "StreamingDataflowPartition":
+                    # recurse into model to manually annotate per-layer resources
+                    sdp_model_filename = getCustomOp(node).get_nodeattr("model")
+                    sdp_model = ModelWrapper(sdp_model_filename)
+                    sdp_model = sdp_model.transform(
+                        AnnotateResources(self.mode, self.res_dict)
+                    )
+                    sdp_dict = sdp_model.get_metadata_prop("res_total_" + self.mode)
+                    # save transformed model
+                    sdp_model.save(sdp_model_filename)
+                    # set res attribute for sdp node
+                    getCustomOp(node).set_nodeattr("res_" + self.mode, str(sdp_dict))
+                    children_dict[node.name] = sdp_dict
         total_dict = {}
-        for lname in res_dict.keys():
-            layer_res_dict = res_dict[lname]
+        for lname in children_dict.keys():
+            layer_res_dict = self.res_dict[lname]
             for r_type in layer_res_dict.keys():
                 r_amount = layer_res_dict[r_type]
                 r_amount = float(r_amount)
@@ -73,9 +100,4 @@ class AnnotateResources(Transformation):
             if "efficiency" in k:
                 total_dict[k] = total_dict[k] / len(graph.node)
         model.set_metadata_prop("res_total_" + self.mode, str(total_dict))
-        for node in graph.node:
-            if _is_fpgadataflow_node(node) and node.name in res_dict.keys():
-                op_inst = registry.getCustomOp(node)
-                op_inst.set_nodeattr("res_" + self.mode, str(res_dict[node.name]))
-
         return (model, False)