开始使用DASK的API是很容易的,但是很好地使用它们需要一些经验。下面重点说一下最佳实践的建议,以及常见问题的解决方案。
这里专门关注在所有DASK的API之间共同的最佳实践。如果想研究一个特定API的最佳实践,可以看以下文档。
从小处着眼
并行性带来了额外的复杂性和开销。通常认为对于处理大的问题是必要的,但并非如此。在将dask并行计算添加到工作负载之前,您可能需要先尝试一些替代方法:
- 使用更好的算法或数据结构:numpy、pandas、scikit learn对于您正在尝试的操作可能具有更快的功能。可能值得咨询专家或再次阅读他们的文档以找到更好的预构建算法。
- 更好的文件格式:支持随机访问的高效二进制格式通常可以帮助您高效、简单地管理大于内存的数据集。请参见下面的“存储数据效率”部分。
- 编译代码:使用numba或cython编译您的python代码可能会使并行性变得不必要。或者您可以使用这些库中可用的多核并行性。
- 抽样:即使您拥有大量数据,使用所有数据也可能没有太大的优势。通过智能抽样,您可能从更易于处理的子集中获得相同的结果。
- 配置文件:如果您试图加快慢代码的速度,那么首先了解慢代码的原因是很重要的。在分析代码方面进行适当的时间投资可以帮助您识别哪些方面会减慢您的速度。这些信息可以帮助您更好地决定并行性是否有帮助,或者其他方法是否更有效。
使用仪表板
使用仪表板
Dask的仪表盘帮助您了解workers的状态。这些信息可以帮助您找到有效的解决方案。在并行和分布式计算中,有新的成本需要注意,因此您的旧直觉可能不再真实。使用仪表板可以帮助您重新学习什么是快速和缓慢以及如何处理它。
有关更多信息,请参阅Dask仪表板上的文档。
避免非常大的分区
您的数据块应该足够小,以便其中许多数据能够同时存储在工作人员的可用内存中。当您在DASK数据帧中选择分区大小或在DASK数组中选择块大小时,通常可以控制这一点。
dask可能会在一台机器上并行处理尽可能多的块,就像您在那台机器上拥有多核一样。因此,如果您有1 GB的块和10个内核,那么DASK可能会使用至少10 GB的内存。此外,DASK通常有2-3倍的可用块来处理,因此它总是有一些要处理的东西。
如果您有一台具有100 GB和10核的机器,那么您可能希望选择1GB范围内的块。每个核心有10个块的空间,这给了dask一个健康的空间,而没有太小的任务
请注意,您还希望避免块大小太小。有关详细信息,请参阅下一节
避免使用非常大的图形
DASK工作负载由任务组成。任务是一个python函数,比如np.sum应用于python对象,或比如pandas的dataframe或numpy array。如果您使用具有多个分区的DASK集合,那么您执行的每个操作(如X+1)都可能生成许多任务,至少与集合中的分区一样多。
每项任务都有一些开销。这在200us到1ms之间。如果你有一个包含数千个任务的计算,这很好,会有大约一秒钟的开销,这可能不会给你带来麻烦。
然而,当您有具有数百万个任务的非常大的图时,这可能会变得麻烦,这不仅是因为开销现在在10分钟到小时的范围内,而且因为处理如此大的图的开销可能会开始压倒调度程序。
您可以做一些事情来解决这个问题:
构建较小的图表。你可以通过……
- 增加块大小:如果您有1000 GB的数据,并且使用10 MB的块,那么您有100000个分区。对此类集合的每个操作都将生成至少100000个任务。
- 但是,如果您将chunksize增加到1GB甚至几个GB,那么您可以将开销减少几个数量级。这要求您的worker拥有超过1GB的内存,但对于较大的工作负载来说,这是典型的情况。
- o将操作融合在一起:dask将自己完成一些这方面的工作,但您可以帮助它。如果您有一个非常复杂的操作和几十个子操作,也许您可以将其打包成一个单独的python函数,并使用类似da.map_块或dd.map_分区的函数。
- 一般来说,您可以在函数中进行的管理工作越多越好。这样,DASK调度程序就不需要考虑所有的细粒度操作。
- 分解计算:对于非常大的工作负载,您可能还希望尝试一次将较小的块发送到DASK。例如,如果您正在处理一个千兆字节的数据,但发现DASK只对100TB感到满意,那么您可以将计算拆分为十个部分,然后逐个提交。
学习定制技术
高级DASK集合(array、dataframe、bag)包括遵循numpy和pandas的标准python API的常见操作。但是,许多Python工作负载都很复杂,可能需要这些高级API中没有包含的操作。
幸运的是,有许多选项可以支持自定义工作负载:
- 所有集合都有一个 map_partitions or map_blocks 函数,该函数在集合中的每个pandas dataframe或numpy数组中应用用户自定义的函数。因为dask集合是由普通的python对象组成的,所以在不做太多修改的情况下,跨数据集分区映射自定义函数通常非常容易
df.map_partitions(my_custom_func)
- 更复杂的map_*函数。有时你的习惯行为并不令人尴尬地平行,但需要更高级的沟通。例如,您可能需要在一个分区和下一个分区之间传递一点信息,或者您可能想要构建一个自定义聚合。
DASK集合也包括这些方法。
- 对于更复杂的工作负载,您可以将集合转换为单个块,并根据需要使用dask delayed来排列这些块。通常在每个集合上都有一个 to_delayed 方法
map_partitions(func, *args, **kwargs) | 在每个dataframe分区上应用python函数。 |
rolling.map_overlap(func, df, before, after, …) | 对每个分区应用一个函数,与相邻分区共享行。 |
Aggregation(name, chunk, agg[, finalize]) | 用户定义的GroupBy聚合。 |
blockwise(func, out_ind, *args, **kwargs) | 张量运算:广义内积和外积 |
map_blocks(func, *args, **kwargs) | 跨DASK array的所有块映射函数。 |
map_overlap(x, func, depth[, boundary, trim]) | 将一个函数映射到数组中有一些重叠的块上 |
reduction(x, chunk, aggregate[, axis, …]) | 缩减的一般版本 |
不再需要时停止使用DASK
在许多工作负载中,通常使用dask读取大量数据,减少数据量,然后迭代更小的数据量。对于较小数据的后一阶段,停止使用DASK并再次开始使用普通的Python可能是有意义的。
df = dd.read_parquet("lots-of-data-*.parquet")
df = df.groupby('name').mean() # reduce data significantly显著减少数据
df = df.compute() # continue on with Pandas/NumPy
尽可能持久化到内存
从RAM访问数据通常比从磁盘访问要快得多。一旦你的数据集处于以下干净的状态:
1.适合内存占用
2.足够干净,可以尝试多种不同的分析
那么现在是在RAM中保存数据是一个好的选择。
df = dd.read_parquet("lots-of-data-*.parquet")
df = df.fillna(...) # clean up things lazily
df = df[df.name == 'Alice'] #缩小到一个更合理的尺寸
df = df.persist() # trigger computation, persist in distributed RAM
请注意,只有当您在分布式计算机上时,这才是有意义的(否则,如上所述,您可能应该在没有DASK的情况下继续)。
高效存储数据
随着计算能力的提高,您可能会发现数据访问和I/O占用了您总时间的很大一部分。此外,并行计算通常会对如何存储数据添加新的约束,尤其是在提供对数据块的随机访问方面,这些数据块与您计划如何对其进行计算相一致。
例如:
- 对于压缩,您可能会发现您放弃了gzip和bz2,采用了LZ4、Snappy和z-Standard等提供更好性能和随机访问的新系统。
- 对于存储格式,您可能会发现需要为随机访问、元数据存储和二进制编码(如Parquet、ORC、Zarr、HDF5、geotiff等)优化的自我描述格式。
- 在云端工作时,您可能会发现某些较旧的格式(如HDF5)可能无法正常工作。
- 您可能希望以与常见查询完全一致的方式对数据进行分区或分块。在DAsk dataframe中,这可能意味着选择一个列作为快速选择和联接的排序依据。对于DASK dataframe,这可能意味着选择与您的访问模式和算法一致的块大小。