5. GPGPU

Como vimos nas seções anteriores, as GPUs a partir da terceira geração permitem que alguns estágios do pipeline gráfico sejam programados. Com o tempo, novas instruções e capacidades foram sendo adicionadas e as GPUs se tornaram capazes de realizar bem mais do que cálculos gráficos específicos para os quais foram desenvolvidas. Atualmente elas são co-processadores eficientes e sua alta velocidade as tornam úteis em uma grande variedade de aplicações.
       As GPUs já foram utilizadas, com grandes ganhos de desempenho, em aplicações como Processamento de Sinais, Busca e Ordenamento, Visão Computacional, Compressão de Dados, dentre outras [REF - GPGPU]. De modo genérico, costumamos dar o nome de GPGPU (general-purpose computation on GPU) a toda e qualquer utilização da GPU diferente daquela para a qual foi desenvolvida originalmente, ou seja, qualquer aplicação não gráfica.
       O hardware da GPU, altamente especializado para matrizes e vetores, faz com que ela seja muito mais rápida que a CPU em cálculos com dados desse tipo. Por outro lado, a CPU é mais eficiente que a GPU em cálculos com valores escalares.
       Outra diferença diz respeito à precisão: o tipo float da GPU possui 4 bytes de precisão e é o máximo que ela suporta por hardware; já na CPU podemos realizar cálculos com o dobro (o double da linguagem C) e com o quádruplo (o long double da linguagem C) de precisão, o que torna a GPU não recomendada para cálculos que necessitem de uma precisão muito alta.
       De maneira simplificada, um bom algoritmo para ser rodado em uma GPU tipicamente costuma:


Para exemplificar, iremos mostrar como programar o Operador de Sobel numa GPU. Este operador é muito utilizado em processamento de imagens, especialmente para detecção de bordas.


onde P(i, j)  representa o pixel da linha i e coluna j de uma imagem.
       Ou seja, o operador utiliza dois kernels de processamento como mostrado na Figura 25. Os números nos kernels representam os pesos dos pixels mais próximos no cálculo dos valores  e  de um pixel qualquer.
Figura 26 - Calculando o Sobel e lendo o resultado.

       Na Figura 26, o código entre as linhas 78 e 90 desenha um quadrado preenchendo toda a janela, e as linhas 95 e 96 copiam o resultado do framebuffer, conforme discutido anteriormente.
       O fragment program usado nesse exemplo pode ser visto na Figura 27.

Figura 25 - Os dois kernels de processamento do Operador de Sobel.
       
       Uma primeira preocupação quando vamos fazer cálculos genéricos na GPU é como passar os seus dados para a GPU. Normalmente usamos texturas para esse fim, de modo que cada texel (pixel de textura) representa um dos dados a serem processados.
       E como obter os dados calculados após o término da execução? A GPU escreve os resultados de seus cálculos em um framebuffer, e é daí que devemos retirar os resultados. O resultado do nosso processamento será a imagem processada pelo operador de Sobel, que será renderizada e copiada do framebuffer.
       Outra questão fundamental é como garantir que a GPU irá processar todos os seus dados. Como vimos anteriormente, um vertex program é executado exatamente uma vez para cada vértice presente na pirâmide de projeção; e um fragment program, uma vez para cada fragmento gerado na etapa de rasterização. Como deveremos ler os resultados do framebuffer, nada mais natural que usar fragment programs, pois os fragmentos estão muito mais próximos do que efetivamente será desenhado no framebuffer que os vértices.        
       Assim, um processamento genérico na GPU possui os seguintes passos:


       Vimos nas seções anteriores como passar texturas como parâmetros para a GPU. Veremos como fazer o segundo e o terceiro passos a seguir.
       Se os nossos dados são representados por uma textura de tamanho (m x n), fazer o segundo passo criando uma janela com o mesmo tamanho da textura e renderizando um quadrado texturizado com os nossos dados preenchendo todas a janela. Como a janela é totalmente preenchida por um quadrado, na etapa de rasterização, serão gerados um fragmento para cada pixel da janela, que corresponde por sua vez, a cada um de nossos dados.
       O terceiro passo pode ser feito facilmente em OpenGL, com as funções glReadBuffer e glReadPixels.

Figura 24 - À esquera, imagem original. À direita, imagem resultante após aplicação do Operador de Sobel.

       Formalmente, o Operador de Sobel pode ser definido como:

Figura 27 - Operador de Sobel na GPU.

       Implementamos também o Operador de Sobel utilizando apenas a CPU e comparamos os resultados. A máquina de teste possuía um processador AMD Athlon 64 x2 5000+, 2GB de memória RAM e placa GeForce 8600 GT 512MB. Foram feitos testes com imagens de tamanhos 32x32, 128x128, 512x512 e 2048x2048. A Figura 28 mostra os resultados.

Figura 28 - Benchmark CPU x GPU

       Como podemos ver a GPU se sai muito melhor em todos os casos, menos nas imagens pequenas 32x32. Nesse caso, as operações de transferência de dados entre a memória da CPU e da GPU consomem muito mais tempo que os cálculos em si.

Figura 29 - Resultados do Operador de Sobel. Da esquerda para direita: imagem original, imagem processada pela CPU e imagem processada pela GPU.

       As pequenas diferenças visuais nos resultados da CPU e GPU, que podem ser vistas na Figura 29, se devem justamente a diferença de precisão entre os dois processadores.