Em meu trabalho recente com o Gibbs sampling
, tenho feito grande uso do RVar
que, na minha opinião, fornece uma interface quase ideal para geração de números aleatórios. Infelizmente, não consegui usar Repa devido à impossibilidade de usar ações monádicas em mapas.
Embora mapas claramente monádicos não possam ser paralelizados em geral, parece-me que RVar
pode ser pelo menos um exemplo de uma mônada onde os efeitos podem ser paralelizados com segurança (pelo menos em princípio; não estou terrivelmente familiarizado com o funcionamento interno de RVar
) . Ou seja, quero escrever algo como o seguinte,
drawClass :: Sample -> RVar Class
drawClass = ...
drawClasses :: Array U DIM1 Sample -> RVar (Array U DIM1 Class)
drawClasses samples = A.mapM drawClass samples
onde A.mapM
ficaria algo como,
mapM :: ParallelMonad m => (a -> m b) -> Array r sh a -> m (Array r sh b)
Embora claramente como isso funcionaria depende crucialmente da implementação RVar
e de sua base RandomSource
, em princípio, alguém poderia pensar que isso envolveria desenhar uma nova semente aleatória para cada thread gerado e proceder normalmente.
Intuitivamente, parece que essa mesma ideia pode se generalizar para algumas outras mônadas.
Portanto, minha pergunta é: Alguém poderia construir uma classe ParallelMonad
de mônadas para a qual os efeitos possam ser paralelizados com segurança (presumivelmente habitada por, pelo menos, RVar
)?
Como pode ser? Que outras mônadas podem habitar esta classe? Outros já consideraram a possibilidade de como isso funcionaria em Repa?
Finalmente, se essa noção de ações monádicas paralelas não pode ser generalizada, alguém vê alguma maneira legal de fazer isso funcionar no caso específico de RVar
(onde seria muito útil)? Desistir RVar
do paralelismo é uma troca muito difícil.
fonte
RandomSource
específico. Minha tentativa ingênua de desenhar uma semente seria fazer algo simples e provavelmente muito errado, como desenhar um vetor de elementos (no caso demwc-random
) e adicionar 1 a cada elemento para produzir uma semente para o primeiro trabalhador, adicionar 2 para o segundo trabalhador, etc. Completamente inadequado se você precisar de entropia de qualidade criptográfica; esperançosamente bem se você só precisa de um passeio aleatório.split
função System.Random . Tem a desvantagem de produzir resultados diferentes (devido à natureza de,split
mas funciona. No entanto, estou tentando estender isso para matrizes Repa e não estou tendo muita sorte. Você fez algum progresso com isso ou está morto? fim?split
fornece uma base necessária, mas observe o comentário na fonte de comosplit
é implementado: "- nenhuma base estatística para isso!". Inclino-me a pensar que qualquer método de divisão de um PRNG deixará uma correlação explorável entre seus ramos, mas não tenho o background estatístico para provar isso. Em relação à questão geral, não tenho certeza seRespostas:
Já se passaram 7 anos desde que essa pergunta foi feita e ainda parece que ninguém apareceu com uma boa solução para esse problema. Repa não tem função
mapM
/traverse
like, mesmo que funcione sem paralelização. Além disso, considerando o progresso que houve nos últimos anos, parece improvável que isso aconteça.Por causa do estado obsoleto de muitas bibliotecas de array em Haskell e minha insatisfação geral com seus conjuntos de recursos, coloquei alguns anos de trabalho em uma biblioteca de array
massiv
, que pega emprestado alguns conceitos do Repa, mas o leva a um nível completamente diferente. Chega de introdução.Antes de hoje, houve três mapa monadic como funções em
massiv
(não contando o sinónimo como funções:imapM
,forM
. Et ai):mapM
- o mapeamento usual em um arbitrárioMonad
. Não é paralelizável por razões óbvias e também é um pouco lento (ao longo das linhas de costumemapM
em uma lista lenta)traversePrim
- aqui estamos restritos aPrimMonad
, que é significativamente mais rápido do quemapM
, mas a razão para isso não é importante para esta discussão.mapIO
- este, como o nome sugere, é restrito aIO
(ou melhorMonadUnliftIO
, mas isso é irrelevante). Como estamos dentroIO
, podemos dividir automaticamente a matriz em tantos pedaços quantos forem os núcleos e usar threads de trabalho separados para mapear aIO
ação sobre cada elemento nesses pedaços. Ao contrário de purofmap
, que também é paralelizável, temos que estarIO
aqui por causa do não determinismo do agendamento combinado com os efeitos colaterais de nossa ação de mapeamento.Então, depois de ler essa pergunta, pensei comigo mesmo que o problema estava praticamente resolvido
massiv
, mas não tão rápido. Geradores de números aleatórios, como inmwc-random
e outros inrandom-fu
não podem usar o mesmo gerador em muitos threads. O que significa que a única peça do quebra-cabeça que faltava era: "desenhar uma nova semente aleatória para cada thread gerado e procedendo normalmente". Em outras palavras, eu precisava de duas coisas:Então foi exatamente isso que eu fiz.
Primeiro, darei exemplos usando as funções
randomArrayWS
e especialmente criadasinitWorkerStates
, pois são mais relevantes para a questão e, posteriormente, passarei para o mapa monádico mais geral. Aqui estão as assinaturas de tipo:Para aqueles que não estão familiarizados com o
massiv
, oComp
argumento é uma estratégia de computação a ser usada, os construtores notáveis são:Seq
- executa a computação sequencialmente, sem bifurcar quaisquer threadsPar
- acione tantos threads quantos forem os recursos e use-os para fazer o trabalho.Vou usar o
mwc-random
pacote como exemplo inicialmente e, posteriormente, mover paraRVarT
:Acima, inicializamos um gerador separado por thread usando a aleatoriedade do sistema, mas poderíamos também ter usado uma semente única por thread derivando-a do
WorkerId
argumento, que é um meroInt
índice do trabalhador. E agora podemos usar esses geradores para criar uma matriz com valores aleatórios:Usando a
Par
estratégia, ascheduler
biblioteca irá dividir uniformemente o trabalho de geração entre os workers disponíveis e cada worker usará seu próprio gerador, tornando-o thread-safe. Nada nos impede de reutilizar o mesmoWorkerStates
número arbitrário de vezes, desde que não seja feito simultaneamente, o que, de outra forma, resultaria em uma exceção:Agora, deixando
mwc-random
de lado, podemos reutilizar o mesmo conceito para outros casos de uso possíveis, usando funções comogenerateArrayWS
:e
mapWS
:Aqui está o exemplo prometido sobre como utilizar esta funcionalidade com
rvar
,random-fu
emersenne-random-pure64
bibliotecas. Poderíamos ter usadorandomArrayWS
aqui também, mas para fins de exemplo, digamos que já temos uma matriz comRVarT
s diferentes , caso em que precisamos demapWS
:É importante observar que, apesar do fato de que a implementação pura de Mersenne Twister está sendo usada no exemplo acima, não podemos escapar do IO. Isso se deve ao escalonamento não determinístico, o que significa que nunca sabemos qual dos trabalhadores estará lidando com qual pedaço do array e, consequentemente, qual gerador será usado para qual parte do array. Por outro lado, se o gerador é puro e divisível, como
splitmix
, então podemos usar a função de geração pura, determinística e paralelizável:,randomArray
mas isso já é uma história separada.fonte
Provavelmente não é uma boa ideia fazer isso devido à natureza sequencial inerente dos PRNGs. Em vez disso, você pode querer fazer a transição de seu código da seguinte maneira:
main
ou o que você quiser).fonte