diff --git a/README.md b/README.md index e2459e4..ea5bf02 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Dokumentation für die Infrastruktur eines privaten Homelabs mit öffentlich erreichbaren Webdiensten, einem WireGuard-Zugang und einem dedizierten KI-Server. Dieses Repository hält Architekturentscheidungen, den belegten Betriebsstand und Verfahren für Einrichtung, Betrieb und Sicherheitsabnahme fest. -> **Projektstand: 20.09.2026.** Die Dokumentationsrevisionen sind zusammengeführt. Mehrere Sicherheitsmaßnahmen und technische Endabnahmen stehen noch aus. Auf dem KI-Server sind Host-Baseline, Podman und getrennte rootless Workload-Konten eingerichtet; LLM- und Encoding-Anwendungen sind noch nicht installiert. +> **Projektstand: 20.09.2026.** Die Dokumentationsrevisionen sind zusammengeführt. Mehrere Sicherheitsmaßnahmen und technische Endabnahmen stehen noch aus. Auf dem KI-Server sind Host-Baseline, Podman, getrennte rootless Workload-Konten und der Gerätezugriff der KI-Konten auf beide AMD-GPUs eingerichtet; LLM- und Encoding-Anwendungen sind noch nicht installiert. ## Überblick @@ -26,7 +26,7 @@ Die drei vorgesehenen VPN-Netzwerkrollen sind **Familie** (gesamtes Heimnetz), * | Bereich | Belegter Stand | Nächster Nachweis oder Schritt | | --- | --- | --- | -| KI-Server | Debian-Basissystem, Deployment-Zugang, Host-Baseline, Podman 5.4.2 und drei getrennte rootless Workload-Konten | GPU-Zugriff für KI prüfen sowie Encoding-Zeitfenster und Ressourcenlimits festlegen | +| KI-Server | Debian-Basissystem, Deployment-Zugang, Podman 5.4.2, drei getrennte rootless Workload-Konten und geprüfter Zugriff beider KI-Konten auf `/dev/kfd` sowie zwei AMD-Render-Nodes | ROCm im rootless `llama.cpp`-Container prüfen und danach den Testing-Stack bereitstellen | | VPN | wg-easy v15.4.0 auf CT 105 aktiv; ein externer Laptop-Teiltest liegt vor | Rollenrechte serverseitig umsetzen und mit erlaubten sowie gesperrten Zielen testen | | Reverse Proxy | NPM auf CT 103 dokumentiert; Fail2Ban laut Betreiber fest gepinnt und wieder in Betrieb | Effektive Jails, Sperren und Wiederanlauf prüfen | | Identität | Keycloak/OIDC laut Betreiber produktiv für Vaultwarden | Öffentlich erreichbare Keycloak-Administration sperren und den OIDC-Login erneut prüfen | @@ -41,7 +41,7 @@ Der [Dokumentationsindex](docs/GNAI-000-DOC_Index.md) enthält die datierten Que | [Plattform-Dokumentation](docs/platform/README.md) | Hostbasis, Zugriffsmodell, Reverse Proxy und VPN | | [KI-Dokumentation](docs/ai/README.md) | KI-Zielarchitektur und nicht finale Ansible-Deployment-Planung | | [Video-Encoding](docs/video-encoding/GNENC-000-DOC_Index.md) | getrennter Planungs- und späterer SOP-Bereich für den CPU-Encoding-Workload | -| [Ansible-Control-Projekt](ansible/README.md) | lokale Einrichtung, Preflight, Deployment-Account, Host-Baseline, Podman und Workload-Konten | +| [Ansible-Control-Projekt](ansible/README.md) | lokale Einrichtung, Preflight, Deployment-Account, Host-Baseline, Podman, Workload-Konten und AMD-GPU-Zugriff | ## Arbeiten mit dem Repository diff --git a/ansible/README.md b/ansible/README.md index bfe2c8d..6809619 100644 --- a/ansible/README.md +++ b/ansible/README.md @@ -2,8 +2,9 @@ This directory contains the reproducible control project for `ghostnet-ki`. The read-only `playbooks/preflight.yml` verifies the documented host identity -and hardware. The first writing milestone, `playbooks/host.yml`, applies a -small shared host baseline through the dedicated deployment account. +and hardware. The shared `playbooks/host.yml` applies the host baseline, +Podman runtime, workload identities and narrowly scoped AMD device access +through the dedicated deployment account. ## Control-node setup @@ -166,5 +167,24 @@ UID/GID and subordinate-ID ranges: Each account has a persistent systemd user manager and a separate, administrator-managed directory below `/etc/containers/systemd/users/`. Configuration is root-owned and group-readable; workload data belongs to its -service account. The role does not grant GPU access and creates no workload -containers. +service account. The two AI identities have `render` as their explicit +supplementary-group baseline; the encoding identity has no GPU group. The role +creates no workload containers. + +## AMD GPU access + +The `amd_gpu_access` role verifies exactly two PCI devices `1002:73bf` using +the `amdgpu` kernel driver. It maps their sysfs identities to the corresponding +DRM render nodes, checks `/dev/kfd`, derives the device groups and confirms +read/write access as both AI workload identities. + +On the current host, the verified devices are `/dev/kfd`, +`/dev/dri/renderD128` and `/dev/dri/renderD129`; all use group `render`. +`gnai-production` and `gnai-testing` can access them, while `gn-encoding` +remains outside that group. The final convergence run reported +`ok=55 changed=0 unreachable=0 failed=0`. + +This proves host-level device permissions only. No ROCm userspace image has +been pulled and no GPU workload has run yet. The next workload check must pass +these devices into a rootless container with `GroupAdd=keep-groups` and prove +that `llama.cpp` detects both cards as `gfx1030`. diff --git a/ansible/playbooks/host.yml b/ansible/playbooks/host.yml index 6cfc630..e1cd9c7 100644 --- a/ansible/playbooks/host.yml +++ b/ansible/playbooks/host.yml @@ -12,3 +12,4 @@ - role: host_baseline - role: podman_runtime - role: workload_accounts + - role: amd_gpu_access diff --git a/ansible/roles/amd_gpu_access/README.md b/ansible/roles/amd_gpu_access/README.md new file mode 100644 index 0000000..e528b81 --- /dev/null +++ b/ansible/roles/amd_gpu_access/README.md @@ -0,0 +1,16 @@ +# AMD GPU access role + +This shared host role verifies the two configured AMD GPUs and their `amdgpu` +kernel driver, discovers only the DRM render nodes belonging to their PCI IDs, +and checks `/dev/kfd`. It grants the Production and Testing service accounts +the minimal supplementary groups required by those device nodes. + +When group membership changes, the role restarts both persistent systemd user +managers so later rootless Quadlets can use `GroupAdd=keep-groups`. It verifies +that each service account can read and write `/dev/kfd` and both selected +render devices. + +The role installs no ROCm userspace packages, pulls no image and starts no +container. ROCm userspace will come from the pinned `llama.cpp` container. A +later workload check must still prove that the container detects both GPUs as +`gfx1030` and performs inference on them. diff --git a/ansible/roles/amd_gpu_access/defaults/main.yml b/ansible/roles/amd_gpu_access/defaults/main.yml new file mode 100644 index 0000000..247dec9 --- /dev/null +++ b/ansible/roles/amd_gpu_access/defaults/main.yml @@ -0,0 +1,10 @@ +--- +amd_gpu_vendor_id: "1002" +amd_gpu_device_id: "73bf" +amd_gpu_expected_count: 2 + +amd_gpu_workload_accounts: + - name: gnai-production + uid: 980 + - name: gnai-testing + uid: 981 diff --git a/ansible/roles/amd_gpu_access/handlers/main.yml b/ansible/roles/amd_gpu_access/handlers/main.yml new file mode 100644 index 0000000..9307497 --- /dev/null +++ b/ansible/roles/amd_gpu_access/handlers/main.yml @@ -0,0 +1,10 @@ +--- +- name: Restart AI workload user managers + ansible.builtin.systemd_service: + name: "user@{{ amd_gpu_account.uid }}.service" + state: restarted + loop: "{{ amd_gpu_workload_accounts }}" + loop_control: + loop_var: amd_gpu_account + label: "{{ amd_gpu_account.name }}" + when: not ansible_check_mode diff --git a/ansible/roles/amd_gpu_access/tasks/main.yml b/ansible/roles/amd_gpu_access/tasks/main.yml new file mode 100644 index 0000000..d684e8a --- /dev/null +++ b/ansible/roles/amd_gpu_access/tasks/main.yml @@ -0,0 +1,212 @@ +--- +- name: Verify AMD GPU access declarations + ansible.builtin.assert: + that: + - amd_gpu_vendor_id is match("^[0-9a-fA-F]{4}$") + - amd_gpu_device_id is match("^[0-9a-fA-F]{4}$") + - amd_gpu_expected_count | int > 0 + - >- + amd_gpu_workload_accounts | map(attribute="name") | unique | length == + amd_gpu_workload_accounts | length + - >- + amd_gpu_workload_accounts | map(attribute="uid") | unique | length == + amd_gpu_workload_accounts | length + fail_msg: AMD GPU identifiers and workload accounts must be valid and unique. + +- name: Read matching PCI devices and kernel drivers + ansible.builtin.command: + argv: + - /usr/bin/lspci + - -Dnnk + - -d + - "{{ amd_gpu_vendor_id }}:{{ amd_gpu_device_id }}" + changed_when: false + check_mode: false + register: amd_gpu_lspci + +- name: Verify expected GPUs use the amdgpu kernel driver + ansible.builtin.assert: + that: + - >- + amd_gpu_lspci.stdout_lines + | select("search", "(?i)" + amd_gpu_vendor_id + ":" + amd_gpu_device_id) + | list | length == amd_gpu_expected_count | int + - >- + amd_gpu_lspci.stdout + | regex_findall("Kernel driver in use: amdgpu") + | length == amd_gpu_expected_count | int + fail_msg: >- + Expected {{ amd_gpu_expected_count }} PCI devices + {{ amd_gpu_vendor_id }}:{{ amd_gpu_device_id }} using amdgpu. + +- name: Find DRM render nodes in sysfs + ansible.builtin.find: + paths: /sys/class/drm + patterns: "renderD*" + file_type: link + recurse: false + register: amd_gpu_sysfs_render_nodes + +- name: Read render node PCI vendor IDs + ansible.builtin.slurp: + src: "{{ amd_gpu_render_node.path }}/device/vendor" + loop: "{{ amd_gpu_sysfs_render_nodes.files }}" + loop_control: + loop_var: amd_gpu_render_node + label: "{{ amd_gpu_render_node.path | basename }}" + register: amd_gpu_render_vendor_ids + +- name: Read render node PCI device IDs + ansible.builtin.slurp: + src: "{{ amd_gpu_render_node.path }}/device/device" + loop: "{{ amd_gpu_sysfs_render_nodes.files }}" + loop_control: + loop_var: amd_gpu_render_node + label: "{{ amd_gpu_render_node.path | basename }}" + register: amd_gpu_render_device_ids + +- name: Initialize matching AMD render device list + ansible.builtin.set_fact: + amd_gpu_render_devices: [] + +- name: Select render devices belonging to the configured AMD GPUs + ansible.builtin.set_fact: + amd_gpu_render_devices: >- + {{ amd_gpu_render_devices + + ['/dev/dri/' + + (amd_gpu_sysfs_render_nodes.files[amd_gpu_render_index].path + | basename)] }} + loop: "{{ range(0, amd_gpu_sysfs_render_nodes.files | length) | list }}" + loop_control: + loop_var: amd_gpu_render_index + label: >- + {{ amd_gpu_sysfs_render_nodes.files[amd_gpu_render_index].path | basename }} + when: + - >- + amd_gpu_render_vendor_ids.results[amd_gpu_render_index].content + | b64decode | trim | lower == "0x" + amd_gpu_vendor_id | lower + - >- + amd_gpu_render_device_ids.results[amd_gpu_render_index].content + | b64decode | trim | lower == "0x" + amd_gpu_device_id | lower + +- name: Verify one render device was found for each configured GPU + ansible.builtin.assert: + that: + - amd_gpu_render_devices | length == amd_gpu_expected_count + fail_msg: >- + Expected {{ amd_gpu_expected_count }} matching AMD render devices but found + {{ amd_gpu_render_devices | length }}: {{ amd_gpu_render_devices }}. + +- name: Inspect the ROCm kernel device + ansible.builtin.stat: + path: /dev/kfd + register: amd_gpu_kfd + +- name: Verify the ROCm kernel device exists + ansible.builtin.assert: + that: + - amd_gpu_kfd.stat.exists + - amd_gpu_kfd.stat.ischr | default(false) + - amd_gpu_kfd.stat.gr_name != "root" + fail_msg: /dev/kfd must exist as a character device with a dedicated group. + +- name: Inspect matching AMD render devices + ansible.builtin.stat: + path: "{{ amd_gpu_render_device }}" + loop: "{{ amd_gpu_render_devices }}" + loop_control: + loop_var: amd_gpu_render_device + register: amd_gpu_render_device_stats + +- name: Verify matching AMD render devices are usable character devices + ansible.builtin.assert: + that: + - amd_gpu_render_stat.stat.exists + - amd_gpu_render_stat.stat.ischr | default(false) + - amd_gpu_render_stat.stat.gr_name != "root" + loop: "{{ amd_gpu_render_device_stats.results }}" + loop_control: + loop_var: amd_gpu_render_stat + label: "{{ amd_gpu_render_stat.amd_gpu_render_device }}" + +- name: Derive the minimal host groups required by the GPU devices + ansible.builtin.set_fact: + amd_gpu_required_group_names: >- + {{ ([amd_gpu_kfd.stat.gr_name] + + (amd_gpu_render_device_stats.results + | map(attribute="stat.gr_name") | list)) + | unique | sort | list }} + +- name: Grant AI workload accounts access to the GPU device groups + ansible.builtin.user: + name: "{{ amd_gpu_account.name }}" + groups: "{{ amd_gpu_required_group_names | join(',') }}" + append: true + loop: "{{ amd_gpu_workload_accounts }}" + loop_control: + loop_var: amd_gpu_account + label: "{{ amd_gpu_account.name }}" + notify: Restart AI workload user managers + +- name: Apply changed group memberships to persistent user managers + ansible.builtin.meta: flush_handlers + +- name: Read effective AI workload account groups + ansible.builtin.command: + argv: + - /usr/bin/id + - -nG + - "{{ amd_gpu_account.name }}" + changed_when: false + loop: "{{ amd_gpu_workload_accounts }}" + loop_control: + loop_var: amd_gpu_account + label: "{{ amd_gpu_account.name }}" + register: amd_gpu_account_groups + when: not ansible_check_mode + +- name: Verify AI workload account group memberships + ansible.builtin.assert: + that: + - >- + amd_gpu_required_group_names + | difference(amd_gpu_account_group.stdout.split()) | length == 0 + fail_msg: >- + {{ amd_gpu_account_group.amd_gpu_account.name }} is missing a required GPU + device group. + loop: "{{ amd_gpu_account_groups.results }}" + loop_control: + loop_var: amd_gpu_account_group + label: "{{ amd_gpu_account_group.amd_gpu_account.name }}" + when: not ansible_check_mode + +- name: Verify AI workload accounts can read and write required GPU devices + ansible.builtin.command: + argv: + - /usr/sbin/runuser + - --user + - "{{ amd_gpu_access_pair.0.name }}" + - -- + - /usr/bin/python3 + - -c + - >- + import os, sys; raise SystemExit( + 0 if os.access(sys.argv[1], os.R_OK | os.W_OK) else 1) + - "{{ amd_gpu_access_pair.1 }}" + changed_when: false + loop: >- + {{ amd_gpu_workload_accounts + | product(['/dev/kfd'] + amd_gpu_render_devices) | list }} + loop_control: + loop_var: amd_gpu_access_pair + label: "{{ amd_gpu_access_pair.0.name }} -> {{ amd_gpu_access_pair.1 }}" + when: not ansible_check_mode + +- name: Report verified AMD GPU access + ansible.builtin.debug: + msg: + pci_id: "{{ amd_gpu_vendor_id }}:{{ amd_gpu_device_id }}" + gpu_count: "{{ amd_gpu_expected_count }}" + render_devices: "{{ amd_gpu_render_devices }}" + device_groups: "{{ amd_gpu_required_group_names }}" + accounts: "{{ amd_gpu_workload_accounts | map(attribute='name') | list }}" diff --git a/ansible/roles/workload_accounts/README.md b/ansible/roles/workload_accounts/README.md index 3be0f85..531e203 100644 --- a/ansible/roles/workload_accounts/README.md +++ b/ansible/roles/workload_accounts/README.md @@ -11,6 +11,7 @@ service group. Workload data and the rootless Podman store belong to the service account. The role verifies that all three Podman stores are rootless and use distinct graph roots. -The role does not grant GPU device access and does not define containers, -networks, schedules or resource limits. Those states belong to the AI and -video-encoding workload roles. +The two AI accounts receive `render` as an explicit supplementary-group +baseline; the encoding account receives no GPU group. Hardware discovery and +effective device-access checks belong to `amd_gpu_access`. This role does not +define containers, networks, schedules or resource limits. diff --git a/ansible/roles/workload_accounts/defaults/main.yml b/ansible/roles/workload_accounts/defaults/main.yml index 60e5069..a154365 100644 --- a/ansible/roles/workload_accounts/defaults/main.yml +++ b/ansible/roles/workload_accounts/defaults/main.yml @@ -7,7 +7,8 @@ workload_accounts: home: /var/lib/ghostnet-ai/production config_directory: /etc/ghostnet-ai/production subid_start: 362144 - supplementary_groups: [] + supplementary_groups: + - render - name: gnai-testing uid: 981 gid: 981 @@ -15,7 +16,8 @@ workload_accounts: home: /var/lib/ghostnet-ai/testing config_directory: /etc/ghostnet-ai/testing subid_start: 427680 - supplementary_groups: [] + supplementary_groups: + - render - name: gn-encoding uid: 982 gid: 982 diff --git a/docs/GNAI-000-DOC_Index.md b/docs/GNAI-000-DOC_Index.md index d0082f3..84d24b0 100644 --- a/docs/GNAI-000-DOC_Index.md +++ b/docs/GNAI-000-DOC_Index.md @@ -61,6 +61,8 @@ Die Rolle `podman_runtime` wurde am 20.09.2026 angewendet. Sie installierte Podm Die Rolle `workload_accounts` richtete am 20.09.2026 die gesperrten Konten `gnai-production` (UID/GID 980), `gnai-testing` (981) und `gn-encoding` (982) mit festen, getrennten SubID-Bereichen ein. Konfiguration bleibt rootverwaltet; Daten, rootless Podman-Stores und persistente systemd-User-Manager sind pro Workload getrennt. Der korrigierte Gesamtlauf meldete `ok=36 changed=0 unreachable=0 failed=0` und bestätigte drei unterschiedliche rootless Graph Roots. Images und Container wurden nicht angelegt. +Die Rolle `amd_gpu_access` bestätigte am 20.09.2026 zwei Sapphire-Navi-21-Geräte `1002:73bf` mit dem Kernel-Treiber `amdgpu`, `/dev/kfd` sowie die zugehörigen Render-Nodes `/dev/dri/renderD128` und `/dev/dri/renderD129`. Alle drei Geräte verwenden die Gruppe `render`. `gnai-production` und `gnai-testing` besitzen nachweislich Lese-/Schreibzugriff; `gn-encoding` erhielt keine GPU-Gruppe. Nach Beseitigung einer konkurrierenden Gruppenverwaltung endete der Gesamtlauf mit `ok=55 changed=0 unreachable=0 failed=0`. ROCm im Container und tatsächliche Inferenz sind damit noch nicht nachgewiesen. + ## 3. Sicherheitsbefunde mit Priorität | Befund | dokumentiertes Verfahren / Status | @@ -105,4 +107,13 @@ rootseitiges `runuser` und eindeutiger Eigentümerzuständigkeit endete der Gesamtlauf mit `ok=36 changed=0 unreachable=0 failed=0`. Die drei rootless Podman-Stores sind voneinander getrennt und noch leer. +Der folgende GPU-Zugriffslauf ordnete beide erkannten AMD-Render-Nodes anhand +von PCI-Vendor und Geräte-ID den Navi-21-Karten zu und leitete aus ihnen sowie +`/dev/kfd` ausschließlich die Gruppe `render` ab. Der erste Wiederholungslauf +deckte auf, dass `workload_accounts` ergänzende Gruppen wieder entfernte. Nach +Übernahme von `render` in den expliziten Kontosollzustand blieb +`amd_gpu_access` für Hardwareerkennung und effektive Rechteprüfung zuständig. +Der Abschlusslauf war mit `changed=0` idempotent; Images und Container blieben +weiterhin leer. + Die WireGuard-Evidenz bleibt der Teiltest vom 12.09.2026: CachyOS-Laptop von WAN, Handshake, Ping auf Gateway/Proxmox und direkter IPv4-Internetpfad bestanden; KI-Server war ausgeschaltet. Neue Rollen, weitere Clientvarianten, Gateway-GUI-Zugriff, WAN-/IPv6-Isolation, Neustart und Restore sind noch nicht abgenommen. GNAI-006 ist erst nach Klärung der offenen Werte, Umsetzung und dokumentierter Abnahme technisch abgeschlossen. diff --git a/docs/ai/GNAI-007-PLAN_Ansible-Deployment.md b/docs/ai/GNAI-007-PLAN_Ansible-Deployment.md index d0fcb34..fe109fc 100644 --- a/docs/ai/GNAI-007-PLAN_Ansible-Deployment.md +++ b/docs/ai/GNAI-007-PLAN_Ansible-Deployment.md @@ -28,6 +28,8 @@ | 2026-09-19 | 0.7 | spektr / Codex | minimale gemeinsame Host-Baseline mit Schutzprüfungen und getrennten Verzeichniswurzeln implementiert, angewendet und auf Idempotenz geprüft | | 2026-09-20 | 0.8 | spektr / Codex | gemeinsame Podman-/Quadlet-Runtime implementiert, installiert und ohne Images oder Container verifiziert | | 2026-09-20 | 0.9 | spektr / Codex | getrennte rootless Konten für AI Production, AI Testing und CPU-Encoding eingerichtet; Parallelitäts- und Repository-Grenzen ergänzt | +| 2026-09-20 | 0.10 | spektr / Codex | ROCm als Backend für zwei RX 6900 XT, initialen `llama.cpp`-Modellstand und Layer-Split mit gleicher GPU-Gewichtung festgehalten | +| 2026-09-20 | 0.11 | spektr / Codex | AMD-Gerätezugriff für beide rootless KI-Konten implementiert, angewendet und idempotent geprüft | --- @@ -378,6 +380,75 @@ rootverwaltet und für die jeweilige Gruppe lesbar. Der Encoding-Workload verwendet entsprechend `/etc/ghostnet-encoding` und `/var/lib/ghostnet-encoding`. +### 7.5 ROCm- und `llama.cpp`-Ausgangskonfiguration + +Der Betreiber hat zwei Sapphire Nitro+ Radeon RX 6900 XT mit jeweils 16 GiB +VRAM als Zielhardware bestätigt. Für diesen homogenen AMD-Aufbau wird ROCm als +Backend verwendet. Vulkan bleibt eine mögliche spätere Alternative, falls der +Server um GPUs mit unterschiedlichen Backend-Anforderungen erweitert wird. + +Als Ausgangspunkt dient eine bereits auf einer RX 7900 XTX verwendete +Podman-Compose-Konfiguration. Die Serverumsetzung wird daraus mit folgenden +Abweichungen als rootless Quadlet erzeugt: + +- das Image `ghcr.io/ggml-org/llama.cpp:server-rocm` wird auf eine geprüfte + Version und den zugehörigen Digest festgelegt; +- `/dev/kfd` und die tatsächlich benötigten `/dev/dri`-Geräte werden explizit + eingebunden; `GroupAdd=keep-groups` erhält die nötigen ergänzenden + Geräte-Gruppen im rootless Container; +- das gemeinsame Modellverzeichnis wird nur lesbar unter `/models` + eingebunden; +- `llama-server` erhält keinen Host-Port und ist ausschließlich für LiteLLM im + umgebungseigenen Podman-Netz erreichbar; +- systemd übernimmt Start, Neustart, Abhängigkeiten und Health-Überwachung; +- Modell- und Laufzeitwerte werden als Ansible-Variablen in die + Quadlet-Kommandozeile gerendert. Zusätzliche Container-Environment-Variablen + werden nur verwendet, wenn das Backend sie technisch benötigt; +- `HSA_OVERRIDE_GFX_VERSION=11.0.0` aus der RX-7900-XTX-Konfiguration wird + nicht übernommen. Die RX 6900 XT verwendet nativ `gfx1030`, das im + vorgesehenen ROCm-Image enthalten sein muss. Ein Override wäre nur nach + einem fehlgeschlagenen Geräte-/Backendtest zulässig und müsste für RDNA2 + gesondert begründet werden; +- `label=disable` wird nur übernommen, falls eine tatsächlich aktive + SELinux-Konfiguration dies erfordert. Für den dokumentierten Debian-Host + wird es nicht vorsorglich gesetzt. + +Der erste Testing-Modellstand verwendet vorbehaltlich Artefakt- und +Checksum-Prüfung: + +| Parameter | Ausgangswert | +| --- | --- | +| Modelldatei | `Qwen3.8-27B-UD-IQ4_XS.gguf` | +| API-Alias | `qwen3.8-27b` | +| Kontext | 51.200 Token | +| parallele Slots | 1 | +| maximale Ausgabe | 4.096 Token | +| GPU-Layer | alle | +| Flash Attention | aktiviert | +| K-/V-KV-Cache | jeweils `q8_0` | +| Reasoning Effort | `medium` | + +Samplingwerte und Chat-Template-Parameter gehören zum Modellprofil und nicht +zu allgemeinen Rollendefaults. Dadurch können andere Modelle später eigene +geprüfte Profile verwenden, ohne die gemeinsame `llama_cpp`-Rolle zu ändern. + +Für die beiden gleich großen GPUs wird zunächst +`--split-mode layer --tensor-split 1,1` festgelegt. Der Layer-Split minimiert +GPU-übergreifende Kommunikation und passt damit zur dokumentierten +PCIe-Topologie, bei der die zweite Karte über einen langsameren, am PCH +angebundenen Slot läuft. `row` wird wegen seiner Ablösung im aktuellen +`llama.cpp` nicht als neue Betriebsvariante vorgesehen. Der experimentelle +`tensor`-Split bleibt ein späterer Benchmarkkandidat; er benötigt deutlich +mehr Kommunikation zwischen den Karten und ist mit dem geplanten +quantisierten V-KV-Cache derzeit nicht kompatibel. + +Vor dem dauerhaften Start muss ein begrenzter Test `gfx1030`, beide GPUs, +Device-Reihenfolge, verfügbaren VRAM und die tatsächliche 1:1-Verteilung in +den `llama.cpp`-Startlogs bestätigen. Zusätzlich werden Prompt-Verarbeitung, +Token-Generierung, VRAM-Nutzung und Stabilität bei 51.200 Token Kontext +gemessen. Der 1:1-Wert ist erst nach diesem Test ein abgenommener +Betriebsparameter. + --- ## 8. Vorgesehener Workflow @@ -555,7 +626,8 @@ nicht als Nachweis einer erfolgreichen Einrichtung. - Podman- und Quadlet-Grundruntime sowie getrennte rootless Workload-Konten eingerichtet und geprüft; -- AMD-GPU-/ROCm- oder alternative Laufzeit nach technischer Prüfung; +- rootless Host-Gerätezugriff auf beide RX 6900 XT eingerichtet und geprüft; +- ROCm-Funktion im rootless Container prüfen; - `llama.cpp` und lokaler Funktionstest; - Wiederanlauf nach geplantem Reboot prüfen. @@ -635,6 +707,8 @@ Herstellerdokumentationen herangezogen: - [Ansible Vault](https://docs.ansible.com/projects/ansible/latest/vault_guide/vault.html) - [Debian 13 – Paket `podman`](https://packages.debian.org/trixie/podman) - [Podman – Quadlet und systemd](https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html) +- [llama.cpp – Multi-GPU](https://github.com/ggml-org/llama.cpp/blob/master/docs/multi-gpu.md) +- [llama.cpp – ROCm-Container](https://github.com/ggml-org/llama.cpp/blob/master/.devops/rocm.Dockerfile) Vor der Implementierung sind versionsspezifische Aussagen gegen die dann tatsächlich eingesetzten Gitea-, Runner- und Ansible-Versionen zu prüfen. @@ -753,25 +827,43 @@ die umgebungsspezifischen Pfade wurden daraufhin vollständig an systemd-User-Manager sowie drei getrennte rootless Podman Graph Roots. Es wurden keine Images oder Workload-Container angelegt. +Anschließend wurde `amd_gpu_access` in das gemeinsame Host-Playbook +aufgenommen. Die Rolle verlangt genau zwei Geräte `1002:73bf` mit `amdgpu`, +ordnet über sysfs ausschließlich deren Render-Nodes zu, prüft `/dev/kfd` und +leitet die minimal erforderlichen Gerätegruppen aus dem Istzustand ab. Auf dem +Host sind dies `/dev/kfd`, `/dev/dri/renderD128` und +`/dev/dri/renderD129`, jeweils mit Gruppe `render`. + +Der Check-Modus prognostizierte nur die ergänzende Gruppenmitgliedschaft für +`gnai-production` und `gnai-testing`. Der reale Lauf bestätigte danach für +beide Konten Lese- und Schreibzugriff auf alle drei Geräte. Ein erster +Wiederholungslauf deckte auf, dass `workload_accounts` die Gruppe zunächst +entfernte und `amd_gpu_access` sie wieder hinzufügte. `render` wurde deshalb in +den expliziten Sollzustand der beiden KI-Konten übernommen; das Encoding-Konto +bleibt ohne GPU-Gruppe. Der korrigierte Gesamtlauf endete mit +`ok=55 changed=0 unreachable=0 failed=0`. Weder ein ROCm-Image noch ein +Container oder eine Inferenz wurden dabei ausgeführt. + --- ## 15. Abschlussgrenze dieses Entwurfs -Mit Version 0.9 sind der aktuelle Planungsstand, der ausdrücklich begrenzte +Mit Version 0.11 sind der aktuelle Planungsstand, der ausdrücklich begrenzte Ansible-Preflight, der verifizierte Deployment-Account und die angewendete minimale Host-Baseline, die Podman-/Quadlet-Grundruntime und die getrennten -Workload-Konten persistiert. +Workload-Konten sowie deren begrenzter AMD-Gerätezugriff persistiert. Insbesondere wurden noch nicht: - ein Gitea Runner angelegt, - Gitea Actions oder Schutzregeln konfiguriert, -- GPU- oder Anwendungsrollen, ein Production-Inventar oder ein Vault erstellt, +- eine ROCm-Container- oder Anwendungsrolle, ein Production-Inventar oder ein + Vault erstellt, - eine Production- oder Testing-Umgebung eingerichtet oder voneinander abgenommen, -- GPU-Komponenten oder Anwendungen auf dem KI-Server installiert, +- ROCm-Userspace, GPU-Container oder Anwendungen auf dem KI-Server installiert, - Backups oder Rollbacks praktisch getestet oder - die geplante Deploymentkette technisch abgenommen. -Die nächsten technischen Schritte sind der begrenzte GPU-Zugriffstest für die -beiden KI-Konten und die Festlegung von Zeitfenster sowie Ressourcenlimits im -getrennten Encoding-Plan. Darauf bauen die jeweiligen Fachrollen und SOPs auf. +Der nächste technische Schritt ist ein begrenzter rootless ROCm-Container-Test +für beide KI-Konten, der beide Karten als `gfx1030` erkennen muss. Darauf bauen +die `llama.cpp`-Rolle, das initiale Testing-Modellprofil und später LiteLLM auf. diff --git a/docs/platform/GNAI-002-INFRA_Users.md b/docs/platform/GNAI-002-INFRA_Users.md index f03424c..df7ad76 100644 --- a/docs/platform/GNAI-002-INFRA_Users.md +++ b/docs/platform/GNAI-002-INFRA_Users.md @@ -16,6 +16,7 @@ | 2026-09-18 | 1.3 | spektr | Workstation-spezifische Schlüsselpfade und Host-Fingerprint in den lokalen Arbeitskontext verlagert. | | 2026-09-19 | 1.4 | spektr | Persönlichen Login `ghost` und Agentenlogin `ghost-agent` als erfolgreich getestet dokumentiert. | | 2026-09-20 | 1.5 | spektr / Codex | Deployment- und getrennte rootless Workload-Konten mit festen IDs und Zuständigkeiten ergänzt. | +| 2026-09-20 | 1.6 | spektr / Codex | `render`-Mitgliedschaft und geprüfte AMD-Geräterechte der beiden KI-Konten ergänzt. | --- @@ -74,7 +75,8 @@ Die drei Workload-Konten besitzen nicht überlappende SubUID-/SubGID-Bereiche, eigene Daten- und Podman-Stores sowie persistente systemd-User-Manager. Ihre rootverwalteten Quadlet-Verzeichnisse liegen unter `/etc/containers/systemd/users/`. Direkte SSH-Schlüssel werden dort nicht -hinterlegt. +hinterlegt. `gnai-production` und `gnai-testing` gehören zusätzlich zur +Gerätegruppe `render`; `gn-encoding` erhält keinen GPU-Zugriff. --- @@ -188,6 +190,7 @@ sudo systemctl restart ssh * [ ] **Benutzerprüfung:** `ghost` und `debugofen` können ihre vorgesehenen sudo-Rechte nutzen; `ghost-agent` bleibt ohne sudo-Zuweisung. * [x] **Workload-Konten:** `gnai-production`, `gnai-testing` und `gn-encoding` besitzen getrennte rootless Podman-Stores und aktive persistente User-Manager; geprüft am 20.09.2026. +* [x] **GPU-Geräterechte:** Beide KI-Konten können `/dev/kfd`, `/dev/dri/renderD128` und `/dev/dri/renderD129` lesen und schreiben; das Encoding-Konto gehört nicht zu `render`; geprüft am 20.09.2026. * [ ] **Netzwerk:** Server ist unter der statischen IP `10.0.0.90` erreichbar. Ausgehender Datenverkehr funktioniert (`ping google.com`). * [x] **SSH-Aliase:** `ghostnet-ai-ghost` meldet `ghost`, `ghostnet-ai-agent` meldet `ghost-agent`. Beide Zugänge wurden am 19.09.2026 bestätigt. Die Passphrase des persönlichen Schlüssels ist keine Server-Passwortanmeldung. * [ ] **Root-Sperre:** Direct-Login als Root per SSH wird abgelehnt (`ssh root@10.0.0.90` -> *Permission denied*).