diff --git a/.ci/scripts/setup-windows.ps1 b/.ci/scripts/setup-windows.ps1 index a38d43bbfd8..ab0f8ffca07 100644 --- a/.ci/scripts/setup-windows.ps1 +++ b/.ci/scripts/setup-windows.ps1 @@ -21,8 +21,8 @@ $env:CMAKE_ARGS = "$env:CMAKE_ARGS -DCMAKE_CXX_STANDARD=20" # The Windows CI image ships CUDA toolkits on PATH, so install_executorch # (setup.py) auto-enables EXECUTORCH_BUILD_CUDA whenever the detected nvcc # version is in SUPPORTED_CUDA_VERSIONS. CPU-only jobs install CPU torch, so a -# CUDA build of _portable_lib then fails to load its CUDA DLLs at import time -# ("DLL load failed while importing _portable_lib"). Force a CPU-only build +# CUDA build of _C then fails to load its CUDA DLLs at import time +# ("DLL load failed while importing _C"). Force a CPU-only build # when the caller asks for it. if ($cpuOnly -eq 'true') { $env:CMAKE_ARGS = "$env:CMAKE_ARGS -DEXECUTORCH_BUILD_CUDA=OFF" diff --git a/.ci/scripts/wheel/pre_build_script.sh b/.ci/scripts/wheel/pre_build_script.sh index e795295cb68..ae184cff309 100755 --- a/.ci/scripts/wheel/pre_build_script.sh +++ b/.ci/scripts/wheel/pre_build_script.sh @@ -128,8 +128,8 @@ if [[ $UNAME_S == *"MINGW"* || $UNAME_S == *"MSYS"* ]]; then # Windows wheels are CPU-only (build-wheels-windows.yml sets # with-cuda: disabled), but the Windows CI image ships a CUDA toolkit on # PATH, which makes setup.py auto-enable EXECUTORCH_BUILD_CUDA. That bakes a - # CUDA _portable_lib into the CPU wheel, which then fails its DLL load in the - # smoke test ("DLL load failed while importing _portable_lib"). Force a + # CUDA _C into the CPU wheel, which then fails its DLL load in the + # smoke test ("DLL load failed while importing _C"). Force a # CPU-only build. export CMAKE_ARGS="${CMAKE_ARGS:-} -DEXECUTORCH_BUILD_CUDA=OFF" echo "CMAKE_ARGS=${CMAKE_ARGS}" >> "${GITHUB_ENV}" diff --git a/.ci/scripts/wheel/test_base.py b/.ci/scripts/wheel/test_base.py index 2b0d3c0dcb4..1ab21a30ca8 100644 --- a/.ci/scripts/wheel/test_base.py +++ b/.ci/scripts/wheel/test_base.py @@ -59,16 +59,16 @@ def test_cmsis_nn_install(): def run_tests(model_tests: List[ModelTest]) -> None: - # Test that we can import the portable_lib module - verifies RPATH is correct - print("Testing portable_lib import...") + # Test that we can import the _C module - verifies RPATH is correct + print("Testing _C import...") try: - from executorch.extension.pybindings._portable_lib import ( # noqa: F401 + from executorch.extension.pybindings._C import ( # noqa: F401 _load_for_executorch, ) - print("✓ Successfully imported _load_for_executorch from portable_lib") + print("✓ Successfully imported _load_for_executorch from _C") except ImportError as e: - print(f"✗ Failed to import portable_lib: {e}") + print(f"✗ Failed to import _C: {e}") raise # Why are we doing this envvar shenanigans? Since we build the testers, which diff --git a/.ci/scripts/wheel/test_shared_libraries.py b/.ci/scripts/wheel/test_shared_libraries.py index abd55842673..27eb755243b 100644 --- a/.ci/scripts/wheel/test_shared_libraries.py +++ b/.ci/scripts/wheel/test_shared_libraries.py @@ -234,6 +234,20 @@ def _declared_requirements() -> set: return names +def _cmake_version() -> tuple[int, ...] | None: + """The running cmake's version, or None when it cannot be determined.""" + cmake = _tool("cmake") + if cmake is None: + return None + probe = subprocess.run([cmake, "--version"], capture_output=True, text=True) + if probe.returncode != 0: + return None + match = re.search(r"cmake version (\d+)\.(\d+)", probe.stdout) + # Unparseable output reads as unknown rather than as old, so a future format change + # runs the real check instead of silently skipping it. + return tuple(int(part) for part in match.groups()) if match else None + + def _installed_package_dir() -> Path: """The installed executorch package, never the source checkout. @@ -675,7 +689,20 @@ def _defines_symbol(library: Path, symbol: str) -> bool: # "defines nothing" would let a duplicate pass. The ELF magic bytes tell them apart # without depending on the reader's wording. with library.open("rb") as handle: - is_object_file = handle.read(4) == b"\x7fELF" + magic = handle.read(4) + # Mach-O too, not only ELF. Testing for the ELF magic alone made every macOS + # library read as "not an object file", so the assert below never fired there and + # an unreadable dylib was scored as defining nothing, which is the case it exists + # to catch. 64 and 32 bit, thin and fat, both byte orders. + is_object_file = magic in ( + b"\x7fELF", + b"\xcf\xfa\xed\xfe", + b"\xce\xfa\xed\xfe", + b"\xfe\xed\xfa\xcf", + b"\xfe\xed\xfa\xce", + b"\xca\xfe\xba\xbe", + b"\xbe\xba\xfe\xca", + ) assert not is_object_file, ( f"nm could not read {library.name}, which is a shipped object file, so the symbol " f"checks cannot be trusted: {result.stderr.strip()[:200]}" @@ -1116,15 +1143,26 @@ def test_python_extensions_import() -> None: _CUSTOM_OP_CMAKE = """\ -cmake_minimum_required(VERSION 3.24) +# 3.28, not the 3.24 floor this package supports, because the imported targets this +# project links are deliberately not created before that version. +cmake_minimum_required(VERSION 3.28) project(custom_op_check CXX) find_package(executorch REQUIRED) +# An unknown name here would be passed to the linker as a plain library rather +# than reported, so the include directories would silently not arrive and the +# build would fail later on a missing header. +if(NOT TARGET executorch::runtime) + message(FATAL_ERROR "cannot link a target that does not exist: executorch::runtime") +endif() + add_library(custom_op_check SHARED custom_op.cpp) -# The legacy contract: a custom-op library links the shipped Python extension, -# which owns the operator registry it registers into. -target_link_libraries(custom_op_check PRIVATE _portable_lib) +# The runtime owns the operator registry. It used to live in the Python extension, so a +# custom-op library linked that; since the split it is in libexecutorch, and linking the +# extension from C++ cannot work because its CPython symbols only resolve inside an +# interpreter. Linking the runtime reaches the same registry singleton. +target_link_libraries(custom_op_check PRIVATE executorch::runtime) # The runtime headers include c10 headers, which belong to torch rather than to # this wheel, so an out-of-tree operator project supplies them the same way it # supplies torch itself. The package config does not and should not ship them. @@ -1191,7 +1229,13 @@ def _dyld_load_failure(library: Path, *, with_torch: bool) -> str: ) if result.returncode == 0: return "" - return (result.stdout + result.stderr).strip() + message = (result.stdout + result.stderr).strip() + # A signal leaves the message empty, and an empty return here reads as a clean load. + # The kernel killing the process during loading is the case an invalid signature + # produces, so it has to be named rather than dropped. + if not message and result.returncode < 0: + return f"the loader was killed by signal {-result.returncode}" + return message def _dyld_missing_names(message: str) -> list[str]: @@ -1582,9 +1626,19 @@ def test_custom_op_compiles(work_dir: Path) -> None: if _tool("cmake") is None: print("- cmake unavailable, skipping the custom op check") return + # The project below requires 3.28, where the imported targets it links start existing. + # An older cmake refuses to configure at all, which would read as the wheel being at + # fault, so it is reported as the version skip it is. + cmake_version = _cmake_version() + if cmake_version is not None and cmake_version < (3, 28): + print( + f"- cmake {'.'.join(str(p) for p in cmake_version)} is older than the 3.28 " + "these targets need, skipping the custom op check" + ) + return package_dir = _installed_package_dir() - if not list(package_dir.glob("extension/pybindings/_portable_lib*")): + if not list(package_dir.glob("extension/pybindings/_C*")): print("- the wheel ships no Python extension, skipping the custom op check") return @@ -2046,10 +2100,8 @@ def test_extension_contains_no_component() -> None: assert _tool("nm") is not None, "nm is required to inspect the wheel" package_dir = _installed_package_dir() - extensions = sorted( - (package_dir / "extension" / "pybindings").glob("_portable_lib.*.so") - ) - assert len(extensions) == 1, f"expected one _portable_lib, found {extensions}" + extensions = sorted((package_dir / "extension" / "pybindings").glob("_C.*.so")) + assert len(extensions) == 1, f"expected one _C, found {extensions}" extension = extensions[0] lib_dir = package_dir / "lib" diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index cfb4106c830..3393f2dbfc6 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -54,7 +54,7 @@ jobs: - name: Generate mypy stubs for C++ bindings run: | - cp extension/pybindings/pybindings.pyi extension/pybindings/_portable_lib.pyi + cp extension/pybindings/pybindings.pyi extension/pybindings/_C.pyi - name: Run mypy run: | diff --git a/CMakeLists.txt b/CMakeLists.txt index 85374a356d2..460c6a4a041 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -486,8 +486,8 @@ if(EXECUTORCH_BUILD_PTHREADPOOL) if(APPLE) # Use hidden visibility for pthreadpool on Apple platforms to avoid issues # with pthreadpool symbols from libtorch_cpu taking precedence over the ones - # from the pthreadpool library statically linked in _portable_lib. The - # pthreadpool public APIs are marked as weak by default on some Apple + # from the pthreadpool library statically linked in the Python extension. + # The pthreadpool public APIs are marked as weak by default on some Apple # platforms, so setting to hidden visibility works around this by not # putting the symbol in the indirection table. See # https://github.com/pytorch/executorch/issues/14321 for more details. @@ -1257,24 +1257,21 @@ if(EXECUTORCH_BUILD_PYBIND) list(APPEND _dep_libs aoti_common) endif() - # RPATH for _portable_lib.so. It sits in + # RPATH for the Python extension. It sits in # /executorch/extension/pybindings, so torch is three levels up - # and the wheel's own lib/ directory is two. Mach-O spells the loader relative - # token differently and takes a list rather than a colon joined string, so - # both differ here while the layout reasoning does not. - if(APPLE) - set(_portable_lib_origin "@loader_path") - set(_portable_lib_rpath_separator ";") - else() - set(_portable_lib_origin "$ORIGIN") - set(_portable_lib_rpath_separator ":") - endif() - set(_portable_lib_rpath "${_portable_lib_origin}/../../../torch/lib") + # and the wheel's own lib/ directory is two. + # + # ELF spelling. The Mach-O branch below takes only the Torch hop from this + # list, because nothing else records it there. The other entries reach a + # Mach-O build another way: executorch_target_shared_runtime_path emits lib + # and src/executorch/lib, and setup.py's _SIBLING_LIBRARY_DIRECTORIES adds + # backends/cuda in the platform's own token at packaging time. + set(_python_extension_rpath "$ORIGIN/../../../torch/lib") # An editable install copies this extension into a package directory whose # subdirectories are symlinks to the checkout, and the loader resolves the # origin token against the real path, so the runtime sits two levels up and # then inside src/executorch/lib rather than beside the copy. - string(APPEND _portable_lib_rpath ":$ORIGIN/../../src/executorch/lib") + string(APPEND _python_extension_rpath ":$ORIGIN/../../src/executorch/lib") if(EXECUTORCH_BUILD_EXTENSION_MODULE) # extension_module_static is already bundled into libexecutorch.so; linking @@ -1324,20 +1321,12 @@ if(EXECUTORCH_BUILD_PYBIND) endif() if(EXECUTORCH_BUILD_CUDA OR EXECUTORCH_BUILD_ROCM) - string( - APPEND - _portable_lib_rpath - "${_portable_lib_rpath_separator}${_portable_lib_origin}/../../backends/cuda" - ) + string(APPEND _python_extension_rpath ":$ORIGIN/../../backends/cuda") endif() if(EXECUTORCH_BUILD_QNN) list(APPEND _dep_libs qnn_executorch_backend) - string( - APPEND - _portable_lib_rpath - "${_portable_lib_rpath_separator}${_portable_lib_origin}/../../backends/qualcomm" - ) + string(APPEND _python_extension_rpath ":$ORIGIN/../../backends/qualcomm") endif() if(EXECUTORCH_BUILD_ENN) @@ -1399,10 +1388,10 @@ if(EXECUTORCH_BUILD_PYBIND) # portable_lib.py in the same python package. PyTorch requires C++20, so # pybindings must be compiled with C++20. set_target_properties( - portable_lib PROPERTIES OUTPUT_NAME "_portable_lib" CXX_STANDARD 20 + portable_lib PROPERTIES OUTPUT_NAME "_C" CXX_STANDARD 20 ) target_compile_definitions( - portable_lib PUBLIC EXECUTORCH_PYTHON_MODULE_NAME=_portable_lib + portable_lib PUBLIC EXECUTORCH_PYTHON_MODULE_NAME=_C ) target_include_directories(portable_lib PRIVATE ${TORCH_INCLUDE_DIRS}) target_compile_options(portable_lib PUBLIC ${_pybind_compile_options}) @@ -1457,13 +1446,13 @@ if(EXECUTORCH_BUILD_PYBIND) # A token with a subpath is a distinct entry, and this target already ships # several. if(APPLE) - set(_portable_lib_torch_path "@loader_path/../../../torch/lib") + set(_python_extension_torch_path "@loader_path/../../../torch/lib") else() - set(_portable_lib_torch_path "${_portable_lib_rpath}") + set(_python_extension_torch_path "${_python_extension_rpath}") endif() set_target_properties( - portable_lib PROPERTIES BUILD_RPATH "${_portable_lib_torch_path}" - INSTALL_RPATH "${_portable_lib_torch_path}" + portable_lib PROPERTIES BUILD_RPATH "${_python_extension_torch_path}" + INSTALL_RPATH "${_python_extension_torch_path}" ) executorch_target_shared_runtime_path( portable_lib "extension/pybindings" "executorch/extension/pybindings" @@ -1499,10 +1488,10 @@ if(EXECUTORCH_BUILD_PYBIND) LIBRARY DESTINATION executorch/extension/pybindings ) - # Copy MLX metallib next to _portable_lib.so for editable installs. MLX uses - # dladdr() to find the directory containing the library with MLX code, then - # looks for mlx.metallib in that directory. When MLX is statically linked into - # _portable_lib.so, we need the metallib colocated with it. + # Copy the MLX metallib next to the Python extension for editable installs. + # MLX uses dladdr() to find the directory containing the library with MLX + # code, then looks for mlx.metallib in that directory. When MLX is statically + # linked into the Python extension, we need the metallib colocated with it. executorch_target_copy_mlx_metallib(portable_lib) endif() diff --git a/backends/mlx/CMakeLists.txt b/backends/mlx/CMakeLists.txt index 9f2ece7155f..903345b2273 100644 --- a/backends/mlx/CMakeLists.txt +++ b/backends/mlx/CMakeLists.txt @@ -339,16 +339,16 @@ install( # exist) # # where {binary_dir} is determined at runtime via dladdr() on the library -# containing MLX code. When MLX is statically linked into _portable_lib.so, this -# is the directory containing _portable_lib.so. +# containing MLX code. When MLX is statically linked into _C.so, this is the +# directory containing _C.so. # # For the installed library, we put metallib in lib/ alongside libmlx.a. The # metallib is produced in the mlx_external build tree (MLX_METAL_JIT=ON does not # install it); _mlx_metallib points there. install(FILES ${_mlx_metallib} DESTINATION ${CMAKE_INSTALL_LIBDIR}) -# Cache the metallib path for pybindings to copy it next to _portable_lib.so -# This enables editable installs to work correctly +# Cache the metallib path for pybindings to copy it next to _C.so. This enables +# editable installs to work correctly set(MLX_METALLIB_PATH "${_mlx_metallib}" CACHE INTERNAL "Path to mlx.metallib for pybindings" diff --git a/extension/pybindings/BUCK b/extension/pybindings/BUCK index 78100500df3..1c39ea14d28 100644 --- a/extension/pybindings/BUCK +++ b/extension/pybindings/BUCK @@ -34,9 +34,9 @@ fbcode_target(_kind = runtime.genrule, outs = { "aten_lib.pyi": ["aten_lib.pyi"], "core.pyi": ["core.pyi"], - "_portable_lib.pyi": ["_portable_lib.pyi"], + "_C.pyi": ["_C.pyi"], }, - cmd = "cp $(location :pybinding_types)/* $OUT/_portable_lib.pyi && cp $(location :pybinding_types)/* $OUT/aten_lib.pyi && cp $(location :pybinding_types)/* $OUT/core.pyi", + cmd = "cp $(location :pybinding_types)/* $OUT/_C.pyi && cp $(location :pybinding_types)/* $OUT/aten_lib.pyi && cp $(location :pybinding_types)/* $OUT/core.pyi", visibility = ["//executorch/extension/pybindings/..."], ) @@ -50,8 +50,8 @@ fbcode_target(_kind = executorch_pybindings, fbcode_target(_kind = executorch_pybindings, cppdeps = PORTABLE_MODULE_DEPS + MODELS_ATEN_OPS_LEAN_MODE_GENERATED_LIB, # Give this an underscore prefix because it has a pure python wrapper. - python_module_name = "_portable_lib", - types = ["//executorch/extension/pybindings:pybindings_types_gen[_portable_lib.pyi]"], + python_module_name = "_C", + types = ["//executorch/extension/pybindings:pybindings_types_gen[_C.pyi]"], visibility = ["PUBLIC"], ) @@ -67,7 +67,7 @@ fbcode_target(_kind = runtime.python_library, srcs = ["portable_lib.py"], visibility = ["PUBLIC"], deps = [ - ":_portable_lib", + ":_C", "//executorch/exir:_warnings", ], ) diff --git a/extension/pybindings/portable_lib.py b/extension/pybindings/portable_lib.py index 4be4b8956e8..d8813539b45 100644 --- a/extension/pybindings/portable_lib.py +++ b/extension/pybindings/portable_lib.py @@ -72,12 +72,12 @@ e, ) -# Let users import everything from the C++ _portable_lib extension as if this +# Let users import everything from the C++ _C extension as if this # python file defined them. Although we could import these dynamically, it # wouldn't preserve the static type annotations. # # Note that all of these are experimental, and subject to change without notice. -from executorch.extension.pybindings._portable_lib import ( # noqa: F401 +from executorch.extension.pybindings._C import ( # noqa: F401 # Disable "imported but unused" (F401) checks. _create_profile_block, # noqa: F401 _dump_profile_results, # noqa: F401 @@ -101,7 +101,7 @@ Verification, # noqa: F401 ) -# Clean up so that `dir(portable_lib)` is the same as `dir(_portable_lib)` +# Clean up so that `dir(portable_lib)` is the same as `dir(_C)` # (apart from some __dunder__ names). del _torch del _exir_warnings diff --git a/kernels/quantized/CMakeLists.txt b/kernels/quantized/CMakeLists.txt index cbb03b7ca18..342f92eb958 100644 --- a/kernels/quantized/CMakeLists.txt +++ b/kernels/quantized/CMakeLists.txt @@ -144,20 +144,19 @@ if(NOT CMAKE_GENERATOR STREQUAL "Xcode" # non-absolute name the way Linux can. if(TARGET portable_lib) # pip wheels will need to be able to find the dependent libraries. On - # Linux, the .so has non-absolute dependencies on libs like - # "_portable_lib.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 portable_lib - # libraries will look like "@rpath/_portable_lib.cpython-310-darwin.so", - # 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 libquantized_ops_lib.dylib`. "extension", not - # "extensions": the plural directory does not exist, so the parent's path - # reached nothing and this library could not find the extension it needs. - # torch is three directories up from here, and this library links it - # directly, so the hop has to be recorded or the only route is the - # absolute path from the build machine. + # Linux, the .so has non-absolute dependencies on libs like "_C.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 extension will look like + # "@rpath/_C.cpython-310-darwin.so", so we can add an LC_RPATH entry to + # look in a directory relative to where that extension is installed. To + # see these LC_* values, run `otool -l libquantized_ops_lib.dylib`. + # "extension", not "extensions": the plural directory does not exist, so + # the parent's path reached nothing and this library could not find the + # extension it needs. torch is three directories up from here, and this + # library links it directly, so the hop has to be recorded or the only + # route is the absolute path from the build machine. if(APPLE) set(RPATH "@loader_path/../../extension/pybindings;@loader_path/../../../torch/lib" diff --git a/runtime/__init__.py b/runtime/__init__.py index 97b99df559b..161786e9574 100644 --- a/runtime/__init__.py +++ b/runtime/__init__.py @@ -122,8 +122,8 @@ ) except ModuleNotFoundError as e: raise ModuleNotFoundError( - "Prebuilt /extension/pybindings/_portable_lib.so " - "is not found. Please reinstall ExecuTorch from pip." + "The prebuilt extension module executorch.extension.pybindings._C is not " + "found. Please reinstall ExecuTorch from pip." ) from e diff --git a/setup.py b/setup.py index 1577e8c273f..392919c4de0 100644 --- a/setup.py +++ b/setup.py @@ -2287,8 +2287,8 @@ def run(self): # noqa C901 # portable kernels, and a selection of backends. This lets users # load and execute .pte files from python. BuiltExtension( - src="_portable_lib.cp*" if _is_windows() else "_portable_lib.*", - modpath="executorch.extension.pybindings._portable_lib", + src="_C.cp*" if _is_windows() else "_C.*", + modpath="executorch.extension.pybindings._C", dependent_cmake_flags=["EXECUTORCH_BUILD_PYBIND"], ), # Install the data_loader pybindings extension which provides the @@ -2298,7 +2298,7 @@ def run(self): # noqa C901 modpath="executorch.extension.pybindings.data_loader", dependent_cmake_flags=["EXECUTORCH_BUILD_PYBIND"], ), - # MLX metallib (Metal GPU kernels) must be colocated with _portable_lib.so + # MLX metallib (Metal GPU kernels) must be colocated with _C.so # because MLX uses dladdr() to find the directory containing the library, # then looks for mlx.metallib in that directory at runtime. # After submodule migration, the path is backends/mlx/mlx/... diff --git a/tools/cmake/Utils.cmake b/tools/cmake/Utils.cmake index f9f04cda758..af009565179 100644 --- a/tools/cmake/Utils.cmake +++ b/tools/cmake/Utils.cmake @@ -493,6 +493,18 @@ function(executorch_target_shared_runtime_path target_name wheel_subdir if(_existing) set(_paths "${_existing}${_separator}${_paths}") endif() + # Collapse repeats here rather than at a call site. A caller that already set + # a runtime path may name a hop this function also emits, and the shipped + # extension carried $ORIGIN/../../src/executorch/lib twice for exactly that + # reason, so the loader searched one nonexistent directory twice on every + # load. This is the only point where both contributions are present, so doing + # it here covers every caller rather than one. + # + # Order is preserved, which matters: the loader searches in sequence, so the + # wheel's own lib/ has to stay ahead of any fallback. + string(REPLACE "${_separator}" ";" _deduplicated "${_paths}") + list(REMOVE_DUPLICATES _deduplicated) + string(REPLACE ";" "${_separator}" _paths "${_deduplicated}") set_target_properties( ${target_name} PROPERTIES BUILD_RPATH "${_paths}" INSTALL_RPATH "${_paths}" ) diff --git a/tools/cmake/executorch-wheel-config.cmake b/tools/cmake/executorch-wheel-config.cmake index fb3b3eb9991..5aa936c91bb 100644 --- a/tools/cmake/executorch-wheel-config.cmake +++ b/tools/cmake/executorch-wheel-config.cmake @@ -43,9 +43,10 @@ # Not the Python extension, which carries unresolved interpreter symbols that # only resolve inside an interpreter, so a standalone application linking it # fails with a page of PyUnicode_InternFromString errors. A project building a -# custom operator against the extension asks for the _portable_lib target by -# name, which is the long-standing contract for that and also carries the C++20 -# requirement PyTorch's headers need. +# custom operator links the runtime, which is where the operator registry lives +# once the runtime is split out of the extension. The extension is not offered +# as a link target: its undefined CPython symbols only resolve inside an +# interpreter. # # EXECUTORCH_BUILD_VERSION -- The full version this package was built from, # including any prerelease suffix and local version label. Compare this when an @@ -384,9 +385,7 @@ if(_executorch_runtime_library AND NOT _executorch_targets_supported) "instead. Linking it is not sufficient on its own: an imported target would also carry the " "include directories, the compile definitions and the C++ standard, so here a consumer has " "to apply EXECUTORCH_INCLUDE_DIRS, EXECUTORCH_COMPILE_DEFINITIONS and " - "EXECUTORCH_CXX_STANDARD itself. The prebuilt Python extension is still " - "defined, with the absolute package path only, so it links in place but is " - "not relocatable." + "EXECUTORCH_CXX_STANDARD itself." ) elseif(_executorch_runtime_library) set(EXECUTORCH_FOUND ON) @@ -650,8 +649,8 @@ _executorch_define_component(backend_openvino executorch_backend_openvino) _executorch_define_component(backend_cuda executorch_backend_cuda) _executorch_define_component(extension_cuda executorch_extension_cuda) -# Find prebuilt _portable_lib..so. This is the legacy contract used -# to build custom-op extensions against the Python module, and is kept working +# Find prebuilt _C..so. This is the legacy contract used to build +# custom-op extensions against the Python module, and is kept working # independently of the runtime target above. # Find python @@ -681,14 +680,14 @@ elseif(_executorch_runtime_library) # Tested on the located library rather than # application on an older CMake is exactly the case this branch exists to keep # working. A C++ application linking only the shared runtime does not need # Python at all, so a missing interpreter must not fail its configure. Skip - # locating the Python extension instead; the legacy _portable_lib target is - # simply not offered in that case. + # locating the Python extension instead; the legacy _C target is simply not + # offered in that case. message( STATUS "Python not usable, skipping the Python extension: ${SYSCONFIG_ERROR}" ) set(EXT_SUFFIX "") - set(_portable_lib_LIBRARY "") + set(_C_LIBRARY "") else() # Reported rather than fatal. The arm above only fires when a runtime library # was located, and a Windows wheel ships none: lib/ holds the CMake package @@ -701,6 +700,7 @@ else() "Python not usable and no runtime library located, so this package offers nothing: ${SYSCONFIG_ERROR}" ) set(EXT_SUFFIX "") + set(_C_LIBRARY "") endif() if(EXT_SUFFIX) @@ -708,131 +708,60 @@ if(EXT_SUFFIX) # package root: the path and the file name are both already known, so a search # only adds the consumer's find-root rules, which reroot an absolute wheel # path into a cross-compile sysroot and report a present extension as missing. - set(_portable_lib_candidate - "${_executorch_package_root}/extension/pybindings/_portable_lib${EXT_SUFFIX}" + set(_C_candidate + "${_executorch_package_root}/extension/pybindings/_C${EXT_SUFFIX}" ) - if(EXISTS "${_portable_lib_candidate}") - set(_portable_lib_LIBRARY "${_portable_lib_candidate}") + if(EXISTS "${_C_candidate}") + set(_C_LIBRARY "${_C_candidate}") else() - set(_portable_lib_LIBRARY "") + set(_C_LIBRARY "") endif() endif() -if(NOT _portable_lib_LIBRARY) +if(NOT _C_LIBRARY) # The interpreter that answered above is whichever python3 is on PATH, which # is not necessarily the one this wheel was built for. A cp310 wheel inspected # by a 3.12 interpreter yields a suffix that names no file here, and the # package then reported itself as not found on a complete install. The shipped # extension carries its own suffix in its name, so take it from the package. - # Restricted to the suffix this platform can load, and chosen once above the - # loop rather than per candidate. Accepting any of the three meant a package - # root from another platform matched: a Windows .pyd inspected from Linux - # defined the library with ELF link options attached to it, which cannot work. + # Loop invariant, so chosen once above the loop rather than per candidate. if(WIN32) - set(_portable_lib_suffixes "pyd") + set(_executorch_extension_suffixes "pyd") elseif(APPLE) - set(_portable_lib_suffixes "so|dylib") + set(_executorch_extension_suffixes "so|dylib") else() - set(_portable_lib_suffixes "so") + set(_executorch_extension_suffixes "so") endif() - file(GLOB _portable_lib_matches - "${_executorch_package_root}/extension/pybindings/_portable_lib.*" - ) - foreach(_candidate IN LISTS _portable_lib_matches) - if(_candidate MATCHES "\\.(${_portable_lib_suffixes})$") - set(_portable_lib_LIBRARY "${_candidate}") + file(GLOB _C_matches "${_executorch_package_root}/extension/pybindings/_C.*") + foreach(_candidate IN LISTS _C_matches) + if(_candidate MATCHES "\\.(${_executorch_extension_suffixes})$") + set(_C_LIBRARY "${_candidate}") break() endif() endforeach() - unset(_portable_lib_matches) - unset(_portable_lib_suffixes) + unset(_C_matches) + unset(_executorch_extension_suffixes) endif() -if(_portable_lib_LIBRARY) - message( - STATUS "ExecuTorch Python extension is found at ${_portable_lib_LIBRARY}" - ) - # Defined so that a caller who specifically wants the extension, such as a - # custom operator project, can name the target, and deliberately kept out of - # EXECUTORCH_LIBRARIES and out of the found decision. The extension carries - # unresolved interpreter symbols, so a plain C++ application that links it - # fails with a page of PyUnicode_InternFromString style errors. Offering it as - # find_package's answer meant a wheel with no linkable library configured - # cleanly and failed at link instead of saying so, and it no longer stands in - # for a fused layout: every platform that ships a runtime now ships it as its - # own file. - # - # The target carries the C++20 requirement PyTorch's headers need, while the - # runtime components require C++17. - if(TARGET _portable_lib) - # This file ran already in the same configure, because another subproject - # called find_package too. No in-tree target uses this name, so it can only - # be the imported one defined below, and re-setting its properties to the - # same values is harmless. - message(STATUS "executorch: _portable_lib is already defined, reusing it") - else() - add_library(_portable_lib STATIC IMPORTED) - endif() - # PyTorch requires C++20, so pybindings must be compiled with C++20. - set_target_properties( - _portable_lib - PROPERTIES - IMPORTED_LOCATION "${_portable_lib_LIBRARY}" - INTERFACE_INCLUDE_DIRECTORIES "${EXECUTORCH_INCLUDE_DIRS}" - # An interface requirement rather than CXX_STANDARD: an imported - # target compiles nothing itself, and CXX_STANDARD does not reach - # consumers, so a custom-op build linking this could still - # compile - # as C++17 and fail against headers that need C++20. - INTERFACE_COMPILE_FEATURES cxx_std_20 - # The runtime's definitions. A custom-op build that links only this target - # compiles against the same headers and needs them too. The thread pool - # definition is appended after this call rather than listed here, because - # whether it applies depends on a variable set further up. - INTERFACE_COMPILE_DEFINITIONS - "C10_USING_CUSTOM_GENERATED_MACROS;@EXECUTORCH_TRACER_DEFINITION@" - ) - # Appended here rather than listed above, because whether the thread pool - # definition applies depends on whether this wheel shipped it. Without it a - # consumer compiles the serial inline copies of parallel_for while the shipped - # libraries carry the real ones, and the serial version wins wherever it was - # inlined, with no diagnostic. It does not arrive transitively because the - # runtime is attached here as a file path, not as the target. - set(_executorch_extension_needs_threadpool OFF) - if("ET_USE_THREADPOOL" IN_LIST EXECUTORCH_COMPILE_DEFINITIONS) - set(_executorch_extension_needs_threadpool ON) - endif() - if(_executorch_extension_needs_threadpool) - set_property( - TARGET _portable_lib - APPEND - PROPERTY INTERFACE_COMPILE_DEFINITIONS ET_USE_THREADPOOL - ) - endif() - # The extension links the runtime rather than containing it, so it no longer - # satisfies the runtime symbols a custom-op library references. Put the - # shipped runtime on this target's interface, which is where the definitions - # moved to, so an out-of-tree operator project keeps building and loading - # against the extension exactly as it did before. Without this a custom - # operator links and then fails to load with an undefined runtime symbol. +if(_C_LIBRARY) + # Reported so a custom operator project can see the path, but deliberately not + # published as a target and kept out of EXECUTORCH_LIBRARIES and out of the + # found decision. The extension carries unresolved interpreter symbols, so a + # plain C++ application that links it fails with a page of + # PyUnicode_InternFromString style errors. Offering it as find_package's + # answer meant a wheel with no linkable library configured cleanly and failed + # at link instead of saying so. # - # The file path rather than executorch::runtime, because that target is only - # defined on CMake 3.28 or newer while this one is defined deliberately on - # every version, so a consumer on an older CMake can still link the extension. - if(_executorch_runtime_library) - set_property( - TARGET _portable_lib - APPEND - PROPERTY INTERFACE_LINK_LIBRARIES "${_executorch_runtime_library}" - ) - # CMake adds a linked library's directory to the consumer's build tree - # runtime search path and strips it on install, so an installed consumer - # library reports libexecutorch.so as not found. Publish the directories - # rather than forcing them onto the target: an interface link option reaches - # every consumer and survives install, which would bake this machine's - # package location into a library the consumer ships onward. A consumer that - # installs elsewhere adds these to its own INSTALL_RPATH. - endif() + # Earlier wheels published this extension as _portable_lib, and that name is + # not carried forward. Aliasing it to the runtime looked compatible and was + # not: the published target was mutable and demanded C++20, and + # $ on it named the extension, so an alias silently dropped to + # C++17, renamed the file it resolves to, and made set_property on it a hard + # error. A project that linked the old name should name executorch::runtime, + # which is what it actually wanted, and a custom operator registers into the + # same registry either way since the registry moved into the runtime when it + # was split out of the extension. + message(STATUS "ExecuTorch Python extension is found at ${_C_LIBRARY}") endif() # find_package checks _FOUND, which is case-sensitive and does not