Burst-Ausführungen und schnelle Nachrichtenwarteschlangen

Mit Neural Networks HAL 1.2 wird das Konzept der Burst-Ausführungen eingeführt. Burst-Ausführungen sind eine Folge von Ausführungen desselben vorbereiteten Modells, die in schneller Folge erfolgen, z. B. bei der Verarbeitung von Frames einer Kameraaufnahme oder aufeinanderfolgenden Audio-Samples. Mit einem Burst-Objekt wird eine Reihe von Burst-Ausführungen gesteuert und Ressourcen zwischen den Ausführungen werden beibehalten, sodass die Ausführungen einen geringeren Overhead haben. Burst-Objekte ermöglichen drei Optimierungen:

  1. Ein Burst-Objekt wird vor einer Ausführungssequenz erstellt und freigegeben, wenn die Sequenz beendet ist. Daher gibt die Lebensdauer des Burst-Objekts einem Treiber an, wie lange er sich in einem leistungsstarken Zustand befinden sollte.
  2. Mit einem Burst-Objekt können Ressourcen zwischen Ausführungen beibehalten werden. Ein Treiber kann beispielsweise bei der ersten Ausführung ein Speicherobjekt zuordnen und die Zuordnung im Burst-Objekt für die Wiederverwendung bei nachfolgenden Ausführungen zwischenspeichern. Jede im Cache gespeicherte Ressource kann freigegeben werden, wenn das Burst-Objekt zerstört wird oder wenn die NNAPI-Laufzeit das Burst-Objekt darüber informiert, dass die Ressource nicht mehr benötigt wird.
  3. Ein Burst-Objekt verwendet schnelle Nachrichtenwarteschlangen (Fast Message Queues, FMQs) für die Kommunikation zwischen App- und Treiberprozessen. Dadurch kann die Latenz reduziert werden, da das FMQ HIDL umgeht und Daten direkt über einen atomaren zirkulären FIFO im gemeinsam genutzten Speicher an einen anderen Prozess übergibt. Der Consumer-Prozess ruft ein Element aus der Warteschlange ab und beginnt mit der Verarbeitung, indem er entweder die Anzahl der Elemente im FIFO abruft oder auf das Ereignis-Flag des FMQ wartet, das vom Producer signalisiert wird. Dieses Ereignis-Flag ist ein schneller Userspace-Mutex (Futex).

Eine FMQ ist eine Low-Level-Datenstruktur, die keine Lebensdauergarantien für Prozesse bietet und keinen integrierten Mechanismus hat, um festzustellen, ob der Prozess am anderen Ende der FMQ wie erwartet ausgeführt wird. Wenn der Producer für die FMQ stirbt, wartet der Consumer möglicherweise auf Daten, die nie eintreffen. Eine Lösung für dieses Problem besteht darin, dass der Treiber FMQs dem Burst-Objekt auf höherer Ebene zuordnet, um zu erkennen, wann die Burst-Ausführung beendet ist.

Da Burst-Ausführungen mit denselben Argumenten arbeiten und dieselben Ergebnisse wie andere Ausführungspfade zurückgeben, müssen die zugrunde liegenden FMQs dieselben Daten an die und von den NNAPI-Diensttreibern übergeben. FMQs können jedoch nur einfache Datentypen übertragen. Die Übertragung komplexer Daten erfolgt durch Serialisieren und Deserialisieren verschachtelter Puffer (Vektortypen) direkt in den FMQs und durch die Verwendung von HIDL-Callback-Objekten, um Memory-Pool-Handles bei Bedarf zu übertragen. Die Producer-Seite der FMQ muss die Anforderungs- oder Ergebnismeldungen atomar an den Consumer senden. Dazu wird MessageQueue::writeBlocking verwendet, wenn die Warteschlange blockiert wird, oder MessageQueue::write, wenn die Warteschlange nicht blockiert wird.

Schnittstellen für Bilderserien

Die Burst-Schnittstellen für die Neural Networks HAL befinden sich in hardware/interfaces/neuralnetworks/1.2/ und werden unten beschrieben. Weitere Informationen zu Burst-Schnittstellen in der NDK-Ebene finden Sie unter frameworks/ml/nn/runtime/include/NeuralNetworks.h.

types.hal

types.hal definiert den Typ der Daten, die über die FMQ gesendet werden.

  • FmqRequestDatum: Ein einzelnes Element einer serialisierten Darstellung eines Request-Objekts für die Ausführung und ein MeasureTiming-Wert, der über die schnelle Nachrichtenwarteschlange gesendet wird.
  • FmqResultDatum: Ein einzelnes Element einer serialisierten Darstellung der Werte, die von einer Ausführung zurückgegeben werden (ErrorStatus, OutputShapes und Timing), die über die schnelle Nachrichtenwarteschlange zurückgegeben wird.

IBurstContext.hal

IBurstContext.hal definiert das HIDL-Schnittstellenobjekt, das sich im Neural Networks-Dienst befindet.

  • IBurstContext: Kontextobjekt zum Verwalten der Ressourcen eines Bursts.

IBurstCallback.hal

IBurstCallback.hal definiert das HIDL-Schnittstellenobjekt für einen Callback, der von der Neural Networks-Laufzeit erstellt wird. Es wird vom Neural Networks-Dienst verwendet, um hidl_memory-Objekte abzurufen, die Slot-IDs entsprechen.

  • IBurstCallback: Callback-Objekt, das von einem Dienst zum Abrufen von Speicherobjekten verwendet wird.

IPreparedModel.hal

IPreparedModel.hal wird in HAL 1.2 um eine Methode zum Erstellen eines IBurstContext-Objekts aus einem vorbereiteten Modell erweitert.

  • configureExecutionBurst: Konfiguriert ein Burst-Objekt, mit dem mehrere Inferenzvorgänge für ein vorbereitetes Modell in schneller Folge ausgeführt werden.

Unterstützung von Burst-Ausführungen in einem Treiber

Die einfachste Möglichkeit, Burst-Objekte in einem HIDL-NNAPI-Dienst zu unterstützen, ist die Verwendung der Burst-Hilfsfunktion ::android::nn::ExecutionBurstServer::create, die sich in ExecutionBurstServer.h befindet und in den statischen Bibliotheken libneuralnetworks_common und libneuralnetworks_util enthalten ist. Diese Factory-Funktion hat zwei Überladungen:

  • Eine Überladung akzeptiert einen Zeiger auf ein IPreparedModel-Objekt. Diese Hilfsfunktion verwendet die Methode executeSynchronously in einem IPreparedModel-Objekt, um das Modell auszuführen.
  • Eine Überladung akzeptiert ein anpassbares IBurstExecutorWithCache-Objekt, mit dem Ressourcen (z. B. hidl_memory-Zuordnungen) zwischengespeichert werden können, die über mehrere Ausführungen hinweg bestehen bleiben.

Jede Überladung gibt ein IBurstContext-Objekt zurück, das das Burst-Objekt darstellt und einen eigenen Listener-Thread enthält und verwaltet. Dieser Thread empfängt Anfragen von der requestChannel FMQ, führt die Inferenz aus und gibt die Ergebnisse dann über die resultChannel FMQ zurück. Dieser Thread und alle anderen im IBurstContext-Objekt enthaltenen Ressourcen werden automatisch freigegeben, wenn der Client des Burst seine Referenz zu IBurstContext verliert.

Alternativ können Sie eine eigene Implementierung von IBurstContext erstellen, die weiß, wie Nachrichten über die requestChannel- und resultChannel-FMQs gesendet und empfangen werden, die an IPreparedModel::configureExecutionBurst übergeben werden.

Die Burst-Hilfsfunktionen befinden sich in 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);

Im Folgenden finden Sie eine Referenzimplementierung einer Burst-Schnittstelle aus dem Beispieltreiber für neuronale Netze unter 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();
}