Computação de alto desempenho utilizando CUDA



Documentos relacionados
ARQUITETURA DE COMPUTADORES

PARALELIZAÇÃO DE APLICAÇÕES NA ARQUITETURA CUDA: UM ESTUDO SOBRE VETORES 1

Capacidade = 512 x 300 x x 2 x 5 = ,72 GB

Arquitetura e Programação de GPU. Leandro Zanotto RA: Anselmo Ferreira RA: Marcelo Matsumoto RA:

Tais operações podem utilizar um (operações unárias) ou dois (operações binárias) valores.

Sistemas Operacionais

Informática I. Aula 5. Aula 5-13/05/2006 1

O hardware é a parte física do computador, como o processador, memória, placamãe, entre outras. Figura 2.1 Sistema Computacional Hardware

Sistemas Distribuídos

Hardware (Nível 0) Organização. Interface de Máquina (IM) Interface Interna de Microprogramação (IIMP)

3/9/2010. Ligação da UCP com o barramento do. sistema. As funções básicas dos registradores nos permitem classificá-los em duas categorias:

Sistemas Operacionais

1.1. Organização de um Sistema Computacional

3. O NIVEL DA LINGUAGEM DE MONTAGEM

Orientação a Objetos

ULA Sinais de Controle enviados pela UC

Prof. Marcos Ribeiro Quinet de Andrade Universidade Federal Fluminense - UFF Pólo Universitário de Rio das Ostras - PURO

IFPE. Disciplina: Sistemas Operacionais. Prof. Anderson Luiz Moreira

Unidade Central de Processamento (CPU) Processador. Renan Manola Introdução ao Computador 2010/01

Capítulo 8 Arquitetura de Computadores Paralelos

Arquiteturas RISC. (Reduced Instructions Set Computers)

Tabela de Símbolos. Análise Semântica A Tabela de Símbolos. Principais Operações. Estrutura da Tabela de Símbolos. Declarações 11/6/2008

Aula 3. Sistemas Operacionais. Prof: Carlos Eduardo de Carvalho Dantas

Organização e Arquitetura de Computadores I. de Computadores

Visão Geral de Sistemas Operacionais

NOTAS DE AULA Prof. Antonio Carlos Schneider Beck Filho (UFSM) Prof. Júlio Carlos Balzano de Mattos (UFPel) Arquitetura de Von Neumann

Sistemas Computacionais II Professor Frederico Sauer

Como foi exposto anteriormente, os processos podem ter mais de um fluxo de execução. Cada fluxo de execução é chamado de thread.

Introdução aos Computadores

UNIVERSIDADE FEDERAL DO RIO GRANDE DO SUL INSTITUTO DE INFORMÁTICA INFORMÁTICA APLICADA

Prof. Engº esp Luiz Antonio Vargas Pinto

FACULDADE PITÁGORAS DISCIPLINA: ARQUITETURA DE COMPUTADORES

BARRAMENTO DO SISTEMA

Sistemas Operacionais

Sistema de Computação

A memória é um recurso fundamental e de extrema importância para a operação de qualquer Sistema Computacional; A memória trata-se de uma grande

ESTUDO DE CASO WINDOWS VISTA

Notas da Aula 17 - Fundamentos de Sistemas Operacionais

1. CAPÍTULO COMPUTADORES

5 Mecanismo de seleção de componentes

Processos e Threads (partes I e II)

Esta dissertação apresentou duas abordagens para integração entre a linguagem Lua e o Common Language Runtime. O objetivo principal da integração foi

Máquinas Multiníveis

28/9/2010. Paralelismo no nível de instruções Processadores superescalares

Organização e Arquitetura de Computadores I. de Computadores

Figura 1 - O computador

7 Processamento Paralelo

6 - Gerência de Dispositivos

3. Arquitetura Básica do Computador

CAPÍTULO 7 NÍVEL DE LINGUAGEM DE MONTAGEM

Introdução a Informática. Prof.: Roberto Franciscatto

Notas da Aula 15 - Fundamentos de Sistemas Operacionais

Conceitos de Banco de Dados

Análises Geração RI (representação intermediária) Código Intermediário

Unidade 13: Paralelismo:

Para construção dos modelos físicos, será estudado o modelo Relacional como originalmente proposto por Codd.

Introdução à Arquitetura de Computadores

VIRTUALIZAÇÃO CONVENCIONAL

Sistemas Operativos. Threads. 3º ano - ESI e IGE (2011/2012) Engenheiro Anilton Silva Fernandes (afernandes@unipiaget.cv)

AULA4: PROCESSADORES. Figura 1 Processadores Intel e AMD.

SISTEMAS OPERACIONAIS ABERTOS Prof. Ricardo Rodrigues Barcelar

Sistemas Operacionais. Conceitos de um Sistema Operacional

TRABALHO COM GRANDES MONTAGENS

Componentes da linguagem C++

Programação de Sistemas

Disciplina: Introdução à Informática Profª Érica Barcelos

Capítulo 4. MARIE (Machine Architecture Really Intuitive and Easy)

4 Estrutura do Sistema Operacional Kernel

LP II Estrutura de Dados. Introdução e Linguagem C. Prof. José Honorato F. Nunes honorato.nunes@ifbaiano.bonfim.edu.br

MÓDULO 7 Modelo OSI. 7.1 Serviços Versus Protocolos

Sistemas Operacionais Gerência de Dispositivos

Bruno Pereira Evangelista.

Um Driver NDIS Para Interceptação de Datagramas IP

Algoritmos e Programação (Prática) Profa. Andreza Leite andreza.leite@univasf.edu.br

BACHARELADO EM SISTEMAS DE INFORMAÇÃO EaD UAB/UFSCar Sistemas de Informação - prof. Dr. Hélio Crestana Guardia

Introdução a Java. Hélder Nunes

Introdução à Computação: Sistemas de Computação

O processador é composto por: Unidade de controlo - Interpreta as instruções armazenadas; - Dá comandos a todos os elementos do sistema.

Sistemas Operacionais. Prof. André Y. Kusumoto

IW10. Rev.: 02. Especificações Técnicas

PROJETO LÓGICO DE COMPUTADORES Prof. Ricardo Rodrigues Barcelar

Paralelismo. Computadores de alto-desempenho são utilizados em diversas áreas:

1. NÍVEL CONVENCIONAL DE MÁQUINA


7.Conclusão e Trabalhos Futuros

Sistemas Operacionais

Sistemas Operacionais

INTRODUÇÃO ÀS LINGUAGENS DE PROGRAMAÇÃO

ORGANIZAÇÃO DE COMPUTADORES MÓDULO 8

Dadas a base e a altura de um triangulo, determinar sua área.

Sistemas Distribuídos. Professora: Ana Paula Couto DCC 064

Sistemas Operacionais. Prof. André Y. Kusumoto

UNIVERSIDADE FEDERAL DO PARANÁ UFPR Bacharelado em Ciência da Computação

SISTEMAS OPERACIONAIS CAPÍTULO 3 CONCORRÊNCIA

Arquitetura de Computadores - Arquitetura RISC. por Helcio Wagner da Silva

11/3/2009. Software. Sistemas de Informação. Software. Software. A Construção de um programa de computador. A Construção de um programa de computador

Memória Cache. Prof. Leonardo Barreto Campos 1

Sistemas Operacionais

Capítulo 1 Introdução

Sistemas Operacionais

Transcrição:

Computação de alto desempenho utilizando CUDA Bruno Cardoso Lopes, Rodolfo Jardim de Azevedo 1 Instituto de Computação Universidade Estadual de Campinas (Unicamp) Caixa Postal 6176 13083-970 Campinas SP Brasil bruno.cardoso@gmail.com, rodolfo@ic.unicamp.br Abstract. The last real time graphic processors in the market are implementations based on the programmable stream processor model. This works presents the internals of these processors and the programming languages that were created to develop software to them, focusing on the NVIDIA 8 series and the CUDA programming model. With this new programming model and the new graphics cards, it is possible to obtain impressive gains in performance when comparing to conventional processors. Resumo. As recentes arquiteturas de processamento gráfico em tempo real são em sua maioria implementações comerciais do modelo de stream processors programáveis. Este trabalho explora o funcionamento interno desta arquitetura e as linguagens de programação criadas ao longo dos anos que facilitam a criação de software, com o foco nas placas gráficas da NVIDIA série 8 e o modelo de programação CUDA. Através deste modelo de programação e das novas placas de vídeo, é possível obter ganhos de desempenho de 1 ou 2 ordens de grandeza em relação aos processadores convencionais. 1. Introdução O processamento em tempo real envolvendo compressão de imagens, processamento de sinais, cálculos para gráficos 3D (como a renderização de polígonos) e codificação de vídeo é bastante complexo envolve operações matemáticas difíceis requerendo cálculos na ordem de bilhões de operações por segundo. O custo dessas operações faz com que diversos fabricantes escolham desenvolver processadores específicos para mídia. O custo e esforço de desenvolvimento envolvidos são altos e o hardware não é flexível não se adapta facilmente a evolução de novos algoritmos e programas de processamento de mídia. Com o objetivo de atender a esta demanda por flexibilidade, surge a motivação para o uso de Stream Processors programáveis. Com a introdução dos Stream Processors programáveis surgiu a necessidade de criar linguagens que pudessem possibilitar ao programador sua utilização de maneira simples. Essas linguagens foram evoluindo junto com as GPUs. Ambas já dominam o mercado de desenvolvimento de jogos e disputam lugar no mercado de computação de alto desempenho. A NVIDIA tem comercializado GPUs que são o estado da arte na implementação de Stream Processors, tratam-se das novas placas gráficas a partir das séries 8, Tesla e algumas Quadro. A nova tecnologia apresentada pela NVIDIA vêm acompanhada de um

1 INTRODUÇÃO 2 Figura 1. Pilha de software. Fonte [5] novo modelo de arquitetura e programação chamado CUDA (Compute Unified Device Architecture). A pilha de software (Figura 1) do CUDA, envolve uma API com suporte direto a diversas funções matemáticas, primitivas de computação gráfica, bibliotecas, suporte ao runtime e ao driver, que otimiza e gerencia a utilização de recursos diretamente com a GPU. Inicialmente, os processadores destas placas não eram flexíveis, atendendo apenas mercados restritos (jogos e aplicações 3D) e, com o tempo, foram se tornando Stream Processors, cada vez mais flexíveis e programáveis. A arquitetura e o modelo de programação iniciais destas placas não apresentavam propriedades que permitissem a sua utilização para computação de problemas genéricos devido a fatores como: a dificuldade em aprender uma linguagem nova, restrições de uso quanto ao acesso à memória DRAM das GPU s e limitação na largura de banda nestes acessos. Ilustrando o poder desta evolução (Figura 2), temos que o número de operações de ponto flutuante destes processadores, quando comparados com os processadores de propósito geral do mercado, cresce mais rápido ao longo dos anos. A grande novidade é a maneira eficiente que o CUDA juntamente com a arquitetura da GPU, possibilita o desenvolvimento de aplicações que podem explorar ao máximo o paralelismo de dados. Em uma dada aplicação, um mesmo trecho de código é executado em paralelo para pequenos blocos de dados, com a existência de várias pequenas caches e níveis de hierarquia de memória que escondem a latência de acesso a estes blocos (Figura 3).

1 INTRODUÇÃO 3 Figura 2. Evolução de processamento das GPU s. Fonte [4] Figura 3. Diferença arquiteturais entre CPU e GPU. Fonte [5]

1 INTRODUÇÃO 4 Estas pequenas caches são também chamadas de memórias on-chip compartilhadas e possuem acesso rápido para leitura e escrita, permitindo o compartilhamento de tarefas entre as threads (Figura 4). Estas memórias são parte da tecnologia de hierarquia de memória que diminui o acesso às memórias externas e, portanto, reduz o tempo de execução dos aplicativos, como será explicado com mais detalhes adiante. Figura 4. Memória compartilhada. Fonte [4] O CUDA possibilitou também o acesso a memória de maneira a suportar operações antes somente suportadas por CPU s, como as operações de gather e scatter (figura 5) com a memória DRAM da GPU (apenas a operação de gather era suportada nos modelos mais antigos). Figura 5. Operações de gather and scatter na memória. Fonte [4] Diversos aplicativos e algoritmos têm sido reescritos para utilizar CUDA a fim de atingir altos níveis de desempenho, um exemplo interessante é a aceleração criptográfica do AES [9], onde foi atingido um desempenho 20 vezes maior comparado ao OpenSSL,

2 STREAM PROCESSORS 5 que é referência na implementação de primitivas criptográficas. A figura 6 mostra os resultados obtidos de desempenho entre o tempo gasto com uma GeForce e um processador comum. Figura 6. Gráfico de desempenho para AES 256. Fonte [9] Este trabalho está organizado da seguinte forma: A seção 2 descreve características gerais dos Stream Processors e é seguida pela seção 3 que descreve a arquitetura das GPUs NVIDIA série 8. Na sequência, a seção 4 mostra o paradigma de programação. O compilador é descrito na seção 5. A Infraestrutura necessária é mostrada na seção 6 e, por fim, a seção 7 mostra um exemplo passo-a-passo de uso da arquitetura. 2. Stream Processors O modelo de programação de Stream Processors[7] é composto por dois princípios básicos: Streams e Kernels. Streams são os fluxos de dados que alimentam os Kernels qu,e por sua vez, recebem estes fluxos de dados realizando processamento e geram novos dados como saída (e que podem ser utilizados para alimentar outros Kernels). A Figura 7 ilustra Streams e Kernels, Input Image é um Stream que passa pelo Kernel Convert, onde recebe processamento e é decomposto em dois novos Streams: Luminance e Chrominance. 2.1. Arquitetura A arquitetura de um Stream Processor consiste de um Application Processor, um Stream Register File (SRF) e um Kernel Execution Unit KEU, como mostrado na Figura 8. Application Processor É responsável por executar códigos como o da figura 7, sendo composto por um conjunto de instruções RISC incrementado de instruções de stream, que controlam o controle de fluxo de dados para KEU e memória off-chip.

2 STREAM PROCESSORS 6 Figura 7. Kernels and Streams. Fonte [7] Stream Register File ou SRF Funciona como uma conexão de comunicação que faz o fluxo de troca de dados entre memória off-chip e KEUs, ambos consomem ou produzem fluxo de dados do SRF. Kernel Execution Unit ou KEU Conjunto de instrução similar a um RISC com restrições de acesso a endereços arbitrários de memória (preservando a localidade em acessos a memória), leituras e escritas devem ser realizados através de streams. Um compilador, gera instruções para o Application Processor que, durante a execução através das instruções de stream, carrega e inicia a execução de streams no KEU. Enquanto um compilador tradicional realiza manipulações em operações escalares simples que exploram apenas desempenho local, um compilador para stream pode operar manipulações sobre várias streams levando, potencialmente, a melhores desempenhos. Figura 8. Arquitetura simplificada. Fonte [7]

2 STREAM PROCESSORS 7 A necessidade de utilizar Stream Processing vêm do fato de que aplicações multimídia possuem grande potencial de paralelização. Localidade e concorrência são os grande focos deste potencial. O modelo de Stream Processing obriga os programadores a utilizarem localidade; além de declararem explicitamente o tipo de comunicação de dados a ser utilizado, estes tipos estão ligados diretamente a hierarquia de memória que também é projetada satisfazendo otimizações de localidade. A fim de explorar paralelismo, estes compiladores podem ainda utilizar técnicas de loop unrolling, software pipelinig e stripmining no código que é executado no KEU. 2.2. Hierarquia de memória Existem três níveis de comunicação: Local Resultados temporários de computações utilizados apenas por Kernels. Os operandos e resultados dessas computações são armazenados em LRFs (local register files), registradores localizados bem próximos às ALU. Stream Fluxos de dados que comunicam Kernels diferentes. Os dados utilizados são armazenados pelos SRFs (Stream register file), que capturam blocos maiores de localidade. Global Somente usado para dados verdadeiramente globais. Utilizado para comunicação com dispositivos de I/O e ocorre no último nível de hierarquia de memória, realizando operações com memórias off-chip. A localidade em aplicações multimídia assegura que a maior parte da demanda por banda de dados é para os LRFs, menor para o SRF e raramente é feito acesso à memória off-chip, ocorrendo somente durante o acesso a estruturas de dados persistentes. Na figura 9 pode-se perceber a diferença entre as larguras de banda nos diferentes níveis de memória, respectivamente 1.570:92,2:16,9 Gbps. Figura 9. Hierarquia de memória. Fonte [7]

2 STREAM PROCESSORS 8 2.3. Paralelismo A concorrência também é explorada em múltiplos níveis: Paralelismo no nível de instrução (ILP), dados e tarefas. A implementação pode conter Clusters de SIMD que capturem paralelismo de dados, ILP através de Clusters com ALUs controladas por programas compilados para VLIW e, finalmente, hardware capaz de transferir mais de um stream através da interface da memória off-chip e executar Kernels concorrentemente. 2.4. The Imagine Stream Processors O Imagine[1] é um stream processor programável e é uma implementação em hardware do modelo de streams. Seu intuito é servir como um coprocessador para um processador de propósito geral principal. Através do processador principal são enviados comandos para Imagine, que repassa da maneira apropriada para diferentes módulos no chip através de instruções no nível de streams. Utilizar grandes quantidades de ALU para processar dados em paralelo é relativamente barato. O Imagine Stream Processor utiliza de recursos como esse para explorar paralelismo de dados em aplicações multimídia. Tabela 1. Desempenho de aplicações float-point e 16-bits. Fonte [1] O Imagine explora localidade de dados com largura de banda suficiente para suportar 48 ALUs em uma única pastilha. Essa junção leva a um pico de 16 GFLOPS em aplicações de precisão simples (32 bits) e 32 GFLOPS em aplicações de 16 bits (a tabela 1 mostra alguns exemplos). Para executar aplicações, estas devem ser mapeados para o modelo de programação de streams. O modelo é organizado de maneira que obriga explicitamente o programador a separar os dados (streams) como entrada para os diferentes kernels para serem, então, computados. Todas as transferências de dados são feitas através do stream register file (SRF) pelo controlador de memória a partir a memória externa (off-chip). Os programas que são carregados no kernel, são convertidos em instruções VLIW e armazenados em um microcontrolador com 2K x 576 bits de RAM. O microcontrolador envia essas instruções para 8 clusters aritméticos de maneira SIMD. Cada cluster, Figura 10, consiste de 6 ALUs (que suportam add, shitfs e operações lógicas), 2 multiplicadores, 1 unidade de divisão/raiz quadrada, 1 unidade de comunicação (para troca de dados entre clusters) e uma memória

2 STREAM PROCESSORS 9 scratch-pad local (bancos de registradores de 256 x 32-bit). As unidades aritméticas suportam operações com ponto flutuante e inteiras - multiplicadores suportam aritmética SIMD em sub palavras. 304 registradores em vários registradores locais (LRFs). Figura 10. Diagrama de blocos do cluster aritmético. Fonte [1] A tabela 2 apresenta uma lista de instruções suportadas no nível de streams. Os operandos dessas instruções são originados e escritos no SRF. O Image pode ser assim considerado uma arquitetura load-store para stream processor. Tabela 2. Conjunto de instruções no nível de stream. Fonte [1] 2.5. Micro-architecture A execução utilizando SIMD dá a largura de banda e de instruções necessária para operar 48 ALUs. Como cada cluster executa a mesma instrução VLIW, são necessários poucos ajustes nas lógicas de decodificação e emissão dessas instruções. Particionando-se as ALUs em clusters de SIMD, os SRFs e LFRs são também particionados reduzindo o número de portas nos bancos de registradores por um fator igual ao número de partições. O modelo de stream, Figura 11, garante que não é possível kernels acessem a memória principal diretamente, toda entrada e saída deve necessariamente passar pelo SRF. As instruções no nível de kernel não possuem operações com latência variáveis tornando desnecessária a presença de hardware para renomeamento de variáveis, detecção de dependência ou reordenamento de instruções. Mesmo com todos os mecanismos existentes, ocorrem casos em que o o código não explora perfeitamente o paralelismo de dados ou os dados são acessados de maneira não regular. Para evitar essa limitação, o Imagine disponibiliza um mecanismo de comunicação que permitem troca de dados entre partições paralelas através de instruções de permutação de 32-bits, comunicando entre partições SIMD. Apesar de não ocorrer frequentemente (não é comum em aplicações multimídia), casos de execução condicional acontecem e são resolvidos através de operações select

2 STREAM PROCESSORS 10 Figura 11. Diagrama de blocos da arquitetura. Fonte [1] em hardware e instruções com predicados. Para resolver casos mais complicados (cases, cláusulas condicionais aninhadas) mecanismos de streams condicionais também são utilizados. O Imagine é uma implementação bem sucedida de um Stream Processor programável e consegue, eficientemente, capturar localidade e concorrência da maneira descrita acima. Normalizando para a mesma tecnologia, ele consegue um melhor valor de eficiência energética por watt do que Pentium 4 e o DSP C67x da Texas. A sua implementação de hierarquia de memória possibilita uma taxa de banda de dados de 470:5:1, ou seja, 98.7% de todos os dados são capturados localmente nas LRFs e apenas 0.21% dos acessos utilizam off-chip RAM. 2.6. Linguagens de programação e Stream Processors 2.6.1. Renderização offline Os primeiros sistemas gráficos com suporte a componentes programáveis pelo usuário foram de renderização offline, como o editor pixel-stream, o sistema shade tree e a linguagem de shading RenderMan. 2.6.2. Renderização em tempo real Já os sistemas programáveis de renderização em tempo real evoluíram junto com a evolução do hardware utilizado, começando com GPUs reconfiguráveis mas não reprogramáveis. Exemplos famosos são PFMan, Rendering API, SGI s OpenGL shader system, Quake III shading language. As últimas duas utilizavam técnicas de renderização em múltiplas etapas. A primeira GPU a aceitar fragments e shaders programáveis foi o sistema RTSL (desenvolvido) em Stanfoard. Neste sistema, um usuário escreve um programa simples

2 STREAM PROCESSORS 11 especificando se o código deve ser executado para o processador de vertex ou shaders através de modificadores de tipos de dados. Como a arquitetura de stream processors é uma generalização de fragments e shaders programáveis, linguagens mais genéricas também foram criadas, como o StreamC (Figura 12). O StreamC é uma linguagem de alto nível utilizada para se implementar funções no nível de stream. Um escalonador de streams traduz o código do StreamC para um escalonamento válido de instruções de stream, aplicando diversas otimizações. Figura 12. Código em StreamC. Fonte [1] A linguagem para GPUs CG, foi criada após a RTSL e antes da StreamC e será descrita com mais detalhes, uma vez que ela é a base dos modelos de programação modernos para GPUs. 2.6.3. CG A linguagem CG[10] foi criada para suportar GPUs com vertex e fragments programáveis. A maneira mais efetiva de programar essas arquiteturas é através de uma linguagem de alto-nível, devido a portabilidade, produtividade e fácil manutenção da base de códigos. CG é um sistema para programar GPUs que suporta programas escritos em uma linguagem semelhante a C. O uso mais comum de CG é para escrever algoritmos de shading, apesar de não ser específico para este propósito. A design do sistema CG (figura 13)) foi feito tentando-se conseguir as melhores propriedades que uma nova linguagem pode oferecer: Facilidade de programação, portabilidade, suporte completo à funcionalidade de hardware, desempenho, fácil aceitação do público, fácil extensão para hardware futuro e suporte para utilização geral da GPU (que não esteja atrelada a usos com shading).

2 STREAM PROCESSORS 12 Figura 13. Arquitetura do CG. Fonte [10] Linguagens que possuem domínio de ação específico tendem a aumentar a produtividade do programador, bem como aproveitar modularidade específica do domínio e utilizar informações específicas para suportar otimizações (desabilitar luz em superfícies onde ela não pode ser vista). Para contrabalancear, algumas das desvantagens são: 1. Custo de operadores em tempo de execução - RenderMan pode calcular transformações desnecessárias em coordenadas. 2. Nível de abstração não condiz com o esperado pelo usuário, como ser obrigado a usar elementos específicos de uma GPU que podem não estar presente em outras. 3. Se a linguagem não for compatível com a GPU utilizada, controle completo deverá ser tomado durante o tempo de execução ou pelo compilador para traduzir de maneira apropriada a linguagem para código de máquina. Com bases nas características acima, CG foi criado escolhendo-se ser de propósito geral e não específico de domínio, seguindo princípios como os de possibilitar usos não específico de shading. A escolha de C como linguagem base vem do seu sucesso, desempenho, portabilidade, familiaridade de desenvolvedores e vasta quantidade de programas para CPU similares aos objetivos de uma linguagem para GPUs. CG utiliza idéias de C++, Java, RenderMan e RTSL e de outras linguagens de shading parecidas com C, como GLSL, HLSL e 3DLSL. Utilização de modelos de multi programas, com vertex e fragments disjuntos, com a possibilidade de execução independente, tornando o compilador e o sistema de execução menos intrusivos. Possui uma linguagem específica para escrever stream kernels possibilitando aos processadores omitir recursos da linguagem. Cada processador define um profile especificando o subconjunto do CG que é suportado. Suporta bind-by-name com namespace auxiliar predefinido para realizar alocação de registradores virtuais. Aumenta assim o controle de geração código, melhorando a interface de programas em CG com programas em assembly (figura 14). bind-by-position também é suportado. Inclui uma API com diversos módulos criados sobre outras APIs gráficas existentes. Natureza modular dificulta otimizações entre módulos, porém é um trade-off a se considerar pelo design.

3 ARQUITETURA DA SÉRIE 8 13 Suporta tipos escalares e suporte de primeira classe para vetores e matrizes. Disponibiliza também overloading de funções através de diferenças dos tipos dos argumentos e também através da informação de profile. Não é possível utilizar funções recursivamente, switch e goto não são suportados. Possui construtores implícitos para vetores e suporta Swizzling (construção de vetores a partir de pedaços de outros vetores). Não suporta ponteiros e operações com bits. Não disponibiliza facilidades para gerenciar partes não programáveis do pipiline gráfico (como framebuffer). Figura 14. Interface com código em assembly. Fonte [10] A figura 15 mostra alguns operadores especiais que podem ser especificados: uniform especifica que o argumento não irá mudar para uma seqüência de processamento, enquanto out deve conter a saída do kernel. Figura 15. Exemplo de processador de vertex. Fonte [10] 3. Arquitetura da série 8 Após acompanhar a evolução das arquiteturas e linguagens de programação para GPUs, chega-se no estado da arte de ambas com a nova série de placas de vídeo da NVIDIA e o seu conjunto de software e hardware: CUDA Compute Unified Device Architecture. Soluções automáticas para resolver o paradigma de programar para placas de vídeo ou em paralelo ainda estão longe da realidade. O CUDA é a tentativa da NVIDIA de solucionar o problema. Como dito anteriormente, um dos objetivos do CUDA foi também suporte para computação paralela em massa de alto desempenho e que pode ser encontrado nas GPUs

3 ARQUITETURA DA SÉRIE 8 14 a partir da série NVIDIA GeForce 8, Tesla e Quadro. A NVIDIA inicialmente atendia apenas mercados de gráficos 3D e jogos eletrônicos e apenas agora tem investido para atender requisitos de alto desempenho. Inicialmente[6] a NVIDIA não tinha a capacidade de atender requisitos de processamento para propósito geral, já que os pixels shaders programáveis presentes nas placas não eram ideais e o modelo de programação não era elegante (modelo semelhante ao sistema RTSL ainda era utilizado). 3.1. Arquitetura Figura 16. Arquitetura. Fonte [6] Nesta nova série de GPUs, a NVIDIA considera seus shaders stream processors ou thread processors (figura 16). A tabela 3 mostra o resumo das características destas GPUs. Stream Processors: 128 Threads por Stream Processors: 96 Registradores por Stream Processors: 1024 32-bit SRAM (ao invés de latches) Shared Memory local: 16KB a cada 8 Stream Processors FPUs por stream processors: 1 com single precision Número total de Threads: 12,288 Tabela 3. Exemplo de configuração de uma placa da Série 8 Quando o CPU invoca a execução de um kernel na GPU, os blocos de threads são executados sobre o conjunto de dados descrito pelo programa. Na 16 pode-se perceber que os thread-processors ou stream processors são agrupados em grupos de 8, cada grupo recebe o nome de multiprocessor. Um multiprocessor consiste também de duas unidades funcionais para o processamento de transcendentals e uma memória compartilhada. Ele é responsável por gerenciar, criar e executar as threads em hardware. Para gerenciar as diversas threads é utilizada

3 ARQUITETURA DA SÉRIE 8 15 uma nova arquitetura, chamada SIMT (Single Instruction Multiple Thread). A unidade SIMT realiza todas as tarefas inerentes ao gerenciamento de threads as dividindo em grupos de 32, chamados warps. Quando um multiprocessor tem que executar um grid ou bloco de threads, elas são dividas em warps que são escalonados pelo SIMT. Cada multiprocessor possui quatro tipos de memórias on-chip, como mostrado na figura 17: Uma cache constante, ou seja, uma cache que trabalha em cima do espaço de memória constante, aumentando a velocidade de leitura. Ela é compartilhada entre os thread processors. Uma cache de textura para otimizar o acesso à memória de texturas. O conjunto de registradores de 32 bits mostrados na tabela acima. Memória compartilhada entre os thread processors. Figura 17. Hierarquia de memória. Fonte [5] A execução de um aplicativo que utiliza CUDA é realizada tanto na GPU (ou device) quanto no CPU (ou host), apenas o código que envolve as diretivas especiais introduzidas é executado na GPU, o resto é executado normalmente pela CPU.

3 ARQUITETURA DA SÉRIE 8 16 O modelo de programação do CUDA é diferente dos modelos convencionais single-threaded para CPUs e também difere de outros modelos de paralelismo para GPUs. Alguns dos modelos conhecidos até então: Single-threaded CPU É realizado o fetch de um único fluxo de instrução que opera serialmente sobre dados. Superscalar CPU O fluxo de instrução pode ser roteado em múltiplos pipelines mas o grau de paralelismo é limitado pelos dados e dependência de recursos. Mesmo com instruções SIMD, que extrai mais paralelismo de dados, a eficiência ainda é limitada por 3 ou 4 operações por ciclo. GPGPU processing Altamente paralelo, mas ainda muito dependente da memória de vídeo off-chip (que é normalmente utilizada para mapas de textura) para operar em conjuntos grandes de dados. As threads operam uma com as outras através da memória off-chip, esses acessos freqüentes limitam o desempenho. 3.2. Hierarquia de memória A abordagem do CUDA, é como a do GPGPU, altamente paralela, porém ele divide o conjunto de dados em pedaços menores na memória on-chip permitindo que várias threads compartilhem cada um destes pedaços. Guardando os dados localmente, reduz a necessidade de acesso a memória off-chip, melhorando o desempenho (as Figuras 18 e 19, ilustram esta hierarquia). Figura 18. Relacionamento entre memórias e threads. Fonte [5] Além da área global de dados, ainda existem dois outros espaços que são acessíveis por qualquer uma das threads, o espaço de constantes e texturas, e são otimizados para diferentes usos de memória. Todas estas 3 memórias são persistentes, não desaparecendo quando um kernel termina. Ambos host e device possuem suas próprias DRAM, que são a memória do host e memória do device. Todo o gerenciamento de memória realizado por um aplicativo (global, constantes e texturas), passa pelo runtime do CUDA, incluindo alocação e desalocação, e transferência de dados entre as memórias do host e do device.

3 ARQUITETURA DA SÉRIE 8 17 Figura 19. Memória global e grids. Fonte [5] 3.3. Gerenciamento de threads Os acessos a memória off-chip geralmente não geram stalls em um stream processor. Durante a requisição de dados, uma thread entra na fila de inatividade e é substituída por outra thread pronta a ser executada. Quando o dado se torna disponível, a primeira volta à fila de thread disponíveis. Grupos de threads executam de maneira round-robin, garantindo que cada thread tenha seu tempo de execução sem prejudicar as outras. Outro recurso importante no CUDA é que threads não precisam ser explicitamente escritas pelos programadores, um thread manager que gerencia automaticamente as thread é implementado em hardware. Toda dificuldade inerente à criação e divisão de blocos de threads é feita pelo hardware Cada thread, da maneira convencional, possui pilha, registradores, program counter e memória local própria. Instruções de múltiplas threads podem dividir o pipeline de um stream processor ao mesmo tempo e, em um ciclo de clock, ele pode trocar entre estas threads. A NVIDIA alega que CUDA elimina completamente deadlocks através da função intrinsic syncthreads(), que está presente na API e é convertida pelo compilador diretamente para uma instrução da GPU), mas adverte sobre os perigos da sua utilização dentro de branches. 3.4. Escolha de dados A fim de utilizar CUDA de maneira a obter o melhor desempenho, programadores devem escolher a melhor maneira de dividir os dados em blocos menores. Há o desafio de achar o número ótimo de threads e blocos que manterão a GPU totalmente utilizada. Os fatores para análise incluem o tamanho do conjunto global de dados, a máxima quantidade de dados locais que um bloco de threads pode compartilhar, o número de stream processors na GPU e o tamanho das memórias on-chip.

4 PARADIGMA DE PROGRAMAÇÃO 18 4. Paradigma de programação 4.1. API A NVIDIA esconde a complexidade de sua GPU através de uma API (Figura 20), assim os programadores podem não escrever diretamente na placa e utilizar funções gráficas predefinidas na API para operar a GPU. A primeira vantagem deste modelo é que os programadores não precisam se preocupar com os detalhes complexos de hardware da GPU. Uma outra vantagem (desta vez para a fabricante) é a flexibilidade, permitindo que a NVIDIA mude bastante e frequentemente a arquitetura de sua GPU sem tornar a API obsoleta e quebrar softwares já existentes. É importante notar que, apesar da API não se tornar obsoleta, será necessário que programadores de novos modelos utilizem os recursos introduzidos a fim de obter um software otimizado para desempenho. Figura 20. API esconde detalhes da arquitetura. Fonte [6] A API consiste em: Um conjunto de marcadores e diretivas especiais para linguagem C que permitem que o compilador reconheça se o código em questão é para GPU ou CPU. Uma biblioteca para runtime. Esta é dividida em componentes que gerenciam operações do host (e principalmente a comunicação deste com device), do device (as funções específicas suportadas pela GPU) e de componentes comuns, como tipos adicionais e subconjunto de funções da biblioteca padrão C que podem ser utilizados tanto no host quanto no device. 4.2. Extensões 4.2.1. Visão geral Para escrever código utilizando CUDA, o programador pode utilizar C e C++[5]. Os motivos para escolha de C/C++ como linguagem são os mesmos justificados para a linguagem CG e para tornar possível a expressão do modelo de programação, o CUDA adiciona diversos marcadores especiais a estas linguagens. Estes marcadores, junto com

4 PARADIGMA DE PROGRAMAÇÃO 19 o conjunto de funções fornecidas pela API, completam o arcabouço necessário para desenvolver aplicativos utilizando CUDA. O entendimento destes marcadores especiais é crucial para entender o modelo de programação, dentre os mais importantes temos o marcador: global Se declarada com esta expressão, uma função é considerada um kernel, é globalmente acessível de todo programa e deve ser compilada para a GPU e não para CPU. // Kernel definition global void VectorSubtract(float* A, float* B) {... } Considerando a função VectorSubtract acima, que possui a diretiva global, podemos especificar também o número de blocos de threads e o número de threads por bloco que executarão a mesma, isto é feito através da diretiva <<<numero de blocos, número de threads por bloco >>>. No código abaixo, temos 1 bloco com N threads, onde cada uma das N threads executará paralelamente a função VectorSubtract. int main() { // Kernel invocation VectorSubtract<<<1, N>>>(A, B); } acima. Na figura 21 temos um exemplo mais completo utilizando os marcadores descritos Na primeira parte do código temos um código seqüencial, onde a função saxpy serial é invocada por apenas uma thread de execução em um CPU normal. A segunda versão é escrita utilizando CUDA, sendo a função saxpy parallel executada por nblocks com 256 threads cada. As diretivas especiais estão coloridas para ressaltar sua utilização. É possível identificar qual é o índice ou ID da thread atual de dentro de um kernel através da variável built-in threadidx. Rescrevendo a função VectorSubtract, podemos obter um código como o abaixo, onde cada thread executada calcula a subtração de um par de posições:

4 PARADIGMA DE PROGRAMAÇÃO 20 Figura 21. Exemplo de código. Fonte [6] // Kernel definition global void VectorSubtract(float* A, float* B) { int idx = threadidx.x; C[idx] = A[idx] - B[idx]; } No CUDA é possível especificar blocos multidimensionais de threads. O caso mais comum em programação multithread é utilizar uma seqüência de uma única dimensão de threads como, por exemplo, um conjunto de threads de 1 a N. Com blocos que possuem threads com mais de uma dimensão é possível que cada thread do bloco acesse uma posição diferente em uma matriz multidimensional, cobrindo no total todas as posições.

4 PARADIGMA DE PROGRAMAÇÃO 21 global void matrixadd(float A[N][N], float B[N][N], float C[N][N]) { int i = threadidx.x; int j = threadidx.y; C[i][j] = A[i][j] + B[i][j]; } int main() {... // Kernel invocation dim3 dimblock(n, N); matrixadd<<<1, dimblock>>>(a, B, C); } O tipo dim3 designa um vetor de inteiros e é utilizado para especificar dimensões. No trecho acima, um bloco contêm N*N threads que podem ser acessadas como se estivessem em uma matriz. No escopo do kernel, obtem-se a posição multidimensional através da variável threadidx, especificando-se a dimensão desejada, por exemplo: threadidx.x, threadidx.y e threadidx.z. global void matadd(float A[N][N], float B[N][N], float C[N][N]) { int i = blockidx.x * blockdim.x + threadidx.x; int j = blockidx.y * blockdim.y + threadidx.y; if (i < N && j < N) C[i][j] = A[i][j] + B[i][j]; } int main() {... // Kernel invocation dim3 dimblock(16, 16); dim3 dimgrid((n + dimblock.x { 1) / dimblock.x, (N + dimblock.y { 1) / dimblock.y); matadd<<<dimgrid, dimblock>>>(a, B, C); } Outra abordagem possível da utilização de threads é com a criação de grids. Os diversos blocos de threads são organizados em um nível acima de abstração, em grids com uma ou duas dimensões, como se cada bloco de threads fosse um elemento de uma matriz. O acesso as blocos é realizado utilizando a variável built-in (blockidx), que permite a indexação utilizando blockidx.x ou blockidx.y, assim como na variável threadidx.

4 PARADIGMA DE PROGRAMAÇÃO 22 Outra variável built-in blockdim, obtêm a dimensão de cada bloco de threads. A figura 22 contêm um exemplo visual dos grids de threads. Figura 22. Blocos e grids. Fonte [5] 4.2.2. Grupos de extensões De maneira geral, as extensões são dividas em alguns grupos: Qualificadores de variáveis. Especificam em que tipo de memória do device a variável deve ser alocada. Qualificadores de funções, que especificam se a função é executada no host ou no device e se pode ser invocada a partir de um deles. Uma diretiva que especifica como um kernel é executado no device a partir do host. Isso recebe também o nome de configuração de execução. Quatro variáveis built-in que especificam as dimensões dos grids e blocos. Os qualificadores utilizados em funções são:

4 PARADIGMA DE PROGRAMAÇÃO 23 device Declara uma função que é executada no device e pode ser somente invocada a partir do mesmo. global Como já visto, especifica um kernel, que é executado no device e invocado somente a partir do host host Declara uma função que só pode ser executada e invocada a partir do host. Se uma função não possui nenhum qualificador, ela é considerada do tipo por padrão. O qualificador global possui as seguintes restrições: host Deve obrigatoriamente retornar void. Chamadas a funções com global devem obrigatoriamente especificar uma configuração de execução. Estas chamadas são assíncronas. Os parâmetros destas funções são passados através de memória compartilhada, tendo limite de 256 bytes. A tabela 4 apresenta mais restrições quanto ao uso destes qualificadores. Tabela 4. Restrições dos qualificadores de funções. Fonte [5] Qualificador Recursão Var. Estáticas a VarArgs Endereço b P. conjunto c device global host a Suporta declaração de variáveis estáticas dentro do escopo b Endereço da função não pode ser obtido c Proibido o uso em conjunto Os qualificadores utilizados em variáveis são: device A variável deve residir no device e na memória global. Durante toda a vida da aplicação é acessível a partir do host (através da biblioteca de runtime) e de todas as threads em um grid. constant Reside no espaço de memória constante, durante toda a vida da aplicação e a mesma acessibilidade do device. shared É alocada no espaço de memória de um bloco de threads, com tempo de vida do bloco e somente acessível pelas threads do mesmo. Tabela 5. Restrições dos qualificadores de variáveis. Fonte [5] Qualificador Static storage External c File scope Ini. na decl. a Atr. pelo device b shared device constant a Inicializada na declaração b Atribuída realizada para variável pelo device, somente permitido pelo host através das bibliotecas de runtime c Variável não pode ser declarada como external através de extern

5 COMPILADOR 24 Uma escrita a uma variável shared somente é visualisada por outra thread após a execução de syncthreads. Os qualificadores acima não podem ser aplicados para os tipos struct e union. A tabela 5 contêm as restrições para os qualificadores utilizados em variáveis. Na última seção foi apresentada a diretiva que deve ser utilizada durante a invocação de um kernel a fim de ser especificar tamanho de blocos, grids, etc, também chamada de configuração de execução. Durante a invocação de um kernel deve-se especificar uma expressão da forma <<<Dg, Db, Ns, S >>>, onde o significado de cada argumento está na tabela 6. Parâmetro Tipo Obrigatório Função Dg dim3 Sim Dimensão e tamanho do grid Db dim3 Sim Dimensão e tamanho de cada bloco Ns size t Não Número de bytes na memória compartilhada alocado dinamicamente por bloco S cudastream t Não Especifica um stream adicional Tabela 6. Parâmetros da configuração de execução. Fonte [5] É importante notar que Dg.x Dg.y é o número total de blocos e Db.x Db.y Db.z totaliza o número de threads por bloco. Por último temos as variáveis built-in, algumas delas já foram mencionadas, mas o conjunto total está presente na tabela 7. Variáveis Built-in Tipo Especificação griddim dim3 Dimensão do grid blockidx uint3 Índice do bloco no grid blockdim dim3 Dimensão do bloco threadidx uint3 Índice da thread no bloco warpsize int Contêm o tamanho do warp em threads. Tabela 7. Built-in. Fonte [5] 5. Compilador O compilador fornecido pela NVIDIA para a utilização do CUDA é o nvcc. O funcionamento do nvcc é regido pela invocação de diversas ferramentas para diferentes estágios de compilação. A figura 23 contêm uma lista das fases de compilação suportados pelo nvcc. A etapa inicial de compilação começa pelo tratamento que é dado aos marcadores introduzidos pelo CUDA, que estão presentes em variáveis e funções ao longo do código, e são tratados de maneira especial pelo compilador, que, em uma primeira etapa, separa os códigos específicos de cada arquitetura, neste caso da GPU (device) e do CPU(host). O resultado final desta separação é código C puro para o host e um objeto cubin (formato binário) para o device. Para atingir estes produtos finais, há um preprocessador no

5 COMPILADOR 25 Figura 23. Fases de compilação suportadas pelo nvcc. Fonte [3] front-end, chamado de EDG (figura 24), que analisa estes códigos e separa em arquivos diferentes o que é referente a cada arquitetura. O código referente ao CPU é então compilado com o compilador nativo (GCC, Microsoft Compiler, etc) e o referente à GPU é compilado com uma versão customizada do Open64 (um compilador de código aberto bastante conhecido e eficaz para geração de códigos para execução em paralelo). O Open64 gera código em um formato assembly chamado PTX (Parallel Thread execution). Geralmente este é o formato que os arquivos de driver da NVIDIA que vêm nos instaladores, e que é convertido para código executável em tempo de instalação. Assim, a segunda etapa de compilação possui um retradutor de assembly que a partir do PTX gera código específico para aquele modelo de GPU. A figura 25 ilustra as etapas de compilação. 5.1. Bibliotecas A biblioteca de runtime possui componentes que são específicos para hosts, devices e componentes comuns para ambos. Dentre os componentes comuns existentes, temos: Os tipos built-in para designar vetores, que são derivados dos tipos básicos de inteiros e dos tipos de ponto flutuante. Exemplos destes tipos são: char4, int4, uint3, ulong2, float3 e double2. Estão disponíveis também diversas funções que

5 COMPILADOR 26 Figura 24. Preprocessamento. Fonte [3] constroem estes tipos a partir dos tipos primitivos e geralmente estão no formato make <nome do tipo >. Abaixo um exemplo: float3 make_float3(float a, float b, float c); O tipo dim3 utilizado nos exemplos das seções anteriores. Suporte a diversas funções matemáticas das bibliotecas padrões de C/C++. Função de tempo que retorna o contador incrementado a cada ciclo de clock por multiprocessor. Tipos para representação de texturas. Os componentes específicos de devices somente podem ser utilizados dentro de funções de devices. Dentre eles: Funções matemática otimizadas (para forçar sua utilização -use fast math.

5 COMPILADOR 27 Figura 25. Etapas de compilação. Fonte [6] Funções de sincronização. A função syncthreads() sincroniza todas as threads em um bloco. Uma vez que todas as threads chegam neste ponto, a execução continua. Funções para manipular texturas. Funções para garantir atomicidade. Os componentes de runtime específicos do host disponibilizam funções para gerenciar o device, contexto, memória, controle de execução e interoperabilidade entre OpenGL e Direct3D. É composto por duas API s, uma API de baixo nível (API do driver), e uma de alto nível (API do runtime). Uma aplicação sempre utiliza uma das duas, mas nunca ambas ao mesmo tempo. A API do runtime é responsável por realizar implicitamente inicialização, gerenciamento de contexto e dos módulos. Quando o compilador fornecido pela NVIDIA (o nvcc) separa o código do host, ele o faz utilizando a API do runtime, assim toda aplicação que for ligada a este código deve utilizar a API do runtime. cudart é a biblioteca dinâmica que disponibiliza as funções desta API, que são todas prefixadas com cuda. A API do driver é bem mais complicada de se utilizar, mas oferece um controle maior dos recursos. Todas as operações realizadas negociam diretamente com objetos cubin. As funções desta API são prefixadas com cu, e estão disponíveis através da biblioteca dinâmica nvcuda 5.1.1. Exemplos de utilização da API de Runtime A API é bastante extensa, alguns exemplos serão apresentados exemplificando tipos de operações que podem ser realizadas.

5 COMPILADOR 28 Para executar os trechos de códigos presentes, não é necessário realizar nenhum tipo de inicialização, as bibliotecas de runtime já se encarregam das inicializações automaticamente. O exemplo de código abaixo utiliza componentes de runtime para enumerar os devices presentes, retornar suas propriedades, e escolher aquele que possui a maior quantidade de memória global disponível. int devicecount, device, devmaxmem=0, MaxMem=0; cudagetdevicecount(&devicecount); cudadeviceprop deviceprop; for (device = 0; device < devicecount; ++device) { cudagetdeviceproperties(&deviceprop, device); if (deviceprop.totalglobalmem > MaxMem) { MaxMem = deviceprop.totalglobalmem; devmaxmem = device; } } cudasetdevice(devmaxmem); Para alocação de memória, as funções cudamalloc(), cudamallocpitch() e cudafree() podem ser utilizadas. A primeira é usada para alocação de memória sequencial e a segunda é recomendada para a alocação de vetores 2D, respeitando restrições de alinhamento, melhorando o desempenho de outras operações com o mesmo vetor. float data[256]; int size = sizeof(data); float* devptr; cudamalloc((void**)&devptr, size); cudamemcpy(devptr, data, size, cudamemcpyhosttodevice); Para criar um stream basta invocar a função cudastreamcreate, realizar as copias necessárias de dados e invocar um kernel.

5 COMPILADOR 29 cudastream_t stream[2]; for (int i = 0; i < 2; ++i) cudastreamcreate(&stream[i]); for (int i = 0; i < 2; ++i) cudamemcpyasync(inputdevptr + i * size, hostptr + i * size, size, cudamemcpyhosttodevice, stream[i]); for (int i = 0; i < 2; ++i) mykernel<<<100, 512, 0, stream[i]>>>(outputdevptr + i * size, inputdevptr + i * size, size); for (int i = 0; i < 2; ++i) cudamemcpyasync(hostptr + i * size, outputdevptr + i * size, size, cudamemcpydevicetohost, stream[i]); cudathreadsynchronize(); No trecho de código acima, os dados de entradas são copiados do host para o device, o stream é processado pelo kernel mykernel e o resultado é copiado de volta para o host. As copias de memória executadas são assíncronas e associadas com cada stream. Para realizar medição de tempo dentro de um código, pode-se usar funções de manipulação de evento, o trecho abaixo faz a medição de tempo do código acima. cudaevent_t start, stop; float elapsedtime; cudaeventcreate(&start); cudaeventcreate(&stop); cudaeventrecord(start, 0); // Código idêntico ao anterior... cudaeventrecord(stop, 0); cudaeventsynchronize(stop); cudaeventelapsedtime(&elapsedtime, start, stop); cudaeventdestroy(start); cudaeventdestroy(stop); Outros usos desta API envolvem a manipulação de referência a texturas e rotinas para interoperabilidade com OpenGL e Direct3D.

5 COMPILADOR 30 5.1.2. Exemplos de utilização da API do Driver O desenvolvimento de aplicações que utilizam a API do Driver é um processo mais complexo e atende a propósitos mais específicos. Antes de utilizar qualquer função da API do Driver, a função cuinit() deve ser invocada para realizar as inicializações necessárias. As funções desta API devem ser todas invocadas passando-se como parâmetro handles predefinidos e específicos para cada tipo de objeto (os objetos existentes estão na tabela 8). Estes handles são tipos opacos não acessíveis diretamente pelo usuário mas são acessados implicitamente pela biblioteca a fim de gerenciar recursos de maneira transparente. Tabela 8. Objetos e handles. Fonte [5] Objeto Handle Descrição Device CUdevice Dispositivo que suporta CUDA Contexto CUcontext Equivalente a um processo do CPU Modulo CUmodule Equivalente a uma biblioteca dinâmica Função CUfunction Kernel Memória heap CUdeviceptr Ponteiro para a memória do device CUDA Array CUarray Container opaco para vetores no device Referência a texturas CUtextref Descreve como interpretar texturas O trecho de código abaixo guarda em devmax o índice do device com maior tamanho de memória. O handle CUdevice é utilizado uma vez que a operação envolve informações do device.... int devicecount, device, devmax=0; unsigned int size, maxsize; CUdevice cudevice; cudevicegetcount(&devicecount); CUdevprop prop; for (int device = 0; device < devicecount; ++device) { cudeviceget(&cudevice, device); cudevicetotalmem(&size, cudevice); if (size > maxsize) { maxsize = size devmax = device; } } O código equivalente para esta API que inicializa um módulo e carrega um kernel segue abaixo.

6 INFRAESTRUTURA 31... CUmodule cumodule; CUfunction cufunction; float f; int offset = 0, i; char data[32]; cumoduleload(&cumodule, \mymodule.cubin"); cumodulegetfunction(&cufunction, cumodule, \mykernel"); cufuncsetblockshape(cufunction, blockwidth, blockheight, 1); cuparamseti(cufunction, offset, i); offset += sizeof(i); cuparamsetf(cufunction, offset, f); offset += sizeof(f); cuparamsetv(cufunction, offset, (void*)data, sizeof(data)); offset += sizeof(data); cuparamsetsize(cufunction, offset); cufuncsetsharedsize(cufunction, numelements * sizeof(float)); culaunchgrid(cufunction, gridwidth, gridheight); 6. Infraestrutura 6.1. Requisitos Para desenvolver aplicativos utilizando CUDA, é necessário possuir em um computador os seguintes recursos: Duas placas de vídeo. A primeira é a placa de vídeo utilizada pelo monitor, pode ser de qualquer marca e utilizar um slot AGP ou PCI-Express. A segunda, é a placa utilizada para o desenvolvimento com CUDA, não deve ser ligada e utilizada como monitor, necessita de um slot PCI-Express e deve obrigatoriamente conter um chipset da NVIDIA Série 8 ou superior, Tesla ou os modelos habilitados da série Quadro. É possível conferir se um modelo específico suporta CUDA [5]. A Figura 26 mostra uma Nvidia 8600 GT. Com os requisitos de placa de vídeo acima, é necessário uma placa mãe (motherboard) com pelo menos um slot PCI-Express, que obrigatoriamente será utilizado para a placa com chipset NVIDIA. O motherboard deve conter também o tipo de slot necessário para a placa de vídeo que será conectada ao dispositivo gráfico. Nos exemplos a seguir, utilizaremos o sistema operacional GNU/Linux, preferencialmente com a distribuição Ubuntu, versão igual ou superior a 7.10. Compilador nativo instalado no Linux. O gcc é necessário pois será utilizado implicitamente pelo compilador da NVIDIA.

6 INFRAESTRUTURA 32 Figura 26. NVIDIA 8600 GT 512MB. Fonte [2] 6.2. Instalação Para instalar o CUDA no Ubuntu é necessário garantir primeiro que o driver atual do sistema não está classificado como restrito, que são os drivers antigos que não possuem o suporte aos recursos dos novos chipsets. Se este for o driver atual instalado, remova-o do seu sistema. A versão 100.14 é um destes tipos de drivers restritos. Realizar o download do programa Envy legacy [8] (cujo nome é EnvyNG no Ubuntu 8.04 ou maior). Executar este programa, que realizará os seguintes passos: Detecção do modelo da placa gráfica e instalação do driver apropriado. No entanto, a detecção pode ser sobrescrita com através da opção Manual instalation. Instalação das dependências necessárias. Configuração automática do servidor X. Para os passos acima, podem ser utilizados uma interface gráfica (que somente pode ser executada dentro de um desktop) ou uma interface de texto caso não seja possível, por algum motivo, iniciar o servidor X. O driver livre da NVIDIA não pode ser escolhido a fim de se habilitar o CUDA, assim devemos colocar nv na lista de módulos desabilitados: $ sudo vim /etc/default/linux-restricted-modules-common... ( DISABLED_MODULES="nv" ) Efetuar o download do toolkit e do SDK CUDA [2]. Supondo que o usuário do Linux que está realizando os passos da instalação, não é o administrador (root) e que seu nome seja aluno, execute: 1. Instalar o toolkit em /home/aluno/cuda, para iniciar a instalação, faça: $ chmod 755 NVIDIA_CUDA_Toolkit_2.0_ubuntu7.10_x86_64.run $./NVIDIA_CUDA_Toolkit_2.0_ubuntu7.10_x86_64.run

6 INFRAESTRUTURA 33 2. Instalar a SDK em /home/aluno/nvidia CUDA SDK. Execute as linhas abaixo para iniciar a instalação. $ chmod 755 NVIDIA_CUDA_SDK_2.02.0807.1535_linux.run $./NVIDIA_CUDA_SDK_2.02.0807.1535_linux.run 3. Mudar as variáveis de ambiente para refletir o local da instalação. Com um editor de texto a escolha, edite o arquivo /home/aluno/.bashrc, e adicione as seguintes linhas: export PATH=$PATH:$HOME/cuda/bin export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$HOME/cuda/lib 4. Para as configurações fazerem efeito imediato, crie uma nova shell ou execute o seguinte comando: $ source.bashrc 5. Entre no diretório do SDK e teste o device: $ cd /home/aluno/nvidia_cuda_sdk $ make $./bin/linux/release/devicequery 6. A execução do comando anterior deve imprimir um trecho de saída semelhante ao trecho abaixo (mais especificamente, o texto Test PASSED deve estar presente): There is 1 device supporting CUDA Device 0: "GeForce 8600 GTS" Major revision number: 1 [...] Clock rate: 1458000 kilohertz Test PASSED O SDK é composto de diversos programas exemplos que podem ser compilados e executados de maneira simples. Para compilar e executar um deles, basta seguir os passos abaixo: $ cd NVIDIA_CUDA_SDK/ $ make $./bin/linux/release/fluidsgl