Skip to content

Commit fb67e17

Browse files
NXP backend: Add generate_etrecord to ExportRecipe. (pytorch#23106)
### Summary `export()` has a `generate_etrecord` parameter. When using profiling, users had to set both profiling flags on the recipe and `generate_etrecord=True` on the export call separately, with no enforcement that the two were kept in sync. This PR adds `generate_etrecord: bool = False` to `ExportRecipe` so backends can declare the requirement as part of the recipe. `ExportSession` resolves the effective value with OR semantics: `generate_etrecord or getattr(export_recipe, "generate_etrecord", False)`. `ExportRecipe.combine()` also uses OR semantics so the flag propagates correctly across combined recipes. `NXPRecipeProvider` sets `generate_etrecord=rc.use_profiling` on the Neutron recipe, so users no longer need to pass `generate_etrecord=True` to `export()` when enabling profiling. ### Test plan `pytest export/tests/test_export_session.py export/tests/test_export_recipe.py backends/nxp/tests/generic_tests/test_recipe_export.py` cc @robert-kalmar @JakeStevens @digantdesai @rascani
1 parent a33342d commit fb67e17

6 files changed

Lines changed: 88 additions & 4 deletions

File tree

‎backends/nxp/recipes/nxp_recipe_provider.py‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,9 @@ class NeutronRecipeConfig:
108108
use_neutron_for_format_conversion: Whether Neutron handles data-format conversion.
109109
fetch_constants_to_sram: Place constant tensors in SRAM on the target.
110110
dump_kernel_selection_code: Generate kernel-selection files after compilation.
111-
use_profiling: Enable Neutron execution profiling. IMPORTANT: To also generate an
112-
ETRecord, pass generate_etrecord=True to export() separately.
111+
use_profiling: Enable Neutron execution profiling. When True, the recipe
112+
automatically sets generate_etrecord=True so that a matching
113+
ETRecord is produced without any extra configuration.
113114
train_fn: Training function required for QAT recipe types (INT8_QAT_NEUTRON and
114115
INT8_QAT_NO_DELEGATE). Receives the prepared GraphModule and must
115116
perform the training loop. Ignored for PTQ recipe types.
@@ -219,6 +220,9 @@ def _build_recipe(
219220
executorch_backend_config=ExecutorchBackendConfig(
220221
extract_delegate_segments=False
221222
),
223+
# Profiling requires an ETRecord; set the flag here so callers do
224+
# not need to pass generate_etrecord=True to export() separately.
225+
generate_etrecord=rc.use_profiling,
222226
)
223227

224228

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,29 @@ def capturing_init(self_, *args, **kwargs):
244244
spec_map = {s.key: s.value.decode() for s in captured[0].delegation_spec[1]}
245245
assert spec_map["use_profiling"] == "True"
246246

247+
def test_use_profiling_sets_generate_etrecord_true(self):
248+
"""use_profiling=True must cause the returned recipe to have generate_etrecord=True."""
249+
rc = NeutronRecipeConfig(INPUT_SHAPE, use_profiling=True)
250+
recipe = NXPRecipeProvider().create_recipe(
251+
NXPRecipeType.INT8_PTQ_NEUTRON, neutron_recipe_config=rc
252+
)
253+
assert recipe.generate_etrecord is True
254+
255+
def test_use_profiling_false_leaves_generate_etrecord_false(self):
256+
"""use_profiling=False must leave generate_etrecord as False on the recipe."""
257+
rc = NeutronRecipeConfig(INPUT_SHAPE, use_profiling=False)
258+
recipe = NXPRecipeProvider().create_recipe(
259+
NXPRecipeType.INT8_PTQ_NEUTRON, neutron_recipe_config=rc
260+
)
261+
assert recipe.generate_etrecord is False
262+
263+
def test_use_profiling_generates_etrecord(self):
264+
"""use_profiling=True must produce a non-None ETRecord after full export."""
265+
model = SimpleCNN()
266+
rc = NeutronRecipeConfig(INPUT_SHAPE, use_profiling=True)
267+
sess = _run_export(model, rc)
268+
assert sess.get_etrecord() is not None
269+
247270
def test_dump_kernel_selection_code(self, tmp_path, monkeypatch):
248271
"""dump_kernel_selection_code=True causes a kernel selection C file to be written."""
249272
monkeypatch.chdir(tmp_path)

‎export/export.py‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ def __init__(
163163
dynamic_shapes: Optional dynamic shape specifications
164164
constant_methods: Optional dictionary of constant methods
165165
artifact_dir: Optional directory to store artifacts
166-
generate_etrecord: Optional flag to generate an etrecord
166+
generate_etrecord: Optional flag to generate an etrecord. Can also be provided by the ExportRecipe.
167167
"""
168168
# Load model from file if string path provided
169169
if isinstance(model, str):
@@ -231,7 +231,8 @@ def __init__(
231231
"export_recipe": self._export_recipe,
232232
"session_name": name,
233233
"artifact_dir": artifact_dir,
234-
"generate_etrecord": generate_etrecord,
234+
"generate_etrecord": generate_etrecord
235+
or getattr(export_recipe, "generate_etrecord", False),
235236
}
236237

237238
self._stage_to_artifacts: Dict[StageType, PipelineArtifact] = {}

‎export/recipe.py‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,7 @@ class _CombineAccumulator:
258258
pipeline_stages_values: list = field(default_factory=list)
259259
source_transform_in_place_values: list = field(default_factory=list)
260260
release_intermediate_artifacts_values: list = field(default_factory=list)
261+
generate_etrecord_values: list = field(default_factory=list)
261262
backend_config: object = None
262263
pre_partitioning_callbacks: list = field(default_factory=list)
263264

@@ -307,6 +308,8 @@ class ExportRecipe:
307308
pipeline_stages: Optional list of stages to execute, defaults to a standard pipeline.
308309
mode: Export mode (debug or release)
309310
strict: Set the strict flag in the torch export call.
311+
generate_etrecord: When True, the export pipeline captures an ETRecord for
312+
use with the ExecuTorch devtools (profiling, debugging).
310313
"""
311314

312315
name: Optional[str] = None
@@ -326,6 +329,7 @@ class ExportRecipe:
326329
] = None
327330
pre_trace_hooks: Optional[List[Callable[[str, torch.nn.Module], None]]] = None
328331
release_intermediate_artifacts: bool = False
332+
generate_etrecord: bool = False
329333

330334
@classmethod
331335
def get_recipe(cls, recipe: "RecipeType", **kwargs) -> "ExportRecipe":
@@ -626,6 +630,7 @@ def _collect_recipe_fields(
626630
acc.release_intermediate_artifacts_values.append(
627631
recipe.release_intermediate_artifacts
628632
)
633+
acc.generate_etrecord_values.append(recipe.generate_etrecord)
629634

630635
# Use the executorch_backend_config from the first recipe that supplies one.
631636
if acc.backend_config is None and recipe.executorch_backend_config:
@@ -711,4 +716,6 @@ def _combine_recipes(
711716
if acc.source_transform_in_place_values
712717
else False
713718
),
719+
# OR semantics: generate ETRecord when at least one recipe requests it.
720+
generate_etrecord=any(acc.generate_etrecord_values),
714721
)

‎export/tests/test_export_recipe.py‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -850,3 +850,19 @@ def test_pass_lists_preserved_when_only_one_recipe_contributes(self) -> None:
850850
)
851851
result = ExportRecipe.combine([r1, r2])
852852
self.assertEqual(result.quantization_recipe.pre_prepare_passes, [p])
853+
854+
def test_combine_generate_etrecord_or_semantics_true_false(self) -> None:
855+
# Any recipe requesting ETRecord generation must make the combined recipe
856+
# request it as well.
857+
r1 = ExportRecipe(name="a", generate_etrecord=True)
858+
r2 = ExportRecipe(name="b", generate_etrecord=False)
859+
result = ExportRecipe.combine([r1, r2])
860+
self.assertTrue(result.generate_etrecord)
861+
862+
def test_combine_generate_etrecord_false_when_none_set(self) -> None:
863+
# When no recipe requests ETRecord generation the combined recipe must
864+
# not request it either.
865+
r1 = ExportRecipe(name="a")
866+
r2 = ExportRecipe(name="b")
867+
result = ExportRecipe.combine([r1, r2])
868+
self.assertFalse(result.generate_etrecord)

‎export/tests/test_export_session.py‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,39 @@ def test_context_propagation_through_pipeline(self) -> None:
284284
self.assertEqual(session._run_context["session_name"], "test_session")
285285
self.assertIsNotNone(session._run_context["constant_methods"])
286286

287+
def test_generate_etrecord_from_recipe(self) -> None:
288+
# When the recipe has generate_etrecord=True and the param is False,
289+
# the effective value in the run context must be True.
290+
recipe = ExportRecipe(name="test", generate_etrecord=True)
291+
session = ExportSession(
292+
model=self.model,
293+
example_inputs=self.example_inputs,
294+
export_recipe=recipe,
295+
)
296+
self.assertTrue(session._run_context["generate_etrecord"])
297+
298+
def test_generate_etrecord_from_param_overrides_recipe(self) -> None:
299+
# The explicit parameter must win when the recipe has generate_etrecord=False.
300+
recipe = ExportRecipe(name="test", generate_etrecord=False)
301+
session = ExportSession(
302+
model=self.model,
303+
example_inputs=self.example_inputs,
304+
export_recipe=recipe,
305+
generate_etrecord=True,
306+
)
307+
self.assertTrue(session._run_context["generate_etrecord"])
308+
309+
def test_generate_etrecord_false_when_neither_set(self) -> None:
310+
# Both the recipe and the explicit parameter default to False, so the
311+
# effective value must also be False.
312+
recipe = ExportRecipe(name="test")
313+
session = ExportSession(
314+
model=self.model,
315+
example_inputs=self.example_inputs,
316+
export_recipe=recipe,
317+
)
318+
self.assertFalse(session._run_context["generate_etrecord"])
319+
287320
def test_stage_registry_unknown_stage_type(self) -> None:
288321
# Test error handling for unknown stage types in pipeline
289322
unknown_stage_type = Mock()

0 commit comments

Comments
 (0)