Skip to content

Commit 1319d82

Browse files
author
Github Executorch
committed
Update base for Update on "stop double-installing ExecuTorch in one-off linux jobs"
setup-linux.sh already installs ExecuTorch with XNNPACK (and it passes use-pt-pinned-commit as it should). Differential Revision: [D67996460](https://our.internmc.facebook.com/intern/diff/D67996460/) [ghstack-poisoned]
2 parents 0ef3827 + 0c4053e commit 1319d82

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

+2687
-412
lines changed

.ci/docker/ci_commit_pins/pytorch.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
2ea4b56ec872424e486c4fe2d55da061067a2ed3
1+
0a94bb432ed75cc2d950d81b2921363218a7e459

.github/workflows/apple-perf.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -410,7 +410,7 @@ jobs:
410410
runs-on: linux.2xlarge
411411
steps:
412412
- name: Download the apps from GitHub
413-
uses: actions/download-artifact@v3
413+
uses: actions/download-artifact@v4
414414
with:
415415
# The name here needs to match the name of the upload-artifact parameter
416416
name: ios-apps

.github/workflows/apple.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ jobs:
5353
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
5454
timeout: 90
5555
secrets-env: BUILD_CERTIFICATE_BASE64 EXECUTORCH_DEMO_BUILD_PROVISION_PROFILE_BASE64 KEYCHAIN_PASSWORD
56-
upload-artifact: ios-apps
56+
upload-artifact: ios-demo-app
5757
script: |
5858
set -eux
5959
@@ -83,10 +83,10 @@ jobs:
8383
runs-on: linux.2xlarge
8484
steps:
8585
- name: Download the artifacts from GitHub
86-
uses: actions/download-artifact@v3
86+
uses: actions/download-artifact@v4
8787
with:
8888
# The name here needs to match the name of the upload-artifact parameter
89-
name: ios-apps
89+
name: ios-demo-app
9090
path: ${{ runner.temp }}/artifacts/
9191

9292
- name: Verify the artifacts
@@ -216,7 +216,7 @@ jobs:
216216
role-to-assume: arn:aws:iam::308535385114:role/gha_executorch_upload-frameworks-ios
217217
aws-region: us-east-1
218218
- name: Download the artifact
219-
uses: actions/download-artifact@v3
219+
uses: actions/download-artifact@v4
220220
with:
221221
# NB: The name here needs to match the upload-artifact name from build-frameworks-ios job
222222
name: executorch-frameworks-ios
@@ -291,7 +291,7 @@ jobs:
291291
python-version: '3.11'
292292
submodules: 'true'
293293
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
294-
upload-artifact: ios-apps
294+
upload-artifact: ios-benchmark-app
295295
secrets-env: BUILD_CERTIFICATE_BASE64 EXECUTORCH_BENCHMARK_BUILD_PROVISION_PROFILE_BASE64 KEYCHAIN_PASSWORD
296296
timeout: 90
297297
script: |

.gitmodules

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@
6666
url = https://github.com/pybind/pybind11.git
6767
[submodule "backends/cadence/fusion_g3/third-party/nnlib/nnlib-FusionG3"]
6868
path = backends/cadence/fusion_g3/third-party/nnlib/nnlib-FusionG3
69-
url = https://github.com/foss-xtensa/nnlib-FusionG3/
69+
url = https://github.com/foss-xtensa/nnlib-FusionG3.git
7070
[submodule "third-party/ao"]
7171
path = third-party/ao
7272
url = https://github.com/pytorch/ao.git

backends/apple/mps/mps_preprocess.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@
3232
CompileSpec,
3333
PreprocessResult,
3434
)
35+
36+
from executorch.exir.passes.memory_format_ops_pass import DimOrderOpsRevertPass
37+
from executorch.exir.program._program import _transform
3538
from torch.export.exported_program import ExportedProgram
3639

3740
FORMAT = "[%(levelname)s %(asctime)s %(filename)s:%(lineno)s] %(message)s"
@@ -83,6 +86,9 @@ def preprocess(
8386
# FlatBuffer graph, process the `output` nodes and add their id to
8487
# the `output_ids` array in the schema.
8588

89+
# TODO: Remove this once we have a better support for the dim-order ops.
90+
edge_program = _transform(edge_program, DimOrderOpsRevertPass())
91+
8692
mps_graph = MPSGraph(
8793
version="0",
8894
mps_nodes=[],

backends/apple/mps/operators/constant_ops.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,25 @@ def define_node(
7979
)
8080

8181

82+
@register_node_visitor
83+
class ToDimOrderEmptyVisitor(NodeVisitor):
84+
target = ["dim_order_ops._empty_dim_order.default"]
85+
86+
def __init__(self, *args) -> None:
87+
super().__init__(*args)
88+
89+
def define_node(
90+
self,
91+
node: torch.fx.Node,
92+
mps_graph: MPSGraph,
93+
) -> None:
94+
# We should never get here, because DimOrderOpsRevertPass replaces this with an aten.empty.memory_format op
95+
# But if we do, we can't handle it ATM, so raise an exception
96+
raise NotImplementedError(
97+
"dim_order_ops._empty_dim_order.default is not supported yet"
98+
)
99+
100+
82101
@register_node_visitor
83102
class FullLikeVisitor(NodeVisitor):
84103
target = "aten.full_like.default"

backends/apple/mps/operators/op_clone.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,22 @@ def define_node(
3333
)
3434
input_id = self.define_tensor(get_input_node(node, 0), mps_graph)
3535
self.tensor_to_id[node] = input_id
36+
37+
38+
@register_node_visitor
39+
class ToDimOrderCopyVisitor(NodeVisitor):
40+
target = ["dim_order_ops._to_dim_order_copy.default"]
41+
42+
def __init__(self, *args) -> None:
43+
super().__init__(*args)
44+
45+
def define_node(
46+
self,
47+
node: torch.fx.Node,
48+
mps_graph: MPSGraph,
49+
) -> None:
50+
# We should never get here, because DimOrderOpsRevertPass replaces this with an aten._to_copy op
51+
# But if we do, we can't handle it ATM, so raise an exception
52+
raise NotImplementedError(
53+
"dim_order_ops._to_dim_order_copy.default is not supported yet"
54+
)

backends/apple/mps/test/test_mps.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1829,6 +1829,21 @@ def forward(self, x):
18291829
Clone(), model_inputs, func_name=inspect.stack()[0].function[5:]
18301830
)
18311831

1832+
def test_mps_backend_to_copy(self):
1833+
class Copy(torch.nn.Module):
1834+
def forward(self, x):
1835+
return (
1836+
torch.ops.aten._to_copy.default(
1837+
x + 2, memory_format=torch.contiguous_format
1838+
)
1839+
+ x
1840+
)
1841+
1842+
model_inputs = (torch.randn(1, 3, 3),)
1843+
self.lower_and_test_with_partitioner(
1844+
Copy(), model_inputs, func_name=inspect.stack()[0].function[5:]
1845+
)
1846+
18321847
def test_mps_backend_floor(self):
18331848
class Floor(torch.nn.Module):
18341849
def forward(self, x):

backends/apple/mps/test/test_mps_utils.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,7 @@
2626

2727
# Config for Capturing the weights, will be moved in the future
2828

29-
# TODO(T182928844): Delegate dim order op to backend.
30-
_EDGE_COMPILE_CONFIG = exir.EdgeCompileConfig(
31-
_check_ir_validity=False, _skip_dim_order=True
32-
)
29+
_EDGE_COMPILE_CONFIG = exir.EdgeCompileConfig(_check_ir_validity=False)
3330

3431

3532
class ansi_colors:
@@ -219,7 +216,6 @@ def lower_module_and_test_output(
219216
dynamic_shapes=dynamic_shapes,
220217
edge_compile_config=EdgeCompileConfig(
221218
_check_ir_validity=False,
222-
_skip_dim_order=True, # TODO(T182928844): Delegate dim order op to backend.
223219
),
224220
)
225221

@@ -250,7 +246,6 @@ def lower_module_and_test_output(
250246
export(delegated_program, sample_inputs, strict=True),
251247
compile_config=exir.EdgeCompileConfig(
252248
_check_ir_validity=False,
253-
_skip_dim_order=True, # TODO(T182928844): Delegate dim order op to backend.
254249
),
255250
).to_executorch(
256251
config=ExecutorchBackendConfig(extract_delegate_segments=False)

backends/arm/README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,28 @@ The you can run the tests with
122122
pytest -c /dev/null -v -n auto backends/arm/test --arm_quantize_io --arm_run_corstoneFVP
123123
```
124124

125+
### Code coverage
126+
127+
To get code coverage:
128+
129+
```
130+
coverage run --source=<SRC> --rcfile=backends/arm/test/.coveragerc -m pytest \
131+
--config-file=/dev/null backends/arm/test/
132+
```
133+
134+
All files in `SRC` and its child directories will be analysed for code coverage,
135+
unless explicitly exluded in the .coveragerc file. If using venv this might be
136+
under `env/lib/python<VERSION_NUMBER>/site-packages/executorch/`. To get the
137+
absolute path, run:
138+
139+
```
140+
python -c "import executorch; print(executorch.__path__)"
141+
```
142+
143+
This contains a list of paths where the source directory is located. Pick the
144+
one that is located in `env/lib`. If that does not work try the others. Add
145+
`backends/arm` to the path in `--source` to only get code coverage for the Arm
146+
backend.
125147

126148
### A note on unit tests
127149

backends/arm/_passes/arm_pass_manager.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@
3737
QuantizeFullArgument,
3838
RetraceFoldedDtypesPass,
3939
)
40+
from executorch.backends.arm._passes.fuse_quantized_activation_pass import (
41+
FuseQuantizedActivationPass,
42+
)
4043
from executorch.backends.arm._passes.insert_table_ops import InsertTableOpsPass
4144
from executorch.backends.arm._passes.keep_dims_false_to_squeeze_pass import (
4245
KeepDimsFalseToSqueezePass,
@@ -73,6 +76,7 @@ def transform_to_backend_pipeline(
7376
self, exported_program: ExportedProgram, compile_spec: list[CompileSpec]
7477
):
7578
"""Apply passes before transforming program to backend"""
79+
self.add_pass(FuseQuantizedActivationPass())
7680
self.add_pass(DecomposeLinearPass())
7781
self.add_pass(RemoveGetItemPass())
7882
self.add_pass(DecomposeLayerNormPass())
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
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 torch
7+
from executorch.backends.arm.tosa_quant_utils import q_op
8+
from executorch.exir.dialects._ops import ops as exir_ops
9+
from executorch.exir.pass_base import ExportPass, PassResult
10+
from torch.fx import Node
11+
12+
13+
class FuseQuantizedActivationPass(ExportPass):
14+
def _is_fuseable_quantized_activation(self, node: Node):
15+
"""Fuse activations that have a 0 lower bound and quantized with a qmin zero-point"""
16+
is_fuseable = node.target == exir_ops.edge.aten.relu.default
17+
if node.target == exir_ops.edge.aten.hardtanh.default:
18+
min_val = node.args[1]
19+
is_fuseable = min_val == 0
20+
21+
is_quantized = len(node.users) == 1 and next(iter(node.users)).target == q_op
22+
if is_quantized:
23+
quant_node = next(iter(node.users))
24+
zp = quant_node.args[2]
25+
qmin = quant_node.args[3]
26+
27+
return is_fuseable and is_quantized and zp == qmin
28+
29+
def _is_fuseable_input(self, node: Node):
30+
return (
31+
node.target
32+
in (
33+
exir_ops.edge.aten.convolution.default,
34+
exir_ops.edge.aten.linear.default,
35+
)
36+
and len(node.users) == 1
37+
)
38+
39+
def call(self, graph_module: torch.fx.GraphModule):
40+
modified = False
41+
for node in graph_module.graph.nodes:
42+
if node.op != "call_function":
43+
continue
44+
45+
if not self._is_fuseable_quantized_activation(node):
46+
continue
47+
48+
input_node = node.args[0]
49+
if not self._is_fuseable_input(input_node):
50+
continue
51+
52+
node.replace_all_uses_with(input_node)
53+
graph_module.graph.erase_node(node)
54+
modified = True
55+
56+
if modified:
57+
graph_module.recompile()
58+
graph_module = super().call(graph_module).graph_module
59+
60+
return PassResult(graph_module, modified)

backends/arm/quantizer/quantization_annotator.py

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,41 @@ def _annotate_output(node: Node, quant_property: _QuantProperty):
8989
_annotate_output_qspec(node, quant_property.qspec)
9090

9191

92+
def _match_pattern(
93+
node: Node, pattern: List[List], filter_fn: Optional[Callable[[Node], bool]] = None
94+
) -> bool:
95+
"""
96+
Check if there's a chain of node.ancestors? -> node -> node.descendant? that matches the
97+
chain provided in 'pattern'. If 'filter_fn' is provided, check that all the nodes in the
98+
chain pass the filtering.
99+
100+
Each 'pattern' element is composed of a list of disjunctive nodes types.
101+
"""
102+
assert len(pattern) == 2, "Only two-nodes patterns supported currently"
103+
104+
if node.target in pattern[0]:
105+
assert len(node.users) != 0
106+
parent = node
107+
child = next(iter(node.users))
108+
elif node.target in pattern[1]:
109+
assert len(node.args) != 0
110+
parent = node.args[0]
111+
child = node
112+
else:
113+
return False
114+
115+
if len(parent.users) != 1:
116+
return False
117+
118+
if parent.target not in pattern[0] or child.target not in pattern[1]:
119+
return False
120+
121+
if filter_fn is not None:
122+
return filter_fn(parent) and filter_fn(child)
123+
124+
return True
125+
126+
92127
_one_to_one = [
93128
torch.ops.aten.exp.default,
94129
torch.ops.aten.log.default,
@@ -164,7 +199,36 @@ def get_quant_properties( # noqa: C901
164199
bias_qspec = quantization_config.get_bias_qspec()
165200

166201
quant_properties = _OpQuantProperties()
167-
if node.target in (
202+
203+
def any_or_hardtanh_min_zero(n: Node):
204+
# Check that if the node is a hardtanh, its min_val is zero
205+
return n.target != torch.ops.aten.hardtanh.default or n.args[1] == 0
206+
207+
if _match_pattern(
208+
node,
209+
[
210+
[
211+
torch.ops.aten.conv1d.default,
212+
torch.ops.aten.conv2d.default,
213+
torch.ops.aten.linear.default,
214+
],
215+
[torch.ops.aten.relu.default, torch.ops.aten.hardtanh.default],
216+
],
217+
any_or_hardtanh_min_zero,
218+
):
219+
if node.target in (
220+
torch.ops.aten.conv1d.default,
221+
torch.ops.aten.conv2d.default,
222+
torch.ops.aten.linear.default,
223+
):
224+
quant_properties.quant_inputs = [
225+
_QuantProperty(0, input_act_qspec),
226+
_QuantProperty(1, weight_qspec, mark_annotated=True),
227+
_QuantProperty(2, bias_qspec, optional=True, mark_annotated=True),
228+
]
229+
else:
230+
quant_properties.quant_output = _QuantProperty(0, output_act_qspec)
231+
elif node.target in (
168232
torch.ops.aten.conv1d.default,
169233
torch.ops.aten.conv2d.default,
170234
torch.ops.aten.linear.default,

backends/arm/test/.coveragerc

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[run]
2+
omit =
3+
*__init__.py*
4+
5+
[report]
6+
skip_covered = true
7+
exclude_also =
8+
raise NotImplementedError

backends/arm/test/ops/test_conv_combos.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -137,10 +137,11 @@ class ComboConvRelu6(torch.nn.Module):
137137
]
138138

139139
test_data = [
140-
(20 * torch.randn(1, 3, 256, 256),),
141-
(5 * torch.randn(1, 3, 256, 256),),
140+
(2 * torch.randn(1, 3, 256, 256),),
141+
(0.5 * torch.randn(1, 3, 256, 256),),
142142
(torch.randn(1, 3, 256, 256),),
143-
(-5 * torch.randn(1, 3, 256, 256),),
143+
(-0.5 * torch.randn(1, 3, 256, 256),),
144+
(-2 * torch.randn(1, 3, 256, 256),),
144145
]
145146

146147
def __init__(self):

0 commit comments

Comments
 (0)