Anlass: die Software-Schmiede bekam "request (8274 tokens) exceeds the available context size (8192 tokens)". Sie holt ctx vom Cluster zwar ab, kuerzt aber nach einem eigenen, von Hand eingetragenen Wert (32768). ARIA-Seite: - Neue Fehlerart context_exceeded (statt model_error) mit promptTokens und ctx — Programme koennen automatisch kuerzen und nochmal senden, statt Text auszuwerten. llm-adapter liest es aus llama.cpps exceed_context_size_error, der Cluster-Router reicht es durch und wiederholt es nicht anderswo. - Diagnostic → LLM-Karten: Kontextfenster pro Karte (4k … 128k). Laedt das Modell mit neuem -c; Programme sehen den neuen Wert ueber ctx. Passt es nicht in den Grafikspeicher, setzt die Box zurueck — jetzt inkl. Registry (vorher blieb bei einem Fehlschlag der zu grosse ctx in aria_models.json stehen und waere beim naechsten Start wieder gescheitert). - Doku: context_exceeded, ctx kann sich aendern, "danach kuerzen, nicht nur anzeigen". Getestet gegen llama-swap: Kontext 512 → 1024 (gleiche Karte), Fehlschlag bei 65536 → Registry + Config wieder 1024, zu langer Verlauf → context_exceeded mit promptTokens=6030, ctx=1024. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
363 lines
15 KiB
Markdown
363 lines
15 KiB
Markdown
# Lokale Sprachmodelle nutzen — Schnittstelle für Programme
|
|
|
|
Wie ein Programm die lokalen Sprachmodelle auf den KI-Boxen von ARIA nutzt.
|
|
|
|
**Kurz:** Du fragst ein **Modell**, nicht eine Box oder Karte. Der Cluster
|
|
sucht sich aus, wo es läuft. Eigene Programme sprechen dafür über den RVS
|
|
(Abschnitt A). Programme, die nur „OpenAI-kompatibel" können, bekommen den
|
|
OpenAI-RVS-Wrapper neben sich gestellt (Abschnitt B).
|
|
|
|
## Die Grundregel
|
|
|
|
**Programme wählen ein Modell aus — laden aber nie.**
|
|
|
|
- Jede KI-Box hat eine oder mehrere Grafikkarten fürs LLM. Jede dieser Karten
|
|
hält **genau ein Modell, dauerhaft geladen**.
|
|
- Welches Modell wo liegt, legt nur der Betreiber in der ARIA-Diagnostic fest
|
|
(Einstellungen → LLM-Karten).
|
|
- Ein Programm sieht die geladenen Modelle des ganzen Clusters — jedes
|
|
**einmal**, egal auf wie vielen Karten es liegt — und wählt eins.
|
|
- Liegt das Modell auf mehreren Karten, **verteilt der Cluster bei Last**.
|
|
Fällt eine Box mitten in einer Anfrage aus, übernimmt eine andere mit
|
|
demselben Modell. Davon merkt das Programm nichts.
|
|
- Ist das Modell nirgends geladen, gibt es einen **Fehler** — nie eine normale
|
|
Antwort mit Fehlertext. Ein Programm würde so einen Text sonst als Aussage
|
|
der KI in seinen Verlauf schreiben.
|
|
- Mehrere Programme dürfen **gleichzeitig dasselbe Modell** nutzen. Die Karte
|
|
bedient sie parallel; jede einzelne Antwort wird dabei etwas langsamer.
|
|
|
|
## Welcher Zugang?
|
|
|
|
| | A) Cluster über RVS | B) OpenAI-RVS-Wrapper | C) Direkt an eine Box |
|
|
|---|---|---|---|
|
|
| Für | eigene Programme | OpenHands, Continue, Open WebUI … | Sonderfälle |
|
|
| Sieht | alle Modelle, jedes einmal | alle Modelle, jedes einmal | nur diese Box |
|
|
| Verteilung + Ausfall-Ersatz | ja | ja | nein |
|
|
| Braucht | ausgehende Verbindung zum RVS | Wrapper-Container auf demselben Rechner | ausgehende Verbindung zum RVS |
|
|
|
|
---
|
|
|
|
## A) Cluster über RVS — empfohlen
|
|
|
|
### Verbinden
|
|
|
|
```
|
|
wss://<RVS_HOST>:<RVS_PORT>/?token=<RVS_TOKEN>
|
|
```
|
|
|
|
- Das **Token ist der Raum** — dasselbe `RVS_TOKEN` wie bei ARIA und den
|
|
KI-Boxen. Wer es hat, ist im Raum.
|
|
- Der Programm-Rechner braucht nur eine **ausgehende** Verbindung zum RVS.
|
|
Keine Portfreigabe, kein VPN — er kann in jedem Netz stehen.
|
|
- Der RVS ist ein **Verteiler**: Jede Nachricht geht an alle anderen im Raum.
|
|
Du siehst also auch Antworten für andere — ordne sie immer über die
|
|
`requestId` zu.
|
|
- Unbekannte Nachrichtentypen verwirft der RVS **ohne Fehlermeldung**. Die hier
|
|
beschriebenen sind freigegeben.
|
|
- **Nur ein Leser pro Verbindung.** Schickst du mehrere Anfragen gleichzeitig,
|
|
liest eine Schleife alle Nachrichten und verteilt sie per `requestId` an die
|
|
Wartenden (siehe Beispiel). Mehrere gleichzeitige `recv()` auf derselben
|
|
Verbindung gehen nicht.
|
|
|
|
Jede Nachricht hat diese Hülle:
|
|
|
|
```json
|
|
{ "type": "<typ>", "payload": { … }, "timestamp": 1760000000000 }
|
|
```
|
|
|
|
Beantwortet werden `llm_cluster_*`-Nachrichten von der **ARIA-Bridge** — sie
|
|
kennt alle Boxen und entscheidet, wohin eine Anfrage geht. Läuft ARIA nicht,
|
|
kommt keine Antwort (dein Programm sollte einen Timeout haben).
|
|
|
|
### 1. Welche Modelle gibt es? — `llm_cluster_models_request`
|
|
|
|
```json
|
|
{ "type": "llm_cluster_models_request", "payload": { "requestId": "a1" } }
|
|
```
|
|
|
|
Antwort:
|
|
|
|
```json
|
|
{
|
|
"type": "llm_cluster_models",
|
|
"payload": {
|
|
"requestId": "a1",
|
|
"models": [
|
|
{
|
|
"model": "qwen3-8b",
|
|
"ctx": 8192,
|
|
"available": 2,
|
|
"cards": [
|
|
{ "node": "ki-box", "instanceId": "llm@ki-box", "gpu": "1", "gpuName": "NVIDIA GeForce RTX 3060", "state": "ready" },
|
|
{ "node": "werkstatt", "instanceId": "llm@werkstatt", "gpu": "0", "gpuName": "NVIDIA RTX A4000", "state": "ready" }
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
| Feld | Bedeutung |
|
|
|---|---|
|
|
| `model` | Name — genau so in der Anfrage angeben. **Das** zeigst du dem Nutzer zur Auswahl. |
|
|
| `ctx` | **Kontextfenster in Tokens** (Eingabe + Ausgabe zusammen). Das kleinste unter allen Karten mit diesem Modell — darauf kannst du dich verlassen, egal wohin verteilt wird. Nicht mehr annehmen. `0` = unbekannt. **Kann sich ändern** (der Betreiber stellt es pro Karte ein) — vor längeren Aufträgen neu holen. |
|
|
| `available` | auf wie vielen Karten es gerade liegt |
|
|
| `cards` | nur zur Info (Anzeige, Fehlersuche) — für die Anfrage nicht nötig |
|
|
|
|
### 2. Fragen — `llm_cluster_request`
|
|
|
|
```json
|
|
{
|
|
"type": "llm_cluster_request",
|
|
"payload": {
|
|
"requestId": "a2",
|
|
"model": "qwen3-8b",
|
|
"messages": [
|
|
{ "role": "system", "content": "Du antwortest kurz." },
|
|
{ "role": "user", "content": "Wie hoch ist die Zugspitze?" }
|
|
],
|
|
"max_tokens": 256,
|
|
"temperature": 0.7
|
|
}
|
|
}
|
|
```
|
|
|
|
| Feld | Pflicht | Bedeutung |
|
|
|---|---|---|
|
|
| `requestId` | ja | eindeutig pro Anfrage (z.B. UUID) |
|
|
| `model` | ja* | aus `llm_cluster_models`. *Darf fehlen, wenn im Cluster genau ein Modell geladen ist. |
|
|
| `messages` | ja | Verlauf im OpenAI-Format (`system` / `user` / `assistant` / `tool`) |
|
|
| `max_tokens` | nein | Standard 512 |
|
|
| `temperature` | nein | Standard 0.7 |
|
|
| `stop` | nein | Liste von Stop-Strings |
|
|
| `tools` | nein | Tool-Definitionen im OpenAI-Format; die Antwort enthält dann ggf. `tool_calls` |
|
|
|
|
### 3. Antwort — `llm_cluster_response`
|
|
|
|
Erfolg:
|
|
|
|
```json
|
|
{
|
|
"type": "llm_cluster_response",
|
|
"payload": {
|
|
"requestId": "a2",
|
|
"ok": true,
|
|
"content": "Die Zugspitze ist 2962 Meter hoch.",
|
|
"model": "qwen3-8b",
|
|
"node": "werkstatt",
|
|
"gpu": "0",
|
|
"usage": { "prompt_tokens": 31, "completion_tokens": 12, "total_tokens": 43 },
|
|
"elapsedMs": 640,
|
|
"attempts": 1
|
|
}
|
|
}
|
|
```
|
|
|
|
`tool_calls` steht drin, wenn das Modell ein Werkzeug aufrufen will (OpenAI-Format).
|
|
`node`/`gpu` sagen, wer geantwortet hat; `attempts: 2` heißt, die erste Box ist
|
|
ausgefallen und eine andere hat übernommen.
|
|
|
|
Fehler — `ok: false`, dazu `error` (Text für Menschen) und `errorCode`:
|
|
|
|
| `errorCode` | Bedeutung | Was tun |
|
|
|---|---|---|
|
|
| `model_unavailable` | Das Modell ist gerade auf keiner Karte im Cluster geladen. `available` nennt, was es gibt. | Dem Nutzer anzeigen; Liste neu holen. |
|
|
| `bad_request` | Anfrage unvollständig, z.B. `messages` leer oder `model` fehlt bei mehreren Modellen | Anfrage korrigieren |
|
|
| `timeout` | Keine Antwort innerhalb der Frist (Standard 600 s) — auch nach Ersatzversuch | später nochmal |
|
|
| `box_offline` | Box ist während der Anfrage weggefallen, und es gab keine Ersatz-Box | später nochmal |
|
|
| `context_exceeded` | Der Verlauf ist länger als das Kontextfenster. Dazu `promptTokens` (so lang war die Anfrage) und `ctx` (so viel geht). | Verlauf kürzen und nochmal senden — z.B. auf ~80 % von `ctx` abzüglich der gewünschten Antwortlänge. |
|
|
| `model_error` | Das Modell selbst hat einen anderen Fehler gemeldet. Wird nicht anderswo wiederholt — wäre dort genauso. | `error` lesen |
|
|
|
|
**Fehler nie als Antwort der KI behandeln.** Zeig `error` dem Nutzer als
|
|
Fehlermeldung an, aber schreib ihn nicht als Assistant-Nachricht in den
|
|
Verlauf.
|
|
|
|
### Beispiel (Python)
|
|
|
|
Braucht Python 3.11+ und `websockets` (`pip install websockets`). Ein Leser
|
|
pro Verbindung, mehrere Anfragen gleichzeitig möglich.
|
|
|
|
```python
|
|
import asyncio, json, uuid
|
|
import websockets
|
|
|
|
RVS = "wss://rvs.example.org:444/?token=DEIN_RVS_TOKEN"
|
|
|
|
|
|
class AriaCluster:
|
|
"""Eine RVS-Verbindung, ein Leser. Antworten gehen per requestId an die
|
|
wartende Anfrage — der Raum ist ein Verteiler, Fremdes wird ignoriert."""
|
|
|
|
def __init__(self, ws):
|
|
self.ws, self.wait = ws, {}
|
|
self.reader = asyncio.create_task(self._read())
|
|
|
|
async def _read(self):
|
|
async for raw in self.ws:
|
|
msg = json.loads(raw)
|
|
p = msg.get("payload") or {}
|
|
fut = self.wait.get((msg.get("type"), p.get("requestId")))
|
|
if fut and not fut.done():
|
|
fut.set_result(p)
|
|
|
|
async def _call(self, mtype, answer, payload, timeout):
|
|
rid = str(uuid.uuid4())
|
|
fut = asyncio.get_running_loop().create_future()
|
|
self.wait[(answer, rid)] = fut
|
|
try:
|
|
await self.ws.send(json.dumps({"type": mtype, "payload": dict(payload, requestId=rid)}))
|
|
return await asyncio.wait_for(fut, timeout)
|
|
finally:
|
|
self.wait.pop((answer, rid), None)
|
|
|
|
async def models(self):
|
|
res = await self._call("llm_cluster_models_request", "llm_cluster_models", {}, 6)
|
|
return res["models"]
|
|
|
|
async def chat(self, model, messages, timeout=660, **opts):
|
|
res = await self._call("llm_cluster_request", "llm_cluster_response",
|
|
dict(opts, model=model, messages=messages), timeout)
|
|
if not res["ok"]:
|
|
raise RuntimeError(f"{res.get('errorCode')}: {res.get('error')}")
|
|
return res
|
|
|
|
|
|
async def main():
|
|
async with websockets.connect(RVS, max_size=2**24) as ws:
|
|
cluster = AriaCluster(ws)
|
|
models = await cluster.models()
|
|
for m in models:
|
|
print(f"{m['model']} (Kontext {m['ctx']}, auf {m['available']} Karte(n))")
|
|
if models:
|
|
res = await cluster.chat(models[0]["model"],
|
|
[{"role": "user", "content": "Sag Hallo in einem Satz."}],
|
|
max_tokens=64)
|
|
print(res["content"], f"— von {res['node']} / Karte {res['gpu']}")
|
|
|
|
asyncio.run(main())
|
|
```
|
|
|
|
### Umstellen von der direkten Box-Ansprache
|
|
|
|
Wer schon gegen Abschnitt C gebaut hat (`llm_models_request` /
|
|
`llm_request` mit `targetInstance`), stellt so um:
|
|
|
|
1. **Modelliste:** `llm_cluster_models_request` statt `llm_models_request`.
|
|
Statt einer Antwort pro Box kommt **eine** Antwort mit jedem Modell
|
|
einmal. Kein Einsammeln über Sekunden mehr.
|
|
2. **Auswahl:** Dem Nutzer nur den **Modellnamen** zeigen — nicht mehr
|
|
„Modell · Karte · Box". Wo es läuft, entscheidet der Cluster.
|
|
3. **Kontextfenster:** `ctx` aus der Liste übernehmen statt einen eigenen Wert
|
|
einzustellen — und **danach kürzen**, nicht nur anzeigen. Die Box gibt nur
|
|
so viel her, wie dort steht; rechnet ein Programm mit mehr, kommt
|
|
`context_exceeded`. Den Fehler zusätzlich abfangen: kürzen und nochmal
|
|
senden (`promptTokens`/`ctx` stehen dabei). Tokens grob schätzen reicht
|
|
nicht immer — Code und Deutsch brauchen mehr Tokens pro Zeichen als
|
|
englischer Fließtext.
|
|
4. **Anfrage:** `llm_cluster_request` statt `llm_request`; **kein**
|
|
`targetInstance` mehr. Die Antwort kommt als `llm_cluster_response`.
|
|
5. **Fehler:** `ok: false` als Fehler anzeigen, nicht als Antwort in den
|
|
Verlauf. Neue Codes siehe Tabelle oben.
|
|
6. **Lesen:** ein Leser pro Verbindung, Antworten per `requestId` zuordnen.
|
|
|
|
Felder und Format von `messages`, `tools`, `tool_calls` und `usage` bleiben
|
|
gleich.
|
|
|
|
---
|
|
|
|
## B) OpenAI-RVS-Wrapper — für Programme, die nur OpenAI können
|
|
|
|
Ein kleiner Container, der **auf demselben Rechner** wie das Programm läuft.
|
|
Für das Programm ist der Cluster dann ein ganz normaler OpenAI-Endpunkt:
|
|
|
|
```
|
|
Programm ──HTTP──▶ Wrapper (127.0.0.1:8099) ──RVS──▶ ARIA-Bridge ──▶ KI-Box / Karte
|
|
```
|
|
|
|
Liegt im Repo unter [`xtts/openai_rvs_wrapper/`](../xtts/openai_rvs_wrapper/):
|
|
|
|
```bash
|
|
cd xtts/openai_rvs_wrapper
|
|
cp .env.example .env # RVS_HOST, RVS_PORT, RVS_TOKEN eintragen
|
|
docker compose up -d --build
|
|
```
|
|
|
|
Im Browser zeigt **http://127.0.0.1:8099/** eine Hilfsseite: ob die Verbindung
|
|
steht und welche Modelle es gibt. Im Programm eintragen:
|
|
|
|
| Feld | Wert |
|
|
|---|---|
|
|
| Base URL | `http://127.0.0.1:8099/v1` |
|
|
| API-Key | beliebig — oder `WRAPPER_API_KEY`, falls gesetzt |
|
|
| Modell | ein Name von der Hilfsseite |
|
|
| Kontextfenster | der Wert von der Hilfsseite |
|
|
|
|
- `GET /v1/models` — jedes Modell einmal, mit `context_length`
|
|
- `POST /v1/chat/completions` — auch `stream: true` (kommt derzeit in einem
|
|
Rutsch) und `tools`
|
|
- Fehler als HTTP-Fehler mit OpenAI-Fehlerobjekt: nicht geladenes Modell →
|
|
404 `model_not_found`, Cluster nicht erreichbar → 503, Zeitüberschreitung → 504
|
|
- Antwort-Header `X-Aria-Node` / `X-Aria-Gpu` sagen, wer geantwortet hat
|
|
- **Läuft das Programm selbst in Docker**, ist `127.0.0.1` dort der eigene
|
|
Container. Dann das Docker-Netz `aria-llm` anhängen und
|
|
`http://openai-rvs-wrapper:8099/v1` nehmen (Details in der README des Wrappers).
|
|
|
|
Mit dem offiziellen OpenAI-Python-SDK getestet (`models.list`,
|
|
`chat.completions.create` mit und ohne Stream, Fehlerklassen).
|
|
|
|
---
|
|
|
|
## C) Direkt an eine Box — Sonderfall
|
|
|
|
Ohne den Cluster, eine bestimmte Box gezielt. Keine Verteilung, kein
|
|
Ausfall-Ersatz. Nur nötig, wenn ein Programm bewusst eine bestimmte Box will
|
|
oder ARIA nicht läuft.
|
|
|
|
**Modelle der Boxen:** `llm_models_request` `{requestId}` → **jede** Box
|
|
antwortet einzeln mit `llm_models` `{requestId, instanceId, node, models:
|
|
[{model, gpu, gpuName, ctx, state}]}`. Ein paar Sekunden einsammeln.
|
|
|
|
**Anfrage:** `llm_request` `{requestId, targetInstance, model, messages, …}`
|
|
→ `llm_response` `{requestId, ok, content, tool_calls, model, instanceId,
|
|
node, gpu, usage, elapsedMs}`. Bei Fehler `errorCode`:
|
|
`model_not_loaded` (mit `loadedModels`) oder `no_model_loaded`.
|
|
|
|
Ohne `targetInstance` antworten nur Boxen, die das Modell geladen haben — die
|
|
anderen schweigen. Hat keine es, kommt **keine** Antwort. Darum hier immer
|
|
`targetInstance` setzen.
|
|
|
|
**OpenAI-Port einer Box:** Jede Box hat zusätzlich einen eigenen
|
|
OpenAI-Endpunkt (`http://<IP der Box>:8081/v1`, Standard nur `localhost` der
|
|
Box). Er sieht nur diese Box; nicht geladene Modelle → HTTP 404. Für mehrere
|
|
Boxen den Wrapper (B) nehmen.
|
|
|
|
---
|
|
|
|
## Was Programme nicht senden
|
|
|
|
`llm_provision_model`, `llm_unload_model` und `llm_remove_model` sind der
|
|
ARIA-Diagnostic vorbehalten. Technisch könnte jeder im Raum sie senden — der
|
|
RVS unterscheidet die Teilnehmer nicht. Ein Programm, das sich nicht daran
|
|
hält, tauscht anderen das Modell unter den Füßen weg.
|
|
|
|
## Gleichzeitige Nutzung und Geschwindigkeit
|
|
|
|
- Mehrere Anfragen an dasselbe Modell laufen auf einer Karte **parallel**.
|
|
Jede einzelne wird etwas langsamer, in Summe schafft die Karte mehr.
|
|
- Liegt ein Modell auf mehreren Karten, schickt der Cluster neue Anfragen an
|
|
die Karte mit den wenigsten laufenden.
|
|
- Der Kontext (`ctx`) ist bei aktuellen llama.cpp-Versionen ein **gemeinsamer
|
|
Vorrat** aller gleichzeitigen Anfragen auf einer Karte. Schicken mehrere
|
|
Programme gleichzeitig sehr lange Verläufe, wird es dort eng.
|
|
|
|
## Wenn sich etwas ändert
|
|
|
|
- Lädt der Betreiber ein anderes Modell in eine Karte, ist das alte dort weg.
|
|
Liegt es noch auf einer anderen Karte, merkt ein Programm nichts; sonst kommt
|
|
`model_unavailable` — dann die Liste neu holen.
|
|
- Beim Laden oder Entladen startet die betroffene Box **alle** ihre Modelle
|
|
kurz neu (wenige Sekunden). Anfragen in diesem Moment warten oder gehen an
|
|
eine andere Box.
|
|
- Diese Schnittstelle wird erweitert, nicht umgebaut: Neue Felder können
|
|
dazukommen. Unbekannte Felder ignorieren.
|