Après avoir passé beaucoup de temps à configurer, mesurer et optimiser des serveurs d’inférence, j’ai fini par dégager quelques principes fondamentaux pour accélérer l’exécution des grands modèles de langage.
L’inférence est une tâche complexe, principalement parce qu’elle se divise en deux étapes aux exigences matérielles très différentes : le prefill (le traitement initial de la requête) et le decode (la génération progressive des tokens). Pour surmonter ces goulots d’étranglement, voici les leviers les plus performants que j’utilise au quotidien :
- FlashAttention et PagedAttention : Deux briques logicielles indispensables pour réduire les accès mémoire lents et éliminer jusqu’à 96 % de la fragmentation de la mémoire vive.
- Le Continuous Batching : Une méthode dynamique de regroupement des requêtes au niveau de chaque token, évitant que le GPU ne reste inactif en attendant la fin des réponses les plus longues.
- La Quantification (AWQ, GPTQ, FP8) : Le moyen le plus direct de diviser par deux ou quatre la taille en mémoire vive du modèle tout en maintenant un niveau de précision très satisfaisant.
- Le Speculative Decoding : Une technique élégante qui s’appuie sur un modèle secondaire très léger pour proposer des prédictions, validées en bloc par le modèle principal. On peut ainsi accélérer la génération d’un facteur 2 à 3.
Le problème fondamental : la double nature de l’inférence
Au début, je pensais bêtement qu’il suffisait d’ajouter des GPU pour accélérer n’importe quelle phase d’inférence. L’expérience montre qu’il faut en réalité composer avec deux régimes de fonctionnement très différents au sein de l’architecture Transformer :
- La phase de Prefill (liée à la puissance de calcul) : Le système traite l’intégralité du prompt d’entrée de manière simultanée. Cette phase s’exécute de façon très efficace sur les architectures GPU modernes car elle repose sur de grandes multiplications de matrices (GEMM) à haute intensité arithmétique. Les cœurs de calcul spécialisés (Tensor Cores) sont alors pleinement exploités.
- La phase de Decode (liée à la bande passante mémoire) : Le modèle génère les caractères ou mots suivants, un token à la fois. C’est ici que l’efficacité s’effondre. Pour chaque nouveau token produit, le processeur doit charger l’intégralité des poids du modèle ainsi que l’historique des clés et valeurs (le cache KV) depuis la mémoire haute bande passante (HBM) vers la mémoire locale ultra-rapide (SRAM). L’intensité arithmétique étant très basse, le processeur passe la majeure partie de son temps à attendre le transfert des données plutôt qu’à effectuer des calculs.
Pour évaluer l’efficacité de mes déploiements, je me concentre sur quatre indicateurs de performance essentiels :
- Le Time to First Token (TTFT) : Indique le temps nécessaire pour traiter la phase de prefill (plus il est bas, plus le système est réactif).
- Le Time Per Output Token (TPOT) : Représente le temps moyen de génération de chaque token lors de la phase de decode (à minimiser pour fluidifier la lecture).
- La vitesse de génération par utilisateur (Tokens par seconde par utilisateur) : Notre budget de latence séquentielle en direct.
- Le débit global du système (Tokens par seconde) : Mesure la capacité de traitement simultané du serveur sous forte charge.
Calculer son empreinte VRAM théorique
Avant d’importer et de charger un modèle sur mes cartes graphiques, j’estime toujours le besoin minimal en mémoire vive pour anticiper les erreurs d’allocation (le classique crash Out of Memory). J’applique la formule de calcul suivante :
VRAM Totale (Go) ≈ ((P × B) / 8) × 1.2 + Mémoire du Cache KV
Dans cette formule, P représente le nombre de paramètres du modèle en milliards, et B correspond à la précision en bits (par exemple 16 pour du format FP16, 8 pour du FP8 ou 4 pour du INT4). Le multiplicateur 1.2 introduit une marge de sécurité de 20 % pour stocker les activations mémoire et les tenseurs de travail temporaires requis par les calculs.
Gestion de la mémoire et techniques de cache
La conservation de l’historique des requêtes sous forme de clés et valeurs (le cache KV) consomme une quantité considérable de VRAM lors des conversations longues. Pour y remédier, j’exploite plusieurs mécanismes d’allocation :
- Le KV Caching standard : Consiste à stocker en mémoire les matrices de clés et de valeurs calculées lors des étapes précédentes afin d’éviter des recalculs quadratiques redondants lors de la génération.
- PagedAttention : Cette approche s’inspire du principe de la mémoire virtuelle des systèmes d’exploitation. En segmentant le cache KV en blocs non contigus, elle résout les pertes d’espace et permet de quadrupler la taille des lots de requêtes (batch size) sur le même matériel informatique.
- Le Prefix Caching : Très performant pour les architectures de génération augmentée par récupération (RAG) ou les serveurs recevant des consignes de système identiques. Il permet de réutiliser le cache KV précalculé associé aux parties de prompt partagées.
- La compression de contexte : Des outils comme LLMLingua trient les tokens du prompt pour supprimer les éléments les moins informatifs avant de lancer l’inférence.
Sur les appareils locaux ou embarqués (mobiles, ordinateurs portables), le stockage à long terme de l’historique sature rapidement le matériel. J’utilise alors deux stratégies spécifiques :
- Le cache KV quantifié : Compresse les clés et les valeurs stockées en les passant de la précision FP16 à INT8 ou INT4, ce qui divise par deux ou quatre l’espace mémoire nécessaire pour l’historique de discussion.
- Le Rolling Buffer Cache : Définit une fenêtre limite fixe (par exemple, 2 000 tokens maximum) et évince les données de contexte les plus anciennes pour éviter que le système ne sature.
Optimiser l’architecture et les calculs du Transformer
Au-delà de la configuration logicielle externe, on peut intervenir sur les équations mathématiques de l’attention et la structure interne du réseau pour gagner en rapidité.
- FlashAttention : L’algorithme d’attention classique possède une complexité quadratique O(N²) par rapport à la longueur N de la séquence. FlashAttention pallie ce problème en découpant le calcul de l’attention par blocs pour les exécuter dans la mémoire SRAM locale du GPU, évitant ainsi d’écrire de lourdes matrices intermédiaires dans la mémoire HBM lente. J’emploie aujourd’hui FlashAttention-3 sur les architectures graphiques récentes (comme NVIDIA Hopper H100 ou Blackwell B200) car elle exploite directement la précision FP8 et synchronise les calculs matriciels avec le déplacement des données.
- FlashDecoding : Une déclinaison particulièrement utile pour les très longs contextes. Elle segmente la phase de calcul de prefill le long de la dimension de la séquence pour paralléliser efficacement le calcul lorsque le contexte compte plusieurs dizaines de milliers de tokens.
- Grouped-Query Attention (GQA) : Contrairement à la structure classique Multi-Head Attention (MHA) où chaque tête de requête possède sa propre tête de clé-valeur (KV), la GQA regroupe plusieurs têtes de requête pour qu’elles se partagent une seule et unique tête KV. L’empreinte physique du cache KV s’en trouve réduite d’un facteur 4 à 8.
- Mixture of Experts (MoE) : Ce système remplace les couches denses de type Feed-Forward par des ensembles d’experts configurés en parallèle. Un réseau d’aiguillage (gating) oriente chaque token vers une sélection de ces experts (par exemple, les 2 meilleurs parmi 8). Cela permet de conserver l’intelligence globale d’un modèle très large (disons 54 milliards de paramètres) tout en n’exécutant que la puissance équivalente à un modèle de 13 milliards de paramètres lors de l’inférence.
- La mise à l’échelle de position RoPE (Rotary Position Embedding) : Modifie les calculs de positionnement pour permettre à un modèle conçu pour traiter de courtes fenêtres (ex : 4 000 tokens) de digérer de très longs contextes (dépassant les 128 000 tokens) sans devoir passer par un réentraînement complet.
- Les structures sub-quadratiques et linéaires : Certains modèles de type State Space Models (SSM) comme Mamba remplacent entièrement l’attention classique par des mécanismes à état interne fixe, offrant ainsi une complexité linéaire O(N) et un temps de traitement par token constant. D’autres approches, regroupées sous le nom d’attention linéaire, modifient l’ordre des multiplications matricielles pour calculer d’abord le produit des clés et des valeurs, ramenant là aussi la complexité générale à une échelle linéaire.
Faut-il choisir la quantification ou l’élagage (pruning) ?
(Pour être tout à fait honnête, j’ai d’abord hésité à utiliser la quantification par peur de détruire la précision du modèle, mais la réalité pratique m’a vite détrompé). Ces deux stratégies réduisent la taille du modèle mais répondent à des problématiques matérielles différentes :
| Caractéristique | Quantification | Élagage (Pruning) |
|---|---|---|
| Mécanisme de base | Réduit le nombre de bits utilisés pour représenter les valeurs. | Supprime les connexions ou couches considérées comme superflues. |
| Réduction de la VRAM | Hautement prévisible (ex: un ratio fixe de 4x en passant en 4-bit). | Variable (dépend directement du niveau de parcimonie appliqué). |
| Matériel requis | Nécessite des cœurs de calcul compatibles avec les formats réduits. | Nécessite un matériel adapté au calcul matriciel creux. |
| Perte de précision | Faible à modérée si on emploie des algorithmes comme AWQ ou GPTQ. | Élevée si l’élagage est structurel ; faible s’il est non structuré. |
| Étape d’application | S’applique après l’entraînement ou en cours d’apprentissage. | Requiert d’importantes sessions de réentraînement et d’ajustement. |
La quantification en détail : Elle convertit les poids et les activations d’un format haute précision (comme le FP16) vers des représentations plus légères (INT8, FP8, ou INT4/FP4). Cela soulage immédiatement la bande passante mémoire lors de la phase de decode. Des techniques comme AWQ (Activation-aware Weight Quantization) repèrent et protègent la fraction de 1 % des poids les plus cruciaux pour maintenir les performances du réseau. Dans le cas du Quantization-Aware Training (QAT), on simule le bruit lié à l’arrondi directement pendant la phase d’apprentissage pour que le modèle s’y accomode. De plus, pour gérer les anomalies de valeurs dans les activations, la co-quantification poids-activation harmonise les échelles de calcul dynamiquement.
L’élagage (Pruning) en détail : Cette méthode vise à éliminer les éléments inutilisés du réseau. L’élagage non structuré consiste à mettre à zéro des poids individuels de faible valeur partout dans le modèle. Cela crée cependant des matrices creuses irrégulières que les processeurs de calcul dense peinent à exploiter, sauf matériel très spécifique. L’élagage structuré, à l’inverse, supprime des structures complètes (des têtes d’attention entières ou des couches complètes, processus aussi appelé Layer Pruning). Les matrices générées restent denses et plus petites, garantissant des gains de performance matérielle immédiats, au prix d’une altération marquée du modèle qui impose de longues phases de finetuning.
Dans la grande majorité des cas pratiques, je commence systématiquement par la quantification. C’est l’approche la plus rapide à mettre en œuvre en production sans nécessiter de réapprentissage. Le pruning s’avère plus utile lors de la conception initiale d’un modèle léger sur mesure (SLM) ou juste avant d’entamer un processus de distillation de connaissances pour instruire un modèle étudiant à partir des prédictions d’un modèle enseignant.
Planification dynamique et mise à l’échelle du système
L’optimisation ne s’arrête pas à la structure interne du réseau. L’orchestration des requêtes au niveau de l’infrastructure de service s’avère tout aussi déterminante.
- Le Continuous Batching : À la différence d’un lot statique traditionnel qui se cale sur la vitesse de la requête la plus lente du groupe, cette planification travaille à la granularité du token pour ajouter ou retirer des requêtes de manière fluide, maintenant un taux d’utilisation du GPU proche de 100 %.
- La désagrégation Prefill-Decode : Sachant que le prefill demande du calcul brut alors que le decode réclame un accès mémoire rapide, cette architecture sépare physiquement les tâches. On confie le traitement du prompt à des nœuds équipés de puces rapides pour le calcul, puis on transmet le cache KV à des serveurs dotés de grandes capacités de stockage pour gérer la phase de decode.
- Le routage de modèles : Un classificateur intermédiaire évalue le prompt pour déterminer son niveau de complexité et redirige les requêtes simples vers des endpoints de calcul plus petits et économiques.
- Le parallélisme de modèles : Pour faire tourner les très grands réseaux, on sépare les calculs. Le parallélisme de pipeline découpe les couches du modèle sur plusieurs GPU successifs, tandis que le parallélisme de tenseur divise directement les grandes opérations matricielles au sein d’une même couche entre plusieurs GPU connectés.
Ma boîte à outils d’optimisation
Je m’appuie sur une sélection d’outils éprouvés selon les cas de figure et les environnements de déploiement visés.
Pour le déploiement local et la quantification sur le “Edge”
L’inférence en local sur ordinateur de bureau, smartphone ou carte embarquée requiert d’adapter l’empreinte mémoire aux contraintes du matériel.
- AutoAWQ / AutoGPTQ : Des bibliothèques Python que j’utilise fréquemment pour compresser des modèles en précision 4-bit après leur entraînement.
- llama.cpp : Un moteur d’exécution codé en C/C++ hautement performant. Il exploite le format de fichier GGUF et permet d’exécuter des modèles sur CPU et GPU grand public via Apple Metal, Vulkan ou CUDA. Il supporte les “k-quant” mixtures (comme Q4_K_M) qui appliquent des taux de quantification variables selon le rôle stratégique de chaque couche.
- ExLlamaV2 : Une solution logicielle spécifiquement conçue pour faire tourner très rapidement des fichiers au format EXL2 sur des cartes graphiques NVIDIA grand public.
- Les modèles légers (SLM) : Souvent, utiliser des modèles de taille réduite (moins de 3 milliards de paramètres) extrêmement compétents comme Microsoft Phi-3/Phi-4, Llama 3.2 1B/3B ou Google Gemma 2B donne de meilleurs résultats que d’essayer de trop compresser un modèle massif.
Pour l’inférence Cloud à haut débit
- vLLM : C’est ma solution de référence pour le déploiement sur serveurs. Elle intègre nativement PagedAttention, le continuous batching et prend en charge l’exécution directe des formats FP8 et INT4.
- TensorRT-LLM : La bibliothèque d’optimisation propriétaire développée par NVIDIA. Elle permet d’atteindre des niveaux de performance élevés sur leurs architectures grâce à des fusions d’opérations au niveau du noyau (kernel fusions) et une gestion fluide du multi-GPU.
- Hugging Face TGI (Text Generation Inference) : Un serveur prêt pour l’intégration en production, très bien conçu pour déployer les architectures populaires comme Llama ou Mistral. Il prend en charge le décodage spéculatif, le cache de préfixes et la lecture fluide de tenseurs (safe-tensor streaming).
Pour les terminaux mobiles et la compilation de graphes
- ExecuTorch : Le framework officiel proposé par Meta pour porter des modèles PyTorch sur iOS et Android en tirant parti des puces NPU et des processeurs DSP locaux.
- MLC LLM : Un compilateur universel d’inférence capable de faire tourner des modèles sur diverses plateformes en s’appuyant sur WebGPU, Vulkan ou CoreML, ce qui est très pratique pour exécuter des calculs directement dans un navigateur ou au sein d’applications multiplateformes.
- ONNX Runtime Mobile : L’environnement multiplateforme développé par Microsoft pour standardiser et accélérer l’inférence des modèles sur différents types d’architectures matérielles.
Pour les phases d’entraînement et d’ajustement
- DeepSpeed : La suite d’optimisation de Microsoft contenant de puissantes fonctions d’économie mémoire, comme le déchargement de tenseurs avec ZeRO (Zero Redundancy Optimizer).
- BitsAndBytes : Une bibliothèque très pratique pour activer la quantification 4-bit et 8-bit lors de l’apprentissage des couches, rendant l’entraînement de type QLoRA accessible sur des machines équipées d’un seul GPU grand public.