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.
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
+76 -8
View File
@@ -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"
+5
View File
@@ -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
+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
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.
+58 -7
View File
@@ -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,9 +1598,7 @@ class Handler(BaseHTTPRequestHandler):
@staticmethod
def _models_payload() -> dict:
return {
"object": "list",
"data": [
models = [
{
"id": f"qwen-{name}",
"object": "model",
@@ -1602,7 +1608,19 @@ class Handler(BaseHTTPRequestHandler):
"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": 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