Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
251 lines
16 KiB
Markdown
251 lines
16 KiB
Markdown
# 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/<Profil>/`; 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/<Profilname>/`, jDownloader2 befüllt `queue/<Profilname>/<Release>/`, 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/<Release>/` 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/<Profil>/*.dlc"]
|
||
VPN["gluetun-Sidecar (ProtonVPN, Kill Switch)"]
|
||
JD2["jDownloader2-Container (gn-download)"]
|
||
QUEUE["queue/<Profil>/<Release>/"]
|
||
WORKER["Encoding-Worker (gn-encoding)"]
|
||
OUT["output/<Profil>/<Release>/"]
|
||
FAILED["output/_failed/<Release>/"]
|
||
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.
|