Skip to content

Commit 71361ac

Browse files
authored
Merge branch 'main' into add-q5k-mlx
2 parents ab1e446 + 539bda2 commit 71361ac

64 files changed

Lines changed: 1361 additions & 839 deletions

File tree

Some content is hidden

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

.ci/scripts/wheel/test_base.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Copyright (c) Meta Platforms, Inc. and affiliates.
22
# All rights reserved.
3+
# Copyright 2026 Arm Limited and/or its affiliates.
34
#
45
# This source code is licensed under the BSD-style license found in the
56
# LICENSE file in the root directory of this source tree.
@@ -40,6 +41,23 @@ class ModelTest:
4041
backend: Backend
4142

4243

44+
def test_cmsis_nn_install():
45+
import executorch.backends.cortex_m.library.cmsis_nn as cmsis_nn
46+
47+
buf_size = cmsis_nn.convolve_wrapper_buffer_size(
48+
cmsis_nn.Backend.MVE,
49+
cmsis_nn.DataType.A8W8,
50+
input_nhwc=[1, 8, 8, 16],
51+
filter_nhwc=[8, 3, 3, 16],
52+
output_nhwc=[1, 6, 6, 8],
53+
padding_hw=[0, 0],
54+
stride_hw=[1, 1],
55+
dilation_hw=[1, 1],
56+
)
57+
58+
assert buf_size == 576
59+
60+
4361
def run_tests(model_tests: List[ModelTest]) -> None:
4462
# Test that we can import the portable_lib module - verifies RPATH is correct
4563
print("Testing portable_lib import...")

.ci/scripts/wheel/test_linux.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#!/usr/bin/env python
22
# Copyright (c) Meta Platforms, Inc. and affiliates.
33
# All rights reserved.
4+
# Copyright 2026 Arm Limited and/or its affiliates.
45
#
56
# This source code is licensed under the BSD-style license found in the
67
# LICENSE file in the root directory of this source tree.
@@ -38,6 +39,8 @@
3839
else:
3940
print("⚠ VulkanBackend not registered (expected for the default wheel)")
4041

42+
test_base.test_cmsis_nn_install()
43+
4144
test_base.run_tests(
4245
model_tests=[
4346
test_base.ModelTest(

.ci/scripts/wheel/test_macos.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#!/usr/bin/env python
22
# Copyright (c) Meta Platforms, Inc. and affiliates.
33
# All rights reserved.
4+
# Copyright 2026 Arm Limited and/or its affiliates.
45
#
56
# This source code is licensed under the BSD-style license found in the
67
# LICENSE file in the root directory of this source tree.
@@ -9,6 +10,8 @@
910
from examples.models import Backend, Model
1011

1112
if __name__ == "__main__":
13+
test_base.test_cmsis_nn_install()
14+
1215
test_base.run_tests(
1316
model_tests=[
1417
test_base.ModelTest(

.ci/scripts/wheel/test_windows.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
#!/usr/bin/env python
22
# Copyright (c) Meta Platforms, Inc. and affiliates.
33
# All rights reserved.
4+
# Copyright 2026 Arm Limited and/or its affiliates.
45
#
56
# This source code is licensed under the BSD-style license found in the
67
# LICENSE file in the root directory of this source tree.
78

89
import platform
910
from typing import List
1011

12+
import test_base
13+
1114
import torch
1215
from executorch.backends.xnnpack.partition.xnnpack_partitioner import XnnpackPartitioner
1316
from executorch.examples.models import Backend, Model, MODEL_NAME_TO_MODEL
@@ -74,6 +77,8 @@ def run_tests(model_tests: List[ModelTest]) -> None:
7477
else:
7578
print("⚠ VulkanBackend not registered (expected for the default wheel)")
7679

80+
test_base.test_cmsis_nn_install()
81+
7782
run_tests(
7883
model_tests=[
7984
ModelTest(

CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -755,7 +755,7 @@ if(EXECUTORCH_BUILD_XNNPACK)
755755
list(APPEND _executorch_backends xnnpack_backend)
756756
endif()
757757

758-
if(EXECUTORCH_BUILD_CORTEX_M)
758+
if(EXECUTORCH_BUILD_CORTEX_M OR EXECUTORCH_BUILD_CMSIS_NN_PYBINDS)
759759
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/backends/cortex_m)
760760
endif()
761761

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}")

backends/arm/quantizer/quantization_annotator.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -859,6 +859,7 @@ def any_or_hardtanh_min_zero(n: Node):
859859
)
860860
elif node.target in (
861861
torch.ops.aten.cat.default,
862+
torch.ops.aten.concat.default,
862863
torch.ops.aten.concatenate.default,
863864
torch.ops.aten.stack.default,
864865
):
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)

0 commit comments

Comments
 (0)