NXP backend: Add recipes for Neutron backend lowering. - #21516
NXP backend: Add recipes for Neutron backend lowering.#21516MartinPavella wants to merge 2 commits into
Conversation
🔗 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 FailuresAs of commit a71bbc1 with merge base 041a6ec ( This comment was automatically generated by Dr. CI and updates every 15 minutes. |
|
4c575c7 to
358c0d4
Compare
There was a problem hiding this comment.
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
EdgeTransformAndLowerStageto run an optionalpre_partitioning_callbackand applypost_partitioning_transformsfrom the lowering recipe. - Extend
QuantizeStage/QuantizationRecipeto 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.
novak-vaclav
left a comment
There was a problem hiding this comment.
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 😊
| compile_spec, | ||
| neutron_target_spec, | ||
| rc.custom_delegation_options, | ||
| preserve_ops=[torch.ops.aten.prelu.default], |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
It should be unified as I commented too. We have it also in aot_neutron_example.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). |
There was a problem hiding this comment.
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.
| m = move_exported_model_to_train(m) | ||
| qr.train_fn(m) | ||
|
|
||
| if qr.is_qat: |
There was a problem hiding this comment.
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)
roman-janik-nxp
left a comment
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
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".
There was a problem hiding this comment.
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.
| pre_partitioning_callback: Optional[ | ||
| Callable[[Optional[list[Partitioner]], dict[str, ExportedProgram]], None] | ||
| ] = None | ||
| post_partitioning_transforms: Optional[ |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
You're right. I chose post_partitioning_transforms on EdgeTransformAndLowerStage over EdgeProgramManagerTransformStage for two reasons:
-
EDGE_PROGRAM_MANAGER_TRANSFORMis not in the default pipeline. Adding it would affect all recipes, and sinceTO_EDGE_TRANSFORM_AND_LOWERwas 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. -
Non-default stages are not supported by recipe combining. The alternative was to add
EDGE_PROGRAM_MANAGER_TRANSFORMonly for the Neutron recipe viapipeline_stages, but_combine_recipescurrently ignorespipeline_stagesentirely, 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?
JakeStevens
left a comment
There was a problem hiding this comment.
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. Is that OK with you? |
358c0d4 to
965b209
Compare
7c59abc to
3cdbda7
Compare
|
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 |
3cdbda7 to
316d247
Compare
316d247 to
a71bbc1
Compare
novak-vaclav
left a comment
There was a problem hiding this comment.
A few of mine and AI's comments, but nothing major. Good job 👍😊
| captured = [] | ||
| original_init = NeutronPartitioner.__init__ | ||
|
|
||
| def capturing_init(self_, *args, **kwargs): |
There was a problem hiding this comment.
nit: I believe the capturing logic could be generalised, however it's definitely not necessary if it decreases the code's clarity.
|
|
||
| def _chained_pre_partitioning_callback(partitioners, programs): | ||
| for cb in _cbs: | ||
| cb(partitioners, programs) |
There was a problem hiding this comment.
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 = [ |
There was a problem hiding this comment.
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.
| # 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) |
There was a problem hiding this comment.
nit: AI suggested checking the correctness of return type of transform(edge_program_manager) to allow easier debugging, I'd add it as well.
| lowering_recipe is not None | ||
| and lowering_recipe.pre_partitioning_callback is not None | ||
| ): | ||
| lowering_recipe.pre_partitioning_callback(self._partitioners, artifact.data) |
There was a problem hiding this comment.
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
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.pycc @robert-kalmar @JakeStevens @digantdesai @rascani