基于 numpy 的计算的低效多处理

Inefficient multiprocessing of numpy-based calculations

我正在尝试在 Python 的 multiprocessing 模块的帮助下并行化一些使用 numpy 的计算。考虑这个简化的例子:

import time
import numpy

from multiprocessing import Pool

def test_func(i):

    a = numpy.random.normal(size=1000000)
    b = numpy.random.normal(size=1000000)

    for i in range(2000):
        a = a + b
        b = a - b
        a = a - b

    return 1

t1 = time.time()
test_func(0)
single_time = time.time() - t1
print("Single time:", single_time)

n_par = 4
pool = Pool()

t1 = time.time()
results_async = [
    pool.apply_async(test_func, [i])
    for i in range(n_par)]
results = [r.get() for r in results_async]
multicore_time = time.time() - t1

print("Multicore time:", multicore_time)
print("Efficiency:", single_time / multicore_time)

当我执行它时,multicore_time 大致等于 single_time * n_par,而我希望它接近 single_time。事实上,如果我用 time.sleep(10) 替换 numpy 计算,这就是我得到的——完美的效率。但出于某种原因,它不适用于 numpy。这可以解决吗,还是 numpy 的一些内部限制?

一些可能有用的附加信息:

您使用的测试函数似乎受内存限制。这意味着您看到的 运行 时间受到计算机将数组从内存拉入缓存的速度的限制。例如,a = a + b 行实际上使用了 3 个数组,ab 和一个将替换 a 的新数组。这三个数组每个大约 8MB(1e6 个浮点数 * 每个浮点数 8 个字节)。我相信不同的 i7 有 3MB - 8MB 的共享 L3 缓存,所以你不能一次将所有 3 个阵列都放入缓存中。您的 cpu 添加浮点数的速度比将数组加载到缓存中的速度快,因此大部分时间都花在等待从内存中读取数组上。因为缓存在核心之间共享,所以您不会通过将工作分散到多个核心来看到任何加速。

内存绑定操作通常是 numpy 的一个问题,我知道处理它们的唯一方法是使用 cython 或 numba 之类的东西。

一件可以提高效率的简单事情应该是在可能的情况下进行就地数组操作——因此 add(a,b,a) 不会 创建一个新数组,而 a = a + b 会。如果可以将 numpy 数组上的 for 循环重写为向量运算,那也应该更有效率。另一种可能性是使用 numpy.ctypeslib 启用共享内存 numpy 数组(参见:)。

我一直在为数学编程数值方法并遇到同样的问题:我没有看到任何 speed-up 用于所谓的 cpu 有界问题。原来我的问题是达到 CPU 缓存内存限制。

我一直在使用 Intel PCM(Intel® Performance Counter Monitor)来查看 cpu 缓存内存的行为(在 Linux ksysguard 中显示)。我还禁用了 2 个处理器以获得更清晰的结果(2 个处于活动状态)。

这是我用这段代码发现的:

def somethinglong(b):
    n=200000
    m=5000
    shared=np.arange(n)
    for i in np.arange(m):
        0.01*shared

pool = mp.Pool(2)
jobs = [() for i in range(8)]
for i in range(5):
    timei = time.time()
    pool.map(somethinglong, jobs , chunksize=1)
    #for job in jobs:
       #somethinglong(job)
print(time.time()-timei)

未达到缓存内存限制的示例:

  • n=10000
  • 米=100000
  • 顺序执行:15s
  • 2 处理器池无缓存内存限制:8s

可以看出没有缓存未命中(所有缓存命中),因此 speed-up 几乎是完美的:15/8。 Memory cache hits 2 pool

达到缓存内存限制的示例:

  • n=200000
  • 米=5000
  • 顺序执行:14s
  • 2 处理器池缓存内存限制:14s

在这种情况下,我增加了我们操作的向量的大小(并减少了循环大小,以查看合理的执行时间)。在这种情况下,我们可以看到内存已满,进程总是错过缓存内存。因此没有得到任何加速:15/15。 Memory cache misses 2 pool

观察:将操作分配给变量(aux = 0.01*shared)也使用缓存内存并且可以通过内存绑定问题(不增加任何向量大小)。