Skip to content

Commit b532ec1

Browse files
authored
Fold redundant DQ - > Q (#21016)
Differential Revision: D112564036 Pull Request resolved: #21016
1 parent 4c9beee commit b532ec1

4 files changed

Lines changed: 139 additions & 0 deletions

File tree

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
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 torch
8+
9+
from executorch.backends.nxp.edge_passes.neutron_edge_pass import NeutronEdgePass
10+
from executorch.exir.passes.remove_noop_pass import _DEQUANT_OPS, eliminate_dq_q
11+
from torch.fx.passes.infra.pass_base import PassResult
12+
13+
14+
class FoldRedundantDequantizeQuantizePass(NeutronEdgePass):
15+
"""Fold redundant ``dequantize -> quantize`` pairs with identical qparams.
16+
17+
A dequantize immediately followed by a quantize at identical qparams is the
18+
identity on the already-quantized value, so this pass reuses the shared
19+
``eliminate_dq_q`` helper to rewire each such quantize's consumers to the
20+
dequantize's quantized input, removing the island and letting the neighboring
21+
clusters delegate as a single subgraph.
22+
"""
23+
24+
def run(self, graph_module: torch.fx.GraphModule) -> PassResult:
25+
dequant_nodes = [
26+
node
27+
for node in graph_module.graph.nodes
28+
if node.op == "call_function" and node.target in _DEQUANT_OPS
29+
]
30+
31+
num_nodes_before = len(graph_module.graph.nodes)
32+
eliminate_dq_q(graph_module, dequant_nodes)
33+
graph_module.graph.eliminate_dead_code()
34+
modified = len(graph_module.graph.nodes) != num_nodes_before
35+
36+
return PassResult(graph_module, modified)

backends/nxp/edge_passes/neutron_edge_pass_manager.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
from executorch.backends.nxp.edge_passes.convert_reshaping_nodes_to_view import (
77
ConvertReshapingNodesToViewPass,
88
)
9+
from executorch.backends.nxp.edge_passes.fold_redundant_qdq_pass import (
10+
FoldRedundantDequantizeQuantizePass,
11+
)
912
from executorch.backends.nxp.edge_passes.move_auxiliary_operator_into_separate_qdq_cluster_pass import (
1013
MoveLeadingAuxiliaryOperatorIntoSeparateQDQClusterPass,
1114
MoveTrailingAuxiliaryOperatorIntoSeparateQDQClusterPass,
@@ -25,6 +28,7 @@ def __init__(self, passes: list[NeutronEdgePass] = None):
2528
MoveTrailingAuxiliaryOperatorIntoSeparateQDQClusterPass(),
2629
RemoveUselessAsStridedCopyNodes(),
2730
ConvertReshapingNodesToViewPass(),
31+
FoldRedundantDequantizeQuantizePass(),
2832
]
2933

3034
super().__init__(

backends/nxp/tests/BUCK

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,19 @@ fbcode_target(_kind = python_pytest,
106106
],
107107
)
108108

109+
fbcode_target(_kind = python_pytest,
110+
name = "test_fold_redundant_qdq",
111+
srcs = [
112+
"test_fold_redundant_qdq.py",
113+
],
114+
deps = [
115+
"//caffe2:torch",
116+
"//executorch/backends/nxp:edge_passes",
117+
"//executorch/backends/nxp:neutron_backend",
118+
":executorch_pipeline",
119+
],
120+
)
121+
109122
fbcode_target(_kind = python_pytest,
110123
name = "test_convert_1d_conv_to_2d",
111124
srcs = [
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
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 torch
8+
9+
from executorch.backends.nxp.edge_passes.fold_redundant_qdq_pass import (
10+
FoldRedundantDequantizeQuantizePass,
11+
)
12+
from executorch.backends.nxp.tests.executorch_pipeline import to_quantized_edge_program
13+
14+
ExecutorchDelegateCall = torch.ops.higher_order.executorch_call_delegate
15+
16+
17+
class ConvDropoutConvModule(torch.nn.Module):
18+
"""Two conv clusters separated by an eval-mode dropout (an identity).
19+
20+
In eval mode the dropout is a no-op, but the quantizer still wraps it in a
21+
shared-spec ``dequantize -> quantize`` cluster (identical scale/zero-point on
22+
both sides). Lowering eliminates the dropout itself, yet leaves that wrapping
23+
quantization behind as a redundant ``dequantize -> quantize`` pair with no
24+
compute node between the two conv clusters. The Neutron partitioner cannot
25+
delegate a compute-free QDQ island, so it splits ``conv1`` and ``conv2`` into
26+
two separate subgraphs -- the same split that
27+
``FoldRedundantDequantizeQuantizePass`` was written to prevent. Folding that
28+
pair away lets both convs delegate as a single subgraph.
29+
"""
30+
31+
def __init__(self) -> None:
32+
super().__init__()
33+
self.conv1 = torch.nn.Conv2d(4, 8, kernel_size=2, bias=False)
34+
self.dropout = torch.nn.Dropout(0.5)
35+
self.conv2 = torch.nn.Conv2d(8, 8, kernel_size=2, bias=False)
36+
37+
def forward(self, x: torch.Tensor) -> torch.Tensor:
38+
x = self.conv1(x)
39+
x = self.dropout(x)
40+
x = self.conv2(x)
41+
return x
42+
43+
44+
def _count_delegates(edge_program) -> int:
45+
graph = edge_program.exported_program().graph_module.graph
46+
return sum(
47+
1
48+
for node in graph.nodes
49+
if node.op == "call_function" and node.target == ExecutorchDelegateCall
50+
)
51+
52+
53+
INPUT_SHAPE = (1, 4, 8, 8)
54+
55+
56+
def test_fold_pass_present_merges_into_single_delegate():
57+
# The fold pass is part of the default NeutronEdgePassManager.
58+
edge_program = to_quantized_edge_program(ConvDropoutConvModule(), INPUT_SHAPE)
59+
60+
num_delegates = _count_delegates(edge_program)
61+
assert (
62+
num_delegates == 1
63+
), f"expected a single delegate with the fold pass, got {num_delegates}"
64+
65+
66+
def test_fold_pass_removes_redundant_qdq():
67+
graph = torch.fx.Graph()
68+
quantized_input = graph.placeholder("quantized_input")
69+
qparams = (0.25, 3, -128, 127, torch.int8)
70+
dequantize = graph.call_function(
71+
torch.ops.quantized_decomposed.dequantize_per_tensor.default,
72+
args=(quantized_input, *qparams),
73+
)
74+
quantize = graph.call_function(
75+
torch.ops.quantized_decomposed.quantize_per_tensor.default,
76+
args=(dequantize, *qparams),
77+
)
78+
graph.output(quantize)
79+
graph_module = torch.fx.GraphModule(torch.nn.Module(), graph)
80+
81+
result = FoldRedundantDequantizeQuantizePass().run(graph_module)
82+
83+
assert result.modified
84+
remaining_nodes = list(result.graph_module.graph.nodes)
85+
assert [node.op for node in remaining_nodes] == ["placeholder", "output"]
86+
assert remaining_nodes[-1].args == (quantized_input,)

0 commit comments

Comments
 (0)