Python——Ray-使用笔记


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
    @ray.remote(num_cpus=1)
    class TaskRunner:
    def __init__(self):
    pass
    ...

    @ray.remote
    def new_method():
    pass

嵌套 .remote() 提交任务

  • 在被 .remote() 提交的任务中,如果继续嵌套 .remote() 提交任务,是被允许的
    • 嵌套 .remote() 会创建一个新的独立任务,并将其提交给Ray的全局调度器
    • .remote() 这是一个非阻塞调用,调用后会立即返回一个对象引用

哪些时候需要 .remote() 来调用函数?

  • 说明:.remote() 是 ray 中调用远程函数的命令,可以同时传递函数参数,比如 x.remote(123)
  • 补充知识:
    • Ray Task 是使用 @ray.remote 装饰的普通函数
    • Ray Actor 是使用 @ray.remote 装饰的类
  • 需要调用 .remote() 函数的情况有:
    • 调用 Ray Task(使用 @ray.remote 装饰的普通函数),调用远程 task 任务(通过 @ray.remote 修正的函数)
    • 初始化 Ray Actor (使用 @ray.remote 装饰的类)对象
    • 调用 Ray Actor 对象的函数(包括静态方法和类方法)时
      • 注:Ray Actor 定义时也是可以有静态方法的

@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
    10
    def 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.remote() 在功能上完全等价于 @ray.remote

    • @ray.remote@ray.remote() 最终调用的是同一个底层函数(return_wrap_function_or_class),传入同样的空配置,所以结果完全等价

为什么推荐 @ray.remote 而不是 @ray.remote()

  • 原因一:Python 装饰器惯例

    • 在 Python 中,当装饰器不需要任何参数时,标准写法是不加括号
      1
      2
      3
      4
      5
      # Python 标准惯例
      @staticmethod

      # 而不是
      @staticmethod() # 虽然能工作,但显得多余
  • 原因二:性能细微差异(几乎可忽略)

    • 每次调用 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)回退启动 :如果以上均未找到,则自动在当前机器启动一个全新的本地单机集群
  • 注:ray.init() 无参调用能自动连接的特性,仅生效于运行了 ray start --head 的头节点
    • 如果在远程 Worker 节点或其他机器上执行代码,必须显式传入 address 参数,否则它会误以为要启动一个新本地集群

常见参数配置(资源调控)

  • init() 可以无参调用,但可以通过显式参数覆盖资源声明:
    • num_cpus / num_gpus
      • 手动指定当前节点可用的 CPU/GPU 数量(例如 ray start --num-cpus=1000,在代码里也可通过参数覆盖)
    • 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
    48
    import 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
    25
    import 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
    @ray.remote
    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) 上
    @ray.remote
    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
      61
      import 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 的资源需求必须能在集群的单个节点 上被满足
    • 例如,如果一个节点只有 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
    20
    import 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 # 验证通过

    @ray.remote
    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
      29
      import ray

      # 1. 定义一个 Actor 类
      @ray.remote
      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}")

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 不存在,抛出 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
    33
    import ray

    @ray.remote
    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
    5
    try:
    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() 将无法再获取到它

ray.get_actor() 其他注意事项

  • 名字要唯一 :在同一命名空间内,Actor 的名字必须全局唯一
  • 命名空间要一致 :获取 Actor 时,必须确保当前作业的命名空间与目标 Actor 所在的命名空间一致
  • 处理异常 :总是假设 Actor 可能不存在,并用 try...exceptget_if_exists 来妥善处理