Skip to content

Commit cc9acaa

Browse files
authored
Emit Conv1d output layout copies after their squeeze (#24256)
Summary: On the explicit-layout pass list, a Conv1d is lowered as a Conv2d with a unit height. `CortexMReplaceOpsWithChannelsLastVariants` wrapped each convolution output in a rank-4 NHWC-to-NCHW layout copy, followed by the squeeze that `ConvertConv1dToConv2dPass` adds to get back to rank 3. `RemovePermutesAroundElementwiseOps` cannot match a rank-3 region against a rank-4 copy behind a squeeze, so in residual or chained Conv1d blocks the layout copies between convolutions survived as transposes. `CortexMReplaceOpsWithChannelsLastVariants` now emits such an output as a squeeze followed by a rank-3 layout copy (`[0, 2, 1]`), reusing the existing squeeze node. It does this only when the convolution's sole user is a `squeeze_copy.dim(conv, 2)` with a rank-3 result; every other output keeps the rank-4 copy. One side effect: when user code transposes a Conv1d output around elementwise ops, the layout copy and the user's `transpose(1, 2)` are now directly adjacent. `RemovePermutesAroundElementwiseOps` cannot merge adjacent permutes and cancels the user's two transposes against each other instead, so both layout copies survive: Conv1d -> transpose -> relu -> transpose -> Conv1d gets four transposes instead of two. The next change fixes this by running `FuseCascadedTransposeOrPermuteOps` before `RemovePermutesAroundElementwiseOps`. A Conv1d -> tanh -> Conv1d -> add -> relu -> Conv1d chain now lowers with only the two boundary transposes instead of six. This makes more regions reachable for `RemovePermutesAroundElementwiseOps`, including regions with lower-rank constants, such as a `[C, L]` or `[L]` buffer added to two Conv1d outputs. The pass's compensation for those constants is fixed separately in the shared pass, and this change depends on that fix. Authored with an AI coding assistant (Claude). Differential Revision: D128001279 Pull Request resolved: #24256
1 parent 8d34671 commit cc9acaa

2 files changed

Lines changed: 103 additions & 1 deletion

File tree

‎backends/cortex_m/passes/explicit_layout_pass.py‎

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
# This source code is licensed under the BSD-style license found in the
66
# LICENSE file in the root directory of this source tree.
77

8+
from typing import cast
9+
810
import executorch.backends.transforms.channels_last_ops # noqa: F401
911

1012
import torch
@@ -26,7 +28,7 @@
2628
from executorch.exir import ExportedProgram
2729
from executorch.exir.dialects._ops import ops as exir_ops
2830
from executorch.exir.pass_base import ExportPass, PassResult
29-
from torch.fx import GraphModule, Node
31+
from torch.fx import Graph, GraphModule, Node
3032
from torch.fx.node import Target
3133

3234

@@ -74,6 +76,17 @@ def _supports_max_pool2d(node: Node) -> bool:
7476
)
7577

7678

79+
def _is_nchw_to_ncw_squeeze(node: Node) -> bool:
80+
# The squeeze ConvertConv1dToConv2dPass emits: squeeze_copy.dim(conv, 2).
81+
return (
82+
node.target == exir_ops.edge.aten.squeeze_copy.dim
83+
# Squeezes NCHW H; % 4 normalizes a negative dim of the rank-4 input.
84+
and cast(int, node.args[1]) % 4 == 2
85+
# Squeezing a non-unit dim is a no-op that keeps rank 4.
86+
and node.meta["val"].dim() == 3
87+
)
88+
89+
7790
_EXPLICIT_LAYOUT_OP_MAP: dict[Target, ChannelsLastOpSpec] = {
7891
exir_ops.edge.aten.convolution.default: ChannelsLastOpSpec(
7992
target=exir_ops.edge.channels_last.convolution.default,
@@ -100,6 +113,30 @@ class CortexMReplaceOpsWithChannelsLastVariants(ReplaceOpsWithChannelsLastVarian
100113
def __init__(self, exported_program: ExportedProgram) -> None:
101114
super().__init__(exported_program, op_map=dict(_EXPLICIT_LAYOUT_OP_MAP))
102115

116+
# Emit a Conv1d-as-Conv2d output as squeeze -> rank-3 layout copy rather
117+
# than rank-4 copy -> squeeze, so RemovePermutesAroundElementwiseOps can
118+
# cancel the copy against the elementwise ops that consume it.
119+
@staticmethod
120+
def _permute_node_output(
121+
graph: Graph,
122+
node_output: Node,
123+
original_node_output: Node,
124+
implicit_batch: bool,
125+
) -> None:
126+
users = list(original_node_output.users)
127+
if len(users) != 1 or not _is_nchw_to_ncw_squeeze(users[0]):
128+
ReplaceOpsWithChannelsLastVariants._permute_node_output(
129+
graph, node_output, original_node_output, implicit_batch
130+
)
131+
return
132+
squeeze = users[0]
133+
squeeze.args = (node_output, 1)
134+
with graph.inserting_after(squeeze):
135+
permute = graph.call_function(LAYOUT_PERMUTE_COPY, (squeeze, [0, 2, 1]))
136+
squeeze.replace_all_uses_with(
137+
permute, delete_user_cb=lambda n: n is not permute
138+
)
139+
103140

104141
class CortexMCanonicalizeViewCopyPermutePass(CanonicalizeViewCopyPermutePass):
105142
def __init__(self) -> None:

‎backends/cortex_m/test/test_explicit_layout_pipeline.py‎

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
CortexMPassManager,
1414
LiftConstantTensorsPass,
1515
)
16+
from executorch.backends.cortex_m.passes.explicit_layout_pass import (
17+
_is_nchw_to_ncw_squeeze,
18+
)
1619
from executorch.backends.cortex_m.quantizer.quantizer import CortexMQuantizer
1720
from executorch.backends.cortex_m.target_config import CortexM, CortexMTargetConfig
1821
from executorch.backends.cortex_m.test.tester import (
@@ -88,6 +91,18 @@ def forward(self, x):
8891
return self.conv2(self.conv1(torch.tanh(x)).view(1, 4, 32))
8992

9093

94+
class Conv1dChain(torch.nn.Module):
95+
def __init__(self) -> None:
96+
super().__init__()
97+
self.conv1 = torch.nn.Conv1d(8, 8, 3, padding=1)
98+
self.conv2 = torch.nn.Conv1d(8, 8, 3, padding=1)
99+
self.conv3 = torch.nn.Conv1d(8, 8, 1)
100+
101+
def forward(self, x):
102+
y = torch.tanh(self.conv1(x))
103+
return self.conv3(torch.relu(self.conv2(y) + y))
104+
105+
91106
class Apply(torch.nn.Module):
92107
def __init__(self, fn) -> None:
93108
super().__init__()
@@ -669,3 +684,53 @@ def test_unquantized_constant_is_not_precomputed(use_explicit_layout):
669684

670685
assert _count(program, exir_ops.edge.aten.full.default) == 1
671686
tester.run_method_and_compare_outputs(inputs=inputs)
687+
688+
689+
def test_conv1d_chain_keeps_only_the_boundary_layout_copies():
690+
torch.manual_seed(0)
691+
inputs = (torch.randn(1, 8, 16),)
692+
tester = _run_explicit_layout_passes(CortexMTester(Conv1dChain().eval(), inputs))
693+
program = tester.get_artifact(StageType.RUN_PASSES).exported_program()
694+
695+
assert _count(program, exir_ops.edge.cortex_m.quantized_conv2d_nhwc.default) == 3
696+
assert [
697+
node.args[1]
698+
for node in program.graph.find_nodes(
699+
op="call_function", target=exir_ops.edge.cortex_m.transpose.default
700+
)
701+
] == [[0, 2, 1], [0, 2, 1]]
702+
assert _count(program, exir_ops.edge.aten.permute_copy.default) == 0
703+
tester.run_method_and_compare_outputs(inputs=inputs, qtol=1)
704+
705+
706+
def test_conv1d_output_is_squeezed_before_its_layout_copy():
707+
torch.manual_seed(0)
708+
inputs = (torch.randn(1, 8, 16),)
709+
tester = _run_explicit_layout_passes(
710+
CortexMTester(torch.nn.Conv1d(8, 4, 3).eval(), inputs)
711+
)
712+
program = tester.get_artifact(StageType.RUN_PASSES).exported_program()
713+
714+
(conv,) = program.graph.find_nodes(
715+
op="call_function", target=exir_ops.edge.cortex_m.quantized_conv2d_nhwc.default
716+
)
717+
(squeeze,) = conv.users
718+
(layout_copy,) = squeeze.users
719+
assert squeeze.target == exir_ops.edge.aten.view_copy.default
720+
assert squeeze.meta["val"].shape == (1, 14, 4)
721+
assert layout_copy.target == exir_ops.edge.cortex_m.transpose.default
722+
assert layout_copy.args[1] == [0, 2, 1]
723+
tester.run_method_and_compare_outputs(inputs=inputs, qtol=1)
724+
725+
726+
@pytest.mark.parametrize(
727+
"dim,expected", [(2, True), (-2, True), (1, False), (-1, False)]
728+
)
729+
def test_only_a_height_squeeze_is_moved_above_the_layout_copy(dim, expected):
730+
graph = torch.fx.Graph()
731+
squeeze = graph.call_function(
732+
exir_ops.edge.aten.squeeze_copy.dim, (graph.placeholder("x"), dim)
733+
)
734+
squeeze.meta["val"] = torch.empty(1, 8, 16)
735+
736+
assert _is_nchw_to_ncw_squeeze(squeeze) is expected

0 commit comments

Comments
 (0)