内存管理#

本页介绍 Ray 中内存管理的工作原理。

另请参阅 调试内存不足,了解如何解决内存不足问题。

概念#

Ray 应用程序有几种内存使用方式

../../_images/memory.svg
Ray 系统内存:这是 Ray 内部使用的内存
  • GCS:用于存储集群中存在的节点和 actor 列表的内存。用于这些目的的内存量通常很小。

  • Raylet:每个节点上运行的 C++ raylet 进程使用的内存。这无法控制,但通常很小。

应用程序内存:这是您的应用程序使用的内存
  • Worker 堆:您的应用程序使用的内存(例如,在 Python 代码或 TensorFlow 中),最好通过命令(如 top)中的应用程序的常驻集大小 (RSS) 减去其共享内存使用量 (SHR) 来衡量。之所以需要减去SHR,是因为对象存储共享内存被操作系统报告为与每个 worker 共享。如果不减去SHR,将会重复计算内存使用量。

  • 对象存储内存:当您的应用程序通过 ray.put 在对象存储中创建对象以及从远程函数返回值时使用的内存。对象是引用计数的,并在超出范围时被逐出。每个节点上都会运行一个对象存储服务器。默认情况下,在启动实例时,Ray 会预留 30% 的可用内存。对象存储的大小可以通过 –object-store-memory 进行控制。默认情况下,内存会分配给 Linux 的 /dev/shm(共享内存)。对于 MacOS,Ray 使用 /tmp(磁盘),这可能会影响性能,相比之下不如 Linux。在 Ray 1.3+ 中,如果对象存储已满,对象将被 溢出到磁盘

  • 对象存储共享内存:当您的应用程序通过 ray.get 读取对象时使用的内存。请注意,如果对象已存在于节点上,则不会产生额外的分配。这使得大型对象可以在许多 actor 和任务之间高效共享。

ObjectRef 引用计数#

Ray 实现分布式引用计数,以便集群中任何在作用域内的 ObjectRef 都会被固定在对象存储中。这包括本地 Python 引用、待处理任务的参数以及序列化在其他对象内部的 ID。

使用 ‘ray memory’ 进行调试#

可以使用 ray memory 命令来帮助跟踪哪些 ObjectRef 引用在作用域内,并可能导致 ObjectStoreFullError

在 Ray 应用程序运行时从命令行运行 ray memory 将为您提供集群中当前由驱动程序、actor 和任务持有的所有 ObjectRef 引用的转储。

======== Object references status: 2021-02-23 22:02:22.072221 ========
Grouping by node address...        Sorting by object size...


--- Summary for node address: 192.168.0.15 ---
Mem Used by Objects  Local References  Pinned Count  Pending Tasks  Captured in Objects  Actor Handles
287 MiB              4                 0             0              1                    0

--- Object references for node address: 192.168.0.15 ---
IP Address    PID    Type    Object Ref                                                Size    Reference Type      Call Site
192.168.0.15  6465   Driver  ffffffffffffffffffffffffffffffffffffffff0100000001000000  15 MiB  LOCAL_REFERENCE     (put object)
                                                                                                                  | test.py:
                                                                                                                  <module>:17

192.168.0.15  6465   Driver  a67dc375e60ddd1affffffffffffffffffffffff0100000001000000  15 MiB  LOCAL_REFERENCE     (task call)
                                                                                                                  | test.py:
                                                                                                                  :<module>:18

192.168.0.15  6465   Driver  ffffffffffffffffffffffffffffffffffffffff0100000002000000  18 MiB  CAPTURED_IN_OBJECT  (put object)  |
                                                                                                                   test.py:
                                                                                                                  <module>:19

192.168.0.15  6465   Driver  ffffffffffffffffffffffffffffffffffffffff0100000004000000  21 MiB  LOCAL_REFERENCE     (put object)  |
                                                                                                                   test.py:
                                                                                                                  <module>:20

192.168.0.15  6465   Driver  ffffffffffffffffffffffffffffffffffffffff0100000003000000  218 MiB  LOCAL_REFERENCE     (put object)  |
                                                                                                                  test.py:
                                                                                                                  <module>:20

--- Aggregate object store stats across all nodes ---
Plasma memory usage 0 MiB, 4 objects, 0.0% full

此输出中的每个条目对应于当前在对象存储中固定对象的 ObjectRef,以及引用所在的位置(在驱动程序中、在 worker 中等)、引用类型(有关引用类型的详细信息,请参见下文)、对象的大小(以字节为单位)、对象被实例化的进程 ID 和 IP 地址,以及引用在应用程序中创建的位置。

ray memory 具有使内存调试体验更有效的特性。例如,您可以添加参数 sort-by=OBJECT_SIZEgroup-by=STACK_TRACE,这可能对跟踪发生内存泄漏的代码行特别有帮助。您可以通过运行 ray memory --help 来查看所有选项。

有五种类型的引用可以使对象保持固定

1. 本地 ObjectRef 引用

import ray

@ray.remote
def f(arg):
    return arg

a = ray.put(None)
b = f.remote(None)

在此示例中,我们创建了两个对象的引用:一个通过 ray.put() 放入对象存储,另一个是 f.remote() 的返回值。

--- Summary for node address: 192.168.0.15 ---
Mem Used by Objects  Local References  Pinned Count  Pending Tasks  Captured in Objects  Actor Handles
30 MiB               2                 0             0              0                    0

--- Object references for node address: 192.168.0.15 ---
IP Address    PID    Type    Object Ref                                                Size    Reference Type      Call Site
192.168.0.15  6867   Driver  ffffffffffffffffffffffffffffffffffffffff0100000001000000  15 MiB  LOCAL_REFERENCE     (put object)  |
                                                                                                                  test.py:
                                                                                                                  <module>:12

192.168.0.15  6867   Driver  a67dc375e60ddd1affffffffffffffffffffffff0100000001000000  15 MiB  LOCAL_REFERENCE     (task call)
                                                                                                                  | test.py:
                                                                                                                  :<module>:13

ray memory 的输出中,我们可以看到其中每个对象在驱动程序进程中都被标记为 LOCAL_REFERENCE,但“引用创建位置”中的注释表明第一个对象是作为“put 对象”创建的,第二个对象是来自“任务调用”。

2. 对象固定在内存中

import numpy as np

a = ray.put(np.zeros(1))
b = ray.get(a)
del a

在此示例中,我们创建一个 numpy 数组,然后将其存储在对象存储中。然后,我们从对象存储中获取同一个 numpy 数组并删除其 ObjectRef。在这种情况下,对象仍然被固定在对象存储中,因为反序列化的副本(存储在 b 中)直接指向对象存储中的内存。

--- Summary for node address: 192.168.0.15 ---
Mem Used by Objects  Local References  Pinned Count  Pending Tasks  Captured in Objects  Actor Handles
243 MiB              0                 1             0              0                    0

--- Object references for node address: 192.168.0.15 ---
IP Address    PID    Type    Object Ref                                                Size    Reference Type      Call Site
192.168.0.15  7066   Driver  ffffffffffffffffffffffffffffffffffffffff0100000001000000  243 MiB  PINNED_IN_MEMORY   test.
                                                                                                                  py:<module>:19

ray memory 的输出将此显示为对象 PINNED_IN_MEMORY。如果我们执行 del b,则可以释放引用。

3. 待处理任务引用

@ray.remote
def f(arg):
    while True:
        pass

a = ray.put(None)
b = f.remote(a)

在此示例中,我们首先通过 ray.put() 创建一个对象,然后提交一个依赖于该对象的任务。

--- Summary for node address: 192.168.0.15 ---
Mem Used by Objects  Local References  Pinned Count  Pending Tasks  Captured in Objects  Actor Handles
25 MiB               1                 1             1              0                    0

--- Object references for node address: 192.168.0.15 ---
IP Address    PID    Type    Object Ref                                                Size    Reference Type      Call Site
192.168.0.15  7207   Driver  a67dc375e60ddd1affffffffffffffffffffffff0100000001000000  ?       LOCAL_REFERENCE     (task call)
                                                                                                                    | test.py:
                                                                                                                  :<module>:29

192.168.0.15  7241   Worker  ffffffffffffffffffffffffffffffffffffffff0100000001000000  10 MiB  PINNED_IN_MEMORY    (deserialize task arg)
                                                                                                                    __main__.f

192.168.0.15  7207   Driver  ffffffffffffffffffffffffffffffffffffffff0100000001000000  15 MiB  USED_BY_PENDING_TASK  (put object)  |
                                                                                                                  test.py:
                                                                                                                  <module>:28

当任务运行时,我们看到 ray memory 在驱动程序进程中显示该对象的 LOCAL_REFERENCEUSED_BY_PENDING_TASK 引用。worker 进程也持有对该对象的引用,因为 Python arg 直接引用了 plasma 中的内存,因此它不能被逐出;因此它是 PINNED_IN_MEMORY

4. 序列化的 ObjectRef 引用

@ray.remote
def f(arg):
    while True:
        pass

a = ray.put(None)
b = f.remote([a])

在此示例中,我们再次通过 ray.put() 创建一个对象,然后将其传递给一个包装在另一个对象(在此例中为一个列表)中的任务。

--- Summary for node address: 192.168.0.15 ---
Mem Used by Objects  Local References  Pinned Count  Pending Tasks  Captured in Objects  Actor Handles
15 MiB               2                 0             1              0                    0

--- Object references for node address: 192.168.0.15 ---
IP Address    PID    Type    Object Ref                                                Size    Reference Type      Call Site
192.168.0.15  7411   Worker  ffffffffffffffffffffffffffffffffffffffff0100000001000000  ?       LOCAL_REFERENCE     (deserialize task arg)
                                                                                                                    __main__.f

192.168.0.15  7373   Driver  a67dc375e60ddd1affffffffffffffffffffffff0100000001000000  ?       LOCAL_REFERENCE     (task call)
                                                                                                                  | test.py:
                                                                                                                  :<module>:38

192.168.0.15  7373   Driver  ffffffffffffffffffffffffffffffffffffffff0100000001000000  15 MiB  USED_BY_PENDING_TASK  (put object)
                                                                                                                  | test.py:
                                                                                                                  <module>:37

现在,驱动程序和运行任务的 worker 进程都持有对对象的 LOCAL_REFERENCE 引用,此外它在驱动程序上也是 USED_BY_PENDING_TASK。如果这是一个 actor 任务,actor 甚至可以在任务完成后通过将 ObjectRef 存储在成员变量中来持有 LOCAL_REFERENCE

5. 捕获的 ObjectRef 引用

a = ray.put(None)
b = ray.put([a])
del a

在此示例中,我们首先通过 ray.put() 创建一个对象,然后将其 ObjectRef 捕获在另一个 ray.put() 对象中,并删除第一个 ObjectRef。在这种情况下,两个对象仍然被固定。

--- Summary for node address: 192.168.0.15 ---
Mem Used by Objects  Local References  Pinned Count  Pending Tasks  Captured in Objects  Actor Handles
233 MiB              1                 0             0              1                    0

--- Object references for node address: 192.168.0.15 ---
IP Address    PID    Type    Object Ref                                                Size    Reference Type      Call Site
192.168.0.15  7473   Driver  ffffffffffffffffffffffffffffffffffffffff0100000001000000  15 MiB  CAPTURED_IN_OBJECT  (put object)  |
                                                                                                                  test.py:
                                                                                                                  <module>:41

192.168.0.15  7473   Driver  ffffffffffffffffffffffffffffffffffffffff0100000002000000  218 MiB  LOCAL_REFERENCE     (put object)  |
                                                                                                                  test.py:
                                                                                                                  <module>:42

ray memory 的输出中,我们看到第二个对象显示为普通的 LOCAL_REFERENCE,而第一个对象被列为 CAPTURED_IN_OBJECT

内存感知调度#

默认情况下,Ray 在调度时不会考虑任务或 actor 的潜在内存使用量。这仅仅是因为它无法提前估计所需的内存量。但是,如果您知道任务或 actor 需要多少内存,可以在其 ray.remote 装饰器的资源要求中指定它,以启用内存感知调度。

重要提示

指定内存要求**不会**对内存使用施加任何限制。要求仅用于调度过程中的准入控制(类似于 Ray 中 CPU 调度的运作方式)。任务本身有责任不使用超过其请求的内存。

要告知 Ray 调度器任务或 actor 需要一定量的可用内存才能运行,请设置 memory 参数。然后,Ray 调度器将在调度过程中预留指定的可用内存量,类似于它处理 CPU 和 GPU 资源的方式。

# reserve 500MiB of available memory to place this task
@ray.remote(memory=500 * 1024 * 1024)
def some_function(x):
    pass

# reserve 2.5GiB of available memory to place this actor
@ray.remote(memory=2500 * 1024 * 1024)
class SomeActor:
    def __init__(self, a, b):
        pass

在上面的示例中,内存配额是通过装饰器静态指定的,但您也可以使用 .options() 在运行时动态设置它们,如下所示:

# override the memory quota to 100MiB when submitting the task
some_function.options(memory=100 * 1024 * 1024).remote(x=1)

# override the memory quota to 1GiB when creating the actor
SomeActor.options(memory=1000 * 1024 * 1024).remote(a=1, b=2)

有问题或疑问?#

您可以通过以下渠道提交问题、反馈或报告问题:

  1. 讨论区:用于**关于 Ray 使用的提问**或**功能请求**。

  2. GitHub Issues:用于** bug 报告**。

  3. Ray Slack:用于**与 Ray 维护者联系**。

  4. StackOverflow:使用 [ray] 标签**提问关于 Ray 的问题**。