Il training procede da ore, la loss scende con la regolarità di un metronomo, e poi al log successivo compare: loss: nan. Da lì in avanti solo nan, per sempre. Nessun bug nel codice, nessun dato corrotto: molto spesso il colpevole è la decisione, presa magari con leggerezza, di allenare in float16 per risparmiare memoria. Per capire perché un formato numerico può sabotare un modello, bisogna aprire il cofano e guardare come è fatto un numero in virgola mobile.
La virgola mobile come budget fisso di cifre, overflow e underflow, il trucco log-sum-exp: la precisione finita, nel libro.
Leggi «Analisi numerica: quando i numeri hanno precisione finita» nel libro →
Com’è fatto un float: segno, esponente, mantissa
Un float è la versione binaria della notazione scientifica. Come scriviamo 6.02 × 10²³ invece di ventiquattro cifre, un computer scrive ogni numero come mantissa × 2^esponente, più un bit per il segno. I bit disponibili vanno divisi tra i due ruoli, e la divisione è un compromesso secco: i bit di esponente decidono quanto lontano puoi arrivare (numeri enormi o minuscoli), i bit di mantissa decidono quanto fine è la grana (quante cifre significative tieni).
Il classico float32 usa 1 bit di segno, 8 di esponente e 23 di mantissa: arriva fino a circa 3.4 × 10³⁸ e tiene circa 7 cifre decimali significative. Per decenni è stato il default indiscusso del calcolo scientifico, e nessuno ci pensava più. Poi i modelli sono diventati grandi, e ogni bit ha ricominciato a costare.
La griglia dei bit nei tre formati: la larghezza di ogni blocco è proporzionale ai bit. bfloat16 sacrifica mantissa per tenersi gli 8 bit di esponente di float32.
Float16: metà bit, metà memoria, doppi guai
Passare da 32 a 16 bit per numero dimezza la memoria occupata dai pesi e raddoppia, in prima approssimazione, quanti dati muovi per ciclo. Le GPU moderne hanno unità dedicate che moltiplicano matrici a 16 bit molto più in fretta che a 32. L’incentivo economico è ovvio.
Il problema è la spartizione dei bit. Lo standard float16 (IEEE) tiene 5 bit di esponente e 10 di mantissa: il numero più grande rappresentabile è 65 504, e sotto circa 6 × 10⁻⁵ i valori cominciano a perdere precisione fino ad azzerarsi del tutto poco più giù. Per la grafica 3D, per cui il formato era nato, va benissimo. Per un training è una coperta corta: le attivazioni possono superare 65 504 (e diventano infinito, poi nan), mentre i gradienti — spesso numeri minuscoli — scivolano sotto la soglia e diventano zero. In entrambi i casi il segnale di apprendimento si distrugge in silenzio.
La bilancia da cucina che deve pesare un camion
Il fenomeno di fondo si chiama gamma dinamica, e l’analogia giusta è una bilancia. Una bilancia da cucina apprezza il grammo ma satura a cinque chili; una pesa a ponte regge il camion ma non si accorge di un pacco di pasta. Ogni strumento di misura sceglie un intervallo, e fuori da quello mente.
Un float è una bilancia con una proprietà in più: la grana peggiora man mano che sali. Tra 1 e 2, un float16 distingue valori a passi di circa un millesimo; tra 32 768 e 65 504 i passi diventano di 32 unità. Sommare 1 a 40 000, in float16, non cambia niente: il risultato è ancora 40 000, perché il passo lì vale più di 1. Quando in un training accumuli migliaia di piccoli contributi dentro un valore già grande — è esattamente ciò che fa una somma di gradienti — quei contributi possono sparire uno per uno. Il modello smette di imparare senza produrre nessun errore visibile.
I bug numerici sono i peggiori della categoria: il codice è corretto, i test passano, e il modello impara peggio senza dirtelo.
bfloat16, o dell’arte di sacrificare la cosa giusta
La soluzione elegante è arrivata cambiando la spartizione: bfloat16 («brain float», nato in Google per le TPU) usa 8 bit di esponente — gli stessi di float32 — e solo 7 di mantissa. Tradotto: identica portata di float32, dal minuscolo all’enorme, ma appena 2-3 cifre significative di grana.
Sembra un pessimo affare, e invece è quello giusto, perché il deep learning ha una tolleranza notevole per il rumore fine — un gradiente impreciso al percento resta un gradiente utile — ma nessuna tolleranza per overflow e underflow, che distruggono l’informazione del tutto. bfloat16 elimina la classe di guasti catastrofica e tiene quella benigna. Non a caso è diventato il formato di default per l’addestramento dei grandi modelli linguistici su hardware recente, mentre float16 sopravvive soprattutto dove la portata ridotta non morde.
Mixed precision: il meglio dei due mondi, con un trucco
Nella pratica quasi nessuno allena «tutto a 16 bit». La ricetta standard si chiama mixed precision: le moltiplicazioni pesanti girano a 16 bit per la velocità, ma una copia dei pesi e gli accumulatori delicati (le somme lunghe, le statistiche di normalizzazione) restano a 32 bit, dove la grana fine serve davvero. È la divisione dei compiti di un cantiere: il martello pneumatico per demolire, il calibro per le finiture.
Con float16 serve un trucco in più, il loss scaling: prima della backpropagation la loss viene moltiplicata per una costante grande, così tutti i gradienti si sollevano sopra la soglia di underflow; dopo, si divide per la stessa costante e i valori tornano corretti. È un cambio di unità di misura temporaneo — pesare in grammi invece che in chili per non perdere le briciole. Con bfloat16, che di portata ne ha in abbondanza, il loss scaling in genere non serve: un motivo in più della sua popolarità.
Perché ti riguarda
Anche se non allenerai mai un modello da zero, questi compromessi ti toccano il portafoglio e i risultati. Quando scarichi un modello open source «in fp16» o «in bf16», stai scegliendo quanta VRAM ti serve. Quando un modello quantizzato a 8 o 4 bit gira sul tuo laptop, sta spingendo lo stesso compromesso — meno bit, più rumore, più velocità — ancora più in là. E quando un fine-tuning produce risultati stranamente instabili, la prima domanda da farsi non è «che bug ho scritto?» ma «in che precisione sto lavorando, e cosa ci sto facendo passare dentro?». I numeri in virgola mobile sono un’approssimazione che finge molto bene di essere esatta: sapere dove finisce la finzione è ciò che separa chi subisce i nan da chi li previene.

I commenti sono riservati agli iscritti.
Accedi per commentare