diff --git a/.ci/scripts/test_wheel_package_qnn.sh b/.ci/scripts/test_wheel_package_qnn.sh deleted file mode 100644 index 39c52a4a396..00000000000 --- a/.ci/scripts/test_wheel_package_qnn.sh +++ /dev/null @@ -1,201 +0,0 @@ -#!/bin/bash -# === CI Wheel Build & Test Script === - -# Exit immediately on error, print each command, and capture all output to build.log -set -e -set -x -exec > >(tee -i build.log) 2>&1 - -# Save repo root -REPO_ROOT=$(pwd) - -# ---------------------------- -# Dynamically create script_qnn_wheel_test.py -# ---------------------------- -cat > "/tmp/script_qnn_wheel_test.py" << 'EOF' -# pyre-ignore-all-errors -import argparse - -import torch -from executorch.backends.qualcomm.quantizer.quantizer import QnnQuantizer -from executorch.backends.qualcomm.utils.utils import ( - generate_htp_compiler_spec, - generate_qnn_executorch_compiler_spec, - get_soc_to_chipset_map, - to_edge_transform_and_lower_to_qnn, -) -from executorch.exir.backend.utils import format_delegated_graph -from executorch.examples.models.model_factory import EagerModelFactory -from executorch.exir.capture._config import ExecutorchBackendConfig -from executorch.extension.export_util.utils import save_pte_program -from torchao.quantization.pt2e.quantize_pt2e import convert_pt2e, prepare_pt2e, prepare_qat_pt2e - -def main() -> None: - parser = argparse.ArgumentParser() - parser.add_argument("-f", "--output_folder", type=str, default="", help="The folder to store the exported program") - parser.add_argument("--soc", type=str, default="SM8650", help="Specify the SoC model.") - parser.add_argument("-q", "--quantization", choices=["ptq", "qat"], help="Run post-traininig quantization.") - args = parser.parse_args() - - class LinearModule(torch.nn.Module): - def __init__(self): - super().__init__() - self.linear = torch.nn.Linear(3, 3) - def forward(self, arg): - return self.linear(arg) - def get_example_inputs(self): - return (torch.randn(3, 3),) - - model = LinearModule() - example_inputs = model.get_example_inputs() - - if args.quantization: - quantizer = QnnQuantizer() - m = torch.export.export(model.eval(), example_inputs, strict=True).module() - if args.quantization == "qat": - m = prepare_qat_pt2e(m, quantizer) - m(*example_inputs) - elif args.quantization == "ptq": - m = prepare_pt2e(m, quantizer) - m(*example_inputs) - m = convert_pt2e(m) - else: - m = model - - use_fp16 = True if args.quantization is None else False - backend_options = generate_htp_compiler_spec(use_fp16=use_fp16) - compile_spec = generate_qnn_executorch_compiler_spec( - soc_model=get_soc_to_chipset_map()[args.soc], - backend_options=backend_options, - ) - delegated_program = to_edge_transform_and_lower_to_qnn(m, example_inputs, compile_spec) - output_graph = format_delegated_graph(delegated_program.exported_program().graph_module) - # Ensure QnnBackend is in the output graph - assert "QnnBackend" in output_graph - executorch_program = delegated_program.to_executorch( - config=ExecutorchBackendConfig(extract_delegate_segments=False) - ) - save_pte_program(executorch_program, "linear", args.output_folder) - -if __name__ == "__main__": - main() -EOF - -# ---------------------------- -# Wheel build and .so checks -# ---------------------------- -echo "=== Building Wheel Package ===" -source .ci/scripts/utils.sh -install_executorch -EXECUTORCH_BUILDING_WHEEL=1 python setup.py bdist_wheel -unset EXECUTORCH_BUILDING_WHEEL - -WHEEL_FILE=$(ls dist/*.whl | head -n 1) -echo "Found wheel: $WHEEL_FILE" - -PYTHON_VERSION=$1 -# ---------------------------- -# Check wheel does NOT contain qualcomm/sdk -# ---------------------------- -echo "Checking wheel does not contain qualcomm/sdk..." -SDK_FILES=$(unzip -l "$WHEEL_FILE" | awk '{print $4}' | grep "executorch/backends/qualcomm/sdk" || true) -if [ -n "$SDK_FILES" ]; then - echo "ERROR: Wheel package contains unexpected qualcomm/sdk files:" - echo "$SDK_FILES" - exit 1 -else - echo "OK: No qualcomm/sdk files found in wheel" -fi - -# ---------------------------- -# Check .so files in the wheel -# ---------------------------- -echo "Checking for .so files inside the wheel..." -WHEEL_SO_FILES=$(unzip -l "$WHEEL_FILE" | awk '{print $4}' | grep "executorch/backends/qualcomm/python" || true) -if [ -z "$WHEEL_SO_FILES" ]; then - echo "ERROR: No .so files found in wheel under executorch/backends/qualcomm/python" - exit 1 -else - echo "Wheel contains the following .so files:" - echo "$WHEEL_SO_FILES" -fi - -# ---------------------------- -# Helpers -# ---------------------------- -get_site_packages_dir () { - local PYBIN="$1" - "$PYBIN" - <<'PY' -import sysconfig, sys -print(sysconfig.get_paths().get("purelib") or sysconfig.get_paths().get("platlib")) -PY -} - -run_core_tests () { - local PYBIN="$1" # path to python - local PIPBIN="$2" # path to pip - local LABEL="$3" # label to print (conda/venv) - - echo "=== [$LABEL] Installing wheel & deps ===" - "$PIPBIN" install --upgrade pip - "$PIPBIN" install "$WHEEL_FILE" - "$PIPBIN" install torch=="2.9.0.dev20250906" --index-url "https://download.pytorch.org/whl/nightly/cpu" - "$PIPBIN" install --pre torchao --index-url "https://download.pytorch.org/whl/nightly/cpu" - - echo "=== [$LABEL] Import smoke tests ===" - "$PYBIN" -c "import executorch; print('executorch imported successfully')" - "$PYBIN" -c "import executorch.backends.qualcomm; print('executorch.backends.qualcomm imported successfully')" - - echo "=== [$LABEL] List installed executorch/backends/qualcomm/python ===" - local SITE_DIR - SITE_DIR="$(get_site_packages_dir "$PYBIN")" - local SO_DIR="$SITE_DIR/executorch/backends/qualcomm/python" - ls -l "$SO_DIR" || echo "Folder does not exist!" - - echo "=== [$LABEL] Run export script to generate linear.pte ===" - (cd "$REPO_ROOT" && "$PYBIN" "/tmp/script_qnn_wheel_test.py") - - if [ -f "$REPO_ROOT/linear.pte" ]; then - echo "[$LABEL] Model file linear.pte successfully created" - else - echo "ERROR: [$LABEL] Model file linear.pte was not created" - exit 1 - fi -} - -# ---------------------------- -# Conda environment setup & tests -# ---------------------------- -echo "=== Testing in Conda env ===" -TEMP_ENV_DIR=$(mktemp -d) -echo "Using temporary directory for conda: $TEMP_ENV_DIR" -conda create -y -p "$TEMP_ENV_DIR/env" python=$PYTHON_VERSION -# derive python/pip paths inside the conda env -CONDA_PY="$TEMP_ENV_DIR/env/bin/python" -CONDA_PIP="$TEMP_ENV_DIR/env/bin/pip" -# Some images require conda run; keep pip/python direct to simplify path math -run_core_tests "$CONDA_PY" "$CONDA_PIP" "conda" - -# Cleanup conda env -conda env remove -p "$TEMP_ENV_DIR/env" -y || true -rm -rf "$TEMP_ENV_DIR" - -# ---------------------------- -# Python venv setup & tests -# ---------------------------- -echo "=== Testing in Python venv ===" -TEMP_VENV_DIR=$(mktemp -d) -echo "Using temporary directory for venv: $TEMP_VENV_DIR" -python3 -m venv "$TEMP_VENV_DIR/venv" -VENV_PY="$TEMP_VENV_DIR/venv/bin/python" -VENV_PIP="$TEMP_VENV_DIR/venv/bin/pip" - -# Ensure venv has wheel/build basics if needed -"$VENV_PIP" install --upgrade pip - -run_core_tests "$VENV_PY" "$VENV_PIP" "venv" - -# Cleanup venv -rm -rf "$TEMP_VENV_DIR" - -echo "=== All tests completed! ===" diff --git a/.github/workflows/pull.yml b/.github/workflows/pull.yml index 815e106ae1e..6e9169132e5 100644 --- a/.github/workflows/pull.yml +++ b/.github/workflows/pull.yml @@ -13,33 +13,6 @@ concurrency: cancel-in-progress: true jobs: - test-qnn-wheel-packages-linux: - name: test-qnn-wheel-packages-linux - uses: pytorch/test-infra/.github/workflows/linux_job_v2.yml@main - permissions: - id-token: write - contents: read - strategy: - fail-fast: false - matrix: - python-version: [ "3.10", "3.11", "3.12" ] - with: - runner: linux.2xlarge - docker-image: ci-image:executorch-ubuntu-22.04-qnn-sdk - submodules: 'recursive' - ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} - timeout: 180 - script: | - # The generic Linux job chooses to use base env, not the one setup by the image - CONDA_ENV=$(conda env list --json | jq -r ".envs | .[-1]") - conda activate "${CONDA_ENV}" - - # Create a clean env for each python version - conda create -y -n test_env_${{ matrix.python-version }} python=${{ matrix.python-version }} - conda activate test_env_${{ matrix.python-version }} - - PYTHON_EXECUTABLE=python bash .ci/scripts/test_wheel_package_qnn.sh "${{ matrix.python-version }}" - test-setup-linux-gcc: name: test-setup-linux-gcc uses: pytorch/test-infra/.github/workflows/linux_job_v2.yml@main diff --git a/.gitignore b/.gitignore index b166f8c9512..511fb324ba2 100644 --- a/.gitignore +++ b/.gitignore @@ -16,8 +16,6 @@ cmake-android-out/ cmake-ios-out/ cmake-out* cmake-out-android/ -build-android/ -build-x86/ dist/ ethos-u-scratch/ executorch.egg-info diff --git a/backends/qualcomm/__init__.py b/backends/qualcomm/__init__.py deleted file mode 100644 index 04ba5fcf24b..00000000000 --- a/backends/qualcomm/__init__.py +++ /dev/null @@ -1,23 +0,0 @@ -import os - -from .scripts.download_qnn_sdk import ( - check_glibc_exist_and_validate, - install_qnn_sdk, - is_linux_x86, -) - - -env_flag = os.getenv("EXECUTORCH_BUILDING_WHEEL", "0").lower() -# If users have preinstalled QNN_SDK_ROOT, we will use it. -qnn_sdk_root_flag = os.getenv("QNN_SDK_ROOT", None) - -if ( - env_flag not in ("1", "true", "yes") - and not qnn_sdk_root_flag - and is_linux_x86() - and check_glibc_exist_and_validate() -): - ok = install_qnn_sdk() - - if not ok: - raise RuntimeError("Failed to install QNN SDK. Please check the logs above.") diff --git a/backends/qualcomm/runtime/backends/QnnImplementation.cpp b/backends/qualcomm/runtime/backends/QnnImplementation.cpp index 7083f2bef30..42f866d22cc 100644 --- a/backends/qualcomm/runtime/backends/QnnImplementation.cpp +++ b/backends/qualcomm/runtime/backends/QnnImplementation.cpp @@ -6,6 +6,7 @@ * LICENSE file in the root directory of this source tree. */ #include + #include "QnnInterface.h" namespace executorch { namespace backends { @@ -51,11 +52,7 @@ Error QnnImplementation::StartBackend( const QnnSaver_Config_t** saver_config) { Qnn_ErrorHandle_t error = QNN_SUCCESS; void* lib_handle = nullptr; - // If the library is already loaded, return the handle. - lib_handle = dlopen(lib_path.c_str(), RTLD_NOW | RTLD_NOLOAD); - if (!lib_handle) { - lib_handle = dlopen(lib_path.c_str(), RTLD_NOW | RTLD_GLOBAL); - } + lib_handle = dlopen(lib_path.c_str(), RTLD_NOW | RTLD_GLOBAL); if (lib_handle == nullptr) { QNN_EXECUTORCH_LOG_ERROR( "Cannot Open QNN library %s, with error: %s", diff --git a/backends/qualcomm/scripts/build.sh b/backends/qualcomm/scripts/build.sh index c84911cf851..297f81fc85d 100755 --- a/backends/qualcomm/scripts/build.sh +++ b/backends/qualcomm/scripts/build.sh @@ -1,4 +1,3 @@ -#!/usr/bin/env bash # Copyright (c) Qualcomm Innovation Center, Inc. # All rights reserved # diff --git a/backends/qualcomm/scripts/download_qnn_sdk.py b/backends/qualcomm/scripts/download_qnn_sdk.py deleted file mode 100644 index 35006a41433..00000000000 --- a/backends/qualcomm/scripts/download_qnn_sdk.py +++ /dev/null @@ -1,444 +0,0 @@ -# Add these imports for additional logging -import ctypes -import logging -import os -import pathlib -import platform -import re -import shutil -import tarfile -import tempfile -import urllib.request -import zipfile -from typing import Dict, List, Optional, Tuple - -logger = logging.getLogger(__name__) -logger.addHandler(logging.NullHandler()) - -PKG_ROOT = pathlib.Path(__file__).parent.parent -SDK_DIR = PKG_ROOT / "sdk" / "qnn" - - -def is_linux_x86() -> bool: - """ - Check if the current platform is Linux x86_64. - - Returns: - bool: True if the system is Linux x86_64, False otherwise. - """ - return platform.system().lower() == "linux" and platform.machine().lower() in ( - "x86_64", - "amd64", - "i386", - "i686", - ) - - -import subprocess - -MINIMUM_LIBC_VERSION = 2.29 - -REQUIRED_LIBC_LIBS = [ - "/lib/x86_64-linux-gnu/libc.so.6", - "/lib64/libc.so.6", - "/lib/libc.so.6", -] - - -def check_glibc_exist_and_validate() -> bool: - """ - Check if users have glibc installed. - """ - exists = False - for path in REQUIRED_LIBC_LIBS: - try: - output = subprocess.check_output( - [path, "--version"], stderr=subprocess.STDOUT - ) - output = output.decode().split("\n")[0] - logger.debug(f"[QNN] glibc version for path {path} is: {output}") - match = re.search(r"version (\d+\.\d+)", output) - if match: - version = match.group(1) - if float(version) >= MINIMUM_LIBC_VERSION: - logger.debug(f"[QNN] glibc version is {version}.") - exists = True - return True - else: - logger.error( - f"[QNN] glibc version is too low. The minimum libc version is {MINIMUM_LIBC_VERSION} Please install glibc following the commands below." - ) - else: - logger.error("[QNN] glibc version not found.") - - except Exception: - continue - - if not exists: - logger.error( - r"""" - [QNN] glibc not found or the version is too low. Please install glibc following the commands below. - Ubuntu/Debian: - sudo apt update - sudo apt install libc6 - - Fedora/Red Hat: - sudo dnf install glibc - - Arch Linux: - sudo pacman -S glibc - - Also please make sure the glibc version is >= MINIMUM_LIBC_VERSION. You can verify the glibc version by running the following command: - Option 1: - ldd --version - Option 2: - /path/to/libc.so.6 --version - """ - ) - return exists - - -def _download_archive(url: str, archive_path: pathlib.Path) -> bool: - """Download archive from URL with progress reporting.""" - logger.debug("Archive will be saved to: %s", archive_path) - - try: - urllib.request.urlretrieve(url, archive_path, _make_report_progress()) - logger.info("Download completed!") - except Exception as e: - logger.exception("Error during download: %s", e) - return False - - if archive_path.exists() and archive_path.stat().st_size == 0: - logger.warning("Downloaded file is empty!") - return False - elif not archive_path.exists(): - logger.error("File was not downloaded!") - return False - return True - - -def _make_report_progress(): - """Return a callback to report download progress.""" - last_reported = 0 - - def report_progress(block_num, block_size, total_size): - nonlocal last_reported - try: - downloaded = block_num * block_size - percent = downloaded / total_size * 100 if total_size else 100.0 - except Exception: - percent, downloaded, total_size = 0.0, block_num * block_size, 0 - if percent - last_reported >= 20 or percent >= 100: - logger.info( - "Downloaded: %d/%d bytes (%.2f%%)", downloaded, total_size, percent - ) - last_reported = percent - - return report_progress - - -def _extract_archive( - url: str, archive_path: pathlib.Path, content_dir: str, dst_folder: pathlib.Path -): - """Extract archive based on type (zip or tar).""" - if url.endswith(".zip"): - logger.info("Extracting ZIP archive...") - _extract_zip(archive_path, content_dir, dst_folder) - elif url.endswith((".tar.gz", ".tgz")): - logger.info("Extracting TAR archive...") - _extract_tar(archive_path, content_dir, dst_folder) - else: - raise ValueError(f"Unsupported archive format: {url}") - - -def _verify_extraction(dst_folder: pathlib.Path): - """Check if extraction succeeded and log contents.""" - logger.info("Verifying extraction to %s", dst_folder) - if dst_folder.exists(): - logger.debug("SDK directory exists. Contents:") - for item in dst_folder.iterdir(): - logger.debug(" %s", item.name) - else: - logger.error("SDK directory was not created!") - - -def _download_qnn_sdk(dst_folder=SDK_DIR) -> Optional[pathlib.Path]: - """ - Download and extract the Qualcomm SDK into dst_folder. - Only runs on Linux x86 platforms. - """ - QNN_VERSION = "2.37.0.250724" - logger.info("Downloading Qualcomm SDK...") - QAIRT_URL = ( - f"https://softwarecenter.qualcomm.com/api/download/software/sdks/" - f"Qualcomm_AI_Runtime_Community/All/{QNN_VERSION}/v{QNN_VERSION}.zip" - ) - QAIRT_CONTENT_DIR = f"qairt/{QNN_VERSION}" - if not is_linux_x86(): - logger.info("[QNN] Skipping Qualcomm SDK (only supported on Linux x86).") - return None - elif not check_glibc_exist_and_validate(): - logger.info("[QNN] Skipping Qualcomm SDK (glibc not found or version too old).") - return None - else: - logger.info("[QNN] Downloading Qualcomm SDK for Linux x86") - - dst_folder.mkdir(parents=True, exist_ok=True) - - with tempfile.TemporaryDirectory() as tmpdir: - archive_path = pathlib.Path(tmpdir) / pathlib.Path(QAIRT_URL).name - if not _download_archive(QAIRT_URL, archive_path): - return None - - _extract_archive(QAIRT_URL, archive_path, QAIRT_CONTENT_DIR, dst_folder) - _verify_extraction(dst_folder) - - return dst_folder - - -def _extract_zip(archive_path, content_dir, target_dir): - logger.debug("Extracting %s to %s", archive_path, target_dir) - logger.debug("Looking for content in subdirectory: %s", content_dir) - - target_dir.mkdir(parents=True, exist_ok=True) - - with zipfile.ZipFile(archive_path, "r") as zip_ref: - files_to_extract = [f for f in zip_ref.namelist() if f.startswith(content_dir)] - - for file in files_to_extract: - relative_path = pathlib.Path(file).relative_to(content_dir) - if relative_path == pathlib.Path("."): - continue - - out_path = target_dir / relative_path - if file.endswith("/"): - out_path.mkdir(parents=True, exist_ok=True) - else: - out_path.parent.mkdir(parents=True, exist_ok=True) - with zip_ref.open(file) as src, open(out_path, "wb") as dst: - shutil.copyfileobj(src, dst) - - -def _extract_tar(archive_path: pathlib.Path, prefix: str, target_dir: pathlib.Path): - with tarfile.open(archive_path, "r:gz") as tf: - for m in tf.getmembers(): - if not m.name.startswith(prefix + "/"): - continue - relpath = pathlib.Path(m.name).relative_to(prefix) - if not relpath.parts or relpath.parts[0] == "..": - continue - - out_path = target_dir / relpath - if m.isdir(): - out_path.mkdir(parents=True, exist_ok=True) - else: - out_path.parent.mkdir(parents=True, exist_ok=True) - src = tf.extractfile(m) - if src is None: - continue - with src, open(out_path, "wb") as dst: - dst.write(src.read()) - - -LLVM_VERSION = "14.0.0" -LIBCXX_BASE_NAME = f"clang+llvm-{LLVM_VERSION}-x86_64-linux-gnu-ubuntu-18.04" -LLVM_URL = f"https://github.com/llvm/llvm-project/releases/download/llvmorg-{LLVM_VERSION}/{LIBCXX_BASE_NAME}.tar.xz" -REQUIRED_LIBCXX_LIBS = [ - "libc++.so.1.0", - "libc++abi.so.1.0", - "libunwind.so.1", -] - - -def _stage_libcxx(target_dir: pathlib.Path): - target_dir.mkdir(parents=True, exist_ok=True) - - if all((target_dir / libname).exists() for libname in REQUIRED_LIBCXX_LIBS): - logger.info("[libcxx] Already staged at %s, skipping download", target_dir) - return - - temp_tar = pathlib.Path("/tmp") / f"{LIBCXX_BASE_NAME}.tar.xz" - temp_extract = pathlib.Path("/tmp") / LIBCXX_BASE_NAME - - if not temp_tar.exists(): - logger.info("[libcxx] Downloading %s", LLVM_URL) - urllib.request.urlretrieve(LLVM_URL, temp_tar) - - logger.info("[libcxx] Extracting %s", temp_tar) - with tarfile.open(temp_tar, "r:xz") as tar: - tar.extractall(temp_extract.parent) - - lib_src = temp_extract / "lib" / "x86_64-unknown-linux-gnu" - for fname in REQUIRED_LIBCXX_LIBS: - src_path = lib_src / fname - if not src_path.exists(): - logger.warning( - "[libcxx] %s not found in extracted LLVM src_path %s", fname, src_path - ) - continue - shutil.copy(src_path, target_dir / fname) - - logger.info("[libcxx] Staged libc++ to %s", target_dir) - - -REQUIRED_QNN_LIBS: List[str] = [ - "libQnnHtp.so", -] - - -def _ld_library_paths() -> List[pathlib.Path]: - """Split LD_LIBRARY_PATH into ordered directories (skip empties).""" - raw = os.environ.get("LD_LIBRARY_PATH", "") - return [pathlib.Path(p) for p in raw.split(":") if p.strip()] - - -def _find_lib_in_ld_paths( - libname: str, ld_dirs: Optional[List[pathlib.Path]] = None -) -> Optional[pathlib.Path]: - """Return first matching path to `libname` in LD_LIBRARY_PATH, or None.""" - if ld_dirs is None: - ld_dirs = _ld_library_paths() - for d in ld_dirs: - candidate = d / libname - try: - if candidate.exists(): - return candidate.resolve() - except Exception: - # Ignore unreadable / permission issues, keep looking. - pass - return None - - -def _check_libs_in_ld( - libnames: List[str], -) -> Tuple[bool, Dict[str, Optional[pathlib.Path]]]: - """ - Check if each lib in `libnames` exists in LD_LIBRARY_PATH directories. - - Returns: - all_present: True iff every lib was found - locations: mapping lib -> path (or None if missing) - """ - ld_dirs = _ld_library_paths() - locations: Dict[str, Optional[pathlib.Path]] = {} - for lib in libnames: - locations[lib] = _find_lib_in_ld_paths(lib, ld_dirs) - all_present = all(locations[lib] is not None for lib in libnames) - return all_present, locations - - -# ----------------------- -# Ensure QNN SDK library -# ----------------------- -def _ensure_qnn_sdk_lib() -> bool: - """ - Ensure libQnnHtp.so is available. - - If found in LD_LIBRARY_PATH: do nothing, return True. - - Otherwise: ensure packaged SDK is present, then load libQnnHtp.so from it. - """ - all_present, locs = _check_libs_in_ld(REQUIRED_QNN_LIBS) - if all_present: - logger.info( - "[QNN] libQnnHtp.so found in LD_LIBRARY_PATH; skipping SDK install." - ) - for lib, p in locs.items(): - logger.info(" - %s: %s", lib, p) - return True - - # Not found → use packaged SDK - qnn_sdk_dir = SDK_DIR - logger.info("[QNN] libQnnHtp.so not found in LD_LIBRARY_PATH.") - if not qnn_sdk_dir.exists(): - logger.info("[QNN] SDK dir missing; downloading...") - _download_qnn_sdk() - else: - logger.info("[QNN] Using existing SDK at %s", qnn_sdk_dir) - - os.environ["QNN_SDK_ROOT"] = str(qnn_sdk_dir) - - qnn_lib = qnn_sdk_dir / "lib" / "x86_64-linux-clang" / "libQnnHtp.so" - logger.info("[QNN] Loading %s", qnn_lib) - lib_loaded = False - try: - ctypes.CDLL(str(qnn_lib), mode=ctypes.RTLD_GLOBAL) - logger.info("[QNN] Loaded libQnnHtp.so from packaged SDK.") - lib_loaded = True - except OSError as e: - logger.error("[QNN][ERROR] Failed to load %s: %s", qnn_lib, e) - return lib_loaded - - -def _load_libcxx_libs(lib_path): - logger.debug("running _load_libcxx_libs") - candidates = list(lib_path.glob("*.so*")) - priority = ["libc++abi", "libc++"] - sorted_candidates = [ - f for name in priority for f in candidates if f.name.startswith(name) - ] - sorted_candidates += [f for f in candidates if f not in sorted_candidates] - logger.debug("sorted_candidates: %s", sorted_candidates) - for sofile in sorted_candidates: - try: - ctypes.CDLL(str(sofile), mode=ctypes.RTLD_GLOBAL) - logger.info("Loaded %s", sofile.name) - except OSError as e: - logger.warning("[WARN] Failed to load %s: %s", sofile.name, e) - - -# --------------------- -# Ensure libc++ family -# --------------------- -def _ensure_libcxx_stack() -> bool: - """ - Ensure libc++ stack is available. - - If all required libc++ libs are found in LD_LIBRARY_PATH: do nothing. - - Otherwise: stage and load the packaged libc++ bundle. - """ - all_present, locs = _check_libs_in_ld(REQUIRED_LIBCXX_LIBS) - if all_present: - logger.info( - "[libcxx] All libc++ libs present in LD_LIBRARY_PATH; skipping staging." - ) - for lib, p in locs.items(): - logger.info(" - %s: %s", lib, p) - return True - - logger.info( - "[libcxx] Some libc++ libs missing in LD_LIBRARY_PATH; staging packaged libc++..." - ) - lib_loaded = False - try: - libcxx_dir = PKG_ROOT / "sdk" / f"libcxx-{LLVM_VERSION}" - _stage_libcxx(libcxx_dir) - _load_libcxx_libs(libcxx_dir) - logger.info("[libcxx] Staged and loaded libc++ from %s", libcxx_dir) - lib_loaded = True - except Exception as e: - logger.exception("[libcxx][ERROR] Failed to stage/load libc++: %s", e) - return lib_loaded - - -# --------------- -# Public entrypoint -# --------------- -def install_qnn_sdk() -> bool: - """ - Initialize Qualcomm backend with separated logic: - - QNN SDK: - - If libQnnHtp.so exists in LD_LIBRARY_PATH: do nothing. - - Else: ensure packaged SDK, load libQnnHtp.so. - - libc++ stack: - - If required libc++ libs exist in LD_LIBRARY_PATH: do nothing. - - Else: stage and load packaged libc++. - - Returns: - True if both steps succeeded (or were already satisfied), else False. - """ - if check_glibc_exist_and_validate(): - if _ensure_libcxx_stack(): - if _ensure_qnn_sdk_lib(): - return True - return False diff --git a/setup.py b/setup.py index 713bdd47f81..514d1af7726 100644 --- a/setup.py +++ b/setup.py @@ -47,18 +47,16 @@ # derivative works thereof, in binary and source code form. import contextlib - -# Import this before distutils so that setuptools can intercept the distuils -# imports. -import logging import os import re import shutil import site -import subprocess import sys -import sysconfig -import tempfile + +# Import this before distutils so that setuptools can intercept the distuils +# imports. +import setuptools # noqa: F401 # usort: skip +import subprocess from distutils import log # type: ignore[import-not-found] from distutils.sysconfig import get_python_lib # type: ignore[import-not-found] @@ -70,11 +68,6 @@ from setuptools.command.build_ext import build_ext from setuptools.command.build_py import build_py -logging.basicConfig( - level=logging.INFO, - format="%(asctime)s [%(levelname)s] %(message)s", -) - try: from tools.cmake.cmake_cache import CMakeCache except ImportError: @@ -463,80 +456,6 @@ def run(self): if self._ran_build: return - try: - # Following code is for building the Qualcomm backend. - from backends.qualcomm.scripts.download_qnn_sdk import ( - _download_qnn_sdk, - check_glibc_exist_and_validate, - is_linux_x86, - ) - - if is_linux_x86() and check_glibc_exist_and_validate(): - os.environ["EXECUTORCH_BUILDING_WHEEL"] = "1" - - with tempfile.TemporaryDirectory() as tmpdir: - tmp_path = Path(tmpdir) - sdk_path = _download_qnn_sdk(dst_folder=tmp_path) - - logging.info("sdk_path: ", sdk_path) - if not sdk_path: - raise RuntimeError( - "Qualcomm SDK not found, cannot build backend" - ) - - # Determine paths - prj_root = Path(__file__).parent.resolve() - logging.info("prj_root: ", prj_root) - build_sh = prj_root / "backends/qualcomm/scripts/build.sh" - build_root = prj_root / "build-x86" - - if not build_sh.exists(): - raise FileNotFoundError(f"{build_sh} not found") - - # Run build.sh with SDK path exported - env = dict(**os.environ) - env["QNN_SDK_ROOT"] = str(sdk_path) - subprocess.check_call([str(build_sh), "--skip_aarch64"], env=env) - - # Copy the main .so into the wheel package - so_src = ( - build_root / "backends/qualcomm/libqnn_executorch_backend.so" - ) - so_dst = Path( - self.get_ext_fullpath( - "executorch.backends.qualcomm.qnn_backend" - ) - ) - self.mkpath(str(so_dst.parent)) # ensure destination exists - self.copy_file(str(so_src), str(so_dst)) - logging.info(f"Copied Qualcomm backend: {so_src} -> {so_dst}") - - # Copy Python adaptor .so files - ext_suffix = sysconfig.get_config_var("EXT_SUFFIX") - - so_files = [ - ( - "executorch.backends.qualcomm.python.PyQnnManagerAdaptor", - prj_root - / f"backends/qualcomm/python/PyQnnManagerAdaptor{ext_suffix}", - ), - ( - "executorch.backends.qualcomm.python.PyQnnWrapperAdaptor", - prj_root - / f"backends/qualcomm/python/PyQnnWrapperAdaptor{ext_suffix}", - ), - ] - - for module_name, so_src in so_files: - so_dst = Path(self.get_ext_fullpath(module_name)) - self.mkpath(str(so_dst.parent)) - self.copy_file(str(so_src), str(so_dst)) - logging.info(f"Copied Qualcomm backend: {so_src} -> {so_dst}") - - except ImportError: - logging.error("Fail to build Qualcomm backend") - logging.exception("Import error") - if self.editable_mode: self._ran_build = True self.run_command("build")