Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
381 changes: 348 additions & 33 deletions .ci/scripts/wheel/test_cpp_sdk.py

Large diffs are not rendered by default.

550 changes: 521 additions & 29 deletions .ci/scripts/wheel/test_shared_libraries.py

Large diffs are not rendered by default.

14 changes: 14 additions & 0 deletions .ci/scripts/wheel/test_windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@

import test_base
import test_clean_install
import test_cpp_sdk
import test_shared_libraries
import torch
from executorch.backends.xnnpack.partition.xnnpack_partitioner import XnnpackPartitioner
from executorch.examples.models import Backend, Model, MODEL_NAME_TO_MODEL
Expand Down Expand Up @@ -86,6 +88,18 @@ def run_tests(model_tests: List[ModelTest]) -> None:

test_base.test_cmsis_nn_install()

# The wheel ships the runtime, the kernels, the delegate and the thread pool as
# separate DLLs here too, so check that each has exactly one owner and that all of
# them load.
with tempfile.TemporaryDirectory() as work_dir:
test_shared_libraries.run_tests(Path(work_dir))

# And that a C++ application outside the wheel can actually use them. Nothing else
# covers this: the Python extension links those DLLs itself, so it passes whether or
# not the package config names them or the shipped headers are complete.
with tempfile.TemporaryDirectory() as work_dir:
test_cpp_sdk.run_tests(Path(work_dir))

run_tests(
model_tests=[
ModelTest(
Expand Down
62 changes: 49 additions & 13 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -228,17 +228,30 @@ if(EXECUTORCH_BUILD_SHARED)
)
endif()
# Said here rather than left to fail somewhere downstream, where packaging
# looked for a library the build never emitted. Windows is still refused:
# there the runtime carries no export annotations, so a DLL would link against
# nothing, which is a missing capability rather than a different spelling of
# one.
if(NOT CMAKE_SYSTEM_NAME STREQUAL "Linux" AND NOT APPLE)
# looked for a library the build never emitted. The runtime carries no export
# annotations, so on Windows each shipped component DLL exports all of its
# symbols instead; see executorch_target_shipped_runtime_path.
if(NOT CMAKE_SYSTEM_NAME STREQUAL "Linux"
AND NOT APPLE
AND NOT (WIN32 AND MSVC)
)
message(
FATAL_ERROR
"EXECUTORCH_BUILD_SHARED is supported on Linux and macOS only, not "
"EXECUTORCH_BUILD_SHARED is supported on Linux, macOS and Windows (MSVC) only, not "
"${CMAKE_SYSTEM_NAME}."
)
endif()
# The Windows DLLs take their components as objects through $<COMPILE_ONLY>,
# which CMake added in 3.27. Older versions fail at generate time with an
# expression error that does not name the cause.
if(MSVC AND CMAKE_VERSION VERSION_LESS 3.27)
message(
FATAL_ERROR
"EXECUTORCH_BUILD_SHARED on Windows needs CMake 3.27 or newer, found "
"${CMAKE_VERSION}. Upgrade CMake (pip install 'cmake>=3.27,<4') or "
"build with -DEXECUTORCH_BUILD_SHARED=OFF."
)
endif()
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
endif()

Expand Down Expand Up @@ -1066,14 +1079,35 @@ if(EXECUTORCH_BUILD_SHARED)
executorch_shared PUBLIC C10_USING_CUSTOM_GENERATED_MACROS
)

# Link executorch without WHOLE_ARCHIVE because its INTERFACE link options
# (from executorch_target_link_options_shared_lib) already force
# Outside MSVC, link executorch without WHOLE_ARCHIVE because its INTERFACE
# link options (from executorch_target_link_options_shared_lib) already force
# whole-archive. Everything else is pulled in through link options rather than
# the WHOLE_ARCHIVE link feature, because these archives also reference each
# other plainly and CMake before 3.29 refuses to mix a feature with a plain
# reference to the same item.
target_link_libraries(executorch_shared PRIVATE executorch)
set(_executorch_shared_whole_archive executorch_core)
if(MSVC)
# Bundled as objects like the rest; linking it would bring its interface
# /WHOLEARCHIVE, which lld-link processes ahead of the bundled objects.
set(_executorch_shared_whole_archive executorch executorch_core)
Comment thread
shoumikhin marked this conversation as resolved.
# The objects come through $<COMPILE_ONLY>, which does not pass on
# executorch_core's cxx_std_20. The public headers need it on Windows (the
# vendored c10 reaches std::countl_zero on the MSVC branch), so a consumer
# linking this target directly has to be told.
target_compile_features(executorch_shared PUBLIC cxx_std_20)
# A DLL resolves its own references at link time, so a weak PAL cannot be
# overridden through it anyway, and the export list
# WINDOWS_EXPORT_ALL_SYMBOLS generates skips weak symbols: without this the
# runtime DLL exports none of the et_pal_* functions, and the public
# clock.h, whose inline ticks_to_ns() calls et_pal_ticks_to_ns_multiplier(),
# fails to link against it.
set_source_files_properties(
${EXECUTORCH_PAL_DEFAULT_FILE_PATH} PROPERTIES COMPILE_DEFINITIONS
ET_PAL_STRONG_SYMBOLS
)
else()
target_link_libraries(executorch_shared PRIVATE executorch)
set(_executorch_shared_whole_archive executorch_core)
endif()
foreach(_ext_target
extension_data_loader extension_flat_tensor extension_named_data_map
extension_module_static extension_tensor
Expand All @@ -1083,7 +1117,8 @@ if(EXECUTORCH_BUILD_SHARED)
endif()
endforeach()
foreach(_whole_target ${_executorch_shared_whole_archive})
executorch_target_whole_archive(executorch_shared ${_whole_target})
# EXPORTED: this is the runtime API every other component links.
executorch_target_whole_archive(executorch_shared ${_whole_target} EXPORTED)
endforeach()
configure_file(
tools/cmake/executorch.pc.in ${CMAKE_CURRENT_BINARY_DIR}/executorch.pc
Expand All @@ -1093,8 +1128,9 @@ if(EXECUTORCH_BUILD_SHARED)
DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig
)
# pip decides where the wheel lands, so the wheel's copy of this file locates
# everything relative to its own location.
if(EXECUTORCH_BUILD_WHEEL_DO_NOT_USE)
# everything relative to its own location. Not on Windows: its link flags are
# GNU-style (-Wl,-rpath) and a DLL records no search path to point them at.
if(EXECUTORCH_BUILD_WHEEL_DO_NOT_USE AND NOT WIN32)
set(_executorch_pc_definitions "-DC10_USING_CUSTOM_GENERATED_MACROS")
if(EXECUTORCH_ENABLE_EVENT_TRACER)
string(APPEND _executorch_pc_definitions " -DET_EVENT_TRACER_ENABLED")
Expand Down
4 changes: 4 additions & 0 deletions README-wheel.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ to run ExecuTorch `.pte` files, with some restrictions:
prebuilt module. OpenVINO requires the runtime to be installed separately:
`pip install executorch[openvino]`

On Linux, macOS and Windows the runtime, kernels and backends also ship as shared libraries
with headers and a CMake package, so a C++ application can link them without building
ExecuTorch; see [Using ExecuTorch with C++](docs/source/using-executorch-cpp.md).

Please visit the [ExecuTorch website](https://pytorch.org/executorch) for
tutorials and documentation. Here are some starting points:
* [Getting Started](https://pytorch.org/executorch/main/getting-started-setup)
Expand Down
20 changes: 20 additions & 0 deletions codegen/tools/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
# All rights reserved.
#
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree.

import os
import sys

# selective_build links the shipped runtime DLL in executorch/lib, and Windows
# records no search path in a DLL. Not resolved: in an editable install this
# directory is a symlink, and executorch/lib sits beside the link.
_lib_dir = os.path.abspath(
os.path.join(
os.path.dirname(os.path.abspath(__file__)), os.pardir, os.pardir, "lib"
)
)
if sys.platform == "win32" and os.path.isdir(_lib_dir):
os.add_dll_directory(_lib_dir)
del os, sys, _lib_dir
62 changes: 52 additions & 10 deletions docs/source/using-executorch-cpp.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ cover.

### Using the prebuilt libraries from the pip package

On Linux and macOS, current main/nightly wheels ship the runtime as prebuilt
On Linux, macOS and Windows, current main/nightly wheels ship the runtime as prebuilt
shared libraries together with the headers and a CMake package. Stable releases
from before this packaging was introduced do not contain the namespaced CMake
targets used below; use the documentation for your installed release.
Expand Down Expand Up @@ -159,6 +159,21 @@ That is the two input arrays added together. The long `python -c` part just prin
CMake package, so CMake can find it. Run `./build/app` from the folder holding `model.pte`, because
the path in `main.cpp` is relative.

On Windows, three things differ. Add the DLL copy step from [the Windows section](#on-windows) to
`CMakeLists.txt`, since a Windows program finds the DLLs only next to itself. Build Release, because
the DLLs use the release C++ library and a Debug build is refused at compile time. And with Visual
Studio the program ends up in a `Release` folder:

```
cmake -S . -B build -DCMAKE_PREFIX_PATH="..." -DCMAKE_BUILD_TYPE=Release
cmake --build build --config Release
.\build\Release\app.exe
```

`-DCMAKE_BUILD_TYPE` is what a single-configuration generator such as Ninja, the default in VS Code
and CLion, reads; it ignores `--config` and leaves the program in `build\app.exe`. Visual Studio
reads `--config` instead. Giving both covers either.

#### Adding kernels and backends

Add a component to both lines to get more. Nothing else in the program changes.
Expand All @@ -175,12 +190,12 @@ These are the components the package provides:

| Component | What it gives you | Where |
| --- | --- | --- |
| `runtime` | The engine. Always needed. | Linux, macOS |
| `kernels_optimized` | Fast CPU operators. The usual choice. | Linux, macOS |
| `backend_xnnpack` | The XNNPACK backend, for models exported with it. | Linux, macOS |
| `threadpool` | Multi-threaded execution. | Linux, macOS |
| `runtime` | The engine. Always needed. | Linux, macOS, Windows |
| `kernels_optimized` | Fast CPU operators. The usual choice. | Linux, macOS, Windows |
| `backend_xnnpack` | The XNNPACK backend, for models exported with it. | Linux, macOS, Windows |
| `threadpool` | Multi-threaded execution. | Linux, macOS, Windows |
| `etdump` | Profiling, to record what ran and how long it took. | Linux, macOS |
| `kernels_quantized` | The quantized operator kernels | Linux, macOS |
| `kernels_quantized` | The quantized operator kernels | Linux, macOS, Windows |
| `kernels_torchao` | The TorchAO low-bit quantized kernels | Linux and macOS, aarch64 only |
| `backend_cuda` | The CUDA delegate | Linux |
| `extension_cuda` | The CUDA stream extension | Linux |
Expand Down Expand Up @@ -263,8 +278,9 @@ message(STATUS "Metal kernels: ${MLX_METALLIB_PATH}")

#### Using pkg-config

Build systems such as Meson and Autotools read pkg-config files. The wheel ships one for the
runtime. It covers the engine and the thread pool. Name the kernel libraries yourself, the same
Build systems such as Meson and Autotools read pkg-config files. The Linux and macOS wheels ship
one for the runtime; the Windows wheel does not, so use the CMake package there. It covers the
engine and the thread pool. Name the kernel libraries yourself, the same
way you add CMake components. Point pkg-config at the file with an absolute path, because the
library search path it gives the linker is built from that path:

Expand Down Expand Up @@ -347,8 +363,9 @@ way, because they sit next to the runtime library.
target_link_libraries(app PRIVATE ${EXECUTORCH_QUANTIZED_KERNELS_LIBRARY})
```

You should not need `LD_LIBRARY_PATH`. The shipped libraries record where their neighbours live, so
they find each other once the program links against the installed package.
On Linux and macOS you should not need `LD_LIBRARY_PATH`. The shipped libraries record where their
neighbours live, so they find each other once the program links against the installed package.
Windows has no such record; see below.

On Linux, linking the runtime asks the linker for `DT_RUNPATH` rather than the older `DT_RPATH`.
That is deliberate: `DT_RPATH` is searched before `LD_LIBRARY_PATH` and applies to a dependency's
Expand All @@ -368,6 +385,31 @@ target_link_libraries(app PRIVATE executorch::runtime prefer_rpath)
The order matters. A target's own link options are emitted before those of its dependencies, and the
last of the two settings decides the tag for every entry in the link.

#### On Windows

A Windows DLL records no search path, so the loader finds the shipped DLLs only next to your
program (or on `PATH`). Copy them there after each build; the imported targets name every DLL your
program links:

```cmake
add_custom_command(
TARGET app POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_RUNTIME_DLLS:app> $<TARGET_FILE_DIR:app>
COMMAND_EXPAND_LISTS
)
```

Build Release: configure with `-DCMAKE_BUILD_TYPE=Release` for a single-configuration generator such
as Ninja, or build with `cmake --build build --config Release` under Visual Studio. The DLLs are built
against the release C++ library, and a Debug program uses the debug one, whose types are laid out
differently, so mixing the two would corrupt memory. The runtime headers therefore refuse a Debug
build at compile time, with an error asking for Release. On CMake older than 3.28, copy the DLLs from
`${EXECUTORCH_RUNTIME_LIBRARY_DIR}` instead, and apply `${EXECUTORCH_COMPILE_DEFINITIONS}`, which
carries that check.

The `etdump` component is not offered on Windows, because the Windows wheel is built without the
event tracer.

### Running on a GPU with the CUDA package

The CUDA build is a separate package. Releases cover CUDA 13.2 and 13.4, so pick the index
Expand Down
16 changes: 16 additions & 0 deletions extension/llm/custom_ops/op_tile_crop_aot.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
# LICENSE file in the root directory of this source tree.

import logging
import os
import sys
from pathlib import Path

import torch
Expand All @@ -13,6 +15,20 @@
tile_crop = torch.ops.preprocess.tile_crop.default
assert tile_crop is not None
except:
# The library depends on the shipped runtime DLLs in executorch/lib, and
# Windows records no search path in a DLL. Not resolved: in an editable
# install this directory is a symlink, and executorch/lib sits beside it.
_lib_dir = os.path.abspath(
os.path.join(
os.path.dirname(os.path.abspath(__file__)),
os.pardir,
os.pardir,
os.pardir,
"lib",
)
)
if sys.platform == "win32" and os.path.isdir(_lib_dir):
os.add_dll_directory(_lib_dir)
libs = list(Path(__file__).parent.resolve().glob("*custom_ops_aot_lib.*"))
assert len(libs) == 1, f"Expected 1 library but got {len(libs)}"
logging.info(f"Loading custom ops library: {libs[0]}")
Expand Down
5 changes: 5 additions & 0 deletions extension/pybindings/portable_lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@
# The extension DLL should be in the same directory as this file.
pybindings_dir = os.path.dirname(os.path.abspath(__file__))
os.add_dll_directory(pybindings_dir)
# The shared runtime and its components ship in executorch/lib. Windows
# records no search path in a DLL, so the directory is registered here.
_lib_dir = os.path.join(pybindings_dir, os.pardir, os.pardir, "lib")
if os.path.isdir(_lib_dir):
os.add_dll_directory(os.path.abspath(_lib_dir))
except Exception as e:
logger.error(
"Failed to add the pybinding extension DLL to the search path. "
Expand Down
11 changes: 9 additions & 2 deletions extension/threadpool/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,16 @@ if(EXECUTORCH_BUILD_SHARED)
# Ships beside libexecutorch.so in the wheel's lib/ directory.
executorch_target_shipped_runtime_path(extension_threadpool)
# cpuinfo and pthreadpool are forced static, so bundle them inside this
# library instead of making every consumer supply them.
# library instead of making every consumer supply them. pthreadpool is
# EXPORTED, because the other components run their work on this one pool.
# cpuinfo is not: on Windows its feature checks (cpuinfo_has_x86_avx2 and the
# rest) are inline reads of cpuinfo_isa, and cpuinfo.h does not declare that
# data dllimport, so another DLL can only ever read its own copy. Exporting
# the functions let XNNPACK initialize this library's copy and read its own,
# all zeros, and run baseline kernels. Kept private, each DLL that uses
# cpuinfo carries all of it. Elsewhere the whole archive is exported anyway.
executorch_target_whole_archive(extension_threadpool cpuinfo)
executorch_target_whole_archive(extension_threadpool pthreadpool)
executorch_target_whole_archive(extension_threadpool pthreadpool EXPORTED)
target_link_libraries(extension_threadpool PUBLIC executorch_shared)
else()
target_link_libraries(
Expand Down
8 changes: 8 additions & 0 deletions extension/threadpool/threadpool.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,15 @@ ThreadPool* get_threadpool() {
return std::min(result, tsan_thread_limit);
})();

#if defined(_WIN32)
// Never destroyed. At process exit Windows terminates the worker threads
// before running a DLL's static destructors, so tearing the pool down there
// waits on a lock a terminated worker may hold and the process never exits.
static auto& threadpool = *new std::unique_ptr<ThreadPool>(
std::make_unique<ThreadPool>(num_threads));
#else
static auto threadpool = std::make_unique<ThreadPool>(num_threads);
#endif

// Inheriting from old threadpool to get around segfault issue
// commented above at child_atfork
Expand Down
15 changes: 15 additions & 0 deletions kernels/quantized/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,23 @@
# LICENSE file in the root directory of this source tree.

try:
import os
import sys
from pathlib import Path

# The library depends on the shipped runtime and quantized kernels DLLs in
# executorch/lib, and Windows records no search path in a DLL. Not resolved:
# in an editable install this directory is a symlink, and executorch/lib
# sits beside the link.
_lib_dir = os.path.abspath(
os.path.join(
os.path.dirname(os.path.abspath(__file__)), os.pardir, os.pardir, "lib"
)
)
if sys.platform == "win32" and os.path.isdir(_lib_dir):
os.add_dll_directory(_lib_dir)
del os, sys, _lib_dir

libs = list(Path(__file__).parent.resolve().glob("**/*quantized_ops_aot_lib.*"))
del Path
assert len(libs) == 1, f"Expected 1 library but got {len(libs)}"
Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
[build-system]
requires = [
"cmake>=3.26,<4.0.0", # For building binary targets in the wheel. 4.0.0 breaks third-party CMake build so temporarily pin the version.
"cmake>=3.26,<4.0.0; sys_platform != 'win32'", # For building binary targets in the wheel. 4.0.0 breaks third-party CMake build so temporarily pin the version.
"cmake>=3.27,<4.0.0; sys_platform == 'win32'", # The shared Windows build uses $<COMPILE_ONLY>, new in 3.27.
"packaging>=24.2", # Lower bound required by setuptools
"patchelf; sys_platform == 'linux'", # Writes the runtime search paths that let the shipped libraries find each other.
"pip>=23", # For building the pip package.
Expand Down
3 changes: 2 additions & 1 deletion requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Pip packages needed to build from source. Mainly for development of ExecuTorch.

cmake>=3.26, <4.0.0 # For building binary targets in the wheel.
cmake>=3.26, <4.0.0; sys_platform != 'win32' # For building binary targets in the wheel.
cmake>=3.27, <4.0.0; sys_platform == 'win32' # The shared Windows build uses $<COMPILE_ONLY>, new in 3.27.
packaging>=24.2 # Lower bound required by setuptools
patchelf; sys_platform == 'linux' # Writes the runtime search paths that let the shipped libraries find each other.
pip>=23 # For building the pip package.
Expand Down
Loading
Loading