Skip to content

Commit 0b139c1

Browse files
pssrawatfacebook-github-bot
authored andcommitted
Postpone conv1d output permutes below their squeeze (#24256)
Summary: On the explicit-layout pass list, a Conv1d is lowered as a Conv2d, so each convolution's NHWC output goes through a rank-4 layout copy followed by a squeeze back to rank 3. `RemovePermutesAroundElementwiseOps` cancels permutes around a region of elementwise ops, but it can only cross a squeeze or unsqueeze while walking downstream. When a region walks upstream into a neighbouring convolution's output, for example through the second operand of a residual add, it reaches that squeeze from below, carrying a rank-3 permutation with a rank-4 permute behind it. It discards the whole region, and the layout copies between consecutive convolutions survive as transposes. Add `CortexMRemovePermutesAroundElementwiseOps`, which sinks each layout copy below a following squeeze and then runs `RemovePermutesAroundElementwiseOps`. The sinking is done by `CortexMPostponeLayoutCopyBelowSqueezePass`, a restricted `PostponePermuteOpBelowSqueezeOrUnsqueezeLikeView`: it only moves the layout copies this pipeline inserts, leaving user permutes in place, and never moves one below an unsqueeze, which would raise its rank, up to a rank-5 layout copy that Cortex-M cannot run. After sinking, a convolution output is a squeeze followed by a rank-3 layout copy, so regions meet copies of their own rank and cancel as they do for Conv2d. The sinking runs inside the pass, outside the pass manager's dtype-preserving retrace, so when only the sinking changed the graph the pass retraces once more to restore the folded int8 dtypes. Authored with an AI coding assistant (Claude). Differential Revision: D128001279
1 parent 91a1d5f commit 0b139c1

5 files changed

Lines changed: 105 additions & 4 deletions

File tree

‎backends/cortex_m/passes/BUCK‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ fbcode_target(_kind = runtime.python_library,
5858
"//executorch/backends/transforms:channels_last_layout",
5959
"//executorch/backends/transforms:channels_last_ops",
6060
"//executorch/backends/transforms:convert_conv1d_to_conv2d_pass",
61+
"//executorch/backends/transforms:postpone_permute_below_squeeze_view",
6162
"//executorch/backends/transforms:remove_getitem_op",
6263
"//executorch/backends/transforms:remove_permutes_around_elementwise_ops",
6364
"//executorch/backends/transforms:remove_unused_constants_pass",

‎backends/cortex_m/passes/cortex_m_pass_manager.py‎

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,6 @@
2121
ConvertConv1dToConv2dPass,
2222
)
2323
from executorch.backends.transforms.remove_getitem_op import RemoveGetItemPass
24-
from executorch.backends.transforms.remove_permutes_around_elementwise_ops import (
25-
RemovePermutesAroundElementwiseOps,
26-
)
2724
from executorch.backends.transforms.remove_unused_constants_pass import (
2825
RemoveUnusedConstantsPass,
2926
)
@@ -55,6 +52,7 @@
5552
from .expand_as_to_expand_pass import ExpandAsToExpandPass
5653
from .explicit_layout_pass import (
5754
CortexMCanonicalizeViewCopyPermutePass,
55+
CortexMRemovePermutesAroundElementwiseOps,
5856
CortexMReplaceOpsWithChannelsLastVariants,
5957
ValidateCortexMExplicitLayoutPass,
6058
)
@@ -176,7 +174,7 @@ class CortexMPassManager(ExportedProgramPassManager):
176174
CortexMReplaceOpsWithChannelsLastVariants,
177175
ReplaceSqueezeAndUnsqueezeWithViewPass,
178176
# Move layout copies across pads before singleton permutations become views.
179-
RemovePermutesAroundElementwiseOps,
177+
CortexMRemovePermutesAroundElementwiseOps,
180178
CortexMCanonicalizeViewCopyPermutePass,
181179
ValidateCortexMExplicitLayoutPass,
182180
ReplaceQuantNodesPass,

‎backends/cortex_m/passes/explicit_layout_pass.py‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@
1919
LAYOUT_PERMUTE_COPY,
2020
PERMUTE_COPY_TARGETS,
2121
)
22+
from executorch.backends.transforms.postpone_permute_below_squeeze_view import (
23+
PostponePermuteOpBelowSqueezeOrUnsqueezeLikeView,
24+
)
25+
from executorch.backends.transforms.remove_permutes_around_elementwise_ops import (
26+
RemovePermutesAroundElementwiseOps,
27+
)
2228
from executorch.backends.transforms.replace_ops_with_channels_last_variants import (
2329
ChannelsLastOpSpec,
2430
ReplaceOpsWithChannelsLastVariants,
@@ -117,6 +123,46 @@ def _set_node_op(self, node, target, input_node, arg) -> None:
117123
)
118124

119125

126+
class CortexMPostponeLayoutCopyBelowSqueezePass(
127+
PostponePermuteOpBelowSqueezeOrUnsqueezeLikeView
128+
):
129+
"""Sink a layout copy below a following squeeze, never below an unsqueeze.
130+
131+
Only the layout copies this pipeline inserts are moved; user permutes keep
132+
their place. Sinking below an unsqueeze would raise the copy's rank, up to
133+
a rank-5 layout copy that Cortex-M cannot run.
134+
"""
135+
136+
def maybe_remove_or_replace(self, node: Node) -> bool:
137+
if node.target != LAYOUT_PERMUTE_COPY:
138+
return False
139+
users = list(node.users)
140+
user_value = users[0].meta.get("val") if len(users) == 1 else None
141+
value = node.meta.get("val")
142+
if (
143+
isinstance(user_value, torch.Tensor)
144+
and isinstance(value, torch.Tensor)
145+
and user_value.dim() > value.dim()
146+
):
147+
return False
148+
return super().maybe_remove_or_replace(node)
149+
150+
151+
class CortexMRemovePermutesAroundElementwiseOps(RemovePermutesAroundElementwiseOps):
152+
"""Remove permutes around elementwise regions, after first sinking each layout
153+
copy below a following squeeze so regions meet copies of their own rank.
154+
"""
155+
156+
def call(self, graph_module: GraphModule) -> PassResult:
157+
sunk = CortexMPostponeLayoutCopyBelowSqueezePass()(graph_module)
158+
result = super().call(sunk.graph_module)
159+
if sunk.modified and not result.modified:
160+
# The sinking retraced outside this pass's call_operator; retrace
161+
# through it so the folded int8 dtypes are restored.
162+
result = ExportPass.call(self, result.graph_module)
163+
return PassResult(result.graph_module, sunk.modified or result.modified)
164+
165+
120166
class ValidateCortexMExplicitLayoutPass(ExportPass):
121167
def call(self, graph_module: GraphModule) -> PassResult:
122168
for node in graph_module.graph.nodes:

‎backends/cortex_m/test/targets.bzl‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ def define_common_targets(is_fbcode = False):
8383
"//executorch/backends/cortex_m/passes:cortex_passes",
8484
"//executorch/backends/cortex_m/quantizer:quantizer",
8585
"//executorch/backends/test/harness:tester",
86+
"//executorch/backends/test:graph_builder",
87+
"//executorch/backends/transforms:channels_last_layout",
8688
"//executorch/backends/transforms:remove_unused_constants_pass",
8789
"//executorch/exir:lib",
8890
"//executorch/exir/dialects:lib",

‎backends/cortex_m/test/test_explicit_layout_pipeline.py‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
CortexMPassManager,
1515
LiftConstantTensorsPass,
1616
)
17+
from executorch.backends.cortex_m.passes.explicit_layout_pass import (
18+
CortexMPostponeLayoutCopyBelowSqueezePass,
19+
)
1720
from executorch.backends.cortex_m.quantizer.quantizer import CortexMQuantizer
1821
from executorch.backends.cortex_m.target_config import CortexM, CortexMTargetConfig
1922
from executorch.backends.cortex_m.test.tester import (
@@ -25,6 +28,8 @@
2528
from executorch.backends.transforms.remove_unused_constants_pass import (
2629
RemoveUnusedConstantsPass,
2730
)
31+
from executorch.backends.test.graph_builder import GraphBuilder
32+
from executorch.backends.transforms.channels_last_layout import LAYOUT_PERMUTE_COPY
2833
from executorch.exir import to_edge
2934
from executorch.exir.dialects._ops import ops as exir_ops
3035
from torch.export import ExportedProgram
@@ -89,6 +94,18 @@ def forward(self, x):
8994
return self.conv2(self.conv1(torch.tanh(x)).view(1, 4, 32))
9095

9196

97+
class Conv1dChain(torch.nn.Module):
98+
def __init__(self) -> None:
99+
super().__init__()
100+
self.conv1 = torch.nn.Conv1d(8, 8, 3, padding=1)
101+
self.conv2 = torch.nn.Conv1d(8, 8, 3, padding=1)
102+
self.conv3 = torch.nn.Conv1d(8, 8, 1)
103+
104+
def forward(self, x):
105+
y = torch.tanh(self.conv1(x))
106+
return self.conv3(torch.relu(self.conv2(y) + y))
107+
108+
92109
class Apply(torch.nn.Module):
93110
def __init__(self, fn) -> None:
94111
super().__init__()
@@ -650,3 +667,40 @@ def test_unquantized_constant_is_not_precomputed(use_explicit_layout):
650667

651668
assert _count(program, exir_ops.edge.aten.full.default) == 1
652669
tester.run_method_and_compare_outputs(inputs=inputs)
670+
671+
672+
def test_conv1d_chain_keeps_only_the_boundary_layout_copies():
673+
torch.manual_seed(0)
674+
inputs = (torch.randn(1, 8, 16),)
675+
tester = _run_explicit_layout_passes(CortexMTester(Conv1dChain().eval(), inputs))
676+
program = tester.get_artifact(StageType.RUN_PASSES).exported_program()
677+
678+
assert _count(program, exir_ops.edge.cortex_m.quantized_conv2d_nhwc.default) == 3
679+
assert _count(program, exir_ops.edge.cortex_m.transpose.default) == 2
680+
assert _count(program, exir_ops.edge.aten.permute_copy.default) == 0
681+
tester.run_method_and_compare_outputs(inputs=inputs, qtol=1)
682+
683+
684+
@pytest.mark.parametrize(
685+
"permute,shape,view_shape,sunk",
686+
[
687+
(LAYOUT_PERMUTE_COPY, (1, 1, 4, 6), [1, 6, 4], True),
688+
(LAYOUT_PERMUTE_COPY, (1, 5, 4, 6), [1, 1, 6, 5, 4], False),
689+
(exir_ops.edge.aten.permute_copy.default, (1, 1, 4, 6), [1, 6, 4], False),
690+
],
691+
ids=["layout_copy_squeeze", "layout_copy_unsqueeze", "user_permute_squeeze"],
692+
)
693+
def test_only_layout_copies_are_sunk_below_a_squeeze(
694+
permute, shape, view_shape, sunk
695+
):
696+
builder = GraphBuilder()
697+
x = builder.placeholder("x", torch.randn(shape))
698+
permuted = builder.call_operator(op=permute, args=(x, [0, 3, 1, 2]))
699+
view = builder.call_operator(
700+
op=exir_ops.edge.aten.view_copy.default, args=(permuted, view_shape)
701+
)
702+
builder.output([view])
703+
704+
result = CortexMPostponeLayoutCopyBelowSqueezePass()(builder.get_graph_module())
705+
706+
assert result.modified == sunk

0 commit comments

Comments
 (0)