Kompilierungs-Caching

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 reduziert wird. Mit dieser Caching-Funktion kann der Treiber die im Cache gespeicherten Dateien verwalten oder bereinigen müssen. Dies ist eine optionale Funktion, die mit NN HAL 1.2 implementiert werden kann. Weitere Informationen zu dieser Funktion Siehe ANeuralNetworksCompilation_setCaching

Der Treiber kann auch unabhängig von der NNAPI ein Kompilierungs-Caching implementieren. Dieses kann implementiert werden, unabhängig davon, 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 Funktion für das Kompilierungs-Caching beschrieben. implementiert.

Cache-Informationen und Cache-Treffer

  1. Die Anwendung übergibt ein Caching-Verzeichnis und eine für das Modell eindeutige Prüfsumme.
  2. Die NNAPI-Laufzeit sucht anhand der Prüfsumme, der Ausführungseinstellung und des Partitionierungsergebnisses nach den Cachedateien und findet sie.
  3. Die NNAPI öffnet die Cache-Dateien und übergibt die Handles an den Treiber. mit prepareModelFromCache
  4. Der Treiber bereitet das Modell direkt aus den Cachedateien vor und gibt das vorbereitete Modell zurück.

Angegebene Cache-Informationen und Cache-Fehler

  1. Die App übergibt eine für das Modell eindeutige Prüfsumme und ein Caching-Verzeichnis.
  2. Die NNAPI-Laufzeit sucht anhand der Prüfsumme, der Ausführungseinstellung und des Partitionierungsergebnisses nach den Caching-Dateien und findet sie nicht.
  3. Die NNAPI erstellt leere Cache-Dateien basierend auf der Prüfsumme, der Ausführung und die Partitionierung, öffnet die Cache-Dateien und übergibt und das Modell mit den prepareModel_1_2
  4. Der Treiber kompiliert das Modell, schreibt Cache-Informationen in die Cachedateien und gibt das vorbereitete Modell zurück.

Cache-Informationen nicht angegeben

  1. Die App ruft die Kompilierung auf, ohne Cache-Informationen anzugeben.
  2. Die Anwendung übergibt nichts im Zusammenhang mit Caching.
  3. Die NNAPI-Laufzeit gibt das Modell mit prepareModel_1_2 an den Treiber weiter.
  4. Der Treiber kompiliert das Modell und gibt das vorbereitete Modell zurück.

Cache-Informationen

Die Caching-Informationen, die einem Treiber bereitgestellt werden, bestehen aus einem Token und Cache-Datei-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)

Die beiden Arten von Cache-Dateien sind der Daten-Cache und der Modell-Cache.

  • Daten-Cache: Wird zum Caching konstanter Daten verwendet, einschließlich vorverarbeiteter und transformierten Tensor-Zwischenspeichern. Eine Änderung am Daten-Cache sollte zu einer schlechteren Auswirkung führen als die Erzeugung ungültiger Ausgabewerte bei der Ausführung. .
  • Modell-Cache:Wird zum Caching sicherheitsrelevanter Daten verwendet, z. B. kompilierte den ausführbaren Maschinencode im nativen Binärformat des Geräts vor. 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 entscheiden, wie Cache-Informationen zwischen den beiden verteilt werden sollen und geben an, wie viele Cache-Dateien jeweils benötigt werden. mit getNumberOfCacheFilesNeeded

Die NNAPI-Laufzeit öffnet Cache-Datei-Handles sowohl mit Lese- als auch Schreibvorgängen Berechtigung.

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. Das heißt, der Parameter Der Fahrer muss in der Lage sein, potenzielle Beschädigung des Modell-Cache, bevor das Modell aus dem Cache vorbereitet wird.

Eine Möglichkeit, dies zu tun, besteht darin, dass der Fahrer eine Karte vom Token zu einem kryptografischer Hash des Modell-Cache. Der Treiber kann das Token und den Hash seines Modell-Caches speichern, wenn er die Kompilierung im Cache speichert. Der Fahrer prüft den neuen Hash des Modell-Cache mit dem aufgezeichneten Token und Hash-Paar, wenn das Abrufen der Kompilierung aus dem Cache. Diese Zuordnung sollte für alle das System neu gestartet wird. Der Fahrer kann die Android Keystore Service, die Dienstprogrammbibliothek in framework/ml/nn/driver/cache, oder einen anderen geeigneten Mechanismus zur Implementierung eines Kartenmanagers implementieren. Nach dem Treiberupdate sollte dieser Zuordnungsmanager neu initialisiert werden, um zu verhindern, dass Cachedateien aus einer früheren Version vorbereitet werden.

Um dies zu verhindern, von der Prüfung bis zur Nutzungszeit (TOCTOU)-Angriffen nutzen, muss der Fahrer den aufgezeichneten Hashwert berechnen, bevor er unter und berechnen den neuen Hash nach dem Kopieren des Dateiinhalts in eine interne Puffer.

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

Erweiterte 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.
  • Kompilierung mit mehreren Phasen:Zu Beginn wird eine schnelle Kompilierung ausgeführt. und zu einem späteren Zeitpunkt wird eine optionale, optimierte Kompilierung durchgeführt. je nach Nutzungshäufigkeit.

Um nach dem Kompilierungsaufruf auf den Cache-Inhalt zugreifen zu können (Lese- oder Schreibzugriff), dass der Fahrer:

  • Hiermit werden die Datei-Handle beim Aufruf von prepareModel_1_2 oder prepareModelFromCache dupliziert und der Cacheinhalt zu einem späteren Zeitpunkt gelesen/aktualisiert.
  • Implementiert Dateisperrlogik außerhalb des normalen Kompilierungsaufrufs um einen Schreibvorgang zu verhindern, der gleichzeitig mit einem Lese- oder einem anderen Schreibvorgang ausgeführt wird.

Caching-Engine implementieren

Neben der NN HAL 1.2-Benutzeroberfläche für das Caching 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 von Kompilierungs-Caching kann mit jeder Version von NN HAL implementiert werden. Wenn die um ein von der HAL-Schnittstelle getrenntes Caching zu implementieren, der Fahrer ist dafür verantwortlich, im Cache gespeicherte Artefakte freizugeben, wenn sie nicht mehr benötigt werden.