Cargue sus sistemas de aprendizaje automático en 4 sencillos pasos |  de Donal Byrne |  octubre de 2023

¡Bienvenido a la montaña rusa de la optimización del aprendizaje automático! Esta publicación lo guiará a través de mi proceso para optimizar cualquier sistema de aprendizaje automático para un entrenamiento e inferencia ultrarrápidos en 4 simples pasos.

Imagínese esto: finalmente lo asignan a un nuevo e interesante proyecto de aprendizaje automático en el que está entrenando a su agente para que cuente cuántos hot dogs hay en una foto, ¡cuyo éxito podría generarle a su empresa decenas de dólares!

Obtienes el último modelo de detección de objetos de moda implementado en tu marco favorito que tiene muchas estrellas de GitHub, ejecutas algunos ejemplos de juguetes y después de aproximadamente una hora estás seleccionando perritos calientes como un estudiante arruinado en su tercer año repetido de universidad, la vida es buena.

Los próximos pasos son obvios, queremos ampliarlo a algunos problemas más difíciles, esto significa más datos, un modelo más grande y, por supuesto, más tiempo de entrenamiento. Ahora estás mirando días de entrenamiento en lugar de horas. Sin embargo, está bien, has estado ignorando al resto de tu equipo durante 3 semanas y probablemente deberías pasar un día revisando la acumulación de revisiones de código y correos electrónicos pasivo-agresivos que se han acumulado.

Regresas un día después, después de sentirte bien por los detalles reveladores y absolutamente necesarios que dejaste en el MR de tus colegas, solo para encontrar que tu rendimiento se derrumbó y se estrelló después de un período de entrenamiento de 15 horas (el karma funciona rápido).

Los días siguientes se transforman en un torbellino de ensayos, pruebas y experimentos, y cada idea potencial tarda más de un día en ejecutarse. Estos rápidamente comienzan a acumular cientos de dólares en costos de computación, lo que 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 del aprendizaje automático! Aquí hay un proceso sencillo de 4 pasos para cambiar el rumbo a su favor:

  1. Punto de referencia
  2. Simplificar
  3. Optimizar
  4. Repetir

Este es un proceso iterativo y habrá muchas ocasiones 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 una caja de herramientas, pero 4 pasos suena mejor.

“Mida dos veces, corte una vez” — Alguien sabio.

Lo primero (y probablemente lo segundo) que siempre debe hacer es perfilar su sistema. Esto puede ser algo tan simple como cronometrar el tiempo que lleva ejecutar un bloque de código específico, o tan complejo como realizar un seguimiento completo del perfil. Lo que importa es que tenga suficiente información para identificar los cuellos de botella en su sistema. Realizo múltiples evaluaciones comparativas dependiendo de dónde nos encontremos en el proceso y normalmente las divido en 2 tipos: evaluación comparativa de alto nivel y evaluación comparativa de bajo nivel.

Nivel alto

Este es el tipo de cosas que le mostrarás a tu jefe en el programa semanal “¿Qué tan jodidos estamos?” reunión y querría estas métricas como parte de cada ejecución. Estos le darán una idea de alto nivel del rendimiento de su sistema.

Lotes por segundo – ¿Qué tan rápido estamos terminando cada uno de nuestros lotes? esto debería ser lo más alto posible

Pasos por segundo – (Específico de RL) la rapidez con la que avanzamos por nuestro entorno para generar nuestros datos debe ser lo más alta posible. Hay algunas interacciones complicadas entre el tiempo de paso y los lotes de trenes en las que no entraré aquí.

Utilidad de GPU — ¿Cuánto de tu GPU se utiliza durante el entrenamiento? Esto debería ser consistentemente cercano al 100%; de lo contrario, tendrá un tiempo de inactividad que se puede optimizar.

Utilidad de CPU — ¿Qué cantidad de CPU se utiliza durante el entrenamiento? Nuevamente, esto debería ser lo más cercano posible al 100%.

fracasos — operaciones de punto flotante por segundo, esto le brinda una vista de la eficacia con la que utiliza todo su hardware.

Nivel bajo

Utilizando las métricas anteriores, podrá comenzar a analizar más a fondo dónde podría estar su cuello de botella. Una vez que los tenga, querrá comenzar a analizar métricas y perfiles más detallados.

Perfil de tiempo — Este es el experimento más sencillo y, a menudo, más útil de realizar. Herramientas de creación de perfiles como perfilador se puede utilizar para obtener una vista panorámica de la sincronización de cada uno de sus componentes en su conjunto o puede observar la sincronización de componentes específicos.

Perfil de memoria — Otro elemento básico de la caja de herramientas de optimización. Los sistemas grandes requieren mucha memoria, por lo que debemos asegurarnos de no desperdiciarla. herramientas como perfilador de memoria le ayudará a determinar dónde su sistema está consumiendo su RAM.

Perfilado de modelos – Herramientas como tablero tensor vienen con excelentes herramientas de creación de perfiles para observar qué está consumiendo su rendimiento dentro de su modelo.

Perfiles de red — La carga de la red es un culpable común de los cuellos de botella de su sistema. Hay herramientas como tiburón de alambre para ayudarte a perfilar esto, pero para ser honesto, nunca lo uso. En cambio, prefiero crear perfiles de tiempo en mis componentes y medir el tiempo total que lleva dentro de mi componente y luego aislar cuánto tiempo proviene de la E/S de la red misma.

Asegúrese de consultar este excelente artículo sobre creación de perfiles en Python de RealPython ¡para más información!

Una vez que haya identificado un área en su perfil que necesita optimizarse, simplifíquela. Recorta todo lo demás excepto esa parte. Continúe reduciendo el sistema a partes más pequeñas hasta llegar al cuello de botella. No tenga miedo de crear un perfil a medida que simplifica; esto garantizará que vaya en la dirección correcta a medida que itera. Sigue repitiendo esto hasta que encuentres el cuello de botella.

Consejos

  • Reemplace otros componentes con códigos auxiliares y funciones simuladas que solo proporcionen los datos esperados.
  • Simule funciones pesadas con sleep funciones o cálculos ficticios.
  • Utilice datos ficticios para eliminar la sobrecarga de generación y procesamiento de datos.
  • Comience con versiones locales de proceso único de su sistema antes de pasar a las distribuidas.
  • Simule múltiples nodos y actores en una sola máquina para eliminar la sobrecarga de la red.
  • Encuentre el rendimiento máximo teórico para cada parte del sistema. Si todos los demás cuellos de botella del sistema desaparecieran excepto este componente, ¿cuál es nuestro rendimiento esperado?
  • ¡Perfil otra vez! Cada vez que simplifique el sistema, vuelva a ejecutar su perfil.

Preguntas

Una vez que nos hemos centrado en el cuello de botella, 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 cuello de botella, entonces deberíamos poder responder a esta pregunta.

¿A qué distancia estamos del máximo?

Esta brecha de optimización nos informará sobre qué tan optimizado está nuestro sistema. Ahora bien, podría darse el caso de que existan otras restricciones estrictas una vez que volvamos a introducir el componente en el sistema y eso está bien, pero es crucial al menos ser conscientes de cuál es la brecha.

¿Existe un cuello de botella más profundo?

Pregúntese siempre esto, tal vez el problema sea más profundo de lo que pensaba inicialmente, en cuyo caso repetimos el proceso de evaluación comparativa y simplificación.

Bien, digamos que hemos identificado el mayor cuello de botella, ahora llegamos a la parte divertida: ¿cómo mejoramos las cosas? Generalmente hay 3 áreas que deberíamos buscar para posibles mejoras.

  1. Calcular
  2. Comunicación
  3. Memoria

Calcular

Para reducir los cuellos de botella en la computación, debemos buscar ser lo más eficientes posible con los datos y algoritmos con los que trabajamos. Obviamente, esto es específico del proyecto y hay una gran cantidad de cosas que se pueden hacer, pero veamos algunas buenas reglas generales.

Paralelizando — asegúrese de realizar la mayor cantidad de trabajo posible en paralelo. Esta es la primera gran victoria en el diseño de su sistema que puede afectar enormemente el rendimiento. Observe métodos como la vectorización, el procesamiento por lotes, los subprocesos múltiples y el procesamiento múltiple.

Almacenamiento en caché — precalcule y reutilice los cálculos siempre que pueda. Muchos algoritmos pueden aprovechar la reutilización de valores calculados previamente y guardar cálculos críticos para cada uno de sus pasos de entrenamiento.

Descarga – todos sabemos que Python no es conocido por su velocidad. Afortunadamente, podemos descargar cálculos críticos a lenguajes de nivel inferior como C/C++.

Escalado de hardware — Esto es una especie de evasión, pero cuando todo lo demás falla, ¡siempre podemos arrojar más computadoras al problema!

Comunicación

Cualquier ingeniero experimentado le dirá que la comunicación es clave para entregar un proyecto exitoso y con eso, por supuesto, nos referimos a la comunicación dentro de nuestro sistema (Dios no quiera que alguna vez tengamos que hablar con nuestros colegas). Algunas buenas reglas generales son:

Sin tiempo de inactividad — Todo el hardware disponible debe utilizarse en todo momento; de lo contrario, dejarás las ganancias de rendimiento sobre la mesa. Esto generalmente se debe a complicaciones y sobrecargas de comunicación en todo el sistema.

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

Asíncrono > Sincronizar — Identifique todo lo que se pueda hacer de forma asincrónica; esto ayudará a reducir el costo de la comunicación al mantener el trabajo en movimiento mientras se mueven los datos.

Evite mover datos — ¡Mover datos de la CPU a la GPU o de un proceso a otro es caro! Haga la menor cantidad de esto posible o reduzca el impacto llevándolo a cabo de forma asincrónica.

Memoria

Por último, pero no menos importante, está la memoria. Muchas de las áreas mencionadas anteriormente pueden ser útiles para aliviar su cuello de botella, ¡pero puede que no sea posible si no tiene memoria disponible! Veamos algunas cosas a considerar.

Tipos de datos — manténgalos lo más pequeños posible, lo que ayudará a reducir el costo de la comunicación y la memoria, y con los aceleradores modernos, también reducirá la computación.

Almacenamiento en caché — De manera similar a reducir la computación, el almacenamiento en caché inteligente puede ayudarlo a ahorrar memoria. Sin embargo, asegúrese de que los datos almacenados en caché se utilicen con la frecuencia suficiente para justificar el almacenamiento en caché.

Preasignar — no es algo a lo que estamos acostumbrados en Python, pero ser estricto con la preasignación de memoria puede significar que sabes exactamente cuánta memoria necesitas, reduce el riesgo de fragmentación y si puedes escribir en la memoria compartida, reducirás la comunicación entre tus procesos!

Recolección de basura — afortunadamente, Python maneja la mayor parte de esto por nosotros, pero es importante asegurarse de no mantener valores grandes dentro del alcance sin necesitarlos o, peor aún, tener una dependencia circular que pueda causar una pérdida de memoria.

Ser flojo — Evaluar expresiones sólo cuando sea necesario. En Python, puede utilizar expresiones generadoras en lugar de listas por comprensión para operaciones que se pueden evaluar de forma diferida.

Entonces, ¿cuándo terminamos? Bueno, eso realmente depende de tu proyecto, cuáles son los requisitos y cuánto tiempo lleva antes de que tu menguante cordura finalmente se rompa.

A medida que elimine los cuellos de botella, obtendrá rendimientos decrecientes del tiempo y esfuerzo que dedica a optimizar su sistema. A medida que avanza en el proceso, debe decidir cuándo lo bueno es lo suficientemente bueno. Recuerde, la velocidad es un medio para lograr un fin, no se deje atrapar en la trampa de optimizar por el simple hecho de hacerlo. Si no va a tener un impacto en los usuarios, entonces probablemente sea hora de seguir adelante.

Construir sistemas de aprendizaje automático a gran escala es DIFÍCIL. Es como jugar un retorcido juego de “¿Dónde está Waldo” cruzado con Dark Souls? Si logras encontrar el problema, tendrás que hacer varios intentos para solucionarlo y terminarás pasando la mayor parte del tiempo pateándote el trasero, preguntándote “¿Por qué paso el viernes por la noche haciendo esto?”. Tener un enfoque simple y basado en principios puede ayudarte a superar la batalla final contra el jefe y probar esos dulces FLOP máximos teóricos.