调试内存问题#

调试内存不足#

在阅读本节之前,请先熟悉 Ray 内存管理 模型。

什么是内存不足错误?#

内存是一种有限的资源。当一个进程请求内存而操作系统无法分配时,操作系统会执行一个例程,通过终止一个内存使用量高的进程(通过SIGKILL)来释放内存,以避免操作系统变得不稳定。这个例程被称为 Linux 内存不足杀手

Linux 内存不足杀手的一个常见问题是,SIGKILL 会杀死进程而 Ray 没有注意到。由于 SIGKILL 无法被进程处理,Ray 难以发出适当的错误消息并采取适当的容错措施。为了解决这个问题,Ray 从 Ray 2.2 开始有一个应用程序级别的 内存监控器,它会持续监控主机的内存使用情况,并在 Linux 内存不足杀手执行之前杀死 Ray 工作进程。

检测内存不足错误#

如果Linux的内存不足杀手终止了任务或角色,Ray工作进程将无法捕获并显示确切的根本原因,因为SIGKILL无法被进程处理。如果你调用``ray.get``到从已死亡的工作者执行的任务和角色,它会引发一个异常,并显示以下错误信息之一(这表明工作者被意外终止)。

Worker exit type: UNEXPECTED_SY STEM_EXIT Worker exit detail: Worker unexpectedly exits with a connection error code 2. End of file. There are some potential root causes. (1) The process is killed by SIGKILL by OOM killer due to high memory usage. (2) ray stop --force is called. (3) The worker is crashed unexpectedly due to SIGSEGV or other unexpected errors.
Worker exit type: SYSTEM_ERROR Worker exit detail: The leased worker has unrecoverable failure. Worker is requested to be destroyed when it is returned.

你也可以使用 dmesg CLI 命令来验证进程是否被 Linux 的内存不足杀手终止。

../../../_images/dmsg.png

如果Ray的内存监视器终止了工作进程,它会自动重试(详见 链接 )。如果任务或角色无法重试,当你调用 ray.get 时,它们会抛出一个异常,并附带一个更清晰的错误信息。

ray.exceptions.OutOfMemoryError: Task was killed due to the node running low on memory.

Task was killed due to the node running low on memory.
Memory on the node (IP: 10.0.62.231, ID: e5d953ef03e55e26f13973ea1b5a0fd0ecc729cd820bc89e4aa50451) where the task (task ID: 43534ce9375fa8e4cd0d0ec285d9974a6a95897401000000, name=allocate_memory, pid=11362, memory used=1.25GB) was running was 27.71GB / 28.80GB (0.962273), which exceeds the memory usage threshold of 0.95. Ray killed this worker (ID: 6f2ec5c8b0d5f5a66572859faf192d36743536c2e9702ea58084b037) because it was the most recently scheduled task; to see more information about memory usage on this node, use `ray logs raylet.out -ip 10.0.62.231`. To see the logs of the worker, use `ray logs worker-6f2ec5c8b0d5f5a66572859faf192d36743536c2e9702ea58084b037*out -ip 10.0.62.231.`
Top 10 memory users:
PID   MEM(GB) COMMAND
410728        8.47    510953  7.19    ray::allocate_memory
610952        6.15    ray::allocate_memory
711164        3.63    ray::allocate_memory
811156        3.63    ray::allocate_memory
911362        1.25    ray::allocate_memory
107230        0.09    python test.py --num-tasks 2011327      0.08    /home/ray/anaconda3/bin/python /home/ray/anaconda3/lib/python3.9/site-packages/ray/dashboard/dashboa...

Refer to the documentation on how to address the out of memory issue: https://docs.ray.io/en/latest/ray-core/scheduling/ray-oom-prevention.html.

Ray 内存监视器还会定期将聚合的内存不足杀手摘要打印到 Ray 驱动程序。

(raylet) [2023-04-09 07:23:59,445 E 395 395] (raylet) node_manager.cc:3049: 10 Workers (tasks / actors) killed due to memory pressure (OOM), 0 Workers crashed due to other reasons at node (ID: e5d953ef03e55e26f13973ea1b5a0fd0ecc729cd820bc89e4aa50451, IP: 10.0.62.231) over the last time period. To see more information about the Workers killed on this node, use `ray logs raylet.out -ip 10.0.62.231`
(raylet)
(raylet) Refer to the documentation on how to address the out of memory issue: https://docs.ray.io/en/latest/ray-core/scheduling/ray-oom-prevention.html. Consider provisioning more memory on this node or reducing task parallelism by requesting more CPUs per task. To adjust the kill threshold, set the environment variable `RAY_memory_usage_threshold` when starting Ray. To disable worker killing, set the environment variable `RAY_memory_monitor_refresh_ms` to zero.

Ray Dashboard 的 指标页面事件页面 也提供了与内存不足杀手相关的特定事件和指标。

../../../_images/oom-metrics.png ../../../_images/oom-events.png

查找每项任务和角色的内存使用情况#

如果任务或执行者因内存不足错误而失败,它们会根据 重试策略 进行重试。然而,通常更倾向于找到内存问题的根本原因并修复它们,而不是依赖容错机制。本节解释了如何在 Ray 中调试内存不足错误。

首先,找到内存使用量高的任务和执行者。查看 每个任务和执行者的内存使用图表 以获取更多详情。每个组件图表中的内存使用量使用的是 RSS - SHR。原因如下。

或者,你也可以使用 CLI 命令 htop

../../../_images/htop.png

查看 allocate_memory 行。查看两列,RSS 和 SHR。

SHR 使用通常是指 Ray 对象存储的内存使用。Ray 对象存储将主机内存的 30% 分配给共享内存(/dev/shm,除非你指定 --object-store-memory)。如果 Ray 工作进程通过 ray.get 访问对象存储中的对象,SHR 使用量会增加。由于 Ray 对象存储支持 零拷贝 反序列化,多个工作进程可以访问同一个对象而无需将其复制到进程内存中。例如,如果有 8 个工作进程访问 Ray 对象存储中的同一个对象,每个进程的 SHR 使用量会增加。然而,它们并没有使用 8 * SHR 内存(共享内存中只有 1 份拷贝)。还要注意,当对象使用量超过限制时,Ray 对象存储会触发 对象溢出,这意味着共享内存的内存使用不会超过主机内存的 30%。

主机上的内存不足问题,是由于每个工作进程的RSS使用量造成的。通过RSS - SHR计算每个进程的内存使用量,因为如上所述,SHR用于Ray对象存储。总内存使用量通常为``SHR(对象存储内存使用量,内存的30%)+ 每个ray进程的RSS - SHR之和 + 系统组件(例如,raylet,GCS。通常很小)的RSS - SHR之和``。

头节点内存不足错误#

首先,从指标页面检查头节点的内存使用情况。从集群页面找到头节点的地址。

../../../_images/head-node-addr.png

然后从仪表板内的节点内存使用情况视图中检查头节点的内存使用情况 指标视图

../../../_images/metrics-node-view.png

Ray 头节点有更多内存需求高的系统组件,如 GCS 或仪表盘。此外,驱动程序默认从头节点运行。如果头节点的内存容量与工作节点相同,并且如果从头节点执行相同数量的任务和参与者,它很容易出现内存不足的问题。在这种情况下,通过在启动头节点时指定 --num-cpus=0 来避免在头节点上运行任何任务和参与者,例如 ray start --head。如果你使用 KubeRay,请查看 这里 <kuberay-num-cpus>

减少并行性#

高并行性可能会触发内存不足错误。例如,如果你有8个执行数据预处理 -> 训练的训练工作器。如果你将过多数据加载到每个工作器中,总内存使用量(训练工作器内存使用量 * 8)可能会超过内存容量。

通过查看 每个任务和角色的内存使用图表 和任务指标来验证内存使用情况。

首先,查看 allocate_memory 任务的内存使用情况。总内存为18GB。同时,验证正在运行的15个并发任务。

../../../_images/component-memory.png ../../../_images/tasks-graph.png

每个任务使用大约 18GB / 15 == 1.2 GB。为了减少并行性:

  • 限制最大运行任务数.

  • 增加 ray.remote()num_cpus 选项。现代硬件通常每个CPU有4GB内存,因此你可以相应地选择CPU需求。这个例子指定每个 allocate_memory 任务使用1个CPU。将CPU需求加倍,同时只能运行一半(7个)任务,并且内存使用量不会超过9GB。

分析任务和参与者内存使用情况#

任务和参与者使用的内存也可能比你预期的要多。例如,参与者或任务可能存在内存泄漏或不必要的副本。

查看以下说明,了解如何对单个执行者和任务进行内存分析。

内存分析 Ray 任务和角色#

要分析 Ray 任务或角色的内存使用情况,请使用 memray。请注意,如果其他内存分析工具支持类似的 API,也可以使用它们。

首先,安装 memray

pip install memray

memray 支持一个 Python 上下文管理器来启用内存分析。你可以将 memray 分析文件写到任何你想写的地方。但在本例中,我们将它们写入 /tmp/ray/session_latest/logs,因为 Ray 仪表板允许你下载日志文件夹内的文件。这将允许你从其他节点下载分析文件。

import memray
import ray


@ray.remote
class Actor:
    def __init__(self):
        # Every memory allocation after `__enter__` method will be tracked.
        memray.Tracker(
            "/tmp/ray/session_latest/logs/"
            f"{ray.get_runtime_context().get_actor_id()}_mem_profile.bin"
        ).__enter__()
        self.arr = [bytearray(b"1" * 1000000)]

    def append(self):
        self.arr.append(bytearray(b"1" * 1000000))


a = Actor.remote()
ray.get(a.append.remote())

请注意,任务的生命周期较短,因此可能会产生大量的内存分析文件。

import memray  # noqa
import ray  # noqa


@ray.remote
def task():
    with memray.Tracker(
        "/tmp/ray/session_latest/logs/"
        f"{ray.get_runtime_context().get_task_id()}_mem_profile.bin"
    ):
        arr = bytearray(b"1" * 1000000)  # noqa


ray.get(task.remote())

任务或角色运行后,转到仪表板的 日志视图 。找到并点击日志文件名。

../../../_images/memory-profiling-files.png

点击下载按钮。

../../../_images/download-memory-profiling-files.png

现在,你有了内存分析文件。运行

memray flamegraph <memory profiling bin file>

你可以看到内存分析的结果!