feat(llm): ein Modell pro Karte, Clients waehlen nur aus — und Doku fuer Drittprogramme
Bisher konnte jeder Client per Modellname llama-swap zum Wechseln bringen — ein Fremdprogramm haette ARIA das Modell unter den Fuessen weggetauscht. Jetzt: - Jede LLM-Karte einer Box (LLM_GPUS=1,2, alt LLM_GPU=1) haelt GENAU ein Modell, dauerhaft. Welches, legt nur die Diagnostic fest. Karten, die nicht fuers LLM vorgesehen sind, tauchen beim LLM nicht auf und werden als Ziel abgelehnt. - Clients (ARIA, Fremdprogramme ueber RVS, OpenHands ueber den OpenAI-Port) sehen nur die geladenen Modelle und laden nie: Anfrage fuer ein anderes → model_not_loaded mit Liste des Geladenen (RVS) bzw. HTTP 404 (OpenAI-Port). Nichts wird verdraengt. - Mehrere Clients koennen dasselbe Modell gleichzeitig nutzen. llm-adapter: Belegung in /models/aria_slots.json. Die llama-swap-Config enthaelt nur belegte Karten: pro Modell CUDA_VISIBLE_DEVICES=<Karte>, ttl 0, groups.swap=false, hooks.on_startup.preload. Alles gegen llama-swap ausprobiert (zwei Modelle gleichzeitig ready, keins verdraengt, Unbekanntes 404, env kommt beim jeweiligen llama-server an). llama-swap laeuft mit --watch-config; die frueher aufgerufenen Reload-Endpunkte (/reload, /api/config/reload) gibt es nicht (404). Neu: llm_unload_model (Karte leeren), llm_models_request → llm_models (Erkundung, jede Box antwortet einzeln). Fehlgeschlagenes Laden setzt die Karte auf ihr vorheriges Modell zurueck, sonst risse jedes Neuladen die anderen Karten mit. Uebernahme: ohne Belegungsdatei landet LLM_MODEL auf der ersten LLM-Karte. Compose: llama-swap sieht alle Karten (NVIDIA_VISIBLE_DEVICES=all) und zaehlt sie wie nvidia-smi (CUDA_DEVICE_ORDER=PCI_BUS_ID); das Pinnen pro Modell macht der Adapter. Bridge: uebernimmt models/slots auch aus worker_ping (nach "Entladen" sonst 30 s veraltet) und sagt sofort ab, wenn das Modell auf keiner Box liegt, statt per Broadcast in den Timeout zu laufen. Diagnostic: Flotte zeigt pro LLM-Karte Modell + Zustand + "Entladen"; Katalog waehlt eine Ziel-KARTE (leere vorausgewaehlt, Ersetzen mit Rueckfrage); Download-Fenster nennt Karte und was ersetzt wird. RVS: llm_unload_model, llm_models_request, llm_models freigegeben. docs/LLM-SCHNITTSTELLE.md: Schnittstelle fuer Drittprogramme (RVS + OpenAI- Port), Fehlercodes, Gleichzeitigkeit, Python-Beispiel. Tests: 23 gegen llama-swap (Adapter direkt) + Ende-zu-Ende ueber den echten RVS mit dem Adapter als Prozess: Laden per RVS mit Fortschritt, das Doku-Beispiel unveraendert (nur Adresse) findet beide Karten und bekommt eine Antwort, Absage/Schweigen wie dokumentiert, zwei Clients parallel. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -3774,6 +3774,9 @@ class ARIABridge:
|
||||
# Fallback auf [model] fuer alte Adapter ohne models-Feld.
|
||||
"models": [m for m in _models if m] if isinstance(_models, list)
|
||||
else ([payload.get("model")] if payload.get("model") else []),
|
||||
# Karten-Belegung (neuer llm-adapter): dann ist "models" nur
|
||||
# noch das GELADENE — Clients laden nichts nach.
|
||||
**({"slots": payload["slots"]} if isinstance(payload.get("slots"), list) else {}),
|
||||
"busy": bool(prev.get("busy", False)),
|
||||
"last_seen": time.time(),
|
||||
}
|
||||
@@ -3819,6 +3822,13 @@ class ARIABridge:
|
||||
"model": "", "busy": False, "last_seen": 0.0,
|
||||
}
|
||||
w["busy"] = bool(payload.get("busy", False))
|
||||
# LLM-Boxen melden mit jedem Ping, welche Modelle GELADEN sind.
|
||||
# Nach "Karte leeren" in der Diagnostic sonst bis zum naechsten
|
||||
# hello (30 s) veraltet — ARIA wuerde ein entladenes Modell anfragen.
|
||||
if isinstance(payload.get("models"), list):
|
||||
w["models"] = payload["models"]
|
||||
if isinstance(payload.get("slots"), list):
|
||||
w["slots"] = payload["slots"]
|
||||
w["last_seen"] = time.time()
|
||||
return
|
||||
|
||||
@@ -4265,6 +4275,16 @@ class ARIABridge:
|
||||
llm_target = self._pick_worker("llm", model=model or None)
|
||||
if llm_target:
|
||||
req_payload["targetInstance"] = llm_target
|
||||
elif model and self._llm_boxes_with_slots():
|
||||
# Boxen mit Karten-Belegung laden nichts nach und schweigen auf
|
||||
# Broadcasts fuer fremde Modelle. Ein Broadcast liefe also nur in
|
||||
# den Timeout — lieber sofort ehrlich absagen.
|
||||
self._pending_llm.pop(request_id, None)
|
||||
loaded = sorted({m for w in self._llm_boxes_with_slots() for m in (w.get("models") or [])})
|
||||
return {"ok": False, "errorCode": "model_not_loaded",
|
||||
"error": (f"Lokales Modell {model!r} ist auf keiner KI-Box geladen. "
|
||||
f"Geladen: {', '.join(loaded) or 'nichts'}. "
|
||||
"Laden: Diagnostic → Modell-Katalog.")}
|
||||
logger.info("[rvs] llm_request → llm-adapter (id=%s, msgs=%d, max_tokens=%d, tools=%d, model=%s, target=%s)",
|
||||
request_id[:8], len(messages), max_tokens, len(tools) if tools else 0,
|
||||
model or "-", llm_target or "(broadcast)")
|
||||
@@ -5234,6 +5254,13 @@ class ARIABridge:
|
||||
})
|
||||
return out
|
||||
|
||||
def _llm_boxes_with_slots(self) -> list:
|
||||
"""Online-LLM-Boxen, die schon Karten-Belegung melden (neuer Adapter)."""
|
||||
now = time.time()
|
||||
return [w for w in self._workers.values()
|
||||
if w.get("service") == "llm" and isinstance(w.get("slots"), list)
|
||||
and (now - w.get("last_seen", 0)) < self.WORKER_OFFLINE_S]
|
||||
|
||||
def _pick_worker(self, service: str, model: Optional[str] = None) -> Optional[str]:
|
||||
"""Waehlt eine online, moeglichst freie Instanz des Diensts (Round-Robin
|
||||
ueber die freien). Gibt die instanceId oder None. Fuer Stage-3-Routing
|
||||
|
||||
Reference in New Issue
Block a user