Pandas 和 Numpy 中关于多线程的奇怪错误

use*_*964 26 python numpy pandas

Numpy 的大部分功能都会默认启用多线程。

例如,我在 8 核英特尔 CPU 工作站上工作,如果我运行脚本

import numpy as np    
x=np.random.random(1000000)
for i in range(100000):
    np.sqrt(x)
Run Code Online (Sandbox Code Playgroud)

linuxtop在运行过程中将显示 800% 的 CPU 使用率,例如 在此处输入图片说明 这意味着 numpy 会自动检测我的工作站有 8 个内核,并np.sqrt自动使用所有 8 个内核来加速计算。

但是,我发现了一个奇怪的错误。如果我运行一个脚本

import numpy as np
import pandas as pd
df=pd.DataFrame(np.random.random((10,10)))
df+df
x=np.random.random(1000000)
for i in range(100000):
    np.sqrt(x)
Run Code Online (Sandbox Code Playgroud)

cpu使用率是100%!! 在此处输入图片说明 这意味着如果你在运行任何 numpy 函数之前加上两个 Pandas DataFrame,那么 numpy 的自动多线程功能就会毫无预警地消失!这绝对不合理,为什么Pandas dataFrame 计算会影响Numpy 线程设置?这是一个错误吗?如何解决这个问题?


PS:

我使用 Linuxperf工具进一步挖掘。

运行第一个脚本显示

在此处输入图片说明

运行第二个脚本时显示

在此处输入图片说明

因此,两个脚本都涉及libmkl_vml_avx2.so,而第一个脚本涉及libiomp5.so似乎与 openMP 有关的附加脚本。

由于 vml 意味着英特尔矢量数学库,所以根据 vml doc 我猜至少下面的函数都是自动多线程的

在此处输入图片说明

ead*_*ead 15

Pandas 使用底层numexpr计算一些操作,并numexpr导入vml 的最大线程数设置为 1 :

# The default for VML is 1 thread (see #39)
set_vml_num_threads(1)
Run Code Online (Sandbox Code Playgroud)

df+dfexpression.py 中求值时,它会被 pandas 导入:

from pandas.core.computation.check import _NUMEXPR_INSTALLED

if _NUMEXPR_INSTALLED:
   import numexpr as ne
Run Code Online (Sandbox Code Playgroud)

然而,水蟒分布也使用VML的功能用于这样的功能如sqrtsincos等等-并且一旦numexpr设置VML线程的最大数目为1时,numpy的函数不再使用并行化。

这个问题很容易在 gdb 中看到(使用你的慢脚本):

>>> gdb --args python slow.py
(gdb) b mkl_serv_domain_set_num_threads
function "mkl_serv_domain_set_num_threads" not defined.
Make breakpoint pending on future shared library load? (y or [n]) y
Breakpoint 1 (mkl_serv_domain_set_num_threads) pending.
(gbd) run
Thread 1 "python" hit Breakpoint 1, 0x00007fffee65cd70 in mkl_serv_domain_set_num_threads () from /home/ed/anaconda37/lib/python3.7/site-packages/numpy/../../../libmkl_intel_thread.so
(gdb) bt 
#0  0x00007fffee65cd70 in mkl_serv_domain_set_num_threads () from /home/ed/anaconda37/lib/python3.7/site-packages/numpy/../../../libmkl_intel_thread.so
#1  0x00007fffe978026c in _set_vml_num_threads(_object*, _object*) () from /home/ed/anaconda37/lib/python3.7/site-packages/numexpr/interpreter.cpython-37m-x86_64-linux-gnu.so
#2  0x00005555556cd660 in _PyMethodDef_RawFastCallKeywords () at /tmp/build/80754af9/python_1553721932202/work/Objects/call.c:694
...
(gdb) print $rdi
$1 = 1
Run Code Online (Sandbox Code Playgroud)

即我们可以看到,numexpr将线程数设置为 1。稍后在调用 vml-sqrt 函数时使用:

(gbd) b mkl_serv_domain_get_max_threads
Breakpoint 2 at 0x7fffee65a900
(gdb) (gdb) c
Continuing.

Thread 1 "python" hit Breakpoint 2, 0x00007fffee65a900 in mkl_serv_domain_get_max_threads () from /home/ed/anaconda37/lib/python3.7/site-packages/numpy/../../../libmkl_intel_thread.so
(gdb) bt
#0  0x00007fffee65a900 in mkl_serv_domain_get_max_threads () from /home/ed/anaconda37/lib/python3.7/site-packages/numpy/../../../libmkl_intel_thread.so
#1  0x00007ffff01fcea9 in mkl_vml_serv_threader_d_1i_1o () from /home/ed/anaconda37/lib/python3.7/site-packages/numpy/../../../libmkl_intel_thread.so
#2  0x00007fffedf78563 in vdSqrt () from /home/ed/anaconda37/lib/python3.7/site-packages/numpy/../../../libmkl_intel_lp64.so
#3  0x00007ffff5ac04ac in trivial_two_operand_loop () from /home/ed/anaconda37/lib/python3.7/site-packages/numpy/core/_multiarray_umath.cpython-37m-x86_64-linux-gnu.so
Run Code Online (Sandbox Code Playgroud)

所以我们可以看到numpy的用途VML的实施vdSqrt,其利用mkl_vml_serv_threader_d_1i_1o,以决定是否计算应并行完成,它看起来线程数:

(gdb) fin
Run till exit from #0  0x00007fffee65a900 in mkl_serv_domain_get_max_threads () from /home/ed/anaconda37/lib/python3.7/site-packages/numpy/../../../libmkl_intel_thread.so
0x00007ffff01fcea9 in mkl_vml_serv_threader_d_1i_1o () from /home/ed/anaconda37/lib/python3.7/site-packages/numpy/../../../libmkl_intel_thread.so
(gdb) print $rax
$2 = 1
Run Code Online (Sandbox Code Playgroud)

该寄存器%rax具有最大线程数,并且为 1。

现在我们可以使用numexpr增加 vml-threads 的数量,即:

import numpy as np
import numexpr as ne
import pandas as pd
df=pd.DataFrame(np.random.random((10,10)))
df+df

#HERE: reset number of vml-threads
ne.set_vml_num_threads(8)

x=np.random.random(1000000)
for i in range(10000):
    np.sqrt(x)     # now in parallel
Run Code Online (Sandbox Code Playgroud)

现在使用了多个内核!