Neural Networks API

Android Neural Networks API (NNAPI) to interfejs API Androida C przeznaczony do uruchamiania intensywnych operacji obliczeniowych na potrzeby systemów uczących się 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 przez stosowanie danych z urządzeń z Androidem do wcześniej wytrenowanych modeli określonych przez programistę. 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ć szczególnie ważne np. w przypadku aplikacji wideo, które przetwarzają kolejno klatki z kamery.
  • Dostępność: aplikacja działa nawet poza zasięgiem sieci.
  • Szybkość: nowy sprzęt dostosowany do przetwarzania sieci neuronowych umożliwia znacznie szybsze obliczenia niż sam procesor do zwykłych obciążeń.
  • 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ą specjalistycznego sterownika dostawcy, środowisko wykonawcze NNAPI wykonuje żądania na CPU.

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

Rysunek 1. Architektura systemu dla 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 graf obliczeń w połączeniu z danymi wejściowymi (np. wagami i błędami 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. Operacje te są charakterystyczne 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ę służącą do kompilowania modelu NNAPI w kod 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 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 przesyłać 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 być wykonywane 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 zostaną zwolnione.

    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 opisujemy czynności, które należy wykonać, aby skonfigurować model NNAPI do 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 deskryptor 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ć wbudowanych 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 względu zapoznaj się z ograniczeniami wymienionymi w dokumentacji referencyjnej ANeuralNetworksMemory_createFromAHardwareBuffer i przetestuj je z wyprzedzeniem na urządzeniach docelowych, aby mieć pewność, że kompilacje i wykonania korzystające z AHardwareBuffer działają zgodnie z oczekiwaniami. Aby określić akcelerator, użyj przypisania urządzenia.

Aby zezwolić środowisku wykonawczym NNAPI na dostęp do obiektu AHardwareBuffer, utwórz instancję ANeuralNetworksMemory, wywołując funkcję ANeuralNetworksMemory_createFromAHardwareBuffer i przekazując obiekt AHardwareBuffer w następujący sposób:

// 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żna używać tylko w przypadku całego bufora; nie można 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 zabezpieczeń synchronizacji.
  • W przypadku AHardwareBuffer z formatami i bitami użytkowania specyficznymi dla dostawcy to implementacja dostawcy określa, 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 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 między operacjami i 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 na ilustracji 3 przedstawiono model z 2 operacjami: dodawanie, po którym następuje mnożenie. Model przyjmuje wejściowy tensor i generuje jeden wyjściowy tensor.

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

Powyższy model 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.

Operatory 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ą być wykonane. Każda operacja składa się z tych elementów:

  • typ operacji (np. dodawanie, mnożenie, sprzężenie),
  • listę indeksów operandów używanych przez operację jako dane wejściowe;
  • 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
Działania matematyczne z użyciem elementów
Manipulacja tensorami
Operacje na obrazie
Operacje wyszukiwania
Operacje normalizacji
Operacje splotowe
Operacje łączenia
Operacje aktywacji
Inne operacje

Znane problemy w przypadku poziomu interfejsu API 28: jeśli 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, wykonaj te czynności:

  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 operandów 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 wartość ANEURALNETWORKS_TENSOR_FLOAT32 może być obliczana z zakresem lub dokładnością na poziomie równym 16-bitowego formatu zmiennoprzecinkowego IEEE 754, wywołując metodę ANeuralNetworksModel_relaxComputationFloat32toFloat16().

  7. Aby dokończyć definiowanie modelu, zadzwoń pod numer ANeuralNetworksModel_finish(). Jeśli nie ma błędów, ta 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 maszyny konkretnego procesora, na którym 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. Możesz opcjonalnie 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 biblioteki getCodeCacheDir() do cacheDir. Podany parametr 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 pozwala aplikacjom pobrać dokładną wersję 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 zapytania ANeuralNetworks_getDeviceCount, aby sprawdzić 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.

Mając odniesienie do 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óre akceleratory mają być używane do wykonywania, wywołaj 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.

Zadaniem aplikacji jest partycjonowanie modelu na modele podrzędne, które mogą działać na określonych urządzeniach. Aplikacje, które nie muszą ręcznie partycjonować, powinny nadal wywoływać prostszy element ANeuralNetworksCompilation_create, który pozwala wykorzystać wszystkie dostępne urządzenia (w tym procesor) w celu przyspieszenia modelu. Jeśli model nie jest w pełni obsługiwany przez urządzenia określone za pomocą ANeuralNetworksCompilation_createForDevices, ANEURALNETWORKS_BAD_DATA zostaje 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 (wyraźnie za pomocą funkcji ANeuralNetworksCompilation_createForDevices lub domyślnie za pomocą funkcji ANeuralNetworksCompilation_create) ważne jest przeprofilowanie powstałej aplikacji.

Aby dowiedzieć się, jak Twój model został partycjonowany przez NNAPI, poszukaj komunikatu w logach Androida (na poziomie INFORMACYJNYM 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

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

Realizacja

W etapie wykonania model jest stosowany do zestawu danych wejściowych, a wyniki obliczeń są przechowywane w co najmniej 1 buforze użytkownika lub w co najmniej 1 miejscu w pamięci przydzielonym 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 obliczeń. Aplikacja może odczytywać wartości wejściowe z bufora użytkownika lub przydzielonego miejsca w pamięci, wywołując odpowiednio 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, ta 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ę udało, funkcja zwraca kod wyniku o wartości 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

Wykonywanie asynchroniczne wymaga czasu na powstawanie i synchronizowanie wątków. Poza tym opóźnienie może być bardzo zmienne – najdłuższe opóźnienie sięga 500 mikrosekund od momentu powiadomienia lub zmodyfikowania wątku do momentu jego powiązania z rdzeniem procesora.

Aby zmniejszyć opóźnienie, możesz zamiast tego przekierować 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ć metodę ANeuralNetworksExecution_startCompute w celu asynchronicznego wywołania wnioskowania do środowiska wykonawczego, aplikacja wywołuje ANeuralNetworksExecution_compute, aby wykonać synchroniczne wywołanie środowiska wykonawczego. 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 NNAPI obsługuje uruchomienia burst z użyciem obiektu ANeuralNetworksBurst. Seria wykonań to sekwencja wykonań tej samej kompilacji, które następują w krótkich odstępach czasu, np. na kadrach z kamery lub na kolejnych próbkach 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 tą samą wartością 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(). Jeśli używasz tej metody, wykonanie czeka przed rozpoczęciem oceny, aż pojawi się sygnał dla wszystkich zdarzeń zależnych. 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. Ograniczenia synchronizacji do obiektu zdarzenia możesz importować za pomocą narzędzia ANeuralNetworksEvent_createFromSyncFenceFd(), a ograniczenia synchronizacji z obiektu zdarzenia – za pomocą narzędzia 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.

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 przywracane dla tej pojedynczej partycji, a jeśli to nadal się nie uda, ponawia próbę wykonania całego modelu na CPU.

Jeśli podział na partycje lub kompilacja zakończą się niepowodzeniem, 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 przełączania awaryjnego procesora nadal mogą być wykonywane operacje w modelu, które są zaplanowane na procesorze. Jeśli procesor znajduje się na liście procesorów przekazanych do ANeuralNetworksCompilation_createForDevices i jest jedynym podmiotem przetwarzającym te operacje, który obsługuje te operacje lub jest procesorem, który deklaruje najlepszą wydajność w przypadku tych operacji, zostanie wybrany jako główny (bez zastępczy) wykonawca.

Aby mieć pewność, że nie ma wykonania na procesorze, użyj ANeuralNetworksCompilation_createForDevices, wykluczając nnapi-reference z listy urządzeń. Począwszy od Androida P możliwe jest wyłączenie działania awaryjnego w przypadku kompilacji DEBUG podczas wykonywania, 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ęci natywne dla urządzenia pomiędzy uruchomieniami, dzięki czemu NNAPI nie kopiuje ani nie przekształca niepotrzebnie danych podczas wykonywania kolejnych wykonań na tym samym sterowniku.

Funkcja domeny pamięci jest przeznaczona do tensorów, które są głównie wewnętrzne dla sterownika i nie wymagają częstego dostępu po stronie klienta. Przykładami takich tensorów są tensory stanów w modelach sekwencji. W przypadku tensorów, które wymagają częstego dostępu do procesora po stronie klienta, użyj puli pamięci 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 wymagane role wejściowe i wyjściowe, wywołując metody 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. 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 przesunięcia i długości muszą mieć wartość 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ć stan ANEURALNETWORKS_OP_FAILED, jeśli nie jest obsługiwane przez podrzędny sterownik. Zalecamy wdrożenie logiki zapasowej przez przydzielenie wystarczająco dużego bufora obsługiwanego przez Ashmem lub tryb 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 wykonania

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 przesłanej pracy a zakończonymi zdarzeniami, a nie czas, jaki sterownik lub akcelerator poświęca na wnioskowanie, co może być przerywane przez przełączanie kontekstu.

Jeśli na przykład rozpocznie się wnioskowanie 1, a następnie sterownik przestanie wykonywać zadanie, aby wykonać wnioskowanie 2, a następnie wznowi i dokończy wnioskowanie 1, czas wykonania wnioskowania 1 będzie obejmował czas, w którym praca została przerwana, 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 na samym sobie lub na akceleratorze, z wyłączeniem 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

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ę i 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 warstwach aplikacji. Aplikacja jest podzielona na następujące warstwy:

  • Application: kod głównej 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 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 ś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 kolektora systrace uruchom aplikację i uruchom 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 w 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

Oto przykładowe dane wyjściowe:

===========================================================================================================================================
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 wystąpić błąd, jeśli w logu czasu występują zdarzenia Systrace wygenerowane w celu oznaczenia końca sekcji, ale nie ma powiązanego 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 jest oparta na wbudowanej funkcji systemu Android Systrace. Możesz dodać logi czasu dla konkretnych operacji w aplikacji za pomocą interfejsu Systrace API (dla Javy i aplikacji natywnych) z niestandardowymi nazwami zdarzeń.

Aby powiązać zdarzenia niestandardowe z fazami cyklu życia aplikacji, dołącz do nazwy zdarzenia jeden z tych ciągó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 na potrzeby kompilacji
  • [NN_LA_PE]: zdarzenie na poziomie aplikacji do 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 parser nnapi systrace:

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 NNAPI zapewnia lepszą jakość usług (QoS), umożliwiając aplikacji wskazywanie względnych priorytetów modeli, maksymalnego czasu potrzebnego na przygotowanie danego modelu oraz maksymalnego czasu wykonywania danego obliczenia. Android 11 wprowadza też dodatkowe kody wyników NNAPI, które pozwalają aplikacjom analizować błędy, takie jak niewykonane w terminie.

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 dalszej części omówiono zaawansowane zagadnienia dotyczące używania 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 skala 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:

  • Twoja aplikacja jest mniejsza, ponieważ wytrenowane waga zajmuje jedną czwartą rozmiaru tensorów 32-bitowych.
  • Obliczenia mogą być często wykonywane szybciej. Wynika to z mniejszej ilości danych, które trzeba pobrać z pamięci, oraz wydajności procesorów, takich jak platformy DSP, przy obliczaniu liczb całkowitych.

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. Dla każdego kwantowego tensora wartości skali i zeroPoint 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 jest pominięty, wywołaj funkcję ANeuralNetworksModel_setOperandValue(), podając wartość NULL jako bufor i 0 dla długości.

Jeśli określasz, czy operand jest obecny czy nie w przypadku każdego wykonania, możesz wskazać, że dany operand jest pomijany za pomocą funkcji ANeuralNetworksExecution_setInput() lub ANeuralNetworksExecution_setOutput(), podając wartość NULL jako bufor i 0 jako długość.

Tensory o nieznanej rangi

Android 9 (poziom interfejsu API 28) wprowadził modele z nieznanymi wymiarami, ale znanymi rangami (liczbą 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 platform/test/mlts/benchmark (aplikacja analizy porównawczej) 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 rozdzielanych spacjami, dwukropkami lub przecinkami:

  • model: budowanie modelu
  • compilation: wygenerowanie 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 logowanie szczegółowe generuje wpisy logu na poziomie INFORMACYJNE z tagiem ustawionym na nazwę etapu 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:

  • Kreator rozbłysku
  • Wywołania zwrotne
  • CompilationBuilder
  • CpuExecutor
  • ExecutionBuilder
  • Sterowanie kręceniem ujęć
  • Serwer ExecutionBurst
  • Plan wykonania
  • FibonacciDriver
  • GraphDump
  • IndexedShapeWrapper
  • IonWatcher
  • Menedżer
  • Pamięć
  • MemoryUtils
  • MetaModel
  • Informacje o argumentach modelu
  • ModelBuilder
  • NeuralNetworks
  • OperationResolver
  • Zarządzanie
  • OperationsUtils
  • PackageInfo
  • TokenHasher
  • Menedżer typów
  • Utils
  • ValidateHal
  • Wersjonowane interfejsy

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.