Shared-Memory-Objekte in Multiprocessing

123

Angenommen, ich habe ein großes Speicher-Numpy-Array. Ich habe eine Funktion func, die dieses riesige Array als Eingabe verwendet (zusammen mit einigen anderen Parametern). funcmit verschiedenen Parametern kann parallel ausgeführt werden. Beispielsweise:

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 Multiprozessor-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, sondern ein beliebiges Python-Objekt ist, gibt es eine Möglichkeit, es zu teilen?

[BEARBEITET]

Ich habe die Antwort gelesen, bin aber immer noch etwas verwirrt. Da fork () Copy-on-Write ist, sollten wir keine zusätzlichen Kosten verursachen, wenn wir neue Prozesse in der Python-Multiprocessing-Bibliothek erzeugen. Der folgende Code deutet jedoch auf einen enormen Overhead hin:

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, sodass ich vermute, dass beim Kopieren des Speichers immer noch Overhead besteht):

construct array =  0.0178790092468
multiprocessing overhead =  0.252444982529

Warum ist der Aufwand so groß, wenn wir das Array nicht kopiert haben? Und welchen Teil rettet mich der gemeinsame Speicher?

Vendetta
quelle
Sie haben sich die Dokumente angesehen , richtig?
Lev Levitsky
@FrancisAvila gibt es eine Möglichkeit, nicht nur Array-, sondern beliebige Python-Objekte gemeinsam zu nutzen?
Vendetta
1
@LevLevitsky Ich muss fragen, gibt es eine Möglichkeit, nicht nur Arrays, sondern beliebige Python-Objekte zu teilen?
Vendetta
2
Diese Antwort erklärt gut, warum beliebige Python-Objekte nicht gemeinsam genutzt werden können.
Janne Karila

Antworten:

121

Wenn Sie ein Betriebssystem verwenden, das Copy-on-Write- fork()Semantik verwendet (wie jedes gängige Unix), steht es allen untergeordneten Prozessen zur Verfügung, solange Sie Ihre Datenstruktur nicht ändern, ohne zusätzlichen Speicherplatz zu beanspruchen. Sie müssen nichts Besonderes tun (außer unbedingt sicherstellen, dass Sie das Objekt nicht verändern).

Das effizienteste, was Sie für Ihr Problem tun können, ist, Ihr Array in eine effiziente Array-Struktur (mit numpyoder array) zu packen , diese in den gemeinsam genutzten Speicher zu legen, sie zu verpacken multiprocessing.Arrayund an Ihre Funktionen zu übergeben. Diese Antwort zeigt, wie das geht .

Wenn Sie einen wollen beschreibbaren gemeinsam genutztes Objekt, dann müssen Sie es mit irgendeiner Art von Synchronisation oder Verriegelung wickeln. multiprocessingHierfür stehen zwei Methoden zur Verfügung : eine mit gemeinsam genutztem Speicher (geeignet für einfache Werte, Arrays oder c-Typen) oder ein ManagerProxy, bei dem ein Prozess den Speicher enthält und ein Manager den Zugriff von anderen Prozessen (auch über ein Netzwerk) auf ihn schaltet .

Der ManagerAnsatz kann mit beliebigen Python-Objekten verwendet werden, ist jedoch bei Verwendung des gemeinsam genutzten Speichers langsamer als der entsprechende Ansatz, da die Objekte serialisiert / deserialisiert und zwischen Prozessen gesendet werden müssen.

Es gibt eine Fülle von parallelen Verarbeitungsbibliotheken und Ansätze in Python zur Verfügung . multiprocessingist eine ausgezeichnete und abgerundete Bibliothek, aber wenn Sie spezielle Bedürfnisse haben, ist vielleicht einer der anderen Ansätze besser.

Francis Avila
quelle
25
Zu beachten ist, dass auf Python fork () beim Zugriff tatsächlich kopiert wird (da nur der Zugriff auf das Objekt die Ref-Anzahl ändert).
Fabio Zadrozny
3
@FabioZadrozny Würde es tatsächlich das gesamte Objekt oder nur die Speicherseite kopieren, die die Refcount enthält?
Zigg
5
AFAIK, nur die Speicherseite, die den Refcount enthält (also 4 KB bei jedem Objektzugriff).
Fabio Zadrozny
1
@max Verwenden Sie einen Verschluss. Die gegebene Funktion apply_asyncsollte auf das gemeinsam genutzte Objekt im Gültigkeitsbereich direkt anstatt über seine Argumente verweisen.
Francis Avila
3
@FrancisAvila Wie benutzt man einen Verschluss? Sollte die Funktion, die Sie apply_async geben, nicht auswählbar sein? Oder ist dies nur eine map_async-Einschränkung?
GermanK
17

Ich bin auf dasselbe Problem gestoßen und habe eine kleine Dienstprogrammklasse für gemeinsam genutzten Speicher geschrieben, 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, dass Sie nicht auf Ihre eigenen Füße schießen.

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

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

'''
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 __name__ == '__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))
martin.preinfalk
quelle
Ich habe gerade festgestellt, dass Sie Ihre Shared-Memory-Arrays einrichten müssen, bevor Sie den Multiprozessor-Pool erstellen. Ich weiß noch nicht warum, aber es funktioniert definitiv nicht umgekehrt.
martin.preinfalk
Der Grund dafür ist, dass der Multiprocessing-Pool beim Instanziieren des Pools fork () aufruft, sodass danach nichts mehr auf den Zeiger auf ein später erstelltes freigegebenes Mem zugreifen kann.
Xiv
Als ich diesen Code unter py35 ausprobierte, bekam ich eine Ausnahme in multiprocessing.sharedctypes.py, also denke ich, dass dieser Code nur für py2 ist.
Dr. Hillier Dániel
11

Dies ist der beabsichtigte Anwendungsfall für Ray , eine Bibliothek für paralleles und verteiltes Python. Unter der Haube werden Objekte mithilfe des Apache Arrow -Datenlayouts (im Nullkopieformat) serialisiert und in einem Objektspeicher mit gemeinsamem Speicher gespeichert, sodass mehrere Prozesse auf sie zugreifen können, ohne Kopien zu erstellen.

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 aufrufen, ray.putwird das Array weiterhin im gemeinsam genutzten Speicher gespeichert. Dies erfolgt jedoch einmal pro Aufruf von func, was nicht das ist, was Sie möchten.

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

Sie können die Leistung der Serialisierung in Ray mit der von 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 mit Pickle, aber die Deserialisierung ist aufgrund der Verwendung des gemeinsam genutzten Speichers 1000-mal schneller (diese Anzahl hängt natürlich vom Objekt ab).

Siehe die Ray-Dokumentation . Weitere Informationen zur schnellen Serialisierung mit Ray und Arrow finden Sie hier . Hinweis: Ich bin einer der Ray-Entwickler.

Robert Nishihara
quelle
1
Ray hört sich gut an! Ich habe bereits versucht, diese Bibliothek zu verwenden, aber leider habe ich gerade festgestellt, dass Ray Windows nicht unterstützt. Ich hoffe ihr könnt Windows so schnell wie möglich unterstützen. Vielen Dank, Entwickler!
Hzzkygcs
5

Wie Robert Nishihara bereits erwähnt hat, macht Apache Arrow dies einfach, insbesondere mit dem Plasma-In-Memory-Objektspeicher, auf dem Ray basiert.

Ich habe Gehirn-Plasma speziell aus diesem Grunde - schnellen Laden und in einer Flasche App von großen Objekten neu zu laden. Es ist ein Objekt-Namespace mit gemeinsamem Speicher für Apache Arrow-serialisierbare Objekte, einschließlich pickle'd Bytestrings, die von generiert wurden pickle.dumps(...).

Der Hauptunterschied zu Apache Ray und Plasma besteht darin, dass die Objekt-IDs für Sie erfasst werden. Alle Prozesse, Threads oder Programme, die lokal ausgeführt werden, können die Werte der Variablen gemeinsam nutzen, indem sie den Namen eines beliebigen BrainObjekts 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,...]
russellthehippo
quelle