From d4c9474e0eae2ef03a57bd184e818cd9e69e7989 Mon Sep 17 00:00:00 2001 From: Lukas Sztefek Date: Thu, 20 Mar 2025 16:05:33 +0100 Subject: [PATCH 1/3] NXP backend: Add support for MobilenetV2 model --- examples/nxp/aot_neutron_compile.py | 5 +- examples/nxp/models/mobilenet_v2.py | 114 ++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 examples/nxp/models/mobilenet_v2.py diff --git a/examples/nxp/aot_neutron_compile.py b/examples/nxp/aot_neutron_compile.py index 5c0634697d0..dba3db60071 100644 --- a/examples/nxp/aot_neutron_compile.py +++ b/examples/nxp/aot_neutron_compile.py @@ -37,6 +37,8 @@ from .experimental.cifar_net.cifar_net import CifarNet, test_cifarnet_model +from .models.mobilenet_v2 import MobilenetV2 + FORMAT = "[%(levelname)s %(asctime)s %(filename)s:%(lineno)s] %(message)s" logging.basicConfig(level=logging.INFO, format=FORMAT) @@ -87,7 +89,7 @@ def get_model_and_inputs_from_name(model_name: str): logging.warning( "Using a model from examples/models not all of these are currently supported" ) - model, example_inputs, _ = EagerModelFactory.create_model( + model, example_inputs, _, _ = EagerModelFactory.create_model( *MODEL_NAME_TO_MODEL[model_name] ) else: @@ -100,6 +102,7 @@ def get_model_and_inputs_from_name(model_name: str): models = { "cifar10": CifarNet, + "mobilenetv2": MobilenetV2, } diff --git a/examples/nxp/models/mobilenet_v2.py b/examples/nxp/models/mobilenet_v2.py new file mode 100644 index 00000000000..ccda4155d39 --- /dev/null +++ b/examples/nxp/models/mobilenet_v2.py @@ -0,0 +1,114 @@ +# 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 itertools +from typing import Iterator + +import torch +import torchvision + +from executorch.examples.models.mobilenet_v2 import MV2Model +from torch.utils.data import DataLoader +from torchvision import transforms + + +class MobilenetV2(MV2Model): + + def get_calibration_inputs( + self, batch_size: int = 1 + ) -> Iterator[tuple[torch.Tensor]]: + """ + Returns an iterator for the Imagenette validation dataset, downloading it if necessary. + + Args: + batch_size (int): The batch size for the iterator. + + Returns: + iterator: An iterator that yields batches of images from the Imagnetette validation dataset. + """ + dataloader = self.get_dataset(batch_size) + + # Return the iterator + dataloader_iterable = itertools.starmap( + lambda data, label: (data,), iter(dataloader) + ) + + # We want approximately 500 samples + batch_count = 500 // batch_size + return itertools.islice(dataloader_iterable, batch_count) + + def get_dataset(self, batch_size): + # Define data transformations + data_transforms = transforms.Compose( + [ + transforms.Resize((224, 224)), + transforms.ToTensor(), + transforms.Normalize( + mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225] + ), # ImageNet stats + ] + ) + + dataset = torchvision.datasets.Imagenette( + root="./data", split="val", transform=data_transforms, download=True + ) + dataloader = torch.utils.data.DataLoader( + dataset, + batch_size=batch_size, + shuffle=False, + num_workers=1, + ) + return dataloader + + +def gather_samples_per_class_from_dataloader( + dataloader, num_samples_per_class=10 +) -> list[tuple]: + """ + Gathers a specified number of samples for each class from a DataLoader. + + Args: + dataloader (DataLoader): The PyTorch DataLoader object. + num_samples_per_class (int): The number of samples to gather for each class. Defaults to 10. + + Returns: + samples: A list of (sample, label) tuples. + """ + + if not isinstance(dataloader, DataLoader): + raise TypeError("dataloader must be a torch.utils.data.DataLoader object") + if not isinstance(num_samples_per_class, int) or num_samples_per_class <= 0: + raise ValueError("num_samples_per_class must be a positive integer") + + labels = sorted( + set([label for _, label in dataloader.dataset]) + ) # Get unique labels from the dataset + samples_per_label = {label: [] for label in labels} # Initialize dictionary + + for sample, label in dataloader: + label = label.item() + if len(samples_per_label[label]) < num_samples_per_class: + samples_per_label[label].append((sample, label)) + + samples = [] + + for label in labels: + samples.extend(samples_per_label[label]) + + return samples + + +def generate_input_samples_file(): + model = MobilenetV2() + dataloader = model.get_dataset(batch_size=1) + samples = gather_samples_per_class_from_dataloader( + dataloader, num_samples_per_class=2 + ) + + torch.save(samples, "calibration_data.pt") + + +if __name__ == "__main__": + generate_input_samples_file() From 744e88235fec05f4c03cd08dbbf13753a95d5f2c Mon Sep 17 00:00:00 2001 From: Lukas Sztefek Date: Tue, 25 Mar 2025 16:54:06 +0100 Subject: [PATCH 2/3] NXP backend: Add support for EdgeProgram test inference with multiple outputs --- backends/nxp/tests/executors.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/backends/nxp/tests/executors.py b/backends/nxp/tests/executors.py index 9bb0eb97193..afdb15af106 100644 --- a/backends/nxp/tests/executors.py +++ b/backends/nxp/tests/executors.py @@ -57,6 +57,13 @@ def inference( return output.detach().numpy() elif isinstance(output, tuple) and len(output) == 1: return output[0].detach().numpy() + elif isinstance(output, tuple): + output_names = self.edge_program.graph_signature.user_outputs + + return { + name: tensor.detach().numpy() + for (name, tensor) in zip(output_names, output) + } raise RuntimeError( "Edge program inference with multiple outputs not implemented" From 39c59c86a1a8270494059e81620a11d5f756002c Mon Sep 17 00:00:00 2001 From: Simon Strycek Date: Mon, 28 Jul 2025 15:28:39 +0200 Subject: [PATCH 3/3] NXP backend: Add aot example test for MobileNetV2 --- .github/workflows/pull.yml | 5 +++-- examples/nxp/run_aot_example.sh | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pull.yml b/.github/workflows/pull.yml index 5df4aa6666f..acf60b14000 100644 --- a/.github/workflows/pull.yml +++ b/.github/workflows/pull.yml @@ -860,8 +860,9 @@ jobs: # Run pytest PYTHON_EXECUTABLE=python bash backends/nxp/run_unittests.sh - # Run aot example: - PYTHON_EXECUTABLE=python bash examples/nxp/run_aot_example.sh + # Run aot examples: + PYTHON_EXECUTABLE=python bash examples/nxp/run_aot_example.sh cifar10 + PYTHON_EXECUTABLE=python bash examples/nxp/run_aot_example.sh mobilenetv2 test-vulkan-models-linux: diff --git a/examples/nxp/run_aot_example.sh b/examples/nxp/run_aot_example.sh index 1710490f6d7..7f864c6f1b8 100755 --- a/examples/nxp/run_aot_example.sh +++ b/examples/nxp/run_aot_example.sh @@ -7,11 +7,12 @@ set -eux SCRIPT_DIR=$(dirname $(readlink -fm $0)) EXECUTORCH_DIR=$(dirname $(dirname $SCRIPT_DIR)) +MODEL=${1:-"cifar10"} cd $EXECUTORCH_DIR # Run the AoT example python -m examples.nxp.aot_neutron_compile --quantize \ - --delegate --neutron_converter_flavor SDK_25_03 -m cifar10 + --delegate --neutron_converter_flavor SDK_25_03 -m ${MODEL} # verify file exists -test -f cifar10_nxp_delegate.pte +test -f ${MODEL}_nxp_delegate.pte