Il tuo assistente interno regge cinquanta persone? Si calcola prima di comprare
La domanda «quanti utenti concorrenti regge un server con Ollama» non ha una risposta in utenti. Ha una risposta in token al secondo: un modello genera testo a una velocità che dipende dalla macchina, ogni risposta consuma un certo numero di token, e il numero di persone che puoi servire discende da queste due grandezze e da quanto spesso ciascuno chiede qualcosa. Questo articolo costruisce quel calcolo, lo applica a uno scenario costruito (cinquanta persone che interrogano un assistente aziendale) e ti dice dove la memoria diventa il vincolo prima del processore.
Il risultato che conviene anticipare è scomodo: su un server senza GPU, cinquanta utenti che chiedono una risposta lunga ogni due minuti richiedono una velocità aggregata che un singolo server di taglia piccola difficilmente offre. Come scegliere il modello che la produce è nell'articolo su Llama, Mistral, Qwen e Gemma; quanto costa tenere acceso il tutto, nell'analisi dei costi di 12 mesi. Qui ci occupiamo di dimensionare e di non far crollare il servizio quando il carico supera la capacità.
Prima si misura: token al secondo, dalla risposta stessa
Non serve alcuno strumento esterno per conoscere la velocità del tuo server: ogni risposta di /api/generate di Ollama riporta il numero di token generati (eval_count), il tempo speso a generarli in nanosecondi (eval_duration), il tempo speso a leggere il prompt (prompt_eval_duration) e il tempo di caricamento del modello (load_duration), come descrive la documentazione dell'API consultata il 2026-11-13. La divisione dei primi due dà i token al secondo. Il programma qui sotto fa l'aritmetica su un JSON di esempio copiato dalla documentazione: l'esempio mostra il calcolo, non la velocità di una macchina reale, e il valore che esce non va preso come riferimento per il tuo server.
"""Token al secondo dai campi che Ollama restituisce in ogni risposta di /api/generate."""
import json
def token_al_secondo(risposta):
# eval_count: token generati; eval_duration: nanosecondi spesi a generarli
return risposta["eval_count"] / (risposta["eval_duration"] / 1e9)
def tempo_prompt_s(risposta):
# prompt_eval_duration: nanosecondi per leggere il prompt non in cache
return risposta["prompt_eval_duration"] / 1e9
# JSON di esempio copiato dalla documentazione ufficiale dell'API: serve a mostrare l'aritmetica,
# NON descrive la velocità di nessuna macchina reale.
esempio = json.loads("""{"total_duration": 174560334, "load_duration": 101397084,
"prompt_eval_count": 11, "prompt_eval_duration": 13074791,
"eval_count": 18, "eval_duration": 52479709}""")
print(f"generazione: {token_al_secondo(esempio):.0f} token/s")
print(f"lettura del prompt: {tempo_prompt_s(esempio) * 1000:.1f} ms per {esempio['prompt_eval_count']} token")
# Output:
# generazione: 343 token/s
# lettura del prompt: 13.1 ms per 11 token
Due cose vanno lette con attenzione. La prima è la separazione fra lettura del prompt e generazione: un contesto RAG con molti chunk allunga la prima fase, che su un processore senza GPU può pesare molto, quindi misura entrambe e non solo i token generati. La seconda è load_duration: la prima richiesta dopo che il modello è stato scaricato dalla memoria paga il caricamento, e se la ometti nelle medie sottostimi la latenza vista dall'utente. Per ottenere dati reali chiama l'endpoint con stream a falso e riusa le stesse funzioni, come nello schema seguente, che non ho eseguito perché richiede un server in ascolto.
# Schema da adattare, NON eseguito: richiede un server Ollama in ascolto e un modello scaricato.
import requests
from misura import token_al_secondo, tempo_prompt_s
PROMPT = "Spiega in 200 parole la differenza fra RAG e fine tuning."
r = requests.post(
"http://127.0.0.1:11434/api/generate",
json={"model": "mistral", "prompt": PROMPT, "stream": False},
timeout=300,
).json()
print(token_al_secondo(r), tempo_prompt_s(r), r["load_duration"] / 1e9)
Lancia lo schema con prompt e lunghezze di risposta realistici per il tuo caso, almeno una ventina di volte, ed esprimi il risultato come intervallo, non come un numero solo: i token al secondo variano da una richiesta all'altra. Quella misura è il parametro token_al_secondo del modello di capacità che segue.
Un modello di capacità che si può discutere
Il modello più semplice che dia indicazioni utili è una coda con un solo servente (M/M/1): le richieste arrivano a caso con una frequenza media, il server le elabora una alla volta e produce in totale un certo numero di token al secondo. Se le risposte sono lunghe in media 300 token e il server produce 20 token al secondo, ogni risposta occupa il server in media 15 secondi; se arrivano più richieste di quante ne chiude, la coda cresce senza limite. La formula del tempo medio dalla richiesta alla risposta completa è 1 diviso la differenza fra la frequenza di servizio e quella di arrivo, e diverge quando le due si avvicinano.
Nel programma le ipotesi sono dichiarate in testa e sono tue da sostituire: risposte da 300 token, un utente che chiede ogni 120 secondi in media, e tre velocità del server (10, 20 e 40 token al secondo) che sono valori di prova, non misure. La funzione latenza_media restituisce l'utilizzazione e il tempo medio, oppure nulla se la coda è instabile; utenti_massimi cerca il numero massimo di utenti che mantiene la risposta media sotto un obiettivo. L'ultimo calcolo dice di quanti token al secondo hai bisogno per cinquanta utenti.
"""Quanti utenti regge un server di inferenza, in funzione dei token al secondo che produce."""
def latenza_media(utenti, pausa_s, token_risposta, token_al_secondo):
"""Modello M/M/1: una coda sola, un servente che produce 'token_al_secondo' in totale.
Ritorna (utilizzazione, secondi medi dalla richiesta alla risposta completa) o None se instabile."""
lam = utenti / pausa_s # richieste al secondo
mu = token_al_secondo / token_risposta # risposte completate al secondo a pieno carico
rho = lam / mu
if rho >= 1:
return rho, None
return rho, 1 / (mu - lam)
def utenti_massimi(pausa_s, token_risposta, token_al_secondo, obiettivo_s):
u = 0
while True:
rho, t = latenza_media(u + 1, pausa_s, token_risposta, token_al_secondo)
if t is None or t > obiettivo_s:
return u
u += 1
# Ipotesi, da sostituire con le TUE misure: risposte da 300 token, ogni utente chiede ogni 120 s.
RISPOSTA, PAUSA = 300, 120
print("token/s servizio utenti con risposta media entro 30 s / 60 s")
for tps in (10, 20, 40):
print(f"{tps:7d} {RISPOSTA / tps:5.1f} s {utenti_massimi(PAUSA, RISPOSTA, tps, 30):4d} / {utenti_massimi(PAUSA, RISPOSTA, tps, 60):4d}")
print()
print("20 token/s: come cresce l'attesa avvicinandosi al limite")
for u in (2, 4, 6, 7, 8):
rho, t = latenza_media(u, PAUSA, RISPOSTA, 20)
print(f"{u:2d} utenti utilizzazione {rho:4.2f} risposta media {'instabile' if t is None else f'{t:5.1f} s'}")
print()
print("fabbisogno per 50 utenti (token/s totali): a utilizzazione 1.0 e 0.5")
fabbisogno = 50 / PAUSA * RISPOSTA
print(f"{fabbisogno:.0f} e {fabbisogno / 0.5:.0f}")
# Output:
# token/s servizio utenti con risposta media entro 30 s / 60 s
# 10 30.0 s 0 / 2
# 20 15.0 s 4 / 5
# 40 7.5 s 11 / 13
#
# 20 token/s: come cresce l'attesa avvicinandosi al limite
# 2 utenti utilizzazione 0.25 risposta media 20.0 s
# 4 utenti utilizzazione 0.50 risposta media 30.0 s
# 6 utenti utilizzazione 0.75 risposta media 60.0 s
# 7 utenti utilizzazione 0.88 risposta media 120.0 s
# 8 utenti utilizzazione 1.00 risposta media instabile
#
# fabbisogno per 50 utenti (token/s totali): a utilizzazione 1.0 e 0.5
# 125 e 250
La tabella superiore ti dice che a 20 token al secondo bastano 4 utenti per tenere la risposta media sotto i 30 secondi, e 5 per tenerla sotto i 60. La seconda tabella spiega il perché: l'attesa non cresce in modo lineare, a utilizzazione 0,50 la risposta media è 30 secondi, a 0,75 è 60 e a 0,88 è 120. Per questo si dimensiona a mezza capacità, non a capacità piena. L'ultima riga chiude lo scenario: cinquanta utenti con quelle ipotesi richiedono 125 token al secondo a utilizzazione 1,0 e 250 a utilizzazione 0,5, che è il regime sano. Il modello è una semplificazione: assume tempi di servizio esponenziali e un servente solo che elabora in sequenza. Servono per mettere ordini di grandezza sul tavolo, e vanno poi confrontati con una prova di carico.
La memoria è il secondo vincolo, e si calcola dai parametri del modello
Ollama può elaborare più richieste insieme per lo stesso modello, ma non è l'impostazione predefinita. La FAQ ufficiale elenca tre variabili: OLLAMA_NUM_PARALLEL (richieste parallele per modello, valore predefinito 1), OLLAMA_MAX_QUEUE (richieste accodate prima di rifiutarne altre, predefinito 512) e OLLAMA_MAX_LOADED_MODELS (modelli caricati insieme, predefinito 3 per l'inferenza su processore). Dice anche che la memoria necessaria scala con il prodotto fra richieste parallele e lunghezza del contesto, perché un contesto da 2K con 4 richieste parallele diventa un contesto da 8K.
OLLAMA_NUM_PARALLEL: sale la concorrenza ma sale anche la memoria; sul processore, in più, le richieste parallele si dividono lo stesso calcolo.OLLAMA_MAX_QUEUE: oltre questo numero, Ollama rifiuta; è la tua prima difesa contro il sovraccarico, ma il valore predefinito (512) è troppo generoso per risposte che durano 15 secondi.OLLAMA_MAX_LOADED_MODELS: conta se usi due modelli, per esempio uno per la generazione e uno per gli embedding.
Le tre impostazioni parlano a livelli diversi: la prima aumenta la concorrenza ma anche i costi di memoria, la seconda decide che cosa succede quando il server è saturo, la terza evita che due modelli si scalzino a vicenda. Il numero che non trovi nella FAQ è quanta memoria costa ogni richiesta in più, ma si calcola. La cache chiave-valore ha per ogni token un costo che dipende dall'architettura: due tensori (chiavi e valori) per ogni strato, per ogni testa di attenzione chiave-valore, per la dimensione di testa, per i byte del numero. Il programma legge i parametri pubblicati nella scheda di Mistral-7B-Instruct-v0.3 e calcola sia il numero di parametri (con la struttura dell'architettura) sia la cache per token.
"""Quanta memoria serve per N richieste parallele, calcolata dai parametri pubblici del modello."""
# Configurazione pubblicata nella scheda di Mistral-7B-Instruct-v0.3 (config.json, consultata il 2026-11-13)
CFG = {
"hidden_size": 4096,
"intermediate_size": 14336,
"num_hidden_layers": 32,
"num_attention_heads": 32,
"num_key_value_heads": 8,
"vocab_size": 32768,
}
def parametri(c):
h, i, L = c["hidden_size"], c["intermediate_size"], c["num_hidden_layers"]
head_dim = h // c["num_attention_heads"]
kv = c["num_key_value_heads"] * head_dim
attenzione = h * h + 2 * h * kv + h * h # q, k, v, o
mlp = 3 * h * i # gate, up, down
norme = 2 * h
embedding = 2 * c["vocab_size"] * h # ingresso e uscita non condivisi
return L * (attenzione + mlp + norme) + embedding + h
def kv_per_token(c, byte_per_valore=2):
head_dim = c["hidden_size"] // c["num_attention_heads"]
return 2 * c["num_hidden_layers"] * c["num_key_value_heads"] * head_dim * byte_per_valore
GIB = 1024 ** 3
n_par = parametri(CFG)
kv = kv_per_token(CFG)
print(f"parametri: {n_par / 1e9:.2f} miliardi")
print(f"cache KV: {kv} byte per token ({kv / 1024:.0f} KiB)")
print(f"pesi a 16 bit: {n_par * 2 / GIB:.1f} GiB, a 4 bit (minimo teorico): {n_par * 0.5 / GIB:.1f} GiB")
print()
print("contesto per richiesta 4096 token; la FAQ di Ollama dice che la memoria scala con parallele x contesto")
print("parallele cache KV totale (pesi 4 bit) totale (pesi 16 bit)")
for parallele in (1, 2, 4, 8):
cache = parallele * 4096 * kv / GIB
print(f"{parallele:9d} {cache:6.2f} GiB {cache + n_par * 0.5 / GIB:9.1f} GiB {cache + n_par * 2 / GIB:15.1f} GiB")
# Output:
# parametri: 7.25 miliardi
# cache KV: 131072 byte per token (128 KiB)
# pesi a 16 bit: 13.5 GiB, a 4 bit (minimo teorico): 3.4 GiB
#
# contesto per richiesta 4096 token; la FAQ di Ollama dice che la memoria scala con parallele x contesto
# parallele cache KV totale (pesi 4 bit) totale (pesi 16 bit)
# 1 0.50 GiB 3.9 GiB 14.0 GiB
# 2 1.00 GiB 4.4 GiB 14.5 GiB
# 4 2.00 GiB 5.4 GiB 15.5 GiB
# 8 4.00 GiB 7.4 GiB 17.5 GiB
Il numero di parametri esce 7,25 miliardi, coerente con il nome del modello, e la cache vale 131.072 byte per token, cioè 128 KiB: un contesto da 4096 token costa mezzo gibibyte per richiesta. Il merito è delle 8 teste chiave-valore (a fronte di 32 teste di attenzione) del modello: senza questa riduzione il costo sarebbe quadruplo. I pesi a 4 bit sono un minimo teorico, i formati reali di quantizzazione aggiungono qualcosa, e non sono inclusi il sistema operativo, il runtime e gli altri servizi. Con otto richieste parallele a 4096 token la cache da sola pesa 4 GiB: non è una voragine, ma sommata a un server che fa anche da database e da indice vettoriale smette di essere trascurabile. Se usi un altro modello, sostituisci il dizionario con la sua config.json.
Più processi sulla stessa macchina non sono più capacità
Il rimedio che si legge più spesso, lanciare due istanze di Ollama su porte diverse davanti a Nginx, aggiunge pochissimo se le due istanze stanno sulla stessa macchina senza GPU: si dividono gli stessi core e la stessa banda di memoria, e ciascuna carica una copia propria dei pesi. È un ragionamento, non una misura, quindi verificalo con il programma di misura prima e dopo. Le istanze aiutano in due casi: hai più GPU e ne assegni una a ciascuna istanza, oppure hai più macchine. In entrambi i casi il bilanciatore decide dove mandare le richieste.
Per il bilanciamento tra macchine, Nginx open source basta: il metodo least_conn manda ogni richiesta al server con meno connessioni attive, ed è sensato per richieste che durano decine di secondi. Il controllo di stato è la parte che si sbaglia più facilmente. Il modulo ngx_http_upstream_hc_module, quello della direttiva health_check, è disponibile solo con la sottoscrizione commerciale, come dice la sua pagina di documentazione; nella versione open source hai il controllo passivo, con i parametri max_fails e fail_timeout del server nell'upstream: dopo un certo numero di errori il server viene escluso per un po'. Lo schema seguente è da adattare, non l'ho eseguito perché Nginx non è installato su questa macchina di lavoro.
# Schema da adattare, NON eseguito: nginx non è installato su questa macchina di lavoro.
upstream inferenza {
least_conn; # alla connessione con meno richieste attive
server 10.0.0.11:11434 max_fails=2 fail_timeout=30s;
server 10.0.0.12:11434 max_fails=2 fail_timeout=30s;
}
server {
listen 8080;
location /api/generate {
proxy_pass http://inferenza;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_buffering off; # le risposte in streaming arrivano a pezzi
proxy_read_timeout 300s; # una risposta lunga non è un guasto
}
}
Le due righe da leggere sono proxy_buffering off, che lascia passare la risposta in streaming mentre viene generata invece di accumularla, e proxy_read_timeout 300s, perché con un modello lento una risposta lunga supera facilmente il timeout predefinito senza che nulla sia rotto. Gli indirizzi sono privati di esempio: non esporre mai la porta di Ollama direttamente su Internet, perché non ha autenticazione.
Per le macchine con GPU, o per prove con due istanze, la unit di systemd con modello a template (ollama@.service) permette di far partire un'istanza per porta: %i è il nome dopo la chiocciola, quindi ollama@11435 usa la porta 11435. È uno schema da adattare e non eseguito; nota che OLLAMA_NUM_PARALLEL e OLLAMA_MAX_QUEUE sono quelle documentate nella FAQ, e che i valori sono di esempio.
# Schema da adattare, NON eseguito. /etc/systemd/system/ollama@.service
[Unit]
Description=Ollama, istanza sulla porta %i
After=network.target
[Service]
User=ollama
Environment="OLLAMA_HOST=127.0.0.1:%i"
Environment="OLLAMA_NUM_PARALLEL=2"
Environment="OLLAMA_MAX_QUEUE=16"
ExecStart=/usr/bin/ollama serve
Restart=on-failure
[Install]
WantedBy=multi-user.target
La coda deve avere un limite, o la latenza non ha limite
Quando le richieste arrivano più in fretta di quanto il server le chiuda, una coda senza limite non protegge nessuno: accumula attesa. Un utente che aspetta sette minuti ha già riaperto la pagina e rilanciato la domanda, e così peggiora il carico. Il programma seguente simula un'ora di traffico con un arrivo ogni 12 secondi e un servizio di 15 in media, cioè il 125% della capacità, con un generatore casuale a seme fisso (le due simulazioni sono ripetibili). Confronta la coda illimitata con una coda che accetta al massimo 3 richieste in attesa e rifiuta le altre.
"""Simulazione a eventi discreti: coda illimitata contro coda limitata, con più richieste di quante il servente ne regga."""
import random
from statistics import quantiles
def simula(coda_max, durata=3600.0, arrivo_medio=12.0, servizio_medio=15.0, seme=42):
rng = random.Random(seme)
t, libero_alle = 0.0, 0.0
attese, rifiutate = [], 0
finiture = [] # istanti di fine servizio delle richieste accettate
while True:
t += rng.expovariate(1 / arrivo_medio)
if t > durata:
break
in_coda = sum(1 for f in finiture if f > t) - (1 if libero_alle > t else 0)
if coda_max is not None and in_coda >= coda_max:
rifiutate += 1
continue
inizio = max(t, libero_alle)
libero_alle = inizio + rng.expovariate(1 / servizio_medio)
finiture.append(libero_alle)
attese.append(libero_alle - t) # dalla richiesta alla risposta completa
q = quantiles(attese, n=20)
return len(attese), rifiutate, quantiles(attese, n=2)[0], q[18]
print("arrivi: uno ogni 12 s in media; servizio: 15 s in media (125% della capacità), 1 ora simulata")
print("coda servite rifiutate mediana 95° percentile")
for nome, limite in (("illimitata", None), ("max 3 attese", 3)):
s, r, med, p95 = simula(limite)
print(f"{nome:12s}{s:7d}{r:11d}{med:9.0f} s{p95:12.0f} s")
# Output:
# arrivi: uno ogni 12 s in media; servizio: 15 s in media (125% della capacità), 1 ora simulata
# coda servite rifiutate mediana 95° percentile
# illimitata 281 0 442 s 740 s
# max 3 attese 211 83 37 s 96 s
La coda illimitata serve più richieste (281 contro 211) ma con una risposta mediana di 442 secondi e un novantacinquesimo percentile di 740. La coda limitata ne rifiuta 83 ma tiene la mediana a 37 secondi e il percentile alto a 96. Non è un risultato universale: dipende da arrivi e servizi simulati, e il numero di rifiuti dipende dal limite che scegli. La lezione è il confronto di forma: sotto sovraccarico, un rifiuto veloce vale più di una risposta tardiva, a patto che il rifiuto dica quando riprovare. Per questo OLLAMA_MAX_QUEUE a 512 è un valore da abbassare nel tuo caso, e il tuo livello applicativo dovrebbe rispondere con il codice 429 e l'intestazione Retry-After.
Il secondo livello di protezione è per utente, non globale: un singolo script impazzito non deve consumare la capacità di tutti. Il secchio di gettoni (token bucket) fa questo con poche righe: ogni richiesta costa un gettone, il secchio si riempie a ritmo costante fino a una capacità massima, e chi lo svuota deve aspettare. Il programma usa un orologio passato come parametro, così la logica è testabile senza attese: l'utente dell'esempio può fare una raffica di 5 richieste e poi una ogni 12 secondi.
"""Secchio di gettoni per utente: ogni richiesta costa un gettone, il secchio si riempie col tempo."""
class Secchio:
def __init__(self, capacita, gettoni_al_secondo):
self.capacita = capacita
self.ritmo = gettoni_al_secondo
self.gettoni = float(capacita)
self.ultimo = 0.0
def consuma(self, ora, costo=1.0):
# Prima si ricarica in base al tempo trascorso, mai oltre la capacità
self.gettoni = min(self.capacita, self.gettoni + (ora - self.ultimo) * self.ritmo)
self.ultimo = ora
if self.gettoni >= costo:
self.gettoni -= costo
return True, 0.0
# Secondi da attendere perché ci sia un gettone intero: è il valore di Retry-After
return False, (costo - self.gettoni) / self.ritmo
# Un utente: raffica fino a 5 richieste, poi una ogni 12 secondi (5 al minuto)
secchio = Secchio(capacita=5, gettoni_al_secondo=5 / 60)
for ora in (0, 1, 2, 3, 4, 5, 6, 20, 21):
ok, attesa = secchio.consuma(ora)
print(f"t={ora:3d}s {'ammessa' if ok else f'rifiutata, riprova fra {attesa:4.1f} s'}")
# Output:
# t= 0s ammessa
# t= 1s ammessa
# t= 2s ammessa
# t= 3s ammessa
# t= 4s ammessa
# t= 5s rifiutata, riprova fra 7.0 s
# t= 6s rifiutata, riprova fra 6.0 s
# t= 20s ammessa
# t= 21s rifiutata, riprova fra 3.0 s
Nell'uscita le prime cinque richieste passano, la sesta al secondo 5 viene rifiutata con il tempo da attendere (7 secondi, il valore da mettere in Retry-After), e al secondo 20 si è ricaricato un gettone. In produzione il secchio vive nello stato condiviso fra le istanze del tuo servizio (per esempio Redis) e il controllo deve essere atomico; il principio non cambia. Un limite sul numero di richieste non sostituisce uno sul costo: una domanda con un prompt lungo costa molto più di una breve, quindi puoi dare alla richiesta un costo proporzionale ai token del prompt.
Cache, GPU, Kubernetes: quando hanno senso
Una cache delle risposte funziona quando le domande si ripetono davvero identiche, e un assistente interno su procedure aziendali ne ha molte. Due avvertenze: la chiave deve includere modello, prompt di sistema e parametri, altrimenti restituisci la risposta sbagliata a una domanda diversa; e una cache conserva testo che può contenere dati personali, quindi ha una durata e va dichiarata nel registro dei trattamenti, come tratta l'articolo sulla privacy by design. Un hit di cache, in più, non entra nel calcolo di capacità: se un terzo delle domande si ripete, il fabbisogno scende di un terzo, ma questo lo misuri, non lo assumi.
Sulla GPU, la regola è la stessa della misura: non esiste un fattore di accelerazione universale, esiste il tuo rapporto fra token al secondo con e senza GPU sullo stesso modello, e il costo mensile dell'una e dell'altra dal listino ufficiale del fornitore al momento dell'acquisto; i prezzi non sono riportati qui perché cambiano. Su Kubernetes, un orchestratore ha senso quando hai più servizi da far crescere in modo indipendente e un team che lo sa gestire; per un'unica funzione di inferenza dietro un bilanciatore, il costo operativo supera quasi sempre il beneficio. Anche questo è un giudizio, non un dato.
Quello che serve se la capacità si esaurisce è sapere che cosa succede quando un nodo si ferma. Questo è il tema dell'articolo sul disaster recovery, e il contrario (quando conviene non gestire nulla in casa) è in Quando NO self-host.
Domande frequenti
Ollama gestisce più richieste insieme?
Sì, per lo stesso modello, ma è disattivato per impostazione predefinita: OLLAMA_NUM_PARALLEL vale 1. Alzarlo aumenta la memoria necessaria, che scala con richieste parallele per lunghezza del contesto, come dice la FAQ ufficiale.
Quanti utenti regge un server senza GPU?
Non c'è un numero valido per tutti: dipende dai token al secondo che produce, dalla lunghezza delle risposte e da quanto spesso si chiede. Misura la velocità con i campi di risposta di Ollama e applica il modello di capacità; con le ipotesi dell'articolo, 20 token al secondo reggono 4-5 utenti con risposta media entro 30-60 secondi.
Conviene lanciare più istanze di Ollama sullo stesso server?
Senza GPU di norma no, perché le istanze si dividono gli stessi core e ciascuna carica i propri pesi in memoria. Hanno senso con più GPU, una per istanza, o su macchine diverse dietro un bilanciatore.
Perché limitare la coda?
Perché una coda senza limite, sotto sovraccarico, fa crescere l'attesa senza fine. Rifiutare subito con un tempo di ripetizione tiene la latenza delle richieste accettate sotto controllo.
Che cosa sai fare adesso
Sai misurare la velocità del tuo server dai campi che Ollama restituisce (e confrontare due configurazioni con il metodo dell'articolo sulla valutazione del RAG), e trasformarla in un numero di utenti con un modello che dichiara le sue ipotesi. Sai calcolare quanta memoria costa ogni richiesta parallela dai parametri pubblici del modello, e perché il parallelismo sul processore non moltiplica la capacità. Sai bilanciare più macchine con Nginx open source sapendo che cosa il controllo passivo copre e che cosa no, e sai proteggere il servizio con una coda limitata e un secchio di gettoni per utente. Il passo dopo, quando il carico è sotto controllo, è proteggere i dati: disaster recovery e backup.
Nella stessa serie trovi anche il server Ollama su un VPS europeo, il recupero ibrido sui documenti aziendali e il riepilogo con la scheda di decisione.
Fonti
- Ollama - FAQ - gestione delle richieste concorrenti e variabili OLLAMA_NUM_PARALLEL, OLLAMA_MAX_QUEUE, OLLAMA_MAX_LOADED_MODELS, consultata il 2026-11-13
- Ollama - API Generate - campi eval_count, eval_duration, prompt_eval_duration, load_duration e JSON di esempio, consultata il 2026-11-13
- Mistral-7B-Instruct-v0.3 - config.json - strati, teste, dimensioni usate nei calcoli di memoria, consultata il 2026-11-13
- Nginx - ngx_http_upstream_module - least_conn, max_fails e fail_timeout, consultata il 2026-11-13
- Nginx - ngx_http_upstream_hc_module - health_check disponibile con la sottoscrizione commerciale, consultata il 2026-11-13
I programmi dell'articolo sono stati eseguiti con Python 3.14.4, due volte ciascuno con uscita identica; velocità, ipotesi di carico e traffico simulato sono valori di prova dichiarati. Lo schema di misura, la configurazione Nginx e la unit systemd non sono stati eseguiti.

Commenti
Caricamento commenti...
Accedi per commentare