Aller au contenu

Comparez avec des benchmarks et passez à l'échelle

Le script d’agrégation des ventes tourne maintenant en Parquet. L’équipe veut aller plus loin : louer une machine à 16 cœurs, ou même une grappe de 64. Un fournisseur promet « des performances 30 % supérieures » sur son benchmark maison. Deux questions se posent alors : comment comparer honnêtement deux machines, et jusqu’où peut-on accélérer un programme en ajoutant des processeurs ?

Ce chapitre répond aux deux, avec deux outils : la bonne façon de résumer un benchmark, et les lois d’Amdahl et de Gustafson.

Un benchmark (banc d’essai) est une charge de travail standardisée, exécutée sur plusieurs systèmes pour les comparer. On en distingue plusieurs familles :

  • les micro-benchmarks mesurent une opération élémentaire : une addition, un accès mémoire, un appel système, une lecture de fichier ;
  • les noyaux (kernels) isolent le cœur d’un calcul fréquent : un produit de matrices, une transformée de Fourier ;
  • les benchmarks applicatifs exécutent des programmes réalistes : SPEC CPU (programmes de calcul), TPC-C (transactions de base de données), MLPerf (entraînement et inférence de modèles d’apprentissage).

Plus un benchmark est proche de votre charge réelle, plus il est pertinent pour vous. Un excellent score SPEC ne dit rien d’une application limitée par le disque.

Un benchmark comporte souvent plusieurs programmes. Pour résumer, on est tenté de faire une moyenne. Mais laquelle ? Prenons deux machines A et B et trois programmes (durées en secondes) :

ProgrammeMachine AMachine BB normalisé par AA normalisé par B
P1242,00,5
P250250,52,0
P310101,01,0
Moyenne arithmétique1,171,17
Moyenne géométrique1,001,00

Normalisé par A, B semble 17 % plus lente. Normalisé par B, c’est A qui semble 17 % plus lente ! La moyenne arithmétique de ratios fait dépendre la conclusion du choix de la référence. La moyenne géométrique, (∏j=1nxj)1/n\left(\prod_{j=1}^{n} x_j\right)^{1/n}, n’a pas ce défaut : le classement ne dépend pas de la machine de référence. C’est pourquoi SPEC l’utilise pour agréger ses ratios.

Mais attention, la moyenne géométrique répond à une question précise : « en moyenne, d’un facteur combien un programme est-il plus rapide ? ». Si votre vraie charge consiste à exécuter les trois programmes une fois chacun, la bonne métrique est la durée totale : 62 s pour A, 39 s pour B. B est alors nettement meilleure, parce que le programme P2, le plus long, y va deux fois plus vite.

P1P2P30,00,51,01,52,0ratio des durées2,00,51,0chacune semble17 % plus lente…B normalisée par AP1P2P30,00,51,01,52,00,52,01,0A normalisée par BAB01020304050607062 s39 sdurée totaledes 3 programmesmoyenne arithmétique des ratios : 1,17moyenne géométrique : 1,00
La moyenne arithmétique des ratios vaut 1,17 quelle que soit la référence : chaque machine « perd » à tour de rôle. La moyenne géométrique reste à 1. Sur la durée totale, B l'emporte nettement.
Pour résumer…utilisez
des durées, quand la charge réelle exécute chaque programmela somme ou la moyenne arithmétique des durées (éventuellement pondérée)
des débits (tâches/s) sur une même quantité de travailla moyenne harmonique
des ratios normalisés par une référencela moyenne géométrique

On appelle jeu de ratios (ratio game) l’art de présenter des chiffres exacts pour leur faire dire ce qu’on veut. À repérer :

  • choisir la base qui arrange, comme dans le tableau ci-dessus ;
  • choisir une métrique ratio plutôt qu’une métrique absolue : « deux fois plus de requêtes par watt » peut cacher un débit divisé par deux ;
  • comparer des gains relatifs : « +50 % » sur un temps de 2 ms ne pèse rien face à « +5 % » sur un temps de 10 s ;
  • calculer des pourcentages dans des conditions différentes (autre charge, autre version, autre machine).

Passons au parallélisme. On mesure l’accélération d’un programme parallèle par le speedup :

S(n)=t1tnS(n) = \frac{t_1}{t_n}

où t1t_1 est la durée de la meilleure implémentation séquentielle, et tnt_n la durée de la version parallèle sur nn processeurs. L’efficacité mesure la fraction du temps où chaque processeur travaille utilement :

E(n)=S(n)n=t1n tnE(n) = \frac{S(n)}{n} = \frac{t_1}{n \, t_n}

Idéalement, S(n)=nS(n) = n et E(n)=1E(n) = 1. En pratique, les communications, les synchronisations et la partie non parallélisable font baisser l’efficacité.

124816nombre de processus n0,02,55,07,510,012,515,0speedup S(n) = t₁ / tₙ1,903,536,008,57idéal S = nAmdahl, f ≈ 0,048mesures124816nombre de processus n0,00,20,40,60,81,01,2efficacité E(n) = S / n1,000,950,880,750,54
Les mesures s'écartent de plus en plus de la droite idéale et suivent la loi d'Amdahl ; l'efficacité tombe à 0,54 sur 16 processus.

La loi d’Amdahl : la partie séquentielle plafonne tout

Section intitulée « La loi d’Amdahl : la partie séquentielle plafonne tout »

Pourquoi l’efficacité s’effondre-t-elle ? Parce qu’une partie du programme reste séquentielle : lire la configuration, répartir le travail, fusionner les résultats. Notons ff cette fraction séquentielle du temps d’exécution sur un processeur. Avec NN processeurs, la partie parallélisable, 1−f1 - f, est divisée par NN, mais pas la partie séquentielle :

tN=(f+1−fN)t1⇒S(N)=1f+1−fN≤1ft_N = \left( f + \frac{1 - f}{N} \right) t_1 \qquad \Rightarrow \qquad S(N) = \frac{1}{f + \dfrac{1 - f}{N}} \leq \frac{1}{f}
  • ff est la fraction séquentielle du travail (mesurée sur l’exécution séquentielle) ;
  • 1−f1 - f est la fraction parallélisable ;
  • NN est le nombre de processeurs.

C’est la loi d’Amdahl. Sa conclusion est brutale : quel que soit le nombre de processeurs, le speedup ne dépassera jamais 1/f1/f. Avec 5 % de code séquentiel, le speedup plafonne à 20 ; avec 10 %, à 10.

nombre de processeurs N (échelle log)speedup S(N)186410241020idéal S = Nf = 5 %f = 10 %

Calculer ainsi ff à partir de chaque mesure (c’est la métrique de Karp-Flatt) donne 0,050, 0,044, 0,048 et 0,058 pour 2, 4, 8 et 16 processus. La valeur reste proche de 5 %, ce qui confirme le modèle d’Amdahl ; sa légère hausse à 16 processus trahit des surcoûts de coordination qui grandissent avec nn.

La loi d’Amdahl s’applique en fait à toute amélioration partielle, pas seulement au parallélisme. Si l’inférence d’un modèle représente 60 % du temps d’une requête et que vous l’accélérez d’un facteur 4, le gain global vaut 1/(0,4+0,6/4)≈1,821/(0{,}4 + 0{,}6/4) \approx 1{,}82. Même une inférence infiniment rapide ne ferait gagner qu’un facteur 1/0,4=2,51/0{,}4 = 2{,}5.

La loi de Gustafson : et si le problème grandissait ?

Section intitulée « La loi de Gustafson : et si le problème grandissait ? »

Amdahl raisonne à taille de problème fixe : on veut traiter le même travail plus vite. Mais souvent, quand on dispose de plus de processeurs, on traite un plus gros problème dans le même temps : plus de données, un maillage plus fin, un modèle plus grand. La partie séquentielle (lire la configuration, fusionner) reste à peu près constante, tandis que la partie parallèle grandit avec NN.

Mesurons sur la machine parallèle : tN=ts+tpt_N = t_s + t_p, où tst_s est le temps séquentiel et tpt_p le temps de la partie parallèle. Sur un seul processeur, le même problème prendrait t1=ts+Ntpt_1 = t_s + N t_p. En notant φ=ts/(ts+tp)\varphi = t_s/(t_s + t_p) la fraction séquentielle mesurée sur l’exécution parallèle, on obtient le speedup à l’échelle (scaled speedup) :

S(N)=t1tN=φ+(1−φ) NS(N) = \frac{t_1}{t_N} = \varphi + (1 - \varphi)\, N

C’est la loi de Gustafson-Barsis. Le speedup croît alors linéairement avec NN : avec φ=10\varphi = 10 %, on obtient 7,3 sur 8 processeurs et 57,7 sur 64.

0,00,51,0durée relative (t₁ = 1)N = 1N = 4N = 16N = 64S = 1,0S = 3,5S = 9,1S = 15,4Amdahl : même problème (f = 5 %)la durée plafonne à la partie séquentiellepartie séquentiellepartie parallèle0,00,51,0durée relative (constante)N = 1N = 4N = 16N = 64S = 1,0S = 3,7S = 14,5S = 57,7Gustafson : même durée (φ = 10 %)le problème grandit avec N
À taille fixe (Amdahl), seule la partie parallèle rétrécit et la partie séquentielle finit par tout occuper. À durée fixe (Gustafson), chaque processeur ajouté apporte son travail parallèle et le speedup reste presque proportionnel à N.
AmdahlGustafson-Barsis
Hypothèsetaille du problème fixetemps d’exécution fixe, problème qui grandit
Fraction séquentielleff, mesurée sur l’exécution séquentielleφ\varphi, mesurée sur l’exécution parallèle
Speedup1/(f+(1−f)/N)1/(f + (1 - f)/N), plafonné à 1/f1/fφ+(1−φ)N\varphi + (1 - \varphi) N, non plafonné
Question posée« combien plus vite ? »« combien plus gros, dans le même temps ? »
Passage à l’échellefort (strong scaling)faible (weak scaling)

Les deux lois ne se contredisent pas : elles ne mesurent pas la fraction séquentielle au même endroit. Avec φ=10\varphi = 10 % sur 8 processeurs, la partie séquentielle ne représente que 0,1/(0,1+8×0,9)≈1,40{,}1/(0{,}1 + 8 \times 0{,}9) \approx 1{,}4 % du travail total mesuré en séquentiel, et Amdahl avec f=1,4f = 1{,}4 % redonne bien 7,3.

def amdahl(f, N):
return 1 / (f + (1 - f) / N)
def gustafson(phi, N):
return phi + (1 - phi) * N
for N in (2, 8, 64, 1024):
print(f"N = {N:4d} : Amdahl(5 %) = {amdahl(0.05, N):5.2f} "
f"Gustafson(10 %) = {gustafson(0.10, N):7.1f}")
  • Un benchmark compare des systèmes sur une charge standard : micro-benchmark, noyau ou benchmark applicatif (SPEC CPU, TPC-C, MLPerf). Il n’a de valeur que s’il ressemble à votre charge.
  • Pour des ratios normalisés, utilisez la moyenne géométrique ; la moyenne arithmétique fait dépendre le classement de la référence. Méfiez-vous des jeux de ratios.
  • Speedup S(n)=t1/tnS(n) = t_1/t_n (avec la meilleure version séquentielle), efficacité E(n)=S(n)/nE(n) = S(n)/n.
  • Amdahl : S(N)=1/(f+(1−f)/N)≤1/fS(N) = 1/(f + (1 - f)/N) \leq 1/f ; elle s’applique à toute amélioration partielle.
  • Gustafson-Barsis : S(N)=φ+(1−φ)NS(N) = \varphi + (1 - \varphi) N, quand la taille du problème grandit avec NN.
  • Passage à l’échelle fort (problème fixe) contre faible (problème qui grandit) : précisez toujours lequel vous mesurez.

Exercice · Speedup et efficacité

facile

Un entraînement de modèle dure 400 min sur un GPU et 70 min sur 8 GPU.

  1. Calculez le speedup et l’efficacité.
  2. En supposant la loi d’Amdahl, quelle est la fraction séquentielle ff ? Quel est le speedup maximal possible ?
Voir la solution
  1. S(8)=400/70≈5,71S(8) = 400/70 \approx 5{,}71 et E(8)=5,71/8≈0,71E(8) = 5{,}71/8 \approx 0{,}71.
  2. 1/S=f+(1−f)/81/S = f + (1 - f)/8 donne f=(1/S−1/8)/(1−1/8)=(0,175−0,125)/0,875≈0,057f = (1/S - 1/8)/(1 - 1/8) = (0{,}175 - 0{,}125)/0{,}875 \approx 0{,}057. Le speedup maximal vaut 1/f≈17,51/f \approx 17{,}5.

En code

t1, t8, n = 400, 70, 8 # durées (min) sur 1 et 8 GPU
S = t1 / t8 # speedup
E = S / n # efficacité
f = (1 / S - 1 / n) / (1 - 1 / n) # fraction séquentielle (Amdahl, Karp-Flatt)
print(round(S, 2), round(E, 2)) # 5.71 0.71
print(round(f, 4), round(1 / f, 1)) # 0.0571 17.5 : speedup maximal

Exercice · Où investir ?

moyen

Une requête à une API de recommandation se décompose ainsi : 30 % de lecture en base, 50 % d’inférence, 20 % de sérialisation et de réseau. Deux options :

  • (a) un GPU qui rend l’inférence 5 fois plus rapide ;
  • (b) un cache qui rend la lecture en base 10 fois plus rapide.

Calculez le gain global de chaque option, puis des deux combinées.

Voir la solution
  • (a) 1/(0,5+0,5/5)=1/0,6≈1,671/(0{,}5 + 0{,}5/5) = 1/0{,}6 \approx 1{,}67.
  • (b) 1/(0,7+0,3/10)=1/0,73≈1,371/(0{,}7 + 0{,}3/10) = 1/0{,}73 \approx 1{,}37.
  • Les deux : la nouvelle durée relative vaut 0,3/10+0,5/5+0,2=0,03+0,1+0,2=0,330{,}3/10 + 0{,}5/5 + 0{,}2 = 0{,}03 + 0{,}1 + 0{,}2 = 0{,}33, soit un gain de 1/0,33≈3,031/0{,}33 \approx 3{,}03.

Le GPU rapporte plus, car il touche la plus grande part du temps. Une fois les deux appliqués, la sérialisation et le réseau représentent 0,2/0,33≈610{,}2/0{,}33 \approx 61 % du temps restant : c’est le nouveau goulot.

En code

parts = {"base": 0.3, "inférence": 0.5, "réseau": 0.2} # fractions du temps d'une requête
def gain(accelerations):
"""Amdahl généralisée : chaque part est divisée par son facteur d'accélération."""
nouvelle = sum(p / accelerations.get(nom, 1) for nom, p in parts.items())
return round(nouvelle, 3), round(1 / nouvelle, 2)
print(gain({"inférence": 5})) # (0.6, 1.67) option (a), le GPU
print(gain({"base": 10})) # (0.73, 1.37) option (b), le cache
duree, g = gain({"inférence": 5, "base": 10})
print(duree, g, round(0.2 / duree, 2)) # 0.33 3.03 0.61 : le réseau pèse 61 %

Exercice · Amdahl ou Gustafson ?

difficile

Une simulation météo dure 1 heure sur 64 processeurs. On mesure pendant cette exécution 3 minutes de partie séquentielle (lecture des données et écriture des résultats).

  1. Quel est le speedup à l’échelle selon Gustafson-Barsis ?
  2. Quelle serait la fraction séquentielle ff au sens d’Amdahl, pour ce même problème exécuté sur un seul processeur ? Vérifiez que la loi d’Amdahl redonne le même speedup.
  3. Le centre de calcul double le nombre de processeurs. Que peut espérer l’équipe si elle garde la même taille de problème ? Et si elle double la résolution (deux fois plus de calcul parallèle) ?
Voir la solution
  1. φ=3/60=0,05\varphi = 3/60 = 0{,}05, donc S(64)=0,05+0,95×64=60,85S(64) = 0{,}05 + 0{,}95 \times 64 = 60{,}85.
  2. Sur un processeur : t1=3+64×57=3 651t_1 = 3 + 64 \times 57 = 3\,651 min. La fraction séquentielle vaut f=3/3 651≈0,000 82f = 3/3\,651 \approx 0{,}000\,82. Amdahl : S=1/(0,000 82+0,999 18/64)≈60,85S = 1/(0{,}000\,82 + 0{,}999\,18/64) \approx 60{,}85. Les deux lois donnent bien le même chiffre, elles ne mesurent simplement pas ff au même endroit.
  3. Même problème sur 128 processeurs (Amdahl, f≈0,000 82f \approx 0{,}000\,82) : S≈1/(0,000 82+0,999 18/128)≈115,9S \approx 1/(0{,}000\,82 + 0{,}999\,18/128) \approx 115{,}9, soit une durée de 3 651/115,9≈31,53\,651/115{,}9 \approx 31{,}5 min (le temps séquentiel de 3 min, plus 64×57/128=28,564 \times 57/128 = 28{,}5 min de calcul parallèle). Résolution doublée sur 128 processeurs (Gustafson) : la partie parallèle double et se répartit sur deux fois plus de processeurs, donc la durée reste d’environ 1 heure, pour un problème deux fois plus gros, avec un speedup à l’échelle de 0,05+0,95×128=121,650{,}05 + 0{,}95 \times 128 = 121{,}65.

En code

t_seq, t_tot, N = 3, 60, 64 # minutes de partie séquentielle, durée, processeurs
phi = t_seq / t_tot # fraction séquentielle mesurée en parallèle
S_gustafson = phi + (1 - phi) * N
t1 = t_seq + N * (t_tot - t_seq) # même problème sur un seul processeur (min)
f = t_seq / t1 # fraction séquentielle au sens d'Amdahl
amdahl = lambda f, N: 1 / (f + (1 - f) / N)
print(round(S_gustafson, 2), t1, round(f, 5), round(amdahl(f, N), 2))
# 60.85 3651 0.00082 60.85 : les deux lois donnent le même speedup
S128 = amdahl(f, 128) # même problème sur 128 processeurs
print(round(S128, 1), round(t1 / S128, 1), round(phi + (1 - phi) * 128, 2))
# 115.9 31.5 121.65 : 31,5 min à taille fixe, ou 1 h pour un problème deux fois plus gros