Skip to content

Commit 6e4248a

Browse files
committed
Ship a C++ SDK in the wheel
The wheel ships the runtime, kernels, delegate, thread pool and profiler as separate shared libraries, but nothing outside Python can use them, because the installed CMake package names none of them. A C++ application would have to hard-code paths into the wheel's private layout. The headers have the same gap. The wheel installs only the subset a custom-operator build needs, which leaves out `extension/module`, the entry point the documentation tells C++ callers to use. So the wheel ships the libraries to run a model and no way to call them. Name each shipped library as a CMake component, so `find_package` locates them, and ship the headers a caller needs. A component is just a name a consumer can ask for, and CMake reports a missing one while configuring rather than at link time. ```cmake find_package(executorch 1.5 REQUIRED COMPONENTS kernels_optimized) target_link_libraries(my_app PRIVATE executorch::runtime executorch::kernels_optimized) ``` | component | library it resolves to | | --- | --- | | `executorch::runtime` | `libexecutorch.so` | | `executorch::kernels_optimized` | `libexecutorch_kernels_optimized.so` | | `executorch::backend_xnnpack` | `libexecutorch_backend_xnnpack.so` | | `executorch::threadpool` | `libexecutorch_threadpool.so` | | `executorch::etdump` | `libexecutorch_etdump.so` | Each component records where the wheel keeps its libraries, so an application built against it finds them without the caller setting a library search path. Headers include the module and tensor entry points, the CPU kernel helpers, the allocator and data loader concrete classes Module's constructors take, the profiler entry points, and the FlatTensorDataMap and MergedDataMap types plus the .ptd file header a caller writing a .ptd needs. CMake 3.28 or newer gets these targets. Older versions do not, because they write the `$ORIGIN` marker (the "look next to me" token in a library search path) incorrectly: ``` 3.24.3, 3.27.9 Makefiles double the dollar sign, Ninja drops the name 3.28.4, 3.31.8 both write the token correctly ``` That would produce a target that runs where it was built and fails once the application is copied elsewhere, so no target is defined below 3.28. Those versions get plain variables instead: `EXECUTORCH_LIBRARIES` with the runtime and every shipped library by path, plus `EXECUTORCH_INCLUDE_DIRS`, `EXECUTORCH_COMPILE_DEFINITIONS` and `EXECUTORCH_CXX_STANDARD`. All four are needed, because an imported target carries the definitions and the C++ standard along with the library and a plain path carries neither. Linking the libraries alone stops at `#error "You need C++17 to compile ExecuTorch"`. `ET_USE_THREADPOOL` is added to `EXECUTORCH_COMPILE_DEFINITIONS` on the pre-3.28 route when the thread pool library ships. Without it the runtime header supplies a local inline serial fallback for `parallel_for`, so a consumer following the documented recipe linked the thread pool library and still ran serial code with no diagnostic. Built the wheel, installed it into a clean environment, and built a C++ application against the installed wheel alone: - the application links the runtime, runs a model, and matches eager PyTorch, and still runs after being copied away from the wheel. - asking for a component the wheel does not ship fails while configuring, naming the component. - a version request is honoured, including ranges. - shipped headers can be included on their own, and one entry point per shipped component also links against the shipped libraries. A small number are exempt because they need something outside the package: a Windows shim, a test framework, or a header that says in its own text not to include it directly. The exempt list is compiled too, so an entry that starts working is reported rather than left in place. - the thread pool probe compiles with `ET_USE_THREADPOOL`, on both the modern-CMake route (from the runtime target) and the pre-3.28 route (from `EXECUTORCH_COMPILE_DEFINITIONS`). Without it the header supplies a local inline definition and the probe linked identically whether or not the library was on the link line, so it could not detect the component being dropped. Measured both ways. - an application's runtime search path is recorded as `DT_RUNPATH`, not the older `DT_RPATH`. That matters because `DT_RPATH` is searched ahead of `LD_LIBRARY_PATH` and is inherited by dependencies, so a consumer could not point a locally built or instrumented runtime at their application. Verified by shadowing the runtime through `LD_LIBRARY_PATH` and watching the loader pick it up, which `DT_RPATH` ignores. - on real CMake 3.24 and 3.27, an application configures, builds and runs through the variables. Measured what each one contributes, with the consumer pinned to C++14 so its own standard does not hide the package's requirement: linking `EXECUTORCH_LIBRARIES` alone fails on a missing header, adding the include directories and definitions then fails on the C++ standard, and applying `EXECUTORCH_CXX_STANDARD` builds and loads a model. The kernels also need scoped retention there, because a registration-only library exports nothing the application references and the linker drops it, which showed up as "Missing operator" at run time rather than as a link error. The smoke test now runs the same shape automatically when `EXECUTORCH_PRE_328_CMAKE` points at an older cmake binary, so a future change on the fallback path fails a check rather than only showing up on the first user with older cmake. - `find_package` succeeds when the interpreter on PATH is not the one the wheel was built for. The extension's own file name carries its suffix, so asking a different interpreter for it reported a complete install as not found. Ran on Linux x86_64 and aarch64. The macOS wheel keeps the fused extension and ships no separate libraries, so these checks do not apply there and its smoke test does not run them. ghstack-source-id: 490d066 ghstack-comment-id: 5215967468 Pull-Request: #21639
1 parent e7589ad commit 6e4248a

11 files changed

Lines changed: 2276 additions & 48 deletions

File tree

.ci/scripts/wheel/test_cpp_sdk.py

Lines changed: 1238 additions & 0 deletions
Large diffs are not rendered by default.

.ci/scripts/wheel/test_linux.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from pathlib import Path
1212

1313
import test_base
14+
import test_cpp_sdk
1415
import test_shared_libraries
1516
from examples.models import Backend, Model
1617

@@ -50,6 +51,13 @@
5051
with tempfile.TemporaryDirectory() as work_dir:
5152
test_shared_libraries.run_tests(Path(work_dir))
5253

54+
# And that a C++ application outside the wheel can actually use them.
55+
# Nothing above covers this: the Python extension links those libraries
56+
# itself, so it passes whether or not the package config names them or the
57+
# shipped headers are complete.
58+
with tempfile.TemporaryDirectory() as work_dir:
59+
test_cpp_sdk.run_tests(Path(work_dir))
60+
5361
test_base.run_tests(
5462
model_tests=[
5563
test_base.ModelTest(

.ci/scripts/wheel/test_linux_aarch64.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from pathlib import Path
1010

1111
import test_base
12+
import test_cpp_sdk
1213
import test_shared_libraries
1314
from examples.models import Backend, Model
1415

@@ -36,6 +37,11 @@
3637
with tempfile.TemporaryDirectory() as work_dir:
3738
test_shared_libraries.run_tests(Path(work_dir))
3839

40+
# And that a C++ application outside the wheel can actually use those
41+
# libraries, which nothing above covers.
42+
with tempfile.TemporaryDirectory() as work_dir:
43+
test_cpp_sdk.run_tests(Path(work_dir))
44+
3945
test_base.run_tests(
4046
model_tests=[
4147
test_base.ModelTest(

README-wheel.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ The prebuilt `executorch.runtime` module included in this package provides a way
2222
to run ExecuTorch `.pte` files, with some restrictions:
2323
* Only [core ATen operators](docs/source/ir-ops-set-definition.md) are linked into the prebuilt module
2424
* Only the [XNNPACK backend delegate](docs/source/backends/xnnpack/xnnpack-overview.md) is linked into the prebuilt module.
25-
* \[macOS only] [Core ML](docs/source/backends/coreml/coreml-overview.md) and [MPS](docs/source/backends/mps/mps-overview.md) backend
26-
are also linked into the prebuilt module.
25+
* \[macOS only] [Core ML](docs/source/backends/coreml/coreml-overview.md) backend is
26+
also linked into the prebuilt module.
2727
* \[Linux x86_64] [QNN](docs/source/backends-qualcomm.md) backend is linked into the prebuilt module.
2828
* \[Linux] [OpenVINO](docs/source/build-run-openvino.md) backend is also linked into the
2929
prebuilt module. OpenVINO requires the runtime to be installed separately:

devtools/etdump/etdump_flatcc.h

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,6 @@ class ETDumpGen : public ::executorch::runtime::EventTracer {
7474
public:
7575
ETDumpGen(::executorch::runtime::Span<uint8_t> buffer = {nullptr, (size_t)0});
7676
~ETDumpGen() override;
77-
void clear_builder();
7877

7978
void create_event_block(const char* name) override;
8079
virtual ::executorch::runtime::EventTracerEntry start_profiling(

docs/source/using-executorch-cpp.md

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,151 @@ Running a model using the low-level runtime APIs allows for a high-degree of con
4040
4141
## Building with CMake
4242
43+
There are two ways to get the C++ runtime. Linking the prebuilt libraries from the pip
44+
package needs no source checkout and is the quicker option. Building from source gives
45+
you every option the project has, and is what you need for a platform the wheel does not
46+
cover.
47+
48+
### Using the prebuilt libraries from the pip package
49+
50+
On Linux, `pip install executorch` includes prebuilt shared libraries, the public
51+
headers, and a CMake package, so a C++ application can link the runtime without building
52+
ExecuTorch itself:
53+
54+
```cmake
55+
# CMakeLists.txt
56+
cmake_minimum_required(VERSION 3.28)
57+
project(my_app CXX)
58+
59+
find_package(executorch REQUIRED COMPONENTS kernels_optimized)
60+
61+
add_executable(my_app main.cpp)
62+
target_link_libraries(my_app PRIVATE executorch::runtime
63+
executorch::kernels_optimized)
64+
```
65+
66+
Point CMake at the installed package when you configure:
67+
68+
```
69+
cmake -S . -B build \
70+
-DCMAKE_PREFIX_PATH="$(python -c 'import executorch, pathlib; print(pathlib.Path(executorch.__path__[0]) / "share" / "cmake")')"
71+
cmake --build build
72+
```
73+
74+
The application uses the same `Module` and `TensorPtr` APIs described above:
75+
76+
```cpp
77+
// main.cpp
78+
#include <executorch/extension/module/module.h>
79+
#include <executorch/extension/tensor/tensor.h>
80+
81+
#include <cstdio>
82+
#include <vector>
83+
84+
using namespace executorch::extension;
85+
86+
int main() {
87+
Module module("model.pte");
88+
89+
std::vector<float> data(2 * 8, 1.0f);
90+
auto input = make_tensor_ptr({2, 8}, data.data());
91+
92+
const auto result = module.forward(input);
93+
if (!result.ok()) {
94+
std::printf("forward failed: 0x%x\n", (unsigned)result.error());
95+
return 1;
96+
}
97+
std::printf("ok, %zu outputs\n", result->size());
98+
return 0;
99+
}
100+
```
101+
102+
#### What each component provides
103+
104+
Ask for the components your model needs. A component the wheel was not built with is
105+
reported while CMake configures, rather than failing later at link time.
106+
107+
| Component | What it provides |
108+
| --- | --- |
109+
| `executorch::runtime` | the program loader and executor. Always present. |
110+
| `executorch::kernels_optimized` | CPU operator kernels. Needed for any operator a delegate does not claim. |
111+
| `executorch::backend_xnnpack` | the XNNPACK delegate. |
112+
| `executorch::threadpool` | the shared thread pool. |
113+
| `executorch::etdump` | the profiler. |
114+
115+
The runtime on its own loads a program but registers only primitive operators, not the
116+
kernels a model computes with, so a model that is not fully delegated needs a kernel
117+
component too. Linking a delegate is what registers it: a program delegated to XNNPACK
118+
fails to load in an application that did not link `executorch::backend_xnnpack`.
119+
120+
To require a minimum version, pass it to `find_package`:
121+
122+
```cmake
123+
find_package(executorch 1.0 REQUIRED)
124+
```
125+
126+
#### On CMake older than 3.28
127+
128+
The example above needs CMake 3.28. Older versions write the `$ORIGIN` marker (the
129+
"look next to me" token in a library search path) incorrectly, which would leave you with
130+
a target that runs where it was built and fails once the application is copied
131+
elsewhere. Rather than hand you a target that behaves that way, the package defines no
132+
imported targets below 3.28 and exports plain variables instead.
133+
134+
An imported target carries more than a library path, so on this route you have to apply
135+
the rest yourself. Linking the libraries alone does not compile:
136+
137+
```cmake
138+
cmake_minimum_required(VERSION 3.19)
139+
project(my_app CXX)
140+
141+
find_package(executorch REQUIRED)
142+
143+
add_executable(my_app main.cpp)
144+
target_include_directories(my_app PRIVATE ${EXECUTORCH_INCLUDE_DIRS})
145+
target_compile_definitions(my_app PRIVATE ${EXECUTORCH_COMPILE_DEFINITIONS})
146+
target_link_libraries(my_app PRIVATE ${EXECUTORCH_LIBRARIES})
147+
set_property(TARGET my_app PROPERTY CXX_STANDARD ${EXECUTORCH_CXX_STANDARD})
148+
set_property(TARGET my_app PROPERTY CXX_STANDARD_REQUIRED ON)
149+
```
150+
151+
On this route the application also has to record where the libraries live, or it runs
152+
from its build directory and then fails to start once installed with a message like
153+
`libexecutorch.so: cannot open shared object file`. CMake records the wheel's library
154+
directory while building, because the libraries are named by absolute path, but it removes
155+
that entry on install. Ask for it to be kept:
156+
157+
```cmake
158+
set_property(TARGET my_app PROPERTY INSTALL_RPATH "${EXECUTORCH_RUNTIME_LIBRARY_DIR}")
159+
target_link_options(my_app PRIVATE "LINKER:--enable-new-dtags")
160+
```
161+
162+
The second line matters on Linux. Without it this linker records the older `DT_RPATH` tag, which is
163+
searched before `LD_LIBRARY_PATH` and also applies to your dependencies' own dependencies, so you
164+
could not point the application at a different build of the runtime. With it you get `DT_RUNPATH`,
165+
which only affects your application and stays overridable.
166+
167+
The imported target route does not need this on Linux: the package sets its search paths as
168+
explicit link options, and those survive installation.
169+
170+
On macOS it does need one line. CMake removes an entry that points at a directory holding a
171+
library the application linked, so the entry naming the wheel's own directory is deleted from
172+
the installed binary and it stops finding the runtime:
173+
174+
```cmake
175+
set_property(TARGET my_app PROPERTY INSTALL_RPATH_USE_LINK_PATH TRUE)
176+
```
177+
178+
An application deployed beside the libraries is unaffected either way, because the
179+
`@loader_path` and `$ORIGIN` entries are kept.
180+
181+
`EXECUTORCH_LIBRARIES` names the runtime and every component the wheel shipped, so you
182+
cannot choose components on this route. Upgrade to CMake 3.28 and link the specific
183+
targets you need instead.
184+
185+
### Building from source
186+
187+
43188
ExecuTorch uses CMake as the primary build system. Inclusion of the module and tensor APIs are controlled by the `EXECUTORCH_BUILD_EXTENSION_MODULE` and `EXECUTORCH_BUILD_EXTENSION_TENSOR` CMake options. As these APIs may not be supported on embedded systems, they are disabled by default when building from source. The low-level API surface is always included. To link, add the `executorch` target as a CMake dependency, along with `executorch_backends`, `executorch_extensions`, and `extension_kernels`, to link all configured backends, extensions, and kernels.
44189

45190
```

extension/memory_allocator/memory_allocator_utils.h

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,10 @@
1616
#include <executorch/runtime/core/result.h>
1717
#include <executorch/runtime/platform/compiler.h>
1818

19-
using executorch::runtime::Error;
20-
using executorch::runtime::Result;
2119
namespace executorch::extension::utils {
2220

2321
// Util to get alighment adjusted allocation size
24-
inline Result<size_t> get_aligned_size(size_t size, size_t alignment) {
22+
inline runtime::Result<size_t> get_aligned_size(size_t size, size_t alignment) {
2523
// The minimum alignment that malloc() is guaranteed to provide.
2624
static constexpr size_t kMallocAlignment = alignof(std::max_align_t);
2725
if (alignment > kMallocAlignment) {
@@ -31,7 +29,7 @@ inline Result<size_t> get_aligned_size(size_t size, size_t alignment) {
3129
const size_t extra = alignment - 1;
3230
if ET_UNLIKELY (extra >= SIZE_MAX - size) {
3331
ET_LOG(Error, "Malloc size overflow: size=%zu + extra=%zu", size, extra);
34-
return Result<size_t>(Error::InvalidArgument);
32+
return runtime::Result<size_t>(runtime::Error::InvalidArgument);
3533
}
3634
size += extra;
3735
}

runtime/executor/platform_memory_allocator.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
#include <cstdint>
1414

1515
#include <c10/util/safe_numerics.h>
16+
#include <executorch/runtime/core/exec_aten/exec_aten.h>
1617
#include <executorch/runtime/core/memory_allocator.h>
1718
#include <executorch/runtime/platform/log.h>
1819
#include <executorch/runtime/platform/platform.h>

0 commit comments

Comments
 (0)