|
15 | 15 | from executorch.backends.qualcomm._passes.qnn_pass_manager import ( |
16 | 16 | get_qnn_pass_manager_cls, |
17 | 17 | ) |
| 18 | +from executorch.backends.qualcomm.builders.op_custom_op import ( |
| 19 | + _resolve_qnn_data_type, |
| 20 | + CustomOp, |
| 21 | +) |
18 | 22 | from executorch.backends.qualcomm.builders.qnn_constants import OpContextLoader |
19 | 23 | from executorch.backends.qualcomm.partition.qnn_partitioner import QnnOperatorSupport |
20 | 24 | from executorch.backends.qualcomm.qnn_preprocess import QnnBackend |
21 | 25 | from executorch.backends.qualcomm.quantizer.quantizer import QnnQuantizer, QuantDtype |
22 | 26 | from executorch.backends.qualcomm.serialization.qc_schema import ( |
23 | 27 | QcomChipset, |
24 | 28 | QnnExecuTorchBackendType, |
| 29 | + QnnExecuTorchOpPackageInfo, |
25 | 30 | ) |
26 | 31 | from executorch.backends.qualcomm.tests.models import ( |
27 | 32 | BroadcastAndMutate, |
@@ -718,6 +723,78 @@ def forward(self, a2d, b): |
718 | 723 | "dedupe must not rank-promote the USER_INPUT_MUTATION write-back", |
719 | 724 | ) |
720 | 725 |
|
| 726 | + def _custom_op_node(self, arg, arg_name): |
| 727 | + """A CustomOp builder plus a single-arg node, driven through define_node so |
| 728 | + the branch dispatch is exercised rather than the type helper in isolation. |
| 729 | + Branch order matters: str must be matched before Iterable, since str is |
| 730 | + itself Iterable and would otherwise take the tensor-param path.""" |
| 731 | + |
| 732 | + class _Arg: |
| 733 | + name = arg_name |
| 734 | + |
| 735 | + class _Schema: |
| 736 | + arguments = [_Arg()] |
| 737 | + |
| 738 | + class _Target: |
| 739 | + _schema = _Schema() |
| 740 | + |
| 741 | + node = MagicMock() |
| 742 | + node.name = "my_ops_foo_default" |
| 743 | + node.target = _Target() |
| 744 | + node.args = (arg,) |
| 745 | + |
| 746 | + info = QnnExecuTorchOpPackageInfo() |
| 747 | + info.custom_op_name = "my_ops.foo.default" |
| 748 | + info.op_package_name = "FooOpPackage" |
| 749 | + info.qnn_op_type_name = "Foo" |
| 750 | + return CustomOp(info, {}, None, False, True), node |
| 751 | + |
| 752 | + def test_custom_op_rejects_str_arg(self): |
| 753 | + """A str custom-op arg must name the argument and the missing binding. |
| 754 | +
|
| 755 | + str is Iterable, so before the guard it reached the tensor-param branch and |
| 756 | + died on QNN_TENSOR_TYPE_MAP[type(arg[0])] with a bare KeyError. Strings are |
| 757 | + not a QNN limitation: QNN_DATATYPE_STRING exists and PyQnnManagerAdaptor.cpp |
| 758 | + already reads it; only AddScalarParam has no case for it. |
| 759 | + """ |
| 760 | + builder, node = self._custom_op_node("bilinear", "soft_nms_method") |
| 761 | + with self.assertRaises(ValueError) as ctx: |
| 762 | + builder.define_node(node, {}) |
| 763 | + message = str(ctx.exception) |
| 764 | + self.assertIn("soft_nms_method", message) |
| 765 | + self.assertIn("my_ops.foo.default", message) |
| 766 | + self.assertIn("QNN_DATATYPE_STRING", message) |
| 767 | + # A str must not be reported as a tensor param element type. |
| 768 | + self.assertNotIn("element type", message) |
| 769 | + |
| 770 | + def test_custom_op_rejects_unmapped_element_type(self): |
| 771 | + """A list whose element type has no QNN mapping -- str[] among them -- must |
| 772 | + report the element type rather than KeyError.""" |
| 773 | + builder, node = self._custom_op_node(["linear", "gaussian"], "modes") |
| 774 | + with self.assertRaises(ValueError) as ctx: |
| 775 | + builder.define_node(node, {}) |
| 776 | + self.assertIn("element type", str(ctx.exception)) |
| 777 | + self.assertIn("modes", str(ctx.exception)) |
| 778 | + |
| 779 | + def test_custom_op_rejects_empty_sequence(self): |
| 780 | + """An empty sequence arg used to IndexError on arg[0] inside define_node.""" |
| 781 | + builder, node = self._custom_op_node([], "sizes") |
| 782 | + with self.assertRaises(ValueError) as ctx: |
| 783 | + builder.define_node(node, {}) |
| 784 | + self.assertIn("sizes", str(ctx.exception)) |
| 785 | + self.assertIn("empty", str(ctx.exception)) |
| 786 | + |
| 787 | + def test_custom_op_resolves_supported_types(self): |
| 788 | + """Guard against a vacuous suite: the mapped scalar types still resolve. |
| 789 | +
|
| 790 | + Checked at the helper rather than through define_node, because a valid arg |
| 791 | + carries on into output-tensor handling, which needs real tensor meta. |
| 792 | + """ |
| 793 | + for py_type in (int, float, bool): |
| 794 | + self.assertIsNotNone( |
| 795 | + _resolve_qnn_data_type(py_type, "arg", "my_ops.foo.default") |
| 796 | + ) |
| 797 | + |
721 | 798 |
|
722 | 799 | if __name__ == "__main__": |
723 | 800 | unittest.main() |
0 commit comments