numpy.memmap:虚假的内存分配

时间:2015-12-01 15:14:06

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

我有一个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)

因此,问题:如何在开头检查,如果有足够的内存用于数据,然后确实为整个数组保留了空间?

2 个答案:

答案 0 :(得分:5)

什么都没有'虚假'关于你正在生成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

初始化后,请尝试在du -h文件上调用np.memmap,以查看它使用的实际磁盘空间大小。

当您开始实际将数据写入np.memmap文件时,一切都会正常,直到您超出存储的物理容量,此时进程将以Bus error终止。这意味着如果你需要写< 250GB的数据到你的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。

  4. 更新:是否有安全的'分配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的操作系统。

答案 1 :(得分:-1)

根据@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的{​​{1}},

当使用可管理的大小参数(例如,1 GB)调用脚本时,数组将映射到非稀疏文件。这由--sparse=never命令的输出确认,该命令现在显示大约1 GB。如果内存不足,脚本将退出并显示错误:

du -h