Buforowanie kompilacji

Od systemu Android 10 interfejs Neural Networks API (NNAPI) udostępnia funkcje obsługujące buforowanie artefaktów kompilacji, co skraca czas kompilacji po uruchomieniu aplikacji. Korzystając z tej funkcji buforowania, sterownik nie musi zarządzać plikami w pamięci podręcznej ani ich czyścić. Jest to opcjonalna funkcja, którą można zaimplementować w NN HAL 1.2. Aby uzyskać więcej informacji na temat tej funkcji, zobacz ANeuralNetworksCompilation_setCaching .

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

Przegląd przepływu pracy

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

Dostarczono informacje o pamięci podręcznej i trafiono do pamięci podręcznej

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

Dostarczono 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, ale nie znajduje plików pamięci podręcznej.
  3. NNAPI tworzy puste pliki pamięci podręcznej w oparciu o sumę kontrolną, preferencje wykonania i partycjonowanie, otwiera pliki pamięci podręcznej i przekazuje uchwyty i model do sterownika za pomocą prepareModel_1_2 .
  4. Sterownik kompiluje model, zapisuje informacje o buforowaniu do plików pamięci podręcznej i zwraca przygotowany model.

Nie podano informacji o pamięci podręcznej

  1. Aplikacja wywołuje kompilację bez podawania żadnych informacji o buforowaniu.
  2. Aplikacja nie przekazuje żadnych informacji związanych z buforowaniem.
  3. Środowisko wykonawcze NNAPI przekazuje model do sterownika za pomocą prepareModel_1_2 .
  4. Sterownik kompiluje model i zwraca przygotowany model.

Informacje o pamięci podręcznej

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

Znak

Token jest tokenem buforującym o długości Constant::BYTE_SIZE_OF_CACHE_TOKEN , który identyfikuje przygotowany model. Ten sam token jest udostępniany 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 tokena. Kolizja skutkuje niepowodzeniem wykonania 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ć żadnym skutkiem gorszym niż wygenerowanie 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 sterownika, a złośliwy klient może to wykorzystać do wykonania operacji wykraczającej poza przyznane uprawnienia. Dlatego sterownik 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 zostaną rozdzielone pomiędzy dwoma typami 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 na bezpieczeństwo, takie jak skompilowany wykonywalny kod maszynowy w natywnym formacie binarnym urządzenia. Jeśli modyfikacja pamięci podręcznej modelu nie jest odpowiednio zabezpieczona, może wpłynąć na zachowanie sterownika. Ponieważ zawartość pamięci podręcznej jest przechowywana w katalogu aplikacji, klient może modyfikować pliki pamięci podręcznej. Klient z błędami 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 charakterystyki urządzenia może to stanowić problem bezpieczeństwa. 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 osiągnięcia tego jest utrzymanie przez sterownik mapy od tokena do kryptograficznego skrótu pamięci podręcznej modelu. Sterownik może przechowywać token i skrót pamięci podręcznej swojego 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 podczas ponownego uruchamiania systemu. Sterownik może skorzystać z usługi magazynu kluczy systemu Android , biblioteki narzędziowej w framework/ml/nn/driver/cache lub dowolnego innego odpowiedniego mechanizmu w celu wdrożenia menedżera mapowania. Po aktualizacji sterownika należy ponownie zainicjować tego menedżera 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 demonstruje, 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 (odczytu lub zapisu) po wywołaniu kompilacji. Przykładowe przypadki użycia obejmują:

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

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

  • Duplikuje uchwyty plików podczas wywołania 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 jednoczesnemu zapisowi z odczytem lub innym zapisem.

Zaimplementuj silnik buforujący

Oprócz interfejsu buforowania kompilacji NN HAL 1.2, w katalogu frameworks/ml/nn/driver/cache można także znaleźć bibliotekę narzędziową do buforowania. Podkatalog nnCache zawiera trwały kod pamięci dla sterownika umożliwiający implementację buforowania kompilacji bez korzystania z funkcji buforowania NNAPI. Tę formę buforowania kompilacji można zaimplementować w dowolnej wersji NN HAL. Jeśli sterownik zdecyduje się na wdrożenie buforowania odłączonego od interfejsu HAL, jest on odpowiedzialny za zwolnienie artefaktów z pamięci podręcznej, gdy nie są już potrzebne.