Skip to content

Commit 7c3f2ed

Browse files
authored
Qualcomm AI Engine Direct - Optimize performance of pcq embedding (#20686)
Summary: - Change pcq embedding pattern for backend optimization - Note that it is supported after QNN 2.48 ### Test plan ``` python3 backends/qualcomm/tests/test_qnn_delegate.py TestQNNQuantizedOperator.test_qnn_backend_embedding_per_channel --build_folder build-android --host {HOST} --device {DEVICE} --soc_model SM8850 -a {ARTIFACTS} ```
1 parent d658507 commit 7c3f2ed

7 files changed

Lines changed: 252 additions & 119 deletions

File tree

backends/qualcomm/builders/op_embedding.py

Lines changed: 138 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99

1010
import numpy as np
1111
import torch
12+
from executorch.backends.qualcomm.utils.check_qnn_version import (
13+
is_qnn_sdk_version_less_than,
14+
)
1215
from executorch.backends.qualcomm.utils.constants import (
1316
QCOM_DATA,
1417
QCOM_DTYPE,
@@ -35,15 +38,19 @@ class Embedding(NodeVisitor):
3538
def __init__(self, *args) -> None:
3639
super().__init__(*args)
3740

38-
def define_node(
41+
def _is_pcq_embedding(self, weight_node: torch.fx.Node) -> bool:
42+
return (
43+
QCOM_QUANT_ATTRS in weight_node.meta
44+
and weight_node.meta[QCOM_QUANT_ATTRS][QCOM_ENCODING]
45+
in PER_CHANNEL_ENCODING
46+
)
47+
48+
def _define_weight_and_indices(
3949
self,
4050
node: torch.fx.Node,
4151
nodes_to_wrappers: Dict[torch.fx.Node, PyQnnManager.TensorWrapper],
42-
) -> PyQnnManager.PyQnnOpWrapper:
52+
):
4353
weight_node = self.get_node(node.args[0])
44-
is_pcq_embedding = QCOM_QUANT_ATTRS in weight_node.meta and weight_node.meta[
45-
QCOM_QUANT_ATTRS
46-
][QCOM_ENCODING] in (PER_CHANNEL_ENCODING)
4754
weight_tensor = get_parameter(weight_node, self.edge_program)
4855
weight_tensor_wrapper = self.define_tensor(
4956
weight_node,
@@ -62,31 +69,131 @@ def define_node(
6269
PyQnnManager.Qnn_TensorType_t.QNN_TENSOR_TYPE_NATIVE,
6370
nodes_to_wrappers,
6471
)
72+
return weight_node, weight_tensor, weight_tensor_wrapper, indices_tensor_wrapper
73+
74+
def _act_dtype(self, node: torch.fx.Node):
75+
act_quant_encoding, act_quant_configs = self.get_quant_encoding_conf(node, node)
76+
act_dtype = (
77+
torch.uint16
78+
if act_quant_configs[QCOM_DTYPE] == torch.int32
79+
else act_quant_configs[QCOM_DTYPE]
80+
)
81+
return act_quant_encoding, act_quant_configs, act_dtype
82+
83+
def _build_gather_op(
84+
self, node_name, gather_input_tensors, gather_output_tensors
85+
) -> PyQnnManager.PyQnnOpWrapper:
86+
gather_op = PyQnnManager.PyQnnOpWrapper(
87+
node_name,
88+
QNN_OP_PACKAGE_NAME_QTI_AISW,
89+
OpGather.op_name,
90+
)
91+
gather_op.AddInputTensors(gather_input_tensors)
92+
gather_op.AddOutputTensors(gather_output_tensors)
93+
# For now, default axis is zero.
94+
gather_op.AddScalarParam(
95+
OpGather.param_axis,
96+
PyQnnManager.Qnn_DataType_t.QNN_DATATYPE_INT_32,
97+
{QCOM_DATA: np.int32(0)},
98+
)
99+
return gather_op
100+
101+
# Note that this pattern is only supported with QNN 2.48+.
102+
# The weight is converted to the activation quantization before gather so
103+
# the backend can fuse the convert into gather for better performance.
104+
def define_node_optimize(
105+
self,
106+
node: torch.fx.Node,
107+
nodes_to_wrappers: Dict[torch.fx.Node, PyQnnManager.TensorWrapper],
108+
) -> PyQnnManager.PyQnnOpWrapper:
109+
op_wrapper_list = []
110+
(
111+
weight_node,
112+
weight_tensor,
113+
weight_tensor_wrapper,
114+
indices_tensor_wrapper,
115+
) = self._define_weight_and_indices(node, nodes_to_wrappers)
116+
117+
gather_input_tensors = []
118+
if self._is_pcq_embedding(weight_node):
119+
act_quant_encoding, act_quant_configs, act_dtype = self._act_dtype(node)
120+
convert_tensor_wrapper = self.define_custom_tensor_wrapper(
121+
node_name=node.name + "_convert",
122+
tensor_type=PyQnnManager.Qnn_TensorType_t.QNN_TENSOR_TYPE_NATIVE,
123+
dtype=QNN_QUANT_TYPE_MAP[act_dtype],
124+
quant_encoding=act_quant_encoding,
125+
quant_configs=act_quant_configs,
126+
dims=weight_tensor.size(),
127+
tensor=weight_tensor,
128+
is_fake_tensor=True,
129+
nodes_to_wrappers=nodes_to_wrappers,
130+
)
131+
convert_op = PyQnnManager.PyQnnOpWrapper(
132+
node.name + "_convert",
133+
QNN_OP_PACKAGE_NAME_QTI_AISW,
134+
OpConvert.op_name,
135+
)
136+
convert_op.AddInputTensors([weight_tensor_wrapper])
137+
convert_op.AddOutputTensors([convert_tensor_wrapper])
138+
op_wrapper_list.append(convert_op)
139+
gather_input_tensors.append(convert_tensor_wrapper)
140+
else:
141+
gather_input_tensors.append(weight_tensor_wrapper)
142+
gather_input_tensors.append(indices_tensor_wrapper)
143+
144+
output_tensor = self.get_tensor(node, node)
145+
output_tensor_wrapper = self.define_tensor(
146+
node,
147+
node,
148+
output_tensor,
149+
PyQnnManager.Qnn_TensorType_t.QNN_TENSOR_TYPE_NATIVE,
150+
nodes_to_wrappers,
151+
node_name=node.name,
152+
)
153+
154+
op_wrapper_list.append(
155+
self._build_gather_op(
156+
node.name, gather_input_tensors, [output_tensor_wrapper]
157+
)
158+
)
159+
return op_wrapper_list
160+
161+
def define_node_legacy(
162+
self,
163+
node: torch.fx.Node,
164+
nodes_to_wrappers: Dict[torch.fx.Node, PyQnnManager.TensorWrapper],
165+
) -> PyQnnManager.PyQnnOpWrapper:
166+
(
167+
weight_node,
168+
_,
169+
weight_tensor_wrapper,
170+
indices_tensor_wrapper,
171+
) = self._define_weight_and_indices(node, nodes_to_wrappers)
172+
is_pcq_embedding = self._is_pcq_embedding(weight_node)
65173

66174
gather_input_tensors = [weight_tensor_wrapper, indices_tensor_wrapper]
67175

68176
output_tensor = self.get_tensor(node, node)
69177
node_name = node.name
70178
if is_pcq_embedding:
71179
node_quant_attrs = node.meta[QCOM_QUANT_ATTRS].copy()
72-
intermediate_quant_attrs = node.meta[QCOM_QUANT_ATTRS].copy()
180+
weight_quant_attrs = weight_node.meta[QCOM_QUANT_ATTRS]
73181
# Based on QNN HTP quantization constraints,
74182
# we should set the scale to max of scales and per-tensor quantization for embedding op
183+
intermediate_quant_attrs = node_quant_attrs.copy()
75184
intermediate_quant_attrs[QCOM_SCALE] = (
76-
weight_node.meta[QCOM_QUANT_ATTRS][QCOM_SCALES].max().item()
185+
weight_quant_attrs[QCOM_SCALES].max().item()
77186
)
78187
intermediate_quant_attrs[QCOM_ZERO_POINT] = (
79-
weight_node.meta[QCOM_QUANT_ATTRS][QCOM_ZERO_POINTS].max().item()
188+
weight_quant_attrs[QCOM_ZERO_POINTS].max().item()
80189
)
81-
intermediate_quant_attrs[QCOM_DTYPE] = weight_node.meta[QCOM_QUANT_ATTRS][
82-
QCOM_DTYPE
190+
intermediate_quant_attrs[QCOM_DTYPE] = weight_quant_attrs[QCOM_DTYPE]
191+
intermediate_quant_attrs[QCOM_QUANT_MAX] = weight_quant_attrs[
192+
QCOM_QUANT_MAX
193+
]
194+
intermediate_quant_attrs[QCOM_QUANT_MIN] = weight_quant_attrs[
195+
QCOM_QUANT_MIN
83196
]
84-
intermediate_quant_attrs[QCOM_QUANT_MAX] = weight_node.meta[
85-
QCOM_QUANT_ATTRS
86-
][QCOM_QUANT_MAX]
87-
intermediate_quant_attrs[QCOM_QUANT_MIN] = weight_node.meta[
88-
QCOM_QUANT_ATTRS
89-
][QCOM_QUANT_MIN]
90197
node.meta[QCOM_QUANT_ATTRS] = intermediate_quant_attrs
91198
node_name += "_intermediate"
92199
output_tensor_wrapper = self.define_tensor(
@@ -99,33 +206,15 @@ def define_node(
99206
)
100207
gather_output_tensors = [output_tensor_wrapper]
101208

102-
gather_op = PyQnnManager.PyQnnOpWrapper(
103-
node_name,
104-
QNN_OP_PACKAGE_NAME_QTI_AISW,
105-
OpGather.op_name,
106-
)
107-
gather_op.AddInputTensors(gather_input_tensors)
108-
gather_op.AddOutputTensors(gather_output_tensors)
109-
110-
# For now, default axis is zero.
111-
gather_op.AddScalarParam(
112-
OpGather.param_axis,
113-
PyQnnManager.Qnn_DataType_t.QNN_DATATYPE_INT_32,
114-
{QCOM_DATA: np.int32(0)},
115-
)
116-
117-
op_wrapper_list = [gather_op]
209+
op_wrapper_list = [
210+
self._build_gather_op(
211+
node_name, gather_input_tensors, gather_output_tensors
212+
)
213+
]
118214

119215
if is_pcq_embedding:
120216
node.meta[QCOM_QUANT_ATTRS] = node_quant_attrs
121-
act_quant_encoding, act_quant_configs = self.get_quant_encoding_conf(
122-
node, node
123-
)
124-
act_dtype = (
125-
torch.uint16
126-
if act_quant_configs[QCOM_DTYPE] == torch.int32
127-
else act_quant_configs[QCOM_DTYPE]
128-
)
217+
act_quant_encoding, act_quant_configs, act_dtype = self._act_dtype(node)
129218
convert_tensor_wrapper = self.define_custom_tensor_wrapper(
130219
node_name=node.name,
131220
tensor_type=PyQnnManager.Qnn_TensorType_t.QNN_TENSOR_TYPE_NATIVE,
@@ -147,3 +236,12 @@ def define_node(
147236
op_wrapper_list.append(convert_op)
148237

149238
return op_wrapper_list
239+
240+
def define_node(
241+
self,
242+
node: torch.fx.Node,
243+
nodes_to_wrappers: Dict[torch.fx.Node, PyQnnManager.TensorWrapper],
244+
) -> PyQnnManager.PyQnnOpWrapper:
245+
if is_qnn_sdk_version_less_than("2.48"):
246+
return self.define_node_legacy(node, nodes_to_wrappers)
247+
return self.define_node_optimize(node, nodes_to_wrappers)

backends/qualcomm/export_utils.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@
3737
QnnExecuTorchLpaiTargetEnv,
3838
QnnExecuTorchOpPackageOptions,
3939
)
40+
from executorch.backends.qualcomm.utils.check_qnn_version import (
41+
get_sdk_build_id,
42+
is_qnn_sdk_version_less_than,
43+
)
4044
from executorch.backends.qualcomm.utils.constants import (
4145
HEXAGON_SDK_ROOT,
4246
HEXAGON_TOOLS_ROOT,
@@ -47,10 +51,8 @@
4751
generate_lpai_compiler_spec,
4852
generate_qnn_executorch_compiler_spec,
4953
get_qnn_context_binary_alignment,
50-
get_sdk_build_id,
5154
get_soc_to_htp_arch_map,
5255
get_soc_to_lpai_hw_ver_map,
53-
is_qnn_sdk_version_less_than,
5456
to_edge_transform_and_lower_to_qnn,
5557
)
5658
from executorch.exir.capture._config import ExecutorchBackendConfig

backends/qualcomm/tests/test_qnn_delegate.py

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@
4444
TestQNN,
4545
validate_context_binary,
4646
)
47+
from executorch.backends.qualcomm.utils.check_qnn_version import (
48+
is_qnn_sdk_version_greater_than,
49+
is_qnn_sdk_version_less_than,
50+
)
4751
from executorch.backends.qualcomm.utils.constants import (
4852
QCOM_ANNOTATION,
4953
QCOM_MODULE,
@@ -61,8 +65,6 @@
6165
generate_htp_compiler_spec,
6266
generate_lpai_compiler_spec,
6367
generate_qnn_executorch_compiler_spec,
64-
is_qnn_sdk_version_greater_than,
65-
is_qnn_sdk_version_less_than,
6668
PyQnnManagerAdaptor,
6769
rewrite_prepared_observer,
6870
skip_annotation,
@@ -3929,18 +3931,20 @@ def test_qnn_backend_embedding(self):
39293931
)
39303932
self.lower_module_and_test_output(modules[i], sample_input)
39313933

3932-
# TODO: Once the accuracy issue is fixed, enable this test.
3933-
@unittest.skip("Bad accuracy for HTP")
3934+
@unittest.skipIf(is_qnn_sdk_version_less_than("2.48"), "UT pass after QNN 2.48")
39343935
def test_qnn_backend_embedding_per_channel(self):
39353936
module = Embedding() # noqa: F405
39363937
sample_input = (torch.Tensor([1, 2, 4, 5]).to(torch.int32),)
3937-
qdq_module = self.get_qdq_module(
3938-
module,
3939-
sample_input,
3940-
quant_dtype=QuantDtype.use_16a8w,
3941-
is_embedding_per_channel=True,
3942-
)
3943-
self.lower_module_and_test_output(qdq_module, sample_input)
3938+
quant_dtype = [QuantDtype.use_16a8w, QuantDtype.use_16a4w]
3939+
for i, qdtype in enumerate(quant_dtype):
3940+
with self.subTest(i=i):
3941+
qdq_module = self.get_qdq_module(
3942+
module,
3943+
sample_input,
3944+
quant_dtype=qdtype,
3945+
is_embedding_per_channel=True,
3946+
)
3947+
self.lower_module_and_test_output(qdq_module, sample_input)
39443948

39453949
def test_qnn_backend_equal(self):
39463950
test_comb = [
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Copyright (c) Qualcomm Innovation Center, Inc.
2+
# All rights reserved
3+
#
4+
# This source code is licensed under the BSD-style license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
7+
import os
8+
import platform
9+
import re
10+
11+
import executorch.backends.qualcomm.python.PyQnnManagerAdaptor as PyQnnManagerAdaptor
12+
13+
14+
def get_qnn_lib_name(base: str) -> str:
15+
"""Returns the platform-specific shared library filename for a QNN library."""
16+
if platform.system().lower() == "windows":
17+
return f"{base}.dll"
18+
return f"lib{base}.so"
19+
20+
21+
def _get_qnn_host_lib_dir_name() -> str:
22+
"""Returns the QNN SDK library subdirectory name for the current x86-64 host OS."""
23+
if platform.system().lower() == "windows":
24+
return "x86_64-windows-msvc"
25+
return "x86_64-linux-clang"
26+
27+
28+
def get_sdk_build_id():
29+
htp_library_path = os.path.join(
30+
os.environ.get("QNN_SDK_ROOT", None),
31+
"lib",
32+
_get_qnn_host_lib_dir_name(),
33+
get_qnn_lib_name("QnnHtp"),
34+
)
35+
# The GetQnnSdkBuildId API can be used without needing to create a backend first, so it works regardless of which backend is used.
36+
sdk_build_id = PyQnnManagerAdaptor.GetQnnSdkBuildId(htp_library_path)
37+
return sdk_build_id
38+
39+
40+
def is_qnn_sdk_version_less_than(target_version):
41+
current_version = get_sdk_build_id()
42+
43+
match = re.search(r"v(\d+)\.(\d+)", current_version)
44+
if match:
45+
current_major, current_minor = map(int, match.groups()[:2])
46+
else:
47+
raise ValueError(
48+
f"Failed to get current major and minor version from QNN SDK Build id {current_version}"
49+
)
50+
51+
target_major, target_minor = map(int, target_version.split(".")[:2])
52+
53+
return current_major == target_major and current_minor < target_minor
54+
55+
56+
def is_qnn_sdk_version_greater_than(target_version):
57+
current_version = get_sdk_build_id()
58+
59+
match = re.search(r"v(\d+)\.(\d+)", current_version)
60+
if match:
61+
current_major, current_minor = map(int, match.groups()[:2])
62+
else:
63+
raise ValueError(
64+
f"Failed to get current major and minor version from QNN SDK Build id {current_version}"
65+
)
66+
67+
target_major, target_minor = map(int, target_version.split(".")[:2])
68+
69+
return current_major == target_major and current_minor > target_minor

0 commit comments

Comments
 (0)