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.
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.
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 einemARect
-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.
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:
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:
Rufen Sie die Funktion
ANeuralNetworksModel_create()
auf, um ein leeres Modell zu definieren.ANeuralNetworksModel* model = NULL; ANeuralNetworksModel_create(&model);
Fügen Sie Ihrem Modell die Operanden hinzu, indem Sie
ANeuralNetworks_addOperand()
aufrufen. Ihre Datentypen werden mit der DatenstrukturANeuralNetworksOperandType
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 6Verwenden Sie für Operanden mit konstanten Werten, z. B. Gewichte und Voreingenommenheiten, die Ihre App aus einem Trainingsvorgang erhält, die Funktionen
ANeuralNetworksModel_setOperandValue()
undANeuralNetworksModel_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));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);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);
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 dazuANeuralNetworksModel_relaxComputationFloat32toFloat16()
auf.Rufen Sie
ANeuralNetworksModel_finish()
auf, um die Definition Ihres Modells abzuschließen. Wenn keine Fehler auftreten, gibt diese Funktion den ErgebniscodeANEURALNETWORKS_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:
Erstellen Sie die entsprechenden Ausführungsuntergraphen (
then
- undelse
-Untergraphen für eineIF
-Anweisung,condition
- undbody
-Untergraphen für eineWHILE
-Schleife) als eigenständigeANeuralNetworksModel*
-Modelle:ANeuralNetworksModel* thenModel = makeThenModel(); ANeuralNetworksModel* elseModel = makeElseModel();
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);
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:
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.
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.
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 dascacheDir
. Der angegebenetoken
muss für jedes Modell in der Anwendung eindeutig sein.Schließen Sie die Definition der Kompilierung ab, indem Sie
ANeuralNetworksCompilation_finish()
aufrufen. Wenn keine Fehler auftreten, gibt diese Funktion den ErgebniscodeANEURALNETWORKS_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:
ANeuralNetworksDevice_getFeatureLevel
ANeuralNetworksDevice_getName
ANeuralNetworksDevice_getType
ANeuralNetworksDevice_getVersion
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:
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);
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()
oderANeuralNetworksExecution_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));
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()
oderANeuralNetworksExecution_setOutputFromMemory()
aufruft.// Set the output float32 myOutput[3][4]; ANeuralNetworksExecution_setOutput(run1, 0, NULL, myOutput, sizeof(myOutput));
Rufen Sie die Funktion
ANeuralNetworksExecution_startCompute()
auf, um die Ausführung zu planen. Wenn keine Fehler auftreten, gibt diese Funktion den ErgebniscodeANEURALNETWORKS_NO_ERROR
zurück.// Starts the work. The work proceeds asynchronously ANeuralNetworksEvent* run1_end = NULL; ANeuralNetworksExecution_startCompute(run1, &run1_end);
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 ErgebniscodeANEURALNETWORKS_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);
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:
Rufen Sie die Funktion
ANeuralNetworksMemoryDesc_create()
auf, um einen neuen Speicherbeschreiber zu erstellen:// Create a memory descriptor ANeuralNetworksMemoryDesc* desc; ANeuralNetworksMemoryDesc_create(&desc);
Geben Sie alle vorgesehenen Eingabe- und Ausgaberollen an, indem Sie
ANeuralNetworksMemoryDesc_addInputRole()
undANeuralNetworksMemoryDesc_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);
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);
Schließe die Deskriptordefinition ab, indem du
ANeuralNetworksMemoryDesc_finish()
aufrufst.ANeuralNetworksMemoryDesc_finish(desc);
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);
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 mitANeuralNetworksCompilation_createForDevices
undnumDevices = 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 HauptanwendungscodeRuntime
: NNAPI RuntimeIPC
: Die interprozedurale Kommunikation zwischen der NNAPI-Laufzeit und dem TreibercodeDriver
: 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:
- 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.
- 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.
Beenden Sie den Systrace, indem Sie im seit Schritt 1 aktiven Terminal die Taste
enter
drücken.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 ListrecognizeImage(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.
- Wenn Sie das Kompilierungszeitlimit festlegen möchten, rufen Sie
ANeuralNetworksCompilation_setTimeout()
auf, bevor SieANeuralNetworksCompilation_finish()
aufrufen. - Wenn Sie das Zeitlimit für die Inferenz festlegen möchten, rufen Sie
ANeuralNetworksExecution_setTimeout()
auf, bevor Sie mit der Kompilierung beginnen.
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:
ANEURALNETWORKS_TENSOR_QUANT8_SYMM_PER_CHANNEL
, mit dem Sie Gewichte fürCONV/DEPTHWISE_CONV/TRANSPOSED_CONV
-Vorgänge angeben können.ANEURALNETWORKS_TENSOR_QUANT16_ASYMM
, die Sie für den internen Status vonQUANTIZED_16BIT_LSTM
verwenden können.ANEURALNETWORKS_TENSOR_QUANT8_SYMM
, die als Eingabe fürANEURALNETWORKS_DEQUANTIZE
verwendet werden kann.
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:
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.
Wenn mehr als ein Android-Gerät verbunden ist, exportieren Sie die Umgebungsvariable
ANDROID_SERIAL
des Zielgeräts.Rufen Sie das oberste Android-Quellverzeichnis auf.
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
: Modellierungcompilation
: Generierung des Ausführungsplans für das Modell und Kompilierungexecution
: Modellausführungcpuexe
: Ausführung von Vorgängen mit der NNAPI-CPU-Implementierungmanager
: Informationen zu NNAPI-Erweiterungen, verfügbaren Schnittstellen und Funktionenall
oder1
: 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.