Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ arm-scratch/
executorch.egg-info
pip-out/
build-profiling/
**/ddr_*_temp

# Any exported models and profiling outputs
*.bin
Expand Down
2 changes: 1 addition & 1 deletion backends/nxp/neutron_partitioner.py
Original file line number Diff line number Diff line change
Expand Up @@ -436,7 +436,7 @@ def partition(self, exported_program: ExportedProgram) -> PartitionResult:

graph_module.recompile()

operators_not_to_delegate = self.delegation_spec[1][3].value.decode().split(",")
operators_not_to_delegate = self.delegation_spec[1][4].value.decode().split(",")
Comment thread
roman-janik-nxp marked this conversation as resolved.
logging.info(f"Operators not to delegate: {operators_not_to_delegate}")

parameters_mapping = EdgeProgramToIRConverter.map_inputs_to_parameters(
Expand Down
43 changes: 28 additions & 15 deletions backends/nxp/nxp_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@
#

import logging
import os
import struct
from typing import final, List, Optional
from typing import final

import numpy as np
import torch
Expand Down Expand Up @@ -45,10 +46,11 @@ class NeutronCompileSpecBuilder:
config: NeutronTargetSpec

def __init__(self):
self.compile_spec: List[CompileSpec] = []
self.compile_spec: list[CompileSpec] = []
self.compiler_flags = []
self.output_format = None
self.operators_not_to_delegate: List[str] = []
self.test_dir = None
self.operators_not_to_delegate: list[str] = []
self.use_neutron_for_format_conversion = True
self.fetch_constants_to_sram = False
self.dump_kernel_selection_code = False
Expand All @@ -62,15 +64,17 @@ def _replace_colons(self, operator: str) -> str:
def neutron_compile_spec(
self,
config: str,
extra_flags: Optional[str] = None,
operators_not_to_delegate: Optional[List[str]] = None,
test_dir: str | None = None,
extra_flags: str | None = None,
operators_not_to_delegate: list[str] | None = None,
use_neutron_for_format_conversion: bool = True,
fetch_constants_to_sram: bool = False,
dump_kernel_selection_code: bool = False,
) -> "NeutronCompileSpecBuilder":
"""Generate compile spec for Neutron NPU

:param config: Neutron accelerator configuration, e.g. "imxrt700"
:param test_dir: Test directory to store test related files.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

intermediates_dir ==> In the nxp_backend you are not storing test related data but intermediates from the nxp_backend conversion flow.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The intermediate models are related to the test. They are stored to the test directory where other test related data are store as well (results, datasets, etc.) Don't see a point in different naming.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"The intermediate models are related to the test."
==> Not related just with test. Consider it from the user perspective. He uses the eIQ Neutron backend and wants to keep the intermediate results. By using this item in the conversion config he can do it. This can be even enabled in the aot_example. He is not testing anything.

"They are stored to the test directory where other test related data are store as well (results, datasets, etc.)"
==> They are stored to the specified destination, what is context related. The test case set its to the test directory for obvious reasons. The aot_example can set it to some tmp folder, and user in its own conversion pipeline can set it differently. From the perspective of the nxp_backend, there is no relation to tests.
Test use this conversion config entry to collect all the artefacts in one test specific directory.

:param extra_flags: Extra flags for the Neutron compiler
:param operators_not_to_delegate: List of operators that should not be delegated
:param use_neutron_for_format_conversion: If True, the EdgeProgramToIRConverter will insert `Transpose` ops to
Expand All @@ -83,6 +87,7 @@ def neutron_compile_spec(
"""

self.config = NeutronTargetSpec(config)
self.test_dir = test_dir if test_dir is not None else os.getcwd()

assert (
self.output_format is None
Expand Down Expand Up @@ -113,6 +118,7 @@ def build(self):
CompileSpec("output_format", "tflite".encode()),
CompileSpec("compile_flags", " ".join(self.compiler_flags).encode()),
CompileSpec("target", self.config.get_name().encode()),
CompileSpec("test_dir", f"{self.test_dir}".encode()),
CompileSpec(
"operators_not_to_delegate",
",".join(self.operators_not_to_delegate).encode(),
Expand All @@ -136,17 +142,19 @@ def build(self):

def generate_neutron_compile_spec(
config: str, # The target platform. For example "imxrt700".
system_config: Optional[str] = None,
extra_flags: Optional[str] = None,
operators_not_to_delegate: Optional[List[str]] = None,
system_config: str | None = None,
extra_flags: str | None = None,
test_dir: str | None = None,
operators_not_to_delegate: list[str] | None = None,
use_neutron_for_format_conversion: bool = True,
fetch_constants_to_sram: bool = False,
dump_kernel_selection_code: bool = False,
) -> List[CompileSpec]:
) -> list[CompileSpec]:
return (
NeutronCompileSpecBuilder()
.neutron_compile_spec(
config,
test_dir=test_dir,
extra_flags=extra_flags,
operators_not_to_delegate=operators_not_to_delegate,
use_neutron_for_format_conversion=use_neutron_for_format_conversion,
Expand All @@ -163,7 +171,7 @@ class NeutronBackend(BackendDetails):
@staticmethod
def preprocess( # noqa C901
edge_program: ExportedProgram,
compile_spec: List[CompileSpec],
compile_spec: list[CompileSpec],
) -> PreprocessResult:
logging.info("NeutronBackend::preprocess")

Expand All @@ -173,6 +181,7 @@ def preprocess( # noqa C901
compile_flags = []
binary = bytes()
target = ""
test_dir = ""
use_neutron_for_format_conversion = None
fetch_constants_to_sram = False
dump_kernel_selection_code = None
Expand All @@ -181,6 +190,8 @@ def preprocess( # noqa C901
output_format = spec.value.decode()
if spec.key == "target":
target = spec.value.decode()
if spec.key == "test_dir":
test_dir = spec.value.decode()
if spec.key == "compile_flags":
compile_flags.append(spec.value.decode())
if spec.key == "use_neutron_for_format_conversion":
Expand Down Expand Up @@ -230,14 +241,16 @@ def preprocess( # noqa C901

# Dump the tflite file if logging level is enabled
if logging.root.isEnabledFor(logging.DEBUG):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As now we already introduce the test_dir or intermediates_dir, we can replace the logic to check if this *_dir is set or None, instead of limit the functionality to DEBUG logging level only.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't understand. The functionality is (and was before) limited to DEBUG logging level only. The logic to check is test_dir is set is because now the backend can be run without the use of lower_run_compare(), e.g. from aot_run_example.py

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was limited to DEBUG level only as there was no other means to enable/disable it. Now you introduced a specific entry in the conversion_config, so you can use that. And not limit the intermediates dump only to DEBUG log level.

import os

logging.debug(
f"Serializing converted graph with tag {delegation_tag} to {os.getcwd()}"
f"Serializing converted graph with tag {delegation_tag} to {test_dir}"
)
with open(f"{delegation_tag}_pure.et.tflite", "wb") as f:
with open(
os.path.join(test_dir, f"{delegation_tag}_pure.et.tflite"), "wb"
) as f:
f.write(bytes(tflite_model))
with open(f"{delegation_tag}_neutron.et.tflite", "wb") as f:
with open(
os.path.join(test_dir, f"{delegation_tag}_neutron.et.tflite"), "wb"
) as f:
f.write(bytes(neutron_model))

binary = PayloadComposer().get_binary_payload(io_formats, neutron_model)
Expand Down
4 changes: 4 additions & 0 deletions backends/nxp/tests/executorch_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ def to_quantized_edge_program(
operators_not_to_delegate: list[str] = None,
get_calibration_inputs_fn: GetCalibrationInputsFn = get_random_calibration_inputs,
target: str = "imxrt700",
test_dir: str | None = None,
use_qat: bool = False,
train_fn: Callable[[torch.fx.GraphModule], None] | None = None,
remove_quant_io_ops: bool = False,
Expand Down Expand Up @@ -217,6 +218,7 @@ def to_quantized_edge_program(
preserve_ops = [torch.ops.aten.prelu.default]
compile_spec = generate_neutron_compile_spec(
target,
test_dir=test_dir,
operators_not_to_delegate=operators_not_to_delegate,
use_neutron_for_format_conversion=use_neutron_for_format_conversion,
fetch_constants_to_sram=fetch_constants_to_sram,
Expand Down Expand Up @@ -266,6 +268,7 @@ def to_quantized_edge_program(
def to_quantized_executorch_program(
model: torch.nn.Module,
input_spec: Iterable[ModelInputSpec] | tuple[int, ...] | list[tuple[int, ...]],
test_dir: str | None = None,
use_qat: bool = False,
train_fn: Callable[[torch.fx.GraphModule], None] | None = None,
use_neutron_for_format_conversion: bool = True,
Expand All @@ -287,6 +290,7 @@ def to_quantized_executorch_program(
edge_program_manager = to_quantized_edge_program(
model,
input_spec,
test_dir=test_dir,
use_qat=use_qat,
train_fn=train_fn,
use_neutron_for_format_conversion=use_neutron_for_format_conversion,
Expand Down
10 changes: 6 additions & 4 deletions backends/nxp/tests/generic_tests/test_cifarnet.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def cifar_test_files(tmp_path_factory):


@pytest.mark.parametrize("channels_last", [False, True])
def test_cifarnet(mocker, cifar_test_files, channels_last):
def test_cifarnet(mocker, request, cifar_test_files, channels_last):
model = (
CifarNet(
pth_file=os.path.join(
Expand Down Expand Up @@ -64,9 +64,10 @@ def test_cifarnet(mocker, cifar_test_files, channels_last):
lower_run_compare(
model,
[input_spec],
BaseGraphVerifier(1, non_dlg_nodes),
request,
dataset_creator=CopyDatasetCreator(cifar_test_files),
output_comparator=comparator,
dlg_model_verifier=BaseGraphVerifier(1, non_dlg_nodes),
mocker=mocker,
# Run the channels last reference in PyTorch as the ExecuTorch CPU model contains incorrectly
# lowered channels last convolution weights, which cause incorrect inference results. The issue
Expand All @@ -79,7 +80,7 @@ def test_cifarnet(mocker, cifar_test_files, channels_last):
)


def test_cifarnet_qat(mocker, cifar_test_files):
def test_cifarnet_qat(mocker, request, cifar_test_files):
model = CifarNet().get_eager_model().eval()

input_shape = (1, 3, 32, 32)
Expand All @@ -94,9 +95,10 @@ def test_cifarnet_qat(mocker, cifar_test_files):
lower_run_compare(
model,
input_shape,
BaseGraphVerifier(1, non_dlg_nodes),
request,
dataset_creator=CopyDatasetCreator(cifar_test_files),
output_comparator=comparator,
dlg_model_verifier=BaseGraphVerifier(1, non_dlg_nodes),
mocker=mocker,
use_qat=True,
)
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ class TestConvertDivToMul:
ids=lambda is_scalar: "scalar" if is_scalar else "tensor",
)
def test__static__full_pipeline(
self, mocker, input_shape: tuple[int, ...], is_scalar: bool
self, mocker, request, input_shape: tuple[int, ...], is_scalar: bool
):
if is_scalar:
divisor = np.random.uniform(0.01, 15)
Expand All @@ -231,5 +231,6 @@ def test__static__full_pipeline(
model,
input_shape,
graph_verifier,
request,
dataset_creator,
)
155 changes: 155 additions & 0 deletions backends/nxp/tests/generic_tests/test_debug_results.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
# 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 logging
import os

import numpy as np
import pytest
import torch

from executorch.backends.nxp.tests.executorch_pipeline import ModelInputSpec
from executorch.backends.nxp.tests.graph_verifier import BaseGraphVerifier
from executorch.backends.nxp.tests.models import AddTensorModule, AvgPool2dModule
from executorch.backends.nxp.tests.nsys_testing import (
get_test_name,
lower_run_compare,
OUTPUTS_DIR,
)


@pytest.fixture(autouse=True)
def reseed_model_per_test_run():
torch.manual_seed(23)
np.random.seed(23)


def test_nsys_test_debug_results__single_input(caplog, request):
Comment thread
roman-janik-nxp marked this conversation as resolved.
# Set log level to DEBUG to create debug results
caplog.set_level(logging.DEBUG)

input_shape = (2, 4, 6, 7)
model = AvgPool2dModule(False, 0)

graph_verifier = BaseGraphVerifier(1, [])

lower_run_compare(
model,
input_shape,
graph_verifier,
request,
remove_quant_io_ops=True,
)

test_name = get_test_name(request)
Comment thread
roman-janik-nxp marked this conversation as resolved.
# Running by CI scripts adds prefix to the name
assert "test_nsys_test_debug_results__single_input" in test_name
assert os.path.isdir(os.path.join(OUTPUTS_DIR, test_name, "diff_cpu_npu_results"))
assert os.path.isfile(os.path.join(OUTPUTS_DIR, test_name, "summary.yaml"))
Comment thread
roman-janik-nxp marked this conversation as resolved.

# Check file contains key symbols
with open(os.path.join(OUTPUTS_DIR, test_name, "summary.yaml")) as f:
content = f.read()
keys = [
"date_time",
"eiq_neutron_sdk_version",
"eiq_nsys_version",
"git_branch",
"git_commit",
"test_name",
]
assert all(key in content for key in keys)
assert os.path.isfile(
os.path.join(OUTPUTS_DIR, test_name, "tag1_neutron.et.tflite")
)
assert os.path.isfile(os.path.join(OUTPUTS_DIR, test_name, "tag1_pure.et.tflite"))

# Check text tensor variants
assert os.path.isfile(
os.path.join(OUTPUTS_DIR, test_name, "dataset", "calibration", "0000.txt")
)
assert os.path.isfile(
os.path.join(OUTPUTS_DIR, test_name, "dataset_quant", "0000.txt")
)
assert os.path.isfile(
os.path.join(OUTPUTS_DIR, test_name, "results_cpu", "0000.bin", "0000.txt")
)
assert os.path.isfile(
os.path.join(OUTPUTS_DIR, test_name, "results_npu", "0000.bin", "0000.txt")
)
assert os.path.isfile(
os.path.join(
OUTPUTS_DIR, test_name, "diff_cpu_npu_results", "0000.bin", "0000.txt"
)
)
assert os.path.isfile(os.path.join(OUTPUTS_DIR, f"{test_name}.zip"))


class TestNsysDebugResults:
def test_nsys_test_debug_results__multiple_input(self, caplog, request):
# Set log level to DEBUG to create debug results
caplog.set_level(logging.DEBUG)

input_shape = (1, 4, 7)
x_input_spec = ModelInputSpec(input_shape)
model = AddTensorModule()

graph_verifier = BaseGraphVerifier(1, [])

lower_run_compare(
model,
[x_input_spec, x_input_spec],
graph_verifier,
request,
)

test_name = get_test_name(request)
# Running by CI scripts adds prefix to the name
assert (
"TestNsysDebugResults__test_nsys_test_debug_results__multiple_input"
in test_name
)
assert os.path.isdir(
os.path.join(OUTPUTS_DIR, test_name, "diff_cpu_npu_results")
)
assert os.path.isfile(os.path.join(OUTPUTS_DIR, test_name, "summary.yaml"))

# Check file contains key symbols
with open(os.path.join(OUTPUTS_DIR, test_name, "summary.yaml")) as f:
Comment thread
roman-janik-nxp marked this conversation as resolved.
content = f.read()
keys = [
"date_time",
"eiq_neutron_sdk_version",
"eiq_nsys_version",
"git_branch",
"git_commit",
"test_name",
]
assert all(key in content for key in keys)
assert os.path.isfile(
os.path.join(OUTPUTS_DIR, test_name, "tag1_neutron.et.tflite")
)
assert os.path.isfile(
os.path.join(OUTPUTS_DIR, test_name, "tag1_pure.et.tflite")
)

# Check text tensor variants
assert os.path.isfile(
os.path.join(
OUTPUTS_DIR, test_name, "dataset", "calibration", "0000", "00.txt"
)
)
assert os.path.isfile(
os.path.join(OUTPUTS_DIR, test_name, "results_cpu", "0000", "0000.txt")
)
assert os.path.isfile(
os.path.join(OUTPUTS_DIR, test_name, "results_npu", "0000", "0000.txt")
)
assert os.path.isfile(
os.path.join(
OUTPUTS_DIR, test_name, "diff_cpu_npu_results", "0000", "0000.txt"
)
)
assert os.path.isfile(os.path.join(OUTPUTS_DIR, f"{test_name}.zip"))
Loading
Loading