Programação GPU com C ++

Gpu Programming With C



Neste guia, exploraremos o poder da programação de GPU com C ++. Os desenvolvedores podem esperar um desempenho incrível com C ++, e acessar o poder fenomenal da GPU com uma linguagem de baixo nível pode render alguns dos cálculos mais rápidos disponíveis atualmente.

Requisitos

Embora qualquer máquina capaz de executar uma versão moderna do Linux possa suportar um compilador C ++, você precisará de uma GPU baseada em NVIDIA para acompanhar este exercício. Se você não tem uma GPU, pode ativar uma instância com GPU no Amazon Web Services ou em outro provedor de nuvem de sua escolha.







Se você escolher uma máquina física, certifique-se de ter os drivers proprietários NVIDIA instalados. Você pode encontrar instruções para isso aqui: https://linuxhint.com/install-nvidia-drivers-linux/



Além do driver, você precisará do kit de ferramentas CUDA. Neste exemplo, usaremos Ubuntu 16.04 LTS, mas há downloads disponíveis para a maioria das principais distribuições no seguinte URL: https://developer.nvidia.com/cuda-downloads



Para o Ubuntu, você deve escolher o download baseado em .deb. O arquivo baixado não terá uma extensão .deb por padrão, então eu recomendo renomeá-lo para ter uma extensão .deb no final. Então, você pode instalar com:





sudo dpkg -eunome-do-pacote.deb

Provavelmente, você será solicitado a instalar uma chave GPG e, em caso afirmativo, siga as instruções fornecidas para fazer isso.

Depois de fazer isso, atualize seus repositórios:



sudo apt-get update
sudo apt-get installmilagres-e

Uma vez feito isso, recomendo reiniciar para garantir que tudo seja carregado corretamente.

Os benefícios do desenvolvimento de GPU

As CPUs lidam com muitas entradas e saídas diferentes e contêm uma grande variedade de funções, não apenas para lidar com uma grande variedade de necessidades do programa, mas também para gerenciar várias configurações de hardware. Eles também lidam com memória, cache, barramento do sistema, segmentação e funcionalidade de E / S, tornando-os um pau para toda obra.

As GPUs são o oposto - elas contêm muitos processadores individuais que se concentram em funções matemáticas muito simples. Por causa disso, eles processam tarefas muitas vezes mais rápido do que CPUs. Ao se especializarem em funções escalares (uma função que recebe uma ou mais entradas, mas retorna apenas uma única saída), eles alcançam desempenho extremo ao custo de especialização extrema.

Código de exemplo

No código de exemplo, adicionamos vetores. Eu adicionei uma versão de CPU e GPU do código para comparação de velocidade.
gpu-example.cpp conteúdo abaixo:

#include 'cuda_runtime.h'
#incluir
#incluir
#incluir
#incluir
#incluir

typedefhoras::crono::high_resolution_clockRelógio;

# define ITER 65535

// Versão da CPU da função de adição de vetor
vaziovector_add_cpu(int *para,int *b,int *c,intn) {
inteu;

// Adicione os elementos do vetor aeb ao vetor c
para (eu= 0;eu<n; ++eu) {
c[eu] =para[eu] +b[eu];
}
}

// Versão GPU da função de adição de vetor
__global__vaziovector_add_gpu(int *gpu_a,int *gpu_b,int *gpu_c,intn) {
inteu=threadIdx.x;
// Não é necessário o loop for porque o tempo de execução CUDA
// irá encadear este ITER vezes
gpu_c[eu] =gpu_a[eu] +gpu_b[eu];
}

inta Principal() {

int *para,*b,*c;
int *gpu_a,*gpu_b,*gpu_c;

para= (int *)Malloc(ITER* tamanho de(int));
b= (int *)Malloc(ITER* tamanho de(int));
c= (int *)Malloc(ITER* tamanho de(int));

// Precisamos de variáveis ​​acessíveis para a GPU,
// então cudaMallocManaged fornece esses
cudaMallocManaged(Egpu_a, ITER* tamanho de(int));
cudaMallocManaged(Egpu_b, ITER* tamanho de(int));
cudaMallocManaged(Egpu_c, ITER* tamanho de(int));

para (inteu= 0;eu<ITER; ++eu) {
para[eu] =eu;
b[eu] =eu;
c[eu] =eu;
}

// Chame a função CPU e cronometre-a
autocpu_start=Relógio::agora();
vector_add_cpu(a, b, c, ITER);
autocpu_end=Relógio::agora();
horas::custo << 'vector_add_cpu:'
<<horas::crono::duration_cast<horas::crono::nanossegundos>(cpu_end-cpu_start).contar()
<< 'nanossegundos. n';

// Chame a função GPU e cronometre-a
// Os freios de ângulo triplo são uma extensão de tempo de execução CUDA que permite
// parâmetros de uma chamada de kernel CUDA a serem passados.
// Neste exemplo, estamos passando um bloco de thread com threads ITER.
autogpu_start=Relógio::agora();
vector_add_gpu<<<1, ITER>>> (gpu_a, gpu_b, gpu_c, ITER);
cudaDeviceSynchronize();
autogpu_end=Relógio::agora();
horas::custo << 'vector_add_gpu:'
<<horas::crono::duration_cast<horas::crono::nanossegundos>(gpu_end-gpu_start).contar()
<< 'nanossegundos. n';

// Libere as alocações de memória baseadas em função GPU
cudaFree(para);
cudaFree(b);
cudaFree(c);

// Libere as alocações de memória baseadas em função da CPU
gratuitamente(para);
gratuitamente(b);
gratuitamente(c);

Retorna 0;
}

Makefile conteúdo abaixo:

INC= -I/usr/local/milagres/incluir
NVCC=/usr/local/milagres/sou/nvcc
NVCC_OPT= -std = c ++onze

tudo:
$(NVCC)$(NVCC_OPT)gpu-example.cpp-ouexemplo de gpu

limpar:
-rm -fexemplo de gpu

Para executar o exemplo, compile-o:

faço

Em seguida, execute o programa:

./exemplo de gpu

Como você pode ver, a versão da CPU (vector_add_cpu) é consideravelmente mais lenta do que a versão da GPU (vector_add_gpu).

Caso contrário, pode ser necessário ajustar a definição de ITER em gpu-example.cu para um número maior. Isso ocorre porque o tempo de configuração da GPU é mais longo do que alguns loops menores com uso intensivo de CPU. Descobri que 65535 funciona bem em minha máquina, mas sua milhagem pode variar. No entanto, depois de ultrapassar esse limite, a GPU é dramaticamente mais rápida do que a CPU.

Conclusão

Espero que você tenha aprendido muito com nossa introdução à programação de GPU com C ++. O exemplo acima não é muito útil, mas os conceitos demonstrados fornecem uma estrutura que você pode usar para incorporar suas ideias para liberar o poder de sua GPU.