diff --git a/backends/qualcomm/tests/rework/common/pass/test.py b/backends/qualcomm/tests/rework/common/pass/test.py
deleted file mode 100644
index b5f86874fd4..00000000000
--- a/backends/qualcomm/tests/rework/common/pass/test.py
+++ /dev/null
@@ -1,5 +0,0 @@
-# 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.
diff --git a/backends/qualcomm/tests/rework/common/utils/test.py b/backends/qualcomm/tests/rework/common/utils/test.py
deleted file mode 100644
index b5f86874fd4..00000000000
--- a/backends/qualcomm/tests/rework/common/utils/test.py
+++ /dev/null
@@ -1,5 +0,0 @@
-# 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.
diff --git a/backends/qualcomm/tests/rework/conftest.py b/backends/qualcomm/tests/rework/conftest.py
index 9655aee8650..29b5e16c79b 100644
--- a/backends/qualcomm/tests/rework/conftest.py
+++ b/backends/qualcomm/tests/rework/conftest.py
@@ -18,6 +18,7 @@
from abc import ABC, abstractmethod
from collections import defaultdict
from contextlib import contextmanager
+from dataclasses import dataclass
from functools import partial
from typing import Any, List, Tuple
@@ -53,6 +54,7 @@
# et framework messages
EXCEPTION_EXIR_PROGRAM = "exir/program"
EXCEPTION_FROM_PASSES = "backends/qualcomm/_passes"
+EXCEPTION_FROM_PREPROCESS = "backends/qualcomm/qnn_preprocess"
def check_exception(msg):
@@ -62,6 +64,15 @@ def _check(msg, _: Exception):
return partial(_check, msg)
+# extend this for backend agnostic tests
+def default_property():
+ @dataclass
+ class Property:
+ soc_model: str = "SM8750"
+
+ return Property()
+
+
class Metrics(ABC):
@abstractmethod
def __init__(self):
diff --git a/backends/qualcomm/tests/rework/htp/op/v68/test.py b/backends/qualcomm/tests/rework/htp/op/v68/test.py
index 19a7843de65..12c46553475 100644
--- a/backends/qualcomm/tests/rework/htp/op/v68/test.py
+++ b/backends/qualcomm/tests/rework/htp/op/v68/test.py
@@ -121,6 +121,24 @@ def test_add(request, kwargs):
Add.test(request, kwargs) # noqa: F405
+@enumerate_activation_dtype(
+ [
+ Tolerance(),
+ pytest.raises(AssertionError, match=EXPECT_NOT_FULLY_DELEGATED),
+ Tolerance(rtol=1e-1),
+ ]
+)
+@with_htp_context
+def test_addmm(request, kwargs):
+ AddMM.test(request, kwargs) # noqa: F405
+
+
+@enumerate_activation_dtype([Tolerance(), Tolerance(), Tolerance(rtol=1e-1)])
+@with_htp_context
+def test_alias(request, kwargs):
+ Alias.test(request, kwargs) # noqa: F405
+
+
@enumerate_activation_dtype([Tolerance(), Tolerance(), Tolerance(rtol=1e-1)])
@with_htp_context
def test_amax(request, kwargs):
@@ -464,6 +482,10 @@ def test_conv2d_transpose(request, kwargs):
},
id="16a4w_lpbq",
),
+ pytest.param(
+ {"act": "fp16", "param": 8, "pcq": True, "expected": Tolerance()},
+ id="fp16a8w_pcq",
+ ),
],
)
@with_htp_context
@@ -527,6 +549,12 @@ def test_div(request, kwargs):
Div.test(request, kwargs) # noqa: F405
+@enumerate_activation_dtype([Tolerance(), Tolerance(), Tolerance(rtol=1e-1)])
+@with_htp_context
+def test_div_with_rounding_mode(request, kwargs):
+ DivWithRoundingMode.test(request, kwargs) # noqa: F405
+
+
@enumerate_activation_dtype(
[
Tolerance(),
@@ -595,6 +623,12 @@ def test_expm1(request, kwargs):
ExpM1.test(request, kwargs) # noqa: F405
+@enumerate_activation_dtype([Tolerance(), Tolerance(), Tolerance(rtol=1e-1)])
+@with_htp_context
+def test_fill(request, kwargs):
+ Fill.test(request, kwargs) # noqa: F405
+
+
@enumerate_activation_dtype([Tolerance(), Tolerance(), Tolerance(rtol=1e-1)])
@with_htp_context
def test_flip(request, kwargs):
@@ -857,6 +891,14 @@ def test_linear_block_quant(request, kwargs):
{"act": 16, "param": 8, "pcq": True, "expected": Tolerance()},
id="16a8w_pcq",
),
+ pytest.param(
+ {"act": "fp16", "param": 8, "pcq": True, "expected": Tolerance()},
+ id="fp16a8w_pcq",
+ ),
+ pytest.param(
+ {"act": 16, "param": 2, "pcq": True, "expected": CosineSimilarity(0.9)},
+ id="16a2w_pcq",
+ ),
pytest.param(
{
"act": None,
@@ -1177,12 +1219,24 @@ def test_sdpa(request, kwargs):
ScaledDotProductAttention.test(request, kwargs) # noqa: F405
+@enumerate_activation_dtype([Tolerance(), Tolerance(), Tolerance(rtol=1e-1)])
+@with_htp_context
+def test_scatter_src(request, kwargs):
+ ScatterSrc.test(request, kwargs) # noqa: F405
+
+
@enumerate_activation_dtype([Tolerance(), Tolerance(), Tolerance(rtol=1e-1)])
@with_htp_context
def test_select_copy(request, kwargs):
SelectCopy.test(request, kwargs) # noqa: F405
+@enumerate_activation_dtype([Tolerance(), Tolerance(), Tolerance(rtol=1e-1)])
+@with_htp_context
+def test_select_scatter(request, kwargs):
+ SelectScatter.test(request, kwargs) # noqa: F405
+
+
@enumerate_activation_dtype([Tolerance(), Tolerance(), Tolerance(rtol=1e-1)])
@with_htp_context
def test_sigmoid(request, kwargs):
@@ -1255,6 +1309,12 @@ def test_swapaxes(request, kwargs):
SwapAxes.test(request, kwargs) # noqa: F405
+@enumerate_activation_dtype([Tolerance(), Tolerance(), Tolerance(rtol=1e-1)])
+@with_htp_context
+def test_tan(request, kwargs):
+ Tan.test(request, kwargs) # noqa: F405
+
+
@enumerate_activation_dtype([Tolerance(), Tolerance(), Tolerance(rtol=1e-1)])
@with_htp_context
def test_tanh(request, kwargs):
@@ -1355,3 +1415,9 @@ def test_view_5d_flatten_last_two_dims(request, kwargs):
@with_htp_context
def test_where(request, kwargs):
Where.test(request, kwargs) # noqa: F405
+
+
+@enumerate_activation_dtype([Tolerance(), Tolerance(), Tolerance(rtol=1e-1)])
+@with_htp_context
+def test_var(request, kwargs):
+ Var.test(request, kwargs) # noqa: F405
diff --git a/backends/qualcomm/tests/rework/src/op.py b/backends/qualcomm/tests/rework/src/op.py
index 14d853ed919..c87eb7d97bd 100644
--- a/backends/qualcomm/tests/rework/src/op.py
+++ b/backends/qualcomm/tests/rework/src/op.py
@@ -257,6 +257,58 @@ def test(subtests, qnn_config, quantizer, compile_spec, expected):
)
+class AddMM(torch.nn.Module):
+ def __init__(self, alpha, beta):
+ super().__init__()
+ self.alpha = alpha
+ self.beta = beta
+
+ def forward(self, bias, input, mat2):
+ return torch.addmm(bias, input, mat2, alpha=self.alpha, beta=self.beta)
+
+ @staticmethod
+ @unpack_fixtures
+ def test(subtests, qnn_config, quantizer, compile_spec, expected):
+ for alpha, beta in [(1, 2), (2, 1), (2, 3)]:
+ with subtests.test(msg=f"alpha={alpha}, beta={beta}"):
+ with expected as metrics:
+ export_and_verify(
+ module=__class__(alpha=alpha, beta=beta),
+ inputs=(
+ torch.randn(8),
+ torch.randn(4, 3),
+ torch.randn(3, 8),
+ ),
+ qnn_config=qnn_config,
+ quantizer=quantizer,
+ compile_specs=compile_spec,
+ metrics=metrics,
+ )
+
+
+class Alias(torch.nn.Module):
+ def __init__(self):
+ super().__init__()
+ self.relu = torch.nn.ReLU()
+
+ def forward(self, x):
+ alias_x = torch.ops.aten.alias.default(x)
+ return self.relu(alias_x)
+
+ @staticmethod
+ @unpack_fixtures
+ def test(qnn_config, quantizer, compile_spec, expected):
+ with expected as metrics:
+ export_and_verify(
+ module=__class__(),
+ inputs=(torch.randn(1, 10),),
+ qnn_config=qnn_config,
+ quantizer=quantizer,
+ compile_specs=compile_spec,
+ metrics=metrics,
+ )
+
+
class AMax(torch.nn.Module):
def __init__(self, dim, keepdim):
super().__init__()
@@ -1398,6 +1450,39 @@ def test(subtests, qnn_config, quantizer, compile_spec, expected):
)
+class DivWithRoundingMode(torch.nn.Module):
+ def __init__(self, rounding_mode):
+ super().__init__()
+ self.rounding_mode = rounding_mode
+ self.scalar = 2.0
+
+ def forward(self, *x):
+ return (
+ torch.div(x[0], x[1], rounding_mode=self.rounding_mode)
+ if len(x) > 1
+ else torch.div(x[0], self.scalar, rounding_mode=self.rounding_mode)
+ )
+
+ @staticmethod
+ @unpack_fixtures
+ def test(subtests, qnn_config, quantizer, compile_spec, expected):
+ inputs = [
+ (torch.randn(2, 3), torch.randn(2, 3).abs() + 1e-6),
+ (torch.randn(2, 3),),
+ ]
+ for input, mode in itertools.product(inputs, [None, "trunc", "floor"]):
+ with subtests.test(msg=f"rounding_mode={mode}"):
+ with expected as metrics:
+ export_and_verify(
+ module=__class__(rounding_mode=mode),
+ inputs=input,
+ qnn_config=qnn_config,
+ quantizer=quantizer,
+ compile_specs=compile_spec,
+ metrics=metrics,
+ )
+
+
class Einsum(torch.nn.Module):
def __init__(self, equation):
super().__init__()
@@ -1623,6 +1708,28 @@ def test(qnn_config, quantizer, compile_spec, expected):
)
+class Fill(torch.nn.Module):
+ def __init__(self, value):
+ super().__init__()
+ self.value = value
+
+ def forward(self, x):
+ return torch.add(x, torch.fill(x, self.value))
+
+ @staticmethod
+ @unpack_fixtures
+ def test(qnn_config, quantizer, compile_spec, expected):
+ with expected as metrics:
+ export_and_verify(
+ module=__class__(value=3.14),
+ inputs=(torch.randn(1, 2, 3, 4),),
+ qnn_config=qnn_config,
+ quantizer=quantizer,
+ compile_specs=compile_spec,
+ metrics=metrics,
+ )
+
+
class Flip(torch.nn.Module):
def __init__(self, dims):
super().__init__()
@@ -4009,6 +4116,40 @@ def test(subtests, qnn_config, quantizer, compile_spec, expected):
)
+class ScatterSrc(torch.nn.Module):
+ def __init__(self, dim):
+ super().__init__()
+ self.dim = dim
+
+ def forward(self, data, index, src):
+ return torch.scatter(data, self.dim, index, src)
+
+ @staticmethod
+ @unpack_fixtures
+ def test(subtests, qnn_config, quantizer, compile_spec, expected):
+ inputs = [
+ (
+ torch.zeros(3, 5),
+ torch.tensor(
+ [[0, 1, 2, 3, 4], [4, 3, 2, 1, 0], [1, 0, 3, 4, 2]],
+ dtype=torch.int64,
+ ),
+ torch.randn(3, 5),
+ ),
+ ]
+ for dim, (data, index, src) in itertools.product([-1, 1], inputs):
+ with subtests.test(msg=f"dim={dim}"):
+ with expected as metrics:
+ export_and_verify(
+ module=__class__(dim=dim),
+ inputs=(data, index, src),
+ qnn_config=qnn_config,
+ quantizer=quantizer,
+ compile_specs=compile_spec,
+ metrics=metrics,
+ )
+
+
class ScaledDotProductAttention(torch.nn.Module):
def __init__(self, scale, is_causal, use_mask):
super().__init__()
@@ -4075,6 +4216,38 @@ def test(subtests, qnn_config, quantizer, compile_spec, expected):
)
+class SelectScatter(torch.nn.Module):
+ def __init__(self, dim, index):
+ super().__init__()
+ self.dim = dim
+ self.index = index
+
+ def forward(self, x, y):
+ return x.select_scatter(y, dim=self.dim, index=self.index)
+
+ @staticmethod
+ @unpack_fixtures
+ def test(subtests, qnn_config, quantizer, compile_spec, expected):
+ cases = [
+ (0, 2, (torch.randn(4, 8), torch.randn(8))),
+ (1, 0, (torch.randn(3, 4, 5), torch.randn(3, 5))),
+ (1, -1, (torch.randn(3, 4, 5), torch.randn(3, 5))),
+ (-1, 2, (torch.randn(3, 4, 5), torch.randn(3, 4))),
+ (3, 1, (torch.randn(2, 3, 4, 5), torch.randn(2, 3, 4))),
+ ]
+ for dim, index, inputs in cases:
+ with subtests.test(msg=f"dim={dim}_index={index}"):
+ with expected as metrics:
+ export_and_verify(
+ module=__class__(dim=dim, index=index),
+ inputs=inputs,
+ qnn_config=qnn_config,
+ quantizer=quantizer,
+ compile_specs=compile_spec,
+ metrics=metrics,
+ )
+
+
class SelectCopy(torch.nn.Module):
def __init__(self, dim, index):
super().__init__()
@@ -4509,6 +4682,27 @@ def test(subtests, qnn_config, quantizer, compile_spec, expected):
)
+class Tan(torch.nn.Module):
+ def __init__(self):
+ super().__init__()
+
+ def forward(self, x):
+ return torch.tan(x)
+
+ @staticmethod
+ @unpack_fixtures
+ def test(qnn_config, quantizer, compile_spec, expected):
+ with expected as metrics:
+ export_and_verify(
+ module=__class__(),
+ inputs=(torch.randn(2, 5, 1, 3),),
+ qnn_config=qnn_config,
+ quantizer=quantizer,
+ compile_specs=compile_spec,
+ metrics=metrics,
+ )
+
+
class Tanh(torch.nn.Module):
def __init__(self):
super().__init__()
@@ -5030,3 +5224,38 @@ def test(subtests, qnn_config, quantizer, compile_spec, expected):
compile_specs=compile_spec,
metrics=metrics,
)
+
+
+class Var(torch.nn.Module):
+ def __init__(self, dim, correction, keepdim):
+ super().__init__()
+ self.dim = dim
+ self.correction = correction
+ self.keepdim = keepdim
+
+ def forward(self, x):
+ return torch.var(
+ x, dim=self.dim, correction=self.correction, keepdim=self.keepdim
+ )
+
+ @staticmethod
+ @unpack_fixtures
+ def test(subtests, qnn_config, quantizer, compile_spec, expected):
+ dims = [-1, [0, 2]]
+ corrections = [0, 1]
+ keepdims = [False, True]
+ for dim, correction, keepdim in itertools.product(dims, corrections, keepdims):
+ with subtests.test(
+ msg=f"dim:{dim}, correction:{correction}, keepdim:{keepdim}"
+ ):
+ with expected as metrics:
+ export_and_verify(
+ module=__class__(
+ dim=dim, correction=correction, keepdim=keepdim
+ ),
+ inputs=(torch.randn(3, 4, 5),),
+ qnn_config=qnn_config,
+ quantizer=quantizer,
+ compile_specs=compile_spec,
+ metrics=metrics,
+ )
diff --git a/backends/qualcomm/tests/rework/src/utils.py b/backends/qualcomm/tests/rework/src/utils.py
new file mode 100644
index 00000000000..9202007b18a
--- /dev/null
+++ b/backends/qualcomm/tests/rework/src/utils.py
@@ -0,0 +1,635 @@
+# 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 operator
+import os
+import tempfile
+from collections import defaultdict
+from functools import reduce
+from typing import Tuple
+
+import pytest
+
+import torch
+
+from executorch.backends.qualcomm._passes.qnn_pass_manager import (
+ get_qnn_pass_manager_cls,
+)
+from executorch.backends.qualcomm.builders.node_visitor_manager import get_node_visitors
+from executorch.backends.qualcomm.debugger.utils import DrawGraph as _DrawGraphTool
+from executorch.backends.qualcomm.export_utils import (
+ convert_pt2e,
+ make_quantizer,
+ prepare_pt2e,
+ prepare_qat_pt2e,
+ QuantDtype,
+ to_edge_transform_and_lower_to_qnn,
+)
+from executorch.backends.qualcomm.tests.rework.conftest import (
+ calibrate,
+ check_exception,
+ EXCEPTION_FROM_PREPROCESS,
+)
+from executorch.backends.qualcomm.tests.utils import validate_context_binary
+from executorch.backends.qualcomm.utils.utils import (
+ capture_program,
+ dump_context_from_pte,
+ rewrite_prepared_observer,
+ skip_annotation,
+)
+from executorch.exir import to_edge
+from torchao.quantization.pt2e.quantizer.quantizer import Q_ANNOTATION_KEY
+
+
+# ---------------------------------------------------------------------------
+# Shared model — multi-input / multi-output
+# forward(x, y) -> (Tensor, Tensor)
+# x -> conv1 -> relu1 -> a ─┐
+# add -> conv3 -> c (outputs: c, a)
+# y -> conv2 -> relu2 -> b ─┘
+# Provides: two inputs, two outputs, two conv2d ops (SeqMSE), one add (skip target)
+# ---------------------------------------------------------------------------
+
+
+class _UtilsModel(torch.nn.Module):
+ def __init__(self):
+ super().__init__()
+ self.conv1 = torch.nn.Conv2d(4, 4, kernel_size=3, padding=1, bias=True)
+ self.conv2 = torch.nn.Conv2d(4, 4, kernel_size=3, padding=1, bias=True)
+ self.conv3 = torch.nn.Conv2d(4, 4, kernel_size=1, bias=True)
+ self.relu1 = torch.nn.ReLU()
+ self.relu2 = torch.nn.ReLU()
+
+ def forward(self, x, y):
+ a = self.relu1(self.conv1(x))
+ b = self.relu2(self.conv2(y))
+ c = self.conv3(a + b)
+ return c, a
+
+
+class _CompositeDelegateModule(torch.nn.Module):
+ def __init__(
+ self,
+ compiler_specs,
+ to_edge_transform_and_lower_method,
+ quantize_method=None,
+ ) -> None:
+ super().__init__()
+ self.modules = [
+ _UtilsModel(),
+ _UtilsModel(),
+ ]
+ self.sample_inputs = [
+ (torch.randn(1, 4, 8, 8), torch.randn(1, 4, 8, 8)),
+ (torch.randn(1, 4, 8, 8), torch.randn(1, 4, 8, 8)),
+ ]
+ self.lowered_modules = []
+ for module, sample_input in zip(self.modules, self.sample_inputs):
+ if quantize_method:
+ module = quantize_method(module, sample_input)
+ edge_prog = to_edge_transform_and_lower_method(
+ module, sample_input, compiler_specs
+ )
+ self.lowered_modules.append(
+ edge_prog.exported_program().graph_module._modules.get(
+ "lowered_module_0"
+ )
+ )
+
+ def forward(self, x1, y1, x2, y2):
+ z11, z12 = self.lowered_modules[0](x1, y1)
+ z21, z22 = self.lowered_modules[1](x2, y2)
+ return z11 + z21, z12 + z22
+
+ def get_random_input(self):
+ return tuple(e for tup in self.sample_inputs for e in tup)
+
+ def get_reference_module(self):
+ class CompositeReferenceModule(torch.nn.Module):
+ def __init__(self, modules):
+ super().__init__()
+ self.modules = modules
+
+ def forward(self, x1, y1, x2, y2):
+ z11, z12 = self.modules[0](x1, y1)
+ z21, z22 = self.modules[1](x2, y2)
+ return z11 + z21, z12 + z22
+
+ return CompositeReferenceModule(self.modules)
+
+
+# ---------------------------------------------------------------------------
+# Test Bodies
+# ---------------------------------------------------------------------------
+
+
+class DumpContextFromPte:
+ @staticmethod
+ def test(quantizer, compile_spec):
+ module = _UtilsModel()
+ inputs = (torch.randn(1, 4, 8, 8), torch.randn(1, 4, 8, 8))
+
+ with calibrate(module, [inputs], quantizer) as quantized:
+ exec_prog = to_edge_transform_and_lower_to_qnn(
+ quantized, inputs, compile_spec
+ ).to_executorch()
+
+ with tempfile.TemporaryDirectory() as tmp_dir:
+ pte_path = f"{tmp_dir}/model.pte"
+ with open(pte_path, "wb") as f:
+ exec_prog.write_to_file(f)
+ dump_context_from_pte(pte_path)
+ binary_path = f"{tmp_dir}/forward_0.bin"
+ assert os.path.isfile(binary_path)
+ with open(binary_path, "rb") as f:
+ validate_context_binary(f.read())
+
+
+class DrawGraph:
+ _golden = """digraph test {
+ rankdir=TB
+ "input_0_x@0" [label=<
+
+ | name: input_0_x@0 |
+ | data_type: Qnn_DataType_t.QNN_DATATYPE_FLOAT_32 |
+ | tensor_type: Qnn_TensorType_t.QNN_TENSOR_TYPE_APP_WRITE |
+ | dims: [1, 4, 8, 8] |
+ | quantization_encoding: Qnn_QuantizationEncoding_t.QNN_QUANTIZATION_ENCODING_UNDEFINED |
+
> color=black fillcolor=transparent shape=box style=rounded]
+ "quantized_decomposed_quantize_per_tensor_default@0" [label=<
+
+ | name: quantized_decomposed_quantize_per_tensor_default@0 |
+ | data_type: Qnn_DataType_t.QNN_DATATYPE_UFIXED_POINT_8 |
+ | tensor_type: Qnn_TensorType_t.QNN_TENSOR_TYPE_NATIVE |
+ | dims: [1, 4, 8, 8] |
+ | quantization_encoding: Qnn_QuantizationEncoding_t.QNN_QUANTIZATION_ENCODING_SCALE_OFFSET |
+
> color=black fillcolor=transparent shape=box style=rounded]
+ "input_1_y@0" [label=<
+
+ | name: input_1_y@0 |
+ | data_type: Qnn_DataType_t.QNN_DATATYPE_FLOAT_32 |
+ | tensor_type: Qnn_TensorType_t.QNN_TENSOR_TYPE_APP_WRITE |
+ | dims: [1, 4, 8, 8] |
+ | quantization_encoding: Qnn_QuantizationEncoding_t.QNN_QUANTIZATION_ENCODING_UNDEFINED |
+
> color=black fillcolor=transparent shape=box style=rounded]
+ "quantized_decomposed_quantize_per_tensor_default_1@0" [label=<
+
+ | name: quantized_decomposed_quantize_per_tensor_default_1@0 |
+ | data_type: Qnn_DataType_t.QNN_DATATYPE_UFIXED_POINT_8 |
+ | tensor_type: Qnn_TensorType_t.QNN_TENSOR_TYPE_NATIVE |
+ | dims: [1, 4, 8, 8] |
+ | quantization_encoding: Qnn_QuantizationEncoding_t.QNN_QUANTIZATION_ENCODING_SCALE_OFFSET |
+
> color=black fillcolor=transparent shape=box style=rounded]
+ "b__frozen_param0@0" [label=<
+
+ | name: b__frozen_param0@0 |
+ | data_type: Qnn_DataType_t.QNN_DATATYPE_SFIXED_POINT_8 |
+ | tensor_type: Qnn_TensorType_t.QNN_TENSOR_TYPE_STATIC |
+ | dims: [3, 3, 4, 4] |
+ | quantization_encoding: Qnn_QuantizationEncoding_t.QNN_QUANTIZATION_ENCODING_AXIS_SCALE_OFFSET |
+
> color=black fillcolor=transparent shape=box style=rounded]
+ "b__frozen_param1@0" [label=<
+
+ | name: b__frozen_param1@0 |
+ | data_type: Qnn_DataType_t.QNN_DATATYPE_SFIXED_POINT_32 |
+ | tensor_type: Qnn_TensorType_t.QNN_TENSOR_TYPE_STATIC |
+ | dims: [4] |
+ | quantization_encoding: Qnn_QuantizationEncoding_t.QNN_QUANTIZATION_ENCODING_AXIS_SCALE_OFFSET |
+
> color=black fillcolor=transparent shape=box style=rounded]
+ "aten_convolution_default@0" [label=<
+
+ | name: aten_convolution_default@0 |
+ | data_type: Qnn_DataType_t.QNN_DATATYPE_UFIXED_POINT_8 |
+ | tensor_type: Qnn_TensorType_t.QNN_TENSOR_TYPE_NATIVE |
+ | dims: [1, 8, 8, 4] |
+ | quantization_encoding: Qnn_QuantizationEncoding_t.QNN_QUANTIZATION_ENCODING_SCALE_OFFSET |
+
> color=black fillcolor=transparent shape=box style=rounded]
+ "aten_relu_default@0" [label=<
+
+ | name: aten_relu_default@0 |
+ | data_type: Qnn_DataType_t.QNN_DATATYPE_UFIXED_POINT_8 |
+ | tensor_type: Qnn_TensorType_t.QNN_TENSOR_TYPE_NATIVE |
+ | dims: [1, 8, 8, 4] |
+ | quantization_encoding: Qnn_QuantizationEncoding_t.QNN_QUANTIZATION_ENCODING_SCALE_OFFSET |
+
> color=black fillcolor=transparent shape=box style=rounded]
+ "output_quantized_decomposed_dequantize_per_tensor_default@0" [label=<
+
+ | name: output_quantized_decomposed_dequantize_per_tensor_default@0 |
+ | data_type: Qnn_DataType_t.QNN_DATATYPE_FLOAT_32 |
+ | tensor_type: Qnn_TensorType_t.QNN_TENSOR_TYPE_APP_READ |
+ | dims: [1, 4, 8, 8] |
+ | quantization_encoding: Qnn_QuantizationEncoding_t.QNN_QUANTIZATION_ENCODING_UNDEFINED |
+
> color=black fillcolor=transparent shape=box style=rounded]
+ "b__frozen_param2@0" [label=<
+
+ | name: b__frozen_param2@0 |
+ | data_type: Qnn_DataType_t.QNN_DATATYPE_SFIXED_POINT_8 |
+ | tensor_type: Qnn_TensorType_t.QNN_TENSOR_TYPE_STATIC |
+ | dims: [3, 3, 4, 4] |
+ | quantization_encoding: Qnn_QuantizationEncoding_t.QNN_QUANTIZATION_ENCODING_AXIS_SCALE_OFFSET |
+
> color=black fillcolor=transparent shape=box style=rounded]
+ "b__frozen_param3@0" [label=<
+
+ | name: b__frozen_param3@0 |
+ | data_type: Qnn_DataType_t.QNN_DATATYPE_SFIXED_POINT_32 |
+ | tensor_type: Qnn_TensorType_t.QNN_TENSOR_TYPE_STATIC |
+ | dims: [4] |
+ | quantization_encoding: Qnn_QuantizationEncoding_t.QNN_QUANTIZATION_ENCODING_AXIS_SCALE_OFFSET |
+
> color=black fillcolor=transparent shape=box style=rounded]
+ "aten_convolution_default_1@0" [label=<
+
+ | name: aten_convolution_default_1@0 |
+ | data_type: Qnn_DataType_t.QNN_DATATYPE_UFIXED_POINT_8 |
+ | tensor_type: Qnn_TensorType_t.QNN_TENSOR_TYPE_NATIVE |
+ | dims: [1, 8, 8, 4] |
+ | quantization_encoding: Qnn_QuantizationEncoding_t.QNN_QUANTIZATION_ENCODING_SCALE_OFFSET |
+
> color=black fillcolor=transparent shape=box style=rounded]
+ "aten_relu_default_1@0" [label=<
+
+ | name: aten_relu_default_1@0 |
+ | data_type: Qnn_DataType_t.QNN_DATATYPE_UFIXED_POINT_8 |
+ | tensor_type: Qnn_TensorType_t.QNN_TENSOR_TYPE_NATIVE |
+ | dims: [1, 8, 8, 4] |
+ | quantization_encoding: Qnn_QuantizationEncoding_t.QNN_QUANTIZATION_ENCODING_SCALE_OFFSET |
+
> color=black fillcolor=transparent shape=box style=rounded]
+ "aten_add_tensor@0" [label=<
+
+ | name: aten_add_tensor@0 |
+ | data_type: Qnn_DataType_t.QNN_DATATYPE_UFIXED_POINT_8 |
+ | tensor_type: Qnn_TensorType_t.QNN_TENSOR_TYPE_NATIVE |
+ | dims: [1, 8, 8, 4] |
+ | quantization_encoding: Qnn_QuantizationEncoding_t.QNN_QUANTIZATION_ENCODING_SCALE_OFFSET |
+
> color=black fillcolor=transparent shape=box style=rounded]
+ "b__frozen_param4@0" [label=<
+
+ | name: b__frozen_param4@0 |
+ | data_type: Qnn_DataType_t.QNN_DATATYPE_SFIXED_POINT_8 |
+ | tensor_type: Qnn_TensorType_t.QNN_TENSOR_TYPE_STATIC |
+ | dims: [1, 1, 4, 4] |
+ | quantization_encoding: Qnn_QuantizationEncoding_t.QNN_QUANTIZATION_ENCODING_AXIS_SCALE_OFFSET |
+
> color=black fillcolor=transparent shape=box style=rounded]
+ "b__frozen_param5@0" [label=<
+
+ | name: b__frozen_param5@0 |
+ | data_type: Qnn_DataType_t.QNN_DATATYPE_SFIXED_POINT_32 |
+ | tensor_type: Qnn_TensorType_t.QNN_TENSOR_TYPE_STATIC |
+ | dims: [4] |
+ | quantization_encoding: Qnn_QuantizationEncoding_t.QNN_QUANTIZATION_ENCODING_AXIS_SCALE_OFFSET |
+
> color=black fillcolor=transparent shape=box style=rounded]
+ "aten_convolution_default_2@0" [label=<
+
+ | name: aten_convolution_default_2@0 |
+ | data_type: Qnn_DataType_t.QNN_DATATYPE_UFIXED_POINT_8 |
+ | tensor_type: Qnn_TensorType_t.QNN_TENSOR_TYPE_NATIVE |
+ | dims: [1, 8, 8, 4] |
+ | quantization_encoding: Qnn_QuantizationEncoding_t.QNN_QUANTIZATION_ENCODING_SCALE_OFFSET |
+
> color=black fillcolor=transparent shape=box style=rounded]
+ "output_quantized_decomposed_dequantize_per_tensor_default_1@0" [label=<
+
+ | name: output_quantized_decomposed_dequantize_per_tensor_default_1@0 |
+ | data_type: Qnn_DataType_t.QNN_DATATYPE_FLOAT_32 |
+ | tensor_type: Qnn_TensorType_t.QNN_TENSOR_TYPE_APP_READ |
+ | dims: [1, 4, 8, 8] |
+ | quantization_encoding: Qnn_QuantizationEncoding_t.QNN_QUANTIZATION_ENCODING_UNDEFINED |
+
> color=black fillcolor=transparent shape=box style=rounded]
+ "input_0_x@0" -> "quantized_decomposed_quantize_per_tensor_default@0"
+ "input_1_y@0" -> "quantized_decomposed_quantize_per_tensor_default_1@0"
+ "quantized_decomposed_quantize_per_tensor_default@0" -> "aten_convolution_default@0"
+ "b__frozen_param0@0" -> "aten_convolution_default@0"
+ "b__frozen_param1@0" -> "aten_convolution_default@0"
+ "aten_convolution_default@0" -> "aten_relu_default@0"
+ "aten_relu_default@0" -> "output_quantized_decomposed_dequantize_per_tensor_default@0"
+ "quantized_decomposed_quantize_per_tensor_default_1@0" -> "aten_convolution_default_1@0"
+ "b__frozen_param2@0" -> "aten_convolution_default_1@0"
+ "b__frozen_param3@0" -> "aten_convolution_default_1@0"
+ "aten_convolution_default_1@0" -> "aten_relu_default_1@0"
+ "aten_relu_default@0" -> "aten_add_tensor@0"
+ "aten_relu_default_1@0" -> "aten_add_tensor@0"
+ "aten_add_tensor@0" -> "aten_convolution_default_2@0"
+ "b__frozen_param4@0" -> "aten_convolution_default_2@0"
+ "b__frozen_param5@0" -> "aten_convolution_default_2@0"
+ "aten_convolution_default_2@0" -> "output_quantized_decomposed_dequantize_per_tensor_default_1@0"
+ }
+ """
+
+ @staticmethod
+ def _build_op_wrapper_list(module, inputs):
+ delegated_program = capture_program(module, inputs)
+ graph_module = get_qnn_pass_manager_cls()().transform_for_preprocess_pipeline(
+ delegated_program.exported_program
+ )
+ nodes_to_wrappers = defaultdict(dict)
+ node_visitors = get_node_visitors(
+ delegated_program.exported_program, enable_tensor_dump=False
+ )
+ py_op_wrapper_list = []
+ for node in graph_module.graph.nodes:
+ if node.op == "call_function" and node.target.__name__ in node_visitors:
+ wrapper = node_visitors[node.target.__name__].define_node(
+ node, nodes_to_wrappers
+ )
+ if wrapper is not None:
+ if isinstance(wrapper, list):
+ py_op_wrapper_list.extend(wrapper)
+ else:
+ py_op_wrapper_list.append(wrapper)
+ return py_op_wrapper_list
+
+ @staticmethod
+ def test(quantizer):
+ module = _UtilsModel()
+ inputs = (torch.randn(1, 4, 8, 8), torch.randn(1, 4, 8, 8))
+
+ with calibrate(module, [inputs], quantizer) as quantized_module:
+ op_wrappers = __class__._build_op_wrapper_list(quantized_module, inputs)
+ with tempfile.TemporaryDirectory() as tmp_dir:
+ _DrawGraphTool("test", tmp_dir, op_wrappers, dot_string=True)
+ with open(os.path.join(tmp_dir, "test.dot")) as f:
+ result = f.read()
+ assert sorted(__class__._golden.split()) == sorted(
+ result.split()
+ ), "Generated .dot file does not match the golden file."
+
+
+class FixedPointFloatingPointMixedPrecision:
+ # test specifically for graph which owns floating point / fixed point
+ # operators simultaneously
+ @staticmethod
+ def test(subtests, quantizer, compile_spec):
+ module = _UtilsModel()
+ inputs = (torch.randn(1, 4, 8, 8), torch.randn(1, 4, 8, 8))
+
+ def calibrator(gm):
+ gm(*inputs)
+
+ for fallback_to_cpu, expected_partitions in [
+ (True, 2),
+ (False, 3),
+ ]:
+ with subtests.test(msg=f"fallback_to_cpu={fallback_to_cpu}"):
+ _, edge_prog_mgrs = skip_annotation(
+ nn_module=module,
+ quantizer=quantizer,
+ compiler_specs=compile_spec,
+ sample_input=inputs,
+ calibration_cb=calibrator,
+ fp_node_id_set={"add"},
+ fallback_to_cpu=fallback_to_cpu,
+ )
+ assert len(edge_prog_mgrs) == expected_partitions
+
+
+class MultiContextsComposite:
+ @staticmethod
+ def test(compile_spec):
+ module = _CompositeDelegateModule(
+ compiler_specs=compile_spec,
+ to_edge_transform_and_lower_method=to_edge_transform_and_lower_to_qnn,
+ )
+ sample_input = module.get_random_input()
+ edge_prog = to_edge(
+ torch.export.export(module, sample_input, strict=True),
+ )
+ # should complete without error
+ edge_prog.to_executorch()
+
+
+class RewritePreparedObserver:
+ @staticmethod
+ def test(quantizer):
+ import math
+
+ from torchao.quantization.pt2e import FixedQParamsObserver
+
+ module = _UtilsModel()
+ inputs = (torch.randn(1, 4, 8, 8), torch.randn(1, 4, 8, 8))
+ exported = torch.export.export(module, inputs, strict=True).module()
+ prepared = prepare_pt2e(exported, quantizer)
+ prepared(*inputs)
+
+ new_obs = FixedQParamsObserver(
+ scale=0.004,
+ zero_point=0,
+ dtype=torch.uint8,
+ quant_min=0,
+ quant_max=255,
+ qscheme=torch.per_tensor_affine,
+ )
+ rewrite_prepared_observer(prepared, {"activation_post_process_3": new_obs})
+ assert (
+ prepared.activation_post_process_3 is new_obs
+ ), "observer is not overridden correctly"
+ # should complete without error
+ converted = convert_pt2e(prepared)
+ q_node = [
+ n
+ for n in converted.graph.nodes
+ if n.name == "quantize_per_tensor_default_2"
+ ][0]
+ assert (
+ math.isclose(q_node.args[1], 0.004, abs_tol=1e-8) and q_node.args[2] == 0
+ ), "scale / offset do not match the overridden values"
+
+
+class SkipNodePartitioner:
+ @staticmethod
+ def _count_lowered_modules(edge_prog_mgr):
+ gm = edge_prog_mgr.exported_program().graph_module
+ return len([k for k in gm._modules if k.startswith("lowered_module")])
+
+ @staticmethod
+ def test(subtests, quantizer, compile_spec):
+ module = _UtilsModel()
+ inputs = (torch.randn(1, 4, 8, 8), torch.randn(1, 4, 8, 8))
+
+ cases = [
+ # skip add by node id: [conv1+relu1 / conv2+relu2], [conv3] → 2 partitions
+ ("node_id", {"skip_node_id_set": {"aten_add_tensor"}}, 2),
+ # skip add by op: same split → 2 partitions
+ ("node_op", {"skip_node_op_set": {"aten.add.Tensor"}}, 2),
+ ]
+
+ for label, skip_kwargs, expected in cases:
+ with subtests.test(msg=label):
+ with calibrate(module, [inputs], quantizer) as quantized:
+ edge_prog_mgr = to_edge_transform_and_lower_to_qnn(
+ quantized, inputs, compile_spec, **skip_kwargs
+ )
+ assert __class__._count_lowered_modules(edge_prog_mgr) == expected
+
+ # expected failure due to fallback per-channel weight required
+ # a per-chennel dequantized op which is unavailable in QNN
+ cases = [
+ ("node_id", {"skip_node_id_set": {"aten_convolution_default"}}, 2),
+ ]
+ for label, skip_kwargs, _ in cases:
+ with subtests.test(msg=label):
+ with pytest.raises( # noqa: B017
+ Exception, check=check_exception(EXCEPTION_FROM_PREPROCESS)
+ ):
+ with calibrate(module, [inputs], quantizer) as quantized:
+ edge_prog_mgr = to_edge_transform_and_lower_to_qnn(
+ quantized, inputs, compile_spec, **skip_kwargs
+ )
+
+
+class SkipNodeQuantizer:
+ @staticmethod
+ def test(subtests, quantizer, compile_spec):
+ module = _UtilsModel()
+ inputs = (torch.randn(1, 4, 8, 8), torch.randn(1, 4, 8, 8))
+
+ def calibrator(gm):
+ gm(*inputs)
+
+ cases = [
+ ("node_id", {"fp_node_id_set": {"add"}}, 2),
+ ("node_id", {"fp_node_id_set": {"conv2d"}}, 1),
+ ("node_op", {"fp_node_op_set": {torch.ops.aten.add.Tensor}}, 2),
+ ]
+
+ for label, skip_kwargs, expected_partitions in cases:
+ with subtests.test(msg=label):
+ graph_module, edge_prog_mgrs = skip_annotation(
+ nn_module=module,
+ quantizer=quantizer,
+ compiler_specs=compile_spec,
+ sample_input=inputs,
+ calibration_cb=calibrator,
+ **skip_kwargs,
+ )
+ assert len(edge_prog_mgrs) == expected_partitions
+
+ # skipped nodes must not be annotated
+ skipped_targets = set(skip_kwargs.get("fp_node_id_set", set())) | {
+ op.__name__ if hasattr(op, "__name__") else str(op)
+ for op in skip_kwargs.get("fp_node_op_set", set())
+ }
+ for node in graph_module.graph.nodes:
+ name_matches = node.name in skipped_targets
+ op_matches = (
+ hasattr(node.target, "__name__")
+ and node.target.__name__ in skipped_targets
+ ) or str(node.target) in skipped_targets
+ if name_matches or op_matches:
+ annotated = (
+ Q_ANNOTATION_KEY in node.meta
+ and node.meta[Q_ANNOTATION_KEY]._annotated
+ )
+ assert (
+ not annotated
+ ), f"node {node.name} should not be annotated"
+
+
+class QAT:
+ @staticmethod
+ def _make_qat_quantizer(quant_dtype, block_size_map=None):
+ q = make_quantizer(
+ quant_dtype=quant_dtype,
+ per_channel_conv=True,
+ is_qat=True,
+ soc_model="SM8650",
+ )
+ if block_size_map:
+ q.set_block_size_map(block_size_map)
+ return q
+
+ @staticmethod
+ def _get_converted_module(
+ ori_module: torch.nn.Module,
+ prepared: torch.nn.Module,
+ inputs: Tuple[torch.Tensor],
+ ) -> torch.fx.GraphModule:
+ optimizer = torch.optim.SGD(prepared.parameters(), lr=0.0001)
+ criterion = torch.nn.CrossEntropyLoss()
+ output = prepared(*inputs)
+ loss = reduce(
+ operator.add,
+ [criterion(qdq, ref) for qdq, ref in zip(output, ori_module(*inputs))],
+ )
+ optimizer.zero_grad()
+ loss.backward()
+ optimizer.step()
+ return convert_pt2e(prepared)
+
+ @staticmethod
+ def test(subtests):
+ from executorch.backends.qualcomm.quantizer.observers.per_block_param_observer import (
+ PerBlockParamFakeQuantize,
+ )
+ from torchao.quantization.pt2e import FusedMovingAvgObsFakeQuantize
+
+ module = _UtilsModel()
+ inputs = (torch.randn(1, 4, 8, 8), torch.randn(1, 4, 8, 8))
+
+ with subtests.test(msg="16a4w_pcq"):
+ # activation: FusedMovingAvgObsFakeQuantize (uint16 range, per-tensor)
+ # weight: FusedMovingAvgObsFakeQuantize (int4 range [-7,7], per-channel)
+ q = __class__._make_qat_quantizer(QuantDtype.use_16a4w)
+ exported = torch.export.export(module, inputs, strict=True).module()
+ prepared = prepare_qat_pt2e(exported, q)
+ fq_modules = [
+ m
+ for m in prepared.modules()
+ if isinstance(m, FusedMovingAvgObsFakeQuantize)
+ ]
+ assert len(fq_modules) > 0, "no FusedMovingAvgObsFakeQuantize found"
+ # activation FQs have uint16 range (quant_max=65535)
+ act_fqs = [m for m in fq_modules if m.quant_max == 65535]
+ # weight FQs have int4 range (quant_max=7)
+ weight_fqs = [m for m in fq_modules if m.quant_max == 7]
+ assert len(act_fqs) > 0, "no uint16-range activation FQ found for 16a4w"
+ assert len(weight_fqs) > 0, "no int4-range weight FQ found for 16a4w"
+ # should complete without error
+ __class__._get_converted_module(module, prepared, inputs)
+
+ with subtests.test(msg="16a4w_block"):
+ # activation: FusedMovingAvgObsFakeQuantize (uint16 range)
+ # weight: PerBlockParamFakeQuantize (int4 range)
+ q = __class__._make_qat_quantizer(
+ QuantDtype.use_16a4w_block,
+ block_size_map={"conv2d": (1, 4, 1, 1)},
+ )
+ exported = torch.export.export(module, inputs, strict=True).module()
+ prepared = prepare_qat_pt2e(exported, q)
+ act_fqs = [
+ m
+ for m in prepared.modules()
+ if isinstance(m, FusedMovingAvgObsFakeQuantize) and m.quant_max == 65535
+ ]
+ block_fqs = [
+ m
+ for m in prepared.modules()
+ if isinstance(m, PerBlockParamFakeQuantize)
+ ]
+ assert (
+ len(act_fqs) > 0
+ ), "no uint16-range activation FQ found for 16a4w_block"
+ assert (
+ len(block_fqs) > 0
+ ), "no PerBlockParamFakeQuantize found for 16a4w_block"
+ # should complete without error
+ __class__._get_converted_module(module, prepared, inputs)
+
+ with subtests.test(msg="fp16a8w"):
+ # activation: None (FP16 — no activation quantization)
+ # weight: FusedMovingAvgObsFakeQuantize (int8 range [-127,127], per-tensor or per-channel)
+ q = __class__._make_qat_quantizer(QuantDtype.use_fp16a8w)
+ exported = torch.export.export(module, inputs, strict=True).module()
+ prepared = prepare_qat_pt2e(exported, q)
+ fq_modules = [
+ m
+ for m in prepared.modules()
+ if isinstance(m, FusedMovingAvgObsFakeQuantize)
+ ]
+ # no activation FQ: quant_max==65535 or quant_max==255 nodes must be absent
+ act_fqs = [m for m in fq_modules if m.quant_max in (255, 65535)]
+ # weight FQ: int8 range (quant_max=127)
+ weight_fqs = [m for m in fq_modules if m.quant_max == 127]
+ assert len(act_fqs) == 0, "fp16a8w should have no activation FQ"
+ assert len(weight_fqs) > 0, "no int8-range weight FQ found for fp16a8w"
+ # should complete without error
+ __class__._get_converted_module(module, prepared, inputs)
diff --git a/backends/qualcomm/tests/rework/utils/conftest.py b/backends/qualcomm/tests/rework/utils/conftest.py
new file mode 100644
index 00000000000..46b79325a18
--- /dev/null
+++ b/backends/qualcomm/tests/rework/utils/conftest.py
@@ -0,0 +1,28 @@
+# 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 pytest
+
+from executorch.backends.qualcomm.export_utils import (
+ generate_htp_compiler_spec,
+ generate_qnn_executorch_compiler_spec,
+ make_quantizer,
+ QcomChipset,
+)
+from executorch.backends.qualcomm.tests.rework.conftest import default_property
+
+
+@pytest.fixture(scope="session")
+def compile_spec():
+ return generate_qnn_executorch_compiler_spec(
+ soc_model=getattr(QcomChipset, default_property().soc_model),
+ backend_options=generate_htp_compiler_spec(use_fp16=True),
+ )
+
+
+@pytest.fixture(scope="session")
+def quantizer():
+ return make_quantizer(soc_model=default_property().soc_model)
diff --git a/backends/qualcomm/tests/rework/utils/test.py b/backends/qualcomm/tests/rework/utils/test.py
new file mode 100644
index 00000000000..8330f79ab88
--- /dev/null
+++ b/backends/qualcomm/tests/rework/utils/test.py
@@ -0,0 +1,41 @@
+# 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 executorch.backends.qualcomm.tests.rework.src.utils import * # noqa: F403
+
+
+def test_dump_context_from_pte(quantizer, compile_spec):
+ DumpContextFromPte.test(quantizer, compile_spec) # noqa: F405
+
+
+def test_draw_graph(quantizer):
+ DrawGraph.test(quantizer) # noqa: F405
+
+
+def test_fixed_point_floating_point_mixed_precision(subtests, quantizer, compile_spec):
+ FixedPointFloatingPointMixedPrecision.test( # noqa: F405
+ subtests, quantizer, compile_spec
+ )
+
+
+def test_multi_contexts_composite(compile_spec):
+ MultiContextsComposite.test(compile_spec) # noqa: F405
+
+
+def test_rewrite_prepared_observer(quantizer):
+ RewritePreparedObserver.test(quantizer) # noqa: F405
+
+
+def test_skip_node_partitioner(subtests, quantizer, compile_spec):
+ SkipNodePartitioner.test(subtests, quantizer, compile_spec) # noqa: F405
+
+
+def test_skip_node_quantizer(subtests, quantizer, compile_spec):
+ SkipNodeQuantizer.test(subtests, quantizer, compile_spec) # noqa: F405
+
+
+def test_qat(subtests):
+ QAT.test(subtests) # noqa: F405