Far girare i grandi modelli linguistici su uno smartphone non è più un esperimento di ricerca. È una realtà commerciale. Eppure, nel momento in cui si passa da una demo giocattolo a un prodotto reale, la latenza si ripresenta. Hai ridotto il modello, quantizzato i pesi, eppure la fase di prefill trascina i piedi. I token si bloccano. L'interfaccia utente si congela. La colpa raramente ricade su chi dovrebbe.
Sui dispositivi Android, il calcolo non è quasi mai il collo di bottiglia durante il prefill dell'LLM. Lo è la larghezza di banda della memoria. I moderni SoC di fascia alta sono dotati di potenti core GPU e NPU che possono processare operazioni aritmetiche molto più velocemente di quanto il sottosistema di memoria riesca a rifornirli. Quando si analizza un'implementazione "naive" dell'attention, le unità di esecuzione non sono sature. Stanno aspettando. Aspettando la DRAM.
Perché il tuo modello quantizzato sembra ancora lento
La quantizzazione è diventata il primo passo predefinito per l'inferenza on-device. Ridurre i pesi da FP16 a INT8 dimezza la dimensione del modello e riduce lo spazio di archiviazione. Aiuta. Ma non risolve la latenza dello strato di attention. Il motivo è semplice: la quantizzazione riduce la quantità di dati memorizzati, ma non riduce il numero di transazioni di memoria effettuate dal meccanismo di attention.
Uno strato di multi-head attention standard, implementato secondo i manuali, compie tre viaggi completi verso la DRAM per ogni strato. Le matrici Query, Key e Value vengono lette dalla memoria principale, i punteggi vengono calcolati e i risultati intermedi vengono riscritti. L'aritmetica è banale. Il movimento dei dati è brutale. Su Android, dove i budget di potenza e termici sono limitati, questo schema manda in crisi il bus di memoria. Il processore, di fatto, paga il pedaggio tre volte per attraversare lo stesso ponte.
Se hai distribuito modelli INT8 e ti stai chiedendo perché la fase di prefill continui a scalare quadraticamente con la lunghezza del prompt, ecco la risposta. I pesi sono più piccoli, ma il traffico di attivazione rimane enorme.
Il collo di bottiglia è la memoria, non la matematica
Per capire la soluzione, guarda il roofline. Le GPU e NPU mobili su chip come lo Snapdragon 8 Gen 3 e il Dimensity 9300 hanno un throughput di calcolo teorico che supera di gran lunga ciò che le loro interfacce LPDDR5X possono sostenere. In un kernel di attention naive, ogni testa calcola la softmax sul prodotto di Q e K, quindi moltiplica per V. Ogni matrice di punteggio intermedia viene materializzata nella memoria globale. Ciò significa che i picchi di lettura della DRAM scalano con il quadrato della lunghezza della sequenza, ovvero O(n²). Per un prompt da 1024 token, il traffico di memoria è già sufficientemente elevato da dominare il tempo di esecuzione.
I core sono sottoutilizzati perché non riescono a nascondere la latenza. I processori moderni si affidano alle cache per mantenere pieni i pipeline. Quando un algoritmo subisce continui cache miss e recupera dati dalla DRAM, le unità di esecuzione rimangono inattive. Nessuna quantità di quantizzazione può risolvere questo disallineamento strutturale tra capacità di calcolo e disponibilità di memoria.
Come il tiling recupera la larghezza di banda
La soluzione è una strategia di tiling che mantiene i punteggi intermedi nella SRAM on-chip invece di inviarli alla DRAM. È lo stesso concetto alla base di Flash Attention, adattato allo stack di calcolo di Android. Invece di materializzare una matrice di punteggio completa n × n in memoria, si suddivide il calcolo in piccoli tile che rientrano nella cache L1. Si calcolano le statistiche locali della softmax, si accumulano i valori massimi correnti e le somme di normalizzazione, e si scrivono i risultati pesati finali solo alla fine nella memoria.
Questo cambia la complessità della larghezza di banda. I picchi di lettura della DRAM scendono da O(n²) a O(n), perché non è più necessario spostare intere matrici di punteggio attraverso la memoria principale. Il lavoro pesante avviene all'interno della SRAM, proprio accanto alle unità di esecuzione.
Per un esempio concreto, considera una dimensione del tile di 64 e una dimensione della testa di 128. Il tile del punteggio occupa 16 KB. Questo ingombro rientra comodamente nella cache L1 degli attuali SoC di fascia alta come lo Snapdragon 8 Gen 3 e il Dimensity 9300. L'aritmetica rimane locale. Il bus di memoria respira.
Implementazione su Android
Lo schema algoritmico è semplice, anche se curare i dettagli è fondamentale.
Suddividi le tue Query, Key
