Memorizzazione nella cache delle compilazioni

A partire da Android 10, l'API Neural Networks (NNAPI) fornisce funzioni per supportare la memorizzazione nella cache degli artefatti di compilazione, riducendo il tempo utilizzato per la compilazione all'avvio di un'app. Grazie a questa funzionalità di memorizzazione nella cache, il driver non ha bisogno di gestire o pulire i file memorizzati nella cache. Si tratta di una funzionalità facoltativa che può essere implementata con NN HAL 1.2. Per maggiori informazioni su questa funzione, consulta ANeuralNetworksCompilation_setCaching.

Il driver può anche implementare la memorizzazione nella cache di compilazione indipendentemente dall'NNAPI. Questa può essere implementata indipendentemente dall'utilizzo o meno delle funzionalità di memorizzazione nella cache NNAPI NDK e HAL. AOSP offre una libreria di utilità di basso livello (un motore di memorizzazione nella cache). Per ulteriori informazioni, consulta Implementazione di un motore di memorizzazione nella cache.

Panoramica del flusso di lavoro

Questa sezione descrive i flussi di lavoro generali con la funzionalità di memorizzazione nella cache di compilazione implementata.

Informazioni cache fornite e successo della cache

  1. L'app passa una directory di memorizzazione nella cache e un checksum univoci per il modello.
  2. Il runtime NNAPI cerca i file della cache in base al checksum, alle preferenze di esecuzione e al risultato del partizionamento e trova i file.
  3. La NNAPI apre i file della cache e passa gli handle al driver con prepareModelFromCache.
  4. Il driver prepara il modello direttamente dai file della cache e restituisce il modello preparato.

Informazioni cache fornite e fallimento della cache

  1. L'app passa un checksum univoco per il modello e una directory di memorizzazione nella cache.
  2. Il runtime NNAPI cerca i file di memorizzazione nella cache in base al checksum, alle preferenze di esecuzione e al risultato del partizionamento e non trova i file della cache.
  3. La NNAPI crea file di cache vuoti in base al checksum, alla preferenza di esecuzione e al partizionamento, apre i file della cache e passa gli handle e il modello al driver con prepareModel_1_2.
  4. Il driver compila il modello, scrive le informazioni di memorizzazione nella cache nei file della cache e restituisce il modello preparato.

Informazioni cache non fornite

  1. L'app richiama la compilazione senza fornire informazioni sulla memorizzazione nella cache.
  2. L'app non trasmette nulla riguardo alla memorizzazione nella cache.
  3. Il runtime NNAPI passa il modello al driver con prepareModel_1_2.
  4. Il driver compila il modello e restituisce il modello preparato.

Informazioni cache

Le informazioni di memorizzazione nella cache fornite a un driver sono costituite da un token e da handle di file della cache.

Token

Il token è un token di memorizzazione nella cache di lunghezza Constant::BYTE_SIZE_OF_CACHE_TOKEN che identifica il modello preparato. Viene fornito lo stesso token durante il salvataggio dei file cache con prepareModel_1_2 e il recupero del modello preparato con prepareModelFromCache. Il client del conducente deve scegliere un token con una bassa percentuale di collisione. Il conducente non è in grado di rilevare una collisione tra token. Una collisione causa un'esecuzione non riuscita o un'esecuzione riuscita che produce valori di output errati.

Handle dei file cache (due tipi di file cache)

I due tipi di file di cache sono cache dei dati e cache del modello.

  • Cache di dati: da utilizzare per memorizzare nella cache i dati costanti, inclusi i buffer dei tensori pre-elaborati e trasformati. Una modifica alla cache dei dati non dovrebbe comportare un effetto peggiore rispetto alla generazione di valori di output errati al momento dell'esecuzione.
  • Cache del modello: da utilizzare per la memorizzazione nella cache di dati sensibili per la sicurezza, come il codice macchina eseguibile compilato nel formato binario nativo del dispositivo. Una modifica alla cache del modello potrebbe influire sul comportamento di esecuzione del driver e un client dannoso potrebbe farne uso per eseguire oltre l'autorizzazione concessa. Di conseguenza, prima di preparare il modello dalla cache, il driver controlla se la cache del modello è danneggiata. Per maggiori informazioni, vedi Sicurezza.

Il conducente deve decidere in che modo distribuire le informazioni relative alla cache tra i due tipi di file di cache e indicare il numero di file di cache necessari per ogni tipo con getNumberOfCacheFilesNeeded.

Il runtime NNAPI apre sempre gli handle dei file della cache con autorizzazione di lettura e scrittura.

Sicurezza

Nella memorizzazione nella cache di compilazione, la cache del modello può contenere dati sensibili per la sicurezza, come il codice macchina eseguibile compilato nel formato binario nativo del dispositivo. Se non protette correttamente, una modifica alla cache del modello potrebbe influire sul comportamento di esecuzione del driver. Poiché i contenuti della cache sono archiviati nella directory delle app, i file della cache sono modificabili dal client. Un client con bug potrebbe danneggiare accidentalmente la cache e un client dannoso potrebbe farne uso intenzionalmente per eseguire codice non verificato sul dispositivo. A seconda delle caratteristiche del dispositivo, potrebbe trattarsi di un problema di sicurezza. Il driver deve quindi essere in grado di rilevare un potenziale danneggiamento della cache del modello prima di preparare il modello dalla cache.

Un modo per farlo è che il conducente conservi una mappa dal token a un hash crittografico della cache del modello. Il driver può archiviare il token e l'hash della cache del modello durante il salvataggio della compilazione nella cache. Il driver controlla il nuovo hash della cache del modello con il token e la coppia di hash registrati durante il recupero della compilazione dalla cache. Questa mappatura deve essere permanente tra i riavvii di sistema. Il driver può utilizzare il servizio di archivio chiavi di Android, la libreria di utilità in framework/ml/nn/driver/cache o qualsiasi altro meccanismo adatto per implementare un gestore di mappatura. Dopo l'aggiornamento del driver, questo gestore di mappatura deve essere reinizializzato per evitare di preparare i file della cache di una versione precedente.

Per evitare attacchi time-of-check to time-of-use (TOCTOU), il driver deve calcolare l'hash registrato prima di salvare nel file e calcolare il nuovo hash dopo aver copiato i contenuti del file in un buffer interno.

Questo codice di esempio mostra come implementare questa logica.

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;
    }
}

Casi d'uso avanzati

In alcuni casi d'uso avanzati, un driver richiede l'accesso ai contenuti della cache (in lettura o scrittura) dopo la chiamata di compilazione. Esempi di casi d'uso includono:

  • Compilazione just-in-time: la compilazione viene ritardata fino alla prima esecuzione.
  • Compilazione in più fasi: viene eseguita una compilazione rapida e una compilazione ottimizzata facoltativa viene eseguita in un secondo momento in base alla frequenza di utilizzo.

Per accedere ai contenuti della cache (lettura o scrittura) dopo la chiamata di compilazione, assicurati che il driver:

  • Duplica gli handle del file durante la chiamata di prepareModel_1_2 o prepareModelFromCache e legge/aggiorna i contenuti della cache in un secondo momento.
  • Implementa una logica di blocco dei file al di fuori della normale chiamata di compilazione per evitare che una scrittura si verifichi in concomitanza con una lettura o un'altra scrittura.

Implementazione di un motore di memorizzazione nella cache

Oltre all'interfaccia di memorizzazione nella cache per la compilazione NN HAL 1.2, nella directory frameworks/ml/nn/driver/cache puoi trovare anche una libreria di utilità di memorizzazione nella cache. La sottodirectory nnCache contiene un codice di archiviazione permanente che il driver può utilizzare per implementare la memorizzazione nella cache di compilazione senza utilizzare le funzionalità di memorizzazione nella cache NNAPI. Questa forma di memorizzazione nella cache di compilazione può essere implementata con qualsiasi versione dell'HAL NN. Se il driver sceglie di implementare la memorizzazione nella cache disconnessa dall'interfaccia HAL, è responsabile di liberare gli artefatti memorizzati nella cache quando non sono più necessari.