Skip to content

Commit d2cffec

Browse files
authored
Arm backend: Add Arm FACTO nightly harness (pytorch#23149)
This patch extends the shared FACTO operator test harness with Arm backend coverage. The existing FACTO harness is updated with backend extension hooks. These hooks let backend integrations patch generated specs, skip unsupported generated cases, customise delegated execution, and choose whether FACTO failures should fail the test. It also adds FACTO_OPS and FACTO_MAX_CASES environment controls so nightly jobs can limit operator and testcase scope without changing the test source. Add an Arm-specific FACTO test module that reuses the shared harness. The new Arm classes cover TOSA FP and TOSA INT flows. The INT flow quantises before export, while both Arm flows compare at the to_executorch stage instead of serialising and trying to run a board executable. This provides a scalable nightly FACTO path for Arm operator exploration without duplicating the FACTO dependency or adding generated cases directly to every Arm op test file. cc @digantdesai @freddan80 @per @zingo @oscarandersson8218 @mansnils @Sebastian-Larsson @robell @rascani Signed-off-by: Baris Demir <baris.demir@arm.com>
1 parent becae5a commit d2cffec

2 files changed

Lines changed: 313 additions & 28 deletions

File tree

‎backends/test/facto/test_facto.py‎

Lines changed: 118 additions & 28 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.
@@ -12,22 +13,35 @@
1213
# clone and install FACTO by running pip install . from the FACTO source
1314
# directory. Then, from the executorch root directory, run the following:
1415
#
15-
# python -m unittest backends.test.operators.test_facto.FactoTestsXNNPACK
16+
# python -m unittest backends.test.facto.test_facto.FactoTestsXNNPACK
17+
#
18+
# Useful environment variables:
19+
# FACTO_OPS="abs.default,acos.default" limits generated tests to selected ops.
20+
# FACTO_MAX_CASES=10 limits the number of generated cases per op.
1621
#
1722

1823
import copy
24+
import fnmatch
1925
import functools
26+
import os
2027
import traceback
2128
import unittest
2229
from typing import Any, Callable, Sequence
2330

2431
import torch
25-
from executorch.backends.test.harness.tester import Tester as TesterBase
32+
from executorch.backends.test.harness.tester import Tester as BackendTester
2633
from executorch.backends.xnnpack.test.tester.tester import Tester as XnnpackTester
27-
from facto.inputgen.argtuple.gen import ArgumentTupleGenerator
28-
from facto.inputgen.specs.model import ConstraintProducer as cp, Spec
29-
from facto.inputgen.utils.random_manager import random_manager
30-
from facto.specdb.db import SpecDictDB
34+
35+
try:
36+
from facto.inputgen.argtuple.gen import ArgumentTupleGenerator
37+
from facto.inputgen.specs.model import ConstraintProducer as cp, Spec
38+
from facto.inputgen.utils.random_manager import random_manager
39+
from facto.specdb.db import SpecDictDB
40+
except ImportError as exc:
41+
raise ImportError(
42+
"FACTO is required to run generated operator tests. Install it with "
43+
"`pip install -e backends/cadence/utils/FACTO`."
44+
) from exc
3145
from torch._ops import OpOverload
3246

3347
from .facto_specs import ExtraSpecDB
@@ -55,6 +69,30 @@
5569
}
5670

5771

72+
TesterFactory = Callable[[torch.nn.Module, tuple[Any, ...]], BackendTester]
73+
74+
75+
def _facto_max_cases() -> int | None:
76+
max_cases = os.environ.get("FACTO_MAX_CASES")
77+
if max_cases is None:
78+
return None
79+
return int(max_cases)
80+
81+
82+
def _selected_op_names() -> list[str]:
83+
op_patterns = os.environ.get("FACTO_OPS")
84+
if op_patterns is None or not op_patterns.strip():
85+
return list(CombinedSpecDB.keys())
86+
87+
patterns = [pattern.strip() for pattern in op_patterns.split(",")]
88+
patterns = [pattern for pattern in patterns if pattern]
89+
return [
90+
op_name
91+
for op_name in CombinedSpecDB.keys()
92+
if any(fnmatch.fnmatch(op_name, pattern) for pattern in patterns)
93+
]
94+
95+
5896
def _patch_spec(spec: Spec) -> Spec:
5997
spec = copy.deepcopy(spec)
6098
for inspec in spec.inspec:
@@ -139,12 +177,14 @@ def get_module_for_op(op: OpOverload):
139177

140178

141179
class FactoTestsBase(unittest.TestCase):
142-
def __init__(self, tester_factory: Callable[[], TesterBase], *args, **kwargs):
180+
__test__ = False
181+
182+
def __init__(self, tester_factory: TesterFactory, *args, **kwargs):
143183
super().__init__(*args, **kwargs)
144184
self._tester_factory = tester_factory
145185

146-
@staticmethod
147-
def _generate_test(op_name: str) -> None:
186+
@classmethod
187+
def _generate_test(cls, op_name: str) -> None:
148188
# Find the torch op with the given name.
149189
sections = op_name.split(".")
150190
torch_op = functools.reduce(getattr, sections, torch.ops.aten)
@@ -154,7 +194,7 @@ def _generate_test(op_name: str) -> None:
154194
def test_body(self):
155195
self._test_op(torch_op)
156196

157-
setattr(FactoTestsBase, test_name, test_body)
197+
setattr(cls, test_name, test_body)
158198

159199
@staticmethod
160200
def get_runtime_input_count(spec: Spec):
@@ -180,6 +220,27 @@ def get_runtime_input_count(spec: Spec):
180220
def setUp(self):
181221
torch.set_printoptions(threshold=3)
182222

223+
def _patch_spec_for_backend(self, spec: Spec, op_name: str) -> Spec:
224+
return spec
225+
226+
def _should_skip_case(self, posargs: Sequence[Any]) -> bool:
227+
return False
228+
229+
def _run_delegated_case(self, tester: BackendTester) -> None:
230+
tester.to_executorch().serialize().run_method_and_compare_outputs()
231+
232+
def _should_fail_on_failures(self) -> bool:
233+
return False
234+
235+
def _count_as_test_failures(
236+
self,
237+
*,
238+
eager_fail_count: int,
239+
export_fail_count: int,
240+
fail_count: int,
241+
) -> int:
242+
return fail_count
243+
183244
def _test_op(self, op: OpOverload) -> None: # noqa: C901
184245
random_manager.seed(0)
185246

@@ -194,27 +255,29 @@ def _test_op(self, op: OpOverload) -> None: # noqa: C901
194255
if op_name not in CombinedSpecDB:
195256
raise ValueError(f"Operator {op_name} not found in SpecDictDB.")
196257
spec = _patch_spec(CombinedSpecDB[op_name])
258+
spec = self._patch_spec_for_backend(spec, op_name)
197259

198260
runtime_input_count = FactoTestsBase.get_runtime_input_count(spec)
199261

200262
print(f"Op: {op_name}, {runtime_input_count} runtime inputs")
201263

202264
# Run test cases
265+
eager_fail_count = 0
266+
export_fail_count = 0
203267
success_count_delegated = 0
204268
success_count_undelegated = 0
205269
fail_count = 0
206270

207-
i = 0
208-
for posargs, inkwargs, _ in ArgumentTupleGenerator(spec).gen():
209-
i += 1
271+
max_cases = _facto_max_cases()
272+
for case_index, (posargs, inkwargs, _) in enumerate(
273+
ArgumentTupleGenerator(spec).gen()
274+
):
275+
if max_cases is not None and case_index >= max_cases:
276+
break
210277

211278
try:
212-
if isinstance(posargs[0], torch.Tensor):
213-
# Temporary for getting around XNN crashes (https://github.com/pytorch/executorch/issues/10960).
214-
# TODO Re-enable when resolved.
215-
if posargs[0].dtype in {torch.int8, torch.uint8}:
216-
print("Skipping (u)int8 case.")
217-
continue
279+
if self._should_skip_case(posargs):
280+
continue
218281

219282
module_cls = get_module_for_op(op)
220283
model = module_cls(
@@ -226,6 +289,7 @@ def _test_op(self, op: OpOverload) -> None: # noqa: C901
226289
try:
227290
model(*posargs[:runtime_input_count])
228291
except Exception as e:
292+
eager_fail_count += 1
229293
print(f"Eager execution failed: {e}")
230294
continue
231295

@@ -237,6 +301,7 @@ def _test_op(self, op: OpOverload) -> None: # noqa: C901
237301
try:
238302
tester.export()
239303
except Exception:
304+
export_fail_count += 1
240305
print("Export failed.")
241306
continue
242307

@@ -250,11 +315,7 @@ def _test_op(self, op: OpOverload) -> None: # noqa: C901
250315

251316
# Only run the runtime test if the op was delegated.
252317
if is_delegated:
253-
(
254-
tester.to_executorch()
255-
.serialize()
256-
.run_method_and_compare_outputs()
257-
)
318+
self._run_delegated_case(tester)
258319

259320
if is_delegated:
260321
success_count_delegated += 1
@@ -277,25 +338,54 @@ def _test_op(self, op: OpOverload) -> None: # noqa: C901
277338
print(
278339
f" {success_count_delegated} DELEGATED, {success_count_undelegated} UNDELEGATED"
279340
)
341+
print(f" {eager_fail_count} EAGER_FAILED, {export_fail_count} EXPORT_FAILED")
280342

281-
282-
# Programatically generate tests for each operator.
283-
for op_name in CombinedSpecDB.keys():
284-
FactoTestsBase._generate_test(op_name)
343+
counted_failures = self._count_as_test_failures(
344+
eager_fail_count=eager_fail_count,
345+
export_fail_count=export_fail_count,
346+
fail_count=fail_count,
347+
)
348+
if counted_failures and self._should_fail_on_failures():
349+
self.fail(
350+
f"{op_name}: {counted_failures} FACTO-generated case(s) failed "
351+
f"(runtime={fail_count}, export={export_fail_count}, "
352+
f"eager={eager_fail_count})."
353+
)
285354

286355

287356
# TODO Figure out where to put these
288357
class FactoTestsXNNPACK(FactoTestsBase):
358+
__test__ = True
359+
289360
def __init__(self, *args, **kwargs):
290361
super().__init__(XnnpackTester, *args, **kwargs)
291362

363+
def _should_skip_case(self, posargs: Sequence[Any]) -> bool:
364+
if isinstance(posargs[0], torch.Tensor):
365+
# Temporary for getting around XNN crashes
366+
# (https://github.com/pytorch/executorch/issues/10960).
367+
# TODO Re-enable when resolved.
368+
if posargs[0].dtype in {torch.int8, torch.uint8}:
369+
print("Skipping (u)int8 case.")
370+
return True
371+
return False
372+
373+
374+
for op_name in _selected_op_names():
375+
FactoTestsXNNPACK._generate_test(op_name)
376+
292377

293378
try:
294379
from executorch.backends.apple.coreml.test.tester import CoreMLTester
295380

296381
class FactoTestsCoreML(FactoTestsBase):
382+
__test__ = True
383+
297384
def __init__(self, *args, **kwargs):
298385
super().__init__(CoreMLTester, *args, **kwargs)
299386

387+
for op_name in _selected_op_names():
388+
FactoTestsCoreML._generate_test(op_name)
389+
300390
except:
301391
print("Skipping Core ML facto tests as Core ML AOT is not available.")

0 commit comments

Comments
 (0)