Neural Networks API

Android Neural Networks API (NNAPI) to interfejs API w języku C na Androida, który umożliwia wykonywanie na urządzeniach z Androidem wymagających obliczeniowo operacji uczenia maszynowego. NNAPI ma zapewniać podstawową warstwę funkcji dla frameworków uczenia maszynowego wyższego poziomu, takich jak TensorFlow Lite i Caffe2, które tworzą i trenują sieci neuronowe. Interfejs API jest dostępny na wszystkich urządzeniach z Androidem 8.1 (interfejs API na poziomie 27) lub nowszym, ale został wycofany w Androidzie 15.

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

Wnioskowanie na urządzeniu ma wiele zalet:

  • Opóźnienie: nie musisz wysyłać żądania przez połączenie sieciowe ani czekać na odpowiedź. Może to być na przykład kluczowe 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ż sam 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ż pamiętać o pewnych kompromisach:

  • Wykorzystanie systemu: ocena sieci neuronowych wymaga wielu obliczeń, co może zwiększyć zużycie baterii. Jeśli stan baterii ma znaczenie dla Twojej aplikacji, zwłaszcza w przypadku długotrwałych obliczeń, warto go monitorować.
  • Rozmiar aplikacji: zwróć uwagę na rozmiar modeli. Modele mogą zajmować kilka megabajtów miejsca. Jeśli dołączenie dużych modeli do pliku APK miałoby negatywny wpływ na użytkowników, możesz rozważyć pobranie modeli po zainstalowaniu aplikacji, użycie mniejszych modeli lub uruchomienie obliczeń w chmurze. NNAPI nie udostępnia funkcji do uruchamiania modeli w chmurze.

Przykład użycia interfejsu NNAPI znajdziesz w przykładowym kodzie interfejsu Android Neural Networks API.

Informacje o środowisku wykonawczym interfejsu Neural Networks API

NNAPI jest przeznaczony do wywoływania przez biblioteki, platformy i narzędzia do uczenia maszynowego, 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 używają platform uczenia maszynowego wyższego poziomu. Te platformy mogą z kolei używać NNAPI do wykonywania operacji wnioskowania z akceleracją sprzętową na obsługiwanych urządzeniach.

Na podstawie wymagań aplikacji i możliwości sprzętowych urządzenia z Androidem środowisko wykonawcze sieci neuronowych Androida może skutecznie rozdzielać obciążenie obliczeniowe między dostępne procesory na urządzeniu, w tym dedykowany sprzęt sieci neuronowych, procesory graficzne (GPU) i procesory sygnałowe (DSP).

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

Rysunek 1 przedstawia architekturę systemu NNAPI na wysokim poziomie.

Rysunek 1. Architektura systemu interfejsu Android Neural Networks API

Model programowania interfejsu Neural Networks API

Aby wykonywać obliczenia za pomocą NNAPI, musisz najpierw utworzyć graf skierowany, który definiuje obliczenia do wykonania. Ten wykres obliczeniowy w połączeniu z danymi wejściowymi (np. wagami i odchyleniami przekazywanymi z platformy uczenia maszynowego) tworzy model do oceny w środowisku wykonawczym NNAPI.

NNAPI korzysta z 4 głównych abstrakcji:

  • Model: graf obliczeniowy operacji matematycznych i stałych wartości wyuczonych w procesie trenowania. Te operacje są specyficzne dla sieci neuronowych. Są to m.in. konwolucja 2D, aktywacja logistyczna (sigmoid), aktywacja liniowa z prostownikiem (ReLU) i inne. Tworzenie modelu to operacja synchroniczna. Po utworzeniu można go używać w różnych wątkach i kompilacjach. W NNAPI model jest reprezentowany jako instancja ANeuralNetworksModel.
  • Kompilacja: reprezentuje konfigurację kompilowania modelu NNAPI do kodu niższego 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 instancja ANeuralNetworksCompilation.
  • Pamięć: reprezentuje pamięć współdzieloną, pliki mapowane w pamięci i podobne bufory pamięci. Korzystanie z bufora pamięci umożliwia środowisku wykonawczemu NNAPI wydajniejsze przesyłanie danych do sterowników. Aplikacja zwykle tworzy jeden bufor pamięci współdzielonej, który zawiera wszystkie tensory potrzebne do zdefiniowania modelu. Możesz też używać buforów pamięci do przechowywania danych wejściowych i wyjściowych instancji wykonania. W NNAPI każdy bufor pamięci jest reprezentowany jako instancja ANeuralNetworksMemory.
  • Wykonanie: interfejs do stosowania modelu NNAPI do zestawu danych wejściowych i zbierania wyników. Wykonanie może być synchroniczne lub asynchroniczne.

    W przypadku wykonywania asynchronicznego wiele wątków może oczekiwać na to samo wykonanie. Po zakończeniu tego wykonania wszystkie wątki zostaną zwolnione.

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

Rysunek 2 przedstawia podstawowy proces programowania.

Rysunek 2. Schemat programowania interfejsu Android Neural Networks API

W pozostałej części tej sekcji opisujemy, jak skonfigurować model NNAPI do wykonywania obliczeń, skompilować go i uruchomić.

Udostępnianie danych treningowych

Dane wytrenowanych wag i odchyleń są prawdopodobnie przechowywane w pliku. Aby zapewnić środowisku wykonawczemu NNAPI wydajny dostęp do tych danych, utwórz instancję ANeuralNetworksMemory, wywołując funkcję ANeuralNetworksMemory_createFromFd() i przekazując deskryptor pliku otwartego pliku danych. Możesz też określić flagi ochrony pamięci i przesunięcie, od którego zaczyna się obszar pamięci współdzielonej 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 instancji ANeuralNetworksMemory dla wszystkich wag, ale można użyć więcej niż jednej instancji ANeuralNetworksMemory dla wielu plików.

Używanie natywnych buforów sprzętowych

W przypadku danych wejściowych i wyjściowych modelu oraz stałych wartości operandów możesz używać natywnych 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 i nie każdy akcelerator NNAPI może obsługiwać wszystkie te konfiguracje. Ze względu na to ograniczenie zapoznaj się z ograniczeniami wymienionymi w ANeuralNetworksMemory_createFromAHardwareBufferdokumentacji referencyjnej i przeprowadź testy na urządzeniach docelowych, aby upewnić się, że kompilacje i wykonania, które korzystają z AHardwareBuffer, działają zgodnie z oczekiwaniami. Użyj przypisywania urządzeń, aby określić akcelerator.

Aby umożliwić środowisku wykonawczemu 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, zwolnij odpowiednią instancję ANeuralNetworksMemory:

ANeuralNetworksMemory_free(mem2);

Uwaga:

  • Możesz używać parametru AHardwareBuffer tylko w przypadku całego bufora. Nie możesz go używać z parametrem ARect.
  • Środowisko wykonawcze NNAPI nie opróżni bufora. Przed zaplanowaniem wykonania musisz się upewnić, że bufory wejściowe i wyjściowe są dostępne.
  • Deskryptory plików bariery synchronizacji nie są obsługiwane.
  • W przypadku AHardwareBuffer z formatami i bitami użycia specyficznymi dla dostawcy to implementacja dostawcy określa, czy za opróżnianie pamięci podręcznej odpowiada klient czy sterownik.

Model

Model to podstawowa jednostka obliczeniowa w NNAPI. Każdy model jest definiowany przez co najmniej 1 operand i 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.

Skalar reprezentuje pojedynczą wartość. NNAPI obsługuje wartości skalarne w formatach logicznym, 16-bitowym zmiennoprzecinkowym, 32-bitowym zmiennoprzecinkowym, 32-bitowym całkowitym i 32-bitowym całkowitym bez znaku.

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

Na przykład rysunek 3 przedstawia model z 2 operacjami: dodawaniem, a następnie mnożeniem. Model przyjmuje tensor wejściowy i generuje 1 tensor wyjściowy.

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

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

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

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

Operandu nie można używać jednocześnie jako danych wejściowych i wyjściowych modelu.

Każdy operand musi być danymi wejściowymi modelu, stałą lub operandem wyjściowym dokładnie jednej operacji.

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, splot),
  • lista indeksów operandów, których operacja używa jako danych wejściowych,
  • listę indeksów operandów, których operacja używa do danych wyjściowych.

Kolejność na tych listach ma znaczenie. Oczekiwane dane wejściowe i wyjściowe każdego typu operacji znajdziesz w dokumentacji interfejsu NNAPI.

Przed dodaniem operacji musisz dodać do modelu operandy, które są przez nią używane lub generowane.

Kolejność dodawania operacji nie ma znaczenia. NNAPI opiera się na zależnościach określonych przez graf obliczeniowy operandów i operacji, aby określić kolejność wykonywania operacji.

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

Kategoria Zarządzanie
Operacje matematyczne na poszczególnych elementach
Manipulacja tensorami
Operacje na obrazach
Operacje wyszukiwania
Operacje normalizacji
Operacje splotu
Operacje pulowania
Operacje aktywacji
Inne operacje

Znany problem na poziomie interfejsu API 28: podczas przekazywania tensorówANEURALNETWORKS_TENSOR_QUANT8_ASYMMdo operacjiANEURALNETWORKS_PAD, która jest dostępna w Androidzie 9 (poziom interfejsu API 28) i nowszych wersjach, dane wyjściowe z NNAPI mogą nie pasować do danych wyjściowych z platform uczenia maszynowego wyższego poziomu, takich jak TensorFlow Lite. Zamiast tego przekaż tylko ANEURALNETWORKS_TENSOR_FLOAT32. Problem został rozwiązany w Androidzie 10 (poziom 29 interfejsu API) i nowszych wersjach.

Tworzenie modeli

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

Aby zbudować model, wykonaj te czynności:

  1. Wywołaj funkcję ANeuralNetworksModel_create() , aby zdefiniować pusty model.

    ANeuralNetworksModel* model = NULL;
    ANeuralNetworksModel_create(&model);
  2. Dodaj operandy do modelu, wywołując funkcję ANeuralNetworks_addOperand(). 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 odchylenia, które aplikacja uzyskuje w procesie trenowania, użyj funkcji ANeuralNetworksModel_setOperandValue()ANeuralNetworksModel_setOperandValueFromMemory().

    W tym przykładzie ustawiamy stałe wartości z pliku danych do trenowania odpowiadające 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 grafie skierowanym, którą chcesz obliczyć, dodaj ją 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 operandów wejściowych,
    • liczba wartości wyjściowych,
    • tablica indeksów operandów wyjściowych,

    Pamiętaj, że operandu nie można używać zarówno jako danych wejściowych, jak i wyjściowych 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 ma 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 możesz określić, czy wartość ANEURALNETWORKS_TENSOR_FLOAT32 może być obliczana z zakresem lub precyzją tak niską jak w przypadku 16-bitowego formatu zmiennoprzecinkowego IEEE 754, wywołując funkcję ANeuralNetworksModel_relaxComputationFloat32toFloat16().

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

    ANeuralNetworksModel_finish(model);

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

Kontrola przepływu

Aby uwzględnić przepływ sterowania w modelu NNAPI:

  1. Utwórz odpowiednie podgrafy wykonania (podgrafy thenelse dla instrukcji IF, 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 modelu zawierającym 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 model, i prosi odpowiednie sterowniki o przygotowanie się do jego wykonania. Może to obejmować generowanie kodu maszynowego dostosowanego do procesorów, na których będzie działać model.

Aby skompilować model, wykonaj te czynności:

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

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

    Opcjonalnie możesz użyć przypisywania urządzeń, aby wyraźnie wybrać urządzenia, na których ma być wykonywane działanie.

  2. Opcjonalnie możesz wpływać na to, jak środowisko wykonawcze będzie równoważyć zużycie baterii i szybkość wykonywania. Możesz to zrobić, dzwoniąc pod numer ANeuralNetworksCompilation_setPreference().

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

    Możesz określić te preferencje:

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

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

    Użyj getCodeCacheDir() w przypadku cacheDir. Wartość token musi być unikalna dla każdego modelu w aplikacji.

  4. Zakończ definiowanie kompilacji, wywołując 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 platformy uczenia maszynowego uzyskiwanie informacji o dostępnych urządzeniach i określanie urządzeń, które mają być używane do wykonywania zadań. Podawanie informacji o dostępnych urządzeniach umożliwia aplikacjom uzyskanie dokładnej wersji sterowników znajdujących się na urządzeniu, aby uniknąć znanych niezgodności. Dzięki możliwości określania przez aplikacje, które urządzenia mają wykonywać różne sekcje modelu, można je zoptymalizować pod kątem urządzenia z Androidem, na którym są wdrażane.

Wykrywanie urządzeń

Użyj polecenia ANeuralNetworks_getDeviceCount , aby uzyskać liczbę dostępnych urządzeń. W przypadku każdego urządzenia użyj ANeuralNetworks_getDevice aby ustawić instancję ANeuralNetworksDevice na odwołanie do tego urządzenia.

Gdy uzyskasz odniesienie do urządzenia, możesz znaleźć dodatkowe informacje o nim, korzystając z tych funkcji:

Przypisanie urządzenia

Użyj ANeuralNetworksModel_getSupportedOperationsForDevices aby sprawdzić, które operacje modelu można uruchomić na konkretnych urządzeniach.

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

Jeśli określono kilka urządzeń, środowisko wykonawcze odpowiada za rozdzielenie pracy między nimi.

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

Obowiązkiem aplikacji jest podzielenie modelu na podmodele, które mogą działać na określonych urządzeniach. Aplikacje, które nie wymagają ręcznego dzielenia na partycje, powinny nadal wywoływać prostszą funkcję ANeuralNetworksCompilation_create, aby używać wszystkich dostępnych urządzeń (w tym procesora) do przyspieszania modelu. Jeśli model nie może być w pełni obsługiwany przez urządzenia określone za pomocą ANeuralNetworksCompilation_createForDevices, zwracana jest wartość ANEURALNETWORKS_BAD_DATA.

Partycjonowanie modelu

Gdy model jest dostępny na wielu urządzeniach, środowisko wykonawcze NNAPI rozdziela pracę między te urządzenia. Jeśli na przykład użytkownikowi ANeuralNetworksCompilation_createForDevices udostępniono więcej niż 1 urządzenie, podczas przydzielania pracy będą brane pod uwagę wszystkie określone urządzenia. Pamiętaj, że jeśli urządzenia CPU nie ma na liście, wykonywanie na procesorze będzie wyłączone. Podczas korzystania z ANeuralNetworksCompilation_create będą brane pod uwagę wszystkie dostępne urządzenia, w tym procesor.

Dystrybucja odbywa się przez wybranie z listy dostępnych urządzeń dla każdej operacji w modelu urządzenia obsługującego tę operację i deklarującego najlepszą wydajność, czyli najkrótszy czas wykonania lub najniższe zużycie energii, w zależności od preferencji wykonania określonych przez klienta. Ten algorytm podziału 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 (jawnie za pomocą ANeuralNetworksCompilation_createForDevices lub niejawnie za pomocą ANeuralNetworksCompilation_create) ważne jest profilowanie wynikowej aplikacji.

Aby dowiedzieć się, jak model został podzielony przez NNAPI, sprawdź logi Androida pod kątem komunikatu (na poziomie INFO z tagiem ExecutionPlan):

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

op-name to opisowa nazwa operacji na wykresie, a device-index to indeks urządzenia kandydującego na liście urządzeń. Ta lista jest danymi wejściowymi przekazywanymi do funkcji ANeuralNetworksCompilation_createForDevices lub, w przypadku korzystania z funkcji ANeuralNetworksCompilation_createForDevices, listą urządzeń zwracaną podczas iteracji po wszystkich urządzeniach za pomocą funkcji ANeuralNetworks_getDeviceCountANeuralNetworks_getDevice.

Wiadomość (na poziomie INFO z tagiem ExecutionPlan):

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

Ten komunikat oznacza, że cały wykres został przyspieszony na urządzeniudevice-name.

Realizacja

Krok wykonania stosuje model do zestawu danych wejściowych i zapisuje wyniki obliczeń w co najmniej jednym buforze użytkownika lub obszarze pamięci przydzielonym przez aplikację.

Aby wykonać skompilowany model, wykonaj te czynności:

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

    // Run the compiled model against a set of inputs
    ANeuralNetworksExecution* run1 = NULL;
    ANeuralNetworksExecution_create(compilation, &run1);
  2. Określ, skąd aplikacja odczytuje wartości wejściowe do obliczeń. Aplikacja może odczytywać wartości wejściowe z bufora użytkownika lub z przydzielonego obszaru pamięci, wywołując odpowiednio funkcje 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 zapisuje wartości wyjściowe. Aplikacja może zapisywać wartości wyjściowe w buforze użytkownika lub w przydzielonym obszarze pamięci, wywołując odpowiednio funkcje ANeuralNetworksExecution_setOutput() lub ANeuralNetworksExecution_setOutputFromMemory().

    // Set the output
    float32 myOutput[3][4];
    ANeuralNetworksExecution_setOutput(run1, 0, NULL, myOutput, sizeof(myOutput));
  4. Zaplanuj rozpoczęcie wykonania, 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. Wywołaj funkcję ANeuralNetworksEvent_wait(), aby poczekać na zakończenie wykonywania. Jeśli wykonanie się powiodło, funkcja zwraca kod wyniku ANEURALNETWORKS_NO_ERROR. Oczekiwanie może odbywać się w innym wątku niż ten, który rozpoczyna wykonanie.

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

Wykonanie synchroniczne

Wykonanie asynchroniczne wymaga czasu na utworzenie i zsynchronizowanie wątków. Co więcej, czas oczekiwania może być bardzo różny, a najdłuższe opóźnienia sięgają nawet 500 mikrosekund między momentem powiadomienia lub wybudzenia wątku a momentem jego powiązania z rdzeniem procesora.

Aby zmniejszyć opóźnienie, możesz zamiast tego skierować aplikację do wykonania synchronicznego wywołania wnioskowania do środowiska wykonawczego. Wywołanie zostanie zwrócone dopiero po zakończeniu wnioskowania, a nie po jego rozpoczęciu. Zamiast wywoływać ANeuralNetworksExecution_startCompute asynchroniczne wywołanie wnioskowania w środowisku wykonawczym, aplikacja wywołuje ANeuralNetworksExecution_compute synchroniczne wywołanie w środowisku wykonawczym. Połączenie z numerem ANeuralNetworksExecution_compute nie korzysta z ANeuralNetworksEvent i nie jest sparowane z połączeniem z numerem ANeuralNetworksEvent_wait.

Uruchomienia pakietowe

Na urządzeniach z Androidem 10 (poziom interfejsu API 29) i nowszym interfejs NNAPI obsługuje wykonywanie pakietowe za pomocą obiektu ANeuralNetworksBurst. Wykonywanie w seriach to sekwencja wykonań tej samej kompilacji, która następuje szybko po sobie, np. w przypadku klatek przechwytywanych przez kamerę lub kolejnych próbek audio. Używanie obiektów ANeuralNetworksBurst może przyspieszyć wykonywanie, ponieważ wskazuje akceleratorom, że zasoby mogą być ponownie wykorzystywane między wykonaniami i że akceleratory powinny pozostawać w stanie wysokiej wydajności przez cały czas trwania serii.

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

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

Wykonania seryjne są synchroniczne. Zamiast jednak używać ANeuralNetworksExecution_compute do każdego wnioskowania, łącz różne obiekty ANeuralNetworksExecution z tym samym ANeuralNetworksBurst w wywołaniach funkcji 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
// ...

Zwolnij obiekt ANeuralNetworksBurst za pomocą funkcji ANeuralNetworksBurst_free gdy nie będzie już potrzebny.

// Cleanup
ANeuralNetworksBurst_free(burst);

Asynchroniczne kolejki poleceń i wykonywanie w izolacji

W Androidzie 11 i nowszych interfejs NNAPI obsługuje dodatkowy sposób planowania wykonania asynchronicznego za pomocą metody ANeuralNetworksExecution_startComputeWithDependencies(). Gdy używasz tej metody, wykonanie czeka na sygnał ze wszystkich zdarzeń zależnych, zanim rozpocznie ocenę. Po zakończeniu wykonania i przygotowaniu danych wyjściowych do użycia zwracane zdarzenie jest sygnalizowane.

W zależności od tego, które urządzenia obsługują wykonanie, zdarzenie może być obsługiwane przez barierę synchronizacji. Aby poczekać na zdarzenie i odzyskać zasoby użyte przez wykonanie, musisz wywołać funkcję ANeuralNetworksEvent_wait(). Możesz importować bariery synchronizacji do obiektu zdarzenia za pomocą funkcji ANeuralNetworksEvent_createFromSyncFenceFd() i eksportować je z obiektu zdarzenia za pomocą funkcji ANeuralNetworksEvent_getSyncFenceFd().

Dane wyjściowe o dynamicznym rozmiarze

Aby obsługiwać modele, w których rozmiar danych wyjściowych zależy od danych wejściowych, czyli w których rozmiaru nie można określić w momencie wykonywania modelu, użyj 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 odpowiada za zwalnianie zasobów wewnętrznych używanych do obliczeń.

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

Zarządzanie błędami i przełączanie na procesor

Jeśli podczas dzielenia na partycje wystąpi błąd, sterownik nie skompiluje (fragmentu) modelu lub nie wykona skompilowanego (fragmentu) modelu, interfejs NNAPI może wrócić do własnej implementacji na procesorze co najmniej jednej operacji.

Jeśli klient NNAPI zawiera zoptymalizowane wersje operacji (np. TFLite), może być korzystne wyłączenie rezerwowego działania na procesorze i obsługa błędów za pomocą zoptymalizowanej implementacji operacji klienta.

W Androidzie 10, jeśli kompilacja jest przeprowadzana za pomocą atrybutu ANeuralNetworksCompilation_createForDevices, to rezerwowe użycie procesora będzie wyłączone.

W Androidzie P wykonanie NNAPI jest przywracane do procesora, jeśli wykonanie na sterowniku się nie powiedzie. Dotyczy to też Androida 10, gdy używana jest wartość ANeuralNetworksCompilation_create zamiast ANeuralNetworksCompilation_createForDevices.

Pierwsze wykonanie w przypadku tej partycji kończy się niepowodzeniem, a jeśli nadal się nie powiedzie, ponawia próbę wykonania całego modelu na procesorze.

Jeśli podział lub kompilacja się nie powiedzie, cały model zostanie wypróbowany na procesorze.

W niektórych przypadkach niektóre operacje nie są obsługiwane na procesorze. W takich sytuacjach kompilacja lub wykonanie zakończy się niepowodzeniem, zamiast powrócić do poprzedniego stanu.

Nawet po wyłączeniu rezerwowego procesora CPU w modelu mogą nadal występować operacje zaplanowane na procesorze CPU. Jeśli procesor znajduje się na liście procesorów dostarczonych do ANeuralNetworksCompilation_createForDevices i jest jedynym procesorem obsługującym te operacje lub procesorem, który zapewnia najlepszą wydajność w przypadku tych operacji, zostanie wybrany jako główny (nieawaryjny) wykonawca.

Aby mieć pewność, że nie będzie wykonywane żadne działanie procesora, użyj ANeuralNetworksCompilation_createForDevices, wykluczając nnapi-reference z listy urządzeń. Od Androida P w wersjach DEBUG można wyłączyć rezerwę w czasie wykonywania, ustawiając wartość właściwości debug.nn.partition na 2.

Domeny pamięci

W Androidzie 11 i nowszych interfejs NNAPI obsługuje domeny pamięci, które udostępniają interfejsy alokatora dla nieprzezroczystych pamięci. Dzięki temu aplikacje mogą przekazywać pamięci natywne dla urządzenia między wykonaniami, więc NNAPI nie musi niepotrzebnie kopiować ani przekształcać danych podczas kolejnych wykonań na tym samym sterowniku.

Funkcja domeny pamięci jest przeznaczona dla tensorów, które są w większości wewnętrzne dla sterownika i nie wymagają częstego dostępu po stronie klienta. Przykładami takich tensorów są tensory stanu w modelach sekwencyjnych. W przypadku tensorów, które wymagają częstego dostępu do procesora po stronie klienta, używaj zamiast tego pul pamięci współdzielonej.

Aby przydzielić pamięć nieprzezroczystą, wykonaj te czynności:

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

    // Create a memory descriptor
    ANeuralNetworksMemoryDesc* desc;
    ANeuralNetworksMemoryDesc_create(&desc);
  2. Określ wszystkie zamierzone role wejściowe i wyjściowe, wywołując ANeuralNetworksMemoryDesc_addInputRole() i 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. Zakończ definiowanie deskryptora, wywołując ANeuralNetworksMemoryDesc_finish().

    ANeuralNetworksMemoryDesc_finish(desc);
  5. Przydziel dowolną liczbę pamięci, przekazując deskryptor do funkcji ANeuralNetworksMemory_createFromDesc().

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

    ANeuralNetworksMemoryDesc_free(desc);

Klient może używać utworzonego obiektu ANeuralNetworksMemory tylko z obiektami 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 za pomocą ANeuralNetworksMemory_copy().

Możesz tworzyć nieprzejrzyste wspomnienia z rolami o nieokreślonych wymiarach lub randze. W takim przypadku utworzenie pamięci może się nie udać i zwrócić stan ANEURALNETWORKS_OP_FAILED, jeśli nie jest ona obsługiwana przez sterownik bazowy. Zalecamy klientowi wdrożenie logiki rezerwowej przez przydzielenie wystarczająco dużego bufora obsługiwanego przez Ashmem lub BLOB-mode AHardwareBuffer.

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

ANeuralNetworksMemory_free(opaqueMem);

Pomiar wyników

Skuteczność aplikacji możesz ocenić, mierząc czas wykonania lub profilując.

Czas wykonywania

Jeśli chcesz określić całkowity czas wykonania w czasie działania, możesz użyć synchronicznego interfejsu API wykonania i zmierzyć czas potrzebny na wywołanie. Jeśli chcesz określić łączny czas wykonania na niższym poziomie stosu oprogramowania, możesz użyć funkcji ANeuralNetworksExecution_setMeasureTimingANeuralNetworksExecution_getDuration, aby uzyskać:

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

Czas wykonania w sterowniku nie obejmuje narzutu, np. narzutu samego środowiska wykonawczego i komunikacji międzyprocesowej potrzebnej do komunikacji środowiska wykonawczego ze sterownikiem.

Te interfejsy API mierzą czas między przesłaniem zadania a jego ukończeniem, a nie czas, jaki sterownik lub akcelerator poświęca na przeprowadzenie wnioskowania, które może być przerywane przez przełączanie kontekstu.

Jeśli na przykład rozpocznie się wnioskowanie 1, a następnie sterownik przerwie pracę, aby przeprowadzić wnioskowanie 2, po czym wznowi i zakończy wnioskowanie 1, czas wykonania wnioskowania 1 będzie obejmował czas, w którym praca została przerwana na potrzeby wnioskowania 2.

Te informacje o czasie mogą być przydatne w przypadku wdrożenia produkcyjnego aplikacji do zbierania danych telemetrycznych na potrzeby korzystania z niej w trybie offline. Dane o czasie możesz wykorzystać do zmodyfikowania aplikacji w celu zwiększenia jej wydajności.

Korzystając z tej funkcji, pamiętaj o tych kwestiach:

  • Zbieranie informacji o czasie może wiązać się z kosztami związanymi z wydajnością.
  • Tylko sterownik może obliczyć czas spędzony w nim samym lub na akceleratorze, z wyłączeniem czasu spędzonego w środowisku wykonawczym NNAPI i w IPC.
  • Tych interfejsów API możesz używać tylko w przypadku ANeuralNetworksExecution utworzonych za pomocą ANeuralNetworksCompilation_createForDevicesnumDevices = 1.
  • Aby zgłaszać informacje o czasie, kierowca nie musi być zalogowany.

Profilowanie aplikacji za pomocą narzędzia Android Systrace

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

Źródło NNAPI zawiera narzędzie parse_systrace do przetwarzania zdarzeń systrace generowanych przez aplikację i tworzenia widoku tabeli pokazującego czas spędzony w różnych fazach cyklu życia modelu (tworzenie instancji, przygotowanie, wykonanie kompilacji i zakończenie) oraz w różnych warstwach aplikacji. Aplikacja jest podzielona na te warstwy:

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

Generowanie danych analizy profilowania

Załóżmy, że drzewo źródłowe AOSP zostało pobrane do katalogu $ANDROID_BUILD_TOP, a jako aplikację docelową wybrano przykład klasyfikacji obrazów TFLite. Dane profilowania NNAPI można wygenerować, wykonując te czynności:

  1. Uruchom śledzenie systemu Android 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 ślady będą zapisywane w pliku trace.html. Podczas profilowania własnej aplikacji musisz zastąpić symbol org.tensorflow.lite.examples.classification nazwą procesu podaną w manifeście aplikacji.

Spowoduje to zajęcie jednej z konsol powłoki. Nie uruchamiaj polecenia w tle, ponieważ interaktywnie czeka ono na zakończenie działania enter.

  1. Po uruchomieniu narzędzia do zbierania danych 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 została 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 systemowe, naciskając enter w terminalu konsoli, który jest aktywny od kroku 1.

  2. Uruchom narzędzie systrace_parser generate cumulative statistics:

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

Parser akceptuje te parametry: - --total-times: pokazuje łączny czas spędzony w warstwie, w tym czas oczekiwania na wykonanie wywołania w warstwie bazowej. - --print-detail: drukuje wszystkie zdarzenia zebrane z systrace. - --per-execution: drukuje tylko wykonanie i jego podfazy (jako czasy wykonania) zamiast statystyk 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. Może się to zdarzyć w szczególności wtedy, gdy w śladzie występują zdarzenia systrace wygenerowane w celu oznaczenia końca sekcji, ale nie ma powiązanego z nimi zdarzenia oznaczającego początek sekcji. Zwykle dzieje się tak, gdy podczas uruchamiania narzędzia do zbierania śladów systemowych generowane są zdarzenia z poprzedniej sesji profilowania. W takim przypadku musisz ponownie przeprowadzić profilowanie.

Dodawanie statystyk kodu aplikacji do danych wyjściowych narzędzia systrace_parser

Aplikacja parse_systrace jest oparta na wbudowanej funkcji systrace Androida. Ślady dotyczące konkretnych operacji w aplikacji możesz dodawać za pomocą interfejsu systrace API (w przypadku Javy, w przypadku aplikacji natywnych) z niestandardowymi nazwami zdarzeń.

Aby powiązać zdarzenia niestandardowe z fazami cyklu życia aplikacji, dodaj na początku nazwy zdarzenia jeden z tych ciągów znaków:

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

Oto przykład, jak zmodyfikować kod przykładu klasyfikacji obrazów TFLite, dodając sekcję runInferenceModel dla fazy Execution i warstwę Application zawierającą inne sekcje preprocessBitmap, które nie będą uwzględniane w śladach NNAPI. Sekcja runInferenceModel będzie częścią zdarzeń systrace przetwarzanych przez analizator 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 wersjach interfejs NNAPI umożliwia lepszą jakość usług (QoS), ponieważ pozwala aplikacji określać względne priorytety modeli, maksymalny czas potrzebny na przygotowanie danego modelu i maksymalny czas potrzebny na wykonanie danego obliczenia. Android 11 wprowadza też dodatkowe kody wyników NNAPI, które umożliwiają aplikacjom zrozumienie przyczyn niepowodzeń, takich jak niedotrzymanie terminów wykonania.

Ustawianie priorytetu zadania

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

Ustawianie terminów

Aplikacje mogą ustawiać terminy zarówno kompilacji modelu, jak i wnioskowania.

Więcej informacji o operandach

W następnej sekcji znajdziesz zaawansowane informacje o używaniu operandów.

Skwantyzowane tensory

Skwantowany tensor to zwarty sposób reprezentowania n-wymiarowej tablicy wartości zmiennoprzecinkowych.

NNAPI obsługuje 8-bitowe asymetryczne tensory skwantyzowane. W przypadku tych tensorów wartość każdej komórki jest reprezentowana przez 8-bitową liczbę całkowitą. Z tenzorem powiązane są wartość skali i punktu zerowego. Służą one do przekształcania 8-bitowych liczb całkowitych na reprezentowane wartości zmiennoprzecinkowe.

Wzór to:

(cellValue - zeroPoint) * scale

gdzie zeroPoint to 32-bitowa liczba całkowita, a scale to 32-bitowa liczba zmiennoprzecinkowa.

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

  • Aplikacja jest mniejsza, ponieważ wytrenowane wagi zajmują 1/4 rozmiaru tensorów 32-bitowych.
  • Obliczenia można często wykonywać 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 wykonywaniu obliczeń na liczbach całkowitych.

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

W NNAPI typy skwantowanych tensorów definiuje się, ustawiając pole typu struktury danych ANeuralNetworksOperandType na ANEURALNETWORKS_TENSOR_QUANT8_ASYMM. W tej strukturze danych określasz też skalę i wartość zeroPoint tensora.

Oprócz 8-bitowych asymetrycznych tensorów skwantyzowanych interfejs NNAPI obsługuje te elementy:

Operandy opcjonalne

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

Jeśli decyzja o tym, czy operand jest obecny, różni się w przypadku każdego wykonania, możesz wskazać, że operand jest pomijany, używając funkcji ANeuralNetworksExecution_setInput() lub ANeuralNetworksExecution_setOutput(), przekazując NULL dla bufora i 0 dla długości.

Tensory o nieznanej randze

W Androidzie 9 (poziom 28 interfejsu API) wprowadzono operandy modelu o nieznanych wymiarach, ale znanym rankingu (liczbie wymiarów). W Androidzie 10 (poziom 29 interfejsu API) wprowadzono tensory o nieznanej randze, co pokazano w ANeuralNetworksOperandType.

Test porównawczy NNAPI

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

Test porównawczy ocenia opóźnienie i dokładność oraz porównuje sterowniki z tą samą pracą wykonaną przy użyciu TensorFlow Lite działającego na procesorze w przypadku tych samych modeli i zbiorów danych.

Aby skorzystać z analizy porównawczej:

  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ż jedno urządzenie z Androidem, wyeksportuj zmienną środowiskową urządzenia docelowego ANDROID_SERIAL.

  3. Otwórz katalog źródłowy najwyższego poziomu 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. Do analizowania logów używaj narzędzia logcat.

Włącz szczegółowe logowanie NNAPI w przypadku określonych faz lub komponentów, ustawiając właściwość debug.nn.vlog (za pomocą adb shell) na tę listę wartości rozdzielonych spacją, dwukropkiem lub przecinkiem:

  • model: Budowanie modelu
  • compilation: generowanie planu wykonania modelu i jego kompilacja;
  • execution: wykonywanie modelu
  • cpuexe: wykonywanie operacji przy użyciu implementacji procesora NNAPI;
  • manager: informacje o rozszerzeniach NNAPI, dostępnych interfejsach i funkcjach;
  • all lub 1: wszystkie powyższe elementy

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

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

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

Aby uzyskać listę komponentów, wyszukaj w drzewie źródłowym 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 komunikatów dziennika wyświetlanych przez logcat, użyj zmiennej środowiskowej ANDROID_LOG_TAGS.

Aby wyświetlić pełny zestaw komunikatów dziennika NNAPI i wyłączyć wszystkie inne, ustaw 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.

Wartość ANDROID_LOG_TAGS możesz ustawić 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 ma zastosowanie do logcat. Aby wygenerować szczegółowe informacje o logach, musisz ustawić właściwość debug.nn.vlog na all.