diff --git a/deployment/poweron_sec.kdbx b/deployment/poweron_sec.kdbx index 5bd02d6..a5c6177 100644 Binary files a/deployment/poweron_sec.kdbx and b/deployment/poweron_sec.kdbx differ diff --git a/test-local-vision/analysis/buy-spec.md b/test-local-vision/analysis/buy-spec.md new file mode 100644 index 0000000..4174116 --- /dev/null +++ b/test-local-vision/analysis/buy-spec.md @@ -0,0 +1,287 @@ +# Hardware-Spezifikation: KI-Inferenz-Server für Qwen2.5-VL-72B + +**Projekt:** On-Premise GPU-Server für Vision-Language-Inferenz +**Datum:** 01.02.2026 +**Modell:** Qwen2.5-VL-72B-Instruct +**Hinweis:** DeepSeek V3.1 läuft bereits lokal auf Notebook — nicht Gegenstand dieser Spezifikation + +--- + +## 1. Modell-Profil: Qwen2.5-VL-72B + +| Eigenschaft | Wert | +|---|---| +| Parameter (Language Decoder) | 72 Milliarden | +| Vision Encoder (ViT) | ~675 Millionen | +| Kontextfenster | 128K Tokens | +| Bildverarbeitung | Dynamische Auflösung, variable Token-Anzahl pro Bild | +| Video-Support | Ja (Frame-Sampling mit temporaler Kodierung) | +| Architektur-Features | Window Attention (ViT), SwiGLU, RMSNorm, mRoPE | + +### VRAM-Bedarf der Modellgewichte + +| Präzision | Gewichte | Empfohlener VRAM gesamt (inkl. KV-Cache, Aktivierungen) | +|---|---|---| +| BF16 (volle Präzision) | ~144 GB | 192–384 GB | +| FP8 (quantisiert) | ~72 GB | 120–192 GB | +| INT4 / AWQ | ~36 GB | 80–120 GB | + +### Warum Vision-Modelle mehr VRAM brauchen als Text-LLMs + +Bei reinen Text-Modellen ist der VRAM-Bedarf relativ vorhersehbar. Bei Vision-Language-Modellen wie Qwen2.5-VL kommt ein **dynamischer Zusatzbedarf** hinzu: + +- Jedes Eingabebild wird in eine variable Anzahl visueller Tokens umgewandelt (standardmässig 256–16.384 Tokens pro Bild, steuerbar über `min_pixels` / `max_pixels`). +- Hochauflösende Bilder oder mehrere Bilder pro Anfrage können den VRAM-Verbrauch sprunghaft erhöhen. +- Der Vision-Encoder selbst ist mit ~675M Parametern klein, aber die erzeugten visuellen Tokens vergrössern den KV-Cache erheblich. +- Video-Inputs erzeugen besonders viele Tokens und können den VRAM um ein Vielfaches steigern. + +**Praxis-Implikation:** Für Qwen2.5-VL-72B muss deutlich mehr VRAM-Headroom eingeplant werden als für ein reines 72B-Textmodell. + +--- + +## 2. GPU-Konfiguration + +### Option A — Empfohlen: 4× NVIDIA RTX 6000 Ada (48 GB) + +| Komponente | Spezifikation | +|---|---| +| **GPU** | **4× NVIDIA RTX 6000 Ada Generation (48 GB GDDR6 ECC)** | +| VRAM gesamt | 192 GB | +| Speicherbandbreite pro GPU | 960 GB/s | +| FP16 Tensor Performance | 1.457 TFLOPS (mit Sparsity) | +| Architektur | Ada Lovelace (4. Gen. Tensor Cores) | +| TDP pro GPU | 300W | +| Interconnect | PCIe Gen 4 ×16 | + +**Begründung:** + +- 192 GB Gesamt-VRAM reicht für Qwen2.5-VL-72B in BF16 (~144 GB Gewichte) mit ausreichend Headroom für KV-Cache und Bildverarbeitung. +- Benchmarks zeigen ~450 Tokens/s Durchsatz für 72B-Modelle auf 4× A6000/RTX 6000 Ada mit vLLM — das ist für die meisten Produktiv-Szenarien schnell genug. +- Die RTX 6000 Ada übertrifft die ältere A6000 bei Inferenz um ~28% dank neuerer Tensor Cores und höherer Bandbreite. +- Preis-Leistung ist bei 48-GB-Workstation-GPUs deutlich besser als bei Datacenter-GPUs (A100/H100). +- 4 GPUs passen in einen Standard-4U-Server ohne exotische Kühlung. + +**Deployment-Konfiguration:** + +```bash +# vLLM mit Tensor-Parallelismus über 4 GPUs +vllm serve Qwen/Qwen2.5-VL-72B-Instruct \ + --host 0.0.0.0 \ + --port 8000 \ + --tensor-parallel-size 4 \ + --mm-encoder-tp-mode data \ + --gpu-memory-utilization 0.95 \ + --max-model-len 65536 \ + --limit-mm-per-prompt '{"image":4,"video":0}' +``` + +**Wichtig:** `--mm-encoder-tp-mode data` ist essenziell — der Vision-Encoder wird im Data-Parallel-Modus betrieben, da er im Vergleich zum 72B-Decoder winzig ist und Tensor-Parallelismus auf dem ViT mehr Kommunikations-Overhead als Gewinn bringt. + +### Option B — Performance-Upgrade: 4× NVIDIA L40S (48 GB) + +| Komponente | Spezifikation | +|---|---| +| **GPU** | **4× NVIDIA L40S (48 GB GDDR6 ECC)** | +| VRAM gesamt | 192 GB | +| Speicherbandbreite pro GPU | 864 GB/s | +| FP16 Tensor Performance | 1.466 TFLOPS (mit Sparsity) | +| Architektur | Ada Lovelace (Datacenter-Variante) | +| TDP pro GPU | 350W | +| Interconnect | PCIe Gen 4 ×16 | + +**Begründung:** + +- Gleicher VRAM wie RTX 6000 Ada, aber als Datacenter-GPU mit besserem Dauerbetriebs-Support, ECC-Speicher und Fernwartung. +- Etwas höhere TDP (350W vs. 300W) ermöglicht höhere Sustained Performance. +- Verfügbar in validierten Server-Plattformen (Supermicro, Dell, Lenovo). +- Preislich zwischen RTX 6000 Ada und A100 positioniert. + +### Option C — Maximale Qualität: 2× NVIDIA A100 SXM 80 GB + +| Komponente | Spezifikation | +|---|---| +| **GPU** | **2× NVIDIA A100 SXM (80 GB HBM2e)** | +| VRAM gesamt | 160 GB | +| Speicherbandbreite pro GPU | 2.039 GB/s | +| FP16 Tensor Performance | 624 TFLOPS (mit Sparsity) | +| Architektur | Ampere | +| TDP pro GPU | 400W | +| Interconnect | NVLink 3.0 (600 GB/s bidirektional) | + +**Begründung:** + +- HBM2e bietet doppelt so hohe Speicherbandbreite wie GDDR6 — das beschleunigt die autoregressive Token-Generierung massiv. +- NVLink ermöglicht schnellere Inter-GPU-Kommunikation als PCIe. +- **Achtung:** 160 GB Gesamt-VRAM ist knapp. Modellgewichte in BF16 (~144 GB) lassen nur ~16 GB für KV-Cache. Bei grossen Bildern oder Batching wird es eng. Empfohlen nur mit FP8-Quantisierung (~72 GB Gewichte → ~88 GB für KV-Cache). +- Höherer Preis und eingeschränkte Verfügbarkeit gegenüber RTX 6000 Ada / L40S. + +### GPU-Vergleich auf einen Blick + +| Kriterium | 4× RTX 6000 Ada | 4× L40S | 2× A100 80GB | +|---|---|---|---| +| VRAM gesamt | 192 GB | 192 GB | 160 GB | +| Bandbreite gesamt | 3.840 GB/s | 3.456 GB/s | 4.078 GB/s | +| BF16 ohne Quantisierung | ✅ Ja | ✅ Ja | ⚠️ Knapp | +| KV-Cache Headroom | ~48 GB | ~48 GB | ~16 GB (BF16) | +| Interconnect | PCIe | PCIe | NVLink | +| Dauerbetrieb im Rack | ⚠️ Workstation-GPU | ✅ Datacenter-GPU | ✅ Datacenter-GPU | +| Stromverbrauch | ~1.200W | ~1.400W | ~800W | +| GPU-Kosten (ca.) | 20.000–28.000 € | 28.000–36.000 € | 30.000–40.000 € | +| **Empfehlung** | **Bestes Preis-Leistungs-Verhältnis** | **Bester Kompromiss** | Höchste Bandbreite pro GPU | + +--- + +## 3. Gesamtsystem-Spezifikation (basierend auf Option A/B) + +### Server-Plattform + +| Komponente | Spezifikation | +|---|---| +| Formfaktor | 4U Rackmount | +| Plattform-Beispiele | Supermicro SYS-421GE-TNRT, Dell PowerEdge R760xa, Lenovo ThinkSystem SR675 V3 | +| GPU-Slots | 4× PCIe Gen 4/5 ×16 (Double-Width) | + +### CPU + +| Komponente | Spezifikation | +|---|---| +| Prozessor | 1× AMD EPYC 9354 (32 Cores, 3.25 GHz) oder Intel Xeon w5-3435X | +| PCIe-Lanes | Mind. 64 Lanes PCIe Gen 4/5 (16 pro GPU) | +| Hinweis | CPU ist nicht der Flaschenhals — wichtig sind genügend PCIe-Lanes | + +### Arbeitsspeicher (RAM) + +| Komponente | Spezifikation | +|---|---| +| Kapazität | 256 GB DDR5-4800 ECC RDIMM | +| Konfiguration | 8× 32 GB DIMMs, alle Kanäle belegt | +| Begründung | Modell wird beim Start in RAM geladen (~144 GB), bevor es auf GPUs verteilt wird | + +### Storage + +| Komponente | Spezifikation | +|---|---| +| Primär (Modell) | 1× 2 TB NVMe PCIe Gen 4 SSD | +| Sekundär (OS/Logs) | 1× 500 GB NVMe SSD | +| Begründung | Qwen2.5-VL-72B in BF16 ≈ 144 GB, plus FP8- und AWQ-Varianten, Tokenizer, Config | + +### Netzwerk + +| Komponente | Spezifikation | +|---|---| +| Primär (API) | 2× 10/25 GbE (Bonding, Redundanz) | +| Management | 1× 1 GbE IPMI/BMC | +| Hinweis | Für Bildübertragung an die API genügt 10 GbE, bei Video-Workloads 25 GbE empfohlen | + +### Stromversorgung + +| Komponente | Spezifikation | +|---|---| +| Netzteil | 2× redundant, mind. 2.400W gesamt | +| Geschätzte TDP | ~1.800–2.200W unter Volllast (4× GPU + System) | +| Rack-Anschluss | 1× 16A/230V CEE oder 2× C19/C20 | + +--- + +## 4. Software-Stack + +### Betriebssystem und Treiber + +| Komponente | Empfehlung | +|---|---| +| OS | Ubuntu 22.04 LTS Server | +| NVIDIA-Treiber | 550+ (Data Center Driver Branch) | +| CUDA | 12.4+ | +| Container | Docker + NVIDIA Container Toolkit | + +### Inferenz-Framework + +| Framework | Eignung für Qwen2.5-VL | Hinweis | +|---|---|---| +| **vLLM** (empfohlen) | ✅ Voller VLM-Support | Offiziell dokumentiertes Setup, Tensor- und Data-Parallelismus, OpenAI-kompatible API | +| SGLang | ✅ Unterstützt | Gute Performance, weniger dokumentiert für VLMs | +| TensorRT-LLM | ⚠️ Eingeschränkt | VLM-Support im Aufbau, beste Performance wenn verfügbar | + +### Optimierungs-Parameter + +| Parameter | Empfehlung | Wirkung | +|---|---|---| +| `--max-model-len` | 65536 (statt 128K default) | Reduziert KV-Cache-Reservierung erheblich | +| `--gpu-memory-utilization` | 0.95 | Nutzt fast den gesamten VRAM | +| `--limit-mm-per-prompt` | `{"image":4,"video":0}` | Begrenzt Bilder pro Anfrage, verhindert VRAM-Spikes | +| `min_pixels` / `max_pixels` | `256×28×28` / `1280×28×28` | Im Processor setzen — grösster Hebel für VRAM-Einsparung | +| Flash Attention 2 | Aktivieren | Reduziert VRAM für Attention signifikant, besonders bei vielen visuellen Tokens | +| FP8-Quantisierung | Optional (RedHatAI-Variante) | Halbiert VRAM der Gewichte, bis zu 1,8× Speedup laut Benchmarks | + +### Produktions-Deployment + +```bash +# Empfohlenes Setup: vLLM mit Qwen2.5-VL-72B auf 4× RTX 6000 Ada +docker run --gpus all \ + -p 8000:8000 \ + vllm/vllm-openai:latest \ + --model Qwen/Qwen2.5-VL-72B-Instruct \ + --tensor-parallel-size 4 \ + --mm-encoder-tp-mode data \ + --gpu-memory-utilization 0.95 \ + --max-model-len 65536 \ + --limit-mm-per-prompt '{"image":4,"video":0}' \ + --host 0.0.0.0 \ + --port 8000 +``` + +**API-Zugriff** (OpenAI-kompatibel): + +```python +from openai import OpenAI +import base64 + +client = OpenAI(base_url="http://:8000/v1", api_key="dummy") + +with open("dokument.png", "rb") as f: + img_b64 = base64.b64encode(f.read()).decode() + +response = client.chat.completions.create( + model="Qwen/Qwen2.5-VL-72B-Instruct", + messages=[{ + "role": "user", + "content": [ + {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{img_b64}"}}, + {"type": "text", "text": "Analysiere dieses Dokument."} + ] + }], + max_tokens=2048 +) +``` + +--- + +## 5. Kostenübersicht + +| Position | Option A (4× RTX 6000 Ada) | Option B (4× L40S) | +|---|---|---| +| GPUs | 20.000–28.000 € | 28.000–36.000 € | +| Server (CPU, RAM, Storage, Gehäuse, PSU) | 8.000–12.000 € | 8.000–12.000 € | +| Netzwerk (NICs, Kabel) | 500–1.500 € | 500–1.500 € | +| **Gesamtsystem** | **~30.000–42.000 €** | **~38.000–50.000 €** | + +--- + +## 6. Empfehlung + +**Für Qwen2.5-VL-72B als dedizierter Vision-Server empfehlen wir Option A (4× RTX 6000 Ada) als bestes Preis-Leistungs-Verhältnis** oder **Option B (4× L40S) bei Bedarf an Datacenter-Zertifizierung und Dauerbetrieb.** + +Beide Konfigurationen bieten: + +- Qwen2.5-VL-72B in voller BF16-Präzision ohne Quantisierungsverlust +- Ausreichend VRAM-Headroom für hochauflösende Bilder und moderate Batch-Sizes +- ~450 Tokens/s Durchsatz bei Textgenerierung +- Skalierungspfad: FP8-Quantisierung verdoppelt den verfügbaren KV-Cache bei minimalem Qualitätsverlust +- Passend für Standard-19"-Rack (4U), handhabbare Stromaufnahme (~2 kW) + +### Vor Beschaffung klären + +1. **Gleichzeitige Nutzer:** Bei >10 parallelen Bild-Anfragen wird der VRAM-Headroom knapp. Dann FP8-Variante nutzen oder auf 8 GPUs erweitern. +2. **Bildauflösung:** Standard-Dokumente (A4-Scans, Screenshots) sind unkritisch. Hochauflösende Fotos oder Multi-Image-Workflows brauchen striktere `max_pixels`-Limits. +3. **Video-Anforderungen:** Video-Inferenz ist drastisch VRAM-intensiver und langsamer. Falls benötigt, unbedingt `max_pixels` begrenzen und separates Benchmarking durchführen. +4. **Rack-Infrastruktur:** 2 kW Abwärme und 4U Platzbedarf im bestehenden Rack einplanen. Luftkühlung ist bei 4 GPUs à 300W noch problemlos machbar. \ No newline at end of file diff --git a/test-local-vision/app.py b/test-local-vision/app.py index b805d73..36d5170 100644 --- a/test-local-vision/app.py +++ b/test-local-vision/app.py @@ -98,6 +98,24 @@ def _renderPdfPageAsImage(pdfBytes, pageNum=0, zoom=2.0): return result +# ============================================================================ +# Model Helper Functions +# ============================================================================ + +def _isVisionModel(modelName): + """ + Prüft ob ein Modell ein Vision-Modell ist basierend auf Namenskonventionen. + Vision-Modelle enthalten typischerweise 'vision', 'vl', 'llava', 'bakllava' im Namen. + """ + if not modelName: + return False + + modelLower = modelName.lower() + visionIndicators = ['vision', 'vl', 'llava', 'bakllava'] + + return any(indicator in modelLower for indicator in visionIndicators) + + # ============================================================================ # Routes # ============================================================================ @@ -111,8 +129,8 @@ def _index(): @app.route('/api/analyze', methods=['POST']) def _analyzeDocument(): """ - Analysiert ein Dokument mit Ollama Vision API - Erwartet: { imageBase64, prompt, ollamaUrl, modelName } + Analysiert ein Dokument mit Ollama Vision API oder verarbeitet Text mit Non-Vision Modellen + Erwartet: { imageBase64 (optional bei Non-Vision), prompt, ollamaUrl, modelName } """ try: data = request.get_json() @@ -122,21 +140,31 @@ def _analyzeDocument(): ollamaUrl = data.get('ollamaUrl', 'http://localhost:11434') modelName = data.get('modelName', 'qwen2.5vl:72b') - if not imageBase64: - return jsonify({'error': 'Kein Bild übermittelt'}), 400 + # Prüfe ob es ein Vision-Modell ist (basierend auf Namenskonvention) + isVisionModel = _isVisionModel(modelName) + + # Bei Vision-Modellen ist ein Bild erforderlich + if isVisionModel and not imageBase64: + return jsonify({'error': 'Kein Bild übermittelt (erforderlich für Vision-Modelle)'}), 400 if not prompt: return jsonify({'error': 'Kein Prompt übermittelt'}), 400 + # Request-Body erstellen + requestBody = { + 'model': modelName, + 'prompt': prompt, + 'stream': False + } + + # Bilder nur hinzufügen wenn vorhanden (für Vision-Modelle) + if imageBase64: + requestBody['images'] = [imageBase64] + # Ollama API aufrufen (Timeout: 60 Minuten für grosse Modelle) response = requests.post( f'{ollamaUrl}/api/generate', - json={ - 'model': modelName, - 'prompt': prompt, - 'images': [imageBase64], - 'stream': False - }, + json=requestBody, timeout=3600 # 60 Minuten ) @@ -153,15 +181,22 @@ def _analyzeDocument(): responseData = response.json() responseText = responseData.get('response', '') - # JSON aus der Antwort extrahieren + # Versuche JSON aus der Antwort zu extrahieren + extractedData = None jsonMatch = re.search(r'\{[\s\S]*\}', responseText) - if not jsonMatch: - return jsonify({ - 'error': 'Keine JSON-Daten in der Antwort gefunden', - 'rawResponse': responseText - }), 400 - extractedData = json.loads(jsonMatch.group()) + if jsonMatch: + try: + extractedData = json.loads(jsonMatch.group()) + except json.JSONDecodeError: + # JSON-ähnlicher Text gefunden, aber ungültig + extractedData = None + + # Wenn kein JSON gefunden, Antwort in JSON-Objekt verpacken + if extractedData is None: + extractedData = { + 'response': responseText.strip() + } return jsonify({ 'success': True, diff --git a/test-local-vision/templates/index.html b/test-local-vision/templates/index.html index 255a1e4..2abfbcd 100644 --- a/test-local-vision/templates/index.html +++ b/test-local-vision/templates/index.html @@ -677,7 +677,7 @@
- +
@@ -741,66 +741,8 @@ Falls ein Feld nicht erkennbar ist, setze den Wert auf null.