Burst-Ausführungen und schnelle Nachrichtenwarteschlangen

Mit neuronalen Netzwerken HAL 1.2 wird das Konzept der Burst-Ausführungen eingeführt. Burst-Ausführungen sind Abfolgen von Ausführungen desselben vorbereiteten Modells, die in schneller Abfolge erfolgen, z. B. bei Bildern von Kameraaufnahmen oder aufeinanderfolgenden Audioinhalten. Ein Burst-Objekt wird verwendet, um eine Reihe von Burst-Ausführungen zu steuern und Ressourcen zwischen Ausführungen beizubehalten, um den Aufwand für Ausführungen zu verringern. Burst-Objekte ermöglichen drei Optimierungen:

  1. Ein Burst-Objekt wird vor einer Abfolge von Ausführungen erstellt und nach dem Ende der Sequenz freigegeben. Aus diesem Grund weist die Lebensdauer des Burst-Objekts einem Treiber darauf hin, wie lange es in einem Hochleistungszustand bleiben soll.
  2. Ein Burst-Objekt kann Ressourcen zwischen Ausführungen beibehalten. Ein Treiber kann beispielsweise bei der ersten Ausführung ein Speicherobjekt zuordnen und die Zuordnung im Burst-Objekt zur Wiederverwendung in nachfolgenden Ausführungen zwischenspeichern. Jede im Cache gespeicherte Ressource kann freigegeben werden, wenn das Burst-Objekt gelöscht wird oder die NNAPI-Laufzeit das Burst-Objekt darüber informiert, dass die Ressource nicht mehr benötigt wird.
  3. Ein Burst-Objekt verwendet für die Kommunikation zwischen Anwendungs- und Treiberprozessen schnelle Nachrichtenwarteschlangen (Fast Message Queues, FMQs). Dies kann die Latenz reduzieren, da der FMQ HIDL umgeht und Daten über einen unteilbaren kreisförmigen FIFO im gemeinsamen Speicher direkt an einen anderen Prozess übergibt. Der Nutzerprozess weiß, dass ein Element aus der Warteschlange entfernt und mit der Verarbeitung begonnen werden muss. Dazu wird entweder die Anzahl der Elemente im FIFO abgefragt oder es wird auf das Ereignis-Flag der FMQ gewartet, das vom Ersteller signalisiert wird. Dieses Ereignis-Flag ist ein schneller Userspace-Mutex (Futex).

Eine FMQ ist eine Low-Level-Datenstruktur, die keine Lebensdauergarantien für alle Prozesse bietet und keinen integrierten Mechanismus hat, um festzustellen, ob der Prozess am anderen Ende der FMQ wie erwartet ausgeführt wird. Wenn also der Ersteller der FMQ abstürzt, wartet der Nutzer möglicherweise auf Daten, die nie ankommen. Eine Lösung für dieses Problem besteht darin, dass der Treiber FMQs dem übergeordneten Burst-Objekt zuordnet, um zu erkennen, wann die Burst-Ausführung beendet wurde.

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 und von den NNAPI-Diensttreibern übergeben. FMQs können jedoch nur alte Datentypen übertragen. Zum Übertragen komplexer Daten werden verschachtelte Zwischenspeicher (Vektortypen) direkt in den FMQs serialisiert und deserialisiert. Außerdem werden HIDL-Callback-Objekte verwendet, um Arbeitsspeicherpool-Handles bei Bedarf zu übertragen. Die Erstellerseite des FMQ muss die Anfrage oder Ergebnisnachrichten in kleinstmöglichen Schritten an den Nutzer senden. Dazu wird MessageQueue::writeBlocking verwendet, wenn die Warteschlange blockiert, oder MessageQueue::write, wenn die Warteschlange nicht blockiert ist.

Burst-Benutzeroberflächen

Die Burst-Schnittstellen für das HAL neuronaler Netzwerke 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.

Typen.hal

types.hal definiert den Datentyp, der über den FMQ gesendet wird.

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

iBurstContext.hal

IBurstContext.hal definiert das HIDL-Schnittstellenobjekt im Dienst für neuronale Netzwerke.

  • IBurstContext: Kontextobjekt zum Verwalten der Ressourcen eines Bursts.

iBurstCallback.hal

IBurstCallback.hal definiert das HIDL-Schnittstellenobjekt für einen Callback, der von der Laufzeit der neuronalen Netzwerke erstellt wurde. Es wird vom Dienst für neuronale Netzwerke 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, das zum Ausführen mehrerer Inferenzen in einem vorbereiteten Modell in schneller Abfolge verwendet wird.

Burst-Ausführungen in einem Treiber unterstützen

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

  • Für die Überlastung kann ein Zeiger auf ein IPreparedModel-Objekt verwendet werden. Diese Dienstfunktion verwendet die Methode executeSynchronously in einem IPreparedModel-Objekt, um das Modell auszuführen.
  • Für eine Überlast wird ein anpassbares IBurstExecutorWithCache-Objekt akzeptiert, mit dem Ressourcen (z. B. hidl_memory-Zuordnungen) im Cache gespeichert werden können, die über mehrere Ausführungen hinweg bestehen bleiben.

Bei jeder Überlastung wird ein IBurstContext-Objekt zurückgegeben, das das Burst-Objekt darstellt und einen eigenen dedizierten Listener-Thread enthält und verwaltet. Dieser Thread empfängt Anfragen vom FMQ requestChannel, führt die Inferenz durch und gibt die Ergebnisse dann über den FMQ resultChannel zurück. Dieser Thread und alle anderen Ressourcen im Objekt IBurstContext werden automatisch freigegeben, wenn der Client des Bursts seinen Verweis auf IBurstContext verliert.

Alternativ können Sie Ihre eigene Implementierung von IBurstContext erstellen, die versteht, wie Nachrichten über die an IPreparedModel::configureExecutionBurst übergebenen FMQs requestChannel und resultChannel gesendet und empfangen werden.

Die Dienstprogrammfunktionen für Bursts finden Sie 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 im Beispieltreiber für neuronale Netzwerke 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();
}