Guzmán D. Darío Senior Python Developer English Contratar
Taller autoguiado

De bloqueante a tiempo real: un pipeline de video multi-cámara con asyncio

Paso 10 de 23
Paso 10 de 23

El modelo detrás de un socket

Vamos a sacar el modelo de tu proceso. La buena noticia es que ya está fuera: el services.py que dejaste corriendo en la terminal 1 desde el paso 2 incluye un servicio de detección escuchando en el puerto 8002. Lo único que falta es hablarle.

Inferencia como I/O de red

Crea detector.py. El corazón son cuatro líneas:

detector.py python
class RemoteDetector:
    async def detect(self, jpeg: bytes) -> dict:
        async with self.session.post(self.url, data=jpeg) as response:
            response.raise_for_status()
            return await response.json()

Eso es todo lo que queda de la inferencia en tu programa: mandar bytes por un socket y esperar un JSON. El modelo corre en otro proceso, y el día de mañana podría correr en otra máquina sin que tu código cambie una línea.

Fíjate en las dos palabras nuevas de aiohttp:

Ninguna de las dos calcula nada. Las dos esperan. Tu proceso volvió a ser lo que asyncio hace bien.

Cuarenta a la vez

Ahora la demostración. remote_inference.py lanza cuarenta detecciones simultáneas con el mismo gather que ya conoces, y mantiene el latido corriendo para vigilar el event loop:

remote_inference.py python
async def main():
    jpeg = one_jpeg()
    beat = asyncio.create_task(heartbeat())
    await asyncio.sleep(0.35)

    async with RemoteDetector() as detector:
        t0 = time.perf_counter()
        results = await asyncio.gather(*(detector.detect(jpeg) for _ in range(40)))
        elapsed = time.perf_counter() - t0

    beat.cancel()
    print(f"\n40 inferences, all in flight at once: {elapsed:.2f}s")
terminal 2 bash
python remote_inference.py
salida text
tick   gap    101 ms
tick   gap    101 ms
tick   gap    101 ms

40 inferences, all in flight at once: 0.37s

Checkpoint · Cuarenta peticiones, cero hilos

Dos cosas a la vez en esa salida, y las dos importan:

  • El latido no se movió de 101 milisegundos. El event loop estuvo libre todo el tiempo.
  • Cuarenta inferencias tardaron 0.37 segundos. Una sola, secuencial, habría tardado eso mismo multiplicado por cuarenta.

Y en tu proceso no hay ni un hilo. Compara con el paso anterior: allí necesitabas un ThreadPoolExecutor con su pool, su coordinación y su ceremonia. Aquí solo hay corrutinas esperando sockets.

Lo que acabas de comprar

Este cambio parece pequeño y es el más importante del taller. Al mover el cálculo detrás de un socket ganaste tres cosas de golpe:

Ojo

Ojo con la lectura fácil de esto: el cálculo no desapareció, se mudó. Alguien sigue pagando la CPU, y en este taller ese alguien es svc_model.py corriendo en tu misma máquina. Lo que ganaste es que ese coste ya no vive dentro de tu event loop, y que puedes escalarlo, moverlo o cambiarlo por separado.

Checkpoint

Ya tienes la mitad del pipeline en su forma final: la inferencia es una llamada de red. Falta la otra mitad, porque la ingesta todavía usa urllib bloqueante del paso 3. Vamos a por ella.