Використовуйте numpy масив у спільній пам'яті для багатопроцесорної обробки


111

Я хотів би використовувати масивний масив у спільній пам'яті для використання з багатопроцесорним модулем. Складність полягає в тому, щоб використовувати його як масив numpy, а не просто як масив 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]

Це дає результат, такий як:

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

До масиву можна отримати доступ, наприклад, arr[i]має сенс. Однак це не нумерований масив, і я не можу виконувати такі операції, як -1*arr, або arr.sum(). Я припускаю, що рішенням було б перетворити масив ctypes в масивний масив. Однак (окрім того, що я не зможу зробити цю роботу), я не вірю, що це було б спільним.

Здається, було б стандартне рішення того, що повинно бути загальною проблемою.


1
Це не те саме, що це? stackoverflow.com/questions/5033799 / ...
pygabriel

1
Це не зовсім те саме питання. Пов'язане питання задається, subprocessа не multiprocessing.
Андрій

Відповіді:


82

Щоб додати відповіді @ unutbu (більше не доступні) та відповіді @Henry Gomersall. Ви можете використовувати shared_arr.get_lock()для синхронізації доступу при необхідності:

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]

Приклад

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

Якщо вам не потрібен синхронізований доступ або ви створюєте власні блокування, то mp.Array()це зайве. Ви можете використовувати mp.sharedctypes.RawArrayв цьому випадку.


2
Прекрасна відповідь! Якщо я хочу мати більше одного спільного масиву, кожен із яких може бути зафіксований окремо, але при цьому кількість масивів, визначених під час виконання, це пряме розширення того, що ви тут зробили?
Андрій

3
@Andrew: загальні масиви повинні бути створені до породження дочірніх процесів.
jfs

Хороший момент щодо порядку операцій. Це те, що я мав на увазі: створити вказану користувачем кількість загальних масивів, а потім породити кілька дочірніх процесів. Це прямо?
Андрій

1
@Chicony: ви не можете змінити розмір масиву. Подумайте про це як про спільний блок пам'яті, який потрібно було виділити до запуску дочірніх процесів. Вам не потрібно використовувати всю пам'ять , наприклад, ви могли б перейти countдо numpy.frombuffer(). Ви можете спробувати зробити це на нижчому рівні, використовуючи mmapщось подібне posix_ipcбезпосередньо для реалізації змінного розміру (може включати копіювання під час зміни розміру) аналога RawArray (або шукати наявну бібліотеку). Або якщо ваше завдання це дозволяє: копіюйте дані по частинах (якщо вам не потрібно все відразу). "Як змінити розмір спільної пам'яті" - це добре окреме питання.
jfs

1
@umopapisdn: Pool()визначає кількість процесів (кількість доступних ядер CPU використовується за замовчуванням). M- кількість разів, коли f()викликається функція.
jfs

21

ArrayОб'єкт має get_obj()метод , пов'язаний з ним, який повертає масив ctypes який представляє інтерфейс буфера. Я думаю, що наступне має працювати ...

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]

Під час запуску це виводить перший елемент, який aнаразі становить 10,0, показуючи aі bє лише двома видами в одну пам'ять.

Для того, щоб переконатися, що це все-таки багатопроцесорний безпечний, я вважаю, що вам доведеться використовувати acquireі releaseметоди, які існують на Arrayоб'єкті a, і його вбудований замок, щоб переконатися в його безпечному доступі (хоча я не фахівець з питань багатопроцесорний модуль).


він не буде працювати без синхронізації, як @unutbu продемонстрував у своїй (тепер видаленій) відповіді.
jfs

1
Імовірно, якщо ви просто хотіли отримати доступ до обробки масиву, це можна зробити чисто, не турбуючись про проблеми одночасності та блокування?
Генрі Гомерсалл

у цьому випадку вам не потрібно mp.Array.
jfs

1
Код обробки може зажадати блокованих масивів, але інтерпретація даних після обробки може бути необов'язково. Я думаю, це відбувається з розуміння, в чому саме проблема. Очевидно, що доступ до спільних даних одночасно потребує певного захисту, що, на мою думку, було б очевидним!
Генрі Гомерсалл

16

Незважаючи на те, що відповіді вже хороші, існує набагато простіше рішення цієї проблеми за умови дотримання двох умов:

  1. Ви знаходитесь в операційній системі, сумісній з POSIX (наприклад, Linux, Mac OSX); і
  2. Ваші дочірні процеси потребують доступу лише до читання до спільного масиву.

У цьому випадку вам не потрібно поспілкуватися з явним поділом змінних, оскільки дочірні процеси будуть створені за допомогою вилки. Дитина, що роздвоєна, автоматично розділяє простір пам’яті батьків. У контексті багатопроцесорної обробки Python це означає, що він розділяє всі змінні рівня модуля ; зауважте, що це не стосується аргументів, які ви явно передаєте вашим дочірнім процесам або функціям, які ви викликаєте на multiprocessing.Poolабо так.

Простий приклад:

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 Дійсно цінна інформація. Чи можете ви пояснити, чому поділяються лише вари на рівні модулів? Чому локальні параметри не є частиною простору пам'яті батьків? Наприклад, чому це не може працювати, якщо у мене функція F з локальним var V і функція G всередині F, на яку посилається V?
Кава_Таблиця

5
Попередження: Ця відповідь є дещо оманливою. Дочірній процес отримує копію стану батьківського процесу, включаючи глобальні змінні, на момент вилки. Держави жодним чином не синхронізовані і від цього моменту будуть розходитися. Ця методика може бути корисною в деяких сценаріях (наприклад, відключення спеціальних дочірніх процесів, які кожен обробляє знімок батьківського процесу, а потім припинення), але в інших не є корисною (наприклад, тривалими дочірніми процесами, які повинні ділитися і синхронізація даних з батьківським процесом).
Девід Штейн

4
@EelkeSpaak: Ваше твердження - «дитина, що роздвоєна, автоматично розділяє простір пам’яті батьків» - невірна. Якщо у мене є дочірній процес, який хоче стежити за станом батьківського процесу, строго лише для читання, розгортання мене там не потрапить: дитина бачить лише знімок стану батьків у момент розпалювання. Насправді саме це я намагався зробити (слідуючи вашої відповіді), коли виявив це обмеження. Звідси післяпис на вашу відповідь. Коротше кажучи: батьківський стан не "поділяється", а просто копіюється на дитину. Це не "обмін" у звичному розумінні.
Девід Штейн

2
Я помиляюся, вважаючи, що це ситуація копіювання при записі, принаймні, у системах posix? Тобто, після вилки, я думаю, що пам'ять поділяється, поки не будуть записані нові дані, в який момент буде створена копія. Так так, це правда, що дані не "поділяються" точно, але вони можуть забезпечити величезне підвищення продуктивності. Якщо ваш процес читається лише, копіювання накладних даних не буде! Я правильно зрозумів суть?
senderle

2
@senderle Так, саме це я мав на увазі! Звідси мій пункт (2) у відповіді про доступ лише для читання.
EelkeSpaak

11

Я написав невеликий модуль python, який використовує POSIX спільну пам’ять для обміну численними масивами між інтерпретаторами python. Можливо, вам це стане в нагоді.

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

Ось як це працює:

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

Ви можете використовувати sharedmemмодуль: https://bitbucket.org/cleemesser/numpy-sharedmem

Ось ваш початковий код тоді, цього разу з використанням спільної пам’яті, яка веде себе як масив NumPy (зверніть увагу на додаткове останнє твердження, що викликає функцію NumPy sum()):

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
Примітка. Це більше не розробляється і, здається, не працює на linux github.com/sturlamolden/sharedmem-numpy/isissue/4
AD

numpy-sharedmem може не розроблятися, але він все ще працює в Linux, перегляньте github.com/vmlaker/benchmark-sharedmem .
Велимир Млакер
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.