SLM — Small Language Model

Cronaca di un modello

Come si costruisce un modello linguistico italiano da zero — il racconto, fase per fase.

Settembre 2026 Completata

Capire ogni pezzo prima di usarlo

Il punto di partenza era una convinzione semplice: se devo costruire un prodotto sopra un modello linguistico, devo capire cosa c'è dentro — non come si chiama, ma come funziona davvero. Così abbiamo deciso di costruirne uno da zero, in Python, senza copiare architetture preconfezionate.

La prima versione non aveva nemmeno un vocabolario di parole: operava carattere per carattere. Ogni lettera, spazio, punto — un token separato. Il modello imparava la probabilità del prossimo carattere dato il contesto precedente. Ingenuo, ma sufficiente per capire i meccanismi fondamentali: come l'attenzione "guarda" indietro nel testo, come il gradiente scorre durante l'apprendimento, perché la loss scende lentamente all'inizio e poi accelera.

Non imparare come si usa uno strumento prima di capire cosa fa. Il valore didattico di questa fase non stava nel risultato — stava nel processo.

Dopo 5.000 passi di training su un corpus di narrativa italiana contemporanea (552 KB), la loss era 1.73. Il modello generava testo italiano leggibile, con struttura sintattica approssimativa. Non era il punto — il punto era che capivamo ogni riga di codice.

Risultato
Val loss 1.73 · 5.000 step · 46 min su Mac M2 · architettura scritta interamente from scratch
· · ·
26 settembre 2026 Completata

Dare al modello un vocabolario vero

Operare carattere per carattere ha un limite pratico: il contesto utile è breve. "La capitale d'Italia" è 20 caratteri — e il modello deve usarli tutti per rappresentare un'informazione che un essere umano codifica in cinque parole. La soluzione è il tokenizer: un vocabolario di pezzi di parola che condensano la struttura della lingua.

Abbiamo addestrato un tokenizer BPE su Wikipedia italiana — 208 milioni di caratteri di testo enciclopedico. Il vocabolario risultante conta 16.000 token: abbastanza per coprire la morfologia italiana (prefissi, suffissi, desinenze) senza esplodere in dimensioni ingestibili. Poi abbiamo scalato il modello: da 6 a 8 strati, dal milione di parametri a 46 milioni.

Il risultato interessante non fu la loss finale (4.08, perplexity circa 59) ma quello che il modello generava: frasi sulla geopolitica internazionale, coerenza narrativa su più clausole, un registro enciclopedico riconoscibile. Era diventato qualcosa che assomigliava a conoscenza.

Questo checkpoint è rimasto nel progetto con una funzione inaspettata: è diventato il "giudice" per valutare la qualità dei testi futuri — un misuratore di quanto un documento sia simile all'italiano enciclopedico che conosce.
Risultato
Val loss 4.08 (perplexity ~59) · 30.000 step · Wikipedia IT · checkpoint usato come quality scorer PPL
· · ·
27 settembre 2026 Completata

Una modifica architetturale e il primo salto su cloud

C'è una differenza tra capire qualcosa e misurarla. Sapevamo dalla letteratura che RoPE — un modo alternativo di codificare la posizione dei token nel testo — funzionava meglio del metodo tradizionale, soprattutto su sequenze lunghe. Volevamo verificarlo empiricamente, non fidarci della letteratura.

Per rendere il confronto significativo, abbiamo usato per la prima volta una GPU in cloud — un RTX 4090 noleggiato su Vast.ai. Il training che sul Mac avrebbe richiesto giorni è durato 76 minuti. Il costo: 4,60 dollari. La differenza di velocità — circa 50 volte più rapido — ha cambiato il modo di pensare alle iterazioni.

RoPE ha vinto chiaramente: val loss 3.59 contro 4.08, perplexity che scende da 59 a 36. E ha superato la versione precedente già al 27% del budget di step — molto prima del previsto.

Risultato
Val loss 3.59 (perplexity ~36) · 76 min su RTX 4090 · $4.60 · miglior checkpoint del progetto
· · ·
1–3 ottobre 2026 Completata

Costruire un corpus: qualità contro quantità

Il modello aveva visto Wikipedia italiana — circa 47 milioni di token. Secondo la formula Chinchilla (una stima del rapporto ottimale tra dati e parametri), ne servono circa 918 milioni. Eravamo al 5%. Bisognava espandere il corpus.

La fonte principale è stata CulturaX, un dataset di testo italiano estratto dal web, già pre-filtrato dai ricercatori che lo hanno pubblicato. 8.2 gigabyte, 2.7 milioni di documenti. Abbiamo applicato il nostro filtro di qualità: regole euristiche (lunghezza, densità di simboli, duplicati), rilevamento della lingua, e infine il nostro quality scorer basato sulla perplexity — lo stesso Run #3 che giudicava quanto un testo somigliasse all'italiano enciclopedico.

Questa scelta ha un problema che abbiamo scoperto guardando i risultati: il giudice conosce solo Wikipedia. Tende a escludere la poesia (sintassi inusuale = alta perplexity) e a tenere la pubblicità (grammatica corretta = bassa perplexity). Non è un errore del sistema — è un bias strutturale da tenere a mente.

Guardare a campione i documenti esclusi è stato più istruttivo che guardare quelli tenuti. Una poesia di Pascoli era stata rigettata. Molti annunci commerciali erano stati accettati. Il filtro faceva quello che doveva — ma "quello che doveva" non era quello che volevamo.

Abbiamo anche trovato parole come "v4sta" — token che mescolano lettere e numeri, tipici del web ma non dell'italiano scritto. Abbiamo corretto il filtro per escluderle.

Risultato
2.722.042 documenti in → 2.439.329 tenuti (89.6%) · 7.5 GB · 7h 27m su RTX 4090
· · ·
3 ottobre 2026 — in corso In corso

Il corpus non stava in memoria — abbiamo cambiato il metodo

Il corpus concatenato — Wikipedia più CulturaX filtrato più il corpus letterario — pesava 8.3 gigabyte. Abbiamo avviato il training. Il processo è andato in crash due volte per esaurimento memoria.

Il problema non era ovvio: il codice aveva già un sistema di cache per evitare di ricodificare il corpus ad ogni avvio. Ma la prima volta — quando la cache non esiste ancora — deve leggere l'intero file, convertirlo in una lista Python di numeri interi, e poi trasformarla in un tensor. Una lista Python di un miliardo di interi pesa circa 28 gigabyte. Su una macchina con 8 gigabyte di RAM, non c'è spazio.

Il problema non era nel training. Era nel modo in cui si caricavano i dati prima che il training potesse iniziare. Una distinzione sottile che ha richiesto tempo per emergere.

Abbiamo riscritto il caricamento del corpus: invece di leggere tutto il file in memoria, lo leggiamo a pezzi da 10 milioni di caratteri alla volta. Ogni pezzo viene codificato e convertito in un formato compatto (int16 invece di int64 — la metà dello spazio, sufficiente per un vocabolario di 16.000 elementi). Il picco di memoria è sceso da oltre 20 gigabyte a circa 3-4.

Poi abbiamo fatto un ulteriore passo: abbiamo spezzato il corpus in 7 shard da circa 1.2 gigabyte ciascuno. Il training procede in sequenza su ciascuno, riprendendo sempre dal checkpoint precedente. È meno elegante di un training continuo, ma funziona — e nel mezzo di un problema di risorse, funzionare vale più dell'eleganza.

Stato attuale
7 shard · 8.000 step / shard · 56.000 step totali · in esecuzione su Mac M2 · ETA ~30h
· · ·
4 ottobre 2026 — note tecniche Approfondimento

Otto gigabyte per tutto: modello, dati, sistema operativo

La macchina su cui gira il training — un Mac M2 con 8 gigabyte di RAM — non funziona come un computer tradizionale con una scheda grafica dedicata. Su queste macchine, la GPU e la CPU condividono la stessa memoria fisica. Non ci sono due serbatoi separati: c'è un unico bacino da 8 gigabyte che deve bastare per tutto.

Durante il training, abbiamo misurato come vengono usati questi 8 gigabyte:

Il modello vive in un sistema operativo che ha quasi esaurito la sua memoria. Ogni gigabyte ha una funzione. Non c'è margine.

La CPU — quella che esegue il codice Python — occupa meno di 40 megabyte, una frazione. Il lavoro pesante lo fa la GPU integrata (chiamata MPS nel framework PyTorch), che esegue le operazioni matematiche in parallelo su migliaia di core minuscoli.

In termini di velocità: su questo hardware il training procede a circa 2.1 secondi per ogni step. Su una GPU professionale come quella usata per il Run #4 (Vast.ai, circa 1,50€/ora), lo stesso step richiedeva 0.15 secondi — quattordici volte più veloce. Il training completo di Run #5, che su GPU professionale richiederebbe meno di un'ora, richiede qui circa 30 ore. La differenza è il prezzo del non pagare per l'infrastruttura.

Nonostante questo, il modello impara. A metà del primo shard dei sette, la sua capacità di prevedere testo italiano ha già superato il risultato migliore del Run #3, che aveva richiesto 30.000 step su Wikipedia. La varietà del corpus nuovo — testi amministrativi, articoli enciclopedici, narrativa — porta più informazione per unità di testo.

Metriche a step 4.500 / 56.000
RAM usata 91.8% · val loss 4.001 · velocità 2.1s/step · sotto Run #3 (4.077) già al 8% del totale

La prima tappa: quattro ore e quaranta minuti

Il primo blocco del training è terminato. Quattro ore e quaranta minuti di calcolo continuo su un chip da otto gigabyte, senza interruzioni. Il modello ha attraversato 8.000 aggiornamenti su un pezzo di corpus italiano — articoli enciclopedici, testi amministrativi, narrativa — e alla fine di questo primo blocco la sua capacità di prevedere il testo italiano si è migliorata in modo misurabile.

La metrica che usiamo per valutarlo, la val loss, è passata da 4.46 a inizio shard a 3.87 a fine shard. In termini concreti: il modello sbaglia meno spesso nella previsione della parola successiva. Non è ancora fluente come un modello addestrato per settimane su supercomputer, ma il miglioramento è reale e documentato passo dopo passo.

Il modello non ha ancora superato il suo risultato migliore. Ci vorranno altri shard.

Il checkpoint più preciso che abbiamo appartiene ancora al run precedente — quello eseguito su GPU professionale nel settembre 2026. Il primo shard del nuovo training non ha ancora raggiunto quel livello. Ma questo era previsto: ogni shard parte da zero in termini di velocità di apprendimento, si scalda nei primi cento aggiornamenti, poi scende lungo una curva a coseno fino a quasi fermarsi. Il vero vantaggio del corpus più ampio si vedrà man mano che i shard si accumulano.

Mentre il primo shard terminava, il programma ha già iniziato a preparare il secondo: sta leggendo e trasformando in numeri il prossimo pezzo di testo italiano, 1.18 gigabyte di dati che il modello non ha ancora mai visto. Questo processo di codifica richiede circa dieci minuti, poi ripartirà il ciclo.

Shard 1 completato — riepilogo
Durata: 4h 40m · Val loss: 4.46 → 3.87 · Best assoluto ancora Run #4 (3.59) · Shard 2/7 in corso

Il secondo blocco impara più in fretta

Il secondo blocco di training ha raggiunto la stessa qualità del primo in meno tempo. Nel primo blocco ci vollero cinque mila aggiornamenti per portare la metrica di qualità sotto una soglia significativa. Nel secondo blocco ci sono voluti quattro mila aggiornamenti — un quinto in meno.

Non è solo una curiosità numerica. Significa che il modello, dopo aver lavorato sul primo blocco di testo, era già in una posizione migliore per imparare dal secondo. I pesi — cioè i milioni di numeri che rappresentano la "conoscenza" del modello — erano già orientati nel verso giusto. Il secondo blocco ha richiesto meno correzioni per adattarsi.

Ogni blocco rende il prossimo leggermente più facile da imparare.

È questo il vantaggio del continued pre-training: non si riparte mai da zero. Il modello accumula una comprensione dell'italiano che si consolida blocco dopo blocco, anche quando i testi cambiano registro — da articoli enciclopedici a testi amministrativi, da narrativa a giornalismo. La varietà non confonde, costruisce.

Shard 2 — step 4000
Val loss 3.98 · sotto 4.0 con 1000 step di anticipo rispetto a shard 1 · 2 ore di training

Un quarto di strada, non una corsa persa

Nel campo dell'addestramento dei modelli linguistici esiste una regola empirica, emersa da un articolo pubblicato da DeepMind nel 2022 e chiamata informalmente legge di Chinchilla. La regola dice: per addestrare un modello in modo ottimale, servono circa venti token per ogni parametro. Il nostro modello ha 46 milioni di parametri. Il target ottimale è quindi circa 920 milioni di token.

Con il batch size che stiamo usando su M2 — otto esempi per ogni aggiornamento — Run #5 raggiungerà circa 229 milioni di token al termine dei suoi 56.000 step. Un quarto del target. Non è un fallimento: è una scelta consapevole dettata dai limiti dell'hardware disponibile in questa fase.

Il budget token non è un limite di un singolo run. È la somma di tutti i run.

Il punto chiave è che i training si sommano, non si sostituiscono. Ogni run parte dai pesi del precedente, come se riprendesse un libro dal segnalibro invece di ricominciare dalla prima pagina. Run #4 ha aggiunto 245 milioni di token su Wikipedia. Run #5 ne aggiunge altri 229 milioni su un corpus più vasto. Run #6 — pianificato su GPU professionale, con un batch quattro volte più grande — completerà il percorso con altri 918 milioni di token. Alla fine, il totale sarà circa 1.15 miliardi di token: il 25% in eccesso rispetto al target Chinchilla.

In questo senso, Run #5 svolge un ruolo specifico: espone il modello a un testo italiano molto più vario di quanto avesse visto finora — articoli enciclopedici, testi amministrativi, narrativa contemporanea — prima che Run #6 consolidi questa conoscenza su scala industriale. Non è una fase di transizione: è un training vero, con risultati misurabili, che produce un checkpoint utile per il passo successivo.

Budget cumulativo pianificato
Run #4: 245M · Run #5: +229M · Run #6: +918M → Totale: ~1.15B token (125% Chinchilla)

Il secondo blocco chiude più in basso — e più in fretta

Shard 2 è terminato. Quattro ore e sei minuti, trentaquattro minuti in meno di shard 1. La val loss finale è 3.834, contro il 3.867 con cui si era concluso il primo blocco. Non sono numeri che cambierebbero un articolo di giornale, ma nel contesto dell'addestramento sono un segnale preciso: il modello sta progredendo.

Ogni shard applica un ciclo completo di apprendimento — 8.000 aggiornamenti con la variante cosine del tasso di apprendimento, che parte alto, scende gradualmente, e si ferma prima di toccare zero per lasciare margine a una ripartenza. I pesi che arrivano a fine shard 2 sono leggermente migliori di quelli che arrivavano a fine shard 1. Il corpus cambia, ma il modello porta con sé tutto quello che ha imparato.

Ogni shard eredita ciò che il precedente ha costruito. Non si riparte da zero: si va più avanti partendo da un punto già più avanzato.

È questo che rende l'addestramento a blocchi diverso da un unico lungo ciclo: invece di un percorso lineare, il modello attraversa sette paesaggi linguistici diversi, ognuno dei quali lascia una traccia. La val loss cumulativa non è l'unica misura che conta — ma il fatto che scenda di shard in shard suggerisce che la direzione è quella giusta.

Shard 2 — riepilogo
Val loss finale: 3.834 · Shard 1: 3.867 · Miglioramento: −0.033 · Durata: 4h 06m

Il terzo blocco tocca la soglia 4.0 — stessa velocità del secondo

Il terzo blocco di addestramento ha raggiunto la soglia 4.0 di val loss al passo 4500, circa due ore e trentacinque minuti dall'avvio. Shard 2 l'aveva toccata al passo 4000. La differenza è contenuta — mezzo blocco, meno di dieci minuti — e suggerisce che la velocità di convergenza si sta stabilizzando: il modello impara sempre più rapidamente rispetto ai blocchi iniziali, ma il margine di miglioramento shard su shard tende a ridursi man mano che i pesi si avvicinano a una configurazione stabile.

La val loss a metà percorso (step 4500 su 8000, il 56%) è 4.001. È un numero che ha in sé qualcosa di simbolico: la soglia del 4 nella perplexità logaritmica corrisponde, in modo approssimativo, a un modello che si è lasciato alle spalle le fasi più elementari dell'apprendimento linguistico e comincia a operare su strutture più sottili. Non è un traguardo finale, ma un indicatore che l'addestramento procede in modo sano.

La val loss di 4.0 non è un traguardo: è il punto in cui comincia la parte più interessante del percorso.

I campioni di generazione a questo passo mostrano ancora artefatti da parole-seme insolite — ripetizioni di singole parole in maiuscolo, loop di termini tecnici — ma questi sono comportamenti legati ai token di innesco, non alla qualità generale dei pesi. Quando il seme è una parola normale, il modello produce già frasi italiane grammaticalmente corrette.

Shard 3 — step 4500
Val loss: 4.001 · Soglia 4.0 raggiunta · 56% del shard completato · Tempo: 2h 35m

Il terzo blocco conclude con il minimo più basso — poi un passo indietro

Shard 3 è terminato in quattro ore e trentasei minuti. Il risultato finale è un po' ambivalente, ma in modo interessante: al passo 7500, cioè a circa novantaquattro per cento del percorso, il modello ha raggiunto una val loss di 3.821 — il valore più basso registrato in tutta la Run #5 finora, meglio del minimo di shard 2 (3.834). Poi, negli ultimi cinquecento passi, c'è stato un leggero rialzo: la val loss finale a step 8000 è 3.845.

Questo comportamento ha un nome: micro-uptick. Accade quando il tasso di apprendimento è talmente basso da non poter più correggere le fluttuazioni casuali introdotte dai singoli batch di testo. In pratica, il modello ha smesso di imparare cose nuove e ha cominciato a oscillare leggermente intorno alla sua posizione migliore, come un ago di bussola che si assesta. Non è un segnale negativo — è anzi una conferma che il training ha estratto quasi tutto quello che poteva da quel corpus con quella configurazione.

Il minimo reale non è sempre l'ultimo passo. A volte il momento migliore arriva poco prima della fine, e il checkpoint lo registra.

Il checkpoint best.pt salva automaticamente il momento di val loss più bassa — in questo caso, il passo 7500 con 3.821. È quello che il modello userà come punto di partenza per shard 4. Il valore finale a step 8000 è quello che appare nel log di completamento, ma il checkpoint effettivo è quello migliore.

La tendenza generale resta chiara: ogni shard sposta il minimo assoluto verso il basso. Shard 1: 3.852. Shard 2: 3.834. Shard 3: 3.821. L'abbassamento rallenta, ma continua.

Shard 3 — riepilogo
Minimo val loss: 3.821 (step 7500) · Val finale: 3.845 · Durata: 4h 36m · Shard 4 già in avvio

Rileggere dall'inizio, ogni volta — e come smettere di farlo

Immaginate di stare scrivendo una lettera a mano, lentamente, una parola alla volta. Prima di scrivere ogni nuova parola, tornate all'inizio della lettera, la rileggete dall'inizio, e solo allora aggiungete la parola successiva. La lettera diventa più lunga, la rilettura diventa più lenta, e il tempo per aggiungere ogni parola cresce senza fermarsi.

Questo è esattamente il problema che il nostro modello ha con la generazione del testo. Ogni volta che produce un nuovo token — una nuova parola, o parte di essa — rilegge l'intera sequenza precedente dall'inizio. Non perché non ricordi quello che ha già visto, ma perché il codice non gli dà la possibilità di conservare i risultati intermedi di quella lettura.

Il problema non è dimenticare. È non conservare.

Dentro un transformer, leggere una sequenza significa eseguire un'operazione chiamata attention: il modello calcola, per ogni parola nella sequenza, una coppia di valori che rappresentano il suo contributo al contesto. Questi valori si chiamano K (key) e V (value). Sono deterministici: per una parola in una certa posizione, K e V sono sempre gli stessi, indipendentemente da quante altre parole verranno dopo. Ricalcolarli ogni volta è lavoro inutile.

La soluzione è ovvia una volta che si capisce il problema: tenere quei valori in memoria e non ricalcolarli. Si chiama KV-cache, dove KV sta per le iniziali di Key e Value. Al primo passo, si calcola tutto per il prompt iniziale. Ai passi successivi, si calcola solo il K e il V del nuovo token appena generato, si aggiungono a quelli già salvati, e si fa l'operazione di attention su tutto l'insieme.

Il costo di generare cento token passa dall'equivalente di 1+2+3+...+100 operazioni (la somma fa 5050) a cento operazioni identiche, una per token. Su sequenze lunghe, il guadagno in velocità è nell'ordine di decine di volte.

La memoria qui non è un'aggiunta — è la correzione di una mancanza.

C'è un aspetto che vale la pena sottolineare, perché può sembrare controintuitivo: questa modifica non tocca in nessun modo il training. Quando il modello si addestra, elabora sequenze intere in parallelo e ha bisogno di ricaricare tutto ogni volta, perché sta imparando e il suo grafo di calcolo deve restare intatto per la backpropagation. La cache è uno strumento per l'inferenza — per quando il modello è già addestrato e deve semplicemente generare. I due percorsi del codice non si incrociano.

L'implementazione tocca tre punti: il meccanismo di attention, la struttura del modello che lo coordina, e il ciclo di generazione che accumula lo stato passo per passo. Ogni modifica aggiunge un parametro opzionale con valore predefinito invariato rispetto al passato — il codice di training non vede la differenza.

Dopo l'implementazione, il primo test che eseguiremo è di identità: generare duecento token con e senza cache, e verificare che il risultato sia esattamente lo stesso. Se c'è un errore di un solo indice nella concatenazione, i token divergono. Solo se i due output coincidono bit per bit il meccanismo è corretto. Il secondo test misura il tempo: quanto ci vuole adesso, quanto ci vorrà dopo.

Fase 08 — KV-cache
Documentata · Implementazione in corso · Test: identità output + speedup wall-clock