Buforowanie kompilacji

Od Androida 10 interfejs Neural Networks API (NNAPI) udostępnia funkcje obsługujące buforowanie artefaktów kompilacji, co skraca czas kompilacji podczas uruchamiania aplikacji. Dzięki tej funkcji buforowania sterownik nie musi zarządzać plikami w pamięci podręcznej ani ich usuwać. Jest to funkcja opcjonalna, którą można wdrożyć za pomocą NN HAL 1.2. Więcej informacji o tej funkcji znajdziesz w sekcji ANeuralNetworksCompilation_setCaching.

Sterownik może też implementować buforowanie kompilacji niezależnie od NNAPI. Można to wdrożyć niezależnie od tego, czy używane są funkcje buforowania NNAPI NDK i HAL. AOSP udostępnia bibliotekę narzędzi niskiego poziomu (silnik pamięci podręcznej). Więcej informacji znajdziesz w artykule Wdrażanie mechanizmu buforowania.

Omówienie przepływu pracy

W tej sekcji opisujemy ogólne przepływy pracy z zastosowaniem funkcji buforowania kompilacji.

Podano informacje z pamięci podręcznej i nastąpiło trafienie w 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 wykonania i wyniku podziału na partycje, a następnie znajduje te pliki.
  3. NNAPI otwiera pliki pamięci podręcznej i przekazuje uchwyty do sterownika za pomocą funkcji prepareModelFromCache.
  4. Sterownik przygotowuje model bezpośrednio z plików pamięci podręcznej i zwraca przygotowany model.

Informacje z pamięci podręcznej i brak danych w pamięci podręcznej

  1. Aplikacja przechodzi weryfikację sumy kontrolnej unikalnej dla modelu i katalogu pamięci podręcznej.
  2. Środowisko wykonawcze NNAPI szuka plików pamięci podręcznej na podstawie sumy kontrolnej, preferencji wykonania i wyniku podziału, ale nie znajduje plików pamięci podręcznej.
  3. NNAPI tworzy puste pliki pamięci podręcznej na podstawie sumy kontrolnej, preferencji wykonania i podziału, otwiera pliki pamięci podręcznej i przekazuje uchwyty oraz model do sterownika za pomocą funkcji prepareModel_1_2.
  4. Sterownik kompiluje model, zapisuje informacje o pamięci podręcznej w plikach pamięci podręcznej i zwraca przygotowany model.

Nie podano informacji o pamięci podręcznej

  1. Aplikacja wywołuje kompilację bez podawania informacji o pamięci podręcznej.
  2. Aplikacja nie przekazuje żadnych informacji związanych z pamięcią podręczną.
  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 przekazywane kierowcy składają się z tokena i uchwytów plików pamięci podręcznej.

Token

Token to token buforowania o długości Constant::BYTE_SIZE_OF_CACHE_TOKEN, który identyfikuje przygotowany model. Ten sam token jest podawany podczas zapisywania plików pamięci podręcznej za pomocą funkcji prepareModel_1_2 i pobierania przygotowanego modelu za pomocą funkcji prepareModelFromCache. Klient kierowcy powinien wybrać token o niskim współczynniku kolizji. Sterownik nie może wykryć kolizji tokenów. Kolizja powoduje nieudane wykonanie lub udane wykonanie, które generuje nieprawidłowe wartości wyjściowe.

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

Istnieją 2 typy plików pamięci podręcznej: pamięć podręczna danychpamięć podręczna modelu.

  • Pamięć podręczna danych: używaj jej do buforowania stałych danych, w tym wstępnie przetworzonych i przekształconych buforów tensorów. Zmiana w pamięci podręcznej danych nie powinna mieć gorszych skutków niż generowanie nieprawidłowych wartości wyjściowych w czasie wykonywania.
  • Pamięć podręczna modelu: służy do przechowywania w pamięci podręcznej danych wrażliwych z punktu widzenia bezpieczeństwa, takich jak skompilowany kod maszynowy w natywnym formacie binarnym urządzenia. Zmiana pamięci podręcznej modelu może wpłynąć na działanie sterownika, a złośliwy klient może to wykorzystać do wykonania działań wykraczających poza przyznane uprawnienia. Dlatego przed przygotowaniem modelu z pamięci podręcznej sterownik musi sprawdzić, czy nie jest ona uszkodzona. Więcej informacji znajdziesz w artykule Bezpieczeństwo.

Kierowca musi zdecydować, jak informacje z pamięci podręcznej mają być rozdzielone między 2 typy plików pamięci podręcznej, i zgłosić, ile plików pamięci podręcznej potrzebuje dla każdego typu, za pomocą parametru 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 z punktu widzenia bezpieczeństwa, takie jak skompilowany wykonywalny kod maszynowy w natywnym formacie binarnym urządzenia. Jeśli pamięć podręczna modelu nie jest odpowiednio chroniona, modyfikacja może wpłynąć na zachowanie sterownika. Zawartość pamięci podręcznej jest przechowywana w katalogu aplikacji, więc pliki pamięci podręcznej mogą być modyfikowane przez klienta. Klient z błędami może przypadkowo uszkodzić pamięć podręczną, a złośliwy klient może celowo wykorzystać tę sytuację do wykonania na urządzeniu niezweryfikowanego kodu. W zależności od charakterystyki urządzenia może to stanowić problem z bezpieczeństwem. Dlatego przed przygotowaniem modelu z pamięci podręcznej sterownik musi być w stanie wykryć potencjalne uszkodzenie pamięci podręcznej modelu.

Jednym ze sposobów na to jest utrzymywanie przez sterownik mapy tokena do kryptograficznego skrótu pamięci podręcznej modelu. Podczas zapisywania kompilacji w pamięci podręcznej sterownik może przechowywać token i hash pamięci podręcznej modelu. Podczas pobierania kompilacji z pamięci podręcznej sterownik sprawdza nowy hash pamięci podręcznej modelu z zarejestrowaną parą tokena i hashu. To mapowanie powinno być trwałe po ponownym uruchomieniu systemu. Kierowca może używać usługi magazynu kluczy Androida, biblioteki narzędziowej w framework/ml/nn/driver/cache lub innego odpowiedniego mechanizmu do wdrożenia 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 go w pliku i obliczyć nowy skrót po skopiowaniu zawartości pliku do wewnętrznego bufora.

Ten przykładowy kod pokazuje, jak wdrożyć 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:

  • Kompilacja w odpowiednim czasie: kompilacja jest opóźniona do pierwszego wykonania.
  • Kompilacja wieloetapowa: początkowo przeprowadzana jest szybka kompilacja, a później opcjonalna kompilacja zoptymalizowana, w zależności od częstotliwości używania.

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ływania funkcji prepareModel_1_2 lub prepareModelFromCache, a później odczytuje i aktualizuje zawartość pamięci podręcznej.
  • Implementuje logikę blokowania plików poza zwykłym wywołaniem kompilacji, aby zapobiec zapisowi występującemu jednocześnie z odczytem lub innym zapisem.

Wdrażanie mechanizmu buforowania

Oprócz interfejsu buforowania kompilacji NN HAL 1.2 w katalogu frameworks/ml/nn/driver/cache znajdziesz też bibliotekę narzędzi do buforowania. Podkatalog nnCache zawiera kod pamięci trwałej, który umożliwia sterownikowi implementację buforowania kompilacji bez używania funkcji buforowania NNAPI. Ta forma buforowania kompilacji może być zaimplementowana w dowolnej wersji NN HAL. Jeśli sterownik zdecyduje się wdrożyć buforowanie odłączone od interfejsu HAL, będzie odpowiedzialny za zwalnianie artefaktów z pamięci podręcznej, gdy nie będą już potrzebne.