Skip to content

Commit 1fd5562

Browse files
authored
Fix dynamic linking issues with prebuilt pip packages (#3049)
* Build pybindings with -D_GLIBCXX_USE_CXX11_ABI=0 to match libtorch.so libtorch.so builds with the old glibc ABI, so we need to as well, for any source files that include torch headers. * Set the RPATH of _portable_lib.so so it can find libtorch pip wheels will need to be able to find the torch libraries. On Linux, the .so has non-absolute dependencies on libs like "libtorch.so" without paths; as long as we `import torch` first, those dependencies will work. But Apple dylibs do not support non-absolute dependencies, so we need to tell the loader where to look for its libraries. The LC_LOAD_DYLIB entries for the torch libraries will look like "@rpath/libtorch.dylib", so we can add an LC_RPATH entry to look in a directory relative to the installed location of our _portable_lib.so file. To see these LC_* values, run `otool -l _portable_lib*.so`. * Disable wheel delocation on macos The executorch build system will ensure that .dylib/.so files have LC_LOAD_DYLIB and LC_RPATH entries that will work when they're installed. Delocating (i.e., making copies of the .dylibs that ET's libs depend on) will break any libs that depend on the torch libraries if users ever import both `torch` and the executorch library. Both import paths must load exactly the same file, not just a copy of it. * Implemement smoke_test.py for pip wheel jobs This script is run by CI after building the executorch wheel. Before running this, the job will install the matching torch package as well as the newly-built executorch package and its dependencies. For now we test the export of a simple model, and try executing it using the runtime pybindings. Test Plan: ``` ./install_requirements.sh python build/packaging/smoke_test.py ```
1 parent c79666a commit 1fd5562

File tree

3 files changed

+118
-8
lines changed

3 files changed

+118
-8
lines changed

.github/workflows/build-wheels-m1.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ jobs:
5454
# "recursive" default to do less work, and to give the buck daemon fewer
5555
# files to look at.
5656
submodules: true
57+
delocate-wheel: false
5758
env-var-script: build/packaging/env_var_script_m1.sh
5859
pre-script: ${{ matrix.pre-script }}
5960
post-script: ${{ matrix.post-script }}

CMakeLists.txt

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -532,9 +532,14 @@ if(EXECUTORCH_BUILD_PYBIND)
532532
list(APPEND _dep_libs custom_ops_aot_lib)
533533
endif()
534534
# compile options for pybind
535-
536-
set(_pybind_compile_options -Wno-deprecated-declarations -fPIC -frtti
537-
-fexceptions)
535+
set(_pybind_compile_options
536+
-Wno-deprecated-declarations
537+
-fPIC
538+
-frtti
539+
-fexceptions
540+
# libtorch is built with the old ABI, so we need to do the same for any
541+
# .cpp files that include torch, c10, or ATen targets.
542+
-D_GLIBCXX_USE_CXX11_ABI=0)
538543
# util lib
539544
add_library(
540545
util
@@ -568,6 +573,24 @@ if(EXECUTORCH_BUILD_PYBIND)
568573
${PYBIND_LINK_COREML}
569574
${PYBIND_LINK_MPS}
570575
${PYBIND_LINK_XNNPACK})
576+
if(APPLE)
577+
# pip wheels will need to be able to find the torch libraries. On Linux, the
578+
# .so has non-absolute dependencies on libs like "libtorch.so" without
579+
# paths; as long as we `import torch` first, those dependencies will work.
580+
# But Apple dylibs do not support non-absolute dependencies, so we need to
581+
# tell the loader where to look for its libraries. The LC_LOAD_DYLIB entries
582+
# for the torch libraries will look like "@rpath/libtorch.dylib", so we can
583+
# add an LC_RPATH entry to look in a directory relative to the installed
584+
# location of our _portable_lib.so file. To see these LC_* values, run
585+
# `otool -l _portable_lib*.so`.
586+
set_target_properties(
587+
portable_lib
588+
PROPERTIES # Assume that this library will be installed in
589+
# `site-packages/executorch/extension/pybindings`, and that
590+
# the torch libs are in `site-packages/torch/lib`.
591+
BUILD_RPATH "@loader_path/../../../torch/lib"
592+
INSTALL_RPATH "@loader_path/../../../torch/lib")
593+
endif()
571594

572595
install(TARGETS portable_lib
573596
LIBRARY DESTINATION executorch/extension/pybindings)

build/packaging/smoke_test.py

Lines changed: 91 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,99 @@
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+
"""
9+
This script is run by CI after building the executorch wheel. Before running
10+
this, the job will install the matching torch package as well as the newly-built
11+
executorch package and its dependencies.
12+
"""
13+
14+
# Import this first. If it can't find the torch.so libraries, the dynamic load
15+
# will fail and the process will exit.
16+
from executorch.extension.pybindings import portable_lib # usort: skip
17+
18+
# Import this after importing the ExecuTorch pybindings. If the pybindings
19+
# links against a different torch.so than this uses, there will be a set of
20+
# symbol comflicts; the process will either exit now, or there will be issues
21+
# later in the smoke test.
22+
import torch # usort: skip
23+
24+
# Import everything else later to help isolate the critical imports above.
25+
import os
26+
import tempfile
27+
from typing import Tuple
28+
29+
from executorch.exir import to_edge
30+
from torch.export import export
31+
32+
33+
class LinearModel(torch.nn.Module):
34+
"""Runs Linear on its input, which should have shape [4]."""
35+
36+
def __init__(self):
37+
super().__init__()
38+
self.linear = torch.nn.Linear(4, 2)
39+
40+
def forward(self, x: torch.Tensor):
41+
"""Expects a single tensor of shape [4]."""
42+
return self.linear(x)
43+
44+
45+
def linear_model_inputs() -> Tuple[torch.Tensor]:
46+
"""Returns some example inputs compatible with LinearModel."""
47+
# The model takes a single tensor of shape [4] as an input.
48+
return (torch.ones(4),)
49+
50+
51+
def export_linear_model() -> bytes:
52+
"""Exports LinearModel and returns the .pte data."""
53+
54+
# This helps the exporter understand the shapes of tensors used in the model.
55+
# Since our model only takes one input, this is a one-tuple.
56+
example_inputs = linear_model_inputs()
57+
58+
# Export the pytorch model and process for ExecuTorch.
59+
print("Exporting program...")
60+
exported_program = export(LinearModel(), example_inputs)
61+
print("Lowering to edge...")
62+
edge_program = to_edge(exported_program)
63+
print("Creating ExecuTorch program...")
64+
et_program = edge_program.to_executorch()
65+
66+
return et_program.buffer
67+
868

969
def main():
10-
"""
11-
Run ExecuTorch binary smoke tests. This is a placeholder for future tests. See
12-
https://github.com/pytorch/test-infra/wiki/Using-Nova-Reusable-Build-Workflows
13-
for more information about Nova binary workflow.
14-
"""
70+
"""Tests the export and execution of a simple model."""
71+
72+
# If the pybindings loaded correctly, we should be able to ask for the set
73+
# of operators.
74+
ops = portable_lib._get_operator_names()
75+
assert len(ops) > 0, "Empty operator list"
76+
print(f"Found {len(ops)} operators; first element '{ops[0]}'")
77+
78+
# Export LinearModel to .pte data.
79+
pte_data: bytes = export_linear_model()
80+
81+
# Try saving to and loading from a file.
82+
with tempfile.TemporaryDirectory() as tempdir:
83+
pte_file = os.path.join(tempdir, "linear.pte")
84+
85+
# Save the .pte data to a file.
86+
with open(pte_file, "wb") as file:
87+
file.write(pte_data)
88+
print(f"ExecuTorch program saved to {pte_file} ({len(pte_data)} bytes).")
89+
90+
# Load the model from disk.
91+
m = portable_lib._load_for_executorch(pte_file)
92+
93+
# Run the model.
94+
outputs = m.forward(linear_model_inputs())
95+
96+
# Should see a single output with shape [2].
97+
assert len(outputs) == 1, f"Unexpected output length {len(outputs)}: {outputs}"
98+
assert outputs[0].shape == (2,), f"Unexpected output size {outputs[0].shape}"
99+
100+
print("PASS")
15101

16102

17103
if __name__ == "__main__":

0 commit comments

Comments
 (0)