NOPE LinkedIn

Catégories:
IA
Performance

Benchmarker llama.cpp sur CPU : ce qu'on apprend en 50 runs

Benchmarker llama.cpp sur CPU : ce qu'on apprend en 50 runs image

Rubrique: IA Rubrique: Performance Tag: llama.cpp Tag: ik_llama.cpp Tag: benchmark Tag: cpu Tag: quantization Tag: openblas Tag: avx2 Tag: avx512 Tag: zen2 Tag: zen3 Tag: skylake Tag: self-hosted

Benchmarker llama.cpp sur CPU : ce qu’on apprend en 50 runs

Résumé Exécutif

Pour les besoins d’un PoC SOC agentique CPU-only, j’ai fait tourner ~50 benchmarks llama-bench sur 4 plateformes différentes (un Ryzen 5 3600 bare-metal, sa contrepartie FreeBSD, un EPYC Milan dedicated chez Hetzner, un Xeon Skylake shared chez Hetzner). Modèle de référence : Qwen 2.5 3B Q4_K_M et son grand frère 7B. Builds comparés : llama.cpp (tag b3813 et b9165) et son fork agressif ik_llama.cpp (tag 4503).

Sept résultats m’ont surpris. Aucun n’est “ce que l’intuition souffle”. Si vous faites tourner un LLM sur CPU — pour de l’edge, du self-hosted, du souverain, ou simplement parce que vous n’avez pas de GPU — ces sept points peuvent vous épargner des heures de tuning à l’aveugle et plusieurs centaines d’euros de matériel mal choisi.

Coût total de la session : 0,06 € de Hetzner. Tous les chiffres sont reproductibles, le wrapper de bench et le CSV brut sont publiés à la fin de l’article.

1. Le contexte

Mon SOC agentique asp-forge tourne sur du CPU-only. Le LLM (Qwen 2.5 3B en Q4_K_M, plus quelques LoRA) sert à analyser des alertes Wazuh et T-Pot, et à proposer des actions à un agent qui pilote un OPNsense. Pas de GPU dans la boucle — pour des raisons à la fois de coût (le hardware reste un Ryzen 5 3600 d’occasion à ~120 €) et de souveraineté (toutes les données restent locales).

Pendant des mois, j’ai utilisé une version pinée de llama.cpp — le tag b3813 — parce qu’un bug ultérieur cassait le chargement dynamique des LoRA WireGuard que j’avais entraînés. C’était stable, ça marchait, je n’avais pas regardé plus loin.

Puis, en préparant la dernière démo, je me suis posé la question naïve : “Et si je rebuilder ?” Une heure plus tard, j’avais construit une VM dédiée aux builds (breach-1-llm-lab, 8 vCPU, 8 GB), une autre VM FreeBSD pour les comparaisons natives (oaf-build-freebsd), et lancé le premier llama-bench de ma vie.

Au bout de 50 runs, j’avais sept convictions secouées. Voici lesquelles.

2. Méthode

Pour que les chiffres soient comparables entre eux, j’ai défini une référence stricte :

Paramètre Valeur
Modèle Qwen 2.5 3B Q4_K_M (et 7B Q4_K_M pour les tests grand modèle)
Outil llama-bench distribué avec llama.cpp (et avec ik_llama.cpp)
Args défaut -p 256 -n 64 (256 tokens prompt-processing, 64 tokens token-generation)
Threads sweep 2 à 12 selon la machine, sweet-spot identifié par bench
Backend CPU pur, pas de GPU, pas de Vulkan
Compilateur Clang 14, ccache pour les rebuilds
Flag clé -DGGML_NATIVE=ON (active -march=native)

Chaque run exécute 5 itérations et reporte la moyenne ± écart-type. J’ai écrit un wrapper run-bench.sh qui parse la sortie de llama-bench et l’écrit en CSV, pour pouvoir agréger sans re-typer les résultats. Le wrapper est dans le repo asp-forge (docs/llm-bench-cpu-2026-05.csv).

Les quatre plateformes :

ID CPU Cores RAM OS BW memcpy
korrig AMD Ryzen 5 3600 (Zen 2) 8 vCPU host-passthrough 8 GB Debian 12 VM 8.6 GB/s
freebsd-vm même Ryzen 5 3600 8 vCPU 8 GB FreeBSD 14.4 — (équivalent)
ccx23-hel AMD EPYC Milan (Zen 3) — dedicated 4 vCPU 16 GB Debian 12 11.6 GB/s
cx33-hel Intel Xeon Skylake — shared 4 vCPU 8 GB Debian 12 5.1 GB/s

La bande passante mémoire (mbw -t 0 -n 5 256) sera importante au point n°5.

3. Résultat n°1 — llama.cpp a fait un saut générationnel récent

Le tag b3813 que je traînais depuis l’automne 2024 est considérablement plus lent que le tag récent b9165.

Sur Qwen 3B Q4_K_M, sweet spot threads :

Build t pp256 (t/s) tg64 (t/s)
llama.cpp b3813 8 59.5 16.5
llama.cpp b9165 6 90.6 17.2
llama.cpp b9165 8 93.3 16.3

Sur Qwen 7B Q4_K_M, le bond est encore plus violent :

Build t=6 pp256 t=6 tg64
llama.cpp b3813 15.3 4.14
llama.cpp b9165 35.4 7.32

+57 % sur le prompt processing du 3B. +132 % sur le 7B. +77 % sur la génération du 7B. Pour le même hardware, le même modèle, le même quant — juste un changement de tag git.

L’enseignement : si vous avez piné un tag llama.cpp pour cause de bug ou de stabilité, revisitez ce choix tous les six mois. Le projet bouge vite, les optimisations matmul/scheduler arrivent en continu, et un upgrade peut littéralement doubler votre throughput sans aucun changement matériel.

4. Résultat n°2 — ik_llama.cpp garde une avance, mais elle dépend de l’architecture CPU

ik_llama.cpp est un fork agressif de llama.cpp maintenu par Iwan Kawrakow (ancien contributeur principal des quants Q4_K_M et IQ*). Sa réputation : “+20 à +40 % de perf CPU vs upstream”.

Sur Zen 2 (mon Ryzen 3600), j’ai vu +8 % en pp256 et égalité en tg64 :

Build pp256 tg64
llama.cpp b9165 @ t=8 93.3 16.3
ik_llama.cpp 4503 @ t=6 100.9 17.4

Sur Zen 3 (Hetzner CCX23 EPYC Milan dedicated), l’avance se réduit à +6 % :

Build (t=4) pp256 tg64
llama.cpp b9165 55.8 20.5
ik_llama.cpp 4503 59.4 19.9

Et sur Intel Skylake (Hetzner cx33 shared), inversion totale :

Build (t=4) pp256 tg64
llama.cpp b9165 44.8 11.4
ik_llama.cpp 4503 35.0 10.4

Sur Intel Skylake, llama.cpp HEAD bat ik_llama.cpp de +28 % sur le prompt et +10 % sur la génération.

L’explication la plus probable : ik_llama optimise prioritairement les kernels matmul AVX2 pour AMD (l’auteur travaille sur AMD). llama.cpp upstream a une dispatch plus large et exploite mieux Intel — sans pour autant tirer parti d’AVX-512 (voir résultat n°6).

L’enseignement : ne supposez pas qu’un fork “plus rapide” est toujours plus rapide. Benchmarkez sur VOTRE CPU. Sur AMD modeste, ik_llama.cpp gagne ; sur Intel ou EPYC récent, llama.cpp HEAD est compétitif voire meilleur.

5. Résultat n°3 — Le sweet spot threads dépend de l’implémentation, pas du nombre de cores

Sur le Ryzen 3600 (6 cores physiques, 12 threads SMT), j’ai sweepé -t de 4 à 8 :

ik_llama.cpp 4503, Qwen 3B Q4_K_M :

Threads pp256 tg64
4 74.5 17.8
6 100.9 17.4
8 83.5 16.9

À 6 threads — exactement 1 thread par core physique, sans SMT — on gagne +21 % sur le prompt vs -t 8. Le SMT crée une contention sur les unités SIMD (AVX2 256 bits) que les deux hyperthreads se partagent ; en saturant les cores avec 1 thread chacun, on évite cette contention et le matmul tourne à pleine vitesse.

Mais sur llama.cpp b9165, l’optimum n’est pas le même :

Threads pp256 tg64
6 90.6 17.2
8 93.3 16.3

llama.cpp HEAD exploite mieux le SMT — son scheduler interne gère plus finement le pinning, et -t 8 gagne légèrement sur -t 6.

L’enseignement : la règle “nproc / 2” n’est pas universelle. Testez -t = cores_physiques ET -t = nproc pour votre couple build × CPU. Le delta peut atteindre 20 %.

6. Résultat n°4 — OpenBLAS détruit les performances (sur tous les CPU x86 testés)

Si vous avez déjà compilé llama.cpp avec -DGGML_BLAS=ON -DGGML_BLAS_VENDOR=OpenBLAS, vous avez probablement perdu beaucoup de t/s sans le savoir.

Sur Zen 2 (korrig), Qwen 3B :

Build t pp256 tg64
llama-b9165 native 6 90.6 17.2
llama-b9165 OpenBLAS 6 31.7 16.7
llama-b9165 OpenBLAS 8 20.9 ❌ 2.3 ❌

Sur Zen 3 (CCX23), Qwen 3B :

Build t=4 pp256 t=4 tg64
llama-b9165 native 55.8 20.5
llama-b9165 OpenBLAS 23.7 20.3

-65 % sur Zen 2, -57 % sur Zen 3. Sur les deux architectures, OpenBLAS détruit le prompt processing.

Sur le 7B, même pattern :

Plateforme Native pp256 OpenBLAS pp256
korrig Zen 2 t=6 35.4 22.2 (-37 %)
CCX23 Zen 3 t=4 23.9 15.5 (-35 %)

Pourquoi ? Sur des modèles 3B-7B en Q4_K_M, les matrices manipulées sont relativement petites (de l’ordre de 2048 × 2048 par couche). Le coût de dispatch BLAS — préparer les buffers, appeler la lib externe, gérer le threading interne d’OpenBLAS — est plus élevé que le gain qu’apporte OpenBLAS sur le matmul lui-même. Les kernels GGML natifs avec -march=native (qui exploitent AVX2 + FMA) battent OpenBLAS dans tous les cas mesurés.

OpenBLAS aide marginalement le tg64 (+6 % sur Zen 2, neutre sur Zen 3), mais détruit tellement le pp qu’on n’y gagne jamais en pratique.

L’enseignement : n’activez jamais OpenBLAS pour llama.cpp / ik_llama.cpp sur CPU x86 avec des modèles 3B-7B. Le -DGGML_NATIVE=ON seul est strictement supérieur. Cette croyance “BLAS = plus rapide” vient du calcul scientifique (FFT, gros matmul denses) et ne s’applique pas à l’inférence LLM quantifiée.

7. Résultat n°5 — Sur CPU, les quants “IQ” sont DRAMATIQUEMENT plus lents que Q4_K_M

J’avais lu partout que les quants IQ4_XS étaient “presque aussi rapides que Q4_K_M, pour ~10 % de RAM en moins”. Faux. Sur CPU, c’est dramatiquement faux.

Bench sur llm-lab Zen 2 @ t=6, Qwen 3B :

Build / Quant pp256 tg64
llama.cpp b9165 + Q4_K_M 90.6 17.2
llama.cpp b9165 + IQ4_XS 21.5 (-76 %) 9.6 (-44 %)
ik_llama.cpp + Q4_K_M 100.9 17.4
ik_llama.cpp + IQ4_XS 40.0 (-60 %) 3.3 (-81 %)

Le tg64 à 3.3 t/s sur ik_llama + IQ4_XS est édifiant — c’est le pire chiffre de toute la session.

Pourquoi ? Les quants IQ* (importance-matrix) ont été conçus pour le GPU, où la bande passante mémoire est le goulot et où on cherche à maximiser le rapport “qualité par bit chargé en RAM”. Le coût compute du dequant importance-matrix (lookup table + correction par importance) est négligeable sur GPU avec ses milliers de cores SIMT, mais sur CPU, c’est un mur.

Q4_K_M, lui, a un dequant trivial (multiplication scalaire par bloc) qui se parallélise bien sur AVX2.

L’enseignement : sur CPU, restez sur Q4_K_M comme quant standard. Les IQ* ne se justifient que si vous êtes contraint en RAM au point qu’un -10 % de taille de modèle change quelque chose — et même alors, vous paierez un -50 à -80 % de throughput.

8. Résultat n°6 — Sur Intel Skylake, AVX-512 ne change rien (en pratique)

Le cx33 Hetzner expose un Xeon Skylake avec AVX-512. J’ai voulu vérifier si activer explicitement les flags -DGGML_AVX512=ON apportait quelque chose vs -DGGML_NATIVE=ON seul.

Threads Native (auto -march=native) AVX-512 explicit
2 23.1 / 6.75 23.1 / 6.53
3 34.3 / 9.30 33.3 / 9.05
4 44.8 / 11.4 44.1 / 11.3

Aucun gain. Légère régression de 1-2 % sur le tg64.

Le -march=native du compilateur génère déjà du code AVX-512 si le CPU le supporte. Les flags GGML_AVX512_* activent des kernels GGML écrits à la main pour AVX-512, mais ces kernels semblent n’être pas (ou mal) implémentés sur les paths matmul critiques de llama.cpp / ik_llama.cpp.

Conséquence : l’avance de llama.cpp HEAD sur ik_llama.cpp sur Intel Skylake (vue au résultat n°2) n’est pas due à l’AVX-512. C’est probablement dû à des kernels matmul plus modernes de llama.cpp HEAD (LLAMAFILE matmul, scheduler amélioré) qui exploitent mieux les caches Intel.

L’enseignement : ne perdez pas de temps avec les flags AVX-512 explicites. -DGGML_NATIVE=ON suffit. Mesurez si vous voulez en avoir le cœur net, mais l’aiguille ne bouge pas.

9. Résultat n°7 — Le tg/s est une fonction de la bande passante mémoire

Voici probablement le résultat le plus utile pour le capacity planning. Sur les 4 plateformes, j’ai mesuré la bande passante mémoire avec mbw -t 0 -n 5 256 (memcpy 256 MiB, moyenne sur 5 runs) et comparé avec le tg64 best :

Plateforme BW memcpy tg64 (3B Q4_K_M, best) MB par (t/s)
CCX23 Zen 3 dedicated 11.6 GB/s 20.5 567
llm-lab VM Zen 2 8.6 GB/s 17.2 500
korrig host Zen 2 8.0 GB/s 17.4 460
cx33 Intel shared 5.1 GB/s 11.4 449

Le ratio “MB de bande passante par token/s” varie entre 449 et 567 — soit ±25 % autour d’une moyenne de ~490 MB/(t/s).

C’est un signal très net : tg64BW_memcpy / 490. Une commande mbw sur n’importe quel CPU permet d’estimer en 5 secondes le throughput génération maximum atteignable, sans installer llama.cpp ni télécharger un modèle.

J’écris un article séparé sur ce résultat et son raisonnement matmul/KV-cache. La conséquence pratique ici : si votre LLM CPU est limité en tg/s, inutile de jouer sur les threads ou le quant — il faut un CPU avec plus de canaux mémoire (Threadripper, EPYC, Xeon Sapphire Rapids) ou de la DDR5 plus rapide. Le compute n’est pas le goulot, la RAM l’est.

10. Synthèse — recommandations prod

Pour mon SOC agentique CPU-only :

  1. Upgrade llama.cpp b3813 → b9165 : gain immédiat de 57 % à 132 % de pp selon la taille du modèle. Pré-requis : revérifier que le bug --lora-init-without-apply est fixé (sinon revenir à b3813 ponctuellement).
  2. Tester ik_llama.cpp sur korrig : +8 % de pp gratuit sur Zen 2. Trade-off à arbitrer selon la maintenance.
  3. -t 6 sur korrig, pas -t 8 : +21 % de pp avec ik_llama. (Pour llama.cpp HEAD, -t 8 reste meilleur d’un cheveu.)
  4. Désactiver OpenBLAS partout : compiler avec -DGGML_NATIVE=ON -DGGML_BLAS=OFF.
  5. Garder Q4_K_M comme quant standard, fuir les IQ* sur CPU.
  6. Pour scale-out streaming : un Hetzner CCX23 dedicated en backup, ~25 €/mois — son tg64 à 20.5 t/s vs 17 t/s sur korrig (+18 %) est intéressant pour les analyses interactives.
  7. Pour upgrade hardware long terme : viser un CPU avec plus de canaux mémoire. C’est la BW qui plafonne le tg, pas le compute.

11. Reproductibilité

Tous les chiffres de cet article viennent du fichier CSV docs/llm-bench-cpu-2026-05.csv du repo asp-forge (gitlab.com/llm_tests/asp-forge), commit 89ca406. Le wrapper run-bench.sh est dans le même commit.

Pour rejouer une mesure, en partant d’une VM Debian 12 (8 vCPU, 8 GB suffisent pour le 3B) :

apt-get install -y build-essential clang cmake ninja-build ccache git
git clone https://github.com/ggml-org/llama.cpp.git
cd llama.cpp && git checkout b9165
cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release \
  -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ \
  -DGGML_NATIVE=ON -DLLAMA_BUILD_SERVER=OFF -DLLAMA_BUILD_TESTS=OFF
cd build && ninja -j$(nproc) llama-bench

wget https://huggingface.co/bartowski/Qwen2.5-3B-Instruct-GGUF/resolve/main/Qwen2.5-3B-Instruct-Q4_K_M.gguf

./bin/llama-bench -m Qwen2.5-3B-Instruct-Q4_K_M.gguf -p 256 -n 64 -t 6

5 minutes, 0 €, vous avez votre chiffre de référence. Faites-le sweep -t 2 à -t $(nproc) et notez votre sweet spot. Bench -DGGML_BLAS=ON -DGGML_BLAS_VENDOR=OpenBLAS si vous ne me croyez pas.

12. Pour aller plus loin

  • Article suivant : « tg/s = MB/s : la formule empirique pour planifier la capacité d’un cluster LLM CPU » — détaille le résultat n°7 avec un raisonnement matmul + KV-cache.
  • Article tiers : « FreeBSD pour l’inférence LLM embarquée : un non-sujet » — bench direct Linux vs FreeBSD sur le même Zen 2, pour les opérateurs réseau qui se demandent si OPNsense + LLM embarqué pénalise le throughput.
  • Repo de référence : asp-forge — le SOC agentique qui a motivé ces benches.
  • Outil : ik_llama.cpp — vaut le détour sur AMD modeste.

50 runs, ~2h de wall-clock, 0,06 € de facture cloud, 7 convictions revues. Le rapport temps/insight est correct.