Skip to content

Commit 9bf80eb

Browse files
authored
Link torch_cpu_library, not torch, so the CUDA wheel imports with a CPU-only torch (#24060)
### Problem The Linux CUDA wheel cannot be used with a CPU-only PyTorch. Four of its libraries record a load-time dependency (`DT_NEEDED`) on `libtorch_cuda.so`, one also on `libc10_cuda.so`: - `extension/training/pybindings/_training_lib` - `extension/llm/runner/_llm_runner` - `extension/llm/custom_ops/libcustom_ops_aot_lib.so` - `kernels/quantized/libquantized_ops_aot_lib.so` None of them uses those libraries, yet with a CPU-only torch they fail to load: ``` ImportError: libtorch_cuda.so: cannot open shared object file: No such file or directory ``` It comes from linking the `torch` CMake target (or `${TORCH_LIBRARIES}`). In a CUDA build of PyTorch that target adds `libtorch_cuda.so` wrapped in `-Wl,--no-as-needed`, so it is recorded even when unused, and `--as-needed` on our side cannot drop it. ### Fix Link `torch_cpu_library` instead of `torch` where these libraries are built. `torch` is `torch_cpu_library` plus `torch_cuda_library`, so the CPU half is linked exactly as before, including the `--as-needed` handling that keeps unused libraries such as MKL out of `DT_NEEDED`. With a CPU-only PyTorch the link is unchanged. The CUDA wheel smoke test gets a new check that fails if any shipped library depends on `libtorch_cuda.so` or `libc10_cuda.so`. ### Testing Built the CUDA 13.0 wheel on Linux x86_64 before and after the change. `readelf -d` on `_llm_runner`, before: ``` libexecutorch.so libtorch_python.so libtorch.so libc10.so libc10_cuda.so libtorch_cpu.so libtorch_cuda.so libcudart.so.13 ``` after: ``` libexecutorch.so libtorch_python.so libcudart.so.13 libtorch_cpu.so libc10.so ``` The other three lose `libtorch_cuda.so` the same way. - The new check fails on the old wheel and passes on the new one. - With CUDA torch 2.14.0 on an A100, the CUDA wheel smoke test gives the same results before and after, apart from the new check. That includes running models through the CUDA delegate and building a C++ app against the wheel. Checks needing the full release setup (every GPU architecture, the Qualcomm backend) could not run locally. - The Python tests for the training bindings, the LLM runner bindings, the custom ops and the quantized ops give the same results before and after. - With a PyTorch that links MKL as shared libraries, linking plain `torch_cpu` would pull MKL into `DT_NEEDED` and the operator libraries then fail to load (`undefined symbol: __kmpc_flush`). `torch_cpu_library` keeps MKL out and they load. - In a fresh environment with torch 2.14.0+cpu: before, the training bindings, the LLM runner and both operator libraries fail to load. After, `import executorch` works, all four modules import, and both operator libraries load.
1 parent d1f7f1f commit 9bf80eb

6 files changed

Lines changed: 51 additions & 9 deletions

File tree

‎.ci/scripts/wheel/test_cuda_linux.py‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,27 @@ def test_cuda_libraries_resolve_relatively() -> None:
143143
print(f"✓ {name} resolves the CUDA runtime relatively ({relative[0]})")
144144

145145

146+
def test_no_library_needs_cuda_torch() -> None:
147+
"""No shipped library may record a dependency on PyTorch's own CUDA libraries.
148+
149+
The CUDA delegate needs only the CUDA runtime. A library that names libtorch_cuda or
150+
libc10_cuda cannot load next to a CPU-only torch, so the whole wheel then fails to import
151+
there even though nothing in it calls into those libraries.
152+
"""
153+
package_dir = _package_dir()
154+
cuda_torch = {"libtorch_cuda.so", "libc10_cuda.so"}
155+
offenders = {}
156+
for library in test_shared_libraries._shipped_shared_objects(package_dir):
157+
needed = test_shared_libraries._recorded_dependencies(library) & cuda_torch
158+
if needed:
159+
offenders[str(library.relative_to(package_dir))] = sorted(needed)
160+
assert not offenders, (
161+
f"these libraries depend on PyTorch's CUDA libraries, so the wheel cannot be imported "
162+
f"with a CPU-only torch: {offenders}"
163+
)
164+
print("✓ no shipped library depends on PyTorch's CUDA libraries")
165+
166+
146167
def _row_architectures() -> list[str]:
147168
"""The architectures this row claims, from the same script the build uses.
148169
@@ -586,6 +607,7 @@ def test_a_model_runs_through_the_delegate() -> None:
586607
test_cuda_libraries_are_shipped()
587608
test_cuda_runtime_is_declared()
588609
test_cuda_libraries_resolve_relatively()
610+
test_no_library_needs_cuda_torch()
589611
test_device_code_covers_the_row()
590612
test_portable_device_code_is_present()
591613
test_the_delegate_registers()

‎CMakeLists.txt‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1450,11 +1450,16 @@ if(EXECUTORCH_BUILD_PYBIND)
14501450
aten_bridge PUBLIC C10_USING_CUSTOM_GENERATED_MACROS
14511451
)
14521452
target_compile_options(aten_bridge PUBLIC ${_pybind_compile_options})
1453+
# torch_cpu_library is the CPU half of torch. Linking torch would also force
1454+
# libtorch_cuda into DT_NEEDED in a CUDA build, and a CPU-only torch could
1455+
# then not load this library.
14531456
if(EXECUTORCH_BUILD_SHARED)
1454-
target_link_libraries(aten_bridge PRIVATE torch c10 executorch_shared)
1457+
target_link_libraries(
1458+
aten_bridge PRIVATE torch_cpu_library c10 executorch_shared
1459+
)
14551460
else()
14561461
target_link_libraries(
1457-
aten_bridge PRIVATE torch c10 executorch extension_tensor
1462+
aten_bridge PRIVATE torch_cpu_library c10 executorch extension_tensor
14581463
)
14591464
endif()
14601465
endif()

‎extension/llm/custom_ops/CMakeLists.txt‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,8 +230,11 @@ if(EXECUTORCH_BUILD_KERNELS_LLM_AOT)
230230
endif()
231231
executorch_target_link_shared_runtime(custom_ops_aot_lib)
232232

233+
# torch_cpu_library is the CPU half of torch. Linking torch would also force
234+
# libtorch_cuda into DT_NEEDED in a CUDA build, and a CPU-only torch could
235+
# then not load this library.
233236
target_link_libraries(
234-
custom_ops_aot_lib PUBLIC cpublas torch extension_tensor
237+
custom_ops_aot_lib PUBLIC cpublas torch_cpu_library extension_tensor
235238
extension_threadpool
236239
)
237240
if(EXECUTORCH_BUILD_KERNELS_LLM_QUANTIZED_MOE_OPTIMIZED)

‎extension/llm/runner/CMakeLists.txt‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,10 +120,14 @@ if(EXECUTORCH_BUILD_PYBIND)
120120
find_library(
121121
TORCH_PYTHON_LIBRARY torch_python PATHS "${TORCH_INSTALL_PREFIX}/lib"
122122
)
123-
# Link with the extension_llm_runner library and its dependencies
123+
# Link with the extension_llm_runner library and its dependencies.
124+
#
125+
# torch_cpu_library is the CPU half of torch. Linking torch would also force
126+
# libtorch_cuda into DT_NEEDED in a CUDA build, and a CPU-only torch could
127+
# then not load this library.
124128
target_link_libraries(
125129
_llm_runner PRIVATE extension_llm_runner tokenizers::tokenizers
126-
portable_lib ${TORCH_PYTHON_LIBRARY} ${TORCH_LIBRARIES}
130+
portable_lib ${TORCH_PYTHON_LIBRARY} torch_cpu_library
127131
)
128132
executorch_target_link_shared_runtime(_llm_runner)
129133

‎extension/training/CMakeLists.txt‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,18 @@ if(EXECUTORCH_BUILD_PYBIND)
5252
# Pybind library. When the consolidated shared runtime is built, the runtime
5353
# is resolved from it rather than from the whole-archive-forcing static
5454
# `executorch` target, so this module shares the one backend registry.
55+
#
56+
# torch_cpu_library is the CPU half of torch. Linking torch would also force
57+
# libtorch_cuda into DT_NEEDED in a CUDA build, and a CPU-only torch could
58+
# then not load this library.
5559
if(EXECUTORCH_BUILD_SHARED)
5660
set(_pybind_training_dep_libs ${TORCH_PYTHON_LIBRARY} etdump aten_bridge
57-
torch extension_training
61+
torch_cpu_library extension_training
5862
)
5963
else()
60-
set(_pybind_training_dep_libs ${TORCH_PYTHON_LIBRARY} etdump executorch
61-
aten_bridge torch extension_training
64+
set(_pybind_training_dep_libs
65+
${TORCH_PYTHON_LIBRARY} etdump executorch aten_bridge torch_cpu_library
66+
extension_training
6267
)
6368
endif()
6469

‎tools/cmake/Codegen.cmake‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,10 @@ function(gen_custom_ops_aot_lib)
334334
set_target_properties(${GEN_LIB_NAME} PROPERTIES CXX_STANDARD 20)
335335
target_compile_definitions(${GEN_LIB_NAME} PRIVATE USE_ATEN_LIB=1)
336336
include_directories(${TORCH_INCLUDE_DIRS})
337-
target_link_libraries(${GEN_LIB_NAME} PRIVATE torch)
337+
# torch_cpu_library is the CPU half of torch. Linking torch would also force
338+
# libtorch_cuda into DT_NEEDED in a CUDA build, and a CPU-only torch could
339+
# then not load this library.
340+
target_link_libraries(${GEN_LIB_NAME} PRIVATE torch_cpu_library)
338341

339342
executorch_target_link_options_shared_lib(${GEN_LIB_NAME})
340343
if(TARGET portable_lib)

0 commit comments

Comments
 (0)