Anatomia degli LLM 🧠
I Large Language Models (LLM) sembrano scatole nere: scrivi un prompt, esce una risposta, e in mezzo miliardi di parametri fanno “AI”. Ma dentro non c’è magia: c’è una pipeline precisa, ripetibile, ottimizzata fino all’ultimo FLOP.
Questa guida apre quella scatola seguendo il percorso completo dei dati: testo in input -> token -> vettori -> blocchi Transformer -> logits -> token generato.
Se capisci questa catena, capisci quasi tutto: limiti, costi, latenza, allucinazioni, qualità output.
Tokenizzazione: da testo a numeri 🔢
Un LLM non legge parole e frasi come facciamo noi. Legge una sequenza di ID numerici prodotti da un tokenizer.
Il tokenizer spezza il testo in pezzi chiamati token: una parola intera, una parte di parola, un segno di punteggiatura. Ogni token ha un ID nel vocabolario del modello.
|
|
Perché non usare parole intere? Sarebbero troppo rigide: nomi nuovi, errori di battitura, inflessioni, codice, testi multilingua produrrebbero continuamente parole mai viste. Perché non usare singoli caratteri? Risolverebbe il problema ma allungherebbe troppo ogni input, riducendo la finestra di contesto utile. I token subword sono il compromesso ragionevole.
Nella pratica, tokenizer moderni usano varianti di BPE o Unigram. Inoltre introducono token speciali (inizio/fine testo, separatori, tool-call markers, ecc.). Questi token non sono dettagli cosmetici: influenzano direttamente il comportamento del modello in chat, coding e function calling.
Costo token e finestra di contesto
Due prompt semanticamente simili possono avere costo molto diverso in token. Testo con JSON verboso, codice o molte lingue miste tende a esplodere in token. Questo impatta:
- costo inferenza (API o GPU time)
- latenza
- spazio disponibile per contesto utile
Regola pratica: ottimizzare prompt significa spesso ottimizzare token, non parole. In pratica, stai ottimizzando la context window disponibile.
Embedding: da ID a vettori 📐
Gli ID numerici sono solo etichette. Il token 15339 non è “vicino” a 15340 in alcun modo significativo.
Il layer di embedding trasforma ogni ID in un vettore (una lista di numeri a virgola mobile), un punto in uno spazio multidimensionale dove token con significati simili finiscono vicini.
flowchart LR
A["ID token<br/>3721"] --> B["Embedding layer<br/>(lookup table)"]
B --> C["Vettore<br/>[0.72, -0.38, 0.11, ...]"]
Questo è il momento in cui i simboli discreti entrano in uno spazio continuo. Una volta diventati vettori, il modello può confrontarli, combinarli, trasformarli.
Importante: l’embedding iniziale è senza contesto. La stessa sequenza di token ottiene lo stesso vettore indipendentemente dal significato: “mole” in “talpa” e “mole” in “molecola” partono dallo stesso embedding. Saranno i layer successivi a riscrivere quel vettore in base alle parole circostanti.
In molti modelli, matrice embedding input e matrice proiezione finale sul vocabolario sono condivise (weight tying). Meno parametri, spesso migliore efficienza statistica.
Attenzione: come i token si parlano 🗣️
Gli embedding da soli sono troppo poveri. Il modello ha bisogno che i token si scambino informazioni. È qui che entra in gioco il meccanismo di attenzione.
Per ogni token, il modello crea tre viste apprese:
- Query (Q): cosa cerca questo token
- Key (K): cosa pubblicizza di sé
- Value (V): le informazioni che può condividere
Il modello confronta Query con Key per calcolare punteggi di attenzione, li normalizza con softmax, e usa quei pesi per mescolare i Value. Q e K decidono dove fluiscono le informazioni. V è l’informazione che fluisce.
Forma compatta operazione per una testa:
$$ Attention(Q,K,V)=\mathrm{softmax}\left(\frac{QK^{T}}{\sqrt{d_k}} + M\right)V $$
Dove $M$ è la maschera causale: mette $-\infty$ sulle posizioni future, così il modello non “bara” guardando token non ancora generati.
flowchart LR
subgraph Input
T1["token<br/>A"]
T2["token<br/>B"]
T3["token<br/>C"]
end
subgraph QKV["Proiezioni Q/K/V"]
Q1["Q"]
K1["K"]
V1["V"]
end
subgraph Att["Attenzione"]
S["Q·K^T / √d"]
SM["softmax"]
M["∑ weight × V"]
end
T1 --> Q1
T1 --> K1
T1 --> V1
Q1 & K1 --> S --> SM
SM & V1 --> M
Multi-Head Attention
Una frase contiene molti tipi di relazione contemporaneamente. Un aggettivo modifica un nome. Un pronome si riferisce a qualcosa di precedente. Una parentesi chiusa corrisponde a una aperta.
La Multi-Head Attention esegue più operazioni di attenzione in parallelo, ciascuna con proiezioni apprese diverse. Ogni testa può imparare un tipo diverso di relazione. I risultati vengono combinati e proiettati nella dimensione del modello.
Non tutte le teste imparano pattern eleganti da slide. Alcune fanno tracking sintattico, alcune aiutano la copia di pattern locali, alcune sembrano ridondanti. Funziona comunque perché la somma finale sfrutta la ridondanza in modo robusto.
In molte architetture recenti trovi anche GQA (Grouped-Query Attention): più teste Query condividono un numero minore di teste Key/Value. Effetto pratico: meno memoria KV in inferenza, con qualità spesso vicina alla MHA classica.
Positional Encoding / RoPE
“L’uomo morde il cane” e “Il cane morde l’uomo” contengono le stesse parole, ma non significano la stessa cosa. L’attenzione confronta i token per contenuto, ma l’ordine è fondamentale.
I modelli moderni usano RoPE (Rotary Positional Embeddings), che ruota parti dei vettori Query e Key in base alla posizione del token. Quando l’attenzione confronta una Query con una Key, il risultato dipende sia dal contenuto che dalla posizione relativa.
RoPE migliora la generalizzazione su contesti lunghi, ma non fa miracoli: oltre certe lunghezze, la qualità può degradare (“lost in the middle”, recupero di fatti remoti meno stabile).
Complessita: attenzione costa $O(n^2)$
Se una sequenza ha $n$ token, la mappa di attenzione è una matrice $n \times n$. Raddoppiare il contesto non raddoppia il costo: lo quadruplica. Da qui nascono ottimizzazioni come FlashAttention, sliding window, paged attention e varianti sparse.
Feed-Forward Network: riscrivere i significati 🏗️
Dopo l’attenzione, ogni token ha raccolto informazioni dagli altri. Ma il vettore di “mole” è ancora ambiguo tra “animale” (inglese) e “unità di misura”. Il Feed-Forward Network (FFN) risolve questa ambiguità.
Il FFN applica la stessa piccola rete neurale a ogni token in modo indipendente. Espande il vettore in una rappresentazione più ampia, applica una non-linearità (tipicamente GELU o SwiGLU), poi lo riproietta alla dimensione originale. Risultato: un vettore più ricco e contestualizzato.
flowchart LR
subgraph Encoder
A["Self-Attention"] --> B["Add & Norm"]
B --> C["Feed-Forward"]
C --> D["Add & Norm"]
end
Ogni blocco è avvolto da residual connection (Add) e normalization (LayerNorm o RMSNorm). Le residual connection preservano il gradiente durante l’addestramento e permettono stack di decine o centinaia di layer.
Nei decoder moderni prevale lo schema pre-norm: normalizzazione prima del sottoblocco, training più stabile su profondità elevate.
Architettura Transformer completa 🏛️
Un LLM moderno (decoder-only come GPT, Llama, Mistral) stacka decine di questi blocchi in sequenza. Ogni blocco mantiene la stessa forma del tensore (sequenza x dimensione modello), ma riscrive progressivamente il contenuto informativo dei vettori.
flowchart LR
subgraph Pipeline
T["Tokenizzazione"] --> E["Embedding"]
E --> B1["Blocco 1"]
B1 --> B2["Blocco 2"]
B2 --> D["... N blocchi"]
D --> L["Layer norm finale"]
L --> P["Proiezione su vocabolario"]
P --> S["Softmax"]
S --> G["Token generato"]
end
In vari modelli recenti compaiono anche architetture MoE (Mixture of Experts): il FFN viene sostituito da un pool di “esperti”, e un router attiva solo pochi esperti per token. Vantaggio: capacità enorme senza attivare tutti i parametri a ogni passo.
Generazione: da logits a testo 🎲
Dopo l’ultimo blocco Transformer, il modello non ha ancora prodotto una parola. Ha un vettore che rappresenta la sua “opinione” su quale dovrebbe essere il prossimo token.
Un layer lineare proietta questo vettore in uno score per ogni token del vocabolario. Questi score grezzi sono chiamati logits. La softmax li trasforma in probabilità.
Dettaglio importante: in training il modello produce logits per tutte le posizioni della sequenza (supervisione next-token su ogni step). In inferenza chat, invece, usiamo la distribuzione dell’ultima posizione per scegliere il prossimo token da emettere.
flowchart LR
H["Hidden vector"] --> L["Linear layer<br/>(vocabolario × 1)"]
L --> LG["Logits<br/>(score grezzi)"]
LG --> SM["Softmax (+ temperatura)"]
SM --> P["Probabilità"]
P --> DC["Decoding<br/>(greedy / top-k / top-p)"]
DC --> T["Token scelto"]
A questo punto entrano in gioco gli iperparametri di campionamento:
- Temperatura (T): controlla la creatività. Più alta → distribuzione più uniforme → più casualità. Più bassa → più deterministica.
- Top-k / Top-p: strategie di campionamento; top-k considera solo i k token più probabili, top-p seleziona dinamicamente l’insieme minimo sopra soglia cumulativa.
Altri controlli utili in produzione:
- Repetition penalty: riduce loop e ripetizioni ossessive.
- Frequency/presence penalty: spinge modello a introdurre contenuti nuovi.
- Stop sequences: taglia output su delimitatori noti.
Next-token prediction -> generazione completa
Il passaggio che fa sembrare “magico” un LLM è in realtà un loop molto semplice:
- predici distribuzione del prossimo token
- scegli un token (greedy o sampling)
- appendi il token al contesto
- ripeti
Questo ciclo autoregressivo è esattamente ciò che vedi quando la risposta arriva in streaming token-per-token.
Prefill vs decode (latenza reale)
L’inferenza autoregressiva ha due fasi operative:
- Prefill: il modello processa tutto il prompt iniziale in parallelo.
- Decode: genera token uno alla volta, riusando KV cache.
Il prefill domina il costo su prompt lunghi. Il decode domina la percezione utente (token/sec). Ottimizzare UX significa bilanciare entrambe.
sequenceDiagram
participant U as Utente
participant M as Modello
participant K as KV cache
U->>M: Prompt completo
M->>M: Prefill parallelo su tutti i token
M->>K: Salva Key/Value iniziali
loop Decode autoregressivo
M->>K: Legge contesto dalla cache
M->>M: Calcola prossimo token
M->>K: Appende nuovo K/V
M-->>U: Stream token
end
Addestramento: come nasce un LLM 📚
La costruzione di un LLM avviene in fasi:
flowchart LR
A["Raccolta dati"] --> B["Data curation\n deduplica, filtri, quality"]
B --> C["Pre-training\n next-token prediction"]
C --> D["SFT\n dialoghi supervisionati"]
D --> E["Preference tuning\n RLHF o DPO"]
E --> F["Valutazione\n safety, quality, latency"]
F --> G["Deploy\n serving e monitoraggio"]
1. Pre-training
Il modello viene esposto a enormi quantità di testo e addestrato a predire il prossimo token. Obiettivo tipico: minimizzare la cross-entropy loss tra distribuzione predetta e token corretto. La backpropagation calcola il gradiente dell’errore rispetto a ogni peso, l’ottimizzatore (spesso AdamW) aggiorna i pesi. Ripetuto miliardi di volte: emergono pattern linguistici, conoscenze, strutture sintattiche.
Non è solo “più dati = meglio”. Serve data curation: deduplica, rimozione spam, bilanciamento dei domini, filtraggio di contenuti problematici.
2. Supervised Fine-Tuning (SFT)
Il modello base è un “completatore di frasi”, non un assistente. Con SFT gli si mostrano migliaia di dialoghi (domanda + risposta ideale) scritti da esperti umani. Il modello impara a rispondere in modo utile, veritiero, non pericoloso.
3. Reinforcement Learning (RLHF / DPO)
Il modello genera risposte a problemi con soluzione nota. Le risposte giuste vengono selezionate e usate per un ulteriore addestramento. Questa fase ha prodotto scoperte sorprendenti: i modelli “pensanti” (come DeepSeek R1) sviluppano capacità di autoriflessione, ricontrollo e ragionamento articolato in più passi.
Nella pratica moderna, molte pipeline usano DPO/varianti preference-based per evitare parte della complessita ingegneristica del RL classico, mantenendo un buon allineamento alle preferenze umane.
Compute, scaling laws, trade-off
La qualità finale dipende dall’equilibrio tra:
- numero parametri
- token di training
- budget compute
Le scaling laws mostrano trend regolari: aumentando questi fattori, la loss scende in modo prevedibile fino a colli di bottiglia pratici (costo energia, banda memoria, qualità dati).
Considerazioni pratiche ⚙️
KV Cache
Durante la generazione, il modello calcola Key e Value per ogni token. Senza caching, a ogni passo dovrebbe ricalcolare tutto da capo. La KV Cache immagazzina i tensori K e V dei token già processati, riducendo drasticamente il costo computazionale a scapito di più memoria. Il compromesso è ciò che rende fattibile la generazione interattiva.
Architetture con GQA aiutano anche qui: condividendo parte delle K/V tra gruppi di query heads, la cache cresce meno e il serving multi-utente scala meglio.
Su sistemi multi-utente, la gestione cache è cruciale: eviction policy, paging e continuous batching determinano il throughput reale molto più dei TFLOPS teorici.
Quantizzazione
I modelli grandi sono limitati dalla memoria. La quantizzazione riduce il numero di bit per rappresentare i pesi (da 16 o 32 bit a 8 o 4 bit), comprimendo il modello e accelerando l’inferenza, con una perdita minima di qualità. È ciò che permette di far girare modelli da miliardi di parametri su hardware consumer.
Attenzione: non esiste “4-bit uguale per tutti”. Lo schema di quantizzazione (per-canale, per-gruppo, activation-aware) cambia molto il trade-off tra qualità e velocità.
Context window e limiti reali
Finestra lunga non significa memoria perfetta. Problemi tipici:
- attenzione dispersa su contesti enormi
- degrado recupero informazioni “in mezzo” al prompt
- aumento latenza/costo non lineare
Per task enterprise, spesso conviene combinare contesto mirato + retrieval (RAG) invece di buttare dentro tutto archivio.
Allucinazioni: perché succedono
Un LLM ottimizza la probabilità del prossimo token, non la verità assoluta. Quando il contesto è ambiguo o mancano dati affidabili, il modello può generare testo plausibile ma falso. Tecniche utili per ridurre il rischio: grounding su fonti, citazioni verificabili, tool use, validazione post-generation.
Modelli encoder-only, decoder-only, encoder-decoder
Non tutti i Transformer sono uguali:
- Encoder-only (BERT): leggono tutto il contesto, usati per classificazione e embedding.
- Decoder-only (GPT, Llama, Mistral): generano testo in modo autoregressivo.
- Encoder-Decoder (T5, Transformer originale): traduzione e summarization.
I modelli conversazionali moderni sono quasi tutti decoder-only.
flowchart TD
T["Famiglie Transformer"] --> E["Encoder-only\n BERT"]
T --> D["Decoder-only\n GPT, Llama, Mistral"]
T --> ED["Encoder-Decoder\n T5"]
E --> E1["Task: classificazione\n e embedding"]
D --> D1["Task: generazione\n autoregressiva"]
ED --> ED1["Task: traduzione\n e summarization"]
Esempi pratici: dal concetto al terminale 🧪
La teoria e utile, ma quando devi stimare costi o debug di output strani servono numeri concreti. Qui sotto trovi due snippet TypeScript copiabili.
Esempio 1: stima token e costo prompt
|
|
|
|
Output atteso (valori indicativi):
|
|
Esempio 2: decoding controllato (temperatura, top-k, top-p)
|
|
Output atteso:
|
|
Best practice e antipattern ⚖️
Cosa fare
- Misura sempre i token in pre-produzione: il costo vero vive li, non nel numero di caratteri.
- Separa prefill e decode nelle metriche: ottimizzi colli di bottiglia diversi.
- Aggiungi grounding (RAG, tool affidabili, citazioni) per ridurre allucinazioni.
- Versiona prompt e parametri (temperatura, top-p, stop sequence) come fosse codice.
Cosa evitare
- Antipattern: prompt-fiume con tutto il database dentro “per sicurezza”.
- Antipattern: temperatura alta in task critici (compliance, security, numeri).
- Antipattern: valutare solo la quality percepita ignorando latenza e costo.
- Antipattern: zero observability su token/sec, hit-rate cache, errori tool-call.
Tool utili in pratica 🧰
- tiktoken: stima token e budgeting.
- vLLM: serving ad alte prestazioni con paged attention.
- llama.cpp: inferenza locale e test su modelli quantizzati.
- Langfuse: tracing, osservabilita e analisi costi/latenza.
Checklist operativa: prima di mettere in produzione un LLM ✅
- Ho stimato token input/output per i casi d’uso principali.
- Ho definito guardrail (stop sequence, policy, validazioni post-output).
- Ho separato KPI di prefill e decode (latenza p95, token/sec).
- Ho testato almeno una strategia anti-allucinazione (RAG o tool grounding).
- Ho benchmarkato una baseline full-precision e una quantizzata.
- Ho monitoraggio continuo su costo, errori e quality drift.
Approfondimenti correlati 🔗
- Prompt Engineering: parametri e strategie di prompting in dettaglio.
- AI generative for Dummies: versione introduttiva se parti da zero.
- Diagrams with Generative AI: flussi visuali e documentazione tecnica.
- Context engineering: la vera magia dietro l’AI: perché il contesto batte il prompt da solo.
Riferimenti 📖
Fonti originali usate in questa guida
-
3Blue1Brown — Transformers, the tech behind LLMs (Deep Learning Chapter 5): video principale usato per il flusso didattico token -> embedding -> attenzione -> logits -> sampling.
-
Roy van Rijn — The Anatomy of an LLM: guida visuale interattiva che copre pipeline, training, KV cache e quantizzazione.
-
Kuhan Sundaram — The Anatomy of an LLM (Part 1): spiegazione divulgativa di attenzione e blocchi Transformer.
-
Enrico Piccinin — Dentro ad un LLM come ChatGPT: sintesi operativa del percorso base model -> assistente -> allineamento.
-
3Blue1Brown — Neural Networks / Transformers: serie visuale su reti neurali e Transformer.
-
Jay Alammar — The Illustrated Transformer: spiegazione visuale classica dell’architettura Transformer.
-
Andrej Karpathy — Neural Networks: Zero to Hero: percorso code-first per costruire un GPT da zero.
-
Attention Is All You Need: il paper originale del Transformer.
-
RoFormer: Enhanced Transformer with Rotary Position Embedding: base teorica di RoPE.
-
FlashAttention: attenzione esatta memory-efficient per contesti lunghi.
Glossario rapido 📘
- Token: unità minima di testo usata dal modello (parola, subword, punteggiatura, spazio).
- Tokenizer: algoritmo che converte testo in sequenze di token ID.
- Vocabolario: insieme dei token noti al modello.
- Embedding: vettore numerico associato a un token ID.
- Context window: numero massimo di token processabili in una singola inferenza.
- Query (Q): rappresentazione di cosa un token sta cercando nel contesto.
- Key (K): rappresentazione di cosa un token rende confrontabile.
- Value (V): contenuto informativo che un token può trasferire ad altri token.
- Self-attention: meccanismo con cui i token della stessa sequenza si pesano tra loro.
- RoPE: codifica posizionale rotazionale applicata a Q/K per rendere l’attenzione sensibile all’ordine.
- FFN (Feed-Forward Network): sottorete che trasforma ogni token in modo indipendente dopo l’attenzione.
- GELU: funzione di attivazione non lineare usata spesso nei blocchi FFN dei Transformer.
- SwiGLU: variante gated del FFN che combina meccanismi di gating con attivazione tipo SiLU/Swish.
- LayerNorm: normalizzazione applicata sulle attivazioni di ciascun token per stabilizzare il training.
- RMSNorm: variante di normalizzazione basata su root-mean-square, più leggera di LayerNorm in molti setup.
- Residual connection: collegamento di skip/add che preserva informazione e aiuta il flusso del gradiente.
- Pre-norm: schema in cui la normalizzazione avviene prima dei sottoblocchi attenzione/FFN.
- MoE (Mixture of Experts): architettura con più esperti attivati selettivamente per token.
- Logits: score grezzi per ciascun token del vocabolario prima della softmax.
- Softmax: funzione che trasforma logits in probabilità normalizzate.
- Temperatura: parametro che rende il decoding più deterministico (bassa) o più creativo (alta).
- Top-k / Top-p: strategie di campionamento che limitano i token candidati.
- Prefill: fase in cui il prompt iniziale è processato in parallelo.
- Decode: fase autoregressiva che genera token uno alla volta.
- KV cache: cache di Key/Value dei token già visti, usata per accelerare il decode.
- GQA: variante attenzione in cui più query heads condividono meno K/V heads, riducendo memoria.
- Quantizzazione: riduzione di precisione numerica dei pesi per risparmiare memoria e aumentare throughput.
- SFT: fine-tuning supervisionato su esempi domanda-risposta.
- RLHF / DPO: tecniche di preference tuning per allineare le risposte alle preferenze umane.
- Cross-entropy loss: funzione obiettivo usata per penalizzare distribuzioni predette lontane dal token corretto.
- Backpropagation: algoritmo che propaga l’errore all’indietro per calcolare i gradienti.
- AdamW: ottimizzatore adattivo molto usato nel training dei Transformer.
- Allucinazione: output plausibile ma non supportato da fatti affidabili.