簡體   English   中英

如何提高 Python 中並行循環的效率

[英]How to improve efficiency on parallel loops in Python

我很好奇 Python 中的並行循環與 Matlab 中的parloop相比效率有多低。 在這里,我提出了一個簡單的尋根問題,在ab之間強制進行初始 10^6 初始猜測。

import numpy as np
from scipy.optimize import root
import matplotlib.pyplot as plt
import multiprocessing

# define the function to find the roots
func = lambda x: np.sin(3*np.pi*np.cos(np.pi*x)*np.sin(np.pi*x))

def forfunc(x0):
    q = [root(func, xi).x for xi in x0]
    q = np.array(q).T[0]
    return q

# variables os the problem
a = -3
b = 5
n = int(1e6)
x0 = np.linspace(a,b,n) # list of initial guesses

# the single-process loop
q = forfunc(x0)

# parallel loop
nc = 4
pool = multiprocessing.Pool(processes=nc)
q = np.hstack(pool.map(forfunc,np.split(x0,nc)))
pool.close()

單進程循環需要 1 分 26 秒的時間,並行循環需要 1 分 7 秒。 我看到一些改進,因為加速為 1.28,但在這種情況下效率(timeloop/timeparallel/n_process)為 0.32。

這里發生了什么以及如何提高這種效率? 難道我做錯了什么?

我還嘗試通過兩種方式使用dask.delayed

import dask

# Every call is a delayed object
q = dask.compute(*[dask.delayed(func)(xi) for xi in x0])

# Every chunk is a delayed object
q = dask.compute(*[dask.delayed(forfunc)(x0i) for x0i in np.split(x0,nc)])

並且這里兩者都比單進程循環花費更多的時間。 第一次嘗試的牆上時間是 3 分鍾,第二次嘗試用了 1 分 27 秒。

Dask(或 Spark)發生了什么

從您的單進程測試來看,您的循環在 90 秒內執行了 100 萬個任務。 因此,在平均情況下,每個任務占用 CPU 大約 90 微秒。

在 Dask 或 Spark 等提供靈活性和彈性的分布式計算框架中,任務的相關開銷很小。 Dask 的開銷低至每個任務200 微秒 Spark 3.0 文檔表明 Spark 可以支持短至 200毫秒的任務,這可能意味着 Dask 的開銷實際上比 Spark 少 1000 倍。 聽起來 Dask 實際上在這里做得非常好!

如果您的任務比框架的每個任務開銷更快,則相對於在相同數量的機器/內核上手動分配您的工作,您只會看到使用它的性能更差。 在這種情況下,您會遇到這種情況。

在分塊數據 Dask 示例中,您只有幾個任務,因此您可以通過減少開銷看到更好的性能。 但是,相對於原始多處理,您可能會從 Dask 的開銷中獲得較小的性能損失,或者您沒有使用 Dask 集群並在單個進程中運行任務。

多處理(和 Dask)應該有幫助

對於這種令人尷尬的並行問題,您的多處理結果通常是出乎意料的。 您可能需要確認機器上的物理內核數量,特別是確保沒有其他東西正在積極使用您的 CPU 內核。 在不知道其他任何事情的情況下,我猜這就是罪魁禍首。

在我有兩個物理內核的筆記本電腦上,您的示例采用:

  • 單進程循環 2min 1s
  • 兩道工序1分2秒
  • 四道工序1分鍾
  • 對於nc=2的分塊 Dask 示例,需要 1 分鍾 5 秒才能拆分為兩個塊和一個由兩個工作人員組成的 LocalCluster,每個工作人員有一個線程。 可能值得仔細檢查您是否在集群上運行。

通過兩個進程獲得大約 2 倍的加速符合我的筆記本電腦的預期,因為對於這個 CPU 密集型任務,從更多進程中看到的好處很少或沒有好處。 Dask 還增加了一些相對於原始多處理的開銷。

%%time
​
# the single-process loop
q = forfunc(x0)
CPU times: user 1min 55s, sys: 1.68 s, total: 1min 57s
Wall time: 2min 1s
%%time
​
# parallel loop
nc = 2
pool = multiprocessing.Pool(processes=nc)
q = np.hstack(pool.map(forfunc,np.split(x0,nc)))
pool.close()
CPU times: user 92.6 ms, sys: 70.8 ms, total: 163 ms
Wall time: 1min 2s
%%time
​
# parallel loop
nc = 4
pool = multiprocessing.Pool(processes=nc)
q = np.hstack(pool.map(forfunc,np.split(x0,nc)))
pool.close()
CPU times: user 118 ms, sys: 94.6 ms, total: 212 ms
Wall time: 1min
from dask.distributed import Client, LocalCluster, wait
client = Client(n_workers=2, threads_per_worker=1)

%%time
​
nc = 2
chunks = np.split(x0,nc)
client.scatter(chunks, broadcast=True)
q = client.compute([dask.delayed(forfunc)(x0i) for x0i in chunks])
wait(q)
/Users/nickbecker/miniconda3/envs/prophet/lib/python3.7/site-packages/distributed/worker.py:3382: UserWarning: Large object of size 4.00 MB detected in task graph: 
  (array([1.000004, 1.000012, 1.00002 , ..., 4.99998 ... 2, 5.      ]),)
Consider scattering large objects ahead of time
with client.scatter to reduce scheduler burden and 
keep data on workers

    future = client.submit(func, big_data)    # bad

    big_future = client.scatter(big_data)     # good
    future = client.submit(func, big_future)  # good
  % (format_bytes(len(b)), s)
CPU times: user 3.67 s, sys: 324 ms, total: 4 s
Wall time: 1min 5s

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM