web-dev-qa-db-fra.com

Partager un grand tableau Numpy en lecture seule entre les processus de multitraitement

J'ai un tableau SciPy de 60 Go (matrice) que je dois partager entre 5+ multiprocessingProcess objets. J'ai vu numpy-sharedmem et lu cette discussion sur la liste SciPy. Il semble y avoir deux approches - numpy-sharedmem Et l'utilisation d'une multiprocessing.RawArray() et le mappage de NumPy dtypes en ctypes. Maintenant, numpy-sharedmem Semble être la voie à suivre, mais je n'ai pas encore vu un bon exemple de référence. Je n'ai besoin d'aucune sorte de verrous, car le tableau (en fait une matrice) sera en lecture seule. Maintenant, en raison de sa taille, j'aimerais éviter une copie. Cela ressemble à la bonne méthode consiste à créer la copie uniquement du tableau en tant que tableau sharedmem, puis passez-le aux objets Process? Quelques questions spécifiques:

  1. Quelle est la meilleure façon de passer les poignées sharedmem à sub -Process() es? Ai-je besoin d'une file d'attente pour passer un seul tableau? Une pipe serait-elle meilleure? Puis-je simplement le passer comme argument à l'init de la sous-classe Process() (où je suppose qu'il est mariné)?

  2. Dans la discussion que j'ai liée ci-dessus, il est mentionné que numpy-sharedmem N'est pas sûr pour 64 bits? J'utilise certainement des structures qui ne sont pas adressables en 32 bits.

  3. Existe-t-il des compromis à l'approche RawArray()? Plus lent, plus bogué?

  4. Ai-je besoin d'un mappage ctype vers dtype pour la méthode numpy-sharedmem?

  5. Quelqu'un at-il un exemple de code OpenSource faisant cela? Je suis un érudit très pratique et il est difficile de faire fonctionner cela sans aucun bon exemple à regarder.

S'il y a des informations supplémentaires que je peux fournir pour aider à clarifier cela pour les autres, veuillez commenter et j'ajouterai. Merci!

Cela doit fonctionner sur Ubuntu Linux et Peut-être Mac OS, mais la portabilité n'est pas une préoccupation majeure.

77
Will

@Velimir Mlaker a donné une excellente réponse. J'ai pensé pouvoir ajouter quelques bribes de commentaires et un petit exemple.

(Je n'ai pas trouvé beaucoup de documentation sur sharedmem - ce sont les résultats de mes propres expériences.)

  1. Avez-vous besoin de passer les poignées au démarrage du sous-processus ou après son démarrage? Si c'est juste le premier, vous pouvez simplement utiliser les arguments target et args pour Process. C'est potentiellement mieux que d'utiliser une variable globale.
  2. À partir de la page de discussion que vous avez liée, il semble que la prise en charge de Linux 64 bits a été ajoutée à sharedmem il y a quelque temps, donc cela pourrait ne pas être un problème.
  3. Je ne sais pas pour celui-ci.
  4. Référez-vous à l'exemple ci-dessous.

Exemple

#!/usr/bin/env python
from multiprocessing import Process
import sharedmem
import numpy

def do_work(data, start):
    data[start] = 0;

def split_work(num):
    n = 20
    width  = n/num
    shared = sharedmem.empty(n)
    shared[:] = numpy.random.Rand(1, n)[0]
    print "values are %s" % shared

    processes = [Process(target=do_work, args=(shared, i*width)) for i in xrange(num)]
    for p in processes:
        p.start()
    for p in processes:
        p.join()

    print "values are %s" % shared
    print "type is %s" % type(shared[0])

if __== '__main__':
    split_work(4)

Sortie

values are [ 0.81397784  0.59667692  0.10761908  0.6736734   0.46349645  0.98340718
  0.44056863  0.10701816  0.67167752  0.29158274  0.22242552  0.14273156
  0.34912309  0.43812636  0.58484507  0.81697513  0.57758441  0.4284959
  0.7292129   0.06063283]
values are [ 0.          0.59667692  0.10761908  0.6736734   0.46349645  0.
  0.44056863  0.10701816  0.67167752  0.29158274  0.          0.14273156
  0.34912309  0.43812636  0.58484507  0.          0.57758441  0.4284959
  0.7292129   0.06063283]
type is <type 'numpy.float64'>

Cette question connexe pourrait être utile.

26
James Lim

Si vous êtes sous Linux (ou tout système compatible POSIX), vous pouvez définir ce tableau en tant que variable globale. multiprocessing utilise fork() sous Linux lorsqu'il démarre un nouveau processus enfant. Un processus enfant nouvellement généré partage automatiquement la mémoire avec son parent tant qu'il ne le modifie pas (mécanisme copy-on-write ).

Puisque vous dites "Je n'ai besoin d'aucun type de verrous, car le tableau (en fait une matrice) sera en lecture seule", tirer parti de ce comportement serait une approche très simple et pourtant extrêmement efficace: tous les processus enfants auront accès les mêmes données dans la mémoire physique lors de la lecture de ce grand tableau numpy.

Ne remettez pas votre tableau au constructeur Process(), cela demandera à multiprocessing de pickle les données à l'enfant, ce qui serait extrêmement inefficace ou impossible dans votre cas. Sous Linux, juste après fork() l'enfant est une copie exacte du parent utilisant la même mémoire physique, donc tout ce que vous devez faire est de vous assurer que la Python variable ' contenant "la matrice est accessible à partir de la fonction target que vous transférez à Process(). Ceci est généralement possible avec une variable" globale ".

Exemple de code:

from multiprocessing import Process
from numpy import random


global_array = random.random(10**4)


def child():
    print sum(global_array)


def main():
    processes = [Process(target=child) for _ in xrange(10)]
    for p in processes:
        p.start()
    for p in processes:
        p.join()


if __== "__main__":
    main()

Sous Windows - qui ne prend pas en charge fork() - multiprocessing utilise l'appel API win32 CreateProcess. Il crée un processus entièrement nouveau à partir de n'importe quel exécutable donné. C'est pourquoi sous Windows, il est nécessaire de décapage des données à l'enfant si l'on a besoin de données qui ont été créées pendant l'exécution du parent.

32
Jan-Philip Gehrcke

Vous pourriez être intéressé par un petit morceau de code que j'ai écrit: github.com/vmlaker/benchmark-sharedmem

Le seul fichier qui vous intéresse est main.py. C'est une référence de numpy-sharedmem - le code passe simplement des tableaux (numpy ou sharedmem) aux processus générés, via Pipe. Les travailleurs appellent simplement sum() sur les données. Je voulais seulement comparer les temps de communication des données entre les deux implémentations.

J'ai également écrit un autre code plus complexe: github.com/vmlaker/sherlock .

Ici, j'utilise le module numpy-sharedmem pour le traitement d'image en temps réel avec OpenCV - les images sont des tableaux NumPy, selon la nouvelle API cv2 D'OpenCV. Les images, en fait les références de celles-ci, sont partagées entre les processus via l'objet dictionnaire créé à partir de multiprocessing.Manager (par opposition à l'utilisation de la file d'attente ou du tuyau.) J'obtiens de grandes améliorations de performances par rapport à en utilisant des tableaux NumPy simples.

Pipe vs file d'attente :

D'après mon expérience, IPC avec Pipe est plus rapide que Queue. Et cela a du sens, car Queue ajoute un verrouillage pour le rendre sûr pour plusieurs producteurs/consommateurs. Pipe ne le fait pas. Mais si vous avez seulement deux processus qui parlent d'avant en arrière, il est sûr d'utiliser Pipe, ou, comme le lisent les documents:

... il n'y a aucun risque de corruption des processus utilisant différentes extrémités du tuyau en même temps.

sharedmem sécurité :

Le principal problème avec le module sharedmem est la possibilité de fuite de mémoire à la sortie du programme. Ceci est décrit dans une longue discussion ici . Bien que le 10 avril 2011, Sturla mentionne un correctif à une fuite de mémoire, j'ai toujours connu des fuites depuis lors, en utilisant les deux dépôts, Sturla Molden's propre sur GitHub ( github.com/sturlamolden/sharedmem-numpy ) et Chris Lee-Messer sur Bitbucket ( bitbucket.org/cleemesser/numpy-sharedmem ).

20
Velimir Mlaker

Si votre tableau est si grand, vous pouvez utiliser numpy.memmap. Par exemple, si vous avez un tableau stocké sur le disque, dites 'test.array', vous pouvez utiliser des processus simultanés pour y accéder même en mode "écriture", mais votre cas est plus simple car vous n'avez besoin que du mode "lecture".

Création du tableau:

a = np.memmap('test.array', dtype='float32', mode='w+', shape=(100000,1000))

Vous pouvez ensuite remplir ce tableau de la même manière que vous le faites avec un tableau ordinaire. Par exemple:

a[:10,:100]=1.
a[10:,100:]=2.

Les données sont stockées sur disque lorsque vous supprimez la variable a.

Plus tard, vous pouvez utiliser plusieurs processus qui accéderont aux données dans test.array:

# read-only mode
b = np.memmap('test.array', dtype='float32', mode='r', shape=(100000,1000))

# read and writing mode
c = np.memmap('test.array', dtype='float32', mode='r+', shape=(100000,1000))

Réponses associées:

11
Saullo G. P. Castro

Vous pouvez également trouver utile de consulter la documentation de pyro comme si vous pouviez partitionner votre tâche de manière appropriée, vous pourriez l'utiliser pour exécuter différentes sections sur différentes machines ainsi que sur différents cœurs de la même manière. machine.

3
Steve Barnes