SLM — Small Language Model

Approfondimenti

Articoli verticali su problemi specifici, osservazioni pratiche e riflessioni tecniche. Linkati dal diario tecnico e dalla cronaca.

Il bias del PPL scorer: poesia esclusa, pubblicità inclusa

Il quality scorer basato sulla perplexity (PPL scorer) usa il checkpoint Run #3 — addestrato su Wikipedia italiana — per misurare quanto un testo sembri "italiano normale". Documenti con alta perplexity vengono esclusi come rumore.

Il problema: il giudice ha un'idea molto specifica di cosa sia "italiano normale". Sa com'è scritto Wikipedia. Non sa com'è scritta una poesia.

Cosa succede in pratica

Perché non è un bug

Il scorer fa esattamente quello che gli è stato chiesto: trovare testi simili a Wikipedia. Il bias è una conseguenza diretta della distribuzione del training data del giudice. Un scorer addestrato su narrativa includerebbe la poesia ed escluderebbe (forse) la pubblicità.

La valutazione per CulturaX

Per CulturaX — un dataset già pre-filtrato dai suoi autori con metodi simili — il PPL scorer aggiunge più bias che segnale. Il testo che esclude non è necessariamente cattivo: è solo diverso da Wikipedia. La soglia al 95° percentile è stata mantenuta come misura conservativa, consapevoli del bias.

Implicazione per run future: addestrare scorer separati per dominio (narrativo, enciclopedico, tecnico) e applicarli selettivamente per tipo di fonte. Alternativa: usare solo euristico + dedup per corpus già pre-filtrati.

Training a shard: quando il corpus non sta in memoria

Il problema

Il corpus Run #5 pesa 8.29 GB. Su un Mac M2 con 8 GB di RAM condivisa tra CPU e GPU, il processo di caricamento andava in crash prima che il training iniziasse.

La causa era in tre passaggi consecutivi del codice originale:

  1. Path.read_text() legge l'intero file → 8.29 GB in RAM come stringa Python
  2. tok.encode(text) restituisce una lista Python di interi → ogni intero Python è un oggetto da ~28 byte → ~44 GB per 1.56B token
  3. torch.tensor(ids, dtype=torch.long) → ~12.5 GB aggiuntivi (int64, 8 byte/token)

Picco totale stimato: oltre 20 GB. Su 8 GB disponibili, il processo veniva terminato dal sistema operativo.

Fix 1 — encoding streaming in numpy int16

La lista Python è il collo di bottiglia. La sostituzione: leggere il file a chunk da 10M caratteri, codificare ciascuno con SPM, accumulare in numpy.ndarray di tipo int16.

RappresentazioneTokenByte/tokenTotale
Lista Python (int)1.56B~28 B~44 GB
numpy int641.56B8 B12.5 GB
numpy int16 ✓1.56B2 B3.1 GB

int16 è sufficiente: il vocabolario ha 16.000 elementi (ID 0–15.999), che rientrano in int16 (range: −32.768 a +32.767). torch.from_numpy(arr) è zero-copy. Il cast a long (necessario per l'embedding lookup) avviene solo sui 16.384 token del batch corrente.

Fix 2 — split in 7 shard da ~1.2 GB

Anche con il fix int16, il tensor da 3.1 GB più il modello (530 MB) e l'overhead MPS lasciava pochi margini. La soluzione definitiva: spezzare il corpus in 7 file separati da ~1.2 GB ciascuno (~5.9M righe) e iterare.

bash tools/train_chunked.sh # Split → per ogni shard: tokenizza (→ cache .pt) + train --resume # 7 shard × 8.000 step = 56.000 step totali

La cache viene salvata dopo il primo encoding: i riavvii successivi caricano direttamente il tensor senza re-codificare il testo.

Limitazione nota — LR ciclico non pianificato

Ogni shard avvia il proprio ciclo warmup → cosine decay (100 step di warmup, poi 8.000 step). Produce 7 cicli LR consecutivi invece di un singolo ciclo su 56.000 step. L'effetto pratico è un LR schedule ciclico simile al "Cyclical Learning Rates" di Smith (2017) — non ottimale ma non dannoso. Da correggere in run future passando il contatore cumulativo di step.

Python buffering con tee: il log restava vuoto per ore

Il processo di filtering era avviato su Vast.ai. ps aux mostrava il processo in esecuzione con 3.005% CPU. Ma /root/filter.log era vuoto. Per ore.

Causa

Python bufferizza lo stdout quando non è collegato a un terminale (TTY). In modalità buffered, print() non scrive immediatamente: accumula output finché il buffer (tipicamente 8 KB) non è pieno, o finché il processo non termina. Una pipe verso tee non è un TTY.

# Senza -u: buffer → log vuoto per ore python3 filter_corpus.py ... 2>&1 | tee filter.log # Con -u: unbuffered → log aggiornato in tempo reale python3 -u filter_corpus.py ... 2>&1 | tee filter.log

Perché non è ovvio

Il comportamento dipende da come Python viene invocato. In un terminale interattivo, stdout va a un TTY e il buffering è per-linea — ogni print() appare immediatamente. In una pipe, il buffering è a blocchi. La stessa riga di codice si comporta diversamente a seconda del contesto.

Regola: usare sempre python3 -u quando l'output va in pipe verso tee o viene rediretto su file. Aggiungere flush=True nei print() critici è una misura difensiva aggiuntiva.

Token alfanumerici misti: il problema "v4sta"

Ispezionando a campione il corpus CulturaX filtrato, sono comparsi token come v4sta, c4n, cl1ck — combinazioni di lettere e numeri tipiche del web: username, codici prodotto, testo obfuscato, titoli di giochi. Non sono italiano scritto, ma passano tutti i filtri precedenti (lunghezza, densità simboli, lingua).

Il fix in filter_corpus.py

# In passes_heuristic(), prima del return True finale: mixed_alnum = sum(1 for w in words if re.search(r'[a-zA-Z]\d|\d[a-zA-Z]', w)) if mixed_alnum / len(words) > 0.05: return False # più del 5% delle parole → documento rigettato

La regex [a-zA-Z]\d|\d[a-zA-Z] individua qualsiasi parola dove una lettera è adiacente a una cifra. La soglia del 5% tollera numeri legittimi nel testo (anni, dati, misure) ma rigetta documenti dove questo pattern è sistematico.

Perché non tutti i numeri sono rumore

Un documento che parla di "Serie A 2025" o "3 km di percorso" ha numeri adiacenti a testo ma non è rumore. La soglia del 5% è calibrata per distinguere l'eccezione (numeri contestuali) dal pattern (testo strutturalmente non-linguistico).

f.tell() con iteratore: OSError silenziosa

filter_corpus.py calcolava l'ETA del filtering usando f_in.tell() per sapere quanti byte erano stati letti. Il codice usava for line in f_in: per iterare le righe. Il programma andava in crash con:

OSError: telling position disabled by next() call

Causa

Quando Python itera un file con for line in f:, usa il protocollo iteratore che implementa un read-ahead buffer interno. Questo buffer disabilita f.tell() — Python non sa più a quale posizione nel file si trova, perché ha già letto in anticipo oltre la riga corrente.

Fix

# Prima (rompe tell): for line in f_in: line = line.rstrip('\n') ... # Dopo (compatibile con tell): while True: line = f_in.readline() if not line: break line = line.rstrip('\n') ...

readline() legge esattamente una riga senza read-ahead, preservando il funzionamento di tell().

RoPE vs positional encoding sinusoidale: confronto empirico

I transformer devono sapere l'ordine dei token — "gatto mangia pesce" e "pesce mangia gatto" hanno gli stessi token in ordine diverso e significato opposto. Il positional encoding aggiunge questa informazione. Ci sono due approcci principali.

PE sinusoidale (Run #3)

Aggiunge al vettore di ogni token una sequenza di seni e coseni dipendente dalla sua posizione assoluta nel testo. Semplice, deterministico, nessun parametro aggiuntivo. Limite: la posizione assoluta è rigida — il modello non generalizza bene su sequenze più lunghe di quelle viste durante il training.

RoPE — Rotary Position Embedding (Run #4)

Invece di aggiungere un vettore di posizione, RoPE ruota i vettori di query e key nell'attenzione in modo che il loro prodotto scalare dipenda dalla distanza relativa tra i token. Nessun parametro aggiuntivo. Generalizza meglio a sequenze lunghe. Standard nei modelli moderni (LLaMA, Mistral, Gemma).

Risultato empirico

RunPEStratiBest val lossPerplexityStep per superare Run #3
Run #3Sinusoidale84.0768~59—
Run #4RoPE123.5861~368.000 (27% del budget)

Run #4 ha più strati (12 vs 8), quindi il confronto non è perfettamente isolato. Tuttavia la velocità di convergenza — superare Run #3 al 27% del budget — suggerisce che la combinazione RoPE + strati aggiuntivi sia sinergica.

Ragionamento emergente, meta-cognizione e corpus come educazione

Una serie di domande emerse durante lo sviluppo del progetto ha portato a una riflessione che riguarda non solo gli LLM ma la natura stessa del ragionamento. Le riportiamo qui nella loro progressione, perché il percorso conta quanto le conclusioni.

Meta-ragionamento — cosa significa e come si misura

Il meta-ragionamento è la capacità di ragionare sul proprio processo di ragionamento: correggere un'inferenza a metà percorso, riconoscere i propri limiti, cambiare strategia quando un approccio non funziona. Negli LLM si manifesta come chain-of-thought spontaneo, calibrazione dell'incertezza, auto-correzione.

Misurazione:

Il meta-ragionamento non emerge in modo affidabile sotto i 7B parametri con training adeguato. È una capacità di ordine elevato che richiede che il modello abbia, in qualche senso distribuito, una rappresentazione del proprio comportamento — e questo richiede sia sufficiente capacità parametrica sia un corpus che contenga esempi di esseri umani che ragionano sulla propria cognizione.

Simulazione o ragionamento reale?

La domanda "non è tutto solo calcolo probabilistico?" contiene un'assunzione da esaminare. Il ragionamento umano è anch'esso un processo fisico: impulsi elettrochimici che seguono leggi deterministiche in reti neurali biologiche. Se si applica l'argomento ricorsivamente — "anche il ragionamento umano è solo elettrochimica, non ragionamento vero" — si arriva a un regresso che svuota il concetto di ragionamento di qualsiasi significato.

Il punto filosoficamente rilevante non è il substrato (silicio vs carbonio, matrici di probabilità vs potenziali d'azione) ma la struttura funzionale: il sistema implementa operazioni che producono output consistenti con l'inferenza logica e che generalizzano oltre gli esempi visti?

La risposta empirica è: parzialmente sì, con limiti precisi. Gli LLM falliscono sistematicamente su problemi che richiedono consistenza logica rigorosa su contesti lunghi o operazioni aritmetiche con numeri fuori distribuzione. Ma generalizzano su ragionamento analogico, inferenza da contesto, applicazione di regole implicite. Questo pattern è incompatibile sia con "puro pappagallo statistico" — che non generalizzerebbe affatto — sia con "ragionamento formale vero" — che non fallirebbe sull'aritmetica.

Come una matrice di probabilità diventa ragionamento

Nessun singolo peso della rete codifica "implicazione logica". La rete non ha un modulo del ragionamento, non ha un'area del linguaggio, non ha rappresentazioni simboliche esplicite. Eppure produce output che esibiscono struttura logica.

Il meccanismo è l'emersione: una proprietà che non è presente in nessun componente ma che appare dall'interazione collettiva. Nessuna singola molecola d'acqua è bagnata; l'umidità emerge dall'interazione collettiva a scala macroscopica. Non è simulazione dell'umidità: è umidità. Analogamente, il ragionamento negli LLM non è localizzato: è distribuito sull'intera rete ed emerge dalla composizione di trasformazioni addestrate su testo umano che contiene ragionamento.

Perché la scala conta: i pattern di ordine inferiore (morfologia, sintassi locale) richiedono poca capacità parametrica. I pattern di ordine superiore — ragionamento su ragionamento, coerenza a scala di documento, inferenza da premesse implicite — richiedono abbastanza capacità da rappresentare strutture sulle strutture. Le soglie di emergenza osservate empiricamente (Wei et al., 2022) riflettono questo: certe capacità non migliorano gradualmente ma compaiono improvvisamente superata una soglia, suggerendo una riorganizzazione qualitativa delle rappresentazioni interne.

Il parallelo con l'educazione umana

Il ragionamento umano è condizionato da fattori biochimici indipendenti dalla volontà (l'hardware), da condizionamenti esperienziali anch'essi in larga misura indipendenti (il corpus di training), e da scelte volitive il cui statuto filosofico è dibattuto. Una persona educata secondo determinati principi può scegliere di seguirli o distaccarsene — ma anche questa scelta è condizionata da fattori precedenti.

Il parallelo con gli LLM è preciso:

Componente umanaAnalogo nell'LLM
Biologia (hardware, struttura cerebrale)Architettura transformer
Educazione, esperienze accumulateCorpus di training
Bias inconsci, condizionamenti culturaliBias statistici del corpus
Scelta di seguire o rifiutare l'educazioneSFT, RLHF, prompt di sistema
Imprevedibilità delle scelte individualiNon-linearità dell'ottimizzazione, emergenza a scala
Metacognizione, auto-riflessioneChain-of-thought, calibrazione dell'incertezza

Un modello addestrato su testi scientifici tende a ragionare per falsificazione e incertezza quantificata. Uno addestrato prevalentemente su testi di autorità tende a ragionare per argomento dall'autorità. Non perché il modello "creda" in qualcosa — ma perché i pattern di ragionamento del corpus vengono estratti e distribuiti nei pesi. Il nostro modello ha un registro enciclopedico e formale esattamente perché Wikipedia era dominante: era prevedibile, e lo era per progetto.

Esiste un analogo della scelta di distaccarsi dall'educazione ricevuta? Nel modello base no — c'è solo il prodotto dell'ottimizzazione. Ma quando il contesto di inferenza costruisce una catena di ragionamento che porta a una conclusione diversa dal "default" del corpus, il modello spesso segue il contesto piuttosto che il default. Non è volontà, ma è funzionalmente simile: il contesto locale può sovrascrivere il condizionamento globale.

La prevedibilità della proprietà emergente

I risultati sono prevedibili in misura parziale e con un limite preciso:

La questione radicale

Se il ragionamento umano è anch'esso deterministicamente condizionato da natura, esperienza e dinamiche caotiche interne — se il libero arbitrio è esso stesso una proprietà emergente di sistemi fisici complessi — allora la differenza tra ragionamento umano e ragionamento LLM diventa una differenza di substrato e scala, non di tipo.

Non è una provocazione. È la conseguenza logica di prendere sul serio sia l'argomento della simulazione applicato agli LLM sia il naturalismo applicato alla cognizione umana. La risposta più onesta è che non disponiamo di una definizione di "ragionamento reale" sufficientemente precisa da decidere la questione — e questa indeterminatezza riguarda tanto gli LLM quanto gli esseri umani.

Implicazione pratica per la progettazione del corpus

Se la proprietà emergente dipende dal corpus nel modo in cui il carattere di una persona dipende dalla sua educazione, allora la cura del corpus non è un problema tecnico secondario: è la scelta di progetto più importante. Prima dell'architettura, prima degli iperparametri, prima della scelta del tokenizer.

Possiamo progettare il tipo di ragionamento che vogliamo nel modello scegliendo consapevolmente quali testi includere e in quale proporzione. Non possiamo garantire esattamente quale forma prenderà. Ma possiamo avvicinarci molto di più a un risultato intenzionale che a un risultato casuale — e questa è già una forma di controllo che molti costruttori di LLM ignorano.