Numpy: A implementação de PCG fornecida pela Numpy tem autocorrelação perigosa e significativa

Criado em 20 mai. 2020  ·  104Comentários  ·  Fonte: numpy/numpy

O gerador de PCG usado pelo Numpy tem uma autocorrelação significativa. Ou seja, para cada sequência gerada a partir de uma semente, há um grande número de sequências correlacionadas e não sobrepostas a partir de outras sementes. Por "correlacionado", quero dizer que intercalar duas dessas sequências e testar o resultado obtém falhas que não apareceram em cada sequência individualmente.

A probabilidade de que dois geradores de um grande conjunto de terminais obtenham duas dessas sequências é insignificante. Por que isso acontece de um ponto de vista matemático é bem conhecido, mas é explicado aqui em detalhes: http://prng.di.unimi.it/pcg.pgp (consulte "Subsequências no mesmo gerador").

Para mostrar esse problema diretamente, escrevi este programa C simples reutilizando o código Numpy: http://prng.di.unimi.it/intpcgnumpy.c . O programa pega dois estados de 128 bits de dois geradores (com a mesma constante LCG ou "fluxo") na forma de bits altos e baixos, intercala sua saída e a escreve em forma binária. Depois de enviá-lo por meio do PractRand, não devemos ver nenhuma falha estatística, pois os dois fluxos devem ser independentes. Mas se tentar começar de dois estados com os mesmos 64 bits inferiores, você obtém:

./intpcgnumpy 0x596d84dfefec2fc7 0x6b79f81ab9f3e37b 0x8d7deae980a64ab0 0x6b79f81ab9f3e37b | stdbuf -oL ~ / svn / c / xorshift / practrand / RNG_test stdin -tf 2 -te 1 -tlmaxonly -multithreaded
RNG_test usando PractRand versão 0.94
RNG = RNG_stdin, semente = desconhecido
conjunto de teste = expandido, dobrável = extra

rng=RNG_stdin, seed=unknown
length= 128 megabytes (2^27 bytes), time= 2.2 seconds
  Test Name                         Raw       Processed     Evaluation
  BCFN(0+0,13-2,T)                  R= +27.6  p =  1.0e-13    FAIL
  BCFN(0+1,13-2,T)                  R= +68.0  p =  2.3e-34    FAIL !!!
  BCFN(0+2,13-3,T)                  R= +90.8  p =  8.8e-43    FAIL !!!
  BCFN(0+3,13-3,T)                  R=+120.6  p =  6.9e-57    FAIL !!!!
  DC6-6x2Bytes-1                    R=  +8.9  p =  4.0e-5   mildly suspicious
  DC6-5x4Bytes-1                    R= +15.7  p =  4.3e-9   very suspicious
  [Low1/8]BCFN(0+0,13-4,T)          R= +11.6  p =  4.9e-5   unusual
  ...and 1074 test result(s) without anomalies

Você pode até ir mais baixo - você só precisa dos mesmos 58 bits mais baixos:

./intpcgnumpy 0x596d84dfefec2fc7 0x0579f81ab9f3e37b 0x8d7deae980a64ab0 0x6b79f81ab9f3e37b | stdbuf -oL ~/svn/c/xorshift/practrand/RNG_test stdin -tf 2 -te 1 -tlmaxonly -multithreaded

[...]
rng=RNG_stdin, seed=unknown
length= 32 gigabytes (2^35 bytes), time= 453 seconds
  Test Name                         Raw       Processed     Evaluation
  [Low1/16]FPF-14+6/32:cross        R= +11.6  p =  4.0e-10   VERY SUSPICIOUS
  [Low1/32]FPF-14+6/32:cross        R= +16.5  p =  3.2e-14    FAIL
  [Low1/32]FPF-14+6/16:cross        R= +12.8  p =  3.8e-11   VERY SUSPICIOUS
  [Low1/64]FPF-14+6/64:cross        R=  +6.8  p =  4.8e-6   mildly suspicious
  [Low1/64]FPF-14+6/32:cross        R=  +6.0  p =  1.9e-5   unusual
  [Low1/64]FPF-14+6/16:cross        R=  +5.5  p =  5.8e-5   unusual
  [Low4/32]FPF-14+6/64:all          R=  +5.8  p =  5.9e-5   unusual
  [Low4/32]FPF-14+6/32:(0,14-0)     R=  +7.7  p =  1.0e-6   unusual
  [Low4/32]FPF-14+6/32:(1,14-0)     R=  +7.7  p =  9.1e-7   unusual
  [Low4/32]FPF-14+6/32:all          R=  +6.5  p =  1.3e-5   unusual
  [Low4/64]FPF-14+6/64:all          R=  +5.9  p =  5.1e-5   unusual
  [Low4/64]FPF-14+6/64:cross        R=  +8.2  p =  3.0e-7   suspicious
  [Low4/64]FPF-14+6/32:(0,14-0)     R=  +7.6  p =  1.0e-6   unusual
  [Low8/64]FPF-14+6/64:(0,14-0)     R= +17.0  p =  2.2e-15    FAIL
  [Low8/64]FPF-14+6/64:(1,14-0)     R=  +9.1  p =  5.1e-8   mildly suspicious
  [Low8/64]FPF-14+6/64:all          R= +12.7  p =  2.1e-11   VERY SUSPICIOUS
  [Low8/64]FPF-14+6/32:(0,14-0)     R= +12.8  p =  1.7e-11   VERY SUSPICIOUS
  [Low8/64]FPF-14+6/32:all          R= +11.0  p =  9.3e-10   VERY SUSPICIOUS
  ...and 1696 test result(s) without anomalies

Observe que para obter mais de 50% de probabilidade de que dois geradores partam de duas sementes correlacionadas (escolhidas aleatoriamente), você precisa de cerca de meio milhão de geradores começando aleatoriamente (paradoxo do aniversário). E se você considerar a probabilidade de que eles não comecem exatamente do mesmo estado, mas tenham sequências correlacionadas sobrepostas significativas, você precisa de muito menos.

Qualquer gerador sensato da literatura não se comportará assim. Você pode escolher adversarialmente quaisquer dois estados iniciais de MRG32k3a, SFC64, CMWC, xoshiro256 ++, etc., e contanto que você gere sequências não sobrepostas, você não verá as falhas acima. Esta é uma grande desvantagem que pode surgir quando vários dispositivos usam o gerador e se assume (como deveria ser) que em pares essas sequências não devem mostrar correlação. A correlação pode induzir um comportamento indesejado que é difícil de detectar.

Documente pelo menos em algum lugar que o gerador não deve ser usado em vários terminais ou em um ambiente altamente paralelo.

O mesmo pode acontecer com diferentes "streams", pois as sequências geradas por um LCG pela mudança da constante aditiva são todas o mesmo módulo uma mudança de sinal e uma constante aditiva. Você pode ver alguma discussão aqui: https://github.com/rust-random/rand/issues/907 e uma discussão matemática completa do problema aqui: https://arxiv.org/abs/2001.05304 .

numpy.random

Todos 104 comentários

@imneme , @bashtage , @rkern seriam as autoridades aqui, mas acho que já falamos sobre isso e é por isso que preferimos a interface SeedSequence.spawn vez da jumped one. Por exemplo, houve essa discussão quando estávamos discutindo a API. Verifique o conselho aqui https://numpy.org/devdocs/reference/random/parallel.html e sugira melhorias conforme necessário.

@mattip Isso não tem nada a ver com pular.

Acho que, na prática, é difícil fazer alterações completas, embora a documentação aprimorada seja sempre uma boa ideia.

Eu provavelmente recomendaria AESCounter para qualquer pessoa com AES-NI ou SPECK128 para qualquer pessoa sem configurações altamente paralelas.

O mesmo pode acontecer com diferentes "streams", pois as sequências geradas por um LCG pela mudança da constante aditiva são todas o mesmo módulo uma mudança de sinal e uma constante aditiva.

Você pode quantificar isso? Posso replicar as falhas usando o mesmo incremento, mas semeamos o incremento, bem como o estado, e ainda não observei uma falha com dois incrementos aleatórios diferentes. Se os incrementos também tiverem que ser construídos com cuidado, isso afetará a frequência prática de colisão de aniversário.

https://gist.github.com/rkern/f46552e030e59b5f1ebbd3b3ec045759

❯ ./pcg64_correlations.py --same-increment | stdbuf -oL ./RNG_test stdin64 -tf 2 -te 1 -tlmaxonly -multithreaded
0x56b35656ede2b560587e4251568a8fed
0x93526034ed105e9e587e4251568a8fed
[
    {
        "bit_generator": "PCG64",
        "state": {
            "state": 115244779949650410574112983538102603757,
            "inc": 137507567477557873606783385380908979143
        },
        "has_uint32": 0,
        "uinteger": 0
    },
    {
        "bit_generator": "PCG64",
        "state": {
            "state": 195824235027336627448689568147458133997,
            "inc": 137507567477557873606783385380908979143
        },
        "has_uint32": 0,
        "uinteger": 0
    }
]
RNG_test using PractRand version 0.93
RNG = RNG_stdin64, seed = 0x4bf19f7b
test set = expanded, folding = extra

rng=RNG_stdin64, seed=0x4bf19f7b
length= 128 megabytes (2^27 bytes), time= 3.0 seconds
  Test Name                         Raw       Processed     Evaluation
  BCFN_FF(2+0,13-3,T)               R= +59.9  p =  3.8e-28    FAIL !!!       
  BCFN_FF(2+1):freq                 R= +89.0  p~=   6e-18     FAIL !         
  BCFN_FF(2+2):freq                 R= +39.6  p~=   6e-18     FAIL !         
  BCFN_FF(2+3):freq                 R= +14.6  p~=   6e-18     FAIL !         
  BCFN_FF(2+4):freq                 R= +10.3  p~=   5e-11   very suspicious  
  DC6-9x1Bytes-1                    R=  +7.1  p =  5.6e-4   unusual          
  DC6-6x2Bytes-1                    R= +18.9  p =  1.0e-10   VERY SUSPICIOUS 
  DC6-5x4Bytes-1                    R= +11.2  p =  1.4e-6   suspicious       
  [Low4/16]BCFN_FF(2+0):freq        R= +19.5  p~=   6e-18     FAIL !         
  [Low4/16]FPF-14+6/16:all          R=  +5.6  p =  1.0e-4   unusual          
  [Low4/16]FPF-14+6/4:all           R=  +5.9  p =  4.6e-5   unusual          
  [Low4/32]BCFN_FF(2+0):freq        R=  +6.5  p~=   2e-5    unusual          
  [Low8/32]BCFN_FF(2+0):freq        R= +15.1  p~=   6e-18     FAIL !         
  [Low8/32]FPF-14+6/32:all          R=  +8.4  p =  2.5e-7   very suspicious  
  [Low8/32]FPF-14+6/32:all2         R=  +9.0  p =  7.8e-5   unusual          
  [Low8/32]FPF-14+6/16:(0,14-0)     R= +12.4  p =  4.5e-11   VERY SUSPICIOUS 
  [Low8/32]FPF-14+6/16:all          R= +15.5  p =  5.2e-14    FAIL           
  [Low8/32]FPF-14+6/16:all2         R= +41.4  p =  2.6e-16    FAIL !         
  [Low8/32]FPF-14+6/4:(0,14-0)      R=  +6.9  p =  5.9e-6   unusual          
  [Low8/32]FPF-14+6/4:all           R=  +7.9  p =  6.6e-7   suspicious       
  ...and 871 test result(s) without anomalies

OK, vou tentar novamente.

Não há fluxos múltiplos em um LCG com módulo de potência de 2. Muitos acreditaram nisso no início, e há até mesmo jornais antigos alegando fazer coisas interessantes com esses "fluxos", mas é sabido há décadas que as órbitas que você obtém alterando as constantes são _todas o mesmo módulo um aditivo constante e possivelmente uma mudança de sinal. O mais longe que posso rastrear é

Mark J. Durst, Usando geradores congruenciais lineares para geração de números aleatórios paralelos,
1989 Winter Simulation Conference Proceedings, IEEE Press, 1989, pp. 462-466.

Então, eu escrevi outro programa http://prng.di.unimi.it/corrpcgnumpy.c no qual você pode definir:

  • Um estado inicial para um PRNG.
  • Um estado inicial para outro PRNG.
  • Uma "constante de fluxo" arbitrária para o primeiro PRNG.
  • Uma "constante de fluxo" arbitrária para o segundo PRNG (eles devem ser pares ou ímpares; essa restrição pode ser removida com algumas alterações adicionais).
  • Um número fixo de bits inferiores que definiremos adversarmente no segundo PRNG, essencialmente de forma que comece com os mesmos bits do primeiro PRNG. O restante dos bits será obtido do estado inicial para o segundo PRNG fornecido.

Portanto, esta é _exatamente_ a configuração do primeiro programa, mas você também pode escolher as constantes.

./corrpcgnumpy 0x596d84dfefec2fc7 0x6b79f81ab9f3e37b 0xac9c8abfcb89f65f 0xe42e8dff1c46de8b 0x8d7deae9efec2fc7 0x6b79f81ab9f3e37b 0x06e13e5e8c92c843 0xf92e8346feee7a21 56 | stdbuf -oL ~/svn/c/xorshift/practrand/RNG_test stdin -tf 2 -te 1 -tlmaxonly -multithreaded

rng=RNG_stdin, seed=unknown
length= 4 gigabytes (2^32 bytes), time= 113 seconds
  Test Name                         Raw       Processed     Evaluation
  [Low1/8]BCFN(0+0,13-1,T)          R= +27.2  p =  4.0e-14    FAIL
  [Low1/8]DC6-6x2Bytes-1            R= +10.9  p =  4.4e-6   suspicious
  [Low1/64]DC6-5x4Bytes-1           R=  -6.4  p =1-1.4e-4   unusual
  [Low8/64]FPF-14+6/64:(0,14-0)     R=  +8.4  p =  2.2e-7   mildly suspicious
  [Low8/64]FPF-14+6/64:all          R=  +8.7  p =  1.2e-7   suspicious
  [Low8/64]FPF-14+6/32:(0,14-0)     R= +10.2  p =  5.1e-9   suspicious
  [Low8/64]FPF-14+6/32:all          R=  +9.4  p =  2.7e-8   very suspicious
  [Low8/64]FPF-14+6/16:all          R=  +5.8  p =  6.4e-5   unusual
  ...and 1439 test result(s) without anomalies

Portanto, há _pelo menos_ 2 ^ 72 subsequências correlacionadas, não importa como você escolha as "constantes de fluxo", exatamente como no caso da mesma constante.

E recebemos uma folga ridícula do gerador: mesmo que, em vez do ponto de partida exato que estou calculando, você usasse um estado um pouco antes ou depois, a correlação apareceria de qualquer maneira. Você pode modificar facilmente o programa com um parâmetro adicional para fazer isso.

Repito, mais uma vez: nenhum gerador moderno existente na literatura científica tem esse mau comportamento (é claro, um LCG com potência de 2 tem esse comportamento, mas, pelo amor de Deus, _não_ é um gerador moderno).

As críticas de Sabastiano ao PCG são abordadas nesta postagem do

A versão resumida é que, se você tiver permissão para inventar sementes específicas, poderá mostrar um comportamento “feio” em praticamente qualquer PRNG. Apesar de sua afirmação de que o PCG é de alguma forma único, na verdade o PCG é bastante convencional - os streams do PCG não são piores do que, digamos, o SplitMix, que é outro PRNG amplamente usado.

Isso é totalmente falso. Para provar que estou errado, mostre duas sequências não sobrepostas correlacionadas de MRG32k3a ou xoshiro256 ++.

Eu nunca disse não sobreposição. Mostre-me um teste atualmente disponível para xoshiro256 ++. que duas sementes evitam a sobreposição.

Em contraste, eu tenho um teste para PCG que mostra que as “correlações” que você mostrou são essencialmente uma forma de sobreposição.

Não posso lutar contra FUD como "essencialmente" e "um formulário", mas modifiquei http://prng.di.unimi.it/intpcgnumpy.c para que inicialmente itere cada PRNG 10 bilhões de vezes e saia com um erro mensagem se a sequência gerada atravessa o estado inicial do outro PRNG. Isso garante que os primeiros 160 GB de dados em Practrand venham de sequências não sobrepostas:

./intpcgnumpy 0x596d84dfefec2fc7 0x0579f81ab9f3e37b 0x8d7deae980a64ab0 0x6c79f81ab9f3e37b | stdbuf -oL ~/svn/c/xorshift/practrand/RNG_test stdin -tf 2 -te 1 -tlmaxonly -multithreaded
[...]
rng=RNG_stdin, seed=unknown
length= 64 gigabytes (2^36 bytes), time= 926 seconds
  Test Name                         Raw       Processed     Evaluation
  [Low1/8]FPF-14+6/64:(0,14-0)      R=  +8.8  p =  8.7e-8   mildly suspicious
  [Low1/8]FPF-14+6/64:all           R=  +6.3  p =  2.1e-5   unusual          
  [Low1/16]FPF-14+6/64:(0,14-0)     R=  +7.6  p =  1.1e-6   unusual          
  [Low1/16]FPF-14+6/64:(1,14-0)     R=  +8.3  p =  2.9e-7   mildly suspicious
  [Low1/16]FPF-14+6/64:all          R=  +8.0  p =  5.8e-7   suspicious       
  [Low1/16]FPF-14+6/32:all          R=  +7.1  p =  3.9e-6   mildly suspicious
  [Low1/64]FPF-14+6/32:cross        R=  +7.1  p =  2.6e-6   mildly suspicious
  [Low4/32]FPF-14+6/64:(0,14-0)     R= +13.5  p =  4.3e-12   VERY SUSPICIOUS 
  [Low4/32]FPF-14+6/64:all          R=  +9.0  p =  5.9e-8   very suspicious  
  [Low4/64]FPF-14+6/64:(0,14-0)     R= +11.4  p =  3.8e-10  very suspicious  
  [Low4/64]FPF-14+6/64:all          R=  +8.0  p =  5.3e-7   suspicious       
  [Low4/64]FPF-14+6/32:(0,14-0)     R= +10.3  p =  3.6e-9   suspicious       
  [Low4/64]FPF-14+6/32:all          R=  +6.1  p =  3.2e-5   unusual          
  [Low8/64]FPF-14+6/64:(0,14-0)     R= +18.6  p =  8.4e-17    FAIL           
  [Low8/64]FPF-14+6/64:(1,14-0)     R= +11.4  p =  3.9e-10  very suspicious  
  [Low8/64]FPF-14+6/64:(2,14-0)     R=  +8.3  p =  2.8e-7   mildly suspicious
  [Low8/64]FPF-14+6/64:all          R= +15.3  p =  6.9e-14    FAIL           
  [Low8/64]FPF-14+6/32:(0,14-0)     R=  +7.8  p =  7.1e-7   unusual          
  [Low8/64]FPF-14+6/32:(1,14-0)     R=  +7.2  p =  2.7e-6   unusual          
  [Low8/64]FPF-14+6/32:all          R=  +5.8  p =  6.9e-5   unusual          
  ...and 1786 test result(s) without anomalies

Esses dados de inicialização específicos têm apenas 56 bits fixos inferiores, portanto, pode-se gerar 2 ^ 72 sequências correlacionadas invertendo os bits superiores. As falhas estatísticas acontecem após apenas 64 GB de dados, mostrando que as sobreposições não são responsáveis ​​pela correlação. É possível que, com outras opções de destino específicas, a sobreposição aconteça antes de 64 GB, é claro - este é um exemplo específico. Mas agora é fácil verificar se a sobreposição não é o problema - o gerador tem apenas muitas correlações internas indesejáveis.

Respeite o código de conduta . Procure manter seus comentários em tom com as diretrizes de ser "empático, acolhedor, amigável e paciente" e "ter cuidado com as palavras que escolhemos. Somos cuidadosos e respeitosos em nossa comunicação".

Eu nunca disse não sobreposição. Mostre-me um teste atualmente disponível para xoshiro256 ++. que duas sementes evitam a sobreposição.

Bem, é trivial: decida o comprimento do fluxo, itere e verifique se os dois fluxos não cruzam o estado inicial. É o mesmo código que usei para mostrar os fluxos PCG correlacionados no programa http://prng.di.unimi.it/intpcgnumpy.c não se sobrepõem.

Apesar de sua afirmação de que o PCG é de alguma forma único, na verdade o PCG é bastante convencional - os streams do PCG não são piores do que, digamos, o SplitMix, que é outro PRNG amplamente usado.

IMHO, a autocorrelação dentro do PCG é muito pior. Não há resultado para o gerador de aditivos subjacente a uma instância SplitMix análogo aos resultados dramáticos de Durst de 1989 sobre LCGs.

Mas os problemas muito leves do SplitMix são conhecidos, e o JEP 356 fornecerá uma nova classe de geradores divisíveis, LXM, tentando resolver esses problemas. Seria hora de seguir em frente e substituir o PCG também por algo menos defeituoso.

O problema subjacente é conhecido por ambos os geradores e é devido à falta de combinação de estados. Se você alterar o bit _k_ do estado de um desses geradores, a alteração nunca se propagará abaixo do bit _k_. Isso não acontece em LCGs com módulo principal, em geradores F₂-lineares, geradores CMWC, etc. Todos os outros geradores tentam misturar seu estado o mais rápido e tanto quanto possível.

Equacionar os problemas de PCG e SplitMix é uma pista falsa. SplitMix tem um gerador subjacente muito simples, apenas aditivo, mas, além disso, há uma função de embaralhamento muito poderosa: é o finalizador de 64 bits da função hash MurmurHash3 de Appleby, que tem sido amplamente usado em vários contextos e foi aprimorado por Stafford (http://zimbry.blogspot.com/2011/09/better-bit-mixing-improving-on.html). As constantes da função foram treinadas para ter propriedades de avalanche específicas e mensuráveis. Mesmo as mudanças em um pequeno número de bits tendem a se espalhar por toda a saída. Em outras palavras, SplitMix está no ombro de gigantes.

Pelo contrário, o LCG subjacente aos geradores de PCG tem os mesmos problemas de falta de mistura, mas as funções de embaralhamento são apenas uma sequência simples de operações aritméticas e lógicas montadas pelo autor sem qualquer garantia teórica ou estatística. Se eles tivessem sido planejados levando em consideração o fato de que todas as sequências do LCG subjacente são o mesmo módulo, uma constante aditiva e possivelmente uma mudança de sinal, teria sido possível resolver o problema.

Mas o autor não tinha ideia de que as sequências eram tão facilmente deriváveis ​​uma da outra. Isso pode ser facilmente visto nesta declaração na Seção 4.2.3 do relatório técnico PCG (https://www.pcg-random.org/pdf/hmc-cs-2014-0905.pdf):

"Cada escolha de _c_ resulta em uma sequência diferente de números que não tem nenhum de seus pares de saídas sucessivas em comum com outra sequência."

Isso é considerado uma prova de que as sequências são diferentes, ou seja, que o LCG subjacente fornece vários fluxos. Os resultados negativos de Durst em 1989 sobre este tópico não aparecem em nenhum lugar do jornal. Como observei anteriormente, por esses resultados todas essas sequências são iguais, módulo uma constante aditiva e possivelmente uma mudança de sinal (para LCGs com módulo de potência de 2 de potência máxima, como acontece no PCG).

Tenho certeza de que não citar os resultados de Durst é um erro _bona fide_, mas o problema é que, uma vez que você esteja convencido de que o LCG subjacente que está usando fornece "fluxos" que são "diferentes" em algum sentido, quando não são, você acaba com um gerador como o PCG no qual para cada subsequência há 2 ^ 72 subsequências correlacionadas não sobrepostas, mesmo se você alterar o "fluxo".

Obrigado a todos por sua contribuição. No momento, não estou interessado em julgamentos binários como "PCG é bom / ruim". Por favor, use seus próprios fóruns para tais discussões. O que está em questão aqui é o que o numpy fará, e esse julgamento final pertence aos desenvolvedores do numpy. Agradecemos a experiência que todos vocês trazem para esta discussão, mas quero focar nos fatos subjacentes, e não no julgamento final. Aprecio especialmente as declarações quantitativas que me dão uma ideia da quantidade de espaço livre que temos. Se meus julgamentos anteriores estavam errados, foi porque eu me precipitei para o julgamento, portanto, agradeceria sua ajuda para evitar isso novamente. Obrigado.

Observe que para obter mais de 50% de probabilidade de que dois geradores partam de duas sementes correlacionadas (escolhidas aleatoriamente), você precisa de cerca de meio milhão de geradores começando aleatoriamente (paradoxo do aniversário).

@vigna Você pode me n -bit em 2**(n/2) itens (mais ou menos um fator de 2). Meio milhão é 2**19 , então você parece estar afirmando que as correlações perigosas começam em torno de uma colisão de 40 bits nos bits inferiores, mas não vi evidências de que isso seja praticamente observável. Testei um par compartilhando os 40 bits inferiores e cheguei a 16 TiB no PractRand antes de cancelar o teste. Se você observou uma falha com uma colisão de 40 bits, quantos TiB você teve que testar para ver?

Estou convencido de que alterar o incremento não afeta a probabilidade de colisão. Uma discussão mais aprofundada sobre os méritos de "fluxos PCG" está fora do tópico. Usar essa discussão como desculpa para martelar repetidamente "o autor" é especialmente indesejável e pisa em nosso código de conduta . Persistir significará que teremos de prosseguir sem sua contribuição. Obrigado.

@imneme Isso parece estar relacionado aos problemas com pular por um múltiplo de uma grande potência de 2. Quando eu construo um par de PCG64 instâncias com o mesmo incremento e compartilhando o n inferior bits, a distância que calculo entre os dois é um múltiplo de 1 << n . Parece que sua função de saída DXSM mais forte parece resolver essa manifestação também. Eu testei um par de instâncias de PCG64DXSM que compartilham um incremento e os 64 bits inferiores de estado para 2 TiB sem problemas.

OK, isso é constrangedor: era meio _bilhão_, não meio _milhão_. Uma única letra pode fazer uma grande diferença. Peço desculpas pelo deslize.

Mas, como eu disse antes, esta é a probabilidade de atingir exatamente o mesmo estado inicial, não a probabilidade de uma sobreposição significativa de subsequências correlacionadas. Pessoalmente, prefiro usar PRNGs sem subsequências correlacionadas, uma vez que existem muitas delas, mas, como você diz com razão, a decisão é apenas sua.

Corrigir a função de embaralhamento para que tenha melhores propriedades de mixagem parece uma solução perfeitamente razoável.

Meu post pretendia apenas esclarecer as diferenças estruturais entre PCG e SplitMix, já que um post anterior afirmava que eles tinham problemas semelhantes, e não acho que seja uma afirmação correta. Você não pode escrever um programa como http://prng.di.unimi.it/corrpcgnumpy.c para SplitMix.

@rkern , você perguntou:

@imneme Isso parece estar relacionado aos problemas com pular por um múltiplo de uma grande potência de 2. Quando eu construo um par de PCG64 instâncias com o mesmo incremento e compartilhando o n inferior bits, a distância que calculo entre os dois é um múltiplo de 1 << n . Parece que sua função de saída DXSM mais forte parece resolver essa manifestação também. Testei um par de instâncias de PCG64DXSM que compartilham um incremento e os 64 bits inferiores de estado para 2 TiB sem problemas.

Obrigado por encontrar e ligar de volta ao tópico de discussão do ano passado. Sim, como Sebastiano observa em sua resposta,

Corrigir a função de embaralhamento para que tenha melhores propriedades de mixagem parece uma solução perfeitamente razoável.

XSL-RR está no lado mais fraco das coisas. Em contraste, a função de saída RXS-M original do papel PCG e a nova função de saída DXSM fazem mais na forma de embaralhamento e, portanto, não mostram esses tipos de problemas. DXSM (adicionado à fonte PCG neste commit no ano passado) foi projetado especificamente para ser mais forte do que XSL-RR, mas tem desempenho de tempo semelhante (cf, RXS-M, que é mais lento). Eu testei DXSM bastante duro no ano passado, mas 67 dias após a execução tivemos uma queda de energia prolongada que desligou o servidor (a bateria do no-break acabou) e encerrei a execução de teste, mas naquele ponto ele tinha se provado muito bem em ambos normais teste (128 TB de saída testada) e saltos de 2 ^ 48 (64 TB de saída testada, já que é executado mais lentamente)

Se, mesmo sem projetar DXSM, RXS-M teria cuidado do problema, uma pergunta é por que eu sempre usei a permutação XSL-RR mais fraca em vez disso - por que não usar sempre embaralhamento de bits muito forte na função de saída? A resposta é que basicamente tudo se resume a ciclos. A velocidade é importante para as pessoas, então você tenta evitar fazer muito mais embaralhamento do que o necessário.

Este é um assunto com o qual Sebastiano está familiarizado, porque sua abordagem e a minha têm muito em comum. Cada um de nós adota uma abordagem estabelecida há muito tempo que falharia em testes estatísticos modernos (LCGs no meu caso, e XorShift LFSRs de Marsaglia no dele) e adiciona uma função de saída de embaralhamento para resgatá-la. Nós dois nos esforçamos para tornar essa função de saída barata, e ambos fomos pegos um pouco onde as deficiências no gerador subjacente que estamos tentando mascarar com nossa função de saída, no entanto, aparecem. No caso dele, são os problemas de linearidade que aparecem .

Mas em um trabalho recente que me agradou muito, ele também mostrou como muitos designs baseados em LFSR têm problemas de peso excessivo (refletindo uma preocupação antiga minha) que são inadequadamente mascarados por suas funções de saída. Seus próprios geradores passam no teste, então isso é alguma coisa, mas em 2018, quando eu estava olhando para seu novo xoshiro PRNG , pareceu-me que os problemas de peso do hamming do gerador subordinado conseguiram passar por sua função de saída. Desde então, ele revisou o xoshiro com uma nova função de saída e espero que funcione (ele fez algumas outras alterações também, então talvez essa também corrija o problema de repetições, destacado por este programa de teste ?).

Quanto aos seus programas de correlação, em 2018, quando ele fez uma crítica ao PCG em seu site que incluía programas que ele havia escrito com vários problemas (como semeadura artificial, etc.), escrevi uma resposta que continha vários programas semelhantes para outros PRNGs estabelecidos há muito tempo, incluindo corrsplitmix2.c que cria fluxos correlacionados em SplitMix. Não tenho certeza do que Sebastian quer dizer quando diz que não pode ser feito, mas devo admitir que não tive a chance de olhar de perto seu programa de teste para ver se o novo é substancialmente diferente dos que ele escreveu alguns anos atrás.

Por favor, perdoe minha falta de compreensão - mas eu poderia pedir a alguém uma conclusão resumida neste estágio? Existem circunstâncias realistas em que o PCG padrão não é seguro? Quais são os argumentos para mudar o padrão?

O caminho fácil é adicionar alguma documentação sobre aplicações de dimensões altas (ultra-altas?) E a probabilidade de sequências correlacionadas.

Um caminho mais difícil seria substituir a função de saída que interromperia o fluxo. Não tenho certeza de quão forte é a promessa que default_rng faz. Os documentos não parecem avisar que isso pode mudar, então provavelmente seria necessário um ciclo de suspensão de uso para mudar. Isso exigiria adicionar a nova função de saída como um gerador de bits autônomo (ou configurável a partir de PCG64, o que seria mais sensato) e, em seguida, avisar aos usuários que ela mudará após o ano XXXX / lançamento Y.ZZ.

O caminho mais difícil seria encontrar um novo rng padrão. Não foi fácil da primeira vez e não acho que nada mudou para mover a agulha em qualquer direção específica nos últimos 18 meses.

Abri gh-16493 sobre a modificação default_rng() , não acho que esteja relacionada a esse assunto, nem mesmo tenho certeza de que precisamos discuti-lo, provavelmente já havíamos estabelecido as regras há muito tempo e eu simplesmente não lembro.


Não pretendo entender esta discussão completamente, mas parece que há duas coisas a descobrir:

  1. Tendo certeza de que temos progresso suficiente, tenho que confiar em Robert nisso e agora parece que deve estar tudo bem, pelo que sabemos? (Ou seja, a probabilidade de uma colisão real é provavelmente embaraçosamente baixa, mesmo em ambientes com magnitudes maiores do que qualquer coisa em que o NumPy possa ser usado? Se pode ou não justificar a alteração do padrão no futuro, é uma questão diferente.)
  2. Nós afirmamos:

    e suporta, [...] bem como: matemática: 2^{127} streams

    que, não sabendo exatamente de onde vem o número, parece que pode ser um pequeno exagero de nossa parte e poderíamos considerá-lo ligeiramente ajustado para estar perfeitamente correto? Ou link para algum recurso externo fornecendo detalhes adicionais?

A coisa mais fácil de fazer agora seria adicionar PCG64DXSM BitGenerator , a variante do PCG64 com o "multiplicador barato" e função de saída DXSM mais forte. Acho que todos concordam que isso é um passo à frente da função de saída XSL-RR que temos agora em nossa implementação PCG64 , tendo um melhor desempenho estatisticamente sem danos ao desempenho do tempo de execução. É uma atualização direta no nicho que PCG64 serve para os BitGenerator s que fornecemos. Acho que devemos adicioná-lo ao lado de PCG64 .

A propósito, prefiro que seja um nome separado BitGenerator vez de uma opção para o construtor PCG64 . Essas opções são ótimas em randomgen , cujo objetivo é fornecer muitos algoritmos e variantes diferentes, mas para numpy , acho que queremos que nossas seleções sejam "instantâneas" como tanto quanto nós podemos.

Acho que não definimos realmente a política de fazer alterações no que default_rng() fornece. Quando o propus, trouxe à tona a noção de que um dos motivos pelos quais preferia apenas colocar sua funcionalidade no construtor Generator() era que poderíamos descontinuar e mudar para uma função com nome diferente se necessário. No entanto, naquela época, estávamos considerando que default_rng() poderia precisar expor muitos detalhes do BitGenerator subjacente, o que subsequentemente evitamos. Como PCG64DXSM expõe a mesma API ( .jumped() em particular) como PCG64 , a única consideração que teríamos é que usar como o novo padrão mudaria o fluxo de bits. Acho que seria razoável seguirmos a mesma linha do tempo de qualquer outra modificação no stream proveniente de Generator métodos por NEP 19 (ou seja, em X.Y.0 lançamentos de recursos). Podemos escolher ser um pouco mais cautelosos, se quisermos, e primeiro expor PCG64DXSM como um BitGenerator em 1.20.0 e documento (mas não warn() , muito barulhento para nenhum efeito) que default_rng() mudará para usá-lo em 1.21.0 .

Como parte da adição do novo BG, seria bom atualizar as notas para PCG64 para começar a servir como um guia e fornecer uma justificativa para preferir a variante mais recente.

  1. Tendo certeza de que temos progresso suficiente, tenho que confiar em Robert nisso e agora parece que deve estar tudo bem, pelo que sabemos? (Ou seja, a probabilidade de uma colisão real é provavelmente embaraçosamente baixa, mesmo em ambientes com magnitudes maiores do que qualquer coisa em que o NumPy possa ser usado? Se pode ou não justificar a alteração do padrão no futuro, é uma questão diferente.)

Isso provavelmente é um pouco simplista. Depende de quantos streams, de quantos dados você extrai de cada stream e de qual é a sua tolerância ao risco para uma colisão de aniversário. Ainda não consegui transformar essa matemática em um parágrafo compreensível para fazer recomendações fáceis de seguir, e é por isso que não reviso esse problema do Github há algum tempo. No entanto, não acho que seja um problema sério que precise ser corrigido _ agora_.

Vou escrever algo mais tarde, mas, pelo que vejo neste tópico, estamos recauchutando o terreno que percorremos no ano passado. Nada mudou, a não ser que Sebastiano descobriu que o NumPy tinha enviado o PCG. A análise da equipe do NumPy no ano passado foi mais aprofundada e considerou cenários mais plausíveis.

Minha preferência seria atualizar o padrão o mais rápido possível - apenas para reduzir a confusão. Quer dizer, não espere por um ciclo de depreciação.

@imneme - muito obrigado - eu acharia uma coisa mais longa muito útil.

Provavelmente, a melhor explosão do post

Eu tinha na cabeça o que queria dizer, mas acho que olhando os posts de um ano atrás, já disse tudo, tanto aqui como em outros lugares ( meu blog (2017), reddit , meu blog de novo (2018) e em discussões NumPy, etc.)

Sobre streams e sua auto-similaridade (para a qual @rkern escreveu um testador de dependência de stream ), escrevi no ano passado:

Conforme observado em minha postagem de blog mencionada anteriormente, os streams do PCG têm muito em comum com os do SplitMix.

Em relação ao gráfico de @mdickinson, para _cada_ PRNG que permite que você semeie todo o seu estado, incluindo os criptográficos baseados em contador, podemos conceber seedings onde teríamos PRNGs cujas saídas foram correlacionadas de alguma forma (a maneira mais fácil de fazer isso é criar estados PRNG que estão a uma curta distância uns dos outros, mas muitas vezes podemos fazer outras coisas com base na compreensão de como funcionam). E embora os PRNGs que não permitem a propagação de estado completo possam evitar esse problema, fazer isso apenas introduz um novo, fornecendo acesso prático apenas a uma pequena fração de seus estados possíveis.

A maneira certa de pensar em fluxos é apenas mais um estado aleatório que precisa ser semeado. Usar valores pequenos como 1,2,3 geralmente é uma má ideia para qualquer propósito de semeadura para _qualquer_ PRNG (porque se todos favorecerem essas sementes, suas sequências iniciais correspondentes serão super-representadas).

Podemos escolher não chamá-lo de fluxo e apenas chamá-lo de estado. Isso é o que Marsaglia fez em XorWow . Se você olhar para o código, a sequência de Weyl counter não interage com o resto do estado de forma alguma e, como LCGs, e variações no valor inicial realmente representam apenas uma constante adicionada.

Os streams de SplitMix, PCG e XorWow são o que podemos chamar de streams “estúpidos”. Eles constituem uma reparametrização trivial do gerador. Há valor nisso, entretanto. Suponha que sem fluxos, nosso PRNG teria uma interessante repetição próxima de 42, onde 42 surge várias vezes em rápida sucessão e só faz isso para 42 e nenhum outro número. Com streams estúpidos de “apenas um incremento” ou “apenas um xor”, na verdade evitaremos conectar a repetição estranha a 42; todos os números têm um fluxo no qual eles se repetem de forma estranha. (Por esse motivo, a correção que eu aplicaria para reparar os problemas de repetição de fechamento no Xoshiro 256 é misturar em uma sequência de Weyl.)

(Eu, então, me aprofundei neste comentário e neste .)

Eu diria que a principal conclusão é que, para quase _qualquer_ PRNG, com um pouco de tempo e energia você pode inventar sementes patológicas. PCG está em uma posição um tanto incomum que tem alguém que gosta de trabalhar em seedings de aparência plausível para PCG especificamente que têm uma patologia feita à mão (por exemplo, Sebastiano). Como resultado desse trabalho, dei meia-volta e fiz o mesmo tanto para seus PRNGs quanto para outros antigos.

Em geral, você deseja inicializar o estado PRNG com algo que pareça “meio aleatório”. Isso é bastante universal, mesmo se as pessoas quiserem fazer o contrário . Por exemplo, para qualquer LFSR PRNG (estilo XorShift, Mersenne Twister, etc.), você não deve inicializá-lo com um estado de zeros porque ele simplesmente ficará preso aí. Mas mesmo estados que são principalmente zero são frequentemente problemáticos para LFSRs (um fenômeno conhecido como zeroland, e por que o pessoal do C ++ ganhou seed_seq .). Se você quiser jogar o jogo “vamos fazer alguma propagação planejada”, não é difícil criar uma coleção de inicializações para 100 LFSRs e ter todos eles a 1K de chegarem ao zero. Todas as inicializações planejadas parecerão inocentes o suficiente, mas todas terão essa estranha queda no peso do hamming ao mesmo tempo.

Se você estiver usando um gerador de PCG que foi inicializado com entropia razoável, tudo bem. Se você deseja inicializá-lo com lixo como 1, 2, 3, isso é realmente problemático com qualquer PRNG . E com o PCG, 99,9% das vezes, mesmo usando algo meio viciado seria bom. Não tem nada parecido com o tipo de problema que os LFSRs têm.

Mas DXSM é certamente mais forte. Acho que é melhor ou não teria conseguido, mas vale a pena ter alguma perspectiva e perceber que, na prática, os usuários não terão problemas com a implementação clássica do PCG64.

Eu gostaria de separar a crítica / defesa dos streams do PCG64 (via incremento LCG) da presente discussão. Embora haja uma certa dualidade envolvida devido à matemática do LCG, não é a questão central que foi originalmente levantada aqui. Além disso, há mais detalhes aqui a serem considerados do que estavam presentes na crítica original de Sebastiano, em sua resposta ou no velho mega-thread. Talvez a conexão seja mais óbvia para os especialistas que gastaram mais tempo com matemática, mas as consequências práticas agora são mais claras para mim, pelo menos.

Eu diria que a principal conclusão é que, para quase _qualquer_ PRNG, com um pouco de tempo e energia você pode inventar sementes patológicas.

Concedido, mas não é o binário pode / não pode que conduz a decisão na minha frente. Se eu extrair muitos números de um PRNG finito, o PractRand acabará descobrindo. Esse fato binário não invalida esse algoritmo PRNG. Afastar-se desse binário e estabelecer o conceito de espaço livre foi uma das coisas que realmente gostei no artigo original do PCG. Dada uma patologia gerada adversarialmente, podemos dar uma olhada em quantas vezes essa patologia pode surgir aleatoriamente de uma boa semeadura de entropia. Quero quantificar isso e transformá-lo em conselhos práticos para os usuários.

Dados dois estados que compartilham os 58 bits inferiores e o mesmo incremento (colocaremos um pino nisso), intercalar as instâncias PCG64 XSL-RR desses estados demonstra falhas praticamente observáveis ​​em PractRand em cerca de 32 GiB. Acho que é razoável querer evitar isso. Então, vamos tomar isso como nosso benchmark e ver com que frequência isso surge com uma boa semeadura de entropia. Felizmente, esse esquema adversário é passível de análise probabilística (nem todos são tão amigáveis). Para n instâncias, a probabilidade de qualquer 2 compartilhar os mesmos 58 bits inferiores é n**2 / 2**58 , mais ou menos 2 para contagem dupla. Portanto, em meio bilhão de instâncias, as chances são boas de que haja um desses pares que falharia em PractRand se intercalado. Meio bilhão é muito! Em minha opinião, provavelmente nunca veremos um programa numpy que tente criar tantas PCG64 instâncias. numpy provavelmente seria a ferramenta errada, então.

Acho que também é razoável querer evitar estados iniciais cujos desenhos subsequentes _cross_ qualquer um dos estados de colisão de 58 bits inferiores dos outros estados iniciais. Ainda estou tentando refletir sobre a lógica disso, mas acho que o comprimento afeta a probabilidade linearmente em vez de quadraticamente. Se eu estiver certo e quiser retirar 16 GiB de cada instância ( 2**28 draws), que é o quanto retiramos de cada par que apresentou falhas PractRand, então só posso trabalhar com cerca de 2**15 instâncias, ou cerca de 32k, antes de se tornar bastante provável a observação de um cruzamento. Isso ainda é uma grande quantidade de instâncias para Python! E a quantidade total de dados gerados é cerca de meio petabyte, o que é muito! Mas está no horizonte da praticidade, e se eu quiser manter a probabilidade baixa, não apenas abaixo da metade, tenho que descer em uma delas. Não estou particularmente preocupado com esses números; Não acho que nenhum programa numpy real tenha problemas ao usar PCG64 com a função de saída XSL-RR. Mas algumas aplicações podem começar a se aproximar (grandes execuções de aprendizado por reforço distribuído , por exemplo).

Vamos pegar esse pino de incremento e resolvê-lo. Acho que é justo dizer que, com a função de saída XSL-RR, também a semeadura de entropia do incremento além do estado não altera essa análise em particular. Parece que para qualquer par de incrementos semeados de entropia, há o mesmo número de estados de colisão prática. O procedimento concreto para construir deliberadamente esses estados parece mais complicado do que bater nos mesmos 58 bits inferiores, mas parece que o número de estados em colisão é o mesmo, então os cálculos de probabilidade permanecem os mesmos. Isso não é intrínseco ao esquema PCG em geral. A função de saída DXSM parece ser forte o suficiente para que a alteração do incremento (mesmo com um simples +2 ) pareça ser suficiente para resistir até mesmo ao pior caso para o LCG subjacente (quando a métrica de distância dá 0 ), pelo menos tanto quanto me preocupei em testar com PractRand.

Quero terminar reiterando o que todos parecemos estar de acordo sobre: PCG64DXSM é uma boa ideia. Se nada mais, suas propriedades estatísticas aprimoradas simplificam os modelos mentais que me sinto compelido a documentar, e qualquer coisa que signifique que eu tenha que escrever menos documentação é bom em meu livro.

Os fluxos ainda são um tanto relevantes porque o problema só aparece se tivermos os geradores no mesmo fluxo.

Mas em que circunstâncias eles teriam os mesmos 58 bits inferiores e estariam no mesmo fluxo? Existe um caso de uso em que isso aconteceria?

O único caso um tanto realista que conheço é aquele sobre o qual falamos no ano passado (quando falamos sobre jumped ) e sobre o qual falei neste post ao qual fiz um link anteriormente.

Os fluxos ainda são um tanto relevantes porque o problema só aparece se tivermos os geradores no mesmo fluxo.

Infelizmente, esse não é o caso do XSL-RR. Vamos considerar duas instâncias de PCG64 XSL-RR. Semeamos com entropia os incrementos arbitrariamente e semeamos com entropia um dos estados. Podemos construir 2**70 estados ruins para a outra instância PCG64 que falhou PractRand da mesma forma que a falha do mesmo incremento de mesmo estado inferior de 58 bits. É apenas mais complicado de fazer do que no caso do mesmo incremento. Em vez de compartilhar os 58 bits mais baixos como o primeiro estado com o incremento diferente, ele compartilha os 58 bits mais baixos do estado que é a distância 0 (de acordo com sua medida de distância LCG) da primeira instância, contabilizando os incrementos. Tenho uma prova construtiva (código Python), mas tenho que ir para a cama agora e limpar amanhã.

@rkern , bom argumento. Eu admito, não testei esse cenário para ver como ele se sai.

Eu diria que a principal conclusão é que, para quase _qualquer_ PRNG, com um pouco de tempo e energia você pode inventar sementes patológicas. PCG está em uma posição um tanto incomum que tem alguém que gosta de trabalhar em seedings de aparência plausível para PCG especificamente que têm uma patologia feita à mão (por exemplo, Sebastiano). Como resultado desse trabalho, dei meia-volta e fiz o mesmo tanto para seus PRNGs quanto para outros antigos.

Como já observei, isso é falso. Não conheço nenhum exemplo de um par de sequências correlacionadas e não sobrepostas, digamos, de xoshiro256 ++ como você pode encontrar facilmente no PCG.

PRNGs que misturam rapidamente todo o seu estado não têm esse problema. Se você puder fornecer um programa que gere duas sequências não sobrepostas de xoshiro256 ++ que são correlacionadas, como os exemplos que postei aqui, faça-o.

Quanto aos seus programas de correlação, em 2018, quando ele fez uma crítica ao PCG em seu site que incluía programas que ele havia escrito com vários problemas (como semeadura artificial, etc.), escrevi uma resposta que continha um monte de programas semelhantes para outros PRNGs estabelecidos há muito tempo, incluindo corrsplitmix2.c que cria fluxos correlacionados em SplitMix. Não tenho certeza do que Sebastian quer dizer quando diz que não pode ser feito, mas devo admitir que não tive a chance de olhar de perto seu programa de teste para ver se o novo é substancialmente diferente dos que ele escreveu alguns anos atrás.

O programa citado acima _escolhe os fluxos_. Obviamente, é fácil escrevê-lo.

Mas isso não tem nada a ver com os problemas do PCG. O programa que forneci permite que o _usuário_ escolha os fluxos e mostra a correlação.

Convido, novamente, @inmeme a fornecer um programa para SplitMix no qual o usuário pode selecionar dois fluxos diferentes arbitrariamente, um estado inicial arbitrário do primeiro gerador, e _então_ o programa encontra uma sequência correlacionada no outro gerador, como http: / /prng.di.unimi.it/corrpcgnumpy.c sim.

Permitir que o usuário escolha o fluxo arbitrariamente mostra uma forma de correlação muito mais forte.

Como já observei, isso é falso. Não conheço nenhum exemplo de um par de sequências correlacionadas e não sobrepostas, digamos, de xoshiro256 ++ como você pode encontrar facilmente no PCG.

Parece que estamos falando um do outro aqui. Não disse que poderia criar sequências correlacionadas e não sobrepostas para qualquer PRNG, disse que poderia criar sementes patológicas em geral, conforme mostrado com os vários programas de correlação que escrevi anteriormente, e outros semelhantes como o programa de demonstração de repetições ruins para Xoshiro ** .

Além disso, PRNGs que não misturam todo o seu estado têm uma longa história, incluindo XorWow, os geradores em receitas numéricas, etc. O argumento de Sebastiano representa o ponto de vista, mas seu argumento diria que de alguma forma Marsaglia fez XorShift _worse_ em XorWow adicionando um Weyl sequência, uma vez que cria um grande número de geradores semelhantes.

Parece que estamos falando um do outro aqui. Não disse que poderia criar sequências correlacionadas e não sobrepostas para qualquer PRNG, disse que poderia criar sementes patológicas em geral, conforme mostrado com os vários programas de correlação que escrevi anteriormente, e outros semelhantes como o programa de demonstração de repetições ruins para Xoshiro ** .

Tente manter a discussão em um nível técnico. "Patológico" não tem significado matemático.

A maneira tecnicamente correta de verificar a autocorrelação é encontrar duas sementes produzindo duas sequências não sobrepostas (sem sobreposição durante o teste - elas inevitavelmente se sobreporão se você for longe o suficiente), intercalá-las e passá-las para uma bateria de testes.

Se você considerar duas sequências que se sobrepõem, elas serão correlacionadas para cada gerador, mesmo um criptográfico, simplesmente porque as mesmas saídas acontecerão duas vezes depois que as sequências se sobrepõem, e qualquer teste razoável detectará isso.

"Semeadura patológica" usando sequências sobrepostas é uma tarefa trivial para todo gerador (qualquer que seja o meio "patológico").

Mais uma vez, uma vez que você afirma ter encontrado correlação semelhante ao PCG (em que as sequências não estão sobrepostas, como mostra o teste) em outros geradores, você pode fornecer um par de sequências correlacionadas e não sobrepostas, digamos, de xoshiro256 ++ ou SFC64 ?

A maneira tecnicamente correta de verificar a autocorrelação é encontrar duas sementes produzindo duas sequências não sobrepostas (sem sobreposição durante o teste - elas inevitavelmente se sobreporão se você for longe o suficiente), intercalá-las e passá-las para uma bateria de testes.

Você pode apontar a literatura para esta definição de correlação, para que eu possa ter certeza de permanecer “tecnicamente correto” sobre isso também?

Sebastiano, você continua querendo que eu responda a um desafio feito nos seus termos. O que você está apontando está relacionado às propriedades intrínsecas dos LCGs, onde há auto-similaridade. Você não encontrará o mesmo problema em um PRNG caótico ou baseado em LFSR.

Mas, para outros PRNGs, haverá outros pontos fracos.

LFSRs têm zeroland, estados ruins, problemas de peso de hamming, linearidade e, como aprendemos com suas tentativas com xoshiro, às vezes outras estranhezas como problemas estranhos com repetições.

PRNGs caóticos têm o risco de ciclos curtos (embora aqueles com um contador evitem isso - Weyl sequencia FTW!) E seu viés intrínseco.

Se as sequências se sobrepõem, como escrevi, o teste _sempre_ falhará. Você não precisa de literatura para entender que um teste que _sempre falha_ não é um teste.

Mais uma vez, uma vez que você afirma ter encontrado correlação semelhante ao PCG (em que as sequências não estão sobrepostas, como mostra o teste) em outros geradores, você pode fornecer um par de sequências correlacionadas e não sobrepostas, digamos, de xoshiro256 ++ ou SFC64 ?

Você realmente parece estar se esquivando da pergunta. Seria muito fácil para você, seguindo suas alegações, fornecer tais evidências, se as tivesse.

A coisa mais fácil de fazer agora seria adicionar PCG64DXSM BitGenerator , a variante do PCG64 com o "multiplicador barato" e função de saída DXSM mais forte. Acho que todos concordam que isso é um passo à frente da função de saída XSL-RR que temos agora em nossa implementação PCG64 , tendo um melhor desempenho estatisticamente sem danos ao desempenho do tempo de execução. É uma atualização direta no nicho que PCG64 serve para os BitGenerator s que fornecemos. Acho que devemos adicioná-lo ao lado de PCG64 .

Observe que os "multiplicadores baratos" de 64 bits têm defeitos comprováveis. Isso é conhecido há muito tempo:

W. Hörmann e G. Derflinger, um gerador portátil de números aleatórios adequado para o
método de rejeição, ACM Trans. Matemática. Softw. 19 (1993), no. 4, 489–495.

Em geral, multiplicadores menores que a raiz quadrada do módulo têm limites inerentes para sua pontuação espectral f₂.

O limite pode ser facilmente superado usando um multiplicador de 65 bits, que o compilador irá transformar apenas em uma operação adicional de "adição", provavelmente nem mesmo alterando a velocidade do gerador.

Guy Steele e eu trabalhamos um pouco no assunto e publicamos tabelas de pontuações espectrais para multiplicadores baratos de vários tamanhos: https://arxiv.org/pdf/2001.05304.pdf . Quanto maior, melhor, mas há uma lacuna comprovável de 64 a 65 bits (para um LCG com 128 bits de estado).

Por exemplo, da Tabela 7 do artigo você obtém 0x1d605bbb58c8abbfd, que tem pontuação de f₂ 0,9919. Nenhum multiplicador de 64 bits pode ir além de 0,9306 (Teorema 4.1 no artigo).

Depois da mistura e de tudo, a melhora na pontuação f₂ pode passar totalmente despercebida do ponto de vista estatístico. Mas, considerando a grande melhoria que você obtém para a dimensão mais relevante com apenas uma operação de adição adicional, acho (bem, achamos, ou não teríamos escrito o artigo) vale a pena o esforço.

Sebastiano, você continua querendo que eu responda a um desafio feito nos seus termos. O que você está apontando está relacionado às propriedades intrínsecas dos LCGs, onde há auto-similaridade. Você não encontrará o mesmo problema em um PRNG caótico ou baseado em LFSR.

Nossa, demorou um pouco para chegar lá!

LFSRs têm zeroland, estados ruins, problemas de peso de hamming, linearidade e, como aprendemos com suas tentativas com xoshiro, às vezes outras estranhezas como problemas estranhos com repetições.

Eu concordo totalmente - é por isso que você tem que embaralhá-los. Observe que LFSRs e geradores lineares F linear são coisas diferentes; relacionados, mas diferentes.

"Problemas estranhos com repetições" é, como sempre, um termo não técnico que não posso comentar.

PRNGs caóticos têm o risco de ciclos curtos (embora aqueles com um contador evitem isso - Weyl sequencia FTW!) E seu viés intrínseco.

[Atualização: perdi a observação do contador entre parênteses, então estou atualizando meu comentário.]

Sim, o SFC64 não tem esses problemas (ele usa um contador), portanto, não generalizaria para toda a categoria. Existem geradores caóticos cuidadosamente projetados que têm comprimento de ciclo comprovável, grande e mais curto.

"Problemas estranhos com repetições" é, como sempre, um termo não técnico que não posso comentar.

Parece estranho não poder comentar porque não usei o jargão correto - execute este programa e sinta-se à vontade para me esclarecer sobre a melhor maneira de descrever o problema no jargão adequado e fornecer qualquer comentário que pareça apropriado. Eu teria imaginado que você e David Blackman teriam discutido o assunto quando ele veio à tona, porque eu me correspondi com ele sobre isso, mas nunca vi você comentar sobre isso.

Discussão de PRNGs que não estão em numpy está fora do tópico. Use seus próprios fóruns para continuar essa discussão. Obrigado.

@rkern - parece um pouco rígido como critério. Se houver uma deficiência na implementação Numpy que não é compartilhada por outras implementações, parece razoável discutir isso.

Posso confirmar que a troca a que me refiro não está me ajudando a tomar as decisões que temos pela frente nesta questão. Até que façamos progresso nisso, preciso que a conversa mantenha o foco.

Acho que é útil entender o contexto mais amplo. Sebastiano gosta um pouco do PCG e há anos vem reclamando dele. Acho que hoje em dia algumas pessoas podem olhar para nós dois, revirar os olhos e dizer "vocês são tão ruins um quanto o outro" porque eu também critiquei seus PRNGs, mas na verdade eu só fiz isso depois que ele fez afirmações de que Eu estava tentando esconder algo nunca falando sobre as coisas dele (quando na realidade, eu simplesmente não tinha tempo / inclinação - na verdade, eu simplesmente presumi que eles estavam bem).

Suas críticas são úteis, porém, e estou feliz por ele ter escolhido passar tanto de sua vida pensando no meu trabalho, mas é importante perceber que seus testes são de natureza adversa. Ele usa o conhecimento da estrutura do PCG para criar arranjos que podem ser inseridos nos testadores RNG e nos testes de falha.

Dado o quão assustador isso pode parecer, parece razoável mostrar que uma abordagem adversária semelhante também tropeçaria em vários outros geradores e que muitas das preocupações que ele levanta sobre o PCG se aplicariam a outros esquemas de geração também, como observei sobre esquemas de geração como XorWow, e costumo usar SplitMix como exemplo, como farei a seguir. (Nenhum de nós está particularmente interessado em SplitMix de uma forma ou de outra, eu imagino.)

Podemos ser super assustadores sobre SplitMix, por exemplo, e mostrar que com a constante de fluxo padrão, se olharmos para cada saída 35185-th, ele falha em um conjunto de testes PRNG. Oh não! Isso ocorre porque internamente ele está incrementando um contador (sequência de Weyl!) Em 0x9e3779b97f4a7c15 (com base em φ, a proporção áurea), mas 35185 * 0x9e3779b97f4a7c15 = 0x86a100000c480245 , que tem apenas 14 bits definidos e uma grande faixa de nada no meio. Ou se olharmos para cada saída 360998717-th, chegaremos a ser equivalente a uma adição ao estado interno de 0x48620000800401 , que é apenas 8 bits sendo adicionados e novamente algo difícil para sua função de saída para mascarar totalmente .

Poderíamos continuar espalhando medo sobre SplitMix e dizer olhe, e se eu tiver dois streams, um com a constante aditiva 0x9e3779b97f4a7c15 e outro com 0xdaa66d2c7ddf743f , veríamos falhas se colocarmos isso em um conjunto de testes PRNG !!! Mas isso é porque o segundo foi planejado para ser apenas 3x o outro.

E, finalmente, se alguém disser “Eu vou te dar os dois streams, faça algo assustador com isso!”, E digamos que os deles sejam baseados em π ( 0x243f6a8885a308d3 ) e _e_ ( 0xb7e151628aed2a6b ), podemos dizer, com certeza, vamos ter um pouco mais de assustar e pegar cada item 6561221343-th do fluxo Pi e misturá-lo com cada item 6663276199-th do fluxo E e baixo-e-belhold, eles produzem dois itens idênticos sequências. E _pior_, eu então continuo mostrando que para cada salto no fluxo a, há um salto correspondente no fluxo b para dar a mesma saída, então há na verdade 2 ^ 64 maneiras de correlacionar !!! (E podemos fazer isso para quaisquer dois fluxos, não havia nada de especial sobre π e _e_.)

Voltando ao PCG, o teste de Sebastiano conta com os dois geradores PCG64 XSH RR alinhados precisamente para que as saídas correspondentes sejam intercaladas. Se apenas avançarmos um dos PRNGs por uma pequena quantidade, rompendo o alinhamento perfeito um pouco, se torna muito mais difícil detectar qualquer coisa suspeita.

Um teste adversário semelhante na outra direção (colocando um fardo sobre Sebastiano) seria fornecer saídas de PCG64 XSH RR que atendam a sua afirmação de que estão correlacionadas, mas não dizemos a ele exatamente como estão alinhadas (estão apenas na vizinhança geral certa). Seu trabalho seria encontrar o alinhamento para mostrar que eles estão correlacionados.

No geral, não acho que seja um problema na prática com incêndios urgentes para serem apagados, mas por outro lado, a versão DXSM é melhor porque foi escrita no ano passado para mitigar precisamente esses tipos de problemas, e eu estaria muito prazer em ter você mudado para ele.

PS Você pode fazer constantes aditivas Weyl mágicas de seu número real favorito usando este código:

WeylConst[r_,bits_] = BitOr[Floor[(r-Floor[r])*2^bits],1]

Esse é o Mathematica, vou deixar a versão Python como exercício.

Veja como construo as colisões de bits inferiores para incrementos diferentes.

Resultados com PCG64 XSL-RR e uma colisão inferior de 58 bits

❯ ./pcg64_correlations.py -m 58 | stdbuf -oL ./RNG_test stdin64 -tf 2 -te 1 -tlmaxonly -multithreaded
s0 = 0b01110010100110011101000110010010101111111001100011001011001011111001001110101010011101111101001101011000011100001111111111100001
s1 = 0b10110001011001100111100010000110101110011010101010011011010100011001011111001100010001101001001011010010110101001011101111111100
dist = 0x2eb6ec432b0ea0f4fc00000000000000
[
    {
        "bit_generator": "PCG64",
        "state": {
            "state": 152330663589051481538402839025803132897,
            "inc": 228410650821285501905570422998802152525
        },
        "has_uint32": 0,
        "uinteger": 0
    },
    {
        "bit_generator": "PCG64",
        "state": {
            "state": 235805414096687854712168706130903874556,
            "inc": 70910205337619270663569052684874994465
        },
        "has_uint32": 0,
        "uinteger": 0
    }
]
RNG_test using PractRand version 0.93
RNG = RNG_stdin64, seed = 0x12d551b8
test set = expanded, folding = extra

rng=RNG_stdin64, seed=0x12d551b8
length= 128 megabytes (2^27 bytes), time= 2.8 seconds
  no anomalies in 891 test result(s)

rng=RNG_stdin64, seed=0x12d551b8
length= 256 megabytes (2^28 bytes), time= 9.4 seconds
  no anomalies in 938 test result(s)

rng=RNG_stdin64, seed=0x12d551b8
length= 512 megabytes (2^29 bytes), time= 18.1 seconds
  no anomalies in 985 test result(s)

rng=RNG_stdin64, seed=0x12d551b8
length= 1 gigabyte (2^30 bytes), time= 31.2 seconds
  Test Name                         Raw       Processed     Evaluation
  [Low1/8]FPF-14+6/16:cross         R=  +4.9  p =  1.7e-4   unusual          
  [Low4/16]FPF-14+6/16:all          R=  +8.4  p =  2.3e-7   very suspicious  
  [Low4/16]FPF-14+6/16:all2         R=  +8.3  p =  8.1e-5   unusual          
  [Low8/32]FPF-14+6/32:all          R=  +6.3  p =  2.1e-5   mildly suspicious
  [Low8/32]FPF-14+6/16:all          R=  +5.7  p =  8.0e-5   unusual          
  ...and 1034 test result(s) without anomalies

rng=RNG_stdin64, seed=0x12d551b8
length= 2 gigabytes (2^31 bytes), time= 52.7 seconds
  Test Name                         Raw       Processed     Evaluation
  [Low4/16]FPF-14+6/32:all          R=  +7.4  p =  2.0e-6   suspicious       
  [Low4/16]FPF-14+6/16:(0,14-0)     R=  +7.7  p =  9.4e-7   unusual          
  [Low4/16]FPF-14+6/16:all          R=  +8.0  p =  5.9e-7   suspicious       
  [Low4/16]FPF-14+6/16:all2         R= +12.2  p =  2.1e-6   mildly suspicious
  [Low4/16]FPF-14+6/4:(0,14-0)      R=  +7.9  p =  6.3e-7   mildly suspicious
  [Low4/16]FPF-14+6/4:all           R=  +5.8  p =  6.7e-5   unusual          
  [Low4/16]FPF-14+6/4:all2          R= +11.5  p =  3.1e-6   mildly suspicious
  [Low8/32]FPF-14+6/32:(0,14-0)     R=  +7.8  p =  8.4e-7   unusual          
  [Low8/32]FPF-14+6/32:all          R=  +7.3  p =  2.3e-6   suspicious       
  [Low8/32]FPF-14+6/32:all2         R= +14.3  p =  3.8e-7   suspicious       
  [Low8/32]FPF-14+6/16:(0,14-0)     R=  +7.7  p =  8.8e-7   unusual          
  [Low8/32]FPF-14+6/16:(1,14-0)     R=  +7.7  p =  9.3e-7   unusual          
  [Low8/32]FPF-14+6/16:all          R=  +6.9  p =  5.3e-6   mildly suspicious
  [Low8/32]FPF-14+6/16:all2         R= +18.3  p =  8.0e-9   very suspicious  
  ...and 1078 test result(s) without anomalies

rng=RNG_stdin64, seed=0x12d551b8
length= 4 gigabytes (2^32 bytes), time= 90.2 seconds
  Test Name                         Raw       Processed     Evaluation
  [Low1/8]BCFN_FF(2+0):freq         R= +14.8  p~=   6e-18     FAIL !         
  [Low1/8]BCFN_FF(2+1):freq         R=  +7.4  p~=   1e-6    mildly suspicious
  [Low1/8]FPF-14+6/16:cross         R=  +8.4  p =  2.1e-7   very suspicious  
  [Low4/16]FPF-14+6/32:(0,14-0)     R=  +8.9  p =  8.1e-8   mildly suspicious
  [Low4/16]FPF-14+6/32:(1,14-0)     R=  +8.5  p =  1.9e-7   mildly suspicious
  [Low4/16]FPF-14+6/32:all          R=  +9.4  p =  2.4e-8   very suspicious  
  [Low4/16]FPF-14+6/32:all2         R= +23.9  p =  5.2e-11   VERY SUSPICIOUS 
  [Low4/16]FPF-14+6/16:(0,14-0)     R= +13.8  p =  2.2e-12   VERY SUSPICIOUS 
  [Low4/16]FPF-14+6/16:(1,14-0)     R= +10.0  p =  7.3e-9   suspicious       
  [Low4/16]FPF-14+6/16:all          R= +12.1  p =  8.0e-11   VERY SUSPICIOUS 
  [Low4/16]FPF-14+6/16:all2         R= +52.5  p =  1.3e-22    FAIL !!        
  [Low4/16]FPF-14+6/4:(0,14-0)      R= +12.2  p =  7.0e-11   VERY SUSPICIOUS 
  [Low4/16]FPF-14+6/4:all           R=  +7.1  p =  3.7e-6   mildly suspicious
  [Low4/16]FPF-14+6/4:all2          R= +29.8  p =  7.1e-14    FAIL           
  [Low4/16]FPF-14+6/4:cross         R=  +5.3  p =  7.8e-5   unusual          
  [Low4/32]FPF-14+6/32:(0,14-0)     R=  +7.6  p =  1.3e-6   unusual          
  [Low4/32]FPF-14+6/32:all          R=  +6.0  p =  4.4e-5   unusual          
  [Low4/32]FPF-14+6/32:all2         R=  +9.4  p =  2.9e-5   unusual          
  [Low4/32]FPF-14+6/16:(0,14-0)     R=  +7.3  p =  2.5e-6   unusual          
  [Low4/32]FPF-14+6/16:all          R=  +6.5  p =  1.4e-5   mildly suspicious
  [Low4/32]FPF-14+6/16:all2         R=  +8.2  p =  8.0e-5   unusual          
  [Low8/32]FPF-14+6/32:(0,14-0)     R= +17.2  p =  1.7e-15    FAIL           
  [Low8/32]FPF-14+6/32:(1,14-0)     R= +12.7  p =  2.3e-11   VERY SUSPICIOUS 
  [Low8/32]FPF-14+6/32:all          R= +15.3  p =  7.9e-14    FAIL           
  [Low8/32]FPF-14+6/32:all2         R= +86.1  p =  1.2e-35    FAIL !!!       
  [Low8/32]FPF-14+6/16:(0,14-0)     R= +16.8  p =  3.5e-15    FAIL           
  [Low8/32]FPF-14+6/16:(1,14-0)     R= +12.2  p =  6.6e-11   VERY SUSPICIOUS 
  [Low8/32]FPF-14+6/16:all          R= +13.1  p =  8.9e-12   VERY SUSPICIOUS 
  [Low8/32]FPF-14+6/16:all2         R= +82.1  p =  1.7e-34    FAIL !!!       
  [Low8/32]FPF-14+6/4:(0,14-0)      R= +12.8  p =  2.0e-11   VERY SUSPICIOUS 
  [Low8/32]FPF-14+6/4:(1,14-0)      R=  +9.4  p =  2.5e-8   suspicious       
  [Low8/32]FPF-14+6/4:all           R= +10.5  p =  2.2e-9    VERY SUSPICIOUS 
  [Low8/32]FPF-14+6/4:all2          R= +42.0  p =  5.8e-19    FAIL !         
  ...and 1118 test result(s) without anomalies

Resultados com PCG64 DXSM e uma colisão inferior de 64 bits (para provocar problemas mais rapidamente, embora não veja nenhum)

❯ ./pcg64_correlations.py -m 64 --dxsm | stdbuf -oL ./RNG_test stdin64 -tf 2 -te 1 -tlmaxonly -multithreaded
s0 = 0b10001000010110111101010101010101111100100011011111011111011111001011110101111100101101101100110101110001101101111111010101111111
s1 = 0b11000101110100011001011000001110100001001111001001100101010000101100011001010111011001100000010010011100101110001110101000011100
dist = 0x3a26b19c91e6da1d0000000000000000
[
    {
        "bit_generator": "PCG64DXSM",
        "state": {
            "state": 181251833403477538233003277050491434367,
            "inc": 46073632738916603716779705377640239269
        },
        "has_uint32": 0,
        "uinteger": 0
    },
    {
        "bit_generator": "PCG64DXSM",
        "state": {
            "state": 262946148724842088422233355148768897564,
            "inc": 125105549038853892415237434774494719583
        },
        "has_uint32": 0,
        "uinteger": 0
    }
]
RNG_test using PractRand version 0.93
RNG = RNG_stdin64, seed = 0x85cea9
test set = expanded, folding = extra

rng=RNG_stdin64, seed=0x85cea9
length= 128 megabytes (2^27 bytes), time= 2.6 seconds
  no anomalies in 891 test result(s)

rng=RNG_stdin64, seed=0x85cea9
length= 256 megabytes (2^28 bytes), time= 9.4 seconds
  no anomalies in 938 test result(s)

rng=RNG_stdin64, seed=0x85cea9
length= 512 megabytes (2^29 bytes), time= 18.5 seconds
  no anomalies in 985 test result(s)

rng=RNG_stdin64, seed=0x85cea9
length= 1 gigabyte (2^30 bytes), time= 32.3 seconds
  Test Name                         Raw       Processed     Evaluation
  [Low4/32]BCFN_FF(2+3,13-3,T)      R=  -8.3  p =1-9.5e-5   unusual          
  ...and 1035 test result(s) without anomalies

rng=RNG_stdin64, seed=0x85cea9
length= 2 gigabytes (2^31 bytes), time= 55.8 seconds
  no anomalies in 1092 test result(s)

rng=RNG_stdin64, seed=0x85cea9
length= 4 gigabytes (2^32 bytes), time= 93.1 seconds
  no anomalies in 1154 test result(s)

rng=RNG_stdin64, seed=0x85cea9
length= 8 gigabytes (2^33 bytes), time= 175 seconds
  no anomalies in 1222 test result(s)

rng=RNG_stdin64, seed=0x85cea9
length= 16 gigabytes (2^34 bytes), time= 326 seconds
  no anomalies in 1302 test result(s)

rng=RNG_stdin64, seed=0x85cea9
length= 32 gigabytes (2^35 bytes), time= 594 seconds
  no anomalies in 1359 test result(s)

rng=RNG_stdin64, seed=0x85cea9
length= 64 gigabytes (2^36 bytes), time= 1194 seconds
  no anomalies in 1434 test result(s)

rng=RNG_stdin64, seed=0x85cea9
length= 128 gigabytes (2^37 bytes), time= 2334 seconds
  no anomalies in 1506 test result(s)
...

@rkern , obrigado por compartilhar seu código. Seria instrutivo adicionar uma opção para adicionar uma inclinação para alimentar a saída intercalada no testador que não está perfeitamente alinhada. Isso é algo que explorei um pouco.

Sim, fiz isso informalmente inserindo bg0.advance(N) para vários N antes do return final. Usei a colisão inferior de 64 bits para ter certeza de ver algo. Pequenos deslocamentos como 16 não mudam muito a falha, mas mesmo mudanças modestas como 128 prolongam a falha para 32 GiB.

Parece que devemos adicionar PCG64 DXSM como um gerador de bits opcional e, eventualmente, torná-lo o padrão. Para termos uma implementação?

Acho que é útil entender o contexto mais amplo. Sebastiano gosta um pouco do PCG e há anos vem reclamando dele. Acho que hoje em dia algumas pessoas podem olhar para nós dois, revirar os olhos e dizer "vocês são tão ruins um quanto o outro" porque eu também critiquei seus PRNGs, mas na verdade eu só fiz isso depois que ele fez afirmações de que Eu estava tentando esconder algo nunca falando sobre as coisas dele (quando na realidade, eu simplesmente não tinha tempo / inclinação - na verdade, eu simplesmente presumi que eles estavam bem).

Acho que essas considerações são totalmente inadequadas.

Insistir em atacar o trabalho de outras pessoas (por exemplo, SplitMix) sem qualquer mérito e sem nenhuma evidência não vai tornar a bagunça do PCG, ou do gerador do Numpy, melhor. Em vez disso, um multiplicador melhor ou um misturador melhor projetado pode ajudar.

Ainda estou esperando por um teste mostrando correlação no SplitMix quando o usuário pode escolher os fluxos. Só para ficar claro, para o gerador do Numpy, provei uma declaração da forma

∀c ∀d ∀x ∃y correlated

onde c, d são incrementos ("fluxos") e x, y são estados iniciais. Na verdade, existem 2 ^ 72 ys. Ou seja, não importa como você escolha c, d e x, há 2 ^ 72 y mostrando correlação.

O suposto código correspondente que você forneceu para SplitMix mostra que

∃c ∃d ∃x ∃y correlated

Ou seja, escolhendo adversarialmente c, d, x e y, você pode mostrar a correlação.

A diferença de força entre as duas afirmações é impressionante. Tentar fundir as duas afirmações é incorreto.

@vigna, você foi avisado duas vezes sobre nosso código de conduta, por @mattip e @rkern. Usar linguagem como "espancar o trabalho de outras pessoas" e "Tentar fundir as duas afirmações é puro FUD" não está certo. Considere este seu último aviso. Por favor, mude seu tom ou nós _vamos_ banir você. Argumentos técnicos ainda são bem-vindos, qualquer outra coisa não é neste ponto.

Modifiquei a mensagem substituindo essas expressões por outras neutras. Eu ainda acho que atacar pessoalmente outro participante da discussão ("Sebastiano tem um pouco de uma coisa sobre PCG e tem protestado contra isso por anos") é totalmente inapropriado. Estou muito surpreso por não ser para você.

Pela terceira e última vez, a discussão sobre SplitMix, em qualquer direção, não está me ajudando nem um pouco. Posso entender por que você acha que ele fornece o contexto necessário ou que se sente compelido a responder ao outro, mas confie que estou dizendo a verdade que ele não está me fornecendo nenhuma informação que me ajude a tomar uma decisão aqui. Ambos têm seus próprios sites. Usa-os.

Modifiquei a mensagem substituindo essas expressões por outras neutras.

Obrigado.

Eu ainda acho que atacar pessoalmente outro participante da discussão ("Sebastiano tem um pouco de uma coisa sobre PCG e tem protestado contra isso por anos") é totalmente inapropriado. Estou muito surpreso por não ser para você.

Eu prefiro não ver isso também. No entanto, o tom da mensagem não é tão ruim.

Eu apreciaria muito se vocês dois pudessem se limitar a declarações factuais construtivas

ESTÁ BEM. Vamos começar do zero: você quer um gerador com algum tipo de fluxo baseado em LCG com módulo de potência de 2 para conveniência e velocidade. A literatura sugere que basear fluxos em constantes aditivas LCG pode levar você a problemas (como acontece agora), mas vamos assumir que é isso que você deseja.

Por que não pegar um LCG com 128 bits de estado e um bom multiplicador (pelo menos 65 bits) e perturbar os bits superiores usando a função mix de SplitMix, que foi fortemente testada em diferentes aplicações (hashing, PRNG, etc.), dando excelentes resultados?

Tenho certeza de que a diferença na velocidade será marginal. E você tem alguma garantia (estatística) de que o resultado dependerá de todos os bits, que é o problema aqui.

Isso me parece mais uma abordagem de "pisar no ombro de gigantes" do que funções de mixagem artesanais em um gerador que tem problemas de autocorrelação.

@imneme O que eu poderia usar é uma postagem de blog sobre DXSM que seja mais fácil de vincular do que este comentário de anúncio no antigo mega problema. Não precisa ser muito mais do que aquele comentário, mas incluir o status atual dos testes que você mencionou aqui seria bom. Se você quisesse resumir um pouco da discussão do mega-problema que levou a esse desenvolvimento, isso seria útil, com certeza, mas não totalmente necessário.

@vigna

Por que não pegar um LCG com 128 bits de estado e um bom multiplicador (pelo menos 65 bits) e perturbar os bits superiores usando a função mix de SplitMix, que foi fortemente testada em diferentes aplicações (hashing, PRNG, etc.), dando excelentes resultados?

Peço desculpas se isso parece sarcástico (embora certamente seja apontado), mas também é sincero: estou ansioso para ver a implementação, análise, benchmarks e resultados PractRand em seu site ou no arXiv. Somos profissionais (razoavelmente informados) aqui, não pesquisadores do PRNG, e não estamos particularmente bem equipados para levar a cabo esta sugestão. Posso ver o sentido disso, mas dadas as outras restrições do meu tempo pessoal, não tenho a tendência de despender o esforço para levar isso da sugestão para uma implementação e análise. Se você está direcionando esta sugestão para o numpy, precisamos de pesquisadores do PRNG para fazer esse trabalho. Se você está realmente dirigindo essa sugestão para outra pessoa, use seu site.

A arquitetura aleatória Generator -> BitGenerator -> SeedSequence em NumPy deve ser conectável. Acho que chegamos ao ponto da discussão em que precisamos que alguém abra um PR para um BitGenerator, para que possamos comparar seus atributos práticos com os atualmente em NumPy. Assim que se tornar parte do projeto, podemos continuar a testá-lo e decidir torná-lo o padrão. Essa decisão seria, espero, baseada em

  • falta de parcialidade (e outros critérios? Eu cedo aos especialistas)
  • desempenho
  • probabilidade de vários tipos de colisão de fluxo através das interfaces normativas que promovemos: usando BitGenerator.spawn e SeedSequence .

Pessoalmente, esta discussão me perdeu quando evitou discutir os méritos de BitGenerators por meio do código que usa a interface spawn . Há uma razão para promovê-lo como melhor prática, e espero que a discussão do futuro PR enfoque nas melhores práticas para usuários do NumPy .

Talvez uma das conclusões aqui seja que devemos permitir apenas spawn como método, visto que usar jumped ou advance pode entrar em conflito com as boas práticas. Um novo problema ou NEP focado nisso também pode ser produtivo.

@mattip A colisão de aniversário de bits inferiores que @vigna notou afeta nossa interface SeedSequence.spawn() também. Tenha certeza de que qualquer parte da discussão na qual me envolvi é relevante para o uso adequado de nossas APIs.

Requer apenas a adição de cerca de 8 linhas ao pcg64.c com alguns blocos #ifdef para usar a abordagem preferida do @rkern de geradores completamente separados. O pyx / pxd seria de outra forma idêntico à classe PCG64 apenas sendo construído com as definições corretas (PCG_DXSM = 1) e uma docstring atualizada.

Eu provavelmente seria mais explícito sobre isso, especialmente para a matemática de 128 bits emulada para as plataformas que precisam disso.

https://github.com/rkern/numpy/compare/v1.17.4...rkern%3Awip/pcg64-dxsm

Pareceu-me mais fácil do que isso, já que usa um multiplicador "barato" de 64 bits. Você pode simplesmente adicionar um novo mixer de saída (que é invariante) e então ifdef em torno da linha final do gerador aleatório que pega a saída do LCG e então aplica o mixer.

https://github.com/bashtage/randomgen/commit/63e50a63f386b5fd725025f2199ff89454321f4c#diff -879bd64ee1e2b88fec97b5315cf77be1R115

Se alguém quisesse, você poderia até adicionar o Murmur Hash 3 neste ponto, se alguém quisesse.

As instruções if serão compiladas imediatamente? Não acho que queremos múltiplos deles dentro de hot loops.

Novamente, isso se resume à diferença de propósito entre randomgen e numpy . Faz sentido em randomgen fazer famílias parametrizadas, mas em numpy , não acho que seja uma boa ideia enredar as implementações de um legado BitGenerator do padrão ativo BitGenerator . Se tivermos que fazer qualquer manutenção ou refatoração para desempenho em um ou outro, isso só tornará o esforço pior do que melhor.

Concordo com Robert aqui. Não tenho dúvidas em colocar um novo gerador de bits na versão 1.19.0, ele não mudaria nenhum comportamento atual.

@bashtage Além disso, observe que pcg_cm_random_r() usa o estado pré-iterado para a saída em vez do estado pós-iterado , então não será tão simples manter o mesmo codepath com #ifdef ou if switches.

As instruções if serão compiladas imediatamente? Não acho que queremos múltiplos deles dentro de hot loops.

Não, em NumPy o if else deve se tornar algo como

#if defined(PCG_DXSM)
    pcg_output_dxsm(state.high, state.low)
#else 
   <old way>
#endif

Estes precisam ser definidos separadamente na versão uint128 e na versão fallback para lidar com a mudança manual do uint128 para alto e baixo.

@bashtage Além disso, observe que pcg_cm_random_r() usa o estado pré-iterado para a saída em vez do estado pós-iterado , então não será tão simples manter o mesmo codepath com #ifdef ou if switches.

Hmm, eu testei contra a implementação de referência @imneme e obtive uma correspondência de 100% em 1000 valores usando 2 sementes distintas:

https://github.com/bashtage/randomgen/blob/master/randomgen/src/pcg64/pcg_dxsm-test-data-gen.cpp

AFAICT (e posso estar errado)

https://github.com/imneme/pcg-cpp/blob/master/include/pcg_random.hpp#L174

e

https://github.com/imneme/pcg-cpp/blob/master/include/pcg_random.hpp#L1045

significa que o caminho uint_128 está sempre usando um multiplicador barato.

Não tenho certeza do que você está tentando dizer aqui.

Não está claro o que é o PCG64 DXSM canônico. Em ambos os casos, a função de saída usa apenas operações de 64 bits. A versão que você tem usa um multiplicador de 64 bits em outro local para ser ainda mais rápido e retorna pré, ao invés de pós. setseq_dxsm_128_64 parece ser a extensão natural do PCG64 existente e apenas altera a função de saída.

Ah eu vejo. Não, você usou um gerador C ++ diferente daquele que implementei em C. Eu implementei o equivalente a cm_setseq_dxsm_128_64 que usa o "multiplicador barato" na iteração LCG, não setseq_dxsm_128_64 que ainda usa o grande multiplicador na iteração LCG. O "multiplicador barato" é reutilizado dentro da função de saída DXSM, mas isso é um eixo de design ortogonal.

Por que não preferir setseq_dxsm_128_64?

@imneme disse que eventualmente mudaria o pcg64 oficial na versão C ++ para apontar para cm_setseq_dxsm_128_64 , não setseq_dxsm_128_64 . O multiplicador barato compensa parte do custo extra do DXSM em comparação com o XSL-RR. E acho que é a variante que ela passou alguns meses testando.

A saída do estado pré-iterado também faz parte do aumento de desempenho .

Aqui estão alguns horários:

In [4]: %timeit p.random_raw(1000000)
3.24 ms ± 4.61 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

In [5]: p = rg.PCG64(mode="sequence",use_dxsm=False)

In [6]: %timeit p.random_raw(1000000)
3.04 ms ± 8.47 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

In [7]: import numpy as np

In [8]: p = np.random.PCG64()

In [9]: %timeit p.random_raw(1000000)
3.03 ms ± 2.54 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

Tudo no Ubuntu-20.04, compilador padrão.

6% mais lento. Parece uma pequena diferença para mim. Todos os tempos em NumPy / randomgen estão muito distantes do que você pode obter em um loop real de código nativo.

Comparar

In [10]: x = rg.Xoroshiro128(mode="sequence")

In [11]: %timeit x.random_raw(1000000)
2.59 ms ± 35.7 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

ao que foi publicado a partir do código C (150% mais lento ??).

Concedido, no contexto de numpy , isso não importa muito. É mais importante no contexto C ++, o que levou à alocação de meses de cluster para teste e a nomeação como o futuro padrão pcg64 no código C ++ oficial. Essas são as motivações imediatas de numpy , IMO.

A diferença entre o estoque PCG64 e meu PCG64DXSM em meu branch ("multiplicador barato", função de saída DXSM, saída de estado pré-iterado, codepath separado):

[practrand]
|1> s = np.random.SeedSequence()

[practrand]
|2> pcg64 = np.random.PCG64(s)

[practrand]
|3> pcg64dxsm = np.random.PCG64DXSM(s)

[practrand]
|4> %timeit pcg64.random_raw(1000000)
100 loops, best of 3: 3.46 ms per loop

[practrand]
|5> %timeit pcg64dxsm.random_raw(1000000)
100 loops, best of 3: 2.9 ms per loop

Ainda digo que são apenas alguns (mais do que eu) #ifdefs entre os dois, mesmo com uma implementação especializada para MSVC. Além disso, os usuários do RSN MS poderão usar o clang # 13816 👍.

Estamos discutindo sobre a duplicação de código? Eu prefiro ter implementações desconexas do que me preocupar com algumas linhas de código ofuscadas com #ifdefs :)

Foi principalmente uma piada, embora tenha destacado a necessidade de uma declaração absolutamente clara definindo "PCG 2.0" (de preferência em algum lugar que não seja o problema do NumPy com o GitHub).

Obrigado, @rkern et al.

@imneme O que eu poderia usar é uma postagem de blog sobre DXSM que seja mais fácil de vincular do que este comentário de anúncio no antigo mega problema. Não precisa ser muito mais do que aquele comentário, mas incluir o status atual dos testes que você mencionou aqui seria bom. Se você quisesse resumir um pouco da discussão do mega-problema que levou a esse desenvolvimento, isso seria útil, com certeza, mas não totalmente necessário.

Você tem um prazo em mente? Eu tenho pretendido fazer isso há algum tempo e ser empurrado por outras coisas, então ter um prazo proposto seria um motivador útil para mim. Uma semana talvez? Dois?

@rkern também citou @vigna , que escreveu:

Por que não pegar um LCG com 128 bits de estado e um bom multiplicador (pelo menos 65 bits) e perturbar os bits superiores usando a função mix de SplitMix, que foi fortemente testada em diferentes aplicações (hashing, PRNG, etc.), dando excelentes resultados?

FWIW, essa abordagem foi discutida no documento PCG original, usando _FastHash_ como a função hash pronta para uso, que é uma função hash multiply-xorshift muito semelhante. Em meus testes, não foi tão rápido quanto outras permutações, mas foi de alta qualidade. Sebastiano também mencionou essa ideia em sua crítica de 2018 à PCG e eu a discuto nesta seção de minha resposta a essa crítica.

Na versão original de sua crítica ao PCG, ele termina escrevendo sua própria variante do PCG, que citarei abaixo:

        #include <stdint.h>

        __uint128_t x;

        uint64_t inline next(void) {
            // Put in z the top bits of state
            uint64_t z = x >> 64;
            // Update state
            x = x * ((__uint128_t)0x2360ed051fc65da4 << 64 ^ 0x4385df649fccf645)
                  + ((__uint128_t)0x5851f42d4c957f2d << 64 ^ 0x14057b7ef767814f);
            // Compute mix
            z = (z ^ (z >> 30)) * 0xbf58476d1ce4e5b9;
            z = (z ^ (z >> 27)) * 0x94d049bb133111eb;
            return z ^ (z >> 31);
        }

Desde então, ele atualizou o código em sua crítica para uma versão ainda mais rápida que usa um multiplicador mais barato e reduziu a constante aditiva para apenas 64 bits,

        #include <stdint.h>

        __uint128_t x;

        uint64_t inline next(void) {
            // Put in z the top bits of state
            uint64_t z = x >> 64;
            // Update state (multiplier from https://arxiv.org/abs/2001.05304)
            x = x * ((__uint128_t)1 << 64 ^ 0xd605bbb58c8abbfd) + 0x14057b7ef767814f;
            // Compute mix
            z = (z ^ (z >> 30)) * 0xbf58476d1ce4e5b9;
            z = (z ^ (z >> 27)) * 0x94d049bb133111eb;
            return z ^ (z >> 31);
        }

Meu problema com essas duas variantes é que a permutação é invertível porque o estado truncado (a metade superior) é permutado / embaralhado - você pode executá-lo para trás e desembaralhar o embaralhamento, deixando-o com um mero LCG truncado com todas as falhas inerentes nele. Minha preferência é permutar / escalonar todo o estado e, em seguida, gerar um truncamento desse estado. (Claro, permutar / embaralhar menos bits será a maneira mais rápida - como de costume, há trocas. Pessoas razoáveis ​​podem discordar sobre o que é importante.)

Mas seu trabalho fazendo sua própria variante PCG forneceu uma inspiração muito útil para a permutação DXSM quando eu escrevi isso no ano passado.

@charris Qual é o seu apetite para tornar a implementação PCG64DXSM disponível (mas ainda não padrão) em 1.19.0? Qual é a linha do tempo? Vejo que já lançamos o 1.19.0rc2, o que não é _grande_ para a introdução de um novo recurso. Mais uma vez, não estou incerto sobre esse assunto. Eu me inclinaria para o lançamento do 1.19.0 apenas documentando nossa política sobre mudanças em default_rng() e a introdução de novos recursos no 1.20.0.

@rkern O rc final precisa estar disponível por> duas semanas, então estamos olhando para um lançamento na segunda metade de junho. Sou a favor de colocar o PCG64DXSM como uma escolha opcional se facilitar os testes, não o considero um novo recurso, mais como um novo acessório. E às vezes ajuda a movimentar as coisas ter um código real funcionando. NumPy é um criador de tendências :)

EDIT: Supondo, é claro, que não haja grandes problemas com o novo código, e parece que não há. Também não estou muito preocupado com problemas com o PCG64, parece improvável que alguém tenha problemas ao usar nossos procedimentos recomendados.

@imneme Uma semana seria ótimo. Duas semanas estariam bem. Obrigado!

Há uma pergunta que tenho feito a mim mesmo que está um pouco fora do assunto. Queremos que nossos geradores de bits produzam bits aleatórios, mas AFAICT, a maioria dos testes envolve inteiros. Quão bem os testes existentes se saem em realmente testar os bits? Se alguém mais familiarizado com a área pudesse responder a essa pergunta, eu ficaria muito grato.

O que está sendo testado é o fluxo de bits. Dizemos ao software de teste o tamanho natural da palavra do PRNG que estamos gerando, mas apenas para que ele possa fazer as melhores dobras dos bits para provocar de forma mais eficiente erros que tendem a surgir nos bits baixos ou altos do palavra em PRNGs ruins. O software que todos nós tendemos a usar hoje em dia é o PractRand, e seus testes são levemente documentados aqui . O melhor papel para ler é provavelmente o do TestU01 , o conjunto de testes padrão ouro anterior. Seu Guia do Usuário contém mais detalhes sobre os testes.

Peço desculpas se isso parece sarcástico (embora certamente seja apontado), mas também é sincero: estou ansioso para ver a implementação, análise, benchmarks e resultados PractRand em seu site ou no arXiv. Somos profissionais (razoavelmente informados) aqui, não pesquisadores do PRNG, e não estamos particularmente bem equipados para levar a cabo esta sugestão. Posso ver o sentido disso, mas dadas as outras restrições do meu tempo pessoal, não tenho a tendência de despender o esforço para levar isso da sugestão para uma implementação e análise. Se você está direcionando esta sugestão para o numpy, precisamos de pesquisadores do PRNG para fazer esse trabalho. Se você está realmente dirigindo essa sugestão para outra pessoa, use seu site.

Posso entender perfeitamente seu ponto de vista. O código e o benchmark estão na parte inferior da página, comentando os problemas do PCG (http://prng.di.unimi.it/pcg.php) há alguns anos com o nome LCG128Mix. Leva 2,16 ns no meu hardware, uma CPU Intel (R) Core (TM) i7-7700 a 3,60 GHz, com gcc 9.2.1 e -fno-move-loop-invariants -fno-unroll-loops.

O código é muito simples - ele combina um LCG padrão com uma função de mixagem padrão (finalizador MurmurHash3 aprimorado de Stafford). Eu o modifiquei ligeiramente para ter uma constante programável:

    #include <stdint.h>
    __uint128_t x; // state
    __uint64_t c;  // stream constant (odd)

    uint64_t inline next(void) {
        // Put in z the top bits of state
        uint64_t z = x >> 64;
        // Update LCG state
        x = x * ((__uint128_t)1 << 64 ^ 0xd605bbb58c8abbfd) + c;
        // Compute mix
        z = (z ^ (z >> 30)) * 0xbf58476d1ce4e5b9;
        z = (z ^ (z >> 27)) * 0x94d049bb133111eb;
        return z ^ (z >> 31);
    }

Como expliquei antes, a constante de 65 bits é muito melhor do que qualquer constante de 64 bits como um multiplicador, devido a problemas teóricos de multiplicadores menores do que a raiz quadrada do módulo.

Se você estiver interessado em um design mais baseado em princípios, executarei testes PractRand. Você deve levar em consideração, entretanto, que esta função de mixagem rendeu um excelente gerador, SplitMix, mesmo com um gerador subjacente muito mais fraco (era apenas aditivo), e com estado menor (64 bits). Portanto, será apenas "melhor" do que SplitMix, que ultrapassa o PractRand em 32 TB.

E o gerador subjacente é um LCG, então você tem todos os sinos e assobios usuais dos anos 60: saltos, distâncias, etc. Mas também você tem uma garantia estatística de que cada bit do resultado depende de cada bit dos 64 bits superiores de Estado.

Se você tem em mente benchmarks contra outros geradores, ou usando outros compiladores, por favor me avise.

Mas, por favor, da mesma forma sincera: somente se você estiver realmente interessado em considerar um projeto "sobre os ombros de gigantes" usando apenas componentes padrão. Também há limitações pessoais no meu tempo e fico feliz em contribuir, mas gostaria de evitar perder tempo com um gerador que não tem chance de ser considerado.

BTW, para dar uma medida mais tangível de como melhorar a qualidade dos multiplicadores envolvidos, calculei as pontuações espectrais, de f₂ a f₈, do atual multiplicador de 64 bits usado pelo PCG DXS e alguma alternativa.

Pontuações espectrais são a forma padrão de julgar a bondade de um multiplicador. 0 é ruim, 1 é excelente. Cada pontuação descreve o quão bem estão distribuídos os pares, triplos, 4 tuplas, etc. na saída.

Esses sete números podem ser resumidos na medida clássica, no mínimo, ou em uma medida ponderada (a primeira pontuação, mais a segunda dividida por dois, etc., normalizada), para tornar as primeiras pontuações mais importantes, conforme sugerido por Knuth no TAoCP , e estes são o mínimo e a medida ponderada para o multiplicador atual:

0xda942042e4dd58b  0.633  0.778

Existem constantes de 64 bits muito melhores do que isso:

0xff37f1f758180525  0.761  0.875

Se você for para 65 bits, essencialmente na mesma velocidade (pelo menos, para LCG128Mix é a mesma velocidade), você obtém uma medida ponderada melhor:

0x1d605bbb58c8abbfd  0.761  0.899

O motivo é que os multiplicadores de 64 bits têm um limite intrínseco para sua pontuação f₂ (≤0,93), que, conforme observado por Knuth, é o mais relevante:

0xda942042e4dd58b5  0.795
0xff37f1f758180525  0.928
0x1d605bbb58c8abbfd  0.992

Portanto, o primeiro multiplicador tem uma pontuação f₂ medíocre. O segundo multiplicador chega muito perto do ótimo para um multiplicador de 64 bits. O multiplicador de 65 bits não tem essas limitações e tem uma pontuação muito próxima de 1, a melhor possível em geral.

Para completar, aqui estão todas as pontuações:

 0xda942042e4dd58b5  0.794572 0.809219 0.911528 0.730396 0.678620 0.632688 0.639625
 0xff37f1f758180525  0.927764 0.913983 0.828210 0.864840 0.775314 0.761406 0.763689 
0x1d605bbb58c8abbfd  0.991889 0.907938 0.830964 0.837980 0.780378 0.797464 0.761493

Você pode recalcular essa pontuação ou procurar seu próprio multiplicador com o código que Guy Steele e eu distribuímos: https://github.com/vigna/CPRNG . Os melhores multiplicadores são retirados do papel associado.

PCG provavelmente será um ótimo prng padrão para numpy, mas não acho que vai resistir ao teste do tempo, pois existem maneiras mais promissoras, mas menos testadas de fazer isso. Eu proponho um no seguinte.

O meio caótico SFC64 é um dos geradores de som estatisticamente mais rápidos, com um período mínimo grande razoável. SFC64 não tem funções de salto, mas pode _sem sobrecarga de velocidade ser estendido para suportar 2 ^ 63 streams únicos garantidos_. Basta adicionar uma sequência de Weyl com uma constante aditiva k escolhida pelo usuário (deve ser ímpar), em vez de apenas incrementar o contador em um. Cada k ímpar produz um período completo exclusivo. Requer 64 bits adicionais de estado para manter a constante de Weyl:

typedef struct {uint64_t a, b, c, w, k;} sfcw64_t; // k = stream

static inline uint64_t sfcw64_next(sfcw64_t* s) {
    enum {LROT = 24, RSHIFT = 11, LSHIFT = 3};
    const uint64_t out = s->a + s->b + (s->w += s->k);
    s->a = s->b ^ (s->b >> RSHIFT);
    s->b = s->c + (s->c << LSHIFT);
    s->c = ((s->c << LROT) | (s->c >> (64 - LROT))) + out;
    return out;
}

Um estado de 320 bits às vezes é indesejável, então tentei reduzi-lo para usar 256 bits novamente. Observe também a função de saída alterada, que utiliza melhor a sequência de Weyl para a mixagem de bits. Ele usa um estado caótico / estruturado de 128/128 bits, que parece atingir um bom equilíbrio:
/ EDIT: removido rotl64 () da saída func + cleanup, 6 de agosto:

typedef struct {uint64_t a, b, w, k;} tylo64_t;

static inline uint64_t tylo64_next(tylo64_t* s) {
    enum {LROT = 24, RSHIFT = 11, LSHIFT = 3};
    const uint64_t b = s->b, out = s->a ^ (s->w += s->k);
    s->a = (b + (b << LSHIFT)) ^ (b >> RSHIFT);
    s->b = ((b << LROT) | (b >> (64 - LROT))) + out;
    return out;
}

Isso atualmente passou 4 TB no teste PractRand sem anomalias, e eu rapidamente executei o teste de peso Hamming de Vigna sem problemas até agora (embora passar nesses testes não seja garantia de saída quase aleatória, em vez de um teste se o prng é defeituoso ou não )

Nota: supostamente, é estatisticamente uma vantagem usar uma constante de Weyl aleatória (única) com aproximadamente 50% dos bits definidos, mas apenas testes ou análises posteriores revelarão o quão significativo isso é.

/ Edits: limpezas.

@ tylo-work SFC64 já está no NumPy, junto com o Philox, trata-se do gerador padrão.

Ok, eu não sabia exatamente quais foram implementados, então isso é apenas sobre como selecionar o mais adequado em geral? Muito justo, e obrigado por esclarecer.

Vou tentar testar meu gerador proposto extensivamente para ver como ele se compara com os outros, e até agora ele parece muito bom em relação à velocidade, qualidade de saída, simplicidade / tamanho / portabilidade e para uso paralelo massivo. Mas eu ficaria feliz se outros testassem também.

Não acho que estamos reabrindo a discussão sobre o PRNG padrão do zero. Temos um problema muito específico com nosso PRNG atual e estamos analisando as variantes disponíveis e intimamente relacionadas que tratam desse problema específico. Uma de nossas preocupações é que o PRNG padrão atual expõe certos recursos do PRNG, como capacidade de salto, que a variante que o substitui ainda deve expor. O SFC64 (nosso ou seu) não tem esse recurso.

É possível que @bashtage esteja disposto a aceitar um PR para randomgen para adicionar suas variantes Weyl-stream de SFC64.

@ tylo-work Se você estiver interessado na execução paralela, talvez queira dar uma olhada na implementação SeedSequence do NumPy.

Não acho que estamos reabrindo a discussão sobre o PRNG padrão do zero. Temos um problema muito específico com nosso PRNG atual e estamos analisando as variantes disponíveis e intimamente relacionadas que tratam desse problema específico.

Supondo que você queira algo parecido com o PCG-DXS, existem outras melhorias que você pode fazer apenas com constantes melhores (e uma desaceleração muito marginal). Por exemplo, o PCG-DXS logo falhará em dois tipos distintos de testes em duas subseqüências correlacionadas intercaladas com os mesmos 112 bits de estado inferiores:

rng=PCGDXS_int112, seed=0x4d198651
length= 128 gigabytes (2^37 bytes), time= 5700 seconds
  Test Name                         Raw       Processed     Evaluation
  [Low1/64]TMFn(0+2):wl             R= +57.3  p~=   2e-27     FAIL !!
  [Low8/64]FPF-14+6/64:(1,14-0)     R= +17.5  p =  8.0e-16    FAIL
  [other failures in the same tests]
  ...and 1893 test result(s) without anomalies

Observe que estamos falando de apenas ≈65536 sequências correlacionadas - nada a temer.

Mas você pode melhorar o gerador escolhendo um multiplicador melhor, como 0x1d605bbb58c8abbfd, e um misturador melhor, como 0x9e3779b97f4a7c15. O primeiro número é um multiplicador de 65 bits que possui pontuações espectrais muito melhores. O segundo número é apenas a proporção áurea em uma representação de ponto fixo de 64 bits, e ele é conhecido por ter boas propriedades de mistura (consulte Knuth TAoCP sobre hash multiplicativo); por exemplo, ele é usado pela biblioteca Eclipse Collections para misturar códigos hash.

Como resultado, você falha apenas no FPF para a mesma quantidade de dados:

rng=PCG65-DXSϕ_int112, seed=0x4d198651
length= 128 gigabytes (2^37 bytes), time= 5014 seconds
  Test Name                         Raw       Processed     Evaluation
  [Low8/64]FPF-14+6/64:(0,14-0)     R= +16.1  p =  1.5e-14    FAIL
  [other failures in the same test]
  ...and 1892 test result(s) without anomalies

Na verdade, se formos mais longe em 2 TB, o PCG-DXS falha _três_ tipos de testes para as mesmas subseqüências correlacionadas intercaladas:

rng=PCGDXS_int112, seed=0x4d198651
length= 2 terabytes (2^41 bytes), time= 53962 seconds
  Test Name                         Raw       Processed     Evaluation
  [Low1/32]TMFn(0+0):wl             R= +50.2  p~=   4e-23     FAIL !!
  [Low8/64]FPF-14+6/64:(1,14-0)     R=+291.1  p =  4.7e-269   FAIL !!!!!!
  [Low8/64]Gap-16:B                 R= +19.5  p =  1.4e-16    FAIL !
  [other failures in the same tests]
  ...and 2153 test result(s) without anomalies

enquanto PCG65-DXSϕ ainda falha apenas FPF:

rng=PCGDXS65ϕ_int112, seed=0x4d198651
length= 2 terabytes (2^41 bytes), time= 55280 seconds
  Test Name                         Raw       Processed     Evaluation
  [Low8/64]FPF-14+6/64:(0,14-0)     R=+232.1  p =  2.0e-214   FAIL !!!!!!
  [other failures in the same test]
  ...and 2153 test result(s) without anomalies

Cedo ou tarde, é claro, também o PCG65-DXSϕ irá falhar no Gap e no TMFn. Mas você precisa ver muito mais saída do que com PCG-DXS.

Este é o código completo para PCG65-DXSϕ, que é apenas PCG-DXS com melhores constantes:

#include <stdint.h>

__uint128_t x; // State
uint64_t c; // Additive constant

static inline uint64_t output(__uint128_t internal) {
    uint64_t hi = internal >> 64;
    uint64_t lo = internal;

    lo |= 1;
    hi ^= hi >> 32;
    hi *= 0x9e3779b97f4a7c15;
    hi ^= hi >> 48;
    hi *= lo;
    return hi;
}

static uint64_t inline next(void) {
    __uint128_t old_x = x;
    x = x *  ((__uint128_t)1 << 64 ^ 0xd605bbb58c8abbfd) + c;
    return output(old_x);
}

A desaceleração marginal é devido a uma instrução add (causada pelo multiplicador de 65 bits) e tendo duas constantes de 64 bits para carregar.

Não estou endossando geradores desse tipo em geral, mas o PCG65-DXSϕ é mensuravelmente melhor do que o PCG-DXS para ocultar a correlação.

@Vigna , FYI, eu também fiz alguns testes de intercalação e percebi que xoshiro256 ** falhou rapidamente ao criar 128 streams intercalados ou mais. Com 256 ele falhou rapidamente. O objetivo do teste é verificar como os PRNGs se comportam quando cada fluxo foi inicializado com algumas dependências lineares. Essencialmente, o estado é inicializado para s[0]=s[1]=s[2]=s[3] = k1 + stream*k2 . Em seguida, 12 saídas são ignoradas, que é basicamente como o sfc64 é inicializado.

Sei que esta não é a inicialização recomendada para xoshiro, mas ainda é interessante - e um pouco preocupante - que os testes pareciam bons para xoshiro com poucos fluxos intercalados, mas falhou com muitos.

seed: 1591888413
RNG_test using PractRand version 0.95
RNG = RNG_stdin64, seed = unknown
test set = core, folding = standard (64 bit)
...
rng=RNG_stdin64, seed=unknown
length= 2 gigabytes (2^31 bytes), time= 29.6 seconds
  Test Name                         Raw       Processed     Evaluation
  [Low1/64]FPF-14+6/16:(1,14-1)     R=  +7.2  p =  3.7e-6   unusual
  [Low1/64]FPF-14+6/16:all          R=  +9.6  p =  1.8e-8   very suspicious
  ...and 261 test result(s) without anomalies

rng=RNG_stdin64, seed=unknown
length= 4 gigabytes (2^32 bytes), time= 55.5 seconds
  Test Name                         Raw       Processed     Evaluation
  [Low1/64]FPF-14+6/16:(0,14-0)     R= +13.4  p =  4.7e-12   VERY SUSPICIOUS
  [Low1/64]FPF-14+6/16:(1,14-0)     R=  +9.4  p =  2.6e-8   suspicious
  [Low1/64]FPF-14+6/16:(2,14-1)     R=  +7.7  p =  1.3e-6   unusual
  [Low1/64]FPF-14+6/16:all          R= +17.4  p =  8.8e-16    FAIL !
  ...and 275 test result(s) without anomalies

Também tentei enfraquecer a inicialização do SFC64 e do TYLO64 para pular apenas 2 saídas, mas elas ainda pareciam OK.
No desempenho: xoshiro256 ** é executado 33% mais lento na minha máquina do que os outros dois. TYLO64 atualiza apenas 196 bits de vars de estado. Aqui está o programa de teste:

int main()
{
    //FILE* f = freopen(NULL, "wb", stdout);  // Only necessary on Windows, but harmless.
    enum {THREADS = 256};
    uint64_t seed = 1591888413; // <- e.g. this fails. // (uint64_t) time(NULL); 
    fprintf(stderr, "seed: %lu\n", seed);

    static tylo64_t tyl[THREADS];
    static sfc64_t sfc[THREADS];
    static uint64_t xo[THREADS][4];

    for (size_t i = 0; i < THREADS; ++i) {
    tyl[i] = tylo64_seed(seed + (12839732 * i), 19287319823 * i);
    sfc[i] = sfc64_seed(seed + (12839732 * i));
    xo[i][0] = xo[i][1] = xo[i][2] = xo[i][3] = seed + (12839732 * i);
    for (int j=0; j<12; ++j) xoshiro256starstar_rand(xo[i]);
    }
    static uint64_t buffer[THREADS];
    size_t n = 1024 * 1024 * 256 / THREADS;

    while (1/*n--*/) {
        for (int i=0; i<THREADS; ++i) {
        //buffer[i] = tylo64_rand(&tyl[i]);
        //buffer[i] = sfc64_rand(&sfc[i]);
            buffer[i] = xoshiro256starstar_rand(xo[i]);
        }
        fwrite((void*) buffer, sizeof(buffer[0]), THREADS, stdout);
    }
    return 0;
}

Vou incluir alguns códigos de cabeçalho relevantes:

typedef struct {uint64_t a, b, w, k;} tylo64_t; // k = stream

static inline uint64_t tylo64_rand(tylo64_t* s) {
    enum {LROT = 24, RSHIFT = 11, LSHIFT = 3};
    const uint64_t b = s->b, w = s->w, out = (s->a + w) ^ (s->w += s->k);
    s->a = (b + (b << LSHIFT)) ^ (b >> RSHIFT);
    s->b = ((b << LROT) | (b >> (64 - LROT))) + out;
    return out;
}

/* stream in range [0, 2^63) */
static inline tylo64_t tylo64_seed(const uint64_t seed, const uint64_t stream) {
    tylo64_t state = {seed, seed, seed, (stream << 1) | 1};
    for (int i = 0; i < 12; ++i) tylo64_rand(&state);
    return state;
}

static inline uint64_t rotl(const uint64_t x, int k) {
    return (x << k) | (x >> (64 - k));
}
static inline uint64_t xoshiro256starstar_rand(uint64_t* s) {
    const uint64_t result = rotl(s[1] * 5, 7) * 9;
    const uint64_t t = s[1] << 17;
    s[2] ^= s[0];
    s[3] ^= s[1];
    s[1] ^= s[2];
    s[0] ^= s[3];
    s[2] ^= t;
    s[3] = rotl(s[3], 45);
    return result;
}

@ tylo-work Agradeço a análise, mas realmente preciso desse problema para manter o foco. Se você gostaria de continuar essa linha de discussão, eu encorajo você a postar seu trabalho em seu próprio repositório Github, e fazer mais uma postagem aqui convidando as pessoas aqui para isso. Todos os outros, por favor, respondam lá. Obrigado pela sua cooperação.

@imneme @rkern O tempo para o lançamento 1.19 está se esgotando.

@rkern Parece que PCG64DXSM não chegará ao 1.19.0, estarei lançando neste fim de semana. Se você pudesse escrever uma nota sobre nossa política de mudança / futuras mudanças que mencionou acima, eu agradeceria.

Desculpe, tenho lidado com alguns outros assuntos não relacionados. Com base em nossa discussão, não acho que um pequeno atraso seja um grande problema, já que PCG64DXSM foi planejado como uma opção alternativa, não um novo padrão (por enquanto, pelo menos).

Agora que o 1.20 está inicializando, é hora de revisitar isso e passar para o DXSM?

Ainda teríamos algum tempo para fazer a mudança antes de ramificar, mas pode ser bom começar na próxima semana ou depois. @bashtage Acho que você tem PCG64DXSM pronto para usar e isso precisa principalmente da decisão de ativar o fluxo padrão?

Pelo que viu, parecia que deveríamos fazer isso por 1,20 se o tivermos disponível.

IIRC, estávamos esperando por uma referência que pudesse ser vinculada. Mas se o número aleatório as pessoas estão felizes com a mudança, devemos usá-lo. Precisamos de algum código especial para o Windows?

É apenas uma constante diferente e uma função de embaralhamento diferente. Nada mais novo do que o que @rkern escreveu para a implementação original do PCG64 no Windows. Acho que a decisão foi ter um PCG64DXSM totalmente autônomo, em vez de compartilhar algum código (para desempenho).

Provavelmente faria sentido começar a partir do branch WIP do rkern .

Eu disse que escreveria um post sobre isso no blog, o que acho que @rkern queria, mas tenho @rkern pode ter gostado de uma permutação de saída ainda mais forte, mas fazer isso custa mais velocidade ou (se você cortar atalhos para ganhar velocidade) adiciona previsibilidade trivial.

Esta página foi útil?
0 / 5 - 0 avaliações

Questões relacionadas

perezpaya picture perezpaya  ·  4Comentários

keithbriggs picture keithbriggs  ·  3Comentários

inducer picture inducer  ·  3Comentários

Kreol64 picture Kreol64  ·  3Comentários

amuresan picture amuresan  ·  4Comentários