SLM · Fondamenti

Come funziona una LLM

Un motore di probabilità sui token: cosa calcola, come genera, cosa sa davvero — e perché funziona.

1. Motore di probabilità

Un Large Language Model non "capisce" il testo nel senso ordinario del termine. Esegue un calcolo: data una sequenza di token in ingresso, restituisce una distribuzione di probabilità sull'intero vocabolario.

P( token_{t+1}  |  token_1, token_2, ..., token_t )

La domanda a cui risponde il modello è: "Dati questi token in quest'ordine, qual è la probabilità di ciascun token del vocabolario come prossima uscita?"

Esempio concreto. Dato il contesto "il cane abbaia nel", il modello potrebbe restituire:

P("cortile")  = 0.18
P("parco")    = 0.12
P("bosco")    = 0.09
P("silenzio") = 0.06
...             ← altri 15.995 token con probabilità residua

Non esiste una risposta giusta. Esiste una distribuzione. Il testo generato nasce dal campionamento ripetuto da questa distribuzione — un token alla volta, ogni token aggiunto al contesto per il passo successivo.

2. Il token — unità atomica

Il modello non lavora con parole intere né con singoli caratteri. Lavora con token: sotto-unità di testo determinate statisticamente dal corpus di addestramento tramite BPE (Byte-Pair Encoding).

BPE parte dai caratteri e unisce iterativamente le coppie più frequenti finché non raggiunge la dimensione del vocabolario target. Il risultato: parole frequenti diventano un token singolo, parole rare si spezzano in pezzi.

"programmare"  →  [4721]          # token unico — parola comune
"trasparenza"  →  [8843, 12041]   # due token
"antidisestablishmentarianism"  →  [5-6 token]  # parola rarissima

Nel nostro SLM con vocabolario da 16.000 token addestrato su italiano:

Il tokenizer è un contratto tra dati e modello. Il modello addestrato con SPM da 16k non può usare un tokenizer diverso — ogni checkpoint è inseparabile dal suo vocabolario. Vedi Vocabolario, Embedding e Parametri per i dettagli.

3. La finestra di contesto

Il modello non ha memoria illimitata. Vede solo gli ultimi seq_len token — la finestra di contesto. Tutto il testo precedente è invisibile.

seq_len = 512  →  ~350 parole  →  ~2 pagine di testo

Quando si supera seq_len, i token più vecchi vengono scartati. Non è una limitazione accidentale — l'attention ha complessità O(seq_len²) in memoria. Raddoppiare seq_len quadruplica la RAM richiesta.

Nel progetto SLM abbiamo ridotto da 512 a 256 per restare nei limiti degli 8 GB di memoria unificata disponibili. Su macchine da 24–32 GB si può tornare a 512 o oltre.

4. La forward pass

Ogni volta che il modello elabora un batch di token, attraversa sempre lo stesso percorso deterministico e differenziabile. Durante il training questo percorso viene eseguito miliardi di volte.

token_id = 42 [batch, seq_len] Embedding [B, S] → [B, S, d_model] + Pos. Encoding [B, S, d_model] TRANSFORMERBLOCK × N Multi-Head Attention Feed-Forward (GELU) [B, S, d_model] LayerNorm [B, S, d_model] lm_head (Linear) [B, S, d_model] → [B, S, vocab_size] Softmax → P(token) ∑ = 1 · [vocab_size]

Il passaggio critico è lm_head: uno strato Linear che proietta ogni posizione da d_model (es. 512) a vocab_size (16.000). Produce un numero per ogni token del vocabolario — i logit. Softmax normalizza i logit in probabilità.

Durante il training, la cross-entropy loss misura quanto la distribuzione predetta si discosta dal token corretto. Il gradiente di questa loss rispetto a ogni parametro guida l'aggiornamento AdamW. Vedi AdamW e Ottimizzazione.

5. Dal logit alla parola

Il modello produce 16.000 logit. Come si sceglie quale token emettere? Dipende dalla strategia di sampling.

Greedy decoding

Prende sempre il token con probabilità massima: argmax(logits). Deterministico e veloce, ma produce output prevedibili e ripetitivi. È quello che usa attualmente --generate in train.py.

Temperature scaling

Prima del softmax, divide tutti i logit per un valore T:

logits_scalati = logits / T
probabilità = softmax(logits_scalati)
T < 1 I picchi di probabilità si accentuano. Output più coerente e prevedibile.
T = 1 Distribuzione originale del modello. Nessuna modifica.
T > 1 La distribuzione si appiattisce. Output più vario, più soggetto a errori.

Top-k sampling

Considera solo i k token con probabilità più alta, azzera tutti gli altri, poi campiona. Con k=50, ogni step sceglie tra i 50 candidati più plausibili.

Top-p (nucleus) sampling

Considera il minimo insieme di token la cui probabilità cumulata raggiunge p. Se i primi 3 token coprono già il 90% della probabilità, con p=0.9 si campiona solo da quei 3. L'insieme si adatta dinamicamente alla forma della distribuzione.

Temperatura, top-k e top-p si usano tipicamente insieme. Valori comuni per testo narrativo: T=0.8, top_k=50, top_p=0.95. Sono i parametri della fase 5 nel piano di sviluppo — vedi la roadmap.

Logit bias — escludere token specifici

A volte non basta regolare la temperatura o il nucleus: si vuole impedire al modello di generare certi token del tutto. Il logit bias permette di farlo: prima che il softmax trasformi i logit in probabilità, si somma un valore scelto al logit di uno o più token specifici. Sommando −100 al logit di un token lo si esclude praticamente dal campionamento: la probabilità risultante è così vicina a zero da non poter mai essere selezionata.

Nella pratica, ogni token nel vocabolario ha un indice numerico. Passare [[id, -100]] a una richiesta di inferenza significa "non generare mai questo token, qualunque cosa suggerisca il modello". Si possono escludere decine o centinaia di token in un colpo solo.

Perché è stato necessario con il nostro modello. La conversione in formato GGUF richiede l'aggiunta di 256 token speciali — uno per ogni byte possibile (0x00–0xFF) — agli indici 16.000–16.255. Servono per garantire che il tokenizer sappia sempre come codificare qualsiasi testo, anche quello con caratteri insoliti. Il problema: il corpus di training conteneva artefatti di encoding (tab, caratteri di controllo, frammenti binari da HTML mal decodificato). Il modello ha imparato che questi byte compaiono dopo certe sequenze. Quando incontra un contesto insolito — un nome proprio raro, un prompt che non ha mai visto — non sa bene cosa generare, la distribuzione di probabilità si appiattisce, e i byte token, anche con probabilità bassa, diventano competitivi. Il campionatore li pesca. Una volta partito su questa strada, il contesto diventa "sequenza di byte", il che rinforza la scelta di altri byte: loop. L'output è una serie di caratteri illeggibili.
La soluzione: escludere tutti e 256 i byte token con logit bias = −100. Il vocabolario operativo torna a 16.000 token, il modello non può più imboccare quella via d'uscita.

Il logit bias è uno strumento chirurgico: non cambia il modello, non richiede un nuovo training, agisce solo in fase di generazione. È utile ogni volta che si conosce in anticipo una categoria di token che non dovrebbe mai comparire nell'output.

6. Cosa sa davvero il modello

Il modello non ha un dizionario di regole grammaticali. Ha distribuzioni condizionali compresse in milioni di parametri float32.

"Sa" che dopo "il" viene quasi sempre un sostantivo o aggettivo maschile — non perché glielo abbia detto qualcuno, ma perché ha osservato questa co-occorrenza miliardi di volte nel corpus. "Sa" che "non" precede spesso un verbo — stessa ragione. La conoscenza è implicita e distribuita: nessun singolo parametro codifica una regola; le regole emergono dall'interazione dell'intero sistema durante la forward pass.

Il nostro SLM da 46M parametri, addestrato su Wikipedia italiana, conosce la distribuzione superficiale della lingua. Non ragiona, non pianifica, non verifica. Produce token statisticamente plausibili dato il contesto. La comprensione emerge solo a scale molto più grandi (1B+ parametri, dati enormi e variegati).

7. Genesi del progetto: obiettivi e vincoli

Prima di scegliere un'architettura vale la pena capire cosa stiamo costruendo, perché, e con quali vincoli concreti. Le scelte tecniche — scala dei parametri, tokenizer, seq_len — non sono arbitrarie: sono la risposta a questi vincoli.

L'obiettivo: un modello base riaddestrabilee

Il SLM da 46 milioni di parametri non è pensato per la produzione conversazionale diretta — quella scala richiede almeno 1–3B parametri per qualità accettabile. Il suo scopo è duplice:

I due track paralleli

Il progetto segue due binari simultanei e complementari:

Vincoli hardware: sviluppo su Mac

Training, inferenza e iterazione girano su un singolo Mac. Non è negoziabile: non ci si può appoggiare a un server cloud per ogni esperimento intermedio. Apple Silicon usa memoria unificata — CPU e GPU (MPS) condividono lo stesso pool fisico, eliminando i trasferimenti PCIe tipici di NVIDIA ma rendendo ogni GB occupato dal sistema un GB sottratto al training.

MacchinaRAM unificataLimite pratico SLMStato
Mac Air M28 GB seq_len=256, batch=8, modello ~46M fp32 Attuale
Mac Air M532 GB seq_len=512+, batch=16+, modelli 1B+ Target

Il vincolo si manifesta soprattutto nell'attention: la matrice di attenzione scala come batch × seq_len². Con batch=16 e seq_len=512 su 8 GB si va in OOM (out of memory). Ridurre seq_len da 512 a 256 dimezza le sequenze ma quadruplica la capienza effettiva in RAM — è il compromesso che usiamo attualmente.

Strategia: pre-training → fine-tuning → LoRA

Il percorso pianificato:

LoRA è nella fase 10 della roadmap. Il pre-training attuale (fase 1–4) è il prerequisito: serve un modello base ben addestrato prima di applicarci qualunque tecnica di adattamento.

8. Decoder-only — perché solo generazione

Esistono tre architetture principali di transformer, con usi diversi:

ArchitetturaEsempiAttenzioneUso tipico
Encoder-only BERT, RoBERTa Bidirezionale — vede tutto il testo in entrambe le direzioni Classificazione, NER, similarity
Decoder-only GPT, LLaMA, SLM Causale — ogni token vede solo i token alla sua sinistra Generazione testuale
Encoder-Decoder T5, BART, mT5 Encoder bidirezionale + decoder causale Traduzione, summarization

Il nostro SLM è decoder-only. La causal mask garantisce che il token in posizione t veda solo i token nelle posizioni 0..t — mai quelli futuri. Questo rende possibile il training in parallelo (la loss si calcola su tutti i token del batch in una sola forward pass) e la generazione autoregressiva (si genera un token alla volta, ognuno aggiunto al contesto per il passo successivo).

Per il writing assistant descritto nella sezione precedente — generazione di continuazioni nello stile dell'autore — decoder-only è la scelta corretta. Un encoder-only (BERT) non genera; un encoder-decoder (T5) genera ma è pensato per trasformazioni input→output strutturate, non per completamento libero.

🗓
Roadmap di sviluppo Dove siamo e cosa viene dopo: sampling avanzato, KV-cache, LoRA, quantizzazione.