穷人的Python数据并行性不能随着处理器的数量而扩展?为什么呢?

时间:2016-07-22 10:10:04

标签: python-2.7 parallel-processing

我需要进行许多计算迭代,比如100左右,我们需要一个非常复杂的函数来接受许多输入参数。虽然参数会有所不同,但每次迭代都需要几乎相同的时间来计算。我找到了穆罕默德·阿尔卡鲁里的this post。我修改了他的代码,以便将脚本分叉N次,我计划将N设置为至少等于我桌面计算机上的内核数。

这是一台旧的MacPro 1,0运行32位Ubuntu 16.04,内存为18GB,并且在以下测试文件的运行中,RAM使用率都不超过15%(从不使用交换)。这是根据系统监视器的资源选项卡,在该选项卡上还显示,当我尝试并行运行4次迭代时,所有四个CPU都以100%运行,而如果我只进行一次迭代,则只有一个CPU被100%利用而另一个3是空转。 (以下是计算机的actual specscat /proc/self/status显示Cpus_allowed为8,是cpu核心总数的两倍,这可能表示超线程。)

所以我预计会发现4个同步运行只会消耗比一个运行时间更多的时间(并且我一般预计运行时间与任何计算机上的核心数成反比例)。但是,我发现相反,4的运行时间不是一个的运行时间,而是一个运行时间的两倍多。例如,对于下面给出的方案,单次迭代的样本“time(1)real”值是0m7.695s,而对于4次“同时”迭代,它是0m17.733s。当我从一次运行4次迭代到8次时,运行时间按比例缩放。

所以我的问题是,为什么它不像我想象的那样扩展(并且可以采取任何措施来解决这个问题)?顺便说一句,这是在几个桌面上部署的;它不必在Windows上扩展或运行。

此外,我现在忽略了multiprocessing.Pool()替代方案,因为我的功能被拒绝,因为它不是可选择的。

以下是经过修改的Alkarouri脚本multifork.py

#!/usr/bin/env python
import os, cPickle, time

import numpy as np
np.seterr(all='raise')

def function(x):
    print '... the parameter is ', x
    arr = np.zeros(5000).reshape(-1,1)
    for r in range(3):
        for i in range(200):
            arr = np.concatenate( (arr, np.log( np.arange(2, 5002) ).reshape(-1,1) ), axis=1 )
    return { 'junk': arr, 'shape': arr.shape }

def run_in_separate_process(func, *args, **kwds):
    numruns = 4
    result = [ None ] * numruns
    pread = [ None ] * numruns
    pwrite = [ None ] * numruns
    pid = [ None ] * numruns
    for i in range(numruns):
        pread[i], pwrite[i] = os.pipe()
        pid[i] = os.fork()
        if pid[i] > 0:
            pass
        else: 
            os.close(pread[i])
            result[i] = func(*args, **kwds)
            with os.fdopen(pwrite[i], 'wb') as f:
                cPickle.dump((0,result[i]), f, cPickle.HIGHEST_PROTOCOL)
            os._exit(0)

    #time.sleep(17)
    while True:
        for i in range(numruns):
            os.close(pwrite[i])
            with os.fdopen(pread[i], 'rb') as f:
                stat, res = cPickle.load(f)
                result[i] = res
            #os.waitpid(pid[i], 0)
        if not None in result: break
    return result

def main():
    print 'Running multifork.py.'

    print run_in_separate_process( function, 3 )

if __name__ == "__main__":
    main()

使用multifork.py,取消注释os.waitpid(pid[i], 0)无效。如果一次运行4次迭代并且延迟未设置为大于约17秒,则也不会取消注释time.sleep()。鉴于时间(1)real就像0m17.733s那样一次完成4次迭代,我认为这表明While True循环本身并不是导致任何明显的低效率的原因(由于这些过程都采取相同的措施)时间量)并且17秒确实仅由子进程消耗。

出于深刻的怜悯之心,我现在放弃了我的另一个计划,我用subprocess.Popen()代替os.fork()。有了这个,我必须将函数发送到辅助脚本,该脚本通过文件定义作为Popen()的第一个参数的命令。然而,我确实使用了相同的While True循环。结果呢?它们与我在这里呈现的更简单的方案相同 - 几乎完全相同。

2 个答案:

答案 0 :(得分:2)

为什么不使用joblib.Parallel功能?

#!/usr/bin/env python

from joblib import Parallel, delayed

import numpy as np
np.seterr(all='raise')

def function(x):
    print '... the parameter is ', x
    arr = np.zeros(5000).reshape(-1,1)
    for r in range(3):
        for i in range(200):
            arr = np.concatenate( (arr, np.log( np.arange(2, 5002) ).reshape(-1,1) ), axis=1 )
    return { 'junk': arr, 'shape': arr.shape }

def main():
    print 'Running multifork.py.'

    print Parallel(n_jobs=2)(delayed(function)(3) for _ in xrange(4))

if __name__ == "__main__":
    main()

您的计算似乎存在一些瓶颈。

在您的示例中,您通过pipe传递数据,这不是一种非常快速的方法。要避免此性能问题,您应该使用共享内存。这就是multiprocessingjoblib.Parallel的工作方式。

此外,您应该记住,在单线程情况下,您不必对数据进行序列化和反序列化,但是在多进程的情况下,您必须这样做。

接下来,即使您有8个内核,也可以启用超线程功能,将核心性能划分为2个线程,因此,如果您有4个HW内核,则OS中有8个内核。使用HT有很多优点和缺点,但主要的是如果你要加载所有核心进行长时间的计算,那么你应该禁用它。

例如,我有一个Intel(R)Core(TM)i3-2100 CPU @ 3.10GHz,带有2个HW内核并启用了HT。所以在top我看到了4个核心。我计算的时间是:

  • n_jobs = 1 - 0:07.13
  • n_jobs = 2 - 0:06.54
  • n_jobs = 4 - 0:07.45

这就是lscpu的样子:

Architecture:          x86_64
CPU op-mode(s):        32-bit, 64-bit
Byte Order:            Little Endian
CPU(s):                4
On-line CPU(s) list:   0-3
Thread(s) per core:    2
Core(s) per socket:    2
Socket(s):             1
NUMA node(s):          1
Vendor ID:             GenuineIntel
CPU family:            6
Model:                 42
Stepping:              7
CPU MHz:               1600.000
BogoMIPS:              6186.10
Virtualization:        VT-x
L1d cache:             32K
L1i cache:             32K
L2 cache:              256K
L3 cache:              3072K
NUMA node0 CPU(s):     0-3

注意每个核心的线程行。

因此,在您的示例中,没有那么多计算而是数据传输。您的应用程序没有时间来获得并行性的优势。如果你有一个很长的计算工作(大约10分钟),我想你会得到它。

<强>此外:

我已经更详细地看了一下你的功能。我只用一次function(3)函数执行就替换了多次执行,并在profiler中运行它:

$ /usr/bin/time -v python -m cProfile so.py

输出很长时间,您可以在此处查看完整版本(http://pastebin.com/qLBBH5zU)。但主要的是该程序大部分时间都在numpy.concatenate函数中。你可以看到它:

ncalls  tottime  percall  cumtime  percall filename:lineno(function)
.........
600    1.375    0.002    1.375    0.002 {numpy.core.multiarray.concatenate}
.........
Elapsed (wall clock) time (h:mm:ss or m:ss): 0:01.64
.........

如果您将运行此程序的多个实例,您将看到时间会随着单个程序实例的执行时间而增加。我同时开始了2份副本:

$ /usr/bin/time -v python -m cProfile prog.py & /usr/bin/time -v python -m cProfile prog.py &

另一方面,我写了一个小的fibo函数:

def fibo(x):
    arr = (0, 1)
    for _ in xrange(x):
        arr = (arr[-1], sum(arr))
    return arr[0]

并将concatinate行替换为fibo(10000)。在这种情况下,单实例程序的执行时间为0:22.82,而两个实例的执行时间每个实例的执行时间几乎相同(0:24.62)。

基于此,我认为numpy可能使用一些共享资源导致并行化问题。或者它可以是numpyscipy特定问题。

关于代码的最后一件事,你必须替换下面的块:

for r in range(3):
    for i in range(200):
        arr = np.concatenate( (arr, np.log( np.arange(2, 5002) ).reshape(-1,1) ), axis=1 )

唯一一行:

arr = np.concatenate( (arr, np.log(np.arange(2, 5002).repeat(3*200).reshape(-1,3*200))), axis=1 )

答案 1 :(得分:1)

我在这里提供一个答案,以免留下如此混乱的记录。首先,我可以报告最平静的第一个joblib代码有效,并注意它更短,并且它也没有受到我的示例的While True循环的限制,这只能有效地工作每个工作大约花费相同的时间。我看到joblib项目目前支持,如果你不介意依赖第三方库,它可能是一个很好的解决方案。我可以采用它。

但是,使用我的测试功能时间(1)真实,使用time包装器的运行时间与joblib或我的穷人代码大致相同。

为了回答为什么与物理核心数量成反比的运行时间缩放不符合预期的问题,我努力地准备了我在这里提出的测试代码,以便产生类似的输出,并且我需要与我的实际项目的代码一样长的运行,每次运行没有并行性(我的实际项目同样是CPU绑定的,而不是I / O绑定的)。我这样做是为了测试,然后在我的相当复杂的项目中进行非常简单的简单代码。但我必须报告,尽管有这种相似性,但我的实际项目的结果要好得多。我很惊讶地看到我做了或多或少得到了物理核心数量的运行时间的反向缩放。

所以我假设 - 这是我对自己问题的初步答案 - 也许操作系统调度程序变幻无常且对工作类型非常敏感。并且由于其他进程可能正在运行,即使在我的情况下,其他进程几乎不使用任何CPU时间(我检查过,但它们不是),可能会有效。

提示#1:永远不要命名您的joblib测试代码joblib.py(您将收到导入错误)。提示#2:永远不要重命名joblib.py文件测试代码文件并运行重命名的文件而不删除joblib.py文件(您将收到导入错误)。