Ab Android 10 bietet die Neural Networks API (NNAPI) Funktionen zum Caching von Kompilierungsartefakten, wodurch die für die Kompilierung beim Starten einer App benötigte Zeit verkürzt wird. Dank dieser Caching-Funktion müssen die zwischengespeicherten Dateien nicht vom Treiber verwaltet oder bereinigt werden. 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 ein Kompilierungs-Caching implementieren. Dies kann unabhängig davon implementiert werden, ob die NNAPI-NDK- und HAL-Caching-Funktionen verwendet werden oder nicht. 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 Kompilierungs-Caching beschrieben.
Cache-Informationen bereitgestellt und Cache-Treffer
- Die App übergibt ein Caching-Verzeichnis und eine Prüfsumme, die für das Modell eindeutig ist.
- Die NNAPI-Laufzeit sucht anhand der Prüfsumme, der Ausführungseinstellung und des Partitionierungsergebnisses nach den Cachedateien und findet sie.
- Die NNAPI öffnet die Cachedateien und übergibt die Handles mit
prepareModelFromCache
an den Treiber. - Der Treiber bereitet das Modell direkt aus den Cachedateien vor und gibt das vorbereitete Modell zurück.
Angegebene Cache-Informationen und Cache-Fehler
- Die App übergibt eine für das Modell eindeutige Prüfsumme und ein Cacheverzeichnis.
- Die NNAPI-Laufzeit sucht anhand der Prüfsumme, der Ausführungseinstellung und des Partitionierungsergebnisses nach den Caching-Dateien und findet sie nicht.
- Die NNAPI erstellt basierend auf der Prüfsumme, der Ausführungspräferenz und der Partitionierung leere Cachedateien, öffnet die Cachedateien und übergibt die Handles und das Modell mit
prepareModel_1_2
an den Treiber. - Der Treiber kompiliert das Modell, schreibt Cache-Informationen in die Cachedateien und gibt das vorbereitete Modell zurück.
Cache-Informationen nicht angegeben
- Die App ruft die Kompilierung auf, ohne Informationen zum Caching anzugeben.
- Die App gibt keine Informationen zum Caching weiter.
- Die NNAPI-Laufzeit gibt das Modell mit
prepareModel_1_2
an den Treiber weiter. - Der Treiber kompiliert das Modell und gibt es zurück.
Cache-Informationen
Die Informationen zum Caching, die einem Treiber zur Verfügung gestellt werden, bestehen aus einem Token und Cachedatei-Handles.
Token
Das Token ist ein Caching-Token mit der Länge Constant::BYTE_SIZE_OF_CACHE_TOKEN
, das das vorbereitete Modell identifiziert. Das gleiche Token wird beim Speichern der Cachedateien mit prepareModel_1_2
und beim Abrufen des vorbereiteten Modells mit prepareModelFromCache
angegeben. Der Kunde des Treibers sollte ein Token mit einer niedrigen Kollisionsrate auswählen. Der Treiber kann keine Token-Kollisionen erkennen. Eine Kollision führt zu einer fehlgeschlagenen Ausführung oder zu einer erfolgreichen Ausführung, die falsche Ausgabewerte liefert.
Cachedatei-Handle (zwei Arten von Cachedateien)
Es gibt zwei Arten von Cachedateien: Datencache und Modellcache.
- Datencache: Zum Caching konstanter Daten, einschließlich vorverarbeiteter und transformierter Tensor-Puffer. Eine Änderung am Datencache sollte nicht zu schlimmeren Auswirkungen führen als zur Generierung fehlerhafter Ausgabewerte bei der Ausführung.
- Modellcache:Zum Caching sicherheitsrelevanter Daten wie kompilierten ausführbaren Maschinencodes im nativen Binärformat des Geräts. Eine Änderung am Modellcache kann sich auf das Ausführungsverhalten des Treibers auswirken. Ein böswilliger Client könnte dies nutzen, um über die gewährte Berechtigung hinaus auszuführen. 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 festlegen, wie Cache-Informationen zwischen den beiden Cachedateitypen verteilt werden, und mit getNumberOfCacheFilesNeeded
angeben, wie viele Cachedateien für jeden Typ benötigt werden.
Die NNAPI-Laufzeit öffnet Cachedatei-Handle immer mit Lese- und Schreibberechtigung.
Sicherheit
Beim Kompilierungs-Caching kann der Modellcache sicherheitsrelevante Daten wie kompilierten ausführbaren Maschinencode im nativen Binärformat des Geräts enthalten. Wenn der Modellcache nicht ordnungsgemäß geschützt ist, kann eine Änderung daran das Ausführungsverhalten des Treibers beeinträchtigen. Da der Cacheinhalt im App-Verzeichnis gespeichert wird, können die Cachedateien vom Client geändert werden. Ein fehlerhafter Client kann den Cache versehentlich beschädigen und ein böswilliger Client kann dies absichtlich nutzen, um nicht verifizierten Code auf dem Gerät auszuführen. Je nach den Eigenschaften des Geräts kann dies ein Sicherheitsproblem darstellen. Daher muss der Treiber potenzielle Beschädigungen des Modell-Caches erkennen können, bevor das Modell aus dem Cache bereitgestellt wird.
Eine Möglichkeit besteht darin, dass der Treiber eine Zuordnung vom Token zu einem kryptografischen Hash des Modellcaches beibehält. Der Treiber kann das Token und den Hash seines Modell-Caches speichern, wenn er die Kompilierung im Cache speichert. Der Treiber vergleicht den neuen Hashwert des Modell-Caches mit dem aufgezeichneten Token- und Hash-Paar, wenn er die Kompilierung aus dem Cache abruft. Diese Zuordnung sollte auch nach Systemneustarts bestehen bleiben. Der Treiber kann den Android-Schlüsselspeicherdienst, die Dienstprogrammbibliothek in framework/ml/nn/driver/cache
oder einen anderen geeigneten Mechanismus verwenden, um einen Zuordnungsmanager zu implementieren. Nach dem Treiberupdate sollte dieser Zuordnungsmanager neu initialisiert werden, um zu verhindern, dass Cachedateien aus einer früheren Version vorbereitet werden.
Um TOCTOU-Angriffe (Time-of-Check to Time-of-Use) zu verhindern, muss der Treiber den aufgezeichneten Hashwert berechnen, bevor er in die Datei gespeichert wird, und den neuen Hashwert berechnen, nachdem der Dateiinhalt in einen internen Puffer kopiert wurde.
In diesem Beispielcode wird gezeigt, wie diese Logik implementiert wird.
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 Lese- oder Schreibzugriff auf den Cacheinhalt. Beispiele für Anwendungsfälle:
- Just-in-time-Kompilierung: Die Kompilierung wird bis zur ersten Ausführung verzögert.
- Mehrere Phasen der Kompilierung:Zuerst wird eine schnelle Kompilierung durchgeführt und je nach Häufigkeit der Nutzung später eine optionale optimierte Kompilierung.
Damit nach dem Kompilierungsaufruf auf den Cacheinhalt zugegriffen werden kann (Lesen oder Schreiben), muss der Treiber folgende Anforderungen erfüllen:
- Hiermit werden die Datei-Handle beim Aufruf von
prepareModel_1_2
oderprepareModelFromCache
dupliziert und der Cacheinhalt zu einem späteren Zeitpunkt gelesen/aktualisiert. - Implementiert die Logik zur Dateisperre außerhalb des normalen Kompilierungsaufrufs, um zu verhindern, dass ein Schreibvorgang gleichzeitig mit einem Lese- oder einem anderen Schreibvorgang ausgeführt wird.
Caching-Engine implementieren
Neben der NN HAL 1.2-Schnittstelle für den Caching-Prozess finden Sie im Verzeichnis frameworks/ml/nn/driver/cache
auch eine Caching-Dienstprogrammbibliothek. Das Unterverzeichnis nnCache
enthält Code für den nichtflüchtigen Speicher, mit dem der Treiber das Kompilierungs-Caching implementieren kann, ohne die NNAPI-Caching-Funktionen zu verwenden. Diese Form des Kompilierungs-Cachings 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, die im Cache gespeicherten Artefakte freizugeben, wenn sie nicht mehr benötigt werden.