Bug estranho em Pandas e Numpy em relação a multithreading

25

A maioria das funções do Numpy habilitará multithreading por padrão.

por exemplo, eu trabalho em uma estação de trabalho intel cpu de 8 núcleos, se eu executar um script

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

o linux topmostrará 800% de uso da CPU durante a execução, como o insira a descrição da imagem aqui que significa que o numpy detecta automaticamente que minha estação de trabalho possui 8 núcleos e np.sqrtusa automaticamente todos os 8 núcleos para acelerar a computação.

No entanto, eu encontrei um bug estranho. Se eu executar um 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)

o uso da CPU é 100% !!. Isso significa que, se você adicionar dois DataFrame de dois pandas antes de executar qualquer função numpy, o recurso de multithreading automático de numpy desaparecerá sem nenhum aviso! Isso não é absolutamente razoável, por que o cálculo do DataFrame do Pandas afetaria a configuração de segmentação do Numpy? Isso é um bug? Como contornar isso?insira a descrição da imagem aqui


PS:

Eu cavo ainda mais usando a perfferramenta Linux .

executando o primeiro script mostra

insira a descrição da imagem aqui

Ao executar o segundo script, mostra

insira a descrição da imagem aqui

Portanto, ambos os scripts envolvem libmkl_vml_avx2.so, enquanto o primeiro script envolve outros libiomp5.soque parecem estar relacionados ao openMP.

E como vml significa intel math math library, de acordo com o vml doc, acho que pelo menos as funções abaixo são automaticamente multithread

insira a descrição da imagem aqui

user15964
fonte
Não sei se entendi sua pergunta. Você pode elaborar?
AM
@AMC eu atualizei meu post, espero que agora está claro
user15964
Eu acho que são necessárias mais informações como np, pandas, versão, CPU, tipo OS ... Não consigo reproduzir na minha máquina. Ele não utiliza várias CPUs nos dois códigos.
hunzter
OK, aqui estão as informações: Ubuntu 16.04.5 LTS numpy 1.17.2 py37haad9e8e_0 pandas 0.25.1 py37he6710b0_0 CPU Intel (R) Xeon (E5) E5-1680 v4 a 3.40GHz. PS. Eu uso anaconda
user15964
11
Poderia, por favor verifique o seguinte: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

Respostas:

13

O Pandas usa numexprsob o capô para calcular algumas operações e numexprdefine o número máximo de threads para vml como 1, quando é importado :

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

e é importado pelo pandas quando df+dfé avaliado em expression.py :

from pandas.core.computation.check import _NUMEXPR_INSTALLED

if _NUMEXPR_INSTALLED:
   import numexpr as ne

No entanto, a distribuição Anaconda também usa VML-funcionalidade para funções tais como sqrt, sin, cose assim por diante - e uma vez numexprdefinido o número máximo de VML-fios para um, as numpy-funções não utilização paralelização.

O problema pode ser facilmente visto no gdb (usando seu script lento):

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

ou seja, podemos ver, numexprdefine o número de threads como 1. Que é usado mais tarde quando a função vml-sqrt é chamada:

(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

Então, podemos ver que numpy usa a implementação de vml vdSqrtutilizada mkl_vml_serv_threader_d_1i_1opara decidir se o cálculo deve ser feito em paralelo e parece o número 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

o registro %raxtem o número máximo de threads e é 1.

Agora podemos usar numexprpara aumentar o número de threads de vml , ou seja:

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

Agora vários núcleos são utilizados!

ead
fonte
Muito obrigado! Finalmente, uma ótima resposta explica tudo. No final, é o numexprpor trás da cena.
user15964 6/01
Concordou .. boa escavação! Próxima pergunta, no entanto .. por que o número de push numexpr conta até 1? Possivelmente devido a problemas de instabilidade / segurança de thread? Em vez de aumentar a contagem para 8, pode ser mais seguro passar para uma versão estável / segura do thread do NumPy. Talvez também seja bom verificar essa variável com o NumPy mais recente e melhor, caso isso não seja mais necessário, portanto tecnicamente um bug.
Andrew Atrens 13/01
@AndrewAtrens, você pode dar uma olhada em github.com/pydata/numexpr/issues/39 e github.com/pydata/numexpr/issues/355
ead
2

Olhando para entorpecido, parece que, sob o capô, ele teve problemas de ativação / desativação com o multithreading e, dependendo da versão que você está usando, você pode começar a ver falhas ao aumentar ne.set_vml_num_threads () ..

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

Eu preciso entender como isso é colado no interpretador python, dado o seu exemplo de código em que parece estar de alguma forma permitindo que várias chamadas aparentemente síncronas / ordenadas ao np.sqrt () prossigam paralelamente. Eu acho que se o intérprete python estiver sempre retornando uma referência a um objeto quando ele aparecer na pilha, e no seu exemplo estiver apenas lançando essas referências e não atribuí-las ou manipulá-las da maneira que seria adequada. Mas se as iterações de loop subsequentes dependem das anteriores, parece menos claro como elas podem ser paralelizadas com segurança. Indiscutivelmente falha silenciosa / resultados errados é um resultado pior do que falhas.

Andrew Atrens
fonte
Olá, Andrew Atrens, você está quase lá. É o problema de ne.set_vml_num_threads (). Muito obrigado pelo seu tempo dedicado ao meu problema.
user15964 11/01
Happy Trails :)
Andrew Atrens
0

Eu acho que sua premissa inicial pode estar incorreta -

Você declarou: O que significa que o numpy detecta automaticamente que minha estação de trabalho possui 8 núcleos e o np.sqrt usa automaticamente todos os 8 núcleos para acelerar a computação.

Uma única função np.sqrt () não consegue adivinhar como será chamada ou retornada a seguir antes de ser parcialmente concluída. Existem mecanismos de paralelismo em python, mas nenhum é automático.

Agora, tendo dito isso, o intérprete python pode otimizar o loop for para paralelismo, que pode ser o que você está vendo, mas eu suspeito fortemente que se você olhar para a hora do relógio de parede para esse loop executá-lo, não haverá diferente, independentemente de você estar (aparentemente) usando 8 núcleos ou 1 núcleo.

ATUALIZAÇÃO: Depois de ler um pouco mais dos comentários, parece que o comportamento multinúcleo que você está vendo está relacionado à distribuição anaconda do interpretador python. Dei uma olhada, mas não consegui encontrar nenhum código-fonte, mas parece que a licença python permite que entidades (como anaconda.com) compilem e distribuam derivados do intérprete sem exigir que suas alterações sejam publicadas.

Eu acho que você pode falar com o pessoal da anaconda - o comportamento que você está vendo será difícil de entender sem saber o que / se alguma coisa mudou no intérprete ..

Faça também uma verificação rápida do relógio do relógio de parede com / sem a otimização para ver se é realmente 8x mais rápido - mesmo se você realmente tiver todos os 8 núcleos trabalhando em vez de 1, seria bom saber se os resultados são realmente 8x mais rápido ou se houver spinlocks em uso que ainda estejam serializando em um único mutex.

Andrew Atrens
fonte
11
Olá, Andrew Atrens. Mas a paralelização não é feita pelo python, é feita pelo back-end do anaconda numpy, que é intel MKL. E sim, eu abri um problema no numpy, eles me sugeriram abrir um problema no anaconda, e eu o fiz. No entanto, ainda não recebi uma única resposta da anaconda por uma semana. Então talvez eles tenham ignorado meu relatório ...
user15964
É um problema com a versão / liberação anaconda do interpretador python - sua versão utiliza openmp, enquanto a versão padrão do python não.
Andrew Atrens 5/01
É um problema com a versão / liberação anaconda do interpretador python - a versão deles vincula / utiliza APIs openmp, enquanto o interpretador padrão de release python não. Quando digo utilizo, quero dizer literalmente chamar as funções openmp api 'under the hood'. Como em qualquer otimização implícita em que não podemos ver o código-fonte, podemos apenas relatá-lo (como você tem) e, se possível, tentar contorná-lo.
Andrew Atrens 5/01
Outra idéia: você pode codificar novamente seu aplicativo para usar explicitamente bibliotecas multithreading python e não confiar no otimizador do intérprete para fazer isso por você. Estou pensando em pools de encadeamentos, dependendo da complexidade do seu aplicativo, e se esta não é sua primeira vez em programação encadeada, isso pode não ser muito difícil. Para manter a portabilidade, o uso provavelmente deve tentar evitar algo específico para anaconda ou openmp - deixarei isso para você, pois não tenho tempo de qualquer maneira ... :) De qualquer forma, boa sorte e espero que isso ajude a desfazer o que você está vendo. :) :)
Andrew Atrens 6/01