Ray para los Curiosos

Decano Wampler

Seguir

Dec 19, 2019 · 10 min de lectura

(Refinamientos, Febrero 8, 2020)

TL;DR Ray es un sistema para la ampliación de las aplicaciones Python a través de clústeres con el mínimo esfuerzo. Este post explica los problemas que resuelve Ray y cómo usarlo.

El Proyecto Ray Local,
El Proyecto Ray, disponible en https://ray.io

Ray (sitio web, GitHub) es un sistema de código abierto para escalar aplicaciones Python de máquinas individuales a clústeres grandes. Su diseño está impulsado por las necesidades únicas de los sistemas de ML/AI de próxima generación, que se enfrentan a varios desafíos únicos, incluidos diversos patrones computacionales, la gestión del estado distribuido y en evolución y el deseo de abordar todas esas necesidades con un esfuerzo de programación mínimo.

Los sistemas de ML/AI típicos requieren diversos patrones computacionales para respaldar la limpieza y preparación de datos, el ajuste de hiperparámetros, la capacitación y el servicio de modelos y otras tareas. El modelo MapReduce original para cargas de trabajo de big data funciona bien para la limpieza y preparación de datos y también para cargas de trabajo de análisis, pero las cargas de trabajo de aprendizaje automático requieren una mezcla de tareas de grano fino a grano grueso, junto con diversos patrones de comunicación entre los componentes. El ajuste de hiperparámetros y la formación de modelos requieren mucha computación, lo que requiere que los recursos del clúster se completen en plazos de tiempo razonables. Ray proporciona la base para crear sistemas y aplicaciones de ML/AI modernos al satisfacer estos diversos requisitos de manera eficiente, con una API mínima e intuitiva.

Se distribuye un segundo desafío, en evolución. En el contexto de ML / AI, el estado distribuido incluye los hiperparámetros, los parámetros del modelo (por ejemplo, y para escenarios de aprendizaje por refuerzo, el estado de las simulaciones (o interacciones con el mundo real) utilizadas para el entrenamiento. A menudo, el estado es mutable, especialmente durante el entrenamiento, por lo que se requieren actualizaciones cuidadosas y seguras para la concurrencia. Una forma posible de manejar la computación distribuida es explotar los sistemas populares «sin servidor», pero ninguno ofrece actualmente facilidades para administrar el estado distribuido y mutable. Los desarrolladores deben recurrir a mantener todo el estado en una base de datos cuando utilizan sistemas sin servicio, pero la base de datos puede ser un cuello de botella y un único punto de falla.

En su lugar, Ray utiliza el popular modelo Actor para proporcionar un mecanismo intuitivo para la administración del estado. Los actores Ray proporcionan un complemento con estado a las tareas Ray, que son apátridas. Este estado es accesible de forma transparente a cualquier otro actor o tarea de Ray a través de una referencia al objeto Python correspondiente (es decir, una instancia de una clase Python). Ray realiza un seguimiento de la ubicación del actor en el clúster, lo que elimina la necesidad de conocer y administrar explícitamente dichas ubicaciones en el código de usuario. La mutación de estado en el actor se maneja de una manera segura para subprocesos, sin la necesidad de primitivas de concurrencia explícitas. Por lo tanto, Ray proporciona una administración de estado distribuida e intuitiva para las aplicaciones, lo que significa que Ray puede ser una excelente plataforma para implementar aplicaciones sin servidor con estado, en general. Además, al comunicarse entre actores o tareas en la misma máquina, el estado se gestiona de forma transparente a través de memoria compartida, con serialización de copia cero entre los actores y las tareas, para un rendimiento óptimo.

Finalmente, debido a que la mayoría de los sistemas de ML/AI están basados en Python, los desarrolladores necesitan una forma de agregar estas capacidades de escalado horizontal con cambios mínimos de código. Un decorador, @ray.remote, marca funciones y clases como unidades lógicas que se pueden crear instancias y ejecutar en un clúster. Ray maneja de forma transparente la mutación de estado, la distribución de estado y la programación intuitiva de tareas dependientes seguras para subprocesos.

La distribución de Ray incluye varias bibliotecas de alto rendimiento dirigidas a aplicaciones de IA, que también motivaron los problemas que impulsaron la creación de Ray. Incluyen RLlib para aprendizaje por refuerzo y Tune para afinación de hiperparámetros. Ambos demuestran las capacidades únicas de Ray. Estas bibliotecas y otras aplicaciones personalizadas escritas con Ray ya se utilizan en muchas implementaciones de producción.

Ray es un proyecto de código abierto iniciado en el RiseLab de UC Berkeley. Actualmente se desarrolla a cualquier escala con importantes contribuciones de muchas otras organizaciones. Los usuarios comerciales de Ray incluyen Ant Financial, JP Morgan, Intel, Microsoft, Ericsson, Skymind, Primer y muchos otros.

Un ejemplo de la API Core Ray

Nota: La lista completa del siguiente ejemplo de código se puede encontrar al final de esta publicación.

Ahora que entendemos las motivaciones y ventajas de Ray, examinemos cómo usaría la API de Ray en sus aplicaciones. Luego veremos más de cerca cómo Ray mejora el rendimiento a través de la paralelización y la distribución. La API de Ray está cuidadosamente diseñada para permitir a los usuarios escalar sus aplicaciones, incluso en un clúster, con cambios de código mínimos.

Considere el ejemplo de un servidor de parámetros, que es un almacén de claves y valores utilizado para entrenar modelos de aprendizaje automático en un clúster. Los valores son los parámetros de un modelo de aprendizaje automático (p. ej. una red neuronal). Las teclas indexan los parámetros del modelo. Si no está familiarizado con los servidores de parámetros, piense en cualquier servicio independiente que pueda necesitar para servir solicitudes de información o datos.

Por ejemplo, en un sistema de recomendación de películas, puede haber una clave por usuario y una clave por película. Para cada usuario y película, hay parámetros específicos del usuario y de la película correspondientes. En una aplicación de modelado de lenguaje, las palabras pueden ser las claves y sus incrustaciones pueden ser los valores.

En su forma más simple, un servidor de parámetros puede tener una sola clave y permitir que todos los parámetros se recuperen y actualicen a la vez.

Aquí hay un ejemplo de un servidor de parámetros tan simple, para una sola matriz NumPy de parámetros. Se implementa como un actor de rayos en menos de 15 líneas de código:

Importación y definición del actor Servidor de parámetros

El decorador @ray.remote define un servicio. Toma la clase Python ordinaria, ParameterServer, y permite que se cree una instancia como un servicio remoto. Debido a que la instancia mantiene el estado (los parámetros actuales, que son mutables), tenemos un servicio con estado.

En este ejemplo, asumimos que una actualización de los parámetros se proporciona como un degradado que se debe agregar al vector de parámetros actual. (Este degradado puede ser un solo número que se agrega a todos los elementos de la matriz o una matriz de degradados.) Diseños más sofisticados son posibles, por supuesto, pero Ray se usaría de la misma manera. Como un ejercicio simple, intente cambiar esto a una implementación de clave-valor (diccionario).

Un servidor de parámetros normalmente existe como un proceso o servicio remoto. Los clientes interactúan con él a través de llamadas a procedimientos remotos. Para crear una instancia del servidor de parámetros como actor remoto, realizamos los siguientes pasos en el indicador interactivo de Python. (Asumiremos que ya definió la clase ParameterServer en la misma sesión). Primero, tienes que empezar con Ray. Al usar un clúster, debe pasar parámetros opcionales al método init() para especificar la ubicación del clúster:

Sesión interactiva de Python: iniciar Ray

A continuación, cree una instancia ParameterServer para una matriz de 10 parámetros:

Construir una instancia de actor de servidor de parámetros

En lugar de llamar a ParameterServer(10) para construir una instancia, como lo haría con una instancia de Python normal, utiliza el método remote(), que fue agregado a la clase por el decorador @ray.remote. Pasa los mismos argumentos que pasaría al constructor normal. Tenga en cuenta que se construye un objeto actor.

Del mismo modo, para invocar métodos en el actor, se usa remote() anexado al nombre del método original, pasando los mismos argumentos que se pasarían al método original:

Llamar a un método remoto

Las invocaciones al método actor devuelven Futuros. Para recuperar los valores reales, utilizamos la llamada de bloqueo ray.get(id) :

Recuperar valores con ray.get (id)

Como es de esperar, los valores iniciales de los parámetros son todos ceros. Lo que ray.get(id) hace en realidad es extraer el valor del servicio de tienda estatal distribuida que proporciona Ray. El valor fue escrito en el almacén de estado distribuido por el actor cuando actualizó su estado. Si el valor y el cliente están en la misma máquina, el valor se extrae de la memoria compartida para un rendimiento rápido. Si el valor y el cliente residen en diferentes máquinas, el valor se transfiere a la máquina que lo necesita.

Para completar, su código también puede escribir valores explícitamente en este almacenamiento utilizando ray.put(id, value). Cuando desee recuperar varios valores a medida que estén disponibles, hay disponible una conveniente función ray.wait(…). Consulte la API de Ray para obtener más detalles.

Siguiendo el modelo actor, cuando los clientes invocan estos métodos de actor, las invocaciones se enrutan a la instancia de actor, en cualquier lugar del clúster. Dado que pueden ocurrir invocaciones concurrentes, Ray asegura que cada invocación se procese de una manera segura para subprocesos, por lo que se evita el riesgo de corromper el estado sin la necesidad de un código explícito de sincronización de subprocesos. Sin embargo, esto no impone ningún tipo de orden global de cuándo se procesan estas invocaciones; es el primero en llegar, el primero en servir.

Nota: Debido a la naturaleza dinámica de Python, habría sido posible que Ray le permitiera llamar a los métodos actores sin remote(), pero se decidió que el cambio explícito de código es una documentación útil para los lectores del código, ya que hay importantes implicaciones de rendimiento cuando se cambia de una llamada a un método local a una invocación similar a RPC. Además, el objeto devuelto ahora es diferente, un futuro, que requiere el uso de la llamada de bloqueo, ray.get(), para extraer el valor.

Ahora, supongamos que queremos ejecutar varias tareas de trabajo que calculen gradientes de forma continua y actualicen los parámetros del modelo. Cada trabajador se ejecutará en un bucle que hace las siguientes tres cosas:

  1. Obtenga los últimos parámetros.
  2. Calcule una actualización de los parámetros.
  3. Actualice los parámetros.

Estos trabajadores serán apátridas, por lo que usaremos una tarea Ray (una función remota) en lugar de un actor. La función worker toma un controlador para el actor del servidor de parámetros como argumento, lo que permite a la tarea de trabajo invocar métodos en el servidor de parámetros:

Defina un trabajador remoto que realice actualizaciones de parámetros

Luego podemos iniciar dos de estas tareas de trabajo de la siguiente manera. Las tareas (funciones) de Ray se inician con la misma invocación remote() :

Use dos tareas para calcular actualizaciones de parámetros simultáneamente

Luego podemos recuperar los parámetros del proceso del controlador repetidamente y ver que los trabajadores los están actualizando:

Repetir consultas para los valores de parámetro actuales

Cuando las actualizaciones se detienen, los valores finales serán 200.

Tenga en cuenta que Ray hace que sea tan fácil iniciar un servicio remoto o actor como definir una clase Python. Los controladores del actor se pueden pasar a otros actores y tareas para permitir patrones de comunicación y mensajería arbitrarios e intuitivos. Las alternativas actuales son mucho más complejas. Por ejemplo, considere cómo se haría con GRPC la creación de servicios en tiempo de ejecución y el paso de controladores de servicio equivalentes, como en esta documentación.

Unificar tareas y actores

Hemos visto que las tareas y los actores usan la misma API de Ray y se usan de la misma manera. Esta unificación de tareas y actores paralelos tiene importantes beneficios, tanto para simplificar el uso de Ray como para crear aplicaciones potentes a través de la composición.

A modo de comparación, los sistemas de procesamiento de datos populares como Apache Hadoop y Apache Spark permiten que tareas sin estado (funciones sin efectos secundarios) operen en datos inmutables. Esta suposición simplifica el diseño general del sistema y hace que sea más fácil para las aplicaciones razonar sobre la corrección.

Sin embargo, el estado mutable compartido es común en las aplicaciones de aprendizaje automático. Ese estado podría ser el peso de una red neuronal, el estado de un simulador de terceros o una representación de interacciones con el mundo físico. La abstracción de actores de Ray proporciona una forma intuitiva de definir y administrar el estado mutable de una manera segura para los hilos.

Lo que hace que esto sea especialmente poderoso es la forma en que Ray unifica la abstracción del actor con la abstracción paralela de la tarea, heredando los beneficios de ambos enfoques. Ray utiliza un gráfico de tareas dinámico subyacente para implementar tareas de actores y sin estado en el mismo marco. Como consecuencia, estas dos abstracciones son completamente interoperables. Las tareas y los actores se pueden crear a partir de otras tareas y actores. Ambos devuelven futuros, que se pueden pasar a otras tareas o métodos de actores para introducir la programación y las dependencias de datos de una manera natural. Como resultado, las aplicaciones Ray heredan las mejores características tanto de las tareas como de los actores.

Estos son algunos de los conceptos principales utilizados internamente por Ray:

Gráficos de tareas dinámicas: Cuando invoca una función remota o un método de actor, las tareas se agregan a un gráfico de crecimiento dinámico, que Ray programa y ejecuta en un clúster (o una sola máquina de varios núcleos). Las tareas pueden ser creadas por la aplicación «driver» o por otras tareas.

Datos: Ray serializa los datos de manera eficiente utilizando el diseño de datos de Apache Arrow. Los objetos se comparten entre trabajadores y actores en la misma máquina a través de la memoria compartida, lo que evita la necesidad de copias o deserialización. Esta optimización es absolutamente crítica para lograr un buen rendimiento.

Programación: Ray utiliza un enfoque de programación distribuida. Cada máquina tiene su propio programador, que administra a los trabajadores y actores de esa máquina. Las tareas son enviadas por aplicaciones y trabajadores al programador en la misma máquina. A partir de ahí, se pueden reasignar a otros trabajadores o pasar a otros planificadores locales. Esto permite a Ray lograr un rendimiento de tareas sustancialmente mayor que el que se puede lograr con un programador centralizado, un cuello de botella potencial y un único punto de falla. Esto es esencial para muchas aplicaciones de aprendizaje automático.

Conclusión

Los sistemas como los servidores de parámetros normalmente se implementan y se envían como sistemas independientes con una cantidad no trivial de código, que podría ser en su mayoría repetitivo para manejar la distribución, invocación, administración de estados, etc. Hemos visto que las abstracciones y características de Ray hacen posible eliminar la mayor parte de esa repetición. Por lo tanto, cualquier mejora de funciones es comparativamente fácil y su productividad se maximiza.

Muchos de los servicios comunes que necesitamos en los entornos de producción actuales se pueden implementar de esta manera, de forma rápida y eficiente. Los ejemplos incluyen registro, procesamiento de secuencias, simulación, servicio de modelos, procesamiento de gráficos y muchos otros.

Espero que haya encontrado intrigante esta breve introducción a Ray. ¡Por favor, inténtelo y hágame saber lo que piensa!

Para obtener más información

Para obtener más información sobre Ray, eche un vistazo a los siguientes enlaces:

  • Sitio web de Ray
  • Página de Ray GitHub
  • Documentación de Ray: página de destino, instrucciones de instalación
  • Tutoriales de Ray
  • Un documento de investigación que describe el sistema Ray en detalle
  • Un documento de investigación que describe las primitivas flexibles internas de Ray para aprendizaje profundo
  • Serialización rápida con Ray y Apache Arrow
  • RLlib: Aprendizaje por refuerzo escalable con Ray (y este documento de investigación de RLlib)
  • Tune: Ajuste eficiente de hiperparámetros con Ray
  • Modin: Aceleración de Pandas con Ray
  • FLOW: un marco computacional que utiliza aprendizaje por refuerzo para el modelado de control de tráfico
  • Anyscale: la empresa detrás de Ray

Las preguntas deben dirigirse al espacio de trabajo Ray Discourse o Ray Slack.

Apéndice: Ejecutar el código

Para ejecutar la aplicación completa, instale primero Ray con pip install ray (o consulte las instrucciones de instalación de Ray). A continuación, ejecute el siguiente código con Python. Implementa el servidor de parámetros como se discutió anteriormente, pero agrega fragmentación de los parámetros en los trabajadores. También puede encontrar una versión más extensa de este ejemplo como cuaderno de Jupyter aquí.

Tenga en cuenta que este ejemplo se centra en la simplicidad y que se puede hacer más para optimizar este código.

Deja una respuesta

Tu dirección de correo electrónico no será publicada.