Arbeitsspeicherpools

Auf dieser Seite werden die Datenstrukturen und Methoden beschrieben, die für die effiziente Kommunikation von Operandenpuffern zwischen dem Treiber und dem Framework verwendet werden.

Zum Zeitpunkt der Modellkompilierung stellt das Framework dem Treiber die Werte der konstanten Operanden zur Verfügung. Abhängig von der Lebensdauer des konstanten Operanden befinden sich seine Werte entweder in einem HIDL-Vektor oder in einem gemeinsamen Speicherpool.

  • Wenn die Lebensdauer CONSTANT_COPY ist, befinden sich die Werte im Feld operandValues der Modellstruktur. Da die Werte im HIDL-Vektor während der interprozeduralen Kommunikation (IPC) kopiert werden, wird er in der Regel nur zum Speichern einer kleinen Menge von Daten wie Skalaroperanden (z. B. der Aktivierungsskalar in ADD) und kleinen Tensorparametern (z. B. der Formtensor in RESHAPE) verwendet.
  • Wenn die Lebensdauer CONSTANT_REFERENCE ist, befinden sich die Werte im Feld pools der Modellstruktur. Bei der Interprozesskommunikation werden nur die Handles der freigegebenen Speicherpools dupliziert, anstatt die Rohwerte zu kopieren. Daher ist es effizienter, große Datenmengen (z. B. die Gewichtsparameter in Convolutionen) mit gemeinsamen Speicherpools zu speichern als mit HIDL-Vektoren.

Während der Modellausführung stellt das Framework dem Treiber die Puffer der Eingabe- und Ausgabeoperanden zur Verfügung. Im Gegensatz zu den Konstanten zur Kompilierzeit, die in einem HIDL-Vektor gesendet werden können, werden die Eingabe- und Ausgabedaten einer Ausführung immer über eine Sammlung von Speicherpools übertragen.

Der HIDL-Datentyp hidl_memory wird sowohl bei der Kompilierung als auch bei der Ausführung verwendet, um einen nicht zugeordneten gemeinsamen Speicherpool darzustellen. Der Treiber sollte den Arbeitsspeicher entsprechend zuordnen, damit er basierend auf dem Namen des hidl_memory-Datentyps verwendet werden kann. Folgende Speichernamen werden unterstützt:

  • ashmem: Android-Speicher Weitere Informationen finden Sie unter Arbeitsspeicher.
  • mmap_fd: Gemeinsam genutzter Arbeitsspeicher, der über mmap durch einen Dateideskriptor unterstützt wird.
  • hardware_buffer_blob: gemeinsamer Speicher, unterstützt von einem AHardwareBuffer mit dem Format AHARDWARE_BUFFER_FORMAT_BLOB. Verfügbar in Neural Networks (NN) HAL 1.2. Weitere Informationen finden Sie unter AHardwareBuffer.
  • hardware_buffer: gemeinsamer Speicher, der von einem allgemeinen AHardwareBuffer gestützt wird, der nicht das Format AHARDWARE_BUFFER_FORMAT_BLOB verwendet. Der Hardware-Buffer im Modus ohne BLOB wird nur bei der Modellausführung unterstützt.Verfügbar ab NN HAL 1.2. Weitere Informationen finden Sie unter AHardwareBuffer.

Ab NN HAL 1.3 unterstützt NNAPI Speicherbereiche, die Allocator-Schnittstellen für treiberverwaltete Puffer bereitstellen. Die vom Treiber verwalteten Puffer können auch als Ausführungseingaben oder ‑ausgaben verwendet werden. Weitere Informationen finden Sie unter Memory-Domains.

NNAPI-Treiber müssen die Zuordnung von ashmem- und mmap_fd-Speichernamen unterstützen. Ab NN HAL 1.3 müssen Treiber auch die Zuordnung von hardware_buffer_blob unterstützen. Die Unterstützung für den allgemeinen Modus ohne BLOBs hardware_buffer und Arbeitsspeicherdomains ist optional.

AHardwareBuffer

AHardwareBuffer ist eine Art von gemeinsamem Arbeitsspeicher, der einen Gralloc-Zwischenspeicher umschließt. In Android 10 unterstützt die Neural Networks API (NNAPI) die Verwendung von AHardwareBuffer, sodass der Treiber Ausführungen ohne Kopieren von Daten ausführen kann. Dadurch werden Leistung und Stromverbrauch von Anwendungen verbessert. Beispielsweise kann ein HAL-Stack für Kameras AHardwareBuffer-Objekte mithilfe von AHardwareBuffer-Handles, die von den NDK-APIs für Kameras und Medien generiert werden, an die NNAPI für Arbeitslasten für maschinelles Lernen übergeben. Weitere Informationen finden Sie unter ANeuralNetworksMemory_createFromAHardwareBuffer.

In NNAPI verwendete AHardwareBuffer-Objekte werden über eine hidl_memory-Struktur mit dem Namen hardware_buffer oder hardware_buffer_blob an den Treiber übergeben. Das hidl_memory-Objekt hardware_buffer_blob stellt nur AHardwareBuffer-Objekte mit dem AHARDWAREBUFFER_FORMAT_BLOB-Format dar.

Die vom Framework erforderlichen Informationen sind im Feld hidl_handle der Struktur hidl_memory codiert. Das Feld hidl_handle umschließt native_handle, das alle erforderlichen Metadaten zu AHardwareBuffer oder Gralloc-Buffer codiert.

Der Treiber muss das angegebene Feld hidl_handle ordnungsgemäß decodieren und auf den in hidl_handle beschriebenen Speicher zugreifen. Wenn die Methode getSupportedOperations_1_2, getSupportedOperations_1_1 oder getSupportedOperations aufgerufen wird, sollte der Treiber erkennen, ob er die angegebene hidl_handle decodieren und auf den von hidl_handle beschriebenen Speicher zugreifen kann. Die Modellvorbereitung muss fehlschlagen, wenn das Feld hidl_handle, das für einen konstanten Operanden verwendet wird, nicht unterstützt wird. Die Ausführung muss fehlschlagen, wenn das Feld hidl_handle, das für einen Eingabe- oder Ausgabeoperanden der Ausführung verwendet wird, nicht unterstützt wird. Es wird empfohlen, dass der Treiber einen GENERAL_FAILURE-Fehlercode zurückgibt, wenn die Modellvorbereitung oder -ausführung fehlschlägt.

Speicherbereiche

Auf Geräten mit Android 11 oder höher unterstützt NNAPI Speicherbereiche, die Allocator-Schnittstellen für treiberverwaltete Puffer bereitstellen. So können geräteeigene Speicher zwischen Ausführungen übergeben werden, wodurch unnötiges Kopieren und Umwandeln von Daten zwischen aufeinanderfolgenden Ausführungen desselben Treibers vermieden wird. Dieser Ablauf ist in Abbildung 1 dargestellt.

Zwischenspeichern des Datenflusses mit und ohne Speicherdomains

Abbildung 1: Datenfluss mithilfe von Speicherbereichen puffern

Die Speicherdomainfunktion ist für Tensoren gedacht, die größtenteils im Treiber intern sind und auf der Clientseite nicht häufig aufgerufen werden müssen. Beispiele für solche Tensoren sind die Zustandstensoren in Sequenzmodellen. Für Tensoren, die clientseitig häufig auf die CPU zugreifen müssen, sollten Sie Shared-Memory-Pools verwenden.

Zur Unterstützung des Memory Domain-Features implementieren Sie IDevice::allocate, damit das Framework die Treiber-verwaltete Zwischenspeicherzuweisung anfordern kann. Während der Zuweisung stellt das Framework die folgenden Eigenschaften und Nutzungsmuster für den Puffer bereit:

  • BufferDesc beschreibt die erforderlichen Eigenschaften des Buffers.
  • BufferRole beschreibt das potenzielle Nutzungsmuster des Buffers als Eingabe oder Ausgabe eines vorbereiteten Modells. Während der Zwischenspeicherzuweisung können mehrere Rollen angegeben werden. Der zugewiesene Zwischenspeicher kann nur als die angegebenen Rollen verwendet werden.

Der zugewiesene Puffer ist intern für den Treiber. Ein Treiber kann jeden Pufferspeicherort oder jedes Datenlayout auswählen. Wenn der Puffer erfolgreich zugewiesen wurde, kann der Client des Treibers über das zurückgegebene Token oder das IBuffer-Objekt auf den Puffer verweisen oder mit ihm interagieren.

Das Token von IDevice::allocate wird bereitgestellt, wenn auf den Puffer als eines der MemoryPool-Objekte in der Request-Struktur einer Ausführung verwiesen wird. Um zu verhindern, dass ein Prozess versucht, auf den Zwischenspeicher zuzugreifen, der in einem anderen Prozess zugewiesen wurde, muss der Treiber bei jeder Verwendung des Zwischenspeichers eine ordnungsgemäße Validierung durchführen. Der Treiber muss validieren, dass die Zwischenspeichernutzung eine der BufferRole-Rollen ist, die während der Zuweisung bereitgestellt werden. Die Ausführung muss sofort fehlschlagen, wenn die Nutzung illegal ist.

Das IBuffer-Objekt wird für das explizite Kopieren von Arbeitsspeicher verwendet. In bestimmten Situationen muss der Client des Treibers den vom Treiber verwalteten Puffer aus einem gemeinsamen Speicherpool initialisieren oder den Puffer in einen gemeinsamen Speicherpool kopieren. Beispiele für Anwendungsfälle:

  • Initialisierung des Zustandstensors
  • Zwischenergebnisse zwischenspeichern
  • Fallback-Ausführung auf der CPU

Zur Unterstützung dieser Anwendungsfälle muss der Treiber IBuffer::copyTo und IBuffer::copyFrom mit ashmem, mmap_fd und hardware_buffer_blob implementieren, wenn er die Adresszuweisung für Speicherdomänen unterstützt. Es ist optional, dass der Treiber den Modus ohne BLOB unterstützthardware_buffer.

Bei der Pufferzuweisung können die Dimensionen des Puffers aus den entsprechenden Modelloperanden aller Rollen abgeleitet werden, die in BufferRole angegeben sind, und den in BufferDesc angegebenen Dimensionen. Wenn alle Dimensionsinformationen kombiniert werden, hat der Puffer möglicherweise unbekannte Dimensionen oder einen unbekannten Rang. In einem solchen Fall befindet sich der Puffer in einem flexiblen Zustand, in dem die Dimensionen bei Verwendung als Modelleingabe fixiert sind, und in einem dynamischen Zustand, wenn sie als Modellausgabe verwendet werden. Derselbe Puffer kann bei verschiedenen Ausführungen mit unterschiedlichen Ausgabeformen verwendet werden und der Treiber muss die Puffergröße richtig anpassen.

Die Speicherdomain ist eine optionale Funktion. Ein Treiber kann aus verschiedenen Gründen feststellen, dass er eine bestimmte Allokierungsanfrage nicht unterstützen kann. Beispiel:

  • Der angeforderte Puffer hat eine dynamische Größe.
  • Der Treiber hat Speichereinschränkungen, die das Bearbeiten großer Puffer verhindern.

Es ist möglich, dass mehrere verschiedene Threads gleichzeitig aus dem Treiber verwalteten Zwischenspeicher lesen. Der gleichzeitige Zugriff auf den Puffer zum Schreiben oder Lesen/Schreiben ist nicht definiert, darf aber nicht zum Absturz des Treiberdienstes oder zum Blockieren des Aufrufers führen. Der Treiber kann einen Fehler zurückgeben oder den Inhalt des Zwischenspeichers in einem unbestimmten Zustand belassen.