Skip to content

Commit 59f88d2

Browse files
committed
Preserve nested collector scopes at arbitrary depth
1 parent a8ab01d commit 59f88d2

2 files changed

Lines changed: 69 additions & 10 deletions

File tree

invokeai/app/services/shared/graph.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -514,14 +514,12 @@ def _initialize_execution_node(self, exec_node_id: str, input_edges: Optional[li
514514
self._state._try_resolve_if_node(exec_node_id)
515515
self._state._enqueue_if_ready(exec_node_id)
516516

517-
def _get_collect_iteration_group_key(self, edge: Edge) -> tuple[int, ...]:
517+
def _get_collect_iteration_group_key(self, edge: Edge, sibling_depth: Optional[int] = None) -> tuple[int, ...]:
518518
path = self._state._get_iteration_path(edge.source.node_id)
519519
if edge.destination.field == ITEM_FIELD:
520-
source_node_id = self._state.prepared_source_mapping[edge.source.node_id]
521-
if self._get_collect_source_iterator_ids(source_node_id):
522-
return path[:-1]
523-
# No active iterator means the path is inherited from a collector boundary; keep it global.
524-
return ()
520+
# Ragged siblings need the deepest path to identify their shared outer group.
521+
depth = len(path) if sibling_depth is None else sibling_depth
522+
return path[: max(depth - 1, 0)]
525523
return path
526524

527525
def _get_collect_source_iterator_ids(self, source_node_id: str) -> list[str]:
@@ -578,12 +576,15 @@ def _get_collect_iteration_mapping_groups(
578576
for edge in input_edges:
579577
group_keys.update(self._get_collect_candidate_group_keys(edge))
580578
prepared_nodes = self._get_ordered_prepared_nodes_for_source(edge.source.node_id)
579+
sibling_depth = max(
580+
(len(self._state._get_iteration_path(prepared_id)) for prepared_id in prepared_nodes), default=0
581+
)
581582
for prepared_id in prepared_nodes:
582583
prepared_edge = Edge(
583584
source=EdgeConnection(node_id=prepared_id, field=edge.source.field),
584585
destination=edge.destination,
585586
)
586-
group_key = self._get_collect_iteration_group_key(prepared_edge)
587+
group_key = self._get_collect_iteration_group_key(prepared_edge, sibling_depth)
587588
group_keys.add(group_key)
588589
prepared_inputs.append(
589590
(prepared_edge, edge.source.node_id, prepared_id, self._state._get_iteration_path(prepared_id))

tests/test_graph_execution_state.py

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,15 @@ def invoke(self, context: InvocationContext) -> IntegerCollectionTestInvocationO
5050
return IntegerCollectionTestInvocationOutput(collection=[base, base + 1])
5151

5252

53+
class IntegerCollectionWithBranchingTestInvocation(BaseInvocation):
54+
value: int = InputField(default=0)
55+
branch_count: int = InputField(default=2)
56+
57+
def invoke(self, context: InvocationContext) -> IntegerCollectionTestInvocationOutput:
58+
base = self.value * 10
59+
return IntegerCollectionTestInvocationOutput(collection=[base + branch for branch in range(self.branch_count)])
60+
61+
5362
class MaybeEmptyIntegerCollectionTestInvocation(BaseInvocation):
5463
value: int = InputField(default=0)
5564
always_empty: bool = InputField(default=False)
@@ -1365,9 +1374,58 @@ def test_graph_chained_collectors_preserve_ragged_empty_scope():
13651374
execute_all_nodes(state)
13661375

13671376
top_collect_ids = state.source_prepared_mapping["top_collect"]
1368-
assert sorted(state._get_iteration_path(node_id) for node_id in top_collect_ids) == [()]
1369-
top_collect_id = next(iter(top_collect_ids))
1370-
assert state.results[top_collect_id].collection == [[], [0, 1], [10, 11]]
1377+
top_collect_results = {
1378+
state._get_iteration_path(node_id): state.results[node_id].collection for node_id in top_collect_ids
1379+
}
1380+
assert top_collect_results == {(0,): [[]], (1,): [[0, 1], [10, 11]]}
1381+
1382+
1383+
@pytest.mark.parametrize(("levels", "branch_count"), [(3, 2), (4, 2), (5, 1), (6, 1), (7, 1)])
1384+
def test_graph_chained_collectors_preserve_all_iteration_scopes(levels: int, branch_count: int):
1385+
graph = Graph()
1386+
graph.add_node(RangeInvocation(id="source", start=0, stop=2, step=1))
1387+
graph.add_node(IterateInvocation(id="iter_0"))
1388+
for level in range(1, levels):
1389+
graph.add_node(IntegerCollectionWithBranchingTestInvocation(id=f"map_{level}", branch_count=branch_count))
1390+
graph.add_node(IterateInvocation(id=f"iter_{level}"))
1391+
graph.add_node(AddInvocation(id="body", b=0))
1392+
1393+
graph.add_edge(create_edge("source", "collection", "iter_0", "collection"))
1394+
for level in range(1, levels):
1395+
graph.add_edge(create_edge(f"iter_{level - 1}", "item", f"map_{level}", "value"))
1396+
graph.add_edge(create_edge(f"map_{level}", "collection", f"iter_{level}", "collection"))
1397+
graph.add_edge(create_edge(f"iter_{levels - 1}", "item", "body", "a"))
1398+
1399+
previous_node_id = "body"
1400+
previous_field = "value"
1401+
for level in reversed(range(levels)):
1402+
collect_id = f"collect_{level}"
1403+
graph.add_node(CollectInvocation(id=collect_id))
1404+
graph.add_edge(create_edge(previous_node_id, previous_field, collect_id, "item"))
1405+
previous_node_id = collect_id
1406+
previous_field = "collection"
1407+
1408+
state = GraphExecutionState(graph=graph)
1409+
execute_all_nodes(state)
1410+
1411+
def paths(depth: int) -> list[tuple[int, ...]]:
1412+
if depth == 0:
1413+
return [()]
1414+
branch_level = depth - 1
1415+
branch_options = range(2) if branch_level == 0 else range(branch_count)
1416+
return [prefix + (branch,) for prefix in paths(depth - 1) for branch in branch_options]
1417+
1418+
def expected_collection(level: int, prefix: tuple[int, ...]):
1419+
branch_options = range(2) if level == 0 else range(branch_count)
1420+
if level == levels - 1:
1421+
return [int("".join(map(str, prefix + (branch,)))) for branch in branch_options]
1422+
return [expected_collection(level + 1, prefix + (branch,)) for branch in branch_options]
1423+
1424+
for level in range(levels):
1425+
prepared_ids = state.source_prepared_mapping[f"collect_{level}"]
1426+
actual = {state._get_iteration_path(node_id): state.results[node_id].collection for node_id in prepared_ids}
1427+
expected = {prefix: expected_collection(level, prefix) for prefix in paths(level)}
1428+
assert actual == expected
13711429

13721430

13731431
def test_graph_collector_reuses_outer_collection_input_for_each_nested_iterator_group():

0 commit comments

Comments
 (0)