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

Размер HDD всего 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)

Поэтому вопрос: как сначала проверить, действительно ли достаточно памяти для данных, а потом действительно зарезервировать место под весь массив?


person Roman    schedule 01.12.2015    source источник


Ответы (2)


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

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

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

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

1374389534720 * 8 = 10995116277760 байт

or

10995116277760 / 1E12 = 10.99511627776 TB


Если у вас есть только 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.

person ali_m    schedule 01.12.2015
comment
Это очень хороший и подробный ответ. Однако, насколько я понял, нет никакого способа сделать это безопасным. Что, если какой-то другой процесс создаст большой файл после того, как я проверил доступное место? Есть ли способ создать обычный файл вместо разреженного и заранее зарезервировать необходимое пространство? - person Roman; 02.12.2015
comment
Хорошее решение, но с одним маленьким недостатком: фактический размер файла memmap несколько больше общего размера элементов массива из-за необходимости хранить dtype и некоторые другие метаданные. Иногда эти накладные расходы также могут привести к Bus error. Было бы неплохо найти способ получить заранее полный размер файла. - person Roman; 02.12.2015
comment
Я не уверен, что вы имеете в виду. Если вы создаете свои массивы с помощью np.memmap, то файлы не должны содержать никакой информации о заголовках — они имеют точно такой же размер, как вы могли бы предсказать на основе размеров массива и размера элемента. Не могли бы вы привести пример? Возможно, есть некоторая путаница с единицами измерения: терабайт в системе СИ составляет 1E12 байт, тогда как двоичный тебибайт 2^40 байт. fallocate может использовать любое соглашение: 10TB означает 1E12 байт, тогда как 10TiB означает 2**40 байт. - person ali_m; 03.12.2015
comment
Да, я ошибся: размер файла точно соответствует размеру данных. Путаница была на самом деле с pickle дампами numpy массивов. - person Roman; 03.12.2015
comment
Ах хорошо. Я бы посоветовал вам избегать использования pickle для массивов numpy - это крайне неэффективный способ хранения числовых данных. Гораздо лучше использовать np.save/np.savez или ndarray.tofile, если вам просто нужен необработанный двоичный файл без заголовков. HDF5 также может иметь смысл для больших наборов данных. - person ali_m; 03.12.2015

Основываясь на ответе @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
person Roman    schedule 02.12.2015
comment
cp --sparse-never будет очень медленнее по сравнению с fallocate для больших разреженных файлов, поскольку для всего файла будет записано ноль байтов. fallocate работает практически мгновенно, так как просто выделяет блоки, не обнуляя их. - person ali_m; 03.12.2015