Esecuzione di un job con Matlab

Esecuzione di un job con Matlab

Di seguto sono descritti i passi necessari per l'esecuzione di un job tramite Matlab.

Il primo passo consiste nel preparare tutto quanto è necessario per l’esecuzione dell’esperimento desiderato: in questo caso un file di testo esempio.m contenente le istruzioni nel linguaggio Matlab che costituiscono l'esperimento.
Il file conterrà le seguenti due istruzioni:

a <- rnorm(1000,0,1)
print(a)

Come per qualsiasi altro job è necessario preparare uno script (esempio_matlab.sh) per il gestore delle code.
Possiamo utilizzare lo stesso del QuickStart ma matlab vuole delle opzioni specifiche per essere esguito in modalità batch e non richiedere quindi un'interazione diretta con l'utente:

#!/bin/sh
#SBATCH --job-name=esempio_matlab                      #  nome attribuito al job
#SBATCH --output=%x_%j.log                 # nome del file di output; produce esempio_base_JOBID.log
#SBATCH --error=%x_%j.err                  # nome del file di errore; produce esempio_base_JOBID.err
#SBATCH --mem-per-cpu=1024M                      # quantità di memoria riservata
#SBATCH --time=0-00:02:00                        # stima tempo esecuzione (giorni-ore:minuti:secondi)
#SBATCH -n 1                                     # numero di processori
#SBATCH -N 1                                     # numero di server
#SBATCH --mail-user=utente.terastat2@uniroma1.it # proprio indirizzo email
#SBATCH --mail-type=ALL

module load matlab/R2020a
matlab -nodisplay -nosplash -nodesktop -batch "run('$HOME/esempio.m');exit;"
sleep 60

Lo script potrà quindi essere messo in esecuzione con il comando sbatch

sbatch esempio_matlab.sh

Esempio parallelilzzazione in R

Esempio parallelizzazione in R

Riportiamo qui un esempio di utilizzo della libreria doParallel di R per prarallelizzare parte di un algoritmo di stima del valore del pi greco.

Per questa stima viene utilizzato  il metodo Monte Carlo. Si considera un quadrato di lato 1 con un cerchio inscritto. Il rapporto delle aree del quadrato e del cerchio è proporzionale a Pi. Si distribuisono poi casualmente sul quadrato dei punti. Il rapporto tra il numero di punti distributi sul quadrato e quello dei punti che finiscono all'interno del cerchio inscritto sarà proporzionale, per grandi numeri di tentativi,  al rapporto tra le aree e, quindi, a pi greco.

Una implementazione sequenziale di questo algoritmo è:

library(ggplot2)
library(scales)

nCicli <- 6
nStime <- 64
nPunti <- 1

df <- data.frame( NumPunti=integer(), stima=double(), std=double(), time=double() )
for ( ciclo in 1:nCicli ){
    start_time <- Sys.time()
    nPunti <- nPunti * 10

    stime <- vector()
    for ( stima in 1:nStime ){
        nIn=0

        for ( step in 1:nPunti ) {
            x=runif(1,0,1)
            y=runif(1,0,1)
            if( x*x+y*y <= 1 ){
                nIn=nIn+1
            }
        }

        stime <- c( stime, 4*nIn/nPunti)
    }
    end_time <- Sys.time()
    df <- rbind( df, data.frame( NumPunti=nPunti, stima=mean(stime), std=sd(stime),
        time=end_time-start_time ) ) }
print(df)
p <- ggplot(df,aes(x = NumPunti, y= stima)) +
    geom_point() +
    geom_pointrange( aes( ymin=stima-std, ymax=stima+std))
    p + scale_x_log10()

ggsave("PiPlot_seq.pdf")

In questa implementazione, attraverso dei cicli, lo script ripete la stima numerose volte e con differenti numeri di punti casuali (da 1 a 1 milione); nello specifico vengono effettuate 64 stime per ognuno di 6 valori differenti del numero di punti.

Premesso che questo algoritmo è particolarmente adatto alla parallelizzazione in quanto le varie stime sono indipendenti l'una dall'altra, bisogna comunque individuare delle sottoprocedure che siano anche abbastanza corpose da giustificare l'inevitabile sovraccarico del controllo della parallelizzazione.

Per effettuare la parallelizzazione la libreria che andremo ad utilizzare (doParallel)  permette di definire un pool di processi worker a cui verrà affidata l'esecuzione delle sottoprocedure e, attraverso delle speciali direttive nei cicli di R permette di indicare quali parti del programma devono essere delegate  e parallelizzate. Ad esempio se si dispone di 10 worker e il ciclo prevede 100 passi la libreria farà si che i primi 10 passi vengano affidati ai processi worker e mano a mano che un worker termina il proprio lavoro, comunica il risultato al processo padre che gli affida il passo successivo fino al completamento del ciclo.

La prima cosa da fare è quindi modificare lo script di schedulazione assicurandosi di avere risorse a sufficienza per eseguire i processi worker:

#!/bin/sh

#SBATCH --job-name=stima_Pi_par # nome del job
#SBATCH --output=%x_32_%j.log # nome del file di output
#SBATCH --error=%x_32_%j.err # nome del file di errore
#SBATCH --mem-per-cpu=1G # memoria richiesta
#SBATCH --time=0-00:40:00 # tempo esecuzione richiesto (giorni-ore:minuti:secondi)
#SBATCH -n 32 # numero di processori da utilizzare
#SBATCH -N 1 # numero di server da utilizzare
#SBATCH --mail-user=utente@esempio.es # proprio indirizzo email
#SBATCH --mail-type=END

module load R
R --slave -f stimaPi_par.R

dove si vede che con la direttiva -n 32 vengono richiesti 32 core.

La versione parallelizzata della procedura diventa:

library(doParallel)
library(ggplot2)
library(scales)

num_cores <- as.numeric(Sys.getenv("SLURM_CPUS_ON_NODE"))
registerDoParallel(cores = num_cores)

nCicli <- 6
nStime <- 64
nPunti <- 1

df <- data.frame( NumPunti=integer(), stima=double(), std=double(), time=double() )
for ( ciclo in 1:nCicli ){
    start_time <- Sys.time()
    nPunti <- nPunti * 10

    stime = foreach ( stima=1:nStime, .combine='c' ) %dopar% {
        nIn <- 0
        for( step in 1:nPunti) {
            x=runif(1,0,1)
            y=runif(1,0,1)
            if( x*x+y*y <= 1 ){
                nIn = nIn+1
            }
        }
        4*nIn/nPunti
    }
    end_time <- Sys.time()
    df <- rbind( df, data.frame( NumPunti=nPunti, stima=mean(stime), std=sd(stime),
            time=as.numeric(end_time-start_time,units="secs") ) )
}

print(df)
p <- ggplot(df,aes(x = NumPunti, y= stima)) +
    geom_point() +
    geom_pointrange( aes( ymin=stima-std, ymax=stima+std))
p + scale_x_log10()

ggsave(paste("PiPlot_",num_cores,".pdf", sep=""))

Oltre all'inclusione della libreria scelta per la parallelizzazione si può vedere che viene indicato alla libreria doParallel il numero di core disponibili; infatti le due righe

num_cores <- as.numeric(Sys.getenv("SLURM_CPUS_ON_NODE"))
registerDoParallel(cores = num_cores)

leggono dall'ambiente di esecuzione il numero di core riservati sul nodo e utilizzano questa informazione per inizializzare il pool di worker.

Il ciclo foreach, il cui blocco calcolo la singola stima di ogni ciclo, tramite la direttiva %dopar% indica alla libreria che il blocco di codice deve essere esguito da uno dei woker.