块
目录
块¶
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>