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)
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+ )
1836from executorch .exir .dialects ._ops import ops as exir_ops
37+ from executorch .backends .nxp .tests .use_qat import * # noqa: F403
1938
2039
2140@pytest .fixture (autouse = True )
@@ -24,11 +43,6 @@ def reseed_model_per_test_run():
2443 np .random .seed (23 )
2544
2645
27- # noinspection PyProtectedMember
28- ExecutorchDelegateCall = torch .ops .higher_order .executorch_call_delegate
29- Clamp = exir_ops .edge .aten .clamp .default
30-
31-
3246class ClampModule (torch .nn .Module ):
3347
3448 # noinspection PyShadowingBuiltins
@@ -180,3 +194,111 @@ def test_convert_clamp__no_delegation__unsupported_bounds(min, max):
180194
181195 # Make sure the `clamp` was NOT delegated.
182196 assert graph_contains_any_of_ops (delegated_ep .graph , [Clamp ])
197+
198+
199+ class TestClampNewNeutronFlow :
200+ @pytest .mark .parametrize (
201+ "min, max" ,
202+ [
203+ pytest .param (- 1 , 2 , id = "min = -1, max = 2 (Max/Min)" ),
204+ pytest .param (None , 1 , id = "min = None, max = 1 (Max/Min)" ),
205+ pytest .param (1 , None , id = "min = 1, max = None (Max/Min)" ),
206+ pytest .param (0 , 2 , id = "min = 0, max = 2 (Max/Min)" ),
207+ pytest .param (0 , 1 , id = "min = 0, max = 1 (Relu0To1)" ),
208+ pytest .param (- 1 , 1 , id = "min = -1, max = 1 (ReluN1To1)" ),
209+ pytest .param (0 , None , id = "min = 0, max = None (Relu)" ),
210+ # # Float bounds
211+ pytest .param (- 1.0 , 2.0 , id = "min = -1.0, max = 2.0 (Max/Min)" ),
212+ pytest .param (None , 1.0 , id = "min = None, max = 1.0 (Max/Min)" ),
213+ pytest .param (1.0 , None , id = "min = 1.0, max = None (Max/Min)" ),
214+ pytest .param (0.1 , 0.5 , id = "min = 0.1, max = 0.5 (Max/Min)" ),
215+ pytest .param (0.0 , 1.0 , id = "min = 0.0, max = 1.0 (Relu0To1)" ),
216+ pytest .param (- 1.0 , 1.0 , id = "min = -1.0, max = 1.0 (ReluN1To1)" ),
217+ pytest .param (0.0 , None , id = "min = 0, max = None (Relu)" ),
218+ ],
219+ )
220+ def test_convert_clamp__full_pipeline (self , mocker , min , max , use_qat ):
221+ input_shape = (2 , 7 , 2 ) # Indivisible by num_macs
222+ model = AddClampModule (min , max )
223+
224+ x_input_spec = ModelInputSpec (input_shape )
225+ comparator = NumericalStatsOutputComparator ()
226+ graph_verifier = DetailedGraphVerifier (
227+ mocker ,
228+ expected_delegated_ops = {
229+ AddTensor : 1 ,
230+ Clamp : 1 ,
231+ },
232+ expected_non_delegated_ops = {},
233+ )
234+
235+ lower_run_compare (
236+ model = model ,
237+ input_spec = [x_input_spec ],
238+ dlg_model_verifier = graph_verifier ,
239+ output_comparator = comparator ,
240+ use_new_flow_neutron_c = True ,
241+ use_qat = use_qat ,
242+ )
243+
244+ # noinspection PyShadowingBuiltins
245+ @pytest .mark .parametrize (
246+ "min, max, expected_tflite_ops" ,
247+ [
248+ pytest .param (
249+ 0.1 ,
250+ 0.5 ,
251+ [Ops .ADD , Ops .MAXIMUM , Ops .MINIMUM ],
252+ id = "min = 0.1, max = 0.5 (Max/Min)" ,
253+ ),
254+ pytest .param (
255+ 0.0 , 1.0 , [Ops .ADD , Ops .RELU_0_TO_1 ], id = "min = 0, max = 1 (Relu0To1)"
256+ ),
257+ pytest .param (
258+ - 1.0 ,
259+ 1.0 ,
260+ [Ops .ADD , Ops .RELU_N1_TO_1 ],
261+ id = "min = -1, max = 1 (ReluN1To1)" ,
262+ ),
263+ pytest .param (
264+ 0.0 , None , [Ops .ADD , Ops .RELU ], id = "min = 0, max = None (Relu)"
265+ ),
266+ ],
267+ )
268+ def test_convert_clamp__relu_vs_maxmin (self , mocker , min , max , expected_tflite_ops ):
269+ input_shape = (23 ,)
270+ model = AddClampModule (min , max )
271+
272+ converter_spy = mocker .spy (EdgeProgramToIRConverter , "convert_program" )
273+ tflite_spy = mocker .spy (AtenModelBuilderDirector , "finish" )
274+
275+ delegated_ep = to_quantized_edge_program (
276+ model ,
277+ input_shape ,
278+ use_new_flow_neutron_c = True ,
279+ ).exported_program ()
280+
281+ # Make sure the `clamp` was delegated.
282+ assert graph_contains_any_of_ops (delegated_ep .graph , [ExecutorchDelegateCall ])
283+ assert not graph_contains_any_of_ops (delegated_ep .graph , [Clamp ])
284+
285+ intermediate_ep = converter_spy .call_args .args [1 ]
286+ quant_node = list (intermediate_ep .graph .nodes )[- 2 ]
287+ dequant_node = list (intermediate_ep .graph .nodes )[- 4 ]
288+ tflite_internal_ops = list (
289+ op .builtin_code for op in tflite_spy .spy_return .operator_codes .vector
290+ )
291+
292+ assert graph_contains_any_of_ops (intermediate_ep .graph , [Clamp ])
293+ assert len (tflite_internal_ops ) == len (expected_tflite_ops ) + 1 # Transpose
294+ assert all (op in tflite_internal_ops for op in expected_tflite_ops )
295+
296+ if len (expected_tflite_ops ) == 3 :
297+ # Min/Max variant should have same input and output quantization
298+ assert all (
299+ q == dq for q , dq in zip (quant_node .args [1 :], dequant_node .args [1 :])
300+ )
301+ else :
302+ assert not all (
303+ q == dq for q , dq in zip (quant_node .args [1 :], dequant_node .args [1 :])
304+ )
0 commit comments