NOPE LinkedIn

Catégories:
Blog
IA
Performance

tg/s = MB/s : la formule empirique pour planifier la capacité d'un cluster LLM CPU

tg/s = MB/s : la formule empirique pour planifier la capacité d'un cluster LLM CPU image

Rubrique: Blog Rubrique: IA Rubrique: Performance Tag: llama.cpp Tag: self-hosted Tag: inference

tg/s = MB/s : la formule empirique pour planifier la capacité d’un cluster LLM CPU

TL;DR

Sur 5 plateformes CPU (x86 AMD, x86 Intel, ARM Ampere Altra), la bande passante mémoire (mesurée par mbw) prédit le throughput de génération LLM (tg64 sur Qwen 3B Q4_K_M) à ±10 % près sur x86 et à ±25 % près si on inclut ARM. Le ratio empirique est ~470 MB par token/s sur x86, et ~650 MB par token/s sur ARM Ampere Altra — l’ARM est moins efficient par MB de BW pour des raisons développées plus loin.

Conséquence pratique : sur n’importe quelle machine, 5 secondes de mbw -t 0 -n 5 256 suffisent à estimer le tg/s maximum atteignable, sans installer llama.cpp ni télécharger un modèle. Très utile en capacity planning ou pour arbitrer entre plusieurs offres cloud.

1. Le contexte

Pendant la session de benchmarks llama.cpp dont je parle dans l’article précédent, j’ai été frappé par un pattern : sur 5 plateformes différentes (incluant ARM), le tg64 (token-generation, 64 tokens) plafonnait à des valeurs prédictibles à partir de la BW.

Plateforme CPU Cores tg64 (Qwen 3B Q4_K_M, best)
llm-lab VM Zen 2 Ryzen 5 3600 (8 vCPU) 8 17.2
korrig host Zen 2 Ryzen 5 3600 (12 threads) 12 ~17.4
CCX23 Helsinki Zen 3 EPYC Milan dedicated 4 20.5
CAX21 Falkenstein ARM Ampere Altra (Neoverse-N1) shared 4 14.6
cx33 Helsinki Intel Xeon Skylake shared 4 11.4

Le Ryzen 8-cores et l’EPYC 4-cores tournent à ~17-20 t/s. L’ARM 4-cores à 14.6, le Xeon Skylake 4-cores à 11.4. Si le compute était le facteur limitant, on aurait un classement par “puissance CPU brute” qui ressemblerait à autre chose.

Ce qui sépare ces machines, c’est la mémoire.

2. La mesure de bande passante

mbw est un petit outil qui mesure la bande passante mémoire d’un processus userspace via memcpy, dumb (boucle scalaire) et mcblock (memcpy par chunks). Le mode MEMCPY est représentatif des accès séquentiels de gros buffers.

apt-get install mbw
mbw -t 0 -n 5 256   # method 0 = MEMCPY, 5 itérations, buffer 256 MiB

Sortie type :

AVG   Method: MEMCPY   Elapsed: 0.02973   MiB: 256.00000   Copy: 8610.541 MiB/s

Sur les 5 plateformes :

Plateforme BW memcpy
CCX23 Zen 3 dedicated 11.6 GB/s
CAX21 Ampere Altra (ARM) 9.6 GB/s
llm-lab VM Zen 2 8.6 GB/s
korrig host Zen 2 8.0 GB/s
cx33 Intel shared 5.1 GB/s

Le classement se calque sur celui du tg64 : CCX23 en tête, ARM Altra et Ryzen au milieu, Skylake shared en queue.

3. La corrélation

Plateforme BW memcpy (MB/s) tg64 best (t/s) MB par (t/s)
CCX23 Zen 3 11626 20.5 567
CAX21 ARM Altra 9561 14.6 654
llm-lab VM Zen 2 8610 17.2 500
korrig host Zen 2 8010 17.4 460
cx33 Intel shared 5117 11.4 449

Deux régimes distincts apparaissent clairement :

  • Sous-ensemble x86 (4 plateformes) : ratio entre 449 et 567, soit ~470 MB/(t/s) en moyenne, avec une variance de ±10 % seulement. C’est très resserré.
  • ARM Ampere Altra : ratio 654, soit environ +40 % de BW nécessaire par token/s vs x86.

L’inefficacité ARM s’explique par un cumul de facteurs : (a) une latence DRAM réputée plus haute sur Neoverse-N1 que sur Zen 3 (non capturée par mbw), (b) moins de cache L2/L3 par cœur, (c) des kernels GGML upstream moins finement tunés sur ARM (moins de PRs / mois côté ggml-cpu/arch/arm/).

Plot mental : sur x86, vous obtenez un nuage très linéaire passant par l’origine, pente ~1/470 t/s par MB/s. Le point ARM Altra est nettement sous cette droite. Pour le capacity planning, séparer les deux régimes est plus juste que prendre une moyenne globale.

La variation ±25 % s’explique par :

  • Différences de cache : Zen 3 a plus de L3 par core que Zen 2, et bien plus que Neoverse-N1. Une partie du KV-cache du modèle peut rester en cache et n’épuise pas la BW DRAM ;
  • Différences de latence : la BW pure (mbw) ne capture pas la latence d’accès aléatoire, importante pour la lecture du KV-cache. ARM Altra a une latence DRAM réputée plus haute que Zen 3 ;
  • Type d’accès : mbw mesure du séquentiel pur, alors que la génération mélange du séquentiel (lecture des poids du modèle) et de l’aléatoire (KV-cache attention) ;
  • Maturité des kernels : llama.cpp upstream a moins de contributeurs ARM que x86 → optimisations de matmul potentiellement moins poussées sur Neoverse ;
  • Imprécision intrinsèque de mbw : ±5 % de variance entre runs successifs.

Mais le signal est très net. Pour un capacity planning :

  • x86 (Intel / AMD) : tg/s ≈ BW_memcpy / 470 — précision ±10 %
  • ARM (Ampere Altra) : tg/s ≈ BW_memcpy / 650 — précision ±25 %

Si vous ne savez pas sur quel CPU vous serez, prendre / 600 comme défaut conservateur, vous donne une estimation basse mais sûre.

4. Pourquoi ça marche

La phase de génération token-par-token d’un LLM autoregressif est bandwidth-bound sur CPU. Pour générer chaque token :

  1. On charge en cache les poids du modèle (~2 GB pour Qwen 3B Q4_K_M) ;
  2. On charge le KV-cache cumulé (~50-200 MB selon le contexte) ;
  3. On fait passer le forward pass à travers les 36 couches (Qwen 3B) — chaque couche fait un attention + un MLP ;
  4. On échantillonne le token suivant.

L’opération dominante est la multiplication matrice-vecteur : à chaque couche, le hidden state (un vecteur) est multiplié par les matrices de poids. Pour Q4_K_M, ces poids sont chargés depuis la RAM, dequantifiés à la volée, et multipliés.

Sur un Ryzen 5 3600 à 8 vCPU avec AVX2, le compute matmul pur peut atteindre ~200 GFLOP/s en théorie. Pour un forward pass de Qwen 3B, ça représente ~0.6 GFLOP par token. Le compute pur prendrait donc ~3 ms par token, soit théoriquement ~330 t/s.

Or on mesure 17 t/s. Il y a un facteur 20 entre le théorique compute-bound et le mesuré.

Ce facteur, c’est la mémoire. À chaque token, on doit relire tous les poids du modèle depuis la RAM (le cache CPU est trop petit pour tenir 2 GB). Avec 8 GB/s de bande passante effective, lire 2 GB prend 250 ms — soit 4 t/s si on était purement séquentiel. La réalité (17 t/s) est meilleure parce qu’une partie des poids reste en cache entre tokens (notamment pour les couches d’attention sur des prompts courts), et parce que la prefetch hardware aide.

Mais l’ordre de grandeur est clair : le LLM CPU lit ~470 MB par token depuis la RAM. La BW est le mur.

5. Conséquences pratiques

5.1 Estimer le tg/s d’une machine avant de l’acheter ou de la louer

Vous hésitez entre un Ryzen 9 7950X (DDR5 5200 dual-channel ≈ 60 GB/s théorique, ~30-40 GB/s pratique) et un Threadripper 7960X (DDR5 4-channel ≈ 100 GB/s théorique, ~70 GB/s pratique) ?

Estimation rapide (ratio x86 ≈ 1/470 t/s par MB/s) :

  • Ryzen 9 7950X : 35 GB/s / 0.47 GB/(t/s) ≈ 74 t/s sur Qwen 3B Q4_K_M
  • Threadripper 7960X : 70 GB/s / 0.47 GB/(t/s) ≈ 149 t/s sur Qwen 3B Q4_K_M

Le doublement attendu correspond bien au doublement des canaux DDR5. Pour un workload streaming-intensif (chatbot, génération de code), le Threadripper apporte un gain réel proportionnel à la BW.

5.2 Choisir entre offres cloud

Sur Hetzner (mesures réelles + estimations) :

  • CX (shared, Intel) : 5 GB/s mesuré → 10 t/s. 0.012 €/h → 1.2 €/(t/s)
  • CAX (shared, ARM Ampere Altra) : 9.6 GB/s mesuré → 14.6 t/s. 0.005 €/h → 0.34 €/(t/s)
  • CPX (shared, AMD) : à mesurer, probablement ~7-8 GB/s → ~15 t/s
  • CCX (dedicated, AMD EPYC) : 11.6 GB/s mesuré → 20.5 t/s. 0.040 €/h → 1.95 €/(t/s)

Surprise du test : la CAX (ARM Ampere Altra) offre le meilleur ratio €/(t/s) — 5× moins cher que CCX23 dedicated pour 70 % des perfs. Si votre workload est purement génération streaming (pas de prompt processing massif), CAX est imbattable. Caveat : ARM peut casser des dépendances (binaires x86-only, comme ik_llama.cpp).

Pour un coût horaire faible avec gros throughput streaming, l’ordre est : CAX > CPX > CCX > CX.

5.3 Ne pas chercher du gain compute là où il n’y en a pas

Si vous êtes plafonné en tg/s :

  • Changer de quant (Q4_K_M → Q3_K_M, Q5_K_M, IQ4_XS…) ne change rien à la BW lue par token (les poids restent du même ordre de grandeur). Voire dégrade sur CPU (cf. résultat n°5 de l’article principal sur les IQ).
  • Ajouter des threads ne change rien — la mémoire est partagée entre les threads.
  • Activer OpenBLAS, AVX-512, etc. ne change rien — la BW est le mur, pas le compute.

Le seul levier qui marche est de changer de hardware avec plus de canaux mémoire.

5.4 Différencier pp et tg

Attention : cette formule ne s’applique PAS au pp (prompt processing). La phase de prompt processing est compute-bound (matmul matrice-matrice avec des batch tokens), et là, le nombre de cores et l’efficacité des kernels matmul (AVX2/AVX-512) dominent. C’est pour ça que mon Ryzen 8-cores bat le CCX23 4-cores sur le pp (101 vs 59 t/s) malgré une BW inférieure.

La règle est :

Phase Bottleneck Levier
pp (prompt processing, matrice × matrice) Compute (cores × FLOP/s) Plus de cores, AVX2/AVX-512, kernels optimisés
tg (token generation, matrice × vecteur) Mémoire (canaux × DDR speed) Plus de canaux mémoire, DDR plus rapide

Pour un workload mixte (analyse longue + streaming court), choisissez le hardware dont les deux dimensions sont équilibrées par rapport à votre profil d’usage.

6. Limites et caveats

  • Mesuré sur Qwen 3B Q4_K_M uniquement. Le ratio MB/(t/s) dépend de la taille du modèle et du quant — un modèle 7B aura un ratio ≈ 2× supérieur (poids 2× plus lourds), un 13B ≈ 4×, etc. Le ratio par-token “absolu” reste donc spécifique au modèle. Mais la proportionnalité avec la BW est universelle.
  • Mesure mbw imparfaite. mbw mesure du memcpy séquentiel, pas le pattern d’accès réel d’un LLM. C’est une approximation. Pour aller plus fin, stream-benchmark ou pmbw donnent des chiffres plus représentatifs (séquentiel vs aléatoire vs scatter/gather).
  • Pas de mesure DDR5 ni Threadripper / EPYC haut de gamme. Mes 4 plateformes sont toutes en DDR4. La constante 490 MB/(t/s) pourrait varier sur des architectures plus récentes (DDR5, HBM, etc.), notamment si la latence change beaucoup vs la BW.
  • NUMA non testé. Sur un dual-socket EPYC, les effets NUMA peuvent dégrader la BW effective si le modèle est mal pinné.

Les corrections à apporter sont mineures pour la majorité des cas : self-hosters sur DDR4 mono-socket, edge devices, VPS cloud standards.

7. La mesure que vous pouvez faire en 30 secondes

# Sur n'importe quelle machine Linux x86 :
apt-get install -y mbw    # ou yum/dnf install mbw
mbw -t 0 -n 5 256 | grep AVG
# AVG   Method: MEMCPY   Elapsed: 0.02973   MiB: 256.00000   Copy: 8610.541 MiB/s

# Estimation tg/s pour Qwen 3B Q4_K_M :
echo "tg/s estimé (x86) ≈ $(echo '8610 / 470' | bc -l)"
# tg/s estimé (x86) ≈ 18.3

Si vous mesurez 18.3 t/s estimé et que vous voulez 30 t/s pour votre cas d’usage, vous savez tout de suite : changez de hardware, ne perdez pas de temps à tuner les threads ou le quant.

C’est le genre de signal qui économise des week-ends entiers de microbenchs frustrants.

8. Pour aller plus loin