【问题标题】:Why this small snippet hangs using multiprocessing with maxtasksperchild, numpy.random.randint and numpy.random.seed?为什么这个小片段在使用 maxtasksperchild、numpy.random.randint 和 numpy.random.seed 的多处理时会挂起?
【发布时间】:2017-06-11 21:31:49
【问题描述】:

我有一个 python 脚本,它以随机方式同时处理 numpy 数组和图像。为了在生成的进程中有适当的随机性,我将一个随机种子从主进程传递给工作人员,以便他们被播种。

当我将maxtasksperchild 用于Pool 时,我的脚本在运行Pool.map 多次后挂起。

以下是重现问题的最小 sn-p:

# This code stops after multiprocessing.Pool workers are replaced one single time.
# They are replaced due to maxtasksperchild parameter to Pool
from multiprocessing import Pool
import numpy as np

def worker(n):
    # Removing np.random.seed solves the issue
    np.random.seed(1) #any seed value
    return 1234 # trivial return value

# Removing maxtasksperchild solves the issue
ppool = Pool(20 , maxtasksperchild=5)
i=0
while True:
    i += 1
    # Removing np.random.randint(10) or taking it out of the loop solves the issue
    rand = np.random.randint(10)
    l  = [3] # trivial input to ppool.map
    result = ppool.map(worker, l)
    print i,result[0]

这是输出

1 1234 2 1234 3 1234 . . . 99 1234 100 1234 # 此时工作人员应该已经达到 maxtasksperchild 任务 101 1234 102 1234 103 1234 104 1234 105 1234 106 1234 107 1234 108 1234 109 1234 110 1234

然后无限期挂起。

我可能会用 python 的 random 替换 numpy.random 并摆脱这个问题。然而,在我的实际应用程序中,worker 将执行我无法控制的用户代码(作为 worker 的参数),并希望允许在该用户代码中使用 numpy.random 函数。所以我有意为全局随机生成器播种(每个进程独立)。

这是使用 Python 2.7.10、numpy 1.11.0、1.12.0 和 1.13.0、Ubuntu 和 OSX 测试的

【问题讨论】:

  • Can't reproduce 在带有 2 进程池的 Ideone 上。 (Ideone 不允许我使用 20。)您的结果是否取决于池大小?
  • 也许..我只是重试了这个,它为 Pool 与 7+ 工人挂起,但在每次运行中挂起的时间不同。所以看起来像一种竞争条件,随着工人的增加而更加突出

标签: python numpy multiprocessing python-multiprocessing numpy-random


【解决方案1】:

事实证明,这是来自 threading.Lockmultiprocessing 的 Python 错误交互。

np.random.seed 和大多数np.random.* 函数使用threading.Lock 来确保线程安全。 np.random.* 函数生成一个随机数,然后更新种子(跨线程共享),这就是需要锁的原因。请参阅 np.random.seedcont0_array(由 np.random.random() 和其他人使用)。

现在这如何导致上述 sn-p 出现问题?

简而言之,sn-p 挂起是因为在 fork 时继承了 threading.Lock 状态。因此,当一个子进程同时在父进程中获取锁时(np.random.randint(10)),子进程就会死锁(np.random.seed)。

@njsmith 在这个 github 问题 https://github.com/numpy/numpy/issues/9248#issuecomment-308054786 中解释了它

multiprocessing.Pool 产生一个后台线程来管理工作线程:https://github.com/python/cpython/blob/aefa7ebf0ff0f73feee7ab24f4cdcb2014d83ee5/Lib/multiprocessing/pool.py#L170-L173

它在后台循环调用_maintain_pool:https://github.com/python/cpython/blob/aefa7ebf0ff0f73feee7ab24f4cdcb2014d83ee5/Lib/multiprocessing/pool.py#L366

如果工作人员退出,例如由于 maxtasksperchild 限制,则 _maintain_pool 调用 _repopulate_pool:https://github.com/python/cpython/blob/aefa7ebf0ff0f73feee7ab24f4cdcb2014d83ee5/Lib/multiprocessing/pool.py#L240

然后 _repopulate_pool 派生了一些新的工人,仍然在这个后台线程中:https://github.com/python/cpython/blob/aefa7ebf0ff0f73feee7ab24f4cdcb2014d83ee5/Lib/multiprocessing/pool.py#L224

所以发生的事情是最终你会倒霉,同时你的主线程正在调用一些 np.random 函数并持有锁,多处理决定派生一个孩子,这从 np.random 锁开始已经持有,但持有它的线程已经消失。然后子进程尝试调用 np.random,这需要获取锁,因此子进程死锁。

这里的简单解决方法是不要将 fork 与多处理一起使用。如果您使用 spawn 或 forkserver 启动方法,那么这应该会消失。

为了适当的修复.... ughhh。我想我们..需要注册一个 pthread_atfork pre-fork 处理程序,该处理程序在 fork 之前获取 np.random 锁,然后在之后释放它?真的,我想我们需要对 numpy 中的每个锁都执行此操作,这需要保留每个 RandomState 对象的弱集,并且 _FFTCache 似乎也有一个锁......

(从好的方面来说,这也让我们有机会重新初始化孩子的全局随机状态,在用户没有明确播种的情况下,我们确实应该这样做。)

【讨论】:

  • 我认为这将是那些丑陋的线程/fork-without-exec 问题之一。 Fork-without-exec 总的来说是相当脆弱的。
【解决方案2】:

使用numpy.random.seed 不是线程安全的。 numpy.random.seed 会全局更改种子的值,而据我所知,您正在尝试在本地更改种子。

the docs

如果您确实想要实现的是在每个工人开始时播种生成器,那么以下是一个解决方案:

def worker(n):
    # Removing np.random.seed solves the problem                                                               
    randgen = np.random.RandomState(45678) # RandomState, not seed!
    # ...Do something with randgen...                                           
    return 1234 # trivial return value                                                                         

【讨论】:

  • 谢谢。但是np.random.RandomState(1) 不会为随机生成器播种。另外,您能否指出我在文档中谈到numpy.random.seed 的线程安全的地方,我找不到它
  • 如果通过线程安全意味着每个进程将生成相同的随机数......是的,这就是为什么我将随机种子从主进程传递给每个工作人员
  • 这取决于您想要实现的目标。如果它在每个工人中都有一个新的生成器,并以 45678 为种子,则此方法有效。据我了解,randgen 现在相当于numpy.random,以 45678 为种子。
  • 再次,据我了解,numpy.seed 每次执行时都会更改全局种子。我不确定,但我认为这意味着在执行一个线程期间,您正在更改会影响其他线程的种子。
  • numpy.seed 全局更改种子。 “全局”,因为所有后续调用 numpy.random 函数都将取决于该种子。但是由于 Unix 中的进程管理,每个进程都是完全相互独立的。每个进程都有自己的内存副本并生活在自己的世界中,除非套接字/队列/..etc 明确用于 IPC
【解决方案3】:

将此作为一个完整的答案,因为它不适合评论。

玩了一会儿,这里的东西闻起来像一个 numpy.random 错误。我能够重现冻结错误,此外还有一些其他不应该发生的奇怪事情,例如手动播种生成器不起作用。

def rand_seed(rand, i):
    print(i)
    np.random.seed(i)
    print(i)
    print(rand())
def test1():
    with multiprocessing.Pool() as pool:
        [pool.apply_async(rand_seed, (np.random.random_sample, i)).get()
        for i in range(5)]
test1()

有输出

0
0
0.3205032737431185
1
1
0.3205032737431185
2
2
0.3205032737431185
3
3
0.3205032737431185
4
4
0.3205032737431185

另一方面,不将 np.random.random_sample 作为参数传递也可以。

def rand_seed2(i):
    print(i)
    np.random.seed(i)
    print(i)
    print(np.random.random_sample())
def test2():
    with multiprocessing.Pool() as pool:
        [pool.apply_async(rand_seed, (i,)).get()
        for i in range(5)]
test2()

有输出

0
0
0.5488135039273248
1
1
0.417022004702574
2
2
0.43599490214200376
3
3
0.5507979025745755
4
4
0.9670298390136767

这表明幕后正在发生一些严重的愚蠢行为。不过不知道还能说什么......

基本上,numpy.random.seed 似乎不仅修改了“种子状态”变量,还修改了random_sample 函数本身。

【讨论】:

  • 我不确定你为什么将 random_sample 传递给工人。但它似乎以状态(RandomState)传递函数。我很惊讶通过random_sample 根本起作用,因为它是一个不可腌制的实例方法,因此不能通过队列(由池使用)。但由于这是一个 cython 类,所以情况可能会有所不同。 random_sample 在这里定义 github.com/numpy/numpy/blob/… 这是一个有趣的问题,但我相信与我的问题不太相关
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-12-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多