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
25 changes: 20 additions & 5 deletions .ci/scripts/wheel/test_cpp_sdk.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@
"executorch::runtime::get_backend_class",
)

# The thread pool accessor. A second definer means a second pool, which
# oversubscribes the CPU because each pool sizes itself to all cores.
_THREADPOOL_SYMBOLS = ("executorch::extension::threadpool::get_threadpool",)

# `nm -DC` prints "<hexaddr> <kind> <name>" for a definition and
# " U <name>" for an undefined reference.
_DEFINED = re.compile(r"^[0-9a-fA-F]+\s+(?P<kind>[A-Za-z])\s+(?P<name>.+)$")
Expand Down Expand Up @@ -106,23 +110,33 @@ def _defines_symbol(library: Path, symbol: str) -> bool:
return False


def test_single_backend_registry() -> None:
"""Exactly one shipped library may define the backend registry."""
def _assert_single_definer(symbols, what: str) -> None:
"""Exactly one shipped library may define each of `symbols`."""
assert shutil.which("nm") is not None, "nm is required to inspect the wheel"

package_dir = _installed_package_dir()
libraries = _shipped_shared_objects(package_dir)
assert libraries, f"no shared libraries found under {package_dir}"

for symbol in _REGISTRY_SYMBOLS:
for symbol in symbols:
definers = [lib for lib in libraries if _defines_symbol(lib, symbol)]
pretty = [str(lib.relative_to(package_dir)) for lib in definers]
assert len(definers) == 1, (
f"expected exactly one library to define {symbol}, found "
f"{len(definers)}: {pretty}. More than one definition means the "
f"process has more than one backend registry."
f"process has more than one {what}."
)
print(f"✓ single backend registry across {len(libraries)} shipped libraries")
print(f"✓ single {what} across {len(libraries)} shipped libraries")


def test_single_backend_registry() -> None:
"""Exactly one shipped library may define the backend registry."""
_assert_single_definer(_REGISTRY_SYMBOLS, "backend registry")


def test_single_threadpool() -> None:
"""Exactly one shipped library may define the thread pool accessor."""
_assert_single_definer(_THREADPOOL_SYMBOLS, "thread pool")


def test_cpp_consumer(work_dir: Path) -> None:
Expand Down Expand Up @@ -224,4 +238,5 @@ def _assert_runs_relocated(consumer, package_dir, work_dir, environment) -> None

def run_tests(work_dir: Path) -> None:
test_single_backend_registry()
test_single_threadpool()
test_cpp_consumer(work_dir)
43 changes: 38 additions & 5 deletions extension/threadpool/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,46 @@ else()
set(_threadpool_size_flag "EXECUTORCH_THREADPOOL_USE_PERFORMANCE_CORES")
endif()

# The thread pool is a process-wide singleton held in a function-local static,
# so every library that links it statically gets its own copy. Build it shared
# for the wheel, where several extensions are loaded into one interpreter, and
# keep it static everywhere else so no other build changes.
if(EXECUTORCH_BUILD_SHARED)
set(_threadpool_library_type SHARED)
else()
set(_threadpool_library_type STATIC)
endif()

add_library(
extension_threadpool threadpool.cpp threadpool_guard.cpp thread_parallel.cpp
cpuinfo_utils.cpp
)
target_link_libraries(
extension_threadpool PUBLIC executorch_core cpuinfo pthreadpool
extension_threadpool
${_threadpool_library_type} threadpool.cpp threadpool_guard.cpp
thread_parallel.cpp cpuinfo_utils.cpp
)
if(EXECUTORCH_BUILD_SHARED)
set_target_properties(
extension_threadpool
PROPERTIES OUTPUT_NAME executorch_threadpool
VERSION "${PROJECT_VERSION}"
SOVERSION "${PROJECT_VERSION_MAJOR}"
)
if(NOT APPLE)
# Ships beside libexecutorch.so in the wheel's lib/ directory.
set_target_properties(
extension_threadpool PROPERTIES BUILD_RPATH "$ORIGIN" INSTALL_RPATH
"$ORIGIN"
)
Comment thread
shoumikhin marked this conversation as resolved.
endif()
# cpuinfo and pthreadpool are forced static, so bundle them inside this
# library instead of making every consumer supply them.
executorch_target_whole_archive(extension_threadpool cpuinfo)
executorch_target_whole_archive(extension_threadpool pthreadpool)
target_link_libraries(extension_threadpool PRIVATE cpuinfo pthreadpool)
target_link_libraries(extension_threadpool PUBLIC executorch_shared)
Comment thread
shoumikhin marked this conversation as resolved.
else()
target_link_libraries(
extension_threadpool PUBLIC executorch_core cpuinfo pthreadpool
)
endif()
target_include_directories(
extension_threadpool PUBLIC ${_common_include_directories}
)
Expand Down
22 changes: 22 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -1114,6 +1114,28 @@ def run(self): # noqa C901
dst=f"executorch/lib/libexecutorch.so.{get_runtime_soname_major()}",
dependent_cmake_flags=["EXECUTORCH_BUILD_SHARED"],
),
# Install the shared thread pool next to it. It is a separate
# library so that a process has one pool rather than one per
# component that uses it.
BuiltFile(
src_dir="%CMAKE_CACHE_DIR%/extension/threadpool/",
src_name=(
"libexecutorch_threadpool.so." f"{get_runtime_soname_major()}.*"
),
dst=(
"executorch/lib/libexecutorch_threadpool.so."
f"{get_runtime_soname_major()}"
),
# The target only exists when both of its dependencies are
# enabled, so packaging has to require them too or a shared
# build with either turned off looks for a file that was
# never built.
dependent_cmake_flags=[
"EXECUTORCH_BUILD_SHARED",
"EXECUTORCH_BUILD_PTHREADPOOL",
"EXECUTORCH_BUILD_CPUINFO",
],
),
# Install the prebuilt pybindings extension wrapper for the runtime,
# portable kernels, and a selection of backends. This lets users
# load and execute .pte files from python.
Expand Down
Loading