web-dev-qa-db-fra.com

multiprocessing.Pool () plus lent que la simple utilisation de fonctions ordinaires

(Cette question concerne la façon de rendre le code multiprocessing.Pool () plus rapide. Je l'ai finalement résolu, et la solution finale peut être trouvée au bas de l'article.)

Question d'origine:

J'essaie d'utiliser Python pour comparer un mot avec de nombreux autres mots dans une liste et récupérer une liste des mots les plus similaires. Pour ce faire, j'utilise la fonction difflib.get_close_matches. I suis sur un ordinateur portable Windows 7 relativement nouveau et puissant, avec Python 2.6.5.

Ce que je veux, c'est accélérer le processus de comparaison car ma liste de comparaison de mots est très longue et je dois répéter le processus de comparaison plusieurs fois. Lorsque j'ai entendu parler du module de multitraitement, il semblait logique que si la comparaison pouvait être divisée en tâches de travail et s'exécuter simultanément (et donc utiliser l'énergie de la machine en échange d'une vitesse plus rapide), ma tâche de comparaison se terminerait plus rapidement.

Cependant, même après avoir essayé de nombreuses façons différentes et utilisé des méthodes qui ont été présentées dans les documents et suggérées dans les messages du forum, la méthode Pool semble être incroyablement lente, beaucoup plus lente que d'exécuter la fonction get_close_matches d'origine sur toute la liste à une fois que. Je voudrais de l'aide pour comprendre pourquoi Pool () est si lent et si je l'utilise correctement. Je n'utilise ce scénario de comparaison de chaînes qu'à titre d'exemple, car c'est l'exemple le plus récent auquel j'ai pu penser où je n'ai pas pu comprendre ou faire fonctionner le multitraitement plutôt que contre moi. Ci-dessous est juste un exemple de code du scénario difflib montrant les différences de temps entre les méthodes ordinaires et regroupées:

from multiprocessing import Pool
import random, time, difflib

# constants
wordlist = ["".join([random.choice([letter for letter in "abcdefghijklmnopqersty"]) for lengthofword in xrange(5)]) for nrofwords in xrange(1000000)]
mainword = "hello"

# comparison function
def findclosematch(subwordlist):
    matches = difflib.get_close_matches(mainword,subwordlist,len(subwordlist),0.7)
    if matches <> []:
        return matches

# pool
print "pool method"
if __name__ == '__main__':
    pool = Pool(processes=3)
    t=time.time()
    result = pool.map_async(findclosematch, wordlist, chunksize=100)
    #do something with result
    for r in result.get():
        pass
    print time.time()-t

# normal
print "normal method"
t=time.time()
# run function
result = findclosematch(wordlist)
# do something with results
for r in result:
    pass
print time.time()-t

Le mot à trouver est "bonjour", et la liste des mots dans lesquels rechercher des correspondances proches est une liste longue de 1 million de 5 caractères joints au hasard (uniquement à des fins d'illustration). J'utilise 3 cœurs de processeur et la fonction de carte avec une taille de bloc de 100 (listitems à traiter par travailleur, je pense ??) (j'ai également essayé des blocs de 1000 et 10 000 mais il n'y avait pas de réelle différence). Notez que dans les deux méthodes, je démarre le temporisateur juste avant d'appeler ma fonction et le termine juste après avoir parcouru les résultats. Comme vous pouvez le voir ci-dessous, les résultats temporels sont clairement en faveur de la méthode originale hors pool:

>>> 
pool method
37.1690001488 seconds
normal method
10.5329999924 seconds
>>> 

La méthode Pool est presque 4 fois plus lente que la méthode originale. Y a-t-il quelque chose qui me manque ici, ou peut-être un malentendu sur le fonctionnement du Pooling/multiprocessing? Je soupçonne qu'une partie du problème ici pourrait être que la fonction de carte renvoie Aucun et ajoute ainsi des milliers d'éléments inutiles à la liste de résultats même si je souhaite uniquement que les correspondances réelles soient renvoyées aux résultats et que je les ai écrites comme telles dans la fonction. D'après ce que je comprends, c'est comme ça que fonctionne la carte. J'ai entendu parler d'autres fonctions comme le filtre qui collecte uniquement les résultats non faux, mais je ne pense pas que le multiprocessing/Pool prend en charge la méthode de filtrage. Y a-t-il d'autres fonctions en plus de map/imap dans le module de multitraitement qui pourraient m'aider à ne renvoyer que ce que ma fonction retourne? La fonction Appliquer est plus pour donner plusieurs arguments si je comprends bien.

Je sais qu'il y a aussi la fonction imap, que j'ai essayée mais sans aucune amélioration de temps. La raison étant la même raison pour laquelle j'ai eu des problèmes pour comprendre ce qui est si génial dans le module itertools, soi-disant "rapide comme l'éclair", ce que j'ai remarqué est vrai pour appeler la fonction, mais d'après mon expérience et d'après ce que j'ai lu, c'est parce que l'appel de la fonction ne fait en fait aucun calcul, donc quand il est temps d'itérer les résultats pour les collecter et les analyser (sans quoi il ne serait pas utile d'appeler le cuntion) cela prend autant ou parfois plus de temps qu'un en utilisant simplement la version normale de la fonction droite. Mais je suppose que c'est pour un autre post.

Quoi qu'il en soit, excité de voir si quelqu'un peut me pousser dans la bonne direction ici, et vraiment apprécier toute aide à ce sujet. Je suis plus intéressé par la compréhension du multitraitement en général que par le fait de faire fonctionner cet exemple, bien qu'il soit utile avec quelques exemples de suggestions de code de solution pour faciliter ma compréhension.

La réponse:

Il semble que le ralentissement soit lié au temps de démarrage lent des processus supplémentaires. Je n'ai pas pu obtenir la fonction .Pool () pour être assez rapide. Ma solution finale pour l'accélérer était de diviser manuellement la liste de charge de travail, d'utiliser plusieurs .Process () au lieu de .Pool () et de renvoyer les solutions dans une file d'attente. Mais je me demande si le changement le plus crucial aurait peut-être été de diviser la charge de travail en termes de mot principal à rechercher plutôt que de mots avec lesquels comparer, peut-être parce que la fonction de recherche difflib est déjà si rapide. Voici le nouveau code qui exécute 5 processus en même temps et s'est avéré environ x10 plus rapide que l'exécution d'un code simple (6 secondes contre 55 secondes). Très utile pour les recherches floues rapides, en plus de la vitesse de difflib déjà.

from multiprocessing import Process, Queue
import difflib, random, time

def f2(wordlist, mainwordlist, q):
    for mainword in mainwordlist:
        matches = difflib.get_close_matches(mainword,wordlist,len(wordlist),0.7)
        q.put(matches)

if __name__ == '__main__':

    # constants (for 50 input words, find closest match in list of 100 000 comparison words)
    q = Queue()
    wordlist = ["".join([random.choice([letter for letter in "abcdefghijklmnopqersty"]) for lengthofword in xrange(5)]) for nrofwords in xrange(100000)]
    mainword = "hello"
    mainwordlist = [mainword for each in xrange(50)]

    # normal approach
    t = time.time()
    for mainword in mainwordlist:
        matches = difflib.get_close_matches(mainword,wordlist,len(wordlist),0.7)
        q.put(matches)
    print time.time()-t

    # split work into 5 or 10 processes
    processes = 5
    def splitlist(inlist, chunksize):
        return [inlist[x:x+chunksize] for x in xrange(0, len(inlist), chunksize)]
    print len(mainwordlist)/processes
    mainwordlistsplitted = splitlist(mainwordlist, len(mainwordlist)/processes)
    print "list ready"

    t = time.time()
    for submainwordlist in mainwordlistsplitted:
        print "sub"
        p = Process(target=f2, args=(wordlist,submainwordlist,q,))
        p.Daemon = True
        p.start()
    for submainwordlist in mainwordlistsplitted:
        p.join()
    print time.time()-t
    while True:
        print q.get()
25
Karim Bahgat

Ma meilleure estimation est la surcharge de communication inter-processus (IPC). Dans l'instance à processus unique, le processus unique a la liste Word. Lors de la délégation à divers autres processus, le processus principal doit constamment transférer des sections de la liste vers d'autres processus.

Ainsi, il s'ensuit qu'une meilleure approche pourrait être de dériver des processus n, chacun étant responsable du chargement/de la génération 1/n segment de la liste et de vérifier si le Le mot est dans cette partie de la liste.

Je ne sais pas comment faire cela avec la bibliothèque multiprocessing de Python, cependant.

8
Multimedia Mike

Ces problèmes se résument généralement aux suivants:

La fonction que vous essayez de paralléliser ne nécessite pas suffisamment de ressources CPU (c'est-à-dire du temps CPU) pour rationaliser la parallélisation !

Bien sûr, lorsque vous parallélisez avec multiprocessing.Pool(8), vous théoriquement ( mais pas pratiquement) pourrait obtenir un 8x accélérer.

Cependant, gardez à l'esprit que ce n'est pas gratuit - vous gagnez cette parallélisation au détriment des frais généraux suivants:

  1. Créer un task pour chaque chunk (de taille chunksize) dans votre iter passé à Pool.map(f, iter)
  2. Pour chaque task
    1. Sérialisez le task et la valeur de retour task's ( pensezpickle.dumps())
    2. Désérialisez le task et la valeur de retour task's ( pensezpickle.loads())
    3. Perdre beaucoup de temps à attendre Locks sur la mémoire partagée Queues, tandis que les processus de travail et les processus parents get() et put() de/vers ces Queues.
  3. Coût unique des appels à os.fork() pour chaque processus de travail, ce qui est coûteux.

En substance, lorsque vous utilisez Pool() vous voulez:

  1. Besoins élevés en ressources CPU
  2. Faible empreinte de données transmise à chaque appel de fonction
  3. iter raisonnablement long pour justifier le coût unique de (3) ci-dessus.

Pour une exploration plus approfondie, cet article et cette conversation liée expliquez la taille des données transmises à Pool.map() ( et amis) obtient vous en difficulté.

Raymond Hettinger parle également de la bonne utilisation de la concurrence de Python ici.

7
The Aelfinn

J'ai vécu quelque chose de similaire avec la piscine sur un problème différent. Je ne suis pas sûr de la cause réelle à ce stade ...

The Answer edit by OP Karim Bahgat est la même solution qui a fonctionné pour moi. Après être passé à un système Process & Queue, j'ai pu voir les accélérations en ligne avec le nombre de cœurs pour une machine.

Voici un exemple.

def do_something(data):
    return data * 2

def consumer(inQ, outQ):
    while True:
        try:
            # get a new message
            val = inQ.get()

            # this is the 'TERM' signal
            if val is None:
                break;

            # unpack the message
            pos = val[0]  # its helpful to pass in/out the pos in the array
            data = val[1]

            # process the data
            ret = do_something(data)

            # send the response / results
            outQ.put( (pos, ret) )


        except Exception, e:
            print "error!", e
            break

def process_data(data_list, inQ, outQ):
    # send pos/data to workers
    for i,dat in enumerate(data_list):
        inQ.put( (i,dat) )

    # process results
    for i in range(len(data_list)):
        ret = outQ.get()
        pos = ret[0]
        dat = ret[1]
        data_list[pos] = dat


def main():
    # initialize things
    n_workers = 4
    inQ = mp.Queue()
    outQ = mp.Queue()
    # instantiate workers
    workers = [mp.Process(target=consumer, args=(inQ,outQ))
               for i in range(n_workers)]

    # start the workers
    for w in workers:
        w.start()

    # gather some data
    data_list = [ d for d in range(1000)]

    # lets process the data a few times
    for i in range(4):
        process_data(data_list)

    # tell all workers, no more data (one msg for each)
    for i in range(n_workers):
        inQ.put(None)
    # join on the workers
    for w in workers:
        w.join()

    # print out final results  (i*16)
    for i,dat in enumerate(data_list):
        print i, dat
1
verdverm