Skip to content
Open
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
14 changes: 2 additions & 12 deletions backends/qualcomm/__init__.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,13 @@
import os

from .scripts.download_qnn_sdk import (
check_glibc_exist_and_validate,
install_qnn_sdk,
is_linux_x86,
)
from .scripts.download_qnn_sdk import 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()
):
if env_flag not in ("1", "true", "yes") and not qnn_sdk_root_flag and is_linux_x86():
ok = install_qnn_sdk()

if not ok:
raise RuntimeError("Failed to install QNN SDK. Please check the logs above.")
280 changes: 215 additions & 65 deletions backends/qualcomm/scripts/download_qnn_sdk.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@
import platform
import re
import shutil
import subprocess
import sys
import tarfile
import tempfile
import urllib.request
import zipfile
from typing import Dict, List, Optional, Tuple


logger = logging.getLogger(__name__)
logger.addHandler(logging.NullHandler())

Expand All @@ -34,68 +37,81 @@ def is_linux_x86() -> bool:
)


import subprocess
#########################
# Cache directory helper
#########################

MINIMUM_LIBC_VERSION = 2.29
APP_NAMESPACE = ["executorch", "qnn"]

REQUIRED_LIBC_LIBS = [
"/lib/x86_64-linux-gnu/libc.so.6",
"/lib64/libc.so.6",
"/lib/libc.so.6",
]

def _get_staging_dir(*parts: str) -> pathlib.Path:
r"""
Return a cross-platform staging directory for staging SDKs/libraries.

- On Linux:
~/.cache/executorch/qnn/<parts...>
(falls back to $HOME/.cache if $XDG_CACHE_HOME is unset)

def check_glibc_exist_and_validate() -> bool:
- On Windows (not supported yet, but as placeholder):
%LOCALAPPDATA%\executorch\qnn\<parts...>
(falls back to $HOME/AppData/Local if %LOCALAPPDATA% is unset)

- Override:
If QNN_STAGING_DIR is set in the environment, that path is used instead.

Args:
parts (str): Subdirectories to append under the root staging dir.

Returns:
pathlib.Path: Fully qualified staging path.
"""
Check if users have glibc installed.
# Environment override wins
base = os.environ.get("QNN_STAGING_DIR")
if base:
return pathlib.Path(base).joinpath(*parts)

system = platform.system().lower()
if system == "windows":
# On Windows, prefer %LOCALAPPDATA%, fallback to ~/AppData/Local
base = pathlib.Path(
os.environ.get("LOCALAPPDATA", pathlib.Path.home() / "AppData" / "Local")
)
elif is_linux_x86():
# On Linux/Unix, prefer $XDG_CACHE_HOME, fallback to ~/.cache
base = pathlib.Path(
os.environ.get("XDG_CACHE_HOME", pathlib.Path.home() / ".cache")
)
else:
raise ValueError(f"Unsupported platform: {system}")

return base.joinpath(*APP_NAMESPACE, *parts)


def _atomic_download(url: str, dest: pathlib.Path):
"""
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.")
Download URL into dest atomically:
- Write to a temp file in the same dir
- Move into place if successful
"""
dest.parent.mkdir(parents=True, exist_ok=True)

except Exception:
continue
# Temp file in same dir (guarantees atomic rename)
with tempfile.NamedTemporaryFile(dir=dest.parent, delete=False) as tmp:
tmp_path = pathlib.Path(tmp.name)

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
try:
urllib.request.urlretrieve(url, tmp_path)
tmp_path.replace(dest) # atomic rename
except Exception:
# Clean up partial file on failure
if tmp_path.exists():
tmp_path.unlink(missing_ok=True)
raise


####################
# qnn sdk download management
####################


def _download_archive(url: str, archive_path: pathlib.Path) -> bool:
Expand Down Expand Up @@ -178,9 +194,6 @@ def _download_qnn_sdk(dst_folder=SDK_DIR) -> Optional[pathlib.Path]:
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")

Expand Down Expand Up @@ -241,6 +254,136 @@ def _extract_tar(archive_path: pathlib.Path, prefix: str, target_dir: pathlib.Pa
dst.write(src.read())


####################
# libc management
####################

GLIBC_VERSION = "2.34"
GLIBC_REEXEC_GUARD = "QNN_GLIBC_REEXEC"
MINIMUM_LIBC_VERSION = GLIBC_VERSION


def _get_glibc_libdir() -> pathlib.Path:
glibc_root = _get_staging_dir(f"glibc-{GLIBC_VERSION}")
return glibc_root / "lib"


def _parse_version(v: str) -> tuple[int, int]:
"""Turn '2.34' → (2,34) so it can be compared."""
parts = v.split(".")
return int(parts[0]), int(parts[1]) if len(parts) > 1 else 0


def _current_glibc_version() -> str:
"""Return system glibc version string (via ctypes)."""
try:
libc = ctypes.CDLL("libc.so.6")
func = libc.gnu_get_libc_version
func.restype = ctypes.c_char_p
return func().decode()
except Exception as e:
return f"error:{e}"


def _resolve_glibc_loader() -> pathlib.Path | None:
"""Return staged ld.so path if available."""
for p in [
_get_glibc_libdir() / f"ld-{GLIBC_VERSION}.so",
_get_glibc_libdir() / "ld-linux-x86-64.so.2",
]:
if p.exists():
return p
return None


def _stage_prebuilt_glibc():
"""Download + extract Fedora 35 glibc RPM into /tmp."""
logger.info(">>> Staging prebuilt glibc-%s from Fedora 35 RPM", GLIBC_VERSION)
_get_glibc_libdir().mkdir(parents=True, exist_ok=True)
rpm_path = _get_staging_dir("glibc") / "glibc.rpm"
work_dir = _get_staging_dir("glibc") / "extracted"
rpm_url = (
"https://archives.fedoraproject.org/pub/archive/fedora/linux/releases/35/"
"Everything/x86_64/os/Packages/g/glibc-2.34-7.fc35.x86_64.rpm"
)

rpm_path.parent.mkdir(parents=True, exist_ok=True)
logger.info("[glibc] Downloading %s -> %s", rpm_url, rpm_path)
try:
urllib.request.urlretrieve(rpm_url, rpm_path)
except Exception as e:
logger.error("[glibc] Failed to download %s: %s", rpm_url, e)
raise

# Extract
if work_dir.exists():
shutil.rmtree(work_dir)
work_dir.mkdir(parents=True)
subprocess.check_call(["bsdtar", "-C", str(work_dir), "-xf", str(rpm_path)])

# Copy runtime libs
staged = [
"ld-linux-x86-64.so.2",
"libc.so.6",
"libdl.so.2",
"libpthread.so.0",
"librt.so.1",
"libm.so.6",
"libutil.so.1",
]
for lib in staged:
src = work_dir / "lib64" / lib
if src.exists():
shutil.copy2(src, _get_glibc_libdir() / lib)
logger.info("[glibc] Staged %s", lib)
else:
logger.warning("[glibc] Missing %s in RPM", lib)


def ensure_glibc_minimum(min_version: str = GLIBC_VERSION):
"""
Ensure process runs under glibc >= min_version.
- If system glibc is new enough → skip.
- Else → stage Fedora RPM and re-exec under staged loader.
"""
current = _current_glibc_version()
logger.info("[glibc] Current loaded glibc: %s", current)

# If system glibc already sufficient → skip everything
m = re.match(r"(\d+\.\d+)", current)
if m and _parse_version(m.group(1)) >= _parse_version(min_version):
logger.info("[glibc] System glibc >= %s, no staging needed.", min_version)
return

# Avoid infinite loop
if os.environ.get(GLIBC_REEXEC_GUARD) == "1":
logger.info("[glibc] Already re-exec'd once, continuing.")
return

# Stage prebuilt if not already staged
if not (_get_glibc_libdir() / "libc.so.6").exists():
_stage_prebuilt_glibc()

loader = _resolve_glibc_loader()
if not loader:
logger.error("[glibc] Loader not found in %s", _get_glibc_libdir())
return

logger.info(
"[glibc] Re-execing under loader %s with libdir %s", loader, _get_glibc_libdir()
)
os.environ[GLIBC_REEXEC_GUARD] = "1"
os.execv(
str(loader),
[str(loader), "--library-path", str(_get_glibc_libdir()), sys.executable]
+ sys.argv,
)


####################
# libc++ management
####################

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"
Expand All @@ -258,12 +401,17 @@ def _stage_libcxx(target_dir: pathlib.Path):
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
libcxx_stage = _get_staging_dir(f"libcxx-{LLVM_VERSION}")
temp_tar = libcxx_stage / f"{LIBCXX_BASE_NAME}.tar.xz"
temp_extract = libcxx_stage / LIBCXX_BASE_NAME

if not temp_tar.exists():
logger.info("[libcxx] Downloading %s", LLVM_URL)
urllib.request.urlretrieve(LLVM_URL, temp_tar)
_atomic_download(LLVM_URL, temp_tar)

# Sanity check before extracting
if not temp_tar.exists() or temp_tar.stat().st_size == 0:
raise FileNotFoundError(f"[libcxx] Tarball missing or empty: {temp_tar}")

logger.info("[libcxx] Extracting %s", temp_tar)
with tarfile.open(temp_tar, "r:xz") as tar:
Expand Down Expand Up @@ -437,8 +585,10 @@ def install_qnn_sdk() -> bool:
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
logger.info("[QNN] Starting SDK installation")

# Make sure we’re running under >= 2.34
ensure_glibc_minimum(GLIBC_VERSION)

# libc++ and QNN SDK setup
return _ensure_libcxx_stack() and _ensure_qnn_sdk_lib()
3 changes: 1 addition & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -467,11 +467,10 @@ def run(self):
# 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():
if is_linux_x86():
os.environ["EXECUTORCH_BUILDING_WHEEL"] = "1"

with tempfile.TemporaryDirectory() as tmpdir:
Expand Down
Loading