Skip to content

Commit 862bbbb

Browse files
cccclaifacebook-github-bot
authored andcommitted
Add qnn backend to pip package
Summary: When generating the wheel packages - download the qnn sdk such that we can build the qnn backend - remove the downloaded sdk files so it won't be part of wheel packages Inside the qnn packages, it includes the AoT part and the the qnn backend.so files (the build from executorch/backends/qualcomm) For users: ``` # This step will install qnn backend pip install executorch-0.8.0a0+e5f94da-cp310-cp310-linux_x86_64.whl # The qnn sdk will be downloaded when users first , if the QNN_SDK_ROOT is not set. This step will only run once from executorch.backend.qualcomm import any module ``` Rollback Plan: Differential Revision: D80992066
1 parent b4e1145 commit 862bbbb

File tree

4 files changed

+195
-1
lines changed

4 files changed

+195
-1
lines changed

backends/qualcomm/__init__.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import os
2+
import pathlib
3+
from scripts.download_qnn_sdk import _download_qnn_sdk
4+
5+
# -----------------------------------------------------------------------------
6+
# Main SDK setup
7+
# -----------------------------------------------------------------------------
8+
qnn_root = os.environ.get("QNN_SDK_ROOT")
9+
if qnn_root:
10+
SDK_DIR = pathlib.Path(qnn_root)
11+
else:
12+
if not SDK_DIR.exists():
13+
print("Qualcomm SDK not found. Downloading...")
14+
_download_qnn_sdk()
15+
os.environ["QNN_SDK_ROOT"] = str(SDK_DIR)

backends/qualcomm/scripts/build.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
#!/bin/bash
12
# Copyright (c) Qualcomm Innovation Center, Inc.
23
# All rights reserved
34
#
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import pathlib
2+
import platform
3+
import sys
4+
import tarfile
5+
import tempfile
6+
import urllib.request
7+
import zipfile
8+
9+
10+
11+
def is_linux_x86() -> bool:
12+
"""
13+
Check if the current system is Linux running on an x86 architecture.
14+
15+
Returns:
16+
bool: True if the system is Linux and the architecture is one of
17+
x86_64, i386, or i686. False otherwise.
18+
"""
19+
return sys.platform.startswith("linux") and platform.machine() in {
20+
"x86_64",
21+
"i386",
22+
"i686",
23+
}
24+
25+
SDK_DIR = pathlib.Path(__file__).parent / "sdk"
26+
27+
def _download_qnn_sdk() -> pathlib.Path:
28+
"""
29+
Download and extract the Qualcomm SDK from the given URL into target_dir.
30+
31+
Args:
32+
url (str): The URL of the archive (.zip, .tar.gz, or .tgz).
33+
prefix_to_strip (str): Top-level directory inside the archive to strip
34+
from extracted file paths.
35+
target_dir (pathlib.Path): Directory to extract the SDK into.
36+
37+
Notes:
38+
- Only runs on Linux x86 platforms. Skips otherwise.
39+
- Creates the target_dir if it does not exist.
40+
"""
41+
# Default path where the Qualcomm SDK will be installed (under the script directory).
42+
43+
# URL to download the Qualcomm AI Runtime SDK archive.
44+
qairt_url = (
45+
"https://softwarecenter.qualcomm.com/api/download/software/sdks/"
46+
"Qualcomm_AI_Runtime_Community/All/2.34.0.250424/v2.34.0.250424.zip"
47+
)
48+
49+
# Top-level directory inside the SDK archive to extract.
50+
qairt_content_dir = "qairt/2.34.0.250424"
51+
52+
if not is_linux_x86():
53+
print("Skipping Qualcomm SDK (only supported on Linux x86).")
54+
return
55+
56+
SDK_DIR.mkdir(parents=True, exist_ok=True)
57+
58+
with tempfile.TemporaryDirectory() as tmpdir:
59+
archive_path = pathlib.Path(tmpdir) / pathlib.Path(qairt_url).name
60+
61+
print(f"Downloading Qualcomm SDK from {qairt_url}...")
62+
urllib.request.urlretrieve(qairt_url, archive_path)
63+
64+
if qairt_url.endswith(".zip"):
65+
_extract_zip(archive_path, qairt_content_dir, SDK_DIR)
66+
elif qairt_url.endswith((".tar.gz", ".tgz")):
67+
_extract_tar(archive_path, qairt_content_dir, SDK_DIR)
68+
else:
69+
raise ValueError(f"Unsupported archive format: {qairt_url}")
70+
71+
print(f"Qualcomm SDK extracted to {SDK_DIR}")
72+
73+
return SDK_DIR
74+
75+
76+
77+
78+
def _extract_zip(archive_path: pathlib.Path, prefix: str, target_dir: pathlib.Path):
79+
"""
80+
Extract files from a zip archive into target_dir, stripping a prefix.
81+
82+
Args:
83+
archive_path (pathlib.Path): Path to the .zip archive.
84+
prefix (str): Prefix folder inside the archive to strip.
85+
target_dir (pathlib.Path): Destination directory.
86+
"""
87+
with zipfile.ZipFile(archive_path, "r") as zf:
88+
for member in zf.infolist():
89+
if not member.filename.startswith(prefix + "/"):
90+
continue
91+
relpath = pathlib.Path(member.filename).relative_to(prefix)
92+
if not relpath.parts or relpath.parts[0] == "..":
93+
continue
94+
out_path = target_dir / relpath
95+
if member.is_dir():
96+
out_path.mkdir(parents=True, exist_ok=True)
97+
else:
98+
out_path.parent.mkdir(parents=True, exist_ok=True)
99+
with zf.open(member) as src, open(out_path, "wb") as dst:
100+
dst.write(src.read())
101+
102+
103+
def _extract_tar(archive_path: pathlib.Path, prefix: str, target_dir: pathlib.Path):
104+
"""
105+
Extract files from a tar.gz archive into target_dir, stripping a prefix.
106+
107+
Args:
108+
archive_path (pathlib.Path): Path to the .tar.gz or .tgz archive.
109+
prefix (str): Prefix folder inside the archive to strip.
110+
target_dir (pathlib.Path): Destination directory.
111+
"""
112+
with tarfile.open(archive_path, "r:gz") as tf:
113+
for m in tf.getmembers():
114+
if not m.name.startswith(prefix + "/"):
115+
continue
116+
relpath = pathlib.Path(m.name).relative_to(prefix)
117+
if not relpath.parts or relpath.parts[0] == "..":
118+
continue
119+
120+
out_path = target_dir / relpath
121+
if m.isdir():
122+
out_path.mkdir(parents=True, exist_ok=True)
123+
else:
124+
out_path.parent.mkdir(parents=True, exist_ok=True)
125+
src = tf.extractfile(m)
126+
if src is None:
127+
# Skip non-regular files (links, devices, etc.)
128+
continue
129+
with src, open(out_path, "wb") as dst:
130+
dst.write(src.read())

setup.py

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,14 @@
5656
# Import this before distutils so that setuptools can intercept the distuils
5757
# imports.
5858
import setuptools # noqa: F401 # usort: skip
59+
import os
60+
import platform
5961
import subprocess
62+
import sys
63+
import tarfile
64+
import tempfile
65+
import urllib.request
66+
import zipfile
6067

6168
from distutils import log # type: ignore[import-not-found]
6269
from distutils.sysconfig import get_python_lib # type: ignore[import-not-found]
@@ -67,6 +74,7 @@
6774
from setuptools.command.build import build
6875
from setuptools.command.build_ext import build_ext
6976
from setuptools.command.build_py import build_py
77+
from setuptools.command.install import install
7078

7179
try:
7280
from tools.cmake.cmake_cache import CMakeCache
@@ -455,6 +463,46 @@ def run(self):
455463
if self._ran_build:
456464
return
457465

466+
try:
467+
from backends.qualcomm.scripts.download_sdk import _download_qnn_sdk, SDK_DIR
468+
_download_qnn_sdk()
469+
470+
sdk_path = Path(SDK_DIR).resolve() # full absolute path
471+
except ImportError:
472+
sdk_path = None
473+
474+
if not sdk_path:
475+
raise RuntimeError("Qualcomm SDK not found, cannot build backend")
476+
477+
# Determine paths
478+
prj_root = Path(__file__).parent.resolve()
479+
build_sh = prj_root / "backends/qualcomm/scripts/build.sh"
480+
build_root = prj_root / "build-x86"
481+
482+
if not build_sh.exists():
483+
raise FileNotFoundError(f"{build_sh} not found")
484+
485+
# Run build.sh with SDK path exported
486+
env = dict(**os.environ)
487+
print("str(sdk_path): ", str(sdk_path))
488+
env["QNN_SDK_ROOT"] = str(sdk_path)
489+
subprocess.check_call([str(build_sh)], env=env)
490+
491+
# Copy the main .so into the wheel package
492+
so_src = build_root / "backends/qualcomm/libqnn_executorch_backend.so"
493+
so_dst = Path(self.get_ext_fullpath("executorch.backends.qualcomm.qnn_backend"))
494+
self.mkpath(so_dst.parent) # ensure destination exists
495+
self.copy_file(str(so_src), str(so_dst))
496+
print(f"Copied Qualcomm backend: {so_src} -> {so_dst}")
497+
498+
# Remove Qualcomm SDK .so so they don’t get packaged
499+
if os.path.exists(SDK_DIR):
500+
for root, dirs, files in os.walk(SDK_DIR):
501+
for f in files:
502+
if f.endswith(".so"):
503+
os.remove(os.path.join(root, f))
504+
print(f"Removed SDK .so from wheel package: {f}")
505+
458506
if self.editable_mode:
459507
self._ran_build = True
460508
self.run_command("build")
@@ -733,7 +781,7 @@ def run(self): # noqa C901
733781

734782
if cmake_cache.is_enabled("EXECUTORCH_BUILD_EXTENSION_MODULE"):
735783
cmake_build_args += ["--target", "extension_module"]
736-
784+
737785
if cmake_cache.is_enabled("EXECUTORCH_BUILD_EXTENSION_TRAINING"):
738786
cmake_build_args += ["--target", "_training_lib"]
739787

0 commit comments

Comments
 (0)