Archivio storico: architettura, prime decisioni di corpus, Run 1–5 e la scoperta del bug di training sequenziale. I run attivi (Run #6 in poi) sono nel diario attivo.
periodo 2026-09-14 — 2026-10-05run coperti #1, #3, #4, #5stato archiviato
Architettura corrente — preset medium
Il modello ha circa 46 milioni di parametri e pesa 175 MB. È addestrato sul corpus integrale di Wikipedia in italiano. Le celle seguono mostrano i dettagli tecnici della sua struttura.
Parametri
45 997 056
Dimensione (fp32)
175.5 MB
d_model
512
layer × head
8 × 8
d_ff
2 048
max_seq_len
256
vocab (BPE-SPM)
16 000
Pos. encoding
sinusoidale → RoPE
Norm
LayerNorm (pre-norm)
Attivazione FFN
GELU
Corpus
Wikipedia IT
Token totali
47.8 M
Timeline delle decisioni
2026-09-14
Run #1 — primo training dall'architettura di base completato
Dopo 46 minuti, il modello formava parole italiane morfologicamente corrette: le desinenze giuste, la struttura riconoscibile. Nessun senso logico. Era esattamente quello che mi aspettavo, e confermava che i meccanismi di base funzionavano.
Tokenizer char-level (102 caratteri unici), corpus narrativo personale 552 KB. Obiettivo: validare che ogni componente funzioni correttamente prima di scalare. Val loss finale 1.7256 in 46 minuti su MPS. Il modello produce morfologia italiana corretta (suffissi -zione, -mente) ma nessuna coerenza semantica, atteso a questa scala e con questo corpus.
Checkpoint: checkpoints/best.pt (char-level, non BPE-compatibile).
2026-09-14
Decisione: corpus Wikipedia IT e tokenizer sub-word
Morfologia corretta non basta: il modello non capisce nulla di quello che genera. Per imparare la struttura della lingua serviva molto più testo, e un sistema più intelligente per leggerlo. Wikipedia offre milioni di frasi ben formate, in italiano standard.
Per superare il pavimento qualitativo del char-level serve un corpus di scala enciclopedica e un tokenizer sub-word. Scelte: tokenizer BPE via SentencePiece (bpe-spm), vocab 16 000 token, corpus Wikipedia IT da dump ufficiale (~208 M caratteri, 47.8 M token). Preset architettura scalato a medium: d_model 512, 8 layer, 45.9 M parametri.
Motivazione per Wikipedia: dominio linguistico ampio e uniforme, struttura prevedibile (articoli enciclopedici), frase ben formata, italiano standard. Limitazione nota: markup residuo (wikitext, tag HTML) parzialmente rimosso.
2026-09-14
Out-of-memory su Mac M2 8 GB — riduzione batch e token cache bloccante risolto
La macchina si è bloccata prima ancora di iniziare. Otto gigabyte di memoria condivisa tra processore e grafica non sono molti per addestrare un modello. Capire perché, e trovare il compromesso giusto, ha insegnato come funziona davvero la memoria di un acceleratore grafico.
Primo tentativo BPE: batch=32, seq_len=512 produce out-of-memory a step 0 su MPS con 8 GB RAM. Fix: riduzione a batch=16, seq_len=256. Implementazione token cache (365 MB su disco) per evitare tokenizzazione al volo ad ogni epoch, riducendo overhead e rendendo il training deterministico.
Con batch=16, seq_len=256: 4 096 token per batch, 122.9 M token totali a 30 000 step. Throughput MPS steady-state: ~22 secondi/step.
2026-09-18
Run #3 — BPE-SPM, 30 000 step completato
Il training principale è partito. Trenta mila passi sull'enciclopedia italiana. Quello che il modello imparerà, lo imparerà da solo, passo dopo passo, leggendo milioni di frasi.
Training BPE-SPM su Wikipedia IT, architettura sinusoidale, preset medium:
Completato il 2026-09-26: 30 000 step, val loss best 4.0768, perplexity ~59. Durata totale: ~230 h su Mac M2 (step 0–14 000) + 59m 22s su hardware dedicato (step 14 000–30 000). Dettaglio nella sezione Analisi sotto.
2026-09-21
Decisione: completare Run #3, poi Run #4 con sistema di posizionamento diverso tecnica
Il formato in cui voglio distribuire il modello usa internamente un sistema diverso per tenere traccia dell'ordine delle parole. Adeguarsi non costa nulla: nessun apprendimento precedente viene perso. I due training finiranno entrambi, e si confronteranno i risultati.
Analisi di compatibilità con llamafile/llama.cpp (target GGUF) porta a pianificare Run #4 con RoPE (Rotary Position Embedding). Run #3 prosegue fino a completamento (30 000 step): i due modelli finali saranno comparati con test mirati. RoPE non è più veloce, il costo per step è ~3-5% superiore, la convergenza in step è identica. La motivazione è architetturale, non computazionale. Dettaglio nella sezione Decisione.
2026-09-30
Analisi Chinchilla — timing del SFT e dipendenza dal paradigma strategica
La domanda non è "SFT sì o no" — è "su quale base?". Con 47.8 M token al 5.3% del compute-optimal, il modello è Chinchilla-undertrained ma già usabile per NLG formulaico. La soglia di utilità cambia radicalmente in funzione del paradigma del task.
Gap Chinchilla: 45 M parametri × 20 = 900 M token compute-optimal. Corpus attuale: 47.8 M — 19× sotto l'ottimale. Il SFT formatta il comportamento ma non inietta conoscenza: se il base model non ha internalizzato un concetto in pre-training, il SFT non può darglielo.
Per NLG formulaico (condominio, moto) il base model supera già la soglia: la struttura dell'italiano è acquisita, il SFT può direzionare l'output su template riconoscibili. Per task NLU e agentic servono 300–500 M token prima del SFT per rappresentazioni sufficientemente robuste. Identificata anche la strategia DAPT (Domain-Adaptive Pre-Training): 5–10 M token di dominio specifico prima del SFT, 10× più efficiente dei token generici per target formulaici. Dettaglio nelle sezioni "Strategia SFT" e "Architettura multi-track".
2026-09-30
Decisione — architettura multi-track, Run #3 come Core inamovibile in progettazione
Invece di un percorso lineare, la sperimentazione diventa ramificata: il checkpoint Run #3 viene congelato come punto di riferimento fisso e da lì partono branch paralleli con quantità di pre-training e target SFT diversi. Le misurazioni decidono cosa conviene investire prima.
Run #3 best.pt diventa il Core — rinominato e mai sovrascritto. Tre branch principali: Branch A (SFT diretto ora, per NLG formulaico), Branch B (DAPT di dominio → SFT, 2–4 settimane), Branch C (extended pre-training +200 M token → nuovo base per Run #6 SFT). Ogni branch ha checkpoint e log separati, valutati su test set standard condivisi. Dettaglio nella sezione "Architettura multi-track".
Quello che segue è il dettaglio tecnico del training in corso. La parte più leggibile per tutti è la sezione sui testi generati, più avanti: mostra concretamente cosa scriveva il modello ai diversi stadi dell'apprendimento.
Validation loss (cross-entropy) — Wikipedia IT — checkpoint ogni 500 step
Checkpoint val loss
Step
Val loss
Perplexity
Δ loss / 500 step
Note
0
9.8258
18 541
—
random, atteso log(16000) = 9.68
500
7.5156
1 829
−2.31
frequenza token, frasi di funzione emergenti
1 000
7.0777
1 187
−0.44
nomi comuni, prime coppie articolo + nome
1 500
6.9932
1 088
−0.08
apostrofi ripetuti (wikitext markup)
2 000
6.8173
910
−0.18
2 500
6.6743
793
−0.14
3 000
6.5763
718
−0.10
prime strutture da enciclopedia
3 500
6.4433
629
−0.13
4 000
6.2337
508
−0.21
prime frasi concettualmente coerenti
5 000
6.0849
440
−0.12
6 000
5.9704
391
−0.11
struttura frasale stabile
7 000
5.8267
339
−0.14
8 000
5.7426
311
−0.08
stile Wikipedia apprezzabile
9 000
5.6512
284
−0.09
10 000
5.5424
255
−0.11
HTML entity leak (markup chimico residuo)
11 000
5.4844
241
−0.06
primo "comune italiano" corretto
11 500
5.4584
234
−0.03
train=5.2245 · gap train/val = 0.23
12 500
5.3787
216
−0.08
strutture architettoniche con anno e stile
13 000
5.3842
218
+0.006
plateau; train 5.1683, gap 0.216; testo "Cosenza in Piemonte" (struttura ok, geografia confusa)
best checkpoint — governo comunista, Aia: riferimenti geopolitici precisi; miglior val del run
29 500
4.0834
59
+0.006
oscillazione finale, best non migliorato
30 000
4.0825
59
−0.001
step finale; best checkpoint 4.0768 salvato a step ~28 500
Train–val gap stabile per tutto il run. A step 11 500: gap 0.234. A step 13 500: gap 0.244. A step 14 000: train 5.0806, val 5.3052, gap 0.225. A step 26 500: val 4.4978. A step 30 000: train 3.6792, val 4.0825, gap 0.403 — gap leggermente cresciuto nelle ultime fasi (LR bassa, più cicli su batch visti), ma nessun overfitting. La val ha continuato a scendere fino a step 28 500 (best 4.0768), poi oscillazione minima fino al completamento.
Cosa scriveva il modello — tappe
Questa è la parte più leggibile di tutto il diario. I testi sotto sono generati dal modello ai diversi stadi del training: mostrano concretamente come l'apprendimento progredisce, da puro rumore statistico a frasi riconoscibili. Temperatura 0.8, top-k 40.
step 500solo le parole più frequenti, nessuna struttura
che il a è e, e che. Nel in...
step 1 000prime coppie articolo+nome, nomi propri
di una città nel...paese del suo momento del mondo nel termin
step 1 500–2 000apostrofi ripetuti — il modello impara la frequenza prima del significatoanomalia corpus
dei un punto del centro della Repubblica di popolazione a Chiesa della territorio
di una quale l'''''Italia dei uninterno di Santa
step 3 000prime strutture enciclopediche, numeri storici
del centro secolo dell'agno di un paese di un fiume delepoca. Dal 186
step 4 000prima frase con senso concettuale
e in cui la sua parte a un punto, come si possono dei suoi di più: si è di una velocità. Per cui, la formula spazio
step 6 000registro enciclopedico riconoscibile
a essere stata il primo progetto del primo e per la stessa parte. La produzione del suo caso
step 8 000toponomastica italiana, stile Wikipedia ★
, che si trovava nella strada, in posizione dell'area di San Giovanni, la prima domenica del paese
step 10 000tag HTML non rimossi — residuo di articoli di chimicaanomalia
meth" (18gt;) { < <chem>(lt;</chem>
step 11 000primo "comune italiano" corretto ★★
è un comune italiano di abitanti della metropolitana di Asti in Lombardia. Geografia fisica. Il territorio comunale comp
ano, e con il suo contributo al potere, con l'eccezione di alcuni storici di diritto. L'alleanza con la Francia nazista.
step 26 500lessico economico/geografico, struttura enciclopedica consolidata ★★★★★val 4.50
rali. Le immagini del flusso delle monete del "Musion des Institution" dei Paesi Bassi hanno permesso alla Francia una p
step 30 000 — completatoregistro geopolitico, nomi propri internazionali, struttura narrativa complessa ★★★★★best val 4.0768
cheno di Aia. A questo punto il governo comunista aveva perso la sua popolarità e il loro appoggio al governo era stato
Come leggere la curva di apprendimento
La discesa avviene in tre fasi, ognuna delle quali corrisponde a qualcosa di riconoscibile nel testo generato.
Step 0–2 000 (loss da 9.8 a 6.8): il modello impara quali parole sono più frequenti. L'output sono le parole comuni dell'italiano. La discesa è rapida perché questa "informazione" è la più semplice. Osservazione: morfologia corretta (suffissi -zione, -mente) comparsa già a step 500, ma nessuna struttura frasale.
Step 2 000–8 000 (loss da 6.8 a 5.7): emerge la struttura della frase. Compaiono articolo, nome, verbo nell'ordine giusto. Il fenomeno degli apostrofi si risolve intorno a step 3 000: il modello capisce che gli apostrofi multipli compaiono in un contesto specifico (la formattazione di Wikipedia), prima ancora di capire la grammatica vera. Osservazione: prime frasi concettualmente coerenti a step 4 000.
Step 8 000–12 500 (loss da 5.7 a 5.38): coerenza di dominio. Il modello produce il registro enciclopedico, la toponomastica italiana, la struttura "comune di X in regione Y". Ogni decimale di loss costa più capacità di prima — siamo nella fase lenta. Anomalia puntuale a step 10 000: entità HTML da articoli di chimica non filtrate.
Step 12 500–13 000 (loss ~5.38, plateau): struttura senza fatto. Il modello costruisce frasi geografiche e amministrative corrette nella forma ("della provincia di X in Y, nel quale…"), ma le associazioni fattuali sono sbagliate: Cosenza finisce in Piemonte, Como nelle Olimpiadi. Il template della lingua è acquisito, il contenuto che quel template dovrebbe veicolare no.
Step 13 000–14 000 (loss da 5.38 a 5.31): il plateau è rotto e la discesa prosegue. A step 13 500 (val 5.3125): vocabolario sportivo — "campionato europeo", "inaugurata", toponimi inventati morfologicamente plausibili. A step 14 000 (val 5.3052): registro culturale e storico, clausole relative complesse, riferimenti temporali ("anni cinquanta"), costruzioni passive. La grammatica è più ambiziosa, i fatti restano imprecisi.
Step 14 000–20 000 (loss da 5.31 a 4.80): discesa sostenuta. Compaiono nomi geografici reali: "Repubblica di Venezia", "Valle del Vallese", "Paesi Bassi". La struttura fraseologica enciclopedica si consolida. Le entità geografiche smettono di essere inventate e iniziano a coincidere con luoghi reali.
Step 20 000–26 500 (loss da 4.80 a 4.50): fase di raffinamento storico-istituzionale. A step 22 500: "La 12 marzo 1431 il feudo di Atella" — date, feudi, nomi medievali corretti nella forma. A step 24 000: "l'alleanza con la Francia nazista" — coerenza storico-politica. A step 26 500: lessico economico e istituzionale ("monete", "Paesi Bassi", "Institution").
Step 26 500–30 000 (loss da 4.50 a 4.08 — best 4.0768): fase finale, LR in decadimento coseno verso il minimo. La loss scende con continuità fino a step ~28 500 (best checkpoint, val 4.0768), poi stabilizzazione con oscillazione minima. A step 30 000: "cheno di Aia. A questo punto il governo comunista aveva perso la sua popolarità e il loro appoggio al governo era stato" — geopolitica internazionale, coerenza narrativa su più clausole, registro enciclopedico maturo. Run #3 completato.
Note sulle anomalie del corpus
Apostrofi ripetuti (step 500–2 500). Wikipedia usa ''testo'' per corsivo e '''testo''' per grassetto. Il modello ha imparato l'alta frequenza degli apostrofi prima del loro contesto grammaticale, producendo sequenze di apostrofi nei primissimi step. Si risolve spontaneamente verso step 3 000. Per il prossimo run: pulizia più aggressiva del markup wikitext ridurrebbe questo rumore.
Tag HTML (step 10 000). Tag <chem> e entità HTML da articoli di chimica e matematica non completamente rimossi dalla fase di estrazione. Il modello genera questo output raramente. Non impatta la qualità complessiva ma è rumore evitabile. Action item: aggiornare wiki_to_corpus.py con regex per <chem>, <math>, <ref> e entità HTML numeriche.
Decisione architetturale — da PE sinusoidale a RoPE
Il sistema che il modello usa per tenere traccia dell'ordine delle parole (dove si trova ogni parola nella frase) non è compatibile con il formato di distribuzione scelto. Il prossimo training usa il sistema corretto. Il lavoro già fatto non va perso: i due approcci si confronteranno a parità di condizioni.
Decisione — 2026-09-21
Run #3 (sinusoidale) e Run #4 (RoPE) entrambi a completamento, comparazione A/B
Run #3 prosegue fino a 30 000 step. Al completamento si implementa RoPE (~50 righe di diff, zero migrazione di pesi) e si avvia Run #4 con parametri identici. Il target di deployment (llamafile/llama.cpp via GGUF con architettura LLaMA) usa RoPE nativamente. La comparazione diretta dei due modelli finali quantifica empiricamente il beneficio di RoPE a questa scala e su questo corpus.
PE sinusoidale — cosa lasciamo
Formula fissa, senza parametri apprendibili
Codifica la posizione assoluta di ogni parola
Generalizzazione scarsa a sequenze più lunghe del training
Non è l'encoding atteso da llama.cpp
RoPE — cosa acquistiamo
Codifica la distanza relativa tra parole, non la posizione assoluta
Nessun parametro apprendibile: costo di migrazione = 0
Migliore generalizzazione a sequenze più lunghe
Standard de facto: LLaMA, Mistral, Qwen, Gemma
Compatibilità nativa con llama.cpp / GGUF
Costo della migrazione
PE sinusoidale non ha parametri apprendibili: non compare nel checkpoint come tensor con gradient. Il checkpoint di Run #3 non contiene nulla da migrare. La migrazione consiste solo in modifiche al codice:
attention.py — aggiungere apply_rope(), modificare MultiHeadAttention.forward() per applicare RoPE a Q e K prima del dot-product
transformer.py — rimuovere PositionalEncoding, passare rope_freq come argomento ai blocchi
model.py — pre-calcolare le frequenze RoPE (cos θ e sin θ per ogni posizione) nel costruttore
Stima: ~50 righe di diff. Zero impatto sulla semantica del resto dell'architettura.
Cosa rimane invariato in Run #4
Corpus: Wikipedia IT identico (stessa token cache)
Tokenizer: BPE-SPM 16 000 vocab
Preset: medium (45.9 M parametri, d_model 512, 8 layer)
Quando entrambi i training saranno completati, i due modelli verranno confrontati su cinque prove progettate per far emergere le differenze tra i due sistemi di posizionamento. La prova più discriminante è la seconda: vedere come si comportano oltre i limiti di lunghezza su cui sono stati addestrati.
Prova 1 — Perplexity su val set comune quantitativa
Il confronto diretto più pulito: misurare la val loss finale di entrambi sullo stesso insieme di testi non visti durante il training. Se la differenza è <0.1, le due architetture sono equivalenti a questa scala.
Prova 2 — Generazione oltre i limiti di training più discriminante
Entrambi i modelli sono stati addestrati su sequenze di al massimo 256 parole (token). Si genera con un testo iniziale di 100 token e si osserva cosa succede tra le posizioni 257 e 512, dove il modello non è mai stato.
Sinusoidale: la formula è matematicamente continua, ma quelle posizioni non compaiono mai nel training. L'attenzione non sa pesare quei valori. Probabilmente: incoerenza, ripetizione, loop.
RoPE: codifica la distanza relativa, non la posizione assoluta. Il modello non vede mai la posizione 300 in assoluto, vede solo distanze relative che ha già imparato. Teoricamente più robusto.
Prova 3 — Struttura ripetuta a distanza variabile qualitativa
Un pattern ripetuto dove rispondere correttamente richiede di tenere il conto delle posizioni relative:
Primo comune: Roma. Secondo comune: Milano. Terzo comune: Napoli. Quarto comune: Torino. Quinto comune:
Prova 4 — Coerenza di argomento su testo lungo qualitativa
Generare 256 token di testo libero con entrambi i modelli, stesso punto di partenza. Contare quante volte il testo cambia argomento. PE sinusoidale potrebbe avere più deriva tematica a distanze lunghe.
Prova 5 — Recupero di informazione nel contesto qualitativa
Inserire un'informazione specifica all'inizio del testo e chiedere al modello di riutilizzarla alla fine:
La capitale della Sardegna è Cagliari. [150 token su altro argomento] La capitale della Sardegna è
RoPE, avendo codifica relativa, dovrebbe gestire meglio le dipendenze a lunga distanza.
Cosa aspettarsi. A 46 M parametri e seq_len=256, la differenza qualitativa all'interno della distribuzione di training sarà probabilmente minima. Il vantaggio di RoPE emerge chiaramente nell'extrapolazione e nella compatibilità GGUF. Il confronto è comunque utile come dato empirico: qualsiasi differenza significativa nella val loss finale sarebbe una sorpresa interessante da documentare.
Dopo i due run
Scegliere il checkpoint migliore per il deployment
46 milioni di parametri — cosa sono e cosa producono
46 milioni di numeri floating point. Non più, non meno. Questi numeri codificano, in forma compressa, i pattern statistici di 47.8 milioni di token di testo italiano. Quello che il modello ha imparato è una sola funzione: P(token successivo | ultimi 256 token) — una distribuzione di probabilità su 16 000 pezzi di parola, condizionata sul contesto recente.
Cosa emerge dall'ottimizzazione
Il log di training mostra la sequenza precisa delle acquisizioni — ognuna emerge quando la distribuzione statistica del corpus è sufficiente per apprenderla:
Step 0–2 000 (morfologia): il modello non viene istruito sulla grammatica italiana. Impara che dopo "la" viene un sostantivo femminile perché quella sequenza è statisticamente dominante. La grammatica è compressa nella frequenza del corpus.
Step 2 000–8 000 (sintassi): emerge la struttura della frase. Le frasi grammaticalmente corrette sono molto più frequenti di quelle scorrette nel testo Wikipedia: la correttezza emerge come massimizzazione della verosimiglianza, non come regola esplicita.
Step 8 000–14 000 (registro enciclopedico): "X è una Y situata nella regione Z" è un template così frequente in Wikipedia che il modello lo interiorizza come struttura probabilistica dominante. Il registro emerge prima dei fatti.
Step 14 000–30 000 (fatti imprecisi): il modello "sa" che Roma è in Italia perché ha visto quella co-occorrenza migliaia di volte. Ma non ha un knowledge graph — ha una distribuzione di probabilità. "Cosenza in Piemonte" a step 13 000 non era un errore casuale: il modello aveva imparato il template geografico prima delle associazioni corrette.
La perplexity come posizione sul continuum
Modello
Perplexity
Nota
Distribuzione uniforme su 16 K vocab
16 000
entropia massima
Run #3 — step 0
~9 700
dopo inizializzazione pesata
Run #3 — step 8 000
~297
registro acquisito
Run #3 — step 30 000 (attuale)
59
val loss 4.0768
GPT-2 Small (117 M, inglese)
~29
riferimento storico
Umano (stima)
~15–20
benchmark superiore
PP = 59 significa che il modello assegna mediamente probabilità 1/59 ≈ 1.7% al token corretto. Un native speaker implicito ne assegnerebbe ~5%. Il modello è circa 3× più "sorpreso" dal testo corretto rispetto a un umano. Questo gap si chiude solo con più parametri e più dati — non con più training sullo stesso corpus.
Il valore educativo — il microscopio
Il modello da 46 M non è "stupido in attesa di diventare intelligente". È uno strumento di osservazione. Con GPT-4 non è possibile tracciare dove finisce la statistica e dove inizia la comprensione — se inizia. Con Run #3 è possibile vedere esattamente quando è emerso il registro enciclopedico (step 8 000), quando si è rotto il template geografico (step 13 000), quando ha acquisito coerenza storico-istituzionale (step 22 000–26 000). Questi sono gli stessi fenomeni che si osservano nei modelli grandi — solo che lì sono sepolti sotto 100–1 000 miliardi di parametri e non si possono leggere.
Scaling e compressione — le opzioni
Ridurre i parametri vs ridurre la precisione
Sono due operazioni fondamentalmente diverse con effetti molto diversi sulla qualità:
Ridurre i parametri (architettura più piccola): meno layer, d_model più piccolo. Il risultato è un modello con meno capacità di memorizzazione e coerenza. Utile solo per deployment con vincoli estremi (embedded, mobile) o come esperimento di distillazione.
Ridurre la precisione (quantizzazione): stessi 46 M parametri, meno bit per parametro. Da float32 (4 byte) a int4 (0.5 byte). Questo è ciò che fa GGUF. Qualità quasi invariata fino a Q4, con compressione 8×.
Formato
Peso
PP stimato
Nota
float32 (attuale)
553 MB
59
baseline
float16
277 MB
≈59.1
quasi identico
int8 / Q8_0
139 MB
≈59.5
trascurabile
Q4_K_M (target GGUF)
~35 MB
≈61–63
deployment ottimale
Q2_K
~18 MB
≈70–80
degrado sensibile
Il percorso verso più "intelligenza"
Il modello è Chinchilla-undertrained: regola empirica DeepMind 2022 prevede N × 20 token di training per un modello ottimale. Con 46 M parametri il target sarebbe 918 M token; il corpus attuale è 47.8 M — circa 19× sotto l'ottimale. Il primo passo per migliorare la qualità è più dati, non più parametri.
Opzione
Params
Corpus target
Costo Vast.ai
PP atteso
Più dati, stessa arch (Run #4+)
46 M
500 M–1 B token IT
~$5–10
~3.3–3.6
Scale a 175 M, corpus serio
175 M
2–3 B token IT
~$15–25
~35–45
Distillazione (5–10 M student)
5–10 M
same corpus + teacher
~$1
~90–120
LoRA su LLaMA 3.1 8B
8 B
corpus letterario attuale
~$3–5
molto basso
La distillazione è l'esperimento più interessante nel perimetro del progetto: addestrare un modello da 5–10 M con Run #3 come teacher dimostra empiricamente quanta conoscenza si trasferisce e cosa si perde — gli stessi principi usati da Microsoft per addestrare Phi-3 da GPT-4.
Un modello esistente può essere espanso a più parametri?
No, non direttamente. I pesi di un transformer sono matrici con dimensioni fissate dall'architettura. d_model 512 significa che ogni matrice di attenzione ha shape [512, 512]. Passare a d_model 768 richiede shape [768, 768] — incompatibili: non si può allargare una matrice senza decidere cosa mettere nei nuovi valori.
Esiste una tecnica chiamata Net2Net (Google Brain, 2015): si duplicano i neuroni esistenti con uno scaling che preserva la funzione del modello, poi si fa fine-tuning. Il modello allargato inizia identico al piccolo e poi diverge. Funziona in teoria, ma è rara in pratica sui LLM: il guadagno rispetto a un training da zero è marginale, perché il modello grande trascorre molto tempo a "disimparare" l'inizializzazione vincolata.
Quello che si trasferisce davvero da un run al successivo:
Il tokenizer SPM-16k — completamente riutilizzabile in qualsiasi architettura. È conoscenza compressa sull'italiano: la segmentazione ottimale del corpus è già calcolata e salvata nel checkpoint.
Il corpus e la pipeline di preprocessing — nessun lavoro da rifare.
Le scelte di iperparametri — LR, warmup, schedule: il run precedente valida già cosa funziona su questo corpus.
I pesi come teacher (distillazione) — se si scala a 175 M e si usa Run #3 come teacher, i pesi tornano utili nel senso opposto: il piccolo insegna al grande, non lo diventa.
La pratica standard quando si scala: training da zero sulla stessa base dati con architettura più grande, stesso tokenizer. Run #3 non è sprecato — è il proof-of-concept che valida corpus, tokenizer e pipeline prima di investire nel training serio.
SFT formatta il comportamento, non inietta conoscenza. La qualità SFT è bounded dalla qualità del base model: Qualità SFT ≤ f(base model, dataset SFT). Migliorare il base model prima ha rendimenti su tutto il SFT costruito sopra.
Il gap Chinchilla e cosa significa
La regola Hoffmann 2022 (DeepMind) definisce il compute-optimal come N × 20 token. Per 45 M parametri: 900 M token. Corpus attuale: 47.8 M — il 5.3% dell'ottimale. Il modello ha ancora ampia capacità inutilizzata: la val loss 4.08 (perplexity ~59) ha un ceiling teorico intorno a 10–12 per un LM italiano competente.
Questo non significa che il modello sia inutile — significa che con più dati migliorerebbe. La domanda operativa è: quanto deve migliorare il base model prima che un dato SFT ne benefici significativamente?
Tre tier di sensibilità al base model
Tier
Task
Soglia token
Base attuale
Vai ora?
1 — NLG formulaico
Condominio, Moto, Domotica
~100–150 M
SOPRA la soglia
Sì
2 — NLG creativo
Scrittura, stile personale
~200–300 M
borderline
Possibile, rischio di output piatto
3 — NLU strutturato
Conti, SQL, Log, Classificazione
~300–500 M
SOTTO la soglia
No, scarsa generalizzazione
4 — Agentic
Assistente personale, tool-use
500 M – 1 B
MOLTO SOTTO
No, o cambia modello base
La logica del Tier 1: per NLG formulaico il task è imitare un template. Il modello non deve "capire" la legge condominiale — deve imparare che dopo "Si convoca l'assemblea" vengono i convocati e poi l'ordine del giorno. Questo pattern è apprendibile con 200–300 esempi SFT anche con un base model di qualità moderata, perché la struttura dell'italiano è già acquisita.
DAPT — shortcut per task di dominio
Domain-Adaptive Pre-Training: continued pre-training su testo non supervisionato di dominio specifico prima del SFT, invece di aspettare 900 M token generici.
CORE (47.8 M tok Wikipedia IT)
→ DAPT: +5–10 M tok testi condominiali (L.220/2012, verbali, fac-simile)
→ SFT: 200–300 esempi strutturati
→ modello specializzato condominio
Efficienza: 5 M token di testo burocratico-legale italiano valgono 10× più di 50 M token aggiuntivi di Wikipedia IT per un task condominiale, perché la distribuzione target è quella. È il meccanismo alla base di BioBERT (PubMed pre-training → fine-tuning medico) e di molti modelli verticali industriali.
Il corpus DAPT per condominio è liberamente accessibile: testo della L.220/2012, artt. 1117–1139 c.c., fac-simile di convocazioni e verbali disponibili su siti di associazioni di categoria. Target: 3–5 M token, realisticamente raccoglibili in un pomeriggio.
Lo sweet spot sintetizzato
0 ────── 50 M ──── 150 M ──────── 300 M ────────── 900 M
^ ^ ^ ^
ora Tier 1 ok Tier 3 ok Chinchilla
(formulaico) (NLU) optimal
Architettura multi-track — branching da Run #3
Run #3 best.pt diventa il Core: un checkpoint frozen che non viene mai sovrascritto. Ogni esperimento successivo parte da lì e produce il suo checkpoint in un path separato. La comparazione è possibile perché tutti i branch condividono lo stesso punto di partenza.
train_sft.py — fine-tuning supervisionato su JSONL (prompt/completion), separato da train.py
tools/eval_branch.py — PPL + BLEU + output qualitativo su più checkpoint
tools/dapt.py — continued pre-training da checkpoint con corpus di dominio (può essere train.py --resume con corpus DAPT)
train_sft.py vs train.py --resume. Il continued pre-training (Branch C) può usare train.py --resume --data data/corpus_extended.txt. Il DAPT (Branch B) idem. Solo il SFT richiede un nuovo script perché il formato dei dati cambia radicalmente: invece di testo continuo sliding-window si usano coppie prompt/completion con loss mascherata sul prompt.
Corpus esteso — strategia Chinchilla-optimal
Obiettivo aggiornato (2026-10-01): 918 M token totali (Chinchilla-optimal: 20 × 45.9 M parametri). Stato attuale Run #3: 47.8 M token — 5.3% dell'ottimale. Gap da colmare: ~870 M token. Le sole fonti letterarie PD (maestrosalvo.it + Gutenberg) coprono ~80–90 M token lordi — per arrivare a scala Chinchilla è necessario un corpus web italiano di grandi dimensioni più una pipeline di filtraggio qualità.
Fonti da maestrosalvo.it — analisi piattaforme (già effettuata)
La pagina non è un archivio diretto ma elenca 10 piattaforme esterne. Quattro sono utilizzabili per training.
Piattaforma
Contenuto
Regime
Token lordi
Utilità per SLM
Gutenberg Italia
~1 000+ opere: Dante, Manzoni, Verga, Pirandello, D'Annunzio, De Amicis…
PD
~30 M
Alta — registro letterario formale
Letteratura Italiana
Classici, testi critici, saggistica letteraria
PD
~20 M
Alta — varietà registro; ottima per SFT scrittura
FeedBooks IT
PD multilingue, sezione italiana
PD
~10 M
Media — parziale sovrapposizione con Gutenberg
Manuali.it
Manuali tecnici, guide pratiche, how-to in italiano
verificare per testo
~20–30 M
Alta — tecnico-pratico, diversifica da enciclopedico
BookRepublic / InMondadori / Amazon
Narrativa contemporanea commerciale
copyright
—
Non utilizzabile per training
Caveat registro: la maggior parte del PD è pre-1928 (Dante, Manzoni, Verga). Italiano ricco ma sintatticamante lontano dal contemporaneo. Peso raccomandato nel mix finale: ≤ 10–15%. Oltre quella soglia il modello sviluppa un italiano "letterario" che richiede correzione in SFT.
Corpus web italiano — fonti a larga scala
Dataset
Dimensione grezza
Token netti stimati
Accesso
Qualità
CulturaX IT
~105 GB
~550–600 M
HF account + accettazione termini
Alta — mC4 + OSCAR già filtrati e deduplicati
CC-100 IT
~72 GB gz
~350–450 M
pubblico, streaming HuggingFace
Media — Common Crawl 2018, filtro lingua base
OSCAR-2301 IT
~50 GB
~300–350 M
HF account + termini
Media-alta — dedup + qualità verificati
mC4 IT
~30 GB
~150–200 M
pubblico, streaming HuggingFace
Media — filtro C4 adattato al multilingue
Wikipedia IT (dump completo)
~3 GB bz2
~235 M totali
pubblico, dump Wikimedia
Alta — enciclopedico, verificato editorialmente
Raccomandazione:CulturaX IT come prima scelta (già deduplicato, qualità più alta). Se accesso non disponibile: CC-100 IT via streaming (nessun account) con pipeline di filtraggio propria. CulturaX + Wikipedia + Gutenberg + Manuali porta a ~880 M token — 96% di Chinchilla-optimal.
Qualità > Quantità — la regola Qwen e la convergenza del settore
La regola emersa dai modelli 2024–2025 è unanime: un corpus di 50 M token curati supera regolarmente un corpus di 500 M non filtrati. Phi-3 (Microsoft, 2024) è addestrato su 3.3 T token sintetici ad alta qualità generati da GPT-4 — un modello da 3.8 B parametri che supera modelli 5× più grandi su molti benchmark. Qwen2.5-Coder e Qwen2.5-Math usano upsapling aggressivo dei dati di dominio (3–5×) e quality scoring basato su perplexity. La variabile dominante non è il numero di token: è la loro densità informativa.
fastText langdetect — scarta se P(italiano) < 0.85
5–15%
--lang it
3. Deduplicazione
Exact dedup su frasi; MinHash per near-duplicate a livello documento
20–40%
--dedup
4. Quality score PPL
Perplexity con Run #3 come scorer: alta PPL = testo rumoroso/non italiano. Scarta top 20%.
15–20%
--checkpoint checkpoints/best.pt --ppl-cut 0.80
5. Domain upsampling
Wikipedia + Gutenberg ripetuti 2× nel mix. Web generale 1×. Upsapling qualità senza overhead di rete.
—
Concatenazione corpus finale
Il filtro PPL al stadio 4 usa il modello Run #3 come quality scorer: testo ad alta perplexity rispetto al baseline Wikipedia è probabilmente rumoroso, frammentato o non italiano. La pipeline migliora ad ogni run — i modelli successivi possono essere usati come scorer più precisi per i corpus dei run futuri.
Composizione target — corpus Chinchilla-optimal (~918 M token)
Categoria
Fonte principale
Token target
%
Probe benchmarking
Enciclopedico
Wikipedia IT (dump completo)
~230 M
25%
probes/wiki_it.txt
Web generale
CulturaX IT / CC-100 IT filtrati
~460 M
50%
probes/web_gen.txt
Letterario PD
Gutenberg IT + Letteratura IT + FeedBooks
~92 M
10%
probes/letter_pd.txt
Tecnico
Manuali.it + PDF/DOCX tecnici
~92 M
10%
probes/tecnico_it.txt
Narrativo contemporaneo
Corpus personale esistente
~44 M
5%
probes/narrativo_it.txt
Totale
~918 M
100%
Il 50% web generale garantisce diversità linguistica e riduce l'overfitting su un singolo registro. Senza filtraggio qualità questo blocco degrada il modello — la pipeline di filtraggio non è opzionale.
Benchmarking per categoria — probe files
Cinque file probe (500–1 000 token ciascuno, verificati manualmente). La PPL su ciascuno è misurata ogni N step durante il training: curve separate per categoria permettono di identificare quando il modello "apprende" ciascun registro e se un tipo di testo causa forgetting su un altro.
probes/
wiki_it.txt # 500 token Wikipedia IT — enciclopedico contemporaneo
web_gen.txt # 500 token web pulito — informale, variato
letter_pd.txt # 500 token Manzoni / Calvino — letterario PD
tecnico_it.txt # 500 token manuale tecnico italiano
narrativo_it.txt # 500 token corpus personale — narrativo contemporaneo
# Training con probe multipli (--probe-dir carica tutti i .txt)
python3 train.py --tokenizer bpe-spm --resume \
--data data/corpus_run4.txt \
--probe-dir probes/ --probe-interval 500
Acquisizione corpus — comandi pratici
# Dipendenze
pip install pdfminer.six python-docx python-pptx odfpy
pip install datasets # HuggingFace streaming
pip install fasttext # filtro lingua
# Wikipedia IT — dump completo (senza limite; attuale è 200 MB, dump ~3 GB bz2)
curl -L "https://dumps.wikimedia.org/itwiki/latest/itwiki-latest-pages-articles.xml.bz2" \
-o data/itwiki_full.xml.bz2
python3 tools/wiki_to_corpus.py data/itwiki_full.xml.bz2 \
--max-mb 3000 --out data/wiki_it_full.txt
# Fonti letterarie PD — converter universale (epub, pdf, docx, html, pptx)
python3 tools/convert_to_text.py --dir ~/Libri/Gutenberg --out data/gutenberg_it.txt
python3 tools/convert_to_text.py --dir ~/Documenti/Tecnici \
--ext pdf,docx,html --out data/tecnico_it.txt
# CC-100 IT — streaming HuggingFace (nessun account)
python3 tools/fetch_cc100.py --lang it --max-gb 10 --out data/cc100_it_raw.txt
# Filtraggio qualità — usa Run #3 come scorer
python3 tools/filter_corpus.py data/cc100_it_raw.txt \
--checkpoint checkpoints/best.pt \
--lang it --min-words 50 --ppl-cut 0.80 --dedup \
--out data/cc100_it_clean.txt
# Corpus finale con upsampling qualità (Wikipedia 2×, resto 1×)
cat data/wiki_it_full.txt > data/corpus_run4.txt
cat data/wiki_it_full.txt >> data/corpus_run4.txt # 2× wiki
cat data/gutenberg_it.txt >> data/corpus_run4.txt
cat data/tecnico_it.txt >> data/corpus_run4.txt
cat data/cc100_it_clean.txt >> data/corpus_run4.txt
cat data/corpus_bpe.txt >> data/corpus_run4.txt # corpus personale
Tools già implementati:tools/convert_to_text.py (converter universale PDF/DOCX/PPTX/HTML/EPUB), tools/fetch_cc100.py (streaming HuggingFace CC-100), tools/filter_corpus.py (filtraggio qualità con PPL scorer), tools/eval_ppl.py (valutazione interattiva PPL su testo libero). Da implementare: flag --probe-dir in train.py per benchmarking multi-categoria durante il training.
Perplexity come quality scorer — esperimento empirico (2026-10-01)
Dopo aver implementato tools/eval_ppl.py, abbiamo eseguito un set di test su Run #3 per capire cosa rileva davvero il modello quando misura la perplexity. Il risultato è più sfumato — e più utile — di quanto ci aspettassimo.
Set di test — risultati completi
#
Testo
PPL
Livello
Diagnosi
1
"ciao sono francesco"
2 690
MOLTO ALTA
Italiano corretto — ma conversazionale. Il modello non ha mai visto questo registro.
2
"hi O'sdg"
2 240
MOLTO ALTA
Non-italiano. Sorprendentemente simile al test 1 — entrambi fuori distribuzione per motivi diversi.
3
"la provincia di Milano è ricca di smog"
78.3
MEDIA
Italiano factual, registro informativo. Entità note (Milano). Nessun errore — PPL bassa.
4
"Carlo Magno ha fondando il Sacro Romano Impero"
79.0
MEDIA
Errore grammaticale (ha fondando). PPL quasi identica al test corretto. Invisibile al modello.
5
"Carlo Magno ha fondato il Saro Romano Impero"
615.2
MOLTO ALTA
Typo in entità nominata (Saro vs Sacro). Rilevato con forza. Il token "Saro" è rarissimo in Wikipedia IT.
6
"Carlo Magno ha fondato il Sacro Romano Impero"
38.7
BASSA ✓
Testo corretto, entità nominata nota. PPL baseline di riferimento.
Il quadro che emerge
Il confronto test 4 / 5 / 6 è il risultato più importante. L'errore grammaticale ha fondando (test 4) produce PPL 79 — praticamente identica al testo corretto su stesso argomento (test 3: 78.3). Il typo Saro Romano Impero (test 5) produce PPL 615, un aumento di 16× rispetto alla baseline (test 6: 38.7).
La PPL non misura la correttezza grammaticale: misura la distanza dalla distribuzione di training. "Ha fondando" è inatteso ma non impossibile — il modello ha visto gerundi e participi in costruzioni varie, la sorpresa è contenuta. "Saro Romano Impero" rompe una sequenza che il modello conosce con certezza quasi assoluta: "Sacro Romano Impero" compare come unità coesa in migliaia di articoli Wikipedia — togliere una lettera è catastrofico per la PPL.
Capacità di Run #3 come quality scorer
Tipo di errore / mismatch
Rilevabile?
Perché
Lingua straniera
Sì — forte
Fuori distribuzione totale
Italiano informale / conversazionale
Sì — forte
Registro mai visto in Wikipedia
Typo in entità nominate note (Sacro → Saro)
Sì — forte
Token raro/OOV, rompe sequenza coesa
OCR difettoso su nomi propri
Sì — probabile
Stesso meccanismo del typo
Errori morfosintattici comuni (ha fondando)
Segnale debole
Pattern simili presenti nel corpus
Errori semantici con entità corrette
No
Modello non ha world knowledge causale
Per il filtraggio del corpus web questo profilo è utile: testi con OCR difettoso, nomi di luogo corrotti, entità frammentate emergono come alta PPL. Il modello Run #3 è di fatto un rilevatore di coerenza fattuale locale più che un correttore grammaticale. La pipeline migliora ad ogni run — Run #4, addestrato su corpus più ampio e vario, produrrà un quality scorer più sensibile anche agli errori morfosintattici.
Nota metodologica — il paradosso del testo ripetitivo
Un'avvertenza da tenere presente nel filtraggio: testo altamente ripetitivo (spam, boilerplate, liste di parole chiave) ottiene PPL bassa — è iper-prevedibile, quindi il modello non si sorprende. Il filtro PPL deve essere preceduto da un filtro euristico (rapporto linee duplicate, densità caratteri non-alfa) per escludere questi falsi negativi. La pipeline in filter_corpus.py gestisce questo: lo stadio 1 (euristico) precede lo stadio 4 (PPL scorer).
# Valutazione interattiva PPL con Run #3
python3 tools/eval_ppl.py
# → REPL: inserisci testo, invio per valutare
# Singolo testo da CLI
python3 tools/eval_ppl.py "Carlo Magno ha fondato il Sacro Romano Impero"
# Scale di riferimento (addestrato su Wikipedia IT)
# PPL < 60 : BASSA — registro enciclopedico, come Wikipedia
# PPL 60–150 : MEDIA — italiano buono, registro diverso
# PPL 150–400: ALTA — probabilmente non-italiano o degradato
# PPL 400+ : MOLTO ALTA — lingua straniera, spam, OCR difettoso
Corpus Run #5 — ricerca fonti Chinchilla-optimal (2026-10-01)
Run #4 ha raggiunto val loss 3.5861 (PPL ~36.1) con 47.8 M token — il 5.3% del target Chinchilla-optimal per 45.9 M parametri (20 × 45.9 M = 918 M token). Il passo successivo è un continued pre-training su corpus molto più ampio, mantenendo la stessa architettura. Prima di avviare il training: cp checkpoints/best.pt checkpoints/run4_best.pt.
Strategia di acquisizione
Mai scaricare dump completi: i dataset più grandi superano i 4 TB. La strategia è sempre streaming HuggingFace → filtro qualità on-the-fly → scrittura solo dei documenti che passano. Target finale: ~4 GB di testo estratto ≈ 918 M token.
Valutazione delle fonti
Dataset
Token IT est.
Priorità
Note
PleIAs/common_corpus
24 B (PD)
★★★★★
Public domain, pre-filtrato, streaming HF. Fonte primaria.
mii-community/liberliber-cleaned
~50–80 M
★★★★
881 MB, letterario IT, già curato da MII Community. Dataset di test pipeline.
gsarti/clean_mc4_it
41 B parole
★★★ ✗
Web IT pulito — ma usa loading script Python deprecato, non caricabile con datasets ≥ 2.x.
Helsinki-NLP/opus_books IT
~19.5 M
★★★
Letterario aggiuntivo, piccolo.
CC-100 IT (fetch_cc100.py)
~500 M
★★★
Web generico, fallback se Common Corpus non basta.
ManyBooks
—
✗
Mirror di Project Gutenberg, nessun valore aggiuntivo.
Open Library
—
✗
Solo metadati/catalogo — nessun testo. Utile solo per verificare se un libro fisico è su Internet Archive.
MII Community: gruppo di ricerca AI italiano (autori di Nesso 0.4B e Zefiro 7B) che mantiene mii-community/liberliber-cleaned — 4510 documenti con correzione umana. Già disponibile su HuggingFace, pronto per streaming diretto.
Mix target 918 M token
Categoria
Token
%
Fonte
Enciclopedico
~230 M
25%
Wikipedia IT dump (già estratto)
Web generale
~460 M
50%
Common Corpus IT
Letterario PD
~92 M
10%
liberliber + opus_books
Tecnico
~92 M
10%
PDF/DOCX personali via convert_to_text.py
Narrativo contemporaneo
~44 M
5%
corpus EPUB personale
Tool implementato: fetch_hf_corpus.py
Streaming generico per qualsiasi dataset HuggingFace. Supporta selezione configurazione, filtro lingua, campo testo annidato (translation.it), limite GB e limite documenti. Non richiede download completo del dump.
# liberliber — dataset di test pipeline (piccolo, alta qualità)
python3 tools/fetch_hf_corpus.py \
--dataset mii-community/liberliber-cleaned \
--text-field text \
--out data/liberliber_it.txt
# Common Corpus — fonte primaria Run #5 (solo italiano, public domain)
python3 tools/fetch_hf_corpus.py \
--dataset PleIAs/common_corpus \
--text-field text --lang-field language --lang it \
--max-gb 5 --out data/common_corpus_it_raw.txt
# opus_books italiano
python3 tools/fetch_hf_corpus.py \
--dataset Helsinki-NLP/opus_books --config it \
--text-field translation.it \
--out data/opus_books_it.txt
# dry-run senza scrivere file
python3 tools/fetch_hf_corpus.py \
--dataset mii-community/liberliber-cleaned \
--text-field text --max-docs 500 --dry-run
# Filtraggio qualità post-estrazione
python3 tools/filter_corpus.py data/liberliber_it.txt \
--checkpoint checkpoints/run4_best.pt \
--dedup --ppl-cut 0.80 --out data/liberliber_it_clean.txt
gsarti/clean_mc4_it — incompatibile: il dataset usa un loading script Python (clean_mc4_it.py) non più supportato da datasets ≥ 2.x. L'autore non ha ancora migrato a formato Parquet. Escluso dal corpus Run #5. Tools corpus:fetch_hf_corpus.py (generico HF), fetch_cc100.py (CC-100), export_db_corpus.py (PostgreSQL/MongoDB), filter_corpus.py (filtraggio qualità 4 stadi).
Test pipeline end-to-end su liberliber (2026-10-01)
Prima esecuzione completa della pipeline fetch → filter sul dataset mii-community/liberliber-cleaned. Obiettivo: verificare che tutti i componenti funzionino prima di attaccare PleIAs/common_corpus (la fonte principale da 24B token).
Fase 1 — estrazione
Metrica
Valore
Dataset
mii-community/liberliber-cleaned
Documenti scritti
4 289
Documenti saltati (vuoti / < 50 char)
221
Testo estratto
1.33 GB
Durata
2m 38s
File output
data/liberliber_it.txt (1 363 MB)
Il dataset ha 4 510 documenti totali — 4 289 + 221 = 4 510. Il 95.1% dei documenti ha superato il filtro minimo di 50 caratteri.
Fase 2 — filtraggio qualità
Filtro a 2 stadi attivi: euristico (lunghezza, rapporto simboli, duplicati interni) + dedup MD5 paragrafo + PPL scorer con checkpoints/best.pt (Run #4, PPL ~36). Soglia --ppl-cut 0.80 = scarta il 20% dei documenti con PPL più alta. Nessun filtro lingua (liberliber è già 100% italiano curato).
Checkpoint backup prima del test:cp checkpoints/best.pt checkpoints/run4_best.pt (530 MB, eseguito il 2026-10-01). I file .gguf nella root non vengono mai toccati dal training.
Decisione: liberliber escluso dal corpus Run #5
Ispezione manuale delle prime 300 righe di data/liberliber_it.txt: il testo è fortemente arcaico — italiano del XIII–XIX secolo (Dante, Manzoni, Leopardi, costruzioni come "eravi", "imperocché", "avvegnadio"). Per un modello base su italiano contemporaneo questo è rumore, non segnale: il modello assorbirebbe forme medievali come italiano normale, degradando la qualità su testo moderno.
La quota letteraria (originariamente 10% = ~92M token) viene redistribuita su PleIAs/common_corpus, che include già testi storici di qualità senza scendere all'italiano arcaico. Il corpus EPUB personale (narrativa contemporanea, prima persona) copre la componente letteraria moderna.
Mix target aggiornato — 918 M token
Categoria
Token
%
Fonte
Enciclopedico
~230 M
25%
Wikipedia IT dump (già estratto)
Web generale / PD
~550 M
60%
Common Corpus IT (PleIAs) — fonte primaria
Tecnico
~92 M
10%
PDF/DOCX personali via convert_to_text.py
Narrativo contemporaneo
~46 M
5%
Corpus EPUB personale (prima persona, IT moderno)
Esclusi:mii-community/liberliber-cleaned (italiano arcaico XIII–XIX sec.), PleIAs/Italian-PD (stesso problema, focus pre-1884), gsarti/clean_mc4_it (loading script deprecato), ManyBooks (mirror Gutenberg), Open Library (solo metadati). PleIAs/common_corpus: streaming lentissimo per l'italiano (1-2% del totale, nessuno shard per lingua). uonlp/CulturaX: gated ma accessibile con autenticazione HF.
Fonte scelta: CulturaX IT (2026-10-01)
CulturaX (uonlp/CulturaX, config it) è la combinazione pulita e deduplicata di mC4 + OSCAR italiano. Italiano moderno del web: giornalismo, blog, forum, articoli. Config it = 100% italiano, nessun filtro lingua necessario. Richiede autenticazione HuggingFace (account gratuito + accettazione termini). Campi: text, timestamp, url, source.
Tool di autenticazione: tools/hf_auth.py — gestisce login, whoami, logout e test accesso dataset gated. Il token viene salvato in ~/.cache/huggingface/token.
# Autenticazione HF (token Read da huggingface.co/settings/tokens)
python3 tools/hf_auth.py login hf_ILTUOTOKEN
python3 tools/hf_auth.py whoami
# Estrazione CulturaX IT — 8 GB corpus web italiano moderno
python3 tools/fetch_hf_corpus.py \
--dataset uonlp/CulturaX --config it \
--text-field text \
--max-gb 8 --out data/culturax_it_raw.txt
# → 2 627 111 doc, 8 GB, 12m 14s (2026-10-01)
# Filtraggio qualità: solo euristico + dedup — NO PPL scorer
python3 tools/filter_corpus.py data/culturax_it_raw.txt \
--dedup \
--out data/culturax_it_clean.txt
Decisione: PPL scorer escluso dal filtraggio CulturaX (2026-10-01)
Durante la prima esecuzione del filtro con --checkpoint checkpoints/best.pt --ppl-cut 0.80 è emersa una contraddizione metodologica: usare Run #4 come quality scorer per selezionare il corpus che dovrebbe correggerne i bias.
Run #4 è addestrato esclusivamente su Wikipedia IT — registro enciclopedico formale. La sua distribuzione interna assegna PPL alta a qualsiasi testo lontano da quel registro: italiano colloquiale, giornalistico informale, dialogo, thread di forum. Applicare il PPL cut su CulturaX con questo scorer avrebbe selezionato preferenzialmente testi enciclopedici-formali, rafforzando lo skew sintattico esistente invece di correggerlo. La frase "ciao, sono Francesco" ottiene PPL 2 690 sul modello Run #4 — non perché sia testo di bassa qualità, ma perché il modello non ha mai visto questo registro.
Il principio generale: il PPL scorer è affidabile come quality filter solo quando scorer e corpus target hanno distribuzione simile. Wikipedia → Wikipedia: funziona. Wikipedia → web colloquiale: introduce bias sistematico verso i propri pesi. Per il corpus di espansione, il filtro corretto è euristico (struttura, lunghezza, rapporto simboli) + deduplicazione — nessun filtro PPL.
Il PPL scorer rimane valido per: filtrare corpus enciclopedici successivi, valutare la qualità del modello post-training, e come quality scorer dopo Run #5 (quando la distribuzione del modello sarà più ampia e rappresentativa dell'italiano moderno).
Confronto controllato: euristico vs euristico + PPL 0.95 (2026-10-02)
Per validare la decisione sul PPL scorer, produciamo due file a partire dallo stesso input grezzo — così l'unica variabile è la presenza o assenza del filtro PPL:
File
Pipeline
Stato
data/culturax_it_clean.txt
raw → euristico + dedup
Prodotto — 2.58M doc, 8.16 GB
data/culturax_it_ppl95.txt
raw → euristico + dedup + PPL 0.95
Da produrre
data/culturax_ppl95_rejected.txt
campione 500 doc scartati da PPL con score
Da produrre (via --rejected)
Il file rejected contiene i primi 500 documenti scartati dal PPL scorer con il loro score — consente di leggere direttamente cosa viene eliminato e valutare se si tratta di garbage reale o di italiano colloquiale valido.
# Produzione file B — stesso raw, aggiunge PPL scorer a p95
python3 tools/filter_corpus.py data/culturax_it_raw.txt \
--checkpoint checkpoints/best.pt \
--ppl-cut 0.95 --dedup \
--out data/culturax_it_ppl95.txt \
--rejected data/culturax_ppl95_rejected.txt
Infrastruttura cloud — Vast.ai (2026-10-02)
Il PPL scorer su CPU (M2 Mac) processa ~7 doc/secondo. Su 2.6 M documenti CulturaX la stima era 4 giorni. Soluzione: spostare il job su GPU via Vast.ai, dove la stessa inference sul modello da 45.9 M parametri è 50–100× più veloce. Stima su RTX 4090: 2–3 ore.
Perché la velocità cala con il PPL scorer
La fase euristico+dedup è I/O bound: processa migliaia di documenti al secondo perché legge testo e applica regole in Python puro. Il PPL scorer aggiunge un forward pass del modello per ogni documento. Su CPU: ~140 ms per documento (tokenizzazione + 12 layer attention + softmax + cross-entropy). Su RTX 4090: ~2–3 ms. Il collo di bottiglia non è il corpus, è l'inferenza.
Fix applicati a filter_corpus.py
Problema
Prima
Dopo
Device hardcoded (1°)
device = 'cpu'
cuda → mps → cpu (auto-detect) override con --device
Device hardcoded (2°) — scoperto in produzione
device = 'cpu' dopo l'auto-detect, dentro load_ppl_scorer()
Riga rimossa. Il parametro device passato alla funzione viene ora usato direttamente da map_location e .to(device)
f.tell() con iteratore — scoperto in produzione
for line in f_in:
while True: line = f_in.readline() L'iteratore Python disabilita f.tell(); readline() no.
Log threshold fisso
ogni 100 000 doc
ogni 5 000 doc se PPL attivo ogni 100 000 altrimenti
Nessun avanzamento visibile
—
% completamento + ETA calcolata da bytes letti / dimensione file
Avviso assente per CPU lento
—
Stampa stima ore all'avvio se device=cpu
Caveat produzione — python3 -u obbligatorio: quando filter_corpus.py è lanciato con 2>&1 | tee filter.log, Python entra in modalità fully-buffered perché stdout non è un terminale. Le print() vengono accumulate in memoria e il log rimane vuoto per ore. Soluzione: usare sempre python3 -u (unbuffered). Senza questo flag il log appare vuoto anche con job attivo.
Workflow Vast.ai — provisioning
Il ciclo di vita è gestito dalla skill .claude/agents/vastai-slm.md. Comandi chiave:
# Autenticazione (API key in .vastai-apikey, mai committata)
KEY=$(cat .vastai-apikey) && vastai set api-key "$KEY"
# Ricerca offerta — RTX 4090 on-demand, ordinata per costo
vastai search offers 'gpu_name=RTX_4090 cuda_vers>=12.0 disk_space>=40 num_gpus=1' -o dph_base --raw
# Crea istanza (ID offerta scelto: 44885351, $0.360/ora, affidabilità 100%)
vastai create instance 44885351 \
--image pytorch/pytorch:2.1.0-cuda12.1-cudnn8-runtime \
--disk 40 --raw
# → restituisce {"new_contract": , "success": true}
# Salva ID in .vastai-instance
# Attendi Running
vastai show instances --raw # controlla actual_status
# SSH access
vastai ssh-url # → ssh -p root@
Criteri di selezione offerta: RTX 4090 (VRAM 24 GB, sufficiente per modello 45.9 M param + batch inference), CUDA ≥ 12.0, disk ≥ 40 GB (8 GB corpus + 6 GB output + margine), ordinata per costo. Istanza on-demand (non interruptible): il job di filtering non ha checkpoint intermedi, una terminazione anticipata perderebbe tutto il progresso.
Setup remoto e avvio job
# Trasferimento (codice + checkpoint, ~600 MB; corpus ri-scaricato su Vast.ai)
rsync -avz -e "ssh -p PORT -o StrictHostKeyChecking=no" \
--exclude='.git' --exclude='data/' --exclude='*.gguf' --exclude='.claude' \
. root@HOST:~/slm/
# Installa dipendenze
ssh -p PORT root@HOST "pip install sentencepiece datasets huggingface_hub"
# Autentica HF (token personale, mai nel codice)
ssh -p PORT root@HOST "export HF_TOKEN=hf_xxx"
# Riscarica CulturaX raw (cloud-to-cloud: ~8 GB in pochi minuti)
ssh -p PORT root@HOST "cd ~/slm && python3 tools/fetch_hf_corpus.py \
--dataset uonlp/CulturaX --config it --text-field text \
--max-gb 8 --out data/culturax_it_raw.txt"
# Avvia filtering in tmux (sopravvive alla disconnessione SSH)
# python3 -u = unbuffered: senza questo flag il log resta vuoto per ore (tee bufferizza)
# --device cuda: esplicito anche se auto-detect è attivo — evita sorprese
ssh -p PORT root@HOST "cd ~/slm && tmux new-session -d -s filter \
'python3 -u tools/filter_corpus.py data/culturax_it_raw.txt \
--checkpoint checkpoints/best.pt \
--ppl-cut 0.95 --dedup \
--device cuda \
--out data/culturax_it_ppl95.txt \
--rejected data/culturax_ppl95_rejected.txt \
2>&1 | tee /root/filter.log'"
Costo operazione: $0.360/ora × ~3 ore = ~$1.08. Con $8.36 di credito disponibili: margine di ~21 ore aggiuntive. Il costo è addebitato anche quando l'istanza è idle — distruggerla subito dopo il rimpatrio.
Procedura manuale completa (per esecuzioni non automatizzate)
Sequenza completa per eseguire il job di filtering su una nuova istanza Vast.ai, passo per passo, senza automazione Claude Code.
# ── FASE 0: Prerequisiti locali ──────────────────────────────────
# Installa vastai CLI (se assente)
pip install vastai
# Configura API key (salvata in .vastai-apikey nel progetto)
KEY=$(cat .vastai-apikey) && vastai set api-key "$KEY"
# Verifica account e credito
vastai show user
# ── FASE 1: Cerca offerta GPU non-US ─────────────────────────────
vastai search offers \
'gpu_name=RTX_4090 cuda_vers>=12.0 disk_space>=40 num_gpus=1' \
-o dph_base --raw | python3 -c "
import json, sys
data = json.load(sys.stdin)
# filtra non-US
offers = [o for o in data if 'US' not in str(o.get('geolocation',''))][:8]
for o in offers:
print(o['id'], o.get('gpu_name'), o.get('dph_base'), o.get('reliability2'), o.get('geolocation'))
"
# Scegli un ID con affidabilità ≥ 99% e prezzo contenuto
# ── FASE 2: Crea istanza ──────────────────────────────────────────
# Immagine leggera: nvidia/cuda base (~3GB, 5-10min) + pip install torch sul nodo
# NON usare pytorch/pytorch completa (~8GB, 20-40min)
vastai create instance \
--image nvidia/cuda:12.1.0-cudnn8-runtime-ubuntu20.04 \
--disk 40 --raw
# → prendi new_contract = ID istanza, salvalo
# Attendi running (controlla ogni 30s)
vastai show instances --raw # cerca actual_status=running per il tuo ID
# ── FASE 3: Connessione SSH ───────────────────────────────────────
# Ottieni HOST e PORTA
vastai ssh-url # → ssh -p root@
# Test connessione
ssh -i ~/.ssh/vastai_slm -o StrictHostKeyChecking=no -p root@ \
"echo OK && nvidia-smi --query-gpu=name --format=csv,noheader"
# ── FASE 4: Trasferimento progetto ───────────────────────────────
# Crea tar locale (solo codice + checkpoint, senza data/ e .git)
cd /path/to/slm
tar -czf ~/slm_vastai.tar.gz \
--exclude='./.git' --exclude='./.claude' \
--exclude='./data' --exclude='./private' --exclude='./docs' \
--exclude='./*.gguf' --exclude='./checkpoints/run3_best.pt' \
--exclude='./checkpoints/run4_best.pt' --exclude='./__pycache__' .
# Upload (~500MB)
rsync -az --progress \
-e "ssh -i ~/.ssh/vastai_slm -o StrictHostKeyChecking=no -p " \
~/slm_vastai.tar.gz root@:~/
# ── FASE 5: Setup remoto ─────────────────────────────────────────
ssh -i ~/.ssh/vastai_slm -o StrictHostKeyChecking=no -p root@
# Sul nodo remoto:
mkdir -p ~/slm && tar -xzf ~/slm_vastai.tar.gz -C ~/slm
# Upgrade pip (l'immagine ubuntu:20.04 ha pip 20.x, troppo vecchio)
python3 -m pip install --upgrade pip
# Installa dipendenze in tmux (sopravvive alla disconnessione SSH)
tmux new-session -d -s setup \
'pip3 install torch --index-url https://download.pytorch.org/whl/cu121 \
sentencepiece datasets huggingface_hub 2>&1 | tee /root/pip.log && echo DONE >> /root/pip.log'
# Monitora
tail -f /root/pip.log # ctrl+c per uscire senza interrompere
# Verifica al termine
python3 -c "import torch; print(torch.__version__, torch.cuda.is_available(), torch.cuda.get_device_name(0))"
# ── FASE 6: Autentica HuggingFace e scarica CulturaX ─────────────
# Autentica (token personale da huggingface.co → Settings → Access Tokens)
export HF_TOKEN=hf_xxxxxxxxxxxx
# oppure:
python3 tools/hf_auth.py login hf_xxxxxxxxxxxx
# Scarica CulturaX in tmux (cloud-to-cloud, ~15-25 min per 8GB)
tmux new-session -d -s fetch \
'cd ~/slm && python3 tools/fetch_hf_corpus.py \
--dataset uonlp/CulturaX --config it \
--text-field text --max-gb 8 \
--out data/culturax_it_raw.txt 2>&1 | tee /root/fetch.log'
tail -f /root/fetch.log
# ── FASE 7: Avvia job di filtering ───────────────────────────────
# IMPORTANTE: python3 -u (unbuffered) obbligatorio con tee — altrimenti log vuoto
# IMPORTANTE: --device cuda esplicito — l'auto-detect funziona ma meglio non rischiare
tmux new-session -d -s filter \
'cd ~/slm && python3 -u tools/filter_corpus.py data/culturax_it_raw.txt \
--checkpoint checkpoints/best.pt \
--ppl-cut 0.95 --dedup \
--device cuda \
--out data/culturax_it_ppl95.txt \
--rejected data/culturax_ppl95_rejected.txt \
2>&1 | tee /root/filter.log'
# Monitora (log ogni 5000 doc, mostra % e ETA)
tail -f /root/filter.log
# Verifica device usato (deve mostrare "cuda", non "cpu"):
grep "Scorer pronto" /root/filter.log
# Distribuzione PPL empirica su CulturaX IT (campione 5000 doc, modello run3):
# p10=47.7 mediana=80.1 p95=203.2 → soglia al 95% = 203.2
# ── FASE 8: Rimpatrio risultati ───────────────────────────────────
# Dal Mac (dopo completamento job, ~4-8 ore)
rsync -avz --progress \
-e "ssh -i ~/.ssh/vastai_slm -o StrictHostKeyChecking=no -p " \
root@:~/slm/data/culturax_it_ppl95.txt data/
rsync -avz \
-e "ssh -i ~/.ssh/vastai_slm -o StrictHostKeyChecking=no -p " \
root@:~/slm/data/culturax_ppl95_rejected.txt data/
# ── FASE 9: Destroy istanza ───────────────────────────────────────
vastai destroy instance
rm .vastai-instance
# Il costo si interrompe immediatamente
Tempi misurati su RTX 4090 (Italia, $0.436/h, 2026-10-02):
Download immagine nvidia/cuda: ~8 min | pip install torch+sentencepiece: ~12 min | Download CulturaX 8GB da HF: ~36 min (misurato: da 0.5k a 1.2k doc/s, totale 2174s) | PPL filter 2.6M doc: ~7h (ETA misurata a inizio job, 100 doc/s su GPU batch=1) | Rimpatrio 6GB: 15–20 min. Costo totale stimato: $3–$4.
Distribuzione PPL empirica su CulturaX IT (campione 5000 doc, scorer run3, cut=95%): p10=47.7 · mediana=80.1 · p95=203.2. Il 5% dei documenti scartati supera PPL=203 — testo non-italiano, boilerplate, o troppo frammentato per il tokenizzatore BPE.
Validazione corpus CulturaX filtrato — osservazioni (2026-10-03)
Dopo il rimpatrio, ispezione manuale a campione di culturax_it_ppl95.txt (kept) e culturax_ppl95_rejected.txt (scartati). Risultati complessivamente positivi, con tre criticità identificate.
Risultati del filtraggio (definitivi)
Metrica
Valore
Documenti in input
2.722.042
Kept (output)
2.439.329 (89.6%)
Scartati euristico
140.635 (5.2%)
Scartati dedup
115 (0.004% — corpus già quasi privo di duplicati)
Scartati PPL >203.2
141.963 (5.2%)
Dimensione output
7.5 GB
Durata RTX 4090
7h 26m 53s
Criticità 1 — Token alfanumerici misti nel corpus kept
Il corpus filtrato contiene documenti con parole come v4sta, c4n — pattern da nomi utente, codici prodotto, leetspeak da web. L'euristico attuale non li coglie perché il documento supera i controlli di lunghezza, caratteri speciali e lingua. Questo tipo di rumore è particolarmente dannoso per il training perché contamina la distribuzione lessicale del BPE: il tokenizzatore (addestrato su Wikipedia) spezza questi token in unità sub-word inattese.
Fix applicato (2026-10-03): aggiunto controllo in passes_heuristic(): se più del 5% delle parole contiene pattern lettera-cifra o cifra-lettera (re.search(r'[a-zA-Z]\d|\d[a-zA-Z]', w)), il documento viene scartato. Soglia conservativa — cattura leetspeak e username senza penalizzare testo tecnico che cita versioni software in modo isolato.
Criticità 2 — PPL scorer esclude poesia, include pubblicità
Il PPL scorer addestrato su Wikipedia produce distorsioni sistematiche su CulturaX (testo web):
Poesia esclusa (falso negativo): sintassi non-standard, enjambement, inversioni → PPL alta → scartata. La poesia è italiano di alta qualità ma non assomiglia a un articolo enciclopedico.
Pubblicità inclusa (falso positivo): frasi brevi, grammaticalmente corrette, registro formale → PPL bassa → mantenuta. Le pubblicità superano il filtro perché il modello le trova "prevedibili".
Comportamento atteso e già predetto nel principio del PPL scorer. Conferma che il filtro PPL su CulturaX non misura "qualità assoluta" ma "somiglianza alla distribuzione Wikipedia".
Decisione: corpus usato così com'è per Run #5
Nonostante le criticità, il corpus da 2.4M documenti / 7.5 GB viene usato per Run #5 senza ulteriore ri-filtraggio. Motivazioni:
Il volume (7.5 GB) vale più della pulizia marginale ottenibile con un secondo passaggio.
Le pubblicità sono italiano reale — il modello impara strutture sintattiche valide anche da testo commerciale.
Il fix alfanumerici misti si applica ai run futuri, non retroattivamente (costo di ri-filtraggio: 7h su GPU).
CulturaX è già pre-filtrato dagli autori (mC4 + OSCAR con deduplication) — la qualità baseline è alta.
Run #5 — setup e avvio training (2026-10-03)
Run #5: prima espansione del corpus oltre Wikipedia. Obiettivo: avvicinarsi al target Chinchilla-optimal di 918 M token partendo dal checkpoint Run #4 (val loss 3.5861, perplexity ~36). Training in locale su Mac M2 (8GB, MPS) — scelta motivata dalla disponibilità della macchina per 36h consecutive e dal costo zero rispetto a Vast.ai (~$1.50 stimato per il training).
Corpus Run #5
Mix target 918 M token (Chinchilla: 20 × 45.9 M param). Composizione attuale disponibile:
Fonte
File
Peso nel mix
Note
Wikipedia IT
data/wiki_it.txt
25%
Già usato in Run #3–4. Registro enciclopedico.
CulturaX IT (filtrato)
data/culturax_it_ppl95.txt
60%
7.5 GB, 2.4M doc, testo web italiano moderno.
Corpus letterario
data/corpus_letterario.txt
5%
EPUB narrativa contemporanea, prima persona.
Corpus tecnico personale
data/corpus.txt
10%
Testi di dominio.
# Backup checkpoint prima di --resume (OBBLIGATORIO)
cp checkpoints/best.pt checkpoints/run4_best.pt
# Prepara corpus concatenato (proporzioni approssimate da dimensione file)
cat data/wiki_it.txt data/culturax_it_ppl95.txt \
data/corpus_letterario.txt data/corpus.txt \
> data/corpus_run5.txt
# Avvia training con --resume da Run #4
# tmux per sopravvivere alla disconnessione del terminale
tmux new-session -d -s run5 \
'python3 train.py \
--tokenizer bpe-spm \
--data data/corpus_run5.txt \
--resume \
2>&1 | tee /tmp/run5.log'
tail -f /tmp/run5.log
Mac M2 vs Vast.ai per Run #5: MPS è ~10–15× più lento di RTX 4090. Stima: 56.000 step → 23–35 ore su M2 vs 2.5 ore su GPU. Scelta M2 motivata da disponibilità macchina per 36h consecutive e credito Vast.ai residuo (~$4.65) da preservare per run futuri o iterazioni rapide.
Run #5 — crash OOM e training a shard (2026-10-03)
Il primo avvio di Run #5 ha prodotto due crash consecutivi per esaurimento memoria. Causa: train.py caricava l'intero corpus da 8.29 GB come stringa Python, poi lo convertiva in una lista Python di interi (overhead ~28 byte/elemento) prima di creare il tensor. Picco stimato: 20+ GB su un sistema da 8 GB.
Fix 1 — encoding in streaming (train.py)
Tre modifiche a train.py:
Eliminato Path.read_text(): il file non viene mai caricato interamente in RAM. Per tokenizer bpe-spm e bpe-tiktoken, il testo non serve al tokenizer (che carica un modello già addestrato).
_encode_with_progress → streaming: legge il file a chunk da 10 M caratteri, codifica ciascuno con SPM, accumula in numpy.ndarray di tipo int16 (vocab 16.000 < 32.767 = max int16). numpy usa array C contigui: nessun overhead per oggetto.
Cache e tensor in int16: torch.from_numpy(arr) è zero-copy. get_batch() fa il cast a .long() solo sui 16.384 token del batch corrente — overhead trascurabile.
Fase
Prima (int64 / lista Python)
Dopo (int16 / numpy streaming)
Lettura file
7.7 GB stringa Python in RAM
10 MB per chunk, scartato dopo encode
Lista intermedia
1.56B interi × 28 B = ~44 GB
array numpy: 1.56B × 2 B = 3.1 GB
Tensor finale
1.56B × 8 B = 12.5 GB (int64)
1.56B × 2 B = 3.1 GB (int16)
Picco totale
>20 GB
~3–4 GB
Fix 2 — training a shard (tools/train_chunked.sh)
Anche con il fix di encoding, il corpus da 8.29 GB genera un tensor int16 da 3.1 GB che, sommato a modello (530 MB), overhead MPS e OS, resta al limite degli 8 GB. Per eliminare il rischio, il corpus viene spezzato in 7 shard da ~1.2 GB ciascuno. Lo script tools/train_chunked.sh orchestra il ciclo: split → per ogni shard: tokenizza + salva cache → training --resume.
LR schedule per shard: ogni shard ricomincia il proprio ciclo warmup → cosine decay (100 step di re-warmup, poi decay su 8.000 step). Produce un LR ciclico non pianificato — tollerabile per una run di ricerca, da correggere in run future con un LR schedule globale (passando il contatore di step cumulativi).
Caveat tokenizzazione: con chunk da 10M caratteri, il confine tra chunk può spezzare una sequenza BPE. L'effetto è trascurabile (al più 1 token scorretto ogni 10M caratteri), ma va tenuto a mente se si osservano anomalie nei token a inizio documento.
Run #5 — shard 1/7: prime metriche (2026-10-03)
Dopo tre avvii falliti (fix --rope mancante, poi OOM MPS con batch 32), il training di Run #5 è stabile con --batch 8. Il training su M2 con batch ridotto comporta ~2.5s/step (vs ~0.15s/step su RTX 4090). ETA per shard 1: ~5.5 ore.
train_chunked.sh mancava di --rope: il modello era costruito con PE sinusoidale invece di RoPE
Aggiunto --rope al comando in train_chunked.sh
OOM MPS (allocated 8.96GB / max 9.07GB)
Batch 32 × seq 512 × 12 layer → attivazioni ~9GB su M2 con memoria GPU/CPU condivisa
Ridotto a --batch 8
GGUF mancanti su disco
Generati su Vast.ai durante Run #4, mai scaricati prima della destroy dell'istanza
Riconvertiti da checkpoints/run4_best.pt via tools/convert_to_gguf.py e tools/quantize_q8.py
Checkpoint Step 500 — shard_aa.txt
Metrica
Valore
Step
500 / 8000 (shard 1)
Train loss
4.2399
Val loss
4.3260
Tempo da avvio
13m 58s
Generazione campione
"come l'imperia che ha dato vita ad una vera e propria famiglia. L'imperia è sempre una sorta di 'pacchetto', dove si p[...]"
Checkpoint Step 1000 — shard_aa.txt
Metrica
Valore
Step
1000 / 8000 (shard 1)
Train loss
4.1928
Val loss
4.2557 ▼ −0.070 da step 500
Tempo da avvio
41m 28s
Generazione campione
"la cui costruzione risale al 1700 e ne conserva l'intero campanile; a destra del portale è presente un'unica navata e"
La generazione a step 1000 è già enciclopedica e grammaticalmente corretta. Il testo descrive un edificio storico con lessico architettonico specifico: segnale che il modello sta assorbendo il registro di CulturaX, diverso da Wikipedia.
Checkpoint Step 2500 — shard_aa.txt
Metrica
Valore
Step
2500 / 8000 (shard 1, 31%)
Train loss
4.1206
Val loss
4.1491 ▼ −0.107 da step 1000
Tempo da avvio
94m 16s (~2.1s/step)
Generazione campione
"e la loro identità sono la stessa: il nome di un luogo che non ha bisogno di farla si è creato in base a un altro punt[...]"
Val loss in discesa costante: 4.456 → 4.326 → 4.256 → 4.149 ogni 500–1000 step. La discesa è più rapida di quanto atteso su corpus nuovo, segno che CulturaX condivide abbastanza distribuzione con Wikipedia per permettere transfer efficace. Velocità effettiva: 2.1s/step → ETA shard 1: ~4.6 ore, totale 7 shard: ~32 ore.
Checkpoint Step 3500 — shard_aa.txt
Metrica
Valore
Step
3500 / 8000 (shard 1, 44%)
Train loss
4.0436
Val loss
4.0916 ▼ −0.058 da step 2500
Tempo da avvio
126m 02s (~2.1s/step)
Generazione campione
"di via A.F. 09/03/2017 (T.F. 10/03/2017), n. 131 del 18/03/2017 (T.F. 10/03/2017). Il sindaco Massimo D'Azeglio è il Pre[...]"
Val loss sotto 4.1 per la prima volta. La generazione mostra testo amministrativo ufficiale con date in formato italiano ("n. 131 del 18/03/2017") — registro completamente assente in Wikipedia, segnale che il modello sta assorbendo la varietà di CulturaX. Il gap train/val (4.04/4.09 = 0.05) è stretto: nessun overfitting. Curva val loss shard 1: 4.456 → 4.326 → 4.256 → 4.149 → 4.092.
Checkpoint Step 4000 — shard_aa.txt ★ sotto Run #3
Metrica
Valore
Step
4000 / 8000 (shard 1, 50%)
Train loss
4.0104
Val loss
4.0138 ▼ −0.078 da step 3500 — sotto Run #3 (4.0768)
Gap train/val
0.003 (nessun overfitting)
Tempo da avvio
142m 06s (~2.13s/step)
Generazione campione
"e il nome di 'Vajia'. La città è un centro polifunzionale per le attività economiche, sociali e sociali e nel rispetto d[...]"
Milestone: val loss 4.0138 — già sotto il best di Run #3 (4.0768), a metà del primo shard su sette. Il gap train/val di 0.003 è quasi nullo: il modello sta generalizzando bene su corpus mai visto. Curva completa shard 1: 4.456 → 4.326 → 4.256 → 4.149 → 4.092 → 4.014.
Checkpoint Step 4500 — shard_aa.txt ★ val loss < 4.0
Metrica
Valore
Step
4500 / 8000 (shard 1, 56%)
Train loss
3.9650
Val loss
4.0010 ▼ −0.013 da step 4000
Tempo da avvio
160m 46s (~2.14s/step)
Generazione campione
"per la produzione di energia elettrica. La tecnologia è utilizzata in ogni settore, ma è più facile da applicare. Per[...]"
Val loss sotto 4.0 per la prima volta — soglia simbolica, ma significativa: con Run #3 ci vollero 30.000 step su Wikipedia per arrivare a 4.08, qui si supera già a 4.500 step su un shard nuovo. Train loss a 3.965 (anche questa sotto 4.0). La curva si sta appiattendo leggermente (delta −0.013 vs −0.078 dello step precedente), atteso: siamo nella fase cosine decay dove il LR scende da 1.4×10⁻⁴ verso zero.
Checkpoint Step 5000 — shard_aa.txt
Metrica
Valore
Step
5000 / 8000 (shard 1, 62%)
Train loss
3.9376
Val loss
3.9416 ▼ sotto 4.0 per la prima volta
Tempo da avvio
176m 31s
Checkpoint Step 5500 — shard_aa.txt plateau
Metrica
Valore
Step
5500 / 8000 (shard 1, 69%)
Train loss
3.8826
Val loss
3.9419 ≈ 0 da step 5000 — plateau
Tempo da avvio
195m 56s
LR
8.92×10⁻⁵ — coda cosine decay
Val loss ferma a 3.942 tra step 5000 e 5500: plateau atteso. Il LR cosine è sceso da 3×10⁻⁴ (step 0) a 8.9×10⁻⁵ (step 5500) — con un tasso così basso gli aggiornamenti sono trascurabili. Il plateau conferma che shard_aa ha estratto quasi tutto ciò che poteva dare. La discesa riprenderà su shard_ab con il re-warmup (100 step) su dati freschi.
Curva completa val loss — shard_aa.txt
Step
Train loss
Val loss
Δ val
0
4.214
4.456
—
1000
4.193
4.256
−0.200
1500
4.166
4.218
−0.038
2000
4.124
4.169
−0.049
2500
4.121
4.149
−0.020
3000
4.071
4.119
−0.030
3500
4.044
4.092
−0.027
4000
4.010
4.014
−0.078
4500
3.965
4.001
−0.013
5000
3.938
3.942
−0.059
5500
3.883
3.942
≈ 0
Run #5 — profilo utilizzo risorse (2026-10-04)
Dati rilevati durante il training shard_aa a step ~4700, su Mac M2 8 GB con batch=8, seq=512, MPS backend.
Risorsa
Valore
Note
MPS footprint
~9.25 GB (virtuale)
pesi fp32 175 MB + AdamW 2 momenti 350 MB + attivazioni + gradient buffers. Supera la RAM fisica: il kernel gestisce con compressione
CPU RAM (RSS)
40 MB
solo interprete Python e data loader — tutto il heavy lifting è su MPS
RAM sistema
7.52 GB / 8 GB = 91.8%
wired 5.76 GB + compressor 952 MB + resto
Libera
81 MB
quasi zero; macOS gestisce con compressione aggressiva
CPU istantaneo
42–87% (1 core)
alterna compute MPS (asincrono) e step Python (sincrono)
CPU accumulato
81m 52s su 161m wall = 51%
l'altra metà: MPS pipeline asincrona e attesa risultati
Swap lifetime
24M pagine in/out
swapping avvenuto durante picchi di allocazione
Velocità
~2.1s/step
vs 0.15s/step RTX 4090 = 14× più lento
Perché è lento e perché funziona comunque: su Apple Silicon, GPU (MPS) e CPU condividono lo stesso pool da 8 GB — non c'è VRAM separata come su GPU desktop. Il modello fp32 da 45.9M parametri richiede con batch=32 abbondantemente più di 9 GB → OOM. Con batch=8 le attivazioni per step calano del 75% e il sistema rimane stabile, al costo di 14× più tempo per shard. Vantaggio: zero costi infrastrutturali per un training di ricerca che può girare overnight.
Batch size e token per step: batch=8 × seq=512 = 4.096 token/step. Batch=32 (usato su RTX 4090) = 16.384 token/step. A parità di step, Run #5 su M2 processa 4× meno token — il token budget totale di Run #5 è ~229M, il 25% del target Chinchilla di 918M. Il budget residuo verrà completato da Run #6 su GPU, partendo dal checkpoint di Run #5 (continued pre-training).
Il plateau di step 5000–5500 si è rotto leggermente: val loss 3.9193 vs 3.9419, differenza −0.023. A LR 7×10⁻⁵ gli aggiornamenti sono minimi, ma il dato è coerente: la curva cosine non è ancora a zero, c'è ancora margine di apprendimento negli ultimi 500 step.
Ripetizione di sillabe — output fuori distribuzione. La val loss 3.9193 rimane il segnale affidabile: è calcolata con teacher forcing su dati held-out, non dipende dalla generazione autoregressiva. La degeneration durante il sampling avviene a temperature/LR molto bassi quando il modello è in deep cosine tail. Non indica degrado dei pesi — lo stesso comportamento è osservabile in qualsiasi modello piccolo senza repeat penalty.
−0.044 in 500 step è il delta più grande osservato nella coda cosine (step 5000–6500). Inatteso a LR 5×10⁻⁵ — suggerisce che il modello stava ancora estraendo informazione reale dal shard, non solo rumore. Val loss 3.8753 è ora sotto la media Run #4 step finale (3.604) ma sopra il suo best (3.5861), su un solo shard vs 30k step su GPU. La proiezione sulla fine del shard (step 8000, ~70 min) è ottimistica.
Generazione step 6500 — recupero dalla degenerazione di step 6000:'ostro. Il tuo nome deriva dal nome di un paese del mondo. La parola "taglietto" deriva dal fatto che il nome del paese d'
Testo coerente, registro enciclopedico, struttura narrativa sull'etimologia. Il campione di step 6000 (ripetizione di sillabe) era probabilmente un artefatto del sampling su una sequenza-seme sfortunata, non un segnale di degrado dei pesi.
Checkpoint Step 7000 — shard_aa.txt
Metrica
Valore
Step
7000 / 8000 (shard 1, 87.5%)
Train loss
3.8317
Val loss
3.8515 ▼ −0.024 da step 6500
Tempo da avvio
250m 56s (4h 10m)
LR
4.05×10⁻⁵
Step corrente
7200 — 800 step al termine del shard (~28 min)
La curva continua a scendere nella coda cosine profonda: −0.024 in 500 step a LR 4×10⁻⁵. La discesa è rallentata rispetto al delta di step 6500 (−0.044) ma non si è fermata. Se il ritmo tiene, step 8000 potrebbe attestarsi intorno a 3.83–3.84.
Generazione step 7000 — loop di ripetizione sull'apertura:'_602 - Puntando su "Puntando su "Puntando su "Puntando su "Puntare su "Puntare su "Puntando su '
Stesso pattern di step 6000: sequenza-seme inusuale (`_602 -`) innesca un loop autoregressivo. Non indica degrado — confermato dal proseguimento della discesa val loss.
Checkpoint Step 7500 — shard_aa.txt
Metrica
Valore
Step
7500 / 8000 (shard 1, 94%)
Train loss
3.8229
Val loss
3.8759 ▲ +0.024 da step 7000
Tempo da avvio
265m 39s
LR
3.27×10⁻⁵
Lieve risalita rispetto al minimo di step 7000 (3.8515 → 3.8759). A LR così basso (3.27×10⁻⁵) gli aggiornamenti sono quasi irrilevanti — il piccolo aumento è probabile rumore statistico sul val set, non degrado. Generazione coerente: '. Si tratta di un progetto di valorizzazione del patrimonio culturale italiano, che ha come obiettivo la partecipazione '
Step 8000 — Fine shard 1 ✓ Shard completato
Metrica
Valore
Step
8000 / 8000 (shard 1, 100%)
Train loss
3.7879 — minimo assoluto di shard 1
Val loss
3.8665
Durata shard 1
280m 15s (4h 40m 15s)
Best val loss complessivo
3.5861 (Run #4 — shard 1 non ha battuto)
Shard 1 completato. Val loss finale 3.8665 — migliore di Run #3 (4.077) ma ancora sopra Run #4 (3.586). Il tracker best.pt non è stato aggiornato: Run #5 shard 1 non ha mai sceso sotto 3.5861. Il best checkpoint rimane quello di Run #4. Il miglioramento atteso verrà dai shard 2–7: ogni shard porta dati freschi + re-warmup del LR, che permette discese più ripide rispetto alla coda cosine di shard 1.
Generazione step 8000:". Non è un caso che la situazione abbia un impatto positivo su questa situazione. Non c'è un rischio e la sicurezza di c" — italiano corretto ma circolare ("situazione" x2). Coerente con l'ultimo step di un cosine cycle: il modello ha "consolidato" il shard ma non ha visto ancora il corpus più ampio dei shard successivi.
Curva val loss completa — shard_aa.txt
Step
Train
Val
Δ val
0
4.214
4.456
—
1000
4.193
4.256
−0.200
2000
4.124
4.169
−0.087
3000
4.071
4.119
−0.050
4000
4.010
4.014
−0.105
4500
3.965
4.001
−0.013
5000
3.938
3.942
−0.059
5500
3.883
3.942
≈ 0
6000
3.877
3.919
−0.023
6500
3.848
3.875
−0.044
7000
3.832
3.852
−0.023 ← min shard
7500
3.823
3.876
+0.024
8000
3.788
3.867
−0.009
Run #5 — Shard 2/7 (shard_ab.txt) — inizio 2026-10-04 02:51
Parametro
Valore
File
shard_ab.txt (1.18 GB)
Token totali
289.5M → 260.6M train / 29M val
Tokenizzazione
878s (14m 38s) — cache salvata in .pt
Checkpoint caricato
best.pt — step 27500, val 3.5861 (Run #4)
Re-warmup
100 step (LR: 1.5×10⁻⁴ → 3×10⁻⁴)
La val loss di shard 2 riparte da 4.46 a step 0 — non è una regressione. Il val set di shard_ab è completamente diverso da quello di shard_aa: il modello viene valutato su testo che non ha mai visto, distribuito diversamente. Il valore iniziale riflette la "sorpresa" del modello di fronte a nuova distribuzione. La discesa rapida a step 500 (4.46 → 4.31, −0.15 in 500 step) indica che il modello si adatta velocemente ai dati freschi.
Checkpoint Step 500 — shard_ab.txt
Metrica
Valore
Step
500 / 8000 (shard 2, 6%)
Train loss
4.3048
Val loss
4.3092
Tempo da avvio shard 2
13m 21s
LR
2.98×10⁻⁴ (post-warmup, peak)
Generazione step 500: "di altri paesi in cui l'energia di vita è abbastanza pesante. È una delle migliori fonti di energia. È una delle fonti p" — testo sull'energia, coerente ma con ripetizione ("È una delle" ×2). Normale a 500 step con LR al massimo: il modello sta ancora "adattando la velocità" ai nuovi dati.
Checkpoint Step 7500 — shard_ab.txt
Metrica
Valore
Step
7500 / 8000 (shard 2, 94%)
Train loss
3.8137
Val loss
3.8476 ▼ −0.017 recupero dal micro-uptick
Tempo da avvio shard 2
231m 09s (3h 51m)
LR
3.27×10⁻⁵
Recupero dal micro-uptick di step 7000 (3.865 → 3.848). Generazione: "e un'azienda che ha la stessa storia in cui si trova. Ma non è altro che una scelta. Perché se l'azienda non ha la capac" — italiano narrativo-aziendale, struttura concessiva corretta.
Step 8000 — Fine shard 2 ✓ Miglior shard finora
Metrica
Valore
Step
8000 / 8000 (shard 2, 100%)
Train loss
3.8387
Val loss
3.8344▼▼ nuovo minimo assoluto — meglio di shard 1 (3.867)
Durata shard 2
245m 53s (4h 05m 53s)
Best val loss complessivo
3.5861 (Run #4 — ancora imbattuto)
Shard 2 finisce a val 3.834 — significativamente meglio della fine di shard 1 (3.867) e del minimo di shard 1 (3.852). Il continued pre-training produce un miglioramento shard su shard misurabile: ogni ciclo cosine porta i pesi in una posizione leggermente migliore di dove si trovavano dopo il ciclo precedente. Generazione step 8000: "a causa dell'insufficienza di questo genere, la perdita di peso non è in gran parte superiore a quella di altri tipi di" — italiano medico coerente.
Confronto finale shard 1 vs shard 2
Metrica
Shard 1
Shard 2
Val loss iniziale (step 0)
4.456
4.462
Val loss a step 4000
4.014
3.981
Minimo assoluto
3.852 (step 7000)
3.834 (step 8000)
Val loss finale (step 8000)
3.867
3.834
Durata
280m 15s
245m 53s (−34 min)
Shard 2 è stato ~34 minuti più veloce di shard 1 (cache .pt già presente, nessun ricalcolo tokenizzazione). La val loss finale è di 0.033 punti migliore. Tendenza confermata: ogni shard migliora il precedente.
Run #5 — Shard 3/7 (shard_ac.txt) — inizio 2026-10-04 07:11
−0.054 da step 500; lessico inventato ma sintassi corretta
1500
4.1798
4.2032
52:33
2.80×10⁻⁴
−0.058; gen. ⁇ UNK (artefatto seed, non degrado)
2000
4.1690
4.1768
67:50
2.63×10⁻⁴
−0.026; rallentamento atteso — ancora in esplorazione larga (LR alta)
2500
4.1314
4.1538
83:36
2.43×10⁻⁴
−0.023; gen. coerente: "delle più belle città d'Italia" ✓
3000
4.0760
4.1122
101:49
2.20×10⁻⁴
−0.042; discesa riprende; gen. "ALLE BUTTA" (artefatto seed corpus)
3500
4.0563
4.0907
120:08
1.94×10⁻⁴
−0.022; sotto 4.1; gen. all-caps CHIRE/REMITO (artefatto seed)
4000
4.0013
4.0208
138:24
1.68×10⁻⁴
−0.070 ← accelerazione; LR entra in zona di convergenza rapida
4500
3.9834
4.0010
154:56
1.41×10⁻⁴
−0.020; val ≈ 4.0 (soglia raggiunta); gen. "Idraulica" ripetuta (artefatto)
5000
3.9464
3.9663
172:40
1.15×10⁻⁴
−0.035; sotto 4.0; gen. filosofica italiana: "teoria di genere... il corpo e l'essere" ✓✓
5500
3.8940
3.9524
190:52
9.14×10⁻⁵
−0.014; LR sotto 10⁻⁴ — deep cosine tail; gen. educativa corretta
6000
3.8972
3.9055
208:02
7.05×10⁻⁵
−0.047; discesa riprende; gen. istituzionale corretta
6500
3.8380
3.8830
226:03
5.48×10⁻⁵
−0.022; gen. giornalistica: "come da tradizione, ha preso parte alla manifestazione" ✓✓
7000
3.8209
3.8439
242:52
4.05×10⁻⁵
−0.039; sotto 3.85; gen. letteraria con congiuntivo: "si sarebbe lasciato andare via" ✓✓✓
7500
3.8339
3.8210
260:02
3.27×10⁻⁵
−0.023; minimo shard 3 (3.821); gen. narrativa ✓✓✓
8000
3.8451
3.8453
275:34
1.50×10⁻⁵
+0.024 micro-uptick da step 7500 (tipico a LR→min); fine shard 3
Riepilogo shard 3 e confronto shard 1–3
Metrica
Shard 1
Shard 2
Shard 3
Val loss iniziale
4.456
4.462
4.444
Val loss minimo
3.852 (step 7000)
3.834 (step 8000)
3.821 (step 7500)
Val loss finale (step 8000)
3.867
3.834
3.845 (+0.024 micro-uptick)
Durata
280m 15s
245m 53s
275m 45s
Shard 3 ha il minimo assoluto più basso di Run #5 finora (3.821 a step 7500), ma un micro-uptick finale porta la val 8000 a 3.845 — tra shard 1 e shard 2. Il micro-uptick è atteso quando LR→min con varianza batch residua. Il checkpoint best.pt registra ancora Run #4 (3.5861). Generazione step 8000: "Il tema è un'opera che ha visto la partecipazione di numerosi artisti e professionisti, con un'accoglienza di appassio" — italiano culturale coerente.
Run #5 — Shard 4/7 (shard_ad.txt) — inizio 2026-10-04 12:02
Shard 4 avviato alle 12:02:36. Corpus: shard_ad.txt (1.18 GB). Tokenizzazione completata: 290,085,185 token totali, train 261M / val 29M. Cache: shard_ad.txt.bpe-spm.v16000.tokens.pt (553 MB). Ripreso da step 27500, val_loss=3.5861 (Run #4 — confermato: tutti gli shard ripartono dallo stesso checkpoint Run #4). Re-warmup: 100 step.
Nota meccanismo: ogni shard riparte da best.pt (Run #4, step 27500, val 3.5861). best.pt non viene aggiornato perché nessuno shard ha superato 3.5861. Il miglioramento shard-su-shard nel minimo di val loss (3.852 → 3.834 → 3.821) è quindi legato a differenze nel corpus (difficoltà, distribuzione linguistica), non ad accumulo di pesi tra shard.
Tabella checkpoint — shard_ad.txt
Step
Train loss
Val loss
Tempo (mm:ss)
LR
Note
0
4.4522
4.4721
01:07
1.50×10⁻⁴ (re-warmup)
Avvio shard 4
500
4.3123
4.2846
14:24
2.98×10⁻⁴
−0.187; gen. meta-ripetitiva "disponibilità" (LR alta)
1000
4.2451
4.2416
33:20
2.91×10⁻⁴
−0.043; gen. coerente: "coniugi... insoddisfazione... possibilità" ✓
1500
4.1879
4.2101
52:13
2.80×10⁻⁴
−0.032; gen. medico-legale: "violazione del trattamento"
2000
4.1660
4.1583
74:14
2.63×10⁻⁴
−0.052; gen. cirillico in apertura + italiano EU coerente — artefatto seed
2500
4.1238
4.1507
91:48
2.43×10⁻⁴
−0.008; curva si appiattisce (atteso); gen. italiana corretta "qualità di vita" ✓
3000
4.0670
4.0961
108:51
2.20×10⁻⁴
−0.055; drop dopo mini-plateau; gen. news/politica IT coerente ✓
3500
4.0509
4.0621
166:33
1.94×10⁻⁴
−0.034; ⚠️ rallentamento per swap — velocità ripresa a step 4000
4000
4.0030
3.9965
191:14
1.68×10⁻⁴
−0.065; 🎯 primo attraversamento sotto 4.0 · gen. commerciale IT ✓
4500
3.9615
4.0091
220:59
1.41×10⁻⁴
+0.013 mini-uptick (normale); gen. commerciale IT; velocità ~3.5 s/step ripristinata
5000
3.9379
3.9558
251:21
~1.16×10⁻⁴
−0.053; nuovo minimo shard 4 · cosine LR in discesa accelerata
5500
3.9148
3.9196
278:42
~9.1×10⁻⁵
−0.036; nuovo minimo · velocità ~2.6 s/step (pressione mem. normalizzata)
6000
3.8810
3.8961
308:15
7.05×10⁻⁵
−0.023; primo attraversamento sotto 3.9 · gen. turismo IT ✓ · ~2.4 s/step
Step 500: generazione "in questo periodo di un'attività di ricerca di personale o di attività di ricerca e di ricerca di attività di ricerca e " — loop ripetitivo tipico con LR alta (2.98×10⁻⁴). Non è un segnale di degrado: il modello è in fase di esplorazione aggressiva dello spazio dei pesi.
Checkpoint Step 7000 — shard_ab.txt
Metrica
Valore
Step
7000 / 8000 (shard 2, 87.5%)
Train loss
3.8363
Val loss
3.8645 ▲ +0.007 micro-uptick da step 6500
Tempo da avvio shard 2
217m 05s (3h 37m)
LR
4.05×10⁻⁵
Step corrente
7450 — ~550 step alla fine del shard (~19 min)
Micro-uptick +0.007 identico al pattern di shard 1 (che aveva +0.024 a step 7500). Entrambi i shard mostrano questa oscillazione nella coda cosine profonda — coerente con la varianza del val set a LR minimo. Generazione: timing sportivi in formato numerico ('ia (TM) 1:56.49,58,58,86...') — il corpus contiene tabelle di risultati atletici.
3.8574▼ nuovo minimo · shard 1 step 6500: 3.875 (−0.018)
Tempo da avvio shard 2
200m 49s (3h 20m)
LR
5.33×10⁻⁵
Shard 2 step 6500: 3.857 vs shard 1 step 6500: 3.875. Gap −0.018, stabile. La proiezione al termine del shard (step 8000) è intorno a 3.79–3.80, sensibilmente meglio della fine di shard 1 (3.867). Generazione: ". L'obiettivo di questi ultimi anni è stato quello di offrire a questi studenti un'esperienza completa e di un'esperienz" — italiano educativo coerente. Leggera ripetizione di "un'esperienz" alla fine: limite di contesto, non degrado.
Checkpoint Step 6000 — shard_ab.txt ▼ 3.895 — gap crescente vs shard 1
Metrica
Valore
Step
6000 / 8000 (shard 2, 75%)
Train loss
3.9003
Val loss
3.8952▼ nuovo minimo shard 2 · shard 1 a stesso step: 3.919 (−0.024)
Tempo da avvio shard 2
185m 08s (3h 05m)
LR
7.05×10⁻⁵
Shard 2 a step 6000: val 3.895 vs shard 1 step 6000: val 3.919. Differenza −0.024 a favore di shard 2 — il divario cresce con l'avanzare del training. Il continued pre-training produce un effetto misurabile: ogni shard parte da pesi leggermente migliori. Se il vantaggio si mantiene, shard 2 finirà il suo ciclo con una val loss inferiore a shard 1. Generazione: URL in loop ('www.singleww.singleww...') — artefatto web nel corpus, non degrado.
Shard 2 a step 5500 (3.929) vs shard 1 a step 5500 (3.942): differenza +0.013. La curva di shard 2 non mostra il plateau che shard 1 aveva tra 5000 e 5500 — scende più uniformemente. Generazione: 'e il suo futuro, grazie all'impegno della Fondazione, in collaborazione con i più importanti produttori di beni e serviz' — italiano istituzionale coerente.
Checkpoint Step 5000 — shard_ab.txt
Metrica
Valore
Step
5000 / 8000 (shard 2, 62.5%)
Train loss
3.9486
Val loss
3.9591 ▼ recupero — sotto 3.96
Tempo da avvio shard 2
154m 27s (2h 34m)
LR
1.15×10⁻⁴
Val loss recuperata dopo il micro-uptick di step 4500. Shard 1 a step 5000: 3.9416 — shard 2: 3.9591, differenza 0.018 spiegata dal val set diverso. Generazione: "L'ultimo appuntamento è stato presentato con una proposta di legge che, a partire dagli anni '90, è stata promossa dal" — italiano istituzionale/legislativo, struttura subordinata corretta, senza ripetizioni. La qualità narrativa continua a migliorare con la discesa della loss.
Checkpoint Step 4500 — shard_ab.txt
Metrica
Valore
Step
4500 / 8000 (shard 2, 56%)
Train loss
3.9873
Val loss
3.9893 ▲ +0.009 da step 4000 — micro-uptick
Tempo da avvio shard 2
135m 42s (2h 15m)
LR
1.41×10⁻⁴
Micro-risalita di +0.009 — identica a quella osservata in shard 1 intorno a step 4500. Rumore statistico del val set, non regressione. Generazione: sequenza numerica '4420-11-12-12-09-1910-11-22-22...' — il seed ha pescato una sezione tabellare/codici dal corpus. Non indica degrado.
Checkpoint Step 4000 — shard_ab.txt ▼▼ sotto 4.0 — 1000 step prima di shard 1
Metrica
Valore
Step
4000 / 8000 (shard 2, 50%)
Train loss
3.9811
Val loss
3.9807▼▼ sotto 4.0 — primo shard a raggiungerlo a step 4000
Tempo da avvio shard 2
120m 37s (2h 00m)
LR
1.68×10⁻⁴
Shard_ab ha attraversato la soglia 4.0 a step 4000 — mille step prima di shard_aa (che l'aveva raggiunta a step 5000). Confronto diretto: shard 1 step 4000 → val 4.014; shard 2 step 4000 → val 3.981, differenza −0.033. La discesa più rapida può indicare che shard_ab contiene testo con maggiore varietà sintattica, oppure che i pesi di shard_aa hanno già pre-adattato il modello alla distribuzione CulturaX, abbassando la soglia di sorpresa sui dati nuovi.
Generazione step 4000: 'enza, con un contributo di 79mila euro all'anno. La crisi finanziaria ha comportato una "disatura" di 10 miliardi di eur' — italiano giornalistico-finanziario coerente. "Disatura" potrebbe essere un errore del corpus originale, ma la struttura è corretta.
Checkpoint Step 3500 — shard_ab.txt sotto 4.05
Metrica
Valore
Step
3500 / 8000 (shard 2, 44%)
Train loss
4.0576
Val loss
4.0484 ▼ sotto 4.05 — migliore di shard 1 a parità di step
Tempo da avvio shard 2
105m 17s (1h 45m)
LR
1.94×10⁻⁴
Shard 2 a step 3500 (val 4.048) vs shard 1 a step 3500 (val 4.092): +0.044 punti di vantaggio. Entrambi partono dal medesimo checkpoint Run #4, ma il val set diverso e la varianza dei dati spiegano la differenza. La proiezione verso step 5000 è ottimistica: se la curva tiene, shard 2 potrebbe scendere sotto 4.0 prima di step 5000. Generazione: ', che non si può, in quanto non si può, ma può, infatti, essere, una parte, di una serie di oggetti, che si manifesta in' — subordinate annidate grammaticalmente corrette, semanticamente circolari.
Checkpoint Step 3000 — shard_ab.txt
Metrica
Valore
Step
3000 / 8000 (shard 2, 37.5%)
Train loss
4.0764
Val loss
4.1387 ▼ −0.009 da step 2500
Tempo da avvio shard 2
91m 25s
LR
2.20×10⁻⁴
Generazione step 3000: catalogo di abbreviazioni ('- L'ultima nota della "Cav.ssa Saverio" - A.A. - A.A.P. - C.S.A. …') — artefatto di una sezione bibliografica/archivistica nel corpus. Il modello riproduce la struttura sintattica del formato, non il contenuto.
Checkpoint Step 2000 — shard_ab.txt
Metrica
Valore
Step
2000 / 8000 (shard 2, 25%)
Train loss
4.1662
Val loss
4.1952 ▼ −0.024 da step 1500
Tempo da avvio shard 2
62m 37s
LR
2.63×10⁻⁴
Generazione step 2000: 'della vita di un autore che, invece, è molto più grande, anche perché, di fronte al successo e all'eleganza, la musica h' — italiano letterario con struttura complessa (avversativa + causale). Registro narrativo, nessuna ripetizione.
Checkpoint Step 2500 — shard_ab.txt
Metrica
Valore
Step
2500 / 8000 (shard 2, 31%)
Train loss
4.1285
Val loss
4.1473 ▼ −0.048 da step 2000
Tempo da avvio shard 2
77m 03s
LR
2.43×10⁻⁴
Generazione step 2500: '. La storia di una famiglia. Per fortuna è riuscita a farci conoscere. In un mondo che oggi non è facile, che non ci si ' — testo narrativo coerente con relative annidate corrette ("un mondo che oggi non è facile, che non ci si"). Qualità migliore rispetto ai campioni precedenti: subordinate formate correttamente, registro colloquiale maturo.
Checkpoint Step 1500 — shard_ab.txt
Metrica
Valore
Step
1500 / 8000 (shard 2, 19%)
Train loss
4.2235
Val loss
4.2189 ▼ −0.072 da step 1000
Tempo da avvio shard 2
45m 36s
LR
2.80×10⁻⁴ — cosine ha iniziato a calare
Accelerazione della discesa rispetto a step 500→1000 (−0.018): ora −0.072 in 500 step. Train loss e val loss quasi identiche (4.224 vs 4.219) — assenza di overfitting. La cosine decay ha preso effetto. Generazione step 1500: all-caps pseudo-italiana ('IL CONNO DELLA GLIDI... CONFRANSI DI ROTALE IN CONFRANSI') — artefatto del seed uppercase da corpus, non degrado dei pesi.
Checkpoint Step 1000 — shard_ab.txt
Metrica
Valore
Step
1000 / 8000 (shard 2, 12.5%)
Train loss
4.2526
Val loss
4.2911
Tempo da avvio shard 2
28m 51s
LR
2.91×10⁻⁴ — cosine inizia appena ora
Discesa da step 500 a 1000: −0.018 (4.309 → 4.291). Ritmo lento atteso: il LR è ancora al 97% del picco, la cosine decay è appena partita. Confronto con shard 1 a step 1000: val 4.256 — shard 2 parte leggermente più alto perché il val set è diverso, non perché il modello regredisca. Generazione: 'per la sua qualità di amica. È possibile accedere alle pagine dei vari corsi online in cui sono presenti e sono presenti' — coerente ma con ripetizione ("sono presenti" ×2).
Run #5 — Shard 4/7 completato (shard_ad.txt)
Shard 4 completato. Val loss finale 3.8227 — minimo assoluto di Run #5 finora. Rispetto a shard 3 (3.845): −0.022. Il meccanismo best.pt non è stato corretto: anche shard 4 è ripartito da Run #4. L'esplorazione parallela continua.
Step
Train
Val
Tempo
Note
0
4.4522
4.4721
01:07
Avvio shard 4
500
4.3123
4.2846
14:24
1000
4.2451
4.2416
33:20
1500
4.1879
4.2101
52:13
2000
4.1660
4.1583
74:14
2500
4.1238
4.1507
91:48
3000
4.0670
4.0961
108:51
3500
4.0509
4.0621
166:33
⚠️ rallentamento swap
4000
4.0030
3.9965
191:14
🎯 primo attraversamento sotto 4.0
4500
3.9615
4.0091
220:59
mini-uptick
5000
3.9379
3.9558
251:21
nuovo minimo shard 4
5500
3.9148
3.9196
278:42
6000
3.8810
3.8961
308:15
primo attraversamento sotto 3.9
6500
3.8433
3.8747
354:26
⚠️ rallentamento swap (57 min per 500 step)
7000
3.8775
3.8724
398:30
7500
3.8343
3.8570
415:47
8000
3.8091
3.8227
434:00
✅ minimo finale shard 4
Run #5 — Shard 5/7 (shard_ae.txt) — esplorazione parallela
Shard 5 avviato automaticamente dallo script dopo il completamento di shard 4. Corpus: shard_ae.txt (1.18 GB). Token totali: 289,840,429. Cache shard_ae.txt.bpe-spm.v16000.tokens.pt (553 MB). Ripartito da Run #4 (meccanismo bug ancora attivo). Generazione step 8000: ", per la quale la scelta di un'immagine di questo libro è stata una delle più importanti del panorama italiano."
Step
Train
Val
Tempo
0
4.4512
4.4567
01:07
500
4.2917
4.2946
16:48
1000
4.2502
4.2723
40:18
1500
4.2106
4.2110
61:14
2000
4.1465
4.1661
82:00
2500
4.1319
4.1208
104:20
3000
4.0751
4.0821
123:07
3500
4.0733
4.0745
146:49
4000
4.0018
4.0197
165:33
4500
3.9760
4.0184
185:39
5000
3.9690
3.9299
203:39
5500
3.8839
3.9277
222:58
6000
3.8449
3.9043
244:08
6500
3.8519
3.8506
262:55
7000
3.8575
3.8529
281:06
7500
3.8548
3.8350
298:44
8000
3.8262
3.8215
315:28
Run #5 — Shard 6/7 (shard_af.txt) — esplorazione parallela
Shard 6 avviato automaticamente. Corpus: shard_af.txt (1.18 GB). Ripartito da Run #4. Val loss finale: 3.8344. Generazione step 8000: "in modo da poter contare su un'ampia gamma di prodotti, di qualità e di servizio."
Step
Train
Val
Tempo
0
4.4346
4.4621
01:07
500
4.3151
4.3133
15:40
1000
4.2597
4.2598
34:41
1500
4.1937
4.2231
51:30
2000
4.1576
4.1717
68:12
2500
4.1137
4.1281
85:44
3000
4.0880
4.1060
103:12
3500
4.0434
4.0297
117:26
4000
4.0065
4.0413
131:23
4500
3.9513
3.9764
154:34
5000
3.9396
3.9669
172:11
5500
3.8849
3.9351
189:45
6000
3.8765
3.8926
208:28
6500
3.8681
3.8811
227:48
7000
3.8397
3.8766
247:39
7500
3.8406
3.8307
265:31
8000
3.8168
3.8344
285:08
Run #5 — Shard 7/7 (shard_ag.txt) — esplorazione parallela
Shard 7 avviato automaticamente. Corpus: shard_ag.txt (1.18 GB). Ripartito da Run #4. In corso al momento della scrittura (step 4000/8000). Generazione step 4000: "quindi che è possibile che non è possibile trovare una nuova idea a chi dovrebbe fare. E la situazione attuale è una del" — val 4.027, superiore agli altri shard a parità di step: corpus ag sembra il più difficile tra quelli osservati.
Step
Train
Val
Tempo
0
4.4778
4.4646
01:07
500
4.3062
4.3293
17:49
1000
4.2294
4.2543
36:44
1500
4.2189
4.2295
57:44
2000
4.1408
4.1732
77:13
2500
4.1248
4.1742
98:22
3000
4.0776
4.0823
117:43
3500
4.0357
4.0872
134:59
4000
4.0147
4.0271
154:21
Run #5 — Riepilogo esplorazione parallela
Tutti e 7 gli shard di Run #5 hanno operato come esperimenti indipendenti, ripartendo ogni volta dal checkpoint Run #4 (val 3.5861). Nessun shard ha mai aggiornato best.pt. La tabella riassume i val loss finali e la difficoltà relativa di ogni segmento del corpus.
Shard
File
Val loss finale
Note
1 / aa
shard_aa.txt
3.8665
Più difficile tra i primi tre
2 / ab
shard_ab.txt
3.845
3 / ac
shard_ac.txt
3.845
Minimo osservato — corpus più facile
4 / ad
shard_ad.txt
3.8227
Minimo assoluto Run #5
5 / ae
shard_ae.txt
3.8215
6 / af
shard_af.txt
3.8344
7 / ag
shard_ag.txt
in corso
Val 4.027 a step 4000 — corpus più difficile
Valore analitico: la variazione dei val loss finali (3.82–3.87) riflette la difficoltà relativa di ogni segmento del corpus. Shard ag (step 4000 val 4.027 vs ad/ae step 4000 val ~4.02–4.02) mostra partenza più alta, coerente con un corpus più variabile o con registri meno frequenti nel pretraining. Questo dato orienta l'ordine dei shard nel training sequenziale successivo (curriculum learning).
Decisione — Run #5 concluso, avvio training sequenziale su GPU (2026-10-05)
Stato finale Run #5: completati 6 shard su 7 in modalità esplorazione parallela (shard 7/ag in corso al momento di questa nota). Il meccanismo best.pt non è mai stato corretto durante Run #5: tutti gli shard ripartivano da Run #4 (best val 3.5861). Il last.pt di shard_ac è stato perso: sovrascritto automaticamente dallo script quando shard_ad è partito.
Decisione: il training sequenziale con meccanismo corretto avverrà su GPU remota (Vast.ai). Parametri del run:
Parametro
Valore
Note
Punto di partenza
checkpoints/run4_best.pt
Run #4, val 3.5861 — catena completa aa→ag
Meccanismo
cp last.pt → best.pt prima di ogni shard
Corregge il bug di Run #5
Sequenza shard
aa → ab → ac → ad → ae → af → ag
Ordine corpus — curriculum da valutare
Batch size
32
vs 8 su M2 — 4× token per step
Steps per shard
8.000
A batch=32: 8000 × 16384 = 131M token per shard (~94% copertura)
Valore dell'esplorazione parallela: Run #5 non è training perso. I dati di val loss per shard forniscono una mappa di difficoltà relativa del corpus — utile per ordinare gli shard in curriculum learning nel run sequenziale. Shard ac/ad/ae risultano i più "facili" (val finale più bassa), ag il più difficile. Questo può informare l'ordine di addestramento: da facile a difficile aumenta la stabilità iniziale, da difficile a facile può accelerare la convergenza finale.
Budget token cumulativo — Run #5 → Run #6 (2026-10-04)
Run
Batch
Step
Token
Cumulativo
% Chinchilla
Run #4
32
30.000
245M
245M
27%
Run #5 (M2)
8
56.000
229M
474M
52%
Run #6 (GPU, pianificato)
32
56.000
918M
1.147B
125%
Run #5 non è un training standalone che fallisce il target Chinchilla — è il primo segmento di un percorso in due fasi. I tre run si sommano perché ognuno parte con --resume dal checkpoint del precedente: i pesi accumulano gradiente su gradiente da testi progressivamente diversi. Al termine di Run #6 il budget sarà ~1.15B token, il 25% in eccesso rispetto al target Chinchilla per 46M parametri.
Perché batch=8 non invalida Run #5: il batch size influenza il rumore dei gradienti e la velocità, non la direzione di convergenza. Un singolo update con batch=8 è meno stabile di uno con batch=32, ma su migliaia di step la media converge allo stesso punto. La val loss 3.942 a step 5000 (già sotto il best di Run #3 su Wikipedia) lo conferma empiricamente. Run #5 produce un checkpoint valido e utile — Run #6 non lo ri-addestra, lo estende.
Archivio completo fino a Run #5.
Il diario prosegue in slm-journal.html — Run #6 (sweet spot run6-ad), confronto Minerva, strategia Run #7 e Run #8.