Mecánica de programación
El procesamiento de datos en un gráfico de MediaPipe se produce dentro de los nodos de procesamiento definidos como subclases CalculatorBase
. El sistema de programación decide cuándo debe ejecutarse cada calculadora.
Cada gráfico tiene al menos una cola de programador. Cada cola de programador tiene exactamente un executor. Los nodos se asignan de forma estática a una cola (y, por lo tanto, a un ejecutor). De forma predeterminada, hay una cola cuyo ejecutor es un conjunto de subprocesos con varios subprocesos basados en las capacidades del sistema.
Cada nodo tiene un estado de programación que puede ser not listo, listo o en ejecución. Una función de preparación determina si un nodo está listo para ejecutarse. Esta función se invoca en la inicialización del grafo, cada vez que un nodo termina de ejecutarse y cuando cambia el estado de las entradas de un nodo.
La función de preparación que se usa depende del tipo de nodo. Un nodo sin entradas de transmisión se conoce como nodo de origen. Los nodos fuente siempre están listos para ejecutarse hasta que le indican al framework que no tienen más datos para generar y que en ese momento se cierran.
Los nodos que no son de origen están listos si tienen entradas que procesar y si esas entradas forman un conjunto de entrada válido según las condiciones que establece la política de entrada del nodo (que se explica a continuación). La mayoría de los nodos usan la política de entrada predeterminada, pero algunos especifican una diferente.
Cuando se prepara un nodo, se agrega una tarea a la cola del programador correspondiente, que es una cola de prioridad. La función de prioridad está fijada actualmente y tiene en cuenta las propiedades estáticas de los nodos y su ordenación topológica dentro del gráfico. Por ejemplo, los nodos más cercanos al lado de salida del gráfico tienen una prioridad más alta, mientras que los nodos de origen tienen la prioridad más baja.
Cada cola es entregada por un ejecutor, que es responsable de ejecutar la tarea mediante la invocación del código de la calculadora. Se pueden proporcionar y configurar diferentes ejecutores; esto se puede usar para personalizar el uso de los recursos de ejecución, p.ej., mediante la ejecución de ciertos nodos en subprocesos de menor prioridad.
Sincronización de marca de tiempo
La ejecución del grafo de MediaPipe está descentralizada: no hay un reloj global y diferentes nodos pueden procesar datos de distintas marcas de tiempo al mismo tiempo. Esto permite una mayor capacidad de procesamiento a través de la canalización.
Sin embargo, la información sobre el tiempo es muy importante para muchos flujos de trabajo de percepción. Por lo general, los nodos que reciben varias transmisiones de entrada necesitan coordinarlas de alguna manera. Por ejemplo, un detector de objetos podría generar una lista de rectángulos de límite de un marco, y esta información podría enviarse a un nodo de renderización, que debería procesarla junto con el marco original.
Por lo tanto, una de las responsabilidades clave del framework de MediaPipe es proporcionar sincronización de entrada para los nodos. En términos de la mecánica del framework, la función principal de una marca de tiempo es servir como una clave de sincronización.
Además, MediaPipe está diseñado para admitir operaciones deterministas, lo cual es importante en muchas situaciones (pruebas, simulación, procesamiento por lotes, etc.), al tiempo que permite a los autores de grafos flexibilizar el determinismo cuando sea necesario para cumplir con las restricciones en tiempo real.
Los dos objetivos de sincronización y determinismo son la base de varias opciones de diseño. En particular, los paquetes enviados a una transmisión determinada deben tener marcas de tiempo que aumenten monótonamente: esta suposición no solo es útil para muchos nodos, sino que la lógica de sincronización también depende de ella. Cada transmisión tiene un
límite de marca de tiempo, que es la marca de tiempo más baja posible para un paquete
nuevo de la transmisión. Cuando llega un paquete con la marca de tiempo T
, el vínculo avanza automáticamente a T+1
, lo que refleja el requisito monótono. Esto permite que el framework sepa con seguridad que no llegarán más paquetes con una marca de tiempo inferior a T
.
Políticas de entrada
La sincronización se controla de forma local en cada nodo, mediante la política de entrada que especifica el nodo.
La política de entrada predeterminada, definida por DefaultInputStreamHandler
, proporciona una sincronización determinista de las entradas, con las siguientes garantías:
Si se proporcionan paquetes con la misma marca de tiempo en varias transmisiones de entrada, siempre se procesarán juntos, independientemente de su orden de llegada en tiempo real.
Los conjuntos de entrada se procesan en orden ascendente estrictamente de marca de tiempo.
No se descarta ningún paquete y el procesamiento es completamente determinista.
El nodo estará listo para procesar datos en cuanto sea posible, dadas las garantías anteriores.
Para explicar cómo funciona, debemos ingresar la definición de una marca de tiempo establecida. Decimos que una marca de tiempo en una transmisión se establece si es inferior al límite de marca de tiempo. En otras palabras, se establece una marca de tiempo para una transmisión una vez que se conoce irrevocablemente el estado de la entrada en esa marca de tiempo: hay un paquete o existe la certeza de que no llegará un paquete con esa marca de tiempo.
Una marca de tiempo se establece en varias transmisiones si se establece en cada una de ellas. Además, si se establece una marca de tiempo, esto implica que todas las marcas de tiempo anteriores también se resuelven. Por lo tanto, las marcas de tiempo establecidas se pueden procesar de forma determinista en orden ascendente.
Dada esta definición, una calculadora con la política de entrada predeterminada está lista si hay una marca de tiempo que se establece en todas las transmisiones de entrada y contiene un paquete en al menos una transmisión de entrada. La política de entrada proporciona todos los paquetes disponibles para una marca de tiempo establecida como un solo conjunto de entradas en la calculadora.
Una consecuencia de este comportamiento determinista es que, en el caso de los nodos con múltiples transmisiones de entrada, puede haber una espera no delimitada en teoría a que se establezca una marca de tiempo y, mientras tanto, se puede almacenar en búfer una cantidad ilimitada de paquetes. (Considera un nodo con dos flujos de entrada, uno de los cuales sigue enviando paquetes mientras que el otro no envía nada y no avanza el límite).
Por lo tanto, también proporcionamos políticas de entrada personalizadas: por ejemplo, dividir las entradas en diferentes conjuntos de sincronización definidos por SyncSetInputStreamHandler
o evitar la sincronización por completo y procesar las entradas inmediatamente a medida que llegan definidas por ImmediateInputStreamHandler
.
Control de flujo
Hay dos mecanismos principales de control de flujo. Un mecanismo de contrapresión limita la ejecución de nodos ascendentes cuando los paquetes almacenados en búfer en una transmisión alcanzan un límite (configurable) definido por CalculatorGraphConfig::max_queue_size
. Este mecanismo mantiene un comportamiento determinista y, además, incluye un sistema de prevención de interbloqueos que disminuye la rigurosidad de los límites configurados cuando es necesario.
El segundo sistema consiste en insertar nodos especiales que pueden descartar paquetes de acuerdo con restricciones en tiempo real (por lo general, con políticas de entrada personalizadas) definidas por FlowLimiterCalculator
. Por ejemplo, un patrón común coloca un nodo de control de flujo en la entrada de un subgrafo, con una conexión de bucle invertido desde la salida final hasta el nodo de control de flujo. Por lo tanto, el nodo de control de flujo puede realizar un seguimiento de cuántas marcas de tiempo se procesan en el gráfico descendente y descartar paquetes si este recuento alcanza un límite (configurable). Y como los paquetes se descartan en sentido ascendente, evitamos el trabajo desperdiciado que resultaría del procesamiento parcial de una marca de tiempo y, luego, descartar los paquetes entre las etapas intermedias.
Este enfoque basado en la calculadora le da al autor del gráfico control sobre dónde se pueden descartar los paquetes y permite flexibilidad para adaptar y personalizar el comportamiento del gráfico según las restricciones de recursos.