7 · Build et environnement¶
Le chapitre le moins glorieux et l'un des plus rentables. Construire une pile logicielle HPC correcte et reproductible est une compétence qui n'est enseignée nulle part dans la maquette, et qui consomme la moitié du temps d'une équipe de compétition mal préparée.
Difficulté : ★★ · ⏱ 30 h
Le problème¶
Un code de calcul dépend typiquement de : un compilateur C, un compilateur C++, un compilateur Fortran, une implémentation MPI, une bibliothèque BLAS et LAPACK, une bibliothèque FFT, HDF5, NetCDF, quelques bibliothèques de solveurs, et une dizaine de dépendances transitives. Chacune doit être compilée avec des options compatibles, et liée aux mêmes versions.
Faire cela à la main prend des jours et n'est pas reproductible. Trois familles d'outils existent pour cela, et il faut connaître les trois.
Les gestionnaires d'environnement : module et lmod¶
Sur tout cluster HPC, les logiciels sont installés en plusieurs versions et
exposés par des modules d'environnement. Charger un module modifie PATH,
LD_LIBRARY_PATH, CPATH et les variables associées.
module avail # ce qui est disponible
module load gcc/13.2.0
module load openmpi/5.0.2 # souvent dépendant du compilateur chargé
module list # ce qui est chargé
module show openmpi/5.0.2 # ce que ça modifie exactement
module purge # tout décharger, avant une mesure propre
lmod, l'implémentation moderne en Lua, ajoute la notion de hiérarchie : le
MPI disponible dépend du compilateur chargé, ce qui empêche les combinaisons
incohérentes.
L'erreur qui coûte le plus d'heures sur un cluster
Compiler avec un compilateur et exécuter avec un autre, ou lier une
bibliothèque MPI et lancer avec un mpirun d'une autre implémentation. Les
symptômes sont absurdes : plantages à l'initialisation, un seul rang qui voit
size == 1, corruptions silencieuses.
La règle : module purge puis charger explicitement la même liste de
modules à la compilation et à l'exécution. Mettez cette liste dans votre
script Slurm, et affichez module list dans la sortie.
Spack¶
Le gestionnaire de paquets conçu pour le HPC. Il compile depuis les sources en gérant les variantes, les compilateurs et les dépendances.
# Installer une bibliothèque avec des contraintes explicites
spack install [email protected] +mpi %gcc@13 ^openmpi@5
# Voir ce qui serait construit avant de lancer
spack spec -I hdf5 +mpi %gcc@13
# Charger dans l'environnement
spack load hdf5
# Un environnement déclaratif, reproductible, versionnable
spack env create mon_projet
spack env activate mon_projet
spack add hdf5+mpi openblas fftw
spack concretize
spack install
La syntaxe à connaître : paquet@version pour la version, +variante et
~variante pour activer ou désactiver, %compilateur@version pour le
compilateur, ^dépendance pour contraindre une dépendance, cflags="..." pour
les options.
Le fichier spack.yaml d'un environnement est le livrable : il décrit
exactement la pile, et un camarade peut la reconstruire. C'est l'équivalent d'un
requirements.txt pour le HPC, en plus puissant et en plus lent.
Les trois pièges de Spack
- La concrétisation est lente et parfois surprenante. Toujours faire
spack specavantspack installpour voir ce qui va être construit. Spack peut décider de recompiler un compilateur entier. - Il préfère tout recompiler. Utilisez les paquets externes
(
packages.yamlavecexternals:) pour réutiliser le MPI et les compilateurs déjà installés sur le cluster, qui sont réglés pour le réseau de la machine. Recompiler le MPI d'un cluster est presque toujours une erreur : vous perdrez le support du réseau rapide. - L'espace disque. Une pile complète représente plusieurs dizaines de gigaoctets. Vérifiez vos quotas avant.
EasyBuild¶
L'alternative européenne à Spack, très utilisée dans les centres de calcul du continent. Philosophie différente : des recettes (easyconfigs) plus prescriptives et testées, moins de flexibilité, plus de reproductibilité. Il produit directement des modules d'environnement.
Lequel choisir ? Spack pour l'expérimentation et la flexibilité ; EasyBuild pour une installation de production que d'autres utiliseront. En pratique, on utilise celui que le centre utilise.
CMake¶
Le système de construction dominant en C++ scientifique. Le minimum à maîtriser :
cmake_minimum_required(VERSION 3.20)
project(mon_solveur LANGUAGES CXX Fortran)
find_package(MPI REQUIRED)
find_package(OpenMP REQUIRED)
find_package(BLAS REQUIRED)
find_package(HDF5 REQUIRED COMPONENTS C HL)
add_library(noyaux src/noyaux.cpp)
target_link_libraries(noyaux PUBLIC OpenMP::OpenMP_CXX ${BLAS_LIBRARIES})
target_compile_features(noyaux PUBLIC cxx_std_20)
add_executable(solveur src/main.cpp)
target_link_libraries(solveur PRIVATE noyaux MPI::MPI_CXX HDF5::HDF5)
enable_testing()
add_test(NAME validation COMMAND solveur --test)
Les quatre choses à savoir faire :
- Trouver une dépendance (
find_package) et lier proprement par cibles importées (MPI::MPI_CXX), pas par variables brutes. - Gérer les types de construction :
-DCMAKE_BUILD_TYPE=ReleaseetRelWithDebInfo, qui est le bon choix pour profiler. - Passer des options de compilation :
-DCMAKE_CXX_FLAGS="-O3 -march=native". - Exporter la base de compilation (
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON), qui permet àclang-tidyet aux éditeurs de fonctionner.
Le réflexe de traçabilité
Faites écrire par CMake, dans un en-tête généré, le hash Git, la date, le compilateur et les options de compilation. Le programme les affiche au démarrage et les écrit dans chaque fichier de sortie.
execute_process(COMMAND git rev-parse --short HEAD
OUTPUT_VARIABLE GIT_HASH
OUTPUT_STRIP_TRAILING_WHITESPACE)
configure_file(version.h.in version.h)
Dix lignes, et toutes vos mesures deviennent reproductibles. Sans cela, dans six mois, vos résultats ne valent rien.
Les conteneurs en contexte HPC¶
Apptainer (anciennement Singularity), pas Docker.
Pourquoi : Docker exige un démon privilégié, ce qui est inacceptable sur un cluster multi-utilisateurs. Apptainer s'exécute sans privilège, avec l'identité de l'utilisateur, et il monte par défaut le répertoire personnel et le répertoire courant, ce qui rend l'usage naturel.
# Construire depuis une définition
apptainer build mon_code.sif mon_code.def
# Ou convertir une image Docker existante
apptainer build mon_code.sif docker://ubuntu:24.04
# Exécuter, y compris sous MPI
srun apptainer exec mon_code.sif ./mon_binaire
Le problème MPI des conteneurs
Un conteneur contenant sa propre installation MPI doit être compatible en ABI avec le MPI de l'hôte pour utiliser le réseau rapide. Deux approches :
- Le modèle hybride : le conteneur contient un MPI compatible, et on injecte celui de l'hôte au lancement (par montage de bibliothèques). C'est ce que font les centres.
- Le modèle bind : le conteneur ne contient pas MPI du tout, et tout vient de l'hôte.
Sans précaution, un code MPI conteneurisé fonctionne sur un nœud et s'effondre sur plusieurs, ou retombe sur TCP. C'est un piège classique, à tester avant d'en avoir besoin.
Le Fortran, en trois heures¶
Aucun module de la maquette n'enseigne le Fortran, et la moitié des codes de calcul en production en est écrite : météorologie, climat, mécanique des fluides, chimie quantique, physique des plasmas. WRF, l'une des applications annoncées pour la compétition SC26, est en Fortran.
L'objectif n'est pas d'écrire du Fortran mais de savoir le lire, le compiler et le profiler.
Le Fortran minimal utile
Les cinq différences qui comptent :
- Les indices commencent à 1 par défaut (et peuvent être déclarés avec
n'importe quelle borne :
real :: a(0:n-1)). - Les tableaux multidimensionnels sont stockés en colonnes
(column-major), à l'inverse du C. Donc dans
a(i,j), c'estiqui doit varier dans la boucle interne. C'est la première source d'erreurs de portage et de régressions de localité. - Pas d'aliasing de pointeurs : le langage garantit que les arguments
intent(in)etintent(out)ne se recouvrent pas. C'est pourquoi les codes Fortran se vectorisent souvent mieux que les codes C équivalents. - La syntaxe de tableaux :
a(:,j),a(i,:),a = b + c,matmul,dot_product,sum(a, dim=1),where. Concise et généralement bien optimisée. - Les modules :
module,use,contains,interface,allocatable,intent(in|out|inout),pure,elemental.
Compiler : gfortran -O3 -march=native -fopenmp. Les autres compilateurs
sont ifx (Intel), nvfortran (NVIDIA, avec support OpenACC et OpenMP
target), flang (LLVM), armflang.
Interfacer avec du C : iso_c_binding, bind(C). Utile quand on veut
écrire un noyau optimisé en C ou en CUDA et l'appeler depuis un code Fortran
existant — situation très fréquente.
Ressource : fortran-lang.org, qui est moderne, bien écrit et gratuit,
avec un guide Fortran Best Practices.
Les bibliothèques mathématiques : lesquelles et comment¶
| Besoin | Options libres | Options vendeur |
|---|---|---|
| BLAS, LAPACK | OpenBLAS, BLIS, ATLAS | Intel MKL, AMD AOCL, NVIDIA cuBLAS, Arm Performance Libraries |
| FFT | FFTW | MKL, cuFFT |
| Solveurs creux directs | SuperLU, MUMPS (français), UMFPACK | MKL PARDISO |
| Solveurs creux itératifs | PETSc, Trilinos, hypre | — |
| Valeurs propres | ARPACK, SLEPc, ELPA | MKL |
| Algèbre linéaire distribuée | ScaLAPACK, SLATE, Elemental | — |
Le piège du nombre de threads
Une bibliothèque BLAS multithread crée ses propres threads. Si votre code est déjà parallélisé en OpenMP et appelle du BLAS dans une région parallèle, vous obtenez un sur-abonnement massif : \(N\) threads OpenMP fois \(N\) threads BLAS.
Le remède : OMP_NUM_THREADS pour votre code et
OPENBLAS_NUM_THREADS=1 (ou MKL_NUM_THREADS=1) pour la bibliothèque si les
appels sont dans une région parallèle ; l'inverse si le BLAS est appelé
séquentiellement sur de grosses matrices.
Ce réglage est une cause fréquente et invisible de mauvaise performance : le code ne plante pas, il rampe.
Exercices¶
E1 · ★ ⏱ 2 h — Le squelette CMake réutilisable. Construire un projet modèle :
bibliothèque, exécutable, tests, find_package pour MPI, OpenMP, BLAS et HDF5,
en-tête de version généré depuis Git, et intégration continue. Le conserver comme
gabarit pour tout le reste de votre scolarité.
E2 · ★★ ⏱ 4 h — Un environnement Spack. Construire un spack.yaml qui
installe une pile complète (compilateur, MPI, OpenBLAS, FFTW, HDF5 parallèle), la
construire, et compiler un code contre elle. Puis donner le fichier à un camarade
et vérifier qu'il obtient la même pile. Chronométrer la construction : c'est long,
et le savoir change la planification d'une compétition.
E3 · ★★ ⏱ 3 h — Les paquets externes. Reprendre E2 en déclarant le MPI et le
compilateur du cluster comme paquets externes dans packages.yaml, de sorte que
Spack ne les recompile pas. Comparer le temps de construction et vérifier, avec
les OSU Micro-Benchmarks, que le MPI utilisé est bien celui du cluster et qu'il
voit le réseau rapide.
E4 · ★★★ ⏱ 4 h — Un conteneur MPI qui fonctionne sur plusieurs nœuds. Construire une image Apptainer contenant un code MPI et la faire tourner sur au moins deux nœuds sous Slurm, avec le réseau rapide. Mesurer la latence avec les OSU Micro-Benchmarks depuis l'intérieur du conteneur et comparer à la valeur obtenue en dehors. Si l'écart est important, le conteneur n'utilise pas le réseau rapide : diagnostiquer.
E5 · ★★ ⏱ 3 h — Le Fortran en pratique. Prendre un noyau de calcul écrit en C,
le traduire en Fortran moderne, et comparer les performances avec les deux
compilateurs correspondants. Puis inverser : écrire un noyau en C et l'appeler
depuis Fortran avec iso_c_binding. Enfin, prendre un code Fortran réel sur un
dépôt public, le compiler, et le profiler.
E6 · ★★ ⏱ 2 h — Le sur-abonnement BLAS. Mesurer un code OpenMP appelant du
DGEMM dans une région parallèle, avec les quatre combinaisons de
OMP_NUM_THREADS et OPENBLAS_NUM_THREADS égales à 1 ou à \(N\). Constater le
facteur perdu dans la mauvaise configuration.
E7 · ★★★ ⏱ 5 h — Le comparatif de compilateurs et de BLAS. Pour un même code, mesurer les combinaisons de deux ou trois compilateurs (GCC, Clang, et un compilateur vendeur si accessible) et deux ou trois BLAS (OpenBLAS, BLIS, MKL). Produire une matrice de résultats. Les écarts sont souvent supérieurs à ce que rapportent des heures d'optimisation manuelle, ce qui est une leçon utile sur la hiérarchie des priorités.
Erreurs fréquentes¶
Sept fautes de construction
- Compiler et exécuter avec des modules différents. Voir l'encadré ci-dessus. Le plus coûteux.
- Recompiler le MPI du cluster. Vous perdez le réseau rapide et vous ne le verrez pas tout de suite.
- Ne pas enregistrer les options de compilation avec les résultats. Une mesure sans sa ligne de compilation est une donnée morte.
- Construire en
Debuget mesurer.CMAKE_BUILD_TYPE=Debugimplique-O0. Pour profiler, utilisezRelWithDebInfo, qui combine-O2et-g. - Le sur-abonnement BLAS. Invisible et coûteux.
- Utiliser Docker sur un cluster. Il n'y sera pas disponible, et si vous le convertissez en Apptainer au dernier moment, le support MPI sera cassé. Testez en amont.
- Construire la pile logicielle la veille de la mesure. Une construction Spack complète prend plusieurs heures. En compétition, la pile doit être prête et testée avant d'arriver.
Ressources¶
Priorité 1 :
- La documentation de Spack (
spack.readthedocs.io), et les Spack Tutorials, qui sont excellents et donnés régulièrement à la conférence SC. - La documentation de CMake, et Craig Scott, Professional CMake: A Practical Guide, qui est le seul bon livre sur le sujet.
- La documentation
lmod(lmod.readthedocs.io).
Priorité 2 :
- La documentation d'Apptainer (
apptainer.org/docs), section MPI. - EasyBuild (
docs.easybuild.io). fortran-lang.org, y compris Fortran Best Practices.- La documentation d'OpenBLAS, de BLIS et de MKL sur le réglage du nombre de threads.
Priorité 3 :
- Les Better Scientific Software guides (
bssw.io) sur la construction et la reproductibilité. - La documentation de PETSc pour un exemple de projet scientifique avec une gestion de dépendances exemplaire.
- Les guides utilisateurs des centres sur les modules disponibles et les piles recommandées : ils documentent les combinaisons testées, ce qui évite de chercher.
À retenir¶
Les six règles de l'environnement
module purgepuis charger explicitement, la même liste à la compilation et à l'exécution, consignée dans le script de soumission.- Ne jamais recompiler le MPI du cluster. Déclarez-le en paquet externe.
- Spack ou EasyBuild pour la pile, avec un fichier d'environnement versionné qui est le livrable.
- Le hash Git et les options de compilation dans chaque sortie. Dix lignes de CMake, et toutes vos mesures deviennent reproductibles.
- Apptainer, pas Docker, et tester le support MPI multi-nœuds à l'avance.
- Surveiller le sur-abonnement BLAS, qui est invisible et coûte un facteur.
Chapitre suivant : Outils de mesure.