• Nenhum resultado encontrado

O algoritmo sequencial BFS, descrito no Capítulo 4 (Algoritmo 4.2), percorre o grafo em fronteiras ou níveis. Primeiro, ele visita todos os vértices de uma única fronteira antes de prosseguir para a próxima. Essa visita vai ocorrendo de forma irregular, ou seja, não contém uma direção predefinida no grafo.

As soluções para o problema do fecho transitivo, como mostrado na Seção 4.2 do Capítulo 4, necessitam de etapas sequenciais. O algoritmo sequencial BFS realiza essas etapas sequenciais através de visitas ordenadas nas fronteiras. Para que a solução do algoritmo sequencial BFS seja válida para o problema do fecho transitivo, o algoritmo precisa ser executado |V| vezes, cada um com vértice de origem diferente.

Visto isso, a implementação paralela proposta do BFS nesta seção envolve paralelizar os acessos a vértices de uma mesma fronteira e executar concorrentemente

5.2 Solução Paralela baseada no Algoritmo BFS para o Problema do Fecho Transitivo 79

vários algoritmos BFS, cada qual com um vértice de origem distinto. Essa implementação trabalha sobre a representação compactada do grafo de entrada, apresentada na Seção 4.1 do Capítulo 4, e a representação da matriz de adjacência. Sendo a primeira usada para armazenar (escrever) o fecho transitivo do grafo e aquela utilizada somente para leitura com objetivo de aglutinar os acessos à memória global, de alta latência, e minimizar acessos desnecessários à memória quando não houver aresta.

A invocação de várias instâncias do algoritmo do BFS na solução aqui proposta ocorre na execução dos blocos de threads. Pois, cada bloco computará um algoritmo paralelo BFS com um vértice de origem distinto. Dessa forma, cada Multiprocessador (SM) fica responsável por calcular uma quantidade predefinida de BFS concorrentemente, ou seja, a quantidade de blocos de threads que cada SM suporta ao mesmo tempo será igual ao número de algoritmos BFSs sendo executados em paralelo. Sendo que esta quantidade é definida no lançamento do kernel, onde o número de threads por bloco é informado, em tempo de compilação, onde é dado o limite de registradores por thread. Uma consequência de implementar os algoritmos BFS por bloco é que o hardware da GPU é mantido totalmente ocupado durante a execução. Torna-se desnecessário qualquer comunicação ou sincronização entre blocos de threads, pois cada instância do algoritmo BFS é independente de outra. Além disso, o uso de uma GPU com número maior de SMs, implica num maior número de algoritmos paralelos BFS executando simultaneamente, tornando o algoritmo totalmente escalável. A ilustração dessa paralelização de vários algoritmos BFS na GPU é mostrado na Figura 5.5.

Figura 5.5: Paralelização de vários algoritmos BFS na GPU

A solução paralela proposta faz o uso de três listas de tamanho O(|V |). Sendo duas para representação das fronteiras e uma para os vértices já visitados. Todas são marcadas com 0 ou 1, indicando se o vértice foi visitado, no caso da lista de visitados, ou se o vértice está contido nas fronteiras atual ou na próxima no caso das listas de fronteiras. No início do algoritmo sequencial BFS, a fila contém somente o vértice de origem. Em cada etapa, o vértice de origem é retirado e todos seus vizinhos são visitados e adicionados no final da fila. Esse processo é repetido até que todos os vértices sejam visitados. Considerando isso, podemos dividir os vértices na fila do algoritmo BFS em

5.2 Solução Paralela baseada no Algoritmo BFS para o Problema do Fecho Transitivo 80

dois tipos, os vértices contidos na fronteira atual e na fronteira posterior. Sendo assim, a fila do algoritmo sequencial é representada na solução paralela proposta por duas listas de fronteiras, uma para fronteira atual e a outra para a próxima fronteira. A Figura 5.6 ilustra as fronteiras de um grafo e as listas das fronteiras.

Figura 5.6: Listas das Fronteiras do algoritmo paralelo proposto

A arquitetura CUDA, como descrito no Capítulo 3, trabalha com uma hierarquia de threads leves (grades, blocos de threads e threads) sobre uma hierarquia de proces- sadores. Como a estrutura do grafo é irregular, uma implementação paralela ingênua do BFS na GPU seria mapear uma thread para cada vértice. Isso levaria a algumas threads terem muito trabalho, enquanto outras, pouco, resultando em uma serialização de tarefas. Ao invés de atribuir uma tarefa (vértice) para cada thread, a solução aqui proposta atribui um conjunto de tarefas a um conjunto de threads para obter um resultado, que neste caso é visitar todos os vértices vizinhos a partir de um dado vértice. Esse conjunto de threads é o mesmo conjunto atribuído à war p, que é descrito no Capítulo 3.

O conjunto de threads da war p foi escolhido, pois as threads de uma mesma war p apresentam algumas pecularidades que ajudam na comunicação e sincronização entre elas. Em relação a essas threads, podem ser citadas as seguintes características:

• São executadas em um mesmo contexto e compartilham os recursos de um único SM;

• Os processadores de fluxo de um único SM trabalham sobre a forma de SIMD síncrono, ou seja, trabalham sobre uma mesma instrução ao mesmo tempo. Conse- quentemente, as threads da war p são síncronas;

• Uma quantidade de threads adequada para o hardware do SM, pois os acessos à memória podem ser otimizados;

• Possuem funções especiais da API CUDA que facilitam na comunicação sem precisar de acessar memória compartilhada, por exemplo, __popc, __ballot, __ffs;

As war ps contém 32 threads. Sendo assim, a lista de fronteira atual é dividida em partes iguais de tamanho 32 vértices, cada parte é mapeada para uma war p. Caso a

5.2 Solução Paralela baseada no Algoritmo BFS para o Problema do Fecho Transitivo 81

quantidade de partição de 32 elementos da lista seja maior que a quantidade de war ps, as war psconterão mais de uma parte da lista da fronteira. A Figura 5.7 ilustra essa divisão de trabalho sobre a lista de fronteira atual.

Figura 5.7: Divisão de trabalhos entre as war ps

Os vértices contidos em uma mesma parte da lista da fronteira atual são proces- sados sequencialmente pela war p, ou seja, todas as thread da war p visitam ao mesmo tempo a lista de adjacência de um único vértice. Como um bloco contém várias war ps, no máximo 32 war ps na arquitetura Fermi, e todo bloco trabalha sobre um mesmo algoritmo paralelo BFS, então listas de adjacências de vértices distintos são visitados ao mesmo tempo, como ilustrado na Figura 5.8. Dessa forma, há dois níveis de paralelização. O pri- meiro ocorre na fronteira atual com a divisão de tarefas por warp e o segundo acontece na fronteira seguinte, onde várias listas de adjacências são visitadas em paralelo, cada qual com várias threads de uma mesma warp visitando-a. Neste momento que se encontra a segunda paralelização da solução proposta, sendo a primeira, a execução de vários algo- ritmos BFS em paralelo como ilustrado na Figura 5.5. A Figura 5.8 mostra a divisão da lista da fronteira atual entre warps e as visitas que cada warp faz na lista de adjacência de cada vértice.

O Algoritmo 5.6 descreve a implementação proposta. Nele, as linhas 1 a 3 criam e inicializam variáveis e estruturas de dados do algoritmo. As linhas 4 a 6 adicionam os identificadores de threads da warp na lista TidW que são utilizadas para identificar as threads de uma mesma warp. Na linha 7, o vértice de origem é adicionado na fronteira atual. O laço da linha 10 é executado em paralelo e nas linhas 10 e 11 a comunicação das threads de uma mesma warp para obter os vértices contidos na parte correspondente da warp na fronteira atual. Isso para que a warp toda visite a mesma lista de adjacência, ocorrendo assim o paralelismo. O laço da linha 14 é, também, executado em paralelo. O laço da linha 15 é executado sequencialmente para toda thread e indica a visita de listas de adjacências em sequência pela warp. Porém, para warps diferentes, listas de adjcências

5.2 Solução Paralela baseada no Algoritmo BFS para o Problema do Fecho Transitivo 82

Figura 5.8: Paralelização de visitas a vértices contidos nas fron- teiras

são visitadas em paralelo. O algoritmo finaliza quando não há vértices na fronteira atual, ou seja, quando não existem vértices não-visitados. Essa condição de parada é apresentada pelo laço da linha 9. Observa-se que antes de verificar, há uma barreira de sincronização. Isso garante que cada fronteira do grafo será visitada somente quando todas anteriores forem. Assim, o algoritmo caminha entre fronteiras, garantindo que a complexidade do algoritmo seja dependente da quantidade de fronteiras contidas no grafo.

5.2 Solução Paralela baseada no Algoritmo BFS para o Problema do Fecho Transitivo 83

Algoritmo 5.6: Implementação Paralela Proposta do BFS

Entrada: (1) Vértice origem s, (2) matriz de adjacência binária Mv×ve (3) estruturas compactadas listaVertices e listaArestas;

Saída: Fecho Transitivo do grafo G representado pela Matriz de Adjacência Binária Mv×v;

1 Obtém os identificadores globais e locais na war p da thread e o identificador da warp (id, idW e idTW , respectivamente)

2 Cria as listas da fronteira atual Frae da próxima fronteira Frprox, de

visitados Visitados de tamanhos O(V ) e as listas W e TidW de 32 elementos 3 Fra← /0; Frprox← /0; Visitados ← /0

4 para todo id em paralelo faça 5 TidW[idTW ] ← id

6 fim 7 Fra← s

8 Barreira de sincronização 9 enquanto Fra6= /0 faça

10 para todo {u} ∈ Fraem paralelo faça

11 W ← {{u} ∈ Frae id ∈ TidW|u = 1 ou u = 0}

12 quantidadeVerticesWar p← |Q|, onde Q = {{u} ∈ W |u = 1}

13 fim

14 para todo id em paralelo faça

15 para i = 0; i < quantidadeVerticesWar p; i + + faça 16 posicaoBit← posicao[u], onde {posicao[u] ∈ |W | e

{u} ∈ W | u = 1 e u é o primeiro bit 1 da lista} 17 vértice v ← Obtem_Vertice(posicaoBit)

18 proporcaoWar p← d(|Ad j[v]|/TAMANHO_WARP)e 19 para j = 0; j < proporcaoWar p; j + + faça

20 posicao← proporcaoWar p × TAMANHO_WARP + idTW 21 vértice u ← listaArestas[posicao]

22 se NÃO Visitado[u] então 23 Frprox[u] ← 1; Mvu← 1 24 fim 25 fim 26 W ← W −W [posicaoBit]; Visitado[u] ← 1 27 fim 28 fim 29 Fra[v] ← 0; Fra← Frprox; Frprox← /0 30 Barreira de sincronização 31 fim

5.2 Solução Paralela baseada no Algoritmo BFS para o Problema do Fecho Transitivo 84

Como todas as threads de uma mesma war p trabalham sobre a mesma lista de adjacência, torna-se necessário uma comunicação eficiente, pois elas executam de maneira síncrona. A comunicação entre as threads de uma mesma war p ocorre da seguinte forma. Primeiro, cada thread da war p verifica se há um vértice na posição correspondente a sua posição na war p, obtendo o valor 0 ou 1. Em seguida, uma avaliação sobre todos esses valores é feita e, assim, retorna para todas elas um valor inteiro cujo N- éssimo bit está definido igual a 1, se e somente se o valor avaliado é 1 para a N-éssimo thread da war p, como descrito na linha 1 do Algoritmo 5.6. A Figura 5.9 ilustra esse processo. Nela, as 32 threads de uma war p obtêm os valores contidos na parte lista da fronteira atual e, então, todos os valores são combinados formando um inteiro de 4 bytes. Esse inteiro é passado por broadcast para todas as threads ativas participantes do processo.

Figura 5.9: Comunicação entre as threads ativas de uma war p sobre uma parte da lista da fronteira atual

Após obtido esse valor inteiro, cada thread irá descobrir qual a sequência de vértices a ser visitada. Sendo essa sequência única para toda a war p, assim, todas as threadsvisitam a lista de adjacência de um mesmo vértice. Essa visita é feita por base dos valores de bits contidos nos 4 bytes retornados. Cada bit com valor 1 nos 4 bytes, indica um vértice na parte da lista da fronteira atual, ou seja, indica uma lista de vizinhos para ser visitada. Assim, evita-se qualquer outra comunicação ou sincronização entre elas. Ao finalizar a comunicação, cada thread irá trabalhar sobre os vértices na lista de bits em sequência, obtendo o próximo vértice a partir da posição dos bits com valor 1. Para isso, a função Obtem_Vertice é chamada (linha 17). Após obtido o vértice, a quantidade de visitas por thread que será feita na lista de adjacência é obtida na linha 18. Com isso, as visitas são feitas em sequência, verificando se o vértice foi visitado (linha 22), caso não seja, ele é adicionado na lista da próxima fronteira e marcado como visitado (linha 23).

5.3 Solução Paralela baseada no Algoritmo Dijkstra para o Problema APSP 85

5.3

Solução Paralela baseada no Algoritmo Dijkstra

Documentos relacionados