web-dev-qa-db-fra.com

Comment utiliser tous les cœurs avec python multiprocessing

ont joué avec la fonction multicore de Python depuis plus d'une heure maintenant, essayant de paralléliser une fonction de traversée de graphe plutôt complexe en utilisant Process et Manager:

import networkx as nx
import csv
import time 
from operator import itemgetter
import os
import multiprocessing as mp

cutoff = 1

exclusionlist = ["cpd:C00024"]

DG = nx.read_gml("KeggComplete.gml", relabel = True)

for exclusion in exclusionlist:
    DG.remove_node(exclusion)

#checks if 'memorizedPaths exists, and if not, creates it
fn = os.path.join(os.path.dirname(__file__), 'memorizedPaths' + str(cutoff+1))
if not os.path.exists(fn):
    os.makedirs(fn)

manager = mp.Manager()
memorizedPaths = manager.dict()
filepaths = manager.dict()
degreelist = sorted(DG.degree_iter(),key=itemgetter(1),reverse=True)

def _all_simple_paths_graph(item, DG, cutoff, memorizedPaths, filepaths):
    source = item[0]
    uniqueTreePaths = []
    if cutoff < 1:
        return
    visited = [source]
    stack = [iter(DG[source])]
    while stack:
        children = stack[-1]
        child = next(children, None)
        if child is None:
            stack.pop()
            visited.pop()
        Elif child in memorizedPaths:
            for path in memorizedPaths[child]:
                newPath = (Tuple(visited) + Tuple(path))
                if (len(newPath) <= cutoff) and (len(set(visited) & set(path)) == 0):
                    uniqueTreePaths.append(newPath)
            continue
        Elif len(visited) < cutoff:
            if child not in visited:
                visited.append(child)
                stack.append(iter(DG[child]))
                if visited not in uniqueTreePaths:
                    uniqueTreePaths.append(Tuple(visited))
        else: #len(visited) == cutoff:
            if (visited not in uniqueTreePaths) and (child not in visited):
                uniqueTreePaths.append(Tuple(visited + [child]))
            stack.pop()
            visited.pop()
    #writes the absolute path of the node path file into the hash table
    filepaths[source] = str(fn) + "/" + str(source) +"path.txt"
    with open (filepaths[source], "wb") as csvfile2:
        writer = csv.writer(csvfile2, delimiter=' ', quotechar='|')
        for path in uniqueTreePaths:
            writer.writerow(path)
    memorizedPaths[source] = uniqueTreePaths

############################################################################

start = time.clock()
if __name__ == '__main__':
    for item in degreelist:
        test = mp.Process(target=_all_simple_paths_graph, args=(DG, cutoff, item, memorizedPaths, filepaths))
        test.start()
        test.join()
end = time.clock()
print (end-start)

Actuellement - bien que la chance et la magie - cela fonctionne (en quelque sorte). Mon problème est que j'utilise seulement 12 de mes 24 cœurs.

Quelqu'un peut-il expliquer pourquoi cela pourrait être le cas? Peut-être que mon code n'est pas la meilleure solution de multitraitement, ou est-ce une caractéristique de mon architecture [CPU Intel Xeon E5-2640 @ 2.50GHz x18 fonctionnant sur Ubuntu 13.04 x64]?

MODIFIER:

J'ai réussi à obtenir:

p = mp.Pool()
for item in degreelist:
    p.apply_async(_all_simple_paths_graph, args=(DG, cutoff, item, memorizedPaths, filepaths))
p.close()
p.join()

Cependant, cela fonctionne TRÈS LENT! Je suppose donc que j'utilise la mauvaise fonction pour le travail. j'espère que cela aide à clarifier exactement ce que j'essaie d'accomplir!

EDIT2: .map tentative:

partialfunc = partial(_all_simple_paths_graph, DG=DG, cutoff=cutoff, memorizedPaths=memorizedPaths, filepaths=filepaths)
p = mp.Pool()
for item in processList:
    processVar = p.map(partialfunc, xrange(len(processList)))   
p.close()
p.join()

Fonctionne, est plus lent que le single-core. Il est temps d'optimiser!

20
Darkstarone

Trop de piles ici pour être traitées dans les commentaires, donc, où mp est multiprocessing:

mp.cpu_count() devrait retourner le nombre de processeurs. Mais testez-le. Certaines plateformes sont géniales, et ces informations ne sont pas toujours faciles à obtenir. Python fait de son mieux.

Si vous démarrez 24 processus, ils feront exactement ce que vous leur demandez de faire ;-) On dirait que mp.Pool() serait le plus pratique pour vous. Vous transmettez le nombre de processus que vous souhaitez créer à son constructeur. mp.Pool(processes=None) utilisera mp.cpu_count() pour le nombre de processeurs.

Ensuite, vous pouvez utiliser, par exemple, .imap_unordered(...) sur votre instance Pool pour répartir votre degreelist entre les processus. Ou peut-être qu'une autre méthode Pool fonctionnerait mieux pour vous - expérimentez.

Si vous ne pouvez pas bash le problème dans la vue du monde de Pool, vous pouvez à la place créer un mp.Queue Pour créer une file d'attente de travail, .put() 'noeuds (ou tranches) de nœuds, pour réduire la surcharge) sur laquelle travailler dans le programme principal, et écrivez les ouvriers dans les éléments de travail .get() hors de cette file d'attente. Demandez si vous avez besoin d'exemples. Notez que vous devez mettre des valeurs sentinelles (une par processus) dans la file d'attente, après tous les "vrais" éléments de travail, afin que les processus de travail puissent tester la sentinelle pour savoir quand ils ont terminé.

Pour info, j'aime les files d'attente car elles sont plus explicites. Beaucoup d'autres préfèrent Pool parce qu'ils sont plus magiques ;-)

Exemple de pool

Voici un prototype exécutable pour vous. Cela montre une façon d'utiliser imap_unordered Avec Pool et chunksize qui ne nécessite la modification d'aucune signature de fonction. Bien sûr, vous devrez brancher votre vrai code ;-) Notez que l'approche init_worker Permet de passer "la plupart" des arguments une seule fois par processeur, pas une fois pour chaque élément de votre degreeslist. La réduction de la quantité de communication inter-processus peut être cruciale pour la vitesse.

import multiprocessing as mp

def init_worker(mps, fps, cut):
    global memorizedPaths, filepaths, cutoff
    global DG

    print "process initializing", mp.current_process()
    memorizedPaths, filepaths, cutoff = mps, fps, cut
    DG = 1##nx.read_gml("KeggComplete.gml", relabel = True)

def work(item):
    _all_simple_paths_graph(DG, cutoff, item, memorizedPaths, filepaths)

def _all_simple_paths_graph(DG, cutoff, item, memorizedPaths, filepaths):
    pass # print "doing " + str(item)

if __name__ == "__main__":
    m = mp.Manager()
    memorizedPaths = m.dict()
    filepaths = m.dict()
    cutoff = 1 ##
    # use all available CPUs
    p = mp.Pool(initializer=init_worker, initargs=(memorizedPaths,
                                                   filepaths,
                                                   cutoff))
    degreelist = range(100000) ##
    for _ in p.imap_unordered(work, degreelist, chunksize=500):
        pass
    p.close()
    p.join()

Je conseille fortement de faire fonctionner cela exactement tel quel, donc vous pouvez voir que c'est très rapide. Ensuite, ajoutez-y un peu de temps pour voir comment cela affecte le temps. Par exemple, en ajoutant simplement

   memorizedPaths[item] = item

to _all_simple_paths_graph() le ralentit énormément. Pourquoi? Parce que le dict devient de plus en plus gros à chaque ajout, et ce dict sûr pour les processus doit être synchronisé (sous les couvertures) entre tous les processus. L'unité de synchronisation est "le dict entier" - il n'y a pas de structure interne que la machine mp puisse exploiter pour effectuer des mises à jour incrémentielles du dict partagé.

Si vous ne pouvez pas vous permettre ces dépenses, vous ne pouvez pas utiliser une Manager.dict() pour cela. Les possibilités d'intelligence ne manquent pas ;-)

45
Tim Peters