From 0447315a1fdd60b4bb720d1bf5f9a0307e47f8d2 Mon Sep 17 00:00:00 2001 From: Siddartha Pothapragada Date: Wed, 19 Aug 2026 00:31:09 -0700 Subject: [PATCH] Qualcomm: key the QnnManager registry on the compile options QnnManagerRegistry keyed managers on backend_type alone, so two specs for one backend resolved to the same manager -- the one built from whichever spec was seen first. Precision, VTCM size and SoC config all come from the options a manager was constructed with, so a graph meant to compile fp16 alongside a quantized one silently compiled at the first spec's precision instead. Mixed precision within a single program was not expressible. Keying on (backend_type, option) gives each distinct spec its own manager. Same-spec lookups still reuse one, so nothing extra is created in the common single-spec case. destroy_qnn_manager now drops every manager for a backend type rather than a single entry. Known remaining gap, not addressed here: preprocess_multimethod still takes list(compile_specs.values())[0][0], so that path applies one partition's spec to all of them even with distinct managers available. Authored with Claude Code. --- backends/qualcomm/tests/test_passes.py | 37 +++++++++++++++++++ .../qualcomm/utils/qnn_manager_lifecycle.py | 32 ++++++++++------ 2 files changed, 57 insertions(+), 12 deletions(-) diff --git a/backends/qualcomm/tests/test_passes.py b/backends/qualcomm/tests/test_passes.py index c87a6a06de3..9d1cddfaa3d 100644 --- a/backends/qualcomm/tests/test_passes.py +++ b/backends/qualcomm/tests/test_passes.py @@ -795,6 +795,43 @@ def test_custom_op_resolves_supported_types(self): _resolve_qnn_data_type(py_type, "arg", "my_ops.foo.default") ) + def test_manager_registry_separates_specs_by_precision(self): + """Two specs for one backend must not share a QnnManager. + + Precision comes from the options a manager was built with, so keying the + registry on backend_type alone silently compiled an fp16 partition and a + quantized one at whichever precision was seen first. + """ + from executorch.backends.qualcomm.utils.qnn_manager_lifecycle import ( + get_current_qnn_manager, + QnnManagerContext, + ) + + specs_fp16 = generate_qnn_executorch_compiler_spec( + soc_model=QcomChipset.SM8650, + backend_options=generate_htp_compiler_spec(use_fp16=True), + ) + specs_quant = generate_qnn_executorch_compiler_spec( + soc_model=QcomChipset.SM8650, + backend_options=generate_htp_compiler_spec(use_fp16=False), + ) + try: + with QnnManagerContext({"fp16": specs_fp16, "quant": specs_quant}): + htp = QnnExecuTorchBackendType.kHtpBackend + self.assertIsNot( + get_current_qnn_manager(htp, specs_fp16), + get_current_qnn_manager(htp, specs_quant), + ) + # Same spec must still be reused, not rebuilt. + self.assertIs( + get_current_qnn_manager(htp, specs_fp16), + get_current_qnn_manager(htp, specs_fp16), + ) + except RuntimeError as e: + if "QNN" in str(e) or "qnn" in str(e): + self.skipTest(f"QNN SDK not available: {e}") + raise + if __name__ == "__main__": unittest.main() diff --git a/backends/qualcomm/utils/qnn_manager_lifecycle.py b/backends/qualcomm/utils/qnn_manager_lifecycle.py index 6bffee0dfba..ca465b103a8 100644 --- a/backends/qualcomm/utils/qnn_manager_lifecycle.py +++ b/backends/qualcomm/utils/qnn_manager_lifecycle.py @@ -20,13 +20,17 @@ class QnnManagerRegistry: def __init__(self): - # Registry stores {backend_type: QnnManager instance} + # Registry stores {(backend_type, option): QnnManager instance}. Keying on + # the option bytes as well keeps two specs for one backend -- an fp16 + # partition and a quantized one, say -- from sharing a manager, since + # precision comes from the options the manager was built with. self._registry = {} def get_or_create_qnn_manager( self, backend_type: QnnExecuTorchBackendType, option: bytes ) -> PyQnnManager.QnnManager: - if backend_type not in self._registry: + key = (backend_type, option) + if key not in self._registry: qnn_manager = PyQnnManager.QnnManager(option) err = qnn_manager.InitBackend() if err.value != 0: @@ -35,13 +39,15 @@ def get_or_create_qnn_manager( "Ensure QNN SDK libraries are available " "(e.g. LD_LIBRARY_PATH includes $QNN_SDK_ROOT/lib/x86_64-linux-clang/)." ) - self._registry[backend_type] = qnn_manager - return self._registry[backend_type] + self._registry[key] = qnn_manager + return self._registry[key] def destroy_qnn_manager(self, backend_type: QnnExecuTorchBackendType): - if backend_type in self._registry: - self._registry[backend_type].Destroy() - del self._registry[backend_type] + keys = [k for k in self._registry if k[0] == backend_type] + if keys: + for k in keys: + self._registry[k].Destroy() + del self._registry[k] else: logging.warning( f"Attempted to destroy non-existent QnnManager for backend type {backend_type.name}" @@ -83,12 +89,14 @@ def get_current_qnn_manager( Return a new QnnManger if no QnnManager is active for the given backend_type in the current context. """ active_registry = getattr(_current_qnn_managers, "active_registry", None) - if active_registry is None or backend_type not in active_registry._registry: + option = generate_qnn_executorch_option(compile_specs) + if ( + active_registry is None + or (backend_type, option) not in active_registry._registry + ): logging.warning( f"No QnnManager active for backend type {backend_type.name} in the current QnnManagerContext. " "It would be better to use to_edge_transform_and_lower_to_qnn to lowering to QNN Backend." ) - return QnnManagerRegistry().get_or_create_qnn_manager( - backend_type, generate_qnn_executorch_option(compile_specs) - ) - return active_registry._registry[backend_type] + return QnnManagerRegistry().get_or_create_qnn_manager(backend_type, option) + return active_registry._registry[(backend_type, option)]