API für neuronale Netzwerke

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

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

Die Inferenz auf dem Gerät hat viele Vorteile:

  • Latenz: Sie müssen keine Anfrage über eine Netzwerkverbindung senden und auf eine Antwort warten. Dies kann beispielsweise für Videoanwendungen wichtig sein, die aufeinanderfolgende Bilder von einer Kamera verarbeiten.
  • Verfügbarkeit: Die Anwendung wird auch außerhalb der Netzwerkabdeckung ausgeführt.
  • Geschwindigkeit: Neue Hardware speziell für die Verarbeitung neuronaler Netzwerke bietet deutlich schnellere Rechenvorgänge als eine Allzweck-CPU allein.
  • Datenschutz: Die Daten werden ausschließlich auf dem Android-Gerät gespeichert.
  • Kosten: Wenn alle Berechnungen auf dem Android-Gerät ausgeführt werden, ist keine Serverfarm erforderlich.

Entwickler sollten außerdem einige Vor- und Nachteile beachten:

  • Systemauslastung: Die Bewertung neuronaler Netzwerke ist mit einem hohen Rechenaufwand verbunden, der den Akkuverbrauch erhöhen kann. Sie sollten den Akkuzustand überwachen, wenn dies für Ihre App ein Problem darstellt, insbesondere bei lang andauernden Berechnungen.
  • Anwendungsgröße: Achten Sie auf die Größe Ihrer Modelle. Modelle können mehrere Megabyte Speicherplatz belegen. Wenn das Bündeln großer Modelle in Ihrem APK unangemessene Auswirkungen auf Ihre Nutzer hätte, kann es sinnvoll sein, die Modelle nach der App-Installation herunterzuladen, kleinere Modelle zu verwenden oder Ihre Berechnungen in der Cloud auszuführen. NNAPI bietet keine Funktion zum Ausführen von Modellen in der Cloud.

Das Android Neural Networks API-Beispiel zeigt ein Beispiel für die Verwendung von NNAPI.

Informationen zur Laufzeit der Neural Networks API

NNAPI soll von Bibliotheken, Frameworks und Tools für maschinelles Lernen aufgerufen werden, mit denen Entwickler ihre Modelle extern trainieren und auf Android-Geräten bereitstellen können. Anwendungen verwenden normalerweise nicht direkt NNAPI, sondern stattdessen übergeordnete Frameworks für maschinelles Lernen. Diese Frameworks wiederum könnten NNAPI verwenden, um hardwarebeschleunigte Inferenzvorgänge auf unterstützten Geräten auszuführen.

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

Bei 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

Neural Networks API-Programmiermodell

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

NNAPI verwendet vier Hauptabstraktionen:

  • Modell: Eine Berechnungsgrafik mathematischer Operationen und der durch einen Trainingsprozess erlernten konstanten Werte. Diese Vorgänge sind spezifisch für neuronale Netzwerke. Dazu gehören unter anderem die zweidimensionale (2D-) Faltung, die logistische Aktivierung (Sigmoid) und die rektifizierte lineare (ReLU)-Aktivierung. Das Erstellen eines Modells ist ein synchroner Vorgang. Nach der Erstellung kann sie über Threads und Kompilierungen hinweg wiederverwendet werden. In NNAPI wird ein Modell als ANeuralNetworksModel-Instanz dargestellt.
  • Kompilierung: Stellt eine Konfiguration zum Kompilieren eines NNAPI-Modells in untergeordneten Code dar. Das Erstellen einer Kompilierung ist ein synchroner Vorgang. Nach ihrer Erstellung kann sie über Threads und Ausführungen hinweg wiederverwendet werden. In NNAPI wird jede Kompilierung als ANeuralNetworksCompilation-Instanz dargestellt.
  • Arbeitsspeicher: Steht für gemeinsamen Arbeitsspeicher, dem Arbeitsspeicher zugeordnete Dateien und ähnliche Arbeitsspeicherpuffer. Durch die Verwendung eines Arbeitsspeicherpuffers kann die NNAPI-Laufzeit Daten effizienter an Treiber übertragen. Eine Anwendung erstellt in der Regel einen Zwischenspeicher für gemeinsamen Arbeitsspeicher, der jeden Tensor enthält, der zum Definieren eines Modells erforderlich ist. Sie können auch Speicherzwischenspeicher verwenden, um die Ein- und Ausgaben für eine Ausführungsinstanz zu speichern. In NNAPI wird jeder Zwischenspeicher des Arbeitsspeichers 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 durchgeführt werden.

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

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

Abbildung 2 zeigt den grundlegenden Programmierablauf.

Abbildung 2. Programmierung für die Android Neural Networks API

Der Rest dieses Abschnitts beschreibt die Schritte zum Einrichten Ihres NNAPI-Modells, um Berechnungen durchzuführen, das Modell zu kompilieren und das kompilierte Modell auszuführen.

Zugriff auf Trainingsdaten gewähren

Ihre trainierten Daten zu Gewichtungen und Verzerrungen werden wahrscheinlich in einer Datei gespeichert. Damit die NNAPI-Laufzeit effizient auf diese Daten zugreifen kann, erstellen Sie eine ANeuralNetworksMemory-Instanz. Rufen Sie dazu die Funktion ANeuralNetworksMemory_createFromFd() auf und übergeben Sie den Dateideskriptor der geöffneten Datendatei. Außerdem geben Sie Speicherschutz-Flags und einen Offset an, bei dem der Bereich des gemeinsamen Arbeitsspeichers 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);

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

Native Hardwarezwischenspeicher verwenden

Sie können native Hardwarezwischenspeicher für Modelleingaben, -ausgaben und konstante Operandenwerte verwenden. In bestimmten Fällen kann ein NNAPI-Beschleuniger auf AHardwareBuffer-Objekte zugreifen, ohne dass der Treiber die Daten kopieren muss. AHardwareBuffer hat viele verschiedene Konfigurationen und nicht jeder NNAPI-Beschleuniger unterstützt möglicherweise alle diese Konfigurationen. Lesen Sie sich daher die Einschränkungen in der Referenzdokumentation zu ANeuralNetworksMemory_createFromAHardwareBuffer durch und testen Sie vorab auf Zielgeräten, ob Kompilierungen und Ausführungen, die AHardwareBuffer verwenden, wie erwartet verhalten. Geben Sie dazu den Beschleuniger über die Gerätezuweisung an.

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

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

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

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

ANeuralNetworksMemory_free(mem2);

Hinweis:

  • Sie können AHardwareBuffer nur für den gesamten Zwischenspeicher verwenden, nicht zusammen mit einem ARect-Parameter.
  • Die NNAPI-Laufzeit leert den Zwischenspeicher nicht. Sie müssen darauf achten, dass die Eingabe- und Ausgabepuffer zugänglich sind, bevor Sie die Ausführung planen.
  • Es gibt keine Unterstützung für die Synchronisierung von Fence-Dateideskriptoren.
  • Bei einer AHardwareBuffer mit anbieterspezifischen Formaten und Nutzungsbits muss die Anbieterimplementierung festlegen, ob der Client oder der Treiber für das Leeren des Caches verantwortlich ist.

Modell

Ein Modell ist die grundlegende Berechnungseinheit 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 Ein- und Ausgaben des Modells, die Zwischenknoten mit den Daten, die von einem Vorgang zum anderen fließen, sowie die Konstanten, die an diese Vorgänge übergeben werden.

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

Ein Skalar stellt einen einzelnen Wert dar. NNAPI unterstützt skalare Werte in den Formaten boolescher Werte, 16-Bit-Gleitkomma, 32-Bit-Gleitkomma, 32-Bit-Ganzzahl und 32-Bit-Ganzzahl ohne Vorzeichen.

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

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

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

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

Die Reihenfolge, in der Sie die Operanden hinzufügen, spielt keine Rolle. Beispielsweise könnte der Modellausgabeoperanden 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 Ausgabeoperanden von genau einem Vorgang sein.

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

Aufgaben und Ablauf

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

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

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

Bevor Sie den Vorgang hinzufügen, müssen Sie die Operanden, die ein Vorgang verbraucht oder erzeugt, dem Modell hinzufügen.

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

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

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

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

Modelle erstellen

Im folgenden Beispiel erstellen wir das Modell mit zwei Vorgängen, das in Abbildung 3 zu sehen ist.

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 mithilfe 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. Gewichtungen und Verzerrungen, die Ihre Anwendung aus einem Trainingsprozess erhält, die Funktionen ANeuralNetworksModel_setOperandValue() und ANeuralNetworksModel_setOperandValueFromMemory().

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

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

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

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

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

    Ein Operand kann nicht sowohl für die Ein- als auch die Ausgabe desselben Vorgangs verwendet werden.

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

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

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

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

Ablauf steuern

So binden Sie den Ablauf steuern in ein NNAPI-Modell ein:

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

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

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

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

Compilation

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

So kompilieren Sie ein Modell:

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

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

    Optional können Sie die Gerätezuweisung verwenden, um explizit auszuwählen, auf welchen Geräten die Ausführung erfolgen soll.

  2. Sie können optional beeinflussen, wie die Laufzeit zwischen Akkuverbrauch und Ausführungsgeschwindigkeit abwägt. 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 die Akkuentladung minimiert wird. Dies ist bei häufig ausgeführten Kompilierungen wünschenswert.
    • ANEURALNETWORKS_PREFER_FAST_SINGLE_ANSWER: Es sollte möglichst schnell eine einzelne Antwort zurückgegeben werden, auch wenn dies den Stromverbrauch erhöht. Das ist die Standardeinstellung.
    • ANEURALNETWORKS_PREFER_SUSTAINED_SPEED: Es wird empfohlen, den Durchsatz aufeinanderfolgender Frames zu maximieren, z. B. bei der Verarbeitung aufeinanderfolgender Frames, die von der Kamera kommen.
  3. Optional können Sie das Kompilierungs-Caching einrichten. Dazu rufen Sie ANeuralNetworksCompilation_setCaching auf.

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

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

  4. Schließen Sie die Kompilierungsdefinition ab, indem Sie ANeuralNetworksCompilation_finish() aufrufen. Wenn keine Fehler vorliegen, 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 Framework-Bibliotheken und -Apps für maschinelles Lernen Informationen zu verfügbaren Geräten abrufen und Geräte angeben können, die für die Ausführung verwendet werden sollen. Durch die Bereitstellung von Informationen zu den verfügbaren Geräten können Apps die genaue Version der Treiber abrufen, die auf einem Gerät gefunden wurden, um bekannte Inkompatibilitäten zu vermeiden. Indem Sie Apps die Möglichkeit geben, anzugeben, welche Geräte die verschiedenen Abschnitte eines Modells ausführen sollen, können Apps für das Android-Gerät optimiert werden, auf dem sie bereitgestellt werden.

Geräteerkennung

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

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

Gerätezuweisung

Mit ANeuralNetworksModel_getSupportedOperationsForDevices können Sie ermitteln, 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 ANeuralNetworksCompilation_createForDevices anstelle von ANeuralNetworksCompilation_create auf. Verwende wie gewohnt das resultierende ANeuralNetworksCompilation-Objekt. 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, sorgt die Laufzeit dafür, dass die Arbeit auf die Geräte verteilt wird.

Ähnlich wie andere Geräte wird die NNAPI-CPU-Implementierung durch eine ANeuralNetworksDevice mit dem Namen nnapi-reference und dem Typ ANEURALNETWORKS_DEVICE_TYPE_CPU dargestellt. Beim Aufrufen von ANeuralNetworksCompilation_createForDevices wird die CPU-Implementierung nicht zum Verarbeiten der Fehlerfälle bei der Modellkompilierung und -ausführung verwendet.

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

Modellpartitionierung

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

Für die Verteilung wird in der Liste der verfügbaren Geräte für jeden Vorgang im Modell das Gerät ausgewählt, das den Vorgang unterstützt, und es wird die beste Leistung angegeben, d.h. die schnellste Ausführungszeit oder der niedrigste Stromverbrauch, je nach Ausführungseinstellung des Clients. 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 bei Verwendung von ANeuralNetworksCompilation_createForDevices oder implizit durch Verwendung von ANeuralNetworksCompilation_create), ist es wichtig, ein Profil für die resultierende Anwendung zu erstellen.

Wenn Sie wissen möchten, wie Ihr Modell durch NNAPI partitioniert wurde, suchen Sie in den Android-Logs nach einer Nachricht (auf INFO-Ebene mit dem Tag ExecutionPlan):

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

op-name ist der beschreibende Name des Vorgangs in der Grafik und device-index ist der Index des möglichen Geräts in der Geräteliste. Diese Liste ist die Eingabe für ANeuralNetworksCompilation_createForDevices oder bei Verwendung von ANeuralNetworksCompilation_createForDevices die Liste der Geräte, die zurückgegeben werden, wenn über alle Geräte mit ANeuralNetworks_getDeviceCount und ANeuralNetworks_getDevice iteriert wird.

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

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

Diese Meldung gibt an, dass das gesamte Diagramm auf dem Gerät device-name beschleunigt wurde.

Umsetzung

Der Ausführungsschritt wendet das Modell auf eine Reihe von Eingaben an und speichert die Berechnungsausgaben in einem oder mehreren Nutzerpuffern oder Arbeitsspeicherbereichen, die von Ihrer Anwendung zugewiesen wurden.

So führen Sie ein kompiliertes Modell aus:

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

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

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

    // Set the output
    float32 myOutput[3][4];
    ANeuralNetworksExecution_setOutput(run1, 0, NULL, myOutput, sizeof(myOutput));
    
  4. Planen Sie den Start der Ausführung durch Aufrufen der Funktion ANeuralNetworksExecution_startCompute(). Wenn keine Fehler vorliegen, gibt diese Funktion den Ergebniscode ANEURALNETWORKS_NO_ERROR zurück.

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

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

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

Synchrone Ausführung

Bei der asynchronen Ausführung wird Zeit benötigt, um Threads zu erstellen und zu synchronisieren. Darüber hinaus kann die Latenz stark variieren, da die längsten Verzögerungen von bis zu 500 Mikrosekunden zwischen dem Zeitpunkt, an dem ein Thread benachrichtigt oder geweckt wird, und dem Zeitpunkt der endgültigen Bindung an einen CPU-Kern erreichen.

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

Serienausführungen

Auf Android-Geräten mit Android 10 (API-Level 29) und höher unterstützt die NNAPI Burst-Ausführungen über das ANeuralNetworksBurst-Objekt. Burst-Ausführungen sind Abfolgen derselben Kompilierung, die in schneller Abfolge auftreten, z. B. bei Bildern einer Kameraaufnahme oder aufeinanderfolgenden Audioproben. Die Verwendung von ANeuralNetworksBurst-Objekten kann zu einer schnelleren Ausführung führen, da sie Beschleunigern signalisieren, dass Ressourcen zwischen Ausführungen wiederverwendet werden können und dass Beschleuniger für die Dauer des Bursts im Hochleistungszustand bleiben sollten.

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

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

Burst-Ausführungen erfolgen synchron. Statt jedoch ANeuralNetworksExecution_compute zur Durchführung jeder Inferenz zu verwenden, kombinieren Sie die verschiedenen ANeuralNetworksExecution-Objekte mit demselben ANeuralNetworksBurst-Wert in Aufrufen der Funktion ANeuralNetworksExecution_burstCompute.

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

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

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

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

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

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

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

// Cleanup
ANeuralNetworksBurst_free(burst);

Asynchrone Befehlswarteschlangen und gegnerische Ausführung

Unter Android 11 und höher unterstützt NNAPI eine zusätzliche Möglichkeit zur Planung einer asynchronen Ausführung mit der Methode ANeuralNetworksExecution_startComputeWithDependencies(). Wenn Sie diese Methode verwenden, wartet die Ausführung, bis alle abhängigen Ereignisse signalisiert wurden, bevor die Bewertung gestartet wird. Sobald die Ausführung abgeschlossen ist und die Ausgaben verarbeitet werden können, wird das zurückgegebene Ereignis signalisiert.

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

Ausgaben mit dynamischer Größe

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

Das folgende Codebeispiel zeigt, wie dies funktioniert:

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

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

Bereinigung

Im Bereinigungsschritt werden interne Ressourcen freigegeben, die für Ihre Berechnung verwendet werden.

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

Fehlerverwaltung und CPU-Fallback

Wenn während der Partitionierung ein Fehler auftritt, ein Treiber ein Modell nicht kompiliert oder ein Treiber ein kompiliertes Modell nicht ausführen kann, greift NNAPI möglicherweise auf seine eigene CPU-Implementierung des einen oder mehrerer Vorgänge zurück.

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

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

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

Bei der ersten Ausführung wird für diese einzelne Partition zurückgegriffen. Sollte dieser immer noch fehlschlagen, 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 ausprobiert.

In manchen Fällen werden einige Vorgänge auf der CPU nicht unterstützt und in solchen Fällen schlägt die Kompilierung oder Ausführung fehl, anstatt zurückgegriffen zu werden.

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

Damit keine CPU ausgeführt wird, verwenden Sie ANeuralNetworksCompilation_createForDevices und schließen Sie nnapi-reference aus der Geräteliste aus. Ab Android P ist es möglich, das Fallback bei der Ausführung von DEBUG-Builds zu deaktivieren, indem das Attribut debug.nn.partition auf 2 gesetzt wird.

Arbeitsspeicherdomains

In Android 11 und höher unterstützt NNAPI Speicherdomains, die Zuweisungsschnittstellen für intransparente Speicher bereitstellen. Dadurch können Anwendungen gerätenative Speicher für verschiedene Ausführungen übergeben, sodass NNAPI Daten nicht unnötig kopiert oder transformiert, wenn sie aufeinanderfolgende Ausführungen auf demselben Treiber ausführen.

Das Speicherdomainfeature ist für Tensoren vorgesehen, die hauptsächlich intern im Treiber sind und keinen häufigen Zugriff auf die Clientseite benötigen. Beispiele für solche Tensoren sind die Zustandstensoren in Sequenzmodellen. Verwenden Sie für Tensoren, die clientseitigen häufigen CPU-Zugriff benötigen, stattdessen Pools mit gemeinsam genutztem Arbeitsspeicher.

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

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

    // Create a memory descriptor
    ANeuralNetworksMemoryDesc* desc;
    ANeuralNetworksMemoryDesc_create(&desc);
    
  2. Geben Sie alle gewünschten 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. Geben Sie optional die Arbeitsspeicherabmessungen an, indem Sie ANeuralNetworksMemoryDesc_setDimensions() aufrufen.

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

    ANeuralNetworksMemoryDesc_finish(desc);
    
  5. Sie können so viele Erinnerungen wie nötig zuweisen. Übergeben Sie dazu den Deskriptor an ANeuralNetworksMemory_createFromDesc().

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

    ANeuralNetworksMemoryDesc_free(desc);
    

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

Sie können intransparente Erinnerungen mit Rollen mit nicht angegebenen Dimensionen oder Rängen erstellen. In diesem Fall kann die Erstellung des Arbeitsspeichers mit dem Status ANEURALNETWORKS_OP_FAILED fehlschlagen, wenn er vom zugrunde liegenden Treiber nicht unterstützt wird. Der Client wird aufgefordert, eine Fallback-Logik zu implementieren, indem er einen ausreichend großen Puffer in Ashmem- oder BLOB-Modus-AHardwareBuffer einordnet.

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

ANeuralNetworksMemory_free(opaqueMem);

Leistungsmessung

Sie können die Leistung Ihrer Anwendung durch Messen der Ausführungszeit oder durch Profilerstellung bewerten.

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 durch den Aufruf benötigte Zeit messen. Wenn Sie die Gesamtausführungszeit über eine niedrigere Ebene des Softwarestacks ermitteln möchten, können Sie mit ANeuralNetworksExecution_setMeasureTiming und ANeuralNetworksExecution_getDuration Folgendes abrufen:

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

Die Ausführungszeit im Treiber schließt Aufwand 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 der gesendeten und der abgeschlossenen Arbeit und nicht die Zeit, die ein Treiber oder Beschleuniger für die Inferenz aufwendet und möglicherweise durch einen Kontextwechsel unterbrochen wird.

Wenn beispielsweise Inferenz 1 beginnt, der Treiber die Arbeit zur Ausführung von Inferenz 2 stoppt, dann fortgesetzt und Inferenz 1 abgeschlossen hat, enthält die Ausführungszeit für Inferenz 1 die Zeit, in der die Arbeit zur Ausführung von Inferenz 2 angehalten wurde.

Diese Zeitinformationen können für eine Produktionsbereitstellung einer Anwendung nützlich sein, um Telemetriedaten für die Offlinenutzung zu erfassen. Sie können die Zeitdaten verwenden, um die Leistung der App zu verbessern.

Beachten Sie bei der Verwendung dieser Funktion Folgendes:

  • Das Erfassen von Zeitinformationen kann Leistungskosten verursachen.
  • Nur ein Treiber kann die für den Beschleuniger oder für sich selbst aufgewendete Zeit berechnen, mit Ausnahme der in der NNAPI-Laufzeit und im IPC verbrachten Zeit.
  • 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 zu melden.

Profil für Ihre Anwendung mit Android Systrace erstellen

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

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

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

Analysedaten zur Profilerstellung generieren

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

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

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

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

  1. Starten Sie nach dem Start des systrace-Collectors Ihre Anwendung und führen Sie den Benchmark-Test aus.

In unserem Fall können Sie die App Image Classification (Bildklassifizierung) über Android Studio oder direkt über die Benutzeroberfläche Ihres Testtelefons starten, sofern sie bereits installiert ist. Zum Generieren einiger NNAPI-Daten müssen Sie die Anwendung für die Verwendung von NNAPI konfigurieren. Wählen Sie dazu NNAPI als Zielgerät im Dialogfeld für die Anwendungskonfiguration aus.

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

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

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

Der Parser akzeptiert die folgenden Parameter: – --total-times: zeigt die Gesamtzeit an, die in einer Ebene aufgewendet wurde, einschließlich der Zeit, die auf die Ausführung eines Aufrufs einer zugrunde liegenden Ebene gewartet wurde – --print-detail: gibt alle Ereignisse aus, die von systrace erfasst wurden – --per-execution: gibt nur die Ausführung und ihre Teilphasen (wie bei der Ausführung) 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 kann fehlschlagen, wenn die erfassten Ereignisse keinen vollständigen Anwendungs-Trace darstellen. Insbesondere kann es fehlschlagen, wenn Systrace-Ereignisse, die generiert wurden, um das Ende eines Abschnitts zu markieren, im Trace ohne ein verknüpftes Abschnittsstartereignis vorhanden sind. Dies geschieht in der Regel, wenn beim Starten des Systrace-Collectors einige Ereignisse aus einer vorherigen Profilerstellungssitzung generiert werden. In diesem Fall müssen Sie die Profilerstellung noch einmal ausführen.

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

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

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

  • [NN_LA_PI]: Ereignis auf Anwendungsebene zur 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

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

Kotlin

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

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

Java

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

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

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

Servicequalität

Unter Android 11 und höher ermöglicht NNAPI eine bessere Dienstqualität, da eine Anwendung die relativen Prioritäten ihrer Modelle, die erwartete maximale Zeit für die Vorbereitung eines bestimmten Modells und die erwartete maximale Zeit für eine bestimmte Berechnung angeben kann. In Android 11 werden außerdem zusätzliche NNAPI-Ergebniscodes eingeführt, mit denen Anwendungen Fehler wie z. B. verpasste Ausführungstermine besser nachvollziehen können.

Priorität einer Arbeitslast festlegen

Rufen Sie zum Festlegen der Priorität einer NNAPI-Arbeitslast ANeuralNetworksCompilation_setPriority() auf, bevor Sie ANeuralNetworksCompilation_finish() aufrufen.

Fristen festlegen

Anwendungen können Fristen sowohl für die Modellkompilierung als auch für die Inferenz festlegen.

Weitere Informationen zu Operanden

Im folgenden Abschnitt werden weiterführende Themen zur Verwendung von Operanden behandelt.

Quantisierte Tensoren

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

NNAPI unterstützt asymmetrische quantisierte 8-Bit-Tensoren. Für diese Tensoren wird der Wert jeder Zelle durch eine 8-Bit-Ganzzahl dargestellt. Dem Tensor sind eine Skala und ein Nullpunktwert zugeordnet. Damit werden die 8-Bit-Ganzzahlen in die dargestellten Gleitkommawerte konvertiert.

Die Formel lautet:

(cellValue - zeroPoint) * scale

wobei der Wert "nullPoint" eine 32-Bit-Ganzzahl und die Skalierung eine 32-Bit-Gleitkommazahl ist.

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

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

Es ist zwar möglich, ein Gleitkommamodell in ein quantisiertes Modell zu konvertieren, aber unsere Erfahrung hat gezeigt, dass bessere Ergebnisse erzielt werden, wenn ein quantisiertes Modell direkt trainiert wird. Tatsächlich lernt das neuronale Netzwerk, den erhöhten Detaillierungsgrad jedes Werts auszugleichen. Für jeden quantisierten Tensor werden während des Trainings die Werte für scale und nullPoint bestimmt.

In NNAPI definieren Sie quantisierte Tensortypen. Dazu setzen Sie das Feld „type“ der Datenstruktur ANeuralNetworksOperandType auf ANEURALNETWORKS_TENSOR_QUANT8_ASYMM. Sie geben in dieser Datenstruktur auch den scale und nullPoint-Wert des Tensors an.

Neben den asymmetrischen quantisierten 8-Bit-Tensoren unterstützt NNAPI Folgendes:

Optionale Operanden

Einige Vorgänge wie ANEURALNETWORKS_LSH_PROJECTION nehmen optionale Operanden. Um im Modell anzugeben, dass der optionale Operand weggelassen wird, rufen Sie die Funktion ANeuralNetworksModel_setOperandValue() auf und übergeben Sie NULL für den Zwischenspeicher und 0 für die Länge.

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

Tensoren mit unbekanntem Rang

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

NNAPI-Benchmark

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

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

So verwenden Sie die Benchmark:

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

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

  3. Rufen Sie das Android-Quellverzeichnis der obersten Ebene auf.

  4. Führen Sie die folgenden Befehle aus:

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

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

NNAPI-Logs

NNAPI generiert nützliche Diagnoseinformationen in den Systemprotokollen. Verwenden Sie zum Analysieren der Logs das logcat-Dienstprogramm.

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

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

Wenn Sie beispielsweise das vollständige ausführliche 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.

Nach der Aktivierung generiert das ausführliche Logging Logeinträge auf INFO-Ebene mit einem Tag, das auf den Phasen- oder Komponentennamen festgelegt ist.

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

Durchsuchen Sie die Quellstruktur mit dem folgenden Ausdruck, um eine Liste der Komponenten abzurufen:

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

Dieser Ausdruck gibt derzeit die folgenden Tags zurück:

  • BurstBuilder
  • Rückrufe
  • CompilationBuilder
  • CPUExecutor
  • AusführungsBuilder
  • ExecutionBurstController
  • ExecutionBurstServer
  • Ausführungsplan
  • Fibonacci-Treiber
  • GraphDump
  • IndexedShapeWrapper
  • IonWatcher
  • Administrator
  • Arbeitsspeicher
  • MemoryUtils
  • MetaModel
  • Modellargumentinformationen
  • Modell-Builder
  • Neuronale Netzwerke
  • OperationResolver
  • Aufgaben und Ablauf
  • Betriebsprogramme
  • Paketinformationen
  • TokenHasher
  • Typmanager
  • Dienstprogramme
  • ValidHal
  • Versionierte Schnittstellen

Um die Ebene der Logmeldungen zu steuern, die von logcat angezeigt werden, verwenden Sie die Umgebungsvariable ANDROID_LOG_TAGS.

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

Beachten Sie, dass dies nur ein Filter ist, der für logcat gilt. Sie müssen das Attribut debug.nn.vlog trotzdem auf all setzen, um ausführliche Loginformationen zu generieren.