Skip to content

Commit 4e89627

Browse files
committed
Qualcomm AI Engine Direct - LPAI Support Partition
1 parent 75fb249 commit 4e89627

15 files changed

Lines changed: 649 additions & 196 deletions

backends/qualcomm/_passes/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
from .insert_reshape_for_reduce_ops import InsertReshapeForReduceOps
5050
from .layout_transform import LayoutTransform
5151
from .lift_constant_scalar_operands import LiftConstantScalarOperands
52+
from .lpai_partition_fallback_support import LpaiPartitionFallbackSupport
5253
from .recompose_pad_maxpool2d import RecomposePadMaxPool2d
5354
from .recompose_pixel_unshuffle import RecomposePixelUnshuffle
5455
from .recompose_rms_norm import RecomposeRmsNorm
@@ -107,6 +108,7 @@
107108
InsertRequantize,
108109
LayoutTransform,
109110
LiftConstantScalarOperands,
111+
LpaiPartitionFallbackSupport,
110112
RecomposePadMaxPool2d,
111113
RecomposePixelUnshuffle,
112114
RecomposeRmsNorm,

backends/qualcomm/_passes/fold_qdq.py

Lines changed: 40 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -100,52 +100,49 @@ def _fold_q(self, graph_module: torch.fx.GraphModule) -> torch.fx.GraphModule:
100100

101101
def _preserve_qdq(self, graph_module: torch.fx.GraphModule) -> torch.fx.GraphModule:
102102
for n in graph_module.graph.nodes:
103-
# skip parameters & buffers
104-
if n.target in dq_ops and is_parameter(n.args[0], self.edge_program):
105-
self._annotate_bypass(n)
106-
continue
107-
108-
# TODO: In LPAI backend v6, there is an accuracy drop for the quantize and dequantize operations.
109-
# To address this, keep the quantize/dequantize operations at the model's input and output.
110-
# For example, input -> q_1 (Fallback) -> dq_1 (Bypass) -> graph -> q_2 (Bypass) -> dq_2 (Fallback) -> output
111-
# Here, q_1 and dq_2 will fallback to CPU, while q_2 and dq_1 will be bypassed in qnn_partition and folded in qnn_preprocess.
112-
if self.backend_type == QnnExecuTorchBackendType.kLpaiBackend:
113-
if (
114-
is_graph_input(n, self.edge_program)
115-
# For tagged quantized I/O, we should not fallback quantize operation.
116-
and QCOM_QUANTIZED_IO not in n.meta
117-
):
118-
user_list = list(n.users.keys())
119-
if len(user_list) > 0:
120-
q_node = user_list[0]
121-
q_node.meta[QCOM_FALLBACK_NODE] = True
122-
# Annotate the q_node since it will serve as the input for the first node during operator validation
123-
q_node.meta[QCOM_QUANT_ATTRS] = get_quant_attrs(
124-
self.edge_program, q_node
125-
)
126-
q_node.meta[QCOM_QUANTIZED_IO] = q_node.args[-1]
127-
dq_node = list(q_node.users.keys())[0]
128-
# Bypass dequantize op for graph validation by torch
129-
dq_node.meta[QCOM_BYPASS_NODE] = True
130-
# Make sure that the quantize operator isn't inserted for input in insert_io_qdq.py
131-
n.meta[QCOM_QUANTIZED_IO] = q_node.args[-1]
132-
elif (
133-
is_graph_output(n)
134-
and n.target in dq_ops
135-
# For tagged quantized I/O, we should not fallback dequantize operation.
136-
and QCOM_QUANTIZED_IO not in n.args[0].args[0].meta
137-
):
138-
n.meta[QCOM_FALLBACK_NODE] = True
139-
q_node = n.args[0]
140-
# Bypass quantize op for graph validation by torch
141-
q_node.meta[QCOM_BYPASS_NODE] = True
142-
op_node = q_node.args[0]
143-
# Make sure that the dequantize operator isn't inserted for output in insert_io_qdq.py
144-
op_node.meta[QCOM_QUANTIZED_IO] = q_node.args[-1]
103+
if (
104+
is_graph_input(n, self.edge_program)
105+
# For tagged quantized I/O, we should not fall back quantize operation.
106+
and QCOM_QUANTIZED_IO not in n.meta
107+
):
108+
user_list = list(n.users.keys())
109+
if len(user_list) > 0:
110+
q_node = user_list[0]
111+
q_node.meta[QCOM_FALLBACK_NODE] = True
112+
# Annotate the q_node since it will serve as the input for the first node during operator validation
113+
q_node.meta[QCOM_QUANT_ATTRS] = get_quant_attrs(
114+
self.edge_program, q_node
115+
)
116+
dq_node = list(q_node.users.keys())[0]
117+
# Bypass dequantize op for graph validation by torch
118+
dq_node.meta[QCOM_BYPASS_NODE] = True
119+
elif (
120+
is_graph_output(n)
121+
and n.target in dq_ops
122+
# For tagged quantized I/O, we should not fall back dequantize operation.
123+
and QCOM_QUANTIZED_IO not in n.args[0].args[0].meta
124+
):
125+
n.meta[QCOM_FALLBACK_NODE] = True
126+
q_node = n.args[0]
127+
# Bypass quantize op for graph validation by torch
128+
q_node.meta[QCOM_BYPASS_NODE] = True
145129

146130
def call(self, graph_module: torch.fx.GraphModule):
147131
if not self.force_fold:
148-
self._preserve_qdq(graph_module)
132+
for node in graph_module.graph.nodes:
133+
# skip parameters & buffers
134+
if node.target in dq_ops and is_parameter(
135+
node.args[0], self.edge_program
136+
):
137+
self._annotate_bypass(node)
138+
continue
139+
140+
if self.backend_type == QnnExecuTorchBackendType.kLpaiBackend:
141+
# In LPAI backend v6, there is an accuracy drop for the quantize and dequantize operations.
142+
# To address this, keep the quantize/dequantize operations at the model's input and output.
143+
# For example, input -> q_1 (Fall back) -> dq_1 (Bypass) -> graph -> q_2 (Bypass) -> dq_2 (Fall back) -> output
144+
# Here, q_1 and dq_2 will fall back to CPU, while q_2 and dq_1 will be bypassed in qnn_partition and folded in qnn_preprocess.
145+
self._preserve_qdq(graph_module)
149146
self._fold_dq(graph_module)
150147
self._fold_q(graph_module)
151148
dead_code_elimination_pass(graph_module)

backends/qualcomm/_passes/insert_io_qdq.py

Lines changed: 27 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,14 @@
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-
from typing import Dict
76

87
import torch
8+
from executorch.backends.qualcomm._passes.utils import (
9+
insert_dequant_node,
10+
insert_quant_node,
11+
)
912

10-
from executorch.backends.qualcomm.builders.node_visitor import q_ops
13+
from executorch.backends.qualcomm.builders.node_visitor import q_dq_map, q_ops
1114

1215
from executorch.backends.qualcomm.builders.utils import (
1316
is_mutable_buffer_input,
@@ -18,7 +21,6 @@
1821
QCOM_QUANT_ATTRS,
1922
QCOM_QUANTIZED_IO,
2023
)
21-
from executorch.exir.dialects._ops import ops as exir_ops
2224
from executorch.exir.pass_base import ExportPass, PassResult
2325
from executorch.exir.passes import dead_code_elimination_pass
2426

@@ -31,98 +33,10 @@ class InsertIOQDQ(ExportPass):
3133
right before outputs according to stored quantization encodings.
3234
"""
3335

34-
q_dq_map = {
35-
# per tensor (quantize -> dequantize)
36-
exir_ops.edge.quantized_decomposed.quantize_per_tensor.default: exir_ops.edge.quantized_decomposed.dequantize_per_tensor.tensor,
37-
exir_ops.edge.quantized_decomposed.quantize_per_tensor.tensor: exir_ops.edge.quantized_decomposed.dequantize_per_tensor.tensor,
38-
# per tensor (dequantize -> dequantize, for pre-quantized params)
39-
exir_ops.edge.quantized_decomposed.dequantize_per_tensor.default: exir_ops.edge.quantized_decomposed.dequantize_per_tensor.tensor,
40-
exir_ops.edge.quantized_decomposed.dequantize_per_tensor.tensor: exir_ops.edge.quantized_decomposed.dequantize_per_tensor.tensor,
41-
# per channel (quantize -> dequantize)
42-
exir_ops.edge.quantized_decomposed.quantize_per_channel.default: exir_ops.edge.quantized_decomposed.dequantize_per_channel.default,
43-
# per channel (dequantize -> dequantize, for pre-quantized params)
44-
exir_ops.edge.quantized_decomposed.dequantize_per_channel.default: exir_ops.edge.quantized_decomposed.dequantize_per_channel.default,
45-
}
46-
4736
def __init__(self, edge_program: torch.export.ExportedProgram):
4837
super(InsertIOQDQ, self).__init__()
4938
self.edge_program = edge_program
5039

51-
def _ceate_args(self, target: torch.fx.node.Target, quant_attrs: Dict):
52-
ret = []
53-
54-
arg_schemas = list(target._schema.arguments)[1:]
55-
for arg_schema in arg_schemas:
56-
name = arg_schema.name
57-
# TODO: Due to the new parameter "out_dtype" in the dequantize node,
58-
# it could not be found in the quant_attrs of other nodes,
59-
# and it will cause a key error. For now, the output type
60-
# of our dequantize node is only float. (by default in pytorch)
61-
if name == "out_dtype":
62-
continue
63-
value = quant_attrs[name]
64-
if isinstance(arg_schema.type, torch.Tensor) and (
65-
isinstance(value, int) or isinstance(value, float)
66-
):
67-
value = torch.tensor(value)
68-
ret.append(value)
69-
return ret
70-
71-
def _create_node(
72-
self,
73-
graph_module: torch.fx.GraphModule,
74-
node: torch.fx.node,
75-
target: torch.fx.node.Target,
76-
quant_attrs: Dict = None,
77-
) -> torch.fx.node:
78-
# check if there has a specified quant_attrs
79-
# if not, use the existent info. from current node
80-
if quant_attrs is None:
81-
quant_attrs = node.meta.get(QCOM_QUANT_ATTRS)
82-
83-
inserted_node = graph_module.graph.create_node(
84-
"call_function",
85-
target,
86-
(node, *self._ceate_args(target, quant_attrs)),
87-
)
88-
meta_val = node.meta["val"]
89-
if target in q_ops:
90-
inserted_node.meta[QCOM_QUANT_ATTRS] = node.meta.pop(QCOM_QUANT_ATTRS)
91-
meta_val = meta_val.to(quant_attrs["dtype"])
92-
93-
inserted_node.meta["val"] = meta_val
94-
return inserted_node
95-
96-
def _insert_quant_node(
97-
self,
98-
graph_module: torch.fx.GraphModule,
99-
node: torch.fx.node,
100-
target: torch.fx.node.Target,
101-
quant_attrs: Dict = None,
102-
) -> torch.fx.Node:
103-
with graph_module.graph.inserting_after(node):
104-
users = list(node.users.keys())
105-
inserted_node = self._create_node(graph_module, node, target, quant_attrs)
106-
for user in users:
107-
# If we found mix quantization pattern and reuse the existing q_node, we skip adding a new q node.
108-
if user.target not in q_ops:
109-
user.replace_input_with(node, inserted_node)
110-
111-
return inserted_node
112-
113-
def _insert_dequant_node(
114-
self,
115-
graph_module: torch.fx.GraphModule,
116-
node: torch.fx.node,
117-
target: torch.fx.node.Target,
118-
) -> None:
119-
with graph_module.graph.inserting_after(node):
120-
users = list(node.users.keys())
121-
inserted_node = self._create_node(graph_module, node, target)
122-
for user in users:
123-
if user.op == "output":
124-
user.replace_input_with(node, inserted_node)
125-
12640
def _insert(self, graph_module: torch.fx.GraphModule) -> torch.fx.GraphModule:
12741
# Snapshot nodes: inserting Q/DQ nodes mutates the graph's linked list,
12842
# so iterating the live list can revisit newly inserted nodes.
@@ -140,20 +54,30 @@ def _insert(self, graph_module: torch.fx.GraphModule) -> torch.fx.GraphModule:
14054
or is_mutable_buffer_input(n, self.edge_program)
14155
)
14256
):
143-
self._insert_quant_node(
144-
graph_module, n, n.meta[QCOM_QUANT_ATTRS][QCOM_ENCODING]
145-
)
57+
# Insert a single Q node and connect single Q node to all users
58+
users = list(n.users.keys())
59+
if users:
60+
input_q_node = insert_quant_node(
61+
graph_module=graph_module,
62+
input_node=n,
63+
output_node=users[0],
64+
target=n.meta[QCOM_QUANT_ATTRS][QCOM_ENCODING],
65+
)
66+
for user in users[1:]:
67+
if user.target not in q_ops:
68+
user.replace_input_with(n, input_q_node)
14669

14770
# insert dq before output or fold mix_quantization q if applicable
148-
users = list(n.users.keys())
149-
if n.meta.get(QCOM_QUANT_ATTRS) and any(
150-
user.op == "output" for user in users
151-
):
152-
self._insert_dequant_node(
153-
graph_module,
154-
n,
155-
self.q_dq_map[n.meta[QCOM_QUANT_ATTRS][QCOM_ENCODING]],
156-
)
71+
users = [user for user in n.users if user.op == "output"]
72+
if n.meta.get(QCOM_QUANT_ATTRS) and len(users) != 0:
73+
# We should always only have 1 output node. Expect len(users) == 1
74+
for user in users:
75+
_ = insert_dequant_node(
76+
graph_module=graph_module,
77+
input_node=n,
78+
output_node=user,
79+
target=q_dq_map[n.meta[QCOM_QUANT_ATTRS][QCOM_ENCODING]],
80+
)
15781

15882
def call(self, graph_module: torch.fx.GraphModule):
15983
self._insert(graph_module)

0 commit comments

Comments
 (0)