Çoklu işlem için paylaşılan bellekte numpy dizisi kullanın


112

Çoklu işlem modülü ile kullanmak için paylaşılan bellekte bir numpy dizisi kullanmak istiyorum. Zorluk, onu sadece bir ctypes dizisi olarak değil, bir numpy dizisi gibi kullanmaktır.

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]

Bu, aşağıdaki gibi çıktılar üretir:

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

Diziye ctypes tarzında erişilebilir, örneğin arr[i]mantıklı. Ancak, bu uyuşuk bir dizi değil ve -1*arr, veya gibi işlemleri gerçekleştiremiyorum arr.sum(). Sanırım bir çözüm, ctypes dizisini uyuşmuş bir diziye dönüştürmek olacaktır. Ancak (bu işi yapamamanın yanı sıra) artık paylaşılacağına inanmıyorum.

Ortak bir problem olması gereken şeyin standart bir çözümü varmış gibi görünüyor.



1
Tamamen aynı soru değil. Bağlantılı soru sormak subprocessyerine soruyor multiprocessing.
Andrew

Yanıtlar:


83

@ Unutbu's (artık mevcut değil) ve @Henry Gomersall'ın yanıtlarına eklemek için. shared_arr.get_lock()Gerektiğinde erişimi senkronize etmek için kullanabilirsiniz :

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]

Misal

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

Senkronize erişime ihtiyacınız yoksa veya kendi kilitlerinizi oluşturuyorsanız, o mp.Array()zaman gereksizdir. mp.sharedctypes.RawArrayBu durumda kullanabilirsiniz .


2
Güzel cevap! Her biri ayrı ayrı kilitlenebilir, ancak çalışma zamanında belirlenen dizi sayısına sahip birden fazla paylaşılan diziye sahip olmak istiyorsam, bu, burada yaptığınız şeyin basit bir uzantısı mı?
Andrew

3
@Andrew: paylaşılan diziler, çocuk süreçler ortaya çıkmadan önce oluşturulmalıdır.
jfs

İşlemlerin sırası hakkında iyi bir nokta. Yine de aklımda olan şey buydu: kullanıcı tarafından belirlenen sayıda paylaşılan diziler oluşturun, ardından birkaç alt süreç üretin. Bu basit mi?
Andrew

1
@Chicony: Dizinin boyutunu değiştiremezsiniz. Bunu, alt süreçler başlatılmadan önce ayrılması gereken paylaşılan bir bellek bloğu olarak düşünün. Tüm bellek örneğin kullanmak zorunda değilsiniz, geçebileceği countkadar numpy.frombuffer(). RawArray analogunu (veya mevcut bir kitaplığı arayarak) yeniden boyutlandırılabilir (yeniden boyutlandırırken kopyalamayı içerebilir) kullanarak mmapveya posix_ipcdoğrudan benzer bir şey kullanarak daha düşük bir seviyede yapmayı deneyebilirsiniz . Veya göreviniz izin veriyorsa: verileri parçalar halinde kopyalayın (hepsine aynı anda ihtiyacınız yoksa). "Paylaşılan bir hafızanın nasıl yeniden boyutlandırılacağı" ayrı bir sorudur.
jfs

1
@umopapisdn: Pool()işlem sayısını tanımlar (varsayılan olarak mevcut CPU çekirdeklerinin sayısı kullanılır). işlevin çağrılma Msayısıdır f().
jfs

21

ArrayNesne bir sahiptir get_obj()tampon bir arabirim oluşturur ctypes dizi döner bağlantılı bir yöntem. Bence aşağıdakiler işe yaramalı ...

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]

Çalıştırdığınızda, bu ilk unsuru dışarı baskılar aşimdi, 10.0 olmanın gösteren ave baynı belleğe sadece iki görüş vardır.

Hâlâ çok işlemcili güvenli olduğundan emin olmak için , nesnede bulunan acquireve releaseyöntemlerini ve hepsine güvenli bir şekilde erişildiğinden emin olmak için yerleşik kilidini kullanmanız gerektiğine inanıyorum (yine de bu konuda uzman değilim çok işlemcili modül).Arraya


@unutbu'nun (artık silinmiş) cevabında gösterdiği gibi senkronizasyon olmadan çalışmaz.
jfs

1
Muhtemelen, dizi son işlemeye erişmek istiyorsanız, eşzamanlılık sorunları ve kilitlenme konusunda endişelenmeden temiz bir şekilde yapılabilir mi?
Henry Gomersall

bu durumda ihtiyacınız yok mp.Array.
jfs

1
İşleme kodu kilitli diziler gerektirebilir, ancak verilerin işlem sonrası yorumlanması zorunlu olmayabilir. Sanırım bu, sorunun tam olarak ne olduğunu anlamaktan kaynaklanıyor. Açıkçası, paylaşılan verilere eşzamanlı olarak erişmek biraz koruma gerektirecek, ki bu açık olacağını düşündüm!
Henry Gomersall

16

Halihazırda verilen cevaplar iyi olsa da, iki koşulun yerine getirilmesi koşuluyla bu soruna çok daha kolay bir çözüm vardır:

  1. Bir Hangi POSIX uyumlu işletim sistemi (örneğin Linux, Mac OSX); ve
  2. Çocuk işlemlerinizin paylaşılan diziye salt okunur erişiminin olması gerekir .

Bu durumda, alt süreçler bir çatal kullanılarak oluşturulacağından, değişkenleri açıkça paylaşarak uğraşmanıza gerek yoktur. Çatallanmış bir çocuk, ebeveynin hafıza alanını otomatik olarak paylaşır. Python çoklu işlem bağlamında bu, tüm modül seviyesindeki değişkenleri paylaştığı anlamına gelir ; bunun , çocuk süreçlerinize veya a ya da öylesine çağırdığınız işlevlere açıkça aktardığınız bağımsız değişkenler için geçerli olmadığını unutmayın multiprocessing.Pool.

Basit bir örnek:

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 Gerçekten değerli bilgiler. Paylaşılanların neden yalnızca modül düzeyinde değişkenler olduğunu açıklayabilir misiniz? Yerel değişkenler neden ebeveynin bellek alanının bir parçası değil? Örneğin, yerel var V ile bir F fonksiyonuna ve F'nin içinde V'ye başvuran bir G fonksiyonuna sahipsem bu neden işe yaramıyor?
Coffee_Table

5
Uyarı: Bu yanıt biraz aldatıcıdır. Alt süreç, çatallanma anında global değişkenler dahil olmak üzere üst sürecin durumunun bir kopyasını alır. Durumlar hiçbir şekilde senkronize değildir ve o andan itibaren farklılaşacaktır. Bu teknik, bazı senaryolarda yararlı olabilir (örneğin: her biri ana sürecin anlık görüntüsünü işleyen ve sonra sonlandıran geçici alt süreçlerin ayrılması), ancak diğerlerinde yararsızdır (örneğin: paylaşmak ve verileri ana işlemle senkronize edin).
David Stein

4
@EelkeSpaak: "Çatallanmış bir çocuk otomatik olarak ebeveynin hafıza alanını paylaşır" ifadeniz yanlış. Üst sürecin durumunu salt okunur bir şekilde izlemek isteyen bir çocuk sürecim varsa, çatallama beni oraya götürmez: çocuk çatallanma anında ebeveyn durumunun yalnızca anlık görüntüsünü görür. Aslında, bu sınırlamayı keşfettiğimde (cevabınızı takiben) yapmaya çalıştığım şey tam olarak buydu. Bu nedenle cevabınızın son yazısı. Özetle: Ebeveyn durumu "paylaşılmaz", yalnızca çocuğa kopyalanır. Bu, her zamanki anlamda "paylaşmak" değil.
David Stein

2
Bunun en azından posix sistemlerinde bir yazma üzerine kopyalama durumu olduğunu düşünmekle yanılıyor muyum? Yani çataldan sonra, hafızanın yeni veri yazılıncaya kadar paylaşıldığını ve bu noktada bir kopya oluşturulduğunu düşünüyorum. Yani evet, verilerin tam olarak "paylaşılmadığı" doğru, ancak potansiyel olarak büyük bir performans artışı sağlayabilir. İşleminiz salt okunursa, kopyalama ek yükü olmayacaktır! Konuyu doğru anladım mı?
2018

2
@senderle Evet, tam olarak bunu kastettim! Bu nedenle, salt okunur erişim konusundaki yanıta (2) değinmek istiyorum.
EelkeSpaak

11

Python yorumlayıcıları arasında numpy dizileri paylaşmak için POSIX paylaşılan belleği kullanan küçük bir python modülü yazdım. Belki kullanışlı bulursun.

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

Şu şekilde çalışır:

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

sharedmemModülü kullanabilirsiniz : https://bitbucket.org/cleemesser/numpy-sharedmem

Öyleyse işte orijinal kodunuz, bu sefer NumPy dizisi gibi davranan paylaşılan bellek kullanarak (NumPy sum()işlevini çağıran ek son ifadeye dikkat edin ):

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
Not: Bu artık geliştirilmiyor ve linux github.com/sturlamolden/sharedmem-numpy/issues/4
AD

numpy-sharedmem geliştirme aşamasında olmayabilir, ancak yine de Linux üzerinde çalışıyor, github.com/vmlaker/benchmark-sharedmem adresini ziyaret edin .
Velimir Mlaker
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.