@@ -252,27 +252,6 @@ @implementation ExecuTorchModule {
252252 std::unique_ptr<Module> _module;
253253 NSMutableDictionary <NSString *, NSMutableArray <ExecuTorchValue *> *> *_inputs;
254254 NSMutableDictionary <NSString *, NSMutableArray <ExecuTorchValue *> *> *_outputs;
255- // Strong reference to the most recently passed BackendOptionsMap. The
256- // C++ Module borrows a pointer into the map's underlying C++ storage and
257- // dereferences it during lazy load_method calls (triggered by forward),
258- // so the ObjC wrapper must keep it alive. ARC handles the lifetime.
259- //
260- // INVARIANT: this ivar is only ever overwritten with another non-nil
261- // BackendOptionsMap, and never reset to nil while `_module` is alive.
262- // Resetting to nil would release the C++ map while `_module` still holds
263- // a borrowed pointer into it.
264- //
265- // THREAD SAFETY: like the rest of `ExecuTorchModule`, write access here
266- // is not thread-safe. The ARC retain/release on assignment is non-atomic
267- // for direct ivars; serialize `loadWithOptions:` calls externally if you
268- // share a `Module` across threads.
269- //
270- // TODO: remove this ivar once the C++ Module owns its LoadBackendOptionsMap
271- // by value (today it borrows a raw pointer). With owned options the ObjC
272- // wrapper has nothing to retain, the thread-safety caveat above goes
273- // away, and -loadMethod:options: / -loadWithOptions: stop needing a
274- // custom lifetime contract between the bindings and the C++ layer.
275- ExecuTorchBackendOptionsMap *_loadedBackendOptions;
276255}
277256
278257- (instancetype )initWithFilePath : (NSString *)filePath
@@ -354,25 +333,10 @@ - (BOOL)loadWithOptions:(ExecuTorchBackendOptionsMap *)options
354333 verification : (ExecuTorchVerification)verification
355334 error : (NSError **)error {
356335 NSParameterAssert (options);
357- // Retain the options object so the C++ borrowed pointer it contains stays
358- // valid for the lifetime of any methods loaded with these options.
359- // (Methods load lazily during forward(), so the borrow may outlive this
360- // call.) See ExecuTorchBackendOptionsMap.h for the lifetime contract.
361- //
362- // No rollback on failure: Module::load updates its backend_options_ raw
363- // pointer BEFORE attempting load_internal, so after a failed call the
364- // C++ side already references `options`. The ObjC retain therefore
365- // always matches what C++ points at, even on the failure path — a
366- // two-phase commit here would instead leave C++ pointing at a map the
367- // wrapper no longer retains. See:
368- // https://github.com/pytorch/executorch/blob/6412f55a54dd3ce1f4ed220a3e96ee19b8f37967/extension/module/module.cpp#L192-L197
369- //
370- // TODO: once Module::load is made transactional (i.e. it only commits
371- // `backend_options_` after load_internal succeeds), replace the
372- // unconditional assignment below with a proper two-phase commit that
373- // only overwrites _loadedBackendOptions on success. This removes the
374- // "match C++'s unconditional write" workaround documented above.
375- _loadedBackendOptions = options;
336+ // Module deep-copies the LoadBackendOptionsMap on the C++ side, so we
337+ // do not need to retain `options` past this call. ARC releases the
338+ // wrapper when the parameter goes out of scope and the Module's owned
339+ // copy keeps lazy load_method paths working.
376340 const auto errorCode = _module->load (*[options cppMap ],
377341 static_cast <Program::Verification>(verification));
378342 if (errorCode != Error::Ok) {
@@ -395,22 +359,10 @@ - (BOOL)loadMethod:(NSString *)methodName
395359 options : (ExecuTorchBackendOptionsMap *)options
396360 error : (NSError **)error {
397361 NSParameterAssert (options);
398- // Do NOT assign to _loadedBackendOptions here. Module::load_method
399- // consumes `backend_options` synchronously within this call — it is
400- // passed through to program_->load_method and is not cached on the
401- // Module. Only Module::load(backend_options, ...) stores the pointer
402- // (via backend_options_). ARC keeps `options` alive for the call
403- // duration via the parameter, so no ivar retention is needed here.
404- // See:
405- // load_method: https://github.com/pytorch/executorch/blob/6412f55a54dd3ce1f4ed220a3e96ee19b8f37967/extension/module/module.cpp#L353-L409
406- // load (stores backend_options_): https://github.com/pytorch/executorch/blob/6412f55a54dd3ce1f4ed220a3e96ee19b8f37967/extension/module/module.cpp#L195
407- //
408- // Overwriting _loadedBackendOptions would release any map previously
409- // installed by -loadWithOptions:, but the C++ Module's backend_options_
410- // raw pointer would still reference that released map's storage — a
411- // use-after-free on the next lazy load_method. The XCTest
412- // testMixedLoadWithOptionsAndLoadMethodWithOptionsOnMultiMethodModel
413- // pins this invariant via a weak reference.
362+ // load_method consumes `options` synchronously: the cppMap pointer is
363+ // passed through to program_->load_method and is not cached on the C++
364+ // Module. ARC keeps `options` alive for the duration of this call via
365+ // the parameter, so no extra retention is needed here.
414366 const auto errorCode = _module->load_method (methodName.UTF8String ,
415367 /* planned_memory=*/ nullptr ,
416368 /* event_tracer=*/ nullptr ,
0 commit comments