Skip to content

Commit 19be2a3

Browse files
authored
Try to get nightly wheel build work with qnn (#14633)
Our current nightly/release wheel package is done following https://github.com/pytorch/test-infra/wiki/Using-Nova-Reusable-Build-Workflows As described by https://github.com/pytorch/test-infra/blob/5398e1a00c39939f43251f29031c37e6d0c84647/.github/workflows/build_wheels_linux.yml#L4, The docker image infra team used to release nightly/release package is from https://github.com/pypa/manylinux, and it's currently using https://github.com/pypa/manylinux?tab=readme-ov-file#manylinux_2_28-almalinux-8-based. It means the glibc version is 2.28 and GCC is 14. The issue is that, QNN .so files are not compatible with 2.28. The minimum version is 2.34 (I tried 2.29 the first time when it failed and asked for 2.29, but it still fails). In this PR, instead of checking glibc and failed directly when minimum version isn't matched, we will download the glibc 2.34 to /tmp. A different strategy compared with glibc++ is that, we don't load them, because the python process itself start with the system glibc 2.28. We need to re-execute the process with the new glibc
1 parent d95143e commit 19be2a3

File tree

3 files changed

+218
-79
lines changed

3 files changed

+218
-79
lines changed

backends/qualcomm/__init__.py

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,13 @@
11
import os
22

3-
from .scripts.download_qnn_sdk import (
4-
check_glibc_exist_and_validate,
5-
install_qnn_sdk,
6-
is_linux_x86,
7-
)
3+
from .scripts.download_qnn_sdk import install_qnn_sdk, is_linux_x86
84

95

106
env_flag = os.getenv("EXECUTORCH_BUILDING_WHEEL", "0").lower()
117
# If users have preinstalled QNN_SDK_ROOT, we will use it.
128
qnn_sdk_root_flag = os.getenv("QNN_SDK_ROOT", None)
139

14-
if (
15-
env_flag not in ("1", "true", "yes")
16-
and not qnn_sdk_root_flag
17-
and is_linux_x86()
18-
and check_glibc_exist_and_validate()
19-
):
10+
if env_flag not in ("1", "true", "yes") and not qnn_sdk_root_flag and is_linux_x86():
2011
ok = install_qnn_sdk()
21-
2212
if not ok:
2313
raise RuntimeError("Failed to install QNN SDK. Please check the logs above.")

backends/qualcomm/scripts/download_qnn_sdk.py

Lines changed: 215 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,15 @@
66
import platform
77
import re
88
import shutil
9+
import subprocess
10+
import sys
911
import tarfile
1012
import tempfile
1113
import urllib.request
1214
import zipfile
1315
from typing import Dict, List, Optional, Tuple
1416

17+
1518
logger = logging.getLogger(__name__)
1619
logger.addHandler(logging.NullHandler())
1720

@@ -34,68 +37,81 @@ def is_linux_x86() -> bool:
3437
)
3538

3639

37-
import subprocess
40+
#########################
41+
# Cache directory helper
42+
#########################
3843

39-
MINIMUM_LIBC_VERSION = 2.29
44+
APP_NAMESPACE = ["executorch", "qnn"]
4045

41-
REQUIRED_LIBC_LIBS = [
42-
"/lib/x86_64-linux-gnu/libc.so.6",
43-
"/lib64/libc.so.6",
44-
"/lib/libc.so.6",
45-
]
4646

47+
def _get_staging_dir(*parts: str) -> pathlib.Path:
48+
r"""
49+
Return a cross-platform staging directory for staging SDKs/libraries.
50+
51+
- On Linux:
52+
~/.cache/executorch/qnn/<parts...>
53+
(falls back to $HOME/.cache if $XDG_CACHE_HOME is unset)
4754
48-
def check_glibc_exist_and_validate() -> bool:
55+
- On Windows (not supported yet, but as placeholder):
56+
%LOCALAPPDATA%\executorch\qnn\<parts...>
57+
(falls back to $HOME/AppData/Local if %LOCALAPPDATA% is unset)
58+
59+
- Override:
60+
If QNN_STAGING_DIR is set in the environment, that path is used instead.
61+
62+
Args:
63+
parts (str): Subdirectories to append under the root staging dir.
64+
65+
Returns:
66+
pathlib.Path: Fully qualified staging path.
4967
"""
50-
Check if users have glibc installed.
68+
# Environment override wins
69+
base = os.environ.get("QNN_STAGING_DIR")
70+
if base:
71+
return pathlib.Path(base).joinpath(*parts)
72+
73+
system = platform.system().lower()
74+
if system == "windows":
75+
# On Windows, prefer %LOCALAPPDATA%, fallback to ~/AppData/Local
76+
base = pathlib.Path(
77+
os.environ.get("LOCALAPPDATA", pathlib.Path.home() / "AppData" / "Local")
78+
)
79+
elif is_linux_x86():
80+
# On Linux/Unix, prefer $XDG_CACHE_HOME, fallback to ~/.cache
81+
base = pathlib.Path(
82+
os.environ.get("XDG_CACHE_HOME", pathlib.Path.home() / ".cache")
83+
)
84+
else:
85+
raise ValueError(f"Unsupported platform: {system}")
86+
87+
return base.joinpath(*APP_NAMESPACE, *parts)
88+
89+
90+
def _atomic_download(url: str, dest: pathlib.Path):
5191
"""
52-
exists = False
53-
for path in REQUIRED_LIBC_LIBS:
54-
try:
55-
output = subprocess.check_output(
56-
[path, "--version"], stderr=subprocess.STDOUT
57-
)
58-
output = output.decode().split("\n")[0]
59-
logger.debug(f"[QNN] glibc version for path {path} is: {output}")
60-
match = re.search(r"version (\d+\.\d+)", output)
61-
if match:
62-
version = match.group(1)
63-
if float(version) >= MINIMUM_LIBC_VERSION:
64-
logger.debug(f"[QNN] glibc version is {version}.")
65-
exists = True
66-
return True
67-
else:
68-
logger.error(
69-
f"[QNN] glibc version is too low. The minimum libc version is {MINIMUM_LIBC_VERSION} Please install glibc following the commands below."
70-
)
71-
else:
72-
logger.error("[QNN] glibc version not found.")
92+
Download URL into dest atomically:
93+
- Write to a temp file in the same dir
94+
- Move into place if successful
95+
"""
96+
dest.parent.mkdir(parents=True, exist_ok=True)
7397

74-
except Exception:
75-
continue
98+
# Temp file in same dir (guarantees atomic rename)
99+
with tempfile.NamedTemporaryFile(dir=dest.parent, delete=False) as tmp:
100+
tmp_path = pathlib.Path(tmp.name)
76101

77-
if not exists:
78-
logger.error(
79-
r""""
80-
[QNN] glibc not found or the version is too low. Please install glibc following the commands below.
81-
Ubuntu/Debian:
82-
sudo apt update
83-
sudo apt install libc6
84-
85-
Fedora/Red Hat:
86-
sudo dnf install glibc
87-
88-
Arch Linux:
89-
sudo pacman -S glibc
90-
91-
Also please make sure the glibc version is >= MINIMUM_LIBC_VERSION. You can verify the glibc version by running the following command:
92-
Option 1:
93-
ldd --version
94-
Option 2:
95-
/path/to/libc.so.6 --version
96-
"""
97-
)
98-
return exists
102+
try:
103+
urllib.request.urlretrieve(url, tmp_path)
104+
tmp_path.replace(dest) # atomic rename
105+
except Exception:
106+
# Clean up partial file on failure
107+
if tmp_path.exists():
108+
tmp_path.unlink(missing_ok=True)
109+
raise
110+
111+
112+
####################
113+
# qnn sdk download management
114+
####################
99115

100116

101117
def _download_archive(url: str, archive_path: pathlib.Path) -> bool:
@@ -178,9 +194,6 @@ def _download_qnn_sdk(dst_folder=SDK_DIR) -> Optional[pathlib.Path]:
178194
if not is_linux_x86():
179195
logger.info("[QNN] Skipping Qualcomm SDK (only supported on Linux x86).")
180196
return None
181-
elif not check_glibc_exist_and_validate():
182-
logger.info("[QNN] Skipping Qualcomm SDK (glibc not found or version too old).")
183-
return None
184197
else:
185198
logger.info("[QNN] Downloading Qualcomm SDK for Linux x86")
186199

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

243256

257+
####################
258+
# libc management
259+
####################
260+
261+
GLIBC_VERSION = "2.34"
262+
GLIBC_REEXEC_GUARD = "QNN_GLIBC_REEXEC"
263+
MINIMUM_LIBC_VERSION = GLIBC_VERSION
264+
265+
266+
def _get_glibc_libdir() -> pathlib.Path:
267+
glibc_root = _get_staging_dir(f"glibc-{GLIBC_VERSION}")
268+
return glibc_root / "lib"
269+
270+
271+
def _parse_version(v: str) -> tuple[int, int]:
272+
"""Turn '2.34' → (2,34) so it can be compared."""
273+
parts = v.split(".")
274+
return int(parts[0]), int(parts[1]) if len(parts) > 1 else 0
275+
276+
277+
def _current_glibc_version() -> str:
278+
"""Return system glibc version string (via ctypes)."""
279+
try:
280+
libc = ctypes.CDLL("libc.so.6")
281+
func = libc.gnu_get_libc_version
282+
func.restype = ctypes.c_char_p
283+
return func().decode()
284+
except Exception as e:
285+
return f"error:{e}"
286+
287+
288+
def _resolve_glibc_loader() -> pathlib.Path | None:
289+
"""Return staged ld.so path if available."""
290+
for p in [
291+
_get_glibc_libdir() / f"ld-{GLIBC_VERSION}.so",
292+
_get_glibc_libdir() / "ld-linux-x86-64.so.2",
293+
]:
294+
if p.exists():
295+
return p
296+
return None
297+
298+
299+
def _stage_prebuilt_glibc():
300+
"""Download + extract Fedora 35 glibc RPM into /tmp."""
301+
logger.info(">>> Staging prebuilt glibc-%s from Fedora 35 RPM", GLIBC_VERSION)
302+
_get_glibc_libdir().mkdir(parents=True, exist_ok=True)
303+
rpm_path = _get_staging_dir("glibc") / "glibc.rpm"
304+
work_dir = _get_staging_dir("glibc") / "extracted"
305+
rpm_url = (
306+
"https://archives.fedoraproject.org/pub/archive/fedora/linux/releases/35/"
307+
"Everything/x86_64/os/Packages/g/glibc-2.34-7.fc35.x86_64.rpm"
308+
)
309+
310+
rpm_path.parent.mkdir(parents=True, exist_ok=True)
311+
logger.info("[glibc] Downloading %s -> %s", rpm_url, rpm_path)
312+
try:
313+
urllib.request.urlretrieve(rpm_url, rpm_path)
314+
except Exception as e:
315+
logger.error("[glibc] Failed to download %s: %s", rpm_url, e)
316+
raise
317+
318+
# Extract
319+
if work_dir.exists():
320+
shutil.rmtree(work_dir)
321+
work_dir.mkdir(parents=True)
322+
subprocess.check_call(["bsdtar", "-C", str(work_dir), "-xf", str(rpm_path)])
323+
324+
# Copy runtime libs
325+
staged = [
326+
"ld-linux-x86-64.so.2",
327+
"libc.so.6",
328+
"libdl.so.2",
329+
"libpthread.so.0",
330+
"librt.so.1",
331+
"libm.so.6",
332+
"libutil.so.1",
333+
]
334+
for lib in staged:
335+
src = work_dir / "lib64" / lib
336+
if src.exists():
337+
shutil.copy2(src, _get_glibc_libdir() / lib)
338+
logger.info("[glibc] Staged %s", lib)
339+
else:
340+
logger.warning("[glibc] Missing %s in RPM", lib)
341+
342+
343+
def ensure_glibc_minimum(min_version: str = GLIBC_VERSION):
344+
"""
345+
Ensure process runs under glibc >= min_version.
346+
- If system glibc is new enough → skip.
347+
- Else → stage Fedora RPM and re-exec under staged loader.
348+
"""
349+
current = _current_glibc_version()
350+
logger.info("[glibc] Current loaded glibc: %s", current)
351+
352+
# If system glibc already sufficient → skip everything
353+
m = re.match(r"(\d+\.\d+)", current)
354+
if m and _parse_version(m.group(1)) >= _parse_version(min_version):
355+
logger.info("[glibc] System glibc >= %s, no staging needed.", min_version)
356+
return
357+
358+
# Avoid infinite loop
359+
if os.environ.get(GLIBC_REEXEC_GUARD) == "1":
360+
logger.info("[glibc] Already re-exec'd once, continuing.")
361+
return
362+
363+
# Stage prebuilt if not already staged
364+
if not (_get_glibc_libdir() / "libc.so.6").exists():
365+
_stage_prebuilt_glibc()
366+
367+
loader = _resolve_glibc_loader()
368+
if not loader:
369+
logger.error("[glibc] Loader not found in %s", _get_glibc_libdir())
370+
return
371+
372+
logger.info(
373+
"[glibc] Re-execing under loader %s with libdir %s", loader, _get_glibc_libdir()
374+
)
375+
os.environ[GLIBC_REEXEC_GUARD] = "1"
376+
os.execv(
377+
str(loader),
378+
[str(loader), "--library-path", str(_get_glibc_libdir()), sys.executable]
379+
+ sys.argv,
380+
)
381+
382+
383+
####################
384+
# libc++ management
385+
####################
386+
244387
LLVM_VERSION = "14.0.0"
245388
LIBCXX_BASE_NAME = f"clang+llvm-{LLVM_VERSION}-x86_64-linux-gnu-ubuntu-18.04"
246389
LLVM_URL = f"https://github.com/llvm/llvm-project/releases/download/llvmorg-{LLVM_VERSION}/{LIBCXX_BASE_NAME}.tar.xz"
@@ -258,12 +401,17 @@ def _stage_libcxx(target_dir: pathlib.Path):
258401
logger.info("[libcxx] Already staged at %s, skipping download", target_dir)
259402
return
260403

261-
temp_tar = pathlib.Path("/tmp") / f"{LIBCXX_BASE_NAME}.tar.xz"
262-
temp_extract = pathlib.Path("/tmp") / LIBCXX_BASE_NAME
404+
libcxx_stage = _get_staging_dir(f"libcxx-{LLVM_VERSION}")
405+
temp_tar = libcxx_stage / f"{LIBCXX_BASE_NAME}.tar.xz"
406+
temp_extract = libcxx_stage / LIBCXX_BASE_NAME
263407

264408
if not temp_tar.exists():
265409
logger.info("[libcxx] Downloading %s", LLVM_URL)
266-
urllib.request.urlretrieve(LLVM_URL, temp_tar)
410+
_atomic_download(LLVM_URL, temp_tar)
411+
412+
# Sanity check before extracting
413+
if not temp_tar.exists() or temp_tar.stat().st_size == 0:
414+
raise FileNotFoundError(f"[libcxx] Tarball missing or empty: {temp_tar}")
267415

268416
logger.info("[libcxx] Extracting %s", temp_tar)
269417
with tarfile.open(temp_tar, "r:xz") as tar:
@@ -437,8 +585,10 @@ def install_qnn_sdk() -> bool:
437585
Returns:
438586
True if both steps succeeded (or were already satisfied), else False.
439587
"""
440-
if check_glibc_exist_and_validate():
441-
if _ensure_libcxx_stack():
442-
if _ensure_qnn_sdk_lib():
443-
return True
444-
return False
588+
logger.info("[QNN] Starting SDK installation")
589+
590+
# Make sure we’re running under >= 2.34
591+
ensure_glibc_minimum(GLIBC_VERSION)
592+
593+
# libc++ and QNN SDK setup
594+
return _ensure_libcxx_stack() and _ensure_qnn_sdk_lib()

setup.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -467,11 +467,10 @@ def run(self):
467467
# Following code is for building the Qualcomm backend.
468468
from backends.qualcomm.scripts.download_qnn_sdk import (
469469
_download_qnn_sdk,
470-
check_glibc_exist_and_validate,
471470
is_linux_x86,
472471
)
473472

474-
if is_linux_x86() and check_glibc_exist_and_validate():
473+
if is_linux_x86():
475474
os.environ["EXECUTORCH_BUILDING_WHEEL"] = "1"
476475

477476
with tempfile.TemporaryDirectory() as tmpdir:

0 commit comments

Comments
 (0)