Il modello è piccolo, ma lo è anche il risultato?
Immaginiamo di trasferire una rete neurale su un dispositivo embedded, un computer integrato in una macchina o in un sensore. Pesi e attivazioni sono rappresentati con interi a 8 bit, quindi ogni numero occupa poco spazio. Un neurone, però, moltiplica molte coppie di numeri e somma i prodotti. Dove finisce quella somma? In un accumulatore, una variabile che deve contenere risultati molto più grandi dei singoli ingressi. La domanda di questo articolo è: come possiamo sapere, prima di eseguire il modello, se quella variabile è abbastanza capiente?
Abstract. Partiamo da un prodotto scalare quantizzato, cioè una somma di prodotti di codici interi che rappresentano valori reali. Ricaviamo un limite conservativo per un accumulatore con segno a 32 bit e costruiamo un caso che lo supera usando soltanto ingressi ammessi a 8 bit. Distinguiamo arrotondamento, saturazione e overflow; mostriamo poi perché bias, pesi effettivi e ordine delle operazioni cambiano l’analisi. I risultati sono calcoli interi esatti e modelli aritmetici espliciti eseguiti in Python, non misure di un microcontrollore o test di un backend TensorFlow Lite.
Il codice intero non è ancora il valore
Una quantizzazione affine rappresenta un valore reale x con x≈s(q−z). q è il codice intero, z il codice che rappresenta lo zero reale e s una scala positiva. Per esempio, se z=−128, il codice q=127 dista 255 passi dallo zero: sottrarre due numeri ciascuno rappresentabile a 8 bit può produrre un numero che non entra in un intero con segno a 8 bit. Il calcolo deve quindi allargare il tipo prima della sottrazione. Il risparmio nella memorizzazione non implica che tutte le operazioni possano mantenere la stessa larghezza.
Usiamo il caso con attivazioni qₐ tra −128 e 127, pesi q𝑤 tra −127 e 127 e zero point dei pesi uguale a zero, coerente con la specifica INT8 TensorFlow Lite consultata. Le attivazioni sono i valori intermedi prodotti dalla rete; i pesi sono i coefficienti appresi. Per un singolo canale di uscita, con scale sₐ e s𝑤, raccogliamo nel numero intero A la somma prima della conversione al formato di uscita.
K è il numero di prodotti che contribuiscono a quell’uscita. b_int è il bias, il termine costante del neurone, espresso nella scala del prodotto sₐs𝑤. A, codici e bias intero sono conteggi senza unità fisica; y assume le unità definite dal modello e dalle scale. Se un canale usa una scala dei pesi diversa, anche la sua scala del bias cambia. Qui studiamo il calcolo di A: la successiva riconversione in INT8 può introdurre ulteriori arrotondamenti e clipping, che non correggono una somma già sbagliata.
Quanto può crescere una somma?
Un intero con segno a 32 bit rappresenta valori da −2.147.483.648 a 2.147.483.647. Per una prima garanzia ignoriamo eventuali cancellazioni tra prodotti positivi e negativi e usiamo il valore assoluto massimo di ogni termine. La differenza |qₐ−zₐ| può arrivare a 255; il peso in valore assoluto a 127. Ogni prodotto è quindi al massimo 32.385 in valore assoluto. La disuguaglianza triangolare dice che il valore assoluto di una somma non supera la somma dei valori assoluti.
La seconda riga è una condizione sufficiente, non necessaria, e presume un margine non negativo dopo il bias. Usiamo il limite positivo anche per i valori negativi, rinunciando prudentemente all’unico valore negativo aggiuntivo disponibile. Senza bias, K≤66.311 garantisce che questa somma diretta e le sue somme parziali restino nel limite per qualunque ingresso ammesso. Non significa che K=66.312 causi sempre un errore: significa che la garanzia universale così costruita non vale più.
Il caso che supera il limite
Scegliamo tutti i codici di attivazione pari a 127, zero point −128 e tutti i pesi pari a 127. Sono valori validi: ciascun prodotto vale 32.385. Con 65.536 termini la somma è 2.122.383.360 e rientra in INT32. Con 66.311 termini è 2.147.481.735; aggiungere un solo altro prodotto porta a 2.147.514.120, oltre il massimo. Il passaggio non è una perdita graduale di qualche decimale, ma l’impossibilità di rappresentare l’intero risultato nel tipo scelto.
| K | Somma esatta | Riavvolgimento modellato | Saturazione modellata |
|---|---|---|---|
| 65536 | 2122383360 | 2122383360 | 2122383360 |
| 66311 | 2147481735 | 2147481735 | 2147481735 |
| 66312 | 2147514120 | −2147453176 | 2147483647 |
| 70000 | 2266950000 | −2028017296 | 2147483647 |
Nella tabella confrontiamo tre aritmetiche, tutte calcolate esplicitamente. Python conserva l’intero esatto. Il modello di riavvolgimento, o wraparound, conserva i 32 bit bassi e li interpreta con segno: può trasformare una grande somma positiva in negativa. Il modello di saturazione ferma invece il risultato al massimo rappresentabile. Nessuno dei due recupera la somma esatta. Non stiamo dicendo che un particolare kernel embedded faccia l’una o l’altra cosa: occorre verificarne contratto, istruzioni e implementazione.

In C++ non è lecito assumere che una normale somma di interi con segno fuori intervallo si riavvolga: il comportamento dell’espressione aritmetica fuori dal tipo è indefinito secondo il testo del working draft consultato. Le istruzioni specifiche di un acceleratore possono avere regole diverse. Per questo il nostro programma usa interi Python e una formula modulare dichiarata, senza provocare overflow nativo per poi scambiare il risultato osservato per una regola portabile.
Il bias e i pesi cambiano il margine
Un bias di 2.000.000.000 è rappresentabile in INT32, ma consuma gran parte del margine positivo. Nel nostro caso estremo restano soltanto 4.554 prodotti garantiti dalla stessa formula. Verificare il tipo del bias separatamente dal tipo dei prodotti non basta: serve un limite sulla loro somma. Viceversa, con zero point delle attivazioni pari a zero, il prodotto massimo in valore assoluto scende a 128·127=16.256 e il limite conservativo senza bias sale a 132.104 termini. La geometria del livello e i parametri di quantizzazione vanno letti insieme.
Il limite globale è spesso troppo prudente per pesi già noti. Per ogni termine con peso wᵢ fisso e codice di attivazione tra lᵢ e uᵢ, calcoliamo i due prodotti agli estremi e prendiamo minimo e massimo. Un peso negativo inverte quale estremo produce il risultato maggiore. Sommare i minimi e sommare i massimi dà un intervallo che contiene ogni somma possibile, anche se alcuni ingressi non possono verificarsi contemporaneamente. È una garanzia condizionata alla validità degli intervalli, non una previsione della frequenza degli errori.
Con pesi (127,−127,64,−64), zₐ=−128 e tutto l’intervallo INT8, i quattro intervalli dei prodotti sono [0,32385], [−32385,0], [0,16320], [−16320,0]. Senza bias, A appartiene a [−48705,48705]. Il limite globale darebbe invece |A|≤129540: corretto, ma molto meno preciso. Non abbiamo usato una media osservata o la speranza che i segni si cancellino; abbiamo usato i coefficienti reali e tutti gli ingressi ammessi. Gli stessi calcoli vanno applicati anche alle somme parziali del programma concreto.
Una somma finale corretta non protegge i passaggi intermedi
Un kernel può riscrivere A come Σqₐq𝑤−zₐΣq𝑤+b_int. Nei numeri reali è la stessa espressione, ma le due somme intermedie richiedono spazio proprio. Con K=150.000, qₐ=zₐ=−128 e q𝑤=127, ogni termine centrato è zero e A=0. Nella forma espansa, però, i due contributi sono −2.438.400.000 e +2.438.400.000: nessuno dei due entra in INT32. Questo non dimostra un difetto di una libreria specifica; dimostra perché la verifica deve seguire il percorso aritmetico realmente implementato, incluse eventuali correzioni precalcolate.
Anche saturare dopo ogni addizione cambia il problema. In un piccolo esempio a 8 bit, la sequenza 100+100−100 vale esattamente 100. Saturando ogni somma, il percorso diventa 100,127,27. Riordinando in 100−100+100, il risultato torna 100. È una dimostrazione dell’effetto dell’ordine nell’aritmetica saturante, non un suggerimento di accumulare reti a 8 bit. Con aritmetica modulare pura, invece, addizioni e sottrazioni conservano il risultato modulo la base: non va attribuito al wraparound lo stesso comportamento della saturazione. Le due regole non sono intercambiabili.
Quale alternativa risolve quale problema?
Allargare l’accumulatore a 64 bit aumenta il margine, purché anche moltiplicazioni, correzioni e conversioni intermedie siano eseguite nel tipo appropriato. Non deduciamo un costo in cicli o energia: dipende dalla piattaforma e richiede misure. Dividere il prodotto scalare in blocchi può limitare le somme locali, ma bisogna ancora combinare i blocchi senza overflow. Se si ridimensiona e arrotonda ciascun blocco prima di sommarlo, si introduce un errore diverso, da analizzare separatamente. Cambiare precisione, scale o architettura può aiutare, ma modifica il compromesso numerico e deve essere valutato anche sulla qualità del modello.
Il protocollo minimo per una verifica futura sul dispositivo è concreto: identificare livello e canale, estrarre pesi, bias, scale e zero point dal modello effettivo; ricostruire K e il percorso del kernel; confrontare un riferimento a precisione sufficiente con casi ordinari e casi estremi; registrare versione della libreria, compilatore, opzioni e hardware. La presente analisi esegue soltanto il riferimento matematico. Non riporta latenza, RAM di picco, potenza o energia per inferenza, perché nessuna di queste grandezze è stata misurata su una scheda.
La risposta: contare i bit dei dati non basta
Un modello INT8 può richiedere somme molto più ampie dei suoi ingressi. La verifica corretta riguarda il numero dei prodotti, il loro intervallo dopo la sottrazione dello zero point, il bias e ogni passaggio intermedio. Il limite conservativo dà una garanzia semplice; l’analisi per peso la rende meno grossolana; l’ispezione del kernel stabilisce se quella garanzia descrive davvero il programma. Il punto non è sospettare di ogni rete quantizzata, ma trasformare una domanda vaga sulla precisione in condizioni verificabili prima del rilascio.
Fonti e programma riproducibile
TensorFlow — TensorFlow Lite 8-bit quantization specification, definitions and CONV_2D requirements.
C++ working draft — Expressions, arithmetic range and evaluation rules.
Le fonti definiscono rappresentazione e semantica; limiti, controesempi e figure sono derivati ed eseguiti per questa monografia didattica. Il codice seguente trova l’ultimo K garantito e confronta le tre regole aritmetiche. Non usa casualità: non occorre un seed. L’archivio include anche bias, intervalli per peso, cancellazione nella forma espansa e caso saturante. Non sono risultati di un prodotto embedded EL-AI né una ricerca originale sottoposta a peer review.
LIMIT = 2**31 - 1
product = (127 - (-128)) * 127
safe_k = LIMIT // product
for k in (safe_k, safe_k + 1, 70000):
exact = k * product # Python arbitrary-precision integer
wrapped = (exact + 2**31) % 2**32 - 2**31
saturated = min(LIMIT, max(-2**31, exact))
print(k, exact, wrapped, saturated)
# Explicit mathematical models, NOT execution of an embedded kernel.
Codice, dati e istruzioni · JSON. Calcoli didattici eseguiti con Python 3.14.0; figure con Matplotlib 3.11.2. Analisi con assistenza AI, senza dichiarare peer review o revisione umana. Copertina originale ImageGen, illustrativa: non documenta persone, sedi o installazioni EL-AI. Fonti consultate il 29 settembre 2026.

