多处理:只使用物理核?

2024-05-17 10:17:25 发布

您现在位置:Python中文网/ 问答频道 /正文

我有一个函数foo,它消耗大量内存,我希望并行运行的几个实例。

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

我的系统有足够的内存来并行容纳4个foo实例,但不能容纳8个。此外,由于这8个核中有4个是逻辑核,我也不认为使用所有8个核将提供比仅使用4个物理核更多的好处。

所以我只想在4个物理核上运行foo。换言之,我希望确保执行multiprocessing.Pool(4)(4是由于内存限制,我可以在此计算机上容纳的函数的最大并发运行数)将作业分派给四个物理核(而不是,例如,分派给两个物理核及其两个逻辑子代的组合)。

在python中如何做到这一点?

编辑:

我之前使用了multiprocessing中的一个代码示例,但我与库无关,所以为了避免混淆,我删除了它。


Tags: 实例函数内存分派foo系统计算机作业
3条回答

注意:此方法在windows上不起作用,仅在linux上测试。

使用multiprocessing.Process

在使用Process()时,为每个进程分配物理核心非常容易。您可以创建一个for循环,循环遍历每个核心,并使用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,第二个进程默认分配给核心0,然后将其关联性更改为下一个核心(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()的常规功能。

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

我觉得没有比这里更好的地方可以提醒读者注意上面的一些答案,所以很抱歉让这个话题重新活跃起来。

-->;要计数CPU(逻辑/物理),请使用PSUTIL模块

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

import psutil 
psutil.cpu_count(logical = False)

4

psutil.cpu_count(logical = True)

8

就这么简单。

在这里,你不用担心操作系统、平台、硬件本身或其他什么。我确信这比多处理要好得多。至少从我自己的经验来看,多处理有时会产生奇怪的结果。

-->;要使用N个物理核(由您选择),请使用YUGI描述的多处理模块

只需计算你有多少个物理进程,启动一个由4个工人组成的多进程池。

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

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

-->;大多数情况下,不要使用超过可用数量的核心(除非您已经对非常特定的代码进行了基准测试并证明它是值得的)

我们可以听到这里和那里(也从一些人回答这里)的“操作系统会妥善照顾,是你使用更多的核心比可用”。这绝对是100%错误的。如果您使用的核心比可用的多,您将面临巨大的性能下降。因为OS调度器将尽其所能以相同的注意力处理每一个任务,定期地从一个任务切换到另一个任务,并且根据OS的不同,它可以将其工作时间的100%用于进程之间的切换,这将是灾难性的。

不要只相信我:试试看,基准测试,你会发现它是多么清晰。

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

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

例如,如果你想在Core3而不是Core1上运行,我想确实有一些解决方案,但是只有当你知道如何编写操作系统的内核和调度程序时才有可能,如果你问这个问题的话,我想情况并非如此。

如果在4个物理/8个逻辑处理器上启动4个CPU密集型进程,调度器将把每个进程都分配给1个不同的物理核心(4个逻辑核心将保持未使用/使用不当)。但是在一个4逻辑/8线程的过程中,如果处理单元是(0,1)(1,2)(2,3)(4,5)(5,6)(6,7),那么在0或1上执行该过程没有区别:它是同一个处理单元。

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

更重要的是你应该让操作系统来决定。因为OS调度器可以利用一些平台(ex i7、i5、i3…)上的硬件逻辑核心turbo boost,而其他一些平台则没有电源,这可能是对你真的很有帮助。

如果在一个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,无论你想做什么,2个大Python进程将需要比单个物理+逻辑单元所能负担的更多、更多的内存和缓存读写,而且更多的是等价的C/C++真正的多线程代码消耗。这一次,几乎总是会导致表演下降。记住,处理器缓存中不可用的每个变量都需要x1000时间才能读入内存。如果一个python进程的缓存已经完全满了,猜猜如果强制两个进程使用它会发生什么:它们一次使用一个,然后永久地切换,导致数据在每次切换时都被愚蠢地刷新并重新读取。当数据从内存读取或写入时,您可能会认为您的CPU“正在”工作,但它不是。它在等数据!什么也不做。

-->;那么,如何利用逻辑核心呢?

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

移除GIL肯定是很多研究的主题(参见实验性PyPy或Cython项目,它们都试图这样做)。

目前,还没有真正的解决方案,因为这是一个比看上去复杂得多的问题。

我承认,还有另一个解决办法: -用C编写函数代码 -用ctype用python包起来 -使用python多线程模块调用包装的C函数

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

例如,像NoMyP这样的库可以处理所有可用的线程,因为它们是在C.编码的,但是如果你来到这里,我总是认为,直接考虑C/C++中的程序是明智的,因为它是一个非常远离原始Pythic精神的考虑。

**-->;不要总是使用所有可用的物理核心**

我经常看到人们这样说:“好的,我有8个身体核心,所以我将采取8个核心为我的工作”。它经常起作用,但有时却是个糟糕的主意,尤其是当你的工作需要大量的I/O时

尝试使用N-1内核(再次尝试,特别是对于高I/O要求的任务),您将看到100%的时间(平均每个任务)单个任务在N-1内核上总是运行得更快。事实上,你的电脑制造了很多不同的东西:USB、鼠标、键盘、网络、硬盘等等。。。即使在工作站上,周期性的任务也会在你不知道的背景下随时执行。如果您不让一个物理核心来管理这些任务,您的计算将定期中断(从内存中刷新/替换回内存中),这也会导致性能问题。

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

处理器一次处理一个任务。每次它切换时,都会浪费大量的时间将所有东西放回内存缓存/注册表中。然后,如果由于某种奇怪的原因,操作系统调度程序频繁地进行这种切换(一些你无法控制的事情),那么所有的计算时间都将永远丢失,而你什么也得不到可以做的。

如果(而且有时会发生)由于某种未知的原因,这个调度程序问题影响的不是1个而是30个任务的性能,那么它可能会导致非常有趣的情况,在29/30的物理核心上工作比在30/30上工作要快得多

更多的CPU并不总是最好的

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

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

典型的情况(在数据科学中,ex)是让N个进程并行运行,并且希望将结果汇总到一个文件中。因为你不能等待工作完成,所以你要通过一个特定的编写过程来完成。编写器将在outputfile中写入在其multiprocessing.Queue(单核和硬盘驱动器受限进程)中推送的所有内容。N个进程填满了multiprocessing.Queue。

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

-->;带回家信息

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

最后但并非最不重要,也是最重要的一点:如果你真的在追求业绩,你绝对应该,永远,永远做基准测试,不要猜测任何事情。基准测试通常会揭示出奇怪的平台/硬件/驱动程序非常具体的行为,而您对此一无所知。

我找到了一个不涉及更改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。

相关问题 更多 >