Multiprocessing.Pool使Numpy矩阵乘法更慢

时间:2013-03-14 15:54:07

标签: python numpy multiprocessing pool

所以,我正在玩multiprocessing.PoolNumpy,但似乎我错过了一些重要的观点。为什么pool版本要慢得多?我看了htop,我可以看到创建了几个进程,但它们共享一个CPU,加起来大约100%。

$ cat test_multi.py 
import numpy as np
from timeit import timeit
from multiprocessing import Pool


def mmul(matrix):
    for i in range(100):
        matrix = matrix * matrix
    return matrix

if __name__ == '__main__':
    matrices = []
    for i in range(4):
        matrices.append(np.random.random_integers(100, size=(1000, 1000)))

    pool = Pool(8)
    print timeit(lambda: map(mmul, matrices), number=20)
    print timeit(lambda: pool.map(mmul, matrices), number=20)

$ python test_multi.py 
16.0265390873
19.097837925

[更新]

  • 更改为基准流程的timeit
  • 具有多个核心的init池
  • 改变计算,以便有更多的计算和更少的内存传输(我希望)

仍然没有变化。 pool版本仍然较慢,我可以在htop中看到只使用了一个核心,也产生了几个进程。

[UPDATE2]

目前我正在阅读@ Jan-Philip Gehrcke关于使用multiprocessing.Process()Queue的建议。但与此同时我想知道:

  1. 为什么我的例子适用于蒂亚戈?可能是因为它无法在我的机器1上运行?
  2. 我的示例代码是否在进程之间进行了任何复制?我打算让我的代码给每个线程一个矩阵列表矩阵。
  3. 我的代码是不好的例子,因为我使用Numpy
  4. 我了解到,当其他人知道我的最终目标时,通常会得到更好的答案:我有很多文件,这些文件是以连续方式加载和处理的。处理是CPU密集型的,所以我假设可以通过并行化获得很多。我的目的是调用并行分析文件的python函数。此外,这个函数只是C代码的接口,我认为,这有所不同。

    1 Ubuntu 12.04,Python 2.7.3,i7 860 @ 2.80 - 如果您需要更多信息,请发表评论。

    [UPDATE3]

    以下是Stefano示例代码的结果。出于某种原因,没有加速。 :/

    testing with 16 matrices
    base  4.27
       1  5.07
       2  4.76
       4  4.71
       8  4.78
      16  4.79
    testing with 32 matrices
    base  8.82
       1 10.39
       2 10.58
       4 10.73
       8  9.46
      16  9.54
    testing with 64 matrices
    base 17.38
       1 19.34
       2 19.62
       4 19.59
       8 19.39
      16 19.34
    

    [更新4]回答Jan-Philip Gehrcke's comment

    很抱歉,我没有让自己更清楚。正如我在Update 2中所写的,我的主要目标是并行化第三方Python库函数的许多串行调用。此函数是某些C代码的接口。我被建议使用Pool,但这不起作用,所以我尝试了一些更简单的方法,上面的示例显示了numpy。但是在那里我无法实现性能提升,即使它在寻找我'贪图并行化'。所以我认为我一定错过了重要的事情。这个信息是我正在寻找的这个问题和赏金。

    [更新5]

    感谢您的巨大投入。但阅读你的答案只会给我带来更多问题。因此,当我更清楚地了解我不知道的内容时,我会阅读basics并创建新的SO问题。

8 个答案:

答案 0 :(得分:17)

关于所有进程都在同一个CPU上运行see my answer here

在导入期间,numpy会更改父进程的CPU亲和性,这样当您以后使用Pool时,它产生的所有工作进程最终都会争夺相同的核心,而不是而不是使用机器上的所有核心。

您可以在导入taskset后调用numpy以重置CPU关联,以便使用所有核心:

import numpy as np
import os
from timeit import timeit
from multiprocessing import Pool


def mmul(matrix):
    for i in range(100):
        matrix = matrix * matrix
    return matrix

if __name__ == '__main__':

    matrices = []
    for i in range(4):
        matrices.append(np.random.random_integers(100, size=(1000, 1000)))

    print timeit(lambda: map(mmul, matrices), number=20)

    # after importing numpy, reset the CPU affinity of the parent process so
    # that it will use all cores
    os.system("taskset -p 0xff %d" % os.getpid())

    pool = Pool(8)
    print timeit(lambda: pool.map(mmul, matrices), number=20)

输出:

    $ python tmp.py                                     
    12.4765810966
    pid 29150's current affinity mask: 1
    pid 29150's new affinity mask: ff
    13.4136221409

如果在运行此脚本时使用top观察CPU使用情况,则应在执行“并行”部分时使用所有核心来查看它。正如其他人所指出的那样,在您的原始示例中,酸洗数据,流程创建等所涉及的开销可能超过并行化带来的任何可能的好处。

编辑:我怀疑单个进程似乎始终更快的部分原因是numpy可能有一些技巧可以加快元素级矩阵乘法的速度当作业分布在多个核心时,无法使用。

例如,如果我只使用普通的Python列表来计算Fibonacci序列,我可以从并行化中获得巨大的加速。同样,如果我以不利于矢量化的方式进行逐元素乘法,我会得到类似的并行版本的加速:

import numpy as np
import os
from timeit import timeit
from multiprocessing import Pool

def fib(dummy):
    n = [1,1]
    for ii in xrange(100000):
        n.append(n[-1]+n[-2])

def silly_mult(matrix):
    for row in matrix:
        for val in row:
            val * val

if __name__ == '__main__':

    dt = timeit(lambda: map(fib, xrange(10)), number=10)
    print "Fibonacci, non-parallel: %.3f" %dt

    matrices = [np.random.randn(1000,1000) for ii in xrange(10)]
    dt = timeit(lambda: map(silly_mult, matrices), number=10)
    print "Silly matrix multiplication, non-parallel: %.3f" %dt

    # after importing numpy, reset the CPU affinity of the parent process so
    # that it will use all CPUS
    os.system("taskset -p 0xff %d" % os.getpid())

    pool = Pool(8)

    dt = timeit(lambda: pool.map(fib,xrange(10)), number=10)
    print "Fibonacci, parallel: %.3f" %dt

    dt = timeit(lambda: pool.map(silly_mult, matrices), number=10)
    print "Silly matrix multiplication, parallel: %.3f" %dt

输出:

$ python tmp.py
Fibonacci, non-parallel: 32.449
Silly matrix multiplication, non-parallel: 40.084
pid 29528's current affinity mask: 1
pid 29528's new affinity mask: ff
Fibonacci, parallel: 9.462
Silly matrix multiplication, parallel: 12.163

答案 1 :(得分:12)

通信开销和计算加速之间的不可预测的竞争肯定是这里的问题。你所观察到的完全没问题。您是否获得净加速取决于许多因素,并且必须正确量化(如您所做)。

那么为什么multiprocessing在您的情况下会出现“意外缓慢”? multiprocessing的{​​{1}}和map函数实际上是腌制Python对象来回通过连接父进程和子进程的管道。这可能需要相当长的时间。在此期间,子进程几乎无关,这可以在map_async中看到。在不同的系统之间,可能存在相当大的管道传输性能差异,这也是为什么对于某些人来说你的池代码比单个CPU代码更快的原因,尽管对你来说它不是(其他因素可能在这里起作用,这只是一个例子,以解释效果)。

你可以做些什么来加快速度?

  1. 请勿在符合POSIX标准的系统上挑选输入。

    如果您使用的是Unix,则可以利用POSIX的进程fork行为(写入时复制内存)来绕过parent->子通信开销:

    全局可访问变量中创建要在父进程中处理的作业输入(例如,大型矩阵列表)。然后通过自己调用htop来创建工作进程。在子项中,从全局变量中获取作业输入。简单地说,这使得孩子访问父母的内存而没有任何通信开销(*,下面的解释)。通过例如将结果发送回父母一个multiprocessing.Process()。这将节省大量的通信开销,特别是如果输出与输入相比较小。这种方法不适用于例如Windows,因为multiprocessing.Queue创建了一个全新的Python进程,它不会继承父进程的状态。

  2. 使用numpy多线程。 根据您的实际计算任务,可能会发生涉及multiprocessing.Process()根本没有帮助的情况。如果您自己编译numpy并启用OpenMP指令,那么对大型矩阵的操作可能会非常有效地多线程(并且分布在许多CPU内核上; GIL不是限制因素)。基本上,这是在numpy / scipy环境中可以获得的多个CPU核心的最有效使用。

  3. *孩子一般不能直接访问父母的记忆。但是,在multiprocessing之后,父级和子级处于等效状态。将父级的整个内存复制到RAM中的另一个位置是愚蠢的。这就是写入时写入原理的原因。只要孩子不更改其内存状态,它实际上就会访问父母的内存。只有在修改后,相应的比特和片段才会被复制到孩子的记忆空间中。

    主要编辑:

    让我添加一段代码,用多个工作进程处理大量输入数据,并遵循建议“1.不要在POSIX兼容系统上挑选输入。”此外,传回工人经理(父流程)的信息量非常低。该示例的重计算部分是单值分解。它可以大量使用OpenMP。我已多次执行该示例:

    • 一次使用1,2或4个工作进程和fork(),因此每个工作进程创建的最大负载为100%。在那里,提到的工人数量计算时间缩放行为几乎是线性的,净增加因子上升对应于所涉及的工人数量。
    • 一次使用1,2或4个工作进程和OMP_NUM_THREADS=1,以便每个进程创建400%的最大负载(通过生成4个OpenMP线程)。我的机器有16个真实核心,因此每个最多400%负载的4个进程将几乎从机器中获得最大性能。缩放不再是完全线性的,加速因子不是所涉及的工人数量,但绝对计算时间与OMP_NUM_THREADS=4相比显着减少,并且时间仍然随工人流程数量显着减少。
    • 使用较大的输入数据,4个核心和OMP_NUM_THREADS=1。它导致平均系统负载为1253%。
    • 使用与上次相同的设置,但OMP_NUM_THREADS=4。它导致平均系统负载为1598%,这表明我们从16核心机器获得了所有东西。但是,与后一种情况相比,实际计算墙时间没有改善。

    代码:

    OMP_NUM_THREADS=5

    输出:

    import os
    import time
    import math
    import numpy as np
    from numpy.linalg import svd as svd
    import multiprocessing
    
    
    # If numpy is compiled for OpenMP, then make sure to control
    # the number of OpenMP threads via the OMP_NUM_THREADS environment
    # variable before running this benchmark.
    
    
    MATRIX_SIZE = 1000
    MATRIX_COUNT = 16
    
    
    def rnd_matrix():
        offset = np.random.randint(1,10)
        stretch = 2*np.random.rand()+0.1
        return offset + stretch * np.random.rand(MATRIX_SIZE, MATRIX_SIZE)
    
    
    print "Creating input matrices in parent process."
    # Create input in memory. Children access this input.
    INPUT = [rnd_matrix() for _ in xrange(MATRIX_COUNT)]
    
    
    def worker_function(result_queue, worker_index, chunk_boundary):
        """Work on a certain chunk of the globally defined `INPUT` list.
        """
        result_chunk = []
        for m in INPUT[chunk_boundary[0]:chunk_boundary[1]]:
            # Perform single value decomposition (CPU intense).
            u, s, v = svd(m)
            # Build single numeric value as output.
            output =  int(np.sum(s))
            result_chunk.append(output)
        result_queue.put((worker_index, result_chunk))
    
    
    def work(n_workers=1):
        def calc_chunksize(l, n):
            """Rudimentary function to calculate the size of chunks for equal 
            distribution of a list `l` among `n` workers.
            """
            return int(math.ceil(len(l)/float(n)))
    
        # Build boundaries (indices for slicing) for chunks of `INPUT` list.
        chunk_size = calc_chunksize(INPUT, n_workers)
        chunk_boundaries = [
            (i, i+chunk_size) for i in xrange(0, len(INPUT), chunk_size)]
    
        # When n_workers and input list size are of same order of magnitude,
        # the above method might have created less chunks than workers available. 
        if n_workers != len(chunk_boundaries):
            return None
    
        result_queue = multiprocessing.Queue()
        # Prepare child processes.
        children = []
        for worker_index in xrange(n_workers):
            children.append(
                multiprocessing.Process(
                    target=worker_function,
                    args=(
                        result_queue,
                        worker_index,
                        chunk_boundaries[worker_index],
                        )
                    )
                )
    
        # Run child processes.
        for c in children:
            c.start()
    
        # Create result list of length of `INPUT`. Assign results upon arrival.
        results = [None] * len(INPUT)
    
        # Wait for all results to arrive.
        for _ in xrange(n_workers):
            worker_index, result_chunk = result_queue.get(block=True)
            chunk_boundary = chunk_boundaries[worker_index]
            # Store the chunk of results just received to the overall result list.
            results[chunk_boundary[0]:chunk_boundary[1]] = result_chunk
    
        # Join child processes (clean up zombies).
        for c in children:
            c.join()
        return results
    
    
    def main():
        durations = []
        n_children = [1, 2, 4]
        for n in n_children:
            print "Crunching input with %s child(ren)." % n
            t0 = time.time()
            result = work(n)
            if result is None:
                continue
            duration = time.time() - t0
            print "Result computed by %s child process(es): %s" % (n, result)
            print "Duration: %.2f s" % duration
            durations.append(duration)
        normalized_durations = [durations[0]/d for d in durations]
        for n, normdur in zip(n_children, normalized_durations):
            print "%s-children speedup: %.2f" % (n, normdur)
    
    
    if __name__ == '__main__':
        main()
    

答案 2 :(得分:4)

您的代码是正确的。我只是运行它我的系统(2核,超线程)并获得以下结果:

$ python test_multi.py 
30.8623809814
19.3914041519

我查看了这些流程,正如预期的那样,并行部分显示了几个工作在接近100%的流程。这必须是你的系统或python安装中的东西。

答案 3 :(得分:2)

默认情况下,Pool仅使用n个进程,其中n是计算机上的CPU数。您需要指定要使用的进程数,例如Pool(5)

See here for more info

答案 4 :(得分:2)

测量算术吞吐量是一项非常困难的任务:基本上你的测试用例太简单了,我发现很多问题。

首先你要测试整数运算:有特殊原因吗?使用浮点数,您可以获得在许多不同体系结构中具有可比性的结果。

第二个matrix = matrix*matrix会覆盖输入参数(矩阵由ref传递,而不是按值传递),每个样本都必须处理不同的数据......

为了掌握一般趋势,最后的测试应该针对更广泛的问题规模和工人数量进行。

所以这是我修改过的测试脚本

import numpy as np
from timeit import timeit
from multiprocessing import Pool

def mmul(matrix):
    mymatrix = matrix.copy()
    for i in range(100):
        mymatrix *= mymatrix
    return mymatrix

if __name__ == '__main__':

    for n in (16, 32, 64):
        matrices = []
        for i in range(n):
            matrices.append(np.random.random_sample(size=(1000, 1000)))

        stmt = 'from __main__ import mmul, matrices'
        print 'testing with', n, 'matrices'
        print 'base',
        print '%5.2f' % timeit('r = map(mmul, matrices)', setup=stmt, number=1)

        stmt = 'from __main__ import mmul, matrices, pool'
        for i in (1, 2, 4, 8, 16):
            pool = Pool(i)
            print "%4d" % i, 
            print '%5.2f' % timeit('r = pool.map(mmul, matrices)', setup=stmt, number=1)
            pool.close()
            pool.join()

和我的结果:

$ python test_multi.py 
testing with 16 matrices
base  5.77
   1  6.72
   2  3.64
   4  3.41
   8  2.58
  16  2.47
testing with 32 matrices
base 11.69
   1 11.87
   2  9.15
   4  5.48
   8  4.68
  16  3.81
testing with 64 matrices
base 22.36
   1 25.65
   2 15.60
   4 12.20
   8  9.28
  16  9.04

[更新]我在家中的另一台计算机上运行此示例,获得一致的减速:

testing with 16 matrices
base  2.42
   1  2.99
   2  2.64
   4  2.80
   8  2.90
  16  2.93
testing with 32 matrices
base  4.77
   1  6.01
   2  5.38
   4  5.76
   8  6.02
  16  6.03
testing with 64 matrices
base  9.92
   1 12.41
   2 10.64
   4 11.03
   8 11.55
  16 11.59

我必须承认我不知道应该责怪谁(numpy,python,编译器,内核)......

答案 5 :(得分:1)

既然你提到你有很多文件,我会建议以下解决方案;

  • 列出文件名。
  • 编写一个函数,用于加载和处理名为输入参数的单个文件。
  • 使用Pool.map()将该功能应用于文件列表。

由于每个实例现在都加载了自己的文件,因此传递的唯一数据是文件名,而不是(可能很大)的numpy数组。

答案 6 :(得分:0)

我还注意到,当我在Pool.map()函数内部运行numpy矩阵乘法时,在某些计算机上运行速度慢得多。我的目标是使用Pool.map()并行化工作,并在计算机的每个核心上运行一个进程。当事情运行很快时,numpy矩阵乘法只是并行执行的全部工作的一小部分。当我查看进程的CPU使用率时,我可以看到每个进程可以使用例如在运行速度较慢的计算机上,CPU占400%以上,但在运行速度较快的计算机上,总是<= 100%。对我来说,解决方案是stop numpy from multithreading。事实证明,在我的Pool.map()运行缓慢的计算机上,将numpy设置为多线程。显然,如果您已经使用Pool.map()进行并行化,那么让numpy也进行并行化只会产生干扰。在运行我的Python代码之前,我刚刚打电话给export MKL_NUM_THREADS=1,它在任何地方都能快速运行。

答案 7 :(得分:0)

解决方案

在任何计算之前 设置以下环境变量(对于某些较早版本的numpy,您可能需要在进行import numpy之前设置它们):

os.environ["OMP_NUM_THREADS"] = "1"
os.environ["MKL_NUM_THREADS"] = "1"
os.environ["OPENBLAS_NUM_THREADS"] = "1"
os.environ["VECLIB_MAXIMUM_THREADS"] = "1"
os.environ["NUMEXPR_NUM_THREADS"] = "1"

它如何工作

使用多线程和优化库(例如OpenMP或MKL或OpenBLAS等)进行numpy的实现已经 。这就是为什么我们看不到通过自己实现多处理而获得的任何改进。更糟糕的是,我们遭受了太多线程。例如,如果我的计算机具有8个CPU内核,则在编写处理代码时,numpy可能会使用8个线程进行计算。然后,我使用多重处理来启动8个进程,我得到64个线程。这是没有好处的,线程之间的上下文切换和其他开销可能会花费更多时间。通过设置上述环境变量,我们将每个进程的线程数限制为1,这样我们就可以得到最有效的总线程数。

代码示例

from timeit import timeit
from multiprocessing import Pool
import sys
import os

import numpy as np

def matmul(_):
    matrix = np.ones(shape=(1000, 1000))
    _ = np.matmul(matrix, matrix)

def mixed(_):
    matrix = np.ones(shape=(1000, 1000))
    _ = np.matmul(matrix, matrix)

    s = 0
    for i in range(1000000):
        s += i

if __name__ == '__main__':
    if sys.argv[1] == "--set-num-threads":
        os.environ["OMP_NUM_THREADS"] = "1"
        os.environ["MKL_NUM_THREADS"] = "1"
        os.environ["OPENBLAS_NUM_THREADS"] = "1"
        os.environ["VECLIB_MAXIMUM_THREADS"] = "1"
        os.environ["NUMEXPR_NUM_THREADS"] = "1"

    if sys.argv[2] == "matmul":
        f = matmul
    elif sys.argv[2] == "mixed":
        f = mixed

    print("Serial:")
    print(timeit(lambda: list(map(f, [0] * 8)), number=20))

    with Pool(8) as pool:
        print("Multiprocessing:")
        print(timeit(lambda: pool.map(f, [0] * 8), number=20))

我在具有8个vCPU(不一定意味着8个内核)的AWS p3.2xlarge实例上测试了代码:

$ python test_multi.py --no-set-num-threads matmul
Serial:
3.3447616740000115
Multiprocessing:
3.5941055110000093

$ python test_multi.py --set-num-threads matmul
Serial:
9.464500446000102
Multiprocessing:
2.570238267999912

在设置这些环境变量之前,串行版本和多处理版本并没有太大区别,大约三秒钟,通常多处理版本较慢,就像OP演示的那样。设置线程数后,我们看到串行版本花费了9.46秒,变得越来越慢!这证明即使在使用单个进程的情况下,numpy仍在利用多线程。多处理版本花费了2.57秒,改进了一点,这可能是因为在我的实现中节省了跨线程数据传输时间。

由于numpy已经在使用并行化,因此该示例并未显示出多处理的强大功能。当普通的Python密集型CPU计算与numpy操作混合使用时,多处理最为有益。例如

$ python test_multi.py --no-set-num-threads mixed
Serial:
12.380275611000116
Multiprocessing:
8.190792100999943

$ python test_multi.py --set-num-threads mixed
Serial:
18.512066430999994
Multiprocessing:
4.8058130150000125

此处将线程数设置为1的多处理最快。

备注:这也适用于其他一些CPU计算库,例如PyTorch。