Skip to content

Commit c65b23a

Browse files
authored
Lower Conv1d atomically through TOSA Conv2d (pytorch#22282)
Differential Revision: D116686805 Pull Request resolved: pytorch#22282
1 parent 02ee1ca commit c65b23a

11 files changed

Lines changed: 428 additions & 91 deletions

‎backends/arm/_passes/BUCK‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,6 @@ fbcode_target(
6767
"//executorch/backends/transforms:fuse_duplicate_users_pass",
6868
"//executorch/backends/transforms:propagate_view_copy_permute_pass",
6969
"//executorch/backends/transforms:fuse_identical_input_transforms_pass",
70-
"//executorch/backends/transforms:convert_conv1d_to_conv2d_pass",
7170
"//executorch/backends/transforms:fuse_view_copy",
7271
"//executorch/backends/transforms:remove_getitem_op",
7372
"//executorch/backends/transforms:replace_scalar_with_tensor",

‎backends/arm/_passes/__init__.py‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
from .cast_int_comparison_inputs_pass import CastIntComparisonInputsPass # noqa
2121
from .cast_to_int32_pass import CastToInt32Pass # noqa
2222
from .constant_folding_pass import ConstantFoldingPass # noqa
23-
from .conv1d_unsqueeze_pass import Conv1dUnsqueezePass # noqa
2423
from .convert_elu_params import ConvertELUParamsPass # noqa
2524
from .convert_expand_copy_to_repeat import ConvertExpandCopyToRepeatPass # noqa
2625
from .convert_full_like_to_full_pass import ConvertFullLikeToFullPass # noqa

‎backends/arm/_passes/arm_pass_manager.py‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
ComputeConstantOpsAOTPass,
2323
ConstantFoldingPass,
2424
ControlFlowConstInlinePass,
25-
Conv1dUnsqueezePass,
2625
ConvertEluFamilyToEluPass,
2726
ConvertELUParamsPass,
2827
ConvertExpandCopyToRepeatPass,
@@ -603,7 +602,6 @@ def _tosa_pipeline(
603602
DecomposeAdaptiveAvgPool2dPass(),
604603
DecomposeDynamicAdaptiveAvgPool2dPass(),
605604
DecomposeAvgPool2dPass(),
606-
Conv1dUnsqueezePass(exported_program),
607605
]
608606
)
609607

‎backends/arm/_passes/conv1d_unsqueeze_pass.py‎

Lines changed: 0 additions & 41 deletions
This file was deleted.

‎backends/arm/_passes/decompose_grouped_conv_pass.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88

99
import torch
1010
from executorch.backends.arm._passes.arm_pass import ArmOpTargetedPass
11-
from executorch.backends.arm._passes.conv1d_unsqueeze_pass import Conv1dUnsqueezePass
1211
from executorch.backends.arm._passes.quant_args import QuantArgs
12+
from executorch.backends.arm._passes.rewrite_conv_pass import RewriteConvPass
1313
from executorch.exir.dialects._ops import ops as exir_ops
1414
from executorch.exir.pass_base import ExportPass
1515

@@ -46,7 +46,7 @@ class DecomposeGroupedConvPass(ArmOpTargetedPass):
4646
4747
"""
4848

49-
_passes_required_after: Set[Type[ExportPass]] = {Conv1dUnsqueezePass}
49+
_passes_required_after: Set[Type[ExportPass]] = {RewriteConvPass}
5050
target_ops = (
5151
exir_ops.edge.aten.convolution.default,
5252
torch.ops.aten.conv_transpose2d.input,

‎backends/arm/_passes/rewrite_conv_pass.py‎

Lines changed: 102 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ def _adjust_pad_if_needed(
9494
pass instead.
9595
9696
"""
97-
mod_remainder = (
97+
mod_remainder: int | torch.SymInt = (
9898
input_len + 2 * pad - dilation * (input_weight - 1) - 1
9999
) % stride
100100

@@ -121,14 +121,14 @@ def _adjust_pad_if_needed(
121121

122122
return pad - mod_remainder
123123

124-
def _is_depthwise_conv2d(self, node: torch.fx.Node) -> bool:
124+
def _is_depthwise_conv(self, node: torch.fx.Node) -> bool:
125125
if (
126126
node.op != "call_function"
127127
or node.target != exir_ops.edge.aten.convolution.default
128128
):
129129
return False
130130
input_tensor = get_first_fake_tensor(node.all_input_nodes[0])
131-
if len(input_tensor.shape) != 4:
131+
if len(input_tensor.shape) not in (3, 4):
132132
return False
133133
groups = node.args[-1]
134134
in_channels = input_tensor.shape[1]
@@ -524,7 +524,7 @@ def _combine_rescale_scales(
524524
@staticmethod
525525
def _is_direct_int32_rescale(node: torch.fx.Node) -> bool:
526526
"""Return whether a node directly rescales its input to INT32."""
527-
return (
527+
return bool(
528528
node.op == "call_function"
529529
and node.target == exir_ops.backend.tosa.RESCALE.default
530530
and len(node.args) > 1
@@ -911,7 +911,70 @@ def call(self, graph_module: torch.fx.GraphModule) -> PassResult: # noqa: C901
911911
dilation = tuple(dilation_list)
912912
pad = pad_attr
913913

914-
if self._is_conv3d(len(input_shape), group):
914+
if spatial_rank == 1:
915+
target_op = (
916+
exir_ops.backend.tosa.DEPTHWISE_CONV2D.default
917+
if self._is_depthwise_conv(node)
918+
else exir_ops.backend.tosa.CONV2D.default
919+
)
920+
pre_permute_dims = (0, 2, 1)
921+
post_permute_dims = (0, 2, 1)
922+
with graph_module.graph.inserting_before(node):
923+
x = create_node(
924+
graph=graph_module.graph,
925+
op_target=exir_ops.edge.aten.permute_copy.default,
926+
args=(x, list(pre_permute_dims)),
927+
from_node=node,
928+
)
929+
permuted_input_fake = permute_fake_tensor_metadata(
930+
input_fake_tensor, pre_permute_dims
931+
)
932+
x.meta["val"] = permuted_input_fake
933+
input_tensor_for_tosa_fake = permuted_input_fake.unsqueeze(1)
934+
x = create_node(
935+
graph=graph_module.graph,
936+
op_target=exir_ops.edge.aten.view_copy.default,
937+
args=(x, list(input_tensor_for_tosa_fake.shape)),
938+
from_node=node,
939+
)
940+
x.meta["val"] = input_tensor_for_tosa_fake
941+
942+
kernel_width = weight_shape[2]
943+
if target_op == exir_ops.backend.tosa.DEPTHWISE_CONV2D.default:
944+
in_channels = input_fake_tensor.shape[1]
945+
channel_multiplier = weight_shape[0] // in_channels
946+
weight = self._rewrite_weight(
947+
graph_module,
948+
weight,
949+
node,
950+
permute_dims=(1, 2, 0),
951+
name_suffix="hwicm",
952+
reshape_dims=(
953+
1,
954+
kernel_width,
955+
in_channels,
956+
channel_multiplier,
957+
),
958+
)
959+
else:
960+
weight = self._rewrite_weight(
961+
graph_module,
962+
weight,
963+
node,
964+
permute_dims=(0, 2, 1),
965+
name_suffix="ohwi",
966+
reshape_dims=(
967+
weight_shape[0],
968+
1,
969+
kernel_width,
970+
weight_shape[1],
971+
),
972+
)
973+
weight_fake_tensor = get_first_fake_tensor(weight)
974+
stride = (1, stride[0])
975+
dilation = (1, dilation[0])
976+
pad = [0, 0, pad[0], pad[1]]
977+
elif self._is_conv3d(len(input_shape), group):
915978
target_op = exir_ops.backend.tosa.CONV3D.default
916979
pre_permute_dims = ODHWI_ORDER
917980
post_permute_dims = ODHWI_INVERSE_ORDER
@@ -934,7 +997,7 @@ def call(self, graph_module: torch.fx.GraphModule) -> PassResult: # noqa: C901
934997
name_suffix="odhwi",
935998
)
936999
weight_fake_tensor = get_first_fake_tensor(weight)
937-
elif self._is_depthwise_conv2d(node):
1000+
elif self._is_depthwise_conv(node):
9381001
target_op = exir_ops.backend.tosa.DEPTHWISE_CONV2D.default
9391002
pre_permute_dims = NHWC_ORDER
9401003
post_permute_dims = NHWC_INVERSE_ORDER
@@ -1039,7 +1102,28 @@ def call(self, graph_module: torch.fx.GraphModule) -> PassResult: # noqa: C901
10391102

10401103
if post_permute_dims is None:
10411104
raise RuntimeError("Expected post permute dims for explicit layout")
1105+
output_conversion_node = node_replacement
10421106
post_permute_input = node_replacement
1107+
squeeze_view: torch.fx.Node | None = None
1108+
if spatial_rank == 1:
1109+
squeezed_output_fake = cast(
1110+
FakeTensor, node_replacement_fake_tensor.squeeze(1)
1111+
)
1112+
special_dtype = node_replacement.meta.get(TosaSpecialDtype.meta_key())
1113+
with graph_module.graph.inserting_after(node_replacement):
1114+
node_replacement = create_node(
1115+
graph=graph_module.graph,
1116+
op_target=exir_ops.edge.aten.view_copy.default,
1117+
args=(node_replacement, list(squeezed_output_fake.shape)),
1118+
from_node=node,
1119+
)
1120+
node_replacement.meta["val"] = squeezed_output_fake
1121+
if special_dtype:
1122+
node_replacement.meta[TosaSpecialDtype.meta_key()] = special_dtype
1123+
squeeze_view = node_replacement
1124+
post_permute_input = node_replacement
1125+
node_replacement_fake_tensor = squeezed_output_fake
1126+
10431127
with graph_module.graph.inserting_after(node_replacement):
10441128
node_replacement = create_node(
10451129
graph=graph_module.graph,
@@ -1059,16 +1143,23 @@ def call(self, graph_module: torch.fx.GraphModule) -> PassResult: # noqa: C901
10591143
tosa_node_fake_tensor.dtype == torch.int32
10601144
and input_fake_tensor.dtype == torch.int16
10611145
)
1062-
if is_a16w8_conv:
1146+
if is_a16w8_conv and spatial_rank != 1:
10631147
# Keep values in INT32 whenever a consumer supports it, even
10641148
# though the declared output is INT16, by branching from the
10651149
# accumulator before narrowing.
1150+
#
1151+
# Rank-three convolutions are excluded. The legacy Conv1d
1152+
# expansion placed a rank-changing view between the convolution
1153+
# and its INT32 consumers, so the convolution narrowed to its
1154+
# exported output domain instead of forking. Forking here would
1155+
# give each branch its own boundary rescale and permute, which
1156+
# Vela materialises as a second full transpose of the output.
10661157
self._insert_a16w8_output_branches(
10671158
graph_module,
10681159
node,
10691160
tosa_op,
10701161
tosa_node_fake_tensor,
1071-
post_permute_input,
1162+
output_conversion_node,
10721163
post_permute_dims,
10731164
)
10741165
# Only users not moved to widened branches remain on the
@@ -1078,7 +1169,9 @@ def call(self, graph_module: torch.fx.GraphModule) -> PassResult: # noqa: C901
10781169
node.replace_all_uses_with(node_replacement)
10791170
else:
10801171
graph_module.graph.erase_node(node_replacement)
1081-
graph_module.graph.erase_node(post_permute_input)
1172+
if squeeze_view is not None:
1173+
graph_module.graph.erase_node(squeeze_view)
1174+
graph_module.graph.erase_node(output_conversion_node)
10821175
else:
10831176
node.replace_all_uses_with(node_replacement)
10841177

‎backends/arm/test/passes/test_canonicalize_view_copy_permute_pass.py‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,38 @@ def test_canonicalize_moves_permute_before_view() -> None:
298298
_validate_numerics(gm_before, result.graph_module, (x_data,))
299299

300300

301+
def test_canonicalize_sinks_singleton_view_below_permute() -> None:
302+
builder = GraphBuilder()
303+
x_data = torch.randn(2, 8, 64)
304+
x = builder.placeholder("x", x_data)
305+
v1 = builder.call_operator(
306+
op=exir_ops.edge.aten.view_copy.default,
307+
args=(x, [2, 8, 1, 64]),
308+
)
309+
p1 = builder.call_operator(
310+
op=exir_ops.edge.aten.permute_copy.default,
311+
args=(v1, [0, 2, 3, 1]),
312+
)
313+
builder.output([p1])
314+
original = builder.get_graph_module()
315+
gm_before = copy.deepcopy(original)
316+
317+
pass_instance = CanonicalizeViewCopyPermutePass()
318+
result = cast(PassResult, pass_instance.call(original))
319+
320+
assert result.modified
321+
compute_nodes = [
322+
node for node in result.graph_module.graph.nodes if node.op == "call_function"
323+
]
324+
assert [node.target for node in compute_nodes] == [
325+
exir_ops.edge.aten.permute_copy.default,
326+
exir_ops.edge.aten.view_copy.default,
327+
]
328+
assert compute_nodes[0].args[1] == [0, 2, 1]
329+
assert compute_nodes[1].args[1] == [2, 1, 64, 8]
330+
_validate_numerics(gm_before, result.graph_module, (x_data,))
331+
332+
301333
def test_canonicalize_follows_interleaved_chain_users() -> None:
302334
builder = GraphBuilder()
303335
x_data = torch.randn(4, 2, 4)

‎backends/arm/test/passes/test_remove_permutes_around_elementwise_tosa_ops.py‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
RESCALE_TARGET = exir_ops.backend.tosa.RESCALE.default
2727
MUL_TARGET = exir_ops.edge.aten.mul.Tensor
2828
ADD_TARGET = exir_ops.edge.aten.add.Tensor
29+
SUB_TARGET = exir_ops.edge.aten.sub.Tensor
30+
VIEW_TARGET = exir_ops.edge.aten.view_copy.default
2931
ERF_TARGET = exir_ops.edge.aten.erf.default
3032

3133

@@ -150,6 +152,40 @@ def test_remove_permutes_around_rescale_tosa_INT() -> None:
150152
assert _count_nodes(result.graph_module, RESCALE_TARGET) == 1
151153

152154

155+
def test_sink_view_preserves_layout_through_rescale_to_broadcast_tosa_INT() -> None:
156+
graph = torch.fx.Graph()
157+
x = graph.placeholder("x")
158+
x.meta["val"] = torch.randn(1, 4, 1, 1)
159+
direct = graph.placeholder("direct")
160+
direct.meta["val"] = torch.randn(1, 8, 4)
161+
162+
permute = graph.create_node("call_function", PERMUTE_TARGET, args=(x, [0, 2, 3, 1]))
163+
permute.meta["val"] = torch.randn(1, 1, 1, 4)
164+
mul = graph.create_node("call_function", MUL_TARGET, args=(permute, permute))
165+
mul.meta["val"] = torch.randn(1, 1, 1, 4)
166+
sink = graph.create_node("call_function", VIEW_TARGET, args=(mul, [1, 1, 4]))
167+
sink.meta["val"] = torch.randn(1, 1, 4)
168+
rescale = graph.create_node(
169+
"call_function",
170+
RESCALE_TARGET,
171+
args=(sink, torch.int8, [1.0], 0, 0),
172+
)
173+
rescale.meta["val"] = torch.randn(1, 1, 4)
174+
sub = graph.create_node("call_function", SUB_TARGET, args=(direct, rescale))
175+
sub.meta["val"] = torch.randn(1, 8, 4)
176+
graph.output(sub)
177+
178+
graph_module = torch.fx.GraphModule({}, graph)
179+
with TosaLoweringContext(TOSA_INT_SPEC):
180+
result = RemovePermutesAroundElementwiseTosaOps(_fake_exported_program()).call(
181+
graph_module
182+
)
183+
184+
assert not result.modified
185+
assert _count_nodes(result.graph_module, PERMUTE_TARGET) == 1
186+
assert sub.args == (direct, rescale)
187+
188+
153189
def test_remove_permutes_around_gelu_with_folded_scalar_constants_tosa_FP() -> None:
154190
graph = torch.fx.Graph()
155191
x = graph.placeholder("x")

0 commit comments

Comments
 (0)