Aller au contenu

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

  1. La concrétisation est lente et parfois surprenante. Toujours faire spack spec avant spack install pour voir ce qui va être construit. Spack peut décider de recompiler un compilateur entier.
  2. Il préfère tout recompiler. Utilisez les paquets externes (packages.yaml avec externals:) 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.
  3. 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 :

  1. Trouver une dépendance (find_package) et lier proprement par cibles importées (MPI::MPI_CXX), pas par variables brutes.
  2. Gérer les types de construction : -DCMAKE_BUILD_TYPE=Release et RelWithDebInfo, qui est le bon choix pour profiler.
  3. Passer des options de compilation : -DCMAKE_CXX_FLAGS="-O3 -march=native".
  4. Exporter la base de compilation (-DCMAKE_EXPORT_COMPILE_COMMANDS=ON), qui permet à clang-tidy et 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 :

  1. Les indices commencent à 1 par défaut (et peuvent être déclarés avec n'importe quelle borne : real :: a(0:n-1)).
  2. Les tableaux multidimensionnels sont stockés en colonnes (column-major), à l'inverse du C. Donc dans a(i,j), c'est i qui doit varier dans la boucle interne. C'est la première source d'erreurs de portage et de régressions de localité.
  3. Pas d'aliasing de pointeurs : le langage garantit que les arguments intent(in) et intent(out) ne se recouvrent pas. C'est pourquoi les codes Fortran se vectorisent souvent mieux que les codes C équivalents.
  4. 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.
  5. 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

  1. Compiler et exécuter avec des modules différents. Voir l'encadré ci-dessus. Le plus coûteux.
  2. Recompiler le MPI du cluster. Vous perdez le réseau rapide et vous ne le verrez pas tout de suite.
  3. Ne pas enregistrer les options de compilation avec les résultats. Une mesure sans sa ligne de compilation est une donnée morte.
  4. Construire en Debug et mesurer. CMAKE_BUILD_TYPE=Debug implique -O0. Pour profiler, utilisez RelWithDebInfo, qui combine -O2 et -g.
  5. Le sur-abonnement BLAS. Invisible et coûteux.
  6. 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.
  7. 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

  1. module purge puis charger explicitement, la même liste à la compilation et à l'exécution, consignée dans le script de soumission.
  2. Ne jamais recompiler le MPI du cluster. Déclarez-le en paquet externe.
  3. Spack ou EasyBuild pour la pile, avec un fichier d'environnement versionné qui est le livrable.
  4. Le hash Git et les options de compilation dans chaque sortie. Dix lignes de CMake, et toutes vos mesures deviennent reproductibles.
  5. Apptainer, pas Docker, et tester le support MPI multi-nœuds à l'avance.
  6. Surveiller le sur-abonnement BLAS, qui est invisible et coûte un facteur.

Chapitre suivant : Outils de mesure.