Skip to content

Commit b27be2f

Browse files
committed
fix(docker): share CUDA compatibility libraries with exec processes
Put a private per-container compatibility directory on the image's library search path. The non-root bootstrap atomically selects the opt-in CUDA libraries there, so the entrypoint, docker exec, and restarted processes use the same loader configuration. Leave the directory inactive by default and report missing compatibility libraries before starting the requested command. Document recreation after changing the setting. The real regression checks enabled and disabled modes, arbitrary UID/GID, exec, restart, and missing libraries; it fails against the previous image at the exec comparison. Validation: the rebuilt CUDA image passes make docker-check and make docker-integration-check (1972 CPU tests and 11 integration tests), including the separate digest bundle fix and offline Compose creation.
1 parent 0139ae5 commit b27be2f

5 files changed

Lines changed: 107 additions & 12 deletions

File tree

‎assets/openwam_usage_docs/docker.md‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -645,7 +645,10 @@ older 535/550 data-center drivers need validation with the image's CUDA 12.8
645645
libraries and `torch.compile`; successful `nvidia-smi` alone is not sufficient.
646646
NVIDIA Container Toolkit normally handles CUDA forward compatibility. If it
647647
does not, test `OPENWAM_CUDA_COMPAT=1` to select `/usr/local/cuda/compat`, then run
648-
the GPU check in this section. This does not install or replace the host kernel driver.
648+
the GPU check in this section. The normal image entrypoint initializes this
649+
selection for both the main process and later `docker exec` commands. Recreate
650+
containers after changing the setting. The default `0` leaves library selection
651+
to the Toolkit. This does not install or replace the host kernel driver.
649652
See [NVIDIA's compatibility guide](https://docs.nvidia.com/deploy/cuda-compatibility/forward-compatibility.html).
650653

651654
</details>

‎docker/Dockerfile‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ ENV XDG_CACHE_HOME=/cache \
6565
LD_PRELOAD=libnss_wrapper.so \
6666
NSS_WRAPPER_PASSWD=/tmp/openwam-nss/passwd \
6767
NSS_WRAPPER_GROUP=/tmp/openwam-nss/group \
68+
LD_LIBRARY_PATH="/tmp/openwam-runtime/cuda-compat:${LD_LIBRARY_PATH}" \
6869
RUFF_CACHE_DIR=/cache/ruff \
6970
CUDA_CACHE_PATH=/cache/cuda \
7071
HF_HOME=/cache/huggingface \

‎docker/entrypoint.sh‎

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,6 @@ for cache_dir in "${XDG_CACHE_HOME:-/cache}" "${RUFF_CACHE_DIR:-/cache/ruff}" \
1212
fi
1313
done
1414

15-
# Usually handled by NVIDIA Container Toolkit. Opt in only when validating an
16-
# older data-center driver that needs the image's forward-compatibility libraries.
17-
if [[ "${OPENWAM_CUDA_COMPAT:-0}" == 1 ]]; then
18-
compat_dir="${CUDA_HOME:-/usr/local/cuda}/compat"
19-
if [[ ! -e "$compat_dir/libcuda.so.1" ]]; then
20-
echo "OpenWAM: CUDA compatibility libraries missing from $compat_dir." >&2
21-
exit 1
22-
fi
23-
export LD_LIBRARY_PATH="$compat_dir${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
24-
fi
25-
2615
case "${1:-serve}" in
2716
serve)
2817
if (( $# )); then shift; fi

‎docker/runtime_user.py‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,27 @@
66
from pathlib import Path
77

88

9+
def configure_cuda_compat():
10+
# This directory is on the image's LD_LIBRARY_PATH for every process,
11+
# including docker exec. Only the opt-in entrypoint creates the link.
12+
link = Path("/tmp/openwam-runtime/cuda-compat")
13+
try:
14+
if os.environ.get("OPENWAM_CUDA_COMPAT", "0") != "1":
15+
link.unlink(missing_ok=True)
16+
return
17+
compat = (Path(os.environ.get("CUDA_HOME", "/usr/local/cuda")) / "compat").resolve()
18+
if not (compat / "libcuda.so.1").is_file():
19+
raise SystemExit(f"OpenWAM: CUDA compatibility libraries missing from {compat}.")
20+
link.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
21+
# Atomic replacement also works when Docker restarts this container.
22+
with tempfile.TemporaryDirectory(prefix=".cuda-compat-", dir=link.parent) as temporary:
23+
staged = Path(temporary) / "compat"
24+
staged.symlink_to(compat, target_is_directory=True)
25+
staged.replace(link)
26+
except OSError as error:
27+
raise SystemExit(f"OpenWAM: cannot configure CUDA compatibility: {error}") from error
28+
29+
930
def main():
1031
home = Path(os.environ["HOME"])
1132
try:
@@ -34,6 +55,8 @@ def main():
3455
temporary.write_text("\n".join([*lines, entry]) + "\n")
3556
temporary.replace(path)
3657

58+
configure_cuda_compat()
59+
3760
# Start bash only after the account files exist: bash itself looks up the
3861
# current UID at startup. exec preserves signal handling and the exit code.
3962
os.execv("/bin/bash", ["bash", "/opt/openwam/docker/entrypoint.sh", *sys.argv[1:]])

‎tests/test_docker_integration.py‎

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -662,6 +662,85 @@ def test_runtime_identity_and_home_survive_run_and_exec(self):
662662
]
663663
)
664664

665+
def test_cuda_compat_library_selection_survives_exec_and_restart(self):
666+
probe = (
667+
"import ctypes, json, os\n"
668+
"from pathlib import Path\n"
669+
"result = {'path': os.environ.get('LD_LIBRARY_PATH'), 'compat': os.environ.get('OPENWAM_CUDA_COMPAT')}\n"
670+
"result['compat_directory'] = str((Path(os.environ['CUDA_HOME']) / 'compat').resolve())\n"
671+
"try:\n"
672+
" library = ctypes.CDLL('libcuda.so.1')\n"
673+
" result['libraries'] = sorted({line.split()[-1] for line in Path('/proc/self/maps').read_text().splitlines() "
674+
"if '/libcuda.so.' in line})\n"
675+
"except OSError as error:\n"
676+
" result['error'] = str(error)\n"
677+
"print(json.dumps(result), flush=True)\n"
678+
)
679+
for enabled in ("0", "1"):
680+
with self.subTest(enabled=enabled):
681+
options = ["--network", "none", "--user", "23456:23457", "-e", "OPENWAM_CUDA_COMPAT=" + enabled]
682+
initial = json.loads(self.run_command([*DOCKER, "run", "--rm", *options, IMAGE, "python", "-c", probe]))
683+
if enabled == "1":
684+
self.assertNotIn("error", initial)
685+
self.assertTrue(initial["libraries"])
686+
self.assertTrue(
687+
all(path.startswith(initial["compat_directory"] + "/") for path in initial["libraries"])
688+
)
689+
name = "openwam-compat-" + uuid.uuid4().hex[:12]
690+
self.run_command(
691+
[
692+
*DOCKER,
693+
"run",
694+
"-d",
695+
"--name",
696+
name,
697+
*options,
698+
IMAGE,
699+
"python",
700+
"-c",
701+
probe + "import time; time.sleep(300)\n",
702+
]
703+
)
704+
self.addCleanup(self.run_command, [*DOCKER, "rm", "-f", name])
705+
706+
def wait_for_start(count):
707+
for _ in range(100):
708+
lines = self.run_command([*DOCKER, "logs", name]).splitlines()
709+
if len(lines) >= count:
710+
self.assertEqual(json.loads(lines[-1]), initial)
711+
return
712+
time.sleep(0.1)
713+
self.fail("compatibility probe did not start")
714+
715+
wait_for_start(1)
716+
self.assertEqual(json.loads(self.run_command([*DOCKER, "exec", name, "python", "-c", probe])), initial)
717+
self.run_command([*DOCKER, "restart", "--time", "1", name])
718+
wait_for_start(2)
719+
self.assertEqual(json.loads(self.run_command([*DOCKER, "exec", name, "python", "-c", probe])), initial)
720+
721+
missing = subprocess.run(
722+
[
723+
*DOCKER,
724+
"run",
725+
"--rm",
726+
"--network",
727+
"none",
728+
"-e",
729+
"OPENWAM_CUDA_COMPAT=1",
730+
"-e",
731+
"CUDA_HOME=/missing-cuda",
732+
IMAGE,
733+
"python",
734+
"-c",
735+
"print('must not start')",
736+
],
737+
capture_output=True,
738+
text=True,
739+
)
740+
self.assertNotEqual(missing.returncode, 0)
741+
self.assertIn("CUDA compatibility libraries missing", missing.stderr)
742+
self.assertNotIn("must not start", missing.stdout)
743+
665744
def test_bootstrap_pip_fallback_installs_the_locked_version(self):
666745
# Local PEP 503 indexes and real wheels make this deterministic/offline:
667746
# the primary lacks the package; the fallback has both 1.0 and 2.0.

0 commit comments

Comments
 (0)