Terza puntata della serie sull'AI locale. Nella puntata 1 abbiamo deciso se ha senso uscire dal cloud, nella puntata 2 quale runtime usare. Qui si sceglie il modello e il file da scaricare: quanti parametri, quanti bit, quanta VRAM.
Il percorso completo è nell'hub AI Engineering. I termini tecnici (GGUF, KV cache, quantizzazione) hanno una voce breve nel glossario.
In breve: La VRAM che serve è la somma di tre voci: pesi, KV cache e un margine. I pesi li decide il file: un 8B in Q4_K_M occupa circa 4,6 GiB, in Q8_0 circa 8. La KV cache la decidi tu, con contesto e richieste parallele: su Llama 3.1 8B in f16 vale circa 128 KiB per token, cioè 4 GiB a 32k. Q4_K_M è un punto di partenza sensato. Sali di precisione solo se, sul tuo compito, vedi errori che a Q6 o Q8 spariscono.
La domanda sbagliata e quella giusta
Non "qual è il modello più grande che entra nella mia GPU?". È: quanto contesto mi serve, quante richieste servo insieme, e quali errori posso tollerare.
La prima domanda porta a riempire la VRAM di pesi e a scoprire dopo che non resta spazio per il contesto. La seconda parte dal lavoro che il modello deve fare.
La quantizzazione è la tecnica che salva i pesi del modello con meno bit, per esempio 4 o 8 invece di 16. Riduce la memoria e spesso velocizza l'inferenza. Il prezzo è una perdita di precisione, che llama.cpp misura con perplexity e KL divergence rispetto al modello a 16 bit (README di llama-quantize).
Il conto in tre voci
Prima di scaricare un modello fai questo conto:
- Pesi — fissi per quel file. Dipendono da parametri e livello di quantizzazione.
- KV cache — variabile. Cresce con la lunghezza del contesto e con il numero di richieste in parallelo.
- Margine — buffer di calcolo, runtime, eventuale desktop sulla stessa GPU.
Il margine non ha un valore universale. Un riferimento concreto: llama-server, con l'opzione --fit attiva di default, regola i parametri per lasciare circa 1024 MiB liberi per dispositivo (README del server llama.cpp). Conta almeno 1 GiB e non progettare sul 100% della scheda.
Voce 1: i pesi
La stima rapida è: parametri × bit per peso ÷ 8. Il risultato è in byte.
Attenzione al nome del file. Q4_K_M non vuol dire "4 bit esatti". I k-quants mescolano tipi diversi tra i tensori. Nella tabella ufficiale di llama.cpp per Llama 3.1 8B, Q4_K_M vale 4,89 bit per peso e Q8_0 8,50 (README di llama-quantize). La pagina GGUF di Hugging Face elenca i bit per peso di ogni tipo.
Lo scoreboard di llama.cpp su Llama 3 8B mette in fila dimensione e distanza dal modello a 16 bit. La colonna KLD è la KL divergence media rispetto a FP16: più è bassa, più le previsioni restano vicine all'originale. I valori sono quelli pubblicati nel README di llama-perplexity, su Wikitext-2 e senza imatrix salvo dove indicato.
Tipo | Dimensione (GiB) | KLD vs FP16 |
|---|---|---|
| 14,97 | 0,0006 |
| 7,96 | 0,0014 |
| 6,14 | 0,0055 |
| 5,33 | 0,0108 |
| 4,58 | 0,0313 (0,0282 con imatrix) |
| 3,74 | 0,1019 (0,0844 con imatrix) |
| 2,96 | 0,4451 (0,33 con imatrix) |
Come leggerla. Da Q8_0 a Q4_K_M la memoria scende di oltre il 40% e la distanza cresce, ma resta lontana dai valori dei gradini sotto. Sotto Q4 la curva cambia pendenza: Q3_K_M si allontana circa tre volte più di Q4_K_M, Q2_K più di dieci volte. Risparmi meno di un gigabyte a ogni gradino e paghi sempre di più.
L'imatrix (importance matrix) è un file che dice al quantizzatore quali pesi contano di più. A parità di dimensione abbassa la distanza, come si vede nelle righe "con imatrix". Spesso chi pubblica un GGUF lo dichiara nella model card: a parità di livello, preferisci quelli.
Per i modelli grandi lo stesso conto diventa un muro. Nella tabella di llama.cpp, Llama 3.1 70B in Q4_K_M pesa 43,1 GB. Non entra in una singola scheda da 24 GB, nemmeno prima di parlare di contesto.
Voce 2: la KV cache
La KV cache conserva chiavi e valori dell'attenzione per ogni token già visto, così il modello non li ricalcola a ogni passo (documentazione di Transformers). È la voce che si dimentica, ed è quella che cresce.
La formula per token, descritta anche nel blog tecnico di NVIDIA, è:
byte per token = 2 × layer × KV heads × head_dim × byte per elementoIl 2 sta per K e V. Con la grouped-query attention, usata dalla maggior parte dei modelli recenti, conti le KV heads, non tutte le teste di attenzione. I valori sono nel config.json del modello.
Esempio su carta, con il config.json di Llama 3.1 8B: 32 layer, 8 KV heads, head_dim 128, cache in f16 (2 byte). Fa 131.072 byte, cioè 128 KiB per token.
Contesto | KV cache f16 | KV cache q8_0 (circa) |
|---|---|---|
4k token | 0,5 GiB | 0,25 GiB |
8k token | 1 GiB | 0,5 GiB |
32k token | 4 GiB | 2 GiB |
128k token | 16 GiB | 8 GiB |
A 128k la cache pesa più di tre volte i pesi in Q4_K_M. Ecco perché un modello che "entra" va in out of memory quando allunghi il contesto.
Due moltiplicatori da non perdere:
- Richieste parallele. Nelle FAQ di Ollama la memoria richiesta scala con
OLLAMA_NUM_PARALLEL×OLLAMA_CONTEXT_LENGTH. Quattro richieste da 8k sono, per la cache, un contesto da 32k. - Il default che cambia. Ollama sceglie il contesto in base alla VRAM: 4k sotto i 24 GiB, 32k fino a 48, 256k sopra. La stessa pagina consiglia almeno 64.000 token per agenti e coding tool (Context length). Su un 8B come quello dell'esempio, 64k in f16 sono 8 GiB di sola cache.
Non tutti i modelli seguono la formula piena. Con sliding window attention (Mistral, Gemma2) la cache di quei layer si ferma alla dimensione della finestra, come nota la documentazione di Transformers. Leggi il config prima di fare il conto.
Un esempio di conto, prima di scaricare
Scenario: una GPU con 12 GiB di VRAM, Llama 3.1 8B, un solo utente, contesto da 32k. È un conto su carta con i numeri delle tabelle sopra, non una misura.
Q4_K_M: 4,58 + 4 (cache f16) + 1 (margine) ≈ 9,6 GiB. Entra.Q8_0: 7,96 + 4 + 1 ≈ 13 GiB. Non entra a 32k. Entra a 8k (circa 10 GiB).Q4_K_Mcon due richieste parallele da 32k: 4,58 + 8 + 1 ≈ 13,6 GiB. Non entra, a meno di quantizzare la cache.
Il compromesso è esplicito: con la stessa scheda scegli tra più precisione nei pesi e più contesto. Non esiste una risposta giusta in astratto. Dipende dal lavoro.
Lo stesso conto in poche righe di Python, partendo dal config.json del modello:
import json
cfg = json.load(open("config.json"))
layers = cfg["num_hidden_layers"]
kv_heads = cfg.get("num_key_value_heads", cfg["num_attention_heads"])
head_dim = cfg.get("head_dim", cfg["hidden_size"] // cfg["num_attention_heads"])
def kv_gib(ctx, parallel=1, byte_per_el=2):
return 2 * layers * kv_heads * head_dim * byte_per_el * ctx * parallel / 2**30
pesi_gib = 4.58 # dimensione del file GGUF che vuoi scaricare
margine_gib = 1.0
print(round(pesi_gib + kv_gib(32768) + margine_gib, 1))Dopo il primo avvio, verifica. ollama ps mostra quanto occupa il modello, il contesto allocato e la colonna PROCESSOR. Se non leggi 100% GPU, una parte è finita in RAM di sistema e la generazione rallenta (FAQ di Ollama).
Ridurre la cache senza cambiare modello
Prima di scendere sotto Q4 con i pesi, guarda la cache. È una leva separata.
- Ollama:
OLLAMA_KV_CACHE_TYPEaccettaf16(default),q8_0eq4_0. Secondo le FAQ,q8_0usa circa metà memoria con una perdita di solito non percepibile.q4_0circa un quarto, con una perdita più visibile a contesti lunghi. Serve Flash Attention, e l'opzione vale per tutti i modelli. - llama.cpp:
-ctke-ctvimpostano il tipo di K e V, defaultf16. Flash Attention è inautodi default (README del server). - vLLM:
--kv-cache-dtype fp8. La documentazione raccomanda di calibrare le scale con llm-compressor e permette di escludere i layer più sensibili (Quantized KV Cache). - Transformers:
QuantizedCache, oppure l'offload della cache su CPU. Con contesti corti e memoria sufficiente, quantizzare la cache può peggiorare la latenza.
Un avvertimento delle FAQ di Ollama: i modelli con un GQA count alto, come Qwen2, possono risentire di più della cache quantizzata. Anche qui si prova sul proprio compito.
Nell'esempio sopra, Q4_K_M con due richieste da 32k e cache q8_0 scende intorno ai 10 GiB e torna dentro i 12.
Q4 o Q8? Dipende da cosa sbaglia il modello
Perplexity e KL divergence dicono quanto il modello quantizzato si allontana dall'originale, in media, su testo generico. Il README di llama.cpp avverte anche che la perplexity non è comparabile tra modelli diversi. Nessuna delle due ti dice se il tuo compito regge.
Il costo di un token sbagliato non è uguale ovunque. In un riassunto o in una bozza una parola diversa passa. In un JSON per il tool calling, in codice da compilare o in un numero estratto da un documento, un token sbagliato rompe l'output.
Per decidere davvero:
- Prepara un set di prova. Venti o trenta prompt presi dal tuo lavoro reale, con l'output atteso o un criterio di correttezza.
- Fai girare due livelli. Per esempio
Q4_K_MeQ6_KoQ8_0, stesso contesto, stessi parametri di sampling. - Classifica gli errori. Formato rotto, fatto sbagliato, istruzione ignorata, risposta troncata. Conta per categoria, non a sensazione.
- Decidi. Se gli errori sono gli stessi, tieni Q4 e spendi la VRAM in contesto. Se Q8 elimina proprio gli errori che ti costano, sali.
C'è anche una terza strada. A parità di memoria, un modello più grande a bit più bassi può battere uno più piccolo a bit più alti. Il conto: 14 miliardi × 4,89 bit ÷ 8 fa circa 8,6 GB, quasi quanto un 8B in Q8_0. Quale dei due sbaglia meno sul tuo set lo dice solo il set.
Se vuoi un numero oggettivo sul tuo testo, llama-perplexity calcola la KL divergence contro il modello a 16 bit. Il file dei logit di riferimento però è grande: 37 GiB per Llama 3 su Wikitext-2, secondo il README.
Errori comuni
- Scegliere dalla dimensione del file. Il file dice quanto pesano i pesi, non quanto costa il contesto che ti serve.
- Alzare il contesto o il parallelismo senza rifare il conto. È la causa più comune dell'out of memory "improvviso".
- Quantizzare un modello già quantizzato. llama.cpp lo permette solo con
--allow-requantizee avverte che la qualità può peggiorare molto. Parti sempre da un file a 16 o 32 bit. - Dare per gratis la cache quantizzata. Spesso costa poco, ma dipende da modello e compito.
- Dimenticare il disco. Oltre alla VRAM conta lo spazio per i file. Con Ollama 0.40, per esempio, durante la conversione i modelli occupano per un po' circa il doppio: i dettagli sono in Ollama 0.40.
Cosa conviene fare adesso
- Apri il
config.jsondel modello: layer, KV heads, head_dim, eventuale sliding window. - Scrivi contesto e richieste parallele reali, non quelle massime.
- Fai il conto: file GGUF + KV cache + almeno 1 GiB.
- Parti da
Q4_K_M, con imatrix se disponibile. - Se la VRAM non basta, nell'ordine: riduci il contesto, quantizza la cache a
q8_0, scegli un modello più piccolo. Solo dopo, scendi sotto Q4. - Prova Q4 contro Q6/Q8 sul tuo set di prompt e decidi dagli errori.
- Verifica con
ollama ps(o con i log del runtime) che tutto stia in GPU.
Se poi esponi il modello ai tuoi tool tramite un endpoint compatibile, i parametri da controllare sono in API OpenAI-compatible in locale.
Domande rapide
Q4_K_M è il default giusto? È il punto di partenza più diffuso, ed è il livello che la documentazione di llama.cpp usa negli esempi. Non è una garanzia per il tuo compito: il set di prova serve a quello.
Su un Mac con memoria unificata vale lo stesso conto? Sì, le tre voci sono le stesse. La differenza è che la memoria è condivisa con sistema e app, quindi il margine va tenuto più largo.
E i modelli MoE? Per la memoria dei pesi conta il numero totale di parametri, perché il file contiene tutti gli esperti. I parametri attivi per token incidono soprattutto sulla velocità.
Prossima puntata: fine-tuning con Unsloth, cioè quando ha senso adattare un modello piccolo invece di comprarne uno più grande in VRAM. Il percorso completo resta sull'hub AI Engineering.



