Sincronização

Mecânica de programação

O processamento de dados em um gráfico do MediaPipe ocorre em nós de processamento definidos como subclasses CalculatorBase. O sistema de agendamento decide quando cada calculadora será executada.

Cada gráfico tem pelo menos uma fila do programador. Cada fila do programador tem exatamente um executor. Os nós são atribuídos estaticamente a uma fila e, portanto, a um executor. Por padrão, há uma fila, cujo executor é um pool de linhas de execução com várias linhas de execução com base nos recursos do sistema.

Cada nó tem um estado de programação, que pode estar não pronto, pronto ou em execução. Uma função de prontidão determina se um nó está pronto para ser executado. Essa função é invocada na inicialização do gráfico sempre que um nó termina de ser executado e quando o estado das entradas de um nó muda.

A função de prontidão usada depende do tipo de nó. Um nó sem entradas de stream é conhecido como nó de origem. Os nós de origem estão sempre prontos para execução até informarem ao framework que não têm mais dados para enviar e, nesse momento, eles são fechados.

Os nós que não são de origem estarão prontos se tiverem entradas para processar e se essas entradas forem um conjunto de entrada válido de acordo com as condições definidas pela política de entrada do nó (discutidas abaixo). A maioria dos nós usa a política de entrada padrão, mas alguns nós especificam uma diferente.

Quando um nó fica pronto, uma tarefa é adicionada à fila do programador correspondente, que é uma fila de prioridade. Atualmente, a função de prioridade é fixa e considera as propriedades estáticas dos nós e a classificação topológica deles no gráfico. Por exemplo, os nós mais próximos do lado da saída do gráfico têm maior prioridade, enquanto os nós de origem têm a prioridade mais baixa.

Cada fila é disponibilizada por um executor, que é responsável por executar a tarefa invocando o código da calculadora. Diferentes executores podem ser fornecidos e configurados. Isso pode ser usado para personalizar o uso de recursos de execução, por exemplo, executando determinados nós em linhas de execução de menor prioridade.

Sincronização de carimbo de data/hora

A execução do gráfico do MediaPipe é descentralizada: não há um relógio global, e nós diferentes podem processar dados de carimbos de data/hora diferentes ao mesmo tempo. Isso permite maior capacidade por meio de pipelines.

No entanto, as informações de tempo são muito importantes para muitos fluxos de trabalho de percepção. Os nós que recebem vários fluxos de entrada geralmente precisam coordená-los de alguma forma. Por exemplo, um detector de objetos pode gerar uma lista de retângulos de limite de um frame, e essas informações podem ser alimentadas em um nó de renderização, que as processará em conjunto com o frame original.

Portanto, uma das principais responsabilidades do framework MediaPipe é fornecer sincronização de entradas para os nós. Em termos da mecânica do framework, o papel principal de um carimbo de data/hora é servir como uma chave de sincronização.

Além disso, o MediaPipe foi projetado para oferecer suporte a operações determinísticas, que são importantes em muitos cenários (testes, simulação, processamento em lote etc.), além de permitir que os autores de gráficos relaxem o determinismo quando necessário para atender a restrições em tempo real.

Os dois objetivos de sincronização e determinismo estão na base de várias opções de design. Os pacotes enviados por push para um determinado stream precisam ter carimbos de data/hora monotonicamente crescentes. Essa não é apenas uma suposição útil para muitos nós, mas também se baseia na lógica de sincronização. Cada stream tem um limite de carimbo de data/hora, que é o menor carimbo de data/hora possível permitido para um novo pacote no stream. Quando um pacote com o carimbo de data/hora T chega, o limite avança automaticamente para T+1, refletindo o requisito monotônico. Isso permite que o framework tenha certeza de que nenhum pacote com carimbo de data/hora menor que T chegará.

Políticas de entrada

A sincronização é tratada localmente em cada nó, usando a política de entrada especificada pelo nó.

A política de entrada padrão, definida por DefaultInputStreamHandler, fornece sincronização determinística de entradas, com as seguintes garantias:

  • Se pacotes com o mesmo carimbo de data/hora forem fornecidos em vários streams de entrada, eles sempre serão processados juntos, independentemente da ordem de chegada em tempo real.

  • Os conjuntos de entrada são processados em ordem estritamente crescente do carimbo de data/hora.

  • Nenhum pacote é descartado, e o processamento é totalmente determinístico.

  • O nó fica pronto para processar dados o mais rápido possível, de acordo com as garantias acima.

Para explicar como funciona, precisamos apresentar a definição de um carimbo de data/hora determinado. Dizemos que um carimbo de data/hora em um stream é resolvido se for menor que o limite do carimbo de data/hora. Em outras palavras, um carimbo de data/hora é determinado para um stream quando o estado da entrada nesse carimbo de data/hora é irrevogavelmente conhecido: ou há um pacote ou a certeza de que um pacote com esse carimbo de data/hora não chegará.

Um carimbo de data/hora é determinado em vários streams, caso seja definido em cada um deles. Além disso, se um carimbo de data/hora for ajustado, isso significa que todos os carimbos de data/hora anteriores também foram. Assim, os carimbos de data/hora definidos podem ser processados de maneira determinista em ordem crescente.

De acordo com essa definição, uma calculadora com a política de entrada padrão estará pronta se houver um carimbo de data/hora estabelecido em todos os streams de entrada e contiver um pacote em pelo menos um stream de entrada. A política de entrada fornece todos os pacotes disponíveis para um carimbo de data/hora estabelecido como um único conjunto de entrada para a calculadora.

Uma consequência desse comportamento determinista é que, para nós com vários fluxos de entrada, pode haver uma espera ilimitada até que um carimbo de data/hora seja liquidado, e um número ilimitado de pacotes pode ser armazenado em buffer nesse meio tempo. Considere um nó com dois streams de entrada, um dos quais continua enviando pacotes enquanto o outro não envia nada e não avança o limite.

Portanto, também oferecemos políticas de entrada personalizadas: por exemplo, dividir as entradas em diferentes conjuntos de sincronização definidos por SyncSetInputStreamHandler ou evitar a sincronização e processar as entradas imediatamente à medida que elas chegam definidas por ImmediateInputStreamHandler.

Controle de fluxo

Há dois mecanismos principais de controle de fluxo. Um mecanismo de contrapressão limita a execução de nós upstream quando os pacotes armazenados em buffer em um stream atingem um limite (configurável) definido por CalculatorGraphConfig::max_queue_size. Esse mecanismo mantém o comportamento determinístico e inclui um sistema de prevenção de impasses que relaxa os limites configurados quando necessário.

O segundo sistema consiste na inserção de nós especiais que podem descartar pacotes de acordo com restrições em tempo real (geralmente usando políticas de entrada personalizadas) definidas por FlowLimiterCalculator. Por exemplo, um padrão comum coloca um nó de controle de fluxo na entrada de um subgráfico, com uma conexão de loopback da saída final para o nó de controle de fluxo. Assim, o nó de controle de fluxo consegue acompanhar quantos carimbos de data/hora estão sendo processados no gráfico downstream e descartar pacotes se essa contagem atingir um limite configurável. Além disso, como os pacotes são descartados upstream, evitamos o trabalho desperdiçado que resultaria do processamento parcial de um carimbo de data/hora e descartando pacotes entre os estágios intermediários.

Essa abordagem baseada em calculadora dá ao autor do gráfico controle sobre onde os pacotes podem ser descartados e permite flexibilidade na adaptação e na personalização do comportamento do gráfico de acordo com as restrições de recursos.