创建多处理数组时出现分段错误

Segmentation fault when creating multiprocessing array

我正在尝试使用多处理填充一个 numpy 数组,紧随 this post。我在 Mac 上运行良好,但是当我将它移植到 Ubuntu 时,我经常遇到分段错误。

我已将代码缩减为以下最小示例:

import numpy as np
from multiprocessing import sharedctypes

a = np.ctypeslib.as_ctypes(np.zeros((224,224,3)))
print("Made a, now making b")
b = sharedctypes.RawArray(a._type_, a)
print("Finished.")

在 Ubuntu 16.04 上,使用 Python 3.6.5 和 numpy 1.15.4(与我的 Mac 上的版本相同),我得到了输出

Made a, now making b
Segmentation fault (core dumped)

现在,我可以稍微更改数组维度,在某些情况下它会起作用(例如,将前 224 更改为 100 并且它会起作用)。但主要是它会出现故障。

任何人都可以提供任何见解吗?

我看到 one post on a related topic from 2016 that no one responded to, and another one involving pointers 我没有使用。

PS-a 指定为多维数组还是扁平化数组(例如 np.zeros(224*224*3)).如果我更改数据类型(例如 float 到 int),它似乎也没有什么区别;它同样失败。

进一步更新: 即使在 original post 的代码中设置 "size=224" 也会导致两台不同的 Ubuntu 机器出现段错误numpy 的版本,但在 Mac 上运行良好。

最后的笔记

将我的测试留给后代,但 有答案。

备注

以下是在 Debian 上的测试结果。在 Ubuntu (WSL) 上测试确实差很多。在 Ubuntu n=193 上,任何形状都会崩溃(如果我将 3d n 替换为 1),以及上面的任何 n。看起来像(见下文 bla.py):

  1. py bla.py n 1A 上分配 3204,为 al 0<n<193
  2. 分配 29323 ob B
  3. 对于 n>=193,在 B 上发生分段错误,并且在 A 上分配了 3208。显然 ubuntu.
  4. 某处存在一些硬内存限制

Debian 上的旧测试

经过一些测试后,我认为这是一个内存问题,内存分配随维度的缩放比例很奇怪。

只有 2 个维度的编辑对我来说不会崩溃,但 3 个维度会崩溃 - 我会假设这个回答。

对我来说:

b = sharedctypes.RawArray(a._type_, a)

在以下情况下不会崩溃:

a = np.ctypeslib.as_ctypes(np.zeros((224**3))) #Though generating b takes a while
a = np.ctypeslib.as_ctypes(np.zeros((100,100,100)))

所以似乎对内存的需求减少解决了这个问题,但奇怪的是,一维数组中所需的相同数量的单元格工作正常 - 所以内存中更深层次的东西似乎正在发生。

当然是用指针了。让我们尝试一些事情 (bla.py):

import tracemalloc
import numpy as np
from sys import argv
from multiprocessing import sharedctypes

n,shape = (int (x) for x in argv[1:])
if shape == 1: shape = n
if shape == 2: shape = (n**2,n)
if shape == 3: shape = (n,n,n)

tracemalloc.start()
a = np.ctypeslib.as_ctypes(np.zeros(shape))
x=tracemalloc.take_snapshot().statistics('lineno')
print(len(x),sum((a.size for a in x)))
b = sharedctypes.RawArray(a._type_, a)
x=tracemalloc.take_snapshot().statistics('lineno')
print(len(x),sum((a.size for a in x)))

导致:

           n   shape    (a mallocs sum) (b mallocs sum)
>py bla.py 100 1     => 5 3478 76 30147
>py bla.py 100 2     => 5 5916 76 948313
>py bla.py 100 3     => 5 8200 76 43033
>py bla.py 150 1     => 5 3478 76 30195
>py bla.py 150 2     => 5 5916 76 2790461
>py bla.py 150 3     => 5 8200 76 45583
>py bla.py 159 1     => 5 3478 76 30195
>py bla.py 159 2     => 5 5916 76 2937854
>py bla.py 159 3     => 5 8200 76 46042
>py bla.py 160 1     => 5 3478 76 30195
>py bla.py 160 2     => 5 5916 72 2953989
>py bla.py 160 3     => 5 8200 Segmentation fault
>py bla.py 161 1     => 5 3478 76 30195
>py bla.py 161 2     => 5 5916 75 2971746
>py bla.py 161 3     => 5 8200 75 46116

>py bla.py 221 1     => 5 3478 76 30195
>py bla.py 221 2     => 5 5916 76 5759398
>py bla.py 221 3     => 5 8200 76 55348
>py bla.py 222 1     => 5 3478 76 30195
>py bla.py 222 2     => 5 5916 76 5782877
>py bla.py 222 3     => 5 8200 76 55399
>py bla.py 223 1     => 5 3478 76 30195
>py bla.py 223 2     => 5 5916 76 5806462
>py bla.py 223 3     => 5 8200 76 55450
>py bla.py 224 1     => 5 3478 76 30195
>py bla.py 224 2     => 5 5916 72 5829381
>py bla.py 224 3     => 5 8200 Segmentation fault
>py bla.py 225 1     => 5 3478 76 30195
>py bla.py 225 2     => 5 5916 76 5853950
>py bla.py 225 3     => 5 8200 76 55552

奇怪的东西 (n**2,n) 在共享类型中为其分配了大量内存,而 n**3(n,n,n) 则没有。但这不是重点。

  1. a mallocs 是一致的,仅略微依赖于维度,完全不依赖于 n(对于测试的数字)。
  2. b mallocs 除了在形状 2 上很高之外,还随 n 略有增加,但随着形状的变化它们变化很大。
  3. 段错误是循环出现的!我的机器上形状 (n,n,n) 的内存分配在 sefault 之前接近一些 n 相关数字 - 但对于 n+1 我们再次确定。似乎是 ~46k 大约 160 和 ~56k 大约 224。

我没有很好的解释,但对 n 的依赖让我认为分配需要很好地适应某些位结构,有时会中断。

我猜测使用 225 作为您的尺寸会起作用 - 作为一种解决方法。

这与其说是答案,不如说是猜测,但您可能 运行 由于底层数据缓冲区的垃圾收集而遇到问题。这可以解释为什么似乎依赖于您尝试创建的数组的整体大小。

如果是这种情况,则解决方法是将您创建的 Numpy 零数组分配给它自己的变量。这将确保缓冲区 "lives" 通过创建 RawArray。代码将是:

zs = np.zeros((224,224,3))
a = np.ctypeslib.as_ctypes(zs)
print("Made a, now making b")
b = sharedctypes.RawArray(a._type_, a)
print("Finished.")

我现在只有一个mac,所以我不能自己测试。

其他分析和根本原因修复。

如上所述,这是垃圾收集错误的结果,这为我提供了修复方法的提示。

通过保持对原始 np.zeros 对象的引用,避免了错误。这意味着(对我而言)原始对象的集合破坏了结果数组。

查看 as_ctypes 的实现(摘自 c52543e4a

def as_ctypes(obj):
    """Create and return a ctypes object from a numpy array.  Actually
    anything that exposes the __array_interface__ is accepted."""
    ai = obj.__array_interface__
    if ai["strides"]:
        raise TypeError("strided arrays not supported")
    if ai["version"] != 3:
        raise TypeError("only __array_interface__ version 3 supported")
    addr, readonly = ai["data"]
    if readonly:
        raise TypeError("readonly arrays unsupported")
    tp = _ctype_ndarray(_typecodes[ai["typestr"]], ai["shape"])
    result = tp.from_address(addr)
    result.__keep = ai
    return result

很明显是原作者想到的(赋值.__keep来维护对原始对象的引用)。但是,他们似乎需要保留对 原始 对象的引用。

我写了 a patch 这样做:

-        result.__keep = ai
+        result.__keep = obj