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
140 changes: 10 additions & 130 deletions backends/qualcomm/__init__.py
Original file line number Diff line number Diff line change
@@ -1,131 +1,11 @@
import os
import platform
import threading

# The Qualcomm SDK setup below is deferred rather than run here, so that importing this
# package has no side effects. It used to run at import time, which meant that merely
# importing the package downloaded the SDK and libc++ over the network, re-executed the
# interpreter under a staged loader, rewrote QNN_SDK_ROOT and LD_LIBRARY_PATH, and raised
# when a machine had no network. It also disabled PyTorch's MKLDNN backend for the whole
# process on any AMD host, which affects every other model in that interpreter.
# Copyright (c) Qualcomm Innovation Center, Inc.
# All rights reserved
#
# 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.

_sdk_ready = False
# Guards the flag above. Python's import lock does not, because the setup is now called from
# several modules rather than from one package __init__, and two threads importing two of them
# concurrently would otherwise both run an installer that downloads and rewrites the
# environment.
_sdk_lock = threading.Lock()


def setup_qnn_sdk() -> None:
"""Make the Qualcomm SDK usable in this process, once.

Safe to call repeatedly and from more than one thread: the work happens on the first call and
later calls return immediately. Called by the code paths that need the SDK, so a caller does
not have to.
"""
if _sdk_ready:
return
with _sdk_lock:
# Another thread may have finished while this one waited.
if _sdk_ready:
return
_setup_qnn_sdk_locked()


def _setup_qnn_sdk_locked() -> None:
global _sdk_ready

# The wheel build imports this package to collect its files, and has no use for an SDK.
if os.getenv("EXECUTORCH_BUILDING_WHEEL", "0").lower() in ("1", "true", "yes"):
_sdk_ready = True
return

# A preinstalled SDK is used as it is.
qnn_sdk_root = os.getenv("QNN_SDK_ROOT", None)
if qnn_sdk_root:
print(f"[QNN] Using QNN SDK at {qnn_sdk_root} (from QNN_SDK_ROOT)", flush=True)
_sdk_ready = True
return

# Downloading a prebuilt SDK is only possible for the platform it is published for.
# 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():
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"
" 1. Download the SDK manually from:\n"
f" {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"
" export LD_LIBRARY_PATH="
"$QNN_SDK_ROOT/lib/x86_64-linux-clang/:$LD_LIBRARY_PATH"
)

_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.

MKLDNN crashes on some AMD hosts, which is why this exists. The original comment described
it as producing wrong results; what was measured is a core dump, from a plain convolution
with nothing from this backend involved.

This changes a global PyTorch setting, so it is applied by the QNN compile paths rather than
at import, where it would also change how unrelated models run in the same interpreter.
"""
import torch

try:
import cpuinfo
except ImportError:
raise ImportError("Please install the cpuinfo with pip install py-cpuinfo.")

vendor = cpuinfo.get_cpu_info().get("vendor_id_raw", "") or ""
if "amd" in vendor.lower():
torch.backends.mkldnn.enabled = False
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree.

# Deliberately empty. Importing this package must have no side effects, and it must not be the
# home of anything a submodule needs, because build systems that assemble a package from a file
# list can leave this file out and synthesize an empty one in its place. The Qualcomm SDK setup
# that used to live here is in utils/qnn_sdk_setup.py, called by the code paths that start a
# backend.
15 changes: 13 additions & 2 deletions backends/qualcomm/debugger/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import pandas as pd
import torch
from executorch.backends.qualcomm.serialization.qc_schema import QcomChipset
from executorch.backends.qualcomm.utils.qnn_sdk_setup import setup_qnn_sdk
from executorch.backends.qualcomm.utils.utils import dump_context_from_pte
from graphviz import Digraph

Expand Down Expand Up @@ -199,10 +200,20 @@ def __init__(
build_folder,
workspace="/data/local/tmp/qnn_executorch_test",
):
# Makes the SDK usable first, because this tool runs a binary from inside it. Setup used to
# happen while this module was imported, which set the variable read below as a side effect.
setup_qnn_sdk()

self.qnn_sdk = os.environ.get("QNN_SDK_ROOT", None)
self.ndk = os.environ.get("ANDROID_NDK_ROOT", None)
assert self.qnn_sdk, "QNN_SDK_ROOT was not found in environment variable"
assert self.ndk, "ANDROID_NDK_ROOT was not found in environment variable"
# Raised rather than asserted, because an assert is stripped under `python -O` and both
# values are used to build real paths a few lines below.
if not self.qnn_sdk:
raise EnvironmentError("QNN_SDK_ROOT was not found in environment variable")
if not self.ndk:
raise EnvironmentError(
"ANDROID_NDK_ROOT was not found in environment variable"
)

self.tmp_dir = tmp_dir
self.workspace = workspace
Expand Down
30 changes: 26 additions & 4 deletions backends/qualcomm/export_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@
HEXAGON_SDK_ROOT,
HEXAGON_TOOLS_ROOT,
)
from executorch.backends.qualcomm.utils.qnn_sdk_setup import (
disable_mkldnn_on_amd,
setup_qnn_sdk,
)
from executorch.backends.qualcomm.utils.utils import (
generate_gpu_compiler_spec,
generate_htp_compiler_spec,
Expand Down Expand Up @@ -129,9 +133,14 @@ def __post_init__(self):
assert not (
self.compile_only and self.pre_gen_pte
), "Cannot set both compile_only and pre_gen_pte as true"
assert (
"QNN_SDK_ROOT" in os.environ
), "Environment variable QNN_SDK_ROOT must be set."
# Every example script builds this config before doing anything else, and the check
# below needs the SDK path. It used to be set as a side effect of importing the backend.
setup_qnn_sdk()
# A usable path, not merely the key being present, because everything below builds real
# paths from it. Raised rather than asserted: an assert is stripped under `python -O`,
Comment on lines +136 to +140
# which would turn this into a confusing failure much later.
if not os.environ.get("QNN_SDK_ROOT"):
raise EnvironmentError("Environment variable QNN_SDK_ROOT must be set.")
if (not self.compile_only and not self.enable_x86_64) and self.device is None:
raise RuntimeError(
"device serial is required if not compile only or run on x86 emulator. Please specify a device serial."
Expand All @@ -146,9 +155,15 @@ def __post_init__(self):
elif get_soc_to_lpai_hw_ver_map()[
self.soc_model
] == LpaiHardwareVersion.V6 and is_qnn_sdk_version_less_than("2.39"):
# Read once, because building this message by querying again raises when there is
# no SDK, which replaces the useful error below with a confusing one.
try:
current = get_sdk_build_id()
except Exception:
current = "unknown, no usable SDK found"
raise RuntimeError(
f"Target soc_model({self.soc_model}) with LPAI backend v6 requires QNN SDK version >= 2.39. \n"
f"Current QNN SDK version: {get_sdk_build_id()}"
f"Current QNN SDK version: {current}"
)
if self.seed:
torch.manual_seed(self.seed)
Expand Down Expand Up @@ -539,6 +554,13 @@ def build_executorch_binary(
)
return

# Both applied here rather than deeper in the lowering. The quantized path below runs the
# model once per calibration sample, which is a real eager run and so needs the AMD guard
# first, and the SDK setup can re-execute the interpreter, which must not happen after a
# model has been traced and calibrated.
setup_qnn_sdk()
disable_mkldnn_on_amd()

sample_input = dataset[0]
if (
qnn_config.backend == QnnExecuTorchBackendType.kGpuBackend
Expand Down
68 changes: 57 additions & 11 deletions backends/qualcomm/quantizer/backend_opinfo_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,39 @@ def add_qnn_python_path():
backend_opinfo = None
_HAS_BACKEND_OPINFO = False

_WARNED_ABOUT_FALLBACK = False


def _load_backend_opinfo() -> bool:
"""Looks for the SDK's op info again, once the SDK has been set up.

The attempt above runs while this module is imported, which can be before anything has made
the SDK usable. Deciding only there would cache the no-op fallback for the rest of the
process, silently dropping every quantization constraint check.
"""
global backend_opinfo, _HAS_BACKEND_OPINFO
if _HAS_BACKEND_OPINFO:
return True

from executorch.backends.qualcomm.utils.qnn_sdk_setup import setup_qnn_sdk

try:
# Inside the try, because a failure here means the same thing as an SDK without this API:
# the op info is unavailable. Letting it escape would make building a quantizer raise on
# any host with no usable SDK, where it used to fall back.
setup_qnn_sdk()
add_qnn_python_path()
from qti.aisw.converters.common import backend_opinfo as loaded
except Exception:
# Logged rather than dropped, because the fallback below is silent about which of the
# several possible causes applied: no SDK, an SDK without this API, or a real error.
logging.debug("The QNN op info could not be loaded", exc_info=True)
return False

backend_opinfo = loaded
_HAS_BACKEND_OPINFO = True
return True


class _NoOpBackendOpInfo:
def __init__(self, *args, **kwargs):
Expand All @@ -57,21 +90,18 @@ def get_op_info(self, op_name: str):
return []


class _NoOpNamespace:
HTP = 1
LPAI = 3
BackendOpInfo = _NoOpBackendOpInfo


if not _HAS_BACKEND_OPINFO:
def _warn_once_about_the_fallback() -> None:
global _WARNED_ABOUT_FALLBACK
if _WARNED_ABOUT_FALLBACK:
return
_WARNED_ABOUT_FALLBACK = True
logging.warning(
"The backend_opinfo module couldn't be imported, so the abstract implementation will be used instead. This might be because $QNN_SDK_ROOT/lib/python isn't included in your PYTHONPATH, or the `BackendOpInfo` API isn't available in your QNN SDK version. Note that the `BackendOpInfo` API is supported starting from QNN SDK 2.41 and above."
)
backend_opinfo = _NoOpNamespace()


@lru_cache()
def get_backend_opinfo(backend: str, soc_model: QcomChipset):
def _get_backend_opinfo_cached(backend: str, soc_model: QcomChipset):
backend_type = getattr(backend_opinfo, backend.upper())
# For qnn 2.41, it only supports HTP backend
# It will support LPAI backend as soon as possible.
Expand All @@ -80,12 +110,28 @@ def get_backend_opinfo(backend: str, soc_model: QcomChipset):
try:
return backend_opinfo.BackendOpInfo(backend_type, soc_model)
except Exception:
print(
f"The 'BackendOpInfo' APIs may not be available for this backend {backend}."
logging.warning(
"The 'BackendOpInfo' APIs may not be available for this backend %s.",
backend,
)
return _NoOpBackendOpInfo()


def get_backend_opinfo(backend: str, soc_model: QcomChipset):
# The SDK is looked for again here rather than only while this module was imported, because
# that ran before anything had made the SDK usable.
#
# Deliberately not cached itself, so a later successful setup can still recover the real
# constraint checks. The inner function is cached, and it does return the do-nothing checker
# for a backend the SDK has no op info for, which is a fixed fact about that backend rather
# than something a later setup changes.
if not _load_backend_opinfo():
_warn_once_about_the_fallback()
return _NoOpBackendOpInfo()

return _get_backend_opinfo_cached(backend, soc_model)


# Helper functions for normalizing OpInfo objects (moved from backend_opinfo_adapter)
def _normalize_datatype_info(datatype_info: Any) -> PortDatatypeConstraints:
"""
Expand Down
6 changes: 6 additions & 0 deletions backends/qualcomm/quantizer/quantizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
QnnExecuTorchBackendType,
)
from executorch.backends.qualcomm.utils.constants import QCOM_QUANT_ANNOTATION_KEY
from executorch.backends.qualcomm.utils.qnn_sdk_setup import disable_mkldnn_on_amd
from torch._ops import OpOverload

from torch.fx import GraphModule
Expand Down Expand Up @@ -390,6 +391,11 @@ def __init__(
self.supported_ops: Set[OpOverload] = set(self._rules_map.keys())
self.quant_ops: Set[OpOverload] = self.supported_ops.copy()

# Applied when the quantizer is built, because that is upstream of calibration, and
# calibration runs the model eagerly. The AMD crash this prevents needs a real
# convolution, so a guard applied at lowering time comes after the risk has passed.
disable_mkldnn_on_amd()

# Load backend_opinfo of current backend and soc_model
self.backend_opinfo = get_backend_opinfo(str(backend), soc_model)

Expand Down
10 changes: 10 additions & 0 deletions backends/qualcomm/recipes/qnn_recipe_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@
QcomChipset,
QnnExecuTorchBackendType,
)
from executorch.backends.qualcomm.utils.qnn_sdk_setup import (
disable_mkldnn_on_amd,
setup_qnn_sdk,
)
from executorch.backends.qualcomm.utils.utils import (
generate_htp_compiler_spec,
generate_qnn_executorch_compiler_spec,
Expand Down Expand Up @@ -53,6 +57,12 @@ def create_recipe(

self._validate_recipe_kwargs(recipe_type, kwargs)

# Done while the recipe is built, which is before the export pipeline traces anything. The
# installer can re-execute the interpreter on an old glibc, and doing that partway through
# would discard a model that has already been traced.
setup_qnn_sdk()
disable_mkldnn_on_amd()

if recipe_type == QNNRecipeType.FP16:
return self._build_fp16_recipe(recipe_type, kwargs)

Expand Down
Loading
Loading