From 211ede9fc5bbea199e7f3eb8805f9c8263c5df82 Mon Sep 17 00:00:00 2001 From: Mikei386 <44135113+Mikei386@users.noreply.github.com> Date: Mon, 31 Aug 2026 21:11:17 +0200 Subject: [PATCH] Update llama runtime and isolate background reviews --- .env.example | 4 ++ compose.yaml | 84 +++++++++++++++++++++++++++++---- config/install.env.example | 5 ++ docs/CURRENT_RUNTIME_NOTES.md | 53 +++++++++++++++++++++ platform/llama/LLAMA_CPP_COMMIT | 2 +- platform/llama/README.md | 12 +++-- router/ai_profile_router.py | 79 +++++++++++++++++++++++++------ 7 files changed, 211 insertions(+), 28 deletions(-) create mode 100644 docs/CURRENT_RUNTIME_NOTES.md diff --git a/.env.example b/.env.example index 38f57d1..bda9f1f 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,10 @@ # Generated as /etc/mike-ai/stack.env by install.sh. Never commit real values. AI_BIND_ADDRESS=10.77.0.2 MODEL_DIR=/data/models +REVIEW_MODEL_FILE=qwen3-4b-review/Qwen3-4B-Instruct-2507-Q4_K_M.gguf +REVIEW_CONTEXT=32768 +REVIEW_GPU_DEVICE=1 +REVIEW_GPU_LAYERS=0 ROUTER_API_KEY=GENERATED_BY_INSTALLER CONTROLLER_TOKEN=GENERATED_BY_INSTALLER FLUX_MODEL_DIR=/data/models/FLUX.2-klein-4B diff --git a/compose.yaml b/compose.yaml index d412108..3862a45 100644 --- a/compose.yaml +++ b/compose.yaml @@ -32,6 +32,76 @@ x-llama-common: &llama-common start_period: 30s services: + # Kleines, dauerhaft geladenes Hilfsmodell fuer Hermes-Hintergrundaufgaben + # (Memory-/Skill-Review). Es laeuft unabhaengig vom wechselnden Hauptmodell + # und kann dessen Prompt-Cache daher nicht mehr + # verdraengen. Athena reserviert die 3060 bereits fuer Qwen und XTTS; der + # robuste Standard ist deshalb CPU/RAM. GPU-Layer koennen spaeter per Env + # zugeschaltet werden, sobald dort garantiert Speicher frei ist. + llama-review: + image: ${LLAMA_IMAGE:-mike-ai/llama.cpp:local} + container_name: mike-ai-llama-review + restart: unless-stopped + gpus: all + ipc: host + read_only: true + tmpfs: + - /tmp:size=512m,mode=1777 + volumes: + - "${MODEL_DIR:-/srv/mike-ai/models}:/models:ro" + environment: + NVIDIA_VISIBLE_DEVICES: ${REVIEW_GPU_DEVICE:-1} + NVIDIA_DRIVER_CAPABILITIES: compute,utility + command: + - --model + - "/models/${REVIEW_MODEL_FILE:-qwen3-4b-review/Qwen3-4B-Instruct-2507-Q4_K_M.gguf}" + - --alias + - qwen-review + - --ctx-size + - "${REVIEW_CONTEXT:-32768}" + - --flash-attn + - "on" + - --cache-type-k + - q4_0 + - --cache-type-v + - q4_0 + - --cache-prompt + - --threads + - "${LLAMA_THREADS:-6}" + - --threads-batch + - "${LLAMA_THREADS_BATCH:-6}" + - --batch-size + - "${REVIEW_BATCH_SIZE:-512}" + - --ubatch-size + - "${REVIEW_UBATCH_SIZE:-64}" + - --parallel + - "1" + - --kv-unified + - --jinja + - --host + - 0.0.0.0 + - --port + - "8080" + - --metrics + - --fit + - "off" + - --n-gpu-layers + - "${REVIEW_GPU_LAYERS:-0}" + - --no-kv-offload + - --no-mmap + - --no-ui + - --temperature + - "0.2" + networks: [inference] + security_opt: ["no-new-privileges:true"] + cap_drop: [ALL] + healthcheck: + test: [CMD, curl, -fsS, "http://127.0.0.1:8080/health"] + interval: 10s + timeout: 5s + retries: 60 + start_period: 30s + wireguard-gateway: build: ./platform/docker/wireguard-gateway image: mike-ai/wireguard-gateway:local @@ -153,17 +223,12 @@ services: environment: NVIDIA_VISIBLE_DEVICES: ${MEDIUM_GPU_DEVICES:-0,1} NVIDIA_DRIVER_CAPABILITIES: compute,utility - # Keep the language model split unchanged while placing the complete - # multimodal projector on the secondary RTX 3060. - MTMD_BACKEND_DEVICE: CUDA1 + # Temporary llama.cpp cache workaround: keep Medium text-only. Current + # multimodal builds can discard the complete KV state on a later turn. + # The model split, context and MTP settings remain unchanged. command: - --model - "/models/${MEDIUM_MODEL_FILE:?MEDIUM_MODEL_FILE is required}" - - --mmproj - - "/models/${VISION_PROJECTOR_FILE:?VISION_PROJECTOR_FILE is required}" - - --mmproj-offload - - --mmproj-device - - CUDA1 - --alias - qwen-medium - --ctx-size @@ -568,6 +633,9 @@ services: ROUTER_STATE_FILE: /var/lib/mike-ai-profile-router/state.json ROUTER_MAX_CONCURRENT_REQUESTS: "16" UPSTREAM_URL: http://llama-upstream:8080 + REVIEW_UPSTREAM_URL: http://llama-review:8080 + REVIEW_MODEL_NAME: qwen-review + REVIEW_CONTEXT_LENGTH: "${REVIEW_CONTEXT:-32768}" PROFILE_CONTROL_URL: http://profile-controller:8090 PROFILE_CONTROL_TOKEN: "${CONTROLLER_TOKEN:?CONTROLLER_TOKEN is required}" SWITCH_TIMEOUT: "600" diff --git a/config/install.env.example b/config/install.env.example index 9054cc9..27aa3a2 100644 --- a/config/install.env.example +++ b/config/install.env.example @@ -4,6 +4,11 @@ AI_HOSTNAME=ki-host ADMIN_USER=mike MODEL_DIR=/data/models +REVIEW_MODEL_FILE=qwen3-4b-review/Qwen3-4B-Instruct-2507-Q4_K_M.gguf +REVIEW_CONTEXT=32768 +# Eine einzelne GPU-ID oder stabile NVIDIA-GPU-UUID; auf Athena die RTX 3060. +REVIEW_GPU_DEVICE=1 +REVIEW_GPU_LAYERS=0 # Installing a new NVIDIA driver can require one reboot. In that case this # installer exits with code 20 (NVIDIA) or 21 (stable NIC rename); rerun the diff --git a/docs/CURRENT_RUNTIME_NOTES.md b/docs/CURRENT_RUNTIME_NOTES.md new file mode 100644 index 0000000..b59d753 --- /dev/null +++ b/docs/CURRENT_RUNTIME_NOTES.md @@ -0,0 +1,53 @@ +# Aktueller temporärer Laufzustand + +Stand: 31. August 2026 + +## Produktive llama.cpp-Runtime + +Alle Textprofile und das Review-Modell verwenden llama.cpp Build 10718, +Commit `41ef91f7c8046087cdfbb276b79bff311ecf1c6d`. Der vorherige Build 10587 +wurde nach einem kontrollierten A/B-Test ersetzt. Der Wechsel verbessert den +Durchsatz bei großen Werkzeuglisten, ohne im 70K-/120K-Kontext- und Cachetest +eine Regression zu zeigen. + +## Qwen Medium: vorübergehend ohne Vision-Projektor + +`qwen-medium` läuft aktuell als reines Textprofil **ohne `--mmproj`**. Das ist +ein vorläufiger Workaround für einen llama.cpp-Fehler, bei dem ein geladener +Multimodal-/Vision-Projektor die Wiederverwendung des Prompt-/KV-Caches +verhindern und dadurch eine vollständige erneute Prompt-Verarbeitung auslösen +kann. + +Dabei gilt ausdrücklich: + +- Der Gesamtkontext bleibt bei **160.000 Tokens**. +- **MTP / Speculative Decoding bleibt aktiviert**; MTP wurde nicht entfernt. +- Modell, Quantisierung, GPU-Aufteilung und KV-Cache-Quantisierung bleiben + unverändert. +- Nur die Bildanalyse über den Qwen-Vision-Projektor ist im Medium-Profil + vorübergehend nicht verfügbar. +- Der Workaround ist noch nicht durch einen vollständig kontrollierten lokalen + A/B-Test mit identischen Anfragen bestätigt. Er entspricht jedoch dem in + llama.cpp Issue #19858 und verwandten Meldungen beschriebenen Workaround. + +Referenz: + +- https://github.com/ggml-org/llama.cpp/issues/19858 +- https://github.com/ggml-org/llama.cpp/issues/21133 + +## Separates bekanntes Problem + +Automatische Hermes-Hintergrundanfragen können weiterhin den einzigen aktiven +llama.cpp-Slot belegen und damit den Cache eines großen Chats verdrängen. Dieses +Slot-Eviction-Problem ist unabhängig vom MMProj-Workaround und muss separat in +der Hermes-Auxiliary-/Hintergrundverarbeitung geklärt werden. + +## Nächster Entscheidungspunkt + +Vor einer dauerhaften Übernahme kontrolliert vergleichen: + +1. identischer Mehrturn-Textchat mit MMProj, +2. identischer Mehrturn-Textchat ohne MMProj, +3. jeweils keine parallelen Hermes-Hintergrundanfragen, +4. gelesene beziehungsweise erneut verarbeitete Prompt-Tokens und Prefill-Zeit + vergleichen. diff --git a/platform/llama/LLAMA_CPP_COMMIT b/platform/llama/LLAMA_CPP_COMMIT index c10ce37..566c641 100644 --- a/platform/llama/LLAMA_CPP_COMMIT +++ b/platform/llama/LLAMA_CPP_COMMIT @@ -1 +1 @@ -3f545beccee69d9975f466ec7e45fd9aacd8ba90 +41ef91f7c8046087cdfbb276b79bff311ecf1c6d diff --git a/platform/llama/README.md b/platform/llama/README.md index c3d99a2..fa60ed4 100644 --- a/platform/llama/README.md +++ b/platform/llama/README.md @@ -21,11 +21,13 @@ nach Standardbenchmark, Tool-Calling-Test und Kontexttest übernommen. - Fast, Medium, Large und Uncensored: integrierte Vision; der jeweilige Projektor liegt vollständig auf der RTX 3060 -Die produktive Runtime ist auf Commit -`3f545beccee69d9975f466ec7e45fd9aacd8ba90` festgeschrieben. Gegen den -vorherigen Commit waren Antworten und Geschwindigkeit praktisch identisch; -übernommen wurde er wegen der Qwen-/MTP-/Multimodal-Korrekturen und des -expliziten `--mmproj-device`. +Die produktive Runtime ist auf llama.cpp Build 10718, Commit +`41ef91f7c8046087cdfbb276b79bff311ecf1c6d`, festgeschrieben. Im lokalen +A/B-Test blieben normaler Durchsatz, 70K-/120K-Kontext und Prompt-Cache +unverändert. Mit 100 angebotenen Werkzeugen stieg der Ausgabedurchsatz von +56,03 auf 60,67 Tokens/s. Der Build begrenzt insbesondere einen teuren +Qwen3-Coder-Workaround wieder auf die betroffenen Modellfamilien, statt ihn +auch auf Qwen3.8 anzuwenden. Die Dateien selbst sind nicht Bestandteil des Repositories. Pfade und Hashes werden im lokalen Modellmanifest verwaltet. diff --git a/router/ai_profile_router.py b/router/ai_profile_router.py index caece2d..6121d2b 100755 --- a/router/ai_profile_router.py +++ b/router/ai_profile_router.py @@ -94,6 +94,9 @@ from router_support import ( HOST = os.environ.get("ROUTER_HOST", "0.0.0.0") PORT = int(os.environ.get("ROUTER_PORT", "8081")) UPSTREAM_URL = os.environ.get("UPSTREAM_URL", "http://127.0.0.1:8080").rstrip("/") +REVIEW_UPSTREAM_URL = os.environ.get("REVIEW_UPSTREAM_URL", "").rstrip("/") +REVIEW_MODEL_NAME = os.environ.get("REVIEW_MODEL_NAME", "qwen-review").strip() +REVIEW_CONTEXT_LENGTH = int(os.environ.get("REVIEW_CONTEXT_LENGTH", "32768")) PROFILE_SCRIPT = os.environ.get("PROFILE_SCRIPT", "/usr/local/bin/llama-profile") PROFILE_DIR = os.environ.get( "PROFILE_DIR", "/etc/systemd/system/mike-ai-llama-ui.service.d") @@ -225,6 +228,11 @@ def _parse_upstream(url: str) -> tuple[str, int]: UPSTREAM_HOST, UPSTREAM_PORT = _parse_upstream(UPSTREAM_URL) +if REVIEW_UPSTREAM_URL: + REVIEW_UPSTREAM_HOST, REVIEW_UPSTREAM_PORT = _parse_upstream( + REVIEW_UPSTREAM_URL) +else: + REVIEW_UPSTREAM_HOST, REVIEW_UPSTREAM_PORT = "", 0 # --------------------------------------------------------------------------- @@ -1590,19 +1598,29 @@ class Handler(BaseHTTPRequestHandler): @staticmethod def _models_payload() -> dict: + models = [ + { + "id": f"qwen-{name}", + "object": "model", + "created": 0, + "owned_by": "ai-profile-router", + "context_length": ctx, + "context_window": ctx, + } + for name, ctx in PROFILES.items() + ] + if REVIEW_UPSTREAM_URL: + models.append({ + "id": REVIEW_MODEL_NAME, + "object": "model", + "created": 0, + "owned_by": "ai-profile-router", + "context_length": REVIEW_CONTEXT_LENGTH, + "context_window": REVIEW_CONTEXT_LENGTH, + }) return { "object": "list", - "data": [ - { - "id": f"qwen-{name}", - "object": "model", - "created": 0, - "owned_by": "ai-profile-router", - "context_length": ctx, - "context_window": ctx, - } - for name, ctx in PROFILES.items() - ], + "data": models, } def _status_payload(self) -> dict: @@ -2165,6 +2183,7 @@ class Handler(BaseHTTPRequestHandler): data = None requested_profile: str | None = None + requested_review = False # Virtuelles Modell erkennen. Umschalten und Chat-Lease werden weiter # unten atomar unter dem zentralen Orchestrierungs-Lock ausgeführt. if body is not None and self.path.startswith("/v1/"): @@ -2173,7 +2192,10 @@ class Handler(BaseHTTPRequestHandler): except ValueError: data = None model = data.get("model") if isinstance(data, dict) else None - if isinstance(model, str) and model in VIRTUAL_MODELS: + if (isinstance(model, str) and REVIEW_UPSTREAM_URL + and model == REVIEW_MODEL_NAME): + requested_review = True + elif isinstance(model, str) and model in VIRTUAL_MODELS: requested_profile = VIRTUAL_MODELS[model] elif isinstance(model, str) and model.startswith("qwen-"): # qwen-* ist der Namensraum des Routers @@ -2185,6 +2207,9 @@ class Handler(BaseHTTPRequestHandler): # kann kein zweiter Client zwischen Profilwahl und Upstream-Request das # Modell austauschen. Vision-Vorbereitung gehört zur selben Transaktion. if isinstance(data, dict) and path == "/v1/chat/completions": + if requested_review: + self._review_chat_proxy(data) + return self._chat_proxy(body, data, requested_profile) return if requested_profile is not None: @@ -2262,6 +2287,28 @@ class Handler(BaseHTTPRequestHandler): if lease_acquired: self._release_model_lease() + def _review_chat_proxy(self, data: dict) -> None: + """Leitet kompakte Hermes-Hintergrundreviews an das Hilfsmodell. + + Absichtlich ohne Hauptmodell-Lease und Profilwechsel: Der Review darf + den aktiven Qwen-Chat weder anhalten noch dessen Prompt-Cache ersetzen. + """ + try: + data = _normalize_llamacpp_reasoning(data) + data = _cap_chat_generation(data) + if _request_has_image(data): + raise ValueError("qwen-review unterstützt keine Bilder") + data["model"] = REVIEW_MODEL_NAME + self._proxy_to( + json.dumps(data).encode(), + REVIEW_UPSTREAM_HOST, + REVIEW_UPSTREAM_PORT, + "Review-Modell", + ) + except ValueError as exc: + self._send_error(400, str(exc), "invalid_request_error", + "invalid_review_request") + def _proxy_with_wait(self, body: bytes | None) -> None: """Leitet an llama.cpp weiter, wartet aber erst, bis Qwen verfügbar ist. @@ -2295,9 +2342,13 @@ class Handler(BaseHTTPRequestHandler): STATE.active_chats -= 1 def _proxy(self, body: bytes | None) -> None: + self._proxy_to(body, UPSTREAM_HOST, UPSTREAM_PORT, "llama.cpp") + + def _proxy_to(self, body: bytes | None, host: str, port: int, + upstream_name: str) -> None: # An llama.cpp weiterleiten (Streaming bleibt erhalten). try: - conn = http.client.HTTPConnection(UPSTREAM_HOST, UPSTREAM_PORT, + conn = http.client.HTTPConnection(host, port, timeout=CONNECT_TIMEOUT) conn.connect() conn.sock.settimeout(REQUEST_TIMEOUT) @@ -2306,7 +2357,7 @@ class Handler(BaseHTTPRequestHandler): conn.request(self.command, self.path, body=body, headers=headers) resp = conn.getresponse() except (OSError, http.client.HTTPException) as e: - self._send_error(502, f"llama.cpp nicht erreichbar: {e}", + self._send_error(502, f"{upstream_name} nicht erreichbar: {e}", "server_error", "upstream_unavailable") return