From e6c7f278326d2d82369c4355a2b896a2af99fd9c Mon Sep 17 00:00:00 2001 From: Cheng-Hsin Weng Date: Mon, 15 Jun 2026 18:18:23 +0800 Subject: [PATCH 1/3] Qualcomm AI Engine Direct - Add QNN HadamardTransform op support Lower a bias-less linear / matmul / pointwise 1x1 conv whose constant weight is a scaled Hadamard matrix into QNN's native HadamardTransform op. A torch.ops.qnn_custom.hadamard_transform op is registered, and a pre-quantization RecomposeHadamard pass detects the matching nodes (via the shared match_hadamard_weight matcher in builders/utils.py) and rewrites them into the custom op. Since hadamard_transform operates on the last dim, linear and matmul rewrite directly, while conv is wrapped in permutes that move the channel dim to the last dim and back. A dedicated htp_rules.py annotator and op_hadamard.py builder handle quantization and lowering. The rewrite is gated on QNN SDK >= 2.47 for backward compatibility. Co-Authored-By: Claude Opus 4.8 --- backends/qualcomm/_passes/__init__.py | 2 + .../backends/htp/qnn_htp_pass_manager.py | 8 +- .../qualcomm/_passes/recompose_hadamard.py | 184 ++++++++++++++++++ backends/qualcomm/builders/README.md | 1 + backends/qualcomm/builders/__init__.py | 4 + backends/qualcomm/builders/custom_ops.py | 37 ++++ .../builders/op_hadamard_transform.py | 66 +++++++ backends/qualcomm/builders/qnn_constants.py | 6 + .../quantizer/annotators/htp_rules.py | 11 ++ backends/qualcomm/tests/models.py | 47 +++++ backends/qualcomm/tests/rework/conftest.py | 7 +- .../qualcomm/tests/rework/htp/op/v68/test.py | 15 ++ backends/qualcomm/tests/rework/passes/test.py | 6 + backends/qualcomm/tests/rework/src/op.py | 53 +++++ backends/qualcomm/tests/rework/src/pattern.py | 118 +++++++++++ backends/qualcomm/tests/test_qnn_delegate.py | 182 +++++++++++++++++ examples/models/llama/hf_download.py | 5 +- .../llama/evaluator/device_evaluator.py | 1 + 18 files changed, 749 insertions(+), 4 deletions(-) create mode 100644 backends/qualcomm/_passes/recompose_hadamard.py create mode 100644 backends/qualcomm/builders/custom_ops.py create mode 100644 backends/qualcomm/builders/op_hadamard_transform.py diff --git a/backends/qualcomm/_passes/__init__.py b/backends/qualcomm/_passes/__init__.py index ec557b4b5c2..c1376b50ba7 100644 --- a/backends/qualcomm/_passes/__init__.py +++ b/backends/qualcomm/_passes/__init__.py @@ -60,6 +60,7 @@ from .layout_transform import LayoutTransform from .lift_constant_scalar_operands import LiftConstantScalarOperands from .lpai_partition_fallback_support import LpaiPartitionFallbackSupport +from .recompose_hadamard import RecomposeHadamard from .recompose_pad_maxpool2d import RecomposePadMaxPool2d from .recompose_pixel_unshuffle import RecomposePixelUnshuffle from .recompose_rms_norm import RecomposeRmsNorm @@ -127,6 +128,7 @@ LayoutTransform, LiftConstantScalarOperands, LpaiPartitionFallbackSupport, + RecomposeHadamard, RecomposePadMaxPool2d, RecomposePixelUnshuffle, RecomposeRmsNorm, diff --git a/backends/qualcomm/_passes/backends/htp/qnn_htp_pass_manager.py b/backends/qualcomm/_passes/backends/htp/qnn_htp_pass_manager.py index c3a8c47f2c1..d83acfd9f7d 100644 --- a/backends/qualcomm/_passes/backends/htp/qnn_htp_pass_manager.py +++ b/backends/qualcomm/_passes/backends/htp/qnn_htp_pass_manager.py @@ -4,7 +4,11 @@ # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. -from executorch.backends.qualcomm._passes import DecomposeReciprocal, RemoveRedundancy +from executorch.backends.qualcomm._passes import ( + DecomposeReciprocal, + RecomposeHadamard, + RemoveRedundancy, +) from executorch.backends.qualcomm._passes.qnn_pass_manager import QnnPassManager @@ -33,7 +37,7 @@ def get_passes_dependency_for_capture_program(cls): @classmethod def get_annotation_passes(cls): - passes = [DecomposeReciprocal] + passes = [DecomposeReciprocal, RecomposeHadamard] passes.extend(super().get_annotation_passes()) return passes diff --git a/backends/qualcomm/_passes/recompose_hadamard.py b/backends/qualcomm/_passes/recompose_hadamard.py new file mode 100644 index 00000000000..d398b0ada22 --- /dev/null +++ b/backends/qualcomm/_passes/recompose_hadamard.py @@ -0,0 +1,184 @@ +# Copyright (c) Qualcomm Innovation Center, Inc. +# All rights reserved +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. +from operator import attrgetter + +# Registers torch.ops.qnn_custom.hadamard_transform. +import executorch.backends.qualcomm.builders.custom_ops # noqa: F401 + +import numpy as np + +import scipy.linalg +import torch + +from executorch.backends.qualcomm.utils.check_qnn_version import ( + is_qnn_sdk_version_less_than, +) +from executorch.exir.pass_base import ExportPass, PassResult +from executorch.exir.passes import dead_code_elimination_pass + +from .utils import copy_meta + + +def _is_power_of_2_sqare_matrix(weight: torch.Tensor) -> bool: + dim = weight.shape[0] + # Shape gate: non-square / non-2D / non-power-of-2 weight can never match. + return ( + weight.dim() != 2 or weight.shape[0] != weight.shape[1] or dim & (dim - 1) != 0 + ) + + +def _match_hadamard_weight(weight: torch.Tensor) -> bool: + # Returns True if `weight == scipy.linalg.hadamard(dim) * s` for some scale s. + # A linear/matmul with such a weight is equivalent to a QNN HadamardTransform. + if _is_power_of_2_sqare_matrix(weight): + return False + + w = weight.detach().to(torch.float64).numpy() + nonzero = w[w != 0] + if nonzero.size == 0: + return False + # The Hadamard weight is H * s for a single global scale s; infer s from any + # nonzero entry (all |H_ij| == 1). For per-channel quant this only matches + # when every channel's dequantized scale reconstructs the same H * s. + scale = float(abs(nonzero.flat[0])) + hadamard = scipy.linalg.hadamard(weight.shape[0]).astype("float64") * scale + return np.allclose(w, hadamard, rtol=0, atol=1e-4) + + +class RecomposeHadamard(ExportPass): + """ + Rewrite a bias-less linear / matmul / 1x1 conv whose weight is a Hadamard + matrix into a single qnn_custom.hadamard_transform op, so it is annotated and + lowered as a first-class HadamardTransform instead of being detected late in + the builder and validated as FullyConnected / MatMul / Conv. + + Runs in the annotation pipeline (before quantization), where the weight is a + real tensor and can be inspected. hadamard_transform acts on the last dim, so + linear / matmul rewrite directly, while conv (which mixes the channel dim) is + wrapped in permutes that move the channel to the last dim and back. + """ + + def __init__(self): + super().__init__() + self.hadamard_target = torch.ops.qnn_custom.hadamard_transform.default + + def _is_pointwise_conv(self, node, weight: torch.Tensor) -> bool: + # Only a 1x1, stride-1, no-pad, dilation-1, groups-1 conv is a pure + # channel-mixing matmul equivalent to a Hadamard transform. conv2d args: + # (input, weight, bias, stride, padding, dilation, groups) with defaults. + stride = node.args[3] if len(node.args) > 3 else [1, 1] + padding = node.args[4] if len(node.args) > 4 else [0, 0] + dilation = node.args[5] if len(node.args) > 5 else [1, 1] + groups = node.args[6] if len(node.args) > 6 else 1 + return ( + weight.dim() == 4 + and all(k == 1 for k in weight.shape[2:]) + and all(s == 1 for s in stride) + and all(p == 0 for p in padding) + and all(d == 1 for d in dilation) + and groups == 1 + ) + + def _get_hadamard_scale(self, weight: torch.Tensor) -> float: + # weight == H * s (all |H_ij| == 1); linear/matmul(x) = x @ H. The op + # applies the orthonormal H / sqrt(dim), so fold the remaining factor + # s * sqrt(dim) into the op's scale (== 1 for an orthonormal Hadamard). + dim = weight.shape[0] + return float(weight.detach().abs().flatten()[0]) * (dim**0.5) + + def _rewrite_last_dim(self, graph, node, scale): + # linear / matmul already transform the last dim: replace in place. + with graph.inserting_before(node): + hadamard_node = graph.create_node( + "call_function", + self.hadamard_target, + (node.args[0], scale), + ) + hadamard_node.meta = copy_meta(node.meta) + for user in node.users.copy(): + user.replace_input_with(node, hadamard_node) + + def _rewrite_channel_dim(self, graph, node, scale): + # conv mixes the channel dim (dim 1). Move it to the last dim, run the + # transform there, then move it back. + input_node = node.args[0] + input_val = input_node.meta["val"] + rank = input_val.dim() + to_last = [0, *range(2, rank), 1] + from_last = [0, rank - 1, *range(1, rank - 1)] + with graph.inserting_before(node): + pre = graph.create_node( + "call_function", torch.ops.aten.permute.default, (input_node, to_last) + ) + pre.meta = copy_meta(node.meta) + pre.meta["val"] = input_val.permute(to_last) + hadamard_node = graph.create_node( + "call_function", self.hadamard_target, (pre, scale) + ) + hadamard_node.meta = copy_meta(node.meta) + post = graph.create_node( + "call_function", + torch.ops.aten.permute.default, + (hadamard_node, from_last), + ) + post.meta = copy_meta(node.meta) + for user in node.users.copy(): + user.replace_input_with(node, post) + + def _is_hadamard_transform(self, graph_module, node): + if node.op != "call_function": + return False + + is_conv = node.target == torch.ops.aten.conv2d.default + is_last_dim = node.target in ( + torch.ops.aten.linear.default, + torch.ops.aten.matmul.default, + ) + if not (is_conv or is_last_dim): + return False + + # linear/conv carry an optional bias in args[2]; matmul never does. + has_bias = len(node.args) >= 3 and node.args[2] is not None + if has_bias: + return False + + weight_node = node.args[1] + if weight_node.op != "get_attr": + return False + weight = attrgetter(weight_node.target)(graph_module) + if is_conv and not self._is_pointwise_conv(node, weight): + return False + # A 1x1 conv filter is [out, in, 1, 1]; squeeze to [out, in] to match. + squeezed = weight.reshape(weight.shape[:2]) if is_conv else weight + if not _match_hadamard_weight(squeezed): + return False + return True + + def call(self, graph_module: torch.fx.GraphModule): + # HadamardTransform is only supported by QNN 2.47+. On older SDKs skip the + # rewrite so the op keeps its normal lowering path. + if is_qnn_sdk_version_less_than("2.47"): + return PassResult(graph_module, False) + + graph = graph_module.graph + modified = False + for node in graph.nodes: + if not self._is_hadamard_transform(graph_module, node): + continue + weight_node = node.args[1] + weight = attrgetter(weight_node.target)(graph_module) + is_conv = node.target == torch.ops.aten.conv2d.default + squeezed = weight.reshape(weight.shape[:2]) if is_conv else weight + scale = self._get_hadamard_scale(squeezed) + if is_conv: + self._rewrite_channel_dim(graph, node, scale) + else: + self._rewrite_last_dim(graph, node, scale) + modified = True + + if modified: + dead_code_elimination_pass(graph_module) + return PassResult(graph_module, modified) diff --git a/backends/qualcomm/builders/README.md b/backends/qualcomm/builders/README.md index 78f477b67ee..a90ea4be7a5 100644 --- a/backends/qualcomm/builders/README.md +++ b/backends/qualcomm/builders/README.md @@ -436,6 +436,7 @@ Please help update following table if you are contributing new operators: | GetSparseValues | ✗ | | GridSample | ✓ | | GroupNorm | ✓ | +| HadamardTransform | ✓ | | HardSwish | ✓ | | InstanceNorm | ✓ | | IsInf | ✓ | diff --git a/backends/qualcomm/builders/__init__.py b/backends/qualcomm/builders/__init__.py index e8c38f28bf2..f68dac483cd 100644 --- a/backends/qualcomm/builders/__init__.py +++ b/backends/qualcomm/builders/__init__.py @@ -5,6 +5,7 @@ # LICENSE file in the root directory of this source tree. from . import ( + custom_ops, node_visitor, op_abs, op_adaptive_avg_pool2d, @@ -49,6 +50,7 @@ op_grid_sampler_2d, op_group_norm, op_gt, + op_hadamard_transform, op_hardsigmoid, op_hardswish, op_hardtanh, @@ -121,6 +123,7 @@ ) __all__ = [ + custom_ops, node_visitor, op_abs, op_adaptive_avg_pool2d, @@ -165,6 +168,7 @@ op_grid_sampler_2d, op_group_norm, op_gt, + op_hadamard_transform, op_hardswish, op_hardtanh, op_hardsigmoid, diff --git a/backends/qualcomm/builders/custom_ops.py b/backends/qualcomm/builders/custom_ops.py new file mode 100644 index 00000000000..016e7bacd73 --- /dev/null +++ b/backends/qualcomm/builders/custom_ops.py @@ -0,0 +1,37 @@ +# Copyright (c) Qualcomm Innovation Center, Inc. +# All rights reserved +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +import torch +from torch.library import impl, Library, register_fake + +# Dedicated namespace, separate from the "qaisw" context-binary namespace. +hadamard_op_lib = Library("qnn_custom", "DEF") +hadamard_op_lib.define("hadamard_transform(Tensor input, float scale) -> Tensor") + + +def _hadamard_matrix(dim: int, device, dtype) -> torch.Tensor: + # Sylvester construction of the (unnormalized, ±1) Hadamard matrix. + h = torch.ones((1, 1), device=device, dtype=dtype) + while h.shape[0] < dim: + h = torch.cat([torch.cat([h, h], dim=1), torch.cat([h, -h], dim=1)], dim=0) + return h + + +@impl(hadamard_op_lib, "hadamard_transform", "CompositeExplicitAutograd") +def hadamard_transform_impl(input: torch.Tensor, scale: float) -> torch.Tensor: + # Normalized Walsh-Hadamard transform along the last dim, times scale. + # Matches a linear/matmul whose weight is scipy.linalg.hadamard(dim) * s, + # where the rewrite pass sets scale = s * sqrt(dim) (scale == 1 when the + # weight is the orthonormal H / sqrt(dim)). + dim = input.shape[-1] + h = _hadamard_matrix(dim, input.device, input.dtype) + return torch.matmul(input, h) * (scale / (dim**0.5)) + + +@register_fake("qnn_custom::hadamard_transform") +def hadamard_transform_fake(input: torch.Tensor, scale: float) -> torch.Tensor: + # Hadamard weight is square, so the transform preserves shape. + return torch.empty_like(input) diff --git a/backends/qualcomm/builders/op_hadamard_transform.py b/backends/qualcomm/builders/op_hadamard_transform.py new file mode 100644 index 00000000000..d39fdd601fd --- /dev/null +++ b/backends/qualcomm/builders/op_hadamard_transform.py @@ -0,0 +1,66 @@ +# Copyright (c) Qualcomm Innovation Center, Inc. +# All rights reserved +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +from typing import Dict + +import executorch.backends.qualcomm.python.PyQnnManagerAdaptor as PyQnnManager + +import numpy as np + +import torch +from executorch.backends.qualcomm.utils.constants import QCOM_DATA + +from .node_visitor import NodeVisitor +from .node_visitor_manager import register_node_visitor +from .qnn_constants import OpHadamardTransform, QNN_OP_PACKAGE_NAME_QTI_AISW + + +@register_node_visitor +class HadamardTransformVisitor(NodeVisitor): + target = ["qnn_custom.hadamard_transform.default"] + + def __init__(self, *args) -> None: + super().__init__(*args) + + def define_node( + self, + node: torch.fx.Node, + nodes_to_wrappers: Dict[torch.fx.Node, PyQnnManager.TensorWrapper], + ) -> PyQnnManager.PyQnnOpWrapper: + input_node = self.get_node(node.args[0]) + input_tensor = self.get_tensor(input_node, node) + input_tensor_wrapper = self.define_tensor( + input_node, + node, + input_tensor, + PyQnnManager.Qnn_TensorType_t.QNN_TENSOR_TYPE_NATIVE, + nodes_to_wrappers, + ) + + output_tensor = self.get_tensor(node, node) + output_tensor_wrapper = self.define_tensor( + node, + node, + output_tensor, + PyQnnManager.Qnn_TensorType_t.QNN_TENSOR_TYPE_NATIVE, + nodes_to_wrappers, + ) + + hadamard_op = PyQnnManager.PyQnnOpWrapper( + node.name, + QNN_OP_PACKAGE_NAME_QTI_AISW, + OpHadamardTransform.op_name, + ) + hadamard_op.AddInputTensors([input_tensor_wrapper]) + hadamard_op.AddOutputTensors([output_tensor_wrapper]) + + scale = node.args[1] + hadamard_op.AddScalarParam( + OpHadamardTransform.param_scale, + PyQnnManager.Qnn_DataType_t.QNN_DATATYPE_FLOAT_32, + {QCOM_DATA: np.float32(scale)}, + ) + return hadamard_op diff --git a/backends/qualcomm/builders/qnn_constants.py b/backends/qualcomm/builders/qnn_constants.py index d1f0d3fff00..6c18d78fdb5 100644 --- a/backends/qualcomm/builders/qnn_constants.py +++ b/backends/qualcomm/builders/qnn_constants.py @@ -382,6 +382,12 @@ class OpGroupNorm: param_group = "group" +@dataclass(init=False, frozen=True) +class OpHadamardTransform: + op_name: str = "HadamardTransform" + param_scale: str = "scale" + + @dataclass(init=False, frozen=True) class OpHardSwish: op_name: str = "HardSwish" diff --git a/backends/qualcomm/quantizer/annotators/htp_rules.py b/backends/qualcomm/quantizer/annotators/htp_rules.py index c68e855856e..80060223508 100644 --- a/backends/qualcomm/quantizer/annotators/htp_rules.py +++ b/backends/qualcomm/quantizer/annotators/htp_rules.py @@ -10,6 +10,9 @@ from functools import partial from typing import Dict, List, Optional, Sequence, Tuple +# Registers torch.ops.qnn_custom.hadamard_transform used by the annotator below. +import executorch.backends.qualcomm.builders.custom_ops # noqa: F401 + import executorch.backends.qualcomm.builders.qnn_constants as QnnConstants import torch @@ -1148,6 +1151,14 @@ def validate( return valid +@register_annotator( + [torch.ops.qnn_custom.hadamard_transform.default], + QnnConstants.OpHadamardTransform.op_name, +) +class HadamardTransform(GeneralOpDef): + pass + + @register_annotator( [torch.ops.aten.max.other, torch.ops.aten.maximum.default], QnnConstants.OpElementWiseMaximum.op_name, diff --git a/backends/qualcomm/tests/models.py b/backends/qualcomm/tests/models.py index 6d8002b7ec6..4dfc6084d32 100644 --- a/backends/qualcomm/tests/models.py +++ b/backends/qualcomm/tests/models.py @@ -4,8 +4,11 @@ # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. +import math from typing import List, Optional, Tuple, Union +import scipy.linalg + import torch # module with related operator only @@ -1682,6 +1685,50 @@ def forward(self, x): return self.linear(x) +class HadamardLinear(torch.nn.Module): + def __init__(self, dim): + super().__init__() + self.linear = torch.nn.Linear(dim, dim, bias=False).eval() + # nn.Linear computes x @ Wᵀ; the Hadamard matrix is symmetric so + # x @ Hᵀ == x @ H, matching hadamard_transform(x). + H = torch.from_numpy(scipy.linalg.hadamard(dim).astype("float32")) / math.sqrt( + dim + ) + self.linear.weight.data.copy_(H) + + def forward(self, x): + return self.linear(x) + + +class HadamardMatMul(torch.nn.Module): + def __init__(self, dim): + super().__init__() + # The Hadamard matrix is symmetric, so matmul(x, H) applies the transform + # along the last dim of x, matching hadamard_transform(x). + H = torch.from_numpy(scipy.linalg.hadamard(dim).astype("float32")) / math.sqrt( + dim + ) + self.register_buffer("weight", H) + + def forward(self, x): + return torch.matmul(x, self.weight) + + +class HadamardConv(torch.nn.Module): + def __init__(self, dim): + super().__init__() + # A 1x1 conv mixing channels is a matmul over the channel dim; a Hadamard + # filter makes it equivalent to hadamard_transform along channels. + self.conv = torch.nn.Conv2d(dim, dim, kernel_size=1, bias=False).eval() + H = torch.from_numpy(scipy.linalg.hadamard(dim).astype("float32")) / math.sqrt( + dim + ) + self.conv.weight.data.copy_(H.reshape(dim, dim, 1, 1)) + + def forward(self, x): + return self.conv(x) + + class LinearLeakyReLU(torch.nn.Module): def __init__(self, negative_slope=0.01): super().__init__() diff --git a/backends/qualcomm/tests/rework/conftest.py b/backends/qualcomm/tests/rework/conftest.py index 257166bd7c2..914211a56c8 100644 --- a/backends/qualcomm/tests/rework/conftest.py +++ b/backends/qualcomm/tests/rework/conftest.py @@ -476,10 +476,11 @@ def export_and_verify( quantizer: QnnQuantizer, compile_specs: List[Any], metrics: Metrics, + expected_targets: set = None, ): with calibrate(module, [inputs], quantizer) as exported_module: + nodes = {node.target for node in exported_module.graph.nodes} if quantizer is not None: - nodes = {node.target for node in exported_module.graph.nodes} q_and_dq = { torch.ops.quantized_decomposed.quantize_per_tensor.default, torch.ops.quantized_decomposed.dequantize_per_tensor.default, @@ -489,6 +490,10 @@ def export_and_verify( torch.ops.torchao.dequantize_affine.default, } assert nodes.intersection(q_and_dq), EXPECT_NOT_ANNOTATED + if expected_targets is not None: + assert ( + expected_targets <= nodes + ), f"expected {expected_targets - nodes} in exported graph" delegated_prog = to_edge_transform_and_lower_to_qnn( module=exported_module, diff --git a/backends/qualcomm/tests/rework/htp/op/v68/test.py b/backends/qualcomm/tests/rework/htp/op/v68/test.py index a9f27a653fe..6bc6a80e36b 100644 --- a/backends/qualcomm/tests/rework/htp/op/v68/test.py +++ b/backends/qualcomm/tests/rework/htp/op/v68/test.py @@ -731,6 +731,21 @@ def test_group_norm(request, kwargs): GroupNorm.test(request, kwargs) # noqa: F405 +# HadamardTransform is activation-16 only in QNN, so test 16a8w only. +@pytest.mark.parametrize( + "kwargs", + [ + pytest.param( + {"act": 16, "param": 8, "pcq": False, "expected": Tolerance()}, + id="16a8w", + ), + ], +) +@with_htp_context +def test_hadamard(request, kwargs): + Hadamard.test(request, kwargs) # noqa: F405 + + @enumerate_activation_dtype([Tolerance(), Tolerance(), Tolerance(rtol=1e-1)]) @with_htp_context def test_hardsigmoid(request, kwargs): diff --git a/backends/qualcomm/tests/rework/passes/test.py b/backends/qualcomm/tests/rework/passes/test.py index be11f2d1f47..922e4674e71 100644 --- a/backends/qualcomm/tests/rework/passes/test.py +++ b/backends/qualcomm/tests/rework/passes/test.py @@ -356,6 +356,12 @@ def test_lpai_partition_fallback_support(request, kwargs): LpaiPartitionFallbackSupport.test(request, kwargs) # noqa: F405 +@enumerate_backends([QnnExecuTorchBackendType.kHtpBackend]) +@repack_pass_fixtures +def test_recompose_hadamard(request, kwargs): + RecomposeHadamard.test(request, kwargs) # noqa: F405 + + @enumerate_backends() @repack_pass_fixtures def test_recompose_pad_maxpool2d(request, kwargs): diff --git a/backends/qualcomm/tests/rework/src/op.py b/backends/qualcomm/tests/rework/src/op.py index 841383d1423..06c71d48603 100644 --- a/backends/qualcomm/tests/rework/src/op.py +++ b/backends/qualcomm/tests/rework/src/op.py @@ -6,10 +6,16 @@ import inspect import itertools +import math import random from functools import partial, reduce from operator import mul +# Registers torch.ops.qnn_custom.hadamard_transform (asserted in Hadamard.test). +import executorch.backends.qualcomm.builders.custom_ops # noqa: F401 + +import scipy.linalg + import torch from executorch.backends.qualcomm.tests.rework.conftest import ( @@ -2210,6 +2216,53 @@ def test(subtests, qnn_config, quantizer, compile_spec, expected): ) +class Hadamard(torch.nn.Module): + # linear / matmul / 1x1-conv with an orthonormal Hadamard weight; the QNN backend + # rewrites each into a single qnn_custom.hadamard_transform during annotation. + def __init__(self, variant, dim): + super().__init__() + H = torch.from_numpy(scipy.linalg.hadamard(dim).astype("float32")) / math.sqrt( + dim + ) + if variant == "linear": + self.op = torch.nn.Linear(dim, dim, bias=False).eval() + self.op.weight.data.copy_(H) # symmetric H -> x@H^T == x@H + elif variant == "conv": + self.op = torch.nn.Conv2d(dim, dim, 1, bias=False).eval() + self.op.weight.data.copy_(H.reshape(dim, dim, 1, 1)) + else: # matmul + self.register_buffer("weight", H) + self.variant = variant + + def forward(self, x): + if self.variant == "matmul": + return torch.matmul(x, self.weight) + return self.op(x) + + @staticmethod + @unpack_fixtures + def test(subtests, qnn_config, quantizer, compile_spec, expected): + dim = 128 + cases = { + "linear": (torch.randn(1, dim),), + "matmul": (torch.randn(1, dim),), + "conv": (torch.randn(1, dim, 4, 4),), + } + target = torch.ops.qnn_custom.hadamard_transform.default + for variant, inputs in cases.items(): + with subtests.test(msg=f"variant:{variant}"): + with expected as metrics: + export_and_verify( + module=__class__(variant=variant, dim=dim), + inputs=inputs, + qnn_config=qnn_config, + quantizer=quantizer, + compile_specs=compile_spec, + metrics=metrics, + expected_targets={target}, + ) + + class HardSigmoid(torch.nn.Module): def __init__(self): super().__init__() diff --git a/backends/qualcomm/tests/rework/src/pattern.py b/backends/qualcomm/tests/rework/src/pattern.py index cd21bbfe08c..26884393384 100644 --- a/backends/qualcomm/tests/rework/src/pattern.py +++ b/backends/qualcomm/tests/rework/src/pattern.py @@ -7,10 +7,15 @@ from __future__ import annotations import inspect +import math import operator from typing import TYPE_CHECKING +# Registers torch.ops.qnn_custom.hadamard_transform (asserted in RecomposeHadamard.test). +import executorch.backends.qualcomm.builders.custom_ops # noqa: F401 + import pytest +import scipy.linalg import torch from executorch.backends.qualcomm import _passes @@ -22,6 +27,9 @@ check_exception, EXCEPTION_FROM_PASSES, ) +from executorch.backends.qualcomm.utils.check_qnn_version import ( + is_qnn_sdk_version_less_than, +) from executorch.backends.qualcomm.utils.constants import ( QCOM_AXIS_ORDER, QCOM_PASS_ACTIVATE_KEY, @@ -4130,6 +4138,116 @@ def test( ) +class RecomposeHadamard: + class _Linear(torch.nn.Module): + def __init__(self, weight: torch.Tensor, bias: bool = False): + super().__init__() + dim = weight.shape[0] + self.linear = torch.nn.Linear(dim, dim, bias=bias).eval() + # nn.Linear computes x @ Wᵀ; a Hadamard matrix is symmetric so + # x @ Hᵀ == x @ H, matching hadamard_transform(x). + self.linear.weight.data.copy_(weight) + + def forward(self, x): + return self.linear(x) + + class _MatMul(torch.nn.Module): + def __init__(self, weight: torch.Tensor): + super().__init__() + self.register_buffer("weight", weight) + + def forward(self, x): + return torch.matmul(x, self.weight) + + class _Conv(torch.nn.Module): + def __init__(self, weight: torch.Tensor, bias: bool = False): + super().__init__() + dim = weight.shape[0] + self.conv = torch.nn.Conv2d(dim, dim, kernel_size=1, bias=bias).eval() + self.conv.weight.data.copy_(weight.reshape(dim, dim, 1, 1)) + + def forward(self, x): + return self.conv(x) + + @staticmethod + @unpack_pass_fixtures + def test( + subtests, + backend_type: QnnExecuTorchBackendType, + assertions: Assertions, + pass_pipeline: PassPipeline, + ): + if is_qnn_sdk_version_less_than("2.47"): + pytest.skip("HadamardTransform requires QNN SDK 2.47 or newer") + + dim = 8 + target_pass = _passes.RecomposeHadamard + hadamard = torch.ops.qnn_custom.hadamard_transform.default + permute = torch.ops.aten.permute.default + H = torch.from_numpy(scipy.linalg.hadamard(dim).astype("float32")) / math.sqrt( + dim + ) + vector_input = (torch.randn(1, dim),) + image_input = (torch.randn(1, dim, 4, 4),) + non_hadamard = torch.randn(dim, dim) + # (label, module, source, inputs, exp_hadamard, exp_permute) + cases = [ + ( + "linear", + RecomposeHadamard._Linear(H), + torch.ops.aten.linear.default, + vector_input, + 1, + 0, + ), + ( + "matmul", + RecomposeHadamard._MatMul(H), + torch.ops.aten.matmul.default, + vector_input, + 1, + 0, + ), + # conv mixes the channel dim, so the rewrite is wrapped in permutes. + ( + "conv", + RecomposeHadamard._Conv(H), + torch.ops.aten.conv2d.default, + image_input, + 1, + 2, + ), + # A bias makes the op more than a plain Hadamard product. + ( + "linear_bias", + RecomposeHadamard._Linear(H, bias=True), + torch.ops.aten.linear.default, + vector_input, + 0, + 0, + ), + ( + "non_hadamard", + RecomposeHadamard._Linear(non_hadamard), + torch.ops.aten.linear.default, + vector_input, + 0, + 0, + ), + ] + for label, module, source, inputs, exp_hadamard, exp_permute in cases: + with subtests.test(msg=label): + gm = pass_pipeline.lower_annotation_gm( + module=module, + sample_input=inputs, + target_pass=target_pass, + backend_type=backend_type, + ) + assertions.assert_target_count(gm, hadamard, exp_hadamard) + assertions.assert_target_count(gm, permute, exp_permute) + assertions.assert_target_count(gm, source, 0 if exp_hadamard else 1) + + class RecomposePadMaxPool2d: class _MaxPoolPadded(torch.nn.Module): def __init__(self): diff --git a/backends/qualcomm/tests/test_qnn_delegate.py b/backends/qualcomm/tests/test_qnn_delegate.py index 1dd67fbd920..1aafaf4c2ce 100644 --- a/backends/qualcomm/tests/test_qnn_delegate.py +++ b/backends/qualcomm/tests/test_qnn_delegate.py @@ -4288,6 +4288,188 @@ def test_qnn_backend_group_norm(self): module = self.get_qdq_module(module, sample_input) self.lower_module_and_test_output(module, sample_input) + @unittest.skipIf( + is_qnn_sdk_version_less_than("2.47"), + "UT pass after QNN 2.47.", + ) + def test_qnn_backend_hadamard_transform_linear(self): + if get_backend_type(self.backend) != QnnExecuTorchBackendType.kHtpBackend: + self.skipTest("The op is only supported on HTP") + if self.enable_x86_64: + self.skipTest( + "At the moment, testing is only being conducted on the device." + ) + # A failed Hadamard match silently falls back to FullyConnected and still + # produces correct outputs, so output parity alone can't confirm the + # fast-path was taken. Inspect the QHAS op types from optrace and assert + # HadamardTransform appears. + sample_inputs = [ + (torch.randn([1, 128]),), + (torch.randn([1, 4, 128]),), + (torch.randn([1, 2, 4, 128]),), + ] + for sample_input, per_channel in itertools.product( + sample_inputs, (False, True) + ): + with self.subTest( + ndim=sample_input[0].dim(), + per_channel=per_channel, + ): + module = HadamardLinear(dim=128) # noqa: F405 + module = self.get_qdq_module( + module, + sample_input, + is_linear_per_channel=per_channel, + quant_dtype=QuantDtype.use_16a8w, + ) + backend_options = generate_htp_compiler_spec(use_fp16=False) + compiler_spec = generate_qnn_executorch_compiler_spec( + soc_model=self.chipset_table[TestQNN.soc_model], + backend_options=backend_options, + profile_level=3, + ) + with tempfile.TemporaryDirectory() as tmp_dir: + edge_prog_mgr = to_edge_transform_and_lower_to_qnn( + module, sample_input, compiler_spec + ).to_executorch() + pte_path = f"{tmp_dir}/model.pte" + with open(pte_path, "wb") as f: + edge_prog_mgr.write_to_file(f) + adb = self.get_adb_tool(pte_path) + binaries_trace = generate_optrace( + tmp_dir, + self.chipset_table[TestQNN.soc_model], + adb, + pte_path, + [sample_input], + ) + htp_ops = [] + for _, (_, qhas) in binaries_trace.items(): + with open(qhas, "r") as qhas_file: + qhas_data = json.load(qhas_file) + for row in qhas_data["data"]["qnn_op_types"]["data"]: + htp_ops.append(row["op"]) + self.assertTrue( + any("HadamardTransform" in op for op in htp_ops), + "Expected linear to be lowered to HadamardTransform " + f"(likely fell back to FullyConnected), got: {htp_ops}", + ) + self.verify_output(module, sample_input, edge_prog_mgr) + + @unittest.skipIf( + is_qnn_sdk_version_less_than("2.47"), + "UT pass after QNN 2.47.", + ) + def test_qnn_backend_hadamard_transform_matmul(self): + if get_backend_type(self.backend) != QnnExecuTorchBackendType.kHtpBackend: + self.skipTest("The op is only supported on HTP") + if self.enable_x86_64: + self.skipTest( + "At the moment, testing is only being conducted on the device." + ) + # A failed Hadamard match silently falls back to MatMul and still produces + # correct outputs, so inspect the QHAS op types and assert HadamardTransform. + sample_inputs = [ + (torch.randn([1, 128]),), + (torch.randn([1, 4, 128]),), + (torch.randn([1, 2, 4, 128]),), + ] + for sample_input in sample_inputs: + with self.subTest(ndim=sample_input[0].dim()): + module = HadamardMatMul(dim=128) # noqa: F405 + module = self.get_qdq_module( + module, + sample_input, + quant_dtype=QuantDtype.use_16a8w, + ) + backend_options = generate_htp_compiler_spec(use_fp16=False) + compiler_spec = generate_qnn_executorch_compiler_spec( + soc_model=self.chipset_table[TestQNN.soc_model], + backend_options=backend_options, + profile_level=3, + ) + with tempfile.TemporaryDirectory() as tmp_dir: + edge_prog_mgr = to_edge_transform_and_lower_to_qnn( + module, sample_input, compiler_spec + ).to_executorch() + pte_path = f"{tmp_dir}/model.pte" + with open(pte_path, "wb") as f: + edge_prog_mgr.write_to_file(f) + adb = self.get_adb_tool(pte_path) + binaries_trace = generate_optrace( + tmp_dir, + self.chipset_table[TestQNN.soc_model], + adb, + pte_path, + [sample_input], + ) + htp_ops = [] + for _, (_, qhas) in binaries_trace.items(): + with open(qhas, "r") as qhas_file: + qhas_data = json.load(qhas_file) + for row in qhas_data["data"]["qnn_op_types"]["data"]: + htp_ops.append(row["op"]) + self.assertTrue( + any("HadamardTransform" in op for op in htp_ops), + "Expected matmul to be lowered to HadamardTransform " + f"(likely fell back to MatMul), got: {htp_ops}", + ) + self.verify_output(module, sample_input, edge_prog_mgr) + + @unittest.skipIf( + is_qnn_sdk_version_less_than("2.47"), + "UT pass after QNN 2.47.", + ) + def test_qnn_backend_hadamard_transform_conv(self): + if get_backend_type(self.backend) != QnnExecuTorchBackendType.kHtpBackend: + self.skipTest("The op is only supported on HTP") + if self.enable_x86_64: + self.skipTest( + "At the moment, testing is only being conducted on the device." + ) + # A failed Hadamard match silently falls back to Conv and still produces + # correct outputs, so inspect the QHAS op types and assert HadamardTransform. + sample_input = (torch.randn([1, 128, 4, 4]),) + module = HadamardConv(dim=128) # noqa: F405 + module = self.get_qdq_module( + module, + sample_input, + quant_dtype=QuantDtype.use_16a8w, + ) + backend_options = generate_htp_compiler_spec(use_fp16=False) + compiler_spec = generate_qnn_executorch_compiler_spec( + soc_model=self.chipset_table[TestQNN.soc_model], + backend_options=backend_options, + profile_level=3, + ) + with tempfile.TemporaryDirectory() as tmp_dir: + edge_prog_mgr = to_edge_transform_and_lower_to_qnn( + module, sample_input, compiler_spec + ).to_executorch() + pte_path = f"{tmp_dir}/model.pte" + with open(pte_path, "wb") as f: + edge_prog_mgr.write_to_file(f) + adb = self.get_adb_tool(pte_path) + binaries_trace = generate_optrace( + tmp_dir, + self.chipset_table[TestQNN.soc_model], + adb, + pte_path, + [sample_input], + ) + htp_ops = [] + for _, (_, qhas) in binaries_trace.items(): + with open(qhas, "r") as qhas_file: + qhas_data = json.load(qhas_file) + for row in qhas_data["data"]["qnn_op_types"]["data"]: + htp_ops.append(row["op"]) + self.assertTrue( + any("HadamardTransform" in op for op in htp_ops), + "Expected conv to be lowered to HadamardTransform " + f"(likely fell back to Conv), got: {htp_ops}", + ) + self.verify_output(module, sample_input, edge_prog_mgr) + def test_qnn_backend_hardsigmoid(self): module = HardSigmoid() # noqa: F405 sample_input = (torch.randn(2, 5, 1, 3),) diff --git a/examples/models/llama/hf_download.py b/examples/models/llama/hf_download.py index fbc4240619b..6c2c115ad4c 100644 --- a/examples/models/llama/hf_download.py +++ b/examples/models/llama/hf_download.py @@ -5,6 +5,8 @@ # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. +import os + from pathlib import Path from typing import Callable @@ -28,7 +30,8 @@ def download_and_convert_hf_checkpoint( # Build cache path. cache_subdir = "meta_checkpoints" - cache_dir = Path.home() / ".cache" / cache_subdir + home_dir = Path(os.environ.get("HF_HOME", Path.home())) + cache_dir = home_dir / ".cache" / cache_subdir cache_dir.mkdir(parents=True, exist_ok=True) # Use repo name to name the converted file. diff --git a/examples/qualcomm/oss_scripts/llama/evaluator/device_evaluator.py b/examples/qualcomm/oss_scripts/llama/evaluator/device_evaluator.py index e04724c3307..0398c5556f0 100644 --- a/examples/qualcomm/oss_scripts/llama/evaluator/device_evaluator.py +++ b/examples/qualcomm/oss_scripts/llama/evaluator/device_evaluator.py @@ -323,6 +323,7 @@ def __init__( self.modality_input_files = [] def run(self, prompt, audio_paths=None, image_paths=None): + prompt = [p.replace('"', '\\"') for p in prompt] multi_prompts = " ".join([f'--prompt "{p}"' for p in prompt]) model_output_holder = [] From 379615865c1b556a839ab29136703989a422c9f9 Mon Sep 17 00:00:00 2001 From: Cheng-Hsin Weng Date: Wed, 12 Aug 2026 11:53:28 +0800 Subject: [PATCH 2/3] Qualcomm AI Engine Direct - Drop scipy dependency for Hadamard matcher _passes/__init__.py imports RecomposeHadamard eagerly, so the module-level `import scipy.linalg` would raise ModuleNotFoundError for every QNN export on hosts without scipy (it isn't a project dependency). Reuse the existing torch-based `_hadamard_matrix` Sylvester construction from builders/custom_ops.py instead, and do the same in the test files that had duplicated the scipy-based construction to build reference Hadamard weights. Co-Authored-By: Claude Sonnet 5 --- .../qualcomm/_passes/recompose_hadamard.py | 20 ++++++++----------- backends/qualcomm/tests/models.py | 14 ++++--------- backends/qualcomm/tests/rework/src/op.py | 10 +++------- backends/qualcomm/tests/rework/src/pattern.py | 9 +++------ 4 files changed, 18 insertions(+), 35 deletions(-) diff --git a/backends/qualcomm/_passes/recompose_hadamard.py b/backends/qualcomm/_passes/recompose_hadamard.py index d398b0ada22..880d57f0ade 100644 --- a/backends/qualcomm/_passes/recompose_hadamard.py +++ b/backends/qualcomm/_passes/recompose_hadamard.py @@ -5,14 +5,10 @@ # LICENSE file in the root directory of this source tree. from operator import attrgetter -# Registers torch.ops.qnn_custom.hadamard_transform. -import executorch.backends.qualcomm.builders.custom_ops # noqa: F401 - -import numpy as np - -import scipy.linalg import torch +# Also registers torch.ops.qnn_custom.hadamard_transform. +from executorch.backends.qualcomm.builders.custom_ops import _hadamard_matrix from executorch.backends.qualcomm.utils.check_qnn_version import ( is_qnn_sdk_version_less_than, ) @@ -31,21 +27,21 @@ def _is_power_of_2_sqare_matrix(weight: torch.Tensor) -> bool: def _match_hadamard_weight(weight: torch.Tensor) -> bool: - # Returns True if `weight == scipy.linalg.hadamard(dim) * s` for some scale s. + # Returns True if `weight == _hadamard_matrix(dim) * s` for some scale s. # A linear/matmul with such a weight is equivalent to a QNN HadamardTransform. if _is_power_of_2_sqare_matrix(weight): return False - w = weight.detach().to(torch.float64).numpy() + w = weight.detach().to(torch.float64) nonzero = w[w != 0] - if nonzero.size == 0: + if nonzero.numel() == 0: return False # The Hadamard weight is H * s for a single global scale s; infer s from any # nonzero entry (all |H_ij| == 1). For per-channel quant this only matches # when every channel's dequantized scale reconstructs the same H * s. - scale = float(abs(nonzero.flat[0])) - hadamard = scipy.linalg.hadamard(weight.shape[0]).astype("float64") * scale - return np.allclose(w, hadamard, rtol=0, atol=1e-4) + scale = float(nonzero.flatten()[0].abs()) + hadamard = _hadamard_matrix(w.shape[0], w.device, w.dtype) * scale + return torch.allclose(w, hadamard, rtol=0, atol=1e-4) class RecomposeHadamard(ExportPass): diff --git a/backends/qualcomm/tests/models.py b/backends/qualcomm/tests/models.py index 4dfc6084d32..18b1395e6f6 100644 --- a/backends/qualcomm/tests/models.py +++ b/backends/qualcomm/tests/models.py @@ -7,7 +7,7 @@ import math from typing import List, Optional, Tuple, Union -import scipy.linalg +from executorch.backends.qualcomm.builders.custom_ops import _hadamard_matrix import torch @@ -1691,9 +1691,7 @@ def __init__(self, dim): self.linear = torch.nn.Linear(dim, dim, bias=False).eval() # nn.Linear computes x @ Wᵀ; the Hadamard matrix is symmetric so # x @ Hᵀ == x @ H, matching hadamard_transform(x). - H = torch.from_numpy(scipy.linalg.hadamard(dim).astype("float32")) / math.sqrt( - dim - ) + H = _hadamard_matrix(dim, "cpu", torch.float32) / math.sqrt(dim) self.linear.weight.data.copy_(H) def forward(self, x): @@ -1705,9 +1703,7 @@ def __init__(self, dim): super().__init__() # The Hadamard matrix is symmetric, so matmul(x, H) applies the transform # along the last dim of x, matching hadamard_transform(x). - H = torch.from_numpy(scipy.linalg.hadamard(dim).astype("float32")) / math.sqrt( - dim - ) + H = _hadamard_matrix(dim, "cpu", torch.float32) / math.sqrt(dim) self.register_buffer("weight", H) def forward(self, x): @@ -1720,9 +1716,7 @@ def __init__(self, dim): # A 1x1 conv mixing channels is a matmul over the channel dim; a Hadamard # filter makes it equivalent to hadamard_transform along channels. self.conv = torch.nn.Conv2d(dim, dim, kernel_size=1, bias=False).eval() - H = torch.from_numpy(scipy.linalg.hadamard(dim).astype("float32")) / math.sqrt( - dim - ) + H = _hadamard_matrix(dim, "cpu", torch.float32) / math.sqrt(dim) self.conv.weight.data.copy_(H.reshape(dim, dim, 1, 1)) def forward(self, x): diff --git a/backends/qualcomm/tests/rework/src/op.py b/backends/qualcomm/tests/rework/src/op.py index 06c71d48603..9311afd7627 100644 --- a/backends/qualcomm/tests/rework/src/op.py +++ b/backends/qualcomm/tests/rework/src/op.py @@ -11,10 +11,8 @@ from functools import partial, reduce from operator import mul -# Registers torch.ops.qnn_custom.hadamard_transform (asserted in Hadamard.test). -import executorch.backends.qualcomm.builders.custom_ops # noqa: F401 - -import scipy.linalg +# Also registers torch.ops.qnn_custom.hadamard_transform (asserted in Hadamard.test). +from executorch.backends.qualcomm.builders.custom_ops import _hadamard_matrix import torch @@ -2221,9 +2219,7 @@ class Hadamard(torch.nn.Module): # rewrites each into a single qnn_custom.hadamard_transform during annotation. def __init__(self, variant, dim): super().__init__() - H = torch.from_numpy(scipy.linalg.hadamard(dim).astype("float32")) / math.sqrt( - dim - ) + H = _hadamard_matrix(dim, "cpu", torch.float32) / math.sqrt(dim) if variant == "linear": self.op = torch.nn.Linear(dim, dim, bias=False).eval() self.op.weight.data.copy_(H) # symmetric H -> x@H^T == x@H diff --git a/backends/qualcomm/tests/rework/src/pattern.py b/backends/qualcomm/tests/rework/src/pattern.py index 26884393384..9ee109453d5 100644 --- a/backends/qualcomm/tests/rework/src/pattern.py +++ b/backends/qualcomm/tests/rework/src/pattern.py @@ -11,11 +11,10 @@ import operator from typing import TYPE_CHECKING -# Registers torch.ops.qnn_custom.hadamard_transform (asserted in RecomposeHadamard.test). -import executorch.backends.qualcomm.builders.custom_ops # noqa: F401 +# Also registers torch.ops.qnn_custom.hadamard_transform (asserted in RecomposeHadamard.test). +from executorch.backends.qualcomm.builders.custom_ops import _hadamard_matrix import pytest -import scipy.linalg import torch from executorch.backends.qualcomm import _passes @@ -4184,9 +4183,7 @@ def test( target_pass = _passes.RecomposeHadamard hadamard = torch.ops.qnn_custom.hadamard_transform.default permute = torch.ops.aten.permute.default - H = torch.from_numpy(scipy.linalg.hadamard(dim).astype("float32")) / math.sqrt( - dim - ) + H = _hadamard_matrix(dim, "cpu", torch.float32) / math.sqrt(dim) vector_input = (torch.randn(1, dim),) image_input = (torch.randn(1, dim, 4, 4),) non_hadamard = torch.randn(dim, dim) From a356d89f549d4d8a427689b7e3339a58e972c3d1 Mon Sep 17 00:00:00 2001 From: Cheng-Hsin Weng Date: Wed, 12 Aug 2026 13:44:18 +0800 Subject: [PATCH 3/3] fix lint --- backends/qualcomm/tests/models.py | 4 ++-- backends/qualcomm/tests/rework/src/op.py | 4 ++-- backends/qualcomm/tests/rework/src/pattern.py | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/backends/qualcomm/tests/models.py b/backends/qualcomm/tests/models.py index 18b1395e6f6..3865f5c0d57 100644 --- a/backends/qualcomm/tests/models.py +++ b/backends/qualcomm/tests/models.py @@ -7,10 +7,10 @@ import math from typing import List, Optional, Tuple, Union -from executorch.backends.qualcomm.builders.custom_ops import _hadamard_matrix - import torch +from executorch.backends.qualcomm.builders.custom_ops import _hadamard_matrix + # module with related operator only diff --git a/backends/qualcomm/tests/rework/src/op.py b/backends/qualcomm/tests/rework/src/op.py index 9311afd7627..a2b41df494f 100644 --- a/backends/qualcomm/tests/rework/src/op.py +++ b/backends/qualcomm/tests/rework/src/op.py @@ -11,11 +11,11 @@ from functools import partial, reduce from operator import mul +import torch + # Also registers torch.ops.qnn_custom.hadamard_transform (asserted in Hadamard.test). from executorch.backends.qualcomm.builders.custom_ops import _hadamard_matrix -import torch - from executorch.backends.qualcomm.tests.rework.conftest import ( export_and_verify, temp_attribute, diff --git a/backends/qualcomm/tests/rework/src/pattern.py b/backends/qualcomm/tests/rework/src/pattern.py index 9ee109453d5..14850025411 100644 --- a/backends/qualcomm/tests/rework/src/pattern.py +++ b/backends/qualcomm/tests/rework/src/pattern.py @@ -11,13 +11,13 @@ import operator from typing import TYPE_CHECKING -# Also registers torch.ops.qnn_custom.hadamard_transform (asserted in RecomposeHadamard.test). -from executorch.backends.qualcomm.builders.custom_ops import _hadamard_matrix - import pytest import torch from executorch.backends.qualcomm import _passes + +# Also registers torch.ops.qnn_custom.hadamard_transform (asserted in RecomposeHadamard.test). +from executorch.backends.qualcomm.builders.custom_ops import _hadamard_matrix from executorch.backends.qualcomm.builders.node_visitor import dq_ops, q_ops from executorch.backends.qualcomm.serialization.qc_schema import ( QnnExecuTorchBackendType,