稀疏数组

稀疏数组

通过将内存中的 NumPy 数组替换为内存中的稀疏数组,我们可以重用 Dask 数组的分块算法来实现并行和分布式稀疏数组。

Dask 数组中的分块算法通常围绕内存中的 NumPy 数组进行并行化。然而,如果另一个内存数组库支持 NumPy 接口,那么它也可以利用 Dask 数组的并行算法。特别是 sparse 数组库满足 NumPy API 的子集,并且与 Dask 数组配合良好(并经过测试)。

示例

假设我们有一个 Dask 数组,其中大部分元素为零

rng = da.random.default_rng()
x = rng.random((100000, 100000), chunks=(1000, 1000))
x[x < 0.95] = 0

我们可以将 NumPy 数组的每个块转换为一个 sparse.COO 数组

import sparse
s = x.map_blocks(sparse.COO)

现在,我们的数组不再由许多 NumPy 数组组成,而是由许多稀疏数组组成。从语义上讲,这没有任何改变。可用的操作将继续完全相同地工作(假设 numpysparse 的行为相同),但性能特征和存储成本可能会发生显著变化

>>> s.sum(axis=0)[:100].compute()
<COO: shape=(100,), dtype=float64, nnz=100>

>>> _.todense()
array([ 4803.06859272,  4913.94964525,  4877.13266438,  4860.7470773 ,
        4938.94446802,  4849.51326473,  4858.83977856,  4847.81468485,
        ... ])

要求

任何复制 NumPy ndarray 接口的内存库都应该在这里工作。sparse 库是一个最小的示例。特别是,内存库应至少实现以下操作:

  1. 使用切片、列表和元素进行简单切片(用于切片、重新分块、重塑等)

  2. 一个 concatenate 函数,与 np.concatenate 的接口匹配。此函数必须注册在 dask.array.core.concatenate_lookup

  3. 所有 ufuncs 必须支持完整的 ufunc 接口,包括 dtype=out= 参数(即使它们功能不正常)

  4. 所有归约操作必须支持完整的 axis=keepdims= 关键字,并且在这方面与 NumPy 的行为一致

  5. 数组类应遵循 __array_priority__ 协议,并准备好响应优先级较低的其他数组

  6. 如果需要支持 dot,则应将一个与 np.tensordot 接口匹配的 tensordot 函数注册到 dask.array.core.tensordot_lookup

其他操作(如 reshape、transpose 等)的实现应遵循标准的 NumPy 约定,关于 shape 和 dtype。不实现这些操作也是可以的;如果在运行时尝试这些操作,并行 dask.array 将会出错。

混合数组

Dask 数组支持混合使用不同类型的内存数组。这依赖于内存数组在必要时知道如何相互作用。当两个数组交互时,具有最高 __array_priority__ 的数组的函数将优先(例如,对于 concatenate、tensordot 等)。