diff --git a/CMakeLists.txt b/CMakeLists.txt index 51573d276b3..536c10e8b59 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -801,6 +801,9 @@ if(EXECUTORCH_BUILD_PYBIND) torch ) + # RPATH for _portable_lib.so + set(_portable_lib_rpath "$ORIGIN/../../../torch/lib") + if(EXECUTORCH_BUILD_EXTENSION_MODULE) # Always use static linking for pybindings to avoid runtime symbol # resolution issues @@ -835,6 +838,7 @@ if(EXECUTORCH_BUILD_PYBIND) if(EXECUTORCH_BUILD_QNN) list(APPEND _dep_libs qnn_executorch_backend) + string(APPEND _portable_lib_rpath ":$ORIGIN/../../backends/qualcomm") endif() if(EXECUTORCH_BUILD_ENN) @@ -886,10 +890,11 @@ if(EXECUTORCH_BUILD_PYBIND) target_compile_options(portable_lib PUBLIC ${_pybind_compile_options}) target_link_libraries(portable_lib PRIVATE ${_dep_libs}) - # Set RPATH to find PyTorch libraries relative to the installation location - # This goes from executorch/extension/pybindings up to site-packages, then to - # torch/lib. Don't do this to APPLE, as it will error out on the following - # error: + # Set RPATH to find PyTorch and backend libraries relative to the installation + # location. This goes from executorch/extension/pybindings up to + # site-packages, then to torch/lib. If QNN is enabled, also add + # backends/qualcomm/. Don't do this to APPLE, as it will error out on the + # following error: # if(APPLE) # Skip setting @loader_path for APPLE, since it causes error like ld: @@ -897,8 +902,8 @@ if(EXECUTORCH_BUILD_PYBIND) # libtorch_cpu.dylib' else() set_target_properties( - portable_lib PROPERTIES BUILD_RPATH "$ORIGIN/../../../torch/lib" - INSTALL_RPATH "$ORIGIN/../../../torch/lib" + portable_lib PROPERTIES BUILD_RPATH "${_portable_lib_rpath}" + INSTALL_RPATH "${_portable_lib_rpath}" ) endif() diff --git a/backends/qualcomm/CMakeLists.txt b/backends/qualcomm/CMakeLists.txt index 07166b92ea2..cc7957dfdbe 100644 --- a/backends/qualcomm/CMakeLists.txt +++ b/backends/qualcomm/CMakeLists.txt @@ -23,6 +23,47 @@ get_filename_component( _common_include_directories "${EXECUTORCH_SOURCE_DIR}/.." ABSOLUTE ) +# We only download QNN SDK when we build pip wheel for ExecuTorch. Please don't +# change this code unless you know what you are doing. +if(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") + 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 + ) + endif() + set(ENV{QNN_SDK_ROOT} ${QNN_SDK_ROOT}) +endif() + if(NOT DEFINED QNN_SDK_ROOT) message( FATAL_ERROR @@ -214,7 +255,9 @@ add_subdirectory( install( TARGETS qnn_executorch_backend EXPORT ExecuTorchTargets - DESTINATION ${CMAKE_INSTALL_LIBDIR} + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}/executorch/backends/qualcomm + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}/executorch/backends/qualcomm + RUNTIME DESTINATION ${CMAKE_INSTALL_LIBDIR}/executorch/backends/qualcomm ) # QNN pybind @@ -275,4 +318,12 @@ if(${CMAKE_SYSTEM_PROCESSOR} MATCHES "x86_64") ${QNN_EXECUTORCH_ROOT_DIR}/aot/python ${CMAKE_CURRENT_BINARY_DIR}/qnn_executorch/python ) + + install( + TARGETS PyQnnManagerAdaptor PyQnnWrapperAdaptor + LIBRARY + DESTINATION ${CMAKE_INSTALL_LIBDIR}/executorch/backends/qualcomm/python + RUNTIME + DESTINATION ${CMAKE_INSTALL_LIBDIR}/executorch/backends/qualcomm/python + ) endif() diff --git a/backends/qualcomm/scripts/download_qnn_sdk.py b/backends/qualcomm/scripts/download_qnn_sdk.py index 747524a0e5b..1bd844e2288 100644 --- a/backends/qualcomm/scripts/download_qnn_sdk.py +++ b/backends/qualcomm/scripts/download_qnn_sdk.py @@ -1,4 +1,4 @@ -# Add these imports for additional logging +import argparse import ctypes import logging import os @@ -592,3 +592,46 @@ def install_qnn_sdk() -> bool: # libc++ and QNN SDK setup return _ensure_libcxx_stack() and _ensure_qnn_sdk_lib() + + +def main(argv: Optional[List[str]] = None) -> int: + parser = argparse.ArgumentParser( + description="Helper utility for Qualcomm SDK staging." + ) + parser.add_argument( + "--dst-folder", + type=pathlib.Path, + default=SDK_DIR, + help="Destination directory for the Qualcomm SDK.", + ) + parser.add_argument( + "--print-sdk-path", + action="store_true", + help="Print the resolved Qualcomm SDK path to stdout.", + ) + parser.add_argument( + "--install-sdk", + action="store_true", + help="Ensure the SDK and runtime libraries are staged and loaded.", + ) + args = parser.parse_args(argv) + + logging.basicConfig(level=logging.INFO) + + sdk_path: Optional[pathlib.Path] + if args.install_sdk: + if not install_qnn_sdk(): + return 1 + sdk_path = pathlib.Path(os.environ.get("QNN_SDK_ROOT", args.dst_folder)) + else: + sdk_path = _download_qnn_sdk(dst_folder=args.dst_folder) + if sdk_path is None: + return 1 + + if args.print_sdk_path and sdk_path is not None: + print(sdk_path) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/setup.py b/setup.py index 8e75ea47cd6..71aa4c543d4 100644 --- a/setup.py +++ b/setup.py @@ -57,8 +57,6 @@ import site import subprocess import sys -import sysconfig -import tempfile from distutils import log # type: ignore[import-not-found] from distutils.sysconfig import get_python_lib # type: ignore[import-not-found] @@ -463,84 +461,6 @@ def run(self): if self._ran_build: return - try: - # Following code is for building the Qualcomm backend. - from backends.qualcomm.scripts.download_qnn_sdk import ( - _download_qnn_sdk, - is_linux_x86, - ) - - if is_linux_x86(): - os.environ["EXECUTORCH_BUILDING_WHEEL"] = "1" - - with tempfile.TemporaryDirectory() as tmpdir: - tmp_path = Path(tmpdir) - sdk_path = _download_qnn_sdk(dst_folder=tmp_path) - - if not sdk_path: - raise RuntimeError( - "Qualcomm SDK not found, cannot build backend" - ) - - # Determine paths - prj_root = Path(__file__).parent.resolve() - build_sh = prj_root / "backends/qualcomm/scripts/build.sh" - build_root = prj_root / "build-x86" - - if not build_sh.exists(): - raise FileNotFoundError(f"{build_sh} not found") - - # Run build.sh with SDK path exported - env = dict(**os.environ) - env["QNN_SDK_ROOT"] = str(sdk_path) - subprocess.check_call( - [ - str(build_sh), - "--skip_linux_android", - "--skip_linux_embedded", - ], - env=env, - ) - - # Copy the main .so into the wheel package - so_src = ( - build_root / "backends/qualcomm/libqnn_executorch_backend.so" - ) - so_dst = Path( - self.get_ext_fullpath( - "executorch.backends.qualcomm.qnn_backend" - ) - ) - self.mkpath(str(so_dst.parent)) # ensure destination exists - self.copy_file(str(so_src), str(so_dst)) - logging.info(f"Copied Qualcomm backend: {so_src} -> {so_dst}") - - # Copy Python adaptor .so files - ext_suffix = sysconfig.get_config_var("EXT_SUFFIX") - - so_files = [ - ( - "executorch.backends.qualcomm.python.PyQnnManagerAdaptor", - prj_root - / f"backends/qualcomm/python/PyQnnManagerAdaptor{ext_suffix}", - ), - ( - "executorch.backends.qualcomm.python.PyQnnWrapperAdaptor", - prj_root - / f"backends/qualcomm/python/PyQnnWrapperAdaptor{ext_suffix}", - ), - ] - - for module_name, so_src in so_files: - so_dst = Path(self.get_ext_fullpath(module_name)) - self.mkpath(str(so_dst.parent)) - self.copy_file(str(so_src), str(so_dst)) - logging.info(f"Copied Qualcomm backend: {so_src} -> {so_dst}") - - except ImportError: - logging.error("Fail to build Qualcomm backend") - logging.exception("Import error") - if self.editable_mode: self._ran_build = True self.run_command("build") @@ -837,6 +757,11 @@ def run(self): # noqa C901 cmake_build_args += ["--target", "custom_ops_aot_lib"] cmake_build_args += ["--target", "quantized_ops_aot_lib"] + if cmake_cache.is_enabled("EXECUTORCH_BUILD_QNN"): + cmake_build_args += ["--target", "qnn_executorch_backend"] + cmake_build_args += ["--target", "PyQnnManagerAdaptor"] + cmake_build_args += ["--target", "PyQnnWrapperAdaptor"] + # Set PYTHONPATH to the location of the pip package. os.environ["PYTHONPATH"] = ( site.getsitepackages()[0] + ";" + os.environ.get("PYTHONPATH", "") @@ -924,5 +849,24 @@ def run(self): # noqa C901 dst="executorch/data/lib/", dependent_cmake_flags=[], ), + BuiltFile( + src_dir="%CMAKE_CACHE_DIR%/backends/qualcomm/%BUILD_TYPE%/", + src_name="qnn_executorch_backend", + dst="executorch/backends/qualcomm/", + is_dynamic_lib=True, + dependent_cmake_flags=["EXECUTORCH_BUILD_QNN"], + ), + BuiltExtension( + src_dir="%CMAKE_CACHE_DIR%/backends/qualcomm/%BUILD_TYPE%/", + src="PyQnnManagerAdaptor.*", + modpath="executorch.backends.qualcomm.python.PyQnnManagerAdaptor", + dependent_cmake_flags=["EXECUTORCH_BUILD_QNN"], + ), + BuiltExtension( + src_dir="%CMAKE_CACHE_DIR%/backends/qualcomm/%BUILD_TYPE%/", + src="PyQnnWrapperAdaptor.*", + modpath="executorch.backends.qualcomm.python.PyQnnWrapperAdaptor", + dependent_cmake_flags=["EXECUTORCH_BUILD_QNN"], + ), ], ) diff --git a/tools/cmake/preset/default.cmake b/tools/cmake/preset/default.cmake index 0dcec0df531..1d12924bfa4 100644 --- a/tools/cmake/preset/default.cmake +++ b/tools/cmake/preset/default.cmake @@ -288,6 +288,12 @@ define_overridable_option( BOOL FALSE ) +define_overridable_option( + EXECUTORCH_BUILD_WHEEL_DO_NOT_USE + "On if in the wheel building process. Should only be used to guard code that is only needed for building the wheel." + BOOL + FALSE +) # ------------------------------------------------------------------------------ # Validations diff --git a/tools/cmake/preset/pybind.cmake b/tools/cmake/preset/pybind.cmake index c71c10ad01f..a0d06d74d17 100644 --- a/tools/cmake/preset/pybind.cmake +++ b/tools/cmake/preset/pybind.cmake @@ -22,6 +22,7 @@ set_overridable_option(EXECUTORCH_BUILD_EXTENSION_FLAT_TENSOR ON) set_overridable_option(EXECUTORCH_BUILD_EXTENSION_DATA_LOADER ON) set_overridable_option(EXECUTORCH_BUILD_EXTENSION_MODULE ON) set_overridable_option(EXECUTORCH_BUILD_EXTENSION_NAMED_DATA_MAP ON) +set_overridable_option(EXECUTORCH_BUILD_WHEEL_DO_NOT_USE ON) # TODO(larryliu0820): Temporarily disable building llm_runner for Windows wheel # due to the issue of tokenizer file path length limitation. @@ -35,6 +36,9 @@ elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") set_overridable_option(EXECUTORCH_BUILD_EXTENSION_TRAINING ON) 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 ON) + endif() elseif(CMAKE_SYSTEM_NAME STREQUAL "Windows" OR CMAKE_SYSTEM_NAME STREQUAL "WIN32" )