Occa: Neues Kernel-Qualifizierer-Makro

Erstellt am 2. Apr. 2020  ·  22Kommentare  ·  Quelle: libocca/occa

Oft ist es hilfreich, dem Kernel-Compiler zusätzliche Informationen über die Anzahl der Threads (Work-Items) in einem Thread-Block (Work-Group) zu geben. Beispielsweise können wir dem HIP-Compiler wie folgt eine Obergrenze für die Anzahl der Threads in einem Thread-Block (z. B. 1024) geben:

__launch_bounds__(1024) __global__ void fooKernel(...) { ... }

Tatsächlich ist es für die aktuelle HIP-Version leider so, dass dies angegeben werden muss, wenn die Thread-Block-Größe 256 überschreitet (siehe https://github.com/ROCm-Developer-Tools/HIP/issues/1310 )

CUDA hat auch das gleiche Attribut. Es gibt auch ein zusätzliches Argument für den Qualifizierer für Startgrenzen für die Mindestanzahl von Threadblöcken (https://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html#launch-bounds).

Die Kernel-Qualifizierer in OpenCL sind etwas anders (siehe 6.7.2 von https://www.khronos.org/registry/OpenCL/specs/2.2/pdf/OpenCL_C.pdf )

Vorschlag v1 – in einer idealen Welt, wenn wir die Thread-Blockgröße zur Kompilierzeit kennen, wird OCCA Kernel-Qualifizierer für geeignete Startgrenzen (CUDA, HIP) oder Hinweise zur Arbeitsgruppengröße (OpenCL) hinzufügen.

Vorschlag v2 - Wenn es zu kompliziert ist, Vorschlag v1 zu machen, dann wäre es nett, ein okl-Attribut für Startgrenzen @qualifier("innere Größen", B) hinzuzufügen, wobei B eine Compiler-Definition sein könnte. Dies würde auf __launch_bounds__(Wert von B) für CUDA/HIP oder __attribute__((work_group_size_hint(value of B))) für OpenCL erweitert. Multi-Dim-Variante wäre auch hilfreich.

feature

Alle 22 Kommentare

Da dies spezifisch für HIP/CUDA/OpenCL zu sein scheint, was ist mit der Übergabe als Build-Eigenschaft?

  addVectors = device.buildKernel("addVectors.okl",
                                  "addVectors",
                                  "launch_bounds: 1024");

Das wäre gut, denke ich.

Ich bin gerade darauf gestoßen. Ich freue mich, dies beizutragen, aber ich habe eine Frage. Kennt OCCA die Dimensionen der inneren Schleife zur JIT-Zeit?

Ich glaube, die Loop-Dimensionen können als Kernel-Argumente übergeben werden.

Das stimmt, aber ich verstehe nicht, warum das nützlich ist. Ich frage, wie Occa zur JIT-Zeit ein __launch_bounds__ -Attribut ausgeben kann. Es muss die Schleifenabmessungen an diesem Punkt kennen. Das ist orthogonal zu den Kernel-Argumenten, nicht wahr?

Er sagt, dass die Dims der inneren Schleifen als Argumente übergeben werden können, dh wir kennen nicht unbedingt die Threadblock-Dimensionen zur JIT-Kompilierungszeit.

Oh, richtig. Ich verstehe jetzt, was du sagst @tcew. Guter Punkt.

Das hatte ich im Sinn:

<strong i="6">@kernel</strong> void runtimeArgs(const int B,
                         const int T,
                         const int N,
                         const float *x,
                         const float *y,
                         float *xPy) {
  for (int b=0;b<B;++b;@outer(0)){
    for (int t=0;t<T;++t;@inner(0)){

      if(b==0 && t==0) printf("B=%d, T=%d\n", B, T);

      int  n = t + T*b;
      if(n<N){
        xPy[n] = x[n] + y[n];
      }
    }
  }
}

OCCA kann offensichtlich die numerischen Schleifengrenzen zur JIT-Zeit nicht kennen.

Es erstellt jedoch einen Launcher, der die Abmessungen des Thread-Rasters festlegt:

extern "C" void runtimeArgs(occa::modeKernel_t * *deviceKernel,
                            const int & B,
                            const int & T,
                            const int & N,
                            occa::modeMemory_t * x,
                            occa::modeMemory_t * y,
                            occa::modeMemory_t * xPy) {
  {
    occa::dim outer, inner;
    outer.dims = 1;
    inner.dims = 1;
    int b = 0;
    outer[0] = B - 0;
    int t = 0;
    inner[0] = T - 0;
    occa::kernel kernel(deviceKernel[0]);
    kernel.setRunDims(outer, inner);
    kernel(B, T, N, x, y, xPy);
  }
}

Somit kann der Benutzer zur Laufzeit eine beliebige Größe für die Schleifengrenzen festlegen.

Gekreuzte Beiträge.

SYCL verwendet eine ähnliche Syntax:

sycl::range<2> global_range(Bx*Tx,By*Ty);
sycl::range<2> local_range(Tx,Ty);
sycl::nd_range<2> kernel_range(global_range, local_range);

device_queue.submit([&](sycl::handler &cgh) {
    ...
  cgh.parallel_for(kernel_range, kernel);
});

(Puffer haben einen ihnen zugeordneten Bereich, was die Notwendigkeit vermeidet, N zu übergeben)

Die SYCL sycl::range -Syntax wird von OpenCL übernommen, das wiederum von CUDA übernommen wird.

In Ihrem Beispiel ist die Angabe der Threading-Dimensionen vom Körper der parallelen for-Schleifen getrennt.

Die OCCA OKL-Syntax wurde speziell entwickelt, um die Loop-Dimensionen und den Body-Code in eine vertrautere parallele For-Loop-Syntax zu bringen.

for (int b=0;b<B;++b;@outer(0)){ /*  grid dimension defined here */
    for (int t=0;t<T;++t;@inner(0)){ /* thread block dimension defined here */

      if(b==0 && t==0) printf("B=%d, T=%d\n", B, T);

      int  n = t + T*b;
      if(n<N){
        xPy[n] = x[n] + y[n];
      }
    }
  }

Der Code im Kernel soll die Grenzen der parallelen for-Schleife in unmittelbarer Nähe zum Hauptteil der parallelen for-Schleifen halten. Auch die Schleifengrenze wird durch kein Eingabearray angegeben, da ein allgemeinerer Kernel möglicherweise eine ganz andere Thread-Grid-Konfiguration als die Datenarrays erfordert.

Die OKL-Kernel-Konstruktion war eine bewusste Entscheidung, die entstand, weil die CUDA/OpenCL-Kernel-Syntax, die Kernel-Startparameter und die Kernel-Threading-Philosophie bei der Schulung von Leuten wiederholt erklärt werden mussten.

Hat SYCL einen analogen Thread-Block-Größenhinweis wie __launch_bounds__ CUDA/HIP, den wir hier diskutieren?

Gute Frage. Ich habe den SYCL-Standard (v1.2.1) doppelt überprüft, um das herauszufinden. Alle in OpenCL C verfügbaren Attribute werden unterstützt und können mit dem C++ 11-Attributbezeichner unter Verwendung des Namensraums cl angegeben werden. Beispielsweise entspricht __attribute__(((reqd_work_group_size(n))) in OpenCL C [[cl::reqd_work_group_size(n)]] in SYCL.

Es gibt zwei Varianten, um die Thread-Gruppengröße anzugeben: work_group_size_hint(n) ist die Soft-Version – die vorschlägt, dass die Thread-Gruppengröße n sein wird – während req_work_group_size(n) eine strikte Anforderung ist .

Einige Optionen für OCCA:

  1. Fügen Sie der occa::kernel-Klasse eine Member-Funktion „innerDimHint“ hinzu, die eine Neukompilierung (falls nicht bereits im Hash enthalten) mit Thread-Dim-Hinweis für CUDA/OpenCL/HIP erzwingt.

  2. Fügen Sie dem Launcher eine Logik hinzu, um eine Neukompilierung auszulösen, wenn eine neue Thread-Array-Größe angegeben wird. Diese Funktion kann durch einige OCCA_ * -Definitionen ein-/ausgeschaltet werden.

Beides kann abwärtskompatibel erfolgen.

Ich würde tatsächlich den separaten Laufzeiten vertrauen, um dies zu verwalten, und mich dafür entscheiden, nichts neu zu kompilieren. Verwenden Sie im Wesentlichen den ursprünglichen Vorschlag von @ dmed256 , es zu einer Build-Prop zu machen, und fügen Sie dann den entsprechenden __launch_bound__ -Hinweis zum Kernel bei der Übersetzung hinzu, wenn das Backend dies unterstützt.

Für CUDA und HIP ist __launch_bound__ eigentlich nur ein Hinweis, also vielleicht eher work_group_size_hint von OpenCL. Es wird nur verwendet, um dem Compiler mitzuteilen, wie viele Register er für jeden Thread im Block zur Verfügung haben kann. Wenn der Benutzer gegen die Startgrenze verstößt, ist dies nicht unbedingt ein Fehler, da der Kernel möglicherweise nicht stark register verwendet. Für den Fall, dass der Benutzer gegen die Startgrenze verstößt und tatsächlich nicht genügend Register vorhanden sind, gibt die Laufzeit einen Fehler aus, den OCCA abfangen sollte.

Beim Ausführen eines Autotuners habe ich Segmentierungsfehler für OCCA:HIP- Kernel festgestellt, die zu viel LDS oder REG erforderten. Ich war skeptisch, dass HIP Fehler wirft, die abgefangen werden. Hoffentlich ist das jetzt behoben.

Ich bin mit einem vom Benutzer bereitgestellten Hinweis oder einem Launcher einverstanden, der zur Laufzeit neue Thread-Konfigurationen erkennt.

Zu Ihrer Information, ich war glücklich darüber, nur explizite Laufzeitschleifengrenzen aus dem Hostcode über die Substitution von Makrokonstanten im OKL-Kernelcode (z. B. K_blockDim für äußere, K_threadDim für innere) anzugeben, die ich weitergebe über die Kernel-Props. Natürlich habe ich meinen eigenen API-Code, der die Eigenschaften und den Status der GPU abfragt. Ich verwende diese Informationen, um die Schleifengrenzen zu berechnen, die weitergegeben werden sollen. Ja, diese Art von dynamischer Sache führt manchmal zur Neukompilierung von JIT während der Laufzeit, aber in meinem Fall ist es selten, weil ich die Schrittschwellenwerte ändere, sodass die tatsächlichen Schleifengrenzen normalerweise gleich bleiben oder in einen gemeinsamen Satz für einen bestimmten Kernel fallen. Ich verwende auch die Vorkompilierung für einige meiner Kernel, sodass auch die JIT-Neukompilierung reduziert wird.

Die explizite Definition dieser Schleifengrenzen zur Laufzeit erleichtert auch die Laufzeitdimensionierung lokaler GPU-Speicherarrays mit ihnen im OKL-Code.

Sieht so aus, als ob diese Funktion wichtig sein wird:
https://rocmdocs.amd.com/en/latest/Current_Release_Notes/Current-Release-Notes.html#performance -impact-for-kernel-launch-bound-attribute

Mit Entschuldigung für die ungeschickte Implementierung ist dies eine Problemumgehung, die ich für einen Kernel in libparanumal verwende, der Np (> 256) Threads erfordert:

occa::properties kernelInfo; 
...
 if(platform.device.mode()=="HIP"){
      char newflag[BUFSIZ];
      sprintf(newflag, " --gpu-max-threads-per-block=%d", mesh.Np);
      kernelInfo["compiler_flags"] += newflag;
    }

Die Verwendung des hipcc-Compiler-Flags zur Angabe der Startgrenzen wurde von Noel Chalmers vorgeschlagen. Die grobe Umsetzung ist von mir.

Es ist wichtig, dabei vorsichtig zu sein, da unklar ist, was passiert, wenn ein Kernel die Max-Grenze verletzt.

Um die Verwendung unangemessener Grenzen zu vermeiden, erstelle ich separate Kopien des occa::properties -Objekts für Kernel, die unterschiedliche maximale Thread-Anzahlen verwenden.

Es ist wahrscheinlich, dass die Verletzung der Startgrenzen in Zukunft zu einem Laufzeitfehler wird.

War diese Seite hilfreich?
0 / 5 - 0 Bewertungen