diff --git a/backends/qualcomm/__init__.py b/backends/qualcomm/__init__.py index 4b45dcdb67c..5b2734c8b83 100644 --- a/backends/qualcomm/__init__.py +++ b/backends/qualcomm/__init__.py @@ -1,4 +1,6 @@ import os +import platform +import sys import threading # The Qualcomm SDK setup below is deferred rather than run here, so that importing this @@ -11,11 +13,33 @@ # 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, -) + +# Resolved on first use rather than at import, because the downloader lives in a sibling +# directory that some builds do not package, and importing this package must not require it. +# A module level __getattr__ keeps these as ordinary attributes, so callers and tests can still +# reach and replace them. +_LAZY_NAMES = ("install_qnn_sdk", "QNN_ZIP_URL") + + +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 __getattr__(name): + if name in _LAZY_NAMES: + from .scripts.download_qnn_sdk import install_qnn_sdk, QNN_ZIP_URL + + globals()["install_qnn_sdk"] = install_qnn_sdk + globals()["QNN_ZIP_URL"] = QNN_ZIP_URL + return globals()[name] + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + _sdk_ready = False # Guards the flag above. Python's import lock does not, because the setup is now called from @@ -57,16 +81,34 @@ 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(): + if not sys.modules[__name__].is_linux_x86(): _sdk_ready = True return - if not install_qnn_sdk(): + module = sys.modules[__name__] + try: + installed = module.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 + # This build does not carry the downloader, so an SDK cannot be fetched here. Say what to + # do rather than surfacing a missing module from a packaging detail. + 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 + + if not installed: raise RuntimeError( "Failed to set up QNN SDK.\n\n" "To resolve, try one of:\n" " 1. Download the SDK manually from:\n" - f" {QNN_ZIP_URL}\n" + f" {module.QNN_ZIP_URL}\n" " Or go to step 2 if QNN SDK already exists.\n" " 2. Set QNN_SDK_ROOT to an existing SDK installation:\n" " export QNN_SDK_ROOT=/path/to/qualcomm/sdk\n" diff --git a/backends/qualcomm/tests/test_import_side_effects.py b/backends/qualcomm/tests/test_import_side_effects.py index 20d3528afce..01814698ea4 100644 --- a/backends/qualcomm/tests/test_import_side_effects.py +++ b/backends/qualcomm/tests/test_import_side_effects.py @@ -84,6 +84,33 @@ def test_importing_the_package_does_not_set_up_the_sdk(): ) +def test_importing_the_package_does_not_need_the_downloader(monkeypatch): + """Importing must not require the sibling scripts directory. + + Some builds package this backend without it, and a module level import of the downloader + made every module that calls setup_qnn_sdk fail to import there with + ModuleNotFoundError. Nothing about loading the package needs it: it is only used to + download an SDK, which is one branch of setup. + """ + monkeypatch.setitem( + sys.modules, "executorch.backends.qualcomm.scripts.download_qnn_sdk", None + ) + monkeypatch.setitem(sys.modules, "executorch.backends.qualcomm.scripts", None) + + importlib.reload(qnn) + + # The case that actually broke: a Linux x86 host with a preinstalled SDK, where setup runs + # its early return. That must not touch the downloader. + monkeypatch.setattr(qnn, "is_linux_x86", lambda: True) + monkeypatch.setattr(qnn, "_sdk_ready", False) + monkeypatch.setenv("QNN_SDK_ROOT", "/opt/qcom/sdk") + monkeypatch.delenv("EXECUTORCH_BUILDING_WHEEL", raising=False) + + qnn.setup_qnn_sdk() + + assert qnn.disable_mkldnn_on_amd is not None + + def test_setup_is_idempotent(monkeypatch): """The compile paths each call it, so only the first call may do the work.""" calls = []