Occa: Nova macro do qualificador do kernel

Criado em 2 abr. 2020  ·  22Comentários  ·  Fonte: libocca/occa

Muitas vezes é útil fornecer ao compilador do kernel algumas informações extras sobre o número de threads (work-items) em um bloco de threads (work-group). Por exemplo, podemos dar ao compilador HIP um limite superior para o número de threads em um bloco de threads (digamos 1024) da seguinte forma:

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

De fato, para a versão atual do HIP, infelizmente, isso deve ser especificado quando o tamanho do bloco de threads excede 256 (consulte https://github.com/ROCm-Developer-Tools/HIP/issues/1310 )

CUDA também tem o mesmo atributo. Há também um argumento extra para o qualificador de limites de inicialização para o número mínimo de blocos de encadeamento (https://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html#launch-bounds).

Os qualificadores do kernel no OpenCL são ligeiramente diferentes (consulte 6.7.2 de https://www.khronos.org/registry/OpenCL/specs/2.2/pdf/OpenCL_C.pdf )

Proposta v1 - em um mundo ideal, quando sabemos o tamanho do bloco de thread em tempo de compilação, a OCCA adicionará qualificadores de kernel para limites de inicialização apropriados (CUDA, HIP) ou dica de tamanho de grupo de trabalho (OpenCL).

Proposta v2 - Se for muito complicado fazer Proposta v1, então seria legal adicionar um atributo okl para os limites de inicialização @qualifier("tamanhos internos", B) onde B poderia ser uma definição do compilador. Isso seria expandido para __launch_bounds__(valor de B) para CUDA/HIP ou __attribute__((work_group_size_hint(valor de B))) para OpenCL. A variante multi-dim também seria útil.

feature

Todos 22 comentários

Como isso parece específico para HIP/CUDA/OpenCL, que tal passá-lo como uma propriedade de compilação?

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

Isso seria bom eu acho.

Acabei de me deparar com isso. Fico feliz em contribuir com isso, mas tenho uma pergunta. O OCCA conhece as dimensões do loop interno em tempo JIT?

Eu acredito que as dimensões do loop podem ser passadas como argumentos do kernel.

Isso é verdade, mas não entendo por que isso é útil. Estou perguntando como occa pode emitir um atributo __launch_bounds__ em tempo JIT. Ele precisa saber as dimensões do loop nesse ponto. Isso é ortogonal aos argumentos do kernel, não é?

Ele está dizendo que os dims dos loops internos podem ser passados ​​como argumentos, ou seja, não sabemos necessariamente as dimensões do bloco de threads em tempo de compilação JIT.

Oh, certo. Eu vejo o que você está dizendo agora @tcew. Bom ponto.

Isto é o que eu tinha em 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];
      }
    }
  }
}

O OCCA obviamente não pode conhecer os limites numéricos do loop em tempo JIT.

No entanto, cria um lançador que define as dimensões da grade de threads:

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

Assim, o usuário pode especificar qualquer tamanho para os limites do loop em tempo de execução.

Postagens cruzadas.

SYCL usa sintaxe semelhante:

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

(Os buffers têm um intervalo associado a eles, o que evita a necessidade de passar em N)

A sintaxe SYCL sycl::range é adaptada do OpenCL, que por sua vez é adaptado do CUDA.

No seu exemplo, a especificação das dimensões de rosqueamento é separada do corpo dos loops paralelos.

A sintaxe OCCA OKL foi projetada especificamente para trazer as dimensões do loop e o código do corpo para uma sintaxe de loop paralela mais familiar.

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

O código dentro do kernel deve manter os limites do loop for paralelo próximo ao corpo dos loops for paralelos. Além disso, o limite de loop não é especificado por nenhum array de entrada, pois um kernel mais geral pode exigir uma configuração de grade de thread muito diferente dos arrays de dados.

As construções do kernel OKL foram uma escolha intencional nascida de ter que explicar repetidamente a sintaxe do kernel CUDA/OpenCL, os parâmetros de inicialização do kernel e a filosofia de threading do kernel ao treinar pessoas.

O SYCL tem uma dica de tamanho de bloco de thread análoga como __launch_bounds__ do CUDA/HIP que estamos discutindo aqui?

Boa pergunta. Eu verifiquei o padrão SYCL (v1.2.1) para descobrir. Todos os atributos disponíveis em OpenCL C são suportados e podem ser fornecidos com o especificador de atributo C++ 11 usando o namespace cl . Por exemplo, __attribute__(((reqd_work_group_size(n))) em OpenCL C é equivalente a [[cl::reqd_work_group_size(n)]] em SYCL.

Existem dois tipos disponíveis para especificar o tamanho do grupo de threads: work_group_size_hint(n) é a versão soft—sugerindo que o tamanho do grupo de threads será n —enquanto req_work_group_size(n) é um requisito estrito .

Algumas opções para OCCA:

  1. adicione uma função de membro "innerDimHint" à classe occa::kernel, que força uma recompilação (se ainda não estiver no hash) com dica de dim de thread para CUDA/OpenCL/HIP.

  2. adicione alguma lógica dentro do iniciador para acionar uma recompilação feita quando um novo tamanho de matriz de thread for especificado. Este recurso pode ser ativado/desativado por alguma definição de OCCA_ * .

Ambos podem ser feitos de maneira compatível com versões anteriores.

Eu realmente confiaria nos tempos de execução separados para gerenciar isso e optaria por não recompilar nada. Em essência, use a proposta original do @dmed256 de torná-lo um suporte de compilação e, em seguida, adicione a respectiva dica __launch_bound__ ao kernel na tradução, se o backend suportar.

Para CUDA e HIP, __launch_bound__ é apenas uma dica, então talvez mais parecido com o work_group_size_hint do OpenCL. É usado apenas para informar ao compilador quantos registros ele pode assumir que estarão disponíveis para cada thread no bloco. Se o usuário violar o limite de inicialização, não é necessariamente um erro, pois o kernel pode não estar usando muito o registro. No caso em que o usuário violar o limite de inicialização e não houver registro suficiente, o tempo de execução lançará um erro que o OCCA deve capturar.

Ao executar um sintonizador automático, notei falhas de segmentação para kernels OCCA:HIP que exigiam muito LDS ou REG. Eu estava cético de que o HIP lançaria erros que seriam detectados. Espero que isso tenha sido corrigido agora.

Estou bem com uma dica fornecida pelo usuário ou um iniciador que detecta novas configurações de thread em tempo de execução.

Para sua informação, fiquei feliz como uma cotovia apenas especificando limites de loop de tempo de execução explícitos do código do host por meio de substituição constante de macro no código do kernel OKL (por exemplo, K_blockDim para externo, K_threadDim para interno) que eu passo para baixo através dos adereços do kernel. Claro, eu tenho meu próprio código de API que consulta as características e o status da GPU. Eu uso essa informação para calcular os limites de loop a serem transmitidos. Sim, esse tipo de coisa dinâmica faz com que o JIT recompile algumas vezes durante o tempo de execução, mas no meu caso é raro porque eu altero o limite de etapas, então os limites reais do loop geralmente permanecem os mesmos ou caem em algum conjunto comum para um determinado kernel. Eu também utilizo pré-compilação para alguns dos meus kernels para que também reduza a recompilação JIT.

Ter esses limites de loop definidos explicitamente em tempo de execução também facilita o dimensionamento de matrizes de memória local da GPU em tempo de execução com eles no código OKL.

Parece que esse recurso será importante:
https://rocmdocs.amd.com/en/latest/Current_Release_Notes/Current-Release-Notes.html#performance -impact-for-kernel-launch-bound-attribute

Com desculpas pela implementação desajeitada, esta é uma solução alternativa que uso para um kernel em libparanumal que requer threads 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;
    }

O uso do sinalizador do compilador hipcc para especificar os limites de inicialização foi sugerido por Noel Chalmers. A implementação bruta é minha.

É importante ter cuidado ao fazer isso, pois não está claro o que acontece se um kernel violar o limite máximo.

Para evitar o uso de limites inadequados, crio cópias separadas do objeto occa::properties para kernels que usam contagens máximas de thread diferentes.

É provável que, no futuro, a violação dos limites de inicialização se torne um erro de tempo de execução.

Esta página foi útil?
0 / 5 - 0 avaliações

Questões relacionadas

amikstcyr picture amikstcyr  ·  11Comentários

awehrfritz picture awehrfritz  ·  7Comentários

dmed256 picture dmed256  ·  4Comentários

jeremylt picture jeremylt  ·  12Comentários

tcew picture tcew  ·  10Comentários