Utilisez numpy array dans la mémoire partagée pour le multitraitement

111

Je voudrais utiliser un tableau numpy en mémoire partagée pour une utilisation avec le module multitraitement. La difficulté est de l'utiliser comme un tableau numpy, et pas seulement comme un tableau 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]

Cela produit une sortie telle que:

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

Le tableau peut être accédé de manière ctypes, par exemple a du arr[i]sens. Cependant, ce n'est pas un tableau numpy et je ne peux pas effectuer d'opérations telles que -1*arr, ou arr.sum(). Je suppose qu'une solution serait de convertir le tableau ctypes en un tableau numpy. Cependant (en plus de ne pas pouvoir faire ce travail), je ne crois pas que ce serait plus partagé.

Il semble qu'il y aurait une solution standard à ce qui doit être un problème commun.

Ian Langmore
la source
1
Ce n'est pas la même chose que celle-là? stackoverflow.com/questions/5033799/…
pygabriel
1
Ce n'est pas tout à fait la même question. La question liée concerne subprocessplutôt que multiprocessing.
Andrew

Réponses:

82

À ajouter aux réponses de @ unutbu (plus disponible) et de @Henry Gomersall. Vous pouvez utiliser shared_arr.get_lock()pour synchroniser l'accès en cas de besoin:

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]

Exemple

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

Si vous n'avez pas besoin d'un accès synchronisé ou si vous créez vos propres verrous, ce mp.Array()n'est pas nécessaire. Vous pouvez utiliser mp.sharedctypes.RawArraydans ce cas.

jfs
la source
2
Belle réponse! Si je veux avoir plus d'un tableau partagé, chacun verrouillable séparément, mais avec le nombre de tableaux déterminé au moment de l'exécution, est-ce une extension simple de ce que vous avez fait ici?
Andrew
3
@Andrew: les tableaux partagés doivent être créés avant que les processus enfants ne soient générés.
jfs
Bon point sur l'ordre des opérations. C'est ce que j'avais à l'esprit, cependant: créer un nombre spécifié par l'utilisateur de tableaux partagés, puis générer quelques processus enfants. Est-ce simple?
Andrew
1
@Chicony: vous ne pouvez pas modifier la taille du tableau. Considérez-le comme un bloc de mémoire partagé qui devait être alloué avant le démarrage des processus enfants. Vous n'avez pas besoin d'utiliser toute la mémoire par exemple, vous pouvez passer countà numpy.frombuffer(). Vous pouvez essayer de le faire à un niveau inférieur en utilisant mmapou quelque chose comme posix_ipcdirectement pour implémenter un analogique RawArray redimensionnable (peut impliquer une copie lors du redimensionnement) (ou rechercher une bibliothèque existante). Ou si votre tâche le permet: copiez les données par parties (si vous n'en avez pas besoin en même temps). "Comment redimensionner une mémoire partagée" est une bonne question distincte.
jfs
1
@umopapisdn: Pool()définit le nombre de processus (le nombre de cœurs CPU disponibles est utilisé par défaut). Mest le nombre de fois que la f()fonction est appelée.
jfs
21

L' Arrayobjet a une get_obj()méthode qui lui est associée, qui retourne le tableau ctypes qui présente une interface de tampon. Je pense que ce qui suit devrait fonctionner ...

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]

Lorsqu'il est exécuté, cela imprime le premier élément d' aêtre maintenant 10.0, montrant aet bsont juste deux vues dans la même mémoire.

Afin de vous assurer qu'il est toujours sécurisé pour le multiprocesseur, je pense que vous devrez utiliser les méthodes acquireet releasequi existent sur l' Arrayobjet a, et son verrou intégré pour vous assurer que tout est accessible en toute sécurité (bien que je ne sois pas un expert sur le module multiprocesseur).

Henry Gomersall
la source
cela ne fonctionnera pas sans synchronisation comme @unutbu l'a démontré dans sa réponse (maintenant supprimée).
jfs
1
Vraisemblablement, si vous vouliez simplement accéder au post-traitement du tableau, cela peut être fait proprement sans vous soucier des problèmes de concurrence et de verrouillage?
Henry Gomersall
dans ce cas, vous n'avez pas besoin mp.Array.
jfs
1
Le code de traitement peut nécessiter des tableaux verrouillés, mais l'interprétation post-traitement des données peut ne pas nécessairement l'être. Je suppose que cela vient de comprendre quel est exactement le problème. De toute évidence, accéder simultanément aux données partagées nécessitera une certaine protection, ce que je pensais être évident!
Henry Gomersall
16

Bien que les réponses déjà données soient bonnes, il existe une solution beaucoup plus simple à ce problème à condition que deux conditions soient remplies:

  1. Vous êtes sur un système d'exploitation compatible POSIX (par exemple Linux, Mac OSX); et
  2. Vos processus enfants ont besoin d' un accès en lecture seule au tableau partagé.

Dans ce cas, vous n'avez pas besoin de jouer avec le partage explicite des variables, car les processus enfants seront créés à l'aide d'un fork. Un enfant forké partage automatiquement l'espace mémoire du parent. Dans le contexte du multitraitement Python, cela signifie qu'il partage toutes les variables au niveau du module ; notez que cela ne vaut pas pour les arguments que vous passez explicitement à vos processus enfants ou aux fonctions que vous appelez sur un multiprocessing.Poolou deux.

Un exemple simple:

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)))
EelkeSpaak
la source
3
+1 Informations vraiment précieuses. Pouvez-vous expliquer pourquoi seuls les variables au niveau du module sont partagées? Pourquoi les variables locales ne font-elles pas partie de l'espace mémoire du parent? Par exemple, pourquoi cela ne peut-il pas fonctionner si j'ai une fonction F avec une var locale V et une fonction G à l'intérieur de F qui fait référence à V?
Coffee_Table
5
Attention: cette réponse est un peu trompeuse. Le processus enfant reçoit une copie de l'état du processus parent, y compris les variables globales, au moment du fork. Les états ne sont en aucun cas synchronisés et divergeront à partir de ce moment. Cette technique peut être utile dans certains scénarios (par exemple: bifurquer des processus enfants ad hoc qui gèrent chacun un instantané du processus parent puis se terminent), mais est inutile dans d'autres (par exemple: processus enfants de longue durée qui doivent partager et synchroniser les données avec le processus parent).
David Stein
4
@EelkeSpaak: Votre déclaration - "un enfant fourchu partage automatiquement l'espace mémoire du parent" - est incorrecte. Si j'ai un processus enfant qui souhaite surveiller l'état du processus parent, de manière strictement en lecture seule, le forking ne m'y amènera pas: l'enfant ne voit qu'un instantané de l'état parent au moment du forking. En fait, c'est précisément ce que j'essayais de faire (suite à votre réponse) lorsque j'ai découvert cette limitation. D'où le post-scriptum de votre réponse. En un mot: l'état parent n'est pas «partagé», mais simplement copié sur l'enfant. Ce n'est pas du «partage» au sens habituel.
David Stein
2
Ai-je tort de penser qu'il s'agit d'une situation de copie sur écriture, du moins sur les systèmes posix? Autrement dit, après la fourchette, je pense que la mémoire est partagée jusqu'à ce que de nouvelles données soient écrites, moment auquel une copie est créée. Alors oui, il est vrai que les données ne sont pas exactement «partagées», mais elles peuvent fournir une augmentation potentiellement énorme des performances. Si votre processus est en lecture seule, il n'y aura pas de surcharge de copie! Ai-je bien compris le point?
senderle
2
@senderle Oui, c'est exactement ce que je voulais dire! D'où mon point (2) dans la réponse sur l'accès en lecture seule.
EelkeSpaak
11

J'ai écrit un petit module python qui utilise la mémoire partagée POSIX pour partager des tableaux numpy entre des interpréteurs python. Peut-être que vous le trouverez pratique.

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

Voici comment ça fonctionne:

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])
tapis
la source
8

Vous pouvez utiliser le sharedmemmodule: https://bitbucket.org/cleemesser/numpy-sharedmem

Voici donc votre code d'origine, cette fois en utilisant la mémoire partagée qui se comporte comme un tableau NumPy (notez la dernière instruction supplémentaire appelant une sum()fonction 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()
Velimir Mlaker
la source
1
Remarque: ceci n'est plus en cours de développement et ne semble pas fonctionner sur linux github.com/sturlamolden/sharedmem-numpy/issues/4
AD
numpy-sharedmem n'est peut-être pas en développement, mais il fonctionne toujours sous Linux, consultez github.com/vmlaker/benchmark-sharedmem .
Velimir Mlaker