Ejecutar simulaciones a gran escala con trabajos de múltiples contenedores de AWS Batch

Expresado por Polly

Industrias como la automotriz, la robótica y las finanzas están incorporando cada vez más cargas de trabajo computacional, como simulaciones, entrenamiento de modelos de aprendizaje automático (ML) y análisis de big data, para mejorar sus productos. Por ejemplo, los fabricantes de automóviles dependen de simulaciones para probar funciones de conducción autónoma, las empresas de robótica entrenan algoritmos de aprendizaje automático para mejorar las capacidades de percepción de los robots, y las empresas financieras realizan análisis profundos para gestionar el riesgo, procesar transacciones y detectar fraudes.

Algunas de estas cargas de trabajo, como las simulaciones, son especialmente difíciles de ejecutar debido a la diversidad de componentes y a los altos requisitos computacionales. Por ejemplo, una simulación de conducción implica la creación de entornos virtuales en 3D, datos de sensores del vehículo, dinámica del vehículo que controla su comportamiento, entre otros. Una simulación de robótica podría involucrar la prueba de cientos de robots de reparto autónomos que interactúan entre sí y con otros sistemas en un almacén masivo.

AWS Batch es un servicio completamente administrado que puede ayudarlo a ejecutar cargas de trabajo por lotes en una variedad de servicios informáticos de AWS, incluidos Amazon Elastic Container Service (Amazon ECS), Amazon Elastic Kubernetes Service (Amazon EKS), AWS Fargate y Amazon EC2 en instancias spot o bajo demanda. Anteriormente, AWS Batch solo permitía trabajos de un solo contenedor y requería pasos adicionales para combinar todos los componentes en un solo contenedor monolítico. Tampoco permitía el uso de contenedores "sidecar" separados, que son contenedores auxiliares que complementan la aplicación principal al proporcionar servicios adicionales como registro de datos. Este esfuerzo adicional requería coordinación entre varios equipos, como desarrollo de software, operaciones de TI y control de calidad (QA), ya que cualquier cambio de código implicaba modificar todo el contenedor.

Ahora, AWS Batch ofrece trabajos de múltiples contenedores, lo que facilita y acelera la ejecución de simulaciones a gran escala en áreas como vehículos autónomos y robótica. Estas cargas de trabajo suelen dividirse entre la propia simulación y el sistema bajo prueba (también conocido como agente) que interactúa con la simulación. Por lo general, estos dos componentes son desarrollados y optimizados por equipos diferentes. Con la capacidad de ejecutar varios contenedores por trabajo, se obtiene el escalado, la programación y la optimización avanzada de costos que ofrece AWS Batch, y se pueden utilizar contenedores modulares que representen diferentes componentes, como entornos en 3D, sensores de robots o sidecars de monitoreo. De hecho, clientes como IPG Automotive, morai y Robotec.ai ya están utilizando trabajos de múltiples contenedores de AWS Batch para ejecutar su software de simulación en la nube.

Veamos cómo funciona esto en la práctica usando un ejemplo simplificado y divirtámonos intentando resolver un laberinto.

Creación de una simulación que se ejecuta en contenedores
En producción, probablemente utilizará software de simulación existente. Para esta publicación, creé una versión simplificada de una simulación de agente/robot. Si no está interesado en los detalles del código, puede omitir esta sección e ir directamente a cómo configurar AWS Batch.

Para esta simulación, el mundo a explorar es un laberinto 2D generado aleatoriamente. El agente tiene la tarea de explorar el laberinto para encontrar una llave y luego llegar a la salida. En cierto modo, es un ejemplo clásico de problemas de búsqueda de caminos con tres ubicaciones.

Aquí hay un mapa de muestra de un laberinto donde resalté las ubicaciones de inicio (S), fin (F) y llave (K).

Ejemplo de mapa de laberinto ASCII.

La separación del agente y el robot en dos contenedores separados permite que diferentes equipos trabajen en cada uno de ellos por separado. Cada equipo puede enfocarse en mejorar su propia parte, ya sea agregando detalles a la simulación o encontrando mejores estrategias sobre cómo el agente explora el laberinto.

Aquí está el código del robot del laberinto (app.py). He utilizado Python para ambos ejemplos. El robot expone una API REST que el agente puede utilizar para moverse por el laberinto y determinar si ha encontrado la llave y llegado a la salida. El robot del laberinto utiliza Flask para la API REST.

import json
import random
from flask import Flask, request, Response

ready = False

# How map data is stored inside a maze
# with size (width x height) = (4 x 3)
#
#    012345678
# 0: +-+-+ +-+
# 1: | |   | |
# 2: +-+ +-+-+
# 3: | |   | |
# 4: +-+-+ +-+
# 5: | | | | |
# 6: +-+-+-+-+
# 7: Not used

class WrongDirection(Exception):
    pass

class Maze:
    UP, RIGHT, DOWN, LEFT = 0, 1, 2, 3
    OPEN, WALL = 0, 1
    
    @staticmethod
    def distance(p1, p2):
        (x1, y1) = p1
        (x2, y2) = p2
        return abs(y2-y1) + abs(x2-x1)

    @staticmethod
    def random_dir():
        return random.randrange(4)

    @staticmethod
    def go_dir(x, y, d):
        if d == Maze.UP:
            return (x, y - 1)
        elif d == Maze.RIGHT:
            return (x + 1, y)
        elif d == Maze.DOWN:
            return (x, y + 1)
        elif d == Maze.LEFT:
            return (x - 1, y)
        else:
            raise WrongDirection(f"Direction: {d}")

    def __init__(self, width, height):
        self.width = width
        self.height = height        
        self.generate()

    def area(self):
        return self.width * self.height

    def min_length(self):
        return self.area() / 5

    def min_distance(self):
        return (self.width + self.height) / 5

    def get_pos_dir(self, x, y, d):
        if d == Maze.UP:
            return self.maze(y)(2 * x + 1)
        elif d == Maze.RIGHT:
            return self.maze(y)(2 * x + 2)
        elif d == Maze.DOWN:
            return self.maze(y + 1)(2 * x + 1)
        elif d == Maze.LEFT:
            return self.maze(y)(2 * x)
        else:
            raise WrongDirection(f"Direction: {d}")

    def set_pos_dir(self, x, y, d, v):
        if d == Maze.UP:
            self.maze(y)(2 * x + 1) = v
        elif d == Maze.RIGHT:
            //texto truncado...```html

            0 and 0 <= x < self.width


    def generate(self):
        self.maze = []
        # Close all borders
        for y in range(0, self.height + 1):
            self.maze.append([Maze.WALL] * (2 * self.width + 1))
        # Get a random starting point on one of the borders
        if random.random() < 0.5:
            sx = random.randrange(self.width)
            if random.random() < 0.5:
                sy = 0
                self.set_pos_dir(sx, sy, Maze.UP, Maze.OPEN)
            else:
                sy = self.height - 1
                self.set_pos_dir(sx, sy, Maze.DOWN, Maze.OPEN)
        else:
            sy = random.randrange(self.height)
            if random.random() < 0.5:
                sx = 0
                self.set_pos_dir(sx, sy, Maze.LEFT, Maze.OPEN)
            else:
                sx = self.width - 1
                self.set_pos_dir(sx, sy, Maze.RIGHT, Maze.OPEN)
        self.start = (sx, sy)
        been = [(self.start)]
        pos = -1
        solved = False
        generate_status = 0
        old_generate_status = 0
        while len(been) < self.area():
            (x, y) = been[pos]
            sd = Maze.random_dir()
            for nd in range(4):
                d = (sd + nd) % 4
                if self.get_pos_dir(x, y, d) != Maze.WALL:
                    continue
                (nx, ny) = Maze.go_dir(x, y, d)
                if (nx, ny) in been:
                    continue
                if self.is_inside(nx, ny):
                    self.set_pos_dir(x, y, d, Maze.OPEN)
                    been.append((nx, ny))
                    pos = -1
                    generate_status = len(been) / self.area()
                    if generate_status - old_generate_status > 0.1:
                        old_generate_status = generate_status
                        print(f"{generate_status * 100:.2f}%")
                    break
                elif solved or len(been) < self.min_lenght():
                    continue
                else:
                    self.set_pos_dir(x, y, d, Maze.OPEN)
                    self.end = (x, y)
                    solved = True
                    pos = -1 - random.randrange(len(been))
                    break
            else:
                pos -= 1
                if pos < -len(been):
                    pos = -1
                    
        self.key = None
        while(self.key == None):
            kx = random.randrange(self.width)
            ky = random.randrange(self.height)
            if (Maze.distance(self.start, (kx,ky)) > self.min_distance()
                and Maze.distance(self.end, (kx,ky)) > self.min_distance()):
                self.key = (kx, ky)


    def get_label(self, x, y):
        if (x, y) == self.start:
            c="S"
        elif (x, y) == self.end:
            c="E"
        elif (x, y) == self.key:
            c="K"
        else:
            c=" "
        return c

                    
    def map(self, moves=()):
        map = ''
        for py in range(self.height * 2 + 1):
            row = ''
            for px in range(self.width * 2 + 1):
                x = int(px / 2)
                y = int(py / 2)
                if py % 2 == 0: #Even rows
                    if px % 2 == 0:
                        c="+"
                    else:
                        v = self.get_pos_dir(x, y, self.UP)
                        if v == Maze.OPEN:
                            c=" "
                        elif v == Maze.WALL:
                            c="-"
                else: # Odd rows
                    if px % 2 == 0:
                        v = self.get_pos_dir(x, y, self.LEFT)
                        if v == Maze.OPEN:
                            c=" "
                        elif v == Maze.WALL:
                            c="|"
                    else:
                        c = self.get_label(x, y)
                        if c == ' ' and (x, y) in moves:
                            c="*"
                row += c
            map += row + '\n'
        return map


app = Flask(__name__)

@app.route('/')
def hello_maze():
    return "<p>Hello, Maze!</p>"

@app.route('/maze/map', methods=('GET', 'POST'))
def maze_map():
    if not ready:
        return Response(status=503, retry_after=10)
    if request.method == 'GET':
        return '<pre>' + maze.map() + '</pre>'
    else:
        moves = request.get_json()
        return maze.map(moves)

@app.route('/maze/start')
def maze_start():
    if not ready:
        return Response(status=503, retry_after=10)
    start = { 'x': maze.start[0], 'y': maze.start[1] }
    return json.dumps(start)

@app.route('/maze/size')
def maze_size():
    if not ready:
        return Response(status=503, retry_after=10)
    size = { 'width': maze.width, 'height': maze.height }
    return json.dumps(size)

@app.route('/maze/pos/<int:y>/<int:x>')
def maze_pos(y, x):
    if not ready:
        return Response(status=503, retry_after=10)
    pos = {
        'here': maze.get_label(x, y),
        'up': maze.get_pos_dir(x, y, Maze.UP),
        'down': maze.get_pos_dir(x, y, Maze.DOWN),
        'left': maze.get_pos_dir(x, y, Maze.LEFT),
        'right': maze.get_pos_dir(x, y, Maze.RIGHT),

    }
    return json.dumps(pos)


WIDTH = 80
HEIGHT = 20
maze = Maze(WIDTH, HEIGHT)
ready = True

El único requisito para el maniquí de maraña (en requirements.txt) es el Flask módulo.

Para crear una imagen de contenedor que ejecute el maniquí de maraña, uso esto Dockerfile.

FROM --platform=linux/amd64 public.ecr.aws/docker/library/python:3.12-alpine

WORKDIR /app

COPY requirements.txt requirements.txt
RUN pip3 install -r requirements.txt

COPY . .

CMD ( "python3", "-m" , "flask", "run", "--host=0.0.0.0", "--port=5555")

Aquí está el código del agente (agent.py). Primero, el agente pregunta al maniquí el tamaño del maraña y la posición auténtico. Luego, aplica su propia táctica para explorar y resolver el maraña. En esta implementación, el agente elige su ruta al azar, intentando evitar seguir el mismo camino más de una vez.


```size('height'):
            continue

        if (nx, ny) in been:
            continue

        x, y = nx, ny
        been.add((x, y))
        moves.append((x, y))
        moves_stack.append((x, y))
        break
    else:
        if len(moves_stack) > 0:
            x, y = moves_stack.pop()
        else:
            print("No moves left")
            break

print(f"Solution length: {len(moves)}")
print(moves)

r = s.post(f'{BASE_URL}/map', json=moves)

print(r.text)

s.close()

La única dependencia del agente (en requirements.txt) es el módulo requests.

Este es el archivo Dockerfile que se utiliza para crear una imagen de contenedor para el agente.

FROM --platform=linux/amd64 public.ecr.aws/docker/library/python:3.12-alpine

WORKDIR /app

COPY requirements.txt requirements.txt
RUN pip3 install -r requirements.txt

COPY . .

CMD ( "python3", "agent.py")

Puede ejecutar fácilmente esta simulación simplificada de forma local, pero la infraestructura le permite ejecutarla a mayor escala (por ejemplo, con una red mucho más amplia y detallada) y probar múltiples agentes para encontrar la mejor estrategia a utilizar. En un entorno del mundo real, las mejoras del agente se implementarían en un dispositivo físico, como un automóvil autónomo o un robot aspirador. Si desea aumentar la complejidad de la simulación y escalarla a decenas o cientos de miles de entidades dinámicas, consulte AWS SimSpace Weaver.

Ejecutar una simulación utilizando trabajos de múltiples contenedores
Para ejecutar un trabajo con AWS Batch, necesita configurar tres componentes:

  • El entorno informático en el que se ejecutará el trabajo.
  • La cola de trabajos en la que se enviará el trabajo.
  • La definición de trabajo que describe cómo ejecutar el trabajo, incluidas las imágenes de contenedor que se utilizarán.

En la Consola de AWS Batch, seleccione Entornos informáticos en el panel de navegación y luego haga clic en Crear. Ahora puede elegir entre Fargate, Amazon EC2 o Amazon EKS. Fargate le permite ajustarse estrechamente a los requisitos de recursos que especifique en las definiciones de trabajo. Sin embargo, las simulaciones suelen requerir acceso a una cantidad amplia pero estática de recursos y utilizan GPU para acelerar los cálculos. Por este motivo, elija Amazon EC2.

Captura de pantalla de la consola.

Seleccione el tipo de orquestación Administrado para que AWS Batch pueda escalar y configurar las instancias EC2 por usted. Luego, ingrese un nombre para el entorno informático y seleccione el rol de servicio asociado (que AWS Batch creó previamente para usted) y el rol de instancia que utiliza el agente de contenedor de ECS (que se ejecuta en las instancias EC2) para hacer llamadas a la API de AWS en su nombre. Luego, haga clic en Siguiente.

Captura de pantalla de la consola.

En la configuración de instancia, elija el tamaño y el tipo de las instancias EC2. Por ejemplo, puede seleccionar tipos de instancias que tengan GPU o utilicen el procesador Graviton. Si no tiene requisitos específicos, deje todas las configuraciones en sus valores predeterminados. En la configuración de red, la consola ya habrá seleccionado su VPC predeterminada y el grupo de seguridad predeterminado. En el último paso, revise todas las configuraciones y complete la creación del entorno informático.

Luego, seleccione Colas de trabajo en el panel de navegación y haga clic en Crear. A continuación, seleccione el mismo tipo de orquestación que usó para el entorno informático (Amazon EC2). En la configuración de la cola de trabajos, ingrese un nombre para la cola de trabajos. En el menú desplegable Entornos informáticos conectados, seleccione el entorno informático que acaba de crear y complete la creación de la cola de trabajos.

Captura de pantalla de la consola.

Seleccione Definiciones de trabajo en el panel de navegación y haga clic en Crear. Como antes, elija Amazon EC2 como tipo de orquestación.

Para utilizar más de un contenedor, desactive la opción Usar estructura de propiedades de contenedor heredada y continúe con el siguiente paso. Por defecto, la consola crea una definición de trabajo heredada de un solo contenedor si ya existe una definición de trabajo heredada en la cuenta. Este es mi caso. Para cuentas sin definiciones de trabajos heredadas, la consola tendrá esta opción deshabilitada.

Captura de pantalla de la consola.

Ingrese un nombre para la definición de trabajo. A continuación, debe determinar los permisos necesarios para este trabajo. Las imágenes de contenedor que desea utilizar para este trabajo están almacenadas en repositorios privados en ECR de Amazon. Para permitir que AWS Batch descargue estas imágenes en el entorno informático, en la sección Propiedades de la tarea, seleccione un Rol de ejecución que proporcione acceso de solo lectura a los repositorios de ECR. No es necesario configurar un Rol de tarea porque el código de la simulación no llama a las API de AWS. Por ejemplo, si su código estuviera cargando resultados en un cubo de Amazon Simple Storage Service (Amazon S3), podría elegir aquí un rol que otorgue los permisos necesarios para hacerlo.

En el siguiente paso, configure los dos contenedores utilizados por este trabajo. El primero es el contenedor maze-model. Ingrese el nombre y la ubicación de la imagen. Aquí puede establecer los requisitos de recursos del contenedor en términos de vCPU, memoria y GPU. Esto es similar a configurar contenedores para un

tarea ECS.

Captura de pantalla de la consola.

Agrego un segundo contenedor para el agente e ingreso el nombre, la ubicación de la imagen y los requisitos de medios como ayer. Correcto a que el agente necesita obtener al laberinto tan pronto como comienza, utilizo el Dependencias sección para especificar una dependencia de contenedor. Yo selecciono maze-model para el nombre del contenedor y COMENZAR como la condición. Si no agrego esta dependencia, el contenedor agent puede fallar antes de que el contenedor maze-model se esté ejecutando y puede replicar. Debido a que ambos contenedores están marcados como esenciales en esta definición de trabajo, el trabajo genérico terminaría con una falla.

Captura de pantalla de la consola.

Reviso todas las configuraciones y completo la definición del trabajo. Ahora puedo comenzar a trabajar.

En la Trabajos sección del panel de navegación, lanzo un nuevo trabajo. Ingreso un nombre y selecciono la cola de trabajos y la definición de trabajo que acabo de crear.

Captura de pantalla de la consola.

En los siguientes pasos, no necesito modificar ninguna configuración y creo el trabajo. Después de unos minutos, el trabajo se completó correctamente y tengo acceso a los registros de los dos contenedores.

Captura de pantalla de la consola.

El agente resolvió el laberinto y puedo ver todos los detalles de los registros. Aquí está el resultado del trabajo para ver cómo el agente inició, tomó el interruptor y luego encontró la salida.

SIZE {'width': 80, 'height': 20}
START {'x': 0, 'y': 18}
(32, 2) key found
(79, 16) exit
Solution length: 437
((0, 18), (1, 18), (0, 18), ..., (79, 14), (79, 15), (79, 16))

En el mapa, los asteriscos rojos (*) representan la ruta utilizada por el agente entre las ubicaciones de inicio (S), interruptor (k) y salida (e).

Mapa basado en ASCII del laberinto resuelto.

Aumento de la observabilidad con un contenedor sidecar
Cuando se ejecutan trabajos complejos utilizando varios componentes, es útil tener más visibilidad de lo que están haciendo estos componentes. Por ejemplo, si hay un error o un problema de rendimiento, esta información puede ayudar a identificar dónde y cuál es el problema.

Para instrumentar mi aplicación, utilizo Distribución AWS para OpenTelemetry:

Al utilizar los datos de telemetría recopilados de esta manera, puedo configurar paneles (por ejemplo, usando CloudWatch o Grafana administrada por Amazon) y alarmas (con CloudWatch o Prometheus) que me ayudan a comprender mejor lo que está sucediendo y reducir el tiempo para resolver un problema. En general, un contenedor complementario puede ayudar a integrar datos de telemetría de trabajos de AWS Batch con sus plataformas de monitoreo y observabilidad.

Cosas que recordar
Lote de AWS admite trabajos de contenedores múltiples hoy en la Consola de administración de AWS, Interfaz de línea de comandos de AWS (AWS CLI) y SDK de AWS en todas las Regiones de AWS donde está disponible. Para obtener más información, consulte la Lista de servicios de AWS por región.

No hay ningún costo adicional por utilizar trabajos de múltiples contenedores con AWS Batch. De hecho, no hay ningún cargo adicional por utilizar AWS Batch. Solo paga por los recursos de AWS que crea para configurar y ejecutar su aplicación, como instancias EC2 y contenedores Fargate. Para optimizar sus costos, puede utilizar Instancias reservadas, Plan de Ahorro e Instancias de spot EC2 y Fargate en sus entornos de cómputo.

El uso de trabajos de múltiples contenedores acelera los tiempos de desarrollo al reducir los esfuerzos de preparación del trabajo y elimina la necesidad de herramientas personalizadas para combinar el trabajo de varios equipos en un solo contenedor. También simplifica DevOps al definir responsabilidades claras de los componentes para que los equipos puedan identificar y resolver rápidamente problemas en sus propias áreas de especialización sin distracciones.

Para obtener más información, consulte cómo configurar trabajos de contenedores múltiples en la Guía del usuario de AWS Batch.

¿Nos apoyarás hoy?

Creemos que todos merecen entender el mundo en el que viven. Este conocimiento ayuda a crear mejores ciudadanos, vecinos, amigos y custodios de nuestro planeta. Producir periodismo explicativo y profundamente investigado requiere recursos. Puedes apoyar esta misión haciendo una donación económica a Gelipsis hoy. ¿Te sumarás a nosotros?

Suscríbete para recibir nuestro boletín:

Recent Articles

Related Stories

DEJA UN COMENTARIO

Por favor ingrese su comentario!
Por favor ingrese su nombre aquí