Спільне використання великого масиву Numpy для читання між багатопроцесорними процесами


88

У мене є 60 Гб масиву SciPy (матриця), я повинен ділитися між 5+ multiprocessing Processоб’єктами. Я бачив numpy-sharedmem і читав цю дискусію в списку SciPy. Здається, існує два підходи - numpy-sharedmemі використання a multiprocessing.RawArray()та відображення NumPy dtypes у ctypes. Зараз, numpy-sharedmemздається, це шлях, але я ще не бачив хорошого довідкового прикладу. Мені не потрібні будь-які замки, оскільки масив (насправді матриця) буде лише для читання. Тепер, через його розмір, я хотів би уникати копії. Це звучить , як правильний метод , щоб створити лише копію масиву у вигляді sharedmemмасиву, а потім передати його на Processоб'єкти? Кілька конкретних питань:

  1. Який найкращий спосіб насправді передавати дескриптори спільного мему в підтеки Process()? Чи потрібна мені черга, щоб просто передати один масив? Чи була б труба краще? Чи можу я просто передати його як аргумент Process()ініціюванню підкласу (де я припускаю, що він маринований)?

  2. У дискусії, яку я зв’язав вище, згадується про numpy-sharedmemте, що він не є 64-бітовим? Я точно використовую деякі структури, які не є 32-розрядними адресними.

  3. Чи є компроміси у RawArray()підході? Повільніше, баггі?

  4. Чи потрібне мені відображення ctype-to-dtype для методу numpy-sharedmem?

  5. Хтось має приклад того, як це робить код OpenSource? Я дуже практичний вчитель, і важко змусити це працювати, не маючи жодного хорошого прикладу.

Якщо є якась додаткова інформація, яку я можу надати, щоб допомогти пояснити це іншим, будь ласка, прокоментуйте, і я додаю. Дякую!

Це має працювати на Ubuntu Linux і, можливо, Mac OS, але портативність не викликає великих проблем.


1
Якщо різні процеси збираються писати в цей масив, сподівайтесь multiprocessingзробити копію всього цього для кожного процесу.
tiago

3
@tiago: "Мені не потрібні будь-які замки, оскільки масив (насправді матриця) буде лише для читання"
д-р Ян-Філіп Герке

1
@tiago: також багатопроцесорність не робить копію, якщо це явно не сказано (через аргументи до target_function). Операційна система збирається копіювати частини батьківської пам’яті в пам’ять дитини лише після модифікації.
Доктор Ян-Філіп Герке


Я вже ставив кілька питань з цього приводу. Моє рішення можна знайти тут: github.com/david-hoffman/peaks/blob/… (вибачте, код є катастрофою).
Девід Хоффман,

Відповіді:


30

@Velimir Mlaker дав чудову відповідь. Я думав, що можу додати декілька коментарів та крихітний приклад.

(Я не зміг знайти багато документації щодо sharedmem - це результати моїх власних експериментів.)

  1. Вам потрібно передавати ручки під час запуску підпроцесу або після його запуску? Якщо це тільки перший, ви можете просто використовувати targetі argsаргументи Process. Це потенційно краще, ніж використання глобальної змінної.
  2. Зі сторінки обговорення, на яку ви зв’язали, з’ясовується, що підтримка 64-розрядної Linux була додана до sharedmem деякий час тому, тому це може бути проблемою.
  3. Я не знаю про це.
  4. Ні. Зверніться до прикладу нижче.

Приклад

#!/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)

Вихідні дані

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

Це відповідне питання може бути корисним.


37

Якщо ви використовуєте Linux (або будь-яку сумісну з POSIX систему), ви можете визначити цей масив як глобальну змінну. multiprocessingвикористовує fork()в Linux, коли запускає новий дочірній процес. Щойно породжений дочірній процес автоматично ділиться пам’яттю зі своїм батьком, якщо він не змінює її (механізм копіювання та запису ).

Оскільки ви говорите "Мені не потрібні будь-які замки, оскільки масив (насправді матриця) буде лише для читання", використання цієї поведінки було б дуже простим, але в той же час надзвичайно ефективним підходом: усі дочірні процеси матимуть доступ ті самі дані у фізичній пам’яті при читанні цього великого масиву numpy.

Не передати свій масив в Process()конструктор, це буде інструктувати multiprocessingдо pickleданими для дитини, що було б вкрай неефективно або неможливо в вашому випадку. У Linux відразу після fork()дочірньої сторінки є точною копією батьків, яка використовує ту саму фізичну пам'ять, тому все, що вам потрібно зробити, це переконатися, що змінна Python, що містить матрицю, доступна з targetфункції, якій ви передаєте Process(). Цього ви зазвичай можете досягти за допомогою глобальної змінної.

Приклад коду:

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

У Windows - яка не підтримує fork()- multiprocessingвикористовує виклик win32 API CreateProcess. Це створює абсолютно новий процес із будь-якого виконуваного файлу. Ось чому в Windows потрібно просочувати дані дочірній організації, якщо їй потрібні дані, створені під час роботи батьківського елемента.


3
Функція копіювання на запис скопіює сторінку, що містить лічильник посилань (тому кожен роздвоєний пітон матиме власний лічильник посилань), але не скопіює весь масив даних.
robince

1
Я б додав, що я мав більше успіху зі змінними рівня модуля, ніж із глобальними змінними ... тобто додати змінну до модуля в глобальній області перед форком
robince

5
Слово обережності для людей, що натрапляють на це запитання / відповідь: якщо ви випадково використовуєте зв’язаний з OpenBLAS Numpy для багатопотокової роботи, переконайтеся, що вимкнено його багатопотоковість (експорт OPENBLAS_NUM_THREADS = 1) при використанні, multiprocessingабо дочірні процеси можуть закінчитися зависанням ( зазвичай використовують 1 / n одного процесора, а не n процесорів) при виконанні операцій лінійної алгебри над спільним глобальним масивом / матрицею. Відомий багатопотокової конфлікт з OpenBLAS , здається, поширюється на Pythonmultiprocessing
Dologan

1
Хто-небудь може пояснити, чому python не просто використовував ОС forkдля передачі заданих параметрів Process, замість їх серіалізації? Тобто, чи не forkможна застосувати до батьківського процесу безпосередньо перед child викликом, так що значення параметра все ще доступне з ОС? Здавалося б, більш ефективним, ніж його серіалізація?
максимум

2
Ми всі усвідомлюємо, що fork()недоступне для Windows, про це було сказано в моїй відповіді та неодноразово в коментарях. Я знаю, що це було вашим початковим запитанням, і я відповів на чотири коментарі вище цього : "Компроміс полягає у використанні одного і того ж методу передачі параметрів на обох платформах за замовчуванням для кращої ремонтопридатності та забезпечення рівної поведінки". Обидва способи мають свої переваги та недоліки, саме тому в Python 3 користувач має більшу гнучкість у виборі методу. Це обговорення не є продуктивним без детальних деталей, чого ми тут робити не повинні.
Доктор Ян-Філіп Герке

24

Вас може зацікавити крихітний фрагмент коду, який я написав: github.com/vmlaker/benchmark-sharedmem

Єдиний файл, що цікавить, - main.py. Це еталон numpy-sharedmem - код просто передає масиви (або numpyабо sharedmem) до породжених процесів через Pipe. Робітники просто закликають sum()дані. Мене цікавило лише порівняння часу передачі даних між двома реалізаціями.

Я також написав інший, більш складний код: github.com/vmlaker/sherlock .

Тут я використовую модуль numpy-sharedmem для обробки зображень у режимі реального часу з OpenCV - зображення є масивами NumPy, згідно з новим cv2API OpenCV . Зображення, фактично посилання на них, розподіляються між процесами через об’єкт словника, створений з multiprocessing.Manager(на відміну від використання Queue або Pipe.) Я отримую значні покращення продуктивності в порівнянні з використанням простих масивів NumPy.

Труба проти черги :

На моєму досвіді, IPC з Pipe швидший, ніж Queue. І це має сенс, оскільки чергу додає блокування, щоб зробити його безпечним для багатьох виробників / споживачів. Труби немає. Але якщо у вас є лише два процеси, які розмовляють взад-вперед, безпечно використовувати Pipe, або, як зазначено в документах:

... немає ризику пошкодження процесів, що використовують одночасно різні кінці труби.

sharedmemбезпека :

Основна проблема sharedmemмодуля - можливість витоку пам’яті при неввічливому виході з програми. Це описано в тривалій дискусії тут . Хоча 10 квітня 2011 року Стурла згадує виправлення витоку пам'яті, з тих пір я все ще відчував витоки, використовуючи обидва репозиторії, власний Стурла Молден на GitHub ( github.com/sturlamolden/sharedmem-numpy ) та Кріс Лі-Мессер на Bitbucket ( bitbucket.org/cleemesser/numpy-sharedmem ).


Дякую, дуже дуже інформативно. Однак витік пам'яті sharedmemздається великою справою. Будь-які підказки щодо вирішення цього?
Воля

1
Окрім того, що я просто помітив витоки, я не шукав цього в коді. Я додав до своєї відповіді в розділі "Безпека спільного доступу" вище, зберігачів двох репозиторіїв sharedmemмодуля з відкритим кодом для довідки.
Velimir Mlaker

14

Якщо ваш масив такий великий, ви можете використовувати його numpy.memmap. Наприклад, якщо у вас є масив, що зберігається на диску, скажімо 'test.array', ви можете використовувати одночасні процеси для доступу до даних на ньому навіть у режимі "запису", але ваш випадок простіший, оскільки вам потрібен лише режим "читання".

Створення масиву:

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

Потім ви можете заповнити цей масив так само, як і звичайним масивом. Наприклад:

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

Дані зберігаються на диску при видаленні змінної a.

Пізніше ви зможете використовувати кілька процесів, які будуть отримувати доступ до даних у 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))

Відповідні відповіді:


3

Вам також може бути корисним поглянути на документацію до pyro, так як якщо ви можете правильно розділити своє завдання, ви можете використовувати його для виконання різних розділів на різних машинах, а також на різних ядрах на одній машині.


0

Чому б не використовувати багатопотоковість? Ресурси основного процесу можуть бути спільно використані його потоками, отже, багатопотоковість, очевидно, є кращим способом спільного використання об'єктів, що належать основному процесу.

Якщо ви турбуєтесь про механізм GIL python, можливо, ви можете вдатися до nogilof numba.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.