• Nenhum resultado encontrado

O cálculo tanto do fecho transitivo quanto do menor caminho entre todos os vértices de um grafo necessita passar por uma série de iterações, sendo que uma iteração depende do resultado da iteração anterior para que possa ser efetivada. Devido a essa dependência de dados, algoritmos como Warshall, Floyd-Warshall e outros com a mesma característica não podem ser executados totalmente em paralelo. Em contrapartida, esses algoritmos apresentam a vantagem de utilizar uma única estrutura de dados para obter o resultado esperado.

Observado esses detalhes dos algoritmos sequenciais, nesta seção são apresen- tadas duas soluções paralelas na GPU baseadas nos algoritmos sequencias de Warshall e Floyd-Warshall. Ambas possuem soluções semelhantes, diferenciando somente no núcleo dos algoritmos. Portanto, as soluções, de maneira geral, valem para os dois problemas, fe- cho transitivo e menor caminho entre todos os pares de vértices.

De acordo com o Capítulo 4, os algoritmo Warshall (Algoritmo 4.1) e Floyd- Warshall (Algoritmo 4.6) possuem três laços, dois dos quais são independentes (linhas

5.1 Soluções Paralelas baseadas nos Algoritmos de Warshall e Floyd-Warshall 70

2 e 3), ou seja, podem ser executados em qualquer ordem sem afetar a solução final. À vista disso, esses laços podem ser simplesmente representados por blocos de threads com duas dimensões, x e y, nos algoritmos paralelos para GPU e, assim, a lógica do algoritmo sequencial é mantida. Dessa forma, as soluções paralelas já reduzem a complexidade de tempo comparado com o sequencial. Porém, o primeiro laço,representado pela linha 1 dos Algoritmos Sequenciais 4.1 e 4.6, apresenta dependência entre iterações, na qual impossibilita a retirada deste laço nas soluções paralelas propostas.

A primeira solução proposta apresenta essa dependência de dados do laço da linha 1 dos Algoritmos 4.1 e 4.6 no código da GPU, com o objetivo de evitar comunicações excessivas entre a GPU e a CPU através do barramento PCI Express. Para isso, é necessário o uso da ocupação intensiva nos SMs, descrita melhor na Seção 3.4. De acordo com Xiao e Feng [41], grande parte do tempo de execução de uma aplicação CUDA é gasto no lançamento de kernels e sincronizações. Isso ocorre principalmente quando há um uso excessivo de comunicação entre os dispositivos. A ocupação intensiva dos SMs evita que qualquer SM fique ocioso devido à dependência de dados no momento da computação (Seção 3.4).

O uso da ocupação intensiva na primeira versão da solução faz a implementação conter barreiras de sincronização entre blocos de threads. Essas barreiras estão localizadas de modo que os blocos de threads sejam sincronizados antes de passar para a próxima iteração. Isso garante a integridade de dados devido às dependências contidas na linha 1 dos Algoritmos 4.1 e 4.6.

A Figura 5.1 ilustra uma abstração da solução paralela utilizando a técnica de ocupação intensiva sobre uma matriz de adjacência. Os quadrados enumerados (0 a 3) representam os blocos de threads, enquanto os quadrados menores apresentam os elementos da matriz e os retângulos enumerados ilustram a área de processamento pertencente ao bloco de threads de sua enumeração. Considerando que o algoritmo é processado sobre uma plataforma GPU com quatro SMs, onde cada qual suporta um único bloco de threads em uma unidade de tempo. Então, lança-se o kernel, que representa o algoritmo de Warshall ou Floyd-Warshall, somente com quatro blocos de threads, conforme ilustrado, de modo que a GPU fique totalmente ocupada sem que haja threads para serem escalonadas. Dessa forma, cada bloco de threads terá que processar diversas partes da matriz, ao invés de uma única parte, como das soluções paralelas apresentadas nas Seções 4.2.3 e 4.3.3.

O Algoritmo 5.1 descreve a implementação da fase sequencial do algoritmo paralelo que será executado no host. Observa-se que o algoritmo recebe de entrada uma matriz de adjacência Mn×n, a quantidade de SMs da GPU que o código será executado, o tamanho do bloco de threads e uma constante k. A quantidade de SMs e a constante k definem a quantidade de blocos de threads na grade (grid). Os valores do tamanho do

5.1 Soluções Paralelas baseadas nos Algoritmos de Warshall e Floyd-Warshall 71

Figura 5.1: Exemplo de ocupação intensiva dos SM’s: (a) Número de blocos de thread; (b) Divisão das partes da matriz por bloco de threads.

bloco de threads e da constante k ditam a quantidade de blocos que cada SM da GPU irá processar simultaneamente. Esses valores são obtidos por base de testes que variam de acordo com o modelo da GPU. Esse Algoritmo 5.1 necessita de uma única chamada ao kernel CUDA, ou seja, uma única chamada à parte paralela do algoritmo, como é mostrado na linha 6. Isso demonstra que toda lógica dos algoritmos sequenciais, Algoritmos 4.1 e 4.6, é implementada na fase paralela do modelo CUDA. Consequentemente, uma única comunicação é realizada entre os dispositivos envolvidos, deixando a maior parte do tempo gasto na computação do algoritmo.

5.1 Soluções Paralelas baseadas nos Algoritmos de Warshall e Floyd-Warshall 72

Algoritmo 5.1: Código da CPU - Implementação Paralela Proposta dos Algoritmos do Warshall e Floyd-Warshall em Blocos

Entrada: Matriz de Adjacência Mv×v do grafo G=(E,V), número de SMs numero_SM, tamanho do bloco de threads bloco_size, constante k;

Saída: Matriz de Adjacência Mv×v com a solução do problema sobre o grafo G=(E,V);

1 numero_Blocos ← numero_SM * k

2 Aloca memória para a matriz Mv×nvna GPU 3 Copia a matriz Mv×v para GPU

4 dimBloco← bloco_size × bloco_size 5 dimGrid← numero_Blocos

6 Invoca o kernel_Imp_Paralela_Em_Blocos <tam_grade,tam_bloco >(Mv×v) na GPU

7 Copia a matriz Mv×v da GPU para CPU

8 Libera o espaço na memória da GPU ocupado pela matriz Mv×v

O kernel lançado na linha 6 do Algoritmo 5.1 é detalhado no Algoritmo 5.2. Ele representa a fase paralela do modelo de programação CUDA e contém toda lógica da solução paralela, como descrito no Algoritmo 5.2. Esse algoritmo, inicialmente, obtém os identificadores das threads no eixo X e Y. Em seguida, as proporções entre quantidade de vértices e as de blocos de threads tanto no eixo X quanto no eixo Y são obtidas (linhas 4 e 5) com objetivo de armazenar as quantidades de divisões da matriz que cada bloco irá trabalhar em cada eixo. O laço da linha 7 representa o laço da linha 1 nos algoritmos sequenciais de Warshall e Floyd-Warshall (Algoritmo 4.1 e 4.6, respectivamente), onde localiza-se a dependência de dados dos respectivos algoritmos. Os laços das linhas 9 e 11 direcionam os blocos de threads para as divisões da matriz que correspondem a eles. Isso é realizado através das variáveis idY e idX que indicam quais partes dos eixos da matriz que cada bloco irá trabalhar. Antes de cada laço, os índices i e j, que correspondem a linha e a coluna da matriz respectivamente, são inicializados. O cálculo do fecho transitivo ou caminho mínimo entre todos vértices encontra-se na chamada da função Calculo na linha 12. A função Calculo é descrita pelos algoritmos 5.3 e 5.4, sendo este para o problema do caminho mínimo e aquele para o fecho transitivo.

No final de cada iteração do laço da linha 7, uma barreira sincronização é realizada entre todos os blocos de threads da GPU. Essa barreira garante a sequência de iterações necessárias para resolução do problema do fecho transitivo e APSP. Como visto no Capítulo 3, não existe uma sincronização da API CUDA que sincronize todos os blocos de threads de uma grade. Por isso, houve a necessidade de implementar uma barreira que

5.1 Soluções Paralelas baseadas nos Algoritmos de Warshall e Floyd-Warshall 73

atendesse esse caso. Desse modo, foi utilizado o método de sincronização proposto por Xiao e Feng [41] que não utiliza operações atômicas para obter a sincronização global.

Algoritmo 5.2: Código da GPU - Implementação Paralela Proposta dos Algoritmos do Warshall e Floyd-Warshall em Blocos

Entrada: Matriz de adjacência Mv×v do grafo G=(E,V);

Saída: Matriz de adjacência Mv×v com a solução do problema sobre o grafo G=(E,V);

1 para todo id em paralelo faça

2 Obtém os identificadores em X e Y das threads na grade 3 Obtém o tamanho da grade de threads em Tam_X e Tam_Y 4 proporcaoX ← |V | Tam_X 5 proporcaoY ← |V | Tam_Y 6 n← |V | 7 para v ← 0; v < n; v + + faça 8 i← Identificador da thread em Y

9 para idY ← 0; idY < proporcaoY ; idY + + faça 10 j← Identificador da thread em X

11 para idX ← 0; idX < proporcaoX ; idX + + faça 12 Calculo(M, v, i, j) 13 j← j + Tam_X 14 fim 15 i← i + Tam_Y 16 fim 17 Barreira_Sincronização_Entre_Blocos 18 fim 19 fim

Algoritmo 5.3: Função - Cálculo do Fecho Transitivo de um Grafo Entrada: (1) Matriz de adjacência binária Mv×vdo grafo G=(E,V), (2)

vértice v e (3) índices i e j;

1 se M[v][ j] = 1 E M[i][v] = 1 então 2 M[i][ j] ← 1

5.1 Soluções Paralelas baseadas nos Algoritmos de Warshall e Floyd-Warshall 74

Algoritmo 5.4: Função - Cálculo do Caminho Mínimo Entre Todos os Pares de Vértices de um Grafo (APSP)

Entrada: (1) Matriz de distância Mv×v do grafo G=(E,V), (2) vértice v e (3) índices i e j;

1 se M[v][ j] + M[i][v] < M[i][ j] então 2 M[i][ j] ← M[v][ j] + M[i][v] 3 fim

A Figura 5.2 ilustra, de forma semelhante à Figura 5.1, as iterações do laço 7 do Algoritmo 5.2 sobre a matriz de adjacência com as divisões destacadas de cada bloco de threads irá trabalhar. Os retângulos em amarelo, horizontal e vertical, representam a localização dos elementos da matriz (linha 12) que são necessários para computação da função Calculo em uma dada iteração. A primeira, segunda e a terceira iterações são representadas em (a), (b) e (c), respectivamente. A N-essíma iteração é mostrada em (d). Observa-se que a matriz de adjacência é dividida em blocos. Por isso, o termo “em blocos” está presente no nome das implementações dos algoritmos 5.1 e 5.2.

Essa primeira solução apresentada reduz a comunicação entre a CPU e a GPU e, por sua vez, não utiliza de forma eficiente a hieraquia de memória da GPU nas iterações do laço 7 dos algoritmos 5.1 e 5.2.

Com o objetivo de obter um melhor desempenho computacional em relação ao estado da arte das implementações paralelas dos algoritmos Warshall e Floyd-Warshall, propõe-se uma segunda abordagem cuja solução faz uso de dois aspectos principais da GPU: localidade de dados e ocupação intensiva dos SMs.

A localidade de dados relaciona-se com a hierarquia de memória disponível na arquitetura CUDA, onde o acesso à memória global possui uma latência muito alta em relação à memória compartilhada. Para uma melhor localidade dos dados, a implementação proposta baseia-se naquela sequencial concebida por Venkataraman et al. [39]. A matriz de distâncias é dividida em blocos de tamanhos iguais. Esses blocos são levados inteiramente para a memória compartilhada e, somente então, é realizado o cálculo do caminho mínimo ou do fecho transitivo. O algoritmo segue em estágios, semelhante à implementação de Katz e Kider [20]. Em cada estágio, que é subdividido em duas fases, um bloco primário é definido.

A Fase 1 do algoritmo realiza o cálculo do caminho mínimo ou fecho transitivo no bloco primário e nos seus respectivos eixos x e y, ou seja, nos blocos que pertencem à linha e à coluna do bloco primário. Para tal, aplica-se uma ocupação intensiva dos SMs e uma divisão em etapas dessa fase, onde todas as etapas são executadas em um único kernel.

5.1 Soluções Paralelas baseadas nos Algoritmos de Warshall e Floyd-Warshall 75

Figura 5.2: Iterações da implementação paralela descrita no Al- goritmo 5.2

A primeira etapa consiste no armazenamento do bloco primário na memória compartilhada e, em seguida, na execução do algoritmo neste bloco primário, em cada bloco de threads. Como o tempo de vida das threads é alterado para persistir (ocupação intensiva) até o final da execução do kernel, cada bloco de threads busca e armazena uma única vez o bloco primário atual, onde encontra-se a dependência de dados. Portanto, o bloco primário é replicado conforme a quantidade de blocos de threads lançados no kernel. A Figura 5.3 exemplifica essa primeira etapa. Como pode ser observado, há 3 SMs na GPU, e cada um executa um único bloco de threads.

Na segunda etapa, os blocos de threads executam o algoritmo na linha e coluna do atual bloco primário. Entretanto, um bloco de threads executa em mais de um bloco da matriz de adjacências, sendo definida a sequência de tarefas antes da execução. Ressalta- se que um SM diferente a cada estágio é responsável por armazenar o atual bloco primário. A Figura 5.4 ilusta essa segunda etapa, onde em (a) a linha está sendo executada; e em (b) a coluna está sendo executada.mostra a proposta de implementação do

5.1 Soluções Paralelas baseadas nos Algoritmos de Warshall e Floyd-Warshall 76

Figura 5.3: Primeira etapa da Fase 1 da implementação do Floyd- Warshall.

calculados de forma convencional proporcionado pelo modelo de programação CUDA. O tempo de vida das threads não é persistente até o fim do kernel. Caso a matriz seja maior que a capacidade de threads em execução, haverá threads esperando para serem escalonadas. Esta fase é realizada somente em um único kernel. Portanto, os blocos da linha e da coluna correspondentes ao bloco de threads a ser computado são armazenados na memória compartilhada.

O Algoritmo 5.5 mostra a estratégia da implementação no código da CPU, onde a variável qtdPartes indica a proporção entre o tamanho da matriz de distâncias (tamanho do grafo) com o número de SMs da GPU e tamanho do bloco de threads. Na GPU, ela indicará quantas partes da matriz de distâncias que cada bloco de threads irá executar. então indica também a quantidade de partes da matriz com que cada SM irá trabalhar. A quantidade exata de blocos de threads para cada kernel é ditado pelas variáveis dimGrid_ f ase1 e dimGrid_ f ase2.

5.1 Soluções Paralelas baseadas nos Algoritmos de Warshall e Floyd-Warshall 77

Figura 5.4: Segunda etapa da Fase 1 da implementação do Floyd- Warshall.

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

Algoritmo 5.5: Código da CPU - Implementação Proposta dos Algoritmos Warshall e Floyd-Warshall

Entrada: Grafo G = (V, E) representado pela matriz An×n, bloco_size, numeroSM; Saída: Caminho Mínimo/Fecho Transitivo do grafo G representado pela matriz

An×n; 1 n←| Anxn| 2 numeroBlocos_SM ← numeroSM 3 numeroBlocos← d( n bloco_size)e 4 qtdPartes← ( n

(bloco_size× numeroBlocos_SM)) 5 Aloca memória para a matriz An×nna GPU 6 Copia a matriz An×n para GPU

7 dimBloco← bloco_size × bloco_size 8 dimGrid_ f ase1 ← numeroBlocos_SM

9 dimGrid_ f ase2 ← numeroBlocos × numeroBlocos 10 enquanto estagio = 0 até n faça

11 Invoca o kernel_fase_1 <dimGrid_ f ase1, dimBloco >(An×n, n, numeroBlocos_SM, qtdPartes)

12 Invoca o kernel_fase_2 <dimGrid_ f ase2, dimBloco >(An×n, n, estagio) 13 estagio← estagio + bloco_size

14 fim

15 Copia a matriz Av×v da GPU para CPU

16 Libera o espaço na memória da GPU ocupado pela matriz An×n

5.2

Solução Paralela baseada no Algoritmo BFS para o

Documentos relacionados