Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,11 @@ Error QnnBackendUnifiedRegistry::GetOrCreateBackendBundle(
bundle->qnn_logger_ptr = std::move(logger);
bundle->qnn_backend_ptr = std::move(backend);
bundle->qnn_device_ptr = std::move(device);
qnn_backend_bundles_map_.emplace(
// insert_or_assign rather than emplace: an expired bundle leaves a dead
// weak_ptr under this key, and emplace will not replace it, so every later
// request would miss the cache and build another backend for the same type.
// CleanupExpired() cannot be used from here -- it takes mutex_, already held.
qnn_backend_bundles_map_.insert_or_assign(
backend_type, bundle); // Store weak_ptr to the bundle

return Error::Ok;
Expand Down
51 changes: 51 additions & 0 deletions backends/qualcomm/tests/test_passes.py
Original file line number Diff line number Diff line change
Expand Up @@ -795,6 +795,57 @@ def test_custom_op_resolves_supported_types(self):
_resolve_qnn_data_type(py_type, "arg", "my_ops.foo.default")
)

def test_backend_bundle_cache_survives_an_expired_entry(self):
"""A backend bundle that expires must not poison the cache.

QnnBackendUnifiedRegistry holds bundles by weak_ptr keyed on backend type.
The stale key left behind by an expired bundle was never replaced, so once
one bundle had come and gone every later request built a fresh backend --
even while another bundle for that type was alive.
"""
import os
import tempfile

import executorch.backends.qualcomm.python.PyQnnManagerAdaptor as PyQnnManager
from executorch.backends.qualcomm.partition.utils import (
generate_qnn_executorch_option,
)

option = generate_qnn_executorch_option(
generate_qnn_executorch_compiler_spec(
soc_model=QcomChipset.SM8650,
backend_options=generate_htp_compiler_spec(use_fp16=True),
)
)

# QNN logs from C++, so capture at the fd level.
with tempfile.TemporaryFile() as cap:
saved = os.dup(2)
os.dup2(cap.fileno(), 2)
try:
first = PyQnnManager.QnnManager(option)
if first.InitBackend().value != 0:
self.skipTest("QNN backend unavailable")
first.Destroy()
del first

kept = PyQnnManager.QnnManager(option)
kept.InitBackend()
later = PyQnnManager.QnnManager(option)
later.InitBackend()
finally:
os.dup2(saved, 2)
os.close(saved)
cap.seek(0)
log = cap.read().decode("utf-8", "replace")

created = log.count("Creating new backend bundle")
reused = log.count("Use cached backend bundle")
self.assertEqual(2, created, f"expected 2 backend creations, got {created}")
self.assertGreaterEqual(reused, 1, "third manager must reuse the live bundle")
kept.Destroy()
later.Destroy()


if __name__ == "__main__":
unittest.main()
Loading