Ab Android 10 bietet die Neural Networks API (NNAPI) Funktionen zur Unterstützung des Caching von Kompilierungsartefakten. Dadurch wird die für die Kompilierung benötigte Zeit beim Start einer App verkürzt. Bei Verwendung dieser Caching-Funktion muss der Treiber die zwischengespeicherten Dateien nicht verwalten oder bereinigen. Dies ist eine optionale Funktion, die mit NN HAL 1.2 implementiert werden kann. Weitere Informationen zu dieser Funktion finden Sie unter ANeuralNetworksCompilation_setCaching
.
Der Treiber kann auch unabhängig von der NNAPI das Zwischenspeichern von Kompilierungen implementieren. Dies kann unabhängig davon implementiert werden, ob die NNAPI NDK- und HAL-Caching-Funktionen verwendet werden. AOSP bietet eine Low-Level-Dienstprogrammbibliothek (eine Caching-Engine). Weitere Informationen finden Sie unter Caching-Engine implementieren.
Workflowübersicht
In diesem Abschnitt werden allgemeine Workflows mit der implementierten Funktion zum Zwischenspeichern der Kompilierung beschrieben.
Bereitgestellte Cache-Informationen und Cache-Treffer
- Die App übergibt ein Caching-Verzeichnis und eine für das Modell eindeutige Prüfsumme.
- Die NNAPI-Laufzeit sucht anhand der Prüfsumme, der Ausführungspräferenz und des Partitionierungsergebnisses nach den Cache-Dateien und findet sie.
- Die NNAPI öffnet die Cache-Dateien und übergibt die Handles mit
prepareModelFromCache
an den Treiber. - Der Treiber bereitet das Modell direkt aus den Cache-Dateien vor und gibt das vorbereitete Modell zurück.
Cache-Informationen bereitgestellt und Cache-Fehler
- Die App übergibt eine für das Modell eindeutige Prüfsumme und ein Caching-Verzeichnis.
- Die NNAPI-Laufzeit sucht anhand der Prüfsumme, der Ausführungseinstellung und des Partitionierungsergebnisses nach den Caching-Dateien, findet sie aber nicht.
- Die NNAPI erstellt leere Cache-Dateien basierend auf der Prüfsumme, der Ausführungspräferenz und der Partitionierung, öffnet die Cache-Dateien und übergibt die Handles und das Modell mit
prepareModel_1_2
an den Treiber. - Der Treiber kompiliert das Modell, schreibt Caching-Informationen in die Cache-Dateien und gibt das vorbereitete Modell zurück.
Keine Cache-Informationen angegeben
- Die App ruft die Kompilierung auf, ohne Informationen zum Caching bereitzustellen.
- Die App übergibt nichts im Zusammenhang mit dem Caching.
- Die NNAPI-Laufzeit übergibt das Modell mit
prepareModel_1_2
an den Treiber. - Der Treiber kompiliert das Modell und gibt das vorbereitete Modell zurück.
Cache-Informationen
Die Caching-Informationen, die einem Fahrer zur Verfügung gestellt werden, bestehen aus einem Token und Cache-Dateihandles.
Token
Das Token ist ein Caching-Token der Länge Constant::BYTE_SIZE_OF_CACHE_TOKEN
, das das vorbereitete Modell identifiziert. Derselbe Token wird bereitgestellt, wenn die Cache-Dateien mit prepareModel_1_2
gespeichert und das vorbereitete Modell mit prepareModelFromCache
abgerufen wird. Der Client des Treibers sollte ein Token mit einer geringen Kollisionsrate auswählen. Der Treiber kann keine Token-Kollision erkennen. Eine Kollision führt zu einer fehlgeschlagenen Ausführung oder zu einer erfolgreichen Ausführung, bei der falsche Ausgabewerte erzeugt werden.
Cache-Datei-Handles (zwei Arten von Cache-Dateien)
Es gibt zwei Arten von Cache-Dateien: Datencache und Modellcache.
- Datencache:Wird zum Zwischenspeichern konstanter Daten verwendet, einschließlich vorverarbeiteter und transformierter Tensorpuffer. Eine Änderung am Datencache sollte keine schlimmeren Auswirkungen haben als die Generierung falscher Ausgabewerte zur Laufzeit.
- Modellcache:Wird zum Zwischenspeichern sicherheitssensibler Daten wie kompilierten ausführbaren Maschinencodes im nativen Binärformat des Geräts verwendet. Eine Änderung des Modell-Cache kann sich auf das Ausführungsverhalten des Treibers auswirken. Ein schädlicher Client könnte dies nutzen, um über die gewährte Berechtigung hinaus zu agieren. Daher muss der Treiber prüfen, ob der Modellcache beschädigt ist, bevor er das Modell aus dem Cache vorbereitet. Weitere Informationen finden Sie unter Sicherheit.
Der Treiber muss entscheiden, wie Cacheinformationen auf die beiden Arten von Cachedateien verteilt werden, und mit getNumberOfCacheFilesNeeded
angeben, wie viele Cachedateien er für jeden Typ benötigt.
Die NNAPI-Laufzeit öffnet Cache-Dateihandles immer mit Lese- und Schreibberechtigung.
Sicherheit
Beim Kompilierungscaching kann der Modellcache sicherheitssensible Daten wie kompilierten ausführbaren Maschinencode im nativen Binärformat des Geräts enthalten. Wenn der Modellcache nicht richtig geschützt ist, kann eine Änderung daran das Ausführungsverhalten des Treibers beeinträchtigen. Da die Cacheinhalte im App-Verzeichnis gespeichert werden, können die Cachedateien vom Client geändert werden. Ein fehlerhafter Client kann den Cache versehentlich beschädigen und ein böswilliger Client könnte dies absichtlich nutzen, um nicht überprüften Code auf dem Gerät auszuführen. Je nach den Eigenschaften des Geräts kann dies ein Sicherheitsproblem darstellen. Daher muss der Treiber in der Lage sein, eine potenzielle Beschädigung des Modellcaches zu erkennen, bevor das Modell aus dem Cache vorbereitet wird.
Eine Möglichkeit hierfür besteht darin, dass der Treiber eine Zuordnung vom Token zu einem kryptografischen Hash des Modell-Cache verwaltet. Der Treiber kann das Token und den Hash des Modell-Cache speichern, wenn die Kompilierung im Cache gespeichert wird. Der Treiber vergleicht den neuen Hash des Modellcaches mit dem aufgezeichneten Token- und Hash-Paar, wenn die Kompilierung aus dem Cache abgerufen wird. Diese Zuordnung sollte auch nach einem Neustart des Systems bestehen bleiben. Der Fahrer kann den Android-Keystore-Dienst, die Dienstprogrammbibliothek in framework/ml/nn/driver/cache
oder einen anderen geeigneten Mechanismus verwenden, um einen Mapping-Manager zu implementieren. Nach einem Treiberupdate sollte dieser Zuordnungsmanager neu initialisiert werden, um zu verhindern, dass Cache-Dateien aus einer früheren Version vorbereitet werden.
Um TOCTOU-Angriffe (Time-of-Check to Time-of-Use) zu verhindern, muss der Treiber den aufgezeichneten Hash berechnen, bevor er in einer Datei gespeichert wird, und den neuen Hash berechnen, nachdem der Dateiinhalt in einen internen Puffer kopiert wurde.
In diesem Beispielcode wird gezeigt, wie Sie diese Logik implementieren.
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;
}
}
Komplexere Anwendungsfälle
In bestimmten erweiterten Anwendungsfällen benötigt ein Treiber nach dem Kompilierungsaufruf Zugriff auf den Cache-Inhalt (Lesen oder Schreiben). Beispielanwendungsfälle:
- Just-in-time-Kompilierung:Die Kompilierung wird bis zur ersten Ausführung verzögert.
- Mehrstufige Kompilierung:Zuerst wird eine schnelle Kompilierung durchgeführt. Eine optionale optimierte Kompilierung erfolgt später, je nach Nutzungshäufigkeit.
Damit nach dem Kompilierungsaufruf auf den Cache zugegriffen werden kann (Lesen oder Schreiben), muss der Treiber Folgendes sicherstellen:
- Dupliziert die Dateihandles beim Aufruf von
prepareModel_1_2
oderprepareModelFromCache
und liest/aktualisiert den Cacheinhalt zu einem späteren Zeitpunkt. - Implementiert die Logik für die Dateisperrung außerhalb des normalen Kompilierungsaufrufs, um zu verhindern, dass ein Schreibvorgang gleichzeitig mit einem Lese- oder einem anderen Schreibvorgang erfolgt.
Caching-Engine implementieren
Zusätzlich zur NN HAL 1.2-Schnittstelle für das Zwischenspeichern von Kompilierungen finden Sie im Verzeichnis frameworks/ml/nn/driver/cache
auch eine Utility-Bibliothek für das Zwischenspeichern. Das Unterverzeichnis nnCache
enthält Code für den persistenten Speicher, damit der Treiber das Kompilierungs-Caching ohne Verwendung der NNAPI-Caching-Funktionen implementieren kann. Diese Form des Kompilierungs-Caching kann mit jeder Version der NN HAL implementiert werden. Wenn der Treiber das Caching unabhängig von der HAL-Schnittstelle implementiert, ist er dafür verantwortlich, zwischengespeicherte Artefakte freizugeben, wenn sie nicht mehr benötigt werden.