Skip to content

fix: skip unavailable delegates and free delegates on model destruction - #197

Open
jslok wants to merge 1 commit into
margelo:mainfrom
jslok:fix/delegate-lifecycle
Open

jslok wants to merge 1 commit into
margelo:mainfrom
jslok:fix/delegate-lifecycle

Conversation

@jslok

@jslok jslok commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Two delegate-lifecycle bugs in createModel, rebased onto the RAII ownership from #205.

Delegate factories can legitimately return nullptr (e.g. TfLiteCoreMlDelegateCreate on devices without a Neural Engine, since the default enabled_devices is ANE-only), and that null went straight into TfLiteInterpreterOptionsAddDelegate. Null delegates are now skipped so the model falls back to CPU, and getDelegates() reports only what was actually registered.

TFLite's C API also doesn't transfer delegate ownership to the interpreter, and nothing ever freed them, so every model destruction leaked the delegate's compiled kernels / driver contexts. Each delegate now lives in a unique_ptr with its own delete function, and ownership moves into the interpreter's shared_ptr deleter, so delegates are freed right after TfLiteInterpreterDelete (they must outlive the interpreter) and on every createModel failure path by plain unwinding. HybridTfliteModel itself is untouched.

We run the pre-#205 form of this as a patch-package fix in production (Android GPU + CoreML), where model load/release cycles no longer accumulate native memory in heapprofd. The rebased version is compile-checked against the NDK; iOS not rebuilt yet.

Two related delegate-lifecycle fixes in createModel:

1. Skip null delegates: delegate factories can legitimately return nullptr
   (e.g. TfLiteCoreMlDelegateCreate on devices without a Neural Engine when
   enabled_devices is ANE-only). Registering that nullptr with
   TfLiteInterpreterOptionsAddDelegate crashes/corrupts the interpreter.
   Now a null delegate is skipped so the model falls back to CPU, and
   getDelegates() reports only the delegates that were actually registered.

2. Free delegates: TFLite's C API does not transfer delegate ownership to
   the interpreter - the caller must delete delegates itself after the
   interpreter is destroyed. They were never freed, so every model
   destruction leaked the delegate's compiled kernels / driver contexts
   (GPU: TfLiteGpuDelegateV2Delete, NNAPI: TfLiteNnapiDelegateDelete,
   CoreML: TfLiteCoreMlDelegateDelete). Each delegate is now held in a
   unique_ptr with its own delete function; ownership moves into the
   interpreter's shared_ptr deleter, so delegates are freed right after
   TfLiteInterpreterDelete (they must outlive the interpreter) and on
   every failure path in createModel via normal unwinding.
@jslok
jslok force-pushed the fix/delegate-lifecycle branch from ad4fec0 to 68dfaf0 Compare September 14, 2026 05:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant