Paralléliser une boucle for en Python

35

Existe-t-il des outils en Python qui ressemblent au parfor de Matlab? J'ai trouvé ce fil , mais il a quatre ans. Je pensais que quelqu'un ici pourrait avoir une expérience plus récente.

Voici un exemple du type de chose que je voudrais paralléliser:

X = np.random.normal(size=(10, 3))
F = np.zeros((10, ))
for i in range(10):
    F[i] = my_function(X[i,:])

my_functionprend une ndarraytaille (1,3)et retourne un scalaire.

Au moins, j'aimerais utiliser plusieurs cœurs simultanément - comme parfor. En d'autres termes, supposons un système de mémoire partagée avec 8 à 16 cœurs.

Paul G. Constantine
la source
Beaucoup de résultats sur google. Celles-ci semblent assez simples: blog.dominodatalab.com/simple-parallelization quora.com/What-is-the-Python-equivalent-of-MATLABs-parfor
Doug Lipinski
Merci, @ doug-lipinski. Ces exemples, comme ceux que j'ai trouvés lors de la recherche sur Google, présentent des calculs triviaux basés sur l'indice d'itération. Et ils prétendent toujours que le code est "incroyablement facile". Mon exemple définit les tableaux (alloue la mémoire) en dehors de la boucle for. Je vais bien le faire d'une autre manière. c'est comme ça que je le fais dans Matlab. La partie délicate qui semble contredire ces exemples est d’obtenir une partie d’un tableau donné à la fonction dans la boucle.
Paul G. Constantine

Réponses:

19

Joblib fait ce que vous voulez. Le modèle d'utilisation de base est:

from joblib import Parallel, delayed

def myfun(arg):
     do_stuff
     return result

results = Parallel(n_jobs=-1, verbose=verbosity_level, backend="threading")(
             map(delayed(myfun), arg_instances))

arg_instancesest la liste des valeurs pour lesquelles myfunest calculé en parallèle. La principale restriction est que cela myfundoit être une fonction de niveau supérieur. Le backendparamètre peut être "threading"ou "multiprocessing".

Vous pouvez transmettre des paramètres communs supplémentaires à la fonction parallélisée. Le corps de myfunpeut également faire référence à des variables globales initialisées, les valeurs qui seront disponibles pour les enfants.

Les args et les résultats peuvent être à peu près tout avec le backend de threading, mais les résultats doivent être sérialisables avec le backend à multi-traitements.


Dask offre également des fonctionnalités similaires. Cela peut être préférable si vous travaillez avec des données non essentielles ou si vous essayez de paralléliser des calculs plus complexes.

Daniel Mahler
la source
Je ne vois aucune valeur ajoutée pour utiliser la batterie, y compris le multitraitement. Je parierais que joblib l'utilise sous le capot.
Xavier Combelle
1
Il convient de mentionner que joblib n’est pas magique, le threadingbackend souffre du goulot d’étranglement de GIL et le multiprocessingbackend génère une surcharge importante en raison de la sérialisation de tous les paramètres et valeurs de retour. Voir cette réponse pour les détails de bas niveau du traitement parallèle en Python.
Jakub Klinkovský
Je ne trouve pas une combinaison de complexité de fonction et de nombre d'itérations pour laquelle joblib serait plus rapide qu'une boucle for. Pour moi, il a la même vitesse si n_jobs = 1 et est beaucoup plus lent dans tous les autres cas
Aleksejs Fomins
@AleksejsFomins Le parallélisme basé sur les threads n'aidera pas le code qui ne libère pas le GIL, mais un nombre significatif le fait, en particulier la science des données ou les bibliothèques numériques. Sinon, vous avez besoin du multi-traitement, Jobli prend en charge les deux. Le module de multitraitement a maintenant aussi un parallèle mapque vous pouvez utiliser directement. De plus, si vous utilisez mkl compiled numpy, les opérations vectorisées seront automatiquement parallélisées sans que vous ne fassiez rien. Le numpy dans Ananconda est mkl activé par défaut. Il n'y a pas de solution universelle cependant. Joblib est très peu agité et il y avait moins d'otions en 2015.
Daniel Mahler
Merci pour vos conseils. Je me souviens d’avoir essayé le multitraitement avant et même d’écrire quelques articles, parce que cela n’avait pas l’échelle escomptée. Peut-être que je devrais lui donner un autre regard
Aleksejs Fomins
9

Ce que vous cherchez, c'est Numba , qui peut automatiquement paralléliser une boucle for. De leur documentation

from numba import jit, prange

@jit
def parallel_sum(A):
    sum = 0.0
    for i in prange(A.shape[0]):
        sum += A[i]

    return sum
LKlevin
la source
8

Sans supposer quelque chose de spécial sur le my_functionchoix multiprocessing.Pool().map()est une bonne idée pour paralléliser de telles boucles simples. joblib, dask, Des mpicalculs ou numbacomme proposé dans d' autres réponses ne regarde pas apporter aucun avantage pour les cas d'utilisation et ajouter des dépendances inutiles (pour résumer leur surpuissance). L'utilisation de threads comme proposé dans une autre réponse n'est probablement pas une bonne solution, car vous devez être familier avec l'interaction GIL de votre code, sinon votre code devrait principalement effectuer des entrées / sorties.

Cela dit numbapourrait être une bonne idée pour accélérer le code python pur séquentiel, mais j'estime que cela sort du cadre de la question.

import multiprocessing
import numpy as np

if __name__ == "__main__":
   #the previous line is necessary under windows to not execute 
   # main module on each child under windows

   X = np.random.normal(size=(10, 3))
   F = np.zeros((10, ))

   pool = multiprocessing.Pool(processes=16)
   # if number of processes is not specified, it uses the number of core
   F[:] = pool.map(my_function, (X[i,:] for i in range(10)) )

Il y a cependant quelques réserves (qui ne devraient pas affecter la plupart des applications):

  • sous Windows, il n’existe pas de prise en charge, donc un interprète avec le module principal est lancé au démarrage de chaque enfant; il peut donc y avoir une surcharge (et c’est la raison de la if __name__ == "__main__"
  • Les arguments et les résultats de my_function sont décapés et non décodés. Il peut s'agir d'un temps système trop important. Voir la réponse à cette question pour la réduire . Https://stackoverflow.com/a/37072511/128629 . Cela rend également les objets non sélectionnables inutilisables
  • my_functionne devrait pas dépendre d'états partagés comme la communication avec des variables globales car les états ne sont pas partagés entre processus. Les fonctions pures (fonctions au sens mathématique) sont des exemples de fonctions qui ne partagent pas d'états
Xavier Combelle
la source
6

Mon impression de Parfor est que MATLAB encapsule les détails de l'implémentation, de sorte qu'il pourrait utiliser à la fois le parallélisme de mémoire partagée (comme vous le souhaitez) et le parallélisme de mémoire distribuée (si vous utilisez un serveur d'informatique distribuée MATLAB ).

Si vous voulez un parallélisme de mémoire partagée et que vous exécutez une sorte de boucle de tâche parallèle, le paquet de bibliothèque standard multitraitement est probablement ce que vous voulez, peut-être avec un joli front-end, comme joblib , comme mentionné dans l'article de Doug. La bibliothèque standard ne va pas disparaître, elle est maintenue et présente donc un risque faible.

Il existe également d'autres options, telles que les fonctionnalités parallèles de Parallel Python et IPython . Un coup d’œil rapide à Parallel Python me fait penser que c’est plus proche de l’esprit de parfor, dans la mesure où la bibliothèque encapsule les détails du cas distribué, mais le coût de cette opération est que vous devez adopter leur écosystème. Le coût d'utilisation d'IPython est similaire. vous devez adopter la façon de faire IPython, qui peut ne pas valoir la peine.

Si vous vous souciez de la mémoire distribuée, je vous recommande mpi4py . Lisandro Dalcin fait un excellent travail et mpi4py est utilisé dans les wrappers PETSc Python. Je ne pense donc pas que cela disparaisse de si tôt. Comme le multitraitement, il s’agit d’une interface de niveau (inférieur) avec le parallélisme que parforfor, mais qui est susceptible de durer un certain temps.

Geoff Oxberry
la source
Merci, Geoff. Avez-vous une expérience de travail avec ces bibliothèques? Peut-être que je vais essayer d'utiliser mpi4py sur une machine à mémoire partagée / un processeur multicœur.
Paul G. Constantine
@PaulGConstantine J'ai utilisé mpi4py avec succès; c'est assez simple, si vous connaissez MPI. Je n'ai pas utilisé le multitraitement, mais je l'ai recommandé à mes collègues, qui ont déclaré que cela fonctionnait bien pour eux. J'ai également utilisé IPython, mais pas les fonctionnalités de parallélisme, je ne peux donc pas vous dire à quel point cela fonctionne.
Geoff Oxberry
1
Aron a préparé un bon tutoriel sur mpi4py pour le cours PyHPC à Supercomputing: github.com/pyHPC/pyhpc-tutorial
Matt Knepley
4

Avant de rechercher un outil "boîte noire" pouvant être utilisé pour exécuter en parallèle des fonctions python "génériques", je suggérerais d'analyser comment il my_function()est possible de paralléliser à la main.

Premièrement, comparez le temps d’exécution de la surcharge de la boucle my_function(v)python for: [C] Les forboucles Python sont assez lentes, le temps d’attente my_function()pourrait donc être négligeable.

>>> timeit.timeit('pass', number=1000000)
0.01692986488342285
>>> timeit.timeit('for i in range(10): pass', number=1000000)
0.47521495819091797
>>> timeit.timeit('for i in xrange(10): pass', number=1000000)
0.42337894439697266

Deuxième vérification s’il existe une simple implémentation vectorielle my_function(v)ne nécessitant pas de boucles:F[:] = my_vector_function(X)

(Ces deux premiers points sont assez triviaux, pardonnez-moi si je les ai mentionnés ici pour des raisons de complétude.)

Le troisième point, et le plus important, du moins pour les implémentations CPython, consiste à vérifier si la my_functionplupart du temps est passé à l' intérieur ou à l' extérieur du verrou d'interpréteur global , ou GIL . Si vous passez du temps en dehors de GIL, vous devez utiliser le threadingmodule de bibliothèque standard . ( Voici un exemple). BTW, on pourrait penser à écrire my_function()comme une extension C juste pour libérer le GIL.

Enfin, si my_function()ne libère pas le GIL, on pourrait utiliser le multiprocessingmodule .

Références: Documents Python sur l'exécution simultanée et intro numpy / scipy sur le traitement parallèle .

Stefano M
la source
2

Tu peux essayer Julia. C'est assez proche de Python et a beaucoup de constructions MATLAB. La traduction est ici:

F = @parallel (vcat) for i in 1:10
    my_function(randn(3))
end

Cela rend les nombres aléatoires en parallèle aussi, et concatène simplement les résultats à la fin de la réduction. Cela utilise le multitraitement (vous devez donc addprocs(N)ajouter des processus avant de l'utiliser et cela fonctionne également sur plusieurs nœuds d'un HPC, comme indiqué dans cet article de blog ).

Vous pouvez également utiliser à la pmapplace:

F = pmap((i)->my_function(randn(3)),1:10)

Si vous voulez un parallélisme entre les threads, vous pouvez utiliser Threads.@threads(assurez-vous toutefois de rendre l'algorithme thread-safe). Avant d'ouvrir Julia, définissez la variable d'environnement JULIA_NUM_THREADS, puis procédez comme suit:

Ftmp = [Float64[] for i in Threads.nthreads()]
Threads.@threads for i in 1:10
    push!(Ftmp[Threads.threadid()],my_function(randn(3)))
end
F = vcat(Ftmp...)

Ici, je crée un tableau séparé pour chaque thread, afin qu’ils ne se contredisent pas lors de l’ajout au tableau, puis concaténent les tableaux par la suite. Les threads étant assez nouveaux, nous utilisons maintenant directement les threads, mais je suis sûr que des réductions et des maps threadées seront ajoutées comme dans le cas du multitraitement.

Chris Rackauckas
la source
0

Je recommande d'utiliser les fonctions parallèles et différées de la bibliothèque joblib, d'utiliser le module "tempfile" pour créer de la mémoire partagée temporaire pour de grands tableaux, les exemples et l'utilisation peuvent être trouvés ici https://pythonhosted.org/joblib/parallel.html

Ramkumar
la source