You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit d938d14
Browse filesBrowse the repository at this point in the historyBrowse files
Isolate cached delegates by Module lifetime (#24462)
Summary:
## TL;DR
Give independently loaded ExecuTorch `Module`s independent QNN manager lifetimes while preserving context sharing between methods within one module. Key the delegate cache by `(context signature, runtime allocator)`, count method references, and release complete manager state on final destruction or initialization failure.
## Context
`QnnExecuTorchBackend` is process-global and caches expensive `QnnManager` instances across methods. A manager is placement-allocated from a module's runtime allocator; methods in that module share its allocator, but independent modules can load identical `.pte` bytes and be destroyed separately. D122851825 hardens this backend contract without changing FCB export contents or artifact selection.
## Problem
A context signature alone also matched independent modules, allowing one module to destroy manager state still used by another. Calling `Destroy()` alone also omitted non-trivial manager member destruction, and competing initializers could publish or abandon duplicate candidates. A process-global HTP spill-fill handle did not represent the lifetime of its owning context.
```text
Before: process cache[signature] -> Manager M (allocator A)
Module A methods ----> M <---- Module B, same program bytes
release Module A ----> M destroyed
Module B executes ---> stale context
```
## Solution
Use a cache key containing the signature and allocator, with mutex-protected lookup, membership, publication and release. Each loaded method holds a reference; `try_emplace` preserves the first published manager and competing initializers destroy their unused candidate. A destructor-only ownership guard covers backend, context, DLC compilation and tensor allocation failures. On final release, erase cache state under the mutex and destroy the manager after unlocking. A missing forward entry is reported and its stale reverse entry removed safely.
```text
After: cache[(signature, allocator A)] -> Manager A -> A's methods
cache[(signature, allocator B)] -> Manager B -> B's methods
release A's last method -> unlock cache -> ~Manager A -> Destroy()
B's methods -> Manager B remains alive
```
Scope HTP spill-fill groups by allocator and configured buffer size. Thread the group handle explicitly through context initialization instead of using a process-global handle. Serialize initialization within each group, retain its first manager until all members and pending initializations are gone, and destroy member contexts before the owner. The registry holds weak group references, preventing an ownership cycle and allowing cleanup followed by a fresh group.
```text
weak registry[(allocator, spill-fill bytes)] -> Group
method entry -> member manager + Group
Group -> first manager (HTP owner)
last member/pending initializer releases Group
-> member context destroyed
-> first context destroyed
-> registry's weak reference expires
```
Keep the registered backend alive for the process lifetime so loaded modules cannot destroy QNN contexts through global cache teardown after allocators or the QNN library have finalized. Normal method destruction still releases manager state; the explicit defaulted backend destructor avoids conflicting empty-body formatting between local and OSS ClangFormat versions. Python lifecycle imports follow the ordering reported by OSS UFMT.
## Compatibility and evidence limits
Separate live modules allocate separate QNN contexts, which can increase HTP memory use for duplicate blobs. Methods within one module still share a manager. FCB weight sharing inside an artifact does not supply shared runtime ownership across independent modules. Concurrent execution through one module and execution racing destruction require client coordination.
Exported lifecycle tests cover both module destruction orders, multiple methods, cleanup/reload and sibling unload/reload on the HTP simulator, including multi-context mode with a nonzero spill-fill size. C++ tests check handle identity, allocator isolation, reference accounting, concurrent publication, stale-map cleanup, spill-fill ownership and process exit. Target SDK spill-fill registration still requires device verification; the recorded FCB AOT failure occurred before runtime module loading and does not establish FCB runtime compatibility.
Authored with AI assistance from Codex.
Differential Revision: D122851825
Signed-off-by: Ali Monfared <alimonfared@meta.com>
0 commit comments