Skip to content

Graph execution stalls when a Collect node is fed by an iterator over an empty collection #9347

Description

@Pfannkuchensack

Summary

A Collect node whose only inputs come from an iterator over an empty collection (zero iterations) is never materialized during graph execution. As a result:

  • the collector and everything downstream of it never run, and
  • GraphExecutionState.next() returns None while GraphExecutionState.is_complete() keeps returning False.

The session is effectively stalled: there is nothing left to execute, but the graph is never reported as complete.

This is a pre-existing issue (reproduces on current main) and is independent of PR #9343 — that PR was verified to neither introduce nor fix this behavior.

Root cause (analysis)

  • An iterator over an empty collection produces zero prepared execution nodes, so its source node is never added to executed (a source is only marked executed once all of its prepared exec nodes have completed, and there are none).
  • _ExecutionMaterializer.prepare() only selects a node for expansion when no ancestor IterateInvocation is still un-executed. Because the empty iterator is never marked executed, the downstream Collect node is never selected.
  • Consequently no collector exec node is created, its downstream consumer is never enqueued, and since is_complete() requires every source node to be in executed, the state can never complete.

Relevant code: invokeai/app/services/shared/graph.py — the prepare() selection guard (skips nodes with un-executed iterator ancestors) and the collector branch of prepare().

Steps to reproduce (minimal, unit-test level)

from unittest.mock import Mock

from invokeai.app.invocations.baseinvocation import InvocationContext
from invokeai.app.invocations.math import AddInvocation
from invokeai.app.invocations.primitives import IntegerCollectionInvocation
from invokeai.app.services.shared.graph import (
    CollectInvocation,
    Graph,
    GraphExecutionState,
    IterateInvocation,
)
from tests.test_graph_execution_state import (
    IntegerCollectionPassthroughTestInvocation,
    create_edge,
)


def test_empty_collection_iterate_collect_stalls():
    graph = Graph()
    graph.add_node(IntegerCollectionInvocation(id="empty", collection=[]))  # empty -> 0 iterations
    graph.add_node(IterateInvocation(id="iter"))
    graph.add_node(AddInvocation(id="item", b=0))
    graph.add_node(CollectInvocation(id="collect"))
    graph.add_node(IntegerCollectionPassthroughTestInvocation(id="consumer"))
    graph.add_edge(create_edge("empty", "collection", "iter", "collection"))
    graph.add_edge(create_edge("iter", "item", "item", "a"))
    graph.add_edge(create_edge("item", "value", "collect", "item"))
    graph.add_edge(create_edge("collect", "collection", "consumer", "collection"))

    g = GraphExecutionState(graph=graph)
    while True:
        n = g.next()
        if n is None:
            break
        g.complete(n.id, n.invoke(Mock(InvocationContext)))

    # Observed:
    assert g.next() is None
    assert g.is_complete() is False                                   # stall
    assert g.source_prepared_mapping.get("collect", set()) == set()   # never materialized
    assert g.source_prepared_mapping.get("consumer", set()) == set()  # never ran

Graph shape:

IntegerCollection([])  ->  Iterate  ->  Add  ->  Collect  ->  <consumer>

Expected behavior

An empty upstream collection should not stall the session. Either:

  • the Collect node produces an empty collection ([]) and its downstream consumer runs with an empty input, or
  • the graph is otherwise explicitly reported as complete.

In all cases is_complete() must eventually become True (or an error must be raised) once next() returns None.

Actual behavior

  • Collect (and its downstream consumer) are never materialized.
  • next() returns None, but is_complete() stays False indefinitely.

Notes

  • Reproduces on main (6ca505fe64fd1deed744dca1ec209324ac894e82 and later).
  • Discovered while reviewing PR Fix nested collector iteration scope #9343 (nested collector iteration scoping). That PR rewrites the collector branch of prepare() but does not change this behavior; the stall exists identically with and without it, so it should be fixed separately.

Metadata

Metadata

Assignees

Labels

Type

Fields

No fields configured for Bug.

Projects

Status
6.14.x Theme: USER EXPERIENCE

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions