วัตถุหน่วยความจำที่ใช้ร่วมกันในการประมวลผลหลายขั้นตอน


124

สมมติว่าฉันมีอาร์เรย์จำนวนมากในหน่วยความจำฉันมีฟังก์ชันfuncที่รับอาร์เรย์ยักษ์นี้เป็นอินพุต (พร้อมกับพารามิเตอร์อื่น ๆ ) funcด้วยพารามิเตอร์ที่แตกต่างกันสามารถทำงานแบบขนานได้ ตัวอย่างเช่น:

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]

ถ้าฉันใช้ไลบรารีการประมวลผลหลายขั้นตอนอาร์เรย์ขนาดยักษ์นั้นจะถูกคัดลอกหลายครั้งในกระบวนการต่างๆ

มีวิธีที่จะให้กระบวนการต่างๆใช้อาร์เรย์เดียวกันได้หรือไม่? วัตถุอาร์เรย์นี้เป็นแบบอ่านอย่างเดียวและจะไม่มีการแก้ไข

จะมีอะไรซับซ้อนไปกว่านั้นถ้า arr ไม่ใช่อาร์เรย์ แต่เป็นวัตถุ python โดยพลการมีวิธีแบ่งปันหรือไม่?

[แก้ไข]

ฉันอ่านคำตอบแล้ว แต่ฉันก็ยังสับสนเล็กน้อย เนื่องจาก fork () เป็นแบบ copy-on-write เราจึงไม่ควรเรียกร้องค่าใช้จ่ายเพิ่มเติมใด ๆ เมื่อสร้างกระบวนการใหม่ใน python multrocessing library แต่รหัสต่อไปนี้แสดงให้เห็นว่ามีค่าใช้จ่ายมาก:

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;

เอาท์พุท (และโดยวิธีนี้ต้นทุนจะเพิ่มขึ้นเมื่อขนาดของอาร์เรย์เพิ่มขึ้นดังนั้นฉันจึงสงสัยว่ายังมีค่าใช้จ่ายที่เกี่ยวข้องกับการคัดลอกหน่วยความจำ):

construct array =  0.0178790092468
multiprocessing overhead =  0.252444982529

เหตุใดจึงมีค่าใช้จ่ายมหาศาลเช่นนี้ถ้าเราไม่ได้คัดลอกอาร์เรย์ และหน่วยความจำที่ใช้ร่วมกันบันทึกฉันไว้ในส่วนใด



คุณดูเอกสารแล้วใช่ไหม
Lev Levitsky

@FrancisAvila มีวิธีในการแบ่งปันไม่ใช่แค่อาร์เรย์เท่านั้น แต่ยังมีวัตถุหลามโดยพลการ?
Vendetta

1
@LevLevitsky ฉันต้องถามมีวิธีแชร์ไม่ใช่แค่อาร์เรย์ แต่เป็นวัตถุหลามโดยพลการ?
Vendetta

2
คำตอบนี้อธิบายได้เป็นอย่างดีว่าเหตุใดจึงไม่สามารถแชร์วัตถุ Python ตามอำเภอใจได้
Janne Karila

คำตอบ:


121

หากคุณใช้ระบบปฏิบัติการที่ใช้ความหมายแบบ copy-on-write fork()(เช่นเดียวกับ unix ทั่วไป) ตราบใดที่คุณไม่เคยเปลี่ยนโครงสร้างข้อมูลของคุณก็จะพร้อมใช้งานสำหรับกระบวนการย่อยทั้งหมดโดยไม่ต้องใช้หน่วยความจำเพิ่มเติม คุณจะไม่ต้องทำอะไรเป็นพิเศษ (ยกเว้นให้แน่ใจว่าคุณไม่ได้เปลี่ยนวัตถุ)

สิ่งที่มีประสิทธิภาพที่สุดที่คุณสามารถทำได้สำหรับปัญหาของคุณคือการบรรจุอาร์เรย์ของคุณลงในโครงสร้างอาร์เรย์ที่มีประสิทธิภาพ (โดยใช้numpyหรือarray) วางไว้ในหน่วยความจำที่ใช้ร่วมกันห่อด้วยmultiprocessing.Arrayและส่งผ่านไปยังฟังก์ชันของคุณ คำตอบนี้แสดงให้เห็นถึงวิธีการที่จะทำเช่นนั้น

หากคุณต้องการวัตถุที่แชร์ที่เขียนได้คุณจะต้องห่อด้วยการซิงโครไนซ์หรือการล็อกบางประเภท multiprocessingมีสองวิธีในการดำเนินการนี้ : วิธีหนึ่งใช้หน่วยความจำที่ใช้ร่วมกัน (เหมาะสำหรับค่าธรรมดาอาร์เรย์หรือ ctypes) หรือManagerพร็อกซีโดยที่กระบวนการหนึ่งเก็บหน่วยความจำและผู้จัดการจะตัดสินว่าสามารถเข้าถึงได้จากกระบวนการอื่น (แม้ผ่านเครือข่าย)

Managerวิธีการที่สามารถใช้กับวัตถุพลหลาม แต่จะช้ากว่าเทียบเท่าหน่วยความจำที่ใช้ร่วมกันโดยใช้เพราะวัตถุที่จะต้องมีการต่อเนื่อง / deserialized และส่งระหว่างกระบวนการ

มีอยู่มากมายของห้องสมุดประมวลผลแบบขนานวิธีการที่มีอยู่ในหลามและ multiprocessingเป็นห้องสมุดที่ยอดเยี่ยมและโค้งมน แต่ถ้าคุณมีความต้องการพิเศษวิธีใดวิธีหนึ่งอาจจะดีกว่า


25
โปรดทราบว่าใน Python fork () หมายถึงการคัดลอกในการเข้าถึง (เพราะการเข้าถึงวัตถุจะเปลี่ยนจำนวนการอ้างอิง)
Fabio Zadrozny

3
@FabioZadrozny มันจะคัดลอกวัตถุทั้งหมดจริงหรือเพียงแค่หน้าหน่วยความจำที่มีการอ้างอิง?
zigg

5
AFAIK เฉพาะเพจหน่วยความจำที่มีการอ้างอิง (ดังนั้น 4kb ในการเข้าถึงแต่ละวัตถุ)
Fabio Zadrozny

1
@max ใช้การปิด ฟังก์ชันที่กำหนดให้apply_asyncควรอ้างอิงอ็อบเจ็กต์ที่แชร์ในขอบเขตโดยตรงแทนที่จะใช้อาร์กิวเมนต์
Francis Avila

3
@FrancisAvila คุณใช้วิธีปิดอย่างไร? ฟังก์ชั่นที่คุณให้กับ apply_async ไม่ควรเลือก? หรือนี่เป็นเพียงข้อ จำกัด map_async เท่านั้น?
GermanK

17

ฉันพบปัญหาเดียวกันและเขียนคลาสยูทิลิตี้หน่วยความจำที่ใช้ร่วมกันเล็กน้อยเพื่อแก้ไขปัญหานี้

ฉันกำลังใช้multiprocessing.RawArray(lockfree) และการเข้าถึงอาร์เรย์ไม่ได้รับการซิงโครไนซ์เลย (ไม่มีล็อก) ระวังอย่ายิงเท้าของคุณเอง

ด้วยวิธีแก้ปัญหาฉันได้รับ speedups โดยประมาณ 3 ปัจจัยบน quad-core i7

นี่คือรหัส: อย่าลังเลที่จะใช้และปรับปรุงและโปรดรายงานข้อบกพร่องใด ๆ กลับมา

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

เพิ่งรู้ว่าคุณต้องตั้งค่าอาร์เรย์หน่วยความจำที่ใช้ร่วมกันของคุณก่อนที่คุณจะสร้าง Multiprocessing Pool ไม่รู้ว่าทำไม แต่มันจะไม่ทำงานในทางกลับกันแน่นอน
martin.preinfalk

สาเหตุที่พูลการประมวลผลหลายตัวเรียกใช้ fork () เมื่อพูลถูกสร้างอินสแตนซ์ดังนั้นสิ่งใดก็ตามหลังจากนั้นจะไม่สามารถเข้าถึงตัวชี้ไปยัง mem ที่แชร์ที่สร้างขึ้นในภายหลัง
Xiv

เมื่อฉันลองใช้รหัสนี้ภายใต้ py35 ฉันได้รับข้อยกเว้นในการประมวลผลหลายขั้นตอนการแบ่งปันไทป์ดังนั้นฉันเดาว่ารหัสนี้ใช้สำหรับ py2 เท่านั้น
Dr. Hillier Dániel

11

นี่เป็นกรณีการใช้งานที่ตั้งใจไว้สำหรับRayซึ่งเป็นไลบรารีสำหรับ Python แบบขนานและแบบกระจาย ภายใต้ฝากระโปรงจะทำให้วัตถุเป็นอนุกรมโดยใช้เค้าโครงข้อมูลApache Arrow (ซึ่งเป็นรูปแบบศูนย์สำเนา) และเก็บไว้ในที่เก็บอ็อบเจ็กต์หน่วยความจำแบบแบ่งใช้เพื่อให้สามารถเข้าถึงได้โดยกระบวนการต่างๆโดยไม่ต้องสร้างสำเนา

รหัสจะมีลักษณะดังต่อไปนี้

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)

หากคุณไม่เรียกray.putอาร์เรย์จะยังคงถูกเก็บไว้ในหน่วยความจำที่ใช้ร่วมกัน แต่จะทำครั้งเดียวต่อการเรียกใช้funcซึ่งไม่ใช่สิ่งที่คุณต้องการ

โปรดทราบว่าการดำเนินการนี้ไม่เพียง แต่ใช้ได้กับอาร์เรย์เท่านั้น แต่ยังใช้ได้กับอ็อบเจ็กต์ที่มีอาร์เรย์เช่นพจนานุกรมการแมป ints กับอาร์เรย์ดังต่อไปนี้

คุณสามารถเปรียบเทียบประสิทธิภาพของการทำให้เป็นอนุกรมใน Ray กับ Pickle ได้โดยเรียกใช้สิ่งต่อไปนี้ใน IPython

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 นั้นเร็วกว่าการดองเพียงเล็กน้อย แต่การดีซีเรียลไลเซชั่นนั้นเร็วกว่า 1,000 เท่าเนื่องจากการใช้หน่วยความจำที่ใช้ร่วมกัน (แน่นอนว่าจำนวนนี้จะขึ้นอยู่กับวัตถุ)

ดูเอกสารเรย์ คุณสามารถอ่านเพิ่มเติมเกี่ยวกับการเป็นอันดับอย่างรวดเร็วโดยใช้เรย์และลูกศร หมายเหตุฉันเป็นหนึ่งในผู้พัฒนา Ray


1
เรย์เสียงดี! แต่ฉันเคยลองใช้ไลบรารีนี้มาก่อน แต่น่าเสียดายที่ฉันเพิ่งรู้ว่า Ray ไม่รองรับ windows ฉันหวังว่าพวกคุณจะสามารถสนับสนุน windows ASAP ได้ ขอบคุณนักพัฒนา!
Hzzkygcs

6

เช่นเดียวกับที่ Robert Nishihara กล่าวถึง Apache Arrow ทำให้เรื่องนี้ง่ายขึ้นโดยเฉพาะกับที่เก็บวัตถุในหน่วยความจำ Plasma ซึ่งเป็นสิ่งที่ Ray สร้างขึ้น

ฉันทำพลาสมาสมองโดยเฉพาะด้วยเหตุผลนี้ - โหลดและโหลดวัตถุขนาดใหญ่ซ้ำได้อย่างรวดเร็วในแอป Flask มันเป็นเนมสเปซออบเจ็กต์หน่วยความจำที่ใช้ร่วมกันสำหรับอ็อบเจ็กต์ Apache Arrow-serializable รวมถึงpickle'd bytestrings ที่สร้างโดยpickle.dumps(...).

ความแตกต่างที่สำคัญของ Apache Ray และ Plasma คือการติดตาม ID วัตถุสำหรับคุณ กระบวนการหรือเธรดหรือโปรแกรมใด ๆ ที่รันบนโลคัลสามารถแชร์ค่าของตัวแปรโดยการเรียกชื่อจากBrainอ็อบเจ็กต์ใด ๆ

$ 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,...]
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.