NOPE LinkedIn

Catégories:
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: IA Rubrique: Performance Tag: llama.cpp Tag: benchmark Tag: cpu Tag: memory-bandwidth Tag: capacity-planning Tag: mbw Tag: self-hosted Tag: inference

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 : 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) ;
  • 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 :

  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 :

  • 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. 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é ≈ $(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