Update llama runtime and isolate background reviews

This commit is contained in:
Mikei386
2026-08-31 21:11:17 +02:00
parent 7e36f1dbb7
commit 211ede9fc5
7 changed files with 211 additions and 28 deletions
+4
View File
@@ -1,6 +1,10 @@
# Generated as /etc/mike-ai/stack.env by install.sh. Never commit real values. # Generated as /etc/mike-ai/stack.env by install.sh. Never commit real values.
AI_BIND_ADDRESS=10.77.0.2 AI_BIND_ADDRESS=10.77.0.2
MODEL_DIR=/data/models 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 ROUTER_API_KEY=GENERATED_BY_INSTALLER
CONTROLLER_TOKEN=GENERATED_BY_INSTALLER CONTROLLER_TOKEN=GENERATED_BY_INSTALLER
FLUX_MODEL_DIR=/data/models/FLUX.2-klein-4B FLUX_MODEL_DIR=/data/models/FLUX.2-klein-4B
+76 -8
View File
@@ -32,6 +32,76 @@ x-llama-common: &llama-common
start_period: 30s start_period: 30s
services: 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: wireguard-gateway:
build: ./platform/docker/wireguard-gateway build: ./platform/docker/wireguard-gateway
image: mike-ai/wireguard-gateway:local image: mike-ai/wireguard-gateway:local
@@ -153,17 +223,12 @@ services:
environment: environment:
NVIDIA_VISIBLE_DEVICES: ${MEDIUM_GPU_DEVICES:-0,1} NVIDIA_VISIBLE_DEVICES: ${MEDIUM_GPU_DEVICES:-0,1}
NVIDIA_DRIVER_CAPABILITIES: compute,utility NVIDIA_DRIVER_CAPABILITIES: compute,utility
# Keep the language model split unchanged while placing the complete # Temporary llama.cpp cache workaround: keep Medium text-only. Current
# multimodal projector on the secondary RTX 3060. # multimodal builds can discard the complete KV state on a later turn.
MTMD_BACKEND_DEVICE: CUDA1 # The model split, context and MTP settings remain unchanged.
command: command:
- --model - --model
- "/models/${MEDIUM_MODEL_FILE:?MEDIUM_MODEL_FILE is required}" - "/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 - --alias
- qwen-medium - qwen-medium
- --ctx-size - --ctx-size
@@ -568,6 +633,9 @@ services:
ROUTER_STATE_FILE: /var/lib/mike-ai-profile-router/state.json ROUTER_STATE_FILE: /var/lib/mike-ai-profile-router/state.json
ROUTER_MAX_CONCURRENT_REQUESTS: "16" ROUTER_MAX_CONCURRENT_REQUESTS: "16"
UPSTREAM_URL: http://llama-upstream:8080 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_URL: http://profile-controller:8090
PROFILE_CONTROL_TOKEN: "${CONTROLLER_TOKEN:?CONTROLLER_TOKEN is required}" PROFILE_CONTROL_TOKEN: "${CONTROLLER_TOKEN:?CONTROLLER_TOKEN is required}"
SWITCH_TIMEOUT: "600" SWITCH_TIMEOUT: "600"
+5
View File
@@ -4,6 +4,11 @@
AI_HOSTNAME=ki-host AI_HOSTNAME=ki-host
ADMIN_USER=mike ADMIN_USER=mike
MODEL_DIR=/data/models 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 # 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 # installer exits with code 20 (NVIDIA) or 21 (stable NIC rename); rerun the
+53
View File
@@ -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.
+1 -1
View File
@@ -1 +1 @@
3f545beccee69d9975f466ec7e45fd9aacd8ba90 41ef91f7c8046087cdfbb276b79bff311ecf1c6d
+7 -5
View File
@@ -21,11 +21,13 @@ nach Standardbenchmark, Tool-Calling-Test und Kontexttest übernommen.
- Fast, Medium, Large und Uncensored: integrierte Vision; der jeweilige - Fast, Medium, Large und Uncensored: integrierte Vision; der jeweilige
Projektor liegt vollständig auf der RTX 3060 Projektor liegt vollständig auf der RTX 3060
Die produktive Runtime ist auf Commit Die produktive Runtime ist auf llama.cpp Build 10718, Commit
`3f545beccee69d9975f466ec7e45fd9aacd8ba90` festgeschrieben. Gegen den `41ef91f7c8046087cdfbb276b79bff311ecf1c6d`, festgeschrieben. Im lokalen
vorherigen Commit waren Antworten und Geschwindigkeit praktisch identisch; A/B-Test blieben normaler Durchsatz, 70K-/120K-Kontext und Prompt-Cache
übernommen wurde er wegen der Qwen-/MTP-/Multimodal-Korrekturen und des unverändert. Mit 100 angebotenen Werkzeugen stieg der Ausgabedurchsatz von
expliziten `--mmproj-device`. 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 Die Dateien selbst sind nicht Bestandteil des Repositories. Pfade und Hashes
werden im lokalen Modellmanifest verwaltet. werden im lokalen Modellmanifest verwaltet.
+58 -7
View File
@@ -94,6 +94,9 @@ from router_support import (
HOST = os.environ.get("ROUTER_HOST", "0.0.0.0") HOST = os.environ.get("ROUTER_HOST", "0.0.0.0")
PORT = int(os.environ.get("ROUTER_PORT", "8081")) PORT = int(os.environ.get("ROUTER_PORT", "8081"))
UPSTREAM_URL = os.environ.get("UPSTREAM_URL", "http://127.0.0.1:8080").rstrip("/") 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_SCRIPT = os.environ.get("PROFILE_SCRIPT", "/usr/local/bin/llama-profile")
PROFILE_DIR = os.environ.get( PROFILE_DIR = os.environ.get(
"PROFILE_DIR", "/etc/systemd/system/mike-ai-llama-ui.service.d") "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) 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,9 +1598,7 @@ class Handler(BaseHTTPRequestHandler):
@staticmethod @staticmethod
def _models_payload() -> dict: def _models_payload() -> dict:
return { models = [
"object": "list",
"data": [
{ {
"id": f"qwen-{name}", "id": f"qwen-{name}",
"object": "model", "object": "model",
@@ -1602,7 +1608,19 @@ class Handler(BaseHTTPRequestHandler):
"context_window": ctx, "context_window": ctx,
} }
for name, ctx in PROFILES.items() 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": models,
} }
def _status_payload(self) -> dict: def _status_payload(self) -> dict:
@@ -2165,6 +2183,7 @@ class Handler(BaseHTTPRequestHandler):
data = None data = None
requested_profile: str | None = None requested_profile: str | None = None
requested_review = False
# Virtuelles Modell erkennen. Umschalten und Chat-Lease werden weiter # Virtuelles Modell erkennen. Umschalten und Chat-Lease werden weiter
# unten atomar unter dem zentralen Orchestrierungs-Lock ausgeführt. # unten atomar unter dem zentralen Orchestrierungs-Lock ausgeführt.
if body is not None and self.path.startswith("/v1/"): if body is not None and self.path.startswith("/v1/"):
@@ -2173,7 +2192,10 @@ class Handler(BaseHTTPRequestHandler):
except ValueError: except ValueError:
data = None data = None
model = data.get("model") if isinstance(data, dict) else 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] requested_profile = VIRTUAL_MODELS[model]
elif isinstance(model, str) and model.startswith("qwen-"): elif isinstance(model, str) and model.startswith("qwen-"):
# qwen-* ist der Namensraum des Routers # qwen-* ist der Namensraum des Routers
@@ -2185,6 +2207,9 @@ class Handler(BaseHTTPRequestHandler):
# kann kein zweiter Client zwischen Profilwahl und Upstream-Request das # kann kein zweiter Client zwischen Profilwahl und Upstream-Request das
# Modell austauschen. Vision-Vorbereitung gehört zur selben Transaktion. # Modell austauschen. Vision-Vorbereitung gehört zur selben Transaktion.
if isinstance(data, dict) and path == "/v1/chat/completions": 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) self._chat_proxy(body, data, requested_profile)
return return
if requested_profile is not None: if requested_profile is not None:
@@ -2262,6 +2287,28 @@ class Handler(BaseHTTPRequestHandler):
if lease_acquired: if lease_acquired:
self._release_model_lease() 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: def _proxy_with_wait(self, body: bytes | None) -> None:
"""Leitet an llama.cpp weiter, wartet aber erst, bis Qwen verfügbar ist. """Leitet an llama.cpp weiter, wartet aber erst, bis Qwen verfügbar ist.
@@ -2295,9 +2342,13 @@ class Handler(BaseHTTPRequestHandler):
STATE.active_chats -= 1 STATE.active_chats -= 1
def _proxy(self, body: bytes | None) -> None: 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). # An llama.cpp weiterleiten (Streaming bleibt erhalten).
try: try:
conn = http.client.HTTPConnection(UPSTREAM_HOST, UPSTREAM_PORT, conn = http.client.HTTPConnection(host, port,
timeout=CONNECT_TIMEOUT) timeout=CONNECT_TIMEOUT)
conn.connect() conn.connect()
conn.sock.settimeout(REQUEST_TIMEOUT) conn.sock.settimeout(REQUEST_TIMEOUT)
@@ -2306,7 +2357,7 @@ class Handler(BaseHTTPRequestHandler):
conn.request(self.command, self.path, body=body, headers=headers) conn.request(self.command, self.path, body=body, headers=headers)
resp = conn.getresponse() resp = conn.getresponse()
except (OSError, http.client.HTTPException) as e: 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") "server_error", "upstream_unavailable")
return return