Caching della compilazione

A partire da Android 10, la Neural Networks API (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. Utilizzando 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à opzionale che può essere implementata con NN HAL 1.2. Per ulteriori informazioni su questa funzione, vedere ANeuralNetworksCompilation_setCaching .

Il driver può anche implementare la memorizzazione nella cache di compilazione indipendentemente dalla NNAPI. Ciò può essere implementato indipendentemente dal fatto che vengano utilizzate o meno le funzionalità di memorizzazione nella cache NNAPI NDK e HAL. AOSP fornisce una libreria di utilità di basso livello (un motore di memorizzazione nella cache). Per ulteriori informazioni, vedere 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 sulla cache fornite e riscontro nella cache

  1. L'app passa una directory di memorizzazione nella cache e un checksum univoco per il modello.
  2. Il runtime NNAPI cerca i file di cache in base al checksum, alla preferenza 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 sulla cache fornite e mancanza di cache

  1. L'app trasmette 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, alla preferenza di esecuzione e al risultato del partizionamento e non trova i file di cache.
  3. La NNAPI crea file di cache vuoti in base al checksum, alla preferenza di esecuzione e al partizionamento, apre i file di 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 di cache e restituisce il modello preparato.

Informazioni sulla cache non fornite

  1. L'app richiama la compilazione senza fornire alcuna informazione sulla memorizzazione nella cache.
  2. L'app non trasmette nulla relativo 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 sulla cache

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

Gettone

Il token è un token di memorizzazione nella cache di lunghezza Constant::BYTE_SIZE_OF_CACHE_TOKEN che identifica il modello preparato. Lo stesso token viene fornito quando si salvano i file della cache con prepareModel_1_2 e si recupera il modello preparato con prepareModelFromCache . Il cliente dell'autista dovrebbe scegliere un gettone con un basso tasso di collisione. Il conducente non è in grado di rilevare una collisione di token. Una collisione provoca un'esecuzione non riuscita o un'esecuzione riuscita che produce valori di output errati.

Handle dei file di cache (due tipi di file di cache)

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

  • Cache dati: utilizzare per memorizzare nella cache dati costanti inclusi buffer tensoriali preelaborati e trasformati. Una modifica alla cache dei dati non dovrebbe comportare alcun effetto peggiore della generazione di valori di output errati in fase di esecuzione.
  • Cache modello: utilizzare per memorizzare nella cache dati sensibili alla sicurezza come 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 malintenzionato potrebbe farne uso per eseguire operazioni oltre l'autorizzazione concessa. Pertanto, il driver deve verificare se la cache del modello è danneggiata prima di preparare il modello dalla cache. Per ulteriori informazioni, vedere Sicurezza .

Il driver deve decidere come distribuire le informazioni sulla cache tra i due tipi di file di cache e segnalare quanti file di cache sono necessari per ciascun tipo con getNumberOfCacheFilesNeeded .

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

Sicurezza

Nella memorizzazione nella cache di compilazione, la cache del modello può contenere dati sensibili alla sicurezza come codice macchina eseguibile compilato nel formato binario nativo del dispositivo. Se non adeguatamente protetta, una modifica alla cache del modello potrebbe influire sul comportamento di esecuzione del driver. Poiché i contenuti della cache sono archiviati nella directory dell'app, i file della cache sono modificabili dal client. Un client difettoso 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, questo potrebbe rappresentare un problema di sicurezza. Pertanto, il driver deve essere in grado di rilevare il potenziale danneggiamento della cache del modello prima di preparare il modello dalla cache.

Un modo per farlo è che il driver mantenga una mappa dal token a un hash crittografico della cache del modello. Il driver può memorizzare 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 la coppia token e hash registrata durante il recupero della compilazione dalla cache. Questa mappatura dovrebbe essere persistente durante i riavvii del sistema. Il driver può utilizzare il servizio keystore 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 impedire la preparazione dei file di cache da una versione precedente.

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

Questo codice di esempio illustra 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 al contenuto della cache (lettura o scrittura) dopo la chiamata di compilazione. I casi d'uso di esempio includono:

  • Compilazione just-in-time: la compilazione viene ritardata fino alla prima esecuzione.
  • Compilazione in più fasi: inizialmente viene eseguita una compilazione veloce e successivamente viene eseguita una compilazione ottimizzata opzionale a seconda della frequenza di utilizzo.

Per accedere al contenuto della cache (in lettura o in scrittura) dopo la chiamata di compilazione, assicurarsi che il driver:

  • Duplica gli handle di file durante l'invocazione di prepareModel_1_2 o prepareModelFromCache e legge/aggiorna il contenuto della cache in un secondo momento.
  • Implementa la logica di blocco dei file al di fuori della normale chiamata di compilazione per impedire che una scrittura avvenga contemporaneamente a una lettura o a un'altra scrittura.

Implementare un motore di memorizzazione nella cache

Oltre all'interfaccia di caching della compilazione NN HAL 1.2, è anche possibile trovare una libreria di utilità di caching nella directory frameworks/ml/nn/driver/cache . La sottodirectory nnCache contiene codice di archiviazione persistente per il driver 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 di NN HAL. Se il driver sceglie di implementare la memorizzazione nella cache disconnessa dall'interfaccia HAL, il driver è responsabile della liberazione degli artefatti memorizzati nella cache quando non sono più necessari.