# Finaler Architekturplan **Dokumenten-ID:** GNENC-001-PLAN **Titel:** CPU-basiertes Video-Encoding auf dem KI-Server **Stand:** 2026-10-19 **Status:** Finaler Plan nach Planungsabschluss am 19.10.2026; die operative Pipeline ist in GNENC-002 spezifiziert, das host-weite Monitoring in GNMON-001 ## 1. Zweck und Abgrenzung Der physische Server `ghostnet-ki` soll neben den KI-Workloads einen getrennten Workload zum Encoding von Videodateien ausführen. Der Encoding-Workload gehört nicht zum LLM-Stack. Seine fachliche Architektur, Bedienung und späteren SOPs werden deshalb in diesem eigenen Dokumentationsbereich geführt. Ansible, Podman, Betriebssystem und hostweite Ressourcenregeln bleiben gemeinsame Plattformbestandteile. Sie werden weiterhin ausschließlich im Infrastructure-Repository verwaltet, damit nicht mehrere Repositories unabhängig konkurrierende Sollzustände auf denselben Host anwenden. Die Pipeline im Detail — Komponenten, Konten, Verzeichnislayout, Datenfluss, Validierung, Statusmodell — ist in [GNENC-002-PLAN_Pipeline.md](GNENC-002-PLAN_Pipeline.md) spezifiziert. Das host-weite Monitoring (Systemmetriken, Dashboard) wird im Monitoring-Bereich unter `docs/platform/llm-server/monitoring/` geführt (GNMON). Die Samba-Anbindung der Pipeline auf CT-101 ist in [GNAI-017](../proxmox-server/lxc/ct-101-samba/GNAI-017-PLAN_Samba-NFS-Export.md) beschrieben. ## 2. Bestätigte Entscheidungen | Thema | Entscheidung | Nachweisgrenze | | --- | --- | --- | | Repository | Host- und Deployment-Ansible bleibt im bestehenden Infrastructure-Repository | angenommen | | Dokumentation | Encoding liegt unter `docs/platform/llm-server/encoding/` und verwendet die Kennung `GNENC`; Monitoring unter `docs/platform/llm-server/monitoring/` mit der Kennung `GNMON`; CT 101 unter `docs/platform/proxmox-server/lxc/ct-101-samba/` mit GNAI-017 | umgesetzt | | Ausführungsidentitäten | drei gesperrte Konten: `gn-encoding` (UID/GID 982, Worker), `gn-download` (983, jDownloader2 + VPN-Sidecar), `gn-dashboard` (984, Dashboard, Monitoring-Bereich); gemeinsame Gruppe `gn-media` (GID 970) für die Übergabe-Verzeichnisse | gn-encoding eingerichtet und geprüft; weitere geplant | | Containertrennung | je Konto ein eigener rootless Podman-Store und ein eigener rootverwalteter Quadlet-Pfad | gn-encoding eingerichtet und verifiziert; weitere geplant | | Rechenressource | Encoding ist CPU-basiert ohne GPU-Zugriff; der host-seitige Metrics-Collector (Monitoring-Bereich) liest GPU-Metriken ausschließlich lesend für das Dashboard | Planungsentscheidung | | Parallelität | darf parallel zu AI Production oder AI Testing laufen | Planungsentscheidung | | Zeitsteuerung | dauerhaft laufende Dienste; der Encoding-Worker startet neue Releases nur nächtlich 00:00–06:00 Uhr Europe/Berlin, laufende Jobs laufen über das Fenster hinaus (kein Abbruch am Fensterende) | Betreiberentscheidung 19.10.2026; geändert gegenüber dem Erststand | | Download-Stufe | jDownloader2-Container (`jlesage/jdownloader-2`, gepinnter Digest) liest `.dlc` aus `input//`; Event-Scripter verschiebt den Release-Ordner atomar in die Queue | Betreiberentscheidung 19.10.2026 | | VPN | der gesamte jDownloader2-Verkehr läuft über einen ProtonVPN-WireGuard-Tunnel: gluetun-Sidecar (`qmcgaw/gluetun`, gepinnt) unter `gn-download`, JD2 teilt dessen Netzwerk-Namespace, Kill Switch aktiv (kein Traffic ohne Tunnel); WireGuard-Credentials server-lokal in `/etc/ghostnet-download/vpn.env`, nicht in git | Betreiberentscheidung 19.10.2026 | | Anwendung | Worker-Image als Eigenbau auf Basis des gepinnten HandBrake-Images `linuxserver/handbrake:1.11.2-ls48` (`sha256:fda92bd1…48a3c7`) mit python3 und ffmpeg; die Digests von Worker- und VPN-Image werden nach dem Bau gepinnt | Betreiberentscheidung 19.10.2026 | | Speicherort | Pipeline-Daten liegen auf CT-101 (Samba/NAS, 10.0.0.9): dort liegen die Medienbibliothek und der Plattenverbund; ghostnet-ki selbst hat nur eine 2-TB-NVMe (OS + LLM) | Betreiberentscheidung 19.10.2026 | | Ein-/Ausgabe | NFS-Export `/media/datenhdd3/Encoding` von CT-101 (10.0.0.9), gemountet auf `/var/lib/ghostnet-encoding`; Verzeichnislayout `input/`, `jd/`, `queue/`, `work/`, `output/`, `state/` | Betreiberentscheidung 19.10.2026; Server, IP und Export-Pfad festgelegt | | Abholung | fertige Outputs per SMB über CT-101: Share `[Encode]` (`output/`, RW für `ghost`, lesend für `daidalaw`/`debugofen`) — Entnahme per `mv` in die Bibliotheks-Shares räumt `output/` auf; optionaler Clean-Job löscht Dateien nach 30 Tagen aus `output/`; kein smbd auf `ghostnet-ki` | Betreiberentscheidung 19.10.2026 | | Samba-Integration (CT-101) | CT-101 wird ins Infrastructure-Repository aufgenommen (GNAI-017): NFS-Export nur für 10.0.0.90, Shares `[Encode]`/`[Encode-In]`, Gruppe `gn-media` (GID 970) auf CT-101 mit `ghost` als Mitglied (UID/GID-Abgleich); `.dlc`-Drop über `[Encode-In]` | Betreiberentscheidung 19.10.2026; GNAI-017 | | Profile | drei versionierte HandBrake-Presets (SVT-AV1 10-bit, MKV, Audio-Passthrough) unter `llm-server/encoding/profiles/`; Auswahl über gleichnamige Ordner unter `input/` bzw. `queue/` | Betreiberentscheidung 19.10.2026; Presets versioniert | | Warteschlange | dateibasiert: `.dlc`-Drop in `input//`, jDownloader2 befüllt `queue///`, der Worker arbeitet die Queue mit genau einem gleichzeitigen Encode ab | Betreiberentscheidung 19.10.2026 | | Filter | Nicht-Video-Dateien und Dateien < 100 MB werden ignoriert; alle übrigen Videodateien eines Releases werden encodet (keine Hauptdatei-Auswahl) | Betreiberentscheidung 19.10.2026 | | Validierung | HandBrakeCLI-Exit 0, nicht-leere Zieldatei, vorhandener Videostream, fehlerfreie ffmpeg-Volldecodierung (leere stderr) und ffprobe-Dauerabweichung ≤ 1 s | Betreiberentscheidung 19.10.2026 | | Fehlerbehandlung | fehlerhafte Zieldatei löschen, Quelldatei nach `output/_failed//` verschieben (Quarantäne pro Datei); erfolgreiche Dateien desselben Releases bleiben verschont | Betreiberentscheidung 19.10.2026; geändert gegenüber nächtlichem Retry | | Aufbewahrung | Quelldatei wird nach erfolgreichem Encoding und Validierung gelöscht; Output bleibt bis zur Abholung; `work/` wird pro Lauf geleert; `done`-Status wird nach 30 Tagen gelöscht | Betreiberentscheidung 19.10.2026 | | Statusmodell | eine JSON-Datei pro Videodatei unter `state/jobs/`, atomar geschrieben (Encoding); Systemmetriken in `state/system.json` werden vom Metrics-Collector des Monitoring-Bereichs bereitgestellt | Betreiberentscheidung 19.10.2026 | | Statusanzeige | Dashboard im Monitoring-Bereich (GNMON-001): Container unter `gn-dashboard` (Python/Flask), nur lesende Mounts; erreichbar über NPM (CT-103) mit Keycloak-Absicherung | Betreiberentscheidung 19.10.2026 | | Ressourcenlimits | Quadlet-/cgroup-Limits: Worker MemoryMax 16G, Nice 10, IOWeight 50; jDownloader2 MemoryMax 2G, CPUWeight 50, IOWeight 20, Nice 10; VPN-Sidecar MemoryMax 256M, CPUWeight 50; keine CPU-Obergrenze fürs Encoding | Betreiberentscheidung 19.10.2026; Feinjustierung nach Lasttest offen | | Zugangsdaten | jDownloader2-Credentials und VPN-WireGuard-Config liegen server-lokal unter `/etc/ghostnet-download/` (`jdownloader/`, `vpn.env`; root-owned, nicht in git); im Repository nur Platzhalter/Beispiele | Betreiberentscheidung 19.10.2026 | ## 3. Zielbild ```mermaid flowchart LR HOST[ghostnet-ki] PROD[gnai-production\nrootless Podman] TEST[gnai-testing\nrootless Podman] ENC[gn-encoding\nrootless Podman] DL[gn-download\nrootless Podman] DASH[gn-dashboard\nrootless Podman] NAS[CT-101 Samba/NFS\n10.0.0.9] GPU[beide GPUs] CPU[CPU / I-O Budget] TIMER[nächtliche Fensterlogik\nim Worker] HOST --> PROD HOST --> TEST HOST --> ENC HOST --> DL HOST --> DASH HOST -->|"NFS-Mount /var/lib/ghostnet-encoding"| NAS PROD --> GPU TEST --> GPU ENC --> CPU DL --> CPU DASH --> CPU TIMER --> ENC ``` AI Production und AI Testing bleiben untereinander exklusiv. Der Encoding-Workload ist von dieser KI-Umschaltung unabhängig und darf parallel zur jeweils aktiven KI-Umgebung laufen. Hostwartung kann weiterhin einen Stopp aller Workloads erfordern. Der Datenfluss der Pipeline folgt dem Filesystem-Prinzip: Jede Komponente liest den Output-Ordner des vorangegangenen Schritts; Übergaben erfolgen per atomarem `mv` innerhalb des NFS-Mounts. Der Download-Verkehr von jDownloader2 läuft ausschließlich über den VPN-Tunnel. ```mermaid flowchart LR DLC["input//*.dlc"] VPN["gluetun-Sidecar (ProtonVPN, Kill Switch)"] JD2["jDownloader2-Container (gn-download)"] QUEUE["queue///"] WORKER["Encoding-Worker (gn-encoding)"] OUT["output///"] FAILED["output/_failed//"] STATE["state/jobs/*.json"] DASH["Dashboard (gn-dashboard, Monitoring)"] DLC --> JD2 JD2 -.->|"teilt Netzwerk-Namespace"| VPN JD2 -->|"atomares mv"| QUEUE QUEUE --> WORKER WORKER -->|"validiert"| OUT WORKER -->|"Fehler"| FAILED WORKER --> STATE STATE --> DASH ``` ## 4. Ressourcen- und Zeitsteuerung Die Encoding-Pipeline läuft als dauerhaft laufende systemd-User-Dienste der Konten `gn-encoding` (Worker), `gn-download` (jDownloader2 und VPN-Sidecar) mit `Restart=on-failure`. Der Worker prüft vor jedem neuen Release, ob das nächtliche Fenster (00:00–06:00 Uhr Europe/Berlin) aktiv ist, und idlet außerhalb des Fensters. Ein laufendes Release wird am Fensterende nicht abgebrochen, sondern zu Ende geführt (Betreiberentscheidung 19.10.2026; ersetzt den früheren geordneten Stopp mit nächtlichem Neustart). Da der Worker außerhalb des Fensters idlet, entsteht tagsüber praktisch keine Last durch das Encoding. Die Ressourcenlimits schützen daher vor allem gegen Fehlerfälle und stellen sicher, dass selbst ein versehentlich tagsüber laufender Encode die Inferenz nicht verdrängt: | Ressource | Encoding-Worker | jDownloader2 | VPN-Sidecar | Dashboard | | --- | --- | --- | --- | --- | | MemoryMax | 16G | 2G | 256M | 512M | | CPUWeight | 100 | 50 | 50 | 50 | | Nice | 10 | 10 | — | — | | IOWeight | 50 | 20 | — | — | Es gibt keine CPU-Obergrenze für den Worker: Nachts stehen alle Kerne zur Verfügung. Konkrete Grenzwerte werden nach einem repräsentativen Encoding-Test unter gleichzeitiger Inferenzlast feinjustiert (Abschnitt 7). `gn-encoding` erhält keine Mitgliedschaft in GPU-Gruppen und keine `/dev/kfd`- oder `/dev/dri`-Weitergabe; `gn-dashboard` ebenso nicht — der host-seitige Metrics-Collector liest GPU-Metriken ausschließlich lesend für das Dashboard. ## 5. Repository-Grenzen Am 19.10.2026 wurde das LLM-Server-Projekt fachlich in ein Hauptverzeichnis pro Aufgabe aufgeteilt; die Struktur folgt den Bereichen unter `docs/platform/llm-server/`: ```text llm-server/ ├── host/ansible/ # gemeinsame Plattform des physischen Servers │ ├── playbooks/ │ │ ├── host.yml # host_baseline, podman_runtime (shared), workload_accounts, amd_gpu_access │ │ ├── preflight.yml │ │ ├── bootstrap-deploy-account.yml │ │ ├── verify-deploy-access.yml │ │ └── verify-rocm-container.yml │ └── roles/ │ ├── host_baseline/ │ ├── workload_accounts/ │ ├── amd_gpu_access/ │ └── rocm_container_probe/ ├── llm/ # LLM-Stack; entsteht mit dem AI-Vertical-Slice (GNAI-001 Meilenstein 2) ├── encoding/ │ ├── ansible/ # Encoding-Workload (Playbooks + Rollen) │ ├── images/ # Containerfiles (Worker) │ ├── profiles/ # versionierte HandBrake-Presets │ └── scripts/ # Worker- und Event-Scripter-Skripte └── monitoring/ ├── ansible/ # Monitoring (Collector-Service, Dashboard-Quadlet) ├── images/ # Containerfile (Dashboard) └── scripts/ # Metrics-Collector ``` Die Samba-LXC CT-101 wird als eigener Bereich unter `proxmox/lxc/ct-101-samba/` geführt (Dokumentation GNAI-017 unter `docs/platform/proxmox-server/lxc/ct-101-samba/`). `host.yml` verwaltet ausschließlich gemeinsame Plattformzustände; die gemeinsame Rolle `workload_accounts` legt dabei auch die Konten `gn-encoding`, `gn-download` und `gn-dashboard` an. `video-encoding.yml` verwaltet den Encoding-spezifischen Zustand — Pfade, Images, Quadlets und Health Checks — und setzt die Host-Baseline voraus. Ein künftiges `ai.yml` im `llm/`-Projekt darf nur KI-Zustände verändern; ein `monitoring.yml` im `monitoring/`-Projekt verwaltet den Monitoring-Zustand (Konto `gn-dashboard`, Collector, Dashboard). Ein späteres `site.yml` kann die unterstützte Gesamtkonfiguration in definierter Reihenfolge anwenden. Eigene Programme liegen versioniert unter `llm-server/encoding/scripts/` bzw. `llm-server/monitoring/scripts/`, Containerfiles unter `images/` und die Presets unter `llm-server/encoding/profiles/` (Betreiberentscheidung 19.10.2026). Ein separates Quellcode-Repository wird erst benötigt, wenn eine Oberfläche oder ein umfangreicher Dienst eigenständig entwickelt wird. Ein solches Repository erzeugt ein versioniertes Artefakt oder Image; ausschließlich das Infrastructure-Repository legt fest, welche Version auf dem Server läuft. ## 6. Aktueller Implementierungsstand Die gemeinsame Rolle `workload_accounts` hat am 20.09.2026 eingerichtet: ```text Konto: gn-encoding (UID/GID 982) Konfiguration: /etc/ghostnet-encoding Daten: /var/lib/ghostnet-encoding Eingang: /var/lib/ghostnet-encoding/input Ausgang: /var/lib/ghostnet-encoding/output Arbeitsdaten: /var/lib/ghostnet-encoding/work Quadlets: /etc/containers/systemd/users/982 Podman-Store: /var/lib/ghostnet-encoding/.local/share/containers/storage ``` Passwort und interaktive Shell sind gesperrt. Der systemd-User-Manager läuft dank Linger unabhängig von einer Anmeldung. Der Podman-Store wurde als rootless und getrennt von beiden KI-Stores geprüft. Es wurden noch keine Images oder Container für den Encoding-Workload angelegt. Am 19.10.2026 wurde das Encoding-Ansible-Projekt unter `llm-server/encoding/ansible/` eingerichtet. `video-encoding.yml` verwaltet und verifiziert seitdem den Encoding-Grundzustand: Konto-Identität und Passwort- sperre, subordinate ID-Bereiche, Verzeichnisstruktur, Quadlet-Verzeichnis, Linger und aktiver User-Manager, getrennter rootless Podman-Store sowie die CPU-Isolation (keine GPU-Gruppenmitgliedschaft, kein Lesezugriff auf `/dev/kfd` oder DRM-Render-Nodes). Solange die Rolle kein Encoder-Image gesetzt hat, stellt sie zusätzlich sicher, dass weder Quadlet-Dateien noch Container für `gn-encoding` existieren. Check-Mode und zwei aufeinanderfolgende Läufe meldeten jeweils `ok=30 changed=0 failed=0`. Das gepinnte HandBrake-Image `linuxserver/handbrake:1.11.2-ls48` (`sha256:fda92bd1…48a3c7`) wurde anschließend in den rootless Store von `gn-encoding` gezogen; der Pull bestätigte den Manifest-Digest. Am 19.10.2026 wurde die Planung abgeschlossen (Abschnitte 2 und 7). Die Implementierungsphase baut als Nächstes auf: der NFS-Export und die Shares `[Encode]`/`[Encode-In]` auf CT-101 (GNAI-017), die Konten `gn-download` und `gn-dashboard` samt Gruppe `gn-media` (GID 970, auch auf CT-101), der NFS-Mount auf ghostnet-ki, die Image-Bauten mit Digest-Pinning (Worker, VPN-Sidecar, Dashboard), die Quadlet-Units für jDownloader2, VPN-Sidecar, Worker und Dashboard, der host-seitige Metrics-Collector sowie die Skripte aus `scripts/`. Die Rolle `video_encoding` stellt bis dahin sicher, dass weder Quadlet-Dateien noch Container für `gn-encoding` existieren. ## 7. Offene Entscheidungen 1. Feinjustierung der Ressourcenlimits nach einem repräsentativen Encoding-Test unter gleichzeitiger Inferenzlast (Abschnitt 4). Diese Werte werden nicht vorab in Playbooks angenommen.