计算阶段

本页描述了计算的所有组成部分、一些常见的速度变慢原因以及如何有效地进行性能分析。本文档适用于那些在较大计算中遇到性能问题的进阶用户。

图构建

对 Dask 集合(数组、DataFrame、Bag、延迟对象)的操作会构建任务图。这些任务图是 Python 函数的字典,每次某个函数需要在某个数据块上运行时,字典中都会包含一个条目。当这些字典变得庞大(数百万个任务)时,构建它们的开销可能会相当大。此外,构建图的代码本身可能效率不高。

幸运的是,这个计算完全在您自己的计算机上的普通 Python 中进行,因此您可以使用诸如 cProfile 模块或 %prun%snakeviz IPython 魔术命令等工具来对其进行性能分析,就像分析您计算机上的任何其他 Python 代码一样。

假设在性能分析时没有出现明显原因,解决此问题的一个常见方法是,如果可能,通过增加块大小来减小图的大小,或者手动将许多操作批量处理到更少的函数中。

图优化

就在您提交图进行执行之前,Dask 会检查是否可以稍微清理一下图。这有助于删除不必要的工作,有时还会替换为更高效的操作。然而,如果您的图非常大(数百万个任务),这同样可能需要一些时间。

同样如前所述,这一切都在您本地机器上的 Python 中进行。您可以使用 dask.optimize 函数独立于计算对优化进行性能分析。

# x, y = dask.compute(x, y)
x, y = dask.optimize(x, y)

人们很少会更改优化设置。它也很少是导致速度变慢的主要原因。

图序列化

当您使用分布式调度器时,图必须发送到调度器进程,然后再从调度器发送到工作进程。要发送这些数据,首先必须将其转换为字节。如果传递的对象非常复杂或非常大,此序列化过程有时可能会很昂贵。

对此进行性能分析的最简单方法是对使用分布式调度器的 persist 调用进行性能分析。这将包括上述的优化阶段,以及下面的序列化和部分通信阶段(序列化通常是最大的组成部分)。幸运的是,persist 会立即返回,而不会等待计算实际完成。

导致序列化时间过长的最常见原因是将大型对象(如 NumPy 数组或 Pandas DataFrame)重复地放入您的图中。Dask 通常会在注意到这一点时发出警告。通常最佳解决方案是将其作为任务读取数据,而不是直接包含数据,预先分散大型数据,或将它们包装在 dask.delayed 中。有时,序列化是由复杂对象的其他问题引起的。这些问题往往与特定库密切相关,因此很难提供通用指南。

图通信

然后必须将图通信给调度器。您可以查看仪表盘的 /system 选项卡来观察与调度器之间的网络通信。目前没有好的方法对此进行性能分析。

调度

调度器现在接收到图,并且必须填充其内部数据结构,以便能够有效地将这些任务调度到各个工作进程。

只有在这些数据结构填充完毕后,仪表盘才会显示任何活动。从按下 compute/persist 到看到活动之间的所有时间都花费在上述阶段中。

您可以使用仪表盘的 /profile-server 页面来分析调度成本。然而,这对用户来说很少有用,因为除非您愿意深入研究调度代码,否则很难在此采取行动。尽管如此,感兴趣的用户可能会发现调度器内部工作原理的性能分析是有意义的。

如果调度开销很大,那么您能做的最好的事情就是减小图的大小,通常通过增加块大小来实现。

执行

最后,您的工作进程会收到一些任务并执行它们。您的代码在工作进程的一个线程上运行,并执行被告知执行的任何操作。

Dask 的仪表盘是这里进行性能分析和调查的好工具,特别是 /status/profile 页面。

加速此阶段通常取决于您提交的任务的作者。如果您使用的是自定义代码,那么作者可能是您,或者可能是 NumPy 或 Pandas 的开发者。我们鼓励您考虑使用高效的库,如 Cython、Numba,或任何其他常用于加速 Python 代码的解决方案。