Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 51 additions & 8 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
// https://pytorch.org/executorch/main/using-executorch-ios

import PackageDescription
import Foundation

let debug_suffix = "_debug"
let dependencies_suffix = "_with_dependencies"
Expand Down Expand Up @@ -126,6 +127,43 @@ for (key, value) in products {
packageTargets.append(target)
}

// Test fixtures. add_coreml.pte and add_mul_coreml.pte are generated at CI
// time by extension/apple/ExecuTorch/__tests__/resources/generate_coreml_test_models.py
// (invoked by scripts/build_apple_frameworks.sh before `swift test`). They
// are gitignored, so include them in test resources only when present so
// that `swift test` runs on dev machines without CoreML python deps don't
// fail at the SwiftPM resolve stage.
let testResourcesDir = "extension/apple/ExecuTorch/__tests__/resources"
var testResources: [Resource] = [.copy("resources/add.pte")]
if FileManager.default.fileExists(atPath: "\(testResourcesDir)/add_coreml.pte") {
testResources.append(.copy("resources/add_coreml.pte"))
}
if FileManager.default.fileExists(atPath: "\(testResourcesDir)/add_mul_coreml.pte") {
testResources.append(.copy("resources/add_mul_coreml.pte"))
}

// SwiftPM resources must live under the target's path, so the ObjC test
// target uses symlinks to the canonical resources directory. The symlinks
// themselves are gitignored and (re)created by scripts/build_apple_frameworks.sh.
let objcTestsDir = "extension/apple/ExecuTorch/__tests__/ObjC"
var objcTestResources: [Resource] = []
if FileManager.default.fileExists(atPath: "\(objcTestsDir)/add.pte") {
objcTestResources.append(.copy("add.pte"))
}
if FileManager.default.fileExists(atPath: "\(objcTestsDir)/add_coreml.pte") {
objcTestResources.append(.copy("add_coreml.pte"))
}
if FileManager.default.fileExists(atPath: "\(objcTestsDir)/add_mul_coreml.pte") {
objcTestResources.append(.copy("add_mul_coreml.pte"))
}

let testLinkerSettings: [LinkerSetting] = [
.unsafeFlags([
"-Xlinker", "-force_load",
"-Xlinker", "cmake-out/kernels_optimized.xcframework/macos-arm64/libkernels_optimized_macos.a",
])
]

let package = Package(
name: "executorch",
platforms: [
Expand All @@ -141,15 +179,20 @@ let package = Package(
.target(name: "kernels_optimized\(dependencies_suffix)"),
],
path: "extension/apple/ExecuTorch/__tests__",
resources: [
.copy("resources/add.pte"),
exclude: ["ObjC", "resources/generate_coreml_test_models.py", "resources/.gitignore"],
resources: testResources,
linkerSettings: testLinkerSettings
),
.testTarget(
name: "objc_tests",
dependencies: [
.target(name: "executorch\(debug_suffix)"),
.target(name: "kernels_optimized\(dependencies_suffix)"),
],
linkerSettings: [
.unsafeFlags([
"-Xlinker", "-force_load",
"-Xlinker", "cmake-out/kernels_optimized.xcframework/macos-arm64/libkernels_optimized_macos.a",
])
]
path: "extension/apple/ExecuTorch/__tests__/ObjC",
exclude: [".gitignore"],
resources: objcTestResources,
linkerSettings: testLinkerSettings
)
]
)
34 changes: 34 additions & 0 deletions extension/apple/ExecuTorch/Exported/ExecuTorch+Module.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,40 @@ public extension MethodMetadata {
}
}

public extension Module {
/// Loads the module's program with per-delegate backend options.
///
/// The receiver retains `options` for as long as the underlying program
/// references it (lifetime tracked via ARC).
///
/// - Parameters:
/// - options: A `BackendOptionsMap` built once from a dict of
/// per-backend `BackendOption`s, e.g.
/// `try BackendOptionsMap(["CoreMLBackend": [BackendOption("compute_unit", "cpu_and_gpu")]])`.
/// - verification: The verification level to apply when loading the program.
/// - Throws: An error if loading fails.
func load(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Module.load(_ options: BackendOptionsMap, verification:) here, plus the existing loadMethod:error: (bridged to Swift as load(_:) at ExecuTorchModule.h:233), produce an autocomplete collision: module.load(someMap) and module.load("forward") both compile cleanly with no first-positional label hint.

Suggest renaming the first label: load(options:) and load(_ method:, options:) (i.e. add the label to options: here and on the sibling at line 76). One keystroke at the call site, eliminates a real ambiguity for anyone using autocomplete.

_ options: BackendOptionsMap,
verification: ModuleVerification = .minimal
) throws {
try __load(withOptions: options, verification: verification)
}

/// Loads a specific method from the program with per-delegate backend options.
///
/// - Parameters:
/// - method: The name of the method to load.
/// - options: A `BackendOptionsMap` built once from a dict of
/// per-backend `BackendOption`s.
/// - Throws: An error if loading fails.
func load(
_ method: String,
options: BackendOptionsMap
) throws {
try __loadMethod(method, options: options)
}
}

public extension Module {
/// Executes a specific method with the provided input values.
/// The method is loaded on demand if not already loaded.
Expand Down
2 changes: 2 additions & 0 deletions extension/apple/ExecuTorch/Exported/ExecuTorch.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
* LICENSE file in the root directory of this source tree.
*/

#import "ExecuTorchBackendOption.h"
#import "ExecuTorchBackendOptionsMap.h"
#import "ExecuTorchError.h"
#import "ExecuTorchLog.h"
#import "ExecuTorchModule.h"
Expand Down
89 changes: 89 additions & 0 deletions extension/apple/ExecuTorch/Exported/ExecuTorchBackendOption.h
Original file line number Diff line number Diff line change
@@ -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 <Foundation/Foundation.h>

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
137 changes: 137 additions & 0 deletions extension/apple/ExecuTorch/Exported/ExecuTorchBackendOption.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/*
* 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];
}

#pragma mark - NSObject

- (NSString *)description {
switch (_type) {
case ExecuTorchBackendOptionTypeBoolean:
return [NSString stringWithFormat:@"<%@ %@=%@ (bool)>",
NSStringFromClass([self class]), _key, _boolValue ? @"true" : @"false"];
case ExecuTorchBackendOptionTypeInteger:
return [NSString stringWithFormat:@"<%@ %@=%ld (int)>",
NSStringFromClass([self class]), _key, (long)_intValue];
case ExecuTorchBackendOptionTypeString:
return [NSString stringWithFormat:@"<%@ %@=%@ (string)>",
NSStringFromClass([self class]), _key,
_stringValue ? [NSString stringWithFormat:@"\"%@\"", _stringValue] : @"(null)"];
}
return [super description];
}

- (NSString *)debugDescription {
return [self description];
}

- (BOOL)isEqual:(id)object {
if (self == object) {
return YES;
}
if (![object isKindOfClass:[ExecuTorchBackendOption class]]) {
return NO;
}
ExecuTorchBackendOption *other = (ExecuTorchBackendOption *)object;
if (_type != other.type || ![_key isEqualToString:other.key]) {
return NO;
}
switch (_type) {
case ExecuTorchBackendOptionTypeBoolean:
return _boolValue == other.boolValue;
case ExecuTorchBackendOptionTypeInteger:
return _intValue == other.intValue;
case ExecuTorchBackendOptionTypeString: {
// Both are non-null when type is String (init enforces it), but be
// defensive in case of subclass/manual misuse.
NSString *otherString = other.stringValue;
if (_stringValue == otherString) {
return YES;
}
if (_stringValue == nil || otherString == nil) {
return NO;
}
return [_stringValue isEqualToString:otherString];
}
}
return NO;
}

- (NSUInteger)hash {
NSUInteger h = _key.hash ^ (NSUInteger)_type;
switch (_type) {
case ExecuTorchBackendOptionTypeBoolean:
h ^= (NSUInteger)(_boolValue ? 1 : 0);
break;
case ExecuTorchBackendOptionTypeInteger:
h ^= (NSUInteger)_intValue;
break;
case ExecuTorchBackendOptionTypeString:
h ^= _stringValue.hash;
break;
}
return h;
}

@end
Loading
Loading