Chia sẻ mảng lớn, chỉ đọc giữa các quy trình đa xử lý


88

Tôi có Mảng SciPy (Ma trận) 60GB, tôi phải chia sẻ giữa 5 multiprocessing Processđối tượng trở lên. Tôi đã xem numpy-sharedmem và đọc cuộc thảo luận này trên danh sách SciPy. Dường như có hai cách tiếp cận-- numpy-sharedmemvà sử dụng a multiprocessing.RawArray()và ánh xạ NumPy dtypes với ctypes. Bây giờ, numpy-sharedmemcó vẻ là một con đường để đi, nhưng tôi vẫn chưa thấy một ví dụ tham khảo tốt. Tôi không cần bất kỳ loại khóa nào, vì mảng (thực ra là ma trận) sẽ ở chế độ chỉ đọc. Bây giờ, do kích thước của nó, tôi muốn tránh một bản sao. Có vẻ như phương pháp đúng là tạo bản sao duy nhất của mảng dưới dạng một sharedmemmảng và sau đó truyền nó cho các Processđối tượng? Một số câu hỏi cụ thể:

  1. Cách tốt nhất để thực sự chuyển các điều khiển sharedmem đến các trình phụ là Process()gì? Tôi có cần một hàng đợi chỉ để chuyển một mảng xung quanh không? Một đường ống sẽ tốt hơn? Tôi có thể chỉ chuyển nó như một đối số cho Process()init của lớp con (trong đó tôi cho rằng nó được chọn) không?

  2. Trong cuộc thảo luận tôi đã liên kết ở trên, có đề cập đến việc numpy-sharedmemkhông an toàn cho 64bit? Tôi chắc chắn đang sử dụng một số cấu trúc không thể địa chỉ 32 bit.

  3. Có phải đánh đổi RawArray()cách tiếp cận không? Chậm hơn, ồn ào hơn?

  4. Tôi có cần bất kỳ ánh xạ ctype-to-dtype nào cho phương thức numpy-sharedmem không?

  5. Có ai có ví dụ về một số mã OpenSource làm điều này không? Tôi là một người học rất thực hành và thật khó để làm cho việc này hoạt động nếu không có bất kỳ tấm gương tốt nào để xem xét.

Nếu có bất kỳ thông tin bổ sung nào tôi có thể cung cấp để giúp làm rõ điều này cho những người khác, vui lòng nhận xét và tôi sẽ bổ sung. Cảm ơn!

Điều này cần phải chạy trên Ubuntu Linux và Có thể là Mac OS, nhưng tính di động không phải là một mối quan tâm lớn.


1
Nếu các quy trình khác nhau sẽ ghi vào mảng đó, multiprocessinghãy tạo một bản sao toàn bộ cho mỗi quy trình.
tiago,

3
@tiago: "Tôi không cần bất kỳ loại ổ khóa, kể từ khi mảng (thực ra là một ma trận) sẽ được read-only"
Tiến sĩ Jan-Philip Gehrcke

1
@tiago: ngoài ra, đa xử lý không phải là tạo một bản sao miễn là không được yêu cầu rõ ràng (thông qua các đối số cho target_function). Hệ điều hành sẽ sao chép các phần trong bộ nhớ của cha mẹ vào không gian bộ nhớ của trẻ chỉ khi sửa đổi.
Tiến sĩ Jan-Philip Gehrcke


Tôi đã hỏi một vài câu hỏi về điều này trước đây. Giải pháp của tôi có thể được tìm thấy tại đây: github.com/david-hoffman/peaks/blob/… (xin lỗi vì mã là một thảm họa).
David Hoffman,

Câu trả lời:


30

@Velimir Mlaker đã đưa ra một câu trả lời tuyệt vời. Tôi nghĩ rằng tôi có thể thêm một số nhận xét và một ví dụ nhỏ.

(Tôi không thể tìm thấy nhiều tài liệu về sharedmem - đây là kết quả của các thử nghiệm của riêng tôi.)

  1. Bạn có cần chuyển các xử lý khi quá trình con đang bắt đầu hay sau khi nó đã bắt đầu? Nếu nó chỉ là cái trước, bạn chỉ có thể sử dụng các đối số targetargscho Process. Điều này có khả năng tốt hơn so với việc sử dụng một biến toàn cục.
  2. Từ trang thảo luận mà bạn đã liên kết, có vẻ như hỗ trợ cho Linux 64 bit đã được thêm vào sharedmem một thời gian trước, vì vậy nó có thể không phải là vấn đề.
  3. Tôi không biết về cái này.
  4. Không. Tham khảo ví dụ bên dưới.

Thí dụ

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

Đầu ra

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

Câu hỏi liên quan này có thể hữu ích.


37

Nếu bạn đang sử dụng Linux (hoặc bất kỳ hệ thống nào tương thích với POSIX), bạn có thể xác định mảng này là một biến toàn cục. multiprocessingđang sử dụng fork()trên Linux khi nó bắt đầu một quy trình con mới. Một tiến trình con mới được tạo ra sẽ tự động chia sẻ bộ nhớ với cha mẹ của nó miễn là nó không thay đổi nó ( copy-on-write cơ chế ).

Vì bạn đang nói "Tôi không cần bất kỳ loại khóa nào, vì mảng (thực ra là ma trận) sẽ ở chế độ chỉ đọc", lợi dụng hành vi này sẽ là một cách tiếp cận rất đơn giản và cực kỳ hiệu quả: tất cả các quy trình con sẽ truy cập cùng dữ liệu trong bộ nhớ vật lý khi đọc mảng lớn này.

Đừng trao mảng của bạn để các Process()nhà xây dựng, đây sẽ hướng dẫn multiprocessingđể picklecác dữ liệu cho đứa trẻ, đó sẽ là cực kỳ hiệu quả hoặc không thể trong trường hợp của bạn. Trên Linux, ngay sau fork()phần tử con là bản sao chính xác của phần tử mẹ sử dụng cùng một bộ nhớ vật lý, vì vậy tất cả những gì bạn cần làm là đảm bảo rằng biến Python 'chứa' ma trận có thể truy cập được từ bên trong targethàm mà bạn chuyển giao choProcess() . Điều này bạn thường có thể đạt được với một biến 'toàn cầu'.

Mã ví dụ:

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

Trên Windows - không hỗ trợ fork()- multiprocessingđang sử dụng lệnh gọi API win32 CreateProcess. Nó tạo ra một quy trình hoàn toàn mới từ bất kỳ tệp thực thi nhất định nào. Đó là lý do tại sao trên Windows người ta bắt buộc phải chọn dữ liệu cho con nếu một người cần dữ liệu đã được tạo trong thời gian chạy của cha mẹ.


3
Copy-on-write sẽ sao chép trang có chứa bộ đếm tham chiếu (do đó, mỗi con trăn được phân nhánh sẽ có bộ đếm tham chiếu riêng) nhưng nó sẽ không sao chép toàn bộ mảng dữ liệu.
robince

1
Tôi muốn nói thêm rằng tôi đã thành công hơn với các biến cấp mô-đun hơn là với các biến toàn cục ... tức là thêm biến vào một mô-đun trong phạm vi toàn cầu trước fork
robince

5
Lời cảnh báo cho những người tình cờ gặp câu hỏi / câu trả lời này: Nếu bạn tình cờ sử dụng Numpy được liên kết với OpenBLAS cho hoạt động đa luồng của nó, hãy đảm bảo tắt đa luồng của nó (xuất OPENBLAS_NUM_THREADS = 1) khi sử dụng multiprocessinghoặc các quy trình con có thể bị treo ( thường sử dụng 1 / n của một bộ xử lý thay vì n bộ xử lý) khi thực hiện các phép toán đại số tuyến tính trên một mảng / ma trận toàn cục được chia sẻ. Các cuộc xung đột đa luồng được biết đến với OpenBLAS dường như kéo dài đến Pythonmultiprocessing
Dologan

1
Bất cứ ai có thể giải thích tại sao python không chỉ sử dụng hệ điều hành forkđể chuyển các tham số được cung cấp cho Process, thay vì tuần tự hóa chúng? Có nghĩa là, không forkthể áp dụng cho quy trình mẹ ngay trước đó child được gọi, để giá trị tham số vẫn có sẵn từ Hệ điều hành? Có vẻ sẽ hiệu quả hơn so với việc đăng hàng loạt?
tối đa

2
Tất cả chúng tôi đều biết rằng điều đó fork()không có sẵn trên Windows, nó đã được nêu trong câu trả lời của tôi và nhiều lần trong các nhận xét. Tôi biết rằng đây là câu hỏi đầu tiên của bạn, và tôi đã trả lời nó bốn ý kiến trên này : "sự thỏa hiệp là sử dụng cùng một phương pháp truyền thông số trên cả hai nền tảng theo mặc định, cho khả năng bảo trì tốt hơn và đảm bảo hành vi bình đẳng.". Cả hai cách đều có ưu và nhược điểm, đó là lý do tại sao trong Python 3, người dùng có thể lựa chọn phương pháp linh hoạt hơn. Cuộc thảo luận này không hữu ích với những chi tiết nói chuyện, điều mà chúng ta không nên làm ở đây.
Tiến sĩ Jan-Philip Gehrcke.

24

Bạn có thể quan tâm đến một đoạn mã nhỏ mà tôi đã viết: github.com/vmlaker/benchmark-sharedmem

Hồ sơ quan tâm duy nhất là main.py. Đó là một điểm chuẩn của numpy-sharedmem - mã chỉ đơn giản là chuyển các mảng (hoặc numpyhoặc sharedmem) đến các quy trình được tạo ra, thông qua Pipe. Các công nhân chỉ cần gọi sum()trên dữ liệu. Tôi chỉ quan tâm đến việc so sánh thời gian giao tiếp dữ liệu giữa hai triển khai.

Tôi cũng đã viết một mã khác phức tạp hơn: github.com/vmlaker/sherlock .

Ở đây tôi sử dụng mô-đun numpy-sharedmem để xử lý hình ảnh thời gian thực với OpenCV - hình ảnh là mảng NumPy, theo cv2API mới hơn của OpenCV . Các hình ảnh, thực sự là tham chiếu đến nó, được chia sẻ giữa các quy trình thông qua đối tượng từ điển được tạo từ multiprocessing.Manager(trái ngược với việc sử dụng Queue hoặc Pipe.) Tôi đang nhận được những cải tiến hiệu suất tuyệt vời khi so sánh với việc sử dụng mảng NumPy đơn thuần.

Đường ống so với Hàng đợi :

Theo kinh nghiệm của tôi, IPC với Pipe nhanh hơn Queue. Và điều đó có ý nghĩa, vì Hàng đợi thêm khóa để đảm bảo an toàn cho nhiều nhà sản xuất / người tiêu dùng. Ống không. Nhưng nếu bạn chỉ có hai quy trình nói chuyện qua lại, thì sẽ an toàn khi sử dụng Pipe hoặc như tài liệu đọc:

... không có nguy cơ hỏng hóc từ các quy trình sử dụng các đầu ống khác nhau cùng một lúc.

sharedmem sự an toàn :

Vấn đề chính với sharedmemmô-đun là khả năng bị rò rỉ bộ nhớ khi thoát chương trình không hợp lệ. Điều này được mô tả trong một cuộc thảo luận dài ở đây . Mặc dù vào ngày 10 tháng 4 năm 2011, Sturla đề cập đến việc sửa lỗi rò rỉ bộ nhớ, tôi vẫn gặp phải tình trạng rò rỉ kể từ đó, khi sử dụng cả hai repo, của riêng Sturla Molden trên GitHub ( github.com/sturlamolden/sharedmem-numpy ) và Chris Lee-Messer trên Bitbucket ( bitbucket.org/cleemesser/numpy-sharedmem ).


Cảm ơn, rất nhiều thông tin. Tuy nhiên, bộ nhớ bị rò rỉ sharedmemnghe có vẻ như là một vấn đề lớn. Bất kỳ hướng dẫn giải quyết điều đó?
Will

1
Ngoài việc chỉ chú ý đến các rò rỉ, tôi còn chưa tìm kiếm nó trong mã. Tôi đã thêm vào câu trả lời của mình, trong phần "an toàn sharedmem" ở trên, những người lưu giữ hai kho lưu trữ mã nguồn mở của sharedmemmô-đun, để tham khảo.
Velimir Mlaker,

14

Nếu mảng của bạn lớn đến mức bạn có thể sử dụng numpy.memmap. Ví dụ: nếu bạn có một mảng được lưu trữ trong đĩa, chẳng hạn 'test.array', bạn có thể sử dụng các quy trình đồng thời để truy cập dữ liệu trong đó ngay cả ở chế độ "ghi", nhưng trường hợp của bạn đơn giản hơn vì bạn chỉ cần chế độ "đọc".

Tạo mảng:

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

Sau đó, bạn có thể điền mảng này giống như cách bạn làm với một mảng thông thường. Ví dụ:

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

Dữ liệu được lưu trữ vào đĩa khi bạn xóa biến a .

Sau này, bạn có thể sử dụng nhiều quy trình sẽ truy cập dữ liệu trong 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))

Các câu trả lời liên quan:


3

Bạn cũng có thể thấy hữu ích khi xem tài liệu về pyro như thể bạn có thể phân vùng nhiệm vụ của mình một cách thích hợp, bạn có thể sử dụng nó để thực thi các phần khác nhau trên các máy khác nhau cũng như trên các lõi khác nhau trong cùng một máy.


0

Tại sao không sử dụng đa luồng? Tài nguyên của quá trình chính có thể được chia sẻ bởi các luồng của nó nguyên bản, do đó đa luồng rõ ràng là một cách tốt hơn để chia sẻ các đối tượng thuộc sở hữu của quá trình chính.

Nếu bạn lo lắng về cơ chế GIL python, có thể bạn có thể dùng đến các nogilcủa numba.

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.