块
目录
块¶
Dask 数组由许多 NumPy(或类似 NumPy)数组组成。这些数组的排列方式会显著影响性能。例如,对于一个方形数组,你可能沿着行、沿着列或以更像方形的方式排列块。不同的 NumPy 数组排列方式对于不同的算法会有不同的速度。
考虑并控制分块对于优化高级算法非常重要。
指定块形状¶
我们总是指定一个 chunks
参数来告诉 dask.array 如何将底层数组分解成块。我们可以通过多种方式指定 chunks
统一的维度大小,如
1000
,表示每个维度的块大小都是1000
统一的块形状,如
(1000, 2000, 3000)
,表示第一轴的块大小为1000
,第二轴为2000
,第三轴为3000
沿着所有维度的所有块的完全显式大小,如
((1000, 1000, 500), (400, 400), (5, 5, 5, 5, 5))
一个字典,指定每个维度的块大小,如
{0: 1000, 1: 2000, 2: 3000}
。这只是上面形式 2 和 3 的另一种写法
你的 chunks 输入将被标准化并存储在第三种也是最显式的形式中。请注意,chunks
代表“块形状”,而不是“块数量”,因此指定 chunks=1
意味着你将拥有许多块,每个块只有一个元素。
为了性能,选择合适的 chunks
应遵循以下规则
块应该足够小,能够舒适地放入内存。我们将在内存中同时容纳许多块
块必须足够大,以便在该块上的计算所需时间显著长于 Dask 调度带来的每任务 1ms 开销。一个任务所需时间应长于 100ms
块大小通常在 10MB-1GB 之间,这取决于可用 RAM 和计算持续时间
块应与你想要进行的计算对齐。
例如,如果你计划经常沿着某个特定维度进行切片,那么如果你的块对齐,使得你需要触及的块更少,效率会更高。如果你想添加两个数组,那么如果这些数组具有匹配的块模式会很方便
如果适用,块应与你的存储对齐。
数组数据格式通常也是分块的。加载或保存数据时,如果 Dask 数组的块与存储的分块对齐会很有用,通常在每个方向上都是其整数倍
在 Genevieve Buckley 的 Choosing good chunk sizes in Dask 中了解更多。
未知块¶
有些数组的块大小是未知的。这发生在数组大小取决于尚未执行的惰性计算时,如下所示
>>> rng = np.random.default_rng()
>>> x = da.from_array(rng.standard_normal(100), chunks=20)
>>> x += 0.1
>>> y = x[x > 0] # don't know how many values are greater than 0 ahead of time
上述操作会导致数组具有未知的形状和块大小。形状或块中的未知值使用 np.nan
表示,而不是整数。这些数组支持许多(但不是所有)操作。特别是,像切片这样的操作是不可能的,会导致错误。
>>> y.shape
(np.nan,)
>>> y[4]
...
ValueError: Array chunk sizes unknown
A possible solution: https://docs.dask.org.cn/en/latest/array-chunks.html#unknown-chunks.
Summary: to compute chunks sizes, use
x.compute_chunk_sizes() # for Dask Array
ddf.to_dask_array(lengths=True) # for Dask DataFrame ddf
使用 compute_chunk_sizes()
允许这个示例运行
>>> y.compute_chunk_sizes()
dask.array<..., chunksize=(19,), ...>
>>> y.shape
(44,)
>>> y[4].compute()
0.78621774046566
请注意,compute_chunk_sizes()
会立即执行计算并就地修改数组。
使用 Dask DataFrame 创建 Dask 数组时也会出现未知块大小的情况
>>> ddf = dask.dataframe.from_pandas(...)
>>> ddf.to_dask_array()
dask.array<..., shape=(nan, 2), ..., chunksize=(nan, 2)>
使用 to_dask_array()
可以解决这个问题
>>> ddf.to_dask_array(lengths=True)
dask.array<..., shape=(100, 2), ..., chunksize=(20, 2)>
关于 to_dask_array()
的更多详细信息在 Dask 数组创建文档中关于如何从 Dask DataFrame 创建 Dask 数组的部分中有所提及。
块示例¶
在这个示例中,我们展示了 chunks=
的不同输入如何切割以下数组
1 2 3 4 5 6
7 8 9 0 1 2
3 4 5 6 7 8
9 0 1 2 3 4
5 6 7 8 9 0
1 2 3 4 5 6
这里,我们展示了不同的 chunks=
参数如何将数组分割成不同的块
chunks=3
:大小为 3 的对称块
1 2 3 4 5 6
7 8 9 0 1 2
3 4 5 6 7 8
9 0 1 2 3 4
5 6 7 8 9 0
1 2 3 4 5 6
chunks=2
:大小为 2 的对称块
1 2 3 4 5 6
7 8 9 0 1 2
3 4 5 6 7 8
9 0 1 2 3 4
5 6 7 8 9 0
1 2 3 4 5 6
chunks=(3, 2)
:大小为 (3, 2)
的非对称但重复的块
1 2 3 4 5 6
7 8 9 0 1 2
3 4 5 6 7 8
9 0 1 2 3 4
5 6 7 8 9 0
1 2 3 4 5 6
chunks=(1, 6)
:大小为 (1, 6)
的非对称但重复的块
1 2 3 4 5 6
7 8 9 0 1 2
3 4 5 6 7 8
9 0 1 2 3 4
5 6 7 8 9 0
1 2 3 4 5 6
chunks=((2, 4), (3, 3))
:非对称且不重复的块
1 2 3 4 5 6
7 8 9 0 1 2
3 4 5 6 7 8
9 0 1 2 3 4
5 6 7 8 9 0
1 2 3 4 5 6
chunks=((2, 2, 1, 1), (3, 2, 1))
:非对称且不重复的块
1 2 3 4 5 6
7 8 9 0 1 2
3 4 5 6 7 8
9 0 1 2 3 4
5 6 7 8 9 0
1 2 3 4 5 6
讨论
后者的示例在原始数据上很少由用户提供,但它们产生于复杂的切片和广播操作。通常人们会使用最简单的形式,直到他们需要更复杂的。chunks 的选择应该与你想要进行的计算对齐。
例如,如果你计划沿着第一个维度取出细条切片,那么你可能希望使该维度比其他维度更细。如果你计划进行线性代数运算,那么你可能需要更对称的块。
加载分块数据¶
现代 NDArray 存储格式,如 HDF5、NetCDF、TIFF 和 Zarr,允许数组以块或瓦片的形式存储,这样可以在无需线性扫描数据流的情况下高效地提取数据块。最好将你的 Dask 数组的块与底层数据存储的块对齐。
然而,数据存储通常比 Dask 数组理想的分块更精细,因此通常会选择一个块大小,它是存储块大小的倍数,否则可能会产生高额开销。
例如,如果你正在加载一个按 (100, 100)
块进行分块的数据存储,那么你可能选择一个更像 (1000, 2000)
的分块方式,它更大,但仍然可以被 (100, 100)
整除。数据存储技术会告诉你它们的数据是如何分块的。
重新分块¶
|
将 Dask 数组 x 中的块转换为新的块。 |
有时你需要改变数据的分块布局。例如,数据可能是按行分块的,但你需要进行一个按列操作会快很多的操作。你可以使用 rechunk
方法改变分块。
x = x.rechunk((50, 1000))
跨轴重新分块可能会很昂贵,并产生大量通信,但 Dask 数组具有相当高效的算法来完成此任务。
注意:rechunk
方法要求输出数组与输入数组具有相同的形状,并且不支持重塑。在使用 rechunk 之前,务必确保所需的输出形状与输入形状匹配。
你可以将任何有效的块形式传递给 rechunk
x = x.rechunk(1000)
x = x.rechunk((50, 1000))
x = x.rechunk({0: 50, 1: 1000})
重塑¶
dask.array.reshape()
的效率很大程度上取决于输入数组的分块方式。在重塑操作中,存在“快速移动”或“高”轴的概念。对于一个 2d 数组,第二轴 (axis=1
) 是移动最快的,其次是第一轴。这意味着如果我们画一条线指示值是如何填充的,我们首先沿着“列”(沿着 axis=1
)移动,然后向下到下一行。考虑 np.ones((3, 4)).reshape(12)

现在考虑 Dask 的分块对该操作的影响。如果慢速移动轴(在本例中只是 axis=0
)的块大于尺寸 1,我们就会遇到问题。

第一个块的形状是 (2, 2)
。遵循 reshape
的规则,我们取出块 1 第一行的两个值。但是,在我们仍然在第一个块中有两个“未使用”值的情况下,我们跨越了一个块边界(从 1 到 2)。无法将输入块与输出形状对齐。我们需要以某种方式重新分块输入以使其与输出形状兼容。我们有两种选择
使用
dask.array.rechunk()
中的逻辑合并块。这避免了生成过多的任务/块,代价是一些通信和更大的中间结果。这是默认行为。使用
da.reshape(x, shape, merge_chunks=False)
通过分割输入来避免合并块。特别是,我们可以将所有慢速移动轴重新分块,使其块大小为 1。这避免了通信和大量数据的移动,代价是更大的任务图(可能大得多,因为慢速移动轴上的块数量将等于这些轴的长度)。
从视觉上看,这是第二种选择

哪种方法更好取决于你的问题。如果通信非常昂贵,并且你的数据在慢速移动轴上相对较小,那么 merge_chunks=False
可能更好。让我们在一个将 3d 数组重塑为 2d 数组的问题上比较这两种方法的任务图,其中输入数组在慢速移动轴上没有 chunksize=1
。
>>> a = da.from_array(np.arange(24).reshape(2, 3, 4), chunks=((2,), (2, 1), (2, 2)))
>>> a
dask.array<array, shape=(2, 3, 4), dtype=int64, chunksize=(2, 2, 2), chunktype=numpy.ndarray>
>>> a.reshape(6, 4).visualize()

>>> a.reshape(6, 4, merge_chunks=False).visualize()

默认情况下,一些中间块会被合并,导致任务图更复杂。使用 merge_chunks=False
,我们分割输入块(导致更多的总任务,取决于数组的大小),但避免后期的通信。
自动分块¶
Chunks 还包括三个特殊值
-1
:沿此维度不进行分块None
:沿此维度的分块不改变(用于 rechunk 时有用)"auto"
:允许此维度的分块适应理想的块大小
因此,例如,可以将一个 3D 数组重新分块,使其沿第零维度不分块,但仍然具有合理的块大小,如下所示
x = x.rechunk({0: -1, 1: 'auto', 2: 'auto'})
或者可以允许所有维度自动缩放以达到一个好的块大小
x = x.rechunk('auto')
自动分块会扩展或收缩所有标记为 "auto"
的维度,以尝试达到与配置值 array.chunk-size
相等的字节数的块大小,该值默认为 128MiB,但你可以在配置中更改。
>>> dask.config.get('array.chunk-size')
'128MiB'
自动重新分块会尝试尊重自动缩放维度的中值块形状,但会进行修改以适应整个数组的形状(块不能大于数组本身),并找到能够很好地分割形状的块形状。
在使用 dask.array.ones
或 dask.array.from_array
等操作创建数组时,也可以使用这些值
>>> dask.array.ones((10000, 10000), chunks=(-1, 'auto'))
dask.array<wrapped, shape=(10000, 10000), dtype=float64, chunksize=(10000, 1250), chunktype=numpy.ndarray>