Skip to content

Commit d28c77e

Browse files
Arm backend: Support dyn. shapes in rewrite_slice (pytorch#23053)
Preserve symbolic and proxy slice bounds when normalizing start and end indices. Build dynamic slice sizes from shape operators and relax TOSA SLICE validation for non-static start and size values. cc @digantdesai @freddan80 @per @zingo @mansnils @Sebastian-Larsson @robell @rascani Signed-off-by: Oscar Andersson <oscar.andersson@arm.com>
1 parent 72ad3d1 commit d28c77e

7 files changed

Lines changed: 288 additions & 34 deletions

File tree

‎backends/arm/_passes/rewrite_slice.py‎

Lines changed: 55 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
#
33
# This source code is licensed under the BSD-style license found in the
44
# LICENSE file in the root directory of this source tree.
5+
import operator
56
from typing import Set, Type
67

78
from executorch.backends.arm._passes import ArmOpTargetedPass
@@ -18,16 +19,29 @@ class RewriteSlicePass(ArmOpTargetedPass):
1819
_passes_required_after: Set[Type[ExportPass]] = set()
1920
target_ops = (exir_ops.edge.aten.slice_copy.Tensor,)
2021

21-
def _fixup_start(self, start, input_shape, dim) -> int:
22-
"""Convert negative and out-of-bounds start indices to valid positive
23-
indices.
24-
"""
25-
idx = start or 0
26-
if idx < 0:
27-
idx = start % input_shape[dim]
28-
if idx >= input_shape[dim]:
29-
idx = input_shape[dim] - 1
30-
return idx
22+
def _normalize_slice_bound(
23+
self,
24+
bound: int | None,
25+
dim_size: int | SymInt | ProxyValue,
26+
*,
27+
is_start: bool,
28+
meta,
29+
) -> int | SymInt | ProxyValue:
30+
if isinstance(dim_size, int):
31+
return (
32+
slice(bound, None).indices(dim_size)[0]
33+
if is_start
34+
else slice(None, bound).indices(dim_size)[1]
35+
)
36+
if bound is None:
37+
return 0 if is_start else dim_size
38+
if bound < 0:
39+
return super().call_sym(
40+
max,
41+
(super().call_sym(operator.add, (dim_size, bound), meta), 0),
42+
meta,
43+
)
44+
return super().call_sym(min, (bound, dim_size), meta)
3145

3246
def call_operator(self, op, args, kwargs, meta, updated=False) -> ProxyValue:
3347
if op not in self.target_ops:
@@ -38,14 +52,32 @@ def call_operator(self, op, args, kwargs, meta, updated=False) -> ProxyValue:
3852
f"Only slice with 4 arguments and step of 1 is supported, got {len(args)} arguments and step {args[4]}"
3953
)
4054
input, dim, start, end = args[:4]
55+
if isinstance(start, (SymInt, ProxyValue)) or isinstance(
56+
end, (SymInt, ProxyValue)
57+
):
58+
return super().call_operator(op, args, kwargs, meta, updated)
59+
4160
input_shape = input.data.shape
42-
start_index = self._fixup_start(start, input_shape, dim)
61+
size_list = list(meta.data["val"].shape)
62+
# TOSA SLICE rejects statically known non-positive sizes, including
63+
# dimensions that are not being sliced.
64+
if any(isinstance(size, int) and size <= 0 for size in size_list):
65+
return super().call_operator(op, args, kwargs, meta, updated)
4366

44-
start_list = [0] * len(input_shape)
67+
dim_size = input_shape[dim]
68+
if isinstance(dim_size, SymInt):
69+
dim_size = super().call_size_operator(input, dim, meta)
70+
start_index = self._normalize_slice_bound(
71+
start, dim_size, is_start=True, meta=meta
72+
)
73+
end_index = self._normalize_slice_bound(
74+
end, dim_size, is_start=False, meta=meta
75+
)
76+
77+
start_list: list[int | SymInt | ProxyValue] = [0] * len(input_shape)
4578
start_list[dim] = start_index
46-
size_list = list(meta.data["val"].shape)
4779

48-
if any(isinstance(dim, SymInt) for dim in start_list):
80+
if any(isinstance(dim, (SymInt, ProxyValue)) for dim in start_list):
4981
starts = start_list
5082
else:
5183
starts = super().call_shape_operator( # type: ignore[assignment]
@@ -55,8 +87,15 @@ def call_operator(self, op, args, kwargs, meta, updated=False) -> ProxyValue:
5587
meta,
5688
True,
5789
)
58-
if any(isinstance(dim, SymInt) for dim in size_list):
59-
sizes = size_list
90+
if any(isinstance(d, SymInt) for d in size_list):
91+
# Express sizes as ProxyValues to allow for symbolic shapes
92+
input_shape_proxy = super().call_size_operator_all(input, meta)
93+
sizes = input_shape_proxy
94+
if isinstance(size_list[dim], SymInt):
95+
size = super().call_sym(operator.sub, (end_index, start_index), meta)
96+
sizes[dim] = super().call_sym(max, (size, 0), meta)
97+
else:
98+
sizes[dim] = size_list[dim]
6099
else:
61100
sizes = super().call_shape_operator( # type: ignore[assignment]
62101
exir_ops.backend.tosa.CONST_SHAPE.default,

‎backends/arm/_passes/symbolic_to_tosa_shape_pass.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
operator.mul: exir_ops.backend.tosa.MUL_SHAPE.default,
2929
operator.mod: exir_ops.backend.tosa.MOD_SHAPE.default,
3030
operator.floordiv: exir_ops.backend.tosa.DIV_FLOOR_SHAPE.default,
31+
max: exir_ops.backend.tosa.MAX_SHAPE.default,
32+
min: exir_ops.backend.tosa.MIN_SHAPE.default,
3133
}
3234

3335

‎backends/arm/operator_support/slice_copy_support.py‎

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,24 +31,53 @@ class SliceCopySupported(SupportedTOSAOperatorCheck):
3131

3232
targets = [exir_ops.edge.aten.slice_copy.Tensor]
3333

34-
def is_node_tosa_supported(
35-
self, node: fx.Node, tosa_spec: TosaSpecification
36-
) -> bool: # type: ignore[override, misc]
34+
@staticmethod
35+
def _has_symbolic_bound(node: fx.Node) -> bool:
36+
return any(
37+
isinstance(bound, torch.SymInt)
38+
or isinstance(bound, fx.Node)
39+
and isinstance(bound.meta.get("val"), torch.SymInt)
40+
for bound in node.args[2:4]
41+
)
42+
43+
@staticmethod
44+
def _has_no_non_positive_static_output_size(node: fx.Node) -> bool:
45+
return all(
46+
not isinstance(output_size, int) or output_size > 0
47+
for output_size in node.meta["val"].shape
48+
)
49+
50+
def _has_valid_slice_arguments(self, node: fx.Node) -> bool:
3751
if len(node.args) not in (4, 5):
3852
self.reporter.report_reject(
3953
node,
4054
f"{node.target}: expected 4 or 5 args, got {len(node.args)}.",
4155
)
4256
return False
57+
if len(node.args) == 5 and node.args[4] <= 0: # type: ignore[operator]
58+
self.reporter.report_reject(
59+
node, f"{node.target}: step must be > 0, got {node.args[4]}."
60+
)
61+
return False
62+
return True
4363

44-
if len(node.args) == 5:
45-
step = node.args[4]
46-
if step <= 0: # type: ignore[operator]
47-
self.reporter.report_reject(
48-
node,
49-
f"{node.target}: step must be > 0, got {step}.",
50-
)
51-
return False
64+
def is_node_tosa_supported(
65+
self, node: fx.Node, tosa_spec: TosaSpecification
66+
) -> bool: # type: ignore[override, misc]
67+
if not self._has_valid_slice_arguments(node):
68+
return False
69+
70+
if self._has_symbolic_bound(node):
71+
self.reporter.report_reject(
72+
node, "Symbolic slice bounds cannot be lowered to TOSA SLICE."
73+
)
74+
return False
75+
76+
if not self._has_no_non_positive_static_output_size(node):
77+
self.reporter.report_reject(
78+
node, "TOSA SLICE requires a guaranteed positive output size."
79+
)
80+
return False
5281

5382
values_dtype = node.args[0].meta["val"].dtype # type: ignore[union-attr]
5483

‎backends/arm/test/misc/tosa_dialect/test_tosa_shape_support.py‎

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@
1212
import executorch.backends.arm.operator_support.sym_size_int_support # noqa: F401
1313
import pytest
1414
import torch
15+
from executorch.backends.arm.operator_support.slice_copy_support import (
16+
SliceCopySupported,
17+
)
1518
from executorch.backends.arm.operator_support.symint_arithmetic_support import (
1619
SymIntArithmeticSupport,
1720
)
18-
1921
from executorch.backends.arm.operator_support.tosa_supported_operators import (
2022
tosa_support_factory,
2123
)
@@ -604,3 +606,51 @@ def test_shape_extension_rejects_non_symint_arithmetic():
604606
)
605607

606608
assert support.is_node_supported({}, arithmetic_node) is False
609+
610+
611+
def test_shape_extension_rejects_slice_with_symbolic_bound():
612+
class SymbolicBoundSlice(torch.nn.Module):
613+
def forward(self, x):
614+
return torch.ops.aten.slice.Tensor(x, 1, 0, x.shape[0], 1)
615+
616+
inputs = (torch.randn(4, 5),)
617+
exported_program = _exported_program(
618+
SymbolicBoundSlice(),
619+
inputs,
620+
dynamic_shapes=({0: Dim("batch", min=2, max=5)},),
621+
)
622+
support, reporter = _support("TOSA-1.1+FP+shape", exported_program)
623+
slice_node = _find_node(exported_program, exir_ops.edge.aten.slice_copy.Tensor)
624+
625+
assert support.is_node_supported(exported_program.graph_module, slice_node) is False
626+
assert "Symbolic slice bounds" in reporter.get_table_report()
627+
628+
629+
def test_shape_extension_rejects_slice_with_empty_unsliced_dimension():
630+
class SliceSecondDimension(torch.nn.Module):
631+
def forward(self, x):
632+
return torch.ops.aten.slice.Tensor(x, 1, 0, 3, 1)
633+
634+
exported_program = _exported_program(SliceSecondDimension(), (torch.randn(0, 5),))
635+
reporter = WhyNoPartitionReporter()
636+
support = SliceCopySupported(
637+
TosaSpecification.create_from_string("TOSA-1.1+FP+shape"), reporter
638+
)
639+
slice_node = _find_node(exported_program, exir_ops.edge.aten.slice_copy.Tensor)
640+
641+
assert support.is_node_supported(exported_program.graph_module, slice_node) is False
642+
643+
644+
def test_shape_extension_rejects_empty_slice():
645+
class EmptySlice(torch.nn.Module):
646+
def forward(self, x):
647+
return torch.ops.aten.slice.Tensor(x, 1, 0, -6, 1)
648+
649+
exported_program = _exported_program(EmptySlice(), (torch.randn(2, 5),))
650+
reporter = WhyNoPartitionReporter()
651+
support = SliceCopySupported(
652+
TosaSpecification.create_from_string("TOSA-1.1+FP+shape"), reporter
653+
)
654+
slice_node = _find_node(exported_program, exir_ops.edge.aten.slice_copy.Tensor)
655+
656+
assert support.is_node_supported(exported_program.graph_module, slice_node) is False

0 commit comments

Comments
 (0)