Skip to content

Commit 89e6f90

Browse files
authored
Qualcomm: stop an expired bundle from making the backend cache stale (pytorch#21944)
QnnBackendUnifiedRegistry caches backend bundles as weak_ptr keyed on backend type, and CleanupExpired() only runs in the registry destructor. When a bundle's last owner goes away, the dead weak_ptr stays in the map. The next request finds the entry, fails to lock it, builds a fresh bundle, and then calls emplace() — which is a no-op because the key already exists. The corpse is never replaced, so every subsequent lowering rebuilds the backend and device instead of sharing them. insert_or_assign replaces the expired entry. CleanupExpired() can't be used here because it takes mutex_, which is already held. Single-lowering processes never reach this, which is why it has been invisible. It shows up once a process does two or more lowerings: with the fix the second lowering reuses the cached bundle instead of creating a second one, which also avoids repeated device configuration. Test: test_backend_bundle_cache_survives_an_expired_entry asserts two creations plus at least one reuse, and fails on unmodified main. cc @cbilgin
1 parent 66a0fec commit 89e6f90

2 files changed

Lines changed: 57 additions & 1 deletion

File tree

‎backends/qualcomm/runtime/backends/QnnBackendUnifiedRegistry.cpp‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,11 @@ Error QnnBackendUnifiedRegistry::GetOrCreateBackendBundle(
167167
bundle->qnn_logger_ptr = std::move(logger);
168168
bundle->qnn_backend_ptr = std::move(backend);
169169
bundle->qnn_device_ptr = std::move(device);
170-
qnn_backend_bundles_map_.emplace(
170+
// insert_or_assign rather than emplace: an expired bundle leaves a dead
171+
// weak_ptr under this key, and emplace will not replace it, so every later
172+
// request would miss the cache and build another backend for the same type.
173+
// CleanupExpired() cannot be used from here -- it takes mutex_, already held.
174+
qnn_backend_bundles_map_.insert_or_assign(
171175
backend_type, bundle); // Store weak_ptr to the bundle
172176

173177
return Error::Ok;

‎backends/qualcomm/tests/test_passes.py‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -795,6 +795,58 @@ def test_custom_op_resolves_supported_types(self):
795795
_resolve_qnn_data_type(py_type, "arg", "my_ops.foo.default")
796796
)
797797

798+
def test_backend_bundle_cache_survives_an_expired_entry(self):
799+
"""A backend bundle that expires must not poison the cache.
800+
801+
QnnBackendUnifiedRegistry holds bundles by weak_ptr keyed on backend type.
802+
The stale key left behind by an expired bundle was never replaced, so once
803+
one bundle had come and gone every later request built a fresh backend --
804+
even while another bundle for that type was alive.
805+
"""
806+
import os
807+
import tempfile
808+
809+
import executorch.backends.qualcomm.python.PyQnnManagerAdaptor as PyQnnManager
810+
from executorch.backends.qualcomm.partition.utils import (
811+
generate_qnn_executorch_option,
812+
)
813+
814+
option = generate_qnn_executorch_option(
815+
generate_qnn_executorch_compiler_spec(
816+
soc_model=QcomChipset.SM8650,
817+
backend_options=generate_htp_compiler_spec(use_fp16=True),
818+
)
819+
)
820+
821+
# QNN logs from C++, so capture at the fd level.
822+
with tempfile.TemporaryFile() as cap:
823+
saved = os.dup(2)
824+
os.dup2(cap.fileno(), 2)
825+
try:
826+
first = PyQnnManager.QnnManager(option)
827+
if first.InitBackend().value != 0:
828+
self.skipTest("QNN backend unavailable")
829+
first.Destroy()
830+
del first
831+
832+
kept = PyQnnManager.QnnManager(option)
833+
self.addCleanup(kept.Destroy)
834+
self.assertEqual(0, kept.InitBackend().value)
835+
836+
later = PyQnnManager.QnnManager(option)
837+
self.addCleanup(later.Destroy)
838+
self.assertEqual(0, later.InitBackend().value)
839+
finally:
840+
os.dup2(saved, 2)
841+
os.close(saved)
842+
cap.seek(0)
843+
log = cap.read().decode("utf-8", "replace")
844+
845+
created = log.count("Creating new backend bundle")
846+
reused = log.count("Use cached backend bundle")
847+
self.assertEqual(2, created, f"expected 2 backend creations, got {created}")
848+
self.assertGreaterEqual(reused, 1, "third manager must reuse the live bundle")
849+
798850

799851
if __name__ == "__main__":
800852
unittest.main()

0 commit comments

Comments
 (0)