Kompilierungs-Caching

Ab Android 10 bietet die Neural Networks API (NNAPI) Funktionen zur Unterstützung der Zwischenspeicherung von Kompilierungsartefakten, wodurch die für die Kompilierung benötigte Zeit beim Start einer App reduziert wird. Mithilfe dieser Caching-Funktionalität 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 Kompilierungscaching unabhängig von der NNAPI implementieren. Dies kann unabhängig davon implementiert werden, ob die NNAPI NDK- und HAL-Caching-Funktionen verwendet werden oder nicht. AOSP stellt eine Low-Level-Dienstprogrammbibliothek (eine Caching-Engine) bereit. Weitere Informationen finden Sie unter Implementieren einer Caching-Engine .

Workflow-Übersicht

In diesem Abschnitt werden allgemeine Arbeitsabläufe mit implementierter Kompilierungs-Caching-Funktion beschrieben.

Bereitgestellte Cache-Informationen und Cache-Treffer

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

Cache-Informationen bereitgestellt 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ührungspräferenz und des Partitionierungsergebnisses nach den Caching-Dateien und findet die Cache-Dateien nicht.
  3. Die NNAPI erstellt leere Cache-Dateien basierend auf der Prüfsumme, der Ausführungseinstellung und der Partitionierung, öffnet die Cache-Dateien und übergibt die Handles und das Modell mit prepareModel_1_2 an den Treiber.
  4. Der Treiber kompiliert das Modell, schreibt Caching-Informationen in die Cache-Dateien und gibt das vorbereitete Modell zurück.

Cache-Informationen nicht bereitgestellt

  1. Die App ruft die Kompilierung auf, ohne Caching-Informationen bereitzustellen.
  2. Die App übergibt nichts, was mit dem Caching zu tun hat.
  3. Die NNAPI-Laufzeit übergibt das Modell mit prepareModel_1_2 an den Treiber.
  4. Der Treiber kompiliert das Modell und gibt das vorbereitete Modell zurück.

Cache-Informationen

Die einem Treiber bereitgestellten Caching-Informationen bestehen aus einem Token und Cache-Datei-Handles.

Zeichen

Das Token ist ein Caching-Token der Länge Constant::BYTE_SIZE_OF_CACHE_TOKEN , das das vorbereitete Modell identifiziert. Das gleiche Token wird bereitgestellt, wenn die Cache-Dateien mit prepareModel_1_2 gespeichert und das vorbereitete Modell mit prepareModelFromCache abgerufen werden. Der Kunde des Fahrers sollte einen Token mit einer geringen Kollisionsrate wählen. Der Fahrer kann eine Token-Kollision nicht erkennen. Eine Kollision führt zu einer fehlgeschlagenen Ausführung oder zu einer erfolgreichen Ausführung, die falsche Ausgabewerte erzeugt.

Cache-Datei-Handles (zwei Arten von Cache-Dateien)

Die beiden Arten von Cache-Dateien sind Datencache und Modellcache .

  • Datencache: Wird zum Zwischenspeichern konstanter Daten einschließlich vorverarbeiteter und transformierter Tensorpuffer verwendet. Eine Änderung am Datencache sollte keine schlimmeren Auswirkungen haben als die Generierung fehlerhafter Ausgabewerte zur Ausführungszeit.
  • Modellcache: Wird zum Zwischenspeichern sicherheitsrelevanter Daten wie kompiliertem ausführbarem Maschinencode im nativen Binärformat des Geräts verwendet. Eine Änderung am Modellcache kann sich auf das Ausführungsverhalten des Treibers auswirken, und ein böswilliger Client könnte dies ausnutzen, um über die erteilte 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 Cache-Dateitypen verteilt werden, und mit getNumberOfCacheFilesNeeded melden, wie viele Cache-Dateien er für jeden Typ benötigt.

Die NNAPI-Laufzeit öffnet Cache-Dateihandles immer mit Lese- und Schreibberechtigung.

Sicherheit

Beim Kompilierungscaching 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 sich eine Änderung am Modellcache auf das Ausführungsverhalten des Treibers auswirken. Da der Cache-Inhalt im App-Verzeichnis gespeichert wird, können die Cache-Dateien vom Client geändert werden. Ein fehlerhafter Client kann versehentlich den Cache beschädigen, und ein böswilliger Client könnte dies absichtlich ausnutzen, um nicht überprüften Code auf dem Gerät auszuführen. Abhängig von den Eigenschaften des Geräts kann dies ein Sicherheitsproblem sein. Daher muss der Treiber in der Lage sein, eine mögliche Beschädigung des Modellcaches zu erkennen, bevor er das Modell aus dem Cache vorbereitet.

Eine Möglichkeit hierfür besteht darin, dass der Treiber eine Zuordnung vom Token zu einem kryptografischen Hash des Modellcaches verwaltet. Der Treiber kann das Token und den Hash seines Modellcaches speichern, wenn er die Kompilierung im Cache speichert. Der Treiber überprüft den neuen Hash des Modellcaches mit dem aufgezeichneten Token- und Hash-Paar, wenn er die Zusammenstellung aus dem Cache abruft. Diese Zuordnung sollte über Systemneustarts hinweg bestehen bleiben. Der Treiber kann den Android-Keystore-Dienst , die Dienstprogrammbibliothek in framework/ml/nn/driver/cache oder einen anderen geeigneten Mechanismus zum Implementieren eines Mapping-Managers verwenden. Bei der Treiberaktualisierung sollte dieser Mapping-Manager neu initialisiert werden, um zu verhindern, dass Cache-Dateien aus einer früheren Version erstellt werden.

Um Time-of-Check-to-Time-of-Use- Angriffe (TOCTOU) zu verhindern, muss der Treiber den aufgezeichneten Hash berechnen, bevor er in einer Datei speichert, und den neuen Hash berechnen, nachdem er den Dateiinhalt in einen internen Puffer kopiert hat.

Dieser Beispielcode zeigt, 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 Zugriff auf den Cache-Inhalt (Lesen oder Schreiben). Beispielhafte Anwendungsfälle sind:

  • Just-in-Time-Kompilierung: Die Kompilierung wird bis zur ersten Ausführung verzögert.
  • Mehrstufige Kompilierung: Zunächst erfolgt eine schnelle Kompilierung und zu einem späteren Zeitpunkt je nach Nutzungshäufigkeit optional eine optimierte Kompilierung.

Um nach dem Kompilierungsaufruf auf den Cache-Inhalt zuzugreifen (Lesen oder Schreiben), stellen Sie sicher, dass der Treiber:

  • Dupliziert die Dateihandles während des Aufrufs von prepareModel_1_2 oder prepareModelFromCache und liest/aktualisiert den Cache-Inhalt zu einem späteren Zeitpunkt.
  • Implementiert Dateisperrlogik außerhalb des normalen Kompilierungsaufrufs, um zu verhindern, dass ein Schreibvorgang gleichzeitig mit einem Lesevorgang oder einem anderen Schreibvorgang erfolgt.

Implementieren Sie eine Caching-Engine

Zusätzlich zur NN HAL 1.2-Kompilierungs-Caching-Schnittstelle finden Sie auch eine Caching-Dienstprogrammbibliothek im Verzeichnis frameworks/ml/nn/driver/cache . Das Unterverzeichnis nnCache enthält dauerhaften Speichercode für den Treiber, um Kompilierungs-Caching zu implementieren, ohne die NNAPI-Caching-Funktionen zu verwenden. Diese Form der Kompilierungszwischenspeicherung kann mit jeder Version des NN HAL implementiert werden. Wenn sich der Treiber dafür entscheidet, das Caching getrennt von der HAL-Schnittstelle zu implementieren, ist der Treiber dafür verantwortlich, zwischengespeicherte Artefakte freizugeben, wenn sie nicht mehr benötigt werden.