Skip to content

Commit c56e6bf

Browse files
authored
Arm backend: Fix pad transpose handling around grid sampler (pytorch#21738)
Arm backend: Fix pad transpose handling around grid sampler Change-Id: I5e9282082c670a086262c13f68bdecbd44ec5a05 Signed-off-by: Rob Elliott <Robert.Elliott@arm.com> cc @digantdesai @freddan80 @per @zingo @oscarandersson8218 @mansnils @Sebastian-Larsson @rascani Signed-off-by: Rob Elliott <Robert.Elliott@arm.com>
1 parent 9958d39 commit c56e6bf

5 files changed

Lines changed: 176 additions & 17 deletions

File tree

‎backends/arm/_passes/arm_pass_manager.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -649,10 +649,10 @@ def _tosa_pipeline(
649649
RewriteMXFPConv2dPass(exported_program),
650650
RewriteMXFPLinearPass(exported_program),
651651
RewriteMatmulPass(),
652-
RewritePadPass(),
653652
FuseViewCopyTransformPass(),
654653
PropagateViewCopyPermuteDownPass(self.compile_spec, exported_program),
655654
PropagateViewCopyPermuteUpPass(self.compile_spec, exported_program),
655+
RewritePadPass(),
656656
# Propagation can leave a binary op with mismatched operand ranks,
657657
# which TOSA rejects; re-match ranks before lowering.
658658
MoveDataMovementOpsToSmallerDtypePass(),

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

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -420,7 +420,10 @@ def test_rewrite_grid_sampler_to_tosa_custom_c3_pad_for_align_corners():
420420
)
421421
assert payload["output_0_type"] == "Image"
422422
assert payload["output_0_vkformat"] == GRID_SAMPLER_2D_SAMPLER_VK_FORMAT
423-
assert any(node.target == exir_ops.edge.aten.cat.default for node in nodes)
423+
assert any(
424+
node.target == exir_ops.edge.aten.constant_pad_nd.default for node in nodes
425+
)
426+
assert not any(node.target == exir_ops.edge.aten.cat.default for node in nodes)
424427
assert any(node.target == exir_ops.edge.aten.slice_copy.Tensor for node in nodes)
425428

426429

@@ -603,11 +606,17 @@ def test_rewrite_grid_sampler_to_tosa_custom_keeps_clean_graph_for_c3_sampler_pa
603606
== 2
604607
)
605608
assert (
606-
_count_call_function_target(graph_module, exir_ops.edge.aten.cat.default) == 1
609+
_count_call_function_target(
610+
graph_module, exir_ops.edge.aten.constant_pad_nd.default
611+
)
612+
== 1
613+
)
614+
assert (
615+
_count_call_function_target(graph_module, exir_ops.edge.aten.cat.default) == 0
607616
)
608617
assert (
609618
_count_call_function_target(graph_module, exir_ops.edge.aten.slice_copy.Tensor)
610-
>= 2
619+
== 1
611620
)
612621

613622

‎backends/arm/vgf/_passes/rewrite_grid_sampler_to_tosa_custom.py‎

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -278,29 +278,45 @@ def _create_grid_qparam_placeholders(
278278
def _pad_c3_input_to_c4(
279279
graph_module: torch.fx.GraphModule,
280280
input_tensor: torch.fx.Node,
281+
input_qparams: QuantArgs | None = None,
281282
) -> torch.fx.Node:
282283
input_val = input_tensor.meta["val"]
283-
first_channel = create_node(
284-
graph_module.graph,
285-
op_target=exir_ops.edge.aten.slice_copy.Tensor,
286-
args=(input_tensor, 1, 0, 1, 1),
287-
from_node=input_tensor,
288-
)
289-
first_channel_val = exir_ops.edge.aten.slice_copy.Tensor(input_val, 1, 0, 1, 1)
290-
_set_fake_tensor_meta(first_channel, first_channel_val)
291-
292284
padded_input = create_node(
293285
graph_module.graph,
294-
op_target=exir_ops.edge.aten.cat.default,
295-
args=([input_tensor, first_channel], 1),
286+
op_target=exir_ops.edge.aten.constant_pad_nd.default,
287+
args=(input_tensor, [0, 0, 0, 0, 0, 1], 0),
296288
from_node=input_tensor,
289+
inherit_qparams=True,
297290
)
291+
pad_qparams = input_qparams
292+
if pad_qparams is None:
293+
try:
294+
pad_qparams = next(iter(get_output_qparams(input_tensor).values()))
295+
except ValueError:
296+
try:
297+
pad_qparams = next(iter(get_input_qparams(input_tensor).values()))
298+
except ValueError:
299+
pad_qparams = None
300+
if pad_qparams is not None:
301+
padded_input.meta["input_qparams"] = {0: pad_qparams}
302+
padded_input.meta["output_qparams"] = {0: pad_qparams}
298303
_set_fake_tensor_meta(
299304
padded_input,
300-
exir_ops.edge.aten.cat.default([input_val, first_channel_val], 1),
305+
exir_ops.edge.aten.constant_pad_nd.default(
306+
input_val, [0, 0, 0, 0, 0, 1], 0
307+
),
301308
)
302309
return padded_input
303310

311+
@staticmethod
312+
def _get_optional_input_qparams(
313+
node: torch.fx.Node, input_index: int
314+
) -> QuantArgs | None:
315+
try:
316+
return get_input_qparams(node).get(input_index)
317+
except ValueError:
318+
return None
319+
304320
@staticmethod
305321
def _slice_c4_output_to_c3(
306322
graph_module: torch.fx.GraphModule,
@@ -372,7 +388,11 @@ def call(self, graph_module):
372388

373389
with graph_module.graph.inserting_before(node):
374390
custom_input = (
375-
self._pad_c3_input_to_c4(graph_module, input_tensor)
391+
self._pad_c3_input_to_c4(
392+
graph_module,
393+
input_tensor,
394+
input_qparams=self._get_optional_input_qparams(node, 0),
395+
)
376396
if pad_c3_for_sampler
377397
else input_tensor
378398
)

‎backends/transforms/remove_permutes_around_elementwise_ops.py‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,9 @@ def __init__(self, extra_permutable_ops: set | None = None) -> None:
5454
exir_ops.edge.aten.hardtanh.default,
5555
exir_ops.edge.aten.clamp.default,
5656
exir_ops.edge.aten.cat.default,
57+
exir_ops.edge.aten.constant_pad_nd.default,
5758
exir_ops.edge.aten.mean.dim,
59+
exir_ops.edge.aten.pad.default,
5860
exir_ops.edge.aten.sum.dim_IntList,
5961
exir_ops.edge.aten.slice_copy.Tensor,
6062
}
@@ -80,6 +82,11 @@ def __init__(self, extra_permutable_ops: set | None = None) -> None:
8082

8183
_SQUEEZE_OPS = (exir_ops.edge.aten.squeeze_copy.dim,)
8284

85+
_PAD_OPS = (
86+
exir_ops.edge.aten.constant_pad_nd.default,
87+
exir_ops.edge.aten.pad.default,
88+
)
89+
8390
@staticmethod
8491
def _find_extra_one(longer: list[int], shorter: list[int]) -> int:
8592
"""If longer has exactly one more element of value 1, return its index. Else -1."""
@@ -450,6 +457,8 @@ def _is_pointwise(target) -> bool:
450457
return False
451458

452459
def is_node_permutable(self, node: torch.fx.Node) -> bool:
460+
if node.target in self._PAD_OPS and not self._is_constant_pad(node):
461+
return False
453462
if node.target in self._permutable_ops:
454463
if node.target in (
455464
exir_ops.edge.aten.mean.dim,
@@ -462,6 +471,24 @@ def is_node_permutable(self, node: torch.fx.Node) -> bool:
462471
return True
463472
return self._is_pointwise(node.target)
464473

474+
def _is_constant_pad(self, node: torch.fx.Node) -> bool:
475+
if len(node.args) < 2:
476+
return False
477+
478+
pad = node.args[1]
479+
if not isinstance(pad, (list, tuple)):
480+
return False
481+
482+
if len(pad) % 2 != 0:
483+
return False
484+
485+
if node.target == exir_ops.edge.aten.pad.default:
486+
mode = node.args[2] if len(node.args) > 2 else node.kwargs.get("mode")
487+
if mode not in (None, "constant"):
488+
return False
489+
490+
return True
491+
465492
def permute_subgraph(self, subgraph: Subgraph) -> bool: # noqa: C901
466493
# Ensure that the subgraph's edges have not been modified by an earlier rewrite before applying changes.
467494
if not self._subgraph_edges_are_current(subgraph):
@@ -495,6 +522,8 @@ def permute_subgraph(self, subgraph: Subgraph) -> bool: # noqa: C901
495522
self.update_mean_dim(node, node_start_perm)
496523
elif node.target == exir_ops.edge.aten.slice_copy.Tensor:
497524
self.update_slice_copy(node, node_start_perm)
525+
elif node.target in self._PAD_OPS:
526+
self.update_pad(node, node_start_perm)
498527
elif node.target in self._VIEW_OPS:
499528
self.update_view_copy(node, node_start_perm)
500529
elif node.target in self._UNSQUEEZE_OPS:
@@ -596,6 +625,24 @@ def update_slice_copy(self, node: torch.fx.Node, start_permute: list[int]) -> No
596625
dim = get_arg(node, "dim", int)
597626
set_arg(node, "dim", start_permute[dim])
598627

628+
def update_pad(self, node: torch.fx.Node, start_permute: list[int]) -> None:
629+
pad = list(cast(list[int], node.args[1]))
630+
rank = len(start_permute)
631+
pad_pairs = [[0, 0] for _ in range(rank)]
632+
for pair_idx, pair_start in enumerate(range(0, len(pad), 2)):
633+
dim = rank - 1 - pair_idx
634+
pad_pairs[dim] = [pad[pair_start], pad[pair_start + 1]]
635+
636+
remapped_pairs = [pad_pairs[start_permute.index(dim)] for dim in range(rank)]
637+
remapped_pad = []
638+
for pair in reversed(remapped_pairs):
639+
remapped_pad.extend(pair)
640+
641+
while len(remapped_pad) > 2 and remapped_pad[-2:] == [0, 0]:
642+
remapped_pad = remapped_pad[:-2]
643+
644+
node.update_arg(1, remapped_pad)
645+
599646
def update_view_copy(self, node: torch.fx.Node, start_permute: list[int]) -> None:
600647
"""Adjust view_copy shape arg after permute removal.
601648

‎backends/transforms/test/test_permute_optimization_passes.py‎

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -810,6 +810,89 @@ def test_permute_view_add_sub_mul_view_permute(self) -> None:
810810
"permute_view_add_sub_mul_view_permute",
811811
)
812812

813+
def test_permute_constant_pad_nd_permute(self) -> None:
814+
builder = GraphBuilder()
815+
x_data = torch.randn(1, 64, 64, 3)
816+
x = builder.placeholder("x", x_data)
817+
p1 = builder.call_operator(
818+
op=exir_ops.edge.aten.permute_copy.default, args=(x, [0, 3, 1, 2])
819+
)
820+
pad = builder.call_operator(
821+
op=exir_ops.edge.aten.constant_pad_nd.default,
822+
args=(p1, [0, 0, 0, 0, 0, 1], 0.0),
823+
)
824+
p2 = builder.call_operator(
825+
op=exir_ops.edge.aten.permute_copy.default, args=(pad, [0, 2, 3, 1])
826+
)
827+
builder.output([p2])
828+
original = builder.get_graph_module()
829+
gm_before = copy.deepcopy(original)
830+
831+
p = RemovePermutesAroundElementwiseOps()
832+
result = cast(PassResult, p(original))
833+
self.assertTrue(result.modified)
834+
self.assertEqual(
835+
count_node(result.graph_module, exir_ops.edge.aten.permute_copy.default), 0
836+
)
837+
self.assertEqual(
838+
count_node(result.graph_module, exir_ops.edge.aten.constant_pad_nd.default),
839+
1,
840+
)
841+
842+
pad_nodes = [
843+
node
844+
for node in result.graph_module.graph.nodes
845+
if node.target == exir_ops.edge.aten.constant_pad_nd.default
846+
]
847+
self.assertEqual(pad_nodes[0].args[1], [0, 1])
848+
validate_numerics(
849+
gm_before,
850+
result.graph_module,
851+
[x_data],
852+
"permute_constant_pad_nd_permute",
853+
)
854+
855+
def test_permute_aten_pad_permute(self) -> None:
856+
builder = GraphBuilder()
857+
x_data = torch.randn(1, 64, 64, 3)
858+
x = builder.placeholder("x", x_data)
859+
p1 = builder.call_operator(
860+
op=exir_ops.edge.aten.permute_copy.default, args=(x, [0, 3, 1, 2])
861+
)
862+
pad = builder.call_operator(
863+
op=exir_ops.edge.aten.pad.default,
864+
args=(p1, [0, 0, 0, 0, 0, 1], "constant", 0.0),
865+
)
866+
p2 = builder.call_operator(
867+
op=exir_ops.edge.aten.permute_copy.default, args=(pad, [0, 2, 3, 1])
868+
)
869+
builder.output([p2])
870+
original = builder.get_graph_module()
871+
gm_before = copy.deepcopy(original)
872+
873+
p = RemovePermutesAroundElementwiseOps()
874+
result = cast(PassResult, p(original))
875+
self.assertTrue(result.modified)
876+
self.assertEqual(
877+
count_node(result.graph_module, exir_ops.edge.aten.permute_copy.default), 0
878+
)
879+
self.assertEqual(
880+
count_node(result.graph_module, exir_ops.edge.aten.pad.default), 1
881+
)
882+
883+
pad_nodes = [
884+
node
885+
for node in result.graph_module.graph.nodes
886+
if node.target == exir_ops.edge.aten.pad.default
887+
]
888+
self.assertEqual(pad_nodes[0].args[1], [0, 1])
889+
validate_numerics(
890+
gm_before,
891+
result.graph_module,
892+
[x_data],
893+
"permute_aten_pad_permute",
894+
)
895+
813896
def test_permute_squeeze_clamp_add_permute(self) -> None:
814897
"""4D permute → squeeze(view) → hardtanh → add(with self) → 3D permute.
815898
Tests clamp + add interacting across a squeeze boundary."""

0 commit comments

Comments
 (0)