ray.remote() 函数将普通类/函数封装为 Actor/Task
ray.remote()函数会返回一个装饰器,可用于 将普通类/函数封装为 Actor/Task示例代码:
1
2
3
4
5
6
7
8
9
10
11
12# 下面的代码是可以分两步理解:
## 第一步:ray.remote(num_cpus=1),返回一个装饰器函数
## 第二步:将返回的装饰器应用到 TaskRunner 上,将 TaskRunner 转换为一个远程可执行的对象
class TaskRunner:
def __init__(self):
pass
...
def new_method():
pass
TaskRunner = ray.remote(num_cpus=1)(TaskRunner) # Python 中的变量名只是标签,这行代码将 TaskRunner 重新绑定到 Ray Actor 类,覆盖了原始的类定义
new_method = ray.remote()(new_method)- 这行代码是 Ray 中一种装饰器调用方式,用于定义一个远程任务(Task)或远程 Actor 类
上面示例代码的等价实现 是:
1
2
3
4
5
6
7
8
9
class TaskRunner:
def __init__(self):
pass
...
def new_method():
pass
嵌套 .remote() 提交任务
- 在被
.remote()提交的任务中,如果继续嵌套.remote()提交任务,是被允许的- 嵌套
.remote()会创建一个新的独立任务,并将其提交给Ray的全局调度器 .remote()这是一个非阻塞调用,调用后会立即返回一个对象引用
- 嵌套
哪些时候需要 .remote() 来调用函数?
- 说明:
.remote()是 ray 中调用远程函数的命令,可以同时传递函数参数,比如x.remote(123) - 补充知识:
- Ray Task 是使用
@ray.remote装饰的普通函数 - Ray Actor 是使用
@ray.remote装饰的类
- Ray Task 是使用
- 需要调用
.remote()函数的情况有:- 调用 Ray Task(使用
@ray.remote装饰的普通函数),调用远程 task 任务(通过@ray.remote修正的函数) - 初始化 Ray Actor (使用
@ray.remote装饰的类)对象 - 调用 Ray Actor 对象的函数(包括静态方法和类方法)时
- 注:Ray Actor 定义时也是可以有静态方法的
- 调用 Ray Task(使用
被 @ray.remote 装饰的普通函数还能单独执行吗?
- 不能,只能使用
.remote()执行,直接执行会报错
Ray Actor 可以没有状态吗?
- Ray Actor 可以没有状态,但这样的话会浪费资源,不如使用 Ray Task
- Task 在执行完成后释放资源
- Actor 会持续占用资源,即使空闲
- 注:如果初始化成本高(如加载模型),使用 Actor 管理这些资源是合理的
Ray Actor 定义时,可以针对内部方法调整 cpu 配置吗?
- 使用
@ray.remote(num_cpus=1)来修饰类以后,还可以修饰方法吗?- 在 Ray 中,Actor 的资源(如 CPU)是在 Actor 类定义时整体指定的,无法在内部方法上独立设置
- 理解一下,这是由 Ray Actor 的设计机制决定的:
- 一个 Actor 代表一个在独立进程中运行的、有状态的服务
- Actor 本身的资源(如 num_cpus)决定了调度器何时启动这个进程,并赋予它相应的资源配额
- Actor 内部的所有方法都共享这个资源池
- 除了定义,真正调用 Actor 的 方法时,还可以重新绑定节点吗?
- 不可以,一旦 Actor 在某个节点上被创建,其所有方法调用都会被调度到该 Actor 所在的同一节点上执行
- 理解,因为 Actor 中包含了状态,Actor 的方法往往需要访问这个状态,这个绑定关系在 Actor 的整个生命周期内保持不变
为什么使用装饰器时 一般 使用 @ray.remote(num_cpus=1) 和 @ray.remote,而不是 @ray.remote()
- TLDR:
@ray.remote(num_cpus=1)和@ray.remote都是正确的,但@ray.remote()也能工作,只是很少被使用 - 关键理解:
ray.remote既是一个装饰器,又是一个装饰器 Factory
从源码看 ray.remote 的两种调用方式
Ray 源码中
ray.remote的设计思路(简化版):1
2
3
4
5
6
7
8
9
10def remote(*args, **kwargs):
"""既可以作为装饰器Factory,也可以直接作为装饰器"""
if len(args) == 1 and len(kwargs) == 0 and callable(args[0]):
# 情况1:直接作为装饰器,如 @ray.remote
return_wrap_function_or_class(args[0], {})
else:
# 情况2:作为装饰器工厂,如 @ray.remote(num_cpus=1)
def decorator(cls_or_func):
return_wrap_function_or_class(cls_or_func, kwargs)
return decorator- 当写
@ray.remote时(无括号):ray.remote被直接调用,传入的是被装饰的函数/类- Ray 检测到第一个参数是可调用对象,直接封装并返回
- 此时相当于
task = ray.remote(task)
- 当写
@ray.remote(num_cpus=1)时(有参数)ray.remote先接收配置参数,返回一个装饰器函数 ,这个装饰器函数再接收被装饰的函数/类- 此时相当于
task = ray.remote(num_cpus=1)(task)
- 当写
@ray.remote()时(有括号无参数):- Ray 检查参数:第一个参数不是可调用对象(因为
args为空),所以进入else分支 - 返回一个空的装饰器
decorator(相当于@ray.remote()等价于@ray.remote()返回的装饰器) - 这个装饰器再接收被装饰的函数/类,用空的配置(默认参数)封装
- 此时相当于
task = ray.remote()(task)
- Ray 检查参数:第一个参数不是可调用对象(因为
- 当写
理解:
@ray.remote()在功能上完全等价于@ray.remote@ray.remote和@ray.remote()最终调用的是同一个底层函数(return_wrap_function_or_class),传入同样的空配置,所以结果完全等价
为什么推荐 @ray.remote 而不是 @ray.remote()?
原因一:Python 装饰器惯例
- 在 Python 中,当装饰器不需要任何参数时,标准写法是不加括号 :
1
2
3
4
5# Python 标准惯例
# 而不是
- 在 Python 中,当装饰器不需要任何参数时,标准写法是不加括号 :
原因二:性能细微差异(几乎可忽略)
- 每次调用
ray.remote()都会创建一个新的装饰器函数对象- 虽然这个开销极小,但在大规模定义时,
@ray.remote直接引用已存在的装饰器更高效
- 虽然这个开销极小,但在大规模定义时,
- 每次调用
ray.init() 在做什么?
ray.init()是 Ray 框架的入口函数 ,它的本质是将当前 Python 进程注册并连接到 Ray 的分布式运行时环境中- 无论是想使用多核并行还是分布式集群,都必须先通过它建立通信桥梁
连接地址解析逻辑
ray.init()决定“连接谁”时,遵循一套严格的优先级顺序 :- 1)环境变量 :检查系统是否设置了
RAY_ADDRESS - 2)显式参数 :检查
address参数是否传入了具体 IP(如ray.init(address="ray://10.0.0.1:10001")) - 3)自动发现(
auto模式) :如果以上未指定,进入auto模式- 它会扫描本地磁盘(通常是
/tmp/ray/目录),查找是否有通过ray start命令启动并留下的集群元数据文件- 如果有,则直接复用该集群
- 注:
ray start命令会留下元数据,所以需要调用
ray.init()函数的address参数的默认值即为"auto",即ray.init()与ray.init("auto")的两者完全等价- 建议选择
ray.init("auto")For 代码可读性
- 建议选择
- 它会扫描本地磁盘(通常是
- 4)回退启动 :如果以上均未找到,则自动在当前机器启动一个全新的本地单机集群
- 1)环境变量 :检查系统是否设置了
- 注:
ray.init()无参调用能自动连接的特性,仅生效于运行了ray start --head的头节点- 如果在远程 Worker 节点或其他机器上执行代码,必须显式传入
address参数,否则它会误以为要启动一个新本地集群
- 如果在远程 Worker 节点或其他机器上执行代码,必须显式传入
常见参数配置(资源调控)
init()可以无参调用,但可以通过显式参数覆盖资源声明:num_cpus/num_gpus:- 手动指定当前节点可用的 CPU/GPU 数量(例如
ray start --num-cpus=1000,在代码里也可通过参数覆盖)
- 手动指定当前节点可用的 CPU/GPU 数量(例如
resources:- 定义自定义资源标签(如
{'heavy_task': 10}),用于更精细的任务调度
- 定义自定义资源标签(如
ignore_reinit_error=True:- 允许在同一个脚本中多次调用而不报错(常用于 Jupyter Notebook 环境)
其他实践经验
- 建议在程序末尾调用
ray.shutdown()释放资源- 若进程结束未调用,运行时也会被终止
- 在 Ray >= 1.5 中,显式调用
ray.init()不再是强制性的- 当第一次使用
@ray.remote装饰任务或 Actor 时,Ray 会在后台自动为你执行一次默认的ray.init() - 但为了确保资源参数的精确控制和连接目标的可控性,显式调用依然是最佳工程实践
- 在程序的最开始就调用
- 当第一次使用
ray.nodes() 函数使用说明
ray.nodes()获取当前 Ray 集群中所有节点的信息列表,每个节点是一个字典- 示例代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48import ray
import json
if not ray.is_initialized():
ray.init(ignore_reinit_error=True)
print("Ray 已初始化(本地模式)")
else:
print("Ray 已存在,直接连接")
# 获取所有节点信息并打印
nodes = ray.nodes()
print(f"\n获取到 {len(nodes)} 个节点:")
print(json.dumps(nodes, ensure_ascii=False, indent=2))
ray.shutdown()
# Ray 已初始化(本地模式)
#
# 获取到 1 个节点:
# [
# {
# "NodeID": "3efe7fbc715fe9876107ae77627053895f47dcdf385ad09ba14a448a",
# "Alive": true,
# "NodeManagerAddress": "127.0.0.1",
# "NodeManagerHostname": "anonymous",
# "NodeManagerPort": 54395,
# "ObjectManagerPort": 54394,
# "ObjectStoreSocketName": "/tmp/ray/session_2026-07-25_18-36-26_135438_91634/sockets/plasma_store",
# "RayletSocketName": "/tmp/ray/session_2026-07-25_18-36-26_135438_91634/sockets/raylet",
# "MetricsExportPort": 54408,
# "MetricsAgentPort": 54407,
# "DashboardAgentListenPort": 52365,
# "NodeName": "127.0.0.1",
# "RuntimeEnvAgentPort": 54405,
# "DeathReason": 0,
# "DeathReasonMessage": "",
# "alive": true,
# "Resources": {
# "memory": 10712793088.0,
# "CPU": 12.0,
# "object_store_memory": 2147483648.0,
# "node:127.0.0.1": 1.0,
# "node:__internal_head__": 1.0
# },
# "Labels": {
# "ray.io/node-id": "3efe7fbc715fe9876107ae77627053895f47dcdf385ad09ba14a448a"
# }
# }
# ]
placement_group 函数使用说明
- Ray 的
placement_group函数用于在集群中原子性地预留一组资源(即“全部成功或全部失败”的 gang scheduling)- 主要用来确保一组紧密协作的任务或 Actor 能同时获得所需资源,避免死锁或部分启动
placement_group函数位于ray.util.placement_group模块中placement_group的核心是资源束(Bundle) ,每个 Bundle 定义了一组资源需求,且必须能完整地容纳在集群的单个节点上
placement_group 函数基本用法
使用示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25import ray
from ray.util.placement_group import placement_group
# 初始化 Ray(本地单节点模式)
if not ray.is_initialized():
ray.init(ignore_reinit_error=True)
print("Ray 已初始化(本地模式)")
else:
print("已连接到现有 Ray 集群")
# 定义资源束(Bundles)
# 例如:创建两个资源束,分别需要2个GPU和2个自定义资源
bundle1 = {"CPU": 2}
# bundle1 = {"CPU": 2, "GPU": 1} # 也可这样定义两个 CPU 和一个 GPU
bundle2 = {"extra_resource": 2} # 自定义资源是用于管理资源的高阶手段,在初始化 ray 集群时可以声明每个节点的自定义资源
# 创建 Placement Group
# 参数:bundles (列表), strategy (策略, 默认为 "PACK")
pg = placement_group([bundle1, bundle2], strategy="STRICT_PACK")
# 等待 Placement Group 创建完成(资源就绪)
# 使用 ray.get 同步等待
ray.get(pg.ready())
# 或使用 ray.wait 异步等待,后续再通过 ready 来读取
# ready, unready = ray.wait([pg.ready()], timeout=10)主要参数说明
bundles: 一个列表,其中每个元素都是一个字典,描述一个资源束的资源需求- 例如
[{"CPU": 1}, {"CPU": 2, "GPU": 1}]
- 例如
strategy: 字符串,指定资源束在集群节点上的放置策略- 默认为
"PACK",Ray 支持四种策略:策略 类型 描述 "PACK"软(尽力而为) 尽可能将所有 Bundle 放在同一个节点上,若节点资源不足,则分散到其他节点 "SPREAD"软(尽力而为) 尽可能将 Bundle 分散到不同节点上,若节点数少于 Bundle 数,则多个 Bundle 可放在同一节点 "STRICT_PACK"硬(严格) 强制要求所有 Bundle 必须在同一个节点上,否则创建失败 "STRICT_SPREAD"硬(严格) 强制要求每个 Bundle 必须在不同节点上,否则创建失败
- 默认为
在任务和 Actor 中使用 Placement Group
创建好 Placement Group 后,需要将任务或 Actor 调度到它预留的资源上,这通过
scheduling_strategy参数实现:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29# 此处省略前面的 pg 定义和资源申请代码 after ray.get(pg.ready())
from ray.util.scheduling_strategies import PlacementGroupSchedulingStrategy
# 定义一个使用 Placement Group 中第一个 Bundle (索引0) 的 Actor
class MyActor:
def__init__(self):
pass
# 调度 Actor 到 pg 的第一个 Bundle 上
# 一个 Actor 实例只能放到一个 Bundle 上,但可以初始化多个相同类的实例放到多个 Bundle 上并行
actor = MyActor.options(
scheduling_strategy=PlacementGroupSchedulingStrategy(
placement_group=pg,
placement_group_bundle_index=0 # 指定使用哪个 Bundle
)
).remote()
# 调度任务到 pg 的第二个 Bundle (索引1) 上
def my_task():
return "Hello"
task_ref = my_task.options(
scheduling_strategy=PlacementGroupSchedulingStrategy(
placement_group=pg,
placement_group_bundle_index=1
)
).remote()- Bundle 资源申请和使用的最小单位(使用时可以将任务分配到指定的 Bundle 上执行)
管理 Placement Group
查看状态 : 可以使用
placement_group_table(pg.id)函数查看 Placement Group 的详细信息(ray._private.state.state.placement_group_table)- 代码示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61import ray
from ray.util.placement_group import placement_group
# 初始化 Ray(本地单节点模式)
if not ray.is_initialized():
ray.init(ignore_reinit_error=True)
print("Ray 已初始化(本地模式)")
else:
print("已连接到现有 Ray 集群")
# 创建一个 Placement Group
# 定义资源束:每个束需要 1 个 CPU,总共创建 2 个束
bundles = [{"CPU": 1} for _ in range(2)]
# 使用 STRICT_PACK 策略确保所有束在同一个节点上
pg = placement_group(bundles, strategy="STRICT_PACK", name="demo_pg")
# 等待 Placement Group 就绪(资源分配完成)
ray.get(pg.ready())
specs = ray._private.state.state.placement_group_table(pg.id)
# 打印原始状态表
print("\n--- Placement Group 状态表 ---")
# 使用 pprint 更美观地输出
import pprint
pprint.pprint(specs)
print("\n--- 核心内容 ---")
print(f"placement_group_id: {specs.get('placement_group_id')}")
print(f"name: {specs.get('name')}")
print(f"state: {specs.get('state')} # 例如 PENDING, CREATED, REMOVED")
print(f"bundles: {specs.get('bundles')}") # 每个束的资源需求
# bundles_to_node_id 是一个列表,索引对应束索引,值为节点 ID
bundles_to_node = specs.get("bundles_to_node_id", [])
# 可以看到不同 bundle 分配到同一个节点上
print(f"bundles_to_node_id: {bundles_to_node}")
ray.shutdown()
# Ray 已初始化(本地模式)
#
# --- Placement Group 状态表 ---
# {'bundles': {0: {'CPU': 1.0}, 1: {'CPU': 1.0}},
# 'bundles_to_node_id': {0: '563f3887740740a1622d29438f3e6dec3ed7230a4bcbdd7fc5b25543',
# 1: '563f3887740740a1622d29438f3e6dec3ed7230a4bcbdd7fc5b25543'},
# 'name': 'demo_pg',
# 'placement_group_id': 'ec1ba7e151e7b2885eb6e8b254a101000000',
# 'state': 'CREATED',
# 'stats': {'end_to_end_creation_latency_ms': 6.231,
# 'highest_retry_delay_ms': 0.0,
# 'scheduling_attempt': 1,
# 'scheduling_latency_ms': 6.179,
# 'scheduling_state': 'FINISHED'},
# 'strategy': 'STRICT_PACK'}
#
# --- 核心内容 ---
# placement_group_id: ec1ba7e151e7b2885eb6e8b254a101000000
# name: demo_pg
# state: CREATED # 例如 PENDING, CREATED, REMOVED
# bundles: {0: {'CPU': 1.0}, 1: {'CPU': 1.0}}
# bundles_to_node_id: {0: '563f3887740740a1622d29438f3e6dec3ed7230a4bcbdd7fc5b25543', 1: '563f3887740740a1622d29438f3e6dec3ed7230a4bcbdd7fc5b25543'}
- 代码示例:
清理 : 当不再需要时,可以使用
ray.util.placement_group.remove_placement_group(pg)函数释放其占用的资源
补充:关于 placement_group 的其他注意事项
- 原子性 : Placement Group 的创建是原子性的
- 如果任何一个 Bundle 无法被放置,整个 Placement Group 都会处于
PENDING状态,直到所有资源同时可用
- 如果任何一个 Bundle 无法被放置,整个 Placement Group 都会处于
- 节点限制 : 每个 Bundle 的资源需求必须能在集群的单个节点 上被满足
- 例如,如果一个节点只有 8 个CPU,而 Bundle 需要 9 个,则该 Bundle 无法被调度
- 自动扩缩容 : Ray 的自动扩缩容机制能够感知到因资源不足而处于
PENDING状态的 Placement Group,并尝试添加新节点来满足其需求
去除 @ray.remote 封装
- 示例代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20import ray
# 定义一个普通类
class MyClass:
def __init__(self, name):
self.name = name
# 使用 @ray.remote 装饰,得到一个 ActorClass 对象
MyActorClass = ray.remote(MyClass)
# 访问 __ray_actor_class__ 属性,会得到原始的 MyClass
assert MyActorClass.__ray_actor_class__ is MyClass # 验证通过
class MyActorClass:
def __init__(self, name):
self.name = name
# assert MyActorClass.__ray_actor_class__ is MyClass # 出错
print(MyActorClass.__ray_actor_class__) # <class '__main__.MyActorClass'>
ray.get_actor() 的用法
ray.get_actor()是 Ray 中用于获取一个已存在的、有名字的 Actor(Named Actor)的句柄(Handle) 的核心 API- 通过这个句柄,可以像操作本地对象一样,远程调用该 Actor 的方法
- 主要应用场景是在不同的 Ray 任务(Task)或作业(Job)之间,共享和访问同一个有状态的 Actor 实例
核心用法:基础示例
- 下面是一个典型的使用流程:
- 1)定义并创建 Actor :使用
Actor.options(name="unique_name").remote()创建一个有名字的 Actor - 2)在另一个地方获取句柄 :在同一个或不同的 Ray 任务/脚本中,使用
ray.get_actor("unique_name")获取它的句柄 - 3)通过句柄调用方法 :使用获取到的句柄远程调用 Actor 的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29import ray
# 1. 定义一个 Actor 类
class Counter:
def__init__(self):
self.count = 0
def increment(self):
self.count += 1
return self.count
# 初始化 Ray
ray.init()
# 2. 在某个地方(例如 Job 1)创建这个 Actor,并给它起名为 "my_counter"
# lifetime="detached" 使其在创建它的作业结束后依然存活
counter_actor = Counter.options(name="my_counter", lifetime="detached").remote()
# 3. 在另一个地方(例如 Job 2 或另一个脚本),通过名字获取它的句柄
# 假设这是另一个独立的 Python 脚本或任务
# ray.init() # 如果尚未初始化
try:
# 获取已存在的 Actor 句柄
actor_handle = ray.get_actor("my_counter")
# 调用其方法
result = ray.get(actor_handle.increment.remote())
print(f"Counter value: {result}") # 输出: Counter value: 1
except ValueError as e:
print(f"Actor not found: {e}")
- 1)定义并创建 Actor :使用
ray.get_actor() API 详解
ray.get_actor的定义如下:1
ray.get_actor(name: str, namespace: str | None = None) -> ActorHandle
参数:
name(str): 必需- 要获取的 Actor 的名字
- 这个名字在创建时通过
Actor.options(name="...")指定
namespace(str | None, 可选): Actor 所属的命名空间(Namespace)- 如果不指定 (
None),则默认在当前作业(Job)的命名空间中查找 - 命名空间是 Ray 中用于逻辑隔离不同作业和命名 Actor 的机制。在同一命名空间内,Actor 的名字必须唯一
- 如果不指定 (
返回值
- 成功时,返回一个
ActorHandle对象- 可以像使用普通 Actor 句柄一样,通过它来远程调用方法(例如
handle.method.remote())
- 可以像使用普通 Actor 句柄一样,通过它来远程调用方法(例如
- 如果指定的 Actor 不存在,抛出
ValueError
- 成功时,返回一个
重要概念:命名空间 (Namespace)
- 隔离性 :命名 Actor 只能在其所属的命名空间内被访问
- 不同命名空间中的同名 Actor 互不影响
- 设置方式 :可以在初始化 Ray 时设置当前作业的命名空间
- 命名空间的作用展示(参考自 Ray 官方文档):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33import ray
class Actor:
pass
# --- Job 1: 在 "colors" 命名空间创建 Actor ---
# 连接到集群,并指定命名空间为 "colors"
ray.init(address="ray://localhost:10001", namespace="colors")
# 创建两个 Actor,名为 "orange" 和 "purple",并设置为 Detached
Actor.options(name="orange", lifetime="detached").remote()
Actor.options(name="purple", lifetime="detached").remote()
ray.shutdown() # 断开连接
# --- Job 2: 在 "fruits" 命名空间尝试获取 Actor ---
ray.init(address="ray://localhost:10001", namespace="fruits")
# 这行会失败,因为 "orange" 在 "colors" 命名空间,当前在 "fruits" 中找不到
try:
ray.get_actor("orange")
except ValueError:
print("Actor 'orange' not found in 'fruits' namespace.")
# 但是可以在 "fruits" 命名空间中创建一个新的 "orange" Actor
Actor.options(name="orange", lifetime="detached").remote()
# 这行会成功,因为它获取的是当前 "fruits" 命名空间下的 "orange"
orange_in_fruits = ray.get_actor("orange")
ray.shutdown()
# --- Job 3: 再次连接到 "colors" 命名空间 ---
ray.init(address="ray://localhost:10001", namespace="colors")
# 这会成功获取 Job 1 中创建的 "orange" Actor,而不是 Job 2 中的那个
orange_in_colors = ray.get_actor("orange")
print(orange_in_colors) # 句柄指向 "colors" 命名空间中的 "orange" Actor
ray.shutdown()
进阶用法与注意事项
优雅地处理 Actor 不存在的情况
- 由于 Actor 可能不存在,直接调用
ray.get_actor()会抛出ValueError - 最佳实践是使用
try...except块来捕获异常,并进行相应的处理(例如创建新的 Actor)1
2
3
4
5try:
actor_handle = ray.get_actor("my_actor")
except ValueError:
# Actor 不存在,执行创建逻辑
actor_handle = MyActor.options(name="my_actor", lifetime="detached").remote()
使用 get_if_exists 原子地“获取或创建”
- 为了更优雅地处理“获取或创建”的场景,Ray 提供了
get_if_exists选项 - 在创建 Actor 时设置此选项,如果同名 Actor 已存在,则直接返回其句柄,否则创建新的
1
2
3
4
5
6
7# 这个操作是原子的,避免了竞态条件
actor_handle = MyActor.options(
name="my_actor",
namespace="my_namespace",
get_if_exists=True,
lifetime="detached"
).remote()
超时设置
ray.get_actor()是一个同步调用 ,默认超时时间为 60 秒- 如果在这个时间内无法从 Ray 的全局控制存储(GCS)中获取到 Actor 信息,调用会失败
- 可以通过设置环境变量
RAY_gcs_server_request_timeout_seconds来修改这个超时时间
Detached Actor 与 Non-detached Actor
ray.get_actor()适用于两种类型的命名 Actor:- Detached Actor :生命周期独立于创建它的作业,即使创建它的 Python 进程退出,Actor 依然存活,可供后续作业获取和使用。这是实现跨作业共享的推荐方式
- Non-detached Actor :生命周期与创建它的作业绑定
- 一旦创建它的作业结束,Actor 就会被销毁,
ray.get_actor()将无法再获取到它
- 一旦创建它的作业结束,Actor 就会被销毁,
ray.get_actor() 其他注意事项
- 名字要唯一 :在同一命名空间内,Actor 的名字必须全局唯一
- 命名空间要一致 :获取 Actor 时,必须确保当前作业的命名空间与目标 Actor 所在的命名空间一致
- 处理异常 :总是假设 Actor 可能不存在,并用
try...except或get_if_exists来妥善处理