python - lock - Joblib paralelo múltiples cpu más lento que solo
python multiprocessing windows (2)
Además de la respuesta anterior, y para futuras referencias, hay dos aspectos de esta pregunta, y las recientes evoluciones de joblib ayudan con ambas.
Gastos generales de creación de agrupaciones paralelas : el problema aquí es que crear una agrupación paralela es costoso. Fue especialmente costoso aquí, ya que el código no protegido por el " principal " se ejecutó en cada trabajo en la creación del objeto paralelo. En la versión más reciente de Joblib (aún beta), Parallel se puede usar como administrador de contexto para limitar el número de veces que se crea una agrupación y, por lo tanto, el impacto de esta sobrecarga.
Gastos generales de envío : es importante tener en cuenta que el envío de un elemento del bucle for tiene una sobrecarga (mucho más grande que iterar un bucle for sin paralelo). Por lo tanto, si estos elementos de cálculo individuales son muy rápidos, esta sobrecarga dominará el cálculo. En el último joblib, joblib rastreará el tiempo de ejecución de cada trabajo y comenzará a agruparlos si son muy rápidos. Esto limita en gran medida el impacto de la sobrecarga de despacho en la mayoría de los casos (consulte la PR para el banco y la discusión).
Descargo de responsabilidad : Soy el autor original de joblib (solo digo para advertir contra posibles conflictos de interés en mi respuesta, aunque aquí creo que es irrelevante).
Acabo de empezar a usar el módulo Joblib y estoy tratando de entender cómo funciona la función paralela. A continuación se muestra un ejemplo de donde la paralelización conduce a tiempos de ejecución más largos, pero no entiendo por qué. Mi tiempo de ejecución en 1 cpu fue de 51 segundos frente a 217 segundos en 2 cpu.
Supongo que ejecutar el bucle en paralelo copiaría las listas a y b a cada procesador. Luego desplace item_n a una cpu y item_n + 1 a la otra cpu, ejecute la función y luego escriba los resultados de nuevo en una lista (en orden). Luego agarra los siguientes 2 elementos y así sucesivamente. Obviamente me estoy perdiendo algo
¿Es este un mal ejemplo o uso de joblib? ¿Simplemente estructuré mal el código?
Aquí está el ejemplo:
import numpy as np
from matplotlib.path import Path
from joblib import Parallel, delayed
## Create pairs of points for line segments
a = zip(np.random.rand(5000,2),np.random.rand(5000,2))
b = zip(np.random.rand(300,2),np.random.rand(300,2))
## Check if one line segment contains another.
def check_paths(path, paths):
for other_path in paths:
res=''no cross''
chck = Path(other_path)
if chck.contains_path(path)==1:
res= ''cross''
break
return res
res = Parallel(n_jobs=2) (delayed(check_paths) (Path(points), a) for points in b)
En resumen: no puedo reproducir tu problema. Si está en Windows, debe usar un protector para su bucle principal: documentación de joblib.Parallel
. El único problema que veo es la sobrecarga de copia de datos, pero parece que los números no son realistas.
En mucho tiempo, aquí están mis tiempos con tu código:
En mi i7 3770k (4 núcleos, 8 hilos) obtengo los siguientes resultados para diferentes n_jobs
:
For-loop: Finished in 33.8521318436 sec
n_jobs=1: Finished in 33.5527760983 sec
n_jobs=2: Finished in 18.9543449879 sec
n_jobs=3: Finished in 13.4856410027 sec
n_jobs=4: Finished in 15.0832719803 sec
n_jobs=5: Finished in 14.7227740288 sec
n_jobs=6: Finished in 15.6106669903 sec
Así que hay una ganancia en el uso de múltiples procesos. Sin embargo, aunque tengo cuatro núcleos, la ganancia ya se satura en tres procesos. Así que supongo que el tiempo de ejecución en realidad está limitado por el acceso a la memoria en lugar del tiempo del procesador.
Debe observar que los argumentos para cada entrada de bucle individual se copian al proceso que lo ejecuta. Esto significa que copia a
para cada elemento en b
. Eso es ineficaz. Así que en lugar de acceder a la global a
. ( Parallel
bifurcará el proceso, copiando todas las variables globales a los nuevos procesos generados, por lo que es accesible). Esto me da el siguiente código (con temporización y protección de bucle principal como recomienda la documentación de joblib
:
import numpy as np
from matplotlib.path import Path
from joblib import Parallel, delayed
import time
import sys
## Check if one line segment contains another.
def check_paths(path):
for other_path in a:
res=''no cross''
chck = Path(other_path)
if chck.contains_path(path)==1:
res= ''cross''
break
return res
if __name__ == ''__main__'':
## Create pairs of points for line segments
a = zip(np.random.rand(5000,2),np.random.rand(5000,2))
b = zip(np.random.rand(300,2),np.random.rand(300,2))
now = time.time()
if len(sys.argv) >= 2:
res = Parallel(n_jobs=int(sys.argv[1])) (delayed(check_paths) (Path(points)) for points in b)
else:
res = [check_paths(Path(points)) for points in b]
print "Finished in", time.time()-now , "sec"
Resultados de tiempo:
n_jobs=1: Finished in 34.2845709324 sec
n_jobs=2: Finished in 16.6254048347 sec
n_jobs=3: Finished in 11.219119072 sec
n_jobs=4: Finished in 8.61683392525 sec
n_jobs=5: Finished in 8.51907801628 sec
n_jobs=6: Finished in 8.21842098236 sec
n_jobs=7: Finished in 8.21816396713 sec
n_jobs=8: Finished in 7.81841087341 sec
La saturación ahora se movió ligeramente a n_jobs=4
que es el valor que se espera.
check_paths
realiza varios cálculos redundantes que pueden eliminarse fácilmente. En primer lugar, para todos los elementos en other_paths=a
la Path(...)
de la línea Path(...)
se ejecuta en cada llamada. Precalcular eso. En segundo lugar, la cadena res=''no cross''
se escribe en cada giro de bucle, aunque solo puede cambiar una vez (seguido de una ruptura y retorno). Mueve la línea delante del bucle. Entonces el código se ve así:
import numpy as np
from matplotlib.path import Path
from joblib import Parallel, delayed
import time
import sys
## Check if one line segment contains another.
def check_paths(path):
#global a
#print(path, a[:10])
res=''no cross''
for other_path in a:
if other_path.contains_path(path)==1:
res= ''cross''
break
return res
if __name__ == ''__main__'':
## Create pairs of points for line segments
a = zip(np.random.rand(5000,2),np.random.rand(5000,2))
a = [Path(x) for x in a]
b = zip(np.random.rand(300,2),np.random.rand(300,2))
now = time.time()
if len(sys.argv) >= 2:
res = Parallel(n_jobs=int(sys.argv[1])) (delayed(check_paths) (Path(points)) for points in b)
else:
res = [check_paths(Path(points)) for points in b]
print "Finished in", time.time()-now , "sec"
con tiempos:
n_jobs=1: Finished in 5.33742594719 sec
n_jobs=2: Finished in 2.70858597755 sec
n_jobs=3: Finished in 1.80810618401 sec
n_jobs=4: Finished in 1.40814709663 sec
n_jobs=5: Finished in 1.50854086876 sec
n_jobs=6: Finished in 1.50901818275 sec
n_jobs=7: Finished in 1.51030707359 sec
n_jobs=8: Finished in 1.51062297821 sec
Un nodo lateral en su código, aunque realmente no he seguido su propósito, ya que no estaba relacionado con su pregunta, contains_path
solo devolverá True
if this path completely contains the given path.
(ver documentation ). Por lo tanto, su función básicamente siempre devolverá no cross
dada la entrada aleatoria.