API für neuronale Netzwerke

Die Android Neural Networks API (NNAPI) ist eine Android C API zum Ausführen rechenintensiver Vorgänge für maschinelles Lernen auf Android-Geräten. NNAPI bietet eine Basisfunktionalität für übergeordnete Frameworks für maschinelles Lernen wie TensorFlow Lite und Caffe2, die neuronale Netzwerke erstellen und trainieren. Die API ist auf allen Android-Geräten mit Android 8.1 (API-Level 27) oder höher verfügbar.

NNAPI unterstützt die Inferenz durch Anwenden von Daten von Android-Geräten auf zuvor trainierte, vom Entwickler definierte Modelle. Beispiele für die Ableitung sind die Klassifizierung von Bildern, die Vorhersage des Nutzerverhaltens und die Auswahl geeigneter Antworten auf eine Suchabfrage.

Die On-Device-Inferenz bietet viele Vorteile:

  • Latenz: Sie müssen keine Anfrage über eine Netzwerkverbindung senden und auf eine Antwort warten. Dies kann beispielsweise bei Videoanwendungen von entscheidender Bedeutung sein, die aufeinanderfolgende Frames von einer Kamera verarbeiten.
  • Verfügbarkeit: Die Anwendung wird auch außerhalb der Netzwerkabdeckung ausgeführt.
  • Geschwindigkeit: Neue Hardware speziell für die Verarbeitung neuronaler Netzwerke ermöglicht eine deutlich schnellere Berechnung als eine Allzweck-CPU alleine.
  • Datenschutz: Die Daten verbleiben auf dem Android-Gerät.
  • Kosten: Wenn alle Berechnungen auf dem Android-Gerät ausgeführt werden, ist keine Serverfarm erforderlich.

Es gibt aber auch einige Nachteile, die ein Entwickler im Hinterkopf behalten sollte:

  • Systemauslastung: Die Auswertung neuronaler Netzwerke ist mit hohem Rechenaufwand verbunden, was den Akkuverbrauch erhöhen könnte. Sie sollten den Akkuzustand überwachen, wenn dies für Ihre App ein Problem ist, insbesondere bei Berechnungen mit langer Ausführungszeit.
  • Anwendungsgröße: Achten Sie auf die Größe Ihrer Modelle. Modelle können mehrere Megabyte Speicherplatz einnehmen. Wenn das Bündeln großer Modelle in Ihrem APK Ihre Nutzer unnötig beeinträchtigen würde, sollten Sie in Betracht ziehen, die Modelle nach der App-Installation herunterzuladen, kleinere Modelle zu verwenden oder Ihre Berechnungen in der Cloud auszuführen. NNAPI bietet keine Funktionen zum Ausführen von Modellen in der Cloud.

Im Beispiel für die Android Neural Networks API finden Sie ein Beispiel für die Verwendung der NNAPI.

Informationen zur Laufzeit der Neural Networks API

NNAPI ist für Bibliotheken, Frameworks und Tools für maschinelles Lernen gedacht, mit denen Entwickler ihre Modelle außerhalb des Geräts trainieren und auf Android-Geräten bereitstellen können. Anwendungen nutzen die NNAPI normalerweise nicht direkt, sondern stattdessen übergeordnete Frameworks für maschinelles Lernen. Diese Frameworks könnten wiederum NNAPI verwenden, um hardwarebeschleunigte Inferenzvorgänge auf unterstützten Geräten auszuführen.

Basierend auf den Anforderungen einer App und den Hardwarefunktionen eines Android-Geräts kann die neuronale Netzwerklaufzeit von Android die Rechenarbeitslast effizient auf die verfügbaren On-Device-Prozessoren verteilen, einschließlich dedizierter neuronaler Netzwerkhardware, Grafikprozessoren (GPUs) und digitalen Signalprozessoren (DSPs).

Bei Android-Geräten, die keinen speziellen Anbietertreiber haben, führt die NNAPI-Laufzeit die Anfragen auf der CPU aus.

Abbildung 1 zeigt die allgemeine Systemarchitektur für NNAPI.

Abbildung 1: Systemarchitektur für die Android Neural Networks API

Neural Networks API-Programmiermodell

Um Berechnungen mit NNAPI durchzuführen, müssen Sie zuerst einen gerichteten Graphen erstellen, der die auszuführenden Berechnungen definiert. Diese Berechnungsgrafik bildet in Kombination mit Ihren Eingabedaten (z. B. den Gewichtungen und Verzerrungen, die von einem Framework für maschinelles Lernen übergeben werden) das Modell für die NNAPI-Laufzeitbewertung.

NNAPI verwendet vier Hauptabstraktionen:

  • Modell: Ein Berechnungsdiagramm mit mathematischen Operationen und den durch einen Trainingsprozess erlernten konstanten Werten. Diese Operationen sind spezifisch für neuronale Netzwerke. Sie umfassen die zweidimensionale Faltung (2D), die logistische Aktivierung (Sigmoid), die rektifizierte lineare (ReLU)-Aktivierung und mehr. Das Erstellen eines Modells ist ein synchroner Vorgang. Nach der erfolgreichen Erstellung kann er in mehreren Threads und Kompilierungen wiederverwendet werden. In NNAPI wird ein Modell als ANeuralNetworksModel-Instanz dargestellt.
  • Kompilierung: Stellt eine Konfiguration zum Kompilieren eines NNAPI-Modells in Lower-Level-Code dar. Das Erstellen einer Kompilierung ist ein synchroner Vorgang. Nach erfolgreicher Erstellung kann er in mehreren Threads und Ausführungen wiederverwendet werden. In NNAPI wird jede Kompilierung als ANeuralNetworksCompilation-Instanz dargestellt.
  • Arbeitsspeicher: Steht für freigegebenen Arbeitsspeicher, Dateien, die dem Arbeitsspeicher zugeordnet sind, und ähnliche Arbeitsspeicherzwischenspeicher. Durch die Verwendung eines Arbeitsspeicherpuffers kann die NNAPI-Laufzeit Daten effizienter an Treiber übertragen. Eine App erstellt in der Regel einen Zwischenspeicher für gemeinsam genutzten Speicher, der alle Tensoren enthält, die zum Definieren eines Modells erforderlich sind. Sie können auch Speicherpuffer verwenden, um die Ein- und Ausgaben für eine Ausführungsinstanz zu speichern. In der NNAPI wird jeder Arbeitsspeicherzwischenspeicher als ANeuralNetworksMemory-Instanz dargestellt.
  • Ausführung: Schnittstelle zum Anwenden eines NNAPI-Modells auf eine Reihe von Eingaben und zum Erfassen der Ergebnisse. Die Ausführung kann synchron oder asynchron erfolgen.

    Bei einer asynchronen Ausführung können mehrere Threads auf dieselbe Ausführung warten. Nach Abschluss dieser Ausführung werden alle Threads freigegeben.

    In NNAPI wird jede Ausführung als ANeuralNetworksExecution-Instanz dargestellt.

Abbildung 2 zeigt den grundlegenden Programmierfluss.

Abbildung 2: Programmierablauf für die Android Neural Networks API

Im weiteren Verlauf dieses Abschnitts werden die Schritte zum Einrichten Ihres NNAPI-Modells beschrieben, um Berechnungen durchzuführen, das Modell zu kompilieren und das kompilierte Modell auszuführen.

Zugriff auf Trainingsdaten gewähren

Die trainierten Daten zu Gewichtungen und Verzerrungen werden wahrscheinlich in einer Datei gespeichert. Damit die NNAPI-Laufzeit effizient auf diese Daten zugreifen kann, erstellen Sie eine ANeuralNetworksMemory-Instanz. Rufen Sie dazu die Funktion ANeuralNetworksMemory_createFromFd() auf und übergeben Sie den Dateideskriptor der geöffneten Datendatei. Außerdem geben Sie Arbeitsspeicherschutz-Flags und einen Offset an, bei dem die Region des freigegebenen Speichers in der Datei beginnt.

// Create a memory buffer from the file that contains the trained data
ANeuralNetworksMemory* mem1 = NULL;
int fd = open("training_data", O_RDONLY);
ANeuralNetworksMemory_createFromFd(file_size, PROT_READ, fd, 0, &mem1);

Auch wenn wir in diesem Beispiel nur eine ANeuralNetworksMemory-Instanz für alle Gewichtungen verwenden, ist es möglich, mehr als eine ANeuralNetworksMemory-Instanz für mehrere Dateien zu verwenden.

Native Hardwarezwischenspeicher verwenden

Sie können native Hardwarezwischenspeicher für Modelleingaben, -ausgaben und konstante Operandenwerte verwenden. In bestimmten Fällen kann ein NNAPI-Beschleuniger auf AHardwareBuffer-Objekte zugreifen, ohne dass der Treiber die Daten kopieren muss. AHardwareBuffer hat viele verschiedene Konfigurationen, und nicht jeder NNAPI-Beschleuniger unterstützt möglicherweise alle diese Konfigurationen. Lesen Sie wegen dieser Einschränkung die Einschränkungen in der Referenzdokumentation zu ANeuralNetworksMemory_createFromAHardwareBuffer und führen Sie Tests im Voraus auf Zielgeräten durch, um sicherzustellen, dass Kompilierungen und Ausführungen, die AHardwareBuffer verwenden, wie erwartet funktionieren. Verwenden Sie dabei die Gerätezuweisung, um den Beschleuniger anzugeben.

Damit die NNAPI-Laufzeit auf ein AHardwareBuffer-Objekt zugreifen kann, erstellen Sie eine ANeuralNetworksMemory-Instanz. Dazu rufen Sie die Funktion ANeuralNetworksMemory_createFromAHardwareBuffer auf und übergeben das Objekt AHardwareBuffer, wie im folgenden Codebeispiel gezeigt:

// Configure and create AHardwareBuffer object
AHardwareBuffer_Desc desc = ...
AHardwareBuffer* ahwb = nullptr;
AHardwareBuffer_allocate(&desc, &ahwb);

// Create ANeuralNetworksMemory from AHardwareBuffer
ANeuralNetworksMemory* mem2 = NULL;
ANeuralNetworksMemory_createFromAHardwareBuffer(ahwb, &mem2);

Wenn die NNAPI nicht mehr auf das AHardwareBuffer-Objekt zugreifen muss, geben Sie die entsprechende ANeuralNetworksMemory-Instanz kostenlos:

ANeuralNetworksMemory_free(mem2);

Hinweis:

  • Sie können AHardwareBuffer nur für den gesamten Zwischenspeicher verwenden. Mit einem ARect-Parameter ist dies nicht möglich.
  • Die NNAPI-Laufzeit leert den Zwischenspeicher nicht. Achten Sie darauf, dass der Eingabe- und Ausgabezwischenspeicher zugänglich ist, bevor Sie die Ausführung planen.
  • Deskriptoren für Synchronisierungsdateien werden nicht unterstützt.
  • Bei einem AHardwareBuffer mit anbieterspezifischen Formaten und Nutzungsbits muss der Anbieter selbst bestimmen, ob der Client oder der Treiber für das Leeren des Cache verantwortlich ist.

Modell

Ein Modell ist die grundlegende Recheneinheit in NNAPI. Jedes Modell wird durch einen oder mehrere Operanden und Operationen definiert.

Operanden

Operanden sind Datenobjekte, die zur Definition der Grafik verwendet werden. Dazu gehören die Ein- und Ausgaben des Modells, die Zwischenknoten mit den Daten, die von einem Vorgang zum anderen fließen, und die Konstanten, die an diese Vorgänge übergeben werden.

Es gibt zwei Arten von Operanden, die NNAPI-Modellen hinzugefügt werden können: Skalare und Tensoren.

Ein Skalar stellt einen einzelnen Wert dar. NNAPI unterstützt skalare Werte in booleschen Formaten, 16-Bit-Gleitkommazahlen, 32-Bit-Gleitkommazahlen, 32-Bit-Ganzzahlen und 32-Bit-Ganzzahlen ohne Vorzeichen.

Die meisten Vorgänge in NNAPI beinhalten Tensoren. Tensoren sind n-dimensionale Arrays. NNAPI unterstützt Tensoren mit 16-Bit-Gleitkomma-, 32-Bit-Gleitkomma-, quantisierten 8-Bit-, 16-Bit-quantisierten, 32-Bit-Ganzzahl- und booleschen 8-Bit-Werten.

Abbildung 3 zeigt beispielsweise ein Modell mit zwei Vorgängen: einer Addition gefolgt von einer Multiplikation. Das Modell verwendet einen Eingabetensor und erzeugt einen Ausgabetensor.

Abbildung 3: Beispiel für Operanden für ein NNAPI-Modell

Das Modell oben hat sieben Operanden. Diese Operanden werden implizit über den Index in der Reihenfolge identifiziert, in der sie dem Modell hinzugefügt werden. Der erste hinzugefügte Operand hat einen Index von 0, der zweite einen Index von 1 usw. Die Operanden 1, 2, 3 und 5 sind konstante Operanden.

Die Reihenfolge, in der Sie die Operanden hinzufügen, spielt keine Rolle. Beispielsweise könnte der Modellausgabeoperand der erste sein, der hinzugefügt wird. Wichtig ist, den richtigen Indexwert zu verwenden, wenn auf einen Operanden verwiesen wird.

Operanden haben Typen. Diese werden angegeben, wenn sie dem Modell hinzugefügt werden.

Ein Operand kann nicht sowohl als Eingabe als auch als Ausgabe eines Modells verwendet werden.

Jeder Operand muss entweder eine Modelleingabe, eine Konstante oder der Ausgabeoperand von genau einem Vorgang sein.

Weitere Informationen zur Verwendung von Operanden finden Sie unter Weitere Informationen zu Operanden.

Aufgaben und Ablauf

Eine Operation gibt die auszuführenden Berechnungen an. Jeder Vorgang besteht aus folgenden Elementen:

  • einen Vorgangstyp (z. B. Addition, Multiplikation, Faltung),
  • eine Liste von Indexen der Operanden, die die Operation für die Eingabe verwendet, und
  • Eine Liste von Indexen der Operanden, die der Vorgang für die Ausgabe verwendet.

Die Reihenfolge in diesen Listen ist wichtig. In der NNAPI API-Referenz finden Sie die erwarteten Ein- und Ausgaben der einzelnen Vorgangstypen.

Sie müssen die Operanden hinzufügen, die eine Operation dem Modell nutzt oder erzeugt, bevor Sie den Vorgang hinzufügen.

Die Reihenfolge, in der Sie Vorgänge hinzufügen, spielt keine Rolle. NNAPI stützt sich auf die Abhängigkeiten, die durch die Berechnungsgrafik der Operanden und Operationen ermittelt werden, um die Reihenfolge zu bestimmen, in der Vorgänge ausgeführt werden.

Die von der NNAPI unterstützten Vorgänge sind in der folgenden Tabelle zusammengefasst:

Kategorie Aufgaben und Ablauf
Elementweise mathematische Operationen
Tensor-Manipulation
Image-Vorgänge
Lookup-Vorgänge
Normalisierungsvorgänge
Faltungsvorgänge
Pooling-Vorgänge
Aktivierungsvorgänge
Andere Vorgänge

Bekanntes Problem in API-Ebene 28: Wenn Sie ANEURALNETWORKS_TENSOR_QUANT8_ASYMM-Tensoren an den Vorgang ANEURALNETWORKS_PAD übergeben, der unter Android 9 (API-Ebene 28) und höher verfügbar ist, stimmt die Ausgabe von NNAPI möglicherweise nicht mit der Ausgabe von übergeordneten Frameworks für maschinelles Lernen wie TensorFlow Lite überein. Stattdessen sollten Sie nur ANEURALNETWORKS_TENSOR_FLOAT32 übergeben. Das Problem wurde in Android 10 (API-Level 29) und höher behoben.

Modelle erstellen

Im folgenden Beispiel erstellen wir das Zwei-Operation-Modell aus Abbildung 3.

So erstellen Sie das Modell:

  1. Rufen Sie die Funktion ANeuralNetworksModel_create() auf, um ein leeres Modell zu definieren.

    ANeuralNetworksModel* model = NULL;
    ANeuralNetworksModel_create(&model);
    
  2. Fügen Sie dem Modell die Operanden hinzu, indem Sie ANeuralNetworks_addOperand() aufrufen. Ihre Datentypen werden mithilfe der ANeuralNetworksOperandType-Datenstruktur definiert.

    // In our example, all our tensors are matrices of dimension [3][4]
    ANeuralNetworksOperandType tensor3x4Type;
    tensor3x4Type.type = ANEURALNETWORKS_TENSOR_FLOAT32;
    tensor3x4Type.scale = 0.f;    // These fields are used for quantized tensors
    tensor3x4Type.zeroPoint = 0;  // These fields are used for quantized tensors
    tensor3x4Type.dimensionCount = 2;
    uint32_t dims[2] = {3, 4};
    tensor3x4Type.dimensions = dims;

    // We also specify operands that are activation function specifiers ANeuralNetworksOperandType activationType; activationType.type = ANEURALNETWORKS_INT32; activationType.scale = 0.f; activationType.zeroPoint = 0; activationType.dimensionCount = 0; activationType.dimensions = NULL;

    // Now we add the seven operands, in the same order defined in the diagram ANeuralNetworksModel_addOperand(model, &tensor3x4Type); // operand 0 ANeuralNetworksModel_addOperand(model, &tensor3x4Type); // operand 1 ANeuralNetworksModel_addOperand(model, &activationType); // operand 2 ANeuralNetworksModel_addOperand(model, &tensor3x4Type); // operand 3 ANeuralNetworksModel_addOperand(model, &tensor3x4Type); // operand 4 ANeuralNetworksModel_addOperand(model, &activationType); // operand 5 ANeuralNetworksModel_addOperand(model, &tensor3x4Type); // operand 6
  3. Verwenden Sie für Operanden mit konstanten Werten die Funktionen ANeuralNetworksModel_setOperandValue() und ANeuralNetworksModel_setOperandValueFromMemory(), z. B. Gewichtungen und Verzerrungen, die die Anwendung aus einem Trainingsprozess erhält.

    Im folgenden Beispiel legen wir aus der Trainingsdatendatei konstante Werte fest, die dem Arbeitsspeicherpuffer entsprechen, den wir unter Zugriff auf Trainingsdaten bereitstellen erstellt haben.

    // In our example, operands 1 and 3 are constant tensors whose values were
    // established during the training process
    const int sizeOfTensor = 3 * 4 * 4;    // The formula for size calculation is dim0 * dim1 * elementSize
    ANeuralNetworksModel_setOperandValueFromMemory(model, 1, mem1, 0, sizeOfTensor);
    ANeuralNetworksModel_setOperandValueFromMemory(model, 3, mem1, sizeOfTensor, sizeOfTensor);

    // We set the values of the activation operands, in our example operands 2 and 5 int32_t noneValue = ANEURALNETWORKS_FUSED_NONE; ANeuralNetworksModel_setOperandValue(model, 2, &noneValue, sizeof(noneValue)); ANeuralNetworksModel_setOperandValue(model, 5, &noneValue, sizeof(noneValue));
  4. Fügen Sie für jeden Vorgang in dem gerichteten Graphen, den Sie berechnen möchten, den Vorgang zu Ihrem Modell hinzu. Rufen Sie dazu die Funktion ANeuralNetworksModel_addOperation() auf.

    Als Parameter für diesen Aufruf muss Ihre App Folgendes bereitstellen:

    • Vorgangstyp
    • Anzahl der Eingabewerte
    • Array der Indexe für Eingabeoperanden
    • die Anzahl der Ausgabewerte
    • Array der Indexe für Ausgabeoperanden

    Ein Operand kann nicht für die Eingabe und die Ausgabe desselben Vorgangs verwendet werden.

    // We have two operations in our example
    // The first consumes operands 1, 0, 2, and produces operand 4
    uint32_t addInputIndexes[3] = {1, 0, 2};
    uint32_t addOutputIndexes[1] = {4};
    ANeuralNetworksModel_addOperation(model, ANEURALNETWORKS_ADD, 3, addInputIndexes, 1, addOutputIndexes);

    // The second consumes operands 3, 4, 5, and produces operand 6 uint32_t multInputIndexes[3] = {3, 4, 5}; uint32_t multOutputIndexes[1] = {6}; ANeuralNetworksModel_addOperation(model, ANEURALNETWORKS_MUL, 3, multInputIndexes, 1, multOutputIndexes);
  5. Rufen Sie die Funktion ANeuralNetworksModel_identifyInputsAndOutputs() auf, um festzulegen, welche Operanden das Modell als Ein- und Ausgaben behandeln soll.

    // Our model has one input (0) and one output (6)
    uint32_t modelInputIndexes[1] = {0};
    uint32_t modelOutputIndexes[1] = {6};
    ANeuralNetworksModel_identifyInputsAndOutputs(model, 1, modelInputIndexes, 1 modelOutputIndexes);
    
  6. Geben Sie optional an, ob ANEURALNETWORKS_TENSOR_FLOAT32 mit einem Bereich oder einer Genauigkeit berechnet werden darf, die nur im 16-Bit-Gleitkommaformat von IEEE 754 liegen. Rufen Sie dazu ANeuralNetworksModel_relaxComputationFloat32toFloat16() auf.

  7. Rufen Sie ANeuralNetworksModel_finish() auf, um die Definition des Modells abzuschließen. Wenn keine Fehler vorhanden sind, gibt diese Funktion den Ergebniscode ANEURALNETWORKS_NO_ERROR zurück.

    ANeuralNetworksModel_finish(model);
    

Nachdem Sie ein Modell erstellt haben, können Sie es beliebig oft kompilieren und jede Kompilierung beliebig oft ausführen.

Ablaufsteuerung

So binden Sie den Kontrollablauf in ein NNAPI-Modell ein:

  1. Erstellen Sie die entsprechenden Teildiagramme der Ausführung (die Teilgrafiken then und else für eine IF-Anweisung sowie die Teilgrafiken condition und body für eine WHILE-Schleife) als eigenständige ANeuralNetworksModel*-Modelle:

    ANeuralNetworksModel* thenModel = makeThenModel();
    ANeuralNetworksModel* elseModel = makeElseModel();
    
  2. Erstellen Sie Operanden, die in dem Modell, das den Kontrollfluss enthält, auf diese Modelle verweisen:

    ANeuralNetworksOperandType modelType = {
        .type = ANEURALNETWORKS_MODEL,
    };
    ANeuralNetworksModel_addOperand(model, &modelType);  // kThenOperandIndex
    ANeuralNetworksModel_addOperand(model, &modelType);  // kElseOperandIndex
    ANeuralNetworksModel_setOperandValueFromModel(model, kThenOperandIndex, &thenModel);
    ANeuralNetworksModel_setOperandValueFromModel(model, kElseOperandIndex, &elseModel);
    
  3. Fügen Sie den Vorgang zur Steuerung hinzu:

    uint32_t inputs[] = {kConditionOperandIndex,
                         kThenOperandIndex,
                         kElseOperandIndex,
                         kInput1, kInput2, kInput3};
    uint32_t outputs[] = {kOutput1, kOutput2};
    ANeuralNetworksModel_addOperation(model, ANEURALNETWORKS_IF,
                                      std::size(inputs), inputs,
                                      std::size(output), outputs);
    

Compilation

Der Kompilierungsschritt bestimmt, auf welchen Prozessoren Ihr Modell ausgeführt wird, und fordert die entsprechenden Treiber auf, sich auf die Ausführung vorzubereiten. Dies kann die Generierung von Maschinencode speziell für die Prozessoren umfassen, auf denen Ihr Modell ausgeführt wird.

So kompilieren Sie ein Modell:

  1. Rufen Sie die Funktion ANeuralNetworksCompilation_create() auf, um eine neue Kompilierungsinstanz zu erstellen.

    // Compile the model
    ANeuralNetworksCompilation* compilation;
    ANeuralNetworksCompilation_create(model, &compilation);
    

    Optional können Sie mithilfe der Gerätezuweisung explizit auswählen, auf welchen Geräten die Ausführung erfolgen soll.

  2. Sie können optional beeinflussen, wie die Laufzeit zwischen der Akkunutzung und der Ausführungsgeschwindigkeit abstimmt. Rufen Sie dazu ANeuralNetworksCompilation_setPreference() auf.

    // Ask to optimize for low power consumption
    ANeuralNetworksCompilation_setPreference(compilation, ANEURALNETWORKS_PREFER_LOW_POWER);
    

    Folgende Einstellungen können Sie festlegen:

    • ANEURALNETWORKS_PREFER_LOW_POWER: Führen Sie die Ausführung so aus, dass der Akku möglichst wenig entladen wird. Dies ist bei häufig ausgeführten Kompilierungen sinnvoll.
    • ANEURALNETWORKS_PREFER_FAST_SINGLE_ANSWER: Gibt bevorzugt eine einzelne Antwort so schnell wie möglich zurück, auch wenn dies einen höheren Stromverbrauch verursacht. Das ist die Standardeinstellung.
    • ANEURALNETWORKS_PREFER_SUSTAINED_SPEED: Maximiert den Durchsatz aufeinanderfolgender Frames, z. B. bei der Verarbeitung aufeinanderfolgender Frames, die von der Kamera kommen.
  3. Sie können optional das Kompilierungs-Caching einrichten. Dazu rufen Sie ANeuralNetworksCompilation_setCaching auf.

    // Set up compilation caching
    ANeuralNetworksCompilation_setCaching(compilation, cacheDir, token);
    

    Verwenden Sie getCodeCacheDir() für cacheDir. Der angegebene token muss für jedes Modell in der Anwendung eindeutig sein.

  4. Beenden Sie die Kompilierungsdefinition durch Aufrufen von ANeuralNetworksCompilation_finish(). Wenn keine Fehler vorhanden sind, gibt diese Funktion den Ergebniscode ANEURALNETWORKS_NO_ERROR zurück.

    ANeuralNetworksCompilation_finish(compilation);
    

Geräteerkennung und -zuweisung

Auf Android-Geräten mit Android 10 (API-Level 29) und höher bietet NNAPI Funktionen, mit denen ML-Framework-Bibliotheken und -Apps Informationen über die verfügbaren Geräte abrufen und Geräte angeben können, die für die Ausführung verwendet werden sollen. Durch die Angabe von Informationen zu den verfügbaren Geräten können Apps die genaue Version der auf dem Gerät gefundenen Treiber abrufen, um bekannte Inkompatibilitäten zu vermeiden. Dadurch, dass Apps angeben können, welche Geräte verschiedene Abschnitte eines Modells ausführen sollen, können Anwendungen für das Android-Gerät optimiert werden, auf dem sie bereitgestellt werden.

Geräteerkennung

Verwenden Sie ANeuralNetworks_getDeviceCount, um die Anzahl der verfügbaren Geräte abzurufen. Verwenden Sie für jedes Gerät ANeuralNetworks_getDevice, um eine ANeuralNetworksDevice-Instanz als Verweis auf dieses Gerät festzulegen.

Sobald Sie eine Gerätereferenz haben, können Sie mithilfe der folgenden Funktionen zusätzliche Informationen zu diesem Gerät ermitteln:

Gerätezuweisung

Mit ANeuralNetworksModel_getSupportedOperationsForDevices können Sie feststellen, welche Vorgänge eines Modells auf bestimmten Geräten ausgeführt werden können.

Wenn Sie festlegen möchten, welche Beschleuniger für die Ausführung verwendet werden, rufen Sie ANeuralNetworksCompilation_createForDevices anstelle von ANeuralNetworksCompilation_create auf. Verwenden Sie das resultierende ANeuralNetworksCompilation-Objekt wie gewohnt. Die Funktion gibt einen Fehler zurück, wenn das angegebene Modell Vorgänge enthält, die von den ausgewählten Geräten nicht unterstützt werden.

Wenn mehrere Geräte angegeben sind, ist die Laufzeit für die Verteilung der Arbeit auf die Geräte verantwortlich.

Ähnlich wie bei anderen Geräten wird die NNAPI-CPU-Implementierung durch eine ANeuralNetworksDevice mit dem Namen nnapi-reference und dem Typ ANEURALNETWORKS_DEVICE_TYPE_CPU dargestellt. Beim Aufrufen von ANeuralNetworksCompilation_createForDevices wird die CPU-Implementierung nicht verwendet, um Fehlerfälle für die Modellkompilierung und -ausführung zu verarbeiten.

Es liegt in der Verantwortung einer Anwendung, ein Modell in Untermodelle zu unterteilen, die auf den angegebenen Geräten ausgeführt werden können. Anwendungen, die keine manuelle Partitionierung erfordern, sollten weiterhin den einfacheren ANeuralNetworksCompilation_create aufrufen, um alle verfügbaren Geräte (einschließlich der CPU) zu verwenden, um das Modell zu beschleunigen. Wenn das Modell von den Geräten, die Sie mit ANeuralNetworksCompilation_createForDevices angegeben haben, nicht vollständig unterstützt werden konnte, wird ANEURALNETWORKS_BAD_DATA zurückgegeben.

Modellpartitionierung

Wenn für das Modell mehrere Geräte verfügbar sind, verteilt die NNAPI-Laufzeit die Arbeit auf die Geräte. Wenn beispielsweise ANeuralNetworksCompilation_createForDevices mehr als ein Gerät zur Verfügung gestellt wurde, werden alle angegebenen Geräte bei der Zuweisung der Arbeit berücksichtigt. Wenn das CPU-Gerät nicht in der Liste aufgeführt ist, wird die CPU-Ausführung deaktiviert. Bei Verwendung von ANeuralNetworksCompilation_create werden alle verfügbaren Geräte berücksichtigt, einschließlich der CPU.

Für die Verteilung wird aus der Liste der verfügbaren Geräte für jeden Vorgang im Modell das Gerät ausgewählt, das den Vorgang unterstützt, und die beste Leistung, d.h. die schnellste Ausführungszeit oder den niedrigsten Stromverbrauch, je nach der vom Client festgelegten Ausführungspräferenz, angegeben. Dieser Partitionierungsalgorithmus berücksichtigt keine möglichen Ineffizienzen, die durch die E/A zwischen den verschiedenen Prozessoren verursacht werden. Daher ist es wichtig, ein Profil für die resultierende Anwendung zu erstellen, wenn mehrere Prozessoren angegeben werden (entweder explizit bei Verwendung von ANeuralNetworksCompilation_createForDevices oder implizit über ANeuralNetworksCompilation_create).

Um zu verstehen, wie Ihr Modell von der NNAPI partitioniert wurde, suchen Sie in den Android-Logs nach einer Nachricht (auf INFO-Ebene mit dem Tag ExecutionPlan):

ModelBuilder::findBestDeviceForEachOperation(op-name): device-index

op-name ist der beschreibende Name des Vorgangs in der Grafik und device-index der Index des jeweiligen Geräts in der Liste der Geräte. Diese Liste wird für ANeuralNetworksCompilation_createForDevices übergeben. Wenn Sie ANeuralNetworksCompilation_createForDevices verwenden, ist dies die Liste der Geräte, die bei einer Iteration über alle Geräte mit ANeuralNetworks_getDeviceCount und ANeuralNetworks_getDevice zurückgegeben wird.

Die Nachricht (auf INFO-Ebene mit dem Tag ExecutionPlan):

ModelBuilder::partitionTheWork: only one best device: device-name

Diese Meldung zeigt an, dass die gesamte Grafik auf dem Gerät device-name beschleunigt wurde.

Umsetzung

Beim Ausführungsschritt wird das Modell auf eine Reihe von Eingaben angewendet und die Rechenausgaben werden in einem oder mehreren Nutzerzwischenspeichern oder Arbeitsspeicherbereichen gespeichert, die Ihre Anwendung zugewiesen hat.

So führen Sie ein kompiliertes Modell aus:

  1. Rufen Sie die Funktion ANeuralNetworksExecution_create() auf, um eine neue Ausführungsinstanz zu erstellen.

    // Run the compiled model against a set of inputs
    ANeuralNetworksExecution* run1 = NULL;
    ANeuralNetworksExecution_create(compilation, &run1);
    
  2. Geben Sie an, wo Ihre Anwendung die Eingabewerte für die Berechnung liest. Ihre App kann Eingabewerte entweder aus einem Nutzerpuffer oder aus einem zugewiesenen Arbeitsspeicherbereich lesen, indem sie ANeuralNetworksExecution_setInput() bzw. ANeuralNetworksExecution_setInputFromMemory() aufruft.

    // Set the single input to our sample model. Since it is small, we won't use a memory buffer
    float32 myInput[3][4] = { ...the data... };
    ANeuralNetworksExecution_setInput(run1, 0, NULL, myInput, sizeof(myInput));
    
  3. Geben Sie an, wohin die Anwendung die Ausgabewerte schreibt. Ihre Anwendung kann Ausgabewerte entweder in einen Nutzerpuffer oder in einen zugewiesenen Arbeitsspeicherbereich schreiben. Dazu muss ANeuralNetworksExecution_setOutput() bzw. ANeuralNetworksExecution_setOutputFromMemory() aufgerufen werden.

    // Set the output
    float32 myOutput[3][4];
    ANeuralNetworksExecution_setOutput(run1, 0, NULL, myOutput, sizeof(myOutput));
    
  4. Um die Ausführung zu starten, rufen Sie die Funktion ANeuralNetworksExecution_startCompute() auf. Wenn keine Fehler vorhanden sind, gibt diese Funktion den Ergebniscode ANEURALNETWORKS_NO_ERROR zurück.

    // Starts the work. The work proceeds asynchronously
    ANeuralNetworksEvent* run1_end = NULL;
    ANeuralNetworksExecution_startCompute(run1, &run1_end);
    
  5. Rufen Sie die Funktion ANeuralNetworksEvent_wait() auf, um auf den Abschluss der Ausführung zu warten. Wenn die Ausführung erfolgreich war, gibt diese Funktion den Ergebniscode ANEURALNETWORKS_NO_ERROR zurück. Der Warten kann in einem anderen Thread erfolgen als dem, der die Ausführung startet.

    // For our example, we have no other work to do and will just wait for the completion
    ANeuralNetworksEvent_wait(run1_end);
    ANeuralNetworksEvent_free(run1_end);
    ANeuralNetworksExecution_free(run1);
    
  6. Optional können Sie einen anderen Satz von Eingaben auf das kompilierte Modell anwenden. Dazu verwenden Sie dieselbe Kompilierungsinstanz, um eine neue ANeuralNetworksExecution-Instanz zu erstellen.

    // Apply the compiled model to a different set of inputs
    ANeuralNetworksExecution* run2;
    ANeuralNetworksExecution_create(compilation, &run2);
    ANeuralNetworksExecution_setInput(run2, ...);
    ANeuralNetworksExecution_setOutput(run2, ...);
    ANeuralNetworksEvent* run2_end = NULL;
    ANeuralNetworksExecution_startCompute(run2, &run2_end);
    ANeuralNetworksEvent_wait(run2_end);
    ANeuralNetworksEvent_free(run2_end);
    ANeuralNetworksExecution_free(run2);
    

Synchrone Ausführung

Bei der asynchronen Ausführung benötigen Sie Zeit, um Threads zu erzeugen und zu synchronisieren. Darüber hinaus kann die Latenz stark schwanken. Die längsten Verzögerungen erreichen bis zu 500 Mikrosekunden zwischen der Benachrichtigung oder dem Aufwachen eines Threads und der schließlich an einen CPU-Kern gebundenen Zeit.

Um die Latenz zu verbessern, können Sie stattdessen eine Anwendung anweisen, einen synchronen Inferenzaufruf an die Laufzeit zu senden. Dieser Aufruf wird erst zurückgegeben, wenn eine Inferenz abgeschlossen wurde, und nicht nach dem Start einer Inferenz. Anstatt ANeuralNetworksExecution_startCompute für einen asynchronen Inferenzaufruf an die Laufzeit aufzurufen, ruft die Anwendung ANeuralNetworksExecution_compute auf, um einen synchronen Aufruf an die Laufzeit auszuführen. Ein Aufruf von ANeuralNetworksExecution_compute nimmt kein ANeuralNetworksEvent an und ist nicht mit einem Aufruf von ANeuralNetworksEvent_wait gekoppelt.

Burst-Ausführungen

Auf Android-Geräten mit Android 10 (API-Level 29) und höher unterstützt die NNAPI Burst-Ausführungen über das Objekt ANeuralNetworksBurst. Burst-Ausführungen sind eine Abfolge von Ausführungen derselben Kompilierung in kurzer Folge, z. B. auf Frames einer Kameraaufnahme oder aufeinanderfolgende Audioproben. Die Verwendung von ANeuralNetworksBurst-Objekten kann zu schnelleren Ausführungen führen, da sie Beschleunigern zeigen, dass Ressourcen zwischen den Ausführungen wiederverwendet werden können und dass Beschleuniger für die Dauer des Bursts in einem Hochleistungszustand bleiben sollen.

Mit ANeuralNetworksBurst wird nur eine kleine Änderung am normalen Ausführungspfad vorgenommen. Ein Burst-Objekt wird mit ANeuralNetworksBurst_create erstellt, wie im folgenden Code-Snippet gezeigt:

// Create burst object to be reused across a sequence of executions
ANeuralNetworksBurst* burst = NULL;
ANeuralNetworksBurst_create(compilation, &burst);

Burst-Ausführungen sind synchron. Anstatt jedoch ANeuralNetworksExecution_compute zum Ausführen der einzelnen Inferenzen zu verwenden, koppeln Sie die verschiedenen ANeuralNetworksExecution-Objekte mit demselben ANeuralNetworksBurst in Aufrufen der Funktion ANeuralNetworksExecution_burstCompute.

// Create and configure first execution object
// ...

// Execute using the burst object
ANeuralNetworksExecution_burstCompute(execution1, burst);

// Use results of first execution and free the execution object
// ...

// Create and configure second execution object
// ...

// Execute using the same burst object
ANeuralNetworksExecution_burstCompute(execution2, burst);

// Use results of second execution and free the execution object
// ...

Geben Sie das ANeuralNetworksBurst-Objekt mit ANeuralNetworksBurst_free kostenlos, wenn es nicht mehr benötigt wird.

// Cleanup
ANeuralNetworksBurst_free(burst);

Asynchrone Befehlswarteschlangen und abgegrenzte Ausführung

Ab Android 11 unterstützt die NNAPI eine zusätzliche Möglichkeit, die asynchrone Ausführung über die Methode ANeuralNetworksExecution_startComputeWithDependencies() zu planen. Bei dieser Methode wartet die Ausführung, bis alle abhängigen Ereignisse signalisiert werden, bevor die Bewertung gestartet wird. Sobald die Ausführung abgeschlossen ist und die Ausgaben verarbeitet werden können, wird das zurückgegebene Ereignis signalisiert.

Je nachdem, welche Geräte die Ausführung übernehmen, wird das Ereignis möglicherweise durch einen Synchronisierungszaun gestützt. Sie müssen ANeuralNetworksEvent_wait() aufrufen, um auf das Ereignis zu warten und die von der Ausführung verwendeten Ressourcen wiederherzustellen. Sie können Synchronisierungszäune mit ANeuralNetworksEvent_createFromSyncFenceFd() in ein Ereignisobjekt importieren und mit ANeuralNetworksEvent_getSyncFenceFd() aus einem Ereignisobjekt Synchronisierungszäune exportieren.

Ausgaben dynamisch angepasst

Verwenden Sie ANeuralNetworksExecution_getOutputOperandRank und ANeuralNetworksExecution_getOutputOperandDimensions, um Modelle zu unterstützen, bei denen die Größe der Ausgabe von den Eingabedaten abhängt, also bei denen die Größe bei der Modellausführung nicht bestimmt werden kann.

Das folgende Codebeispiel zeigt, wie das funktioniert:

// Get the rank of the output
uint32_t myOutputRank = 0;
ANeuralNetworksExecution_getOutputOperandRank(run1, 0, &myOutputRank);

// Get the dimensions of the output
std::vector<uint32_t> myOutputDimensions(myOutputRank);
ANeuralNetworksExecution_getOutputOperandDimensions(run1, 0, myOutputDimensions.data());

Bereinigung

Der Bereinigungsschritt sorgt dafür, dass interne Ressourcen freigegeben werden, die für die Berechnung verwendet werden.

// Cleanup
ANeuralNetworksCompilation_free(compilation);
ANeuralNetworksModel_free(model);
ANeuralNetworksMemory_free(mem1);

Fehlerverwaltung und CPU-Fallback

Tritt während der Partitionierung ein Fehler auf, greift NNAPI unter Umständen auf seine eigene CPU-Implementierung des einen oder mehrerer Vorgänge zurück, wenn ein Treiber ein Modell (Teil eines A) oder ein kompiliertes Modell (Teil eines A) nicht kompilieren kann.

Wenn der NNAPI-Client optimierte Versionen des Vorgangs enthält (z. B. TFLite), kann es vorteilhaft sein, das CPU-Fallback zu deaktivieren und die Fehler mit der optimierten Vorgangsimplementierung des Clients zu beheben.

Wenn in Android 10 die Kompilierung mit ANeuralNetworksCompilation_createForDevices durchgeführt wird, wird das CPU-Fallback deaktiviert.

In Android P wird die NNAPI-Ausführung auf die CPU zurückgesetzt, wenn die Ausführung auf dem Treiber fehlschlägt. Dies gilt auch für Android 10, wenn ANeuralNetworksCompilation_create statt ANeuralNetworksCompilation_createForDevices verwendet wird.

Die erste Ausführung greift auf diese einzelne Partition zurück. Wenn auch diese fehlschlägt, wird das gesamte Modell auf der CPU wiederholt.

Wenn die Partitionierung oder Kompilierung fehlschlägt, wird das gesamte Modell auf der CPU ausprobiert.

Es gibt Fälle, in denen einige Vorgänge nicht auf der CPU unterstützt werden. In solchen Fällen schlägt die Kompilierung oder Ausführung fehl, anstatt zurückgesetzt zu werden.

Auch nach dem Deaktivieren des CPU-Fallbacks kann es im Modell noch Vorgänge geben, die auf der CPU geplant sind. Wenn sich die CPU in der Liste der für ANeuralNetworksCompilation_createForDevices bereitgestellten Prozessoren befindet und entweder der einzige Prozessor ist, der diese Vorgänge unterstützt, oder der Prozessor ist, der für diese Vorgänge die beste Leistung behauptet, wird sie als primärer Executor (kein Fallback) ausgewählt.

Damit keine CPU-Ausführung erfolgt, verwenden Sie ANeuralNetworksCompilation_createForDevices und schließen dabei nnapi-reference aus der Geräteliste aus. Ab Android P ist es möglich, das Fallback zur Ausführungszeit von FEHLER-Builds zu deaktivieren, indem Sie das Attribut debug.nn.partition auf 2 setzen.

Speicherdomains

Ab Android 11 unterstützt die NNAPI Speicherdomains, die Zuordnungsschnittstellen für intransparente Erinnerungen bereitstellen. Dadurch können Anwendungen gerätenative Erinnerungen über verschiedene Ausführungen hinweg übergeben, sodass NNAPI keine Daten unnötigerweise kopiert oder umwandelt, wenn aufeinanderfolgende Ausführungen auf demselben Treiber erfolgen.

Die Speicherdomain-Funktion ist für Tensoren vorgesehen, die hauptsächlich treiberintern sind und keinen häufigen Zugriff auf die Clientseite benötigen. Beispiele für solche Tensoren sind die Zustandstensoren in Sequenzmodellen. Verwenden Sie für Tensoren, die auf Clientseite häufigen CPU-Zugriff benötigen, stattdessen gemeinsam genutzte Arbeitsspeicherpools.

Führen Sie die folgenden Schritte aus, um einen intransparenten Speicher zuzuweisen:

  1. Rufen Sie die Funktion ANeuralNetworksMemoryDesc_create() auf, um einen neuen Arbeitsspeicherdeskriptor zu erstellen:

    // Create a memory descriptor
    ANeuralNetworksMemoryDesc* desc;
    ANeuralNetworksMemoryDesc_create(&desc);
    
  2. Geben Sie alle vorgesehenen Eingabe- und Ausgaberollen an, indem Sie ANeuralNetworksMemoryDesc_addInputRole() und ANeuralNetworksMemoryDesc_addOutputRole() aufrufen.

    // Specify that the memory may be used as the first input and the first output
    // of the compilation
    ANeuralNetworksMemoryDesc_addInputRole(desc, compilation, 0, 1.0f);
    ANeuralNetworksMemoryDesc_addOutputRole(desc, compilation, 0, 1.0f);
    
  3. Optional können Sie die Arbeitsspeicherdimensionen angeben, indem Sie ANeuralNetworksMemoryDesc_setDimensions() aufrufen.

    // Specify the memory dimensions
    uint32_t dims[] = {3, 4};
    ANeuralNetworksMemoryDesc_setDimensions(desc, 2, dims);
    
  4. Beenden Sie die Deskriptordefinition durch Aufrufen von ANeuralNetworksMemoryDesc_finish().

    ANeuralNetworksMemoryDesc_finish(desc);
    
  5. Übergeben Sie den Deskriptor an ANeuralNetworksMemory_createFromDesc(), um so viele Erinnerungen zuzuweisen, wie Sie benötigen.

    // Allocate two opaque memories with the descriptor
    ANeuralNetworksMemory* opaqueMem;
    ANeuralNetworksMemory_createFromDesc(desc, &opaqueMem);
    
  6. Geben Sie den Arbeitsspeicherdeskriptor kostenlos, wenn Sie ihn nicht mehr benötigen.

    ANeuralNetworksMemoryDesc_free(desc);
    

Der Client darf gemäß den im ANeuralNetworksMemoryDesc-Objekt angegebenen Rollen nur das erstellte ANeuralNetworksMemory-Objekt mit ANeuralNetworksExecution_setInputFromMemory() oder ANeuralNetworksExecution_setOutputFromMemory() verwenden. Die Offset- und Längenargumente müssen auf 0 gesetzt werden, um anzugeben, dass der gesamte Arbeitsspeicher verwendet wird. Der Client kann den Inhalt des Speichers auch mit ANeuralNetworksMemory_copy() explizit festlegen oder extrahieren.

Sie können intransparente Erinnerungen mit Rollen mit nicht angegebenen Dimensionen oder Rangfolge erstellen. In diesem Fall schlägt die Arbeitsspeichererstellung möglicherweise mit dem Status ANEURALNETWORKS_OP_FAILED fehl, wenn sie vom zugrunde liegenden Treiber nicht unterstützt wird. Der Client wird empfohlen, die Fallback-Logik zu implementieren, indem ein ausreichend großer Puffer zugewiesen wird, der von Ashmem oder AHardwareBuffer im BLOB-Modus unterstützt wird.

Wenn die NNAPI nicht mehr auf das intransparente Speicherobjekt zugreifen muss, geben Sie die entsprechende ANeuralNetworksMemory-Instanz kostenlos:

ANeuralNetworksMemory_free(opaqueMem);

Messung der Leistung

Sie können die Leistung Ihrer Anwendung bewerten, indem Sie die Ausführungszeit messen oder ein Profil erstellen.

Ausführungszeit

Wenn Sie die Gesamtausführungszeit während der Laufzeit ermitteln möchten, können Sie die API zur synchronen Ausführung verwenden und die für den Aufruf benötigte Zeit messen. Wenn Sie die Gesamtausführungszeit über eine niedrigere Ebene des Softwarestacks bestimmen möchten, können Sie ANeuralNetworksExecution_setMeasureTiming und ANeuralNetworksExecution_getDuration verwenden, um Folgendes abzurufen:

  • Ausführungszeit auf einem Beschleuniger (nicht im Treiber, der auf dem Hostprozessor ausgeführt wird).
  • Ausführungszeit im Treiber, einschließlich der Zeit auf dem Beschleuniger.

Die Ausführungszeit im Treiber schließt Overhead wie die Laufzeit selbst und den IPC aus, der für die Kommunikation der Laufzeit mit dem Treiber erforderlich ist.

Diese APIs messen die Dauer zwischen der gesendeten Arbeit und der abgeschlossenen Arbeit und nicht die Zeit, die ein Fahrer oder Beschleuniger für die Ausführung der Inferenz benötigt, was möglicherweise durch Kontextwechsel unterbrochen wird.

Wenn beispielsweise Inferenz 1 beginnt und der Fahrer die Arbeit an der Ausführung von Inferenz 2 anhält, dann wird Inferenz 1 fortgesetzt und abgeschlossen. Die Ausführungszeit für Inferenz 1 beinhaltet die Zeit, zu der die Arbeit zur Ausführung von Inferenz 2 angehalten wurde.

Diese Zeitinformationen können bei einer Produktionsbereitstellung einer Anwendung nützlich sein, um Telemetrie für die Offlinenutzung zu erfassen. Sie können die Zeitdaten verwenden, um die App anzupassen, um eine bessere Leistung zu erzielen.

Beachten Sie bei der Verwendung dieser Funktion Folgendes:

  • Das Erfassen von Zeitinformationen kann Leistungskosten verursachen.
  • Nur ein Treiber ist in der Lage, die Zeit zu berechnen, die an sich selbst oder am Beschleuniger aufgewendet wird, mit Ausnahme der Zeit, die in der NNAPI-Laufzeit und in IPC aufgewendet wurde.
  • Sie können diese APIs nur mit einem ANeuralNetworksExecution verwenden, der mit ANeuralNetworksCompilation_createForDevices und numDevices = 1 erstellt wurde.
  • Es ist kein Fahrer erforderlich, um Zeitinformationen zu melden.

Profil für Ihre Anwendung mit Android Systrace erstellen

Ab Android 10 generiert NNAPI automatisch systrace-Ereignisse, mit denen Sie ein Profil für Ihre Anwendung erstellen können.

Die NNAPI-Quelle enthält ein parse_systrace-Dienstprogramm, um die von Ihrer Anwendung generierten Systrace-Ereignisse zu verarbeiten und eine Tabellenansicht zu generieren, die die in den verschiedenen Phasen des Modelllebenszyklus (Instanziierung, Vorbereitung, Kompilierungsausführung und Beendigung) aufgewendete Zeit und verschiedene Ebenen der Anwendungen zeigt. Ihre Anwendung wird in folgende Ebenen aufgeteilt:

  • Application: der Hauptanwendungscode
  • Runtime: NNAPI-Laufzeit
  • IPC: die prozessübergreifende Kommunikation zwischen der NNAPI-Laufzeit und dem Treibercode
  • Driver: der Beschleunigertreiberprozess.

Analysedaten zur Profilerstellung generieren

Wenn Sie sich die AOSP-Quellstruktur unter $ANDROID_build_TOP angesehen haben und das Beispiel für die TFLite-Bildklassifizierung als Zielanwendung verwenden, können Sie die NNAPI-Profildaten mit den folgenden Schritten generieren:

  1. Starten Sie den Android-Systrace mit dem folgenden Befehl:
$ANDROID_BUILD_TOP/external/chromium-trace/systrace.py  -o trace.html -a org.tensorflow.lite.examples.classification nnapi hal freq sched idle load binder_driver

Der Parameter -o trace.html gibt an, dass die Traces in trace.html geschrieben werden. Wenn Sie ein Profil für eine eigene Anwendung erstellen, müssen Sie org.tensorflow.lite.examples.classification durch den Prozessnamen ersetzen, der in Ihrem Anwendungsmanifest angegeben ist.

Dadurch wird eine Ihrer Shell-Konsolen ausgelastet. Führen Sie den Befehl nicht im Hintergrund aus, da er interaktiv auf das Beenden eines enter wartet.

  1. Nachdem der Systrace-Collector gestartet wurde, starten Sie Ihre Anwendung und führen Sie den Benchmark-Test aus.

In unserem Fall können Sie die App für die Bildklassifizierung über Android Studio oder direkt über die Benutzeroberfläche Ihres Testtelefons starten, wenn die App bereits installiert wurde. Zum Generieren von NNAPI-Daten müssen Sie die App für die Verwendung von NNAPI konfigurieren. Wählen Sie dazu im Dialogfeld für die App-Konfiguration NNAPI als Zielgerät aus.

  1. Beenden Sie nach Abschluss des Tests den Systrace durch Drücken von enter auf dem Konsolenterminal, das seit Schritt 1 aktiv ist.

  2. Führen Sie das Dienstprogramm systrace_parser aus, um kumulative Statistiken zu generieren:

$ANDROID_BUILD_TOP/frameworks/ml/nn/tools/systrace_parser/parse_systrace.py --total-times trace.html

Der Parser akzeptiert die folgenden Parameter: – --total-times: zeigt die Gesamtzeit an, die in einer Ebene verbracht wurde, einschließlich der Zeit, die auf die Ausführung eines Aufrufs einer zugrunde liegenden Ebene verbracht wurde – --print-detail: gibt alle Ereignisse aus, die von systrace erfasst wurden – --per-execution: gibt nur die Ausführung und ihre Unterphasen (pro Ausführungszeit) anstelle der Statistiken für alle Phasen aus – --json: erstellt die Ausgabe im JSON-Format

Hier ein Beispiel für die Ausgabe:

===========================================================================================================================================
NNAPI timing summary (total time, ms wall-clock)                                                      Execution
                                                           ----------------------------------------------------
              Initialization   Preparation   Compilation           I/O       Compute      Results     Ex. total   Termination        Total
              --------------   -----------   -----------   -----------  ------------  -----------   -----------   -----------   ----------
Application              n/a         19.06       1789.25           n/a           n/a         6.70         21.37           n/a      1831.17*
Runtime                    -         18.60       1787.48          2.93         11.37         0.12         14.42          1.32      1821.81
IPC                     1.77             -       1781.36          0.02          8.86            -          8.88             -      1792.01
Driver                  1.04             -       1779.21           n/a           n/a          n/a          7.70             -      1787.95

Total                   1.77*        19.06*      1789.25*         2.93*        11.74*        6.70*        21.37*         1.32*     1831.17*
===========================================================================================================================================
* This total ignores missing (n/a) values and thus is not necessarily consistent with the rest of the numbers

Der Parser kann fehlschlagen, wenn die erfassten Ereignisse keinen vollständigen Anwendungs-Trace darstellen. Er kann insbesondere fehlschlagen, wenn Systrace-Ereignisse, die zum Markieren des Endes eines Abschnitts generiert wurden, im Trace ohne zugehöriges Abschnittsstartereignis vorhanden sind. Dies ist in der Regel der Fall, wenn beim Starten des Systrace-Collectors einige Ereignisse aus einer vorherigen Profilerstellungssitzung generiert werden. In diesem Fall müssten Sie die Profilerstellung noch einmal ausführen.

Fügen Sie der systrace_parser-Ausgabe Statistiken für Ihren Anwendungscode hinzu

Die Anwendung parse_systrace basiert auf der integrierten Android-Systrace-Funktion. Mit der Systrace API (für Java, für native Anwendungen) mit benutzerdefinierten Ereignisnamen können Sie Traces für bestimmte Vorgänge in Ihrer Anwendung hinzufügen.

Wenn Sie Ihre benutzerdefinierten Ereignisse mit Phasen des Anwendungslebenszyklus verknüpfen möchten, stellen Sie dem Ereignisnamen einen der folgenden Strings voran:

  • [NN_LA_PI]: Ereignis auf Anwendungsebene für die Initialisierung
  • [NN_LA_PP]: Ereignis auf Anwendungsebene für die Vorbereitung
  • [NN_LA_PC]: Ereignis auf Anwendungsebene für die Kompilierung
  • [NN_LA_PE]: Ausführungsereignis auf Anwendungsebene

Hier ist ein Beispiel dafür, wie Sie den Beispielcode für die TFLite-Bildklassifizierung ändern können, indem Sie einen runInferenceModel-Abschnitt für die Execution-Phase und die Application-Ebene mit weiteren Abschnitten preprocessBitmap hinzufügen, die in NNAPI-Traces nicht berücksichtigt werden. Der Abschnitt runInferenceModel ist Teil der Systrace-Ereignisse, die vom nnapi-Systrace-Parser verarbeitet werden:

Kotlin

/** Runs inference and returns the classification results. */
fun recognizeImage(bitmap: Bitmap): List {
   // This section won’t appear in the NNAPI systrace analysis
   Trace.beginSection("preprocessBitmap")
   convertBitmapToByteBuffer(bitmap)
   Trace.endSection()

   // Run the inference call.
   // Add this method in to NNAPI systrace analysis.
   Trace.beginSection("[NN_LA_PE]runInferenceModel")
   long startTime = SystemClock.uptimeMillis()
   runInference()
   long endTime = SystemClock.uptimeMillis()
   Trace.endSection()
    ...
   return recognitions
}

Java

/** Runs inference and returns the classification results. */
public List recognizeImage(final Bitmap bitmap) {

 // This section won’t appear in the NNAPI systrace analysis
 Trace.beginSection("preprocessBitmap");
 convertBitmapToByteBuffer(bitmap);
 Trace.endSection();

 // Run the inference call.
 // Add this method in to NNAPI systrace analysis.
 Trace.beginSection("[NN_LA_PE]runInferenceModel");
 long startTime = SystemClock.uptimeMillis();
 runInference();
 long endTime = SystemClock.uptimeMillis();
 Trace.endSection();
  ...
 Trace.endSection();
 return recognitions;
}

Servicequalität

Unter Android 11 und höher ermöglicht NNAPI eine bessere Dienstqualität, da eine Anwendung die relativen Prioritäten ihrer Modelle, die maximale Zeit, die für die Vorbereitung eines bestimmten Modells erwartet wird, und die maximale Zeit, die für die Durchführung einer bestimmten Berechnung erwartet wird, angeben können. Android 11 bietet außerdem zusätzliche NNAPI-Ergebniscodes, mit denen Anwendungen Fehler wie verpasste Ausführungsfristen erkennen können.

Priorität einer Arbeitslast festlegen

Rufen Sie vor dem Aufrufen von ANeuralNetworksCompilation_finish() ANeuralNetworksCompilation_setPriority() auf, um die Priorität einer NNAPI-Arbeitslast festzulegen.

Fristen festlegen

Anwendungen können Fristen für die Modellkompilierung und Inferenz festlegen.

Weitere Informationen zu Operanden

Im folgenden Abschnitt werden erweiterte Themen zur Verwendung von Operanden behandelt.

Quantisierte Tensoren

Ein quantisierter Tensor ist eine kompakte Möglichkeit, ein n-dimensionales Array von Gleitkommawerten darzustellen.

NNAPI unterstützt asymmetrische 8-Bit-Tensoren. Bei diesen Tensoren wird der Wert jeder Zelle durch eine 8-Bit-Ganzzahl dargestellt. Dem Tensor werden eine Skala und ein Nullpunktwert zugeordnet. Damit werden die 8-Bit-Ganzzahlen in die dargestellten Gleitkommawerte umgewandelt.

Die Formel lautet:

(cellValue - zeroPoint) * scale

Dabei ist der nullPoint-Wert eine 32-Bit-Ganzzahl und die Skalierung ein 32-Bit-Gleitkommawert.

Im Vergleich zu Tensoren von 32-Bit-Gleitkommawerten haben 8-Bit-quantisierte Tensoren zwei Vorteile:

  • Ihre Anwendung ist kleiner, da die trainierten Gewichtungen ein Viertel der Größe von 32-Bit-Tensoren benötigen.
  • Berechnungen können oft schneller ausgeführt werden. Dies liegt an der geringeren Datenmenge, die aus dem Speicher abgerufen werden muss, und an der Effizienz von Prozessoren, wie z. B. DSPs bei der Integer-Berechnung.

Es ist zwar möglich, ein Gleitkommamodell in ein quantisiertes Modell zu konvertieren, unsere Erfahrung hat jedoch gezeigt, dass durch das direkte Training eines quantisierten Modells bessere Ergebnisse erzielt werden können. Das neuronale Netzwerk lernt, den erhöhten Detaillierungsgrad jedes Wertes auszugleichen. Für jeden quantisierten Tensor werden während des Trainingsvorgangs die Werte für Maßstab und Nullpunkt bestimmt.

In NNAPI definieren Sie quantisierte Tensortypen. Dazu setzen Sie das Typfeld der Datenstruktur ANeuralNetworksOperandType auf ANEURALNETWORKS_TENSOR_QUANT8_ASYMM. Außerdem geben Sie den Maßstab und den ZeroPoint-Wert des Tensors in dieser Datenstruktur an.

Zusätzlich zu den asymmetrischen 8-Bit-Tensoren unterstützt NNAPI Folgendes:

Optionale Operanden

Für einige Vorgänge wie ANEURALNETWORKS_LSH_PROJECTION werden optionale Operanden verwendet. Wenn Sie im Modell angeben möchten, dass der optionale Operand weggelassen wird, rufen Sie die Funktion ANeuralNetworksModel_setOperandValue() auf und übergeben Sie NULL für den Zwischenspeicher und 0 für die Länge.

Wenn die Entscheidung, ob der Operand vorhanden ist, bei jeder Ausführung unterschiedlich ist, geben Sie mithilfe der Funktionen ANeuralNetworksExecution_setInput() oder ANeuralNetworksExecution_setOutput() an, dass der Operand weggelassen wird. Dabei übergeben Sie NULL für den Zwischenspeicher und 0 für die Länge.

Tensoren mit unbekanntem Rang

Mit Android 9 (API-Level 28) wurden Modelloperanden mit unbekannten Dimensionen, aber mit einem bekannten Rang (der Anzahl der Dimensionen) eingeführt. Mit Android 10 (API-Level 29) wurden Tensoren mit unbekanntem Rang eingeführt, wie in ANeuralNetworksOperandType gezeigt.

NNAPI-Benchmark

Die NNAPI-Benchmark ist für AOSP in platform/test/mlts/benchmark (Benchmark-App) und platform/test/mlts/models (Modelle und Datasets) verfügbar.

Die Benchmark bewertet Latenz und Genauigkeit und vergleicht Treiber mit der gleichen Arbeit, die mit Tensorflow Lite auf der CPU für dieselben Modelle und Datasets ausgeführt wird.

So verwenden Sie die Benchmark:

  1. Verbinden Sie ein Android-Zielgerät mit Ihrem Computer, öffnen Sie ein Terminalfenster und achten Sie darauf, dass das Gerät über ADB erreichbar ist.

  2. Wenn mehrere Android-Geräte verbunden sind, exportieren Sie die Umgebungsvariable ANDROID_SERIAL des Zielgeräts.

  3. Gehen Sie zum obersten Android-Quellverzeichnis.

  4. Führen Sie die folgenden Befehle aus:

    lunch aosp_arm-userdebug # Or aosp_arm64-userdebug if available
    ./test/mlts/benchmark/build_and_run_benchmark.sh
    

    Am Ende einer Benchmarkausführung werden die Ergebnisse als HTML-Seite angezeigt, die an xdg-open übergeben wird.

NNAPI-Logs

NNAPI generiert in den Systemprotokollen nützliche Diagnoseinformationen. Analysieren Sie die Logs mit dem Dienstprogramm logcat.

Aktivieren Sie das ausführliche NNAPI-Logging für bestimmte Phasen oder Komponenten. Legen Sie dazu für das Attribut debug.nn.vlog (mit adb shell) die folgende Liste von Werten fest, die durch Leerzeichen, Doppelpunkte oder Kommas getrennt sind:

  • model: Modellerstellung
  • compilation: Erstellung des Modellausführungsplans und Kompilierung
  • execution: Modellausführung
  • cpuexe: Ausführung von Vorgängen unter Verwendung der NNAPI-CPU-Implementierung
  • manager: Informationen zu NNAPI-Erweiterungen, verfügbaren Schnittstellen und Funktionen
  • all oder 1: alle oben genannten Elemente

Verwenden Sie beispielsweise den Befehl adb shell setprop debug.nn.vlog all, um das vollständige Logging zu aktivieren. Verwenden Sie den Befehl adb shell setprop debug.nn.vlog '""', um das ausführliche Logging zu deaktivieren.

Nach der Aktivierung generiert das ausführliche Logging Logeinträge auf INFO-Ebene, wobei ein Tag auf den Namen der Phase oder Komponente festgelegt ist.

Neben den debug.nn.vlog-gesteuerten Meldungen stellen die NNAPI-Komponenten weitere Logeinträge auf verschiedenen Ebenen bereit, die jeweils ein bestimmtes Log-Tag verwenden.

Um eine Liste der Komponenten abzurufen, suchen Sie mit dem folgenden Ausdruck in der Quellstruktur:

grep -R 'define LOG_TAG' | awk -F '"' '{print $2}' | sort -u | egrep -v "Sample|FileTag|test"

Dieser Ausdruck gibt derzeit die folgenden Tags zurück:

  • BurstBuilder
  • Rückrufe
  • CompilationBuilder
  • CpuExecutor
  • ExecutionBuilder
  • ExecutionBurstController
  • ExecutionBurstServer
  • Ausführungsplan
  • Fibonacci-Treiber
  • GraphDump
  • IndexedShapeWrapper
  • IonWatcher
  • Administrator
  • Informationen merken
  • MemoryUtils (Speicherdienstprogramme)
  • Meta-Modell
  • ModellArgumentInfo
  • ModelBuilder
  • Neuronale Netzwerke
  • OperationResolver
  • Aufgaben und Ablauf
  • OperationsUtils
  • Paketinformationen
  • TokenHasher
  • Typmanager
  • Dienstprogramme
  • ValidateHal
  • Versionierte Schnittstellen

Verwenden Sie die Umgebungsvariable ANDROID_LOG_TAGS, um die Ebene der von logcat angezeigten Logmeldungen zu steuern.

Wenn Sie alle NNAPI-Lognachrichten aufrufen und alle anderen deaktivieren möchten, legen Sie für ANDROID_LOG_TAGS Folgendes fest:

BurstBuilder:V Callbacks:V CompilationBuilder:V CpuExecutor:V ExecutionBuilder:V ExecutionBurstController:V ExecutionBurstServer:V ExecutionPlan:V FibonacciDriver:V GraphDump:V IndexedShapeWrapper:V IonWatcher:V Manager:V MemoryUtils:V Memory:V MetaModel:V ModelArgumentInfo:V ModelBuilder:V NeuralNetworks:V OperationResolver:V OperationsUtils:V Operations:V PackageInfo:V TokenHasher:V TypeManager:V Utils:V ValidateHal:V VersionedInterfaces:V *:S.

Sie können ANDROID_LOG_TAGS mit dem folgenden Befehl festlegen:

export ANDROID_LOG_TAGS=$(grep -R 'define LOG_TAG' | awk -F '"' '{ print $2 ":V" }' | sort -u | egrep -v "Sample|FileTag|test" | xargs echo -n; echo ' *:S')

Dies ist nur ein Filter, der für logcat gilt. Sie müssen trotzdem die Property debug.nn.vlog auf all setzen, um ausführliche Loginformationen zu generieren.