并行随机数生成

有三种实现的策略可用于跨多个进程(本地或分布式)生成可重复的伪随机数。

SeedSequence 产卵

SeedSequence implements an algorithm 处理用户提供的种子,通常作为某个大小的整数,并将其转换为 BitGenerator . 它使用散列技术来确保低质量的种子转化为高质量的初始状态(至少,概率非常高)。

例如, MT19937 has a state consisting of 624 uint32 integers. A naive way to take a 32-bit integer seed would be to just set the last element of the state to the 32-bit seed and leave the rest 0s. This is a valid state for MT19937, but not a good one. The Mersenne Twister algorithm suffers if there are too many 0s . 类似地,两个相邻的32位整数种子(即。 1234512346 )会产生非常相似的溪流。

SeedSequence avoids these problems by using successions of integer hashes with good avalanche properties 以确保翻转输入中的任何位有大约50%的几率翻转输出中的任何位。两个相互非常接近的输入种子将产生彼此非常远的初始状态(概率非常高)。它的构造方式还可以提供任意大小的整数或整数列表。 SeedSequence 将所有的位,你提供和混合起来,产生多少位消费 BitGenerator 需要初始化自身。

这些属性一起意味着我们可以安全地将用户提供的普通种子与简单的递增计数器混合在一起,以获得 BitGenerator 相互独立的状态。我们可以将其包装成一个易于使用且不易误用的API。

from numpy.random import SeedSequence, default_rng

ss = SeedSequence(12345)

# Spawn off 10 child SeedSequences to pass to child processes.
child_seeds = ss.spawn(10)
streams = [default_rng(s) for s in child_seeds]

孩子 SeedSequence 对象也可以生成孙子,等等。每个 SeedSequence 在产卵树中的位置 SeedSequence 对象与用户提供的种子混合,以生成独立(概率非常高)的流。

grandchildren = child_seeds[0].spawn(4)
grand_streams = [default_rng(s) for s in grandchildren]

此功能允许您在本地决定何时以及如何拆分流,而无需在进程之间进行协调。您不必预先分配空间,以避免重叠或从公共全局服务请求流。这种通用的“树散列”方案是 not unique to numpy 但还没有普及。Python具有越来越灵活的并行化机制,这个方案非常适合这种用途。

使用此方案,如果知道所导出的流的数目,则可以估计碰撞概率的上界。 SeedSequence 默认情况下,将其输入(种子路径和繁殖树路径)散列为128位池。悲观地估计,在该池中发生碰撞的概率 (1) ,大约 n^2*2^{{-128}} 在哪里? n 是生成的流数。如果一个程序使用了百万个流,大约 2^{{20}} ,则至少有一对相同的概率约为 2^{{-88}} 在一个完全不可忽视的领域 (2) .

1

算法经过精心设计,消除了许多可能的碰撞方式。例如,如果只进行一级繁殖,则可以保证所有状态都是唯一的。但更容易估计餐巾纸上的天真上限,并在知道概率实际上较低的情况下感到安慰。

2

在这个计算中,我们可以忽略从每个流中提取的数字量。我们提供的每个PRNG都有一些内置的额外保护,以避免在 SeedSequence 游泳池有一点点不同。 PCG642^{{127}} 除种子在种子中的位置外,由种子决定的单独循环 2^{{128}} 每个周期的周期都很长,所以一个人必须同时进入或接近同一个周期 and 在循环中的附近位置播种。 Philox 由种子决定的完全独立的循环。 SFC64 包含64位计数器,因此每个唯一种子至少 2^{{64}} 迭代远离任何其他种子。最后, MT19937 有一个难以想象的巨大时期。内部发生碰撞 SeedSequence 是观察失败的方式。

独立流

Philox 是一个基于计数器的RNG-based,它通过使用弱加密原语加密递增计数器来生成值。种子确定用于加密的密钥。唯一键创建唯一、独立的流。 Philox 将密钥设置为128位可直接绕过该算法。类似但不同的键仍将创建独立的流。

import secrets
from numpy.random import Philox

# 128-bit number as a seed
root_seed = secrets.getrandbits(128)
streams = [Philox(key=root_seed + stream_id) for stream_id in range(10)]

此方案要求您避免重用流ID。这可能需要并行进程之间的协调。

跳过BitGenerator状态

jumped 提升位生成器的状态 as-if 已绘制大量随机数,并返回具有此状态的新实例。绘制的具体数目因位生成器而异,范围从 2^{{64}}2^{{128}} . 另外, as-if 绘制还取决于特定位生成器生成的默认随机数的大小。支持 jumped ,以及位生成器的周期,跳转的大小和默认无符号随机数中的位如下所示。

BitGenerator

期间

跳转大小

MT19937型

2^{19937}

2^{128}

32

PCG64型

2^{128}

~2^{{127}} (3)

64

菲洛克斯

2^{256}

2^{128}

64

3

跳转大小为 (\phi-1)*2^{{128}} 在哪里? \phi 是黄金比例。当相邻的时间段之间的距离小于经典的时间段时,这个时间段的长度会比经典的时间段的长度要小。在你的一生中,你将无法跳跃到足以使这些距离小到可以重叠的程度。

jumped 应该用来产生足够长的重叠块。

import secrets
from numpy.random import PCG64

seed = secrets.getrandbits(128)
blocked_rng = []
rng = PCG64(seed)
for i in range(10):
    blocked_rng.append(rng.jumped(i))

使用时 jumped ,则必须注意不要跳到已使用的流。在上面的例子中,以后不能使用 blocked_rng[0].jumped() 因为它会与 blocked_rng[1] . 与独立流一样,如果这里的主进程希望通过跳转分离出10个以上的流,那么它需要从 range(10, 20) ,否则它将重新创建相同的流。另一方面,如果您仔细地构造流,那么您就可以保证有不重叠的流。