并行随机数生成#

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

SeedSequence产卵#

NumPy 允许您通过其方法生成新的(以非常高的概率)独立的 BitGenerator实例。该生成是通过用于初始化位生成器随机流来实现的。Generatorspawn()SeedSequence

SeedSequence 实现一种算法来处理用户提供的种子(通常作为某种大小的整数),并将其转换为 的初始状态BitGenerator。它使用散列技术来确保低质量的种子转变为高质量的初始状态(至少,以非常高的概率)。

例如,具有由 624 个uint32整数MT19937组成的状态 。采用 32 位整数种子的一种简单方法是将状态的最后一个元素设置为 32 位种子,并保留其余的 0。这是 的一种有效状态,但不是一个好的状态。如果 0 太多, Mersenne Twister 算法就会受到影响。类似地,两个相邻的 32 位整数种子(即和)将产生非常相似的流。MT199371234512346

SeedSequence通过使用具有良好雪崩特性的连续整数哈希来避免这些问题,以确保翻转输入中的任何位都有大约 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为了方便起见,没有必要直接使用。上面的内容streams可以通过以下方式直接从父生成器生成spawn

parent_rng = default_rng(12345)
streams = parent_rng.spawn(10)

子对象还可以生成子对象,等等。每个子对象都有一个SeedSequence在生成的子对象树中的位置,与用户提供的种子混合以生成独立的(具有非常高的概率)流。

grandchildren = streams[0].spawn(4)

此功能使您可以就何时以及如何拆分流做出本地决策,而无需在进程之间进行协调。您不必预先分配空间来避免重叠或来自公共全局服务的请求流。这种通用的“树哈希”方案并不是numpy 独有的,但尚未广泛应用。 Python 具有越来越灵活的并行化机制,并且该方案非常适合这种用途。

使用这一方案,如果知道所导出的流的数量,则可以估计冲突概率的上限。SeedSequence 默认情况下,将其输入(种子和生成树路径)散列到 128 位池。悲观估计( [ 1 ] )该池中发生冲突的概率约为\(n^2*2^{-128}\)其中 n是生成的流的数量。如果一个程序使用了数以百万计的流,大约\(2^{20}\),那么它们中至少一对相同的概率约为\(2^{-88}\),这是完全可以忽略的领域([ 2 ])。

整数种子序列#

正如上一节所讨论的,SeedSequence不仅可以采用整数种子,还可以采用任意长度的(非负)整数序列。如果稍微小心一点,就可以使用此功能来设计 临时方案,以获得安全的并行 PRNG 流,并具有与生成类似的安全保证。

例如,一种常见的用例是,为整个计算向工作进程传递一个根种子整数以及一个整数工作 ID(或者更细粒度的内容,如作业 ID、批次 ID 或类似内容)。如果这些 ID 是确定性且唯一地创建的,则可以通过将 ID 和根种子整数组合在一个列表中来导出可再现的并行 PRNG 流。

# default_rng() and each of the BitGenerators use SeedSequence underneath, so
# they all accept sequences of integers as seeds the same way.
from numpy.random import default_rng

def worker(root_seed, worker_id):
    rng = default_rng([worker_id, root_seed])
    # Do work ...

root_seed = 0x8c3c010cb4754c905776bdac5ee7501
results = [worker(root_seed, worker_id) for worker_id in range(10)]

这可以用来取代过go使用的许多不安全策略,这些策略尝试将根种子和 ID 组合回单个整数种子值。例如,经常会看到用户将工作 ID 添加到根种子中,尤其是使用遗留RandomState代码时。

# UNSAFE! Do not do this!
worker_seed = root_seed + worker_id
rng = np.random.RandomState(worker_seed)

确实,对于以这种方式构建的并行程序的任何一次运行,每个工作线程都将具有不同的流。然而,使用不同种子多次调用程序很可能会得到重叠的工作种子集。在进行这些重复运行时,仅仅改变一两个增量的根种子并不罕见(根据作者的自身经验)。如果工作人员种子也是通过工作人员 ID 的小增量导出的,那么工作人员的子集将返回相同的结果,从而导致整体结果出现偏差。

将工作 ID 和根种子组合为整数列表可以消除这种风险。惰性播种做法仍然相当安全。

该方案确实要求额外的 ID 是唯一的并且确定性地创建。这可能需要工作进程之间的协调。建议将不同的 ID 放在不变的根种子 之前。在用户提供的种子后面附加整数,因此,如果您可能混合使用这种临时机制和生成,或者将对象传递给可能生成的库代码,那么在前面添加工作 ID 会比在前面添加工作 ID 更安全一些添加它们以避免碰撞。spawn

# Good.
worker_seed = [worker_id, root_seed]

# Less good. It will *work*, but it's less flexible.
worker_seed = [root_seed, worker_id]

考虑到这些注意事项,防止碰撞的安全保证与上一节中讨论的生成大致相同。算法机制是相同的。

独立流#

Philox是一种基于计数器的 RNG,它通过使用弱加密原语对递增计数器进行加密来生成值。种子决定用于加密的密钥。唯一的键创建唯一的、独立的流。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推进 BitGenerator 的状态,就好像已抽取大量随机数一样,并返回具有此状态的新实例。具体抽奖次数因BitGenerator而异,范围为 \(2^{64}\)\(2^{128}\)。此外,模拟抽奖还取决于特定 BitGenerator 生成的默认随机数的大小。下面列出了支持 的 BitGenerators jumped,以及 BitGenerator 的周期、跳转的大小和默认无符号随机数中的位。

比特生成器

时期

跳跃尺寸

每次绘制位数

MT19937

\(2^{19937}-1\)

\(2^{128}\)

32

PCG64

\(2^{128}\)

\(~2^{127}\)( [ 3 ] )

64

PCG64DXSM

\(2^{128}\)

\(~2^{127}\)( [ 3 ] )

64

菲洛克斯

\(2^{256}\)

\(2^{128}\)

64

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)