Skip to content

Commit 843f77e

Browse files
committed
Arm backend: Delegate reflection padding on U55
Enable legal static ReflectionPad1d, ReflectionPad2d, and ReflectionPad3d cases proven through Vela and Corstone-300. Reflection padding mirrors edge values when extending a tensor: Original: [A B C D] Pad 2 each side: [C B | A B C D | C B] Cover ranks, padding shapes, batching, and quantization modes, and leave dynamic shapes undelegated when padding validity cannot be proven at compile time. Authored with Codex. Change-Id: Ib0b833c2c866b13e504920671303ee22ceac01b6 Signed-off-by: Per Held <per.held@arm.com>
1 parent c8d5189 commit 843f77e

5 files changed

Lines changed: 275 additions & 3 deletions

File tree

‎backends/arm/operator_support/ethos_u55_support.py‎

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -208,9 +208,6 @@ class EthosU55NotSupported(OperatorSupportBase):
208208
exir_ops.edge.aten.scatter_reduce.two,
209209
exir_ops.edge.aten.scatter_add.default,
210210
exir_ops.edge.aten.upsample_bilinear2d.vec, # RESIZE
211-
exir_ops.edge.aten.reflection_pad1d.default, # REVERSE
212-
exir_ops.edge.aten.reflection_pad2d.default, # REVERSE
213-
exir_ops.edge.aten.reflection_pad3d.default, # REVERSE
214211
exir_ops.edge.aten.where.self, # SELECT
215212
]
216213

@@ -355,6 +352,35 @@ def is_node_supported(
355352
)
356353
return False
357354

355+
reflection_pad_constraints = {
356+
exir_ops.edge.aten.reflection_pad1d.default: ((2, 3), (2,)),
357+
exir_ops.edge.aten.reflection_pad2d.default: ((3, 4), (2, 4)),
358+
exir_ops.edge.aten.reflection_pad3d.default: ((4, 5), (6,)),
359+
}
360+
if node.target in reflection_pad_constraints:
361+
input_shape = get_first_fake_tensor(node.all_input_nodes[0]).shape
362+
padding = typing.cast(typing.Sequence[int], node.args[1])
363+
supported_ranks, supported_padding_lengths = reflection_pad_constraints[
364+
node.target
365+
]
366+
if (
367+
len(input_shape) in supported_ranks
368+
and len(padding) in supported_padding_lengths
369+
):
370+
spatial_sizes = tuple(reversed(input_shape[-(len(padding) // 2) :]))
371+
pad_pairs = tuple(zip(padding[::2], padding[1::2]))
372+
if all(
373+
isinstance(size, int) and 0 <= before < size and 0 <= after < size
374+
for (before, after), size in zip(pad_pairs, spatial_sizes)
375+
):
376+
return True
377+
self.reporter.report_reject(
378+
node,
379+
"U55 reflection padding requires a supported static input rank "
380+
"and nonnegative padding smaller than its spatial dimension.",
381+
)
382+
return False
383+
358384
return True
359385

360386

‎backends/arm/operator_support/tosa_supported_operators.py‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -762,6 +762,15 @@ def is_node_supported(
762762
input_node = node.all_input_nodes[0]
763763
input_quantized = FuseQuantizedActivationPass._is_fuseable_input(input_node)
764764

765+
if any(
766+
isinstance(input_node.meta["val"], torch.SymInt)
767+
for input_node in node.all_input_nodes
768+
):
769+
self.reporter.report_reject(
770+
node, "Symbolic scalar inputs cannot be delegated."
771+
)
772+
return False
773+
765774
input_quantized = input_quantized or all(
766775
(input_node.target in DQ_OPS)
767776
or _is_integer_dtype(get_first_fake_tensor(input_node).dtype)
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# Copyright 2026 Arm Limited and/or its affiliates.
2+
#
3+
# This source code is licensed under the BSD-style license found in the
4+
# LICENSE file in the root directory of this source tree.
5+
6+
from typing import Tuple
7+
8+
import torch
9+
10+
from executorch.backends.arm.test import common
11+
from executorch.backends.arm.test.tester.arm_tester import ArmTester
12+
from executorch.backends.arm.test.tester.test_pipeline import EthosU55PipelineINT
13+
14+
15+
input_t1 = Tuple[torch.Tensor]
16+
17+
test_data_suite_u55 = {
18+
"rank2_symmetric": lambda: (torch.rand(2, 5), (1, 1)),
19+
"rank3_symmetric": lambda: (torch.rand(1, 2, 5), (1, 1)),
20+
"asymmetric": lambda: (torch.rand(1, 2, 5), (1, 3)),
21+
"maximum_legal": lambda: (torch.rand(1, 2, 5), (4, 4)),
22+
"batched": lambda: (torch.rand(2, 2, 5), (1, 1)),
23+
}
24+
25+
26+
class ReflectionPad1d(torch.nn.Module):
27+
def __init__(self, padding):
28+
super().__init__()
29+
self.padding = padding
30+
31+
def forward(self, x):
32+
return torch.nn.functional.pad(x, self.padding, mode="reflect")
33+
34+
35+
@common.parametrize("test_data", test_data_suite_u55)
36+
@common.XfailIfNoCorstone300
37+
def test_reflection_pad1d_u55_INT(test_data):
38+
data, padding = test_data()
39+
pipeline = EthosU55PipelineINT[input_t1](
40+
ReflectionPad1d(padding),
41+
(data,),
42+
aten_ops=[],
43+
exir_ops=[],
44+
)
45+
pipeline.run()
46+
47+
48+
@common.XfailIfNoCorstone300
49+
def test_reflection_pad1d_u55_INT_a16w8():
50+
pipeline = EthosU55PipelineINT[input_t1](
51+
ReflectionPad1d((1, 1)),
52+
(torch.rand(1, 2, 5),),
53+
aten_ops=[],
54+
exir_ops=[],
55+
a16w8_quantization=True,
56+
)
57+
pipeline.run()
58+
59+
60+
def test_reflection_pad1d_u55_INT_symbolic_width_not_delegated():
61+
width = torch.export.Dim("width", min=3, max=8)
62+
tester = ArmTester(
63+
ReflectionPad1d((1, 1)),
64+
(torch.rand(1, 2, 5),),
65+
common.get_u55_compile_spec(),
66+
dynamic_shapes={"x": {2: width}},
67+
)
68+
tester.quantize().export().to_edge().partition()
69+
70+
targets = {
71+
node.target
72+
for node in tester.stages[tester.cur].artifact.exported_program().graph.nodes
73+
}
74+
assert torch.ops.aten.scalar_tensor.default in targets
75+
assert torch.ops.higher_order.executorch_call_delegate not in targets
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# Copyright 2026 Arm Limited and/or its affiliates.
2+
#
3+
# This source code is licensed under the BSD-style license found in the
4+
# LICENSE file in the root directory of this source tree.
5+
6+
from typing import Tuple
7+
8+
import torch
9+
10+
from executorch.backends.arm.test import common
11+
from executorch.backends.arm.test.tester.arm_tester import ArmTester
12+
from executorch.backends.arm.test.tester.test_pipeline import EthosU55PipelineINT
13+
14+
15+
input_t1 = Tuple[torch.Tensor]
16+
17+
test_data_suite_u55 = {
18+
"both_axes": lambda: (torch.rand(1, 2, 4, 3), (1, 1, 1, 1)),
19+
"width_only": lambda: (torch.rand(1, 2, 4, 3), (1, 1, 0, 0)),
20+
"height_only": lambda: (torch.rand(1, 2, 4, 3), (0, 0, 1, 1)),
21+
"asymmetric": lambda: (torch.rand(1, 2, 5, 4), (1, 2, 3, 1)),
22+
"maximum_legal": lambda: (torch.rand(1, 2, 4, 3), (2, 2, 3, 3)),
23+
"batched": lambda: (torch.rand(2, 2, 4, 3), (1, 1, 1, 1)),
24+
}
25+
26+
27+
class ReflectionPad2d(torch.nn.Module):
28+
def __init__(self, padding):
29+
super().__init__()
30+
self.padding = padding
31+
32+
def forward(self, x):
33+
return torch.nn.functional.pad(x, self.padding, mode="reflect")
34+
35+
36+
@common.parametrize("test_data", test_data_suite_u55)
37+
@common.XfailIfNoCorstone300
38+
def test_reflection_pad2d_u55_INT(test_data):
39+
data, padding = test_data()
40+
pipeline = EthosU55PipelineINT[input_t1](
41+
ReflectionPad2d(padding),
42+
(data,),
43+
aten_ops=[],
44+
exir_ops=[],
45+
)
46+
pipeline.run()
47+
48+
49+
@common.XfailIfNoCorstone300
50+
def test_reflection_pad2d_u55_INT_a16w8():
51+
pipeline = EthosU55PipelineINT[input_t1](
52+
ReflectionPad2d((1, 1, 1, 1)),
53+
(torch.rand(1, 2, 4, 3),),
54+
aten_ops=[],
55+
exir_ops=[],
56+
a16w8_quantization=True,
57+
)
58+
pipeline.run()
59+
60+
61+
@common.XfailIfNoCorstone300
62+
def test_reflection_pad2d_u55_INT_rank3():
63+
pipeline = EthosU55PipelineINT[input_t1](
64+
ReflectionPad2d((1, 1, 1, 1)),
65+
(torch.rand(2, 4, 3),),
66+
aten_ops=[],
67+
exir_ops=[],
68+
)
69+
pipeline.run()
70+
71+
72+
def test_reflection_pad2d_u55_INT_symbolic_width_not_delegated():
73+
width = torch.export.Dim("width", min=3, max=8)
74+
tester = ArmTester(
75+
ReflectionPad2d((1, 1, 1, 1)),
76+
(torch.rand(1, 2, 4, 5),),
77+
common.get_u55_compile_spec(),
78+
dynamic_shapes={"x": {3: width}},
79+
)
80+
tester.quantize().export().to_edge().partition()
81+
82+
targets = {
83+
node.target
84+
for node in tester.stages[tester.cur].artifact.exported_program().graph.nodes
85+
}
86+
assert torch.ops.aten.scalar_tensor.default in targets
87+
assert torch.ops.higher_order.executorch_call_delegate not in targets
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# Copyright 2026 Arm Limited and/or its affiliates.
2+
#
3+
# This source code is licensed under the BSD-style license found in the
4+
# LICENSE file in the root directory of this source tree.
5+
6+
from typing import Tuple
7+
8+
import torch
9+
10+
from executorch.backends.arm.test import common
11+
from executorch.backends.arm.test.tester.arm_tester import ArmTester
12+
from executorch.backends.arm.test.tester.test_pipeline import EthosU55PipelineINT
13+
14+
15+
input_t1 = Tuple[torch.Tensor]
16+
17+
test_data_suite_u55 = {
18+
"rank4_symmetric": lambda: (torch.rand(2, 4, 4, 4), (1, 1, 1, 1, 1, 1)),
19+
"rank5_symmetric": lambda: (torch.rand(1, 2, 4, 4, 4), (1, 1, 1, 1, 1, 1)),
20+
"asymmetric": lambda: (torch.rand(1, 2, 5, 5, 5), (1, 2, 2, 1, 3, 1)),
21+
"maximum_legal": lambda: (torch.rand(1, 2, 4, 4, 4), (3, 3, 3, 3, 3, 3)),
22+
"batched": lambda: (torch.rand(2, 2, 4, 4, 4), (1, 1, 1, 1, 1, 1)),
23+
}
24+
25+
26+
class ReflectionPad3d(torch.nn.Module):
27+
def __init__(self, padding):
28+
super().__init__()
29+
self.padding = padding
30+
31+
def forward(self, x):
32+
return torch.nn.functional.pad(x, self.padding, mode="reflect")
33+
34+
35+
@common.parametrize("test_data", test_data_suite_u55)
36+
@common.XfailIfNoCorstone300
37+
def test_reflection_pad3d_u55_INT(test_data):
38+
data, padding = test_data()
39+
pipeline = EthosU55PipelineINT[input_t1](
40+
ReflectionPad3d(padding),
41+
(data,),
42+
aten_ops=[],
43+
exir_ops=[],
44+
)
45+
pipeline.run()
46+
47+
48+
@common.XfailIfNoCorstone300
49+
def test_reflection_pad3d_u55_INT_a16w8():
50+
pipeline = EthosU55PipelineINT[input_t1](
51+
ReflectionPad3d((1, 1, 1, 1, 1, 1)),
52+
(torch.rand(1, 2, 4, 4, 4),),
53+
aten_ops=[],
54+
exir_ops=[],
55+
a16w8_quantization=True,
56+
)
57+
pipeline.run()
58+
59+
60+
def test_reflection_pad3d_u55_INT_symbolic_width_not_delegated():
61+
width = torch.export.Dim("width", min=3, max=8)
62+
tester = ArmTester(
63+
ReflectionPad3d((1, 1, 1, 1, 1, 1)),
64+
(torch.rand(1, 2, 4, 4, 5),),
65+
common.get_u55_compile_spec(),
66+
dynamic_shapes={"x": {4: width}},
67+
)
68+
tester.quantize().export().to_edge().partition()
69+
70+
targets = {
71+
node.target
72+
for node in tester.stages[tester.cur].artifact.exported_program().graph.nodes
73+
}
74+
assert torch.ops.aten.scalar_tensor.default in targets
75+
assert torch.ops.higher_order.executorch_call_delegate not in targets

0 commit comments

Comments
 (0)