☰
Ray分布式计算框架深度解析:从任务调度到集群部署实战
2026/10/22 18:26:57 网站建设 项目流程

从最早拿 Celery 跑异步任务,到后来用 Spark 处理批量数据,再到花一整周时间调 Spark 的 Executor 内存参数,我的感觉一直是:分布式计算本身不难,难的是让这套系统既能跑批处理,又能跑实时计算,还得照顾到机器学习训练这种有状态、高吞吐的负载。

直到我深入用了 Ray,我才意识到分布式计算框架的架构设计差距,能差出这么大。

Ray 不是要把 Spark 或 Celery 替换掉,而是它解决的是它们各自边界之外的问题。Ray 既能像 Celery 一样处理细粒度的异步任务,又能像 Spark 一样做数据并行,还能把 GPU 集群编排得明明白白。这篇文章我想从架构层面拆解 Ray 为什么能做到这一点,然后带你走一遍它的高级模式,最后说说我在实际部署和调优中踩过的坑。

1. 为什么需要 Ray——分布式计算框架的范式之争

1.1 从 Celery 到 Spark:两个时代的工具与各自的边界

Celery 是 Python 生态里最经典的分布式任务队列,我最早用它做爬虫任务分发。它的架构很简单:Broker(Redis/RabbitMQ)接收任务,Worker 拉取任务执行,结果存到 Backend。这个模式解决的核心问题是“异步化”,也就是把需要耗时执行的任务从请求链路里剥离出去。但 Celery 有两个致命短板:第一,任务之间难以表达复杂的依赖关系,虽然有 chain、group、chord 这些原语,但一旦任务拓扑复杂起来,代码会变得非常难维护;第二,任务执行过程中的状态管理完全交给开发者自己处理,没有内建的数据共享机制,想做一个多阶段流水线就得不停地读写外部存储。

Spark 则是另一个极端。它把计算抽象成 RDD/DataFrame 上的算子(map、filter、join 等),通过 Lineage(血统)机制实现了强大的容错和延迟计算。Spark 的优点在于它对“数据密集”型计算做了极致的优化,比如 Catalyst 优化器、Tungsten 执行引擎、Shuffle 过程的堆外内存管理。但 Spark 的架构决定了它更适合“粗粒度”的批处理任务。我实际用下来发现,Spark 在调度延迟上难以做到毫秒级,每个 Stage 之间的 Barrier 同步开销很大,而且如果你要写一个自定义的有状态分布式算法,你会感觉到 CPU 和内存的想象力被框架给束缚住了。

1.2 Ray 诞生的动机:AI 时代的计算范式转变

Ray 来自 UC Berkeley RISELab,它的定位不是“又一个数据处理引擎”,而是“一个面向 AI 应用的通用分布式执行引擎”。深度学习和强化学习任务有几个特点:需要细粒度的任务调度(毫秒级)、需要支持有状态的计算角色(像参数量服务器)、需要动态的任务依赖图(不像 Spark 那样一次建好 DAG 就固定了)。这些需求恰好是 Celery 和 Spark 都覆盖不到的。

我用一个比喻来理解:Spark 是一条流水线,数据从一端进去,加工完从另一端出来,中间不能随便插队;Celery 就是一个邮局,你把信封投进去就完事,但它不关心信封之间的先后顺序;Ray 更像是一个操作系统,它给你提供了“进程”和“线程”级别的原语,你可以用这些原语自由地编排任意结构的分布式计算图。

1.3 架构对比:一张表看清三者的定位差异

维度CelerySparkRay
编程模型任务队列RDD/DataFrame 算子Task + Actor + Object
调度粒度任务级,依赖链路由原语拼装Stage 级,批量调度函数调用级,动态依赖图
有状态支持弱,需外部存储弱,RDD 不可变强,Actor 长期运行
典型延迟秒级百毫秒~秒级毫秒级
适用场景Web 异步任务、定时任务批处理、SQL 分析、ETL机器学习训练、RL、实时推理、分布式仿真
容错机制Worker 重启重试Lineage+RDD 重算血缘追踪+对象重建

这张表不是说谁好谁坏,而是它们的架构决策完全服务于不同的目标。如果你只需要把发邮件的动作异步化,Celery 是正确选择;如果你要做 T+1 的数据报表,Spark 是正确选择;但如果你要做在线强化学习,环境模拟和策略更新需要高频交互,那 Spark 和 Celery 都不够优雅,Ray 才是设计上就考虑了这个问题的方案。

2. Ray 核心架构拆解:控制平面与数据平面的工程哲学

2.1 全局控制存储(GCS):去中心化架构的关键

Ray 架构里最核心的设计之一是 GCS(Global Control Store),它是一个基于内存的分布式键值存储,底层用了 Raft 协议做高可用。所有节点的元数据(比如对象位置、任务状态、Actor 位置、节点心跳)都存放在 GCS 里。你可能会问:这难道不是单点瓶颈吗?这正是 Ray 设计的高明之处。

P2P 架构的问题在于节点间需要互相维护一致性,这在节点规模到几百上千时会变得非常复杂。而 GCS 把元数据集中管理,数据平面则完全走对象存储和 gRPC 点对点传输。核心思想是让元数据和数据分离。元数据居中统管,数据分布存储传输,这解决了大规模集群中“找数据”和“传数据”的复杂度失衡问题。在实际生产中,GCS 所在节点需要配置足够的内存和 CPU,因为所有任务的状态变更都会打到 GCS。

2.2 自顶向下的调度器与任务依赖管理

Ray 的调度器是分层的。中心调度器在 GCS 里,负责跨节点的负载均衡;每个节点上有一个本地调度器(Raylet),负责本地的任务排队和资源分配。当你在 driver 进程里调用一个ray.remote函数时,流程是这样的:

  1. Driver 把任务描述发给中心调度器;
  2. 中心调度器根据资源需求(CPU、GPU、内存)和数据局部性选择一个节点;
  3. 目标节点的 Raylet 把任务放入本地队列;
  4. 当任务参数依赖的 ObjectRef 都已就绪时,Raylet 才真正执行任务。

这种设计最聪明的地方在于它对“未就绪依赖”的处理。普通函数调用遇到依赖不满足就只能阻塞等待,但 Ray 会把任务重写成wait+execute两个阶段,依赖未完成时任务处于 pending 状态,不会占用执行线程。这个机制让 Ray 可以在超大规模任务图中保持极高的吞吐。

2.3 分布式对象存储与零拷贝传输

Ray 的每个节点上跑着一个对象存储(在 Ray 1.x 之后使用 Apache Arrow 的内存格式)。当一个函数返回一个对象时,这个对象只会存储在调用者的节点上(除非被其他节点引用才会自动复制)。其他节点读取这个对象时,如果是本地节点就直接共享内存读取,如果是远程节点就通过 gRPC 拉取。

这里有个很实用的细节:传给ray.remote函数的大对象(比如一个 100MB 的 DataFrame)默认不会在每个任务里复制一次,而是通过 Arrow 的零拷贝机制共享给所有本地任务。我曾经在一个 32 核的节点上跑 100 个并发任务处理同一个 2GB 的模型参数文件,内存占用只有 2GB 多一点,而不是 200GB。这就是对象存储和序列化层配合的威力。

2.4 容错机制:从任务重试到对象重建

Ray 的容错分为两层。任务级别的容错是自动重试:如果任务因为节点故障失败,调度器会重新调度到另一个节点。对象级别的容错则更有意思:如果某个节点的对象存储损坏了,GCS 里记录了每个对象的血缘(lineage),如果对象不可达,Ray 会自动调用生成它的任务重算一遍。

这个机制在面对大规模集群时非常有用。我遇到过一台 GPU 机器因为驱动问题突然挂掉,Spark 的做法是整个 Application 崩溃重启(或者你开启了动态分配会稍微好一点),但 Ray 会自动把这家机器上的任务全部重新调度,不需要人工干预。当然,容错是有代价的,如果你的任务有副作用(比如往数据库里写了数据),重试会导致重复写入,所以生产环境中给任务加幂等设计是必须的。

3. 深入 Ray 核心编程模型:Task、Actor、Object 的底层逻辑

3.1 Task:从 Python 函数到分布式任务的魔法

在 Ray 里创建一个任务非常简单:

import ray ray.init() @ray.remote(num_cpus=1, num_gpus=0) def process_chunk(chunk_id: int, data: list) -> int: # 模拟一个耗时计算 return sum(data) * chunk_id # 异步提交任务,立即返回 ObjectRef futures = [process_chunk.remote(i, [1, 2, 3]) for i in range(10)] results = ray.get(futures)

这里的.remote()调用与普通函数调用的差异非常关键。当你调用process_chunk.remote()时,传入的参数会被立即序列化并复制到对象存储中,而返回的是一个 ObjectRef(可以理解为一个分布式指针)。ray.get()会阻塞直到对象可读,ray.wait()则可以设置超时和返回数量,用于流式地获取结果。

我在设计任务并行代码时总结了一个原则:尽量不要在任务之间传递大型对象,而是把大对象先放到对象存储里,再传入 ObjectRef。因为参数传递和结果返回都会产生序列化和反序列化开销,每个大对象的传输耗时可能超过任务本身的计算耗时。一个 10MB 的 dict 用 pickle 序列化需要约 20ms,但如果任务本身就是轻量级计算(比如 5ms),序列化就成了性能瓶颈。

3.2 Actor:有状态计算的正确打开方式

如果你有多个任务需要共享一个状态(比如模型参数、数据库连接池、计数器),用纯 Task 是非常痛苦的。Ray 的 Actor 模式是为这种场景设计的:

@ray.remote class Counter: def __init__(self): self.count = 0 def increment(self) -> int: self.count += 1 return self.count counter = Counter.remote() results = [counter.increment.remote() for _ in range(100)] print(ray.get(results[-1])) # 100

Actor 的本质是把一个类的实例放在集群的某个节点上,执行对它的方法调用时走 gRPC。每个 Actor 方法默认是串行执行的,这保证了状态的一致性。你可以为 Actor 指定有状态的 GPU 资源:

@ray.remote(num_gpus=1, max_restarts=2) class Trainer: def __init__(self, model_config: dict): import torch self.model = torch.nn.Linear(**model_config).cuda()

max_restarts参数让 Actor 在进程崩溃后自动重建,这在长时训练任务中非常重要。我踩过的一个坑是:如果 Actor 的__init__里加载的模型达到了几个 GB,重建的代价很高,所以应该尽量把大模型放到 Ray 的ray.put()对象存储中,让多个 Actor 引用同一个 ObjectRef,而不是每个 Actor 各自加载一遍。

3.3 ObjectRef 与分布式引用计数

ObjectRef 是 Ray 实现分布式计算的基础。它的底层是内嵌在 GCS 中的引用计数机制,每个 ObjectRef 都有一个全局唯一的 ID。当所有引用它的ray.get()调用完成并销毁引用后,对象会自动从对象存储中清除。

这个机制引出一个重要问题:如果你的代码里持有 ObjectRef 时间太长,对象就会一直占用内存。我在写长时间运行的 worker 时曾经遇到过内存持续上涨的问题,排查后发现是在一个循环里反复创建了 ObjectRef 但没有显式删除。推荐使用del手动释放不再需要的 ObjectRef,或让对象作用域缩小。

另外,Ray 有对象封装(object spilling)机制,当内存不足时会将对象溢出到磁盘。我在生产环境把RAY_object_spilling_threshold设置为 0.7,意思是当节点内存使用达到 70% 时就开始溢写,这样避免了内存爆掉导致的节点崩溃。

4. Ray 高级模式:从异步 Actor 到 Serve 部署实战

4.1 异步 Actor 与流水线并行

Ray 支持 AsyncActor,这在 IO 密集型的推理服务中非常有用。普通 Actor 的方法调用是串行的,如果方法内部有网络请求,整个 Actor 的吞吐量就会受限于单次请求的耗时。而 AsyncActor 允许方法内部使用asyncio并发处理多个请求:

import asyncio import aiohttp @ray.remote class AsyncAPICaller: async def call(self, url: str) -> dict: async with aiohttp.ClientSession() as session: async with session.get(url) as resp: return await resp.json()

实际体验下来,AsyncActor 对单 Actor 的吞吐量提升是数量级的。尤其适合调用外部 REST API、数据库查询等 IO 密集操作。但要注意 AsyncActor 仍然受限于单节点的 CPU 资源,如果计算密集,还是需要开多个 Actor 配合。

流水线并行模式是我在训练数据处理时常用的一种模式:

@ray.remote class Producer: def produce(self, batch_id: int): return [batch_id * 100 + i for i in range(100)] @ray.remote class Consumer: def consume(self, data: list): return sum(data) producer = Producer.remote() consumer = Consumer.remote() futures = [] for i in range(10): data_ref = producer.produce.remote(i) futures.append(consumer.consume.remote(data_ref))

这里 Producer 和 Consumer 分别作为长期运行的 Actor,任务通过 ObjectRef 衔接,避免了每次创建新 Actor 的初始化开销。对于流式数据处理场景,这种方式比纯 Task 更稳定。

4.2 嵌套任务与任务并行模式

Ray 里任务可以递归调用其他远程任务,这在实现分而治之的算法(如快速排序、归并)时非常自然:

@ray.remote def parallel_sort(arr): if len(arr) <= 1000: return sorted(arr) mid = len(arr) // 2 left_ref = parallel_sort.remote(arr[:mid]) right_ref = parallel_sort.remote(arr[mid:]) left, right = ray.get([left_ref, right_ref]) return merge(left, right)

这个模式有一个有趣的副作用:driver 进程只负责提交顶层任务,子任务由其他 worker 递归提交,这样把调度压力分散到了整个集群,而不是集中在 driver 上。在处理几十亿级数据量时,这种“递归折叠”模式可以极大减少 driver 和调度器之间的通信量。

另外一个值得掌握的并行模式是ray.wait的流式处理。假设你有 1000 个任务,但只想一次处理 10 个结果:

pending = [task.remote(i) for i in range(1000)] while pending: done, pending = ray.wait(pending, num_returns=10, timeout=5) for ref in done: process_result(ray.get(ref))

这种写法能有效控制内存使用。如果不做流式处理,一次性ray.get所有 1000 个结果会把 driver 节点的内存打爆。

4.3 Ray Serve:一套代码搞定在线推理服务

Ray Serve 是 Ray 的模型推理服务组件,它能让你把训练好的模型部署成 HTTP 服务,自动扩容缩容。下面是一个完整的部署示例:

from ray import serve import requests @serve.deployment(num_replicas=2, ray_actor_options={"num_gpus": 1}) class ModelServer: def __init__(self, model_path: str): import torch self.model = torch.load(model_path) async def __call__(self, request): data = await request.json() return {"prediction": self.model(data["input"]).tolist()} serve.run(ModelServer.bind(model_path="/models/my_model.pt")) # 调用测试 resp = requests.post("http://localhost:8000/", json={"input": [1, 2, 3]}) print(resp.json())

Serve 最大的价值在于它把“在线推理”和“离线训练”统一到了一套系统里。模型更新时只需要做一个原子部署(零宕机更新),旧版本流量会平滑切换到新版本。我实际应用时发现它对于多模型 A/B 测试特别方便,多个 Deployment 之间可以按权重路由流量。

4.4 Ray Tune:分布式超参搜索的快速实践

模型调参时,Ray Tune 能大幅减少等待时间。它的核心机制是并行的 Trial 调度器,调度器会把不同的超参组合分配到不同的 CPU/GPU 上同步训练:

from ray import tune def train_model(config): model = MyModel(learning_rate=config["lr"]) for epoch in range(config["epochs"]): train_loss = model.fit() tune.report(loss=train_loss) analysis = tune.run( train_model, config={ "lr": tune.loguniform(1e-5, 1e-1), "epochs": tune.choice([10, 50, 100]), "batch_size": tune.choice([32, 64, 128]), }, num_samples=100, resources_per_trial={"gpu": 1, "cpu": 2}, ) best_trial = analysis.get_best_trial("loss", mode="min")

这里最值得学习的工具是搜索算法。tune.loguniform表示在 log 空间均匀采样,这比在原始空间均匀采样更适合学习率等超参数。R 中提到早期终止(Early Stopping),Ray 会通过 ASHA 等调度器自动终止那些效果不理想的 Trial,把资源留给更有希望的配置。我自己使用下来,一个 50 组配置的搜索任务,用 ASHA 可以提前 50% 以上完成。

4.5 与 MLOps 生态集成:RLlib、数据屏障与生产部署

可以说 Ray 一个非常有吸引力的地方在于它内建了 RLlib,这是用于强化学习训练的高层库。RLlib 本身支持多智能体、多策略、异策略,代码相对简洁:

from ray.rllib.algorithms.ppo import PPOConfig from ray.tune.logger import pretty_print algo = ( PPOConfig() .environment("CartPole-v1") .training(gamma=0.95, lr=0.01) .resources(num_gpus=0, num_cpus_per_worker=1) .rollouts(num_rollout_workers=4) .build() ) for i in range(10): result = algo.train() print(pretty_print(result))

RLlib 在处理“环境模拟器并行”这种高频率、高交互的任务时,优势非常明显。用 Message Passing 做 worker 同步,比直接 for 循环跑环境快很多。

5. 集群部署与性能调优:从单机到多节点的完整实践

5.1 本地模式与集群模式:不同规模的正确启动方式

Ray 支持单机多核的本地模式(ray.init()不带参数)和多节点集群模式。如果只是做开发和调试,本地模式完全够用,它默认会占用机器的所有 CPU 资源。但生产环境必须走集群模式:

# 头节点(head node) ray start --head --port=6379 --dashboard-host=0.0.0.0 # 工作节点(worker node) ray start --address=<head-node-ip>:6379 --num-cpus=32 --num-gpus=4

集群模式的头节点会启动 GCS、Dashboard、Raylet。工作节点的 Raylet 负责本地的任务调度和对象存储。我用过几套不同规模的集群部署(3 节点到 50 节点都试过),总结了一个经验:头节点不要承载计算任务。因为 GCS 和调度器的负载已经很高,如果头节点的 CPU 被打满,整个集群的任务调度延迟会急剧上升。

5.2 与 Kubernetes 集成:Ray Operator 解析

如果集群已经用 Kubernetes 管理,Ray 官方提供了 Ray Operator(KubeRay)来自动化部署。部署一个 Ray 集群到 K8s 的核心资源是RayCluster:

apiVersion: ray.io/v1 kind: RayCluster metadata: name: example-cluster spec: headGroupSpec: template: spec: containers: - name: ray-head image: rayproject/ray:latest ports: - containerPort: 6379 workerGroupSpecs: - groupName: small-group replicas: 3 template: spec: containers: - name: ray-worker image: rayproject/ray:latest resources: limits: cpu: "4" memory: "8Gi" nvidia.com/gpu: "1"

这里有个相对易踩的坑:Kubernetes 的调度单位是 Pod,而 Ray 的资源调度单位是 CPU/GPU 逻辑单元。如果 Pod 限制了 4 CPU,但 Ray 的任务配置了num_cpus=8,任务会被 hang 住。务必要确保 Ray 任务声明的资源量不超过对应 Pod 的资源上限。

5.3 内存管理:对象存储上限与溢出策略

Ray 的对象存储默认使用节点内存的 30%(可以通过object_store_memory参数调整)。当你使用大 DataFrame 或大模型加载时,这部分内存可能不够。我推荐在生产配置中用环境变量显式指定:

export RAY_object_spilling_threshold=0.7 export RAY_total_memory=1073741824 # 节点对象存储硬上限 1GB

有一个常见的认知误区:对象溢写不能解决所有问题。如果溢写频繁发生,说明你的程序设计上就有问题——你应该用ray.put()把大对象放入对象存储一次,而不是每次调用任务时重新传一遍引用。或者重新设计数据分片粒度,让每个任务处理的数据块更小。

5.4 调度策略优化:数据局部性、优先级与抢占

Ray 默认使用 Local-First 调度策略。它会优先把任务调度到数据所在节点,避免远程数据传输。实践中,如果你有一个 10GB 的大数据集要共享给 1000 个任务,应该用ray.put把它放入对象存储,让任务只传递 ObjectRef。但如果你的数据分片是 1000 个小文件,任务最好各自加载自己需要的文件,而不要预先全部加载到对象存储,因为内存吃不消。

我踩过一次坑:为了图省事,把一个巨大的配置 dict 用ray.put放到了对象存储,然后让 2000 个任务引用它。结果每个节点都要把它解序列化一份放到本地内存,8 个节点的 64GB 内存直接爆了。后来改成每个任务动态读取共享文件,问题才解决。

6. 常见问题与排查技巧:Ray 生产环境的避坑实录

6.1 任务卡死的排查:从 Dashboard 到堆栈追踪

在生产中用 Ray 最痛苦的就是“任务既不报错也不结束”——实际上是卡住了。我遇到过的一个典型案例:任务里调用了一个外部 HTTP 接口,接口没有设置超时,导致任务无限等待。

排查流程如下:

  1. 打开 Ray Dashboard(默认端口 8265),找到对应 Job 下运行中的任务;
  2. 点击任务详情,查看当前执行堆栈;
  3. 如果堆栈显示停在网络等待,说明是外呼依赖问题,应该设置超时或重试。

Ray Dashboard 是一个极好的运维工具,它展示了集群拓扑、节点资源使用率、活跃任务列表、Actor 生命周期。我强烈建议生产环境开一个独立节点专门跑 Dashboard,避免和头节点争抢资源。

6.2 内存泄漏问题的诊断:Pprof 与 Ray 对象存储监控

Python 的内存管理比较隐蔽,使用 Ray 时最容易出现内存问题的地方是:Actor 内部持有大对象但被周期性地重新赋值。旧对象只有在引用计数归零时才会被回收,但如果 Actor 方法里关闭了引用却没有触发 GC,内存就会持续上涨。

我使用过几个有效的排查工具:

  • objgraph(Python)查看对象引用关系;
  • Ray Dashboard 的 Memory 页,查看每个 Actor 的堆内存占用;
  • 给 Actor 里的容器类型加上__del__方法手动清理。

在写长期运行的 Actor 时,我建议在关键路径上加上gc.collect()或者及时释放不用的变量。简单粗暴但确有效果。

6.3 性能瓶颈分析:哪些参数需要重点关注

Ray 任务调度的性能瓶颈经常出在下面几个参数上:

瓶颈原因调优方案
调度延迟高中心调度器过载增加头节点 CPU / 使用张量任务合并
对象传输慢节点间网络带宽不足使用 InfiniBand 或 VPC 内网高带宽实例
序列化开销大默认 pickle 序列化复杂对象使用 Arrow 的pyarrow或 Ray Dataset
负载不均衡数据局部性策略选择不当设置override_environment_variable条件,或使用placement_group

其中序列化开销是最容易被低估的。如果你在任务里传 Python dict,每个 key 都要被 pickle 逐个序列化,性能很差。用ray.dataAPI 或者直接把 dict 转成 numpy/pandas 可以提升 10 倍以上的传输效率。

6.4 生产环境监控:Prometheus + Grafana 实现集群可视化

Ray 内置了 Prometheus 的 metrics 端点(默认 8080 端口),可以用下面的方式配置抓取:

- job_name: 'ray' static_configs: - targets: ['<ray-head-ip>:8080']

我建议重点关注这几个指标:

  • ray_available_resources(可用 CPU/GPU 资源);
  • ray_object_store_memory_used_bytes(对象存储内存使用);
  • ray_pending_tasks(待调度的任务数);
  • ray_func_pending_seconds(任务排队时间)。

把 Grafana 的告警规则设置好,当ray_pending_tasks持续高于一定程度时发送告警,可以提前发现资源不够或任务泄漏的问题。我记得有一次我的集群里有一个无限循环的 Actor 在不断提交任务,就是靠这个指标发现的。

6.5 跨语言集成:Ray 的 Python + Java 互操作

最后再说一个偏实战的方向:Ray 从 1.2 版本开始引入了cross_language调用的实验特性,允许从 Python 调用 Java 任务,反之亦然。在真实业务里,你可能需要用 Python 做数据处理和模型推理,但核心高并发的模块是 Java 写的,这时候跨语言能力就很实用。

from ray import cross_language # 调用 Java 任务的示例 java_class = cross_language.java_actor_class("com.example.MyActor") actor = java_class.remote() result = actor.method.remote("hello")

不过说实话,跨语言功能的 API 变化较快,生产使用前一定要先做兼容性测试。我更常用的替代方案是:在 Java 侧起一个 gRPC 服务,Python 侧用 AsyncActor 调用,这样解耦更彻底,也不会被框架版本拖累。

我个人的真实体验是,Ray 的学习曲线比 Spark 和 Celery 都要陡峭,因为它暴露给开发者的自由度过高。一旦你掌握了 Task、Actor、Object 这三个基本原语,再看它的其余组件(Serve、Tune、RLlib、Dataset)就顺畅了。如果你正在做 AI 训练、在线推理、分布式仿真这类 Spark 和 Celery 都别扭的场景,花两周时间深入 Ray 是值得的。最后再分享一个小技巧:如果条件允许,把 Ray 集群的网络延迟做到 1ms 以下,很多高级模式(比如嵌套任务和流水线并行)的性能表现会有一个本质的提升。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询