Occa: Nueva macro calificadora de kernel

Creado en 2 abr. 2020  ·  22Comentarios  ·  Fuente: libocca/occa

A menudo es útil darle al compilador del kernel información adicional sobre el número de subprocesos (elementos de trabajo) en un bloque de subprocesos (grupo de trabajo). Por ejemplo, podemos darle al compilador HIP un límite superior en el número de subprocesos en un bloque de subprocesos (digamos 1024) de la siguiente manera:

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

De hecho, para la versión actual de HIP, lamentablemente se debe especificar cuando el tamaño del bloque de subprocesos supera los 256 (consulte https://github.com/ROCm-Developer-Tools/HIP/issues/1310)

CUDA también tiene el mismo atributo. También hay un argumento adicional para el calificador de límites de lanzamiento para la cantidad mínima de bloques de subprocesos (https://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html#launch-bounds).

Los calificadores del kernel en OpenCL son ligeramente diferentes (ver 6.7.2 de https://www.khronos.org/registry/OpenCL/specs/2.2/pdf/OpenCL_C.pdf)

Propuesta v1 : en un mundo ideal cuando conocemos el tamaño del bloque de subprocesos en el momento de la compilación, OCCA agregará calificadores de kernel para los límites de lanzamiento apropiados (CUDA, HIP) o sugerencias de tamaño de grupo de trabajo (OpenCL).

Propuesta v2 : si es demasiado complicado hacer la Propuesta v1, sería bueno agregar un atributo okl para los límites de lanzamiento @qualifier ("tamaños internos", B) donde B podría ser una definición del compilador. Esto se expandiría a __launch_bounds__(valor de B) para CUDA/HIP o __attribute__((work_group_size_hint(valor de B))) para OpenCL. La variante multi-dim también sería útil.

feature

Todos 22 comentarios

Dado que esto parece específico de HIP/CUDA/OpenCL, ¿qué hay de pasarlo como una propiedad de compilación?

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

Eso sería bueno, creo.

Acabo de encontrarme con esto. Estoy feliz de contribuir con esto, pero tengo una pregunta. ¿OCCA conoce las dimensiones del bucle interno en tiempo JIT?

Creo que las dimensiones del ciclo se pueden pasar como argumentos del núcleo.

Eso es cierto, pero no entiendo por qué eso es útil. Estoy preguntando cómo occa puede emitir un atributo __launch_bounds__ en tiempo JIT. Necesita saber las dimensiones del bucle en ese punto. Esto es ortogonal a los argumentos del núcleo, ¿no?

Dice que las dimensiones de los bucles internos se pueden pasar como argumentos, es decir, no conocemos necesariamente las dimensiones del bloque de subprocesos en el momento de la compilación JIT.

Correcto. Veo lo que estás diciendo ahora @tcew. Buen punto.

Esto es lo que tenía en mente:

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

Obviamente, OCCA no puede conocer los límites numéricos del bucle en el tiempo JIT.

Sin embargo, crea un lanzador que establece las dimensiones de la cuadrícula de subprocesos:

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

Por lo tanto, el usuario puede especificar cualquier tamaño para los límites del bucle en tiempo de ejecución.

Postes cruzados.

SYCL usa una sintaxis similar:

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

(Los búferes tienen un rango asociado a ellos, lo que evita la necesidad de pasar en N)

La sintaxis SYCL sycl::range está adaptada de OpenCL, que a su vez está adaptada de CUDA.

En su ejemplo, la especificación de las dimensiones de subprocesamiento está separada del cuerpo del bucle for paralelo.

La sintaxis OKL de OCCA está diseñada específicamente para llevar las dimensiones del bucle y el código del cuerpo a un paralelo más familiar para la sintaxis del bucle.

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

Se supone que el código dentro del núcleo mantiene los límites del bucle for paralelo muy cerca del cuerpo del bucle for paralelo. Además, el límite del bucle no está especificado por ninguna matriz de entrada, ya que un kernel más general puede requerir una configuración de cuadrícula de subprocesos muy diferente a la de las matrices de datos.

Las construcciones del kernel OKL fueron una elección intencional que surgió de tener que explicar repetidamente la sintaxis del kernel CUDA/OpenCL, los parámetros de lanzamiento del kernel y la filosofía de subprocesamiento del kernel al capacitar a las personas.

¿SYCL tiene una sugerencia de tamaño de bloque de hilo análoga como __launch_bounds__ de CUDA/HIP que estamos discutiendo aquí?

Buena pregunta. Revisé dos veces el estándar SYCL (v1.2.1) para averiguarlo. Todos los atributos disponibles en OpenCL C son compatibles y se pueden proporcionar con el especificador de atributos de C++ 11 utilizando el espacio de nombres cl . Por ejemplo, __attribute__(((reqd_work_group_size(n))) en OpenCL C es equivalente a [[cl::reqd_work_group_size(n)]] en SYCL.

Hay dos sabores disponibles para especificar el tamaño del grupo de subprocesos: work_group_size_hint(n) es la versión suave, lo que sugiere que el tamaño del grupo de subprocesos será n mientras que req_work_group_size(n) es un requisito estricto .

Algunas opciones para OCCA:

  1. agregue una función de miembro "innerDimHint" a la clase occa::kernel, que fuerza una recompilación (si aún no está en el hash) con una sugerencia de atenuación de subprocesos para CUDA/OpenCL/HIP.

  2. agregue algo de lógica dentro del iniciador para desencadenar una recompilación cuando se especifica un nuevo tamaño de matriz de subprocesos. Esta función puede ser activada/desactivada por alguna definición de OCCA_ * .

Ambos se pueden hacer de forma compatible con versiones anteriores.

De hecho, confiaría en los tiempos de ejecución separados para administrar esto y optaría por no volver a compilar nada. En esencia, use la propuesta original de @ dmed256 de convertirlo en un complemento de compilación y luego agregue la sugerencia respectiva __launch_bound__ al kernel en la traducción si el backend lo admite.

Para CUDA y HIP, __launch_bound__ es realmente solo una pista, por lo que tal vez se parezca más a work_group_size_hint de OpenCL. Solo se usa para decirle al compilador cuántos registros puede suponer que estarán disponibles para cada subproceso en el bloque. Si el usuario viola el límite de lanzamiento, no es necesariamente un error, ya que es posible que el kernel no esté usando mucho el registro. En el caso de que el usuario infrinja el límite de inicio y, de hecho, no haya suficiente registro, el tiempo de ejecución generará un error que OCCA debería detectar.

Al ejecutar un sintonizador automático, noté fallas de segmentación para los núcleos OCCA: HIP que requerían demasiado LDS o REG. Era escéptico de que HIP lanzará errores que serán detectados. Esperemos que eso se haya arreglado ahora.

Estoy de acuerdo con una sugerencia proporcionada por el usuario o un iniciador que detecta nuevas configuraciones de subprocesos en tiempo de ejecución.

FYI, he sido feliz como una alondra simplemente especificando límites de bucle de tiempo de ejecución explícitos desde el código del host a través de la sustitución constante de macros en el código del kernel OKL (por ejemplo, K_blockDim para el exterior, K_threadDim para el interior) que transmito a través de los accesorios del núcleo. Por supuesto, tengo mi propio código API que consulta las características y el estado de la GPU. Utilizo esa información para calcular los límites del bucle para transmitir. Sí, ese tipo de cosas dinámicas hacen que JIT se vuelva a compilar a veces durante el tiempo de ejecución, pero en mi caso es raro porque paso los cambios de umbral, por lo que los límites reales del bucle generalmente siguen siendo los mismos o caen en algún conjunto común para un kernel determinado. También utilizo la precompilación para algunos de mis núcleos, por lo que también reduce la recompilación JIT.

Tener estos límites de bucle definidos explícitamente en el tiempo de ejecución también facilita el dimensionamiento del tiempo de ejecución de las matrices de memoria local de la GPU con ellos en el código OKL.

Con disculpas por la implementación torpe, esta es una solución que uso para un kernel en libparanumal que requiere subprocesos Np (> 256):

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

Noel Chalmers sugirió usar el indicador del compilador hipcc para especificar los límites de lanzamiento. La implementación bruta es mía.

Es importante tener cuidado al hacer esto, ya que no está claro qué sucede si un kernel viola el límite máximo.

Para evitar el uso de límites inapropiados, creo copias separadas del objeto occa::properties para kernels que usan diferentes recuentos máximos de subprocesos.

Es probable que en el futuro, la violación de los límites de lanzamiento se convierta en un error de tiempo de ejecución.

¿Fue útil esta página
0 / 5 - 0 calificaciones

Temas relacionados

jeremylt picture jeremylt  ·  12Comentarios

tcew picture tcew  ·  10Comentarios

dmed256 picture dmed256  ·  4Comentarios

awehrfritz picture awehrfritz  ·  7Comentarios

amikstcyr picture amikstcyr  ·  11Comentarios