• Nenhum resultado encontrado

3.2 CUDA

3.2.4 Programação em CUDA

Um programa desenvolvido para a arquitetura CUDA possui capacidade de executar instruções em unidades de processamento gráfico [32]. O procedimento executado na GPU é chamado de kernel e várias instâncias de um kernel são executadas em paralelo através de um conjunto de threads [74]. Este conjunto de threads é organizado em blocos de threads e em grids de blocos de threads. Os blocos e os grids podem possuir 1, 2 ou 3 dimensões [32, 74, 102].

Cada thread possui um identificador único (threadIdx) dentro do seu bloco. Além disso, cada thread mantém o seu conjunto de registradores e uma memória local privada. Cada bloco de threads é um conjunto de threads que acessa uma mesma memória compartilhada. Cada bloco possui um identificador único dentro do seu grid. As threads de um bloco podem ser sincronizadas através de uma diretiva barreira (__syncthreads()), o que permite acessar a memória compartilhada sem gerar inconsistências.

Um grid é uma matriz que executa o mesmo kernel. Ao contrário dos blocos, o grid só pode ser sincronizado através de uma nova chamada ao kernel, não existindo uma diretiva de sincronização específica. Todas as threads de um grid compartilham a mesma memória global, que deve ser acessada de maneira cuidadosa para não gerar inconsistências.

A Figura 3.5 ilustra cada nível de agrupamento de threads e a sua memória relacio- nada. Além dessas memórias, ainda existem duas memórias somente-leitura: a memória constante, que é uma memória pequena (64 KB1) e rápida; e a textura, que é uma me-

mória somente-leitura de acesso global que utiliza mecanismos de cache. Em especial, a textura permite acelerar o acesso a uma área da memória global utilizando o princípio da localidade espacial, inclusive em matrizes de 2 e 3 dimensões. Não existem mecanismos de coerência de cache em texturas, então uma operação de escrita na área de memória

1Até o momento da escrita desta tese, todas as arquiteturas CUDA (compute capability 1.0 a 5.2)

Memória Local Bloco Memória Compartilhada Thread Grid

...

Memória Global Regs.

Figura 3.5: Hierarquia de memória da arquitetura CUDA [74].

global mapeada para uma textura pode gerar incoerências no cache. O número máximo de elementos em uma textura de uma única dimensão é de 227.

As threads são executadas em grupos chamados warp, cada um contendo 32 threads. O agrupamento é determinado através do identificador da thread, de maneira crescente a partir da thread 0, agrupando de 32 em 32. Dentro de um warp as threads executam paralelamente a mesma instrução por vez (SIMT). Quando um warp encontra um branch onde as threads divergem seus caminhos, o warp serializa a execução das instruções. Assim, um grupo de threads fica desabilitado até que o outro grupo seja processado. Isso leva a uma perda de desempenho em branches divergentes [74, 103]. Para evitar este problema, um cuidado especial deve ser tomado para que, dentro de um mesmo warp, todas as threads sigam pelo mesmo caminho de execução.

O número de blocos que podem ser executados em paralelo em um mesmo multi- processador depende do número de registradores que uma thread utiliza, assim como a quantidade de memória compartilhada entre as threads de um bloco. Os blocos então são enumerados e alocados aos vários multiprocessadores. À medida que um bloco é ter- minado, um novo bloco é ativado naquele processador. Isso se repete até que todos os blocos tenham sido computados. Observe que a ordem de execução dos blocos não pode ser determinada e, para que não haja inconsistência de dados, eles devem ser capazes de executar independentemente dos demais blocos.

A programação em CUDA é feita utilizando a linguagem C com algumas modificações. A definição de um kernel é feita simplesmente com a adição da diretiva __global__ antes da declaração de uma função. A chamada ao kernel deve ser feita informando o número de threads T e o número de blocos B através da sintaxe ≪ T, B ≫. Este par (B, T ) é chamado de configuração de execução.

No Programa 3.1, vemos um exemplo de código em CUDA que soma dois vetores A e B, armazenando o resultado no vetor C. O kernel, definido com a diretiva __global__, é

executado por várias threads, sendo que cada thread recebe um identificador único acessado através da variável threadIdx. Este identificador é utilizado para que cada thread acesse um elemento distinto no vetor. A chamada ao kernel é feita com N threads através da sintaxe vecAdd≪ 1, N ≫.

Programa 3.1 Exemplo de código em CUDA [74].

// definição do kernel

__global__ void vecAdd(float* A, float* B, float* C) {

int i = threadIdx.x; C[i] = A[i] + B[i]; } int main() { // chamada ao kernel vecAdd<<<1, N>>>(A, B, C); }

Capítulo 4

Comparação Paralela de Sequências

Biológicas

A comparação de sequências biológicas é uma tarefa que consome muitos recursos com- putacionais. Dependendo do tamanho das sequências ou do número de comparações realizadas, o processamento pode levar vários dias. Esse fato é ainda agravado pois os bancos de sequências públicas crescem exponencialmente. O GenBank, por exemplo, é um banco de sequências que duplica de tamanho a cada 30 meses [104]. Nos algoritmos de comparação de sequências vistos no Capítulo 2, a maior parte do tempo de execução é gasta calculando a matriz de programação dinâmica e esta é a parte dos algoritmos que usualmente é paralelizada.

Este capítulo visa apresentar as estratégias de paralelização (Seção 4.1) e algumas soluções que comparam sequências biológicas de maneira paralela em CPU (Seção 4.2), GPU (Seção 4.3) e outras plataformas (Seção 4.4). Por fim, apresentaremos um quadro comparativo com as soluções abordadas neste capítulo (Seção 4.5). Um artigo esten- dendo esta revisão bibliográfica foi produzido e submetido ao periódico ACM Computing Surveys [43] e encontra-se em processo de revisão.

Documentos relacionados