Guzmán D. Darío Senior Python Developer Español Hire me
Taller autoguiado

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

Paso 15 de 23
Paso 15 de 23

Escala: sesenta y cuatro cámaras

Cuatro cámaras funcionan. La pregunta honesta es qué pasa con sesenta y cuatro, porque ahí es donde las arquitecturas enseñan de qué están hechas.

Y de paso vas a descubrir algo incómodo y muy útil: cuál es de verdad tu cuello de botella.

terminal 1 bash
python services.py --fake --cameras 64

Ese --cameras 64 le dice al servicio que sirva sesenta y cuatro streams. No necesitas sesenta y cuatro archivos de video: el servicio reutiliza los clips que tengas y arranca cada stream en un punto distinto, así que cada cámara entrega frames diferentes.

Ahora, en la otra terminal, corre el pipeline con dieciséis trabajadores en vez de ocho:

terminal 2 bash
INFER_WORKERS=16 python main.py
salida text
64 cameras, 16 inference workers

ingested:  1511.7 frames/s
inferred:   118.2 frames/s
dropped:   1393.5 frames/s   (92% of ingest)

Esa salida cuenta dos historias opuestas, y hay que leerlas por separado.

La ingesta escaló sin despeinarse. Un solo hilo leyendo sesenta y cuatro sockets a la vez está entregando 1511 frames por segundo. Sesenta y cuatro conexiones simultáneas y el event loop ni se inmuta, porque esperar es barato.

La inferencia no escaló. Se quedó en 118 por segundo, prácticamente lo mismo que con cuatro cámaras. Y como consecuencia, el 92% de los frames se está descartando.

Descartar no es fallar

Ese 92% asusta después de haber presumido de 0 dropped en el paso anterior. Pero mira lo que está pasando de verdad: es put_or_drop haciendo exactamente lo que le pediste en el paso 11.

La cola de inferencia está llena porque el modelo no da más. Cada frame nuevo bota el más viejo. El sistema procesa lo más reciente que puede y sigue en pie.

Piensa en la alternativa. Sin esa política, esos 1393 frames por segundo que sobran se acumularían en memoria, y en menos de un minuto el proceso estaría muerto. El descarte no es el problema: es lo que evita el problema.

Ojo

Lo que sí sería un fallo es descartar sin saberlo. Por eso put_or_drop devuelve False y el pipeline lleva la cuenta. Un sistema que pierde datos y te lo dice es honesto; uno que los pierde en silencio es una bomba de tiempo.

Y el cuello de botella no es tuyo

Aquí viene lo bonito de haber sacado el modelo del proceso en el paso 8. El límite está en el servicio de detección, así que vamos a darle más recursos. Para en la terminal 1 y arráncalo así:

terminal 1 bash
MODEL_THREADS=16 python services.py --fake --cameras 64
terminal 2 bash
INFER_WORKERS=16 python main.py
salida text
inferred:   118.2 frames/s      ->    441.1 frames/s

Ojo

Cuidado con los dos dieciséis, que son cosas distintas y es fácil confundirlos:

  • INFER_WORKERS=16 son tus corrutinas: cuántas peticiones mantienes en vuelo hacia el modelo.
  • MODEL_THREADS=16 son hilos dentro del servicio del modelo, que es quien de verdad calcula.

El primero decide cuánto trabajo pides a la vez. El segundo, cuánto trabajo puede atender el otro lado.

Checkpoint · Casi cuatro veces más, sin tocar tu código

Pasaste de 118 a 441 frames por segundo sin cambiar una sola línea de tu pipeline. Solo le diste más hilos al servicio que corre detrás del socket.

Eso es lo que compraste en el paso 8. Si el modelo viviera dentro de tu proceso, escalarlo significaría rediseñar tu programa. Viviendo detrás de un socket, es una variable de entorno, o mañana una máquina con GPU, o pasado un servicio replicado.

Este es el argumento del taller

Con todo lo que has visto, ya puedes contestar la pregunta que mucha gente hace al principio: ¿no habría sido más fácil con hilos?

Con cuatro cámaras y el modelo dentro del proceso: , y en menos líneas. Sería deshonesto decirte otra cosa.

Lo que cambia la respuesta es esto:

Con cuatro cámaras los hilos van bien. Con sesenta y cuatro, y con seiscientas, no.