numpy.memmap: фиктивное выделение памяти

У меня есть 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. Тем не менее, он может каким-то образом генерировать большие файлы 10T в /tmpи соответствующий массив все еще кажется доступным. Вывод скрипта следующий:

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 ответа

Решение

Нет ничего "поддельного" в том, что вы генерируете файлы размером 10 ТБ

Вы просите массивы размера

2 ** 37 * 10 = 1374389534720 элементов

Тип D 'i8' означает 8-байтовое (64-битное) целое число, поэтому ваш конечный массив будет иметь размер

1374389534720 * 8 = 10995116277760 байт

или же

10995116277760 / 1E12 = 10.99511627776 ТБ


Если у вас есть только 250 ГБ свободного дискового пространства, как вы можете создать файл "10 ТБ"?

Предполагая, что вы используете достаточно современную файловую систему, ваша ОС будет способна генерировать практически произвольно большие разреженные файлы, независимо от того, достаточно ли у вас физического места на диске для их резервного копирования.

Например, на моей машине с 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, Это означает, что если вам нужно записать < 250 ГБ данных в ваш np.memmap с массивом, то проблем может не быть (на практике это, вероятно, также будет зависеть от того, где вы пишете в массиве, и от того, является ли он основным или столбцом строки).


Как процесс может использовать 10 ТБ виртуальной памяти?

Когда вы создаете карту памяти, ядро ​​выделяет новый блок адресов в виртуальном адресном пространстве вызывающего процесса и отображает их в файл на вашем диске. Поэтому объем виртуальной памяти, который использует ваш процесс Python, будет увеличиваться на размер только что созданного файла. Поскольку файл также может быть разреженным, виртуальная память может не только превышать общий объем доступной оперативной памяти, но также может превышать общий объем физического дискового пространства на вашей машине.


Как вы можете проверить, достаточно ли у вас места на диске для хранения полного np.memmap массив?

Я предполагаю, что вы хотите сделать это программно на Python.

  1. Получите объем свободного места на диске. В ответах на этот предыдущий вопрос SO приведены различные методы. Один вариант 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 на PyPI, который должен работать для любой ОС на основе Posix.

Основываясь на ответе @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 ГБ), массив сопоставляется с не разреженным файлом. Это подтверждается выводом du -h команда, которая теперь показывает размер ~1 ГБ. Если памяти недостаточно, сценарии завершаются с ошибкой:

cp: ‘/tmp/tmps_thxud2’: write failed: No space left on device
Другие вопросы по тегам