KINDLE HOME

GPU per LLM locale: dall'hardware all'API

Il numero che conta davvero: la VRAM

Quando si sceglie un server GPU per fare girare un modello linguistico locale, la prima cosa che tutti guardano sono i CUDA core. Sbagliato. I CUDA core contano per il rendering 3D, per il gaming, per i workload che fanno milioni di operazioni in parallelo su piccoli tensori. Per l'inferenza LLM, il numero che determina se ce la fai o no è uno solo: la VRAM, la memoria video. Un modello da 27 miliardi di parametri in quantizzazione a 4 bit occupa circa 18 gigabyte. Se la tua GPU ne ha 16, non parte. Non è lenta — non parte. Fine della storia.

La VRAM è il vincolo fisico su cui tutto il resto si costruisce. Prima di guardare qualsiasi altra specifica, fai questo calcolo: prendi i miliardi di parametri del modello che vuoi usare, moltiplicali per 0.65 per una quantizzazione Q4, per 0.875 per Q6, per 1.125 per Q8. Il risultato in gigabyte è quanto occupa il modello a riposo, senza ancora aver servito nessun utente. Poi aggiungi il KV cache. Quello che rimane è il tuo margine operativo — e margine, in questo contesto, è sinonimo di utenti concorrenti.

Per un Qwen3.6-27B in Q6_K, il modello pesa circa 24 gigabyte. Su una scheda con 48 gigabyte totali — come due RTX 3090 in parallelo — ti restano 24 gigabyte per il KV cache. Con quei 24 gigabyte puoi servire comodamente 10-15 utenti in contemporanea con contesto medio. Se scali a Q4_K_M (18 gigabyte), ne restano 30 e il margine aumenta ancora. La scelta della quantizzazione non è solo una questione di qualità del modello: è una leva diretta sulla capacità concorrente del sistema.

Il collo di bottiglia reale: il memory bandwidth

Hai la VRAM. Il modello ci sta. Ora la domanda è: quanto velocemente riesci a generare token? Qui entra in scena il secondo numero che davvero conta, e che quasi nessuna guida per principianti menziona: il memory bandwidth, la velocità con cui la GPU legge i propri dati dalla memoria.

L'inferenza LLM è memory-bound, non compute-bound. Ad ogni token generato, il sistema deve leggere tutti i parametri del modello dalla VRAM — decine di gigabyte di dati, milioni di volte al secondo. I CUDA core in questo scenario sono in attesa: non hanno abbastanza dati da elaborare perché la memoria non fa in tempo a fornirglieli.

Una RTX 3090 ha 936 gigabyte al secondo di bandwidth. Una RTX 6000 Ada ne ha 1457. La differenza si traduce direttamente in token al secondo: circa 160 tok/s su RTX 6000 contro circa 100-110 tok/s su 3090, con lo stesso modello. Non è una differenza che l'utente finale percepisce in una singola conversazione — 100 token al secondo è molto più veloce di quanto un essere umano riesca a leggere. Ma quando hai 10 utenti simultanei, quei 100 tok/s si dividono tra tutti.

Quantizzazione: l'arte della compressione controllata

La quantizzazione riduce la precisione numerica con cui i pesi del modello vengono rappresentati. Un modello BF16 usa 2 byte per parametro. Un Q4_K_M ne usa circa 0.65, un Q6_K circa 0.875. La perdita di qualità è reale ma spesso trascurabile: Q6_K è indistinguibile da BF16 nella stragrande maggioranza dei task, mentre Q4_K_M introduce degradazioni percepibili solo in ragionamento matematico complesso.

Per applicazioni aziendali pratiche — analisi CV, assistenza recruitment, drafting comunicazioni — Q6_K è la scelta naturale. Q4_K_M è ragionevole se vuoi massimizzare la concorrenza. Q8_0 ha senso solo per operazioni che richiedono alta precisione numerica.

Esiste anche la Dynamic quantization di Unsloth, che applica quantizzazioni diverse ai diversi layer del modello in base alla loro importanza — i layer critici mantengono precisione maggiore, quelli meno critici vengono compressi di più. I file GGUF con prefisso UD- usano questa tecnica e offrono qualità superiore a parità di VRAM rispetto alla quantizzazione uniforme standard.

KV cache e il calcolo degli utenti concorrenti

Ogni volta che un utente invia un messaggio, il modello non rielabora tutta la conversazione da capo. Usa il KV cache — una struttura che memorizza i risultati intermedi dei layer di attenzione per tutti i token già processati. Questo cache cresce con la lunghezza del contesto e con il numero di conversazioni aperte in parallelo, e risiede nella VRAM libera dopo il caricamento del modello.

Per un Qwen3.6-27B con contesto di 8192 token, ogni sessione utente attiva occupa nell'ordine del gigabyte di KV cache. Con 24 gigabyte liberi dopo aver caricato il modello in Q6_K, puoi tenere aperte 15-20 sessioni simultanee con contesti medi. Il parametro -c di llama.cpp controlla la lunghezza massima del contesto allocato: tenerlo a 8192 invece di 32768 non è una limitazione severa per la maggior parte dei casi d'uso aziendali, e permette di servire il triplo degli utenti con la stessa VRAM.

RAM di sistema: necessaria ma secondaria

La RAM del server conta molto meno della VRAM per l'inferenza pura. Il modello gira sulla GPU, non sulla CPU. Detto questo, ospita il processo di inference, bufferizza le richieste in arrivo, gestisce il sistema operativo e i processi ancillari. Per un setup da 10 utenti concorrenti con un modello 27B, 64 gigabyte sono abbondanti. 128 gigabyte sono lusso rassicurante, utili se vuoi far girare servizi aggiuntivi sullo stesso server.

MTP: più veloce senza perdere qualità

Multi-Token Prediction è una tecnica di speculative decoding che permette al modello di predire più token in un singolo passaggio invece di uno alla volta. L'implementazione Unsloth per Qwen3.6 porta a un incremento di 1.4-2.2x nella velocità di generazione senza perdita di qualità — la validazione avviene comunque con il modello completo, e i token errati vengono scartati automaticamente.

In pratica: da 100 token al secondo a 140-220 token al secondo sulla stessa hardware. Il parametro --spec-draft-n-max 2 è il punto di partenza consigliato, ma vale la pena testare valori tra 1 e 4 perché l'ottimo varia per hardware specifico.

Dal server acceso al modello in VRAM: cosa succede davvero

Hai noleggiato il server. È acceso. Hai SSH. Cosa succede nei prossimi minuti prima che qualcuno possa fare una chiamata API? Questa sequenza è importante capirla perché ogni passaggio ha implicazioni pratiche su prestazioni, affidabilità e manutenzione.

Prima di tutto, scarichi il modello. Un file GGUF da 24 gigabyte (Q6_K del 27B) arriva da Hugging Face o da un mirror locale e viene salvato su disco — sulla SSD del server, non ancora sulla GPU. Questa operazione richiede bandwidth di rete e spazio su storage: un SSD da 480 gigabyte ospita comodamente il modello più i file di sistema. Il download avviene una volta sola; dopo, il file rimane sul disco e viene ricaricato ad ogni riavvio del processo di inference.

Al riavvio del processo, llama.cpp o vLLM leggono il file GGUF dal disco e caricano i pesi nella VRAM della GPU — operazione che richiede 30-90 secondi a seconda della velocità dell'SSD e del bandwidth PCIe. Durante questo tempo il server non è disponibile. Dopodiché il processo rimane in memoria e serve le richieste senza mai rileggere il modello dal disco: lo shut down del processo equivale a svuotare la VRAM, e il riavvio richiede di ripassare da capo.

Lo stack software: da llama.cpp all'API OpenAI-compatibile

L'inference engine è il software che si interfaccia direttamente con la GPU, legge i pesi del modello e gestisce il calcolo dei token. Le due scelte principali per modelli GGUF su hardware consumer sono llama.cpp e vLLM. Ollama, popolare per uso desktop, non è compatibile con i modelli Qwen3.6 per via dei file separati di visione (mmproj) — da evitare in produzione.

llama.cpp è scritto in C++ con binding per GPU via CUDA. È leggero, compatibile con praticamente tutto l'hardware NVIDIA, e si compila in pochi minuti. Espone un server HTTP con endpoint compatibili OpenAI: /v1/chat/completions, /v1/completions, /v1/models. Ogni applicazione che parla con OpenAI può essere puntata su llama.cpp con una sola riga di configurazione — cambi la base URL e la chiave API, il resto rimane identico. Questo è il motivo per cui "compatibile OpenAI" è diventato lo standard de facto per i server LLM locali.

vLLM è l'alternativa Python, più pesante ma con batching continuo più sofisticato. Il continuous batching di vLLM è la sua killer feature: invece di aspettare che tutti gli utenti in una batch abbiano finito prima di iniziare la prossima, processa i token in modo fluido e inserisce nuove richieste nel flusso non appena si liberano slot. Per uso con 10 utenti concorrenti la differenza rispetto a llama.cpp è marginale; diventa rilevante quando si parla di decine di utenti simultanei.

Il batching: come la GPU serve più utenti insieme

Una GPU non serve un utente alla volta in sequenza — o almeno, non dovrebbe. Il batching è il meccanismo con cui il processo di inference raggruppa più richieste in un singolo passaggio computazionale, sfruttando la natura parallela del hardware. Capire il batching è fondamentale per dimensionare correttamente il sistema.

Nel batching statico, il server raccoglie N richieste, le elabora insieme in un colpo solo, poi passa alle N successive. Semplice da implementare, inefficiente in pratica: se una richiesta è corta (50 token) e un'altra è lunga (500 token), tutta la batch aspetta la più lenta prima di liberarsi. Nel continuous batching — implementato da vLLM e in forma più semplice anche da llama.cpp con il parametro -np — le richieste entrano ed escono dal flusso in modo asincrono. Una risposta completata libera immediatamente il suo slot per la richiesta successiva in coda.

Il parametro -np di llama.cpp (numero di slot paralleli) è il cursore principale per configurare la concorrenza. Con -np 10 il server gestisce fino a 10 richieste in contemporanea; ogni slot occupa parte del KV cache. Il valore ottimale dipende dalla VRAM disponibile dopo il modello: troppi slot e la VRAM si esaurisce, troppo pochi e la GPU sta ferma mentre le richieste aspettano in coda.

Il reverse proxy: nginx davanti all'inference engine

Il processo llama.cpp o vLLM ascolta su una porta locale — tipicamente 8080 o 8000 — senza autenticazione, senza HTTPS, senza rate limiting. Non è pensato per essere esposto direttamente su internet. Davanti ci va nginx, che fa tre cose: termina SSL, gestisce l'autenticazione via API key nell'header Authorization, e fa rate limiting per evitare che un singolo client monopolizzi il server.

La configurazione nginx per un LLM server è minimale: proxy_pass verso localhost:8080, timeout generosi (i modelli LLM sono lenti rispetto a un database), e un blocco di autenticazione basato su una variabile di ambiente. La API key che i client passano nell'header non viene verificata dal modello — viene verificata da nginx, che la confronta con un valore in configurazione. Semplice e sufficiente per un uso interno o con un numero limitato di client fidati.

Per un deployment più strutturato, tra nginx e il modello può stare un API gateway come Traefik o un proxy specifico come LiteLLM. LiteLLM merita menzione separata: è un proxy Python che espone un endpoint OpenAI-compatibile e può stare davanti a più modelli diversi — locali e cloud — presentando un'interfaccia unificata. Se hai un modello locale per le richieste normali e vuoi fallback su un API cloud per i picchi o per modelli specializzati, LiteLLM gestisce il routing in modo trasparente per il client.

Monitoring, restart automatico e manutenzione

Un processo llama.cpp che gira senza supervisione è una fonte di problemi silenziosi. Il processo può crashare per un input malformato, esaurire la VRAM per un contesto eccezionalmente lungo, o semplicemente bloccarsi senza morire. La soluzione standard su Linux è systemd: un service file che definisce il comando di avvio, imposta le variabili d'ambiente, e configura il restart automatico in caso di crash. Con Type=simple e Restart=always, il processo viene riavviato entro pochi secondi da qualsiasi tipo di uscita inattesa.

Per il monitoring, la metrica più utile non è il CPU usage (basso per definizione, il lavoro lo fa la GPU) ma la VRAM occupata e il numero di richieste in coda. nvidia-smi fornisce entrambe via CLI e può essere integrato con Prometheus via nvidia-gpu-prometheus-exporter. llama.cpp espone un endpoint /metrics in formato Prometheus quando avviato con --metrics: token generati, tempo medio per token, richieste in coda, slot occupati. Cinque minuti di configurazione Grafana danno una dashboard che mostra esattamente quanto è carico il sistema in tempo reale.

Il checklist completo: dall'hardware all'API

In ordine cronologico dall'accensione del server alla prima chiamata API andata a buon fine. Scegli il server verificando prima la VRAM (almeno 1.3x il peso del modello quantizzato), poi il memory bandwidth (proxy diretto per tok/s), poi RAM di sistema (64 GB minimo). Noleggia, configura accesso SSH, installa CUDA toolkit nella versione compatibile con il tuo modello — attenzione a CUDA 13.2 che produce output gibberish con Qwen3.6, usa la versione precedente. Compila llama.cpp con DGGML_CUDA=ON per abilitare l'accelerazione GPU. Scarica il file GGUF del modello scelto da Hugging Face. Avvia llama.cpp con i parametri giusti: -ngl 99 per caricare tutti i layer sulla GPU, -c per il contesto, -np per gli slot paralleli, --spec-type draft-mtp per MTP. Metti nginx davanti con SSL e autenticazione. Configura systemd per il restart automatico. Punta la tua applicazione sulla base URL del server con una API key qualsiasi nell'header. Fine.

Per un modello 27B-30B con 10 utenti simultanei, il punto dolce nel 2026 è 48 gigabyte di VRAM totale — due RTX 3090 o una scheda professionale equivalente — con modello in Q6_K e MTP abilitato. Costo mensile in Europa: tra 250 e 400 euro su provider dedicati come LeaderGPU o Trooper.ai. Un costo fisso, prevedibile, senza sorprese legate al consumo di token. E soprattutto: i dati non lasciano mai il tuo perimetro.

- FINE -
1