Interfejs API sieci neuronowych

Android Neural Networks API (NNAPI) to interfejs API Androida C przeznaczony do wykonywania operacji wymagających dużej mocy obliczeniowej na potrzeby systemów uczących się na urządzeniach z Androidem. NNAPI zapewnia podstawową warstwę funkcji dla platform systemów uczących się wyższego poziomu, takich jak TensorFlow Lite i Caffe2, które budują i trenują sieci neuronowe. Interfejs API jest dostępny na wszystkich urządzeniach z Androidem 8.1 (poziom API 27) lub nowszym.

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

Określanie na urządzeniu ma wiele zalet:

  • Czas oczekiwania: nie musisz wysyłać żądania przez połączenie sieciowe i czekać na odpowiedź. Może to być np. kluczowe w aplikacjach wideo, które przetwarzają kolejne klatki z kamery.
  • Dostępność: aplikacja działa nawet poza zasięgiem sieci.
  • Szybkość: nowy sprzęt dostosowany do przetwarzania sieci neuronowych zapewnia znacznie szybsze obliczenia niż procesor do zwykłych obciążeń.
  • Prywatność: dane nie opuszczają urządzenia z Androidem.
  • Koszt: farma serwerów nie jest potrzebna, gdy wszystkie obliczenia są wykonywane na urządzeniu z Androidem.

Deweloper powinien też pamiętać o pewnych kompromisach:

  • Wykorzystanie systemu: ocena sieci neuronowych wymaga dużej ilości zasobów obliczeniowych, co może zwiększyć wykorzystanie baterii. Jeśli problem dotyczy aplikacji, zwłaszcza w przypadku długotrwałych obliczeń, rozważ monitorowanie stanu baterii.
  • Rozmiar aplikacji: zwróć uwagę na rozmiar swoich modeli. Modele mogą zajmować wiele megabajtów miejsca. Jeśli grupowanie dużych modeli w pliku APK miałoby nadmierne 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 zapewnia funkcji uruchamiania modeli w chmurze.

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

Środowisko wykonawcze Neural Networks API

Interfejs NNAPI ma być wywoływany przez biblioteki, platformy i narzędzia systemów uczących się, które umożliwiają programistom trenowanie modeli poza urządzeniem i wdrażanie ich na urządzeniach z Androidem. Aplikacje zwykle nie używałyby bezpośrednio NNAPI, ale używałyby platform systemów uczących się wyższego poziomu. Te platformy mogą z kolei używać NNAPI do wykonywania operacji wnioskowania z akceleracją sprzętową na obsługiwanych urządzeniach.

W zależności od wymagań aplikacji i możliwości sprzętowych urządzeń z Androidem środowisko wykonawcze sieci neuronowej tego systemu może sprawnie rozłożyć zadania obliczeniowe między dostępne procesory na urządzeniu, w tym dedykowany sprzęt sieci neuronowej, procesory graficzne (GPU) i procesory sygnału cyfrowego (DSP).

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

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

Rysunek 1. Architektura systemu dla interfejsu Android Neural Networks API

Model programowania interfejsu API sieci neuronowych

Aby wykonać obliczenia za pomocą NNAPI, musisz najpierw utworzyć skierowany wykres określający obliczenia do wykonania. Ten wykres obliczeniowy w połączeniu z danymi wejściowymi (np. wagi i odchylenia przekazane ze środowiska systemów uczących się) tworzy model oceny środowiska wykonawczego NNAPI.

NNAPI wykorzystuje 4 główne abstrakcje:

  • Model: wykres obliczeniowy działań matematycznych i wartości stałych nauczonych w trakcie trenowania. Dotyczą one sieci neuronowych. Obejmują one m.in. aktywację splotu (2D), logistycznej (sigmoidalnej), prostokątnej (ReLU) i innych. Tworzenie modelu to operacja synchroniczna. Po utworzeniu można go używać ponownie w wątkach i kompilacjach. W NNAPI model jest reprezentowany jako wystąpienie ANeuralNetworksModel.
  • Kompilacja: reprezentuje konfigurację skompilowania modelu NNAPI do kodu niższego poziomu. Tworzenie kompilacji to operacja synchroniczna. Po utworzeniu można go używać ponownie w wątkach i wykonaniach. W NNAPI każda kompilacja jest reprezentowana jako instancja ANeuralNetworksCompilation.
  • Pamięć: reprezentuje pamięć udostępnioną, pliki zmapowane na pamięć i podobne bufory pamięci. Bufor pamięci pozwala środowiska wykonawczego NNAPI skuteczniej przesyłać dane do sterowników. Aplikacja zwykle tworzy 1 bufor pamięci współdzielonej, który zawiera wszystkich tensorów potrzebnych do zdefiniowania modelu. Możesz też używać buforów pamięci do przechowywania danych wejściowych i wyjściowych dla instancji wykonawczej. W NNAPI każdy bufor pamięci jest reprezentowany jako wystąpienie ANeuralNetworksMemory.
  • Wykonanie: interfejs do stosowania modelu NNAPI do zbioru danych wejściowych i uzyskiwania 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 wystąpienie ANeuralNetworksExecution.

Rysunek 2 przedstawia podstawowy proces programowania.

Rysunek 2. Proces programowania dla interfejsu Android Neural Networks API

W pozostałej części tej sekcji znajdziesz opis czynności, które musisz wykonać, aby skonfigurować model NNAPI do wykonywania obliczeń, skompilowania go i wykonania skompilowanego modelu.

Zapewnianie dostępu do danych treningowych

Wytrenowane dane dotyczące wag i odchylenia są prawdopodobnie przechowywane w pliku. Aby zapewnić środowisku wykonawczemu NNAPI efektywny 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 oraz przesunięcie, w którym zaczyna się region 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);

Chociaż w tym przykładzie do wszystkich naszych wag używamy tylko 1 instancji ANeuralNetworksMemory, można użyć więcej niż 1 instancji ANeuralNetworksMemory na potrzeby wielu plików.

Użyj natywnych buforów sprzętowych

Na potrzeby danych wejściowych i wyjściowych modelu oraz wartości argumentów stałych 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 z nich. Z tego powodu zapoznaj się z ograniczeniami wymienionymi w dokumentacji referencyjnej dotyczącej 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 oraz określać akcelerator za pomocą przypisania urządzenia.

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 będzie już potrzebować dostępu do obiektu AHardwareBuffer, zwolnij odpowiednią instancję ANeuralNetworksMemory:

ANeuralNetworksMemory_free(mem2);

Uwaga:

  • Możesz użyć właściwości AHardwareBuffer tylko do całego bufora; nie możesz użyć jej z parametrem ARect.
  • Środowisko wykonawcze NNAPI nie opróżnia bufora. Przed zaplanowaniem wykonania musisz sprawdzić, czy bufory danych wejściowych i wyjściowych są dostępne.
  • Brak obsługi deskryptorów plików ogrodzenia synchronizacji.
  • W przypadku obiektu AHardwareBuffer z formatami i informacjami dotyczącymi wykorzystania specyficznymi dla danego dostawcy to od jego wdrożenia zależy, czy to klient czy sterownik odpowiada za wyczyszczenie pamięci podręcznej.

Model

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

Operatory

Operatory 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 przesyłane między operacjami oraz stałe przekazywane do tych operacji.

Istnieją 2 typy operandów, które można dodawać do modeli NNAPI: skalary i tenery.

Skalar reprezentuje pojedynczą wartość. NNAPI obsługuje wartości skalarne w formatach wartości logicznych, 16-bitowych, zmiennoprzecinkowych, 32-bitowych, liczb całkowitych i 32-bitowych liczb całkowitych bez znaku.

Większość operacji w NNAPI dotyczy tensorów. Tensory to macierze n-wymiarowe. NNAPI obsługuje tensory z 16-bitową liczbą zmiennoprzecinkową, 32-bitową, 8-bitową, kwantyzowaną, 16-bitową kwantyzacją, 32-bitową liczbą całkowitą i 8-bitową wartością logiczną.

Na przykład rys. 3 przedstawia model z 2 operacjami: dodawanie, po którym następuje mnożenie. Model wykorzystuje tensor wejściowy i generuje 1 tensor wyjściowy.

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

Powyższy model ma 7 operandów. Te operandy są domyślnie identyfikowane przez indeks w kolejności, w jakiej zostały dodane do modelu. Pierwszy dodany operand ma indeks wynoszący 0, drugi indeks wynoszący 1 itd. Operatory 1, 2, 3 i 5 to stałe operandy.

Kolejność, w jakiej dodajesz operandy, nie ma znaczenia. Na przykład operandem wyjściowym modelu może być pierwszy dodany. Ważne jest, aby przy odwoływaniu się do operandu używać prawidłowej wartości indeksu.

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

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

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

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

Zarządzanie

Operacja określa, jakie obliczenia zostaną wykonane. Każda operacja składa się z tych elementów:

  • typu operacji (np. dodawanie, mnożenie, splot),
  • listę indeksów operandów używanych przez operację do wprowadzania danych wejściowych oraz
  • lista indeksów operandów używanych przez operację na potrzeby danych wyjściowych.

Kolejność na tych listach ma znaczenie. W dokumentacji interfejsu API NNAPI znajdziesz potrzebne dane wejściowe i odpowiedzi każdego typu operacji.

Przed dodaniem operacji musisz dodać do modelu operandy wykorzystywane lub generowane przez operację.

Kolejność dodawania operacji nie ma znaczenia. NNAPI określa kolejność wykonywania operacji na podstawie zależności określonych przez wykres obliczeniowy operandów i operacji.

Podsumowanie operacji obsługiwanych przez NNAPI znajdziesz w tej tabeli:

Kategoria Zarządzanie
Operacje matematyczne na podstawie elementów
Manipulacja Tensor
Operacje na obrazie
Operacje wyszukiwania
Operacje normalizacji
Operacje konwolucji
Operacje puli
Operacje aktywacji
Inne operacje

Znany problem na poziomie interfejsu API 28: podczas przekazywania tensorów ANEURALNETWORKS_TENSOR_QUANT8_ASYMM do operacji ANEURALNETWORKS_PAD, która jest dostępna w Androidzie 9 (poziom interfejsu API 28) i wyższych, dane wyjściowe z NNAPI mogą nie odpowiadać danym wyjściowym platform systemów uczących się wyższego poziomu, takich jak TensorFlow Lite. Zamiast tego przekaż tylko ANEURALNETWORKS_TENSOR_FLOAT32. Problem został rozwiązany w Androidzie 10 (poziom interfejsu API 29) i nowszych.

Tworzenie modeli

W poniższym przykładzie tworzymy model z 2 operacjami widoczny na rysunku 3.

Aby utworzyć 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 metodę 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 wartościach stałych, takich jak wagi i odchylenia, które aplikacja uzyskuje w procesie trenowania, użyj funkcji ANeuralNetworksModel_setOperandValue() i ANeuralNetworksModel_setOperandValueFromMemory().

    W przykładzie poniżej ustawiamy stałe wartości z pliku danych treningowych, które odpowiadają buforowi pamięci utworzonym w sekcji Zapewnianie dostępu do danych treningowych.

    // 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. Dla każdej operacji na skierowanym wykresie, którą chcesz obliczyć, dodaj operację do modelu, wywołując funkcję ANeuralNetworksModel_addOperation().

    Jako parametry tego wywołania aplikacja musi zapewnić:

    • typ operacji.
    • liczbę wartości wejściowych
    • tablica indeksów dla argumentów wejściowych
    • liczbę wartości wyjściowych
    • tablica indeksów dla argumentów wyjściowych

    Pamiętaj, że operandu nie można używać jednocześnie na potrzeby tej samej operacji wejściowej i wyjściowej.

    // 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. Aby określić, które operandy model powinien traktować jako dane wejściowe i wyjściowe, wywołaj 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 zasięgiem lub precyzją równą z zakresem lub precyzją 16-bitowego formatu IEEE 754, wywołując metodę ANeuralNetworksModel_relaxComputationFloat32toFloat16().

  7. Wywołaj ANeuralNetworksModel_finish(), aby dokończyć definicję modelu. Jeśli nie ma błędów, funkcja zwraca kod wyniku ANEURALNETWORKS_NO_ERROR.

    ANeuralNetworksModel_finish(model);
    

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

Sterowanie przepływem

Aby włączyć przepływ sterowania do modelu NNAPI, wykonaj te czynności:

  1. Utwórz odpowiednie podtytuły wykonania (podtytuły then i else dla instrukcji IF, condition i body dla pętli WHILE) jako samodzielne modele ANeuralNetworksModel*:

    ANeuralNetworksModel* thenModel = makeThenModel();
    ANeuralNetworksModel* elseModel = makeElseModel();
    
  2. Utwórz operandy odwołujące 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ę związaną z przepływem 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, w których procesorach model zostanie wykonany, i prosi odpowiednie sterowniki o przygotowanie się do jego wykonania. Może to obejmować generowanie kodu maszynowego specyficznego dla 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ć przypisania urządzenia, aby wyraźnie wybrać urządzenia, na których chcesz wykonywać działania.

  2. Opcjonalnie możesz wpłynąć na zmianę czasu działania środowiska wykonawczego na wykorzystanie baterii i szybkość wykonywania. Aby to zrobić, wywołaj metodę ANeuralNetworksCompilation_setPreference().

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

    Możesz określić te ustawienia:

  3. Możesz opcjonalnie skonfigurować buforowanie kompilacji, wywołując metodę ANeuralNetworksCompilation_setCaching.

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

    Użyj getCodeCacheDir() jako cacheDir. Podany token musi być unikalny dla każdego modelu w aplikacji.

  4. Zakończ definicję kompilacji, wywołując metodę 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 NNAPI udostępnia funkcje, które umożliwiają bibliotekom i aplikacjom platformy systemów uczących się uzyskiwanie informacji o dostępnych urządzeniach i określanie urządzeń, które mają być wykorzystane do realizacji. Podanie informacji o dostępnych urządzeniach umożliwia aplikacjom pobranie dokładnej wersji sterowników znalezionych na urządzeniu, co pozwala uniknąć znanych niezgodności. Umożliwiając aplikacjom określanie, które urządzenia mają uruchamiać różne sekcje modelu, można je optymalizować pod kątem urządzenia z Androidem, na którym są wdrożone.

Wykrywanie urządzeń

Aby sprawdzić liczbę dostępnych urządzeń, użyj parametru ANeuralNetworks_getDeviceCount. Dla każdego urządzenia użyj funkcji ANeuralNetworks_getDevice, aby ustawić instancję 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

Za pomocą ANeuralNetworksModel_getSupportedOperationsForDevices możesz sprawdzać, które operacje modelu można uruchamiać na konkretnych urządzeniach.

Aby określić, które akceleratory mają być używane do wykonywania, wywołaj ANeuralNetworksCompilation_createForDevices zamiast ANeuralNetworksCompilation_create. Użyj wynikowego obiektu ANeuralNetworksCompilation w zwykły sposób. Ta funkcja zwraca błąd, jeśli podany model zawiera operacje, które nie są obsługiwane przez wybrane urządzenia.

Jeśli określisz wiele urządzeń, za rozpowszechnianie utworu między nimi odpowiada środowisko wykonawcze.

Podobnie jak inne urządzenia, implementacja procesora NNAPI jest reprezentowana przez ANeuralNetworksDevice o nazwie nnapi-reference i typie ANEURALNETWORKS_DEVICE_TYPE_CPU. Gdy wywołujesz ANeuralNetworksCompilation_createForDevices, implementacja procesora nie jest używana do obsługi przypadków błędów przy kompilacji i wykonaniu modelu.

Partycjonowanie modelu na podmodele, które mogą działać na określonych urządzeniach, należy do aplikacji. Aplikacje, które nie wymagają ręcznego partycjonowania, nadal powinny wywoływać prostszy model ANeuralNetworksCompilation_create, aby wykorzystywać wszystkie dostępne urządzenia (w tym procesor) w celu przyspieszenia modelu. Jeśli model nie był w pełni obsługiwany przez urządzenia wskazane za pomocą ANeuralNetworksCompilation_createForDevices, zwracana jest wartość ANEURALNETWORKS_BAD_DATA.

Partycjonowanie modelu

Gdy dla modelu dostępnych jest wiele urządzeń, środowisko wykonawcze NNAPI rozdziela pracę między urządzenia. Jeśli na przykład firmie ANeuralNetworksCompilation_createForDevices zostało udostępnione więcej niż 1 urządzenie, przy przydzielaniu zadania zostaną brane pod uwagę wszystkie wymienione urządzenia. Pamiętaj, że jeśli procesora nie ma na liście, wykonywanie procesora będzie wyłączone. Gdy używany jest ANeuralNetworksCompilation_create, uwzględniane są wszystkie dostępne urządzenia, w tym procesor.

Rozkład odbywa się przez wybranie z listy dostępnych urządzeń dla każdej operacji w modelu urządzenie obsługujące daną operację i deklarowanie najlepszej wydajności, czyli najkrótszy czas wykonania lub najmniejsze zużycie energii w zależności od preferencji dotyczących wykonywania określonych przez klienta. Ten algorytm partycjonowania nie uwzględnia możliwych różnic spowodowanych przez operacje wejścia-wyjścia między różnymi procesorami. Podczas określania wielu procesorów (jednoznacznie przy użyciu funkcji ANeuralNetworksCompilation_createForDevices lub niejawnie za pomocą ANeuralNetworksCompilation_create) ważne jest profilowanie wynikowej aplikacji.

Aby dowiedzieć się, jak Twój model został partycjonowany przez NNAPI, sprawdź, czy w dziennikach Androida znajduje się komunikat (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 proponowanego urządzenia na liście urządzeń. Ta lista jest danymi wejściowymi przekazywanymi przez funkcję ANeuralNetworksCompilation_createForDevices lub, w przypadku użycia funkcji ANeuralNetworksCompilation_createForDevices, na liście urządzeń zwróconych podczas iteracji na wszystkich urządzeniach, które używają ANeuralNetworks_getDeviceCount i ANeuralNetworks_getDevice.

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

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

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

Realizacja

W ramach tego etapu model jest stosowany do zbioru danych wejściowych, a wyniki obliczeniowe są zapisywane w co najmniej 1 buforze użytkownika lub miejscu pamięci przydzielonym przez aplikację.

Aby uruchomić 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, gdzie aplikacja ma odczytywać wartości wejściowe na potrzeby obliczeń. Aplikacja może odczytywać wartości wejściowe z bufora użytkownika lub przydzielonego miejsca w pamięci, wywołując odpowiednio metodę 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 miejscu w 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 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 zakończyło się powodzeniem, funkcja zwraca kod wyniku w postaci ANEURALNETWORKS_NO_ERROR. Oczekiwanie można wykonać w innym wątku niż ten, w którym rozpoczęto 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 poświęca czas na generowanie i synchronizowanie wątków. Poza tym czas oczekiwania może być bardzo zmienny – najdłuższe opóźnienia między momentem powiadomienia lub wyłączenia wątku a najdłuższymi opóźnieniami sięgają 500 mikrosekund do ostatecznego powiązania z rdzeń procesora.

Aby skrócić czas oczekiwania, możesz zamiast tego skłonić aplikację do synchronicznego wywoływania wnioskowania w środowisku wykonawczym. To wywołanie pojawi się dopiero po zakończeniu wnioskowania, a nie po jego rozpoczęciu. Zamiast wywoływać algorytm ANeuralNetworksExecution_startCompute w celu asynchronicznego wywołania wnioskowania w środowisku wykonawczym, aplikacja wywołuje metodę ANeuralNetworksExecution_compute, aby wykonać synchroniczne wywołanie środowiska wykonawczego. Połączenie z numerem ANeuralNetworksExecution_compute nie odbiera sygnału ANeuralNetworksEvent i nie jest sparowane z połączeniem z numerem ANeuralNetworksEvent_wait.

Uruchomienia serii

Na urządzeniach z Androidem 10 (poziom interfejsu API 29) lub nowszym interfejs NNAPI obsługuje wykonywanie serii za pomocą obiektu ANeuralNetworksBurst. Uruchomienia serii to sekwencje uruchomień tej samej kompilacji, które mają miejsce w szybkich odstępach czasu, np. w klatkach nagrywania z kamery lub kolejnych próbkach audio. Użycie obiektów ANeuralNetworksBurst może przyspieszyć wykonywanie, ponieważ wskazują akceleratorom, że zasoby mogą być używane ponownie między wykonaniami, a 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 tworzysz 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);

Wykonanie serii jest synchroniczne. Jednak zamiast używać ANeuralNetworksExecution_compute do wykonywania każdego wnioskowania, sparujesz różne obiekty ANeuralNetworksExecution z tym samym atrybutem 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ą ANeuralNetworksBurst_free, gdy nie jest już potrzebny.

// Cleanup
ANeuralNetworksBurst_free(burst);

Asynchroniczne kolejki poleceń i chronione wykonywanie czynności

W Androidzie 11 i nowszych NNAPI obsługuje dodatkowy sposób planowania wykonywania asynchronicznego za pomocą metody ANeuralNetworksExecution_startComputeWithDependencies(). W przypadku tej metody przed rozpoczęciem oceny wykonanie czeka na sygnalizowanie wszystkich zależnych zdarzeń. Gdy wykonywanie kodu się zakończy, a dane wyjściowe będą gotowe do wykorzystania, zwrócone zdarzenie jest sygnalizowane.

W zależności od tego, które urządzenia obsługują wykonywanie, zdarzenie może być obsługiwane przez fencing synchronizacji. Musisz wywołać metodę ANeuralNetworksEvent_wait(), by czekać na zdarzenie i odzyskać zasoby użyte przez wykonanie. Możesz importować granice synchronizacji do obiektu zdarzenia za pomocą ANeuralNetworksEvent_createFromSyncFenceFd() oraz eksportować granice synchronizacji z obiektu zdarzenia za pomocą metody ANeuralNetworksEvent_getSyncFenceFd().

Dynamicznie o rozmiarze wyjściowym

Aby obsługiwać modele, w przypadku których rozmiar danych wyjściowych zależy od danych wejściowych (czyli takich, których rozmiaru nie można określić w czasie wykonywania modelu), użyj ANeuralNetworksExecution_getOutputOperandRank i ANeuralNetworksExecution_getOutputOperandDimensions.

Poniższy przykładowy kod ilustruje, 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

Etap czyszczenia pozwala na zwolnienie zasobów wewnętrznych używanych do obliczeń.

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

Zarządzanie błędami i awaryjne procesory

Jeśli podczas partycjonowania wystąpi błąd, sterownik nie skompiluje fragmentu modelu a lub jeśli sterownik nie wykona skompilowanego (fragmentu a) modelu, NNAPI może wrócić do własnej implementacji procesora tej operacji lub kilku operacji.

Jeśli klient NNAPI zawiera zoptymalizowane wersje operacji (np. TFLite), korzystne może być wyłączenie zastępczego procesora i obsługę błędów przy użyciu optymalizacji operacji wykonywanej przez klienta.

W Androidzie 10, jeśli kompilacja jest wykonywana za pomocą ANeuralNetworksCompilation_createForDevices, zastępcza szybkość procesora zostanie wyłączona.

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

Pierwsze wykonanie jest wykonywane dla tej pojedynczej partycji, a jeśli to się nie uda, następuje ponawianie całego modelu na procesorze.

Jeśli partycjonowanie lub kompilacja się nie uda, zostanie podjęta próba pełnego modelu modelu korzystającego z procesora.

W pewnych przypadkach niektóre operacje nie są obsługiwane przez procesor, a w takich sytuacjach kompilacja lub wykonanie kończy się niepowodzeniem, a nie wykonywanym działaniem awaryjnym.

Nawet po wyłączeniu opcji zastępczej procesora nadal mogą być wykonywane w modelu operacje, które są zaplanowane na procesorze. Jeśli procesor znajduje się na liście procesorów dostarczanych do usługi ANeuralNetworksCompilation_createForDevices i jest jedynym podmiotem przetwarzającym, który obsługuje te operacje lub deklaruje, że jego wydajność ma największą wydajność, zostanie wybrany jako główny (niezastępczy).

Aby upewnić się, że nie będzie wykonywania procesora, użyj ANeuralNetworksCompilation_createForDevices, wykluczając nnapi-reference z listy urządzeń. Począwszy od Androida P, można wyłączyć kreację zastępczą w czasie wykonywania w kompilacjach DEBUG, ustawiając właściwość debug.nn.partition na 2.

Domeny pamięci

W Androidzie 11 i nowszych NNAPI obsługuje domeny pamięci, które zapewniają interfejsy alokatora nieprzezroczystych wspomnień. Umożliwia to aplikacjom przekazywanie pamięci natywnych urządzenia między wykonaniami, dzięki czemu NNAPI nie kopiują ani nie przekształca danych w niepotrzebny sposób podczas wykonywania kolejnych uruchomień tego samego sterownika.

Funkcja domeny pamięci jest przeznaczona dla tensorów, które są głównie wewnętrzne w sterowniku 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órzy często potrzebują dostępu do procesora po stronie klienta, użyj pul pamięci współdzielonej.

Aby przydzielić nieprzejrzystą pamięć, 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 wymagane role wejścia i wyjścia, 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 określ wymiary pamięci, wywołując metodę ANeuralNetworksMemoryDesc_setDimensions().

    // Specify the memory dimensions
    uint32_t dims[] = {3, 4};
    ANeuralNetworksMemoryDesc_setDimensions(desc, 2, dims);
    
  4. Dokończ definicję deskryptora, wywołując metodę ANeuralNetworksMemoryDesc_finish().

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

    // Allocate two opaque memories with the descriptor
    ANeuralNetworksMemory* opaqueMem;
    ANeuralNetworksMemory_createFromDesc(desc, &opaqueMem);
    
  6. Zwalnianie deskryptora pamięci, gdy już go nie potrzebujesz.

    ANeuralNetworksMemoryDesc_free(desc);
    

Klient może używać utworzonego obiektu ANeuralNetworksMemory z ANeuralNetworksExecution_setInputFromMemory() lub ANeuralNetworksExecution_setOutputFromMemory() tylko zgodnie z rolami określonymi w obiekcie ANeuralNetworksMemoryDesc. Argumenty przesunięcia i długości muszą mieć wartość 0, co wskazuje, że używana jest cała pamięć. Klient może też bezpośrednio ustawić lub wyodrębnić zawartość pamięci za pomocą ANeuralNetworksMemory_copy().

Możesz tworzyć nieprzejrzyste wspomnienia z rolami o nieokreślonych wymiarach lub rankingu. W takim przypadku utworzenie pamięci może się nie udać ze stanem ANEURALNETWORKS_OP_FAILED, jeśli nie jest obsługiwana przez sterownik. Zachęcamy klienta do wdrożenia logiki zastępczej przez przypisanie odpowiednio dużego bufora w trybie Ashmem lub BLOB w trybie AHardwareBuffer.

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

ANeuralNetworksMemory_free(opaqueMem);

Pomiar wyników

Wydajność aplikacji możesz ocenić, mierząc czas jej wykonywania lub profilując.

Czas wykonywania

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

  • czas wykonywania w akceleratorze (nie w sterowniku, który działa na procesorze hosta).
  • czas wykonywania w sterowniku z uwzględnieniem czasu pracy akceleratora.

Czas wykonywania w sterowniku nie uwzględnia nadmiarowości, takich jak czas działania samego środowiska wykonawczego oraz IPC potrzebne do komunikacji ze sterownikiem w środowisku wykonawczym.

Te interfejsy API mierzą czas między przesłanymi zadaniami a zdarzeniami zakończenia pracy, a nie czas poświęcany przez kierowcę lub akcelerator na przeprowadzanie wnioskowania, który może zostać przerwany przez zmianę kontekstu.

Jeśli na przykład zaczyna się wnioskowanie 1, to kierowca zatrzyma pracę, aby wykonać wnioskowanie 2, a następnie wznowi i zakończy wnioskowanie 1, czas wykonania wnioskowania 1 będzie obejmował czas, w którym zatrzymano pracę w celu wykonania wnioskowania 2.

Informacje o czasie mogą być przydatne przy wdrażaniu produkcyjnym aplikacji w celu zbierania danych telemetrycznych do użytku w trybie offline. Dane o czasie mogą posłużyć do modyfikowania aplikacji w celu zwiększenia jej wydajności.

Korzystając z tej funkcji, pamiętaj o następujących kwestiach:

  • Zbieranie informacji o czasie może się wiązać z kosztami wydajności.
  • Tylko sterownik może obliczyć czas spędzony sam w sobie lub na akceleratorze, z wyłączeniem czasu spędzonego w środowisku wykonawczym NNAPI i w IPC.
  • Tych interfejsów API można używać tylko za pomocą interfejsu ANeuralNetworksExecution, który został utworzony za pomocą polecenia ANeuralNetworksCompilation_createForDevices przy użyciu metody numDevices = 1.
  • Kierowcy nie muszą mieć możliwości przesłania informacji o czasie trwania.

Profilowanie aplikacji za pomocą systemu Android Systrace

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

Źródło NNAPI zawiera narzędzie parse_systrace do przetwarzania zdarzeń systrace wygenerowanych przez aplikację oraz do generowania widoku tabeli pokazującego czas spędzony w różnych fazach cyklu życia modelu (Implementacja, Przygotowanie, Wykonanie kompilacji i Zakończenie) oraz różne warstwy aplikacji. Podzielona jest aplikacja:

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

Generowanie danych analiz profilowania

Zakładając, że już znasz drzewo źródłowe AOSP w witrynie $ANDROID_BUILD_TOP i użyjesz przykładowej klasyfikacji obrazów TFLite jako aplikacji docelowej, możesz wygenerować dane profilowania NNAPI, wykonując te czynności:

  1. Uruchom systrace Androida 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 oznacza, że ślady będą zapisywane w trace.html. Podczas profilowania własnej aplikacji musisz zastąpić ciąg org.tensorflow.lite.examples.classification nazwą procesu określoną w pliku manifestu aplikacji.

Dzięki temu jedna z konsoli powłoki pozostanie zajęta. Nie uruchamiaj tego polecenia w tle, ponieważ czeka ono interaktywnie na zakończenie działania enter.

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

W takim przypadku możesz uruchomić aplikację Klasyfikacja obrazów z Androida Studio lub bezpośrednio z testowego interfejsu telefonu, jeśli jest już zainstalowana. Aby wygenerować dane NNAPI, musisz skonfigurować aplikację do korzystania z NNAPI. W tym celu w oknie konfiguracji aplikacji wybierz NNAPI jako urządzenie docelowe.

  1. Po zakończeniu testu zakończ ciąg systrace, naciskając enter na terminalu konsoli, który jest aktywny od kroku 1.

  2. Uruchom narzędzie systrace_parser, aby wygenerować statystyki łączne:

$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 do warstwy bazowej; – --print-detail: wyświetla wszystkie zdarzenia zebrane ze śledzenia systemu; – --per-execution: wyświetla tylko wykonanie i jego etapy (jako czas 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

Parser może przestać działać, jeśli zebrane zdarzenia nie przedstawiają pełnego logu czasu aplikacji. 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 bez powiązanego zdarzenia rozpoczęcia sekcji. Zwykle dzieje się tak, jeśli przy uruchomieniu kolektora systrace są generowane niektóre zdarzenia z poprzedniej sesji profilowania. W takim przypadku trzeba będzie ponownie uruchomić profilowanie.

Dodaj statystyki dotyczące kodu aplikacji do danych wyjściowych funkcji systrace_parser

Aplikacja parse_systrace jest oparta na wbudowanej funkcji systemu Android. Możesz dodać logi czasu określonych operacji w aplikacji za pomocą interfejsu systrace API (dla Javy, aplikacji natywnych) z niestandardowymi nazwami zdarzeń.

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

  • [NN_LA_PI]: zdarzenie inicjowania na poziomie aplikacji
  • [NN_LA_PP]: zdarzenie na poziomie aplikacji dotyczące przygotowań
  • [NN_LA_PC]: zdarzenie na poziomie aplikacji na potrzeby kompilacji
  • [NN_LA_PE]: zdarzenie na poziomie aplikacji na potrzeby wykonania

Oto przykład, jak można zmienić przykładowy kod klasyfikacji obrazów TFLite przez dodanie sekcji 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 interfejs NNAPI zapewnia lepszą jakość usługi (QoS) przez umożliwienie aplikacji wskazania względnych priorytetów modeli, maksymalnego czasu oczekiwanego na przygotowanie danego modelu oraz maksymalnego czasu oczekiwanego na wykonanie danych obliczeń. W Androidzie 11 wprowadziliśmy też dodatkowe kody wyników NNAPI, które pozwalają aplikacjom rozpoznawać błędy, takie jak brakujące terminy wykonania.

Ustaw priorytet zadania

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

Ustaw terminy

Aplikacje mogą określać terminy zarówno kompilacji modelu, jak i wnioskowania.

Więcej informacji o operandach

W tej sekcji znajdziesz zaawansowane tematy korzystania z operandów.

Kwantyzowane tensory

Kwantyzowany tensor to kompaktowy 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 tenisorem powiązane jest skala i wartość zero-punktowa. Pozwalają one przekonwertować 8-bitowe liczby całkowite na przedstawiane wartości zmiennoprzecinkowe.

Zastosowana formuła to:

(cellValue - zeroPoint) * scale

Wartość zeroPoint jest 32-bitową liczbą całkowitą, a skala 32-bitową liczbą zmiennoprzecinkową.

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

  • Twoja aplikacja jest mniejsza, ponieważ wytrenowane wagi zajmują 1/4 rozmiaru tensorów 32-bitowych.
  • Obliczenia często można wykonywać szybciej. Wynika to z mniejszej ilości danych do pobrania z pamięci i większej wydajności procesorów, takich jak platformy DSP, w wykonywaniu obliczeń na podstawie liczb całkowitych.

Chociaż można przekonwertować model zmiennoprzecinkowy na skwantyzowany, z naszego doświadczenia wynika, że lepsze wyniki osiąga się przez bezpośrednie trenowanie modelu skwantyzowanego. W efekcie sieć neuronowa uczy się, aby kompensować rosnącą szczegółowość poszczególnych wartości. Dla każdego skwantyzowanego tensora skala i wartości zeroPoint są określane podczas procesu trenowania.

W NNAPI typy tensorów skwantyzowanych możesz zdefiniować, ustawiając pole typu struktury danych ANeuralNetworksOperandType na ANEURALNETWORKS_TENSOR_QUANT8_ASYMM. Określasz też skalę i wartość zeroPoint dla tensora w tej strukturze danych.

Oprócz 8-bitowych asymetrycznych tensorów skwantyzowanych, NNAPI obsługuje następujące funkcje:

Opcjonalne operandy

Kilka operacji, np. ANEURALNETWORKS_LSH_PROJECTION, przyjmuje opcjonalne operandy. Aby wskazać w modelu, że opcjonalny operand został pominięty, wywołaj funkcję ANeuralNetworksModel_setOperandValue(), przekazując NULL jako bufor i wartość 0 jako długość.

Jeśli decyzja o obecności operandu jest inna w przypadku każdego wykonania, wskazujesz, że operand jest pomijany za pomocą funkcji ANeuralNetworksExecution_setInput() lub ANeuralNetworksExecution_setOutput() – wartość NULL oznacza bufor i 0.

Tensory o nieznanej pozycji

W Androidzie 9 (poziom interfejsu API 28) wprowadzono operandy modelu o nieznanych wymiarach, ale o znanej pozycji (liczbie wymiarów). W Androidzie 10 (poziom interfejsu API 29) wprowadzono tenisery o nieznanej pozycji, co widać w elemencie ANeuralNetworksValueType.

Test porównawczy NNAPI

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

Test porównawczy ocenia czas oczekiwania i dokładność oraz porównuje sterowniki z pracą wykonywaną w ramach Tensorflow Lite w CPU dla tych samych modeli i zbiorów danych.

Aby użyć testu porównawczego:

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

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

  3. Przejdź do katalogu źródłowego 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 wyniki zostaną przedstawione jako strona HTML przesłana do xdg-open.

Logi NNAPI

NNAPI generuje w dziennikach systemowych przydatne informacje diagnostyczne. Do przeanalizowania logów użyj narzędzia logcat.

Włącz szczegółowe logowanie NNAPI dla konkretnych etapów lub komponentów, ustawiając właściwość debug.nn.vlog (za pomocą adb shell) na listę wartości oddzielonych spacją, dwukropkiem lub przecinkiem:

  • model: tworzenie modeli
  • compilation: generowanie planu wykonywania i kompilacji modelu
  • execution: wykonanie modelu
  • cpuexe: wykonywanie operacji przy użyciu implementacji procesora NNAPI
  • manager: rozszerzenia NNAPI, informacje związane z dostępnymi interfejsami i możliwościami
  • 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ć 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ę etapu lub komponentu.

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

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 obecnie zwraca następujące tagi:

  • Budowa rozgrywek
  • Wywołania zwrotne
  • Kreator kompilacji
  • CpuExecutor
  • Twórca ExecutionBuild
  • Kontroler ExecutionBurstController
  • Serwer ExecutionBurstServer
  • Plan wykonania
  • FibonacciDriver
  • Wykres słupkowy
  • Element Indexed shapeWrapper
  • Zegarek Ion
  • Menedżer
  • Pamięć
  • MemoryUtils
  • Metamodel
  • Informacje o argumentach modelu
  • Kreator modeli
  • Sieci neuronowe
  • Program do rozpoznawania operacji
  • Zarządzanie
  • Narzędzia operacyjne
  • Informacje o pakiecie
  • Parametr TokenHasher
  • Menedżer typów
  • Narzędzia
  • ValidateHal
  • Wersjonowane interfejsy

Aby kontrolować poziom komunikatów logu wyświetlanych przez funkcję logcat, użyj zmiennej środowiskowej ANDROID_LOG_TAGS.

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

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 ma zastosowanie do logcat. Aby generować szczegółowe informacje logu, musisz jeszcze ustawić właściwość debug.nn.vlog na all.