Interfejs NNAPI (Android Neural Networks API) to interfejs API w języku C na Androida, który umożliwia wykonywanie na urządzeniach z Androidem operacji wymagających dużych zasobów obliczeniowych na potrzeby uczenia maszynowego. Interfejs NNAPI ma zapewniać podstawowy zestaw funkcji dla zaawansowanych frameworków uczenia maszynowego, takich jak TensorFlow Lite i Caffe2, które budują i trenowały sieci neuronowe. Interfejs API jest dostępny na wszystkich urządzeniach z Androidem w wersji 8.1 (poziom interfejsu API 27) lub nowszej.
NNAPI obsługuje wnioskowanie, stosując dane z urządzeń z Androidem do wcześniej wytrenowanych modeli zdefiniowanych przez dewelopera. Przykłady wnioskowania to m.in. klasyfikowanie obrazów, przewidywanie zachowań użytkowników i wybieranie odpowiednich odpowiedzi na zapytania wyszukiwania.
Wyciąganie wniosków na urządzeniu ma wiele zalet:
- Opóźnienie: nie musisz wysyłać żądania przez połączenie sieciowe i czekać na odpowiedź. Może to być na przykład bardzo ważne w przypadku aplikacji wideo, które przetwarzają kolejne klatki pochodzące z kamery.
- Dostępność: aplikacja działa nawet poza zasięgiem sieci.
- Szybkość: nowy sprzęt przeznaczony do przetwarzania sieci neuronowych zapewnia znacznie szybsze obliczenia niż procesor ogólnego przeznaczenia.
- Prywatność: dane nie opuszczają urządzenia z Androidem.
- Koszt: nie jest potrzebna farma serwerów, ponieważ wszystkie obliczenia są wykonywane na urządzeniu z Androidem.
Deweloper powinien też wziąć pod uwagę pewne kompromisy:
- Wykorzystanie systemu: ocena sieci neuronowych wymaga wielu obliczeń, które mogą zwiększyć zużycie baterii. Jeśli stan baterii jest dla Ciebie istotny, zwłaszcza w przypadku długotrwałych obliczeń, rozważ monitorowanie stanu baterii.
- Rozmiar aplikacji: zwróć uwagę na rozmiar swoich modeli. Modele mogą zajmować kilka megabajtów miejsca. Jeśli zgrupowanie dużych modeli w pliku APK będzie miało niekorzystny wpływ na użytkowników, rozważ pobranie modeli po zainstalowaniu aplikacji, użycie mniejszych modeli lub przeprowadzenie obliczeń w chmurze. NNAPI nie udostępnia funkcji do uruchamiania modeli w chmurze.
Aby zobaczyć przykład użycia interfejsu NNAPI, zapoznaj się z przykładem interfejsu NNAPI na Androida.
Interfejs Neural Networks API w czasie wykonywania
Interfejs NNAPI jest wywoływany przez biblioteki, frameworki i narzędzia systemów uczących się, które umożliwiają deweloperom trenowanie modeli poza urządzeniem i wdrażanie ich na urządzeniach z Androidem. Aplikacje zwykle nie korzystają bezpośrednio z NNAPI, ale z ramek uczenia maszynowego wyższego poziomu. Te frameworki mogłyby z kolei używać NNAPI do wykonywania operacji wnioskowania przyspieszonego sprzętowo na obsługiwanych urządzeniach.
Na podstawie wymagań aplikacji i możliwości sprzętowych urządzenia z Androidem środowisko uruchomieniowe sieci neuronowej na Androidzie może efektywnie rozkładać obciążenie obliczeniowe na dostępne procesory na urządzeniu, w tym na dedykowany sprzęt sieci neuronowej, jednostki przetwarzania graficznego (GPU) i procesory sygnałowe (DSP).
W przypadku urządzeń z Androidem, które nie mają dedykowanego sterownika dostawcy, środowisko wykonawcze NNAPI wykonuje żądania na procesorze.
Rysunek 1 przedstawia ogólną architekturę systemu NNAPI.
Model programowania interfejsu Neural Networks API
Aby wykonywać obliczenia za pomocą NNAPI, musisz najpierw utworzyć skierowany graf, który definiuje obliczenia do wykonania. Ten graf obliczeń, połączony z danymi wejściowymi (np. wagami i błędami przekazywanymi z ramy sztucznej inteligencji), tworzy model do oceny w czasie wykonywania NNAPI.
NNAPI korzysta z 4 głównych abstrakcji:
- Model: wykres obliczeń operacji matematycznych i wartości stałych wyuczonych w trakcie procesu trenowania. Te operacje są specyficzne dla sieci neuronowych. Są to m.in. 2D-konwolucja, funkcja logistyczna (sigmoidalna), funkcja liniowa z wyprostowaniem (ReLU) i inne. Tworzenie modelu jest operacją synchroniczną.
Po utworzeniu można go używać w różnych wątkach i kompilacjach.
W NNAPI model jest reprezentowany przez instancję
ANeuralNetworksModel
. - Kompilacja: reprezentuje konfigurację kompilacji modelu NNAPI na kod niskiego poziomu. Tworzenie kompilacji jest operacją synchroniczną. Po utworzeniu można go używać ponownie w różnych wątkach i wykonaniach. W NNAPI każda kompilacja jest reprezentowana jako instancja
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 odbywać się synchronicznie lub asynchronicznie.
W przypadku wykonania asynchronicznego wiele wątków może czekać na to samo wykonanie. Po zakończeniu tego wykonania wszystkie wątki są zwalniane.
W NNAPI każde wykonanie jest reprezentowane jako instancja
ANeuralNetworksExecution
.
Rysunek 2 przedstawia podstawowy proces programowania.
W pozostałej części tej sekcji opisano czynności konfigurowania modelu NNAPI w celu wykonywania obliczeń, kompilowania modelu i uruchamiania skompilowanego modelu.
Udostępnianie danych treningowych
Trenowane wagi i dane o uprzedzeniach są prawdopodobnie przechowywane w pliku. Aby zapewnić środowisku uruchomieniowemu NNAPI skuteczny dostęp do tych danych, utwórz instancję ANeuralNetworksMemory
, wywołując funkcję ANeuralNetworksMemory_createFromFd()
i przekazując 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 powodu zapoznaj się z ograniczeniami wymienionymi w dokumentacji referencyjnej ANeuralNetworksMemory_createFromAHardwareBuffer
i przeprowadź wcześniej testy na urządzeniach docelowych, aby mieć pewność, że kompilacje i wykonania korzystające z AHardwareBuffer
działają zgodnie z oczekiwaniami. Do określenia akceleratora użyj przypisania urządzenia.
Aby umożliwić środowisku uruchomieniowemu NNAPI dostęp do obiektu AHardwareBuffer
, utwórz instancję ANeuralNetworksMemory
, wywołując funkcję ANeuralNetworksMemory_createFromAHardwareBuffer
i przekazując obiekt AHardwareBuffer
, jak pokazano w tym przykładowym kodzie:
// Configure and create AHardwareBuffer object AHardwareBuffer_Desc desc = ... AHardwareBuffer* ahwb = nullptr; AHardwareBuffer_allocate(&desc, &ahwb); // Create ANeuralNetworksMemory from AHardwareBuffer ANeuralNetworksMemory* mem2 = NULL; ANeuralNetworksMemory_createFromAHardwareBuffer(ahwb, &mem2);
Gdy NNAPI nie potrzebuje już dostępu do obiektu AHardwareBuffer
, zwalniaj odpowiednie wystąpienie ANeuralNetworksMemory
:
ANeuralNetworksMemory_free(mem2);
Uwaga:
- Parametru
AHardwareBuffer
możesz używać tylko w przypadku całego bufora; nie możesz go używać z parametremARect
. - Środowisko uruchomieniowe NNAPI nie będzie czyścić bufora. Zanim zaplanujesz wykonanie, musisz się upewnić, że bufor wejściowy i wyjściowy są dostępne.
- Nie ma obsługi deskryptorów plików bariery synchronizacji.
- W przypadku
AHardwareBuffer
z formatami i bitami użytkowania specyficznymi dla dostawcy to implementacja dostawcy decyduje, czy za czyszczenie pamięci podręcznej odpowiada klient, czy sterownik.
Model
Model jest podstawową jednostką obliczeniową w NNAPI. Każdy model jest definiowany przez co najmniej 1 operand i co najmniej 1 operację.
Operandy
Operandy to obiekty danych używane do definiowania wykresu. Obejmują one dane wejściowe i wyjściowe modelu, węzły pośrednie zawierające dane przepływające z jednej operacji do drugiej oraz stałe przekazywane do tych operacji.
Do modeli NNAPI można dodawać 2 typy operandów: skalary i tensory.
Wartość skalarna reprezentuje jedną wartość. NNAPI obsługuje wartości skalarne w formatach logicznym, 16-bitowym zmiennoprzecinkowym, 32-bitowym zmiennoprzecinkowym, 32-bitowym całkowitym i niezawierającym znaku 32-bitowym.
Większość operacji w NNAPI dotyczy tensorów. Tensory to tablice n-wymiarowe. NNAPI obsługuje tensory z 16-bitowymi wartościami zmiennoprzecinkowymi, 32-bitowymi wartościami zmiennoprzecinkowymi, 8-bitowymi wartościami kwantowanymi, 16-bitowymi wartościami kwantowanymi, 32-bitowymi wartościami całkowitymi i 8-bitowymi wartościami logicznymi.
Na przykład rysunek 3 przedstawia model z 2 działaniami: dodawaniem, a potem mnożeniem. Model przyjmuje wejściowy tensor i generuje jeden wyjściowy tensor.
Model powyżej ma 7 operandów. Te operandy są identyfikowane domyślnie na podstawie indeksu kolejności, w jakiej są dodawane do modelu. Pierwszy operand ma indeks 0, drugi – indeks 1 itd. Operandy 1, 2, 3, 5 są operandami stałymi.
Kolejność dodawania operandów nie ma znaczenia. Na przykład operand wyjściowy modelu może być dodany jako pierwszy. Ważne jest, aby podczas odwoływania się do operandu używać prawidłowej wartości indeksu.
Operandy mają typy. Są one określane podczas dodawania ich do modelu.
Operand nie może być używany jako dane wejściowe i dane wyjściowe modelu.
Każdy operand musi być albo wejściem modelu, stałą albo operandem wyjściowym dokładnie jednego działania.
Więcej informacji o używaniu operandów znajdziesz w artykule Więcej informacji o operandach.
Zarządzanie
Operacja określa obliczenia, które mają być wykonane. Każda operacja składa się z tych elementów:
- typ operacji (np. dodawanie, mnożenie, sprzężenie),
- lista indeksów operandów, których używa operacja jako danych wejściowych;
- lista indeksów operandów, których używa operacja do wygenerowania danych wyjściowych;
Kolejność na tych listach ma znaczenie. Informacje o oczekiwanych danych wejściowych i wyjściowych dla każdego typu operacji znajdziesz w dokumentacji NNAPI API Reference (w języku angielskim).
Przed dodaniem operacji do modelu musisz dodać do niego operandy, które operacja zużywa lub wytwarza.
Kolejność dodawania operacji nie ma znaczenia. NNAPI korzysta z zależności ustalonych przez obliczony graf operandów i operacji, aby określić kolejność wykonywania operacji.
Operacje obsługiwane przez NNAPI znajdziesz w tabeli poniżej:
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:
Aby zdefiniować pusty model, wywołaj funkcję
ANeuralNetworksModel_create()
.ANeuralNetworksModel* model = NULL; ANeuralNetworksModel_create(&model);
Dodaj operandy do modelu, wywołując funkcję
ANeuralNetworks_addOperand()
. Ich typy danych są definiowane za pomocą struktury danychANeuralNetworksOperandType
.// 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 6W 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()
iANeuralNetworksModel_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));W przypadku każdej operacji w kierunkowym grafu, którą chcesz obliczyć, dodaj operację do modelu, wywołując funkcję
ANeuralNetworksModel_addOperation()
.Jako parametry tego wywołania aplikacja musi podać:
- typ operacji,
- liczba wartości wejściowych,
- tablica indeksów dla danych wejściowych
- liczba wartości wyjściowych
- tablica indeksów dla wyjściowych operandów;
Pamiętaj, że operand nie może być używany zarówno jako argument wejściowy, jak i wyjściowy tej samej operacji.
// We have two operations in our example // The first consumes operands 1, 0, 2, and produces operand 4 uint32_t addInputIndexes[3] = {1, 0, 2}; uint32_t addOutputIndexes[1] = {4}; ANeuralNetworksModel_addOperation(model, ANEURALNETWORKS_ADD, 3, addInputIndexes, 1, addOutputIndexes);
// The second consumes operands 3, 4, 5, and produces operand 6 uint32_t multInputIndexes[3] = {3, 4, 5}; uint32_t multOutputIndexes[1] = {6}; ANeuralNetworksModel_addOperation(model, ANEURALNETWORKS_MUL, 3, multInputIndexes, 1, multOutputIndexes);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);
Opcjonalnie określ, czy
ANEURALNETWORKS_TENSOR_FLOAT32
może być obliczana z zakresem lub dokładnością tak niską jak w 16-bitowym formacie zmiennoprzecinkowym IEEE 754, wywołując funkcjęANeuralNetworksModel_relaxComputationFloat32toFloat16()
.Aby dokończyć definiowanie modelu, zadzwoń pod numer
ANeuralNetworksModel_finish()
. Jeśli nie ma błędów, funkcja zwraca kod wynikuANEURALNETWORKS_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:
Utwórz odpowiednie podgrafy wykonania (podgrafy
then
ielse
dla instrukcjiIF
oraz podgrafycondition
ibody
dla pętliWHILE
) jako samodzielne modeleANeuralNetworksModel*
:ANeuralNetworksModel* thenModel = makeThenModel(); ANeuralNetworksModel* elseModel = makeElseModel();
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);
Dodaj operację przepływu sterowania:
uint32_t inputs[] = {kConditionOperandIndex, kThenOperandIndex, kElseOperandIndex, kInput1, kInput2, kInput3}; uint32_t outputs[] = {kOutput1, kOutput2}; ANeuralNetworksModel_addOperation(model, ANEURALNETWORKS_IF, std::size(inputs), inputs, std::size(output), outputs);
Kompilacja
Etap kompilacji określa, na których procesorach będzie wykonywany Twój model, oraz prosi odpowiednie sterowniki o przygotowanie się do jego wykonania. Może to obejmować generowanie kodu maszynowego odpowiedniego dla procesorów, na których będzie działać model.
Aby skompilować model, wykonaj te czynności:
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.
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żą:
ANEURALNETWORKS_PREFER_LOW_POWER
: preferuj wykonywanie w sposób, który minimalizuje zużycie baterii. Jest to pożądane w przypadku kompilacji, które są często wykonywane.ANEURALNETWORKS_PREFER_FAST_SINGLE_ANSWER
: preferuj zwracanie pojedynczej odpowiedzi tak szybko, jak to możliwe, nawet jeśli spowoduje to większe zużycie energii. Jest to ustawienie domyślne.ANEURALNETWORKS_PREFER_SUSTAINED_SPEED
: preferuj maksymalizację przepustowości kolejnych klatek, np. podczas przetwarzania kolejnych klatek pochodzących z kamery.
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()
docacheDir
. Podanytoken
musi być niepowtarzalny dla każdego modelu w aplikacji.Aby zakończyć definiowanie kompilacji, wywołaj funkcję
ANeuralNetworksCompilation_finish()
. Jeśli nie ma błędów, funkcja zwraca kod wynikuANEURALNETWORKS_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 parametru ANeuralNetworks_getDeviceCount
, aby uzyskać liczbę dostępnych urządzeń. W przypadku każdego urządzenia użyj elementu ANeuralNetworks_getDevice
, aby ustawić wystąpienie elementu ANeuralNetworksDevice
jako odwołanie do tego urządzenia.
Gdy masz dane referencyjne urządzenia, możesz uzyskać o nim dodatkowe informacje, korzystając z tych funkcji:
ANeuralNetworksDevice_getFeatureLevel
ANeuralNetworksDevice_getName
ANeuralNetworksDevice_getType
ANeuralNetworksDevice_getVersion
Przypisanie urządzenia
Użyj opcji ANeuralNetworksModel_getSupportedOperationsForDevices
, aby sprawdzić, które operacje modelu można wykonywać na określonych urządzeniach.
Aby określić, których akceleratorów użyć podczas wykonywania, wywołaj funkcję ANeuralNetworksCompilation_createForDevices
zamiast ANeuralNetworksCompilation_create
.
Użyj powstałego obiektu ANeuralNetworksCompilation
w zwykły sposób.
Funkcja zwraca błąd, jeśli podany model zawiera operacje, które nie są obsługiwane przez wybrane urządzenia.
Jeśli podano wiele urządzeń, środowisko uruchomieniowe odpowiada za rozłożenie pracy na te urządzenia.
Podobnie jak w przypadku innych urządzeń implementacja procesora NNAPI jest reprezentowana przez element ANeuralNetworksDevice
o nazwie nnapi-reference
i typie ANEURALNETWORKS_DEVICE_TYPE_CPU
. Podczas wywoływania funkcji ANeuralNetworksCompilation_createForDevices
implementacja procesora nie jest używana do obsługi przypadków niepowodzenia kompilacji i wykonania modelu.
Za podział modelu na modele podrzędne, które mogą działać na określonych urządzeniach, odpowiada aplikacja. Aplikacje, które nie wymagają ręcznego partycjonowania, powinny nadal wywoływać prostszą funkcję ANeuralNetworksCompilation_create
, aby używać wszystkich dostępnych urządzeń (w tym procesora), aby przyspieszyć model. 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 zadań. 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 model został podzielony przez NNAPI, sprawdź logi Androida, aby znaleźć wiadomość (na poziomie INFO z tagiem ExecutionPlan
):
ModelBuilder::findBestDeviceForEachOperation(op-name): device-index
op-name
to nazwa opisowa operacji na wykresie, a device-index
to indeks urządzenia docelowego na liście urządzeń.
Ta lista jest wartością wejściową przekazywaną do funkcji ANeuralNetworksCompilation_createForDevices
. Jeśli używasz funkcji ANeuralNetworksCompilation_createForDevices
, lista urządzeń jest zwracana podczas iteracji po wszystkich urządzeniach za pomocą funkcji ANeuralNetworks_getDeviceCount
i ANeuralNetworks_getDevice
.
Komunikat (na poziomie INFO z tagiem ExecutionPlan
):
ModelBuilder::partitionTheWork: only one best device: device-name
Ta wiadomość informuje, że cały wykres został przyspieszony na urządzeniu device-name
.
Realizacja
Etap wykonania stosuje model do zestawu danych wejściowych i przechowuje wyniki obliczeń w co najmniej 1 buforze użytkownika lub przestrzeni pamięci przydzielonej przez aplikację.
Aby wykonać skompilowany model:
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);
Określ, gdzie aplikacja odczytuje wartości wejściowe do obliczeń. Aplikacja może odczytywać wartości wejściowe z bufora użytkownika lub z zarezerwowanego obszaru pamięci, odpowiednio wywołując funkcję
ANeuralNetworksExecution_setInput()
lubANeuralNetworksExecution_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));
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()
lubANeuralNetworksExecution_setOutputFromMemory()
.// Set the output float32 myOutput[3][4]; ANeuralNetworksExecution_setOutput(run1, 0, NULL, myOutput, sizeof(myOutput));
Zaplanuj rozpoczęcie wykonania, wywołując funkcję
ANeuralNetworksExecution_startCompute()
. Jeśli nie ma błędów, funkcja zwraca kod wynikuANEURALNETWORKS_NO_ERROR
.// Starts the work. The work proceeds asynchronously ANeuralNetworksEvent* run1_end = NULL; ANeuralNetworksExecution_startCompute(run1, &run1_end);
Aby poczekać na zakończenie wykonywania, wywołaj funkcję
ANeuralNetworksEvent_wait()
. Jeśli wykonanie się powiedzie, funkcja zwróci kod wynikuANEURALNETWORKS_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);
Opcjonalnie możesz zastosować do skompilowanego modelu inny zestaw danych wejściowych, używając tej samej instancji kompilacji do utworzenia nowej instancji
ANeuralNetworksExecution
.// Apply the compiled model to a different set of inputs ANeuralNetworksExecution* run2; ANeuralNetworksExecution_create(compilation, &run2); ANeuralNetworksExecution_setInput(run2, ...); ANeuralNetworksExecution_setOutput(run2, ...); ANeuralNetworksEvent* run2_end = NULL; ANeuralNetworksExecution_startCompute(run2, &run2_end); ANeuralNetworksEvent_wait(run2_end); ANeuralNetworksEvent_free(run2_end); ANeuralNetworksExecution_free(run2);
Wykonywanie synchroniczne
Wykonanie asynchroniczne wymaga czasu na tworzenie i synchronizowanie wątków. Ponadto opóźnienie może się znacznie różnić, a najdłuższe opóźnienia mogą sięgać nawet 500 mikrosekund między momentem powiadomienia lub przebudzenia wątku a momentem, w którym zostaje ono ostatecznie przypisane do rdzenia procesora.
Aby zmniejszyć opóźnienie, możesz zamiast tego 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ć
ANeuralNetworksExecution_startCompute
aby asynchronicznie wywołać inferencję w czasie działania, aplikacja wywołuje
ANeuralNetworksExecution_compute
aby wywołać synchronicznie czas działania. Rozmowa z ANeuralNetworksExecution_compute
nie wymaga ANeuralNetworksEvent
i nie jest połączona z rozmową z ANeuralNetworksEvent_wait
.
Wykonywanie burst
Na urządzeniach z Androidem 10 (poziom interfejsu API 29) lub nowszym interfejs NNAPI obsługuje wykonywanie serii za pomocą obiektu ANeuralNetworksBurst
. Wykonywanie w ciągu to sekwencja wykonywania tej samej kompilacji, która występuje w szybkiej kolejności, np. w przypadku ujęć z filmowania lub kolejnych próbek audio. Korzystanie z obiektów ANeuralNetworksBurst
może przyspieszyć wykonywanie kodu, ponieważ wskazuje akceleratorom, że zasoby mogą być ponownie użyte między wykonaniami i że akceleratory powinny pozostać w stanie wysokiej wydajności przez cały czas działania.
ANeuralNetworksBurst
wprowadza tylko niewielką zmianę w normalnej ścieżce wykonywania. Obiekt burst tworzysz za pomocą funkcji ANeuralNetworksBurst_create
, jak w tym fragmencie kodu:
// Create burst object to be reused across a sequence of executions ANeuralNetworksBurst* burst = NULL; ANeuralNetworksBurst_create(compilation, &burst);
Burstowe wykonania są synchroniczne. Zamiast jednak używać parametru ANeuralNetworksExecution_compute
do wykonywania poszczególnych wnioskowań, w wywołaniach funkcji ANeuralNetworksExecution_burstCompute
należy sparować różne obiekty ANeuralNetworksExecution
z 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()
. Gdy używasz tej metody, przed rozpoczęciem oceny wykonanie oczekuje na sygnał wszystkich zależnych zdarzeń. Gdy wykonanie zostanie zakończone, a dane wyjściowe będą gotowe do użycia, zwrócone zdarzenie zostanie zgłoszone.
W zależności od tego, które urządzenia obsługują wykonanie, zdarzenie może być obsługiwane przez furtkę synchronizacji. Musisz wywołać funkcję ANeuralNetworksEvent_wait()
, aby zaczekać na zdarzenie i odzyskać zasoby użyte podczas wykonania. Możesz importować bariery synchronizacji do obiektu zdarzenia za pomocą elementu ANeuralNetworksEvent_createFromSyncFenceFd()
, a także eksportować bariery synchronizacji z obiektu zdarzenia za pomocą elementu ANeuralNetworksEvent_getSyncFenceFd()
.
Dane wyjściowe o dynamicznym rozmiarze
Aby obsługiwać modele, w których przypadku rozmiar danych wyjściowych zależy od danych wejściowych (czyli rozmiar nie może być określony w momencie wykonywania modelu), użyj wartości ANeuralNetworksExecution_getOutputOperandRank
i ANeuralNetworksExecution_getOutputOperandDimensions
.
Poniższy przykładowy kod pokazuje, jak to zrobić:
// Get the rank of the output uint32_t myOutputRank = 0; ANeuralNetworksExecution_getOutputOperandRank(run1, 0, &myOutputRank); // Get the dimensions of the output std::vector<uint32_t> myOutputDimensions(myOutputRank); ANeuralNetworksExecution_getOutputOperandDimensions(run1, 0, myOutputDimensions.data());
Uporządkuj
Krok czyszczenia zajmuje się zwalnianiem zasobów wewnętrznych używanych do obliczeń.
// Cleanup ANeuralNetworksCompilation_free(compilation); ANeuralNetworksModel_free(model); ANeuralNetworksMemory_free(mem1);
Zarządzanie błędami i awaryjne użycie procesora
Jeśli wystąpi błąd podczas partycjonowania, jeśli skompilowanie modelu (lub jego części) przez sterownik się nie powiedzie lub jeśli sterownik nie wykona skompilowanego modelu (lub jego części), NNAPI może przejść na własną implementację operacji na procesorze.
Jeśli klient NNAPI zawiera zoptymalizowane wersje operacji (np. TFLite), korzystne może być wyłączenie korzystania z procesora w przypadku awarii i obsługa błędów za pomocą zoptymalizowanej implementacji operacji klienta.
W Androidzie 10, jeśli kompilacja jest wykonywana za pomocą ANeuralNetworksCompilation_createForDevices
, awaryjne użycie procesora zostanie wyłączone.
W Androidzie P wykonanie NNAPI jest przekierowywane na procesor, jeśli wykonanie w sterowniku zakończy się niepowodzeniem.
Dotyczy to też Androida 10, gdy używana jest metoda ANeuralNetworksCompilation_create
zamiast ANeuralNetworksCompilation_createForDevices
.
Pierwsze wykonanie jest wykonywane na tej jednej partycji, a jeśli i wtedy się nie powiedzie, cały model jest ponownie wykonywany na procesorze.
Jeśli podział na partycje lub kompilacja się nie powiedzie, cały model zostanie przesłany do procesora.
W niektórych przypadkach niektóre operacje nie są obsługiwane przez procesor. W takich sytuacjach kompilacja lub wykonanie nie powiedzie się, a nie zostanie użyta metoda zastępcza.
Nawet po wyłączeniu zastępczego użycia procesora w modelu mogą nadal występować operacje zaplanowane na procesorze. Jeśli procesor znajduje się na liście procesorów podanych do ANeuralNetworksCompilation_createForDevices
i jest jedynym procesorem, który obsługuje te operacje, lub jest procesorem, który zapewnia najlepszą wydajność w przypadku tych operacji, zostanie wybrany jako główny (a nie zapasowy) procesor wykonawczy.
Aby mieć pewność, że nie ma wykonania na procesorze, użyj ANeuralNetworksCompilation_createForDevices
, wykluczając nnapi-reference
z listy urządzeń.
Od Androida P można wyłączyć alternatywne wykonanie w czasie wykonywania wersji DEBUG, ustawiając właściwość debug.nn.partition
na 2.
Domeny wspomnień
W Androidzie 11 i nowszych NNAPI obsługuje domeny pamięci, które udostępniają interfejsy alokacji dla nieprzezroczystych pamięci. Dzięki temu aplikacje mogą przekazywać pamięć natywnej implementacji na urządzeniu w ramach kolejnych wywołań, aby NNAPI nie kopiował ani nie przekształcał danych niepotrzebnie podczas wykonywania kolejnych wywołań na tym samym sterowniku.
Funkcja domeny pamięci jest przeznaczona 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 stanu 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ą:
Aby utworzyć nowy deskryptor pamięci, wywołaj funkcję
ANeuralNetworksMemoryDesc_create()
:// Create a memory descriptor ANeuralNetworksMemoryDesc* desc; ANeuralNetworksMemoryDesc_create(&desc);
Określ wszystkie żądane role danych wejściowych i wyjściowych, wywołując metody
ANeuralNetworksMemoryDesc_addInputRole()
iANeuralNetworksMemoryDesc_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);
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);
Aby zakończyć definiowanie opisu, wywołaj funkcję
ANeuralNetworksMemoryDesc_finish()
.ANeuralNetworksMemoryDesc_finish(desc);
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);
zwalniać opis pamięci, gdy nie jest już potrzebny.
ANeuralNetworksMemoryDesc_free(desc);
Klient może używać utworzonego obiektu ANeuralNetworksMemory
tylko w stosunku do obiektu ANeuralNetworksExecution_setInputFromMemory()
lub ANeuralNetworksExecution_setOutputFromMemory()
zgodnie z rolami określonymi w obiekcie ANeuralNetworksMemoryDesc
. Argumenty offset i length muszą być ustawione na 0, co oznacza, że używana jest cała pamięć. Klient może też jawnie ustawić lub wyodrębnić zawartość pamięci, używając ANeuralNetworksMemory_copy()
.
Możesz tworzyć nieprzezroczyste wspomnienia z rolami o nieokreślonych wymiarach lub rangach.
W takim przypadku tworzenie pamięci może się nie udać i otrzymać stan ANEURALNETWORKS_OP_FAILED
, jeśli nie jest obsługiwane przez podrzędny sterownik. Zachęcamy klienta do zaimplementowania logiki zapasowej poprzez 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 wykonywania
Jeśli chcesz określić łączny czas wykonania za pomocą czasu wykonywania, możesz użyć interfejsu API do wykonania synchronicznego i zmierzyć czas trwania wywołania. Jeśli chcesz określić łączny czas wykonania na niższym poziomie stosu oprogramowania, możesz użyć funkcji ANeuralNetworksExecution_setMeasureTiming
i ANeuralNetworksExecution_getDuration
, aby uzyskać:
- czas wykonywania na akceleratorze (nie w sterowniku, który działa na procesorze hosta);
- czas wykonywania w sterowniku, w tym czas przyspieszania.
Czas wykonywania w sterowniku nie uwzględnia obciążenia związanego ze środowiskiem wykonawczym ani interfejsu IPC potrzebnego do komunikacji środowiska wykonawczego ze sterownikiem.
Te interfejsy API mierzą czas między zdarzeniami przesłania i zakończenia pracy, a nie czas poświęcony przez kierowcę lub akcelerator na wykonanie wnioskowania, które może zostać przerwane przez przełączanie kontekstu.
Jeśli na przykład rozpocznie się wnioskowanie 1, a następnie sterownik zatrzyma pracę, aby wykonać wnioskowanie 2, a potem wznowi i dokończy wnioskowanie 1, czas wykonania wnioskowania 1 będzie obejmował czas, w którym praca została wstrzymana, aby wykonać wnioskowanie 2.
Te informacje o czasie mogą być przydatne w przypadku wdrożenia produkcyjnego aplikacji w celu zbierania danych telemetrycznych na potrzeby korzystania offline. Na podstawie danych o czasie możesz zmodyfikować aplikację, aby zwiększyć jej wydajność.
Podczas korzystania z tej funkcji pamiętaj o tych kwestiach:
- Zbieranie informacji o czasie trwania może mieć wpływ na wydajność.
- Tylko sterownik może obliczyć czas spędzony w samym sterowniku lub akceleratorze, z wyjątkiem czasu spędzonego w czasie działania NNAPI i w IPC.
- Z tych interfejsów API możesz korzystać tylko w przypadku
ANeuralNetworksExecution
utworzonego za pomocąANeuralNetworksCompilation_createForDevices
z użyciemnumDevices = 1
. - Nie jest wymagana obecność kierowcy, aby zgłosić informacje o czasie.
Profilowanie aplikacji za pomocą narzędzia Android Systrace
Począwszy od Androida 10, NNAPI automatycznie generuje zdarzenia systrace, których możesz używać do profilowania aplikacji.
NNAPI Source zawiera narzędzie parse_systrace
do przetwarzania zdarzeń systrace wygenerowanych przez aplikację oraz generowania widoku tabeli pokazującego czas spędzony na różnych etapach cyklu życia modelu (tworzenie, przygotowanie, kompilacja, wykonanie i zakończenie) oraz na różnych poziomach aplikacji. Aplikacja jest podzielona na te warstwy:
Application
: główny kod aplikacjiRuntime
: środowisko wykonawcze NNAPIIPC
: komunikacja między procesami NNAPI Runtime a kodem sterownikaDriver
: 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:
- Uruchom śledzenie systrace na Androidzie za pomocą tego polecenia:
$ANDROID_BUILD_TOP/external/chromium-trace/systrace.py -o trace.html -a org.tensorflow.lite.examples.classification nnapi hal freq sched idle load binder_driver
Parametr -o trace.html
wskazuje, że ścieżki zostaną zapisane w pliku trace.html
. Podczas profilowania własnej aplikacji musisz zastąpić org.tensorflow.lite.examples.classification
nazwą procesu podaną w manifeście aplikacji.
To sprawi, że jedna z konsol powłoki będzie zajęta. Nie uruchamiaj polecenia w tle, ponieważ będzie ono czekać na zakończenie procesu enter
.
- Po uruchomieniu zbieracza systrace uruchom aplikację i przeprowadź test porównawczy.
W naszym przypadku aplikację Image Classification możesz uruchomić 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.
Po zakończeniu testu zakończ śledzenie systrace, naciskając
enter
w terminalu konsoli aktywnym od kroku 1.Uruchom narzędzie
systrace_parser
, aby wygenerować skumulowane statystyki:
$ANDROID_BUILD_TOP/frameworks/ml/nn/tools/systrace_parser/parse_systrace.py --total-times trace.html
Parsowanie akceptuje te parametry:
- --total-times
: pokazuje łączny czas spędzony na warstwie, w tym czas oczekiwania na wykonanie wywołania do warstwy docelowej
- --print-detail
: wypisuje wszystkie zdarzenia zebrane z systrace
- --per-execution
: wypisuje tylko wykonanie i jego podfazy (jako czasy wykonania) zamiast statystyk dla wszystkich faz
- --json
: generuje dane wyjściowe w formacie JSON
Przykład danych wyjściowych:
===========================================================================================================================================
NNAPI timing summary (total time, ms wall-clock) Execution
----------------------------------------------------
Initialization Preparation Compilation I/O Compute Results Ex. total Termination Total
-------------- ----------- ----------- ----------- ------------ ----------- ----------- ----------- ----------
Application n/a 19.06 1789.25 n/a n/a 6.70 21.37 n/a 1831.17*
Runtime - 18.60 1787.48 2.93 11.37 0.12 14.42 1.32 1821.81
IPC 1.77 - 1781.36 0.02 8.86 - 8.88 - 1792.01
Driver 1.04 - 1779.21 n/a n/a n/a 7.70 - 1787.95
Total 1.77* 19.06* 1789.25* 2.93* 11.74* 6.70* 21.37* 1.32* 1831.17*
===========================================================================================================================================
* This total ignores missing (n/a) values and thus is not necessarily consistent with the rest of the numbers
Jeśli zebrane zdarzenia nie stanowią pełnego śladu aplikacji, analizator może nie działać. W szczególności może się to zdarzyć, jeśli w śladzie występują zdarzenia systrace służące do oznaczania końca sekcji, ale nie ma powiązanego z nimi zdarzenia rozpoczęcia sekcji. Zwykle dzieje się tak, jeśli podczas uruchamiania zbieracza systrace generowane są niektóre zdarzenia z poprzedniej sesji profilowania. W takim przypadku musisz ponownie przeprowadzić profilowanie.
Dodawanie statystyk kodu aplikacji do danych wyjściowych programu systrace_parser
Aplikacja parse_systrace korzysta z wbudowanej funkcji systrace w Androidzie. Możesz dodawać ścieżki dla określonych operacji w aplikacji za pomocą interfejsu systrace API (w przypadku Javy lub aplikacji natywnych) z niestandardowymi nazwami zdarzeń.
Aby powiązać zdarzenia niestandardowe z etapami cyklu życia aplikacji, dołącz do nazwy zdarzenia jeden z tych ciągów znaków:
[NN_LA_PI]
: zdarzenie na poziomie aplikacji dotyczące inicjalizacji[NN_LA_PP]
: zdarzenie na poziomie aplikacji dotyczące przygotowania[NN_LA_PC]
: zdarzenie na poziomie aplikacji dotyczące kompilacji[NN_LA_PE]
: zdarzenie na poziomie aplikacji dotyczące wykonania
Oto przykład tego, jak możesz zmienić kod przykładowego przykładowego kodu klasyfikacji obrazów TFLite, dodając sekcję runInferenceModel
dla fazy Execution
i warstwy Application
zawierającej inne sekcje preprocessBitmap
, które nie będą uwzględniane w śladach NNAPI. Sekcja runInferenceModel
będzie częścią zdarzeń systrace przetwarzanych przez parsownik systrace nnapi:
Kotlin
/** Runs inference and returns the classification results. */ fun recognizeImage(bitmap: Bitmap): List{ // This section won’t appear in the NNAPI systrace analysis Trace.beginSection("preprocessBitmap") convertBitmapToByteBuffer(bitmap) Trace.endSection() // Run the inference call. // Add this method in to NNAPI systrace analysis. Trace.beginSection("[NN_LA_PE]runInferenceModel") long startTime = SystemClock.uptimeMillis() runInference() long endTime = SystemClock.uptimeMillis() Trace.endSection() ... return recognitions }
Java
/** Runs inference and returns the classification results. */ public ListrecognizeImage(final Bitmap bitmap) { // This section won’t appear in the NNAPI systrace analysis Trace.beginSection("preprocessBitmap"); convertBitmapToByteBuffer(bitmap); Trace.endSection(); // Run the inference call. // Add this method in to NNAPI systrace analysis. Trace.beginSection("[NN_LA_PE]runInferenceModel"); long startTime = SystemClock.uptimeMillis(); runInference(); long endTime = SystemClock.uptimeMillis(); Trace.endSection(); ... Trace.endSection(); return recognitions; }
Jakość usługi
W Androidzie 11 i nowszych interfejs NNAPI umożliwia uzyskanie lepszej jakości usług (QoS), ponieważ pozwala aplikacji wskazać względną ważność swoich modeli, maksymalny czas oczekiwania na przygotowanie danego modelu i maksymalny czas oczekiwania na zakończenie danego obliczenia. Android 11 wprowadza też dodatkowe kody wyników NNAPI, które umożliwiają aplikacjom rozpoznawanie błędów, takich jak przekroczenie limitu czasu wykonania.
Ustawianie priorytetu zadania
Aby ustawić priorytet zadania NNAPI, przed wywołaniem funkcji ANeuralNetworksCompilation_finish()
wywołaj funkcję ANeuralNetworksCompilation_setPriority()
.
Ustawianie terminów
W przypadku aplikacji można ustawić terminy zarówno kompilacji, jak i wykonywania wnioskowania na podstawie modelu.
- Aby ustawić limit czasu kompilacji, przed wywołaniem funkcji
ANeuralNetworksCompilation_finish()
wywołaj funkcjęANeuralNetworksCompilation_setTimeout()
. - Aby ustawić czas oczekiwania na wnioskowanie, wywołaj funkcję
ANeuralNetworksExecution_setTimeout()
przed rozpoczęciem kompilacji.
Więcej informacji o operandach
W następnej sekcji omówimy zaawansowane tematy dotyczące operandów.
tensory zaokrąglone;
Kwantyzacja tensora to zwięzły sposób na reprezentowanie n-wymiarowej tablicy wartości zmiennoprzecinkowych.
NNAPI obsługuje 8-bitowe niesymetryczne tensory kwantowe. W przypadku tych tensorów wartość każdej komórki jest reprezentowana przez 8-bitową liczbę całkowitą. Z tensorem powiązana jest skala i wartość punktu zerowego. Służy on do konwertowania 8-bitowych liczb całkowitych na wartości zmiennoprzecinkowe, które są reprezentowane.
Formuła:
(cellValue - zeroPoint) * scale
gdzie wartość zeroPoint jest 32-bitową liczbą całkowitą, a wartość scale jest 32-bitową wartością zmiennoprzecinkową.
W porównaniu z tensorami o 32-bitowych wartościach zmiennoprzecinkowych 8-bitowe tensory z kwantyzacją mają 2 zalety:
- Aplikacja jest mniejsza, ponieważ wytrenowane wagi zajmują ćwierć rozmiaru 32-bitowych tensorów.
- Obliczenia mogą być często wykonywane szybciej. Wynika to z mniejszej ilości danych, które trzeba pobrać z pamięci, oraz z wydajności procesorów, takich jak DSP, w obliczeniach całkowitoliczbowych.
Chociaż można przekonwertować model zmiennoprzecinkowy na model kwantyzowany, nasze doświadczenie pokazuje, że lepsze wyniki uzyskuje się, trenując bezpośrednio model kwantyzowany. W efekcie sieć neuronowa uczy się kompensować zwiększoną szczegółowość poszczególnych wartości. W przypadku każdego zaokrąglonego tensora wartości skali i zera są określane podczas procesu trenowania.
W NNAPI typy zaokrąglonych tensorów definiujesz, ustawiając pole typu struktury danych ANeuralNetworksOperandType
na ANEURALNETWORKS_TENSOR_QUANT8_ASYMM
.
W tej strukturze danych musisz też określić skalę i wartość zeroPoint tensora.
Oprócz 8-bitowych niesymetrycznych tensorów z kwantyzacją NNAPI obsługuje też:
ANEURALNETWORKS_TENSOR_QUANT8_SYMM_PER_CHANNEL
, które możesz wykorzystać do reprezentowania wag w operacjachCONV/DEPTHWISE_CONV/TRANSPOSED_CONV
.ANEURALNETWORKS_TENSOR_QUANT16_ASYMM
, którego możesz użyć do wewnętrznego stanuQUANTIZED_16BIT_LSTM
.ANEURALNETWORKS_TENSOR_QUANT8_SYMM
, która może być wartością wejściową dlaANEURALNETWORKS_DEQUANTIZE
.
Operandy opcjonalne
Niektóre operacje, takie jak ANEURALNETWORKS_LSH_PROJECTION
, przyjmują opcjonalne operandy. Aby wskazać w modelu, że opcjonalny operand został pominięty, wywołaj funkcję ANeuralNetworksModel_setOperandValue()
, podając wartość NULL
dla bufora i 0 dla długości.
Jeśli decyzja o tym, czy operand jest obecny, zmienia się w przypadku każdego wykonania, możesz wskazać, że operand jest pominięty, używając funkcji ANeuralNetworksExecution_setInput()
lub ANeuralNetworksExecution_setOutput()
, przekazując NULL
jako bufor i 0 jako długość.
Tensory o nieznanej rangi
Android 9 (poziom interfejsu API 28) wprowadził modele 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 do testów porównawczych) 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:
Podłącz docelowe urządzenie z Androidem do komputera, otwórz okno terminala i upewnij się, że urządzenie jest dostępne przez ADB.
Jeśli połączonych jest więcej niż 1 urządzenie z Androidem, wyeksportuj zmienną środowiskową
ANDROID_SERIAL
urządzenia docelowego.Przejdź do najwyższego katalogu źródłowego Androida.
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 modelucompilation
: generowanie planu wykonania modelu i kompilacjiexecution
: wykonanie modelucpuexe
: wykonywanie operacji przy użyciu implementacji NNAPI na procesorzemanager
: rozszerzenia NNAPI, dostępne interfejsy i informacje o funkcjachall
lub1
: wszystkie elementy powyżej.
Aby na przykład włączyć pełne logowanie szczegółowe, użyj polecenia adb shell setprop debug.nn.vlog all
. Aby wyłączyć szczegółowe logowanie, użyj polecenia adb shell setprop debug.nn.vlog '""'
.
Po włączeniu szczegółowe rejestrowanie generuje wpisy logów na poziomie INFO z tagiem ustawionym na nazwę fazy lub komponentu.
Oprócz wiadomości kontrolowanych przez debug.nn.vlog
komponenty interfejsu NNAPI udostępniają inne wpisy dziennika na różnych poziomach, z których każdy używa określonego znacznika dziennika.
Aby uzyskać listę komponentów, przeszukaj drzewo źródłowe za pomocą tego wyrażenia:
grep -R 'define LOG_TAG' | awk -F '"' '{print $2}' | sort -u | egrep -v "Sample|FileTag|test"
To wyrażenie zwraca obecnie te tagi:
- BurstBuilder
- Wywołania zwrotne
- CompilationBuilder
- CpuExecutor
- ExecutionBuilder
- ExecutionBurstController
- ExecutionBurstServer
- ExecutionPlan
- FibonacciDriver
- GraphDump
- IndexedShapeWrapper
- IonWatcher
- Menedżer
- Pamięć
- MemoryUtils
- MetaModel
- ModelArgumentInfo
- ModelBuilder
- NeuralNetworks
- OperationResolver
- Zarządzanie
- OperationsUtils
- PackageInfo
- TokenHasher
- TypeManager
- Utils
- ValidateHal
- VersionedInterfaces
Aby kontrolować poziom wiadomości dziennika wyświetlanych przez logcat
, użyj zmiennej środowiskowej ANDROID_LOG_TAGS
.
Aby wyświetlić pełny zestaw komunikatów NNAPI i wyłączyć wszystkie inne, ustaw wartość parametru ANDROID_LOG_TAGS
na:
BurstBuilder:V Callbacks:V CompilationBuilder:V CpuExecutor:V ExecutionBuilder:V ExecutionBurstController:V ExecutionBurstServer:V ExecutionPlan:V FibonacciDriver:V GraphDump:V IndexedShapeWrapper:V IonWatcher:V Manager:V MemoryUtils:V Memory:V MetaModel:V ModelArgumentInfo:V ModelBuilder:V NeuralNetworks:V OperationResolver:V OperationsUtils:V Operations:V PackageInfo:V TokenHasher:V TypeManager:V Utils:V ValidateHal:V VersionedInterfaces:V *:S.
Możesz ustawić ANDROID_LOG_TAGS
za pomocą tego polecenia:
export ANDROID_LOG_TAGS=$(grep -R 'define LOG_TAG' | awk -F '"' '{ print $2 ":V" }' | sort -u | egrep -v "Sample|FileTag|test" | xargs echo -n; echo ' *:S')
Pamiętaj, że jest to tylko filtr, który dotyczy logcat
. Aby wygenerować obszerne informacje dziennika, musisz ustawić właściwość debug.nn.vlog
na all
.