Skip to content

Commit 15d935b

Browse files
[Executorch][target recipes] Add target based recipes for lowering models to a target device
This diff introduces multi backend/ target based recipes to lower a model with very less code. Target recipes provide pre-configured backend recipes to use them and retarget if needed. See RFC: #13732 ## Usage ``` from executorch.export import export, ExportRecipe, IOSTargetRecipeType # CoreML + XNNPACK coreml_xnnpack_recipe = ExportRecipe.get_recipe(IOSTargetRecipeType.IOS_ARM64_COREML_FP32) session = export(model, coreml_xnnpack_recipe, example_inputs) session.save_pte_file("model.pte") ``` ## Advanced usage one can directly use `ExportRecipe.combine_recipes()` to combine specific backend recipes. ``` recipe1 = ExportRecipe.get_recipe(AndroidRecipeType.XYZ) recipe2 = ExportRecipe.get_recipe(XNNPackRecipeType.FP32) combined_recipe = ExportRecipe.combine( [recipe1, recipe2], recipe_name="multi_backend_coreml_xnnpack_fp32" ) session = export(model, combined_recipe, example_inputs) ``` Fixes: #13732 Differential Revision: [D81297451](https://our.internmc.facebook.com/intern/diff/D81297451/) ghstack-source-id: 306434236 Pull Request resolved: #13791
1 parent 6208340 commit 15d935b

File tree

9 files changed

+541
-7
lines changed

9 files changed

+541
-7
lines changed

backends/apple/coreml/TARGETS

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,16 +61,21 @@ runtime.python_library(
6161
)
6262

6363
runtime.python_library(
64-
name = "recipes",
65-
srcs = glob([
66-
"recipes/*.py",
67-
]),
64+
name = "coreml_recipes",
65+
srcs = [
66+
"recipes/__init__.py",
67+
"recipes/coreml_recipe_provider.py"
68+
],
6869
visibility = [
6970
"@EXECUTORCH_CLIENTS",
71+
"//executorch/export/...",
7072
],
7173
deps = [
7274
"fbsource//third-party/pypi/coremltools:coremltools",
75+
":coreml_recipe_types",
7376
":backend",
77+
":partitioner",
78+
":quantizer",
7479
"//caffe2:torch",
7580
"//executorch/exir:lib",
7681
"//executorch/exir/backend:compile_spec_schema",
@@ -80,6 +85,20 @@ runtime.python_library(
8085
],
8186
)
8287

88+
runtime.python_library(
89+
name = "coreml_recipe_types",
90+
srcs = [
91+
"recipes/coreml_recipe_types.py",
92+
],
93+
visibility = [
94+
"@EXECUTORCH_CLIENTS",
95+
"//executorch/export/...",
96+
],
97+
deps = [
98+
"//executorch/export:recipe",
99+
],
100+
)
101+
83102
runtime.cxx_python_extension(
84103
name = "executorchcoreml",
85104
srcs = [
@@ -124,7 +143,7 @@ runtime.python_test(
124143
"fbsource//third-party/pypi/pytest:pytest",
125144
":partitioner",
126145
":quantizer",
127-
":recipes",
146+
":coreml_recipes",
128147
"//caffe2:torch",
129148
"//pytorch/vision:torchvision",
130149
"fbsource//third-party/pypi/scikit-learn:scikit-learn",

backends/apple/coreml/recipes/coreml_recipe_provider.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,13 @@ def _validate_recipe_kwargs(self, recipe_type: RecipeType, **kwargs: Any) -> Non
121121

122122
def _get_expected_keys(self, recipe_type: RecipeType) -> set:
123123
"""Get expected parameter keys for a recipe type"""
124-
common_keys = {"minimum_deployment_target", "compute_unit"}
124+
common_keys = {
125+
"minimum_deployment_target",
126+
"compute_unit",
127+
"skip_ops_for_coreml_delegation",
128+
"lower_full_graph",
129+
"take_over_constant_data",
130+
}
125131

126132
if recipe_type in [
127133
CoreMLRecipeType.TORCHAO_INT4_WEIGHT_ONLY_PER_GROUP,
@@ -377,9 +383,19 @@ def _get_coreml_lowering_recipe(
377383
if minimum_deployment_target and minimum_deployment_target < ct.target.iOS18:
378384
take_over_mutable_buffer = False
379385

386+
# Extract additional partitioner parameters
387+
skip_ops_for_coreml_delegation = kwargs.get(
388+
"skip_ops_for_coreml_delegation", None
389+
)
390+
lower_full_graph = kwargs.get("lower_full_graph", False)
391+
take_over_constant_data = kwargs.get("take_over_constant_data", True)
392+
380393
partitioner = CoreMLPartitioner(
381394
compile_specs=compile_specs,
382395
take_over_mutable_buffer=take_over_mutable_buffer,
396+
skip_ops_for_coreml_delegation=skip_ops_for_coreml_delegation,
397+
lower_full_graph=lower_full_graph,
398+
take_over_constant_data=take_over_constant_data,
383399
)
384400

385401
edge_compile_config = EdgeCompileConfig(

backends/xnnpack/recipes/TARGETS

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,6 @@ runtime.python_library(
3030
"@EXECUTORCH_CLIENTS",
3131
],
3232
deps = [
33-
"//executorch/export:lib",
33+
"//executorch/export:recipe",
3434
],
3535
)
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
load("@fbsource//xplat/executorch/build:runtime_wrapper.bzl", "runtime")
2+
3+
oncall("executorch")
4+
5+
runtime.python_library(
6+
name = "multi_backend_recipe_provider",
7+
srcs = [
8+
"__init__.py",
9+
"multi_backend_recipe_provider.py",
10+
],
11+
deps = [
12+
":target_recipe_types",
13+
"//executorch/export:lib",
14+
]
15+
)
16+
17+
runtime.python_library(
18+
name = "target_recipe_types",
19+
srcs = [
20+
"target_recipe_types.py",
21+
],
22+
deps = [
23+
"//executorch/export:recipe",
24+
"//executorch/backends/xnnpack/recipes:xnnpack_recipe_types",
25+
"//executorch/backends/apple/coreml:coreml_recipes",
26+
]
27+
)
28+
29+
runtime.python_test(
30+
name = "test_multi_backend_target_recipes",
31+
srcs = [
32+
"tests/test_multi_backend_target_recipes.py",
33+
],
34+
deps = [
35+
"//executorch/export:lib",
36+
":multi_backend_recipe_provider",
37+
"//executorch/backends/apple/coreml:coreml_recipe_types",
38+
"//executorch/backends/xnnpack:xnnpack_delegate",
39+
"fbsource//third-party/pypi/coremltools:coremltools",
40+
"fbsource//third-party/pypi/scikit-learn:scikit-learn",
41+
"//executorch/backends/xnnpack/test/tester:tester",
42+
]
43+
)
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
# All rights reserved.
3+
#
4+
# This source code is licensed under the BSD-style license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
7+
# pyre-strict
8+
9+
from executorch.export import recipe_registry
10+
11+
from .multi_backend_recipe_provider import MultiBackendRecipeProvider
12+
from .target_recipe_types import TargetRecipeType
13+
14+
# Auto-register MultiBackendRecipeProvider
15+
recipe_registry.register_backend_recipe_provider(MultiBackendRecipeProvider())
16+
17+
__all__ = [
18+
"MultiBackendRecipeProvider",
19+
"TargetRecipeType",
20+
]
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
# All rights reserved.
3+
#
4+
# This source code is licensed under the BSD-style license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
7+
"""
8+
Multi-backend recipe provider for target-specific deployment.
9+
10+
This module provides the recipe provider that combines multiple backends
11+
for optimized deployment on specific targets.
12+
"""
13+
14+
from typing import Any, Optional, Sequence
15+
16+
from executorch.export import BackendRecipeProvider, ExportRecipe, RecipeType
17+
18+
from .target_recipe_types import IOSTargetRecipeType, TargetRecipeType
19+
import logging
20+
21+
22+
class MultiBackendRecipeProvider(BackendRecipeProvider):
23+
"""
24+
Recipe provider that combines multiple backends for target-specific deployment.
25+
"""
26+
27+
@property
28+
def backend_name(self) -> str:
29+
return "multi_backend"
30+
31+
def get_supported_recipes(self) -> Sequence[RecipeType]:
32+
"""
33+
Returns all available target recipe types.
34+
"""
35+
# Collect all target recipe types from all target classes
36+
recipes = []
37+
for target_class in [IOSTargetRecipeType]:
38+
recipes.extend(list(target_class.__members__.values()))
39+
40+
return recipes
41+
42+
def create_recipe(
43+
self, recipe_type: RecipeType, **kwargs: Any
44+
) -> Optional[ExportRecipe]:
45+
"""
46+
Create a multi-backend recipe for the given target recipe type.
47+
48+
Args:
49+
recipe_type: Target recipe type (e.g., IOSTargetRecipeType.IOS_ARM64_COREML_FP32)
50+
**kwargs: Additional parameters to pass to individual backend recipes
51+
52+
Returns:
53+
ExportRecipe configured for multi-backend deployment
54+
"""
55+
if not isinstance(recipe_type, TargetRecipeType):
56+
return None
57+
58+
# Get the backend combination for this target
59+
backend_recipe_types = recipe_type.get_backend_combination()
60+
if not backend_recipe_types:
61+
logging.warning(
62+
f"No backend recipes available for target: {recipe_type.value}"
63+
)
64+
return None
65+
66+
# Create individual backend recipes using the existing recipe registry
67+
backend_recipes = []
68+
for backend_recipe_type in backend_recipe_types:
69+
# Extract backend-specific kwargs
70+
backend_recipe_kwargs = self._extract_backend_recipe_kwargs(
71+
backend_recipe_type.get_backend_name(), kwargs
72+
)
73+
74+
backend_recipe = ExportRecipe.get_recipe(
75+
backend_recipe_type, **backend_recipe_kwargs
76+
)
77+
backend_recipes.append(backend_recipe)
78+
79+
if not backend_recipes:
80+
raise ValueError(
81+
f"No available backend recipes for target '{recipe_type.value}'. "
82+
f"Ensure required backend dependencies are installed."
83+
)
84+
85+
# Combine the recipes into a single multi-backend recipe
86+
return ExportRecipe.combine(backend_recipes, recipe_type.value)
87+
88+
def _extract_backend_recipe_kwargs(self, backend_name: str, kwargs: dict) -> dict:
89+
"""
90+
Extract backend-specific kwargs using nested dictionary structure.
91+
92+
Args:
93+
backend_name: Name of the backend (e.g., "xnnpack", "coreml")
94+
kwargs: All kwargs passed to the target recipe
95+
96+
Returns:
97+
Dict of kwargs specific to this backend
98+
"""
99+
backend_kwargs = {}
100+
101+
# Extract from nested backend_configs structure
102+
backend_configs = kwargs.get("backend_configs", {})
103+
if backend_name in backend_configs:
104+
backend_kwargs.update(backend_configs[backend_name])
105+
106+
# Include common kwargs (parameters passed directly without backend_configs)
107+
common_kwargs = self._get_common_kwargs(kwargs)
108+
backend_kwargs.update(common_kwargs)
109+
110+
return backend_kwargs
111+
112+
def _get_common_kwargs(self, kwargs: dict) -> dict:
113+
excluded_keys = {
114+
"backend_configs", # Skip, represents nested backend recipe kwargs
115+
}
116+
common_kwargs = {}
117+
for key, value in kwargs.items():
118+
if key not in excluded_keys:
119+
common_kwargs[key] = value
120+
return common_kwargs
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
# All rights reserved.
3+
#
4+
# This source code is licensed under the BSD-style license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
7+
"""
8+
Target-specific recipe types for multi-backend deployment.
9+
10+
This module defines target-specific recipe types that combine multiple backends
11+
for optimized deployment on specific platforms and hardware configurations.
12+
"""
13+
14+
from abc import abstractmethod
15+
from typing import List
16+
17+
# pyre-ignore
18+
from executorch.backends.apple.coreml.recipes.coreml_recipe_types import (
19+
CoreMLRecipeType,
20+
)
21+
22+
from executorch.backends.xnnpack import XNNPackRecipeType
23+
24+
from executorch.export.recipe import RecipeType, RecipeTypeMeta
25+
26+
27+
class TargetRecipeTypeMeta(RecipeTypeMeta):
28+
"""Metaclass that extends RecipeTypeMeta for target recipe types"""
29+
30+
pass
31+
32+
33+
class TargetRecipeType(RecipeType, metaclass=TargetRecipeTypeMeta):
34+
"""
35+
Base class for target-specific recipe types that combine multiple backends.
36+
Target recipes define optimal backend combinations for specific deployment scenarios.
37+
"""
38+
39+
@abstractmethod
40+
def get_backend_combination(self) -> List[RecipeType]:
41+
"""
42+
Return the backend combination for this target recipe type.
43+
44+
Returns:
45+
List of RecipeType enums in order of precedence.
46+
Earlier backends get first chance at partitioning operations.
47+
"""
48+
pass
49+
50+
@classmethod
51+
@abstractmethod
52+
def get_target_platform(cls) -> str:
53+
"""
54+
Return the target platform for this recipe type.
55+
56+
Returns:
57+
str: The target platform (e.g., "android", "ios", "linux")
58+
"""
59+
pass
60+
61+
@classmethod
62+
def get_backend_name(cls) -> str:
63+
return "multi_backend"
64+
65+
66+
class IOSTargetRecipeType(TargetRecipeType):
67+
"""iOS-specific target recipe types, refer individual backend recipes for customization"""
68+
69+
# FP32 - iOS with CoreML backend as primary, fallback XNNPACK
70+
IOS_ARM64_COREML_FP32 = "ios-arm64-coreml-fp32"
71+
72+
# FP16 - iOS with CoreML backend as primary, fallback XNNPACK
73+
IOS_ARM64_COREML_FP16 = "ios-arm64-coreml-fp16"
74+
75+
# INT8 Static Quantization (weights + activations)
76+
# No xnnpack fallback for quantization as coreml uses torch.ao quantizer vs xnnpack uses torchao quantizer
77+
IOS_ARM64_COREML_INT8_STATIC = "ios-arm64-coreml-int8-static"
78+
79+
80+
@classmethod
81+
def get_target_platform(cls) -> str:
82+
return "ios"
83+
84+
def get_backend_combination(self) -> List[RecipeType]:
85+
"""
86+
Get backend combinations for iOS targets.
87+
88+
Returns combinations optimized for iOS deployment scenarios.
89+
"""
90+
if self == IOSTargetRecipeType.IOS_ARM64_COREML_FP32:
91+
return [CoreMLRecipeType.FP32, XNNPackRecipeType.FP32]
92+
elif self == IOSTargetRecipeType.IOS_ARM64_COREML_FP16:
93+
return [CoreMLRecipeType.FP16, XNNPackRecipeType.FP32]
94+
elif self == IOSTargetRecipeType.IOS_ARM64_COREML_INT8_STATIC:
95+
return [CoreMLRecipeType.PT2E_INT8_STATIC]
96+
return []

0 commit comments

Comments
 (0)