Skip to content

Commit 8e9da43

Browse files
pytorchbotrascani
andauthored
Promote NXP redundant Q/DQ cleanup to to_edge (#22913)
Eval-mode dropout can disappear before shared no-op cleanup runs, leaving adjacent dequantize/quantize conversions. Promote NXP's `FoldRedundantDequantizeQuantizePass` from #21016 into the shared lowering sequence used by `to_edge` and `to_edge_transform_and_lower`. Cortex-M MLPerf Tiny DS-CNN then finishes with one quantize and one dequantize operation in both layouts, down from three of each. MobileNetV2 similarly drops from two to one of each. The shared `eliminate_dq_q` matcher remains unchanged, keeping this release patch focused on promoting the existing cleanup. Remove the stale dropout target from `RemoveNoopPass`. NXP retains a later invocation of the shared pass because its auxiliary-operator splitting can introduce new adjacent pairs. Update the Cortex-M model counts and correct the clone regression's obsolete Q/DQ signatures so the test also checks numerical equivalence. Validation on PyTorch 2.14 CPU: 77 EXIR and clone-transform tests passed, with 28 passing subtests and one expected failure. The dropout regression covers both edge APIs, per-tensor and per-channel quantization, and int8/uint8/int16/int32. All 25 selected Cortex-M DS-CNN, MobileNetV2, and explicit-layout tests passed, including Corstone-300 FVP implementation tests. Additional explicit-layout DS-CNN and legacy `ExirExportedProgram.to_edge` numerical checks passed. Formatting, flake8, and `git diff --check` passed. Authored with AI assistance from OpenAI Codex. Co-authored-by: RJ Ascani <rja@meta.com>
1 parent 65bfc94 commit 8e9da43

12 files changed

Lines changed: 157 additions & 72 deletions

File tree

backends/cortex_m/test/models/test_ds_cnn.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,15 @@
1616
"executorch_exir_dialects_edge__ops_aten_relu_default": 9,
1717
"executorch_exir_dialects_edge__ops_aten_view_copy_default": 1,
1818
"executorch_exir_dialects_edge__ops_quantized_decomposed_dequantize_per_channel_default": 18,
19-
"executorch_exir_dialects_edge__ops_quantized_decomposed_dequantize_per_tensor_default": 17,
20-
"executorch_exir_dialects_edge__ops_quantized_decomposed_quantize_per_tensor_default": 15,
19+
"executorch_exir_dialects_edge__ops_quantized_decomposed_dequantize_per_tensor_default": 15,
20+
"executorch_exir_dialects_edge__ops_quantized_decomposed_quantize_per_tensor_default": 13,
2121
}
2222

2323
ops_after_transforms: dict[str, int] = {
2424
"executorch_exir_dialects_edge__ops_aten_view_copy_default": 1,
25-
"executorch_exir_dialects_edge__ops_cortex_m_dequantize_per_tensor_default": 3,
25+
"executorch_exir_dialects_edge__ops_cortex_m_dequantize_per_tensor_default": 1,
2626
"executorch_exir_dialects_edge__ops_cortex_m_pad_default": 1,
27-
"executorch_exir_dialects_edge__ops_cortex_m_quantize_per_tensor_default": 3,
27+
"executorch_exir_dialects_edge__ops_cortex_m_quantize_per_tensor_default": 1,
2828
"executorch_exir_dialects_edge__ops_cortex_m_quantized_avg_pool2d_default": 1,
2929
"executorch_exir_dialects_edge__ops_cortex_m_quantized_conv2d_default": 4,
3030
"executorch_exir_dialects_edge__ops_cortex_m_quantized_depthwise_conv2d_default": 5,

backends/cortex_m/test/models/test_mobilenet_v2.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,14 @@
2121
"executorch_exir_dialects_edge__ops_aten_linear_default": 1,
2222
"executorch_exir_dialects_edge__ops_aten_view_copy_default": 1,
2323
"executorch_exir_dialects_edge__ops_quantized_decomposed_dequantize_per_channel_default": 104,
24-
"executorch_exir_dialects_edge__ops_quantized_decomposed_dequantize_per_tensor_default": 79,
25-
"executorch_exir_dialects_edge__ops_quantized_decomposed_quantize_per_tensor_default": 67,
24+
"executorch_exir_dialects_edge__ops_quantized_decomposed_dequantize_per_tensor_default": 78,
25+
"executorch_exir_dialects_edge__ops_quantized_decomposed_quantize_per_tensor_default": 66,
2626
}
2727

2828
ops_after_transforms: dict[str, int] = {
2929
"executorch_exir_dialects_edge__ops_aten_view_copy_default": 1,
30-
"executorch_exir_dialects_edge__ops_cortex_m_dequantize_per_tensor_default": 2,
31-
"executorch_exir_dialects_edge__ops_cortex_m_quantize_per_tensor_default": 2,
30+
"executorch_exir_dialects_edge__ops_cortex_m_dequantize_per_tensor_default": 1,
31+
"executorch_exir_dialects_edge__ops_cortex_m_quantize_per_tensor_default": 1,
3232
"executorch_exir_dialects_edge__ops_cortex_m_quantized_add_default": 10,
3333
"executorch_exir_dialects_edge__ops_cortex_m_quantized_avg_pool2d_default": 1,
3434
"executorch_exir_dialects_edge__ops_cortex_m_quantized_conv2d_default": 35,

backends/nxp/BUCK

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ fbcode_target(_kind = runtime.python_library,
3131
"//caffe2:torch",
3232
"//executorch/exir:lib",
3333
"//executorch/exir:pass_manager",
34+
"//executorch/exir/passes:fold_redundant_qdq_pass",
3435
],
3536
)
3637

backends/nxp/edge_passes/neutron_edge_pass_manager.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,28 +6,29 @@
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-
)
129
from executorch.backends.nxp.edge_passes.move_auxiliary_operator_into_separate_qdq_cluster_pass import (
1310
MoveLeadingAuxiliaryOperatorIntoSeparateQDQClusterPass,
1411
MoveTrailingAuxiliaryOperatorIntoSeparateQDQClusterPass,
1512
)
16-
from executorch.backends.nxp.edge_passes.neutron_edge_pass import NeutronEdgePass
1713
from executorch.backends.nxp.edge_passes.remove_as_strided_copy_nodes import (
1814
RemoveUselessAsStridedCopyNodes,
1915
)
20-
from torch.fx.passes.infra.pass_manager import PassManager
16+
from executorch.exir.pass_base import ExportPass
17+
from executorch.exir.pass_manager import PassManager
18+
from executorch.exir.passes.fold_redundant_qdq_pass import (
19+
FoldRedundantDequantizeQuantizePass,
20+
)
2121

2222

2323
class NeutronEdgePassManager(PassManager):
2424

25-
def __init__(self, passes: list[NeutronEdgePass] = None):
26-
passes: list[NeutronEdgePass] = passes or [
25+
def __init__(self, passes: list[ExportPass] = None):
26+
passes: list[ExportPass] = passes or [
2727
MoveLeadingAuxiliaryOperatorIntoSeparateQDQClusterPass(),
2828
MoveTrailingAuxiliaryOperatorIntoSeparateQDQClusterPass(),
2929
RemoveUselessAsStridedCopyNodes(),
3030
ConvertReshapingNodesToViewPass(),
31+
# Auxiliary-op splitting can introduce DQ -> Q on fanout branches.
3132
FoldRedundantDequantizeQuantizePass(),
3233
]
3334

backends/nxp/tests/test_fold_redundant_qdq.py

Lines changed: 1 addition & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,6 @@
66

77
import torch
88

9-
from executorch.backends.nxp.edge_passes.fold_redundant_qdq_pass import (
10-
FoldRedundantDequantizeQuantizePass,
11-
)
129
from executorch.backends.nxp.tests.executorch_pipeline import to_quantized_edge_program
1310

1411
ExecutorchDelegateCall = torch.ops.higher_order.executorch_call_delegate
@@ -54,33 +51,10 @@ def _count_delegates(edge_program) -> int:
5451

5552

5653
def test_fold_pass_present_merges_into_single_delegate():
57-
# The fold pass is part of the default NeutronEdgePassManager.
54+
# The fold pass runs during to_edge, before Neutron partitioning.
5855
edge_program = to_quantized_edge_program(ConvDropoutConvModule(), INPUT_SHAPE)
5956

6057
num_delegates = _count_delegates(edge_program)
6158
assert (
6259
num_delegates == 1
6360
), 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,)

backends/transforms/test/test_remove_clone_ops.py

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def test_dq_clone_q_linear(self):
3737
Test RemoveCloneOpsTransform on a graph with d/q -> clone -> q -> linear pattern
3838
3939
Before: Should contain all nodes
40-
After: Should only have the linear operation
40+
After: Should only have the final dequantize and linear operations
4141
"""
4242

4343
# Create a graph module directly with the pattern: quant -> clone -> dequant -> fp linear
@@ -60,19 +60,13 @@ def forward(self, x):
6060
input_node = graph.placeholder("x")
6161

6262
# Create nodes for our pattern: quant -> clone -> dequant -> fp linear
63-
# Constants for quantization parameters
64-
scale = graph.create_node(
65-
"call_function", torch.tensor, args=([0.1],), kwargs={}
66-
)
67-
zero_point = graph.create_node(
68-
"call_function", torch.tensor, args=([0],), kwargs={}
69-
)
63+
qparams = (0.1, 0, -128, 127, torch.int8)
7064

7165
# Dequantize node
7266
dequant_node = graph.create_node(
7367
"call_function",
7468
torch.ops.quantized_decomposed.dequantize_per_tensor.default,
75-
args=(input_node, scale, zero_point, torch.int8),
69+
args=(input_node, *qparams),
7670
kwargs={},
7771
)
7872

@@ -89,19 +83,20 @@ def forward(self, x):
8983
quant_node = graph.create_node(
9084
"call_function",
9185
torch.ops.quantized_decomposed.quantize_per_tensor.default,
92-
args=(clone_node, scale, zero_point, torch.int8),
86+
args=(clone_node, *qparams),
9387
kwargs={},
9488
)
9589

96-
# Linear node (using the module's linear layer)
97-
# Technically, should use quantized weight and bias
98-
# but we are just inspecting graph patterns in this test
90+
linear_input = graph.call_function(
91+
torch.ops.quantized_decomposed.dequantize_per_tensor.default,
92+
args=(quant_node, *qparams),
93+
)
9994
weight = graph.create_node("get_attr", "linear.weight")
10095
bias = graph.create_node("get_attr", "linear.bias")
10196
linear_node = graph.create_node(
10297
"call_function",
10398
torch.nn.functional.linear,
104-
args=(quant_node, weight, bias),
99+
args=(linear_input, weight, bias),
105100
kwargs={},
106101
)
107102

@@ -125,12 +120,16 @@ def forward(self, x):
125120
)
126121

127122
# Apply the transform
123+
inputs = torch.randint(-128, 128, (2, 10), dtype=torch.int8)
124+
expected = gm(inputs)
128125
transformed_gm = RemoveCloneOpsTransform()(gm).graph_module
129126

130127
# Verify the dq -> clone -> q pattern is removed and linear op is still present using FileCheck
131128
FileCheck().check_not(
132129
"executorch_exir_dialects_edge__ops_aten_clone_default"
133-
).check_not("quantized_decomposed.dequantize_per_tensor.default").check_not(
130+
).check_count(
131+
"quantized_decomposed.dequantize_per_tensor.default", 1, exactly=True
132+
).check_not(
134133
"quantized_decomposed.quantize_per_tensor.default"
135134
).check_count(
136135
"torch._C._nn.linear",
@@ -139,6 +138,7 @@ def forward(self, x):
139138
).run(
140139
transformed_gm.code
141140
)
141+
torch.testing.assert_close(transformed_gm(inputs), expected, rtol=0, atol=0)
142142

143143
def test_clone_non_identity_survives(self):
144144
"""Verify clone ops that modify memory_format are preserved by RemoveCloneOpsTransform."""

exir/passes/BUCK

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ fbcode_target(_kind = runtime.python_library,
1313
":convert_constant_dim_order_pass",
1414
":debug_handle_generator_pass",
1515
":external_constants_pass",
16+
":fold_redundant_qdq_pass",
1617
":init_mutable_pass",
1718
":insert_write_back_for_buffers_pass",
1819
":legalize_portable_dim_order_pass",
@@ -233,6 +234,18 @@ fbcode_target(_kind = runtime.python_library,
233234
],
234235
)
235236

237+
fbcode_target(_kind = runtime.python_library,
238+
name = "fold_redundant_qdq_pass",
239+
srcs = [
240+
"fold_redundant_qdq_pass.py",
241+
],
242+
deps = [
243+
":remove_noop_pass",
244+
"//caffe2:torch",
245+
"//executorch/exir:pass_base",
246+
],
247+
)
248+
236249
fbcode_target(_kind = runtime.python_library,
237250
name = "remove_noop_pass",
238251
srcs = [

exir/passes/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@
3737
from executorch.exir.passes.debug_handle_generator_pass import DebugHandleGeneratorPass
3838

3939
from executorch.exir.passes.executorch_prim_ops_registry import _EXECUTORCH_SYM_OPS
40+
from executorch.exir.passes.fold_redundant_qdq_pass import (
41+
FoldRedundantDequantizeQuantizePass,
42+
)
4043
from executorch.exir.passes.insert_write_back_for_buffers_pass import (
4144
insert_write_back_for_buffers_pass,
4245
)
@@ -517,6 +520,7 @@ def dead_code_elimination_pass(graph_module: torch.fx.GraphModule) -> PassResult
517520
RemoveNoopPass(),
518521
PruneEmptyTensorsPass(),
519522
RemoveToCopyPass(),
523+
FoldRedundantDequantizeQuantizePass(),
520524
]
521525
).passes
522526
)

backends/nxp/edge_passes/fold_redundant_qdq_pass.py renamed to exir/passes/fold_redundant_qdq_pass.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,19 @@
66

77
import torch
88

9-
from executorch.backends.nxp.edge_passes.neutron_edge_pass import NeutronEdgePass
9+
from executorch.exir.pass_base import ExportPass
1010
from executorch.exir.passes.remove_noop_pass import _DEQUANT_OPS, eliminate_dq_q
1111
from torch.fx.passes.infra.pass_base import PassResult
1212

1313

14-
class FoldRedundantDequantizeQuantizePass(NeutronEdgePass):
15-
"""Fold redundant ``dequantize -> quantize`` pairs with identical qparams.
14+
class FoldRedundantDequantizeQuantizePass(ExportPass):
15+
"""Fold adjacent ``dequantize -> quantize`` pairs using the shared qparam matcher.
1616
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.
17+
Decomposition can erase a quantized no-op, such as eval-mode dropout, leaving
18+
its surrounding dequantize and quantize nodes adjacent.
2219
"""
2320

24-
def run(self, graph_module: torch.fx.GraphModule) -> PassResult:
21+
def call(self, graph_module: torch.fx.GraphModule) -> PassResult:
2522
dequant_nodes = [
2623
node
2724
for node in graph_module.graph.nodes
@@ -32,5 +29,8 @@ def run(self, graph_module: torch.fx.GraphModule) -> PassResult:
3229
eliminate_dq_q(graph_module, dequant_nodes)
3330
graph_module.graph.eliminate_dead_code()
3431
modified = len(graph_module.graph.nodes) != num_nodes_before
32+
if modified:
33+
graph_module.graph.lint()
34+
graph_module.recompile()
3535

3636
return PassResult(graph_module, modified)

exir/passes/remove_noop_pass.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,6 @@ def call(self, graph_module: GraphModule) -> PassResult:
6161

6262
if node.target not in (
6363
torch.ops.aten.to.dtype,
64-
torch.ops.aten.dropout.default,
6564
torch.ops.aten.slice_copy.Tensor,
6665
):
6766
continue

0 commit comments

Comments
 (0)