Accueil / Articles

Décodage spéculatif, sortie précoce et quantification : un décodage des LLM plus rapide

Comment le décodage spéculatif et la sortie précoce réduisent la latence d’inférence des LLM, lorsque l’acceptation préliminaire s’avère bénéfique, et comment ils s’intègrent avec la quantification et le pagination du cache KV.

Éditeur : REACTAPP.TOP8 min de lecture1877 mots

Sources: **Foundations of LLM Inference Optimization: Speculative Decoding and Early Exit | Part 3D**

Le déroulement de l’inférence des LLM se fait en deux phases. La phase de préremplissage traite l’ensemble du prompt en parallèle et peut maintenir une GPU occupée. La phase de décodage émet un token à la fois, et chaque token dépend du précédent, ce qui fait que le matériel reste principalement en attente. C’est cette dépendance séquentielle qui explique pourquoi la latence interactive reste élevée, même lorsque le nombre de FLOPs semble important sur papier.

Prefill phase:
  Input: [token_1, token_2, ..., token_512]  → all 512 tokens processed in parallel
  Matrix shape: [batch, 512, 4096]
  GPU utilization: high — large matrix, full Tensor Core throughput

Decode phase (one step):
  Input: [token_513]  → one token processed
  Matrix shape: [batch, 1, 4096]
  GPU utilization: low - tiny matrix, most CUDA cores idle

Pourquoi le décodage se trouve sur le chemin critique

À chaque étape de décodage, le modèle effectue des requêtes sur un cache KV en expansion. Le travail par étape augmente avec la longueur du contexte, et dans le cas des conversations, il n’est pas possible d’effectuer des traitements en lot librement, car les objectifs de performance en termes de latence limitent le temps pendant lequel une requête peut attendre une réponse de la part de l’entreprise. Les calculs montrent à quel point une seule étape utilise peu de ressources du processeur :

One attention head, decode step:

Query Q: [8, 1, 128]   →  8 × 128 = 1,024 elements
Keys  K: [8, 2048, 128] →  8 × 2048 × 128 = 2M elements
Values V: [8, 2048, 128] →  same

Matrix multiply (QKᵀ) per head:
  Shape: [8, 1, 128] × [8, 128, 2048] → [8, 1, 2048]
  FLOPs: 8 × 1 × 128 × 2048 ≈ 2.1M FLOPs per head

For 32 heads: ≈ 67M FLOPs per layer
For 32 layers: ≈ 2.1B FLOPs total per decode step

A100 peak at BF16: ~312 TFLOPS = 312 × 10¹² FLOPs/sec

Time to execute 2.1B FLOPs at 100% utilization:
  2.1 × 10⁹ / 312 × 10¹² ≈ 0.0067 ms

Actual observed decode latency per step: ~10–30ms

Effective compute utilization: < 0.1%

Un taux d’utilisation efficace inférieur à 0,1 % signifie que la décodage est limité par le trafic mémoire et le nombre d’étapes, et non par les capacités de calcul brutes. Il existe deux façons d’y remédier : produire davantage de tokens acceptés par étape coûteuse, ou effectuer moins de travail par étape. La décodage spéculatif fait le premier, tandis que l’arrêt précoce fait le second.

Décodage spéculatif : ébauche, puis vérification

Un petit modèle d’ébauche propose plusieurs tokens futurs. Le modèle cible les vérifie tous en une seule passe en avant parallèle, conserve le préfixe le plus long qui correspond, et effectue un rééchantillonnage dès la première réjection. Chaque token d’ébauche accepté correspond à un token pour lequel vous n’avez pas payé le coût complet d’une étape cible.

Without speculative decoding:
  Generate 5 tokens: 5 × 30ms = 150ms

With speculative decoding (K=4):
  Draft 4 tokens: 4 × 1.5ms = 6ms
  1 target verification pass: ~35ms  (slightly longer than decode,
                                       processes K+1=5 positions)
  Expected accepted tokens per round:
    (1 - 0.80^5) / (1 - 0.80) ≈ 3.36 tokens
  Time per round: 6ms + 35ms = 41ms
  Time per token: 41ms / 3.36 ≈ 12.2ms
Speedup: 30ms → 12.2ms ≈ 2.5×

La vitesse d’exécution dépend entièrement de la fréquence à laquelle le brouillon et le texte cible coïncident. Un texte simple à faible entropie accepte de longs brouillons ; un token inattendu raccourcit rapidement la séquence. Voici le cycle sous forme conceptuelle (les implémentations réelles gèrent également le cache KV entre les deux modèles) :

import torch
import torch.nn.functional as F

def speculative_decode(target_model, draft_model, input_ids,
                       max_new_tokens, K=4, temperature=1.0):
    """
    Conceptual speculative decoding loop.
    Real implementations handle KV cache management across both models.
    """
    generated = input_ids.clone()
    while generated.shape[1] - input_ids.shape[1] < max_new_tokens:
        # --- Draft phase ---
        draft_tokens = []
        draft_probs = []
        draft_input = generated.clone()
        for _ in range(K):
            with torch.no_grad():
                draft_logits = draft_model(draft_input).logits[:, -1, :]
            q = F.softmax(draft_logits / temperature, dim=-1)
            token = torch.multinomial(q, num_samples=1)
            draft_tokens.append(token)
            draft_probs.append(q)
            draft_input = torch.cat([draft_input, token], dim=1)
        # --- Verify phase: one target forward pass over all K+1 positions ---
        verify_input = torch.cat([generated] + draft_tokens, dim=1)
        with torch.no_grad():
            target_logits = target_model(verify_input).logits
        # target_logits[:, -K-1:, :] covers all K draft positions + bonus
        # --- Accept/reject ---
        accepted = 0
        for i in range(K):
            p = F.softmax(target_logits[:, -(K+1)+i, :] / temperature, dim=-1)
            q = draft_probs[i]
            token = draft_tokens[i]
            # Acceptance probability
            accept_prob = torch.min(
                torch.ones_like(p.gather(1, token)),
                p.gather(1, token) / (q.gather(1, token) + 1e-9)
            )
            if torch.rand(1) < accept_prob:
                generated = torch.cat([generated, token], dim=1)
                accepted += 1
            else:
                # Sample corrected token and stop this round
                corrected_dist = F.relu(p - q)
                corrected_dist = corrected_dist / corrected_dist.sum(dim=-1, keepdim=True)
                corrected_token = torch.multinomial(corrected_dist, num_samples=1)
                generated = torch.cat([generated, corrected_token], dim=1)
                break
        else:
            # All K accepted - take bonus token
            bonus_logits = target_logits[:, -1, :]
            p_bonus = F.softmax(bonus_logits / temperature, dim=-1)
            bonus_token = torch.multinomial(p_bonus, num_samples=1)
            generated = torch.cat([generated, bonus_token], dim=1)
    return generated

Choisissez, si possible, un brouillon de la même famille, comme un sous-ensemble distillé ou quantifié, et mesurez le taux d’acceptation sur votre propre trafic plutôt que sur des exemples de blogs publics.

Lorsque les brouillons s’éloignent

Si la distribution du brouillon s’écarte de celle de la cible, le taux d’acceptation chute et vous payez le coût du brouillon pour presque rien. Surveillez en continu le taux d’acceptation et revenez à une décodage simple lorsque celui-ci diminue.

Vérification rapide avec des chiffres arrondis : disons qu’une étape cible coûte 10 ms et que le brouillon propose 5 tokens, dont environ 3 sont acceptés en moyenne (60 %). Le coût par token accepté diminue par rapport aux étapes à un seul token, même après les frais liés au brouillon, tant que le taux d’acceptation reste élevé. Si ce taux tombe à environ un token, le système devient inefficace. C’est cette sensibilité qui fait que les tableaux de bord valent mieux que les anecdotes.

Sortie anticipée : sautez les couches profondes pour les tokens simples

Certaines architectures permettent à un token de quitter le réseau à un niveau intermédiaire dès que le degré de confiance est élevé, ce qui permet d’économiser des ressources de calcul pour les ponctuations, les articles et autres tokens prévisibles.

Layer distribution of exits:Exit at layers 1–8  (very easy tokens like punctuation, articles):  15%
Exit at layers 9–16 (medium tokens, common continuations):           35%
Exit at layers 17–24 (harder tokens, named entities, numbers):       30%
Exit at layers 25–32 (full computation required):                    20%Weighted average layers executed:
  0.15 × 6 + 0.35 × 12 + 0.30 × 20 + 0.20 × 32
  = 0.90 + 4.20 + 6.00 + 6.40
  = 17.5 layers averageSpeedup vs always running 32 layers:
  32 / 17.5 ≈ 1.83×

Contrairement à la décodage spéculatif, la sortie anticipée modifie bien les résultats en sautant des couches, comme c’est son but. Elle sacrifie la qualité pour la vitesse et dépend fortement du degré de calibration de la confiance, il convient donc d’estimer l’écart d’exactitude et de le mesurer.

Empilement avec quantification, lotification et cache KV

La décodage spéculatif modifie le cycle de proposition de tokens en ajoutant un deuxième modèle ; la sortie anticipée modifie la profondeur à l’intérieur d’un seul modèle. Elles rencontrent des goulets d’étranglement similaires mais de manières différentes, et toutes deux peuvent parfois être combinées avec la quantification, la lotification continue et le pagination du cache KV :

Combined stack example:

Target: 7B model, BF16, FlashAttention, PagedAttention
  → Model: ~14 GB, memory-efficient attention, paged KV cache

Draft: 70M model, INT4, FlashAttention
  → Model: ~35 MB, near-zero memory overhead

Speculative decode:
  → with K=4, α=0.80
  → ~2.5× token generation speedup on long outputs

Full stack speedup vs FP32 no-optimization baseline:
  - Quantization: 2–2.3× throughput
  - FlashAttention: 20–40% attention latency reduction
  - Speculative decoding: 2–2.5× decode speedup (on eligible requests)

Combined: 5–8× improvement in end-to-end tokens/second

Le regroupement des tâches est essentiel. La longueur des brouillons interagit avec les hypothèses d’un planificateur de lot continu, et un planificateur inefficace fragmente les lots, ce qui nuit à l’utilisation des ressources. Les politiques d’éviction de la mémoire cache KV doivent être ajustées afin que les conversations longues ne provoquent pas de surcharge. Le cacheage des prompts côté préremplissage est également important, sinon on « corrige la décodage » au détriment d’un préremplissage inefficace. De plus, combiner une sortie précoce avec une quantification agressive peut rapidement augmenter les hallucinations. Une conception de service cohérente vaut mieux qu’un algorithme issu d’une étude scientifique appliqué directement dans le flux principal.

Quand chaque technique est utile

La décodage spéculative est efficace lorsque l’accord entre les brouillons est élevé et que les étapes cibles dominent la latence. Elle échoue lorsque les brouillons ne correspondent que rarement ou lorsque les coûts liés aux brouillons annulent les avantages. La sortie précoce est utile lorsque de nombreux tokens sont faciles à traiter et que le budget de précision le permet ; elle échoue pour les tokens difficiles ou lorsque la confiance dans les résultats n’est pas correctement calibrée.

Exemple concret : un modèle cible de 7 milliards de paramètres servant à des conversations avec des réponses de 50 à 150 tokens. Préremplir un prompt système de 2 000 tokens est coûteux mais se produit rarement par tour, tandis que les étapes de décodage sont celles qui provoquent le retard ressenti par les utilisateurs. Réduire le nombre d’étapes de décodage grâce à des acceptations spéculatives, ou la charge par étape grâce à une sortie anticipée, c’est ce que les utilisateurs perçoivent réellement.

Est-ce important pour de petits modèles exécutés sur le dispositif ? Généralement moins que pour des modèles cibles de grande taille. Le prototype peut-il fonctionner sur CPU ? Parfois ; il faut le mesurer. En termes de priorités, corrigez d’abord le regroupement des requêtes et le stockage en mémoire cache, puis ajoutez le décodage spéculatif, et enfin l’ sortie anticipée si votre stack le permet.

Intégration dans l’inférence de LLM en production

  • Obtenez d’abord des valeurs de référence pour la latence p95 et la qualité avant de modifier quoi que ce soit.
  • Colocalisez le modèle en version bêta et le modèle cible sur le même nœud. Les versions bêta exécutées sur des hôtes différents génèrent des fluctuations réseau qui peuvent annuler les avantages obtenus, et deux modèles ainsi que le cache KV peuvent épuiser la mémoire d’un système qui pouvait aisément en héberger un seul.
  • Enregistrez les histogrammes d’acceptation, la longueur moyenne des textes acceptés, les écarts d’évaluation de qualité, l’utilisation de la GPU et la latence p95.
  • Surveillez l’équilibre entre la CPU et la GPU lorsque les versions bêta sont exécutées sur des appareils différents.
  • Vérifiez la qualité des résultats en mode A/B pour des ensembles de données factuelles. Avant un déploiement global, exécutez des prompts fixes sur des ensembles de tests factuels, de codage et de refus, puis comparez les taux de résultats identiques en termes de tokens lorsque la décodage spéculatif est activé pour assurer une correspondance parfaite des distributions. Pour une sortie anticipée, suivez le taux de réussite sur les tâches évaluées et les préférences humaines là où elles sont disponibles.
  • Mettez chaque optimisation derrière un indicateur de fonctionnalité doté d’un interrupteur de désactivation permettant de revenir à la décodage standard, et répétez régulièrement les procédures de réversion.
  • Revoyez tout après toute modification apportée au tokenizer ou à la fine-tuning.

Un décodeur spéculatif correctement implémenté devrait correspondre à la distribution cible, mais vérifiez-le à l’aide de tests croisés plutôt que de vous en fier aveuglément. Utiliser un brouillon non pertinent, ignorer l’impact de la température sur le taux d’acceptation, ou proclamer la victoire sur la base de benchmarks de débit sans contrôles de qualité sont les méthodes habituelles qui entraînent des erreurs, et chacune d’elles est mesurable.

Les équipes de plateforme doivent gérer ces optimisations au niveau de la couche de service afin que chaque équipe d’application ne conçoive pas son propre système de sélection de brouillons. Coordonnez-vous avec les responsables de la maintenance de la plateforme plutôt que de modifier directement le code de l’application ; exposez des en-têtes ou des attributs de suivi indiquant si un décodage spéculatif ou une sortie anticipée a été utilisé, et étiquetez les coûts liés aux tokens selon l’optimisation afin que le service financier puisse en mesurer les avantages.

Le goulot d’étranglement qui reste

Lorsque le décodage deviendra plus rapide, les utilisateurs pourront encore être en attente des appels aux outils, du chargement des données ou du rendu du Markdown côté client. La limite restante peut provenir de la bande passante mémoire, du réseau ou du client, et non des FLOPs ; il convient donc d’analyser le processus de bout en bout et de réaliser des profils avant d’appliquer tous les trucs décrits dans les articles. Le décodage spéculatif ne remplace pas non plus un bon système de récupération des données ou de bons prompts ; il ne fait qu’abaisser le coût des tokens suivants lorsque le modèle sait déjà ce qu’il veut dire.

Le décodage séquentiel représente la contrainte structurelle inhérente à la génération autoregressive. Le décodage spéculatif et la sortie anticipée réduisent cette contrainte dans des conditions mesurables ; il faut donc les intégrer comme n’importe quelle fonctionnalité de production : avec des métriques, des indicateurs, des mécanismes de rollback et un responsable clair au sein de l’équipe de la plateforme de déploiement.

Lectures complémentaires

  • L’inférence LLM du début à la fin : préremplissage, décodage, cache KV et échantillonnage : Une présentation adaptée à un tableau blanc sur le traitement des tokens en flux continu, les raisons pour lesquelles le préremplissage est limité par les ressources de calcul, le décodage par la bande passante, ainsi que la manière dont GQA, le pagination et la spéculation modifient les aspects économiques.
  • Le cache KV est la base de données invisible qui consomme la mémoire GPU lors du décodage des LLM : Préremplissage versus décodage, mathématiques du cache KV, GQA/MQA, PagedAttention, quantification et éviction pour l’inférence en production.