CuDNN Graph-API

A grayscale illustration shows a 3dimensional cube with many different cubes suspended in it connected through a grid. A figure looks up at this network representation coding.

Autor: Damian, auticon Swiss IT-Consultant

CuDNN ist eine von NVIDIA gepflegte GPU-beschleunigte Bibliothek mit grundlegenden Implementierungen für tiefe neuronale Netze. Sie bietet Low-Level-Funktionen, die äusserst performant sind. Ziel dieser Bibliothek ist es, die beste verfügbare Leistung auf NVIDIA-GPUs für wichtige Anwendungsfälle des Deep Learning zu erreichen. Diese optimierte Leistung und wird durch die Verwendung von vorgefertigten Kernels erreicht, die sorgfältig auf verschiedene GPU-Architekturen abgestimmt sind, sowie durch die Echtzeit-Generierung von Kernels, welche dynamisch auf spezifische Anforderungen zugeschnitten sind. Dies gewährleistet eine effiziente und flexible Ausführung von Operationen in neuronalen Netzen.

Seit Version 8.0 von CuDNN ist die Bibliothek in zwei Gruppen von Operationen aufgeteilt: Legacy API und Graph API. Die Legacy-API besteht aus einem festen Satz von Operationen, die grösstenteils veraltet sind. Die Graph-API bietet ein deklaratives Programmiermodell zur Beschreibung von Berechnungen als Graph von Operationen, um mehr Flexibilität zu erreichen. Dieser Beitrag konzentriert sich auf die Graph-API.

CuDNN wird von vielen branchenführenden Deep-Learning-Frameworks wie Caffe2ChainerKerasMATLABMxNetPaddlePaddlePyTorch und TensorFlow verwendet.

Ressourcen

Die besten Informationsquellen zur CuDNN Graph-API und deren Implementierung sind die offizielle Dokumentationsowie CuDNN-Frontend.

Die Dokumentation bietet einen Überblick und Erläuterungen über Typen, Enums, Funktionen und Deskriptoren und deren Attribute. Außerdem gibt es einen Leitfaden für Entwickler mit weiteren Informationen zu spezifischen Implementierungen und Attributen von Deskriptoren sowie Unterstützungseinschränkungen für verschiedene GPU-Architekturen und deren Rechenkapazitäten. Allerdings kann die Dokumentation manchmal etwas überwältigend sein. Sie enthält auch einige Flüchtigkeitsfehler, die zu Verwirrung führen können, und manchmal scheint es Widersprüche zu geben. Sobald klar ist, wie die Graph-API aufgebaut ist, wird es auch einfacher, sich in der Dokumentation zurechtzufinden.

CuDNN-Frontend, ebenfalls von NVIDIA gepflegt, ist ein C++ Wrapper für die Low-Level C-API, mit Beispielen, wie bestimmte Operationen implementieren werden. Diese Implementierungen können verwendet werden, um die Informationen aus der Dokumentation zu überprüfen und zu sehen, wie bestimmte Komponenten funktionieren. CuDNN-Frontend enthält ausserdem eine Python-API.

Mit Hilfe von Logging können Informationen gesammelt werden, um zu sehen, was vor sich geht. Einzelheiten zur Einrichtung von Logging und zu den verschiedenen Detail-Level können hier gefunden werden. Logging kann als Einblick nützlich sein. Wenn man ein Log für die CuDNN-Frontend Testfälle erzeugen, mit Detail-Level auf Maximum, erhalten man einen guten Überblick über die erwarteten Typen, Werte und Beispiel-Werte, z.B. für die Dimensionen und Strides eines Tensors für eine bestimmte Operation.

Hinweis: Wenn Umgebungsvariablen für die Protokollierung unter Windows verwenden werden und die Werte geändert werden, ist ein Neustart erforderlich, damit die Änderungen wirksam werden.

Funktionen

Hauptfunktionen

Die Graph-API arbeitet mit Deskriptoren für die verschiedenen Strukturen, welche zur Erstellung eines Graphen erforderlich sind. Zunächst wird der Deskriptor erstellt, dann werden verschiedene Attribute gesetzt, um die Eigenschaften des Deskriptors zu definieren, und schließlich wird der Deskriptor finalisiert. Folgende Funktionen führen die einzelnen Schritte aus:

Es ist wichtig, die verwendeten Ressourcen nach Gebrauch freizugeben, daher gibt es eine Funktion zum Zerstören von Deskriptoren:

Einige Deskriptoren haben Attribute, welche nur gelesen werden können. Solche Attribute können mit dieser Funktion abgerufen werden:

Es gibt auch ein Handle, welches zwar kein Deskriptor ist, aber für die Ausführung des Graphen erforderlich ist. Dieses Handle kann mit den folgenden Funktionen erstellt und zerstört werden:

Alle Grap-API-Funktionen geben einen Integer-Wert zurück, der angibt, ob der Funktionsaufruf erfolgreich war oder ob dieser fehlgeschlagen ist. Um eine besser lesbare Rückmeldung zu erhalten, bietet CuDNN eine Funktion zum Abrufen von Fehlermeldungen:

Hinweis: Es ist möglich, benutzerdefinierte Callback-Funktionen zum Abrufen von Fehlern, Informationen und Warnungen zu setzen.

Neben diesen Funktionen gibt es noch einige weitere API-Funktionen für verschiedene Zwecke. Hier ist eine Übersicht der verfügbaren API-Funktionen für die Graph-API.

Zusätzlich zu diesen Funktionen gibt es auch Typen und Enums.

Speicher

Für einige Deskriptoren ist es notwendig, Speicher auf dem Gerät zuzuweisen, z.B. für Daten oder Workspace. Ausserdem werden Speicher-Funktionen zur Übertragung von Daten vom Host-Speicher zum Gerätespeicher, oder umgekehrt, verwendet. CuDNN selbst bietet keine Funktionen, um mit Speicher zu arbeiten, aber da CuDNN auf Cuda aufbaut, können die Cuda-Memory-Funktionalität verwendet werden, um Speicher auf dem Gerät zuzuweisen, zu kopieren und freizugeben.

Die drei Hauptfunktionen für die Speicherverwaltung mit Cuda und CuDNN Graph API:

Diese Funktionen sollten alle grundlegenden Anforderungen an die Speicherverwaltung mit der Graph-API bedienen. CudaMalloc wird verwendet, um Speicher auf dem Gerät zuzuweisen, cudaMemcpy, um Daten vom/zum Host/Gerät zu übertragen und cudaFree, um den zugewiesenen Speicher wieder freizugeben.

Hinweis: Da die Speicherfunktionen von Cuda bereitgestellt werden, sind für die Arbeit mit den entsprechenden Fehlern separate Fehlerfunktionalitäten erforderlich.

Schritte zur Implementierung eines einfachen Graphen mit CuDNN

Struktur

Die Deskriptoren lassen sich grob in die folgenden Gruppen einteilen:

  • Tensor
  • Knoten/Operationen
  • Graph
  • Ausführung
  • Information

Eingabe- und Ausgabedaten für Operationen werden als Tensor-Deskriptoren in Kombination mit Speicherzeigern, die auf die Daten zeigen, bereitgestellt. Graph-Deskriptoren verwenden die Operationsknoten, um so den Operations-Graphen abzubilden. Wenn alle Komponenten zusammengefügt sind, kann der Graph ausgeführt werden.

Für die Ausführung ist ein Graph von einer EngineConfig abhängig. Es ist möglich, eine Engine von Grund auf zu definieren, indem man die gewünschten Regler setzt, und diese für die Erstellung der EngineConfig verwendet. Die andere Möglichkeit ist es, Heuristik zu verwendet, um eine Liste von möglichen EngineConfigs für den angegebenen Graphen abzurufen.

Tensor

CuDNN Graph-API arbeitet mit Tensoren. Tensoren sind Deskriptoren, die die Informationen des Tensors enthalten, z.B. seine Dimensionen, Strides oder Ausrichtung. Tensoren haben eine eindeutige ID, mit der sie identifiziert werden können. Die Daten werden mit Hilfe der Speicherfunktionen verwaltet. Es ist notwendig, den Speicher-Zeiger und den zugehörigen Tensor-Deskriptor im Auge zu behalten.

Tensoren haben verschiedene Speicher-Layouts. Die beiden am häufigsten verwendeten Layouts sind NHWC und NCHW. Einige Operationen erfordern ein bestimmtes Layout, andere können mit mehreren Layouts arbeiten. Die Dokumentation enthält einige Angaben dazu, welche Art von Layout für welche Art von Operation erforderlich ist. Dies kann auf verschiedenen GPU-Architekturen variieren. Welches Layout ein Tensor verwendet, wird durch die Strides des Tensors definiert. Hier ist ein Beispiel für eine generische Funktion, welche die Strides für verschiedene Layouts berechnen kann.

Schritt Eins

Zunächst wird ein cudnnHandle erstellt. Dieses Handle wird von Deskriptoren in mehreren der folgenden Schritte verwendet.

Schritt Zwei

Als nächstes werden die Tensor-Deskriptoren, die von der gewünschten Operation verwendet werden, definiert. Tensoren benötigen Speicher, dieser sollte ebenfalls zugewiesen werden und, falls erforderlich, sollten ausserdem die Daten in den zugewiesenen Gerätespeicher übertragen werden. Diese Tensor-Deskriptoren werden dann verwendet, um die Operationsknoten zu erstellen.

Einige der gängigen Operationsknoten sind:

Die meisten dieser Operationsknoten benötigen einen zusätzlichen Deskriptor, der die Operation im Detail definiert. Ein Beispiel: Pointwise benötigt einen Deskriptor, in dem die Art der punktweisen Operation und andere Attribute definiert sind, um korrekt finalisiert zu werden.

Zusammengefasst:

  • Erforderliche Tensor-Deskriptoren erstellen
  • Speicher zuweisen und Daten übertragen
  • Erstellen von Operationsdeskriptoren mit den zugehörigen Detaildeskriptoren

Schritt Drei

Einen OperationGraph-Deskriptor erstellen. Dazu wird das in Schritt Eins erstellte Handle benötigt. Die im vorherigen Schritt erstellten Operationen werden verwendet, um den Operationsgraph zu definieren. Die Abfolge der Operationen im Graph wird mit Hilfe der UIDs der Tensoren der verschiedenen Operationen bestimmt, bzw. gefolgert.

Schritt Vier

In diesem Schritt wird der EngineConfig-Deskriptor erstellt. Wie bereits erwähnt, gibt es zwei Möglichkeiten, eine gültige EngineConfig zu erhalten:

Entweder es wird ein Engine-Deskriptor mit dem Operationsgraphen aus dem letzten Schritt erstellt, und dann wird diese Engine verwendet, um eine Engine-Konfiguration zu erstellen. Es könnte erforderlich sein, Regler in Form von Knob-Deskriptoren zu setzen, um eine funktionierende Engine-Konfiguration für den Graphen zu erstellen.

Die andere Möglichkeit besteht darin, einen Heuristik-Deskriptor zu erstellen, der ebenfalls den Operationsgraphen aus dem letzten Schritt benötigt, und von diesem eine gültige Engine-Konfiguration auszulesen.

Schritt Fünf

Nun werden das Handle aus Schritt eins und die Engine-Konfiguration aus dem letzten Schritt verwendet, um einen ExecutionPlan-Descriptor zu erstellen. Es sollte geprüft werden, ob der Ausführungsplan Workspace benötigt, indem die erforderliche Workspace-Größe abgefragt wird. Wenn Workspace benötigt wird, wird der entsprechende Speicher zugewiesen und der Zeiger für Workspace gespeichert.

Schritt Sechs

Für diesen Schritt ist es erforderlich, die Daten in Form von Tensor-Deskriptoren und Speicherzeigern zu sammeln. Die notwendigen Informationen müssen gesammelt werden, um einen VariantPack-Deskriptor zu erstellen. Für das Variantenpaket ist es wichtig, die Tensor-UIDs und die zugehörigen Speicherzeiger in der gleichen Reihenfolge zu übergeben. Das bedeutet, dass das erste Element des UIDs-Arrays die Tensor-UID des Tensors enthält, der seine Daten an dem Speicherzeiger speichert, der das erste Element des Speicherzeiger-Arrays ist, und so weiter.

Schritt Sieben

Da nun alle erforderlichen Deskriptoren erstellt sind, kann der Graph ausgeführt werden.

Schritt Acht

Nach der Ausführung werden die berechneten Daten im Gerätespeicher gespeichert, so dass es notwendig ist, die Daten vom Gerätespeicher in den Hostspeicher zu übertragen, um mit diesen zu arbeiten.

Schritt Neun

Dieser Schritt ist sehr wichtig, um Speicherlecks und andere Probleme zu vermeiden: Aufräumen aller verwendeten Ressourcen. Der Speicher muss freigegeben werden; Deskriptoren und Handles müssen zerstört werden.

Beispiel

Dieses Beispiel zeigt, wie man mit der CuDNN Graph-API einen einfachen Graphen mit einem einzelnen Knoten erstellt, um die Verwendung der grundlegenden CuDNN Graph-API Komponenten zu demonstrieren. In einem tatsächlichen Anwendungsfall wäre der Graph komplexer und würde mehr Knoten und Tensoren enthalten oder sogar in Untergraphen aufgeteilt werden.

Damit dieses Beispiel funktioniert, müssen alle erforderlichen Komponenten installiert sein. Die Dokumentation bietet eine Anleitung zur Installation von CuDNN für Windows oder Linux.

#include <cudnn.h>
#include "cuda_runtime.h"
#include <iostream>
#include <algorithm>

// Makro für die Überprüfung von CuDNN-Fehlern
#define checkCUDNN(expression)                                   \
  {                                                              \
    cudnnStatus_t status = (expression);                         \
    if (status != CUDNN_STATUS_SUCCESS) {                        \
      std::cerr << "Error on line " << __LINE__ << ": "          \
                << cudnnGetErrorString(status) << std::endl;     \
      std::exit(EXIT_FAILURE);                                   \
    }                                                            \
  }

// Tensordimensionen für Matrixmultiplikation
constexpr int B = 16;
constexpr int M = 128;
constexpr int N = 128;
constexpr int K = 256;

// Hilfs-Funktionen:
// Zuweisung von Speicherplatz auf dem Gerät
void allocateMemory(void** ptr, size_t size) {
    cudaError_t err = cudaMalloc(ptr, size);
    if (err != cudaSuccess) {
        std::cerr << "cudaMalloc failed for size " << size << ": "
            << cudaGetErrorString(err) << std::endl;
        std::exit(EXIT_FAILURE);
    }
}

// Generische Funktion zur Berechnung von Tensor-Strides. 
// Diese implementierung folgt der Beispielfunktion, die im Blogbeitrag verlinkt ist.
void generate_stride(const int64_t* dim, int64_t* stride, const int64_t* stride_order, size_t num_dims) {
    struct DimInfo {
        int64_t order;
        size_t index;
        int64_t size;
    } sorted_dims[8];

    for (size_t i = 0; i < num_dims; ++i) {
        sorted_dims[i] = { stride_order[i], i, dim[i] };
    }

    std::sort(sorted_dims, sorted_dims + num_dims, [](const DimInfo& a, const DimInfo& b) {
        return a.order < b.order;
        });

    int64_t product = 1;
    for (size_t i = 0; i < num_dims; ++i) {
        stride[sorted_dims[i].index] = product;
        product *= sorted_dims[i].size;
    }
}

// Tensordaten mit den angegebenen Werten füllen
void fillTensorWithValues(float* hostData, size_t size, float value) {
    for (size_t i = 0; i < size; ++i) {
        hostData[i] = value;
    }
}

// Daten ausgeben (die Ausgabe wird bei großen Tensoren gekürzt)
void printTensorData(const float* hostData, size_t rows, size_t cols, size_t maxPrint = 5) {
    std::cout << "Tensor data (" << rows << "x" << cols << "):" << std::endl;
    for (size_t i = 0; i < std::min(rows, maxPrint); ++i) {
        for (size_t j = 0; j < std::min(cols, maxPrint); ++j) {
            std::cout << hostData[i * cols + j] << " ";
        }
        std::cout << (cols > maxPrint ? "... " : "") << std::endl;
    }
    if (rows > maxPrint) std::cout << "...\n";
}

// Funktionen für das Erstellen von Deskriptoren:
// Einen Tensor-Deskriptor erstellen und Finalisieren
void createTensorDescriptor(
    cudnnBackendDescriptor_t& tensorDesc, 
    int64_t* dimensions, 
    int64_t* strides, 
    cudnnDataType_t dtype, 
    int64_t unique_id, 
    int64_t alignment) 
{
    // Deskriptor erstellen
    checkCUDNN(cudnnBackendCreateDescriptor(CUDNN_BACKEND_TENSOR_DESCRIPTOR, &tensorDesc));
    // Attribute setzen
    checkCUDNN(cudnnBackendSetAttribute(tensorDesc, CUDNN_ATTR_TENSOR_DATA_TYPE, CUDNN_TYPE_DATA_TYPE, 1, &dtype));
    checkCUDNN(cudnnBackendSetAttribute(tensorDesc, CUDNN_ATTR_TENSOR_DIMENSIONS, CUDNN_TYPE_INT64, 3, dimensions));
    checkCUDNN(cudnnBackendSetAttribute(tensorDesc, CUDNN_ATTR_TENSOR_STRIDES, CUDNN_TYPE_INT64, 3, strides));
    checkCUDNN(cudnnBackendSetAttribute(tensorDesc, CUDNN_ATTR_TENSOR_UNIQUE_ID, CUDNN_TYPE_INT64, 1, &unique_id));
    checkCUDNN(cudnnBackendSetAttribute(tensorDesc, CUDNN_ATTR_TENSOR_BYTE_ALIGNMENT, CUDNN_TYPE_INT64, 1, &alignment));
    // Finalisieren
    checkCUDNN(cudnnBackendFinalize(tensorDesc));
}

// Erstellen und Finalisieren eines MatMul-Deskriptors
void createMatMulOpDescriptor(
    cudnnBackendDescriptor_t& matmulOpDesc,
    cudnnDataType_t computeType) 
{
    // Deskriptor erstellen
    checkCUDNN(cudnnBackendCreateDescriptor(CUDNN_BACKEND_MATMUL_DESCRIPTOR, &matmulOpDesc));
    // Attribute setzen
    checkCUDNN(cudnnBackendSetAttribute(matmulOpDesc, CUDNN_ATTR_MATMUL_COMP_TYPE, CUDNN_TYPE_DATA_TYPE, 1, &computeType));
    // Finalisieren
    checkCUDNN(cudnnBackendFinalize(matmulOpDesc));
}

// Erstellen und Finalisieren eines MatMul-Knoten-Deskriptors
void createMatMulNode(
    cudnnBackendDescriptor_t& matmulNodeDesc,
    cudnnBackendDescriptor_t matmulOpDesc,
    cudnnBackendDescriptor_t aDesc,
    cudnnBackendDescriptor_t bDesc,
    cudnnBackendDescriptor_t cDesc) 
{
    // Deskriptor erstellen
    checkCUDNN(cudnnBackendCreateDescriptor(CUDNN_BACKEND_OPERATION_MATMUL_DESCRIPTOR, &matmulNodeDesc));
    // Attribute setzen
    checkCUDNN(cudnnBackendSetAttribute(matmulNodeDesc, CUDNN_ATTR_OPERATION_MATMUL_ADESC, CUDNN_TYPE_BACKEND_DESCRIPTOR, 1, &aDesc));
    checkCUDNN(cudnnBackendSetAttribute(matmulNodeDesc, CUDNN_ATTR_OPERATION_MATMUL_BDESC, CUDNN_TYPE_BACKEND_DESCRIPTOR, 1, &bDesc));
    checkCUDNN(cudnnBackendSetAttribute(matmulNodeDesc, CUDNN_ATTR_OPERATION_MATMUL_CDESC, CUDNN_TYPE_BACKEND_DESCRIPTOR, 1, &cDesc));
    checkCUDNN(cudnnBackendSetAttribute(matmulNodeDesc, CUDNN_ATTR_OPERATION_MATMUL_DESC, CUDNN_TYPE_BACKEND_DESCRIPTOR, 1, &matmulOpDesc));
    // Finalisieren
    checkCUDNN(cudnnBackendFinalize(matmulNodeDesc));
}

// Erstellen und Finalisieren eines OperationGraph-Deskriptors
// Der Operationsgraph wird mit Operationsknoten befüllt
void createOperationGraphDescriptor(
    cudnnBackendDescriptor_t& opGraphDesc, 
    cudnnHandle_t handle, 
    cudnnBackendDescriptor_t* ops, 
    int64_t numOps) 
{
    // Deskriptor erstellen
    checkCUDNN(cudnnBackendCreateDescriptor(CUDNN_BACKEND_OPERATIONGRAPH_DESCRIPTOR, &opGraphDesc));
    // Attribute setzen
    checkCUDNN(cudnnBackendSetAttribute(opGraphDesc, CUDNN_ATTR_OPERATIONGRAPH_OPS, CUDNN_TYPE_BACKEND_DESCRIPTOR, numOps, ops));
    checkCUDNN(cudnnBackendSetAttribute(opGraphDesc, CUDNN_ATTR_OPERATIONGRAPH_HANDLE, CUDNN_TYPE_HANDLE, 1, &handle));
    // Finalisieren
    checkCUDNN(cudnnBackendFinalize(opGraphDesc));
}

// Erstellen und Finalisieren eines Heuristic-Deskriptors
// Dieser Deskriptor wird zur Abfrage von EngineConfiguration verwendet
void createHeuristicDescriptor(
    cudnnBackendDescriptor_t& heuristicDesc,
    cudnnBackendDescriptor_t opGraphDesc, 
    cudnnBackendHeurMode_t heurMode) 
{
    // Deskriptor erstellen
    checkCUDNN(cudnnBackendCreateDescriptor(CUDNN_BACKEND_ENGINEHEUR_DESCRIPTOR, &heuristicDesc));
    // Attribute setzen
    checkCUDNN(cudnnBackendSetAttribute(heuristicDesc, CUDNN_ATTR_ENGINEHEUR_OPERATION_GRAPH, CUDNN_TYPE_BACKEND_DESCRIPTOR, 1, &opGraphDesc));
    checkCUDNN(cudnnBackendSetAttribute(heuristicDesc, CUDNN_ATTR_ENGINEHEUR_MODE, CUDNN_TYPE_HEUR_MODE, 1, &heurMode));
    // Finalisieren
    checkCUDNN(cudnnBackendFinalize(heuristicDesc));
}

// Erstellen und Finalisieren eines ExecutionPlan-Deskriptors
// Dieser definiert, wie der Operationsgraph ausgeführt werden soll
void createExecutionPlanDescriptor(
    cudnnBackendDescriptor_t& planDesc,
    cudnnHandle_t handle,
    cudnnBackendDescriptor_t engCfgDesc) 
{
    // Deskriptor erstellen
    checkCUDNN(cudnnBackendCreateDescriptor(CUDNN_BACKEND_EXECUTION_PLAN_DESCRIPTOR, &planDesc));
    // Attribute setzen
    checkCUDNN(cudnnBackendSetAttribute(planDesc, CUDNN_ATTR_EXECUTION_PLAN_HANDLE, CUDNN_TYPE_HANDLE, 1, &handle));
    checkCUDNN(cudnnBackendSetAttribute(planDesc, CUDNN_ATTR_EXECUTION_PLAN_ENGINE_CONFIG, CUDNN_TYPE_BACKEND_DESCRIPTOR, 1, &engCfgDesc));
    // Finalisieren
    checkCUDNN(cudnnBackendFinalize(planDesc));
}

// Erstellen und Finalisieren eines VariantPack-Deskriptors
// Das VariantPack bindet Tensor-Datenzeiger an eindeutige Tensor-IDs
void createVariatnPack(
    cudnnBackendDescriptor_t& varPackDesc,
    void* workspace,
    void** dataPtrs,
    int64_t* uids, 
    int numDataPtrs)
{
    // Deskriptor erstellen
    checkCUDNN(cudnnBackendCreateDescriptor(CUDNN_BACKEND_VARIANT_PACK_DESCRIPTOR, &varPackDesc));
    // Attribute setzen
    checkCUDNN(cudnnBackendSetAttribute(varPackDesc, CUDNN_ATTR_VARIANT_PACK_DATA_POINTERS, CUDNN_TYPE_VOID_PTR, numDataPtrs, dataPtrs));
    checkCUDNN(cudnnBackendSetAttribute(varPackDesc, CUDNN_ATTR_VARIANT_PACK_UNIQUE_IDS, CUDNN_TYPE_INT64, numDataPtrs, uids));
    checkCUDNN(cudnnBackendSetAttribute(varPackDesc, CUDNN_ATTR_VARIANT_PACK_WORKSPACE, CUDNN_TYPE_VOID_PTR, 1, &workspace));
    // Finalisieren
    checkCUDNN(cudnnBackendFinalize(varPackDesc));
}

// Hauptfunktion zur Demonstration der Ausführung eines CuDNN-Operationsgraphen
int main() 
{
    // Schritt Eins: Erstellen eines CuDNN-Handles
    cudnnHandle_t cudnn;
    checkCUDNN(cudnnCreate(&cudnn));

    // Schritt Zwei: Tensoren definieren und die Deskriptoren erstellen
    int64_t aDims[] = { B, M, K };
    int64_t bDims[] = { B, K, N };
    int64_t cDims[] = { B, M, N };

    int64_t aStrides[3];
    int64_t bStrides[3];
    int64_t cStrides[3];

    int64_t aMemSize = B * M * K * sizeof(float);
    int64_t bMemSize = B * K * N * sizeof(float);
    int64_t cMemSize = B * M * N * sizeof(float);

    // Berechnung der Strides für die Tensoren
    int64_t stride_order[] = { 2, 1, 0 }; // Standard-Stride Reihenfolge: [B, M, K]
    generate_stride(aDims, aStrides, stride_order, 3);
    generate_stride(bDims, bStrides, stride_order, 3);
    generate_stride(cDims, cStrides, stride_order, 3);

    // Tensor-Deskriptoren erstellen
    cudnnDataType_t dtype = CUDNN_DATA_FLOAT; // Tensor Datentyp
    cudnnBackendDescriptor_t aDesc, bDesc, cDesc;
    createTensorDescriptor(aDesc, aDims, aStrides, dtype, 0, 4);
    createTensorDescriptor(bDesc, bDims, bStrides, dtype, 1, 4);
    createTensorDescriptor(cDesc, cDims, cStrides, dtype, 2, 4);

    // Gerätespeicher für Tensoren zuweisen
    void* aData, * bData, * cData, * workspace;
    allocateMemory(&aData, aMemSize);
    allocateMemory(&bData, bMemSize);
    allocateMemory(&cData, cMemSize);

    // Host-Speicher für Tensor-Daten zuweisen
    float* aHost = (float*)malloc(aMemSize);
    float* bHost = (float*)malloc(bMemSize);
    float* cHost = (float*)malloc(cMemSize);

    // Eingabe-Tensoren mit Standardwerten füllen
    fillTensorWithValues(aHost, B * M * K, 1.0);
    fillTensorWithValues(bHost, B * K * N, 2.0);

    // Übertragung von Eingangsdaten vom Host zum Gerät
    cudaMemcpy(aData, aHost, aMemSize, cudaMemcpyHostToDevice);
    cudaMemcpy(bData, bHost, bMemSize, cudaMemcpyHostToDevice);

    // Erstellen eines MatMul-Operationsdeskriptors
    cudnnBackendDescriptor_t matmulOpDesc;
    createMatMulOpDescriptor(matmulOpDesc, dtype);

    // Erstellen eines MatMul-Operationsknotens
    cudnnBackendDescriptor_t matmulNodeDesc;
    createMatMulNode(matmulNodeDesc, matmulOpDesc, aDesc, bDesc, cDesc);

    // Dritter Schritt: Erstellen eines Operationsgraphen-Deskriptors
    cudnnBackendDescriptor_t opGraphDesc;
    cudnnBackendDescriptor_t ops[] = { matmulNodeDesc }; // Liste der Operations-Knoten
    createOperationGraphDescriptor(opGraphDesc, cudnn, ops, 1);

    // Vierter Schritt: Abfrage von EngineConfiguration mit Hilfe eines Heuristic-Deskriptors
    cudnnBackendDescriptor_t heuristicDesc;
    cudnnBackendHeurMode_t heurMode = CUDNN_HEUR_MODE_A;
    createHeuristicDescriptor(heuristicDesc, opGraphDesc, heurMode);

    // Abrufen der ersten EngineConfiguration aus dem Heuristic-Deskriptors
    cudnnBackendDescriptor_t engineConfig;
    checkCUDNN(cudnnBackendCreateDescriptor(CUDNN_BACKEND_ENGINECFG_DESCRIPTOR, &engineConfig));
    checkCUDNN(cudnnBackendGetAttribute(heuristicDesc, CUDNN_ATTR_ENGINEHEUR_RESULTS, CUDNN_TYPE_BACKEND_DESCRIPTOR, 1, NULL, &engineConfig));

    // Fünfter Schritt: Erstellen eines ExecutionPlan-Deskriptors
    cudnnBackendDescriptor_t planDesc;
    createExecutionPlanDescriptor(planDesc, cudnn, engineConfig);

    // Abfrage und Zuweisung des für den Ausführungsplan erforderlichen Speichers
    size_t workspaceSize;
    checkCUDNN(cudnnBackendGetAttribute(planDesc, CUDNN_ATTR_EXECUTION_PLAN_WORKSPACE_SIZE, CUDNN_TYPE_INT64, 1, NULL, &workspaceSize));
    allocateMemory(&workspace, workspaceSize);

    // Sechster Schritt: Erstellen eines VariantPack-Deskriptors
    void* dataPtrs[] = { aData, bData, cData }; // Speicherzeiger für Tensordaten
    int64_t uids[] = { 0, 1, 2 }; // UIDs für Tensoren
    cudnnBackendDescriptor_t varPackDesc;
    createVariatnPack(varPackDesc, workspace, dataPtrs, uids, 3);

    // Schritt Sieben: Ausführen des Operationsgraphen
    checkCUDNN(cudnnBackendExecute(cudnn, planDesc, varPackDesc));

    // Achter Schritt: Übertragung von Ausgabedaten vom Gerät zum Host
    cudaMemcpy(cHost, cData, cMemSize, cudaMemcpyDeviceToHost);
    // Anzeigen des Ausgabetensors
    std::cout << "Output Tensor C:" << std::endl;
    printTensorData(cHost, 10, 10);

    // Schritt Neun: Ressourcen freigeben
    free(aHost);
    free(bHost);
    free(cHost);
    cudaFree(aData);
    cudaFree(bData);
    cudaFree(cData);
    cudaFree(workspace);
    cudnnBackendDestroyDescriptor(aDesc);
    cudnnBackendDestroyDescriptor(bDesc);
    cudnnBackendDestroyDescriptor(cDesc);
    cudnnBackendDestroyDescriptor(matmulOpDesc);
    cudnnBackendDestroyDescriptor(matmulNodeDesc);
    cudnnBackendDestroyDescriptor(opGraphDesc);
    cudnnBackendDestroyDescriptor(heuristicDesc);
    cudnnBackendDestroyDescriptor(varPackDesc);
    cudnnDestroy(cudnn);

    return 0;
}

Related Posts

Skip to content