Skip to content

Commit 3f8d2ce

Browse files
committed
up
1 parent 4735f7f commit 3f8d2ce

8 files changed

Lines changed: 149 additions & 40 deletions

File tree

Package.swift

Lines changed: 14 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,22 +127,24 @@ for (key, value) in products {
126127
packageTargets.append(target)
127128
}
128129

129-
// Test fixtures. add_coreml.pte is generated at CI time by
130-
// extension/apple/ExecuTorch/__tests__/resources/generate_coreml_test_models.py
131-
// (invoked by scripts/build_apple_frameworks.sh before `swift test`). It is
132-
// gitignored, so include it in test resources only when present so that
133-
// `swift test` runs on dev machines without CoreML python deps don't fail
134-
// at the SwiftPM resolve stage.
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.
135136
let testResourcesDir = "extension/apple/ExecuTorch/__tests__/resources"
136137
var testResources: [Resource] = [.copy("resources/add.pte")]
137138
if FileManager.default.fileExists(atPath: "\(testResourcesDir)/add_coreml.pte") {
138139
testResources.append(.copy("resources/add_coreml.pte"))
139140
}
141+
if FileManager.default.fileExists(atPath: "\(testResourcesDir)/add_mul_coreml.pte") {
142+
testResources.append(.copy("resources/add_mul_coreml.pte"))
143+
}
140144

141145
// SwiftPM resources must live under the target's path, so the ObjC test
142146
// target uses symlinks to the canonical resources directory. The symlinks
143-
// themselves are gitignored and (re)created by scripts/build_apple_frameworks.sh
144-
// (CI) and swift_play/run.sh (local).
147+
// themselves are gitignored and (re)created by scripts/build_apple_frameworks.sh.
145148
let objcTestsDir = "extension/apple/ExecuTorch/__tests__/ObjC"
146149
var objcTestResources: [Resource] = []
147150
if FileManager.default.fileExists(atPath: "\(objcTestsDir)/add.pte") {
@@ -150,6 +153,9 @@ if FileManager.default.fileExists(atPath: "\(objcTestsDir)/add.pte") {
150153
if FileManager.default.fileExists(atPath: "\(objcTestsDir)/add_coreml.pte") {
151154
objcTestResources.append(.copy("add_coreml.pte"))
152155
}
156+
if FileManager.default.fileExists(atPath: "\(objcTestsDir)/add_mul_coreml.pte") {
157+
objcTestResources.append(.copy("add_mul_coreml.pte"))
158+
}
153159

154160
let testLinkerSettings: [LinkerSetting] = [
155161
.unsafeFlags([

extension/apple/ExecuTorch/Exported/ExecuTorchBackendOptionsMap.mm

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,11 @@ Error buildBackendOptionsMap(
7171
break;
7272
case ExecuTorchBackendOptionTypeInteger:
7373
// The C++ runtime stores integer option values as 32-bit `int`.
74-
// Reject anything that would silently narrow.
74+
// Reject anything that would silently narrow. On Apple's current
75+
// 64-bit-only targets NSInteger is int64_t, so this check is
76+
// meaningful; on a hypothetical 32-bit build NSInteger would be
77+
// int32_t and the comparison would be tautological (still correct,
78+
// just never trips).
7579
if (opt.intValue < INT_MIN || opt.intValue > INT_MAX) {
7680
return Error::InvalidArgument;
7781
}
@@ -138,6 +142,13 @@ - (nullable instancetype)initWithOptions:(NSDictionary<NSString *, NSArray<Execu
138142
return nil;
139143
}
140144
_storage = std::move(storage);
145+
// Move-assignment order matters: `_storage` is moved first so the Spans
146+
// inside `map` (which point at each inner vector's heap buffer) survive
147+
// into `_storage`. std::vector's move preserves data() pointers, so the
148+
// Spans remain valid. This relies on `LoadBackendOptionsMap`'s move
149+
// being a shallow member-wise move that does not recompute span
150+
// pointers; if that ever changes, the move order here would need to be
151+
// revisited. The end-to-end CoreML-delegated test exercises this path.
141152
_map = std::move(map);
142153
// Snapshot the input as a shallow-immutable dictionary, then also copy
143154
// each value array immutably. Combined with ExecuTorchBackendOption

extension/apple/ExecuTorch/Exported/ExecuTorchModule.mm

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -375,13 +375,14 @@ - (BOOL)loadMethod:(NSString *)methodName
375375
options:(ExecuTorchBackendOptionsMap *)options
376376
error:(NSError **)error {
377377
NSParameterAssert(options);
378-
// Retain the options for the Module's lifetime, matching -loadWithOptions:.
379-
// Today Module::load_method consumes the pointer synchronously and does
380-
// not cache it, so retention is not strictly required for this call.
381-
// We retain uniformly so the ObjC ownership rule is the same for both
382-
// entry points: any BackendOptionsMap handed to the Module stays alive
383-
// as long as the Module does.
384-
_loadedBackendOptions = options;
378+
// Do NOT assign to _loadedBackendOptions here. Module::load_method
379+
// consumes `backend_options` synchronously within this call and does
380+
// not cache it (see module.cpp); ARC keeps `options` alive for the
381+
// call duration via the parameter. Overwriting _loadedBackendOptions
382+
// would release any map previously installed by -loadWithOptions:,
383+
// but the C++ Module's `backend_options_` raw pointer (set by
384+
// -loadWithOptions:) would still reference that released map's
385+
// storage — a use-after-free on the next lazy load_method.
385386
const auto errorCode = _module->load_method(methodName.UTF8String,
386387
/*planned_memory=*/nullptr,
387388
/*event_tracer=*/nullptr,

extension/apple/ExecuTorch/__tests__/ModuleTest.swift

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,4 +374,74 @@ class ModuleTest: XCTestCase {
374374
XCTAssertEqual(outs.first?.tensor(), Tensor([Float(2)]))
375375
}
376376
}
377+
378+
// Covers the Module.load(_:options:) Swift wrapper over the ObjC
379+
// loadMethod:options: bridge. Loads a specific method with per-delegate
380+
// backend options, then executes it.
381+
func testLoadMethodWithBackendOptionsThenExecuteOnCoreMLDelegatedModel() throws {
382+
let modelPath = try requireFixture("add_coreml", ofType: "pte")
383+
let module = Module(filePath: modelPath)
384+
let options = try BackendOptionsMap(options: [
385+
"CoreMLBackend": [
386+
BackendOption("compute_unit", "cpu_and_gpu"),
387+
]
388+
])
389+
try module.load("forward", options: options)
390+
XCTAssertTrue(module.isLoaded("forward"))
391+
392+
let inputs: [Tensor<Float>] = [Tensor([1]), Tensor([1])]
393+
let outputs: [Value] = try module.forward(inputs)
394+
XCTAssertEqual(outputs.first?.tensor(), Tensor([Float(2)]))
395+
}
396+
397+
// Mixed sequence on a multi-method delegated model:
398+
// 1. load(optionsA) — installs optionsA; the C++ Module
399+
// stores a raw pointer into optionsA's storage and the ObjC
400+
// wrapper retains optionsA via _loadedBackendOptions.
401+
// 2. load("mul", options: optionsB) — loads "mul" explicitly with
402+
// optionsB, synchronously. Must NOT release optionsA (doing so
403+
// would leave _module->backend_options_ dangling).
404+
// 3. forward(inputs) — triggers a lazy load_method on
405+
// "forward" which falls back to the stored pointer (into optionsA).
406+
//
407+
// The XCTAssertNotNil(weakA) after step 2 is the deterministic check:
408+
// a buggy loadMethod:options: that assigns `_loadedBackendOptions =
409+
// options` releases optionsA's last strong ref there, weakA becomes
410+
// nil, and the assertion fails independent of heap layout. With the
411+
// correct implementation weakA stays non-nil. The forward/execute
412+
// assertions additionally verify the positive path end-to-end.
413+
func testMixedLoadWithOptionsAndLoadMethodWithOptionsOnMultiMethodModel() throws {
414+
let modelPath = try requireFixture("add_mul_coreml", ofType: "pte")
415+
let module = Module(filePath: modelPath)
416+
417+
weak var weakA: BackendOptionsMap?
418+
try autoreleasepool {
419+
let optionsA = try BackendOptionsMap(options: [
420+
"CoreMLBackend": [BackendOption("compute_unit", "cpu_only")]
421+
])
422+
weakA = optionsA
423+
try module.load(optionsA)
424+
}
425+
XCTAssertNotNil(weakA, "Module must retain optionsA after load(optionsA)")
426+
427+
try autoreleasepool {
428+
let optionsB = try BackendOptionsMap(options: [
429+
"CoreMLBackend": [BackendOption("compute_unit", "cpu_and_gpu")]
430+
])
431+
try module.load("mul", options: optionsB)
432+
}
433+
XCTAssertTrue(module.isLoaded("mul"))
434+
XCTAssertNotNil(weakA,
435+
"load(\"mul\", options: optionsB) must not release optionsA — " +
436+
"_module->backend_options_ still points into its storage")
437+
438+
// Lazy load_method("forward") must still see valid optionsA storage.
439+
let inputs: [Tensor<Float>] = [Tensor([2]), Tensor([3])]
440+
let addOuts: [Value] = try module.forward(inputs)
441+
XCTAssertEqual(addOuts.first?.tensor(), Tensor([Float(5)]))
442+
443+
// "mul" was loaded explicitly with optionsB and should compute 2 * 3.
444+
let mulOuts: [Value] = try module.execute("mul", inputs)
445+
XCTAssertEqual(mulOuts.first?.tensor(), Tensor([Float(6)]))
446+
}
377447
}
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
# Resource symlinks created at build time by scripts/build_apple_frameworks.sh
2-
# (CI) and swift_play/run.sh (local). Pointing at ../resources/*.pte so the
3-
# ObjC test target's .copy(...) can find them under its own path per SwiftPM
4-
# rules.
1+
# Resource symlinks created at build time by scripts/build_apple_frameworks.sh.
2+
# Pointing at ../resources/*.pte so the ObjC test target's .copy(...) can
3+
# find them under its own path per SwiftPM rules.
54
add.pte
65
add_coreml.pte
6+
add_mul_coreml.pte
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
# Generated by generate_coreml_test_models.py at CI time, not committed.
22
add_coreml.pte
3+
add_mul_coreml.pte
Lines changed: 37 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,22 @@
11
"""Generate CoreML-delegated test fixtures for the Swift/ObjC bindings.
22
33
Currently produces:
4-
- add_coreml.pte: a CoreML-delegated tensor-add model whose forward(x, y)
5-
returns x + y. Used by ModuleTest.testLoadWithBackendOptionsThenExecuteOnCoreML
6-
to exercise the BackendOptionsMap lifetime path end-to-end against a
7-
delegated model (add.pte has no delegates, so the per-delegate option
8-
lookup path is only exercised with a delegated fixture like this one).
4+
- add_coreml.pte: a single-method CoreML-delegated tensor-add model whose
5+
forward(x, y) returns x + y.
6+
- add_mul_coreml.pte: a two-method CoreML-delegated model exposing
7+
forward(x, y) = x + y and mul(x, y) = x * y. Used to exercise mixed
8+
load(options:) / load(_:options:) sequences where one method is loaded
9+
explicitly with its own options and another is loaded lazily, so the
10+
C++ Module's stored backend_options_ is consulted during the lazy path.
11+
A non-delegated or single-method fixture does not reach that code path.
912
1013
Usage:
1114
python extension/apple/ExecuTorch/__tests__/resources/generate_coreml_test_models.py
1215
1316
This script is invoked by scripts/build_apple_frameworks.sh in CI before
14-
`swift test` so the fixture is always present in CI runs. The output .pte is
15-
gitignored; local developers who want to run the CoreML-dependent tests should
16-
run this script once.
17+
`swift test` so the fixtures are always present in CI runs. The output .pte
18+
files are gitignored; local developers who want to run the CoreML-dependent
19+
tests should run this script once.
1720
"""
1821

1922
import os
@@ -30,22 +33,38 @@ def forward(self, x: torch.Tensor, y: torch.Tensor) -> torch.Tensor:
3033
return x + y
3134

3235

33-
def main() -> None:
34-
model = AddModule().eval()
35-
example_inputs = (torch.tensor([1.0]), torch.tensor([1.0]))
36+
class MulModule(nn.Module):
37+
def forward(self, x: torch.Tensor, y: torch.Tensor) -> torch.Tensor:
38+
return x * y
3639

37-
ep = torch.export.export(model, example_inputs)
38-
lowered = to_edge_transform_and_lower(
39-
ep,
40-
partitioner=[CoreMLPartitioner()],
41-
)
42-
exec_program = lowered.to_executorch()
4340

44-
out_path = os.path.join(os.path.dirname(__file__), "add_coreml.pte")
41+
def _write_pte(exec_program, filename: str) -> None:
42+
out_path = os.path.join(os.path.dirname(__file__), filename)
4543
with open(out_path, "wb") as f:
4644
exec_program.write_to_file(f)
4745
print(f"Wrote {out_path} ({os.path.getsize(out_path)} bytes)")
4846

4947

48+
def main() -> None:
49+
example_inputs = (torch.tensor([1.0]), torch.tensor([1.0]))
50+
51+
# Single-method add model.
52+
ep_add = torch.export.export(AddModule().eval(), example_inputs)
53+
add_only = to_edge_transform_and_lower(
54+
ep_add,
55+
partitioner=[CoreMLPartitioner()],
56+
).to_executorch()
57+
_write_pte(add_only, "add_coreml.pte")
58+
59+
# Two-method model: forward (add) and mul. Both are CoreML-delegated so
60+
# each has its own per-delegate option set to query at load time.
61+
ep_mul = torch.export.export(MulModule().eval(), example_inputs)
62+
add_mul = to_edge_transform_and_lower(
63+
{"forward": ep_add, "mul": ep_mul},
64+
partitioner=[CoreMLPartitioner()],
65+
).to_executorch()
66+
_write_pte(add_mul, "add_mul_coreml.pte")
67+
68+
5069
if __name__ == "__main__":
5170
main()

scripts/build_apple_frameworks.sh

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -342,8 +342,9 @@ python3 extension/apple/ExecuTorch/__tests__/resources/generate_coreml_test_mode
342342

343343
# SwiftPM requires resources to live under the test target's path. The ObjC
344344
# test target shares fixtures with the Swift one via symlinks.
345-
ln -sfn ../resources/add.pte extension/apple/ExecuTorch/__tests__/ObjC/add.pte
346-
ln -sfn ../resources/add_coreml.pte extension/apple/ExecuTorch/__tests__/ObjC/add_coreml.pte
345+
ln -sfn ../resources/add.pte extension/apple/ExecuTorch/__tests__/ObjC/add.pte
346+
ln -sfn ../resources/add_coreml.pte extension/apple/ExecuTorch/__tests__/ObjC/add_coreml.pte
347+
ln -sfn ../resources/add_mul_coreml.pte extension/apple/ExecuTorch/__tests__/ObjC/add_mul_coreml.pte
347348

348349
echo "Running tests"
349350

0 commit comments

Comments
 (0)