Oggetti a memoria condivisa in multiprocessing


124

Supponiamo di avere un grande array numpy in memoria, ho una funzione funcche accetta questo array gigante come input (insieme ad alcuni altri parametri). funccon diversi parametri possono essere eseguiti in parallelo. Per esempio:

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]

Se utilizzo la libreria multiprocessing, quell'array gigante verrà copiato più volte in processi diversi.

C'è un modo per consentire a processi diversi di condividere lo stesso array? Questo oggetto array è di sola lettura e non verrà mai modificato.

Cosa c'è di più complicato, se arr non è un array, ma un oggetto Python arbitrario, c'è un modo per condividerlo?

[MODIFICATO]

Ho letto la risposta ma sono ancora un po 'confuso. Poiché fork () è copy-on-write, non dovremmo invocare alcun costo aggiuntivo quando si generano nuovi processi nella libreria multiprocessing python. Ma il codice seguente suggerisce che c'è un enorme sovraccarico:

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;

output (e, a proposito, il costo aumenta con l'aumentare della dimensione dell'array, quindi sospetto che ci sia ancora un sovraccarico relativo alla copia della memoria):

construct array =  0.0178790092468
multiprocessing overhead =  0.252444982529

Perché c'è un sovraccarico così enorme, se non abbiamo copiato l'array? E quale parte mi salva la memoria condivisa?



Hai guardato i documenti , giusto?
Lev Levitsky,

@FrancisAvila c'è un modo per condividere non solo array, ma oggetti Python arbitrari?
Vendetta

1
@ LevLevitsky devo chiedere, c'è un modo per condividere non solo array, ma oggetti Python arbitrari?
Vendetta

2
Questa risposta spiega bene perché gli oggetti Python arbitrari non possono essere condivisi.
Janne Karila

Risposte:


121

Se utilizzi un sistema operativo che utilizza la fork()semantica copy-on-write (come qualsiasi unix comune), finché non modifichi mai la struttura dei dati, sarà disponibile per tutti i processi figli senza occupare memoria aggiuntiva. Non dovrai fare nulla di speciale (tranne assicurarti assolutamente di non alterare l'oggetto).

La cosa più efficiente che puoi fare per il tuo problema sarebbe comprimere il tuo array in una struttura di array efficiente (usando numpyo array), posizionarlo nella memoria condivisa, avvolgerlo multiprocessing.Arraye passarlo alle tue funzioni. Questa risposta mostra come farlo .

Se si desidera un oggetto condiviso scrivibile , sarà necessario includerlo in una sorta di sincronizzazione o blocco. multiprocessingfornisce due metodi per farlo : uno che utilizza la memoria condivisa (adatta a valori semplici, array o ctypes) o un Managerproxy, in cui un processo contiene la memoria e un manager arbitrerà l'accesso ad essa da altri processi (anche su una rete).

L' Managerapproccio può essere utilizzato con oggetti Python arbitrari, ma sarà più lento dell'equivalente utilizzando la memoria condivisa perché gli oggetti devono essere serializzati / deserializzati e inviati tra i processi.

In Python sono disponibili numerose librerie e approcci per l'elaborazione parallela . multiprocessingè un'ottima libreria a tutto tondo, ma se hai esigenze speciali forse uno degli altri approcci potrebbe essere migliore.


25
Solo per notare, in Python fork () significa effettivamente copia all'accesso (perché il solo accesso all'oggetto cambierà il suo ref-count).
Fabio Zadrozny

3
@FabioZadrozny Copierebbe effettivamente l'intero oggetto o solo la pagina di memoria contenente il suo refcount?
zigg

5
Per quanto ne so, solo la pagina di memoria contenente il refcount (quindi, 4kb su ogni accesso all'oggetto).
Fabio Zadrozny

1
@max Usa una chiusura. La funzione assegnata a apply_asyncdovrebbe fare riferimento all'oggetto condiviso nell'ambito direttamente invece che tramite i suoi argomenti.
Francis Avila

3
@FrancisAvila come usi una chiusura? La funzione che dai a apply_async non dovrebbe essere selezionabile? O questa è solo una restrizione map_async?
Tedesco K

17

Mi imbatto nello stesso problema e ho scritto una piccola classe di utilità della memoria condivisa per aggirarlo.

Sto usando multiprocessing.RawArray(lockfree), e anche l'accesso agli array non è affatto sincronizzato (lockfree), fai attenzione a non spararti ai piedi.

Con la soluzione ottengo accelerazioni di un fattore di circa 3 su un i7 quad-core.

Ecco il codice: sentiti libero di usarlo e migliorarlo e per favore segnala eventuali bug.

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

Mi sono appena reso conto che devi configurare i tuoi array di memoria condivisa prima di creare il pool multiprocessing, non so ancora perché ma sicuramente non funzionerà al contrario.
martin.preinfalk

il motivo è che il pool multiprocessing chiama fork () quando viene istanziato il pool, quindi qualsiasi cosa dopo non avrà accesso al puntatore a nessun mem condiviso creato in seguito.
Xiv

Quando ho provato questo codice sotto py35 ho ottenuto un'eccezione in multiprocessing.sharedctypes.py, quindi immagino che questo codice sia solo per py2.
Dottor Hillier Dániel

11

Questo è il caso d'uso previsto per Ray , che è una libreria per Python parallelo e distribuito. Sotto il cofano, serializza gli oggetti utilizzando il layout dei dati Apache Arrow (che è un formato a copia zero) e li memorizza in un archivio di oggetti a memoria condivisa in modo che possano essere accessibili da più processi senza creare copie.

Il codice sarebbe simile al seguente.

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)

Se non si chiama, ray.putl'array verrà comunque archiviato nella memoria condivisa, ma ciò verrà eseguito una volta per invocazione di func, che non è ciò che si desidera.

Si noti che questo funzionerà non solo per gli array ma anche per gli oggetti che contengono array , ad esempio, dizionari che mappano gli interi agli array come di seguito.

È possibile confrontare le prestazioni della serializzazione in Ray rispetto a pickle eseguendo quanto segue in 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 serializzazione con Ray è solo leggermente più veloce di pickle, ma la deserializzazione è 1000 volte più veloce a causa dell'uso della memoria condivisa (questo numero dipenderà ovviamente dall'oggetto).

Vedere la documentazione di Ray . Puoi leggere di più sulla serializzazione veloce usando Ray e Arrow . Nota sono uno degli sviluppatori Ray.


1
Ray suona bene! Ma ho già provato a usare questa libreria, ma sfortunatamente mi sono appena reso conto che Ray non supporta Windows. Spero che possiate supportare Windows il prima possibile. Grazie, sviluppatori!
Hzzkygcs

6

Come menzionato da Robert Nishihara, Apache Arrow lo rende facile, in particolare con l'archivio oggetti in memoria Plasma, che è ciò su cui è costruito Ray.

Ho creato plasma cerebrale appositamente per questo motivo: caricamento e ricarica rapidi di oggetti di grandi dimensioni in un'app Flask. È uno spazio dei nomi di oggetti a memoria condivisa per oggetti serializzabili con Apache Arrow, comprese picklele stringhe 'd bytestrings generate da pickle.dumps(...).

La differenza fondamentale con Apache Ray e Plasma è che tiene traccia degli ID oggetto per te. Qualsiasi processo o thread o programma in esecuzione localmente può condividere i valori delle variabili chiamando il nome da qualsiasi Brainoggetto.

$ 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,...]
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.