Skip to content
Merged
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
46 changes: 39 additions & 7 deletions backends/qualcomm/__init__.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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():
Comment on lines +58 to +62
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"
Expand All @@ -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.

Expand Down
13 changes: 0 additions & 13 deletions backends/qualcomm/builders/node_visitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 0 additions & 6 deletions backends/qualcomm/debugger/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 0 additions & 7 deletions backends/qualcomm/quantizer/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
70 changes: 63 additions & 7 deletions backends/qualcomm/tests/test_import_side_effects.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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")

Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down
6 changes: 0 additions & 6 deletions backends/qualcomm/utils/check_qnn_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
6 changes: 0 additions & 6 deletions backends/qualcomm/utils/qnn_manager_lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
6 changes: 0 additions & 6 deletions backends/qualcomm/utils/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading