Aller au contenu principal

Performance

Décodage accéléré : spéculatif, MTP, EAGLE et DFlash 2

Décodage spéculatif, MTP, EAGLE-3, DFlash 2 : générer plusieurs tokens par passe sans perte de qualité — et ce que ça coûte vraiment en mémoire.

Par Antoine Michéa, Fondateur & ingénieur infrastructure · · mis à jour le

La phase de génération d’un LLM est memory-bound : le GPU passe son temps à charger les poids depuis la mémoire pour produire un seul token. Les techniques de décodage accéléré exploitent cette inefficacité pour produire plusieurs tokens par étape, sans modifier la qualité du modèle final.

Le problème : une inférence bornée par la mémoire

Lire les ~14 Go d’un modèle 7B en BF16 prend quelques millisecondes, et pendant ce temps on ne génère qu’un seul token. Les unités de calcul restent inactives plus de 95 % du temps en phase de decode. C’est encore plus marqué sur une plateforme bandwidth-bound comme un Strix Halo (128 Gio de mémoire unifiée), où le débit dépend directement de la bande passante mémoire.

L’idée commune à toutes les techniques ci-dessous : proposer plusieurs tokens d’avance, puis valider ou rejeter ces propositions en un seul forward pass du grand modèle. Ce forward coûte presque autant qu’un seul token, mais on en obtient plusieurs.

Décodage spéculatif (speculative decoding)

Le décodage spéculatif utilise deux modèles :

  • Modèle « draft » (brouillon) : petit, rapide, prédit K tokens à l’avance.
  • Modèle « target » (cible) : grand, précis, valide les K tokens en un seul forward.
1. Draft autoregressif → propose [t1, t2, t3, t4, t5]   (rapide)

2. Target en un forward → distributions [p1, ..., p6]    (un seul pass)

3. Acceptance test (rejection sampling) :
   - Si q_draft(ti) ≤ p_target(ti)  : accepter ti
   - Sinon : accepter avec proba p/q, sinon rejeter et resampler depuis p
   - On s'arrête au premier rejet, mais on garde TOUJOURS un token bonus (p6)

→ Distribution finale strictement identique à un decode classique du target

Le résultat est mathématiquement identique à un décodage classique du grand modèle : la qualité est préservée, seule la vitesse change.

Choix du couple draft / target

Target Draft recommandé Ratio params Speedup typique
Qwen3.8-27B (dense) Qwen3-0.6B ou Qwen3-1.7B ~1/40 ×2,0 – ×2,5
DeepSeek V3 (671B) MTP head intégré natif ×1,8
Llama 4 Maverick (400B) Llama 3.2 1B ~1/400 ×2,5
Qwen3.6-35B-A3B (MoE) Qwen3-1.7B dense ×1,5 (MoE déjà sparse)

Pièges à éviter :

  • Le draft doit partager le tokenizer du target, sinon conversion coûteuse.
  • Si le draft est trop précis (proche du target), on gagne peu.
  • Si le draft est trop divergent, le taux d’acceptation chute et on perd du temps.
  • Le meilleur K est souvent 4 à 8 ; au-delà, les rejets dominent.

Multi-Token Prediction (MTP)

Le Multi-Token Prediction intègre la spéculation directement dans l’entraînement : au lieu d’apprendre à prédire le token n+1, le modèle apprend à prédire n+1, n+2, n+3… simultanément via plusieurs « têtes » de prédiction.

      Backbone Transformer

   ┌──────────┼──────────┐
   ▼          ▼          ▼
Head t+1   Head t+2   Head t+3
(principal)(auxiliaire)(auxiliaire)

À l'inférence : les heads auxiliaires servent de draft « intégré »
→ pas de modèle séparé, pas de problème de tokenizer

Avantages : pas de modèle séparé à charger en VRAM, tokenizer identique par construction, distribution alignée sur le target donc taux d’acceptation élevé (80–90 %).

Notre tuning MTP en pratique

Le MTP reste l’une des optimisations les plus rentables en mono-utilisateur — mais nous ne l’utilisons plus sur notre carte principale, pour les raisons détaillées plus bas (§ Le verdict, après quatre algorithmes). Sur llama.cpp, depuis la PR #23269, on active le draft MTP intégré :

--spec-type draft-mtp --spec-draft-n-max 3

Le réglage de n_max est critique : à 3, on obtient un taux d’acceptation ~86 % et un débit qui passe de ~52 à ~73 t/s (~+40 %, soit un speedup ~1,40×). À n_max=6, l’acceptation s’effondre à 47 % et on perd le bénéfice. Et comme la procédure est mathématiquement exacte, la qualité est inchangée.

Côté vLLM (lors de nos tests sur RTX 3090), la configuration équivalente est :

--speculative-config '{"method":"mtp","num_speculative_tokens":3}'

EAGLE — Extrapolation Algorithm for Greater Language-model Efficiency

EAGLE ajoute un petit decoder dédié qui prend en entrée les features cachées du target (et pas seulement les tokens). Cette connexion riche permet un taux d’acceptation très supérieur au draft classique.

Version Année Innovation Speedup vs vanilla
EAGLE-1 2024 Tree attention + features du target ×2,7 – ×3,5
EAGLE-2 2024 Draft tree dynamique (taille adaptée) ×3,0 – ×4,5
EAGLE-3 2025 Agrégation multi-couches + scale-up ×3,5 – ×6,5

EAGLE-3 est le standard de facto en 2026 pour le serving production : supporté par TensorRT-LLM, vLLM et SGLang. Sur un modèle 70B, il atteint jusqu’à ×6,5 de speedup.

Medusa et Lookahead Decoding

  • Medusa ajoute plusieurs têtes de prédiction sur le target lui-même, en fine-tuning post-hoc. Les têtes prédisent les tokens futurs en parallèle, validés via un arbre de candidats en un seul forward. Pas de draft séparé. Speedup ×2,2 – ×3,3.
  • Lookahead Decoding (LADE) : pas de modèle additionnel ni de fine-tuning. Utilise un algorithme inspiré de l’itération de Jacobi pour deviner les tokens futurs depuis le target. Plus lent qu’EAGLE mais zéro coût d’intégration. Speedup ×1,5 – ×2,0, idéal pour les modèles non re-entraînables.

DFlash 2 : le drafter par diffusion de blocs

Toutes les techniques ci-dessus produisent leur brouillon de façon autorégressive : le drafter génère un token, puis le suivant, etc. DFlash prend le problème autrement — c’est un drafter par diffusion de blocs qui prédit un bloc entier de tokens en une seule passe, en conservant les meilleurs candidats à chaque position, un sélecteur léger traçant ensuite un chemin cohérent. Le décodage reste sans perte : en greedy, la sortie est identique à celle du modèle cible.

Les chiffres publiés sont impressionnants — jusqu’à 3,4× à concurrence 1 sur H200, avec des longueurs d’acceptation de 4 à 5,5 tokens là où un MTP intégré plafonne vers 2 à 3.

Pourquoi nous ne l’utilisons pas (encore) : la mémoire

Nous avons testé DFlash 2 sur notre RTX 5090, avec le drafter officiel du modèle. Verdict mesuré, et il est instructif :

Poste MTP intégré DFlash 2
Poids du drafter 5,53 Gio 3,73 Gio
États intermédiaires de vérification 2,53 Go 5,06 Go
Cache KV par token 32 Kio 41,9 Kio
Contexte servable au final 210 202 69 533

Le drafter est bien plus léger — mais deux surcoûts l’emportent. D’abord les snapshots d’états nécessaires à la vérification, qui croissent avec le produit (requêtes simultanées × tokens du bloc) : DFlash travaille par blocs de 8, contre 4 pour notre MTP, d’où le doublement. Ensuite le cache KV du drafter lui-même, qui a cinq couches d’attention à payer par token.

Résultat : sur 32 Go, DFlash 2 et le contexte long sont mutuellement exclusifs. C’est cohérent avec la recette officielle, qui réserve ce réglage au profil « faible latence, un seul utilisateur ». Sur un GPU de 48 Go ou plus, l’arbitrage s’inverserait probablement.

Post-scriptum, trois mois plus tard : le drafter n’était pas le bon. Un checkpoint du même drafter est paru en quantification 4 bits, à 1,36 Go au lieu de 3,85. Ces 2,5 Go d’écart sont exactement ce qui écrasait notre contexte — l’idée était bonne, c’est le format du brouillon qui ne l’était pas. Le schéma courant, à l’époque, était de quantifier agressivement le modèle cible tout en laissant le drafter en pleine précision : on optimise le gros fichier et on oublie le petit, alors que sur une carte saturée les deux se disputent la même mémoire.

Nous n’avons pas pu le rejouer pour autant, et pour des raisons qui n’ont rien à voir avec le mérite du drafter : notre moteur d’inférence ne reconnaît pas cette version de l’architecture, et son cache 4 bits est incompatible avec notre noyau d’attention. Deux murs d’outillage, pas de conception. C’est fréquent dans ce domaine : une bonne idée peut rester inaccessible pendant des mois parce que la brique qui l’implémente n’est pas encore dans votre version.

La leçon générale vaut au-delà de DFlash : un décodage spéculatif ne se juge pas à son accélération annoncée, mais à ce qu’il laisse de mémoire au contexte et à la concurrence. Une accélération de 2× qui divise le contexte par trois n’est pas une accélération, c’est un changement de produit.

Le verdict, après quatre algorithmes mesurés sur la même carte

Nous avons fini par tester, sur une même carte de 32 Go et un même modèle, les quatre familles disponibles. Voici ce que ça donne quand on mesure au lieu de lire les annonces :

Configuration Débit solo 5 flux simultanés Contexte servable Flux max
MTP intégré 119,6 t/s 422 t/s 185 000 4
Drafter 4 bits autorégressif 108,0 t/s 140 t/s 185 000 3
DFlash 2 (drafter pleine précision) 69 533
Sans aucune spéculation 88,5 t/s 426 t/s 262 144 8

Le tableau se lit dans les deux sens. En mono-utilisateur, la spéculation gagne nettement : 119,6 contre 88,5 tokens par seconde, soit +35 %. Dès cinq utilisateurs simultanés, l’écart disparaît — et le contexte servable, lui, a été divisé par 1,4.

La raison tient en une phrase : la spéculation achète de la latence avec de la mémoire. Elle est rentable quand le GPU attend, c’est-à-dire quand un seul flux tourne. Dès que plusieurs requêtes le remplissent, le continuous batching fait déjà le travail — les unités de calcul ne chôment plus — et le brouillon ne fait plus que consommer de la place.

Le coût caché : les emplacements d’état

Sur les architectures récentes dites hybrides, qui alternent attention classique et couches récurrentes, chaque requête en cours occupe un nombre fixe d’emplacements d’état. Nous avons découvert deux choses en les mesurant :

D’abord, chaque token spéculé consomme un emplacement supplémentaire par requête. Un drafter qui propose des blocs de huit tokens coûte donc neuf emplacements par requête là où une configuration à quatre tokens en coûte cinq. Sur un budget fixe, cela divise mécaniquement le nombre d’utilisateurs simultanés.

Ensuite — et c’est le plus contre-intuitif — ces emplacements sont consommés même sans aucune spéculation. Ils servent aussi au cache de préfixes, qui doit conserver l’état récurrent des conversations en cours. Autrement dit, le paramètre qui pilote réellement le parallélisme n’est pas celui qui porte le nom évident : chez nous, le réglage nommé « nombre maximum de requêtes » ne fixe rien du tout. Seul le message affiché au démarrage du serveur dit la vérité, et il faut le relire à chaque changement de configuration.

Ce que nous avons retenu

En retirant purement et simplement la spéculation, nous avons gagné 42 % de contexte et doublé le nombre d’utilisateurs simultanés, pour 25 % de débit en moins sur un flux isolé — un débit qui redevient équivalent dès le deuxième utilisateur.

C’est un résultat qui va à l’encontre du discours ambiant, où le décodage spéculatif est présenté comme un gain net. Il l’est, mais sur un seul axe, et cet axe n’est pas celui d’un service partagé. Sobre par conception, performant par nature : ici, la performance est venue d’un retrait, pas d’un ajout.

Un dernier point de méthode, appris à nos dépens : deux de ces configurations ont fonctionné plus de six heures avant de s’effondrer. Une validation de vingt minutes ne prouve rien sur ce type de plateforme — il faut mesurer sous trafic réel, sur plusieurs heures, avant de conclure quoi que ce soit.

Tableau comparatif

Technique Modèle additionnel Re-training Speedup Cas d’usage idéal
Spéculatif vanilla Petit modèle même famille Non ×2,0 – ×2,5 Setup simple, open-source
MTP intégré Heads dans le target Pré-training ×1,8 (jusqu’à ~1,4× réel) DeepSeek V3, Qwen MoE (natif)
EAGLE-3 Petit decoder spécialisé Fine-tuning du draft ×3,5 – ×6,5 Production max performance
Medusa Têtes ajoutées au target Fine-tuning léger ×2,2 – ×3,3 Compromis simplicité/perf
Lookahead (LADE) Aucun Non ×1,5 – ×2,0 Modèles non-modifiables
DFlash 2 Drafter diffusion de blocs Drafter publié par l’éditeur ×2,7 – ×3,4 (concurrence 1) Latence mono-utilisateur, GPU ≥ 48 Go

Mise en pratique

llama.cpp — spéculatif vanilla (target Qwen3.8-27B + draft Qwen3-0.6B) :

./llama-server \
    -m models/qwen3.8-27b-Q4_K_XL.gguf \
    -md models/qwen3-0.6b-Q8_0.gguf \
    --draft-max 8 \
    --draft-min 0 \
    --draft-p-min 0.6 \
    --flash-attn \
    -ngl 99 -ngld 99 \
    --port 8080

vLLM — EAGLE-3 :

from vllm import LLM

llm = LLM(
    model="Qwen/Qwen3.8-27B",
    speculative_config={
        "method": "eagle3",
        "model": "yuhuili/EAGLE3-Qwen3.6-27B",
        "num_speculative_tokens": 5,
        "draft_tensor_parallel_size": 1,
    },
)

Le décodage spéculatif correctement implémenté est strictement équivalent en distribution au décodage classique : vous gardez vos paramètres habituels (temperature, top-p, top-k). Méfiez-vous toutefois des implémentations approximatives (greedy-only, top-k tronqué côté draft) qui peuvent introduire un léger biais.

Quand le décodage accéléré n’aide pas

  • Batch size élevé en serving : les unités de calcul sont déjà saturées, le decode devient compute-bound. EAGLE garde un avantage, le spéculatif vanilla en perd.
  • MoE déjà très sparse : Qwen3.6-35B-A3B n’active que 3B de paramètres par token, le decode est déjà rapide. Le draft doit alors être < 500M.
  • Génération très créative (temperature > 1,2) : le draft prédit moins bien, l’acceptation chute.
  • Sortie structurée stricte (JSON, grammaires) : les rejets sont fréquents si le draft ignore la grammaire.

Pour aller plus loin

← Toutes les découvertes