Usa l'array numpy nella memoria condivisa per il multiprocessing


111

Vorrei utilizzare un array numpy nella memoria condivisa da utilizzare con il modulo multiprocessing. La difficoltà è usarlo come un array numpy e non solo come array ctypes.

from multiprocessing import Process, Array
import scipy

def f(a):
    a[0] = -a[0]

if __name__ == '__main__':
    # Create the array
    N = int(10)
    unshared_arr = scipy.rand(N)
    arr = Array('d', unshared_arr)
    print "Originally, the first two elements of arr = %s"%(arr[:2])

    # Create, start, and finish the child processes
    p = Process(target=f, args=(arr,))
    p.start()
    p.join()

    # Printing out the changed values
    print "Now, the first two elements of arr = %s"%arr[:2]

Questo produce output come:

Originally, the first two elements of arr = [0.3518653236697369, 0.517794725524976]
Now, the first two elements of arr = [-0.3518653236697369, 0.517794725524976]

È possibile accedere all'array in un modo ctypes, ad esempio arr[i]ha senso. Tuttavia, non è un array numpy e non posso eseguire operazioni come -1*arr, o arr.sum(). Suppongo che una soluzione sarebbe convertire l'array ctypes in un array numpy. Tuttavia (oltre a non essere in grado di farlo funzionare), non credo che sarebbe più condiviso.

Sembra che ci sarebbe una soluzione standard a quello che deve essere un problema comune.


1
Non è lo stesso di questo? stackoverflow.com/questions/5033799/...
pygabriel

1
Non è proprio la stessa domanda. La domanda collegata sta chiedendo subprocesspiuttosto che multiprocessing.
Andrew

Risposte:


82

Da aggiungere alle risposte di @ unutbu (non più disponibile) e di @Henry Gomersall. È possibile utilizzare shared_arr.get_lock()per sincronizzare l'accesso quando necessario:

shared_arr = mp.Array(ctypes.c_double, N)
# ...
def f(i): # could be anything numpy accepts as an index such another numpy array
    with shared_arr.get_lock(): # synchronize access
        arr = np.frombuffer(shared_arr.get_obj()) # no data copying
        arr[i] = -arr[i]

Esempio

import ctypes
import logging
import multiprocessing as mp

from contextlib import closing

import numpy as np

info = mp.get_logger().info

def main():
    logger = mp.log_to_stderr()
    logger.setLevel(logging.INFO)

    # create shared array
    N, M = 100, 11
    shared_arr = mp.Array(ctypes.c_double, N)
    arr = tonumpyarray(shared_arr)

    # fill with random values
    arr[:] = np.random.uniform(size=N)
    arr_orig = arr.copy()

    # write to arr from different processes
    with closing(mp.Pool(initializer=init, initargs=(shared_arr,))) as p:
        # many processes access the same slice
        stop_f = N // 10
        p.map_async(f, [slice(stop_f)]*M)

        # many processes access different slices of the same array
        assert M % 2 # odd
        step = N // 10
        p.map_async(g, [slice(i, i + step) for i in range(stop_f, N, step)])
    p.join()
    assert np.allclose(((-1)**M)*tonumpyarray(shared_arr), arr_orig)

def init(shared_arr_):
    global shared_arr
    shared_arr = shared_arr_ # must be inherited, not passed as an argument

def tonumpyarray(mp_arr):
    return np.frombuffer(mp_arr.get_obj())

def f(i):
    """synchronized."""
    with shared_arr.get_lock(): # synchronize access
        g(i)

def g(i):
    """no synchronization."""
    info("start %s" % (i,))
    arr = tonumpyarray(shared_arr)
    arr[i] = -1 * arr[i]
    info("end   %s" % (i,))

if __name__ == '__main__':
    mp.freeze_support()
    main()

Se non hai bisogno dell'accesso sincronizzato o crei i tuoi lucchetti, mp.Array()non è necessario. Potresti usare mp.sharedctypes.RawArrayin questo caso.


2
Bella risposta! Se voglio avere più di un array condiviso, ciascuno bloccabile separatamente, ma con il numero di array determinato in fase di esecuzione, è una semplice estensione di ciò che hai fatto qui?
Andrew

3
@Andrew: gli array condivisi dovrebbero essere creati prima che vengano generati processi figlio.
jfs

Buon punto sull'ordine delle operazioni. Questo è quello che avevo in mente, però: creare un numero specificato dall'utente di array condivisi, quindi generare alcuni processi figlio. È semplice?
Andrew

1
@ Chicony: non è possibile modificare le dimensioni dell'array. Consideralo come un blocco di memoria condiviso che doveva essere allocato prima dell'avvio dei processi figlio. Non è necessario utilizzare tutta la memoria, ad esempio, potresti passare counta numpy.frombuffer(). Potresti provare a farlo a un livello inferiore usando mmapo qualcosa di simile posix_ipcdirettamente per implementare un analogo RawArray ridimensionabile (potrebbe comportare la copia durante il ridimensionamento) (o cercare una libreria esistente). Oppure, se il tuo compito lo consente: copia i dati in parti (se non ti servono tutti in una volta). "Come ridimensionare una memoria condivisa" è una buona domanda a parte.
jfs

1
@umopapisdn: Pool()definisce il numero di processi (il numero di core CPU disponibili è utilizzato per impostazione predefinita). Mè il numero di volte in cui la f()funzione viene chiamata.
jfs

21

L' Arrayoggetto ha un get_obj()metodo associato con esso, che restituisce la matrice ctypes che presenta un'interfaccia buffer. Penso che quanto segue dovrebbe funzionare ...

from multiprocessing import Process, Array
import scipy
import numpy

def f(a):
    a[0] = -a[0]

if __name__ == '__main__':
    # Create the array
    N = int(10)
    unshared_arr = scipy.rand(N)
    a = Array('d', unshared_arr)
    print "Originally, the first two elements of arr = %s"%(a[:2])

    # Create, start, and finish the child process
    p = Process(target=f, args=(a,))
    p.start()
    p.join()

    # Print out the changed values
    print "Now, the first two elements of arr = %s"%a[:2]

    b = numpy.frombuffer(a.get_obj())

    b[0] = 10.0
    print a[0]

Quando viene eseguito, viene stampato il primo elemento di anow 10.0, che mostra ae bsono solo due visualizzazioni nella stessa memoria.

Per assicurarti che sia ancora sicuro per multiprocessore, credo che dovrai usare i metodi acquiree releaseche esistono Arraysull'oggetto a, e il suo blocco integrato per assicurarti che sia tutto accessibile in modo sicuro (anche se non sono un esperto di modulo multiprocessore).


non funzionerà senza sincronizzazione come ha dimostrato @unutbu nella sua risposta (ora eliminata).
jfs

1
Presumibilmente, se si desidera solo accedere all'elaborazione successiva dell'array, è possibile eseguirla in modo pulito senza preoccuparsi di problemi di concorrenza e blocchi?
Henry Gomersall

in questo caso non serve mp.Array.
jfs

1
Il codice di elaborazione potrebbe richiedere array bloccati, ma l'interpretazione successiva all'elaborazione dei dati potrebbe non necessariamente. Immagino che questo derivi dalla comprensione di quale sia esattamente il problema. Chiaramente, l'accesso simultaneo ai dati condivisi richiederà una certa protezione, cosa che pensavo sarebbe stata ovvia!
Henry Gomersall

16

Sebbene le risposte già fornite siano buone, esiste una soluzione molto più semplice a questo problema a condizione che siano soddisfatte due condizioni:

  1. Sei su un sistema operativo conforme a POSIX (ad esempio Linux, Mac OSX); e
  2. I tuoi processi figli necessitano dell'accesso in sola lettura all'array condiviso.

In questo caso non è necessario giocherellare con la condivisione esplicita delle variabili, poiché i processi figli verranno creati utilizzando un fork. Un bambino biforcuto condivide automaticamente lo spazio di memoria del genitore. Nel contesto del multiprocessing di Python, questo significa che condivide tutte le variabili a livello di modulo ; nota che questo non vale per gli argomenti che passi esplicitamente ai tuoi processi figli o alle funzioni che chiami su uno multiprocessing.Poolo giù di lì.

Un semplice esempio:

import multiprocessing
import numpy as np

# will hold the (implicitly mem-shared) data
data_array = None

# child worker function
def job_handler(num):
    # built-in id() returns unique memory ID of a variable
    return id(data_array), np.sum(data_array)

def launch_jobs(data, num_jobs=5, num_worker=4):
    global data_array
    data_array = data

    pool = multiprocessing.Pool(num_worker)
    return pool.map(job_handler, range(num_jobs))

# create some random data and execute the child jobs
mem_ids, sumvals = zip(*launch_jobs(np.random.rand(10)))

# this will print 'True' on POSIX OS, since the data was shared
print(np.all(np.asarray(mem_ids) == id(data_array)))

3
+1 Informazioni davvero preziose. Puoi spiegare perché sono solo le variabili a livello di modulo che vengono condivise? Perché le variabili locali non fanno parte dello spazio di memoria del genitore? Ad esempio, perché non può funzionare se ho una funzione F con var locale V e una funzione G all'interno di F che fa riferimento a V?
Coffee_Table

5
Avvertenza: questa risposta è un po 'ingannevole. Il processo figlio riceve una copia dello stato del processo padre, comprese le variabili globali, al momento del fork. Gli stati non sono in alcun modo sincronizzati e divergeranno da quel momento. Questa tecnica può essere utile in alcuni scenari (ad esempio: biforcare processi figlio ad-hoc che gestiscono ciascuno un'istantanea del processo genitore e quindi terminano), ma è inutile in altri (ad esempio: processi figli di lunga durata che devono condividere e sincronizzare i dati con il processo genitore).
David Stein

4
@EelkeSpaak: La tua affermazione - "un bambino biforcuto condivide automaticamente lo spazio di memoria del genitore" - non è corretta. Se ho un processo figlio che vuole monitorare lo stato del processo genitore, in modo rigorosamente di sola lettura, il fork non mi porterà lì: il bambino vede solo un'istantanea dello stato genitore al momento del fork. In effetti, è proprio quello che stavo cercando di fare (seguendo la tua risposta) quando ho scoperto questa limitazione. Da qui il poscritto sulla tua risposta. In poche parole: lo stato genitore non è "condiviso", ma semplicemente copiato al bambino. Non è "condivisione" nel solito senso.
David Stein

2
Mi sbaglio a pensare che questa sia una situazione di copia su scrittura, almeno sui sistemi posix? Cioè, dopo il fork, penso che la memoria sia condivisa fino a quando non vengono scritti nuovi dati, a quel punto viene creata una copia. Quindi sì, è vero che i dati non sono esattamente "condivisi", ma possono fornire un potenziale enorme aumento delle prestazioni. Se il tuo processo è di sola lettura, non ci sarà alcun sovraccarico di copia! Ho capito bene il punto?
mittente

2
@senderle Sì, è esattamente quello che volevo dire! Da qui il mio punto (2) nella risposta sull'accesso in sola lettura.
EelkeSpaak

11

Ho scritto un piccolo modulo python che utilizza la memoria condivisa POSIX per condividere array numpy tra interpreti python. Forse lo troverai utile.

https://pypi.python.org/pypi/SharedArray

Ecco come funziona:

import numpy as np
import SharedArray as sa

# Create an array in shared memory
a = sa.create("test1", 10)

# Attach it as a different array. This can be done from another
# python interpreter as long as it runs on the same computer.
b = sa.attach("test1")

# See how they are actually sharing the same memory block
a[0] = 42
print(b[0])

# Destroying a does not affect b.
del a
print(b[0])

# See how "test1" is still present in shared memory even though we
# destroyed the array a.
sa.list()

# Now destroy the array "test1" from memory.
sa.delete("test1")

# The array b is not affected, but once you destroy it then the
# data are lost.
print(b[0])

8

Puoi usare il sharedmemmodulo: https://bitbucket.org/cleemesser/numpy-sharedmem

Ecco quindi il tuo codice originale, questa volta usando la memoria condivisa che si comporta come un array NumPy (nota l'ultima istruzione aggiuntiva che chiama una sum()funzione NumPy ):

from multiprocessing import Process
import sharedmem
import scipy

def f(a):
    a[0] = -a[0]

if __name__ == '__main__':
    # Create the array
    N = int(10)
    unshared_arr = scipy.rand(N)
    arr = sharedmem.empty(N)
    arr[:] = unshared_arr.copy()
    print "Originally, the first two elements of arr = %s"%(arr[:2])

    # Create, start, and finish the child process
    p = Process(target=f, args=(arr,))
    p.start()
    p.join()

    # Print out the changed values
    print "Now, the first two elements of arr = %s"%arr[:2]

    # Perform some NumPy operation
    print arr.sum()

1
Nota: questo non è più in fase di sviluppo e non sembra funzionare su linux github.com/sturlamolden/sharedmem-numpy/issues/4
AD

numpy-sharedmem potrebbe non essere in fase di sviluppo, ma funziona ancora su Linux, controlla github.com/vmlaker/benchmark-sharedmem .
Velimir Mlaker
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.