Skip to content

Commit 8f21367

Browse files
committed
Arm backend: Add recipe infrastructure
Recipes were introduced with issue pytorch#12248 to simplify the ET api for new users. A backend introduces standard recipes with partitioner and quantizer configuration, and possibly passes. In addition to the benefits of integrating with existing infra, the arm backend can benefit from the recipe structure as a test-framework-agnostic way of defining how a tested model should be lowered. Additonally, it ties into simplifying the aot_arm_compiler interface. Signed-off-by: Erik Lundell <erik.lundell@arm.com> Change-Id: Iebd3f101f94a3df657af3b49ea563f72cbca3a9a
1 parent 8fbc42c commit 8f21367

File tree

4 files changed

+306
-0
lines changed

4 files changed

+306
-0
lines changed

backends/arm/recipe/__init__.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Copyright 2025 Arm Limited and/or its affiliates.
2+
#
3+
# This source code is licensed under the BSD-style license found in the
4+
# LICENSE file in the root directory of this source tree.
5+
6+
from executorch.export import recipe_registry # type: ignore[import-untyped]
7+
8+
from .recipe import ArmExportRecipe, TargetRecipe # noqa # usort: skip
9+
from .arm_recipe_types import ArmRecipeType # noqa # usort: skip
10+
from .arm_recipe_provider import ArmRecipeProvider # noqa # usort: skip
11+
12+
# Auto-register Arm recipe provider
13+
recipe_registry.register_backend_recipe_provider(ArmRecipeProvider())
14+
15+
__all__ = ["ArmRecipeType", "ArmExportRecipe", "TargetRecipe"]
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
# Copyright 2025 Arm Limited and/or its affiliates.
2+
#
3+
# This source code is licensed under the BSD-style license found in the
4+
# LICENSE file in the root directory of this source tree.
5+
6+
# pyre-strict
7+
8+
from typing import Any, Callable, Optional, Sequence
9+
10+
from executorch.backends.arm.quantizer import (
11+
get_symmetric_quantization_config,
12+
TOSAQuantizer,
13+
)
14+
from executorch.backends.arm.recipe import ArmExportRecipe, ArmRecipeType, TargetRecipe
15+
from executorch.backends.arm.test import common
16+
from executorch.backends.arm.util._factory import create_quantizer
17+
from executorch.export import ( # type: ignore[import-untyped]
18+
BackendRecipeProvider,
19+
ExportRecipe,
20+
QuantizationRecipe,
21+
RecipeType,
22+
)
23+
24+
QuantizerConfigurator = Callable[[TOSAQuantizer], None]
25+
26+
27+
def global_int8_per_channel(quantizer: TOSAQuantizer):
28+
quantizer.set_global(get_symmetric_quantization_config(is_per_channel=True))
29+
30+
31+
def global_int8_per_tensor(quantizer: TOSAQuantizer):
32+
quantizer.set_global(get_symmetric_quantization_config(is_per_channel=False))
33+
34+
35+
class ArmRecipeProvider(BackendRecipeProvider):
36+
@property
37+
def backend_name(self) -> str:
38+
return ArmRecipeType.get_backend_name()
39+
40+
def get_supported_recipes(self) -> Sequence[RecipeType]:
41+
return list(ArmRecipeType)
42+
43+
@classmethod
44+
def build_export_recipe(
45+
cls,
46+
recipe_type: RecipeType,
47+
target_recipe: TargetRecipe,
48+
quantization_configurators: Optional[list[QuantizerConfigurator]] = None,
49+
) -> ArmExportRecipe:
50+
51+
if quantization_configurators is not None:
52+
quantizer = create_quantizer(target_recipe.compile_spec)
53+
for configure in quantization_configurators:
54+
configure(quantizer)
55+
quantization_recipe = QuantizationRecipe([quantizer])
56+
else:
57+
quantization_recipe = None
58+
59+
return ArmExportRecipe(
60+
name=str(recipe_type),
61+
target_recipe=target_recipe,
62+
quantization_recipe=quantization_recipe,
63+
)
64+
65+
def create_recipe(
66+
self, recipe_type: RecipeType, **kwargs: Any
67+
) -> Optional[ExportRecipe]:
68+
"""Create arm recipe"""
69+
return create_recipe(recipe_type, **kwargs)
70+
71+
72+
def create_recipe(recipe_type: RecipeType, **kwargs: Any) -> ArmExportRecipe:
73+
"""Create an ArmExportRecipe depending on the ArmRecipeType enum, with some kwargs. See documentation for
74+
the ArmRecipeType for the available kwargs."""
75+
76+
match recipe_type:
77+
case ArmRecipeType.TOSA_FP:
78+
return ArmRecipeProvider.build_export_recipe(
79+
recipe_type,
80+
TargetRecipe(common.get_tosa_compile_spec("TOSA-1.0+FP", **kwargs)),
81+
)
82+
case ArmRecipeType.TOSA_INT8_STATIC_PER_TENSOR:
83+
return ArmRecipeProvider.build_export_recipe(
84+
recipe_type,
85+
TargetRecipe(common.get_tosa_compile_spec("TOSA-1.0+INT", **kwargs)),
86+
[global_int8_per_tensor],
87+
)
88+
case ArmRecipeType.TOSA_INT8_STATIC_PER_CHANNEL:
89+
return ArmRecipeProvider.build_export_recipe(
90+
recipe_type,
91+
TargetRecipe(common.get_tosa_compile_spec("TOSA-1.0+INT", **kwargs)),
92+
[global_int8_per_channel],
93+
)
94+
case ArmRecipeType.ETHOSU_U55_INT8_STATIC_PER_CHANNEL:
95+
return ArmRecipeProvider.build_export_recipe(
96+
recipe_type,
97+
TargetRecipe(common.get_u55_compile_spec(**kwargs)),
98+
[global_int8_per_channel],
99+
)
100+
case ArmRecipeType.ETHOSU_U55_INT8_STATIC_PER_TENSOR:
101+
return ArmRecipeProvider.build_export_recipe(
102+
recipe_type,
103+
TargetRecipe(common.get_u55_compile_spec(**kwargs)),
104+
[global_int8_per_tensor],
105+
)
106+
case ArmRecipeType.ETHOSU_U85_INT8_STATIC_PER_TENSOR:
107+
return ArmRecipeProvider.build_export_recipe(
108+
recipe_type,
109+
TargetRecipe(common.get_u85_compile_spec(**kwargs)),
110+
[global_int8_per_tensor],
111+
)
112+
case ArmRecipeType.ETHOSU_U85_INT8_STATIC_PER_CHANNEL:
113+
return ArmRecipeProvider.build_export_recipe(
114+
recipe_type,
115+
TargetRecipe(common.get_u85_compile_spec(**kwargs)),
116+
[global_int8_per_channel],
117+
)
118+
119+
case ArmRecipeType.VGF_FP:
120+
return ArmRecipeProvider.build_export_recipe(
121+
recipe_type,
122+
TargetRecipe(common.get_vgf_compile_spec("TOSA-1.0+FP", **kwargs)),
123+
)
124+
case ArmRecipeType.VGF_INT8_STATIC_PER_TENSOR:
125+
return ArmRecipeProvider.build_export_recipe(
126+
recipe_type,
127+
TargetRecipe(common.get_vgf_compile_spec("TOSA-1.0+INT", **kwargs)),
128+
[global_int8_per_tensor],
129+
)
130+
case ArmRecipeType.VGF_INT8_STATIC_PER_CHANNEL:
131+
return ArmRecipeProvider.build_export_recipe(
132+
recipe_type,
133+
TargetRecipe(common.get_vgf_compile_spec("TOSA-1.0+INT", **kwargs)),
134+
[global_int8_per_channel],
135+
)
136+
case ArmRecipeType.CUSTOM:
137+
if "recipe" not in kwargs or not isinstance(
138+
kwargs["recipe"], ArmExportRecipe
139+
):
140+
raise ValueError(
141+
"ArmRecipeType.CUSTOM requires a kwarg 'recipe' that provides the ArmExportRecipe"
142+
)
143+
return kwargs["recipe"]
144+
case _:
145+
raise ValueError(f"Unsupported recipe type {recipe_type}")
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# Copyright 2025 Arm Limited and/or its affiliates.
2+
#
3+
# This source code is licensed under the BSD-style license found in the
4+
# LICENSE file in the root directory of this source tree.
5+
6+
# pyre-strict
7+
8+
from executorch.export import RecipeType # type: ignore[import-untyped]
9+
10+
11+
class ArmRecipeType(RecipeType):
12+
"""Arm-specific recipe types"""
13+
14+
TOSA_FP = "arm_tosa_fp"
15+
""" Kwargs:
16+
- custom_path: str=None,
17+
- tosa_debug_mode: TosaCompileSpec.DebugMode | None = None,
18+
"""
19+
TOSA_INT8_STATIC_PER_TENSOR = "arm_tosa_int8_static_per_tensor"
20+
""" Kwargs:
21+
- custom_path: str=None,
22+
- tosa_debug_mode: TosaCompileSpec.DebugMode | None = None,
23+
"""
24+
TOSA_INT8_STATIC_PER_CHANNEL = "arm_tosa_int8_static_per_channel"
25+
""" Kwargs:
26+
- custom_path: str=None,
27+
- tosa_debug_mode: TosaCompileSpec.DebugMode | None = None,
28+
"""
29+
ETHOSU_U55_INT8_STATIC_PER_CHANNEL = "arm_ethosu_u55_int_static_per_channel"
30+
""" Kwargs:
31+
- macs: int = 128,
32+
- system_config: str = "Ethos_U55_High_End_Embedded",
33+
- memory_mode: str = "Shared_Sram",
34+
- extra_flags: str = "--debug-force-regor --output-format=raw",
35+
- custom_path: Optional[str] = None,
36+
- config: Optional[str] = None,
37+
- tosa_debug_mode: EthosUCompileSpec.DebugMode | None = None,
38+
"""
39+
ETHOSU_U55_INT8_STATIC_PER_TENSOR = "arm_ethosu_u55_int_static_per_channel"
40+
""" Kwargs:
41+
- macs: int = 128,
42+
- system_config: str = "Ethos_U55_High_End_Embedded",
43+
- memory_mode: str = "Shared_Sram",
44+
- extra_flags: str = "--debug-force-regor --output-format=raw",
45+
- custom_path: Optional[str] = None,
46+
- config: Optional[str] = None,
47+
- tosa_debug_mode: EthosUCompileSpec.DebugMode | None = None,
48+
"""
49+
ETHOSU_U85_INT8_STATIC_PER_TENSOR = "arm_ethosu_u85_int_static_per_tensor"
50+
""" Kwargs:
51+
- macs: int = 128,
52+
- system_config="Ethos_U85_SYS_DRAM_Mid",
53+
- memory_mode="Shared_Sram",
54+
- extra_flags="--output-format=raw",
55+
- custom_path: Optional[str] = None,
56+
- config: Optional[str] = None,
57+
- tosa_debug_mode: EthosUCompileSpec.DebugMode | None = None,
58+
"""
59+
ETHOSU_U85_INT8_STATIC_PER_CHANNEL = "arm_ethosu_u85_int_static_per_channel"
60+
""" Kwargs:
61+
- macs: int = 128,
62+
- system_config="Ethos_U85_SYS_DRAM_Mid",
63+
- memory_mode="Shared_Sram",
64+
- extra_flags="--output-format=raw",
65+
- custom_path: Optional[str] = None,
66+
- config: Optional[str] = None,
67+
- tosa_debug_mode: EthosUCompileSpec.DebugMode | None = None,
68+
"""
69+
70+
VGF_FP = "arm_vgf_fp"
71+
""" Kwargs:
72+
- compiler_flags: Optional[str] = "",
73+
- custom_path=None,
74+
- tosa_debug_mode: VgfCompileSpec.DebugMode | None = None,
75+
"""
76+
VGF_INT8_STATIC_PER_TENSOR = "arm_vgf_int8_static_per_tensor"
77+
""" Kwargs:
78+
- compiler_flags: Optional[str] = "",
79+
- custom_path=None,
80+
- tosa_debug_mode: VgfCompileSpec.DebugMode | None = None,
81+
"""
82+
VGF_INT8_STATIC_PER_CHANNEL = "arm_vgf_int8_static_per_channel"
83+
""" Kwargs:
84+
- compiler_flags: Optional[str] = "",
85+
- custom_path=None,
86+
- tosa_debug_mode: VgfCompileSpec.DebugMode | None = None,
87+
"""
88+
CUSTOM = "Provide your own ArmRecipeType to the kwarg 'recipe'."
89+
90+
@classmethod
91+
def get_backend_name(cls) -> str:
92+
return "arm"

backends/arm/recipe/recipe.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# Copyright 2025 Arm Limited and/or its affiliates.
2+
#
3+
# This source code is licensed under the BSD-style license found in the
4+
# LICENSE file in the root directory of this source tree.
5+
6+
from dataclasses import dataclass, field
7+
from typing import List
8+
9+
from executorch.backends.arm.common.arm_compile_spec import ArmCompileSpec
10+
from executorch.backends.arm.util._factory import create_partitioner
11+
from executorch.exir import EdgeCompileConfig
12+
from executorch.exir.pass_manager import PassType
13+
from executorch.export import ( # type: ignore[import-untyped]
14+
ExportRecipe,
15+
LoweringRecipe,
16+
QuantizationRecipe,
17+
)
18+
19+
20+
@dataclass
21+
class TargetRecipe:
22+
"""Contains target-level export configuration."""
23+
24+
compile_spec: ArmCompileSpec
25+
edge_compile_config: EdgeCompileConfig = field(
26+
default_factory=lambda: EdgeCompileConfig(_check_ir_validity=False)
27+
)
28+
edge_transform_passes: List[PassType] = field(default_factory=lambda: [])
29+
30+
31+
class ArmExportRecipe(ExportRecipe):
32+
"""Wraps ExportRecipe to provide the constructor we want and easy access to some variables."""
33+
34+
def __init__(
35+
self,
36+
name,
37+
target_recipe: TargetRecipe,
38+
quantization_recipe: QuantizationRecipe | None,
39+
):
40+
self.compile_spec = target_recipe.compile_spec
41+
self.edge_transform_passes = target_recipe.edge_transform_passes
42+
43+
lowering_recipe = LoweringRecipe(
44+
[create_partitioner(self.compile_spec)],
45+
edge_transform_passes=[lambda _, __: target_recipe.edge_transform_passes],
46+
edge_compile_config=target_recipe.edge_compile_config,
47+
)
48+
super().__init__(
49+
name=name,
50+
quantization_recipe=quantization_recipe,
51+
lowering_recipe=lowering_recipe,
52+
executorch_backend_config=None,
53+
pipeline_stages=None,
54+
)

0 commit comments

Comments
 (0)