다중 처리를 위해 공유 메모리에서 numpy 배열 사용


111

다중 처리 모듈과 함께 사용하기 위해 공유 메모리에서 numpy 배열을 사용하고 싶습니다. 어려운 점은 ctypes 배열이 아니라 numpy 배열처럼 사용하는 것입니다.

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]

배열은 ctypes 방식으로 액세스 할 수 있습니다 arr[i]. 그러나 numpy 배열이 아니며 -1*arr, 또는 arr.sum(). 해결책은 ctypes 배열을 numpy 배열로 변환하는 것입니다. 그러나 (이 작업을 할 수 없다는 것 외에도) 더 이상 공유 될 것이라고 생각하지 않습니다.

일반적인 문제에 대한 표준 솔루션이있는 것 같습니다.



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
아름다운 대답! 두 개 이상의 공유 배열을 원하고 각각 개별적으로 잠글 수 있지만 런타임에 배열 수가 결정되는 경우 여기에서 수행 한 작업을 간단하게 확장 할 수 있습니까?
Andrew

3
@Andrew : 자식 프로세스가 생성 되기 전에 공유 배열을 만들어야합니다 .
jfs

작업 순서에 대한 좋은 점. 그것이 제가 염두에 두었던 것입니다. 사용자가 지정한 수의 공유 배열을 만든 다음 몇 개의 자식 프로세스를 생성합니다. 간단합니까?
Andrew

1
@Chicony : 배열의 크기를 변경할 수 없습니다. 하위 프로세스가 시작되기 전에 할당되어야하는 공유 메모리 블록으로 생각하십시오. 당신은 모든 메모리 등을 사용할 필요가 없습니다, 당신은 통과 할 수 countnumpy.frombuffer(). 크기 조정이 가능한 (크기를 조정하는 동안 복사가 필요할 수 있음) RawArray 아날로그 (또는 기존 라이브러리를 찾음)를 구현하기 위해 직접 mmap또는 이와 유사한 것을 사용하여 하위 수준 posix_ipc에서 수행 할 수 있습니다. 또는 작업에서 허용하는 경우 : 데이터를 부분적으로 복사합니다 (한 번에 모두 필요하지 않은 경우). "공유 메모리 크기 조정 방법"은 별도의 좋은 질문입니다.
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 인 도시 ab같은 메모리에 두 도면이다.

다중 프로세서가 여전히 안전한지 확인 하려면 객체에 존재 하는 acquirerelease메서드 를 사용해야하며 , 및 내장 된 잠금 장치를 사용하여 안전하게 액세스 할 수 있도록해야합니다. 다중 프로세서 모듈).Arraya


@unutbu가 (현재 삭제 된) 답변에서 보여준 것처럼 동기화 없이는 작동하지 않습니다.
jfs

1
아마도 어레이 사후 처리에 액세스하려는 경우 동시성 문제 및 잠금에 대한 걱정없이 깔끔하게 수행 할 수 있습니까?
Henry Gomersall 2011 년

이 경우에는 mp.Array.
jfs

1
처리 코드에는 잠긴 배열이 필요할 수 있지만 데이터의 사후 처리 해석이 반드시 필요한 것은 아닙니다. 나는 이것이 정확히 문제가 무엇인지 이해에서 비롯된 것 같습니다. 분명히, 공유 데이터에 동시에 액세스하려면 약간의 보호가 필요할 것입니다.
Henry Gomersall 2010 년

16

이미 주어진 대답은 좋지만 두 가지 조건이 충족되면이 문제에 대한 훨씬 더 쉬운 해결책이 있습니다.

  1. 당신은에있는 POSIX 호환 운영 체제 (예 : 리눅스, 맥 OSX); 과
  2. 자식 프로세스 는 공유 배열에 대한 읽기 전용 액세스 권한 이 필요합니다 .

이 경우 자식 프로세스가 포크를 사용하여 생성되므로 명시 적으로 변수를 공유하도록 만들 필요가 없습니다. 분기 된 자식은 자동으로 부모의 메모리 공간을 공유합니다. 파이썬 다중 처리의 맥락에서 이것은 모든 모듈 수준 변수를 공유 함을 의미합니다 . 이것은 자식 프로세스 또는 호출하는 함수에 명시 적으로 전달하는 인수 에는 적용되지 않습니다 .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 정말 귀중한 정보. 공유되는 것이 모듈 수준 변수 인 이유를 설명 할 수 있습니까? 로컬 변수가 부모 메모리 공간의 일부가 아닌 이유는 무엇입니까? 예를 들어, 로컬 var V가있는 함수 F와 V를 참조하는 F 내부 함수 G가있는 경우 왜 이것이 작동하지 않습니까?
Coffee_Table

5
경고 :이 답변은 약간 기만적입니다. 자식 프로세스는 분기 할 때 전역 변수를 포함하여 부모 프로세스의 상태 복사본을받습니다. 상태는 전혀 동기화되지 않으며 그 순간부터 분기됩니다. 이 기술은 일부 시나리오에서 유용 할 수 있지만 (예 : 상위 프로세스의 스냅 샷을 각각 처리 한 다음 종료하는 임시 하위 프로세스를 분기) 다른 시나리오에서는 쓸모가 없습니다 (예 : 공유해야하는 장기 실행 하위 프로세스). 부모 프로세스와 데이터 동기화).
David Stein

4
@EelkeSpaak : "분기 된 자식이 자동으로 부모의 메모리 공간을 공유합니다"라는 진술이 잘못되었습니다. 부모 프로세스의 상태를 엄격하게 읽기 전용 방식으로 모니터링하려는 자식 프로세스가있는 경우 분기는 해당 위치로 이동하지 않습니다. 자식은 분기 순간에 부모 상태의 스냅 샷 만 볼 수 있습니다. 사실, 이것이 바로 제가이 한계를 발견했을 때 (당신의 대답에 따라)하려고했던 것입니다. 따라서 귀하의 답변에 대한 포스트 스크립트. 간단히 말해서 부모 상태는 "공유"가 아니라 단순히 자식에게 복사됩니다. 그것은 일반적인 의미에서 "공유"가 아닙니다.
David Stein

2
적어도 posix 시스템에서 이것이 copy-on-write 상황이라고 생각하는 것이 잘못된 것입니까? 즉, 포크 후에는 새로운 데이터가 쓰여질 때까지 메모리가 공유되고 사본이 생성되는 시점이라고 생각합니다. 그렇습니다. 데이터가 정확히 "공유"되지는 않지만 잠재적으로 엄청난 성능 향상을 제공 할 수 있다는 것은 사실입니다. 프로세스가 읽기 전용이면 복사 오버 헤드가 없습니다! 요점을 올바르게 이해 했습니까?
senderle

2
@senderle 네, 정확히 제가 의미하는 바입니다! 따라서 읽기 전용 액세스에 대한 대답에서 내 요점 (2).
EelkeSpaak

11

POSIX 공유 메모리를 사용하여 파이썬 인터프리터간에 numpy 배열을 공유하는 작은 파이썬 모듈을 작성했습니다. 아마도 당신은 그것이 편리하다는 것을 알게 될 것입니다.

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/issues/4
AD

numpy-sharedmem 은 개발 중이 아닐 수 있지만 여전히 Linux에서 작동합니다 . github.com/vmlaker/benchmark-sharedmem을 확인하세요 .
Velimir Mlaker
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.