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 c44220f
Browse filesBrowse the repository at this point in the historyBrowse files
Isolate cached delegates by Module lifetime (#24462)
Summary:
Pull Request resolved: #24462
## 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. Full `QnnManager` destruction also releases its input/output tensor wrappers and other non-trivial members; their storage was not released by calling `Destroy()` alone. 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.
### QAI Hub shard spill-fill cost
`gen_pte_from_ctx_bin()` calls `update_spill_fill_size()` over the shard programs before writing separate `.pte` files. The helper sets each participating context's `max_sf_buf_size` to the largest requirement in the export group. The QAI Hub Llama runner constructs a separate `Module` for each shard and loads every shard method; default `Module` construction gives each one a separate runtime allocator.
On device, `HtpContextCustomConfig::CreateContextCustomConfig()` registers an HTP group using `firstGroupHandle` and `maxSpillFillBuffer`. The former process-global handle allowed shard contexts to join one group. With this diff, the `(allocator, max_sf_buf_size)` key isolates those independently allocated Modules, so each shard starts its own spill-fill group even when the exported size matches.
```text
N resident shard Modules, common configured S bytes (S > 0)
Before: shard 1 ... shard N -> one HTP spill-fill group -> S bytes
After: shard i / allocator i -> group i -> S bytes each
Expected configured reservation: S -> N * S bytes
Expected additional reservation: (N - 1) * S bytes
```
This is a projection of the spill-fill reservation for the described shard layout, not measured RSS, PSS, DSP heap usage or peak device memory. SDK allocation behavior, alignment and other context/model memory affect the observed totals. The existing host simulator tests do not issue target spill-fill group registration and cannot measure this device cost. No connected physical device was available for this follow-up, so the magnitude on a representative QAI Hub shard model remains unmeasured.
If device evidence shows a material cost, an opt-in cross-Module group ID is a possible follow-up. Such a group must keep its first context and allocator-owned manager storage alive until all shard Modules and pending initializations release it; changing the key to an ID alone would not satisfy that lifetime requirement. This diff does not introduce that opt-in.
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
0 commit comments