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.
Les benchmarks
Section intitulée « Les benchmarks »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.
Résumer plusieurs résultats : quelle moyenne ?
Section intitulée « Résumer plusieurs résultats : quelle moyenne ? »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) :
| Programme | Machine A | Machine B | B normalisé par A | A normalisé par B |
|---|---|---|---|---|
| P1 | 2 | 4 | 2,0 | 0,5 |
| P2 | 50 | 25 | 0,5 | 2,0 |
| P3 | 10 | 10 | 1,0 | 1,0 |
| Moyenne arithmétique | 1,17 | 1,17 | ||
| Moyenne géométrique | 1,00 | 1,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, , 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.
| Pour résumer… | utilisez |
|---|---|
| des durées, quand la charge réelle exécute chaque programme | la 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 travail | la moyenne harmonique |
| des ratios normalisés par une référence | la moyenne géométrique |
Les jeux de ratios
Section intitulée « Les jeux de ratios »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).
Accélération et efficacité
Section intitulée « Accélération et efficacité »Passons au parallélisme. On mesure l’accélération d’un programme parallèle par le speedup :
où est la durée de la meilleure implémentation séquentielle, et la durée de la version parallèle sur processeurs. L’efficacité mesure la fraction du temps où chaque processeur travaille utilement :
Idéalement, et . En pratique, les communications, les synchronisations et la partie non parallélisable font baisser l’efficacité.
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 cette fraction séquentielle du temps d’exécution sur un processeur. Avec processeurs, la partie parallélisable, , est divisée par , mais pas la partie séquentielle :
- est la fraction séquentielle du travail (mesurée sur l’exécution séquentielle) ;
- est la fraction parallélisable ;
- 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 . Avec 5 % de code séquentiel, le speedup plafonne à 20 ; avec 10 %, à 10.
Calculer ainsi à 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 .
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 . Même une inférence infiniment rapide ne ferait gagner qu’un facteur .
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 .
Mesurons sur la machine parallèle : , où est le temps séquentiel et le temps de la partie parallèle. Sur un seul processeur, le même problème prendrait . En notant la fraction séquentielle mesurée sur l’exécution parallèle, on obtient le speedup à l’échelle (scaled speedup) :
C’est la loi de Gustafson-Barsis. Le speedup croît alors linéairement avec : avec %, on obtient 7,3 sur 8 processeurs et 57,7 sur 64.
| Amdahl | Gustafson-Barsis | |
|---|---|---|
| Hypothèse | taille du problème fixe | temps d’exécution fixe, problème qui grandit |
| Fraction séquentielle | , mesurée sur l’exécution séquentielle | , mesurée sur l’exécution parallèle |
| Speedup | , plafonné à | , non plafonné |
| Question posée | « combien plus vite ? » | « combien plus gros, dans le même temps ? » |
| Passage à l’échelle | fort (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 % sur 8 processeurs, la partie séquentielle ne représente que % du travail total mesuré en séquentiel, et Amdahl avec % 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}")En résumé
Section intitulée « En résumé »- 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 (avec la meilleure version séquentielle), efficacité .
- Amdahl : ; elle s’applique à toute amélioration partielle.
- Gustafson-Barsis : , quand la taille du problème grandit avec .
- Passage à l’échelle fort (problème fixe) contre faible (problème qui grandit) : précisez toujours lequel vous mesurez.
Exercices
Section intitulée « Exercices »Exercice · Speedup et efficacité
facileUn entraînement de modèle dure 400 min sur un GPU et 70 min sur 8 GPU.
- Calculez le speedup et l’efficacité.
- En supposant la loi d’Amdahl, quelle est la fraction séquentielle ? Quel est le speedup maximal possible ?
Voir la solution
- et .
- donne . Le speedup maximal vaut .
En code
t1, t8, n = 400, 70, 8 # durées (min) sur 1 et 8 GPU
S = t1 / t8 # speedupE = 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.71print(round(f, 4), round(1 / f, 1)) # 0.0571 17.5 : speedup maximalt1 <- 400; t8 <- 70; n <- 8 # durées (min) sur 1 et 8 GPU
S <- t1 / t8 # speedupE <- S / n # efficacitéf <- (1 / S - 1 / n) / (1 - 1 / n) # fraction séquentielle (Amdahl, Karp-Flatt)round(c(S = S, E = E, f = f, S_max = 1 / f), 4) # 5.7143 0.7143 0.0571 17.5Exercice · Où investir ?
moyenUne 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) .
- (b) .
- Les deux : la nouvelle durée relative vaut , soit un gain de .
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 % 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 GPUprint(gain({"base": 10})) # (0.73, 1.37) option (b), le cacheduree, 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 %parts <- c(base = 0.3, inference = 0.5, reseau = 0.2) # fractions du temps d'une requête
# Amdahl généralisée : chaque part est divisée par son facteur d'accélérationgain <- function(acc) { facteurs <- c(base = 1, inference = 1, reseau = 1) facteurs[names(acc)] <- acc nouvelle <- sum(parts / facteurs) c(duree = nouvelle, gain = 1 / nouvelle)}gain(c(inference = 5)) # 0.60 1.67 option (a), le GPUgain(c(base = 10)) # 0.73 1.37 option (b), le cacheg <- gain(c(inference = 5, base = 10))c(g, part_reseau = 0.2 / g[["duree"]]) # 0.33 3.03 0.61 : le réseau pèse 61 %Exercice · Amdahl ou Gustafson ?
difficileUne 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).
- Quel est le speedup à l’échelle selon Gustafson-Barsis ?
- Quelle serait la fraction séquentielle 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.
- 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
- , donc .
- Sur un processeur : min. La fraction séquentielle vaut . Amdahl : . Les deux lois donnent bien le même chiffre, elles ne mesurent simplement pas au même endroit.
- Même problème sur 128 processeurs (Amdahl, ) : , soit une durée de min (le temps séquentiel de 3 min, plus 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 .
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èleS_gustafson = phi + (1 - phi) * Nt1 = 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'Amdahlamdahl = 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 processeursprint(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 grost_seq <- 3; t_tot <- 60; N <- 64 # minutes de partie séquentielle, durée, processeurs
phi <- t_seq / t_tot # fraction séquentielle mesurée en parallèleS_gustafson <- phi + (1 - phi) * Nt1 <- 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'Amdahlamdahl <- function(f, N) 1 / (f + (1 - f) / N)c(S_gustafson, t1) # 60.85 3651c(f, amdahl(f, N)) # 0.00082 60.85 : même speedup
S128 <- amdahl(f, 128) # même problème sur 128 processeursround(c(S128, t1 / S128, phi + (1 - phi) * 128), 2) # 115.90 31.50 121.65