From 0d13bba41ff14ad5933460adcd4010a1f57e8ee7 Mon Sep 17 00:00:00 2001 From: Irina Korchakova Date: Thu, 9 Apr 2026 11:51:56 +0200 Subject: [PATCH 1/3] NXP backend: Add support for `softmax` with the new Neutron flow. --- .../ops_converters/softmax_converter.py | 42 ++- .../nxp/tests/generic_tests/test_cifarnet.py | 23 +- .../tests/generic_tests/test_integration.py | 8 +- .../test_neutron_backend_executor.py | 6 +- .../nxp/tests/generic_tests/test_profiling.py | 5 - .../node_converter/test_softmax_converter.py | 254 +++++------------- .../test_remove_io_quant_ops_pass.py | 6 +- 7 files changed, 105 insertions(+), 239 deletions(-) diff --git a/backends/nxp/backend/ir/converter/node_converters/ops_converters/softmax_converter.py b/backends/nxp/backend/ir/converter/node_converters/ops_converters/softmax_converter.py index c11dc0d7e2c..7db799997bb 100644 --- a/backends/nxp/backend/ir/converter/node_converters/ops_converters/softmax_converter.py +++ b/backends/nxp/backend/ir/converter/node_converters/ops_converters/softmax_converter.py @@ -4,6 +4,7 @@ # LICENSE file in the root directory of this source tree. import numpy as np +import torch from executorch.backends.nxp.backend.custom_delegation_options import ( CustomDelegationOptions, @@ -58,40 +59,31 @@ def _is_supported_on_target( parameters_mapping: dict[str, Parameter], custom_delegation_options: CustomDelegationOptions, ) -> bool: - """Check if the softmax operation can be executed on Neutron hardware. - - Hardware constraints: - 1. Input rank must be >= 2 (Neutron does not support 1D) - 2. Channels must be a multiple of num_macs - 3. Channels < 4096 / num_pipes * 4 - 4. Total spatial size (N*H*W) <= 4096 - 5. (channels * spatial_size) / num_macs <= 65536 + """Hardware constraints: + 1. Input and Output must be INT8/UINT8 + 2. Channels <= 2040 + 3. Total spatial size (N*H*W) <= 4096 + 4. Total size (channels * spatial_size) <= 524288 """ - input_shape = node.meta["val"].shape - - # Constraint 1: Neutron does not support 1D SoftMax - if len(input_shape) == 1: + # Constraint 1: Input and Output must be INT8/UINT8. + supported_types = [torch.int8, torch.uint8] + if not NodeConverter.uses_quantization_type_for_io( + node, supported_types, [0], [0] + ): return False - num_macs = neutron_target_spec.get_num_macs() - num_pipes = neutron_target_spec.get_num_pipes() + # Constraint 2: Channel size limit channels = SoftmaxConverter._get_channels(node) - total_spatial_size = SoftmaxConverter._get_total_spatial_size(node) - - # Constraint 2: Channels must be a multiple of num_macs - if channels % num_macs != 0: + if channels > 2040: return False - # Constraint 3: Channel size limit - if channels >= 4096 / num_pipes * 4: - return False - - # Constraint 4: Spatial size limit + # Constraint 3: Spatial size limit + total_spatial_size = SoftmaxConverter._get_total_spatial_size(node) if total_spatial_size > 4096: return False - # Constraint 5: Total processing size limit - if channels * total_spatial_size / num_macs > 65536: + # Constraint 4: Total processing size limit + if channels * total_spatial_size > 524288: return False return True diff --git a/backends/nxp/tests/generic_tests/test_cifarnet.py b/backends/nxp/tests/generic_tests/test_cifarnet.py index c874ba24e47..e7ec03fa891 100644 --- a/backends/nxp/tests/generic_tests/test_cifarnet.py +++ b/backends/nxp/tests/generic_tests/test_cifarnet.py @@ -11,10 +11,7 @@ from executorch.backends.nxp.tests.config_importer import test_config from executorch.backends.nxp.tests.dataset_creator import CopyDatasetCreator from executorch.backends.nxp.tests.executorch_pipeline import ModelInputSpec -from executorch.backends.nxp.tests.graph_verifier import ( - BaseGraphVerifier, - NonDelegatedNode, -) +from executorch.backends.nxp.tests.graph_verifier import BaseGraphVerifier from executorch.backends.nxp.tests.model_output_comparator import ( NumericalStatsOutputComparator, ) @@ -56,15 +53,15 @@ def test_cifarnet(mocker, request, cifar_test_files, channels_last): model.to(memory_format=torch.channels_last) input_spec.dim_order = torch.channels_last - non_dlg_nodes = [NonDelegatedNode("aten__softmax_default", 1)] - + # Allow MSE up to the theoretical error introduced by 1-bit quantization (1/256). comparator = NumericalStatsOutputComparator( - max_mse_error=1.0e-3, is_classification_task=True + max_mse_error=0.00390625, + is_classification_task=True, ) lower_run_compare( model, [input_spec], - BaseGraphVerifier(1, non_dlg_nodes), + BaseGraphVerifier(1, []), request, dataset_creator=CopyDatasetCreator(cifar_test_files), output_comparator=comparator, @@ -84,18 +81,16 @@ def test_cifarnet_qat(mocker, request, cifar_test_files): model = CifarNet().get_eager_model().eval() input_shape = (1, 3, 32, 32) - non_dlg_nodes = [NonDelegatedNode("aten__softmax_default", 1)] - # The higher MSE threshold is due to using weaker "MovingAbs" observers instead of "MinMax" observers. - # The "MovingAbs" observers capture only limited number of past calibration samples compared to "MinMax", - # which uses statistics from the whole calibration set. + # Allow MSE up to the theoretical error introduced by 1-bit quantization (1/256). comparator = NumericalStatsOutputComparator( - max_mse_error=8e-2, is_classification_task=True + max_mse_error=0.00390625, + is_classification_task=True, ) lower_run_compare( model, input_shape, - BaseGraphVerifier(1, non_dlg_nodes), + BaseGraphVerifier(1, []), request, dataset_creator=CopyDatasetCreator(cifar_test_files), output_comparator=comparator, diff --git a/backends/nxp/tests/generic_tests/test_integration.py b/backends/nxp/tests/generic_tests/test_integration.py index bb7acdde984..9916cba5bdd 100644 --- a/backends/nxp/tests/generic_tests/test_integration.py +++ b/backends/nxp/tests/generic_tests/test_integration.py @@ -30,8 +30,8 @@ def test_conv_fc_softmax__to_executorch_program(use_qat): delegation_info = get_delegation_info(program.graph_module) assert delegation_info.num_delegated_subgraphs == 1 - assert delegation_info.num_non_delegated_nodes == 11 - assert delegation_info.num_delegated_nodes == 15 + assert delegation_info.num_non_delegated_nodes == 5 + assert delegation_info.num_delegated_nodes == 18 # Make sure Convolution and AddMM are delegated. assert not graph_contains_any_of_ops(program.graph, [Convolution, AddMM]) @@ -46,8 +46,8 @@ def test_cifarnet(use_qat): delegation_info = get_delegation_info(exec_prog.exported_program().graph_module) assert delegation_info.num_delegated_subgraphs == 1 - assert delegation_info.num_non_delegated_nodes == 11 - assert delegation_info.num_delegated_nodes == 47 + assert delegation_info.num_non_delegated_nodes == 5 + assert delegation_info.num_delegated_nodes == 50 nodes = list(exec_prog.exported_program().graph.nodes) # `nodes[2].target` is an OpOverload (not and EdgeOpOverload that we usually test against), so just check the name. diff --git a/backends/nxp/tests/generic_tests/test_neutron_backend_executor.py b/backends/nxp/tests/generic_tests/test_neutron_backend_executor.py index 52654a482b9..06a95142b1a 100644 --- a/backends/nxp/tests/generic_tests/test_neutron_backend_executor.py +++ b/backends/nxp/tests/generic_tests/test_neutron_backend_executor.py @@ -89,7 +89,7 @@ def test_conv_fc__lowered_program_and_tflite_output_match(mocker): # No Transpose ops in produced TFLite model tflite_subgraph = Model.GetRootAs(tflite_flatbuffers_model).Subgraphs(0) - assert tflite_subgraph.OperatorsLength() == 3 + assert tflite_subgraph.OperatorsLength() == 4 assert ( tflite_subgraph.Operators(0).BuiltinOptionsType() == BuiltinOptions.Conv2DOptions @@ -102,6 +102,10 @@ def test_conv_fc__lowered_program_and_tflite_output_match(mocker): tflite_subgraph.Operators(2).BuiltinOptionsType() == BuiltinOptions.FullyConnectedOptions ) + assert ( + tflite_subgraph.Operators(3).BuiltinOptionsType() + == BuiltinOptions.SoftmaxOptions + ) # Verify outputs of program and TFLite model input_data = ( diff --git a/backends/nxp/tests/generic_tests/test_profiling.py b/backends/nxp/tests/generic_tests/test_profiling.py index ccd03a81639..cd90bdd345b 100644 --- a/backends/nxp/tests/generic_tests/test_profiling.py +++ b/backends/nxp/tests/generic_tests/test_profiling.py @@ -186,10 +186,6 @@ def forward(self, x): class TestProfiling: - @pytest.mark.xfail( - reason="Profiling support for cmodel and SoftMax fix will be available in Neutron SW 3.2.", - strict=True, - ) def test__softmax(self, caplog, request): caplog.set_level(logging.INFO) model = SoftmaxModule(-1) @@ -240,7 +236,6 @@ def test__simple_parallel_pool(self, caplog, request): 10: (), # Neutron Dump } - @pytest.mark.xfail(reason="SoftMax support PR is not merged so far.", strict=True) def test__cifar(self, caplog, request): caplog.set_level(logging.INFO) input_shape = (1, 3, 32, 32) diff --git a/backends/nxp/tests/ir/converter/node_converter/test_softmax_converter.py b/backends/nxp/tests/ir/converter/node_converter/test_softmax_converter.py index 7f3d0d7276c..103f1fdd087 100644 --- a/backends/nxp/tests/ir/converter/node_converter/test_softmax_converter.py +++ b/backends/nxp/tests/ir/converter/node_converter/test_softmax_converter.py @@ -7,22 +7,17 @@ import pytest import torch -from executorch.backends.nxp.backend.edge_program_converter import ( - EdgeProgramToIRConverter, -) from executorch.backends.nxp.tests.executorch_pipeline import to_quantized_edge_program -from executorch.backends.nxp.tests.executors import ( - convert_run_compare, - graph_contains_any_of_ops, - ToChannelFirstPreprocess, - ToChannelLastPreprocess, +from executorch.backends.nxp.tests.executors import graph_contains_any_of_ops + +from executorch.backends.nxp.tests.graph_verifier import DetailedGraphVerifier + +from executorch.backends.nxp.tests.model_output_comparator import ( + NumericalStatsOutputComparator, ) from executorch.backends.nxp.tests.models import SoftmaxModule -from executorch.exir.dialects._ops import ops as exir_ops - -# noinspection PyProtectedMember -ExecutorchDelegateCall = torch._higher_order_ops.executorch_call_delegate -Softmax = exir_ops.edge.aten._softmax.default +from executorch.backends.nxp.tests.nsys_testing import lower_run_compare +from executorch.backends.nxp.tests.ops_aliases import ExecutorchDelegateCall, Softmax @pytest.fixture(autouse=True) @@ -52,182 +47,67 @@ def assert_softmax_not_delegated(graph): assert graph_contains_any_of_ops(graph, [Softmax]) -def random_input_data(input_shape): - return (np.random.random(input_shape).astype(np.float32) * 256.0 - 128.0).astype( - np.int8 +class TestSoftmax: + @pytest.mark.parametrize( + "input_shape, dim", + [ + # Dim must always be the last dimension. + pytest.param((10,), -1, id="1D_dim_-1"), + pytest.param((5, 21), -1, id="2D_dim_-1"), + pytest.param((2, 3, 13), -1, id="3D_dim_-1"), + pytest.param((1, 3, 3, 200), -1, id="4D_dim_-1"), + pytest.param((5, 4, 3, 2, 180), -1, id="5D_dim_-1"), + ], ) - - -@pytest.mark.parametrize( - "input_shape, dim", - [ - # Dim must always be the last dimension, which must be a multiple of 8 (num_macs). - pytest.param((4096, 128), -1, id="2D_total_size_limit"), - pytest.param((5, 8), -1, id="2D_dim_-1"), - pytest.param((5, 8), 1, id="2D_dim_1"), - pytest.param((4096, 8), -1, id="2D_WxH_limit"), - pytest.param((2, 2048 - 8), -1, id="2D_channels_limit"), - pytest.param((5, 4, 8), -1, id="3D_dim_-1"), - pytest.param((4096, 1, 8), -1, id="3D_WxH_limit"), - pytest.param((5, 4, 3, 8), -1, id="4D_dim_-1"), - pytest.param((1, 64, 64, 8), -1, id="4D_WxH_limit"), - pytest.param((64, 1, 64, 128), -1, id="4D_total_size_limit"), - pytest.param((5, 4, 3, 2, 8), -1, id="5D_dim_-1"), - ], -) -def test_softmax_delegation(input_shape, dim: int, mocker): - model = SoftmaxModule(dim) - - converter_spy = mocker.spy(EdgeProgramToIRConverter, "convert_program") - delegated_ep = to_quantized_edge_program(model, input_shape).exported_program() - - assert_softmax_delegated(delegated_ep.graph) - - # Verify correct behavior of the converted NeutronIR model. - intermediate_ep = converter_spy.call_args.args[1] - neutron_ir_model, *_ = converter_spy.spy_return - input_data = random_input_data(input_shape) - - # Make sure the tested program contains the `softmax`, and its input has the expected rank. - nodes = list(intermediate_ep.graph.nodes) - assert nodes[2].target == Softmax - assert len(nodes[2].args[0].meta["val"].shape) == len(input_shape) - - convert_run_compare( - intermediate_ep, - tfl_model=neutron_ir_model, - input_data=input_data, + def test__basic_nsys_inference(self, mocker, request, input_shape, dim): + model = SoftmaxModule(dim) + graph_verifier = DetailedGraphVerifier( + mocker, + expected_delegated_ops={Softmax: 1}, + expected_non_delegated_ops={}, + ) + output_comparator = NumericalStatsOutputComparator( + max_mse_error=0.001, is_classification_task=True + ) + lower_run_compare( + model, + input_shape, + graph_verifier, + request, + output_comparator=output_comparator, + ) + + @pytest.mark.parametrize( + "input_shape, dim", + [ + pytest.param((4096, 8), -1, id="2D_spatial_size_limit"), + pytest.param((2040,), -1, id="1D_channels_limit"), + pytest.param((4096, 128), -1, id="2D_total_size_limit"), + pytest.param((1, 64, 64, 8), -1, id="4D_spatial_size_limit_1x64x64"), + pytest.param((2, 32, 64, 8), -1, id="4D_spatial_size_limit_2x32x64"), + ], ) - - -@pytest.mark.parametrize( - "input_shape,dim", - [ - # `dim` must be the second dimension, which must be a multiple of 8 (num_macs). - pytest.param((1, 8, 2, 3), 1, id="4D_dim_1"), - pytest.param((1, 8, 64, 64), 1, id="4D_WxH_limit"), - pytest.param( - (64, 128, 1, 64), - -3, - id="4D_dim_-3_total_size_limit", - marks=pytest.mark.xfail( - reason="EIEX-877: should start working when `softmax` is properly supported using new MLIR flow.", - strict=True, - ), - ), - ], -) -def test_softmax_delegation__channel_first(input_shape, dim: int, mocker): - model = ConvSoftmaxModule(dim, input_shape[1]) - - converter_spy = mocker.spy(EdgeProgramToIRConverter, "convert_program") - delegated_ep = to_quantized_edge_program( - model, input_shape, use_neutron_for_format_conversion=False - ).exported_program() - - assert_softmax_delegated(delegated_ep.graph) - - # Verify correct behavior of the converted NeutronIR model. - intermediate_ep = converter_spy.call_args.args[1] - neutron_ir_model, *_ = converter_spy.spy_return - input_data = random_input_data(input_shape) - - # Make sure the tested program contains the `softmax`. - assert graph_contains_any_of_ops(intermediate_ep.graph, [Softmax]) - - convert_run_compare( - intermediate_ep, - tfl_model=neutron_ir_model, - input_data=input_data, - tflite_input_preprocess=ToChannelLastPreprocess(), - tflite_output_preprocess=ToChannelFirstPreprocess(), + def test__limits(self, input_shape, dim, mocker): + model = SoftmaxModule(dim) + delegated_ep = to_quantized_edge_program(model, input_shape).exported_program() + + # Make sure the `softmax` was delegated. + assert_softmax_delegated(delegated_ep.graph) + + @pytest.mark.parametrize( + "input_shape, dim", + [ + pytest.param((4097, 8), -1, id="2D_spatial_size_exceeded"), + pytest.param((2048,), -1, id="1D_channels_exceeded"), + pytest.param((4096, 129), -1, id="2D_total_size_exceeded"), + pytest.param((1, 64, 65, 8), -1, id="4D_spatial_size_exceeded_1x64x65"), + pytest.param((2, 32, 65, 8), -1, id="4D_spatial_size_exceeded_2x32x65"), + ], ) + def test__limits_exceeded(self, input_shape, dim): + model = SoftmaxModule(dim) + delegated_ep = to_quantized_edge_program(model, input_shape).exported_program() + # Make sure the `softmax` was NOT delegated. -@pytest.mark.parametrize( - "input_shape,dim", - [ - # `dim` is not the last dimension. - pytest.param((10, 32), 0, id="2D_dim_0"), - pytest.param((10, 32, 32), 1, id="3D_dim_1"), - pytest.param((10, 32, 32, 8), 2, id="4D_dim_2"), - pytest.param((10, 32, 32, 8, 8), 3, id="5D_dim_3"), - pytest.param((10, 32, 32, 8, 8), 2, id="5D_dim_2"), - ], -) -def test_softmax_delegation__unsupported_dims(input_shape, dim: int): - model = SoftmaxModule(dim) - delegated_ep = to_quantized_edge_program(model, input_shape).exported_program() - assert_softmax_not_delegated(delegated_ep.graph) - - -@pytest.mark.parametrize( - "input_shape,dim", - [ - # `dim` is not the second dimension. - pytest.param((10, 32, 32, 8), 2, id="dim_2"), - pytest.param((10, 32, 32, 8), -1, id="dim_-1"), - pytest.param((10, 32, 32, 8), 3, id="dim_3"), - ], -) -def test_softmax_delegation__unsupported_dims__channels_first(input_shape, dim: int): - model = SoftmaxModule(dim) - delegated_ep = to_quantized_edge_program(model, input_shape).exported_program() - assert_softmax_not_delegated(delegated_ep.graph) - - -@pytest.mark.parametrize( - "input_shape,dim", - [ - pytest.param((4096 + 1, 8), -1, id="2D_WxH_exceeded"), - pytest.param((4096, 2, 8), -1, id="3D_WxH_exceeded"), - pytest.param((2, 64, 64, 8), -1, id="4D_WxH_exceeded"), - pytest.param((1, 2048), -1, id="2D_channels_exceeded"), - pytest.param((4096, 128 + 8), -1, id="2D_total_size_exceeded"), - pytest.param((64, 1, 64, 128 + 8), -1, id="4D_total_size_exceeded"), - ], -) -def test_softmax_delegation__unsupported_dimension_sizes(input_shape, dim: int): - model = SoftmaxModule(dim) - delegated_ep = to_quantized_edge_program(model, input_shape).exported_program() - assert_softmax_not_delegated(delegated_ep.graph) - - -@pytest.mark.parametrize( - "input_shape,dim", - [ - pytest.param( - (2, 8, 64, 64), - -1, - id="4D_WxH_exceeded", - marks=pytest.mark.xfail( - reason="EIEX-877: should start working when `softmax` is properly supported using new MLIR flow.", - strict=True, - ), - ), - pytest.param( - (64, 128 + 8, 1, 64), - -1, - id="4D_total_size_exceeded", - marks=pytest.mark.xfail( - reason="EIEX-877: should start working when `softmax` is properly supported using new MLIR flow.", - strict=True, - ), - ), - ], -) -def test_softmax_delegation__unsupported_dimension_sizes__channels_first( - input_shape, dim: int -): - model = ConvSoftmaxModule(dim, input_shape[1]) - delegated_ep = to_quantized_edge_program(model, input_shape).exported_program() - assert_softmax_not_delegated(delegated_ep.graph) - - -def test_softmax_delegation__1d(): - input_shape = (8,) - dim = 0 - - model = SoftmaxModule(dim) - delegated_ep = to_quantized_edge_program(model, input_shape).exported_program() - assert_softmax_not_delegated(delegated_ep.graph) + assert_softmax_not_delegated(delegated_ep.graph) diff --git a/backends/nxp/tests/ir/edge_passes/test_remove_io_quant_ops_pass.py b/backends/nxp/tests/ir/edge_passes/test_remove_io_quant_ops_pass.py index 148172a00fa..ef669897b51 100644 --- a/backends/nxp/tests/ir/edge_passes/test_remove_io_quant_ops_pass.py +++ b/backends/nxp/tests/ir/edge_passes/test_remove_io_quant_ops_pass.py @@ -1,4 +1,4 @@ -# Copyright 2024-2025 NXP +# Copyright 2024-2026 NXP # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. @@ -63,12 +63,12 @@ def test_remove_io_quant_ops_pass__cifarnet(): ) nodes = list(exec_prog.exported_program().graph.nodes) - assert len(nodes) == 11 + assert len(nodes) == 5 assert ( nodes[0].meta["val"].dtype == torch.int8 ), "Input tensor doesn't have type INT8." assert ( - nodes[10].meta["val"][0].dtype == torch.int8 + nodes[4].meta["val"][0].dtype == torch.int8 ), "Output tensor doesn't have type INT8." assert ( From 79b5ba58d0579f85d92de4965ed22bf2cc935786 Mon Sep 17 00:00:00 2001 From: Irina Korchakova Date: Tue, 4 Aug 2026 09:37:36 +0200 Subject: [PATCH 2/3] Update comparators in tests --- backends/nxp/tests/generic_tests/test_cifarnet.py | 6 ++---- .../ir/converter/node_converter/test_softmax_converter.py | 7 +++---- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/backends/nxp/tests/generic_tests/test_cifarnet.py b/backends/nxp/tests/generic_tests/test_cifarnet.py index e7ec03fa891..6db8ebc9a03 100644 --- a/backends/nxp/tests/generic_tests/test_cifarnet.py +++ b/backends/nxp/tests/generic_tests/test_cifarnet.py @@ -53,9 +53,8 @@ def test_cifarnet(mocker, request, cifar_test_files, channels_last): model.to(memory_format=torch.channels_last) input_spec.dim_order = torch.channels_last - # Allow MSE up to the theoretical error introduced by 1-bit quantization (1/256). comparator = NumericalStatsOutputComparator( - max_mse_error=0.00390625, + max_mse_error=1.53e-5, is_classification_task=True, ) lower_run_compare( @@ -82,9 +81,8 @@ def test_cifarnet_qat(mocker, request, cifar_test_files): input_shape = (1, 3, 32, 32) - # Allow MSE up to the theoretical error introduced by 1-bit quantization (1/256). comparator = NumericalStatsOutputComparator( - max_mse_error=0.00390625, + max_mse_error=1.53e-5, is_classification_task=True, ) lower_run_compare( diff --git a/backends/nxp/tests/ir/converter/node_converter/test_softmax_converter.py b/backends/nxp/tests/ir/converter/node_converter/test_softmax_converter.py index 103f1fdd087..b4fa320f9c1 100644 --- a/backends/nxp/tests/ir/converter/node_converter/test_softmax_converter.py +++ b/backends/nxp/tests/ir/converter/node_converter/test_softmax_converter.py @@ -13,7 +13,7 @@ from executorch.backends.nxp.tests.graph_verifier import DetailedGraphVerifier from executorch.backends.nxp.tests.model_output_comparator import ( - NumericalStatsOutputComparator, + AllCloseOutputComparator, ) from executorch.backends.nxp.tests.models import SoftmaxModule from executorch.backends.nxp.tests.nsys_testing import lower_run_compare @@ -66,15 +66,14 @@ def test__basic_nsys_inference(self, mocker, request, input_shape, dim): expected_delegated_ops={Softmax: 1}, expected_non_delegated_ops={}, ) - output_comparator = NumericalStatsOutputComparator( - max_mse_error=0.001, is_classification_task=True - ) + output_comparator = AllCloseOutputComparator(atol=1) lower_run_compare( model, input_shape, graph_verifier, request, output_comparator=output_comparator, + remove_quant_io_ops=True, ) @pytest.mark.parametrize( From 8bdb59e5f4521345ad4c4e28149277f45f97354f Mon Sep 17 00:00:00 2001 From: Irina Korchakova Date: Wed, 5 Aug 2026 11:10:32 +0200 Subject: [PATCH 3/3] Add Conv2d + Softmax test --- .../node_converter/test_softmax_converter.py | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/backends/nxp/tests/ir/converter/node_converter/test_softmax_converter.py b/backends/nxp/tests/ir/converter/node_converter/test_softmax_converter.py index b4fa320f9c1..704595707d5 100644 --- a/backends/nxp/tests/ir/converter/node_converter/test_softmax_converter.py +++ b/backends/nxp/tests/ir/converter/node_converter/test_softmax_converter.py @@ -17,7 +17,12 @@ ) from executorch.backends.nxp.tests.models import SoftmaxModule from executorch.backends.nxp.tests.nsys_testing import lower_run_compare -from executorch.backends.nxp.tests.ops_aliases import ExecutorchDelegateCall, Softmax +from executorch.backends.nxp.tests.ops_aliases import ( + Convolution, + ExecutorchDelegateCall, + Softmax, + ViewCopy, +) @pytest.fixture(autouse=True) @@ -110,3 +115,33 @@ def test__limits_exceeded(self, input_shape, dim): # Make sure the `softmax` was NOT delegated. assert_softmax_not_delegated(delegated_ep.graph) + + @pytest.mark.parametrize( + "input_shape,dim", + [ + pytest.param((9, 7, 7), -1, id="3D_dim_-1"), + pytest.param((1, 9, 2, 3), -3, id="4D_dim_-3"), + ], + ) + def test_basic_nsys_inference__conv_softmax( + self, mocker, request, input_shape, dim: int + ): + model = ConvSoftmaxModule(dim, input_shape[-3]) + graph_verifier = DetailedGraphVerifier( + mocker, + expected_delegated_ops={ + Softmax: 1, + Convolution: 1, + **({ViewCopy: 2} if len(input_shape) == 3 else {}), + }, + expected_non_delegated_ops={}, + ) + output_comparator = AllCloseOutputComparator(atol=1) + lower_run_compare( + model, + input_shape, + graph_verifier, + request, + output_comparator=output_comparator, + remove_quant_io_ops=True, + )