Các đối tượng bộ nhớ dùng chung trong đa xử lý


124

Giả sử tôi có một mảng lớn trong bộ nhớ, tôi có một hàm funcnhận mảng khổng lồ này làm đầu vào (cùng với một số tham số khác). funcvới các thông số khác nhau có thể được chạy song song. Ví dụ:

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]

Nếu tôi sử dụng thư viện đa xử lý, thì mảng khổng lồ đó sẽ được sao chép nhiều lần vào các quy trình khác nhau.

Có cách nào để cho phép các quy trình khác nhau chia sẻ cùng một mảng không? Đối tượng mảng này là chỉ đọc và sẽ không bao giờ được sửa đổi.

Có gì phức tạp hơn, nếu arr không phải là một mảng mà là một đối tượng python tùy ý, có cách nào để chia sẻ nó không?

[ĐÃ CHỈNH SỬA]

Tôi đọc câu trả lời nhưng tôi vẫn còn một chút bối rối. Vì fork () là copy-on-write, chúng tôi không nên gọi thêm bất kỳ chi phí nào khi tạo các quy trình mới trong thư viện đa xử lý python. Nhưng đoạn mã sau cho thấy có một chi phí lớn:

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;

đầu ra (và nhân tiện, chi phí tăng lên khi kích thước của mảng tăng lên, vì vậy tôi nghi ngờ vẫn còn chi phí liên quan đến sao chép bộ nhớ):

construct array =  0.0178790092468
multiprocessing overhead =  0.252444982529

Tại sao lại có chi phí lớn như vậy, nếu chúng ta không sao chép mảng? Và bộ nhớ dùng chung giúp tôi lưu lại phần nào?



Bạn đã xem các tài liệu , phải không?
Lev Levitsky

@FrancisAvila có cách nào để chia sẻ không chỉ mảng mà còn cả các đối tượng python tùy ý không?
Vendetta

1
@LevLevitsky Tôi phải hỏi, có cách nào để chia sẻ không chỉ mảng mà còn các đối tượng python tùy ý không?
Vendetta

2
Câu trả lời này giải thích lý do tại sao các đối tượng Python tùy ý không thể được chia sẻ.
Janne Karila

Câu trả lời:


121

Nếu bạn sử dụng một hệ điều hành sử dụng fork()ngữ nghĩa copy-on-write (giống như bất kỳ unix thông thường nào), thì miễn là bạn không bao giờ thay đổi cấu trúc dữ liệu của mình, nó sẽ có sẵn cho tất cả các tiến trình con mà không chiếm thêm bộ nhớ. Bạn sẽ không phải làm bất cứ điều gì đặc biệt (ngoại trừ đảm bảo tuyệt đối không làm thay đổi đối tượng).

Điều hiệu quả nhất bạn có thể làm cho vấn đề của mình là đóng gói mảng của bạn thành một cấu trúc mảng hiệu quả (sử dụng numpyhoặc array), đặt nó vào bộ nhớ dùng chung, bọc nó multiprocessing.Arrayvà chuyển nó vào các hàm của bạn. Câu trả lời này cho thấy làm thế nào để làm điều đó .

Nếu bạn muốn một đối tượng chia sẻ có thể ghi , thì bạn sẽ cần phải bọc nó bằng một số loại đồng bộ hóa hoặc khóa. multiprocessingcung cấp hai phương pháp thực hiện việc này : một phương pháp sử dụng bộ nhớ được chia sẻ (phù hợp với các giá trị đơn giản, mảng hoặc ctypes) hoặc Managerproxy, trong đó một tiến trình giữ bộ nhớ và trình quản lý phân xử quyền truy cập vào nó từ các tiến trình khác (thậm chí qua mạng).

Cách Managertiếp cận này có thể được sử dụng với các đối tượng Python tùy ý, nhưng sẽ chậm hơn so với cách tương đương bằng cách sử dụng bộ nhớ dùng chung vì các đối tượng cần được tuần tự hóa / giải mã hóa và gửi giữa các quy trình.

Có rất nhiều thư viện và phương pháp xử lý song song có sẵn trong Python . multiprocessinglà một thư viện hoàn hảo và tuyệt vời, nhưng nếu bạn có nhu cầu đặc biệt, có lẽ một trong những cách tiếp cận khác có thể tốt hơn.


25
Chỉ cần lưu ý, trên Python fork () thực sự có nghĩa là sao chép khi truy cập (vì chỉ cần truy cập đối tượng sẽ thay đổi số lượng tham chiếu của nó).
Fabio Zadrozny

3
@FabioZadrozny Liệu nó có thực sự sao chép toàn bộ đối tượng hay chỉ trang bộ nhớ chứa tài khoản của nó?
zigg

5
AFAIK, chỉ trang bộ nhớ có chứa tài khoản lại (vì vậy, 4kb trên mỗi quyền truy cập đối tượng).
Fabio Zadrozny

1
@max Sử dụng cách đóng. Hàm được cung cấp apply_asyncphải tham chiếu trực tiếp đến đối tượng được chia sẻ trong phạm vi thay vì thông qua các đối số của nó.
Francis Avila

3
@FrancisAvila bạn sử dụng cách đóng như thế nào? Không nên chọn hàm mà bạn cung cấp cho apply_async? Hay đây chỉ là một hạn chế của map_async?
Tiếng ĐứcK

17

Tôi gặp phải vấn đề tương tự và đã viết một lớp tiện ích bộ nhớ chia sẻ nhỏ để giải quyết nó.

Tôi đang sử dụng multiprocessing.RawArray(không khóa), và quyền truy cập vào các mảng cũng không được đồng bộ hóa (không khóa), hãy cẩn thận để không bắn vào chân của bạn.

Với giải pháp này, tôi nhận được tốc độ tăng lên khoảng 3 trên i7 lõi ​​tứ.

Đây là mã: Hãy sử dụng và cải thiện nó, và vui lòng báo cáo lại bất kỳ lỗi nào.

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

Chỉ cần nhận ra rằng bạn phải thiết lập các mảng bộ nhớ chia sẻ của mình trước khi tạo Nhóm đa xử lý, không biết tại sao nhưng nó chắc chắn sẽ không hoạt động theo cách khác.
martin.preinfalk

lý do tại sao nhóm Đa xử lý đó gọi fork () khi Nhóm được khởi tạo, vì vậy mọi thứ sau đó sẽ không có quyền truy cập vào con trỏ tới bất kỳ bản ghi nhớ được chia sẻ nào được tạo sau đó.
Xiv

Khi tôi thử mã này trong py35, tôi gặp ngoại lệ trong multiprocessing.sharedctypes.py, vì vậy tôi đoán mã này chỉ dành cho py2.
Tiến sĩ Hillier Dániel

11

Đây là trường hợp sử dụng dự kiến ​​cho Ray , là một thư viện cho Python song song và phân tán. Dưới mui xe, nó tuần tự hóa các đối tượng bằng cách sử dụng bố cục dữ liệu Apache Arrow (là định dạng không sao chép) và lưu trữ chúng trong kho lưu trữ đối tượng bộ nhớ dùng chung để chúng có thể được truy cập bởi nhiều quy trình mà không cần tạo bản sao.

Mã sẽ giống như sau.

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)

Nếu bạn không gọi ray.putthì mảng sẽ vẫn được lưu trữ trong bộ nhớ dùng chung, nhưng điều đó sẽ được thực hiện một lần cho mỗi lần gọi func, đây không phải là điều bạn muốn.

Lưu ý rằng điều này sẽ không chỉ hoạt động đối với các mảng mà còn đối với các đối tượng chứa mảng , ví dụ: từ điển ánh xạ các int với các mảng như bên dưới.

Bạn có thể so sánh hiệu suất của tuần tự hóa trong Ray so với pickle bằng cách chạy phần sau trong 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

Serialization với Ray chỉ nhanh hơn pickle một chút, nhưng deserialization nhanh hơn 1000 lần vì sử dụng bộ nhớ chia sẻ (con số này tất nhiên sẽ phụ thuộc vào đối tượng).

Xem tài liệu Ray . Bạn có thể đọc thêm về tuần tự hóa nhanh bằng Ray và Mũi tên . Lưu ý Tôi là một trong những nhà phát triển Ray.


1
Ray nghe hay đấy! Tuy nhiên, tôi đã thử sử dụng thư viện này trước đây, nhưng thật không may, tôi nhận ra rằng Ray không hỗ trợ windows. Tôi hy vọng các bạn có thể hỗ trợ windows càng sớm càng tốt. Cảm ơn bạn, các nhà phát triển!
Hzzkygcs

6

Giống như Robert Nishihara đã đề cập, Apache Arrow làm cho điều này trở nên dễ dàng, cụ thể là với kho lưu trữ đối tượng trong bộ nhớ Plasma, vốn là thứ mà Ray được xây dựng trên đó.

Tôi đã tạo ra plasma não đặc biệt vì lý do này - tải nhanh và tải lại các đối tượng lớn trong ứng dụng Flask. Đó là không gian tên đối tượng bộ nhớ dùng chung cho các đối tượng có thể tuần tự hóa của Apache Arrow, bao gồm pickle'd bytestrings được tạo bởi pickle.dumps(...).

Sự khác biệt chính với Apache Ray và Plasma là nó theo dõi các ID đối tượng cho bạn. Bất kỳ quy trình hoặc luồng hoặc chương trình nào đang chạy cục bộ đều có thể chia sẻ các giá trị của biến bằng cách gọi tên từ bất kỳ Brainđối tượng nào .

$ 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,...]
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.