Multiplicação de matrizes: uma comparação entre as abordagens sequencial (CPU) e paralela (GPU)

Multiplicação de matrizes: uma comparação entre as abordagens sequencial (CPU) e paralela (GPU)

Andre G. C. Pacheco
Abstract

A modelagem de problemas utilizando matrizes é de extrema importância para Ciência da Computação. Áreas como computação gráfica, grafos e aprendizado de máquina utilizam matrizes com alta frequência para solucionar seus respectivos problemas. Dessa forma, operar matrizes de maneira eficiente é muito importante para o desempenho de algoritmos. Uma das operações de matrizes mais utilizadas é a multiplicação, que se torna um empecilho para o desempenho computacional de algoritmos na medida que o tamanho das matrizes a serem multiplicadas aumentam. Por conta disso, a computação paralela se tornou uma solução padrão para abordar tal problema. Neste trabalho é apresentado uma comparação entre as abordagens sequencial e paralela para multiplicação de matrizes utilizando CUDA e OpenMP. O resultado da análise realizada entre o tamanho da matriz e o desempenho da multiplicação mostra a importância da paralelização principalmente para matrizes de ordem elevada.

GPU CUDA OpenMP Computação Paralela Multiplicação de Matrizes
\templatetype

pnasresearcharticle \datesEste relatório técnico foi produzido em Novembro de 2016 \doiRelatórioTécnico \leadauthorAndré G. C. Pacheco \correspondingauthor1Autor correspondente. E-mail: agcpacheco@inf.ufes.br

1 Introdução

Operações com matrizes são fundamentais para computação científica de maneira geral. Diversos algoritmos das mais variadas áreas da computação são modelados e executados utilizando o conceitos e operações matriciais (1). Uma operação matricial fundamental é a multiplicação. Tal operação é bastante comum em algoritmos de diversas áreas e possui um grande potencial de consumo de tempo computacional. Com a crescente demanda de dados, áreas como a de aprendizado de máquina (machine learning), exigem algoritmos cada vez mais eficientes para que os mesmos sejam capazes de processar mais informações em menos tempo. Com isso, otimizar operações matriciais como a multiplicação é de suma importância para que esse objetivo seja alcançado. Uma abordagem padrão para otimização de processamento de dados é o uso de paralelismo computacional e uma forma eficiente e massivamente utilizada de paralelização é o uso de uma Unidade de Processamento Gráfico (Graphics Processing Unit - GPU). Atualmente paralelização e GPUs se tornaram quase que sinônimos e diversos algoritmos, das mais diversas áreas da computação, utilizam GPUs para otimizar seu processamento (2).

A computação acelerada por placas de vídeo é o uso de uma GPU juntamente com uma CPU (Central Processing Unit) para acelerar algoritmos de aprendizado, análise e engenharia de maneira geral. O uso de GPUs como propósito geral de computação foi introduzido pela NVIDIA corporation em 2006 por meio da plataforma de programação CUDA (Compute Unified Device Architecture), que permite desenvolvedores codificá-la utilizando a linguagem de alto nível C (3). Desde seu lançamento, a plataforma vem potencializando data centers, universidade, empresas de médio e grande porte ao redor do mundo (4).

Atualmente GPUs desempenham um papel fundamental na aceleração de aplicativos em plataformas que variam de inteligência artificial até carros, drones e robôs (5). Ao longo dos anos, diversos trabalhos vêm sendo desenvolvido utilizando a plataforma CUDA. Jang et al. (6) apresentaram uma implementação de uma rede neural utilizando CUDA; Krizhevsky, Sutskever e Hinton (7) classificaram 1.3 milhões de imagens utilizando redes neurais profundas e computação paralela; Veronese e Krohling (8) utilizaram a computação paralela aplicada à otimização por meio de algoritmo evolutivo.

Uma outra maneira simples de se obter paralelismo utilizando apenas os núcleos de uma CPU é utilizando OpenMP, uma interface de programação paralela de memória compartilhada para arquitetura de múltiplos processadores (9). Sendo assim, neste trabalho a computação paralela utilizando GPU e OpenMP é realizada para computar a multiplicação de matrizes e seus resultados são comparados com versão sequencial utilizando a CPU. O restante deste artigo esta organizado da seguinte forma: na seção 2 são apresentados conceitos básicos relacionados a arquitetura de uma GPU; na seção 3 a multiplicação de matrizes é discutida; na seção 4 os resultados experimentais são analisados; por fim, na seção 5 é realizada uma breve conclusão.

2 Arquitetura de GPUs - uma visão geral

Nesta seção, são apresentados os conceitos básicos relacionados a arquitetura de GPUs e a plataforma CUDA.

2.1 Arquitetura

GPUs são unidades de processamento especializadas em processamento gráfico da classe SIMT (Single Instruction Multiple Threads), neste modelo, múltiplas threads independentes são executadas de maneira concorrente utilizando uma instrução única (3). Desde 2006, com lançamento da plataforma CUDA, GPUs vêm sendo utilizadas com propósito geral de computação. Uma maneira simples de compreender a diferença entre uma GPU e uma CPU é comparar o modo que as mesmas processam suas tarefas. Uma CPU possui alguns núcleos otimizados para o processamento serial sequencial, enquanto uma GPU tem uma arquitetura paralela gigantesca que consiste em milhares de núcleos menores e eficientes criados para lidar com múltiplas tarefas simultaneamente (5). Como mostra a Figura 1, a CPU dedica grande parte dos seus circuitos ao controle, enquanto a GPU foca mais nas ALUs (Arithmetic Logic Units), fazendo com que a mesma seja mais adequada para cálculos paralelos.

Figura 1: Comparação entre CPU e GPU

Um pré-requisito para codificar em CUDA é conhecer a arquitetura das GPUs. Quanto mais conhecimento o programador adquirir, mais otimizados serão seus códigos. Existem diferentes arquiteturas de GPU, todavia existem diversas características comuns a todas elas. De maneira geral, GPUs da NVIDIA são divididas em SMs (Stream Multiprocessors), local onde um conjunto de threads, denominado warp, é executado. Cada SM possui vários núcleos de processamento chamados de CUDA cores (ou Streaming Processor - SP), que por sua vez possuem pipelines completos de operações aritméticas (ALU) e pontos flutuantes (Floating Point Unit - FPU). Cada SM possui uma memória dedicada chamada de memória compartilhada (shared memory). Essa memória é fisicamente próxima dos cores e possuem acesso muito rápido, porém são pequenas (na ordem de KB). Além disso uma SM possui um cache de instruções, um cache de constantes, uma unidade de funções especiais (SFU - Special Function Units) e um bloco para escalonar warps (Multithread Instruction Fetch and Issue Unit - MT Issue). O número de SMs, cores, tamanho de cache, dentre outros, podem variar de acordo com o modelo da GPU. A Figura 2 ilustra um SM de uma NVIDIA G80/G90, na qual possui 8 CUDA cores.

Figura 2: Streaming Multiprocessor de uma NVIDIA G80/G90

Threads podem ser agrupadas em blocos que executam na mesma SM compartilhando a mesma shared memory. Cada SM possui um limite máximo de blocos, que por sua vez possui um limite máximo de threads. Um conjunto de blocos criados por um código CUDA é chamado de grid. Os valores dos limites também são parâmetros de cada modelo de GPU.

As principais memórias de uma GPU são descritas a seguir:

  • Memória global: é a memória principal da GPU. Pode ser acessada por todas as threads/cores, porém possui alta latência e baixo throughput.

  • Memória compartilhada: como já mencionado, é a memória dedicada de cada SM que possui baixa latência. Somente threads de um mesmo bloco pode acessá-la.

  • Memória local: possui este nome pois é a memória específica de uma thread.

Das memórias descritas, a CPU tem acesso somente a memória global. A distribuição de threads e hierárquia de memória é ilustrada pela Figura 3.

Independente da arquitetura da GPU e número de cores por SMs, cada warp pode abrigar no máximo 32 threads executando ao mesmo tempo. Dessa forma para uma melhor performance é recomendado utilizar o número de threads por bloco multiplo de 32. Quando um bloco de threads é entregue a um SM, o mesmo particiona essas threads em warps e cada thread recebe um índice único. A thread decide em qual dado atuar de acordo com seu índice. Por fim, a SM escalona cada bloco de threads para execução em seus cores e os mesmos são executados de maneira aleatória.

Figura 3: Distribuição de threads em uma GPU

2.2 Plataforma CUDA

Como já mencionado, CUDA é a plataforma que permite utilizar uma GPU com propósito geral de computação. CUDA é um software desenvolvido pela NVIDIA corporation, portanto somente GPUs da NVIDIA são capaz de executar códigos CUDA. Uma opção à plataforma é a OpenCL (10) que permite codificar tanto para GPUs NVIDIA quanto para ATI/AMD. O desenvolvimento de códigos em CUDA podem ser realizados utilizando C/C++ juntamente com alguns comandos específicos da plataforma. De maneira geral, programadores com alguma experiência em C/C++ não encontram dificuldades com a linguagem.

Para executar uma aplicação em uma GPU sempre será necessário um código na CPU. Basicamente, a codificação consistem em um programa na CPU enviar dados para GPU computar alguma operação específica e a GPU devolve para a CPU o resultado final. Essa comunicação deve ser realizada através da memória principal da CPU e a memória global da GPU. Resumidamente, para se executar um código na GPU são necessários cumprir os seguintes passos em CUDA:

  • Implementar uma função especial chamada Kernel. Essa função será executada dentro da GPU e utilizará os índices das threads para operar nos dados.

  • Definir a quantidade de grids e blocos definindo assim o número total de threads que serão executados na GPU. Esses parâmetro dependem da arquitetura da placa e quanto mais o programador é familiar com a mesma, mais proveito ele tira da plataforma.

  • Enviar os dados a serem executados da memória da CPU para memória global da GPU. Após a conclusão da execução da GPU o caminho inverso deve ser realizado.

Para maior compreensão da plataforma CUDA, bem como detalhes específicos de codificação, sugere-se ao leitor o manual da plataforma (3).

3 Multiplicação de matrizes

Nesta seção, é realizada uma discussão da metodologia de multiplicação de matrizes utilizado GPU e OpenMP com CPU. Recapitulando brevemente a multiplicação de matrizes, supõe-se que deseja-se multiplicar as matrizes por . O resultado dessa operação sera uma matriz , ou seja, . A Figura 4 mostra de maneira intuitiva a ideia por trás da multiplicação de matrizes. Para se obter os elementos de , cada linha de é multiplicada elemento a elemento por uma coluna de . Ao final o valor é agregado por meio de um somatório. É importante ressaltar que para a multiplicação ser viável, o número de colunas de deve ser igual ao número de linhas de .

Figura 4: Exemplo de multiplicação de matrizes com uma matriz 2 x 2

Dessa maneira é fácil observar que multiplicar matrizes é um bom exemplo de computação paralela. Cada elemento de é computado de maneira independente, logo, pode ser paralelizado.

3.1 Multiplicando matrizes na GPU

A primeira abordagem para paralelização da multiplicação de matrizes utilizando uma GPU é disparar diversas threads fazendo com que cada uma calcule um elemento da matriz resultante . Nesta abordagem, cada thread ler uma linha de e uma coluna de para computar o elemento de , sendo e . Na Figura 5 é ilustrada essa abordagem (3). Utilizando essa abordagem de multiplicação, as matrizes e são é carregadas na memória global e vezes, respectivamente. Com isso o algoritmo faz muitos acessos à memória global, que possui alta latência e baixo throughput.

Figura 5: Multiplicação de matrizes na qual cada thread calcula

Com intuito de extrair a eficiência máxima que uma GPU pode entregar, a segunda abordagem de multiplicação de matrizes na GPU tem como objetivo utilizar a memória compartilhada de cada bloco de threads visando reduzir o número de acessos a memória global do dispositivo. Para isso as matrizes são subdivididas em pequenos blocos como mostrado na Figura 6 (3). Essa técnica é conhecida como multiplicação por ladrilhamento e diferentemente da abordagem anterior, no qual toda linha de e coluna era multiplicada de uma só vez gerando assim um elemento de , no ladrilhamento tem-se submatrizes de e que vão gerar um valor parcial de um elemento de . Quando todos os ladrilhos forem processados o valor final de cada um dos elementos da matriz será o somatório de cada elemento parcial obtido pela multiplicação de cada uma das linhas e colunas submatrizes e , respectivamente.

Figura 6: Multiplicação de matrizes utilizando ladrilhamento

O objetivo de principal de se usar o ladrilhamento em GPU é carregar cada submatriz na memória compartilhada do bloco de threads. O caso ideal seria alocar toda matriz dentro de uma memória compartilhada. Porém, como já mencionado, essa memória é de tamanho reduzido quando comparada com a memória global. Dessa forma, a ideia é carregar na memória compartilhada apenas submatrizes de e , na qual cada bloco de threads pode compartilhar seus dados de maneira rápida. Com isso, um requisito de projeto é que o ladrilho caiba dentro de um bloco de threads da GPU. O valor do tamanho do bloco varia de acordo com o dispositivo e cabe ao programador conhecer a arquitetura para melhor aproveitá-la.

Fazendo uma breve comparação entre as duas abordagens de multiplicação de matrizes em GPU apresentadas anteriormente, a Figura 7 apresenta um gráfico de tempo de execução de cada um dos métodos em relação a ordem das matrizes multiplicadas. É possível observar o ganho de performance na medida que as matrizes aumentam. A arquitetura utilizada para gerar este gráfico será discutida na seção IV.

Figura 7: Comparação entre as abordagens com e sem ladrilhamento na GPU utilizando precisão simples. As matrizes A e B são quadradas e os tamanhos são indicados no gráfico

3.2 Multiplicando matrizes na CPU com OpenMP

A ideia de ladrilhamento das matrizes, discutida na seção anterior, também aplicada na CPU. Na GPU, as threads são disparadas e com os índices de cada uma delas é possível operar sobre os dados de acordo com seus blocos. Na CPU esses índices são obtidos por loops de controle. O objetivo principal do ladrilhamento também é o mesmo: tirar proveito da arquitetura de CPU através da memória cache de acesso rápido. Mesmo no código sequencial já se obtém um ganho de performance, como mostrado na Figura 8

Figura 8: Comparação entre as abordagens com e sem ladrilhamento na CPU para precisão simples. As matrizes A e B são quadradas e os tamanhos são indicados no gráfico

Para realizar paralelização por meio da CPU é utilizado a OpenMP. A API dá suporte as linguagens C/C++ e Fortran, e basicamente o que deve ser incluído ao código original são diretiva em trechos de códigos que devem ser paralelizado. Com isso, a API é capaz de disparar threads utilizando os núcleos da CPU em questão. Um exemplo de diretiva para paralelizar um loop em C é descrito a seguir:

#pragma omp parallel for default(none) \
  shared(n,x,y) private(i)
for (i=0; i<n; i++)
    Ψx[i] += y[i];
}

A simples anotação indica para paralelizar o loop na sequência, compartilhar as variáveis e e manter privado. Dessa maneira simples já é possível obter ganho de desempenho utilizando os núcles da CPU. O número de threads disparadas como default é uma por núcleo do processador, todavia o programador pode alterar esse valor por meio da variável de ambiente OMP_NUM_THREAD. Para mais informações sobre OpenMP sugere-se (9). Na próxima seção será abordada a comparação entre a implementação sequencial e paralela utilizando OpenMP.

4 Experimentos

Nesta seção são realizados experimentos para comparar a performance da multiplicação de matrizes utilizando a computação sequencial e a paralela utilizando CUDA e OpenMP. Os experimentos foram executados em uma máquina com sistema operacional Linux, distribuição Xubuntu, com processador intel core i7, 2.5 GHz, 2 núcleos, 3 MB de memória cache e 6GB de memória RAM e uma placa gráfica NVIDIA Geforce 940M. A apresentação da GPU e as considerações para máximo desempenho serão descritos na sequências. O código de todos os experimentos esta disponível neste repositório do Github.

Os experimentos serão realizados da seguinte forma:

  • A multiplicação será realizada considerando a implementação por ladrilho sequencial, paralelizada em OpenMP e em GPU

  • As abordagens serão aplicadas para precisão simples (float) e dupla (double)

  • A multiplicação será executada em três diferentes configurações: 1 vez, 100 vezes, e 1000 vezes. As duas últimas paralelizadas com OpenMP

  • As matrizes e os ladrilhos serão ajustados para máximo desempenho

  • O desempenho será medido em termos de tempo de execução, speedup, através da lei de Amdahl (11), e em GFLOPS

4.1 Características da GPU utilizada

A GPU utilizada neste trabalho, modelo Geforce 940M, possui as seguintes características:

  • 3 SMs

  • 384 CUDA cores, 128 por SM

  • 32 threads por warp

  • Máximo de 1024 threads por bloco

  • Máximo de 2048 threads SM

  • Máximo de 32 blocos por SM

  • Memória global: 2 GB

  • Memória compartilhada: 49 KB

  • Processamento máximo pode atingir 790.3 GFLOPS para precisão simples e 24.7 GFLOPS para dupla

De acordo com as configurações do dispositivo, o tamanho máximo do ladrilho, para que o mesmo encaixe em um bloco, deverá ser threads, limite da placa. Como a SM suporta até 2048 threads, o número de blocos por SM será igual a blocos, respeitando também o limite de blocos por SM. Na Figura 9 é ilustrado a variação de GFLOPS de acordo com o tamanho do bloco escolhido para dados de precisão simples. Pode ser observado que na medida que as matrizes aumentam, as configurações de blocos e diminuem a capacidade de cálculo em relação ao tamanho .

Figura 9: Comparação de GFLOPS de acordo com tamanho do bloco para precisão simples. As matrizes A e B são quadradas e os tamanhos são idicados no gráfico

4.2 Experimento parte I - Multiplicação para precisão simples

Nesta primeira etapa será analisado os resultados para matrizes de precisão simples (float). Para obter eficiência máxima, as matrizes escolhidas possuem número de linhas e colunas divisíveis pelo tamanho máximo de ladrilho, ou seja um bloco de . Dessa forma um grid de blocos se encaixa perfeitamente nas dimensões da matriz, sem necessidade de verificações na função de kernel da GPU, o que ocorreria se o ladrilho não fosse divisível. Na Figura 10 é ilustrado um exemplo simples de distribuição de blocos na matriz. Neste caso existe uma matriz e um ladrilho/bloco , como a divisão dimensões da matriz pelo bloco é inteira, é utilizado um grid de blocos de .

Figura 10: Um grid de blocos em uma matriz

Como o intuito deste trabalho é simplesmente comparar performance, essa premissa é compreensível. Que fique claro que isso não é uma limitação da GPU, é simplesmente um artifício para verificar performance máxima. Nada impede da verificação ser acrescida no Kernel e o tamanho da matriz ser variável. As ordens das matrizes escolhidas, tanto de quanto de , variam de até . Com esses valores, tanto para precisão simples, quanto para precisão dupla, as matrizes cabem dentro da memória global. No caso máximo, considerando a precisão dupla, bytes bytes bytes MB, ao alocar espaço para 3 matrizes deste mesmo tamanho, tem-se MB, bem abaixo dos 2GB possíveis.

Para a abordagem sequencial e paralela via OpenMP o tamanho do bloco escolhido é o mesmo da GPU. Além disso, como a CPU utilizada possui apenas dois núcleos, o número de threads disparadas será igual a quatro, duas por núcleo. Para facilitar a visualização dos gráficos, primeiramente a abordagem sequencial é comparada com a paralela via OpenMP. Na sequencia, a abordagem paralela via OpenMP é comparada com a GPU. Essa divisão foi escolhida pois, conforme será exposto, o desempenho da GPU é extremamente melhor do que as duas anteriores. Se todos os gráficos fossem plotados juntos seria muito difícil comparar as duas piores por questão de escala.

4.2.1 Cpu CPU + OpenMP

O desempenho do experimento executado apenas uma vez utilizando a CPU e a CPU + OpenMP é ilustrado nos gráficos da Figura 11. Como mostrado na Figura 11(a), a partir da multiplicação das matrizes de ordem já se obtém um pequeno ganho de de processamento. O tempo de execução, como ilustrado na Figura 11(b), começa a fazer diferença na ordem , o que também é mostrado no gráfico de speedup na 11(c).

Comparando o desempenho anterior com as execuções de 100 e 1000 vezes, ilustradas nos gráficos da Figura 12 e 13, respectivamente, é possível notar nos gráficos das Figura 12(a) e 13(a) que o processamento cresce um pouco, principalmente para as multiplicações com matrizes de menor ordem. A diferença de tempo computacional, apontada nos gráficos das Figura 12(b) e 13(b) se mantém praticamente a mesma. Por fim ocorre uma variação de speedup, quando comparado os gráficos os gráficos das Figura 11(c), 12(c) e 13(c). Neste caso, como os tempos de execução até a ordem são bem baixos (na ordem de mseg), o valor do speedup é mais constante e coerente a partir da ordem , se mantendo acima de 1, com máximo de 1.5.

De maneira geral a paralelização via OpenMP obtém um ganho em relação a sequencial quando as matrizes multiplicadas alcançam a ordem de . Utilizando essa CPU o ganho não tão evidente pois a mesma possui apenas dois núcleos, o que limita o potencial do framework. Todavia, devido a facilidade de se incluir a paralelização, ainda assim é recomendável o uso da API para matrizes grandes.

(a) Desempenho em GFLOPS
(b) Desempenho em tempo de execução
(c) Speedup da CPU + OpenMP em relação a CPU
Figura 11: Desempenho CPU CPU + OpenMP para multiplicação de matrizes executada apenas uma vez - Precisão simples
(a) Desempenho em GFLOPS
(b) Desempenho em tempo de execução
(c) Speedup da CPU + OpenMP em relação a CPU
Figura 12: Desempenho CPU CPU + OpenMP para multiplicação de matrizes executada 100 vezes - Precisão simples
(a) Desempenho em GFLOPS
(b) Desempenho em tempo de execução
(c) Speedup da CPU + OpenMP em relação a CPU
Figura 13: Desempenho CPU CPU + OpenMP para multiplicação de matrizes executada 1000 vezes - Precisão simples

4.2.2 CPU + OpenMP Gpu

O desempenho do experimento executado apenas uma vez é ilustrado nos gráficos da Figura 14. Como mostrado na Figura 14(a), na primeira configuração de matriz a GPU possui uma taxa de GFLOPS cerca de 30x maior do que a CPU+OpenMP. A partir desta configuração a proporção dispara para mais de 200x, deixando claro que a GPU efetua muito mais cálculos por segundo do que a CPU e a CPU+OpenMP. A diferença do tempo de execução, até a multiplicação de matrizes de , não é tão perceptível pois está na ordem de milissegundos, como ilustrado na Figura 14(b). A partir da configuração o tempo de execução começa a fazer bastante diferença, sendo que para última configuração a GPU executa a multiplicação em torno de 1 seg e a CPU+OpenMP mais de 50 seg. Por fim, o speedup é ilustrado na Figura 14(c), na qual é possível observar a superioridade da GPU para com a CPU+OpenMP, principalmente quando a ordem das matrizes aumentam.

O desempenho dos experimentos executando a multiplicação 100 e 1000 vezes são ilustrados nas Figura 15 e 16, respectivamente. Sendo assim, é possível observar que a diferença de GFLOPS se mantém. Todavia, no caso da GPU, o valor aumenta um pouco para as matrizes menores. No caso da ordem ocorre um acréscimo de quase 3x. As diferenças de tempo de execução também se matém, oscilando muito pouco. Com isso o valor do speedup também oscila pouco, sendo o valor mínimo em torno de 25 e o máximo por volta de 325.

De maneira geral, baseado nos gráficos apresentados nas Figuras 11 a 16, é possível notar a diferença de desempenho da GPU para CPU e CPU+OpenMP. A capacidade de processamento é extremamente maior sendo muito vantajoso seu uso, principalmente quando a multiplicação de matrizes atinge a ordem de , no qual o crescimento da curva de tempo de execução para CPU e CPU+OpenMP é muito alto.

(a) Desempenho em GFLOPS
(b) Desempenho em tempo de execução
(c) Speedup da CPU + OpenMP em relação a CPU
Figura 14: Desempenho GPU CPU + OpenMP para multiplicação de matrizes executada apenas uma vez - Precisão simples
(a) Desempenho em GFLOPS
(b) Desempenho em tempo de execução
(c) Speedup da CPU + OpenMP em relação a CPU
Figura 15: Desempenho GPU CPU + OpenMP para multiplicação de matrizes executada 100 vezes - Precisão simples
(a) Desempenho em GFLOPS
(b) Desempenho em tempo de execução
(c) Speedup da CPU + OpenMP em relação a CPU
Figura 16: Desempenho GPU CPU + OpenMP para multiplicação de matrizes executada 1000 vezes - Precisão simples

4.3 Experimento parte II - Multiplicação para precisão dupla

Nesta segunda parte de experimentos serão analisados os resultados para matrizes de precisão dupla (double). As configurações de matrizes e testes seguem os mesmos moldes descritos na parte I. Nesta ocasião é esperado um queda do poder de processamento da GPU e consequentemente o aumento no tempo de execução da matriz de doubles. Isso ocorre devido ao fato da GPU ter menos processadores dedicados a cálculo de precisão dupla.

4.3.1 Cpu CPU + OpenMP

O desempenho do experimento executado apenas uma vez utilizando a CPU e a CPU + OpenMP para precisão dupla é ilustrado nos gráficos da Figura 17. Como mostrado na Figura 17(a), o processamento é bem próximo ao da precisão simples. A diferença de tempo de execução também segue a mesma linha, sendo perceptível a partir da ordem , como mostrado na Figura 17(b). Como os tempos até a ordem estão na ordem de milissegundos, o speedup e mais constante a partir da ordem , como mostrado na Figura 17(c).

Novamente, comparando o desempenho anterior com as execuções de 100 e 1000 vezes, ilustradas nos gráficos da Figura 18 e 19, respectivamente, é possível notar nos gráficos das Figuras 18(a) e 19(a) que o processamento estabiliza em torno de 0.4 e 0.5. A diferença de tempo computacional, apontada nos gráficos das Figuras 18(b) e 19(b) sem mantém quase que inalterados. Por fim, o speedup possui um comportamento um tanto quanto estranho para as duas primeiras matrizes, todavia, para as demais, o valor estabiliza entre 1.3 e 1.6.

A conclusão da comparação entre as duas abordagens é semelhante a precisão simples, a paralelização via OpenMP obtém um ganho em relação a sequencial quando as matrizes multiplicadas alcançam a ordem de . Vale a pena ressaltar novamente, que essa CPU possui apenas 2 núcleos de processamento.

(a) Desempenho em GFLOPS
(b) Desempenho em tempo de execução
(c) Speedup da CPU + OpenMP em relação a CPU
Figura 17: Desempenho CPU CPU + OpenMP para multiplicação de matrizes executada apenas uma vez - Precisão Dupla
(a) Desempenho em GFLOPS
(b) Desempenho em tempo de execução
(c) Speedup da CPU + OpenMP em relação a CPU
Figura 18: Desempenho CPU CPU + OpenMP para multiplicação de matrizes executada 100 vezes - Precisão Dupla
(a) Desempenho em GFLOPS
(b) Desempenho em tempo de execução
(c) Speedup da CPU + OpenMP em relação a CPU
Figura 19: Desempenho CPU CPU + OpenMP para multiplicação de matrizes executada 1000 vezes - Precisão Dupla

4.3.2 Gpu CPU + OpenMP

Desta vez o experimento é executado 1, 100 e 1000 vezes para precisão dupla, como ilustrados nas Figuras 20, 21 e 22, respectivamente. Para o caso de uma execução, na Figura 20(a) é ilustrado a diferença de GFLOPS entre a CPU+OpenMP e a GPU. Ainda existe uma boa diferença de processamento, todavia muito menor do que a apresentada para a precisão simples. A diferença de tempo de execução, mostrado no gráfico da Figura 20(b), ainda é bem discrepante quando a ordem das matrizes multiplicadas aumentam. O speedup, mostrado na Figura 20(c), também diminui bastante em relação a precisão simples, porém ainda é possível observar o ganho em relação a GPU. Especificamente para a multiplicação de matrizes de ordem , o speedup cresce rapidamente. Espera-se que para os testes com mais execuções esse valor seja convergido para um número mais próximo do padrão da curva. Na próxima subseção será apresentado gráficos comparando as performaces para precisão simples e dupla.

Abordando a multiplicação executada 100 e 1000 vezes, nos gráfico das Figuras 21 e 22, é possível observar que a quantidade de GFLOPS a partir da ordem se estabiliza em torno de 18. A diferença de tempo de execução, ilustrada nos gráficos das Figuras 21(b) e 22(b), continuam bem alta, como ocorre na precisão simples. Todavia, o tempo de execução da GPU aumenta um pouco quando comparada com o cálculo com floats (essa diferença será melhor observada na próxima subseção). Já nos gráficos de speedup das 21(c) e 22(c), é possível observar que para a menor matriz é obtido o menor valor, assim como anteriormente, todavia, para ordem o valor se estabiliza em torno de 30 e o speedup máximo fica em torno 43, valores abaixo da precisão simples, como já era esperado.

(a) Desempenho em GFLOPS
(b) Desempenho em tempo de execução
(c) Speedup da CPU + OpenMP em relação a CPU
Figura 20: Desempenho GPU CPU + OpenMP para multiplicação de matrizes executada apenas uma vez - Precisão Dupla
(a) Desempenho em GFLOPS
(b) Desempenho em tempo de execução
(c) Speedup da CPU + OpenMP em relação a CPU
Figura 21: Desempenho GPU CPU + OpenMP para multiplicação de matrizes executada 100 vezes - Precisão Dupla
(a) Desempenho em GFLOPS
(b) Desempenho em tempo de execução
(c) Speedup da CPU + OpenMP em relação a CPU
Figura 22: Desempenho GPU CPU + OpenMP para multiplicação de matrizes executada 1000 vezes - Precisão Dupla

4.4 Comparação entre precisão simples e dupla

Com objetivo de realizar uma breve comparação entre os experimentos com precisão simples e dupla na GPU, são ilustrados na Figura 23 os gráficos de comparação de GFLOPS, tempo de execução e speedup em relação a CPU + OpenMP. Foi utilizado a configuração de 100 execuções. A partir do gráfico na Figura 23(a), é possível observar que a partir da multiplicação de matrizes de ordem , o processamento da precisão simples cresce substancialmente quando comparada com a dupla. A partir deste ponto, ambas se tornam estáveis. No tempo de execução, ilustrado na Figura 23(b), a partir da ordem a diferença de tempo já perceptível. Por fim, na comparação de speedup em relação a CPU, ilustrada na Figura 23(c), é possível perceber que na medida que a ordem das matrizes aumentam, o speedup da precisão simples se distancia da precisão dupla.

(a) Desempenho em GFLOPS
(b) Desempenho em tempo de execução
(c) Speedup da CPU + OpenMP em relação a CPU
Figura 23: Comparação do desempenho da GPU para precisão simples e dupla

5 Conclusão

Neste trabalho foi realizado uma comparação entre multiplicação de matrizes utilizando computação paralela e sequencial. A paralelização dos códigos foi alcançada utilizando CUDA, plataforma de codificação em GPU, e OpenMP, framework de paralelização em CPU. Para realizar a comparação das abordagens, foram elaborados diversos experimentos utilizando diferentes tamanhos de matrizes. Resumidamente, os resultados dos experimentos mostraram a importância da paralelização ao multiplicar matrizes, principalmente quando o número de elementos das matrizes utilizadas ultrapassa a ordem de 1 milhão. Para casos como este, a GPU se mostrou centenas de vezes mais rápida do que a CPU e a CPU + OpenMP. Também foram analisados a multiplicação com precisão simples e dupla. Neste caso, a paralelização via GPU para precisão simples se mostra mais eficiente do que a dupla pelo fato de sua arquitetura ser privilegiada com número de processadores em relação a dupla. Com isso, vale a pena optar pela precisão simples se a dupla não for um limitante da aplicação. Por fim, ficou claro que para aplicações que utilizam matrizes de grande porte, paralelizar os processos utilizando GPU é uma forma extremamente eficiente de aumentar a performance final.

Referências

References

  • Kakaradov (2004) Bi Kakaradov. Ultra-fast matrix multiplication: An empirical analysis of highly optimized vector algorithms. Stanford Undergraduate Research Journal, 3:33–36, 2004.
  • Sanders and Kandrot (2010) Jason Sanders and Edward Kandrot. CUDA by example: an introduction to general-purpose GPU programming. Addison-Wesley Professional, 1 edition, 2010.
  • NVIDIA (2016a) NVIDIA. CUDA C programming guide - Design Guide. NVIDIA Corporation, 2016a. Disponível em: www.docs.nvidia.com/cuda/pdf/CUDA_C_Programming_Guide.pdf. Acesso 25 de novembro de 2016.
  • Cook (2012) Shane Cook. CUDA programming: a developer’s guide to parallel computing with GPUs. Morgan Kaufmann, 1 edition, 2012.
  • NVIDIA (2016b) NVIDIA. CUDA Zone. NVIDIA Corporation, 2016b. Disponível em: https://developer.nvidia.com/cuda-zone. Acesso em 25 de novembro de 2016.
  • Jang et al. (2008) Honghoon Jang, Anjin Park, and Keechul Jung. Neural network implementation using cuda and openmp. In Digital Image Computing: Techniques and Applications (DICTA), 2008, pages 155–161. IEEE, 2008.
  • Krizhevsky et al. (2012) Alex Krizhevsky, Ilya Sutskever, and Geoffrey E Hinton. Imagenet classification with deep convolutional neural networks. In Advances in neural information processing systems, pages 1097–1105, 2012.
  • Veronese and Krohling (2010) Lucas de P Veronese and Renato A Krohling. Differential evolution algorithm on the gpu with c-cuda. In IEEE Congress on Evolutionary Computation, pages 1–7. IEEE, 2010.
  • Chapman et al. (2008) Barbara Chapman, Gabriele Jost, and Ruud Van Der Pas. Using OpenMP: portable shared memory parallel programming, volume 10. MIT press, 2008.
  • AMD (2016) AMD. OpenCL Zone – Accelerate Your Applications. AMD Corporation, 2016. Disponível em: http://developer.amd.com/tools-and-sdks/opencl-zone/. Acesso em 25 de novembro de 2016.
  • Hennessy and Patterson (2011) John L Hennessy and David A Patterson. Computer architecture: a quantitative approach, chapter 8, pages 29–32. Elsevier, 2 edition, 2011.
Comments 0
Request Comment
You are adding the first comment!
How to quickly get a good reply:
  • Give credit where it’s due by listing out the positive aspects of a paper before getting into which changes should be made.
  • Be specific in your critique, and provide supporting evidence with appropriate references to substantiate general statements.
  • Your comment should inspire ideas to flow and help the author improves the paper.

The better we are at sharing our knowledge with each other, the faster we move forward.
""
The feedback must be of minimum 40 characters and the title a minimum of 5 characters
   
Add comment
Cancel
Loading ...
361099
This is a comment super asjknd jkasnjk adsnkj
Upvote
Downvote
""
The feedback must be of minumum 40 characters
The feedback must be of minumum 40 characters
Submit
Cancel

You are asking your first question!
How to quickly get a good answer:
  • Keep your question short and to the point
  • Check for grammar or spelling errors.
  • Phrase it like a question
Test
Test description