Como semear um gerador de números pseudo-aleatórios congruenciais lineares paralelos para o período máximo?

8

Normalmente, quando eu proponho um gerador seqüencial de números aleatórios em C, uso a chamada

srand(time(NULL))

então use

rand() mod N

para obter um número inteiro aleatório entre 0 e N-1. No entanto, quando faço isso em paralelo, as chamadas ao tempo (NULL) são tão próximas uma da outra que acabam sendo exatamente o mesmo número.

Eu tentei usar um gerador de números aleatórios congruencial linear:

xn+1:=axn+c(modm) para alguns inteiros a,c, e m .

Eu sei que escolher m=2k para um número inteiro grande k produz resultados rápidos porque o operador do módulo pode ser calculado truncando dígitos. No entanto, acho difícil estabelecer sementes que produzam seqüências aleatórias com um grande período em paralelo. Eu sei que a duração de um período é máxima se

  1. c e m são números primos entre si
  2. a-1 é divisível por todos os fatores primos de m
  3. se m for múltiplo de 4, a-1 também deverá ser múltiplo de 4.

(fonte: wikipedia )

Mas como garantir que todos os fluxos de números aleatórios tenham essa propriedade máxima? Em termos de MPI, como incorporar ranke sizeproduzir períodos máximos usando o método congruencial linear? Seria mais fácil usar um Fibonacci Lagged ou Mersenne Twister para produzir fluxos aleatórios paralelos mais longos?

Paulo
fonte
3
Apenas como uma observação lateral, você quase certamente não deseja usar um PRNG linear congruente para computação científica. Eles não podem provar adequadamente espaços de dimensão maior que 1. Ou seja: eles não podem nem desenhar uma amostra adequada de pontos em um plano.
dmckee --- ex-moderador gatinho
1
@dmckee: o teorema de Marsaglia é certamente relevante para alguns cálculos científicos, mas seria injusto dizer que desqualifica os LCGs para todos os cálculos científicos. Às vezes, ter um PRNG rápido é igualmente importante, e o número de vetores de abrangência (através da origem) geralmente é mais importante que o número de hiperplanos que cobrem o espaço da amostra.
22412 Jack Poulson
2
@ dmckee: você pode estar certo sobre LCG, mas para muitas aplicações, 1D é suficiente.
Paul
1
@JackPerhaps Estou tendencioso pela natureza do meu trabalho, e estou ciente disso, por isso especifiquei a natureza do defeito. Paul: Há uma variedade de PRNGs bem estudados e muita troca (velocidade, período, segurança criptográfica e capacidade de desenhar tuplas de alta dimensionalidade). Eu uso o Mersenne Twister tanto em coisas que eu codifico manualmente quanto como o PRNG padrão no ROOT . Não é o mais rápido, mas desenhar números é geralmente uma contribuição modesta para o meu tempo de execução.
dmckee --- ex-moderador gatinho
1
Como outra observação lateral, se você precisar usar LC PRNGs (talvez por motivos de velocidade), definitivamente não deseja usar modos bits de baixa ordem - como sugeriu Jonathan Dursi , eles são muito menos aleatórios. Em vez disso, divida seu número aleatório (int) por maxint / range para obter o range necessário. Custa uma divisão, mas provavelmente é uma opção mais barata para melhorar a qualidade do seu fluxo de números aleatórios do que mudar para outro PRNG.
Mark Booth

Respostas:

9

O truque é intercalar o fluxo LCG de cada processo: para processos , modificamos o LCGp

xn+1:=axn+c(modm),

ser estar

xn+p:=Apxn+Cp(modm),

onde e efetivamente etapas. Podemos derivá-los rapidamente expandindo a etapa LCG original:ApCpp

xn+2=a(axn+c)+c(modm)=a2xn+(a+1)c(modm),

e o padrão é que e é o resultado de, começando com o número , multiplicando sucessivamente por e adicionando vezes , depois multiplicando por , todos .Apapmodm,Cp0a1 pcmodm

A última etapa é garantir que o LCG do passo de cada processo não se sobreponha: simplesmente inicialize o processo com a classificação com e um LCG paralelo com períodos individuais de está pronto, onde é o período original é o número de processos. Se o LCG modificado de cada processo for usado igualmente, o período completo será recuperado em paralelo.prxrN/pNp

Eu implementei isso há cerca de seis meses (talvez ingenuamente), e você pode encontrar o código aqui .

Jack Poulson
fonte
Essa é uma abordagem interessante. Ele efetivamente recebe N tuplas de um único fluxo LC PRNG de maneira distribuída. Ele ainda sofre dos problemas de correlação mencionados na wikipedia, mas não requer sobrecarga de sincronização de uma fonte PRNG centralizada. Eu estaria interessado em ver como a qualidade desses fluxos se compara à correlação entre fluxos criados por múltiplos PRNGs de LC com diferentes constantes.
Mark Booth
Boa ideia; isso parece implorar para ser escrito para GPU.
Chinasaur
Sua implementação está ajustada para coerência de memória? Eu imagino que tentar dar a cada thread um bloco contíguo maior e fazer com que eles pulem um sobre o outro em etapas maiores funcionará mais suavemente? Na GPU, por outro lado, a versão completa é perfeita.
Chinasaur
6

Há um artigo muito bom sobre o tutorial de Katzgrabber, Números Aleatórios em Computação Científica: Uma Introdução , que eu indico para quem deseja ser usuário de PRNGs para computação científica. Geradores congruenciais lineares são rápidos, mas isso é tudo o que eles têm a seu favor; eles têm períodos curtos e podem facilmente errar; combinações perfeitamente razoáveis ​​de a, c e m podem resultar em saídas terrivelmente correlacionadas, mesmo se você atender aos requisitos usuais entre a, c e m.

Pior, em um caso comum em que m é uma potência de dois (para que a operação de modificação seja rápida), os bits de ordem inferior têm um período muito mais curto que a sequência como um todo; portanto, se você estiver usando rand ()% N, você tem um período ainda mais curto do que o esperado.

Como regra geral, os geradores de lag-fibbonacci, MT e WELL têm propriedades muito melhores e ainda são muito rápidos.

Em termos de propagação em paralelo, o método de Jack Poulson é bom porque fornece uma sequência bem definida de números divididos igualmente entre os processadores. Se isso não importa, você pode fazer algo razoável para propagar os diferentes PRNGs; o mesmo artigo sugere algo que muitas pessoas inventaram independentemente, combinando o número da tarefa PID ou MPI com o tempo. A fórmula específica sugerida existe

long seedgen(void)  {
    long s, seed, pid;

    pid = getpid();
    s = time ( &seconds );

    seed = abs(((s*181)*((pid-83)*359))%104729); 
    return seed;
}

Não tenho opiniões particulares sobre essa implementação específica, mas a abordagem geral é certamente razoável.


fonte
Eu gosto do documento de visão geral, obrigado! Suponho que devo defender os LCGs argumentando que agora podemos simplesmente escolher bons valores para da Wikipedia ou dos Algoritmos Seminuméricos de Knuth, e que o exemplo no artigo de Katzgrabber é um pouco injusto, pois é um LCG sem um turno ( ). (a,c,m)c=0
21412 Jack Poulson
Existe esse, mas mesmo aquele com a = 106, c = 1283, m = 6075 (fig 2) é uma pilha inteira de falhas. Mas sim, existem bons triplos disponíveis.
@ JackPoulson: Eu sempre achei muito frustrante ter que tirar a, c e m do alto da minha cabeça ... quando faço dessa maneira, sempre parece levar a períodos muito pequenos. Obrigado pela citação! Vou procurar os Algoritmos Seminuméricos de Knuth.
Paul
2

Uma idéia simples para espalhar um RNG seqüencial típico em um número decente de encadeamentos é fazer com que um único encadeamento avance a semente o mais rápido possível e envie apenas a cada milésima semente para a memória. Em seguida, faça com que cada um dos seus outros threads pegue uma dessas sementes de referência espaçadas e processe os 1000 valores nesse bloco, ou seja, gere novamente as 1000 sementes no bloco, gere seus empates psuedorandom e faça o que qualquer outro processamento envolver sua tarefa.

Isso funciona porque para RNGs que não computam tanto assim (o LCG é certamente um, mas muitos outros deveriam estar nessa categoria) o gargalo real está enviando as sementes para a memória (e provavelmente também para o processamento subsequente). Se você executa um LCG sem enviar nada para a memória, tudo deve permanecer nos registros da CPU e ser extremamente rápido. Mesmo para um RNG mais complicado, você deve permanecer no cache L1 e ser muito rápido.

Eu usei essa abordagem muito simples com um LCG que, por motivos legados, precisamos manter. Basicamente, obtemos aceleração linear até os 4-8 threads em uma estação de trabalho multicore típica. Mas agora vou tentar o método da resposta de Jack Poulson e espero ficar ainda mais rápido :).

OTOH, acredito que esse truque simples funcione para outros RNGs inerentemente sequenciais.

Chinasaur
fonte
2

Esta resposta está atrasada, mas você deve dar uma olhada no SPRNG . Ele foi projetado especificamente para escalabilidade em paralelo e suporta vários tipos de PRNGs.

Bill Barth
fonte