Udostępniaj dużą, tylko do odczytu tablicę numeryczną między procesami wieloprocesowymi

88

Mam 60 GB SciPy Array (Matrix), które muszę udostępniać między 5+ multiprocessing Processobiektami. Widziałem numpy-sharedmem i przeczytałem tę dyskusję na liście SciPy. Wydaje się, że są dwa podejścia - numpy-sharedmemi używając a multiprocessing.RawArray()oraz mapując NumPy dtypes na ctypes. numpy-sharedmemWydaje się , że jest to właściwy sposób, ale nie widziałem jeszcze dobrego przykładu referencyjnego. Nie potrzebuję żadnych blokad, ponieważ tablica (właściwie macierz) będzie tylko do odczytu. Teraz, ze względu na jego rozmiar, chciałbym uniknąć kopii. To brzmi jak poprawna metoda jest stworzenie tylko kopię tablicy jako sharedmemtablicy, a następnie przekazać je do Processobiektów? Kilka szczegółowych pytań:

  1. Jaki jest najlepszy sposób przekazania uchwytów pamięci współdzielonej do podrzędnych Process()? Czy potrzebuję kolejki tylko do przekazywania jednej tablicy? Czy fajka byłaby lepsza? Czy mogę po prostu przekazać to jako argument do Process()init podklasy (gdzie zakładam, że jest wytrawiony)?

  2. W dyskusji, którą połączyłem powyżej, jest wzmianka o numpy-sharedmembraku bezpieczeństwa 64-bitowego? Zdecydowanie używam niektórych struktur, które nie są 32-bitowe adresowalne.

  3. Czy są jakieś kompromisy w tym RawArray()podejściu? Wolniej, buggier?

  4. Czy potrzebuję mapowania ctype-to-dtype dla metody numpy-sharedmem?

  5. Czy ktoś ma przykład, jak jakiś kod OpenSource to robi? Jestem bardzo praktyczny i ciężko jest mi to udać bez żadnego dobrego przykładu.

Jeśli mogę podać dodatkowe informacje, które pomogą wyjaśnić to innym, skomentuj, a dodam. Dzięki!

To musi działać na Ubuntu Linux i może Mac OS, ale przenośność nie jest wielkim problemem.

Będzie
źródło
1
Jeśli różne procesy będą zapisywać w tej tablicy, spodziewaj multiprocessingsię wykonania kopii całości dla każdego procesu.
tiago,
3
@tiago: „Nie potrzebuję żadnych blokad, ponieważ tablica (a właściwie macierz) będzie tylko do odczytu”
Dr. Jan-Philip Gehrcke
1
@tiago: również przetwarzanie wieloprocesowe nie polega na tworzeniu kopii, o ile nie jest to wyraźnie nakazane (za pośrednictwem argumentów do target_function). System operacyjny będzie kopiował części pamięci rodzica do przestrzeni pamięci dziecka tylko po modyfikacji.
Dr.Jan-Philip Gehrcke
Zadałem kilka pytań o tym wcześniej. Moje rozwiązanie można znaleźć tutaj: github.com/david-hoffman/peaks/blob/ ... (przepraszam, że kod to katastrofa).
David Hoffman

Odpowiedzi:

30

@Velimir Mlaker dał świetną odpowiedź. Pomyślałem, że mógłbym dodać kilka komentarzy i mały przykład.

(Nie mogłem znaleźć dużo dokumentacji na współdzielonej pamięci - to są wyniki moich własnych eksperymentów.)

  1. Czy musisz przekazywać uchwyty podczas uruchamiania podprocesu, czy po jego uruchomieniu? Jeśli to tylko pierwsza, możesz po prostu użyć argumentów targeti . Jest to potencjalnie lepsze niż używanie zmiennej globalnej.argsProcess
  2. Z połączonej strony dyskusji wynika, że ​​wsparcie dla 64-bitowego Linuksa zostało dodane do pamięci współdzielonej jakiś czas temu, więc może to nie być problem.
  3. Nie wiem o tym.
  4. Nie. Zobacz przykład poniżej.

Przykład

#!/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 __name__ == '__main__':
    split_work(4)

Wynik

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

To powiązane pytanie może być przydatne.

James Lim
źródło
37

Jeśli korzystasz z Linuksa (lub dowolnego systemu zgodnego z POSIX), możesz zdefiniować tę tablicę jako zmienną globalną. multiprocessingużywa fork()w systemie Linux, kiedy rozpoczyna nowy proces potomny. Nowo utworzony proces potomny automatycznie współdzieli pamięć ze swoim rodzicem, o ile jej nie zmienia ( mechanizm kopiowania przy zapisie ).

Ponieważ mówisz „Nie potrzebuję żadnych blokad, ponieważ tablica (właściwie macierz) będzie tylko do odczytu” skorzystanie z tego zachowania byłoby bardzo prostym, a jednocześnie niezwykle wydajnym podejściem: wszystkie procesy potomne będą miały dostęp te same dane w pamięci fizycznej podczas odczytu tej dużej tablicy numpy.

Nie przekazuj swojej tablicy Process()konstruktorowi, spowoduje multiprocessingto pickleprzekazanie danych dziecku, co w twoim przypadku byłoby wyjątkowo nieefektywne lub niemożliwe. W Linuksie zaraz za fork()dzieckiem znajduje się dokładna kopia rodzica korzystająca z tej samej pamięci fizycznej, więc wszystko, co musisz zrobić, to upewnić się, że zmienna Pythona „zawierająca” macierz jest dostępna z poziomu targetfunkcji, której przekazujesz Process(). Zwykle można to osiągnąć za pomocą zmiennej „globalnej”.

Przykładowy kod:

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 __name__ == "__main__":
    main()

W systemie Windows - który nie obsługuje fork()- multiprocessingużywa wywołania Win32 API CreateProcess. Tworzy całkowicie nowy proces z dowolnego pliku wykonywalnego. Dlatego w systemie Windows wymagane jest wytrawianie danych dziecku, jeśli potrzebne są dane, które zostały utworzone podczas działania rodzica.

Dr Jan-Philip Gehrcke
źródło
3
Copy-on-write skopiuje stronę zawierającą licznik odniesienia (więc każdy rozwidlony Python będzie miał swój własny licznik odwołań), ale nie skopiuje całej tablicy danych.
robince
1
Dodam, że miałem więcej sukcesów z poziomu modułu zmiennych niż ze zmiennych globalnych ... czyli dodać zmienną do modułu w zakresie globalnym przed widelcem
robince
5
Uwaga dla ludzi, którzy natknęli się na to pytanie / odpowiedź: Jeśli zdarzy się, że używasz Numpy połączonego z OpenBLAS do operacji wielowątkowej, upewnij się, że wyłączasz jego wielowątkowość (eksport OPENBLAS_NUM_THREADS = 1) podczas używania multiprocessinglub procesy potomne mogą się zawiesić ( zazwyczaj używa 1 / n jednego procesora zamiast n procesorów) podczas wykonywania operacji algebry liniowej na wspólnej globalnej tablicy / macierzy. Znany wielowątkowe konflikt z OpenBLAS wydaje się przedłużyć do Pythonamultiprocessing
Dologan
1
Czy ktoś może wyjaśnić, dlaczego Python nie używałby tylko systemu operacyjnego forkdo przekazywania podanych parametrów Process, zamiast ich serializacji? To znaczy, czy nie forkmożna zastosować do procesu nadrzędnego tuż przed child wywołaniem, tak aby wartość parametru była nadal dostępna z systemu operacyjnego? Wydawałoby się, że jest bardziej wydajna niż serializacja?
max
2
Wszyscy wiemy, że fork()nie jest dostępny w systemie Windows, zostało to stwierdzone w mojej odpowiedzi i wielokrotnie w komentarzach. Wiem, że to była twoja początkowa pytanie i odpowiedziałem jej cztery komentarze powyżej to : „Kompromis jest użycie tej samej metody przekazania parametrów na obu platformach domyślnie dla lepszej konserwacji oraz dla zapewnienia równego zachowania.”. Oba sposoby mają swoje wady i zalety, dlatego w Pythonie 3 istnieje większa elastyczność w wyborze metody przez użytkownika. Ta dyskusja nie jest produktywna bez omawiania szczegółów, czego nie powinniśmy tutaj robić.
Dr Jan-Philip Gehrcke
24

Może zainteresuje Cię mały fragment kodu, który napisałem: github.com/vmlaker/benchmark-sharedmem

Jedyny interesujący plik to main.py. Jest to punkt odniesienia dla numpy-sharedmem - kod po prostu przekazuje tablice (albo numpylub sharedmem) do zrodzonych procesów, za pośrednictwem potoku. Pracownicy po prostu sum()korzystają z danych. Interesowało mnie tylko porównanie czasów przesyłania danych między dwoma wdrożeniami.

Napisałem też inny, bardziej złożony kod: github.com/vmlaker/sherlock .

Tutaj używam modułu numpy-sharedmem do przetwarzania obrazu w czasie rzeczywistym z OpenCV - obrazy są tablicami NumPy, zgodnie z nowszym cv2API OpenCV . Obrazy, a właściwie ich odniesienia, są współdzielone między procesami za pośrednictwem obiektu słownika utworzonego z multiprocessing.Manager(w przeciwieństwie do kolejki lub potoku). W porównaniu ze zwykłymi tablicami NumPy osiągam znaczną poprawę wydajności.

Rura a kolejka :

Z mojego doświadczenia wynika, że ​​IPC z potokiem jest szybsze niż kolejka. Ma to sens, ponieważ Queue dodaje blokowanie, aby było bezpieczne dla wielu producentów / konsumentów. Rura nie. Ale jeśli masz tylko dwa procesy rozmawiające w tę iz powrotem, możesz bezpiecznie użyć potoku lub, jak czytają dokumenty:

... nie ma ryzyka korupcji w procesach wykorzystujących jednocześnie różne końcówki rury.

sharedmembezpieczeństwo :

Głównym problemem związanym z sharedmemmodułem jest możliwość wycieku pamięci po niezręcznym zakończeniu programu. Opisano to w obszernej dyskusji tutaj . Chociaż 10 kwietnia 2011 Sturla wspomina naprawę wycieku pamięci, od tego czasu wciąż doświadczałem wycieków, używając obu repozytoriów , własnego Sturla Molden na GitHub ( github.com/sturlamolden/sharedmem-numpy ) i Chrisa Lee-Messera na Bitbucket ( bitbucket.org/cleemesser/numpy-sharedmem ).

Velimir Mlaker
źródło
Dzięki, bardzo pouczające. Jednak wyciek pamięci sharedmembrzmi jak wielka sprawa. Jakieś wskazówki, jak to rozwiązać?
Will
1
Poza tym, że zauważyłem wycieki, nie szukałem tego w kodzie. Dodałem do mojej odpowiedzi, w sekcji „bezpieczeństwo pamięci współdzielonej” powyżej, opiekunów dwóch repozytoriów sharedmemmodułu open source , w celach informacyjnych.
Velimir Mlaker
14

Jeśli twoja tablica jest tak duża, możesz użyć numpy.memmap. Na przykład, jeśli masz tablicę przechowywaną na dysku, powiedzmy 'test.array', możesz użyć jednoczesnych procesów, aby uzyskać dostęp do danych w niej nawet w trybie „zapisu”, ale sprawa jest prostsza, ponieważ potrzebujesz tylko trybu „odczytu”.

Tworzenie tablicy:

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

Następnie możesz wypełnić tę tablicę w taki sam sposób, jak w przypadku zwykłej tablicy. Na przykład:

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

Dane są zapisywane na dysku, gdy usuwasz zmienną a.

Później możesz korzystać z wielu procesów, które będą uzyskiwać dostęp do danych w 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))

Powiązane odpowiedzi:

Saullo GP Castro
źródło
3

Przydatne może być również zapoznanie się z dokumentacją dla pyro, tak jakbyś mógł odpowiednio podzielić swoje zadanie, mógłbyś go użyć do wykonania różnych sekcji na różnych maszynach, a także na różnych rdzeniach tej samej maszyny.

Steve Barnes
źródło
0

Dlaczego nie skorzystać z wielowątkowości? Zasoby procesu głównego mogą być współdzielone przez jego wątki w sposób natywny, dlatego wielowątkowość jest oczywiście lepszym sposobem udostępniania obiektów należących do procesu głównego.

Jeśli martwisz się mechanizmem GIL w Pythonie, może możesz skorzystać z nogilof numba.

Nico
źródło