From c8878489b5f09000a76e6a141f9dc28874591f3a Mon Sep 17 00:00:00 2001 From: ValueOn AG Date: Sun, 1 Feb 2026 09:51:26 +0100 Subject: [PATCH] local llm mvp demo --- deployment/poweron_sec.kdbx | Bin 15534 -> 15502 bytes test-local-vision/analysis/buy-spec.md | 287 +++++++++++++++++++++++++ test-local-vision/app.py | 69 ++++-- test-local-vision/templates/index.html | 262 +++++++++++++--------- 4 files changed, 499 insertions(+), 119 deletions(-) create mode 100644 test-local-vision/analysis/buy-spec.md diff --git a/deployment/poweron_sec.kdbx b/deployment/poweron_sec.kdbx index 5bd02d6d0474df26d99bc3535feb5fdea3ca1099..a5c617737af6927057d8249e4ab1f5f3cdb70dc0 100644 GIT binary patch literal 15502 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 QcfPUp#kygh!YPd!McuU-Hvj+t literal 15534 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 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.