内存管理#
本页介绍了 Ray 中的内存管理工作原理。
另请参阅 调试内存不足 问题,了解如何排查内存不足问题。
概念#
Ray 应用使用内存的方式有几种
- 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
(磁盘),这可能会影响性能。在 Ray 1.3+ 中,如果对象存储满,对象会被溢出到磁盘。对象存储共享内存:您的应用通过
ray.get
读取对象时使用的内存。请注意,如果对象已存在于节点上,这不会导致额外的分配。这使得大对象可以在许多 Actor 和任务之间高效共享。
ObjectRef 引用计数#
Ray 实现了分布式引用计数,以便集群中作用域内的任何 ObjectRef
都固定在对象存储中。这包括本地 Python 引用、待处理任务的参数以及序列化在其他对象内部的 ID。
使用“ray memory”命令进行调试#
ray memory
命令可用于帮助查找作用域内的 ObjectRef
引用,这些引用可能导致 ObjectStoreFullError
。
在 Ray 应用运行时从命令行运行 ray memory
将输出集群中 Driver、Actor 和 Task 当前持有的所有 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
,同时还显示引用的位置(在 Driver 中、在 Worker 中等)、引用的类型(有关引用类型的详细信息,请参阅下文)、对象的大小(以字节为单位)、对象实例化所在的进程 ID 和 IP 地址,以及应用中创建引用的位置。
ray memory
提供了功能,使内存调试体验更有效。例如,您可以添加参数 sort-by=OBJECT_SIZE
和 group-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
的输出中,我们可以看到这些引用在 Driver 进程中都被标记为 LOCAL_REFERENCE
,但在“Reference Creation Site”的注释中指示第一个是通过“put object”创建的,第二个来自“task call”。
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
显示该对象在 Driver 进程中同时有 LOCAL_REFERENCE
和 USED_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
现在,Driver 和运行任务的 Worker 进程都持有该对象的 LOCAL_REFERENCE
,此外在 Driver 上它还是 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)
疑问或问题?#
您可以通过以下渠道发布疑问、问题或反馈
讨论论坛:用于咨询 Ray 用法问题或提交功能请求。
GitHub Issues:用于提交 Bug 报告。
Ray Slack:用于与 Ray 维护者联系。
StackOverflow:使用 [ray] 标签提问关于 Ray 的问题。