diff --git a/ansible/group_vars/phy_srv_gpu01.yml b/ansible/group_vars/phy_srv_gpu01.yml index cd21505..59cfde5 100644 --- a/ansible/group_vars/phy_srv_gpu01.yml +++ b/ansible/group_vars/phy_srv_gpu01.yml @@ -1,2 +1,41 @@ --- +# overrides of group_vars/all.yml +hostname: phy-srv-gpu01 + +# geerlingguy.docker +docker_users: + - "{{ main_username }}" + +# ironicbadger.docker_compose_generator +# services//NN-/compose.yml next to run.yml +appdata_path: "/home/{{ main_username }}/appdata" +docker_compose_generator_output_path: "/home/{{ main_username }}/docker/" +docker_compose_hostname: phy-srv-gpu01 +docker_compose_generator_uid: "{{ main_uid }}" +docker_compose_generator_gid: "{{ main_gid }}" + +# --- LLM stack ------------------------------------------------------------- +llm_dns_name: chat.phytron.local + +# ⚠️ TODO: pin to a build verified on SM120 (Blackwell) before the first deploy +vllm_image: vllm/vllm-openai:latest +vllm_model: Qwen/Qwen3-32B-FP8 +vllm_served_model_name: qwen3-32b +vllm_gpu_memory_utilization: "0.80" +vllm_max_model_len: "32768" + +embedding_model: BAAI/bge-m3 +embedding_model_name: bge-m3 +embeddings_gpu_memory_utilization: "0.10" + +openwebui_image: ghcr.io/open-webui/open-webui:main +pgvector_image: pgvector/pgvector:pg16 + +# --- AD/LDAP --------------------------------------------------------------- +# ⚠️ TODO: real bind DN / base DN from the customer; group llm_users is agreed +ldap_server_host: dc.phytron.local +ldap_server_port: "389" +ldap_bind_dn: "CN=svc-llm,OU=Service,DC=phytron,DC=local" +ldap_search_base: "DC=phytron,DC=local" +ldap_search_filter: "(&(objectClass=user)(memberOf=CN=llm_users,OU=Groups,DC=phytron,DC=local))" diff --git a/ansible/requirements.yml b/ansible/requirements.yml index 5a4c875..082c7de 100644 --- a/ansible/requirements.yml +++ b/ansible/requirements.yml @@ -1,4 +1,7 @@ --- +# docker_compose_generator MUST stay pinned — its galaxy tags mix formats +# (1.0.x uses a `containers:` data structure, 2.x uses native compose files) +# and an unpinned install can silently change the expected layout. roles: #- name: geerlingguy.pip - name: geerlingguy.docker @@ -6,3 +9,4 @@ roles: - name: geerlingguy.security - name: geerlingguy.ntp - name: ironicbadger.docker_compose_generator + version: 2.0.1 diff --git a/ansible/run.yml b/ansible/run.yml index b13e903..97d5db5 100644 --- a/ansible/run.yml +++ b/ansible/run.yml @@ -88,3 +88,8 @@ tags: docker - role: nvidia_gpu tags: gpu + # renders services/phy-srv-gpu01/*/compose.yml into ~/docker/compose.yaml. + # It only WRITES the file — starting the stack stays a deliberate manual + # step (see ansible/services/phy-srv-gpu01/README.md). + - role: ironicbadger.docker_compose_generator + tags: compose diff --git a/ansible/services/phy-srv-gpu01/01-vllm/compose.yml b/ansible/services/phy-srv-gpu01/01-vllm/compose.yml new file mode 100644 index 0000000..a8c721f --- /dev/null +++ b/ansible/services/phy-srv-gpu01/01-vllm/compose.yml @@ -0,0 +1,36 @@ +services: + vllm: + image: "{{ vllm_image }}" + container_name: vllm + networks: + - llmnet + ports: + - "8000:8000" + volumes: + # model cache on local disk — tens of GB per model + - "{{ appdata_path }}/models/huggingface:/root/.cache/huggingface" + environment: + - NVIDIA_VISIBLE_DEVICES=all + - NVIDIA_DRIVER_CAPABILITIES=compute,utility + - "HUGGING_FACE_HUB_TOKEN={{ hf_token | default('') }}" + command: + - --model + - "{{ vllm_model }}" + - --served-model-name + - "{{ vllm_served_model_name }}" + # fixed VRAM share so the embedding server keeps its slice (§0: "it just works") + - --gpu-memory-utilization + - "{{ vllm_gpu_memory_utilization }}" + - --max-model-len + - "{{ vllm_max_model_len }}" + # vLLM needs a large shared-memory segment; without this it dies on startup + ipc: host + deploy: + resources: + reservations: + devices: + - driver: nvidia + count: 1 + capabilities: [gpu] + runtime: nvidia + restart: unless-stopped diff --git a/ansible/services/phy-srv-gpu01/02-embeddings/compose.yml b/ansible/services/phy-srv-gpu01/02-embeddings/compose.yml new file mode 100644 index 0000000..7556364 --- /dev/null +++ b/ansible/services/phy-srv-gpu01/02-embeddings/compose.yml @@ -0,0 +1,37 @@ +services: + # Embeddings run on a second vLLM instance rather than a separate toolchain + # (e.g. text-embeddings-inference): whatever vLLM build works on SM120 then + # covers embeddings too, instead of having to solve Blackwell support twice. + embeddings: + image: "{{ vllm_image }}" + container_name: embeddings + networks: + - llmnet + ports: + - "8001:8000" + volumes: + - "{{ appdata_path }}/models/huggingface:/root/.cache/huggingface" + environment: + - NVIDIA_VISIBLE_DEVICES=all + - NVIDIA_DRIVER_CAPABILITIES=compute,utility + - "HUGGING_FACE_HUB_TOKEN={{ hf_token | default('') }}" + command: + - --model + - "{{ embedding_model }}" + - --served-model-name + - "{{ embedding_model_name }}" + - --task + - embed + # small fixed slice — the chat model gets the rest + - --gpu-memory-utilization + - "{{ embeddings_gpu_memory_utilization }}" + ipc: host + deploy: + resources: + reservations: + devices: + - driver: nvidia + count: 1 + capabilities: [gpu] + runtime: nvidia + restart: unless-stopped diff --git a/ansible/services/phy-srv-gpu01/03-openwebui/compose.yml b/ansible/services/phy-srv-gpu01/03-openwebui/compose.yml new file mode 100644 index 0000000..d19ce47 --- /dev/null +++ b/ansible/services/phy-srv-gpu01/03-openwebui/compose.yml @@ -0,0 +1,62 @@ +services: + openwebui: + image: "{{ openwebui_image }}" + container_name: openwebui + networks: + - llmnet + ports: + # plain HTTP for now — no TLS yet (projektplan §2.2). Put a reverse proxy + # in front (new stack 04-proxy) once an internal CA certificate exists. + - "80:8080" + depends_on: + - vllm + - owui-db + volumes: + - "{{ appdata_path }}/apps/open-webui:/app/backend/data" + environment: + - "WEBUI_URL=http://{{ llm_dns_name }}" + - "WEBUI_SECRET_KEY={{ vault_owui_secret_key }}" + + # --- inference: vLLM speaks the OpenAI API, Ollama stays off --- + - OPENAI_API_BASE_URL=http://vllm:8000/v1 + - OPENAI_API_KEY=dummy + - ENABLE_OLLAMA_API=false + + # --- vector store: pgvector rather than the default chroma, which does + # not scale to the corpus sizes discussed in §2.6 --- + - VECTOR_DB=pgvector + - "PGVECTOR_DB_URL=postgresql://openwebui:{{ vault_owui_db_password }}@owui-db:5432/openwebui" + + # --- RAG embeddings from the second vLLM instance --- + - RAG_EMBEDDING_ENGINE=openai + - RAG_OPENAI_API_BASE_URL=http://embeddings:8000/v1 + - RAG_OPENAI_API_KEY=dummy + - "RAG_EMBEDDING_MODEL={{ embedding_model_name }}" + + # --- AD/LDAP login (group llm_users) --- + # NOTE: verify these variable names against the Open WebUI docs for the + # tag actually deployed — they have changed between releases. + - ENABLE_LDAP=true + - LDAP_SERVER_LABEL=Phytron AD + - "LDAP_SERVER_HOST={{ ldap_server_host }}" + - "LDAP_SERVER_PORT={{ ldap_server_port }}" + - "LDAP_APP_DN={{ ldap_bind_dn }}" + - "LDAP_APP_PASSWORD={{ vault_ldap_bind_password }}" + - "LDAP_SEARCH_BASE={{ ldap_search_base }}" + - "LDAP_SEARCH_FILTER={{ ldap_search_filter }}" + - LDAP_ATTRIBUTE_FOR_USERNAME=sAMAccountName + - LDAP_USE_TLS=false + restart: unless-stopped + + owui-db: + image: "{{ pgvector_image }}" + container_name: owui-db + networks: + - llmnet + volumes: + - "{{ appdata_path }}/databases/openwebui-pgdata:/var/lib/postgresql/data" + environment: + - POSTGRES_DB=openwebui + - POSTGRES_USER=openwebui + - "POSTGRES_PASSWORD={{ vault_owui_db_password }}" + restart: unless-stopped diff --git a/ansible/services/phy-srv-gpu01/99-network/compose.yml b/ansible/services/phy-srv-gpu01/99-network/compose.yml new file mode 100644 index 0000000..ad95f1f --- /dev/null +++ b/ansible/services/phy-srv-gpu01/99-network/compose.yml @@ -0,0 +1,7 @@ +# network definition (leading comment required: the compose generator +# indents the first line of each fragment; networks: must stay top-level) +networks: + llmnet: + driver: bridge + driver_opts: + com.docker.network.bridge.name: br-llm diff --git a/ansible/services/phy-srv-gpu01/README.md b/ansible/services/phy-srv-gpu01/README.md new file mode 100644 index 0000000..3ee8406 --- /dev/null +++ b/ansible/services/phy-srv-gpu01/README.md @@ -0,0 +1,39 @@ +# Compose stacks for phy-srv-gpu01 + +Rendered by `ironicbadger.docker_compose_generator` (pinned to 2.0.1) into +`{{ docker_compose_generator_output_path }}/compose.yaml` on the host. +The fragments here are **Jinja2 templates**, not plain compose files — Ansible +variables are substituted at render time. + +| Stack | Contains | +| --- | --- | +| `01-vllm` | vLLM, serves the chat model on port 8000 (OpenAI API) | +| `02-embeddings` | second vLLM instance, embedding model on port 8001 | +| `03-openwebui` | Open WebUI (port 80) + pgvector Postgres | +| `99-network` | the shared `llmnet` bridge — must sort last | + +Ordering comes from the `NN-` prefix; `99-network` keeps `networks:` at the +bottom of the generated file. The leading comment in `99-network/compose.yml` +is load-bearing: the role indents only the first line of each fragment, and the +comment absorbs that indentation so `networks:` stays top-level. + +## Deploy + +```sh +just compose phy_srv_gpu01 # renders ~/docker/compose.yaml on the host +``` + +The role only **writes** the file. Bringing the stack up is deliberate: + +```sh +ssh sbxadmin@192.168.66.69 'cd ~/docker && docker compose up -d' +``` + +## Before the first run + +- Secrets must exist in `group_vars/secrets.yml` (`just vault edit`): + `vault_owui_secret_key`, `vault_owui_db_password`, `vault_ldap_bind_password` +- LDAP parameters in `group_vars/phy_srv_gpu01.yml` need the real bind DN and + base DN from the customer +- `vllm_image` must point at a build that works on SM120 (Blackwell) — verify + before deploying, the official image is not guaranteed to work diff --git a/server/phy-srv-gpu01/notes/20260710-projektplan.md b/server/phy-srv-gpu01/notes/20260710-projektplan.md index 40b3684..24b5462 100644 --- a/server/phy-srv-gpu01/notes/20260710-projektplan.md +++ b/server/phy-srv-gpu01/notes/20260710-projektplan.md @@ -93,24 +93,33 @@ GPU-Server nötig). Zu erheben: Ergebnis bestimmt: Korpus-Umfang (→ §1-Trigger), OCR-Pipeline ja/nein, Index-Größe (→ Storage-Check §2.3), Erst-Indexierungsdauer. -**Ergebnis (Lauf 2026-07-14, Skript `scripts/share-analysis.ps1` auf `D:` des -Fileservers Z-FILESERVER):** +**Ergebnis (Lauf 2026-09-03, Skript `scripts/share-analysis.ps1` auf `D:` des +Fileservers Z-FILESERVER; Zahlen ggü. dem Erstlauf 2026-07-14 leicht gewachsen):** | Kennzahl | Wert | Konsequenz | |---|---|---| -| Gesamt | **3.322.175 Dateien, 2.224 GB** | Scan lief über `D:` gesamt, nicht je Share → Obergrenze, enthält evtl. nicht freigegebene Daten | -| `office` (extrahierbar) | **1.258.642 Dateien, 587 GB** | **12× über der §1-Schwelle** → Eingrenzung zwingend (§2.6) | -| `other` (unklassifiziert) | 1.575.752 Dateien, 934 GB | größter Block, **noch unidentifiziert** → `D-file-types.csv` auswerten | +| Gesamt | **3.338.643 Dateien, 2.231 GB** | Scan lief über `D:` gesamt, nicht je Share → Obergrenze, enthält evtl. nicht freigegebene Daten | +| `office` (extrahierbar) | **1.271.467 Dateien, 591 GB** | **12× über der §1-Schwelle** → Eingrenzung zwingend (§2.6) | +| `other` (unklassifiziert) | 1.576.747 Dateien, 919 GB | größter Block, **noch unidentifiziert** → `D-file-types.csv` auswerten | | Bilder / CAD / Archive / Medien | 403k / 60k / 12,6k / 6,2k Dateien | nicht extrahierbar → Erwartungsmanagement | -| PDFs | **924.076**, davon ~**18 % ohne Textlayer** (Stichprobe 200) | ≈ **166.000 Scan-PDFs → OCR**; eigener Zeit-/GPU-Aufwand, konkurriert mit vLLM | +| PDFs | **935.186**, davon ~**22 % ohne Textlayer** (Stichprobe 200) | ≈ **200.000 Scan-PDFs → OCR**; eigener Zeit-/GPU-Aufwand, konkurriert mit vLLM | | Änderungsrate | 94 % älter als 1 Jahr; nur ~158k in den letzten 365 Tagen | Sync-Frequenz unkritisch; **Alter ≠ Irrelevanz** (Datenblätter, Normen) → Filter primär ordner-/inhaltsbasiert, Aktualität nur sekundär | -| Duplikat-/Altversions-Muster | 33.179 Dateien | Filterregeln | -| Pfade > 240 Zeichen | 25.169 | Extraktions-Toolchain gezielt testen | -| Nicht-ASCII-Dateinamen | 126.111 | UTF-8 durchgängig (CIFS-Mount-Optionen → Extraktion → Index) | +| Duplikat-/Altversions-Muster | 33.378 Dateien | Filterregeln | +| Pfade > 240 Zeichen | 25.866 | Extraktions-Toolchain gezielt testen | +| Nicht-ASCII-Dateinamen | 126.899 | UTF-8 durchgängig (CIFS-Mount-Optionen → Extraktion → Index) | -**Noch auszuwerten** (CSVs liegen auf dem Fileserver, noch nicht gesichtet): -`D-toplevel-folders.csv` (Größe/Anzahl je Top-Level-Ordner — die Grundlage für -die Include-/Exclude-Liste) und `D-file-types.csv` (Auflösung der `other`-Kategorie). +**Bug im Erstlauf:** Beim Lauf über `D:` wurden **keine CSVs geschrieben** +(`Export-Csv` scheiterte an `D:` im Dateinamen). Behoben am 2026-09-03; für die +Ordner-Aufschlüsselung neu laufen lassen mit: + +```powershell +.\share-analysis.ps1 -Paths "D:" -OutDir .\file-analysis -FolderDepth 2 +``` + +`-FolderDepth 2` liefert Größe/Anzahl je `Abteilungen\` — genau die +Granularität der Freigaben (s. §2.7) und die Grundlage für die Include-/ +Exclude-Liste. Ebenfalls auszuwerten: `D-file-types.csv` zur Auflösung der +`other`-Kategorie (1,58 Mio. Dateien, größter unidentifizierter Block). ### 2.2 Mit dem Kunden zu klären (Checkliste) — Stand 2026-09-03 @@ -206,6 +215,36 @@ lautete sinngemäß „unsere Dokumente auslesen". Realistisch ist „ein kurati Teil unserer Dokumente, dafür mit guten Antworten". Das früh sagen — nicht erst, wenn der Vollindex schlechte Treffer liefert. +### 2.7 Freigaben-Inventar (Stand 2026-09-03) + +`scripts/list-shares.ps1` auf Z-FILESERVER: **30 Freigaben, alle unterhalb `D:`**, +kein DFS. Vorschlag als *Gesprächsgrundlage* — die Zuordnung der Kürzel muss der +Kunde bestätigen, ebenso die endgültige Auswahl: + +| Freigabe | Pfad | Vorschlag | Begründung | +|---|---|---|---| +| QM | `D:\Abteilungen\QM` | **rein** | Qualitätsmanagement — Normen, Prüfvorschriften: Kern der Wissensbasis | +| Arbeitssicherheit | `D:\Arbeitssicherheit` | **rein** | Vorschriften, Betriebsanweisungen | +| Competence Center | `D:\Abteilungen\Competence Center` | **rein** | Name deutet auf kuratiertes Wissen | +| EDV | `D:\Abteilungen\EDV` | **rein** | IT-Doku | +| Produktion | `D:\Produktion` | **prüfen** | vermutlich groß; ggf. nur Unterordner mit Doku | +| Projekte | `D:\Projekte` | **prüfen** | vermutlich der größte Block; nur mit Unterordner-Filter sinnvoll | +| Lagerlogistik, Marketing, Kalkulationen, Controlling, Immobilie | div. | **prüfen** | fachlich abhängig, eher zweite Welle | +| AA, AZ, BU, EE, EF, EK, ME, MF, MM, PL, SG, VT | `D:\Abteilungen\*` | **klären** | Abteilungskürzel — Bedeutung und Inhalt vom Kunden bestätigen lassen | +| Zeichnungen | `D:\Abteilungen\Zeichnungen` | **raus** | CAD/Zeichnungen — nicht extrahierbar (s. `cad`/`image` in §2.1) | +| Datenbank | `D:\Datenbank` | **raus** | Datenbank-Dateien, kein Fließtext | +| Tempwork | `D:\Tempwork` | **raus** | temporär, per Definition kein Wissensbestand | +| Benutzer | `D:\Benutzer` | **raus** | persönliche Ablagen — Datenschutz, kein geteiltes Wissen | +| **Personal** | `D:\Abteilungen\Personal` | **raus** | **HR — explizit ausgeschlossen** (§0: „alle sehen alles" gilt nur ohne Personaldaten) | +| **GF** | `D:\Abteilungen\GF` | **raus** | Geschäftsführung — vertraulich | +| **BR** | `D:\Abteilungen\BR` | **raus (klären)** | falls Betriebsrat: besonders schutzwürdig, auf keinen Fall indizieren | + +⚠️ **Wichtig:** Weil alle Nutzer alles sehen (§0), ist jede indizierte Freigabe für +alle 25 Nutzer lesbar — unabhängig von den NTFS-Rechten auf dem Fileserver. Die +Ausschlüsse oben sind deshalb keine Kosmetik, sondern die einzige Zugriffskontrolle. +`list-shares.ps1` gibt die NTFS-Gruppen je Freigabe mit aus: Alles, was heute nur +einem eingeschränkten Personenkreis offensteht, gehört **nicht** in den Index. + ## 3. Phasen ab Server-Lieferung | Phase | Inhalt | Ergebnis/Abnahme |