Skip to content

Commit b81a779

Browse files
committed
Split the runtime out of the Python bindings extension
## The problem `pip install executorch` gives you the Python half of ExecuTorch and nothing a C++ program can link. Everything is fused into one large Python extension file, so a C++ developer has to clone the repository, sync submodules, and build from source. There is a correctness problem underneath the packaging one. Because the runtime is fused into the extension, anything else that needs it gets its own private copy, and two copies mean two registries. A backend registered in one is invisible to the other. ## The change Build the runtime and the pieces around it as separate shared libraries, and make the Python extension link them instead of embedding them. A shared library is a file a program loads at run time, so several programs can use one copy instead of each carrying its own. ``` executorch/ lib/libexecutorch.so the runtime lib/libexecutorch_kernels_optimized.so CPU operator kernels lib/libexecutorch_backend_xnnpack.so the XNNPACK delegate lib/libexecutorch_threadpool.so one thread pool per process lib/libexecutorch_etdump.so the profiler extension/pybindings/_portable_lib.so now under a megabyte, links the above ``` The extension is much smaller than before, because it no longer contains what it now links. Linux only, and only when the CUDA backend is off. macOS and Windows keep the fused extension because the split relies on ELF sonames, the `$ORIGIN` search-path token and GNU linker options, none of which apply there, and enabling it elsewhere now fails while configuring rather than much later. A CUDA build also keeps the fused extension for now, because the CUDA libraries are not yet shipped alongside the others. ## Test plan Built the wheel from source, installed it into a clean environment, and checked: - exactly one library defines each component, and it is the library that should own it. Counting owners alone would also pass on the old fused layout, which has exactly one too. - the Python extension defines none of them and resolves all of them from outside. - every shipped library loads with no unresolved dependency, and none of them searches a directory from the machine that built the wheel. The build-directory patterns are matched as whole path components, because a bare substring also matched an unrelated directory a user could really have, such as `/home/user/cmake-outputs/torchlibs`, and stripping that breaks a dependency the library legitimately resolves there. - a read-only build output is no longer archived world-writable. The previous code granted write to the group and to everyone via an absolute chmod mode; the fix only grants owner-write, so a `0555` file lands as `0755` instead of `0777`. - a custom operator library compiles and links against the shipped Python extension, which is the existing contract this must not break. Linking these libraries from a standalone C++ application additionally needs an installed CMake package, which the wheel does not carry yet. - the ahead-of-time quantized library records its route to the runtime through the same helper the other targets use. It was written by hand in two blocks that between them covered only the wheel layout and only when the wheel flag was set, so a plain `-DEXECUTORCH_BUILD_SHARED=ON` build left it with no route at all. Checked all four combinations of the shared build and the presence of the Python extension. Ran on Linux x86_64 and aarch64, including a Jetson device. Not fixed here: these libraries bundle third-party code that torch also links, and both keep it visible, so a process holds two definitions of symbols like `pthreadpool_create`. A caller reaches whichever the loader found first. Fixing it means hiding or dropping the bundled copies, which is a larger change. ghstack-source-id: b7977f7 ghstack-comment-id: 5200527760 Pull-Request: #21610
1 parent 43f89fb commit b81a779

31 files changed

Lines changed: 2719 additions & 147 deletions
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
# All rights reserved.
3+
#
4+
# This source code is licensed under the BSD-style license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
7+
"""Unit tests for the wheel platform tag comparison.
8+
9+
Here rather than in the wheel checks, because the decision under test is a pure
10+
function of two strings. Running it as part of the wheel checks meant it needed eleven
11+
built wheels to exercise one comparison, and it still could not run on a machine that
12+
had not built one.
13+
14+
The comparison had a real defect that this covers. The release pipeline builds in a
15+
manylinux image and rewrites the wheel's file name, so the tag on the file and the tag
16+
auditwheel reports never agree in spelling, and comparing them as text rejected every
17+
correct wheel. No local build reproduces that rewrite, so nothing short of a unit test
18+
catches it before CI.
19+
"""
20+
21+
import sys
22+
from pathlib import Path
23+
24+
import pytest
25+
26+
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "wheel"))
27+
28+
from test_shared_libraries import ( # noqa: E402
29+
_tag_architectures_match,
30+
_wheel_architecture,
31+
)
32+
33+
34+
@pytest.mark.parametrize(
35+
"claimed,supported",
36+
[
37+
# What the release pipeline actually produces: it builds in a manylinux image
38+
# and rewrites the file name, while auditwheel reports a plain linux tag
39+
# because the wheel depends on torch without vendoring torch's libraries.
40+
("manylinux_2_28_x86_64", "linux_x86_64"),
41+
("manylinux_2_28_aarch64", "linux_aarch64"),
42+
# The legacy spelling, which has no underscore before its version.
43+
("manylinux2014_x86_64", "linux_x86_64"),
44+
("manylinux2014_aarch64", "linux_aarch64"),
45+
# A local build, where nothing rewrites the name.
46+
("linux_x86_64", "linux_x86_64"),
47+
("linux_aarch64", "linux_aarch64"),
48+
],
49+
)
50+
def test_accepts_tags_the_release_pipeline_produces(claimed, supported):
51+
assert _tag_architectures_match(claimed, supported) is True
52+
53+
54+
@pytest.mark.parametrize(
55+
"claimed,supported",
56+
[
57+
("manylinux_2_28_aarch64", "linux_x86_64"),
58+
("manylinux_2_28_x86_64", "linux_aarch64"),
59+
("linux_aarch64", "linux_x86_64"),
60+
],
61+
)
62+
def test_rejects_an_architecture_mismatch(claimed, supported):
63+
"""A wheel labelled for the wrong architecture installs where it cannot run."""
64+
assert _tag_architectures_match(claimed, supported) is False
65+
66+
67+
@pytest.mark.parametrize("tag", ["win_amd64", "macosx_11_0_arm64", "any", "", "linux"])
68+
def test_reports_a_tag_it_cannot_read(tag):
69+
"""None, not False, so an unreadable tag is not mistaken for a mismatch."""
70+
assert _wheel_architecture(tag) is None
71+
assert _tag_architectures_match(tag, "linux_x86_64") is None
72+
73+
74+
def test_reads_every_architecture_the_project_builds_for():
75+
for architecture in ("x86_64", "aarch64", "i686", "ppc64le", "s390x", "armv7l"):
76+
assert _wheel_architecture(f"linux_{architecture}") == architecture
77+
assert _wheel_architecture(f"manylinux_2_28_{architecture}") == architecture

.ci/scripts/wheel/test_linux.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,11 @@
77
# LICENSE file in the root directory of this source tree.
88

99
import platform
10+
import tempfile
11+
from pathlib import Path
1012

1113
import test_base
14+
import test_shared_libraries
1215
from examples.models import Backend, Model
1316

1417
if __name__ == "__main__":
@@ -41,6 +44,12 @@
4144

4245
test_base.test_cmsis_nn_install()
4346

47+
# The wheel ships the runtime, the kernels, the delegate, the thread
48+
# pool and the profiler as separate shared libraries now, so check that
49+
# each has exactly one owner and that all of them are loadable.
50+
with tempfile.TemporaryDirectory() as work_dir:
51+
test_shared_libraries.run_tests(Path(work_dir))
52+
4453
test_base.run_tests(
4554
model_tests=[
4655
test_base.ModelTest(

.ci/scripts/wheel/test_linux_aarch64.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@
55
# This source code is licensed under the BSD-style license found in the
66
# LICENSE file in the root directory of this source tree.
77

8+
import tempfile
9+
from pathlib import Path
10+
811
import test_base
12+
import test_shared_libraries
913
from examples.models import Backend, Model
1014

1115
if __name__ == "__main__":
@@ -26,6 +30,12 @@
2630
), f"OpenvinoBackend not found in registered backends: {registered}"
2731
print("✓ OpenvinoBackend is registered")
2832

33+
# The wheel ships the runtime, the kernels, the delegate, the thread pool and
34+
# the profiler as separate shared libraries now, so check that each has
35+
# exactly one owner and that all of them are loadable.
36+
with tempfile.TemporaryDirectory() as work_dir:
37+
test_shared_libraries.run_tests(Path(work_dir))
38+
2939
test_base.run_tests(
3040
model_tests=[
3141
test_base.ModelTest(

0 commit comments

Comments
 (0)