Résultats
Les benchmarks actuels sont sur le site https://gguf-bench.com/
On notera cependant plusieurs choses:
- Les benchmarks arc et ifeval sont assez saturés, et les résultats ne permettent pas de différenciers les quants
- D’autres benchmarks sont en préparation (Notamment un extrait de MMLU-Pro)
- Les résultats bruts sont sur github: https://github.com/yrougy/llm-quant-bench
Notes pour futur article — À rédiger
1. La saturation des benchmarks classiques
Les benchmarks standards — IFEval, HumanEval, GSM8K et ARC-Challenge — sont devenus peu discriminants sur les modèles locaux récents. Les scores plafonnent : la plupart des quantizations d’un même modèle obtiennent des résultats quasi identiques, ce qui rend ces métriques inutiles pour différencier la qualité des quants entre eux.
Ce n’est pas que les benchmarks soient mauvais en soi — ils ont eu leur utilité — mais ils ont été trop utilisés comme cibles d’optimisation lors de l’entraînement, au point que la plupart des modèles frontiers les “résolvent” presque parfaitement.
Sources à intégrer :
- ARC-Challenge leaderboard (HuggingFace)
- Discussion sur la saturation des benchmarks NLP : BIG-bench paper
2. Ce que ça dit de l’évolution des modèles
Il y a 18 mois, des modèles comme LLaMA-2 13B ou Mistral 7B avaient des scores ARC-Challenge autour de 55-60%. Aujourd’hui, des modèles de taille similaire (voire plus petits en paramètres actifs, comme les MoE) dépassent 85-90%.
Ce phénomène illustre deux choses :
- L’amélioration réelle des architectures et des données d’entraînement (mixture of experts, meilleure qualité des corpus, RLHF/DPO affinés)
- L’effet de “goodharting” : quand un benchmark devient une cible, il cesse d’être une mesure fiable
→ Les modèles frontier d’il y a 6 mois (GPT-4 Turbo, Claude 3 Opus) sont maintenant concurrencés ou dépassés sur certains axes par des modèles locaux de quelques milliards de paramètres. C’est une rupture importante.
Sources :
- Scaling Laws (Chinchilla)
- Comparatif historique sur les leaderboards HuggingFace
3. Pourquoi MMLU-Pro (version limitée à 2800 questions)
MMLU-Pro est une version renforcée de MMLU avec des questions plus difficiles, des distracteurs de meilleure qualité, et une couverture disciplinaire élargie (14 domaines vs 57 pour le MMLU original).
Pour mes runs, j’utilise un sous-ensemble de 2800 questions (environ 200 par domaine), ce qui représente un bon compromis :
- Résultats statistiquement représentatifs (marge d’erreur faible)
- Durée de run acceptable selon la puissance de la machine
- Comparaison équitable entre modèles si le même sous-ensemble est utilisé à chaque fois
Le MMLU-Pro complet (~12 000 questions) serait plus précis mais représente un coût en temps prohibitif pour des évaluations régulières.
Sources :
4. Reproductibilité : temperature=0 et seed fixe
Tous les runs sont effectués avec :
temperature=0— élimine le caractère stochastique du sampling, le modèle choisit toujours le token le plus probableseeddéterminée (ex:seed=42) — garantit que les runs identiques produisent des résultats identiques
Cela permet de :
- Comparer des runs sur des périodes différentes sans bruit statistique
- Débugger les anomalies : si un score change entre deux runs sur le même modèle avec les mêmes paramètres, c’est un signal d’un problème de configuration
- Publier des résultats reproductibles que n’importe qui peut vérifier
Note : certains modèles avec du “thinking” (chaîne de raisonnement interne) peuvent se comporter différemment même à temp=0 selon si le thinking est activé ou non — à documenter.
5. La complexité cachée du setup — Ce que les benchmarks publics ne montrent pas
C’est probablement la partie la plus instructive à partager : mettre en place un benchmark reproductible et fiable est loin d’être trivial.
Cas concret : Qwen3.6 35B-A3B (MoE)
En testant les différentes quantizations, un résultat contre-intuitif est apparu : le IQ2 obtenait de bien meilleurs scores que le Q3_K_M, alors qu’une quantization plus fine devrait en théorie être plus précise.
Après plusieurs heures de debugging :
- Le chat template du modèle interprétait mal un token de fin de séquence (
<|im_end|>ou équivalent) dans certaines quantizations - Résultat : le modèle continuait à générer du texte après la réponse, ou mal formater sa réponse, faussant le parsing du benchmark
- Solution : corriger explicitement le chat template dans la configuration llama-server
Désactivation du template “thinking”
Pour les modèles avec chaîne de raisonnement (Qwen3, DeepSeek-R1, etc.), llama-server peut activer automatiquement un mode “thinking” qui pousse le modèle à raisonner avant de répondre. Ce mode est intéressant pour la qualité des réponses mais incompatible avec les benchmarks MCQ standard : le modèle produit un long raisonnement avant de donner la lettre de réponse, ce qui casse le parsing.
Il faut donc désactiver explicitement le thinking template : --chat-template ou via les paramètres de génération selon la version de llama-server.
Ce que ça implique pour la fiabilité des benchmarks publiés
Beaucoup de benchmarks publiés en ligne souffrent de ces problèmes silencieux. Un score bas peut vouloir dire :
- Le modèle est effectivement moins bon
- Le chat template était mal configuré
- Le thinking était activé/désactivé à tort
- Le contexte était insuffisant pour les questions longues
→ Conclusion à développer : un benchmark fiable demande autant de rigueur dans le setup que dans l’interprétation des résultats. C’est là que la valeur ajoutée de gguf-bench.com réside — les paramètres sont documentés et reproductibles.
6. Le combo llama.cpp + lm-eval : une limitation structurelle
lm-eval (EleutherAI LM Evaluation Harness) est l’outil de référence pour les benchmarks LLM. Mais combiné à llama.cpp via son API HTTP, tous les benchmarks basés sur des QCM ne fonctionnent pas correctement.
Pourquoi : logprobs + echo
Beaucoup de tâches dans lm-eval (MMLU, ARC, HellaSwag, Winogrande…) utilisent une technique de scoring appelée log-probabilités avec echo :
- Le harness envoie la question + chaque option possible (A, B, C, D) comme completion à scorer
- Il demande au modèle : “quelle est la probabilité de ce texte complet ?” via
logprobs=Trueetecho=True - L’option avec la log-probabilité la plus haute est choisie comme réponse
Ce mode est natif dans OpenAI API, vLLM, et llama.cpp CLI natif — mais llama-server (l’API HTTP de llama.cpp) n’implémente pas correctement le paramètre echo, ou du moins ne l’expose pas de façon compatible avec lm-eval.
Résultat : soit les tâches MCQ sont silencieusement skippées, soit elles tombent en erreur, soit elles utilisent un fallback en mode génération (qui change fondamentalement la nature du test).
→ Pour des benchmarks MCQ fiables avec llama.cpp, il faut soit passer par le binaire CLI directement, soit utiliser un backend qui implémente correctement l’API (vLLM, TabbyAPI…), soit écrire ses propres scripts de scoring — ce que j’ai fait pour MMLU-Pro.
Sources :
- lm-eval documentation sur les types de tâches
- Issue llama.cpp sur echo/logprobs : à retrouver dans les issues GitHub llama.cpp
7. Les modèles instruct ne se comportent pas toujours comme prévu
Un exemple frappant : HumanEval sur Gemma 4 a produit des scores catastrophiques, complètement décorrélés des capacités réelles du modèle.
HumanEval teste la capacité à compléter des fonctions Python. Pour un modèle instruct, le problème est fondamental :
- Le modèle a été fine-tuné pour répondre à des instructions (“écris une fonction qui fait X”)
- HumanEval lui demande de compléter du code brut sans instruction explicite — un usage qui correspond davantage aux modèles base
- Le modèle instruct peut alors produire des outputs inattendus : explications en langage naturel, reformulation de la question, refus poli, etc.
Le benchmark peut être valide si le prompt est adapté au format instruct (ex: wrapper la question dans un <instruction>), mais la plupart des harness utilisent les prompts originaux pensés pour les modèles base.
→ À débugger : vérifier quel prompt template lm-eval utilise pour HumanEval, et si un prompt instruct-compatible existe en option. Les scores de Gemma 4 sont probablement récupérables avec le bon template.
8. Le coût silencieux des erreurs de configuration
C’est peut-être le point le plus frustrant à documenter, mais le plus utile pour quiconque veut reproduire ce travail :
Réaliser qu’un run entier est invalide peut représenter plusieurs jours de calcul perdus.
Sur une GTX 1070 (ou hardware équivalent), un run MMLU-Pro complet peut prendre 6 à 20h selon le modèle. Si on découvre après coup qu’un paramètre était mal configuré — thinking activé, chat template incorrect, logprobs en mode fallback silencieux — le run entier est à refaire.
Les signaux d’alerte à surveiller :
- Score anormalement bas sur une tâche où le modèle devrait performer (ex: Gemma 4 sur HumanEval)
- Score identique entre deux quantizations très différentes (ex: Q2 = Q8 → parsing cassé)
- Score trop élevé sur des tâches saturées sans avoir changé le modèle (probablement un problème de cache ou de réutilisation de résultats)
- Variance élevée entre deux runs identiques sur le même modèle → temperature n’était pas à 0
→ La leçon : valider la configuration sur un mini-run de 50-100 questions avant de lancer un benchmark complet. C’est 30 minutes perdues qui peuvent éviter 2 jours de calcul inutile.
Mais même avec cette précaution, on n’est jamais totalement à l’abri d’une surprise qui n’apparaît qu’à grande échelle. C’est exactement ce qui s’est passé avec MMLU-Pro : la configuration fonctionnait parfaitement sur les petits modèles, les mini-runs validaient tout, et c’est seulement en passant au Q3_K_M que les scores explosaient de façon inexpliquée. Le problème n’était pas visible sur les modèles légers car il était spécifique à la façon dont cette quantization particulière gérait le token de fin — un bug qui ne se manifeste qu’au-delà d’un certain seuil de complexité du modèle.
Ce genre de cas illustre que la validation croisée entre quantizations est aussi importante que la validation initiale : un setup correct sur un IQ2 ne garantit pas qu’il sera correct sur un Q4_K_M du même modèle.
8. Et pour la vitesse
Mesurer la vitesse de génération d’un LLM est plus simple. Mais cette dernière va beaucoup varier, en fonction de:
- Type de génération: code, créativité, etc
- Taille du contexte: plus le contexte se rempli, plus la génération ralenti. C’est très variable en fonction des modèles
L’outil que j’utilise est llama-benchy
uvx llama-benchy --base-url http://localhost:8002/v1 --model Qwen/Qwen3.6-35B-A3B --pp 512 2048 --tg 128 --depth 0 4096 --runs 1
- Le
--modeldoit correspondre au modèle initial sur Huggingface pour avoir le bon tokenizer, le résultat du benchmark ne voudrait rien dire avec un mauvais tokenizer (en gros c’est la table de conversion token<->nombre, un peu comme l’ASCII ou UTF-8 pour les caractères) - le
--pp 512 2048c’est la taille du prompt envoyé pour mesurer la vitesse d’analyse du prompt. Très important quand on envoie beaucoup de données au LLM - le
--tg 128c’est le nombre de token générés pour mesurer la vitesse. 128 je vois ça comme le minima - le
--depth 0 4096correspond à la taille du contexte injecté avant les mesures - le
--runs 1permet de faire des mesures plus représentatives en faisant la moyenne ou la médiane de plusieurs exécutions.
Et pour le résultat on a:
| model | test | t/s | peak t/s | ttfr (ms) | est_ppt (ms) | e2e_ttft (ms) |
|:---------------------|---------------:|--------------:|-------------:|----------------:|----------------:|----------------:|
| Qwen/Qwen3.6-35B-A3B | pp512 | 360.76 ± 0.00 | | 1422.78 ± 0.00 | 1422.00 ± 0.00 | 1422.78 ± 0.00 |
| Qwen/Qwen3.6-35B-A3B | tg128 | 24.28 ± 0.00 | 29.00 ± 0.00 | | | |
| Qwen/Qwen3.6-35B-A3B | pp2048 | 392.69 ± 0.00 | | 5218.58 ± 0.00 | 5217.81 ± 0.00 | 5218.58 ± 0.00 |
| Qwen/Qwen3.6-35B-A3B | tg128 | 23.13 ± 0.00 | 29.00 ± 0.00 | | | |
| Qwen/Qwen3.6-35B-A3B | pp512 @ d4096 | 382.32 ± 0.00 | | 12055.99 ± 0.00 | 12055.21 ± 0.00 | 12055.99 ± 0.00 |
| Qwen/Qwen3.6-35B-A3B | tg128 @ d4096 | 22.82 ± 0.00 | 28.00 ± 0.00 | | | |
| Qwen/Qwen3.6-35B-A3B | pp2048 @ d4096 | 371.18 ± 0.00 | | 16556.27 ± 0.00 | 16555.49 ± 0.00 | 16556.27 ± 0.00 |
| Qwen/Qwen3.6-35B-A3B | tg128 @ d4096 | 21.59 ± 0.00 | 29.00 ± 0.00 | | | |
pp512prompt processing de 512 tokenstg128génération de 128 tokensttfrtime to first responseest_pptestimated prompt processing timee2e_ttftend to end time to first token
Voici un paragraphe qui s’insère dans le style de tes notes :
9. MMLU-Pro et le piège du thinking : quand le modèle compense la quantization
L’expérience la plus instructive de cette campagne de benchmarks concerne MMLU-Pro sur Qwen3.6-35B-A3B, qui illustre à quel point les conditions d’évaluation peuvent changer radicalement les résultats — et les conclusions qu’on en tire.
Premier run : sans chat template (local-completions). Le modèle reçoit le prompt brut, sans formatage Qwen, sans mode thinking. Résultat : 49% en IQ1_M, 61% en IQ4_XS. L’amplitude entre quants est excellente (~12 points), les scores semblent discriminants. Le problème est apparu plus tard avec des quant plus élevés qui montraient des résultats de plus en plus mauvais. Une analyse des résultats est arrivée à la conclusion que le modele interprétait mal les tag de fin de chaine, et de nombreux résultats étaient compris comme vides, alors que le modèle était dans une phase de raisonnement.
Deuxième run : avec chat template et thinking activé (local-chat-completions). Le modèle reçoit son format attendu et peut raisonner dans un espace <think> avant de répondre. Résultat : 81% en IQ1_M, 82% en IQ2_M, 84% en Q3_K_M. Le score explose à la hausse, mais l’amplitude entre quants s’effondre à ~3 points — dans la marge d’erreur statistique. Le mode thinking compense la dégradation des poids : le modèle “raisonne autour” de ses trous de connaissance et arrive quand même à la bonne réponse.
Troisième run : chat template avec thinking désactivé (enable_thinking=false). On découvre alors que Qwen3.6 est un reasoning model par conception : même sans l’espace <think>, le modèle fait du chain-of-thought naturel dans sa réponse visible. C’est dans les poids, on ne peut pas l’éteindre. Les tags <think></think> apparaissent vides, mais le modèle raisonne quand même. C’est d’ailleurs dans cette configuration que le score officiel de 85.2% a été obtenu par Alibaba.
Ce que ça implique : MMLU-Pro avec un reasoning model est saturé pour la comparaison de quants. Le CoT — qu’il soit structuré ou naturel — agit comme un correcteur d’erreur qui masque l’impact de la quantization. Pour discriminer les quants sur ce type de modèle, il faut soit des benchmarks où le raisonnement ne suffit pas à compenser (MATH, où une erreur de calcul à n’importe quelle étape est fatale), soit désactiver complètement le formatage chat — au prix de mesurer le modèle hors de ses conditions d’usage réelles.
Autre point important : la comparaison avec les scores historiques des frontiers est tentante (GPT-4o scorait 72.6% sur MMLU-Pro mi-2024) mais fragile. Les conditions d’évaluation diffèrent (5-shot CoT pour GPT-4o, 0-shot pour Qwen), le risque de contamination est bien plus élevé pour un modèle entraîné deux ans après la publication du dataset, et le compute à l’inférence n’est pas comparable (des milliers de tokens de raisonnement vs une réponse directe). La tendance générale — les modèles open-weight de 2026 rivalisent avec les frontiers de 2024 — est réelle et confirmée par de multiples benchmarks indépendants, mais les comparaisons point-à-point de scores sont à prendre avec précaution.
10. Préparation MATH
- Benchmark à utiliser:
minerva_mathcar soucis de parsing avechendrycks_math - Paramètres llama-server: reasoning dans result:
/data/tensor2/llama.cpp/build/bin/llama-server \
-m "$MODEL" \
-c "$CTX"\
--top-p 0.8\
--presence-penalty 0.0\
--repeat-penalty 1.0\
--no-mmap\
--reasoning off\
--chat-template-file /data/models/templates/chat_template_no_think.jinja \
--jinja -t 6\
--temp 0.7\
--top-k 20\
-ngl 99\
--port 8050\
--ubatch-size 64 \
--batch-size 256 \
-np 1\
-fa on\
--host 0.0.0.0\
-sm tensor\
-fit off\
--metrics\
--reasoning-format none \
--spec-draft-p-min 0.0 --spec-draft-n-max 3 --spec-type draft-mtp >> ./llama-server.log 2>&1 &
- appel du harness:
seedfixée ettemperature=0pour la reproductibilité
- Un run complet -> 5000 questions
lm_eval --model local-chat-completions \
--model_args "model=${MODEL},base_url=http://localhost:8050/v1/chat/completions,api_key=EMPTY,tokenizer=${MODEL},max_length=16384" \
--gen_kwargs 'temperature=0,top_p=0.95,top_k=20,min_p=0.0,presence_penalty=0.0,repetition_penalty=1.0,max_gen_toks=8192,until=[]' \
--tasks minerva_math \
--apply_chat_template\
--batch_size 1 \
--num_fewshot 0 \
--log_samples \
--seed 1234 \
--confirm_run_unsafe_code \
--output_path "/data/benches/${MODEL_TESTED}-MATH"
Résultat du premier bench de 10h de run: C’est saturé:
(lm-evaluation-harness) yves@desk:/data/benches/scripts$ cat Qwen3.6-35B-A3B-UD-IQ2_M.gguf-MATH-log
local-chat-completions ({'model': 'Qwen/Qwen3.6-35B-A3B', 'base_url': 'http://localhost:8050/v1/chat/completions', 'api_key': 'EMPTY',
'tokenizer': 'Qwen/Qwen3.6-35B-A3B', 'max_length': 16384}), gen_kwargs: ({'temperature': 0, 'top_p': 0.95, 'top_k': 20, 'min_p': 0.0, '
presence_penalty': 0.0, 'repetition_penalty': 1.0, 'max_gen_toks': 8192, 'until': []}), limit: None, num_fewshot: 0, batch_size: 1
| Tasks |Version|Filter|n-shot| Metric | |Value | |Stderr|
|------------------------------------|------:|------|-----:|-----------|---|-----:|---|-----:|
|minerva_math | 1|none | 0|exact_match|↑ |0.0000|± |0.0000|
| | |none | 0|math_verify|↑ |0.8658|± |0.0048|
| - minerva_math_algebra | 3|none | 0|exact_match|↑ |0.0000|± |0.0000|
| | |none | 0|math_verify|↑ |0.9183|± |0.0080|
| - minerva_math_counting_and_prob | 3|none | 0|exact_match|↑ |0.0000|± |0.0000|
| | |none | 0|math_verify|↑ |0.9177|± |0.0126|
| - minerva_math_geometry | 3|none | 0|exact_match|↑ |0.0000|± |0.0000|
| | |none | 0|math_verify|↑ |0.8351|± |0.0170|
| - minerva_math_intermediate_algebra| 3|none | 0|exact_match|↑ |0.0000|± |0.0000|
| | |none | 0|math_verify|↑ |0.8007|± |0.0133|
| - minerva_math_num_theory | 3|none | 0|exact_match|↑ |0.0000|± |0.0000|
| | |none | 0|math_verify|↑ |0.8981|± |0.0130|
| - minerva_math_prealgebra | 3|none | 0|exact_match|↑ |0.0000|± |0.0000|
| | |none | 0|math_verify|↑ |0.8657|± |0.0116|
| - minerva_math_precalc | 3|none | 0|exact_match|↑ |0.0000|± |0.0000|
| | |none | 0|math_verify|↑ |0.8095|± |0.0168|
| Groups |Version|Filter|n-shot| Metric | |Value | |Stderr|
|------------|------:|------|-----:|-----------|---|-----:|---|-----:|
|minerva_math| 1|none | 0|exact_match|↑ |0.0000|± |0.0000|
| | |none | 0|math_verify|↑ |0.8658|± |0.0048|
11. Ruler
Enfin un benchmark qui ne semble pas saturer. Si les benchmarks précédents ont de bons résultats est-ce que c’est le cas avec un contexte long.
Avec le IQ1:
local-chat-completions ({'model': 'Qwen/Qwen3.6-35B-A3B', 'base_url': 'http://localhost:8050/v1/chat/completions', 'api_key': 'EMPTY', 'tokenizer': 'Qwen/Qwen3.6-35B-A3B', 'max_length': 32768}), gen_kwargs: ({'temperatu
re': 0, 'top_p': 0.95, 'top_k': 20, 'min_p': 0.0, 'presence_penalty': 0.0, 'repetition_penalty': 1.0, 'max_gen_toks': 128, 'until': []}), limit: 200.0, num_fewshot: 0, batch_size: 1
| Tasks |Version|Filter|n-shot|Metric| |Value | |Stderr|
|------------------|------:|------|-----:|-----:|---|-----:|---|------|
|ruler | 1|none | 0| 4096|↑ |0.9511|± | N/A|
| - niah_multikey_1| 1|none | 0| 4096|↑ |1.0000|± | N/A|
| - niah_multikey_2| 1|none | 0| 4096|↑ |1.0000|± | N/A|
| - niah_multikey_3| 1|none | 0| 4096|↑ |1.0000|± | N/A|
| - niah_multiquery| 1|none | 0| 4096|↑ |1.0000|± | N/A|
| - niah_multivalue| 1|none | 0| 4096|↑ |1.0000|± | N/A|
| - niah_single_1 | 1|none | 0| 4096|↑ |1.0000|± | N/A|
| - niah_single_2 | 1|none | 0| 4096|↑ |1.0000|± | N/A|
| - niah_single_3 | 1|none | 0| 4096|↑ |1.0000|± | N/A|
| - ruler_cwe | 1|none | 0| 4096|↑ |0.9205|± | N/A|
| - ruler_fwe | 1|none | 0| 4096|↑ |0.9950|± | N/A|
| - ruler_qa_hotpot| 1|none | 0| 4096|↑ |0.6600|± | N/A|
| - ruler_qa_squad | 1|none | 0| 4096|↑ |0.7892|± | N/A|
| - ruler_vt | 1|none | 0| 4096|↑ |1.0000|± | N/A|
|Groups|Version|Filter|n-shot|Metric| |Value | |Stderr|
|------|------:|------|-----:|-----:|---|-----:|---|------|
|ruler | 1|none | 0| 4096|↑ |0.9511|± | N/A|
Faisons donc un set de tests avec ruler_qa_hotpot et ruler_qa_squad
lm_eval --model local-chat-completions \
--model_args "model=${MODEL},base_url=http://localhost:8050/v1/chat/completions,api_key=EMPTY,tokenizer=${MODEL},max_length=32768" \
--gen_kwargs 'temperature=0,top_p=0.95,top_k=20,min_p=0.0,presence_penalty=0.0,repetition_penalty=1.0,max_gen_toks=128,until=[]' \
--tasks ruler_qa_hotpot,ruler_qa_squad \
--metadata='{"max_seq_lengths":[4096, 8192, 16384, 32768, 65536]}'\
--limit 200\
--apply_chat_template\
--batch_size 1 \
--num_fewshot 0 \
--log_samples \
--seed 1234 \
--confirm_run_unsafe_code \
--output_path "/data/benches/${MODEL_TESTED}-ruler"
à tester
12. BBEH
BBEH Par Google Normalement ça ne doit pas saturer !
Non intégré à lm-eval
13. La découverte de inspect_evals et inspect_ai
- Framework de benchmarks: https://inspect.aisi.org.uk/ & https://github.com/UKGovernmentBEIS/inspect_evals
- Fait par le gouvernement du Royaume Uni
- Comporte plus de tests, dont BBEH et SWE-Bench
- Plus adapté que lm-evaluation-harness pour parser les réponses d’un modèle instruct
Stratégie
- Nouveaux benchmarks moins saturés avec inspect AI
- BBEH en cours sur les 100 premiers tests (BBEH complet est trop long)
- swe-bench-mini à la place de humanEval