任务图
内容
任务图¶
在内部,Dask 使用 Python 字典、元组和函数以一种简单的格式编码算法。这种图格式可以独立于 Dask 集合使用。不过,直接使用 Dask 图的情况很少见,除非你打算使用 Dask 开发新模块。即便如此,dask.delayed 通常是更好的选择。如果你是 核心开发者,那么你应该从这里开始。
动机¶
通常,人类编写程序,然后编译器/解释器解释它们(例如,python
、javac
、clang
)。有时人类不同意这些编译器/解释器选择如何解释和执行他们的程序。在这些情况下,人类通常将代码的分析、优化和执行带入代码本身。
通常,对并行执行的渴望导致了这种责任从编译器转移到人类开发者。在这些情况下,我们经常将程序的结构明确地表示为程序本身中的数据。
用户空间中并行执行的常见方法是 任务调度。在任务调度中,我们将程序分解为许多中等大小的任务或计算单元,通常是对大量数据的函数调用。我们将这些任务表示为图中的节点,如果一个任务依赖于另一个任务生成的数据,则在节点之间存在边。我们调用 任务调度器 来执行这个图,以尊重这些数据依赖性并在可能的情况下利用并行性,从而使多个独立任务可以同时运行。
存在许多解决方案。这是并行执行框架中的常见方法。通常,任务调度逻辑隐藏在其他更大的框架(例如 Luigi、Storm、Spark、IPython Parallel 等)中,因此经常被重新发明。Dask 是一种规范,它使用所有 Python 项目通用的术语(即字典、元组和可调用对象),以最小的附带复杂性编码完整的任务调度。理想情况下,这个最小解决方案易于被广大社区采用和理解。
示例¶
考虑以下简单的程序:
def inc(i):
return i + 1
def add(a, b):
return a + b
x = 1
y = inc(x)
z = add(y, 10)
我们以下列方式将此编码为字典:
d = {'x': 1,
'y': (inc, 'x'),
'z': (add, 'y', 10)}
以下 Dask 图表示:
虽然这种表示方式不如我们原来的代码令人愉快,但它可以被其他Python代码分析和执行,而不仅仅是CPython解释器。我们不建议用户以这种方式编写代码,而是认为它是自动化系统的合适目标。此外,在非玩具示例中,执行时间可能比``inc``和``add``大得多,这证明了额外的复杂性是合理的。
调度器¶
Dask 库目前包含几个调度器来执行这些图。每个调度器的工作方式不同,提供不同的性能保证并在不同的上下文中运行。这些实现并不特殊,其他人可以轻松编写更适合其他应用或架构的不同调度器。发出 dask 图的系统(如 Dask Array、Dask Bag 等)可能会利用适合应用和硬件的适当调度器。
任务期望¶
当一个任务提交给Dask执行时,关于该任务有一些假设。
不要就地修改数据¶
通常,不推荐使用具有副作用的任务来改变未来状态。直接修改存储在Dask中的数据可能会产生意想不到的后果。例如,考虑一个涉及Numpy数组的工作流程:
from dask.distributed import Client
import numpy as np
client = Client()
x = client.submit(np.arange, 10) # [0, 1, 2, 3, ...]
def f(arr):
arr[arr > 5] = 0 # modifies input directly without making a copy
arr += 1 # modifies input directly without making a copy
return arr
y = client.submit(f, x)
在上面的例子中,Dask 将就地更新 Numpy 数组 x
的值。虽然效率高,但这种行为可能会产生意想不到的后果,特别是如果其他任务需要使用 x
,或者由于工作节点失败,Dask 需要多次重新运行此计算。
避免持有GIL¶
一些封装了外部 C/C++ 代码的 Python 函数可以持有 GIL,这会阻止其他 Python 代码在后台运行。这是一个问题,因为当 Dask 工作线程运行您的函数时,它们还需要在后台相互通信。
如果你封装了外部代码,请尝试释放GIL。如果你使用的是Cython、Numba、ctypes或其他常见的代码封装解决方案,这通常很容易做到。