Skip to content

Commit ebe9f99

Browse files
metascroyfacebook-github-bot
authored andcommitted
Add BackendOption loading to swift bindings (#18855) (#18855)
Summary: Add backend options support to the ExecuTorch Swift/ObjC bindings. The C++ Module class supports LoadBackendOptionsMap for passing per-delegate configuration (e.g. compute unit, thread count, cache directory) at model load time, but this was not exposed through the Swift/ObjC layer. This diff adds: * ExecuTorchBackendOption (BackendOption in Swift) — a single key/value configuration entry with boolean, integer, or string value types. Immutable, equatable/hashable, with a human-readable description for debugging. * ExecuTorchBackendOptionsMap (BackendOptionsMap in Swift) — a map from backend identifier to [BackendOption]. Built once from a dict; validates input at construction time (rejects integer overflow of the C++ 32-bit storage and strings that would exceed the fixed-size option-key/value buffers). Owns the backing C++ storage that the runtime borrows from, and is retained by the Module for the lifetime of the borrow so lazy load_method calls (triggered by forward) still see a valid map. * New load/loadMethod overloads on ExecuTorchModule that accept a BackendOptionsMap, plus idiomatic Swift wrappers Module.load(_:verification:) and Module.load(_:options:). Current CoreML backend options are documented here: https://github.com/pytorch/executorch/blob/main/backends/apple/coreml/runtime/include/coreml_backend/coreml_backend_options.h Swift usage: ``` let module = Module(filePath: "model.pte") let options = try BackendOptionsMap(options: [ "CoreMLBackend": [ BackendOption("compute_unit", "cpu_and_gpu"), BackendOption("_use_new_cache", true), ] ]) try module.load(options) ``` Reviewed By: shoumikhin Differential Revision: D100710833 Pulled By: metascroy
1 parent 99f1f0b commit ebe9f99

16 files changed

Lines changed: 1524 additions & 8 deletions

Package.swift

Lines changed: 58 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
// https://pytorch.org/executorch/main/using-executorch-ios
1919

2020
import PackageDescription
21+
import Foundation
2122

2223
let debug_suffix = "_debug"
2324
let dependencies_suffix = "_with_dependencies"
@@ -126,6 +127,48 @@ for (key, value) in products {
126127
packageTargets.append(target)
127128
}
128129

130+
// Test fixtures. add_coreml.pte and add_mul_coreml.pte are generated at CI
131+
// time by extension/apple/ExecuTorch/__tests__/resources/generate_coreml_test_models.py
132+
// (invoked by scripts/build_apple_frameworks.sh before `swift test`). They
133+
// are gitignored, so include them in test resources only when present so
134+
// that `swift test` runs on dev machines without CoreML python deps don't
135+
// fail at the SwiftPM resolve stage.
136+
let testResourcesDir = "extension/apple/ExecuTorch/__tests__/resources"
137+
var testResources: [Resource] = [.copy("resources/add.pte")]
138+
if FileManager.default.fileExists(atPath: "\(testResourcesDir)/add_coreml.pte") {
139+
testResources.append(.copy("resources/add_coreml.pte"))
140+
}
141+
if FileManager.default.fileExists(atPath: "\(testResourcesDir)/add_mul_coreml.pte") {
142+
testResources.append(.copy("resources/add_mul_coreml.pte"))
143+
}
144+
145+
// SwiftPM resources must live under the target's path, so the ObjC test
146+
// target uses copies of the canonical resources directory's fixtures. The
147+
// copies themselves are gitignored and (re)created by scripts/build_apple_frameworks.sh.
148+
let objcTestsDir = "extension/apple/ExecuTorch/__tests__/ObjC"
149+
var objcTestResources: [Resource] = []
150+
if FileManager.default.fileExists(atPath: "\(objcTestsDir)/add.pte") {
151+
objcTestResources.append(.copy("add.pte"))
152+
}
153+
if FileManager.default.fileExists(atPath: "\(objcTestsDir)/add_coreml.pte") {
154+
objcTestResources.append(.copy("add_coreml.pte"))
155+
}
156+
if FileManager.default.fileExists(atPath: "\(objcTestsDir)/add_mul_coreml.pte") {
157+
objcTestResources.append(.copy("add_mul_coreml.pte"))
158+
}
159+
160+
let testLinkerSettings: [LinkerSetting] = [
161+
.unsafeFlags([
162+
"-Xlinker", "-force_load",
163+
"-Xlinker", "cmake-out/kernels_optimized.xcframework/macos-arm64/libkernels_optimized_macos.a",
164+
// CoreML backend registers itself with the global delegate registry via a
165+
// static initializer; -force_load ensures that initializer is pulled in so
166+
// the CoreML-delegated test fixtures can actually instantiate the backend.
167+
"-Xlinker", "-force_load",
168+
"-Xlinker", "cmake-out/backend_coreml.xcframework/macos-arm64/libbackend_coreml_macos.a",
169+
])
170+
]
171+
129172
let package = Package(
130173
name: "executorch",
131174
platforms: [
@@ -139,17 +182,24 @@ let package = Package(
139182
dependencies: [
140183
.target(name: "executorch\(debug_suffix)"),
141184
.target(name: "kernels_optimized\(dependencies_suffix)"),
185+
.target(name: "backend_coreml\(dependencies_suffix)"),
142186
],
143187
path: "extension/apple/ExecuTorch/__tests__",
144-
resources: [
145-
.copy("resources/add.pte"),
188+
exclude: ["ObjC", "resources/generate_coreml_test_models.py", "resources/.gitignore"],
189+
resources: testResources,
190+
linkerSettings: testLinkerSettings
191+
),
192+
.testTarget(
193+
name: "objc_tests",
194+
dependencies: [
195+
.target(name: "executorch\(debug_suffix)"),
196+
.target(name: "kernels_optimized\(dependencies_suffix)"),
197+
.target(name: "backend_coreml\(dependencies_suffix)"),
146198
],
147-
linkerSettings: [
148-
.unsafeFlags([
149-
"-Xlinker", "-force_load",
150-
"-Xlinker", "cmake-out/kernels_optimized.xcframework/macos-arm64/libkernels_optimized_macos.a",
151-
])
152-
]
199+
path: "extension/apple/ExecuTorch/__tests__/ObjC",
200+
exclude: [".gitignore"],
201+
resources: objcTestResources,
202+
linkerSettings: testLinkerSettings
153203
)
154204
]
155205
)

extension/apple/ExecuTorch/Exported/ExecuTorch+Module.swift

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,40 @@ public extension MethodMetadata {
4747
}
4848
}
4949

50+
public extension Module {
51+
/// Loads the module's program with per-delegate backend options.
52+
///
53+
/// The receiver retains `options` for as long as the underlying program
54+
/// references it (lifetime tracked via ARC).
55+
///
56+
/// - Parameters:
57+
/// - options: A `BackendOptionsMap` built once from a dict of
58+
/// per-backend `BackendOption`s, e.g.
59+
/// `try BackendOptionsMap(["CoreMLBackend": [BackendOption("compute_unit", "cpu_and_gpu")]])`.
60+
/// - verification: The verification level to apply when loading the program.
61+
/// - Throws: An error if loading fails.
62+
func load(
63+
options: BackendOptionsMap,
64+
verification: ModuleVerification = .minimal
65+
) throws {
66+
try __load(withOptions: options, verification: verification)
67+
}
68+
69+
/// Loads a specific method from the program with per-delegate backend options.
70+
///
71+
/// - Parameters:
72+
/// - method: The name of the method to load.
73+
/// - options: A `BackendOptionsMap` built once from a dict of
74+
/// per-backend `BackendOption`s.
75+
/// - Throws: An error if loading fails.
76+
func load(
77+
_ method: String,
78+
options: BackendOptionsMap
79+
) throws {
80+
try __loadMethod(method, options: options)
81+
}
82+
}
83+
5084
public extension Module {
5185
/// Executes a specific method with the provided input values.
5286
/// The method is loaded on demand if not already loaded.

extension/apple/ExecuTorch/Exported/ExecuTorch.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
* LICENSE file in the root directory of this source tree.
77
*/
88

9+
#import "ExecuTorchBackendOption.h"
10+
#import "ExecuTorchBackendOptionsMap.h"
911
#import "ExecuTorchError.h"
1012
#import "ExecuTorchLog.h"
1113
#import "ExecuTorchModule.h"
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
#import <Foundation/Foundation.h>
10+
11+
NS_ASSUME_NONNULL_BEGIN
12+
13+
/**
14+
* Enum to define the type of a backend option value.
15+
*/
16+
typedef NS_ENUM(NSInteger, ExecuTorchBackendOptionType) {
17+
ExecuTorchBackendOptionTypeBoolean,
18+
ExecuTorchBackendOptionTypeInteger,
19+
ExecuTorchBackendOptionTypeString,
20+
} NS_SWIFT_NAME(BackendOptionType);
21+
22+
/**
23+
* Represents a single key-value configuration option for a backend.
24+
*
25+
* Backend options are used to pass per-delegate configuration (e.g., compute
26+
* unit, thread count, cache directory) when loading a module. Each option has
27+
* a string key and a typed value (boolean, integer, or string).
28+
*/
29+
NS_SWIFT_NAME(BackendOption)
30+
__attribute__((objc_subclassing_restricted))
31+
@interface ExecuTorchBackendOption : NSObject
32+
33+
/** The option key name (e.g. "compute_unit", "num_threads"). */
34+
@property (nonatomic, readonly) NSString *key;
35+
36+
/** The type of the option value. */
37+
@property (nonatomic, readonly) ExecuTorchBackendOptionType type;
38+
39+
/** The boolean value. Only valid when type is Boolean. */
40+
@property (nonatomic, readonly) BOOL boolValue;
41+
42+
/** The integer value. Only valid when type is Integer. */
43+
@property (nonatomic, readonly) NSInteger intValue;
44+
45+
/** The string value. Only valid when type is String. */
46+
@property (nullable, nonatomic, readonly) NSString *stringValue;
47+
48+
/**
49+
* Creates a backend option with a boolean value.
50+
*
51+
* @param key The option key.
52+
* @param value The boolean value.
53+
* @return A new ExecuTorchBackendOption instance.
54+
*/
55+
+ (instancetype)optionWithKey:(NSString *)key
56+
booleanValue:(BOOL)value
57+
NS_SWIFT_NAME(init(_:_:))
58+
NS_RETURNS_RETAINED;
59+
60+
/**
61+
* Creates a backend option with an integer value.
62+
*
63+
* @param key The option key.
64+
* @param value The integer value.
65+
* @return A new ExecuTorchBackendOption instance.
66+
*/
67+
+ (instancetype)optionWithKey:(NSString *)key
68+
integerValue:(NSInteger)value
69+
NS_SWIFT_NAME(init(_:_:))
70+
NS_RETURNS_RETAINED;
71+
72+
/**
73+
* Creates a backend option with a string value.
74+
*
75+
* @param key The option key.
76+
* @param value The string value.
77+
* @return A new ExecuTorchBackendOption instance.
78+
*/
79+
+ (instancetype)optionWithKey:(NSString *)key
80+
stringValue:(NSString *)value
81+
NS_SWIFT_NAME(init(_:_:))
82+
NS_RETURNS_RETAINED;
83+
84+
+ (instancetype)new NS_UNAVAILABLE;
85+
- (instancetype)init NS_UNAVAILABLE;
86+
87+
@end
88+
89+
NS_ASSUME_NONNULL_END
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
#import "ExecuTorchBackendOption.h"
10+
11+
@implementation ExecuTorchBackendOption {
12+
NSString *_key;
13+
ExecuTorchBackendOptionType _type;
14+
BOOL _boolValue;
15+
NSInteger _intValue;
16+
NSString *_stringValue;
17+
}
18+
19+
- (instancetype)initWithKey:(NSString *)key
20+
booleanValue:(BOOL)value {
21+
self = [super init];
22+
if (self) {
23+
_key = [key copy];
24+
_type = ExecuTorchBackendOptionTypeBoolean;
25+
_boolValue = value;
26+
}
27+
return self;
28+
}
29+
30+
- (instancetype)initWithKey:(NSString *)key
31+
integerValue:(NSInteger)value {
32+
self = [super init];
33+
if (self) {
34+
_key = [key copy];
35+
_type = ExecuTorchBackendOptionTypeInteger;
36+
_intValue = value;
37+
}
38+
return self;
39+
}
40+
41+
- (instancetype)initWithKey:(NSString *)key
42+
stringValue:(NSString *)value {
43+
self = [super init];
44+
if (self) {
45+
_key = [key copy];
46+
_type = ExecuTorchBackendOptionTypeString;
47+
_stringValue = [value copy];
48+
}
49+
return self;
50+
}
51+
52+
+ (instancetype)optionWithKey:(NSString *)key
53+
booleanValue:(BOOL)value {
54+
return [[self alloc] initWithKey:key booleanValue:value];
55+
}
56+
57+
+ (instancetype)optionWithKey:(NSString *)key
58+
integerValue:(NSInteger)value {
59+
return [[self alloc] initWithKey:key integerValue:value];
60+
}
61+
62+
+ (instancetype)optionWithKey:(NSString *)key
63+
stringValue:(NSString *)value {
64+
return [[self alloc] initWithKey:key stringValue:value];
65+
}
66+
67+
#pragma mark - NSObject
68+
69+
- (NSString *)description {
70+
switch (_type) {
71+
case ExecuTorchBackendOptionTypeBoolean:
72+
return [NSString stringWithFormat:@"<%@ %@=%@ (bool)>",
73+
NSStringFromClass([self class]), _key, _boolValue ? @"true" : @"false"];
74+
case ExecuTorchBackendOptionTypeInteger:
75+
return [NSString stringWithFormat:@"<%@ %@=%ld (int)>",
76+
NSStringFromClass([self class]), _key, (long)_intValue];
77+
case ExecuTorchBackendOptionTypeString:
78+
return [NSString stringWithFormat:@"<%@ %@=%@ (string)>",
79+
NSStringFromClass([self class]), _key,
80+
_stringValue ? [NSString stringWithFormat:@"\"%@\"", _stringValue] : @"(null)"];
81+
}
82+
return [super description];
83+
}
84+
85+
- (NSString *)debugDescription {
86+
return [self description];
87+
}
88+
89+
- (BOOL)isEqual:(id)object {
90+
if (self == object) {
91+
return YES;
92+
}
93+
if (![object isKindOfClass:[ExecuTorchBackendOption class]]) {
94+
return NO;
95+
}
96+
ExecuTorchBackendOption *other = (ExecuTorchBackendOption *)object;
97+
if (_type != other.type || ![_key isEqualToString:other.key]) {
98+
return NO;
99+
}
100+
switch (_type) {
101+
case ExecuTorchBackendOptionTypeBoolean:
102+
return _boolValue == other.boolValue;
103+
case ExecuTorchBackendOptionTypeInteger:
104+
return _intValue == other.intValue;
105+
case ExecuTorchBackendOptionTypeString: {
106+
// Both are non-null when type is String (init enforces it), but be
107+
// defensive in case of subclass/manual misuse.
108+
NSString *otherString = other.stringValue;
109+
if (_stringValue == otherString) {
110+
return YES;
111+
}
112+
if (_stringValue == nil || otherString == nil) {
113+
return NO;
114+
}
115+
return [_stringValue isEqualToString:otherString];
116+
}
117+
}
118+
return NO;
119+
}
120+
121+
- (NSUInteger)hash {
122+
NSUInteger h = _key.hash ^ (NSUInteger)_type;
123+
switch (_type) {
124+
case ExecuTorchBackendOptionTypeBoolean:
125+
h ^= (NSUInteger)(_boolValue ? 1 : 0);
126+
break;
127+
case ExecuTorchBackendOptionTypeInteger:
128+
h ^= (NSUInteger)_intValue;
129+
break;
130+
case ExecuTorchBackendOptionTypeString:
131+
h ^= _stringValue.hash;
132+
break;
133+
}
134+
return h;
135+
}
136+
137+
@end

0 commit comments

Comments
 (0)