Bug étrange dans Pandas et Numpy concernant le multithreading

25

La plupart des fonctions de Numpy activent le multithreading par défaut.

par exemple, je travaille sur un poste de travail cpu Intel à 8 cœurs, si je lance un script

import numpy as np    
x=np.random.random(1000000)
for i in range(100000):
    np.sqrt(x)

Linux topaffichera une utilisation de 800% du processeur pendant l'exécution, entrez la description de l'image ici ce qui signifie que numpy détecte automatiquement que mon poste de travail possède 8 cœurs et np.sqrtutilise automatiquement les 8 cœurs pour accélérer le calcul.

Cependant, j'ai trouvé un bug bizarre. Si je lance un script

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)

l'utilisation du processeur est de 100% !!. Cela signifie que si vous plus deux pandas DataFrame avant d'exécuter une fonction numpy, la fonction de multithreading automatique de numpy a disparu sans aucun avertissement! Ce n'est absolument pas raisonnable, pourquoi le calcul de Pandas dataFrame affecterait-il le paramètre de filetage Numpy? Est-ce un bug? Comment contourner cela?entrez la description de l'image ici


PS:

Je creuse plus loin en utilisant l' perfoutil Linux .

exécution des premières émissions de script

entrez la description de l'image ici

Lors de l'exécution du deuxième script

entrez la description de l'image ici

Ainsi, les deux scripts impliquent libmkl_vml_avx2.so, tandis que le premier script implique des éléments supplémentaires libiomp5.soqui semblent être liés à openMP.

Et puisque vml signifie bibliothèque de mathématiques vectorielles Intel, donc selon le doc vml, je suppose qu'au moins les fonctions ci-dessous sont toutes automatiquement multithread

entrez la description de l'image ici

user15964
la source
Je ne suis pas sûr de comprendre votre question. Peux-tu élaborer?
AMC
@AMC J'ai mis à jour mon message, j'espère qu'il est maintenant clair
user15964
Je pense que plus d'informations sont nécessaires comme np, pandas, version, CPU, type d'OS ... Je ne peux pas reproduire sur ma machine. Il n'utilise pas plusieurs processeurs dans les deux codes.
hunzter
@hunzter OK, voici les informations: Ubuntu 16.04.5 LTS numpy 1.17.2 py37haad9e8e_0 pandas 0.25.1 py37he6710b0_0 Intel (R) Xeon (R) CPU E5-1680 v4 @ 3.40GHz. PS. J'utilise anaconda
user15964
1
Pourriez-vous s'il vous plaît vérifier ceci:import numpy as np import pandas as pd import os os.environ["MKL_NUM_THREADS"] = '4' print(os.environ["MKL_NUM_THREADS"]) df=pd.DataFrame(np.random.random((10,10))) df+df print(os.environ["MKL_NUM_THREADS"]) a = np.random.random((20000000, 3)) b = np.random.random((3, 30)) for _ in range(10): c = np.dot(a, b)
Stas Buzuluk

Réponses:

13

Pandas utilise numexprsous le capot pour calculer certaines opérations et numexprdéfinit le nombre maximal de threads pour vml à 1, lors de son importation :

# The default for VML is 1 thread (see #39)
set_vml_num_threads(1)

et il est importé par les pandas lorsqu'il df+dfest évalué dans expressions.py :

from pandas.core.computation.check import _NUMEXPR_INSTALLED

if _NUMEXPR_INSTALLED:
   import numexpr as ne

Cependant, la distribution Anaconda utilise également VML fonctionnalité pour des fonctions telles que sqrt, sin, coset ainsi de suite - et une fois numexprfixé le nombre de maximal de fils de VML à 1, les fonctions de numpy-plus parallélisation d'utilisation.

Le problème peut être facilement vu dans gdb (en utilisant votre script lent):

>>> 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

c'est-à-dire que nous pouvons voir, numexprdéfinit le nombre de threads à 1. Qui est ensuite utilisé lorsque la fonction vml-sqrt est appelée:

(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

Nous pouvons donc voir que numpy utilise l'implémentation de vml vdSqrtqui permet mkl_vml_serv_threader_d_1i_1ode décider si le calcul doit être effectué en parallèle et il regarde le nombre de threads:

(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

le registre %raxa le nombre maximal de threads et il est de 1.

Maintenant, nous pouvons utiliser numexprpour augmenter le nombre de threads vml , c'est -à- dire:

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

Maintenant, plusieurs cœurs sont utilisés!

ead
la source
Merci beaucoup! Enfin, une excellente réponse explique tout. Au final, c'est l' numexprarrière-scène.
user15964
D'accord .. bon creusage! Question suivante cependant .. pourquoi numexpr pousse-t-il le nombre de threads à 1? Peut-être en raison de problèmes d'instabilité / thread-safe? Au lieu de repousser le nombre à 8, il peut être plus sûr de passer à une version stable et sans fil de NumPy. Peut-être aussi bon de vérifier cette variable avec le dernier et le plus grand NumPy au cas où cela ne serait plus nécessaire, d'où techniquement un bug.
Andrew Atrens
Vous pouvez @AndrewAtrens jeter un oeil à github.com/pydata/numexpr/issues/39 et github.com/pydata/numexpr/issues/355
ead
2

En regardant numpy, il semble que sous le capot, il y ait eu des problèmes d'activation / désactivation avec le multithreading, et selon la version que vous utilisez, vous pouvez vous attendre à voir des plantages lorsque vous augmentez ne.set_vml_num_threads () ..

http://numpy-discussion.10968.n7.nabble.com/ANN-NumExpr-2-7-0-Release-td47414.html

J'ai besoin de comprendre comment cela est collé à l'interpréteur python, compte tenu de votre exemple de code où il semble permettre en quelque sorte à plusieurs appels apparemment synchrones / ordonnés à np.sqrt () de procéder en parallèle. Je suppose que l'interpréteur python retourne toujours juste une référence à un objet quand il fait apparaître la pile, et dans votre exemple, il suffit de lancer ces références et de ne pas les assigner ou les manipuler de quelque manière que ce soit. Mais si les itérations de boucle suivantes dépendent des précédentes, il semble alors moins clair comment elles pourraient être parallélisées en toute sécurité. Un échec / des résultats incorrects est sans doute pire que les plantages.

Andrew Atrens
la source
Salut, Andrew Atrens, vous y êtes presque. C'est le problème de ne.set_vml_num_threads (). Merci beaucoup pour le temps que vous avez consacré à mon problème.
user15964
Happy Trails :)
Andrew Atrens
0

Je pense que votre prémisse initiale peut être incorrecte -

Vous avez déclaré: ce qui signifie que numpy détecte automatiquement que mon poste de travail possède 8 cœurs et que np.sqrt utilise automatiquement les 8 cœurs pour accélérer le calcul.

Une seule fonction np.sqrt () ne peut pas deviner comment elle sera ensuite invoquée ou retourner avant qu'elle ne soit partiellement terminée. Il existe des mécanismes de parallélisme en python, mais aucun n'est automatique.

Maintenant, cela dit, l'interpréteur python peut être en mesure d'optimiser la boucle for pour le parallélisme, ce qui peut être ce que vous voyez, mais je soupçonne fortement que si vous regardez l'heure de l'horloge murale pour que cette boucle s'exécute, ce ne sera pas différent, peu importe si vous utilisez (apparemment) 8 cœurs ou 1 cœur.

MISE À JOUR: Après avoir lu un peu plus de commentaires, il semble que le comportement multicœur que vous voyez est lié à la distribution anaconda de l'interpréteur python. J'ai jeté un coup d'œil mais je n'ai pas pu trouver de code source pour cela, mais il semble que la licence python permette aux entités (comme anaconda.com) de compiler et de distribuer des dérivés de l'interpréteur sans exiger la publication de leurs modifications.

Je suppose que vous pouvez tendre la main aux gens d'anaconda - le comportement que vous voyez sera difficile à comprendre sans savoir quoi / si quelque chose a changé dans l'interpréteur.

Faites également une vérification rapide de l'heure de l'horloge murale avec / sans l'optimisation pour voir si elle est en effet 8 fois plus rapide - même si vous avez vraiment les 8 cœurs qui fonctionnent au lieu de 1, il serait bon de savoir si les résultats sont réellement 8 fois plus rapide ou s'il y a des verrous tournants en cours d'utilisation qui sérialisent toujours sur un seul mutex.

Andrew Atrens
la source
1
Salut, Andrew Atrens. Mais la parallélisation n'est pas effectuée par python, elle est effectuée par le backend d'anaconda numpy qui est Intel MKL. Et oui, j'ai ouvert un problème sur numpy, ils m'ont suggéré d'ouvrir un problème sur anaconda, et je l'ai fait. Cependant, je n'ai pas encore reçu une seule réponse d'anaconda pendant une semaine. Alors peut-être qu'ils ont juste ignoré mon rapport ...
user15964
C'est un problème avec la version / version anaconda de l'interpréteur python - leur version utilise openmp alors que la version standard de python ne le fait pas.
Andrew Atrens
C'est un problème avec la version / version anaconda de l'interpréteur python - leur version établit un lien vers / utilise openmp apis, contrairement à l'interpréteur de version python standard. quand je dis utilise, je veux dire littéralement appeler les fonctions api openmp «sous le capot». Comme pour toute optimisation implicite où nous ne pouvons pas voir le code source, nous ne pouvons que le signaler (comme vous l'avez) et si possible essayer de le contourner.
Andrew Atrens
Une autre pensée à ce sujet .. vous pouvez recoder votre application pour utiliser explicitement les bibliothèques de multithreading python et ne pas compter sur l'optimiseur de l'interpréteur pour le faire pour vous .. Je pense aux pools de threads .. selon la complexité de votre application, et si ce n'est pas votre premier essai de programmation filetée, cela ne sera peut-être pas trop difficile. Pour maintenir la portabilité, essayez probablement d'éviter tout ce qui est spécifique à anaconda ou openmp - je vous laisse cela car je n'ai pas le temps pour y creuser ... :) Quoi qu'il en soit, bonne chance et j'espère que cela aidera à désembuer ce que vous voyez. :) :)
Andrew Atrens