Files
ProxMenux/oci/tests/test_immich_acceleration.py
T
MacRimi 9b5cefb81a feat(oci): GPU selection per host and per image, one notification per update, and App tab for stack containers
- Immich asks what runs its video and its recognition in one menu, in both
  modes, and gives the GPU to the server and to Machine learning; AMD uses ROCm
- Frigate, Ollama, llama.cpp, Faster Whisper and Piper take the image built
  for the chosen GPU
- The acceleration menu offers only what the host can run
- An update or a recreation sends one notification with its result instead of
  the stop, backup and start of each container
- A private bridge with nothing connected is not reported as down
- Secondary containers of a stack appear in the App tab with their version and
  logo; Secure Gateway shows the same update state in both views
- A mistyped value in the wizard asks the same question again
2026-10-02 21:45:38 +02:00

130 lines
6.7 KiB
Python

"""Immich asks in one menu, in both installation modes, what runs its video
transcoding and its recognition: the CPU, or each usable GPU of the host for
both or for only one of them."""
from pathlib import Path
import sys
import unittest
from unittest.mock import patch
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "src"))
sys.path.insert(0, str(Path(__file__).resolve().parent))
from proxmenux_oci.catalog import Catalog
from proxmenux_oci.installer import ADVANCED_MODE, DEFAULT_MODE, build_deployment
from test_advanced_flow_order import RecordingUI, addresses, storages
PROMPT = "Hardware acceleration for Immich"
NODE = "/dev/dri/renderD128"
INTEL = {"intel": [NODE], "amd": [], "nvidia": False}
AMD = {"intel": [], "amd": [NODE], "nvidia": False}
BOTH = {"intel": [NODE], "amd": [], "nvidia": True}
NOTHING = {"intel": [], "amd": [], "nvidia": False}
class OptionsUI(RecordingUI):
def __init__(self, answers=None):
super().__init__(answers)
self.options = {}
self.defaults = {}
self.messages = []
def choose(self, text, options, default=None):
self.options[text] = [tag for tag, _ in options]
self.defaults[text] = default
return super().choose(text, options, default)
def message(self, text, title=None):
self.messages.append(text)
@patch("proxmenux_oci.i18n.language", return_value="en")
@patch("proxmenux_oci.installer.host.storages", side_effect=storages)
@patch("proxmenux_oci.installer.host.bridges", return_value=[{"iface": "vmbr0", "cidr": "192.0.2.10/24"}])
@patch("proxmenux_oci.installer.host.timezone", return_value="Europe/Madrid")
@patch("proxmenux_oci.installer.host.rocm_blocker", return_value=None)
@patch("proxmenux_oci.installer.access.ask_addresses", side_effect=addresses)
class ImmichAccelerationTests(unittest.TestCase):
template = Catalog(ROOT).compose("immich")
def plan(self, gpus, mode, answer=None):
ui = OptionsUI({PROMPT: answer} if answer else None)
with patch("proxmenux_oci.installer.host.gpus", return_value=gpus):
plan = build_deployment(self.template, ui, mode)
return ui, (plan["video_transcoding"]["acceleration"], plan["machine_learning"]["acceleration"]), plan
def test_the_menu_is_asked_in_both_modes_with_what_the_host_has(self, *_):
for mode in (DEFAULT_MODE, ADVANCED_MODE):
ui, _, _ = self.plan(INTEL, mode)
self.assertEqual(ui.options[PROMPT], ["cpu", "intel", "intel-video", "intel-ml"], mode)
ui, _, _ = self.plan(BOTH, DEFAULT_MODE)
self.assertEqual(ui.options[PROMPT], ["cpu", "intel", "intel-video", "intel-ml",
"nvidia", "nvidia-video", "nvidia-ml", "intel+nvidia"])
def test_the_first_gpu_is_proposed_whole(self, *_):
ui, result, _ = self.plan(BOTH, DEFAULT_MODE)
self.assertEqual(ui.defaults[PROMPT], "intel")
self.assertEqual(result, ("vaapi", "openvino"))
def test_every_option_gives_the_gpu_to_what_it_names(self, *_):
expected = {"cpu": ("cpu", "cpu"), "intel": ("vaapi", "openvino"), "intel-video": ("vaapi", "cpu"),
"intel-ml": ("cpu", "openvino"), "nvidia": ("nvenc", "cuda"), "nvidia-video": ("nvenc", "cpu"),
"nvidia-ml": ("cpu", "cuda"), "intel+nvidia": ("vaapi", "cuda")}
for answer, result in expected.items():
self.assertEqual(self.plan(BOTH, DEFAULT_MODE, answer)[1], result, answer)
for answer, result in {"amd": ("vaapi", "rocm"), "amd-video": ("vaapi", "cpu"),
"amd-ml": ("cpu", "rocm")}.items():
self.assertEqual(self.plan(AMD, DEFAULT_MODE, answer)[1], result, answer)
def test_recognition_alone_still_gets_its_render_device(self, *_):
for gpus, answer in ((INTEL, "intel-ml"), (AMD, "amd-ml")):
_, _, plan = self.plan(gpus, DEFAULT_MODE, answer)
self.assertEqual(plan["machine_learning"]["render_device"], NODE, answer)
self.assertIsNone(plan["video_transcoding"]["render_device"], answer)
def test_a_host_without_usable_gpu_says_so_and_installs_on_the_cpu(self, *_):
for mode in (DEFAULT_MODE, ADVANCED_MODE):
ui, result, _ = self.plan(NOTHING, mode)
self.assertNotIn(PROMPT, ui.options, mode)
self.assertEqual(len(ui.messages), 1, mode)
self.assertIn("No usable GPU", ui.messages[0])
self.assertEqual(result, ("cpu", "cpu"), mode)
def test_an_amd_host_that_cannot_run_rocm_says_so_and_recognises_on_the_cpu(self, *_):
for blocker, text in (("kfd", "/dev/kfd"), ("space", "40 GB")):
with patch("proxmenux_oci.installer.host.rocm_blocker", return_value=blocker):
ui, result, _ = self.plan(AMD, DEFAULT_MODE, "amd")
self.assertEqual(result, ("vaapi", "cpu"), blocker)
self.assertTrue(any(text in message and "Recognition runs on the CPU." in message
for message in ui.messages), blocker)
def test_machine_learning_gets_four_cores_and_at_least_four_gigabytes(self, *_):
_, _, plan = self.plan(BOTH, DEFAULT_MODE, "cpu")
self.assertEqual(plan["machine_learning"]["resources"]["cores"], 4)
self.assertEqual(plan["machine_learning"]["resources"]["memory_mb"], 4096)
for gpus, answer in ((BOTH, "nvidia"), (INTEL, "intel"), (AMD, "amd")):
_, _, plan = self.plan(gpus, DEFAULT_MODE, answer)
self.assertEqual(plan["machine_learning"]["resources"]["memory_mb"], 8192, answer)
def test_the_installers_give_the_gpu_to_both_containers(self, *_):
script = (ROOT / "remote/install_immich_stack.sh").read_text()
helper = (ROOT / "remote/oci_immich_ml.sh").read_text()
self.assertIn('configure_immich_nvidia "$SERVER_ID" "compute,video,utility"', script)
self.assertIn('configure_immich_nvidia "$ML_ID" "compute,utility"', helper)
self.assertIn('--dev1 "path=/dev/kfd', helper)
self.assertIn("MIGraphXExecutionProvider", helper)
self.assertIn("ML_ROOTFS_SIZE=40", helper)
self.assertIn('--rootfs "${ROOTFS_STORAGE}:${ML_ROOTFS_SIZE}"', script)
self.assertIn("'rocm')", (ROOT / "remote/oci_stack_replay.py").read_text())
self.assertIn("MIGraphXExecutionProvider", (ROOT / "remote/oci_stack_native.py").read_text())
def test_the_name_of_the_machine_learning_container_is_not_translated(self, *_):
script = (ROOT / "remote/install_immich_stack.sh").read_text()
self.assertNotIn('translate "Machine learning"', script)
self.assertNotIn('translate("Machine learning")', (ROOT / "src/proxmenux_oci/cli.py").read_text())
if __name__ == "__main__":
unittest.main()