Skip to content

Commit f3f0c96

Browse files
authored
Qualcomm AI Engine Direct - test framework refactor (pytorch#22072)
Co-author: @winskuo-quic ### Summary - extend op / feature test for htp arch (v69~v81) - extend op / feature test for lpai / gpu backend ### Test plan ```bash pytest backends/qualcomm/tests/rework/lpai/feature/v6/test.py --device f3c0531 --soc_model SM8850 --build_folder ./build-android/ --backend lpai pytest backends/qualcomm/tests/rework/lpai/op/v6/test.py ``` ```bash pytest backends/qualcomm/tests/rework/gpu/feature/test.py --device f3c0531 --soc_model SM8650 --build_folder ./build-android/ --backend gpu ytest backends/qualcomm/tests/rework/gpu/op/test.py --device f3c0531 --soc_model SM8650 --build_folder ./build-android/ --backend gpu ```
1 parent 33fad23 commit f3f0c96

31 files changed

Lines changed: 11136 additions & 322 deletions

File tree

‎backends/qualcomm/_passes/backends/lpai/qnn_lpai_pass_manager.py‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,17 +51,18 @@ def get_passes_dependency_for_capture_program(cls):
5151
{
5252
DecomposeHardsigmoid: [RemoveRedundancy],
5353
DecomposeReciprocal: [RemoveRedundancy],
54-
LpaiPartitionFallbackSupport: [TagQuantIO],
55-
ResolveDebugHandle: [LpaiPartitionFallbackSupport],
54+
LpaiPartitionFallbackSupport: [TagQuantIO, ResolveDebugHandle],
5655
}
5756
)
5857
return deps
5958

6059
def _validate_edge_passes(self) -> None:
61-
super()._validate_edge_passes()
6260
assert isinstance(
63-
self.passes[-2], LpaiPartitionFallbackSupport
64-
), "Please ensure LpaiPartitionFallbackSupport is the last edge pass before ResolveDebugHandle."
61+
self.passes[-2], ResolveDebugHandle
62+
), "Please ensure ResolveDebugHandle is the last edge pass before LpaiPartitionFallbackSupport."
63+
assert isinstance(
64+
self.passes[-1], LpaiPartitionFallbackSupport
65+
), "Please ensure LpaiPartitionFallbackSupport is the last pass."
6566

6667
@classmethod
6768
def get_annotation_passes(cls):

‎backends/qualcomm/_passes/lpai_partition_fallback_support.py‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,9 @@ def insert_partition_qdq(
254254
output_dq_node.meta[QCOM_BYPASS_NODE] = True
255255
graph_module.graph.eliminate_dead_code()
256256

257-
def handle_back_to_back_nodes(self, graph_module: torch.fx.GraphModule):
257+
def handle_back_to_back_nodes(
258+
self, graph_module: torch.fx.GraphModule, unsupported_nodes: set[torch.fx.Node]
259+
):
258260
"""
259261
This function takes care of following cases:
260262
1. When 2 contiguous fall back nodes ``a`` and ``b`` (both
@@ -279,6 +281,7 @@ def handle_back_to_back_nodes(self, graph_module: torch.fx.GraphModule):
279281
input_node
280282
for input_node in node.all_input_nodes
281283
if input_node.op == "call_function"
284+
and input_node not in unsupported_nodes
282285
]
283286
assert all(
284287
input_node.target in dq_ops for input_node in input_call_func_nodes
@@ -327,7 +330,7 @@ def call(self, graph_module: torch.fx.GraphModule) -> PassResult:
327330
unsupported_nodes = self.get_unsupported_nodes(graph_module)
328331
for node in unsupported_nodes:
329332
self.insert_partition_qdq(graph_module, node)
330-
self.handle_back_to_back_nodes(graph_module)
333+
self.handle_back_to_back_nodes(graph_module, unsupported_nodes)
331334
graph_module.graph.eliminate_dead_code()
332335
graph_module.recompile()
333-
return PassResult(graph_module, bool(unsupported_nodes))
336+
return PassResult(graph_module, True)

‎backends/qualcomm/_passes/qnn_pass_manager.py‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -323,9 +323,7 @@ def get_passes_dependency_for_capture_program(cls):
323323
RecomposePixelUnshuffle: [RemoveRedundancy],
324324
RecomposeRmsNorm: [RemoveRedundancy],
325325
TagQuantIO: [LayoutTransform],
326-
ResolveDebugHandle: [
327-
TagQuantIO
328-
], # IMPORTANT: Please always ensure ResolveDebugHandle is the last executed pass.
326+
ResolveDebugHandle: [TagQuantIO],
329327
}
330328

331329
@classmethod

‎backends/qualcomm/builders/op_batch_norm.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ class BatchNorm(NodeVisitor):
2929
target = [
3030
"aten._native_batch_norm_legit_no_training.default",
3131
"aten._native_batch_norm_legit.no_stats",
32+
"aten._native_batch_norm_legit_functional.default",
3233
]
3334

3435
def __init__(self, *args) -> None:

‎backends/qualcomm/qnn_preprocess.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,7 @@ def preprocess_multimethod( # noqa: C901
243243
(handle_id := node.meta.get(DEBUG_HANDLE_KEY))
244244
and QCOM_TENSOR_NAME in node.meta
245245
and len(node.meta[QCOM_TENSOR_NAME]) == 1
246+
and node.op == "call_function"
246247
):
247248
debug_handle_builder.insert_delegate_mapping_entry(
248249
handles=handle_id,

‎backends/qualcomm/quantizer/annotators/lpai_rules.py‎

Lines changed: 10 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ class AvgPool2d(GeneralOpDef):
129129

130130
# TODO: Batch_norm op cannot directly map to QNN OpBatchnorm due to the number of input doesn't match.
131131
@register_annotator(
132-
[torch.ops.aten.batch_norm.default, torch.ops.aten.instance_norm.default],
132+
[torch.ops.aten.batch_norm.default],
133133
qnn_op=None,
134134
)
135135
class BatchNorm(GeneralOpDef):
@@ -420,7 +420,8 @@ def annotate(node: Node, quantization_config: QuantizationConfig) -> None:
420420
torch.ops.aten.topk.default,
421421
torch.ops.aten.sort.default,
422422
):
423-
out_act_quantization_spec = SharedQuantizationSpec(node.args[0])
423+
# assign to None since they are not supported so far
424+
out_act_quantization_spec = None
424425
node.meta[Q_ANNOTATION_KEY] = QuantizationAnnotation(
425426
output_qspec=out_act_quantization_spec,
426427
_annotated=True,
@@ -807,21 +808,6 @@ class ReluMinMax(GeneralOpDef):
807808
pass
808809

809810

810-
# TODO: Expand_as op cannot directly map to QNN OpTile due to the number of input doesn't match.
811-
@register_annotator(
812-
[
813-
torch.ops.aten.expand_as.default,
814-
],
815-
qnn_op=None,
816-
)
817-
class ExpandAs(GeneralOpDef):
818-
@staticmethod
819-
def annotate(node: Node, quantization_config: QuantizationConfig) -> None:
820-
annotate_in_out_obs_sharing_op(node, quantization_config)
821-
if not _is_annotated([node]):
822-
annotate_single_in_share_out(node, quantization_config)
823-
824-
825811
@register_annotator(
826812
[
827813
torch.ops.aten.flatten.using_ints,
@@ -854,7 +840,6 @@ def annotate(node: Node, quantization_config: QuantizationConfig) -> None:
854840
return
855841

856842
act_node = node.args[0]
857-
weight_node = node.args[2]
858843

859844
# TODO current only support 16a16w
860845
annotate_input_qspec_map(
@@ -863,94 +848,23 @@ def annotate(node: Node, quantization_config: QuantizationConfig) -> None:
863848
quantization_config.input_activation,
864849
)
865850

866-
annotate_input_qspec_map(
867-
node,
868-
weight_node,
869-
quantization_config.input_activation,
870-
)
851+
if len(node.args) > 2 and node.args[2] is not None:
852+
weight_node = node.args[2]
853+
annotate_input_qspec_map(
854+
node,
855+
weight_node,
856+
quantization_config.input_activation,
857+
)
871858
nodes_to_mark_annotated = [node]
872859
annotate_output_qspec(node, quantization_config.output_activation)
873860
_mark_nodes_as_annotated(nodes_to_mark_annotated)
874861

875862

876-
# TODO: There is a bug in the BackendOpInfo library, so it is bypassed now.
877-
@register_annotator([torch.ops.aten.rsqrt.default], qnn_op=None)
878-
class Rsqrt(GeneralOpDef):
879-
pass
880-
881-
882863
@register_annotator([torch.ops.aten.scaled_dot_product_attention.default], qnn_op=None)
883864
class ScaledDotProductAttention(GeneralOpDef):
884865
pass
885866

886867

887-
@register_annotator(
888-
[
889-
torch.ops.aten.scatter.src,
890-
torch.ops.aten.scatter.value,
891-
torch.ops.aten.scatter_add.default,
892-
torch.ops.aten.scatter_reduce.two,
893-
],
894-
qnn_op=None,
895-
)
896-
class ScatterElements(GeneralOpDef):
897-
@staticmethod
898-
def annotate(node: Node, quantization_config: QuantizationConfig) -> None:
899-
if _is_annotated([node]):
900-
return
901-
902-
input_act = node.args[0]
903-
if not isinstance(input_act, Node) or not _is_float_tensor(input_act):
904-
return
905-
906-
input_qspec_map = {}
907-
input_qspec_map[input_act] = quantization_config.input_activation
908-
909-
if (
910-
len(node.args) > 3
911-
and isinstance(node.args[3], Node)
912-
and _is_float_tensor(node.args[3])
913-
):
914-
input_qspec_map[node.args[3]] = SharedQuantizationSpec((input_act, node))
915-
916-
output_act_qspec = (
917-
SharedQuantizationSpec((input_act, node))
918-
if _is_float_tensor(node)
919-
else None
920-
)
921-
922-
if len(input_qspec_map) > 0 or output_act_qspec is not None:
923-
node.meta[Q_ANNOTATION_KEY] = QuantizationAnnotation(
924-
input_qspec_map=input_qspec_map,
925-
output_qspec=output_act_qspec,
926-
_annotated=True,
927-
)
928-
929-
930-
@register_annotator([torch.ops.aten.sort.default], QnnConstants.OpTopK.op_name)
931-
class Sort(GeneralOpDef):
932-
@staticmethod
933-
def annotate(node: Node, quantization_config: QuantizationConfig) -> None:
934-
if _is_annotated([node]):
935-
return
936-
937-
input_qspec_map = {}
938-
input_act_qspec = quantization_config.input_activation
939-
out_act_quantization_spec = None
940-
if input_act_qspec is not None:
941-
if _is_float_tensor(node.args[0]):
942-
input_act = node.args[0]
943-
assert isinstance(input_act, Node)
944-
input_qspec_map[input_act] = input_act_qspec
945-
out_act_quantization_spec = SharedQuantizationSpec((input_act, node))
946-
947-
node.meta[Q_ANNOTATION_KEY] = QuantizationAnnotation(
948-
input_qspec_map=input_qspec_map,
949-
output_qspec=out_act_quantization_spec,
950-
_annotated=True,
951-
)
952-
953-
954868
@register_annotator(
955869
[torch.ops.aten.sigmoid, torch.ops.aten.sigmoid.default],
956870
QnnConstants.OpSigmoid.op_name,

‎backends/qualcomm/tests/models.py‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2943,6 +2943,16 @@ def forward(self, x):
29432943
)
29442944

29452945

2946+
class ConvRelu(torch.nn.Module):
2947+
def __init__(self):
2948+
super().__init__()
2949+
self.conv = torch.nn.Conv2d(3, 8, kernel_size=3, padding=1)
2950+
self.relu = torch.nn.ReLU()
2951+
2952+
def forward(self, x):
2953+
return self.relu(self.conv(x))
2954+
2955+
29462956
class TopKandIndex(torch.nn.Module):
29472957
def __init__(self):
29482958
super().__init__()

‎backends/qualcomm/tests/rework/conftest.py‎

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
get_qnn_context_binary_alignment,
3333
prepare_pt2e,
3434
QnnConfig,
35+
QnnExecuTorchBackendType,
3536
QnnQuantizer,
3637
setup_common_args_and_variables,
3738
SimpleADB,
@@ -269,7 +270,7 @@ def qnn_config(global_setup, request):
269270
f'invalid configuration detected, fall back to emulator workload:\n"{e}"'
270271
)
271272
config = QnnConfig(
272-
soc_model="unknown", build_folder="build-x86", compile_only=True
273+
soc_model="unknown", build_folder="build-x86", enable_x86_64=True
273274
)
274275

275276
return config
@@ -349,6 +350,7 @@ def invoke_remote(
349350
qnn_config: QnnConfig,
350351
executorch_prog: ExecutorchProgramManager,
351352
callback: callable,
353+
inputs: Tuple[torch.Tensor] = None,
352354
):
353355
with tempfile.TemporaryDirectory() as tmp_dir:
354356
pte_fname = f"{tmp_dir}/qnn_executorch_test.pte"
@@ -363,7 +365,7 @@ def invoke_remote(
363365
pte_path=[pte_fname],
364366
workspace=f"/data/local/tmp/{device_workspace}",
365367
)
366-
adb.push()
368+
adb.push(inputs=[inputs] if inputs is not None else None)
367369
callback(adb)
368370

369371

@@ -478,7 +480,23 @@ def export_and_verify(
478480
metrics: Metrics,
479481
):
480482
with calibrate(module, [inputs], quantizer) as exported_module:
481-
if quantizer is not None:
483+
fake_tensors = (
484+
[
485+
node.meta["val"]
486+
for node in exported_module.graph.nodes
487+
if node.op == "call_function" and "val" in node.meta
488+
]
489+
if quantizer
490+
else []
491+
)
492+
dtypes = set()
493+
for tensor in fake_tensors:
494+
if isinstance(tensor, (tuple, list)):
495+
dtypes.update([n.dtype for n in tensor])
496+
else:
497+
dtypes.add(tensor.dtype)
498+
499+
if quantizer and {torch.float, torch.float32} & dtypes:
482500
nodes = {node.target for node in exported_module.graph.nodes}
483501
q_and_dq = {
484502
torch.ops.quantized_decomposed.quantize_per_tensor.default,
@@ -505,15 +523,34 @@ def export_and_verify(
505523
)
506524
)
507525
execution_plan = executorch_prog.executorch_program.execution_plan[0]
526+
527+
def validate():
528+
match qnn_config.backend:
529+
case QnnExecuTorchBackendType.kHtpBackend:
530+
return len(execution_plan.operators) == 0
531+
case QnnExecuTorchBackendType.kGpuBackend:
532+
return len(execution_plan.operators) == 0
533+
case QnnExecuTorchBackendType.kLpaiBackend:
534+
aten_op_names = {
535+
op.name
536+
for op in execution_plan.operators
537+
if "quantize" not in op.name
538+
}
539+
return len(aten_op_names) == 0
540+
case _:
541+
return True
542+
508543
assert all(
509544
[
510-
len(execution_plan.delegates) == 1,
511-
execution_plan.delegates[0].id == "QnnBackend",
512-
len(execution_plan.operators) == 0,
545+
(
546+
len(execution_plan.delegates) == 1
547+
and execution_plan.delegates[0].id == "QnnBackend"
548+
),
549+
validate(),
513550
]
514551
), EXPECT_NOT_FULLY_DELEGATED
515552

516-
mode = "emulator" if qnn_config.build_folder == "build-x86" else "remote"
553+
mode = "emulator" if qnn_config.enable_x86_64 else "remote"
517554
globals()[f"verify_output_{mode}"](
518555
module=module,
519556
inputs=inputs,

‎backends/qualcomm/tests/rework/gpu/conftest.py‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,40 @@
33
#
44
# This source code is licensed under the BSD-style license found in the
55
# LICENSE file in the root directory of this source tree.
6+
7+
from typing import Any
8+
9+
import pytest
10+
11+
from executorch.backends.qualcomm.export_utils import (
12+
generate_gpu_compiler_spec,
13+
generate_qnn_executorch_compiler_spec,
14+
QcomChipset,
15+
)
16+
17+
18+
def with_gpu_context(func):
19+
def wrapper(request, kwargs):
20+
preserved = {k: kwargs.pop(k) for k in ["expected"]}
21+
qnn_config = request.getfixturevalue("qnn_config")
22+
fixtures = {
23+
"quantizer": None,
24+
"compile_spec": generate_qnn_executorch_compiler_spec(
25+
soc_model=getattr(QcomChipset, qnn_config.soc_model),
26+
backend_options=generate_gpu_compiler_spec(),
27+
online_prepare=True,
28+
),
29+
}
30+
return func(request, fixtures | preserved)
31+
32+
return wrapper
33+
34+
35+
def enumerate_fp_dtype(metric: Any):
36+
def wrapper(test_body):
37+
return pytest.mark.parametrize(
38+
"kwargs",
39+
[pytest.param({"act": None, "expected": metric}, id="fp")],
40+
)(test_body)
41+
42+
return wrapper

0 commit comments

Comments
 (0)