在分布式调度程序

时间:2017-05-20 08:50:16

标签: python multithreading multiprocessing distributed dask

Dask示例,与线程调度程序相比,具有进程的调度程序需要过多的内存

您好,

我遇到了一个dask.array示例,其中计算时间和所需内存在线程(共享内存)调度程序(dask.getdask.threaded.get)和具有工作进程的调度程序({{ 1}},dask.multiprocessing.get

我在带有SSD的2013 Core i7 16GB macOS Macbook Pro上测试了这个设置。

手头的模拟实现了长度为distributed.Client().get的多个ndims向量之间的网格网格操作,然后对它们执行一些简单的元素运算,然后通过在每个维度中求和得到最终结果。没有dimlen,示例就是这样,网格数组的副本将与dask一样大。由于我们有8*(dimlen**ndims)/1024**3 = 7.4 GByte个参数,如果所有参数都是通过网格参数的简单副本完成的,那么我们需要超过16 GB的RAM。 (顺便说一下:如果通过ndim = 3接近示例,则numpynumpy.broadcast_to无论如何都不会创建完整副本。仅在创建numpy.transpose完整数组{时{1}}将被分配。)

到目前为止,据我所知,多处理和分布式调度程序的减速是由于大量的RAM消耗和一些写入磁盘的任务(在分布式诊断网页中看到)。但是我无法解释这种行为,因为我目前对要计算的dask图的理解是:

使用res的想法是通过使用7.4 GByte分块来减少内存需求。假设dask,一个网格参数块块的副本的最大量为dask.array。同样在缩小操作(一次一个维度)期间,当必须连接块块时,我们应该最终得到8*(chunklen**ndims)/1024**2 = 7.6 MByte的最大块。假设我们有4个进程,那么在任何给定时间我们只需要提到的块大小的四倍。然而,资源监控显示,对于涉及各种过程的调度程序,总共燃烧了16GB RAM。

我希望能够更深入地解释我在这里错过了什么。

提前致谢Markus

float64

设置环境

使用

创建的Conda环境
8*(dimlen/chunklen)*(chunklen**ndims)/1024**2 = 76 MByte

进一步跟进@ kakk11建议

Hello @ kakk11,

非常感谢你的回答和努力。我已经对它进行了进一步的调查,如果我将您的示例增加到我的问题大小,即import numpy as np import dask.array as da from dask import get as single_threaded_get from dask.threaded import get as threaded_get from dask.multiprocessing import get as multiprocessing_get ndims = 3 dimlen = 1000 chunklen = 100 # Some input data, which usually would come elsewhere xs = [np.random.randn(dimlen) for _ in range(ndims)] # Cast them to dask.array ys = [da.from_array(x, chunks=chunklen) for x in xs] # Meshgrid zs = [da.broadcast_to(y, ndims*(dimlen,)) for y in ys] zs = [da.rechunk(z, chunks=ndims*(chunklen,)) for z in zs] _a = tuple(range(ndims)) zs = [da.transpose(z, axes=_a[i:] + _a[:i]) for i, z in enumerate(zs)] # Some simple element-wise processing of n dimensional arguments res = zs[0] for z in zs[1:]: res = res + z # Some reduction of all dimensions to a scalar for i in range(ndims): res = da.sum(res, axis=-1) res dask.array<sum-aggregate, shape=(), dtype=float64, chunksize=()> len(list(res.dask.keys())) #12617 %%timeit -n 1 -r 1 x = res.compute(get=single_threaded_get) #10.4 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each) %%timeit -n 1 -r 1 x = res.compute(get=threaded_get) #7.32 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each) %%timeit -n 1 -r 1 x = res.compute(get=multiprocessing_get) #5h 14min 52s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each) from distributed import Client client = Client() %%timeit -n 1 -r 1 x = res.compute(get=client.get) #7min 37s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each) $conda create -n py35 python=3.5 dask distributed jupyter $source activate py35 $jupyter notebook . !conda list -e # This file may be used to create an environment using: # $ conda create --name <env> --file <this file> # platform: osx-64 appnope=0.1.0=py35_0 bleach=1.5.0=py35_0 bokeh=0.12.5=py35_1 chest=0.2.3=py35_0 click=6.7=py35_0 cloudpickle=0.2.2=py35_0 dask=0.14.3=py35_0 decorator=4.0.11=py35_0 distributed=1.16.3=py35_0 entrypoints=0.2.2=py35_1 heapdict=1.0.0=py35_1 html5lib=0.999=py35_0 icu=54.1=0 ipykernel=4.6.1=py35_0 ipython=6.0.0=py35_1 ipython_genutils=0.2.0=py35_0 ipywidgets=6.0.0=py35_0 jedi=0.10.2=py35_2 jinja2=2.9.6=py35_0 jsonschema=2.6.0=py35_0 jupyter=1.0.0=py35_3 jupyter_client=5.0.1=py35_0 jupyter_console=5.1.0=py35_0 jupyter_core=4.3.0=py35_0 locket=0.2.0=py35_1 markupsafe=0.23=py35_2 mistune=0.7.4=py35_0 mkl=2017.0.1=0 msgpack-python=0.4.8=py35_0 nbconvert=5.1.1=py35_0 nbformat=4.3.0=py35_0 notebook=5.0.0=py35_0 numpy=1.12.1=py35_0 openssl=1.0.2k=2 pandas=0.20.1=np112py35_0 pandocfilters=1.4.1=py35_0 partd=0.3.8=py35_0 path.py=10.3.1=py35_0 pexpect=4.2.1=py35_0 pickleshare=0.7.4=py35_0 pip=9.0.1=py35_1 prompt_toolkit=1.0.14=py35_0 psutil=5.2.2=py35_0 ptyprocess=0.5.1=py35_0 pygments=2.2.0=py35_0 pyqt=5.6.0=py35_2 python=3.5.3=1 python-dateutil=2.6.0=py35_0 pytz=2017.2=py35_0 pyyaml=3.12=py35_0 pyzmq=16.0.2=py35_0 qt=5.6.2=2 qtconsole=4.3.0=py35_0 readline=6.2=2 requests=2.14.2=py35_0 setuptools=27.2.0=py35_0 simplegeneric=0.8.1=py35_1 sip=4.18=py35_0 six=1.10.0=py35_0 sortedcollections=0.5.3=py35_0 sortedcontainers=1.5.7=py35_0 sqlite=3.13.0=0 tblib=1.3.2=py35_0 terminado=0.6=py35_0 testpath=0.3=py35_0 tk=8.5.18=0 toolz=0.8.2=py35_0 tornado=4.5.1=py35_0 traitlets=4.3.2=py35_0 wcwidth=0.1.7=py35_0 wheel=0.29.0=py35_0 widgetsnbextension=2.0.0=py35_0 xz=5.2.2=1 yaml=0.1.6=0 zict=0.1.2=py35_0 zlib=1.2.8=3 ndims = 3,我会得到以下行为,多处理调度程序需要这样做“缓慢的解决方案”要长得多。但请记住,由于实际应用,我需要慢速解决方案的结构。

dimlen = 1000

此外,您可以尝试以下代码来查看两个版本的dask图中的差异。 chunklen = 100仅包含直到最后的并行路径,而%%timeit -r 1 -n 1 fast_solution(x).compute(get=single_threaded_get) # 2min 4s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each) %%timeit -r 1 -n 1 fast_solution(x).compute(get=threaded_get) # 49.5 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each) %%timeit -r 1 -n 1 fast_solution(x).compute(get=multiprocessing_get) # 55.4 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each) %%timeit -r 1 -n 1 slow_solution(x).compute(get=single_threaded_get) # 2min 21s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each) %%timeit -r 1 -n 1 slow_solution(x).compute(get=threaded_get) # 56.6 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each) %%timeit -r 1 -n 1 slow_solution(x).compute(get=multiprocessing_get) # 10min 31s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each) 具有更多金字塔结构。但是考虑到我对传递的单个块大小的想法,我不知道问题是什么。

fast_solution

进一步跟进@mrocklin对简化示例

的请求

你好@MRocklin,

谢谢你的回答。据我所知,一些计算图表具有较高的数据交换成本,而手头的示例属于此类别。我进一步简化了它,导致两个维度的网格网格等效操作,然后将维度总数减少到标量。也许这可以作为一个例子来更好地洞察问题,如果

  • slow_solution行为正常,示例具有较高的数据交换成本
  • 我们可以提出一些缓解问题的dask图优化
  • 如果是ndims = 2 dimlen = 1000 chunklen = 500 # ... from dask.dot import dot_graph dot_graph(fast_solution(x).dask) dot_graph(slow_solution(x).dask) 调度程序的问题。

请注意,该示例仅应用于分析内存使用情况,而不是CPU性能。它不会为CPU产生足够的工作量,因为它是一个简化的示例。

我想强调,我之所以关注这个问题,是因为我有这样的愿景,即dask / distributed应该能够处理图形结构,其中间的总数组大小远远超过工作者累积的RAM,如果chunksizes和本地图形结构远低于工作者RAM容量。该示例实现了这种图形结构。

所以这是更新的jupyter笔记本:dask_distributed_RAM_usage_meshgrid_operation.ipynb.zip

期待有关该主题的更多讨论,并感谢您在此问题上的努力。

马库斯

2 个答案:

答案 0 :(得分:1)

感谢有趣的测试。看起来multiprocessing_get在列表总和方面存在问题,我只能猜测,为什么。默认情况下,dask.Bag中使用多处理,这是python对象的用例,而不是数组,并且在需要进程间通信时不能快速执行。

无论如何,当您对计算中的所有步骤使用dask函数时,它实际上在所有情况下都能快速运行,请参阅我的示例

import dask.array as da
from dask.multiprocessing import get as multiprocessing_get
import time

t0 = time.time()

ndims = 3
dimlen = 400
chunklen = 100

x = [da.random.normal(10, 0.1, size=ndims*(dimlen,), chunks=ndims*(chunklen,)) for k in range(ndims)]

def slow_solution(x):
    res = x[0]
    for z in x[1:]:
        res = res + z
    return da.sum(res)

def fast_solution(x):
    return da.sum(da.stack(x))

t1 = time.time()
print("start fast f-n")
fast_solution(x).compute(get=multiprocessing_get)

t2 = time.time()
print("start slow f-n")
slow_solution(x).compute(get=multiprocessing_get)

t3 = time.time()
print("Whole script: ", t3 - t0)
print("fast function: ", t2 - t1)
print("slow function: ", t3 - t2)

<强>更新 是否有特殊原因需要使用multiprocessing_getthreaded不适合您,或者您只是好奇? dask的文档并不完全全面,但从我得到的,multiprocessing解决方案通常用于dask Bag,这是任何类型的python对象的更通用的解决方案。它的性能存在已知限制,请参阅http://dask.pydata.org/en/latest/shared.html#known-limitations

答案 1 :(得分:0)

数据交换成本

我怀疑你的计算会强制进行大量的数据交换,如果你在同一个进程中这是免费的,但如果你想使用不同的进程可能会很昂贵。这引入了两个成本:

  1. 必须移动数据,这需要时间
  2. 数据必须以冗余方式存储,这需要空间,这会导致Dask必须将数据溢出到磁盘,这需要更多时间。
  3. dask调度程序通常会尝试计算允许它清理中间结果的任务,但这并不总是可行的。当我使用分布式调度程序web diagnostic dashboard在我的机器上运行计算时,我发现大部分时间都花在进程间通信上,并将数据溢出到磁盘并将其读回。

    我还没有深入研究你的问题,以确定这是你的问题所固有的,还是dask安排事情的缺陷。如果您能够进一步简化计算,同时仍然显示相同的性能缺陷,那么这将使诊断更容易。