Files
computer-playbook/tests/unit/python/roles/sys-svc-container/test_gpu_runtime.py
T
kevinveenbirkenbachandClaude Opus 5 472017da44 fix(sys-svc-container): name the GPU devices the runtime hook reads
runtime: nvidia on its own is a no-op. The NVIDIA container runtime hook reads NVIDIA_VISIBLE_DEVICES off the environment of the container and injects nothing when it is unset, and that variable appeared nowhere in the repository. The engine images build on python slim with CUDA wheels rather than on an nvidia/cuda base, so they carry no such default in their image environment either; a role built on an nvidia/cuda base would have worked by accident. The deployed nllb-engine had been crash-looping on "Found no NVIDIA driver on your system" for 229 restarts.

Measured against the same image on the same nested daemon: --runtime=nvidia alone reports no devices, while adding NVIDIA_VISIBLE_DEVICES and NVIDIA_DRIVER_CAPABILITIES yields the device nodes, a working nvidia-smi and torch reporting 2.9.1+cu128 True 1. After the deploy the live container answers True 1 NVIDIA GeForce RTX 3080 Ti Laptop GPU, with the restart count back to 0 and health reported healthy.

Devices are named explicitly rather than with all. 00_core/gpu.yml asks the driver for memory.total and uuid, sorts by memory descending and publishes the uuid list, so a framework that grabs device zero gets the largest card. With a single card it degenerates to that card. NVIDIA_DRIVER_CAPABILITIES lives in group_vars rather than in the role vars because role vars are out of scope while a different role renders the template, which an earlier attempt proved by failing on an undefined name while rendering svc-ai-nllb-engine; SYS_SVC_CONTAINER_GPU_DEVICES crosses that boundary because it is a host-scoped set_fact.

meta/packages.yml already declared the toolkit for Archlinux, Debian and RedHat, the latter two carrying a note that sys-svc-container configures the nvidia.github.io repository before installing, yet only the apt path was ever built and the include was gated to Debian and Ubuntu. The declaration and the tasks contradicted each other. nvidia.yml now dispatches per family the way the role already does for Docker: an apt source for Debian, a repo file for the RedHat family, and nothing for Arch, where the package sits in extra. The install itself goes through package_install, and the runtime registration in daemon_config.yml was already presence-based rather than distro-gated, so it needed no change.

The discovery tasks live in their own file because a when-guarded block is capped at three direct children, and splitting the guard across two siblings would have tripped the repeated-when rule instead. Nothing renders on a host without a card: the gpu lookup and the set_fact hang on the same register, so the template emits the block exactly when the fact exists.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-21 15:34:46 +02:00

92 lines
2.8 KiB
Python

"""The GPU keys of the shared resource template.
The template asks the ``gpu`` lookup and decides nothing itself: combining
the declared flag with the host's device is that lookup's job, and its own
tests own the matrix.
``runtime: nvidia`` alone hands the container nothing. The runtime's hook
reads ``NVIDIA_VISIBLE_DEVICES`` off the container's environment and is a
no-op while that is unset, so the two keys below belong to the same branch.
"""
import unittest
from pathlib import Path
from jinja2 import (
Environment,
FileSystemLoader,
StrictUndefined,
select_autoescape,
)
from utils import PROJECT_ROOT
TEMPLATES = Path(PROJECT_ROOT) / "roles" / "sys-svc-container" / "templates"
BASE = {
"application_id": "web-svc-libretranslate",
"service_name": "libretranslate",
"RESOURCE_CPUS": "1",
"RESOURCE_HOST_CPUS": "8",
"RESOURCE_MEM_LIMIT": "1g",
"RESOURCE_MEM_RESERVATION": "256m",
"RESOURCE_PIDS_LIMIT": "512",
"SYS_SVC_CONTAINER_GPU_DEVICES": "GPU-strong,GPU-weak",
"NVIDIA_DRIVER_CAPABILITIES": "compute,utility",
}
class TestGpuRuntimeRegistration(unittest.TestCase):
def _render(self, *, gpu):
env = Environment(
loader=FileSystemLoader(str(TEMPLATES)),
trim_blocks=True,
lstrip_blocks=False,
undefined=StrictUndefined,
autoescape=select_autoescape(),
)
env.filters["resource_filter"] = lambda _apps, _id, _key, _svc, default, **_k: (
default
)
self.asked = []
def _lookup(name, *terms, **_kwargs):
self.asked.append((name, terms))
return gpu if name == "gpu" else {}
env.globals["lookup"] = _lookup
return env.get_template("resource.yml.j2").render(BASE)
def test_the_lookups_yes_reaches_the_runtime_key(self):
self.assertIn("runtime: nvidia", self._render(gpu=True))
def test_the_lookups_no_leaves_the_runtime_key_out(self):
self.assertNotIn("runtime:", self._render(gpu=False))
def test_the_runtime_is_told_which_devices_to_hand_over(self):
self.assertIn(
'NVIDIA_VISIBLE_DEVICES: "GPU-strong,GPU-weak"',
self._render(gpu=True),
)
def test_the_runtime_is_told_to_inject_the_cuda_driver(self):
self.assertIn(
'NVIDIA_DRIVER_CAPABILITIES: "compute,utility"',
self._render(gpu=True),
)
def test_the_lookups_no_leaves_the_device_env_out(self):
self.assertNotIn("NVIDIA_VISIBLE_DEVICES", self._render(gpu=False))
def test_the_lookup_is_asked_for_this_service(self):
self._render(gpu=True)
self.assertIn(
("gpu", ("web-svc-libretranslate", "libretranslate")),
self.asked,
)
if __name__ == "__main__":
unittest.main()