Neural Networks API

Die Android Neural Networks API (NNAPI) ist eine Android-C API, die für die Ausführung von rechenintensiven Vorgängen für maschinelles Lernen auf Android-Geräten entwickelt wurde. NNAPI wurde entwickelt, um eine Basisschicht für Frameworks für maschinelles Lernen höherer Ebene wie TensorFlow Lite und Caffe2 bereitzustellen, mit denen neuronale Netze erstellt und trainiert werden. Die API ist auf allen Android-Geräten mit Android 8.1 (API-Level 27) oder höher verfügbar.

NNAPI unterstützt Inferenzen, indem Daten von Android-Geräten auf zuvor trainierte, vom Entwickler definierte Modelle angewendet werden. Beispiele für Inferenzen sind die Klassifizierung von Bildern, die Vorhersage von Nutzerverhalten und die Auswahl geeigneter Antworten auf eine Suchanfrage.

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 für Videoanwendungen entscheidend sein, die aufeinanderfolgende Frames von einer Kamera verarbeiten.
  • Verfügbarkeit: Die Anwendung wird auch dann ausgeführt, wenn Sie sich außerhalb des Netzwerks befinden.
  • Geschwindigkeit: Neue Hardware, die speziell für die Verarbeitung von neuronalen Netzwerken entwickelt wurde, ermöglicht eine deutlich schnellere Berechnung als eine allgemeine CPU allein.
  • Datenschutz: Die Daten verlassen das Android-Gerät nicht.
  • Kosten: Wenn alle Berechnungen auf dem Android-Gerät ausgeführt werden, ist keine Serverfarm erforderlich.

Außerdem gibt es einige Kompromisse, die Entwickler im Hinterkopf behalten sollten:

  • Systemauslastung: Die Auswertung von neuronalen Netzwerken erfordert viele Berechnungen, was den Akkuverbrauch erhöhen kann. Sie sollten den Akkuzustand im Blick behalten, wenn dies für Ihre App ein Problem darstellt, insbesondere bei langwierigen Berechnungen.
  • Größe der Anwendung: Achten Sie auf die Größe Ihrer Modelle. Modelle können mehrere Megabyte Speicherplatz belegen. Wenn das Binden großer Modelle in Ihrem APK Ihre Nutzer unverhältnismäßig beeinträchtigen würde, können Sie die Modelle nach der App-Installation herunterladen, kleinere Modelle verwenden oder Ihre Berechnungen in der Cloud ausfü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 soll von Machine-Learning-Bibliotheken, ‑Frameworks und ‑Tools aufgerufen werden, mit denen Entwickler ihre Modelle außerhalb des Geräts trainieren und auf Android-Geräten bereitstellen können. Apps verwenden NNAPI in der Regel nicht direkt, sondern Frameworks für maschinelles Lernen höherer Ebene. 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 Neural Network Runtime von Android die Rechenlast effizient auf die verfügbaren On-Device-Prozessoren verteilen, einschließlich spezieller Hardware für neuronale Netze, Grafikprozessoren (GPUs) und digitaler Signalprozessoren (DSPs).

Auf Android-Geräten ohne speziellen Anbietertreiber 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

Programmiermodell der Neural Networks API

Wenn Sie Berechnungen mit NNAPI ausführen möchten, müssen Sie zuerst einen gerichteten Graphen erstellen, der die durchzuführenden Berechnungen definiert. Dieser Berechnungsgraph in Kombination mit Ihren Eingabedaten (z. B. den Gewichten und Vorurteilen, die von einem Framework für maschinelles Lernen übergeben werden) bildet das Modell für die NNAPI-Laufzeitbewertung.

NNAPI verwendet vier Hauptabstraktionen:

  • Modell: Ein Berechnungsgraph mit mathematischen Vorgängen und den Konstanten, die durch einen Trainingsvorgang gelernt wurden. Diese Vorgänge sind spezifisch für neuronale Netze. Dazu gehören unter anderem die 2-dimensionale (2D) Konvolution, die logistischer (sigmoide) Aktivierung und die gerade lineare (ReLU) Aktivierung. Das Erstellen eines Modells ist ein synchroner Vorgang. Nach der Erstellung kann es für Threads und Kompilationen wiederverwendet werden. In NNAPI wird ein Modell als ANeuralNetworksModel-Instanz dargestellt.
  • Kompilierung: Stellt eine Konfiguration zum Kompilieren eines NNAPI-Modells in Code niedrigerer Ebene dar. Das Erstellen einer Kompilierung ist ein synchroner Vorgang. Nach dem Erstellen kann es für Threads und Ausführungen wiederverwendet werden. In NNAPI wird jede Kompilierung als ANeuralNetworksCompilation-Instanz dargestellt.
  • Speicher: Stellt gemeinsamen Arbeitsspeicher, arbeitsspeicherzugeordnete Dateien und ähnliche Speicher-Buffer dar. Mit einem Speicherpuffer kann die NNAPI-Laufzeit Daten effizienter an Treiber übertragen. Eine App erstellt in der Regel einen gemeinsamen Speicherbuffer, der alle Tensoren enthält, die zum Definieren eines Modells erforderlich sind. Sie können auch Speicher-Buffer verwenden, um die Eingaben und Ausgaben für eine Ausführungsinstanz zu speichern. In NNAPI wird jeder Speicherpuffer 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 der 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 Programmierablauf.

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

Im Rest dieses Abschnitts wird beschrieben, wie Sie Ihr NNAPI-Modell einrichten, um Berechnungen durchzuführen, das Modell zu kompilieren und das kompilierte Modell auszuführen.

Zugriff auf Trainingsdaten gewähren

Die trainierten Gewichte und Voreingenommenheiten werden wahrscheinlich in einer Datei gespeichert. Um der NNAPI-Laufzeit einen effizienten Zugriff auf diese Daten zu ermöglichen, erstellen Sie eine ANeuralNetworksMemory-Instanz, indem Sie die Funktion ANeuralNetworksMemory_createFromFd() aufrufen und den Dateideskriptor der geöffneten Datendatei übergeben. Außerdem geben Sie Flags für den Speicherschutz und einen Offset an, an dem der freigegebene Speicherbereich 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);

In diesem Beispiel verwenden wir zwar nur eine ANeuralNetworksMemory-Instanz für alle Gewichte, aber es ist möglich, mehrere ANeuralNetworksMemory-Instanzen für mehrere Dateien zu verwenden.

Native Hardware-Buffer verwenden

Sie können native Hardware-Buffer für Modellinputs, ‑outputs 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. Aufgrund dieser Einschränkung sollten Sie sich an die in der ANeuralNetworksMemory_createFromAHardwareBuffer-Referenzdokumentation aufgeführten Einschränkungen halten und die Kompilierung und Ausführung mit AHardwareBuffer vorab auf Zielgeräten testen, um sicherzustellen, dass sie wie erwartet funktionieren. Verwenden Sie dazu die Gerätezuweisung, um den Accelerator anzugeben.

Damit die NNAPI-Laufzeit auf ein AHardwareBuffer-Objekt zugreifen kann, erstellen Sie eine ANeuralNetworksMemory-Instanz, indem Sie die Funktion ANeuralNetworksMemory_createFromAHardwareBuffer aufrufen und das AHardwareBuffer-Objekt übergeben, 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 NNAPI nicht mehr auf das AHardwareBuffer-Objekt zugreifen muss, befreien Sie die entsprechende ANeuralNetworksMemory-Instanz:

ANeuralNetworksMemory_free(mem2);

Hinweis:

  • Sie können AHardwareBuffer nur für den gesamten Puffer verwenden. Eine Verwendung mit einem ARect-Parameter ist nicht zulässig.
  • Die NNAPI-Laufzeit löscht den Puffer nicht. Sie müssen dafür sorgen, dass auf die Eingabe- und Ausgabe-Buffer zugegriffen werden kann, bevor Sie die Ausführung planen.
  • Dateien mit Synchronisierungsgrenzwerten werden nicht unterstützt.
  • Bei einem AHardwareBuffer mit anbieterspezifischen Formaten und Nutzungsbits liegt es in der Verantwortung der Anbieterimplementierung, zu bestimmen, ob der Client oder der Treiber für das Leeren des Caches verantwortlich ist.

Modell

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

Operanden

Operanden sind Datenobjekte, die zum Definieren des Graphen verwendet werden. Dazu gehören die Eingaben und Ausgaben des Modells, die Zwischenknoten mit den Daten, die von einem Vorgang zum nächsten fließen, und die Konstanten, die an diese Vorgänge übergeben werden.

NNAPI-Modellen können zwei Arten von Operanden hinzugefügt werden: Skalarwerte und Tensoren.

Ein Skalar steht für einen einzelnen Wert. NNAPI unterstützt Skalarwerte in den Formaten Boolescher Wert, 16‑Bit-Gleitkomma, 32‑Bit-Gleitkomma, 32‑Bit-Ganzzahl und vorzeichenlose 32‑Bit-Ganzzahl.

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

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

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

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

Die Reihenfolge, in der Sie die Operanden hinzufügen, spielt keine Rolle. Der Modellausgabeoperand könnte beispielsweise der erste hinzugefügte Operand sein. Wichtig ist, dass Sie beim Verweis auf einen Operanden den richtigen Indexwert verwenden.

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 genau eines Vorgangs sein.

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

Aufgaben und Ablauf

Ein Vorgang gibt die durchzuführenden Berechnungen an. Jeder Vorgang besteht aus den folgenden Elementen:

  • einen Vorgangstyp (z. B. Addition, Multiplikation, Convolution),
  • eine Liste der Indizes der Operanden, die der Vorgang als Eingabe verwendet, und
  • eine Liste der Indizes der Operanden, die für die Ausgabe des Vorgangs verwendet werden.

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

Sie müssen dem Modell die Operanden hinzufügen, die ein Vorgang benötigt oder erzeugt, bevor Sie den Vorgang hinzufügen.

Die Reihenfolge, in der Sie Vorgänge hinzufügen, spielt keine Rolle. NNAPI nutzt die Abhängigkeiten, die durch den Verarbeitungsgraphen von Operanden und Vorgängen festgelegt werden, um die Reihenfolge der Ausführung von Vorgängen zu bestimmen.

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

Kategorie Aufgaben und Ablauf
Elementweise mathematische Vorgänge
Tensor manipulation
Bildvorgänge
Suchvorgänge
Normalisierungsvorgänge
Faltungsvorgänge
Pooling-Vorgänge
Aktivierungsvorgänge
Sonstige Vorgänge

Bekanntes Problem bei API-Level 28:Wenn ANEURALNETWORKS_TENSOR_QUANT8_ASYMM-Tensoren an den ANEURALNETWORKS_PAD-Vorgang übergeben werden, der unter Android 9 (API-Level 28) und höher verfügbar ist, stimmt die Ausgabe der NNAPI möglicherweise nicht mit der Ausgabe von Frameworks für maschinelles Lernen auf höherer Ebene überein, z. B. TensorFlow Lite. Du solltest stattdessen 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 Modell mit zwei Vorgängen 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 Ihrem Modell die Operanden hinzu, indem Sie ANeuralNetworks_addOperand() aufrufen. Ihre Datentypen werden mit der Datenstruktur ANeuralNetworksOperandType 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, z. B. Gewichte und Voreingenommenheiten, die Ihre App aus einem Trainingsvorgang erhält, die Funktionen ANeuralNetworksModel_setOperandValue() und ANeuralNetworksModel_setOperandValueFromMemory().

    Im folgenden Beispiel legen wir konstante Werte aus der Trainingsdatendatei fest, die dem Speicherpuffer entsprechen, den wir unter Zugriff auf Trainingsdaten gewähren 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 Ihrem Modell für jeden Vorgang im gerichteten Graphen, den Sie berechnen möchten, den Vorgang hinzu, indem Sie die Funktion ANeuralNetworksModel_addOperation() aufrufen.

    Ihre App muss für diesen Aufruf folgende Parameter bereitstellen:

    • den Vorgangstyp
    • Anzahl der Eingabewerte
    • das Array der Indizes für Eingabeoperanden
    • Anzahl der Ausgabewerte
    • das Array der Indizes für Ausgabeoperanden

    Ein Operand kann nicht sowohl für die Eingabe als auch für die Ausgabe derselben Operation 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. Geben Sie an, welche Operanden das Modell als Eingaben und Ausgaben behandeln soll, indem Sie die Funktion ANeuralNetworksModel_identifyInputsAndOutputs() aufrufen.

    // 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. Optional können Sie angeben, ob ANEURALNETWORKS_TENSOR_FLOAT32 mit einem Bereich oder einer Genauigkeit berechnet werden darf, die so niedrig ist wie beim IEEE 754-16-Bit-Gleitkommaformat. Rufen Sie dazu ANeuralNetworksModel_relaxComputationFloat32toFloat16() auf.

  7. Rufen Sie ANeuralNetworksModel_finish() auf, um die Definition Ihres Modells abzuschließen. Wenn keine Fehler auftreten, 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.

Kontrollfluss

So fügen Sie einem NNAPI-Modell eine Ablaufsteuerung hinzu:

  1. Erstellen Sie die entsprechenden Ausführungsuntergraphen (then- und else-Untergraphen für eine IF-Anweisung, condition- und body-Untergraphen für eine WHILE-Schleife) als eigenständige ANeuralNetworksModel*-Modelle:

    ANeuralNetworksModel* thenModel = makeThenModel();
    ANeuralNetworksModel* elseModel = makeElseModel();
    
  2. Erstellen Sie Operanden, die auf diese Modelle im Modell mit der Ablaufsteuerung 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 Kontrollflussvorgang 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

Beim Kompilierungsschritt wird festgelegt, auf welchen Prozessoren Ihr Modell ausgeführt wird, und die entsprechenden Treiber werden auf die Ausführung vorbereitet. Dazu kann die Generierung von Maschinencode für die Prozessoren gehören, 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 mit der Gerätezuweisung explizit auswählen, auf welchen Geräten die Ausführung erfolgen soll.

  2. Sie können optional festlegen, wie die Laufzeit zwischen Akkunutzung und Ausführungsgeschwindigkeit abgewogen wird. Rufen Sie dazu ANeuralNetworksCompilation_setPreference() auf.

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

    Sie können folgende Einstellungen festlegen:

    • ANEURALNETWORKS_PREFER_LOW_POWER: Die Ausführung sollte so erfolgen, dass der Akkuverbrauch minimiert wird. Das ist bei häufig ausgeführten Kompilierungen wünschenswert.
    • ANEURALNETWORKS_PREFER_FAST_SINGLE_ANSWER: Es wird bevorzugt eine einzelne Antwort so schnell wie möglich zurückgegeben, auch wenn dies zu einem höheren Stromverbrauch führt. Das ist die Standardeinstellung.
    • ANEURALNETWORKS_PREFER_SUSTAINED_SPEED: Der Durchsatz aufeinanderfolgender Frames wird maximiert, z. B. bei der Verarbeitung aufeinanderfolgender Frames, die von der Kamera stammen.
  3. Optional können Sie den Kompilierungs-Cache einrichten, indem Sie ANeuralNetworksCompilation_setCaching aufrufen.

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

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

  4. Schließen Sie die Definition der Kompilierung ab, indem Sie ANeuralNetworksCompilation_finish() aufrufen. Wenn keine Fehler auftreten, 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 Bibliotheken und Apps für Frameworks für maschinelles Lernen Informationen zu den verfügbaren Geräten abrufen und Geräte für die Ausführung angeben können. Wenn Sie Informationen zu den verfügbaren Geräten angeben, können Apps die genaue Version der auf einem Gerät gefundenen Treiber abrufen, um bekannte Inkompatibilitäten zu vermeiden. Da Apps angeben können, auf welchen Geräten verschiedene Bereiche eines Modells ausgeführt werden sollen, können sie für das Android-Gerät optimiert werden, auf dem sie bereitgestellt werden.

Geräteerkennung

Mit ANeuralNetworks_getDeviceCount können Sie die Anzahl der verfügbaren Geräte abrufen. Verwenden Sie für jedes Gerät ANeuralNetworks_getDevice, um eine ANeuralNetworksDevice-Instanz auf eine Referenz auf dieses Gerät festzulegen.

Sobald du eine Gerätereferenz hast, kannst du mit den folgenden Funktionen weitere Informationen zu diesem Gerät abrufen:

Gerätezuweisung

Mit ANeuralNetworksModel_getSupportedOperationsForDevices können Sie herausfinden, 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 sollen, rufen Sie anstelle von ANeuralNetworksCompilation_create ANeuralNetworksCompilation_createForDevices auf. Verwende das resultierende ANeuralNetworksCompilation-Objekt wie gewohnt. Die Funktion gibt einen Fehler zurück, wenn das bereitgestellte 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 dafür verantwortlich, die Arbeit auf die Geräte zu verteilen.

Ä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 Aufruf von ANeuralNetworksCompilation_createForDevices wird die CPU-Implementierung nicht verwendet, um Fehler bei der Modellkompilierung und -ausführung zu behandeln.

Es liegt in der Verantwortung der Anwendung, ein Modell in Untermodelle zu partitionieren, die auf den angegebenen Geräten ausgeführt werden können. Bei Anwendungen, für die keine manuelle Partitionierung erforderlich ist, sollte weiterhin die einfachere Funktion ANeuralNetworksCompilation_create aufgerufen werden, um alle verfügbaren Geräte (einschließlich der CPU) zur Beschleunigung des Modells zu verwenden. Wenn das Modell von den mit ANeuralNetworksCompilation_createForDevices angegebenen Geräten 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 mehr als ein Gerät für ANeuralNetworksCompilation_createForDevices angegeben 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 der Verwendung von ANeuralNetworksCompilation_create werden alle verfügbaren Geräte berücksichtigt, einschließlich der CPU.

Bei der Verteilung wird für jeden Vorgang im Modell aus der Liste der verfügbaren Geräte das Gerät ausgewählt, das den Vorgang unterstützt und die beste Leistung bietet, d.h. die kürzeste Ausführungszeit oder die niedrigste Stromaufnahme, je nach den vom Kunden angegebenen Ausführungseinstellungen. Dieser Partitionierungsalgorithmus berücksichtigt keine möglichen Ineffizienzen, die durch die E/A zwischen den verschiedenen Prozessoren verursacht werden. Wenn Sie also mehrere Prozessoren angeben (entweder explizit mit ANeuralNetworksCompilation_createForDevices oder implizit mit ANeuralNetworksCompilation_create), ist es wichtig, die resultierende Anwendung zu profilieren.

Wenn Sie wissen möchten, wie Ihr Modell von NNAPI partitioniert wurde, suchen Sie in den Android-Protokollen 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 ist der Index des Kandidatengeräts in der Geräteliste. Diese Liste ist die Eingabe für ANeuralNetworksCompilation_createForDevices oder, wenn ANeuralNetworksCompilation_createForDevices verwendet wird, die Liste der Geräte, die zurückgegeben wird, wenn alle Geräte mit ANeuralNetworks_getDeviceCount und ANeuralNetworks_getDevice durchlaufen werden.

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

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

Diese Meldung gibt an, dass die gesamte Grafik auf dem Gerät beschleunigt wurdedevice-name.

Umsetzung

Im Ausführungsschritt wird das Modell auf eine Reihe von Eingaben angewendet und die Berechnungsergebnisse werden in einem oder mehreren Nutzerpuffern oder Speicherbereichen gespeichert, die von Ihrer App zugewiesen wurden.

So führen Sie ein kompiliertes Modell aus:

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

    // Run the compiled model against a set of inputs
    ANeuralNetworksExecution* run1 = NULL;
    ANeuralNetworksExecution_create(compilation, &run1);
    
  2. Geben Sie an, wo Ihre App die Eingabewerte für die Berechnung liest. Ihre App kann Eingabewerte entweder aus einem Nutzer- oder einem zugewiesenen Arbeitsspeicherbereich lesen, indem sie ANeuralNetworksExecution_setInput() oder 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, wo Ihre App die Ausgabewerte schreibt. Ihre App kann Ausgabewerte entweder in einen Nutzerpuffer oder in einen zugewiesenen Arbeitsspeicherbereich schreiben, indem sie ANeuralNetworksExecution_setOutput() oder ANeuralNetworksExecution_setOutputFromMemory() aufruft.

    // Set the output
    float32 myOutput[3][4];
    ANeuralNetworksExecution_setOutput(run1, 0, NULL, myOutput, sizeof(myOutput));
    
  4. Rufen Sie die Funktion ANeuralNetworksExecution_startCompute() auf, um die Ausführung zu planen. Wenn keine Fehler auftreten, 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 zu warten, bis die Ausführung abgeschlossen ist. Wenn die Ausführung erfolgreich war, gibt diese Funktion den Ergebniscode ANEURALNETWORKS_NO_ERROR zurück. Das Warten kann in einem anderen Thread erfolgen als dem, in dem die Ausführung gestartet wird.

    // 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 dem kompilierten Modell eine andere Eingabe anwenden, indem Sie mit derselben Kompilierungsinstanz eine neue ANeuralNetworksExecution-Instanz 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 wird Zeit für das Starten und Synchronisieren von Threads benötigt. Außerdem kann die Latenz sehr variieren. Die längsten Verzögerungen zwischen der Benachrichtigung oder dem Aufwecken eines Threads und der Bindung an einen CPU-Kern betragen bis zu 500 Mikrosekunden.

Um die Latenz zu verbessern, können Sie stattdessen eine Anwendung anweisen, einen synchronen Inferenzaufruf an die Laufzeit zu senden. Dieser Aufruf gibt nur dann ein Ergebnis zurück, wenn eine Inferenz abgeschlossen ist, und nicht, wenn sie gestartet wurde. Anstatt ANeuralNetworksExecution_startCompute für einen asynchronen Inferenzaufruf an die Laufzeit aufzurufen, ruft die Anwendung ANeuralNetworksExecution_compute für einen synchronen Aufruf an die Laufzeit auf. Ein Aufruf von ANeuralNetworksExecution_compute nimmt keinen ANeuralNetworksEvent entgegen 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, die in schneller Folge erfolgen, z. B. bei der Verarbeitung von Frames einer Kameraaufnahme oder aufeinanderfolgenden Audio-Samples. Die Verwendung von ANeuralNetworksBurst-Objekten kann zu schnelleren Ausführungen führen, da sie Beschleunigern signalisieren, dass Ressourcen zwischen Ausführungen wiederverwendet werden können und dass Beschleuniger während der Burst-Phase in einem Hochleistungsstatus bleiben sollten.

ANeuralNetworksBurst führt nur eine kleine Änderung am normalen Ausführungspfad ein. Sie erstellen ein Burst-Objekt mit ANeuralNetworksBurst_create, 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 für jede Inferenz zu verwenden, ordnen Sie die verschiedenen ANeuralNetworksExecution-Objekte in Aufrufen der Funktion ANeuralNetworksExecution_burstCompute demselben ANeuralNetworksBurst zu.

// 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
// ...

Wenn das ANeuralNetworksBurst-Objekt nicht mehr benötigt wird, kannst du es mit ANeuralNetworksBurst_free freigeben.

// Cleanup
ANeuralNetworksBurst_free(burst);

Asynchrone Befehlswarteschlangen und abgegrenzte Ausführung

Unter Android 11 und höher unterstützt NNAPI eine zusätzliche Möglichkeit zur Planung der asynchronen Ausführung über die Methode ANeuralNetworksExecution_startComputeWithDependencies(). Bei dieser Methode wartet die Ausführung, bis alle abhängigen Ereignisse signalisiert wurden, bevor die Auswertung gestartet wird. Sobald die Ausführung abgeschlossen ist und die Ausgabedaten bereit für die Verwendung sind, wird das zurückgegebene Ereignis signalisiert.

Je nachdem, welche Geräte die Ausführung übernehmen, wird das Ereignis möglicherweise von einem Synchronisationsgrenzwert unterstü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 Synchronisierungsgrenzwerte mit ANeuralNetworksEvent_createFromSyncFenceFd() in ein Ereignisobjekt importieren und mit ANeuralNetworksEvent_getSyncFenceFd() aus einem Ereignisobjekt exportieren.

Ausgabe mit dynamischer Größe

Wenn Sie Modelle unterstützen möchten, bei denen die Größe der Ausgabe von den Eingabedaten abhängt, also nicht zum Zeitpunkt der Modellausführung bestimmt werden kann, verwenden Sie ANeuralNetworksExecution_getOutputOperandRank und ANeuralNetworksExecution_getOutputOperandDimensions.

Das folgende Codebeispiel zeigt, wie das geht:

// 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

Im Bereinigungsschritt werden die internen Ressourcen freigegeben, die für die Berechnung verwendet wurden.

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

Fehlermanagement und CPU-Fallback

Wenn bei der Partitionierung ein Fehler auftritt, ein Treiber ein Modell (oder einen Teil eines Modells) nicht kompilieren kann oder ein Treiber ein kompiliertes Modell (oder einen Teil eines Modells) nicht ausführen kann, greift NNAPI möglicherweise auf die eigene CPU-Implementierung der einen oder mehrerer Vorgänge zurück.

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

Wenn die Kompilierung unter Android 10 mit ANeuralNetworksCompilation_createForDevices ausgeführt wird, wird der CPU-Fallback deaktiviert.

In Android P wird bei einem Fehler bei der Ausführung auf dem Treiber auf die CPU zurückgegriffen. Das gilt auch für Android 10, wenn ANeuralNetworksCompilation_create anstelle von ANeuralNetworksCompilation_createForDevices verwendet wird.

Bei der ersten Ausführung wird auf diese einzelne Partition zurückgegriffen. Wenn das immer noch fehlschlägt, wird das gesamte Modell auf der CPU noch einmal ausgeführt.

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

Es gibt Fälle, in denen einige Vorgänge von der CPU nicht unterstützt werden. In solchen Situationen schlägt die Kompilierung oder Ausführung fehl, anstatt auf die GPU umzuschalten.

Auch wenn der CPU-Fallback deaktiviert ist, gibt es möglicherweise weiterhin Vorgänge im Modell, die auf der CPU geplant werden. Wenn die CPU in der Liste der für ANeuralNetworksCompilation_createForDevices bereitgestellten Prozessoren enthalten ist und entweder der einzige Prozessor ist, der diese Vorgänge unterstützt, oder der Prozessor, der für diese Vorgänge die beste Leistung verspricht, wird sie als primärer (nicht als Fallback-)Ausführer ausgewählt.

Wenn keine CPU-Ausführung erfolgen soll, verwenden Sie ANeuralNetworksCompilation_createForDevices und schließen Sie nnapi-reference aus der Liste der Geräte aus. Ab Android P können Sie den Fallback bei der Ausführung von DEBUG-Builds deaktivieren, indem Sie die Eigenschaft debug.nn.partition auf 2 setzen.

Speicherbereiche

Unter Android 11 und höher unterstützt NNAPI Speicherbereiche, die Allocator-Schnittstellen für nicht transparente Speicher bereitstellen. So können Anwendungen geräteeigene Speicher zwischen Ausführungen übergeben, sodass NNAPI bei aufeinanderfolgenden Ausführungen desselben Treibers keine Daten unnötig kopiert oder umwandelt.

Die Speicherdomainfunktion ist für Tensoren gedacht, die größtenteils im Treiber intern sind und keinen häufigen Zugriff auf die Clientseite erfordern. Beispiele für solche Tensoren sind die Zustandstensoren in Sequenzmodellen. Verwenden Sie für Tensoren, die clientseitig häufig auf die CPU zugreifen müssen, stattdessen gemeinsam genutzte Arbeitsspeicherpools.

So weisen Sie undurchsichtiges Arbeitsspeicher zu:

  1. Rufen Sie die Funktion ANeuralNetworksMemoryDesc_create() auf, um einen neuen Speicherbeschreiber 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. Sie können die Arbeitsspeicherdimensionen optional durch Aufrufen von ANeuralNetworksMemoryDesc_setDimensions() angeben.

    // Specify the memory dimensions
    uint32_t dims[] = {3, 4};
    ANeuralNetworksMemoryDesc_setDimensions(desc, 2, dims);
    
  4. Schließe die Deskriptordefinition ab, indem du ANeuralNetworksMemoryDesc_finish() aufrufst.

    ANeuralNetworksMemoryDesc_finish(desc);
    
  5. Weisen Sie so viel Arbeitsspeicher zu, wie Sie benötigen, indem Sie den Descriptor an ANeuralNetworksMemory_createFromDesc() übergeben.

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

    ANeuralNetworksMemoryDesc_free(desc);
    

Der Kunde darf das erstellte ANeuralNetworksMemory-Objekt nur mit ANeuralNetworksExecution_setInputFromMemory() oder ANeuralNetworksExecution_setOutputFromMemory() verwenden, je nach den im ANeuralNetworksMemoryDesc-Objekt angegebenen Rollen. Die Argumente „offset“ und „length“ müssen auf „0“ gesetzt werden, was bedeutet, dass der gesamte Arbeitsspeicher verwendet wird. Der Client kann den Inhalt des Arbeitsspeichers auch explizit mit ANeuralNetworksMemory_copy() festlegen oder extrahieren.

Sie können undurchsichtige Erinnerungen mit Rollen mit nicht angegebenen Dimensionen oder Rang erstellen. In diesem Fall schlägt die Erstellung des Arbeitsspeichers möglicherweise mit dem Status ANEURALNETWORKS_OP_FAILED fehl, wenn er vom zugrunde liegenden Treiber nicht unterstützt wird. Der Client sollte eine Fallback-Logik implementieren, indem er einen ausreichend großen Puffer zuweist, der von Ashmem oder dem BLOB-Modus AHardwareBuffer unterstützt wird.

Wenn NNAPI nicht mehr auf das opake Speicherobjekt zugreifen muss, müssen Sie die entsprechende ANeuralNetworksMemory-Instanz freigeben:

ANeuralNetworksMemory_free(opaqueMem);

Leistungsmessung

Sie können die Leistung Ihrer App bewerten, indem Sie die Ausführungszeit messen oder ein Profiling durchführen.

Ausführungszeit

Wenn Sie die Gesamtausführungszeit über die Laufzeit ermitteln möchten, können Sie die API für die synchrone Ausführung verwenden und die Zeit messen, die für den Aufruf benötigt wird. Wenn Sie die Gesamtausführungszeit über eine niedrigere Ebene des Softwarestacks ermitteln möchten, können Sie ANeuralNetworksExecution_setMeasureTiming und ANeuralNetworksExecution_getDuration verwenden, um Folgendes zu erhalten:

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

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

Diese APIs messen die Dauer zwischen den Ereignissen „Abgegebene Arbeit“ und „Abgeschlossene Arbeit“, nicht die Zeit, die ein Treiber oder Beschleuniger für die Durchführung der Inferenz aufwendet, die möglicherweise durch einen Kontextwechsel unterbrochen wird.

Wenn beispielsweise die Inferenz 1 beginnt, der Treiber die Arbeit stoppt, um die Inferenz 2 auszuführen, und dann die Inferenz 1 fortsetzt und abschließt, wird die Ausführungszeit für die Inferenz 1 die Zeit enthalten, in der die Arbeit für die Ausführung der Inferenz 2 angehalten wurde.

Diese Zeitangaben können für die Produktionsbereitstellung einer Anwendung nützlich sein, um Telemetriedaten für die Offlinenutzung zu erfassen. Anhand der Zeitdaten können Sie die App für eine bessere Leistung anpassen.

Beachten Sie bei der Verwendung dieser Funktion Folgendes:

  • Das Erheben von Zeitinformationen kann sich auf die Leistung auswirken.
  • Nur ein Treiber kann die Zeit berechnen, die im Treiber selbst oder im Accelerator verbracht wurde, ausgenommen die Zeit, die in der NNAPI-Laufzeit und in der IPC verbracht wurde.
  • Sie können diese APIs nur mit einer ANeuralNetworksExecution verwenden, die mit ANeuralNetworksCompilation_createForDevices und numDevices = 1 erstellt wurde.
  • Es ist kein Fahrer erforderlich, um Zeitinformationen melden zu können.

Anwendung mit Android Systrace profilieren

Ab Android 10 generiert NNAPI automatisch systrace-Ereignisse, mit denen Sie Ihre Anwendung profilieren können.

Die NNAPI-Quelle enthält ein parse_systrace-Dienstprogramm, mit dem die von Ihrer Anwendung generierten Systrace-Ereignisse verarbeitet und eine Tabellenansicht erstellt werden kann, die die Zeit in den verschiedenen Phasen des Modelllebenszyklus (Instanziierung, Vorbereitung, Kompilierung, Ausführung und Beendigung) und in den verschiedenen Schichten der Anwendungen anzeigt. Ihre Anwendung ist in folgende Schichten unterteilt:

  • Application: den Hauptanwendungscode
  • Runtime: NNAPI Runtime
  • IPC: Die interprozedurale Kommunikation zwischen der NNAPI-Laufzeit und dem Treibercode
  • Driver: den Beschleunigertreiberprozess.

Daten für die Analyse des Nutzerprofils generieren

Angenommen, Sie haben den AOSP-Quellbaum unter $ANDROID_BUILD_TOP auscheckt und verwenden das Beispiel für die TFLite-Bildklassifizierung als Zielanwendung. Dann können Sie die NNAPI-Profilierungsdaten 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 Ihre eigene Anwendung profilieren, müssen Sie org.tensorflow.lite.examples.classification durch den im App-Manifest angegebenen Prozessnamen ersetzen.

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

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

In unserem Fall können Sie die App Bildklassifizierung über Android Studio oder direkt über die Benutzeroberfläche Ihres Testsmartphones starten, wenn die App bereits installiert ist. Wenn Sie NNAPI-Daten generieren möchten, müssen Sie die App so konfigurieren, dass sie NNAPI verwendet. Wählen Sie dazu im Dialogfeld „App-Konfiguration“ NNAPI als Zielgerät aus.

  1. Beenden Sie den Systrace, indem Sie im seit Schritt 1 aktiven Terminal die Taste enter drücken.

  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 Schicht verbracht wurde, einschließlich der Zeit, die auf die Ausführung eines Aufrufs an eine untergeordnete Schicht gewartet wurde. - --print-detail: Druckt alle Ereignisse aus, die von systrace erfasst wurden. - --per-execution: Druckt nur die Ausführung und ihre Unterphasen (als Ausführungszeiten) anstelle von Statistiken für alle Phasen aus. - --json: Erzeugt 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 schlägt möglicherweise fehl, wenn die erfassten Ereignisse keinen vollständigen Anwendungs-Trace darstellen. Insbesondere kann es zu Fehlern kommen, wenn im Trace Systrace-Ereignisse zum Markieren des Endes eines Abschnitts ohne zugehöriges Ereignis zum Starten des Abschnitts vorhanden sind. Das passiert in der Regel, wenn beim Starten des Systrace-Collectors einige Ereignisse aus einer vorherigen Profiling-Sitzung generiert werden. In diesem Fall müssen Sie das Profiling noch einmal ausführen.

Statistiken für Ihren Anwendungscode zur systrace_parser-Ausgabe hinzufügen

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

Wenn Sie Ihre benutzerdefinierten Ereignisse den Phasen des Anwendungslebenszyklus zuordnen möchten, fügen Sie dem Ereignisnamen einen der folgenden Strings vor:

  • [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]: Ereignis auf Anwendungsebene für die Ausführung

Hier ist ein Beispiel dafür, wie Sie den Beispielcode für die TFLite-Bildklassifizierung ändern können, indem Sie einen Abschnitt runInferenceModel für die Phase Execution und die Schicht Application mit anderen 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;
}

Dienstqualität

In Android 11 und höher ermöglicht NNAPI eine bessere Dienstqualität (Quality of Service, QoS), 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 Ausführung einer bestimmten Berechnung erwartet wird, angeben kann. Android 11 führt außerdem zusätzliche NNAPI-Ergebniscodes ein, mit denen Anwendungen Fehler wie verpasste Ausführungsfristen besser nachvollziehen können.

Priorität einer Arbeitslast festlegen

Wenn Sie die Priorität einer NNAPI-Arbeitslast festlegen möchten, rufen Sie ANeuralNetworksCompilation_setPriority() vor dem Aufrufen von ANeuralNetworksCompilation_finish() auf.

Fristen festlegen

Für Anwendungen können sowohl Fristen für die Modellkompilierung als auch für die Inferenz festgelegt werden.

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 8‑Bit-asymmetrisch quantisierte Tensoren. Bei diesen Tensoren wird der Wert jeder Zelle durch eine 8‑Bit-Ganzzahl dargestellt. Dem Tensor sind eine Skala und ein Nullpunktwert zugeordnet. Mit diesen werden die 8‑Bit-Ganzzahlen in die dargestellten Gleitkommawerte umgewandelt.

Die Formel lautet:

(cellValue - zeroPoint) * scale

wobei „zeroPoint“ eine 32-Bit-Ganzzahl und „scale“ ein 32-Bit-Gleitkommawert ist.

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

  • Ihre Anwendung ist kleiner, da die trainierten Gewichte ein Viertel der Größe von 32‑Bit-Tensoren haben.
  • Berechnungen können oft schneller ausgeführt werden. Das liegt an der geringeren Datenmenge, die aus dem Arbeitsspeicher abgerufen werden muss, und an der Effizienz von Prozessoren wie DSPs bei Ganzzahlberechnungen.

Es ist zwar möglich, ein Gleitkommamodell in ein quantisiertes Modell umzuwandeln, aber unsere Erfahrung hat gezeigt, dass bessere Ergebnisse erzielt werden, wenn ein quantisiertes Modell direkt trainiert wird. Das neuronale Netzwerk lernt sozusagen, die erhöhte Detailgenauigkeit der einzelnen Werte zu kompensieren. Für jeden quantisierten Tensor werden die Werte „scale“ und „zeroPoint“ während des Trainings bestimmt.

In NNAPI definieren Sie quantisierte Tensortypen, indem Sie das Typfeld der Datenstruktur ANeuralNetworksOperandType auf ANEURALNETWORKS_TENSOR_QUANT8_ASYMM festlegen. Sie geben auch den Skalierungs- und Nullpunktwert des Tensors in dieser Datenstruktur an.

Neben 8‑Bit-asymmetrisch quantisierten Tensoren unterstützt NNAPI Folgendes:

Optionale Operanden

Einige Vorgänge, z. B. ANEURALNETWORKS_LSH_PROJECTION, akzeptieren optionale Operanden. 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 Puffer und 0 für die Länge.

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

Tensoren mit unbekanntem Rang

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

NNAPI-Benchmark

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

Der Benchmark bewertet Latenz und Genauigkeit und vergleicht die Treiber mit derselben Arbeit, die mit Tensorflow Lite auf der CPU ausgeführt wird, für dieselben Modelle und Datensätze.

So verwenden Sie den Benchmark:

  1. Schließen Sie ein Ziel-Android-Gerät an Ihren Computer an, öffnen Sie ein Terminalfenster und prüfen Sie, ob das Gerät über ADB erreichbar ist.

  2. Wenn mehr als ein Android-Gerät verbunden ist, exportieren Sie die Umgebungsvariable ANDROID_SERIAL des Zielgeräts.

  3. Rufen Sie das oberste Android-Quellverzeichnis auf.

  4. Führen Sie folgende Befehle aus:

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

    Am Ende eines Benchmarklaufs werden die Ergebnisse als HTML-Seite angezeigt, die an xdg-open übergeben wird.

NNAPI-Protokolle

NNAPI generiert nützliche Diagnoseinformationen in den Systemprotokollen. Verwenden Sie das Dienstprogramm logcat, um die Protokolle zu analysieren.

Wenn Sie detaillierte NNAPI-Protokollierung für bestimmte Phasen oder Komponenten aktivieren möchten, legen Sie das Attribut debug.nn.vlog (mit adb shell) auf die folgende Liste von Werten fest, die durch Leerzeichen, Doppelpunkte oder Kommas getrennt sind:

  • model: Modellierung
  • compilation: Generierung des Ausführungsplans für das Modell und Kompilierung
  • execution: Modellausführung
  • cpuexe: Ausführung von Vorgängen mit der NNAPI-CPU-Implementierung
  • manager: Informationen zu NNAPI-Erweiterungen, verfügbaren Schnittstellen und Funktionen
  • all oder 1: Alle oben genannten Elemente

Wenn Sie beispielsweise ein vollständiges ausführliches Logging aktivieren möchten, verwenden Sie den Befehl adb shell setprop debug.nn.vlog all. Verwenden Sie den Befehl adb shell setprop debug.nn.vlog '""', um das ausführliche Logging zu deaktivieren.

Wenn die ausführliche Protokollierung aktiviert ist, werden Protokolleinträge auf INFO-Ebene generiert, die mit einem Tag für den Namen der Phase oder Komponente versehen sind.

Neben den debug.nn.vlog-gesteuerten Nachrichten enthalten NNAPI API-Komponenten weitere Protokolleinträge auf verschiedenen Ebenen, die jeweils ein bestimmtes Protokoll-Tag verwenden.

Um eine Liste der Komponenten zu erhalten, suchen Sie im Quellbaum mit dem folgenden Ausdruck:

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
  • Callbacks
  • CompilationBuilder
  • CpuExecutor
  • ExecutionBuilder
  • ExecutionBurstController
  • ExecutionBurstServer
  • ExecutionPlan
  • FibonacciDriver
  • GraphDump
  • IndexedShapeWrapper
  • IonWatcher
  • Managerin/Manager
  • Arbeitsspeicher
  • MemoryUtils
  • MetaModel
  • ModelArgumentInfo
  • ModelBuilder
  • NeuralNetworks
  • OperationResolver
  • Aufgaben und Ablauf
  • OperationsUtils
  • PackageInfo
  • TokenHasher
  • TypeManager
  • Utils
  • ValidateHal
  • VersionedInterfaces

Mit der Umgebungsvariablen ANDROID_LOG_TAGS können Sie festlegen, welche Protokollmeldungen von logcat angezeigt werden.

Wenn Sie alle NNAPI-Logmeldungen anzeigen 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')

Hinweis: Dies ist nur ein Filter, der auf logcat angewendet wird. Sie müssen die Eigenschaft debug.nn.vlog weiterhin auf all festlegen, um ausführliche Protokollinformationen zu generieren.