J’ai un léger problème avec les captures de benchmarks de LLM locaux : elles affichent généralement un grand nombre, l’unité tok/s, et juste assez de contexte pour induire tout le monde en erreur.

Un modèle peut avaler un long prompt très vite et traîner pour écrire sa réponse. Il peut sembler vif dans un chat individuel puis devenir lamentable dès que plusieurs personnes sollicitent le serveur. Il peut aussi briller dans une boucle synthétique qui ne ressemble en rien à l’usage pour lequel on a acheté la machine.

Donc non, « mon modèle fait 18 tokens par seconde » n’est pas un résultat de benchmark. C’est le début d’un résultat de benchmark.

L’inférence a deux métiers

Pour les modèles autoregressifs à décodeur que nous utilisons en local, une requête comporte deux phases réellement différentes :

  1. Traitement du prompt, ou pré-remplissage : le modèle lit l’entrée et construit son cache KV.
  2. Génération, ou décodage : le modèle produit la réponse token par token et étend ce cache.

L’outil officiel llama-bench les traite comme des tests distincts : pp pour le traitement du prompt, tg pour la génération et pg pour un test combiné. Ce n’est pas de la pédanterie d’auteur d’outil. C’est le sujet entier. Les charges ne sont pas interchangeables.

Le traitement du prompt répond à : combien de temps avant le premier token possible ? La génération répond à : à quelle vitesse la réponse arrive-t-elle ensuite ? Les fusionner rend le résultat fortement dépendant de longueurs de prompt et de réponse choisies arbitrairement.

Un benchmark qui ne donne qu’un seul débit ressemble à un essai automobile qui ne donnerait que la vitesse maximale. C’est techniquement une mesure. C’est surtout une esquive.

Les deux chiffres que je veux voir

Pour un assistant local interactif, je veux, séparément :

  • le débit de traitement du prompt à des tailles d’entrée réalistes ;
  • le débit de génération à des tailles de réponse réalistes.

Ensuite je veux un essai de bout en bout qui donne la forme exacte de la requête : tokens de prompt, tokens générés, taille de contexte, taille de lot, backend, quantification et répétitions. Sans cela, le chiffre est une anecdote.

llama-bench rend cette séparation explicite. Un test de prompt seul utilise -p, un test de génération seul -n, un test combiné -pg. L’outil répète les essais et fournit une moyenne et un écart-type. C’est une base bien plus saine que de chronométrer une complétion chanceuse quand la machine ne fait rien d’autre.

# Ingestion de prompts courts, moyens et longs
llama-bench -m model.gguf -p 512,2048,8192 -n 0 -r 5

# Vitesse de décodage pour une réponse utile
llama-bench -m model.gguf -p 0 -n 256 -r 5

# La sensation d'une vraie requête
llama-bench -m model.gguf -pg 2048,256 -r 5

Ces commandes ne sont pas magiques. Elles obligent simplement à poser une question précise.

Faire correspondre le benchmark au travail

Les charges locales ne pénalisent pas toutes les mêmes échecs.

Le chat et l’assistance au code se jouent sur l’attente avant le premier token visible et sur le rythme qui suit. Une machine qui décode vite mais met une éternité à ingérer une longue conversation semble cassée, même si son score de génération est flatteur.

Le RAG, l’analyse de documents et les gros prompts système sollicitent fortement le traitement du prompt. Si le travail utile commence par plusieurs milliers de tokens récupérés, tester un minuscule prompt revient à se mentir avec un chronomètre.

L’extraction par lots peut privilégier le débit global plutôt que la latence du premier token d’un seul utilisateur. llama.cpp propose un benchmark dédié aux lots pour cette raison. Il sépare le temps de prompt de la génération et permet de tester des séquences parallèles. Une boucle mono-utilisateur ne dit rien du comportement d’un serveur sous charge.

Les agents persistants ont besoin de tout cela, plus de reproductibilité. Un agent qui lit de gros résultats d’outils, écrit une action courte et recommence n’est ni du chat pur ni de la génération en masse. Mesurez la boucle que vous exécutez réellement.

Il n’existe donc pas de « meilleure vitesse de modèle local » universelle. Il existe des modèles et des machines rapides pour une distribution de requêtes explicitement décrite. Réponse agaçante ? Oui. Réponse honnête ? Aussi.

Ce qu’un résultat crédible doit contenir

Je rejetterais tout benchmark local sans :

  • le fichier modèle exact et sa quantification ;
  • le moteur d’inférence et le backend ;
  • les nombres de tokens de prompt et de génération ;
  • la fenêtre de contexte et la taille de lot ;
  • les répétitions et la variance ;
  • la nature de la mesure : prompt, décodage ou bout en bout ;
  • la concurrence, si l’on prétend mesurer un serveur.

Il reste un piège. llama-bench documente que ses mesures n’incluent ni la tokenisation ni l’échantillonnage. C’est très bien pour un benchmark ciblé du moteur. Mais son chiffre n’est pas toute l’expérience mur-à-mur d’une requête applicative. HTTP, mise en file, formatage du prompt, tokenisation, échantillonnage, streaming et contention vivent hors de cette valeur propre.

N’abusez pas d’une mesure précise parce qu’elle est commodément incomplète.

Une fiche de benchmark à laquelle je crois

Modèle : <fichier GGUF exact et quantification>
Moteur/backend : <build et backend exacts>
Contexte / lot : <valeurs>
Répétitions : 5

Traitement du prompt : prompt de 2 048 tokens, moyenne ± écart-type
Génération : décodage de 256 tokens, moyenne ± écart-type
Bout en bout : prompt de 2 048 tokens + réponse de 256 tokens, moyenne mur-à-mur ± écart-type
Concurrence : 1 (ou charge serveur explicitement indiquée)

Cette fiche est moins spectaculaire qu’une énorme image tok/s. Tant mieux. Elle est réutilisable, comparable et difficile à fausser par accident.

Je ne suis pas contre les tokens par seconde. Je suis contre l’idée qu’il n’en existe qu’une variété. L’ingestion du prompt et le décodage token par token sont deux métiers différents ; un benchmark utile donne un score à chacun.

Le reste, ce sont deux benchmarks sous un trench-coat.

Sources