Skip to content

Commit d87e5e5

Browse files
authored
Revert "Revert "Arm backend: Add real implementation for TOSA dialect ops (re…"
This reverts commit 5909e23.
1 parent 5909e23 commit d87e5e5

32 files changed

Lines changed: 534 additions & 118 deletions

backends/arm/_passes/fuse_constant_ops_pass.py

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,20 @@ def _is_tosa_dialect_op(target) -> bool:
9292
or "<EdgeOpOverload: tosa." in target_str
9393
)
9494

95+
@staticmethod
96+
def _has_real_tosa_dialect_impl(target) -> bool:
97+
schema = getattr(target, "_schema", None)
98+
op_name = getattr(schema, "name", None)
99+
if op_name is None:
100+
return False
101+
102+
try:
103+
return torch._C._dispatch_has_kernel_for_dispatch_key(
104+
op_name, "CompositeExplicitAutograd"
105+
)
106+
except RuntimeError:
107+
return False
108+
95109
@staticmethod
96110
def _arg_contains_symbolic_shape(arg) -> bool:
97111
if isinstance(arg, torch.fx.Node):
@@ -197,19 +211,31 @@ def resolve_arg(arg, arg_index=None):
197211

198212
return True
199213

214+
def maybe_delete(self, input_nodes_to_maybe_delete):
215+
for input_node in input_nodes_to_maybe_delete:
216+
if input_node.meta.get("is_input", False):
217+
# Never delete submodule inputs, they need to match the parameters from the outer module.
218+
continue
219+
if len(input_node.users) == 0:
220+
self._delete_constant_placeholder(input_node)
221+
200222
def call(self, graph_module):
201223
modified = False
202224
input_nodes_to_maybe_delete = set()
203225
for node in graph_module.graph.nodes:
204226
if node.op != "call_function":
205227
continue
206-
# Don't fuse TOSA dialect ops as they do not have eager forward functions.
207-
# Also don't fuse ops whose explicit args/kwargs include symbolic shape values.
208-
if (
209-
self._is_tosa_dialect_op(node.target)
210-
or self._arg_contains_symbolic_shape(node.args)
211-
or self._arg_contains_symbolic_shape(node.kwargs)
212-
):
228+
if node.target == exir_ops.backend.tosa.RESCALE.default:
229+
# Leave fusing of RESCALES to the compiler.
230+
continue
231+
if self._is_tosa_dialect_op(
232+
node.target
233+
) and not self._has_real_tosa_dialect_impl(node.target):
234+
continue
235+
# Don't fuse ops whose explicit args/kwargs include symbolic shape values.
236+
if self._arg_contains_symbolic_shape(
237+
node.args
238+
) or self._arg_contains_symbolic_shape(node.kwargs):
213239
continue
214240

215241
input_nodes = node.all_input_nodes
@@ -241,10 +267,7 @@ def call(self, graph_module):
241267

242268
if modified:
243269
graph_module.graph.eliminate_dead_code()
244-
for input_node in input_nodes_to_maybe_delete:
245-
if len(input_node.users) == 0:
246-
self._delete_constant_placeholder(input_node)
247-
270+
self.maybe_delete(input_nodes_to_maybe_delete)
248271
graph_module = super().call(graph_module).graph_module
249272

250273
return PassResult(graph_module, modified)

backends/arm/operators/node_visitor.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
TosaSpecMapping,
3232
)
3333

34+
3435
logger = logging.getLogger(__name__)
3536

3637

@@ -246,3 +247,15 @@ def get_node_visitors(*args) -> Dict[str, NodeVisitor]:
246247
node_visitors[target] = visitor(*args)
247248

248249
return node_visitors
250+
251+
252+
def get_node_visitor(target: str, tosa_spec: TosaSpecification):
253+
# Ensure all operator modules are imported so visitors are registered.
254+
import executorch.backends.arm.operators # noqa: F401
255+
256+
node_visitor_tuples = _node_visitor_tuples.get(tosa_spec)
257+
for target_name, node_visitor_cls in node_visitor_tuples:
258+
if target_name == target:
259+
return node_visitor_cls(tosa_spec)
260+
261+
raise ValueError(f"No {target} NodeVisitor registered for {tosa_spec}")
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
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+
import executorch.backends.arm.tosa.dialect # noqa: F401
7+
8+
import torch
9+
from executorch.backends.arm.tosa.specification import (
10+
TosaLoweringContext,
11+
TosaSpecification,
12+
)
13+
from executorch.exir.dialects._ops import ops as exir_ops
14+
from torch._subclasses.fake_tensor import FakeTensorMode
15+
16+
17+
def test_gather_tosa_FP_fake() -> None:
18+
values = torch.randn((1, 4, 3), dtype=torch.float32)
19+
indices = torch.tensor([[0, 2]], dtype=torch.int32)
20+
21+
with TosaLoweringContext(
22+
TosaSpecification.create_from_string("TOSA-1.0+FP")
23+
), FakeTensorMode() as mode:
24+
output = exir_ops.backend.tosa.GATHER.default(
25+
mode.from_tensor(values),
26+
mode.from_tensor(indices),
27+
)
28+
29+
assert output.dtype == values.dtype
30+
assert tuple(output.shape) == (1, 2, 3)
31+
32+
33+
def test_gather_tosa_FP_real() -> None:
34+
values = torch.tensor(
35+
[[[1.0, 10.0], [2.0, 20.0], [3.0, 30.0], [4.0, 40.0]]],
36+
dtype=torch.float32,
37+
)
38+
indices = torch.tensor([[3, 1]], dtype=torch.int32)
39+
40+
with TosaLoweringContext(TosaSpecification.create_from_string("TOSA-1.0+FP")):
41+
output = exir_ops.backend.tosa.GATHER.default(values, indices)
42+
43+
expected = values[:, indices[0], :]
44+
torch.testing.assert_close(output, expected)

backends/arm/test/misc/tosa_dialect/test_tosa_identity.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from torch._subclasses.fake_tensor import FakeTensorMode
1414

1515

16-
def test_identity_tosa_FP() -> None:
16+
def test_identity_tosa_FP_fake() -> None:
1717
sample_input = torch.randn((1, 2, 3, 4), dtype=torch.float32)
1818

1919
with TosaLoweringContext(
@@ -23,3 +23,12 @@ def test_identity_tosa_FP() -> None:
2323

2424
assert output.dtype == sample_input.dtype
2525
assert tuple(output.shape) == tuple(sample_input.shape)
26+
27+
28+
def test_identity_tosa_FP_real() -> None:
29+
sample_input = torch.randn((1, 2, 3, 4), dtype=torch.float32)
30+
31+
with TosaLoweringContext(TosaSpecification.create_from_string("TOSA-1.0+FP")):
32+
output = exir_ops.backend.tosa.IDENTITY.default(sample_input)
33+
34+
torch.testing.assert_close(output, sample_input)
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
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+
import executorch.backends.arm.tosa.dialect # noqa: F401
7+
import pytest
8+
import torch
9+
10+
from executorch.backends.arm.tosa.specification import (
11+
TosaLoweringContext,
12+
TosaSpecification,
13+
)
14+
from executorch.exir.dialects._ops import ops as exir_ops
15+
from torch._subclasses.fake_tensor import FakeTensor, FakeTensorMode
16+
17+
18+
@pytest.mark.parametrize(
19+
"kwargs",
20+
[
21+
{},
22+
{"input_unsigned": False, "output_unsigned": False},
23+
],
24+
)
25+
def test_rescale_real_impl_with_and_without_kwargs(kwargs):
26+
input_tensor = torch.tensor(
27+
[[1, -2, 3], [4, 0, -5]],
28+
dtype=torch.int32,
29+
)
30+
31+
with TosaLoweringContext(
32+
TosaSpecification.create_from_string("TOSA-1.0+INT")
33+
), FakeTensorMode() as mode:
34+
fake_output = exir_ops.backend.tosa.RESCALE.default(
35+
mode.from_tensor(input_tensor),
36+
torch.int32,
37+
[1.0],
38+
0,
39+
0,
40+
**kwargs,
41+
)
42+
43+
assert isinstance(fake_output, FakeTensor)
44+
assert fake_output.dtype == torch.int32
45+
assert tuple(fake_output.shape) == tuple(input_tensor.shape)
46+
47+
with TosaLoweringContext(TosaSpecification.create_from_string("TOSA-1.0+INT")):
48+
output = exir_ops.backend.tosa.RESCALE.default(
49+
input_tensor,
50+
torch.int32,
51+
[1.0],
52+
0,
53+
0,
54+
**kwargs,
55+
)
56+
57+
assert not isinstance(output, FakeTensor)
58+
assert output.dtype == torch.int32
59+
assert tuple(output.shape) == tuple(input_tensor.shape)
60+
assert torch.equal(output, input_tensor)

backends/arm/test/passes/test_ensure_unique_output_nodes_pass.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ def test_ensure_unique_output_nodes_no_target_inserts_identity_per_repeated_outp
3535
"executorch_exir_dialects_backend__ops_tosa_IDENTITY_default": 2,
3636
},
3737
)
38-
pipeline.pop_stage("run_method_and_compare_outputs")
3938
pipeline.run()
4039

4140
graph_module = (
@@ -62,5 +61,4 @@ def test_ensure_unique_output_nodes_no_target_keeps_unique_outputs_unchanged() -
6261
"executorch_exir_dialects_backend__ops_tosa_IDENTITY_default",
6362
],
6463
)
65-
pipeline.pop_stage("run_method_and_compare_outputs")
6664
pipeline.run()

0 commit comments

Comments
 (0)