Skip to content

Commit 4dc1b5b

Browse files
authored
Reland "Arm backend: Add real implementation for TOSA dialect ops (re-land)" v2 (#20674) (#20674)
Summary: Reverts #20670 and relands #20537 In a first commit, Arm backend: Add infra for real implementations of Tosa ops Tested by applying the infra to the avg_pool2d Tosa dialect op. The rewrite_avg_pool2d tests can now be ran to verify that the produced Tosa is correct. To make it completely correct, two additional passes need to be added to the test. Then, Arm backend: Add real impls to all TOSA dialect ops Additionally, Remove special case in ComputeOpsAOT pass for such ops, since they can now be executed. Start running the model in tests were this was previously impossible due to ops not having a real impl. Differential Revision: D110362915 Pulled By: rascani
1 parent 9e43417 commit 4dc1b5b

33 files changed

Lines changed: 538 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)