From 12e84c123cfcfc647b8ace50cc8872e7cf472ffe Mon Sep 17 00:00:00 2001 From: PyTorch Bot Date: Mon, 31 Aug 2026 21:53:41 -0700 Subject: [PATCH] Stop the Qualcomm backend setup from running during an import ### The short version Importing the Qualcomm backend used to download a toolchain over the network and change global settings. That made the package impossible to import in some builds. This moves the setup off the import path. ### Why an import did real work Every Python package has an `__init__.py`, and Python runs it the moment you import anything from that package. In this backend it used to fetch the Qualcomm SDK (the vendor toolchain), set `QNN_SDK_ROOT` and `LD_LIBRARY_PATH`, and switch off a PyTorch CPU math library for the whole process. None of it is needed just to load the package. ### Two ways it broke **1. The helpers disappear.** Six modules had this at the top: ```python from executorch.backends.qualcomm import setup_qnn_sdk setup_qnn_sdk() ``` Some build systems list a package's files one by one. They can leave `__init__.py` out and put an empty file there instead, so everything defined in it is gone and all six fail: ``` ImportError: cannot import name 'setup_qnn_sdk' from 'executorch.backends.qualcomm' ``` **2. The whole backend disappears.** `__init__.py` also imported the downloader at the top: ```python from .scripts.download_qnn_sdk import install_qnn_sdk # needs backends/qualcomm/scripts/ ``` The same builds do not ship that `scripts` directory. Importing any submodule runs `__init__.py` first, so nothing in the backend could be imported at all: ``` ModuleNotFoundError: No module named 'executorch.backends.qualcomm.scripts' ``` ### The fix Remove the seven module level calls, and import the downloader only inside the function that installs an SDK. Whether a download is possible is now answered without it: ```python def _is_linux_x86() -> bool: return platform.system().lower() == "linux" and platform.machine().lower() in (...) ``` A Mac or an ARM machine returns straight away. If the downloader is missing on a machine that would have downloaded, the error now says to set `QNN_SDK_ROOT` instead of naming a missing module. One of the seven calls is not SDK setup: `node_visitor.py` also called `disable_mkldnn_on_amd()`, which avoids a crash on some AMD hosts. Nothing calls it after this change, so this and the follow-up are best landed together. ### Test plan `test_import_side_effects.py` passes, 12 tests. Three are new: the package imports with the `scripts` directory absent, setup returns on a platform with no published SDK, and a missing downloader names `QNN_SDK_ROOT`. All three fail if the top level import comes back. Checked against an installed wheel with `__init__.py` emptied and `scripts` removed, in all four combinations: 20 of 20 imports clean. Before this change the cases without `scripts` fail all five. `flake8` and `black` are clean. I do not have a Qualcomm device, so compiling a model end to end is not covered here. --- backends/qualcomm/__init__.py | 46 ++++++++++-- backends/qualcomm/builders/node_visitor.py | 13 ---- backends/qualcomm/debugger/utils.py | 6 -- backends/qualcomm/quantizer/validators.py | 7 -- .../tests/test_import_side_effects.py | 70 +++++++++++++++++-- backends/qualcomm/utils/check_qnn_version.py | 6 -- .../qualcomm/utils/qnn_manager_lifecycle.py | 6 -- backends/qualcomm/utils/utils.py | 6 -- 8 files changed, 102 insertions(+), 58 deletions(-) diff --git a/backends/qualcomm/__init__.py b/backends/qualcomm/__init__.py index 4b45dcdb67c..a37caed3e11 100644 --- a/backends/qualcomm/__init__.py +++ b/backends/qualcomm/__init__.py @@ -1,4 +1,5 @@ import os +import platform import threading # The Qualcomm SDK setup below is deferred rather than run here, so that importing this @@ -11,11 +12,6 @@ # Nothing about loading this package needs any of that. The native adaptor does not link # the SDK; it resolves QNN symbols with dlopen when a model is actually compiled, and says # so plainly when they are missing. So setup happens on the first call that needs it. -from .scripts.download_qnn_sdk import ( # noqa: F401 - install_qnn_sdk, - is_linux_x86, - QNN_ZIP_URL, -) _sdk_ready = False # Guards the flag above. Python's import lock does not, because the setup is now called from @@ -57,11 +53,15 @@ def _setup_qnn_sdk_locked() -> None: return # Downloading a prebuilt SDK is only possible for the platform it is published for. - if not is_linux_x86(): + # Decided here rather than by asking the downloader, so that a build which does not + # package the downloader still gets this far and returns. + if not _is_linux_x86(): _sdk_ready = True return - if not install_qnn_sdk(): + if not _install_qnn_sdk(): + from .scripts.download_qnn_sdk import QNN_ZIP_URL + raise RuntimeError( "Failed to set up QNN SDK.\n\n" "To resolve, try one of:\n" @@ -77,6 +77,38 @@ def _setup_qnn_sdk_locked() -> None: _sdk_ready = True +def _is_linux_x86() -> bool: + """True when a prebuilt Qualcomm SDK is published for this platform.""" + return platform.system().lower() == "linux" and platform.machine().lower() in ( + "x86_64", + "amd64", + "i386", + "i686", + ) + + +def _install_qnn_sdk() -> bool: + # Imported here rather than at module scope because the downloader lives in a sibling + # directory that some builds do not package, and it imports the network stack. Importing + # this package must not require either. + try: + from .scripts.download_qnn_sdk import install_qnn_sdk + except ModuleNotFoundError as error: + # Only when the downloader itself is absent. A dependency missing from inside it is a + # different problem and is left to speak for itself. + if not (error.name or "").startswith(f"{__name__}.scripts"): + raise + raise RuntimeError( + "This build cannot download a QNN SDK. Set QNN_SDK_ROOT to an existing " + "installation:\n" + " export QNN_SDK_ROOT=/path/to/qualcomm/sdk\n" + " export LD_LIBRARY_PATH=" + "$QNN_SDK_ROOT/lib/x86_64-linux-clang/:$LD_LIBRARY_PATH" + ) from error + + return install_qnn_sdk() + + def disable_mkldnn_on_amd() -> None: """Turn off PyTorch's MKLDNN backend on an AMD host. diff --git a/backends/qualcomm/builders/node_visitor.py b/backends/qualcomm/builders/node_visitor.py index 45620e90eca..29967fe799d 100644 --- a/backends/qualcomm/builders/node_visitor.py +++ b/backends/qualcomm/builders/node_visitor.py @@ -4,19 +4,6 @@ # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. -# The SDK has to be usable before a model is compiled, and every builder in this package -# imports this module, so this is where it is arranged. Not in the package's __init__, because -# that made simply importing the package fetch an SDK over the network and change a global -# PyTorch setting. -# -# The adaptor imported below does not itself need the SDK to load: it links no QNN library and -# resolves those symbols with dlopen when a backend is started. Placing the call here is about -# covering the compile paths, not about ordering against that import. -from executorch.backends.qualcomm import disable_mkldnn_on_amd, setup_qnn_sdk - -setup_qnn_sdk() -disable_mkldnn_on_amd() - from typing import Any, Dict, Optional, Tuple import executorch.backends.qualcomm.python.PyQnnManagerAdaptor as PyQnnManager diff --git a/backends/qualcomm/debugger/utils.py b/backends/qualcomm/debugger/utils.py index a84b9b33525..97a0de9c310 100644 --- a/backends/qualcomm/debugger/utils.py +++ b/backends/qualcomm/debugger/utils.py @@ -6,12 +6,6 @@ import tempfile from typing import Sequence, Tuple -# The SDK has to be usable before a model is compiled. See node_visitor.py for why this is -# here rather than in the package's __init__. -from executorch.backends.qualcomm import setup_qnn_sdk - -setup_qnn_sdk() - import executorch.backends.qualcomm.python.PyQnnManagerAdaptor as PyQnnManager import pandas as pd import torch diff --git a/backends/qualcomm/quantizer/validators.py b/backends/qualcomm/quantizer/validators.py index 8cbbe109247..866e8af89c4 100644 --- a/backends/qualcomm/quantizer/validators.py +++ b/backends/qualcomm/quantizer/validators.py @@ -8,13 +8,6 @@ from dataclasses import dataclass, field from typing import cast, Dict, List, Optional, Set, Tuple -# The SDK has to be usable before a model is compiled. See node_visitor.py for why this is -# here rather than in the package's __init__. Called from this module too because it does not -# import node_visitor before the adaptor, so it cannot leave the call to that module. -from executorch.backends.qualcomm import setup_qnn_sdk - -setup_qnn_sdk() - import executorch.backends.qualcomm.python.PyQnnManagerAdaptor as PyQnnManager import torch from executorch.backends.qualcomm.builders.node_visitor import ( diff --git a/backends/qualcomm/tests/test_import_side_effects.py b/backends/qualcomm/tests/test_import_side_effects.py index 20d3528afce..f5b08fddb9c 100644 --- a/backends/qualcomm/tests/test_import_side_effects.py +++ b/backends/qualcomm/tests/test_import_side_effects.py @@ -84,11 +84,67 @@ def test_importing_the_package_does_not_set_up_the_sdk(): ) +def test_the_package_imports_without_the_downloader(tmp_path, monkeypatch): + """The package must load in a build that does not ship the downloader directory. + + Some builds assemble a package from an explicit file list and leave the sibling `scripts` + directory out. A module level import of it then fails, and with it every module in the + backend, which is what this guards. + """ + package = tmp_path / "qnnpkg" + package.mkdir() + (package / "__init__.py").write_text(Path(qnn.__file__).read_text()) + monkeypatch.syspath_prepend(str(tmp_path)) + + module = importlib.import_module("qnnpkg") + + assert module.setup_qnn_sdk is not None + + +def test_platform_check_needs_no_downloader(tmp_path, monkeypatch): + """Setup must return on a platform with no prebuilt SDK, downloader present or not. + + The platform is decided locally for this reason. Asking the downloader would import it, so + a host that needs no download at all would fail on a build that does not ship it. + """ + package = tmp_path / "qnnpkg2" + package.mkdir() + (package / "__init__.py").write_text(Path(qnn.__file__).read_text()) + monkeypatch.syspath_prepend(str(tmp_path)) + monkeypatch.delenv("QNN_SDK_ROOT", raising=False) + monkeypatch.delenv("EXECUTORCH_BUILDING_WHEEL", raising=False) + + module = importlib.import_module("qnnpkg2") + monkeypatch.setattr(module, "_is_linux_x86", lambda: False) + + module.setup_qnn_sdk() + + +def test_a_missing_downloader_names_the_way_out(tmp_path, monkeypatch): + """On a platform that would download, an absent downloader has to say what to do instead. + + A bare ModuleNotFoundError names an internal packaging detail and leaves the reader nothing + to act on. + """ + package = tmp_path / "qnnpkg3" + package.mkdir() + (package / "__init__.py").write_text(Path(qnn.__file__).read_text()) + monkeypatch.syspath_prepend(str(tmp_path)) + monkeypatch.delenv("QNN_SDK_ROOT", raising=False) + monkeypatch.delenv("EXECUTORCH_BUILDING_WHEEL", raising=False) + + module = importlib.import_module("qnnpkg3") + monkeypatch.setattr(module, "_is_linux_x86", lambda: True) + + with pytest.raises(RuntimeError, match="QNN_SDK_ROOT"): + module.setup_qnn_sdk() + + def test_setup_is_idempotent(monkeypatch): """The compile paths each call it, so only the first call may do the work.""" calls = [] - monkeypatch.setattr(qnn, "install_qnn_sdk", lambda: calls.append(1) or True) - monkeypatch.setattr(qnn, "is_linux_x86", lambda: True) + monkeypatch.setattr(qnn, "_install_qnn_sdk", lambda: calls.append(1) or True) + monkeypatch.setattr(qnn, "_is_linux_x86", lambda: True) monkeypatch.setattr(qnn, "_sdk_ready", False) monkeypatch.delenv("QNN_SDK_ROOT", raising=False) monkeypatch.delenv("EXECUTORCH_BUILDING_WHEEL", raising=False) @@ -101,7 +157,7 @@ def test_setup_is_idempotent(monkeypatch): def test_setup_honours_a_preinstalled_sdk(monkeypatch): calls = [] - monkeypatch.setattr(qnn, "install_qnn_sdk", lambda: calls.append(1) or True) + monkeypatch.setattr(qnn, "_install_qnn_sdk", lambda: calls.append(1) or True) monkeypatch.setattr(qnn, "_sdk_ready", False) monkeypatch.setenv("QNN_SDK_ROOT", "/opt/qcom/sdk") @@ -129,8 +185,8 @@ def slow_install(): calls.append(1) return True - monkeypatch.setattr(qnn, "install_qnn_sdk", slow_install) - monkeypatch.setattr(qnn, "is_linux_x86", lambda: True) + monkeypatch.setattr(qnn, "_install_qnn_sdk", slow_install) + monkeypatch.setattr(qnn, "_is_linux_x86", lambda: True) monkeypatch.setattr(qnn, "_sdk_ready", False) monkeypatch.delenv("QNN_SDK_ROOT", raising=False) monkeypatch.delenv("EXECUTORCH_BUILDING_WHEEL", raising=False) @@ -149,8 +205,8 @@ def slow_install(): def test_setup_reports_a_failed_install(monkeypatch): """A failure has to name the two ways out, since it cannot be resolved automatically.""" - monkeypatch.setattr(qnn, "install_qnn_sdk", lambda: False) - monkeypatch.setattr(qnn, "is_linux_x86", lambda: True) + monkeypatch.setattr(qnn, "_install_qnn_sdk", lambda: False) + monkeypatch.setattr(qnn, "_is_linux_x86", lambda: True) monkeypatch.setattr(qnn, "_sdk_ready", False) monkeypatch.delenv("QNN_SDK_ROOT", raising=False) monkeypatch.delenv("EXECUTORCH_BUILDING_WHEEL", raising=False) diff --git a/backends/qualcomm/utils/check_qnn_version.py b/backends/qualcomm/utils/check_qnn_version.py index 02a49cf31b5..d38629a78b1 100644 --- a/backends/qualcomm/utils/check_qnn_version.py +++ b/backends/qualcomm/utils/check_qnn_version.py @@ -9,12 +9,6 @@ import platform import re -# The SDK has to be usable before a model is compiled. See node_visitor.py for why this is -# here rather than in the package's __init__. -from executorch.backends.qualcomm import setup_qnn_sdk - -setup_qnn_sdk() - import executorch.backends.qualcomm.python.PyQnnManagerAdaptor as PyQnnManagerAdaptor diff --git a/backends/qualcomm/utils/qnn_manager_lifecycle.py b/backends/qualcomm/utils/qnn_manager_lifecycle.py index 278f82ad9da..daee778d297 100644 --- a/backends/qualcomm/utils/qnn_manager_lifecycle.py +++ b/backends/qualcomm/utils/qnn_manager_lifecycle.py @@ -3,12 +3,6 @@ import threading from typing import Dict, List -# The SDK has to be usable before a model is compiled. See node_visitor.py for why this is -# here rather than in the package's __init__. -from executorch.backends.qualcomm import setup_qnn_sdk - -setup_qnn_sdk() - import executorch.backends.qualcomm.python.PyQnnManagerAdaptor as PyQnnManager from executorch.backends.qualcomm.partition.utils import generate_qnn_executorch_option from executorch.backends.qualcomm.serialization.qc_schema import ( diff --git a/backends/qualcomm/utils/utils.py b/backends/qualcomm/utils/utils.py index f7efa874c7c..32da32288fd 100644 --- a/backends/qualcomm/utils/utils.py +++ b/backends/qualcomm/utils/utils.py @@ -9,12 +9,6 @@ from enum import Enum from typing import Any, Callable, Dict, List, Optional, Tuple, Union -# The SDK has to be usable before a model is compiled. See node_visitor.py for why this is -# here rather than in the package's __init__. -from executorch.backends.qualcomm import setup_qnn_sdk - -setup_qnn_sdk() - import executorch.backends.qualcomm.python.PyQnnManagerAdaptor as PyQnnManagerAdaptor import executorch.exir as exir import torch