Skip to content

NXP backend: Add recipes for Neutron backend lowering. - #21516

Open
MartinPavella wants to merge 2 commits into
pytorch:mainfrom
nxp-upstream:nxg01483/EIEX-999-implement-recipe-for-neutron-backend
Open

NXP backend: Add recipes for Neutron backend lowering.#21516
MartinPavella wants to merge 2 commits into
pytorch:mainfrom
nxp-upstream:nxg01483/EIEX-999-implement-recipe-for-neutron-backend

Conversation

@MartinPavella

@MartinPavella MartinPavella commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Summary

This PR introduces a declarative recipe-based lowering for the NXP Neutron backend. The previous solution was implemented as executorch_pipeline.py:to_quantized_executorch_program(). The new solution provides the same functionailty and produces the same results. The benefit of the recipe-based approach is compatibility with other backends (recipe fusing) and adhering to ExecuTorch standards.

Once this is merged, the old Neutron lowering pipeline can be removed completely.

Test plan

pytest backends/nxp/tests/generic_tests/test_recipe_export.py

cc @robert-kalmar @JakeStevens @digantdesai @rascani

@MartinPavella MartinPavella self-assigned this Jul 31, 2026
@MartinPavella MartinPavella added module: nxp Issues related to NXP Neutron NPU delegation and code under backends/nxp/ release notes: nxp Changes to the NXP Neutron backend delegate labels Jul 31, 2026
@pytorch-bot

pytorch-bot Bot commented Jul 31, 2026

Copy link
Copy Markdown

🔗 Helpful Links

🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/21516

Note: Links to docs will display an error until the docs builds have been completed.

✅ No Failures

As of commit a71bbc1 with merge base 041a6ec (image):
💚 Looks good so far! There are no failures yet. 💚

This comment was automatically generated by Dr. CI and updates every 15 minutes.

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Jul 31, 2026
@linux-foundation-easycla

linux-foundation-easycla Bot commented Jul 31, 2026

Copy link
Copy Markdown

CLA Not Signed

@MartinPavella
MartinPavella force-pushed the nxg01483/EIEX-999-implement-recipe-for-neutron-backend branch from 4c575c7 to 358c0d4 Compare August 3, 2026 10:40
@rascani
rascani requested a review from JacobSzwejbka August 3, 2026 12:48
@novak-vaclav
novak-vaclav requested a review from Copilot August 3, 2026 15:38

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR adds a recipe-driven export/lowering path for the NXP Neutron backend by extending the generic export pipeline to support additional lowering hooks (pre-partition callback, post-partition transforms) and a richer quantization recipe flow (PTQ/QAT phases + pass hooks). It also introduces NXP-specific recipe types/provider implementations and a new test suite to validate recipe behavior and combination.

Changes:

  • Extend EdgeTransformAndLowerStage to run an optional pre_partitioning_callback and apply post_partitioning_transforms from the lowering recipe.
  • Extend QuantizeStage / QuantizationRecipe to support QAT (prepare_qat_pt2e), optional training, custom calibration inputs, and post-phase GraphModule pass hooks.
  • Add NXP Neutron recipe types/provider and a new test suite validating PTQ/QAT/no-delegate flows and recipe-combination behavior.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
export/stages.py Adds lowering pre/post partition hooks and expands quantization flow to support QAT/training/calibration hooks.
export/recipe.py Extends recipe dataclasses (quantization + lowering) and updates recipe-combination logic for new hooks.
backends/nxp/tests/generic_tests/test_recipe_export.py New tests covering Neutron recipe export behavior, config flags, and recipe combination.
backends/nxp/tests/executorch_pipeline.py Deprecates imperative lowering helpers in favor of recipes.
backends/nxp/recipes/nxp_recipe_types.py Introduces NXP-specific RecipeType enum for Neutron exports.
backends/nxp/recipes/nxp_recipe_provider.py Implements the NXP recipe provider, recipe config, and lowering/quantization recipe builders.
backends/nxp/edge_passes/neutron_edge_pass_manager.py Switches PassManager import to ExecuTorch’s pass manager implementation.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread export/stages.py Outdated
Comment thread backends/nxp/recipes/nxp_recipe_provider.py Outdated
Comment thread backends/nxp/tests/generic_tests/test_recipe_export.py
Comment thread backends/nxp/recipes/nxp_recipe_provider.py
@robert-kalmar robert-kalmar changed the title NXB backend: Add recipes for Neutron backend lowering. NXP backend: Add recipes for Neutron backend lowering. Aug 4, 2026
Comment thread backends/nxp/tests/executorch_pipeline.py

@novak-vaclav novak-vaclav left a comment

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.

Below I have a few questions, comments and suggestions, otherwise very good job!!
I really like the idea of recipes, I believe it will make everything much more understandable for our users 👍👍👍😄

btw. I'm attaching an extended test suite my AI agents generated, feel free to run it to check the correctness after making modifications to the code or to draw inspiration from 😊

test_recipe_export_extended.py

Comment thread backends/nxp/recipes/nxp_recipe_provider.py
Comment thread backends/nxp/recipes/nxp_recipe_provider.py Outdated
compile_spec,
neutron_target_spec,
rc.custom_delegation_options,
preserve_ops=[torch.ops.aten.prelu.default],

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.

This comment might not be directly related to this PR, but I think it's time to refactor the preserve_ops list we now use in multiple different calls (afaik it's here and in executorch_pipeline).
I'm saying this because the logic of preserving ops became more complicated and conditional in the aten.pad PR, and also Roman added aten.hardswish in his PR.
I suggest modifying NeutronPartitioner to set preserve_ops to [prelu, hardswish, pad] as default or extracting [prelu, hardswish, pad] to some global variable.
There is also a core_aten_ops_exception_list variable in our backend, which seems to do something similar to preserve_ops.
If you don't want to solve it in this PR, just add aten.pad and aten.harswish here and we will tackle the refactoring in another issue.

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 should be unified as I commented too. We have it also in aot_neutron_example.py.

Comment thread backends/nxp/tests/generic_tests/test_recipe_export.py
Comment thread backends/nxp/tests/generic_tests/test_recipe_export.py Outdated
Comment thread backends/nxp/recipes/nxp_recipe_provider.py
post_prepare_passes=[_post_prepare],
# Applied after training (or after prepare when no train_fn).
post_train_passes=[_remove_simulated_bn_and_fuse],
# Applied after calibration (only reached when train_fn is None).

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.

Are you sure post_calibration_passes are applied only when train_fn is None?
in stages.py on line 512, I see post_calibration_passes are always run.

Comment thread export/stages.py Outdated
m = move_exported_model_to_train(m)
qr.train_fn(m)

if qr.is_qat:

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.

I would restructure these conditions containing qr.is_qat and qr.train_fn, since train_fn is not None implies qr.is_qat (or vice versa). Because of move_exported_model_to_train being idempotent, I would simply do:

if qr.train_fn is not None:
    m = move_exported_model_to_train(m)
    qr.train_fn(m)

m = move_exported_model_to_eval(m)

Comment thread backends/nxp/recipes/nxp_recipe_provider.py Outdated
Comment thread backends/nxp/recipes/nxp_recipe_provider.py

@roman-janik-nxp roman-janik-nxp left a comment

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.

Great job Martin 👍 . Implementation is solid in my opinion. I found mostly text errors or bugs in tests. I studied the code and the classes thoroughly to try to understand it.

case NXPRecipeType.INT8_PTQ_NEUTRON:
return self._build_recipe(recipe_type, rc, is_qat=False, delegate=True)
case NXPRecipeType.INT8_QAT_NEUTRON:
if rc.train_fn is None:

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.

Is the train_fn really needed to be required? In calibrate_and_quantize() we don't require it. In _build_quantization_recipe() you reference it "or after prepare when no train_fn".

Comment thread backends/nxp/recipes/nxp_recipe_provider.py Outdated
Comment thread backends/nxp/tests/generic_tests/test_recipe_export.py
Comment thread backends/nxp/tests/generic_tests/test_recipe_export.py
Comment thread backends/nxp/tests/generic_tests/test_recipe_export.py Outdated
Comment thread backends/nxp/tests/generic_tests/test_recipe_export.py Outdated
Comment thread backends/nxp/tests/generic_tests/test_recipe_export.py
Comment thread backends/nxp/tests/executorch_pipeline.py
Comment thread export/recipe.py

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think the changes here to recipe & stages are significant enough to warrant a separate PR. I think this highlighted a gap in recipes.

We also need to add test coverage here, particularly around the new QAT infra.

Comment thread export/recipe.py
pre_partitioning_callback: Optional[
Callable[[Optional[list[Partitioner]], dict[str, ExportedProgram]], None]
] = None
post_partitioning_transforms: Optional[

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Shouldn't we be able to do this with edge_manager_transform_passes? I think that is commented out for to_edge_and_lower, but I'm not sure why.

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.

You're right. I chose post_partitioning_transforms on EdgeTransformAndLowerStage over EdgeProgramManagerTransformStage for two reasons:

  1. EDGE_PROGRAM_MANAGER_TRANSFORM is not in the default pipeline. Adding it would affect all recipes, and since TO_EDGE_TRANSFORM_AND_LOWER was already commented out as a valid predecessor (with a # TODO, as you mentioned), I was cautious about whether there was a known reason for that restriction.

  2. Non-default stages are not supported by recipe combining. The alternative was to add EDGE_PROGRAM_MANAGER_TRANSFORM only for the Neutron recipe via pipeline_stages, but _combine_recipes currently ignores pipeline_stages entirely, which would make combined recipes silently broken. And I didn't want to tackle that in this PR.

Adding the passes inline to EdgeTransformAndLowerStage was the safer option given these constraints. The proper fix (enabling TO_EDGE_TRANSFORM_AND_LOWER as a valid predecessor for EdgeProgramManagerTransformStage and adding pipeline_stages support to recipe combining) would be a good follow-up PR.

What do you think?

Comment thread export/recipe.py Outdated

@JakeStevens JakeStevens left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I agree with @rascani .

I would prefer

(1) We implement a PTQ recipe first, so can make sure Neutron recipes are working for exisiting recipes
(2) split out the QAT changes to a separate PR. There will likely be some design discussion here.
(3) Implement the Neutron QAT recipe

@MartinPavella

Copy link
Copy Markdown
Collaborator Author

I agree with @rascani .

I would prefer

(1) We implement a PTQ recipe first, so can make sure Neutron recipes are working for exisiting recipes (2) split out the QAT changes to a separate PR. There will likely be some design discussion here. (3) Implement the Neutron QAT recipe

Thank you @JakeStevens @rascani for the insights.
I agree with your suggestions. I will update this PR to only include the Neutron PTQ recipe, with minimal changes to the stages and recipes. Then, I will raise a second PR with the QAT updates to the QuantizeStage and QuantizationRecipe. Once that is resolved, a third PR will enable QAT for the Neutron recipe.

Is that OK with you?

@MartinPavella
MartinPavella force-pushed the nxg01483/EIEX-999-implement-recipe-for-neutron-backend branch from 358c0d4 to 965b209 Compare August 17, 2026 12:36
@MartinPavella
MartinPavella force-pushed the nxg01483/EIEX-999-implement-recipe-for-neutron-backend branch 3 times, most recently from 7c59abc to 3cdbda7 Compare August 17, 2026 13:54
@MartinPavella

Copy link
Copy Markdown
Collaborator Author

The changes suggested by the reviewers have been addressed and implemented. Now, this PR only contains the PTQ Neutron recipe, with only the minimal required modifications to recipe.by and stages.py.
Comments regarding QAT will be taken into account in a parallel PR soon.
Please feel free to review again.

@MartinPavella
MartinPavella force-pushed the nxg01483/EIEX-999-implement-recipe-for-neutron-backend branch from 3cdbda7 to 316d247 Compare August 18, 2026 12:23
@MartinPavella
MartinPavella force-pushed the nxg01483/EIEX-999-implement-recipe-for-neutron-backend branch from 316d247 to a71bbc1 Compare August 18, 2026 12:39

@novak-vaclav novak-vaclav left a comment

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.

A few of mine and AI's comments, but nothing major. Good job 👍😊

captured = []
original_init = NeutronPartitioner.__init__

def capturing_init(self_, *args, **kwargs):

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.

nit: I believe the capturing logic could be generalised, however it's definitely not necessary if it decreases the code's clarity.

Comment thread export/recipe.py

def _chained_pre_partitioning_callback(partitioners, programs):
for cb in _cbs:
cb(partitioners, programs)

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.

nit: it would be better to add try... except and log any errors during the execution. Or raise RuntimeError


# Aten dialect operators that are allowed to be in the edge dialect model. These operators are usually created by a
# transform pass or by a prevented operator decomposition during lowering to edge.
core_aten_ops_exception_list = [

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.

Isn't this the same as default_preserve_ops? We preserve them so they are not decomposed into simpler edge ops - in another words, we allow them to be in the edge dialect model.

Comment thread export/stages.py
# that require the fully partitioned graph or for side effect operations.
if lowering_recipe is not None and lowering_recipe.post_partitioning_transforms:
for transform in lowering_recipe.post_partitioning_transforms:
edge_program_manager = transform(edge_program_manager)

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.

nit: AI suggested checking the correctness of return type of transform(edge_program_manager) to allow easier debugging, I'd add it as well.

Comment thread export/stages.py
lowering_recipe is not None
and lowering_recipe.pre_partitioning_callback is not None
):
lowering_recipe.pre_partitioning_callback(self._partitioners, artifact.data)

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.

nit: AI says it's cleaner to forbid self._partitioners to be None and use [] instead. I agree since partition list equal to None might be a bit confusing for people creating the pre_partitioning_callbacks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. module: nxp Issues related to NXP Neutron NPU delegation and code under backends/nxp/ release notes: nxp Changes to the NXP Neutron backend delegate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants