Android Neural Networks API (NNAPI) to interfejs API Androida w języku C, który umożliwia wykonywanie wymagających obliczeniowo operacji uczenia maszynowego na urządzeniach z Androidem. Interfejs NNAPI został zaprojektowany tak, aby zapewniać podstawową warstwę funkcjonalności dla platform uczenia maszynowego wyższego poziomu, takich jak TensorFlow Lite i Caffe2, które tworzą i trenują sieci neuronowe. Interfejs API jest dostępny na wszystkich urządzeniach z Androidem 8.1 (interfejs API na poziomie 27) lub nowszym, ale został wycofany w Androidzie 15.
NNAPI obsługuje wnioskowanie przez stosowanie danych z urządzeń z Androidem do wcześniej wytrenowanych modeli zdefiniowanych przez dewelopera. Przykłady wnioskowania obejmują klasyfikowanie obrazów, przewidywanie zachowań użytkowników i wybieranie odpowiednich odpowiedzi na zapytanie.
Wnioskowanie na urządzeniu ma wiele zalet:
- Czas oczekiwania: nie musisz wysyłać żądania przez połączenie sieciowe i czekać na odpowiedź. Może to mieć kluczowe znaczenie na przykład w przypadku aplikacji wideo, które przetwarzają kolejne klatki z kamery.
- Dostępność: aplikacja działa nawet wtedy, gdy jesteś poza zasięgiem sieci.
- Szybkość: nowy sprzęt przeznaczony do przetwarzania sieci neuronowych zapewnia znacznie szybsze obliczenia niż sam procesor do zwykłych obciążeń.
- Prywatność: dane nie opuszczają urządzenia z Androidem.
- Koszt: gdy wszystkie obliczenia są wykonywane na urządzeniu z Androidem, nie jest potrzebna farma serwerów.
Deweloper powinien też pamiętać o pewnych kompromisach:
- Wykorzystanie systemu: ocena sieci neuronowych wymaga wielu obliczeń, co może zwiększyć zużycie baterii. Jeśli jest to problem w przypadku Twojej aplikacji, zwłaszcza w przypadku długotrwałych obliczeń, rozważ monitorowanie stanu baterii.
- Rozmiar aplikacji: zwróć uwagę na rozmiar modeli. Modele mogą zajmować kilka megabajtów miejsca. Jeśli umieszczenie dużych modeli w pliku APK miałoby negatywny wpływ na użytkowników, rozważ pobieranie modeli po zainstalowaniu aplikacji, używanie mniejszych modeli lub wykonywanie 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 Android Neural Networks API.
Informacje o środowisku wykonawczym interfejsu Neural Networks API
NNAPI jest przeznaczony do wywoływania przez biblioteki, platformy i narzędzia uczenia maszynowego, które umożliwiają deweloperom trenowanie modeli poza urządzeniem i wdrażanie ich na urządzeniach z Androidem. Aplikacje zwykle nie używają NNAPI bezpośrednio, ale korzystają z platform uczenia maszynowego wyższego poziomu. Te platformy mogą z kolei używać NNAPI do wykonywania operacji wnioskowania z akceleracją sprzętową na obsługiwanych urządzeniach.
W zależności od wymagań aplikacji i możliwości sprzętowych urządzenia z Androidem środowisko wykonawcze sieci neuronowej Androida może efektywnie rozdzielać obciążenie obliczeniowe między dostępne procesory na urządzeniu, w tym dedykowany sprzęt sieci neuronowej, procesory graficzne (GPU) i procesory sygnałowe (DSP).
W przypadku urządzeń z Androidem, które nie mają specjalistycznego sterownika dostawcy, środowisko wykonawcze NNAPI wykonuje żądania na procesorze.
Rysunek 1 przedstawia ogólną architekturę systemu NNAPI.
Model programowania interfejsu Neural Networks API
Aby wykonywać obliczenia za pomocą NNAPI, musisz najpierw utworzyć graf skierowany, który definiuje obliczenia do wykonania. Ten graf obliczeniowy w połączeniu z danymi wejściowymi (np. wagami i odchyleniami przekazanymi z platformy uczenia maszynowego) tworzy model do oceny środowiska wykonawczego NNAPI.
NNAPI używa 4 głównych abstrakcji:
- Model: wykres obliczeniowy operacji matematycznych i stałych wartości poznanych w procesie trenowania. Te operacje są specyficzne dla sieci neuronowych. Obejmują one m.in. dwuwymiarowy (2D) splot, aktywację logistyczną (sigmoidalną) i aktywację prostoliniową (ReLU). Tworzenie modelu jest operacją synchroniczną.
Po utworzeniu można go używać w różnych wątkach i kompilacjach.
W NNAPI model jest reprezentowany jako instancja
ANeuralNetworksModel. - Kompilacja: reprezentuje konfigurację kompilacji modelu NNAPI na kod niższego poziomu. Tworzenie kompilacji jest operacją synchroniczną. Po utworzeniu można go używać w różnych wątkach i wykonaniach. W NNAPI każda kompilacja jest reprezentowana jako instancja
ANeuralNetworksCompilation. - Pamięć: reprezentuje pamięć współdzieloną, pliki mapowane w pamięci i podobne buforowanie pamięci. Użycie bufora pamięci umożliwia środowisku wykonawczemu NNAPI bardziej efektywne przesyłanie danych do sterowników. Aplikacja zwykle tworzy 1 bufor pamięci współdzielonej, który zawiera wszystkie tensory potrzebne do zdefiniowania modelu. Możesz też używać buforów pamięci do przechowywania danych wejściowych i wyjściowych instancji wykonania. W NNAPI każdy bufor pamięci jest reprezentowany jako instancja
ANeuralNetworksMemory. Execution: interfejs do stosowania modelu NNAPI do zestawu danych wejściowych i zbierania wyników. Wykonywanie może odbywać się synchronicznie lub asynchronicznie.
W przypadku wykonywania asynchronicznego wiele wątków może czekać na to samo wykonanie. Po zakończeniu wykonywania wszystkie wątki zostaną zwolnione.
W NNAPI każde wykonanie jest reprezentowane jako instancja
ANeuralNetworksExecution.
Rysunek 2 przedstawia podstawowy przepływ programowania.
W pozostałej części tej sekcji opisano czynności, które należy wykonać, aby skonfigurować model NNAPI do wykonywania obliczeń, skompilować model i uruchomić skompilowany model.
Zapewnianie dostępu do danych treningowych
Wytrenowane wagi i dane stronnicze są prawdopodobnie przechowywane w pliku. Aby zapewnić środowisku wykonawczemu NNAPI wydajny dostęp do tych danych, utwórz instancję ANeuralNetworksMemory, wywołując funkcję ANeuralNetworksMemory_createFromFd() i przekazując deskryptor otwartego pliku danych. Określasz też flagi ochrony pamięci i przesunięcie, od którego 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 używamy tylko jednej instancji ANeuralNetworksMemory dla wszystkich wag, można użyć więcej niż jednej instancji ANeuralNetworksMemory dla wielu plików.
Używaj natywnych buforów sprzętowych
Możesz używać natywnych buforów sprzętowych do danych wejściowych i wyjściowych modelu oraz wartości stałych operandów. W niektórych przypadkach akcelerator NNAPI może uzyskać 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ć je wszystkie. Z powodu tego ograniczenia zapoznaj się z ograniczeniami wymienionymi w dokumentacji referencyjnej ANeuralNetworksMemory_createFromAHardwareBuffer i przetestuj wcześniej na urządzeniach docelowych, czy kompilacje i wykonania, które używają AHardwareBuffer, działają zgodnie z oczekiwaniami, używając przypisania urządzenia do określenia akceleratora.
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 musi już uzyskiwać dostępu do obiektu AHardwareBuffer, zwolnij odpowiednią instancję ANeuralNetworksMemory:
ANeuralNetworksMemory_free(mem2);
Uwaga:
- Możesz używać
AHardwareBuffertylko w przypadku całego bufora. Nie możesz używać tego parametru z parametremARect. - Środowisko wykonawcze NNAPI nie opróżni bufora. Przed zaplanowaniem wykonania musisz się upewnić, że bufory wejściowe i wyjściowe są dostępne.
- Deskryptory plików barier synchronizacji nie są obsługiwane.
- W przypadku
AHardwareBufferz formatami i bitami użycia specyficznymi dla dostawcy to od implementacji dostawcy zależy, czy za opróżnianie pamięci podręcznej odpowiada klient czy sterownik.
Model
Model jest podstawową jednostką obliczeniową w NNAPI. Każdy model jest definiowany przez co najmniej 1 operand i operację.
Operandy
Operandy to obiekty danych używane do definiowania grafu. Obejmują one dane wejściowe i wyjściowe modelu, węzły pośrednie zawierające dane, które przepływają z jednej operacji do drugiej, oraz stałe przekazywane do tych operacji.
Do modeli NNAPI można dodawać 2 rodzaje operandów: skalary i tensory.
Skalar reprezentuje pojedynczą wartość. NNAPI obsługuje wartości skalarne w formatach wartości logicznych, 16-bitowych zmiennoprzecinkowych, 32-bitowych zmiennoprzecinkowych, 32-bitowych liczb całkowitych i 32-bitowych liczb całkowitych bez znaku.
Większość operacji w NNAPI obejmuje tensory. Tensory to tablice n-wymiarowe. NNAPI obsługuje tensory z 16-bitowymi i 32-bitowymi wartościami zmiennoprzecinkowymi, 8-bitowymi i 16-bitowymi wartościami skwantyzowanymi, 32-bitowymi liczbami całkowitymi oraz 8-bitowymi wartościami logicznymi.
Na przykład rysunek 3 przedstawia model z 2 operacjami: dodawaniem, po którym następuje mnożenie. Model pobiera tensor wejściowy i generuje jeden tensor wyjściowy.
Model powyżej ma 7 operandów. Te operandy są identyfikowane pośrednio przez indeks kolejności, w jakiej są dodawane do modelu. Pierwszy dodany operand ma indeks 0, drugi – indeks 1 itd. Operandy 1, 2, 3 i 5 są stałe.
Kolejność, w jakiej dodajesz operandy, nie ma znaczenia. Na przykład operand danych wyjściowych 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 do modelu.
Nie można używać operandu jednocześnie jako danych wejściowych i wyjściowych modelu.
Każdy operand musi być danymi wejściowymi modelu, stałą lub operandem wyjściowym dokładnie jednej operacji.
Więcej informacji o używaniu operandów znajdziesz w artykule Więcej informacji o operandach.
Operacje
Operacja określa obliczenia, które mają zostać wykonane. Każda operacja składa się z tych elementów:
- typ operacji (np. dodawanie, mnożenie, splot);
- lista indeksów operandów, których operacja używa jako danych wejściowych, oraz
- lista indeksów operandów, których operacja używa do generowania 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 interfejsu NNAPI.
Zanim dodasz operację, musisz dodać do modelu operandy, które są przez nią wykorzystywane lub generowane.
Kolejność dodawania operacji nie ma znaczenia. NNAPI opiera się na zależnościach ustanowionych przez wykres obliczeniowy operandów i operacji, aby określić kolejność wykonywania operacji.
Operacje obsługiwane przez NNAPI znajdziesz w tabeli poniżej:
Znany problem na poziomie API 28: podczas przekazywania tensorów ANEURALNETWORKS_TENSOR_QUANT8_ASYMM do operacji ANEURALNETWORKS_PAD, która jest dostępna na Androidzie 9 (poziom API 28) i nowszych, dane wyjściowe z NNAPI mogą nie odpowiadać danym wyjściowym z platform 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.
Tworzenie modeli
W tym przykładzie tworzymy model z 2 operacjami, który jest widoczny na rysunku 3.
Aby utworzyć model, wykonaj te czynności:
Wywołaj funkcję
ANeuralNetworksModel_create(), aby zdefiniować pusty model.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, które mają stałe wartości, takich jak wagi i odchylenia, które aplikacja uzyskuje w procesie trenowania, użyj funkcji
ANeuralNetworksModel_setOperandValue()iANeuralNetworksModel_setOperandValueFromMemory().W tym przykładzie ustawiamy stałe wartości z pliku danych treningowych odpowiadające buforowi pamięci utworzonemu 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));W przypadku każdej operacji w grafie skierowanym, którą chcesz obliczyć, dodaj ją do modelu, wywołując funkcję
ANeuralNetworksModel_addOperation().W wywołaniu aplikacja musi podać te parametry:
- typ operacji,
- liczba wartości wejściowych,
- tablica indeksów operandów wejściowych,
- liczbę wartości wyjściowych,
- tablica indeksów operandów wyjściowych,
Pamiętaj, że operandu nie można używać zarówno do danych wejściowych, jak i wyjściowych tej samej operacji.
// We have two operations in our example // The first consumes operands 1, 0, 2, and produces operand 4 uint32_t addInputIndexes[3] = {1, 0, 2}; uint32_t addOutputIndexes[1] = {4}; ANeuralNetworksModel_addOperation(model, ANEURALNETWORKS_ADD, 3, addInputIndexes, 1, addOutputIndexes);
// The second consumes operands 3, 4, 5, and produces operand 6 uint32_t multInputIndexes[3] = {3, 4, 5}; uint32_t multOutputIndexes[1] = {6}; ANeuralNetworksModel_addOperation(model, ANEURALNETWORKS_MUL, 3, multInputIndexes, 1, multOutputIndexes);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 możesz określić, czy
ANEURALNETWORKS_TENSOR_FLOAT32może być obliczany z zakresem lub precyzją tak niską jak w przypadku 16-bitowej reprezentacji zmiennoprzecinkowej IEEE 754, wywołującANeuralNetworksModel_relaxComputationFloat32toFloat16().Wywołaj
ANeuralNetworksModel_finish(), aby sfinalizować definicję modelu. Jeśli nie wystąpią żadne błędy, ta funkcja zwraca kod wynikuANEURALNETWORKS_NO_ERROR.ANeuralNetworksModel_finish(model);
Po utworzeniu modelu możesz go skompilować dowolną liczbę razy i wykonać każdą kompilację dowolną liczbę razy.
Kontrola przepływu
Aby uwzględnić przepływ sterowania w modelu NNAPI, wykonaj te czynności:
Utwórz odpowiednie podgrafy wykonania (podgrafy
thenielsedla instrukcjiIF, podgrafyconditionibodydla pętliWHILE) jako samodzielne modeleANeuralNetworksModel*:ANeuralNetworksModel* thenModel = makeThenModel(); ANeuralNetworksModel* elseModel = makeElseModel();
Utwórz operandy, które odwołują się do tych modeli w modelu zawierającym przepływ sterowania:
ANeuralNetworksOperandType modelType = { .type = ANEURALNETWORKS_MODEL, }; ANeuralNetworksModel_addOperand(model, &modelType); // kThenOperandIndex ANeuralNetworksModel_addOperand(model, &modelType); // kElseOperandIndex ANeuralNetworksModel_setOperandValueFromModel(model, kThenOperandIndex, &thenModel); ANeuralNetworksModel_setOperandValueFromModel(model, kElseOperandIndex, &elseModel);
Dodaj operację przepływu sterowania:
uint32_t inputs[] = {kConditionOperandIndex, kThenOperandIndex, kElseOperandIndex, kInput1, kInput2, kInput3}; uint32_t outputs[] = {kOutput1, kOutput2}; ANeuralNetworksModel_addOperation(model, ANEURALNETWORKS_IF, std::size(inputs), inputs, std::size(output), outputs);
Kompilacja
Etap kompilacji określa, na których procesorach będzie wykonywany model, i prosi odpowiednie sterowniki o przygotowanie się do jego wykonania. Może to obejmować generowanie kodu maszynowego dostosowanego do procesorów, na których będzie działać model.
Aby skompilować model, wykonaj te czynności:
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 chcesz wykonywać operacje.
Możesz opcjonalnie wpływać na to, jak środowisko wykonawcze równoważy zużycie baterii i szybkość wykonywania. Możesz to zrobić, dzwoniąc pod numer
ANeuralNetworksCompilation_setPreference().// Ask to optimize for low power consumption ANeuralNetworksCompilation_setPreference(compilation, ANEURALNETWORKS_PREFER_LOW_POWER);
Możesz określić te preferencje:
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 jak najszybsze zwrócenie pojedynczej odpowiedzi, 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 z aparatu.
Opcjonalnie możesz skonfigurować buforowanie kompilacji, wywołując funkcję
ANeuralNetworksCompilation_setCaching.// Set up compilation caching ANeuralNetworksCompilation_setCaching(compilation, cacheDir, token);
Użyj
getCodeCacheDir()w przypadkucacheDir. Określona wartośćtokenmusi być unikalna dla każdego modelu w aplikacji.Sfinalizuj definicję kompilacji, wywołując
ANeuralNetworksCompilation_finish(). Jeśli nie ma błędów, ta 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 NNAPI udostępnia funkcje, które umożliwiają bibliotekom i aplikacjom platformy uczenia maszynowego uzyskiwanie informacji o dostępnych urządzeniach i określanie urządzeń, które mają być używane do wykonywania operacji. Dzięki informacjom o dostępnych urządzeniach aplikacje mogą pobierać dokładną wersję sterowników znalezionych na urządzeniu, aby uniknąć znanych niezgodności. Dzięki możliwości określenia, które urządzenia mają wykonywać różne sekcje modelu, aplikacje można zoptymalizować pod kątem urządzenia z Androidem, na którym są wdrażane.
Wykrywanie urządzeń
Użyj ANeuralNetworks_getDeviceCount, aby uzyskać liczbę dostępnych urządzeń. W przypadku każdego urządzenia użyj ANeuralNetworks_getDevice, aby ustawić instancję ANeuralNetworksDevice jako odniesienie do tego urządzenia.
Gdy masz już odwołanie do urządzenia, możesz uzyskać dodatkowe informacje o nim, korzystając z tych funkcji:
ANeuralNetworksDevice_getFeatureLevelANeuralNetworksDevice_getNameANeuralNetworksDevice_getTypeANeuralNetworksDevice_getVersion
Przypisanie urządzenia
Użyj ANeuralNetworksModel_getSupportedOperationsForDevices, aby dowiedzieć się, które operacje modelu można uruchamiać na określonych urządzeniach.
Aby określić, których akceleratorów chcesz używać do wykonywania, wywołaj ANeuralNetworksCompilation_createForDevices zamiast ANeuralNetworksCompilation_create.
Użyj wynikowego obiektu ANeuralNetworksCompilation w normalny sposób.
Jeśli podany model zawiera operacje, które nie są obsługiwane przez wybrane urządzenia, funkcja zwraca błąd.
Jeśli określono kilka urządzeń, środowisko wykonawcze odpowiada za rozdzielenie pracy między nie.
Podobnie jak w przypadku innych urządzeń implementacja procesora NNAPI jest reprezentowana przez ANeuralNetworksDevice o nazwie nnapi-reference i typie ANEURALNETWORKS_DEVICE_TYPE_CPU. Podczas wywoływania ANeuralNetworksCompilation_createForDevices implementacja procesora nie jest używana do obsługi przypadków niepowodzenia kompilacji i wykonywania modelu.
Aplikacja odpowiada za podzielenie modelu na podmodele, które mogą działać na określonych urządzeniach. Aplikacje, które nie wymagają ręcznego partycjonowania, powinny nadal wywoływać prostszą metodę ANeuralNetworksCompilation_create, aby używać wszystkich dostępnych urządzeń (w tym procesora) do przyspieszania modelu. Jeśli model nie może być w pełni obsługiwany przez urządzenia określone za pomocą ANeuralNetworksCompilation_createForDevices, zwracany jest błąd ANEURALNETWORKS_BAD_DATA.
Partycjonowanie modelu
Gdy dla modelu dostępnych jest kilka urządzeń, środowisko wykonawcze NNAPI rozdziela pracę między nie. Jeśli na przykład do ANeuralNetworksCompilation_createForDevices podano więcej niż 1 urządzenie, podczas przydzielania pracy zostaną uwzględnione wszystkie określone urządzenia. Pamiętaj, że jeśli procesora nie ma na liście, wykonywanie na nim będzie wyłączone. Gdy używasz ANeuralNetworksCompilation_create,
uwzględniane są wszystkie dostępne urządzenia, w tym procesor.
Dystrybucja odbywa się przez wybranie z listy dostępnych urządzeń dla każdej operacji w modelu urządzenia, które obsługuje daną operację i zapewnia najlepszą wydajność, czyli najkrótszy czas wykonania lub najniższe zużycie energii, w zależności od preferencji wykonania określonych przez klienta. Ten algorytm 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 (jawnie przy użyciu ANeuralNetworksCompilation_createForDevices lub niejawnie przy użyciu ANeuralNetworksCompilation_create) ważne jest profilowanie wynikowej aplikacji.
Aby dowiedzieć się, jak model został podzielony przez NNAPI, sprawdź logi Androida pod kątem komunikatu (na poziomie INFO z tagiem ExecutionPlan):
ModelBuilder::findBestDeviceForEachOperation(op-name): device-index
op-name to opisowa nazwa operacji na wykresie, a device-index to indeks urządzenia kandydującego na liście urządzeń.
Ta lista jest danymi wejściowymi przekazywanymi do ANeuralNetworksCompilation_createForDevices lub, jeśli używasz ANeuralNetworksCompilation_createForDevices, listą urządzeń zwracaną podczas iteracji po wszystkich urządzeniach za pomocą ANeuralNetworks_getDeviceCount i ANeuralNetworks_getDevice.
Komunikat (na poziomie INFO z tagiem ExecutionPlan):
ModelBuilder::partitionTheWork: only one best device: device-name
Ten komunikat oznacza, że cały graf został przyspieszony na urządzeniu device-name.
Wykonanie
Krok wykonywania stosuje model do zestawu danych wejściowych i zapisuje wyniki obliczeń w co najmniej 1 buforze użytkownika lub w przestrzeni pamięci przydzielonej przez aplikację.
Aby wykonać skompilowany model, wykonaj te czynności:
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);
Określ, skąd aplikacja ma odczytywać wartości wejściowe do obliczeń. Aplikacja może odczytywać wartości wejściowe z bufora użytkownika lub przydzielonego miejsca w pamięci, wywołując odpowiednio 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 przydzielonym obszarze pamięci, wywołując odpowiednio funkcję
ANeuralNetworksExecution_setOutput()lubANeuralNetworksExecution_setOutputFromMemory().// Set the output float32 myOutput[3][4]; ANeuralNetworksExecution_setOutput(run1, 0, NULL, myOutput, sizeof(myOutput));
Zaplanuj rozpoczęcie wykonywania, wywołując funkcję
ANeuralNetworksExecution_startCompute(). Jeśli nie ma błędów, ta funkcja zwraca kod wynikuANEURALNETWORKS_NO_ERROR.// Starts the work. The work proceeds asynchronously ANeuralNetworksEvent* run1_end = NULL; ANeuralNetworksExecution_startCompute(run1, &run1_end);
Wywołaj funkcję
ANeuralNetworksEvent_wait(), aby poczekać na zakończenie wykonywania. Jeśli wykonanie się powiedzie, funkcja zwróci kod wynikuANEURALNETWORKS_NO_ERROR. Oczekiwanie może odbywać się w innym wątku niż ten, który rozpoczyna 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ć inny zestaw danych wejściowych do skompilowanego modelu, używając tej samej instancji kompilacji do utworzenia nowej instancji
ANeuralNetworksExecution.// Apply the compiled model to a different set of inputs ANeuralNetworksExecution* run2; ANeuralNetworksExecution_create(compilation, &run2); ANeuralNetworksExecution_setInput(run2, ...); ANeuralNetworksExecution_setOutput(run2, ...); ANeuralNetworksEvent* run2_end = NULL; ANeuralNetworksExecution_startCompute(run2, &run2_end); ANeuralNetworksEvent_wait(run2_end); ANeuralNetworksEvent_free(run2_end); ANeuralNetworksExecution_free(run2);
Wykonywanie synchroniczne
Wykonywanie asynchroniczne wymaga czasu na utworzenie i zsynchronizowanie wątków. Ponadto opóźnienie może być bardzo zmienne, a najdłuższe opóźnienia mogą wynosić nawet 500 mikrosekund między momentem powiadomienia lub wybudzenia wątku a momentem, w którym zostanie on ostatecznie powiązany z rdzeniem procesora.
Aby zmniejszyć opóźnienie, możesz zamiast tego skierować aplikację do wykonania synchronicznego wywołania wnioskowania do środowiska wykonawczego. To wywołanie zwróci wynik dopiero po zakończeniu wnioskowania, a nie po jego rozpoczęciu. Zamiast wywoływać funkcję ANeuralNetworksExecution_startCompute w celu asynchronicznego wywołania wnioskowania do środowiska wykonawczego, aplikacja wywołuje funkcję ANeuralNetworksExecution_compute, aby wykonać wywołanie synchroniczne. Wywołanie funkcji ANeuralNetworksExecution_compute nie przyjmuje argumentu ANeuralNetworksEvent i nie jest sparowane z wywołaniem funkcji ANeuralNetworksEvent_wait.
Wykonania impulsowe
Na urządzeniach z Androidem 10 (poziom interfejsu API 29) i nowszym NNAPI obsługuje wykonywanie serii za pomocą obiektu ANeuralNetworksBurst. Wykonywanie seryjne to sekwencja wykonań tej samej kompilacji, które następują po sobie w szybkim tempie, np. operacje na klatkach z kamery lub kolejnych próbkach dźwięku. Używanie obiektów ANeuralNetworksBurst może przyspieszyć wykonywanie, ponieważ wskazują one akceleratorom, że zasoby mogą być ponownie używane między wykonaniami i że akceleratory powinny pozostać w stanie wysokiej wydajności przez cały czas trwania serii.
ANeuralNetworksBurst wprowadza tylko niewielką zmianę w normalnej ścieżce wykonywania. Obiekt serii tworzy się za pomocą metody 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);
Wykonywanie serii jest synchroniczne. Jednak zamiast używać ANeuralNetworksExecution_compute do wykonywania poszczególnych wnioskowań, łączysz różne obiekty ANeuralNetworksExecution z tym samym obiektem 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 będzie już potrzebny.
// Cleanup ANeuralNetworksBurst_free(burst);
Asynchroniczne kolejki poleceń i wykonywanie w izolacji
W Androidzie 11 i nowszych NNAPI obsługuje dodatkowy sposób planowania wykonywania asynchronicznego za pomocą metody ANeuralNetworksExecution_startComputeWithDependencies(). Gdy używasz tej metody, wykonanie czeka na zasygnalizowanie wszystkich zależnych zdarzeń przed rozpoczęciem oceny. Po zakończeniu wykonywania i przygotowaniu danych wyjściowych do wykorzystania zwracane zdarzenie jest sygnalizowane.
W zależności od tego, które urządzenia obsługują wykonanie, zdarzenie może być obsługiwane przez barierę synchronizacji. Musisz wywołać ANeuralNetworksEvent_wait(), aby poczekać na zdarzenie i odzyskać zasoby użyte podczas wykonywania. Możesz zaimportować bariery synchronizacji do obiektu zdarzenia za pomocą ANeuralNetworksEvent_createFromSyncFenceFd() i wyeksportować je z obiektu zdarzenia za pomocą ANeuralNetworksEvent_getSyncFenceFd().
Dane wyjściowe o rozmiarze dynamicznym
Aby obsługiwać modele, w których rozmiar danych wyjściowych zależy od danych wejściowych, czyli nie można go określić w momencie wykonywania modelu, użyj 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());
Czyszczenie
Krok czyszczenia obsługuje zwalnianie zasobów wewnętrznych używanych do obliczeń.
// Cleanup ANeuralNetworksCompilation_free(compilation); ANeuralNetworksModel_free(model); ANeuralNetworksMemory_free(mem1);
Zarządzanie błędami i procesor zastępczy
Jeśli podczas partycjonowania wystąpi błąd, jeśli sterownik nie skompiluje (fragmentu) modelu lub jeśli sterownik nie wykona skompilowanego (fragmentu) modelu, NNAPI może powrócić do własnej implementacji procesora dla co najmniej jednej operacji.
Jeśli klient NNAPI zawiera zoptymalizowane wersje operacji (np. TFLite), korzystne może być wyłączenie zastępczego procesora i obsługa błędów za pomocą zoptymalizowanej implementacji operacji klienta.
Jeśli w Androidzie 10 kompilacja jest wykonywana za pomocą ANeuralNetworksCompilation_createForDevices, zastępcze użycie procesora będzie wyłączone.
W Androidzie P wykonywanie NNAPI wraca do procesora, jeśli nie powiedzie się w sterowniku.
Dotyczy to również Androida 10, gdy używana jest metoda ANeuralNetworksCompilation_create, a nie ANeuralNetworksCompilation_createForDevices.
Pierwsze wykonanie jest wycofywane w przypadku tej pojedynczej partycji, a jeśli to nadal nie działa, cały model jest ponownie uruchamiany na procesorze.
Jeśli partycjonowanie lub kompilacja się nie powiedzie, cały model zostanie wypróbowany na procesorze.
W niektórych przypadkach pewne operacje nie są obsługiwane na procesorze i w takich sytuacjach kompilacja lub wykonanie nie powiedzie się, zamiast wrócić do poprzedniego stanu.
Nawet po wyłączeniu zastępczego procesora w modelu mogą nadal występować operacje zaplanowane na procesorze. Jeśli procesor znajduje się na liście procesorów dostarczonych 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 (niezastępczy) wykonawca.
Aby mieć pewność, że nie będzie wykonywania na procesorze, 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 do debugowania, 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 udostępniają interfejsy alokatora dla pamięci nieprzezroczystych. Dzięki temu aplikacje mogą przekazywać pamięć natywną urządzenia między wykonaniami, dzięki czemu NNAPI nie kopiuje ani nie przekształca niepotrzebnie danych podczas kolejnych wykonań na tym samym sterowniku.
Funkcja domeny pamięci jest przeznaczona dla tensorów, które są głównie wewnętrzne dla sterownika i nie wymagają częstego dostępu po stronie klienta. Przykładami takich tensorów są tensory stanu w modelach sekwencyjnych. W przypadku tensorów, które wymagają częstego dostępu do procesora po stronie klienta, używaj współdzielonych pul pamięci.
Aby przydzielić nieprzezroczystą pamięć, wykonaj te czynności:
Wywołaj funkcję
ANeuralNetworksMemoryDesc_create(), aby utworzyć nowy deskryptor pamięci:// Create a memory descriptor ANeuralNetworksMemoryDesc* desc; ANeuralNetworksMemoryDesc_create(&desc);
Określ wszystkie zamierzone role wejściowe i wyjściowe, wywołując funkcje
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);
Zakończ definiowanie deskryptora, wywołując funkcję
ANeuralNetworksMemoryDesc_finish().ANeuralNetworksMemoryDesc_finish(desc);
Przydziel tyle pamięci, ile potrzebujesz, przekazując deskryptor do
ANeuralNetworksMemory_createFromDesc().// Allocate two opaque memories with the descriptor ANeuralNetworksMemory* opaqueMem; ANeuralNetworksMemory_createFromDesc(desc, &opaqueMem);
Zwolnij deskryptor pamięci, gdy nie będzie już potrzebny.
ANeuralNetworksMemoryDesc_free(desc);
Klient może używać utworzonego obiektu ANeuralNetworksMemory tylko z ANeuralNetworksExecution_setInputFromMemory() lub ANeuralNetworksExecution_setOutputFromMemory() zgodnie z rolami określonymi w obiekcie ANeuralNetworksMemoryDesc. Argumenty przesunięcia i długości muszą być ustawione na 0, co oznacza, ż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ć nieprzezroczyste pamięci z rolami o nieokreślonych wymiarach lub rangach.
W takim przypadku tworzenie pamięci może się nie powieść i wyświetli się stan ANEURALNETWORKS_OP_FAILED, jeśli nie jest on obsługiwany przez podstawowy sterownik. Zalecamy, aby klient wdrożył logikę kreacji zastępczej, przydzielając wystarczająco duży bufor obsługiwany przez Ashmem lub AHardwareBuffer w trybie BLOB.
Gdy NNAPI nie musi już uzyskiwać dostępu do nieprzezroczystego obiektu pamięci, zwolnij odpowiednią instancję ANeuralNetworksMemory:
ANeuralNetworksMemory_free(opaqueMem);
Pomiar wyników
Możesz ocenić wydajność aplikacji, mierząc czas wykonywania lub profilując.
Czas wykonywania
Jeśli chcesz określić łączny czas wykonywania przez środowisko wykonawcze, możesz użyć interfejsu API do wykonywania synchronicznego i zmierzyć czas potrzebny na wywołanie. 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 na akceleratorze (nie w sterowniku, który działa na procesorze hosta).
- czas wykonywania w sterowniku, w tym czas na akceleratorze.
Czas wykonywania w sterowniku nie obejmuje narzutu, takiego jak narzut środowiska wykonawczego i narzut IPC potrzebny do komunikacji środowiska wykonawczego ze sterownikiem.
Te interfejsy API mierzą czas między zdarzeniami przesłania i zakończenia zadania, a nie czas, jaki sterownik lub akcelerator poświęca na wykonanie wnioskowania, który może być przerywany przez przełączanie kontekstu.
Jeśli na przykład rozpocznie się wnioskowanie 1, a następnie sterownik przerwie pracę, aby wykonać wnioskowanie 2, po czym wznowi i zakończy wnioskowanie 1, czas wykonywania wnioskowania 1 będzie obejmował czas, w którym praca została przerwana w celu wykonania wnioskowania 2.
Te informacje o czasie mogą być przydatne w przypadku wdrożenia produkcyjnego aplikacji, aby zbierać dane telemetryczne do użytku offline. Możesz użyć danych o czasie, aby zmodyfikować aplikację i zwiększyć jej wydajność.
Podczas korzystania z tej funkcji pamiętaj o tych kwestiach:
- Zbieranie informacji o czasie może mieć wpływ na wydajność.
- Tylko sterownik może obliczyć czas spędzony w nim lub w akceleratorze, z wyłączeniem czasu spędzonego w środowisku wykonawczym NNAPI i w IPC.
- Tych interfejsów API możesz używać tylko z
ANeuralNetworksExecutionutworzonym za pomocąANeuralNetworksCompilation_createForDevicesznumDevices = 1. - Nie jest wymagany żaden sterownik, aby można było raportować informacje o czasie.
Profilowanie aplikacji za pomocą Android Systrace
Począwszy od Androida 10, NNAPI automatycznie generuje zdarzenia systrace, których możesz używać do profilowania aplikacji.
Źródło NNAPI zawiera narzędzie parse_systrace do przetwarzania zdarzeń systrace generowanych przez aplikację i generowania widoku tabeli pokazującego czas spędzony w różnych fazach cyklu życia modelu (tworzenie instancji, przygotowanie, kompilacja, wykonanie i zakończenie) oraz w różnych warstwach aplikacji. Warstwy, na które jest podzielona aplikacja:
Application: główny kod aplikacji,Runtime: środowisko wykonawcze NNAPIIPC: komunikacja między procesami między środowiskiem wykonawczym NNAPI a kodem sterownika.Driver: proces sterownika akceleratora.
Wygeneruj dane analizy profilowania
Zakładając, że drzewo źródłowe AOSP zostało pobrane w $ANDROID_BUILD_TOP, a jako aplikacji docelowej używasz przykładu klasyfikacji obrazów TFLite, możesz wygenerować dane profilowania NNAPI, wykonując te czynności:
- Uruchom narzędzie Android systrace 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 logi czasu będą zapisywane w trace.html. Podczas profilowania własnej aplikacji musisz zastąpić org.tensorflow.lite.examples.classification nazwą procesu określoną w manifeście aplikacji.
To polecenie zajmie jedną z konsol powłoki. Nie uruchamiaj go w tle, ponieważ interaktywnie czeka na zakończenie działania enter.
- Po uruchomieniu kolektora systrace uruchom aplikację i przeprowadź test porównawczy.
W naszym przypadku możesz uruchomić aplikację Image Classification z poziomu Android Studio lub bezpośrednio z interfejsu telefonu testowego, jeśli aplikacja została już zainstalowana. Aby wygenerować 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 działanie narzędzia systrace, naciskając
enterw 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
Parser akceptuje te parametry:
- --total-times: pokazuje łączny czas spędzony w warstwie, w tym czas oczekiwania na wykonanie wywołania warstwy bazowej.
- --print-detail: wyświetla wszystkie zdarzenia zebrane z systrace.
- --per-execution: wyświetla tylko wykonanie i jego podfazy (jako czasy wykonania) zamiast statystyk wszystkich faz.
- --json: generuje dane wyjściowe w formacie JSON.
Poniżej znajduje się 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
Parser może nie działać, jeśli zebrane zdarzenia nie reprezentują pełnego śledzenia aplikacji. Może się to zdarzyć, jeśli zdarzenia systrace wygenerowane w celu oznaczenia końca sekcji są obecne w śladzie bez powiązanego zdarzenia rozpoczęcia sekcji. Zwykle dzieje się tak, gdy podczas uruchamiania kolektora systrace generowane są zdarzenia z poprzedniej sesji profilowania. W takim przypadku musisz ponownie przeprowadzić profilowanie.
Dodawanie statystyk kodu aplikacji do danych wyjściowych systrace_parser
Aplikacja parse_systrace jest oparta na wbudowanej funkcji systrace na Androidzie. Możesz dodawać logi czasu dla konkretnych operacji w aplikacji, korzystając z interfejsu systrace API (w przypadku Javy, w przypadku aplikacji natywnych) z niestandardowymi nazwami zdarzeń.
Aby powiązać zdarzenia niestandardowe z etapami cyklu życia aplikacji, dodaj na początku nazwy zdarzenia jeden z tych ciągów znaków:
[NN_LA_PI]: zdarzenie na poziomie aplikacji dotyczące inicjowania.[NN_LA_PP]: zdarzenie na poziomie aplikacji dotyczące przygotowania[NN_LA_PC]: zdarzenie na poziomie aplikacji dotyczące kompilacji[NN_LA_PE]: zdarzenie na poziomie aplikacji dotyczące wykonania.
Oto przykład, jak możesz zmienić przykładowy kod klasyfikacji obrazów TFLite, dodając sekcję runInferenceModel dla fazy Execution i warstwę Application zawierającą inne sekcje preprocessBitmap, które nie będą uwzględniane w śladach NNAPI. Sekcja runInferenceModel będzie częścią zdarzeń systrace przetwarzanych przez parser 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; }
Quality of Service
W Androidzie 11 i nowszych NNAPI zapewnia lepszą jakość usług (QoS), umożliwiając aplikacji wskazywanie względnych priorytetów jej modeli, maksymalnego czasu oczekiwanego na przygotowanie danego modelu i maksymalnego czasu oczekiwanego na wykonanie danego obliczenia. Android 11 wprowadza też dodatkowe kody wyników NNAPI, które umożliwiają aplikacjom rozpoznawanie błędów, takich jak niedotrzymanie terminów wykonania.
Ustawianie priorytetu zadania
Aby ustawić priorytet zbioru zadań NNAPI, wywołaj ANeuralNetworksCompilation_setPriority() przed wywołaniem ANeuralNetworksCompilation_finish().
Ustawianie terminów
Aplikacje mogą wyznaczać terminy zarówno kompilacji, jak i wnioskowania modelu.
- Aby ustawić limit czasu kompilacji, wywołaj
ANeuralNetworksCompilation_setTimeout()przed wywołaniemANeuralNetworksCompilation_finish(). - Aby ustawić limit czasu wnioskowania, wywołaj
ANeuralNetworksExecution_setTimeout()przed rozpoczęciem kompilacji.
Więcej informacji o operandach
W tej sekcji znajdziesz zaawansowane informacje o używaniu operandów.
Kwantyzowane tensory
Tensor skwantyzowany 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 tensorem powiązana jest skala i wartość punktu zerowego. Służą one do konwertowania 8-bitowych liczb całkowitych na reprezentowane wartości zmiennoprzecinkowe.
Zastosowany wzór to:
(cellValue - zeroPoint) * scale
gdzie wartość zeroPoint jest 32-bitową liczbą całkowitą, a skala jest 32-bitową wartością zmiennoprzecinkową.
W porównaniu z tensorami 32-bitowych wartości zmiennoprzecinkowych tensory skwantyzowane 8-bitowo mają 2 zalety:
- Aplikacja jest mniejsza, ponieważ wytrenowane wagi zajmują 1/4 rozmiaru 32-bitowych tensorów.
- Obliczenia można często wykonywać szybciej. Wynika to z mniejszej ilości danych, które trzeba pobrać z pamięci, oraz z wydajności procesorów, takich jak DSP, w wykonywaniu obliczeń na liczbach całkowitych.
Chociaż można przekonwertować model zmiennoprzecinkowy na model skwantyzowany, nasze doświadczenie pokazuje, że lepsze wyniki można osiągnąć, trenując bezpośrednio model skwantyzowany. W efekcie sieć neuronowa uczy się kompensować zwiększoną szczegółowość każdej wartości. W przypadku każdego skwantyzowanego tensora wartości skali i punktu zerowego są określane podczas procesu trenowania.
W NNAPI typy skwantyzowanych tensorów definiuje się, ustawiając pole typu struktury danych ANeuralNetworksOperandType na ANEURALNETWORKS_TENSOR_QUANT8_ASYMM.
W tej strukturze danych określasz też skalę i wartość zeroPoint tensora.
Oprócz 8-bitowych asymetrycznych kwantyzowanych tensorów NNAPI obsługuje:
ANEURALNETWORKS_TENSOR_QUANT8_SYMM_PER_CHANNEL, którego możesz używać do reprezentowania wag w operacjachCONV/DEPTHWISE_CONV/TRANSPOSED_CONV.ANEURALNETWORKS_TENSOR_QUANT16_ASYMM, którego możesz użyć do określenia stanu wewnętrznegoQUANTIZED_16BIT_LSTM.ANEURALNETWORKS_TENSOR_QUANT8_SYMM, które mogą być danymi wejściowymi dlaANEURALNETWORKS_DEQUANTIZE.
Opcjonalne operandy
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(), przekazując NULL jako bufor i 0 jako długość.
Jeśli decyzja o tym, czy operand jest obecny, różni się w przypadku każdego wykonania, możesz wskazać, że operand jest pomijany, używając funkcji ANeuralNetworksExecution_setInput() lub ANeuralNetworksExecution_setOutput() i przekazując NULL jako bufor i 0 jako długość.
Tensory o nieznanej randze
W Androidzie 9 (poziom API 28) wprowadzono operandy modelu o nieznanych wymiarach, ale znanej randze (liczbie wymiarów). W Androidzie 10 (poziom interfejsu API 29) wprowadzono 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).
Test porównawczy ocenia czas oczekiwania i dokładność oraz porównuje sterowniki z tą samą pracą wykonywaną przy użyciu TensorFlow Lite na procesorze w przypadku tych samych modeli i zbiorów danych.
Aby użyć testu porównawczego:
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 podłączonych jest więcej urządzeń z Androidem, wyeksportuj zmienną środowiskową urządzenia docelowego
ANDROID_SERIAL.Przejdź do katalogu źródłowego najwyższego poziomu 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 logach systemowych. Aby przeanalizować logi, użyj narzędzia logcat.
Włącz szczegółowe rejestrowanie NNAPI dla określonych faz lub komponentów, ustawiając właściwość debug.nn.vlog (za pomocą adb shell) na następującą listę wartości rozdzielonych spacją, dwukropkiem lub przecinkiem:
model: tworzenie modelucompilation: generowanie planu wykonania modelu i kompilacjaexecution: wykonywanie modelucpuexe: wykonywanie operacji przy użyciu implementacji procesora NNAPImanager: rozszerzenia NNAPI, dostępne interfejsy i informacje o możliwościachalllub1: wszystkie powyższe elementy.
Aby na przykład włączyć pełne logowanie szczegółowe, użyj polecenia adb shell setprop debug.nn.vlog all. Aby wyłączyć logowanie szczegółowe, użyj polecenia adb shell setprop debug.nn.vlog '""'.
Po włączeniu szczegółowego logowania generowane są wpisy logu na poziomie INFO z tagiem ustawionym na nazwę fazy 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 używa 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 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
- Operacje
- OperationsUtils
- PackageInfo
- TokenHasher
- TypeManager
- Narzędzia
- ValidateHal
- VersionedInterfaces
Aby kontrolować poziom komunikatów logu wyświetlanych przez 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:
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. Nadal musisz ustawić właściwość debug.nn.vlog na all, aby generować szczegółowe informacje dziennika.