Skip to content

Commit 8c11101

Browse files
committed
Build and ship the OpenVINO backend in the Windows wheel
The OpenVINO delegate lets ExecuTorch run models on Intel hardware (CPUs, integrated and discrete GPUs, and NPUs). Until now the prebuilt delegate shipped only in the Linux wheels. The Windows wheel left it out, even though Intel publishes an OpenVINO runtime for Windows x86-64, so Windows users could not use it without building from source. This brings the Windows wheel to parity with Linux. What changed: - The runtime loader now works on Windows. It loaded the OpenVINO C library with POSIX calls (dlopen/dlsym) and a hardcoded libopenvino_c.so name. The loader is now split by platform: LoadLibraryExA/GetProcAddress/FreeLibrary and openvino_c.dll on Windows, the existing dlopen path on Linux. On Windows it loads with LOAD_WITH_ALTERED_SEARCH_PATH so the runtime's own dependencies (openvino.dll, TBB, the device plugins) resolve from its folder. - The build file sets exceptions and RTTI with the compiler's own flags: /EHsc and /GR for MSVC, -frtti and -fexceptions otherwise. - setup.py enables EXECUTORCH_BUILD_OPENVINO on Windows as well as Linux, and packages the delegate's import library (.lib) beside the other Windows DLLs. - The openvino extra now installs the OpenVINO runtime on Windows x86-64 too, so pip install "executorch[openvino]" activates the backend on both platforms. The Python auto-discovery looks for openvino_c.dll on Windows. - The Windows wheel smoke test now checks the delegate registers and runs a model through it, the same checks the Linux wheel already runs. Test plan: - Compiled both the Windows and POSIX branches of the loader in isolation. - Confirmed the single-owner symbol check matches the delegate's exported name in both the Linux (nm) and Windows (dumpbin) spellings. - Ran black, clang-format and cmake-format on the changed files. - The Windows wheel CI job builds the wheel and runs the smoke test end to end.
1 parent c1d7efb commit 8c11101

10 files changed

Lines changed: 120 additions & 31 deletions

File tree

‎.ci/scripts/wheel/test_shared_libraries.py‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,12 @@
149149
# The delegate's own entry points. A second definer means the delegate is compiled
150150
# into the Python extension as well, which would register it twice in one process.
151151
_QNN_BACKEND_SYMBOLS = ("QnnExecuTorchBackendRegister",)
152-
_OPENVINO_BACKEND_SYMBOLS = ("executorch::backends::openvino::OpenvinoBackend",)
152+
# A member rather than the bare class name: a class has no symbol of its own, and the
153+
# PE reader matches an MSVC decorated name, which spells a member as
154+
# ?is_available@OpenvinoBackend@... and never the class alone.
155+
_OPENVINO_BACKEND_SYMBOLS = (
156+
"executorch::backends::openvino::OpenvinoBackend::is_available",
157+
)
153158

154159
_BUNDLED_XNNPACK_SYMBOLS = ("xnn_create_runtime_v4",)
155160

@@ -1099,6 +1104,9 @@ def _wheel_cuda_train() -> str:
10991104

11001105
# Marker for a row whose owner every Linux wheel carries and no macOS wheel does.
11011106
_REQUIRED_ON_LINUX = "linux-only"
1107+
# Marker for a row whose owner every Linux and Windows wheel carries. The OpenVINO
1108+
# delegate loads its runtime by name on both, so neither needs the SDK at build time.
1109+
_REQUIRED_ON_LINUX_AND_WINDOWS = "linux-and-windows"
11021110
# Marker for a row whose owner every macOS wheel carries and no other wheel does.
11031111
# The Core ML delegate is built on every macOS wheel, so its presence is decidable
11041112
# from the installed wheel and a macOS wheel that dropped it should fail rather than
@@ -1122,6 +1130,8 @@ def _resolve_required(required):
11221130
return bool(_wheel_cuda_train())
11231131
if required == _REQUIRED_ON_LINUX:
11241132
return sys.platform == "linux"
1133+
if required == _REQUIRED_ON_LINUX_AND_WINDOWS:
1134+
return sys.platform in ("linux", "win32")
11251135
if required == _REQUIRED_ON_MACOS:
11261136
return sys.platform == "darwin"
11271137
if required == _REQUIRED_ON_LINUX_X86:
@@ -1287,7 +1297,7 @@ def _resolve_required(required):
12871297
"OpenVINO delegate",
12881298
_OPENVINO_BACKEND_SYMBOLS,
12891299
_library_file_name("libexecutorch_backend_openvino"),
1290-
_REQUIRED_ON_LINUX,
1300+
_REQUIRED_ON_LINUX_AND_WINDOWS,
12911301
),
12921302
# Required on Linux x86_64 only, where the Qualcomm SDK the delegate builds against
12931303
# is published. A fixed False would pass a wheel that compiled the delegate back

‎.ci/scripts/wheel/test_windows.py‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,11 @@ def run_tests(model_tests: List[ModelTest]) -> None:
7979

8080
if platform.system() == "Windows":
8181
registered = _get_registered_backend_names()
82+
assert (
83+
"OpenvinoBackend" in registered
84+
), f"OpenvinoBackend not found in registered backends: {registered}"
85+
print("✓ OpenvinoBackend is registered")
86+
8287
# Vulkan backend is optional: only present when the wheel was built with
8388
# EXECUTORCH_BUILD_VULKAN=1 and the Vulkan SDK (glslc) was available.
8489
if "VulkanBackend" in registered:
@@ -88,6 +93,9 @@ def run_tests(model_tests: List[ModelTest]) -> None:
8893

8994
test_base.test_cmsis_nn_install()
9095

96+
# Registration above proves the delegate loaded, not that it computes.
97+
test_base.test_a_model_runs_through_the_openvino_delegate()
98+
9199
# The wheel ships the runtime, the kernels, the delegate and the thread pool as
92100
# separate DLLs here too, so check that each has exactly one owner and that all of
93101
# them load.

‎backends/openvino/CMakeLists.txt‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,9 @@ set(COMMON_INCLUDE_DIRS ${EXECUTORCH_ROOT}/..)
2828
# Include utility CMake scripts from ExecuteTorch
2929
include(${EXECUTORCH_ROOT}/tools/cmake/Utils.cmake)
3030

31-
# The backend resolves OpenVINO C API symbols via dlopen/dlsym at runtime, so
32-
# there is no build-time dependency on the OpenVINO SDK.
31+
# The backend resolves OpenVINO C API symbols at runtime (dlopen on Linux,
32+
# LoadLibrary on Windows), so there is no build-time dependency on the OpenVINO
33+
# SDK.
3334

3435
# Shared when the wheel build asks for it, static otherwise.
3536
if(EXECUTORCH_BUILD_SHARED)
@@ -40,7 +41,11 @@ endif()
4041
add_library(openvino_backend ${_openvino_backend_library_type})
4142

4243
# Enable exceptions and RTTI for OpenVINO backend
43-
target_compile_options(openvino_backend PRIVATE -frtti -fexceptions)
44+
target_compile_options(
45+
openvino_backend
46+
PRIVATE $<$<CXX_COMPILER_ID:MSVC>:/EHsc /GR>
47+
$<$<NOT:$<CXX_COMPILER_ID:MSVC>>:-frtti -fexceptions>
48+
)
4449

4550
# Add source files for OpenVINO backend
4651
target_sources(

‎backends/openvino/README.md‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,21 @@ For more information on the supported hardware, please refer to [OpenVINO System
1414

1515
## Quick Start (pip wheel)
1616

17-
On Linux, the OpenVINO backend is included in the ExecuTorch pip wheel. Install the OpenVINO runtime to activate it:
17+
On Linux and Windows, the OpenVINO backend is included in the ExecuTorch pip wheel. Install the OpenVINO runtime to activate it:
1818

1919
```bash
2020
pip install executorch[openvino]
2121
```
2222

23-
The backend automatically discovers the OpenVINO C library from the pip-installed package — no `LD_LIBRARY_PATH` setup is needed.
23+
The backend automatically discovers the OpenVINO C library from the pip-installed package, so no library search path setup is needed.
2424

2525
If auto-discovery fails (e.g. non-standard install), you can point to the library explicitly:
2626

2727
```bash
28+
# Linux
2829
export OPENVINO_LIB_PATH=$(python3 -c "import openvino, os; print(os.path.join(os.path.dirname(openvino.__file__), 'libs', 'libopenvino_c.so'))")
30+
# Windows (PowerShell)
31+
$env:OPENVINO_LIB_PATH = python -c "import openvino, os; print(os.path.join(os.path.dirname(openvino.__file__), 'libs', 'openvino_c.dll'))"
2932
```
3033

3134
Verify the backend is available:

‎backends/openvino/runtime/OpenvinoApi.h‎

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88

99
#pragma once
1010

11-
#include <dlfcn.h>
1211
#include <cstddef>
1312
#include <cstdint>
1413
#include <memory>
@@ -96,12 +95,9 @@ using ov_shape_create_fn =
9695
ov_status_e (*)(int64_t, const int64_t*, ov_shape_t*);
9796
using ov_shape_free_fn = ov_status_e (*)(ov_shape_t*);
9897

98+
// Defined in OpenvinoBackend.cpp, where the platform's loader API is included.
9999
struct DlCloser {
100-
void operator()(void* handle) {
101-
if (handle) {
102-
dlclose(handle);
103-
}
104-
}
100+
void operator()(void* handle);
105101
};
106102
using DlHandle = std::unique_ptr<void, DlCloser>;
107103

‎backends/openvino/runtime/OpenvinoBackend.cpp‎

Lines changed: 65 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@
1010
#include <cstring>
1111
#include <string>
1212

13+
#ifdef _WIN32
14+
#include <windows.h>
15+
#else
16+
#include <dlfcn.h>
17+
#endif
18+
1319
#include <executorch/runtime/backend/interface.h>
1420
#include <executorch/runtime/core/error.h>
1521
#include <executorch/runtime/core/evalue.h>
@@ -23,7 +29,45 @@ namespace openvino {
2329

2430
namespace {
2531

32+
#ifdef _WIN32
33+
constexpr const char* kDefaultLibName = "openvino_c.dll";
34+
constexpr const char* kLibrarySearchPathHint = "PATH";
35+
36+
void* open_library(const char* path) {
37+
// Resolves openvino_c.dll's own dependencies (openvino.dll, TBB, the device
38+
// plugins) from its directory when it is given as a full path.
39+
return LoadLibraryExA(path, nullptr, LOAD_WITH_ALTERED_SEARCH_PATH);
40+
}
41+
42+
std::string last_error() {
43+
return "error " + std::to_string(GetLastError());
44+
}
45+
46+
template <typename FuncPtr>
47+
FuncPtr load_symbol(void* handle, const char* name) {
48+
FARPROC sym = GetProcAddress(static_cast<HMODULE>(handle), name);
49+
if (!sym) {
50+
ET_LOG(
51+
Error,
52+
"OpenVINO: failed to resolve symbol '%s': %s",
53+
name,
54+
last_error().c_str());
55+
return nullptr;
56+
}
57+
return reinterpret_cast<FuncPtr>(sym);
58+
}
59+
#else
2660
constexpr const char* kDefaultLibName = "libopenvino_c.so";
61+
constexpr const char* kLibrarySearchPathHint = "LD_LIBRARY_PATH";
62+
63+
void* open_library(const char* path) {
64+
return dlopen(path, RTLD_NOW | RTLD_LOCAL);
65+
}
66+
67+
std::string last_error() {
68+
const char* err = dlerror();
69+
return err ? err : "unknown error";
70+
}
2771

2872
template <typename FuncPtr>
2973
FuncPtr load_symbol(void* handle, const char* name) {
@@ -36,26 +80,41 @@ FuncPtr load_symbol(void* handle, const char* name) {
3680
}
3781
return reinterpret_cast<FuncPtr>(sym);
3882
}
83+
#endif
3984

4085
} // namespace
4186

87+
void DlCloser::operator()(void* handle) {
88+
if (!handle) {
89+
return;
90+
}
91+
#ifdef _WIN32
92+
FreeLibrary(static_cast<HMODULE>(handle));
93+
#else
94+
dlclose(handle);
95+
#endif
96+
}
97+
4298
// Loading is attempted exactly once via std::call_once. If the first attempt
43-
// fails (e.g. library not on LD_LIBRARY_PATH), subsequent calls return false
99+
// fails (e.g. library not on the search path), subsequent calls return false
44100
// without retrying. Users must fix their environment and restart the process.
45101
bool OpenvinoBackend::ensure_loaded() const {
46102
std::call_once(load_flag_, [this]() {
47103
const char* lib_path = std::getenv("OPENVINO_LIB_PATH");
48104
const char* effective_path = lib_path ? lib_path : kDefaultLibName;
49105

50-
void* handle = dlopen(effective_path, RTLD_NOW | RTLD_LOCAL);
106+
void* handle = open_library(effective_path);
51107
if (!handle) {
52108
ET_LOG(
53109
Error,
54-
"OpenVINO runtime not found (dlopen failed: %s). "
55-
"Ensure 'libopenvino_c.so' is on your library search path "
56-
"(set OPENVINO_LIB_PATH or LD_LIBRARY_PATH), or install with: "
110+
"OpenVINO runtime not found (loading '%s' failed: %s). "
111+
"Ensure '%s' is on your library search path "
112+
"(set OPENVINO_LIB_PATH or %s), or install with: "
57113
"pip install \"openvino>=2025.1.0,<2026.0.0\"",
58-
dlerror());
114+
effective_path,
115+
last_error().c_str(),
116+
kDefaultLibName,
117+
kLibrarySearchPathHint);
59118
return;
60119
}
61120
lib_handle_.reset(handle);

‎extension/pybindings/portable_lib.py‎

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,28 +52,29 @@ class _LightweightExperimentalWarning(DeprecationWarning):
5252

5353

5454
# Auto-discover the OpenVINO C library path from the pip-installed openvino
55-
# package so the C++ backend's dlopen("libopenvino_c.so") works without the
56-
# user having to set LD_LIBRARY_PATH or OPENVINO_LIB_PATH manually.
55+
# package so the C++ backend can load it without the user having to set
56+
# OPENVINO_LIB_PATH or the platform's library search path manually.
5757
if not os.environ.get("OPENVINO_LIB_PATH"):
5858
try:
5959
import glob
6060
import importlib.util
6161

62+
_ov_c_lib = "openvino_c.dll" if sys.platform == "win32" else "libopenvino_c.so"
6263
spec = importlib.util.find_spec("openvino")
6364
if spec is not None and spec.submodule_search_locations:
6465
_ov_dir = spec.submodule_search_locations[0]
65-
_ov_libs = sorted(
66-
glob.glob(os.path.join(_ov_dir, "libs", "libopenvino_c.so*"))
67-
)
66+
_ov_libs = sorted(glob.glob(os.path.join(_ov_dir, "libs", _ov_c_lib + "*")))
6867
if _ov_libs:
6968
os.environ["OPENVINO_LIB_PATH"] = _ov_libs[0]
7069
else:
7170
logger.warning(
72-
"OpenVINO package found but libopenvino_c.so not in %s; "
71+
"OpenVINO package found but %s not in %s; "
7372
"set OPENVINO_LIB_PATH manually if needed",
73+
_ov_c_lib,
7474
os.path.join(_ov_dir, "libs"),
7575
)
7676
del _ov_libs, _ov_dir, spec
77+
del _ov_c_lib
7778
except Exception as e:
7879
logger.debug("OpenVINO auto-discovery failed: %s", e)
7980

‎pyproject.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ ethos_u = [
8686
"tosa-tools==2026.5.0",
8787
]
8888
openvino = [
89-
"openvino>=2025.1.0,<2026.0.0; platform_system == 'Linux'",
89+
"openvino>=2025.1.0,<2026.0.0; platform_system == 'Linux' or (platform_system == 'Windows' and platform_machine == 'AMD64')",
9090
]
9191

9292
[project.urls]

‎setup.py‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1411,6 +1411,12 @@ def _windows_import_libraries() -> List["BuiltFile"]:
14111411
None,
14121412
["EXECUTORCH_BUILD_CUDA"],
14131413
),
1414+
(
1415+
"backends/openvino/",
1416+
"executorch_backend_openvino",
1417+
None,
1418+
["EXECUTORCH_BUILD_OPENVINO"],
1419+
),
14141420
]
14151421
return [
14161422
BuiltFile(
@@ -2814,11 +2820,12 @@ def run(self): # noqa C901
28142820
f"-DQNN_SDK_ROOT={qnn_sdk_root}",
28152821
]
28162822

2817-
# Enable OpenVINO backend on Linux. The backend uses dlopen at
2818-
# runtime so it has no build-time SDK dependency.
2823+
# Enable the OpenVINO backend on Linux and Windows. The backend loads
2824+
# the OpenVINO runtime at run time, so it has no build-time SDK
2825+
# dependency.
28192826
if (
28202827
not minimal_build
2821-
and sys.platform == "linux"
2828+
and sys.platform in ("linux", "win32")
28222829
and install_utils.is_cmake_option_on(
28232830
cmake_configuration_args,
28242831
"EXECUTORCH_BUILD_OPENVINO",

‎tools/cmake/executorch-wheel-config.cmake‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,8 @@
8989
# CUDAToolkit_ROOT or CMake's standard toolkit
9090
# search. A program that calls the CUDA runtime
9191
# itself links CUDA::cudart.
92-
# executorch::backend_openvino The OpenVINO delegate. Linux only. Opens the
93-
# OpenVINO runtime by name, which a C++ program
92+
# executorch::backend_openvino The OpenVINO delegate. Linux and Windows. Opens
93+
# the OpenVINO runtime by name, which a C++ program
9494
# installs and points OPENVINO_LIB_PATH at.
9595
# executorch::threadpool The shared thread pool.
9696
# executorch::etdump The profiler. Not on Windows.

0 commit comments

Comments
 (0)