SLM · Diario di Bordo · Storico

Run #1 — Run #5: il percorso fino a Vast.ai

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-05 run coperti #1, #3, #4, #5 stato 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:

python3 train.py --tokenizer bpe-spm --data data/wiki_it.txt \ --preset medium --steps 30000 --batch 16 --seq-len 256

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".

Run #3 — Analisi approfondita

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
random completato (step 30 000) step 8k 10k⚠ 11k★ 10.0 9.0 8.0 7.0 6.0 5.0 4.0 val loss 0 5k 10k 15k 20k 25k 30k step val loss (checkpoint) step finale (30 000 ✓) baseline random (loss 9.68)

Checkpoint val loss

StepVal lossPerplexityΔ loss / 500 stepNote
09.825818 541—random, atteso log(16000) = 9.68
5007.51561 829−2.31frequenza token, frasi di funzione emergenti
1 0007.07771 187−0.44nomi comuni, prime coppie articolo + nome
1 5006.99321 088−0.08apostrofi ripetuti (wikitext markup)
2 0006.8173910−0.18
2 5006.6743793−0.14
3 0006.5763718−0.10prime strutture da enciclopedia
3 5006.4433629−0.13
4 0006.2337508−0.21prime frasi concettualmente coerenti
5 0006.0849440−0.12
6 0005.9704391−0.11struttura frasale stabile
7 0005.8267339−0.14
8 0005.7426311−0.08stile Wikipedia apprezzabile
9 0005.6512284−0.09
10 0005.5424255−0.11HTML entity leak (markup chimico residuo)
11 0005.4844241−0.06primo "comune italiano" corretto
11 5005.4584234−0.03train=5.2245 · gap train/val = 0.23
12 5005.3787216−0.08strutture architettoniche con anno e stile
13 0005.3842218+0.006plateau; train 5.1683, gap 0.216; testo "Cosenza in Piemonte" (struttura ok, geografia confusa)
13 5005.3125203−0.072plateau rotto; train 5.0688, gap 0.244; testo sportivo ("campionato europeo", "inaugurata")
14 0005.3052201−0.007train 5.0806, gap 0.225; registro culturale/storico ("opera", "anni cinquanta", clausole relative)
15 0005.2079183−0.097discesa riprende
16 0005.1063165−0.102
17 0005.0312153−0.075
18 0004.9704144−0.061"Valle del Vallese", "Repubblica di Venezia"
19 0004.8562129−0.114
20 0004.7966121−0.060nomi geografici reali; registro enciclopedico stabile
22 5004.6749107−0.121"feudo di Atella" con data 1431 e nomi corretti
24 0004.569797−0.105"alleanza con la Francia nazista" — coerenza storica emergente
25 5004.527793−0.042struttura bibliografica corretta, lessico culturale consolidato
26 5004.497890−0.030"Paesi Bassi", "monete" — lessico economico/istituzionale
27 0004.382180−0.116
28 0004.213467−0.169
28 5004.076859−0.137best checkpoint — governo comunista, Aia: riferimenti geopolitici precisi; miglior val del run
29 5004.083459+0.006oscillazione finale, best non migliorato
30 0004.082559−0.001step 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 500 solo le parole più frequenti, nessuna struttura
che il a è e, e che. Nel in...
step 1 000 prime coppie articolo+nome, nomi propri
di una città nel...paese del suo momento del mondo nel termin
step 1 500–2 000 apostrofi ripetuti — il modello impara la frequenza prima del significato anomalia 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 000 prime strutture enciclopediche, numeri storici
del centro secolo dell'agno di un paese di un fiume delepoca. Dal 186
step 4 000 prima 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 000 registro enciclopedico riconoscibile
a essere stata il primo progetto del primo e per la stessa parte. La produzione del suo caso
step 8 000 toponomastica italiana, stile Wikipedia ★
, che si trovava nella strada, in posizione dell'area di San Giovanni, la prima domenica del paese
step 10 000 tag HTML non rimossi — residuo di articoli di chimica anomalia
meth" (18gt;) { < <chem>(lt;</chem>
step 11 000 primo "comune italiano" corretto ★★
è un comune italiano di abitanti della metropolitana di Asti in Lombardia. Geografia fisica. Il territorio comunale comp
step 11 500 struttura geografica/amministrativa multi-clausola
ani di un altro di "Coms" (che non può essere una provincia di Bolzano, la provincia di Verona), una regione calcistica.
step 12 500 strutture architettoniche con anno e stile ★★★
di Cacerno. La chiesa fu costruito in stile romanico nel 1786 e successivamente ristrutturata in possesso dell'antico ca
step 13 000 struttura geografica/amministrativa corretta, fatti geografici sbagliati
della provincia di Cosenza in Piemonte, nel quale furono abolite con i comuni di Como in Italia per le Olimpiadi di Se
step 13 500 plateau rotto — vocabolario sportivo e da eventi ★★★★
Nel settembre del'Italia si tiene l'Azione della squadra. L'Amilia italiana è stata inaugurata nel campionato europeo
step 14 000 registro culturale/storico, clausole relative complesse ★★★★ val 5.31
, con la sua opera, la quale è stata fatta da essere riaperto. La maggior parte degli anni cinquanta è stata in corso di
step 20 000 nomi geografici reali, costruzioni subordinate ★★★★ val 4.80
dopo la nascita della Repubblica di Venezia. Le truppe italiane del Regno di Sardegna e del Regno d'Italia e dell'annessione
step 22 500 date e nomi storici corretti ★★★★ val 4.67
La 12 marzo 1431 il feudo di Atella è assegnato alla passaggio dei città, con l'aiuto di nuovi il borgo. Il paese è si
step 24 000 coerenza storico-politica, lessico istituzionale ★★★★★ val 4.57
ano, e con il suo contributo al potere, con l'eccezione di alcuni storici di diritto. L'alleanza con la Francia nazista.
step 26 500 lessico 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 — completato registro 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.

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.

Timing e proiezione

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:

Stima: ~50 righe di diff. Zero impatto sulla semantica del resto dell'architettura.

Cosa rimane invariato in Run #4

Piano di comparazione Run #3 vs Run #4

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.

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.

Script di comparazione

python3 tools/compare_runs.py \ --model-a checkpoints/run3_sinusoidal_best.pt \ --model-b checkpoints/run4_rope_best.pt \ --tokenizer bpe-spm \ --test-set data/wiki_it_val.txt \ --extrapolation-len 512
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

Dettaglio completo: Roadmap di sviluppo · Documentazione

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:

La perplexity come posizione sul continuum

ModelloPerplexityNota
Distribuzione uniforme su 16 K vocab16 000entropia massima
Run #3 — step 0~9 700dopo inizializzazione pesata
Run #3 — step 8 000~297registro acquisito
Run #3 — step 30 000 (attuale)59val loss 4.0768
GPT-2 Small (117 M, inglese)~29riferimento storico
Umano (stima)~15–20benchmark 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à:

FormatoPesoPP stimatoNota
float32 (attuale)553 MB59baseline
float16277 MB≈59.1quasi identico
int8 / Q8_0139 MB≈59.5trascurabile
Q4_K_M (target GGUF)~35 MB≈61–63deployment ottimale
Q2_K~18 MB≈70–80degrado 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.

OpzioneParamsCorpus targetCosto Vast.aiPP atteso
Più dati, stessa arch (Run #4+)46 M500 M–1 B token IT~$5–10~3.3–3.6
Scale a 175 M, corpus serio175 M2–3 B token IT~$15–25~35–45
Distillazione (5–10 M student)5–10 Msame corpus + teacher~$1~90–120
LoRA su LLaMA 3.1 8B8 Bcorpus letterario attuale~$3–5molto 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:

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.

Dettaglio completo parametri, corpus e framework di valutazione: Fine-tuning e Valutazione →

Strategia SFT — timing e tier per paradigma

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

TierTaskSoglia tokenBase attualeVai 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.

Struttura dei branch

CORE → checkpoints/run3_best.pt [LOCKED, mai sovrascritto] │ ├── Branch A — SFT diretto (ora, Tier 1) │ ├── A1: SFT-condominio → checkpoints/run5a_sft_cond.pt │ ├── A2: SFT-moto → checkpoints/run5b_sft_moto.pt │ └── A3: SFT-scrittura → checkpoints/run5c_sft_scrit.pt │ ├── Branch B — DAPT → SFT (2–4 settimane) │ ├── B1: DAPT-legale → checkpoints/run5d_dapt_legale.pt │ │ └── SFT-cond → checkpoints/run5e_sft_cond_dapt.pt │ └── B2: DAPT-moto → checkpoints/run5f_dapt_moto.pt │ └── SFT-moto → checkpoints/run5g_sft_moto_dapt.pt │ └── Branch C — Extended pre-training (background) └── +200 M token IT → checkpoints/run6_pt200m.pt └── SFT (tutti i Tier) → checkpoints/run6a_*, run6b_*, ...

Logica di confronto

Esperimenti chiave
Tre confronti che rispondono a domande precise

A1 vs B1 — DAPT vale la pena?

  • Stessa SFT dataset (condominio)
  • Differenza: B1 ha 5–10 M token DAPT legale
  • Metrica: PPL test set, qualità output su prompt standard
  • Atteso: B1 migliore su output burocratico, A1 sufficiente per template base

A1 vs C/SFT-cond — più pre-training aiuta?

  • Stessa SFT dataset (condominio)
  • Differenza: C ha base da 200 M token
  • Metrica: generalizzazione a prompt non visti
  • Atteso: C migliore su task NLU, A1 equivalente per NLG puro

A3 (scrittura) — funziona a questo livello?

  • Borderline (Tier 2): base potrebbe essere insufficiente
  • SFT su corpus personale narrativo
  • Misura empirica del floor qualitativo per stile personale
  • Se deludente → rifare su base C

Cross-domain: SFT fa dimenticare l'italiano?

  • Prompt generico in italiano somministrato a A1 e A2
  • Misura il grado di catastrophic forgetting
  • Con 200–300 esempi SFT atteso: forgetting trascurabile
  • Con dataset più grande: potrebbe servire regularizzazione

Framework di valutazione

Per rendere i branch comparabili serve un evaluation framework uniforme:

python3 tools/eval_branch.py \ --model checkpoints/run5a_sft_cond.pt \ --test data/sft_condominio_test.jsonl \ --cross data/wiki_it_val.txt \ --out logs/run5a/eval.json

Script da sviluppare

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.
PiattaformaContenutoRegimeToken lordiUtilità 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

DatasetDimensione grezzaToken netti stimatiAccessoQualità
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.
StadioTecnicaRimozione tipicaTool nel progetto
1. Euristico < 50 parole, > 30% char non-alfa, linee duplicate interne, densità simboli anomala 15–25% tools/filter_corpus.py
2. Lingua 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)

CategoriaFonte principaleToken target%Probe benchmarking
EnciclopedicoWikipedia IT (dump completo)~230 M25%probes/wiki_it.txt
Web generaleCulturaX IT / CC-100 IT filtrati~460 M50%probes/web_gen.txt
Letterario PDGutenberg IT + Letteratura IT + FeedBooks~92 M10%probes/letter_pd.txt
TecnicoManuali.it + PDF/DOCX tecnici~92 M10%probes/tecnico_it.txt
Narrativo contemporaneoCorpus personale esistente~44 M5%probes/narrativo_it.txt
Totale~918 M100%

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

#TestoPPLLivelloDiagnosi
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 / mismatchRilevabile?Perché
Lingua stranieraSì — forteFuori distribuzione totale
Italiano informale / conversazionaleSì — forteRegistro mai visto in Wikipedia
Typo in entità nominate note (Sacro → Saro)Sì — forteToken raro/OOV, rompe sequenza coesa
OCR difettoso su nomi propriSì — probabileStesso meccanismo del typo
Errori morfosintattici comuni (ha fondando)Segnale debolePattern simili presenti nel corpus
Errori semantici con entità corretteNoModello 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

DatasetToken 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

CategoriaToken%Fonte
Enciclopedico~230 M25%Wikipedia IT dump (già estratto)
Web generale~460 M50%Common Corpus IT
Letterario PD~92 M10%liberliber + opus_books
Tecnico~92 M10%PDF/DOCX personali via convert_to_text.py
Narrativo contemporaneo~44 M5%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

MetricaValore
Datasetmii-community/liberliber-cleaned
Documenti scritti4 289
Documenti saltati (vuoti / < 50 char)221
Testo estratto1.33 GB
Durata2m 38s
File outputdata/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).
python3 tools/filter_corpus.py data/liberliber_it.txt \ --checkpoint checkpoints/best.pt \ --dedup --ppl-cut 0.80 \ --out data/liberliber_it_clean.txt
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

CategoriaToken%Fonte
Enciclopedico~230 M25%Wikipedia IT dump (già estratto)
Web generale / PD~550 M60%Common Corpus IT (PleIAs) — fonte primaria
Tecnico~92 M10%PDF/DOCX personali via convert_to_text.py
Narrativo contemporaneo~46 M5%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:
FilePipelineStato
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

ProblemaPrimaDopo
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'"

Rimpatrio e cleanup

# Scarica risultati (~6 GB output + 300 KB rejected) rsync -avz --progress -e "ssh -p PORT -o StrictHostKeyChecking=no" \ root@HOST:~/slm/data/culturax_it_ppl95.txt data/ rsync -avz -e "ssh -p PORT -o StrictHostKeyChecking=no" \ root@HOST:~/slm/data/culturax_ppl95_rejected.txt data/ # Distruggi istanza (interrompe il costo) vastai destroy instance rm .vastai-instance
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)

MetricaValore
Documenti in input2.722.042
Kept (output)2.439.329 (89.6%)
Scartati euristico140.635 (5.2%)
Scartati dedup115 (0.004% — corpus già quasi privo di duplicati)
Scartati PPL >203.2141.963 (5.2%)
Dimensione output7.5 GB
Durata RTX 40907h 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): 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:

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:
FonteFilePeso nel mixNote
Wikipedia ITdata/wiki_it.txt25%Già usato in Run #3–4. Registro enciclopedico.
CulturaX IT (filtrato)data/culturax_it_ppl95.txt60%7.5 GB, 2.4M doc, testo web italiano moderno.
Corpus letterariodata/corpus_letterario.txt5%EPUB narrativa contemporanea, prima persona.
Corpus tecnico personaledata/corpus.txt10%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:

  1. 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).
  2. _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.
  3. 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.
FasePrima (int64 / lista Python)Dopo (int16 / numpy streaming)
Lettura file7.7 GB stringa Python in RAM10 MB per chunk, scartato dopo encode
Lista intermedia1.56B interi × 28 B = ~44 GBarray numpy: 1.56B × 2 B = 3.1 GB
Tensor finale1.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.
# Avvio training a shard (7 shard, 8000 step ciascuno = 56000 totali) tmux new-session -d -s run5 \ 'bash tools/train_chunked.sh 2>&1 | tee /tmp/run5_chunked.log' # Monitoraggio tail -f /tmp/run5_chunked.log # oppure tmux attach -t run5
ParametroValore
Shard7 × ~1.2 GB (~5.9M righe/shard)
Step per shard8.000 (LR schedule completo per shard)
Step totali56.000 (~917M token processati)
Cache shardsalvata dopo primo encoding → riavvii istantanei
ETA stimata~4–6h tokenizzazione + ~24–30h training su MPS
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.

Correzioni rispetto al piano iniziale

ProblemaCausaFix
Checkpoint incompatibile (missing key: pos_encoding.pe)train_chunked.sh mancava di --rope: il modello era costruito con PE sinusoidale invece di RoPEAggiunto --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 condivisaRidotto a --batch 8
GGUF mancanti su discoGenerati su Vast.ai durante Run #4, mai scaricati prima della destroy dell'istanzaRiconvertiti da checkpoints/run4_best.pt via tools/convert_to_gguf.py e tools/quantize_q8.py

Checkpoint Step 500 — shard_aa.txt

MetricaValore
Step500 / 8000 (shard 1)
Train loss4.2399
Val loss4.3260
Tempo da avvio13m 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

MetricaValore
Step1000 / 8000 (shard 1)
Train loss4.1928
Val loss4.2557 ▼ −0.070 da step 500
Tempo da avvio41m 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

MetricaValore
Step2500 / 8000 (shard 1, 31%)
Train loss4.1206
Val loss4.1491 ▼ −0.107 da step 1000
Tempo da avvio94m 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

MetricaValore
Step3500 / 8000 (shard 1, 44%)
Train loss4.0436
Val loss4.0916 ▼ −0.058 da step 2500
Tempo da avvio126m 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

MetricaValore
Step4000 / 8000 (shard 1, 50%)
Train loss4.0104
Val loss4.0138 ▼ −0.078 da step 3500 — sotto Run #3 (4.0768)
Gap train/val0.003 (nessun overfitting)
Tempo da avvio142m 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

MetricaValore
Step4500 / 8000 (shard 1, 56%)
Train loss3.9650
Val loss4.0010 ▼ −0.013 da step 4000
Tempo da avvio160m 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

MetricaValore
Step5000 / 8000 (shard 1, 62%)
Train loss3.9376
Val loss3.9416 ▼ sotto 4.0 per la prima volta
Tempo da avvio176m 31s

Checkpoint Step 5500 — shard_aa.txt plateau

MetricaValore
Step5500 / 8000 (shard 1, 69%)
Train loss3.8826
Val loss3.9419 ≈ 0 da step 5000 — plateau
Tempo da avvio195m 56s
LR8.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

StepTrain lossVal lossΔ val
04.2144.456—
10004.1934.256−0.200
15004.1664.218−0.038
20004.1244.169−0.049
25004.1214.149−0.020
30004.0714.119−0.030
35004.0444.092−0.027
40004.0104.014−0.078
45003.9654.001−0.013
50003.9383.942−0.059
55003.8833.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.
RisorsaValoreNote
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 MBsolo interprete Python e data loader — tutto il heavy lifting è su MPS
RAM sistema7.52 GB / 8 GB = 91.8%wired 5.76 GB + compressor 952 MB + resto
Libera81 MBquasi zero; macOS gestisce con compressione aggressiva
CPU istantaneo42–87% (1 core)alterna compute MPS (asincrono) e step Python (sincrono)
CPU accumulato81m 52s su 161m wall = 51%l'altra metà: MPS pipeline asincrona e attesa risultati
Swap lifetime24M pagine in/outswapping avvenuto durante picchi di allocazione
Velocità~2.1s/stepvs 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).

Checkpoint Step 6000 — shard_aa.txt ▼ plateau rotto

MetricaValore
Step6000 / 8000 (shard 1, 75%)
Train loss3.8774
Val loss3.9193 ▼ −0.023 dal plateau
Tempo da avvio214m 48s (3h 34m)
LR7.05×10⁻⁵
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.
Generazione step 6000 — output degenerato:
'Hawarahahahhayahhahhahhahhahhahhahhahhamhah.hhahhahhahhhahhahhahhahhhahhhahhahhahhahhahhahhahhahhahremhuhhihhahhahhahhah'
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.

Checkpoint Step 6500 — shard_aa.txt ▼▼ salto significativo

MetricaValore
Step6500 / 8000 (shard 1, 81%)
Train loss3.8476
Val loss3.8753 ▼▼ −0.044 da step 6000
Tempo da avvio234m 31s (3h 54m)
LR5.33×10⁻⁵
−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

MetricaValore
Step7000 / 8000 (shard 1, 87.5%)
Train loss3.8317
Val loss3.8515 ▼ −0.024 da step 6500
Tempo da avvio250m 56s (4h 10m)
LR4.05×10⁻⁵
Step corrente7200 — 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

MetricaValore
Step7500 / 8000 (shard 1, 94%)
Train loss3.8229
Val loss3.8759 ▲ +0.024 da step 7000
Tempo da avvio265m 39s
LR3.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

MetricaValore
Step8000 / 8000 (shard 1, 100%)
Train loss3.7879 — minimo assoluto di shard 1
Val loss3.8665
Durata shard 1280m 15s (4h 40m 15s)
Best val loss complessivo3.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

StepTrainValΔ val
04.2144.456—
10004.1934.256−0.200
20004.1244.169−0.087
30004.0714.119−0.050
40004.0104.014−0.105
45003.9654.001−0.013
50003.9383.942−0.059
55003.8833.942≈ 0
60003.8773.919−0.023
65003.8483.875−0.044
70003.8323.852−0.023 ← min shard
75003.8233.876+0.024
80003.7883.867−0.009

Run #5 — Shard 2/7 (shard_ab.txt) — inizio 2026-10-04 02:51

ParametroValore
Fileshard_ab.txt (1.18 GB)
Token totali289.5M → 260.6M train / 29M val
Tokenizzazione878s (14m 38s) — cache salvata in .pt
Checkpoint caricatobest.pt — step 27500, val 3.5861 (Run #4)
Re-warmup100 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

MetricaValore
Step500 / 8000 (shard 2, 6%)
Train loss4.3048
Val loss4.3092
Tempo da avvio shard 213m 21s
LR2.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

MetricaValore
Step7500 / 8000 (shard 2, 94%)
Train loss3.8137
Val loss3.8476 ▼ −0.017 recupero dal micro-uptick
Tempo da avvio shard 2231m 09s (3h 51m)
LR3.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

MetricaValore
Step8000 / 8000 (shard 2, 100%)
Train loss3.8387
Val loss3.8344 ▼▼ nuovo minimo assoluto — meglio di shard 1 (3.867)
Durata shard 2245m 53s (4h 05m 53s)
Best val loss complessivo3.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

MetricaShard 1Shard 2
Val loss iniziale (step 0)4.4564.462
Val loss a step 40004.0143.981
Minimo assoluto3.852 (step 7000)3.834 (step 8000)
Val loss finale (step 8000)3.8673.834
Durata280m 15s245m 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

Shard 3 avviato alle 07:11:52. Corpus: shard_ac.txt (1.18 GB). Tokenizzazione completata: 290,173,474 token totali, train 261M / val 29M. Cache: shard_ac.txt.bpe-spm.v16000.tokens.pt (553 MB). Ripreso da step 27500, val_loss=3.5861. Re-warmup: 100 step.

Tabella checkpoint — shard_ac.txt

StepTrain lossVal lossTempo (mm:ss)LRNote
04.44914.444301:071.50×10⁻⁴ (re-warmup)Avvio shard 3 — val reset atteso
5004.30684.315213:582.98×10⁻⁴−0.129 da step 0; gen. ripetitiva (LR alta)
10004.24094.261332:522.91×10⁻⁴−0.054 da step 500; lessico inventato ma sintassi corretta
15004.17984.203252:332.80×10⁻⁴−0.058; gen. ⁇ UNK (artefatto seed, non degrado)
20004.16904.176867:502.63×10⁻⁴−0.026; rallentamento atteso — ancora in esplorazione larga (LR alta)
25004.13144.153883:362.43×10⁻⁴−0.023; gen. coerente: "delle più belle città d'Italia" ✓
30004.07604.1122101:492.20×10⁻⁴−0.042; discesa riprende; gen. "ALLE BUTTA" (artefatto seed corpus)
35004.05634.0907120:081.94×10⁻⁴−0.022; sotto 4.1; gen. all-caps CHIRE/REMITO (artefatto seed)
40004.00134.0208138:241.68×10⁻⁴−0.070 ← accelerazione; LR entra in zona di convergenza rapida
45003.98344.0010154:561.41×10⁻⁴−0.020; val ≈ 4.0 (soglia raggiunta); gen. "Idraulica" ripetuta (artefatto)
50003.94643.9663172:401.15×10⁻⁴−0.035; sotto 4.0; gen. filosofica italiana: "teoria di genere... il corpo e l'essere" ✓✓
55003.89403.9524190:529.14×10⁻⁵−0.014; LR sotto 10⁻⁴ — deep cosine tail; gen. educativa corretta
60003.89723.9055208:027.05×10⁻⁵−0.047; discesa riprende; gen. istituzionale corretta
65003.83803.8830226:035.48×10⁻⁵−0.022; gen. giornalistica: "come da tradizione, ha preso parte alla manifestazione" ✓✓
70003.82093.8439242:524.05×10⁻⁵−0.039; sotto 3.85; gen. letteraria con congiuntivo: "si sarebbe lasciato andare via" ✓✓✓
75003.83393.8210260:023.27×10⁻⁵−0.023; minimo shard 3 (3.821); gen. narrativa ✓✓✓
80003.84513.8453275:341.50×10⁻⁵+0.024 micro-uptick da step 7500 (tipico a LR→min); fine shard 3

Riepilogo shard 3 e confronto shard 1–3

MetricaShard 1Shard 2Shard 3
Val loss iniziale4.4564.4624.444
Val loss minimo3.852 (step 7000)3.834 (step 8000)3.821 (step 7500)
Val loss finale (step 8000)3.8673.8343.845 (+0.024 micro-uptick)
Durata280m 15s245m 53s275m 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

StepTrain lossVal lossTempo (mm:ss)LRNote
04.45224.472101:071.50×10⁻⁴ (re-warmup)Avvio shard 4
5004.31234.284614:242.98×10⁻⁴−0.187; gen. meta-ripetitiva "disponibilità" (LR alta)
10004.24514.241633:202.91×10⁻⁴−0.043; gen. coerente: "coniugi... insoddisfazione... possibilità" ✓
15004.18794.210152:132.80×10⁻⁴−0.032; gen. medico-legale: "violazione del trattamento"
20004.16604.158374:142.63×10⁻⁴−0.052; gen. cirillico in apertura + italiano EU coerente — artefatto seed
25004.12384.150791:482.43×10⁻⁴−0.008; curva si appiattisce (atteso); gen. italiana corretta "qualità di vita" ✓
30004.06704.0961108:512.20×10⁻⁴−0.055; drop dopo mini-plateau; gen. news/politica IT coerente ✓
35004.05094.0621166:331.94×10⁻⁴−0.034; ⚠️ rallentamento per swap — velocità ripresa a step 4000
40004.00303.9965191:141.68×10⁻⁴−0.065; 🎯 primo attraversamento sotto 4.0 · gen. commerciale IT ✓
45003.96154.0091220:591.41×10⁻⁴+0.013 mini-uptick (normale); gen. commerciale IT; velocità ~3.5 s/step ripristinata
50003.93793.9558251:21~1.16×10⁻⁴−0.053; nuovo minimo shard 4 · cosine LR in discesa accelerata
55003.91483.9196278:42~9.1×10⁻⁵−0.036; nuovo minimo · velocità ~2.6 s/step (pressione mem. normalizzata)
60003.88103.8961308:157.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

MetricaValore
Step7000 / 8000 (shard 2, 87.5%)
Train loss3.8363
Val loss3.8645 ▲ +0.007 micro-uptick da step 6500
Tempo da avvio shard 2217m 05s (3h 37m)
LR4.05×10⁻⁵
Step corrente7450 — ~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.

Checkpoint Step 6500 — shard_ab.txt ▼ 3.857 — nuovo minimo assoluto

MetricaValore
Step6500 / 8000 (shard 2, 81%)
Train loss3.8695
Val loss3.8574 ▼ nuovo minimo · shard 1 step 6500: 3.875 (−0.018)
Tempo da avvio shard 2200m 49s (3h 20m)
LR5.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

MetricaValore
Step6000 / 8000 (shard 2, 75%)
Train loss3.9003
Val loss3.8952 ▼ nuovo minimo shard 2 · shard 1 a stesso step: 3.919 (−0.024)
Tempo da avvio shard 2185m 08s (3h 05m)
LR7.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.

Checkpoint Step 5500 — shard_ab.txt

MetricaValore
Step5500 / 8000 (shard 2, 69%)
Train loss3.9020
Val loss3.9287 ▼ −0.030 da step 5000 · shard 1 step 5500: 3.942 (−0.013)
Tempo da avvio shard 2170m 57s (2h 50m)
LR9.14×10⁻⁵
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

MetricaValore
Step5000 / 8000 (shard 2, 62.5%)
Train loss3.9486
Val loss3.9591 ▼ recupero — sotto 3.96
Tempo da avvio shard 2154m 27s (2h 34m)
LR1.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

MetricaValore
Step4500 / 8000 (shard 2, 56%)
Train loss3.9873
Val loss3.9893 ▲ +0.009 da step 4000 — micro-uptick
Tempo da avvio shard 2135m 42s (2h 15m)
LR1.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

MetricaValore
Step4000 / 8000 (shard 2, 50%)
Train loss3.9811
Val loss3.9807 ▼▼ sotto 4.0 — primo shard a raggiungerlo a step 4000
Tempo da avvio shard 2120m 37s (2h 00m)
LR1.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

MetricaValore
Step3500 / 8000 (shard 2, 44%)
Train loss4.0576
Val loss4.0484 ▼ sotto 4.05 — migliore di shard 1 a parità di step
Tempo da avvio shard 2105m 17s (1h 45m)
LR1.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

MetricaValore
Step3000 / 8000 (shard 2, 37.5%)
Train loss4.0764
Val loss4.1387 ▼ −0.009 da step 2500
Tempo da avvio shard 291m 25s
LR2.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

MetricaValore
Step2000 / 8000 (shard 2, 25%)
Train loss4.1662
Val loss4.1952 ▼ −0.024 da step 1500
Tempo da avvio shard 262m 37s
LR2.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

MetricaValore
Step2500 / 8000 (shard 2, 31%)
Train loss4.1285
Val loss4.1473 ▼ −0.048 da step 2000
Tempo da avvio shard 277m 03s
LR2.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

MetricaValore
Step1500 / 8000 (shard 2, 19%)
Train loss4.2235
Val loss4.2189 ▼ −0.072 da step 1000
Tempo da avvio shard 245m 36s
LR2.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

MetricaValore
Step1000 / 8000 (shard 2, 12.5%)
Train loss4.2526
Val loss4.2911
Tempo da avvio shard 228m 51s
LR2.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.
StepTrainValTempoNote
04.45224.472101:07Avvio shard 4
5004.31234.284614:24
10004.24514.241633:20
15004.18794.210152:13
20004.16604.158374:14
25004.12384.150791:48
30004.06704.0961108:51
35004.05094.0621166:33⚠️ rallentamento swap
40004.00303.9965191:14🎯 primo attraversamento sotto 4.0
45003.96154.0091220:59mini-uptick
50003.93793.9558251:21nuovo minimo shard 4
55003.91483.9196278:42
60003.88103.8961308:15primo attraversamento sotto 3.9
65003.84333.8747354:26⚠️ rallentamento swap (57 min per 500 step)
70003.87753.8724398:30
75003.83433.8570415:47
80003.80913.8227434: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."
StepTrainValTempo
04.45124.456701:07
5004.29174.294616:48
10004.25024.272340:18
15004.21064.211061:14
20004.14654.166182:00
25004.13194.1208104:20
30004.07514.0821123:07
35004.07334.0745146:49
40004.00184.0197165:33
45003.97604.0184185:39
50003.96903.9299203:39
55003.88393.9277222:58
60003.84493.9043244:08
65003.85193.8506262:55
70003.85753.8529281:06
75003.85483.8350298:44
80003.82623.8215315: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."
StepTrainValTempo
04.43464.462101:07
5004.31514.313315:40
10004.25974.259834:41
15004.19374.223151:30
20004.15764.171768:12
25004.11374.128185:44
30004.08804.1060103:12
35004.04344.0297117:26
40004.00654.0413131:23
45003.95133.9764154:34
50003.93963.9669172:11
55003.88493.9351189:45
60003.87653.8926208:28
65003.86813.8811227:48
70003.83973.8766247:39
75003.84063.8307265:31
80003.81683.8344285: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.
StepTrainValTempo
04.47784.464601:07
5004.30624.329317:49
10004.22944.254336:44
15004.21894.229557:44
20004.14084.173277:13
25004.12484.174298:22
30004.07764.0823117:43
35004.03574.0872134:59
40004.01474.0271154: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.
ShardFileVal loss finaleNote
1 / aashard_aa.txt3.8665Più difficile tra i primi tre
2 / abshard_ab.txt3.845
3 / acshard_ac.txt3.845Minimo osservato — corpus più facile
4 / adshard_ad.txt3.8227Minimo assoluto Run #5
5 / aeshard_ae.txt3.8215
6 / afshard_af.txt3.8344
7 / agshard_ag.txtin corsoVal 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:
ParametroValoreNote
Punto di partenzacheckpoints/run4_best.ptRun #4, val 3.5861 — catena completa aa→ag
Meccanismocp last.pt → best.pt prima di ogni shardCorregge il bug di Run #5
Sequenza shardaa → ab → ac → ad → ae → af → agOrdine corpus — curriculum da valutare
Batch size32vs 8 su M2 — 4× token per step
Steps per shard8.000A batch=32: 8000 × 16384 = 131M token per shard (~94% copertura)
Token totali stimati~917M7 × 8000 × 16384 ≈ Chinchilla parity (918M target)
Tempo stimato~8 oreRTX 4090, ~0.5 s/step
Costo stimato~$12Vast.ai spot
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)

RunBatchStepTokenCumulativo% Chinchilla
Run #43230.000245M245M27%
Run #5 (M2)856.000229M474M52%
Run #6 (GPU, pianificato)3256.000918M1.147B125%
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.