Android Neural Networks API (NNAPI) to interfejs API Androida C przeznaczony do uruchamiania intensywnych operacji obliczeniowych na potrzeby systemów uczących się na urządzeniach z Androidem. NNAPI ma zapewnić podstawową funkcjonalność na potrzeby platform systemów uczących się 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 (poziom interfejsu API 27) lub nowszym.
NNAPI obsługuje wnioskowanie przez stosowanie danych z urządzeń z Androidem do wcześniej wytrenowanych modeli określonych przez programistę. Przykłady wnioskowania to klasyfikowanie obrazów, przewidywanie zachowań użytkowników i wybieranie odpowiednich odpowiedzi na zapytanie.
Określanie lokalizacji na urządzeniu przynosi wiele korzyści:
- Opóźnienie: nie musisz wysyłać żądania przez połączenie sieciowe i czekać na odpowiedź. Może to być szczególnie ważne np. w przypadku aplikacji wideo, które przetwarzają kolejno klatki z kamery.
- Dostępność: aplikacja działa nawet poza zasięgiem sieci.
- Szybkość: nowy sprzęt dostosowany do przetwarzania sieci neuronowych umożliwia znacznie szybsze obliczenia niż sam procesor do zwykłych obciążeń.
- Prywatność: dane nie opuszczają urządzenia z Androidem.
- Koszt: gdy wszystkie obliczenia są wykonywane na urządzeniu z Androidem, nie jest potrzebna farma serwerów.
Są też inne kompromisy, o których deweloper powinien pamiętać:
- Wykorzystanie systemu: ocena sieci neuronowych wymaga dużej mocy obliczeniowej, co może zwiększyć wykorzystanie baterii. Jeśli stanowi to problem z aplikacją, zwłaszcza w przypadku długotrwałych obliczeń, zastanów się nad monitorowaniem stanu baterii.
- Rozmiar aplikacji: zwróć uwagę na rozmiar modeli. Modele mogą zajmować wiele megabajtów miejsca. Jeśli grupowanie dużych modeli w pliku APK miałoby niekorzystny wpływ na użytkowników, rozważ pobranie modeli po zainstalowaniu aplikacji, zastosowanie mniejszych modeli lub uruchomienie obliczeń w chmurze. NNAPI nie udostępnia funkcji do uruchamiania modeli w chmurze.
Jeden z przykładów użycia interfejsu NNAPI znajdziesz w przykładzie interfejsu Android Neural Networks API.
Omówienie środowiska wykonawczego interfejsu Neural Networks API
NNAPI ma być wywoływane przez biblioteki, platformy i narzędzia systemów uczących się, które pozwalają programistom trenować modele poza urządzeniem i wdrażać je na urządzeniach z Androidem. Aplikacje zwykle nie korzystają bezpośrednio z NNAPI, ale korzystają z platform systemów uczących się wyższego poziomu. Platformy te mogłyby z kolei korzystać z NNAPI do przeprowadzania z akceleracją sprzętową operacji wnioskowania 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 wydajnie rozłożyć zadania obliczeniowe na procesory dostępne na urządzeniu, w tym na dedykowany sprzęt do sieci neuronowej, procesory graficzne (GPU) i procesory sygnału cyfrowego (DSP).
W przypadku urządzeń z Androidem, które nie mają specjalistycznego sterownika dostawcy, środowisko wykonawcze NNAPI wykonuje żądania na CPU.
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 wykres obliczeniowy w połączeniu z Twoimi danymi wejściowymi (np. wagami i odchyleniami przekazywanymi przez platformę systemów uczących się) tworzy model do oceny środowiska wykonawczego NNAPI.
NNAPI wykorzystuje 4 główne abstrakcje:
- Model: wykres obliczeniowy działań matematycznych i wartości stałych uzyskanych w trakcie procesu trenowania. Operacje te są charakterystyczne
dla sieci neuronowych. Są to m.in. aktywacja 2D
splotu, logistyczna
(sigmoid)
aktywacji,
rekreatywna aktywacja i inne. Tworzenie modelu jest operacją synchroniczną.
Po utworzeniu można go używać ponownie w wątkach i kompilacjach.
W NNAPI model jest reprezentowany jako instancja
ANeuralNetworksModel
. - Kompilacja: reprezentuje konfigurację służącą do kompilowania modelu NNAPI w kod niższego poziomu. Tworzenie kompilacji jest operacją synchroniczną. Po utworzeniu można go używać ponownie w wątkach i wykonaniach. W NNAPI każda kompilacja jest reprezentowana jako instancja
ANeuralNetworksCompilation
. - Pamięć: reprezentuje pamięć współdzieloną, pliki mapowane w pamięci i podobne bufory pamięci. Dzięki buforowi pamięci środowisko wykonawcze NNAPI może skuteczniej przesyłać dane do sterowników. Aplikacja zwykle tworzy jeden bufor współdzielonej pamięci, 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
. Wykonywanie: interfejs do stosowania modelu NNAPI do zbioru danych wejściowych i do zbierania wyników. Wykonywanie może być wykonywane synchronicznie lub asynchronicznie.
W przypadku wykonania asynchronicznego wiele wątków może czekać na to samo wykonanie. Po zakończeniu tego wykonania wszystkie wątki zostaną zwolnione.
W NNAPI każde wykonanie jest reprezentowane jako instancja
ANeuralNetworksExecution
.
Rysunek 2 przedstawia podstawowy proces programowania.
Pozostała część tej sekcji zawiera opis czynności, które musisz wykonać, aby skonfigurować model NNAPI w celu wykonywania obliczeń, skompilowania modelu i wykonywania skompilowanego modelu.
Zapewnianie dostępu do danych treningowych
Wytrenowane wagi i dane dotyczące uprzedzeń są prawdopodobnie przechowywane w pliku. Aby zapewnić środowisko wykonawcze NNAPI skuteczny dostęp do tych danych, utwórz instancję ANeuralNetworksMemory
, wywołując funkcję ANeuralNetworksMemory_createFromFd()
i przekazując deskryptor otwartego pliku danych. Możesz też określić flagi ochrony pamięci i przesunięcie, w którym zaczyna się region pamięci współdzielonej w pliku.
// Create a memory buffer from the file that contains the trained data
ANeuralNetworksMemory* mem1 = NULL;
int fd = open("training_data", O_RDONLY);
ANeuralNetworksMemory_createFromFd(file_size, PROT_READ, fd, 0, &mem1);
Chociaż w tym przykładzie używamy tylko jednego wystąpienia ANeuralNetworksMemory
dla wszystkich naszych wag, w przypadku wielu plików może się zdarzyć, że użycia więcej niż 1 instancji ANeuralNetworksMemory
.
Używaj natywnych buforów sprzętowych
Możesz używać natywnych buforów sprzętowych na potrzeby danych wejściowych i wyjściowych modelu oraz stałych wartości operandu. 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, a nie każdy akcelerator NNAPI może obsługiwać wszystkie z nich. Z tego względu zapoznaj się z ograniczeniami wymienionymi w dokumentacji referencyjnej ANeuralNetworksMemory_createFromAHardwareBuffer
i przetestuj je z wyprzedzeniem na urządzeniach docelowych, aby mieć pewność, że kompilacje i wykonania korzystające z AHardwareBuffer
działają zgodnie z oczekiwaniami. Aby określić akcelerator, użyj przypisania urządzenia.
Aby zezwolić środowisku wykonawczym NNAPI na dostęp do obiektu AHardwareBuffer
, utwórz instancję ANeuralNetworksMemory
, wywołując funkcję ANeuralNetworksMemory_createFromAHardwareBuffer
i przekazując obiekt AHardwareBuffer
w następujący sposób:
// Configure and create AHardwareBuffer object AHardwareBuffer_Desc desc = ... AHardwareBuffer* ahwb = nullptr; AHardwareBuffer_allocate(&desc, &ahwb); // Create ANeuralNetworksMemory from AHardwareBuffer ANeuralNetworksMemory* mem2 = NULL; ANeuralNetworksMemory_createFromAHardwareBuffer(ahwb, &mem2);
Gdy NNAPI nie będzie już potrzebować dostępu do obiektu AHardwareBuffer
, zwolnij odpowiednią instancję ANeuralNetworksMemory
:
ANeuralNetworksMemory_free(mem2);
Uwaga:
- Możesz użyć
AHardwareBuffer
tylko dla całego bufora; nie możesz używać go z parametremARect
. - Środowisko wykonawcze NNAPI nie usuwa bufora. Zanim zaplanujesz wykonanie, upewnij się, że bufory wejściowe i wyjściowe są dostępne.
- Nie ma obsługi deskryptorów plików zabezpieczeń synchronizacji.
- W przypadku
AHardwareBuffer
z formatami i informacjami dotyczącymi użycia specyficznymi dla dostawcy to implementacja dostawcy decyduje, czy za opróżnienie 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ę.
Operatory
Argumenty to obiekty danych używane do definiowania wykresu. Obejmują one dane wejściowe i wyjściowe modelu, węzły pośrednie zawierające dane przepływające między operacjami i stałe przekazywane do tych operacji.
Istnieją 2 typy operandów, które można dodawać do modeli NNAPI: skalary i tensory.
Argument skalarny reprezentuje jedną wartość. NNAPI obsługuje wartości skalarne w formacie logicznym, 16-bitowej zmiennoprzecinkowej, 32-bitowej, 32-bitowej liczby całkowitej i 32-bitowej liczby całkowitej bez znaku.
Większość operacji w NNAPI obejmuje tensory. Tensory to tablice n-wymiarowe. NNAPI obsługuje tensory z 16-bitową liczbą zmiennoprzecinkową, 32-bitową, 8-bitową kwantyzowaną, 16-bitową kwantyzowaną, 32-bitową liczbą całkowitą i 8-bitową wartością logiczną.
Na przykład na ilustracji 3 przedstawiono model z 2 operacjami: dodawanie, po którym następuje mnożenie. Model wykorzystuje tensor wejściowy i generuje 1 tensor wyjściowy.
Powyższy model 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 równy 0, drugi indeks równy 1 itd. Argumenty 1, 2, 3 i 5 są stałymi argumentami.
Kolejność dodawania operandów nie ma znaczenia. Na przykład jako pierwszy dodany operand może być operand modelu. Ważne jest, aby używać prawidłowej wartości indeksu w odniesieniu do operandu.
Operatory mają typy. Są one określane podczas dodawania do modelu.
operandu nie można używać jako danych wejściowych i wyjściowych modelu.
Każdy operand musi być danymi wejściowymi modelu, stałą lub wyjściowym operandem dokładnie jednej operacji.
Więcej informacji o używaniu operandów znajdziesz w artykule Więcej informacji o operandach.
Zarządzanie
Operacja określa obliczenia do wykonania. Każda operacja składa się z tych elementów:
- typ operacji (np. dodawanie, mnożenie, splot),
- listę indeksów operandów używanych przez operację na potrzeby danych wejściowych;
- lista indeksów operandów używanych przez operację na potrzeby danych wyjściowych.
Kolejność na tych listach ma znaczenie. Listę oczekiwanych danych wejściowych i wyjściowych każdego typu operacji znajdziesz w dokumentacji interfejsu NNAPI API.
Przed dodaniem operacji musisz dodać operandy, które operacja wykorzystuje lub tworzy do modelu.
Kolejność dodawania operacji nie ma znaczenia. NNAPI określa kolejność wykonywania operacji na podstawie zależności ustalonych przez graf obliczeniowy operandów i operacji.
Operacje obsługiwane przez NNAPI zostały opisane w tabeli poniżej:
Znany problem z poziomem interfejsu API 28: podczas przekazywania tensorów ANEURALNETWORKS_TENSOR_QUANT8_ASYMM
do operacji ANEURALNETWORKS_PAD
, która jest dostępna w Androidzie 9 (poziom interfejsu API 28) i nowszych, dane wyjściowe z NNAPI mogą nie być zgodne z danymi wyjściowymi z platform systemów uczących się wyższego poziomu, takich jak TensorFlow Lite. Zamiast tego należy zdać tylko ANEURALNETWORKS_TENSOR_FLOAT32
.
Problem został rozwiązany w Androidzie 10 (poziom interfejsu API 29) i nowszych.
Tworzenie modeli
W poniższym przykładzie tworzymy model dwuoperacyjny widoczny na ilustracji 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 metodę
ANeuralNetworks_addOperand()
. Typy ich danych są zdefiniowane 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 podczas trenowania, użyj funkcji
ANeuralNetworksModel_setOperandValue()
iANeuralNetworksModel_setOperandValueFromMemory()
.W przykładzie poniżej ustawiliśmy stałe wartości z pliku danych treningowych odpowiadające buforowi pamięci, który utworzyliśmy 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));Do każdej operacji na ukierunkowanym wykresie, którą chcesz obliczyć, dodaj ją do modelu, wywołując funkcję
ANeuralNetworksModel_addOperation()
.Jako parametry tego wywołania aplikacja musi zawierać:
- typ operacji,
- liczba wartości wejściowych
- tablica indeksów dla operandów wejściowych
- liczba wartości wyjściowych
- tablica indeksów operandów wyjściowych
Pamiętaj, że operandu nie można używać jednocześnie w przypadku danych wejściowych i wyjściowych w 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 mają być traktowane przez model 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 wartość
ANEURALNETWORKS_TENSOR_FLOAT32
może być obliczana z zakresem lub dokładnością na poziomie równym 16-bitowego formatu zmiennoprzecinkowego IEEE 754, wywołując metodęANeuralNetworksModel_relaxComputationFloat32toFloat16()
.Wywołaj metodę
ANeuralNetworksModel_finish()
, aby dokończyć definicję modelu. Jeśli nie ma błędów, ta funkcja zwraca kod wynikuANEURALNETWORKS_NO_ERROR
.ANeuralNetworksModel_finish(model);
Utworzony model możesz skompilować dowolną liczbę razy i wykonywać każdą kompilację dowolną liczbę razy.
Sterowanie przepływem pracy
Aby włączyć przepływ sterowania w modelu NNAPI, wykonaj te czynności:
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 odwołujące się do tych modeli w modelu zawierającym przepływ kontrolny:
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
Na etapie kompilacji określa on, które procesory zostaną wykonane, i wyświetla odpowiednie sterowniki, aby przygotować się do jego wykonania. Może to obejmować generowanie kodu maszyny konkretnego procesora, na którym będzie działać model.
Aby skompilować model, wykonaj te czynności:
Wywołaj funkcję
ANeuralNetworksCompilation_create()
, aby utworzyć nową instancję kompilacji.// Compile the model ANeuralNetworksCompilation* compilation; ANeuralNetworksCompilation_create(model, &compilation);
Opcjonalnie możesz użyć przypisania urządzenia, aby bezpośrednio wybrać urządzenia, na których mają zostać uruchomione aplikacje.
Możesz też opcjonalnie wpływać na stosunek czasu działania do zużycia baterii i szybkości działania. Aby to zrobić, zadzwoń pod numer
ANeuralNetworksCompilation_setPreference()
.// Ask to optimize for low power consumption ANeuralNetworksCompilation_setPreference(compilation, ANEURALNETWORKS_PREFER_LOW_POWER);
Możesz określić następujące ustawienia:
ANEURALNETWORKS_PREFER_LOW_POWER
: preferuj sposób, w jaki minimalizuje obciążenie baterii. Przydaje się to w przypadku kompilacji, które są często wykonywane.ANEURALNETWORKS_PREFER_FAST_SINGLE_ANSWER
: wolę jak najszybciej zwracać jedną odpowiedź, nawet jeśli powoduje 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 metodę
ANeuralNetworksCompilation_setCaching
.// Set up compilation caching ANeuralNetworksCompilation_setCaching(compilation, cacheDir, token);
Użyj
getCodeCacheDir()
dlacacheDir
. Podanytoken
musi być unikalny dla każdego modelu w aplikacji.Dokończ definicję kompilacji, wywołując metodę
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 NNAPI udostępnia funkcje, które umożliwiają bibliotekom i aplikacjom platformy systemów uczących się uzyskiwanie informacji o dostępnych urządzeniach i określanie urządzeń, które mają zostać użyte do wykonania. Podanie informacji o dostępnych urządzeniach umożliwia aplikacjom pobieranie dokładnej wersji sterowników urządzenia, co pozwala uniknąć znanych niezgodności. Dzięki możliwości określenia, na których urządzeniach mają być wykonywane różne sekcje modelu, aplikacje mogą być optymalizowane pod kątem urządzenia z Androidem, na którym zostały wdrożone.
Wykrywanie urządzeń
Użyj zapytania ANeuralNetworks_getDeviceCount
, aby sprawdzić liczbę dostępnych urządzeń. W przypadku każdego urządzenia użyj metody ANeuralNetworks_getDevice
, aby ustawić odwołanie do tego urządzenia w instancji ANeuralNetworksDevice
.
Mając odniesienie do 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
ANeuralNetworksModel_getSupportedOperationsForDevices
pozwala określić, które operacje modelu można uruchamiać na określonych urządzeniach.
Aby określić, które akceleratory mają być używane do wykonywania, wywołaj ANeuralNetworksCompilation_createForDevices
zamiast ANeuralNetworksCompilation_create
.
Użyj powstałego w ten sposób 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.
W przypadku określenia wielu urządzeń środowisko wykonawcze odpowiada za rozłożenie pracy między urządzeniami.
Tak jak w przypadku innych urządzeń, implementacja CPU NNAPI jest reprezentowana przez ANeuralNetworksDevice
o nazwie nnapi-reference
i typie ANEURALNETWORKS_DEVICE_TYPE_CPU
. Gdy wywołujesz funkcję ANeuralNetworksCompilation_createForDevices
, implementacja procesora nie jest używana do obsługi przypadków awarii przy kompilowaniu i wykonywaniu modeli.
Zadaniem aplikacji jest partycjonowanie modelu na modele podrzędne, które mogą działać na określonych urządzeniach. Aplikacje, które nie muszą ręcznie partycjonować, powinny nadal wywoływać prostszy element ANeuralNetworksCompilation_create
, który pozwala wykorzystać wszystkie dostępne urządzenia (w tym procesor) w celu przyspieszenia modelu. Jeśli model nie był w pełni obsługiwany przez urządzenia określone za pomocą funkcji ANeuralNetworksCompilation_createForDevices
, zwracany jest parametr ANEURALNETWORKS_BAD_DATA
.
Partycjonowanie modelu
Gdy dla modelu dostępnych jest wiele urządzeń, środowisko wykonawcze NNAPI rozdziela pracę między te urządzenia. Jeśli na przykład do ANeuralNetworksCompilation_createForDevices
wskazano więcej niż 1 urządzenie, przy przydzielaniu utworu brane są pod uwagę wszystkie z nich. Pamiętaj, że jeśli procesora nie ma na liście, wykonywanie procesora będzie wyłączone. Podczas korzystania z funkcji ANeuralNetworksCompilation_create
uwzględniane są wszystkie dostępne urządzenia, w tym procesor.
Rozkład jest dzielony na liście dostępnych urządzeń, dla każdej operacji w modelu, przez wybór urządzenia obsługującego dane działanie i deklarowanie największej wydajności, tj. najkrótszy czas wykonywania lub najmniejsze zużycie energii w zależności od preferencji wykonania określonych przez klienta. Ten algorytm partycjonowania nie uwzględnia możliwych rozbieżności spowodowanych przez zamówienie reklamowe między różnymi procesorami, dlatego podczas określania wielu procesorów (wyraźnie w przypadku użycia funkcji ANeuralNetworksCompilation_createForDevices
lub pośrednio z użyciem ANeuralNetworksCompilation_create
) ważne jest profilowanie wynikowej aplikacji.
Aby dowiedzieć się, jak Twój model został partycjonowany przez NNAPI, poszukaj komunikatu w logach Androida (na poziomie INFORMACYJNYM z tagiem ExecutionPlan
):
ModelBuilder::findBestDeviceForEachOperation(op-name): device-index
op-name
to opisowa nazwa operacji na wykresie, a device-index
to indeks urządzenia kandydata na liście urządzeń.
Ta lista zawiera dane wejściowe dla funkcji ANeuralNetworksCompilation_createForDevices
lub, w przypadku funkcji ANeuralNetworksCompilation_createForDevices
, listę urządzeń zwracanych podczas wykonywania iteracji na wszystkich urządzeniach z wykorzystaniem ANeuralNetworks_getDeviceCount
i ANeuralNetworks_getDevice
.
Wiadomość (na poziomie INFO z tagiem ExecutionPlan
):
ModelBuilder::partitionTheWork: only one best device: device-name
Ten komunikat oznacza, że cały wykres został przyspieszony na urządzeniu device-name
.
Realizacja
W kroku wykonania model stosuje model do zbioru danych wejściowych i zapisuje dane wyjściowe obliczeń w co najmniej 1 buforze użytkownika lub obszarze pamięci przydzielonym aplikacji.
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, gdzie aplikacja odczytuje wartości wejściowe obliczeń. Aplikacja może odczytywać wartości wejściowe z bufora użytkownika lub przydzielonego miejsca w pamięci, wywołując odpowiednio
ANeuralNetworksExecution_setInput()
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 zapisuje wartości wyjściowe. Aplikacja może zapisywać wartości wyjściowe w buforze użytkownika lub w przydzielonym miejscu w pamięci, wywołując odpowiednio
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, 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()
, by poczekać na zakończenie wykonywania. Jeśli wykonanie się udało, funkcja zwraca kod wyniku o wartościANEURALNETWORKS_NO_ERROR
. Oczekiwanie można wykonać w innym wątku niż ten, w którym rozpoczyna się wykonanie.// For our example, we have no other work to do and will just wait for the completion ANeuralNetworksEvent_wait(run1_end); ANeuralNetworksEvent_free(run1_end); ANeuralNetworksExecution_free(run1);
Opcjonalnie możesz zastosować do skompilowanego modelu inny zbiór danych wejściowych, używając tej samej instancji kompilacji do utworzenia nowej instancji
ANeuralNetworksExecution
.// Apply the compiled model to a different set of inputs ANeuralNetworksExecution* run2; ANeuralNetworksExecution_create(compilation, &run2); ANeuralNetworksExecution_setInput(run2, ...); ANeuralNetworksExecution_setOutput(run2, ...); ANeuralNetworksEvent* run2_end = NULL; ANeuralNetworksExecution_startCompute(run2, &run2_end); ANeuralNetworksEvent_wait(run2_end); ANeuralNetworksEvent_free(run2_end); ANeuralNetworksExecution_free(run2);
Wykonanie synchroniczne
Wykonywanie asynchroniczne wymaga czasu na powstawanie i synchronizowanie wątków. Poza tym opóźnienie może być bardzo zmienne – najdłuższe opóźnienie sięga 500 mikrosekund od momentu powiadomienia lub zmodyfikowania wątku do momentu jego powiązania z rdzeniem procesora.
Aby skrócić czas oczekiwania, możesz zamiast tego skierować aplikację do synchronicznego wywołania wnioskowania do środowiska wykonawczego. To wywołanie nie wyświetli się dopiero po zakończeniu wnioskowania, a nie po rozpoczęciu wnioskowania. Zamiast wywoływać metodę ANeuralNetworksExecution_startCompute
w celu asynchronicznego wywołania wnioskowania do środowiska wykonawczego, aplikacja wywołuje ANeuralNetworksExecution_compute
, aby wykonać synchroniczne wywołanie środowiska wykonawczego. Wywołanie ANeuralNetworksExecution_compute
nie oznacza ANeuralNetworksEvent
i nie jest sparowane z wywołaniem ANeuralNetworksEvent_wait
.
Uruchomienia serii
Na urządzeniach z Androidem 10 (poziom interfejsu API 29) lub nowszym NNAPI obsługuje uruchomienia burst z użyciem obiektu ANeuralNetworksBurst
. Seria wykonań to sekwencja wykonań tej samej kompilacji, które następują w krótkich odstępach czasu, np. na ujęciach z kamery lub na kolejnych próbkach dźwięku. Użycie obiektów ANeuralNetworksBurst
może spowodować szybsze wykonania, ponieważ sygnalizuje akceleratorom, że zasoby mogą być używane ponownie w różnych wykonaniach i że akceleratory powinny pozostawać w stanie wysokiej wydajności przez cały czas trwania wybuchu.
ANeuralNetworksBurst
wprowadza tylko niewielką zmianę w zwykłej ścieżce wykonywania. Obiekt ciągły możesz utworzyć za pomocą 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);
Wykonania serii są synchroniczne. Jednak zamiast używać metody ANeuralNetworksExecution_compute
do wykonywania każdego wnioskowania, możesz sparować różne obiekty ANeuralNetworksExecution
z tym samym parametrem 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ą klasy ANeuralNetworksBurst_free
, gdy nie jest już potrzebny.
// Cleanup ANeuralNetworksBurst_free(burst);
Asynchroniczne kolejki poleceń i zabezpieczone wykonywanie
W Androidzie 11 i nowszych NNAPI obsługuje dodatkowy sposób planowania wykonywania asynchronicznego za pomocą metody ANeuralNetworksExecution_startComputeWithDependencies()
. Jeśli używasz tej metody, wykonanie czeka przed rozpoczęciem oceny, aż pojawi się sygnał dla wszystkich zdarzeń zależnych. Gdy wykonanie się zakończy, a dane wyjściowe będą gotowe do wykorzystania, zwrócone zdarzenie będzie sygnalizowane.
W zależności od urządzeń, które obsługują wykonywanie, zdarzenie może być objęte zabezpieczeniem synchronizacji. Musisz wywołać metodę ANeuralNetworksEvent_wait()
, aby poczekać na zdarzenie i odzyskać zasoby użyte przez wykonanie. Ograniczenia synchronizacji do obiektu zdarzenia możesz importować za pomocą narzędzia ANeuralNetworksEvent_createFromSyncFenceFd()
, a ograniczenia synchronizacji z obiektu zdarzenia – za pomocą narzędzia ANeuralNetworksEvent_getSyncFenceFd()
.
Dynamiczne wymiary wyjściowe
Aby obsługiwać modele, w których rozmiar danych wyjściowych zależy od danych wejściowych (czyli takich, w których nie można określić rozmiaru w czasie wykonywania modelu), użyj właściwości ANeuralNetworksExecution_getOutputOperandRank
i ANeuralNetworksExecution_getOutputOperandDimensions
.
Jak to zrobić:
// Get the rank of the output uint32_t myOutputRank = 0; ANeuralNetworksExecution_getOutputOperandRank(run1, 0, &myOutputRank); // Get the dimensions of the output std::vector<uint32_t> myOutputDimensions(myOutputRank); ANeuralNetworksExecution_getOutputOperandDimensions(run1, 0, myOutputDimensions.data());
Uporządkuj
Etap czyszczenia służy do zwalniania wewnętrznych zasobów używanych na potrzeby obliczeń.
// Cleanup ANeuralNetworksCompilation_free(compilation); ANeuralNetworksModel_free(model); ANeuralNetworksMemory_free(mem1);
Zarządzanie błędami i korzystanie z procesora awaryjnego
W przypadku wystąpienia błędu podczas partycjonowania, gdy sterownik nie skompiluje skompilowanego modelu (elementu) lub nie uruchomi skompilowanego modelu, NNAPI może przełączyć się na własną implementację procesora w przypadku jednej lub wielu operacji.
Jeśli klient NNAPI zawiera zoptymalizowane wersje operacji (np. TFLite), korzystnie może być wyłączenie działania zastępczej procesora CPU i rozwiązanie błędów przez zoptymalizowaną implementację operacji klienta.
Jeśli w Androidzie 10 kompilacja jest wykonywana za pomocą ANeuralNetworksCompilation_createForDevices
, zastępcza konfiguracja procesora zostanie wyłączona.
W Androidzie P wykonanie NNAPI wraca do procesora, jeśli nie uda się wykonać na sterowniku.
Dzieje się tak również w Androidzie 10, gdy używany jest ANeuralNetworksCompilation_create
zamiast ANeuralNetworksCompilation_createForDevices
.
Pierwsze wykonanie jest przywracane dla tej pojedynczej partycji, a jeśli to nadal się nie uda, ponawia próbę wykonania całego modelu na CPU.
Jeśli partycjonowanie lub kompilacja się nie uda, na CPU zostanie wypróbowany cały model.
W niektórych przypadkach niektóre operacje nie są obsługiwane na procesorze i kompilacja lub wykonanie kończy się niepowodzeniem.
Nawet po wyłączeniu przełączania awaryjnego procesora nadal mogą być wykonywane operacje w modelu, które są zaplanowane na procesorze. Jeśli procesor znajduje się na liście procesorów przekazanych do ANeuralNetworksCompilation_createForDevices
i jest jedynym podmiotem przetwarzającym te operacje, który obsługuje te operacje lub jest procesorem, który deklaruje najwyższą wydajność w przypadku tych operacji, zostanie wybrany jako główny (bez zastępczy) wykonawca.
Aby mieć pewność, że procesor nie będzie uruchamiany, użyj funkcji ANeuralNetworksCompilation_createForDevices
, wykluczając jednocześnie urządzenie nnapi-reference
z listy urządzeń.
Począwszy od Androida P, można wyłączyć funkcję zastępczą w czasie wykonywania kompilacji DEBUG, ustawiając właściwość debug.nn.partition
na 2.
Domeny pamięci
W Androidzie 11 i nowszych NNAPI obsługuje domeny pamięci, które udostępniają interfejsy alokatora dla nieprzezroczystych wspomnień. Dzięki temu aplikacje mogą przekazywać pamięci natywne dla urządzenia pomiędzy uruchomieniami, dzięki czemu NNAPI nie kopiuje ani nie przekształca niepotrzebnie danych podczas wykonywania kolejnych wykonań na tym samym sterowniku.
Funkcja domeny pamięci jest przeznaczona dla tensorów, które są w większości wewnętrzne dla sterownika i nie wymagają częstego dostępu po stronie klienta. Przykładami takich tensorów są tensory stanów w modelach sekwencji. W przypadku tensorów, które potrzebują częstego dostępu do procesora po stronie klienta, użyj pul współdzielonej pamięci.
Aby przydzielić nieprzejrzystą 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 wymagane role wejściowe i wyjściowe, 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 określ wymiary pamięci, wywołując metodę
ANeuralNetworksMemoryDesc_setDimensions()
.// Specify the memory dimensions uint32_t dims[] = {3, 4}; ANeuralNetworksMemoryDesc_setDimensions(desc, 2, dims);
Dokończ definicję deskryptora, wywołując metodę
ANeuralNetworksMemoryDesc_finish()
.ANeuralNetworksMemoryDesc_finish(desc);
Przydziel tyle wspomnień, 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 go już nie potrzebujesz.
ANeuralNetworksMemoryDesc_free(desc);
Klient może używać utworzonych obiektów ANeuralNetworksMemory
w ANeuralNetworksExecution_setInputFromMemory()
lub ANeuralNetworksExecution_setOutputFromMemory()
tylko zgodnie z rolami określonymi w obiekcie ANeuralNetworksMemoryDesc
. Argumenty przesunięcia i długości muszą mieć wartość 0, co oznacza, że używana jest cała pamięć. Klient może też bezpośrednio ustawić lub wyodrębnić zawartość pamięci za pomocą metody ANeuralNetworksMemory_copy()
.
Możesz tworzyć nieprzezroczyste wspomnienia z rolami o nieokreślonych wymiarach lub pozycji.
W takim przypadku tworzenie pamięci może się nie powieść ze stanem ANEURALNETWORKS_OP_FAILED
, jeśli nie jest ona obsługiwana przez bazowy sterownik. Zachęca się klienta do wdrożenia logiki awaryjnej przez przydzielenie odpowiednio dużego bufora wspieranego przez Ashmem lub tryb BLOB AHardwareBuffer
.
Gdy NNAPI nie musi już uzyskiwać dostępu do nieprzejrzystego obiektu pamięci, zwolnij odpowiednią instancję ANeuralNetworksMemory
:
ANeuralNetworksMemory_free(opaqueMem);
Pomiar wyników
Możesz ocenić wydajność aplikacji, mierząc czas wykonywania lub przez profilowanie.
Czas wykonania
Jeśli chcesz określić łączny czas wykonywania w środowisku wykonawczym, możesz użyć interfejsu API wykonywania synchronicznego i mierzyć czas wykonywania wywołania. Jeśli chcesz określić łączny czas wykonywania na niższym poziomie stosu oprogramowania, możesz użyć metod ANeuralNetworksExecution_setMeasureTiming
i ANeuralNetworksExecution_getDuration
, aby uzyskać:
- czasu wykonywania w 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 uwzględnia narzutów, takich jak samo środowisko wykonawcze i IPC niezbędne do komunikacji środowiska wykonawczego ze sterownikiem.
Te interfejsy API mierzą czas między przesłanej pracy a zakończonymi zdarzeniami, a nie czas, jaki sterownik lub akcelerator poświęca na wnioskowanie, co może być przerywane przez przełączanie kontekstu.
Jeśli na przykład rozpocznie się wnioskowanie 1, sterownik zatrzyma pracę, aby wykonać wnioskowanie 2, a następnie wznowi i zakończy wnioskowanie 1, czas wykonania wnioskowania 1 będzie obejmował czas zatrzymania pracy w celu wykonania wnioskowania 2.
Te informacje o czasie mogą być przydatne przy wdrażaniu produkcyjnym aplikacji w celu zbierania danych telemetrycznych do użytku w trybie offline. Dane o czasie mogą służyć do modyfikowania aplikacji w celu zwiększenia jej wydajności.
Podczas korzystania z tej funkcji pamiętaj o tych kwestiach:
- Zbieranie informacji o czasie może mieć wpływ na wydajność.
- Tylko sterownik jest w stanie obliczyć czas spędzony w sobie lub w akceleratorze, z wyjątkiem czasu w środowisku wykonawczym NNAPI i IPC.
- Tych interfejsów API możesz używać tylko za pomocą interfejsu
ANeuralNetworksExecution
, który został utworzony za pomocąANeuralNetworksCompilation_createForDevices
za pomocąnumDevices = 1
. - Aby przesłać informacje o czasie, nie jest wymagany żaden kierowca.
Profilowanie aplikacji przy użyciu systemu 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 wygenerowanych przez aplikację i generowania widoku tabeli pokazującego czas spędzony na różnych fazach cyklu życia modelu (wystąpienie, przygotowanie, wykonanie i zakończenie kompilacji) oraz różne warstwy aplikacji. Aplikacja jest podzielona na następujące warstwy:
Application
: kod głównej aplikacji.Runtime
: środowisko wykonawcze NNAPIIPC
: komunikacja między procesami między środowiskiem wykonawczym NNAPI a kodem sterownikaDriver
: proces sterownika akceleratora.
Generowanie danych analiz profilowania
Przy założeniu, że sprawdzono drzewo źródłowe AOSP pod adresem $ANDROID_BUILD_TOP i użyjesz przykładowej klasyfikacji obrazów TFLite jako aplikacji docelowej, możesz wygenerować dane profilowania NNAPI, wykonując te czynności:
- Uruchom 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 ślady będą zapisywane w trace.html
. Podczas profilowania własnej aplikacji musisz zastąpić org.tensorflow.lite.examples.classification
nazwą procesu określoną w pliku manifestu aplikacji.
Sprawi to, że jedna z konsoli powłoki będzie zajęta. Nie uruchamiaj tego polecenia w tle, ponieważ czeka ono interaktywnie na zakończenie działania enter
.
- Po uruchomieniu kolektora systrace uruchom aplikację i uruchom test porównawczy.
W naszym przypadku możesz uruchomić aplikację Image Classification w Android Studio lub bezpośrednio z interfejsu telefonu testowego, jeśli aplikacja została już zainstalowana. Aby wygenerować niektóre dane NNAPI, musisz skonfigurować aplikację do używania NNAPI, wybierając NNAPI jako urządzenie docelowe w oknie konfiguracji aplikacji.
Gdy test się zakończy, przerwij systrace, naciskając
enter
na terminalu konsoli (aktywnym od kroku 1).Uruchom narzędzie
systrace_parser
, aby wygenerować łączne 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 w warstwie, w tym czas oczekiwania na wykonanie w warstwie bazowej,
– --print-detail
: drukuje wszystkie zdarzenia zebrane z systrace,
– --per-execution
: wyświetla tylko wykonanie i jego podfazy (jako czas wykonania) zamiast statystyk ze wszystkich faz.
– --json
: tworzy dane wyjściowe w formacie JSON
Oto przykładowe dane wyjściowe:
===========================================================================================================================================
NNAPI timing summary (total time, ms wall-clock) Execution
----------------------------------------------------
Initialization Preparation Compilation I/O Compute Results Ex. total Termination Total
-------------- ----------- ----------- ----------- ------------ ----------- ----------- ----------- ----------
Application n/a 19.06 1789.25 n/a n/a 6.70 21.37 n/a 1831.17*
Runtime - 18.60 1787.48 2.93 11.37 0.12 14.42 1.32 1821.81
IPC 1.77 - 1781.36 0.02 8.86 - 8.88 - 1792.01
Driver 1.04 - 1779.21 n/a n/a n/a 7.70 - 1787.95
Total 1.77* 19.06* 1789.25* 2.93* 11.74* 6.70* 21.37* 1.32* 1831.17*
===========================================================================================================================================
* This total ignores missing (n/a) values and thus is not necessarily consistent with the rest of the numbers
Parser może ulec awarii, jeśli zebrane zdarzenia nie odzwierciedlają pełnego logu czasu aplikacji. W szczególności może wystąpić błąd, jeśli w logu czasu występują zdarzenia Systrace wygenerowane w celu oznaczenia końca sekcji, ale nie ma powiązanego zdarzenia rozpoczęcia sekcji. Dzieje się tak zwykle wtedy, gdy po uruchomieniu kolektora systrace są generowane niektóre zdarzenia z poprzedniej sesji profilowania. W takim przypadku musisz ponownie przeprowadzić profilowanie.
Dodaj statystyki kodu aplikacji do danych wyjściowych systrace_parser
Aplikacja parse_systrace jest oparta na wbudowanej funkcji systemu Android Systrace. Możesz dodać logi czasu dla konkretnych operacji w aplikacji za pomocą interfejsu Systrace API (dla Javy i aplikacji natywnych) z niestandardowymi nazwami zdarzeń.
Aby powiązać zdarzenia niestandardowe z fazami cyklu życia aplikacji, dołącz do nazwy zdarzenia jeden z tych ciągów:
[NN_LA_PI]
: zdarzenie na poziomie aplikacji podczas inicjowania[NN_LA_PP]
: zdarzenie na poziomie aplikacji w ramach przygotowań[NN_LA_PC]
: zdarzenie na poziomie aplikacji na potrzeby kompilacji[NN_LA_PE]
: zdarzenie na poziomie aplikacji do wykonania
Oto przykład zmiany kodu przykładowej klasyfikacji obrazów TFLite przez dodanie sekcji runInferenceModel
dla fazy Execution
i warstwy Application
zawierającej inne sekcje preprocessBitmap
, których nie można uwzględnić w logach NNAPI. Sekcja runInferenceModel
będzie częścią zdarzeń systrace przetwarzanych przez parser nnapi systrace:
Kotlin
/** Runs inference and returns the classification results. */ fun recognizeImage(bitmap: Bitmap): List{ // This section won’t appear in the NNAPI systrace analysis Trace.beginSection("preprocessBitmap") convertBitmapToByteBuffer(bitmap) Trace.endSection() // Run the inference call. // Add this method in to NNAPI systrace analysis. Trace.beginSection("[NN_LA_PE]runInferenceModel") long startTime = SystemClock.uptimeMillis() runInference() long endTime = SystemClock.uptimeMillis() Trace.endSection() ... return recognitions }
Java
/** Runs inference and returns the classification results. */ public 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 NNAPI zapewnia lepszą jakość usług (QoS), umożliwiając aplikacji wskazywanie względnych priorytetów modeli, maksymalnego czasu potrzebnego na przygotowanie danego modelu oraz maksymalnego czasu wykonywania danego obliczenia. Android 11 wprowadza też dodatkowe kody wyników NNAPI, które pozwalają aplikacjom analizować błędy, takie jak niewykonane w terminie.
Ustawianie priorytetu zadania
Aby ustawić priorytet zadania NNAPI, wywołaj ANeuralNetworksCompilation_setPriority()
przed wywołaniem ANeuralNetworksCompilation_finish()
.
Wyznaczanie terminów
Aplikacje mogą określać terminy zarówno kompilacji modeli, jak i wnioskowania.
- Aby ustawić czas oczekiwania na kompilację, wywołaj
ANeuralNetworksCompilation_setTimeout()
przed wywołaniemANeuralNetworksCompilation_finish()
. - Aby ustawić czas oczekiwania na wnioskowanie, wywołaj
ANeuralNetworksExecution_setTimeout()
przed rozpoczęciem kompilacji.
Więcej informacji o operandach
W dalszej części omówiono zaawansowane zagadnienia dotyczące używania operandów.
Tensory poddane kwantyzacji
Kwantowy tensor to kompaktowy sposób przedstawiania n-wymiarowej tablicy wartości zmiennoprzecinkowych.
NNAPI obsługuje 8-bitowe tensory asymetryczne kwantyzowane. W przypadku tych tensorów wartość każdej komórki jest reprezentowana przez 8-bitową liczbę całkowitą. Z tensorem powiązany jest skala i wartość punktowa zero. Służą do konwertowania 8-bitowych liczb całkowitych na reprezentowane wartości zmiennoprzecinkowe.
Zastosowana formuła to:
(cellValue - zeroPoint) * scale
gdzie wartość zeroPoint to 32-bitowa liczba całkowita, a skala to 32-bitowa wartość zmiennoprzecinkowa.
W porównaniu z tensorami 32-bitowych wartości zmiennoprzecinkowych tensory 8-bitowe mają dwie zalety:
- Twoja aplikacja jest mniejsza, ponieważ wytrenowane waga zajmuje jedną czwartą rozmiaru tensorów 32-bitowych.
- Obliczenia często są często wykonywane szybciej. Wynika to z mniejszej ilości danych, które trzeba pobrać z pamięci, oraz wydajności procesorów, takich jak platformy DSP, przy obliczaniu liczb całkowitych.
Chociaż można przekonwertować model zmiennoprzecinkowy na model kwantowy, nasze doświadczenie wykazało, że lepsze wyniki osiąga się przez bezpośrednie trenowanie modelu kwantowego. W efekcie sieć neuronowa uczy się kompensować zwiększoną szczegółowość każdej wartości. Dla każdego kwantowego tensora wartości skali i zeroPoint są określane podczas procesu trenowania.
W NNAPI definiujesz typy tensorów kwantowych, ustawiając pole typu struktury danych ANeuralNetworksOperandType
na ANEURALNETWORKS_TENSOR_QUANT8_ASYMM
.
Określasz też skalę i wartość zeroPoint tensora w tej strukturze danych.
Oprócz 8-bitowych tensorów asymetrycznych kwantyzowanych NNAPI obsługuje następujące funkcje:
ANEURALNETWORKS_TENSOR_QUANT8_SYMM_PER_CHANNEL
, którego możesz używać do przedstawiania wagi w operacjachCONV/DEPTHWISE_CONV/TRANSPOSED_CONV
.ANEURALNETWORKS_TENSOR_QUANT16_ASYMM
, którego możesz używać na potrzeby stanu wewnętrznegoQUANTIZED_16BIT_LSTM
.ANEURALNETWORKS_TENSOR_QUANT8_SYMM
, który może być danymi wejściowymi dlaANEURALNETWORKS_DEQUANTIZE
.
Opcjonalne operandy
Kilka operacji, na przykład ANEURALNETWORKS_LSH_PROJECTION
, pobiera opcjonalne operandy. Aby wskazać w modelu, że opcjonalny operand jest pominięty, wywołaj funkcję ANeuralNetworksModel_setOperandValue()
, podając wartość NULL
jako bufor i 0 dla długości.
Jeśli decyzja o tym, czy operand jest obecny czy nie w przypadku każdego wykonania, wskazujesz, że jest on pomijany za pomocą funkcji ANeuralNetworksExecution_setInput()
lub ANeuralNetworksExecution_setOutput()
, podając wartość NULL
jako bufor i 0 jako długość.
Tensory nieznanego rangi
W Androidzie 9 (poziom interfejsu API 28) wprowadzono operandy modelu o nieznanych wymiarach, ale znana pozycja (liczba wymiarów). W Androidzie 10 (poziom interfejsu API 29) wprowadzono komponenty o nieznanej pozycji, co można zobaczyć w ANeuralNetworksOperandType.
Test porównawczy NNAPI
Test porównawczy NNAPI jest dostępny w AOSP w platform/test/mlts/benchmark
(aplikacja analizy porównawczej) i platform/test/mlts/models
(modele i zbiory danych).
Test porównawczy ocenia czas oczekiwania i dokładność oraz porównuje sterowniki z tymi samymi modelami i zbiorami danych, które zostały wykonane za pomocą Tensorflow Lite uruchomionej na procesorze.
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 osiągalne za pomocą narzędzia adb.
Jeśli podłączone jest więcej niż 1 urządzenie z Androidem, wyeksportuj zmienną środowiskową
ANDROID_SERIAL
urządzenia docelowego.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
Na koniec testu porównawczego jego wyniki będą wyświetlane w postaci strony HTML przekazywanej do adresu
xdg-open
.
Logi NNAPI
NNAPI generuje przydatne informacje diagnostyczne w dziennikach systemowych. Do analizy logów użyj narzędzia logcat.
Włącz szczegółowe logowanie NNAPI dla konkretnych faz lub komponentów, ustawiając właściwość debug.nn.vlog
(za pomocą adb shell
) na poniższą listę wartości rozdzielonych spacją, dwukropkiem lub przecinkiem:
model
: budowanie modelicompilation
: generowanie planu wykonania modelu i kompilacjiexecution
: wykonanie modelucpuexe
: wykonywanie operacji przy użyciu implementacji procesora NNAPImanager
: rozszerzenia NNAPI, dostępne interfejsy i informacje związane z funkcjamiall
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ć logowanie szczegółowe, użyj polecenia adb shell setprop debug.nn.vlog '""'
.
Po włączeniu logowanie szczegółowe generuje wpisy logu na poziomie INFORMACYJNE z tagiem ustawionym na nazwę etapu lub komponentu.
Oprócz komunikatów kontrolowanych debug.nn.vlog
komponenty interfejsu NNAPI API udostępniają inne wpisy logu na różnych poziomach, z których każdy za pomocą określonego tagu logu.
Aby uzyskać listę komponentów, przeszukaj drzewo źródłowe, używając następującego wyrażenia:
grep -R 'define LOG_TAG' | awk -F '"' '{print $2}' | sort -u | egrep -v "Sample|FileTag|test"
To wyrażenie zwraca obecnie następujące tagi:
- Kreator rozbłysku
- Wywołania zwrotne
- Kompilator kompilacji
- Wykonawca procesora graficznego
- Kreator wykonywania
- Sterowanie kręceniem ujęć
- Serwer ExecutionBurst
- Plan wykonania
- FibonacciDriver
- Zrzut wykresu
- Element IndexedStatusWrapper
- IonWatcher
- Menedżer
- Pamięć
- Memoryutils
- Metamodel
- Informacje o argumentach modelu
- Kreator modeli
- NeuralNetworks
- Program do rozpoznawania operacji
- Zarządzanie
- Narzędzia operacyjne
- Informacje o pakiecie
- Haszer tokenów
- Menedżer typów
- Narzędzia
- ValidateHal
- Wersjonowane interfejsy
Aby kontrolować poziom komunikatów logu wyświetlanych przez funkcję logcat
, użyj zmiennej środowiskowej ANDROID_LOG_TAGS
.
Aby wyświetlić pełny zestaw komunikatów logu NNAPI i wyłączyć wszystkie inne, ustaw ANDROID_LOG_TAGS
na:
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.
Aby ustawić ANDROID_LOG_TAGS
, użyj 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 ten filtr ma zastosowanie tylko do logcat
. Aby wygenerować szczegółowe informacje logu, musisz jeszcze ustawić właściwość debug.nn.vlog
na all
.