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 = [ 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 1685a106e72..6d09b2bf959 100644 --- a/backends/qualcomm/tests/test_passes.py +++ b/backends/qualcomm/tests/test_passes.py @@ -473,6 +473,46 @@ 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. + + 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 FracModule(torch.nn.Module): + def forward(self, 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(FracModule().eval(), sample_input) + self.assertTrue( + any( + node.op == "call_function" and "frac" in str(node.target) + for node in exported.graph.nodes + ), + "expected aten.frac.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), + ) + try: + # Must not raise KeyError: frac falls back to CPU; the rest may delegate. + edge = to_edge_transform_and_lower_to_qnn( + FracModule().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}") + def test_index_put_int64_value_not_quantized(self): """QNN's IndexPut annotator must skip a non-float (int64) value arg.