8.3. Paralelismo, gestión de recursos y configuración

8.3.1. Paralelismo

Algunos estimadores y utilidades de scikit-learn pueden paralelizar operaciones costosas utilizando varios núcleos de CPU, gracias a los siguientes componentes:

  • a través de la biblioteca joblib. En este caso el número de hilos o procesos se puede controlar con el parámetro n_jobs.

  • a través de OpenMP, utilizado en código C o Cython.

Además, algunas de las rutinas de numpy que son usadas internamente por scikit-learn también pueden ser paralelizadas si numpy es instalado con bibliotecas numéricas específicas como MKL, OpenBLAS, o BLIS.

Describimos estos 3 escenarios en las siguientes subsecciones.

8.3.1.1. Paralelismo basado en Joblib

Cuando la implementación subyacente utiliza joblib, el número de trabajadores (hilos o procesos) que se generan en paralelo puede controlarse mediante el parámetro n_jobs.

Nota

Dónde (y cómo) ocurre la paralelización en los estimadores está actualmente mal documentado. ¡Por favor, ayúdanos a mejorar nuestra documentación y a resolver el problema 14228!

Joblib es capaz de soportar tanto el multiprocesamiento como el multihilo. El hecho de que joblib elija generar un hilo o un proceso depende del backend que esté utilizando.

Scikit-learn generalmente confía en el backend loky, que es el backend por defecto de joblib. Loky es un backend de multiprocesamiento. Cuando se hace multiprocesamiento, para evitar duplicar la memoria en cada proceso (lo que no es razonable con grandes conjuntos de datos), joblib creará un memmap que todos los procesos pueden compartir, cuando los datos son mayores de 1MB.

En algunos casos específicos (cuando el código que se ejecuta en paralelo libera el GIL), scikit-learn indicará a joblib que es preferible un backend multihilo.

Como usuario, puede controlar el backend que joblib utilizará (independientemente de lo que scikit-learn recomiende) utilizando un gestor de contexto:

from joblib import parallel_backend

with parallel_backend('threading', n_jobs=2):
    # Your scikit-learn code here

Consulta la documentación de joblib’s para más detalles.

En la práctica, que el paralelismo sea útil para mejorar el tiempo de ejecución depende de muchos factores. Suele ser una buena idea experimentar en lugar de asumir que aumentar el número de trabajadores es siempre algo bueno. En algunos casos puede ser muy perjudicial para el rendimiento ejecutar varias copias de algunos estimadores o funciones en paralelo (ver la sobresuscripción más adelante).

8.3.1.2. Paralelismo basado en OpenMP

OpenMP se utiliza para paralelizar código escrito en Cython o C, basándose exclusivamente en el multihilo. Por defecto (y a menos que joblib intente evitar la sobresuscripción), la implementación utilizará tantos hilos como sea posible.

Puedes controlar el número exacto de hilos que se utilizan mediante la variable de entorno OMP_NUM_THREADS:

OMP_NUM_THREADS=4 python my_script.py

8.3.1.3. Rutinas paralelas Numpy de bibliotecas numéricas

Scikit-learn depende en gran medida de NumPy y SciPy, que llaman internamente a rutinas de álgebra lineal multihilo implementadas en bibliotecas como MKL, OpenBLAS o BLIS.

El número de hilos utilizados por las bibliotecas OpenBLAS, MKL o BLIS puede establecerse mediante las variables de entorno MKL_NUM_THREADS, OPENBLAS_NUM_THREADS y BLIS_NUM_THREADS.

Ten en cuenta que scikit-learn no tiene ningún control directo sobre estas implementaciones. Scikit-learn depende únicamente de Numpy y Scipy.

Nota

En el momento de escribir este artículo (2019), los paquetes NumPy y SciPy distribuidos en pypi.org (utilizados por pip) y en el canal de conda-forge están enlazados con OpenBLAS, mientras que los paquetes de conda distribuidos en el canal «defaults» de anaconda.org están enlazados por defecto con MKL.

8.3.1.4. Sobresuscripción: creación de demasiados hilos

Por lo general, se recomienda evitar el uso de un número de procesos o hilos significativamente mayor que el número de CPUs de una máquina. El exceso de hilos ocurre cuando un programa ejecuta demasiados hilos al mismo tiempo.

Imagina que tienes una máquina con 8 CPUs. Considera un caso en el que estás ejecutando una GridSearchCV (paralelizada con joblib) con n_jobs=8 sobre un HistGradientBoostingClassifier (paralelizado con OpenMP). Cada instancia de HistGradientBoostingClassifier generará 8 hilos (ya que tienes 8 CPUs). Esto supone un total de 8 * 8 = 64 hilos, lo que conlleva una sobresuscripción de los recursos físicos de la CPU y una sobrecarga de programación.

La sobresuscripción puede surgir exactamente de la misma manera con las rutinas paralelas de MKL, OpenBLAS o BLIS que están anidadas en las llamadas de joblib.

A partir de joblib >= 0.14, cuando se utiliza el backend loky (que es el predeterminado), joblib indicará a sus procesos hijos que limiten el número de hilos que pueden utilizar, para evitar la sobresuscripción. En la práctica, la heurística que utiliza joblib es decirle a los procesos que utilicen max_threads = n_cpus // n_jobs, a través de su correspondiente variable de entorno. Volviendo a nuestro ejemplo anterior, como el backend de joblib de GridSearchCV es loky, cada proceso sólo podrá utilizar 1 hilo en lugar de 8, mitigando así el problema de la sobresuscripción.

Ten en cuenta que:

  • La configuración manual de una de las variables de entorno (OMP_NUM_THREADS, MKL_NUM_THREADS, OPENBLAS_NUM_THREADS, o BLIS_NUM_THREADS) tendrá prioridad sobre lo que intente hacer joblib. El número total de hilos será n_jobs * <LIB>_NUM_THREADS. Ten en cuenta que establecer este límite también afectará a sus cálculos en el proceso principal, que sólo utilizará <LIB>_NUM_THREADS. Joblib expone un gestor de contexto para un control más fino sobre el número de hilos en sus trabajadores (ver la documentación de joblib enlazada más abajo).

  • Joblib es actualmente incapaz de evitar la sobresuscripción en un contexto de multihilo. Sólo puede hacerlo con el backend loky (que genera procesos).

Encontrarás detalles adicionales sobre la mitigación de la sobresuscripción de joblib en la documentación de joblib.

8.3.2. Interruptores de configuración

8.3.2.1. Tiempo de ejecución de Python

sklearn.set_config controla los siguientes comportamientos:

assume_finite

se utiliza para omitir la validación, lo que permite realizar cálculos más rápidos pero puede dar lugar a fallos de segmentación si los datos contienen NaNs.

working_memory

el tamaño óptimo de los arreglos temporales utilizados por algunos algoritmos.

8.3.2.2. Variables de entorno

Estas variables de entorno deben establecerse antes de importar scikit-learn.

SKLEARN_SITE_JOBLIB

Cuando esta variable de entorno se establece en un valor distinto de cero, scikit-learn utiliza el joblib del sitio en lugar de su versión vendida. En consecuencia, joblib debe ser instalado para que scikit-learn se ejecute. Tenga en cuenta que el uso de la joblib del sitio es bajo su propio riesgo: las versiones de scikit-learn y joblib deben ser compatibles. Actualmente, joblib 0.11+ es compatible. Además, los volcados de joblib.Memory podrían ser incompatibles, y podrías perder algunas cachés y tener que volver a descargar algunos conjuntos de datos.

Obsoleto desde la versión 0.21: A partir de la versión 0.21 este parámetro no tiene efecto, se ha eliminado el joblib vendido y se utiliza siempre el joblib de sitio.

SKLEARN_ASSUME_FINITE

Establece el valor predeterminado del argumento assume_finite de sklearn.set_config.

SKLEARN_WORKING_MEMORY

Establece el valor predeterminado del argumento working_memory de sklearn.set_config.

SKLEARN_SEED

Establece la semilla del generador aleatorio global cuando se ejecutan las pruebas, para la reproducibilidad.

SKLEARN_SKIP_NETWORK_TESTS

Cuando esta variable de entorno tiene un valor distinto de cero, se omiten las pruebas que necesitan acceso a la red. Cuando esta variable de entorno no se establece, se omiten las pruebas de red.