Super Carga tus Sistemas de ML en 4 Sencillos Pasos

Maximiza el Rendimiento de tus Sistemas de Aprendizaje Automático en 4 Pasos Sencillos

Imagen generada con DALL.E-3

¡Bienvenido a la montaña rusa de la optimización de ML! En esta publicación te guiaré a través de mi proceso para optimizar cualquier sistema de ML para un entrenamiento y una inferencia ultrarrápidos en 4 simples pasos.

Imagina esto: Finalmente te asignan un nuevo y genial proyecto de ML en el que entrenas a tu agente para contar cuántos hot dogs hay en una foto, ¡el éxito de lo cual podría hacer que tu empresa gane decenas de dólares!

Obtienes el último y destacado modelo de detección de objetos implementado en tu framework favorito que tiene muchas estrellas en GitHub, ejecutas algunos ejemplos de prueba y después de una hora o algo así, está detectando hot dogs como un estudiante quebrado en su tercer año repetitivo de la universidad, la vida es buena.

Los siguientes pasos son obvios, queremos escalarlo a problemas más difíciles, esto significa más datos, un modelo más grande y, por supuesto, un tiempo de entrenamiento más largo. Ahora estás viendo días de entrenamiento en lugar de horas. Eso está bien, aunque has estado ignorando al resto de tu equipo durante 3 semanas y probablemente deberías dedicar un día a revisar el código acumulado y los correos electrónicos pasivo-agresivos.

Regresas un día después de sentirte bien por las críticas perspicaces y absolutamente necesarias que dejaste en las solicitudes de fusión de tus colegas, solo para descubrir que tu rendimiento se desplomó y se estrelló después de una sesión de entrenamiento de 15 horas (el karma actúa rápido).

Los días siguientes se convierten en un torbellino de pruebas y experimentos, donde cada idea potencial tarda más de un día en ejecutarse. Rápidamente, estos comienzan a acumular cientos de dólares en costos de computación, todo lleva a la gran pregunta: ¿Cómo podemos hacer esto más rápido y más barato?

¡Bienvenido a la montaña rusa emocional de la optimización de ML! Aquí hay un proceso de 4 pasos sencillo para cambiar las tornas a tu favor:

  1. Evaluar el rendimiento
  2. Simplificar
  3. Optimizar
  4. Repetir

Este es un proceso iterativo, y habrá muchas veces en las que repetirás algunos pasos antes de pasar al siguiente, por lo que es menos un sistema de 4 pasos y más un conjunto de herramientas, pero 4 pasos suenan mejor.

1 — Evaluar el rendimiento

“Mide dos veces, corta una vez” — Alguien sabio.

Lo primero (y probablemente lo segundo) que siempre debes hacer es perfilar tu sistema. Esto puede ser algo tan simple como medir el tiempo que tarda en ejecutarse un bloque de código específico, o algo tan complejo como realizar un seguimiento completo del rendimiento. Lo que importa es que tengas suficiente información para identificar los cuellos de botella en tu sistema. Realizo varias evaluaciones dependiendo de en qué etapa nos encontremos en el proceso y, por lo general, lo divido en 2 tipos: evaluaciones de alto nivel y evaluaciones de bajo nivel.

Evaluaciones de Alto Nivel

Esto es lo que mostrarás a tu jefe en la reunión semanal de “¿Qué tan jodidos estamos?” y querrás que estas métricas sean parte de cada ejecución. Te darán una idea general de cómo está funcionando tu sistema en términos de rendimiento.

Lotes por segundo — ¿con qué rapidez estamos procesando cada uno de nuestros lotes? esto debería ser lo más alto posible

Pasos por segundo — (específico de RL) ¿con qué rapidez avanzamos en nuestro entorno para generar nuestros datos? Debería ser lo más alto posible. Aquí hay algunas interacciones complicadas entre el tiempo de paso y los lotes de entrenamiento que no entraré en detalle aquí.

Utilización de la GPU — ¿cuánto de tu GPU se está utilizando durante el entrenamiento? Esto debería ser constantemente cercano al 100%, de lo contrario, tendrás tiempo inactivo que se puede optimizar.

Utilización de la CPU — ¿cuánto de tus CPUs se están utilizando durante el entrenamiento? Nuevamente, esto debería ser lo más cercano posible al 100%.

FLOPS — operaciones de punto flotante por segundo, esto te brinda una vista de cómo estás utilizando eficazmente tu hardware total.

Evaluaciones de Bajo Nivel

Usando las métricas antes mencionadas, puedes comenzar a profundizar en dónde podría estar tu cuello de botella. Una vez que las tengas, querrás empezar a analizar métricas y perfiles más detallados.

Análisis de Tiempo — Este es el experimento más simple y a menudo más útil, para ejecutarlo se utilizan herramientas de análisis de rendimiento como cprofiler. Esto ofrece una visión general de los tiempos de ejecución de cada uno de tus componentes en su conjunto o puede examinar los tiempos de ejecución de componentes específicos.

Perfilado de memoria – Otro elemento básico del conjunto de herramientas de optimización. Los sistemas grandes requieren mucha memoria, por lo que debemos asegurarnos de no desperdiciar ninguna. Herramientas como memory-profiler te ayudarán a identificar dónde se está consumiendo la RAM de tu sistema.

Perfilado de modelo – Herramientas como Tensorboard vienen con excelentes herramientas de perfilado para ver qué está afectando el rendimiento de tu modelo.

Perfilado de red – La carga de red es una causa común de estrangulamiento del sistema. Hay herramientas como wireshark que te ayudarán a perfilar esto, pero para ser sincero, nunca lo uso. En su lugar, prefiero hacer perfilado de tiempo en mis componentes y medir el tiempo total que lleva cada componente y luego aislar cuánto tiempo viene del propio E/S de red.

¡Asegúrate de consultar este excelente artículo sobre el perfilado en Python de RealPython para obtener más información!

2 – Simplifica

Una vez que has identificado un área en tu perfilado que necesita ser optimizada, simplifícala. Elimina todo excepto esa parte. Reduce el sistema a partes más pequeñas hasta que llegues al estrangulamiento. No tengas miedo de perfilar mientras simplificas, esto asegurará que estás yendo en la dirección correcta a medida que iteras. Repite esto hasta encontrar el estrangulamiento.

Consejos

  • Sustituye otros componentes con stubs y funciones ficticias que solo proporcionen datos esperados.
  • Simula funciones pesadas con funciones de sleep o cálculos ficticios.
  • Utiliza datos ficticios para eliminar la sobrecarga de generación y procesamiento de datos.
  • Comienza con versiones locales y de un solo proceso de tu sistema antes de pasar a la distribución.
  • Simula múltiples nodos y actores en una sola máquina para eliminar la sobrecarga de red.
  • Encuentra el rendimiento máximo teórico para cada parte del sistema. Si todos los demás estrangulamientos en el sistema desaparecieran, excepto este componente, ¿cuál sería nuestro rendimiento esperado?
  • ¡Vuelve a perfilar! Cada vez que simplifiques el sistema, vuelve a ejecutar tu perfilado.

Preguntas

Una vez que nos hemos centrado en el estrangulamiento, hay algunas preguntas clave que queremos responder

¿Cuál es el rendimiento máximo teórico de este componente?

Si hemos aislado suficientemente el componente que está provocando el estrangulamiento, deberíamos poder responder esto.

¿Qué tan lejos estamos del máximo?

Este margen de optimización nos informará sobre qué tan optimizado está nuestro sistema. Ahora, podría ser que haya otras restricciones una vez que introducimos el componente nuevamente en el sistema, y eso está bien, pero es crucial al menos ser consciente de cuál es la brecha.

¿Existe un estrangulamiento más profundo?

Siempre hazte esta pregunta, tal vez el problema sea más profundo de lo que inicialmente pensaste, en cuyo caso, debemos repetir el proceso de benchmarking y simplificación.

3 – Optimiza

De acuerdo, supongamos que hemos identificado el mayor estrangulamiento, ahora llegamos a la parte divertida, ¿cómo mejoramos las cosas? Por lo general, hay 3 áreas en las que debemos buscar posibles mejoras

  1. Cálculo
  2. Comunicación
  3. Memoria

Cálculo

Para reducir los estrangulamientos de cálculo, debemos ser lo más eficientes posible con los datos y algoritmos con los que estamos trabajando. Esto obviamente depende del proyecto y hay una gran cantidad de cosas que se pueden hacer, pero veamos algunas buenas reglas generales.

Paralelizar – asegúrate de llevar a cabo tanto trabajo como sea posible en paralelo. Esta es la primera gran victoria en el diseño de tu sistema que puede tener un impacto masivo en el rendimiento. Considera métodos como vectorización, agrupación de tareas, multi-hilos y multi-procesamiento.

Caché – precalcula y reutiliza cálculos cuando sea posible. Muchos algoritmos pueden aprovechar la reutilización de valores precalculados y ahorrar recursos críticos para cada paso de entrenamiento.

Externalización – Todos sabemos que Python no se caracteriza por su velocidad. Afortunadamente, podemos externalizar los cálculos críticos a lenguajes de nivel inferior como C/C++.

Escalar el hardware – Esto es un poco evasivo, pero cuando todo lo demás falla, siempre podemos añadir más ordenadores al problema.

Comunicación

Cualquier ingeniero experimentado te dirá que la comunicación es fundamental para entregar un proyecto exitoso, y por eso, nos referimos a la comunicación dentro de nuestro sistema (Dios no quiera tener que hablar con nuestros colegas). Algunas reglas básicas son:

Sin tiempo de inactividad – Debes utilizar todo el hardware disponible en todo momento, de lo contrario, estarás dejando ganancias de rendimiento sobre la mesa. Esto se debe, generalmente, a las complicaciones y sobrecarga de la comunicación en todo tu sistema.

Mantenerlo local – Mantén todo en una sola máquina el mayor tiempo posible antes de pasar a un sistema distribuido. Esto mantiene tu sistema simple y evita la sobrecarga de comunicación de un sistema distribuido.

Async > Sync – Identifica cualquier tarea que se pueda realizar de forma asíncrona. Esto ayudará a aliviar el costo de la comunicación al mantener el trabajo en movimiento mientras se mueven los datos.

Evitar mover datos – ¡Mover datos de la CPU a la GPU o de un proceso a otro es costoso! Hazlo lo menos posible o reduce el impacto realizándolo de forma asíncrona.

Memoria

Por último, pero no menos importante, está la memoria. Muchas de las áreas mencionadas anteriormente pueden ayudar a aliviar tus cuellos de botella, ¡pero puede que no sea posible si no tienes memoria disponible! Veamos algunas cosas a considerar:

Tipos de datos – Manténlos lo más pequeños posible para reducir el costo de la comunicación y la memoria. Además, con los aceleradores modernos, también reducirá la carga computacional.

Caching – Al igual que reducir la carga computacional, un almacenamiento en caché inteligente puede ayudarte a ahorrar memoria. Sin embargo, asegúrate de que los datos en caché se utilicen con la suficiente frecuencia como para justificar el almacenamiento en caché.

Pre-asignación – No es algo a lo que estemos acostumbrados en Python, pero ser estricto con la pre-asignación de memoria significa que sabrás exactamente cuánta memoria necesitas, reducirás el riesgo de fragmentación y, si puedes escribir en memoria compartida, ¡reducirás la comunicación entre tus procesos!

Recolección de basura – Afortunadamente, Python se encarga en su mayoría de esto por nosotros, pero es importante asegurarse de no mantener valores grandes en el alcance sin necesitarlos o, peor aún, tener una dependencia circular que pueda provocar una fuga de memoria.

Sé indolente – Evalúa las expresiones solo cuando sea necesario. En Python, puedes usar expresiones generadoras en lugar de comprensiones de listas para operaciones que se pueden evaluar de forma perezosa.

4 – Repetir

Entonces, ¿cuándo hemos terminado? Bueno, eso realmente depende de tu proyecto, de los requisitos y de cuánto tiempo pasa antes de que tu cordura disminuya.

A medida que elimines los cuellos de botella, obtendrás rendimientos decrecientes por el tiempo y esfuerzo que inviertas en optimizar tu sistema. Durante el proceso, deberás decidir cuándo es suficientemente bueno. Recuerda, la velocidad es un medio para un fin, no te quedes atrapado en el juego de optimizar solo por hacerlo. Si no va a tener un impacto en los usuarios, probablemente sea hora de pasar a otra cosa.

Conclusión

Construir sistemas de ML a gran escala es DIFÍCIL. Es como jugar a un juego retorcido de “¿Dónde está Wally?” mezclado con Dark Souls. Si logras encontrar el problema, tendrás que intentarlo varias veces para superarlo y terminarás pasando la mayor parte del tiempo recibiendo golpes, preguntándote “¿Por qué estoy haciendo esto un viernes por la noche?”. Tener un enfoque simple y estructurado puede ayudarte a superar esa batalla final del jefe y saborear esos dulces, dulces FLOPs máximos teóricos.

ML en Acción | Donal Byrne | Substack

El boletín sobre Aprendizaje Automático que ofrece consejos no solicitados, información práctica y lecciones aprendidas en este campo en rápido desarrollo…

donalbyrne.substack.com

We will continue to update Zepes; if you have any questions or suggestions, please contact us!

Share:

Was this article helpful?

93 out of 132 found this helpful

Discover more

Inteligencia Artificial

Papel de los Contratos de Datos en la Canalización de Datos

¿Qué son los Contratos de Datos? Un contrato de datos es un acuerdo o conjunto de reglas que define cómo debe estruct...

Investigación

Un sistema robótico de cuatro patas para jugar al fútbol en diversos terrenos.

DribbleBot puede maniobrar un balón de fútbol en terrenos como arena, grava, barro y nieve, utilizando el aprendizaje...

Inteligencia Artificial

Mirando hacia adentro

La biosensibilidad lleva los diagnósticos médicos a un nivel más profundo.