From d53e2b3be7667410e02c8e3f91a3439fa995bf27 Mon Sep 17 00:00:00 2001 From: Siddartha Pothapragada Date: Wed, 5 Aug 2026 16:39:53 -0700 Subject: [PATCH 1/4] Qualcomm: partitioner falls back on ops without a node visitor QnnOperatorSupport.is_node_supported indexed self.node_visitors[op] directly, so any op lacking a QNN node visitor raised KeyError and aborted the whole partition instead of falling back to CPU (as XNNPACK's partitioner does). Guard the lookup and return False for unsupported ops. Surfaced by Mamba2, whose causal mask uses ~torch.tril(...) -> aten.bitwise_not.default, an op QNN has no visitor for. Adds a regression test that fails with KeyError before. Co-authored-with: Claude --- .../qualcomm/partition/qnn_partitioner.py | 5 ++++ backends/qualcomm/tests/test_passes.py | 23 +++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/backends/qualcomm/partition/qnn_partitioner.py b/backends/qualcomm/partition/qnn_partitioner.py index ce48b8bd949..0d3fc3d3aa8 100644 --- a/backends/qualcomm/partition/qnn_partitioner.py +++ b/backends/qualcomm/partition/qnn_partitioner.py @@ -110,6 +110,11 @@ def is_node_supported(self, _, node: torch.fx.Node) -> bool: return False supported = False + if node.target.__name__ not in self.node_visitors: + logger.info( + f"[{self.phase}] {node.target.__name__} | No node visitor, unsupported" + ) + return False op_wrapper = self.node_visitors[node.target.__name__].define_node( node, self.nodes_to_wrappers ) diff --git a/backends/qualcomm/tests/test_passes.py b/backends/qualcomm/tests/test_passes.py index 1124b01d613..243c869053f 100644 --- a/backends/qualcomm/tests/test_passes.py +++ b/backends/qualcomm/tests/test_passes.py @@ -378,6 +378,29 @@ def test_decompose_hardsigmoid_backend_aware(self): f"hardsigmoid {'should' if should_decompose else 'should NOT'} be decomposed for {backend.name}", ) + def test_partitioner_falls_back_on_op_without_visitor(self): + """QnnOperatorSupport must reject an op that has no node visitor by returning + False (CPU fallback), not by KeyError-ing on the node_visitors lookup. + + Regression for Mamba2 QNN lowering: its causal mask uses ~torch.tril(...), + which traces to aten.bitwise_not.default; QNN ships no visitor for it, so the + unguarded lookup aborted the whole partition instead of falling back. + """ + + class BitwiseNot(torch.nn.Module): + def forward(self, x): + return (~(x > 0)).to(torch.float32) + x + + compiler_specs = generate_qnn_executorch_compiler_spec( + soc_model=QcomChipset.SM8650, + backend_options=generate_htp_compiler_spec(use_fp16=True), + ) + # Must not raise KeyError: bitwise_not falls back to CPU; the rest may delegate. + edge = to_edge_transform_and_lower_to_qnn( + BitwiseNot().eval(), (torch.randn(1, 4),), compiler_specs + ) + edge.to_executorch() + if __name__ == "__main__": unittest.main() From 3d928b932c9c93fdf84afd357583720851edfbf3 Mon Sep 17 00:00:00 2001 From: Siddartha Pothapragada Date: Wed, 5 Aug 2026 17:07:16 -0700 Subject: [PATCH 2/4] Address review: assert bitwise_not present + guard QNN SDK availability Co-authored-with: Claude --- backends/qualcomm/tests/test_passes.py | 27 +++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/backends/qualcomm/tests/test_passes.py b/backends/qualcomm/tests/test_passes.py index 3f516881b45..44903f780f7 100644 --- a/backends/qualcomm/tests/test_passes.py +++ b/backends/qualcomm/tests/test_passes.py @@ -483,15 +483,32 @@ class BitwiseNot(torch.nn.Module): def forward(self, x): return (~(x > 0)).to(torch.float32) + x + sample_input = (torch.randn(1, 4),) + + # Guard against a vacuous test: the visitor-less op must actually be present. + exported = torch.export.export(BitwiseNot().eval(), sample_input) + self.assertTrue( + any( + node.op == "call_function" and "bitwise_not" in str(node.target) + for node in exported.graph.nodes + ), + "expected aten.bitwise_not.default in the traced graph", + ) + compiler_specs = generate_qnn_executorch_compiler_spec( soc_model=QcomChipset.SM8650, backend_options=generate_htp_compiler_spec(use_fp16=True), ) - # Must not raise KeyError: bitwise_not falls back to CPU; the rest may delegate. - edge = to_edge_transform_and_lower_to_qnn( - BitwiseNot().eval(), (torch.randn(1, 4),), compiler_specs - ) - edge.to_executorch() + try: + # Must not raise KeyError: bitwise_not falls back to CPU; the rest may delegate. + edge = to_edge_transform_and_lower_to_qnn( + BitwiseNot().eval(), sample_input, compiler_specs + ) + edge.to_executorch() + except RuntimeError as e: + if "QNN" in str(e) or "qnn" in str(e): + self.skipTest(f"QNN SDK not available: {e}") + raise if __name__ == "__main__": From 2b89f8c4646d0c5b0c6f3fa3686cb0700221ef92 Mon Sep 17 00:00:00 2001 From: Siddartha Pothapragada Date: Wed, 5 Aug 2026 21:41:49 -0700 Subject: [PATCH 3/4] Qualcomm: track aten.bitwise_not.default in to_be_implemented_operator Per review: list bitwise_not as a known-unsupported op so it reports the tracked 'can be supported, please report an issue' message. The partitioner guard remains as the general safety net for any other visitor-less op. Co-authored-with: Claude --- backends/qualcomm/partition/common_defs.py | 1 + 1 file changed, 1 insertion(+) diff --git a/backends/qualcomm/partition/common_defs.py b/backends/qualcomm/partition/common_defs.py index 41f28b3929d..1680f608563 100644 --- a/backends/qualcomm/partition/common_defs.py +++ b/backends/qualcomm/partition/common_defs.py @@ -25,6 +25,7 @@ exir_ops.edge.aten.median.dim, exir_ops.edge.aten.round.decimals, exir_ops.edge.aten.le.Scalar, + exir_ops.edge.aten.bitwise_not.default, ] constant_operator = [ From 16b46e033e342ec2e83c910d1ee27f4c9a12f204 Mon Sep 17 00:00:00 2001 From: Siddartha Pothapragada Date: Thu, 6 Aug 2026 16:24:14 -0700 Subject: [PATCH 4/4] Address review: use aten.frac (no visitor, unlisted) to exercise the guard Per @hutton: bitwise_not now sits in to_be_implemented_operator and returns earlier, so it no longer reaches the missing-visitor guard. frac has no node visitor and is on no partition list, so it hits the guard directly. Co-authored-with: Claude --- backends/qualcomm/tests/test_passes.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/backends/qualcomm/tests/test_passes.py b/backends/qualcomm/tests/test_passes.py index 44903f780f7..d8815f170e8 100644 --- a/backends/qualcomm/tests/test_passes.py +++ b/backends/qualcomm/tests/test_passes.py @@ -474,25 +474,26 @@ def test_partitioner_falls_back_on_op_without_visitor(self): """QnnOperatorSupport must reject an op that has no node visitor by returning False (CPU fallback), not by KeyError-ing on the node_visitors lookup. - Regression for Mamba2 QNN lowering: its causal mask uses ~torch.tril(...), - which traces to aten.bitwise_not.default; QNN ships no visitor for it, so the - unguarded lookup aborted the whole partition instead of falling back. + Uses aten.frac.default: it has no QNN node visitor and is not on any partition + operator list, so it exercises the missing-visitor guard directly. (bitwise_not, + the op that first surfaced this on Mamba2, is now in to_be_implemented_operator, + which returns earlier and would not reach the guard.) """ - class BitwiseNot(torch.nn.Module): + class FracModule(torch.nn.Module): def forward(self, x): - return (~(x > 0)).to(torch.float32) + x + return torch.frac(x) + x sample_input = (torch.randn(1, 4),) # Guard against a vacuous test: the visitor-less op must actually be present. - exported = torch.export.export(BitwiseNot().eval(), sample_input) + exported = torch.export.export(FracModule().eval(), sample_input) self.assertTrue( any( - node.op == "call_function" and "bitwise_not" in str(node.target) + node.op == "call_function" and "frac" in str(node.target) for node in exported.graph.nodes ), - "expected aten.bitwise_not.default in the traced graph", + "expected aten.frac.default in the traced graph", ) compiler_specs = generate_qnn_executorch_compiler_spec( @@ -500,9 +501,9 @@ def forward(self, x): backend_options=generate_htp_compiler_spec(use_fp16=True), ) try: - # Must not raise KeyError: bitwise_not falls back to CPU; the rest may delegate. + # Must not raise KeyError: frac falls back to CPU; the rest may delegate. edge = to_edge_transform_and_lower_to_qnn( - BitwiseNot().eval(), sample_input, compiler_specs + FracModule().eval(), sample_input, compiler_specs ) edge.to_executorch() except RuntimeError as e: