Parallelizzare un for-loop in Python


35

Ci sono strumenti in Python che sono simili al parfor di Matlab? Ho trovato questa discussione , ma ha quattro anni. Pensavo che qualcuno qui potesse avere un'esperienza più recente.

Ecco un esempio del tipo di cosa che vorrei parallelizzare:

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

dove my_functionprende una ndarraydimensione (1,3)e restituisce uno scalare.

Almeno, mi piacerebbe usare più core contemporaneamente --- come parfor. In altre parole, assumere un sistema di memoria condivisa con core da 8 a 16.



Grazie, @ Doug-Lipinski. Questi esempi, come altri che ho trovato mentre cercavo su google, hanno un calcolo banale basato sull'indice di iterazione. E sostengono sempre che il codice è "incredibilmente facile". Il mio esempio definisce gli array (alloca la memoria) al di fuori del for-loop. Va bene farlo in un altro modo; è così che lo faccio in Matlab. La parte difficile che sembra invertire questi esempi è ottenere parte di un determinato array nella funzione all'interno del loop.
Paul G. Constantine,

Risposte:


19

Joblib fa quello che vuoi. Il modello di utilizzo di base è:

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

dove arg_instancesè un elenco di valori per i quali myfunviene calcolato in parallelo. La restrizione principale è che myfundeve essere una funzione di livello superiore. Il backendparametro può essere "threading"o "multiprocessing".

È possibile passare ulteriori parametri comuni alla funzione parallelizzata. Il corpo di myfunpuò anche riferirsi a variabili globali inizializzate, i valori che saranno disponibili per i bambini.

Argomenti e risultati possono essere praticamente qualsiasi cosa con il back-end di threading ma i risultati devono essere serializzabili con il back-end multiprocessore.


Dask offre anche funzionalità simili. Potrebbe essere preferibile se stai lavorando con dati non core o stai cercando di parallelizzare calcoli più complessi.


Vedo il valore zero aggiunto per utilizzare la batteria incluso il multiprocessing. Scommetto che Joblib lo sta usando sotto il cofano.
Xavier Combelle,

1
Va detto che il joblib non è magico, il threadingbackend soffre del collo di bottiglia di GIL e il multiprocessingbackend porta grandi spese generali a causa della serializzazione di tutti i parametri e dei valori di ritorno. Vedi questa risposta per i dettagli di basso livello dell'elaborazione parallela in Python.
Jakub Klinkovský,

Non riesco a trovare una combinazione di complessità delle funzioni e numero di iterazioni per le quali il joblib sarebbe più veloce di un for-loop. Per me, ha la stessa velocità se n_jobs = 1, ed è molto più lento in tutti gli altri casi
Aleksejs Fomins

@AleksejsFomins Il parallelismo basato su thread non aiuterà per il codice che non rilascia il GIL, ma un numero significativo lo fa, in particolare la scienza dei dati o le librerie numeriche. Altrimenti hai bisogno del mutiprocessing, Jobli supporta entrambi. Il modulo multiprocessore ora ha anche un parallelo mapche puoi usare direttamente. Inoltre, se usi numkl compilato da mkl, le operazioni vettorializzate verranno parallelizzate automaticamente senza che tu faccia nulla. Il numpy in Ananconda è abilitato per impostazione predefinita a mkl. Non esiste tuttavia una soluzione universale. Joblib è molto agitato e c'erano meno otioni nel 2015.
Daniel Mahler,

Grazie per il tuo consiglio. Ricordo di aver provato il multiprocessing prima e persino di aver scritto alcuni post, perché non si ridimensionava come mi aspettavo. Forse dovrei dare un'altra occhiata
Aleksejs Fomins il

9

Quello che stai cercando è Numba , che può parallelizzare automaticamente un ciclo for. Dalla loro documentazione

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

Senza presumere che qualcosa di speciale nella my_functionscelta multiprocessing.Pool().map()sia una buona ipotesi per parallelizzare cicli così semplici. joblib, dask, mpiCalcoli o numbacome proposto in altre risposte, non sembra destinata portare alcun vantaggio per questi casi d'uso e aggiungere le dipendenze inutili (per riassumere sono eccessivo). È improbabile che utilizzare il threading come proposto in un'altra risposta sia una buona soluzione, perché devi essere intimo con l'interazione GIL del tuo codice o il tuo codice dovrebbe fare principalmente input / output.

Detto questo, numbapotrebbe essere una buona idea accelerare il codice sequenziale in puro pitone, ma ritengo che questo esuli dall'ambito della domanda.

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

Esistono tuttavia alcuni avvertimenti (ma che non dovrebbero influire sulla maggior parte delle applicazioni):

  • sotto windows non c'è il supporto fork, quindi un interprete con il modulo principale viene lanciato all'avvio di ogni figlio, quindi potrebbe avere un overhead (annuncio è il motivo per il if __name__ == "__main__"
  • Gli argomenti e i risultati di my_function sono decapati e non elaborati, potrebbe essere un sovraccarico troppo grande, vedi questa risposta per ridurlo https://stackoverflow.com/a/37072511/128629 . Inoltre, rende inutilizzabili oggetti non selezionabili
  • my_functionnon dovrebbe dipendere da stati condivisi come la comunicazione con variabili globali perché gli stati non sono condivisi tra i processi. le funzioni pure (funzioni nei sensi matematici) sono esempi di funzioni che non condividono gli stati

6

La mia impressione di parfor è che MATLAB sta incapsulando i dettagli dell'implementazione, quindi potrebbe utilizzare sia il parallelismo della memoria condivisa (che è ciò che si desidera) sia il parallelismo della memoria distribuita (se si esegue un server di elaborazione distribuito MATLAB ).

Se vuoi il parallelismo della memoria condivisa e stai eseguendo una sorta di task parallel loop, il pacchetto di librerie standard multiprocessore è probabilmente quello che vuoi, forse con un bel front-end, come il joblib , come menzionato nel post di Doug. La libreria standard non scomparirà ed è mantenuta, quindi è a basso rischio.

Esistono anche altre opzioni, come Parallel Python e le funzionalità parallele di IPython . Una rapida occhiata a Parallel Python mi fa pensare che sia più vicino allo spirito di parfor, in quanto la libreria incapsula i dettagli per il caso distribuito, ma il costo per farlo è che devi adottare il loro ecosistema. Il costo dell'utilizzo di IPython è simile; devi adottare il modo di fare IPython, che può valere o meno la pena per te.

Se ti interessa la memoria distribuita, ti consiglio mpi4py . Lisandro Dalcin fa un ottimo lavoro e mpi4py viene utilizzato negli involucri Python PETSc, quindi non credo che sparirà presto. Come il multiprocessing, è un'interfaccia a basso (er) livello di parallelismo rispetto a parfor, ma è probabile che duri un po '.


Grazie, @Geoff. Hai qualche esperienza di lavoro con queste librerie? Forse proverò ad usare mpi4py su una macchina a memoria condivisa / processore multicore.
Paul G. Constantine,

@PaulGConstantine Ho usato mpi4py con successo; è abbastanza indolore, se hai familiarità con MPI. Non ho usato il multiprocessing, ma l'ho consigliato ai colleghi, che hanno affermato che ha funzionato bene per loro. Ho usato anche IPython, ma non le funzionalità di parallelismo, quindi non posso parlare di quanto funzioni.
Geoff Oxberry,

1
Aron ha un bel tutorial mpi4py che ha preparato per il corso PyHPC su Supercomputing: github.com/pyHPC/pyhpc-tutorial
Matt Knepley

4

Prima di cercare uno strumento "scatola nera", che può essere utilizzato per eseguire in parallelo funzioni "generiche" di Python, suggerirei di analizzare come my_function()parallelizzare a mano.

Innanzitutto, confronta i tempi di esecuzione dell'overhead del loop my_function(v)python for: [C] I forloop Python sono piuttosto lenti, quindi il tempo trascorso my_function()potrebbe essere trascurabile.

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

Secondo controllo se esiste una semplice implementazione vettoriale my_function(v)che non richiede loop:F[:] = my_vector_function(X)

(Questi due primi punti sono piuttosto banali, perdonami se li ho menzionati qui solo per completezza.)

Il terzo e più importante punto, almeno per le implementazioni di CPython, è verificare se my_functiontrascorre la maggior parte del tempo all'interno o all'esterno del blocco dell'interprete globale o GIL . Se il tempo è trascorso al di fuori di GIL, threadingè necessario utilizzare il modulo di libreria standard . ( Ecco un esempio). A proposito, si potrebbe pensare di scrivere my_function()come un'estensione C solo per rilasciare il GIL.

Infine, se my_function()non rilascia il GIL, si potrebbe usare il multiprocessingmodulo .

Riferimenti: documenti Python su Esecuzione simultanea e introduzione numpy / scipy sull'elaborazione parallela .


2

Puoi provare Julia. È abbastanza vicino a Python e ha molti costrutti MATLAB. La traduzione qui è:

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

Questo rende anche i numeri casuali in parallelo e concatena i risultati alla fine durante la riduzione. Questo utilizza il multiprocessing (quindi è necessario fare addprocs(N)per aggiungere processi prima dell'uso, e questo funziona anche su più nodi su un HPC come mostrato in questo post sul blog ).

Puoi anche usare pmap invece:

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

Se si desidera il parallelismo dei thread, è possibile utilizzare Threads.@threads (sebbene assicurarsi di rendere l'algoritmo thread-safe). Prima di aprire Julia, imposta la variabile di ambiente JULIA_NUM_THREADS, quindi è:

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

Qui creo un array separato per ogni thread, in modo che non si scontrino quando si aggiungono all'array, quindi concatenano gli array in seguito. Il threading è piuttosto nuovo, quindi in questo momento c'è solo l'uso diretto dei thread, ma sono sicuro che le riduzioni e le mappe threaded verranno aggiunte proprio come lo era per il multiprocessing.


Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.