Occa: Nouvelle macro de qualificateur de noyau

Créé le 2 avr. 2020  ·  22Commentaires  ·  Source: libocca/occa

Il est souvent utile de donner au compilateur du noyau des informations supplémentaires sur le nombre de threads (éléments de travail) dans un bloc de threads (groupe de travail). Par exemple, nous pouvons donner au compilateur HIP une limite supérieure sur le nombre de threads dans un bloc de threads (disons 1024) comme suit :

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

En fait, pour la version actuelle de HIP, il est malheureusement nécessaire de le spécifier lorsque la taille du thread-block dépasse 256 (voir https://github.com/ROCm-Developer-Tools/HIP/issues/1310 )

CUDA a également le même attribut. Il existe également un argument supplémentaire pour le qualificatif des limites de lancement pour le nombre minimum de blocs de threads (https://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html#launch-bounds).

Les qualificateurs du noyau dans OpenCL sont légèrement différents (voir 6.7.2 de https://www.khronos.org/registry/OpenCL/specs/2.2/pdf/OpenCL_C.pdf )

Proposition v1 - dans un monde idéal où nous connaissons la taille du bloc de threads au moment de la compilation, OCCA ajoutera des qualificateurs de noyau pour les limites de lancement appropriées (CUDA, HIP) ou un indice de taille de groupe de travail (OpenCL).

Proposition v2 - S'il est trop compliqué de faire la proposition v1, il serait intéressant d'ajouter un attribut okl pour les limites de lancement @qualifier("taille intérieure", B) où B pourrait être une définition du compilateur. Cela serait étendu à __launch_bounds__(valeur de B) pour CUDA/HIP ou __attribute__((work_group_size_hint(valeur de B))) pour OpenCL. Une variante multi-dim serait également utile.

feature

Tous les 22 commentaires

Puisque cela semble spécifique à HIP/CUDA/OpenCL, qu'en est-il de le transmettre en tant que propriété de construction ?

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

Ce serait bien je pense.

Je viens de tomber sur ça. Je suis heureux de contribuer ceci mais j'ai une question. OCCA connaît-il les dimensions de la boucle interne au moment JIT ?

Je crois que les dimensions de la boucle peuvent être transmises en tant qu'arguments du noyau.

C'est vrai, mais je ne comprends pas pourquoi c'est utile. Je demande comment occa peut émettre un attribut __launch_bounds__ au moment du JIT. Il doit connaître les dimensions de la boucle à ce point. C'est orthogonal aux arguments du noyau, n'est-ce pas ?

Il dit que les dimensions des boucles internes peuvent être passées en arguments, c'est-à-dire que nous ne connaissons pas nécessairement les dimensions du threadblock au moment de la compilation JIT.

Ah d'accord. Je vois ce que vous dites maintenant @tcew. Bon point.

Voici ce que j'avais en tête :

<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 ne peut évidemment pas connaître les limites numériques de la boucle au temps JIT.

Cependant, il crée un lanceur qui définit les dimensions de la grille de fils :

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);
  }
}

Ainsi, l'utilisateur peut spécifier n'importe quelle taille pour les limites de la boucle lors de l'exécution.

Postes croisés.

SYCL utilise une syntaxe similaire :

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);
});

(Les tampons ont une plage qui leur est associée, ce qui évite d'avoir à passer en N)

La syntaxe SYCL sycl::range est adaptée d'OpenCL, elle-même adaptée de CUDA.

Dans votre exemple, la spécification des dimensions de filetage est distincte du corps du parallèle pour les boucles.

La syntaxe OCCA OKL est spécifiquement conçue pour amener les dimensions de la boucle et le code du corps dans un parallèle plus familier pour la syntaxe de la boucle.

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];
      }
    }
  }

Le code à l'intérieur du noyau est censé maintenir les limites de la boucle for parallèle à proximité du corps des boucles for parallèles. De plus, la boucle liée n'est spécifiée par aucun tableau d'entrée, car un noyau plus général peut nécessiter une configuration de grille de threads très différente des tableaux de données.

Les constructions du noyau OKL étaient un choix intentionnel né de devoir expliquer à plusieurs reprises la syntaxe du noyau CUDA/OpenCL, les paramètres de lancement du noyau et la philosophie de threading du noyau lors de la formation des personnes.

SYCL a-t-il un indice de taille de bloc de thread analogue comme le __launch_bounds__ CUDA/HIP dont nous discutons ici ?

Bonne question. J'ai revérifié la norme SYCL (v1.2.1) pour le savoir. Tous les attributs disponibles dans OpenCL C sont pris en charge et peuvent être donnés avec le spécificateur d'attribut C++ 11 en utilisant l'espace de noms cl . Par exemple, __attribute__(((reqd_work_group_size(n))) dans OpenCL C équivaut à [[cl::reqd_work_group_size(n)]] dans SYCL.

Il existe deux variantes disponibles pour spécifier la taille du groupe de threads : work_group_size_hint(n) est la version logicielle, ce qui suggère que la taille du groupe de threads sera n , tandis req_work_group_size(n) est une exigence stricte. .

Quelques options pour OCCA :

  1. ajoutez une fonction membre "innerDimHint" à la classe occa::kernel, qui force une recompilation (si elle n'est pas déjà dans le hachage) avec un indice de fil conducteur pour CUDA/OpenCL/HIP.

  2. ajouter une logique à l'intérieur du lanceur pour déclencher une recompilation effectuée lorsqu'une nouvelle taille de tableau de threads est spécifiée. Cette fonctionnalité peut être activée/désactivée par une définition OCCA_ * .

Ces deux opérations peuvent être effectuées de manière rétrocompatible.

En fait, je ferais confiance aux runtimes séparés pour gérer cela et choisirais de ne rien recompiler. Essentiellement, utilisez la proposition originale de @ dmed256 d'en faire un accessoire de construction, puis d'ajouter l'indice respectif __launch_bound__ au noyau lors de la traduction si le backend le prend en charge.

Pour CUDA et HIP, __launch_bound__ n'est vraiment qu'un indice, donc peut-être plus proche de work_group_size_hint d'OpenCL. Il est uniquement utilisé pour indiquer au compilateur combien de registres il peut supposer être disponibles pour chaque thread du bloc. Si l'utilisateur ne respecte pas la limite de lancement, ce n'est pas nécessairement une erreur, car le noyau n'utilise peut-être pas beaucoup register. Dans le cas où l'utilisateur viole la limite de lancement et qu'il n'y a effectivement pas assez de registre, le runtime génère une erreur que OCCA doit intercepter.

Lors de l'exécution d'un tuner automatique, j'ai remarqué des défauts de segmentation pour les noyaux OCCA:HIP qui nécessitaient trop de LDS ou de REG. J'étais sceptique quant au fait que HIP lancera des erreurs qui seront détectées. Espérons que cela a été corrigé maintenant.

Je suis d'accord avec un indice fourni par l'utilisateur ou un lanceur qui repère les nouvelles configurations de threads au moment de l'exécution.

Pour info, j'ai été heureux comme une alouette juste en spécifiant des limites de boucle d'exécution explicites à partir du code hôte via une substitution de constante de macro dans le code du noyau OKL (par exemple, K_blockDim pour l'extérieur, K_threadDim pour l'intérieur) que je transmets via les accessoires du noyau. Bien sûr, j'ai mon propre code API qui interroge les caractéristiques et l'état du GPU. J'utilise ces informations pour calculer les limites de boucle à transmettre. Oui, ce genre de chose dynamique provoque parfois une recompilation JIT pendant l'exécution, mais dans mon cas, c'est rare car je change de seuil, de sorte que les limites réelles de la boucle restent généralement les mêmes ou tombent dans un ensemble commun pour un noyau donné. J'utilise également la pré-compilation pour certains de mes noyaux, ce qui réduit également la recompilation JIT.

La définition explicite de ces limites de boucle lors de l'exécution facilite également le dimensionnement à l'exécution des matrices de mémoire locale GPU avec elles dans le code OKL.

Il semble que cette fonctionnalité va être importante :
https://rocmdocs.amd.com/en/latest/Current_Release_Notes/Current-Release-Notes.html#performance -impact-for-kernel-launch-bound-attribute

Avec mes excuses pour l'implémentation maladroite, voici une solution de contournement que j'utilise pour un noyau dans libparanumal qui nécessite Np (> 256) threads :

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;
    }

L'utilisation du drapeau du compilateur hipcc pour spécifier les limites de lancement a été suggérée par Noel Chalmers. La mise en œuvre brute est la mienne.

Il est important d'être prudent lors de cette opération car on ne sait pas ce qui se passe si un noyau viole la limite maximale.

Pour éviter d'utiliser des limites inappropriées, je crée des copies séparées de l'objet occa::properties pour les noyaux qui utilisent différents nombres de threads maximum.

Il est probable qu'à l'avenir, la violation des limites de lancement deviendra une erreur d'exécution.

Cette page vous a été utile?
0 / 5 - 0 notes