diff --git a/docker/qualcomm/cdi/install_cdi.py b/docker/qualcomm/cdi/install_cdi.py index 831d79f667..b701dc3716 100644 --- a/docker/qualcomm/cdi/install_cdi.py +++ b/docker/qualcomm/cdi/install_cdi.py @@ -2,8 +2,62 @@ import argparse import json import os +# The CDI descriptor is authored against the Qualcomm Linux BSP filesystem layout, +# where the QNN/HTP runtime libraries and the Hexagon "skel" files live under +# /usr/lib and /usr/lib/rfsa/adsp. On other officially-supported images for the +# same boards -- notably Ubuntu for RB3 Gen 2 / Rubik Pi 3 with the QAIRT apt +# packages (qairt-libs/qairt-tools) -- the same files are board-selected into a +# different directory, and /usr/lib/rfsa/adsp may even contain self-referential +# symlink loops. When a mount's authored hostPath is missing (or a broken/looping +# symlink), look the file up by name in these fallback directories and rewrite the +# mount to the real, resolved file so NPU offload works on both layouts. +FALLBACK_LIB_DIRS = [ + "/usr/lib/dsp/cdsp", # QAIRT apt packages (board-selected from /usr/share/qcom/.../dsp/cdsp) +] + +# Libraries without which HTP/NPU offload cannot work. If any of these cannot be +# resolved we warn prominently (and abort under --strict) instead of silently +# producing a CDI that starts fine but never offloads to the NPU. +CRITICAL_BASENAMES = { + "libQnnTFLiteDelegate.so", + "libQnnHtp.so", + "libQnnHtpV68Skel.so", + "libQnnHtpV73Skel.so", + "libQnnHtpV75Skel.so", + "libQnnSystem.so", +} + + +def resolve_host_path(host_path): + """Resolve a mount's hostPath to a usable, real file/dir on this host. + + Returns the original path when it already resolves (the Qualcomm Linux BSP + layout). If it is missing or a broken/looping symlink, search FALLBACK_LIB_DIRS + by basename and return the real (symlink-resolved) path, so the rewritten mount + is stable across reboots / apt reconfigures. Returns None if not found anywhere. + """ + # os.path.exists() follows symlinks and returns False for a broken link or an + # ELOOP ("too many levels of symbolic links") -- exactly the treat-as-missing + # behavior we want before falling back. + if os.path.exists(host_path): + return host_path + + basename = os.path.basename(host_path) + for fallback_dir in FALLBACK_LIB_DIRS: + candidate = os.path.join(fallback_dir, basename) + if candidate != host_path and os.path.exists(candidate): + return os.path.realpath(candidate) + + return None + + parser = argparse.ArgumentParser(description="Install CDI on Dragonwing IoT boards") parser.add_argument("--file", type=str, required=True, help="CDI input file") +parser.add_argument( + "--strict", + action="store_true", + help="Exit non-zero if any critical QNN/HTP library cannot be resolved", +) args, unknown = parser.parse_known_args() if os.path.exists("/etc/cdi/cdi-hw-acc.json"): @@ -32,18 +86,64 @@ else: with open(args.file, "r") as f: cdi = json.loads(f.read()) -print("Finding missing mount paths...") +print("Resolving mount paths...") +relocated = [] +missing = [] for device in cdi["devices"]: new_mounts = [] - for mount in device["containerEdits"]["mounts"]: - if not os.path.exists(mount["hostPath"]): - print(f" Missing {mount['hostPath']}") - else: - new_mounts.append(mount) + for mount in device["containerEdits"].get("mounts", []): + resolved = resolve_host_path(mount["hostPath"]) + + if resolved is None: + missing.append(mount["hostPath"]) + print(f" Missing {mount['hostPath']}") + continue + + if resolved != mount["hostPath"]: + relocated.append((mount["hostPath"], resolved)) + print(f" Resolved {mount['hostPath']} -> {resolved}") + # Keep containerPath as authored; only the host source moves. + mount["hostPath"] = resolved + + new_mounts.append(mount) device["containerEdits"]["mounts"] = new_mounts + # The QNN HTP backend (libcdsprpc) locates the Hexagon skel purely via + # ADSP_LIBRARY_PATH. Point it at the in-container directory where the skels are + # mounted so offload works regardless of the host's on-disk layout. + container_edits = device["containerEdits"] + env = container_edits.setdefault("env", []) + if not any(e.startswith("ADSP_LIBRARY_PATH=") for e in env): + env.append("ADSP_LIBRARY_PATH=/usr/lib/rfsa/adsp") + +print("") +if relocated: + print( + f"Resolved {len(relocated)} mount(s) from a fallback library directory " + "(non-BSP image layout)." + ) + +critical_missing = [m for m in missing if os.path.basename(m) in CRITICAL_BASENAMES] +if missing: + print(f"WARNING: {len(missing)} expected host path(s) were missing and skipped.") +if critical_missing: + print("") + print( + "WARNING: critical QNN/HTP libraries could not be found on this host -- NPU " + "offload will NOT work and detection would fall back to CPU:" + ) + for path in critical_missing: + print(f" {path}") + print( + "Confirm the QAIRT runtime is installed (e.g. the qairt-libs/qairt-tools " + "packages) and that the Hexagon skels exist as real files." + ) + if args.strict: + print("Aborting due to --strict.") + exit(1) + print("") print("Writing to /etc/cdi/cdi-hw-acc.json...") with open("/etc/cdi/cdi-hw-acc.json", "w") as f: diff --git a/docs/docs/frigate/installation.md b/docs/docs/frigate/installation.md index e075db4dff..246b8fa191 100644 --- a/docs/docs/frigate/installation.md +++ b/docs/docs/frigate/installation.md @@ -485,7 +485,9 @@ The Qualcomm integration uses [CDI (Container Device Interface)](https://docs.do #### Prerequisites -- **Qualcomm Linux BSP**: Your board must be running the official Qualcomm Linux Board Support Package image. The QNN runtime libraries (including `libQnnTFLiteDelegate.so` and `libQnnHtp.so`) and NPU device nodes (`/dev/fastrpc-cdsp`) are provided by the BSP and are bind-mounted into the container via CDI. +- **QNN runtime on the host**: The QNN runtime libraries (including `libQnnTFLiteDelegate.so` and `libQnnHtp.so`), the Hexagon "skel" files, and the NPU device nodes (`/dev/fastrpc-cdsp`) must be present on the host; they are bind-mounted into the container via CDI. Both officially-supported images for these boards provide them: + - **Qualcomm Linux BSP** — ships them under the layout the bundled CDI descriptors target (`/usr/lib`, `/usr/lib/rfsa/adsp`). + - **Ubuntu for RB3 Gen 2 / Rubik Pi 3** — install the `qairt-libs` and `qairt-tools` packages from the Qualcomm IoT PPA. These board-select the Hexagon skels into a different directory (`/usr/lib/dsp/cdsp`), and `/usr/lib/rfsa/adsp` may contain self-referential symlink loops; `install_cdi.py` detects this and rewrites the affected mounts to the real files automatically (see below). - **Docker 25.0+**: CDI device support requires Docker 25.0 or later. Check with `docker --version`. - **arm64 architecture**: The Qualcomm Docker image is built for `linux/arm64` only. If building locally, you must run the build on the target board itself. @@ -503,12 +505,13 @@ The Qualcomm integration uses [CDI (Container Device Interface)](https://docs.do sudo python3 install_cdi.py --file cdi-hw-acc-6490.json ``` - This writes a CDI descriptor to `/etc/cdi/cdi-hw-acc.json`. The script will automatically skip any host paths that do not exist on your system. + This writes a CDI descriptor to `/etc/cdi/cdi-hw-acc.json`. For any mount whose default (BSP) path is absent or a broken symlink, the script first tries to resolve the file from the QAIRT layout (e.g. `/usr/lib/dsp/cdsp`) and rewrites the mount to the real file; paths it cannot resolve anywhere are skipped with a warning. If a **critical** QNN/HTP library is missing it prints a prominent warning — and, with `--strict`, exits non-zero — so a misconfigured host fails at install time instead of silently falling back to CPU at runtime. -3. Verify CDI is set up by checking the file exists: +3. Verify the descriptor was written and that it contains the QNN delegate and Hexagon skel mounts (their absence means the QNN runtime is not installed on the host): ```bash ls -l /etc/cdi/cdi-hw-acc.json + grep -oE 'libQnn(TFLiteDelegate|HtpV[0-9]+Skel|System)\.so' /etc/cdi/cdi-hw-acc.json | sort -u ``` #### Setup