Wykonywanie zadań w krótkich odstępach czasu i szybkie kolejki wiadomości

W interfejsie HAL sieci neuronowych w wersji 1.2 wprowadzono koncepcję wykonywania seryjnego. Wykonywanie seryjne to sekwencja wykonywania tego samego przygotowanego modelu, która następuje szybko po sobie, np. w przypadku klatek przechwytywanych przez aparat lub kolejnych próbek dźwięku. Obiekt seryjny służy do sterowania zestawem wykonywań seryjnych i do zachowywania zasobów między wykonaniami, co pozwala zmniejszyć obciążenie. Obiekty seryjne umożliwiają 3 optymalizacje:

  1. Obiekt seryjny jest tworzony przed sekwencją wykonywań i zwalniany po jej zakończeniu. Dzięki temu czas życia obiektu seryjnego podpowiada sterownikowi, jak długo powinien on pozostawać w stanie wysokiej wydajności.
  2. Obiekt seryjny może zachowywać zasoby między wykonaniami. Na przykład sterownik może zmapować obiekt pamięci podczas pierwszego wykonania i zapisać mapowanie w obiekcie seryjnym, aby użyć go ponownie w kolejnych wykonaniach. Każdy zasób zapisany w pamięci podręcznej można zwolnić, gdy obiekt seryjny zostanie zniszczony lub gdy środowisko wykonawcze NNAPI powiadomi obiekt seryjny, że zasób nie jest już potrzebny.
  3. Obiekt seryjny używa szybkich kolejek komunikatów (FMQ) do komunikacji między procesami aplikacji i sterownika. Może to zmniejszyć opóźnienie, ponieważ FMQ omija HIDL i przekazuje dane bezpośrednio do innego procesu za pomocą atomowej cyklicznej kolejki FIFO w pamięci współdzielonej. Proces odbiorcy wie, że ma usunąć element z kolejki i rozpocząć przetwarzanie, sprawdzając liczbę elementów w kolejce FIFO lub czekając na flagę zdarzenia FMQ, która jest sygnalizowana przez producenta. Ta flaga zdarzenia to szybki mutex przestrzeni użytkownika (futex).

FMQ to struktura danych niskiego poziomu, która nie gwarantuje czasu życia w różnych procesach i nie ma wbudowanego mechanizmu do określania, czy proces po drugiej stronie FMQ działa zgodnie z oczekiwaniami. W związku z tym, jeśli producent FMQ przestanie działać, odbiorca może utknąć w oczekiwaniu na dane, które nigdy nie dotrą. Jednym z rozwiązań tego problemu jest powiązanie przez sterownik kolejek FMQ z obiektem seryjnym wyższego poziomu, aby wykrywać, kiedy zakończy się wykonywanie seryjne.

Ponieważ wykonywanie seryjne działa na tych samych argumentach i zwraca te same wyniki co inne ścieżki wykonywania, podstawowe kolejki FMQ muszą przekazywać te same dane do i z sterowników usług NNAPI. Kolejki FMQ mogą jednak przesyłać tylko proste typy danych. Przesyłanie złożonych danych odbywa się przez serializację i deserializację zagnieżdżonych buforów (typów wektorowych) bezpośrednio w kolejkach FMQ oraz używanie obiektów wywołania zwrotnego HIDL do przesyłania uchwytów puli pamięci na żądanie. Producent strony FMQ musi wysyłać żądania lub komunikaty z wynikami do odbiorcy atomowo, używając MessageQueue::writeBlocking, jeśli kolejka jest blokująca, lub używając MessageQueue::write, jeśli kolejka jest nieblokująca.

Interfejsy seryjne

Interfejsy seryjne dla interfejsu HAL sieci neuronowych znajdują się w hardware/interfaces/neuralnetworks/1.2/ i są opisane poniżej. Więcej informacji o interfejsach seryjnych w warstwie NDK znajdziesz w frameworks/ml/nn/runtime/include/NeuralNetworks.h.

types.hal

types.hal określa typ danych wysyłanych przez FMQ.

  • FmqRequestDatum: Pojedynczy element serializowanej reprezentacji obiektu wykonania Request i wartości MeasureTiming, która jest wysyłana przez szybką kolejkę komunikatów.
  • FmqResultDatum: pojedynczy element serializowanej reprezentacji wartości zwracanych z wykonania (ErrorStatus, OutputShapes i Timing), która jest zwracana przez szybką kolejkę komunikatów.

IBurstContext.hal

IBurstContext.hal definiuje obiekt interfejsu HIDL, który znajduje się w usłudze sieci neuronowych.

  • IBurstContext: obiekt kontekstu do zarządzania zasobami serii.

IBurstCallback.hal

IBurstCallback.hal definiuje obiekt interfejsu HIDL dla wywołania zwrotnego utworzonego przez środowisko wykonawcze sieci neuronowych i jest używany przez usługę sieci neuronowych do pobierania obiektów hidl_memory odpowiadających identyfikatorom slotów.

  • IBurstCallback: obiekt wywołania zwrotnego używany przez usługę do pobierania obiektów pamięci.

IPreparedModel.hal

IPreparedModel.hal jest rozszerzany w HAL 1.2 o metodę tworzenia obiektu IBurstContext z przygotowanego modelu.

  • configureExecutionBurst: konfiguruje obiekt seryjny używany do szybkiego wykonywania wielu wnioskowań na przygotowanym modelu.

Obsługa wykonywania seryjnego w sterowniku

Najprostszym sposobem na obsługę obiektów seryjnych w usłudze HIDL NNAPI jest użycie funkcji użytkowej ::android::nn::ExecutionBurstServer::create, która znajduje się w ExecutionBurstServer.h i jest spakowana w libneuralnetworks_common i libneuralnetworks_util bibliotekach statycznych. Ta funkcja fabryczna ma 2 przeciążenia:

  • Jedno przeciążenie akceptuje wskaźnik do obiektu IPreparedModel. Ta funkcja użytkowa używa metody executeSynchronously w obiekcie IPreparedModel do wykonania modelu.
  • Jedno przeciążenie akceptuje konfigurowalny obiekt IBurstExecutorWithCache, który może służyć do buforowania zasobów (takich jak mapowania hidl_memory), które są zachowywane w wielu wykonaniach.

Każde przeciążenie zwraca obiekt IBurstContext (który reprezentuje obiekt seryjny) zawierający i zarządzający własnym dedykowanym wątkiem odbiornika. Ten wątek odbiera żądania z FMQ requestChannel, wykonuje wnioskowanie, a następnie zwraca wyniki przez FMQ resultChannel. Ten wątek i wszystkie inne zasoby zawarte w obiekcie IBurstContext są automatycznie zwalniane, gdy klient serii utraci odniesienie do IBurstContext.

Możesz też utworzyć własną implementację IBurstContext, która będzie wiedzieć, jak wysyłać i odbierać wiadomości przez kolejki FMQ requestChannel i resultChannel przekazywane do IPreparedModel::configureExecutionBurst.

Funkcje narzędziowe serii znajdują się w ExecutionBurstServer.h.

/**
 * Create automated context to manage FMQ-based executions.
 *
 * This function is intended to be used by a service to automatically:
 * 1) Receive data from a provided FMQ
 * 2) Execute a model with the given information
 * 3) Send the result to the created FMQ
 *
 * @param callback Callback used to retrieve memories corresponding to
 *     unrecognized slots.
 * @param requestChannel Input FMQ channel through which the client passes the
 *     request to the service.
 * @param resultChannel Output FMQ channel from which the client can retrieve
 *     the result of the execution.
 * @param executorWithCache Object which maintains a local cache of the
 *     memory pools and executes using the cached memory pools.
 * @result IBurstContext Handle to the burst context.
 */
static sp<ExecutionBurstServer> create(
        const sp<IBurstCallback>& callback, const FmqRequestDescriptor& requestChannel,
        const FmqResultDescriptor& resultChannel,
        std::shared_ptr<IBurstExecutorWithCache> executorWithCache);

/**
 * Create automated context to manage FMQ-based executions.
 *
 * This function is intended to be used by a service to automatically:
 * 1) Receive data from a provided FMQ
 * 2) Execute a model with the given information
 * 3) Send the result to the created FMQ
 *
 * @param callback Callback used to retrieve memories corresponding to
 *     unrecognized slots.
 * @param requestChannel Input FMQ channel through which the client passes the
 *     request to the service.
 * @param resultChannel Output FMQ channel from which the client can retrieve
 *     the result of the execution.
 * @param preparedModel PreparedModel that the burst object was created from.
 *     IPreparedModel::executeSynchronously will be used to perform the
 *     execution.
 * @result IBurstContext Handle to the burst context.
 */
  static sp<ExecutionBurstServer> create(const sp<IBurstCallback>& callback,
                                         const FmqRequestDescriptor& requestChannel,
                                         const FmqResultDescriptor& resultChannel,
                                         IPreparedModel* preparedModel);

Poniżej znajdziesz implementację referencyjną interfejsu seryjnego w przykładowym sterowniku sieci neuronowych w frameworks/ml/nn/driver/sample/SampleDriver.cpp.

Return<void> SamplePreparedModel::configureExecutionBurst(
        const sp<V1_2::IBurstCallback>& callback,
        const MQDescriptorSync<V1_2::FmqRequestDatum>& requestChannel,
        const MQDescriptorSync<V1_2::FmqResultDatum>& resultChannel,
        configureExecutionBurst_cb cb) {
    NNTRACE_FULL(NNTRACE_LAYER_DRIVER, NNTRACE_PHASE_EXECUTION,
                 "SampleDriver::configureExecutionBurst");
    // Alternatively, the burst could be configured via:
    // const sp<V1_2::IBurstContext> burst =
    //         ExecutionBurstServer::create(callback, requestChannel,
    //                                      resultChannel, this);
    //
    // However, this alternative representation does not include a memory map
    // caching optimization, and adds overhead.
    const std::shared_ptr<BurstExecutorWithCache> executorWithCache =
            std::make_shared<BurstExecutorWithCache>(mModel, mDriver, mPoolInfos);
    const sp<V1_2::IBurstContext> burst = ExecutionBurstServer::create(
            callback, requestChannel, resultChannel, executorWithCache);
    if (burst == nullptr) {
        cb(ErrorStatus::GENERAL_FAILURE, {});
    } else {
        cb(ErrorStatus::NONE, burst);
    }
    return Void();
}