On ne se souvient pas d’un CPU par ses diapositives marketing. On s’en souvient par le bruit du pager : le 99e percentile qui se dégrade,
la facture d’électricité qui grimpe, et la réunion gênante où quelqu’un dit : « Mais il a huit cœurs. »
L’ère Bulldozer d’AMD a été exactement ce type de leçon. Ce n’était pas un design stupide. C’était un design audacieux qui demandait au logiciel
et aux charges de travail de faire moitié chemin. En production, « faisons moitié chemin » se traduit souvent par « débrouillez-vous à 2 h du matin ».
Ce que Bulldozer a tenté de faire (et pourquoi cela semblait raisonnable)
Bulldozer est arrivé avec un pari : le monde devenait « wide ». Des threads partout. Serveurs web, VM, tâches en arrière-plan, pipelines par lots — plus de travail exécutable que ce qu’on pouvait jeter sur un « cœur lourd » traditionnel sans gaspiller du silicium.
La réponse d’AMD fut le module. Chaque module contenait deux cœurs entiers qui partageaient certains éléments coûteux du front-end et
des ressources en virgule flottante. L’argument : pour des charges fortement multi‑threads, vous obtenez presque 2x de débit pour moins de 2x de surface
et de consommation. C’est comme construire deux studios qui partagent une cuisine. Efficace — jusqu’à ce que les deux locataires décident de préparer le dîner de Thanksgiving en même temps.
Le design n’était pas une pure nouveauté. Il s’alignait sur une tendance : les centres de données s’intéressaient au débit par baie, pas seulement à la performance d’un seul thread. Et AMD avait un vrai problème : ils ne pouvaient pas continuer à faire évoluer les cœurs monolithiques traditionnels au même rythme que les microarchitectures dominantes d’Intel à l’époque.
Là où le pari a dérapé : les logiciels ne planifiaient ni ne compilaient naturellement en étant « conscients des modules ». Beaucoup de charges réelles n’étaient pas aussi parallèles que la feuille de route l’imaginait. Et les pièces partagées — en particulier le front-end et la FP — sont devenues des goulets d’étranglement précisément là où vous ne le souhaitiez pas : systèmes sensibles à la latence, charges mixtes et tout ce qui ressemble à « quelques threads chauds qui font vraiment le travail ».
Cadre opérationnel fiable : Bulldozer n’était pas un « mauvais CPU ». C’était un CPU qui exigeait que vous compreniez vraiment où se trouve votre goulot d’étranglement. Si vous vous trompiez, vous ne perdiez pas 5 %. Vous perdiez un quart.
Faits et contexte à retenir réellement
La trivia historique n’est utile que si elle change vos décisions. Ces points le font.
- Bulldozer a fait ses débuts en 2011 (série FX pour desktop, Opteron 6200 pour serveurs), remplaçant les cœurs de type K10/Phenom II par l’approche module.
- Les puces FX « huit cœurs » étaient typiquement quatre modules : huit cœurs entiers, mais pas huit front-ends indépendants ni unités FP autonomes.
- Chaque module partageait un complexe d’unités en virgule flottante (deux FMAC 128 bits pouvant se combiner pour de l’AVX 256 bits), donc deux threads FP par module pouvaient se concurrencer.
- Il y a eu une vraie histoire d’ordonnancement Linux : au début, les ordonnanceurs pouvaient placer les threads de manière inefficace, provoquant une contention intra-module évitable.
- L’ordonnancement Windows importait aussi : le placement des threads affectait la performance de façons que les utilisateurs n’avaient pas l’habitude de voir sur des cœurs conventionnels.
- Bulldozer a introduit le support AVX chez AMD, mais l’implémentation et le comportement du pipeline environnant ne se sont pas toujours traduits par des gains réels.
- Le comportement en fréquence et en puissance était central : les fréquences annoncées semblaient excellentes sur fiche technique, mais les horloges soutenues sous charge pouvaient donner une réalité plus chaude et différente.
- Ce n’était pas unique et figé : Piledriver et itérations ultérieures ont amélioré certains éléments (fréquence, IPC, gestion de la puissance), mais le pari fondamental du module est resté.
Une blague sèche, pour respirer : le marketing de Bulldozer a donné une leçon importante — « jusqu’à » est l’unité de mesure de l’espoir, pas du débit.
La réalité du module : où partent les cycles
1) Front-end partagé : fetch/decode n’est pas gratuit
Les cœurs modernes vivent et meurent selon leur capacité à alimenter l’arrière-plan d’exécution. Le module de Bulldozer partageait une grosse partie des mécanismes du front-end entre les deux cœurs entiers. Cela signifie que lorsque vous exécutez deux threads occupés dans un même module, ils peuvent se heurter à l’étape « préparer les instructions » avant même d’aborder les ports d’exécution.
En termes pratiques : deux threads entiers qui semblent « légers » en pourcentage CPU peuvent néanmoins être lourds en pression front-end : beaucoup de branchements, un grand empreinte code, beaucoup de churn dans le cache d’instructions, beaucoup de demande de décodage. Votre monitoring indique « le CPU va bien », mais votre latence dit le contraire.
2) FP partagé : la taxe silencieuse sur les charges mixtes
Les ressources en virgule flottante de Bulldozer étaient partagées à l’intérieur du module. Si vous aviez un thread lourd en FP et un thread principalement entier, vous pouviez vous en sortir. Si vous aviez deux threads FP lourds collés (ou planifiés) dans le même module, vous pouviez obtenir une performance donnant l’impression que quelqu’un avait remplacé votre CPU par un comité poli.
C’est particulièrement pertinent pour :
- pipelines de compression/décompression
- piles crypto utilisant des mathématiques vectorisées
- traitement média
- code scientifique
- certains comportements JVM et .NET sous des chemins JIT vectorisés
3) IPC : l’écart inconfortable
Le grand titre public pour Bulldozer fut la déception d’IPC (instructions par cycle) comparé aux contemporains d’Intel. On peut parler de profondeur de pipeline, de comportement de branchement et d’effets cache toute la journée. Les opérateurs l’expérimentent ainsi :
« Pourquoi cette machine à 3,6 GHz semble-t-elle plus lente que celle à 3,2 GHz ? »
L’IPC est un symptôme composite. C’est ce que vous voyez quand le front-end ne nourrit pas le back-end, quand la spéculation ne paie pas, quand la latence mémoire n’est pas masquée correctement, et quand votre charge n’est pas alignée sur les hypothèses du CPU. Les hypothèses de Bulldozer favorisaient fortement de nombreux threads exécutables et moins de cas « un thread chaud doit être rapide ».
4) Le module est une unité d’ordonnancement que vous le vouliez ou non
Si vous traitez un CPU Bulldozer à 8 cœurs entiers comme « 8 cœurs symétriques », vous prendrez de mauvaises décisions. La frontière du module compte pour la contention. Ce n’est pas philosophique. C’est mesurable.
Quand les ordonnanceurs sont conscients des modules, ils essaient d’étaler d’abord les threads chauds à travers les modules, puis de remplir le deuxième thread de chaque module. Quand ils ne le sont pas, vous pouvez vous retrouver avec deux threads lourds partageant un module pendant qu’un autre module est inactif — le classique « mon CPU est à 50 % mais mon service meurt ».
Une citation à garder au mur, parce que c’est ainsi que vous survivez ces architectures :
L’espoir n’est pas une stratégie.
— General Gordon R. Sullivan
Bulldozer punissait la planification de capacité basée sur l’espoir. Il récompensait la mesure, le pinning et des benchmarks réalistes.
Ajustement des charges : où Bulldozer brille et où il trébuche
Bonnes adéquations (toujours conditionnelles)
Bulldozer pouvait sembler respectable pour des charges qui étaient :
- fortement multi‑threads et peu sensibles à la latence par thread
- à dominance entière et tolérantes à la contention front-end
- orientées débit comme des jobs par lots où l’on peut sur‑souscrire et garder la machine occupée
- services qui montent en charge presque linéairement avec plus de threads exécutables (certains workloads web, certaines fermes de compilation)
Dans ces cas, l’approche module peut offrir un bon travail par dollar — surtout si vous l’avez acheté au bon moment et que vous ne le compariez pas à une fantaisie.
Mauvaises adéquations (celles qui ont coûté des emplois)
Bulldozer avait tendance à décevoir dans :
- goulots d’étranglement mono‑thread sensibles à la latence (verrous chauds, pauses GC, handlers de requêtes mono‑thread, points chauds de sérialisation)
- travail intensif FP/vectoriel lorsque les threads se rencontrent dans un module
- jeux et charges interactives où quelques threads dominent le temps de frame
- tenancy mixte où des « voisins bruyants » viennent concurrencer des ressources partagées de module de façon imprévisible
Traduction : si votre business est « répondre rapidement », Bulldozer exigeait plus de soin. Si votre business est « finir éventuellement », Bulldozer pouvait convenir — jusqu’à ce que la puissance ou la thermique devienne la contrainte suivante.
Le piège commercial : les « cœurs » comme fiction d’achat
Les entreprises adorent des chiffres à coller dans des tableurs. « Cœurs » en est un. Bulldozer a exploité cette faiblesse, parfois accidentellement, parfois non.
Si vos calculs de licence, de capacité ou vos SLO supposent que chaque « cœur » équivaut à chaque « cœur » d’un autre fournisseur, arrêtez-vous.
Recalibrez autour du débit mesuré, pas du nombre inscrit sur l’étiquette. C’est vrai aujourd’hui avec SMT, les cœurs d’efficacité et les accélérateurs partagés aussi. Bulldozer n’était qu’un rappel précoce et tonitruant.
Vue SRE : modes de défaillance observables
En opération, vous ne débuggez pas la microarchitecture directement. Vous déboguez des symptômes : files d’attente, latence tail, profondeur de run queue,
steal time, stalls mémoire, throttling thermique, artefacts d’ordonnancement.
Symptômes typiques Bulldozer
- Latence élevée à utilisation CPU modérée parce que le travail « chargé » se dispute des ressources partagées du module.
- Variabilité de performance entre hôtes identiques due aux paramètres du BIOS, aux C‑states, au comportement turbo et au placement des threads.
- Surprises en virtualisation quand la topologie vCPU ne correspond pas à la topologie des modules physiques, provoquant une contention évitable ou des accès NUMA morts.
- Plafonds de puissance/thermiques qui empêchent le boost soutenu, rendant « la fréquence annoncée » sans rapport dans des tests longue durée.
Seconde blague, et c’est tout : traiter les cœurs Bulldozer comme identiques, c’est comme traiter toutes les files comme FIFO — réconfortant, jusqu’à ce que vous consultiez les graphiques.
Pourquoi les ingénieurs stockage devraient s’en soucier (oui, vraiment)
Les piles de stockage sont pleines de travail CPU : checksums, compression, chiffrement, traitement réseau, churn des métadonnées filesystem,
gestion des interruptions et orchestration en espace utilisateur. Un CPU avec des patterns de contention étranges peut transformer un « problème disque »
en un problème d’ordonnancement CPU portant l’étiquette disque.
Si vous exécutez du stockage réseau, des gateways d’object storage, ou tout ce qui est proche de ZFS sur du silicium de l’ère Bulldozer, vous devez
séparer « le périphérique est lent » de « le CPU est lent pour ce type de travail spécifique ». La correction peut être un réglage d’ordonnanceur, pas un nouveau SSD.
Feuille de route pour un diagnostic rapide
Lorsqu’un hôte de l’ère Bulldozer sous‑perform, ne commencez pas par débattre de l’IPC sur des forums. Commencez par localiser le goulot d’étranglement en trois passes : ordonnancement/topologie, fréquence/puissance, puis mémoire/IO.
Premier point : vérifier le placement des threads et l’alignement topologique
- Confirmer combien de sockets, nœuds NUMA et cœurs vous avez réellement.
- Vérifier si des threads chauds sont packés dans le même module.
- Dans les VM, valider que la topologie vCPU présentée à l’invité correspond à ce que l’hyperviseur peut garantir en ressources physiques.
Deuxième point : confirmer que vous ne perdez pas face aux fréquences (governor, turbo, thermiques)
- Vérifier la fréquence CPU sous charge soutenue, pas au repos.
- Vérifier que le governor est correctement réglé pour des charges serveur.
- Rechercher du throttling thermique, des limites de puissance ou des C‑states trop agressifs.
Troisième point : mesurer les stalls et la mise en file (CPU vs mémoire vs IO)
- Utiliser perf pour voir si vous êtes en front-end stalled, backend stalled, ou en attente mémoire.
- Vérifier la profondeur de run queue et les changements de contexte.
- Valider que les interruptions stockage et réseau ne s’accumulent pas sur un CPU malheureux.
Règle de décision : si corriger le placement et les fréquences améliore la latence de plusieurs dizaines de pourcents, arrêtez‑vous là et stabilisez.
Sinon, passez au profilage plus profond.
Tâches pratiques : 12+ vérifications avec commandes, sorties et décisions
Ce ne sont pas des « benchmarks pour un blog ». Ce sont les vérifications à faire sur une machine qui ne respecte pas ses SLO. Chaque tâche inclut :
une commande, une sortie d’exemple, ce que cela signifie et la décision à prendre.
1) Identifier rapidement le modèle CPU et la topologie
cr0x@server:~$ lscpu
Architecture: x86_64
CPU(s): 16
Thread(s) per core: 1
Core(s) per socket: 8
Socket(s): 2
NUMA node(s): 2
Model name: AMD Opteron(tm) Processor 6272
NUMA node0 CPU(s): 0-7
NUMA node1 CPU(s): 8-15
Ce que cela signifie : Vous êtes probablement sur une série Opteron 6200 (dérivée de Bulldozer). Threads-per-core est 1 (pas de SMT), mais les modules partagent toujours des ressources.
Décision : Traitez l’ordonnancement et le pinning comme critiques. Vérifiez aussi l’affinité NUMA pour les services gourmands en mémoire.
2) Confirmer la version du noyau et le contexte de l’ordonnanceur
cr0x@server:~$ uname -r
5.15.0-94-generic
Ce que cela signifie : Noyau moderne, généralement meilleure conscience topologique que l’ère 3.x. Toujours : votre hyperviseur, BIOS et workload peuvent le contourner.
Décision : N’assumez pas que « un noyau récent a tout résolu ». Mesurez le comportement de placement avec des cartes de threads réelles.
3) Voir l’utilisation par CPU pour détecter le packing
cr0x@server:~$ mpstat -P ALL 1 3
Linux 5.15.0-94-generic (server) 01/21/2026 _x86_64_ (16 CPU)
12:00:01 AM CPU %usr %sys %iowait %irq %soft %idle
12:00:02 AM all 42.10 6.20 0.10 0.20 0.50 50.90
12:00:02 AM 0 88.00 8.00 0.00 0.00 0.00 4.00
12:00:02 AM 1 86.00 10.00 0.00 0.00 0.00 4.00
12:00:02 AM 2 5.00 2.00 0.00 0.00 0.00 93.00
...
Ce que cela signifie : CPU0 et CPU1 sont saturés tandis que CPU2 est inactif. Ça sent le packing de threads, un problème d’affinité d’IRQ, ou un processus épinglé.
Décision : Identifier le processus et son masque d’affinité ; corriger le pinning ou ajuster la distribution ordonnanceur/IRQ.
4) Trouver le processus chaud et la disposition de ses threads
cr0x@server:~$ ps -eo pid,comm,%cpu --sort=-%cpu | head
PID COMMAND %CPU
8421 java 175.3
2310 ksoftirqd/0 35.0
1023 nginx 22.4
Ce que cela signifie : Une JVM utilise ~1.75 cœurs de CPU ; ksoftirqd/0 est aussi occupé sur CPU0. La contention est probable.
Décision : Inspecter le placement par thread et la distribution des interruptions avant de modifier les options JVM.
5) Inspecter le mapping thread→CPU pour le PID chaud
cr0x@server:~$ pid=8421; ps -L -p $pid -o pid,tid,psr,pcpu,comm --sort=-pcpu | head
PID TID PSR %CPU COMMAND
8421 8421 0 98.5 java
8421 8434 1 77.2 java
8421 8435 0 15.0 java
8421 8436 1 12.1 java
Ce que cela signifie : Les threads sont bloqués sur CPU0/CPU1. Sur Bulldozer, cela peut signifier « deux threads se battent à l’intérieur d’un même module » selon la numérotation et la topologie.
Décision : Supprimer les affinités trop strictes ; étaler à travers cœurs/modules ; ou pinner explicitement les threads critiques à travers les modules.
6) Vérifier le masque d’affinité CPU du processus
cr0x@server:~$ taskset -pc 8421
pid 8421's current affinity list: 0-1
Ce que cela signifie : Quelqu’un a épinglé la JVM sur les CPU 0–1. C’est le pied-de-biche de l’optimisation qui vous tire dans le pied.
Décision : Étendre à un ensemble sensé (par exemple, répartir à travers des modules ou un nœud NUMA), puis retester la latence.
7) Appliquer une affinité plus sûre (exemple) et vérifier
cr0x@server:~$ sudo taskset -pc 0-7 8421
pid 8421's current affinity list: 0-7
Ce que cela signifie : Le processus peut maintenant s’exécuter sur les CPUs 0–7 (un nœud NUMA dans cet exemple).
Décision : Si la charge est locale mémoire, restez dans un nœud NUMA. Si elle est CPU-bound et fortement multithread, envisagez d’utiliser les deux nœuds avec précaution.
8) Vérifier le governor de fréquence CPU et la politique actuelle
cr0x@server:~$ cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
powersave
Ce que cela signifie : « powersave » sur un serveur sous charge peut être catastrophique pour la latence.
Décision : Passer à « performance » pour les services sensibles à la latence, puis valider les thermiques et la marge de puissance.
9) Basculer temporairement le governor en performance et vérifier
cr0x@server:~$ sudo cpupower frequency-set -g performance
Setting cpu: 0
Setting cpu: 1
Setting cpu: 2
Setting cpu: 3
Setting cpu: 4
Setting cpu: 5
Setting cpu: 6
Setting cpu: 7
Setting cpu: 8
Setting cpu: 9
Setting cpu: 10
Setting cpu: 11
Setting cpu: 12
Setting cpu: 13
Setting cpu: 14
Setting cpu: 15
Ce que cela signifie : Tous les CPU ciblent maintenant la fréquence maximale sous charge.
Décision : Relancer votre test de charge ; si la latence tail diminue, rendre le changement persistant via votre gestion de configuration.
10) Observer la fréquence réelle sous charge
cr0x@server:~$ sudo turbostat --Summary --interval 2
turbostat: Snapshot every 2.0 sec
Avg_MHz Busy% Bzy_MHz IRQ SMI
2875 62.10 4630 1234 0
Ce que cela signifie : Les CPUs boostent (Bzy_MHz plus élevé que Avg_MHz). Si vous aviez vu un Bzy_MHz faible, vous soupçonneriez du throttling ou une politique inappropriée.
Décision : Si le boost s’effondre lors d’exécutions soutenues, investiguez les thermiques, les limites de puissance du BIOS ou le flux d’air du châssis.
11) Vérifier la pression de la run queue et le switching de contexte
cr0x@server:~$ vmstat 1 5
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
4 0 0 812340 90212 993220 0 0 1 2 1100 4200 45 6 49 0 0
9 0 0 811900 90212 993500 0 0 0 0 2100 9800 52 8 40 0 0
Ce que cela signifie : La run queue (r) pique à 9 sur un système 16‑CPU ; les context switches (cs) sont élevés. Peut-être une sur‑souscription ou une contention sur des verrous.
Décision : Si sensible à la latence, réduire le nombre de threads exécutables (tailles de pool), corriger les hotspots de verrou, ou s’assurer que les threads ne se disputent pas à l’intérieur des modules.
12) Inspecter la localité NUMA (placement mémoire vs CPU)
cr0x@server:~$ numactl --hardware
available: 2 nodes (0-1)
node 0 cpus: 0 1 2 3 4 5 6 7
node 0 size: 32768 MB
node 0 free: 12000 MB
node 1 cpus: 8 9 10 11 12 13 14 15
node 1 size: 32768 MB
node 1 free: 9000 MB
Ce que cela signifie : Deux nœuds NUMA. Si votre processus tourne sur les CPUs du nœud 0 mais alloue de la mémoire sur le nœud 1, vous payez la latence distante.
Décision : Lier processus et mémoire au même nœud pour une latence cohérente (numactl --cpunodebind/--membind), ou corriger le placement de vos VM.
13) Vérifier si les interruptions sont concentrées sur un CPU
cr0x@server:~$ cat /proc/interrupts | head -n 10
CPU0 CPU1 CPU2 CPU3 CPU4 CPU5 CPU6 CPU7
24: 982312 0 0 0 0 0 0 0 IO-APIC 24-fasteoi eth0
25: 1200 980 870 910 940 905 899 920 IO-APIC 25-fasteoi ahci
Ce que cela signifie : Les IRQ réseau frappent presque exclusivement CPU0. Cela peut priver le travail réel et créer de la contention de module.
Décision : Configurer l’affinité IRQ ou vérifier le comportement d’irqbalance ; pour des NIC hauts débits, répartir les queues entre CPUs et cœurs NUMA‑locaux.
14) Vérification rapide de latence stockage (parce qu’on l’accuse toujours)
cr0x@server:~$ iostat -x 1 3
avg-cpu: %user %nice %system %iowait %steal %idle
44.21 0.00 6.44 0.20 0.00 49.15
Device r/s w/s rkB/s wkB/s await svctm %util
sda 2.0 8.0 64.0 512.0 3.10 0.45 0.90
Ce que cela signifie : Le disque n’est pas le goulot (await faible, %util faible). Votre ticket « le stockage est lent » est probablement un problème d’ordonnancement CPU ou de comportement applicatif.
Décision : Arrêtez d’acheter des disques. Concentrez-vous sur le placement CPU, les IRQ et la contention de verrous.
15) Mesurer où le temps CPU passe (approche top-down) avec perf
cr0x@server:~$ sudo perf stat -p 8421 -a -- sleep 10
Performance counter stats for 'system wide':
32,145.12 msec task-clock # 3.214 CPUs utilized
120,331,221 context-switches # 3.742 M/sec
2,110,334 cpu-migrations # 65.655 K/sec
98,771,234,112 cycles # 3.070 GHz
61,220,110,004 instructions # 0.62 insn per cycle
9,112,004,991 branches # 283.381 M/sec
221,004,112 branch-misses # 2.43% of all branches
Ce que cela signifie : IPC ~0.62 est faible pour beaucoup de charges serveur ; migrations et context switches sont énormes, suggérant un churn de l’ordonnanceur.
Décision : Réduire les migrations (affinité/liaison NUMA), redimensionner les pools de threads, isoler les IRQs. Puis re-mesurer l’IPC et la latence.
16) Vérifier le steal time en virtualisation (si applicable)
cr0x@server:~$ mpstat 1 3 | tail -n 3
12:01:01 AM all 40.20 7.10 0.10 0.20 0.50 48.40 3.50
12:01:02 AM all 41.10 7.30 0.10 0.20 0.40 47.60 3.30
12:01:03 AM all 39.80 6.90 0.10 0.20 0.50 49.10 3.40
Ce que cela signifie : %steal ~3–4% indique que votre VM attend du CPU physique. Sur des hôtes Bulldozer, un mauvais placement vCPU→module peut amplifier la douleur.
Décision : Coordonnez-vous avec l’équipe hyperviseur : appliquez du pinning CPU aligné sur modules/NUMA, réduisez l’overcommit pour les tenants sensibles à la latence.
Trois mini-histoires d’entreprise (anonymisées, réalistes)
Mini-histoire 1 : L’incident causé par une mauvaise hypothèse
Une entreprise SaaS de taille moyenne a migré une couche d’API en lecture intensive de quad‑cores plus anciens vers des serveurs « 8‑cœurs » flambant neufs. Le deck d’approvisionnement disait qu’ils doublaient les cœurs par nœud, donc ils ont réduit le nombre de nœuds d’un tiers. Le déploiement est resté vert pendant deux heures, puis le 99e percentile de latence a dérivé vers le haut comme une fuite lente.
L’ingénieur on‑call a fait l’habituel : vérifié la latence disque (ok), les erreurs réseau (aucune), l’utilisation CPU (surprenamment modérée). L’indice killer était le placement des threads : les workers du service étaient épinglés par une unité systemd héritée aux CPUs 0–3 « pour la localité cache ». Sur ce matériel, ces CPUs correspondaient à moins de modules que prévu, et les threads chauds se disputaient les ressources front‑end partagées.
Ils ont essayé de remettre à l’échelle — plus de nœuds — mais le budget était déjà engagé. Ils ont donc fait le travail ennuyeux : retiré l’ancien pinning, validé une répartition adaptée aux modules, et ajusté les comptes de workers pour correspondre au débit réel plutôt qu’aux cœurs indiqués. La latence tail est retombée à des niveaux acceptables.
Le postmortem ne portait pas sur AMD ou Intel. Il portait sur une hypothèse : que « 8 cœurs » signifiait « deux fois le parallélisme de 4 ». Sur Bulldozer, la topologie compte suffisamment pour que cette hypothèse soit opérationnellement fausse.
Mini-histoire 2 : L’optimisation qui a mal tourné
Une équipe d’analytique financière exécutait des simulations Monte Carlo la nuit. Ils ont optimisé tout : flags de compilateur, huge pages, threads épinglés, allocateurs mémoire maison. Quelqu’un a remarqué que les runs étaient plus rapides en épinglant deux workers au même pair de modules — moins de communication inter‑module, pensaient‑ils. Ça « marchait » sur un microbenchmark et semblait génial.
Puis ils ont activé une bibliothèque mathématique mise à jour qui utilisait des instructions vectorielles plus larges et une stratégie de threading légèrement différente. Maintenant, deux threads par module martelaient les ressources FP partagées. Le débit a chuté. Pire, c’est devenu bruyant : certaines nuits le job finissait avant les heures ouvrables, d’autres nuits non.
L’équipe a réagi comme les équipes le font : elle a bricolé les comptes de threads et accusé la bibliothèque. La vraie correction fut un pinning conscient de la topologie : garder les threads FP lourds répartis entre modules, et ne rapprocher des threads dans un module que lorsque vous pouvez prouver qu’ils ne se concurrenceront pas sur l’unité partagée pour votre mix d’instructions réel.
La leçon : une optimisation basée sur un « comportement microarchitectural stable » est fragile. Bulldozer a rendu cette fragilité visible, mais le même piège existe sur les CPU modernes avec caches partagés, SMT et scalings de fréquence.
Mini-histoire 3 : La pratique ennuyeuse mais correcte qui a sauvé la mise
Une équipe plateforme interne gérait une flotte mixte : certains nœuds Intel, certains AMD de l’ère Bulldozer. Ils avaient une règle impopulaire : tout changement de capacité exigeait un benchmark spécifique au workload et un artefact sauvegardé (graphiques, configs et versions exactes du noyau/BIOS). Pas d’exceptions. Les gens se plaignaient. Ça ralentissait les « mises à jour simples ».
Une équipe produit voulait déployer une fonctionnalité doublant le parsing JSON et ajoutant une charge TLS. Sur les nœuds Intel, c’était OK. Sur les nœuds Bulldozer, l’utilisation CPU a grimpé et la latence tail a commencé à osciller lors des tests de charge. Parce que l’équipe plateforme disposait de profils de référence, ils ont pu voir immédiatement le delta : plus de cycles en crypto et parsing, plus de pression de branchement, et un IPC pire.
Ils n’ont pas acheté du hardware en panique. Ils ont séparé les flottes : les nœuds Bulldozer ont pris le traitement par lots moins sensible à la latence ; les nœuds Intel ont géré la couche interactive. Ils ont aussi réglé l’affinité IRQ et le governor CPU sur les boîtes Bulldozer restantes pour réduire le jitter. La fonctionnalité a été lancée à l’heure.
Personne n’a reçu de trophée pour « avoir gardé des benchmarks de base ». Mais cela a évité un incident la semaine de la release. En SRE, c’est le trophée.
Erreurs courantes : symptôme → cause racine → correction
1) Symptom : pics de latence alors que le CPU est ~50 %
Cause racine : Threads chauds en contention au sein des modules (front-end / FP partagés) ou épinglés sur un jeu restreint de CPUs.
Correction : Inspecter le placement des threads (ps -L), supprimer l’affinité trop stricte (taskset -pc), répartir les threads chauds à travers les modules et réduire le churn de l’ordonnanceur.
2) Symptom : les chiffres de benchmark sont excellents, la production est pire
Cause racine : Les microbenchmarks tiennent dans le cache, évitent la contention FP et tournent dans des conditions turbo idéales.
Correction : Utiliser des tests de charge soutenue avec des jeux de données représentatifs ; mesurer la fréquence dans le temps (turbostat) et la latence tail, pas seulement le débit.
3) Symptom : performance VM inconsistante entre hôtes
Cause racine : La topologie vCPU n’est pas alignée sur les modules/NUMA physiques ; overcommit et steal time amplifient la contention de module.
Correction : Utiliser du pinning au niveau hôte aligné avec NUMA ; éviter que des VMs sensibles à la latence partagent des modules sous charge ; surveiller %steal.
4) Symptom : tickets « Stockage lent », mais disques inactifs
Cause racine : Composants de la pile stockage liés au CPU (checksums, compression, crypto) ou concentration d’IRQ sur un CPU.
Correction : Valider avec iostat -x et /proc/interrupts ; redistribuer les IRQ ; envisager de désactiver des fonctionnalités lourdes CPU sur ces hôtes ou déplacer la charge.
5) Symptom : chute de performance après des initiatives d’économie d’énergie
Cause racine : governor réglé sur powersave, C‑states profonds, ou politiques BIOS agressives réduisant la fréquence soutenue.
Correction : Mettre le governor sur performance pour les services critiques ; auditer les réglages BIOS de façon homogène sur la flotte ; valider les thermiques.
6) Symptom : deux serveurs « identiques » se comportent différemment
Cause racine : Différences BIOS (turbo, C‑states), microcode, population DIMM affectant NUMA, ou routage d’IRQ différent.
Correction : Standardiser les profils BIOS, confirmer les paquets microcode, comparer lscpu/numactl --hardware, et diff les distributions d’interruptions.
Listes de contrôle / plan étape par étape
Lors de la reprise d’une flotte de l’ère Bulldozer (plan de première semaine)
- Inventaire topologique : capturer
lscpu,numactl --hardwareet versions de noyau pour chaque classe d’hôte. - Standardiser le BIOS et la politique de puissance : confirmer C‑states, comportement turbo et toute limite de puissance ; choisir un profil cohérent.
- Définir et appliquer la politique de governor CPU : « performance » pour les couches sensibles à la latence ; documenter les exceptions.
- Benchmarks de référence par workload : un profil interactif et un profil batch ; stocker sorties et configs.
- Auditer le pinning CPU : grep vos unit files, configs de conteneurs et politiques d’orchestration pour des réglages d’affinité qui supposent des cœurs symétriques.
- Auditer la distribution des IRQ : en particulier les queues NIC ; valider avec des échantillons de
/proc/interruptssous charge. - Règles de placement NUMA : définir quels services sont liés à un nœud vs répartis ; intégrer dans vos outils de déploiement.
Quand un service sous‑performe sur Bulldozer (triage 60 minutes)
- Vérifier l’utilisation par CPU (
mpstat -P ALL) pour détecter le packing. - Vérifier l’affinité du processus (
taskset -pc) et les threads par CPU (ps -L). - Vérifier le governor et la fréquence réelle (sysfs
cpufreq+turbostat). - Vérifier la run queue et les context switches (
vmstat). - Vérifier la topologie NUMA et si vous êtes accidentellement en remote (
numactl --hardware+ politique de placement). - Vérifier les hotspots IRQ (
/proc/interrupts) et les répartir si nécessaire. - Puis seulement : profiler avec
perf statpour confirmer CPU‑bound vs mémoire‑bound.
Lors de la planification d’une migration (ce qu’il faut mesurer avant d’acheter)
- Mesurer débit et latence tail par watt sous charge soutenue.
- Quantifier quelle part de votre temps est passée en FP/vector, parsing branch‑heavy ou stalls mémoire.
- Simuler des scénarios « voisin bruyant » si vous exécutez des charges mixtes ou de la virtualisation.
- Tarifer les licences avec une équivalence de cœurs réaliste (ou ne pas utiliser de cœurs du tout — utiliser des unités de capacité mesurées).
FAQ
1) Bulldozer était‑il « réellement huit cœurs » ?
Il avait huit cœurs entiers sur les modèles FX « 8‑core » (quatre modules), mais certaines ressources clés étaient partagées par module.
Pour beaucoup de charges, ce partage le fait se comporter différemment que huit cœurs pleinement indépendants.
2) Pourquoi Bulldozer a‑t‑il si mal performé sur certains benchmarks mono‑thread ?
Parce que l’accent du design était mis sur le débit avec des ressources partagées, et l’efficacité par thread du front‑end et de l’exécution
n’égalaient pas les concurrents optimisés pour l’IPC mono‑thread à l’époque.
3) L’ordonnanceur du système d’exploitation comptait‑il vraiment autant ?
Oui. Un placement de threads qui accumule des threads chauds dans le même module peut créer de la contention évitable. Un meilleur comportement de l’ordonnanceur aide, mais le pinning, la topologie de virtualisation et le placement des IRQ restent importants.
4) Bulldozer est‑il toujours mauvais pour les serveurs ?
Non. Pour des charges suffisamment parallèles, orientées débit et majoritairement entières, il peut être acceptable. Le risque est que beaucoup de « workloads serveur » ont une queue tail sensible dominée par un petit nombre de threads.
5) Quel est le gain opérationnel le plus simple si je dois composer avec du Bulldozer ?
Corriger la politique de fréquence et le placement : utiliser le bon governor, vérifier les horloges soutenues, retirer le mauvais pinning et empêcher les IRQ de camper sur CPU0.
6) Comment savoir si je souffre de contention de module spécifiquement ?
Vous verrez souvent une latence élevée avec un CPU moyen modéré, une utilisation par CPU inégale, un IPC faible dans perf stat, et une amélioration lorsque vous répartissez les threads ou réduisez les appariements par module.
7) Les architectures AMD plus récentes racontent‑elles la même histoire ?
Non. Les designs AMD ultérieurs se sont éloignés des compromis du module de Bulldozer de façons qui ont considérablement amélioré la performance par cœur
et réduit les surprises de goulet partagé. Ne généralisez pas la douleur Bulldozer à tous les CPU AMD.
8) Dois‑je désactiver les fonctionnalités d’économie d’énergie sur ces systèmes ?
Pour les couches sensibles à la latence, souvent oui — au moins désactiver les politiques les plus agressives et utiliser le governor performance.
Mais validez les thermiques et la capacité électrique ; ne sacrifiez pas la stabilité thermique pour éviter des pics de latence.
9) Cela a‑t‑il de l’importance si j’exécute des conteneurs au lieu de VM ?
Cela peut importer davantage. Les conteneurs facilitent la sur‑souscription CPU accidentelle et l’application d’un pinning naïf. Si votre orchestrateur n’est pas conscient de la topologie, vous pouvez créer des patterns de contention ressemblant à une « lenteur mystère ».
10) Si j’achète du matériel d’occasion pour un laboratoire, Bulldozer en vaut‑il la peine ?
Pour apprendre l’ordonnancement, NUMA et le diagnostic de performance, c’est un excellent professeur. Pour un calcul efficace et prédictible par watt, vous pouvez mieux faire avec des générations plus récentes.
Conclusion : que faire ensuite dans une vraie flotte
La vraie histoire de Bulldozer n’est pas « AMD a échoué ». C’est que les paris architecturaux audacieux ont des conséquences opérationnelles. Les ressources partagées n’apparaissent pas sur une fiche technique comme le nombre de cœurs, mais elles apparaissent dans votre timeline d’incident.
Si vous exploitez encore des boîtes de l’ère Bulldozer, traitez‑les comme une classe matérielle spéciale :
standardisez le BIOS et la politique de fréquence, auditez le pinning et les IRQ, et faites des benchmarks des charges que vous exécutez réellement. Si vous planifiez de la capacité, arrêtez d’utiliser les cœurs labellisés comme unité de calcul. Utilisez le débit mesuré et la latence tail sous charge soutenue.
Étapes pratiques suivantes, dans l’ordre :
- Exécuter la feuille de diagnostic rapide sur un hôte « mauvais » et un hôte « bon », côte à côte.
- Retirer le pinning accidentel, répartir les threads chauds et corriger les hotspots IRQ.
- Verrouiller des paramètres de puissance/fréquence cohérents appropriés à vos SLO.
- Décider quelles charges appartiennent à ce silicium — et migrer les autres.
Bulldozer voulait un monde où tout était parfaitement parallèle. La production n’est pas ce monde. La production est désordonnée, partagée,
pilotée par des interruptions, et pleine de petits goulots d’étranglement déguisés. Planifiez en conséquence.