Rockchip · AI & Linux Hub

KI-Inferenz

RKNN2 SDK V2.5.0 · Speicheranalyse, gemeinsame Nutzung und Wiederverwendung

RKNN2 SDK V2.5.0 · Speicheranalyse, gemeinsame Nutzung und Wiederverwendung · Vollständige Entwicklerhandbuchreihe

· ArmSoM Editorial · Rockchip source documentation · 8 Min. Lesezeit

Plattformreferenz nach dem Rockchip RKNN2 SDK Development Guide V2.5.0. Erfasst werden nur die im Original genannten SoCs sowie Linux-/Android-Runtime- und Toolkit-Kombinationen. Funktionen gelten unter den jeweiligen Chip-, SDK-, Treiber- und Kernelbedingungen und nicht automatisch für jedes ArmSoM-Board. Prüfen Sie vor der Bereitstellung BSP, NPU-Treiber, Runtime und Modellversion des konkreten Boards. Es erfolgte keine ArmSoM-Hardwarevalidierung; Protokolle, Leistungswerte und Beispiele stammen aus dem Original.

Quellen und Prüfung: Chinesisch folgt der chinesischen V2.5.0 (182 Seiten); Englisch, Russisch und Deutsch folgen der englischen V2.5.0 (208 Seiten). Quellenspezifische technische Angaben bleiben erhalten; Inhalte älterer Versionen werden nicht als neue Fakten dargestellt. Chinesische Abbildungen stammen aus dem chinesischen PDF, EN/RU/DE-Abbildungen aus dem englischen PDF einschließlich separat extrahierter Vektorflussdiagramme. Abschnitts- und Formelnummern bleiben erhalten. Einige Querverweise verwenden alte Kapitelnummern; Dynamic-Shape-Beispiele enthalten „1224+140=264“, TP_NORM-Aussagen widersprechen sich, und init()/rknn.val_perf() weichen von späteren API-Beschreibungen ab. Diese Quellenprobleme bleiben dokumentiert und sind vor Ausführung anhand der tatsächlichen V2.5.0-Header und API-Referenzen zu prüfen. Code und Parameter bleiben erhalten; PDF-Umbrüche und Tabellenfortsetzungen wurden bereinigt.

Rockchip RKNN2 SDK V2.5.0

RKNN2 SDK V2.5.0 · 21/22

1. Softwarestack und Grundbegriffe

2. Entwicklungsumgebung, Versionen und Board-Anbindung

3. Entwicklungsablauf und Schnittstellenwahl

4. Modellkonvertierung und ONNX-Bearbeitung

5. Quantisierung, hybride Quantisierung und QAT

6. Inferenz sowie Genauigkeits-, Leistungs- und Speicherbewertung

7. Runtime-Lebenszyklus, Layouts und Speicher

8. Zero-Copy-Schnittstellen und Ein-/Ausgabebedingungen

9. C-API-Bereitstellung und Codegenerierung

10. Python-Bereitstellung mit Toolkit Lite2

11. Matmul-API und Matrixlayouts

12. NPU-Mehrkernkonfiguration und Leistung

13. Modelle mit dynamischen Formen und Bereitstellung

14. Inferenz mit mehreren Batches

15. NPU-SRAM auf RK3588/RK3576

16. Cache-Kohärenz und Sparse-Inferenz

17. Benutzerdefinierte Operatoren: Python und C API

18. Bereitstellung verschlüsselter Modelle

19. Diagnose von Genauigkeitsproblemen

20. Leistungsanalyse und Netzwerkoptimierung

Speicheranalyse, gemeinsame Nutzung und Wiederverwendung

22. FAQ, Fehlerbehandlung und Referenzen

5.3 Speicheranalyse und -optimierung

5.3.1 Speicheraufteilung und Analysemethoden

5.3.1.1 Speicheraufteilung während der RKNN-Modellausführung

Der Laufzeitspeicher eines RKNN-Modells besteht hauptsächlich aus vier Teilen: Gewichten und internen Tensoren, Registerkonfigurationen, Eingabe- und Ausgabetensoren. Üblicherweise wird er während rknn_init angelegt.

5.3.1.2 Modellspeicher analysieren

Nach rknn_init() bei Bedarf den zugewiesenen Modellspeicher abfragen oder die externe Zuweisung der Gewichte vorbereiten: rknn_query mit RKNN_QUERY_MEM_SIZE aufrufen. Abgefragt werden Gewichte und interner Speicher ohne Ein-/Ausgaben sowie die für die Inferenz belegten DMA- und SRAM-Speicherbereiche. SRAM wird als 0 gemeldet, wenn es nicht aktiviert oder diese Funktion nicht vorhanden ist.

Beispielcode:

 rknn_context ctx = 0;

 // Load RKNN Model
 int ret = rknn_init(&ctx, model_path, 0, NULL, NULL);
 if (ret < 0) {
    printf("rknn_init fail! ret=%d\n", ret);
    return -1;
 }

 // Get weight and internal mem size
 rknn_mem_size mem_size;
 ret = rknn_query(ctx, RKNN_QUERY_MEM_SIZE, &mem_size, sizeof(mem_size));
 if (ret != RKNN_SUCC) {
    printf("rknn_query fail! ret=%d\n", ret);
    return -1;
 }
 printf("total weight size: %d, total internal size: %d\n", mem_size.total_weight_size, mem_size.total_internal_size);

5.3.2 Extern zugewiesenen Speicher verwenden

5.3.2.1 Ein- und Ausgabespeicher extern zuweisen

Wie unter „C API Zero-Copy-Ablauf“ beschrieben, kann Speicher für Ein- und Ausgabetensoren bei Verwendung der zero-copy API extern bereitgestellt und für die NPU konfiguriert werden; siehe dortiges Ablaufdiagramm. Externer Speicher ist nur mit der zero-copy API nutzbar. Dies ist vor allem für Fälle gedacht, in denen der Anwender den NPU-Speicher selbst statt über rknn_create_mem() bereitstellen muss.

Externer Speicher kann über physische Adressen oder fds beschrieben werden und wird hauptsächlich über zwei Schnittstellen eingebunden:

rknn_create_mem_from_phys(): Erzeugt die Struktur rknn_tensor_mem aus einer physischen Adresse

rknn_create_mem_from_fd(): Erzeugt die Struktur rknn_tensor_mem aus einem fd

Das folgende Beispiel legt Speicher mit mpi mmz an. rknn_create_mem_from_phys() importiert die physische Adresse des externen Speichers und erstellt eine rknn_tensor_mem-Struktur mit diesen Speicherinformationen. Beispielcode:

// Create input tensor memory
rknn_tensor_mem* input_mems[1];
// default input type is int8 (normalize and quantize need compute in outside)
// if set uint8, will fuse normalize and quantize to npu
input_attrs[0].type = input_type;
// default fmt is NHWC, npu only support NHWC in zero copy mode
input_attrs[0].fmt = input_layout;

input_mems[0] = rknn_create_mem_from_phys(ctx, input_phys, input_virt, input_attrs[0].size_with_stride);

...

// Create output tensor memory
rknn_tensor_mem* output_mems[io_num.n_output];
for (uint32_t i = 0; i < io_num.n_output; ++i) {
    output_mems[i] = rknn_create_mem_from_phys(ctx, output_physs[i], output_virts[i], output_attrs[i].size);
}

// Set input tensor memory
ret = rknn_set_io_mem(ctx, input_mems[0], &input_attrs[0]);
if (ret < 0) {
    printf("rknn_set_io_mem fail! ret=%d\n", ret);
    return -1;
}

// Set output tensor memory
for (uint32_t i = 0; i < io_num.n_output; ++i) {
    // set output memory and attribute
    ret = rknn_set_io_mem(ctx, output_mems[i], &output_attrs[i]);
    if (ret < 0) {
        printf("rknn_set_io_mem fail! ret=%d\n", ret);
        return -1;
    }
}

Externer Speicher kann statt über die physische Adresse auch über einen fd eingebunden werden. Beispielcode:

int mb_flags = RK_MMZ_ALLOC_TYPE_CMA | RK_MMZ_ALLOC_UNCACHEABLE;

// Allocate weight memory in outside
MB_BLK weight_mb;
rknn_tensor_mem* weight_mem;
ret = RK_MPI_MMZ_Alloc(&weight_mb, mem_size.total_weight_size, mb_flags);
if (ret < 0) {
    printf("RK_MPI_MMZ_Alloc failed, ret: %d\n", ret);
    return ret;
}

void* weight_virt = RK_MPI_MMZ_Handle2VirAddr(weight_mb);
if (weight_virt == NULL) {
    printf("RK_MPI_MMZ_Handle2VirAddr failed!\n");
    return -1;
}

int weight_fd = RK_MPI_MMZ_Handle2Fd(weight_mb);
if (weight_fd < 0) {
    printf("RK_MPI_MMZ_Handle2Fd failed!\n");
    return -1;
}

weight_mem = rknn_create_mem_from_fd(ctx, weight_fd, weight_virt, mem_size.total_weight_size, 0);
printf("weight mb info: virt = %p, fd = %d, size: %d\n", weight_virt, weight_fd, mem_size.total_weight_size);

int mb_flags = RK_MMZ_ALLOC_TYPE_CMA | RK_MMZ_ALLOC_UNCACHEABLE;

// Allocate weight memory in outside
MB_BLK weight_mb;
rknn_tensor_mem* weight_mem;
ret = RK_MPI_MMZ_Alloc(&weight_mb, mem_size.total_weight_size, mb_flags);
if (ret < 0) {
    printf("RK_MPI_MMZ_Alloc failed, ret: %d\n", ret);
    return ret;
}

void* weight_virt = RK_MPI_MMZ_Handle2VirAddr(weight_mb);
if (weight_virt == NULL) {
    printf("RK_MPI_MMZ_Handle2VirAddr failed!\n");
    return -1;
}

int weight_fd = RK_MPI_MMZ_Handle2Fd(weight_mb);
if (weight_fd < 0) {
    printf("RK_MPI_MMZ_Handle2Fd failed!\n");
    return -1;
}

weight_mem = rknn_create_mem_from_fd(ctx, weight_fd, weight_virt, mem_size.total_weight_size, 0);
printf("weight mb info: virt = %p, fd = %d, size: %d\n", weight_virt, weight_fd, mem_size.total_weight_size);

5.3.2.2 Modellspeicher extern zuweisen

Wie in 9.1 beschrieben, besteht der Modellspeicher aus internal memory und weight memory. Extern bereitgestellter Speicher kann über rknn_set_weight_mem() und rknn_set_internal_mem() für Gewichte und interne Daten festgelegt werden. Beispiel:

// Load RKNN Model
ret = rknn_init(&ctx, model_virt, model_size, RKNN_FLAG_MEM_ALLOC_OUTSIDE, NULL);
TIME_END(rknn_init);
if (ret < 0) {
    printf("rknn_init fail! ret=%d\n", ret);
    return -1;
}

//query and inset input / output tensor
...

// Allocate weight memory in outside
MB_BLK weight_mb;
rknn_tensor_mem* weight_mem;
ret = RK_MPI_MMZ_Alloc(&weight_mb, SIZE_ALIGN_128(mem_size.total_weight_size), mb_flags);
void* weight_virt = RK_MPI_MMZ_Handle2VirAddr(weight_mb);
int weight_fd = RK_MPI_MMZ_Handle2Fd(weight_mb);
weight_mem = rknn_create_mem_from_fd(ctx, weight_fd, weight_virt, mem_size.total_weight_size, 0);
ret = rknn_set_weight_mem(ctx, weight_mem);
if (ret < 0) {
    printf("rknn_set_weight_mem fail! ret=%d\n", ret);
    return -1;
}
printf("weight mb info: virt = %p, fd = %d, size: %d\n", weight_virt, weight_fd, mem_size.total_weight_size);

// Allocate internal memory in outside
MB_BLK internal_mb;
rknn_tensor_mem* internal_mem;
ret = RK_MPI_MMZ_Alloc(&internal_mb, SIZE_ALIGN_128(mem_size.total_internal_size), mb_flags);
void* internal_virt = RK_MPI_MMZ_Handle2VirAddr(internal_mb);
int internal_fd = RK_MPI_MMZ_Handle2Fd(internal_mb);

internal_mem = rknn_create_mem_from_fd(ctx, internal_fd, internal_virt, mem_size.total_internal_size, 0);
ret = rknn_set_internal_mem(ctx, internal_mem);
if (ret < 0) {
    printf("rknn_set_internal_mem fail! ret=%d\n", ret);
    return -1;
}
printf("internal mb info: virt = %p, fd = %d, size: %d\n", internal_virt, internal_fd, mem_size.total_internal_size);

5.3.3 Internen Speicher wiederverwenden

Die RKNN API ermöglicht externe Verwaltung des NPU-Speichers. RKNN_FLAG_INTERNAL_ALLOC_OUTSIDE legt fest, dass der Speicher für Zwischenmerkmale extern bereitgestellt wird. Typische Anwendungsfälle:

Bei der Bereitstellung weist der Benutzer den gesamten NPU-Speicher selbst zu und kann damit die Speicheraufteilung des Gesamtsystems koordinieren.

Bei serieller Ausführung mehrerer Modelle wird der Zwischentensorspeicher zwischen Kontexten wiederverwendet. Das ist besonders bei knappem Speicher, etwa auf RV1103/RV1106, hilfreich.

Im Beispiel benötigt der Internal Tensor von Modell 1 mehr Speicher als der von Modell 2. Bei serieller Ausführung reicht ein gemeinsam genutzter Block im Bereich 0x00000000~0x000c4000. Nach Modell 1 kann Modell 2 darin Internal-Tensor-Daten lesen und schreiben, wodurch Speicher gespart wird.

Abbildung 5-16. Zwei Modelle teilen denselben Adressraum für Internal Tensor

Abbildung 5-16. Zwei Modelle teilen denselben Adressraum für Internal Tensor

Die Modellpfade seien model_path_a und model_path_b. Beispielcode:

rknn_init(&ctx_a, model_path_a, 0, RKNN_FLAG_INTERNAL_ALLOC_OUTSIDE, NULL);
rknn_query(ctx_a, RKNN_QUERY_MEM_SIZE, &mem_size_a, sizeof(mem_size_a));
rknn_init(&ctx_b, model_path_b, 0, RKNN_FLAG_INTERNAL_ALLOC_OUTSIDE, NULL);
rknn_query(ctx_b, RKNN_QUERY_MEM_SIZE, &mem_size_b, sizeof(mem_size_b));

// Get the largest internal size of the two models
max_internal_size = MAX(mem_size_a.total_internal_size,
mem_size_b.total_internal_size);
internal_mem_max = rknn_create_mem(ctx_a, max_internal_size);

// Set the internal memory of model a
internal_mem_a = rknn_create_mem_from_fd(ctx_a, internal_mem_max->fd, internal_mem_max->virt_addr, mem_size_a.total_internal_size, 0);
rknn_set_internal_mem(ctx_a, internal_mem_a);

// Set the internal memory of model b
internal_mem_b = rknn_create_mem_from_fd(ctx_b, internal_mem_max->fd, internal_mem_max->virt_addr, mem_size_b.total_internal_size, 0);
rknn_set_internal_mem(ctx_b, internal_mem_b);

5.3.4 RKNN-Kontext in mehreren Threads wiederverwenden

In mehreren Threads kann dasselbe Modell gleichzeitig ausgeführt werden. Separate Kontexte pro Thread benötigen viel Speicher. Durch Wiederverwendung eines Kontexts lassen sich wiederholte Datenstrukturaufbauten vermeiden und Runtime-Speicher sparen. Die RKNN API stellt dafür folgende Schnittstelle bereit:

int rknn_dup_context(rknn_context* context_in,rknn_context* context_out)

context_in ist der bereits initialisierte Kontext; context_out verwendet context_in wieder. In der Abbildung besitzen beide Kontexte dieselbe Modellstruktur und können daher wiederverwendet werden.

Abbildung 5-17. Kontextwiederverwendung zweier gleicher Modelle

Abbildung 5-17. Kontextwiederverwendung zweier gleicher Modelle

5.3.5 Gewichte zwischen Modellen mit unterschiedlicher Auflösung teilen

Modelle unterschiedlicher Auflösung mit denselben Gewichten können diese zur Speicherersparnis teilen. In RKNPU SDK <= 1.5.0 ermöglichte dies dynamischen Wechsel zwischen Auflösungen mit geringerem Speicherbedarf. Nach 1.5.0 wurde diese Funktion durch dynamische Shapes ersetzt.

Im folgenden Diagramm sind die Gewichte der Modelle A und B vollständig identisch.

Abbildung 5-18. Gemeinsame Gewichte zweier Modelle unterschiedlicher Auflösung

Abbildung 5-18. Gemeinsame Gewichte zweier Modelle unterschiedlicher Auflösung

Für gemeinsame Gewichte bei unterschiedlichen Auflösungen führen Sie folgende Schritte aus:

  1. Legen Sie bei der Konvertierung ein Modell als master mit remove_weight=False in rknn.config fest; das andere wird slave mit remove_weight=True. Das Master-RKNN-Modell enthält Gewichte, das Slave-Modell keine Gewichte der Faltungsoperatoren.

  2. Initialisieren Sie bei der Bereitstellung zuerst master und dann slave. Für slave verwenden Sie RKNN_FLAG_SHARE_WEIGHT_MEM und den zusätzlichen Parameter rknn_init_extend mit dem Master-Kontext als Wert. Die Pfade heißen im Beispiel model_A und model_B. Beispielcode:

rknn_context context_A;
rknn_context context_B;
ret = rknn_init(&context_A,model_A,0,0,NULL);
...
rknn_init_extend extend;
extend.ctx = context_A;
ret = rknn_init(&context_B,model_B,0,RKNN_FLAG_SHARE_WEIGHT_MEM,&extend);

Dieses Thema entdecken → · Lernpfade →