Tim*_*Tim 5 python numpy multiprocessing cpu-cache python-multiprocessing
附加的脚本针对不同大小的矩阵上不同数量的并行进程评估 numpy.conjugate 例程,并记录相应的运行时间。矩阵形状仅在第一维上有所不同(从 1,64,64 到 256,64,64)。共轭调用始终在 1,64,64 个子矩阵上进行,以确保正在处理的部分适合我系统上的 L2 缓存(每个核心 256 KB,在我的情况下 L3 缓存为 25MB)。运行脚本会生成下图(轴标签和颜色略有不同)。
正如您所看到的,从大约 100,64,64 的形状开始,运行时间取决于所使用的并行进程的数量。
这可能是什么原因造成的?
或者为什么 (100,64,64) 以下的矩阵对进程数的依赖性如此之低?
我的主要目标是找到对此脚本的修改,以便运行时尽可能独立于任意大小的矩阵“a”的进程数量。
如果有 20 个进程:
所有“a”矩阵最多占用:20 * 16 * 256 * 64 * 64 字节 = 320MB
所有“b”子矩阵最多占用:20 * 16 * 1 * 64 * 64 字节 = 1.25MB
因此,所有子矩阵同时适合 L3 缓存,并且单独适合 CPU 每个核心的 L2 缓存。我在这些测试中只使用了物理核心,没有使用超线程。
这是脚本:
from multiprocessing import Process, Queue
import time
import numpy as np
import os
from matplotlib import pyplot as plt
os.environ['OPENBLAS_NUM_THREADS'] = '1'
os.environ['MKL_NUM_THREADS'] = '1'
def f(q,size):
a = np.random.rand(size,64,64) + 1.j*np.random.rand(size,64,64)
start = time.time()
n=a.shape[0]
for i in range(20):
for b in a:
b.conj()
duration = time.time()-start
q.put(duration)
def speed_test(number_of_processes=1,size=1):
number_of_processes = number_of_processes
process_list=[]
queue = Queue()
#Start processes
for p_id in range(number_of_processes):
p = Process(target=f,args=(queue,size))
process_list.append(p)
p.start()
#Wait until all processes are finished
for p in process_list:
p.join()
output = []
while queue.qsize() != 0:
output.append(queue.get())
return np.mean(output)
if __name__ == '__main__':
processes=np.arange(1,20,3)
data=[[] for i in processes]
for p_id,p in enumerate(processes):
for size_0 in range(1,257):
data[p_id].append(speed_test(number_of_processes=p,size=size_0))
fig,ax = plt.subplots()
for d in data:
ax.plot(d)
ax.set_xlabel('Matrix Size: 1-256,64,64')
ax.set_ylabel('Runtime in seconds')
fig.savefig('result.png')
Run Code Online (Sandbox Code Playgroud)
该问题至少是由两种复杂效应的组合造成的:缓存抖动和频率缩放。我可以在我的 6 核 i5-9600KF 处理器上重现该效果。
最大的影响来自缓存抖动问题。通过查看 RAM 吞吐量可以轻松跟踪它。事实上,1 个进程为 4 GiB/s,6 个进程为 20 GiB/s。读取吞吐量与写入吞吐量类似,因此吞吐量是对称的。我的 RAM 最高可达约 40 GiB/s,但通常仅适用于混合读/写模式约 32 GiB/s。这意味着RAM压力相当大。此类用例通常发生在两种情况下:
乍一看,第一种情况更有可能发生在这里,因为数组是连续的并且非常大(不幸的是,另一种效果也会发生,见下文)。事实上,主要问题是a阵列太大,无法放入 L3。事实上,当大小 >128 时,a需要的时间超过128*64*64*8*2 = 8 MiB/process. 实际上,a它是由两个必须读取的数组构建的,因此缓存中所需的空间比这个大 3 倍:即。>24 MiB/进程。问题是所有进程分配相同数量的内存,因此进程数量越多,占用的累积空间就越大a。当累积空间大于缓存时,处理器需要将数据写入RAM并读回,速度较慢。
事实上,这更糟糕:进程没有完全同步,因此某些进程可能会因a.
此外,b.conj()创建一个新数组可能不会每次都在相同的内存分配中分配,因此处理器还需要写回数据。此效果取决于所使用的低级分配器。可以使用out参数 so 来解决这个问题。话虽这么说,这个问题在我的机器上并不重要(使用out6 个进程时速度提高了 2%,使用 1 个进程时速度同样快)。
简而言之,更多的进程访问更多的数据,并且全局数据量不适合 CPU 缓存,从而降低性能,因为需要一遍又一遍地重新加载数组。
现代处理器使用频率缩放(如涡轮增压)来使(相当)顺序应用程序更快,但它们在进行计算时不能对所有内核使用相同的频率,因为处理器的功率预算有限。这导致理论上的可扩展性较低。问题是所有进程都在做相同的工作,因此在 N 个核心上运行的 N 个进程并不比在 1 个核心上运行的 1 个进程花费更多时间。
当创建 1 个进程时,两个内核运行在 4550-4600 MHz(其他内核运行在 3700 MHz),而当运行 6 个进程时,所有内核运行在 4300 MHz。这足以解释我的机器上高达 7% 的差异。
您几乎无法控制涡轮频率,但您可以完全禁用它或控制频率,以便最小-最大频率都设置为基础频率。请注意,在异常情况下(即,当达到临界温度时进行节流),处理器可以自由地使用低得多的频率。我确实看到通过调整频率可以改善行为(实践中改善 7~10%)。
当进程数量等于核心数量时,与一个核心空闲用于其他任务相比,操作系统会执行更多的进程上下文切换。上下文切换会稍微降低进程的性能。当所有核心都被分配时尤其如此,因为操作系统调度程序很难避免不必要的迁移。这种情况通常发生在有许多正在运行的进程的 PC 上,但在计算机上则很少发生。这个开销在我的机器上大约是 5-10%。
请注意,进程数不应超过核心数(而不是超线程数)。超过这个限制,性能就很难预测,并且会出现许多复杂的开销(主要是调度问题)。
| 归档时间: |
|
| 查看次数: |
166 次 |
| 最近记录: |