diff --git a/backends/qualcomm/CMakeLists.txt b/backends/qualcomm/CMakeLists.txt index e93731ec34e..c1f96b1df14 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 52fbfb50eb1..ac84b261f33 100755 --- a/backends/qualcomm/scripts/build.sh +++ b/backends/qualcomm/scripts/build.sh @@ -16,21 +16,34 @@ 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 -fi +SCRIPT_DIR="$( cd "$(dirname "$0")" ; pwd -P)" + +# 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 "" + 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 "(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 "e.g.: executorch$ ./backends/qualcomm/scripts/build.sh --skip_x86_64" exit 1 } @@ -51,13 +64,13 @@ BUILD_TYPE="RelWithDebInfo" BUILD_JOB_NUMBER="16" # Default to use CDSP for now -DSP_TYPE=3 +DSP_TYPE=3 -if [ -z PYTHON_EXECUTABLE ]; then +if [ -z "$PYTHON_EXECUTABLE" ]; then PYTHON_EXECUTABLE="python3" fi -if [ -z BUCK2 ]; then +if [ -z "$BUCK2" ]; then BUCK2="buck2" fi @@ -85,8 +98,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 @@ -112,7 +131,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 \ @@ -126,7 +145,7 @@ if [ "$BUILD_ANDROID" = true ]; then CMAKE_PREFIX_PATH="${BUILD_ROOT};${BUILD_ROOT}/third-party/gflags;" # DSP_TYPE variable only matters when building direct_mode. - # Ignore the variable for traditional mode. + # Ignore the variable for traditional mode. if [ "$BUILD_HEXAGON" = "true" ]; then DIRECT_MODE_FLAG="-DBUILD_DIRECT_MODE=ON" @@ -274,7 +293,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 @@ -335,7 +354,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 7117ef80a9b..896d96b0cb7 100644 --- a/backends/qualcomm/scripts/download_qnn_sdk.py +++ b/backends/qualcomm/scripts/download_qnn_sdk.py @@ -23,18 +23,23 @@ _handler.setFormatter(logging.Formatter("%(message)s")) logger.addHandler(_handler) logger.setLevel(logging.INFO) + logger.propagate = False 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) ########################## @@ -317,6 +322,10 @@ def _download_qnn_sdk( logger.info("[QNN] Skipping Qualcomm SDK (only supported on Linux x86).") return None + if dst_folder.exists() and any(dst_folder.iterdir()): + logger.info(f"[QNN] Using cached QNN SDK v{QNN_VERSION} at {dst_folder}") + return dst_folder + logger.info(f"[QNN] Downloading Qualcomm AI Runtime SDK v{QNN_VERSION}...") dst_folder.mkdir(parents=True, exist_ok=True) @@ -761,6 +770,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"