it-swarm.com.de

Shared-Memory-Objekte im Multiprocessing

Angenommen, ich habe ein großes numpy-Array im Speicher, ich habe eine Funktion func, die dieses riesige Array (zusammen mit einigen anderen Parametern) als Eingabe akzeptiert. func mit verschiedenen Parametern kann parallel ausgeführt werden. Zum Beispiel:

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]

Wenn ich eine Multiprocessing-Bibliothek verwende, wird dieses riesige Array mehrmals in verschiedene Prozesse kopiert. 

Gibt es eine Möglichkeit, verschiedene Prozesse dasselbe Array gemeinsam nutzen zu lassen? Dieses Array-Objekt ist schreibgeschützt und wird niemals geändert. 

Was ist komplizierter, wenn arr kein Array ist, sondern ein beliebiges Python-Objekt. Gibt es eine Möglichkeit, es zu teilen? 

[BEARBEITET]

Ich lese die Antwort, bin aber immer noch etwas verwirrt. Da fork () Copy-on-Write ist, sollten Sie keine zusätzlichen Kosten verursachen, wenn Sie neue Prozesse in der Python-Multiprocessing-Bibliothek starten. Der folgende Code legt jedoch nahe, dass es einen erheblichen Aufwand gibt: 

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;

ausgabe (und im Übrigen steigen die Kosten mit zunehmender Größe des Arrays, daher vermute ich, dass immer noch ein zusätzlicher Aufwand für das Kopieren von Speicherkarten besteht) 

construct array =  0.0178790092468
multiprocessing overhead =  0.252444982529

Warum gibt es so viel Aufwand, wenn wir das Array nicht kopieren? Und welchen Teil rettet mich das Shared Memory? 

89
CodeNoob

Wenn Sie ein Betriebssystem verwenden, das die Copy-on-Write-Funktion fork() verwendet (wie bei einem gewöhnlichen Unix), können Sie Ihre Datenstruktur nicht für alle untergeordneten Prozesse verwenden, ohne zusätzlichen Speicherplatz zu beanspruchen. Sie müssen nichts Besonderes tun (außer stellen Sie sicher, dass Sie das Objekt nicht verändern).

Das effizienteste, was Sie für Ihr Problem tun können, ist das Packen Ihres Arrays in eine effiziente Array-Struktur (mithilfe von numpy oder array ). Platzieren Sie dieses in den gemeinsam genutzten Speicher, umschließen Sie es mit multiprocessing.Array und übergeben Sie dies an Ihre Funktionen. Diese Antwort zeigt, wie das geht .

Wenn Sie ein gemeinsam genutztes Objekt writeable möchten, müssen Sie es mit einer Art Synchronisierung oder Sperrung umschließen. multiprocessing bietet zwei Methoden, um dies zu tun }: eine, die Shared Memory (geeignet für einfache Werte, Arrays oder ctypes) oder einen Manager-Proxy verwendet, wobei ein Prozess den Speicher hält und ein Manager den Zugriff von anderen Prozessen auf ihn regelt (auch über ein Netzwerk).

Die Manager-Methode kann mit beliebigen Python-Objekten verwendet werden, ist jedoch langsamer als die Verwendung von Shared Memory, da die Objekte serialisiert/deserialisiert und zwischen Prozessen gesendet werden müssen.

Es gibt eine eine Fülle von parallelen Verarbeitungsbibliotheken und -ansätzen in Python . multiprocessing ist eine exzellente und gut abgerundete Bibliothek, aber wenn Sie spezielle Bedürfnisse haben, ist möglicherweise einer der anderen Ansätze besser.

91
Francis Avila

Ich stieß auf das gleiche Problem und schrieb eine kleine Shared-Memory-Utility-Klasse, um das Problem zu umgehen.

Ich verwende Multiprocessing.RawArray (lockfree), und auch der Zugriff auf die Arrays ist überhaupt nicht synchronisiert (lockfree). Achten Sie darauf, nicht mit den eigenen Füßen zu schießen.

Mit der Lösung bekomme ich auf einem Quad-Core-i7 um den Faktor 3 beschleunigt.

Hier ist der Code: Fühlen Sie sich frei, ihn zu verwenden und zu verbessern, und melden Sie uns bitte alle Fehler.

'''
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

Wie bereits von Robert Nishihara erwähnt, macht es Apache Arrow einfach, insbesondere mit dem In-Memory-Objektspeicher Plasma, auf dem Ray aufbaut.

Ich habe brain-plasma speziell aus diesem Grund erstellt - schnelles Laden und Neuladen von großen Objekten in einer Flask App. Es handelt sich um einen Shared-Memory-Objektnamespace für Apache Arrow-serialisierbare Objekte, einschließlich pickle von pickle.dumps (...) generierter Bytestrings.

Der Hauptunterschied zu Apache Ray und Plasma besteht darin, dass die Objekt-IDs für Sie protokolliert werden. Alle Prozesse, Threads oder Programme, die lokal ausgeführt werden, können die Werte der Variablen gemeinsam nutzen, indem sie den Namen von einem beliebigen brain -Objekt aufrufen.

$ 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

Dies ist der beabsichtigte Anwendungsfall für Ray , eine Bibliothek für paralleles und verteiltes Python. Unter der Haube serialisiert es Objekte mit dem Datenlayout Apache Arrow (das ein Zero-Copy-Format ist) und speichert sie in einem Shared-Memory-Objektspeicher , sodass sie ohne Erstellung von mehreren Prozessen darauf zugreifen können Kopien.

Der Code würde wie folgt aussehen.

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)

Wenn Sie nicht ray.put aufrufen, wird das Array immer noch im gemeinsam genutzten Speicher gespeichert. Dies geschieht jedoch einmal pro Aufruf von func, was Sie nicht möchten.

Beachten Sie, dass dies nicht nur für Arrays funktioniert, sondern auch auch für Objekte, die Arrays enthalten, z. B. Wörterbücher, die wie unten dargestellt Arrays zu Arrays zuordnen.

Sie können die Leistung der Serialisierung in Ray im Vergleich zu Pickle vergleichen, indem Sie in IPython Folgendes ausführen.

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

Die Serialisierung mit Ray ist nur geringfügig schneller als die Pickle-Funktion, aber die Deserialisierung ist aufgrund der Verwendung von Shared Memory um das 1000-fache schneller (diese Anzahl hängt natürlich vom Objekt ab).

Siehe Ray-Dokumentation . Sie können mehr über schnelle Serialisierung mit Ray und Pfeil lesen. Hinweis Ich bin einer der Ray-Entwickler.

0