Çoklu işlemede paylaşılan bellek nesneleri


124

Büyük bir bellek dizilimine sahip olduğumu varsayalım func, bu dev diziyi girdi olarak alan bir işleve sahibim (diğer bazı parametrelerle birlikte). funcfarklı parametrelerle paralel olarak çalıştırılabilir. Örneğin:

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]

Çoklu işlem kitaplığı kullanırsam, o zaman bu dev dizi farklı işlemlere birden çok kez kopyalanacaktır.

Farklı işlemlerin aynı diziyi paylaşmasına izin vermenin bir yolu var mı? Bu dizi nesnesi salt okunurdur ve hiçbir zaman değiştirilmeyecektir.

Daha karmaşık olan şey, arr bir dizi değilse de keyfi bir python nesnesiyse, onu paylaşmanın bir yolu var mı?

[REDAKTE]

Cevabı okudum ama hala biraz kafam karışık. Fork (), yazma üzerine kopyalama olduğundan, python çoklu işlem kütüphanesinde yeni süreçler üretirken herhangi bir ek maliyet getirmemeliyiz. Ancak aşağıdaki kod, çok büyük bir ek yük olduğunu gösteriyor:

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;

çıktı (ve bu arada, dizinin boyutu arttıkça maliyet artar, bu nedenle bellek kopyalamayla ilgili hala ek yük olduğundan şüpheleniyorum):

construct array =  0.0178790092468
multiprocessing overhead =  0.252444982529

Diziyi kopyalamadıysak, neden bu kadar büyük ek yük var? Ve paylaşılan hafıza beni hangi bölümden kurtarıyor?



Dokümanlara baktın değil mi?
Lev Levitsky

@FrancisAvila sadece diziyi değil, keyfi python nesnelerini paylaşmanın bir yolu var mı?
Vendetta

1
@LevLevitsky Sormak zorundayım, sadece diziyi değil, keyfi python nesnelerini paylaşmanın bir yolu var mı?
Vendetta

2
Bu cevap , keyfi Python nesnelerinin neden paylaşılamayacağını güzel bir şekilde açıklıyor.
Janne Karila

Yanıtlar:


121

Yazma üzerine kopyalama fork()semantiğini kullanan bir işletim sistemi kullanıyorsanız (herhangi bir yaygın unix gibi), veri yapınızı asla değiştirmediğiniz sürece, ek bellek kullanmadan tüm alt süreçler tarafından kullanılabilir olacaktır. Özel bir şey yapmanız gerekmeyecek (nesneyi değiştirmediğinizden kesinlikle emin olmanız dışında).

En verimli şey size sorununuzla için yapabileceğiniz (kullanarak etkin bir dizi yapı halinde diziyi paketi olacaktır numpyya array), paylaşılan hafızada, ile sarın o yer multiprocessing.Array, ve işlevlere o geçmektedir. Bu cevap, bunun nasıl yapılacağını gösterir .

Eğer bir istiyorsanız yazılabilir paylaşılan nesne, o zaman senkronizasyonu veya kilitleme çeşit sarmak gerekir. bunu yapmak için iki yöntemmultiprocessing sağlar : paylaşılan bellek (basit değerler, diziler veya türler için uygundur) ya da bir işlemin belleği tuttuğu ve bir yöneticinin diğer işlemlerden (hatta bir ağ üzerinden) ona erişimi düzenlediği bir proxy kullanan bir proxy.Manager

ManagerYaklaşım keyfi Python nesneleri ile kullanılabilir, ancak nesneler / tefrika serileştirilemezse ve süreçler arasında gönderilmesi gereken çünkü yavaş eşdeğer kullanarak paylaşılan hafızada daha olacaktır.

Orada bir olan paralel işleme kütüphaneleri zenginliği ve Python mevcuttur yaklaşımlar . multiprocessingmükemmel ve çok yönlü bir kütüphanedir, ancak özel ihtiyaçlarınız varsa, belki diğer yaklaşımlardan biri daha iyi olabilir.


25
Python'da fork () aslında erişimde kopyalama anlamına gelir (çünkü sadece nesneye erişmek ref sayısını değiştirecektir).
Fabio Zadrozny

3
@FabioZadrozny Aslında nesnenin tamamını mı yoksa sadece refcount'unu içeren bellek sayfasını mı kopyalar?
zigg

5
AFAIK, yalnızca refcount içeren bellek sayfası (yani, her nesne erişiminde 4kb).
Fabio Zadrozny

1
@max Bir kapatma kullanın. Verilen işlev apply_async, bağımsız değişkenleri yerine doğrudan kapsamdaki paylaşılan nesneye başvurmalıdır .
Francis Avila

3
@FrancisAvila bir kapatmayı nasıl kullanıyorsunuz? Apply_async için verdiğiniz işlev seçilebilir olmamalı mı? Veya bu yalnızca bir map_async kısıtlaması mı?
GermanK

17

Ben de aynı problemle karşılaştım ve bu sorunu çözmek için küçük bir paylaşılan bellek yardımcı sınıfı yazdım.

Ben kullanıyorum multiprocessing.RawArray, kendi ayakları vurmamaya dikkat (lockfree) ve aynı zamanda diziler için erişimi tüm (lockfree) de senkronize edilmez.

Çözümle, dört çekirdekli bir i7'de yaklaşık 3 kat hızlanma elde ediyorum.

İşte kod: Kullanmaktan ve iyileştirmekten çekinmeyin ve lütfen hataları bize bildirin.

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

Çoklu işlem Havuzunu oluşturmadan önce paylaşılan bellek dizilerinizi kurmanız gerektiğini fark ettim, nedenini henüz bilmiyorum ama kesinlikle tersi şekilde çalışmayacaktır.
martin.preinfalk

Bunun nedeni, Çoklu işlem havuzunun, Havuz başlatıldığında fork () 'u çağırmasıdır, bu nedenle bundan sonraki hiçbir şey, daha sonra oluşturulan herhangi bir paylaşılan mem'e işaretçiye erişemez.
Xiv

Bu kodu py35 altında denediğimde multiprocessing.sharedctypes.py'de istisna yaşadım, bu yüzden bu kodun yalnızca py2 için olduğunu tahmin ediyorum.
Dr.Hillier Dániel

11

Bu, paralel ve dağıtılmış Python için bir kitaplık olan Ray için amaçlanan kullanım durumudur . Başlık altında, Apache Arrow veri düzenini (sıfır kopya formatı) kullanarak nesneleri serileştirir ve kopyalar oluşturmadan birden çok işlemle erişilebilmeleri için bunları paylaşılan bir bellek nesne deposunda saklar .

Kod aşağıdaki gibi görünecektir.

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)

Arama yapmazsanız ray.put, dizi yine de paylaşılan bellekte saklanacaktır, ancak bu her çağrı için bir kez yapılacaktır func, ki bu istediğiniz şey değildir.

Bunun sadece diziler için değil, aynı zamanda diziler içeren nesneler için de işe yarayacağına dikkat edin , örneğin, aşağıdaki gibi dizileri dizilere eşleyen sözlükler.

Aşağıdakileri IPython'da çalıştırarak Ray'deki serileştirmenin performansını turşu ile karşılaştırabilirsiniz.

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

Ray ile serileştirme turşudan yalnızca biraz daha hızlıdır, ancak serileştirme, paylaşılan belleğin kullanımı nedeniyle 1000 kat daha hızlıdır (bu sayı elbette nesneye bağlı olacaktır).

Ray belgelerine bakın . Ray ve Arrow'u kullanarak hızlı serileştirme hakkında daha fazla bilgi edinebilirsiniz . Not Ray geliştiricilerinden biriyim.


1
Ray kulağa hoş geliyor! Ama daha önce bu kitaplığı kullanmayı denedim, ancak ne yazık ki Ray'in pencereleri desteklemediğini fark ettim. Umarım en kısa sürede pencereleri destekleyebilirsiniz. Teşekkürler geliştiriciler!
Hzzkygcs

6

Robert Nishihara'nın bahsettiği gibi, Apache Arrow bunu, özellikle Ray'in üzerine kurulu olduğu Plasma bellek içi nesne deposu ile kolaylaştırıyor.

Beyin plazmasını özellikle bu nedenle yaptım - bir Flask uygulamasında büyük nesnelerin hızlı yüklenmesi ve yeniden yüklenmesi. Bu, Apache Arrow-serileştirilebilir nesneler için paylaşılan bellek nesnesi ad picklealanıdır , bunlara 'd bytestrings dahil pickle.dumps(...).

Apache Ray ve Plasma'nın temel farkı, sizin için nesne kimliklerini takip etmesidir. Yerel olarak çalışan herhangi bir süreç veya iş parçacığı veya program, herhangi bir Brainnesneden adı çağırarak değişkenlerin değerlerini paylaşabilir .

$ 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,...]
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.