Exécution d'un code R avec parallélisation (parLapply)

Bonjour !

J'essaie d'exécuter un code R incluant une parallélisation avec la fonction parLapply. Lorsque je spécifie une parallélisation sur 3 cores dans la fonction parLapply, les 3 processus parallèles vont très vite, alors que si je demande de paralléliser sur 50 cores, les 50 processus parallèles deviennent tous très lents. Pourtant, dans le batch, je précise bien un nombre de CPUs suffisant :

#!/bin/bash
#
#SBATCH -A mon_projet
#SBATCH -p long
#SBATCH --mem 250GB
#SBATCH -N 1
#SBATCH --cpus-per-task=100
#SBATCH -o slurm.%N.%j.out
#SBATCH -e slurm.%N.%j.err

module load r/4.4.1  
srun Rscript mon_code.R

Je vous remercie par avance pour votre aide.

Bonjour,

Avant toute chose, notamment au vu des ressources demandées, pensez bien à vérifier leur bon usage. Par exemple avec reportseff à la fin du job: SLURM job efficiency - IFB Core Cluster Documentation, ou avec htop pendant le job: SLURM job efficiency - IFB Core Cluster Documentation

Les ressources sont bien réservés sur le noeud de calcul.
Le problème vient donc du script.

En ce moment, on peut observer qu'il y a plus de 80 processus R qui lance chacun 128 threads R ...

Sortie de pstree sur le noeud de calcul
$ pstree  -u arossignol 
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
R───128*[{R}]
slurm_script───srun─┬─srun
                    └─3*[{srun}]
R───128*[{R}]

C'est donc complètement contre-productif puisqu'il faudrait 80*128 threads ~ 10240 CPU pour faire le traitement efficacement.

Il y a donc un couac dans le script/fonction/appel.

Je vous remercie pour votre éclairage. Mais je ne vois malheureusement pas ce qui ne fonctionne pas dans mon code R. Quand je travaille sur ma machine personnelle, il me suffit de paralléliser sur mes 10 cores et ça marche habituellement bien.

En fait, à quoi correspondent exactement les 128 threads ?

En effet, dans la fonction R que j'utilise pour la parallélisation (parLapply), je ne spécifie que le nombre de cores sur lesquels envoyer successivement et parallèlement les calculs (en l'occurrence, j'ai lancé sur 80 cores comme). Mais je ne spécifie jamais un nombre de threads à employer.

En outre, lorsque j'utilise la fonction R detectCores() qui donne le nombre de cores disponibles, la fonction renvoie 256. Je ne vois donc pas à quoi peuvent correspondre ces 256 cores (si ce n'est que 128 x 2 = 256)...

Merci beaucoup !

Vous ne spécifier pas le nombre de threads mais je pense que la fonction le fait pour vous.
Je pense qu'elle détecte elle même le nombre de cœurs disponible et lance autant de threads correspondants par processus. De manière générale ce genre de fonctionnalité n'est pas bonne sur le cluster puisque le nombre de CPU alloué est souvent inférieur au nombre de cœur sur la machine.

La machine sur laquelle le job 44625861 s'est exécuté est cpu-node-89.
Cette machine est équipé de 2 "AMD EPYC 7662 64-Core Processor".
Il y a donc 128 cœurs disponibles. Et c'est ce que détecte, je pense, la fonction.
Il se trouve que ces cœurs utilisent aussi la fonctionnalité d'hyperthread qui peut permettre de monter à 2 threads par cœur (128*2 = 256).

Bref, dans tous les cas, je vous conseille de forcer le nombre de threads/cores/process à utiliser pour maîtriser complètement la parallélisation. Vous pouvez d'ailleurs utiliser la variable $SLURM_CPUS_PER_TASK qui corresponds au nombre de coeurs demandés à Slurm (cf. SLURM user guide - IFB Core Cluster Documentation).

Je vous remercie pour ces précisions !

Après quelques investigations, il s'avère que certaines fonctions sur R vont naturellement utiliser tous les threads disponibles sur les cores, sans rester nécessairement sur le core affecté par la fonction de parallélisation. Par conséquent, il est nécessaire de fixer explicitement dans le code R le nombre de threads utilisables par processus à 1 (ou à une autre valeur mais qui doit éviter un "télescopage" threads/cores en parallèle). Pour cela, il existe la fonction blas_set_num_threads du package RhpcBLASctl (https://cran.r-project.org/web/packages/RhpcBLASctl/RhpcBLASctl.pdf) ou la fonction threads_fst du package fst (https://cran.r-project.org/web/packages/fst/fst.pdf). Il semblerait que d'autres fonctions existent pour certains packages mais, en tout cas, celles que je cite m'ont permis de résoudre mon problème.

1 « J'aime »

Parfait. Merci pour les précisions sur la fonction et le package R.