Suche…


Einführung

Vor der Verwendung von OpenCL muss der Code für die Verwendung von OpenCL eingerichtet werden. In diesem Thema wird beschrieben, wie Sie Ihr Projekt öffnen und einen grundlegenden Kernel ausführen können. Die Beispiele basieren auf dem C # - Wrapper OpenCL.NET. Da der Wrapper jedoch keine Abstraktion zu OpenCL hinzufügt, wird der Code wahrscheinlich auch mit wenigen Änderungen in C / C ++ ausgeführt.

Aufrufe in C # können wie folgt aussehen: 'Cl.GetPlatformIDs'. Für die C-Style OpenCL-API würden Sie 'clGetPlatformIDs' und für den C ++ - Stil 'cl :: GetPlatformIDs' aufrufen.

Bemerkungen

  • NVidia, AMD und Intel haben unterschiedliche OpenCL-Implementierungen, aber die bekannten Unterschiede sind (meiner Erfahrung nach) auf Klammeranforderungen und implizite Umwandlungen beschränkt. Manchmal stürzt NVidia Ihren Kernel ab, während Sie versuchen, die richtige Überladung für eine Methode herauszufinden. In diesem Fall hilft es, eine explizite Besetzung anzubieten, um die GPU zu unterstützen. Das Problem wurde bei Runtime-kompilierten Kerneln beobachtet.

  • Um weitere Informationen zu den verwendeten Aufrufen in diesem Thema zu erhalten, reicht es aus, 'OpenCL' gefolgt von dem Funktionsnamen aufzurufen. Die Khronos-Gruppe verfügt über eine vollständige Dokumentation aller auf ihrer Website verfügbaren Parameter und Datentypen.

Zielgerät initialisieren

OpenCL-Kernel können entweder auf der GPU oder auf der CPU ausgeführt werden. Dies ermöglicht Fallback-Lösungen, bei denen der Kunde ein sehr veraltetes System hat. Der Programmierer kann seine Funktionalität auch auf die CPU oder die GPU beschränken.

Um mit OpenCL arbeiten zu können, benötigen Sie einen 'Kontext' und ein 'Gerät'. Beide sind von der OpenCL-API definierte Strukturen (auch bekannt als cl :: Context oder clContext & ~ Device) und definieren den verwendeten Zielprozessor.

Um Ihr Gerät und den Kontext abzurufen, müssen Sie eine Liste der verfügbaren Plattformen abfragen, die jeweils mehrere Geräte hosten können. Eine Plattform stellt Ihre physische GPU und CPU dar, während ein Gerät die enthaltenen Recheneinheiten weiter unterscheiden kann. Für GPUs verfügen die meisten Plattformen nur über ein Gerät. Eine CPU kann jedoch neben den CPU-Funktionen eine zusätzliche integrierte GPU bieten.

Der Kontext verwaltet Speicher, Befehlswarteschlangen, die verschiedenen Kernel und Programme. Ein Kontext kann entweder auf ein einzelnes Gerät beschränkt sein, aber auch auf mehrere Geräte verweisen.

Eine kurze API-Anmerkung, bevor wir mit dem Codieren beginnen: Fast jeder Aufruf von OpenCL liefert einen Fehlerwert, entweder als Rückgabewert oder über einen Ref-Wert (Zeiger in C). Jetzt können wir loslegen.

ErrorCode err;
var platforms = Cl.GetPlatformIDs(out err);
if(!CheckError(err, "Cl.GetPlatformIDs")) return;
foreach (var platform in platforms) {
    foreach (var device in Cl.GetDeviceIDs(platform, DeviceType.Gpu, out err)) {
        if(!CheckError(err, "Cl.GetDeviceIDs")) continue;
        [...]
    }
}

Dieses Code-Snippet fragt alle verfügbaren GPU-Geräte im System ab. Sie können sie nun einer Liste hinzufügen oder Ihren Kontext direkt mit dem ersten Treffer beginnen. Die Funktion 'CheckError (...)' ist ein einfaches Dienstprogramm, das prüft, ob der Fehlercode den Erfolgswert oder einen anderen Wert hat, und kann Ihnen eine Protokollierung anbieten. Es wird empfohlen, eine separate Funktion oder ein Makro zu verwenden, da dies häufig aufgerufen wird.

ErrorCode ist nur eine Aufzählung des Datentyps cl_int für C #. C / C ++ kann den int-Wert mit den hier angegebenen vordefinierten Fehlerkonstanten vergleichen: https://www.khronos.org/registry/OpenCL/sdk/1.0/docs/man/ xhtml / errors.html

Möglicherweise möchten Sie auch prüfen, ob das Gerät alle erforderlichen Funktionen unterstützt. Andernfalls können Ihre Kernel zur Laufzeit abstürzen. Sie können eine Gerätefunktion mit abfragen

Cl.GetDeviceInfo(_device, DeviceInfo.ImageSupport, out err)

In diesem Beispiel wird das Gerät gefragt, ob es Bildfunktionen ausführen kann. Für den nächsten und letzten Schritt müssen wir unseren Kontext aus den gesammelten Geräten konstruieren.

_context = Cl.CreateContext(null, 1, new[] { _device }, ContextNotify, IntPtr.Zero, out err);

Hier ist einiges los. Für C / C ++ - Folks ist IntPtr eine Zeigeradresse in C #. Ich werde mich hier auf die wichtigen Teile konzentrieren.

  • Der zweite Parameter definiert die Anzahl der Geräte, die Sie verwenden möchten
  • Der dritte Parameter ist ein Array dieser Geräte (oder ein Zeiger in C / C ++).
  • Und der dritte Parameter ist ein Funktionszeiger für eine Rückruffunktion. Diese Funktion wird immer dann verwendet, wenn Fehler im Kontext auftreten.

Für die weitere Verwendung müssen Sie Ihre verwendeten Geräte und den Kontext irgendwo beibehalten.

Wenn Sie alle Ihre OpenCL-Interaktionen beendet haben, müssen Sie den Kontext erneut mit freigeben

Cl.ReleaseContext(_context);

Kompilieren Sie Ihren Kernel

Kernel können zur Laufzeit auf dem Zielgerät kompiliert werden. Dafür brauchen Sie

  • der Kernel-Quellcode
  • das Zielgerät, auf dem kompiliert werden soll
  • ein mit dem Zielgerät erstellter Kontext

Ein kurzes Update zur Terminologie: Ein Programm enthält eine Sammlung von Kerneln. Sie können sich ein Programm als vollständige C / C ++ / C # -Quelldatei vorstellen, während Kernel die verschiedenen Funktionsmitglieder dieser Datei sind.

Zuerst müssen Sie ein Programm aus Ihrem Quellcode erstellen.

var program = Cl.CreateProgramWithSource(_context, 1, new[] { source }, null, out err);

Sie können mehrere Quelldateien in einem Programm zusammenfassen und zusammenstellen, sodass Kernel in verschiedenen Dateien gespeichert und in einem Arbeitsschritt zusammengestellt werden können.

Im nächsten Schritt müssen Sie das Programm auf Ihrem Zielgerät kompilieren.

err = Cl.BuildProgram(program, 1, new[] { _device }, string.Empty, null, IntPtr.Zero);

Hier nun ein kleiner Nachteil: Der Fehlercode sagt Ihnen nur, ob der Funktionsaufruf selbst erfolgreich war, nicht aber ob Ihr Code tatsächlich kompiliert wurde. Um dies zu überprüfen, müssen wir einige zusätzliche Informationen abfragen

BuildStatus status;
status = Cl.GetProgramBuildInfo(program, _device, ProgramBuildInfo.Status, out err).CastTo<BuildStatus>();
if (status != BuildStatus.Success) {
    var log = Cl.GetProgramBuildInfo(program, _device, ProgramBuildInfo.Log, out err);
}

C / C ++ - Leute können die Umwandlung am Ende ignorieren und die zurückgegebene Ganzzahl einfach mit der entsprechenden Konstante vergleichen.

Beim ersten Aufruf wird geprüft, ob unser Build tatsächlich erfolgreich war. Wenn nicht, können wir ein Protokoll zurückholen und genau sehen, wo etwas schief gelaufen ist. In den Anmerkungen zu einigen Plattformen finden Sie einige allgemeine Punkte.

Sobald das Programm erstellt ist, müssen Sie Ihre verschiedenen Kernel aus dem kompilierten Programm extrahieren. Dazu erstellen Sie Ihre Kernel mit

_kernel = Cl.CreateKernel(_program, kernel, out err);

Dabei ist 'Kernel' eine Zeichenfolge des Kernelnamens. Wenn Sie mit Ihrem Kernel fertig sind, müssen Sie ihn mit freigeben

Cl.ReleaseKernel(_kernel);

Erstellen einer Befehlswarteschlange

Um einen Vorgang auf Ihren Geräten zu initiieren, benötigen Sie für jedes Gerät eine Befehlswarteschlange. Die Warteschlange verfolgt verschiedene Anrufe, die Sie mit dem Zielgerät getätigt haben, und hält sie in der richtigen Reihenfolge. Die meisten Befehle können auch im blockierenden oder nicht blockierenden Modus ausgeführt werden.

Das Erstellen einer Warteschlange ist ziemlich einfach:

_queue = Cl.CreateCommandQueue(_context, _device, CommandQueueProperties.None, out err);

Die grundlegende Interaktion mit Ihrer Befehlswarteschlange besteht darin, verschiedene Operationen, die Sie ausführen möchten, in die Warteschlange aufzunehmen, z. B. Daten von und auf Ihr Gerät zu kopieren und einen Kernel zu starten.

Wenn Sie die Befehlswarteschlange nicht mehr benötigen, müssen Sie die Warteschlange mit einem Anruf an an freigeben

Cl.ReleaseCommandQueue(_queue);

Den Kernel ausführen

Nun kommen wir zum eigentlichen Zeug, indem wir Ihre Kernel auf dem parallelen Gerät ausführen. Bitte lesen Sie die Hardware-Grundlagen, um die Kernel-Verteilung vollständig zu verstehen.

Zuerst müssen Sie die Kernel-Argumente setzen, bevor Sie den Kernel wirklich aufrufen. Dies erfolgt über

err = Cl.SetKernelArg(_kernel, $argumentIndex, $argument);

Wenn Sie nicht jedes Argument vor dem Start des Kernels festlegen, schlägt der Kernel fehl.

Bevor wir unseren Kernel tatsächlich starten, müssen wir die 'globale Arbeitsgröße' und die 'lokale Arbeitsgröße' berechnen.

Die globale Arbeitsgröße ist die Gesamtzahl der Threads, die auf Ihrer GPU gestartet werden. Die lokale Arbeitsgröße ist die Anzahl der Threads in jedem Threadblock. Die lokale Arbeitsgröße kann weggelassen werden, wenn der Kernel keine besonderen Anforderungen benötigt. Wenn jedoch die lokale Arbeitsgröße angegeben wird, muss die globale Arbeitsgröße ein Vielfaches der lokalen Arbeitsgröße sein.

Die Arbeitsgrößen können entweder eindimensional, zweidimensional oder dreidimensional sein. Die Wahl, wie viele Dimensionen Sie wünschen, liegt ganz bei Ihnen, und Sie können auswählen, was Ihrem Algorithmus am besten entspricht.

Nun, da wir uns für unsere Arbeitsgrößen entschieden haben, können wir den Kernel nennen.

Event clevent;
err = Cl.EnqueueNDRangeKernel(_queue, _kernel, $dimensions, null, $globalWorkSize, $localWorkSize, 0, null, out clevent);

Die $ -Abmessungen definieren die gewünschte Anzahl von Dimensionen. $ GlobalWorkSize ist ein Array von Dimensionen der Größe $ mit der globalen Arbeitsgröße und dem für $ localWorkSize. Das letzte Argument gibt Ihnen ein Objekt, das Ihren aktuell ausgeführten Kernel darstellt.



Modified text is an extract of the original Stack Overflow Documentation
Lizenziert unter CC BY-SA 3.0
Nicht angeschlossen an Stack Overflow