Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 50 additions & 8 deletions backends/qualcomm/__init__.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Comment on lines +94 to +95
# 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"
Expand Down
27 changes: 27 additions & 0 deletions backends/qualcomm/tests/test_import_side_effects.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = []
Expand Down
Loading