Python 中的多处理:Numpy + 向量求和 -> 大幅减速
Posted
技术标签:
【中文标题】Python 中的多处理:Numpy + 向量求和 -> 大幅减速【英文标题】:Multi-processing in Python: Numpy + Vector Summation -> Huge Slowdown 【发布时间】:2016-07-22 21:23:34 【问题描述】:请不要因为这篇长文而气馁。我尝试提供尽可能多的数据,我真的需要帮助解决这个问题:S。如果有新的提示或想法,我会每天更新
问题:
我尝试在并行进程的帮助下在两核机器上并行运行 Python 代码(以避免 GIL),但存在代码显着变慢的问题。例如,在单核机器上运行每个工作负载需要 600 秒,但在双核机器上运行需要 1600 秒(每个工作负载 800 秒)。
我已经尝试过的:
我测量了内存,似乎没有内存问题。 [仅在高点使用 20%]。
我使用“htop”来检查我是否真的在不同的内核上运行程序,或者我的内核亲和力是否混乱。但也没有运气,我的程序正在我所有的内核上运行。
问题是一个 CPU 受限问题,因此我检查并确认我的代码大部分时间在所有内核上以 100% CPU 运行。
我检查了进程 ID,并且确实生成了两个不同的进程。
我将提交给执行程序 [ e.submit(function,[…]) ] 的函数更改为计算饼图函数,并观察到了巨大的加速。所以问题很可能出现在我提交给执行程序的 process_function(...) 中,而不是之前的代码中。
目前我正在使用“并发”中的“期货”来并行化任务。但我也尝试了“多处理”中的“池”类。但是结果还是一样。
代码:
衍生进程:
result = [None]*psutil.cpu_count()
e = futures.ProcessPoolExecutor( max_workers=psutil.cpu_count() )
for i in range(psutil.cpu_count()):
result[i] = e.submit(process_function, ...)
处理函数:
from math import floor
from math import ceil
import numpy
import mysqldb
import time
db = MySQLdb.connect(...)
cursor = db.cursor()
query = "SELECT ...."
cursor.execute(query)
[...] #save db results into the variable db_matrix (30 columns, 5.000 rows)
[...] #save db results into the variable bp_vector (3 columns, 500 rows)
[...] #save db results into the variable option_vector( 3 columns, 4000 rows)
cursor.close()
db.close()
counter = 0
for i in range(4000):
for j in range(500):
helper[:] = (1-bp_vector[j,0]-bp_vector[j,1]-bp_vector[j,2])*db_matrix[:,0]
+ db_matrix[:,option_vector[i,0]] * bp_vector[j,0]
+ db_matrix[:,option_vector[i,1]] * bp_vector[j,1]
+ db_matrix[:,option_vector[i,2]] * bp_vector[j,2]
result[counter,0] = (helper < -7.55).sum()
counter = counter + 1
return result
我的猜测:
我的猜测是,由于某种原因,创建向量“助手”的加权向量乘法会导致问题。 [我相信时间测量部分证实了这个猜测]
是不是 numpy 会造成这些问题? numpy 与多处理兼容吗?如果没有,我该怎么办? [已在 cmets 中回答]
会不会是缓存的原因?我在论坛上读过它,但说实话,并没有真正理解它。但如果问题根源在那里,我会让自己熟悉这个话题。
时间测量:(编辑)
一个核心:从数据库获取数据的时间:8 秒。
两核:从数据库获取数据的时间:12秒。
一个核心:在 process_function 中执行双循环的时间:~ 640 秒。
两个核心:在process_function中做双循环的时间:~1600秒
更新:(编辑)
当我为循环中的每 100 个 i 测量两个进程的时间时,我发现它大约是我在仅在一个进程上运行时测量同一事物时观察到的时间的 220%。但更神奇的是,如果我在运行过程中退出某个进程,其他进程就会加速!然后另一个过程实际上加速到它在单人运行期间的相同水平。因此,我目前看不到的进程之间肯定存在一些依赖关系:S
Update-2:(编辑)
所以,我又进行了几次测试运行和测量。在测试运行中,我使用 单核 linux 机器(n1-standard-1, 1 vCPU, 3.75 GB 内存)或 双核 linux 机器(n1-standard-2、2 个 vCPU、7.5 GB 内存)来自 Google 云计算引擎。但是,我也在本地计算机上进行了测试,并观察到大致相同的结果。 (-> 因此,虚拟化环境应该没问题)。结果如下:
P.S:这里的时间与上面的测量值不同,因为我稍微限制了循环并在 Google Cloud 上而不是在我的家用电脑上进行了测试。
1核机器,启动1个进程:
时间:225 秒,CPU 利用率:~100%
1核机器,启动2个进程:
时间:557 秒,CPU 利用率:~100%
1 核机器,启动 1 个进程,最大限制。 CPU 利用率达到 50%:
时间:488 秒,CPU 利用率:~50%
.
2核机器,启动2个进程:
时间:665 秒,CPU-1 利用率:~100%,CPU-2 利用率:~100%
进程没有在核心之间跳转,每个使用1个核心
(至少 htop 在“进程”列中显示了这些结果)
2核机器,启动1个进程:
时间:222 秒,CPU-1 利用率:~100% (0%),CPU-2 利用率:~0% (100%)
但是,进程有时会在内核之间跳转
2核机器,启动1个进程,最大限制。 CPU 利用率达到 50%:
时间:493 秒,CPU-1 利用率:~50% (0%),CPU-2 利用率:~0% (100%)
但是,进程在内核之间非常频繁地跳转
我使用“htop”和python模块“time”来获得这些结果。
更新 - 3:(编辑)
我使用 cProfile 来分析我的代码:
python -m cProfile -s cumtime fun_name.py
文件太长,无法在此处发布,但我相信如果它们包含有价值的信息,那么这些信息可能是结果文本之上的信息。因此,我将在此处发布结果的第一行:
1核机器,启动1个进程:
623158 function calls (622735 primitive calls) in 229.286 seconds
Ordered by: cumulative time
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.371 0.371 229.287 229.287 20_with_multiprocessing.py:1(<module>)
3 0.000 0.000 225.082 75.027 threading.py:309(wait)
1 0.000 0.000 225.082 225.082 _base.py:378(result)
25 225.082 9.003 225.082 9.003 method 'acquire' of 'thread.lock' objects
1 0.598 0.598 3.081 3.081 get_BP_Verteilung_Vektoren.py:1(get_BP_Verteilung_Vektoren)
3 0.000 0.000 2.877 0.959 cursors.py:164(execute)
3 0.000 0.000 2.877 0.959 cursors.py:353(_query)
3 0.000 0.000 1.958 0.653 cursors.py:315(_do_query)
3 0.000 0.000 1.943 0.648 cursors.py:142(_do_get_result)
3 0.000 0.000 1.943 0.648 cursors.py:351(_get_result)
3 1.943 0.648 1.943 0.648 method 'store_result' of '_mysql.connection' objects
3 0.001 0.000 0.919 0.306 cursors.py:358(_post_get_result)
3 0.000 0.000 0.917 0.306 cursors.py:324(_fetch_row)
3 0.917 0.306 0.917 0.306 built-in method fetch_row
591314 0.161 0.000 0.161 0.000 range
1核机器,启动2个进程:
626052 function calls (625616 primitive calls) in 578.086 seconds
Ordered by: cumulative time
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.310 0.310 578.087 578.087 20_with_multiprocessing.py:1(<module>)
30 574.310 19.144 574.310 19.144 method 'acquire' of 'thread.lock' objects
2 0.000 0.000 574.310 287.155 _base.py:378(result)
3 0.000 0.000 574.310 191.437 threading.py:309(wait)
1 0.544 0.544 2.854 2.854 get_BP_Verteilung_Vektoren.py:1(get_BP_Verteilung_Vektoren)
3 0.000 0.000 2.563 0.854 cursors.py:164(execute)
3 0.000 0.000 2.563 0.854 cursors.py:353(_query)
3 0.000 0.000 1.715 0.572 cursors.py:315(_do_query)
3 0.000 0.000 1.701 0.567 cursors.py:142(_do_get_result)
3 0.000 0.000 1.701 0.567 cursors.py:351(_get_result)
3 1.701 0.567 1.701 0.567 method 'store_result' of '_mysql.connection' objects
3 0.001 0.000 0.848 0.283 cursors.py:358(_post_get_result)
3 0.000 0.000 0.847 0.282 cursors.py:324(_fetch_row)
3 0.847 0.282 0.847 0.282 built-in method fetch_row
591343 0.152 0.000 0.152 0.000 range
.
2核机器,启动1个进程:
623164 function calls (622741 primitive calls) in 235.954 seconds
Ordered by: cumulative time
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.246 0.246 235.955 235.955 20_with_multiprocessing.py:1(<module>)
3 0.000 0.000 232.003 77.334 threading.py:309(wait)
25 232.003 9.280 232.003 9.280 method 'acquire' of 'thread.lock' objects
1 0.000 0.000 232.003 232.003 _base.py:378(result)
1 0.593 0.593 3.104 3.104 get_BP_Verteilung_Vektoren.py:1(get_BP_Verteilung_Vektoren)
3 0.000 0.000 2.774 0.925 cursors.py:164(execute)
3 0.000 0.000 2.774 0.925 cursors.py:353(_query)
3 0.000 0.000 1.981 0.660 cursors.py:315(_do_query)
3 0.000 0.000 1.970 0.657 cursors.py:142(_do_get_result)
3 0.000 0.000 1.969 0.656 cursors.py:351(_get_result)
3 1.969 0.656 1.969 0.656 method 'store_result' of '_mysql.connection' objects
3 0.001 0.000 0.794 0.265 cursors.py:358(_post_get_result)
3 0.000 0.000 0.792 0.264 cursors.py:324(_fetch_row)
3 0.792 0.264 0.792 0.264 built-in method fetch_row
591314 0.144 0.000 0.144 0.000 range
2核机器,启动2个进程:
626072 function calls (625636 primitive calls) in 682.460 seconds
Ordered by: cumulative time
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.334 0.334 682.461 682.461 20_with_multiprocessing.py:1(<module>)
4 0.000 0.000 678.231 169.558 threading.py:309(wait)
33 678.230 20.552 678.230 20.552 method 'acquire' of 'thread.lock' objects
2 0.000 0.000 678.230 339.115 _base.py:378(result)
1 0.527 0.527 2.974 2.974 get_BP_Verteilung_Vektoren.py:1(get_BP_Verteilung_Vektoren)
3 0.000 0.000 2.723 0.908 cursors.py:164(execute)
3 0.000 0.000 2.723 0.908 cursors.py:353(_query)
3 0.000 0.000 1.749 0.583 cursors.py:315(_do_query)
3 0.000 0.000 1.736 0.579 cursors.py:142(_do_get_result)
3 0.000 0.000 1.736 0.579 cursors.py:351(_get_result)
3 1.736 0.579 1.736 0.579 method 'store_result' of '_mysql.connection' objects
3 0.001 0.000 0.975 0.325 cursors.py:358(_post_get_result)
3 0.000 0.000 0.973 0.324 cursors.py:324(_fetch_row)
3 0.973 0.324 0.973 0.324 built-in method fetch_row
5 0.093 0.019 0.304 0.061 __init__.py:1(<module>)
1 0.017 0.017 0.275 0.275 __init__.py:106(<module>)
1 0.005 0.005 0.198 0.198 add_newdocs.py:10(<module>)
591343 0.148 0.000 0.148 0.000 range
就我个人而言,我真的不知道如何处理这些结果。很高兴收到提示、提示或任何其他帮助 - 谢谢 :)
回复答案 1:(编辑)
Roland Smith 研究了数据并提出,多处理可能对性能的伤害大于它的帮助。因此,我在没有多处理的情况下又进行了一次测量(如他建议的代码):
我的结论是否正确,事实并非如此?因为测量的时间似乎与之前使用多处理测量的时间相似?
1核机:
数据库访问耗时 2.53 秒
矩阵操作耗时 236.71 秒
1842384 function calls (1841974 primitive calls) in 241.114 seconds
Ordered by: cumulative time
ncalls tottime percall cumtime percall filename:lineno(function)
1 219.036 219.036 241.115 241.115 20_with_multiprocessing.py:1(<module>)
406000 0.873 0.000 18.097 0.000 method 'sum' of 'numpy.ndarray' objects
406000 0.502 0.000 17.224 0.000 _methods.py:31(_sum)
406001 16.722 0.000 16.722 0.000 method 'reduce' of 'numpy.ufunc' objects
1 0.587 0.587 3.222 3.222 get_BP_Verteilung_Vektoren.py:1(get_BP_Verteilung_Vektoren)
3 0.000 0.000 2.964 0.988 cursors.py:164(execute)
3 0.000 0.000 2.964 0.988 cursors.py:353(_query)
3 0.000 0.000 1.958 0.653 cursors.py:315(_do_query)
3 0.000 0.000 1.944 0.648 cursors.py:142(_do_get_result)
3 0.000 0.000 1.944 0.648 cursors.py:351(_get_result)
3 1.944 0.648 1.944 0.648 method 'store_result' of '_mysql.connection' objects
3 0.001 0.000 1.006 0.335 cursors.py:358(_post_get_result)
3 0.000 0.000 1.005 0.335 cursors.py:324(_fetch_row)
3 1.005 0.335 1.005 0.335 built-in method fetch_row
591285 0.158 0.000 0.158 0.000 range
2核机:
数据库访问耗时 2.32 秒
矩阵操作耗时 242.45 秒
1842390 function calls (1841980 primitive calls) in 246.535 seconds
Ordered by: cumulative time
ncalls tottime percall cumtime percall filename:lineno(function)
1 224.705 224.705 246.536 246.536 20_with_multiprocessing.py:1(<module>)
406000 0.911 0.000 17.971 0.000 method 'sum' of 'numpy.ndarray' objects
406000 0.526 0.000 17.060 0.000 _methods.py:31(_sum)
406001 16.534 0.000 16.534 0.000 method 'reduce' of 'numpy.ufunc' objects
1 0.617 0.617 3.113 3.113 get_BP_Verteilung_Vektoren.py:1(get_BP_Verteilung_Vektoren)
3 0.000 0.000 2.789 0.930 cursors.py:164(execute)
3 0.000 0.000 2.789 0.930 cursors.py:353(_query)
3 0.000 0.000 1.938 0.646 cursors.py:315(_do_query)
3 0.000 0.000 1.920 0.640 cursors.py:142(_do_get_result)
3 0.000 0.000 1.920 0.640 cursors.py:351(_get_result)
3 1.920 0.640 1.920 0.640 method 'store_result' of '_mysql.connection' objects
3 0.001 0.000 0.851 0.284 cursors.py:358(_post_get_result)
3 0.000 0.000 0.849 0.283 cursors.py:324(_fetch_row)
3 0.849 0.283 0.849 0.283 built-in method fetch_row
591285 0.160 0.000 0.160 0.000 range
【问题讨论】:
numpy和multiprocessing都没有问题。 如果您不知道导致问题的原因,请测量。数据库访问需要多长时间? numpy 计算需要多长时间?顺序处理和并行处理在这些时间上有区别吗? 数据库是否在同一台服务器上?如果是这样,那么对数据库进行查询可能会阻塞导致上下文切换的其他进程 感谢您的所有快速cmets!我将尝试解决所有问题:@Smith:感谢您指出 numpy 和多处理之间没有问题。一个不用担心的理由。我确实进行了测量,并将其包含在原始帖子中。 @YnkDK:是的,数据库在同一台服务器上,并行运行的数据获取时间确实比顺序运行的要长,但是时间差异并没有那么大。 [参见“原始帖子中的测量编辑] 你不能向量化那个 for 循环吗?你根本没有使用 numpy 的潜力。 【参考方案1】:您的程序似乎大部分时间都在获取锁。这似乎表明,在您的情况下,多处理弊大于利。
删除所有多处理的东西并开始测量没有它需要多长时间。例如。像这样。
from math import floor
from math import ceil
import numpy
import MySQLdb
import time
start = time.clock()
db = MySQLdb.connect(...)
cursor = db.cursor()
query = "SELECT ...."
cursor.execute(query)
stop = time.clock()
print "Database access took :.2f seconds".format(stop - start)
start = time.clock()
[...] #save db results into the variable db_matrix (30 columns, 5.000 rows)
[...] #save db results into the variable bp_vector (3 columns, 500 rows)
[...] #save db results into the variable option_vector( 3 columns, 4000 rows)
stop = time.clock()
print "Creating matrices took :.2f seconds".format(stop - start)
cursor.close()
db.close()
counter = 0
start = time.clock()
for i in range(4000):
for j in range(500):
helper[:] = (1-bp_vector[j,0]-bp_vector[j,1]-bp_vector[j,2])*db_matrix[:,0]
+ db_matrix[:,option_vector[i,0]] * bp_vector[j,0]
+ db_matrix[:,option_vector[i,1]] * bp_vector[j,1]
+ db_matrix[:,option_vector[i,2]] * bp_vector[j,2]
result[counter,0] = (helper < -7.55).sum()
counter = counter + 1
stop = time.clock()
print "Matrix manipulation took :.2f seconds".format(stop - start)
Edit-1
根据您的测量结果,我坚持我的结论(以稍微改写的形式),在多核机器上,使用multiprocessing
就像您现在所做的那样会极大地损害您的性能。在双核机器上,具有多处理的程序比没有它的程序花费的时间要长得多!
我认为,在使用单核机器时使用多处理与不使用没有区别并不是很重要。无论如何,单核机器不会从多处理中看到那么多好处。
新的测量结果表明,大部分时间都花在了矩阵操作上。这是合乎逻辑的,因为您使用的是显式嵌套 for 循环,速度不是很快。
基本上有四种可能的解决方案;
第一个是将嵌套循环重写为 numpy 操作。 Numpy 操作具有隐式循环(用 C 编写)而不是 Python 中的显式循环,因此速度更快。 (一种罕见的情况,显式比隐式更糟糕。;-))缺点是这可能会占用大量内存。
第二个选项是拆分helper
的计算,它由4 个部分组成。在单独的过程中执行每个部分,并在最后将结果相加。这确实会产生一些开销;每个进程都必须从数据库中检索所有数据,并且必须将部分结果传输回主进程(可能还通过数据库?)。
第三个选项可能是使用pypy
而不是Cpython
。它可以明显更快。
第四个选项是用 Cython 或 C 重写关键矩阵操作。
【讨论】:
感谢您的支持。我在开始帖子中编辑了“回复 Answer-1”部分。不幸的是,测量的时间几乎相同。以上是关于Python 中的多处理:Numpy + 向量求和 -> 大幅减速的主要内容,如果未能解决你的问题,请参考以下文章