17 - GPU e números aleatórios¶
Revisão de números aleatórios¶
Um gerador de números pseudo-aleatórios (RNG) é um algoritmo determinístico que gera uma sequência de números que parece aleatória. Essa frase possui dois termos importantes que precisamos destrinchar:
- determinístico: Um RNG tipicamente recebe como entrada um inteiro seed (que representa uma sequência de bits "aleatória") e gera uma sequência de números baseada no seed. Ou seja, o algoritmo é determinístico pois gera sempre a mesma sequência para uma determinada entrada (seed).
- parece aleatória: Se compararmos duas sequências de números, uma gerada por um RNG e outra por uma distribuição uniforme de verdade, não conseguimos dizer qual distribuição foi gerada pelo RNG.
Ou seja, ao escolhermos um seed a sequência gerada será sempre a mesma, mesmo se executarmos o programa em outras máquinas. Isso torna a utilização de RNGs para experimentos bastante interessante: é possível reproduzir os resultados feitos por outros desenvolvedores/cientistas. Para isto é necessário
- que o programa permita escolher o seed da simulação;
- que o seed usado seja publicado junto com os resultados.
Muitas implementações de RNGs são divididas em duas partes:
- engine: algoritmo que gera um inteiro cujos bits formam uma sequência pseudo-aleatória.
- distribution: utiliza os bits acima para retornar números que sigam alguma distribuição estatística (como normal ou uniforme).
A thrust
contém uma API de geração de números aleatórios muito parecida com a API da biblioteca padrão de C++.
Question
Consulte a documentação oficial da thrust
e encontre as páginas que descrevem os engines e distributions implementados.
Resposta
Abaixo temos um trecho de um código-fonte que nos mostra como gerar números aleatórios com thrust.
#include <thrust/random.h>
#include <thrust/device_vector.h>
#include <thrust/transform.h>
#include <thrust/iterator/counting_iterator.h>
#include <vector>
int main() {
thrust::default_random_engine eng(10);
thrust::uniform_real_distribution<double> d(20, 30);
for (int i = 0; i < 10; i++) {
std::cout << d(eng) << "\n";
}
}
Vamos agora fazer um uso básico dessas funções.
Example
Crie um programa que leia um inteiro seed do terminal e:
- crie um objeto
default_random_engine
que o utilize como seed. - mostre no terminal uma sequência de 10 números fracionários tirados de uma distribuição uniforme
[25, 40]
.
Seu programa deverá estar implementado usando os tipos definidos em thrust::random
.
Um ponto importante da API thrust
para geração de números aleatórios é que essas funções podem ser chamadas dentro de operações customizadas! Nosso próximo exercício trata justamente deste uso.
Example
Faça um programa que cria um vetor com 10 elementos e aplica uma operação customizada que seta cada elemento com um valor aleatório. Use as mesmas configurações do exercício anterior.
Resposta
Use o trecho de código disponibilizado neste gist para ver a estrutura da sua função customizada e como você pode criar uma transformação que a invoca para cada elemento de um vetor.
Warning
Você pode prosseguir mesmo se seu vetor tem 10 números iguais.
Gerando números pseudo-aleatórios em GPU¶
Um desafio em programas paralelos é gerar sequências pseudo-aleatórias de qualidade. Se não tormarmos cuidado acabamos gerando os mesmos números em threads diferentes e desperdiçamos grande quantidade de trabalho!
Question
Que abordagem você usou para gerar números aleatórios em paralelo com OpenMP?
Resposta
Se você realizou todas as tarefas, deve ter criado um gerador de números pseudo-aleatórios para cada thread para calcular o valor de \pi usando Monte Carlo.
Question
Você acha que poderíamos aplicar a mesma abordagem para geração de números aleatórios em GPU? Por quê?
Resposta
Sem cuidados adicionais, não obteríamos bons resultados. A abordagem usada na aula de openMP pressupõe um número relativamente pequeno de threads, de forma que a sequência criada por cada gerador é menor, porém grande o suficiente para ter qualidade. No caso de usarmos uma GPU, o número de threads pode ser muito grande, de forma que cada gerador geraria sequências muito curtas, e por isso perderíamos em qualidade da sequência.
Seeds em programas massivamente paralelos¶
Em computação massivamente paralela, em geral, existem duas abordagens.
Abordagem 1: usar seeds diferentes em cada thread.
Abordagem 2: usar a mesma seed em todas as threads, mas cada uma começa em um ponto diferente da sequência daquela seed.
Note que em ambos os casos os resultados dependem do número de threads usadas! Como vimos em aulas anteriores, um RNG tem estado interno e não pode ser facilmente compartilhado entre várias threads.
Example
Implemente a abordagem 1 no exercício da parte anterior. Para isto você pode usar a estratégia de acesso direto aos dados e usar o índice recebido como seed. Lembre-se que a seed é definida ao criar o default_random_engine
. No exemplo abaixo, você pode ver que ela é definida com o valor 10
.
thrust::default_random_engine eng(10);
Example
Multiplique i
por um valor grande e tente de novo. Verifique que agora os números são diferentes
Exercício prático¶
Vamos trabalhar com um método probabilístico de estimação do pi
neste último exercício. O algoritmo sequencial se baseia em sorteios de pontos dentro de um quadrado de lado 2
. Se a distância entre o ponto e o centro do quadrado for menor que 1 então o ponto cai dentro do círculo inscrito no quadrado. A quantidade de pontos que caem dentro do quadrado é proporcional a \pi.
sum = 0
- De
i=0
atéN
:- sorteie pontos x,y \in [0,1]
- se x^2 + y^2 \leq 1,
sum += 1
- devolva
4 * sum / N
Example
Resgate a implementação sequencial deste algoritmo realizada na aula (disponível neste gist) e rode-a para N=100000000
Example
Paralelize o código acima em GPU. Use ambas as abordagens acima em programas distintos para lidar com os geradores de números aleatórios.