Skip to content

Commit 75a868a

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 `CortexMPostponeLayoutCopyBelowSqueezePass`, a restricted `PostponePermuteOpBelowSqueezeOrUnsqueezeLikeView`, and run it just before `RemovePermutesAroundElementwiseOps` in the explicit-layout pass list. It only moves the layout copies this pipeline inserts (its `targets` is `LAYOUT_PERMUTE_COPY`), 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. As its own pass-list entry, the pass gets the pass manager's dtype-preserving retrace, so the folded int8 dtypes survive the sinking. Authored with an AI coding assistant (Claude). Differential Revision: D128001279
1 parent 904f381 commit 75a868a

5 files changed

Lines changed: 113 additions & 0 deletions

File tree

‎backends/cortex_m/passes/BUCK‎

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

‎backends/cortex_m/passes/cortex_m_pass_manager.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
from .decompose_sdpa_pass import DecomposeSDPAPass
5656
from .explicit_layout_pass import (
5757
CortexMCanonicalizeViewCopyPermutePass,
58+
CortexMPostponeLayoutCopyBelowSqueezePass,
5859
CortexMReplaceOpsWithChannelsLastVariants,
5960
ValidateCortexMExplicitLayoutPass,
6061
)
@@ -175,6 +176,8 @@ class CortexMPassManager(ExportedProgramPassManager):
175176
ConvertConv1dToConv2dPass,
176177
CortexMReplaceOpsWithChannelsLastVariants,
177178
ReplaceSqueezeAndUnsqueezeWithViewPass,
179+
# Let elementwise regions meet layout copies of their own rank.
180+
CortexMPostponeLayoutCopyBelowSqueezePass,
178181
# Move layout copies across pads before singleton permutations become views.
179182
RemovePermutesAroundElementwiseOps,
180183
CortexMCanonicalizeViewCopyPermutePass,

‎backends/cortex_m/passes/explicit_layout_pass.py‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,16 @@
1919
LAYOUT_PERMUTE_COPY,
2020
PERMUTE_COPY_TARGETS,
2121
)
22+
from executorch.backends.transforms.postpone_permute_below_squeeze_view import (
23+
PostponePermuteOpBelowSqueezeOrUnsqueezeLikeView,
24+
)
2225
from executorch.backends.transforms.replace_ops_with_channels_last_variants import (
2326
ChannelsLastOpSpec,
2427
ReplaceOpsWithChannelsLastVariants,
2528
)
2629
from executorch.exir import ExportedProgram
2730
from executorch.exir.dialects._ops import ops as exir_ops
31+
from executorch.exir.dialects.edge._ops import EdgeOpOverload
2832
from executorch.exir.pass_base import ExportPass, PassResult
2933
from torch.fx import GraphModule, Node
3034
from torch.fx.node import Target
@@ -117,6 +121,33 @@ def _set_node_op(self, node, target, input_node, arg) -> None:
117121
)
118122

119123

124+
class CortexMPostponeLayoutCopyBelowSqueezePass(
125+
PostponePermuteOpBelowSqueezeOrUnsqueezeLikeView
126+
):
127+
"""Sink a layout copy below a following squeeze, never below an unsqueeze.
128+
129+
Only the layout copies this pipeline inserts are moved; user permutes keep
130+
their place. Sinking below an unsqueeze would raise the copy's rank, up to
131+
a rank-5 layout copy that Cortex-M cannot run.
132+
"""
133+
134+
@property
135+
def targets(self) -> list[EdgeOpOverload]:
136+
return [LAYOUT_PERMUTE_COPY]
137+
138+
def maybe_remove_or_replace(self, node: Node) -> bool:
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+
120151
class ValidateCortexMExplicitLayoutPass(ExportPass):
121152
def call(self, graph_module: GraphModule) -> PassResult:
122153
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: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,19 @@
1313
CortexMPassManager,
1414
LiftConstantTensorsPass,
1515
)
16+
from executorch.backends.cortex_m.passes.explicit_layout_pass import (
17+
CortexMPostponeLayoutCopyBelowSqueezePass,
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 (
1922
CortexMQuantize,
2023
CortexMRunPasses,
2124
CortexMTester,
2225
)
26+
from executorch.backends.test.graph_builder import GraphBuilder
2327
from executorch.backends.test.harness.stages import Quantize, StageType
28+
from executorch.backends.transforms.channels_last_layout import LAYOUT_PERMUTE_COPY
2429
from executorch.backends.transforms.remove_unused_constants_pass import (
2530
RemoveUnusedConstantsPass,
2631
)
@@ -88,6 +93,18 @@ def forward(self, x):
8893
return self.conv2(self.conv1(torch.tanh(x)).view(1, 4, 32))
8994

9095

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

670687
assert _count(program, exir_ops.edge.aten.full.default) == 1
671688
tester.run_method_and_compare_outputs(inputs=inputs)
689+
690+
691+
def test_conv1d_chain_keeps_only_the_boundary_layout_copies():
692+
torch.manual_seed(0)
693+
inputs = (torch.randn(1, 8, 16),)
694+
tester = _run_explicit_layout_passes(CortexMTester(Conv1dChain().eval(), inputs))
695+
program = tester.get_artifact(StageType.RUN_PASSES).exported_program()
696+
697+
assert _count(program, exir_ops.edge.cortex_m.quantized_conv2d_nhwc.default) == 3
698+
assert _count(program, exir_ops.edge.cortex_m.transpose.default) == 2
699+
assert _count(program, exir_ops.edge.aten.permute_copy.default) == 0
700+
tester.run_method_and_compare_outputs(inputs=inputs, qtol=1)
701+
702+
703+
def _permute_then_view(permute, shape, view_shape) -> torch.fx.GraphModule:
704+
builder = GraphBuilder()
705+
x = builder.placeholder("x", torch.randn(shape))
706+
permuted = builder.call_operator(op=permute, args=(x, [0, 3, 1, 2]))
707+
view = builder.call_operator(
708+
op=exir_ops.edge.aten.view_copy.default, args=(permuted, view_shape)
709+
)
710+
builder.output([view])
711+
return builder.get_graph_module()
712+
713+
714+
def test_layout_copy_is_sunk_below_a_squeeze():
715+
x = torch.randn(1, 1, 4, 6)
716+
graph_module = _permute_then_view(LAYOUT_PERMUTE_COPY, x.shape, [1, 6, 4])
717+
718+
result = CortexMPostponeLayoutCopyBelowSqueezePass()(graph_module)
719+
720+
assert result.modified
721+
assert [
722+
(node.target, node.args[1])
723+
for node in result.graph_module.graph.nodes
724+
if node.op == "call_function"
725+
] == [
726+
(exir_ops.edge.aten.view_copy.default, [1, 4, 6]),
727+
(LAYOUT_PERMUTE_COPY, [0, 2, 1]),
728+
]
729+
torch.testing.assert_close(
730+
result.graph_module(x)[0], x.permute(0, 3, 1, 2).reshape(1, 6, 4)
731+
)
732+
733+
734+
@pytest.mark.parametrize(
735+
"permute,shape,view_shape",
736+
[
737+
(LAYOUT_PERMUTE_COPY, (1, 5, 4, 6), [1, 1, 6, 5, 4]),
738+
(exir_ops.edge.aten.permute_copy.default, (1, 1, 4, 6), [1, 6, 4]),
739+
],
740+
ids=["layout_copy_unsqueeze", "user_permute_squeeze"],
741+
)
742+
def test_other_permutes_are_not_sunk(permute, shape, view_shape):
743+
graph_module = _permute_then_view(permute, shape, view_shape)
744+
745+
result = CortexMPostponeLayoutCopyBelowSqueezePass()(graph_module)
746+
747+
assert not result.modified

0 commit comments

Comments
 (0)