Files
computer-playbook/plugins/lookup/gpu.py
T
kevinveenbirkenbachandClaude Opus 5.5 137afc60cc fix(sys-svc-container): require device and driver for a GPU host, refuse a driverless bare host
Follow-up to ae259a9f58 after an independent review:

- The 136-character task name broke the 80-character name lint
  (tests/lint/repository/test_name_emoji_and_length.py). Names are short
  again; the device/driver/host matrix lives in the role README.
- Both stats now always run and lookup('gpu') requires both registers,
  instead of reading a skipped device stat as "no device".
- A bare host that shows /dev/nvidia0 without nvidia-smi stops the
  deploy with a message naming the fix. It used to die on the missing
  binary anyway; ae259a9f58 would have run its GPU services on the CPU
  silently. A container in that state (a swarm lab node, the dev
  container without its GPU override) still runs on the CPU.
- svc-ai-s1 decided its CUDA build from its own stat of /dev/nvidia0 and
  nvidia-container-runtime while its runtime came from lookup('gpu'), so
  a host with the toolkit but no nvidia-smi would have built a CUDA image
  that gets no GPU. S1_GPU now reads lookup('gpu'), the one detection.
- The libretranslate-engine README and the s1 TODO describe the new
  detection.

test_gpu_detection walks the real tasks over device, driver and
container; against the original 00_core.yml it fails in the
driverless-container, driverless-bare and device-and-driver cells.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 11:57:24 +01:00

96 lines
3.4 KiB
Python

"""Whether a service gets a GPU: it asked for one AND the host carries one.
Two facts have to agree and used to be restated at every call site. The
declaration lives in ``applications`` under ``services.<service>.gpu``; the
hardware is ``sys-svc-container``'s two stats on the target host: the device
node ``/dev/nvidia0`` (``sys_svc_container_nvidia_device``) and the driver's
``nvidia-smi`` (``sys_svc_container_nvidia_driver``). Asking only the
declaration selects a ``-cuda`` image on a host that has no device, and that
tag has no arm64 manifest at all, so the build fails with ``no match for
platform in manifest``. A privileged container sees the host's device node
without the driver, so the node alone does not make a GPU host either.
The stats are target-host facts, so this reads the registers out of the
play's variables rather than inspecting the controller it runs on.
"""
from __future__ import annotations
from typing import Any
from ansible.errors import AnsibleError
from ansible.module_utils.parsing.convert_bool import boolean
from ansible.plugins.loader import lookup_loader
from ansible.plugins.lookup import LookupBase
from plugins.filter.resource_filter import resource_filter
DEVICE_FACT = "sys_svc_container_nvidia_device"
DRIVER_FACT = "sys_svc_container_nvidia_driver"
def _stat_exists(variables: dict, fact: str) -> bool:
"""Whether a stat register of the target host found its path.
Args:
variables: the play's variables, carrying the register.
fact: name of the register.
Returns:
False while the register is absent, which is also the answer before
``sys-svc-container`` has run.
"""
register = variables.get(fact) or {}
if not isinstance(register, dict):
return False
return bool((register.get("stat") or {}).get("exists"))
def _device_present(variables: dict) -> bool:
"""Whether the target host carries an NVIDIA device node and its driver.
Args:
variables: the play's variables, carrying both registers.
Returns:
True only when both stats found their path.
"""
return _stat_exists(variables, DEVICE_FACT) and _stat_exists(variables, DRIVER_FACT)
class LookupModule(LookupBase):
def run(
self,
terms: list[Any],
variables: dict | None = None,
**kwargs: Any,
) -> list[bool]:
"""Return ``[bool]``: the service is GPU-accelerated on this host.
Args:
terms: ``application_id`` and optionally the service name; the
service defaults to the application's own entity.
variables: the play's variables.
Raises:
AnsibleError: wrong number of terms.
"""
if not 1 <= len(terms) <= 2:
raise AnsibleError(
"gpu: expected 1 or 2 terms: lookup('gpu', application_id[, service])"
)
application_id = str(terms[0])
service_name = str(terms[1]) if len(terms) == 2 else ""
templar = getattr(self, "_templar", None)
variables = variables or getattr(templar, "available_variables", {}) or {}
applications = lookup_loader.get(
"applications", loader=self._loader, templar=templar
).run([], variables=variables)[0]
wanted = resource_filter(
applications, application_id, "gpu", service_name, False
)
return [boolean(wanted, strict=False) and _device_present(variables)]