Paralelizando um loop for em Python


35

Existem ferramentas no Python que são como o parfor do Matlab? Encontrei este tópico , mas tem quatro anos. Eu pensei que talvez alguém aqui possa ter uma experiência mais recente.

Aqui está um exemplo do tipo de coisa que eu gostaria de paralelizar:

X = np.random.normal(size=(10, 3))
F = np.zeros((10, ))
for i in range(10):
    F[i] = my_function(X[i,:])

onde my_functionpega um ndarraytamanho (1,3)e retorna um escalar.

No mínimo, eu gostaria de usar vários núcleos simultaneamente - como o parfor. Em outras palavras, suponha um sistema de memória compartilhada com 8 a 16 núcleos.



Obrigado, @ doug-lipinski. Esses exemplos, como outros que encontrei no Google, têm uma computação trivial com base no índice de iteração. E eles sempre afirmam que o código é "incrivelmente fácil". Meu exemplo define as matrizes (aloca a memória) fora do loop for. Eu estou bem fazendo isso de outra maneira; é assim que faço no Matlab. A parte complicada que parece reverter esses exemplos é fazer parte de um determinado array para a função dentro do loop.
Paul G. Constantine

Respostas:


19

Joblib faz o que você deseja. O padrão de uso básico é:

from joblib import Parallel, delayed

def myfun(arg):
     do_stuff
     return result

results = Parallel(n_jobs=-1, verbose=verbosity_level, backend="threading")(
             map(delayed(myfun), arg_instances))

onde arg_instancesé a lista de valores para os quais myfuné calculado em paralelo. A principal restrição é que myfundeve ser uma função de nível superior. O backendparâmetro pode ser "threading"ou "multiprocessing".

Você pode passar parâmetros comuns adicionais para a função paralelizada. O corpo de myfuntambém pode se referir a variáveis ​​globais inicializadas, os valores que estarão disponíveis para as crianças.

Args e resultados podem ser praticamente qualquer coisa com o back-end de threading, mas os resultados precisam ser serializados com o back-end de multiprocessamento.


O Dask também oferece funcionalidade semelhante. Pode ser preferível se você estiver trabalhando com dados fora do núcleo ou tentando paralelizar cálculos mais complexos.


Vejo valor zero adicionado ao uso da bateria, incluindo o multiprocessamento. Aposto que o joblib está usando-o sob o capô.
Xavier Combelle 19/05

11
Deve-se mencionar que joblib não é mágico, o threadingback - end sofre com o gargalo do GIL e o multiprocessingback - end gera uma grande sobrecarga devido à serialização de todos os parâmetros e valores de retorno. Veja esta resposta para obter detalhes de baixo nível do processamento paralelo no Python.
Jakub Klinkovský

Não consigo encontrar uma combinação de complexidade de funções e número de iterações para as quais o joblib seria mais rápido que um loop for. Para mim, tem a mesma velocidade se n_jobs = 1 e é muito mais lenta em todos os outros casos
Aleksejs Fomins 07/01

@AleksejsFomins O paralelismo baseado em thread não ajudará no código que não libera o GIL, mas em um número significativo, principalmente nas bibliotecas de ciência de dados ou numéricas. Caso contrário, você precisará de mutiprocessamento, Jobli suporta ambos. O módulo de multiprocessamento agora também tem paralelos mapque você pode usar diretamente. Além disso, se você usar o numk compilado mkl, ele paralelizará automaticamente as operações vetorizadas sem que você faça nada. O numpy em Ananconda é mkl ativado por padrão. Não existe uma solução universal. Joblib é muito barulhento e houve menos opções em 2015.
Daniel Mahler

Obrigado pelo teu conselho. Lembro-me de tentar o multiprocessamento antes e até escrever algumas postagens, porque não foi dimensionado como eu esperava. Talvez eu deva dar outra olhada
Aleksejs Fomins 12/01

9

O que você está procurando é o Numba , que pode paralelizar automaticamente um loop for. Da documentação deles

from numba import jit, prange

@jit
def parallel_sum(A):
    sum = 0.0
    for i in prange(A.shape[0]):
        sum += A[i]

    return sum

8

Sem assumir que algo especial na my_functionescolha multiprocessing.Pool().map()é um bom palpite para paralelizar loops tão simples. joblib, dask, mpiCálculos ou numbacomo proposto em outras respostas não parece trazer qualquer vantagem para tais casos de uso e adicionar dependências inúteis (para resumir eles são um exagero). É improvável que usar o encadeamento proposto em outra resposta seja uma boa solução, porque você precisa estar íntimo da interação GIL do seu código ou seu código deve fazer principalmente entrada / saída.

Dito isso, numbapode ser uma boa idéia acelerar o código python puro seqüencial, mas acho que isso está fora do escopo da questão.

import multiprocessing
import numpy as np

if __name__ == "__main__":
   #the previous line is necessary under windows to not execute 
   # main module on each child under windows

   X = np.random.normal(size=(10, 3))
   F = np.zeros((10, ))

   pool = multiprocessing.Pool(processes=16)
   # if number of processes is not specified, it uses the number of core
   F[:] = pool.map(my_function, (X[i,:] for i in range(10)) )

No entanto, existem algumas ressalvas (mas que não devem afetar a maioria das aplicações):

  • no Windows, não há suporte para bifurcação; portanto, um intérprete com o módulo principal é iniciado na inicialização de cada filho, para que ele possa ter uma sobrecarga (anúncio é o motivo do if __name__ == "__main__"
  • Os argumentos e os resultados da função my_ são selecionados e não selecionados; pode ser uma sobrecarga muito grande; consulte esta resposta para reduzi-lo https://stackoverflow.com/a/37072511/128629 . Também inutiliza objetos não selecionáveis
  • my_functionnão deve depender de estados compartilhados, como a comunicação com variáveis ​​globais, porque os estados não são compartilhados entre processos. funções puras (funções nos sentidos matemáticos) são exemplos de funções que não compartilham estados

6

Minha impressão do parfor é que o MATLAB está encapsulando detalhes da implementação; portanto, ele pode estar usando paralelismo de memória compartilhada (que é o que você deseja) e paralelismo de memória distribuída (se você estiver executando um servidor de computação distribuída MATLAB ).

Se você deseja paralelismo de memória compartilhada e está executando algum tipo de loop paralelo de tarefa, o pacote de biblioteca padrão de multiprocessamento é provavelmente o que você deseja, talvez com um bom front-end, como joblib , como mencionado na publicação de Doug. A biblioteca padrão não vai desaparecer e é mantida, por isso é de baixo risco.

Existem outras opções também, como o Parallel Python e os recursos paralelos do IPython . Um rápido vislumbre do Parallel Python me faz pensar que está mais próximo do espírito do parfor, pois a biblioteca contém detalhes para o caso distribuído, mas o custo de fazer isso é que você deve adotar o ecossistema deles. O custo do uso do IPython é semelhante; você precisa adotar a maneira IPython de fazer as coisas, que podem ou não valer a pena para você.

Se você se preocupa com a memória distribuída, recomendo o mpi4py . Lisandro Dalcin faz um ótimo trabalho, e o mpi4py é usado nos wrappers PETSc Python, então eu não acho que isso vai desaparecer tão cedo. Assim como o multiprocessamento, é uma interface de baixo nível (er) para paralelismo do que o parfor, mas provavelmente durará um tempo.


Obrigado, @Geoff. Você tem alguma experiência trabalhando com essas bibliotecas? Talvez eu tente usar o mpi4py em uma máquina de memória compartilhada / processador multicore.
Paul G. Constantine

@PaulGConstantine Usei o mpi4py com êxito; é bem indolor, se você conhece o MPI. Não usei o multiprocessamento, mas o recomendei aos colegas, que disseram que funcionava bem para eles. Também usei o IPython, mas não os recursos de paralelismo, por isso não posso falar sobre como ele funciona.
Geoff Oxberry

11
Aron tem um bom mpi4py tutorial ele preparado para o PyHPC curso em Supercomputação: github.com/pyHPC/pyhpc-tutorial
Matt Knepley

4

Antes de procurar uma ferramenta "caixa preta", que pode ser usada para executar funções python "genéricas" paralelas, sugiro que analise como my_function()pode ser paralelizado manualmente.

Primeiro, compare o tempo de execução my_function(v)com o foroverhead do loop python : [C] Os forloops do Python são muito lentos, portanto o tempo gasto my_function()pode ser insignificante.

>>> timeit.timeit('pass', number=1000000)
0.01692986488342285
>>> timeit.timeit('for i in range(10): pass', number=1000000)
0.47521495819091797
>>> timeit.timeit('for i in xrange(10): pass', number=1000000)
0.42337894439697266

Segunda verificação, se houver uma implementação de vetor simples my_function(v)que não exija loops:F[:] = my_vector_function(X)

(Esses dois primeiros pontos são bastante triviais, perdoe-me se eu os mencionei aqui apenas por completude.)

O terceiro e mais importante ponto, pelo menos para as implementações de CPython, é verificar se my_functionpassa a maior parte do tempo dentro ou fora do bloqueio global de intérpretes , ou GIL . Se o tempo for gasto fora do GIL, o threadingmódulo de biblioteca padrão deve ser usado. ( Aqui está um exemplo). BTW, pode-se pensar em escrevermy_function() como uma extensão C apenas para liberar o GIL.

Finalmente, se my_function()não liberar o GIL, pode-se usar o multiprocessingmódulo .

Referências: documentos em Python sobre execução simultânea e introdução numpy / scipy no processamento paralelo .


2

Você pode tentar Julia. É bem parecido com o Python e possui muitas construções do MATLAB. A tradução aqui é:

F = @parallel (vcat) for i in 1:10
    my_function(randn(3))
end

Isso também torna os números aleatórios paralelamente e apenas concatena os resultados no final durante a redução. Isso usa multiprocessamento (então você precisa addprocs(N)adicionar processos antes de usar, e isso também funciona em vários nós em um HPC, como mostrado nesta postagem do blog ).

Você também pode usar pmap:

F = pmap((i)->my_function(randn(3)),1:10)

Se você quiser paralelismo de encadeamento, poderá usá-lo Threads.@threads(embora certifique-se de tornar o algoritmo seguro para encadeamento). Antes de abrir Julia, defina a variável de ambiente JULIA_NUM_THREADS, então é:

Ftmp = [Float64[] for i in Threads.nthreads()]
Threads.@threads for i in 1:10
    push!(Ftmp[Threads.threadid()],my_function(randn(3)))
end
F = vcat(Ftmp...)

Aqui, faço uma matriz separada para cada thread, para que não entrem em conflito ao serem adicionadas à matriz e concatenem as matrizes posteriormente. O encadeamento é bem novo, então agora existe apenas o uso direto de encadeamentos, mas tenho certeza de que reduções e mapas de encadeamentos serão adicionados exatamente como no multiprocessamento.


Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.