Faire fonctionner des modèles de langage de grande taille sur un téléphone n'est plus une simple expérience de recherche. C'est une réalité de mise sur le marché. Pourtant, dès que l'on passe d'une simple démonstration à un produit réel, la latence réapparaît. Vous avez réduit la taille du modèle, quantifié les poids, et pourtant la phase de prefill traîne. Les tokens stagnent. L'interface utilisateur se fige. La faute incombe rarement là où elle devrait.

Sur les appareils Android, le calcul n'est presque jamais le goulot d'étranglement lors du prefill d'un LLM. C'est la bande passante mémoire qui l'est. Les SoC haut de gamme modernes sont équipés de puissants cœurs GPU et NPU capables d'exécuter des calculs arithmétiques bien plus vite que le sous-système mémoire ne peut les alimenter. Lorsque vous profillez une implémentation naïve de l'attention, les unités d'exécution ne sont pas saturées. Elles attendent. Elles attendent la DRAM.

Pourquoi votre modèle quantifié semble toujours lent

La quantification est devenue la première étape par défaut pour l'inférence sur l'appareil. Réduire les poids de FP16 à INT8 divise par deux la taille du modèle et réduit le stockage. Cela aide. Mais cela ne résout pas la latence des couches d'attention. La raison est simple : la quantification réduit la quantité de données stockées, mais elle ne réduit pas le nombre de transactions mémoire effectuées par le mécanisme d'attention.

Une couche d'attention multi-tête standard, implémentée de manière classique, effectue trois allers-retours complets vers la DRAM pour chaque couche. Les matrices Query, Key et Value sont lues depuis la mémoire principale, les scores sont calculés, et les résultats intermédiaires sont réécrits. L'arithmétique est triviale. Le mouvement des données est brutal. Sur Android, où les budgets de puissance et de thermique sont serrés, ce schéma sature le bus mémoire. Le processeur paie concrètement un péage trois fois pour traverser le même pont.

Si vous déployez des modèles INT8 et que vous vous demandez pourquoi l'étape de prefill augmente toujours de manière quadratique avec la longueur du prompt, voici la réponse. Les poids sont plus petits, mais le trafic d'activation reste énorme.

Le goulot d'étranglement est la mémoire, pas les mathématiques

Pour comprendre la solution, regardez le modèle roofline. Les GPU et NPU mobiles sur des puces comme le Snapdragon 8 Gen 3 et le Dimensity 9300 ont un débit de calcul théorique qui dépasse de loin ce que leurs interfaces LPDDR5X peuvent supporter. Dans un noyau d'attention naïf, chaque tête calcule le softmax sur le produit de Q et K, puis multiplie par V. Chaque matrice de score intermédiaire est matérialisée dans la mémoire globale. Cela signifie que vos lectures DRAM de pointe augmentent avec le carré de la longueur de la séquence, soit O(n²). Pour un prompt de 1024 tokens, le trafic mémoire est déjà assez important pour dominer le temps d'exécution.

Les cœurs sont sous-utilisés car ils ne peuvent pas masquer la latence. Les processeurs modernes s'appuient sur les caches pour maintenir les pipelines pleins. Lorsqu'un algorithme subit constamment des échecs de cache et doit aller chercher des données dans la DRAM, les unités d'exécution restent inactives. Aucune quantité de quantification ne peut corriger ce décalage structurel entre la capacité de calcul et l'apport mémoire.

Comment le tiling permet de récupérer de la bande passante

La solution est une stratégie de tiling qui conserve les scores intermédiaires dans la SRAM intégrée au lieu de les envoyer vers la DRAM. C'est la même intuition qui anime Flash Attention, adaptée à la pile de calcul d'Android. Plutôt que de matérialiser une matrice de score complète de n × n en mémoire, vous divisez le calcul en petits blocs (tiles) qui tiennent dans le cache L1. Vous calculez les statistiques de softmax locales, accumulez les valeurs max glissantes et les sommes de normalisation, et n'écrivez les sorties pondérées finales en mémoire qu'à la fin.

Cela modifie la complexité de la bande passante. Les lectures DRAM de pointe passent de O(n²) à O(n), car vous n'avez plus besoin de faire transiter des matrices de score complètes par la mémoire principale. Le gros du travail se fait à l'intérieur de la SRAM, juste à côté des unités d'exécution.

Pour un exemple concret, considérons une taille de tile de 64 et une dimension de tête de 128. Le tile de score occupe 16 Ko. Cette empreinte tient confortablement dans le cache L1 des SoC haut de gamme actuels comme le Snapdragon 8 Gen 3 et le Dimensity 9300. L'arithmétique reste locale. Le bus mémoire respire.

Implémenter cela sur Android

L'esquisse algorithmique est simple, bien que la précision des détails soit cruciale.

Divisez vos Query, Key