From 7d4c42ffc37d1082bd36b09952e3aff02dfa7fd3 Mon Sep 17 00:00:00 2001 From: Siddartha Pothapragada Date: Wed, 19 Aug 2026 02:09:16 -0700 Subject: [PATCH] Qualcomm: stop an expired bundle from poisoning the backend cache QnnBackendUnifiedRegistry keeps backend bundles in an unordered_map>. When the last QnnManager for a type goes away the bundle expires, leaving a dead weak_ptr under that key. The insert on the create path used emplace, which does not replace an existing key, so the dead entry stayed put: every subsequent lookup failed to lock, built a new backend, and again failed to record it. From the first expiry onward the cache never hit, so a live bundle could sit unused while a second QnnBackend and QnnDevice were created for the same backend type. Reproduced with one compile spec: create a manager and destroy it, keep a second alive, then ask for a third. Before this change the third builds its own backend; after it reuses the second's bundle. CleanupExpired() is not usable from GetOrCreateBackendBundle -- it takes mutex_, which is already held there -- hence insert_or_assign. Authored with Claude Code. --- .../backends/QnnBackendUnifiedRegistry.cpp | 6 ++- backends/qualcomm/tests/test_passes.py | 51 +++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/backends/qualcomm/runtime/backends/QnnBackendUnifiedRegistry.cpp b/backends/qualcomm/runtime/backends/QnnBackendUnifiedRegistry.cpp index 0cc8bff02c4..3c31de6bbe3 100644 --- a/backends/qualcomm/runtime/backends/QnnBackendUnifiedRegistry.cpp +++ b/backends/qualcomm/runtime/backends/QnnBackendUnifiedRegistry.cpp @@ -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; diff --git a/backends/qualcomm/tests/test_passes.py b/backends/qualcomm/tests/test_passes.py index c87a6a06de3..e5c8094d333 100644 --- a/backends/qualcomm/tests/test_passes.py +++ b/backends/qualcomm/tests/test_passes.py @@ -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()