Skip to content

Commit 3efa3b3

Browse files
Arm backend: Fuse consecutive slices (pytorch#20839)
Fuse consecutive slices with static shapes. cc @digantdesai @freddan80 @per @zingo @mansnils @Sebastian-Larsson @robell @rascani Signed-off-by: Oscar Andersson <oscar.andersson@arm.com>
1 parent e9eb01a commit 3efa3b3

6 files changed

Lines changed: 277 additions & 1 deletion

File tree

backends/arm/_passes/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@
114114
from .fuse_batch_norm2d_pass import FuseBatchNorm2dPass # noqa
115115
from .fuse_consecutive_concat_shapes import FuseConsecutiveConcatShapesPass # noqa
116116
from .fuse_consecutive_rescales_pass import FuseConsecutiveRescalesPass # noqa
117+
from .fuse_consecutive_slices_pass import FuseConsecutiveSlicesPass # noqa
117118
from .fuse_constant_ops_pass import ( # noqa
118119
ComputeConstantOpsAOTPass,
119120
FuseConstantArgsPass,

backends/arm/_passes/arm_pass_manager.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@
110110
FuseBatchNorm2dPass,
111111
FuseConsecutiveConcatShapesPass,
112112
FuseConsecutiveRescalesPass,
113+
FuseConsecutiveSlicesPass,
113114
FuseConstantArgsPass,
114115
FuseDuplicateUsersPass,
115116
FuseEqualPlaceholdersPass,
@@ -630,6 +631,7 @@ def _tosa_pipeline(
630631
RewriteHighRankSingletonPermutePass(),
631632
DecomposePermuteForU55Pass(),
632633
RewriteSlicePass(),
634+
FuseConsecutiveSlicesPass(),
633635
InsertConstShapesPass(),
634636
]
635637
)
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
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 Sequence, Set, Type
7+
8+
import torch
9+
from executorch.backends.arm._passes import ArmPass
10+
from executorch.backends.arm._passes.arm_pass_utils import create_node
11+
from executorch.backends.arm.tosa.mapping import TosaSpecialDtype
12+
from executorch.exir.dialects._ops import ops as exir_ops
13+
from executorch.exir.pass_base import ExportPass, PassResult
14+
from torch.fx import GraphModule, Node
15+
16+
_SLICE_INPUT = 0
17+
_SLICE_START = 1
18+
_SLICE_SIZE = 2
19+
20+
21+
class FuseConsecutiveSlicesPass(ArmPass):
22+
"""Fuse consecutive ``tosa.SLICE`` operations into one ``tosa.SLICE``.
23+
24+
For static slice operands, ``slice(slice(x, start_1, size_1), start_2,
25+
size_2)`` is equivalent to ``slice(x, start_1 + start_2, size_2)``. The
26+
pass rewrites the second slice to use the original input and removes the
27+
first slice when it becomes dead. Dynamic shape operands are left unchanged.
28+
29+
"""
30+
31+
_passes_required_after: Set[Type[ExportPass]] = set()
32+
33+
def call(self, graph_module: GraphModule) -> PassResult:
34+
graph = graph_module.graph
35+
modified = False
36+
37+
for node in list(graph.nodes):
38+
if self._try_fuse_slice(node):
39+
modified = True
40+
41+
if modified:
42+
graph.eliminate_dead_code()
43+
graph.lint()
44+
graph_module.recompile()
45+
46+
return PassResult(graph_module, modified)
47+
48+
@staticmethod
49+
def _is_slice(node: Node) -> bool:
50+
return (
51+
node.op == "call_function"
52+
and node.target == exir_ops.backend.tosa.SLICE.default
53+
)
54+
55+
@staticmethod
56+
def _constant_shape(shape_arg) -> list[int] | None:
57+
if isinstance(shape_arg, Node):
58+
if shape_arg.target != exir_ops.backend.tosa.CONST_SHAPE.default:
59+
return None
60+
shape_arg = shape_arg.args[0]
61+
62+
if not isinstance(shape_arg, Sequence) or isinstance(shape_arg, str):
63+
return None
64+
if not all(isinstance(dim, int) for dim in shape_arg):
65+
return None
66+
return list(shape_arg)
67+
68+
@staticmethod
69+
def _create_const_shape(graph: torch.fx.Graph, shape: list[int]) -> Node:
70+
node = graph.create_node(
71+
"call_function",
72+
exir_ops.backend.tosa.CONST_SHAPE.default,
73+
args=(shape,),
74+
)
75+
node.meta = {
76+
"val": shape,
77+
TosaSpecialDtype.meta_key(): TosaSpecialDtype.SHAPE,
78+
}
79+
return node
80+
81+
def _try_fuse_slice(self, node: Node) -> bool:
82+
if not self._is_slice(node):
83+
return False
84+
85+
input_node = node.args[_SLICE_INPUT]
86+
if not isinstance(input_node, Node) or not self._is_slice(input_node):
87+
return False
88+
89+
first_start = self._constant_shape(input_node.args[_SLICE_START])
90+
second_start = self._constant_shape(node.args[_SLICE_START])
91+
second_size = self._constant_shape(node.args[_SLICE_SIZE])
92+
if first_start is None or second_start is None or second_size is None:
93+
return False
94+
if not (len(first_start) == len(second_start) == len(second_size)):
95+
return False
96+
97+
fused_start = [outer + inner for outer, inner in zip(first_start, second_start)]
98+
graph = node.graph
99+
100+
with graph.inserting_before(node):
101+
fused_start_node = self._create_const_shape(graph, fused_start)
102+
fused_size_node = self._create_const_shape(graph, second_size)
103+
fused_slice = create_node(
104+
graph,
105+
exir_ops.backend.tosa.SLICE.default,
106+
args=(input_node.args[_SLICE_INPUT], fused_start_node, fused_size_node),
107+
from_node=node,
108+
inherit_qparams=True,
109+
)
110+
111+
node.replace_all_uses_with(fused_slice)
112+
return True

backends/arm/test/ops/test_slice.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ def test_slice_tensor_tosa_FP_fp8(test_data):
9494
exir_op,
9595
tosa_extensions=[tosa_extension],
9696
)
97-
pipeline.count_tosa_ops({"SLICE": 3})
97+
pipeline.count_tosa_ops({"SLICE": 1})
9898
pipeline.run()
9999

100100

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
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 executorch.backends.arm.tosa.dialect # noqa: F401
9+
import torch
10+
from executorch.backends.arm._passes import FuseConsecutiveSlicesPass, RewriteSlicePass
11+
from executorch.backends.arm.test.tester.test_pipeline import PassPipeline
12+
from executorch.backends.arm.tosa.mapping import TosaSpecialDtype
13+
from executorch.backends.arm.tosa.specification import (
14+
TosaLoweringContext,
15+
TosaSpecification,
16+
)
17+
from executorch.exir.dialects._ops import ops as exir_ops
18+
19+
input_t = Tuple[torch.Tensor]
20+
TOSA_FP_SPEC = TosaSpecification.create_from_string("TOSA-1.0+FP")
21+
22+
23+
class ConsecutiveSlice(torch.nn.Module):
24+
def forward(self, x: torch.Tensor) -> torch.Tensor:
25+
y = torch.ops.aten.slice.Tensor(x, 1, 1, 7, 1)
26+
return torch.ops.aten.slice.Tensor(y, 2, 2, 5, 1)
27+
28+
29+
def _const_shape(graph: torch.fx.Graph, shape: list[int]) -> torch.fx.Node:
30+
node = graph.create_node(
31+
"call_function",
32+
exir_ops.backend.tosa.CONST_SHAPE.default,
33+
args=(shape,),
34+
)
35+
node.meta = {
36+
"val": shape,
37+
TosaSpecialDtype.meta_key(): TosaSpecialDtype.SHAPE,
38+
}
39+
return node
40+
41+
42+
def _slice(
43+
graph: torch.fx.Graph,
44+
input_node: torch.fx.Node,
45+
start: list[int],
46+
size: list[int],
47+
output_shape: tuple[int, ...],
48+
) -> torch.fx.Node:
49+
slice_node = graph.create_node(
50+
"call_function",
51+
exir_ops.backend.tosa.SLICE.default,
52+
args=(input_node, _const_shape(graph, start), _const_shape(graph, size)),
53+
)
54+
slice_node.meta["val"] = torch.ones(output_shape)
55+
return slice_node
56+
57+
58+
def _slice_nodes(graph_module: torch.fx.GraphModule) -> list[torch.fx.Node]:
59+
return [
60+
node
61+
for node in graph_module.graph.nodes
62+
if node.op == "call_function"
63+
and node.target == exir_ops.backend.tosa.SLICE.default
64+
]
65+
66+
67+
def _shape_arg_value(node: torch.fx.Node, arg_index: int) -> list[int]:
68+
shape_node = node.args[arg_index]
69+
assert isinstance(shape_node, torch.fx.Node)
70+
assert shape_node.target == exir_ops.backend.tosa.CONST_SHAPE.default
71+
return list(shape_node.args[0]) # type: ignore[arg-type]
72+
73+
74+
def test_fuse_consecutive_slices_combines_static_starts() -> None:
75+
graph = torch.fx.Graph()
76+
x = graph.placeholder("x")
77+
x.meta["val"] = torch.ones((1, 8, 10))
78+
first_slice = _slice(graph, x, [0, 1, 0], [1, 6, 10], (1, 6, 10))
79+
second_slice = _slice(graph, first_slice, [0, 2, 3], [1, 2, 4], (1, 2, 4))
80+
graph.output(second_slice)
81+
graph_module = torch.fx.GraphModule({}, graph)
82+
83+
with TosaLoweringContext(TOSA_FP_SPEC):
84+
result = FuseConsecutiveSlicesPass().call(graph_module)
85+
86+
slice_nodes = _slice_nodes(result.graph_module)
87+
88+
assert result.modified
89+
assert len(slice_nodes) == 1
90+
assert slice_nodes[0].args[0] is x
91+
assert _shape_arg_value(slice_nodes[0], 1) == [0, 3, 3]
92+
assert _shape_arg_value(slice_nodes[0], 2) == [1, 2, 4]
93+
94+
95+
def test_fuse_consecutive_slices_tosa_FP_pipeline() -> None:
96+
pipeline = PassPipeline[input_t](
97+
ConsecutiveSlice(),
98+
(torch.randn(1, 8, 10),),
99+
ops_before_pass={
100+
"executorch_exir_dialects_edge__ops_aten_slice_copy_Tensor": 2
101+
},
102+
ops_after_pass={"backend__ops_tosa_SLICE_default": 1},
103+
ops_not_after_pass=[
104+
"executorch_exir_dialects_edge__ops_aten_slice_copy_Tensor"
105+
],
106+
pass_list=[RewriteSlicePass, FuseConsecutiveSlicesPass],
107+
)
108+
pipeline.pop_stage(-1)
109+
110+
pipeline.run()

backends/arm/test/passes/test_fuse_duplicate_users_pass.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,16 @@
55

66
from typing import Dict, Tuple
77

8+
import executorch.backends.arm.tosa.dialect # noqa: F401
89
import torch
910
from executorch.backends.arm._passes import FuseDuplicateUsersPass
1011
from executorch.backends.arm.test import common
1112
from executorch.backends.arm.test.tester.test_pipeline import PassPipeline
13+
from executorch.backends.arm.tosa.specification import (
14+
TosaLoweringContext,
15+
TosaSpecification,
16+
)
17+
from executorch.exir.dialects._ops import ops as exir_ops
1218
from torch.fx import Graph, GraphModule
1319

1420
input_t = Tuple[torch.Tensor] # Input x
@@ -61,6 +67,36 @@ def _set_val(node, val):
6167
return node
6268

6369

70+
def _rescale_nodes(graph_module):
71+
return [
72+
node
73+
for node in graph_module.graph.nodes
74+
if node.op == "call_function"
75+
and node.target == exir_ops.backend.tosa.RESCALE.default
76+
]
77+
78+
79+
def _graph_with_duplicate_rescale_users() -> GraphModule:
80+
graph = Graph()
81+
x = _set_val(graph.placeholder("x"), torch.ones(1, dtype=torch.int8))
82+
rescale_args = (x, torch.int32, [1.0], 16, 0)
83+
first_rescale = _set_val(
84+
graph.call_function(exir_ops.backend.tosa.RESCALE.default, rescale_args),
85+
torch.ones(1, dtype=torch.int32),
86+
)
87+
second_rescale = _set_val(
88+
graph.call_function(exir_ops.backend.tosa.RESCALE.default, rescale_args),
89+
torch.ones(1, dtype=torch.int32),
90+
)
91+
output = graph.output((first_rescale, second_rescale))
92+
output.meta["val"] = (
93+
torch.ones(1, dtype=torch.int32),
94+
torch.ones(1, dtype=torch.int32),
95+
)
96+
graph.lint()
97+
return GraphModule(torch.nn.Module(), graph)
98+
99+
64100
def _graph_with_users_not_in_node_order() -> GraphModule:
65101
graph = Graph()
66102
x = _set_val(graph.placeholder("x"), torch.ones(1))
@@ -116,3 +152,18 @@ def test_fuse_duplicate_users_preserves_graph_order_for_representative():
116152
result.graph_module.graph.lint()
117153
assert result.modified
118154
assert len(_add_node_names(result.graph_module)) == 1
155+
156+
157+
def test_fuse_duplicate_users_removes_identical_rescale_users():
158+
graph_module = _graph_with_duplicate_rescale_users()
159+
160+
with TosaLoweringContext(TosaSpecification.create_from_string("TOSA-1.0+INT")):
161+
result = FuseDuplicateUsersPass()(graph_module)
162+
163+
rescale_nodes = _rescale_nodes(result.graph_module)
164+
165+
result.graph_module.graph.lint()
166+
assert result.modified
167+
assert len(rescale_nodes) == 1
168+
output_node = result.graph_module.graph.output_node()
169+
assert output_node.args[0] == (rescale_nodes[0], rescale_nodes[0])

0 commit comments

Comments
 (0)