Neural Networks API

Interfejs NNAPI (Android Neural Networks API) to interfejs API w języku C na Androida, który umożliwia wykonywanie operacji wymagających dużych zasobów obliczeniowych na potrzeby uczenia maszynowego na urządzeniach z Androidem. Interfejs NNAPI ma zapewniać podstawowy zestaw funkcji dla zaawansowanych frameworków uczenia maszynowego, takich jak TensorFlow Lite i Caffe2, które budują i trenowały sieci neuronowe. Interfejs API jest dostępny na wszystkich urządzeniach z Androidem w wersji 8.1 (poziom interfejsu API 27) lub nowszej.

NNAPI obsługuje wnioskowanie, stosując dane z urządzeń z Androidem do wcześniej wytrenowanych modeli zdefiniowanych przez dewelopera. Przykłady wnioskowania to m.in. klasyfikowanie obrazów, przewidywanie zachowań użytkowników i wybieranie odpowiednich odpowiedzi na zapytania wyszukiwania.

Wykorzystanie wnioskowania na urządzeniu ma wiele zalet:

  • Opóźnienie: nie musisz wysyłać żądania przez połączenie sieciowe i czekać na odpowiedź. Może to być na przykład bardzo ważne w przypadku aplikacji wideo, które przetwarzają kolejne klatki pochodzące z kamery.
  • Dostępność: aplikacja działa nawet poza zasięgiem sieci.
  • Szybkość: nowy sprzęt przeznaczony do przetwarzania sieci neuronowych zapewnia znacznie szybsze obliczenia niż procesor ogólnego przeznaczenia.
  • Prywatność: dane nie opuszczają urządzenia z Androidem.
  • Koszt: nie jest potrzebna farma serwerów, ponieważ wszystkie obliczenia są wykonywane na urządzeniu z Androidem.

Deweloper powinien też wziąć pod uwagę pewne kompromisy:

  • Wykorzystanie systemu: ocena sieci neuronowych wymaga wielu obliczeń, które mogą zwiększyć zużycie baterii. Jeśli stan baterii jest dla Ciebie istotny, zwłaszcza w przypadku długotrwałych obliczeń, rozważ monitorowanie stanu baterii.
  • Rozmiar aplikacji: zwróć uwagę na rozmiar swoich modeli. Modele mogą zajmować kilka megabajtów miejsca. Jeśli zgrupowanie dużych modeli w pliku APK będzie miało niekorzystny wpływ na użytkowników, rozważ pobranie modeli po zainstalowaniu aplikacji, użycie mniejszych modeli lub przeprowadzenie obliczeń w chmurze. NNAPI nie udostępnia funkcji do uruchamiania modeli w chmurze.

Aby zobaczyć przykład użycia interfejsu NNAPI, zapoznaj się z przykładem interfejsu NNAPI na Androida.

Interfejs Neural Networks API w czasie wykonywania

Interfejs NNAPI jest przeznaczony do wywoływania przez biblioteki, frameworki i narzędzia systemów uczących się, które umożliwiają deweloperom trenowanie modeli poza urządzeniem i wdrażanie ich na urządzeniach z Androidem. Aplikacje zwykle nie korzystają bezpośrednio z NNAPI, ale z ramek uczenia maszynowego wyższego poziomu. Te frameworki mogłyby z kolei używać NNAPI do wykonywania operacji wnioskowania przyspieszonego sprzętowo na obsługiwanych urządzeniach.

Na podstawie wymagań aplikacji i możliwości sprzętowych urządzenia z Androidem środowisko uruchomieniowe sieci neuronowej na Androidzie może efektywnie rozkładać obciążenie obliczeniowe na dostępne procesory na urządzeniu, w tym na dedykowany sprzęt sieci neuronowej, jednostki przetwarzania graficznego (GPU) i procesory sygnałowe (DSP).

W przypadku urządzeń z Androidem, które nie mają dedykowanego sterownika dostawcy, środowisko wykonawcze NNAPI wykonuje żądania na procesorze.

Rysunek 1 przedstawia ogólną architekturę systemu NNAPI.

Rysunek 1. Architektura systemu interfejsu Neural Networks API na Androida

Model programowania interfejsu Neural Networks API

Aby wykonywać obliczenia za pomocą NNAPI, musisz najpierw stworzyć skierowany graf, który definiuje obliczenia do wykonania. Ten graf obliczeń w połączeniu z danymi wejściowymi (np. wagami i uśrednieniami przekazywanymi z ramy uczącego się maszyny) tworzy model do oceny w czasie wykonywania NNAPI.

NNAPI korzysta z 4 głównych abstrakcji:

  • Model: wykres obliczeń operacji matematycznych i wartości stałych wyuczonych w trakcie procesu trenowania. Te operacje są specyficzne dla sieci neuronowych. Są to m.in. 2D-konwolucja, funkcja logistyczna (sigmoidalna), funkcja liniowa z wyprostowaniem (ReLU) i inne. Tworzenie modelu jest operacją synchroniczną. Po utworzeniu można go używać w różnych wątkach i kompilacjach. W NNAPI model jest reprezentowany przez instancję ANeuralNetworksModel.
  • Kompilacja: reprezentuje konfigurację kompilacji modelu NNAPI na kod niskiego poziomu. Tworzenie kompilacji jest operacją synchroniczną. Po utworzeniu można go używać ponownie w różnych wątkach i wykonaniach. W NNAPI każda kompilacja jest reprezentowana jako wystąpienie ANeuralNetworksCompilation.
  • Pamięć: reprezentuje pamięć współdzielona, pliki zmapowane na pamięć i podobne bufory pamięci. Korzystanie z bufora pamięci pozwala środowisku wykonawczemu NNAPI efektywniej przekazywać dane do sterowników. Aplikacja zwykle tworzy jeden wspólny bufor pamięci, który zawiera wszystkie tensory potrzebne do zdefiniowania modelu. Do przechowywania danych wejściowych i wyjściowych w przypadku instancji wykonania możesz też użyć buforów pamięci. W NNAPI każdy bufor pamięci jest reprezentowany jako instancja ANeuralNetworksMemory.
  • Wykonanie: interfejs do stosowania modelu NNAPI do zbioru danych wejściowych i zbierania wyników. Wykonywanie może odbywać się synchronicznie lub asynchronicznie.

    W przypadku wykonania asynchronicznego wiele wątków może czekać na to samo wykonanie. Po zakończeniu tego wykonania wszystkie wątki są zwalniane.

    W NNAPI każde wykonanie jest reprezentowane jako instancja ANeuralNetworksExecution.

Rysunek 2 przedstawia podstawowy proces programowania.

Rysunek 2. Proces programowania interfejsu Neural Networks API na Androida

W pozostałej części tej sekcji opisano czynności konfigurowania modelu NNAPI w celu wykonywania obliczeń, kompilowania modelu i uruchamiania skompilowanego modelu.

Udostępnianie danych treningowych

Trenowane wagi i dane o uprzedzeniach są prawdopodobnie przechowywane w pliku. Aby zapewnić środowisku uruchomieniowemu NNAPI skuteczny dostęp do tych danych, utwórz instancję ANeuralNetworksMemory, wywołując funkcję ANeuralNetworksMemory_createFromFd() i przekazując wskaźnik pliku otwartego pliku danych. Musisz też określić flagi ochrony pamięci i przesunięcie, w którym region pamięci współdzielonej zaczyna się w pliku.

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

W tym przykładzie używamy tylko jednej instancjiANeuralNetworksMemory dla wszystkich wag, ale w przypadku wielu plików można użyć więcej niż jednej instancjiANeuralNetworksMemory.

Używanie natywnych buforów sprzętowych

Do obsługi wejść i wyjść modelu oraz stałych wartości operandów możesz używać własnych buforów sprzętowych. W niektórych przypadkach akcelerator NNAPI może uzyskiwać dostęp do obiektów AHardwareBuffer bez konieczności kopiowania danych przez sterownik. AHardwareBuffer ma wiele różnych konfiguracji, a nie każdy akcelerator NNAPI może obsługiwać wszystkie te konfiguracje. Z tego powodu zapoznaj się z ograniczeniami wymienionymi w dokumentacji referencyjnej ANeuralNetworksMemory_createFromAHardwareBuffer i przeprowadź wcześniej testy na urządzeniach docelowych, aby mieć pewność, że kompilacje i wykonania korzystające z AHardwareBuffer działają zgodnie z oczekiwaniami. Do określenia akceleratora użyj przypisania urządzenia.

Aby umożliwić środowisku uruchomieniowemu NNAPI dostęp do obiektu AHardwareBuffer, utwórz instancję ANeuralNetworksMemory, wywołując funkcję ANeuralNetworksMemory_createFromAHardwareBuffer i przekazując obiekt AHardwareBuffer, jak pokazano w tym przykładowym kodzie:

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

Gdy NNAPI nie potrzebuje już dostępu do obiektu AHardwareBuffer, zwalniaj odpowiednie wystąpienie ANeuralNetworksMemory:

ANeuralNetworksMemory_free(mem2);

Uwaga:

  • Parametru AHardwareBuffer możesz używać tylko w przypadku całego bufora; nie możesz go używać z parametrem ARect.
  • Środowisko uruchomieniowe NNAPI nie będzie czyścić bufora. Zanim zaplanujesz wykonanie, musisz się upewnić, że bufor wejściowy i wyjściowy są dostępne.
  • Nie ma obsługi deskryptorów plików bariery synchronizacji.
  • W przypadku AHardwareBuffer z formatami i bitami użytkowania specyficznymi dla dostawcy to implementacja dostawcy decyduje, czy za czyszczenie pamięci podręcznej odpowiada klient, czy sterownik.

Model

Model jest podstawową jednostką obliczeniową w NNAPI. Każdy model jest definiowany przez co najmniej 1 operand i co najmniej 1 operację.

Operandy

Operandy to obiekty danych używane do definiowania wykresu. Obejmują one dane wejściowe i wyjściowe modelu, węzły pośrednie zawierające dane przepływające z jednej operacji do drugiej oraz stałe przekazywane do tych operacji.

Do modeli NNAPI można dodawać 2 typy operandów: skalarytensory.

Wartość skalarna reprezentuje jedną wartość. NNAPI obsługuje wartości skalarne w formatach logicznych, 16-bitowych zmiennoprzecinkowych, 32-bitowych zmiennoprzecinkowych, 32-bitowych całkowitych i niezawierających znaku 32-bitowych całkowitych.

Większość operacji w NNAPI dotyczy tensorów. Tensory to tablice n-wymiarowe. NNAPI obsługuje tensory z 16-bitowymi wartościami zmiennoprzecinkowymi, 32-bitowymi wartościami zmiennoprzecinkowymi, 8-bitowymi wartościami kwantowanymi, 16-bitowymi wartościami kwantowanymi, 32-bitowymi wartościami całkowitymi i 8-bitowymi wartościami logicznymi.

Na przykład rysunek 3 przedstawia model z 2 działaniami: dodawaniem, a potem mnożeniem. Model przyjmuje wejściowy tensor i generuje jeden wyjściowy tensor.

Rysunek 3. Przykład operandów dla modelu NNAPI

Model powyżej ma 7 operandów. Te operandy są identyfikowane domyślnie na podstawie indeksu kolejności, w jakiej są dodawane do modelu. Pierwszy operand ma indeks 0, drugi – indeks 1 itd. Operandy 1, 2, 3, 5 są operandami stałymi.

Kolejność dodawania operandów nie ma znaczenia. Na przykład operand wyjściowy modelu może być dodany jako pierwszy. Ważne jest, aby podczas odwoływania się do operandu używać prawidłowej wartości indeksu.

Operandy mają typy. Są one określane podczas dodawania ich do modelu.

Operand nie może być używany jako dane wejściowe i dane wyjściowe modelu.

Każdy operand musi być albo wejściem modelu, stałą albo operandem wyjściowym dokładnie jednego działania.

Więcej informacji o używaniu operandów znajdziesz w artykule Więcej informacji o operandach.

Zarządzanie

Operacja określa obliczenia, które mają zostać wykonane. Każda operacja składa się z tych elementów:

  • typ operacji (np. dodawanie, mnożenie, sprzężenie),
  • lista indeksów operandów, których używa operacja jako danych wejściowych;
  • lista indeksów operandów, których używa operacja do wygenerowania danych wyjściowych;

Kolejność na tych listach ma znaczenie. Informacje o oczekiwanych danych wejściowych i wyjściowych dla każdego typu operacji znajdziesz w dokumentacji NNAPI API Reference (w języku angielskim).

Przed dodaniem operacji do modelu musisz dodać do niego operandy, które operacja zużywa lub wytwarza.

Kolejność dodawania operacji nie ma znaczenia. NNAPI korzysta z zależności ustalonych przez obliczony graf operandów i operacji, aby określić kolejność wykonywania operacji.

Operacje obsługiwane przez NNAPI znajdziesz w tabeli poniżej:

Kategoria Zarządzanie
Elementarne operacje matematyczne
Manipulacja tensorami
Operacje na obrazach
Operacje wyszukiwania
Operacje normalizacji
Operacje splotu
Operacje z udziałem puli
Operacje aktywacji
Inne operacje

Znane problemy w przypadku poziomu interfejsu API 28: gdy przekazujesz tensory ANEURALNETWORKS_TENSOR_QUANT8_ASYMM do operacji ANEURALNETWORKS_PAD, która jest dostępna w Androidzie 9 (poziom interfejsu API 28) i wyższych wersjach, dane wyjściowe z NNAPI mogą nie pasować do danych wyjściowych z ramek uczenia maszynowego wyższego poziomu, takich jak TensorFlow Lite. Zamiast tego należy przekazać tylko ANEURALNETWORKS_TENSOR_FLOAT32. Problem został rozwiązany w Androidzie 10 (poziom interfejsu API 29) i nowszych.

Kompilowanie modeli

W tym przykładzie tworzymy model z 2 operacjami przedstawiony na rysunku 3.

Aby utworzyć model:

  1. Aby zdefiniować pusty model, wywołaj funkcję ANeuralNetworksModel_create().

    ANeuralNetworksModel* model = NULL;
    ANeuralNetworksModel_create(&model);
  2. Dodaj operandy do modelu, wywołując funkcję ANeuralNetworks_addOperand(). Ich typy danych są definiowane za pomocą struktury danych ANeuralNetworksOperandType.

    // 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. W przypadku operandów o stałych wartościach, takich jak wagi i uśrednione błędy, które aplikacja uzyskuje z procesu trenowania, użyj funkcji ANeuralNetworksModel_setOperandValue()ANeuralNetworksModel_setOperandValueFromMemory().

    W tym przykładzie ustawiamy stałe wartości z pliku danych do trenowania, które odpowiadają buforowi pamięci utworzonemu w sekcji Udzielanie dostępu do danych do trenowania.

    // 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. W przypadku każdej operacji w kierunkowym grafu, którą chcesz obliczyć, dodaj operację do modelu, wywołując funkcję ANeuralNetworksModel_addOperation().

    Jako parametry tego wywołania aplikacja musi podać:

    • typ operacji,
    • liczba wartości wejściowych,
    • tablica indeksów dla danych wejściowych
    • liczba wartości wyjściowych
    • tablica indeksów dla wyjściowych operandów;

    Pamiętaj, że operand nie może być używany zarówno jako argument wejściowy, jak i wyjściowy tej samej operacji.

    // 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. Określ, które operandy model powinien traktować jako dane wejściowe i wyjściowe, wywołując funkcję ANeuralNetworksModel_identifyInputsAndOutputs().

    // 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. Opcjonalnie określ, czy ANEURALNETWORKS_TENSOR_FLOAT32 może być obliczana z zakresem lub dokładnością tak niską jak w 16-bitowym formacie zmiennoprzecinkowym IEEE 754, wywołując funkcję ANeuralNetworksModel_relaxComputationFloat32toFloat16().

  7. Aby dokończyć definiowanie modelu, zadzwoń pod numer ANeuralNetworksModel_finish(). Jeśli nie ma błędów, funkcja zwraca kod wyniku ANEURALNETWORKS_NO_ERROR.

    ANeuralNetworksModel_finish(model);

Po utworzeniu modelu możesz skompilować go dowolną liczbę razy i wykonać każdą kompilację dowolną liczbę razy.

Kontrola przepływu

Aby uwzględnić sterowanie w modelu NNAPI:

  1. Utwórz odpowiednie podgrafy wykonania (podgrafy thenelse dla instrukcji IF oraz podgrafy conditionbody dla pętli WHILE) jako samodzielne modele ANeuralNetworksModel*:

    ANeuralNetworksModel* thenModel = makeThenModel();
    ANeuralNetworksModel* elseModel = makeElseModel();
  2. Utwórz operandy, które odwołują się do tych modeli w ramach modelu zawierającego przepływ sterowania:

    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. Dodaj operację przepływu sterowania:

    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);

Kompilacja

Etap kompilacji określa, na których procesorach będzie wykonywany Twój model, oraz prosi odpowiednie sterowniki o przygotowanie się do jego wykonania. Może to obejmować generowanie kodu maszynowego odpowiedniego dla procesorów, na których będzie działać model.

Aby skompilować model, wykonaj te czynności:

  1. Aby utworzyć nową instancję kompilacji, wywołaj funkcję ANeuralNetworksCompilation_create().

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

    Opcjonalnie możesz użyć przypisania urządzenia, aby wyraźnie wybrać urządzenia, na których ma być wykonywany.

  2. Opcjonalnie możesz wpływać na to, jak środowisko uruchomieniowe dba o balans między zużyciem energii baterii a szybkością wykonywania. Aby to zrobić, zadzwoń pod numer ANeuralNetworksCompilation_setPreference().

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

    Do preferencji tych należą:

  3. Opcjonalnie możesz skonfigurować buforowanie kompilacji, wywołując funkcję ANeuralNetworksCompilation_setCaching.

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

    Użyj wartości getCodeCacheDir() do cacheDir. Podany token musi być niepowtarzalny dla każdego modelu w aplikacji.

  4. Aby zakończyć definiowanie kompilacji, wywołaj funkcję ANeuralNetworksCompilation_finish(). Jeśli nie ma błędów, funkcja zwraca kod wyniku ANEURALNETWORKS_NO_ERROR.

    ANeuralNetworksCompilation_finish(compilation);

Wykrywanie i przypisywanie urządzeń

Na urządzeniach z Androidem 10 (poziom interfejsu API 29) lub nowszym interfejs NNAPI udostępnia funkcje, które umożliwiają bibliotekom i aplikacjom frameworków uczenia maszynowego uzyskiwanie informacji o dostępnych urządzeniach oraz określanie urządzeń do użycia podczas wykonywania. Podanie informacji o dostępnych urządzeniach umożliwia aplikacjom pobieranie dokładnej wersji sterowników znalezionych na urządzeniu, aby uniknąć znanych niezgodności. Dzięki możliwości określenia, na których urządzeniach mają być wykonywane poszczególne sekcje modelu, aplikacje mogą być optymalizowane pod kątem urządzenia z Androidem, na którym są wdrażane.

Wykrywanie urządzeń

Użyj parametru ANeuralNetworks_getDeviceCount, aby uzyskać liczbę dostępnych urządzeń. W przypadku każdego urządzenia użyj elementu ANeuralNetworks_getDevice, aby ustawić wystąpienie elementu ANeuralNetworksDevice jako odwołanie do tego urządzenia.

Gdy masz dane referencyjne urządzenia, możesz uzyskać o nim dodatkowe informacje, korzystając z tych funkcji:

Przypisanie urządzenia

Użyj opcji ANeuralNetworksModel_getSupportedOperationsForDevices, aby sprawdzić, które operacje modelu można wykonywać na określonych urządzeniach.

Aby określić, których akceleratorów użyć podczas wykonywania, wywołaj funkcję ANeuralNetworksCompilation_createForDevices zamiast ANeuralNetworksCompilation_create. Użyj powstałego obiektu ANeuralNetworksCompilation w zwykły sposób. Funkcja zwraca błąd, jeśli podany model zawiera operacje, które nie są obsługiwane przez wybrane urządzenia.

Jeśli podano wiele urządzeń, środowisko uruchomieniowe odpowiada za rozłożenie pracy na te urządzenia.

Podobnie jak w przypadku innych urządzeń, implementacja procesora NNAPI jest reprezentowana przez element ANeuralNetworksDevice o nazwie nnapi-reference i typie ANEURALNETWORKS_DEVICE_TYPE_CPU. Podczas wywoływania funkcji ANeuralNetworksCompilation_createForDevices implementacja procesora nie jest używana do obsługi przypadków niepowodzenia kompilacji i wykonania modelu.

Za podział modelu na modele podrzędne, które mogą działać na określonych urządzeniach, odpowiada aplikacja. Aplikacje, które nie wymagają ręcznego partycjonowania, powinny nadal wywoływać prostszą funkcję ANeuralNetworksCompilation_create, aby używać wszystkich dostępnych urządzeń (w tym procesora), aby przyspieszyć działanie modelu. Jeśli model nie jest w pełni obsługiwany przez urządzenia określone za pomocą ANeuralNetworksCompilation_createForDevices, ANEURALNETWORKS_BAD_DATAzostanie zwrócony.

Partycjonowanie modelu

Jeśli model ma dostęp do wielu urządzeń, środowisko uruchomieniowe NNAPI rozprowadza pracę na te urządzenia. Jeśli na przykład ANeuralNetworksCompilation_createForDevices zostało przypisanych więcej niż 1 urządzenie, wszystkie te urządzenia zostaną uwzględnione przy przydzielaniu pracy. Pamiętaj, że jeśli procesor nie znajduje się na liście, jego wykonywanie zostanie wyłączone. Gdy używasz ANeuralNetworksCompilation_create, wszystkie dostępne urządzenia są brane pod uwagę, w tym procesor.

Dystrybucja jest realizowana przez wybór z listy dostępnych urządzeń, dla każdego z operacji w modelu, urządzenia obsługującego operację i deklarowanie najlepszej wydajności, czyli najszybszego czasu wykonania lub najmniejszego zużycia energii, w zależności od preferencji wykonania określonych przez klienta. Ten algorytm partycjonowania nie uwzględnia możliwych nieefektywności spowodowanych przez operacje wejścia/wyjścia między różnymi procesorami, dlatego podczas określania wielu procesorów (czy to jawnie przy użyciu ANeuralNetworksCompilation_createForDevices, czy też domyślnie przy użyciu ANeuralNetworksCompilation_create) ważne jest przeprofilowanie powstałej aplikacji.

Aby dowiedzieć się, jak model został podzielony przez NNAPI, sprawdź logi Androida, szukając wiadomości (na poziomie INFO z tagiem ExecutionPlan):

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

op-name to nazwa opisowa operacji na wykresie, a device-index to indeks urządzenia docelowego na liście urządzeń. Ta lista jest wartością wejściową przekazywaną do funkcji ANeuralNetworksCompilation_createForDevices. Jeśli używasz funkcji ANeuralNetworksCompilation_createForDevices, lista urządzeń jest zwracana podczas iteracji po wszystkich urządzeniach za pomocą funkcji ANeuralNetworks_getDeviceCountANeuralNetworks_getDevice.

Komunikat (na poziomie INFO z tagiem ExecutionPlan):

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

Ta wiadomość informuje, że cały wykres został przyspieszony na urządzeniu device-name.

Realizacja

Etap wykonania stosuje model do zbioru danych wejściowych i przechowuje wyniki obliczeń w co najmniej 1 buforze użytkownika lub przestrzeni pamięci przydzielonej przez aplikację.

Aby wykonać skompilowany model:

  1. Aby utworzyć nową instancję wykonania, wywołaj funkcję ANeuralNetworksExecution_create().

    // Run the compiled model against a set of inputs
    ANeuralNetworksExecution* run1 = NULL;
    ANeuralNetworksExecution_create(compilation, &run1);
  2. Określ, gdzie aplikacja odczytuje wartości wejściowe do obliczeń. Aplikacja może odczytywać wartości wejściowe z bufora użytkownika lub z zarezerwowanego obszaru pamięci, odpowiednio wywołując funkcję ANeuralNetworksExecution_setInput() lub ANeuralNetworksExecution_setInputFromMemory().

    // 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. Określ, gdzie aplikacja ma zapisywać wartości wyjściowe. Aplikacja może zapisywać wartości wyjściowe w buforze użytkownika lub w zarezerwowanej przestrzeni pamięci, wywołując odpowiednio metodę ANeuralNetworksExecution_setOutput() lub ANeuralNetworksExecution_setOutputFromMemory().

    // Set the output
    float32 myOutput[3][4];
    ANeuralNetworksExecution_setOutput(run1, 0, NULL, myOutput, sizeof(myOutput));
  4. Zaplanuj wykonanie, wywołując funkcję ANeuralNetworksExecution_startCompute(). Jeśli nie ma błędów, funkcja zwraca kod wyniku ANEURALNETWORKS_NO_ERROR.

    // Starts the work. The work proceeds asynchronously
    ANeuralNetworksEvent* run1_end = NULL;
    ANeuralNetworksExecution_startCompute(run1, &run1_end);
  5. Aby poczekać na zakończenie wykonywania, wywołaj funkcję ANeuralNetworksEvent_wait(). Jeśli wykonanie się powiedzie, funkcja zwróci kod wyniku ANEURALNETWORKS_NO_ERROR. Czekanie może odbywać się w innym wątku niż ten, który rozpoczął wykonywanie.

    // 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. Opcjonalnie możesz zastosować do skompilowanego modelu inny zestaw danych wejściowych, używając tej samej instancji kompilacji do utworzenia nowej instancji ANeuralNetworksExecution.

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

Wykonywanie synchroniczne

Wykonanie asynchroniczne wymaga czasu na tworzenie i synchronizowanie wątków. Ponadto opóźnienie może się znacznie różnić, a najdłuższe opóźnienia mogą sięgać nawet 500 mikrosekund między momentem powiadomienia lub przebudzenia wątku a momentem, w którym zostaje ono ostatecznie przypisane do rdzenia procesora.

Aby zmniejszyć opóźnienie, możesz zamiast tego skierować aplikację do wywołania synchronicznego inferencji do środowiska uruchomieniowego. Ten wywołanie zwróci wartość dopiero po zakończeniu wnioskowania, a nie po jego rozpoczęciu. Zamiast wywoływać ANeuralNetworksExecution_startCompute aby asynchronicznie wywołać inferencję w czasie działania, aplikacja wywołuje ANeuralNetworksExecution_compute aby wywołać synchronicznie czas działania. Rozmowa z ANeuralNetworksExecution_compute nie wymaga ANeuralNetworksEvent i nie jest połączona z rozmową z ANeuralNetworksEvent_wait.

Wykonywanie burst

Na urządzeniach z Androidem 10 (poziom interfejsu API 29) lub nowszym interfejs NNAPI obsługuje wykonywanie serii za pomocą obiektu ANeuralNetworksBurst. Wykonywanie w ciągu to sekwencja wykonywania tej samej kompilacji, która występuje w szybkiej kolejności, np. w przypadku ujęć z filmowania lub kolejnych próbek audio. Korzystanie z obiektów ANeuralNetworksBurst może przyspieszyć wykonywanie kodu, ponieważ wskazuje akceleratorom, że zasoby mogą być ponownie użyte między wykonaniami i że akceleratory powinny pozostać w stanie wysokiej wydajności przez cały czas działania.

ANeuralNetworksBurst wprowadza tylko niewielką zmianę w normalnej ścieżce wykonywania. Obiekt burst tworzysz za pomocą funkcji ANeuralNetworksBurst_create, jak w tym fragmencie kodu:

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

Burstowe wykonania są synchroniczne. Zamiast jednak używać parametru ANeuralNetworksExecution_compute do wykonywania poszczególnych wnioskowań, w wywołaniach funkcji ANeuralNetworksExecution_burstCompute należy sparować różne obiekty ANeuralNetworksExecution z tym samym parametrem ANeuralNetworksBurst.

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

Uwolnij obiekt ANeuralNetworksBurst za pomocą funkcji ANeuralNetworksBurst_free, gdy nie jest już potrzebny.

// Cleanup
ANeuralNetworksBurst_free(burst);

Asynchroniczne kolejki poleceń i wyodrębne wykonywanie

W Androidzie 11 i nowszych NNAPI obsługuje dodatkowy sposób planowania wykonywania asynchronicznego za pomocą metody ANeuralNetworksExecution_startComputeWithDependencies(). Gdy używasz tej metody, przed rozpoczęciem oceny wykonanie oczekuje na sygnał wszystkich zależnych zdarzeń. Gdy wykonanie zostanie zakończone, a dane wyjściowe będą gotowe do użycia, zwrócone zdarzenie zostanie zgłoszone.

W zależności od tego, które urządzenia obsługują wykonanie, zdarzenie może być obsługiwane przez furtkę synchronizacji. Musisz wywołać funkcję ANeuralNetworksEvent_wait(), aby zaczekać na zdarzenie i odzyskać zasoby użyte podczas wykonania. Możesz importować bariery synchronizacji do obiektu zdarzenia za pomocą elementu ANeuralNetworksEvent_createFromSyncFenceFd(), a także eksportować bariery synchronizacji z obiektu zdarzenia za pomocą elementu ANeuralNetworksEvent_getSyncFenceFd().

Dane wyjściowe o dynamicznym rozmiarze

Aby obsługiwać modele, w których przypadku rozmiar danych wyjściowych zależy od danych wejściowych (czyli rozmiar nie może być określony w momencie wykonywania modelu), użyj wartości ANeuralNetworksExecution_getOutputOperandRankANeuralNetworksExecution_getOutputOperandDimensions.

Poniższy przykładowy kod pokazuje, jak to zrobić:

// 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());

Uporządkuj

Krok czyszczenia zajmuje się zwalnianiem zasobów wewnętrznych używanych do obliczeń.

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

Zarządzanie błędami i awaryjne użycie procesora

Jeśli wystąpi błąd podczas partycjonowania, jeśli skompilowanie modelu (lub jego części) przez sterownik nie powiedzie się lub jeśli sterownik nie wykona skompilowanego modelu (lub jego części), NNAPI może przejść na własną implementację operacji na procesorze.

Jeśli klient NNAPI zawiera zoptymalizowane wersje operacji (np. TFLite), korzystne może być wyłączenie korzystania z procesora w przypadku awarii i obsługa błędów za pomocą zoptymalizowanej implementacji operacji klienta.

W Androidzie 10, jeśli kompilacja jest wykonywana za pomocą ANeuralNetworksCompilation_createForDevices, awaryjne użycie procesora zostanie wyłączone.

W Androidzie P wykonanie NNAPI jest przekierowywane na procesor, jeśli wykonanie w sterowniku zakończy się niepowodzeniem. Dotyczy to też Androida 10, gdy używana jest metoda ANeuralNetworksCompilation_create zamiast ANeuralNetworksCompilation_createForDevices.

Pierwsze wykonanie jest wykonywane na tej jednej partycji, a jeśli i tak się nie powiedzie, cały model jest ponownie wykonywany na procesorze.

Jeśli podział na partycje lub kompilacja się nie powiedzie, cały model zostanie przesłany do procesora.

W niektórych przypadkach niektóre operacje nie są obsługiwane przez procesor. W takich sytuacjach kompilacja lub wykonanie nie powiedzie się, a nie zostanie użyta metoda zastępcza.

Nawet po wyłączeniu zastępczego użycia procesora w modelu mogą nadal występować operacje zaplanowane na procesorze. Jeśli procesor znajduje się na liście procesorów podanych do ANeuralNetworksCompilation_createForDevices i jest jedynym procesorem, który obsługuje te operacje, lub jest procesorem, który zapewnia najlepszą wydajność w przypadku tych operacji, zostanie wybrany jako główny (a nie zapasowy) procesor wykonawczy.

Aby mieć pewność, że nie ma wykonania na procesorze, użyj ANeuralNetworksCompilation_createForDevices, wykluczając nnapi-reference z listy urządzeń. Od Androida P można wyłączyć alternatywne wykonanie w czasie wykonywania wersji DEBUG, ustawiając właściwość debug.nn.partition na 2.

Domeny wspomnień

W Androidzie 11 i nowszych NNAPI obsługuje domeny pamięci, które udostępniają interfejsy alokacji dla nieprzezroczystych pamięci. Dzięki temu aplikacje mogą przekazywać pamięć natywnej implementacji na urządzeniu w ramach kolejnych wywołań, aby NNAPI nie kopiował ani nie przekształcał danych niepotrzebnie podczas wykonywania kolejnych wywołań na tym samym sterowniku.

Funkcja domeny pamięci jest przeznaczona dla tensorów, które są głównie wewnętrzne dla sterownika i nie wymagają częstego dostępu po stronie klienta. Przykłady takich tensorów to tensory stanu w modelach sekwencyjnych. W przypadku tensorów, które wymagają częstego dostępu do procesora po stronie klienta, użyj puli współdzielonej.

Aby przydzielić pamięć nieprzezroczystą:

  1. Aby utworzyć nowy deskryptor pamięci, wywołaj funkcję ANeuralNetworksMemoryDesc_create():

    // Create a memory descriptor
    ANeuralNetworksMemoryDesc* desc;
    ANeuralNetworksMemoryDesc_create(&desc);
  2. Określ wszystkie żądane role danych wejściowych i wyjściowych, wywołując metody ANeuralNetworksMemoryDesc_addInputRole()ANeuralNetworksMemoryDesc_addOutputRole().

    // 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. Opcjonalnie możesz określić wymiary pamięci, wywołując funkcję ANeuralNetworksMemoryDesc_setDimensions().

    // Specify the memory dimensions
    uint32_t dims[] = {3, 4};
    ANeuralNetworksMemoryDesc_setDimensions(desc, 2, dims);
  4. Aby zakończyć definiowanie opisu, wywołaj funkcję ANeuralNetworksMemoryDesc_finish().

    ANeuralNetworksMemoryDesc_finish(desc);
  5. Przypisz tyle wspomnień, ile potrzebujesz, przekazując deskryptor do funkcji ANeuralNetworksMemory_createFromDesc().

    // Allocate two opaque memories with the descriptor
    ANeuralNetworksMemory* opaqueMem;
    ANeuralNetworksMemory_createFromDesc(desc, &opaqueMem);
  6. zwalniać opis pamięci, gdy nie jest już potrzebny.

    ANeuralNetworksMemoryDesc_free(desc);

Klient może używać utworzonego obiektu ANeuralNetworksMemory tylko w stosunku do obiektu ANeuralNetworksExecution_setInputFromMemory() lub ANeuralNetworksExecution_setOutputFromMemory() zgodnie z rolami określonymi w obiekcie ANeuralNetworksMemoryDesc. Argumenty offset i length muszą być ustawione na 0, co oznacza, że używana jest cała pamięć. Klient może też jawnie ustawić lub wyodrębnić zawartość pamięci, używając ANeuralNetworksMemory_copy().

Możesz tworzyć nieprzezroczyste wspomnienia z rolami o nieokreślonych wymiarach lub rangach. W takim przypadku tworzenie pamięci może się nie udać i otrzymać stanANEURALNETWORKS_OP_FAILED, jeśli nie jest obsługiwane przez podrzędny sterownik. Zachęcamy klienta do zaimplementowania logiki zapasowej przez przydzielenie bufora o wystarczająco dużej wielkości, który jest obsługiwany przez tryb Ashmem lub BLOB.AHardwareBuffer

Gdy NNAPI nie potrzebuje już dostępu do obiektu zaciemnionej pamięci, zwolnij odpowiednią instancję ANeuralNetworksMemory:

ANeuralNetworksMemory_free(opaqueMem);

Pomiar wyników

Skuteczność aplikacji możesz ocenić, mierząc czas jej działania lub wykonując profilowanie.

Czas wykonywania

Jeśli chcesz określić łączny czas wykonania za pomocą czasu wykonywania, możesz użyć interfejsu API do wykonania synchronicznego i zmierzyć czas trwania wywołania. Jeśli chcesz określić łączny czas wykonania na niższym poziomie stosu oprogramowania, możesz użyć funkcji ANeuralNetworksExecution_setMeasureTiming i ANeuralNetworksExecution_getDuration, aby uzyskać:

  • czas wykonywania na akceleratorze (nie w sterowniku, który działa na procesorze hosta).
  • czas wykonywania w sterowniku, w tym czas przyspieszania.

Czas wykonywania w sterowniku nie uwzględnia obciążeń związanych ze środowiskiem wykonawczym ani interfejsu IPC potrzebnego do komunikacji środowiska wykonawczego ze sterownikiem.

Te interfejsy API mierzą czas między zdarzeniami przesłania i zakończenia pracy, a nie czas poświęcony przez kierowcę lub akcelerator na wykonanie wnioskowania, które może zostać przerwane przez przełączanie kontekstu.

Jeśli na przykład rozpocznie się wnioskowanie 1, a następnie sterownik zatrzyma pracę, aby wykonać wnioskowanie 2, a potem wznowi i dokończy wnioskowanie 1, czas wykonania wnioskowania 1 będzie obejmował czas, w którym praca została wstrzymana, aby wykonać wnioskowanie 2.

Te informacje o czasie mogą być przydatne w przypadku wdrożenia produkcyjnego aplikacji w celu zbierania danych telemetrycznych na potrzeby korzystania offline. Na podstawie danych o czasie możesz zmodyfikować aplikację, aby zwiększyć jej wydajność.

Podczas korzystania z tej funkcji pamiętaj o tych kwestiach:

  • Zbieranie informacji o czasie trwania może mieć wpływ na wydajność.
  • Tylko sterownik może obliczyć czas spędzony w samym sterowniku lub akceleratorze, z wyjątkiem czasu spędzonego w czasie działania NNAPI i w IPC.
  • Z tych interfejsów API możesz korzystać tylko w przypadku ANeuralNetworksExecution utworzonego za pomocą ANeuralNetworksCompilation_createForDevices z użyciem numDevices = 1.
  • Nie jest wymagana obecność kierowcy, aby zgłosić informacje o czasie.

Profilowanie aplikacji za pomocą narzędzia Android Systrace

Począwszy od Androida 10, NNAPI automatycznie generuje zdarzenia systrace, których możesz używać do profilowania aplikacji.

NNAPI Source zawiera narzędzie parse_systrace do przetwarzania zdarzeń systrace wygenerowanych przez aplikację oraz generowania widoku tabeli pokazującego czas spędzony na różnych etapach cyklu życia modelu (tworzenie, przygotowanie, kompilacja, wykonanie i zakończenie) oraz na różnych poziomach aplikacji. Aplikacja jest podzielona na te warstwy:

  • Application: główny kod aplikacji
  • Runtime: środowisko wykonawcze NNAPI
  • IPC: komunikacja między procesami NNAPI Runtime a kodem sterownika
  • Driver: proces sterownika akceleratora.

Generowanie danych analizy profilowania

Zakładając, że masz pobrany drzewo źródłowe AOSP w katalogu $ANDROID_BUILD_TOP, i że używasz przykładu klasyfikacji obrazów w TFLite jako aplikacji docelowej, możesz wygenerować dane do profilowania NNAPI, wykonując te czynności:

  1. Uruchom śledzenie systrace na Androidzie za pomocą tego polecenia:
$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

Parametr -o trace.html wskazuje, że ścieżki zostaną zapisane w pliku trace.html. Podczas profilowania własnej aplikacji musisz zastąpić org.tensorflow.lite.examples.classification nazwą procesu podaną w manifeście aplikacji.

Spowoduje to zajęcie jednej z konsol powłoki, a polecenie nie zostanie wykonane w tle, ponieważ będzie ono czekać na zakończenie procesu enter.

  1. Po uruchomieniu zbieracza systrace uruchom aplikację i przeprowadź test porównawczy.

W naszym przypadku możesz uruchomić aplikację Image Classification z Android Studio lub bezpośrednio z interfejsu testowego telefonu, jeśli aplikacja jest już zainstalowana. Aby wygenerować niektóre dane NNAPI, musisz skonfigurować aplikację do korzystania z NNAPI, wybierając NNAPI jako urządzenie docelowe w oknie konfiguracji aplikacji.

  1. Po zakończeniu testu zakończ śledzenie systrace, naciskając enter w terminalu konsoli aktywnym od kroku 1.

  2. Uruchom narzędzie systrace_parser, aby wygenerować skumulowane statystyki:

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

Parsowanie akceptuje te parametry: - --total-times: pokazuje łączny czas spędzony na warstwie, w tym czas oczekiwania na wykonanie wywołania do warstwy docelowej - --print-detail: wypisuje wszystkie zdarzenia zebrane z systrace - --per-execution: wypisuje tylko wykonanie i jego podfazy (jako czasy wykonania) zamiast statystyk dla wszystkich faz - --json: generuje dane wyjściowe w formacie JSON

Przykład danych wyjściowych:

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

Jeśli zebrane zdarzenia nie stanowią pełnego śladu aplikacji, analizator może nie działać prawidłowo. W szczególności może się to zdarzyć, jeśli w śladzie występują zdarzenia systrace służące do oznaczania końca sekcji, ale nie ma powiązanego z nimi zdarzenia rozpoczęcia sekcji. Zwykle dzieje się tak, jeśli podczas uruchamiania zbieracza systrace są generowane niektóre zdarzenia z poprzedniej sesji profilowania. W takim przypadku musisz ponownie przeprowadzić profilowanie.

Dodawanie statystyk kodu aplikacji do danych wyjściowych programu systrace_parser

Aplikacja parse_systrace korzysta z wbudowanej funkcji systrace Androida. Możesz dodawać ścieżki dla określonych operacji w aplikacji za pomocą interfejsu systrace API (w przypadku Javy lub aplikacji natywnych) z niestandardowymi nazwami zdarzeń.

Aby powiązać zdarzenia niestandardowe z etapami cyklu życia aplikacji, dołącz do nazwy zdarzenia jeden z tych ciągów znaków:

  • [NN_LA_PI]: zdarzenie na poziomie aplikacji dotyczące inicjalizacji
  • [NN_LA_PP]: zdarzenie na poziomie aplikacji dotyczące przygotowania
  • [NN_LA_PC]: zdarzenie na poziomie aplikacji dotyczące kompilacji
  • [NN_LA_PE]: zdarzenie na poziomie aplikacji dotyczące wykonania.

Oto przykład tego, jak możesz zmienić kod przykładowego przykładowego kodu klasyfikacji obrazów TFLite, dodając sekcję runInferenceModel dla fazy Execution i warstwy Application zawierającej inne sekcje preprocessBitmap, które nie będą uwzględniane w śladach NNAPI. Sekcja runInferenceModel będzie częścią zdarzeń systrace przetwarzanych przez parsownik systrace nnapi:

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;
}

Jakość usługi

W Androidzie 11 i nowszych interfejs NNAPI umożliwia uzyskanie lepszej jakości usług (QoS), ponieważ pozwala aplikacji wskazać względne priorytety swoich modeli, maksymalny czas oczekiwania na przygotowanie danego modelu oraz maksymalny czas oczekiwania na zakończenie danego obliczenia. Android 11 wprowadza też dodatkowe kody wyników NNAPI, które umożliwiają aplikacjom rozpoznawanie błędów, takich jak przekroczenie limitu czasu wykonania.

Ustawianie priorytetu zadania

Aby ustawić priorytet zadania NNAPI, przed wywołaniem funkcji ANeuralNetworksCompilation_finish() wywołaj funkcję ANeuralNetworksCompilation_setPriority().

Ustawianie terminów

W przypadku aplikacji można ustawić terminy zarówno kompilacji, jak i wykonywania wnioskowania na podstawie modelu.

Więcej informacji o operandach

W następnej sekcji omówimy zaawansowane tematy dotyczące operandów.

tensory zaokrąglone;

Kwantyzacja tensora to zwięzły sposób na reprezentowanie n-wymiarowej tablicy wartości zmiennoprzecinkowych.

NNAPI obsługuje 8-bitowe niesymetryczne tensory kwantowe. W przypadku tych tensorów wartość każdej komórki jest reprezentowana przez 8-bitową liczbę całkowitą. Z tensorem powiązana jest skala i wartość punktu zerowego. Służy on do konwertowania 8-bitowych liczb całkowitych na wartości zmiennoprzecinkowe, które są reprezentowane.

Formuła:

(cellValue - zeroPoint) * scale

gdzie wartość zeroPoint jest 32-bitową liczbą całkowitą, a wartość scale jest 32-bitową wartością zmiennoprzecinkową.

W porównaniu z tensorami o 32-bitowych wartościach zmiennoprzecinkowych 8-bitowe tensory z kwantyzacją mają 2 zalety:

  • Aplikacja jest mniejsza, ponieważ wytrenowane wagi zajmują ćwierć rozmiaru 32-bitowych tensorów.
  • Obliczenia mogą być często wykonywane szybciej. Wynika to z mniejszej ilości danych, które trzeba pobrać z pamięci, oraz z wydajności procesorów, takich jak DSP, w obliczeniach całkowitoliczbowych.

Chociaż można przekonwertować model zmiennoprzecinkowy na model kwantyzowany, nasze doświadczenie pokazuje, że lepsze wyniki uzyskuje się, trenując bezpośrednio model kwantyzowany. W efekcie sieć neuronowa uczy się kompensować zwiększoną szczegółowość poszczególnych wartości. W przypadku każdego kwantowanego tensora wartości skali i zera są określane podczas procesu trenowania.

W NNAPI typy zaokrąglonych tensorów definiujesz, ustawiając pole typu struktury danych ANeuralNetworksOperandType na ANEURALNETWORKS_TENSOR_QUANT8_ASYMM. W tej strukturze danych musisz też określić skalę i wartość zeroPoint tensora.

Oprócz 8-bitowych niesymetrycznych tensorów z kwantyzacją NNAPI obsługuje też:

Operandy opcjonalne

Niektóre operacje, takie jak ANEURALNETWORKS_LSH_PROJECTION, przyjmują opcjonalne operandy. Aby wskazać w modelu, że opcjonalny operand został pominięty, wywołaj funkcję ANeuralNetworksModel_setOperandValue(), podając wartość NULL dla bufora i 0 dla długości.

Jeśli decyzja o tym, czy operand jest obecny, zmienia się w przypadku każdego wykonania, możesz wskazać, że operand jest pominięty, używając funkcji ANeuralNetworksExecution_setInput() lub ANeuralNetworksExecution_setOutput(), przekazując NULL jako bufor i 0 jako długość.

Tensory o nieznanej rangi

Android 9 (poziom interfejsu API 28) wprowadził modele operandów o nieznanych wymiarach, ale znanej rangi (liczby wymiarów). Android 10 (poziom 29 interfejsu API) wprowadził tensory o nieznanej randze, jak pokazano w ANeuralNetworksOperandType.

Test porównawczy NNAPI

Test porównawczy NNAPI jest dostępny w AOSP w aplikacji platform/test/mlts/benchmark (aplikacja porównawcza) i platform/test/mlts/models (modele i zbiory danych).

Benchmark ocenia opóźnienie i dokładność, a także porównuje sterowniki z tymi samymi zadaniami wykonywanymi za pomocą Tensorflow Lite na procesorze w przypadku tych samych modeli i zbiorów danych.

Aby skorzystać z benchmarku:

  1. Podłącz docelowe urządzenie z Androidem do komputera, otwórz okno terminala i upewnij się, że urządzenie jest dostępne przez ADB.

  2. Jeśli połączonych jest więcej niż 1 urządzenie z Androidem, wyeksportuj zmienną środowiskowąANDROID_SERIAL urządzenia docelowego.

  3. Przejdź do najwyższego katalogu źródłowego Androida.

  4. Uruchom te polecenia:

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

    Po zakończeniu testu porównawczego jego wyniki zostaną przedstawione jako strona HTML przekazana do xdg-open.

Logi NNAPI

NNAPI generuje przydatne informacje diagnostyczne w dziennikach systemowych. Aby przeanalizować logi, użyj narzędzia logcat.

Aby włączyć szczegółowe rejestrowanie NNAPI w przypadku określonych faz lub komponentów, ustaw właściwość debug.nn.vlog (za pomocą adb shell) na podaną niżej listę wartości rozdzielonych spacjami, dwukropkami lub przecinkami:

  • model: budowanie modelu
  • compilation: generowanie planu wykonania modelu i kompilacji
  • execution: wykonanie modelu
  • cpuexe: wykonywanie operacji przy użyciu implementacji NNAPI na procesorze
  • manager: rozszerzenia NNAPI, dostępne interfejsy i informacje o funkcjach
  • all lub 1: wszystkie elementy powyżej.

Aby na przykład włączyć pełne logowanie szczegółowe, użyj polecenia adb shell setprop debug.nn.vlog all. Aby wyłączyć szczegółowe logowanie, użyj polecenia adb shell setprop debug.nn.vlog '""'.

Po włączeniu szczegółowe rejestrowanie generuje wpisy logów na poziomie INFO z tagiem ustawionym na nazwę fazy lub komponentu.

Oprócz wiadomości kontrolowanych przez debug.nn.vlog komponenty interfejsu NNAPI udostępniają inne wpisy dziennika na różnych poziomach, z których każdy używa określonego znacznika dziennika.

Aby uzyskać listę komponentów, przeszukaj drzewo źródłowe za pomocą tego wyrażenia:

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

To wyrażenie zwraca obecnie te tagi:

  • BurstBuilder
  • Wywołania zwrotne
  • CompilationBuilder
  • CpuExecutor
  • ExecutionBuilder
  • ExecutionBurstController
  • ExecutionBurstServer
  • ExecutionPlan
  • FibonacciDriver
  • GraphDump
  • IndexedShapeWrapper
  • IonWatcher
  • Menedżer
  • Pamięć
  • MemoryUtils
  • MetaModel
  • ModelArgumentInfo
  • ModelBuilder
  • NeuralNetworks
  • OperationResolver
  • Zarządzanie
  • OperationsUtils
  • PackageInfo
  • TokenHasher
  • TypeManager
  • Utils
  • ValidateHal
  • VersionedInterfaces

Aby kontrolować poziom wiadomości dziennika wyświetlanych przez logcat, użyj zmiennej środowiskowej ANDROID_LOG_TAGS.

Aby wyświetlić pełny zestaw komunikatów NNAPI i wyłączyć wszystkie inne, ustaw wartość parametru ANDROID_LOG_TAGS na:

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.

Możesz ustawić ANDROID_LOG_TAGS za pomocą tego polecenia:

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')

Pamiętaj, że jest to tylko filtr, który dotyczy logcat. Aby wygenerować obszerne informacje dziennika, musisz ustawić właściwość debug.nn.vlog na all.