Meccanismi di pianificazione
L'elaborazione dei dati in un grafico MediaPipe avviene all'interno di nodi di elaborazione definiti come sottoclassi CalculatorBase
. Il sistema di pianificazione decide quando
eseguire ogni calcolatore.
Ogni grafico ha almeno una coda scheduler. Ogni coda dello scheduler ha esattamente un executor. I nodi sono assegnati in modo statico a una coda (quindi a un esecutore). Per impostazione predefinita, è presente una coda, il cui esecutore è un pool di thread con una serie di thread in base alle funzionalità del sistema.
Ogni nodo ha uno stato di pianificazione, che può essere not ready, ready o in esecuzione. Una funzione di idoneità determina se un nodo è pronto per l'esecuzione. Questa funzione viene richiamata all'inizializzazione del grafico, ogni volta che termina l'esecuzione di un nodo e ogni volta che lo stato degli input del nodo cambia.
La funzione di idoneità utilizzata dipende dal tipo di nodo. Un nodo senza input di flusso è noto come nodo di origine: questi sono sempre pronti per l'esecuzione fino a quando non comunicano al framework che non hanno più dati da restituire, dopodiché vengono chiusi.
I nodi non di origine sono pronti se hanno input da elaborare e se questi input formano un set di input valido in base alle condizioni impostate dal criterio di input del nodo (esaminato di seguito). La maggior parte dei nodi utilizza il criterio di input predefinito, ma alcuni ne specificano uno diverso.
Quando un nodo è pronto, viene aggiunta un'attività alla coda dello scheduler corrispondente, che è una coda prioritaria. La funzione di priorità attualmente è fissa e prende in considerazione le proprietà statiche dei nodi e il loro ordinamento topologico all'interno del grafico. Ad esempio, i nodi più vicini al lato di output del grafico hanno una priorità più alta, mentre i nodi di origine hanno la priorità più bassa.
Ogni coda è gestita da un esecutore, che è responsabile dell'esecuzione effettiva dell'attività richiamando il codice della calcolatrice. È possibile fornire e configurare diversi esecutori. Questo può essere utilizzato per personalizzare l'uso delle risorse di esecuzione, ad esempio eseguendo determinati nodi su thread a priorità inferiore.
Sincronizzazione dei timestamp
L'esecuzione del grafico MediaPipe è decentralizzata: non esiste un orologio globale e nodi diversi possono elaborare contemporaneamente i dati di timestamp diversi. Ciò consente una velocità effettiva superiore tramite la pipeline.
Tuttavia, le informazioni sull'ora sono molto importanti per molti flussi di lavoro percepiti. I nodi che ricevono più flussi di input in genere devono coordinarli in qualche modo. Ad esempio, un rilevatore di oggetti potrebbe restituire un elenco di rettangoli di confine da un frame e queste informazioni possono essere inviate a un nodo di rendering, che dovrebbe elaborarle insieme al frame originale.
Pertanto, una delle responsabilità principali del framework MediaPipe è quella di fornire la sincronizzazione degli input per i nodi. In termini di meccanica del framework, il ruolo principale di un timestamp è quello di fungere da chiave di sincronizzazione.
Inoltre, MediaPipe è progettato per supportare operazioni deterministiche, che sono importanti in molti scenari (test, simulazione, elaborazione batch e così via), consentendo agli autori dei grafici di ridurre il determinismo laddove necessario per soddisfare i vincoli in tempo reale.
I due obiettivi della sincronizzazione e del determinismo sono alla base di diverse scelte progettuali. In particolare, i pacchetti inviati in un determinato flusso devono avere timestamp monotonicamente crescenti: questo non è solo un'ipotesi utile per molti nodi, ma è anche utilizzata dalla logica di sincronizzazione. Ogni stream ha un limite di timestamp, ovvero il timestamp più basso consentito per un nuovo pacchetto nello stream. Quando arriva un pacchetto con timestamp T
, il vincolo
avanza automaticamente a T+1
, riflettendo il requisito monotonico. Ciò consente al framework di sapere con certezza che non arriveranno più pacchetti con timestamp inferiore a T
.
Criteri di input
La sincronizzazione viene gestita localmente su ciascun nodo, utilizzando il criterio di input specificato dal nodo.
Il criterio di input predefinito, definito da DefaultInputStreamHandler
, fornisce
una sincronizzazione deterministica degli input, con le seguenti garanzie:
Se su più flussi di input vengono forniti pacchetti con lo stesso timestamp, verranno sempre elaborati insieme, indipendentemente dall'ordine di arrivo, in tempo reale.
I set di input vengono elaborati in ordine timestamp rigorosamente crescente.
Non viene ignorato nessun pacchetto e l'elaborazione è completamente deterministica.
Il nodo diventa pronto a elaborare i dati il prima possibile in base alle garanzie precedenti.
Per spiegarne il funzionamento, dobbiamo introdurre la definizione di un timestamp stabile. Diciamo che un timestamp in uno stream viene regolato se è inferiore ai limiti del timestamp. In altre parole, un timestamp viene stabilito per un flusso una volta che lo stato dell'input a quel timestamp è irrevocabilmente noto: se esiste un pacchetto o esiste la certezza che un pacchetto con quel timestamp non arriverà.
Se viene stabilito per ciascuno di questi stream, il timestamp viene impostato su più stream. Inoltre, se un timestamp viene impostato, significa che vengono corretti anche tutti i timestamp precedenti. Di conseguenza, i timestamp stabiliti possono essere elaborati in ordine crescente.
Data questa definizione, una calcolatrice con il criterio di input predefinito è pronta se esiste un timestamp che viene stabilito per tutti i flussi di input e contiene un pacchetto in almeno un flusso di input. Il criterio di input fornisce tutti i pacchetti disponibili per un timestamp stabilito come un singolo insieme di input per la calcolatrice.
Una conseguenza di questo comportamento deterministico è che, per i nodi con più flussi di input, può verificarsi un'attesa teoricamente illimitata per la regolazione di un timestamp e nel frattempo è possibile eseguire il buffer di un numero illimitato di pacchetti. (considera un nodo con due flussi di input, uno dei quali continua a inviare pacchetti mentre l'altro non invia nulla e non avanza il limite).
Pertanto, forniamo anche criteri di input personalizzati, ad esempio suddividere gli input in diversi set di sincronizzazione definiti da SyncSetInputStreamHandler
o evitare del tutto la sincronizzazione ed elaborare gli input non appena arrivano definiti da ImmediateInputStreamHandler
.
Controllo del flusso
Esistono due principali meccanismi di controllo del flusso. Un meccanismo di retropressione limita l'esecuzione dei nodi upstream quando i pacchetti sottoposti a buffer in un flusso raggiungono un limite (configurabile) definito da CalculatorGraphConfig::max_queue_size
. Questo meccanismo mantiene il comportamento deterministico e include un sistema di prevenzione dei deadlock che, se necessario, allenta i limiti configurati.
Il secondo sistema consiste nell'inserimento di nodi speciali che possono eliminare pacchetti in base a vincoli in tempo reale (di solito utilizzando criteri di input personalizzati) definiti da FlowLimiterCalculator
. Ad esempio, un pattern comune posiziona un nodo di controllo del flusso nell'input di un sottografico, con una connessione di loopback dall'output finale al nodo di controllo del flusso. Il nodo flow-control è quindi in grado di tenere traccia del numero di timestamp elaborati nel grafico a valle e di rilasciare pacchetti se questo conteggio raggiunge un limite (configurabile). Inoltre, poiché i pacchetti vengono eliminati a monte, evitiamo lo spreco di lavoro che potrebbe derivare dall'elaborazione parziale di un timestamp e dall'eliminazione dei pacchetti tra le fasi intermedie.
Questo approccio basato sulla calcolatrice consente all'autore dei grafici di controllare dove possono essere inviati i pacchetti e offre flessibilità nell'adattare e personalizzare il comportamento del grafico in base ai vincoli delle risorse.