A partire da Android 10, l'API Neural Networks (NNAPI)
fornisce funzioni per supportare
la memorizzazione nella cache degli artefatti di compilazione, il che riduce il tempo utilizzato per la compilazione
all'avvio di un'app. Utilizzando questa funzionalità di memorizzazione nella cache, il driver non
deve gestire o pulire i file memorizzati nella cache. Questa è una funzionalità facoltativa che
può essere implementata con NN HAL 1.2. Per saperne di più su questa funzione,
consulta
ANeuralNetworksCompilation_setCaching
.
Il driver può anche implementare la memorizzazione nella cache della compilazione indipendentemente dall'NNAPI. Questa può essere implementata indipendentemente dall'utilizzo o meno delle funzionalità di memorizzazione nella cache di NNAPI NDK e HAL. AOSP fornisce una libreria di utilità di basso livello (un motore di memorizzazione nella cache). Per saperne di più, 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 della compilazione implementata.
Informazioni sulla cache fornite e successo della cache
- L'app supera una directory di memorizzazione nella cache e un checksum univoco per il modello.
- L'ambiente di runtime NNAPI cerca i file della cache in base al checksum, alla preferenza di esecuzione e al risultato del partizionamento e trova i file.
- L'API NN apre i file della cache e passa gli handle al driver
con
prepareModelFromCache
. - Il driver prepara il modello direttamente dai file della cache e restituisce il modello preparato.
Informazioni sulla cache fornite e mancata corrispondenza della cache
- L'app supera un checksum univoco per il modello e una directory di memorizzazione nella cache.
- 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 della cache.
- L'API NN 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
. - Il driver compila il modello, scrive le informazioni di memorizzazione nella cache nei file della cache e restituisce il modello preparato.
Informazioni sulla cache non fornite
- L'app richiama la compilazione senza fornire informazioni sulla memorizzazione nella cache.
- L'app non trasmette nulla di correlato alla memorizzazione nella cache.
- Il runtime NNAPI passa il modello al driver con
prepareModel_1_2
. - Il driver compila il modello e restituisce il modello preparato.
Informazioni sulla cache
Le informazioni sulla memorizzazione nella cache fornite a un autista 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. 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 client del conducente deve scegliere un token con un
basso tasso di collisione. Il conducente non riesce a rilevare una collisione di token. Una collisione
comporta un'esecuzione non riuscita o un'esecuzione riuscita che produce
valori di output errati.
Handle dei file della cache (due tipi di file della cache)
I due tipi di file della cache sono cache dei dati e cache dei modelli.
- Cache dei dati:utilizzala per memorizzare nella cache i dati costanti, inclusi i buffer tensore preelaborati e trasformati. Una modifica alla cache dei dati non deve avere un effetto peggiore della generazione di valori di output errati al momento dell'esecuzione.
- Cache del modello:utilizzala per memorizzare nella cache dati sensibili alla sicurezza, ad esempio 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 sfruttare questa situazione per eseguire operazioni al di là dell'autorizzazione concessa. Pertanto, il conducente deve verificare se la cache del modello è danneggiata prima di preparare il modello dalla cache. Per ulteriori informazioni, vedi Sicurezza.
Il driver deve decidere come distribuire le informazioni della cache tra i due tipi di file della cache e segnalare il numero di file della cache necessari per ogni tipo con getNumberOfCacheFilesNeeded
.
Il runtime NNAPI apre sempre gli handle dei file della cache con autorizzazioni di lettura e scrittura.
Sicurezza
Nella memorizzazione nella cache della compilazione, la cache del modello può contenere dati sensibili alla sicurezza, ad esempio codice macchina eseguibile compilato nel formato binario nativo del dispositivo. Se non protetta correttamente, 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 con bug potrebbe danneggiare accidentalmente la cache e un client dannoso potrebbe intenzionalmente utilizzarla per eseguire codice non verificato sul dispositivo. A seconda delle caratteristiche del dispositivo, questo potrebbe essere un problema di sicurezza. Pertanto, il driver deve essere in grado di rilevare una potenziale corruzione della cache del modello prima di preparare il modello dalla cache.
Un modo per farlo è che il driver mantenga una mappatura dal token a un hash crittografico della cache del modello. Il driver può archiviare il token e l'hash della cache del modello quando salva la compilazione nella cache. Il driver controlla
il nuovo hash della cache del modello con la coppia di token e hash registrata quando
recupera la compilazione dalla cache. Questa mappatura deve essere persistente
durante i riavvii del sistema. Il conducente 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 mapping. Dopo l'aggiornamento
del driver, questo gestore di mapping deve essere reinizializzato per evitare la preparazione dei file
della cache da 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 il contenuto del file in un buffer interno.
Questo codice campione 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 (lettura o scrittura) dopo la chiamata di compilazione. Esempi di casi d'uso:
- Compilazione just-in-time: la compilazione viene ritardata fino alla prima esecuzione.
- Compilazione in più fasi: inizialmente viene eseguita una compilazione rapida e, in un secondo momento, viene eseguita una compilazione ottimizzata facoltativa a seconda della frequenza di utilizzo.
Per accedere ai contenuti della cache (lettura o scrittura) dopo la chiamata di compilazione, assicurati che il driver:
- Duplica gli handle dei file durante l'invocazione di
prepareModel_1_2
oprepareModelFromCache
e legge/aggiorna i contenuti 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.
Implementa un motore di memorizzazione nella cache
Oltre all'interfaccia di memorizzazione nella cache della compilazione NN HAL 1.2, puoi trovare anche una
libreria di utilità di memorizzazione nella cache nella directory
frameworks/ml/nn/driver/cache
. La sottodirectory
nnCache
contiene il codice di archiviazione permanente per l'implementazione della memorizzazione nella cache della compilazione da parte del driver
senza utilizzare le funzionalità di memorizzazione nella cache dell'API NN. Questa forma di
memorizzazione nella cache della 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
del rilascio degli artefatti memorizzati nella cache quando non sono più necessari.