Il runtime di inferenza
Usa il tuo runtime
Collega questo componente a un'istanza di llama.cpp, Ollama o vLLM già in esecuzione, il requisito di applicazione della grammatica, i trabocchetti di configurazione specifici di ogni runtime
Questa pagina è tradotta automaticamente dalla documentazione in inglese.
Se usi già llama.cpp, Ollama o qualsiasi altro strumento che supporti il protocollo OpenAI, punta questo servizio verso di esso. In questa modalità non scarica alcun modello e non ne avvia una seconda copia sul tuo hardware.
Controlla la matrice di supporto prima di scegliere un runtime: questa pipeline richiede una decodifica vincolata da grammatica, e non tutto ciò che dichiara compatibilità con OpenAI la supporta davvero. llama.cpp, Ollama e vLLM su una GPU risultano tutti funzionanti nei test. la build per CPU di vLLM va in crash alla prima scansione: stessa versione, nessun acceleratore, processo in stallo.
Il percorso di una scansione è breve, e la grammatica ne fa parte integrante. Il browser invia una foto a questo servizio, che verifica la chiave, accetta la richiesta, riduce la risoluzione dell'immagine ed esegue una singola chiamata vision con uno schema JSON. Il tuo runtime è l'unico componente che applica tale schema, ed è per questo che la matrice seguente riguarda il supporto dei vincoli piuttosto che la velocità. La risposta contiene nomi e grammi; i macronutrienti vengono cercati dopo, dalla sorgente alimentare configurata, e il modello non ne scrive mai alcuno.
Sorgente del diagramma
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 è l'unico selettore. Salta lo scaricamento dei pesi e non avvia alcun llama-server, quindi non c'è alcun volume /models.
Variabili per la modalità esterna
MODEL_RUNTIME_URL: niente/v1finale, il servizio aggiunge da sé i percorsi di OpenAI. La risoluzione deve avvenire da all'interno del container, quindilocalhostindica il container, non il tuo host. Impostarlo su un indirizzo diversa senzaMODEL_PROFILE=externalproduce un errore di avvio invece di una sovrascrittura silenziosa: un container integrato risponde sempre dal proprio loopback. (Impostarlo esattamente sull'indirizzo di loopback integrato è consentito e non cambia nulla: le copie più vecchie di.env.exampleincludevano quella riga non commentata).MODEL_ID: inviato al tuo runtime. llama.cpp lo ignora, ma sia vLLM sia Ollama richiedono il nome esatto del modello servito: vLLM rifiuta le discrepanze, mentre Ollama lo usa per selezionare e caricare il modello. L'ID usato dai client resta comunqueopenplate-plate-1in ogni caso.MODEL_RUNTIME_API_KEY: opzionale, inviato comeAuthorization: Bearer …al tuo runtime. Impostalo pervllm serve --api-key …o per un proxy di autenticazione. È separato daAPI_KEYS, che è ciò che i chiamanti presentano a questo servizio.RUNTIME_COMPLETION_TIMEOUT_MS: limite complessivo per una singola chiamata di completamento, in millisecondi. Valore predefinito600000(10 minuti);0lo disabilita. Si applica anche in modalità integrata: unllama-serverbloccato resta muto tanto quanto un proxy instradato male. Se il tuo hardware richiede legittimamente più di dieci minuti per piatto, aumentalo o impostalo su0; il messaggio di errore indica il nome della variabile.Questo non è
LATENCY_CEILING_MS. Quello riguarda la politica di ammissione: rifiutare il lavoro che non puoi completare in tempo, prima ancora di iniziarlo. Questo riguarda la vitalità: liberare uno slot di un worker che un upstream non restituirà mai.Un limite era già attivo prima dell'esistenza di questa variabile: l'oggetto
fetchdi Node impostaheadersTimeouta 300 s come valore predefinito, e dato che un completamento senza streaming scrive le intestazioni solo al termine della generazione, questo funge da limite totale di 300 s, raggiungibile sull'hardware supportato da questo progetto. L'impostazione esplicita diRUNTIME_COMPLETION_TIMEOUT_MSallenta tale valore predefinito.Non è un "attendi all'infinito" perché con
CONCURRENCY=2, due scansioni bloccate in corso occupano entrambi gli slot dei worker in modo permanente mentre/readyzrimane verde: le verifiche di idoneità interrogano un endpoint diverso e non possono rilevarlo.
Il tuo runtime di inferenza deve applicare la decodifica vincolata dalla grammatica
Questo è un requisito vincolante. La pipeline esegue una sola chiamata di visione con response_format: {"type": "json_schema", "json_schema": {"name": …, "strict": true, "schema": …}} e si fida della struttura restituita. Non esiste alcun ciclo di analisi e nuovo tentativo.
Il fallimento pericoloso non è il rifiuto, ma l'accettazione con omissione: un runtime che riceve il campo response_format, lo ignora e restituisce un JSON plausibile ma non conforme al contratto previsto. Questo non emerge come errore, bensì come stime in grammi leggermente errate.
Verifica il tuo con un solo comando prima di fidarti. Chiedi un testo discorsivo imponendo al contempo uno schema; se la risposta rispetta comunque lo schema, la grammatica è effettivamente attiva:
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}}}}'Un oggetto JSON con una chiave x indica che il vincolo funziona. Un haiku indica che non funziona, e questo servizio restituirà un errore 502 a ogni scansione con un messaggio che lo segnala.
Matrice di supporto
Misurato in prima persona il 2026-08-15. Le righe che non abbiamo eseguito direttamente lo indicano esplicitamente.
| Runtime | Versione | Grammatica | Input immagine | Verdetto |
|---|---|---|---|---|
llama.cpp (llama-server) | b10330 | imposta (GBNF) | data URL base64 | ✅ funziona, è ciò che esegue l'immagine inclusa |
| Ollama | 0.32.13 | imposta | data URL base64 | ✅ funziona, vedi la nota sulla disponibilità qui sotto |
| vLLM (build CPU) | 0.27.1 | nessuno | nessuno | ❌ non funziona: una richiesta json_schema termina il server (pin_memory=True requires a CUDA or other accelerator backend). I completamenti di solo testo funzionano, quindi il servizio sembra attivo finché la prima scansione reale non blocca il processo |
| vLLM (build GPU) | 0.27.1 | imposta (xgrammar) | data URL base64 | ✅ funziona, misurato su una RTX 3090 con Qwen3-VL-2B-Instruct |
| qualsiasi altra configurazione | nessuno | nessuno | nessuno | ⚠️ non testato, esegui il comando curl qui sopra |
Stessa versione, esiti opposti: il problema riguarda nello specifico la build CPU. Il test su GPU ha usato la identico immagine vLLM che fallisce su CPU (vllm/vllm-openai:latest e v0.27.1 condividono lo stesso digest), con lo stesso schema e la stessa immagine base64 inviati da questo servizio. Su GPU ha restituito JSON conforme allo schema in 1.8 s ed è rimasto attivo. Interpreta la riga con ❌ come "non eseguire vLLM senza una GPU", non come "vLLM non è supportato".
Perché la versione CPU si arresta. La decodifica vincolata da grammatica genera una maschera di bit di token e la alloca nella memoria bloccata in memoria, un buffer con pagine bloccate pensato per la copia rapida verso una GPU. Se la richiedi senza un acceleratore presente, PyTorch solleva un'eccezione invece di ignorarla, e poiché questo avviene durante la gestione delle richieste e non durante la convalida, il worker si arresta invece di restituire un errore. Si tratta di un bug a monte: llama.cpp e Ollama gestiscono entrambi correttamente lo stesso payload.
Una particolarità di vLLM che vale la pena conoscere. Con un prompt a cui il modello non può rispondere nella forma richiesta, abbiamo notato che emette { seguito da spazi vuoti finché non raggiunge il limite di token: la grammatica regge (non ha mai prodotto testo libero, nemmeno quando abbiamo chiesto esplicitamente un haiku), ma il modello riempie gli spazi vuoti non vincolati invece del contenuto. Qui il problema si manifesta con l'errore finish_reason: length, che indica il limite di token. Una vera foto del piatto non lo ha attivato; un prompt fuori contesto sì.
Se esegui una configurazione che non abbiamo testato, apri una issue con il risultato.
Tre insidie di configurazione
1. Finestra di contesto: il sintomo è un output corrotto, non un errore di avvio. Una finestra troppo piccola non causa un errore esplicito. Elimina la parte iniziale del tuo prompt e ottieni testo privo di senso generato con sicurezza.
La dimensione del prompt dipende dal tuo modello. Due misurazioni dirette sulle foto del piatto ridotte a stesso 896 px:
| Modello | Token del prompt (piatto da 896 px) |
|---|---|
| Qwen3-VL-8B su llama.cpp | Da 1.287 a 3.507 sul corpus di 10 piatti |
| moondream su Ollama | 746 |
Non impostare le dimensioni basandoti su questi numeri: leggi i tuoi. Esegui una scansione sul tuo runtime e controlla ciò che restituisce:
curl -s .../v1/chat/completions -d '…' | jq .usage.prompt_tokensConfigura la finestra oltre il tuo caso peggiore, con un margine di sicurezza. Su vLLM questo valore è --max-model-len. Su Ollama è num_ctx: impostalo in un Modelfile, perché abbiamo verificato che OLLAMA_CONTEXT_LENGTH=8192 non riesce ad aumentare il contesto dichiarato di un modello oltre 2048 (sembra che Ollama limiti il valore al contesto di addestramento del modello stesso, quindi a un modello con una finestra di addestramento piccola non se ne può assegnare una più grande).
2. CONCURRENCY deve corrispondere al numero reale di slot del tuo runtime. Avere più richieste in corso di quanti siano gli slot del runtime non aumenta la capacità di elaborazione: sposta la coda in un punto che questo servizio non può vedere o misurare, e il controller di ammissione prende decisioni basandosi su una situazione non reale.
| Runtime | Il flag che imposta gli slot |
|---|---|
| llama.cpp | --parallel N |
| vLLM | --max-num-seqs N |
| Ollama | OLLAMA_NUM_PARALLEL=N |
3. L'opzione HEALTHCHECK del container concede 60 minuti per l'avvio. Il tempo è calibrato per scaricare i pesi al primo avvio su una connessione domestica. In modalità esterna non c'è alcun download, quindi un valore errato di MODEL_RUNTIME_URL rimane nascosto per un'ora invece di fallire in pochi secondi. start_period è integrato in fase di compilazione, quindi sovrascrivilo: vedi il blocco commentato in docker/compose.yml.
Stato pronto, e cosa non ti dice
/readyz interroga il tuo runtime con GET /health, e ripiega su GET /v1/models per i runtime privi di /health (Ollama non lo ha). Due limitazioni:
- Con Ollama,
/v1/modelsindica l'attività, non la disponibilità effettiva. Risponde200con zero modelli residenti in memoria (Ollama li carica in modo lazy alla prima richiesta), quindi lo stato pronto significa "raggiungibile", non "pronto all'uso". La tua prima scansione assorbe il tempo di caricamento. /readyznon può convalidareMODEL_RUNTIME_API_KEYquando/healthè aperto. Su vLLM,/healthnon richiede autenticazione mentre/v1/chat/completionssì. Una chiave errata restituisce quindi pronto e fa fallire ogni scansione con un 502. Se le scansioni restituiscono 502 verso un servizio pronto, controlla prima di tutto la chiave.
Un 503 da /health di llama.cpp indica in caricamento e viene segnalato come non pronto: non viene mai interpretato come "questo runtime non ha alcun /health".
/readyz segnala anche il runtime degli embedding per opzionale, tramite un campo a tre stati che non influisce mai sul codice di stato (il recupero puramente lessicale è il comportamento predefinito, non un disservizio):
embeddingReady | Significato |
|---|---|
null | EMBEDDING_RUNTIME_URL non è impostata. Il recupero è puramente lessicale per configurazione. Normale. |
true | Configurato e funzionante. Il recupero è ibrido. |
false | Configurato e in errore: embeddingReason ne spiega il motivo (ad es. http 401). L'ordinamento è peggiore finché non viene risolto. |
embeddingReason è null ogni volta che non ci sono problemi, quindi impostare un avviso su embeddingReason !== null è sicuro. Una EMBEDDING_RUNTIME_API_KEY errata compare qui e da nessun'altra parte: peggiora l'ordinamento invece di far fallire le scansioni.
Il tuo runtime fornisce buone risposte?
La compatibilità non equivale all'accuratezza. Un runtime può rispettare lo schema alla perfezione ed essere comunque abbinato a un modello che interpreta male i piatti. eval/run-50img.sh valuta qualsiasi endpoint rispetto allo stesso insieme di riferimento di 50 immagini da cui provengono tutti i numeri in questi documenti: puntalo verso il tuo prima di fidarti dell'output.
Per le note di servizio specifiche per modello (flag, particolarità e comportamento misurato per i modelli che abbiamo eseguito), vedi eval/SERVING.md.