Skip to content

Commit a971a5a

Browse files
committed
Update
[ghstack-poisoned]
2 parents 33ca14b + 0222074 commit a971a5a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+1241
-540
lines changed

backends/arm/CMakeLists.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2023 Arm Limited and/or its affiliates.
1+
# Copyright 2023, 2025 Arm Limited and/or its affiliates.
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.
@@ -22,7 +22,7 @@ set(THIRD_PARTY_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/third-party")
2222
set(DRIVER_ETHOSU_INCLUDE_DIR "${THIRD_PARTY_ROOT}/ethos-u-core-driver/include")
2323
include_directories(${DRIVER_ETHOSU_INCLUDE_DIR})
2424

25-
set(_arm_baremetal_sources backends/arm/runtime/ArmBackendEthosU.cpp
25+
set(_arm_baremetal_sources backends/arm/runtime/EthosUBackend.cpp
2626
backends/arm/runtime/VelaBinStream.cpp
2727
)
2828
list(TRANSFORM _arm_baremetal_sources PREPEND "${EXECUTORCH_ROOT}/")

backends/arm/README.md

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ ethos-u-vela compilation stack. which follows the fully AoT flow.
1515
## Layout
1616

1717
Export:
18-
- `arm_backend.py` - Main entrypoint for the ArmPartitioner and ArmBackend. For more information see the section on
18+
- `ethosu_backend.py` - Main entrypoint for the EthosUBackend. For more information see the section on
1919
[Arm Backend Architecture](#arm-backend-architecture). For examples of use see `executorch/examples/arm`.
2020
- `tosa_mapping.py` - utilities for mapping edge dialect to TOSA
2121
- `tosa_quant_utils.py` - utilities for mapping quantization information to TOSA encoding
@@ -29,11 +29,11 @@ Passes:
2929
- `*_pass.py` - Compiler passes derived from ExportPass
3030

3131
Quantization:
32-
- `arm_quantizer.py` - Quantizer for Arm backend
32+
- `arm_quantizer.py` - Quantizers for Arm backend. Contains the EthosUQuantizer which inherits from the TOSAQuantizer
3333
- `arm_quantizer_utils.py` - Utilities for quantization
3434

3535
Runtime:
36-
- `runtime/ArmBackendEthosU.cpp` - The Arm backend implementation of the ExecuTorch runtime backend (BackendInterface) for Ethos-U
36+
- `runtime/ArmEthosUBackend.cpp` - The Arm backend implementation of the ExecuTorch runtime backend (BackendInterface) for Ethos-U
3737

3838
Other:
3939
- `third-party/` - Dependencies on other code - in particular the TOSA serialization_lib for compiling to TOSA and the ethos-u-core-driver for the bare-metal backend supporting Ethos-U
@@ -177,6 +177,7 @@ create an issue on [github](https://www.github.com/pytorch/executorch/issues).
177177
# Arm Backend Architecture
178178

179179
The broad principle with the Arm backend implemention for ExecuTorch is to support multiple Arm devices and device configurations through a largely Homogeneous flow with maximal sharing of class logic.
180+
The EthosUBackend is currently the one user facing API that target the Ethos-U55 and Ethos-U85 hardware IP. It is using the TOSABackend under the hood to share code and functionality, but also to separate testing possibilities to the TOSA flow itself.
180181

181182
In practice for compilation, this means that the flow goes via [Arm TOSA](https://www.mlplatform.org/tosa/tosa_spec.html) to produce a common IR and quantization behaviour compatible with our various IP, and typically, device-specific backends to further lower to a device specific binary which can happen ahead of time (within the Python development flow) or at runtime (during a JIT compilation stage).
182183

@@ -185,22 +186,22 @@ In practice for the runtime, this means we will share common runtime backend fun
185186

186187
## Arm Backend Status and Maturity
187188

188-
The Arm Backend should be considered a prototype quality at this point, likely subject to significant change and improvement, and with a limited coverage of functionality. We are actively developing this codebase.
189+
The Arm EthosU Backend should be considered a prototype quality at this point, likely subject to significant change and improvement, and with a limited coverage of functionality. We are actively developing this codebase.
189190

190191
## Current flows
191192

192-
The ArmBackend has a two stage process,
193-
- Compile to TOSA to rationalise the graph into known hardware support profiles. Currently this is to v0.80 TOSA BI with specific concern to a subset which gives support on Ethos-U55, the target of the initial prototype efforts.
193+
The EthosUBackend has a two stage process,
194+
- Compile to TOSA to rationalise the graph into known hardware support profiles. Currently this is to v0.80 TOSA BI with specific concern to a subset which gives support on Ethos-U55 and Ethos-U85, the target of the initial prototype efforts. This calls into the TOSABackend.
194195
- Lower via the ethos-u-vela compilation flow which takes TOSA v0.80 as an input and produces a low level commandstream for the hardware which is then passed via the delegate to the ethos-u-core-driver for direct execution.
195196

196-
The ArmPartitioner is currenly used to ensure the operations converted are Ethos-U compatible, but will be extended to offer spec-correct TOSA Base inference and TOSA Main Inference generation in future.
197+
The EthosUPartitioner is currenly used to ensure the operations converted are Ethos-U compatible, but will be extended to offer spec-correct TOSA Base inference and TOSA Main Inference generation in future.
198+
199+
There is also a generic TOSABackend with accompanying TOSAPartitioner and TOSAQuantizer, which are used by the EthosUBackend and friends. The Arm TOSA Backend can be used by it's own to verify the lowering to the TOSA representation of the model (refer to the unit tests in backends/arm/test which uses the TOSA backend in the test suites).
197200

198201
### Controlling compilation
199202

200203
It is possible to control the compilation flow to aid in development and debug of both networks and the code itself.
201204

202-
Configuration of the ArmBackend export flow is controlled by CompileSpec information (essentially used as compilation flags) to determine which of these outputs is produced. In particular this allows for use of the tosa_reference_model to run intermediate output to check for correctness and quantization accuracy without a full loop via hardware implemntation.
203-
204-
As this is in active development see the ArmBackend for accurate information on [compilation flags](https://github.com/pytorch/executorch/blob/29f6dc9353e90951ed3fae3c57ae416de0520067/backends/arm/arm_backend.py#L319-L324)
205+
Configuration of the EthosUBackend export flow is controlled by CompileSpec information (essentially used as compilation flags) to determine which of these outputs is produced. In particular this allows for use of the tosa_reference_model to run intermediate output to check for correctness and quantization accuracy without a full loop via hardware implemntation.
205206

206-
You can also refer to the [example TOSA end-to-end code](/examples/arm/arm_tosa_e2e.py)
207+
As this is in active development see the EthosUBackend for accurate information on [compilation flags](https://github.com/pytorch/executorch/blob/29f6dc9353e90951ed3fae3c57ae416de0520067/backends/arm/arm_backend.py#L319-L324)

backends/arm/_passes/arm_pass_manager.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Copyright (c) Meta Platforms, Inc. and affiliates.
2-
# Copyright 2024-2025 Arm Limited and/or its affiliates.
32
# All rights reserved.
3+
# Copyright 2024-2025 Arm Limited and/or its affiliates.
44
#
55
# This source code is licensed under the BSD-style license found in the
66
# LICENSE file in the root directory of this source tree.
@@ -18,6 +18,9 @@
1818
from executorch.backends.arm._passes.convert_expand_copy_to_repeat import (
1919
ConvertExpandCopyToRepeatPass,
2020
)
21+
from executorch.backends.arm._passes.convert_full_like_to_full_pass import (
22+
ConvertFullLikeToFullPass,
23+
)
2124
from executorch.backends.arm._passes.convert_split_to_slice import (
2225
ConvertSplitToSlicePass,
2326
)
@@ -49,6 +52,7 @@
4952
from executorch.backends.arm._passes.fuse_quantized_activation_pass import ( # type: ignore[import-not-found]
5053
FuseQuantizedActivationPass,
5154
)
55+
from executorch.backends.arm._passes.insert_rescales_pass import InsertRescalePass
5256
from executorch.backends.arm._passes.insert_table_ops import InsertTableOpsPass
5357
from executorch.backends.arm._passes.keep_dims_false_to_squeeze_pass import (
5458
KeepDimsFalseToSqueezePass,
@@ -72,6 +76,7 @@
7276
UnsqueezeScalarPlaceholdersPass,
7377
)
7478
from executorch.backends.arm.tosa_specification import TosaSpecification
79+
7580
from executorch.backends.xnnpack._passes.remove_getitem_op import RemoveGetItemPass
7681
from executorch.exir import ExportedProgram
7782
from executorch.exir.pass_manager import PassManager
@@ -95,6 +100,7 @@ def _tosa_080_BI_pipeline(self, exported_program: ExportedProgram) -> GraphModul
95100
self.add_pass(ConvertMmToBmmPass())
96101
self.add_pass(DecomposeLinearPass())
97102
self.add_pass(ConvertMeanDimToAveragePoolPass())
103+
self.add_pass(ConvertFullLikeToFullPass())
98104

99105
self.add_pass(AnnotateDecomposedMatmulPass())
100106
self.add_pass(QuantizeOperatorArguments())
@@ -115,7 +121,7 @@ def _tosa_080_BI_pipeline(self, exported_program: ExportedProgram) -> GraphModul
115121
self.add_pass(ConvertSqueezesToViewPass())
116122

117123
self.add_pass(AnnotateChannelsLastDimOrder())
118-
124+
self.add_pass(InsertRescalePass())
119125
return self._transform(exported_program.graph_module)
120126

121127
def _tosa_080_MI_pipeline(self, exported_program: ExportedProgram) -> GraphModule:
@@ -133,7 +139,7 @@ def _tosa_080_MI_pipeline(self, exported_program: ExportedProgram) -> GraphModul
133139
self.add_pass(ConvertMeanDimToAveragePoolPass())
134140
self.add_pass(DecomposeDivPass())
135141
self.add_pass(DecomposeSoftmaxesPass())
136-
142+
self.add_pass(ConvertFullLikeToFullPass())
137143
self.add_pass(AnnotateDecomposedMatmulPass())
138144
self.add_pass(QuantizeOperatorArguments())
139145
self.add_pass(FoldAndAnnotateQParamsPass()) # type: ignore[call-arg]
@@ -153,6 +159,7 @@ def _tosa_080_MI_pipeline(self, exported_program: ExportedProgram) -> GraphModul
153159
self.add_pass(ConvertSqueezesToViewPass())
154160

155161
self.add_pass(AnnotateChannelsLastDimOrder())
162+
self.add_pass(InsertRescalePass())
156163

157164
return self._transform(exported_program.graph_module)
158165

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Copyright 2025 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 executorch.exir.dialects._ops import ops as exir_ops
7+
from executorch.exir.pass_base import ExportPass
8+
9+
10+
class ConvertFullLikeToFullPass(ExportPass):
11+
"""As per the full_like pytorch documentation,
12+
`torch.full_like(input, fill_value)` is equivalent to
13+
`torch.full(input.size(),
14+
fill_value,
15+
dtype=input.dtype,
16+
layout=input.layout,
17+
device=input.device
18+
)`
19+
Skip layout and device since it's not relevant for our backend.
20+
"""
21+
22+
def call_operator(self, op, args, kwargs, meta):
23+
if op not in [
24+
exir_ops.edge.aten.full_like.default,
25+
]:
26+
return super().call_operator(op, args, kwargs, meta)
27+
28+
tensor = args[0].data
29+
full_args = (list(tensor.shape), args[1])
30+
full_kwargs = {"dtype": tensor.dtype}
31+
return super().call_operator(
32+
exir_ops.edge.aten.full.default, full_args, full_kwargs, meta
33+
)

backends/arm/_passes/fold_qdq_with_annotated_qparams_pass.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,9 @@ def call(self, graph_module: GraphModule) -> PassResult:
131131
n = cast(Node, n)
132132
if n.op != "call_function":
133133
continue
134+
# Don't fold chains of quant-ops into each other.
135+
if n.target in (q_op, dq_op):
136+
continue
134137

135138
# Make sure we haven't already set qparams meta information on the node
136139
assert "input_qparams" not in n.meta.keys()
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
# Copyright 2025 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 logging
7+
from copy import copy
8+
from typing import cast
9+
10+
import torch
11+
from executorch.backends.arm._passes.arm_pass_utils import create_node
12+
from executorch.backends.arm.tosa_quant_utils import dq_op, q_op, QuantArgs
13+
from executorch.exir.pass_base import ExportPass, PassResult
14+
from torch import Tensor
15+
from torch.fx import GraphModule, Node
16+
from torch.library import custom_op, register_fake
17+
18+
logger = logging.getLogger(__name__)
19+
20+
21+
@custom_op("tosa::_rescale", mutates_args=()) # type: ignore[misc]
22+
def rescale(
23+
x: Tensor, dtype: torch.dtype, scale: float, in_zp: int, out_zp: int
24+
) -> Tensor:
25+
logger.warning(
26+
"Ran default implementation of tosa::_rescale."
27+
"This op is meant to always be inserted inside a partition and a correct default implementation is not implemented."
28+
)
29+
# Clone is needed to not return reference when rescaling to same dtype.
30+
# This is a neccessary requirement for non-mutating custom ops.
31+
return x.to(dtype=dtype).clone()
32+
33+
34+
@register_fake("tosa::_rescale") # type: ignore[misc]
35+
def rescale_fake(
36+
x: Tensor, dtype: torch.dtype, scale: float, in_zp: int, out_zp: int
37+
) -> Tensor:
38+
"""Casts the input tensor to dtype `dtype` to produce the correct tensor meta for a _rescale op.
39+
Additionally validates TOSA constraints of a RESCALE op.
40+
"""
41+
if not (dtype == torch.int32 or dtype == torch.int8):
42+
raise NotImplementedError(
43+
"tosa::rescale currently only supports int32 and int8."
44+
)
45+
if dtype == torch.int32 and out_zp != 0:
46+
raise ValueError(
47+
"TOSA requires output_zp to be zero when the output dtype is int32."
48+
)
49+
if x.dtype == torch.int32 and in_zp != 0:
50+
raise ValueError(
51+
"TOSA requires input_zp to be zero when the input dtype is int32."
52+
)
53+
if x.dtype == torch.int8 and not -128 <= in_zp <= 127:
54+
raise ValueError(f"{in_zp=} outside valid range (-128,127) for int8.")
55+
if dtype == torch.int8 and not -128 <= out_zp <= 127:
56+
raise ValueError(f"{out_zp=} outside valid range (-128,127) for int8.")
57+
58+
return x.to(dtype=dtype).clone()
59+
60+
61+
class InsertRescalePass(ExportPass):
62+
"""Finds patterns of dq -> q, and replaces them
63+
with passthrough_to_tosa::rescales.
64+
65+
Does not garantuee that the dtypes and zero points are valid
66+
in TOSA, that is the job of the quantization annotator that
67+
produced the dq and q nodes. The TOSA constraints are validated
68+
in the fake implementation of passthrough_to_tosa:rescale.
69+
"""
70+
71+
def fold_dq_q_to_rescale(self, node: Node, user: Node, graph_module: GraphModule):
72+
dq_args = QuantArgs.from_operator(node.target, node.args)
73+
q_args = QuantArgs.from_operator(user.target, user.args)
74+
new_scale = dq_args.scale / q_args.scale
75+
76+
with graph_module.graph.inserting_before(node):
77+
rescale_node = create_node(
78+
graph_module.graph,
79+
torch.ops.tosa._rescale.default,
80+
(
81+
node.all_input_nodes[0],
82+
q_args.dtype,
83+
new_scale,
84+
dq_args.zp,
85+
q_args.zp,
86+
),
87+
)
88+
rescale_node.meta = copy(user.meta)
89+
user.replace_all_uses_with(rescale_node)
90+
graph_module.graph.erase_node(user)
91+
92+
def call(self, graph_module: GraphModule) -> PassResult:
93+
modified = False
94+
for node in graph_module.graph.nodes:
95+
node = cast(Node, node)
96+
97+
if node.target is not dq_op:
98+
continue
99+
# Copy users since we remove them while iterating, modyfing the node.users list.
100+
for user in copy(node.users):
101+
if user.target is q_op:
102+
self.fold_dq_q_to_rescale(node, user, graph_module)
103+
modified = True
104+
if len(node.users) == 0:
105+
graph_module.graph.erase_node(node)
106+
107+
graph_module = super().call(graph_module).graph_module
108+
graph_module.recompile()
109+
return PassResult(graph_module, modified)

0 commit comments

Comments
 (0)