Estimer la consommation avec des sondes logicielles
RAPL
La technologie RAPL (Running Average Power Limit) fournit un ensemble de compteurs donnant accès à des mesures de consommation énergétique et de puissance. Cette technologie s’appuie sur un ensemble de sondes matérielles et sur un modèle de puissance. Elle est exposée par les processeurs Intel à partir de la génération Sandy Bridge. Il est important également de noter que cette technologie doit avoir un accès direct aux compteurs, par le biais du noyau Linux, ce qui exclut de l’utiliser au sein d’une machine virtuelle.
Pour savoir si son architecture est compatible avec ces contraintes, on peut tout d’abord inspecter les informations concernant le micro-processeur :
$ lscpu
Architecture : x86_64
Mode(s) opératoire(s) des processeurs : 32-bit, 64-bit
Boutisme : Little Endian
Address sizes: 39 bits physical, 48 bits virtual
Processeur(s) : 4
[...]
Famille de processeur : 6
[...]
Ici, il s’agit donc d’un processeur de 6ème génération, autrement dit Skylake, famille ultérieure à Sandy Bridge. Pour en avoir le cœur net :
$ cat /sys/devices/cpu/caps/pmu_name
skylake
Afin de bien comprendre comment les relevés des compteurs sont effectués, il faut avoir une idée du découpage logique des unités de consommation1. Le découpage s’effectue de la manière suivante :
- le niveau package correspond à la consommation totale de tout le bloc processeur, incluant les cœurs, le cache L3, le contrôleur de mémoire et le processeur graphique intégré (GPU, s’il y en a un) :
- le niveau core correspond à la consommation de l’ensemble des cœurs CPU ;
- le niveau uncore correspond à la consommation du processeur graphique intégré. On a donc \(C_{core} + C_{uncore} \geq C_{package}\) ;
- le niveau dram, séparé du processeur.
Le schema de la figure 1 nous montre une vue synthétique du découpage logique des compteurs RAPL.
Figure 1: Découpage logique des compteurs RAPL pour un processeur unique (à plusieurs cœurs)..
Il existe plusieurs outils et bibliothèques capables de lire les compteurs RAPL sous Linux. En s’appuyant directement sur le noyau Linux, il est possible d’accéder à ces valeurs de trois manières différentes2 :
- en lisant les fichiers du dossier
/sys/class/powercap/intel-rapl/intel-rapl:0
en utilisant l’interfacepowercap
3. Cette interface ne requiert aucune permission spéciale et a été introduite dans la version 3.13 du noyau Linux ; - en utilisant l’API
perf_event
(ouperf
)4. Cette API est un outil d’analyse de performance capable de remonter à l’utilisateur un grand nombre de métriques liées aux événements matériels sur une machine. Elle est disponible dans le noyau Linux depuis la version 2.6.31 et intègre les compteurs RAPL depuis la version 3.14. L’utilisation deperf_event
nécessitera un accès superutilisateur. La liste de tous les événements disponibles peut être trouvée parperf list
ou en consultant les fichiers du dosser/sys/bus/event_source/devices/power/events/
; - en accédant directement aux MSR bruts. Les MSR (Model Specific
Registers) correspondent aux registres de contrôle du jeu
d’instructions x86 utilisé pour suivre l’exécution d’un programme, et
faire de la mesure de performance. Le module
msr
(chargé avecmodprobe msr
) fournit une interface pour accéder aux MSR, situés dans/dev/cpu/CPUNUM/msr
(CPUNUM
étant ici le numéro du processeur, tel qu’il apparaît dans le fichier/proc/cpuinfo
).
Note importante : Il semble que dans les versions la version 5.4.77 du noyau Linux, et plus précisément depuis le commit 19f6d91bdad42200aac557a683c17b1f65ee6c94 (10 novembre 2020), l’accès aux fichiers de mesure d’énergie de powercap soit restreint aux utilisateurs privilégiés. L’explication est qu’un accès non restreint aux mesures d’énergie expose le système à des failles de sécurité. On pourra trouver des exemples d’exploitation possible dans un article récent publié dans à ASIA-CCS’21 (Zhang et al. 2021), et qui démontre entre autres comment exploiter des signatures énergétiques afin de déterminer à quelles pages web un utilisateur accède.
Il y a plusieurs manières de contourner cette restriction. La première est d’effectuer toutes les mesures de consommation en tant que superutilisateur, ce qui implique donc de lancer tous les scripts de mesure qui nécessitent un accès à l’interface powercap (par exemple ceux qui concernent PyJoules ou CodeCarbon) en tant que superutilisateur. Une autre manière de contourner cette restriction est d’outrepasser les mesures de sécurité imposées par le noyau en donnant à tous les utilisateurs les droits en lecture sur les fichiers
/sys/class/powercap/intel-rapl
:$ sudo chmod -R a+r /sys/class/powercap/intel-rapl
Par la suite, nous détaillerons essentiellement l’utilisation du middleware PowerAPI (INRIA 2020), qui s’appuie entre autres sur les sondes RAPL, mais dont l’objectif, orthogonal, est de fournir un cadre permettant d’automatiser au niveau d’un cluster la mesure de consommation s’appuyant sur des sondes logicielles.
PowerAPI
Comme l’indique la page Web dédiée, PowerAPI (INRIA 2020) fournit des outils dédiés au déploiement de sondes de mesure de puissance au niveau logiciel. Ces sondes permettent d’évaluer la consommation électrique d’un nœud de calcul ou d’un processus en temps réel.
L’architecture de PowerAPI est pensée pour découpler la partie mesure à proprement parler (réalisée à l’aide de sondes logicielles) de la partie agrégation des mesures pour calculer une estimation globale de la consommation. La partie mesure est réalisée par le déploiement de capteurs (sensors). Ces capteurs s’appuient sur les outils systèmes pour effectuer des mesures brutes de consommation, et peupler une base de données. Afin d’exploiter ces données, il faut déployer une formule (formula), qui se charge de récupérer les mesures brutes, de les agréger (par exemple s’il y a plusieurs capteurs), et de les transformer en estimations de consommation exploitables, dont elle peuplera une base de données de sortie.
Prérequis
Avant même de déployer des sondes logicielles propulsées par PowerAPI, il est préférable d’avoir sous la main un Système de Gestion de Bases de Données installé et configuré pour pouvoir accueillir les données recueillies par les sondes. Par défaut, les différentes sondes et formules fournies par PowerAPI sont capables d’exporter les données dans une base MongoDB, ce qui reste une manière simple et efficace de stocker et d’exploiter les données exportées (un export sous forme de fichiers CSV est également possible). Pour un export MongoDB, il faut donc avoir une instance de ce Système de Gestion de Bases de Données déployée.
On peut par exemple déployer cette instance localement en lançant le serveur de la manière suivante :
$ mongod --shardsvr --dbpath powerapi --port 27021
En outre, la plupart des capteurs et formules disponibles sur la page de PowerAPI sont fournis non seulement sous forme de code, mais également sous forme de conteneur Docker prêt à l’emploi. Ainsi, l’installation de Docker, même si elle n’est pas strito sensu nécessaire, facilite grandement le recueil des mesures.
Enfin, le capteur HWPC fourni pour la mesure des indicateurs de
performance s’appuie sur la technologie RAPL (voir sec. 1), ce qui
signifie que pour fonctionner, il a besoin d’être déployé sur une
machine compatible avec cette technologie (processeur Intel à partir de
la génération Sandy Bridge, accès direct aux compteurs, donc
pas de machine virtuelle). En outre, pour effectuer ses relevés de
consommation, ce capteur a besoin d’un accès privilégié à la machine
hôte. Très concrètement, cela signifie que si on déploie ce capteur
directement sur la machine hôte sans passer par Docker, il nous faudra
un accès superutilisateur. Cela peut être contourné si l’on utilise la
version Docker, qui peut avoir un accès privilégié au système hôte. Cela
nécessitera bien entendu que l’utilisateur que l’on utilise soit bien
autorisé à lancer des conteneurs docker avec l’option --priviledged
.
Déployer le capteur HWPC
L’un des capteurs fournis par PowerAPI est le capteur HWPC-Sensor
(Hardware Performance Counters Sensor). Ce capteur s’appuie sur
l’API perf
que nous avons évoquée en sec. 1.
Le capteur peut être déployé en utilisant le conteneur Docker fourni :
$ docker run --net=host --privileged --name powerapi-sensor \
-v /sys:/sys -v /var/lib/docker/containers:/var/lib/docker/containers:ro \
-v /tmp/powerapi-sensor-reporting:/reporting \
powerapi/hwpc-sensor:latest \
-n $SENSOR_NAME \
-r "mongodb" -U $MONGO_ADDR -D $DB -C $COL \
-s "rapl" -o -e RAPL_ENERGY_PKG \
-s "msr" -e "TSC" -e "APERF" -e "MPERF" \
-c "core" -e "CPU_CLK_THREAD_UNHALTED:REF_P" \
-e "CPU_CLK_THREAD_UNHALTED:THREAD_P" \
-e "LLC_MISSES" -e "INSTRUCTIONS_RETIRED"
Ici, il faut remplacer les variables SENSOR_NAME
,
MONGO_ADDR
, DB
et COL
,
représentant respectivement le nom du capteur (qu’on peut choisir
arbitrairement, mais unique pour une campagne de mesure), l’adresse de
l’instance de MongoDB, le nom de la base de données dans laquelle
exporter, et le nom de la collection Mongo utilisée, par les valeurs qui
conviennent. Ainsi, par exemple :
SENSOR_NAME="hwpcSensor"
MONGO_ADDR="mongodb://127.0.0.1:27021"
DB="powerapi"
COL="hwpcMeasures"
Le paramètre -e
permet de spécifier la liste des
événements que l’on souhaite mesurer. On doit spécifier un nom de groupe
pour chaque groupe d’événements que l’on souhaite analyser : il s’agit
du paramètre -c
. La liste de événements à mesurer dépend de
la formule qui sera utilisée pour calculer la consommation. Les
paramètres donnés dans la commande ci-dessus sont ceux qui sont
préconisés pour l’utilisation de la formule SmartWatts, que
nous allons utiliser.
Une fois que le capteur HWPC est lancé, on peut vérifier la collecte des données en se connectant à la base Mongo concernée.
$ mongo mongodb://localhost:27021
MongoDB shell version v3.6.8
connecting to: mongodb://localhost:27021
[...]
test@machine:27021 >>> use powerapi
switched to db powerapi
powerapi@machine:27021 >>> db.hwpcMeasures.find().pretty()
{
"_id" : ObjectId("600bc903e0f2d02f11007902"),
"timestamp" : ISODate("2021-01-23T06:58:11.134Z"),
"sensor" : "hwpcSensor",
"target" : "all",
"groups" : {
"rapl" : {
"0" : {
"3" : {
"RAPL_ENERGY_PKG" : 34340864,
"time_enabled" : 1382541,
"time_running" : 1382541
}
}
},
"msr" : {
"0" : {
"3" : {
"MPERF" : 3257807,
"APERF" : 1768703,
"TSC" : 3281312,
"time_enabled" : 1367132,
"time_running" : 1367132
},
"0" : {
"MPERF" : 3174711,
"APERF" : 1721538,
"TSC" : 3297630,
"time_enabled" : 1373942,
"time_running" : 1373942
},
"1" : {
"MPERF" : 2109174,
"APERF" : 1149755,
"TSC" : 3298888,
"time_enabled" : 1374444,
"time_running" : 1374444
},
"2" : {
"MPERF" : 3264342,
"APERF" : 1771445,
"TSC" : 3284442,
"time_enabled" : 1368436,
"time_running" : 1368436
}
}
}
}
}
[...]
L’examen de la collection nous montre que le capteur HWPC exporte ses
données sous deux cibles (target
) distinctes :
all
et powerapi-sensor
. La cible
powerapi-sensor
contient le groupe core
qui se
focalise sur les événements paramétrés avec -c core
lors du
déploiement du capteur. La cible all
, quant à elle, relève
les événements liés aux groupes msr
et
rapl
.
Le groupe rapl
a un statut spécial. Il permet de relever
les événements liés à la consommation générale du processeur, en
utilisant les sondes RAPL exposées par celui-ci. Selon l’architecture du
processeur, les trois types d’événements suivants peuvent être
disponibles :
RAPL_ENERGY_PKG
: cet événement est dédié à la consommation de l’intégralité du socket CPU ;RAPL_ENERGY_DRAM
: cet événement est dédié à la consommation de la RAM ;RAPL_ENERGY_GPU
: cet événement est dédié au processeur graphique intégré.
Sur ma machine, l’événement RAPL_ENERGY_DRAM
n’est pas
disponible, mais RAPL_ENERGY_GPU
l’est.
Déployer la formule SmartWatts
L’objectif d’une formule de calcul est de convertir les données brutes fournies par les capteurs RAPL en données énergétiques exploitables, c’est-à-dire des données de consommation exprimées en Watts. La formule de calcul va collecter les données issues des capteurs dans la base de données Mongo, et va stocker les données converties dans une autre base de données Mongo.
Le site de PowerAPI (INRIA 2020) propose d’utilise la formule simple RAPL pour faire des relevés généraux de la consommation à l’échelle d’un processeur. Cette formule est présentée comme une formule de démonstration pour illustrer les capacités de PowerAPI. En production, il est plutôt conseillé d’utiliser la formule SmartWatts qui permet une mesure beaucoup plus fine de la consommation. Le fonctionnement de la formule s’appuie sur une calibration en ligne qui permet un ajustement en temps réel du modèle de consommation à la machine sur laquelle elle est déployée. Le principe de fonctionnement est décrit en détails dans l’article de Fieni et al. (Fieni, Rouvoy, and Seinturier 2020).
Concrètement, la formule SmartWatts5 se présente sous la forme d’un programme Python que l’on peut installer et lancer, mais elle existe également sous la forme d’un conteneur Docker que l’on peut lancer comme suit :
$ docker run -td --net=host --name powerapi-formula powerapi/smartwatts-formula \
-s \
--input mongodb --model HWPCReport \
-u $MONGO_ADDR -d $INPUT_DB -c $INPUT_COL \
--output mongodb --name power --model PowerReport \
-u $MONGO_ADDR -d $OUTPUT_DB -c $OUTPUT_COL \
--output mongodb --name formula --model FormulaReport \
-u $MONGO_ADDR -d $OUTPUT_DB -c frep \
--formula smartwatts --cpu-ratio-base $BASE_CPU_RATIO \
--cpu-ratio-min $MIN_CPU_RATIO \
--cpu-ratio-max $MAX_CPU_RATIO \
--cpu-error-threshold 2.0 \
--dram-error-threshold 2.0 \
--disable-dram-formula
Ici, il faut remplacer les variables respectives INPUT_*
et OUTPUT_*
par les informations concernant la connexion à
la base de données d’entrée (données capteurs) et de sortie (écriture
des données de consommation) de la formule SmartWatts. Ainsi par
exemple :
MONGO_ADDR="mongodb://127.0.0.1:27021"
INPUT_DB="powerapi"
INPUT_COL="hwpcMeasures"
OUTPUT_DB="powerapi"
OUTPUT_COL="smartWattsValues"
Il y a également trois nouveaux paramètres à configurer, les paramètres relatifs à la fréquence du processeur :
$BASE_CPU_RATIO
est la fréquence nominale du processeur, divisée par 100 ;$MIN_CPU_RATIO
est la fréquence minimale du processeur, divisée par 100. Cette valeur peut être trouvée à l’aide de la commandelscpu
(ligneCPU min MHz
) ;$MAX_CPU_RATIO
est la fréquence maximale du processeur, divisée par 100. Cette valeur peut être trouvée à l’aide de la commandelscpu
(ligneCPU max MHz
).
Une fois que la formule SmartWatts est déployée, on peut vérifier la collecte des données en se connectant à la base Mongo concernée.
$ mongo mongodb://localhost:27021
MongoDB shell version v3.6.8
connecting to: mongodb://localhost:27021
[...]
test@machine:27021 >>> use powerapi
switched to db powerapi
powerapi@machine:27021 >>> db.smartWattsValues.find().pretty()
{
"_id" : ObjectId("600ef211804a0d545698ced4"),
"timestamp" : ISODate("2021-01-25T16:29:57.533Z"),
"sensor" : "hwpcSensor",
"target" : "rapl",
"metadata" : {
"scope" : "cpu",
"socket" : "0",
"formula" : "RAPL_ENERGY_PKG",
"ratio" : 1,
"predict" : 0
},
"power" : 6.37811279296875,
"socket" : "0"
}
{
"_id" : ObjectId("600ef212804a0d545698ced5"),
"timestamp" : ISODate("2021-01-25T16:29:58.534Z"),
"sensor" : "hwpcSensor",
"target" : "rapl",
"metadata" : {
"scope" : "cpu",
"socket" : "0",
"formula" : "RAPL_ENERGY_PKG",
"ratio" : 1,
"predict" : 0
},
"power" : 4.3121337890625,
"socket" : "0"
}
[...]
Ici, la valeur indiquée dans le champ power
(6.37811279296875
et 4.3121337890625
en
l’occurrence) correspond à la valeur de consommation estimée par la
formule, exprimée en watts.
Mesure de la consommation pour un groupe de processus
PowerAPI fournit également la possibilité de mesurer la consommation
d’un processus unique ou d’un groupe de processus. Cela passe par
l’utilisation des cgroups
6,
fonctionnalité incluse dans le noyau Linux depuis la version 2.6.24, et
dont l’objectif est de mettre en place et de gérer des groupes de
processus, dans le but de mesurer, limiter, ou contrôler l’accès à
certaines ressources.
Techniquement, le capteur HWPC est capable de monitorer la
consommation de tous les groupes de processus rattachés au contrôleur
perf_event
, qui est l’un des contrôleurs présents par
défaut7.
La première étape pour cela est de créer un groupe de
processus dont le contrôleur est perf_event
. Cela peut se
faire grâce à la commande (lancée en tant que superutilisateur) :
# cgcreate -g perf_event:$GROUP
Ici, $GROUP
est le nom du groupe de processus qui
servira à identifier la consommation. Cette commande doit être exécutée
en tant que superutilisateur.
Ensuite, on peut rattacher tous les processus que l’on souhaite surveiller au groupe nouvellement créé.
# cgclassify -g perf_event:$GROUP $(pidof $PROGRAM_NAME)
Ici, on rattache tous les processus qui se rattachent au nom de
programme $PROGRAM_NAME
au groupe $GROUP
.
Une fois cette classification accomplie, il faut, tout comme dans la
sec. 2.0.2, déployer le capteur HWPC, puis la formule SmartWatts (voir
sec. 2.0.3) pour transformer les données brutes en mesures estimées de
la consommation. Dans les données exportées par cette formule, on trouve
désormais toutes les données concernant les groupes de processus que
nous avons crées. Ainsi, par exemple, si nous créons le groupe
firefox
pour monitorer les données de consommation du
navigateur, nous retrouvons dans notre collection Mongo un ensemble
d’objets dont la cible (target
) correspond à la valeur
/firefox
:
powerapi@machine:27021 >>> db.smartWattsValues.find({"target": "/firefox"}).pretty()
{
"_id" : ObjectId("6012bdb89279bf32da7f4b00"),
"timestamp" : ISODate("2021-01-28T13:35:46.670Z"),
"sensor" : "hwpcSensor",
"target" : "/firefox",
"metadata" : {
"scope" : "cpu",
"socket" : "0",
"formula" : "4f87dd0a3afd331acc89541f4cd46e856f119c59",
"ratio" : 0.20662675213078738,
"predict" : 1.835740527645692
},
"power" : 1.835740527645692,
"socket" : "0"
}
[...]
Traitement et visualisation des données
Il existe plusieurs possibilités pour traiter les données de
consommation issues de PowerAPI. Une possibilité est d’utiliser un outil
de visualisation tel que Grafana8, qui nécessitera un
export des données dans une base de données temporelle InfluxDB9. Une autre possibilité est
d’exporter les données dans un format utilisable facilement (CSV par
exemple) afin de pouvoir l’exploiter dans un logiciel d’analyse. Là
encore, il existe plusieurs possibilités : soit on peut directement
exporter les données issues de la formule RAPL dans ce format, en
paramétrant correctement la sortie du script de lancement ; soit on peut
exporter ces données depuis la collection Mongo adéquate grâce à
mongoexport
. La commande suivante exporte toute les données
de la collection smartWattsValues
en se limitant aux champs
target
, timestamp
et power
, qui
sont suffisants pour une analyse et une visualisation basiques.
$ mongoexport --uri "mongodb://localhost:27021/powerapi" \
--collection=smartWattsValues --type=csv \
--fields=target,timestamp,power --out=$OUTPUT_FILE
Il est ensuite possible d’exploiter ces données en utilisant les
logiciels ou les bibliothèques adéquates. Voici un exemple de script
Python permettant simplement de tracer le graphe de consommation en
fonction du temps, en utilisant les bibliothèques pandas
(pour la lecture du fichier CSV) et matplotlib
(pour le
tracé du graphe).
#!/usr/bin/env python3
import sys
import pandas as pd
import matplotlib.pyplot as plt
import dateutil.parser
import matplotlib.dates as mdates
= 50
MOVING_AVG_WINDOW = 20
NB_X_TICKS = {'rapl', 'powerapi-formula', 'powerapi-sensor'}
EXCLUDE_TARGETS
if len(sys.argv) < 2:
f"Usage: {sys.argv[0]} csv_file\n")
sys.stderr.write(1)
sys.exit(
# We read the CSV file
= pd.read_csv(sys.argv[1])
df
# We create the figure and configure the plot
= plt.figure()
fig = fig.add_subplot(111)
ax
# We group the tuples by target
= df.groupby('target')
gb
# We plot the graph for each group
# Using a rolling average to smooth the graph and better show trends
for name, group in gb:
if name not in EXCLUDE_TARGETS:
for d in group['timestamp']],
ax.plot([dateutil.parser.isoparse(d) 'power'].rolling(MOVING_AVG_WINDOW).mean(), label=name)
group[
# Now we configure the plot
=(0.9, 0.9, 0.9))
ax.grid(color= [dateutil.parser.isoparse(d) for d in df['timestamp']]
x # We show only NB_X_TICKS ticks on the x axis
ax.set_xticks(for i in range(len(x) // NB_X_TICKS // 2, len(x), len(x) // NB_X_TICKS)])
[x[i] # We format the date
'%H:%M:%S'))
ax.xaxis.set_major_formatter(mdates.DateFormatter(
fig.autofmt_xdate()# We set the axes labels
"Temps")
ax.set_xlabel("Consommation (watts)")
ax.set_ylabel(# We position the legend
="upper left", frameon=True)
plt.legend(loc# We show the graph
plt.show()
Ce script produit le graphe apparaissant dans la figure 2. Nous nous
limitons à l’estimation de la consommation globale, et à deux groupes de
processus créés au préalable avec cgcreate
comme indiqué
ci-dessus. La figure montre une moyenne glissante des données avec une
fenêtre de 50 échantillons. Cela permet de mieux visualiser les
tendances en lissant les variations locales, mais annule les valeurs
extrêmes (ainsi, par exemple, dans les données d’entrée visualisées sur
la figure, la valeur de consommation maximale est d’environ 58 W, ce qui
ne transparaît pas du tout sur le graphe).
PyJoules
Le middleware PowerAPI présenté dans la sec. 2 est dédié essentiellement à la mesure de consommation énergétique à l’échelle d’un cluster. Le cas d’usage typique de PowerAPI est le suivi de consommation sur du long terme. Il n’a a priori pas été pensé pour faire de la mesure comparative de performances d’algorithmes ou de programmes.
Si l’on souhaite effectuer des mesures comparatives de performance d’algorithmes, on peut utiliser la bibliothèque PyJoules10. Il s’agit d’une bibliothèque Python qui s’appuie sur les compteurs RAPL (voir sec. 1) pour mesurer l’empreinte énergétique d’une machine lors de l’exécution de portions de code Python. PyJoules est également capable de mesurer la consommation énergétique de certains processeurs graphiques NVidia, en s’appuyant sur la bibliothèque Nvidia Management Library.
Il faut noter que PyJoules mesure la consommation d’énergie globale au niveau de tous les processus, et non au niveau du simple programme que l’on cherche à mesurer. C’est pourquoi il est important d’éliminer tous les programmes extérieurs lorsque l’on effectue le relevé, afin de minimiser les consommations additionnelles induites qui auraient pour effet de biaiser les mesures.
Utilisation de PyJoules
La manière la plus simple d’utiliser PyJoules est d’équiper les
fonctions dont on souhaite mesurer la consommation à l’aide du
décorateur @measure_energy
from pyJoules.energy_meter import measure_energy
@measure_energy
def python_sort(shuffled):
shuffled.sort()
Lorsque l’on le lance, le programme affiche une ligne de mesure pour chaque passage dans la fonction équipée du décorateur en question :
begin timestamp : 1612345928.8571808; tag : python_sort; duration : 0.0008618831634521484; package_0 : 7751.0; dram_0 : 610.0; core_0 : 3418.0; uncore_0 : 0.0
Ici, la durée est exprimée en secondes, et la consommation des différents domaines en microjoules (μJ).
Le décorateur peut également être paramétré par :
- l’ensemble des différents domaines, parmi
pyJoules.device.rapl_device.RaplPackageDomain
,pyJoules.device.rapl_device.RaplDramDomain
,pyJoules.device.rapl_device.RaplUnCoreDomain
,pyJoules.device.rapl_device.RaplCoreDomain
etpyJoules.device.nvidia_device.NvidiaGPUDomain
- un gestionnaire (handler), qui permet de spécifier vers
quelle sortie les données seront envoyées. Les gestionnaires disponibles
sont :
- la sortie standard (par défaut) ;
- le gestionnaire CSV (
pyJoules.handler.csv_handler.CSVHandler
) qui exporte les mesures sous la forme d’un fichier CSV ; - le gestionnaire Pandas (
pyJoules.handler.pandas_handler.PandasHandler
) qui exporte les mesures sous la forme d’une variable de typeDataFrame
de la bibliothèquepandas
; - le gestionnaire MongoDB (
pyJoules.handler.mongo_handler.MongoHandler
) qui exporte les données vers une collection Mongo.
Outre l’utilisation d’un décorateur, il est également possible d’utiliser un gestionnaire de contexte (ContextManager) qui rend l’instrumentation du code plus souple :
from pyJoules.energy_meter import measure_energy
@measure_energy(handler=pandas_handler)
def numpy_sort(tab):
with EnergyContext(handler=pandas_handler, start_tag='Numpy create') as ctx:
= np.array(tab)
array ='Numpy sort')
ctx.record(tag np.sort(array)
Ainsi, par exemple, le code ci-dessus instrumente une partie du code
de tri, en effectuant deux relevés : le premier correspond au code situé
entre la création de l’objet EnergyContext
et l’instruction ctx.record()
, et le second entre cette
instruction et la fin du gestionnaire de contexte.
Cas d’utilisation complet
Nous allons dans cette section présenter un cas d’utilisation complet de mesure de consommation avec pyJoules. Nous nous fixons comme objectif de comparer différents algorithmes de tri à l’aune de leur consommation énergétique et d’afficher le résultat sous forme de graphique avec Matplotlib.
Commençons par le préambule du programme, et quelques bibliothèques dont nous allons nous servir :
#!/usr/bin/python3
import numpy as np
import random
import sys
from pyJoules.energy_meter import measure_energy, EnergyContext
from pyJoules.handler.pandas_handler import PandasHandler
import matplotlib.pyplot as plt
import matplotlib.cm as cm
Pour le traitement des données, nous allons utiliser la bibliothèque Pandas, et le gestionnaire pyJoules qui lui est associé :
= PandasHandler() pandas_handler
Instrumentation du code de tri
La deuxième partie du programme est dédiée aux algorithmes de tri à proprement parler, instrumentés pour les relevés de consommation. Le tri par insertion :
@measure_energy(handler=pandas_handler)
def insertion_sort(tab):
for i in range(1, len(tab)):
= i
j while tab[j - 1] > tab[j] and j > 0:
- 1], tab[j] = tab[j], tab[j - 1]
tab[j -= 1 j
Le tri rapide (quicksort), avec un choix de pivot déterministe défini comme le premier élément du tableau à trier :
def qsort(tab):
def split(tab, first_idx, last_idx, pivot_idx):
= tab[last_idx], tab[pivot_idx]
tab[pivot_idx], tab[last_idx] = first_idx
j for i in range(first_idx, last_idx):
if tab[i] <= tab[last_idx]:
= tab[j], tab[i]
tab[i], tab[j] += 1
j = tab[j], tab[last_idx]
tab[last_idx], tab[j] return j
def quick_sort(tab, first_idx, last_idx):
if first_idx < last_idx:
= first_idx
pivot_idx = split(tab, first_idx, last_idx, pivot_idx)
pivot_idx - 1)
quick_sort(tab, first_idx, pivot_idx + 1, last_idx)
quick_sort(tab, pivot_idx
0, len(tab) - 1) quick_sort(tab,
La fonction de tri par défaut de Python fondée sur l’algorithme TimSort :
@measure_energy(handler=pandas_handler)
def python_sort(tab):
tab.sort()
La fonction de tri par défaut de Numpy, fondée sur l’algorithme QuickSort. Ici, nous instrumentons à la fois la portion de code dédiée à l’exécution de l’algorithme lui-même, et la portion de code dédiée à la création du tableau Numpy à partir d’une liste Python :
def numpy_sort(tab):
with EnergyContext(handler=pandas_handler, start_tag='Numpy create') as ctx:
= np.array(tab)
array ='Numpy sort')
ctx.record(tag np.sort(array)
Traitement des données pour la visualisation
La troisième partie du programme est dédiée au traitement des données
exportées par pyJoules sous la forme d’un objet DataFrame
de la bibliothèque Pandas. Nous commençons par utiliser cette
bibliothèque pour calculer un regroupement des données par compteur
(tag
), et calculer la moyenne et l’écart-type pour chaque
groupe, en effectuant une agrégation.
Le code restant est dédié au tracé du diagramme à barres, équipé des marges d’erreur pour chaque classe du diagramme.
def process_data(df):
# We group the tuples by algorithm and compute the means and standard deviations
= df.groupby('tag').agg([np.mean, np.min, np.max])
aggregate
# We get the list of domains (package, dram,...) and the list of algorithms tested
= ['core_0', 'uncore_0', 'package_0', 'dram_0']
domains = aggregate.head().index.values
algos = len(algos)
N = len(domains)
M
= [[aggregate[domain]['mean'][algo] / 1000000 for algo in algos]
means for domain in domains]
= [[aggregate[domain]['amin'][algo] / 1000000 for algo in algos]
mins for domain in domains]
= [[aggregate[domain]['amax'][algo] / 1000000 for algo in algos]
maxs for domain in domains]
= np.arange(N)
ind
# We configure the bar chart
= 0.15 # bar width
bar_width = 0.05
bar_sep = (M - 1) * (bar_width + bar_sep)
total_width = total_width / (M - 1)
unit_offset
def offset(i):
return (- total_width / 2 + i * unit_offset + unit_offset / 2)
= cm.get_cmap('Paired') # color sheme
colors = plt.figure()
fig = fig.add_subplot(111)
ax
# We draw the bar charts with error bars
= [None] * M
bars for i in range(M - 1):
= ax.bar(ind + offset(i), means[i], bar_width, color=colors(2*i))
bars[i] + offset(i), means[i], yerr=[mins[i], maxs[i]],
ax.errorbar(ind ='o', markersize=4, capsize=4,
fmt=colors(2*i + 1))
color- 1] = ax.bar(ind + offset(M - 2), means[M - 1], bar_width, bottom=means[M - 2],
bars[M =colors(2*M - 2))
color+ offset(M - 2) + 0.05, np.array(means[M - 1]) + means[M - 2],
ax.errorbar(ind =[mins[M - 1], maxs[M - 1]],
yerr='o', markersize=4, capsize=4,
fmt=colors(2*M - 1))
color
# We configure the axes and legend
True)
ax.set_axisbelow(=(0.8, 0.8, 0.8), linestyle='dashed')
ax.yaxis.grid(color'Consommation moyenne (joules, échelle logarithmique)')
plt.ylabel('log')
plt.yscale('Algorithme')
plt.xlabel(
plt.xticks(ind, algos)0] for bar in bars], domains)
ax.legend([bar[
plt.show()
Script de lancement des tests
Enfin, la dernière partie du programme est dédiée au lancement des tests à proprement parler. Nous pouvons paramétrer à la ligne de commande la taille des listes à trier, ainsi que le nombre d’exécutions de chaque algorithme de tri. La liste à trier est une permutation aléatoire de l’intervalle d’entiers \(\{ 0, ..., N-1 \}\), où \(N\) est initialisé grâce à la ligne de commande.
def test_sort(tab, sort_func, nb_tries):
for i in range(nb_tries):
= tab.copy()
cpy
sort_func(cpy)
if __name__ == "__main__":
if len(sys.argv) < 3:
f"Usage: {sys.argv[0]} size_list nb_tries\n")
sys.stderr.write(1)
sys.exit(
= map(int, sys.argv[1:3])
size_list, nb_tries = list(range(size_list))
shuffled 123456789)
random.seed(= [insertion_sort, qsort, python_sort, numpy_sort]
tested_algorithms for algo in tested_algorithms:
random.shuffle(shuffled)
test_sort(shuffled, algo, nb_tries)
= pandas_handler.get_dataframe()
df process_data(df)
La figure 3 montre le résultat de l’exécution de ce script sur des
listes de 10000 éléments, chaque algorithme étant exécuté 10 fois. Les
domaines core_0
et uncore_0
étant des
sous-domaines du domaine package_0
, l’invariant
core_0 + uncore_0 <= package_0
est censé être vérifié
pour chaque algorithme. En pratique, les incertitudes de mesures font
qu’il ne l’est pas toujours. Dans la figure, la barre empilée
package_0 + dram_0
montre la consommation totale des
domaines mesurés par pyJoules (voir figure 1 pour un rappel sur le
périmètre englobé par chaque domaine). En pratique, dans ce cas de
figure, on peut voir que la consommation du domaine dram_0
est faible par rapport aux autres consommations.
D’après la figure 3, on peut constater qu’il y a presque 4 ordres de grandeur entre l’algorithme le moins efficace énergétiquement (tri par insertion codé manuellement) et l’algorithme le plus efficace (quicksort Numpy).
Enfin, afin de donner un ordre de grandeur, la consommation énergétique engendrée par le tri par insertion tourne en moyenne autour de 55 joules, ce qui revient approximativement à 0,015 wattheures. À titre de comparaison, la consommation énergétique engendrée par le tri Numpy de 1000000 de valeurs (soit 100 fois plus) tourne en moyenne autour de 0,7 J (0,00019 Wh) pour la création du tableau Numpy et 1,3 J (0,000361 Wh) pour le tri en lui-même.
Bien entendu, toutes ces valeurs ne sont qu’approximatives, étant donné que la mesure a été faite dans un environnement où d’autres processus s’exécutaient en parallèle (en particulier l’environnement graphique de bureau).
Nous pouvons par curiosité refaire les mêmes analyses sur des listes déjà triées. La figure 4 montre que les résultats sont très différents. La forte empreinte énergétique de l’algorithme quicksort codé en Python s’explique très probablement par le fait que le choix déterministe du pivot, fixé au premier élément du tableau, impose dans ce cas une profondeur de récursion linéaire (il faut d’ailleurs augmenter la taille de la pile Python car dans le cas de 10000 éléments, elle est par défaut insuffisante).
CodeCarbon
CodeCarbon (Schmidt et al. 2021) est une bibliothèque Python sous licence MIT développée par un collectif de data scientists. L’objectif de cette bibliothèque est de fournir un outil permettant d’estimer très simplement l’impact carbone de l’exécution d’une portion de code. Même si la page Web de CodeCarbon11 met en avant l’utilisation de cette bibliothèque pour l’évaluation d’impact d’algorithmes d’apprentissage machine, elle peut être utilisée avec n’importe quel code Python, de la même manière que PyJoules (cf sec. 3).
Afin de calculer l’impact carbone d’un calcul, CodeCarbon a besoin de deux éléments :
- un système de mesure de consommation d’énergie sur le système hôte ;
- une formule de conversion de l’énergie consommée (en J ou Wh) en équivalent CO2.
Mesure de la consommation d’énergie
Afin de pouvoir évaluer l’énergie consommée par le programme,
CodeCarbon va s’appuyer sur Intel Power Gadget sous Windows et
Mac, et sur le système RAPL (voir sec. 1) sous Linux, ce qui nécessite
donc un accès aux fichiers de /sys/class/powercap/intel-rapl
, tout comme pour
PyJoules − CodeCarbon accède notamment aux fichiers energy_uj
qui contiennent la valeur de
consommation cumulée en µJoules. Si jamais CodeCarbon ne parvient pas à
accéder à ces sondes logicielles, la bibliothèque utilisera une
stratégie de repli pour évaluer la consommation, à savoir s’appuyer sur
le thermal design power (TDP) du CPU hôte. CodeCarbon s’appuie
pour cela sur une liste de données de plusieurs centaines de CPU AMD et
Intel. Si CodeCarbon ne parvient pas à trouver le CPU hôte dans cette
liste, il suppose une valeur par défaut à 85W. Dans tous les cas,
CodeCarbon suppose que le CPU fonction à 50% de son TDP.
Évaluation de l’impact carbone
Afin de pouvoir calculer l’impact carbone d’un calcul, CodeCarbon transforme la quantité d’énergie électrique consommée en un équivalent CO2. Pour ce faire, la bibliothèque estime l’intensité carbone du système de production électrique du pays dans lequel a lieu le calcul en s’appuyant sur une base de données recensant le mix électrique pour la plupart des pays du monde. Ce mix électrique est réparti en quatre sources :
- charbon ;
- pétrole ;
- gaz naturel ;
- autre source à faible impact carbone (hydroélectricité, solaire, éolien, nucléaire…)
Pour chacune de ces sources, un coefficient d’intensité carbone est utilisé. L’intensité carbone est donc simplement calculée comme une somme de ces coefficients d’intensité pondérée par les pourcentages de chaque source dans le mix énergétique du pays. À noter toutefois que ces coefficients d’intensité ne prennent en compte que les émissions durant la phase de génération, et donc sont légèrement sous-estimées. En particulier, pour les sources de type 4 (autre), le coefficient d’intensité est considéré comme étant égal à 0.
Utilisation de CodeCarbon
L’utilisation de CodeCarbon est très similaire à celle de PyJoules. On peut par exemple instrumenter une fonction Python grâce à l’utilisation d’un décorateur :
from codecarbon import track_emissions
@track_emissions
def python_sort(tab):
tab.sort()
Ce décorateur par défaut nécessite un accès Internet afin de
déterminer la localisation de la machine sur laquelle est lancée le
calcul. Si l’on ne souhaite pas utiliser cette fonctionnalité de
détection (ou que le calcul est lancé hors-ligne), il faut utiliser le
paramètre offline=True
et préciser en même temps le pays
dans lequel le calcul se déroule :
from codecarbon import track_emissions
@track_emissions(offline=True, country_iso_code="FRA")
def python_sort(tab):
tab.sort()
Une autre manière de procéder, sans passer par le décorateur, est
d’instancier directement un objet de type EmissionsTracker
ou OfflineEmissionsTracker
et de le lancer
manuellement :
from codecarbon import EmissionsTracker
def python_sort(tab):
= EmissionsTracker()
tracker
tracker.start()
tab.sort() tracker.stop()
La liste de tous les paramètres pouvant être passés à ces objets est détaillée sur la page de documentation.
Exploitation des résultats
Une fois le calcul terminé, CodeCarbon stocke les résultats dans un
fichier CSV (par défaut emissions.csv
) qui
contient une ligne de données par projet. Un projet peut être défini
simplement en fixant la valeur du paramètre project_name
à l’instanciation du
décorateur ou de l’objet EmissionsTracker
. Fixer ce paramètre
permet de recenser plusieurs valeurs différentes pour différents
endroits du code.
Il faut noter que CodeCarbon ne stocke qu’une seule valeur par
projet, bien qu’il soit possible de régler la fréquence avec laquelle la
bibliothèque effectue ses mesures de consommation (avec le paramètre
measure_power_secs
). Étant donné
que CodeCarbon lit les valeurs des sondes RAPL en quantité d’énergie (en
microjoules), et non en puissance, et que CodeCarbon ne fournit pas de
relevés instantanés de consommation énergétique pour observer une
évolution dans le temps, l’utilité de ce paramètre measure_power_secs
ne semble pas
extrêmement claire.
Voici un exemple de fichier de sortie produit par CodeCarbon sur l’exemple du tri déjà utilisé pour PyJoules (voir sec. 3.0.2) :
To Do
On peut par exemple trouver quelques explications par sur le blog de Chi (https://blog.chih.me/read-cpu-power-with-RAPL.html), ou encore sur la page de pyJoules (https://pyjoules.readthedocs.io/en/latest/devices/intel_cpu.html#domains).↩︎
Voir la page de Vince Weaver pour plus d’informations sur la question : http://web.eece.maine.edu/~vweaver/projects/rapl/index.html↩︎
Pour vérifier qu’il est bien présent,
cat /proc/cgroups | grep perf_event
↩︎