Poursuivant sur ma lancée d’exploration de code à haute performance, regardons aujourd’hui comment ajouter du traitement parallèle dans vos scripts R. Bien que plusieurs options existent pour paralléliser le traitement de vos données, concentrons nous sur quelque chose de très facile à mettre en place pour commencer.

Il y a quelques temps, j’ai eu à écrire un script ayant pour but de rouler un grand nombre de regressions logistiques (à l’aide du package glm) dans un effort de modélisation de données. Le tout produisait les résultats escomptés mais prenait un temps non-négligeable puisqu’un grand nombre de ces régressions devaient être calculées (et optimisées !). Par contre, le calcul de chacune des ces régression était indépendant des autres… Je me suis donc mis à la recherche de solutions me permettant de paralléliser ce traitement.

La solution retenue (et qui correspondait à la structure de mon code) à été la simple substitution de la fonction lapply par une implémentation parallèle de celle-ci nommée mclapply et disponible à travers le package parallel (qui fait partie de la distribution de R depuis la version 2.14). Une simple substitution de fonction dans mon code à coupé mon de temps de calcul en 4 ! (Grosso modo.. :)).

Et quand je parle de substitution simple, voici ce que j’ai en tête:
Fragment de code original:

...

gene_scores <- data.frame()
gene_scores <- do.call('rbind', lapply(genes, function(x, data, formula) {
			yvar <- all.vars(formula)[1]
			work <- data[,c(yvar, x)]
			model <- glm (formula, data=work, family=binomial)
			s <- summary(model)
			crossval <- CV_JPL(model, print.details=FALSE)
			return(data.frame(gene=x, deviance=s$deviance, 
                                          acc.cv=crossval$acc.cv, 
                                          acc.internal=crossval$acc.internal))
}, data=training, formula=formula))

...

Fragment parallélisé:

library(parallel) # Il faut charger la librairie en mémoire !

...

gene_scores <- data.frame()
gene_scores <- do.call('rbind', mclapply(genes, function(x, data, formula) {
			yvar <- all.vars(formula)[1]
			work <- data[,c(yvar, x)]
			model <- glm (formula, data=work, family=binomial)
			s <- summary(model)
			crossval <- CV_JPL(model, print.details=FALSE)
			return(data.frame(gene=x, deviance=s$deviance, 
                                          acc.cv=crossval$acc.cv, 
                                          acc.internal=crossval$acc.internal))
}, data=training, formula=formula, mc.cores=4))

...

J'ai mis les changement en gras pour qu'ils ressortent mieux.
En gros, les voici:
1- charger la librairie en mémoire
2- modifier l'appel à la fonction lapply pour mclapply
3- spécifier le nombre de coeurs disponibles pour l'exécution à l'aide du paramètre mc.cores=X

Le plus gros problème rencontré une fois ce changement effectué se trouve au niveau des étapes de test/débuggage. Il est plus compliqué de stopper le traitement de fonctions parallélisées que leur versions non parallèles. Mais bon, on peut facilement contourner ce léger désagrément en travaillant avec un jeu de données réduit ou encore en ne substituant lapply pour mclapply qu'au moment de lancer les calculs finaux !

En conclusion, voici une modification extrêmement simple à mettre en place si votre code fait appel à lapply, alors il n'y a vraiment pas de raison de s'en passer ! Toutefois, si vous n'utilisez pas lapply dans votre code, bon nombre de librairies offrent la possibilité de paralléliser votre traitement de vos données. Le duo foreach / DoMC vient rapidement à l'esprit. De plus, n'hésitez pas à jeter un oeil sur cette page qui liste un grand nombre de ressources supplémentaires.

À la prochaine 🙂