Skip to content

Commit 1795dff

Browse files
Erik-LundellAdrianLundell
authored andcommitted
Arm unittest refactor of Add and Conv test cases
This is a showcase of multiple improvements which can be done across all tests: 1. Move pipeline definition from tests to a general (flexible) pipeline class - Define a default step of stages using e.g. TosaPipelineBI() - Add custom config or debug stages using helper functions s.a. .change_args(), .add_stage(), .dump() etc. - Run the full pipeline using .run() 2. Move towards a pure pytest approach to remove dependencies on unittest and parametrize 3. Separate tests running on FVP from tests not running on FVP rather than configuring this from the command line - FVP tests are skipped if not installed - To filter out tests one may instead use pytest markers/name filtering - This should give a clearer picture of what has been tested 4. Introduces one favored way of marking tests as xfails, in the parameterize decorator Change-Id: Ieb88209fd7bcbb4b700bb970d6877ca6785752a4
1 parent a29dc49 commit 1795dff

File tree

5 files changed

+635
-291
lines changed

5 files changed

+635
-291
lines changed

backends/arm/test/common.py

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2024 Arm Limited and/or its affiliates.
1+
# Copyright 2024-2025 Arm Limited and/or its affiliates.
22
# All rights reserved.
33
#
44
# This source code is licensed under the BSD-style license found in the
@@ -9,12 +9,16 @@
99

1010
import tempfile
1111
from datetime import datetime
12+
1213
from pathlib import Path
14+
from typing import Any
1315

16+
import pytest
1417
from executorch.backends.arm.arm_backend import ArmCompileSpecBuilder
1518

1619
from executorch.backends.arm.test.conftest import is_option_enabled
1720
from executorch.exir.backend.compile_spec_schema import CompileSpec
21+
from runner_utils import corstone300_installed, corstone320_installed
1822

1923

2024
def get_time_formatted_path(path: str, log_prefix: str) -> str:
@@ -185,3 +189,41 @@ def get_target_board(compile_spec: list[CompileSpec]) -> str | None:
185189
elif "u85" in flags:
186190
return "corstone-320"
187191
return None
192+
193+
194+
u55_fvp_mark = pytest.mark.skipif(
195+
not corstone300_installed(), reason="Did not find Corstone-300 FVP on path"
196+
)
197+
""" Marks a test as running on Ethos-U55 FVP, e.g. Corstone 300. Skips the test if this is not installed."""
198+
199+
u85_fvp_mark = pytest.mark.skipif(
200+
not corstone320_installed(), reason="Did not find Corstone-320 FVP on path"
201+
)
202+
""" Marks a test as running on Ethos-U85 FVP, e.g. Corstone 320. Skips the test if this is not installed."""
203+
204+
205+
def parametrize(
206+
arg_name: str, test_data: dict[str, Any], xfails: dict[str, str] = None
207+
):
208+
"""
209+
Custom version of pytest.mark.parametrize with some syntatic sugar and added xfail functionality
210+
- test_data is expected as a dict of (id, test_data) pairs
211+
- alllows to specifiy a dict of (id, failure_reason) pairs to mark specific tests as xfail
212+
"""
213+
if xfails is None:
214+
xfails = {}
215+
216+
def decorator_func(func):
217+
pytest_testsuite = []
218+
for id, test_parameters in test_data.items():
219+
if id in xfails:
220+
pytest_param = pytest.param(
221+
test_parameters, id=id, marks=pytest.mark.xfail(reason=xfails[id])
222+
)
223+
else:
224+
pytest_param = pytest.param(test_parameters, id=id)
225+
pytest_testsuite.append(pytest_param)
226+
227+
return pytest.mark.parametrize(arg_name, pytest_testsuite)(func)
228+
229+
return decorator_func

backends/arm/test/ops/test_add.py

Lines changed: 132 additions & 166 deletions
Original file line numberDiff line numberDiff line change
@@ -1,178 +1,144 @@
11
# Copyright (c) Meta Platforms, Inc. and affiliates.
2-
# Copyright 2024 Arm Limited and/or its affiliates.
2+
# Copyright 2024-2025 Arm Limited and/or its affiliates.
33
# All rights reserved.
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.
77

8-
import unittest
98

109
from typing import Tuple
1110

12-
import pytest
1311
import torch
14-
from executorch.backends.arm.test import common, conftest
15-
from executorch.backends.arm.test.tester.arm_tester import ArmTester
16-
from executorch.exir import EdgeCompileConfig
17-
from executorch.exir.backend.compile_spec_schema import CompileSpec
18-
from parameterized import parameterized
19-
20-
21-
class TestSimpleAdd(unittest.TestCase):
22-
"""Tests a single add op, x+x and x+y."""
23-
24-
class Add(torch.nn.Module):
25-
test_parameters = [
26-
(torch.FloatTensor([1, 2, 3, 5, 7]),),
27-
(3 * torch.ones(8),),
28-
(10 * torch.randn(8),),
29-
(torch.ones(1, 1, 4, 4),),
30-
(torch.ones(1, 3, 4, 2),),
31-
]
32-
33-
def forward(self, x):
34-
return x + x
35-
36-
class Add2(torch.nn.Module):
37-
test_parameters = [
38-
(
39-
torch.FloatTensor([1, 2, 3, 5, 7]),
40-
(torch.FloatTensor([2, 1, 2, 1, 10])),
41-
),
42-
(torch.ones(1, 10, 4, 6), torch.ones(1, 10, 4, 6)),
43-
(torch.randn(1, 1, 4, 4), torch.ones(1, 1, 4, 1)),
44-
(torch.randn(1, 3, 4, 4), torch.randn(1, 3, 4, 4)),
45-
(10000 * torch.randn(1, 1, 4, 4), torch.randn(1, 1, 4, 1)),
46-
]
47-
48-
def __init__(self):
49-
super().__init__()
50-
51-
def forward(self, x, y):
52-
return x + y
53-
54-
_edge_compile_config: EdgeCompileConfig = EdgeCompileConfig(
55-
_skip_dim_order=True, # TODO(T182928844): Delegate dim order op to backend.
12+
from executorch.backends.arm.test import common
13+
from executorch.backends.arm.test.tester.test_pipeline import (
14+
EthosU55PipelineBI,
15+
EthosU85PipelineBI,
16+
TosaPipelineBI,
17+
TosaPipelineMI,
18+
)
19+
20+
aten_op = "torch.ops.aten.add.Tensor"
21+
exir_op = "executorch_exir_dialects_edge__ops_aten_add_Tensor"
22+
23+
test_data = {
24+
"5d_float": (torch.FloatTensor([1, 2, 3, 5, 7]),),
25+
"1d_ones": ((3 * torch.ones(8),)),
26+
"1d_randn": (10 * torch.randn(8),),
27+
"4d_ones_1": (torch.ones(1, 1, 4, 4),),
28+
"4d_ones_2": (torch.ones(1, 3, 4, 2),),
29+
}
30+
T1 = Tuple[torch.Tensor]
31+
32+
test_data2 = {
33+
"5d_float": (
34+
torch.FloatTensor([1, 2, 3, 5, 7]),
35+
(torch.FloatTensor([2, 1, 2, 1, 10])),
36+
),
37+
"4d_ones": (torch.ones(1, 10, 4, 6), torch.ones(1, 10, 4, 6)),
38+
"4d_randn_1": (torch.randn(1, 1, 4, 4), torch.ones(1, 1, 4, 1)),
39+
"4d_randn_2": (torch.randn(1, 3, 4, 4), torch.randn(1, 3, 4, 4)),
40+
"4d_randn_big": (10000 * torch.randn(1, 1, 4, 4), torch.randn(1, 1, 4, 1)),
41+
}
42+
T2 = Tuple[torch.Tensor, torch.Tensor]
43+
44+
45+
class Add(torch.nn.Module):
46+
def forward(self, x):
47+
return x + x
48+
49+
50+
@common.parametrize("test_data", test_data)
51+
def test_add_tosa_MI(test_data):
52+
pipeline = TosaPipelineMI[T1](Add(), test_data, aten_op, exir_op)
53+
pipeline.run()
54+
55+
56+
@common.parametrize("test_data", test_data)
57+
def test_add_tosa_BI(test_data):
58+
pipeline = TosaPipelineBI[T1](Add(), test_data, aten_op, exir_op)
59+
pipeline.run()
60+
61+
62+
@common.parametrize("test_data", test_data)
63+
def test_add_u55_BI(test_data):
64+
pipeline = EthosU55PipelineBI[T1](
65+
Add(), test_data, aten_op, exir_op, run_on_fvp=False
5666
)
67+
pipeline.run()
5768

58-
def _test_add_tosa_MI_pipeline(
59-
self, module: torch.nn.Module, test_data: Tuple[torch.Tensor]
60-
):
61-
(
62-
ArmTester(
63-
module,
64-
example_inputs=test_data,
65-
compile_spec=common.get_tosa_compile_spec("TOSA-0.80+MI"),
66-
)
67-
.export()
68-
.check_count({"torch.ops.aten.add.Tensor": 1})
69-
.check_not(["torch.ops.quantized_decomposed"])
70-
.to_edge(config=self._edge_compile_config)
71-
.partition()
72-
.check_count({"torch.ops.higher_order.executorch_call_delegate": 1})
73-
.to_executorch()
74-
.run_method_and_compare_outputs(inputs=test_data)
75-
)
76-
77-
def _test_add_tosa_BI_pipeline(
78-
self, module: torch.nn.Module, test_data: Tuple[torch.Tensor]
79-
):
80-
(
81-
ArmTester(
82-
module,
83-
example_inputs=test_data,
84-
compile_spec=common.get_tosa_compile_spec("TOSA-0.80+BI"),
85-
)
86-
.quantize()
87-
.export()
88-
.check_count({"torch.ops.aten.add.Tensor": 1})
89-
.check(["torch.ops.quantized_decomposed"])
90-
.to_edge(config=self._edge_compile_config)
91-
.partition()
92-
.check_count({"torch.ops.higher_order.executorch_call_delegate": 1})
93-
.to_executorch()
94-
.run_method_and_compare_outputs(inputs=test_data, qtol=1)
95-
)
96-
97-
def _test_add_ethos_BI_pipeline(
98-
self,
99-
module: torch.nn.Module,
100-
compile_spec: CompileSpec,
101-
test_data: Tuple[torch.Tensor],
102-
):
103-
tester = (
104-
ArmTester(
105-
module,
106-
example_inputs=test_data,
107-
compile_spec=compile_spec,
108-
)
109-
.quantize()
110-
.export()
111-
.check_count({"torch.ops.aten.add.Tensor": 1})
112-
.check(["torch.ops.quantized_decomposed"])
113-
.to_edge()
114-
.partition()
115-
.check_count({"torch.ops.higher_order.executorch_call_delegate": 1})
116-
.to_executorch()
117-
.serialize()
118-
)
119-
if conftest.is_option_enabled("corstone_fvp"):
120-
tester.run_method_and_compare_outputs(qtol=1, inputs=test_data)
121-
122-
return tester
123-
124-
@parameterized.expand(Add.test_parameters)
125-
def test_add_tosa_MI(self, test_data: torch.Tensor):
126-
test_data = (test_data,)
127-
self._test_add_tosa_MI_pipeline(self.Add(), test_data)
128-
129-
@parameterized.expand(Add.test_parameters)
130-
def test_add_tosa_BI(self, test_data: torch.Tensor):
131-
test_data = (test_data,)
132-
self._test_add_tosa_BI_pipeline(self.Add(), test_data)
133-
134-
@parameterized.expand(Add.test_parameters)
135-
@pytest.mark.corstone_fvp
136-
def test_add_u55_BI(self, test_data: torch.Tensor):
137-
test_data = (test_data,)
138-
self._test_add_ethos_BI_pipeline(
139-
self.Add(),
140-
common.get_u55_compile_spec(permute_memory_to_nhwc=True),
141-
test_data,
142-
)
143-
144-
@parameterized.expand(Add.test_parameters)
145-
@pytest.mark.corstone_fvp
146-
def test_add_u85_BI(self, test_data: torch.Tensor):
147-
test_data = (test_data,)
148-
self._test_add_ethos_BI_pipeline(
149-
self.Add(),
150-
common.get_u85_compile_spec(permute_memory_to_nhwc=True),
151-
test_data,
152-
)
153-
154-
@parameterized.expand(Add2.test_parameters)
155-
def test_add2_tosa_MI(self, operand1: torch.Tensor, operand2: torch.Tensor):
156-
test_data = (operand1, operand2)
157-
self._test_add_tosa_MI_pipeline(self.Add2(), test_data)
158-
159-
@parameterized.expand(Add2.test_parameters)
160-
def test_add2_tosa_BI(self, operand1: torch.Tensor, operand2: torch.Tensor):
161-
test_data = (operand1, operand2)
162-
self._test_add_tosa_BI_pipeline(self.Add2(), test_data)
163-
164-
@parameterized.expand(Add2.test_parameters)
165-
@pytest.mark.corstone_fvp
166-
def test_add2_u55_BI(self, operand1: torch.Tensor, operand2: torch.Tensor):
167-
test_data = (operand1, operand2)
168-
self._test_add_ethos_BI_pipeline(
169-
self.Add2(), common.get_u55_compile_spec(), test_data
170-
)
171-
172-
@parameterized.expand(Add2.test_parameters)
173-
@pytest.mark.corstone_fvp
174-
def test_add2_u85_BI(self, operand1: torch.Tensor, operand2: torch.Tensor):
175-
test_data = (operand1, operand2)
176-
self._test_add_ethos_BI_pipeline(
177-
self.Add2(), common.get_u85_compile_spec(), test_data
178-
)
69+
70+
@common.parametrize("test_data", test_data)
71+
def test_add_u85_BI(test_data):
72+
pipeline = EthosU85PipelineBI[T1](
73+
Add(), test_data, aten_op, exir_op, run_on_fvp=False
74+
)
75+
pipeline.run()
76+
77+
78+
@common.parametrize("test_data", test_data)
79+
@common.u55_fvp_mark
80+
def test_add_u55_BI_on_fvp(test_data):
81+
pipeline = EthosU55PipelineBI[T1](
82+
Add(), test_data, aten_op, exir_op, run_on_fvp=True
83+
)
84+
pipeline.run()
85+
86+
87+
@common.parametrize("test_data", test_data)
88+
@common.u85_fvp_mark
89+
def test_add_u85_BI_on_fvp(test_data):
90+
pipeline = EthosU85PipelineBI[T1](
91+
Add(), test_data, aten_op, exir_op, run_on_fvp=True
92+
)
93+
pipeline.run()
94+
95+
96+
class Add2(torch.nn.Module):
97+
def forward(self, x, y):
98+
return x + y
99+
100+
101+
@common.parametrize("test_data", test_data2)
102+
def test_add2_tosa_MI(test_data):
103+
pipeline = TosaPipelineMI[T2](Add2(), test_data, aten_op, exir_op)
104+
pipeline.run()
105+
106+
107+
@common.parametrize("test_data", test_data2)
108+
def test_add2_tosa_BI(test_data):
109+
pipeline = TosaPipelineBI[T2](Add2(), test_data, aten_op, exir_op)
110+
pipeline.run()
111+
112+
113+
@common.parametrize("test_data", test_data2)
114+
def test_add2_u55_BI(test_data):
115+
pipeline = EthosU55PipelineBI[T2](
116+
Add2(), test_data, aten_op, exir_op, run_on_fvp=False
117+
)
118+
pipeline.run()
119+
120+
121+
@common.parametrize("test_data", test_data2)
122+
@common.u55_fvp_mark
123+
def test_add2_u55_BI_on_fvp(test_data):
124+
pipeline = EthosU55PipelineBI[T2](
125+
Add2(), test_data, aten_op, exir_op, run_on_fvp=True
126+
)
127+
pipeline.run()
128+
129+
130+
@common.parametrize("test_data", test_data2)
131+
def test_add2_u85_BI(test_data):
132+
pipeline = EthosU85PipelineBI[T2](
133+
Add2(), test_data, aten_op, exir_op, run_on_fvp=False
134+
)
135+
pipeline.run()
136+
137+
138+
@common.parametrize("test_data", test_data2)
139+
@common.u85_fvp_mark
140+
def test_add2_u85_BI_on_fvp(test_data):
141+
pipeline = EthosU85PipelineBI[T2](
142+
Add2(), test_data, aten_op, exir_op, run_on_fvp=True
143+
)
144+
pipeline.run()

0 commit comments

Comments
 (0)