Skip to content

Commit 8de26ed

Browse files
Isolate cached delegates by Module lifetime (#24462)
Summary: The QNN backend caches a `QnnManager` so methods in one ExecuTorch `Module` can share a QNN context. A context signature alone also matched independent modules loading identical program bytes, even though the manager was placement-allocated from only one module's runtime allocator. Key the cache by both signature and allocator to give independent modules independent manager lifetimes. If a reverse-cache entry has no matching forward entry during destruction, report it and remove the stale reverse entry before returning. The regression deliberately creates this inconsistent state and verifies safe cleanup and a repeated release. Count each loaded method's reference to its cached manager and destroy the manager after its last method releases it. Protect cache operations with the mutex and use `try_emplace` to preserve the first manager published for a key. A competing initializer returns that manager and destroys its own candidate. A destructor-only ownership guard cleans up every initialization error path, including backend, context, DLC compilation, and tensor allocation failures. Scope HTP spill-fill groups by runtime allocator and configured buffer size. Each context receives its group handle explicitly, replacing the process-global static handle. Keep the first group manager alive until all group members and pending initializations are gone, and free each member before the first context. Serialize initialization within a group so concurrent loads select one first context. Separate live modules now allocate separate QNN contexts, which can increase HTP memory use when they load identical blobs. Sharing between methods within one module remains intact. Full `QnnManager` destruction also releases its tensor wrappers and other non-trivial members. FCB export contents and selection are unchanged; FCB weight sharing within an artifact does not provide shared runtime ownership across independent modules. Export the lifecycle tests so they are visible in the PR. They cover both module destruction orders, execution of multiple methods in one module, cleanup/reload, and method unload/reload on the HTP simulator. C++ tests verify shared cache handle identity, allocator isolation, reference accounting, concurrent cache publication, spill-fill group isolation, cleanup/reload, and owner destruction order. The simulator lifecycle suite also runs with multi-context enabled and a nonzero spill-fill size; target SDK registration still requires device verification. Concurrent execution through one module and execution racing destruction still require client coordination. Authored with AI assistance from Codex. Differential Revision: D122851825 Signed-off-by: Ali Monfared <alimonfared@meta.com>
1 parent bdc7c35 commit 8de26ed

11 files changed

Lines changed: 611 additions & 69 deletions

‎backends/qualcomm/runtime/QnnExecuTorchBackend.cpp‎

Lines changed: 124 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
#include <executorch/backends/qualcomm/runtime/backends/QnnCustomProtocol.h>
1515
#include <executorch/runtime/backend/interface.h>
1616
#include <executorch/runtime/backend/options.h>
17+
18+
#include <memory>
19+
1720
namespace executorch {
1821
namespace backends {
1922
namespace qnn {
@@ -65,36 +68,58 @@ Result<DelegateHandle*> QnnExecuTorchBackend::init(
6568
QNN_EXECUTORCH_LOG_WARN("unknown argument: %s", compile_spec.key);
6669
}
6770

68-
// Create QnnManager
6971
MemoryAllocator* runtime_allocator = context.get_runtime_allocator();
70-
QnnManager* qnn_manager = runtime_allocator->allocateInstance<QnnManager>();
71-
if (qnn_manager == nullptr) {
72-
return Error::MemoryAllocationFailed;
73-
}
74-
75-
// NOTE: Since we use placement new and since this type is not trivially
76-
// destructible, we must call the destructor manually in destroy().
77-
new (qnn_manager) QnnManager(qnn_executorch_options, qnn_context_blob);
78-
// TODO: this is a temporal solution for multi-graph support, will be
72+
// TODO: this is a temporary solution for multi-graph support, will be
7973
// removed once framework starts to accept runtime configuration
8074
// ---
8175
// check if current context binary has already been initialized
8276
// return cached one for reducing memory footprint
83-
84-
auto iter = delegate_map_.find(signature);
85-
if (iter != delegate_map_.end()) {
77+
const DelegateKey key{signature, runtime_allocator};
78+
if (auto cached_handle = get_cached_delegate(key)) {
8679
QNN_EXECUTORCH_LOG_INFO(
8780
"Use cached delegate handle for current method: %s",
8881
context.get_method_name());
89-
return iter->second;
82+
return *cached_handle;
83+
}
84+
85+
std::shared_ptr<SpillFillGroup> group;
86+
std::unique_lock<std::mutex> group_lock;
87+
auto* backend_options = qnn_executorch_options->backend_options();
88+
if (backend_options->backend_type() ==
89+
QnnExecuTorchBackendType::kHtpBackend) {
90+
auto* htp_options = backend_options->htp_options();
91+
if (htp_options->use_multi_contexts() &&
92+
htp_options->max_sf_buf_size() != 0) {
93+
group = get_spill_fill_group(
94+
{runtime_allocator, htp_options->max_sf_buf_size()});
95+
group_lock = std::unique_lock<std::mutex>(group->mutex);
96+
if (auto cached_handle = get_cached_delegate(key)) {
97+
return *cached_handle;
98+
}
99+
}
100+
}
101+
102+
// Create QnnManager
103+
QnnManager* qnn_manager = runtime_allocator->allocateInstance<QnnManager>();
104+
if (qnn_manager == nullptr) {
105+
return Error::MemoryAllocationFailed;
90106
}
91107

108+
// The runtime allocator owns the storage, so only invoke the destructor.
109+
new (qnn_manager) QnnManager(qnn_executorch_options, qnn_context_blob);
110+
auto destroy_manager = [](QnnManager* manager) { manager->~QnnManager(); };
111+
std::shared_ptr<QnnManager> manager_guard(qnn_manager, destroy_manager);
112+
92113
ET_CHECK_OR_RETURN_ERROR(
93114
qnn_manager->InitBackend() == Error::Ok,
94115
Internal,
95116
"Fail to initialize Qnn Manager");
96117
ET_CHECK_OR_RETURN_ERROR(
97-
qnn_manager->InitContext() == Error::Ok,
118+
qnn_manager->InitContext(
119+
std::nullopt,
120+
group && group->owner
121+
? static_cast<QnnManager*>(group->owner.get())->GetContextHandle()
122+
: nullptr) == Error::Ok,
98123
Internal,
99124
"Fail to initialize Qnn Manager");
100125

@@ -111,24 +136,26 @@ Result<DelegateHandle*> QnnExecuTorchBackend::init(
111136
"Fail to allocate tensor");
112137
}
113138
}
114-
add_cached_delegate(signature, qnn_manager);
139+
DelegateHandle* cached_handle =
140+
add_cached_delegate(key, qnn_manager, manager_guard, group);
141+
if (group && !group->owner && cached_handle == qnn_manager) {
142+
group->owner = manager_guard;
143+
}
115144

116145
#ifndef __hexagon__
117146
// This backend does not need its processed data after Init.
118147
processed->Free();
119148
#endif
120149

121-
return qnn_manager;
150+
return cached_handle;
122151
}
123152

124153
Error QnnExecuTorchBackend::execute(
125154
BackendExecutionContext& context,
126155
DelegateHandle* handle,
127156
Span<EValue*> args) const {
128157
ET_CHECK_OR_RETURN_ERROR(
129-
delegate_map_rev_.count(handle) != 0,
130-
Internal,
131-
"DelegateHandle has been deleted");
158+
is_cached_delegate(handle), Internal, "DelegateHandle has been deleted");
132159
QnnManager* qnn_manager = static_cast<QnnManager*>(handle);
133160

134161
std::string method_name = context.get_method_name();
@@ -225,11 +252,9 @@ Error QnnExecuTorchBackend::execute(
225252
}
226253

227254
void QnnExecuTorchBackend::destroy(DelegateHandle* handle) const {
228-
if (handle != nullptr && delegate_map_rev_.count(handle)) {
229-
QnnManager* qnn_manager = static_cast<QnnManager*>(handle);
230-
qnn_manager->Destroy();
231-
erase_cached_delegate(handle);
232-
}
255+
// Releasing the final shared owner invokes ~QnnManager(), which calls
256+
// Destroy(). The returned entry releases owners after mutex_ is unlocked.
257+
(void)erase_cached_delegate(handle);
233258
}
234259

235260
executorch::runtime::Error QnnExecuTorchBackend::set_option(
@@ -365,41 +390,98 @@ bool QnnExecuTorchBackend::is_available() const {
365390
return true;
366391
}
367392

368-
void QnnExecuTorchBackend::add_cached_delegate(
369-
const std::int64_t& signature,
393+
std::optional<executorch::runtime::DelegateHandle*>
394+
QnnExecuTorchBackend::get_cached_delegate(const DelegateKey& key) const {
395+
std::lock_guard<std::mutex> guard(mutex_);
396+
auto iter = delegate_map_.find(key);
397+
if (iter == delegate_map_.end()) {
398+
return std::nullopt;
399+
}
400+
++iter->second.users;
401+
return iter->second.handle;
402+
}
403+
404+
std::shared_ptr<QnnExecuTorchBackend::SpillFillGroup>
405+
QnnExecuTorchBackend::get_spill_fill_group(const SpillFillKey& key) const {
406+
std::lock_guard<std::mutex> guard(mutex_);
407+
for (auto iter = spill_fill_groups_.begin();
408+
iter != spill_fill_groups_.end();) {
409+
if (iter->second.expired()) {
410+
iter = spill_fill_groups_.erase(iter);
411+
} else {
412+
++iter;
413+
}
414+
}
415+
auto& weak_group = spill_fill_groups_[key];
416+
auto group = weak_group.lock();
417+
if (!group) {
418+
group = std::make_shared<SpillFillGroup>();
419+
weak_group = group;
420+
}
421+
return group;
422+
}
423+
424+
executorch::runtime::DelegateHandle* QnnExecuTorchBackend::add_cached_delegate(
425+
const DelegateKey& key,
426+
executorch::runtime::DelegateHandle* handle,
427+
std::shared_ptr<executorch::runtime::DelegateHandle> manager,
428+
std::shared_ptr<SpillFillGroup> group) const {
429+
std::lock_guard<std::mutex> guard(mutex_);
430+
auto [iter, inserted] = delegate_map_.try_emplace(
431+
key, DelegateEntry{handle, 1, std::move(group), std::move(manager)});
432+
if (inserted) {
433+
delegate_map_rev_.emplace(handle, key);
434+
} else {
435+
++iter->second.users;
436+
}
437+
return iter->second.handle;
438+
}
439+
440+
bool QnnExecuTorchBackend::is_cached_delegate(
370441
executorch::runtime::DelegateHandle* handle) const {
371442
std::lock_guard<std::mutex> guard(mutex_);
372-
delegate_map_[signature] = handle;
373-
delegate_map_rev_[handle] = signature;
443+
return delegate_map_rev_.count(handle) != 0;
374444
}
375445

376-
void QnnExecuTorchBackend::erase_cached_delegate(
446+
std::optional<QnnExecuTorchBackend::DelegateEntry>
447+
QnnExecuTorchBackend::erase_cached_delegate(
377448
executorch::runtime::DelegateHandle* handle) const {
378449
std::lock_guard<std::mutex> guard(mutex_);
379450
auto iter = delegate_map_rev_.find(handle);
380451
if (iter == delegate_map_rev_.end()) {
381-
return;
452+
return std::nullopt;
382453
}
383-
delegate_map_.erase(iter->second);
384-
delegate_map_rev_.erase(handle);
454+
auto delegate_iter = delegate_map_.find(iter->second);
455+
if (delegate_iter == delegate_map_.end()) {
456+
QNN_EXECUTORCH_LOG_ERROR("Cached delegate has no matching entry.");
457+
delegate_map_rev_.erase(iter);
458+
return std::nullopt;
459+
}
460+
if (--delegate_iter->second.users != 0) {
461+
return std::nullopt;
462+
}
463+
DelegateEntry entry = std::move(delegate_iter->second);
464+
delegate_map_.erase(delegate_iter);
465+
delegate_map_rev_.erase(iter);
466+
return entry;
385467
}
386468

387469
namespace {
470+
// Loaded modules may outlive static teardown. Their managers must be released
471+
// by destroy(), while their allocators and QNN libraries are still alive.
472+
const executorch::runtime::Backend& qnn_backend() {
473+
static const executorch::runtime::Backend backend{
474+
QNN_BACKEND, new QnnExecuTorchBackend()};
475+
return backend;
476+
}
388477
#if !defined(_MSC_VER)
389-
auto cls = QnnExecuTorchBackend();
390-
executorch::runtime::Backend backend{QNN_BACKEND, &cls};
391478
// On non-Windows platforms a single copy of executorch_core is shared between
392479
// the DLL and the exe, so the static initializer writes into the correct
393480
// registered_backends[] array.
394-
static auto success_with_compiler = register_backend(backend);
481+
const auto success_with_compiler = register_backend(qnn_backend());
395482
#endif
396483
} // namespace
397484

398-
#if defined(_MSC_VER)
399-
static QnnExecuTorchBackend cls_msvc;
400-
static executorch::runtime::Backend backend_msvc{QNN_BACKEND, &cls_msvc};
401-
#endif
402-
403485
} // namespace qnn
404486
} // namespace backends
405487
} // namespace executorch
@@ -417,7 +499,7 @@ void QnnExecuTorchBackendRegister(void* register_fn) {
417499
return;
418500
}
419501
executorch::runtime::Error err = reinterpret_cast<RegisterFn>(register_fn)(
420-
executorch::backends::qnn::backend_msvc);
502+
executorch::backends::qnn::qnn_backend());
421503
if (err != executorch::runtime::Error::Ok) {
422504
ET_LOG(
423505
Error,

‎backends/qualcomm/runtime/QnnExecuTorchBackend.h‎

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,12 @@
1212
#include <executorch/runtime/core/error.h>
1313
#include <executorch/runtime/core/evalue.h>
1414

15+
#include <map>
16+
#include <memory>
1517
#include <mutex>
18+
#include <optional>
1619
#include <unordered_map>
20+
#include <utility>
1721

1822
namespace executorch {
1923
namespace backends {
@@ -22,7 +26,7 @@ namespace qnn {
2226
class QnnExecuTorchBackend final
2327
: public ::executorch::runtime::BackendInterface {
2428
public:
25-
~QnnExecuTorchBackend(){};
29+
~QnnExecuTorchBackend() {};
2630

2731
executorch::runtime::Result<executorch::runtime::DelegateHandle*> init(
2832
executorch::runtime::BackendInitContext& context,
@@ -51,16 +55,49 @@ class QnnExecuTorchBackend final
5155
bool is_available() const override;
5256

5357
private:
54-
void add_cached_delegate(
55-
const std::int64_t& signature,
58+
friend class QnnExecuTorchBackendTest;
59+
60+
// A context binary signature plus the runtime allocator that the methods of
61+
// one Module share. Other Modules can be destroyed independently, so they
62+
// must never receive this Module's QnnManager.
63+
using DelegateKey =
64+
std::pair<std::int64_t, const executorch::runtime::MemoryAllocator*>;
65+
66+
struct SpillFillGroup {
67+
std::mutex mutex;
68+
// HTP requires the first context to outlive every other group member.
69+
std::shared_ptr<executorch::runtime::DelegateHandle> owner;
70+
};
71+
using SpillFillKey =
72+
std::pair<const executorch::runtime::MemoryAllocator*, uint64_t>;
73+
74+
struct DelegateEntry {
75+
executorch::runtime::DelegateHandle* handle;
76+
size_t users;
77+
// Destroy each member context before releasing the group's first context.
78+
std::shared_ptr<SpillFillGroup> group;
79+
std::shared_ptr<executorch::runtime::DelegateHandle> manager;
80+
};
81+
82+
std::shared_ptr<SpillFillGroup> get_spill_fill_group(
83+
const SpillFillKey& key) const;
84+
std::optional<executorch::runtime::DelegateHandle*> get_cached_delegate(
85+
const DelegateKey& key) const;
86+
executorch::runtime::DelegateHandle* add_cached_delegate(
87+
const DelegateKey& key,
88+
executorch::runtime::DelegateHandle* handle,
89+
std::shared_ptr<executorch::runtime::DelegateHandle> manager = nullptr,
90+
std::shared_ptr<SpillFillGroup> group = nullptr) const;
91+
bool is_cached_delegate(executorch::runtime::DelegateHandle* handle) const;
92+
std::optional<DelegateEntry> erase_cached_delegate(
5693
executorch::runtime::DelegateHandle* handle) const;
57-
void erase_cached_delegate(executorch::runtime::DelegateHandle* handle) const;
5894

5995
mutable std::mutex mutex_;
6096
mutable std::mutex runtime_option_mutex_;
61-
mutable std::unordered_map<int64_t, executorch::runtime::DelegateHandle*>
62-
delegate_map_;
63-
mutable std::unordered_map<executorch::runtime::DelegateHandle*, std::int64_t>
97+
mutable std::map<DelegateKey, DelegateEntry> delegate_map_;
98+
mutable std::map<SpillFillKey, std::weak_ptr<SpillFillGroup>>
99+
spill_fill_groups_;
100+
mutable std::unordered_map<executorch::runtime::DelegateHandle*, DelegateKey>
64101
delegate_map_rev_;
65102

66103
RuntimeOption qnn_runtime_log_level_{false, 0};

‎backends/qualcomm/runtime/QnnManager.cpp‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -342,7 +342,8 @@ Error QnnManager::InitBackend() {
342342
}
343343

344344
Error QnnManager::InitContext(
345-
std::optional<std::vector<std::string>> graph_names) {
345+
const std::optional<std::vector<std::string>>& graph_names,
346+
Qnn_ContextHandle_t spill_fill_handle) {
346347
if (backend_params_ptr_->backend_init_state_ ==
347348
BackendInitializeState::UNINITIALIZED) {
348349
QNN_EXECUTORCH_LOG_INFO(
@@ -356,7 +357,8 @@ Error QnnManager::InitContext(
356357
backend_bundle_ptr_->qnn_device_ptr.get(),
357358
qnn_context_blob_,
358359
options_,
359-
qnn_dlc_manager_.get());
360+
qnn_dlc_manager_.get(),
361+
spill_fill_handle);
360362
ET_CHECK_OR_RETURN_ERROR(
361363
backend_params_ptr_ != nullptr,
362364
Internal,

‎backends/qualcomm/runtime/QnnManager.h‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,12 @@ class QnnManager {
3838
// provided graph_names. Note: For online_prepare or deserialization, the
3939
// graph name will be obtained from the binary.
4040
executorch::runtime::Error InitContext(
41-
std::optional<std::vector<std::string>> graph_names = std::nullopt);
41+
const std::optional<std::vector<std::string>>& graph_names = std::nullopt,
42+
Qnn_ContextHandle_t spill_fill_handle = nullptr);
43+
44+
Qnn_ContextHandle_t GetContextHandle() const {
45+
return backend_params_ptr_->qnn_context_ptr_->GetHandle();
46+
}
4247
// This function only initialize the context cache to get spill fill buffer
4348
// size
4449
executorch::runtime::Error InitContextCache();

‎backends/qualcomm/runtime/backends/QnnBackendFactory.cpp‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ std::unique_ptr<BackendConfigParameters> QnnBackendFactory::Create(
2020
QnnDevice* qnn_device_ptr,
2121
const QnnExecuTorchContextBinary& qnn_context_blob,
2222
const QnnExecuTorchOptions* options,
23-
QnnDlcManager* qnn_dlc_manager) {
23+
QnnDlcManager* qnn_dlc_manager,
24+
Qnn_ContextHandle_t spill_fill_handle) {
2425
auto backend_params = std::make_unique<BackendConfigParameters>();
2526

2627
switch (options->backend_options()->backend_type()) {
@@ -79,7 +80,8 @@ std::unique_ptr<BackendConfigParameters> QnnBackendFactory::Create(
7980
htp_options,
8081
options->target_options(),
8182
qnn_dlc_manager,
82-
get_option(options->profile_level(), QNN_RUNTIME_PROFILE_LEVEL));
83+
get_option(options->profile_level(), QNN_RUNTIME_PROFILE_LEVEL),
84+
spill_fill_handle);
8385

8486
backend_params->qnn_graph_ptr_ = std::make_unique<HtpGraph>(
8587
implementation_ptr,

‎backends/qualcomm/runtime/backends/QnnBackendFactory.h‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,8 @@ class QnnBackendFactory {
6868
QnnDevice* qnn_device_ptr,
6969
const QnnExecuTorchContextBinary& qnn_context_blob,
7070
const QnnExecuTorchOptions* options,
71-
QnnDlcManager* qnn_dlc_manager);
71+
QnnDlcManager* qnn_dlc_manager,
72+
Qnn_ContextHandle_t spill_fill_handle = nullptr);
7273
};
7374
} // namespace qnn
7475
} // namespace backends

0 commit comments

Comments
 (0)