Skip to content

Commit 8f0ae45

Browse files
committed
Check every dependency of the CUDA wheel, not two names
The CUDA wheel smoke test checked that no shipped library records libtorch_cuda.so or libc10_cuda.so. That misses other libraries that only a CUDA build of torch installs. The wheel before the linking fix also recorded libnvrtc.so.13 on the LLM runner module. With the two names removed, the old check passed while that module still failed to import next to a CPU-only torch. Turn it into an allow list. Every recorded dependency must be shipped in the wheel, be one of the torch libraries a CPU-only torch also has, be part of the C and C++ system runtime, or be installed by a package that a plain install of the wheel requires (today that is the CUDA runtime). A requirement behind an extra, or for another platform, is skipped, so an optional package that happens to be installed does not widen the list. The check compares names only. It does not load anything.
1 parent 703a411 commit 8f0ae45

1 file changed

Lines changed: 68 additions & 14 deletions

File tree

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

Lines changed: 68 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -143,25 +143,79 @@ 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.
146+
# The torch libraries a shipped library may link. A CUDA build of torch adds libtorch_cuda,
147+
# libc10_cuda and more, which a CPU-only torch does not have.
148+
_ALLOWED_TORCH_LIBRARIES = frozenset(
149+
{"libtorch.so", "libtorch_cpu.so", "libtorch_python.so", "libc10.so"}
150+
)
151+
152+
# The system libraries the manylinux policy lets a wheel record, and the loader.
153+
# libatomic is one of them; the aarch64 build links it for sentencepiece.
154+
_SYSTEM_LIBRARIES = frozenset(
155+
{
156+
"libatomic.so.1",
157+
"libc.so.6",
158+
"libm.so.6",
159+
"libdl.so.2",
160+
"libpthread.so.0",
161+
"librt.so.1",
162+
"libstdc++.so.6",
163+
"libgcc_s.so.1",
164+
"ld-linux-x86-64.so.2",
165+
"ld-linux-aarch64.so.1",
166+
}
167+
)
168+
148169

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.
170+
def _declared_requirement_libraries() -> Set[str]:
171+
"""The shared library file names installed by the packages a plain install requires."""
172+
import importlib.metadata as metadata
173+
174+
from packaging.requirements import Requirement
175+
176+
names: Set[str] = set()
177+
for line in metadata.requires("executorch") or []:
178+
requirement = Requirement(line)
179+
# Skip extras and requirements for other platforms, which a plain install does not get.
180+
if requirement.marker and not requirement.marker.evaluate({"extra": ""}):
181+
continue
182+
try:
183+
files = metadata.files(requirement.name) or []
184+
except metadata.PackageNotFoundError:
185+
continue
186+
names.update(Path(file).name for file in files if ".so" in Path(file).name)
187+
return names
188+
189+
190+
def test_recorded_dependencies_are_allowed() -> None:
191+
"""Every library a shipped library records must be on an allow list.
192+
193+
The list is: shipped in the wheel, a torch library a CPU-only torch also has, the system
194+
C and C++ runtime, or installed by a package a plain install requires. A name outside
195+
it, such as libtorch_cuda or libnvrtc, may be missing next to a CPU-only torch, and
196+
then the library that records it fails to load there.
152197
"""
153198
package_dir = _package_dir()
154-
cuda_torch = {"libtorch_cuda.so", "libc10_cuda.so"}
199+
libraries = test_shared_libraries._shipped_shared_objects(package_dir)
200+
allowed = (
201+
{library.name for library in libraries}
202+
| _ALLOWED_TORCH_LIBRARIES
203+
| _SYSTEM_LIBRARIES
204+
| _declared_requirement_libraries()
205+
)
155206
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)
207+
for library in libraries:
208+
unexpected = test_shared_libraries._recorded_dependencies(library) - allowed
209+
if unexpected:
210+
offenders[str(library.relative_to(package_dir))] = sorted(unexpected)
160211
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}"
212+
f"these libraries record dependencies outside the allow list, so they may fail to "
213+
f"load next to a CPU-only torch: {offenders}"
214+
)
215+
print(
216+
f"✓ every dependency recorded by the {len(libraries)} shipped libraries is on the "
217+
f"allow list"
163218
)
164-
print("✓ no shipped library depends on PyTorch's CUDA libraries")
165219

166220

167221
def _row_architectures() -> list[str]:
@@ -607,7 +661,7 @@ def test_a_model_runs_through_the_delegate() -> None:
607661
test_cuda_libraries_are_shipped()
608662
test_cuda_runtime_is_declared()
609663
test_cuda_libraries_resolve_relatively()
610-
test_no_library_needs_cuda_torch()
664+
test_recorded_dependencies_are_allowed()
611665
test_device_code_covers_the_row()
612666
test_portable_device_code_is_present()
613667
test_the_delegate_registers()

0 commit comments

Comments
 (0)