Buforowanie kompilacji

Począwszy od Androida 10 interfejs Neural Networks API (NNAPI) udostępnia funkcje umożliwiające buforowanie artefaktów kompilacji, co skraca czas potrzebny na kompilację przy uruchamianiu aplikacji. Dzięki tej funkcji buforowania sterownik nie musi zarządzać plikami w pamięci podręcznej ani ich czyścić. To opcjonalna funkcja, którą można wdrożyć za pomocą NN HAL 1.2. Więcej informacji o tej funkcji znajdziesz na stronie ANeuralNetworksCompilation_setCaching.

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

Omówienie przepływu pracy

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

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

  1. Aplikacja przekazuje katalog buforowania i sumę kontrolną unikalną dla modelu.
  2. Środowisko wykonawcze NNAPI wyszukuje pliki pamięci podręcznej na podstawie sumy kontrolnej, preferencji wykonania i wyniku partycjonowania, a następnie znajduje 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 gotowy model.

Podano informacje dotyczące pamięci podręcznej i jej brak

  1. Aplikacja przekazuje sumę kontrolną unikalną dla modelu i katalogu buforowania.
  2. Środowisko wykonawcze NNAPI wyszukuje pliki w pamięci podręcznej na podstawie sumy kontrolnej, preferencji wykonania i wyniku partycjonowania, ale nie znajduje plików pamięci podręcznej.
  3. NNAPI tworzy puste pliki pamięci podręcznej na podstawie sumy kontrolnej, ustawienia wykonania i partycjonowania, otwiera pliki pamięci podręcznej oraz przekazuje uchwyty i model do sterownika za pomocą 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 żadnych informacji o pamięci podręcznej.
  2. Aplikacja nie przekazuje żadnych danych związanych z buforowaniem.
  3. Środowisko wykonawcze NNAPI przekazuje model do sterownika za pomocą prepareModel_1_2.
  4. Sterownik skompiluje model i zwróci gotowy model.

Informacje o pamięci podręcznej

Informacje dotyczące buforowania przekazywane do sterownika 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 z użyciem funkcji prepareModel_1_2 i pobierania przygotowanego modelu za pomocą funkcji prepareModelFromCache. Klient kierowcy powinien wybrać token o małej częstotliwości kolizji. Kierowca nie może wykryć kolizji tokenów. Kolizja powoduje niepowodzenie lub pomyślne wykonanie, które zwraca nieprawidłowe wartości wyjściowe.

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

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

  • Pamięć podręczna na dane: służy do buforowania stałych danych, w tym wstępnie przetworzonych i przekształconych buforów tensorów. Modyfikacja pamięci podręcznej danych nie powinna spowodować gorszego efektu niż generowanie nieprawidłowych wartości wyjściowych podczas wykonywania.
  • Pamięć podręczna modelu: służy do buforowania danych wrażliwych dotyczących bezpieczeństwa, takich jak skompilowany wykonywalny kod maszynowy w natywnym formacie binarnym urządzenia. Modyfikacja pamięci podręcznej modelu może wpłynąć na działanie sterownika, a szkodliwy klient może wykorzystać tę możliwość do wykonania poza przyznanym uprawnieniem. Dlatego przed przygotowaniem modelu z pamięci podręcznej sterownik musi sprawdzić, czy pamięć podręczna modelu nie jest uszkodzona. Więcej informacji znajdziesz w artykule Bezpieczeństwo.

Sterownik musi określić sposób rozdzielenia informacji o pamięci podręcznej między 2 typy plików pamięci podręcznej i zgłosić za pomocą getNumberOfCacheFilesNeeded, ile plików pamięci podręcznej potrzebuje dla każdego typu.

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

Zabezpieczenia

Podczas 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 nie jest odpowiednio zabezpieczona, modyfikacja pamięci podręcznej modelu może wpłynąć na działanie sterownika. Zawartość pamięci podręcznej jest przechowywana w katalogu aplikacji, dlatego klient może modyfikować pliki pamięci podręcznej. Wadliwy klient może przypadkowo uszkodzić pamięć podręczną, co z kolei szkodliwy klient może celowo wykorzystać do wykonania niezweryfikowanego kodu na urządzeniu. W zależności od cech urządzenia może to być problem z bezpieczeństwem. Oznacza to, że 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 jest zachowanie przez sterownika mapowania z tokena na hasz kryptograficzny pamięci podręcznej modelu. Sterownik może zapisać token i szyfr pamięci podręcznej modelu podczas zapisywania kompilacji w pamięci podręcznej. Podczas pobierania kompilacji z pamięci podręcznej sterownik sprawdza nowy hasz pamięci podręcznej modelu z zarejestrowaną parą tokena i skrótu. Mapowanie powinno być trwałe po ponownym uruchomieniu systemu. Sterownik może użyć usługi magazynu kluczy Androida, biblioteki narzędzi w framework/ml/nn/driver/cache lub innego odpowiedniego mechanizmu do wdrożenia menedżera map. 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 od kontroli do momentu użycia (TOCTOU), sterownik musi obliczyć zarejestrowany hasz przed zapisaniem pliku w pliku i obliczyć go 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 (do odczytu lub zapisu) po wywołaniu kompilacji. Przykładowe zastosowania:

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

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

  • Powiela uchwyty plików podczas wywoływania prepareModel_1_2 lub prepareModelFromCache i odczytuje/aktualizuje zawartość pamięci podręcznej później.
  • Stosuje logikę blokowania plików poza zwykłym wywołaniem kompilacji, aby zapobiec jednoczesnemu odczytowi lub innym zapisom.

Wdróż mechanizm buforowania

Oprócz interfejsu buforowania kompilacji NN HAL 1.2 w katalogu frameworks/ml/nn/driver/cache znajdziesz też bibliotekę narzędzia do buforowania. Podkatalog nnCache zawiera kod trwałej pamięci masowej, który umożliwia sterownikowi wdrożenie buforowania kompilacji bez korzystania z funkcji buforowania NNAPI. Tę formę buforowania kompilacji można wdrożyć w dowolnej wersji NN HAL. Jeśli sterownik zdecyduje się wdrożyć buforowanie odłączone od interfejsu HAL, sterownik jest odpowiedzialny za zwalnianie artefaktów z pamięci podręcznej, gdy nie są już potrzebne.