Die Inferenz-Runtime
Eigene Laufzeitumgebung einbinden
Verbindung zu bereits laufenden Instanzen von llama.cpp, Ollama oder vLLM herstellen, Grammatikerzwingung als Voraussetzung, Fallstricke bei der Einrichtung pro Laufzeitumgebung
Diese Seite wurde maschinell aus der englischen Dokumentation übersetzt.
Wenn du bereits llama.cpp, Ollama oder etwas anderes betreibst, das das OpenAI-Protokoll versteht, richte diesen Dienst darauf aus. In diesem Modus lädt er kein Modell herunter und startet keine zweite Kopie davon auf deiner Hardware.
Prüfe die Support-Matrix, bevor du dich auf eine Runtime festlegst: Diese Pipeline benötigt grammatikgebundenes Decoding, und nicht alles, was mit OpenAI-Kompatibilität wirbt, liefert das. Bei llama.cpp, Ollama und vLLM auf einer GPU wurde die Funktion nachgewiesen. Der CPU-Build von vLLM stürzt beim ersten Scan ab: dieselbe Version, kein Beschleuniger, abgestürzter Worker.
Der Weg eines Scans ist kurz, und die Grammatik liegt genau in der Mitte. Der Browser sendet ein Foto an diesen Dienst, der den Schlüssel prüft, die Anfrage annimmt, das Bild herunterskaliert und einen einzelnen Vision-Aufruf unter einem JSON-Schema ausführt. Deine Runtime ist die einzige Stelle, die dieses Schema erzwingt, weshalb sich die Tabelle unten um diese Durchsetzung dreht und nicht um Geschwindigkeit. Zurück kommen Namen und Gramm; die Makronährstoffe werden danach aus der konfigurierten Lebensmittelquelle nachgeschlagen, und das Modell schreibt nie einen davon.
Quellcode des Diagramms
flowchart LR
browser["Browser"]
subgraph svc["openplate-inference"]
gate["Key, rate limit, admission"]
prep["Downscale to 896 px"]
vision["One vision call, json_schema"]
macros["Look up macros, food source"]
end
runtime["Your model runtime"]
browser -->|"photo, POST /v1/chat/completions"| gate
gate --> prep --> vision
vision -->|"grammar constrained decoding"| runtime
runtime -->|"names and grams, no macros"| macros
macros -->|"one plate, as JSON"| browserdocker run -d --name openplate-inference \
-p 8300:8300 \
-e MODEL_PROFILE=external \
-e MODEL_RUNTIME_URL=http://your-runtime.lan:8000 \
-e MODEL_ID=your-served-model-name \
-e API_KEYS=opk_your_key \
ghcr.io/lowcarbcheck/openplate-inference:latestMODEL_PROFILE=external reicht als Schalter völlig aus. Es überspringt den Download der Gewichte und startet kein llama-server, sodass es kein /models-Volume gibt.
Variablen für den externen Modus
MODEL_RUNTIME_URL: kein abschließender/v1, der Dienst hängt die OpenAI-Pfade selbst an. Die Auflösung muss aus dem Container heraus gelingen, daher bedeutetlocalhostden Container, nicht deinen Host. Das Setzen auf eine andere-Adresse ohneMODEL_PROFILE=externalist ein Startfehler und kein stilles Überschreiben: Ein gebündelter Container bedient Anfragen immer über sein eigenes Loopback. (Das Setzen auf exakt die gebündelte Loopback-Adresse ist erlaubt und ändert nichts: Ältere Kopien von.env.examplelieferten diese Zeile einkommentiert aus.)MODEL_ID: wird an deine Runtime gesendet. llama.cpp ignoriert dies, aber vLLM und Ollama benötigen beide den genauen Namen des bereitgestellten Modells: vLLM weist eine Abweichung ab, und Ollama wählt und lädt damit das Modell. Die ID, die Clients nutzen, ist unabhängig davon immeropenplate-plate-1.MODEL_RUNTIME_API_KEY: optional, wird alsAuthorization: Bearer …an deine Runtime gesendet. Setze dies fürvllm serve --api-key …oder einen Authentifizierungs-Proxy. Dies ist getrennt vonAPI_KEYS, was Aufrufer an diesen übergeben.RUNTIME_COMPLETION_TIMEOUT_MS: Gesamtlaufzeitgrenze für einen Completion-Aufruf in Millisekunden. Standardwert600000(10 Minuten);0schaltet sie ab. Dies greift in im mitgelieferten Modus: Ein blockierterllama-serverantwortet genauso wenig wie ein falsch gerouteter Proxy. Wenn deine Hardware berechtigterweise länger als zehn Minuten pro Teller braucht, erhöhe den Wert oder setze ihn auf0; die Fehlermeldung nennt die Variable.Das ist nicht
LATENCY_CEILING_MS. Jenes ist Zugangskontrolle: Lehne Arbeit ab, die du nicht rechtzeitig beenden kannst, bevor du damit beginnst. Dies hier ist Lebendigkeitsprüfung: Gib einen Worker-Slot frei, den ein Upstream-Dienst niemals freigeben wird.Eine Begrenzung griff bereits vor der Existenz dieser Variable: Nodes
fetchsetztheadersTimeoutstandardmäßig auf 300 s, und da eine nicht-streamende Completion ihre Header erst nach Ende der Generierung schreibt, wirkt dies wie ein 300-s-Gesamtlipid, das auf von diesem Projekt unterstützter Hardware erreichbar ist. Das explizite Setzen vonRUNTIME_COMPLETION_TIMEOUT_MSlockert diesen Standardwert.Es bedeutet nicht „ewig warten“, denn bei
CONCURRENCY=2belegen zwei blockierte, laufende Scans dauerhaft beide Worker-Slots, während/readyzgrün bleibt: Die Readiness-Probes fragen einen anderen Endpunkt ab und sehen das Problem nicht.
Deine Laufzeitumgebung muss grammatikgesteuertes Decodieren erzwingen
Das ist eine feste Voraussetzung. Die Pipeline setzt einen einzelnen Vision-Aufruf mit response_format: {"type": "json_schema", "json_schema": {"name": …, "strict": true, "schema": …}} ab und verlässt sich auf die Struktur der Antwort. Es gibt keine Schleife zum Parsen mit erneutem Versuch.
Der gefährliche Fehler ist nicht die Ablehnung, sondern das Akzeptieren und Ignorieren: eine Runtime, die das Feld response_format annimmt, verwirft und plausibles JSON zurückgibt, das die Vorgaben verfehlt. Das äußert sich nicht als Fehler. Es äußert sich in leicht falschen Grammschätzungen.
Prüfe deine Umgebung mit einem einzigen Befehl vor der Inbetriebnahme. Verlange Fließtext bei gleichzeitig gefordertem Schema; entspricht die Antwort dennoch dem Schema, greift die Grammatik tatsächlich:
curl -s http://your-runtime.lan:8000/v1/chat/completions \
-H 'Content-Type: application/json' \
-d '{"model":"your-model","messages":[{"role":"user","content":"Write a haiku about rain."}],
"response_format":{"type":"json_schema","json_schema":
{"name":"t","strict":true,"schema":{"type":"object","properties":{"x":{"type":"string"}},
"required":["x"],"additionalProperties":false}}}}'Ein JSON-Objekt mit dem Schlüssel x bedeutet, dass die Erzwingung funktioniert. Erhältst du ein Haiku, funktioniert sie nicht, und dieser Dienst liefert bei jedem Scan den Fehler 502 mit einem entsprechenden Hinweis zurück.
Kompatibilitätsübersicht
Selbst gemessen am 15.08.2026. Zeilen, die wir nicht selbst getestet haben, sind entsprechend gekennzeichnet.
| Laufzeitumgebung | Version | Grammatik | Bildeingabe | Ergebnis |
|---|---|---|---|---|
llama.cpp (llama-server) | b10330 | erzwungen (GBNF) | Base64-Data-URL | ✅ funktioniert, dies führt das mitgelieferte Image aus |
| Ollama | 0.32.13 | erzwungen | Base64-Data-URL | ✅ funktioniert, siehe den Hinweis zur Betriebsbereitschaft unten |
| vLLM (CPU-Build) | 0.27.1 | nichts | nichts | ❌ kaputt: Ein Request an json_schema schlägt fehl (bringt den Server zum Absturz, pin_memory=True requires a CUDA or other accelerator backend). Einfache Textvervollständigungen funktionieren, daher wirkt der Dienst fehlerfrei, bis der erste echte Scan den Prozess beendet. |
| vLLM (GPU-Build) | 0.27.1 | erzwungen (xgrammar) | Base64-Data-URL | ✅ funktioniert, gemessen auf einer RTX 3090 mit Qwen3-VL-2B-Instruct |
| alles andere | nichts | nichts | nichts | ⚠️ ungetestet, führe den obigen curl-Befehl aus |
Gleiche Version, gegensätzliche Ergebnisse: Es liegt spezifisch am CPU-Build. Der GPU-Test lief mit demselben identischem vLLM-Image, das auf der CPU abstürzt (vllm/vllm-openai:latest und v0.27.1 teilen sich einen Digest), mit demselben Schema und demselben Base64-Bild, das dieser Dienst sendet. Auf der GPU lieferte es schemakonformes JSON in 1,8 s und blieb erreichbar. Verstehe die ❌-Zeile als „Betreibe vLLM nicht ohne GPU“, nicht als „vLLM wird nicht unterstützt“.
Warum der CPU-Build abstürzt. Grammatikgestützte Decodierung baut eine Token-Bitmaske auf und allokiert sie im fixiert-Speicher, einem seitenfixierten Puffer für schnelles Kopieren auf eine GPU. Fordert man das ohne Beschleuniger an, wirft PyTorch einen Fehler, statt ihn zu ignorieren. Da dies während der Ausführung und nicht bei der Validierung geschieht, stürzt der Worker ab, anstatt einen Fehler zurückzugeben. Das ist ein Upstream-Bug: llama.cpp und Ollama verarbeiten dieselbe Nutzlast fehlerfrei.
Eine Eigenheit von vLLM, die man kennen sollte. Bei einem Prompt, den das Modell nicht in der geforderten Struktur beantworten kann, gab es { gefolgt von Whitespace aus, bis das Token-Limit erreicht war: Die Grammatik greift (es entstand nie Fließtext, selbst bei expliziter Aufforderung zu einem Haiku), aber das Modell füllt den unbeschränkten Leerraum statt des Inhalts. Das äußert sich hier als Fehler finish_reason: length, der das Token-Limit nennt. Ein echtes Tellerfoto löste dies nicht aus, ein themenfremder Prompt hingegen schon.
Wenn du eine Kombination nutzt, die wir nicht getestet haben, erstelle bitte ein Issue mit dem Ergebnis.
Drei Konfigurationsfallen
1. Kontextfenster: Das Symptom ist verstümmelte Ausgabe, kein Startfehler. Ein zu kleines Kontextfenster schlägt nicht lautstark fehl. Es schneidet den Anfang deines Prompts ab, und du erhältst plausibel wirkenden Unsinn.
Die Prompt-Größe hängt von deinem Modell ab. Zwei direkte Messungen der auf selben 896 px herunterskalierten Teller:
| Modell | Prompt-Tokens (896-px-Teller) |
|---|---|
| Qwen3-VL-8B auf llama.cpp | 1.287 bis 3.507 über den Korpus von 10 Tellern |
| moondream auf Ollama | 746 |
Leite deine Dimensionierung nicht aus diesen Zahlen ab, lies deine eigenen aus. Führe einen Scan gegen deine Laufzeitumgebung aus und prüfe deren Rückmeldung:
curl -s .../v1/chat/completions -d '…' | jq .usage.prompt_tokensDimensioniere das Fenster mit ausreichend Puffer über deinem Worst Case. Bei vLLM ist das --max-model-len. Bei Ollama ist es num_ctx: Setze den Wert in einem Modelfile, da OLLAMA_CONTEXT_LENGTH=8192 in unseren Messungen das gemeldete Kontextfenster eines Modells nicht über 2048 vergrößern konnte (Ollama begrenzt dies offenbar auf den Trainingskontext des Modells; einem Modell mit kleinem Trainingsfenster lässt sich also kein größeres zuweisen).
2. CONCURRENCY muss mit der tatsächlichen Slot-Anzahl deiner Laufzeitumgebung übereinstimmen. Mehr gleichzeitige Anfragen, als die Laufzeitumgebung Slots hat, bringen keinen Durchsatzgewinn: Die Warteschlange verschiebt sich an eine Stelle, die dieser Dienst weder einsehen noch messen kann, und die Zulassungssteuerung trifft Entscheidungen auf falscher Grundlage.
| Laufzeitumgebung | Das Flag, das Slots festlegt |
|---|---|
| llama.cpp | --parallel N |
| vLLM | --max-num-seqs N |
| Ollama | OLLAMA_NUM_PARALLEL=N |
3. Das HEALTHCHECK des Containers erlaubt 60 Minuten für den Start. Das ist für das Herunterladen der Modellgewichte beim ersten Start über einen Heimanschluss bemessen. Im externen Modus gibt es keinen Download, weshalb ein falsches MODEL_RUNTIME_URL eine Stunde lang unbemerkt bleibt, statt nach wenigen Sekunden abzubrechen. start_period ist fest im Build verankert, daher überschreibe es: Siehe den auskommentierten Block in docker/compose.yml.
Readiness und was sie dir nicht verrät
/readyz fragt deine Laufzeitumgebung GET /health und weicht auf GET /v1/models für Laufzeitumgebungen ohne /health aus (Ollama hat keines). Zwei Einschränkungen:
- Gegenüber Ollama bedeutet
/v1/modelsLiveness, nicht Readiness. Es beantwortet200ganz ohne geladene Modelle (Ollama lädt erst verzögert beim ersten Request), ein Status von „ready“ bedeutet also lediglich „erreichbar“, nicht „betriebsbereit“. Dein erster Scan fängt die Ladezeit auf. /readyzkannMODEL_RUNTIME_API_KEYnicht validieren, wenn/healthoffen ist. Bei vLLM ist/healthnicht authentifiziert,/v1/chat/completionshingegen schon. Ein falscher Schlüssel meldet daher ready und schlägt bei jedem Scan mit 502 fehl. Wenn Scans gegen einen betriebsbereiten Dienst 502 liefern, prüfe zuerst den Schlüssel.
Ein 503 von /health aus llama.cpp bedeutet lädt und wird als nicht bereit gemeldet: Es wird nie so behandelt, als habe diese Laufzeitumgebung kein /health.
/readyz meldet auch die Laufzeitumgebung für optional-Embeddings als dreistufiges Feld, das den Statuscode nie beeinflusst (rein lexikalisches Abrufen ist der Standard, kein Ausfall):
embeddingReady | Bedeutung |
|---|---|
null | EMBEDDING_RUNTIME_URL ist nicht gesetzt. Retrieval ist per Konfiguration rein lexikalisch. Normalzustand. |
true | Konfiguriert und antwortet. Retrieval ist hybrid. |
false | Konfiguriert und schlägt fehl: embeddingReason nennt den Grund (z. B. http 401). Das Ranking bleibt bis zur Behebung schlechter. |
embeddingReason ist null, solange kein Problem vorliegt, daher ist ein Alerting auf embeddingReason !== null sicher. Ein falsches EMBEDDING_RUNTIME_API_KEY taucht hier und nirgendwo sonst auf: Es verschlechtert das Ranking, anstatt Scans fehlschlagen zu lassen.
Liefert deine Laufzeitumgebung gute Antworten?
Kompatibilität ist keine Genauigkeit. Eine Laufzeitumgebung kann das Schema perfekt einhalten und trotzdem mit einem Modell gekoppelt sein, das Teller schlecht erkennt. eval/run-50img.sh bewertet jeden Endpunkt anhand desselben Gold-Standards aus 50 Bildern, aus dem alle Zahlen dieser Dokumentation stammen: Richte das Werkzeug auf deinen Endpunkt, bevor du den Ausgaben vertraust.
Serving-Hinweise zu den einzelnen Modellen (Flags, Eigenheiten und gemessenes Verhalten für die getesteten Modelle) findest du unter eval/SERVING.md.