From 6521277e2f964f438f4052c9f1c1b083d5edcd2f Mon Sep 17 00:00:00 2001 From: Vaclav Novak Date: Thu, 21 May 2026 01:25:02 +0200 Subject: [PATCH 1/2] feat: enabled splitting of group convolution for bias=False --- .../aten_passes/split_group_convolution.py | 45 +++++++++++++------ 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/backends/nxp/aten_passes/split_group_convolution.py b/backends/nxp/aten_passes/split_group_convolution.py index 58c87730c84..22fc3a83cee 100644 --- a/backends/nxp/aten_passes/split_group_convolution.py +++ b/backends/nxp/aten_passes/split_group_convolution.py @@ -111,15 +111,27 @@ def _create_convolution_node(self, conv_target, args: tuple) -> Node: # Compute the output shapes for the `convolution`, and assign the `val` meta. with FakeTensorMode() as mode: input_shapes = [ - input_.meta["val"].shape if hasattr(input_, "meta") else input_.shape + ( + input_.meta["val"].shape + if hasattr(input_, "meta") + else input_.shape if input_ is not None else None + ) for input_ in args[:3] ] input_dtypes = [ - input_.meta["val"].dtype if hasattr(input_, "meta") else input_.dtype + ( + input_.meta["val"].dtype + if hasattr(input_, "meta") + else input_.dtype if input_ is not None else None + ) for input_ in args[:3] ] fake_inputs = [ - FakeTensor.from_tensor(torch.empty(shape, dtype=dtype), mode) + ( + FakeTensor.from_tensor(torch.empty(shape, dtype=dtype), mode) + if shape is not None and dtype is not None + else None + ) for shape, dtype in zip(input_shapes, input_dtypes) ] output = conv_target(*fake_inputs, *args[3:]) @@ -211,8 +223,11 @@ def _is_conv(node_: Node): w_data = self._get_tensor_constant_from_node(w) b_data = self._get_tensor_constant_from_node(b) - if w_data is None or b_data is None: - continue # Only the standard case with static weights and bias is supported. + + with_bias = b is not None + # Only the standard case with static weights and static bias (or bias=False) is supported. + if w_data is None or (b_data is None and with_bias): + continue # Create a `split` node to split the main input. # Split across dimension `1` (channels), `groups` slices of size `input_split_size`. @@ -227,10 +242,9 @@ def _is_conv(node_: Node): for i in range(groups) ] - # Split the weights and bias, across dimension `0`, slices of size `weight_split_size`. + # Split the weights across dimension `0`, slices of size `weight_split_size`. weight_split_size = w.meta["val"].shape[0] // groups split_weights_data = torch.split(w_data, weight_split_size, 0) - split_bias_data = torch.split(b_data, weight_split_size, 0) # Turn the weights and biases into parameter nodes containing the data. # Use a different name for every parameter. The function internally ensures the name's uniqueness, but @@ -241,12 +255,17 @@ def _is_conv(node_: Node): ) for i, weight_data in enumerate(split_weights_data) ] - split_bias_nodes = [ - self._create_parameter_node_for_data( - bias_data, b.name + f"_{i}_", split_node - ) - for i, bias_data in enumerate(split_bias_data) - ] + + if with_bias: + split_bias_data = torch.split(b_data, weight_split_size, 0) + split_bias_nodes = [ + self._create_parameter_node_for_data( + bias_data, b.name + f"_{i}_", split_node + ) + for i, bias_data in enumerate(split_bias_data) + ] + else: + split_bias_nodes = [None] * len(split_weight_nodes) # Create the `conv` nodes. with self.module.graph.inserting_after( From 735d329ac99bb30bb5f2f4cd1151023d2aeb0954 Mon Sep 17 00:00:00 2001 From: Vaclav Novak Date: Fri, 12 Jun 2026 13:17:16 +0200 Subject: [PATCH 2/2] feat: added support for `conv2d` using new NeutronC flow --- .../ops_converters/convolution_converter.py | 227 +- .../generic_tests/test_batch_norm_fusion.py | 2 +- .../test_neutron_converter_manager.py | 29 +- .../node_converter/test_conv_converter.py | 2037 +++++++++++++---- .../test_slice_tensor_converter.py | 9 +- .../test_view_copy_converter.py | 161 +- backends/nxp/tests/model_output_comparator.py | 8 +- 7 files changed, 1838 insertions(+), 635 deletions(-) diff --git a/backends/nxp/backend/ir/converter/node_converters/ops_converters/convolution_converter.py b/backends/nxp/backend/ir/converter/node_converters/ops_converters/convolution_converter.py index 5fa994be7ae..18b7c0e363e 100644 --- a/backends/nxp/backend/ir/converter/node_converters/ops_converters/convolution_converter.py +++ b/backends/nxp/backend/ir/converter/node_converters/ops_converters/convolution_converter.py @@ -48,78 +48,171 @@ from torch.nn import Parameter +# The arguments of the conv are: +# convolution( +# Tensor input, Tensor weight, Tensor? bias, +# SymInt[] stride, SymInt[] padding, SymInt[] dilation, +# bool transposed, SymInt[] output_padding, SymInt groups +# ) -> Tensor +Stride = Padding = Dilation = OutPadding = list[int] +Transposed = bool +Groups = int +ConvolutionArgs = tuple[ + Node, Node, Node | None, Stride, Padding, Dilation, Transposed, OutPadding, Groups +] + + class ConvolutionConverter(NodeConverter): @staticmethod - def _is_supported_on_target( + def _is_supported_on_target_regular_conv( + node: Node, + parameters_mapping: dict[str, Parameter], + ) -> bool: + ( + inp_node, + w_node, + b_node, + stride, + _, + dilation, + _, + _, + _, + ) = ConvolutionConverter._get_convolution_arguments(node) + + # Input must be INT8/UINT8 + # Output must be INT8/UINT8 + inp_out_supported_types = [torch.int8, torch.uint8] + if not NodeConverter.uses_quantization_type_for_io( + node, inp_out_supported_types, [0], [0] + ): + return False + + # Weights must be INT8 + w_supported_types = [torch.int8] + if not NodeConverter.uses_quantization_type_for_io( + node, w_supported_types, [1], [] + ): + return False + + # Bias must be INT32 + if b_node is not None: + b_supported_types = [torch.int32] + if not NodeConverter.uses_quantization_type_for_io( + node, b_supported_types, [2], [] + ): + return False + + # Weights must be constant + if not node_is_effectively_static_tensor(w_node, parameters_mapping): + return False + + # Bias must be constant (if present) + if b_node is not None and not node_is_effectively_static_tensor( + b_node, parameters_mapping + ): + return False + + # kernelH <= 4096, kernelW <= 4096 + # strideH <= 4096, strideW <= 4096 + # dilationH <= 4096, dilationW <= 4096 + w_node_shape = w_node.meta["val"].shape + + kernel_h = w_node_shape[2] + kernel_w = w_node_shape[3] + stride_h = stride[0] + stride_w = stride[1] + dilation_h = dilation[0] + dilation_w = dilation[1] + + dim_sizes = [kernel_h, kernel_w, stride_h, stride_w, dilation_h, dilation_w] + + if any(dim > 4096 for dim in dim_sizes): + return False + + # kernelH * kernelW * inpC <= 65535 + inp_node_shape = inp_node.meta["val"].shape + inp_channels = ( + inp_node_shape[1] if len(inp_node_shape) == 4 else inp_node_shape[0] + ) + + if kernel_h * kernel_w * inp_channels > 65535: + return False + + return True + + @staticmethod + def _is_supported_on_target_transp_conv( node: Node, neutron_target_spec: NeutronTargetSpec, parameters_mapping: dict[str, Parameter], - custom_delegation_options: CustomDelegationOptions, ) -> bool: + # TODO: EIEX-894 update the requirements of delegation for new Neutron flow + _, w_node, _, stride, padding, dilation, transposed, _, groups = ( + ConvolutionConverter._get_convolution_arguments(node) + ) + num_macs = neutron_target_spec.get_num_macs() node_t_params = get_node_tensor_params(node) - weights = node.args[1] - conv_params = ConvParameters( - *ConvolutionConverter._get_convolution_arguments(node) - ) if node_t_params["batch_size"] != 1: - # Only batch size 1 is supported on neutron. + # Only TransposeConv2d with batch size = 1 is supported on neutron. return False - if conv_params.transposed: - # TransposeConv2d with groups > 1 is not supported - # TODO: split into multiple convs with groups = 1 - if conv_params.groups > 1: - return False - if not node_is_effectively_static_tensor(weights, parameters_mapping): - # Only supported if the weights are static, because TFLite `TransposeConv` uses permuted - # weights. In case the weights are dynamic, a Transpose operator would have to be added, which - # is not supported on Neutron. - return False - # neutron-library/src/utils/NeutronLibraryInterrogation.cpp#876 TransposeConv2DKernelKind - if ( - conv_params.dilation != [1, 1] - or conv_params.padding[0] != 0 - or conv_params.padding[1] >= node_t_params["kernel_width"] - or ( - conv_params.padding[1] != 0 and node_t_params["inp_height"] != 1 - ) # Slice added by explicit padding - or conv_params.stride[0] != 1 - or ( - ( - conv_params.stride[1] != node_t_params["kernel_width"] / 2 - or node_t_params["out_height"] != 1 - ) - and conv_params.stride[1] != node_t_params["kernel_width"] - ) - or conv_params.stride[1] % 2 != 0 - or node_t_params["inp_channels"] % num_macs != 0 - or node_t_params["out_channels"] % num_macs != 0 - or node_t_params["kernel_width"] % 2 != 0 - or node_t_params["kernel_height"] != 1 - ): - return False - elif conv_params.groups == 1: # Regular convolution. - pass - elif conv_utils.group_conv_convertible_as_depthwise( - node, conv_params.groups - ): # Depthwise convolution. - # Only supported if the weights are static, because TFLite `DepthwiseConv2D` uses permuted + # TransposeConv2d with groups > 1 is not supported + # TODO: split into multiple convs with groups = 1 + if groups > 1: + return False + if not node_is_effectively_static_tensor(w_node, parameters_mapping): + # Only supported if the weights are static, because TFLite `TransposeConv` uses permuted # weights. In case the weights are dynamic, a Transpose operator would have to be added, which # is not supported on Neutron. - if not node_is_effectively_static_tensor(weights, parameters_mapping): - return False - elif conv_utils.group_conv_convertible_into_multiple_convolutions( - node, conv_params.groups - ): # Separable conv. - # Requires addition of `Split` and `Concatenation` operators, which are not supported on Neutron. return False - else: # Unexpected case (should never happen). + # neutron-library/src/utils/NeutronLibraryInterrogation.cpp#876 TransposeConv2DKernelKind + if ( + dilation != [1, 1] + or padding[0] != 0 + or padding[1] >= node_t_params["kernel_width"] + or ( + padding[1] != 0 and node_t_params["inp_height"] != 1 + ) # Slice added by explicit padding + or stride[0] != 1 + or ( + ( + stride[1] != node_t_params["kernel_width"] / 2 + or node_t_params["out_height"] != 1 + ) + and stride[1] != node_t_params["kernel_width"] + ) + or stride[1] % 2 != 0 + or node_t_params["inp_channels"] % num_macs != 0 + or node_t_params["out_channels"] % num_macs != 0 + or node_t_params["kernel_width"] % 2 != 0 + or node_t_params["kernel_height"] != 1 + ): return False return True + @staticmethod + def _is_supported_on_target( + node: Node, + neutron_target_spec: NeutronTargetSpec, + parameters_mapping: dict[str, Parameter], + custom_delegation_options: CustomDelegationOptions, + ) -> bool: + is_transposed = (ConvolutionConverter._get_convolution_arguments(node))[6] + + if is_transposed: + return ConvolutionConverter._is_supported_on_target_transp_conv( + node, neutron_target_spec, parameters_mapping + ) + + else: + return ConvolutionConverter._is_supported_on_target_regular_conv( + node, parameters_mapping + ) + @staticmethod def _is_supported_in_IR( node: Node, @@ -149,10 +242,6 @@ def _is_supported_in_IR( return True - Stride = Padding = Dilation = OutPadding = list[int] - Transposed = bool - Groups = int - def _compute_slicing_params( self, output_shape, explicit_padding ) -> tuple[list[int], list[int]]: @@ -170,14 +259,14 @@ def _compute_slicing_params( @staticmethod def _get_convolution_arguments( conv_node: Node, - ) -> (Stride, Padding, Dilation, Transposed, OutPadding, Groups): - # The arguments of the conv are: - # [x, w, b, stride, padding, dilation, transposed, output padding, groups] - # https://github.com/pytorch/pytorch/blob/v2.6.0/aten/src/ATen/native/Convolution.cpp#L286-L291 - _, _, _, stride, padding, dilation, transposed, out_padding, groups = ( + ) -> ConvolutionArgs: + x, w, b, stride, padding, dilation, transposed, out_padding, groups = ( conv_node.args ) return ( + x, + w, + b, list(stride), list(padding), list(dilation), @@ -380,16 +469,8 @@ def _convert_2d_conv( elif conv_utils.group_conv_convertible_into_multiple_convolutions( t_op, conv_params.groups - ): # Convert to separated `Conv2D`. - t_op.builtin_options = conv_2d_options.Conv2D() - - return conv_utils.create_separated_convolutions_based_on_group( - t_op, - conv_params, - self.builder, - self._convert_unpadded_2D, - conv_utils.conv_op_factory, - ) + ): + raise RuntimeError("NXP backend: Group convolution was not decomposed.") else: # Convert to regular `Conv2D`. @@ -419,7 +500,7 @@ def _convert_2d_conv( def convert(self, node: Node): self.assert_convertible(node) - stride, padding, dilation, transposed, out_padding, groups = ( + _, _, _, stride, padding, dilation, transposed, out_padding, groups = ( self._get_convolution_arguments(node) ) diff --git a/backends/nxp/tests/generic_tests/test_batch_norm_fusion.py b/backends/nxp/tests/generic_tests/test_batch_norm_fusion.py index 5648f29b9be..64cd7ce83f0 100644 --- a/backends/nxp/tests/generic_tests/test_batch_norm_fusion.py +++ b/backends/nxp/tests/generic_tests/test_batch_norm_fusion.py @@ -112,7 +112,7 @@ def test_batch_norm_conv_fusing__full_pipeline__1d(bias: bool): module, tuple(input_shape) ).exported_program() - assert len(edge_program.graph.nodes) == 21 + assert len(edge_program.graph.nodes) == 7 assert not graph_contains_any_of_ops(edge_program.graph, batch_norm_target_ops) diff --git a/backends/nxp/tests/generic_tests/test_neutron_converter_manager.py b/backends/nxp/tests/generic_tests/test_neutron_converter_manager.py index 0705203db06..8bd3446da7a 100644 --- a/backends/nxp/tests/generic_tests/test_neutron_converter_manager.py +++ b/backends/nxp/tests/generic_tests/test_neutron_converter_manager.py @@ -6,38 +6,11 @@ import multiprocessing import pickle -import torch -from executorch import exir -from executorch.backends.nxp.backend.edge_program_converter import ( - EdgeProgramToIRConverter, -) from executorch.backends.nxp.backend.neutron_converter_manager import ( NeutronConverterManager, ) -from executorch.backends.nxp.backend.node_format_inference import NodeFormatInference from executorch.backends.nxp.tests.executorch_pipeline import to_quantized_edge_program -from executorch.backends.nxp.tests.models import Conv2dModule, LinearModule - - -def test_conv2d_neutron_conversion(): - model = Conv2dModule() - - example_input = (torch.ones(1, 4, 32, 32),) - exir_program = torch.export.export(model, example_input) - edge_program_manager = exir.to_edge(exir_program) - - NodeFormatInference(edge_program_manager.exported_program()).identify_node_formats() - edge_program_converter = EdgeProgramToIRConverter() - tflite_model, _ = edge_program_converter.convert_program( - edge_program_manager.exported_program() - ) - - neutron_converter_manager = NeutronConverterManager() - neutron_model = neutron_converter_manager.convert(tflite_model, "imxrt700", False) - - assert len( - neutron_model - ), "Produced NeutronGraph-based TFLite model has zero length!" +from executorch.backends.nxp.tests.models import LinearModule def test_conv2d_neutron_conversion__prefetching(mocker): diff --git a/backends/nxp/tests/ir/converter/node_converter/test_conv_converter.py b/backends/nxp/tests/ir/converter/node_converter/test_conv_converter.py index 828647d2113..b28c6d7f419 100644 --- a/backends/nxp/tests/ir/converter/node_converter/test_conv_converter.py +++ b/backends/nxp/tests/ir/converter/node_converter/test_conv_converter.py @@ -6,30 +6,27 @@ import numpy as np import pytest import torch - -from executorch.backends.nxp.backend.edge_program_converter import ( - EdgeProgramToIRConverter, -) -from executorch.backends.nxp.backend.ir.conversion_config import ConversionConfig -from executorch.backends.nxp.backend.ir.converter.builder.model_builder import ( - ModelBuilder, -) -from executorch.backends.nxp.backend.ir.lib.tflite.BuiltinOperator import ( - BuiltinOperator, -) -from executorch.backends.nxp.tests.executorch_pipeline import ( - to_edge_program, - to_quantized_edge_program, -) +from executorch.backends.nxp.tests.dataset_creator import RandomDatasetCreator +from executorch.backends.nxp.tests.executorch_pipeline import to_quantized_edge_program from executorch.backends.nxp.tests.executors import ( convert_run_compare, + EdgeProgramToIRConverter, + ExportedProgram, graph_contains_any_of_ops, ToChannelFirstPreprocess, ToChannelLastPreprocess, ) +from executorch.backends.nxp.tests.graph_verifier import DetailedGraphVerifier from executorch.backends.nxp.tests.models import Conv2dModule -from executorch.exir.dialects._ops import ops as exir_ops -from torch.export import ExportedProgram +from executorch.backends.nxp.tests.nsys_testing import ( + AllCloseOutputComparator, + lower_run_compare, +) +from executorch.backends.nxp.tests.ops_aliases import ( + Convolution, + ExecutorchDelegateCall, + ViewCopy, +) from executorch.backends.nxp.tests.use_qat import * # noqa F403 @@ -39,444 +36,1616 @@ def reseed_model_per_test_run(): np.random.seed(23) -@pytest.mark.parametrize( - "model, input_shape", - [ - pytest.param( - Conv2dModule(in_channels=8, out_channels=32, kernel_size=5), - (1, 8, 32, 32), - id="In ch 8, out ch 32, kernel 5", - ), - pytest.param( - Conv2dModule(in_channels=8, out_channels=32, kernel_size=5, padding=3), - (1, 8, 32, 32), - id="In ch 8, out ch 32, kernel 5, padding 3", - ), - pytest.param( - Conv2dModule(in_channels=8, out_channels=32, kernel_size=5, padding=(2, 3)), - (1, 8, 31, 31), - id="In ch 8, out ch 32, kernel 5, padding (2, 3)", - ), - pytest.param( - Conv2dModule( - in_channels=8, - out_channels=32, - kernel_size=5, - padding=(2, 3), - dilation=(1, 2), - ), - (1, 8, 31, 31), - id="In ch 8, out ch 32, kernel 5, padding (2, 3), dilation (1, 2)", - ), - pytest.param( - Conv2dModule( - in_channels=16, out_channels=32, kernel_size=3, padding=2, dilation=2 - ), - (1, 16, 32, 32), - id="In ch 16, out ch 32, kernel 3, padding 2, dilation 2", - ), - pytest.param( - Conv2dModule(in_channels=32, out_channels=32, kernel_size=3, dilation=2), - (1, 32, 32, 32), - id="In ch 32, out ch 32, kernel 3, dilation 2", - ), - pytest.param( - Conv2dModule( - in_channels=32, - out_channels=32, - kernel_size=3, - padding=(0, 1), - dilation=2, - ), - (1, 32, 35, 35), - id="In ch 32, out ch 32, kernel 3, padding (0, 1), dilation 2", - ), - pytest.param( - Conv2dModule( - in_channels=32, - out_channels=32, - kernel_size=3, - padding=(1, 0), - dilation=(3, 1), - ), - (1, 32, 35, 35), - id="In ch 32, out ch 32, kernel 3, padding (1, 0), dilation (3, 1)", - ), - pytest.param( - Conv2dModule( - in_channels=32, out_channels=32, kernel_size=3, dilation=(2, 3) - ), - (1, 32, 32, 32), - id="In ch 32, out ch 32, kernel 3, dilation (2, 3)", - ), - pytest.param( - Conv2dModule(in_channels=32, out_channels=64, kernel_size=4), - (1, 32, 32, 32), - id="In ch 32, out ch 32, kernel 4", - ), - pytest.param( - Conv2dModule( - in_channels=32, out_channels=64, kernel_size=4, padding=(1, 2) - ), - (1, 32, 33, 33), - id="In ch 32, out ch 32, kernel 4, padding (1, 2)", - ), - pytest.param( - Conv2dModule( - in_channels=32, out_channels=64, kernel_size=4, padding=(1, 0) - ), - (1, 32, 33, 33), - id="In ch 32, out ch 32, kernel 4, padding (1, 0)", - ), - pytest.param( - Conv2dModule( - in_channels=32, out_channels=64, kernel_size=4, padding=(0, 2) - ), - (1, 32, 32, 32), - id="In ch 32, out ch 32, kernel 4, padding (0, 2)", - ), - pytest.param( - Conv2dModule( - in_channels=32, - out_channels=64, - kernel_size=4, - padding=(0, 2), - dilation=(1, 2), - ), - (1, 32, 32, 32), - id="In ch 32, out ch 32, kernel 4, padding (0, 2), dilation (1, 2)", - ), - pytest.param( - Conv2dModule( - in_channels=8, out_channels=32, kernel_size=5, padding=3, bias=False - ), - (1, 8, 32, 32), - id="In ch 8, out ch 32, kernel 5, padding 3, no bias", - ), - pytest.param( - Conv2dModule( - in_channels=32, - out_channels=32, - kernel_size=3, - padding=(1, 0), - dilation=(3, 1), - bias=False, - ), - (1, 32, 35, 35), - id="In ch 32, out ch 32, kernel 3, padding (1, 0), dilation (3, 1)," - "no bias", - ), - ], -) -def test_conv2d_quant_conversion(mocker, model: torch.nn.Module, input_shape, use_qat): - converter_spy = mocker.spy(EdgeProgramToIRConverter, "convert_program") +class TestTransposedConvFromLegacyFlow: + @pytest.mark.parametrize( + "model, input_shape", + [ + pytest.param( + torch.nn.ConvTranspose2d(8, 16, (1, 4), stride=(1, 2)), + (1, 8, 1, 16), + id="In ch 8, out ch 16, kernel (1, 4), stride (1, 2)", + ), + pytest.param( + torch.nn.ConvTranspose2d(64, 64, (1, 2), stride=(1, 2)), + (1, 64, 3, 12), + id="In ch 64, out ch 64, kernel (1, 2), stride (1, 2)", + ), + pytest.param( + torch.nn.ConvTranspose2d(16, 40, (1, 4), stride=(1, 2), padding=(0, 1)), + (1, 16, 1, 27), + id="In ch 16, out ch 40, kernel (1, 4), stride (1, 2), padding (0, 1)", + ), + pytest.param( + torch.nn.ConvTranspose2d(8, 16, (1, 4), stride=(1, 2), padding=(0, 1)), + (1, 8, 1, 16), + id="In ch 8, out ch 16, kernel (1, 4), stride (1, 2), padding (0, 1)", + ), + pytest.param( + torch.nn.ConvTranspose2d( + 8, 16, (1, 4), stride=(1, 2), output_padding=(0, 1) + ), + (1, 8, 1, 16), + id="In ch 8, out ch 16, kernel (1, 8), stride (1, 2), output_padding (0, 1)", + ), + pytest.param( + torch.nn.ConvTranspose2d(16, 16, (1, 4), stride=(1, 2)), + (1, 16, 1, 16), + id="In ch 16, out ch 16, kernel (1, 4), stride (1, 2)", + ), + pytest.param( + torch.nn.ConvTranspose2d(8, 16, (1, 4), stride=(1, 2), bias=False), + (1, 8, 1, 16), + id="In ch 8, out ch 16, kernel (1, 4), stride (1, 2), no bias", + ), + pytest.param( + torch.nn.ConvTranspose2d( + 8, 16, (1, 4), stride=(1, 2), padding=(0, 1), bias=False + ), + (1, 8, 1, 16), + id="In ch 8, out ch 16, kernel (1, 4), stride (1, 2)," + "padding (0, 1), no bias", + ), + ], + ) + def test_conv_transpose2d_conversion__quantized( + self, mocker, model: torch.nn.Module, input_shape, use_qat + ): + converter_spy = mocker.spy(EdgeProgramToIRConverter, "convert_program") + + edge_program = to_quantized_edge_program( + model, input_shape, use_qat=use_qat, use_neutron_for_format_conversion=False + ).exported_program() + + # Make sure the `TransposeConv` was delegated. + assert not graph_contains_any_of_ops( + graph=edge_program.graph, ops=[Convolution] + ) + assert graph_contains_any_of_ops( + graph=edge_program.graph, ops=[ExecutorchDelegateCall] + ) + + # 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, + tflite_input_preprocess=ToChannelLastPreprocess(), + tfl_model=tflite_flatbuffers_model, + tflite_output_preprocess=ToChannelFirstPreprocess(), + input_data=input_data, + atol=1.0, + ) + + @pytest.mark.parametrize( + "model, input_shape", + [ + pytest.param( + torch.nn.ConvTranspose2d(8, 16, (1, 4), stride=(1, 2), dilation=(1, 2)), + (1, 8, 1, 16), + id="In ch 8, out ch 16, kernel (1, 4), stride (1, 2), " + "dilation (1, 2) - Dilation != (1, 1)", + ), + pytest.param( + torch.nn.ConvTranspose2d(6, 16, (1, 4), stride=(1, 2)), + (1, 6, 1, 16), + id="In ch 6, out ch 16, kernel (1, 4), stride (1, 2) - In channels % num_macs != 0", + ), + pytest.param( + torch.nn.ConvTranspose2d(8, 16, (1, 4), stride=(1, 2)), + (1, 8, 4, 16), + id="In ch 8, out ch 16, kernel (1, 4), stride (1, 2) - Out height != 1, stride width" + " != kernel width", + ), + pytest.param( + torch.nn.ConvTranspose2d(8, 16, (2, 4), stride=(1, 2), padding=(0, 1)), + (1, 8, 1, 16), + id="In ch 8, out ch 16, kernel (2, 4), stride (1, 2), padding " + "(0, 1) - Out height != 1, stride width != kernel width", + ), + pytest.param( + torch.nn.ConvTranspose2d(8, 16, (1, 5), stride=(1, 4)), + (1, 8, 1, 16), + id="In ch 8, out ch 16, kernel (1, 5), stride (1, 4) - Stride width != kernel width / 2" + ", stride width != kernel width", + ), + pytest.param( + torch.nn.ConvTranspose2d(16, 12, (1, 4), stride=(3, 3)), + (1, 16, 1, 16), + id="In ch 16, out ch 12, kernel (1, 4), stride (3, 3) - Out channels % num_macs != 0", + ), + pytest.param( + torch.nn.ConvTranspose2d(64, 64, (1, 4), stride=(1, 2)), + (1, 64, 3, 12), + id="In ch 64, out ch 64, kernel (1, 4), stride (1, 2) - Out height != 1, stride width" + " != kernel width", + ), + pytest.param( + torch.nn.ConvTranspose2d(16, 40, (1, 4), stride=(1, 4), padding=(0, 1)), + (1, 16, 4, 27), + id="In ch 16, out ch 40, kernel (1, 4), stride (1, 4), padding (0, 1) - Padding width " + "!= 1 and input height != 1", + ), + ], + ) + def test_conv_transpose2d_non_delegated_conversion__quantized( + self, model: torch.nn.Module, input_shape, use_qat + ): + edge_program = to_quantized_edge_program( + model, input_shape, use_qat=use_qat + ).exported_program() + + nodes = list(edge_program.graph.nodes) + assert len(nodes) == 15 + assert nodes[11].target == Convolution # TransposeConv not delegated. + + +class TestConv: + @staticmethod + def _conv_id(ins, oc, ks=3, s=2, d=1, p=0, b=True, g=1): + return ( + f"input_shape={ins}, " + f"out_channels={oc}, " + f"kernel_size={ks}, " + f"stride={s}, " + f"dilation={d}, " + f"padding={p}, " + f"bias={b}, " + f"group={g}" + ) + + @staticmethod + def assert_delegated_and_correct(model, input_shape, mocker, use_qat): + graph_verifier = DetailedGraphVerifier( + mocker, + expected_delegated_ops={Convolution: 1}, + expected_non_delegated_ops={}, + ) + dataset = RandomDatasetCreator(low=-256, high=256) + comparator = AllCloseOutputComparator(atol=1) + + lower_run_compare( + model, + input_shape, + graph_verifier, + dataset, + comparator, + use_qat=use_qat, + ) + + @staticmethod + def assert_not_delegated(model, input_shape, use_qat): + delegated_ep = to_quantized_edge_program( + model, + input_shape, + use_qat=use_qat, + ).exported_program() + + # Make sure the `convolution` was NOT delegated. + assert not graph_contains_any_of_ops( + delegated_ep.graph, [ExecutorchDelegateCall] + ) + assert graph_contains_any_of_ops(delegated_ep.graph, [Convolution]) + + @pytest.mark.parametrize( + "input_shape, out_channels, is_qat", + [ + pytest.param( + ins := (1, 8, 16, 24), + oc := 8, + qat := True, + id=f"qat={qat}, basic inference: " + _conv_id(ins, oc), + ), + pytest.param( + ins := (1, 8, 16, 24), + oc := 8, + qat := False, + id=f"qat={qat}, basic inference: " + _conv_id(ins, oc), + ), + pytest.param( + ins := (8, 16, 8, 32), + oc := 16, + qat := True, + id=f"qat={qat}, basic inference: " + _conv_id(ins, oc), + marks=pytest.mark.xfail( + reason="AIR-14679", + strict=True, + ), + ), + pytest.param( + ins := (8, 16, 8, 32), + oc := 16, + qat := False, + id=f"qat={qat}, basic inference: " + _conv_id(ins, oc), + marks=pytest.mark.xfail( + reason="AIR-14679", + strict=True, + ), + ), + pytest.param( + ins := (16, 8, 32, 64), + oc := 32, + qat := True, + id=f"qat={qat}, basic inference: " + _conv_id(ins, oc), + marks=pytest.mark.xfail( + reason="AIR-14679", + strict=True, + ), + ), + pytest.param( + ins := (16, 8, 32, 64), + oc := 32, + qat := False, + id=f"qat={qat}, basic inference: " + _conv_id(ins, oc), + marks=pytest.mark.xfail( + reason="AIR-14679", + strict=True, + ), + ), + pytest.param( + ins := (1, 8, 32, 64), + oc := 16, + qat := True, + id=f"qat={qat}, basic inference: " + _conv_id(ins, oc), + ), + pytest.param( + ins := (1, 8, 32, 64), + oc := 16, + qat := False, + id=f"qat={qat}, basic inference: " + _conv_id(ins, oc), + ), + pytest.param( + ins := (1, 32, 48, 8), + oc := 24, + qat := True, + id=f"qat={qat}, basic inference: " + _conv_id(ins, oc), + marks=pytest.mark.xfail( + reason="AIR-14679", + strict=True, + ), + ), + pytest.param( + ins := (1, 32, 48, 8), + oc := 24, + qat := False, + id=f"qat={qat}, basic inference: " + _conv_id(ins, oc), + ), + ], + ) + def test__basic_nsys_inference(self, input_shape, out_channels, is_qat, mocker): + in_channels = input_shape[1] + model = Conv2dModule(in_channels=in_channels, out_channels=out_channels) + + self.assert_delegated_and_correct(model, input_shape, mocker, is_qat) - # Run conversion - _ = to_quantized_edge_program( - model, input_shape, use_qat=use_qat, use_neutron_for_format_conversion=False + @pytest.mark.parametrize( + "input_shape, is_qat", + [ + pytest.param( + ins := (1, 8, 16, 24), + qat := True, + id=f"qat={qat}, basic inference, depthwise: " + + _conv_id(ins, ins[1], g=ins[1]), + ), + pytest.param( + ins := (1, 8, 16, 24), + qat := False, + id=f"qat={qat}, basic inference, depthwise: " + + _conv_id(ins, ins[1], g=ins[1]), + marks=pytest.mark.xfail( + reason="AIR-14679", + strict=True, + ), + ), + pytest.param( + ins := (8, 16, 8, 32), + qat := True, + id=f"qat={qat}, basic inference, depthwise: " + + _conv_id(ins, ins[1], g=ins[1]), + marks=pytest.mark.xfail( + reason="AIR-14679", + strict=True, + ), + ), + pytest.param( + ins := (8, 16, 8, 32), + qat := False, + id=f"qat={qat}, basic inference, depthwise: " + + _conv_id(ins, ins[1], g=ins[1]), + marks=pytest.mark.xfail( + reason="AIR-14679", + strict=True, + ), + ), + pytest.param( + ins := (16, 8, 32, 64), + qat := True, + id=f"qat={qat}, basic inference, depthwise: " + + _conv_id(ins, ins[1], g=ins[1]), + marks=pytest.mark.xfail( + reason="AIR-14679", + strict=True, + ), + ), + pytest.param( + ins := (16, 8, 32, 64), + qat := False, + id=f"qat={qat}, basic inference, depthwise: " + + _conv_id(ins, ins[1], g=ins[1]), + marks=pytest.mark.xfail( + reason="AIR-14679", + strict=True, + ), + ), + pytest.param( + ins := (1, 16, 32, 64), + qat := True, + id=f"qat={qat}, basic inference, depthwise: " + + _conv_id(ins, ins[1], g=ins[1]), + marks=pytest.mark.xfail( + reason="AIR-14679", + strict=True, + ), + ), + pytest.param( + ins := (1, 16, 32, 64), + qat := False, + id=f"qat={qat}, basic inference, depthwise: " + + _conv_id(ins, ins[1], g=ins[1]), + ), + pytest.param( + ins := (1, 32, 48, 8), + qat := True, + id=f"qat={qat}, basic inference, depthwise: " + + _conv_id(ins, ins[1], g=ins[1]), + marks=pytest.mark.xfail( + reason="AIR-14679", + strict=True, + ), + ), + pytest.param( + ins := (1, 32, 48, 8), + qat := False, + id=f"qat={qat}, basic inference, depthwise: " + + _conv_id(ins, ins[1], g=ins[1]), + ), + ], ) + def test__basic_nsys_inference_depthwise(self, input_shape, is_qat, mocker): + out_channels = input_shape[1] + group = input_shape[1] + model = Conv2dModule( + in_channels=input_shape[1], out_channels=out_channels, group=group + ) - # Capture generated model - tflite_flatbuffers_model, io_formats = converter_spy.spy_return + self.assert_delegated_and_correct(model, input_shape, mocker, is_qat) - # Capture converted program - exported_program: ExportedProgram = converter_spy.call_args.args[1] + @pytest.mark.parametrize( + "input_shape, out_channels, is_qat", + [ + pytest.param( + ins := (1, 3, 7, 14), + oc := 3, + qat := True, + id=f"qat={qat}, unusual shape inference: " + _conv_id(ins, oc), + ), + pytest.param( + ins := (1, 3, 7, 14), + oc := 3, + qat := False, + id=f"qat={qat}, unusual shape inference: " + _conv_id(ins, oc), + ), + pytest.param( + ins := (2, 3, 13, 27), + oc := 7, + qat := True, + id=f"qat={qat}, unusual shape inference: " + _conv_id(ins, oc), + ), + pytest.param( + ins := (2, 3, 13, 27), + oc := 7, + qat := False, + id=f"qat={qat}, unusual shape inference: " + _conv_id(ins, oc), + ), + pytest.param( + ins := (3, 7, 3, 14), + oc := 4, + qat := True, + id=f"qat={qat}, unusual shape inference: " + _conv_id(ins, oc), + ), + pytest.param( + ins := (3, 7, 3, 14), + oc := 4, + qat := False, + id=f"qat={qat}, unusual shape inference: " + _conv_id(ins, oc), + ), + pytest.param( + ins := (1, 7, 7, 21), + oc := 1, + qat := True, + id=f"qat={qat}, unusual shape inference: " + _conv_id(ins, oc), + ), + pytest.param( + ins := (1, 7, 7, 21), + oc := 1, + qat := False, + id=f"qat={qat}, unusual shape inference: " + _conv_id(ins, oc), + ), + pytest.param( + ins := (7, 7, 7, 7), + oc := 10, + qat := True, + id=f"qat={qat}, unusual shape inference: " + _conv_id(ins, oc), + ), + pytest.param( + ins := (7, 7, 7, 7), + oc := 10, + qat := False, + id=f"qat={qat}, unusual shape inference: " + _conv_id(ins, oc), + ), + pytest.param( + ins := (4, 21, 13, 17), + oc := 27, + qat := True, + id=f"qat={qat}, unusual shape inference: " + _conv_id(ins, oc), + marks=pytest.mark.xfail( + reason="AIR-14679", + strict=True, + ), + ), + pytest.param( + ins := (4, 21, 13, 17), + oc := 27, + qat := False, + id=f"qat={qat}, unusual shape inference: " + _conv_id(ins, oc), + marks=pytest.mark.xfail( + reason="AIR-14679", + strict=True, + ), + ), + ], + ) + def test__basic_nsys_inference__unusual_shapes( + self, input_shape, out_channels, is_qat, mocker + ): + model = Conv2dModule(in_channels=input_shape[1], out_channels=out_channels) - input_data = (np.random.random(input_shape).astype(np.float32) * 50).astype(np.int8) + self.assert_delegated_and_correct(model, input_shape, mocker, is_qat) - convert_run_compare( - exported_program, - tflite_input_preprocess=ToChannelLastPreprocess(), - tfl_model=tflite_flatbuffers_model, - tflite_output_preprocess=ToChannelFirstPreprocess(), - input_data=input_data, - atol=1.0, + @pytest.mark.parametrize( + "input_shape, is_qat", + [ + pytest.param( + ins := (1, 3, 7, 14), + qat := True, + id=f"qat={qat}, unusual shape inference, depthwise: " + + _conv_id(ins, ins[1], g=ins[1]), + ), + pytest.param( + ins := (1, 3, 7, 14), + qat := False, + id=f"qat={qat}, unusual shape inference, depthwise: " + + _conv_id(ins, ins[1], g=ins[1]), + ), + pytest.param( + ins := (2, 3, 13, 27), + qat := True, + id=f"qat={qat}, unusual shape inference, depthwise: " + + _conv_id(ins, ins[1], g=ins[1]), + ), + pytest.param( + ins := (2, 3, 13, 27), + qat := False, + id=f"qat={qat}, unusual shape inference, depthwise: " + + _conv_id(ins, ins[1], g=ins[1]), + ), + pytest.param( + ins := (3, 7, 3, 14), + qat := True, + id=f"qat={qat}, unusual shape inference, depthwise: " + + _conv_id(ins, ins[1], g=ins[1]), + ), + pytest.param( + ins := (3, 7, 3, 14), + qat := False, + id=f"qat={qat}, unusual shape inference, depthwise: " + + _conv_id(ins, ins[1], g=ins[1]), + ), + pytest.param( + ins := (1, 7, 7, 21), + qat := True, + id=f"qat={qat}, unusual shape inference, depthwise: " + + _conv_id(ins, ins[1], g=ins[1]), + ), + pytest.param( + ins := (1, 7, 7, 21), + qat := False, + id=f"qat={qat}, unusual shape inference, depthwise: " + + _conv_id(ins, ins[1], g=ins[1]), + ), + pytest.param( + ins := (7, 7, 7, 7), + qat := True, + id=f"qat={qat}, unusual shape inference, depthwise: " + + _conv_id(ins, ins[1], g=ins[1]), + ), + pytest.param( + ins := (7, 7, 7, 7), + qat := False, + id=f"qat={qat}, unusual shape inference, depthwise: " + + _conv_id(ins, ins[1], g=ins[1]), + ), + pytest.param( + ins := (4, 21, 13, 17), + qat := True, + id=f"qat={qat}, unusual shape inference, depthwise: " + + _conv_id(ins, ins[1], g=ins[1]), + marks=pytest.mark.xfail( + reason="AIR-14679", + strict=True, + ), + ), + pytest.param( + ins := (4, 21, 13, 17), + qat := False, + id=f"qat={qat}, unusual shape inference, depthwise: " + + _conv_id(ins, ins[1], g=ins[1]), + marks=pytest.mark.xfail( + reason="AIR-14679", + strict=True, + ), + ), + ], ) + def test__basic_nsys_inference_depthwise__unusual_shapes( + self, input_shape, is_qat, mocker + ): + out_channels = input_shape[1] + group = input_shape[1] + model = Conv2dModule( + in_channels=input_shape[1], out_channels=out_channels, group=group + ) -@pytest.mark.parametrize("bias", [False, True]) -@pytest.mark.parametrize("stride", [1, 2]) -@pytest.mark.parametrize("dilation", [1, 2]) -@pytest.mark.parametrize("kernel_shape", [[1, 2], [3, 3], [4, 1]]) -def test_conv2d_conversion__depthwise__quantized( - bias, stride, dilation, kernel_shape, mocker, use_qat -): - input_shape = (1, 4, 12, 12) - group = input_shape[1] - spy = mocker.spy(ModelBuilder, "finish") + self.assert_delegated_and_correct(model, input_shape, mocker, is_qat) - edge_program = to_quantized_edge_program( - Conv2dModule( - bias=bias, - group=group, - in_channels=group, - out_channels=group, + @pytest.mark.parametrize( + "input_shape, out_channels, is_qat", + [ + pytest.param( + ins := (21, 4, 7), + oc := 45, + qat := True, + id=f"qat={qat}, `conv2d` implicit batch: " + _conv_id(ins, oc), + ), + pytest.param( + ins := (21, 4, 7), + oc := 45, + qat := False, + id=f"qat={qat}, `conv2d` implicit batch: " + _conv_id(ins, oc), + ), + ], + ) + def test__basic_nsys_inference__implicit_batch( + self, input_shape, out_channels, is_qat, mocker + ): + in_channels = input_shape[0] + + model = Conv2dModule(in_channels=in_channels, out_channels=out_channels) + + # `view_copy` is inserted to convert to explicit batch + graph_verifier = DetailedGraphVerifier( + mocker, + expected_delegated_ops={Convolution: 1, ViewCopy: 2}, + expected_non_delegated_ops={}, + ) + dataset = RandomDatasetCreator(low=-256, high=256) + comparator = AllCloseOutputComparator(atol=1) + + lower_run_compare( + model, + input_shape, + graph_verifier, + dataset, + comparator, + use_qat=is_qat, + ) + + @pytest.mark.parametrize( + "input_shape, out_channels, kernel_size, stride, dilation, is_qat", + [ + pytest.param( + ins := (2, 3, 1, 4100), + oc := 7, + ks := (1, 4096), + s := 1, + d := 1, + qat := True, + id=f"qat={qat}, bounds of kernel width: {_conv_id(ins, oc, ks=ks, s=s, d=d)}", + ), + pytest.param( + ins := (2, 3, 1, 4100), + oc := 7, + ks := (1, 4096), + s := 1, + d := 1, + qat := False, + id=f"qat={qat}, bounds of kernel width: {_conv_id(ins, oc, ks=ks, s=s, d=d)}", + ), + pytest.param( + ins := (3, 3, 4100, 1), + oc := 9, + ks := (4096, 1), + s := 1, + d := 1, + qat := True, + id=f"qat={qat}, bounds of kernel height: {_conv_id(ins, oc, ks=ks, s=s, d=d)}", + ), + pytest.param( + ins := (3, 3, 4100, 1), + oc := 9, + ks := (4096, 1), + s := 1, + d := 1, + qat := False, + id=f"qat={qat}, bounds of kernel height: {_conv_id(ins, oc, ks=ks, s=s, d=d)}", + ), + pytest.param( + ins := (4, 3, 3, 8500), + oc := 5, + ks := 3, + s := (1, 4096), + d := 1, + qat := True, + id=f"qat={qat}, bounds of stride width: {_conv_id(ins, oc, ks=ks, s=s, d=d)}", + ), + pytest.param( + ins := (4, 3, 3, 8500), + oc := 5, + ks := 3, + s := (1, 4096), + d := 1, + qat := False, + id=f"qat={qat}, bounds of stride width: {_conv_id(ins, oc, ks=ks, s=s, d=d)}", + ), + pytest.param( + ins := (2, 3, 8500, 3), + oc := 11, + ks := 3, + s := (4096, 1), + d := 1, + qat := True, + id=f"qat={qat}, bounds of stride height: {_conv_id(ins, oc, ks=ks, s=s, d=d)}", + ), + pytest.param( + ins := (2, 3, 8500, 3), + oc := 11, + ks := 3, + s := (4096, 1), + d := 1, + qat := False, + id=f"qat={qat}, bounds of stride height: {_conv_id(ins, oc, ks=ks, s=s, d=d)}", + ), + pytest.param( + ins := (3, 3, 3, 8500), + oc := 9, + ks := 3, + s := 1, + d := (1, 4096), + qat := True, + id=f"qat={qat}, bounds of dilation width: {_conv_id(ins, oc, ks=ks, s=s, d=d)}", + marks=pytest.mark.xfail( + reason="AIR-14679", + strict=True, + ), + ), + pytest.param( + ins := (3, 3, 3, 8500), + oc := 9, + ks := 3, + s := 1, + d := (1, 4096), + qat := False, + id=f"qat={qat}, bounds of dilation width: {_conv_id(ins, oc, ks=ks, s=s, d=d)}", + marks=pytest.mark.xfail( + reason="AIR-14679", + strict=True, + ), + ), + pytest.param( + ins := (4, 3, 8500, 3), + oc := 7, + ks := 3, + s := 1, + d := (4096, 1), + qat := True, + id=f"qat={qat}, bounds of dilation height: {_conv_id(ins, oc, ks=ks, s=s, d=d)}", + marks=pytest.mark.xfail( + reason="AIR-14679", + strict=True, + ), + ), + pytest.param( + ins := (4, 3, 8500, 3), + oc := 7, + ks := 3, + s := 1, + d := (4096, 1), + qat := False, + id=f"qat={qat}, bounds of dilation height: {_conv_id(ins, oc, ks=ks, s=s, d=d)}", + marks=pytest.mark.xfail( + reason="AIR-14679", + strict=True, + ), + ), + pytest.param( + ins := (2, 80, 35, 34), + oc := 13, + ks := (32, 24), + s := 1, + d := 1, + qat := True, + id=f"qat={qat}, bounds of kernel_h * kernel_w * input_channels: {_conv_id(ins, oc, ks=ks, s=s, d=d)}", + marks=pytest.mark.xfail( + reason="AIR-14679", + strict=True, + ), + ), + pytest.param( + ins := (2, 80, 35, 34), + oc := 13, + ks := (32, 24), + s := 1, + d := 1, + qat := False, + id=f"qat={qat}, bounds of kernel_h * kernel_w * input_channels: {_conv_id(ins, oc, ks=ks, s=s, d=d)}", + marks=pytest.mark.xfail( + reason="AIR-14679", + strict=True, + ), + ), + ], + ) + def test__basic_nsys_inference__big( + self, input_shape, out_channels, kernel_size, stride, dilation, is_qat, mocker + ): + model = Conv2dModule( + in_channels=input_shape[1], + out_channels=out_channels, + kernel_size=kernel_size, stride=stride, dilation=dilation, - kernel_size=kernel_shape, - ), - tuple(input_shape), - use_qat=use_qat, - use_neutron_for_format_conversion=False, - ).exported_program() - - ops = spy.spy_return.sub_graphs[0].operators.vector - assert len(ops) == 1 - assert ops[0].builtin_options.operator_type == BuiltinOperator.DEPTHWISE_CONV_2D - - nodes = list(edge_program.graph.nodes) - assert ( - len(nodes) == 7 - ) # input, Quant, lowered_module, delegate_call, getitem, Deq, output - assert nodes[2].target == "lowered_module_0" - - -@pytest.mark.parametrize("padding", [1, 2]) -def test_conv2d_conversion__depthwise__padded(padding, mocker): - input_shape = (1, 3, 13, 15) - group = input_shape[1] - edge_program = to_edge_program( - Conv2dModule( - group=group, in_channels=group, out_channels=group, padding=padding - ), - input_shape, - ).exported_program() + ) - input_data = np.random.random(input_shape).astype(np.float32) + self.assert_delegated_and_correct(model, input_shape, mocker, is_qat) - spy = mocker.spy(ModelBuilder, "finish") - - convert_run_compare( - edge_program, - input_data, - tflite_input_preprocess=ToChannelLastPreprocess(), - tflite_output_preprocess=ToChannelFirstPreprocess(), - atol=4e-7, - conversion_config=ConversionConfig( - {"use_neutron_for_format_conversion": False} - ), + @pytest.mark.parametrize( + "input_shape, kernel_size, stride, dilation, is_qat", + [ + pytest.param( + ins := (2, 3, 1, 4100), + ks := (1, 4096), + s := 1, + d := 1, + qat := True, + id=f"qat={qat}, bounds of kernel width: {_conv_id(ins, ins[1], ks=ks, s=s, d=d, g=ins[1])}", + ), + pytest.param( + ins := (2, 3, 1, 4100), + ks := (1, 4096), + s := 1, + d := 1, + qat := False, + id=f"qat={qat}, bounds of kernel width: {_conv_id(ins, ins[1], ks=ks, s=s, d=d, g=ins[1])}", + ), + pytest.param( + ins := (3, 3, 4100, 1), + ks := (4096, 1), + s := 1, + d := 1, + qat := True, + id=f"qat={qat}, bounds of kernel height: {_conv_id(ins, ins[1], ks=ks, s=s, d=d, g=ins[1])}", + ), + pytest.param( + ins := (3, 3, 4100, 1), + ks := (4096, 1), + s := 1, + d := 1, + qat := False, + id=f"qat={qat}, bounds of kernel height: {_conv_id(ins, ins[1], ks=ks, s=s, d=d, g=ins[1])}", + ), + pytest.param( + ins := (2, 3, 3, 8500), + ks := 3, + s := (1, 4096), + d := 1, + qat := True, + id=f"qat={qat}, bounds of stride width: {_conv_id(ins, ins[1], ks=ks, s=s, d=d, g=ins[1])}", + ), + pytest.param( + ins := (2, 3, 3, 8500), + ks := 3, + s := (1, 4096), + d := 1, + qat := False, + id=f"qat={qat}, bounds of stride width: {_conv_id(ins, ins[1], ks=ks, s=s, d=d, g=ins[1])}", + ), + pytest.param( + ins := (4, 3, 8500, 3), + ks := 3, + s := (4096, 1), + d := 1, + qat := True, + id=f"qat={qat}, bounds of stride height: {_conv_id(ins, ins[1], ks=ks, s=s, d=d, g=ins[1])}", + ), + pytest.param( + ins := (4, 3, 8500, 3), + ks := 3, + s := (4096, 1), + d := 1, + qat := False, + id=f"qat={qat}, bounds of stride height: {_conv_id(ins, ins[1], ks=ks, s=s, d=d, g=ins[1])}", + ), + pytest.param( + ins := (4, 3, 3, 8500), + ks := 3, + s := 1, + d := (1, 4096), + qat := True, + id=f"qat={qat}, bounds of dilation width: {_conv_id(ins, ins[1], ks=ks, s=s, d=d, g=ins[1])}", + marks=pytest.mark.xfail( + reason="AIR-14679", + strict=True, + ), + ), + pytest.param( + ins := (4, 3, 3, 8500), + ks := 3, + s := 1, + d := (1, 4096), + qat := False, + id=f"qat={qat}, bounds of dilation width: {_conv_id(ins, ins[1], ks=ks, s=s, d=d, g=ins[1])}", + ), + pytest.param( + ins := (3, 3, 8500, 3), + ks := 3, + s := 1, + d := (4096, 1), + qat := True, + id=f"qat={qat}, bounds of dilation height: {_conv_id(ins, ins[1], ks=ks, s=s, d=d, g=ins[1])}", + ), + pytest.param( + ins := (3, 3, 8500, 3), + ks := 3, + s := 1, + d := (4096, 1), + qat := False, + id=f"qat={qat}, bounds of dilation height: {_conv_id(ins, ins[1], ks=ks, s=s, d=d, g=ins[1])}", + marks=pytest.mark.xfail( + reason="AIR-14679", + strict=True, + ), + ), + pytest.param( + ins := (2, 80, 35, 34), + ks := (32, 24), + s := 1, + d := 1, + qat := True, + id=f"qat={qat}, bounds of kernel_h * kernel_w * input_channels: {_conv_id(ins, ins[1], ks=ks, s=s, d=d, g=ins[1])}", + marks=pytest.mark.xfail( + reason="AIR-14679", + strict=True, + ), + ), + pytest.param( + ins := (2, 80, 35, 34), + ks := (32, 24), + s := 1, + d := 1, + qat := False, + id=f"qat={qat}, bounds of kernel_h * kernel_w * input_channels: {_conv_id(ins, ins[1], ks=ks, s=s, d=d, g=ins[1])}", + marks=pytest.mark.xfail( + reason="AIR-14679", + strict=True, + ), + ), + ], ) - conversion_result = spy.spy_return - ops = conversion_result.sub_graphs[0].operators.vector - - assert len(ops) == 2 - assert ops[0].builtin_options.operator_type == BuiltinOperator.PAD - assert ops[1].builtin_options.operator_type == BuiltinOperator.DEPTHWISE_CONV_2D - - -@pytest.mark.parametrize("padding", [1, 2]) -def test_conv2d_conversion__depthwise__padded__quantized(padding, mocker, use_qat): - input_shape = (1, 4, 12, 12) - group = input_shape[1] - spy = mocker.spy(ModelBuilder, "finish") - - edge_program = to_quantized_edge_program( - Conv2dModule( - group=group, in_channels=group, out_channels=group, padding=padding - ), - tuple(input_shape), - use_qat=use_qat, - use_neutron_for_format_conversion=False, - ).exported_program() - - ops = spy.spy_return.sub_graphs[0].operators.vector - assert len(ops) == 2 - assert ops[0].builtin_options.operator_type == BuiltinOperator.PADV2 - assert ops[1].builtin_options.operator_type == BuiltinOperator.DEPTHWISE_CONV_2D - - nodes = list(edge_program.graph.nodes) - assert ( - len(nodes) == 7 - ) # input, Quant, lowered_module, delegate_call, getitem, Deq, output - assert nodes[2].target == "lowered_module_0" - - -@pytest.mark.parametrize( - "model, input_shape", - [ - pytest.param( - torch.nn.ConvTranspose2d(8, 16, (1, 4), stride=(1, 2)), - (1, 8, 1, 16), - id="In ch 8, out ch 16, kernel (1, 4), stride (1, 2)", - ), - pytest.param( - torch.nn.ConvTranspose2d(64, 64, (1, 2), stride=(1, 2)), - (1, 64, 3, 12), - id="In ch 64, out ch 64, kernel (1, 2), stride (1, 2)", - ), - pytest.param( - torch.nn.ConvTranspose2d( - 16, 24, (1, 6), stride=(1, 6), output_padding=(0, 3) - ), - (1, 16, 7, 15), - id="In ch 16, out ch 24, kernel (1, 6), stride (1, 6), output_padding (0, 3)", - marks=pytest.mark.skip(reason="AIR-14676"), - ), - pytest.param( - torch.nn.ConvTranspose2d(16, 40, (1, 4), stride=(1, 4), padding=(0, 1)), - (1, 16, 1, 27), - id="In ch 16, out ch 40, kernel (1, 4), stride (1, 4), padding (0, 1)", - marks=pytest.mark.skip(reason="AIR-14676"), - ), - pytest.param( - torch.nn.ConvTranspose2d(8, 16, (1, 4), stride=(1, 2), padding=(0, 1)), - (1, 8, 1, 16), - id="In ch 8, out ch 16, kernel (1, 4), stride (1, 2), padding (0, 1)", - ), - pytest.param( - torch.nn.ConvTranspose2d( - 8, 16, (1, 8), stride=(1, 4), output_padding=(0, 2) - ), - (1, 8, 1, 16), - id="In ch 8, out ch 16, kernel (1, 8), stride (1, 4), output_padding (0, 2)", - marks=pytest.mark.skip(reason="AIR-14676"), - ), - pytest.param( - torch.nn.ConvTranspose2d(16, 16, (1, 4), stride=(1, 2)), - (1, 16, 1, 16), - id="In ch 16, out ch 16, kernel (1, 4), stride (1, 2)", - ), - pytest.param( - torch.nn.ConvTranspose2d(8, 16, (1, 4), stride=(1, 2), bias=False), - (1, 8, 1, 16), - id="In ch 8, out ch 16, kernel (1, 4), stride (1, 2), no bias", - ), - pytest.param( - torch.nn.ConvTranspose2d( - 8, 16, (1, 4), stride=(1, 2), padding=(0, 1), bias=False - ), - (1, 8, 1, 16), - id="In ch 8, out ch 16, kernel (1, 4), stride (1, 2)," - "padding (0, 1), no bias", - ), - ], -) -def test_conv_transpose2d_conversion__quantized( - mocker, model: torch.nn.Module, input_shape, use_qat -): - converter_spy = mocker.spy(EdgeProgramToIRConverter, "convert_program") - - edge_program = to_quantized_edge_program( - model, input_shape, use_qat=use_qat, use_neutron_for_format_conversion=False - ).exported_program() - - # Make sure the `TransposeConv` was delegated. - assert not graph_contains_any_of_ops( - graph=edge_program.graph, ops=[exir_ops.edge.aten.convolution.default] + def test__basic_nsys_inference_depthwise__big( + self, input_shape, kernel_size, stride, dilation, is_qat, mocker + ): + out_channels = input_shape[1] + group = input_shape[1] + + model = Conv2dModule( + in_channels=input_shape[1], + out_channels=out_channels, + kernel_size=kernel_size, + stride=stride, + dilation=dilation, + group=group, + ) + + self.assert_delegated_and_correct(model, input_shape, mocker, is_qat) + + @pytest.mark.parametrize( + "input_shape, out_channels, kernel_size, stride, dilation, padding, bias, is_qat", + [ + pytest.param( + ins := (1, 8, 32, 32), + oc := 7, + ks := (5, 3), + s := (2, 1), + d := (1, 2), + p := (2, 1), + b := True, + qat := True, + id=f"qat={qat}, some params not default: {_conv_id(ins, oc, ks=ks, s=s, d=d, p=p, b=b)}", + marks=pytest.mark.xfail( + reason="AIR-14679", + strict=True, + ), + ), + pytest.param( + ins := (1, 8, 32, 32), + oc := 7, + ks := (5, 3), + s := (2, 1), + d := (1, 2), + p := (2, 1), + b := True, + qat := False, + id=f"qat={qat}, some params not default: {_conv_id(ins, oc, ks=ks, s=s, d=d, p=p, b=b)}", + ), + pytest.param( + ins := (2, 7, 31, 17), + oc := 9, + ks := (7, 7), + s := (3, 2), + d := (2, 1), + p := (3, 3), + b := False, + qat := True, + id=f"qat={qat}, some params not default: {_conv_id(ins, oc, ks=ks, s=s, d=d, p=p, b=b)}", + ), + pytest.param( + ins := (2, 7, 31, 17), + oc := 9, + ks := (7, 7), + s := (3, 2), + d := (2, 1), + p := (3, 3), + b := False, + qat := False, + id=f"qat={qat}, some params not default: {_conv_id(ins, oc, ks=ks, s=s, d=d, p=p, b=b)}", + marks=pytest.mark.xfail( + reason="AIR-14679", + strict=True, + ), + ), + pytest.param( + ins := (2, 12, 28, 28), + oc := 11, + ks := (3, 5), + s := (2, 2), + d := (2, 2), + p := (1, 2), + b := True, + qat := True, + id=f"qat={qat}, some params not default: {_conv_id(ins, oc, ks=ks, s=s, d=d, p=p, b=b)}", + marks=pytest.mark.xfail( + reason="AIR-14679", + strict=True, + ), + ), + pytest.param( + ins := (2, 12, 28, 28), + oc := 11, + ks := (3, 5), + s := (2, 2), + d := (2, 2), + p := (1, 2), + b := True, + qat := False, + id=f"qat={qat}, some params not default: {_conv_id(ins, oc, ks=ks, s=s, d=d, p=p, b=b)}", + ), + pytest.param( + ins := (3, 2, 40, 20), + oc := 13, + ks := (1, 5), + s := (1, 2), + d := (3, 1), + p := (0, 2), + b := False, + qat := True, + id=f"qat={qat}, some params not default: {_conv_id(ins, oc, ks=ks, s=s, d=d, p=p, b=b)}", + marks=pytest.mark.xfail( + reason="AIR-14679", + strict=True, + ), + ), + pytest.param( + ins := (3, 2, 40, 20), + oc := 13, + ks := (1, 5), + s := (1, 2), + d := (3, 1), + p := (0, 2), + b := False, + qat := False, + id=f"qat={qat}, some params not default: {_conv_id(ins, oc, ks=ks, s=s, d=d, p=p, b=b)}", + marks=pytest.mark.xfail( + reason="AIR-14679", + strict=True, + ), + ), + pytest.param( + ins := (4, 6, 30, 30), + oc := 5, + ks := (3, 3), + s := (2, 2), + d := (1, 1), + p := (2, 2), + b := True, + qat := True, + id=f"qat={qat}, some params not default: {_conv_id(ins, oc, ks=ks, s=s, d=d, p=p, b=b)}", + marks=pytest.mark.xfail( + reason="AIR-14679", + strict=True, + ), + ), + pytest.param( + ins := (4, 6, 30, 30), + oc := 5, + ks := (3, 3), + s := (2, 2), + d := (1, 1), + p := (2, 2), + b := True, + qat := False, + id=f"qat={qat}, some params not default: {_conv_id(ins, oc, ks=ks, s=s, d=d, p=p, b=b)}", + ), + pytest.param( + ins := (3, 12, 7, 7), + oc := 7, + ks := (5, 5), + s := (1, 3), + d := (1, 2), + p := (2, 4), + b := False, + qat := True, + id=f"qat={qat}, some params not default: {_conv_id(ins, oc, ks=ks, s=s, d=d, p=p, b=b)}", + ), + pytest.param( + ins := (3, 12, 7, 7), + oc := 7, + ks := (5, 5), + s := (1, 3), + d := (1, 2), + p := (2, 4), + b := False, + qat := False, + id=f"qat={qat}, some params not default: {_conv_id(ins, oc, ks=ks, s=s, d=d, p=p, b=b)}", + ), + pytest.param( + ins := (1, 4, 15, 15), + oc := 9, + ks := (2, 2), + s := (2, 2), + d := (2, 2), + p := (1, 1), + b := True, + qat := True, + id=f"qat={qat}, some params not default: {_conv_id(ins, oc, ks=ks, s=s, d=d, p=p, b=b)}", + ), + pytest.param( + ins := (1, 4, 15, 15), + oc := 9, + ks := (2, 2), + s := (2, 2), + d := (2, 2), + p := (1, 1), + b := True, + qat := False, + id=f"qat={qat}, some params not default: {_conv_id(ins, oc, ks=ks, s=s, d=d, p=p, b=b)}", + ), + ], ) - assert any("lowered_module" in node.name for node in edge_program.graph.nodes) + def test__nsys_inference__non_default_params( + self, + input_shape, + out_channels, + kernel_size, + stride, + dilation, + padding, + bias, + is_qat, + mocker, + ): + model = Conv2dModule( + in_channels=input_shape[1], + out_channels=out_channels, + kernel_size=kernel_size, + stride=stride, + dilation=dilation, + padding=padding, + bias=bias, + ) + + self.assert_delegated_and_correct(model, input_shape, mocker, is_qat) - # Capture generated model - tflite_flatbuffers_model, io_formats = converter_spy.spy_return + @pytest.mark.parametrize( + "input_shape, kernel_size, stride, dilation, padding, bias, is_qat", + [ + pytest.param( + ins := (1, 8, 32, 32), + ks := (5, 3), + s := (2, 1), + d := (1, 2), + p := (2, 1), + b := True, + qat := True, + id=f"qat={qat}, some params not default: {_conv_id(ins, ins[1], ks=ks, s=s, d=d, p=p, b=b, g=ins[1])}", + marks=pytest.mark.xfail( + reason="AIR-14679", + strict=True, + ), + ), + pytest.param( + ins := (1, 8, 32, 32), + ks := (5, 3), + s := (2, 1), + d := (1, 2), + p := (2, 1), + b := True, + qat := False, + id=f"qat={qat}, some params not default: {_conv_id(ins, ins[1], ks=ks, s=s, d=d, p=p, b=b, g=ins[1])}", + marks=pytest.mark.xfail( + reason="AIR-14679", + strict=True, + ), + ), + pytest.param( + ins := (3, 7, 31, 17), + ks := (7, 7), + s := (3, 2), + d := (2, 1), + p := (3, 3), + b := False, + qat := True, + id=f"qat={qat}, some params not default: {_conv_id(ins, ins[1], ks=ks, s=s, d=d, p=p, b=b, g=ins[1])}", + ), + pytest.param( + ins := (3, 7, 31, 17), + ks := (7, 7), + s := (3, 2), + d := (2, 1), + p := (3, 3), + b := False, + qat := False, + id=f"qat={qat}, some params not default: {_conv_id(ins, ins[1], ks=ks, s=s, d=d, p=p, b=b, g=ins[1])}", + ), + pytest.param( + ins := (2, 12, 28, 28), + ks := (3, 5), + s := (2, 2), + d := (2, 2), + p := (1, 2), + b := True, + qat := True, + id=f"qat={qat}, some params not default: {_conv_id(ins, ins[1], ks=ks, s=s, d=d, p=p, b=b, g=ins[1])}", + marks=pytest.mark.xfail( + reason="AIR-14679", + strict=True, + ), + ), + pytest.param( + ins := (2, 12, 28, 28), + ks := (3, 5), + s := (2, 2), + d := (2, 2), + p := (1, 2), + b := True, + qat := False, + id=f"qat={qat}, some params not default: {_conv_id(ins, ins[1], ks=ks, s=s, d=d, p=p, b=b, g=ins[1])}", + ), + pytest.param( + ins := (3, 2, 40, 20), + ks := (1, 5), + s := (1, 2), + d := (3, 1), + p := (0, 2), + b := False, + qat := True, + id=f"qat={qat}, some params not default: {_conv_id(ins, ins[1], ks=ks, s=s, d=d, p=p, b=b, g=ins[1])}", + marks=pytest.mark.xfail( + reason="AIR-14679", + strict=True, + ), + ), + pytest.param( + ins := (3, 2, 40, 20), + ks := (1, 5), + s := (1, 2), + d := (3, 1), + p := (0, 2), + b := False, + qat := False, + id=f"qat={qat}, some params not default: {_conv_id(ins, ins[1], ks=ks, s=s, d=d, p=p, b=b, g=ins[1])}", + marks=pytest.mark.xfail( + reason="AIR-14679", + strict=True, + ), + ), + pytest.param( + ins := (4, 6, 30, 30), + ks := (3, 3), + s := (2, 2), + d := (1, 1), + p := (2, 2), + b := True, + qat := True, + id=f"qat={qat}, some params not default: {_conv_id(ins, ins[1], ks=ks, s=s, d=d, p=p, b=b, g=ins[1])}", + marks=pytest.mark.xfail( + reason="AIR-14679", + strict=True, + ), + ), + pytest.param( + ins := (4, 6, 30, 30), + ks := (3, 3), + s := (2, 2), + d := (1, 1), + p := (2, 2), + b := True, + qat := False, + id=f"qat={qat}, some params not default: {_conv_id(ins, ins[1], ks=ks, s=s, d=d, p=p, b=b, g=ins[1])}", + ), + pytest.param( + ins := (3, 12, 7, 7), + ks := (5, 5), + s := (1, 3), + d := (1, 2), + p := (2, 4), + b := False, + qat := True, + id=f"qat={qat}, some params not default: {_conv_id(ins, ins[1], ks=ks, s=s, d=d, p=p, b=b, g=ins[1])}", + ), + pytest.param( + ins := (3, 12, 7, 7), + ks := (5, 5), + s := (1, 3), + d := (1, 2), + p := (2, 4), + b := False, + qat := False, + id=f"qat={qat}, some params not default: {_conv_id(ins, ins[1], ks=ks, s=s, d=d, p=p, b=b, g=ins[1])}", + ), + pytest.param( + ins := (1, 4, 15, 15), + ks := (2, 2), + s := (2, 2), + d := (2, 2), + p := (1, 1), + b := True, + qat := True, + id=f"qat={qat}, some params not default: {_conv_id(ins, ins[1], ks=ks, s=s, d=d, p=p, b=b, g=ins[1])}", + ), + pytest.param( + ins := (1, 4, 15, 15), + ks := (2, 2), + s := (2, 2), + d := (2, 2), + p := (1, 1), + b := True, + qat := False, + id=f"qat={qat}, some params not default: {_conv_id(ins, ins[1], ks=ks, s=s, d=d, p=p, b=b, g=ins[1])}", + ), + ], + ) + def test__nsys_inference_depthwise__non_default_params( + self, input_shape, kernel_size, stride, dilation, padding, bias, is_qat, mocker + ): + out_channels = input_shape[1] + group = input_shape[1] - # Capture converted program - exported_program: ExportedProgram = converter_spy.call_args.args[1] + model = Conv2dModule( + in_channels=input_shape[1], + out_channels=out_channels, + kernel_size=kernel_size, + stride=stride, + dilation=dilation, + padding=padding, + bias=bias, + group=group, + ) - input_data = (np.random.random(input_shape).astype(np.float32) * 50).astype(np.int8) + self.assert_delegated_and_correct(model, input_shape, mocker, is_qat) - convert_run_compare( - exported_program, - tflite_input_preprocess=ToChannelLastPreprocess(), - tfl_model=tflite_flatbuffers_model, - tflite_output_preprocess=ToChannelFirstPreprocess(), - input_data=input_data, - atol=1.0, + @pytest.mark.parametrize( + "input_shape, out_channels, kernel_size, stride, dilation, is_qat", + [ + pytest.param( + ins := (3, 7, 5000, 11), + oc := 7, + ks := (4097, 1), + s := 1, + d := 1, + qat := True, + id=f"qat={qat}, kernel height too big: {_conv_id(ins, oc, ks=ks, s=s, d=d)}", + ), + pytest.param( + ins := (3, 7, 5000, 11), + oc := 7, + ks := (4097, 1), + s := 1, + d := 1, + qat := False, + id=f"qat={qat}, kernel height too big: {_conv_id(ins, oc, ks=ks, s=s, d=d)}", + ), + pytest.param( + ins := (3, 7, 13, 5000), + oc := 9, + ks := (1, 4097), + s := 1, + d := 1, + qat := True, + id=f"qat={qat}, kernel width too big: {_conv_id(ins, oc, ks=ks, s=s, d=d)}", + ), + pytest.param( + ins := (3, 7, 13, 5000), + oc := 9, + ks := (1, 4097), + s := 1, + d := 1, + qat := False, + id=f"qat={qat}, kernel width too big: {_conv_id(ins, oc, ks=ks, s=s, d=d)}", + ), + pytest.param( + ins := (3, 7, 5000, 11), + oc := 11, + ks := 3, + s := (4097, 1), + d := 1, + qat := True, + id=f"qat={qat}, stride height too big: {_conv_id(ins, oc, ks=ks, s=s, d=d)}", + ), + pytest.param( + ins := (3, 7, 5000, 11), + oc := 11, + ks := 3, + s := (4097, 1), + d := 1, + qat := False, + id=f"qat={qat}, stride height too big: {_conv_id(ins, oc, ks=ks, s=s, d=d)}", + ), + pytest.param( + ins := (3, 7, 13, 5000), + oc := 5, + ks := 3, + s := (1, 4097), + d := 1, + qat := True, + id=f"qat={qat}, stride width too big: {_conv_id(ins, oc, ks=ks, s=s, d=d)}", + ), + pytest.param( + ins := (3, 7, 13, 5000), + oc := 5, + ks := 3, + s := (1, 4097), + d := 1, + qat := False, + id=f"qat={qat}, stride width too big: {_conv_id(ins, oc, ks=ks, s=s, d=d)}", + ), + pytest.param( + ins := (3, 7, 8500, 11), + oc := 7, + ks := 3, + s := 1, + d := (4097, 1), + qat := True, + id=f"qat={qat}, dilation height too big: {_conv_id(ins, oc, ks=ks, s=s, d=d)}", + ), + pytest.param( + ins := (3, 7, 8500, 11), + oc := 7, + ks := 3, + s := 1, + d := (4097, 1), + qat := False, + id=f"qat={qat}, dilation height too big: {_conv_id(ins, oc, ks=ks, s=s, d=d)}", + ), + pytest.param( + ins := (3, 7, 13, 8500), + oc := 9, + ks := 3, + s := 1, + d := (1, 4097), + qat := True, + id=f"qat={qat}, dilation width too big: {_conv_id(ins, oc, ks=ks, s=s, d=d)}", + ), + pytest.param( + ins := (3, 7, 13, 8500), + oc := 9, + ks := 3, + s := 1, + d := (1, 4097), + qat := False, + id=f"qat={qat}, dilation width too big: {_conv_id(ins, oc, ks=ks, s=s, d=d)}", + ), + pytest.param( + ins := (3, 113, 123, 133), + oc := 11, + ks := (41, 15), + s := 1, + d := 1, + qat := True, + id=f"qat={qat}, kernel_h * kernel_w * input_channels too big: {_conv_id(ins, oc, ks=ks, s=s, d=d)}", + ), + pytest.param( + ins := (3, 113, 123, 133), + oc := 11, + ks := (41, 15), + s := 1, + d := 1, + qat := False, + id=f"qat={qat}, kernel_h * kernel_w * input_channels too big: {_conv_id(ins, oc, ks=ks, s=s, d=d)}", + ), + ], ) + def test__non_delegation( + self, input_shape, out_channels, kernel_size, stride, dilation, is_qat + ): + in_channels = input_shape[1] + model = Conv2dModule( + in_channels=in_channels, + out_channels=out_channels, + kernel_size=kernel_size, + stride=stride, + dilation=dilation, + ) -@pytest.mark.parametrize( - "model, input_shape", - [ - pytest.param( - torch.nn.ConvTranspose2d(8, 16, (1, 4), stride=(1, 2), dilation=(1, 2)), - (1, 8, 1, 16), - id="In ch 8, out ch 16, kernel (1, 4), stride (1, 2), " - "dilation (1, 2) - Dilation != (1, 1)", - ), - pytest.param( - torch.nn.ConvTranspose2d(6, 16, (1, 4), stride=(1, 2)), - (1, 6, 1, 16), - id="In ch 6, out ch 16, kernel (1, 4), stride (1, 2) - In channels % num_macs != 0", - ), - pytest.param( - torch.nn.ConvTranspose2d(8, 16, (1, 4), stride=(1, 2)), - (1, 8, 4, 16), - id="In ch 8, out ch 16, kernel (1, 4), stride (1, 2) - Out height != 1, stride width" - " != kernel width", - ), - pytest.param( - torch.nn.ConvTranspose2d(8, 16, (2, 4), stride=(1, 2), padding=(0, 1)), - (1, 8, 1, 16), - id="In ch 8, out ch 16, kernel (2, 4), stride (1, 2), padding " - "(0, 1) - Out height != 1, stride width != kernel width", - ), - pytest.param( - torch.nn.ConvTranspose2d(8, 16, (1, 5), stride=(1, 4)), - (1, 8, 1, 16), - id="In ch 8, out ch 16, kernel (1, 5), stride (1, 4) - Stride width != kernel width / 2" - ", stride width != kernel width", - ), - pytest.param( - torch.nn.ConvTranspose2d(16, 12, (1, 4), stride=(3, 3)), - (1, 16, 1, 16), - id="In ch 16, out ch 12, kernel (1, 4), stride (3, 3) - Out channels % num_macs != 0", - ), - pytest.param( - torch.nn.ConvTranspose2d(64, 64, (1, 4), stride=(1, 2)), - (1, 64, 3, 12), - id="In ch 64, out ch 64, kernel (1, 4), stride (1, 2) - Out height != 1, stride width" - " != kernel width", - ), - pytest.param( - torch.nn.ConvTranspose2d(16, 40, (1, 4), stride=(1, 4), padding=(0, 1)), - (1, 16, 4, 27), - id="In ch 16, out ch 40, kernel (1, 4), stride (1, 4), padding (0, 1) - Padding width " - "!= 1 and input height != 1", - ), - ], -) -def test_conv_transpose2d_non_delegated_conversion__quantized( - model: torch.nn.Module, input_shape, use_qat -): - edge_program = to_quantized_edge_program( - model, input_shape, use_qat=use_qat - ).exported_program() - - nodes = list(edge_program.graph.nodes) - assert len(nodes) == 15 - assert ( - nodes[11].target.__name__ == "aten.convolution.default" - ) # TransposeConv not delegated. - - -def test_conv2d_conversion__depthwise__delegates_without_post_quantization_state_dict( - mocker, -): - """Depthwise convolution should delegate even when post_quantization_state_dict - is not provided to the partitioner. Without the fallback in map_inputs_to_parameters, - the parameter mapping would be incomplete and the depthwise conv would fail to - delegate due to missing weight data. - - """ - input_shape = (1, 4, 12, 12) - group = input_shape[1] - spy = mocker.spy(ModelBuilder, "finish") - - edge_program = to_quantized_edge_program( - Conv2dModule( + self.assert_not_delegated(model, input_shape, is_qat) + + @pytest.mark.parametrize( + "input_shape, kernel_size, stride, dilation, is_qat", + [ + pytest.param( + ins := (3, 7, 5000, 11), + ks := (4097, 1), + s := 1, + d := 1, + qat := True, + id=f"qat={qat}, kernel height too big, depthwise: {_conv_id(ins, ins[1], ks=ks, s=s, d=d, g=ins[1])}", + ), + pytest.param( + ins := (3, 7, 5000, 11), + ks := (4097, 1), + s := 1, + d := 1, + qat := False, + id=f"qat={qat}, kernel height too big, depthwise: {_conv_id(ins, ins[1], ks=ks, s=s, d=d, g=ins[1])}", + ), + pytest.param( + ins := (3, 7, 13, 5000), + ks := (1, 4097), + s := 1, + d := 1, + qat := True, + id=f"qat={qat}, kernel width too big, depthwise: {_conv_id(ins, ins[1], ks=ks, s=s, d=d, g=ins[1])}", + ), + pytest.param( + ins := (3, 7, 13, 5000), + ks := (1, 4097), + s := 1, + d := 1, + qat := False, + id=f"qat={qat}, kernel width too big, depthwise: {_conv_id(ins, ins[1], ks=ks, s=s, d=d, g=ins[1])}", + ), + pytest.param( + ins := (3, 7, 5000, 11), + ks := 3, + s := (4097, 1), + d := 1, + qat := True, + id=f"qat={qat}, stride height too big, depthwise: {_conv_id(ins, ins[1], ks=ks, s=s, d=d, g=ins[1])}", + ), + pytest.param( + ins := (3, 7, 5000, 11), + ks := 3, + s := (4097, 1), + d := 1, + qat := False, + id=f"qat={qat}, stride height too big, depthwise: {_conv_id(ins, ins[1], ks=ks, s=s, d=d, g=ins[1])}", + ), + pytest.param( + ins := (3, 7, 13, 5000), + ks := 3, + s := (1, 4097), + d := 1, + qat := True, + id=f"qat={qat}, stride width too big, depthwise: {_conv_id(ins, ins[1], ks=ks, s=s, d=d, g=ins[1])}", + ), + pytest.param( + ins := (3, 7, 13, 5000), + ks := 3, + s := (1, 4097), + d := 1, + qat := False, + id=f"qat={qat}, stride width too big, depthwise: {_conv_id(ins, ins[1], ks=ks, s=s, d=d, g=ins[1])}", + ), + pytest.param( + ins := (3, 7, 8500, 11), + ks := 3, + s := 1, + d := (4097, 1), + qat := True, + id=f"qat={qat}, dilation height too big, depthwise: {_conv_id(ins, ins[1], ks=ks, s=s, d=d, g=ins[1])}", + ), + pytest.param( + ins := (3, 7, 8500, 11), + ks := 3, + s := 1, + d := (4097, 1), + qat := False, + id=f"qat={qat}, dilation height too big, depthwise: {_conv_id(ins, ins[1], ks=ks, s=s, d=d, g=ins[1])}", + ), + pytest.param( + ins := (3, 7, 13, 8500), + ks := 3, + s := 1, + d := (1, 4097), + qat := True, + id=f"qat={qat}, dilation width too big, depthwise: {_conv_id(ins, ins[1], ks=ks, s=s, d=d, g=ins[1])}", + ), + pytest.param( + ins := (3, 7, 13, 8500), + ks := 3, + s := 1, + d := (1, 4097), + qat := False, + id=f"qat={qat}, dilation width too big, depthwise: {_conv_id(ins, ins[1], ks=ks, s=s, d=d, g=ins[1])}", + ), + pytest.param( + ins := (3, 113, 123, 133), + ks := (41, 15), + s := 1, + d := 1, + qat := True, + id=f"qat={qat}, kernel_h * kernel_w * input_channels too big, depthwise: {_conv_id(ins, ins[1], ks=ks, s=s, d=d, g=ins[1])}", + ), + pytest.param( + ins := (3, 113, 123, 133), + ks := (41, 15), + s := 1, + d := 1, + qat := False, + id=f"qat={qat}, kernel_h * kernel_w * input_channels too big, depthwise: {_conv_id(ins, ins[1], ks=ks, s=s, d=d, g=ins[1])}", + ), + ], + ) + def test__non_delegation_depthwise( + self, input_shape, kernel_size, stride, dilation, is_qat + ): + out_channels = input_shape[1] + group = input_shape[1] + + model = Conv2dModule( + in_channels=input_shape[1], + out_channels=out_channels, + kernel_size=kernel_size, + stride=stride, + dilation=dilation, group=group, - in_channels=group, - out_channels=group, - kernel_size=3, - ), - tuple(input_shape), - use_neutron_for_format_conversion=False, - use_quant_state_dict=False, - ).exported_program() - - ops = spy.spy_return.sub_graphs[0].operators.vector - assert len(ops) == 1 - assert ops[0].builtin_options.operator_type == BuiltinOperator.DEPTHWISE_CONV_2D - - nodes = list(edge_program.graph.nodes) - assert any(n.target == "lowered_module_0" for n in nodes) + ) + + self.assert_not_delegated(model, input_shape, is_qat) diff --git a/backends/nxp/tests/ir/converter/node_converter/test_slice_tensor_converter.py b/backends/nxp/tests/ir/converter/node_converter/test_slice_tensor_converter.py index cb0ec09bcce..f10910e40ee 100644 --- a/backends/nxp/tests/ir/converter/node_converter/test_slice_tensor_converter.py +++ b/backends/nxp/tests/ir/converter/node_converter/test_slice_tensor_converter.py @@ -336,6 +336,11 @@ def test_nsys_inference__identity(self, input_shape, dims, starts, ends): self.assert_model_without_slices(model, input_shape) + pytest.mark.xfail( + reason="AIR-14679: should start working after `conv2d` giving incorrect results is fixed by Neutron team.", + strict=True, + ) + def test_nsys_inference__with_conv(self, mocker): input_shape = (11, 13, 5, 7) in_channels = input_shape[1] @@ -350,8 +355,8 @@ def test_nsys_inference__with_conv(self, mocker): num_slices = len(dims) graph_verifier = DetailedGraphVerifier( mocker, - expected_delegated_ops={SliceCopy: num_slices}, - expected_non_delegated_ops={Convolution: 1}, + expected_delegated_ops={SliceCopy: num_slices, Convolution: 1}, + expected_non_delegated_ops={}, ) dataset = RandomDatasetCreator(low=-255.0, high=255.0) comparator = AllCloseOutputComparator() diff --git a/backends/nxp/tests/ir/converter/node_converter/test_view_copy_converter.py b/backends/nxp/tests/ir/converter/node_converter/test_view_copy_converter.py index cb5f398fa21..1fa14fe5b30 100644 --- a/backends/nxp/tests/ir/converter/node_converter/test_view_copy_converter.py +++ b/backends/nxp/tests/ir/converter/node_converter/test_view_copy_converter.py @@ -8,23 +8,16 @@ import numpy as np import pytest import torch - from executorch.backends.nxp.backend.edge_program_converter import ( EdgeProgramToIRConverter, ) -from executorch.backends.nxp.backend.ir.conversion_config import ConversionConfig from executorch.backends.nxp.backend.ir.converter.builder.model_builder import ( ModelBuilder, ) -from executorch.backends.nxp.backend.ir.tflite_generator.builtin_options.conv_2d_options import ( - Conv2D, -) from executorch.backends.nxp.backend.ir.tflite_generator.builtin_options.reshape_options import ( Reshape, ) -from executorch.backends.nxp.backend.ir.tflite_generator.builtin_options.transpose_options import ( - Transpose, -) +from executorch.backends.nxp.tests.dataset_creator import RandomDatasetCreator from executorch.backends.nxp.tests.executorch_pipeline import ( to_edge_program, to_quantized_edge_program, @@ -35,6 +28,13 @@ ToChannelFirstPreprocess, ToChannelLastPreprocess, ) +from executorch.backends.nxp.tests.graph_verifier import DetailedGraphVerifier +from executorch.backends.nxp.tests.model_output_comparator import ( + AllCloseOutputComparator, +) +from executorch.backends.nxp.tests.nsys_testing import lower_run_compare + +from executorch.backends.nxp.tests.ops_aliases import Convolution, ViewCopy from torch import nn from torch.export import ExportedProgram from executorch.backends.nxp.tests.use_qat import * # noqa F403 @@ -143,92 +143,67 @@ def forward(self, x): return x -def test__view_copy__channels_first_to_2d(mocker): - input_shape = (1, 4, 7, 9) - new_shape = (6, 32) # Mix up the dimensions for a thorough test. - - torch_model = ConvReshapeModule(channels=input_shape[1], new_shape=new_shape) - edge_program = to_edge_program(torch_model, input_shape).exported_program() - - input_data = np.random.random(input_shape).astype(np.float32) - - converter_spy = mocker.spy(ModelBuilder, "finish") - - convert_run_compare( - edge_program, - input_data, - tflite_input_preprocess=ToChannelLastPreprocess(), - conversion_config=ConversionConfig( - {"use_neutron_for_format_conversion": False} - ), - ) - - tflite_model = converter_spy.spy_return - ops = tflite_model.sub_graphs[0].operators.vector - assert len(ops) == 3 - assert isinstance(ops[0].builtin_options, Conv2D) - assert isinstance(ops[1].builtin_options, Transpose) - assert isinstance(ops[2].builtin_options, Reshape) - - -def test__view_copy__channels_first_to_4d(mocker): - input_shape = (1, 8, 6, 8) - new_shape = (7, 4, 2, 5) - - torch_model = ConvReshapeModule(channels=input_shape[1], new_shape=new_shape) - edge_program = to_edge_program(torch_model, input_shape).exported_program() - - input_data = np.random.random(input_shape).astype(np.float32) - - converter_spy = mocker.spy(ModelBuilder, "finish") - - convert_run_compare( - edge_program, - input_data, - tflite_input_preprocess=ToChannelLastPreprocess(), - atol=2.0e-7, - conversion_config=ConversionConfig( - {"use_neutron_for_format_conversion": False} - ), - ) - - tflite_model = converter_spy.spy_return - ops = tflite_model.sub_graphs[0].operators.vector - assert len(ops) == 3 - assert isinstance(ops[0].builtin_options, Conv2D) - assert isinstance(ops[1].builtin_options, Transpose) - assert isinstance(ops[2].builtin_options, Reshape) - - -def test__view_copy__formatless_to_channels_first(mocker): - input_shape = (12, 32) - new_shape = (1, 4, 12, 8) # Mix up the dimensions for a thorough test. - - torch_model = FormatlessToChannelsFirstModule( - channels=new_shape[1], new_shape=new_shape - ) - edge_program = to_edge_program(torch_model, input_shape).exported_program() - - input_data = np.random.random(input_shape).astype(np.float32) - - converter_spy = mocker.spy(ModelBuilder, "finish") - - convert_run_compare( - edge_program, - input_data, - tflite_output_preprocess=ToChannelFirstPreprocess(), - atol=2.0e-7, - conversion_config=ConversionConfig( - {"use_neutron_for_format_conversion": False} - ), +class TestViewCopyNewFlow: + # some of the old tests are reworked to utilize NSYS so they don't fail + # the rest will be done as part of adding support for `view_copy` using new Neutron flow (EIEX-882) + @staticmethod + def assert_delegated_and_correct( + mocker, model, input_shape, exp_deleg_ops, exp_non_deleg_ops, use_qat=False + ): + graph_verifier = DetailedGraphVerifier( + mocker, + expected_delegated_ops=exp_deleg_ops, + expected_non_delegated_ops=exp_non_deleg_ops, + ) + + dataset = RandomDatasetCreator(low=-128, high=128) + comparator = AllCloseOutputComparator(atol=1) + + lower_run_compare( + model, + input_shape, + graph_verifier, + dataset, + comparator, + mocker=mocker, + use_qat=use_qat, + ) + + @pytest.mark.parametrize( + "input_shape, new_shape", + [ + pytest.param((1, 4, 7, 9), (6, 32), id="channels_first to 2D"), + pytest.param((1, 8, 6, 8), (7, 4, 2, 5), id="channels_first to 4D"), + ], ) - - tflite_model = converter_spy.spy_return - ops = tflite_model.sub_graphs[0].operators.vector - assert len(ops) == 3 - assert isinstance(ops[0].builtin_options, Reshape) - assert isinstance(ops[1].builtin_options, Transpose) - assert isinstance(ops[2].builtin_options, Conv2D) + def test__basic_nsys_inference__channels_first_input( + self, mocker, input_shape, new_shape + ): + model = ConvReshapeModule(channels=input_shape[1], new_shape=new_shape) + + self.assert_delegated_and_correct( + mocker, + model, + input_shape, + exp_deleg_ops={Convolution: 1, ViewCopy: 1}, + exp_non_deleg_ops={}, + ) + + def test__basic_nsys_inference__formatless_to_channels_first(self, mocker): + input_shape = (12, 32) + new_shape = (1, 4, 12, 8) # Mix up the dimensions for a thorough test. + + model = FormatlessToChannelsFirstModule( + channels=new_shape[1], new_shape=new_shape + ) + + self.assert_delegated_and_correct( + mocker, + model, + input_shape, + exp_deleg_ops={Convolution: 1, ViewCopy: 1}, + exp_non_deleg_ops={}, + ) def test__view_copy__formatless_to_formatless(mocker): diff --git a/backends/nxp/tests/model_output_comparator.py b/backends/nxp/tests/model_output_comparator.py index f0dd7cd2d60..81293c1380c 100644 --- a/backends/nxp/tests/model_output_comparator.py +++ b/backends/nxp/tests/model_output_comparator.py @@ -92,12 +92,12 @@ def compare_sample(self, sample_dir, cpu_output_tensors, npu_output_tensors): cpu_tensor ), "Output tensor contains only zeros. This is suspicious." all_close = np.allclose(cpu_tensor, npu_tensor, atol=self.atol) + max_diff = None if not all_close: max_diff = np.abs(cpu_tensor - npu_tensor).max() - print( - f"NPU output doesn't match reference. Maximum absolute difference: {max_diff}" - ) - assert all_close + assert ( + all_close + ), f"NPU output doesn't match reference. Maximum absolute difference: {max_diff}" def _default_postprocess_fn(outputs: np.ndarray, _: str):