Perché il tuo LLM self-hosted rallenta con 5 utenti
Con un utente funziona, con cinque rallenta: batching, cache KV, prefill e coda spiegano quante richieste può gestire davvero il tuo server LLM.
Perché un LLM self-hosted rallenta quando arrivano più utenti?
Un LLM self-hosted si blocca con 5 utenti simultanei perché il server sta ancora generando una risposta alla volta e gli altri quattro sono in coda. La documentazione di Ollama è esplicita sul valore predefinito: OLLAMA_NUM_PARALLEL è «il numero massimo di richieste parallele che ogni modello elabora contemporaneamente; il valore predefinito è 1». Non c'è alcun guasto. Quattro delle cinque persone stanno aspettando il proprio turno.
La soluzione raramente consiste nell'usare un server più potente. Serve un motore di serving che elabori molte richieste nello stesso forward pass, oltre a una quantità sufficiente di memoria libera per mantenere le conversazioni di tutti durante l'elaborazione. Entrambi gli aspetti sono importanti, ma è il secondo a determinare il limite effettivo.
Le due fasi attraversate da ogni richiesta
Il prefill legge l’intero prompt in una sola volta e crea la cache dell’attenzione corrispondente. Ogni token del prompt attraversa il modello insieme agli altri. Per questo il prefill consiste in una grande moltiplicazione tra matrici ed è limitato dalla capacità di calcolo aritmetico. Il decode genera quindi la risposta un token alla volta. Per ogni token è necessario rileggere dalla memoria tutti i pesi del modello, mentre le operazioni aritmetiche eseguite su quel singolo token sono minime. Il decode è quindi limitato dalla larghezza di banda della memoria.
Questa asimmetria spiega perché il batching è efficace. Il decode di un singolo utente legge, ad esempio, 5 GB di pesi per token e lascia inutilizzata la maggior parte delle unità aritmetiche. Aggiungendo una seconda richiesta, il motore legge una sola volta gli stessi 5 GB e calcola quindi due token. Il secondo utente aggiunge un costo temporale quasi nullo. Gestire le richieste rigorosamente una dopo l’altra elimina questo vantaggio.
Due valori descrivono l’esperienza dell’utente. Il TTFT (time to first token) è dato dalla somma del tempo di attesa in coda e del prefill. L’ITL (inter-token latency) è l’intervallo tra i token trasmessi in streaming ed è determinato dal decode. Un server lento è generalmente lento in uno di questi due aspetti, e le relative correzioni sono diverse.
Il batching statico obbliga tutti ad attendere la risposta più lenta
Il batching statico è la versione più semplice. È quello che si ottiene raggruppando manualmente le richieste nel codice dell'applicazione. Il motore raccoglie N richieste, le esegue insieme e mantiene occupato ogni slot fino al completamento della generazione più lunga del gruppo.
Un utente che richiede un riepilogo di 1,200 token mantiene quattro risposte di una sola riga bloccate nel batch, perché il batch non libera alcuno slot finché non termina il membro più lento.
Ne derivano due costi. Le sequenze già completate continuano a occupare slot che non eseguono operazioni utili. Di conseguenza, il throughput effettivo diminuisce quando variano le lunghezze dell'output, come accade spesso nelle chat. Una richiesta che arriva un solo step dopo la formazione del batch deve attendere che l'intero batch venga elaborato prima ancora di iniziare il prefill. Il suo TTFT dipende quindi dal testo lungo richiesto da un altro utente.
Il continuous batching ammette e rimuove le richieste a ogni token
Il continuous batching pianifica l'esecuzione a livello di un singolo passaggio di decodifica. Dopo ogni passaggio, lo scheduler rimuove le sequenze che hanno appena emesso il token di arresto, quindi ammette le richieste in attesa negli slot liberi. Una risposta che termina al passaggio 40 libera il proprio slot al passaggio 40, non al termine di un batch.
Questo comportamento non è insolito. llama-server documenta -cb, --cont-batching come «se abilitare il continuous batching (detto anche dynamic batching) (valore predefinito: abilitato)» e vLLM si basa su questo principio. Anche Ollama gestisce richieste parallele. Il valore predefinito limita semplicemente il numero a uno. Per questo molti concludono che il proprio hardware non supporti la concorrenza, quando in realtà è la configurazione a disabilitarla.
I risultati pubblicati sul continuous batching vengono generalmente misurati su schede datacenter che dispongono sia di capacità di calcolo inutilizzata sia di decine di gigabyte per la cache. La tendenza di questi risultati si applica anche al tuo sistema. Le dimensioni dei risultati, invece, no: il motivo è spiegato nella sezione sulla memoria qui sotto.
Il prefill compete con il decode per le stesse risorse di calcolo
Quando arriva una nuova richiesta mentre quattro risposte sono in streaming, il relativo prompt deve prima essere elaborato in prefill, un'operazione che richiede molte risorse di calcolo. Se lo scheduler assegna a questo prefill uno step separato, durante quello step i quattro utenti che ricevono le risposte in streaming non ottengono alcun token. Con un prompt lungo, ogni finestra aperta mostra una pausa evidente. È il rallentamento a cui ci si riferisce quando si dice che il server ha un'interruzione ogni volta che un altro utente invia una richiesta.
Il chunked prefill suddivide un prompt lungo in blocchi e inserisce ogni blocco nello stesso step dei decode già in esecuzione. La guida al tuning di vLLM descrive direttamente il compromesso: budget più piccoli "achieve better ITL because there are fewer prefills slowing down decodes", mentre valori più alti "achieve better time to first token (TTFT) as you can process more prefill tokens in a batch". È necessario scegliere quale esperienza proteggere: quella della persona che aspetta l'inizio della risposta oppure quella degli utenti che osservano il testo scorrere.
La lunghezza del prompt determina l'impatto del problema. Un prompt di 6,000 token con una risposta di 200 token richiede 6,000 token di lavoro di prefill a fronte di 200 step di decode. Le chat con retrieval-augmented generation e i prompt di sistema lunghi rientrano spesso in questo scenario. Il prefill smette quindi di essere un costo trascurabile e diventa l'operazione che gli utenti devono attendere. Il prefix caching è utile quando la parte lunga si ripete: vLLM espone --enable-prefix-caching, che riutilizza la cache per un prefisso di prompt condiviso invece di ricalcolarlo per ogni richiesta.
La memoria che si esaurisce per prima è la cache KV
Ogni token di ogni conversazione attiva lascia un vettore key e un vettore value in ogni layer del modello. Questa è la cache KV (cache key/value) e consente alla fase di decode di evitare di ricalcolare l'intero prompt per ogni nuovo token. La dimensione per token è determinata dalla struttura del modello: 2 (una key e un value) moltiplicato per il numero di layer, per il numero di head key/value, per la dimensione della head e per i byte per valore. Leggi questi numeri dal config.json del modello.
Esegui il calcolo una volta e il limite massimo non sarà più misterioso. Un modello 8B tipico con 36 layer, 8 head key/value e una dimensione della head pari a 128, che mantiene la cache a 16 bit, richiede 2 36 8 128 2 byte per token. Sono 147,456 byte, circa 144 KiB. Una conversazione da 8,192 token richiede quindi circa 1.2 GB di cache. Cinque conversazioni richiedono circa 6 GB, oltre alla memoria occupata dai pesi. Questo determina il numero effettivo di utenti gestibili.
La concorrenza moltiplica il contesto, e gli strumenti lo dichiarano esplicitamente. Le FAQ di Ollama riportano: "L'elaborazione parallela delle richieste per un determinato modello aumenta la dimensione del contesto in base al numero di richieste parallele. Ad esempio, un contesto da 2K con 4 richieste parallele diventa un contesto da 8K e richiede un'allocazione di memoria aggiuntiva." La RAM richiesta cresce in proporzione a OLLAMA_NUM_PARALLEL moltiplicato per OLLAMA_CONTEXT_LENGTH. In llama-server, il contesto richiesto con -c viene distribuito tra gli slot -np. Aumentare soltanto il numero di slot riduce quindi la quantità di contesto disponibile per ogni richiesta. Leggi il contesto per slot dal log di avvio invece di presupporlo.
vLLM esegue invece una preallocazione. --gpu-memory-utilization (valore predefinito 0.92) è "la frazione della memoria GPU da utilizzare per l'esecutore del modello". La memoria che rimane dopo i pesi diventa il pool KV paginato. Quando il pool si esaurisce, lo scheduler espelle una richiesta invece di interromperla:
WARNING 05-09 00:49:33 scheduler.py:1057] Sequence group 0 is preempted by PreemptionMode.RECOMPUTE mode because there is not enough KV cache space.Nel motore V1 di vLLM, la modalità di preemption predefinita è RECOMPUTE. Una richiesta espulsa elimina quindi la propria cache e riesegue il prefill quando viene ammessa nuovamente. Questa elaborazione viene eseguita due volte. La documentazione avverte che "la preemption e il ricalcolo possono influire negativamente sulla latenza end-to-end". Questa riga di log è la spiegazione più utile del motivo per cui un utente ha atteso molto più a lungo degli altri, mentre la latenza media sembrava regolare. Imposta disable_log_stats=False per registrare il conteggio cumulativo oppure leggi il contatore delle preemption dalle metriche Prometheus esposte da vLLM.
Cosa cambia con 2, 5 e 20 utenti simultanei
Due utenti. Su una GPU con cache disponibile, l'impatto è quasi impercettibile, perché il secondo flusso di decodifica si affianca al primo con un tempo aggiuntivo minimo. Su un VPS con soli CPU e 4 to 8 GB di RAM, invece, il costo è reale: entrambi i flussi condividono gli stessi pochi vCPU e la stessa larghezza di banda della RAM. Ogni utente vede quindi circa la metà dei token al secondo, mentre la richiesta di cache raddoppia a fronte di una capacità molto più limitata.
Cinque utenti. A questo punto i valori predefiniti non sono più sufficienti e il problema iniziale diventa la coda. Con OLLAMA_NUM_PARALLEL impostato su 1, quattro persone attendono quella che ha richiesto la risposta più lunga, ma ciascuna raggiunge la velocità normale non appena arriva il proprio turno. Aumentando il numero di richieste parallele, il problema cambia natura: cinque slot con un contesto di 8K ciascuno richiedono una cache da 40K token. Se non entra nella VRAM, il motore sposta alcuni layer nella RAM di sistema. Se non entra nemmeno nella RAM, il server usa lo swap e il numero di token al secondo crolla.
Venti utenti. Venti persone in un'interfaccia di chat non corrispondono normalmente a venti richieste simultanee. Questo è l'aspetto più importante da capire prima di acquistare l'hardware. Una persona legge una risposta e riflette per 20 to 60 secondi tra un turno e l'altro, quindi la maggior parte della sessione rimane inattiva. Venti agenti o venti attività di riepilogo di documenti generano invece venti flussi reali senza alcun tempo di inattività. Si tratta di una macchina diversa.
Gli utenti generano richieste contemporaneamente oppure sono soltanto connessi?
Calcola prima le richieste in corso, poi dimensiona l'infrastruttura. Il calcolo è semplice: richieste in corso uguale utenti, moltiplicati per i secondi necessari a generare ogni turno, divisi per i secondi tra un turno e l'altro.
- Misura prima la velocità del tuo singolo flusso, sia per il prefill sia per il decode. Non usare il valore rilevato sulla scheda di qualcun altro: misura i token al secondo sul tuo sistema e usa il risultato ottenuto.
- Stima il ciclo di attività. Venti utenti di chat, 12 secondi di generazione per turno e un turno ogni 90 secondi producono 20 * 12 / 90, cioè circa 2.7 richieste in corso.
- Imposta il numero di slot leggermente al di sopra di questo valore, quindi verifica che sia compatibile con la memoria: il numero di slot moltiplicato per il contesto di ogni richiesta deve rientrare nei token di cache effettivamente disponibili.
- Mantieni breve la coda, in modo che le richieste oltre il limite falliscano rapidamente e in modo visibile.
I token di cache disponibili corrispondono alla memoria libera dopo il caricamento dei pesi, divisa per il costo per token indicato nella sezione precedente. Una scheda da 24 GB che esegue un modello 8B in 16-bit utilizza circa 16 GB per i pesi e dispone di circa 6 GB di cache utilizzabile con l'utilizzo predefinito, sufficienti per circa cinque conversazioni da 8K. Per gestire più richieste, riduci il contesto per richiesta oppure memorizza la cache in 8-bit (llama-server richiede --cache-type-k q8_0). Entrambe le opzioni aumentano la concorrenza introducendo una rinuncia, e conviene leggere la descrizione completa di questo compromesso prima di acquistare l'hardware: quando un GPU VPS raggiunge il pareggio rispetto ai token API.
Quando i valori predefiniti di Ollama non bastano più
Aumenta il numero di richieste parallele tramite l'unità di servizio, perché un export nella shell non raggiunge un demone gestito da systemd.
sudo systemctl edit ollama.service[Service]
Environment="OLLAMA_NUM_PARALLEL=4"
Environment="OLLAMA_CONTEXT_LENGTH=8192"
Environment="OLLAMA_MAX_QUEUE=64"sudo systemctl daemon-reload
sudo systemctl restart ollama
systemctl show ollama --property=Environment
ollama pssystemctl show deve stampare le tre variabili appena impostate. In caso contrario, il drop-in non è stato salvato e le altre operazioni non avranno effetto. ollama ps elenca quindi il modello caricato con una dimensione superiore a quella dei soli pesi, perché quattro slot da 8,192 token riservano altri 32,768 token di cache. Se la colonna PROCESSOR mostra una parte del modello sulla CPU quando ti aspettavi che fosse tutto sulla GPU, significa che hai richiesto più cache di quella disponibile sulla scheda. Riduci uno dei due valori.
Il valore predefinito della coda merita una seconda verifica. Ollama accoda fino a OLLAMA_MAX_QUEUE richieste e "il valore predefinito è 512". Oltre questo limite risponde "con un errore 503 che indica che il server è sovraccarico". Una coda da 512 richieste su un host che ne gestisce quattro alla volta è una promessa che non puoi mantenere, perché il client in posizione 300 va in timeout molto prima del proprio turno. Una coda breve restituisce un errore che l'applicazione può ritentare o segnalare. È preferibile a un indicatore di caricamento che non termina mai.
Esegui un test reale. Invia due richieste nello stesso momento da due terminali e osserva entrambe. Se la seconda non produce alcun risultato finché la prima non termina, l'impostazione della parallelizzazione non è stata applicata.
Quando un motore di serving completo inizia a ripagarsi
vLLM giustifica la configurazione aggiuntiva quando si dispone di una GPU con capacità residua e di più di circa quattro richieste effettivamente in elaborazione contemporaneamente. Il suo scheduler opera per token, la sua cache è paginata per riutilizzare i frammenti liberi e converte la VRAM inutilizzata in concorrenza invece di lasciarla libera. Ad agosto 2026, l'installazione e l'avvio documentati richiedono due comandi:
uv pip install vllm --torch-backend=auto
vllm serve Qwen/Qwen2.5-1.5B-Instructcurl http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "Qwen/Qwen2.5-1.5B-Instruct",
"messages": [{"role": "user", "content": "Say hello."}]
}'Una risposta contenente un array choices indica che il server è attivo e che il modello è stato caricato. Sotto carico, i due parametri importanti sono --max-num-seqs, il "numero massimo di sequenze da elaborare in una singola iterazione", e --max-num-batched-tokens, il "numero massimo di token che possono essere elaborati in una singola iterazione". Il primo limita la concorrenza. Il secondo definisce il budget di prefill suddiviso in blocchi descritto in precedenza.
Con meno di circa quattro richieste in elaborazione contemporaneamente, o su un server privo di una GPU supportata, vLLM aggiunge complessità e offre pochi vantaggi. Richiede una scheda di classe CUDA e all'avvio riserva gran parte della memoria, un compromesso inadatto a un VPS con 4–8 GB. In questo caso, la scelta corretta è un modello più piccolo con un contesto più breve e una coda sotto il tuo controllo. come differiscono Ollama e vLLM come motori di serving analizza la scelta in dettaglio, mentre eseguire Qwen 3 8B su un VPS mostra quali risorse richiede un modello di dimensioni medie prima di aggiungere anche un solo utente.
Il compromesso nascosto dalla spiegazione comune
Il batching continuo aumenta il throughput complessivo e di solito migliora anche la latenza mediana, perché una richiesta in coda inizia prima. La latenza di coda si comporta invece nel modo opposto, ma questo aspetto viene menzionato raramente.
Ogni sequenza aggiuntiva in uno step introduce una piccola quantità di lavoro, quindi l’ITL aumenta per tutti quando il batch si riempie. Il prefill di una nuova richiesta occupa una parte dello step che altrimenti sarebbe stata disponibile per le richieste in streaming. Quando la cache è sotto pressione, lo scheduler effettua il preemption e riporta una richiesta parzialmente generata all’inizio del relativo prefill.
Un’interfaccia di chat rende visibile la latenza di coda, non le medie. Uno stream che si interrompe per due secondi a metà frase sembra non funzionare, anche quando il tempo totale di completamento è buono. Misurare p95 TTFT e p95 ITL sotto il carico previsto e considerare la media dei token al secondo come un valore di capacità, non come una descrizione dell’esperienza.
Da questo deriva l’impostazione pratica. Limitare la concorrenza a un valore leggermente inferiore a quello consentito dalla memoria, in modo che il motore non debba mai effettuare il preemption. Una coda breve e prevedibile è preferibile a un batch profondo che causa thrashing, perché un utente che aspetta quattro secondi e poi riceve uno stream regolare è più soddisfatto di un utente la cui richiesta inizia subito ma si interrompe due volte.
Cosa controllare quando è lento
Ogni richiesta singola ha prestazioni normali, ma l'attesa è lunga. Si tratta di una coda, non di un problema di velocità. Controllare prima l'impostazione della concorrenza. Il modello sta elaborando correttamente una richiesta alla volta.
Ollama restituisce HTTP 503. La coda è piena. Il server è realmente al limite della capacità oppure OLLAMA_MAX_QUEUE è impostato volutamente su un valore basso per ridurre il carico, che è esattamente il comportamento desiderato.
I token al secondo crollano sotto carico su un server con CPU. Eseguire vmstat 1 mentre il problema si verifica. Valori diversi da zero nelle colonne si e so indicano che la macchina sta usando lo swap; quindi i pesi vengono letti dal disco a ogni token. Nessuna modifica alla configurazione può risolvere il problema. Ridurre le dimensioni del modello oppure il numero di slot.
Un utente su dieci attende molto più degli altri. Cercare preempted nel log di vLLM. La causa usuale è il preemption con ricalcolo. Questo indica che la cache è sovrascritta rispetto alla lunghezza di contesto consentita.
Il TTFT è elevato anche quando il server è inattivo. Si tratta del prefill, non della concorrenza. I prompt lunghi richiedono tempo prima che venga visualizzato il primo token. Controllare quindi le dimensioni del prompt e il prefix caching prima di esaminare l'hardware.
FAQ
Perché il mio LLM self-hosted rallenta quando lo usa una seconda persona?
Nella maggior parte dei casi non rallenta affatto. Le richieste vengono accodate. Ollama viene fornito con OLLAMA_NUM_PARALLEL impostato su 1, quindi la seconda richiesta attende che la prima generi il token finale. Per distinguere i due casi, misura il flusso dei token di un utente mentre un altro è in attesa: se la velocità in token al secondo torna normale quando la generazione inizia, hai una coda e aumentare il numero di richieste parallele risolve il problema. Se entrambi i flussi procedono a metà velocità, stai davvero condividendo la larghezza di banda della memoria e questo è un limite hardware.
Quanti utenti simultanei può gestire una piccola GPU?
Considera la memoria, non il numero di utenti. Prima i pesi, poi la cache KV, che per ogni token e per ogni conversazione attiva richiede 2 volte il numero di layer per il numero di teste key/value per la dimensione della testa per il numero di byte. Un modello tipico da 8B con 36 layer, 8 teste key/value e dimensione della testa pari a 128 richiede circa 144 KiB per token in formato a 16 bit; una conversazione di 8,192 token richiede quindi circa 1.2 GB. Una scheda da 24 GB che contiene quel modello in formato a 16 bit lascia circa 6 GB per la cache, sufficienti per circa cinque conversazioni con il contesto completo, o per un numero maggiore se riduci il contesto.
Il continuous batching rende più lenta la risposta di ogni utente?
La latenza mediana di solito migliora, perché le richieste non devono più attendere il completamento di un intero batch. La latenza di coda peggiora. Ogni sequenza aggiuntiva aumenta il lavoro a ogni fase di decoding, l'arrivo di una nuova richiesta sottrae parte di una fase agli utenti che ricevono lo stream e una richiesta interrotta deve eseguire il prefill due volte. Misura la latenza inter-token p95, non la media, perché una finestra di chat rende evidenti le pause che i valori medi possono nascondere.
Devo aumentare OLLAMA_NUM_PARALLEL o passare a vLLM?
Aumenta prima il numero di richieste parallele. Non comporta costi e richiede un solo file sostitutivo; risolve il caso comune in cui quattro persone attendono in coda dietro una risposta lunga. Il limite è la memoria: le richieste parallele moltiplicano il contesto che devi mantenere, quindi controlla se alcuni layer vengono spostati sulla CPU. Passa a vLLM quando hai una GPU con VRAM disponibile e più di circa quattro richieste effettivamente in esecuzione, perché da quel punto la cache paginata e la pianificazione per token offrono vantaggi superiori al loro costo.
Più core CPU risolvono la lentezza di un server LLM?
Non per la parte che gli utenti percepiscono maggiormente. Il decoding legge l'intero modello dalla memoria per ogni token, quindi è limitato dalla larghezza di banda della RAM; i core aggiuntivi non aiutano più quando la larghezza di banda è satura. Il prefill scala invece con il numero di core, quindi averne di più riduce il tempo al primo token per i prompt lunghi. Su un VPS da 4 a 8 GB, il vincolo principale è di solito la capacità della memoria; la soluzione efficace è usare un modello più piccolo o un contesto più breve, non aggiungere vCPU.