From c60d694d81f9f2d0777a18e25ad1a26e9c643d6f Mon Sep 17 00:00:00 2001 From: Vaclav Novak Date: Tue, 23 Sep 2025 15:39:27 +0200 Subject: [PATCH 01/10] NXP backend: added aten.sub operator support --- .../nxp/backend/edge_program_converter.py | 1 + .../ops_converters/__init__.py | 4 + .../ops_converters/sub_tensor_converter.py | 62 ++++++++ backends/nxp/neutron_partitioner.py | 1 + backends/nxp/quantizer/neutron_quantizer.py | 2 + backends/nxp/quantizer/patterns.py | 26 ++++ .../test_sub_tensor_converter.py | 145 ++++++++++++++++++ backends/nxp/tests/models.py | 28 ++++ 8 files changed, 269 insertions(+) create mode 100644 backends/nxp/backend/ir/converter/node_converters/ops_converters/sub_tensor_converter.py create mode 100644 backends/nxp/tests/ir/converter/node_converter/test_sub_tensor_converter.py diff --git a/backends/nxp/backend/edge_program_converter.py b/backends/nxp/backend/edge_program_converter.py index febcd03913a..3c42ab09333 100644 --- a/backends/nxp/backend/edge_program_converter.py +++ b/backends/nxp/backend/edge_program_converter.py @@ -31,6 +31,7 @@ exir_ops.edge.aten._adaptive_avg_pool2d.default: AdaptiveAvgPool2dConverter, # noqa F405 exir_ops.edge.aten.addmm.default: AddMMConverter, # noqa F405 exir_ops.edge.aten.add.Tensor: AddTensorConverter, # noqa F405 + exir_ops.edge.aten.sub.Tensor: SubTensorConverter, # noqa F405 exir_ops.edge.aten.avg_pool2d.default: AvgPool2dConverter, # noqa F405 exir_ops.edge.aten.cat.default: CatConverter, # noqa F405 exir_ops.edge.aten.clone.default: CloneConverter, # noqa F405 diff --git a/backends/nxp/backend/ir/converter/node_converters/ops_converters/__init__.py b/backends/nxp/backend/ir/converter/node_converters/ops_converters/__init__.py index 472a3495e19..3cf70f46b8d 100755 --- a/backends/nxp/backend/ir/converter/node_converters/ops_converters/__init__.py +++ b/backends/nxp/backend/ir/converter/node_converters/ops_converters/__init__.py @@ -56,6 +56,9 @@ from executorch.backends.nxp.backend.ir.converter.node_converters.ops_converters.softmax_converter import ( SoftmaxConverter, ) +from executorch.backends.nxp.backend.ir.converter.node_converters.ops_converters.sub_tensor_converter import ( + SubTensorConverter, +) from executorch.backends.nxp.backend.ir.converter.node_converters.ops_converters.tanh_converter import ( TanhConverter, ) @@ -80,6 +83,7 @@ "MaxPool2dConverter", "AvgPool2dConverter", "AddTensorConverter", + "SubTensorConverter", "CloneConverter", "AbsConverter", "AdaptiveAvgPool2dConverter", diff --git a/backends/nxp/backend/ir/converter/node_converters/ops_converters/sub_tensor_converter.py b/backends/nxp/backend/ir/converter/node_converters/ops_converters/sub_tensor_converter.py new file mode 100644 index 00000000000..02f3684436a --- /dev/null +++ b/backends/nxp/backend/ir/converter/node_converters/ops_converters/sub_tensor_converter.py @@ -0,0 +1,62 @@ +# Copyright 2025 NXP +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +from executorch.backends.nxp.backend.ir.converter.conversion.common import ( + node_uses_shape_broadcasting, +) +from executorch.backends.nxp.backend.ir.converter.node_converter import ( + CustomDelegationOptions, + NodeConverter, + Target, +) +from executorch.backends.nxp.backend.ir.tflite_generator.builtin_options import ( + sub_options, +) +from torch.fx import Node +from torch.nn import Parameter + + +class SubTensorConverter(NodeConverter): + @staticmethod + def _is_supported_on_target( + node: Node, + target: Target, + parameters_mapping: dict[str, Parameter], + custom_delegation_options: CustomDelegationOptions, + ) -> bool: + match target: + case Target.RT700: + if node_uses_shape_broadcasting(node): + # Shape broadcasting may require the addition of `Transpose` ops during conversion. + return False + + return True + + case _: + return False + + @staticmethod + def _is_supported_in_IR( + node: Node, + parameters_mapping: dict[str, Parameter], + custom_delegation_options: CustomDelegationOptions, + ) -> bool: + if len(node.args) != 2: + return False + + if hasattr(node.kwargs, "alpha"): + return False + + return True + + # sub.Tensor Node format: (Tensor self, Tensor other, *, Scalar alpha=1) + def convert(self, node: Node): + """Convert 'sub_tensor' operator to TFLite 'sub'.""" + self.assert_convertible(node) + + t_op = self._create_tflite_op_with_io_tensors(node) + + t_op.builtin_options = sub_options.Sub() + self.builder.append_operators([t_op]) diff --git a/backends/nxp/neutron_partitioner.py b/backends/nxp/neutron_partitioner.py index 917545e6c89..28893b117a3 100644 --- a/backends/nxp/neutron_partitioner.py +++ b/backends/nxp/neutron_partitioner.py @@ -198,6 +198,7 @@ def tag_qdq_clusters(self, nodes: list[torch.fx.Node]): exir_ops.edge.aten._adaptive_avg_pool2d.default: AdaptiveAvgPool2dConverter, # noqa F405 exir_ops.edge.aten.addmm.default: AddMMConverter, # noqa F405 exir_ops.edge.aten.add.Tensor: AddTensorConverter, # noqa F405 + exir_ops.edge.aten.sub.Tensor: SubTensorConverter, # noqa F405 exir_ops.edge.aten.avg_pool2d.default: AvgPool2dConverter, # noqa F405 exir_ops.edge.aten.cat.default: CatConverter, # noqa F405 exir_ops.edge.aten.clone.default: CloneConverter, # noqa F405 diff --git a/backends/nxp/quantizer/neutron_quantizer.py b/backends/nxp/quantizer/neutron_quantizer.py index db19bcb8ba8..02b9e7e9b83 100644 --- a/backends/nxp/quantizer/neutron_quantizer.py +++ b/backends/nxp/quantizer/neutron_quantizer.py @@ -14,6 +14,7 @@ AdaptiveAvgPoolPattern, AddmmPattern, AddTensorPattern, + SubTensorPattern, AvgPoolPattern, CatPattern, Conv1dPattern, @@ -188,6 +189,7 @@ def __init__(self): NeutronAtenQuantizer(AbsPattern(), static_qconfig), NeutronAtenQuantizer(AdaptiveAvgPoolPattern(), static_qconfig), NeutronAtenQuantizer(AddTensorPattern(), static_qconfig), + NeutronAtenQuantizer(SubTensorPattern(), static_qconfig), NeutronAtenQuantizer(AddmmPattern(), static_fc_qconfig), NeutronAtenQuantizer(AvgPoolPattern(), static_qconfig), NeutronAtenQuantizer(CatPattern(), static_qconfig), diff --git a/backends/nxp/quantizer/patterns.py b/backends/nxp/quantizer/patterns.py index 34ee611b8b2..27007262252 100644 --- a/backends/nxp/quantizer/patterns.py +++ b/backends/nxp/quantizer/patterns.py @@ -224,6 +224,32 @@ def get_anchors( ) +class SubTensorPattern(QuantizationPattern): + """ + Quantization pattern for Sub Tensor quantization. Accepts 1 or 2 input nodes. + + Basic quantization for all inputs and output. + """ + + def partition_types(self) -> List[Type[torch.nn.Module]]: + return [torch.ops.aten.sub.Tensor] + + def get_anchors( + self, gm: fx.GraphModule, fused_partition: List[fx.GraphModule] + ) -> PartitionAnchors | None: + node = fused_partition[0].nodes[-1] + inputs = [(node, 0)] + if len(fused_partition[0].input_nodes) == 2: + inputs = [(node, 0), (node, 1)] + + return PartitionAnchors( + inputs=inputs, + weights=[], + biases=[], + output=[(node,)], + ) + + class AvgPoolPattern(SharedSpecPattern): """ Quantizer for AvgPool2D operator. diff --git a/backends/nxp/tests/ir/converter/node_converter/test_sub_tensor_converter.py b/backends/nxp/tests/ir/converter/node_converter/test_sub_tensor_converter.py new file mode 100644 index 00000000000..6c47ca65536 --- /dev/null +++ b/backends/nxp/tests/ir/converter/node_converter/test_sub_tensor_converter.py @@ -0,0 +1,145 @@ +import numpy as np +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, + ToChannelFirstPreprocess, + ToChannelLastPreprocess, +) +from executorch.backends.nxp.tests.models import ( + SubTensorConvModule, + SubTensorModule, + SubTensorOneInputModule, +) +from torch.export import ExportedProgram + + +@pytest.fixture(autouse=True) +def reseed_model_per_test_run(): + torch.manual_seed(23) + np.random.seed(23) + + +@pytest.mark.parametrize( + "input_shape", + [ + pytest.param((4,), id="1D."), + pytest.param((6, 6), id="2D."), + pytest.param((1, 4, 8), id="3D."), + pytest.param((1, 4, 8, 8), id="4D."), + ], +) +def test_sub_tensor_quant_conversion(mocker, input_shape): + model = SubTensorModule() + + converter_spy = mocker.spy(EdgeProgramToIRConverter, "convert_program") + + # Run conversion + _ = to_quantized_edge_program(model, [input_shape, input_shape]) + + # Capture generated model + tflite_flatbuffers_model, io_formats = converter_spy.spy_return + + # Capture converted program + exported_program: ExportedProgram = converter_spy.call_args.args[1] + + input_data = (np.random.random(input_shape).astype(np.float32) * 50).astype(np.int8) + input_data = {0: input_data, 1: input_data} + + convert_run_compare( + exported_program, tfl_model=tflite_flatbuffers_model, input_data=input_data + ) + + +@pytest.mark.parametrize( + "input_shape", + [ + pytest.param((4,), id="1D."), + pytest.param((6, 6), id="2D."), + pytest.param((1, 4, 8), id="3D."), + pytest.param((1, 4, 8, 8), id="4D."), + ], +) +def test_sub_tensor_one_input_quant_conversion(mocker, input_shape): + model = SubTensorOneInputModule() + + converter_spy = mocker.spy(EdgeProgramToIRConverter, "convert_program") + + # Run conversion + _ = to_quantized_edge_program(model, input_shape) + + # Capture generated model + tflite_flatbuffers_model, io_formats = converter_spy.spy_return + + # Capture converted program + exported_program: ExportedProgram = converter_spy.call_args.args[1] + + input_data = (np.random.random(input_shape).astype(np.float32) * 50).astype(np.int8) + + convert_run_compare( + exported_program, tfl_model=tflite_flatbuffers_model, input_data=input_data + ) + + +@pytest.mark.parametrize( + "input_shape", + [ + pytest.param((1, 4, 8, 8), id="4D."), + pytest.param((1, 4, 5, 5), id="4D, product of dims is not a multiple of 8."), + ], +) +def test_sub_tensor_w_conv_quant_conversion(mocker, input_shape): + model = SubTensorConvModule() + + converter_spy = mocker.spy(EdgeProgramToIRConverter, "convert_program") + + # Run conversion + _ = to_quantized_edge_program(model, input_shape) + + # Capture generated model + tflite_flatbuffers_model, io_formats = converter_spy.spy_return + + # Capture converted program + exported_program: ExportedProgram = converter_spy.call_args.args[1] + + input_data = (np.random.random(input_shape).astype(np.float32) * 50).astype(np.int8) + + convert_run_compare( + exported_program, + input_data, + tflite_input_preprocess=ToChannelLastPreprocess(), + tfl_model=tflite_flatbuffers_model, + tflite_output_preprocess=ToChannelFirstPreprocess(), + ) + + +@pytest.mark.parametrize( + "x_input_shape, y_input_shape", + [ + pytest.param((1, 4, 7), (4, 7), id="3D -> 2D."), + pytest.param((1, 4, 8), (1, 4, 4, 8), id="3D -> 4D."), + pytest.param((1, 1, 4, 4, 8), (1, 4, 4, 8), id="5D -> 4D."), + pytest.param((4,), (4, 4), id="1D -> 2D."), + pytest.param((4,), (4, 4, 4), id="1D -> 3D."), + pytest.param((6, 6), (1, 8, 6, 6), id="2D -> 4D."), + pytest.param((6, 6), (6,), id="2D -> 1D."), + ], +) +def test_sub_tensor_broadcasting_unsupported_quant_conversion( + x_input_shape, y_input_shape +): + model = SubTensorModule() + + # Run conversion + edge_program = to_quantized_edge_program( + model, [x_input_shape, y_input_shape] + ).exported_program() + nodes = list(edge_program.graph.nodes) + + # Broadcast is not supported, node is not converted + assert nodes[6].target.__name__ == "aten.sub.Tensor" # Sub Tensor is not delegated. diff --git a/backends/nxp/tests/models.py b/backends/nxp/tests/models.py index e7b60b2566c..1b708875455 100644 --- a/backends/nxp/tests/models.py +++ b/backends/nxp/tests/models.py @@ -451,6 +451,34 @@ def forward(x): return x + x +class SubTensorModule(torch.nn.Module): + def __init__(self): + super().__init__() + + @staticmethod + def forward(x, y): + return x - y + + +class SubTensorConvModule(torch.nn.Module): + def __init__(self): + super().__init__() + self.conv = Conv2dModule(padding=1, stride=1) + + def forward(self, x): + x = self.conv(x) + return x - x + + +class SubTensorOneInputModule(torch.nn.Module): + def __init__(self): + super().__init__() + + @staticmethod + def forward(x): + return x - x + + class MeanDimLinearModule(torch.nn.Module): def __init__(self, dim, keepdim): super().__init__() From c7346d93c956e179d4b7abcb1bb83d0672c606ad Mon Sep 17 00:00:00 2001 From: Vaclav Novak Date: Wed, 24 Sep 2025 09:22:55 +0200 Subject: [PATCH 02/10] fix: fixed lint error --- backends/nxp/quantizer/neutron_quantizer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backends/nxp/quantizer/neutron_quantizer.py b/backends/nxp/quantizer/neutron_quantizer.py index 02b9e7e9b83..4a8a8a49236 100644 --- a/backends/nxp/quantizer/neutron_quantizer.py +++ b/backends/nxp/quantizer/neutron_quantizer.py @@ -14,7 +14,6 @@ AdaptiveAvgPoolPattern, AddmmPattern, AddTensorPattern, - SubTensorPattern, AvgPoolPattern, CatPattern, Conv1dPattern, @@ -37,6 +36,7 @@ SharedSpecPattern, SigmoidPattern, SoftMaxPattern, + SubTensorPattern, TanhInPlacePattern, TanhPattern, ViewPattern, From 5ea56637783eb4f98c89e5b87fbcec0c708bdbfe Mon Sep 17 00:00:00 2001 From: Vaclav Novak Date: Tue, 30 Sep 2025 09:35:29 +0200 Subject: [PATCH 03/10] fix: applied feedback from PR --- .../nxp/backend/edge_program_converter.py | 2 +- .../ops_converters/sub_tensor_converter.py | 4 +++- backends/nxp/neutron_partitioner.py | 2 +- .../test_add_tensor_converter.py | 4 ++++ .../test_sub_tensor_converter.py | 23 ++++++++++++++++--- 5 files changed, 29 insertions(+), 6 deletions(-) diff --git a/backends/nxp/backend/edge_program_converter.py b/backends/nxp/backend/edge_program_converter.py index 3c42ab09333..03d55548d2d 100644 --- a/backends/nxp/backend/edge_program_converter.py +++ b/backends/nxp/backend/edge_program_converter.py @@ -31,7 +31,6 @@ exir_ops.edge.aten._adaptive_avg_pool2d.default: AdaptiveAvgPool2dConverter, # noqa F405 exir_ops.edge.aten.addmm.default: AddMMConverter, # noqa F405 exir_ops.edge.aten.add.Tensor: AddTensorConverter, # noqa F405 - exir_ops.edge.aten.sub.Tensor: SubTensorConverter, # noqa F405 exir_ops.edge.aten.avg_pool2d.default: AvgPool2dConverter, # noqa F405 exir_ops.edge.aten.cat.default: CatConverter, # noqa F405 exir_ops.edge.aten.clone.default: CloneConverter, # noqa F405 @@ -44,6 +43,7 @@ exir_ops.edge.aten.permute_copy.default: PermuteCopyConverter, # noqa F405 exir_ops.edge.aten.relu.default: ReLUConverter, # noqa F405 exir_ops.edge.aten._softmax.default: SoftmaxConverter, # noqa F405 + exir_ops.edge.aten.sub.Tensor: SubTensorConverter, # noqa F405 exir_ops.edge.aten.tanh.default: TanhConverter, # noqa F405 exir_ops.edge.aten.view_copy.default: ViewCopyConverter, # noqa F405 exir_ops.edge.aten.sigmoid.default: SigmoidConverter, # noqa F405 diff --git a/backends/nxp/backend/ir/converter/node_converters/ops_converters/sub_tensor_converter.py b/backends/nxp/backend/ir/converter/node_converters/ops_converters/sub_tensor_converter.py index 02f3684436a..08c0df18120 100644 --- a/backends/nxp/backend/ir/converter/node_converters/ops_converters/sub_tensor_converter.py +++ b/backends/nxp/backend/ir/converter/node_converters/ops_converters/sub_tensor_converter.py @@ -46,6 +46,8 @@ def _is_supported_in_IR( if len(node.args) != 2: return False + # The `alpha` attribute can be represented by adding an extra `Mul` operator. + # However, this is not implemented as `alpha` is rarely used. if hasattr(node.kwargs, "alpha"): return False @@ -53,7 +55,7 @@ def _is_supported_in_IR( # sub.Tensor Node format: (Tensor self, Tensor other, *, Scalar alpha=1) def convert(self, node: Node): - """Convert 'sub_tensor' operator to TFLite 'sub'.""" + """Convert 'sub_tensor' operator to NeutronIR 'Sub'.""" self.assert_convertible(node) t_op = self._create_tflite_op_with_io_tensors(node) diff --git a/backends/nxp/neutron_partitioner.py b/backends/nxp/neutron_partitioner.py index 28893b117a3..e7ad7ff7a0b 100644 --- a/backends/nxp/neutron_partitioner.py +++ b/backends/nxp/neutron_partitioner.py @@ -198,7 +198,6 @@ def tag_qdq_clusters(self, nodes: list[torch.fx.Node]): exir_ops.edge.aten._adaptive_avg_pool2d.default: AdaptiveAvgPool2dConverter, # noqa F405 exir_ops.edge.aten.addmm.default: AddMMConverter, # noqa F405 exir_ops.edge.aten.add.Tensor: AddTensorConverter, # noqa F405 - exir_ops.edge.aten.sub.Tensor: SubTensorConverter, # noqa F405 exir_ops.edge.aten.avg_pool2d.default: AvgPool2dConverter, # noqa F405 exir_ops.edge.aten.cat.default: CatConverter, # noqa F405 exir_ops.edge.aten.clone.default: CloneConverter, # noqa F405 @@ -211,6 +210,7 @@ def tag_qdq_clusters(self, nodes: list[torch.fx.Node]): exir_ops.edge.aten.mm.default: MMConverter, # noqa F405 exir_ops.edge.aten.relu.default: ReLUConverter, # noqa F405 exir_ops.edge.aten._softmax.default: SoftmaxConverter, # noqa F405 + exir_ops.edge.aten.sub.Tensor: SubTensorConverter, # noqa F405 exir_ops.edge.aten.tanh.default: TanhConverter, # noqa F405 exir_ops.edge.aten.view_copy.default: ViewCopyConverter, # noqa F405 exir_ops.edge.aten.sigmoid.default: SigmoidConverter, # noqa F405 diff --git a/backends/nxp/tests/ir/converter/node_converter/test_add_tensor_converter.py b/backends/nxp/tests/ir/converter/node_converter/test_add_tensor_converter.py index 567b593e05b..2c3107eae77 100644 --- a/backends/nxp/tests/ir/converter/node_converter/test_add_tensor_converter.py +++ b/backends/nxp/tests/ir/converter/node_converter/test_add_tensor_converter.py @@ -1,3 +1,7 @@ +# Copyright 2025 NXP +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. import numpy as np import pytest import torch diff --git a/backends/nxp/tests/ir/converter/node_converter/test_sub_tensor_converter.py b/backends/nxp/tests/ir/converter/node_converter/test_sub_tensor_converter.py index 6c47ca65536..0005b2968fc 100644 --- a/backends/nxp/tests/ir/converter/node_converter/test_sub_tensor_converter.py +++ b/backends/nxp/tests/ir/converter/node_converter/test_sub_tensor_converter.py @@ -16,6 +16,7 @@ SubTensorModule, SubTensorOneInputModule, ) +from executorch.exir.dialects._ops import ops as exir_ops from torch.export import ExportedProgram @@ -48,8 +49,16 @@ def test_sub_tensor_quant_conversion(mocker, input_shape): # Capture converted program exported_program: ExportedProgram = converter_spy.call_args.args[1] - input_data = (np.random.random(input_shape).astype(np.float32) * 50).astype(np.int8) - input_data = {0: input_data, 1: input_data} + input_data_1 = (np.random.random(input_shape).astype(np.float32) * 50).astype( + np.int8 + ) + input_data_2 = (np.random.random(input_shape).astype(np.float32) * 50).astype( + np.int8 + ) + input_data = {0: input_data_1, 1: input_data_2} + + nodes = list(exported_program.graph.nodes) + assert nodes[4].name == "aten_sub_tensor" convert_run_compare( exported_program, tfl_model=tflite_flatbuffers_model, input_data=input_data @@ -81,6 +90,9 @@ def test_sub_tensor_one_input_quant_conversion(mocker, input_shape): input_data = (np.random.random(input_shape).astype(np.float32) * 50).astype(np.int8) + nodes = list(exported_program.graph.nodes) + assert nodes[2].name == "aten_sub_tensor" + convert_run_compare( exported_program, tfl_model=tflite_flatbuffers_model, input_data=input_data ) @@ -109,6 +121,9 @@ def test_sub_tensor_w_conv_quant_conversion(mocker, input_shape): input_data = (np.random.random(input_shape).astype(np.float32) * 50).astype(np.int8) + nodes = list(exported_program.graph.nodes) + assert nodes[9].name == "aten_sub_tensor" + convert_run_compare( exported_program, input_data, @@ -142,4 +157,6 @@ def test_sub_tensor_broadcasting_unsupported_quant_conversion( nodes = list(edge_program.graph.nodes) # Broadcast is not supported, node is not converted - assert nodes[6].target.__name__ == "aten.sub.Tensor" # Sub Tensor is not delegated. + assert ( + nodes[6].target == exir_ops.edge.aten.sub.Tensor + ) # Sub Tensor is not delegated. From 58992efaead5d967dfb3de61525caf0a89ebe77b Mon Sep 17 00:00:00 2001 From: Vaclav Novak Date: Tue, 30 Sep 2025 11:56:55 +0200 Subject: [PATCH 04/10] fix: added missing license --- .../ir/converter/node_converter/test_sub_tensor_converter.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/backends/nxp/tests/ir/converter/node_converter/test_sub_tensor_converter.py b/backends/nxp/tests/ir/converter/node_converter/test_sub_tensor_converter.py index 0005b2968fc..f103997bd0d 100644 --- a/backends/nxp/tests/ir/converter/node_converter/test_sub_tensor_converter.py +++ b/backends/nxp/tests/ir/converter/node_converter/test_sub_tensor_converter.py @@ -1,3 +1,7 @@ +# Copyright 2025 NXP +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. import numpy as np import pytest import torch From a3aeccc8c31ca5c76fe937852da565dfef160087 Mon Sep 17 00:00:00 2001 From: Vaclav Novak Date: Tue, 30 Sep 2025 14:47:51 +0200 Subject: [PATCH 05/10] fix: improved test effectiveness --- .../test_sub_tensor_converter.py | 24 ++++++++++++------- backends/nxp/tests/models.py | 4 ++-- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/backends/nxp/tests/ir/converter/node_converter/test_sub_tensor_converter.py b/backends/nxp/tests/ir/converter/node_converter/test_sub_tensor_converter.py index f103997bd0d..faf6b457438 100644 --- a/backends/nxp/tests/ir/converter/node_converter/test_sub_tensor_converter.py +++ b/backends/nxp/tests/ir/converter/node_converter/test_sub_tensor_converter.py @@ -103,19 +103,21 @@ def test_sub_tensor_one_input_quant_conversion(mocker, input_shape): @pytest.mark.parametrize( - "input_shape", + "x_input_shape, y_input_shape", [ - pytest.param((1, 4, 8, 8), id="4D."), - pytest.param((1, 4, 5, 5), id="4D, product of dims is not a multiple of 8."), + pytest.param((1, 4, 8, 8), (1, 8, 8, 8), id="4D."), + pytest.param( + (1, 4, 5, 5), (1, 8, 5, 5), id="4D, product of dims is not a multiple of 8." + ), ], ) -def test_sub_tensor_w_conv_quant_conversion(mocker, input_shape): +def test_sub_tensor_w_conv_quant_conversion(mocker, x_input_shape, y_input_shape): model = SubTensorConvModule() converter_spy = mocker.spy(EdgeProgramToIRConverter, "convert_program") # Run conversion - _ = to_quantized_edge_program(model, input_shape) + _ = to_quantized_edge_program(model, [x_input_shape, y_input_shape]) # Capture generated model tflite_flatbuffers_model, io_formats = converter_spy.spy_return @@ -123,14 +125,20 @@ def test_sub_tensor_w_conv_quant_conversion(mocker, input_shape): # Capture converted program exported_program: ExportedProgram = converter_spy.call_args.args[1] - input_data = (np.random.random(input_shape).astype(np.float32) * 50).astype(np.int8) + input_data_1 = (np.random.random(x_input_shape).astype(np.float32) * 50).astype( + np.int8 + ) + input_data_2 = (np.random.random(y_input_shape).astype(np.float32) * 50).astype( + np.int8 + ) + input_data = {0: input_data_1, 1: input_data_2} nodes = list(exported_program.graph.nodes) - assert nodes[9].name == "aten_sub_tensor" + assert nodes[11].name == "aten_sub_tensor" convert_run_compare( exported_program, - input_data, + input_data=input_data, tflite_input_preprocess=ToChannelLastPreprocess(), tfl_model=tflite_flatbuffers_model, tflite_output_preprocess=ToChannelFirstPreprocess(), diff --git a/backends/nxp/tests/models.py b/backends/nxp/tests/models.py index 1b708875455..f613349fed0 100644 --- a/backends/nxp/tests/models.py +++ b/backends/nxp/tests/models.py @@ -465,9 +465,9 @@ def __init__(self): super().__init__() self.conv = Conv2dModule(padding=1, stride=1) - def forward(self, x): + def forward(self, x, y): x = self.conv(x) - return x - x + return x - y class SubTensorOneInputModule(torch.nn.Module): From cc8ebaa9ae028a1d8987f1ad39a889babe0b299d Mon Sep 17 00:00:00 2001 From: Vaclav Novak Date: Wed, 1 Oct 2025 14:23:59 +0200 Subject: [PATCH 06/10] fix: fixed issues from PR --- backends/nxp/quantizer/neutron_quantizer.py | 2 +- .../test_sub_tensor_converter.py | 19 ++++++++++--------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/backends/nxp/quantizer/neutron_quantizer.py b/backends/nxp/quantizer/neutron_quantizer.py index 4a8a8a49236..2681e221869 100644 --- a/backends/nxp/quantizer/neutron_quantizer.py +++ b/backends/nxp/quantizer/neutron_quantizer.py @@ -189,7 +189,6 @@ def __init__(self): NeutronAtenQuantizer(AbsPattern(), static_qconfig), NeutronAtenQuantizer(AdaptiveAvgPoolPattern(), static_qconfig), NeutronAtenQuantizer(AddTensorPattern(), static_qconfig), - NeutronAtenQuantizer(SubTensorPattern(), static_qconfig), NeutronAtenQuantizer(AddmmPattern(), static_fc_qconfig), NeutronAtenQuantizer(AvgPoolPattern(), static_qconfig), NeutronAtenQuantizer(CatPattern(), static_qconfig), @@ -210,6 +209,7 @@ def __init__(self): NeutronAtenQuantizer(ReshapePattern(), static_qconfig), NeutronAtenQuantizer(SigmoidPattern(), static_qconfig), NeutronAtenQuantizer(SoftMaxPattern(), static_qconfig), + NeutronAtenQuantizer(SubTensorPattern(), static_qconfig), NeutronAtenQuantizer(TanhPattern(), static_qconfig), NeutronAtenQuantizer(TanhInPlacePattern(), static_qconfig), NeutronAtenQuantizer(ViewPattern(), static_qconfig), diff --git a/backends/nxp/tests/ir/converter/node_converter/test_sub_tensor_converter.py b/backends/nxp/tests/ir/converter/node_converter/test_sub_tensor_converter.py index faf6b457438..d2242a447d3 100644 --- a/backends/nxp/tests/ir/converter/node_converter/test_sub_tensor_converter.py +++ b/backends/nxp/tests/ir/converter/node_converter/test_sub_tensor_converter.py @@ -62,7 +62,7 @@ def test_sub_tensor_quant_conversion(mocker, input_shape): input_data = {0: input_data_1, 1: input_data_2} nodes = list(exported_program.graph.nodes) - assert nodes[4].name == "aten_sub_tensor" + assert nodes[4].target == exir_ops.edge.aten.sub.Tensor convert_run_compare( exported_program, tfl_model=tflite_flatbuffers_model, input_data=input_data @@ -95,7 +95,7 @@ def test_sub_tensor_one_input_quant_conversion(mocker, input_shape): input_data = (np.random.random(input_shape).astype(np.float32) * 50).astype(np.int8) nodes = list(exported_program.graph.nodes) - assert nodes[2].name == "aten_sub_tensor" + assert nodes[2].target == exir_ops.edge.aten.sub.Tensor convert_run_compare( exported_program, tfl_model=tflite_flatbuffers_model, input_data=input_data @@ -103,19 +103,20 @@ def test_sub_tensor_one_input_quant_conversion(mocker, input_shape): @pytest.mark.parametrize( - "x_input_shape, y_input_shape", + "x_input_shape", [ - pytest.param((1, 4, 8, 8), (1, 8, 8, 8), id="4D."), - pytest.param( - (1, 4, 5, 5), (1, 8, 5, 5), id="4D, product of dims is not a multiple of 8." - ), + pytest.param((1, 4, 8, 8), id="4D."), + pytest.param((1, 4, 5, 5), id="4D, product of dims is not a multiple of 8."), ], ) -def test_sub_tensor_w_conv_quant_conversion(mocker, x_input_shape, y_input_shape): +def test_sub_tensor_w_conv_quant_conversion(mocker, x_input_shape): model = SubTensorConvModule() converter_spy = mocker.spy(EdgeProgramToIRConverter, "convert_program") + n, c, w, h = x_input_shape + y_input_shape = (n, 8, w, h) + # Run conversion _ = to_quantized_edge_program(model, [x_input_shape, y_input_shape]) @@ -134,7 +135,7 @@ def test_sub_tensor_w_conv_quant_conversion(mocker, x_input_shape, y_input_shape input_data = {0: input_data_1, 1: input_data_2} nodes = list(exported_program.graph.nodes) - assert nodes[11].name == "aten_sub_tensor" + assert nodes[11].target == exir_ops.edge.aten.sub.Tensor convert_run_compare( exported_program, From ab21d6a373ae38c5d3bf0a1af69b16b09937d438 Mon Sep 17 00:00:00 2001 From: Vaclav Novak Date: Wed, 1 Oct 2025 14:37:31 +0200 Subject: [PATCH 07/10] fix: fixed issues from PR --- .../ir/converter/node_converter/test_sub_tensor_converter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backends/nxp/tests/ir/converter/node_converter/test_sub_tensor_converter.py b/backends/nxp/tests/ir/converter/node_converter/test_sub_tensor_converter.py index d2242a447d3..5ecf5855393 100644 --- a/backends/nxp/tests/ir/converter/node_converter/test_sub_tensor_converter.py +++ b/backends/nxp/tests/ir/converter/node_converter/test_sub_tensor_converter.py @@ -114,8 +114,8 @@ def test_sub_tensor_w_conv_quant_conversion(mocker, x_input_shape): converter_spy = mocker.spy(EdgeProgramToIRConverter, "convert_program") - n, c, w, h = x_input_shape - y_input_shape = (n, 8, w, h) + n, c, h, w = x_input_shape + y_input_shape = (n, 8, h, w) # Run conversion _ = to_quantized_edge_program(model, [x_input_shape, y_input_shape]) From 6f6ac9dcb9f45938b463efa8bb630c0a6bb2434f Mon Sep 17 00:00:00 2001 From: Martin Pavella Date: Thu, 2 Oct 2025 11:48:36 +0200 Subject: [PATCH 08/10] Fix type hints. --- backends/nxp/quantizer/patterns.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backends/nxp/quantizer/patterns.py b/backends/nxp/quantizer/patterns.py index 27007262252..590ddc491f0 100644 --- a/backends/nxp/quantizer/patterns.py +++ b/backends/nxp/quantizer/patterns.py @@ -231,11 +231,11 @@ class SubTensorPattern(QuantizationPattern): Basic quantization for all inputs and output. """ - def partition_types(self) -> List[Type[torch.nn.Module]]: + def partition_types(self) -> list[type[torch.nn.Module]]: return [torch.ops.aten.sub.Tensor] def get_anchors( - self, gm: fx.GraphModule, fused_partition: List[fx.GraphModule] + self, gm: fx.GraphModule, fused_partition: list[fx.GraphModule] ) -> PartitionAnchors | None: node = fused_partition[0].nodes[-1] inputs = [(node, 0)] From c588b7b8bd6e108bb1a6a11bc4e1c92562ca2359 Mon Sep 17 00:00:00 2001 From: Martin Pavella Date: Thu, 2 Oct 2025 14:53:26 +0200 Subject: [PATCH 09/10] Fix sub quantization pattern. --- backends/nxp/quantizer/patterns.py | 6 +++--- .../converter/node_converter/test_sub_tensor_converter.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/backends/nxp/quantizer/patterns.py b/backends/nxp/quantizer/patterns.py index 590ddc491f0..9588ce24c9e 100644 --- a/backends/nxp/quantizer/patterns.py +++ b/backends/nxp/quantizer/patterns.py @@ -231,16 +231,16 @@ class SubTensorPattern(QuantizationPattern): Basic quantization for all inputs and output. """ - def partition_types(self) -> list[type[torch.nn.Module]]: + def partition_types(self) -> list[torch.nn.Module]: return [torch.ops.aten.sub.Tensor] def get_anchors( self, gm: fx.GraphModule, fused_partition: list[fx.GraphModule] ) -> PartitionAnchors | None: node = fused_partition[0].nodes[-1] - inputs = [(node, 0)] + inputs = [(node, NodeArgsIdx(0))] if len(fused_partition[0].input_nodes) == 2: - inputs = [(node, 0), (node, 1)] + inputs = [(node, NodeArgsIdx(0)), (node, NodeArgsIdx(1))] return PartitionAnchors( inputs=inputs, diff --git a/backends/nxp/tests/ir/converter/node_converter/test_sub_tensor_converter.py b/backends/nxp/tests/ir/converter/node_converter/test_sub_tensor_converter.py index 5ecf5855393..98566ff1ad6 100644 --- a/backends/nxp/tests/ir/converter/node_converter/test_sub_tensor_converter.py +++ b/backends/nxp/tests/ir/converter/node_converter/test_sub_tensor_converter.py @@ -135,7 +135,7 @@ def test_sub_tensor_w_conv_quant_conversion(mocker, x_input_shape): input_data = {0: input_data_1, 1: input_data_2} nodes = list(exported_program.graph.nodes) - assert nodes[11].target == exir_ops.edge.aten.sub.Tensor + assert nodes[15].target == exir_ops.edge.aten.sub.Tensor convert_run_compare( exported_program, From 2b59bbecc98f360720767c129347018da36b9c11 Mon Sep 17 00:00:00 2001 From: Martin Pavella Date: Fri, 3 Oct 2025 08:43:50 +0200 Subject: [PATCH 10/10] Fix rebase. --- .../ops_converters/sub_tensor_converter.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/backends/nxp/backend/ir/converter/node_converters/ops_converters/sub_tensor_converter.py b/backends/nxp/backend/ir/converter/node_converters/ops_converters/sub_tensor_converter.py index 08c0df18120..e9522c87114 100644 --- a/backends/nxp/backend/ir/converter/node_converters/ops_converters/sub_tensor_converter.py +++ b/backends/nxp/backend/ir/converter/node_converters/ops_converters/sub_tensor_converter.py @@ -9,11 +9,11 @@ from executorch.backends.nxp.backend.ir.converter.node_converter import ( CustomDelegationOptions, NodeConverter, - Target, ) from executorch.backends.nxp.backend.ir.tflite_generator.builtin_options import ( sub_options, ) +from executorch.backends.nxp.backend.neutron_target_spec import NeutronTargetSpec from torch.fx import Node from torch.nn import Parameter @@ -22,20 +22,15 @@ class SubTensorConverter(NodeConverter): @staticmethod def _is_supported_on_target( node: Node, - target: Target, + neutron_target_spec: NeutronTargetSpec, parameters_mapping: dict[str, Parameter], custom_delegation_options: CustomDelegationOptions, ) -> bool: - match target: - case Target.RT700: - if node_uses_shape_broadcasting(node): - # Shape broadcasting may require the addition of `Transpose` ops during conversion. - return False - - return True + if node_uses_shape_broadcasting(node): + # Shape broadcasting may require the addition of `Transpose` ops during conversion. + return False - case _: - return False + return True @staticmethod def _is_supported_in_IR(