Respuesta corta
El algoritmo de chunksize de Pool es heurístico. Proporciona una solución sencilla para todos los escenarios de problemas imaginables que está intentando introducir en los métodos de Pool. Como consecuencia, no se puede optimizar para ningún escenario específico .
El algoritmo divide arbitrariamente el iterable en aproximadamente cuatro veces más partes que el enfoque ingenuo. Más fragmentos significan más gastos generales, pero una mayor flexibilidad de programación. Cómo se mostrará esta respuesta, esto conduce a una mayor utilización de los trabajadores en promedio, pero sin la garantía de un tiempo de cálculo general más corto para cada caso.
"Es bueno saberlo", podría pensar, "pero ¿cómo me ayuda saber esto con mis problemas concretos de multiprocesamiento?" Bueno, no es así. La respuesta corta más honesta es "no hay una respuesta corta", "el multiprocesamiento es complejo" y "depende". Un síntoma observado puede tener diferentes raíces, incluso para escenarios similares.
Esta respuesta intenta brindarle conceptos básicos que lo ayudarán a obtener una imagen más clara de la caja negra de programación de Pool. También intenta brindarle algunas herramientas básicas a mano para reconocer y evitar posibles acantilados en la medida en que estén relacionados con el tamaño del trozo.
Tabla de contenido
Parte I
- Definiciones
- Metas de paralelización
- Escenarios de paralelización
- Riesgos de Chunksize> 1
- Algoritmo de tamaño de trozos de la piscina
Cuantificación de la eficiencia del algoritmo
6.1 Modelos
6.2 Programación paralela
6.3 Eficiencias
6.3.1 Eficiencia de distribución absoluta (ADE)
6.3.2 Eficiencia de distribución relativa (RDE)
Parte II
- Ingenuo vs.Algoritmo de Chunksize de Pool
- Verificación de la realidad
- Conclusión
Primero es necesario aclarar algunos términos importantes.
1. Definiciones
Pedazo
Un fragmento aquí es una parte del iterable
argumento especificado en una llamada al método pool. Cómo se calcula el tamaño del trozo y qué efectos puede tener, es el tema de esta respuesta.
Tarea
La representación física de una tarea en un proceso de trabajo en términos de datos se puede ver en la siguiente figura.
La figura muestra una llamada de ejemplo a pool.map()
, mostrada a lo largo de una línea de código, tomada de la multiprocessing.pool.worker
función, donde inqueue
se desempaqueta una tarea leída de . worker
es la función principal subyacente en MainThread
un proceso de trabajo de grupo. El func
-argumento especificado en el método pool solo coincidirá con la func
-variable dentro de la función worker
-para métodos de llamada única como apply_async
y para imap
con chunksize=1
. Para el resto de los métodos de grupo con un chunksize
parámetro, la función de procesamiento func
será una función de mapeador ( mapstar
o starmapstar
). Esta función asigna el func
parámetro especificado por el usuario en cada elemento del fragmento transmitido del iterable (-> "tareas de mapa"). El tiempo que toma, define una tareatambién como unidad de trabajo .
Taskel
Si bien el uso de la palabra "tarea" para todo el procesamiento de un fragmento se corresponde con el código interno multiprocessing.pool
, no hay ninguna indicación de cómo una sola llamada al usuario especificado func
, con un elemento del fragmento como argumento (s), debe ser referido a. Para evitar la confusión que surge de los conflictos de nombres (piense en el parámetro -paramaxtasksperchild
el __init__
método de Pool ), esta respuesta se referirá a las unidades individuales de trabajo dentro de una tarea como taskel .
Un taskel (de tarea + elemento ) es la unidad de trabajo más pequeña dentro de una tarea . Es la ejecución única de la función especificada con el func
parámetro -de un Pool
método-, llamado con argumentos obtenidos de un solo elemento del fragmento transmitido . Una tarea consta de chunksize
tareas .
Sobrecarga de paralelización (PO)
PO consta de gastos generales internos de Python y gastos generales para la comunicación entre procesos (IPC). La sobrecarga por tarea dentro de Python viene con el código necesario para empaquetar y desempaquetar las tareas y sus resultados. IPC-overhead viene con la necesaria sincronización de hilos y la copia de datos entre diferentes espacios de direcciones (se necesitan dos pasos de copia: padre -> cola -> hijo). La cantidad de sobrecarga de IPC depende del tamaño de los datos, el hardware y el sistema operativo, lo que dificulta las generalizaciones sobre el impacto.
2. Objetivos de paralelización
Cuando usamos multiprocesamiento, nuestro objetivo general (obviamente) es minimizar el tiempo total de procesamiento para todas las tareas. Para alcanzar este objetivo general, nuestro objetivo técnico debe ser optimizar la utilización de los recursos de hardware .
Algunos subobjetivos importantes para lograr el objetivo técnico son:
- minimizar la sobrecarga de paralelización (el más famoso, pero no solo: IPC )
- alta utilización en todos los núcleos de cpu
- mantener el uso de la memoria limitado para evitar que el sistema operativo pague en exceso ( papelera )
Al principio, las tareas deben ser lo suficientemente pesadas (intensivas) computacionalmente, para recuperar el PO que tenemos que pagar por la paralelización. La relevancia de PO disminuye al aumentar el tiempo de cálculo absoluto por tarea. O, para decirlo al revés, cuanto mayor sea el tiempo de cálculo absoluto por tarea para su problema, menos relevante será la necesidad de reducir el PO. Si su cálculo tomará horas por tarea, la sobrecarga de IPC será insignificante en comparación. La principal preocupación aquí es evitar que los procesos de trabajo inactivos después de que se hayan distribuido todas las tareas. Mantener todos los núcleos cargados significa que estamos paralelizando tanto como sea posible.
3. Escenarios de paralelización
¿Qué factores determinan un argumento de tamaño de trozo óptimo para métodos como el multiprocesamiento. Pool.map ()
El factor principal en cuestión es cuánto tiempo de cálculo puede variar entre nuestras tareas individuales. Para nombrarlo, la elección de un tamaño de trozo óptimo está determinada por el coeficiente de variación ( CV ) para los tiempos de cálculo por tarea.
Los dos escenarios extremos en una escala, según el alcance de esta variación son:
- Todas las tareas necesitan exactamente el mismo tiempo de cálculo.
- Una tarea puede tardar segundos o días en completarse.
Para una mejor memorización, me referiré a estos escenarios como:
- Escenario denso
- Escenario amplio
Escenario denso
En un escenario denso , sería deseable distribuir todas las tareas a la vez, para mantener la IPC necesaria y el cambio de contexto al mínimo. Esto significa que queremos crear solo la cantidad de fragmentos, la cantidad de procesos de trabajo que haya. Como ya se dijo anteriormente, el peso de PO aumenta con tiempos de cálculo más cortos por tarea.
Para un rendimiento máximo, también queremos que todos los procesos de trabajo estén ocupados hasta que se procesen todas las tareas (sin trabajadores inactivos). Para este objetivo, los fragmentos distribuidos deben ser del mismo tamaño o cerca de.
Escenario amplio
El mejor ejemplo de un escenario amplio sería un problema de optimización, donde los resultados convergen rápidamente o el cálculo puede llevar horas, si no días. Por lo general, no es predecible qué combinación de "tareas ligeras" y "tareas pesadas" contendrá una tarea en tal caso, por lo que no es aconsejable distribuir demasiadas tareas en un lote de tareas a la vez. Distribuir menos tareas a la vez de lo posible significa aumentar la flexibilidad de programación. Esto es necesario aquí para alcanzar nuestro subobjetivo de una alta utilización de todos los núcleos.
Si los Pool
métodos, por defecto, estuvieran totalmente optimizados para el escenario denso, crearían cada vez más tiempos subóptimos para cada problema ubicado más cerca del escenario amplio.
4. Riesgos de Chunksize> 1
Considere este ejemplo simplificado de pseudocódigo de un Escenario Amplio -iterable, que queremos pasar a un método de grupo:
good_luck_iterable = [60, 60, 86400, 60, 86400, 60, 60, 84600]
En lugar de los valores reales, pretendemos ver el tiempo de cálculo necesario en segundos, por simplicidad solo 1 minuto o 1 día. Suponemos que el grupo tiene cuatro procesos de trabajo (en cuatro núcleos) y chunksize
está configurado en 2
. Debido a que se mantendrá el orden, los fragmentos enviados a los trabajadores serán los siguientes:
[(60, 60), (86400, 60), (86400, 60), (60, 84600)]
Como tenemos suficientes trabajadores y el tiempo de cálculo es lo suficientemente alto, podemos decir que cada proceso de trabajo tendrá una parte en la que trabajar en primer lugar. (Este no tiene por qué ser el caso para completar tareas rápidamente). Además, podemos decir que todo el procesamiento tomará aproximadamente 86400 + 60 segundos, porque ese es el tiempo de cálculo total más alto para un fragmento en este escenario artificial y distribuimos fragmentos solo una vez.
Ahora considere este iterable, que tiene solo un elemento cambiando su posición en comparación con el iterable anterior:
bad_luck_iterable = [60, 60, 86400, 86400, 60, 60, 60, 84600]
... y los trozos correspondientes:
[(60, 60), (86400, 86400), (60, 60), (60, 84600)]
¡Solo mala suerte con la clasificación de nuestro iterable casi duplicó (86400 + 86400) nuestro tiempo total de procesamiento! El trabajador que recibe el fragmento vicioso (86400, 86400) está bloqueando el segundo taskel pesado en su tarea para que no se distribuya a uno de los trabajadores inactivos que ya terminaron con sus (60, 60) fragmentos. Obviamente, no nos arriesgaríamos a un resultado tan desagradable si nos dispusiéramos chunksize=1
.
Este es el riesgo de trozos más grandes. Con tamaños más altos, cambiamos la flexibilidad de programación por menos gastos generales y, en casos como el anterior, eso es un mal negocio.
Cómo lo veremos en el capítulo 6. Cuantificación de la eficiencia del algoritmo , los fragmentos más grandes también pueden conducir a resultados subóptimos para los escenarios densos .
5. Algoritmo de trozos de pool
A continuación, encontrará una versión ligeramente modificada del algoritmo dentro del código fuente. Como puede ver, corté la parte inferior y la envolví en una función para calcular el chunksize
argumento externamente. También reemplacé 4
con un factor
parámetro y subcontraté las len()
llamadas.
def calc_chunksize(n_workers, len_iterable, factor=4):
"""Calculate chunksize argument for Pool-methods.
Resembles source-code within `multiprocessing.pool.Pool._map_async`.
"""
chunksize, extra = divmod(len_iterable, n_workers * factor)
if extra:
chunksize += 1
return chunksize
Para asegurarnos de que todos estamos en la misma página, esto es lo que divmod
hace:
divmod(x, y)
es una función incorporada que regresa (x//y, x%y)
.
x // y
es la división de piso, que devuelve el cociente redondeado hacia abajo de x / y
, mientras que
x % y
es la operación de módulo que devuelve el resto de x / y
. De ahí, por ejemplo, divmod(10, 3)
devoluciones (3, 1)
.
Ahora, cuando nos fijamos en chunksize, extra = divmod(len_iterable, n_workers * 4)
, se dará cuenta de n_workers
que aquí es el divisor y
en x / y
y por la multiplicación 4
, sin un ajuste adicional a través de if extra: chunksize +=1
más adelante, conduce a una chunksize inicial de al menos cuatro veces más pequeño (por len_iterable >= n_workers * 4
) de lo que sería de otra manera.
Para ver el efecto de la multiplicación por 4
en el resultado de tamaño de fragmento intermedio, considere esta función:
def compare_chunksizes(len_iterable, n_workers=4):
"""Calculate naive chunksize, Pool's stage-1 chunksize and the chunksize
for Pool's complete algorithm. Return chunksizes and the real factors by
which naive chunksizes are bigger.
"""
cs_naive = len_iterable // n_workers or 1
cs_pool1 = len_iterable // (n_workers * 4) or 1
cs_pool2 = calc_chunksize(n_workers, len_iterable)
real_factor_pool1 = cs_naive / cs_pool1
real_factor_pool2 = cs_naive / cs_pool2
return cs_naive, cs_pool1, cs_pool2, real_factor_pool1, real_factor_pool2
La función anterior calcula el tamaño de trozo ingenuo ( cs_naive
) y el tamaño de trozo del primer paso del algoritmo de tamaño de trozo ( cs_pool1
) de Pool , así como el tamaño de trozo del algoritmo de grupo completo ( cs_pool2
). Además, calcula los factores reales rf_pool1 = cs_naive / cs_pool1
y rf_pool2 = cs_naive / cs_pool2
, que nos dicen cuántas veces los tamaños de trozos calculados ingenuamente son más grandes que las versiones internas de Pool.
A continuación, verá dos figuras creadas con la salida de esta función. La figura de la izquierda solo muestra los tamaños de trozos n_workers=4
hasta una longitud iterable de 500
. La figura de la derecha muestra los valores de rf_pool1
. Para longitudes iterables 16
, el factor real se convierte en >=4
(para len_iterable >= n_workers * 4
) y su valor máximo es 7
para longitudes iterables 28-31
. Esa es una desviación masiva del factor original 4
al que converge el algoritmo para iterables más largos. "Más tiempo" aquí es relativo y depende del número de trabajadores especificados.
Recuerde que chunksize cs_pool1
todavía carece del extra
ajuste -con el resto del divmod
contenido cs_pool2
del algoritmo completo.
El algoritmo continúa con:
if extra:
chunksize += 1
Ahora bien, en los casos estaban allí es un resto (una extra
de la DIVMOD-operación), el aumento de la chunksize por 1, obviamente, no puede trabajar para cada tarea. Después de todo, si así fuera, para empezar, no quedaría un resto.
Como se puede ver en las figuras siguientes, el " extra-tratamiento " tiene el efecto, que el verdadero factor de rf_pool2
ahora converge hacia 4
desde abajo 4
y la desviación es un poco más suave. Desviación estándar para n_workers=4
y len_iterable=500
cae de 0.5233
para rf_pool1
a 0.4115
para rf_pool2
.
Eventualmente, aumentar chunksize
en 1 tiene el efecto de que la última tarea transmitida solo tenga un tamaño de len_iterable % chunksize or chunksize
.
Sin embargo , el efecto más interesante y como veremos más adelante, más consecuente, del tratamiento adicional se puede observar para la cantidad de fragmentos generados ( n_chunks
). Para iterables lo suficientemente largos, el algoritmo de tamaño de trozos completo de Pool ( n_pool2
en la figura siguiente) estabilizará el número de trozos en n_chunks == n_workers * 4
. Por el contrario, el algoritmo ingenuo (después de un eructo inicial) sigue alternando entre n_chunks == n_workers
y a n_chunks == n_workers + 1
medida que crece la duración del iterable.
A continuación, encontrará dos funciones de información mejoradas para Pool y el ingenuo algoritmo chunksize. La salida de estas funciones será necesaria en el próximo capítulo.
from collections import namedtuple
Chunkinfo = namedtuple(
'Chunkinfo', ['n_workers', 'len_iterable', 'n_chunks',
'chunksize', 'last_chunk']
)
def calc_chunksize_info(n_workers, len_iterable, factor=4):
"""Calculate chunksize numbers."""
chunksize, extra = divmod(len_iterable, n_workers * factor)
if extra:
chunksize += 1
n_chunks = len_iterable // chunksize + (len_iterable % chunksize > 0)
last_chunk = len_iterable % chunksize or chunksize
return Chunkinfo(
n_workers, len_iterable, n_chunks, chunksize, last_chunk
)
No se confunda por el aspecto probablemente inesperado de calc_naive_chunksize_info
. El extra
desde divmod
no se utiliza para calcular el tamaño del fragmento.
def calc_naive_chunksize_info(n_workers, len_iterable):
"""Calculate naive chunksize numbers."""
chunksize, extra = divmod(len_iterable, n_workers)
if chunksize == 0:
chunksize = 1
n_chunks = extra
last_chunk = chunksize
else:
n_chunks = len_iterable // chunksize + (len_iterable % chunksize > 0)
last_chunk = len_iterable % chunksize or chunksize
return Chunkinfo(
n_workers, len_iterable, n_chunks, chunksize, last_chunk
)
6. Cuantificación de la eficiencia del algoritmo
Ahora, después de haber visto cómo la salida del Pool
algoritmo de tamaño de chunksize se ve diferente en comparación con la salida del algoritmo ingenuo ...
- ¿Cómo saber si el enfoque de Pool realmente mejora algo?
- ¿Y qué podría ser exactamente este algo ?
Como se muestra en el capítulo anterior, para iterables más largos (una mayor cantidad de tareas), el algoritmo de tamaño de trozos de Pool divide aproximadamente el iterable en cuatro veces más partes que el método ingenuo. Los fragmentos más pequeños significan más tareas y más tareas significan más gastos indirectos de paralelización (PO) , un costo que debe sopesarse con el beneficio de una mayor flexibilidad de programación (recuerde "Riesgos de tamaño de fragmento> 1" ).
Por razones bastante obvias, el algoritmo de chunksize básico de Pool no puede sopesar la flexibilidad de programación con la PO para nosotros. La sobrecarga de IPC depende del tamaño de los datos, el hardware y el sistema operativo. El algoritmo no puede saber en qué hardware ejecutamos nuestro código, ni tiene idea de cuánto tardará una tarea en completarse. Es una heurística que proporciona una funcionalidad básica para todos los escenarios posibles. Esto significa que no se puede optimizar para ningún escenario en particular. Como se mencionó anteriormente, PO también se vuelve cada vez menos preocupante con el aumento de los tiempos de cálculo por tarea (correlación negativa).
Cuando recuerde los Objetivos de Paralelización del capítulo 2, un punto fue:
- alta utilización en todos los núcleos de cpu
El anteriormente mencionado algo , chunksize-algoritmo de piscina puede tratar de mejorar es la minimización de ralentí trabajadores-procesos , respectivamente, la utilización de la CPU-cores .
Las multiprocessing.Pool
personas que se preguntan acerca de los núcleos no utilizados / procesos de trabajo inactivos hacen una pregunta repetida sobre SO con respecto a situaciones en las que se esperaría que todos los procesos de trabajo estén ocupados. Si bien esto puede tener muchas razones, los procesos de trabajo inactivos hacia el final de un cálculo son una observación que a menudo podemos hacer, incluso con Escenarios densos (tiempos de cálculo iguales por tarea) en los casos en que el número de trabajadores no es un divisor del número. de trozos ( n_chunks % n_workers > 0
).
La pregunta ahora es:
¿Cómo podemos traducir prácticamente nuestra comprensión de los fragmentos en algo que nos permita explicar la utilización observada del trabajador, o incluso comparar la eficiencia de diferentes algoritmos en ese sentido?
6.1 Modelos
Para obtener información más profunda aquí, necesitamos una forma de abstracción de cálculos paralelos que simplifique la realidad demasiado compleja hasta un grado manejable de complejidad, al tiempo que conserva la importancia dentro de los límites definidos. Tal abstracción se llama modelo . Una implementación de tal " Modelo de Paralelización" (PM) genera metadatos mapeados por el trabajador (marcas de tiempo) como lo harían los cálculos reales, si los datos fueran recopilados. Los metadatos generados por el modelo permiten predecir métricas de cálculos paralelos bajo ciertas restricciones.
Uno de los dos submodelos dentro del PM definido aquí es el Modelo de Distribución (DM) . El DM explica cómo las unidades atómicas de trabajo (taskels) se distribuyen entre los trabajadores paralelos y el tiempo , cuando no se consideran otros factores que el algoritmo chunksize-size respectivo, el número de trabajadores, el input-iterable (número de taskels) y su duración de cálculo. . Esto significa que no se incluye ningún tipo de gastos generales .
Para obtener un MP completo , el DM se amplía con un Modelo de Sobrecarga (OM) , que representa varias formas de Sobrecarga de Paralelización (PO) . Dicho modelo debe calibrarse para cada nodo individualmente (dependencias de hardware, SO). Se deja abierta la cantidad de formas de sobrecarga representadas en un OM y, por lo tanto, pueden existir múltiples OM con diversos grados de complejidad. El nivel de precisión que necesita el OM implementado está determinado por el peso total de PO para el cálculo específico. Las tareas más cortas conducen a un mayor peso de PO , lo que a su vez requiere un OM más precisosi estuviéramos intentando predecir las Eficiencias de Paralelización (PE) .
6.2 Programación paralela (PS)
El programa paralelo es una representación bidimensional del cálculo paralelo, donde el eje x representa el tiempo y el eje y representa un grupo de trabajadores paralelos. El número de trabajadores y el tiempo total de cálculo marcan la extensión de un rectángulo, en el que se dibujan rectángulos más pequeños. Estos rectángulos más pequeños representan unidades atómicas de trabajo (taskels).
A continuación, encontrará la visualización de un PS extraído con datos del DM del algoritmo de tamaño de grupo para el escenario denso .
- El eje x se divide en unidades de tiempo iguales, donde cada unidad representa el tiempo de cálculo que requiere un taskel.
- El eje y se divide en el número de procesos de trabajo que utiliza el grupo.
- Un taskel aquí se muestra como el rectángulo de color cian más pequeño, colocado en una línea de tiempo (un programa) de un proceso de trabajo anónimo.
- Una tarea es una o varias tareas en una línea de tiempo de trabajador resaltadas continuamente con el mismo tono.
- Las unidades de tiempo de inactividad se representan mediante mosaicos de color rojo.
- El horario paralelo está dividido en secciones. La última sección es la sección de la cola.
Los nombres de las partes compuestas se pueden ver en la siguiente imagen.
En un PM completo que incluye un OM , Idling Share no se limita a la cola, sino que también comprende el espacio entre tareas e incluso entre tareas.
6.3 Eficiencias
Los modelos presentados anteriormente permiten cuantificar la tasa de utilización de los trabajadores. Podemos distinguir:
- Eficiencia de distribución (DE) : calculada con ayuda de un DM (o un método simplificado para escenario denso ).
- Eficiencia de paralelización (PE) : calculada con la ayuda de un PM calibrado (predicción) o calculada a partir de metadatos de cálculos reales.
Es importante tener en cuenta que las eficiencias calculadas no se correlacionan automáticamente con un cálculo general más rápido para un problema de paralelización determinado. La utilización del trabajador en este contexto solo distingue entre un trabajador que tiene una tarea iniciada pero no terminada y un trabajador que no tiene una tarea tan "abierta". Eso significa que no se registra la posible inactividad durante el período de tiempo de un taskel .
Todas las eficiencias mencionadas anteriormente se obtienen básicamente mediante el cálculo del cociente de la división Ocupado Compartido / Horario Paralelo . La diferencia entre DE y PE viene con el Ocupado ocupando una porción más pequeña del Programa Paralelo general para el MP extendido. .
Esta respuesta solo discutirá un método simple para calcular DE para el escenario denso. Esto es lo suficientemente adecuado para comparar diferentes algoritmos de tamaño de trozo, ya que ...
- ... el DM es la parte del PM , que cambia con los diferentes algoritmos de tamaño de trozo empleados.
- ... el escenario denso con duraciones de cálculo iguales por grupo de tareas representa un "estado estable", por lo que estos períodos de tiempo se eliminan de la ecuación. Cualquier otro escenario conduciría a resultados aleatorios, ya que el orden de las tareas sería importante.
6.3.1 Eficiencia de distribución absoluta (ADE)
Esta eficiencia básica se puede calcular en general dividiendo la participación ocupada entre todo el potencial del horario paralelo :
Eficiencia de distribución absoluta (ADE) = Participación ocupada / Programa paralelo
Para el escenario denso , el código de cálculo simplificado se ve así:
def calc_ade(n_workers, len_iterable, n_chunks, chunksize, last_chunk):
"""Calculate Absolute Distribution Efficiency (ADE).
`len_iterable` is not used, but contained to keep a consistent signature
with `calc_rde`.
"""
if n_workers == 1:
return 1
potential = (
((n_chunks // n_workers + (n_chunks % n_workers > 1)) * chunksize)
+ (n_chunks % n_workers == 1) * last_chunk
) * n_workers
n_full_chunks = n_chunks - (chunksize > last_chunk)
taskels_in_regular_chunks = n_full_chunks * chunksize
real = taskels_in_regular_chunks + (chunksize > last_chunk) * last_chunk
ade = real / potential
return ade
Si no hay cuota inactiva , la cuota ocupada será igual a la programación paralela , por lo que obtenemos un ADE del 100%. En nuestro modelo simplificado, este es un escenario en el que todos los procesos disponibles estarán ocupados durante todo el tiempo necesario para procesar todas las tareas. En otras palabras, todo el trabajo se paraleliza efectivamente al 100 por ciento.
Pero, ¿por qué sigo refiriéndome a la educación física como educación física absoluta aquí?
Para comprender eso, tenemos que considerar un caso posible para el tamaño de chunksize (cs) que garantiza la máxima flexibilidad de programación (también, el número de montañeses que puede haber. ¿Coincidencia?):
__________________________________ ~ UNO ~ __________________________________
Si, por ejemplo, tenemos cuatro procesos de trabajo y 37 tareas, habrá trabajadores inactivos incluso con chunksize=1
, solo porque n_workers=4
no es un divisor de 37. El resto de la división 37/4 es 1. Esta única tarea restante tendrá que ser procesado por un solo trabajador, mientras que los tres restantes están inactivos.
Del mismo modo, todavía habrá un trabajador inactivo con 39 tareas, como puede ver en la imagen de abajo.
Cuando compare el Programa paralelo superior para chunksize=1
con la versión siguiente para chunksize=3
, notará que el Programa paralelo superior es más pequeño, la línea de tiempo en el eje x más corta. Debería ser obvio ahora, cómo los fragmentos más grandes de forma inesperada también pueden conducir a un aumento de los tiempos de cálculo generales, incluso para los escenarios densos .
Pero, ¿por qué no usar la longitud del eje x para los cálculos de eficiencia?
Porque los gastos generales no están contenidos en este modelo. Será diferente para ambos tamaños de trozos, por lo que el eje x no es realmente comparable directamente. La sobrecarga aún puede conducir a un tiempo total de cálculo más largo, como se muestra en el caso 2 de la figura siguiente.
6.3.2 Eficiencia de distribución relativa (RDE)
El valor de ADE no contiene la información si es posible una mejor distribución de las tareas con chunksize establecido en 1. Mejor aquí todavía significa una participación inactiva más pequeña .
Para obtener un valor DE ajustado por la máxima DE posible , tenemos que dividir el ADE considerado entre el ADE que obtenemos chunksize=1
.
Eficiencia de distribución relativa (RDE) = ADE_cs_x / ADE_cs_1
Así es como se ve esto en el código:
def calc_rde(n_workers, len_iterable, n_chunks, chunksize, last_chunk):
"""Calculate Relative Distribution Efficiency (RDE)."""
ade_cs1 = calc_ade(
n_workers, len_iterable, n_chunks=len_iterable,
chunksize=1, last_chunk=1
)
ade = calc_ade(n_workers, len_iterable, n_chunks, chunksize, last_chunk)
rde = ade / ade_cs1
return rde
RDE , como se define aquí, en esencia es un cuento sobre la cola de un Programa Paralelo . RDE está influenciado por el tamaño de trozo máximo efectivo contenido en la cola. (Esta cola puede tener la longitud del eje x chunksize
o last_chunk
). Esto tiene la consecuencia de que el RDE converge naturalmente al 100% (uniforme) para todo tipo de "apariencia de cola" como se muestra en la figura siguiente.
Un RDE bajo ...
- es un fuerte indicio de potencial de optimización.
- naturalmente, se vuelve menos probable para iterables más largos, porque la porción de cola relativa de la programación paralela general se reduce.
Encuentre la Parte II de esta respuesta aquí .
4
Es arbitrario y todo el cálculo de chunksize es heurístico. El factor relevante es cuánto puede variar su tiempo de procesamiento real. Un poco más sobre esto aquí hasta que tenga tiempo para una respuesta si todavía es necesario.