【问题标题】:numpy.memmap: bogus memory allocationnumpy.memmap:虚假的内存分配
【发布时间】:2015-12-01 15:14:06
【问题描述】:

我有一个python3 脚本,它与numpy.memmap 数组一起运行。它将一个数组写入位于/tmp的新生成的临时文件中:

import numpy, tempfile

size = 2 ** 37 * 10
tmp = tempfile.NamedTemporaryFile('w+')
array = numpy.memmap(tmp.name, dtype = 'i8', mode = 'w+', shape = size)
array[0] = 666
array[size-1] = 777
del array
array2 = numpy.memmap(tmp.name, dtype = 'i8', mode = 'r+', shape = size)
print('File: {}. Array size: {}. First cell value: {}. Last cell value: {}'.\
      format(tmp.name, len(array2), array2[0], array2[size-1]))
while True:
    pass

硬盘的大小只有 250G。尽管如此,它还是能以某种方式在/tmp 中生成10T 的大文件,并且对应的数组似乎仍然可以访问。脚本的输出如下:

File: /tmp/tmptjfwy8nr. Array size: 1374389534720. First cell value: 666. Last cell value: 777

文件确实存在,显示为10T大:

$ ls -l /tmp/tmptjfwy8nr
-rw------- 1 user user 10995116277760 Dec  1 15:50 /tmp/tmptjfwy8nr

但是,/tmp 的整体大小要小得多:

$ df -h /tmp
Filesystem      Size  Used Avail Use% Mounted on
/dev/sda1       235G  5.3G  218G   3% /

进程也是假装使用10T虚拟内存,这也是不可能的。 top 命令的输出:

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND 
31622 user      20   0 10.000t  16592   4600 R 100.0  0.0   0:45.63 python3

据我了解,这意味着在调用numpy.memmap 期间,未分配整个数组所需的内存,因此显示的文件大小是虚假的。这反过来意味着,当我开始逐渐用我的数据填充整个数组时,有时我的程序会崩溃或我的数据会损坏。

确实,如果我在我的代码中引入以下内容:

for i in range(size):
    array[i] = i

一段时间后我得到了错误:

Bus error (core dumped)

所以,问题来了:一开始如何检查,如果真的有足够的内存来存储数据,然后确实为整个数组保留空间?

【问题讨论】:

    标签: python linux numpy memory-mapped-files sparse-file


    【解决方案1】:

    您正在生成 10 TB 文件这一事实并没有什么“虚假”

    您要求的数组大小

    2 ** 37 * 10 = 1374389534720 个元素

    'i8' 的 dtype 表示 8 字节(64 位)整数,因此您的最终数组的大小为

    1374389534720 * 8 = 10995116277760 字节

    10995116277760 / 1E12 = 10.99511627776 TB


    如果您只有 250 GB 的可用磁盘空间,那么如何创建“10 TB”文件?

    假设您使用的是相当现代的文件系统,您的操作系统将能够生成几乎任意大的sparse files,无论您是否实际上有足够的物理磁盘空间来支持它们。

    例如,在我的 Linux 机器上,我可以这样做:

    # I only have about 50GB of free space...
    ~$ df -h /
    Filesystem     Type  Size  Used Avail Use% Mounted on
    /dev/sdb1      ext4  459G  383G   53G  88% /
    
    ~$ dd if=/dev/zero of=sparsefile bs=1 count=0 seek=10T
    0+0 records in
    0+0 records out
    0 bytes (0 B) copied, 0.000236933 s, 0.0 kB/s
    
    # ...but I can still generate a sparse file that reports its size as 10 TB
    ~$ ls -lah sparsefile
    -rw-rw-r-- 1 alistair alistair 10T Dec  1 21:17 sparsefile
    
    # however, this file uses zero bytes of "actual" disk space
    ~$ du -h sparsefile
    0       sparsefile
    

    在您的np.memmap 文件初始化后尝试调用du -h 以查看它使用了多少实际磁盘空间。

    当您开始将数据实际写入您的np.memmap 文件时,一切都会正常,直到您超出存储的物理容量,此时该过程将以Bus error 终止。这意味着如果您需要向 np.memmap 数组写入


    一个进程怎么可能使用 10 TB 的虚拟内存?

    当您创建memory map 时,内核会在调用进程的虚拟地址空间内分配一个新的地址块,并将它们映射到您磁盘上的文件中。因此,您的 Python 进程正在使用的虚拟内存量将随着刚刚创建的文件的大小而增加。由于文件也可以是稀疏文件,因此虚拟内存不仅可以超过可用 RAM 的总量,而且还可以超过您机器上的总物理磁盘空间。


    如何检查是否有足够的磁盘空间来存储完整的np.memmap 数组?

    我假设您想在 Python 中以编程方式执行此操作。

    1. 获取可用的可用磁盘空间量。 this previous SO question 的答案中给出了各种方法。一种选择是os.statvfs

      import os
      
      def get_free_bytes(path='/'):
          st = os.statvfs(path)
          return st.f_bavail * st.f_bsize
      
      print(get_free_bytes())
      # 56224485376
      
    2. 计算数组的大小(以字节为单位):

      import numpy as np
      
      def check_asize_bytes(shape, dtype):
          return np.prod(shape) * np.dtype(dtype).itemsize
      
      print(check_asize_bytes((2 ** 37 * 10,), 'i8'))
      # 10995116277760
      
    3. 检查是否为2.> 1.


    更新:是否有一种“安全”的方式来分配np.memmap 文件,以保证保留足够的磁盘空间来存储整个阵列?

    一种可能是使用fallocate 预先分配磁盘空间,例如:

    ~$ fallocate -l 1G bigfile
    
    ~$ du -h bigfile
    1.1G    bigfile
    

    您可以从 Python 调用它,例如使用 subprocess.check_call:

    import subprocess
    
    def fallocate(fname, length):
        return subprocess.check_call(['fallocate', '-l', str(length), fname])
    
    def safe_memmap_alloc(fname, dtype, shape, *args, **kwargs):
        nbytes = np.prod(shape) * np.dtype(dtype).itemsize
        fallocate(fname, nbytes)
        return np.memmap(fname, dtype, *args, shape=shape, **kwargs)
    
    mmap = safe_memmap_alloc('test.mmap', np.int64, (1024, 1024))
    
    print(mmap.nbytes / 1E6)
    # 8.388608
    
    print(subprocess.check_output(['du', '-h', 'test.mmap']))
    # 8.0M    test.mmap
    

    我不知道使用标准库执行此操作的独立于平台的方法,但有一个 fallocate Python module on PyPI 应该适用于任何基于 Posix 的操作系统。

    【讨论】:

    • 这是一个非常好的和详细的答案。但是,据我了解,没有办法让它安全。如果其他进程在我检查了可用空间之后创建了一个大文件怎么办?有没有办法生成普通文件而不是稀疏文件并提前保留所需的空间?
    • 很好的解决方案,虽然有一个小缺陷:由于需要存储dtype 和其他一些元数据,memmap 文件的实际大小比数组元素的总大小要大一些。有时,这种开销也可能导致Bus error。最好找到一种方法来提前获得完整的文件大小。
    • 我不确定你指的是什么。如果您使用 np.memmap 创建数组,则文件不应包含任何标题信息 - 它们的大小与您根据数组的尺寸和项目大小预测的大小完全相同。你能举个例子吗?也许对单位有些混淆 - “SI”太字节是 1E12 字节,而二进制 "tebibyte" 是 2^40 字节。 fallocate 可以使用任一约定 10TB 表示 1E12 字节,而 10TiB 表示 2**40 字节。
    • 是的,我错了:文件大小与数据大小完全匹配。混淆实际上是pickle 转储numpy 数组。
    • 嗯,好的。我建议您避免将 pickle 用于 numpy 数组 - 这是一种非常低效的存储数值数据的方法。如果您只想要一个没有标题的原始二进制文件,最好使用np.save/np.savezndarray.tofile。 HDF5 也可能适用于大型数据集。
    【解决方案2】:

    基于@ali_m 的回答,我终于找到了这个解决方案:

    # must be called with the argumant marking array size in GB
    import sys, numpy, tempfile, subprocess
    
    size = (2 ** 27) * int(sys.argv[1])
    tmp_primary = tempfile.NamedTemporaryFile('w+')
    array = numpy.memmap(tmp_primary.name, dtype = 'i8', mode = 'w+', shape = size)
    tmp = tempfile.NamedTemporaryFile('w+')
    check = subprocess.Popen(['cp', '--sparse=never', tmp_primary.name, tmp.name])
    stdout, stderr = check.communicate()
    if stderr:
        sys.stderr.write(stderr.decode('utf-8'))
        sys.exit(1)
    del array
    tmp_primary.close()
    array = numpy.memmap(tmp.name, dtype = 'i8', mode = 'r+', shape = size)
    array[0] = 666
    array[size-1] = 777
    print('File: {}. Array size: {}. First cell value: {}. Last cell value: {}'.\
          format(tmp.name, len(array), array[0], array[size-1]))
    while True:
        pass
    

    这个想法是将最初生成的稀疏文件复制到一个新的普通文件。为此,cp 使用了选项--sparse=never

    当使用可管理的大小参数(例如 1 GB)调用脚本时,数组将被映射到非稀疏文件。 du -h 命令的输出证实了这一点,该命令现在显示 ~1 GB 大小。如果内存不够,脚本会退出并报错:

    cp: ‘/tmp/tmps_thxud2’: write failed: No space left on device
    

    【讨论】:

    • 对于大型稀疏文件,cp --sparse-neverfallocate 相比会非常慢,因为它将为整个文件写入零字节。 fallocate 几乎是瞬时的,因为它只是分配块而不将它们归零。
    猜你喜欢
    • 2011-03-08
    • 2011-09-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-01-24
    • 2010-10-12
    • 2015-12-17
    相关资源
    最近更新 更多