优化性能#
无加速#
你刚刚使用 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 工作进程。
点击“堆栈跟踪”将使用 py-spy
返回当前的堆栈跟踪样本。默认情况下,仅显示 Python 堆栈跟踪。要显示本地代码帧,请设置 URL 参数 ``native=1``(仅在 Linux 上支持)。
点击“CPU Flame Graph”会获取多个堆栈跟踪样本并将它们合并成火焰图可视化。这个火焰图有助于理解特定进程的CPU活动。要调整火焰图的持续时间,可以在URL中更改``duration``参数。同样,可以更改``native``参数以启用原生分析。
分析功能需要安装 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 开发者的分析。