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
TL;DR
Sur 4 plateformes CPU x86 — un Ryzen desktop, une VM, un EPYC dedicated, un Xeon shared — 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) avec une précision de ±25 %. Le ratio empirique est ~490 MB de bande passante par token/s.
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 4 plateformes différentes, le tg64 (token-generation, 64 tokens) plafonnait à des valeurs très proches malgré des différences de CPU importantes.
| 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 |
| cx33 Helsinki Intel | Xeon Skylake shared | 4 | 11.4 |
Le Ryzen 8-cores et l’EPYC 4-cores tournent à ~17-20 t/s. Le Xeon Skylake 4-cores tombe à 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 4 plateformes :
| Plateforme | BW memcpy |
|---|---|
| CCX23 Zen 3 dedicated | 11.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 renverse : c’est CCX23 qui domine, suivi du Ryzen, suivi de Skylake shared. C’est exactement le classement du tg64.
3. La corrélation
| Plateforme | BW memcpy (MB/s) | tg64 best (t/s) | MB par (t/s) |
|---|---|---|---|
| CCX23 Zen 3 | 11626 | 20.5 | 567 |
| llm-lab VM Zen 2 | 8610 | 17.2 | 500 |
| korrig host Zen 2 | 8010 | 17.4 | 460 |
| cx33 Intel shared | 5117 | 11.4 | 449 |
Le ratio MB/(t/s) varie de 449 à 567 — soit ±25 % autour d’une moyenne de ~490 MB par t/s.
Plot mental : si vous tracez (BW memcpy) vs (tg64) sur ces 4 points, vous obtenez un nuage très linéaire passant par l’origine, avec une pente de ~1/490 t/s par MB/s.
La variation ±25 % s’explique par :
- Différences de cache : Zen 3 a plus de L3 par core que Zen 2, donc 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 ;
- Type d’accès :
mbwmesure 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) ; - Imprécision intrinsèque de
mbw: ±5 % de variance entre runs successifs.
Mais le signal est très net. Pour un capacity planning à ±25 %, tg/s ≈ BW_memcpy / 490 est utilisable.
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 :
- On charge en cache les poids du modèle (~2 GB pour Qwen 3B Q4_K_M) ;
- On charge le KV-cache cumulé (~50-200 MB selon le contexte) ;
- On fait passer le forward pass à travers les 36 couches (Qwen 3B) — chaque couche fait un attention + un MLP ;
- 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 :
- Ryzen 9 7950X : 35 GB/s / 0.49 GB/(t/s) ≈ 71 t/s sur Qwen 3B Q4_K_M
- Threadripper 7960X : 70 GB/s / 0.49 GB/(t/s) ≈ 143 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 :
- CX (shared, Intel) : ~5 GB/s mesuré → ~10 t/s attendu sur 3B
- CPX (shared, AMD) : à mesurer, probablement ~7-8 GB/s → ~16 t/s
- CCX (dedicated, AMD EPYC) : 11+ GB/s mesuré → 20+ t/s
Pour un coût de 0.012 €/h (cx33) à 0.04 €/h (CCX23), vous doublez le tg/s. Si votre charge utilisateur est en streaming continu, le ratio €/(t/s) est meilleur sur CCX malgré le coût horaire 3× plus élevé.
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.
mbwmesure du memcpy séquentiel, pas le pattern d’accès réel d’un LLM. C’est une approximation. Pour aller plus fin,stream-benchmarkoupmbwdonnent 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é ≈ $(echo '8610 / 490' | bc -l)"
# tg/s estimé ≈ 17.5
Si vous mesurez 17.5 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
- L’article principal de cette série : « Benchmarker llama.cpp sur CPU : ce qu’on apprend en 50 runs » — les 7 résultats contre-intuitifs dont la BW-bound est le n°7.
- Données brutes :
asp-forge/docs/llm-bench-cpu-2026-05.csv, commit89ca406. - Pour aller plus fin sur les benchmarks BW : STREAM benchmark, pmbw.
- Lecture complémentaire : “LLM inference is memory-bound” par Horace He — le post qui a popularisé l’argument côté GPU. Cet article confirme empiriquement que c’est encore plus vrai côté CPU.
