Interfejs API sieci neuronowych

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.

Rysunek 1. Architektura systemu dla interfejsu Android Neural Networks API

Model programowania interfejsu Neural Networks API

Aby wykonywać obliczenia za pomocą NNAPI, musisz najpierw utworzyć graf skierowany, który definiuje obliczenia do wykonania. Ten 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.

Rysunek 2. Proces programowania interfejsu Android Neural Networks API

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 parametrem ARect.
  • Ś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.

Rysunek 3. Przykład operandów modelu NNAPI

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:

Kategoria Zarządzanie
Działania matematyczne z użyciem elementów
Manipulacja Tensor
Operacje na obrazie
Operacje wyszukiwania
Operacje normalizacji
Operacje splotowe
Operacje łączenia
Operacje aktywacji
Inne operacje

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:

  1. Wywołaj funkcję ANeuralNetworksModel_create(), aby zdefiniować pusty model.

    ANeuralNetworksModel* model = NULL;
    ANeuralNetworksModel_create(&model);
    
  2. Dodaj operandy do modelu, wywołując metodę ANeuralNetworks_addOperand(). Typy ich danych są zdefiniowane za pomocą struktury danych ANeuralNetworksOperandType.

    // In our example, all our tensors are matrices of dimension [3][4]
    ANeuralNetworksOperandType tensor3x4Type;
    tensor3x4Type.type = ANEURALNETWORKS_TENSOR_FLOAT32;
    tensor3x4Type.scale = 0.f;    // These fields are used for quantized tensors
    tensor3x4Type.zeroPoint = 0;  // These fields are used for quantized tensors
    tensor3x4Type.dimensionCount = 2;
    uint32_t dims[2] = {3, 4};
    tensor3x4Type.dimensions = dims;

    // We also specify operands that are activation function specifiers ANeuralNetworksOperandType activationType; activationType.type = ANEURALNETWORKS_INT32; activationType.scale = 0.f; activationType.zeroPoint = 0; activationType.dimensionCount = 0; activationType.dimensions = NULL;

    // Now we add the seven operands, in the same order defined in the diagram ANeuralNetworksModel_addOperand(model, &tensor3x4Type); // operand 0 ANeuralNetworksModel_addOperand(model, &tensor3x4Type); // operand 1 ANeuralNetworksModel_addOperand(model, &activationType); // operand 2 ANeuralNetworksModel_addOperand(model, &tensor3x4Type); // operand 3 ANeuralNetworksModel_addOperand(model, &tensor3x4Type); // operand 4 ANeuralNetworksModel_addOperand(model, &activationType); // operand 5 ANeuralNetworksModel_addOperand(model, &tensor3x4Type); // operand 6
  3. W przypadku operandów, które mają stałe wartości, takich jak wagi i odchylenia, które aplikacja uzyskuje podczas trenowania, użyj funkcji ANeuralNetworksModel_setOperandValue() i ANeuralNetworksModel_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));
  4. 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);
  5. 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);
    
  6. Opcjonalnie określ, czy wartość ANEURALNETWORKS_TENSOR_FLOAT32 może być obliczana z zakresem lub dokładnością na poziomie równym 16-bitowego formatu zmiennoprzecinkowego IEEE 754, wywołując metodę ANeuralNetworksModel_relaxComputationFloat32toFloat16().

  7. Wywołaj metodę ANeuralNetworksModel_finish(), aby dokończyć definicję modelu. Jeśli nie ma błędów, ta funkcja zwraca kod wyniku ANEURALNETWORKS_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:

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

    ANeuralNetworksModel* thenModel = makeThenModel();
    ANeuralNetworksModel* elseModel = makeElseModel();
    
  2. Utwórz operandy odwołujące się do tych modeli w modelu zawierającym przepływ 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);
    
  3. Dodaj operację przepływu sterowania:

    uint32_t inputs[] = {kConditionOperandIndex,
                         kThenOperandIndex,
                         kElseOperandIndex,
                         kInput1, kInput2, kInput3};
    uint32_t outputs[] = {kOutput1, kOutput2};
    ANeuralNetworksModel_addOperation(model, ANEURALNETWORKS_IF,
                                      std::size(inputs), inputs,
                                      std::size(output), outputs);
    

Kompilacja

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:

  1. Wywołaj funkcję ANeuralNetworksCompilation_create(), aby utworzyć nową instancję kompilacji.

    // Compile the model
    ANeuralNetworksCompilation* compilation;
    ANeuralNetworksCompilation_create(model, &compilation);
    

    Opcjonalnie możesz użyć przypisania urządzenia, aby bezpośrednio wybrać urządzenia, na których mają zostać uruchomione aplikacje.

  2. 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:

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

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

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

  4. Dokończ definicję kompilacji, wywołując metodę ANeuralNetworksCompilation_finish(). Jeśli nie ma błędów, funkcja zwraca kod wyniku ANEURALNETWORKS_NO_ERROR.

    ANeuralNetworksCompilation_finish(compilation);
    

Wykrywanie i przypisywanie urządzeń

Na urządzeniach z Androidem 10 (poziom interfejsu API 29) lub nowszym NNAPI udostępnia funkcje, które umożliwiają bibliotekom i aplikacjom platformy systemów uczących się uzyskiwanie informacji o dostępnych urządzeniach i określanie urządzeń, które mają 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:

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:

  1. Wywołaj funkcję ANeuralNetworksExecution_create(), aby utworzyć nową instancję wykonania.

    // Run the compiled model against a set of inputs
    ANeuralNetworksExecution* run1 = NULL;
    ANeuralNetworksExecution_create(compilation, &run1);
    
  2. Określ, gdzie aplikacja odczytuje wartości wejściowe obliczeń. Aplikacja może odczytywać wartości wejściowe z bufora użytkownika lub przydzielonego miejsca w pamięci, wywołując odpowiednio ANeuralNetworksExecution_setInput() lub ANeuralNetworksExecution_setInputFromMemory().

    // Set the single input to our sample model. Since it is small, we won't use a memory buffer
    float32 myInput[3][4] = { ...the data... };
    ANeuralNetworksExecution_setInput(run1, 0, NULL, myInput, sizeof(myInput));
    
  3. Określ, gdzie aplikacja 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() lub ANeuralNetworksExecution_setOutputFromMemory().

    // Set the output
    float32 myOutput[3][4];
    ANeuralNetworksExecution_setOutput(run1, 0, NULL, myOutput, sizeof(myOutput));
    
  4. Zaplanuj rozpoczęcie wykonywania, wywołując funkcję ANeuralNetworksExecution_startCompute(). Jeśli nie ma błędów, funkcja zwraca kod wyniku ANEURALNETWORKS_NO_ERROR.

    // Starts the work. The work proceeds asynchronously
    ANeuralNetworksEvent* run1_end = NULL;
    ANeuralNetworksExecution_startCompute(run1, &run1_end);
    
  5. Wywołaj funkcję ANeuralNetworksEvent_wait(), by poczekać na zakończenie wykonywania. Jeśli wykonanie się udało, funkcja zwraca kod wyniku o wartości ANEURALNETWORKS_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);
    
  6. 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:

  1. Wywołaj funkcję ANeuralNetworksMemoryDesc_create(), aby utworzyć nowy deskryptor pamięci:

    // Create a memory descriptor
    ANeuralNetworksMemoryDesc* desc;
    ANeuralNetworksMemoryDesc_create(&desc);
    
  2. Określ wszystkie wymagane role wejściowe i wyjściowe, wywołując metody ANeuralNetworksMemoryDesc_addInputRole() i ANeuralNetworksMemoryDesc_addOutputRole().

    // Specify that the memory may be used as the first input and the first output
    // of the compilation
    ANeuralNetworksMemoryDesc_addInputRole(desc, compilation, 0, 1.0f);
    ANeuralNetworksMemoryDesc_addOutputRole(desc, compilation, 0, 1.0f);
    
  3. Opcjonalnie określ wymiary pamięci, wywołując metodę ANeuralNetworksMemoryDesc_setDimensions().

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

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

    // Allocate two opaque memories with the descriptor
    ANeuralNetworksMemory* opaqueMem;
    ANeuralNetworksMemory_createFromDesc(desc, &opaqueMem);
    
  6. 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 NNAPI
  • IPC: komunikacja między procesami między środowiskiem wykonawczym NNAPI a kodem sterownika
  • Driver: 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:

  1. 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.

  1. 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.

  1. Gdy test się zakończy, przerwij systrace, naciskając enter na terminalu konsoli (aktywnym od kroku 1).

  2. 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 List recognizeImage(final Bitmap bitmap) {

 // This section won’t appear in the NNAPI systrace analysis
 Trace.beginSection("preprocessBitmap");
 convertBitmapToByteBuffer(bitmap);
 Trace.endSection();

 // Run the inference call.
 // Add this method in to NNAPI systrace analysis.
 Trace.beginSection("[NN_LA_PE]runInferenceModel");
 long startTime = SystemClock.uptimeMillis();
 runInference();
 long endTime = SystemClock.uptimeMillis();
 Trace.endSection();
  ...
 Trace.endSection();
 return recognitions;
}

Jakość usługi

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

Ustawianie priorytetu zadania

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

Wyznaczanie terminów

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

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:

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:

  1. 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.

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

  3. Przejdź do katalogu źródłowego najwyższego poziomu Androida.

  4. Uruchom te polecenia:

    lunch aosp_arm-userdebug # Or aosp_arm64-userdebug if available
    ./test/mlts/benchmark/build_and_run_benchmark.sh
    

    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 modeli
  • compilation: generowanie planu wykonania modelu i kompilacji
  • execution: wykonanie modelu
  • cpuexe: wykonywanie operacji przy użyciu implementacji procesora NNAPI
  • manager: rozszerzenia NNAPI, dostępne interfejsy i informacje związane z funkcjami
  • all lub 1: wszystkie elementy powyżej

Aby na przykład włączyć pełne logowanie szczegółowe, użyj polecenia adb shell setprop debug.nn.vlog all. Aby wyłączyć logowanie szczegółowe, użyj polecenia adb shell setprop debug.nn.vlog '""'.

Po włączeniu 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.