Buforowanie kompilacji

Od systemu Android 10 interfejs API sieci neuronowych (NNAPI) udostępnia funkcje obsługujące buforowanie artefaktów kompilacji, co skraca czas potrzebny na kompilację podczas uruchamiania aplikacji. Korzystając z tej funkcji buforowania, sterownik nie musi zarządzać ani czyścić buforowanych plików. Jest to opcjonalna funkcja, którą można zaimplementować za pomocą NN HAL 1.2. Aby uzyskać więcej informacji na temat tej funkcji, zobacz ANeuralNetworksCompilation_setCaching .

Sterownik może również zaimplementować buforowanie kompilacji niezależne od NNAPI. Można to zaimplementować niezależnie od tego, czy funkcje buforowania NNAPI NDK i HAL są używane, czy nie. AOSP zapewnia bibliotekę narzędzi niskiego poziomu (silnik buforowania). Aby uzyskać więcej informacji, zobacz Implementowanie mechanizmu buforowania .

Omówienie przepływu pracy

W tej sekcji opisano ogólne przepływy pracy z zaimplementowaną funkcją buforowania kompilacji.

Dostarczone informacje o pamięci podręcznej i trafienie w pamięci podręcznej

  1. Aplikacja przekazuje katalog pamięci podręcznej i sumę kontrolną unikatową dla modelu.
  2. Środowisko wykonawcze NNAPI wyszukuje pliki pamięci podręcznej na podstawie sumy kontrolnej, preferencji wykonywania oraz wyniku partycjonowania i znajduje pliki.
  3. NNAPI otwiera pliki pamięci podręcznej i przekazuje uchwyty do sterownika za pomocą prepareModelFromCache .
  4. Driver przygotowuje model bezpośrednio z plików pamięci podręcznej i zwraca przygotowany model.

Dostarczone informacje o pamięci podręcznej i brak pamięci podręcznej

  1. Aplikacja przekazuje sumę kontrolną unikalną dla modelu i katalogu pamięci podręcznej.
  2. Środowisko wykonawcze NNAPI szuka plików pamięci podręcznej na podstawie sumy kontrolnej, preferencji wykonywania i wyniku partycjonowania i nie znajduje plików pamięci podręcznej.
  3. NNAPI tworzy puste pliki pamięci podręcznej na podstawie sumy kontrolnej, preferencji wykonywania i partycjonowania, otwiera pliki pamięci podręcznej i przekazuje uchwyty oraz model do sterownika za pomocą prepareModel_1_2 .
  4. Sterownik kompiluje model, zapisuje informacje o pamięci podręcznej do plików pamięci podręcznej i zwraca przygotowany model.

Brak informacji o pamięci podręcznej

  1. Aplikacja wywołuje kompilację bez podawania jakichkolwiek informacji o pamięci podręcznej.
  2. Aplikacja nie przekazuje niczego związanego z buforowaniem.
  3. Środowisko uruchomieniowe NNAPI przekazuje model do sterownika za pomocą prepareModel_1_2 .
  4. Driver kompiluje model i zwraca przygotowany model.

Informacje o buforowaniu

Informacje o buforowaniu, które są dostarczane do sterownika, składają się z uchwytów pliku tokenu i pamięci podręcznej.

Znak

Token jest tokenem pamięci podręcznej o długości Constant::BYTE_SIZE_OF_CACHE_TOKEN , który identyfikuje przygotowany model. Ten sam token jest dostarczany podczas zapisywania plików pamięci podręcznej za pomocą prepareModel_1_2 i pobierania przygotowanego modelu za pomocą prepareModelFromCache . Klient kierowcy powinien wybrać token o niskim współczynniku kolizji. Kierowca nie może wykryć kolizji tokenów. Kolizja skutkuje nieudanym wykonaniem lub pomyślnym wykonaniem, które generuje nieprawidłowe wartości wyjściowe.

Uchwyty plików pamięci podręcznej (dwa typy plików pamięci podręcznej)

Dwa typy plików pamięci podręcznej to pamięć podręczna danych i pamięć podręczna modelu .

  • Pamięć podręczna danych: służy do buforowania stałych danych, w tym wstępnie przetworzonych i przekształconych buforów tensorowych. Modyfikacja pamięci podręcznej danych nie powinna skutkować gorszym skutkiem niż generowanie złych wartości wyjściowych w czasie wykonywania.
  • Pamięć podręczna modelu: służy do buforowania danych wrażliwych na bezpieczeństwo, takich jak skompilowany wykonywalny kod maszynowy w natywnym formacie binarnym urządzenia. Modyfikacja pamięci podręcznej modelu może wpłynąć na zachowanie wykonawcze sterownika, a złośliwy klient może to wykorzystać do wykonania poza przyznanymi uprawnieniami. Dlatego kierowca musi sprawdzić, czy pamięć podręczna modelu nie jest uszkodzona przed przygotowaniem modelu z pamięci podręcznej. Aby uzyskać więcej informacji, zobacz Bezpieczeństwo .

Sterownik musi zdecydować, w jaki sposób informacje o pamięci podręcznej są rozdzielane między dwa typy plików pamięci podręcznej, i zgłosić liczbę potrzebnych plików pamięci podręcznej dla każdego typu za pomocą getNumberOfCacheFilesNeeded .

Środowisko wykonawcze NNAPI zawsze otwiera uchwyty plików pamięci podręcznej z uprawnieniami do odczytu i zapisu.

Bezpieczeństwo

W przypadku buforowania kompilacji pamięć podręczna modelu może zawierać dane wrażliwe pod względem bezpieczeństwa, takie jak skompilowany wykonywalny kod maszynowy w natywnym formacie binarnym urządzenia. Jeśli nie jest odpowiednio chroniony, modyfikacja pamięci podręcznej modelu może wpłynąć na zachowanie wykonania sterownika. Ponieważ zawartość pamięci podręcznej jest przechowywana w katalogu aplikacji, pliki pamięci podręcznej mogą być modyfikowane przez klienta. Błędny klient może przypadkowo uszkodzić pamięć podręczną, a złośliwy klient może celowo wykorzystać to do wykonania niezweryfikowanego kodu na urządzeniu. W zależności od cech urządzenia może to stanowić problem z bezpieczeństwem. Dlatego sterownik musi być w stanie wykryć potencjalne uszkodzenie pamięci podręcznej modelu przed przygotowaniem modelu z pamięci podręcznej.

Jednym ze sposobów, aby to zrobić, jest utrzymywanie przez sterownik mapy z tokenu do kryptograficznego skrótu pamięci podręcznej modelu. Sterownik może przechowywać token i skrót swojej pamięci podręcznej modelu podczas zapisywania kompilacji w pamięci podręcznej. Sterownik sprawdza nowy skrót pamięci podręcznej modelu z zarejestrowanym tokenem i parą skrótów podczas pobierania kompilacji z pamięci podręcznej. To mapowanie powinno być trwałe po ponownym uruchomieniu systemu. Sterownik może korzystać z usługi magazynu kluczy Androida , biblioteki narzędziowej w framework/ml/nn/driver/cache lub dowolnego innego odpowiedniego mechanizmu do zaimplementowania menedżera mapowania. Po aktualizacji sterownika należy ponownie zainicjować ten menedżer mapowania, aby zapobiec przygotowywaniu plików pamięci podręcznej z wcześniejszej wersji.

Aby zapobiec atakom typu time-of-check to time-of-use (TOCTOU), sterownik musi obliczyć zarejestrowany skrót przed zapisaniem do pliku i obliczyć nowy skrót po skopiowaniu zawartości pliku do wewnętrznego bufora.

Ten przykładowy kod pokazuje, jak zaimplementować tę logikę.

bool saveToCache(const sp<V1_2::IPreparedModel> preparedModel,
                 const hidl_vec<hidl_handle>& modelFds, const hidl_vec<hidl_handle>& dataFds,
                 const HidlToken& token) {
    // Serialize the prepared model to internal buffers.
    auto buffers = serialize(preparedModel);

    // This implementation detail is important: the cache hash must be computed from internal
    // buffers instead of cache files to prevent time-of-check to time-of-use (TOCTOU) attacks.
    auto hash = computeHash(buffers);

    // Store the {token, hash} pair to a mapping manager that is persistent across reboots.
    CacheManager::get()->store(token, hash);

    // Write the cache contents from internal buffers to cache files.
    return writeToFds(buffers, modelFds, dataFds);
}

sp<V1_2::IPreparedModel> prepareFromCache(const hidl_vec<hidl_handle>& modelFds,
                                          const hidl_vec<hidl_handle>& dataFds,
                                          const HidlToken& token) {
    // Copy the cache contents from cache files to internal buffers.
    auto buffers = readFromFds(modelFds, dataFds);

    // This implementation detail is important: the cache hash must be computed from internal
    // buffers instead of cache files to prevent time-of-check to time-of-use (TOCTOU) attacks.
    auto hash = computeHash(buffers);

    // Validate the {token, hash} pair by a mapping manager that is persistent across reboots.
    if (CacheManager::get()->validate(token, hash)) {
        // Retrieve the prepared model from internal buffers.
        return deserialize<V1_2::IPreparedModel>(buffers);
    } else {
        return nullptr;
    }
}

Zaawansowane przypadki użycia

W niektórych zaawansowanych przypadkach użycia sterownik wymaga dostępu do zawartości pamięci podręcznej (odczyt lub zapis) po wywołaniu kompilacji. Przykładowe przypadki użycia obejmują:

  • Kompilacja just-in-time: Kompilacja jest opóźniona do pierwszego wykonania.
  • Kompilacja wieloetapowa: Szybka kompilacja jest wykonywana na początku, a opcjonalna kompilacja zoptymalizowana jest wykonywana później, w zależności od częstotliwości użytkowania.

Aby uzyskać dostęp do zawartości pamięci podręcznej (odczyt lub zapis) po wywołaniu kompilacji, upewnij się, że sterownik:

  • Duplikuje uchwyty pliku podczas wywoływania metody prepareModel_1_2 lub prepareModelFromCache i odczytuje/aktualizuje zawartość pamięci podręcznej w późniejszym czasie.
  • Implementuje logikę blokowania plików poza zwykłym wywołaniem kompilacji, aby zapobiec współbieżnemu zapisowi z odczytem lub innym zapisem.

Implementacja silnika pamięci podręcznej

Oprócz interfejsu buforowania kompilacji NN HAL 1.2 można również znaleźć bibliotekę narzędzi do buforowania w katalogu frameworks/ml/nn/driver/cache . nnCache zawiera trwały kod pamięci dla sterownika, aby zaimplementować buforowanie kompilacji bez korzystania z funkcji buforowania NNAPI. Ta forma buforowania kompilacji może być zaimplementowana w dowolnej wersji NN HAL. Jeśli sterownik zdecyduje się zaimplementować buforowanie odłączone od interfejsu HAL, jest odpowiedzialny za zwolnienie buforowanych artefaktów, gdy nie są już potrzebne.