Skip to content

Commit 1fc3bec

Browse files
rocm: don't bundle librocjpeg; RPATH libtorchcodec_image.so instead
Bundling librocjpeg moves it out of _rocm_sdk_core/lib, breaking AMD's own relative RPATH inside it that points to librocm_sysdeps_* transitive deps. We then have to re-patch those paths ourselves, which is fragile and was causing the missing librocm_sysdeps_va.so.2 / libamdhip64.so.7 failures at runtime. The cleaner approach: exclude librocjpeg from auditwheel bundling and instead append _rocm_sdk_core/lib (ROCm >= 7.14) and /opt/rocm/lib (ROCm <= 7.2) to the RPATH of libtorchcodec_image.so itself. At runtime the dynamic linker finds librocjpeg in its original location; AMD's own RPATH on that library then correctly resolves all transitive deps without any intervention from us. _find_rocjpeg_lib() is kept: auditwheel still needs librocjpeg in LD_LIBRARY_PATH to resolve the DT_NEEDED during its analysis pass, even though we exclude it from bundling. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent efaac78 commit 1fc3bec

2 files changed

Lines changed: 76 additions & 61 deletions

File tree

.github/workflows/linux_rocm.yaml

Lines changed: 14 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -139,10 +139,10 @@ jobs:
139139
bash packaging/remove_src.sh
140140
141141
echo '::group::Install rocJPEG runtime'
142-
# librocjpeg is bundled into the wheel by repair_wheel.py (ROCm 7.14
143-
# pip-wheel build) or must be present on the system (ROCm <= 7.2 system
144-
# install). install_rocjpeg.sh ensures the runtime side-deps (libva)
145-
# are present regardless of how librocjpeg itself was obtained.
142+
# librocjpeg is NOT bundled into the wheel. Instead, libtorchcodec_image.so
143+
# has an RPATH entry pointing to _rocm_sdk_core/lib (ROCm 7.14 pip-wheel) and
144+
# /opt/rocm/lib (ROCm <= 7.2 system install). install_rocjpeg.sh ensures the
145+
# runtime side-deps (libva) are present regardless of layout.
146146
bash packaging/install_rocjpeg.sh
147147
echo '::endgroup::'
148148
@@ -162,36 +162,24 @@ jobs:
162162
echo '::endgroup::'
163163
164164
echo '::group::Run FFmpeg-free image decoder tests (incl. rocJPEG GPU)'
165-
# ROCm 7.14 pip-wheel layout: the ROCm runtime and its vendored system
166-
# deps (librocm_sysdeps_va.so.2 etc.) live inside _rocm_sdk_core/lib/
167-
# and _rocm_sdk_core/lib/rocm_sysdeps/lib/. The RPATH on the bundled
168-
# librocjpeg-HASH.so points there via $ORIGIN, but the dynamic linker
169-
# may not expand $ORIGIN correctly without execution permission. Set
170-
# LD_LIBRARY_PATH explicitly as belt-and-suspenders.
171-
rocm_core_lib=$(python -c "
172-
import importlib.util, pathlib, os
173-
spec = importlib.util.find_spec('_rocm_sdk_core')
174-
if spec:
175-
p = pathlib.Path(spec.submodule_search_locations[0])
176-
dirs = [str(p/'lib'), str(p/'lib'/'rocm_sysdeps'/'lib')]
177-
print(os.pathsep.join(d for d in dirs if pathlib.Path(d).is_dir()))
178-
" 2>/dev/null || true)
165+
# ROCm 7.14 pip-wheel layout: librocjpeg lives in _rocm_sdk_core/lib and
166+
# its transitive deps (librocm_sysdeps_va.so.2 etc.) are in
167+
# _rocm_sdk_core/lib/rocm_sysdeps/lib. The RPATH on libtorchcodec_image.so
168+
# points to _rocm_sdk_core/lib, and AMD's own RPATH on librocjpeg handles
169+
# the sysdeps. Set LD_LIBRARY_PATH as belt-and-suspenders for the HIP
170+
# runtime (libamdhip64) that librocjpeg needs.
171+
rocm_core_lib=$(python -c "import importlib.util, pathlib, os; spec = importlib.util.find_spec('_rocm_sdk_core'); p = pathlib.Path(spec.submodule_search_locations[0]) if spec else None; dirs = [str(p/'lib'), str(p/'lib'/'rocm_sysdeps'/'lib')] if p else []; print(os.pathsep.join(d for d in dirs if pathlib.Path(d).is_dir()))" 2>/dev/null || true)
179172
if [ -n "${rocm_core_lib}" ]; then
180173
export LD_LIBRARY_PATH="${rocm_core_lib}:${LD_LIBRARY_PATH:-}"
181174
echo "LD_LIBRARY_PATH (rocm_sdk_core): ${rocm_core_lib}"
182175
fi
183-
# Diagnostics: dump ldd and RPATH so any remaining missing dep is visible.
176+
# Diagnostics: dump ldd on the image .so so any missing dep is visible.
184177
image_so=$(python -c "import importlib.util, pathlib; spec = importlib.util.find_spec('torchcodec'); print(pathlib.Path(spec.origin).parent / 'libtorchcodec_image.so')" 2>/dev/null || true)
185178
if [ -n "${image_so}" ] && [ -f "${image_so}" ]; then
179+
echo "RPATH of ${image_so}:"
180+
objdump -p "${image_so}" 2>/dev/null | grep -E "RPATH|RUNPATH" || true
186181
echo "ldd ${image_so}:"
187182
ldd "${image_so}" || true
188-
rocjpeg_so=$(find "$(dirname "${image_so}")/../torchcodec.libs" -name 'librocjpeg*.so*' 2>/dev/null | head -1 || true)
189-
if [ -n "${rocjpeg_so}" ]; then
190-
echo "ldd ${rocjpeg_so}:"
191-
ldd "${rocjpeg_so}" || true
192-
echo "RPATH of ${rocjpeg_so}:"
193-
objdump -p "${rocjpeg_so}" 2>/dev/null | grep -E "RPATH|RUNPATH" || true
194-
fi
195183
fi
196184
# torch.cuda.is_available() is True on ROCm (HIP masquerades as CUDA), so
197185
# the needs_cuda image tests run here and exercise the GPU JPEG decoder,

packaging/repair_wheel.py

Lines changed: 62 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,12 @@ def _find_rocjpeg_license():
163163

164164

165165
def _find_rocjpeg_lib():
166-
"""Find librocjpeg.so at wheel repair time so auditwheel can bundle it.
166+
"""Find librocjpeg.so at wheel repair time so auditwheel can resolve it.
167+
168+
auditwheel needs librocjpeg to be resolvable in LD_LIBRARY_PATH during
169+
`auditwheel repair` even though we exclude it from bundling (so it stays
170+
in the user's ROCm install). This function returns the directory to add to
171+
LD_LIBRARY_PATH before calling auditwheel.
167172
168173
Searches ROCM_HOME / ROCM_PATH env vars, torch's ROCM_HOME, the standard
169174
/opt/rocm fallback, and (for ROCm >= 7.14) the _rocm_sdk_* pip-wheel
@@ -230,16 +235,21 @@ def _find_rocjpeg_lib():
230235
return None
231236

232237

233-
def _patch_rocjpeg_rpath_in_wheel(wheel_path: Path) -> None:
234-
"""Set RPATH on the bundled librocjpeg-*.so.* inside a repaired wheel so
235-
it can find its ROCm runtime deps at the user's install time without
236-
LD_LIBRARY_PATH.
238+
def _patch_image_so_rpath_in_wheel(wheel_path: Path) -> None:
239+
"""Append ROCm library search paths to libtorchcodec_image.so's RPATH.
240+
241+
librocjpeg is NOT bundled in the wheel (it is excluded from auditwheel so
242+
it stays in the user's ROCm install). At runtime the dynamic linker must
243+
find librocjpeg via RPATH on libtorchcodec_image.so itself.
237244
238245
Two layouts are covered:
239246
- ROCm >= 7.14 (TheRock / rocm-sdk-* Python wheels):
240-
libamdhip64 etc. live in <site-packages>/_rocm_sdk_core/lib/.
247+
librocjpeg lives in <site-packages>/_rocm_sdk_core/lib/.
241248
$ORIGIN/../../_rocm_sdk_core/lib reaches that dir from
242-
<site-packages>/torchcodec.libs/librocjpeg-HASH.so.1.
249+
<site-packages>/torchcodec/libtorchcodec_image.so.
250+
AMD already set the correct RPATH inside _rocm_sdk_core's librocjpeg
251+
to find librocm_sysdeps_* transitive deps, so we do not need to touch
252+
those at all.
243253
- ROCm <= 7.2 (system install):
244254
/opt/rocm/lib is the standard path; the AMD installer always
245255
creates the /opt/rocm symlink even for versioned installs.
@@ -251,39 +261,44 @@ def _patch_rocjpeg_rpath_in_wheel(wheel_path: Path) -> None:
251261
"repairing ROCm wheels."
252262
)
253263

254-
rpath = ":".join([
255-
"$ORIGIN",
256-
# ROCm >= 7.14 layout A: torch bundles ROCm runtime inside torch/lib/
257-
# (same layout PyTorch uses for CUDA runtime on CUDA builds).
258-
"$ORIGIN/../../torch/lib",
259-
# ROCm >= 7.14 layout B: rocm-sdk-core ships as a separate pip wheel
260-
# and ROCm runtime lives in _rocm_sdk_core/lib.
261-
"$ORIGIN/../../_rocm_sdk_core/lib",
262-
"$ORIGIN/../../_rocm_sdk_core/lib/rocm_sysdeps/lib",
263-
# ROCm <= 7.2: standard system install path (AMD installer always
264-
# creates the /opt/rocm symlink even for versioned installs like 7.2.0).
265-
"/opt/rocm/lib",
266-
"/opt/rocm/lib/rocm_sysdeps/lib",
267-
])
268-
269264
import hashlib, base64, tempfile
270265
with tempfile.TemporaryDirectory() as tmp:
271266
tmp_path = Path(tmp)
272267
with zipfile.ZipFile(wheel_path, "r") as zf:
273268
zf.extractall(tmp_path)
274269

275-
rocjpeg_libs = list(tmp_path.rglob("librocjpeg*.so*"))
276-
if not rocjpeg_libs:
277-
print(f"No librocjpeg found in {wheel_path.name}; skipping RPATH patch.", flush=True)
270+
image_libs = list(tmp_path.rglob("libtorchcodec_image*.so*"))
271+
if not image_libs:
272+
print(
273+
f"No libtorchcodec_image found in {wheel_path.name}; "
274+
"skipping ROCm RPATH patch.",
275+
flush=True,
276+
)
278277
return
279-
for lib in rocjpeg_libs:
280-
print(f"Patching RPATH on {lib.name}: {rpath}", flush=True)
281-
subprocess.run([patchelf, "--set-rpath", rpath, str(lib)], check=True)
278+
279+
for lib in image_libs:
280+
# Read the RPATH auditwheel already set (e.g. $ORIGIN/../torchcodec.libs)
281+
# and append the ROCm search dirs without clobbering them.
282+
result = subprocess.run(
283+
[patchelf, "--print-rpath", str(lib)],
284+
capture_output=True, text=True, check=True,
285+
)
286+
existing = result.stdout.strip()
287+
extra = ":".join([
288+
# ROCm >= 7.14: librocjpeg lives in _rocm_sdk_core/lib alongside
289+
# the other ROCm libraries shipped as a pip wheel.
290+
"$ORIGIN/../../_rocm_sdk_core/lib",
291+
# ROCm <= 7.2: standard system install (always symlinked to /opt/rocm).
292+
"/opt/rocm/lib",
293+
])
294+
new_rpath = f"{existing}:{extra}" if existing else extra
295+
print(f"Setting RPATH on {lib.name}: {new_rpath}", flush=True)
296+
subprocess.run([patchelf, "--set-rpath", new_rpath, str(lib)], check=True)
282297

283298
# Update the RECORD file so pip's integrity check passes.
284299
# RECORD format: path,sha256=<base64url>,size (or ",," for RECORD itself)
285300
record_files = list(tmp_path.rglob("RECORD"))
286-
patched_rel_names = {lib.relative_to(tmp_path).as_posix() for lib in rocjpeg_libs}
301+
patched_rel_names = {lib.relative_to(tmp_path).as_posix() for lib in image_libs}
287302
for record_file in record_files:
288303
lines = record_file.read_text(encoding="utf-8").splitlines()
289304
new_lines = []
@@ -333,7 +348,10 @@ def repair_linux(wheels):
333348
print(f"Found librocjpeg in {rocjpeg_lib_dir}", flush=True)
334349
else:
335350
print(
336-
"WARNING: librocjpeg not found; rocJPEG will not be bundled. "
351+
"WARNING: librocjpeg not found; auditwheel cannot resolve the "
352+
"DT_NEEDED entry for librocjpeg. The wheel will still be built "
353+
"but ROCm JPEG decoding will fail at runtime unless librocjpeg "
354+
"is reachable via /opt/rocm/lib or _rocm_sdk_core/lib. "
337355
"Set ROCM_HOME or ROCM_PATH if ROCm is in a non-standard location.",
338356
flush=True,
339357
)
@@ -366,11 +384,18 @@ def repair_linux(wheels):
366384
"libnvshmem*",
367385
"libnvfatbin*",
368386
"libnvcuvid*",
387+
# librocjpeg is NOT bundled. Instead, libtorchcodec_image.so gets an RPATH
388+
# entry pointing to _rocm_sdk_core/lib (ROCm >= 7.14) and /opt/rocm/lib
389+
# (ROCm <= 7.2), so the linker finds librocjpeg in its original location.
390+
# This is intentional: AMD already set correct RPATHs inside their
391+
# librocjpeg to find librocm_sysdeps_* and other transitive deps relative
392+
# to _rocm_sdk_core/lib. Moving it (bundling) breaks those relative paths
393+
# and requires us to re-patch them, which is fragile.
394+
"librocjpeg*",
369395
# ROCm/HIP runtime and its system deps: provided by the torch-ROCm wheel
370396
# or the system ROCm install at runtime. Never bundle them — they would
371397
# duplicate torch's copies and bloat the wheel significantly (libLLVM
372398
# alone is ~200 MB).
373-
# librocjpeg itself IS bundled (not listed here); only its deps are excluded.
374399
"libamdhip64*",
375400
"libamd_comgr*",
376401
"libhsa-runtime64*",
@@ -412,12 +437,14 @@ def repair_linux(wheels):
412437
env=env,
413438
)
414439

415-
# After auditwheel bundles librocjpeg-HASH.so.*, patch its RPATH so the
416-
# bundled copy can find its ROCm runtime deps (libamdhip64 etc.) at the
417-
# user's install time without requiring LD_LIBRARY_PATH.
440+
# After auditwheel repair, patch libtorchcodec_image.so's RPATH to include
441+
# _rocm_sdk_core/lib (ROCm >= 7.14) and /opt/rocm/lib (ROCm <= 7.2) so the
442+
# dynamic linker can find librocjpeg at runtime without LD_LIBRARY_PATH.
443+
# librocjpeg itself is NOT bundled; it stays in the ROCm install so AMD's
444+
# own RPATH inside it correctly resolves all transitive deps.
418445
if any(_is_rocm_wheel(w) for w in wheels):
419446
for repaired_whl in REPAIRED_DIR.glob("*.whl"):
420-
_patch_rocjpeg_rpath_in_wheel(repaired_whl)
447+
_patch_image_so_rpath_in_wheel(repaired_whl)
421448

422449

423450
def repair_macos(wheels):

0 commit comments

Comments
 (0)