|
1 | 1 | import unittest |
| 2 | +from unittest.mock import MagicMock |
2 | 3 |
|
3 | 4 | import torch |
4 | 5 | from executorch.backends.qualcomm._passes import ( |
|
13 | 14 | from executorch.backends.qualcomm._passes.qnn_pass_manager import ( |
14 | 15 | get_qnn_pass_manager_cls, |
15 | 16 | ) |
| 17 | +from executorch.backends.qualcomm.builders.qnn_constants import OpContextLoader |
| 18 | +from executorch.backends.qualcomm.partition.qnn_partitioner import QnnOperatorSupport |
| 19 | +from executorch.backends.qualcomm.qnn_preprocess import QnnBackend |
16 | 20 | from executorch.backends.qualcomm.quantizer.quantizer import QnnQuantizer, QuantDtype |
17 | 21 | from executorch.backends.qualcomm.serialization.qc_schema import ( |
18 | 22 | QcomChipset, |
|
28 | 32 | generate_qnn_executorch_compiler_spec, |
29 | 33 | to_edge_transform_and_lower_to_qnn, |
30 | 34 | ) |
31 | | -from executorch.exir import to_edge |
| 35 | +from executorch.exir import EdgeCompileConfig, to_edge |
32 | 36 | from executorch.exir.debug_handle_utils import DEBUG_HANDLE_KEY |
33 | 37 | from executorch.exir.dialects._ops import ops as exir_ops |
| 38 | +from torch.library import Library |
34 | 39 | from torchao.quantization.pt2e.quantize_pt2e import convert_pt2e, prepare_pt2e |
35 | 40 |
|
36 | 41 |
|
37 | 42 | class TestPasses(unittest.TestCase): |
| 43 | + def _build_context_loader_edge_program(self, op_name, check_ir_validity=True): |
| 44 | + graph_name = "forward" |
| 45 | + custom_op = Library(OpContextLoader.namespace, "FRAGMENT") |
| 46 | + self.addCleanup(custom_op._destroy) |
| 47 | + custom_op.define(f"{op_name}(Tensor[] inputs) -> Any") |
| 48 | + |
| 49 | + @torch.library.impl( |
| 50 | + custom_op, op_name, dispatch_key="CompositeExplicitAutograd" |
| 51 | + ) |
| 52 | + def op_impl(inputs): |
| 53 | + return (torch.zeros((1, 2), device="meta", dtype=inputs[0].dtype),) |
| 54 | + |
| 55 | + class Model(torch.nn.Module): |
| 56 | + def forward(self, x): |
| 57 | + return getattr( |
| 58 | + getattr(torch.ops, OpContextLoader.namespace), op_name |
| 59 | + ).default((x,)) |
| 60 | + |
| 61 | + exported_program = torch.export.export( |
| 62 | + Model(), (torch.ones(1, 2),), strict=True |
| 63 | + ) |
| 64 | + compile_config = ( |
| 65 | + None if check_ir_validity else EdgeCompileConfig(_check_ir_validity=False) |
| 66 | + ) |
| 67 | + edge_program_manager = to_edge( |
| 68 | + {graph_name: exported_program}, |
| 69 | + compile_config=compile_config, |
| 70 | + ) |
| 71 | + edge_program = edge_program_manager._edge_programs[graph_name] |
| 72 | + context_loader_nodes = [ |
| 73 | + node |
| 74 | + for node in edge_program.graph.nodes |
| 75 | + if node.op == "call_function" |
| 76 | + and OpContextLoader.namespace in str(node.target) |
| 77 | + ] |
| 78 | + return edge_program, context_loader_nodes |
| 79 | + |
| 80 | + def test_context_loader_edge_op_is_delegated(self): |
| 81 | + op_name = "ctx_loader_delegation" |
| 82 | + ctx_bin = b"qnn_context_binary" |
| 83 | + _, context_loader_nodes = self._build_context_loader_edge_program( |
| 84 | + op_name, check_ir_validity=False |
| 85 | + ) |
| 86 | + self.assertEqual(1, len(context_loader_nodes)) |
| 87 | + context_loader_nodes[0].meta[OpContextLoader.meta_ctx_bin] = ctx_bin |
| 88 | + |
| 89 | + # A fully constructed QnnOperatorSupport needs a live QNN manager, so |
| 90 | + # drive is_node_supported with a mock self: the context-loader node is |
| 91 | + # force-passed without touching instance state beyond the log label. |
| 92 | + support = MagicMock() |
| 93 | + support.phase = "QnnPartitioner" |
| 94 | + self.assertTrue( |
| 95 | + QnnOperatorSupport.is_node_supported(support, None, context_loader_nodes[0]) |
| 96 | + ) |
| 97 | + |
| 98 | + def test_build_op_wrappers_returns_context_binary(self): |
| 99 | + op_name = "ctx_loader_build" |
| 100 | + ctx_bin = b"qnn_context_binary" |
| 101 | + edge_program, context_loader_nodes = self._build_context_loader_edge_program( |
| 102 | + op_name, check_ir_validity=False |
| 103 | + ) |
| 104 | + for node in context_loader_nodes: |
| 105 | + node.meta[OpContextLoader.meta_ctx_bin] = ctx_bin |
| 106 | + |
| 107 | + # For a graph whose only op is the context-binary loader, _build_op_wrappers |
| 108 | + # returns the stamped context binary directly, before any QNN compilation. |
| 109 | + result = QnnBackend._build_op_wrappers( |
| 110 | + edge_program, |
| 111 | + enable_tensor_dump=False, |
| 112 | + op_package_infos=[], |
| 113 | + use_mha2sha=False, |
| 114 | + backend_type=QnnExecuTorchBackendType.kHtpBackend, |
| 115 | + ) |
| 116 | + self.assertEqual(ctx_bin, result) |
| 117 | + |
| 118 | + def test_context_loader_op_lowers_with_ir_validation(self): |
| 119 | + op_name = "ctx_loader_validation" |
| 120 | + |
| 121 | + # from_context_binary lowers with IR validity checks enabled (the |
| 122 | + # default). The context-loader custom op survives the edge verifier |
| 123 | + # because its namespace is not aten, so no validation is disabled and |
| 124 | + # the loader node is still present for downstream stamping. |
| 125 | + _, context_loader_nodes = self._build_context_loader_edge_program( |
| 126 | + op_name, check_ir_validity=True |
| 127 | + ) |
| 128 | + self.assertEqual(1, len(context_loader_nodes)) |
| 129 | + |
38 | 130 | def _build_quantized_graph(self): |
39 | 131 | """Build a quantized graph through AnnotateQuantAttrs + FoldQDQ.""" |
40 | 132 |
|
|
0 commit comments