Skip to content

Commit 1e915be

Browse files
authored
Arm backend: Integrate exir to Tosa pass (pytorch#20281)
Adds pass to arm pass manager to integrate exir_to_dialect_pass. Also adds TOSA dialect activation node visitors. Signed-off-by: Saoirse Stewart <saoirse.stewart@arm.com>
1 parent b094b0e commit 1e915be

11 files changed

Lines changed: 194 additions & 57 deletions

File tree

backends/arm/_passes/TARGETS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ runtime.python_library(
5151
deps = [
5252
":core",
5353
":arm_pass_manager_base" if runtime.is_oss else ":arm_pass_manager_fb",
54+
"//executorch/backends/transforms:aten_to_dialect_pass",
5455
"//executorch/backends/arm/tosa:utils",
5556
"//executorch/backends/arm/tosa/dialect:lib",
5657
"//executorch/backends/transforms:fuse_view_copy",

backends/arm/_passes/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@
104104
from .decorate_fp32_to_int32_casting_pass import DecorateFp32toInt32CastingPass # noqa
105105
from .deduplicate_get_attr_pass import DeduplicateGetAttrPass # noqa
106106
from .ensure_unique_output_nodes_pass import EnsureUniqueOutputNodesPass # noqa
107+
from .exir_to_tosa_pass import ExirToTosaPass # noqa
107108
from .fold_qdq_with_annotated_qparams_pass import ( # noqa
108109
FoldAndAnnotateQParamsPass,
109110
QuantizeClampArgumentsPass,

backends/arm/_passes/arm_pass_manager.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@
104104
DecorateFp32toInt32CastingPass,
105105
DeduplicateGetAttrPass,
106106
EnsureUniqueOutputNodesPass,
107+
ExirToTosaPass,
107108
FoldAndAnnotateQParamsPass,
108109
FuseBatchNorm2dPass,
109110
FuseConsecutiveConcatShapesPass,
@@ -622,6 +623,7 @@ def _tosa_pipeline(
622623
DecomposePermuteForU55Pass(),
623624
RewriteSlicePass(),
624625
InsertConstShapesPass(),
626+
ExirToTosaPass(exported_program),
625627
]
626628
)
627629

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
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 torch
7+
from executorch.backends.arm.tosa.specification import get_context_spec
8+
from executorch.backends.transforms.aten_to_dialect_pass import (
9+
AtenToDialectPass,
10+
DialectNodeSpec,
11+
)
12+
from executorch.exir.dialects._ops import ops as exir_ops
13+
from torch.fx import Node
14+
15+
16+
# Each rewrite returns the TOSA dialect node spec for one supported ATen
17+
# activation op, preserving args unless TOSA requires normalized attributes.
18+
def rewrite_erf(node: Node, pass_: AtenToDialectPass) -> DialectNodeSpec:
19+
return DialectNodeSpec(
20+
exir_ops.backend.tosa.ERF.default,
21+
node.args,
22+
dict(node.kwargs),
23+
)
24+
25+
26+
def rewrite_sigmoid(node: Node, pass_: AtenToDialectPass) -> DialectNodeSpec:
27+
return DialectNodeSpec(
28+
exir_ops.backend.tosa.SIGMOID.default,
29+
node.args,
30+
dict(node.kwargs),
31+
)
32+
33+
34+
def rewrite_tanh(node: Node, pass_: AtenToDialectPass) -> DialectNodeSpec:
35+
return DialectNodeSpec(
36+
exir_ops.backend.tosa.TANH.default,
37+
node.args,
38+
dict(node.kwargs),
39+
)
40+
41+
42+
def _extract_dtype(node: Node) -> torch.dtype | None:
43+
value = node.meta.get("val")
44+
if isinstance(value, tuple):
45+
value = value[0]
46+
if isinstance(value, list):
47+
if not value:
48+
return None
49+
value = value[0]
50+
return getattr(value, "dtype", None)
51+
52+
53+
def _dtype_bounds(dtype: torch.dtype) -> tuple[int | float, int | float]:
54+
if dtype.is_floating_point:
55+
fp_info = torch.finfo(dtype)
56+
return fp_info.min, fp_info.max
57+
58+
int_info = torch.iinfo(dtype)
59+
return int_info.min, int_info.max
60+
61+
62+
def _is_tosa_clamp_dtype_supported(dtype: torch.dtype) -> bool:
63+
tosa_spec = get_context_spec()
64+
65+
if dtype == torch.int8:
66+
return tosa_spec.support_integer()
67+
68+
if dtype == torch.int16:
69+
return tosa_spec.support_integer() and tosa_spec.support_extension("int16")
70+
71+
if dtype in (torch.float16, torch.float32):
72+
return tosa_spec.support_float()
73+
74+
if dtype == torch.bfloat16:
75+
return tosa_spec.support_float() and tosa_spec.support_extension("bf16")
76+
77+
return False
78+
79+
80+
def _normalize_clamp_bound(
81+
bound,
82+
*,
83+
dtype: torch.dtype,
84+
default: int | float,
85+
) -> int | float | None:
86+
if bound is None:
87+
return default
88+
if isinstance(bound, bool):
89+
return None
90+
if dtype.is_floating_point:
91+
if isinstance(bound, (int, float)):
92+
return float(bound)
93+
return None
94+
if isinstance(bound, int):
95+
return bound
96+
return None
97+
98+
99+
def _get_min_max_arguments(
100+
node: Node, dtype: torch.dtype
101+
) -> tuple[int | float, int | float] | None:
102+
dtype_min, dtype_max = _dtype_bounds(dtype)
103+
min_val = _normalize_clamp_bound(
104+
node.args[1] if len(node.args) > 1 else node.kwargs.get("min"),
105+
dtype=dtype,
106+
default=dtype_min,
107+
)
108+
max_val = _normalize_clamp_bound(
109+
node.args[2] if len(node.args) > 2 else node.kwargs.get("max"),
110+
dtype=dtype,
111+
default=dtype_max,
112+
)
113+
if min_val is None or max_val is None:
114+
return None
115+
return min_val, max_val
116+
117+
118+
def rewrite_clamp(node: Node, pass_: AtenToDialectPass) -> DialectNodeSpec | None:
119+
dtype = _extract_dtype(node)
120+
if dtype is None or not _is_tosa_clamp_dtype_supported(dtype):
121+
return None
122+
123+
min_max_args = _get_min_max_arguments(node, dtype)
124+
if min_max_args is None:
125+
return None
126+
127+
return DialectNodeSpec(
128+
exir_ops.backend.tosa.CLAMP.default,
129+
(node.args[0], *min_max_args),
130+
)
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
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+
from executorch.backends.arm._passes.aten_to_tosa_activation_functions import (
8+
rewrite_clamp,
9+
rewrite_erf,
10+
rewrite_sigmoid,
11+
rewrite_tanh,
12+
)
13+
from executorch.backends.transforms.aten_to_dialect_pass import AtenToDialectPass
14+
from executorch.exir.dialects._ops import ops as exir_ops
15+
16+
17+
class ExirToTosaPass(AtenToDialectPass):
18+
"""Rewrite simple EXIR ops to equivalent backend TOSA dialect ops.
19+
20+
Rewrite functions are grouped by op category and registered with the shared
21+
ATen-to-dialect pass infrastructure.
22+
23+
"""
24+
25+
26+
_ACTIVATION_FUNCTION_REWRITES = {
27+
exir_ops.edge.aten.clamp.default: rewrite_clamp,
28+
exir_ops.edge.aten.erf.default: rewrite_erf,
29+
exir_ops.edge.aten.sigmoid.default: rewrite_sigmoid,
30+
exir_ops.edge.aten.tanh.default: rewrite_tanh,
31+
}
32+
33+
_DIRECT_REWRITE_CATEGORIES = {
34+
"activation_functions": _ACTIVATION_FUNCTION_REWRITES,
35+
}
36+
37+
# Register each category's ATen targets with the function that builds the
38+
# corresponding TOSA dialect node spec.
39+
for _rewrite_category in _DIRECT_REWRITE_CATEGORIES.values():
40+
for _edge_target, _rewrite_fn in _rewrite_category.items():
41+
ExirToTosaPass.register_dialect_substitution(_edge_target)(_rewrite_fn)

backends/arm/operators/__init__.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,9 @@
1919
op_bitwise_not,
2020
op_cat,
2121
op_ceil,
22-
op_clamp,
2322
op_cond_if,
2423
op_cos,
2524
op_eq,
26-
op_erf,
2725
op_exp,
2826
op_floor,
2927
op_ge,
@@ -40,18 +38,18 @@
4038
op_repeat,
4139
op_rshift_tensor,
4240
op_rsqrt,
43-
op_sigmoid,
4441
op_sin,
4542
op_sub,
4643
op_sum,
47-
op_tanh,
4844
op_to_dim_order_copy,
4945
op_tosa_avg_pool2d,
5046
op_tosa_avg_pool2d_adaptive,
47+
op_tosa_clamp,
5148
op_tosa_conv2d,
5249
op_tosa_conv3d,
5350
op_tosa_custom,
5451
op_tosa_depthwise_conv2d,
52+
op_tosa_erf,
5553
op_tosa_gather,
5654
op_tosa_identity,
5755
op_tosa_matmul,
@@ -62,8 +60,10 @@
6260
op_tosa_resize,
6361
op_tosa_scatter,
6462
op_tosa_shapes,
63+
op_tosa_sigmoid,
6564
op_tosa_slice,
6665
op_tosa_table,
66+
op_tosa_tanh,
6767
op_tosa_transpose_conv2d,
6868
op_view,
6969
op_where,
Lines changed: 10 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@
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
55

6-
7-
from typing import Any, List, Tuple
6+
from typing import Any, cast, List
87

98
import torch
109
import tosa_serializer as ts
@@ -18,49 +17,19 @@
1817
validate_same_dtype,
1918
validate_valid_dtype,
2019
)
21-
2220
from executorch.backends.arm.tosa.mapping import TosaArg
2321
from torch.fx import Node
2422

2523

2624
@register_node_visitor
2725
class ClampVisitor(NodeVisitor):
28-
target = "aten.clamp.default"
29-
30-
def __init__(self, *args):
31-
super().__init__(*args)
32-
33-
def _get_min_max_arguments(
34-
self, node: Node, dtype: torch.dtype
35-
) -> Tuple[int | float, int | float]:
36-
def cast_type(value: Any) -> int | float:
37-
if isinstance(value, int):
38-
return value
39-
else:
40-
# Attempt to cast to float
41-
return float(value)
42-
43-
if dtype.is_floating_point:
44-
dtype_min = torch.finfo(dtype).min
45-
dtype_max = torch.finfo(dtype).max
46-
else:
47-
dtype_min = torch.iinfo(dtype).min
48-
dtype_max = torch.iinfo(dtype).max
26+
target = "tosa.CLAMP.default"
4927

50-
min_arg = dtype_min
51-
max_arg = dtype_max
52-
53-
if node.args[1] is not None:
54-
min_arg = cast_type(node.args[1])
55-
56-
if len(node.args) > 2:
57-
if node.args[2] is not None:
58-
max_arg = cast_type(node.args[2])
59-
60-
return min_arg, max_arg
61-
62-
def _to_bytes(self, value: int | float, dtype: torch.dtype) -> bytes:
63-
return torch.full((1,), value, dtype=dtype).view(torch.uint8).numpy().tolist()
28+
def _to_bytes(self, value: int | float, dtype: torch.dtype) -> List[int]:
29+
return cast(
30+
List[int],
31+
torch.full((1,), value, dtype=dtype).view(torch.uint8).numpy().tolist(),
32+
)
6433

6534
def define_node(
6635
self,
@@ -71,12 +40,7 @@ def define_node(
7140
) -> None:
7241
validate_num_inputs(self.target, inputs, [2, 3])
7342
validate_same_dtype(self.target, [inputs[0], output], ts)
74-
supported_dtypes = [
75-
ts.DType.INT8,
76-
ts.DType.FP16,
77-
ts.DType.BF16,
78-
ts.DType.FP32,
79-
]
43+
supported_dtypes = [ts.DType.INT8, ts.DType.FP16, ts.DType.BF16, ts.DType.FP32]
8044
if self.tosa_spec.support_extension("int16"):
8145
supported_dtypes.append(ts.DType.INT16)
8246
validate_valid_dtype(
@@ -87,8 +51,8 @@ def define_node(
8751
)
8852

8953
node_input_dtype = node.meta["val"].dtype
90-
# NOTE: Quantization of the min/max arguments is handled by QuantizeOperatorArguments
91-
min_val, max_val = self._get_min_max_arguments(node, node_input_dtype)
54+
min_val = cast(int | float, node.args[1])
55+
max_val = cast(int | float, node.args[2])
9256

9357
attr = ts.TosaSerializerAttribute()
9458
attr.ClampAttribute(
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
@register_node_visitor
1919
class ErfVisitor(SimpleNodeVisitor):
20-
target = "aten.erf.default"
20+
target = "tosa.ERF.default"
2121
tosa_specs = FP_SPECS
2222

2323
@classmethod
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
@register_node_visitor
1919
class SigmoidVisitor(SimpleNodeVisitor):
20-
target = "aten.sigmoid.default"
20+
target = "tosa.SIGMOID.default"
2121
tosa_specs = FP_SPECS
2222

2323
@classmethod
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
@register_node_visitor
1919
class TanhVisitor(SimpleNodeVisitor):
20-
target = "aten.tanh.default"
20+
target = "tosa.TANH.default"
2121
tosa_specs = FP_SPECS
2222

2323
@classmethod

0 commit comments

Comments
 (0)