diff --git a/.ci/scripts/wheel/test_shared_libraries.py b/.ci/scripts/wheel/test_shared_libraries.py index a21480318ab..b7f99208ea4 100644 --- a/.ci/scripts/wheel/test_shared_libraries.py +++ b/.ci/scripts/wheel/test_shared_libraries.py @@ -130,6 +130,10 @@ # have exactly one owner while the bundled code underneath them does not. That is # the same failure the split exists to prevent, reached by a different route. _BUNDLED_THREADPOOL_SYMBOLS = ("pthreadpool_create", "cpuinfo_initialize") +# The delegate's own entry points. A second definer means the delegate is compiled +# into the Python extension as well, which would register it twice in one process. +_OPENVINO_BACKEND_SYMBOLS = ("executorch::backends::openvino::OpenvinoBackend",) + _BUNDLED_XNNPACK_SYMBOLS = ("xnn_create_runtime_v4",) # A representative symbol from the profiler. A second definer means two event @@ -464,6 +468,18 @@ def _wheel_cuda_train() -> str: # wheel is inspected, and a row cannot call that at import time. _REQUIRED_ON_A_CUDA_WHEEL = "cuda-wheel-only" +# Marker for a row whose owner every Linux wheel carries and no macOS wheel does. +_REQUIRED_ON_LINUX = "linux-only" + + +def _resolve_required(required): + """Turn a row's requirement marker into the answer for the installed wheel.""" + if required == _REQUIRED_ON_A_CUDA_WHEEL: + return bool(_wheel_cuda_train()) + if required == _REQUIRED_ON_LINUX: + return sys.platform == "linux" + return required + # The exact dependency names packaging declares per CUDA train, mirroring # _CUDA_RUNTIME_PACKAGES in setup.py. Listed here rather than imported because setup.py @@ -569,6 +585,16 @@ def _wheel_cuda_train() -> str: "libexecutorch_backend_xnnpack.so", True, ), + # Required on Linux, where packaging turns the backend on for every non-minimal + # build. A fixed False passed a wheel that had compiled the delegate back into the + # extension: no library ships, so the row skips, and one definer inside the + # extension is exactly the monolithic layout a definer count cannot distinguish. + ( + "OpenVINO delegate", + _OPENVINO_BACKEND_SYMBOLS, + "libexecutorch_backend_openvino.so", + _REQUIRED_ON_LINUX, + ), ) # The one component that legitimately exists twice. The quantized kernels are compiled into the runtime @@ -592,13 +618,11 @@ def test_each_component_has_one_owner() -> None: # ships under backends/cuda/, and scanning lib/ alone reported it as absent, which each row # treats as an acceptable state and so would have skipped the check entirely. shipped = {path.name for path in _shipped_shared_objects(_installed_package_dir())} - on_a_cuda_wheel = bool(_wheel_cuda_train()) for what, symbols, owner, required in _OWNED_COMPONENTS: - if required == _REQUIRED_ON_A_CUDA_WHEEL: - # Resolved here rather than in the table, because it depends on the installed - # wheel. A fixed False let a wheel tagged +cu126 ship with no CUDA library at - # all and still pass, which is the whole point of these three rows. - required = on_a_cuda_wheel + # Resolved here rather than in the table, because it depends on the installed + # wheel. A fixed False let a wheel tagged +cu126 ship with no CUDA library at + # all and still pass, which is the whole point of the conditional rows. + required = _resolve_required(required) present = any(name.startswith(owner) for name in shipped) assert present or not required, ( f"the wheel ships no {owner}, which owns the {what}. Either packaging " @@ -1641,6 +1665,7 @@ def test_shipped_library_names_are_expected() -> None: # dependency, so both have to ship and both are expected here. "libextension_cuda", "libexecutorch_backend_xnnpack", + "libexecutorch_backend_openvino", "libexecutorch_threadpool", "libexecutorch_etdump", ) diff --git a/CMakeLists.txt b/CMakeLists.txt index 16eff0055f3..3632b3df581 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1309,6 +1309,9 @@ if(EXECUTORCH_BUILD_PYBIND) endif() if(EXECUTORCH_BUILD_OPENVINO) + # Under a shared build this resolves to the shipped library rather than a + # static archive, so the delegate is not copied into the extension and the + # process registers it once. list(APPEND _dep_libs openvino_backend) endif() diff --git a/backends/openvino/CMakeLists.txt b/backends/openvino/CMakeLists.txt index 5b7a1349bf5..0f3a04e97c2 100644 --- a/backends/openvino/CMakeLists.txt +++ b/backends/openvino/CMakeLists.txt @@ -31,8 +31,13 @@ include(${EXECUTORCH_ROOT}/tools/cmake/Utils.cmake) # The backend resolves OpenVINO C API symbols via dlopen/dlsym at runtime, so # there is no build-time dependency on the OpenVINO SDK. -# Define OpenVINO backend as a static library -add_library(openvino_backend STATIC) +# Shared when the wheel build asks for it, static otherwise. +if(EXECUTORCH_BUILD_SHARED) + set(_openvino_backend_library_type SHARED) +else() + set(_openvino_backend_library_type STATIC) +endif() +add_library(openvino_backend ${_openvino_backend_library_type}) # Enable exceptions and RTTI for OpenVINO backend target_compile_options(openvino_backend PRIVATE -frtti -fexceptions) @@ -47,7 +52,25 @@ target_sources( target_include_directories(openvino_backend PRIVATE ${COMMON_INCLUDE_DIRS}) # Link ExecuteTorch core and dynamic loading libraries -target_link_libraries(openvino_backend PRIVATE executorch_core ${CMAKE_DL_LIBS}) +target_link_libraries(openvino_backend PRIVATE ${CMAKE_DL_LIBS}) + +if(EXECUTORCH_BUILD_SHARED) + # Named after what the library provides rather than after the target that + # produces it, matching the other shipped delegates, so the file reads as + # libexecutorch_backend_openvino.so. + set_target_properties( + openvino_backend PROPERTIES OUTPUT_NAME executorch_backend_openvino + ) + executorch_target_soname_policy(openvino_backend) + # The shared runtime, never also the static core: linking both would compile + # the backend registry into this library as well and give the process two of + # them. + target_link_libraries(openvino_backend PUBLIC executorch_shared) + # Ships beside the runtime in the wheel's lib/ directory. + executorch_target_shipped_runtime_path(openvino_backend) +else() + target_link_libraries(openvino_backend PUBLIC executorch_core) +endif() executorch_target_link_options_shared_lib(openvino_backend) diff --git a/docs/source/using-executorch-cpp.md b/docs/source/using-executorch-cpp.md index 4b8191afa9f..74690fb5c42 100644 --- a/docs/source/using-executorch-cpp.md +++ b/docs/source/using-executorch-cpp.md @@ -180,6 +180,7 @@ These are the components the Linux package provides: | `kernels_quantized` | The quantized operator kernels | Linux | | `backend_cuda` | The CUDA delegate | Linux | | `extension_cuda` | The CUDA stream extension | Linux | +| `backend_openvino` | The OpenVINO delegate | Linux | To see what your own install offers, ask CMake: @@ -187,7 +188,7 @@ To see what your own install offers, ask CMake: find_package(executorch REQUIRED) foreach(_component runtime kernels_optimized kernels_quantized backend_xnnpack - backend_cuda extension_cuda threadpool etdump) + backend_cuda extension_cuda backend_openvino threadpool etdump) if(TARGET executorch::${_component}) message(STATUS "have ${_component}") endif() @@ -213,6 +214,19 @@ The quantized kernels are deliberately left out of that variable, because loadin `executorch.kernels.quantized` in Python registers the same operators and a duplicate registration stops the runtime. +The OpenVINO delegate needs one more step. The wheel ships the adapter, not the OpenVINO runtime +itself, and the adapter opens `libopenvino_c.so` by name when the model is loaded. Python callers +get that path set for them on import; a standalone C++ program does not, so install the runtime and +point the program at it: + +```bash +pip install "executorch[openvino]" +export OPENVINO_LIB_PATH="$(python -c 'import glob, openvino, os; print(sorted(glob.glob(os.path.join(os.path.dirname(openvino.__file__), "libs", "libopenvino_c.so*")))[0])')" +``` + +Without it the delegate still registers and the program still links, and the failure arrives later, +when the model is loaded. + #### When something does not work - `find_package` could not find executorch: the `-DCMAKE_PREFIX_PATH=...` argument is missing or diff --git a/setup.py b/setup.py index f26023f12ea..50a545d33a2 100644 --- a/setup.py +++ b/setup.py @@ -2149,6 +2149,18 @@ def run(self): # noqa C901 "EXECUTORCH_BUILD_KERNELS_QUANTIZED", ], ), + # The OpenVINO delegate, so a C++ application can link it from the + # wheel. Only the adapter ships here: the OpenVINO runtime itself is + # loaded at run time and comes from the openvino extra. + BuiltFile( + src_dir="%CMAKE_CACHE_DIR%/backends/openvino/%BUILD_TYPE%/", + src_name="*executorch_backend_openvino" + _dynamic_lib_suffix(), + dst="executorch/lib/", + dependent_cmake_flags=[ + "EXECUTORCH_BUILD_SHARED", + "EXECUTORCH_BUILD_OPENVINO", + ], + ), # Install the XNNPACK delegate beside them, so a process has one # copy of it instead of one per component that uses it. BuiltFile( diff --git a/tools/cmake/executorch-wheel-config.cmake b/tools/cmake/executorch-wheel-config.cmake index e068dffe74c..c7e87480ae0 100644 --- a/tools/cmake/executorch-wheel-config.cmake +++ b/tools/cmake/executorch-wheel-config.cmake @@ -72,6 +72,9 @@ # executorch::backend_xnnpack The XNNPACK delegate. # executorch::backend_cuda The CUDA delegate. Linux only. # executorch::extension_cuda The CUDA stream extension. Linux only. +# executorch::backend_openvino The OpenVINO delegate. Linux only. Opens the +# OpenVINO runtime by name, which a C++ program +# installs and points OPENVINO_LIB_PATH at. # executorch::threadpool The shared thread pool. # executorch::etdump The profiler. # ~~~ @@ -311,9 +314,13 @@ if(_executorch_runtime_library AND NOT _executorch_targets_supported) # links that as well. foreach( _executorch_component IN - ITEMS libexecutorch_kernels_optimized libexecutorch_backend_xnnpack - libexecutorch_backend_cuda libexecutorch_extension_cuda - libexecutorch_threadpool libexecutorch_etdump + ITEMS libexecutorch_kernels_optimized + libexecutorch_backend_xnnpack + libexecutorch_backend_cuda + libexecutorch_extension_cuda + libexecutorch_backend_openvino + libexecutorch_threadpool + libexecutorch_etdump ) _executorch_find_library( _executorch_component_library "${_executorch_component}" @@ -603,6 +610,7 @@ if(TARGET executorch::runtime AND TARGET executorch::threadpool) endif() _executorch_define_component(backend_xnnpack executorch_backend_xnnpack) +_executorch_define_component(backend_openvino executorch_backend_openvino) # The CUDA delegate and its stream helper, present only in a wheel built from a # CUDA index. A CPU wheel defines neither, so a consumer asking for one is told # while configuring.