From f9e248abf571b3318875e2d1b3a8471281551ee8 Mon Sep 17 00:00:00 2001 From: Siddartha Pothapragada Date: Sun, 30 Aug 2026 00:36:21 -0700 Subject: [PATCH] Fix tensor name/type disagreement on multi-output nodes Summary: `define_tensor` decides twice whether the tensor it is defining is a graph output: once for the tensor *name* (`get_tensor_name` -> `is_graph_output`, which prefixes `output_`) and once for the tensor *type* (`get_tensor_type` -> `is_graph_output`, which returns `QNN_TENSOR_TYPE_APP_READ`). Since `is_graph_output` gained an `output_index` parameter, only the type path has been passing it: output_index = wrapper_idx if tensor_source_node is target_build_node else None The name path still passed the raw `wrapper_idx`. `wrapper_idx` only indexes a node's own outputs when the tensor being defined belongs to that node; builders also use it to key scratch tensors sourced from some *other* node (see `op_scatter_elements`), where it carries no output meaning. For a non-owner multi-output source node at a partition boundary the two paths therefore answer differently: the tensor gets an `output_`-prefixed name while its type stays NATIVE, or gets APP_READ without the name the runtime expects. QNN graph construction then fails, and because it depends on partitioning order it fails intermittently -- this is the flake in `test-qnn-testsuite-linux` / `test-backend-linux (qnn, models)`. This computes `output_index` once and feeds it to both paths, so naming and typing can no longer disagree. `get_tensor_name` takes `output_index` as a new optional argument that defaults to `None`, preserving the behavior of any external caller that does not pass it. The `fbcode/` and `xplat/` copies of `node_visitor.py` are byte-identical mirrors, so both are updated. Authored with AI assistance (Claude Code). Differential Revision: D118035167 --- backends/qualcomm/builders/node_visitor.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/backends/qualcomm/builders/node_visitor.py b/backends/qualcomm/builders/node_visitor.py index 45620e90eca..bbca6ea028c 100644 --- a/backends/qualcomm/builders/node_visitor.py +++ b/backends/qualcomm/builders/node_visitor.py @@ -447,6 +447,7 @@ def get_tensor_name( self, node: torch.fx.Node, wrapper_idx: int = 0, + output_index: Optional[int] = None, ): tensor_name = f"{node.name}@{wrapper_idx}" # The `input_{id}` is utilized for sorting at runtime. Due to multiple passes in qnn_preprocess, @@ -469,7 +470,7 @@ def get_tensor_name( self.edge_program.graph_signature.buffers_to_mutate.keys() ).index(node.name) tensor_name = f"output_mutbuf_{position_index}_{tensor_name}" - elif is_graph_output(node, wrapper_idx): + elif is_graph_output(node, output_index): tensor_name = f"output_{tensor_name}" # Only add qcom_tensor_name when enable tensor dump. @@ -541,14 +542,18 @@ def define_tensor( if cached := nodes_to_wrappers[node_name].get(wrapper_idx, None): return cached - tensor_name = self.get_tensor_name(tensor_source_node, wrapper_idx) - dims = torch.Size([1]) if len(tensor.size()) == 0 else tensor.size() - dynamic_dims, nominal_dims = self.get_dynamic_dimension(dims) # wrapper_idx indexes a node's own outputs only when the tensor being # defined belongs to that node. Builders also use it to key scratch # tensors built from some other node (op_scatter_elements), where it - # carries no output meaning. + # carries no output meaning. The naming and the type path must agree on + # this, otherwise a tensor gets an `output_` name without the matching + # APP_READ type (or vice versa) and the QNN graph build fails. output_index = wrapper_idx if tensor_source_node is target_build_node else None + tensor_name = self.get_tensor_name( + tensor_source_node, wrapper_idx, output_index + ) + dims = torch.Size([1]) if len(tensor.size()) == 0 else tensor.size() + dynamic_dims, nominal_dims = self.get_dynamic_dimension(dims) tensor_type = self.get_tensor_type( tensor_source_node, tensor_type, output_index )