LLM-Server: Encoding-Pipeline (GNENC-002) und Monitoring-Dokumentation (GNMON) aufgenommen; Bereichsindizes aktualisiert

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
2026-10-26 19:20:36 +01:00
co-authored by Mistral Vibe
parent 9b3b54bd99
commit 61f2158508
8 changed files with 598 additions and 115 deletions
+5 -4
View File
@@ -1,16 +1,17 @@
# LLM-Server `ghostnet-ki`
Dieser Gerätebereich dokumentiert den physischen Server `10.0.0.90`. Er
gruppiert die gemeinsame Hostbasis, den eigentlichen LLM-Stack und den davon
fachlich getrennten CPU-Encoding-Workload. Alle drei Bereiche teilen Hardware,
Betriebssystem und Ansible-Control-Projekt.
gruppiert die gemeinsame Hostbasis, den eigentlichen LLM-Stack, den davon
fachlich getrennten CPU-Encoding-Workload und das host-weite Monitoring. Alle
vier Bereiche teilen Hardware, Betriebssystem und Ansible-Control-Projekt.
| Bereich | Inhalt |
| --- | --- |
| [Host](host/GNAI-002-INFRA_BaseSystem.md) | Hardware, Debian-Basis und GPU-Ausgangslage |
| [Zugänge](host/GNAI-003-INFRA_Users.md) | Benutzer, Netzwerk, Resolver und SSH |
| [AI](ai/README.md) | Architektur, Ansible-Deployment, Modellbewertung und `llama.cpp`-Serving |
| [Encoding](encoding/GNENC-000-DOC_Index.md) | CPU-Encoding-Architektur und offener Implementierungsstand |
| [Encoding](encoding/GNENC-000-DOC_Index.md) | CPU-Encoding-Architektur und Pipeline-Plan |
| [Monitoring](monitoring/GNMON-000-DOC_Index.md) | Systemmetriken, Collector und Dashboard |
Übergreifende Rollen stehen im
[Zugriffskonzept](../GNAI-005-SEC_Access-Control.md), der Gesamtstand im
@@ -1,51 +1,73 @@
# Video-Encoding – Dokumentationsindex und Projektstand
**Dokumenten-ID:** GNENC-000-DOC
**Stand:** 2026-10-05
**Status:** Nicht finaler Arbeitsstand; Encoding-Workload noch nicht installiert
**Stand:** 2026-10-19
**Status:** Planungsabschluss; Pipeline geplant, Implementierung noch nicht begonnen
## Dokumente
| Dokument | Zweck / Zielgruppe |
| --- | --- |
| [GNENC-001-PLAN_Architecture.md](GNENC-001-PLAN_Architecture.md) | vorläufige Architektur, Repository-Grenzen, Ressourcenmodell und offene Entscheidungen; Infrastrukturadministratoren |
| [GNENC-001-PLAN_Architecture.md](GNENC-001-PLAN_Architecture.md) | finale Architektur, Repository-Grenzen, Ressourcenmodell und offene Entscheidungen; Infrastrukturadministratoren |
| [GNENC-002-PLAN_Pipeline.md](GNENC-002-PLAN_Pipeline.md) | operative Pipeline: Komponenten, Konten, Verzeichnislayout, Datenfluss, Validierung, Statusmodell; Infrastrukturadministratoren und Betreiber |
Dashboard und Systemmetriken sind in den Monitoring-Bereich ausgelagert:
[Monitoring → GNMON-000](../monitoring/GNMON-000-DOC_Index.md).
Die Samba-/NFS-Anbindung auf CT-101 ist in
[GNAI-017](../../proxmox-server/lxc/ct-101-samba/GNAI-017-PLAN_Samba-NFS-Export.md)
beschrieben.
## Bestätigter Stand
- Das gemeinsame Infrastructure-Repository bleibt die einzige Quelle für den
Ansible-Sollzustand des physischen Servers.
- Encoding-Dokumentation verwendet innerhalb des LLM-Server-Gerätebereichs
`docs/platform/llm-server/encoding/` und die Dokumentenkennung `GNENC`.
- Encoding ist nach Betreiberfestlegung CPU-basiert und darf parallel zur
jeweils aktiven KI-Umgebung laufen.
- Encoding läuft automatisch nächtlich von 00:00 bis 06:00 Uhr Europe/Berlin
und darf parallel zur jeweils aktiven KI-Umgebung laufen.
- Das gesperrte Servicekonto `gn-encoding` mit UID/GID 982 und eigenem
rootless Podman-Store ist eingerichtet und verifiziert.
- Das Encoding-Ansible-Projekt unter `llm-server/encoding/ansible/` verwaltet
und verifiziert den Encoding-Grundzustand (Konto, Pfade, CPU-Isolation,
rootless Store); Check-Mode und Idempotenzlauf meldeten jeweils `changed=0`.
- Das Image `linuxserver/handbrake:1.11.2-ls48` ist mit dem amd64-Manifest-Digest
`sha256:fda92bd1b43a07a770b10ad793172b10451ba1690803112dbaf2b48a6148a3c7`
gepinnt und per Pull im rootless Store von `gn-encoding` verifiziert.
- Drei HandBrake-Presets (SVT-AV1 10-bit, MKV, Audio-Passthrough) liegen
versioniert unter `llm-server/encoding/profiles/`; die Profilwahl erfolgt
über gleichnamige Ordner unter `input/`.
- Aufbewahrung: Input-Dateien werden nach erfolgreichem Encoding und
Validierung gelöscht; Output bleibt bis zur Abholung; `work/` wird pro Lauf
geleert.
- Statusanzeige: Dashboard-Endpunkte für Arbeitsfortschritt und
System-Ressourcen des Encoding-Workloads.
- Es sind noch kein Container, keine Quadlet-Unit und kein Timer eingerichtet.
- Planungsabschluss am 19.10.2026: Die Pipeline besteht aus VPN-Sidecar
(ProtonVPN), jDownloader2 (Download), Encoding-Worker (HandBrakeCLI +
ffmpeg); Handover ausschließlich über das Filesystem, In-Progress-Schutz
über Work-Ordner und atomares `mv`.
- Speicherort: CT-101 (Samba/NAS, 10.0.0.9) — NFS-Export
`/media/datenhdd3/Encoding`, gemountet auf `/var/lib/ghostnet-encoding`
(ghostnet-ki hat nur eine 2-TB-NVMe; die Medienbibliothek liegt auf CT-101).
- Verzeichnislayout: `input/<Profil>/*.dlc`, `jd/`, `queue/<Profil>/<Release>/`,
`work/`, `output/<Profil>/<Release>/` + `output/_failed/`, `state/jobs/` +
`state/system.json`.
- Konten: `gn-encoding` (982, eingerichtet und verifiziert), `gn-download`
(983, jDownloader2 + VPN-Sidecar), `gn-dashboard` (984, Dashboard,
Monitoring-Bereich); gemeinsame Gruppe `gn-media` (GID 970, auch auf CT-101).
- VPN: gesamter jDownloader2-Verkehr über ProtonVPN-WireGuard
(gluetun-Sidecar mit Kill Switch); WireGuard-Credentials server-lokal in
`/etc/ghostnet-download/vpn.env`, nicht in git.
- Profile: drei versionierte HandBrake-Presets (SVT-AV1 10-bit, MKV,
Audio-Passthrough) unter `llm-server/encoding/profiles/`; Auswahl über
gleichnamige Ordner, keine Auto-Detection.
- Zeitplan: dauerhaft laufende Dienste; der Worker startet neue Releases nur
00:00–06:00 Uhr Europe/Berlin, laufende Jobs laufen über das Fenster hinaus
(kein Abbruch am Fensterende).
- Validierung: Exit 0, nicht-leere Zieldatei, Videostream vorhanden,
ffmpeg-Volldecodierung ohne stderr-Fehler, Dauerabweichung ≤ 1 s.
- Fehlerbehandlung: Quarantäne pro Datei nach `output/_failed/`; Status
`failed` bis zur manuellen Prüfung.
- Abholung: fertige Outputs per SMB über CT-101 (Share `[Encode]`, RW für
`ghost`; Entnahme per `mv` räumt auf; optionaler Clean-Job nach 30 Tagen);
`.dlc`-Drop über Share `[Encode-In]`; kein smbd auf `ghostnet-ki`.
- Statusmodell: JSON pro Videodatei in `state/jobs/`, atomar geschrieben;
Systemmetriken in `state/system.json` (Metrics-Collector des
Monitoring-Bereichs).
- Ressourcenlimits: Quadlet/cgroup (Worker MemoryMax 16G, Nice 10, IOWeight 50;
jDownloader2 MemoryMax 2G, CPUWeight 50, IOWeight 20; VPN-Sidecar 256M).
- Images: Worker als Eigenbau auf Basis des gepinnten HandBrake-Digests
(`linuxserver/handbrake:1.11.2-ls48`, `sha256:fda92bd1…48a3c7`) mit python3
und ffmpeg; `jlesage/jdownloader-2` und `qmcgaw/gluetun` gepinnt.
- Credentials: server-lokal unter `/etc/ghostnet-download/` (`jdownloader/`,
`vpn.env`), nicht in git; Platzhalter im Repository.
- Der Ansible-Grundzustand für `gn-encoding` ist eingerichtet und verifiziert
(Check-Mode und Idempotenzlauf `changed=0`); es existieren noch keine
Quadlets, Container oder Timer für den Encoding-Workload.
## Nächste Festlegungen
## Noch offen
Am 19.10.2026 festgelegt: HandBrake im Image `linuxserver/handbrake:1.11.2-ls48`
(amd64-Manifest-Digest gepinnt), nächtliches Zeitfenster 00:00–06:00 Uhr
Europe/Berlin mit geordnetem Stopp und nächtlichem Retry, gemeinsamer
NFS-Mount mit `input/`- und `output/`-Unterverzeichnissen, drei HandBrake-Presets
mit Auswahl über Input-Ordner, Aufbewahrung (Input-Löschung nach Encoding und
Validierung, Output bis Abholung, `work/` pro Lauf) sowie Dashboard-Endpunkte
für Fortschritt und Ressourcen. Noch offen: NFS-Server-Adresse und
Export-Pfad, Validierungsverfahren, technische Umsetzung der Endpunkte sowie
CPU-/I/O-Grenzen nach einem Lasttest unter Inferenzlast.
1. Feinjustierung der Ressourcenlimits nach einem repräsentativen Encoding-Test
unter gleichzeitiger Inferenzlast.
2. Rootless-TUN-Verifizierung für den VPN-Sidecar auf dem Host.
3. Implementierung: CT-101 (NFS-Export, Shares `[Encode]`/`[Encode-In]`,
Gruppe `gn-media`/GID-Abgleich — GNAI-017), Konten `gn-download`/
`gn-dashboard`, NFS-Mount, Image-Bauten, Quadlet-Units (inkl. VPN-Sidecar),
Skripte, Monitoring-Bereich (Collector, Dashboard).
@@ -1,9 +1,9 @@
# Arbeitsentwurf
# Finaler Architekturplan
**Dokumenten-ID:** GNENC-001-PLAN
**Titel:** CPU-basiertes Video-Encoding auf dem KI-Server
**Stand:** 2026-10-05
**Status:** Nicht finaler Arbeitsentwurf zur Persistierung der Planungsentscheidung
**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
@@ -17,23 +17,43 @@ 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` | umgesetzt |
| Ausführungsidentität | eigenes gesperrtes Konto `gn-encoding`, UID/GID 982 | eingerichtet und mit rootless Podman geprüft |
| Containertrennung | eigener rootless Podman-Store und eigener rootverwalteter Quadlet-Pfad | eingerichtet; Grundzustand per Ansible verwaltet und verifiziert; noch keine Workload definiert |
| Rechenressource | Encoding ist nach Betreiberfestlegung rein CPU-basiert und erhält keinen GPU-Zugriff | Planungsentscheidung; reale Last noch nicht gemessen |
| 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 | nächtlich 00:00–06:00 Uhr Europe/Berlin; am Fensterende geordneter Stopp, unfertige Jobs starten nächtlich erneut | Betreiberentscheidung 19.10.2026 |
| Anwendung | HandBrake im Image `linuxserver/handbrake:1.11.2-ls48` mit gepinntem amd64-Manifest-Digest `sha256:fda92bd1b43a07a770b10ad793172b10451ba1690803112dbaf2b48a6148a3c7` | Betreiberentscheidung 19.10.2026; Digest aufgelöst und per Pull im gn-encoding-Store bestätigt |
| Ein-/Ausgabe | ein NFS-Export, gemountet auf `/var/lib/ghostnet-encoding`; `input/` und `output/` als Unterverzeichnisse | Betreiberentscheidung 19.10.2026; Server-Adresse und Export-Pfad offen |
| Profile | drei versionierte HandBrake-Presets (SVT-AV1 10-bit, MKV, Audio-Passthrough) unter `llm-server/encoding/profiles/`; Auswahl über gleichnamige Ordner unter `input/` | Betreiberentscheidung 19.10.2026; Presets versioniert |
| Warteschlange | dateibasiert über `input/<Profilname>/`; genau ein gleichzeitiger Job; unfertige oder fehlerhafte Jobs werden nächtlich wiederholt; Teilresultate in `work/` | Betreiberentscheidung 19.10.2026 |
| Aufbewahrung | Input-Datei wird nach erfolgreichem Encoding und Validierung gelöscht; Output bleibt bis zur Abholung; `work/` wird pro Lauf geleert | Betreiberentscheidung 19.10.2026 |
| Statusanzeige | Dashboard-Endpunkte für Arbeitsfortschritt und System-Ressourcen des Encoding-Workloads | Betreiberentscheidung 19.10.2026; technische Umsetzung offen |
| 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
@@ -43,16 +63,24 @@ flowchart LR
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 systemd-Zeitsteuerung]
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
```
@@ -61,24 +89,61 @@ 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 nächtliche Einplanung allein schützt Production nicht vollständig: Ein
Encoding-Lauf kann länger dauern als sein Startfenster, und Inferenz benötigt
trotz GPU-Schwerpunkt CPU für Tokenisierung, Netzwerk und Begleitdienste. Die
spätere Encoding-Unit soll deshalb mindestens verwalten:
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).
- ein eindeutig definiertes Zeitfenster mit Zeitzone;
- Verhalten bei verpasstem Start, Neustart und überlangem Lauf;
- maximale CPU-Nutzung oder CPU-Gewichtung;
- niedrigere Scheduling-Priorität gegenüber interaktiven Diensten;
- I/O-Gewichtung und freien Speicher;
- geordneten Abbruch beziehungsweise Abschluss am Ende des erlaubten Fensters;
- Begrenzung auf genau einen gleichzeitigen Encoding-Job.
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:
Konkrete Grenzwerte werden erst nach einem repräsentativen Encoding-Test unter
gleichzeitiger Inferenzlast festgelegt. `gn-encoding` erhält keine Mitgliedschaft
in GPU-Gruppen und keine `/dev/kfd`- oder `/dev/dri`-Weitergabe.
| 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
@@ -101,27 +166,37 @@ llm-server/
│ ├── amd_gpu_access/
│ └── rocm_container_probe/
├── llm/ # LLM-Stack; entsteht mit dem AI-Vertical-Slice (GNAI-001 Meilenstein 2)
└── encoding/ansible/ # CPU-Encoding-Workload
├── playbooks/
│ └── video-encoding.yml # video_encoding
└── roles/
└── video_encoding/
├── 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
```
`host.yml` verwaltet ausschließlich gemeinsame Plattformzustände; die
gemeinsame Rolle `workload_accounts` legt dabei auch das Konto `gn-encoding`
an. `video-encoding.yml` verwaltet den Encoding-spezifischen Zustand des Kontos
`gn-encoding` — seine Pfade, Images, Quadlets, Timer 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 späteres `site.yml` kann die unterstützte
Gesamtkonfiguration in definierter Reihenfolge anwenden.
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/`).
Eigene Programme (z. B. das Queue-/Warteschlangen-Skript) liegen versioniert
unter `llm-server/encoding/scripts/` (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.
`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
@@ -141,7 +216,7 @@ 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 angelegt.
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
@@ -149,33 +224,27 @@ 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 offenen Entscheidungen in Abschnitt 7 nicht
getroffen sind, stellt die Rolle 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.
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
Am 19.10.2026 wurden Encoder und Image (HandBrake, gepinnter Digest), das
nächtliche Zeitfenster (00:00–06:00 Uhr Europe/Berlin), das Abbruchverhalten am
Fensterende (geordneter Stopp, nächtlicher Retry), das Ein-/Ausgabeverfahren
(gemeinsamer NFS-Mount mit `input/`- und `output/`-Unterverzeichnissen), die
drei HandBrake-Presets mit profilbezogener Auswahl über Input-Ordner, die
Aufbewahrung (Input-Löschung nach Encoding und Validierung, Output bis zur
Abholung, `work/` pro Lauf) und die Statusanzeige (Dashboard-Endpunkte für
Arbeitsfortschritt und System-Ressourcen) festgelegt. Offen bleiben:
1. Welche NFS-Server-Adresse, welcher Export-Pfad und welche Mount-Optionen
gelten für den gemeinsamen Mount auf `/var/lib/ghostnet-encoding`?
2. Wie wird ein fertiger Encode validiert, bevor die Input-Datei gelöscht wird
(Vorschlag: HandBrakeCLI-Exit 0, nicht-leere Zieldatei, fehlerfreies ffprobe)?
3. Wie werden die Dashboard-Endpunkte technisch umgesetzt (Metriken für
Arbeitsfortschritt und System-Ressourcen, Transport, Authentifizierung)?
4. Welche CPU-, Speicher- und I/O-Grenzen schützen die KI-Dienste? Konkrete
Grenzwerte werden erst nach einem repräsentativen Encoding-Test unter
gleichzeitiger Inferenzlast festgelegt (Abschnitt 4).
1. Feinjustierung der Ressourcenlimits nach einem repräsentativen Encoding-Test
unter gleichzeitiger Inferenzlast (Abschnitt 4).
Diese Werte werden nicht vorab in Playbooks angenommen.
@@ -0,0 +1,239 @@
# Encoding-Pipeline
**Dokumenten-ID:** GNENC-002-PLAN
**Titel:** Encoding-Pipeline: Komponenten, Konten, Verzeichnislayout und Datenfluss
**Stand:** 2026-10-19
**Status:** Finaler Plan nach Betreiberentscheidungen vom 19.10.2026
**Vorgänger:** [GNENC-001-PLAN_Architecture.md](GNENC-001-PLAN_Architecture.md)
**Monitoring:** [GNMON-001-PLAN_Architecture.md](../monitoring/GNMON-001-PLAN_Architecture.md)
**Samba/NFS (CT-101):** [GNAI-017-PLAN_Samba-NFS-Export.md](../../proxmox-server/lxc/ct-101-samba/GNAI-017-PLAN_Samba-NFS-Export.md)
## 1. Architekturprinzip
Jede Komponente führt einen Schritt aus und lebt in einem Podman-Container
(rootless, eigener Store je Konto). Der Handover erfolgt ausschließlich über
das Filesystem auf dem NFS-Mount `/var/lib/ghostnet-encoding`: Jede Komponente
liest einen Input-Ordner, der der Output-Ordner des vorangegangenen Schritts
ist. In-Progress-Daten liegen in Work-Ordnern; Übergaben erfolgen per atomarem
`mv` (`rename` innerhalb desselben Mounts). Dadurch erkennt der nächste Schritt
niemals halbfertige Dateien.
## 2. Komponenten
| Komponente | Konto | Image | Aufgabe | Betrieb |
| --- | --- | --- | --- | --- |
| VPN-Sidecar | `gn-download` (983) | `qmcgaw/gluetun` (gepinnter Digest) | ProtonVPN-WireGuard-Tunnel mit Kill Switch; jDownloader2 teilt seinen Netzwerk-Namespace, gesamter Download-Verkehr läuft durch den Tunnel | dauerhaft |
| Download | `gn-download` (983) | `jlesage/jdownloader-2` (gepinnter Digest) | liest `.dlc` aus `input/<Profil>/`, lädt über den VPN-Tunnel herunter, entpackt nach `jd/`, Event-Scripter verschiebt den Release-Ordner atomar nach `queue/<Profil>/<Release>/` und räumt die `.dlc` auf | dauerhaft |
| Encoding-Worker | `gn-encoding` (982) | Worker-Image (Eigenbau, gepinnt) | scannt `queue/`, filtert, encodet alle gültigen Videodateien, validiert, räumt auf, schreibt Status, Housekeeping | dauerhaft; Fenster-Check 00:00–06:00 Europe/Berlin |
Dashboard und Metrics-Collector werden im Monitoring-Bereich betrieben
(GNMON-001); das Dashboard liest `state/` read-only.
## 3. Konten und Berechtigungen
| Konto | UID/GID | Sub-ID-Bereich | Home | Config | Gruppen |
| --- | --- | --- | --- | --- | --- |
| `gn-encoding` | 982 | 493216–558751 | `/var/lib/ghostnet-encoding` | `/etc/ghostnet-encoding` | `gn-media` |
| `gn-download` | 983 | 558752–624287 | `/var/lib/ghostnet-download` | `/etc/ghostnet-download` | `gn-media` |
| `gn-dashboard` | 984 | 624288–689823 | `/var/lib/ghostnet-dashboard` | `/etc/ghostnet-dashboard` | `gn-media` (nur lesend) |
Gruppe `gn-media`: gemeinsame Gruppe der drei Konten für die Übergabe-
Verzeichnisse und den lesenden Zugriff auf `state/`; GID 970, auf `ghostnet-ki`
und CT-101 identisch angelegt (GNAI-017). Alle Konten sind gesperrt
(Passwort `!`, Shell `/usr/sbin/nologin`, Linger aktiv).
## 4. Verzeichnislayout
```text
/var/lib/ghostnet-encoding/ (NFS-Mount: 10.0.0.9:/media/datenhdd3/Encoding, CT-101)
├── input/<Profil>/ gn-media 2770 Drop-Ziel, nur .dlc
├── jd/ gn-download JD2-Arbeitsverzeichnis
│ ├── <Release>/ Download + Entpacken
│ ├── dlc_done/ erfolgreich abgearbeitete .dlc
│ └── failed/ .dlc mit Download-Fehler
├── queue/<Profil>/<Release>/ gn-media 2770 Roh-Queue, atomar befüllt
├── work/<Release>/ gn-encoding 0750 Encode-Ziele in Arbeit
├── output/
│ ├── <Profil>/<Release>/ gn-encoding Erfolge bis Abholung
│ └── _failed/<Release>/ gn-encoding Quarantäne
└── state/
├── jobs/<job_id>.json gn-encoding Status pro Videodatei
├── tmp/ gn-encoding atomare Schreib-Zwischenlage
└── system.json Collector Systemmetriken (Monitoring)
```
`state/` ist `gn-encoding:gn-media` mit Mode `2750`, damit `gn-dashboard` lesen
kann; `state/jobs/` und `state/tmp/` bleiben `0750` (nur Worker).
`state/system.json` wird vom Metrics-Collector des Monitoring-Bereichs mit
`0644` geschrieben.
Handover-Regeln:
- Übergabe immer per atomarem `mv` innerhalb des Mounts; Work-Dateien liegen
nie in Übergabe-Ordnern.
- `input/`, `jd/`, `queue/` mit Mode `2770` (setgid), damit neue Dateien die
Gruppe `gn-media` erben.
- jDownloader2 läuft mit `UMASK=022`, damit der Worker die Dateien lesen kann.
- Der Worker löscht Quelldateien und geleerte Release-Ordner aus `queue/`;
Löschrechte folgen aus dem Schreibrecht der Gruppe am Verzeichnis (kein
Sticky-Bit).
- `work/` wird pro Lauf geleert; `state/tmp/` dient nur atomaren
Schreibzugriffen.
**Abholung:** Die fertigen Outputs werden über CT-101 per SMB abgeholt: Share
`[Encode]` (Pfad `/media/datenhdd3/Encoding/output`, RW für `ghost`, lesend für
`daidalaw`/`debugofen`). Die Entnahme per `mv` in die Bibliotheks-Shares
(Filme/Serien/Anime) räumt `output/` auf. Ein optionaler Clean-Job
(Worker-Housekeeping, Abschnitt 6) löscht Dateien aus `output/`, die älter als
30 Tage sind. Auf `ghostnet-ki` läuft kein smbd. Der `.dlc`-Drop erfolgt über
Share `[Encode-In]` (Pfad `/media/datenhdd3/Encoding/input`, RW für `ghost`)
— siehe GNAI-017.
## 5. Ablauf: Download (jDownloader2 über VPN)
1. `.dlc`-Datei in `input/<Profil>/` ablegen (Profil = Preset-Name) — per SMB
über die Share `Encode-In` oder direkt auf dem Mount.
2. jDownloader2 lädt über den VPN-Tunnel herunter und entpackt nach
`jd/<Release>/`.
3. Event-Scripter (versioniert in git, ohne Secrets, per Ansible in die
JD2-Config deployed):
- Paket fertig: atomares `mv jd/<Release>/ queue/<Profil>/<Release>/`;
`.dlc` nach `jd/dlc_done/`.
- Download-Fehler: `.dlc` nach `jd/failed/`.
4. Credentials liegen server-lokal in `/etc/ghostnet-download/jdownloader/`
(root-owned, Gruppe `gn-download`, Mode 0750, als `/config` gemountet). Im
Repository liegt nur ein Platzhalter/Beispiel; Ansible prüft Existenz, nie
Inhalt.
5. Die Web-UI des Images wird nicht extern freigegeben; Zugriff nur über
localhost/SSH-Tunnel.
**VPN-Tunnel (gluetun-Sidecar):**
- Image `qmcgaw/gluetun` (Digest gepinnt), Konto `gn-download`; JD2-Container
mit `Network=container:<vpn>` teilt den Netzwerk-Namespace — JD2 hat keine
eigene IP, der gesamte Verkehr verlässt den Host über ProtonVPN.
- ProtonVPN über WireGuard (`VPN_SERVICE_PROVIDER=protonvpn`); die Config
(Private Key, Server, Land) stammt aus dem ProtonVPN-Account und liegt
server-lokal in `/etc/ghostnet-download/vpn.env` (root-owned, `0640
root:gn-download`, per `EnvironmentFile=` geladen). Im Repository nur
`vpn.env.example`.
- Kill Switch aktiv: gluetuns Firewall blockiert jeglichen Nicht-VPN-Traffic,
sobald der Tunnel fällt.
- Quadlet: `NET_ADMIN` + `--device /dev/net/tun`. TUN in der rootless User-
Namespace wird bei der Umsetzung auf dem Host verifiziert; Fallback ist ein
host-seitiger WireGuard-Service mit Policy-Routing für den Download-Pfad.
- Hinweis: Hoster limitieren gelegentlich VPN-IPs; bei stockenden Downloads
Serverland in `vpn.env` wechseln. Die Management-VPN (CT-105) bleibt davon
unberührt.
## 6. Ablauf: Encoding-Worker
Pro Release (älteste zuerst, genau ein gleichzeitiger Encode), pro Videodatei
sequenziell:
1. **Scan & Filter:** `queue/<Profil>/<Release>/` durchsuchen. Video-Endungen
(case-insensitiv): `mkv, mp4, m4v, avi, mov, wmv, flv, webm, mpg, mpeg,
ts, m2ts, vob, 3gp, ogv`. Dateien < 100 MB und Nicht-Video-Dateien werden
ignoriert. Es gibt keine Hauptdatei; alle gültigen Videodateien werden
encodet.
2. **Status `queued` → `encoding`** in `state/jobs/<job_id>.json` (inkl.
Fortschritt aus dem HandBrakeCLI-Output).
3. **Encode:** `HandBrakeCLI --import-preset <Profil>.json -Z "<Profil>" -i
<quelle> -o work/<Release>/<stem>.mkv`. Presets liegen unter `profiles/`,
werden per Ansible auf den Host deployed und read-only gemountet.
4. **Validierung** (am Ziel in `work/`, vor der Übergabe); Status `verifying`:
- HandBrakeCLI-Exit 0
- Zieldatei nicht leer
- `ffprobe` findet mindestens einen Videostream
- `ffmpeg -v error -i <ziel> -f null -`: Exit 0 **und** leere stderr
(ffmpeg dekodiert tolerant; Fehlerzeilen auf stderr gelten als Defekt)
- ffprobe-Dauerabweichung Ziel vs. Quelle ≤ 1 s
5. **Erfolg:** atomares `mv` nach `output/<Profil>/<Release>/`, Quelldatei
löschen, Status `done`.
**Fehler:** Zieldatei löschen, Quelldatei nach `output/_failed/<Release>/`
verschieben, Status `failed` mit Fehlermeldung. Übrige Dateien des Releases
werden weiter verarbeitet.
6. Release-Ordner in `queue/` löschen, sobald leer.
**Zeitfenster:** Vor jedem neuen Release prüft der Worker, ob 00:00–06:00 Uhr
Europe/Berlin aktiv ist; außerhalb idlet er. Ein laufendes Release wird nicht
abgebrochen und darf über 06:00 hinaus laufen.
**Housekeeping (optional, standardmäßig aktiviert, deaktivierbar):** Im
Idle-Durchlauf löscht der Worker Dateien in `output/`, die älter als 30 Tage
sind (Clean-Job als Sicherheitsnetz; die Abholung per `mv` räumt regulär auf).
## 7. Statusmodell
Eine JSON-Datei pro Videodatei unter `state/jobs/<job_id>.json`, atomar
geschrieben (Schreibzugriff in `state/tmp/`, dann `mv`):
```json
{
"job_id": "<Release>__<Dateiname>",
"release": "<Release>",
"profile": "AV1 MKV 1080p30 RF25",
"source": "queue/<Profil>/<Release>/<Datei>",
"target": "output/<Profil>/<Release>/<Datei>.mkv",
"status": "queued | encoding | verifying | done | failed",
"progress_pct": 0.0,
"source_duration_s": 7231.4,
"target_duration_s": 7231.2,
"source_bytes": 0,
"target_bytes": 0,
"started_at": "…",
"finished_at": "…",
"error": null
}
```
Lebenszyklus: `queued` → `encoding` → `verifying` → `done` | `failed`.
Aufbewahrung: `done` nach 30 Tagen gelöscht; `failed` bis zur manuellen
Prüfung.
`state/system.json` (Systemmetriken) wird vom Metrics-Collector des
Monitoring-Bereichs bereitgestellt (GNMON-001).
## 8. Ressourcenlimits
Quadlet-/cgroup-Limits, in der User-Session durchgereicht:
| Ressource | Encoding-Worker | jDownloader2 | VPN-Sidecar |
| --- | --- | --- | --- |
| MemoryMax | 16G | 2G | 256M |
| CPUWeight | 100 | 50 | 50 |
| Nice | 10 | 10 | — |
| IOWeight | 50 | 20 | — |
Keine CPU-Obergrenze fürs Encoding. Feinjustierung nach einem repräsentativen
Lasttest unter Inferenzlast (GNENC-001 Abschnitt 4).
## 9. Monitoring (Auslagerung)
Dashboard und Metrics-Collector werden im Monitoring-Bereich geplant und
betrieben (GNMON-001, `llm-server/monitoring/`). Die Encoding-Pipeline stellt
`state/jobs/*.json` bereit; der Collector schreibt `state/system.json`; das
Dashboard mountet `state/` und `queue/` read-only und zeigt Ressourcen sowie
Encoding-Übersicht. Das Dashboard hat keinen GPU- und keinen Schreibzugriff.
## 10. Images
| Image | Basis | Inhalt | Pinning |
| --- | --- | --- | --- |
| Worker | `linuxserver/handbrake:1.11.2-ls48` @ `sha256:fda92bd1…48a3c7` | + python3, ffmpeg | Digest nach Bau in Ansible-Vars gepinnt; Build rootless als `gn-encoding` |
| jDownloader2 | `jlesage/jdownloader-2` | — | Digest gepinnt; Pull als `gn-download` |
| VPN-Sidecar | `qmcgaw/gluetun` | — | Digest gepinnt; Pull als `gn-download` |
Containerfiles unter `llm-server/encoding/images/`, Skripte unter
`llm-server/encoding/scripts/`, Presets unter `llm-server/encoding/profiles/`.
Skripte und Presets werden per Ansible auf den Host deployed und read-only in
die Container gemountet (Updates ohne Image-Rebuild, versioniert über git).
## 11. Offene Punkte
1. Feinjustierung der Ressourcenlimits nach Lasttest unter Inferenzlast
(GNENC-001 Abschnitt 4).
2. Rootless-TUN-Verifizierung für den VPN-Sidecar auf dem Host (Fallback:
host-seitiger WireGuard mit Policy-Routing).
3. UID/GID-Abgleich: Gruppe `gn-media` (GID 970) muss auf CT-101 identisch
existieren; `ghost` Mitglied für die Abholung per `mv` (GNAI-017).
@@ -0,0 +1,32 @@
# Monitoring – Dokumentationsindex und Projektstand
**Dokumenten-ID:** GNMON-000-DOC
**Stand:** 2026-10-19
**Status:** Planungsabschluss; Implementierung noch nicht begonnen
## Dokumente
| Dokument | Zweck / Zielgruppe |
| --- | --- |
| [GNMON-001-PLAN_Architecture.md](GNMON-001-PLAN_Architecture.md) | Host-Monitoring: Metriken, Collector, Dashboard, Anbindung Encoding; Infrastrukturadministratoren |
## Bestätigter Stand
- Planungsabschluss 19.10.2026: host-weites Monitoring für `ghostnet-ki`,
fachlich getrennt vom Encoding-Workload (aus GNENC ausgegliedert).
- Komponenten: host-seitiger Metrics-Collector (systemd-Service, schreibt
`state/system.json` alle 10 s) und Dashboard-Container unter `gn-dashboard`
(984).
- Metriken: CPU, RAM, Disk (Root + NFS-Export), GPU-Util, VRAM.
- Dashboard: Python/Flask, read-only Mounts, kein GPU-/Schreibzugriff;
erreichbar über NPM (CT-103) mit Keycloak.
- Anbindung Encoding: Dashboard liest `state/jobs/*.json` read-only
(`state/` group-readable via `gn-media`).
- Noch nicht implementiert: Konto `gn-dashboard`, Collector-Service,
Dashboard-Quadlet, Image-Bau.
## Noch offen
1. NPM-Hostname und Keycloak-Client (Umsetzung).
2. Collector-Identität (root vs. Metrics-Account mit `render`-Gruppe).
3. Erweiterung um LLM-Metriken (später).
@@ -0,0 +1,107 @@
# Monitoring
**Dokumenten-ID:** GNMON-001-PLAN
**Titel:** Host-Monitoring auf dem KI-Server: Systemmetriken und Workload-Dashboards
**Stand:** 2026-10-19
**Status:** Finaler Plan nach Betreiberentscheidungen vom 19.10.2026
**Referenzen:** [GNENC-001](../encoding/GNENC-001-PLAN_Architecture.md), [GNENC-002](../encoding/GNENC-002-PLAN_Pipeline.md), [GNAI-005](../../GNAI-005-SEC_Access-Control.md), [GNAI-006](../../proxmox-server/lxc/ct-103-reverse-proxy/GNAI-006-SEC_Reverse-Proxy.md)
## 1. Zweck und Abgrenzung
Host-weites Monitoring für `ghostnet-ki`: Systemmetriken (CPU, RAM, Disk,
GPU-Util, VRAM) und Dashboards für Workloads. Aktuell angebunden ist die
Encoding-Pipeline (GNENC-002); später erweiterbar um LLM-Metriken (GPU-Util
unter Inferenz, llama.cpp-Statistik). Der Bereich ist fachlich getrennt vom
Encoding-Workload (aus GNENC ausgegliedert, Betreiberentscheidung 19.10.2026), eilt aber Hardware und Host-Basis.
## 2. Architekturprinzip
Filesystem-Handover wie in der Encoding-Pipeline: Der Metrics-Collector
(host-seitig) schreibt `state/system.json` atomar; Workloads stellen ihre
Status-Dateien bereit (Encoding: `state/jobs/*.json`). Das Dashboard liest
ausschließlich Dateien (read-only Mounts) und hat weder GPU- noch
Schreibzugriff.
## 3. Komponenten
| Komponente | Konto | Image | Aufgabe | Betrieb |
| --- | --- | --- | --- | --- |
| Metrics-Collector | host-seitig (systemd-Service; root oder dedizierter Metrics-Account mit `render`-Gruppe) | — | sampelt alle 10 s CPU, RAM, Disk (Root + NFS-Export), GPU-Util und VRAM (rocm-smi/sysfs, lesend); schreibt `state/system.json` atomar | dauerhaft |
| Dashboard | `gn-dashboard` (984) | Dashboard-Image (Eigenbau, gepinnt; Python + Flask) | liest `system.json` + `state/jobs/*.json` (Encoding, read-only Mount); zeigt Ressourcen und Workload-Übersicht | dauerhaft |
## 4. Konto `gn-dashboard`
UID/GID 984, subid/subgid ab 624288 (65536 Stück), Home
`/var/lib/ghostnet-dashboard`, Config `/etc/ghostnet-dashboard`, Quadlets
`/etc/containers/systemd/users/984`, eigener rootless Podman-Store, gesperrt
(Passwort `!`, Shell `/usr/sbin/nologin`, Linger aktiv). Mitglied der Gruppe
`gn-media` (nur lesender Zugriff auf `state/` und `queue/` der
Encoding-Pipeline). Keine GPU-Gruppenmitgliedschaft, keine `/dev/kfd`-/
`/dev/dri`-Weitergabe.
## 5. Metrics-Collector
- Sampling alle 10 s; atomares Schreiben (`state/tmp/` + `mv`) nach
`/var/lib/ghostnet-encoding/state/system.json` (Mode `0644`).
- Schema:
```json
{
"ts": "…",
"cpu_util_pct": 0.0,
"ram_total_bytes": 0,
"ram_used_bytes": 0,
"disks": [{"mount": "/", "total_bytes": 0, "used_bytes": 0}],
"gpu_util_pct": 0.0,
"vram_used_bytes": 0,
"vram_total_bytes": 0
}
```
- GPU-Metriken werden host-seitig gelesen (rocm-smi oder sysfs); der
Dashboard-Container selbst hat keinen GPU-Zugriff.
- Ressourcenlimits: MemoryMax 512M, CPUWeight 50.
## 6. Dashboard
- **Views:** Ressourcen (CPU, RAM, Disk, GPU-Util, VRAM aus `system.json`);
Encoding-Übersicht (aktive Jobs mit Fortschritt, Queue-Tiefe pro Profil,
Historie done/failed der letzten 30 Tage, Fehlerfälle mit Ablageort in
`output/_failed/`); später LLM-Metriken.
- **Technik:** Python/Flask, server-seitig gerendert, ohne JS-Framework. Nur
read-only Mounts (`state/`, `queue/`); kein GPU-Zugriff, kein Schreibzugriff.
- **Erreichbarkeit:** Ingress über NPM (CT-103) mit TLS, zugriffsgeschützt
über Keycloak — analog zu den bestehenden Admin-Pfaden
(GNAI-005/GNAI-006).
- **Image:** schlankes Python-Base-Image + Flask, Eigenbau unter
`llm-server/monitoring/images/`, Digest gepinnt.
## 7. Anbindung Encoding-Pipeline
- Der Encoding-Worker schreibt `state/jobs/*.json` (Schema in GNENC-002
Abschnitt 7); das Dashboard liest read-only.
- `state/` ist `gn-encoding:gn-media` mit Mode `2750`, damit `gn-dashboard`
lesen kann; Schreibzugriffe bleiben beim Worker (`state/jobs/`, `state/tmp/`
`0750`) und beim Collector (`state/system.json`).
- Das Dashboard hat keine Schreibzugriffe in die Pipeline.
## 8. Repository-Grenzen
```text
llm-server/monitoring/
├── ansible/ # Playbook + Rolle (Collector-Service, Dashboard-Quadlet, Konto-Zustand)
├── images/ # Dashboard-Containerfile
└── scripts/ # Metrics-Collector
```
Eigene Programme liegen versioniert unter `llm-server/monitoring/scripts/`;
das Dashboard-Image wird gebaut und per Digest gepinnt (analog GNENC-001
Abschnitt 5).
## 9. Offene Punkte
1. NPM-Hostname und Keycloak-Client für das Dashboard (Umsetzung mit
GNAI-005/GNAI-006).
2. Collector-Identität: root oder dedizierter Metrics-Account mit
`render`-Gruppe — finale Festlegung bei der Umsetzung.
3. Erweiterung um LLM-Metriken (später, eigenes Thema im AI-Bereich).
+1
View File
@@ -8,6 +8,7 @@ This host root contains the executable infrastructure for the physical server at
| [`host/`](host/README.md) | shared server baseline: Ansible control project, host roles and inventories |
| [`llm/`](llm/README.md) | LLM serving stack (created with the internal AI vertical slice) |
| [`encoding/`](encoding/README.md) | CPU-based video encoding workload and its Ansible control project |
| [`monitoring/`](monitoring/README.md) | host-wide monitoring: metrics collector and dashboard |
Reusable roles that are also consumed by other host projects live under
[`../shared/ansible/`](../shared/ansible/).
+12
View File
@@ -0,0 +1,12 @@
# Monitoring workload
Host-wide monitoring on `ghostnet-ki`: system metrics (CPU, RAM, disk, GPU
utilization, VRAM) collected by a host-side metrics collector and visualised by
a dashboard container. The workload is separated from the encoding and LLM
workloads; the dashboard currently shows the encoding pipeline and is
extensible to LLM metrics.
Architecture and decisions are documented under
[`../../docs/platform/llm-server/monitoring/`](../../docs/platform/llm-server/monitoring/GNMON-000-DOC_Index.md).
The executable Ansible control project under `ansible/` entsteht mit der
Implementierung.