66import numpy as np
77import pytest
88import torch
9-
109from executorch .backends .nxp .backend .edge_program_converter import (
1110 EdgeProgramToIRConverter ,
1211)
13- from executorch .backends .nxp .tests .executorch_pipeline import to_quantized_edge_program
12+ from executorch .backends .nxp .backend .ir .converter .builder .aten_model_builder_director import (
13+ AtenModelBuilderDirector ,
14+ )
15+ from executorch .backends .nxp .backend .ir .lib .tflite .BuiltinOperator import (
16+ BuiltinOperator as Ops ,
17+ )
18+ from executorch .backends .nxp .tests .executorch_pipeline import (
19+ ModelInputSpec ,
20+ to_quantized_edge_program ,
21+ )
1422from executorch .backends .nxp .tests .executors import (
1523 convert_run_compare ,
1624 graph_contains_any_of_ops ,
1725)
18- from executorch .exir .dialects ._ops import ops as exir_ops
26+ from executorch .backends .nxp .tests .graph_verifier import DetailedGraphVerifier
27+ from executorch .backends .nxp .tests .model_output_comparator import (
28+ NumericalStatsOutputComparator ,
29+ )
30+ from executorch .backends .nxp .tests .nsys_testing import lower_run_compare
31+ from executorch .backends .nxp .tests .ops_aliases import (
32+ AddTensor ,
33+ Clamp ,
34+ ExecutorchDelegateCall ,
35+ )
36+ from executorch .backends .nxp .tests .use_qat import * # noqa: F403 F401
1937
2038
2139@pytest .fixture (autouse = True )
@@ -24,11 +42,6 @@ def reseed_model_per_test_run():
2442 np .random .seed (23 )
2543
2644
27- # noinspection PyProtectedMember
28- ExecutorchDelegateCall = torch .ops .higher_order .executorch_call_delegate
29- Clamp = exir_ops .edge .aten .clamp .default
30-
31-
3245class ClampModule (torch .nn .Module ):
3346
3447 # noinspection PyShadowingBuiltins
@@ -180,3 +193,138 @@ def test_convert_clamp__no_delegation__unsupported_bounds(min, max):
180193
181194 # Make sure the `clamp` was NOT delegated.
182195 assert graph_contains_any_of_ops (delegated_ep .graph , [Clamp ])
196+
197+
198+ class TestClampNewNeutronFlow :
199+ @pytest .mark .parametrize (
200+ "min, max" ,
201+ [
202+ pytest .param (- 1 , 2 , id = "min = -1, max = 2 (Max/Min)" ),
203+ pytest .param (None , 1 , id = "min = None, max = 1 (Max/Min)" ),
204+ pytest .param (1 , None , id = "min = 1, max = None (Max/Min)" ),
205+ pytest .param (0 , 2 , id = "min = 0, max = 2 (Max/Min)" ),
206+ pytest .param (0 , 1 , id = "min = 0, max = 1 (Relu0To1)" ),
207+ pytest .param (- 1 , 1 , id = "min = -1, max = 1 (ReluN1To1)" ),
208+ pytest .param (0 , None , id = "min = 0, max = None (Relu)" ),
209+ # Float bounds
210+ pytest .param (- 1.0 , 2.0 , id = "min = -1.0, max = 2.0 (Max/Min)" ),
211+ pytest .param (None , 1.0 , id = "min = None, max = 1.0 (Max/Min)" ),
212+ pytest .param (1.0 , None , id = "min = 1.0, max = None (Max/Min)" ),
213+ pytest .param (1.0 , float ("inf" ), id = "min = 1.0, max = infinity (Max/Min)" ),
214+ pytest .param (- float ("inf" ), 1.0 , id = "min = infinity, max = 1.0 (Max/Min)" ),
215+ pytest .param (0.1 , 0.5 , id = "min = 0.1, max = 0.5 (Max/Min)" ),
216+ pytest .param (0.0 , 1.0 , id = "min = 0.0, max = 1.0 (Relu0To1)" ),
217+ pytest .param (- 1.0 , 1.0 , id = "min = -1.0, max = 1.0 (ReluN1To1)" ),
218+ pytest .param (0.0 , None , id = "min = 0, max = None (Relu)" ),
219+ ],
220+ )
221+ def test_convert_clamp__full_pipeline (self , mocker , min , max , use_qat ):
222+ input_shape = (2 , 7 , 2 ) # Indivisible by num_macs
223+ model = AddClampModule (min , max )
224+
225+ x_input_spec = ModelInputSpec (input_shape )
226+ comparator = NumericalStatsOutputComparator ()
227+ graph_verifier = DetailedGraphVerifier (
228+ mocker ,
229+ expected_delegated_ops = {
230+ AddTensor : 1 ,
231+ Clamp : 1 ,
232+ },
233+ expected_non_delegated_ops = {},
234+ )
235+
236+ lower_run_compare (
237+ model = model ,
238+ input_spec = [x_input_spec ],
239+ dlg_model_verifier = graph_verifier ,
240+ output_comparator = comparator ,
241+ use_new_flow_neutron_c = True ,
242+ use_qat = use_qat ,
243+ )
244+
245+ @pytest .mark .parametrize (
246+ "min, max" ,
247+ [
248+ pytest .param (
249+ float ("inf" ), float ("inf" ), id = "min = inf, max = inf (invalid)"
250+ ),
251+ pytest .param (None , float ("inf" ), id = "min = None, max = inf (invalid)" ),
252+ pytest .param (float ("inf" ), None , id = "min = inf, max = None (invalid)" ),
253+ ],
254+ )
255+ def test_convert_clamp__invalid_bounds (self , min , max ):
256+ input_shape = (2 , 7 , 2 )
257+ model = ClampModule (min , max )
258+
259+ delegated_ep = to_quantized_edge_program (model , input_shape ).exported_program ()
260+
261+ # Make sure the `clamp` was NOT delegated.
262+ assert graph_contains_any_of_ops (delegated_ep .graph , [Clamp ])
263+
264+ # noinspection PyShadowingBuiltins
265+ @pytest .mark .parametrize (
266+ "min, max, expected_tflite_ops" ,
267+ [
268+ pytest .param (
269+ 0.1 ,
270+ 0.5 ,
271+ [Ops .ADD , Ops .MAXIMUM , Ops .MINIMUM ],
272+ id = "min = 0.1, max = 0.5 (Max/Min)" ,
273+ ),
274+ pytest .param (
275+ 0.0 , 1.0 , [Ops .ADD , Ops .RELU_0_TO_1 ], id = "min = 0, max = 1 (Relu0To1)"
276+ ),
277+ pytest .param (
278+ - 1.0 ,
279+ 1.0 ,
280+ [Ops .ADD , Ops .RELU_N1_TO_1 ],
281+ id = "min = -1, max = 1 (ReluN1To1)" ,
282+ ),
283+ pytest .param (
284+ 0.0 , None , [Ops .ADD , Ops .RELU ], id = "min = 0, max = None (Relu)"
285+ ),
286+ pytest .param (
287+ 0.0 ,
288+ float ("inf" ),
289+ [Ops .ADD , Ops .RELU ],
290+ id = "min = 0, max = infinity (Relu)" ,
291+ ),
292+ ],
293+ )
294+ def test_convert_clamp__relu_vs_maxmin (self , mocker , min , max , expected_tflite_ops ):
295+ input_shape = (23 ,)
296+ model = AddClampModule (min , max )
297+
298+ converter_spy = mocker .spy (EdgeProgramToIRConverter , "convert_program" )
299+ tflite_spy = mocker .spy (AtenModelBuilderDirector , "finish" )
300+
301+ delegated_ep = to_quantized_edge_program (
302+ model ,
303+ input_shape ,
304+ use_new_flow_neutron_c = True ,
305+ ).exported_program ()
306+
307+ # Make sure the `clamp` was delegated.
308+ assert graph_contains_any_of_ops (delegated_ep .graph , [ExecutorchDelegateCall ])
309+ assert not graph_contains_any_of_ops (delegated_ep .graph , [Clamp ])
310+
311+ intermediate_ep = converter_spy .call_args .args [1 ]
312+ quant_node = list (intermediate_ep .graph .nodes )[- 2 ]
313+ dequant_node = list (intermediate_ep .graph .nodes )[- 4 ]
314+ tflite_internal_ops = [
315+ op .builtin_code for op in tflite_spy .spy_return .operator_codes .vector
316+ ]
317+
318+ assert graph_contains_any_of_ops (intermediate_ep .graph , [Clamp ])
319+ assert len (tflite_internal_ops ) == len (expected_tflite_ops ) + 1 # Transpose
320+ assert all (op in tflite_internal_ops for op in expected_tflite_ops )
321+
322+ if len (expected_tflite_ops ) == 3 :
323+ # Min/Max variant should have same input and output quantization
324+ assert all (
325+ q == dq for q , dq in zip (quant_node .args [1 :], dequant_node .args [1 :])
326+ )
327+ else :
328+ assert not all (
329+ q == dq for q , dq in zip (quant_node .args [1 :], dequant_node .args [1 :])
330+ )
0 commit comments