多处理:只使用物理内核?

新手上路,请多包涵

我有一个函数 foo 消耗大量内存,我想并行运行多个实例。

假设我有一个有 4 个物理内核的 CPU,每个物理内核有两个逻辑内核。

我的系统有足够的内存来并行容纳 foo 的 4 个实例,但不能容纳 8 个。此外,由于这 8 个内核中有 4 个是逻辑内核,我也不希望使用所有 8 个内核会提供比上面更多的收益并且不仅仅使用 4 个物理的。

所以我 只想 在 4 个物理内核上运行 foo 。换句话说,我想确保做 multiprocessing.Pool(4) (由于内存限制,4 是我可以在这台机器上容纳的函数的最大并发运行数)将作业分派到四个物理内核(而不是,例如,两个物理核心及其两个逻辑后代的组合)。

如何在 python 中做到这一点?

编辑:

我之前使用了来自 multiprocessing 的代码示例,但我是库不可知论者,所以为了避免混淆,我删除了它。

原文由 user189035 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 782
2 个回答

我找到了一个不涉及更改 python 模块源代码的解决方案。它使用 此处 建议的方法。通过执行以下操作,可以在运行该脚本后检查是否只有物理内核处于活动状态:

 lscpu

在 bash 返回中:

 CPU(s):                8
On-line CPU(s) list:   0,2,4,6
Off-line CPU(s) list:  1,3,5,7
Thread(s) per core:    1

[可以从 python 中运行上面链接的脚本]。在任何情况下,运行上面的脚本后,在 python 中输入这些命令:

 import multiprocessing
multiprocessing.cpu_count()

返回 4。

原文由 user189035 发布,翻译遵循 CC BY-SA 3.0 许可协议

我知道这个话题现在已经很老了,但是因为它仍然是在谷歌中输入“多处理逻辑核心”时的第一个答案……我觉得我必须给出一个额外的答案,因为我可以看到它有可能2018年(甚至更晚..)的人在这里很容易混淆(有些答案确实有点混乱)

我认为没有比这里更好的地方来警告读者上面的一些答案了,很抱歉让这个话题复活。

–> 计算 CPU(逻辑/物理)使用 PSUTIL 模块

对于 ex 的 4 物理核心/8 线程 i7,它将返回

import psutil
psutil.cpu_count(logical = False)

4个

psutil.cpu_count(logical = True)

8个

就如此容易。

在那里你不必担心操作系统、平台、硬件本身或其他任何东西。 我相信它比 multiprocessing.cpu_count() 好得多,后者有时会产生奇怪的结果,至少从我自己的经验来看是这样。

–> 要使用 N 个物理内核(由您选择)使用 YUGI 描述的多处理模块

只需计算您有多少个物理进程,启动一个 multiprocessing.Pool of 4 workers。

或者你也可以尝试使用 joblib.Parallel() 函数

2018 年的 joblib 不是 python 标准发行版的一部分,而只是 Yugi 描述的多处理模块的包装器。

–> 大多数时候,不要使用超过可用数量的核心(除非你已经对非常具体的代码进行了基准测试并证明它是值得的)

错误信息比比皆是,“如果您指定的内核多于可用内核,操作系统将处理事情”。 这绝对是 100% 错误 的。如果您使用的内核多于可用内核,您将面临巨大的性能下降。例外情况是工作进程受 IO 限制。因为 OS 调度程序会尽最大努力以相同的注意力处理每个任务,定期从一个任务切换到另一个任务,并且根据操作系统的不同,它可能会花费高达 100% 的工作时间来在进程之间切换,这将是灾难性的。

不要只相信我:尝试一下,对其进行基准测试,您会发现它有多清晰。

是否可以决定代码是在逻辑内核还是物理内核上执行?

如果你问这个问题,这意味着你不了解物理和逻辑核心的设计方式,所以也许你应该多检查一下处理器的架构。

例如,如果你想在核心 3 而不是核心 1 上运行,那么我想确实有一些解决方案,但只有你知道如何编写操作系统的内核和调度程序时才可用,我认为如果你是问这个问题。

如果您在 4 个物理处理器/8 个逻辑处理器上启动 4 个 CPU 密集型进程,调度程序会将您的每个进程归因于 1 个不同的物理核心(并且 4 个逻辑核心将保持未使用/未充分使用)。但是在 4 逻辑/8 线程 proc 上,如果处理单元是 (0,1) (1,2) (2,3) (4,5) (5,6) (6,7),那么它不会如果进程是在 0 或 1 上执行的区别:它是相同的处理单元。

至少据我所知(但专家可以确认,也许它也不同于非常具体的硬件规格)我认为在 0 或 1 上执行代码之间没有或只有很小的区别。在处理单元 (0,1) 中,我不确定 0 是逻辑的而 1 是物理的,反之亦然。根据我的理解(这可能是错误的),两者都是来自同一个处理单元的处理器,它们只是共享它们的高速缓存/对硬件(包括 RAM)的访问,并且 0 不比 1 多一个物理单元。

不仅如此,您还应该让操作系统决定。因为 OS 调度程序可以利用某些平台(例如 i7、i5、i3…)上存在的硬件逻辑核心涡轮增压,这是您无权控制的其他东西,而这可能对您真正有帮助。

如果您在 4 个物理内核/8 个逻辑内核上启动 5 个 CPU 密集型任务,行为将是混乱的,几乎不可预测,主要取决于您的硬件和操作系统。调度程序将尽力而为。几乎每一次,你都会遇到非常糟糕的表现。

让我们暂时假设我们仍在谈论 4(8) 经典架构:因为调度程序会尽力而为(因此经常切换属性),根据您正在执行的进程,启动可能更糟5 个逻辑核心而不是 8 个逻辑核心(至少他知道一切都会以 100% 的速度使用,所以迷路了他不会尝试避免它,不会太频繁地切换,因此不会丢失太多时间切换)。

但是,有 99% 的把握(但要在您的硬件上进行基准测试以确保)如果您使用的物理内核多于可用的物理内核,几乎所有多处理程序的运行速度都会变慢。

很多事情都可以干预…程序、硬件、操作系统的状态、它使用的调度程序、您今天早上吃的水果、您姐姐的名字…如果您对某些事情有疑问,只需对其进行基准测试,没有其他简单的方法可以查看您是否正在失去性能。有时信息学真的很奇怪。

–> 大多数时候,额外的逻辑核心在 PYTHON 中确实无用(但并非总是如此)

在 python 中有两种主要的方式来执行真正的并行任务。

  • 多处理(无法利用逻辑核心)
  • 多线程(可以利用逻辑核心)

例如并行运行 4 个任务

–> multiprocessing 将创建 4 个不同的 python 解释器。对于它们中的每一个,您都必须启动一个 python 解释器,定义读/写权限,定义环境,分配大量内存等。按原样说:您将从 0 开始一个全新的程序实例。这可能会花费大量时间,因此您必须确保这个新程序能够运行足够长的时间,这样它才值得。

如果你的程序有足够的工作(比方说,至少几秒钟的工作),那么因为操作系统将消耗 CPU 的进程分配到不同的物理内核上,它就可以工作,你可以获得很多性能,这很好。而且由于操作系统几乎总是允许进程在它们之间进行通信(尽管速度很慢),它们甚至可以交换(一点点)数据。

–> 多线程是不同的。在你的 python 解释器中,它只会创建少量内存,许多 CPU 可以共享这些内存,并同时处理它。它的生成速度要快得多(有时在旧计算机上生成一个新进程可能需要很多秒,而生成一个线程的时间却少得离谱)。您不会创建新进程,而是创建更轻量的“线程”。

线程可以非常快速地在线程之间共享内存,因为它们实际上是在同一内存上一起工作(而在使用不同进程时必须复制/交换它)。

但是:为什么我们不能在大多数情况下使用多线程?看起来很方便?

python 中有一个非常大的限制:在 python 解释器中一次只能执行一个 python 行,这称为 GIL(全局解释器锁)。所以大多数时候,你甚至会因为使用多线程而失去性能,因为不同的线程将不得不等待访问相同的资源。对于纯计算处理(没有 IO),多线程是无用的,如果您的代码是纯 python,则多线程甚至更糟。但是,如果您的线程涉及任何等待 IO,多线程可能会非常有益。

–> 为什么在使用多进程时我不应该使用逻辑内核?

逻辑核心没有自己的内存访问。它们只能处理内存访问及其托管物理处理器的缓存。例如,同一处理单元的逻辑和物理核心很可能(并且经常使用)同时在高速缓存的不同位置上使用相同的 C/C++ 函数。确实使治疗速度大大加快。

但是…这些是 C/C++ 函数! Python 是一个大的 C/C++ 包装器,它比等效的 C++ 代码需要更多的内存和 CPU。很可能在 2018 年,无论您想做什么,2 个大型 python 进程将需要比单个物理+逻辑单元所能承受的更多、更多的内存和缓存读/写,并且比等效的 C/ C++ 真正的多线程代码会消耗。这又一次几乎总是会导致性能下降。请记住,处理器缓存中不可用的每个变量都将花费 x1000 倍的时间来读取内存。如果你的缓存对于 1 个 python 进程来说已经完全满了,猜猜如果你强制 2 个进程使用它会发生什么:他们将一次使用它,并永久切换,导致数据被愚蠢地刷新并每次重新读取它切换的时间。当从内存中读取或写入数据时,您可能认为您的 CPU“正在”工作,但实际上并没有。它在等待数据!什么都不做。

–> 那么您如何利用逻辑核心呢?

就像我说的那样,由于全局解释器锁,默认 python 中没有真正的多线程(因此没有真正使用逻辑核心)。您可以在程序的某些部分强制删除 GIL,但我认为如果您不确切知道自己在做什么,最好不要碰它。

删除 GIL 肯定是很多研究的主题(请参阅都尝试这样做的实验性 PyPy 或 Cython 项目)。

目前,还没有真正的解决方案,因为它比看起来要复杂得多。

我承认,还有另一种可行的解决方案:

  • 用 C 编写您的函数
  • 用 ctype 将其包装在 python 中
  • 使用 python 多线程模块调用包装的 C 函数

这将 100% 有效,您将能够在 python 中使用多线程,并且真正使用所有逻辑核心。 GIL 不会打扰您,因为您不会执行真正的 python 函数,而是执行 C 函数。

例如,像 Numpy 这样的一些库可以在所有可用的线程上工作,因为它们是用 C 编写的。但是如果你到了这一点,我一直认为考虑直接用 C/C++ 编写程序可能是明智的,因为它是与最初的 pythonic 精神相去甚远。

**–> 不要总是使用所有可用的物理内核 **

我经常看到人们说“好吧,我有 8 个物理核心,所以我会为我的工作选择 8 个核心”。它通常有效,但有时会被证明是一个糟糕的主意,尤其是当您的工作需要大量 I/O 时。

尝试使用 N-1 核(再一次,特别是对于 I/O 要求很高的任务),你会发现 100% 的时间,在每个任务/平均上,单个任务总是在 N-1 核上运行得更快。确实,您的计算机会产生很多不同的东西:USB、鼠标、键盘、网络、硬盘驱动器等……即使在工作站上,周期性任务也会随时在您不知道的后台执行。如果您不让 1 个物理内核来管理这些任务,您的计算将经常中断(从内存中清除/重新放入内存),这也会导致性能问题。

您可能会想“好吧,后台任务只会使用 5% 的 CPU 时间,所以还剩下 95%”。但事实并非如此。

处理器一次处理一个任务。每次切换时,都会浪费大量时间将所有内容放回内存缓存/注册表中的位置。然后,如果出于某种奇怪的原因,操作系统调度程序过于频繁地进行这种切换(这是您无法控制的),那么所有这些计算时间都会永远丢失,您对此无能为力。

如果(有时会发生)由于某种未知原因,此调度程序问题影响的不是 1 个任务的性能,而是 30 个任务的性能,它可能会导致真正有趣的情况,即在 2930 物理内核上工作比在 3030 上工作要快得多

更多的 CPU 并不总是最好的

当您使用 multiprocessing.Pool 时,经常会使用 multiprocessing.Queue 或管理器队列,在进程之间共享,以允许它们之间进行一些基本通信。有时(我必须说 100 次,但我重复一遍),以硬件相关的方式,它可能会发生(但你应该针对你的特定应用程序、你的代码实现和你的硬件进行基准测试)使用更多的 CPU 可能会造成瓶颈当您使进程通信/同步时。在这些特定情况下,在较低的 CPU 数量上运行可能会很有趣,或者甚至尝试在更快的处理器上执行同步任务(当然,我在这里谈论的是在集群上运行的科学密集型计算)。由于多处理通常用于集群,因此您必须注意,出于节能目的,集群通常会降低频率。因此,单核性能可能 非常 糟糕(通过更多数量的 CPU 来平衡),当您将代码从本地计算机(少核,高单核性能)扩展到一个集群(很多核心,单核性能较低),因为根据 single_core_perf/nb_cpu 比率你的代码瓶颈,有时真的很烦人

每个人都有使用尽可能多的 CPU 的诱惑。但是这些案例的基准是强制性的。

典型情况(例如在数据科学中)是让 N 个进程并行运行,并且您想将结果汇总到一个文件中。因为您不能等待工作完成,所以您可以通过特定的编写程序来完成。作者将在输出文件中写入他的 multiprocessing.Queue(单核和硬盘驱动器受限进程)中推送的所有内容。 N 个进程填充了 multiprocessing.Queue。

很容易想象,如果你有 31 个 CPU 向一个非常慢的 CPU 写入信息,那么你的性能将会下降(如果你克服了系统处理临时数据的能力,可能会出现崩溃)

–> 带回家消息

  • 使用 psutil 来计算逻辑/物理处理器,而不是 multiprocessing.cpu_count() 或其他
  • 多处理只能在物理核心上工作(或者至少对其进行基准测试以证明它在您的情况下不是真的)
  • 多线程将在逻辑核心上工作,但你必须用 C 编写代码并将你的函数包装起来,或者删除全局锁解释器(每次你这样做,一只小猫都会在世界某个地方残忍地死去)
  • 如果你试图在纯 python 代码上运行多线程,你将有巨大的性能下降,所以你应该在 99% 的时间使用 multiprocessing
  • 除非你的进程/线程有很长的暂停,你可以利用,永远不要使用比可用更多的核心,如果你想尝试正确地进行基准测试
  • 如果您的任务是 I/O 密集型任务,您应该让 1 个物理核心来处理 I/O,如果您有足够的物理核心,那将是值得的。对于多处理实现,它需要使用 N-1 个物理内核。对于经典的2路多线程来说,就是使用N-2个逻辑核心。
  • 如果您需要更多性能,请尝试 PyPy(未准备好生产)或 Cython,甚至用 C 编写代码

最后但并非最不重要,也是最重要的一点:如果你真的在追求性能,你应该绝对、始终、始终进行基准测试,而不是猜测任何事情。基准测试经常揭示奇怪的平台/硬件/驱动程序非常具体的行为,您可能不知道。

原文由 Delevoye Guillaume 发布,翻译遵循 CC BY-SA 4.0 许可协议

推荐问题