Aktivieren Sie die GPU-Beschleunigung. Rechnen auf der GPU. Berechnung von GPU zu CPU

💖 Gefällt es dir? Teilen Sie den Link mit Ihren Freunden

Verwendung von GPU-Computing mit C++ AMP

Bisher haben wir bei der Erörterung paralleler Programmiertechniken nur Prozessorkerne berücksichtigt. Wir haben einige Fähigkeiten im Parallelisieren von Programmen über mehrere Prozessoren hinweg, im Synchronisieren des Zugriffs auf gemeinsam genutzte Ressourcen und im Verwenden von Hochgeschwindigkeits-Synchronisationsprimitiven ohne den Einsatz von Sperren erworben.

Es gibt jedoch eine andere Möglichkeit, Programme zu parallelisieren – Grafikprozessoren (GPUs), haben eine große Anzahl Kerne als selbst Hochleistungsprozessoren. GPU-Kerne eignen sich hervorragend für die Implementierung paralleler Datenverarbeitungsalgorithmen, und ihre große Anzahl macht die Unannehmlichkeiten, die mit der Ausführung von Programmen auf ihnen verbunden sind, mehr als wett. In diesem Artikel lernen wir eine der Möglichkeiten kennen, Programme auf einer GPU auszuführen, indem wir eine Reihe von C++-Spracherweiterungen namens „ C++AMP.

Die C++ AMP-Erweiterungen basieren auf der Sprache C++, weshalb in diesem Artikel Beispiele in C++ gezeigt werden. Allerdings bei mäßiger Nutzung des Interaktionsmechanismus in. NET können Sie C++ AMP-Algorithmen in Ihren .NET-Programmen verwenden. Aber darüber werden wir am Ende des Artikels sprechen.

Einführung in C++ AMP

Tatsächlich, GPU ist derselbe Prozessor wie jeder andere, jedoch mit einem speziellen Befehlssatz, einer großen Anzahl von Kernen und einem eigenen Speicherzugriffsprotokoll. Es gibt jedoch große Unterschiede zwischen modernen GPUs und herkömmlichen Prozessoren, und deren Verständnis ist der Schlüssel zum Erstellen von Programmen, die die Rechenleistung der GPU effektiv nutzen.

    Moderne GPUs verfügen über einen sehr kleinen Befehlssatz. Dies impliziert einige Einschränkungen: fehlende Möglichkeit zum Aufrufen von Funktionen, begrenzte Anzahl unterstützter Datentypen, fehlende Bibliotheksfunktionen und andere. Einige Vorgänge, beispielsweise bedingte Verzweigungen, können erheblich mehr kosten als ähnliche Vorgänge, die auf herkömmlichen Prozessoren ausgeführt werden. Offensichtlich erfordert das Verschieben großer Codemengen von der CPU auf die GPU unter solchen Bedingungen einen erheblichen Aufwand.

    Die Anzahl der Kerne ist bei einer durchschnittlichen GPU deutlich höher als bei einem durchschnittlichen herkömmlichen Prozessor. Einige Aufgaben sind jedoch zu klein oder können nicht in ausreichend große Teile zerlegt werden, um von der GPU zu profitieren.

    Die Synchronisierungsunterstützung zwischen GPU-Kernen, die dieselbe Aufgabe ausführen, ist sehr schlecht und zwischen GPU-Kernen, die unterschiedliche Aufgaben ausführen, fehlt sie völlig. Dieser Umstand erfordert eine Synchronisierung des Grafikprozessors mit einem herkömmlichen Prozessor.

Es stellt sich sofort die Frage: Welche Aufgaben eignen sich zur Lösung auf einer GPU? Bedenken Sie, dass nicht jeder Algorithmus für die Ausführung auf einer GPU geeignet ist. GPUs haben beispielsweise keinen Zugriff auf E/A-Geräte, sodass Sie die Leistung eines Programms, das RSS-Feeds aus dem Internet scrapt, nicht mithilfe einer GPU verbessern können. Viele Rechenalgorithmen lassen sich jedoch auf die GPU übertragen und massiv parallelisieren. Nachfolgend finden Sie einige Beispiele für solche Algorithmen (diese Liste ist keineswegs vollständig):

    zunehmende und abnehmende Bildschärfe und andere Transformationen;

    Schnelle Fourier-Transformation;

    Matrixtransposition und -multiplikation;

    Zahlensortierung;

    direkte Hash-Inversion.

Eine hervorragende Quelle für zusätzliche Beispiele ist der Microsoft Native Concurrency-Blog, der Codeausschnitte und Erklärungen für verschiedene in C++ AMP implementierte Algorithmen bereitstellt.

C++ AMP ist ein in Visual Studio 2012 enthaltenes Framework, das C++-Entwicklern eine einfache Möglichkeit bietet, Berechnungen auf der GPU durchzuführen und dafür lediglich einen DirectX 11-Treiber zu benötigen. Microsoft hat C++ AMP als offene Spezifikation veröffentlicht, die von jedem Compiler-Anbieter implementiert werden kann.

Mit dem C++ AMP-Framework können Sie Code ausführen Grafikbeschleuniger Beschleuniger, das sind Computergeräte. Mithilfe des DirectX 11-Treibers erkennt das C++ AMP-Framework alle Beschleuniger dynamisch. C++ AMP beinhaltet auch Software-Emulator Beschleuniger und Emulator basierend auf einem herkömmlichen Prozessor, WARP, der als Fallback auf Systemen ohne GPU oder mit GPU, aber ohne DirectX 11-Treiber dient und mehrere Kerne und SIMD-Anweisungen verwendet.

Beginnen wir nun mit der Erforschung eines Algorithmus, der für die Ausführung auf einer GPU leicht parallelisiert werden kann. Die folgende Implementierung nimmt zwei Vektoren gleicher Länge und berechnet das punktweise Ergebnis. Etwas Einfacheres kann man sich kaum vorstellen:

Void VectorAddExpPointwise(float* first, float* second, float* result, int length) ( for (int i = 0; i< length; ++i) { result[i] = first[i] + exp(second[i]); } }

Um diesen Algorithmus auf einem regulären Prozessor zu parallelisieren, müssen Sie den Iterationsbereich in mehrere Unterbereiche aufteilen und für jeden einen Ausführungsthread ausführen. Wir haben in früheren Artikeln viel Zeit damit verbracht, genau diese Art der Parallelisierung unseres ersten Beispiels für eine Primzahlensuche zu untersuchen – wir haben gesehen, wie dies durch manuelles Erstellen von Threads, Übergeben von Jobs an einen Thread-Pool und die Verwendung von Parallel.For erreicht werden kann und PLINQ zur automatischen Parallelisierung. Denken Sie auch daran, dass wir bei der Parallelisierung ähnlicher Algorithmen auf einem herkömmlichen Prozessor besonders darauf geachtet haben, das Problem nicht in zu kleine Aufgaben aufzuteilen.

Für die GPU sind diese Warnungen nicht erforderlich. GPUs verfügen über mehrere Kerne, die Threads sehr schnell ausführen, und die Kosten für den Kontextwechsel sind deutlich geringer als bei herkömmlichen Prozessoren. Unten sehen Sie einen Ausschnitt, der versucht, die Funktion zu verwenden parallel_for_each aus dem C++ AMP-Framework:

#enthalten #enthalten Verwendung der Namespace-Parallelität; void VectorAddExpPointwise(float* first, float* second, float* result, int length) ( array_view avFirst(length, first); array_view avSecond(length, second); array_view avResult(length, result); avResult.discard_data(); parallel_for_each(avResult.extent, [=](index<1>i) strict(amp) ( avResult[i] = avFirst[i] + fast_math::exp(avSecond[i]); )); avResult.synchronize(); )

Lassen Sie uns nun jeden Teil des Codes einzeln untersuchen. Beachten wir sofort, dass die allgemeine Form der Hauptschleife beibehalten wurde, die ursprünglich verwendete for-Schleife jedoch durch einen Aufruf der Funktion parallel_for_each ersetzt wurde. Tatsächlich ist das Prinzip der Umwandlung einer Schleife in einen Funktions- oder Methodenaufruf für uns nicht neu – eine solche Technik wurde bereits zuvor mit den Methoden Parallel.For() und Parallel.ForEach() aus der TPL-Bibliothek demonstriert.

Als nächstes werden die Eingabedaten (Parameter erster, zweiter und Ergebnis) mit Instanzen umschlossen array_view. Die Klasse array_view wird zum Umschließen von Daten verwendet, die an die GPU (Beschleuniger) übergeben werden. Sein Vorlagenparameter gibt den Datentyp und seine Dimension an. Um Anweisungen auf einer GPU auszuführen, die auf Daten zugreifen, die ursprünglich auf einer herkömmlichen CPU verarbeitet wurden, muss sich jemand oder etwas darum kümmern, die Daten auf die GPU zu kopieren, da die meisten modernen Grafikkarten separate Geräte mit eigenem Speicher sind. array_view-Instanzen lösen dieses Problem – sie ermöglichen das Kopieren von Daten bei Bedarf und nur dann, wenn es wirklich benötigt wird.

Wenn die GPU die Aufgabe abschließt, werden die Daten zurückkopiert. Indem wir array_view mit einem const-Argument instanziieren, stellen wir sicher, dass „first“ und „second“ in den GPU-Speicher kopiert, aber nicht zurückkopiert werden. Ebenso anrufen verwerfen_data() Wir schließen das Kopieren des Ergebnisses aus dem Speicher eines regulären Prozessors in den Beschleunigerspeicher aus, diese Daten werden jedoch in die entgegengesetzte Richtung kopiert.

Die Funktion parallel_for_each benötigt ein Extent-Objekt, das die Form der zu verarbeitenden Daten angibt, und eine Funktion, die auf jedes Element im Extent-Objekt angewendet werden soll. Im obigen Beispiel haben wir eine Lambda-Funktion verwendet, deren Unterstützung im ISO C++2011 (C++11)-Standard enthalten ist. Das Schlüsselwort „restrict“ (amp) weist den Compiler an, zu prüfen, ob der Funktionskörper auf der GPU ausgeführt werden kann, und deaktiviert die meisten C++-Syntaxen, die nicht in GPU-Anweisungen kompiliert werden können.

Lambda-Funktionsparameter, Index<1>Objekt, stellt einen eindimensionalen Index dar. Es muss mit dem verwendeten Extent-Objekt übereinstimmen. Wenn wir das Extent-Objekt als zweidimensional deklarieren würden (z. B. indem wir die Form der Quelldaten als zweidimensionale Matrix definieren), müsste der Index ebenfalls zwei sein -dimensional. Ein Beispiel für eine solche Situation ist unten aufgeführt.

Zum Schluss der Methodenaufruf synchronisieren() Am Ende der VectorAddExpPointwise-Methode wird sichergestellt, dass die von der GPU erzeugten Berechnungsergebnisse aus array_view avResult zurück in das Ergebnisarray kopiert werden.

Damit ist unsere erste Einführung in die Welt von C++ AMP abgeschlossen, und jetzt sind wir bereit für detailliertere Recherchen sowie weitere interessante Beispiele, die die Vorteile der Verwendung von parallelem Computing auf einer GPU demonstrieren. Die Vektoraddition ist kein guter Algorithmus und aufgrund des hohen Aufwands beim Kopieren von Daten nicht der beste Kandidat für die Demonstration der GPU-Nutzung. Im nächsten Unterabschnitt werden zwei weitere interessante Beispiele gezeigt.

Matrix-Multiplikation

Das erste „echte“ Beispiel, das wir uns ansehen werden, ist die Matrixmultiplikation. Zur Implementierung verwenden wir einen einfachen kubischen Matrixmultiplikationsalgorithmus und nicht den Strassen-Algorithmus, der eine Ausführungszeit nahe der kubischen ~O(n 2,807) hat. Gegeben zwei Matrizen, eine m x w-Matrix A und eine w x n-Matrix B, multipliziert das folgende Programm sie und gibt das Ergebnis zurück, eine m x n-Matrix C:

Void MatrixMultiply(int* A, int m, int w, int* B, int n, int* C) ( for (int i = 0; i< m; ++i) { for (int j = 0; j < n; ++j) { int sum = 0; for (int k = 0; k < w; ++k) { sum += A * B; } C = sum; } } }

Es gibt mehrere Möglichkeiten, diese Implementierung zu parallelisieren. Wenn Sie diesen Code so parallelisieren möchten, dass er auf einem regulären Prozessor ausgeführt wird, wäre die Parallelisierung der äußeren Schleife die richtige Wahl. Allerdings verfügt die GPU über eine relativ große Anzahl an Kernen, und wenn wir nur die äußere Schleife parallelisieren, können wir nicht genügend Jobs erstellen, um alle Kerne mit Arbeit zu belasten. Daher ist es sinnvoll, die beiden äußeren Schleifen zu parallelisieren und die innere Schleife unberührt zu lassen:

Void MatrixMultiply (int* A, int m, int w, int* B, int n, int* C) ( array_view avA(m, w, A); array_view avB(w, n, B); array_view avC(m, n, C); avC.discard_data(); parallel_for_each(avC.extent, [=](index<2>idx) strict(amp) ( int sum = 0; for (int k = 0; k< w; ++k) { sum + = avA(idx*w, k) * avB(k*w, idx); } avC = sum; }); }

Diese Implementierung ähnelt immer noch stark der sequentiellen Implementierung der Matrixmultiplikation und dem oben angegebenen Beispiel für die Vektoraddition, mit Ausnahme des Index, der jetzt zweidimensional ist und in der inneren Schleife mithilfe des Operators zugänglich ist. Wie viel schneller ist diese Version als die sequentielle Alternative, die auf einem normalen Prozessor läuft? Wenn man zwei Matrizen (Ganzzahlen) der Größe 1024 x 1024 multipliziert, benötigt die sequentielle Version auf einer normalen CPU durchschnittlich 7350 Millisekunden, während die GPU-Version – halten Sie sich fest – 50 Millisekunden benötigt, 147-mal schneller!

Partikelbewegungssimulation

Die oben vorgestellten Beispiele zur Lösung von Problemen auf der GPU verfügen über eine sehr einfache Implementierung der internen Schleife. Es ist klar, dass dies nicht immer der Fall sein wird. Der oben verlinkte Blog „Native Concurrency“ zeigt ein Beispiel für die Modellierung von Gravitationswechselwirkungen zwischen Partikeln. Die Simulation umfasst unendlich viele Schritte; Bei jedem Schritt werden für jedes Teilchen neue Werte der Elemente des Beschleunigungsvektors berechnet und anschließend deren neue Koordinaten bestimmt. Hier wird der Partikelvektor parallelisiert – mit einer ausreichend großen Anzahl von Partikeln (von mehreren Tausend und mehr) können Sie eine ausreichend große Anzahl von Aufgaben erstellen, um alle GPU-Kerne mit Arbeit zu belasten.

Grundlage des Algorithmus ist die Implementierung der Bestimmung des Ergebnisses von Wechselwirkungen zwischen zwei Partikeln, wie unten dargestellt, was sich leicht auf die GPU übertragen lässt:

// hier sind float4 Vektoren mit vier Elementen // die die an den Operationen beteiligten Partikel repräsentieren void bodybody_interaction (float4& Beschleunigung, const float4 p1, const float4 p2) strict(amp) ( float4 dist = p2 – p1; // wird hier nicht verwendet float absDist = dist.x*dist.x + dist.y*dist.y + dist.z*dist.z; float invDist = 1.0f / sqrt(absDist); float invDistCube = invDist*invDist*invDist; Beschleunigung + = dist*PARTICLE_MASS*invDistCube; )

Die Ausgangsdaten bei jedem Modellierungsschritt sind ein Array mit den Koordinaten und Geschwindigkeiten der Partikel. Als Ergebnis der Berechnungen wird ein neues Array mit den Koordinaten und Geschwindigkeiten der Partikel erstellt:

Struct-Partikel ( float4 Position, Geschwindigkeit; // Implementierungen von Konstruktor, Kopierkonstruktor und // Operator =, wobei restriktiv(amp) weggelassen wird, um Platz zu sparen ); void simulation_step(array & vorheriges Array & weiter, int-Körper) ( Umfang<1>ext(körper); parallel_for_each (ext, [&](index<1>idx) beschränken(amp) ( Partikel p = vorherige; float4 Beschleunigung(0, 0, 0, 0); for (int body = 0; body< bodies; ++body) { bodybody_interaction (acceleration, p.position, previous.position); } p.velocity + = acceleration*DELTA_TIME; p.position + = p.velocity*DELTA_TIME; next = p; }); }

Mit Hilfe einer entsprechenden grafischen Oberfläche kann die Modellierung sehr interessant sein. Das vollständige Beispiel des C++ AMP-Teams finden Sie im Native Concurrency-Blog. Auf meinem System mit einem Intel Core i7-Prozessor und einer Geforce GT 740M-Grafikkarte läuft die Simulation von 10.000 Partikeln mit ~2,5 fps (Schritten pro Sekunde) bei Verwendung der sequentiellen Version, die auf dem regulären Prozessor ausgeführt wird, und 160 fps, wenn die optimierte Version ausgeführt wird auf der GPU - eine enorme Leistungssteigerung.

Bevor wir diesen Abschnitt abschließen, gibt es noch eine weitere wichtige Funktion des C++ AMP-Frameworks, die die Leistung von Code, der auf der GPU ausgeführt wird, weiter verbessern kann. GPU-Unterstützung programmierbarer Datencache(oft angerufen geteilte Erinnerung). Die in diesem Cache gespeicherten Werte werden von allen Ausführungsthreads in einer einzelnen Kachel gemeinsam genutzt. Dank der Speicherkachelung können Programme, die auf dem C++ AMP-Framework basieren, Daten aus dem Grafikkartenspeicher in den gemeinsamen Speicher des Mosaiks lesen und dann von mehreren Ausführungsthreads darauf zugreifen, ohne die Daten erneut aus dem Grafikkartenspeicher abrufen zu müssen. Der Zugriff auf den gemeinsam genutzten Mosaikspeicher ist etwa zehnmal schneller als auf den Grafikkartenspeicher. Mit anderen Worten: Sie haben Gründe, weiterzulesen.

Um eine gekachelte Version der Parallelschleife bereitzustellen, wird die Methode parallel_for_each übergeben Domäne „tiled_extent“., der das mehrdimensionale Extent-Objekt in mehrdimensionale Kacheln unterteilt, und den lambda-Parameter „tiled_index“, der die globale und lokale ID des Threads innerhalb der Kachel angibt. Beispielsweise kann eine 16x16-Matrix in 2x2-Kacheln unterteilt werden (wie im Bild unten gezeigt) und dann an die Funktion parallel_for_each übergeben werden:

Ausmaß<2>Matrix(16,16); kachel_ausdehnung<2,2>TiledMatrix = Matrix.tile<2,2>(); parallel_for_each(tiledMatrix, [=](tiled_index<2,2>idx) strict(amp) ( // ... ));

Jeder der vier Ausführungsthreads, die zum selben Mosaik gehören, kann die im Block gespeicherten Daten gemeinsam nutzen.

Bei der Ausführung von Operationen mit Matrizen im GPU-Kern anstelle des Standardindex<2>, wie in den obigen Beispielen, können Sie verwenden idx.global. Die ordnungsgemäße Verwendung von lokalem Kachelspeicher und lokalen Indizes kann zu erheblichen Leistungssteigerungen führen. Um gekachelten Speicher zu deklarieren, der von allen Ausführungsthreads in einer einzelnen Kachel gemeinsam genutzt wird, können lokale Variablen mit dem Bezeichner „tile_static“ deklariert werden.

In der Praxis wird häufig die Technik verwendet, gemeinsam genutzten Speicher zu deklarieren und seine einzelnen Blöcke in verschiedenen Ausführungsthreads zu initialisieren:

Parallel_for_each(tiledMatrix, [=](tiled_index<2,2>idx) strict(amp) ( // 32 Bytes werden von allen Threads im Block gemeinsam genutzt Tile_static int local; // Weisen Sie dem Element für diesen Ausführungsthread einen Wert zu local = 42; ));

Offensichtlich können die Vorteile der Verwendung von Shared Memory nur dann erzielt werden, wenn der Zugriff auf diesen Speicher synchronisiert ist. Das heißt, Threads dürfen nicht auf den Speicher zugreifen, bis dieser von einem von ihnen initialisiert wurde. Die Synchronisierung von Threads in einem Mosaik erfolgt mithilfe von Objekten Tile_Barrier(erinnert an die Barrier-Klasse aus der TPL-Bibliothek) – Sie können die Ausführung erst fortsetzen, nachdem die Methode „tile_barrier.Wait()“ aufgerufen wurde, die die Kontrolle erst zurückgibt, wenn alle Threads „tile_barrier.Wait“ aufgerufen haben. Zum Beispiel:

Parallel_for_each(tiledMatrix, (tiled_index<2,2>idx) strict(amp) ( // 32 Bytes werden von allen Threads im Block gemeinsam genutzt Tile_static int local; // Weisen Sie dem Element für diesen Ausführungsthread einen Wert zu local = 42; // idx.barrier ist eine Instanz von Tile_barrier idx.barrier.wait(); // Jetzt kann dieser Thread auf das „lokale“ Array zugreifen // unter Verwendung der Indizes anderer Ausführungsthreads! ));

Jetzt ist es an der Zeit, das Gelernte in ein konkretes Beispiel umzusetzen. Kehren wir zur Implementierung der Matrixmultiplikation zurück, die ohne die Verwendung einer Kachelspeicherorganisation durchgeführt wird, und fügen wir die beschriebene Optimierung hinzu. Nehmen wir an, dass die Matrixgröße ein Vielfaches von 256 ist – das ermöglicht uns, mit 16 x 16 Blöcken zu arbeiten. Die Natur von Matrizen ermöglicht eine blockweise Multiplikation, und wir können diese Funktion nutzen (eigentlich Division). Die Umwandlung von Matrizen in Blöcke ist eine typische Optimierung des Matrixmultiplikationsalgorithmus und sorgt für eine effizientere CPU-Cache-Nutzung.

Das Wesentliche dieser Technik ist Folgendes. Um C i,j (das Element in Zeile i und Spalte j in der Ergebnismatrix) zu finden, müssen Sie das Skalarprodukt zwischen A i,* (i-te Zeile der ersten Matrix) und B *,j (j) berechnen -te Spalte in der zweiten Matrix). Dies entspricht jedoch der Berechnung der Partialskalarprodukte der Zeile und Spalte und der anschließenden Summierung der Ergebnisse. Wir können diese Tatsache nutzen, um den Matrixmultiplikationsalgorithmus in eine Kachelversion umzuwandeln:

Void MatrixMultiply(int* A, int m, int w, int* B, int n, int* C) ( array_view avA(m, w, A); array_view avB(w, n, B); array_view avC(m, n, C); avC.discard_data(); parallel_for_each(avC.extent.tile<16,16>(), [=](tiled_index<16,16>idx) strict(amp) ( int sum = 0; int localRow = idx.local, localCol = idx.local; for (int k = 0; k

Der Kern der beschriebenen Optimierung besteht darin, dass jeder Thread im Mosaik (256 Threads werden für einen 16 x 16-Block erstellt) sein Element in 16 x 16 lokalen Kopien von Fragmenten der Originalmatrizen A und B initialisiert. Jeder Thread im Mosaik erfordert nur eine Zeile und eine Spalte dieser Blöcke, aber alle Threads zusammen greifen 16 Mal auf jede Zeile und jede Spalte zu. Dieser Ansatz reduziert die Anzahl der Zugriffe auf den Hauptspeicher erheblich.

Um das Element (i,j) in der Ergebnismatrix zu berechnen, benötigt der Algorithmus die vollständige i-te Zeile der ersten Matrix und die j-te Spalte der zweiten Matrix. Wenn die Threads im Diagramm als 16x16-Kacheln dargestellt sind und k=0 ist, werden die schattierten Bereiche in der ersten und zweiten Matrize in den gemeinsamen Speicher eingelesen. Das Berechnungselement (i,j) des Ausführungsthreads in der Ergebnismatrix berechnet das Teilskalarprodukt der ersten k Elemente aus der i-ten Zeile und der j-ten Spalte der ursprünglichen Matrizen.

In diesem Beispiel führt die Verwendung einer gekachelten Organisation zu einer enormen Leistungssteigerung. Die gekachelte Version der Matrixmultiplikation ist viel schneller als die einfache Version und dauert etwa 17 Millisekunden (für die gleichen 1024 x 1024 Eingabematrizen), was 430-mal schneller ist als die Version, die auf einem herkömmlichen Prozessor läuft!

Bevor wir unsere Diskussion über das C++ AMP-Framework beenden, möchten wir die Tools (in Visual Studio) erwähnen, die Entwicklern zur Verfügung stehen. Visual Studio 2012 bietet einen GPU-Debugger (Graphics Processing Unit), mit dem Sie Haltepunkte festlegen, den Aufrufstapel untersuchen und lokale Variablenwerte lesen und ändern können (einige Beschleuniger unterstützen das GPU-Debugging direkt; für andere verwendet Visual Studio einen Softwaresimulator). und einen Profiler, mit dem Sie die Vorteile bewerten können, die eine Anwendung durch die Parallelisierung von Vorgängen mithilfe einer GPU erhält. Weitere Informationen zu Debugfunktionen in Visual Studio finden Sie im Walkthrough-Artikel. Debuggen einer C++ AMP-Anwendung“ auf MSDN.

GPU-Computing-Alternativen in .NET

Bisher wurden in diesem Artikel nur Beispiele in C++ gezeigt, es gibt jedoch mehrere Möglichkeiten, die Leistung der GPU in verwalteten Anwendungen zu nutzen. Eine Möglichkeit besteht darin, Interop-Tools zu verwenden, mit denen Sie die Arbeit mit GPU-Kernen auf C++-Komponenten auf niedriger Ebene verlagern können. Diese Lösung eignet sich hervorragend für diejenigen, die das C++ AMP-Framework verwenden möchten oder die Möglichkeit haben, vorgefertigte C++ AMP-Komponenten in verwalteten Anwendungen zu verwenden.

Eine andere Möglichkeit besteht darin, eine Bibliothek zu verwenden, die über verwalteten Code direkt mit der GPU zusammenarbeitet. Derzeit gibt es mehrere solcher Bibliotheken. Zum Beispiel GPU.NET und CUDAfy.NET (beides kommerzielle Angebote). Unten finden Sie ein Beispiel aus dem GPU.NET GitHub-Repository, das die Implementierung des Skalarprodukts zweier Vektoren demonstriert:

Öffentliche statische Leere MultiplyAddGpu(double a, double b, double c) ( int ThreadId = BlockDimension.X * BlockIndex.X + ThreadIndex.X; int TotalThreads = BlockDimension.X * GridDimension.X; for (int ElementIdx = ThreadId; ElementIdx

Ich bin der Meinung, dass es viel einfacher und effizienter ist, eine Spracherweiterung (basierend auf C++ AMP) zu erlernen, als zu versuchen, Interaktionen auf Bibliotheksebene zu orchestrieren oder wesentliche Änderungen an der IL-Sprache vorzunehmen.

Nachdem wir uns also die Möglichkeiten der parallelen Programmierung in .NET und der Verwendung der GPU angesehen haben, zweifelt niemand daran, dass die Organisation paralleler Datenverarbeitung eine wichtige Möglichkeit zur Steigerung der Produktivität ist. Auf vielen Servern und Workstations auf der ganzen Welt bleibt die unschätzbare Rechenleistung von CPUs und GPUs ungenutzt, weil Anwendungen sie einfach nicht nutzen.

Die Task Parallel Library bietet uns die einzigartige Möglichkeit, alle verfügbaren CPU-Kerne einzubeziehen, obwohl dies die Lösung einiger interessanter Probleme der Synchronisierung, übermäßiger Aufgabenfragmentierung und ungleicher Arbeitsverteilung zwischen Ausführungsthreads erfordern wird.

Das C++ AMP-Framework und andere Mehrzweck-GPU-Parallel-Computing-Bibliotheken können erfolgreich verwendet werden, um Berechnungen über Hunderte von GPU-Kernen hinweg zu parallelisieren. Schließlich gibt es eine bisher unerschlossene Möglichkeit, Produktivitätssteigerungen durch den Einsatz von Cloud-Distributed-Computing-Technologien zu erzielen, die in letzter Zeit zu einer der Hauptrichtungen in der Entwicklung der Informationstechnologie geworden sind.

Merkmale der AMD/ATI Radeon-Architektur

Dies ähnelt der Entstehung neuer biologischer Arten, wenn sich Lebewesen im Zuge der Entwicklung von Lebensräumen weiterentwickeln, um ihre Anpassungsfähigkeit an die Umwelt zu verbessern. Ebenso entwickelte die GPU, beginnend mit der Beschleunigung der Rasterung und Texturierung von Dreiecken, zusätzliche Fähigkeiten zum Ausführen von Shader-Programmen zum Färben dieser Dreiecke. Und auch im nicht-grafischen Computing sind diese Fähigkeiten gefragt, wo sie teilweise deutliche Leistungssteigerungen im Vergleich zu herkömmlichen Lösungen ermöglichen.

Lassen Sie uns weitere Analogien ziehen: Nach einer langen Entwicklung an Land drangen Säugetiere ins Meer vor, wo sie gewöhnliche Meeresbewohner verdrängten. Im Wettbewerb nutzten Säugetiere sowohl neue fortgeschrittene Fähigkeiten, die auf der Erdoberfläche auftauchten, als auch solche, die speziell für die Anpassung an das Leben im Wasser erworben wurden. Ebenso fügen GPUs, basierend auf den Stärken der Architektur für 3D-Grafiken, zunehmend spezielle Funktionen hinzu, die für nichtgrafische Aufgaben nützlich sind.

Was ermöglicht es GPUs also, ihren eigenen Sektor im Bereich der Allzwecksoftware zu beanspruchen? Die GPU-Mikroarchitektur ist völlig anders aufgebaut als die herkömmlicher CPUs und bringt von Natur aus gewisse Vorteile mit sich. Grafikaufgaben erfordern eine unabhängige Parallelverarbeitung und die GPU ist nativ multithreaded. Aber diese Parallelität macht ihm nur Freude. Die Mikroarchitektur ist darauf ausgelegt, die große Anzahl verfügbarer Threads auszunutzen, die ausgeführt werden müssen.

Die GPU besteht aus mehreren Dutzend (30 für Nvidia GT200, 20 für Evergreen, 16 für Fermi) Prozessorkernen, die in der Nvidia-Terminologie als Streaming Multiprocessor und in der ATI-Terminologie als SIMD Engine bezeichnet werden. Für die Zwecke dieses Artikels nennen wir sie Miniprozessoren, da sie mehrere hundert Programmthreads ausführen und fast alles können, was eine normale CPU kann, aber immer noch nicht alles.

Marketingnamen sind verwirrend – was noch wichtiger ist, sie geben die Anzahl der Funktionsmodule an, die subtrahieren und multiplizieren können: zum Beispiel 320 Vektor-„Kerne“. Diese Kerne ähneln eher Getreide. Es ist besser, sich die GPU als eine Art Multi-Core-Prozessor mit einer großen Anzahl von Kernen vorzustellen, die viele Threads gleichzeitig ausführen.

Jeder Miniprozessor verfügt über lokalen Speicher, 16 KB für GT200, 32 KB für Evergreen und 64 KB für Fermi (im Wesentlichen ein programmierbarer L1-Cache). Seine Zugriffszeit ähnelt der des First-Level-Cache einer herkömmlichen CPU und er führt ähnliche Funktionen für die schnellste Übermittlung von Daten an Funktionsmodule aus. In der Fermi-Architektur kann ein Teil des lokalen Speichers als regulärer Cache konfiguriert werden. In einer GPU wird lokaler Speicher für den schnellen Datenaustausch zwischen ausführenden Threads verwendet. Eines der üblichen Schemata eines GPU-Programms ist wie folgt: Zunächst werden Daten aus dem globalen Speicher der GPU in den lokalen Speicher geladen. Hierbei handelt es sich um einen gewöhnlichen Videospeicher, der (wie der Systemspeicher) getrennt von „seinem“ Prozessor liegt – im Fall von Video ist er durch mehrere Chips auf der Platine der Grafikkarte verlötet. Anschließend arbeiten mehrere hundert Threads mit diesen Daten im lokalen Speicher und schreiben das Ergebnis in den globalen Speicher, um es anschließend an die CPU zu übertragen. Es liegt in der Verantwortung des Programmierers, Anweisungen zum Laden und Entladen von Daten aus dem lokalen Speicher zu schreiben. Im Wesentlichen handelt es sich dabei um die Partitionierung der Daten [einer bestimmten Aufgabe] zur parallelen Verarbeitung. Die GPU unterstützt auch atomare Schreib-/Leseanweisungen in den Speicher, diese sind jedoch wirkungslos und werden normalerweise in der Endphase benötigt, um die Berechnungsergebnisse aller Miniprozessoren „zusammenzukleben“.

Der lokale Speicher ist allen im Miniprozessor ausgeführten Threads gemeinsam, daher wird er beispielsweise in der Nvidia-Terminologie sogar als gemeinsam genutzt bezeichnet, und der Begriff lokaler Speicher bezeichnet das genaue Gegenteil, nämlich: einen bestimmten persönlichen Bereich eines separaten Threads im globalen Gedächtnis, nur für ihn sichtbar und zugänglich. Doch neben dem lokalen Speicher verfügt der Miniprozessor über einen weiteren Speicherbereich, der in allen Architekturen etwa viermal so groß ist. Es wird zu gleichen Teilen auf alle ausführenden Threads aufgeteilt; dabei handelt es sich um Register zur Speicherung von Variablen und Berechnungszwischenergebnissen. Jeder Thread verfügt über mehrere Dutzend Register. Die genaue Anzahl hängt davon ab, wie viele Threads der Miniprozessor ausführt. Diese Zahl ist sehr wichtig, da die Latenz des globalen Speichers sehr hoch ist, Hunderte von Zyklen, und ohne Caches keine Möglichkeit besteht, Zwischenergebnisse von Berechnungen zu speichern.

Und noch ein wichtiges Merkmal der GPU: „sanfte“ Vektorisierung. Jeder Miniprozessor verfügt über eine große Anzahl von Rechenmodulen (8 für GT200, 16 für Radeon und 32 für Fermi), aber sie können alle nur denselben Befehl mit derselben Programmadresse ausführen. In diesem Fall können die Operanden unterschiedlich sein, verschiedene Threads haben ihre eigenen. Zum Beispiel Anweisungen Addieren Sie den Inhalt von zwei Registern: Es wird von allen Computergeräten gleichzeitig ausgeführt, die Register werden jedoch unterschiedlich verwendet. Es wird davon ausgegangen, dass sich alle Threads des GPU-Programms, die eine parallele Datenverarbeitung durchführen, grundsätzlich parallel durch den Programmcode bewegen. Somit werden alle Rechenmodule gleichmäßig belastet. Und wenn die Threads aufgrund von Verzweigungen im Programm in ihrem Code-Ausführungspfad voneinander abweichen, kommt es zur sogenannten Serialisierung. Dann werden nicht alle Rechenmodule genutzt, da die Threads verschiedene Anweisungen zur Ausführung übermitteln und ein Block von Rechenmodulen, wie bereits gesagt, nur eine Anweisung mit einer Adresse ausführen kann. Und natürlich sinkt die Produktivität im Verhältnis zum Maximum.

Der Vorteil besteht darin, dass die Vektorisierung vollständig automatisch erfolgt und keine Programmierung mit SSE, MMX usw. erforderlich ist. Und die GPU selbst kümmert sich um die Abweichungen. Theoretisch können Sie im Allgemeinen Programme für die GPU schreiben, ohne über die Vektornatur der Ausführungsmodule nachzudenken, aber die Geschwindigkeit eines solchen Programms wird nicht sehr hoch sein. Der Nachteil ist die große Breite des Vektors. Sie ist größer als die nominelle Anzahl funktionaler Module und beträgt 32 für Nvidia-GPUs und 64 für Radeon. Die Threads werden in Blöcken entsprechender Größe verarbeitet. Nvidia nennt diesen Thread-Block den Begriff Warp, AMD nennt ihn Wellenfront, was dasselbe ist. Somit wird auf 16 Rechengeräten eine „Wellenfront“ mit einer Länge von 64 Threads in vier Taktzyklen verarbeitet (bei üblicher Befehlslänge). Der Autor bevorzugt in diesem Fall den Begriff Warp, da er mit dem nautischen Begriff Warp in Verbindung gebracht wird, der ein aus gedrehten Tauen geknüpftes Seil bedeutet. Die Fäden „verdrehen“ sich also und bilden ein festes Bündel. „Wellenfront“ kann aber auch mit dem Meer in Verbindung gebracht werden: Anweisungen gelangen auf die gleiche Weise zu den Aktoren, wie Wellen nacheinander ans Ufer rollen.

Wenn alle Threads in der Programmausführung gleich weit fortgeschritten sind (sich an derselben Stelle befinden) und somit dieselbe Anweisung ausführen, ist alles in Ordnung, wenn nicht, kommt es zu einer Verlangsamung. In diesem Fall befinden sich Threads von einer Warp- oder Wellenfront an verschiedenen Stellen im Programm; sie werden in Gruppen von Threads unterteilt, die denselben Anweisungsnummernwert (mit anderen Worten Anweisungszeiger) haben. Und es werden immer nur noch die Threads einer Gruppe gleichzeitig ausgeführt – alle führen die gleiche Anweisung aus, aber mit unterschiedlichen Operanden. Infolgedessen läuft Warp um ein Vielfaches langsamer als die Anzahl der Gruppen, in die es unterteilt ist, und die Anzahl der Threads in der Gruppe spielt keine Rolle. Selbst wenn die Gruppe nur aus einem Thread besteht, dauert die Ausführung genauso lange wie die eines vollständigen Warps. In der Hardware wird dies durch die Maskierung bestimmter Threads implementiert, d. h. Anweisungen werden formal ausgeführt, die Ergebnisse ihrer Ausführung werden jedoch nirgendwo aufgezeichnet und in Zukunft nicht verwendet.

Obwohl jeder Miniprozessor (Streaming MultiProcessor oder SIMD Engine) zu jedem Zeitpunkt Anweisungen ausführt, die nur zu einem Warp (einem Bündel von Threads) gehören, verfügt er über mehrere Dutzend aktive Warps im Ausführungspool. Nachdem er die Anweisungen eines Warps ausgeführt hat, führt der Miniprozessor nicht die nächste Anweisung der Threads dieses Warps aus, sondern die Anweisungen eines anderen Warps. Dieser Warp kann sich an einer völlig anderen Stelle im Programm befinden. Dies hat keinen Einfluss auf die Geschwindigkeit, da nur innerhalb des Warps die Anweisungen aller Threads für die Ausführung mit voller Geschwindigkeit gleich sein müssen.

In diesem Fall verfügt jede der 20 SIMD Engines über vier aktive Wellenfronten mit jeweils 64 Threads. Jeder Thread ist durch einen kurzen Strich gekennzeichnet. Gesamt: 64×4×20=5120 Fäden

Da jede Warp- oder Wellenfront aus 32–64 Threads besteht, verfügt der Miniprozessor über mehrere hundert aktive Threads, die fast gleichzeitig ausgeführt werden. Im Folgenden werden wir sehen, welche architektonischen Vorteile eine so große Anzahl paralleler Threads verspricht. Zunächst betrachten wir jedoch, welche Einschränkungen die Miniprozessoren haben, aus denen die GPU besteht.

Die Hauptsache ist, dass die GPU keinen Stack hat, auf dem Funktionsparameter und lokale Variablen gespeichert werden könnten. Aufgrund der großen Anzahl an Threads ist auf dem Chip einfach kein Platz für den Stack. Da die GPU gleichzeitig etwa 10.000 Threads ausführt und die Stapelgröße eines Threads 100 KB beträgt, beträgt das Gesamtvolumen tatsächlich 1 GB, was der Standardgröße des gesamten Videospeichers entspricht. Darüber hinaus gibt es keine Möglichkeit, einen Stapel nennenswerter Größe im GPU-Kern selbst zu platzieren. Wenn Sie beispielsweise 1000 Bytes Stapel auf einen Thread legen, würde nur ein Miniprozessor 1 MB Speicher benötigen, was fast dem Fünffachen der kombinierten Menge an lokalem Speicher des Miniprozessors und dem für die Speicherung von Registern zugewiesenen Speicher entspricht.

Daher gibt es in einem GPU-Programm keine Rekursion und es gibt nicht viel zu tun mit Funktionsaufrufen. Alle Funktionen werden beim Kompilieren des Programms direkt in den Code eingefügt. Dies beschränkt den Umfang von GPU-Anwendungen auf rechnerische Aufgaben. Manchmal ist es möglich, eine begrenzte Stapelemulation unter Verwendung des globalen Speichers für Rekursionsalgorithmen mit bekannten kleinen Iterationstiefen zu verwenden, dies ist jedoch keine typische GPU-Anwendung. Dazu ist es notwendig, einen Algorithmus speziell zu entwickeln und die Möglichkeit seiner Umsetzung zu prüfen, ohne eine erfolgreiche Beschleunigung gegenüber der CPU zu gewährleisten.

Fermi führte zum ersten Mal die Möglichkeit ein, virtuelle Funktionen zu verwenden, aber auch hier ist ihre Verwendung durch das Fehlen eines großen, schnellen Caches für jeden Thread eingeschränkt. 1536 Threads machen 48 KB oder 16 KB L1 aus, d. h. virtuelle Funktionen in einem Programm können relativ selten verwendet werden, da der Stack sonst auch langsamen globalen Speicher belegt, was die Ausführung verlangsamt und höchstwahrscheinlich keine Vorteile bringt im Vergleich zur CPU-Version.

Somit wird die GPU als Rechen-Coprozessor dargestellt, in den Daten geladen, von einem Algorithmus verarbeitet und das Ergebnis erzeugt wird.

Vorteile für die Architektur

Aber es berechnet die GPU sehr schnell. Und sein hohes Multithreading hilft ihm dabei. Eine große Anzahl aktiver Threads ermöglicht es, die hohe Latenz des separat angeordneten globalen Videospeichers, die etwa 500 Taktzyklen beträgt, teilweise auszublenden. Besonders gut nivelliert es sich bei Code mit einer hohen Dichte an Rechenoperationen. Daher ist die Transistor-intensive L1-L2-L3-Cache-Hierarchie nicht erforderlich. Stattdessen können mehrere Rechenmodule auf dem Chip platziert werden, was eine hervorragende Rechenleistung bietet. Während die Anweisungen eines Threads oder Warps ausgeführt werden, warten die verbleibenden Hunderte von Threads stillschweigend auf ihre Daten.

Fermi hat einen etwa 1 MB großen L2-Cache eingeführt, der allerdings nicht mit den Caches moderner Prozessoren zu vergleichen ist, sondern eher für die Kommunikation zwischen Kernen und diverse Software-Tricks gedacht ist. Wenn seine Größe auf alle Zehntausende Threads aufgeteilt wird, hat jeder Thread ein sehr vernachlässigbares Volumen.

Doch zusätzlich zur globalen Speicherlatenz gibt es in einem Computergerät noch viele weitere Latenzen, die ausgeblendet werden müssen. Dabei handelt es sich um die Latenz der On-Chip-Datenübertragung von Computergeräten zum First-Level-Cache, also dem lokalen Speicher der GPU, und zu den Registern sowie dem Befehlscache. Die Registerdatei sowie der lokale Speicher befinden sich getrennt von den Funktionsmodulen und die Zugriffsgeschwindigkeit auf sie beträgt etwa eineinhalb Dutzend Zyklen. Und wiederum kann eine große Anzahl von Threads, aktiven Warps, diese Latenz effektiv verbergen. Darüber hinaus ist die gesamte Zugriffsbandbreite (Bandbreite) auf den lokalen Speicher der gesamten GPU unter Berücksichtigung der Anzahl der Miniprozessoren, aus denen sie besteht, deutlich größer als die Zugriffsbandbreite auf den First-Level-Cache moderner CPUs. Die GPU kann deutlich mehr Daten pro Zeiteinheit verarbeiten.

Wir können sofort sagen, dass die GPU, wenn sie nicht mit einer großen Anzahl paralleler Threads ausgestattet ist, nahezu keine Leistung aufweist, da sie mit der gleichen Geschwindigkeit wie bei voller Auslastung arbeitet und viel weniger Arbeit leistet. Wenn es beispielsweise nur einen Thread statt 10.000 gibt: Die Leistung sinkt um etwa das Tausendfache, da nicht nur nicht alle Blöcke geladen werden, sondern auch alle Latenzen betroffen sind.

Das Problem, Latenzen zu verbergen, ist auch bei modernen Hochfrequenz-CPUs akut; zu seiner Beseitigung werden ausgefeilte Methoden eingesetzt – Deep Pipelining, Ausführung von Anweisungen außerhalb der Reihenfolge. Dies erfordert komplexe Befehlsplaner, verschiedene Puffer usw., die Platz auf dem Chip beanspruchen. Dies alles ist für die beste Single-Threaded-Leistung erforderlich.

Für die GPU ist dies alles jedoch nicht erforderlich; sie ist architektonisch schneller für Rechenaufgaben mit einer großen Anzahl von Threads. Aber es verwandelt Multithreading in Leistung, so wie der Stein der Weisen Blei in Gold verwandelt.

Die GPU wurde ursprünglich für die optimale Ausführung von Shader-Programmen für Dreieckpixel konzipiert, die offensichtlich unabhängig sind und parallel ausgeführt werden können. Und von diesem Zustand aus hat es sich durch das Hinzufügen verschiedener Funktionen (lokaler Speicher und adressierbarer Zugriff auf den Videospeicher sowie die Komplizierung des Befehlssatzes) zu einem sehr leistungsstarken Computergerät entwickelt, das immer noch nur für Algorithmen effektiv genutzt werden kann, die eine hochparallele Implementierung ermöglichen Verwendung einer begrenzten Menge an lokalem Speicher. Speicher.

Beispiel

Eines der klassischsten Probleme der GPU ist die Berechnung der Wechselwirkung von N Körpern, die ein Gravitationsfeld erzeugen. Aber wenn wir zum Beispiel die Entwicklung des Systems Erde-Mond-Sonne berechnen müssen, dann ist die GPU eine schlechte Hilfe für uns: Es gibt nur wenige Objekte. Für jedes Objekt müssen die Interaktionen mit allen anderen Objekten berechnet werden, und es gibt nur zwei davon. Bei der Bewegung des Sonnensystems mit allen Planeten und ihren Monden (etwa ein paar hundert Objekte) ist die GPU immer noch nicht sehr effizient. Allerdings wird ein Multicore-Prozessor aufgrund des hohen Overheads des Thread-Managements auch nicht seine volle Leistung entfalten können und im Single-Threaded-Modus arbeiten. Wenn Sie aber auch die Flugbahnen von Kometen und Asteroidengürtelobjekten berechnen müssen, ist dies bereits eine Aufgabe für die GPU, da genügend Objekte vorhanden sind, um die erforderliche Anzahl paralleler Berechnungsthreads zu erstellen.

Die GPU leistet auch dann gute Dienste, wenn Sie die Kollision von Kugelsternhaufen aus Hunderttausenden Sternen berechnen müssen.

Eine weitere Möglichkeit, die GPU-Leistung bei einem N-Körper-Problem zu nutzen, ergibt sich, wenn Sie viele einzelne Probleme berechnen müssen, wenn auch mit einer kleinen Anzahl von Körpern. Wenn Sie beispielsweise Optionen für die Entwicklung eines Systems für verschiedene Optionen für Anfangsgeschwindigkeiten berechnen müssen. Dann können Sie die GPU problemlos effektiv nutzen.

Details zur AMD Radeon-Mikroarchitektur

Wir haben uns die Grundprinzipien der GPU-Organisation angesehen; sie sind Videobeschleunigern aller Hersteller gemeinsam, da sie ursprünglich eine Zielaufgabe hatten – Shader-Programme. Allerdings haben die Hersteller eine Möglichkeit gefunden, hinsichtlich der Details der Mikroarchitektur-Implementierung unterschiedlicher Meinung zu sein. Obwohl CPUs verschiedener Hersteller teilweise sehr unterschiedlich sind, auch wenn sie kompatibel sind, wie zum Beispiel Pentium 4 und Athlon oder Core. Die Nvidia-Architektur ist bereits recht weithin bekannt, nun werfen wir einen Blick auf Radeon und beleuchten die wesentlichen Unterschiede in den Ansätzen dieser Anbieter.

AMD-Grafikkarten erhielten volle Unterstützung für Allzweck-Computing, beginnend mit der Evergreen-Familie, die erstmals auch DirectX 11-Spezifikationen implementierte. Karten der 47xx-Familie weisen eine Reihe erheblicher Einschränkungen auf, die im Folgenden erläutert werden.

Die Unterschiede in der Größe des lokalen Speichers (32 KB bei Radeon gegenüber 16 KB bei GT200 und 64 KB bei Fermi) sind im Allgemeinen nicht signifikant. Sowie die Wellenfrontgröße von 64 Threads für AMD gegenüber 32 Threads im Warp für Nvidia. Nahezu jedes GPU-Programm lässt sich problemlos umkonfigurieren und an diese Parameter anpassen. Die Leistung kann sich um mehrere zehn Prozent ändern, aber im Fall einer GPU ist das nicht so wichtig, da ein GPU-Programm normalerweise zehnmal langsamer als sein CPU-Pendant oder zehnmal schneller läuft oder überhaupt nicht funktioniert.

Wichtiger ist die Verwendung der VLIW-Technologie (Very Long Instruction Word) durch AMD. Nvidia verwendet skalare einfache Anweisungen, die auf skalaren Registern arbeiten. Seine Beschleuniger implementieren einfaches klassisches RISC. AMD-Grafikkarten verfügen über die gleiche Anzahl an Registern wie die GT200, die Register sind jedoch 128-Bit-Vektorregister. Jeder VLIW-Befehl arbeitet mit mehreren 32-Bit-Registern mit vier Komponenten, was SSE ähnelt, VLIW jedoch über viel mehr Funktionen verfügt. Dies ist kein SIMD (Single Instruction Multiple Data) wie SSE – hier können die Anweisungen für jedes Operandenpaar unterschiedlich und sogar abhängig sein! Die Komponenten des Registers A heißen beispielsweise a1, a2, a3, a4; Register B ist ähnlich. Kann mit einem einzelnen Befehl berechnet werden, der in einem Taktzyklus ausgeführt wird, zum Beispiel die Zahl a1×b1+a2×b2+a3×b3+a4×b4 oder ein zweidimensionaler Vektor (a1×b1+a2×b2, a3). ×b3+a4×b4 ).

Möglich wurde dies durch die geringere Taktung der GPU im Vergleich zur CPU und den starken Rückgang der Prozesstechnik in den letzten Jahren. In diesem Fall ist kein Scheduler erforderlich, fast alles wird in einem Taktzyklus ausgeführt.

Dank Vektoranweisungen ist die Spitzenleistung von Radeon bei einfacher Präzision sehr hoch und erreicht Teraflops.

Ein Vektorregister kann eine Zahl mit doppelter Genauigkeit anstelle von vier Zahlen mit einfacher Genauigkeit speichern. Und eine VLIW-Anweisung kann entweder zwei Paare doppelter Zahlen addieren oder zwei Zahlen multiplizieren oder zwei Zahlen multiplizieren und mit einer dritten addieren. Somit ist die Spitzenleistung im Double etwa fünfmal niedriger als im Float. Bei älteren Radeon-Modellen entspricht sie der Leistung von Nvidia Tesla auf der neuen Fermi-Architektur und liegt deutlich über der Leistung von Doppelkarten auf der GT200-Architektur. Bei Fermi-basierten Geforce-Consumer-Grafikkarten wurde die maximale Geschwindigkeit von Doppelberechnungen um das Vierfache reduziert.


Schematische Darstellung des Radeon-Betriebs. Es wird nur einer von 20 parallel laufenden Miniprozessoren vorgestellt

GPU-Hersteller sind im Gegensatz zu CPU-Herstellern (hauptsächlich x86-kompatible) nicht an Kompatibilitätsprobleme gebunden. Ein GPU-Programm wird zunächst in einen Zwischencode kompiliert. Wenn das Programm ausgeführt wird, kompiliert der Treiber diesen Code in modellspezifische Maschinenanweisungen. Wie oben beschrieben, haben GPU-Hersteller dies ausgenutzt, indem sie eine praktische ISA (Instruction Set Architecture) für ihre GPUs entwickelt und diese von Generation zu Generation geändert haben. Auf jeden Fall führte dies aufgrund des (unnötigen) Fehlens eines Decoders zu einem gewissen Leistungszuwachs. Aber AMD ging noch einen Schritt weiter und entwickelte ein eigenes Format zum Anordnen von Anweisungen im Maschinencode. Sie sind nicht sequentiell (gemäß Programmauflistung) geordnet, sondern abschnittsweise.

Zuerst kommt der Abschnitt mit bedingten Verzweigungsanweisungen, der Links zu Abschnitten mit kontinuierlichen arithmetischen Anweisungen enthält, die den verschiedenen Verzweigungszweigen entsprechen. Sie werden VLIW-Bundles genannt. Diese Abschnitte enthalten ausschließlich arithmetische Anweisungen mit Daten aus Registern oder dem lokalen Speicher. Diese Organisation vereinfacht die Verwaltung des Befehlsflusses und deren Übermittlung an ausführende Geräte. Dies ist umso nützlicher, da VLIW-Anweisungen relativ groß sind. Es gibt auch Abschnitte mit Anweisungen zum Speicherzugriff.

Abschnitte mit bedingten Sprunganweisungen
Abschnitt 0Zweig 0Link zu Abschnitt 3 der Anweisungen zum kontinuierlichen Rechnen
Abschnitt 1Zweig 1Link zu Abschnitt Nr. 4
Sektion 2Zweig 2Link zu Abschnitt Nr. 5
Kontinuierliche Arithmetik-Anweisungsabschnitte
Sektion 3VLIW-Anweisung 0VLIW-Anweisung 1VLIW-Anweisung 2VLIW-Anweisung 3
Sektion 4VLIW-Anweisung 4VLIW-Anweisung 5
Abschnitt 5VLIW-Anweisung 6VLIW-Anweisung 7VLIW-Anweisung 8VLIW-Anweisung 9

GPUs von Nvidia und AMD verfügen außerdem über integrierte Anweisungen zur schnellen Berechnung grundlegender mathematischer Funktionen, Quadratwurzel, Exponent, Logarithmen, Sinus und Kosinus für Zahlen mit einfacher Genauigkeit in wenigen Taktzyklen. Hierfür gibt es spezielle Recheneinheiten. Sie „entstehen“ aus der Notwendigkeit, eine schnelle Approximation dieser Funktionen in Geometrie-Shadern zu implementieren.

Selbst wenn jemand nicht wüsste, dass GPUs für Grafiken verwendet werden, und nur die technischen Eigenschaften gelesen hat, könnte er anhand dieses Zeichens vermuten, dass diese Rechen-Coprozessoren von Videobeschleunigern stammen. Ebenso erkannten Wissenschaftler anhand bestimmter Merkmale von Meeressäugern, dass ihre Vorfahren Landlebewesen waren.

Ein offensichtlicheres Merkmal, das den grafischen Ursprung des Geräts verrät, sind jedoch die 2D- und 3D-Texturleseeinheiten mit Unterstützung für bilineare Interpolation. Sie werden häufig in GPU-Programmen verwendet, da sie das Lesen schreibgeschützter Datenarrays beschleunigen und vereinfachen. Eines der Standardverhalten einer GPU-Anwendung besteht darin, Arrays mit Quelldaten zu lesen, sie in den Rechenkernen zu verarbeiten und das Ergebnis in ein anderes Array zu schreiben, das dann zurück an die CPU übertragen wird. Dieses Schema ist Standard und üblich, da es für die GPU-Architektur praktisch ist. Aufgaben, die intensive Lese- und Schreibvorgänge in einem großen Bereich des globalen Speichers erfordern und somit Datenabhängigkeiten enthalten, lassen sich auf der GPU nur schwer parallelisieren und effizient implementieren. Außerdem hängt ihre Leistung stark von der Latenz des globalen Speichers ab, die sehr hoch ist. Wenn die Aufgabe jedoch durch das Muster „Daten lesen – verarbeiten – Ergebnis schreiben“ beschrieben wird, können Sie durch die Ausführung auf der GPU mit ziemlicher Sicherheit einen großen Schub erzielen.

Für Texturdaten in der GPU gibt es eine separate Hierarchie kleiner Caches der ersten und zweiten Ebene. Dies sorgt für eine Beschleunigung bei der Verwendung von Texturen. Diese Hierarchie erschien ursprünglich in GPUs, um die Lokalität des Zugriffs auf Texturen zu nutzen: Offensichtlich benötigt nach der Verarbeitung eines Pixels ein benachbartes Pixel (mit hoher Wahrscheinlichkeit) nahegelegene Texturdaten. Viele Algorithmen für herkömmliche Berechnungen weisen jedoch eine ähnliche Art des Datenzugriffs auf. Daher werden Textur-Caches aus Grafiken sehr nützlich sein.

Obwohl die Größe der L1-L2-Caches in Nvidia- und AMD-Karten ungefähr gleich ist, was offensichtlich auf die Anforderungen an die optimale Spielgrafik zurückzuführen ist, variiert die Zugriffslatenz auf diese Caches erheblich. Nvidia hat eine höhere Zugriffslatenz und Textur-Caches in GeForce tragen in erster Linie dazu bei, die Belastung des Speicherbusses zu reduzieren, anstatt den Datenzugriff direkt zu beschleunigen. Dies fällt bei Grafikprogrammen nicht auf, ist aber bei Allzweckprogrammen wichtig. Bei Radeon ist die Latenz des Texturcaches geringer, dafür ist die Latenz des lokalen Speichers von Miniprozessoren höher. Wir können das folgende Beispiel geben: Für eine optimale Matrixmultiplikation auf Nvidia-Karten ist es besser, den lokalen Speicher zu verwenden und die Matrix dort Block für Block zu laden, während es für AMD besser ist, sich auf einen Textur-Cache mit geringer Latenz zu verlassen und Matrixelemente zu lesen wie benötigt. Dabei handelt es sich aber bereits um eine recht subtile Optimierung, und das für einen Algorithmus, der bereits grundlegend auf die GPU übertragen wurde.

Dieser Unterschied zeigt sich auch bei der Verwendung von 3D-Texturen. Einer der ersten GPU-Computing-Benchmarks, der einen gravierenden Vorteil für AMD zeigte, verwendete 3D-Texturen, da er mit einem dreidimensionalen Datenarray arbeitete. Und die Latenz beim Zugriff auf Texturen ist bei Radeon deutlich schneller und das 3D-Gehäuse ist zudem hardwaremäßig besser optimiert.

Um die maximale Leistung der Hardware verschiedener Unternehmen zu erzielen, ist eine gewisse Abstimmung der Anwendung für eine bestimmte Karte erforderlich, die jedoch im Prinzip um eine Größenordnung weniger bedeutsam ist als die Entwicklung eines Algorithmus für die GPU-Architektur.

Einschränkungen der Radeon 47xx-Serie

In dieser Familie ist die Unterstützung für GPU-Computing unvollständig. Drei wichtige Punkte können beachtet werden. Erstens gibt es keinen lokalen Speicher, das heißt, er ist physisch vorhanden, verfügt aber nicht über den universellen Zugriff, den der moderne Standard von GPU-Programmen erfordert. Es wird in Software im globalen Speicher emuliert, was bedeutet, dass seine Verwendung im Gegensatz zu einer voll ausgestatteten GPU keine Vorteile bringt. Der zweite Punkt ist die eingeschränkte Unterstützung verschiedener Anweisungen für atomare Speicheroperationen und Synchronisierungsanweisungen. Und der dritte Punkt ist die eher geringe Größe des Befehlscache: Ab einer bestimmten Programmgröße lässt die Geschwindigkeit deutlich nach. Es gibt weitere geringfügige Einschränkungen. Wir können sagen, dass nur Programme, die ideal für die GPU geeignet sind, auf dieser Grafikkarte gut funktionieren. Obwohl eine Grafikkarte in einfachen Testprogrammen, die nur mit Registern arbeiten, gute Ergebnisse in Gigaflops zeigen kann, ist es problematisch, etwas Komplexes dafür effektiv zu programmieren.

Vor- und Nachteile von Evergreen

Wenn man AMD- und Nvidia-Produkte vergleicht, sieht die 5xxx-Serie aus GPU-Computing-Perspektive wie ein sehr leistungsstarker GT200 aus. So leistungsstark, dass es Fermi in der Spitzenleistung um etwa das Zweieinhalbfache übertrifft. Vor allem, nachdem die Parameter der neuen Nvidia-Grafikkarten gekürzt und die Anzahl der Kerne reduziert wurden. Aber die Einführung eines L2-Cache in Fermi vereinfacht die Implementierung einiger Algorithmen auf der GPU und erweitert so den Umfang der GPU. Interessanterweise haben Fermis Architekturinnovationen bei CUDA-Programmen, die für die vorherige GT200-Generation gut optimiert waren, oft nichts bewirkt. Sie beschleunigten sich proportional zur Zunahme der Anzahl der Rechenmodule, also um weniger als das Doppelte (für Zahlen mit einfacher Genauigkeit) oder sogar weniger, weil die Speicherbandbreite nicht zunahm (oder aus anderen Gründen).

Und bei Aufgaben, die gut zur GPU-Architektur passen und einen ausgeprägten Vektorcharakter haben (z. B. Matrixmultiplikation), zeigt Radeon eine Leistung, die relativ nahe am theoretischen Höhepunkt liegt und Fermi übertrifft. Ganz zu schweigen von Multi-Core-CPUs. Besonders bei Problemen mit Zahlen mit einfacher Genauigkeit.

Aber Radeon hat eine kleinere Chipfläche, weniger Wärmeableitung, Stromverbrauch, höhere Ausbeute und dementsprechend niedrigere Kosten. Und direkt bei 3D-Grafikaufgaben ist Fermis Gewinn, sofern er überhaupt vorhanden ist, viel geringer als der Unterschied in der Kristallfläche. Dies liegt vor allem daran, dass die Radeon-Rechenarchitektur mit 16 Recheneinheiten pro Miniprozessor, einer Wellenfrontgröße von 64 Threads und VLIW-Vektoranweisungen hervorragend für ihre Hauptaufgabe – die Berechnung von Grafik-Shadern – geeignet ist. Für die überwiegende Mehrheit der normalen Benutzer stehen Spieleleistung und Preis im Vordergrund.

Aus professioneller, wissenschaftlicher Software-Perspektive bietet die Radeon-Architektur das beste Preis-Leistungs-Verhältnis, die beste Leistung pro Watt und die absolute Leistung bei Aufgaben, die von Natur aus gut auf GPU-Architekturen abgestimmt sind und Parallelisierung und Vektorisierung ermöglichen.

Beispielsweise ist Radeon bei einer vollständig parallelen, leicht vektorisierbaren Schlüsselauswahlaufgabe um ein Vielfaches schneller als GeForce und mehrere zehnmal schneller als die CPU.

Dies steht im Einklang mit dem allgemeinen Konzept von AMD Fusion, wonach GPUs die CPU ergänzen und künftig in den CPU-Kern selbst integriert werden sollen, so wie der mathematische Coprozessor zuvor von einem separaten Chip auf den Prozessorkern verlagert wurde (dies geschah vor zwanzig Jahren, vor dem Erscheinen der ersten Pentium-Prozessoren). Die GPU wird ein integrierter Grafikkern und Vektor-Coprozessor für Streaming-Aufgaben sein.

Radeon nutzt eine clevere Technik, bei der Anweisungen aus verschiedenen Wellenfronten gemischt werden, wenn sie von Funktionsmodulen ausgeführt werden. Dies ist einfach zu bewerkstelligen, da die Anweisungen völlig unabhängig voneinander sind. Das Prinzip ähnelt der Pipeline-Ausführung unabhängiger Anweisungen durch moderne CPUs. Dies ermöglicht offenbar die effiziente Ausführung komplexer Multibyte-Vektor-VLIW-Anweisungen. Dies erfordert in einer CPU einen ausgefeilten Scheduler zur Identifizierung unabhängiger Anweisungen oder den Einsatz der Hyper-Threading-Technologie, die die CPU auch mit bewusst unabhängigen Anweisungen aus verschiedenen Threads versorgt.

Maß 0Maßnahme 1Maßnahme 2Maßnahme 3Takt 4Takt 5Takt 6Takt 7VLIW-Modul
Wellenfront 0Wellenfront 1Wellenfront 0Wellenfront 1Wellenfront 0Wellenfront 1Wellenfront 0Wellenfront 1
Instr. 0Instr. 0Instr. 16Instr. 16Instr. 32Instr. 32Instr. 48Instr. 48VLIW0
Instr. 1VLIW1
Instr. 2VLIW2
Instr. 3VLIW3
Instr. 4VLIW4
Instr. 5VLIW5
Instr. 6VLIW6
Instr. 7VLIW7
Instr. 8VLIW8
Instr. 9VLIW9
Instr. 10VLIW10
Instr. elfVLIW11
Instr. 12VLIW12
Instr. 13VLIW13
Instr. 14VLIW14
Instr. 15VLIW15

128 Befehle zweier Wellenfronten, die jeweils aus 64 Operationen bestehen, werden von 16 VLIW-Modulen in acht Taktzyklen ausgeführt. Es findet eine Verschachtelung statt, und jedes Modul hat in Wirklichkeit zwei Taktzyklen, um einen gesamten Befehl auszuführen, vorausgesetzt, dass es beim zweiten Taktzyklus mit der parallelen Ausführung eines neuen Befehls beginnt. Dies hilft wahrscheinlich dabei, einen VLIW-Befehl wie a1×a2+b1×b2+c1×c2+d1×d2 schnell auszuführen, also acht solcher Befehle in acht Taktzyklen auszuführen. (Formell stellt sich heraus, dass es einer pro Takt ist.)

Nvidia verfügt offenbar nicht über eine solche Technologie. Und ohne VLIW erfordert eine hohe Leistung unter Verwendung skalarer Anweisungen einen Hochfrequenzbetrieb, der automatisch die Wärmeableitung erhöht und hohe Anforderungen an den Prozess stellt (um den Schaltkreis zu zwingen, mit einer höheren Frequenz zu arbeiten).

Der Nachteil von Radeon aus Sicht des GPU-Computings ist die große Abneigung gegen Branching. GPUs bevorzugen aufgrund der oben beschriebenen Technologie zur Ausführung von Anweisungen im Allgemeinen keine Verzweigungen: gleichzeitig in einer Gruppe von Threads mit einer Programmadresse. (Diese Technik heißt übrigens SIMT: Single Instruction – Multiple Threads (ein Befehl – ​​viele Threads), analog zu SIMD, wo ein Befehl eine Operation mit unterschiedlichen Daten ausführt.) Radeon mag Verzweigungen jedoch nicht besonders: das wird durch die größere Größe des Fadenbündels verursacht. Es ist klar, dass, wenn das Programm nicht vollständig vektorisiert ist, es umso schlimmer ist, je größer die Warp- oder Wellenfront ist, denn wenn benachbarte Threads in ihren Programmpfaden auseinanderlaufen, werden mehr Gruppen gebildet, die sequentiell (serialisiert) ausgeführt werden müssen. Nehmen wir an, alle Threads sind verstreut. Wenn die Warp-Größe 32 Threads beträgt, arbeitet das Programm 32-mal langsamer. Und bei Größe 64 ist es wie bei Radeon 64-mal langsamer.

Dies ist eine auffällige, aber nicht die einzige Manifestation von „Feindseligkeit“. Bei Nvidia-Grafikkarten verfügt jedes Funktionsmodul, auch CUDA-Kern genannt, über eine spezielle Zweigverarbeitungseinheit. Und in Radeon-Grafikkarten mit 16 Rechenmodulen gibt es nur zwei Zweigsteuereinheiten (sie werden aus der Domäne der Recheneinheiten entfernt). Selbst die einfache Verarbeitung einer bedingten Sprunganweisung nimmt daher zusätzliche Zeit in Anspruch, selbst wenn das Ergebnis für alle Threads in der Wellenfront gleich ist. Und die Geschwindigkeit sinkt.

AMD produziert auch CPUs. Sie glauben, dass für Programme mit vielen Zweigen die CPU immer noch besser geeignet ist, während die GPU für reine Vektorprogramme gedacht ist.

Radeon bietet also insgesamt eine geringere Programmiereffizienz, bietet aber in vielen Fällen ein besseres Preis-Leistungs-Verhältnis. Mit anderen Worten: Es gibt weniger Programme, die effizient (gewinnbringend) von einer CPU auf eine Radeon migriert werden können, als Programme, die effizient auf Fermi laufen können. Aber diejenigen, die effektiv übertragen werden können, funktionieren auf Radeon in vielerlei Hinsicht effizienter.

API für GPU-Computing

Die technischen Spezifikationen von Radeon selbst sehen attraktiv aus, obwohl es keinen Grund gibt, GPU-Computing zu idealisieren und zu verabsolutieren. Aber nicht weniger wichtig für die Produktivität ist die Software, die zum Entwickeln und Ausführen eines GPU-Programms erforderlich ist – Compiler aus einer Hochsprache und Laufzeit, also ein Treiber, der zwischen dem auf der CPU laufenden Programmteil und der GPU interagiert selbst. Es ist sogar noch wichtiger als bei einer CPU: Die CPU benötigt keinen Treiber, um Datenübertragungen zu verwalten, und aus Sicht des Compilers ist die GPU anspruchsvoller. Beispielsweise muss der Compiler mit einer minimalen Anzahl von Registern auskommen, um Zwischenergebnisse von Berechnungen zu speichern, und auch Funktionsaufrufe sorgfältig integrieren, wiederum unter Verwendung einer minimalen Anzahl von Registern. Denn je weniger Register ein Thread verwendet, desto mehr Threads können gestartet werden und desto vollständiger kann die GPU ausgelastet werden, wodurch die Speicherzugriffszeit besser ausgeblendet wird.

Und der Software-Support für Radeon-Produkte bleibt immer noch hinter der Hardware-Entwicklung zurück. (Anders als bei Nvidia, wo sich die Veröffentlichung der Hardware verzögerte und das Produkt in abgespeckter Form auf den Markt kam.) Erst kürzlich befand sich der von AMD produzierte OpenCL-Compiler im Beta-Status, mit vielen Mängeln. Es generierte zu oft fehlerhaften Code oder weigerte sich, Code aus dem richtigen Quellcode zu kompilieren, oder es erzeugte selbst einen Fehler und stürzte ab. Erst Ende des Frühlings erschien ein Release mit hoher Leistung. Es ist auch nicht fehlerfrei, aber es gibt deutlich weniger davon, und sie treten tendenziell in seitlicher Richtung auf, wenn versucht wird, etwas zu programmieren, das kurz vor der Richtigkeit steht. Sie arbeiten beispielsweise mit dem Typ uchar4, der eine 4-Byte-Variable mit vier Komponenten definiert. Dieser Typ ist in den OpenCL-Spezifikationen enthalten, aber es lohnt sich nicht, damit auf Radeon zu arbeiten, da die Register 128-Bit sind: die gleichen vier Komponenten, aber 32-Bit. Und eine solche uchar4-Variable belegt immer noch ein ganzes Register, es sind lediglich zusätzliche Packvorgänge und der Zugriff auf einzelne Byte-Komponenten erforderlich. Der Compiler sollte keine Fehler aufweisen, es gibt jedoch keine Compiler ohne Fehler. Sogar der Intel Compiler weist nach 11 Versionen Kompilierungsfehler auf. Die identifizierten Fehler werden in der nächsten Version behoben, die kurz vor dem Herbst veröffentlicht wird.

Aber es gibt noch viele Dinge, die verbessert werden müssen. Beispielsweise unterstützt der Standard-Radeon-GPU-Treiber immer noch kein GPU-Computing mit OpenCL. Der Benutzer muss ein zusätzliches Spezialpaket herunterladen und installieren.

Aber das Wichtigste ist das Fehlen jeglicher Funktionsbibliotheken. Für reelle Zahlen mit doppelter Genauigkeit gibt es nicht einmal einen Sinus, Cosinus oder Exponenten. Nun, das ist für die Matrixaddition und -multiplikation nicht erforderlich, aber wenn Sie etwas komplexeres programmieren möchten, müssen Sie alle Funktionen von Grund auf neu schreiben. Oder warten Sie auf eine neue SDK-Version. ACML (AMD Core Math Library) für die Evergreen GPU-Familie mit Unterstützung für grundlegende Matrixfunktionen sollte bald veröffentlicht werden.

Laut dem Autor des Artikels scheint es derzeit machbar, die Direct Compute 5.0-API zum Programmieren von Radeon-Grafikkarten zu verwenden, natürlich unter Berücksichtigung der Einschränkungen: Ausrichtung auf die Plattformen Windows 7 und Windows Vista. Microsoft verfügt über umfassende Erfahrung in der Erstellung von Compilern und wir können sehr bald mit einer voll funktionsfähigen Veröffentlichung rechnen. Microsoft ist daran direkt interessiert. Doch Direct Compute konzentriert sich auf die Bedürfnisse interaktiver Anwendungen: etwas berechnen und das Ergebnis sofort visualisieren – zum Beispiel den Fluss einer Flüssigkeit über eine Oberfläche. Das bedeutet nicht, dass es nicht einfach für Berechnungen verwendet werden kann, aber das ist nicht sein natürlicher Zweck. Nehmen wir an, Microsoft plant nicht, Direct Compute Bibliotheksfunktionen hinzuzufügen – nur solche, die AMD derzeit nicht hat. Das heißt, was auf Radeon mittlerweile effektiv berechnet werden kann – einige nicht sehr ausgefeilte Programme – lässt sich auch auf Direct Compute umsetzen, was deutlich einfacher als OpenCL ist und stabiler sein dürfte. Außerdem ist es vollständig portabel und läuft sowohl auf Nvidia als auch auf AMD, sodass Sie das Programm nur einmal kompilieren müssen, während die OpenCL SDK-Implementierungen von Nvidia und AMD nicht vollständig kompatibel sind. (In dem Sinne, dass, wenn Sie ein OpenCL-Programm auf einem AMD-System mit dem AMD OpenCL SDK entwickeln, es möglicherweise nicht so einfach auf Nvidia läuft. Möglicherweise müssen Sie denselben Text mit dem Nvidia SDK kompilieren. Und natürlich umgekehrt .)

Darüber hinaus gibt es in OpenCL viele redundante Funktionen, da OpenCL eine universelle Programmiersprache und API für eine Vielzahl von Systemen sein soll. Und GPU, CPU und Zelle. Wenn Sie also nur ein Programm für ein typisches Benutzersystem (Prozessor plus Grafikkarte) schreiben müssen, scheint OpenCL sozusagen nicht „hochproduktiv“ zu sein. Jede Funktion verfügt über zehn Parameter, von denen neun auf 0 gesetzt werden müssen. Und um jeden Parameter festzulegen, müssen Sie eine spezielle Funktion aufrufen, die ebenfalls über Parameter verfügt.

Und der wichtigste aktuelle Vorteil von Direct Compute besteht darin, dass der Benutzer kein spezielles Paket installieren muss: Alles, was benötigt wird, ist bereits in DirectX 11 enthalten.

Probleme der GPU-Computing-Entwicklung

Betrachtet man den Bereich der Personalcomputer, stellt sich die Situation wie folgt dar: Es gibt nicht viele Aufgaben, die eine große Rechenleistung erfordern, und ein herkömmlicher Dual-Core-Prozessor mangelt stark daran. Es war, als wären große, gefräßige, aber tollpatschige Monster aus dem Meer an Land gekrochen, und an Land gäbe es fast nichts zu essen. Und die ursprünglichen Lebensräume der Erdoberfläche werden kleiner und lernen, weniger zu verbrauchen, wie es immer der Fall ist, wenn die natürlichen Ressourcen knapp sind. Wenn heute derselbe Bedarf an Leistung bestünde wie vor 10 bis 15 Jahren, wäre GPU-Computing ein großer Erfolg. Und so rücken die Kompatibilitätsprobleme und die relative Komplexität der GPU-Programmierung in den Vordergrund. Es ist besser, ein Programm zu schreiben, das auf allen Systemen läuft, als ein Programm, das schnell läuft, aber nur auf der GPU läuft.

Für den Einsatz in professionellen Anwendungen und im Workstation-Bereich sind die Aussichten für GPUs etwas besser, da dort ein größerer Bedarf an Leistung besteht. Es gibt Plugins für 3D-Editoren mit GPU-Unterstützung: zum Beispiel für das Rendern mittels Raytracing – nicht zu verwechseln mit regulärem GPU-Rendering! Auch für 2D- und Präsentationseditoren zeichnet sich eine schnellere Erstellung komplexer Effekte ab. Auch Videoverarbeitungsprogramme erhalten nach und nach GPU-Unterstützung. Die oben genannten Aufgaben passen aufgrund ihrer parallelen Natur gut zur GPU-Architektur, aber jetzt wurde eine sehr große Codebasis erstellt, debuggt und für alle Fähigkeiten der CPU optimiert, sodass es einige Zeit dauern wird, bis gute GPU-Implementierungen erscheinen .

In diesem Segment treten auch solche Schwächen von GPUs auf, wie etwa der begrenzte Videospeicher – etwa 1 GB bei herkömmlichen GPUs. Einer der Hauptfaktoren, die die Leistung von GPU-Programmen verringern, ist die Notwendigkeit, Daten zwischen CPU und GPU über einen langsamen Bus auszutauschen, und aufgrund des begrenzten Speichers müssen mehr Daten übertragen werden. Und hier sieht AMDs Konzept, GPU und CPU in einem Modul zu vereinen, vielversprechend aus: Man kann die hohe Bandbreite des Grafikspeichers für einen einfachen und unkomplizierten Zugriff auf Shared Memory opfern, und das bei geringerer Latenz. Diese hohe Bandbreite des aktuellen DDR5-Videospeichers wird direkt von Grafikprogrammen viel stärker nachgefragt als von den meisten GPU-Rechnerprogrammen. Im Allgemeinen wird der gemeinsame Speicher von GPU und CPU den Umfang der GPU einfach erheblich erweitern und es ermöglichen, ihre Rechenkapazitäten in kleinen Teilaufgaben von Programmen zu nutzen.

Und GPUs sind im Bereich des wissenschaftlichen Rechnens am gefragtesten. Es wurden bereits mehrere GPU-basierte Supercomputer gebaut, die im Matrix-Operations-Test sehr gute Ergebnisse zeigen. Wissenschaftliche Probleme sind so vielfältig und zahlreich, dass es immer viele gibt, die perfekt in die GPU-Architektur passen, für die der Einsatz einer GPU es einfach macht, eine hohe Leistung zu erzielen.

Wenn Sie unter allen Aufgaben moderner Computer eine auswählen, ist es die Computergrafik – das Bild der Welt, in der wir leben. Und die optimale Architektur für diesen Zweck kann nicht schlecht sein. Dies ist eine so wichtige und grundlegende Aufgabe, dass speziell dafür entwickelte Hardware universell und für verschiedene Aufgaben optimal sein muss. Darüber hinaus entwickeln sich Grafikkarten erfolgreich weiter.

Oft stellt sich die Frage: Warum gibt es in Adobe Media Encoder CC keine GPU-Beschleunigung? Wir haben herausgefunden, dass Adobe Media Encoder GPU-Beschleunigung verwendet, und haben auch die Nuancen seiner Verwendung festgestellt. Es gibt auch eine Aussage, dass Adobe Media Encoder CC die Unterstützung für GPU-Beschleunigung entfernt hat. Dies ist eine falsche Meinung und ergibt sich aus der Tatsache, dass das Hauptprogramm von Adobe Premiere Pro CC jetzt ohne eine registrierte und empfohlene Grafikkarte funktionieren kann und um die GPU-Engine in Adobe Media Encoder CC zu aktivieren, muss die Grafikkarte in den Dokumenten registriert werden : cuda_supported_cards oder opencl_supported_cards. Wenn mit nVidia-Chipsätzen alles klar ist, nehmen Sie einfach den Namen des Chipsatzes und geben Sie ihn in das Dokument cuda_supported_cards ein. Bei Verwendung von AMD-Grafikkarten müssen Sie dann nicht den Namen des Chipsatzes, sondern den Codenamen des Kerns eingeben. Schauen wir uns also in der Praxis an, wie Sie die GPU-Engine in Adobe Media Encoder CC auf einem ASUS N71JQ-Laptop mit separater ATI Mobility Radeon HD 5730-Grafikkarte aktivieren. Technische Daten des ATI Mobility Radeon HD 5730-Grafikadapters, angezeigt vom GPU-Z-Dienstprogramm:

Starten Sie Adobe Premiere Pro CC und schalten Sie die Engine ein: Mercury Playback Engine GPU Acceleration (OpenCL).

Drei DSLR-Videos auf einer Timeline, eines über dem anderen, davon zwei, erzeugen einen Bild-in-Bild-Effekt.

Strg+M, wählen Sie die Voreinstellung „MPEG2-DVD“ aus und entfernen Sie die schwarzen Balken an den Seiten mit der Option „Auf Füllung skalieren“. Wir bieten auch eine erhöhte Qualität für Tests ohne GPU: MRQ (Use Maximum Render Quality). Klicken Sie auf die Schaltfläche: Exportieren. CPU-Auslastung bis zu 20 % und Arbeitsspeicher 2,56 GB.


Die GPU-Auslastung des ATI Mobility Radeon HD 5730-Chipsatzes beträgt 97 % und der integrierte Videospeicher 352 MB. Der Laptop wurde im Akkubetrieb getestet, sodass der Grafikkern/Speicher mit niedrigeren Frequenzen arbeitet: 375/810 MHz.

Gesamtrenderzeit: 1 Minute und 55 Sekunden(Das Ein-/Ausschalten von MRQ bei Verwendung einer GPU-Engine hat keinen Einfluss auf die endgültige Renderzeit.)
Wenn das Kontrollkästchen Maximale Renderqualität verwenden aktiviert ist, klicken Sie nun auf die Schaltfläche: Warteschlange.


Prozessortaktfrequenz im Akkubetrieb: 930 MHz.

Führen Sie AMEEncodingLog aus und sehen Sie sich die endgültige Renderzeit an: 5 Minuten und 14 Sekunden.

Wir wiederholen den Test, klicken jedoch bei deaktiviertem Kontrollkästchen Maximale Renderqualität verwenden auf die Schaltfläche: Warteschlange.

Gesamtrenderzeit: 1 Minute und 17 Sekunden.

Jetzt schalten wir die GPU-Engine in Adobe Media Encoder CC ein, starten das Adobe Premiere Pro CC-Programm, drücken die Tastenkombination: Strg + F12, führen Konsole > Konsolenansicht aus und geben GPUSniffer in das Befehlsfeld ein, drücken die Eingabetaste.


Wählen Sie den Namen aus und kopieren Sie ihn in GPU Computation Info.

Öffnen Sie im Programmverzeichnis von Adobe Premiere Pro CC das Dokument opencl_supported_cards und geben Sie den Codenamen des Chipsatzes in alphabetischer Reihenfolge ein, Strg+S.

Klicken Sie auf die Schaltfläche: Warteschlange, und wir erhalten eine GPU-Beschleunigung beim Rendern eines Adobe Premiere Pro CC-Projekts in Adobe Media Encoder CC.

Gesamtzeit: 1 Minute und 55 Sekunden.

Wir schließen den Laptop an die Steckdose an und wiederholen die Ergebnisse der Berechnungen. Warteschlange, das MRQ-Kontrollkästchen ist deaktiviert, ohne die Engine einzuschalten, hat sich die RAM-Auslastung etwas erhöht:


Prozessortaktfrequenz: 1,6 GHz bei Betrieb über eine Steckdose und aktiviertem Modus: Hohe Leistung.

Gesamtzeit: 46 Sekunden.

Wir schalten die Engine ein: Mercury Playback Engine GPU Acceleration (OpenCL), wie aus dem Netzwerk ersichtlich ist, läuft die Laptop-Grafikkarte mit ihren Grundfrequenzen, die GPU-Auslastung in Adobe Media Encoder CC erreicht 95 %.

Die Gesamtrenderzeit verringerte sich von 1 Minute 55 Sekunden, Vor 1 Minute und 5 Sekunden.

*Adobe Media Encoder CC verwendet jetzt eine Grafikverarbeitungseinheit (GPU) zum Rendern. CUDA- und OpenCL-Standards werden unterstützt. In Adobe Media Encoder CC wird die GPU-Engine für die folgenden Rendering-Prozesse verwendet:
- Ändern Sie die Klarheit (von hoch auf Standard und umgekehrt).
- Timecode-Filter.
- Konvertierungen des Pixelformats.
- Disinterleaving.
Wenn Sie ein Premiere Pro-Projekt rendern, verwendet AME die für dieses Projekt angegebenen GPU-Rendering-Einstellungen. Dadurch werden alle GPU-Rendering-Funktionen von Premiere Pro genutzt. AME-Projekte werden mit einem begrenzten Satz an GPU-Rendering-Funktionen gerendert. Wenn die Sequenz mit nativer Unterstützung gerendert wird, wird die GPU-Einstellung von AME angewendet, die Projekteinstellung wird ignoriert. In diesem Fall werden alle GPU-Rendering-Funktionen von Premiere Pro direkt in AME genutzt. Wenn das Projekt VSTs von Drittanbietern enthält, wird die GPU-Einstellung des Projekts verwendet. Die Sequenz wird wie in früheren Versionen von AME mit PProHeadless codiert. Wenn „Nativen Premiere Pro-Sequenzimport aktivieren“ deaktiviert ist, werden immer PProHeadless und die GPU-Einstellung verwendet.

Wir haben von einer versteckten Partition auf der Systemfestplatte des ASUS N71JQ-Laptops gelesen.

Es kann nie zu viele Kerne geben...

Moderne GPUs sind monströse, schnelle Biester, die Gigabytes an Daten fressen können. Allerdings ist der Mensch schlau und egal, wie viel Rechenleistung wächst, er kommt auf immer komplexere Probleme, sodass der Moment kommt, in dem wir leider zugeben müssen, dass eine Optimierung erforderlich ist 🙁

In diesem Artikel werden die grundlegenden Konzepte beschrieben, um die Navigation in der Theorie der GPU-Optimierung und die Grundregeln zu erleichtern, sodass diese Konzepte seltener behandelt werden müssen.

Gründe, warum GPUs für die Arbeit mit großen Datenmengen, die verarbeitet werden müssen, effektiv sind:

  • Sie verfügen über hervorragende Fähigkeiten zur parallelen Ausführung von Aufgaben (viele, viele Prozessoren).
  • hohe Speicherbandbreite

Speicherbandbreite– so viele Informationen – ein Bit oder ein Gigabyte – können pro Zeiteinheit – einer Sekunde oder einem Prozessorzyklus – übertragen werden.

Eine der Optimierungsaufgaben besteht darin, den maximalen Durchsatz zu nutzen – um die Leistung zu steigern Durchsatz(Idealerweise sollte sie der Speicherbandbreite entsprechen).

So verbessern Sie die Bandbreitennutzung:

  • Erhöhen Sie die Informationsmenge – nutzen Sie die Bandbreite optimal (z. B. arbeitet jeder Thread mit float4)
  • Latenz reduzieren – Verzögerung zwischen Vorgängen

Latenz– die Zeitspanne zwischen dem Zeitpunkt, an dem der Controller eine bestimmte Speicherzelle anforderte, und dem Zeitpunkt, an dem die Daten dem Prozessor zur Ausführung von Anweisungen zur Verfügung standen. Auf die Verzögerung selbst können wir keinerlei Einfluss nehmen – diese Einschränkungen liegen auf Hardwareebene vor. Aufgrund dieser Verzögerung kann der Prozessor mehrere Threads gleichzeitig bedienen – während Thread A die Zuweisung von Speicher für ihn angefordert hat, kann Thread B etwas berechnen und Thread C kann warten, bis die angeforderten Daten bei ihm eintreffen.

So reduzieren Sie die Latenz, wenn die Synchronisierung verwendet wird:

  • Reduzieren Sie die Anzahl der Threads in einem Block
  • Erhöhen Sie die Anzahl der Blockgruppen

Vollständige Nutzung der GPU-Ressourcen – GPU-Belegung

In anspruchsvollen Gesprächen über Optimierung taucht häufig der Begriff auf: GPU-Belegung oder Kernelbelegung– Es spiegelt die Effizienz der Nutzung der Ressourcen der Grafikkarte wider. Ich möchte gesondert darauf hinweisen, dass selbst die Nutzung aller Ressourcen nicht bedeutet, dass Sie diese richtig nutzen.

Die Rechenleistung der GPU besteht aus Hunderten von rechenhungrigen Prozessoren; beim Erstellen eines Programms – des Kernels – liegt die Last der Lastverteilung auf den Schultern des Programmierers. Ein Fehler kann dazu führen, dass viele dieser wertvollen Ressourcen ungenutzt bleiben. Jetzt werde ich erklären, warum. Wir müssen aus der Ferne beginnen.

Ich möchte Sie daran erinnern, dass Warp ( Kette in der NVidia-Terminologie: Wellenfront – in der AMD-Terminologie) ist eine Reihe von Threads, die gleichzeitig die gleiche Kernelfunktion auf dem Prozessor ausführen. Vom Programmierer in Blöcken zusammengefasste Threads werden von einem Thread-Scheduler (separat für jeden Multiprozessor) in Warps unterteilt – während ein Warp arbeitet, wartet der zweite auf die Verarbeitung von Speicheranforderungen usw. Wenn einige der Warp-Threads noch Berechnungen durchführen, während andere bereits alles getan haben, was sie konnten, liegt eine ineffiziente Nutzung der Rechenressource vor – im Volksmund auch Leerkapazität genannt.

Jeder Synchronisationspunkt, jeder Logikzweig kann eine solche Leerlaufsituation erzeugen. Die maximale Divergenz (Verzweigung der Ausführungslogik) hängt von der Größe des Warps ab. Bei NVidia-GPUs sind es 32, bei AMD 64.

So reduzieren Sie die Ausfallzeit mehrerer Prozessoren während der Warp-Ausführung:

  • Minimieren Sie die Wartezeit an der Barriere
  • Minimieren Sie die Divergenz der Ausführungslogik in der Kernelfunktion

Um dieses Problem effektiv zu lösen, ist es sinnvoll zu verstehen, wie Warps gebildet werden (für den Fall mit mehreren Dimensionen). Tatsächlich ist die Reihenfolge einfach – zuerst in X, dann in Y und zuletzt in Z.

Der Kernel wird mit Blöcken der Größe 64x16 gestartet, Threads werden in Warps in der Reihenfolge X, Y, Z unterteilt – d. h. Die ersten 64 Elemente werden in zwei Warps unterteilt, dann das zweite usw.

Der Kernel läuft mit 16x64 Blöcken. Die ersten und zweiten 16 Elemente werden zur ersten Kette hinzugefügt, das dritte und vierte zur zweiten Kette usw.

So reduzieren Sie Divergenzen (denken Sie daran, dass Verzweigungen nicht immer die Ursache für kritische Leistungsverluste sind)

  • Wenn benachbarte Flüsse unterschiedliche Ausführungspfade haben – entlang dieser gibt es viele Bedingungen und Übergänge –, suchen Sie nach Möglichkeiten zur Umstrukturierung
  • Suchen Sie nach einer unausgeglichenen Thread-Last und entfernen Sie sie entschieden (in diesem Fall haben wir nicht nur Bedingungen, sondern aufgrund dieser Bedingungen berechnet der erste Thread immer etwas, und der fünfte erfüllt diese Bedingung nicht und ist inaktiv).

So holen Sie das Beste aus Ihren GPU-Ressourcen heraus

Leider haben auch GPU-Ressourcen ihre Grenzen. Und genau genommen ist es sinnvoll, vor dem Start der Kernel-Funktion die Grenzen zu ermitteln und diese bei der Lastverteilung zu berücksichtigen. Warum ist es wichtig?

Für Grafikkarten gelten Einschränkungen hinsichtlich der Gesamtzahl der Threads, die ein Multiprozessor ausführen kann, der maximalen Anzahl von Threads in einem Block, der maximalen Anzahl von Warps auf einem Prozessor, Einschränkungen für verschiedene Speichertypen usw. Alle diese Informationen können entweder programmgesteuert, über die entsprechende API oder zuvor mithilfe von Dienstprogrammen aus dem SDK angefordert werden. (DeviceQuery-Module für NVidia-Geräte, CLInfo – für AMD-Grafikkarten).

Allgemeine Praxis:

  • Die Anzahl der Thread-Blöcke/Arbeitsgruppen muss ein Vielfaches der Anzahl der Stream-Prozessoren sein
  • Die Block-/Arbeitsgruppengröße muss ein Vielfaches der Warp-Größe sein

Es sollte berücksichtigt werden, dass das absolute Minimum 3-4 Warps/Wayfronts sind, die sich gleichzeitig auf jedem Prozessor drehen; weise Ratgeber empfehlen, von der Betrachtung von mindestens sieben Wayfronts auszugehen. Vergessen Sie dabei nicht die Hardware-Einschränkungen!

All diese Details im Kopf zu behalten wird schnell langweilig. Um die GPU-Auslastung zu berechnen, hat NVidia ein unerwartetes Tool angeboten – einen Excel(!)-Rechner voller Makros. Dort können Sie Informationen zur maximalen Anzahl von Threads für SM, zur Anzahl der Register und zur Größe des gesamten (gemeinsam genutzten) Speichers, der auf dem Stream-Prozessor verfügbar ist, sowie zu den verwendeten Funktionsstartparametern eingeben – und die Effizienz der Ressourcennutzung wird angezeigt einen Prozentsatz (und Sie raufen sich die Haare, weil Ihnen klar wird, dass Ihnen zur Nutzung aller Kerne Register fehlen).

Nutzungsinformationen:
http://docs.nvidia.com/cuda/cuda-c-best-practices-guide/#calculated-occupancy

GPU- und Speicheroperationen

Grafikkarten sind für 128-Bit-Speicheroperationen optimiert. Diese. Im Idealfall sollte jede Speichermanipulation idealerweise 4 Vier-Byte-Werte gleichzeitig ändern. Das Hauptproblem für einen Programmierer besteht darin, dass moderne GPU-Compiler nicht wissen, wie sie solche Dinge optimieren können. Dies muss direkt im Funktionscode erfolgen und bringt im Durchschnitt einen Bruchteil eines Prozents Leistungssteigerung. Die Häufigkeit der Speicheranforderungen hat einen viel größeren Einfluss auf die Leistung.

Das Problem ist folgendes: Jede Anfrage gibt ein Datenelement zurück, dessen Größe ein Vielfaches von 128 Bit ist. Und jeder Thread nutzt nur ein Viertel davon (im Fall einer regulären Vier-Byte-Variable). Wenn benachbarte Threads gleichzeitig mit Daten arbeiten, die sich nacheinander in Speicherzellen befinden, verringert sich die Gesamtzahl der Speicherzugriffe. Dieses Phänomen wird als kombinierte Lese- und Schreibvorgänge bezeichnet ( Zusammengeführter Zugang – gut! sowohl lesen als auch schreiben) – und mit der richtigen Organisation des Codes ( Der Zugriff auf einen zusammenhängenden Teil des Speichers wurde gestreikt – schlecht!) kann die Leistung erheblich verbessern. Wenn Sie Ihren Kern – denken Sie daran – zusammenhängenden Zugriff – innerhalb der Elemente einer Speicherzeile organisieren, ist die Arbeit mit Spaltenelementen nicht mehr so ​​effizient. Möchten Sie weitere Details? Mir hat dieses PDF gefallen – oder google nach „ Techniken zur Gedächtniszusammenführung “.

Den Spitzenplatz in der Kategorie „Engpass“ nimmt eine weitere Speicheroperation ein – Kopieren von Daten vom Hostspeicher zur GPU . Das Kopieren erfolgt nicht ohnehin, sondern aus einem vom Treiber und System speziell zugewiesenen Speicherbereich: Bei einer Anforderung zum Kopieren von Daten kopiert das System diese Daten zunächst dorthin und lädt sie erst dann auf die GPU hoch. Die Geschwindigkeit des Datentransports wird durch die Bandbreite des PCI Express xN-Busses (wobei N die Anzahl der Datenleitungen ist) begrenzt, über den moderne Grafikkarten mit dem Host kommunizieren.

Allerdings ist das unnötige Kopieren von langsamem Speicher auf dem Host manchmal ein ungerechtfertigter Kostenfaktor. Die Lösung besteht darin, das sogenannte zu verwenden angehefteter Speicher – ein speziell gekennzeichneter Speicherbereich, sodass das Betriebssystem keine Operationen damit ausführen kann (z. B. nach eigenem Ermessen in Swap/Move verschieben usw.). Die Datenübertragung vom Host zur Grafikkarte erfolgt ohne Beteiligung Betriebssystem– asynchron, über DMA (direkter Speicherzugriff).

Und zum Schluss noch etwas mehr zum Thema Erinnerung. Der gemeinsam genutzte Speicher auf einem Multiprozessor ist normalerweise in Form von Speicherbänken organisiert, die 32-Bit-Wörter – Daten – enthalten. Die Anzahl der Bänke variiert der guten Tradition nach von GPU-Generation zu GPU-Generation: 16/32. Wenn jeder Thread für Daten auf eine separate Bank zugreift, ist alles in Ordnung. Andernfalls erhalten wir mehrere Lese-/Schreibanfragen an eine Bank und es kommt zu einem Konflikt ( Konflikt mit gemeinsam genutzter Speicherbank). Solche widersprüchlichen Aufrufe werden serialisiert und daher sequentiell und nicht parallel ausgeführt. Wenn alle Threads auf eine Bank zugreifen, wird eine „Broadcast“-Antwort verwendet ( übertragen) und es gibt keinen Konflikt. Es gibt mehrere Möglichkeiten, Zugriffskonflikte effektiv zu lösen. Mir hat es gefallen Beschreibung der wichtigsten Techniken zur Beseitigung von Zugriffskonflikten auf Speicherbänke – .

Wie kann man mathematische Operationen noch schneller machen? Erinnere dich daran:

  • Berechnungen mit doppelter Genauigkeit sind eine Hochlastoperation mit fp64 >> fp32
  • Konstanten der Form 3.13 im Code werden standardmäßig als fp64 interpretiert, wenn 3.14f nicht explizit angegeben ist
  • Um die Mathematik zu optimieren, wäre es eine gute Idee, die Anleitungen zu überprüfen, um zu sehen, ob der Compiler irgendwelche Flags hat
  • Hersteller integrieren Funktionen in ihre SDKs, die Gerätefunktionen nutzen, um Leistung zu erzielen (häufig auf Kosten der Portabilität).

Für CUDA-Entwickler ist es sinnvoll, dem Konzept große Aufmerksamkeit zu schenken Cuda-Stream Damit können Sie mehrere Kernelfunktionen gleichzeitig auf einem Gerät ausführen oder das asynchrone Kopieren von Daten vom Host auf das Gerät kombinieren, während Funktionen ausgeführt werden. OpenCL bietet eine solche Funktionalität noch nicht :)

Schrott zum Profilieren:

NVifia Visual Profiler ist ein interessantes Dienstprogramm, das sowohl CUDA- als auch OpenCL-Kernel analysiert.

P.S. Als umfassendere Anleitung zur Optimierung kann ich empfehlen, alle möglichen Dinge zu googeln Best Practices-Leitfaden für OpenCL und CUDA.

  • ,

Heutzutage sind an jeder Ecke Neuigkeiten über den Einsatz von GPUs für allgemeine Computer zu hören. Wörter wie CUDA, Stream und OpenCL sind in nur zwei Jahren fast zu den am häufigsten zitierten Wörtern im IT-Internet geworden. Allerdings weiß nicht jeder, was diese Worte bedeuten und was die Technologien dahinter bedeuten. Und für Linux-Benutzer, die es gewohnt sind, „on the fly“ zu sein, scheint das alles wie ein dunkler Wald.

Geburt von GPGPU

Wir sind es alle gewohnt zu glauben, dass die einzige Komponente eines Computers, die jeden Code ausführen kann, der ihr zugewiesen wird, der Zentralprozessor ist. Lange Zeit waren fast alle Massen-PCs mit einem einzigen Prozessor ausgestattet, der alle erdenklichen Berechnungen erledigte, einschließlich des Betriebssystemcodes, aller unserer Software und Viren.

Später erschien Multi-Core-Prozessoren und Multiprozessorsysteme, in denen es mehrere solcher Komponenten gab. Dadurch konnten Maschinen mehrere Aufgaben gleichzeitig ausführen und die gesamte (theoretische) Systemleistung stieg genau so stark wie die Anzahl der in der Maschine installierten Kerne. Es stellte sich jedoch heraus, dass es zu schwierig und zu teuer war, Mehrkernprozessoren herzustellen und zu entwerfen.

Jeder Kern musste einen vollwertigen Prozessor einer komplexen und komplizierten x86-Architektur mit eigenem (ziemlich großem) Cache, eigener Befehlspipeline, SSE-Blöcken, vielen Blöcken zur Durchführung von Optimierungen usw. beherbergen. usw. Daher verlangsamte sich der Prozess der Erhöhung der Anzahl der Kerne erheblich, und weiße Universitätsmäntel, denen zwei oder vier Kerne eindeutig nicht ausreichten, fanden einen Weg, andere Rechenleistung für ihre wissenschaftlichen Berechnungen zu nutzen, die im Video reichlich vorhanden war Karte (als Ergebnis erschien sogar das BrookGPU-Tool, das emuliert zusätzlicher Prozessor unter Verwendung von DirectX- und OpenGL-Funktionsaufrufen).

Grafikprozessoren, die viele Nachteile des Zentralprozessors nicht aufwiesen, erwiesen sich als hervorragende und sehr schnelle Rechenmaschinen, und sehr bald begannen die GPU-Hersteller selbst, die Entwicklungen wissenschaftlicher Köpfe (und tatsächlich engagierte nVidia) genauer zu untersuchen die meisten Forscher). Als Ergebnis erschien die nVidia CUDA-Technologie, die eine Schnittstelle definiert, mit der es möglich wurde, die Berechnung komplexer Algorithmen ohne Krücken auf die Schultern der GPU zu übertragen. Später folgte ATi (AMD) mit einer eigenen Version der Technologie namens Close to Metal (jetzt Stream) und sehr bald erschien eine Standardversion von Apple namens OpenCL.

Ist die GPU alles?

Trotz aller Vorteile weist die GPGPU-Technik mehrere Probleme auf. Das erste davon ist der sehr enge Anwendungsbereich. GPUs sind dem Zentralprozessor hinsichtlich der Steigerung der Rechenleistung und der Gesamtzahl der Kerne weit voraus (Grafikkarten verfügen über eine Recheneinheit mit mehr als hundert Kernen), eine solch hohe Dichte wird jedoch durch die Maximierung der Vereinfachung des Designs erreicht des Chips selbst.

Im Wesentlichen läuft die Hauptaufgabe der GPU darauf hinaus mathematische Berechnungen mit Hilfe einfache Algorithmen, die nicht sehr große Mengen vorhersehbarer Daten als Eingabe erhalten. Aus diesem Grund haben GPU-Kerne ein sehr einfaches Design, geringe Cache-Größen und einen bescheidenen Befehlssatz, was letztendlich zu niedrigen Produktionskosten und der Möglichkeit einer sehr dichten Platzierung auf dem Chip führt. GPUs sind wie eine chinesische Fabrik mit Tausenden von Arbeitern. Einige einfache Dinge erledigen sie ganz gut (und vor allem schnell und günstig), aber wenn man ihnen den Zusammenbau eines Flugzeugs anvertraut, wird das Ergebnis höchstens ein Drachenflieger sein.

Daher besteht die erste Einschränkung von GPUs in ihrem Fokus auf schnelle mathematische Berechnungen, was den Anwendungsbereich von GPUs auf die Unterstützung beim Betrieb von Multimedia-Anwendungen sowie allen Programmen beschränkt, die an der komplexen Datenverarbeitung beteiligt sind (z. B. Archivierer oder Verschlüsselungssysteme). sowie Software für Fluoreszenzmikroskopie, Molekulardynamik, Elektrostatik und andere Dinge, die für Linux-Benutzer von geringem Interesse sind).

Das zweite Problem bei GPGPU besteht darin, dass nicht jeder Algorithmus für die Ausführung auf der GPU angepasst werden kann. Einzelne GPU-Kerne sind recht langsam und ihre Leistung entfaltet sich erst im Zusammenspiel. Das bedeutet, dass der Algorithmus so effektiv ist, wie der Programmierer ihn effektiv parallelisieren kann. In den meisten Fällen kann nur ein guter Mathematiker solche Arbeiten bewältigen, von denen es nur sehr wenige Softwareentwickler gibt.

Und drittens arbeiten GPUs mit Speicher, der auf der Grafikkarte selbst installiert ist, sodass bei jeder Verwendung der GPU zwei zusätzliche Kopiervorgänge stattfinden: Eingabedaten aus dem RAM der Anwendung selbst und Ausgabedaten aus dem GRAM zurück in den Anwendungsspeicher. Wie Sie sich vorstellen können, kann dies jegliche Vorteile in der Anwendungslaufzeit zunichte machen (wie es beim FlacCL-Tool der Fall ist, das wir später betrachten werden).

Aber das ist nicht alles. Trotz der Existenz eines allgemein akzeptierten Standards in Form von OpenCL bevorzugen viele Programmierer immer noch die Verwendung herstellerspezifischer Implementierungen der GPGPU-Technik. Als besonders beliebt erwies sich CUDA, das zwar eine flexiblere Programmierschnittstelle bietet (übrigens OpenCL in nVidia-Treiber implementiert auf CUDA), bindet die Anwendung jedoch eng an Grafikkarten eines Herstellers.

KGPU oder Linux-Kernel, beschleunigt durch GPU

Forscher der University of Utah haben ein KGPU-System entwickelt, das die Ausführung einiger Linux-Kernelfunktionen auf einer GPU mithilfe des CUDA-Frameworks ermöglicht. Um diese Aufgabe auszuführen, werden ein modifizierter Linux-Kernel und ein spezieller Daemon verwendet, der im Userspace läuft, Kernel-Anfragen abhört und diese mithilfe der CUDA-Bibliothek an den Grafikkartentreiber weiterleitet. Interessanterweise ist es den Autoren von KGPU trotz des erheblichen Mehraufwands, den eine solche Architektur verursacht, gelungen, eine Implementierung des AES-Algorithmus zu erstellen, die die Verschlüsselungsgeschwindigkeit erhöht Dateisystem eCryptfs 6 Mal.

Was gibt es jetzt?

Aufgrund seiner Jugend und auch aufgrund der oben beschriebenen Probleme wurde GPGPU jedoch nie zu einer wirklich weit verbreiteten Technologie nützliche Software, das seine Fähigkeiten nutzt, existiert (wenn auch in geringen Mengen). Zu den ersten, die auftauchten, gehörten Cracker verschiedener Hashes, deren Algorithmen sehr einfach zu parallelisieren sind.

Es entstanden auch Multimedia-Anwendungen wie der FlacCL-Encoder, mit dem Sie eine Audiospur in das FLAC-Format transkodieren können. Einige bereits vorhandene Anwendungen haben auch GPGPU-Unterstützung erhalten, die bemerkenswerteste davon ist ImageMagick, das jetzt mithilfe von OpenCL einen Teil seiner Arbeit auf die GPU verlagern kann. Es gibt auch Projekte, um Datenarchivierer und andere Iauf CUDA/OpenCL zu übertragen (ATi-Unixoide sind nicht beliebt). Wir werden uns die interessantesten dieser Projekte in den folgenden Abschnitten des Artikels ansehen, aber zunächst versuchen wir herauszufinden, was wir brauchen, damit alles in Gang kommt und stabil funktioniert.

GPUs haben die Leistung von x86-Prozessoren längst übertroffen

· Zweitens müssen die neuesten proprietären Treiber für die Grafikkarte im System installiert sein; sie unterstützen sowohl die nativen GPGPU-Technologien der Karte als auch offenes OpenCL.

· Und drittens, da Distributionsentwickler noch nicht damit begonnen haben, Anwendungspakete mit GPGPU-Unterstützung zu verteilen, müssen wir Anwendungen selbst erstellen, und dafür benötigen wir offizielle SDKs von Herstellern: CUDA Toolkit oder ATI Stream SDK. Sie enthalten die Header-Dateien und Bibliotheken, die zum Erstellen von Anwendungen erforderlich sind.

Installieren Sie das CUDA Toolkit

Folgen Sie dem Link oben und laden Sie das CUDA Toolkit für Linux herunter (Sie können aus mehreren Versionen wählen, für die Distributionen Fedora, RHEL, Ubuntu und SUSE gibt es Versionen sowohl für x86- als auch für x86_64-Architekturen). Darüber hinaus müssen Sie dort auch Treiberkits für Entwickler herunterladen (Entwicklertreiber für Linux stehen an erster Stelle auf der Liste).

Starten Sie das SDK-Installationsprogramm:

$ sudo sh cudatoolkit_4.0.17_linux_64_ubuntu10.10.run

Wenn die Installation abgeschlossen ist, fahren wir mit der Installation der Treiber fort. Fahren Sie dazu den X-Server herunter:

# sudo /etc/init.d/gdm stop

Öffnen Sie die Konsole und führen Sie das Treiberinstallationsprogramm aus:

$ sudo sh devdriver_4.0_linux_64_270.41.19.run

Nachdem die Installation abgeschlossen ist, starten Sie X:

Damit Anwendungen mit CUDA/OpenCL arbeiten können, legen wir den Pfad zum Verzeichnis mit CUDA-Bibliotheken in der Variablen LD_LIBRARY_PATH fest:

$ export LD_LIBRARY_PATH=/usr/local/cuda/lib64

Oder, wenn Sie die 32-Bit-Version installiert haben:

$ export LD_LIBRARY_PATH=/usr/local/cuda/lib32

Sie müssen außerdem den Pfad zu den CUDA-Header-Dateien angeben, damit der Compiler sie in der Anwendungserstellungsphase findet:

$ export C_INCLUDE_PATH=/usr/local/cuda/include

Jetzt können Sie mit der Erstellung von CUDA/OpenCL-Software beginnen.

Installieren Sie das ATI Stream SDK

Das Stream SDK erfordert keine Installation, daher kann das von der Website heruntergeladene AMD-Archiv einfach in ein beliebiges Verzeichnis entpackt werden ( beste Wahl wird /opt sein) und schreiben Sie den Pfad dorthin in dieselbe LD_LIBRARY_PATH-Variable:

$ wget http://goo.gl/CNCNo

$ sudo tar -xzf ~/AMD-APP-SDK-v2.4-lnx64.tgz -C /opt

$ export LD_LIBRARY_PATH=/opt/AMD-APP-SDK-v2.4-lnx64/lib/x86_64/

$ export C_INCLUDE_PATH=/opt/AMD-APP-SDK-v2.4-lnx64/include/

Wie beim CUDA Toolkit muss x86_64 auf 32-Bit-Systemen durch x86 ersetzt werden. Gehen Sie nun in das Stammverzeichnis und entpacken Sie das Archiv icd-registration.tgz (dies ist eine Art kostenloses Archiv). Lizenzschlüssel):

$ sudo tar -xzf /opt/AMD-APP-SDK-v2.4-lnx64/icd-registration.tgz - MIT /

Wir überprüfen die korrekte Installation/Funktionsweise des Pakets mit dem clinfo-Tool:

$ /opt/AMD-APP-SDK-v2.4-lnx64/bin/x86_64/clinfo

ImageMagick und OpenCL

OpenCL-Unterstützung ist in ImageMagick schon seit geraumer Zeit verfügbar, sie ist jedoch in keiner Distribution standardmäßig aktiviert. Daher müssen wir IM selbst aus dem Quellcode kompilieren. Daran ist nichts Kompliziertes, alles, was Sie brauchen, ist bereits im SDK enthalten, sodass für die Montage keine Installation zusätzlicher Bibliotheken von nVidia oder AMD erforderlich ist. Laden Sie also das Archiv mit den Quellen herunter/entpacken Sie es:

$ wget http://goo.gl/F6VYV

$ tar -xjf ImageMagick-6.7.0-0.tar.bz2

$ cd ImageMagick-6.7.0-0

$ sudo apt-get install build-essential

Wir starten den Konfigurator und holen uns seine Ausgabe für die OpenCL-Unterstützung:

$ LDFLAGS=-L$LD_LIBRARY_PATH ./configure | grep -e cl.h -e OpenCL

Die korrekte Ausgabe des Befehls sollte etwa so aussehen:

Überprüfung der CL/cl.h-Benutzerfreundlichkeit ... ja

Überprüfen der Anwesenheit von CL/cl.h... ja

Überprüfung auf CL/cl.h... ja

Überprüfen der Benutzerfreundlichkeit von OpenCL/cl.h... nein

Überprüfung der Anwesenheit von OpenCL/cl.h... nein

Suche nach OpenCL/cl.h... nein

Suche nach OpenCL-Bibliothek... -lOpenCL

Das Wort „Ja“ muss entweder in den ersten drei Zeilen oder in der zweiten (oder in beiden Optionen gleichzeitig) markiert werden. Ist dies nicht der Fall, wurde höchstwahrscheinlich die Variable C_INCLUDE_PATH nicht korrekt initialisiert. Wenn das Wort „Nein“ markiert ist letzte Linie, dann liegt das Problem in der Variablen LD_LIBRARY_PATH. Wenn alles in Ordnung ist, starten Sie den Build-/Installationsprozess:

$sudo make installieren sauber

Überprüfen wir, ob ImageMagick tatsächlich mit OpenCL-Unterstützung kompiliert wurde:

$ /usr/local/bin/convert -version | grep-Funktionen

Funktionen: OpenMP OpenCL

Nun messen wir den resultierenden Geschwindigkeitsgewinn. Die ImageMagick-Entwickler empfehlen hierfür den Convolve-Filter:

$ time /usr/bin/convert image.jpg -convolve "-1, -1, -1, -1, 9, -1, -1, -1, -1" image2.jpg

$ time /usr/local/bin/convert image.jpg -convolve "-1, -1, -1, -1, 9, -1, -1, -1, -1" image2.jpg

Einige andere Vorgänge, wie zum Beispiel die Größenänderung, sollten jetzt ebenfalls viel schneller funktionieren, aber Sie sollten nicht erwarten, dass ImageMagick mit der Verarbeitung von Grafiken in rasender Geschwindigkeit beginnt. Bisher wurde ein sehr kleiner Teil des Pakets mithilfe von OpenCL optimiert.

FlacCL (Flacuda)

FlacCL ist ein Encoder von Audiodateien im FLAC-Format, der bei seiner Arbeit die Fähigkeiten von OpenCL nutzt. Es ist im CUETools-Paket für Windows enthalten, kann aber dank Mono auch unter Linux verwendet werden. Um ein Archiv mit einem Encoder zu erhalten, führen Sie den folgenden Befehl aus:

$ mkdir flaccl && cd flaccl

$ wget www.cuetools.net/install/flaccl03.rar

$ sudo apt-get install unrar mono

$ unrar x fl accl03.rar

Damit das Programm die OpenCL-Bibliothek finden kann, erstellen wir einen symbolischen Link:

$ ln -s $LD_LIBRARY_PATH/libOpenCL.so libopencl.so

Lassen Sie uns nun den Encoder ausführen:

$ mono CUETools.FLACCL.cmd.exe music.wav

Wenn auf dem Bildschirm die Fehlermeldung „Fehler: Die angeforderte Kompilierungsgröße ist größer als die erforderliche Arbeitsgruppengröße von 32“ angezeigt wird, ist die Grafikkarte in unserem System zu schwach und die Anzahl der beteiligten Kerne sollte auf die angegebene Anzahl reduziert werden Verwenden Sie das „-- Flag Gruppengröße XX“, wobei XX die erforderliche Anzahl von Kernen ist.

Ich sage gleich, dass aufgrund der langen Initialisierungszeit von OpenCL nur auf ausreichend langen Strecken spürbare Gewinne erzielt werden können. FlacCL verarbeitet kurze Audiodateien fast mit der gleichen Geschwindigkeit wie seine herkömmliche Version.

oclHashcat oder schnelle Brute-Force

Wie ich bereits sagte, gehörten die Entwickler verschiedener Cracker und Brute-Force-Passwortsysteme zu den ersten, die ihren Produkten GPGPU-Unterstützung hinzufügten. Für Sie neue Technologie wurde zu einem wahren Heiligen Gral, der es ermöglichte, natürlich leicht parallelisierten Code problemlos auf die Schultern schneller GPU-Prozessoren zu übertragen. Daher ist es nicht verwunderlich, dass es inzwischen Dutzende verschiedener Implementierungen solcher Programme gibt. Aber in diesem Artikel werde ich nur über einen davon sprechen – oclHashcat.

oclHashcat ist ein Hacker, der Passwörter basierend auf ihrem Hash mit Extreme erraten kann hohe Geschwindigkeit, während die Leistung der GPU mithilfe von OpenCL genutzt wird. Glaubt man den auf der Projektwebsite veröffentlichten Messungen, beträgt die Geschwindigkeit der Auswahl von MD5-Passwörtern auf der nVidia GTX580 bis zu 15.800 Millionen Kombinationen pro Sekunde, wodurch oclHashcat in nur 9 Minuten ein achtstelliges Passwort mittlerer Komplexität finden kann.

Das Programm unterstützt die Algorithmen OpenCL und CUDA, MD5, md5($pass.$salt), md5(md5($pass)) und vBulletin< v3.8.5, SHA1, sha1($pass.$salt), хэши MySQL, MD4, NTLM, Domain Cached Credentials, SHA256, поддерживает распределенный подбор паролей с задействованием мощности нескольких машин.

7 $ x oclHashcat-0,25,7 z

$cd oclHashcat-0.25

Und führen Sie das Programm aus (wir verwenden eine Beispielliste von Hashes und ein Beispielwörterbuch):

$ ./oclHashcat64.bin example.hash ?l?l?l?l example.dict

oclHashcat öffnet den Text der Benutzervereinbarung, der Sie durch Eingabe von „JA“ zustimmen müssen. Danach beginnt der Suchvorgang, dessen Fortschritt durch Drücken von angezeigt werden kann . Um den Vorgang anzuhalten, klicken Sie auf

Wieder aufzunehmen - . Sie können auch eine direkte Aufzählung verwenden (z. B. von aaaaaaaa bis zzzzzzzz):

$ ./oclHashcat64.bin hash.txt ?l?l?l?l ?l?l?l?l

Und verschiedene Modifikationen des Wörterbuchs und der Direktsuchmethode sowie deren Kombinationen (Sie können dies in der Datei docs/examples.txt nachlesen). In meinem Fall betrug die Durchsuchungsgeschwindigkeit des gesamten Wörterbuchs 11 Minuten, während die direkte Suche (von aaaaaaaa bis zzzzzzzz) etwa 40 Minuten dauerte. Die durchschnittliche Geschwindigkeit der GPU (RV710-Chip) betrug 88,3 Millionen/s.

Schlussfolgerungen

Trotz vieler verschiedener Einschränkungen und der Komplexität der Softwareentwicklung ist GPGPU die Zukunft leistungsstarker Desktop-Computer. Aber das Wichtigste ist, dass Sie die Möglichkeiten dieser Technologie schon jetzt nutzen können, und das gilt nicht nur für Windows-Rechner, sondern auch für Linux.




Freunden erzählen