Об'єкти спільної пам’яті в багатопроцесорній


123

Припустимо, у мене є великий масив масивної пам'яті, у мене є функція, funcяка приймає цей гігантський масив як вхідний (разом з деякими іншими параметрами). funcз різними параметрами можна запускати паралельно. Наприклад:

def func(arr, param):
    # do stuff to arr, param

# build array arr

pool = Pool(processes = 6)
results = [pool.apply_async(func, [arr, param]) for param in all_params]
output = [res.get() for res in results]

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

Чи є спосіб дозволити різним процесам ділити один і той же масив? Цей об’єкт масиву доступний лише для читання і ніколи не буде змінений.

Що складніше, якщо arr - це не масив, а довільний об'єкт python, чи є спосіб поділитися ним?

[ЗДОРОВО]

Я прочитав відповідь, але все ще трохи розгублений. Оскільки fork () є копіюванням, ми не повинні вимагати додаткових витрат при нерестуванні нових процесів у багатопроцесорній бібліотеці python. Але наступний код говорить про величезні накладні витрати:

from multiprocessing import Pool, Manager
import numpy as np; 
import time

def f(arr):
    return len(arr)

t = time.time()
arr = np.arange(10000000)
print "construct array = ", time.time() - t;


pool = Pool(processes = 6)

t = time.time()
res = pool.apply_async(f, [arr,])
res.get()
print "multiprocessing overhead = ", time.time() - t;

вихід (і, до речі, вартість збільшується зі збільшенням розміру масиву, тому я підозрюю, що все ще є накладні витрати, пов'язані з копіюванням пам'яті):

construct array =  0.0178790092468
multiprocessing overhead =  0.252444982529

Чому виникають такі величезні накладні витрати, якщо ми не копіювали масив? І яка частина мене врятує загальна пам'ять?



Ви подивилися на документи , правда?
Лев Левицький

@FrancisAvila Чи є спосіб поділитися не просто масивом, а довільними об’єктами python?
Вендетта

1
@LevLevitsky Мені потрібно запитати, чи є спосіб поділитися не просто масивом, а довільними об’єктами python?
Вендетта

2
Ця відповідь добре пояснює, чому довільних об’єктів Python не можна ділити.
Janne Karila

Відповіді:


121

Якщо ви використовуєте операційну систему, яка використовує fork()семантику копіювання під час запису (як і будь-який загальний Unix), до тих пір, поки ви ніколи не зміните свою структуру даних, вона буде доступна для всіх дочірніх процесів, не займаючи додаткову пам'ять. Вам не доведеться робити нічого особливого (крім абсолютно впевненого, що ви не змінюєте об'єкт).

Найбільш ефективним, що ви можете зробити для своєї проблеми , буде упакувати масив в ефективну структуру масиву (використовуючи numpyабо array), розмістити його у спільній пам’яті, обернути його multiprocessing.Arrayі передати його своїм функціям. Ця відповідь показує, як це зробити .

Якщо ви хочете, щоб спільний об'єкт, що записується , вам знадобиться обгорнути його якоюсь синхронізацією чи блокуванням. multiprocessingнадає два способи цього : один, використовуючи спільну пам'ять (підходить для простих значень, масивів або ctypes) або Managerпроксі-сервер, де один процес зберігає пам'ять, а менеджер арбітрує доступ до неї з інших процесів (навіть через мережу).

ManagerПідхід може бути використаний з об'єктами довільної Python, але буде повільніше , ніж еквівалентний з використанням спільно використовуваної пам'яті , тому що об'єкти повинні бути серіалізовать / десеріалізациі і надсилаються між процесами.

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


25
Зазначимо, на Python fork () насправді означає копію доступу (адже просто доступ до об'єкта змінить його перерахунок).
Фабіо Задрозний

3
@FabioZadrozny Чи насправді це буде копіювати весь об'єкт або лише сторінку пам'яті, що містить його знижку?
zigg

5
AFAIK, лише сторінка пам'яті, що містить перерахунок (так, 4 кб на кожен об'єкт доступу).
Фабіо Задрозний

1
@max Використовуйте закриття. Функція, що надається, apply_asyncповинна посилатися на об'єкт, що поділяється, в області застосування безпосередньо, а не через його аргументи.
Франциск Авіла

3
@FrancisAvila як ви використовуєте закриття? Чи не має бути функція, яку ви надаєте для apply_async, обраною? Або це лише обмеження map_async?
GermanK

17

Я зіткнувся з тією ж проблемою і написав невеликий клас утиліти спільної пам’яті, щоб обійти її.

Я використовую multiprocessing.RawArray(lockfree), а також доступ до масивів взагалі не синхронізований (lockfree), будьте обережні, щоб не стріляти власними ногами.

За допомогою рішення я отримую прискорення на коефіцієнт приблизно 3 на чотирьохядерному i7.

Ось код: Сміливо користуйтеся та вдосконалюйте його, і повідомте про будь-які помилки.

'''
Created on 14.05.2013

@author: martin
'''

import multiprocessing
import ctypes
import numpy as np

class SharedNumpyMemManagerError(Exception):
    pass

'''
Singleton Pattern
'''
class SharedNumpyMemManager:    

    _initSize = 1024

    _instance = None

    def __new__(cls, *args, **kwargs):
        if not cls._instance:
            cls._instance = super(SharedNumpyMemManager, cls).__new__(
                                cls, *args, **kwargs)
        return cls._instance        

    def __init__(self):
        self.lock = multiprocessing.Lock()
        self.cur = 0
        self.cnt = 0
        self.shared_arrays = [None] * SharedNumpyMemManager._initSize

    def __createArray(self, dimensions, ctype=ctypes.c_double):

        self.lock.acquire()

        # double size if necessary
        if (self.cnt >= len(self.shared_arrays)):
            self.shared_arrays = self.shared_arrays + [None] * len(self.shared_arrays)

        # next handle
        self.__getNextFreeHdl()        

        # create array in shared memory segment
        shared_array_base = multiprocessing.RawArray(ctype, np.prod(dimensions))

        # convert to numpy array vie ctypeslib
        self.shared_arrays[self.cur] = np.ctypeslib.as_array(shared_array_base)

        # do a reshape for correct dimensions            
        # Returns a masked array containing the same data, but with a new shape.
        # The result is a view on the original array
        self.shared_arrays[self.cur] = self.shared_arrays[self.cnt].reshape(dimensions)

        # update cnt
        self.cnt += 1

        self.lock.release()

        # return handle to the shared memory numpy array
        return self.cur

    def __getNextFreeHdl(self):
        orgCur = self.cur
        while self.shared_arrays[self.cur] is not None:
            self.cur = (self.cur + 1) % len(self.shared_arrays)
            if orgCur == self.cur:
                raise SharedNumpyMemManagerError('Max Number of Shared Numpy Arrays Exceeded!')

    def __freeArray(self, hdl):
        self.lock.acquire()
        # set reference to None
        if self.shared_arrays[hdl] is not None: # consider multiple calls to free
            self.shared_arrays[hdl] = None
            self.cnt -= 1
        self.lock.release()

    def __getArray(self, i):
        return self.shared_arrays[i]

    @staticmethod
    def getInstance():
        if not SharedNumpyMemManager._instance:
            SharedNumpyMemManager._instance = SharedNumpyMemManager()
        return SharedNumpyMemManager._instance

    @staticmethod
    def createArray(*args, **kwargs):
        return SharedNumpyMemManager.getInstance().__createArray(*args, **kwargs)

    @staticmethod
    def getArray(*args, **kwargs):
        return SharedNumpyMemManager.getInstance().__getArray(*args, **kwargs)

    @staticmethod    
    def freeArray(*args, **kwargs):
        return SharedNumpyMemManager.getInstance().__freeArray(*args, **kwargs)

# Init Singleton on module load
SharedNumpyMemManager.getInstance()

if __name__ == '__main__':

    import timeit

    N_PROC = 8
    INNER_LOOP = 10000
    N = 1000

    def propagate(t):
        i, shm_hdl, evidence = t
        a = SharedNumpyMemManager.getArray(shm_hdl)
        for j in range(INNER_LOOP):
            a[i] = i

    class Parallel_Dummy_PF:

        def __init__(self, N):
            self.N = N
            self.arrayHdl = SharedNumpyMemManager.createArray(self.N, ctype=ctypes.c_double)            
            self.pool = multiprocessing.Pool(processes=N_PROC)

        def update_par(self, evidence):
            self.pool.map(propagate, zip(range(self.N), [self.arrayHdl] * self.N, [evidence] * self.N))

        def update_seq(self, evidence):
            for i in range(self.N):
                propagate((i, self.arrayHdl, evidence))

        def getArray(self):
            return SharedNumpyMemManager.getArray(self.arrayHdl)

    def parallelExec():
        pf = Parallel_Dummy_PF(N)
        print(pf.getArray())
        pf.update_par(5)
        print(pf.getArray())

    def sequentialExec():
        pf = Parallel_Dummy_PF(N)
        print(pf.getArray())
        pf.update_seq(5)
        print(pf.getArray())

    t1 = timeit.Timer("sequentialExec()", "from __main__ import sequentialExec")
    t2 = timeit.Timer("parallelExec()", "from __main__ import parallelExec")

    print("Sequential: ", t1.timeit(number=1))    
    print("Parallel: ", t2.timeit(number=1))

Щойно зрозумів, що перед тим, як створити багатопроцесорний пул, ви повинні налаштувати спільні масиви пам’яті, не знаю, чому поки, але це точно не буде працювати навпаки.
martin.preinfalk

Причина полягає в тому, що багатопроцесорний пул викликає fork (), коли пул екземплярується, тому нічого після цього не отримає доступ до вказівника до будь-якої спільної пам’яті, створеної після цього.
Xiv

Коли я спробував цей код під py35, я отримав виняток у multiprocessing.sharedctypes.py, тому я думаю, що цей код призначений лише для py2.
Доктор Хільєр Даніель

11

Це призначений випадок використання для Ray , який є бібліотекою для паралельного та розподіленого Python. Під капотом вона серіалізує об'єкти, використовуючи макет даних Apache Arrow (який є форматом нульової копії) і зберігає їх у сховищі об'єктів спільної пам'яті, щоб до них можна було отримати доступ за допомогою декількох процесів без створення копій.

Код виглядатиме наступним чином.

import numpy as np
import ray

ray.init()

@ray.remote
def func(array, param):
    # Do stuff.
    return 1

array = np.ones(10**6)
# Store the array in the shared memory object store once
# so it is not copied multiple times.
array_id = ray.put(array)

result_ids = [func.remote(array_id, i) for i in range(4)]
output = ray.get(result_ids)

Якщо ви не зателефонували, ray.putмасив все ще зберігатиметься у спільній пам'яті, але це буде зроблено один раз за виклик func, що не є тим, що ви хочете.

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

Ви можете порівняти ефективність серіалізації в Ray проти маринованих, виконавши наступне в IPython.

import numpy as np
import pickle
import ray

ray.init()

x = {i: np.ones(10**7) for i in range(20)}

# Time Ray.
%time x_id = ray.put(x)  # 2.4s
%time new_x = ray.get(x_id)  # 0.00073s

# Time pickle.
%time serialized = pickle.dumps(x)  # 2.6s
%time deserialized = pickle.loads(serialized)  # 1.9s

Серіалізація з Реєм лише трохи швидша, ніж мариновані, але деріаріалізація на 1000 разів швидша через використання спільної пам'яті (це число, звичайно, буде залежати від об'єкта).

Дивіться документацію промені . Ви можете прочитати більше про швидку серіалізацію за допомогою Ray and Arrow . Зауважте, я один із розробників Ray.


1
Рей звучить добре! Але я раніше намагався використовувати цю бібліотеку, але, на жаль, я просто зрозумів, що Ray не підтримує windows. Сподіваюся, ви, хлопці, зможете підтримати Windows як можна швидше. Дякую, розробники!
Hzzkygcs

5

Як згадував Роберт Нішіхара, Apache Arrow робить це легко, спеціально з магазином об'єктів пам'яті Плазми, на чому створений Рей.

Я зробив мозкову плазму спеціально з цієї причини - швидке завантаження та перезавантаження великих об'єктів у програмі Flask. Це простір імен об'єктів спільної пам'яті для об'єктів, що серіалізуються Apache Arrow, включаючи pickle'd bytestrings, породжені pickle.dumps(...).

Ключова відмінність Apache Ray від Plasma полягає в тому, що він відстежує ідентифікатори об’єктів для вас. Будь-які процеси або потоки або програми, що працюють на локальному рівні, можуть ділитися значеннями змінних, викликаючи ім'я з будь-якого Brainоб'єкта.

$ pip install brain-plasma
$ plasma_store -m 10000000 -s /tmp/plasma

from brain_plasma import Brain
brain = Brain(path='/tmp/plasma/)

brain['a'] = [1]*10000

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