Paralelizar un bucle for en Python

35

¿Hay alguna herramienta en Python que sea como el parfor de Matlab? Encontré este hilo , pero tiene cuatro años. Pensé que tal vez alguien aquí podría tener una experiencia más reciente.

Aquí hay un ejemplo del tipo de cosas que me gustaría paralelizar:

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

donde my_functiontoma un ndarraytamaño (1,3)y devuelve un escalar.

Al menos, me gustaría usar varios núcleos simultáneamente, como parfor. En otras palabras, suponga un sistema de memoria compartida con 8 a 16 núcleos.

Paul G. Constantine
fuente
Muchos resultados en google. Estos parecen bastante simples: blog.dominodatalab.com/simple-parallelization quora.com/What-is-the-Python-equivalent-of-MATLABs-parfor
Doug Lipinski
Gracias, @ doug-lipinski. Esos ejemplos, como otros que encontré mientras busqué en Google, tienen algunos cálculos triviales basados ​​en el índice de iteración. Y siempre afirman que el código es "increíblemente fácil". Mi ejemplo define las matrices (asigna la memoria) fuera del ciclo for. Estoy bien haciéndolo de otra manera; así es como lo hago en Matlab. La parte difícil que parece contrarrestar esos ejemplos es obtener parte de una matriz dada para la función dentro del bucle.
Paul G. Constantine

Respuestas:

19

Joblib hace lo que quieres. El patrón de uso básico es:

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

donde arg_instanceses la lista de valores para los que myfunse calcula en paralelo. La restricción principal es que myfundebe ser una función de nivel superior. El backendparámetro puede ser "threading"o "multiprocessing".

Puede pasar parámetros comunes adicionales a la función paralelizada. El cuerpo de myfuntambién puede referirse a variables globales inicializadas, los valores que estarán disponibles para los hijos.

Los argumentos y los resultados pueden ser prácticamente cualquier cosa con el backend de subprocesos, pero los resultados deben ser serializables con el backend de multiprocesamiento.


Dask también ofrece una funcionalidad similar. Puede ser preferible si está trabajando con datos fuera del núcleo o si está tratando de paralelizar cálculos más complejos.

Daniel Mahler
fuente
Veo cero valor agregado para usar la batería, incluido el multiprocesamiento. Apuesto a que joblib lo está usando debajo del capó.
Xavier Combelle
1
Debe mencionarse que joblib no es mágico, el threadingbackend sufre el cuello de botella de GIL y el multiprocessingbackend genera una gran sobrecarga debido a la serialización de todos los parámetros y valores de retorno. Vea esta respuesta para los detalles de bajo nivel del procesamiento paralelo en Python.
Jakub Klinkovský
No puedo encontrar una combinación de la complejidad de la función y el número de iteraciones para las cuales joblib sería más rápido que un ciclo for. Para mí, tiene la misma velocidad si n_jobs = 1, y es mucho más lento en todos los demás casos
Aleksejs Fomins
@AleksejsFomins El paralelismo basado en subprocesos no ayudará para el código que no libera el GIL, pero un número significativo sí, particularmente la ciencia de datos o las bibliotecas numéricas. De lo contrario, necesita multiprocesamiento, Jobli admite ambos. El módulo de multiprocesamiento ahora también tiene paralelo mapque puede usar directamente. Además, si utiliza mkl compilado numpy, paralelizará las operaciones vectorizadas automáticamente sin que usted haga nada. El numpy en Ananconda está habilitado para mkl por defecto. Sin embargo, no hay una solución universal. Joblib está muy preocupado y hubo menos oportunidades en 2015.
Daniel Mahler
Gracias por su consejo. Recuerdo haber intentado el multiprocesamiento antes e incluso escribir algunas publicaciones, porque no escalaba como esperaba. Tal vez debería darle otra mirada
Aleksejs Fomins
9

Lo que estás buscando es Numba , que puede paralelizar automáticamente un bucle for. De su documentación

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
LKlevin
fuente
8

Sin suponer que algo especial en la my_functionelección multiprocessing.Pool().map()es una buena suposición para paralelizar dichos bucles simples. joblib, dask, mpiCálculos o numbacomo se propone en otras respuestas no se ve con lo que ninguna ventaja para este tipo de casos de uso y agregar dependencias inútiles (para resumir que son una exageración). Es poco probable que el uso de subprocesos como se propone en otra respuesta sea una buena solución, ya que debe ser íntimo con la interacción GIL de su código o su código debe hacer principalmente entrada / salida.

Dicho esto, numbapodría ser una buena idea acelerar el código secuencial de Python puro, pero creo que esto está fuera del alcance de la pregunta.

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

Sin embargo, hay algunas advertencias (pero que no deberían afectar la mayoría de las aplicaciones):

  • En Windows no hay soporte de fork, por lo que se inicia un intérprete con el módulo principal al inicio de cada hijo, por lo que podría tener una sobrecarga (y es la razón de la if __name__ == "__main__"
  • Los argumentos y los resultados de my_function se conservan en vinagre y no se recogen, podría ser una sobrecarga demasiado grande, vea esta respuesta para reducirla https://stackoverflow.com/a/37072511/128629 . También hace que los objetos no seleccionables sean inutilizables
  • my_functionno debe depender de estados compartidos como comunicarse con variables globales porque los estados no se comparten entre procesos. Las funciones puras (funciones en los sentidos matemáticos) son ejemplos de funciones que no comparten estados
Xavier Combelle
fuente
6

Mi impresión de parfor es que MATLAB está encapsulando los detalles de implementación, por lo que podría estar utilizando tanto el paralelismo de memoria compartida (que es lo que quiere) como el paralelismo de memoria distribuida (si está ejecutando un servidor de computación distribuida de MATLAB ).

Si desea un paralelismo de memoria compartido y está ejecutando algún tipo de bucle paralelo de tareas, el paquete de biblioteca estándar de multiprocesamiento es probablemente lo que desea, tal vez con un buen front-end, como joblib , como se menciona en la publicación de Doug. La biblioteca estándar no va a desaparecer, y se mantiene, por lo que es de bajo riesgo.

También hay otras opciones, como Parallel Python y las capacidades paralelas de IPython . Un vistazo rápido a Parallel Python me hace pensar que está más cerca del espíritu de parfor, ya que la biblioteca encapsula los detalles del caso distribuido, pero el costo de hacerlo es que tienes que adoptar su ecosistema. El costo de usar IPython es similar; debes adoptar la forma de hacer las cosas de IPython, que puede o no valer la pena para ti.

Si le importa la memoria distribuida, le recomiendo mpi4py . Lisandro Dalcin hace un gran trabajo, y mpi4py se usa en los envoltorios de PETSc Python, por lo que no creo que desaparezca pronto. Al igual que el multiprocesamiento, es una interfaz de bajo (er) nivel de paralelismo que parfor, pero es probable que dure un tiempo.

Geoff Oxberry
fuente
Gracias, @ Geoff. ¿Tienes alguna experiencia trabajando con estas bibliotecas? Tal vez intentaré usar mpi4py en una máquina de memoria compartida / procesador multinúcleo.
Paul G. Constantine
@PaulGConstantine He usado mpi4py con éxito; es bastante indoloro, si estás familiarizado con MPI. No he usado multiprocesamiento, pero lo he recomendado a mis colegas, quienes dijeron que les funcionó bien. También he usado IPython, pero no las características de paralelismo, por lo que no puedo hablar de lo bien que funciona.
Geoff Oxberry
1
Aron tiene un buen tutorial mpi4py que preparó para el curso PyHPC en Supercomputing: github.com/pyHPC/pyhpc-tutorial
Matt Knepley
4

Antes de buscar una herramienta de "recuadro negro", que pueda usarse para ejecutar en paralelo funciones "genéricas" de Python, sugeriría analizar cómo my_function()se puede paralelizar manualmente.

Primero, compare el tiempo de ejecución de la sobrecarga del bucle de my_function(v)Python for: [C] Los forbucles de Python son bastante lentos, por lo que el tiempo invertido my_function()podría 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 verificación si hay una implementación vectorial simple my_function(v)que no requiere bucles:F[:] = my_vector_function(X)

(Estos dos primeros puntos son bastante triviales, perdóname si los mencioné aquí solo para completarlos).

El tercer y más importante punto, al menos para las implementaciones de CPython, es verificar si my_functionpasa la mayor parte del tiempo dentro o fuera del bloqueo global del intérprete , o GIL . Si se pasa tiempo fuera del GIL, se debe usar el threadingmódulo de biblioteca estándar . ( Aquí un ejemplo). Por cierto, uno podría pensar en escribir my_function()como una extensión C solo para liberar el GIL.

Finalmente, si my_function()no libera el GIL, uno podría usar el multiprocessingmódulo .

Referencias: documentos de Python sobre ejecución simultánea e introducción numpy / scipy sobre procesamiento paralelo .

Stefano M
fuente
2

Puedes probar Julia. Está bastante cerca de Python y tiene muchas construcciones de MATLAB. La traducción aquí es:

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

Esto hace que los números aleatorios también sean paralelos, y simplemente concatena los resultados al final durante la reducción. Eso usa multiprocesamiento (por lo que debe hacer addprocs(N)para agregar procesos antes de usar, y esto también funciona en múltiples nodos en un HPC como se muestra en esta publicación de blog ).

También puedes usar pmapen su lugar:

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

Si desea paralelismo de subprocesos, puede usar Threads.@threads(aunque asegúrese de hacer que el algoritmo sea seguro para subprocesos). Antes de abrir Julia, configure la variable de entorno JULIA_NUM_THREADS, luego es:

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

Aquí hago una matriz separada para cada hilo, de modo que no choquen cuando se agreguen a la matriz, luego simplemente concatenen las matrices después. Los subprocesos son bastante nuevos, por lo que en este momento solo hay un uso directo de los subprocesos, pero estoy seguro de que se agregarán reducciones de subprocesos y mapas como lo fue para el multiprocesamiento.

Chris Rackauckas
fuente
0

Recomiendo usar las funciones paralelas y retrasadas de la biblioteca joblib para usar el módulo "tempfile" para crear memoria temporal compartida para grandes matrices, los ejemplos y el uso se pueden encontrar aquí https://pythonhosted.org/joblib/parallel.html

Ramkumar
fuente