From 25361b3cc3699b8ac394a5a0a972d88d3bb19ce2 Mon Sep 17 00:00:00 2001 From: Scott Roy Date: Tue, 14 Apr 2026 16:25:15 -0700 Subject: [PATCH] Add BackendOption loading to swift bindings (#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: A new ExecuTorchBackendOption ObjC class (BackendOption in Swift) representing a single key-value configuration option with support for boolean, integer, and string value types. New load and loadMethod overloads on ExecuTorchModule that accept a dictionary mapping backend identifiers to arrays of backend options. Swift extensions providing idiomatic load(backendOptions:verification:) and load(_:backendOptions:) APIs on Module. Current available options for CoreML backend are here: https://github.com/pytorch/executorch/blob/main/backends/apple/coreml/runtime/include/coreml_backend/coreml_backend_options.h ``` let module = Module(filePath: "model.pte") try module.load(backendOptions: [ "CoreMLBackend": [ BackendOption("compute_unit", "cpu_and_gpu"), BackendOption("_use_new_cache", true), ] ]) ``` Differential Revision: D100710833 --- .../Exported/ExecuTorch+Module.swift | 30 ++++++ .../apple/ExecuTorch/Exported/ExecuTorch.h | 1 + .../Exported/ExecuTorchBackendOption.h | 89 +++++++++++++++++ .../Exported/ExecuTorchBackendOption.mm | 67 +++++++++++++ .../ExecuTorch/Exported/ExecuTorchModule.h | 38 ++++++++ .../ExecuTorch/Exported/ExecuTorchModule.mm | 97 +++++++++++++++++++ .../ExecuTorch/__tests__/ModuleTest.swift | 78 +++++++++++++++ 7 files changed, 400 insertions(+) create mode 100644 extension/apple/ExecuTorch/Exported/ExecuTorchBackendOption.h create mode 100644 extension/apple/ExecuTorch/Exported/ExecuTorchBackendOption.mm diff --git a/extension/apple/ExecuTorch/Exported/ExecuTorch+Module.swift b/extension/apple/ExecuTorch/Exported/ExecuTorch+Module.swift index 86e9f7d3cc9..ed0bcb3c04a 100644 --- a/extension/apple/ExecuTorch/Exported/ExecuTorch+Module.swift +++ b/extension/apple/ExecuTorch/Exported/ExecuTorch+Module.swift @@ -47,6 +47,36 @@ public extension MethodMetadata { } } +public extension Module { + /// Loads the module's program with per-delegate backend options. + /// + /// - Parameters: + /// - backendOptions: A dictionary mapping backend identifiers (e.g. "CoreMLBackend") + /// to arrays of `BackendOption` objects configuring that backend. + /// - verification: The verification level to apply when loading the program. + /// - Throws: An error if loading fails. + func load( + backendOptions: [String: [BackendOption]], + verification: ModuleVerification = .minimal + ) throws { + try __load(backendOptions: backendOptions, verification: verification) + } + + /// Loads a specific method from the program with per-delegate backend options. + /// + /// - Parameters: + /// - method: The name of the method to load. + /// - backendOptions: A dictionary mapping backend identifiers (e.g. "CoreMLBackend") + /// to arrays of `BackendOption` objects configuring that backend. + /// - Throws: An error if loading fails. + func load( + _ method: String, + backendOptions: [String: [BackendOption]] + ) throws { + try __loadMethod(method, backendOptions: backendOptions) + } +} + public extension Module { /// Executes a specific method with the provided input values. /// The method is loaded on demand if not already loaded. diff --git a/extension/apple/ExecuTorch/Exported/ExecuTorch.h b/extension/apple/ExecuTorch/Exported/ExecuTorch.h index 3a12a5ddbae..67384821516 100644 --- a/extension/apple/ExecuTorch/Exported/ExecuTorch.h +++ b/extension/apple/ExecuTorch/Exported/ExecuTorch.h @@ -6,6 +6,7 @@ * LICENSE file in the root directory of this source tree. */ +#import "ExecuTorchBackendOption.h" #import "ExecuTorchError.h" #import "ExecuTorchLog.h" #import "ExecuTorchModule.h" diff --git a/extension/apple/ExecuTorch/Exported/ExecuTorchBackendOption.h b/extension/apple/ExecuTorch/Exported/ExecuTorchBackendOption.h new file mode 100644 index 00000000000..9509f6fce8a --- /dev/null +++ b/extension/apple/ExecuTorch/Exported/ExecuTorchBackendOption.h @@ -0,0 +1,89 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * Enum to define the type of a backend option value. + */ +typedef NS_ENUM(NSInteger, ExecuTorchBackendOptionType) { + ExecuTorchBackendOptionTypeBoolean, + ExecuTorchBackendOptionTypeInteger, + ExecuTorchBackendOptionTypeString, +} NS_SWIFT_NAME(BackendOptionType); + +/** + * Represents a single key-value configuration option for a backend. + * + * Backend options are used to pass per-delegate configuration (e.g., compute + * unit, thread count, cache directory) when loading a module. Each option has + * a string key and a typed value (boolean, integer, or string). + */ +NS_SWIFT_NAME(BackendOption) +__attribute__((objc_subclassing_restricted)) +@interface ExecuTorchBackendOption : NSObject + +/** The option key name (e.g. "compute_unit", "num_threads"). */ +@property (nonatomic, readonly) NSString *key; + +/** The type of the option value. */ +@property (nonatomic, readonly) ExecuTorchBackendOptionType type; + +/** The boolean value. Only valid when type is Boolean. */ +@property (nonatomic, readonly) BOOL boolValue; + +/** The integer value. Only valid when type is Integer. */ +@property (nonatomic, readonly) NSInteger intValue; + +/** The string value. Only valid when type is String. */ +@property (nullable, nonatomic, readonly) NSString *stringValue; + +/** + * Creates a backend option with a boolean value. + * + * @param key The option key. + * @param value The boolean value. + * @return A new ExecuTorchBackendOption instance. + */ ++ (instancetype)optionWithKey:(NSString *)key + booleanValue:(BOOL)value + NS_SWIFT_NAME(init(_:_:)) + NS_RETURNS_RETAINED; + +/** + * Creates a backend option with an integer value. + * + * @param key The option key. + * @param value The integer value. + * @return A new ExecuTorchBackendOption instance. + */ ++ (instancetype)optionWithKey:(NSString *)key + integerValue:(NSInteger)value + NS_SWIFT_NAME(init(_:_:)) + NS_RETURNS_RETAINED; + +/** + * Creates a backend option with a string value. + * + * @param key The option key. + * @param value The string value. + * @return A new ExecuTorchBackendOption instance. + */ ++ (instancetype)optionWithKey:(NSString *)key + stringValue:(NSString *)value + NS_SWIFT_NAME(init(_:_:)) + NS_RETURNS_RETAINED; + ++ (instancetype)new NS_UNAVAILABLE; +- (instancetype)init NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END diff --git a/extension/apple/ExecuTorch/Exported/ExecuTorchBackendOption.mm b/extension/apple/ExecuTorch/Exported/ExecuTorchBackendOption.mm new file mode 100644 index 00000000000..eb95024a018 --- /dev/null +++ b/extension/apple/ExecuTorch/Exported/ExecuTorchBackendOption.mm @@ -0,0 +1,67 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "ExecuTorchBackendOption.h" + +@implementation ExecuTorchBackendOption { + NSString *_key; + ExecuTorchBackendOptionType _type; + BOOL _boolValue; + NSInteger _intValue; + NSString *_stringValue; +} + +- (instancetype)initWithKey:(NSString *)key + booleanValue:(BOOL)value { + self = [super init]; + if (self) { + _key = [key copy]; + _type = ExecuTorchBackendOptionTypeBoolean; + _boolValue = value; + } + return self; +} + +- (instancetype)initWithKey:(NSString *)key + integerValue:(NSInteger)value { + self = [super init]; + if (self) { + _key = [key copy]; + _type = ExecuTorchBackendOptionTypeInteger; + _intValue = value; + } + return self; +} + +- (instancetype)initWithKey:(NSString *)key + stringValue:(NSString *)value { + self = [super init]; + if (self) { + _key = [key copy]; + _type = ExecuTorchBackendOptionTypeString; + _stringValue = [value copy]; + } + return self; +} + ++ (instancetype)optionWithKey:(NSString *)key + booleanValue:(BOOL)value { + return [[self alloc] initWithKey:key booleanValue:value]; +} + ++ (instancetype)optionWithKey:(NSString *)key + integerValue:(NSInteger)value { + return [[self alloc] initWithKey:key integerValue:value]; +} + ++ (instancetype)optionWithKey:(NSString *)key + stringValue:(NSString *)value { + return [[self alloc] initWithKey:key stringValue:value]; +} + +@end diff --git a/extension/apple/ExecuTorch/Exported/ExecuTorchModule.h b/extension/apple/ExecuTorch/Exported/ExecuTorchModule.h index 9b8400d739f..195becbd844 100644 --- a/extension/apple/ExecuTorch/Exported/ExecuTorchModule.h +++ b/extension/apple/ExecuTorch/Exported/ExecuTorchModule.h @@ -6,6 +6,7 @@ * LICENSE file in the root directory of this source tree. */ +#import "ExecuTorchBackendOption.h" #import "ExecuTorchValue.h" NS_ASSUME_NONNULL_BEGIN @@ -186,6 +187,30 @@ NS_SWIFT_NAME(Module) */ - (BOOL)load:(NSError **)error; +/** + * Loads the module's program with per-delegate backend options. + * + * @param backendOptions A dictionary mapping backend identifiers (e.g. "CoreMLBackend") + * to arrays of ExecuTorchBackendOption objects configuring that backend. + * @param verification The verification level to apply when loading the program. + * @param error A pointer to an NSError pointer that will be set if an error occurs. + * @return YES if the program was successfully loaded; otherwise, NO. + */ +- (BOOL)loadWithBackendOptions:(NSDictionary *> *)backendOptions + verification:(ExecuTorchVerification)verification + error:(NSError **)error NS_REFINED_FOR_SWIFT; + +/** + * Loads the module's program with per-delegate backend options using minimal verification. + * + * @param backendOptions A dictionary mapping backend identifiers (e.g. "CoreMLBackend") + * to arrays of ExecuTorchBackendOption objects configuring that backend. + * @param error A pointer to an NSError pointer that will be set if an error occurs. + * @return YES if the program was successfully loaded; otherwise, NO. + */ +- (BOOL)loadWithBackendOptions:(NSDictionary *> *)backendOptions + error:(NSError **)error NS_REFINED_FOR_SWIFT; + /** * Checks if the module is loaded. * @@ -203,6 +228,19 @@ NS_SWIFT_NAME(Module) - (BOOL)loadMethod:(NSString *)methodName error:(NSError **)error NS_SWIFT_NAME(load(_:)); +/** + * Loads a specific method from the program with per-delegate backend options. + * + * @param methodName A string representing the name of the method to load. + * @param backendOptions A dictionary mapping backend identifiers (e.g. "CoreMLBackend") + * to arrays of ExecuTorchBackendOption objects configuring that backend. + * @param error A pointer to an NSError pointer that is set if an error occurs. + * @return YES if the method was successfully loaded; otherwise, NO. + */ +- (BOOL)loadMethod:(NSString *)methodName + backendOptions:(NSDictionary *> *)backendOptions + error:(NSError **)error NS_REFINED_FOR_SWIFT; + /** * Checks if a specific method is loaded. * diff --git a/extension/apple/ExecuTorch/Exported/ExecuTorchModule.mm b/extension/apple/ExecuTorch/Exported/ExecuTorchModule.mm index 69bb59c860e..ce99b60e381 100644 --- a/extension/apple/ExecuTorch/Exported/ExecuTorchModule.mm +++ b/extension/apple/ExecuTorch/Exported/ExecuTorchModule.mm @@ -8,11 +8,14 @@ #import "ExecuTorchModule.h" +#import "ExecuTorchBackendOption.h" #import "ExecuTorchError.h" #import "ExecuTorchUtils.h" #import #import +#import +#import using namespace executorch::extension; using namespace executorch::runtime; @@ -63,6 +66,49 @@ static inline EValue toEValue(ExecuTorchValue *value) { return [ExecuTorchValue new]; } +static Error buildBackendOptionsMap( + NSDictionary *> *backendOptions, + std::vector> &allOptions, + LoadBackendOptionsMap &map) { + allOptions.reserve(backendOptions.count); + for (NSString *backendId in backendOptions) { + NSArray *options = backendOptions[backendId]; + std::vector opts; + opts.reserve(options.count); + for (ExecuTorchBackendOption *opt in options) { + BackendOption bo; + strncpy(bo.key, opt.key.UTF8String, kMaxOptionKeyLength - 1); + bo.key[kMaxOptionKeyLength - 1] = '\0'; + switch (opt.type) { + case ExecuTorchBackendOptionTypeBoolean: + bo.value = (bool)opt.boolValue; + break; + case ExecuTorchBackendOptionTypeInteger: + if (opt.intValue < INT_MIN || opt.intValue > INT_MAX) { + return Error::InvalidArgument; + } + bo.value = (int)opt.intValue; + break; + case ExecuTorchBackendOptionTypeString: { + std::array arr{}; + strncpy(arr.data(), opt.stringValue.UTF8String, kMaxOptionValueLength - 1); + arr[kMaxOptionValueLength - 1] = '\0'; + bo.value = arr; + break; + } + } + opts.push_back(bo); + } + allOptions.push_back(std::move(opts)); + auto &backOpts = allOptions.back(); + const auto err = map.set_options(backendId.UTF8String, Span(backOpts.data(), backOpts.size())); + if (err != Error::Ok) { + return err; + } + } + return Error::Ok; +} + @interface ExecuTorchTensorMetadata () - (instancetype)initWithTensorMetadata:(const TensorInfo &)tensorInfo @@ -324,6 +370,57 @@ - (BOOL)loadMethod:(NSString *)methodName return YES; } +- (BOOL)loadWithBackendOptions:(NSDictionary *> *)backendOptions + verification:(ExecuTorchVerification)verification + error:(NSError **)error { + std::vector> allOptions; + LoadBackendOptionsMap map; + const auto buildError = buildBackendOptionsMap(backendOptions, allOptions, map); + if (buildError != Error::Ok) { + if (error) { + *error = ExecuTorchErrorWithCode((ExecuTorchErrorCode)buildError); + } + return NO; + } + const auto errorCode = _module->load(map, static_cast(verification)); + if (errorCode != Error::Ok) { + if (error) { + *error = ExecuTorchErrorWithCode((ExecuTorchErrorCode)errorCode); + } + return NO; + } + return YES; +} + +- (BOOL)loadWithBackendOptions:(NSDictionary *> *)backendOptions + error:(NSError **)error { + return [self loadWithBackendOptions:backendOptions + verification:ExecuTorchVerificationMinimal + error:error]; +} + +- (BOOL)loadMethod:(NSString *)methodName + backendOptions:(NSDictionary *> *)backendOptions + error:(NSError **)error { + std::vector> allOptions; + LoadBackendOptionsMap map; + const auto buildError = buildBackendOptionsMap(backendOptions, allOptions, map); + if (buildError != Error::Ok) { + if (error) { + *error = ExecuTorchErrorWithCode((ExecuTorchErrorCode)buildError); + } + return NO; + } + const auto errorCode = _module->load_method(methodName.UTF8String, nullptr, nullptr, &map); + if (errorCode != Error::Ok) { + if (error) { + *error = ExecuTorchErrorWithCode((ExecuTorchErrorCode)errorCode); + } + return NO; + } + return YES; +} + - (BOOL)isMethodLoaded:(NSString *)methodName { return _module->is_method_loaded(methodName.UTF8String); } diff --git a/extension/apple/ExecuTorch/__tests__/ModuleTest.swift b/extension/apple/ExecuTorch/__tests__/ModuleTest.swift index 1cc4a31c4a3..a9f27984424 100644 --- a/extension/apple/ExecuTorch/__tests__/ModuleTest.swift +++ b/extension/apple/ExecuTorch/__tests__/ModuleTest.swift @@ -193,4 +193,82 @@ class ModuleTest: XCTestCase { XCTAssertNoThrow(try module.setInputs(Tensor([2]), Tensor([3]))) XCTAssertEqual(try module.forward(), Tensor([5])) } + + func testBackendOptionCreation() { + let boolOption = BackendOption("use_cache", true) + XCTAssertEqual(boolOption.key, "use_cache") + XCTAssertEqual(boolOption.type, .boolean) + XCTAssertTrue(boolOption.boolValue) + + let intOption = BackendOption("num_threads", 4) + XCTAssertEqual(intOption.key, "num_threads") + XCTAssertEqual(intOption.type, .integer) + XCTAssertEqual(intOption.intValue, 4) + + let stringOption = BackendOption("compute_unit", "cpu_and_gpu") + XCTAssertEqual(stringOption.key, "compute_unit") + XCTAssertEqual(stringOption.type, .string) + XCTAssertEqual(stringOption.stringValue, "cpu_and_gpu") + } + + func testLoadWithBackendOptions() { + guard let modelPath = resourceBundle.path(forResource: "add", ofType: "pte") else { + XCTFail("Couldn't find the model file") + return + } + let module = Module(filePath: modelPath) + let backendOptions: [String: [BackendOption]] = [ + "SomeBackend": [ + BackendOption("num_threads", 4), + BackendOption("use_cache", true), + ] + ] + XCTAssertNoThrow(try module.load(backendOptions: backendOptions)) + XCTAssertTrue(module.isLoaded()) + } + + func testLoadWithEmptyBackendOptions() { + guard let modelPath = resourceBundle.path(forResource: "add", ofType: "pte") else { + XCTFail("Couldn't find the model file") + return + } + let module = Module(filePath: modelPath) + let backendOptions: [String: [BackendOption]] = [:] + XCTAssertNoThrow(try module.load(backendOptions: backendOptions)) + XCTAssertTrue(module.isLoaded()) + } + + func testLoadMethodWithBackendOptions() { + guard let modelPath = resourceBundle.path(forResource: "add", ofType: "pte") else { + XCTFail("Couldn't find the model file") + return + } + let module = Module(filePath: modelPath) + let backendOptions: [String: [BackendOption]] = [ + "SomeBackend": [ + BackendOption("compute_unit", "cpu_and_gpu"), + ] + ] + XCTAssertNoThrow(try module.load("forward", backendOptions: backendOptions)) + XCTAssertTrue(module.isLoaded("forward")) + } + + func testLoadWithBackendOptionsThenExecute() { + guard let modelPath = resourceBundle.path(forResource: "add", ofType: "pte") else { + XCTFail("Couldn't find the model file") + return + } + let module = Module(filePath: modelPath) + let backendOptions: [String: [BackendOption]] = [ + "SomeBackend": [ + BackendOption("num_threads", 4), + ] + ] + XCTAssertNoThrow(try module.load(backendOptions: backendOptions)) + + let inputs: [Tensor] = [Tensor([1]), Tensor([1])] + var outputs: [Value]? + XCTAssertNoThrow(outputs = try module.forward(inputs)) + XCTAssertEqual(outputs?.first?.tensor(), Tensor([Float(2)])) + } }