Sử dụng mảng numpy trong bộ nhớ dùng chung để xử lý đa


111

Tôi muốn sử dụng một mảng numpy trong bộ nhớ dùng chung để sử dụng với mô-đun đa xử lý. Khó khăn là sử dụng nó như một mảng numpy, và không chỉ như một mảng 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]

Điều này tạo ra đầu ra như:

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

Mảng có thể được truy cập theo cách ctypes, ví dụ như hợp arr[i]lý. Tuy nhiên, nó không phải là một mảng phức tạp và tôi không thể thực hiện các phép toán như -1*arr, hoặc arr.sum(). Tôi cho rằng một giải pháp sẽ là chuyển đổi mảng ctypes thành một mảng numpy. Tuy nhiên (ngoài việc không thể thực hiện điều này), tôi không tin rằng nó sẽ được chia sẻ nữa.

Có vẻ như sẽ có một giải pháp tiêu chuẩn cho những gì phải là một vấn đề chung.


1
Nó không giống với cái này? stackoverflow.com/questions/5033799/…
pygabriel

1
Nó không hoàn toàn giống một câu hỏi. Câu hỏi được liên kết đang hỏi về subprocesshơn là multiprocessing.
Andrew

Câu trả lời:


82

Để thêm vào câu trả lời của @ unutbu (không còn nữa) và @Henry Gomersall. Bạn có thể sử dụng shared_arr.get_lock()để đồng bộ hóa quyền truy cập khi cần:

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]

Thí dụ

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

Nếu bạn không cần quyền truy cập đồng bộ hoặc bạn tạo khóa của riêng mình thì mp.Array()không cần thiết. Bạn có thể sử dụng mp.sharedctypes.RawArraytrong trường hợp này.


2
Câu trả lời tuyệt vời! Nếu tôi muốn có nhiều hơn một mảng được chia sẻ, mỗi mảng có thể khóa riêng biệt, nhưng với số lượng mảng được xác định trong thời gian chạy, đó có phải là một phần mở rộng đơn giản của những gì bạn đã làm ở đây không?
Andrew

3
@Andrew: mảng chia sẻ nên được tạo trước khi các tiến trình con được tạo ra.
jfs

Điểm tốt về thứ tự hoạt động. Tuy nhiên, đó là điều tôi đã nghĩ đến: tạo một số mảng được chia sẻ do người dùng chỉ định, sau đó tạo ra một vài quy trình con. Đó có phải là đơn giản?
Andrew

1
@Chicony: bạn không thể thay đổi kích thước của Mảng. Hãy nghĩ về nó như một khối bộ nhớ dùng chung phải được cấp phát trước khi các tiến trình con được bắt đầu. Bạn không cần phải sử dụng tất cả bộ nhớ, ví dụ, bạn có thể chuyển countsang numpy.frombuffer(). Bạn có thể thử làm điều đó ở cấp độ thấp hơn bằng cách sử dụng mmaphoặc một cái gì đó tương tự như posix_ipctrực tiếp để triển khai một tương tự RawArray có thể thay đổi kích thước (có thể liên quan đến việc sao chép trong khi thay đổi kích thước) (hoặc tìm kiếm một thư viện hiện có). Hoặc nếu nhiệm vụ của bạn cho phép: sao chép dữ liệu theo từng phần (nếu bạn không cần tất cả cùng một lúc). "Làm thế nào để thay đổi kích thước bộ nhớ dùng chung" là một câu hỏi riêng biệt.
jfs

1
@umopapisdn: Pool()xác định số lượng quá trình (số lõi CPU có sẵn được sử dụng theo mặc định). Mlà số lần f()hàm được gọi.
jfs

21

Đối Arraytượng có một get_obj()phương thức được liên kết với nó, phương thức này trả về mảng ctypes hiển thị một giao diện đệm. Tôi nghĩ những điều sau đây sẽ hoạt động ...

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]

Khi chạy, phần tử này in ra phần tử đầu tiên ahiện là 10.0, hiển thị abchỉ là hai khung nhìn vào cùng một bộ nhớ.

Để đảm bảo rằng nó vẫn an toàn với đa xử lý, tôi tin rằng bạn sẽ phải sử dụng các phương thức acquirereleasephương thức tồn tại trên Arrayđối tượng acũng như khóa tích hợp của nó để đảm bảo rằng tất cả các đối tượng đều được truy cập an toàn (mặc dù tôi không phải là chuyên gia về mô-đun đa xử lý).


nó sẽ không hoạt động nếu không đồng bộ hóa như @unutbu đã trình bày trong câu trả lời (hiện đã bị xóa) của anh ấy.
jfs

1
Có lẽ, nếu bạn chỉ muốn truy cập xử lý bài đăng mảng, nó có thể được thực hiện một cách sạch sẽ mà không phải lo lắng về các vấn đề đồng thời và khóa?
Henry Gomersall

trong trường hợp này bạn không cần mp.Array.
jfs

1
Mã xử lý có thể yêu cầu các mảng bị khóa, nhưng việc giải thích dữ liệu sau xử lý có thể không nhất thiết. Tôi đoán điều này xuất phát từ việc hiểu chính xác vấn đề là gì. Rõ ràng, truy cập dữ liệu được chia sẻ đồng thời sẽ yêu cầu một số biện pháp bảo vệ, điều mà tôi nghĩ là hiển nhiên!
Henry Gomersall

16

Mặc dù các câu trả lời đã đưa ra là tốt, nhưng có một giải pháp dễ dàng hơn nhiều cho vấn đề này với điều kiện hai điều kiện được đáp ứng:

  1. Bạn đang sử dụng hệ điều hành tương thích với POSIX (ví dụ: Linux, Mac OSX); và
  2. Các quy trình con của bạn cần quyền truy cập chỉ đọc vào mảng được chia sẻ.

Trong trường hợp này, bạn không cần phải làm cho các biến được chia sẻ một cách rõ ràng, vì các quy trình con sẽ được tạo bằng cách sử dụng một nhánh rẽ. Một con được chia tự động chia sẻ không gian bộ nhớ của cha mẹ. Trong bối cảnh đa xử lý Python, điều này có nghĩa là nó chia sẻ tất cả các biến cấp mô-đun ; lưu ý rằng điều này không áp dụng cho các đối số mà bạn chuyển rõ ràng cho các quy trình con của bạn hoặc cho các hàm mà bạn gọi trên một multiprocessing.Poolhoặc lâu hơn.

Một ví dụ đơn giản:

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 Thông tin thực sự có giá trị. Bạn có thể giải thích tại sao chỉ có các vars cấp mô-đun mới được chia sẻ? Tại sao các vars cục bộ không phải là một phần của không gian bộ nhớ của cha mẹ? Ví dụ: tại sao điều này không thể hoạt động nếu tôi có một hàm F với var V cục bộ và một hàm G bên trong F tham chiếu đến V?
Coffee_Table

5
Cảnh báo: Câu trả lời này hơi lừa dối. Tiến trình con nhận được một bản sao của trạng thái của tiến trình mẹ, bao gồm các biến toàn cục, tại thời điểm fork. Các trạng thái không được đồng bộ hóa và sẽ khác nhau kể từ thời điểm đó. Kỹ thuật này có thể hữu ích trong một số trường hợp (ví dụ: bỏ qua các quy trình con đặc biệt mà mỗi quy trình xử lý một ảnh chụp nhanh của quy trình mẹ và sau đó kết thúc), nhưng vô dụng trong các trường hợp khác (ví dụ: các quy trình con chạy lâu phải chia sẻ và đồng bộ dữ liệu với quy trình mẹ).
David Stein

4
@EelkeSpaak: Tuyên bố của bạn - "một đứa trẻ được chia tự động chia sẻ không gian bộ nhớ của cha mẹ" - không chính xác. Nếu tôi có một quy trình con muốn theo dõi trạng thái của quy trình mẹ, theo cách chỉ đọc nghiêm ngặt, việc fork sẽ không đưa tôi đến đó: đứa trẻ chỉ nhìn thấy ảnh chụp nhanh của trạng thái cha tại thời điểm fork. Trên thực tế, đó chính xác là những gì tôi đang cố gắng làm (theo câu trả lời của bạn) khi tôi phát hiện ra hạn chế này. Do đó, phần tái bút trên câu trả lời của bạn. Tóm lại: Trạng thái gốc không được "chia sẻ", mà chỉ được sao chép cho trẻ. Đó không phải là "chia sẻ" theo nghĩa thông thường.
David Stein

2
Tôi có nhầm lẫn khi nghĩ rằng đây là một tình huống copy-on-write, ít nhất là trên hệ thống posix không? Đó là, sau fork, tôi nghĩ rằng bộ nhớ được chia sẻ cho đến khi dữ liệu mới được ghi, lúc đó một bản sao được tạo ra. Vì vậy, có, đúng là dữ liệu không được "chia sẻ" chính xác, nhưng nó có thể cung cấp khả năng tăng hiệu suất rất lớn. Nếu quy trình của bạn chỉ được đọc, thì sẽ không có chi phí sao chép! Tôi đã hiểu chính xác vấn đề chưa?
gửi

2
@senderle Vâng, đó chính là ý của tôi! Do đó, quan điểm của tôi (2) trong câu trả lời về quyền truy cập chỉ đọc.
EelkeSpaak

11

Tôi đã viết một mô-đun python nhỏ sử dụng bộ nhớ chia sẻ POSIX để chia sẻ các mảng numpy giữa các trình thông dịch python. Có thể bạn sẽ thấy nó tiện dụng.

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

Đây là cách nó hoạt động:

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

Bạn có thể sử dụng sharedmemmô-đun: https://bitbucket.org/cleemesser/numpy-sharedmem

Sau đó, đây là mã ban đầu của bạn, lần này sử dụng bộ nhớ dùng chung hoạt động giống như một mảng NumPy (lưu ý câu lệnh cuối cùng bổ sung gọi một sum()hàm NumPy ):

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
Lưu ý: tính năng này không còn được phát triển và dường như không hoạt động trên linux github.com/sturlamolden/sharedmem-numpy/issues/4
sau Công nguyên

numpy-sharedmem có thể không được phát triển, nhưng nó vẫn hoạt động trên Linux, hãy xem github.com/vmlaker/benchmark-sharedmem .
Velimir Mlaker
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.