MKL et parallélisme

Nous avons vu dans l'introduction à la MKL que cette librairie est par défaut en mode multi-threadé.

L'objet de cet article est d'exposer dans les grandes lignes :

  • les gains que vous pouvez en attendre
  • les interactions avec votre code si vous utilisez déjà du threading
  • le fonctionnement de la couche de threading
  • le bon usage sous l'ordonnanceur Slurm :!:

Limites

Nous ne traiterons pas spécifiquement dans cet article les interactions avec un programme utilisant MPI.

Dans ce cas, vous devez comprendre le contenu de cette page, et introduire en plus le fait que MPI génère des processus distincts. Suivant la façon dont vous ferez votre demande de réservation sous slurm, ces processus seront répartis sur des nœuds différents ou pourront être localisés sur le même nœud.

Fonctions threadées

  • Toute les fonctions de la MKL ne se prêtent pas à du multi-threading
  • Les familles de fonctions concernées sont :
    • Direct sparse solver
    • LAPACK : certaines fonctions
    • Level1 and Level2 BLAS : certaines fonctions
    • Level 3 BLAS et presque toutes les fonctions Sparse BLAS
    • Toutes les fonctions mathématiques VML
    • FFT
  • La liste exhaustive des méthodes est donnée dans le User's Guide, section Threaded Functions and Problems

Avec un programme threadé

Thread-safe

Nous rappelons que la MKL est thread-safe.

Autrement dit, si votre programme est déjà multi-threadé (quel que soit le moyen utilisé) et que vous faites appel à la MKL dans des threads, l'exécution du programme ne risque pas de “planter” ou de générer des résultats aléatoires.

Conflits possibles

Si votre programme utilise les compilateurs Intel et OpenMP, la MKL gérera parfaitement les inter-actions, car elle utilise la même librairie de parallélisation. C'est le cas général.

Par contre, si votre code utilise un autre mécanisme de multi-threading, ou un autre compilateur, des inter-actions malvenues surviendront. Le code fonctionnera, mais l'utilisation des ressources sera sous-optimisé, en générant plus de threads que le système n'a de ressources. Vous devez absolument consulter la documentation (section Avoiding Conflicts in the Execution Environment) pour gérer ce cas.

Nombre de threads

Nous arrivons maintenant à la partie critique. Si vous compilez votre programme avec la MKL en mode threadé (mode par défaut pour rappel), vous devez absolument comprendre comment votre code, à travers les appels à la MKL, consommera des cœurs sur les nœuds calcul.

Par défaut : tous les cœurs

Dans un cas “standard”, plus précisément :

  • si votre programme n'est pas dans une zone threadé avec OpenMP et compilateur Intel
  • et si vous n'utilisez aucune directive particulière pour les threads
    • pas d'appel à des fonctions OpenMP pour contrôler le nombre de threads
    • pas de variables d'environnement spécifique

alors la couche de threading utilisera tous les cœurs disponibles sur la machine.

Il est donc très important de prendre cela en compte quand vous lancerez un job sous slurm : il faudra dans ce cas réserver un nœud complet pour votre job, sinon le nœud sera en sur-allocation.

Dans une zone threadée

Si l'appel à une méthode threadée de la MKL survient à l'intérieur d'un thread de votre programme, et que vous utilisez OpenMP plus les compilateurs Intel, alors la MKL détectera qu'elle est déjà dans une zone threadée, et par défaut ne lancera pas de thread (fonctionnement par défaut modifiable).

Autrement dit, elle se comportera en mode séquentiel.

Fixer le nombre de threads

Plusieurs techniques sont possibles pour fixer explicitement le nombre de threads :

  • via des appels à des fonctions OpenMP ou MKL dans le code
  • via des variables d'environnement

Le plus simple consiste à utiliser une des deux variables suivantes :

  • OMP_NUM_THREADS, la variable générale d'OpenMP spécifiant le nombre de threads possibles
  • MKL_NUM_THREADS, variable spécifique à la MKL. Elle permet alors d'avoir un comportement différent entre la MKL et le reste de votre programme si vous utilisez aussi OpenMP dans votre code.

Slurm

A ce stade, nous avons vu que :

  • la couche de threading va en général lancer plusieurs threads
  • si nous le désirons, nous pouvons explicitement fixer un nombre maximum de threads utilisables

Nous allons maintenant voir comment écrire correctement votre job slurm.

Pourquoi y prêter attention ?

Sur le cluster, nous lançons les jobs via slurm. La réservation demandée à slurm devra correspondre à la consommation de CPU occasionnée par les threads, sinon les nœuds de calcul seront en sur-allocation et leur performance se dégradera pour tous les usagers.

Une alternative courante dans les grands centres de calcul est de ne permettre que un et un seul job sur un nœud à un instant donné. Un problème de sur-allocation n'impactera alors que l'utilisateur qui a commis une erreur dans sa demande. Mais ce n'est pas la politique d'ordonnancement choisie sur CALI, car les codes exécutés par nos chercheurs ne s'y prêtent pas toujours, un certains nombre de programme étant purement séquentiels.

Plusieurs approches possibles

Plusieurs manières de créer une demande de réservation correcte sont possibles.

Nous proposons ici deux méthodes. Suivant vos besoins et votre compréhension de la MKL et de slurm, vous pouvez envisager des alternatives.

Réserver un nœud complet

Puisque la MKL “mange” par défaut tous les cœurs d'une machine, la stratégie consistera ici à s'assurer que votre job disposera d'un nœud dans son intégralité. Ici encore, plusieurs solutions sont possibles, nous nous limiterons à vous proposer la plus “souple”.

Ajouter dans votre fichier de job :

#SBATCH --exclusive

Réserver le "bon" nombre de cœurs

Cette stratégie consiste simplement à réserver N cœurs au niveau de slurm (en vous rappelant que slurm les appelle des CPU), et à indiquer ce nombre à la MKL. Nous construisons simplement ci-après un job de type OpenMP :

#!/bin/bash
#
#SBATCH --ntasks=1
#SBATCH --cpus-per-task=8
#SBATCH --mem-per-cpu=300
#SBATCH --time 00:01:00

export OMP_NUM_THREADS=$SLURM_CPUS_PER_TASK
./mon_programme

Passer en séquentiel

Bien entendu, si vous rencontrez des problèmes de sur-allocation ou en cas de doute, vous pouvez aussi passer en mode séquentiel :

  • en compilant votre code avec la version séquentielle :
    • -mkl=sequential avec les compilateurs Intel
    • ou en choisissant le mode séquentiel dans le Advisor
    • ou avec la variable d'environnement MKL_THREADING_LAYER=SEQUENTIAL si vous utilisez la SDL (-lmkl_rt)
  • ou en limitant le nombre de thread à la valeur 1 via la variable MKL_NUM_THREADS=1 dans votre job