From 1838421de9e0940f06d4ed7ce215dcc60c9cdfde Mon Sep 17 00:00:00 2001 From: Abhinay Kukkadapu Date: Fri, 6 Mar 2026 18:41:44 -0800 Subject: [PATCH] Improve QNN backend build-from-source user experience MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove EXECUTORCH_BUILD_WHEEL_DO_NOT_USE gate in CMakeLists.txt so QNN SDK auto-downloads for all build paths (editable installs, build.sh), not just wheel builds - Use shared cache dir (~/.cache/executorch/qnn/) instead of cmake build dir, so SDK survives build dir cleans and is shared across all build flows - Remove hard QNN_SDK_ROOT requirement from build.sh — cmake handles auto-download during configure when SDK is not set - Auto-download Android NDK in build.sh via install_qnn_sdk.sh when ANDROID_NDK_ROOT is not set - Fix PYTHON_EXECUTABLE default in build.sh (was checked before being used, with a buggy test that never triggered) - Default EXECUTORCH_BUILD_QNN to ON on Linux x86 in pybind preset, matching wheel behavior so editable installs build QNN out of the box - Redirect download_qnn_sdk.py progress output to stderr when --print-sdk-path is used, so cmake can capture the path cleanly [ghstack-poisoned] --- backends/qualcomm/CMakeLists.txt | 71 ++++++++----------- backends/qualcomm/scripts/build.sh | 51 ++++++++----- backends/qualcomm/scripts/download_qnn_sdk.py | 16 ++++- tools/cmake/preset/pybind.cmake | 2 +- 4 files changed, 78 insertions(+), 62 deletions(-) diff --git a/backends/qualcomm/CMakeLists.txt b/backends/qualcomm/CMakeLists.txt index b468a3dbb49..b285f6c3795 100644 --- a/backends/qualcomm/CMakeLists.txt +++ b/backends/qualcomm/CMakeLists.txt @@ -33,53 +33,40 @@ if(NOT DEFINED QNN_SDK_ROOT AND DEFINED ENV{QNN_SDK_ROOT}) ) endif() -# Last-resort fallback: download during cmake configure when building wheels and -# QNN_SDK_ROOT was not provided externally. -if(NOT DEFINED QNN_SDK_ROOT AND EXECUTORCH_BUILD_WHEEL_DO_NOT_USE) - set(_qnn_default_sdk_dir "${CMAKE_CURRENT_BINARY_DIR}/sdk/qnn") - - if(EXISTS "${_qnn_default_sdk_dir}" AND EXISTS "${_qnn_default_sdk_dir}/lib") - message(STATUS "Found cached Qualcomm SDK at ${_qnn_default_sdk_dir}") - set(QNN_SDK_ROOT - ${_qnn_default_sdk_dir} - CACHE PATH "Qualcomm SDK root directory" FORCE - ) - else() - message(STATUS "Downloading Qualcomm SDK (fallback)") - execute_process( - COMMAND - ${PYTHON_EXECUTABLE} - ${EXECUTORCH_SOURCE_DIR}/backends/qualcomm/scripts/download_qnn_sdk.py - --dst-folder ${_qnn_default_sdk_dir} --print-sdk-path - WORKING_DIRECTORY ${EXECUTORCH_SOURCE_DIR} - RESULT_VARIABLE _qnn_sdk_download_result - OUTPUT_VARIABLE _qnn_sdk_download_output - ERROR_VARIABLE _qnn_sdk_download_error - OUTPUT_STRIP_TRAILING_WHITESPACE - ) - if(NOT _qnn_sdk_download_result EQUAL 0 OR _qnn_sdk_download_output - STREQUAL "" - ) - message( - FATAL_ERROR - "Failed to download Qualcomm SDK. stdout: ${_qnn_sdk_download_output}\n" - "stderr: ${_qnn_sdk_download_error}" - ) - endif() - set(QNN_SDK_ROOT - ${_qnn_sdk_download_output} - CACHE PATH "Qualcomm SDK root directory" FORCE +# Auto-download QNN SDK when QNN_SDK_ROOT was not provided externally. +# The SDK is cached in ~/.cache/executorch/qnn/ so it survives build dir cleans +# and is shared across editable installs, build.sh, and wheel builds. +if(NOT DEFINED QNN_SDK_ROOT) + message(STATUS "QNN_SDK_ROOT not set. Auto-downloading QNN SDK...") + execute_process( + COMMAND + ${PYTHON_EXECUTABLE} + ${EXECUTORCH_SOURCE_DIR}/backends/qualcomm/scripts/download_qnn_sdk.py + --print-sdk-path + WORKING_DIRECTORY ${EXECUTORCH_SOURCE_DIR} + RESULT_VARIABLE _qnn_sdk_download_result + OUTPUT_VARIABLE _qnn_sdk_download_output + ERROR_VARIABLE _qnn_sdk_download_error + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + if(NOT _qnn_sdk_download_result EQUAL 0 OR _qnn_sdk_download_output + STREQUAL "" + ) + message( + FATAL_ERROR + "Failed to download Qualcomm SDK.\n" + "stderr: ${_qnn_sdk_download_error}\n" + "Set QNN_SDK_ROOT manually, e.g. cmake <..> -DQNN_SDK_ROOT=<...>" ) endif() + set(QNN_SDK_ROOT + ${_qnn_sdk_download_output} + CACHE PATH "Qualcomm SDK root directory" FORCE + ) set(ENV{QNN_SDK_ROOT} ${QNN_SDK_ROOT}) endif() -if(NOT DEFINED QNN_SDK_ROOT) - message( - FATAL_ERROR - "Please define QNN_SDK_ROOT, e.g. cmake <..> -DQNN_SDK_ROOT=<...>" - ) -elseif(CMAKE_TOOLCHAIN_FILE MATCHES ".*(iOS|ios\.toolchain)\.cmake$") +if(CMAKE_TOOLCHAIN_FILE MATCHES ".*(iOS|ios\.toolchain)\.cmake$") message(FATAL_ERROR "ios is not supported by Qualcomm AI Engine Direct") endif() diff --git a/backends/qualcomm/scripts/build.sh b/backends/qualcomm/scripts/build.sh index b8f366d2f7c..6e6c4bb6aba 100755 --- a/backends/qualcomm/scripts/build.sh +++ b/backends/qualcomm/scripts/build.sh @@ -16,21 +16,36 @@ if [[ "$(uname -s)" == "Darwin" ]]; then exit 1 fi -if [[ -z ${QNN_SDK_ROOT} ]]; then - echo "Please export QNN_SDK_ROOT=/path/to/qnn_sdk" - exit -1 +SCRIPT_DIR="$( cd "$(dirname "$0")" ; pwd -P)" + +if [ -z "$PYTHON_EXECUTABLE" ]; then + PYTHON_EXECUTABLE="python3" fi +# If QNN_SDK_ROOT is set, pass it to cmake. Otherwise cmake will +# auto-download the SDK via download_qnn_sdk.py during configure. +if [[ -n ${QNN_SDK_ROOT} ]]; then + QNN_SDK_CMAKE_FLAG="-DQNN_SDK_ROOT=${QNN_SDK_ROOT}" + + # Ensure LD_LIBRARY_PATH includes QNN SDK libs + QNN_LIB_DIR="${QNN_SDK_ROOT}/lib/x86_64-linux-clang" + if [[ -d "${QNN_LIB_DIR}" ]] && [[ ":${LD_LIBRARY_PATH:-}:" != *":${QNN_LIB_DIR}:"* ]]; then + export LD_LIBRARY_PATH="${QNN_LIB_DIR}:${LD_LIBRARY_PATH:-}" + fi +else + QNN_SDK_CMAKE_FLAG="" + echo "[QNN] QNN_SDK_ROOT not set. SDK will be auto-downloaded during cmake configure." +fi set -o xtrace usage() { echo "Usage: Build the aarch64 version of executor runner or the python interface of Qnn Manager" - echo "First, you need to set the environment variable for QNN_SDK_ROOT" - echo ", and if you want to build the android version of executor runner" - echo ", you need to export ANDROID_NDK_ROOT=/path/to/android_ndkXX" - echo "(or export TOOLCHAIN_ROOT_HOST=/path/to/sysroots/xx_host, " - echo "TOOLCHAIN_ROOT_TARGET=/path/to/sysroots/xx_target for linux embedded with --enable_linux_embedded)" + echo "" + echo "QNN SDK and Android NDK will be auto-downloaded if not set." + echo "To use a custom SDK, export QNN_SDK_ROOT=/path/to/qnn_sdk" + echo "To use a custom NDK, export ANDROID_NDK_ROOT=/path/to/android_ndkXX" + echo "" echo "e.g.: executorch$ ./backends/qualcomm/scripts/build.sh --skip_x86_64" exit 1 } @@ -48,10 +63,6 @@ CLEAN="true" BUILD_TYPE="RelWithDebInfo" BUILD_JOB_NUMBER="16" -if [ -z PYTHON_EXECUTABLE ]; then - PYTHON_EXECUTABLE="python3" -fi - if [ -z BUCK2 ]; then BUCK2="buck2" fi @@ -79,8 +90,14 @@ PRJ_ROOT="$( cd "$(dirname "$0")/../../.." ; pwd -P)" if [ "$BUILD_ANDROID" = true ]; then if [[ -z ${ANDROID_NDK_ROOT} ]]; then - echo "Please export ANDROID_NDK_ROOT=/path/to/android_ndkXX" - exit -1 + echo "[QNN] ANDROID_NDK_ROOT not set. Auto-downloading Android NDK..." + source "${SCRIPT_DIR}/install_qnn_sdk.sh" + setup_android_ndk + if [[ -z ${ANDROID_NDK_ROOT} ]]; then + echo "[QNN] Error: Failed to download Android NDK." + echo "[QNN] Set ANDROID_NDK_ROOT manually." + exit 1 + fi fi BUILD_ROOT=$PRJ_ROOT/$CMAKE_ANDROID @@ -106,7 +123,7 @@ if [ "$BUILD_ANDROID" = true ]; then -DEXECUTORCH_BUILD_EXTENSION_TENSOR=ON \ -DEXECUTORCH_ENABLE_EVENT_TRACER=ON \ -DEXECUTORCH_ENABLE_LOGGING=ON \ - -DQNN_SDK_ROOT=$QNN_SDK_ROOT \ + ${QNN_SDK_CMAKE_FLAG} \ -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK_ROOT/build/cmake/android.toolchain.cmake \ -DANDROID_ABI='arm64-v8a' \ -DEXECUTORCH_BUILD_KERNELS_QUANTIZED=ON \ @@ -191,7 +208,7 @@ if [ "$BUILD_OE_LINUX" = true ]; then -DEXECUTORCH_BUILD_EXTENSION_TENSOR=ON \ -DEXECUTORCH_ENABLE_EVENT_TRACER=ON \ -DEXECUTORCH_ENABLE_LOGGING=ON \ - -DQNN_SDK_ROOT=$QNN_SDK_ROOT \ + ${QNN_SDK_CMAKE_FLAG} \ -DEXECUTORCH_BUILD_KERNELS_QUANTIZED=ON \ -DPYTHON_EXECUTABLE=$PYTHON_EXECUTABLE \ -B$BUILD_ROOT @@ -252,7 +269,7 @@ if [ "$BUILD_X86_64" = true ]; then cmake \ -DCMAKE_BUILD_TYPE=$BUILD_TYPE \ -DCMAKE_INSTALL_PREFIX=$BUILD_ROOT \ - -DQNN_SDK_ROOT=${QNN_SDK_ROOT} \ + ${QNN_SDK_CMAKE_FLAG} \ -DEXECUTORCH_BUILD_QNN=ON \ -DEXECUTORCH_BUILD_DEVTOOLS=ON \ -DEXECUTORCH_BUILD_EXTENSION_LLM=ON \ diff --git a/backends/qualcomm/scripts/download_qnn_sdk.py b/backends/qualcomm/scripts/download_qnn_sdk.py index b9b55e513b9..96134b6173a 100644 --- a/backends/qualcomm/scripts/download_qnn_sdk.py +++ b/backends/qualcomm/scripts/download_qnn_sdk.py @@ -26,15 +26,19 @@ PKG_ROOT = pathlib.Path(__file__).parent.parent +# Output stream for progress messages. Defaults to stdout, but redirected to +# stderr when --print-sdk-path is used (so stdout only contains the path). +_output_stream = sys.stdout + def _progress(msg: str) -> None: """Print a progress line with carriage return (no newline). Not suited for logging.""" - print(msg, end="", flush=True) + print(msg, end="", flush=True, file=_output_stream) def _progress_newline() -> None: """End a progress line.""" - print(flush=True) + print(flush=True, file=_output_stream) ########################## @@ -758,6 +762,14 @@ def main(argv: Optional[List[str]] = None) -> int: ) args = parser.parse_args(argv) + # When --print-sdk-path is used, stdout must contain ONLY the SDK path. + # Redirect all logger and progress output to stderr. + if args.print_sdk_path: + global _output_stream + _output_stream = sys.stderr + for handler in logger.handlers: + handler.stream = sys.stderr + logging.basicConfig(level=logging.INFO) dst = args.dst_folder if args.dst_folder else _get_sdk_dir() diff --git a/tools/cmake/preset/pybind.cmake b/tools/cmake/preset/pybind.cmake index 699a7c50358..a0d06d74d17 100644 --- a/tools/cmake/preset/pybind.cmake +++ b/tools/cmake/preset/pybind.cmake @@ -37,7 +37,7 @@ elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") set_overridable_option(EXECUTORCH_BUILD_EXTENSION_LLM_RUNNER ON) set_overridable_option(EXECUTORCH_BUILD_EXTENSION_LLM ON) if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|amd64|i.86)$") - set_overridable_option(EXECUTORCH_BUILD_QNN OFF) + set_overridable_option(EXECUTORCH_BUILD_QNN ON) endif() elseif(CMAKE_SYSTEM_NAME STREQUAL "Windows" OR CMAKE_SYSTEM_NAME STREQUAL "WIN32"