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

Posted

技术标签:

【中文标题】多处理:仅使用物理内核?【英文标题】:Multiprocessing: use only the physical cores? 【发布时间】:2017-03-06 04:46:00 【问题描述】:

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

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

我的系统有足够的内存来并行容纳 4 个 foo 实例,但不是 8 个。此外,由于这 8 个内核中的 4 个无论如何都是逻辑内核,我也不期望使用所有 8 个内核会在上述和除了只使用 4 个物理的。

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

如何在 python 中做到这一点?

编辑:

我之前使用了来自 multiprocessing 的代码示例,但我与库无关,因此为避免混淆,我将其删除。

【问题讨论】:

@GáborErdős 但这是汇集所有 物理 核心还是仅汇集前四个核心? @GáborErdős:你确定吗? import psutils psutil.cpu_count(logical=False) 似乎知道其中的区别。 @Yugi:不,我不认为这是重复的,尽管我的问题可能表述错误(因为过度强调了“全部”部分)。 我真的不知道,但我猜操作系统应该足够聪明,如果那是最优的。 @zvone: '你不能从 ought 得到 is'。在其他语言中(例如R),多处理有一个特定的选项来仅池化物理内核。因此,这不能被认为是由操作系统智能管理的。 【参考方案1】:

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

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

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

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

import psutil 
psutil.cpu_count(logical = False)

4

psutil.cpu_count(logical = True)

8

就这么简单。

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

--> 使用 N 物理核心(由您选择)使用 YUGI 描述的多处理模块

只需计算您有多少物理进程,启动一个由 4 个工作人员组成的 multiprocessing.Pool。

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

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

--> 大多数时候,不要使用比可用内核更多的内核(除非您对非常具体的代码进行了基准测试并证明这是值得的)

我们可以听到这里和那里(也有一些人在这里回答)“操作系统会妥善处理您使用的核心多于可用的核心”。 这绝对是 100% 错误。如果您使用的核心多于可用核心,您将面临巨大的性能下降。因为操作系统调度程序会尽最大努力以相同的注意力处理每一项任务,定期从一个任务切换到另一个任务,并且取决于操作系统,它可以将高达 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 更多的物理单元。

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

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

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

但是,如果您使用的物理核心多于可用的物理核心,几乎所有多处理程序都会运行得更慢,这是 99% 的肯定(但要确保在您的硬件上对其进行基准测试)。

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

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

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

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

例如并行运行 4 个任务

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

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

--> 多线程是不同的。在你的 python 解释器中,它只会创建少量的内存供许多 CPU 共享,并同时处理它。它的生成要快得多(有时在旧计算机上生成一个新进程可能需要几秒钟,而生成一个线程是在极短的时间内完成的)。您不会创建新进程,而是创建更轻量级的“线程”。

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

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

python 有一个非常大的限制:在 python 解释器中一次只能执行一条 python 行,称为 GIL(全局解释器锁)。所以大多数时候,你甚至会因为使用多线程而失去性能,因为不同的线程将不得不等待访问相同的资源。如果你的代码是纯 python,多线程总是没用的,甚至更糟。

-->为什么我在使用多处理时不应该使用逻辑核心?

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

但是...这些是 C/C++ 函数! Python 是一个大的 C/C++ 包装器,它比其等效的 C++ 代码需要更多的内存和 CPU。很可能在 2018 年,无论您想做什么,两个大型 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 个任务的性能,它可能会导致非常有趣的情况,即在 29/30 物理内核上工作可能比在 30/ 30

更多的 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% 的时间使用多处理来代替 除非您的进程/线程存在可利用的长时间暂停,否则切勿使用比可用内核更多的内核,如果您想尝试,请正确进行基准测试 如果你的任务是 I/O 密集型的,你应该让 1 个物理核心来处理 I/O,如果你有足够的物理核心,那是值得的。对于多处理实现,它需要使用 N-1 个物理内核。对于经典的 2 路多线程,这意味着使用 N-2 个逻辑内核。 如果您需要更多性能,请尝试 PyPy(未准备好生产)或 Cython,甚至用 C 编写代码

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

【讨论】:

“如果你的代码是纯 python,多线程总是没用的,甚至更糟” - NO.NO.NO。如果您的代码有很多 IO,例如网络爬虫,单个线程将在等待操作系统返回(套接字/文件)数据时释放 GIL……在这种情况下,我已经看到基于线程的并行性几乎线性的性能改进(我的项目是一个纯 python torrent 客户端)【参考方案2】:

注意:此方法不适用于 windows,仅在 linux 上进行测试。

使用multiprocessing.Process

使用Process() 时,为每个进程分配一个物理内核非常容易。您可以创建一个循环遍历每个核心并使用taskset -p [mask] [pid] 将新进程分配给新核心:

import multiprocessing
import os

def foo():
    return

if __name__ == "__main__" :
    for process_idx in range(multiprocessing.cpu_count()):
        p = multiprocessing.Process(target=foo)
        os.system("taskset -p -c %d %d" % (process_idx % multiprocessing.cpu_count(), os.getpid()))
        p.start()

我的工作站上有 32 个内核,所以我将部分结果放在这里:

pid 520811's current affinity list: 0-31
pid 520811's new affinity list: 0
pid 520811's current affinity list: 0
pid 520811's new affinity list: 1
pid 520811's current affinity list: 1
pid 520811's new affinity list: 2
pid 520811's current affinity list: 2
pid 520811's new affinity list: 3
pid 520811's current affinity list: 3
pid 520811's new affinity list: 4
pid 520811's current affinity list: 4
pid 520811's new affinity list: 5
...

如您所见,这里的每个进程的前一个和新的相似性。第一个用于所有核心 (0-31),然后分配给核心 0,第二个进程默认分配给 core0,然后其亲和性更改为下一个核心 (1),依此类推。

使用multiprocessing.Pool

警告:这种方法需要调整 pool.py 模块,因为我不知道您可以从 Pool() 中提取 pid。此更改还在 python 2.7multiprocessing.__version__ = '0.70a1' 上进行了测试。

Pool.py 中,找到调用_task_handler_start() 方法的行。在下一行中,您可以使用以下命令将池中的进程分配给每个“物理”核心(我将import os 放在这里,以便读者不会忘记导入它):

import os
for worker in range(len(self._pool)):
    p = self._pool[worker]
    os.system("taskset -p -c %d %d" % (worker % cpu_count(), p.pid))

你就完成了。测试:

import multiprocessing

def foo(i):
    return

if __name__ == "__main__" :
    pool = multiprocessing.Pool(multiprocessing.cpu_count())
    pool.map(foo,'iterable here')

结果:

pid 524730's current affinity list: 0-31
pid 524730's new affinity list: 0
pid 524731's current affinity list: 0-31
pid 524731's new affinity list: 1
pid 524732's current affinity list: 0-31
pid 524732's new affinity list: 2
pid 524733's current affinity list: 0-31
pid 524733's new affinity list: 3
pid 524734's current affinity list: 0-31
pid 524734's new affinity list: 4
pid 524735's current affinity list: 0-31
pid 524735's new affinity list: 5
...

请注意,对pool.py 的修改会将作业循环分配给核心。因此,如果您分配的作业多于 cpu 核心,您最终将在同一个核心上拥有多个作业。

编辑:

OP 正在寻找的是能够在特定核心上盯着池的pool()。为此,需要对multiprocessing 进行更多调整(首先撤消上述更改)。

警告:

不要尝试复制粘贴函数定义和函数调用。只复制粘贴应该在self._worker_handler.start() 之后添加的部分(您将在下面看到它)。请注意,我的multiprocessing.__version__ 告诉我版本是'0.70a1',但没关系,只要添加您需要添加的内容即可:

multiprocessingpool.py

cores_idx = None 参数添加到__init__() 定义。在我的版本中,添加后它看起来像这样:

def __init__(self, processes=None, initializer=None, initargs=(),
             maxtasksperchild=None,cores_idx=None)

你也应该在self._worker_handler.start()之后添加以下代码:

if not cores_idx is None:
    import os
    for worker in range(len(self._pool)):
        p = self._pool[worker]
        os.system("taskset -p -c %d %d" % (cores_idx[worker % (len(cores_idx))], p.pid))

multiprocessing__init__.py

cores_idx=None 参数添加到Pool() 的定义中以及返回部分中的其他Pool() 函数调用。在我的版本中,它看起来像:

def Pool(processes=None, initializer=None, initargs=(), maxtasksperchild=None,cores_idx=None):
    '''
    Returns a process pool object
    '''
    from multiprocessing.pool import Pool
    return Pool(processes, initializer, initargs, maxtasksperchild,cores_idx)

你就完成了。以下示例仅在核心 0 和 2 上运行由 5 个工作人员组成的池:

import multiprocessing


def foo(i):
    return

if __name__ == "__main__":
    pool = multiprocessing.Pool(processes=5,cores_idx=[0,2])
    pool.map(foo,'iterable here')

结果:

pid 705235's current affinity list: 0-31
pid 705235's new affinity list: 0
pid 705236's current affinity list: 0-31
pid 705236's new affinity list: 2
pid 705237's current affinity list: 0-31
pid 705237's new affinity list: 0
pid 705238's current affinity list: 0-31
pid 705238's new affinity list: 2
pid 705239's current affinity list: 0-31
pid 705239's new affinity list: 0

当然,您仍然可以通过删除cores_idx 参数来获得multiprocessing.Poll() 的常用功能。

【讨论】:

@user189035 你在实现它时遇到过任何具体问题吗?因为使用我的答案的一部分multiprocessing.Process 来实现这似乎是完全可以的。除非我错过了什么 @user189035 也许我错过了一些东西,因为现在我认为它可能需要两者的结合。但是,如果您遇到麻烦,请告诉我,我会努力解决的 我不明白你的最后评论。我也很难通过你的回答。您能否附上您的答案,以说明如何查看 foo 的实例是否确实在物理核心而不是逻辑核心上运行? @user189035 检查答案。我添加了你想要的。如果这是您想要的,请接受答案,以便其他可能正在寻找相同内容的人在第一眼看到它就对您有用。 @user189035 cores_idx 参数是一个列表,您可以在其中分配 CPU 内核。不要分配比您的 cpu 内核更高的索引,否则会引发异常(我应该输入 asserts)。例如,cores_idx=[0] 仅使用核心 0,cores_idx=[0,1,2,3] 使用前 4 个核心。如果您不放 cores_idx ,则可能会照常使用它们中的任何/全部。【参考方案3】:

我找到了一个不涉及更改 python 模块源代码的解决方案。它使用建议的方法here。只能检查 运行该脚本后,物理内核处于活动状态:

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。

【讨论】:

简洁的解决方案,+1

以上是关于多处理:仅使用物理内核?的主要内容,如果未能解决你的问题,请参考以下文章

物理CPU,物理CPU内核,逻辑CPU概念详解

以编程方式检测物理内核的数量

linux内核源码分析之物理内存组织结构

Linux 内核CPU 分类与状态 ( CPU 处理器分类 | 根据物理属性分类 SMTMCSoC | Linux 内核中 CPU 分类 | Linux 内核源码中的 CPU 状态源码 )

Linux 内核 内存管理物理内存组织结构 ① ( 多处理器体系结构 | SMP/UMA 对称多处理器结构 | NUMA 非一致内存访问结构 )

8350cpu四个内核八个逻辑处理器啥意思啊啊