web-dev-qa-db-fra.com

Objets à mémoire partagée en multitraitement

Supposons que j'ai un grand tableau numpy en mémoire, j'ai une fonction func qui prend en entrée ce tableau géant (avec quelques autres paramètres). func avec différents paramètres peut être exécuté en parallèle. Par exemple:

def func(arr, param):
    # do stuff to arr, param

# build array arr

pool = Pool(processes = 6)
results = [pool.apply_async(func, [arr, param]) for param in all_params]
output = [res.get() for res in results]

Si j'utilise une bibliothèque multitraitement, alors ce tableau géant sera copié plusieurs fois dans différents processus. 

Existe-t-il un moyen de laisser différents processus partager le même tableau? Cet objet de tableau est en lecture seule et ne sera jamais modifié. 

Quoi de plus compliqué, si arr n'est pas un tableau, mais un objet python arbitraire, y a-t-il un moyen de le partager? 

[ÉDITÉ]

J'ai lu la réponse mais je suis encore un peu confus. Fork () étant une copie sur écriture, nous ne devrions invoquer aucun coût supplémentaire lors de la création de nouveaux processus dans la bibliothèque de multitraitement python. Mais le code suivant suggère qu'il y a une énorme surcharge: 

from multiprocessing import Pool, Manager
import numpy as np; 
import time

def f(arr):
    return len(arr)

t = time.time()
arr = np.arange(10000000)
print "construct array = ", time.time() - t;


pool = Pool(processes = 6)

t = time.time()
res = pool.apply_async(f, [arr,])
res.get()
print "multiprocessing overhead = ", time.time() - t;

sortie (et d'ailleurs, le coût augmente à mesure que la taille de la matrice augmente, donc je suppose qu'il y a toujours une surcharge liée à la copie en mémoire): 

construct array =  0.0178790092468
multiprocessing overhead =  0.252444982529

Pourquoi existe-t-il une telle surcharge si nous ne copions pas le tableau? Et quelle partie la mémoire partagée me sauve-t-elle? 

89
CodeNoob

Si vous utilisez un système d'exploitation qui utilise la sémantique fork() de copie à l'écriture (comme tout système Unix commun), tant que vous ne modifiez jamais votre structure de données, elle sera disponible pour tous les processus enfants sans utiliser de mémoire supplémentaire. Vous n'aurez rien de spécial à faire (sauf assurez-vous de ne pas modifier l'objet).

La chose la plus efficace que vous pouvez faire pour votre problème serait de compresser votre tableau dans un tableau efficace structure (avec numpyou arrayNAME _ ), placez-le dans la mémoire partagée, encapsulez-le avec multiprocessing.Array et transmettez-le à vos fonctions. Cette réponse montre comment faire cela .

Si vous voulez un objet partagé accessible en écriture , vous devrez alors l'envelopper avec une sorte de synchronisation ou de verrouillage. multiprocessingfournit deux méthodes pour ce faire : l'un utilise la mémoire partagée (convient pour les valeurs simples, les tableaux ou les ctypes) ou un proxy Managername__, où un processus détient la mémoire et un gestionnaire en arbitre l'accès. processus (même sur un réseau).

L'approche Managerpeut être utilisée avec des objets Python arbitraires, mais sera plus lente que son équivalent en utilisant la mémoire partagée car les objets doivent être sérialisés/désérialisés et envoyés entre processus.

Il existe un richesse de bibliothèques et d’approches de traitement parallèle disponibles en Python . multiprocessingest une bibliothèque excellente et complète, mais si vous avez des besoins particuliers, une des autres approches est peut-être meilleure.

105
Francis Avila

Je rencontre le même problème et ai écrit une petite classe d’utilitaires à mémoire partagée pour la contourner.

J'utilise le multitraitement.RawArray (lockfree), et l'accès aux tableaux n'est pas du tout synchronisé (lockfree), veillez à ne pas vous prendre à vous-même.

Avec la solution, les accélérations sont multipliées par 3 sur un i7 quad-core.

Voici le code: N'hésitez pas à l'utiliser et à l'améliorer, et s'il vous plait, rapportez tout bogue.

'''
Created on 14.05.2013

@author: martin
'''

import multiprocessing
import ctypes
import numpy as np

class SharedNumpyMemManagerError(Exception):
    pass

'''
Singleton Pattern
'''
class SharedNumpyMemManager:    

    _initSize = 1024

    _instance = None

    def __new__(cls, *args, **kwargs):
        if not cls._instance:
            cls._instance = super(SharedNumpyMemManager, cls).__new__(
                                cls, *args, **kwargs)
        return cls._instance        

    def __init__(self):
        self.lock = multiprocessing.Lock()
        self.cur = 0
        self.cnt = 0
        self.shared_arrays = [None] * SharedNumpyMemManager._initSize

    def __createArray(self, dimensions, ctype=ctypes.c_double):

        self.lock.acquire()

        # double size if necessary
        if (self.cnt >= len(self.shared_arrays)):
            self.shared_arrays = self.shared_arrays + [None] * len(self.shared_arrays)

        # next handle
        self.__getNextFreeHdl()        

        # create array in shared memory segment
        shared_array_base = multiprocessing.RawArray(ctype, np.prod(dimensions))

        # convert to numpy array vie ctypeslib
        self.shared_arrays[self.cur] = np.ctypeslib.as_array(shared_array_base)

        # do a reshape for correct dimensions            
        # Returns a masked array containing the same data, but with a new shape.
        # The result is a view on the original array
        self.shared_arrays[self.cur] = self.shared_arrays[self.cnt].reshape(dimensions)

        # update cnt
        self.cnt += 1

        self.lock.release()

        # return handle to the shared memory numpy array
        return self.cur

    def __getNextFreeHdl(self):
        orgCur = self.cur
        while self.shared_arrays[self.cur] is not None:
            self.cur = (self.cur + 1) % len(self.shared_arrays)
            if orgCur == self.cur:
                raise SharedNumpyMemManagerError('Max Number of Shared Numpy Arrays Exceeded!')

    def __freeArray(self, hdl):
        self.lock.acquire()
        # set reference to None
        if self.shared_arrays[hdl] is not None: # consider multiple calls to free
            self.shared_arrays[hdl] = None
            self.cnt -= 1
        self.lock.release()

    def __getArray(self, i):
        return self.shared_arrays[i]

    @staticmethod
    def getInstance():
        if not SharedNumpyMemManager._instance:
            SharedNumpyMemManager._instance = SharedNumpyMemManager()
        return SharedNumpyMemManager._instance

    @staticmethod
    def createArray(*args, **kwargs):
        return SharedNumpyMemManager.getInstance().__createArray(*args, **kwargs)

    @staticmethod
    def getArray(*args, **kwargs):
        return SharedNumpyMemManager.getInstance().__getArray(*args, **kwargs)

    @staticmethod    
    def freeArray(*args, **kwargs):
        return SharedNumpyMemManager.getInstance().__freeArray(*args, **kwargs)

# Init Singleton on module load
SharedNumpyMemManager.getInstance()

if __== '__main__':

    import timeit

    N_PROC = 8
    INNER_LOOP = 10000
    N = 1000

    def propagate(t):
        i, shm_hdl, evidence = t
        a = SharedNumpyMemManager.getArray(shm_hdl)
        for j in range(INNER_LOOP):
            a[i] = i

    class Parallel_Dummy_PF:

        def __init__(self, N):
            self.N = N
            self.arrayHdl = SharedNumpyMemManager.createArray(self.N, ctype=ctypes.c_double)            
            self.pool = multiprocessing.Pool(processes=N_PROC)

        def update_par(self, evidence):
            self.pool.map(propagate, Zip(range(self.N), [self.arrayHdl] * self.N, [evidence] * self.N))

        def update_seq(self, evidence):
            for i in range(self.N):
                propagate((i, self.arrayHdl, evidence))

        def getArray(self):
            return SharedNumpyMemManager.getArray(self.arrayHdl)

    def parallelExec():
        pf = Parallel_Dummy_PF(N)
        print(pf.getArray())
        pf.update_par(5)
        print(pf.getArray())

    def sequentialExec():
        pf = Parallel_Dummy_PF(N)
        print(pf.getArray())
        pf.update_seq(5)
        print(pf.getArray())

    t1 = timeit.Timer("sequentialExec()", "from __main__ import sequentialExec")
    t2 = timeit.Timer("parallelExec()", "from __main__ import parallelExec")

    print("Sequential: ", t1.timeit(number=1))    
    print("Parallel: ", t2.timeit(number=1))
14
martin.preinfalk

Comme Robert Nishihara l'a mentionné, Apache Arrow simplifie cela, en particulier avec le magasin d'objets Plasma en mémoire, sur lequel Ray est construit.

J'ai créé brain-plasma spécifiquement pour cette raison - chargement et rechargement rapides de gros objets dans une application Flask. Il s'agit d'un espace de nom d'objet à mémoire partagée pour les objets sérialisables Apache Arrow, y compris pickle 'd bytestrings générés par pickle.dumps (...).

La différence essentielle avec Apache Ray et Plasma est qu’elle garde la trace des ID d’objet pour vous. Tous les processus, threads ou programmes s'exécutant localement peuvent partager les valeurs des variables en appelant le nom à partir de tout objet brain.

$ pip install brain-plasma
$ plasma_store -m 10000000 -s /tmp/plasma

from brain_plasma import Brain
brain = Brain(path='/tmp/plasma/)

brain['a'] = [1]*10000

brain['a']
# >>> [1,1,1,1,...]
1
russellthehippo

Ceci est le cas d'utilisation prévu de Ray , qui est une bibliothèque pour Python parallèle et distribué. Sous le capot, il sérialise les objets à l’aide de la structure de données Apache Arrow (format zéro copie) et les stocke dans un magasin d’objets à mémoire partagée afin que plusieurs processus puissent y accéder sans créer copies.

Le code ressemblerait à ceci.

import numpy as np
import ray

ray.init()

@ray.remote
def func(array, param):
    # Do stuff.
    return 1

array = np.ones(10**6)
# Store the array in the shared memory object store once
# so it is not copied multiple times.
array_id = ray.put(array)

result_ids = [func.remote(array_id, i) for i in range(4)]
output = ray.get(result_ids)

Si vous n'appelez pas ray.put, le tableau sera toujours stocké dans la mémoire partagée, mais cela sera fait une fois par invocation de func, ce qui n'est pas ce que vous voulez.

Notez que cela fonctionnera non seulement pour les tableaux, mais aussi pour les {également pour les objets contenant des tableaux}, par exemple, les dictionnaires mappant les entrées sur des tableaux comme ci-dessous.

Vous pouvez comparer les performances de la sérialisation dans Ray versus pickle en exécutant ce qui suit dans IPython.

import numpy as np
import pickle
import ray

ray.init()

x = {i: np.ones(10**7) for i in range(20)}

# Time Ray.
%time x_id = ray.put(x)  # 2.4s
%time new_x = ray.get(x_id)  # 0.00073s

# Time pickle.
%time serialized = pickle.dumps(x)  # 2.6s
%time deserialized = pickle.loads(serialized)  # 1.9s

La sérialisation avec Ray n'est que légèrement plus rapide que le pickle, mais la désérialisation est 1000 fois plus rapide en raison de l'utilisation de la mémoire partagée (ce nombre dépendra bien sûr de l'objet).

Voir la documentation Ray . Vous pouvez en savoir plus sur la sérialisation rapide en utilisant Ray et Arrow . Notez que je suis l'un des développeurs de Ray.

0
Robert Nishihara