From ae8d1e04c03927eb21a8717f64d536cc420de461 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20S=C5=82uszniak?= Date: Wed, 2 Sep 2026 20:47:58 +0200 Subject: [PATCH] Do not assert on a graph output that carries no tensors The memory planning verifier walks the placeholder and output nodes and records, for each one, whether the planner allocated it. A node with no tensor specs is skipped, and the skip happens before graph_output_allocated is set, so a graph whose output node carries only non-tensor values leaves that flag at None and trips "assert graph_output_allocated is not None". An output node like that is legitimate. A Vulkan partition of a dynamic-shape LLM ends up with one whose only results are symints (sym_size / et_vk.select_as_symint), and the graph is fully planned when the verifier rejects it. The neighbouring len(specs) == 0 branch already treats "nothing to allocate" as success for the all-const case; do the same here. Repro before this change: export any llama-family model with enable_dynamic_shape=True and backend.vulkan.enabled=True, e.g. qwen3 0.6B, and lowering fails with "graph_output_allocated not set". --- exir/memory_planning.py | 8 ++++++++ exir/tests/test_memory_planning.py | 24 ++++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/exir/memory_planning.py b/exir/memory_planning.py index 44b53585455..ac83e99f2eb 100644 --- a/exir/memory_planning.py +++ b/exir/memory_planning.py @@ -275,6 +275,14 @@ def verify_graph_input_output(self) -> None: for nd in graph_module.graph.nodes: if nd.op in check_list: if not (specs := get_node_tensor_specs(nd)): + # A node that carries no tensor specs has nothing to + # allocate. An output node can legitimately be in that + # position, e.g. when a delegated subgraph's only results + # are symints, and skipping it silently leaves + # graph_output_allocated unset so the assert below fires on + # a graph that is in fact fully planned. + if nd.op == "output": + graph_output_allocated = self.alloc_graph_output continue if _is_mutable_buffer(nd, self.graph_signature): continue diff --git a/exir/tests/test_memory_planning.py b/exir/tests/test_memory_planning.py index 37bc088cfcc..041ebcfe2ef 100644 --- a/exir/tests/test_memory_planning.py +++ b/exir/tests/test_memory_planning.py @@ -2011,3 +2011,27 @@ def forward(self, a: torch.Tensor, b: torch.Tensor) -> torch.Tensor: if node.op == "call_function" ) self.assertFalse(has_inplace) + + +class TestTensorlessGraphOutput(unittest.TestCase): + def test_verifier_accepts_output_carrying_no_tensors(self) -> None: + """An output node is allowed to carry no tensors at all. + + A delegated subgraph whose only results are symints is shaped like + this, and there is nothing for the planner to allocate. The verifier + used to skip such a node without recording that it had seen an output, + then assert `graph_output_allocated not set` on a graph that was in + fact fully planned. + """ + graph = Graph() + sym = graph.placeholder("sym") + graph.output((sym,)) + graph_module = GraphModule({}, graph) + + verifier = Verifier( + graph_module, + alloc_graph_input=True, + alloc_graph_output=True, + alloc_mutable_buffers=True, + ) + verifier.verify_graph_input_output()