Ray项目中使用Dask进行分布式计算的技术指南
引言
在分布式计算领域,Ray和Dask都是广受欢迎的Python库。Ray作为一个通用的分布式计算框架,提供了强大的任务调度和执行能力;而Dask则专注于数据分析领域,提供了类似NumPy和Pandas的接口来处理大规模数据。本文将详细介绍如何在Ray项目中使用Dask进行分布式计算,充分发挥两者的优势。
Dask与Ray的集成概述
Dask的核心是一个任务调度系统,它可以将复杂的计算任务分解为有向无环图(DAG)并高效执行。Ray同样提供了强大的分布式任务执行能力。通过dask_on_ray
调度器,我们可以让Dask的任务在Ray集群上执行,从而获得Ray的分布式优势。
版本兼容性
在使用Dask on Ray时,版本兼容性非常重要。以下是主要版本对应关系:
- Ray 2.40.0及以上版本:
- Python < 3.12:Dask 2022.10.2
- Python ≥ 3.12:Dask 2024.6.0
- Ray 2.34.0至2.39.0:
- Python < 3.12:Dask 2022.10.1
- Python ≥ 3.12:Dask 2024.6.0
- Ray 2.8.0至2.33.x:Dask 2022.10.1
- 更早版本请参考完整版本对应表
基本使用方法
要在Ray上运行Dask任务,只需在计算时指定ray_dask_get
作为调度器:
import ray
import dask.array as da
from ray.util.dask import ray_dask_get
ray.init()
# 创建Dask数组
x = da.random.random((10000, 10000))
# 在Ray上计算
result = x.sum().compute(scheduler=ray_dask_get)
注意:在Ray集群上运行时,不应使用Dask的分布式客户端,直接使用普通的Dask集合即可。
为什么选择Dask on Ray?
- 利用Ray特有功能:如云集群启动和共享内存存储
- 统一计算环境:在同一个应用中使用Dask和Ray库,无需维护两个集群
- 生产级执行环境:利用Ray快速、容错的分布式任务执行系统
最佳实践
大规模工作负载处理
对于大规模或内存密集型工作负载,建议:
- 降低
scheduler_spread_threshold
配置值,让调度器更倾向于将任务分散到集群 - 将头节点的
num-cpus
设置为0,避免任务调度到头节点
启动命令示例:
# 头节点
RAY_scheduler_spread_threshold=0.0 ray start --head --num-cpus=0
# 工作节点
RAY_scheduler_spread_threshold=0.0 ray start --address=[head-node-address]
超出内存的数据处理
Ray的对象溢出功能允许处理大于集群内存的数据集。从Ray 1.3+开始,此功能默认启用,对象会在内存不足时自动溢出到本地磁盘。
高级特性
持久化(Persist)
Dask-on-Ray扩展了dask.persist()
功能,使其行为类似于Dask Distributed的持久化语义。调用后,任务会提交到Ray集群,并返回内联在Dask集合中的Ray futures。
import dask.array as da
from ray.util.dask import ray_dask_get
x = da.random.random((10000, 10000))
x = x.persist(scheduler=ray_dask_get) # 立即开始计算
# 后续操作会更快,因为x已经计算完成
result1 = x.sum().compute()
result2 = x.mean().compute()
资源注解与任务选项
通过Dask的注解API,可以为特定Dask操作指定资源或其他Ray任务选项:
from dask import annotate
with annotate(ray_remote_args={"num_gpus": 1}):
# 这个操作将在有GPU的节点上执行
result = x.sum().compute(scheduler=ray_dask_get)
注意:可能需要禁用图优化,因为它可能会破坏注解。
DataFrame洗牌优化
Dask-on-Ray提供了一个DataFrame优化器,利用Ray的多返回任务能力,可以加速洗牌操作达4倍:
from ray.util.dask import dataframe_optimize
with dask.config.set(dataframe_optimize=dataframe_optimize):
df = dd.read_parquet(...)
result = df.groupby('column').apply(my_func).compute()
回调机制
Dask-on-Ray扩展了Dask的回调抽象,添加了Ray特定的回调钩子,允许用户在Ray任务提交和执行生命周期中插入自定义逻辑。
回调类型
ray_presubmit
: 提交Ray任务前运行ray_postsubmit
: 提交Ray任务后运行ray_pretask
: 在Ray worker中执行Dask任务前运行ray_posttask
: 在Ray worker中执行Dask任务后运行ray_postsubmit_all
: 所有Ray任务提交后运行ray_finish
: 所有Ray任务执行完成后运行
回调示例
计时回调示例:
from ray.util.dask import RayDaskCallback
import time
class TimerCallback(RayDaskCallback):
def _ray_pretask(self, key, object_refs):
return time.time()
def _ray_posttask(self, key, result, pre_state):
duration = time.time() - pre_state
print(f"Task {key} took {duration:.2f} seconds")
结合Actor实现状态管理
回调与Actor结合可以实现状态化数据聚合,如捕获任务执行统计信息和缓存结果:
@ray.remote
class CacheActor:
def __init__(self, threshold):
self.cache = {}
self.threshold = threshold
def should_cache(self, key, duration):
return duration > self.threshold
def get(self, key):
return self.cache.get(key)
def put(self, key, value):
self.cache[key] = value
class CachingCallback(RayDaskCallback):
def __init__(self, actor, threshold):
self.actor = actor
self.threshold = threshold
def _ray_presubmit(self, task, key, deps):
cached = ray.get(self.actor.get.remote(key))
if cached is not None:
return cached
return None
def _ray_posttask(self, key, result, pre_state):
duration = time.time() - pre_state
if ray.get(self.actor.should_cache.remote(key, duration)):
self.actor.put.remote(key, result)
结论
Dask-on-Ray为数据科学家和分析师提供了一个强大的工具,使他们能够利用熟悉的Dask API,同时在Ray的强大分布式执行引擎上运行计算。通过本文介绍的各种技术和最佳实践,用户可以更高效地处理大规模数据分析任务。
虽然Dask-on-Ray仍在发展中,可能无法达到直接使用Ray的性能,但它为需要在同一应用中使用Dask和Ray生态系统的用户提供了极大的便利。随着项目的不断成熟,我们可以期待更紧密的集成和更好的性能表现。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考