-
Notifications
You must be signed in to change notification settings - Fork 1k
[EIEX-885] Add log support using new Neutron flow
#20145
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| # Copyright 2026 NXP | ||
| # | ||
| # This source code is licensed under the BSD-style license found in the | ||
| # LICENSE file in the root directory of this source tree. | ||
| import torch | ||
| from executorch.backends.nxp.backend.ir.converter.node_converter import ( | ||
| CustomDelegationOptions, | ||
| NeutronTargetSpec, | ||
| NodeConverter, | ||
| ) | ||
| from executorch.backends.nxp.backend.ir.lib.tflite.BuiltinOperator import ( | ||
| BuiltinOperator, | ||
| ) | ||
| from torch.fx import Node | ||
| from torch.nn import Parameter | ||
|
|
||
|
|
||
| class LogConverter(NodeConverter): | ||
|
|
||
| @staticmethod | ||
| def _is_supported_in_IR( | ||
| node: Node, | ||
| parameters_mapping: dict[str, Parameter], | ||
| custom_delegation_options: CustomDelegationOptions, | ||
| ) -> bool: | ||
| return True | ||
|
|
||
| @staticmethod | ||
| def _is_supported_on_target( | ||
| node: Node, | ||
| neutron_target_spec: NeutronTargetSpec, | ||
| parameters_mapping: dict[str, Parameter], | ||
| custom_delegation_options: CustomDelegationOptions, | ||
| ) -> bool: | ||
| # Requirements specified by the new Neutron flow documentation. | ||
| # Input and Output must be INT8/UINT8. | ||
| if not NodeConverter.uses_quantization_type_for_io( | ||
| node, | ||
| supported_types=[torch.int8, torch.uint8], | ||
| input_indices=[0], | ||
| output_indices=[0], | ||
| ): | ||
| return False | ||
| return True | ||
|
|
||
| def convert(self, node: Node): | ||
| """Convert the `aten.log.default` operator to Neutron IR `Log`. | ||
| The schema is: | ||
| aten::log( | ||
| Tensor self | ||
| ) -> Tensor | ||
| """ | ||
|
|
||
| self.assert_convertible(node) | ||
|
|
||
| t_op = self._create_tflite_op_with_io_tensors(node) | ||
| t_op.opcode_index = self.builder.op_code_index_for_op_type(BuiltinOperator.LOG) | ||
|
|
||
| self.builder.append_operators([t_op]) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -337,3 +337,81 @@ def _gen_samples( | |
|
|
||
| bin_file_name = f"{dataset_dir}/example_{label}_{cl}_{i}_i{str(inp_idx).zfill(2)}.bin" | ||
| dt.tofile(bin_file_name) | ||
|
|
||
|
|
||
| class LinearRampDatasetCreator(DatasetCreator): | ||
| """Dataset creator that generates deterministic linear ramp input samples. | ||
|
|
||
| The generated data forms a monotonic sequence where values are evenly | ||
| distributed between a specified range (low to high) and span the full | ||
| interval. The first element is equal to `low` and the last element is | ||
| equal to `high`, with increments depending on the total number of elements. | ||
| """ | ||
|
|
||
| def __init__(self, num_samples=2, low=0.0, high=1.0): | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: Perhaps it would be more useful to have the default range include negative values (for example if we want to use this to test relu/prelu/...). How about |
||
| self._num_samples = num_samples | ||
| self.low = low | ||
| self.high = high | ||
|
|
||
| def generate_samples( | ||
| self, dataset_dir: str, input_spec: list[ModelInputSpec] | ||
| ) -> tuple[str, str]: | ||
| assert isinstance(input_spec, list) and all( | ||
| isinstance(spec, ModelInputSpec) for spec in input_spec | ||
| ), "Input_spec must be a list of ModelInputSpec." | ||
|
|
||
| calibration_dir, test_dir = ( | ||
| _get_calibration_and_testing_dataset_directory_names(dataset_dir) | ||
| ) | ||
|
|
||
| if any(spec.dim_order == torch.channels_last for spec in input_spec): | ||
| # We will need to generate a separate testing dataset, containing the same data as is in the calibration | ||
| # dataset, just permuted to channels last where necessary. | ||
| self._gen_samples(test_dir, input_spec) | ||
|
|
||
| else: | ||
| # Use the calibration dataset for testing as well. | ||
| test_dir = calibration_dir | ||
|
|
||
| # Make sure the calibration dataset contains contiguous tensors. | ||
| contiguous_input_spec = deepcopy(input_spec) | ||
| for spec in contiguous_input_spec: | ||
| spec.dim_order = torch.contiguous_format | ||
|
|
||
| # Generate the calibration dataset. Calibration amd testing dataset s will contain | ||
| # the same data (except for the permutation). | ||
| self._gen_samples(calibration_dir, contiguous_input_spec) | ||
|
|
||
| return calibration_dir, test_dir | ||
|
|
||
| def _gen_samples(self, dataset_dir: str, input_spec: list[ModelInputSpec]): | ||
| for idx in range(self._num_samples): | ||
| sample_dir = dataset_dir | ||
|
|
||
| # Multi-input, use a subdirectory containing the inputs for each sample | ||
| if len(input_spec) > 1: | ||
| sample_dir = os.path.join(dataset_dir, f"{str(idx).zfill(4)}") | ||
| mkdir(sample_dir) | ||
|
|
||
| for spec_idx, spec in enumerate(input_spec): | ||
| match spec.dim_order: | ||
| case torch.contiguous_format: | ||
| shape = spec.shape | ||
| case torch.channels_last: | ||
| shape = tuple( | ||
| translator.dims_to_channels_last(list(spec.shape)) | ||
| ) | ||
| case _: | ||
| raise ValueError(f"Unsupported dim_order: {spec.dim_order}") | ||
|
|
||
| sample_vector = ( | ||
| np.linspace(self.low, self.high, num=np.prod(shape)) | ||
| .astype(torch_type_to_numpy_type(spec.dtype)) | ||
| .reshape(shape) | ||
| ) | ||
| file_name = ( | ||
| f"{str(spec_idx).zfill(2)}.bin" | ||
| if len(input_spec) > 1 | ||
| else f"{str(idx).zfill(4)}.bin" | ||
| ) | ||
| sample_vector.tofile(os.path.join(sample_dir, file_name)) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| # Copyright 2026 NXP | ||
| # | ||
| # This source code is licensed under the BSD-style license found in the | ||
| # LICENSE file in the root directory of this source tree. | ||
|
|
||
| import numpy as np | ||
|
|
||
| # noinspection PyUnusedImports | ||
| import pytest | ||
| import torch | ||
|
|
||
| from executorch.backends.nxp.tests.graph_verifier import DetailedGraphVerifier | ||
| from executorch.backends.nxp.tests.nsys_testing import lower_run_compare | ||
| from executorch.backends.nxp.tests.ops_aliases import Clamp, Convolution, Log, Relu | ||
| from executorch.backends.nxp.tests.use_qat import * # noqa F403 | ||
| from executorch.backends.nxp.tests.dataset_creator import LinearRampDatasetCreator | ||
|
|
||
|
|
||
| @pytest.fixture(autouse=True) | ||
| def reseed_model_per_test_run(): | ||
| torch.manual_seed(42) | ||
| np.random.seed(23) | ||
|
|
||
|
|
||
| class LogModule(torch.nn.Module): | ||
|
|
||
| def __init__(self): | ||
| super().__init__() | ||
|
|
||
| def forward(self, x): | ||
| return torch.log(x) | ||
|
|
||
|
|
||
| class ConvBlocksWithLogModule(torch.nn.Module): | ||
| def __init__(self, conv_in_channels: int = 3): | ||
| super().__init__() | ||
| self.block1 = torch.nn.Sequential( | ||
| torch.nn.Conv2d( | ||
| in_channels=conv_in_channels, | ||
| out_channels=3, | ||
| kernel_size=(2, 2), | ||
| stride=(2, 2), | ||
| ), | ||
| torch.nn.ReLU(), | ||
| ) | ||
| self.block2 = torch.nn.Sequential( | ||
| torch.nn.Conv2d( | ||
| in_channels=conv_in_channels, | ||
| out_channels=10, | ||
| kernel_size=(2, 2), | ||
| stride=(2, 2), | ||
| ), | ||
| torch.nn.ReLU(), | ||
| ) | ||
|
|
||
| def forward(self, x): | ||
| x = self.block1(x) | ||
| x = x.clamp_min(1e-6).log() | ||
| return self.block2(x) | ||
|
Comment on lines
+34
to
+59
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No change needed here, but such a complex test model is counterproductive. We prefer simple models (1 to 3 ops at most) which focus on some specific case as efficiently as possible. Here fore example, the |
||
|
|
||
|
|
||
| class TestLog: | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please add a test using |
||
| def test__basic_nsys_inference(self, mocker): | ||
| # Use 256 elements so that, after quantization to uint8, the input can | ||
| # cover the full discrete range [0, 255]. | ||
| # The dataset is generated as a linear float ramp and later quantized, | ||
| # which effectively exercises all uint8 values. | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: |
||
| input_shape = (256,) | ||
| model = LogModule() | ||
| graph_verifier = DetailedGraphVerifier( | ||
| mocker, expected_delegated_ops={Log: 1}, expected_non_delegated_ops={} | ||
| ) | ||
| lower_run_compare( | ||
| model, | ||
| input_shape, | ||
| graph_verifier, | ||
| dataset_creator=LinearRampDatasetCreator(), | ||
| ) | ||
|
|
||
| def test__basic_nsys_inference__with_conv(self, mocker): | ||
| input_shape = (2, 3, 6, 7) | ||
| in_channels = input_shape[1] | ||
| model = ConvBlocksWithLogModule(conv_in_channels=in_channels) | ||
|
|
||
| # `clamp` and one `relu` ends up in the same delegated partition as `log` | ||
| graph_verifier = DetailedGraphVerifier( | ||
| mocker, | ||
| expected_delegated_ops={Log: 1, Relu: 1, Clamp: 1}, | ||
| expected_non_delegated_ops={Relu: 1, Convolution: 2}, | ||
| ) | ||
|
|
||
| lower_run_compare( | ||
| model, | ||
| input_shape, | ||
| graph_verifier, | ||
| ) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice idea 👍🏻
Does this also work with the "int8 testing" added by @roman-janik-nxp last week?