优化性能#

无加速#

你刚刚使用 Ray 运行了一个应用程序,但它并没有你预期的那么快。或者更糟糕的是,它可能比应用程序的串行版本还要慢!最常见的原因如下。

  • 核心数量: Ray 使用了多少个核心?当你启动 Ray 时,它会通过 psutil.cpu_count() 确定每台机器上的 CPU 数量。Ray 通常不会并行调度比 CPU 数量更多的任务。因此,如果 CPU 数量是 4,你最多应该期望获得 4 倍的加速。

  • 物理CPU与逻辑CPU: 你运行的机器上的**物理**核心数是否少于**逻辑**核心数?你可以使用 psutil.cpu_count() 检查逻辑核心数,使用 psutil.cpu_count(logical=False) 检查物理核心数。这在许多机器上很常见,尤其是在EC2上。对于许多工作负载(尤其是数值工作负载),你通常不能期望获得比物理CPU数量更多的加速。

  • 小任务: 你的任务非常小吗?Ray 为每个任务引入了一些开销(开销的大小取决于传入的参数)。如果你的任务耗时少于十毫秒,你不太可能看到速度提升。对于许多工作负载,你可以通过将任务批量处理来轻松增加任务的大小。

  • 变量持续时间: 你的任务有变量持续时间吗?如果你并行运行10个具有变量持续时间的任务,你不应该期望N倍的加速(因为你最终会等待最慢的任务)。在这种情况下,考虑使用 ray.wait 来开始处理最先完成的任务。

  • 多线程库: 你的所有任务是否都在尝试使用机器上的所有核心?如果是这样,它们很可能会遇到争用,从而阻止你的应用程序实现加速。这在某些版本的 numpy 中很常见。为了避免争用,请将环境变量设置为 MKL_NUM_THREADS``(或根据你的安装设置相应的变量)为 ``1

    对于许多——但不是所有——库,你可以通过在应用程序运行时打开 top 来诊断这个问题。如果一个进程占用了大部分CPU,而其他进程只使用了少量CPU,这可能是问题所在。最常见的例外是PyTorch,尽管需要调用 torch.set_num_threads(1) 来避免争用,但它会显示为使用所有核心。

如果你仍然遇到减速问题,但上述问题都不适用,我们真的很想知道!创建一个 GitHub issue 并提交一个展示问题的最小代码示例。

本文档讨论了人们在使用 Ray 时遇到的一些常见问题以及一些已知问题。如果你遇到其他问题,告诉我们

使用 Ray 时间线可视化任务#

查看 如何在仪表板中使用 Ray Timeline 以获取更多详细信息。

除了使用仪表板UI下载跟踪文件外,您还可以通过从命令行运行 ray timeline 或从Python API运行 ray.timeline 将跟踪文件导出为JSON文件。

import ray

ray.init()

ray.timeline(filename="timeline.json")

仪表板中的 Python CPU 分析#

通过点击活动工作进程、角色和作业的“堆栈跟踪”或“CPU火焰图”操作,Ray 仪表板 允许您分析 Ray 工作进程。

../../../_images/profile.png

点击“堆栈跟踪”将使用 py-spy 返回当前的堆栈跟踪样本。默认情况下,仅显示 Python 堆栈跟踪。要显示本地代码帧,请设置 URL 参数 ``native=1``(仅在 Linux 上支持)。

../../../_images/stack.png

点击“CPU Flame Graph”会获取多个堆栈跟踪样本并将它们合并成火焰图可视化。这个火焰图有助于理解特定进程的CPU活动。要调整火焰图的持续时间,可以在URL中更改``duration``参数。同样,可以更改``native``参数以启用原生分析。

../../../_images/flamegraph.png

分析功能需要安装 py-spy。如果未安装,或者 py-spy 二进制文件没有 root 权限,仪表板会提示如何正确设置 py-spy 的说明:

This command requires `py-spy` to be installed with root permissions. You
can install `py-spy` and give it root permissions as follows:
  $ pip install py-spy
  $ sudo chown root:root `which py-spy`
  $ sudo chmod u+s `which py-spy`

Alternatively, you can start Ray with passwordless sudo / root permissions.

备注

在使用 py-spy 时,您可能会遇到权限错误。要解决此问题:

  • 如果你在Docker容器中手动启动Ray,请按照 py-spy文档 来解决。

  • 如果你是 KubeRay 用户,请按照 KubeRay 配置指南 进行操作并解决问题。

使用 Python 的 cProfile 进行性能分析#

你可以使用 Python 的原生 cProfile 分析模块 来分析你的 Ray 应用程序的性能。cProfile 不是逐行跟踪你的应用程序代码,而是可以给出每个循环函数的总运行时间,以及列出在分析代码中进行的所有函数调用的次数和执行时间。

与上述 line_profiler 不同,此详细的分析函数调用列表 包括 内部函数调用和在 Ray 中进行的函数调用。

然而,类似于 line_profiler ,cProfile 可以在对应用程序代码进行最小改动的情况下启用(前提是您希望分析的每一段代码都定义为其自己的函数)。要使用 cProfile ,请添加一个导入语句,然后按如下方式替换对循环函数的调用:

import cProfile  # Added import statement

def ex1():
    list1 = []
    for i in range(5):
        list1.append(ray.get(func.remote()))

def main():
    ray.init()
    cProfile.run('ex1()')  # Modified call to ex1
    cProfile.run('ex2()')
    cProfile.run('ex3()')

if __name__ == "__main__":
    main()

现在,当你执行你的Python脚本时,每次调用 cProfile.run() 时,终端上都会打印出一份 cProfile 的分析函数调用列表。在 cProfile 输出的最顶部,给出了 'ex1()' 的总执行时间:

601 function calls (595 primitive calls) in 2.509 seconds

以下是针对 'ex1()' 的分析函数调用片段。这些调用中的大多数都非常快,大约需要 0.000 秒,因此感兴趣的函数是那些执行时间不为零的函数:

ncalls  tottime  percall  cumtime  percall filename:lineno(function)
...
    1    0.000    0.000    2.509    2.509 your_script_here.py:31(ex1)
    5    0.000    0.000    0.001    0.000 remote_function.py:103(remote)
    5    0.000    0.000    0.001    0.000 remote_function.py:107(_remote)
...
   10    0.000    0.000    0.000    0.000 worker.py:2459(__init__)
    5    0.000    0.000    2.508    0.502 worker.py:2535(get)
    5    0.000    0.000    0.000    0.000 worker.py:2695(get_global_worker)
   10    0.000    0.000    2.507    0.251 worker.py:374(retrieve_and_deserialize)
    5    0.000    0.000    2.508    0.502 worker.py:424(get_object)
    5    0.000    0.000    0.000    0.000 worker.py:514(submit_task)
...

Ray 的 get 的 5 次独立调用,每次调用耗时 0.502 秒,可以在 worker.py:2535(get) 处注意到。同时,在 remote_function.py:103(remote) 处调用远程函数本身仅在 5 次调用中耗时 0.001 秒,因此不是 ex1() 性能缓慢的原因。

使用 cProfile 分析 Ray 角色#

考虑到cProfile的详细输出可能会因我们使用的Ray功能而有所不同,让我们看看如果我们的示例涉及Actor,cProfile的输出可能会是什么样子(有关Ray Actor的介绍,请参阅我们的 Actor文档)。

现在,我们不再像在 ex1 中那样循环调用五次远程函数,而是创建一个新示例,并在一个 actor 内部 循环调用五次远程函数。我们的 actor 的远程函数再次只是休眠 0.5 秒:

# Our actor
@ray.remote
class Sleeper:
    def __init__(self):
        self.sleepValue = 0.5

    # Equivalent to func(), but defined within an actor
    def actor_func(self):
        time.sleep(self.sleepValue)

回顾 ex1 的次优性,让我们首先看看如果我们尝试在一个actor中执行所有五个 actor_func() 调用会发生什么:

def ex4():
    # This is suboptimal in Ray, and should only be used for the sake of this example
    actor_example = Sleeper.remote()

    five_results = []
    for i in range(5):
        five_results.append(actor_example.actor_func.remote())

    # Wait until the end to call ray.get()
    ray.get(five_results)

我们在这个示例中启用 cProfile 如下:

def main():
    ray.init()
    cProfile.run('ex4()')

if __name__ == "__main__":
    main()

运行我们的新 Actor 示例,cProfile 的缩写输出如下:

12519 function calls (11956 primitive calls) in 2.525 seconds

ncalls  tottime  percall  cumtime  percall filename:lineno(function)
...
1    0.000    0.000    0.015    0.015 actor.py:546(remote)
1    0.000    0.000    0.015    0.015 actor.py:560(_remote)
1    0.000    0.000    0.000    0.000 actor.py:697(__init__)
...
1    0.000    0.000    2.525    2.525 your_script_here.py:63(ex4)
...
9    0.000    0.000    0.000    0.000 worker.py:2459(__init__)
1    0.000    0.000    2.509    2.509 worker.py:2535(get)
9    0.000    0.000    0.000    0.000 worker.py:2695(get_global_worker)
4    0.000    0.000    2.508    0.627 worker.py:374(retrieve_and_deserialize)
1    0.000    0.000    2.509    2.509 worker.py:424(get_object)
8    0.000    0.000    0.001    0.000 worker.py:514(submit_task)
...

事实证明,整个示例仍然需要2.5秒来执行,或者说是五个``actor_func()``函数串行运行的时间。如果你记得``ex1``,这种行为是因为我们没有等到提交所有五个远程函数任务后再调用``ray.get()``,但我们可以在cProfile的输出行``worker.py:2535(get)``中验证,``ray.get()``仅在最后被调用了一次,耗时2.509秒。发生了什么?

事实证明,Ray 无法并行化这个示例,因为我们只初始化了一个 Sleeper 演员。由于每个演员都是一个单一的、有状态的工作者,我们的整个代码在整个过程中都被提交并在一个工作者上运行。

为了更好地并行化 ex4 中的角色,我们可以利用每次调用 actor_func() 都是独立的这一特点,转而创建五个 Sleeper 角色。这样,我们创建了五个可以并行运行的工作者,而不是创建一个一次只能处理一次 actor_func() 调用的工作者。

def ex4():
    # Modified to create five separate Sleepers
    five_actors = [Sleeper.remote() for i in range(5)]

    # Each call to actor_func now goes to a different Sleeper
    five_results = []
    for actor_example in five_actors:
        five_results.append(actor_example.actor_func.remote())

    ray.get(five_results)

我们的示例现在总共只需1.5秒即可运行:

1378 function calls (1363 primitive calls) in 1.567 seconds

ncalls  tottime  percall  cumtime  percall filename:lineno(function)
...
5    0.000    0.000    0.002    0.000 actor.py:546(remote)
5    0.000    0.000    0.002    0.000 actor.py:560(_remote)
5    0.000    0.000    0.000    0.000 actor.py:697(__init__)
...
1    0.000    0.000    1.566    1.566 your_script_here.py:71(ex4)
...
21    0.000    0.000    0.000    0.000 worker.py:2459(__init__)
1    0.000    0.000    1.564    1.564 worker.py:2535(get)
25    0.000    0.000    0.000    0.000 worker.py:2695(get_global_worker)
3    0.000    0.000    1.564    0.521 worker.py:374(retrieve_and_deserialize)
1    0.000    0.000    1.564    1.564 worker.py:424(get_object)
20    0.001    0.000    0.001    0.000 worker.py:514(submit_task)
...

使用 PyTorch Profiler 进行 GPU 分析#

以下是在使用 Ray Train 进行训练或使用 Ray Data 进行批量推理时使用 PyTorch Profiler 的步骤:

  • 按照 PyTorch Profiler 文档 记录您的 PyTorch 代码中的事件。

  • 将您的 PyTorch 脚本转换为 Ray Train 训练脚本Ray Data 批量推理脚本。(您的性能分析相关代码无需更改)

  • 运行您的训练或批量推理脚本。

  • 从所有节点收集分析结果(与非分布式设置中的1个节点相比)。

    • 你可能希望将每个节点上的结果上传到NFS或像S3这样的对象存储中,这样你就不必分别从每个节点获取结果。

  • 使用 Tensorboard 等工具可视化结果。

使用 Nsight System Profiler 进行 GPU 分析#

GPU 分析对于机器学习训练和推理至关重要。Ray 允许用户使用 Ray 参与者和任务运行 Nsight System Profiler。详情请参阅

开发者性能分析#

如果你正在开发 Ray Core 或调试一些系统级故障,分析 Ray Core 可能会有所帮助。在这种情况下,请参阅 Ray 开发者的分析