จะเขียนโปรแกรมแบบขนานใน Python ได้อย่างไร


141

สำหรับ C ++ เราสามารถใช้ OpenMP เพื่อทำการเขียนโปรแกรมแบบขนาน อย่างไรก็ตาม OpenMP จะไม่ทำงานสำหรับ Python ฉันควรทำอย่างไรถ้าฉันต้องการขนานบางส่วนของโปรแกรมไพ ธ อนของฉัน

โครงสร้างของรหัสอาจพิจารณาเป็น:

solve1(A)
solve2(B)

ที่ไหนsolve1และsolve2มีสองฟังก์ชั่นอิสระ วิธีการเรียกใช้รหัสนี้ในแบบคู่ขนานแทนในลำดับเพื่อลดเวลาทำงาน? หวังว่าใครบางคนสามารถช่วยฉันได้ ขอบคุณมากล่วงหน้า รหัสคือ:

def solve(Q, G, n):
    i = 0
    tol = 10 ** -4

    while i < 1000:
        inneropt, partition, x = setinner(Q, G, n)
        outeropt = setouter(Q, G, n)

        if (outeropt - inneropt) / (1 + abs(outeropt) + abs(inneropt)) < tol:
            break

        node1 = partition[0]
        node2 = partition[1]

        G = updateGraph(G, node1, node2)

        if i == 999:
            print "Maximum iteration reaches"
    print inneropt

โดยที่ setinner และ setouter เป็นฟังก์ชันอิสระสองฟังก์ชัน นั่นคือสิ่งที่ฉันต้องการขนาน ...


31
ลองดูที่multiprocessing หมายเหตุ: เธรดของ Python ไม่เหมาะสำหรับงานที่ผูกกับ CPU เฉพาะสำหรับ I / O-bound
9000

4
@ 9000 +100 internets เพื่อพูดถึงงานที่ต้องพึ่งพา CPU vs I / O
Hyperboreus

@ 9000 จริงๆแล้วเธรดไม่เหมาะสำหรับงานที่ใช้ CPU เท่าที่ฉันรู้! กระบวนการคือหนทางที่จะดำเนินไปเมื่อทำภาระผูกพันกับ CPU จริง
Omar Al-Ithawi

6
@OmarIthawi: ทำไมกระทู้ทำงานได้ดีถ้าคุณมีคอร์ CPU จำนวนมาก (ตามปกติตอนนี้) จากนั้นกระบวนการของคุณสามารถรันหลายเธรดโหลดแกนทั้งหมดเหล่านี้ในแบบคู่ขนานและแบ่งปันข้อมูลทั่วไประหว่างพวกเขาโดยปริยาย (นั่นคือโดยไม่ต้องมีพื้นที่หน่วยความจำที่ใช้ร่วมกันอย่างชัดเจนหรือการส่งข้อความระหว่างกระบวนการ)
9000

1
@ user2134774: ใช่แล้วความคิดเห็นที่สองของฉันสมเหตุสมผลเล็กน้อย อาจเป็นเพียงส่วนขยาย C ที่เปิดตัว GIL เท่านั้นที่จะได้รับประโยชน์จากส่วนขยายนั้น เช่นบางส่วนของ NumPy และ Pandas ทำเช่นนั้น ในกรณีอื่น ๆ มันผิด (แต่ฉันไม่สามารถแก้ไขได้ตอนนี้)
9000

คำตอบ:


162

คุณสามารถใช้โมดูลมัลติโปรเซสเซอร์ สำหรับกรณีนี้ฉันอาจใช้กลุ่มการประมวลผล:

from multiprocessing import Pool
pool = Pool()
result1 = pool.apply_async(solve1, [A])    # evaluate "solve1(A)" asynchronously
result2 = pool.apply_async(solve2, [B])    # evaluate "solve2(B)" asynchronously
answer1 = result1.get(timeout=10)
answer2 = result2.get(timeout=10)

สิ่งนี้จะวางไข่กระบวนการที่สามารถทำงานทั่วไปให้คุณได้ เนื่องจากเราไม่ได้ผ่านprocessesมันจะวางไข่หนึ่งกระบวนการสำหรับแต่ละคอร์ CPU ในเครื่องของคุณ ซีพียูแต่ละคอร์สามารถดำเนินการหนึ่งกระบวนการพร้อมกัน

หากคุณต้องการแมปรายการกับฟังก์ชั่นเดียวคุณจะทำสิ่งนี้:

args = [A, B]
results = pool.map(solve1, args)

อย่าใช้เธรดเนื่องจากGILล็อคการดำเนินการกับวัตถุหลาม


1
ไม่pool.mapยังยอมรับพจนานุกรมเป็น args? หรือรายการง่าย ๆ เท่านั้น?
Bndr

ฉันคิดว่ารายการ แต่คุณสามารถผ่านใน dict.items () ซึ่งจะเป็นรายการของสิ่งอันดับสำคัญ
Matt Williamson

น่าเสียดายที่สิ่งนี้จบลงด้วยข้อผิดพลาดประเภท `unhashable: 'list'`
The Bndr

นอกจากความคิดเห็นสุดท้ายของฉัน: `dict.items ()` งาน ข้อผิดพลาดเกิดขึ้นเพราะฉันต้องเปลี่ยนการจัดการความเข้าใจด้านตัวแปรของกระบวนการทำงาน น่าเสียดายที่ข้อความแสดงข้อผิดพลาดไม่ได้มีประโยชน์มาก ... ดังนั้น: ขอบคุณสำหรับคำแนะนำของคุณ :-)
The Bndr

2
การหมดเวลาที่นี่คืออะไร
gamma

26

ซึ่งสามารถทำได้สวยงามมากกับเรย์

การคู่ขนานตัวอย่างของคุณคุณจะต้องกำหนดฟังก์ชั่นของคุณด้วยมัณฑนากรและจากนั้นเรียกพวกเขาด้วย@ray.remote.remote

import ray

ray.init()

# Define the functions.

@ray.remote
def solve1(a):
    return 1

@ray.remote
def solve2(b):
    return 2

# Start two tasks in the background.
x_id = solve1.remote(0)
y_id = solve2.remote(1)

# Block until the tasks are done and get the results.
x, y = ray.get([x_id, y_id])

มีข้อดีหลายประการบนโมดูลมัลติโพรเซสเซอร์

  1. รหัสเดียวกันจะทำงานบนเครื่องมัลติคอร์รวมถึงคลัสเตอร์ของเครื่อง
  2. กระบวนการใช้ข้อมูลร่วมกันได้อย่างมีประสิทธิภาพผ่านหน่วยความจำที่ใช้ร่วมกันและเป็นศูนย์สำเนาอนุกรม
  3. ข้อความผิดพลาดจะแพร่กระจายอย่างดี
  4. การเรียกใช้ฟังก์ชันเหล่านี้สามารถรวมเข้าด้วยกันเช่น

    @ray.remote
    def f(x):
        return x + 1
    
    x_id = f.remote(1)
    y_id = f.remote(x_id)
    z_id = f.remote(y_id)
    ray.get(z_id)  # returns 4
  5. นอกเหนือจากฟังก์ชั่นการเรียกใช้จากระยะไกลแล้วคลาสยังสามารถสร้างอินสแตนซ์จากระยะไกลในฐานะนักแสดงได้

โปรดทราบว่าเรย์เป็นกรอบที่ฉันได้ช่วยพัฒนา


ฉันได้รับข้อผิดพลาดที่แจ้งว่า "ไม่พบรุ่นที่ตรงตามข้อกำหนด ray (จากรุ่น
:)

2
pipมักจะหมายถึงชนิดของข้อผิดพลาดนี้ที่คุณจะต้องอัพเกรด pip install --upgrade pipผมขอแนะนำให้พยายาม หากคุณจำเป็นต้องใช้sudoเลยอาจเป็นไปได้ว่าเวอร์ชั่นpipที่คุณใช้ติดตั้งrayนั้นไม่ใช่รุ่นเดียวกับที่กำลังอัพเกรด pip --versionคุณสามารถตรวจสอบกับ นอกจากนี้ Windows ยังไม่รองรับดังนั้นหากคุณใช้ Windows ซึ่งอาจเป็นปัญหา
Robert Nishihara

1
เพียงแค่ทราบว่านี่คือการกระจายงานที่เกิดขึ้นพร้อมกันในหลาย ๆ เครื่องเป็นหลัก
Matt Williamson

2
มันถูกปรับให้เหมาะสมสำหรับทั้งเคสเครื่องเดียวและการตั้งค่าคลัสเตอร์ การตัดสินใจในการออกแบบจำนวนมาก (เช่นหน่วยความจำที่ใช้ร่วมกันการทำให้เป็นศูนย์แบบคัดลอกเป็นศูนย์) มีเป้าหมายเพื่อสนับสนุนเครื่องเดียวได้ดี
Robert Nishihara

2
มันคงจะดีถ้าเอกสารชี้ให้เห็นมากกว่านั้น ฉันเข้าใจได้จากการอ่านเอกสารที่ไม่ได้มีไว้สำหรับเคสเครื่องเดียว
เลื่อน

4

CPython ใช้ Global Interpreter Lock ซึ่งทำให้การวางโปรแกรมแบบขนานมีความน่าสนใจมากกว่า C ++

หัวข้อนี้มีตัวอย่างที่เป็นประโยชน์และคำอธิบายของความท้าทายหลายประการ:

วิธีแก้ปัญหา Python Global Interpreter Lock (GIL) บนระบบมัลติคอร์โดยใช้ชุดงานบน Linux?


13
คุณเรียกความไม่สามารถในการเรียกใช้รหัสจริงพร้อมกัน "น่าสนใจ" หรือไม่? : - /
ManuelSchneid3r

4

วิธีการแก้ปัญหาดังที่คนอื่นได้กล่าวไว้คือการใช้หลายกระบวนการ อย่างไรก็ตามกรอบใดที่เหมาะสมกว่านั้นขึ้นอยู่กับหลายปัจจัย นอกจากที่กล่าวมาแล้วยังมีcharm4pyและmpi4py (ฉันเป็นผู้พัฒนา charm4py)

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

ตัวอย่างเช่นใน charm4py สามารถทำได้ดังนี้:

class Worker(Chare):

    def __init__(self, Q, G, n):
        self.G = G
        ...

    def setinner(self, node1, node2):
        self.updateGraph(node1, node2)
        ...


def solve(Q, G, n):
    # create 2 workers, each on a different process, passing the initial state
    worker_a = Chare(Worker, onPE=0, args=[Q, G, n])
    worker_b = Chare(Worker, onPE=1, args=[Q, G, n])
    while i < 1000:
        result_a = worker_a.setinner(node1, node2, ret=True)  # execute setinner on worker A
        result_b = worker_b.setouter(node1, node2, ret=True)  # execute setouter on worker B

        inneropt, partition, x = result_a.get()  # wait for result from worker A
        outeropt = result_b.get()  # wait for result from worker B
        ...

โปรดทราบว่าสำหรับตัวอย่างนี้เราต้องการเพียงหนึ่งคนเท่านั้น การวนซ้ำหลักสามารถดำเนินการหนึ่งในฟังก์ชันและให้ผู้ปฏิบัติงานดำเนินการอื่น แต่รหัสของฉันช่วยในการอธิบายสองสิ่ง:

  1. ผู้ปฏิบัติงาน A ทำงานในกระบวนการ 0 (เหมือนกับวนรอบหลัก) ในขณะที่result_a.get()ถูกบล็อกรอผลผลลัพธ์ผู้ปฏิบัติงาน A ทำการคำนวณในกระบวนการเดียวกัน
  2. อาร์กิวเมนต์จะถูกส่งโดยอัตโนมัติโดยอ้างอิงถึงผู้ปฏิบัติงาน A เนื่องจากอยู่ในกระบวนการเดียวกัน (ไม่มีการคัดลอกที่เกี่ยวข้อง)

2

ในบางกรณีเป็นไปได้ที่จะใช้ลูปขนานโดยอัตโนมัติโดยใช้Numbaแม้ว่าจะใช้งานได้กับชุดย่อยของ Python เพียงเล็กน้อยเท่านั้น:

from numba import njit, prange

@njit(parallel=True)
def prange_test(A):
    s = 0
    # Without "parallel=True" in the jit-decorator
    # the prange statement is equivalent to range
    for i in prange(A.shape[0]):
        s += A[i]
    return s

น่าเสียดายที่ดูเหมือนว่า Numba ใช้งานได้กับ Numpy Array เท่านั้น แต่ไม่ใช่กับวัตถุ Python อื่น ๆ ในทางทฤษฎีแล้วมันอาจจะเป็นไปได้ที่จะคอมไพล์ Python ถึง C ++และขนานมันโดยอัตโนมัติโดยใช้ Intel C ++ คอมไพเลอร์แม้ว่าฉันยังไม่ได้ลองเลย


2

คุณสามารถใช้joblibไลบรารีเพื่อทำการคำนวณแบบขนานและมัลติโปรเซสเซอร์

from joblib import Parallel, delayed

คุณสามารถสร้างฟังก์ชั่นfooที่คุณต้องการให้ทำงานแบบขนานและใช้โค้ดต่อไปนี้เพื่อประมวลผลแบบขนาน:

output = Parallel(n_jobs=num_cores)(delayed(foo)(i) for i in input)

ในกรณีที่num_coresสามารถหาได้จากmultiprocessingห้องสมุดดังต่อไปนี้:

import multiprocessing

num_cores = multiprocessing.cpu_count()

หากคุณมีฟังก์ชั่นที่มีมากกว่าหนึ่งอาร์กิวเมนต์โต้แย้งและคุณต้องการที่จะทำซ้ำมากกว่าหนึ่งอาร์กิวเมนต์โดยรายการคุณสามารถใช้partialฟังก์ชั่นจากfunctoolsห้องสมุดดังนี้

from joblib import Parallel, delayed
import multiprocessing
from functools import partial
def foo(arg1, arg2, arg3, arg4):
    '''
    body of the function
    '''
    return output
input = [11,32,44,55,23,0,100,...] # arbitrary list
num_cores = multiprocessing.cpu_count()
foo_ = partial(foo, arg2=arg2, arg3=arg3, arg4=arg4)
# arg1 is being fetched from input list
output = Parallel(n_jobs=num_cores)(delayed(foo_)(i) for i in input)

คุณสามารถหาคำอธิบายที่สมบูรณ์ของงูหลามและ R multiprocessing กับคู่ของตัวอย่างที่นี่

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